From 85c93ad7d316e0a4b0d7d5e263ce3e1d8e3c603f Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 23 Jul 2026 13:30:17 +0200 Subject: [PATCH 001/390] [ty] Change `--add-ignore` to add space after the colon for `ty: ignore` (#27120) --- crates/ty/tests/cli/fixes.rs | 2 +- crates/ty_ide/src/code_action.rs | 32 +++++++++---------- ...tests__add_ignore_trailing_whitespace.snap | 2 +- crates/ty_python_semantic/src/fixes.rs | 28 ++++++++-------- .../src/suppression/add_ignore.rs | 4 +-- ...action_attribute_access_on_unimported.snap | 2 +- ...n_existing_import_undefined_decorator.snap | 2 +- ...ode_action_invalid_string_annotations.snap | 2 +- ..._possible_missing_submodule_attribute.snap | 2 +- ...ions__code_action_undefined_decorator.snap | 2 +- ...code_action_undefined_reference_multi.snap | 2 +- ...tion_with_full_diagnostic_output_link.snap | 2 +- 12 files changed, 41 insertions(+), 41 deletions(-) diff --git a/crates/ty/tests/cli/fixes.rs b/crates/ty/tests/cli/fixes.rs index 6f5f3233fd..03cdb6abe0 100644 --- a/crates/ty/tests/cli/fixes.rs +++ b/crates/ty/tests/cli/fixes.rs @@ -138,7 +138,7 @@ fn add_ignore_unfixable() -> anyhow::Result<()> { info[revealed-type]: Revealed type --> different_violations.py:6:13 | - 6 | reveal_type(x) # ty:ignore[undefined-reveal] + 6 | reveal_type(x) # ty: ignore[undefined-reveal] | ^ `Unknown` | diff --git a/crates/ty_ide/src/code_action.rs b/crates/ty_ide/src/code_action.rs index 908994a012..cacbcc7880 100644 --- a/crates/ty_ide/src/code_action.rs +++ b/crates/ty_ide/src/code_action.rs @@ -106,7 +106,7 @@ mod tests { | | - b = a / 10 - 1 + b = a / 10 # ty:ignore[unresolved-reference] + 1 + b = a / 10 # ty: ignore[unresolved-reference] | "); } @@ -124,7 +124,7 @@ mod tests { | | - b = a / 10 # fmt: off - 1 + b = a / 10 # fmt: off # ty:ignore[unresolved-reference] + 1 + b = a / 10 # fmt: off # ty: ignore[unresolved-reference] | "); } @@ -343,7 +343,7 @@ mod tests { | 1 | - b = a / 0 # type:ignore[mypy-code] - 2 + b = a / 0 # type:ignore[mypy-code] # ty:ignore[unresolved-reference] + 2 + b = a / 0 # type:ignore[mypy-code] # ty: ignore[unresolved-reference] | "); } @@ -368,7 +368,7 @@ mod tests { | 3 | - b = a / 0 - 4 + b = a / 0 # ty:ignore[unresolved-reference] + 4 + b = a / 0 # ty: ignore[unresolved-reference] | "); } @@ -437,7 +437,7 @@ mod tests { | 1 | - b = a / 0 # ty:ignore[division-by-zero] some explanation - 2 + b = a / 0 # ty:ignore[division-by-zero] some explanation # ty:ignore[unresolved-reference] + 2 + b = a / 0 # ty:ignore[division-by-zero] some explanation # ty: ignore[unresolved-reference] | "); } @@ -553,7 +553,7 @@ mod tests { | 4 | more text - """ - 5 + """ # ty:ignore[unresolved-reference] + 5 + """ # ty: ignore[unresolved-reference] | "#); } @@ -581,7 +581,7 @@ mod tests { | 3 | { - a - 4 + a # ty:ignore[unresolved-reference] + 4 + a # ty: ignore[unresolved-reference] 5 | } | "); @@ -607,7 +607,7 @@ mod tests { | 3 | more text - """ - 4 + """ # ty:ignore[unresolved-reference] + 4 + """ # ty: ignore[unresolved-reference] | "#); } @@ -631,7 +631,7 @@ mod tests { | 2 | b = a \ - + "test" - 3 + + "test" # ty:ignore[unresolved-reference] + 3 + + "test" # ty: ignore[unresolved-reference] | "#); } @@ -658,7 +658,7 @@ mod tests { | 4 | + ddd \ - - 5 + # ty:ignore[unresolved-reference] + 5 + # ty: ignore[unresolved-reference] 6 | ] # test | "); @@ -694,7 +694,7 @@ mod tests { | 1 | - reveal_type(1) - 2 + reveal_type(1) # ty:ignore[undefined-reveal] + 2 + reveal_type(1) # ty: ignore[undefined-reveal] | "); } @@ -730,7 +730,7 @@ mod tests { | 1 | - @deprecated("do not use") - 2 + @deprecated("do not use") # ty:ignore[unresolved-reference] + 2 + @deprecated("do not use") # ty: ignore[unresolved-reference] 3 | def my_func(): ... | "#); @@ -783,7 +783,7 @@ mod tests { | 3 | - @deprecated("do not use") - 4 + @deprecated("do not use") # ty:ignore[unresolved-reference] + 4 + @deprecated("do not use") # ty: ignore[unresolved-reference] 5 | def my_func(): ... | "#); @@ -820,7 +820,7 @@ mod tests { | 1 | - ExecutionLoader - 2 + ExecutionLoader # ty:ignore[unresolved-reference] + 2 + ExecutionLoader # ty: ignore[unresolved-reference] | "); } @@ -860,7 +860,7 @@ mod tests { | 2 | import importlib - ExecutionLoader - 3 + ExecutionLoader # ty:ignore[unresolved-reference] + 3 + ExecutionLoader # ty: ignore[unresolved-reference] | "); } @@ -910,7 +910,7 @@ mod tests { | 2 | import importlib.abc - ExecutionLoader - 3 + ExecutionLoader # ty:ignore[unresolved-reference] + 3 + ExecutionLoader # ty: ignore[unresolved-reference] | "); } diff --git a/crates/ty_ide/src/snapshots/ty_ide__code_action__tests__add_ignore_trailing_whitespace.snap b/crates/ty_ide/src/snapshots/ty_ide__code_action__tests__add_ignore_trailing_whitespace.snap index 47b7efbbb1..e806867f13 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__code_action__tests__add_ignore_trailing_whitespace.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__code_action__tests__add_ignore_trailing_whitespace.snap @@ -10,5 +10,5 @@ info[code-action]: Ignore 'unresolved-reference' for this line | | - b = a / 10 -1 + b = a / 10 # ty:ignore[unresolved-reference] +1 + b = a / 10 # ty: ignore[unresolved-reference] | diff --git a/crates/ty_python_semantic/src/fixes.rs b/crates/ty_python_semantic/src/fixes.rs index 3c967bddc3..991416bde3 100644 --- a/crates/ty_python_semantic/src/fixes.rs +++ b/crates/ty_python_semantic/src/fixes.rs @@ -447,7 +447,7 @@ struct ApplicableFix { /// Gets fixed to: /// /// ```py - /// enumerate(0, "1") # ty:ignore[invalid-argument-type] + /// enumerate(0, "1") # ty: ignore[invalid-argument-type] /// ``` /// /// In which case `fixed_diagnostics` is 2. @@ -832,7 +832,7 @@ mod tests { ## Fixed source ```py - a = b + 10 # ty:ignore[unresolved-reference] + a = b + 10 # ty: ignore[unresolved-reference] ``` "); } @@ -849,7 +849,7 @@ mod tests { ## Fixed source ```py - a = b + 10 + c # ty:ignore[unresolved-reference] + a = b + 10 + c # ty: ignore[unresolved-reference] ``` "); } @@ -868,7 +868,7 @@ mod tests { ```py import sys - a = b + 10 + sys.veeersion # ty:ignore[unresolved-attribute, unresolved-reference] + a = b + 10 + sys.veeersion # ty: ignore[unresolved-attribute, unresolved-reference] ``` "); } @@ -967,8 +967,8 @@ mod tests { test( a = 10, - c = "unknown" # ty:ignore[unknown-argument] - ) # ty:ignore[missing-argument] + c = "unknown" # ty: ignore[unknown-argument] + ) # ty: ignore[missing-argument] ``` "#); } @@ -1013,8 +1013,8 @@ mod tests { def f() -> None: diag = get_data() - diag["home_assistant"]["entities"] = sorted( # ty:ignore[invalid-assignment] - diag["home_assistant"]["entities"], key=lambda ent: ent["entity_id"] # ty:ignore[invalid-argument-type, not-subscriptable] + diag["home_assistant"]["entities"] = sorted( # ty: ignore[invalid-assignment] + diag["home_assistant"]["entities"], key=lambda ent: ent["entity_id"] # ty: ignore[invalid-argument-type, not-subscriptable] ) ``` "#); @@ -1056,8 +1056,8 @@ mod tests { def f() -> None: diag = get_data() - diag["home_assistant"]["entities"] = sorted( # ty:ignore[invalid-assignment] - diag["home_assistant"]["entities"], key=lambda ent: ent["entity_id"] # ty:ignore[invalid-argument-type, not-subscriptable] + diag["home_assistant"]["entities"] = sorted( # ty: ignore[invalid-assignment] + diag["home_assistant"]["entities"], key=lambda ent: ent["entity_id"] # ty: ignore[invalid-argument-type, not-subscriptable] ); missing # ty: ignore[unresolved-reference] ``` "# @@ -1094,7 +1094,7 @@ class B(A): def test( self, b: str - ) -> A.b: # ty:ignore[invalid-method-override, unresolved-attribute] + ) -> A.b: # ty: ignore[invalid-method-override, unresolved-attribute] pass ``` "#); @@ -1130,7 +1130,7 @@ class B(A): def test( # ty:ignore[unresolved-reference, invalid-method-override] self, b: str - ) -> A.b: # ty:ignore[unresolved-attribute] + ) -> A.b: # ty: ignore[unresolved-attribute] pass ``` @@ -1179,7 +1179,7 @@ class B(A): ## Fixed source ```py - value = missing # ty: ignore[] tracked by [123] # ty:ignore[unresolved-reference] + value = missing # ty: ignore[] tracked by [123] # ty: ignore[unresolved-reference] ``` ## Diagnostics after applying fixes @@ -1187,7 +1187,7 @@ class B(A): warning[unused-ignore-comment]: Unused `ty: ignore` without a code --> test.py:1:18 | - 1 | value = missing # ty: ignore[] tracked by [123] # ty:ignore[unresolved-reference] + 1 | value = missing # ty: ignore[] tracked by [123] # ty: ignore[unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | help: Remove the unused suppression comment diff --git a/crates/ty_python_semantic/src/suppression/add_ignore.rs b/crates/ty_python_semantic/src/suppression/add_ignore.rs index f2b672e57c..94781ed3f6 100644 --- a/crates/ty_python_semantic/src/suppression/add_ignore.rs +++ b/crates/ty_python_semantic/src/suppression/add_ignore.rs @@ -69,7 +69,7 @@ pub fn suppress_all( // // This is important because a suppression inserted at the end of a narrower range // can result in a start-line suppression for a wider range. In the example above, - // inserting a `ty:ignore` after `sorted(` suppresses the diagnostic with the narrower range + // inserting a `ty: ignore` after `sorted(` suppresses the diagnostic with the narrower range // but also the diagnostic with the wider range (because the suppression is on its start line). ids_with_suppression_range.sort_unstable_by_key(|(_, _, range)| (range.start(), range.end())); @@ -242,7 +242,7 @@ fn add_end_of_line_suppression(source: &str, codes: &[LintName], line_end: TextS let trailing_whitespace_len = up_to_line_end.text_len() - up_to_first_content.text_len(); let insertion = format!( - " # ty:ignore[{codes}]", + " # ty: ignore[{codes}]", codes = Codes(SuppressionKind::Ty, codes) ); diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_attribute_access_on_unimported.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_attribute_access_on_unimported.snap index 8baf89d666..18f08b423c 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_attribute_access_on_unimported.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_attribute_access_on_unimported.snap @@ -87,7 +87,7 @@ expression: code_actions "character": 24 } }, - "newText": " # ty:ignore[unresolved-reference]" + "newText": " # ty: ignore[unresolved-reference]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_existing_import_undefined_decorator.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_existing_import_undefined_decorator.snap index 7c41413d40..fa5ac20dc2 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_existing_import_undefined_decorator.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_existing_import_undefined_decorator.snap @@ -132,7 +132,7 @@ expression: code_actions "character": 28 } }, - "newText": " # ty:ignore[unresolved-reference]" + "newText": " # ty: ignore[unresolved-reference]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_invalid_string_annotations.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_invalid_string_annotations.snap index 3f6b711aad..d2acbb63dd 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_invalid_string_annotations.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_invalid_string_annotations.snap @@ -42,7 +42,7 @@ expression: code_actions "character": 12 } }, - "newText": " # ty:ignore[unresolved-reference]" + "newText": " # ty: ignore[unresolved-reference]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_possible_missing_submodule_attribute.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_possible_missing_submodule_attribute.snap index 709c85ce87..7310c18cff 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_possible_missing_submodule_attribute.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_possible_missing_submodule_attribute.snap @@ -42,7 +42,7 @@ expression: code_actions "character": 11 } }, - "newText": " # ty:ignore[possibly-missing-submodule]" + "newText": " # ty: ignore[possibly-missing-submodule]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_decorator.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_decorator.snap index a3fac399ac..f4035122a6 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_decorator.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_decorator.snap @@ -87,7 +87,7 @@ expression: code_actions "character": 28 } }, - "newText": " # ty:ignore[unresolved-reference]" + "newText": " # ty: ignore[unresolved-reference]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_reference_multi.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_reference_multi.snap index 58d613c0dc..0d5c74cb85 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_reference_multi.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_reference_multi.snap @@ -87,7 +87,7 @@ expression: code_actions "character": 17 } }, - "newText": " # ty:ignore[unresolved-reference]" + "newText": " # ty: ignore[unresolved-reference]" } ] } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_with_full_diagnostic_output_link.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_with_full_diagnostic_output_link.snap index e0f3c2ad2d..7378beac5e 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_with_full_diagnostic_output_link.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_with_full_diagnostic_output_link.snap @@ -87,7 +87,7 @@ expression: code_actions "character": 17 } }, - "newText": " # ty:ignore[unresolved-reference]" + "newText": " # ty: ignore[unresolved-reference]" } ] } From d96eab07b4aa9d0e2f2377212dc15d2d9b7a9835 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 23 Jul 2026 14:22:45 +0200 Subject: [PATCH 002/390] Insert a space after the colon in Ruff suppression comments (#27123) --- crates/ruff/src/args.rs | 2 +- crates/ruff/tests/cli/lint.rs | 26 +++---- .../resources/mdtest/ruff/noqa-comments.md | 76 +++++++++---------- crates/ruff_linter/src/noqa.rs | 18 ++--- .../src/rules/ruff/rules/noqa_comments.rs | 22 +++--- .../rule_codes_in_suppression_comments.rs | 6 +- crates/ruff_linter/src/suppression.rs | 56 +++++++------- crates/ruff_server/tests/e2e/diagnostics.rs | 2 +- docs/configuration.md | 2 +- docs/linter.md | 2 +- docs/tutorial.md | 2 +- 11 files changed, 107 insertions(+), 107 deletions(-) diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index 15159230c5..b26ded9b53 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -467,7 +467,7 @@ pub struct CheckCommand { conflicts_with = "diff", )] pub add_noqa: Option, - /// Enable automatic additions of `ruff:ignore` comments to failing lines. + /// Enable automatic additions of `ruff: ignore` comments to failing lines. /// Optionally provide a reason to append after the rule names. /// Requires preview mode. #[arg( diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index a1edec6a7a..e7a428edae 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -2628,9 +2628,9 @@ fn add_ignore() -> Result<()> { test_code, @" - def first_square(): - return [x * x for x in range(20)][0] # ruff:ignore[unnecessary-iterable-allocation-for-first-element] - ", + def first_square(): + return [x * x for x in range(20)][0] # ruff: ignore[unnecessary-iterable-allocation-for-first-element] + ", ); Ok(()) @@ -3744,18 +3744,18 @@ def foo(): ]) .pass_stdin(source), @" - success: true - exit_code: 0 - ----- stdout ----- - # ruff:file-ignore[unused-import] - import os + success: true + exit_code: 0 + ----- stdout ----- + # ruff: file-ignore[unused-import] + import os - def foo(): - value = 1 # ruff:ignore[unused-variable] + def foo(): + value = 1 # ruff: ignore[unused-variable] - ----- stderr ----- - Found 4 errors (4 fixed, 0 remaining). - ", + ----- stderr ----- + Found 4 errors (4 fixed, 0 remaining). + ", ); Ok(()) diff --git a/crates/ruff_linter/resources/mdtest/ruff/noqa-comments.md b/crates/ruff_linter/resources/mdtest/ruff/noqa-comments.md index c84e2a8568..6b9635b336 100644 --- a/crates/ruff_linter/resources/mdtest/ruff/noqa-comments.md +++ b/crates/ruff_linter/resources/mdtest/ruff/noqa-comments.md @@ -17,17 +17,17 @@ import math ``` ```snapshot -error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` +error[RUF105]: `ruff: noqa` comment used instead of `ruff: file-ignore` --> src/mdtest_snippet.py:2:1 | 2 | # ruff: noqa: F401 | ^^^^^^^^^^^^^^^^^^ | -help: Use `ruff:file-ignore` instead +help: Use `ruff: file-ignore` instead | 1 | # snapshot: noqa-comments - # ruff: noqa: F401 -2 + # ruff:file-ignore[F401] +2 + # ruff: file-ignore[F401] 3 | import math | ``` @@ -45,17 +45,17 @@ for os in []: ``` ```snapshot -error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` +error[RUF105]: `ruff: noqa` comment used instead of `ruff: file-ignore` --> src/mdtest_snippet.py:2:1 | 2 | # ruff: noqa: F401, F402, F403 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | -help: Use `ruff:file-ignore` instead +help: Use `ruff: file-ignore` instead | 1 | # snapshot: noqa-comments - # ruff: noqa: F401, F402, F403 -2 + # ruff:file-ignore[F401, F402, F403] +2 + # ruff: file-ignore[F401, F402, F403] 3 | import math | ``` @@ -73,17 +73,17 @@ for os in []: ``` ```snapshot -error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` +error[RUF105]: `ruff: noqa` comment used instead of `ruff: file-ignore` --> src/mdtest_snippet.py:2:1 | 2 | # ruff: noqa: F401, F402, F403 for some reason | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | -help: Use `ruff:file-ignore` instead +help: Use `ruff: file-ignore` instead | 1 | # snapshot: noqa-comments - # ruff: noqa: F401, F402, F403 for some reason -2 + # ruff:file-ignore[F401, F402, F403] for some reason +2 + # ruff: file-ignore[F401, F402, F403] for some reason 3 | import math | ``` @@ -101,17 +101,17 @@ for os in []: ``` ```snapshot -error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` +error[RUF105]: `ruff: noqa` comment used instead of `ruff: file-ignore` --> src/mdtest_snippet.py:2:1 | 2 | # ruff: noqa: F401, F402, F403 # fmt:skip | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | -help: Use `ruff:file-ignore` instead +help: Use `ruff: file-ignore` instead | 1 | # snapshot: noqa-comments - # ruff: noqa: F401, F402, F403 # fmt:skip -2 + # ruff:file-ignore[F401, F402, F403] # fmt:skip +2 + # ruff: file-ignore[F401, F402, F403] # fmt:skip 3 | import math | ``` @@ -134,17 +134,17 @@ import math # noqa: F401, UNK001 ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:3:14 | 3 | import math # noqa: F401, UNK001 | ^^^^^^^^^^^^^^^^^^^^ | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead | 2 | # snapshot: noqa-comments - import math # noqa: F401, UNK001 -3 + import math # ruff:ignore[F401, UNK001] +3 + import math # ruff: ignore[F401, UNK001] | ``` @@ -166,7 +166,7 @@ import math # noqa: EXT001, EXT002 However, if only some of the codes are `external`, a diagnostic is emitted without an autofix. In this case, the external codes likely need to remain in a `noqa` comment, while the codes known by -Ruff could potentially move into a `ruff:ignore` comment. +Ruff could potentially move into a `ruff: ignore` comment. ```py # snapshot: noqa-comments @@ -174,20 +174,20 @@ import math # noqa: F401, EXT001 ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:4:14 | 4 | import math # noqa: F401, EXT001 | ^^^^^^^^^^^^^^^^^^^^ | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead ``` ### Any unmatched code disables the fix This leaves an unused `noqa` comment to be cleaned up by `RUF100` instead, which can be especially important in the case of a standalone `noqa` comment, which has no effect (in almost all cases), but -could become an effectful own-line `ruff:ignore` comment if `RUF105` applied. +could become an effectful own-line `ruff: ignore` comment if `RUF105` applied. ```py # snapshot: noqa-comments @@ -196,13 +196,13 @@ import math ``` ```snapshot -error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` +error[RUF105]: `ruff: noqa` comment used instead of `ruff: file-ignore` --> src/mdtest_snippet.py:2:1 | 2 | # ruff: noqa: F401, F402 | ^^^^^^^^^^^^^^^^^^^^^^^^ | -help: Use `ruff:file-ignore` instead +help: Use `ruff: file-ignore` instead ``` ### Flake8 comments are ignored @@ -222,17 +222,17 @@ import math # noqa: F401 ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:2:14 | 2 | import math # noqa: F401 | ^^^^^^^^^^^^ | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead | 1 | # snapshot: noqa-comments - import math # noqa: F401 -2 + import math # ruff:ignore[F401] +2 + import math # ruff: ignore[F401] | ``` @@ -246,13 +246,13 @@ import os # noqa: F401, F402 ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:2:12 | 2 | import os # noqa: F401, F402 | ^^^^^^^^^^^^^^^^^^ | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead ``` ### Nested pragma comment before the directive @@ -263,17 +263,17 @@ import math # fmt:skip # noqa: F401 ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:2:25 | 2 | import math # fmt:skip # noqa: F401 | ^^^^^^^^^^^^ | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead | 1 | # snapshot: noqa-comments - import math # fmt:skip # noqa: F401 -2 + import math # fmt:skip # ruff:ignore[F401] +2 + import math # fmt:skip # ruff: ignore[F401] | ``` @@ -290,17 +290,17 @@ import math # noqa ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:2:14 | 2 | import math # noqa | ^^^^^^ | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead | 1 | # snapshot: noqa-comments - import math # noqa -2 + import math # ruff:ignore[F401] +2 + import math # ruff: ignore[F401] 3 | # snapshot: noqa-comments | ``` @@ -313,17 +313,17 @@ import foo, bar # noqa ``` ```snapshot -error[RUF105]: `noqa` comment used instead of `ruff:ignore` +error[RUF105]: `noqa` comment used instead of `ruff: ignore` --> src/mdtest_snippet.py:4:18 | 4 | import foo, bar # noqa | ^^^^^^ | -help: Use `ruff:ignore` instead +help: Use `ruff: ignore` instead | 3 | # snapshot: noqa-comments - import foo, bar # noqa -4 + import foo, bar # ruff:ignore[F401] +4 + import foo, bar # ruff: ignore[F401] | ``` @@ -338,13 +338,13 @@ import math ``` ```snapshot -error[RUF105]: `ruff: noqa` comment used instead of `ruff:file-ignore` +error[RUF105]: `ruff: noqa` comment used instead of `ruff: file-ignore` --> src/mdtest_snippet.py:2:1 | 2 | # ruff: noqa | ^^^^^^^^^^^^ | -help: Use `ruff:file-ignore` instead +help: Use `ruff: file-ignore` instead ``` ## Inline self-suppression @@ -368,7 +368,7 @@ But a suppression for `RUF100` should not prevent the rule from firing: import math # noqa: RUF100, F401 ``` -## Suppression with `ruff:ignore` +## Suppression with `ruff: ignore` ```toml [lint] diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index f7fe29b14b..741f384c88 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -763,7 +763,7 @@ impl Error for LexicalError {} pub enum SuppressionKind { /// A `noqa` comment Noqa, - /// A `ruff:ignore` comment + /// A `ruff: ignore` comment Ignore, } @@ -1060,7 +1060,7 @@ impl SuppressionEdit<'_> { } match self.suppression_kind { SuppressionKind::Noqa => write!(writer, "# noqa: ").unwrap(), - SuppressionKind::Ignore => write!(writer, "# ruff:ignore[").unwrap(), + SuppressionKind::Ignore => write!(writer, "# ruff: ignore[").unwrap(), } push_codes( writer, @@ -1107,7 +1107,7 @@ fn generate_suppression_edit<'a>( (edit_range, blank_line) = suppression_edit_range(locator, line_range, codes.start()); existing_codes.extend(codes.iter().map(Code::as_str)); } - // Add additional rule names to an existing `ruff:ignore` comment. + // Add additional rule names to an existing `ruff: ignore` comment. (Some(ExistingDirective::Ignore(comment)), SuppressionKind::Ignore) => { (edit_range, blank_line) = suppression_edit_range(locator, line_range, comment.start()); existing_codes.extend(comment.codes_as_str(locator.contents())); @@ -3132,7 +3132,7 @@ mod tests { ## Fixed source ```py - def unused(x): # noqa: ANN001, ARG001, D103 # ruff:ignore[missing-return-type-undocumented-public-function] + def unused(x): # noqa: ANN001, ARG001, D103 # ruff: ignore[missing-return-type-undocumented-public-function] pass ``` " @@ -3182,7 +3182,7 @@ mod tests { ## Fixed source ```py - import math # noqa: F401 # ruff:ignore[noqa-comments] + import math # noqa: F401 # ruff: ignore[noqa-comments] ``` " @@ -3213,7 +3213,7 @@ mod tests { ## Fixed source ```py - def unused(x): # ruff:ignore[ANN001, ARG001, D103, missing-return-type-undocumented-public-function] + def unused(x): # ruff: ignore[ANN001, ARG001, D103, missing-return-type-undocumented-public-function] pass ``` " @@ -3243,7 +3243,7 @@ mod tests { ## Fixed source ```py - def unused(x): # ruff:ignore[missing-return-type-undocumented-public-function, missing-type-function-argument, undocumented-public-function] + def unused(x): # ruff: ignore[missing-return-type-undocumented-public-function, missing-type-function-argument, undocumented-public-function] pass ``` " @@ -3269,7 +3269,7 @@ mod tests { ## Fixed source ```py - import z # ruff:ignore[unsorted-imports] + import z # ruff: ignore[unsorted-imports] import c import a ``` @@ -3301,7 +3301,7 @@ mod tests { ## Fixed source ```py - # ruff:ignore[ANN001, missing-return-type-undocumented-public-function] + # ruff: ignore[ANN001, missing-return-type-undocumented-public-function] def public(x): """Return x.""" return x diff --git a/crates/ruff_linter/src/rules/ruff/rules/noqa_comments.rs b/crates/ruff_linter/src/rules/ruff/rules/noqa_comments.rs index d01abd0e6d..47c210a160 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/noqa_comments.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/noqa_comments.rs @@ -11,11 +11,11 @@ use crate::{ /// ## What it does /// -/// Checks for the use of `noqa` comments instead of Ruff-specific `ruff:ignore` comments. +/// Checks for the use of `noqa` comments instead of Ruff-specific `ruff: ignore` comments. /// /// ## Why is this bad? /// -/// `ruff:ignore` comments allow the use of rule names instead of codes and can be used in more +/// `ruff: ignore` comments allow the use of rule names instead of codes and can be used in more /// places than `noqa` comments. /// /// Note that this is an opinionated, stylistic rule. `noqa` comments may be needed for backwards @@ -30,13 +30,13 @@ use crate::{ /// /// Use instead: /// ```python -/// import os # ruff:ignore[F401] +/// import os # ruff: ignore[F401] /// ``` /// /// Or if you prefer the own-line form: /// /// ```python -/// # ruff:ignore[unused-import] +/// # ruff: ignore[unused-import] /// import os /// ``` /// @@ -56,7 +56,7 @@ use crate::{ /// /// This rule avoids offering a fix if any of the rule codes in a `noqa` comment are unused. See /// `unused-noqa` for a rule that will remove these and allow the remaining codes to be moved into a -/// `ruff:ignore` comment. +/// `ruff: ignore` comment. #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "0.15.22")] pub(crate) struct NoqaComments { @@ -69,17 +69,17 @@ impl Violation for NoqaComments { #[derive_message_formats] fn message(&self) -> String { if !self.file_level { - "`noqa` comment used instead of `ruff:ignore`".to_string() + "`noqa` comment used instead of `ruff: ignore`".to_string() } else { - "`ruff: noqa` comment used instead of `ruff:file-ignore`".to_string() + "`ruff: noqa` comment used instead of `ruff: file-ignore`".to_string() } } fn fix_title(&self) -> Option { Some(if self.file_level { - "Use `ruff:file-ignore` instead".to_string() + "Use `ruff: file-ignore` instead".to_string() } else { - "Use `ruff:ignore` instead".to_string() + "Use `ruff: ignore` instead".to_string() }) } } @@ -143,14 +143,14 @@ pub(crate) fn noqa_comments( // import math // ``` // - // by converting it to a valid `ruff:ignore` comment. + // by converting it to a valid `ruff: ignore` comment. if has_unused_codes { return; } let edit = Edit::range_replacement( format!( - "# ruff:{action}[{codes}]", + "# ruff: {action}[{codes}]", action = if file_level { "file-ignore" } else { "ignore" }, ), codes.range, diff --git a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_suppression_comments.rs b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_suppression_comments.rs index 53d42e3cc5..3c386f6ab6 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_suppression_comments.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/rule_codes_in_suppression_comments.rs @@ -11,18 +11,18 @@ use crate::AlwaysFixableViolation; /// Human-readable rule names are easier to understand than rule codes. Using names also avoids /// requiring readers to look up the meaning of each code. /// -/// This rule applies to `ruff:ignore`, `ruff:file-ignore`, `ruff:disable`, and `ruff:enable` +/// This rule applies to `ruff: ignore`, `ruff: file-ignore`, `ruff: disable`, and `ruff: enable` /// comments. /// /// ## Example /// /// ```python -/// import os # ruff:ignore[F401] +/// import os # ruff: ignore[F401] /// ``` /// /// Use instead: /// ```python -/// import os # ruff:ignore[unused-import] +/// import os # ruff: ignore[unused-import] /// ``` #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "0.15.22")] diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index 01fe547eb9..fb04ee5b50 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -32,13 +32,13 @@ use crate::{Locator, Violation, warn_user_once}; #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] enum SuppressionAction { - /// # ruff:file-ignore[...] file level suppression + /// # ruff: file-ignore[...] file level suppression FileIgnore, - /// # ruff:disable[...] start of a block suppression + /// # ruff: disable[...] start of a block suppression Disable, - /// # ruff:enable[...] end of a block suppression + /// # ruff: enable[...] end of a block suppression Enable, - /// # ruff:ignore[...] ignore a single line or multi-line statement + /// # ruff: ignore[...] ignore a single line or multi-line statement Ignore, } @@ -49,8 +49,8 @@ pub(crate) struct SuppressionComment { /// For example: /// /// ```py - /// import math # start # ruff:ignore[F401] reason # end - /// ^^^^^^^^^^^^^^^^^^^^^^^^^^ + /// import math # start # ruff: ignore[F401] reason # end + /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ /// ``` range: TextRange, @@ -59,8 +59,8 @@ pub(crate) struct SuppressionComment { /// For example: /// /// ```py - /// import math # start # ruff:ignore[F401] reason # end - /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + /// import math # start # ruff: ignore[F401] reason # end + /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ /// ``` token_range: TextRange, @@ -136,7 +136,7 @@ impl Suppression { &self.comments.first().codes } - /// Returns whether or not the suppression is a standalone `ruff:ignore` comment. + /// Returns whether or not the suppression is a standalone `ruff: ignore` comment. fn is_ignore(&self) -> bool { matches!( self.comments, @@ -149,7 +149,7 @@ impl Suppression { /// Returns whether the suppression's range applies to a diagnostic. /// - /// `ruff:ignore` comments only need to contain the start of the diagnostic range (or its + /// `ruff: ignore` comments only need to contain the start of the diagnostic range (or its /// parent), while range suppression comments must contain the entire diagnostic range. fn applies_to_diagnostic(&self, range: TextRange, parent: Option) -> bool { if self.is_ignore() { @@ -177,9 +177,9 @@ impl Suppression { #[derive(Debug)] pub(crate) enum SuppressionComments { - /// A #ruff:ignore comment, or #ruff:disable without a matching #ruff:enable + /// A # ruff: ignore comment, or # ruff: disable without a matching # ruff: enable Single(SuppressionComment), - /// A matching pair of #ruff:disable and #ruff:enable comments. + /// A matching pair of # ruff: disable and # ruff: enable comments. DisableEnable(SuppressionComment, SuppressionComment), } @@ -309,28 +309,28 @@ impl Suppressions { /// the subscript expression: /// /// ```py - /// # ruff:disable[RUF015] + /// # ruff: disable[RUF015] /// value = [ /// *range(10) /// ][0] - /// # ruff:enable[RUF015] + /// # ruff: enable[RUF015] /// ``` /// /// is suppressed, but /// /// ```py - /// # ruff:disable[RUF015] + /// # ruff: disable[RUF015] /// value = [ - /// # ruff:enable[RUF015] + /// # ruff: enable[RUF015] /// *range(10) /// ][0] /// ``` /// - /// is not. For `ruff:ignore`, this rule is augmented to check whether the diagnostic's start + /// is not. For `ruff: ignore`, this rule is augmented to check whether the diagnostic's start /// offset is contained instead, meaning that this _will_ be suppressed: /// /// ```python - /// suppressed = [ # ruff:ignore[RUF015] + /// suppressed = [ # ruff: ignore[RUF015] /// *range(10) /// ][0] /// ``` @@ -861,7 +861,7 @@ impl<'a> SuppressionsBuilder<'a> { } } - /// Handles a single-comment suppression like `ruff:ignore` or `ruff:file-ignore` and returns + /// Handles a single-comment suppression like `ruff: ignore` or `ruff: file-ignore` and returns /// `true` if such a comment was found. fn register_standalone_suppression( &mut self, @@ -1021,7 +1021,7 @@ impl<'a> SuppressionsBuilder<'a> { /// ```py /// /// # V--- from here - /// # ruff:ignore[code] + /// # ruff: ignore[code] /// foo = [ /// 1, /// 2, @@ -1029,7 +1029,7 @@ impl<'a> SuppressionsBuilder<'a> { /// # ^--- to here /// /// # V--- from here - /// # ruff:ignore[code] + /// # ruff: ignore[code] /// def foo( /// arg1, /// arg2, @@ -1047,7 +1047,7 @@ impl<'a> SuppressionsBuilder<'a> { /// /// foo = [ /// # V--- from here - /// # ruff:ignore[code] + /// # ruff: ignore[code] /// 1, /// # ^--- to here /// 2, @@ -1112,13 +1112,13 @@ impl<'a> SuppressionsBuilder<'a> { /// /// ```py /// # V-- from here - /// foo = 1 # ruff:ignore[code] - /// # to here -----------------^ + /// foo = 1 # ruff: ignore[code] + /// # to here ------------------^ /// /// foo = [ /// # V--- from here - /// 1, # ruff:ignore[code] - /// # to here ------------^ + /// 1, # ruff: ignore[code] + /// # to here -------------^ /// ] /// ``` /// @@ -1130,8 +1130,8 @@ impl<'a> SuppressionsBuilder<'a> { /// # V--- from here /// value = """ /// some text - /// """ # ruff:ignore[code] - /// # to here -------------^ + /// """ # ruff: ignore[code] + /// # to here --------------^ /// /// ``` /// diff --git a/crates/ruff_server/tests/e2e/diagnostics.rs b/crates/ruff_server/tests/e2e/diagnostics.rs index 5e86a01a7c..4fb75272c8 100644 --- a/crates/ruff_server/tests/e2e/diagnostics.rs +++ b/crates/ruff_server/tests/e2e/diagnostics.rs @@ -58,7 +58,7 @@ fn uses_human_readable_names_in_preview() -> Result<()> { } ], "noqa_edit": { - "newText": " # ruff:ignore[unused-import]\n", + "newText": " # ruff: ignore[unused-import]\n", "range": { "end": { "character": 0, diff --git a/docs/configuration.md b/docs/configuration.md index 64b5af9e02..2af6928d1c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -648,7 +648,7 @@ Options: Enable automatic additions of `noqa` directives to failing lines. Optionally provide a reason to append after the codes --add-ignore[=] - Enable automatic additions of `ruff:ignore` comments to failing + Enable automatic additions of `ruff: ignore` comments to failing lines. Optionally provide a reason to append after the rule names. Requires preview mode --show-files diff --git a/docs/linter.md b/docs/linter.md index 22ce06c4b6..e4b1f73045 100644 --- a/docs/linter.md +++ b/docs/linter.md @@ -541,7 +541,7 @@ lines, run Ruff with `--add-noqa`: $ ruff check /path/to/file.py --add-noqa ``` -The `--add-noqa` flag adds `noqa` directives with rule codes. To add `ruff:ignore` comments with +The `--add-noqa` flag adds `noqa` directives with rule codes. To add `ruff: ignore` comments with human-readable rule names instead, use `--add-ignore` with preview mode enabled. ### isort action comments diff --git a/docs/tutorial.md b/docs/tutorial.md index 450dbc81df..d57ee6ca5d 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -361,7 +361,7 @@ index 71fca60c8d..e92d839f1b 100644 +from typing import Iterable # noqa: UP035 ``` -To add `# ruff:ignore[...]` comments with human-readable rule names instead, use the +To add `# ruff: ignore[...]` comments with human-readable rule names instead, use the `--add-ignore` flag with preview mode enabled. ## Integrations From c32e6651038505efd7e746fd929a74c718e08472 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 23 Jul 2026 13:48:56 +0100 Subject: [PATCH 003/390] Revisions to AGENTS.md (#27122) --- AGENTS.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4251a9bacf..d41e7f7dae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ This repository contains both Ruff (a Python linter and formatter) and ty (a Python type checker). The crates follow a naming convention: `ruff_*` for Ruff-specific code and `ty_*` for ty-specific code. ty reuses several Ruff crates, including the Python parser (`ruff_python_parser`) and AST definitions (`ruff_python_ast`). -## Code reviews +## Code Review Rules When reviewing a branch or pull request, be deliberately nitpicky. Report not only bugs and regressions, but also architectural and maintenance risks, weak @@ -136,18 +136,18 @@ Parts of `.github/workflows/release.yml` are generated by cargo-dist from `dist- ## Development Guidelines -- All changes must be tested. If you're not testing your changes, you're not done. +- All significant changes must be tested. Add or update focused tests for semantic changes when existing coverage does not already establish the intended behavior. - Look to see if your tests could go in an existing file before adding a new file for your tests. - Get your tests to pass. If you didn't run the tests, your code does not work. - Follow existing code style. Check neighboring files for patterns. - Prefer narrow visibility by default because this workspace is generally its own consumer. However, do not add workarounds solely to avoid `pub`: make an item public when another workspace crate needs it and that produces the cleaner implementation. - Rust imports should always go at the top of the file, never locally in functions. - Run `uv run --only-group dev --locked prek` at the end of a task if you changed files in the repo. This includes changes such as rebases or addressing review comments. Use `uv run --only-group dev --locked prek run --files ` and pass every file you changed. This keeps the hook run independent of staged state and avoids sweeping unrelated changes. Use `uv run --only-group dev --locked prek run --all-files` when a full-repository hook sweep is specifically needed. -- Avoid writing significant amounts of new code. This is often a sign that we're missing an existing method or mechanism that could help solve the problem. Look for existing utilities first. -- Try hard to avoid patterns that require `panic!`, `unreachable!`, or `.unwrap()`. Instead, try to encode those constraints in the type system. Don't be afraid to write code that's more verbose or requires largeish refactors if it enables you to avoid these unsafe calls. -- Prefer let chains (`if let` combined with `&&`) over nested `if let` statements to reduce indentation and improve readability. At the end of a task, always check your work to see if you missed opportunities to use `let` chains. +- Before writing significant amounts of new code, look for existing utilities or mechanisms that could solve the problem. Avoid expanding the task to unrelated issues, but do not confuse keeping the task focused with minimizing the size of the implementation. Prefer addressing the underlying architectural problem over adding a localized workaround, even when doing so requires a substantial refactor or rearchitecture. Ask the user for guidance if in doubt about whether to attempt a larger refactor or not. +- Try hard to avoid patterns that require `panic!`, `unreachable!`, `.unwrap()` or `.expect()`. Instead, try to encode those constraints in the type system. Don't be afraid to write code that's more verbose or requires largeish refactors if it enables you to avoid these unsafe calls. +- Prefer let chains (`if let` combined with `&&`) and let guards (`PAT if let ... =>`) over nested `if let` statements to reduce indentation and improve readability. At the end of a task, always check your work to see if you missed opportunities to use `let` chains or `let` guards. - If you *have* to suppress a Clippy lint, prefer to use `#[expect()]` over `[allow()]`, where possible. But if a lint is complaining about unused/dead code, it's usually best to just delete the unused code. -- Use comments purposefully. Don't use comments to narrate code, but do use them to explain invariants and why something unusual was done a particular way. +- Don't use comments to narrate code, but do use them to explain invariants and why something unusual was done a particular way. Make sure that a comment will make sense to somebody who's reading the code for the first time. Prefer plain language, avoid jargon, and don't be afraid to be more verbose if it's necessary to explain something well. Giving examples of the kind of Python code we're trying to model at this particular point in Ruff or ty can often be very helpful for future readers of the code. - Run `cargo dev generate-all` after changing configuration options, CLI arguments, lint rules, or environment variable definitions, as these changes require regeneration of schemas, docs, and CLI references. - Don't prefix tests with `test_`. - Don't separate struct definitions from their `impl` blocks unless the `impl` is deliberately placed in a separate file, as for large structs. From 9b64be4e407d1979704812e4fb949df1b7b58b43 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 23 Jul 2026 15:15:20 +0200 Subject: [PATCH 004/390] [ty] Simplify script metadata query (#27121) --- Cargo.lock | 1 - crates/ruff_python_ast/src/script.rs | 22 ++++------ crates/ty_project/Cargo.toml | 1 - crates/ty_project/src/metadata/script.rs | 35 +--------------- .../resources/mdtest/scripts.md | 41 +++++++++++++++++++ 5 files changed, 50 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c12baa867..1147a4c8cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4784,7 +4784,6 @@ dependencies = [ "get-size2", "globset", "insta", - "memchr", "notify", "ordermap", "parking_lot", diff --git a/crates/ruff_python_ast/src/script.rs b/crates/ruff_python_ast/src/script.rs index 9180b3ccdb..00c0fc7c30 100644 --- a/crates/ruff_python_ast/src/script.rs +++ b/crates/ruff_python_ast/src/script.rs @@ -54,30 +54,22 @@ impl ScriptTag { /// /// See: pub fn parse(contents: &[u8]) -> Option { - // Identify the opening pragma. - let index = FINDER.find(contents)?; - - Self::parse_at(contents, index) + FINDER + .find_iter(contents) + .find_map(|index| Self::parse_at(contents, index)) } - /// Extracts a `script` metadata block known to start at `index`. - /// - /// Returns `None` if `index` does not point to an exact opening pragma at the start of a line. - pub fn parse_at(contents: &[u8], index: usize) -> Option { - let (prelude, contents) = contents.split_at_checked(index)?; - + fn parse_at(contents: &[u8], index: usize) -> Option { // The opening pragma must be the first line, or immediately preceded by a newline. - if prelude - .last() - .is_some_and(|byte| !matches!(*byte, b'\r' | b'\n')) - { + if !(index == 0 || matches!(contents[index - 1], b'\r' | b'\n')) { return None; } // Extract the preceding content. - let prelude = std::str::from_utf8(prelude).ok()?; + let prelude = std::str::from_utf8(&contents[..index]).ok()?; // Decode as UTF-8. + let contents = &contents[index..]; let contents = std::str::from_utf8(contents).ok()?; let mut lines = contents.lines(); diff --git a/crates/ty_project/Cargo.toml b/crates/ty_project/Cargo.toml index 9fd69f82d6..de0e541de9 100644 --- a/crates/ty_project/Cargo.toml +++ b/crates/ty_project/Cargo.toml @@ -40,7 +40,6 @@ compact_str = { workspace = true, features = ["serde"] } crossbeam = { workspace = true } get-size2 = { workspace = true, features = ["ordermap", "parking_lot"] } globset = { workspace = true } -memchr = { workspace = true } notify = { workspace = true } ordermap = { workspace = true, features = ["serde"] } parking_lot = { workspace = true } diff --git a/crates/ty_project/src/metadata/script.rs b/crates/ty_project/src/metadata/script.rs index 2ec036a57e..1800ddc9b5 100644 --- a/crates/ty_project/src/metadata/script.rs +++ b/crates/ty_project/src/metadata/script.rs @@ -1,27 +1,15 @@ -use std::sync::{Arc, LazyLock}; +use std::sync::Arc; -use memchr::memmem::Finder; use ruff_db::Db; use ruff_db::files::File; -use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_db::system::SystemPathBuf; use ruff_python_ast::script::ScriptTag; -use ruff_python_ast::token::TokenKind; use ruff_ranged_value::ValueSource; -use ruff_text_size::Ranged; use crate::metadata::pyproject::PyProject; -const SCRIPT_TAG: &str = "# /// script"; -static SCRIPT_TAG_FINDER: LazyLock> = - LazyLock::new(|| Finder::new(SCRIPT_TAG.as_bytes())); - /// Returns the PEP 723 metadata embedded in `file`. -/// -/// The byte search keeps the overwhelmingly common non-script path cheap. Parsing is only -/// necessary after finding a possible opening tag at the start of a line, where the token stream -/// disambiguates an actual comment from the same text inside a string literal. #[salsa::tracked(returns(ref))] pub(crate) fn script_metadata(db: &dyn Db, file: File) -> Option> { let path = file.path(db); @@ -34,26 +22,7 @@ pub(crate) fn script_metadata(db: &dyn Db, file: File) -> Option> return None; } - let source_bytes = source.as_bytes(); - let mut candidates = SCRIPT_TAG_FINDER - .find_iter(source_bytes) - .filter(|&offset| offset == 0 || matches!(source_bytes[offset - 1], b'\r' | b'\n')); - let first_candidate = candidates.next()?; - - let parsed = parsed_module(db, file).load(db); - let tokens = parsed.tokens(); - let tag = std::iter::once(first_candidate) - .chain(candidates) - .filter(|&offset| { - let Ok(index) = tokens.binary_search_by_key(&offset, |token| token.start().to_usize()) - else { - return false; - }; - let token = &tokens[index]; - - token.kind() == TokenKind::Comment && &source[token.range()] == SCRIPT_TAG - }) - .find_map(|opening| ScriptTag::parse_at(source_bytes, opening))?; + let tag = ScriptTag::parse(source.as_bytes())?; let value_source = ValueSource::File(Arc::new(SystemPathBuf::from(path.as_str()))); PyProject::from_toml_str_without_spans(tag.metadata(), value_source) diff --git a/crates/ty_python_semantic/resources/mdtest/scripts.md b/crates/ty_python_semantic/resources/mdtest/scripts.md index 66f9ade3d1..abffa906eb 100644 --- a/crates/ty_python_semantic/resources/mdtest/scripts.md +++ b/crates/ty_python_semantic/resources/mdtest/scripts.md @@ -126,3 +126,44 @@ print(missing) # error: [unresolved-reference] print(missing) ``` + +# Valid blocks after invalid opening tags + +Invalid opening tags do not prevent a later valid metadata block from being recognized. + +```py +value = 1 # /// script +# [tool.ty.rules] +# unresolved-reference = "error" +# /// + +# /// script invalid +# [tool.ty.rules] +# unresolved-reference = "error" +# /// + +# /// script +# [tool.ty.rules] +# unresolved-reference = "ignore" +# /// + +print(missing) +``` + +# Valid blocks after unclosed blocks + +An earlier unclosed block does not prevent a later valid metadata block from being recognized. + +```py +# /// script +# [tool.ty.rules] +# unresolved-reference = "error" +value = 1 + +# /// script +# [tool.ty.rules] +# unresolved-reference = "ignore" +# /// + +print(missing) +``` From 0d605c1b9bbb644c0bf758bcfd84aa944f049c88 Mon Sep 17 00:00:00 2001 From: zaniebot Date: Thu, 23 Jul 2026 09:38:48 -0500 Subject: [PATCH 005/390] Use Namespace runners for Windows (#27101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This continues expanding use of Namespace runners as we assess their viability for our projects. They appear to provide significant performance benefits over the Depot runners at the same size. It's worth noting they do not use a warm pool and the queue times consequently include machine boot and are expected to be higher than Depot on average. Follows #27097 and https://github.com/astral-sh/uv/pull/19751 -------- Move Ruff's Windows test and binary-build jobs to the `namespace-profile-windows-2022-x86-64-16x32` runner already used by uv. At the configured runner sizes, five sequential warm Namespace samples were compared with 20 recent Depot Windows test runs and the ten most recent production releases on `windows-latest`. | Test metric | Depot mean, n=20 (95% CI) | Namespace mean, n=5 (95% CI) | Difference (95% CI) | | --- | ---: | ---: | ---: | | Whole test job | 5:36 (5:18–5:55) | 3:26 (2:49–4:03) | −2:10 / −38.8% (−2:47 to −1:34) | | Compile and test | 2:54 (2:43–3:06) | 2:43 (2:20–3:06) | −11s / −6.5% (−35s to +12s) | | Test metric | Depot median | Namespace median | Difference | | --- | ---: | ---: | ---: | | Whole test job | 5:20 | 3:11 | −2:09 / −40.3% | | Compile and test | 2:41 | 2:33 | −8s / −5.0% | | Release job | GitHub mean, n=10 (95% CI) | Namespace mean, n=5 (95% CI) | Difference (95% CI) | | --- | ---: | ---: | ---: | | x86_64 release | 12:37 (11:46–13:29) | 5:14 (4:14–6:14) | −7:23 / −58.5% (−8:32 to −6:14) | | i686 release | 13:21 (12:37–14:05) | 4:59 (4:28–5:30) | −8:22 / −62.7% (−9:10 to −7:33) | | aarch64 release | 12:49 (12:04–13:33) | 5:25 (4:31–6:20) | −7:24 / −57.7% (−8:25 to −6:22) | | Release job | GitHub median | Namespace median | Difference | | --- | ---: | ---: | ---: | | x86_64 release | 12:47 | 5:11 | −7:36 / −59.5% | | i686 release | 13:16 | 4:55 | −8:21 / −62.9% | | aarch64 release | 12:39 | 5:35 | −7:04 / −55.8% | Runner queue time is separate from execution: | Job | Baseline median (range) | Namespace median (range) | | --- | ---: | ---: | | Windows tests | 0:02 (0:01–0:03), n=20 | 0:28 (0:05–0:43), n=5 | | x86_64 release | 0:04 (0:03–0:56), n=9 | 0:45 (0:18–1:04), n=5 | | i686 release | 0:04 (0:03–0:40), n=9 | 0:31 (0:19–0:48), n=5 | | aarch64 release | 0:05 (0:03–1:06), n=9 | 0:20 (0:20–0:46), n=5 | The Namespace profile provides 16 vCPU and 32 GB RAM, matching Depot. --------- Co-authored-by: Zanie Blue Co-authored-by: zaniebot --- .github/actionlint.yaml | 2 +- .github/workflows/build-binaries.yml | 2 +- .github/workflows/ci.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 2528699099..1fd2d87add 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -9,7 +9,7 @@ self-hosted-runner: - depot-ubuntu-22.04-16 - depot-ubuntu-22.04-32 - namespace-profile-macos-15 - - depot-windows-2022-16 + - namespace-profile-windows-2022-x86-64-16x32 - depot-ubuntu-22.04-arm-4 - github-windows-2025-x86_64-8 - github-windows-2025-x86_64-16 diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index c0c66cd9a2..5bed4e7d45 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -166,7 +166,7 @@ jobs: windows: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: windows-latest + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-windows-2022-x86-64-16x32' || 'windows-latest' }} strategy: matrix: platform: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dac406d455..a03c2fe203 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -422,7 +422,7 @@ jobs: strategy: matrix: platform: - - ${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }} + - ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-windows-2022-x86-64-16x32' || 'windows-latest' }} - ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-macos-15' || 'macos-latest' }} name: "cargo test (${{ matrix.platform }})" runs-on: ${{ matrix.platform }} From f774523168e62f3776bd78d1e9c5af35cb9e45e9 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 23 Jul 2026 07:58:45 -0700 Subject: [PATCH 006/390] [ty] Support generic manual PEP 695 type aliases (#27083) ## Summary Add specialization support for generic aliases created with `TypeAliasType`, including defaults, reordered parameters, `ParamSpec`, `TypeVarTuple`, nested aliases, constructor inference, and recursive aliases. Validate malformed `type_params` declarations. Closes https://github.com/astral-sh/ty/issues/1737. ## Test plan Added and updated mdtests covering manual-alias specialization in annotations and value positions, defaults and bounds, nested and recursive aliases, `Callable`/`ParamSpec` and variadic aliases, constructor inference, invalid and scoped type parameters, duplicate/default-order/variadic validation, non-generic and double specialization errors, and preservation of native PEP 695 aliases in `Callable` types. Ecosystem impact is large because scipy-stubs makes extensive use of generic `TypeAliasType`, and several other ecosystem projects depend on it. Ecosystem changes look correct/expected for newly understanding these aliases; some reveal existing limitations of PEP 695 type aliases that should be fixed separately. --- crates/ruff_benchmark/benches/ty_walltime.rs | 4 +- crates/ty/docs/rules.md | 7 +- .../lint_docs/invalid-type-alias-type.md | 7 +- .../resources/mdtest/pep613_type_aliases.md | 9 +- .../resources/mdtest/pep695_type_aliases.md | 286 +++++++++++++++++- .../ty_python_semantic/src/types/call/bind.rs | 8 +- .../src/types/infer/builder.rs | 169 ++++++++++- .../src/types/infer/builder/subscript.rs | 13 - .../types/infer/builder/type_expression.rs | 25 +- .../src/types/known_instance.rs | 2 +- .../ty_python_semantic/src/types/subscript.rs | 11 +- .../src/types/type_alias.rs | 161 +++++++--- ty.schema.json | 2 +- 13 files changed, 589 insertions(+), 115 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index 6d54fbb17b..a6eee29bca 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -110,7 +110,7 @@ static ALTAIR: Benchmark = Benchmark::new( max_dep_date: TY_ECOSYSTEM_PIN, python_version: SupportedPythonVersion::Py311, }, - 3, + 5, ); static COLOUR_SCIENCE: Benchmark = Benchmark::new( @@ -227,7 +227,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new( max_dep_date: TY_ECOSYSTEM_PIN, python_version: SupportedPythonVersion::Py311, }, - 1950, + 2000, ); #[track_caller] diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 4c929c659d..f3f9f9d22f 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -2814,7 +2814,7 @@ python-version = "3.12" ``` ```python -from typing import TypeAliasType +from typing import TypeAliasType, TypeVar def get_name() -> str: @@ -2824,6 +2824,11 @@ def get_name() -> str: IntOrStr = TypeAliasType("IntOrStr", int | str) # okay # TypeAliasType name must be a string literal NewAlias = TypeAliasType(get_name(), int) # error + +T = TypeVar("T") +GenericAlias = TypeAliasType("GenericAlias", list[T], type_params=(T,)) # okay +# TypeAliasType type parameters must be type variables +InvalidAlias = TypeAliasType("InvalidAlias", list[T], type_params=(list[T],)) # error ``` ## `invalid-type-arguments` diff --git a/crates/ty_python_semantic/resources/lint_docs/invalid-type-alias-type.md b/crates/ty_python_semantic/resources/lint_docs/invalid-type-alias-type.md index 005be90ce8..57d44faebf 100644 --- a/crates/ty_python_semantic/resources/lint_docs/invalid-type-alias-type.md +++ b/crates/ty_python_semantic/resources/lint_docs/invalid-type-alias-type.md @@ -14,7 +14,7 @@ python-version = "3.12" ``` ```python -from typing import TypeAliasType +from typing import TypeAliasType, TypeVar def get_name() -> str: @@ -24,4 +24,9 @@ def get_name() -> str: IntOrStr = TypeAliasType("IntOrStr", int | str) # okay # TypeAliasType name must be a string literal NewAlias = TypeAliasType(get_name(), int) # error + +T = TypeVar("T") +GenericAlias = TypeAliasType("GenericAlias", list[T], type_params=(T,)) # okay +# TypeAliasType type parameters must be type variables +InvalidAlias = TypeAliasType("InvalidAlias", list[T], type_params=(list[T],)) # error ``` diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index edfee69d75..acb2b5726d 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -271,8 +271,7 @@ MyList = TypeAliasType("MyList", list[T], type_params=(T,)) MyAlias5 = Callable[[MyList[T]], int] def _(c: MyAlias5[int]): - # TODO: should be (list[int], /) -> int - reveal_type(c) # revealed: (Unknown, /) -> int + reveal_type(c) # revealed: (MyList[int], /) -> int K = TypeVar("K") V = TypeVar("V") @@ -282,14 +281,12 @@ MyDict = TypeAliasType("MyDict", dict[K, V], type_params=(K, V)) MyAlias6 = Callable[[MyDict[K, V]], int] def _(c: MyAlias6[str, bytes]): - # TODO: should be (dict[str, bytes], /) -> int - reveal_type(c) # revealed: (Unknown, /) -> int + reveal_type(c) # revealed: (MyDict[str, bytes], /) -> int ListOrDict: TypeAlias = MyList[T] | dict[str, T] def _(x: ListOrDict[int]): - # TODO: should be list[int] | dict[str, int] - reveal_type(x) # revealed: Unknown | dict[str, int] + reveal_type(x) # revealed: list[int] | dict[str, int] MyAlias7: TypeAlias = Callable[Concatenate[T, ...], None] diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index 350630e215..bdb917e8c6 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -419,16 +419,296 @@ def f(x: IntOrStr) -> None: ### Generic example +Manual aliases can be specialized in annotations and value positions, including when they are used +in `type[...]` or nested inside another alias. + ```py -from typing_extensions import TypeAliasType, TypeVar +from typing import Callable, Concatenate, Generic +from typing_extensions import ParamSpec, TypeAliasType, TypeVar, TypeVarTuple, Union, Unpack T = TypeVar("T") IntAndT = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,)) def f(x: IntAndT[str]) -> None: - # TODO: This should be `tuple[int, str]` - reveal_type(x) # revealed: Unknown + reveal_type(x) # revealed: tuple[int, str] + +reveal_type(IntAndT[str]) # revealed: + +def generic_meta(value: type[IntAndT[str]]) -> None: + reveal_type(value) # revealed: type[tuple[int, str]] + +Nested = TypeAliasType("Nested", list[IntAndT[T]], type_params=(T,)) + +def nested(value: Nested[str]) -> None: + reveal_type(value) # revealed: list[IntAndT[str]] +``` + +Defaults apply to unspecialized aliases, and the order of `type_params` determines how type +arguments are mapped even if the parameters appear in a different order in the alias value. + +```py +U = TypeVar("U", default=str) + +ListOrSet = TypeAliasType("ListOrSet", Union[list[U], set[U]], type_params=(U,)) +MyDict = TypeAliasType("MyDict", dict[T, U], type_params=(T, U)) +Reordered = TypeAliasType("Reordered", tuple[U, T], type_params=(T, U)) + +def g( + list_or_set_of_int: ListOrSet[int], + list_or_set_of_str: ListOrSet, + dict_int_str: MyDict[int, str], + dict_unknown_str: MyDict, + reordered: Reordered[int, str], +) -> None: + reveal_type(list_or_set_of_int) # revealed: list[int] | set[int] + reveal_type(list_or_set_of_str) # revealed: list[str] | set[str] + reveal_type(dict_int_str) # revealed: dict[int, str] + reveal_type(dict_unknown_str) # revealed: dict[Unknown, str] + reveal_type(reordered) # revealed: tuple[str, int] +``` + +Constructor inference sees through the specialized `ModelAlias[T]`: passing `Model` infers `T` as +`Model` in `ViaAlias[T]`. + +```py +ModelAlias = TypeAliasType("ModelAlias", type[T], type_params=(T,)) + +class Model: ... + +class ViaAlias(Generic[T]): + def __init__(self, value: ModelAlias[T]) -> None: ... + +reveal_type(ViaAlias(Model)) # revealed: ViaAlias[Model] +``` + +`ParamSpec` parameters can be specialized alongside regular type variables and are preserved when a +callable alias is used as a decorator return type. + +```py +P = ParamSpec("P") +R = TypeVar("R") +WrappedMethod = TypeAliasType("WrappedMethod", Callable[Concatenate[T, P], R], type_params=(T, P, R)) + +def wrapped_method(value: WrappedMethod[int, P, str]) -> None: + reveal_type(value) # revealed: (int, /, *args: P@wrapped_method.args, **kwargs: P@wrapped_method.kwargs) -> str + +WrapsMethod = TypeAliasType("WrapsMethod", Callable[Concatenate[T, ...], R], type_params=(T, R)) + +def decorate(value: WrapsMethod[T, R], /) -> WrappedMethod[T, P, R]: + return value + +@decorate +def decorated(value: int) -> int: + return value + +reveal_type(decorated) # revealed: [**P'return](int, /, *args: P'return.args, **kwargs: P'return.kwargs) -> int +``` + +`TypeVarTuple` parameters accept multiple type arguments when specializing a variadic alias. + +```py +Ts = TypeVarTuple("Ts") +Variadic = TypeAliasType("Variadic", tuple[Unpack[Ts]], type_params=(Ts,)) + +def variadic(value: Variadic[int, str]) -> None: + reveal_type(value) # revealed: tuple[int, str] +``` + +### Recursive generic example + +```py +from typing import Callable +from typing_extensions import TypeAliasType, TypeVar, Union + +T = TypeVar("T") +Recursive = TypeAliasType("Recursive", Union[T, list["Recursive[T]"]], type_params=(T,)) +RecursiveCallable = Callable[[Recursive[T]], None] + +def recursive(value: Recursive[int]) -> None: + reveal_type(value) # revealed: int | list[Recursive[int]] + +def recursive_callable(value: RecursiveCallable[int]) -> None: + reveal_type(value) # revealed: (Recursive[int], /) -> None +``` + +### Generic specialization errors + +```py +from typing_extensions import TypeAliasType, TypeVar + +T = TypeVar("T") +BoundedT = TypeVar("BoundedT", bound=int) + +GenericAlias = TypeAliasType("GenericAlias", list[T], type_params=(T,)) +BoundedAlias = TypeAliasType("BoundedAlias", list[BoundedT], type_params=(BoundedT,)) +NonGenericAlias = TypeAliasType("NonGenericAlias", list[int]) +DefaultedT = TypeVar("DefaultedT", default=str) + +# error: [invalid-type-variable-default] "Type parameter `T` without a default cannot follow earlier parameter `DefaultedT` with a default" +InvalidOrder = TypeAliasType("InvalidOrder", tuple[DefaultedT, T], type_params=(DefaultedT, T)) + +# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2" +reveal_type(GenericAlias[int, str]) # revealed: + +# error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `BoundedT@BoundedAlias`" +reveal_type(BoundedAlias[str]) # revealed: + +# error: [not-subscriptable] "Cannot subscript non-generic type alias `NonGenericAlias`" +reveal_type(NonGenericAlias[int]) # revealed: Unknown + +# error: [not-subscriptable] "Cannot specialize non-generic type alias `NonGenericAlias`" +def non_generic(value: NonGenericAlias[int]) -> None: + reveal_type(value) # revealed: Unknown +``` + +### Invalid type parameters + +```py +from typing_extensions import TypeAliasType, TypeVar, TypeVarTuple, Union, Unpack + +T = TypeVar("T") +U = TypeVar("U") +Ts = TypeVarTuple("Ts") +Us = TypeVarTuple("Us") + +# error: [invalid-type-alias-type] "The `type_params` argument to `TypeAliasType` must be a tuple literal" +InvalidList = TypeAliasType("InvalidList", list[T], type_params=[T]) + +# error: [invalid-type-alias-type] "The `type_params` argument to `TypeAliasType` must be a tuple literal" +InvalidBare = TypeAliasType("InvalidBare", list[T], type_params=T) + +params = (T,) +# error: [invalid-type-alias-type] "The `type_params` argument to `TypeAliasType` must be a tuple literal" +InvalidTupleVariable = TypeAliasType("InvalidTupleVariable", list[T], type_params=params) + +# error: [invalid-type-alias-type] "Each `type_params` entry for `TypeAliasType` must be a type variable" +InvalidUnpack = TypeAliasType("InvalidUnpack", list[T], type_params=(*params,)) + +# error: [invalid-type-alias-type] "Each `type_params` entry for `TypeAliasType` must be a type variable" +InvalidNested = TypeAliasType("InvalidNested", list[T], type_params=(list[T],)) + +# error: [invalid-type-alias-type] "Each `type_params` entry for `TypeAliasType` must be a type variable" +InvalidMixed = TypeAliasType("InvalidMixed", list[T], type_params=(int, T)) + +# error: [invalid-type-alias-type] "Type parameter `U` used in the alias value must be included in `type_params`" +Missing = TypeAliasType("Missing", dict[T, U], type_params=(T,)) + +# error: [invalid-type-alias-type] "Type parameter `T` used in the alias value must be included in `type_params`" +MissingAll = TypeAliasType("MissingAll", list[T]) + +# error: [invalid-type-alias-type] "Type parameter `T` is duplicated in `type_params`" +Duplicate = TypeAliasType("Duplicate", tuple[T, U], type_params=(T, U, T)) + +MultipleTypeVarTuples = TypeAliasType( + "MultipleTypeVarTuples", + Union[tuple[Unpack[Ts]], tuple[Unpack[Us]]], + # error: [invalid-type-alias-type] "Only one `TypeVarTuple` parameter is allowed in `type_params`" + type_params=(Ts, Us), +) + +DefaultedT = TypeVar("DefaultedT", default=int) + +DefaultAfterTypeVarTuple = TypeAliasType( + "DefaultAfterTypeVarTuple", + tuple[Unpack[Ts], DefaultedT], + # error: [invalid-type-variable-default] "Type parameter `DefaultedT` with a default follows TypeVarTuple `Ts`" + type_params=(Ts, DefaultedT), +) + +InvalidOrderAndEntries = TypeAliasType( + "InvalidOrderAndEntries", + tuple[DefaultedT, T], + # error: [invalid-type-variable-default] "Type parameter `T` without a default cannot follow earlier parameter `DefaultedT` with a default" + # error: [invalid-type-alias-type] "Type parameter `T` is duplicated in `type_params`" + # error: [invalid-type-alias-type] "Each `type_params` entry for `TypeAliasType` must be a type variable" + type_params=(DefaultedT, T, T, "V"), +) +``` + +### Scoped type parameters + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable +from typing_extensions import TypeAliasType, TypeVar + +LegacyT = TypeVar("LegacyT") + +def pep695_outer[T]() -> None: + # error: [invalid-type-alias-type] "Type parameter `T` is bound in an outer scope and cannot be used in `type_params`" + Pep695Alias = TypeAliasType("Pep695Alias", list[T], type_params=(T,)) + # error: [not-subscriptable] "Cannot specialize non-generic type alias `Pep695Alias`" + def check(value: Pep695Alias[int]) -> None: ... + +def legacy_outer(value: LegacyT) -> None: + # error: [invalid-type-alias-type] "Type parameter `LegacyT` is bound in an outer scope and cannot be used in `type_params`" + LegacyAlias = TypeAliasType("LegacyAlias", list[LegacyT], type_params=(LegacyT,)) + # error: [not-subscriptable] "Cannot specialize non-generic type alias `LegacyAlias`" + def check(value: LegacyAlias[int]) -> None: ... + +class Pep695Outer[T]: + # error: [invalid-type-alias-type] "Type parameter `T` is bound in an outer scope and cannot be used in `type_params`" + ClassAlias = TypeAliasType("ClassAlias", list[T], type_params=(T,)) + +def paramspec_outer[**P]() -> None: + # error: [invalid-type-alias-type] "Type parameter `P` is bound in an outer scope and cannot be used in `type_params`" + ParamSpecAlias = TypeAliasType("ParamSpecAlias", Callable[P, int], type_params=(P,)) + +def variadic_outer[*Ts]() -> None: + # error: [invalid-type-alias-type] "Type parameter `Ts` is bound in an outer scope and cannot be used in `type_params`" + VariadicAlias = TypeAliasType("VariadicAlias", tuple[*Ts], type_params=(Ts,)) +``` + +### Generic alias from `typing` + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypeAliasType, TypeVar + +K = TypeVar("K") +V = TypeVar("V") +MyDict = TypeAliasType("MyDict", dict[K, V], type_params=(K, V)) + +def generic_from_typing(value: MyDict[str, int]) -> None: + reveal_type(value) # revealed: dict[str, int] +``` + +### PEP 695 aliases in `Callable` + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, TypeVar + +T = TypeVar("T") + +type Pep695List[A] = list[A] +Pep695ConcreteCallable = Callable[[Pep695List[int]], None] +Pep695GenericCallable = Callable[[Pep695List[T]], None] + +type Recursive[A] = A | list[Recursive[A]] +RecursiveCallable = Callable[[Recursive[int]], None] + +def _( + concrete: Pep695ConcreteCallable, + generic: Pep695GenericCallable[str], + recursive: RecursiveCallable, +) -> None: + reveal_type(concrete) # revealed: (Pep695List[int], /) -> None + reveal_type(generic) # revealed: (Pep695List[str], /) -> None + reveal_type(recursive) # revealed: (Recursive[int], /) -> None ``` ### Generic value binds type variables to alias definition diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 4c07de42f4..22eb24c573 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -67,9 +67,9 @@ use crate::types::{ BindingContext, BoundMethodType, BoundTypeVarInstance, CallableType, CallableTypes, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, DynamicType, GenericAlias, InternedConstraintSet, IntersectionType, KnownBoundMethodType, KnownClass, KnownInstanceType, - LiteralValueTypeKind, NominalInstanceType, PropertyInstanceType, SpecialFormType, - TypeAliasType, TypeContext, TypeMapping, TypeVarBoundOrConstraints, TypeVarVariance, - UnionAccumulator, UnionBuilder, UnionType, WrapperDescriptorKind, enums, list_members, + LiteralValueTypeKind, NominalInstanceType, PropertyInstanceType, SpecialFormType, TypeContext, + TypeMapping, TypeVarBoundOrConstraints, TypeVarVariance, UnionAccumulator, UnionBuilder, + UnionType, WrapperDescriptorKind, enums, list_members, }; use crate::{DisplaySettings, FxOrderSet, Program}; use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}; @@ -2124,7 +2124,7 @@ impl<'db> Bindings<'db> { } Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::PEP695(alias), + alias, )) => alias.generic_context(db).map(wrap_generic_context), _ => None, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index cd649077ca..145b5e0b49 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -57,11 +57,12 @@ use crate::types::diagnostic::{ GeneratorMismatchKind, INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION, INVALID_LEGACY_TYPE_VARIABLE, INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, - INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_MISSING_IMPLICIT_CALL, - POSSIBLY_MISSING_SUBMODULE, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, - UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, - hint_if_stdlib_attribute_exists_on_other_versions, report_attempted_protocol_instantiation, - report_bad_dunder_delattr_call, report_bad_dunder_delete_call, report_call_to_abstract_method, + INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPE_VARIABLE_DEFAULT, + POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_SUBMODULE, TypeCheckDiagnostics, + UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE, + UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, hint_if_stdlib_attribute_exists_on_other_versions, + report_attempted_protocol_instantiation, report_bad_dunder_delattr_call, + report_bad_dunder_delete_call, report_call_to_abstract_method, report_cannot_pop_required_field_on_typed_dict, report_invalid_assignment, report_invalid_class_match_pattern, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, @@ -103,20 +104,22 @@ use crate::types::tuple::promotion::TupleSizePromotionConstraints; use crate::types::tuple::{Tuple, TupleLength, TupleSpecBuilder, TupleType, VariableSegment}; use crate::types::type_alias::{ManualPEP695TypeAliasType, PEP695TypeAliasType}; use crate::types::typed_dict::{TypedDictAssignmentKind, TypedDictKeyAssignment}; -use crate::types::typevar::{BoundTypeVarIdentity, TypeVarConstraints, TypeVarIdentity}; +use crate::types::typevar::{ + BoundTypeVarIdentity, TypeVarConstraints, TypeVarIdentity, TypeVarInstance, +}; use crate::types::unpacker::UnpackResult; use crate::types::{ - BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypes, ClassType, - DynamicType, InferenceFlags, InternedConstraintSet, InternedType, IntersectionBuilder, - IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LiteralValueType, - LiteralValueTypeKind, MemberLookupPolicy, ParamSpecAttrKind, Parameter, Parameters, - SentinelInstance, Signature, SpecialFormType, SubclassOfType, Type, TypeAliasType, + BindingContext, BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, + CallableTypes, ClassType, DynamicType, InferenceFlags, InternedConstraintSet, InternedType, + IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, + LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, ParamSpecAttrKind, Parameter, + Parameters, SentinelInstance, Signature, SpecialFormType, SubclassOfType, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarKind, TypeVarVariance, TypedDictModule, TypedDictType, UnionAccumulator, UnionBuilder, UnionType, any_over_type, binding_type, extract_fixed_length_iterable_element_types, infer_complete_scope_types, infer_scope_types, is_discarded_dict_key_assignment, todo_type, }; -use crate::{AnalysisSettings, Db, FxIndexSet, Program}; +use crate::{AnalysisSettings, Db, FxIndexSet, FxOrderSet, Program}; use ty_python_core::ast_ids::ScopedUseId; use ty_python_core::definition::{ AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, ComprehensionDefinitionKind, @@ -3741,7 +3744,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.deferred.insert(definition); Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new(db, name, definition)), + TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new(db, name, definition, None)), )) } @@ -3755,10 +3758,148 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // in the alias value are bound to the alias definition. let previous_context = self.typevar_binding_context.replace(definition); - self.infer_type_expression(&arguments.args[1]); + let value_ty = self.infer_type_expression(&arguments.args[1]); + let mut type_params = FxHashSet::default(); + let mut valid_type_params = true; // Infer keyword arguments (e.g. `type_params`) so their types are stored. for keyword in &arguments.keywords { self.infer_expression(&keyword.value, TypeContext::default()); + + if keyword.arg.as_deref() != Some("type_params") { + continue; + } + + let Some(tuple) = keyword.value.as_tuple_expr() else { + valid_type_params = false; + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_ALIAS_TYPE, &keyword.value) + { + builder.into_diagnostic( + "The `type_params` argument to `TypeAliasType` must be a tuple literal", + ); + } + continue; + }; + + let db = self.db(); + let mut typevar_with_default = None; + let mut typevar_tuple: Option = None; + let mut reported_default_order_error = false; + + for element in &tuple.elts { + let bound_typevar = match self.expression_type(element) { + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => bind_typevar( + db, + self.index, + definition.file_scope(db), + Some(definition), + typevar, + ), + _ => None, + }; + let Some(bound_typevar) = bound_typevar else { + valid_type_params = false; + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_ALIAS_TYPE, element) + { + builder.into_diagnostic( + "Each `type_params` entry for `TypeAliasType` must be a type variable", + ); + } + continue; + }; + let typevar = bound_typevar.typevar(db); + + if bound_typevar.binding_context(db) != BindingContext::Definition(definition) { + valid_type_params = false; + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_ALIAS_TYPE, element) + { + builder.into_diagnostic(format_args!( + "Type parameter `{}` is bound in an outer scope and cannot be used in `type_params`", + typevar.name(db), + )); + } + continue; + } + + if !type_params.insert(bound_typevar.identity(db)) { + valid_type_params = false; + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_ALIAS_TYPE, element) + { + builder.into_diagnostic(format_args!( + "Type parameter `{}` is duplicated in `type_params`", + typevar.name(db), + )); + } + } + + if typevar.default_type(db).is_some() { + if let Some(typevar_tuple) = typevar_tuple { + valid_type_params = false; + if let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, element) + { + builder.into_diagnostic(format_args!( + "Type parameter `{}` with a default follows TypeVarTuple `{}`", + typevar.name(db), + typevar_tuple.name(db), + )); + } + } + typevar_with_default.get_or_insert(typevar); + } else if let Some(typevar_with_default) = typevar_with_default { + valid_type_params = false; + if !reported_default_order_error + && let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_VARIABLE_DEFAULT, element) + { + reported_default_order_error = true; + builder.into_diagnostic(format_args!( + "Type parameter `{}` without a default cannot follow earlier parameter `{}` with a default", + typevar.name(db), + typevar_with_default.name(db), + )); + } + } + + if typevar.is_typevartuple(db) { + if typevar_tuple.is_some() { + valid_type_params = false; + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_ALIAS_TYPE, element) + { + builder.into_diagnostic( + "Only one `TypeVarTuple` parameter is allowed in `type_params`", + ); + } + } else { + typevar_tuple = Some(typevar); + } + } + } + } + + if valid_type_params { + let mut value_typevars = FxOrderSet::default(); + value_ty.find_legacy_typevars(self.db(), Some(definition), &mut value_typevars); + + for typevar in value_typevars { + if !type_params.contains(&typevar.identity(self.db())) + && let Some(builder) = self + .context + .report_lint(&INVALID_TYPE_ALIAS_TYPE, &arguments.args[1]) + { + builder.into_diagnostic(format_args!( + "Type parameter `{}` used in the alias value must be included in `type_params`", + typevar.name(self.db()), + )); + } + } } self.typevar_binding_context = previous_context; diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index 46ca8ecf68..a311cbcb65 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -240,19 +240,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } } - Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695( - _, - ))) => { - let slice_ty = self.infer_expression(slice, TypeContext::default()); - let mut variables = FxOrderSet::default(); - slice_ty.bind_and_find_all_legacy_typevars( - db, - self.typevar_binding_context, - &mut variables, - ); - let generic_context = GenericContext::from_typevar_instances(db, variables); - return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); - } Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => { if let Some(generic_context) = type_alias.generic_context(db) { return self.infer_explicit_type_alias_type_specialization( diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index b6e7157ccc..8addf06db2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -23,9 +23,9 @@ use ty_python_core::scope::ScopeKind; use crate::types::{ BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, LiteralValueTypeKind, - Parameter, Parameters, SpecialFormType, SubclassOfType, Type, TypeAliasType, TypeContext, - TypeFormType, TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder, UnionType, - any_over_type, todo_type, + Parameter, Parameters, SpecialFormType, SubclassOfType, Type, TypeContext, TypeFormType, + TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder, UnionType, any_over_type, + todo_type, }; use crate::{FxOrderSet, Program, add_inferred_python_version_hint_to_diagnostic}; @@ -1357,9 +1357,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); invalid_type_argument(self, slice) } - value_ty @ Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::PEP695(_), - )) => { + value_ty @ Type::KnownInstance(KnownInstanceType::TypeAliasType(_)) => { let slice_ty = self.infer_subscript_type_expression(subscript, value_ty); subclass_of_type_argument(self, slice, slice_ty) } @@ -1592,7 +1590,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } Type::unknown() } - KnownInstanceType::TypeAliasType(type_alias @ TypeAliasType::PEP695(_)) => { + KnownInstanceType::TypeAliasType(type_alias) => { match type_alias.generic_context(self.db()) { Some(generic_context) => { let specialized_type_alias = self @@ -1642,19 +1640,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } } - KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695(_)) => { - // TODO: support generic "manual" PEP 695 type aliases - let slice_ty = self.infer_expression(slice, TypeContext::default()); - let mut variables = FxOrderSet::default(); - slice_ty.bind_and_find_all_legacy_typevars( - self.db(), - self.typevar_binding_context, - &mut variables, - ); - let generic_context = - GenericContext::from_typevar_instances(self.db(), variables); - Type::Dynamic(DynamicType::UnknownGeneric(generic_context)) - } KnownInstanceType::Literal(ty) => { if !self.in_string_annotation() { self.infer_expression(slice, TypeContext::default()); diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs index 45786f5c85..92f0deea33 100644 --- a/crates/ty_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -309,7 +309,7 @@ impl<'db> KnownInstanceType<'db> { KnownClass::TypeVarTuple } Self::TypeVar(_) => KnownClass::TypeVar, - Self::TypeAliasType(TypeAliasType::PEP695(alias)) if alias.is_specialized(db) => { + Self::TypeAliasType(alias) if alias.specialization(db).is_some() => { KnownClass::GenericAlias } Self::TypeAliasType(_) => KnownClass::TypeAliasType, diff --git a/crates/ty_python_semantic/src/types/subscript.rs b/crates/ty_python_semantic/src/types/subscript.rs index 059e26c70d..d2bbc9acce 100644 --- a/crates/ty_python_semantic/src/types/subscript.rs +++ b/crates/ty_python_semantic/src/types/subscript.rs @@ -779,16 +779,13 @@ impl<'db> Type<'db> { Some(Ok(todo_type!("doubly-specialized typing.Protocol"))) } - ( - Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(alias))), - _, - ) if alias.generic_context(db).is_none() => { + (Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)), _) + if alias.generic_context(db).is_none() => + { debug_assert!(alias.specialization(db).is_none()); Some(Err(SubscriptError::new( Type::unknown(), - SubscriptErrorKind::NonGenericTypeAlias { - alias: TypeAliasType::PEP695(alias), - }, + SubscriptErrorKind::NonGenericTypeAlias { alias }, ))) } diff --git a/crates/ty_python_semantic/src/types/type_alias.rs b/crates/ty_python_semantic/src/types/type_alias.rs index 952591b361..c21f76e99a 100644 --- a/crates/ty_python_semantic/src/types/type_alias.rs +++ b/crates/ty_python_semantic/src/types/type_alias.rs @@ -1,12 +1,13 @@ use std::fmt::Write; use crate::{ - Db, + Db, FxOrderSet, types::{ - ApplyTypeMappingVisitor, BoundTypeVarIdentity, GenericContext, Type, TypeContext, - TypeMapping, TypeVarVariance, definition_expression_type, + ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, GenericContext, + KnownInstanceType, Type, TypeContext, TypeMapping, TypeVarVariance, + definition_expression_type, display::qualified_name_components_from_scope, - generics::{ApplySpecialization, Specialization}, + generics::{ApplySpecialization, Specialization, bind_typevar}, variance::VarianceInferable, visitor, }, @@ -54,7 +55,12 @@ impl<'db> PEP695TypeAliasType<'db> { /// The RHS type of a PEP-695 style type alias with specialization applied. pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { - self.apply_function_specialization(db, self.raw_value_type(db)) + apply_type_alias_specialization( + db, + self.raw_value_type(db), + self.generic_context(db), + self.specialization(db), + ) } /// The RHS type of a PEP-695 style type alias with *no* specialization applied. @@ -76,32 +82,6 @@ impl<'db> PEP695TypeAliasType<'db> { definition_expression_type(db, definition, &type_alias_stmt_node.node(&module).value) } - fn apply_function_specialization(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { - if let Some(generic_context) = self.generic_context(db) { - let specialization = self - .specialization(db) - .unwrap_or_else(|| generic_context.default_specialization(db, None)); - let type_mapping = match specialization.materialization_kind(db) { - None => { - TypeMapping::ApplySpecialization(ApplySpecialization::TypeAlias(specialization)) - } - Some(materialization_kind) => TypeMapping::ApplySpecializationWithMaterialization { - specialization: ApplySpecialization::TypeAlias(specialization), - materialization_kind, - }, - }; - - ty.apply_type_mapping_impl( - db, - &type_mapping, - TypeContext::default(), - &ApplyTypeMappingVisitor::default(), - ) - } else { - ty - } - } - pub(crate) fn apply_specialization( self, db: &'db dyn Db, @@ -127,10 +107,6 @@ impl<'db> PEP695TypeAliasType<'db> { } } - pub(crate) fn is_specialized(self, db: &'db dyn Db) -> bool { - self.specialization(db).is_some() - } - #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { let scope = self.rhs_scope(db); @@ -160,6 +136,9 @@ pub struct ManualPEP695TypeAliasType<'db> { pub name: Name, #[returns(copy)] pub definition: Definition<'db>, + + #[returns(copy)] + pub(super) specialization: Option>, } // The Salsa heap is tracked separately. @@ -177,6 +156,18 @@ pub(super) fn walk_manual_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + impl<'db> ManualPEP695TypeAliasType<'db> { /// The value type of this manual type alias. /// + /// Computed lazily from the definition with specialization applied. + pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + apply_type_alias_specialization( + db, + self.raw_value_type(db), + self.generic_context(db), + self.specialization(db), + ) + } + + /// The value type of this manual type alias with no specialization applied. + /// /// Computed lazily from the definition to avoid including the value in the interned /// struct's identity. Returns `Divergent` if the type alias is defined cyclically. #[salsa::tracked( @@ -187,7 +178,7 @@ impl<'db> ManualPEP695TypeAliasType<'db> { }, heap_size=ruff_memory_usage::heap_size )] - pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { let definition = self.definition(db); let file = definition.file(db); let module = parsed_module(db, file).load(db); @@ -204,6 +195,89 @@ impl<'db> ManualPEP695TypeAliasType<'db> { }; definition_expression_type(db, definition, value_arg) } + + pub(crate) fn apply_specialization( + self, + db: &'db dyn Db, + f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, + ) -> Self { + let Some(generic_context) = self.generic_context(db) else { + return self; + }; + + Self::new( + db, + self.name(db), + self.definition(db), + Some(f(generic_context)), + ) + } + + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + let definition = self.definition(db); + let file = definition.file(db); + let module = parsed_module(db, file).load(db); + let DefinitionKind::Assignment(assignment) = definition.kind(db) else { + return None; + }; + let ast::Expr::Call(call) = assignment.value(&module) else { + return None; + }; + let type_params = call + .arguments + .find_argument_value("type_params", 2)? + .as_tuple_expr()?; + let index = semantic_index(db, file); + + let mut variables = FxOrderSet::default(); + for element in &type_params.elts { + let typevar = match definition_expression_type(db, definition, element) { + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => bind_typevar( + db, + index, + definition.file_scope(db), + Some(definition), + typevar, + )?, + _ => return None, + }; + if typevar.binding_context(db) != BindingContext::Definition(definition) { + return None; + } + variables.insert(typevar); + } + + (!variables.is_empty()).then(|| GenericContext::from_typevar_instances(db, variables)) + } +} + +fn apply_type_alias_specialization<'db>( + db: &'db dyn Db, + ty: Type<'db>, + generic_context: Option>, + specialization: Option>, +) -> Type<'db> { + let Some(generic_context) = generic_context else { + return ty; + }; + + let specialization = + specialization.unwrap_or_else(|| generic_context.default_specialization(db, None)); + let type_mapping = match specialization.materialization_kind(db) { + None => TypeMapping::ApplySpecialization(ApplySpecialization::TypeAlias(specialization)), + Some(materialization_kind) => TypeMapping::ApplySpecializationWithMaterialization { + specialization: ApplySpecialization::TypeAlias(specialization), + materialization_kind, + }, + }; + + ty.apply_type_mapping_impl( + db, + &type_mapping, + TypeContext::default(), + &ApplyTypeMappingVisitor::default(), + ) } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize, salsa::SalsaValue)] @@ -258,7 +332,7 @@ impl<'db> TypeAliasType<'db> { pub(crate) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { match self { TypeAliasType::PEP695(type_alias) => type_alias.raw_value_type(db), - TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db), + TypeAliasType::ManualPEP695(type_alias) => type_alias.raw_value_type(db), } } @@ -271,7 +345,9 @@ impl<'db> TypeAliasType<'db> { alias.rhs_scope(db), None, )), - TypeAliasType::ManualPEP695(_) => self, + TypeAliasType::ManualPEP695(alias) => TypeAliasType::ManualPEP695( + ManualPEP695TypeAliasType::new(db, alias.name(db), alias.definition(db), None), + ), } } @@ -283,17 +359,16 @@ impl<'db> TypeAliasType<'db> { } pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { - // TODO: Add support for generic non-PEP695 type aliases. match self { TypeAliasType::PEP695(type_alias) => type_alias.generic_context(db), - TypeAliasType::ManualPEP695(_) => None, + TypeAliasType::ManualPEP695(type_alias) => type_alias.generic_context(db), } } pub(crate) fn specialization(self, db: &'db dyn Db) -> Option> { match self { TypeAliasType::PEP695(type_alias) => type_alias.specialization(db), - TypeAliasType::ManualPEP695(_) => None, + TypeAliasType::ManualPEP695(type_alias) => type_alias.specialization(db), } } @@ -306,7 +381,9 @@ impl<'db> TypeAliasType<'db> { TypeAliasType::PEP695(type_alias) => { TypeAliasType::PEP695(type_alias.apply_specialization(db, f)) } - TypeAliasType::ManualPEP695(_) => self, + TypeAliasType::ManualPEP695(type_alias) => { + TypeAliasType::ManualPEP695(type_alias.apply_specialization(db, f)) + } } } diff --git a/ty.schema.json b/ty.schema.json index aec51f3dc2..4f5d00f4ad 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -959,7 +959,7 @@ }, "invalid-type-alias-type": { "title": "detects invalid TypeAliasType definitions", - "description": "## What it does\n\nChecks for the creation of invalid `TypeAliasType`s\n\n## Why is this bad?\n\nThere are several requirements that you must follow when creating a `TypeAliasType`.\n\n## Examples\n\n```toml\n[environment]\npython-version = \"3.12\"\n```\n\n```python\nfrom typing import TypeAliasType\n\n\ndef get_name() -> str:\n return \"NewAlias\"\n\n\nIntOrStr = TypeAliasType(\"IntOrStr\", int | str) # okay\n# TypeAliasType name must be a string literal\nNewAlias = TypeAliasType(get_name(), int) # error\n```", + "description": "## What it does\n\nChecks for the creation of invalid `TypeAliasType`s\n\n## Why is this bad?\n\nThere are several requirements that you must follow when creating a `TypeAliasType`.\n\n## Examples\n\n```toml\n[environment]\npython-version = \"3.12\"\n```\n\n```python\nfrom typing import TypeAliasType, TypeVar\n\n\ndef get_name() -> str:\n return \"NewAlias\"\n\n\nIntOrStr = TypeAliasType(\"IntOrStr\", int | str) # okay\n# TypeAliasType name must be a string literal\nNewAlias = TypeAliasType(get_name(), int) # error\n\nT = TypeVar(\"T\")\nGenericAlias = TypeAliasType(\"GenericAlias\", list[T], type_params=(T,)) # okay\n# TypeAliasType type parameters must be type variables\nInvalidAlias = TypeAliasType(\"InvalidAlias\", list[T], type_params=(list[T],)) # error\n```", "default": "error", "oneOf": [ { From a6e447c4678d2d8b0f2902c5b45e2e32e253ac16 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:58:15 -0400 Subject: [PATCH 007/390] [`airflow`] Stabilize `airflow3-incompatible-function-signature` (`AIR303`) (#26897) --- .../src/rules/airflow/rules/function_signature_change_in_3.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs index 7551133336..53c649def2 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs @@ -36,7 +36,7 @@ use ruff_text_size::Ranged; /// collector.create_asset(uri="s3://bucket/key") /// ``` #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.14.11")] +#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] pub(crate) struct Airflow3IncompatibleFunctionSignature { function_name: String, change: FunctionSignatureChange, From a6ce36e0fc71163147bc856a4d84cb1e92340df2 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:58:41 -0400 Subject: [PATCH 008/390] [`pylint`] Stabilize `stop-iteration-return` (`PLR1708`) (#26903) --- .../ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs b/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs index 6ae47fe66b..0e09655130 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs @@ -38,7 +38,7 @@ use crate::checkers::ast::Checker; /// - [PEP 479](https://peps.python.org/pep-0479/) /// - [Python documentation](https://docs.python.org/3/library/exceptions.html#StopIteration) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.14.3")] +#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] pub(crate) struct StopIterationReturn; impl Violation for StopIterationReturn { From 33dbe88e609dfe991a577094a1d1dedb5b48dabd Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:58:53 -0400 Subject: [PATCH 009/390] [`ruff`] Stabilize `access-annotations-from-class-dict` (`RUF063`) (#26905) This would be a really good candidate for mdtests but otherwise everything looked fine --- .../rules/ruff/rules/access_annotations_from_class_dict.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs b/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs index 33435c648e..dcf2d218ea 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs @@ -7,8 +7,7 @@ use ruff_text_size::Ranged; /// ## What it does /// Checks for uses of `foo.__dict__.get("__annotations__")` or /// `foo.__dict__["__annotations__"]` on Python 3.10+ and Python < 3.10 when -/// [typing-extensions](https://docs.astral.sh/ruff/settings/#lint_typing-extensions) -/// is enabled. +/// [`lint.typing-extensions`] is enabled. /// /// ## Why is this bad? /// Starting with Python 3.14, directly accessing `__annotations__` via @@ -74,7 +73,7 @@ use ruff_text_size::Ranged; /// ## References /// - [Python Annotations Best Practices](https://docs.python.org/3.14/howto/annotations.html) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.12.1")] +#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] pub(crate) struct AccessAnnotationsFromClassDict { python_version: PythonVersion, } From 5fb26c30a36654958a82cbd6f98ab183c8b178dd Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:59:23 -0400 Subject: [PATCH 010/390] [`refurb`] Stabilize `unnecessary-from-float` (`FURB164`) (#26913) Codex noticed a fix safety bug where comments could be deleted in a safe fix branch, so I fixed that too. Docs and tests looked good ```py from decimal import Decimal _ = Decimal.from_float( # keep this comment float("inf") ) ``` [Playground](https://play.ruff.rs/1016fd80-d170-4765-9856-c5b5f6b7fdeb) --- .../resources/test/fixtures/refurb/FURB164.py | 5 ++++ .../refurb/rules/unnecessary_from_float.rs | 14 ++++++++--- ...es__refurb__tests__FURB164_FURB164.py.snap | 23 +++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py index 1ac29fbf70..992427c376 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB164.py @@ -75,3 +75,8 @@ # text .from_float(4.2) ) + +_ = Decimal.from_float( + # keep this comment + float("inf") +) diff --git a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs index a60a378c66..ec1abc3003 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs @@ -61,7 +61,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `decimal`](https://docs.python.org/3/library/decimal.html) /// - [Python documentation: `fractions`](https://docs.python.org/3/library/fractions.html) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "v0.3.5")] +#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] pub(crate) struct UnnecessaryFromFloat { method_name: MethodName, constructor: Constructor, @@ -134,6 +134,7 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) { }; let constructor_name = checker.locator().slice(&**value).to_string(); + let has_comments = checker.comment_ranges().intersects(call.range()); // Special case for non-finite float literals: Decimal.from_float(float("inf")) -> Decimal("inf") if let Some(replacement) = handle_non_finite_float_special_case( @@ -144,7 +145,14 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) { &constructor_name, checker, ) { - diagnostic.set_fix(Fix::safe_edit(replacement)); + diagnostic.set_fix(Fix::applicable_edit( + replacement, + if has_comments { + Applicability::Unsafe + } else { + Applicability::Safe + }, + )); return; } @@ -152,7 +160,7 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) { let is_type_safe = is_valid_argument_type(arg_value, method_name, constructor, checker); // Determine fix safety - let applicability = if is_type_safe && !checker.comment_ranges().intersects(call.range()) { + let applicability = if is_type_safe && !has_comments { Applicability::Safe } else { Applicability::Unsafe diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap index 313bd5ff97..93d0384f59 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap @@ -687,3 +687,26 @@ help: Replace with `Fraction` constructor 75 | ) | note: This is an unsafe fix and may change runtime behavior + +FURB164 [*] Verbose method `from_float` in `Decimal` construction + --> FURB164.py:79:5 + | +77 | ) +78 | +79 | _ = Decimal.from_float( + | _____^ +80 | | # keep this comment +81 | | float("inf") +82 | | ) + | |_^ + | +help: Replace with `Decimal` constructor + | +78 | + - _ = Decimal.from_float( + - # keep this comment + - float("inf") + - ) +79 + _ = Decimal("inf") + | +note: This is an unsafe fix and may change runtime behavior From 80f0da716b0dceeba58531e099db245a0643159b Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:59:36 -0400 Subject: [PATCH 011/390] [`pylint`] Stabilize `invalid-bool-return-type` (`PLE0304`) (#26914) --- .../ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs index cd49f9ad69..1d8b8bf409 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs @@ -35,7 +35,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `__bool__` method](https://docs.python.org/3/reference/datamodel.html#object.__bool__) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "v0.3.3")] +#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] pub(crate) struct InvalidBoolReturnType; impl Violation for InvalidBoolReturnType { From a7cd5082904a9cba98997d83f4274be2a6cd53e2 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 20 Jul 2026 09:22:36 -0400 Subject: [PATCH 012/390] Stabilize fix diffs in full `check` output (#26958) Summary -- Closes #22946 Test Plan -- Updated existing snapshots --- crates/ruff/src/printer.rs | 4 +- .../cli__lint__output_format_full.snap | 4 ++ crates/ruff/tests/integration_test.rs | 63 +++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/crates/ruff/src/printer.rs b/crates/ruff/src/printer.rs index aad9984176..d7c83a701e 100644 --- a/crates/ruff/src/printer.rs +++ b/crates/ruff/src/printer.rs @@ -241,7 +241,7 @@ impl Printer { .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) .with_fix_applicability(self.unsafe_fixes.required_applicability()) - .show_fix_diff(preview.is_enabled()); + .show_fix_diff(true); render_diagnostics(writer, self.format, config, &context, &diagnostics.inner)?; @@ -415,7 +415,7 @@ impl Printer { .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) .with_fix_applicability(self.unsafe_fixes.required_applicability()) - .show_fix_diff(preview.is_enabled()); + .show_fix_diff(true); render_diagnostics(writer, self.format, config, &context, &diagnostics.inner)?; } writer.flush()?; diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap index ce1e673e64..59e2fcbe6d 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__output_format_full.snap @@ -25,6 +25,10 @@ F401 [*] `os` imported but unused 3 | match 42: # invalid-syntax | help: Remove unused import: `os` + | + - import os # F401 +1 | x = y # F821 + | F821 Undefined name `y` --> input.py:2:5 diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs index 66d1729017..33a3317b61 100644 --- a/crates/ruff/tests/integration_test.rs +++ b/crates/ruff/tests/integration_test.rs @@ -122,6 +122,9 @@ fn stdin_error() { | ^^ | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -147,6 +150,9 @@ fn stdin_filename() { | ^^ | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -183,6 +189,10 @@ import bar # unused import | ^^^ | help: Remove unused import: `bar` + | + 1 | + - import bar # unused import + | F401 [*] `foo` imported but unused --> foo.py:2:8 @@ -191,6 +201,10 @@ import bar # unused import | ^^^ | help: Remove unused import: `foo` + | + 1 | + - import foo # unused import + | Found 2 errors. [*] 2 fixable with the `--fix` option. @@ -219,6 +233,9 @@ fn check_warn_stdin_filename_with_files() { | ^^ | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -246,6 +263,9 @@ fn stdin_source_type_py() { | ^^ | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -583,6 +603,10 @@ fn stdin_override_parser_ipynb() { | ^^ | help: Remove unused import: `os` + ::: cell 1 + | + - import os + | F401 [*] `sys` imported but unused --> Jupyter.py:cell 3:1:8 @@ -591,6 +615,10 @@ fn stdin_override_parser_ipynb() { | ^^^ | help: Remove unused import: `sys` + ::: cell 3 + | + - import sys + | Found 2 errors. [*] 2 fixable with the `--fix` option. @@ -621,6 +649,9 @@ fn stdin_override_parser_py() { | ^^ | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -656,6 +687,9 @@ extension = {ipynb="python"} | ^^ | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -1835,6 +1869,9 @@ fn check_input_from_argfile() -> Result<()> { | ^^ | help: Remove unused import: `os` + | + - import os + | Found 1 error. [*] 1 fixable with the `--fix` option. @@ -1878,6 +1915,9 @@ fn check_hints_hidden_unsafe_fixes() { ----- stdout ----- RUF901 [*] Hey this is a stable test rule with a safe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-safe-fix + | RUF902 Hey this is a stable test rule with an unsafe fix. --> -:1:1 @@ -1920,6 +1960,9 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() { ----- stdout ----- RUF901 [*] Hey this is a stable test rule with a safe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-safe-fix + | RUF902 Hey this is a stable test rule with an unsafe fix. --> -:1:1 @@ -1963,9 +2006,16 @@ fn check_shows_unsafe_fixes_with_opt_in() { ----- stdout ----- RUF901 [*] Hey this is a stable test rule with a safe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-safe-fix + | RUF902 [*] Hey this is a stable test rule with an unsafe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-unsafe-fix + | + note: This is an unsafe fix and may change runtime behavior Found 2 errors. [*] 2 fixable with the `--fix` option. @@ -2241,9 +2291,15 @@ extend-safe-fixes = ["RUF902"] ----- stdout ----- RUF901 [*] Hey this is a stable test rule with a safe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-safe-fix + | RUF902 [*] Hey this is a stable test rule with an unsafe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-unsafe-fix + | Found 2 errors. [*] 2 fixable with the `--fix` option. @@ -2279,6 +2335,9 @@ extend-safe-fixes = ["RUF902"] ----- stdout ----- RUF901 [*] Hey this is a stable test rule with a safe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-safe-fix + | RUF902 Hey this is a stable test rule with an unsafe fix. --> -:1:1 @@ -2325,6 +2384,10 @@ extend-safe-fixes = ["RUF9"] RUF902 [*] Hey this is a stable test rule with an unsafe fix. --> -:1:1 + | + 1 + # fix from stable-test-rule-unsafe-fix + 2 | x = {'a': 1, 'a': 1} + | RUF903 Hey this is a stable test rule with a display only fix. --> -:1:1 From cad676a4a2618aca342d114bae3131790cea0f95 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 20 Jul 2026 09:52:43 -0400 Subject: [PATCH 013/390] Stabilize `format --check` output formats (#26960) Summary -- Closes #20755. I'll stack another PR on top of this to fix #24528, but you can see the same issue in one of the new snapshots. Test Plan -- Updated existing snapshots --- crates/ruff/src/args.rs | 3 - crates/ruff/src/commands/format.rs | 39 ++------- crates/ruff/src/lib.rs | 6 -- crates/ruff/tests/cli/format.rs | 128 +++++++++++++++++++---------- 4 files changed, 93 insertions(+), 83 deletions(-) diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index b26ded9b53..7258722d82 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -621,9 +621,6 @@ pub struct FormatCommand { /// Output serialization format for violations, when used with `--check`. /// The default serialization format is "full". - /// - /// Note that this option is currently only respected in preview mode. A warning will be emitted - /// if this flag is used on stable. #[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")] pub output_format: Option, } diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 78cab1a17a..8a10f52d1b 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -78,7 +78,6 @@ pub(crate) fn format( let (paths, resolver) = project_files_in_path(&files, pyproject_config, config_arguments)?; let output_format = pyproject_config.settings.output_format; - let preview = pyproject_config.settings.formatter.preview; if paths.is_empty() { warn_user_once!("No Python files found under the given path(s)"); @@ -191,9 +190,9 @@ pub(crate) fn format( // Report on any errors. // - // We only convert errors to `Diagnostic`s in `Check` mode with preview enabled, otherwise we - // fall back on printing simple messages. - if !(preview.is_enabled() && mode.is_check()) { + // We only convert errors to `Diagnostic`s in `Check` mode, otherwise we fall back on printing + // simple messages. + if !mode.is_check() { errors.sort_unstable_by(|a, b| a.path().cmp(&b.path())); for error in &errors { @@ -206,11 +205,7 @@ pub(crate) fn format( match mode { FormatMode::Write => {} FormatMode::Check => { - if preview.is_enabled() { - results.write_changed_preview(&mut stdout().lock(), output_format, &errors)?; - } else { - results.write_changed(&mut stdout().lock())?; - } + results.write_changed(&mut stdout().lock(), output_format, &errors)?; } FormatMode::Diff => { results.write_diff(&mut stdout().lock())?; @@ -223,7 +218,7 @@ pub(crate) fn format( if mode.is_diff() { // Allow piping the diff to e.g. a file by writing the summary to stderr results.write_summary(&mut stderr().lock())?; - } else if !preview.is_enabled() || output_format.is_human_readable() { + } else if output_format.is_human_readable() { results.write_summary(&mut stdout().lock())?; } } @@ -588,28 +583,8 @@ impl<'a> FormatResults<'a> { Ok(()) } - /// Write a list of the files that would be changed to the given writer. - fn write_changed(&self, f: &mut impl Write) -> io::Result<()> { - for path in self - .results - .iter() - .filter_map(|result| { - if result.result.is_diff() { - Some(result.path.as_path()) - } else { - None - } - }) - .sorted_unstable() - { - writeln!(f, "Would reformat: {}", fs::relativize_path(path).bold())?; - } - - Ok(()) - } - /// Write a list of the files that would be changed and any errors to the given writer. - fn write_changed_preview( + fn write_changed( &self, f: &mut impl Write, output_format: OutputFormat, @@ -1346,7 +1321,7 @@ mod tests { let results = FormatResults::new(&[], FormatMode::Check); let mut buf = Vec::new(); - results.write_changed_preview( + results.write_changed( &mut buf, ruff_linter::settings::types::OutputFormat::Full, &errors, diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index d7395c1ab9..350e57f3b4 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -212,14 +212,8 @@ pub fn run( } fn format(args: FormatCommand, global_options: GlobalConfigArgs) -> Result { - let cli_output_format_set = args.output_format.is_some(); let (cli, config_arguments) = args.partition(global_options)?; let pyproject_config = resolve::resolve(&config_arguments, cli.stdin_filename.as_deref())?; - if cli_output_format_set && !pyproject_config.settings.formatter.preview.is_enabled() { - warn_user_once!( - "The --output-format flag for the formatter is unstable and requires preview mode to use." - ); - } if is_stdin(&cli.files, cli.stdin_filename.as_deref()) { commands::format_stdin::format_stdin(&cli, &config_arguments, &pyproject_config) } else { diff --git a/crates/ruff/tests/cli/format.rs b/crates/ruff/tests/cli/format.rs index e660c93ad2..0395d10ff1 100644 --- a/crates/ruff/tests/cli/format.rs +++ b/crates/ruff/tests/cli/format.rs @@ -51,16 +51,28 @@ fn default_files() -> Result<()> { assert_cmd_snapshot!(test.format_command() .arg("--isolated") - .arg("--check"), @" + .arg("--check"), @r#" success: false exit_code: 1 ----- stdout ----- - Would reformat: bar.py - Would reformat: foo.py + unformatted: File would be reformatted + --> bar.py:1:1 + | + - bar = "needs formatting" + 1 + bar = "needs formatting" + | + + unformatted: File would be reformatted + --> foo.py:1:1 + | + - foo = "needs formatting" + 1 + foo = "needs formatting" + | + 2 files would be reformatted ----- stderr ----- - "); + "#); Ok(()) } @@ -446,16 +458,30 @@ OTHER = "OTHER" // Explicitly pass test.py, should be formatted regardless of it being excluded by format.exclude .arg("test.py") // Format all other files in the directory, should respect the `exclude` and `format.exclude` options - .arg("."), @" + .arg("."), @r#" success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py - Would reformat: test.py + unformatted: File would be reformatted + --> main.py:1:1 + | + - + 1 | from test import say_hy + | + + unformatted: File would be reformatted + --> test.py:1:1 + | + - + 1 | def say_hy(name: str): + - print(f"Hy {name}") + 2 + print(f"Hy {name}") + | + 2 files would be reformatted ----- stderr ----- - "); + "#); Ok(()) } @@ -490,7 +516,13 @@ exclude = ["format_excluded.py"] success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py + unformatted: File would be reformatted + --> main.py:1:1 + | + - x = 1 + 1 + x = 1 + | + 1 file would be reformatted ----- stderr ----- @@ -512,7 +544,13 @@ fn deduplicate_directory_and_explicit_file() -> Result<()> { success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py + unformatted: File would be reformatted + --> main.py:1:1 + | + - x = 1 + 1 + x = 1 + | + 1 file would be reformatted ----- stderr ----- @@ -538,9 +576,11 @@ from module import = success: false exit_code: 2 ----- stdout ----- + invalid-syntax: Expected an import name + --> main.py:1:1 + ----- stderr ----- - error: Failed to parse main.py:2:20: Expected an import name "); Ok(()) @@ -565,7 +605,13 @@ if __name__ == "__main__": success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py + unformatted: File would be reformatted + --> main.py:1:1 + | + - + 1 | from test import say_hy + | + 1 file would be reformatted ----- stderr ----- @@ -628,13 +674,8 @@ if __name__ == "__main__": assert_cmd_snapshot!( snapshot, - test.format_command().args([ - "--output-format", - output_format, - "--preview", - "--check", - "input.py", - ]), + test.format_command() + .args(["--output-format", output_format, "--check", "input.py",]), ); Ok(()) @@ -646,7 +687,7 @@ fn output_format_notebook() -> Result<()> { let path = test.fixture_path("unformatted.ipynb"); assert_cmd_snapshot!( - test.format_command().args(["--isolated", "--preview", "--check"]).arg(path), + test.format_command().args(["--isolated", "--check"]).arg(path), @" success: false exit_code: 1 @@ -786,7 +827,15 @@ fn check_quiet_mode_shows_diagnostics_only() -> Result<()> { success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py + unformatted: File would be reformatted + --> main.py:1:1 + | + - def foo(): + - pass + 1 + def foo(): + 2 + pass + | + ----- stderr ----- "); @@ -802,7 +851,15 @@ fn check_default_mode_shows_diagnostics_and_summary() -> Result<()> { success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py + unformatted: File would be reformatted + --> main.py:1:1 + | + - def foo(): + - pass + 1 + def foo(): + 2 + pass + | + 1 file would be reformatted ----- stderr ----- @@ -859,7 +916,13 @@ OTHER = "OTHER" success: false exit_code: 1 ----- stdout ----- - Would reformat: main.py + unformatted: File would be reformatted + --> main.py:1:1 + | + - + 1 | from test import say_hy + | + 1 file would be reformatted ----- stderr ----- @@ -2440,25 +2503,6 @@ fn cookiecutter_globbing() -> Result<()> { Ok(()) } -#[test] -fn stable_output_format_warning() -> Result<()> { - let test = CliTest::new()?; - assert_cmd_snapshot!( - test.format_command() - .args(["--output-format=full", "-"]) - .pass_stdin(""), - @" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - warning: The --output-format flag for the formatter is unstable and requires preview mode to use. - ", - ); - Ok(()) -} - #[test] fn markdown_formatting_preview_disabled() -> Result<()> { let test = CliTest::new()?; From 02d7fa47507026b746fef4d3e518e14de6d6cdc9 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:32:48 -0400 Subject: [PATCH 014/390] Stabilize optional filename and location in JSON output (#26957) Summary -- This PR closes #19263 by stabilizing the part of the JSON output changes that prompted me to open the issue. The remaining uses of `DisplayDiagnosticConfig::preview` relate to human-readable names and severity. I don't think it would really hurt in this case to stabilize all of the differences, but I preserved the separation between the earlier location/filename changes and name/severity for now. Test Plan -- Updated snapshots --- crates/ruff_db/src/diagnostic/render/json.rs | 110 +++++-------------- 1 file changed, 26 insertions(+), 84 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/json.rs b/crates/ruff_db/src/diagnostic/render/json.rs index 18debc4cbf..dba41f8a0b 100644 --- a/crates/ruff_db/src/diagnostic/render/json.rs +++ b/crates/ruff_db/src/diagnostic/render/json.rs @@ -93,48 +93,38 @@ pub(super) fn diagnostic_to_json<'a>( edits: ExpandedEdits { edits: fix.edits(), notebook_index, - config, diagnostic_source, }, }); - // In preview, the locations and filename can be optional - // and the severity is displayed. - if config.preview { - JsonDiagnostic { - code: diagnostic.secondary_code().map(|code| code.as_str()), - name: diagnostic.id().as_str(), - severity: diagnostic.severity(), - url: diagnostic.documentation_url(), - message: diagnostic.concise_message(), - fix, - cell: notebook_cell_index, - location: start_location.map(JsonLocation::from), - end_location: end_location.map(JsonLocation::from), - filename, - noqa_row: noqa_location.map(|location| location.line), - } + // In preview, the code can be optional and the severity is displayed. + let (code, severity) = if config.preview { + ( + diagnostic.secondary_code().map(|code| code.as_str()), + diagnostic.severity(), + ) } else { - JsonDiagnostic { - code: Some(diagnostic.secondary_code_or_id()), - name: diagnostic.id().as_str(), - severity: Severity::Error, - url: diagnostic.documentation_url(), - message: diagnostic.concise_message(), - fix, - cell: notebook_cell_index, - location: Some(start_location.unwrap_or_default().into()), - end_location: Some(end_location.unwrap_or_default().into()), - filename: Some(filename.unwrap_or_default()), - noqa_row: noqa_location.map(|location| location.line), - } + (Some(diagnostic.secondary_code_or_id()), Severity::Error) + }; + + JsonDiagnostic { + code, + name: diagnostic.id().as_str(), + severity, + url: diagnostic.documentation_url(), + message: diagnostic.concise_message(), + fix, + cell: notebook_cell_index, + location: start_location.map(JsonLocation::from), + end_location: end_location.map(JsonLocation::from), + filename, + noqa_row: noqa_location.map(|location| location.line), } } struct ExpandedEdits<'a> { edits: &'a [Edit], notebook_index: Option, - config: &'a DisplayDiagnosticConfig, diagnostic_source: Option, } @@ -199,19 +189,10 @@ impl Serialize for ExpandedEdits<'_> { (None, None) }; - // In preview, the locations can be optional. - let value = if self.config.preview { - JsonEdit { - content: edit.content().unwrap_or_default(), - location: location.map(JsonLocation::from), - end_location: end_location.map(JsonLocation::from), - } - } else { - JsonEdit { - content: edit.content().unwrap_or_default(), - location: Some(location.unwrap_or_default().into()), - end_location: Some(end_location.unwrap_or_default().into()), - } + let value = JsonEdit { + content: edit.content().unwrap_or_default(), + location: location.map(JsonLocation::from), + end_location: end_location.map(JsonLocation::from), }; s.serialize_element(&value)?; @@ -298,7 +279,7 @@ mod tests { } #[test] - fn missing_file_stable() { + fn missing_file() { let mut env = TestEnvironment::new(); env.format(DiagnosticFormat::Json); env.preview(false); @@ -315,45 +296,6 @@ mod tests { { "cell": null, "code": "test-diagnostic", - "end_location": { - "column": 1, - "row": 1 - }, - "filename": "", - "fix": null, - "location": { - "column": 1, - "row": 1 - }, - "message": "main diagnostic message", - "name": "test-diagnostic", - "noqa_row": null, - "severity": "error", - "url": "https://docs.astral.sh/ruff/rules/test-diagnostic" - } - ] - "#, - ); - } - - #[test] - fn missing_file_preview() { - let mut env = TestEnvironment::new(); - env.format(DiagnosticFormat::Json); - env.preview(true); - - let diag = env - .err() - .documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic") - .build(); - - insta::assert_snapshot!( - env.render(&diag), - @r#" - [ - { - "cell": null, - "code": null, "end_location": null, "filename": null, "fix": null, From 5058564eb53204765dd5175b2afccf839f4e98f9 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:59:08 -0400 Subject: [PATCH 015/390] Remove `DisplayDiagnosticConfig::show_fix_diff` (#27006) Summary -- Follow up to https://github.com/astral-sh/ruff/pull/26958#discussion_r3607966160. This setting was now always* set to true and only read in the `full` output format, so we can just remove it. The few cases where it wasn't already set to true were in tests, but it seems fine or even preferable to show the fixes there too. Test Plan -- Updated existing snapshots --- crates/mdtest/src/lib.rs | 1 - crates/ruff/src/commands/format.rs | 1 - crates/ruff/src/printer.rs | 6 +-- crates/ruff_db/src/diagnostic/mod.rs | 13 ------ crates/ruff_db/src/diagnostic/render.rs | 6 --- crates/ruff_db/src/diagnostic/render/full.rs | 34 ++++++++++++---- crates/ruff_linter/src/test.rs | 2 - crates/ty/src/lib.rs | 1 - crates/ty_ide/src/code_action.rs | 1 - crates/ty_ide/src/inlay_hints.rs | 1 - crates/ty_python_semantic/src/fixes.rs | 43 ++++++++++++++++++++ 11 files changed, 71 insertions(+), 38 deletions(-) diff --git a/crates/mdtest/src/lib.rs b/crates/mdtest/src/lib.rs index a1d345dd39..24a41dcb0f 100644 --- a/crates/mdtest/src/lib.rs +++ b/crates/mdtest/src/lib.rs @@ -317,7 +317,6 @@ impl TestFile<'_> { pub(crate) fn diagnostic_display_config(tool_name: &'static str) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig::new(tool_name) .color(false) - .show_fix_diff(true) .with_fix_applicability(Applicability::DisplayOnly) // Surrounding context in source annotations can be confusing in mdtests, // since you may get to see context from the *subsequent* code block (all diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 8a10f52d1b..685eac33cc 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -601,7 +601,6 @@ impl<'a> FormatResults<'a> { let context = EmitterContext::new(¬ebook_index); let config = DisplayDiagnosticConfig::new("ruff") .hide_severity(true) - .show_fix_diff(true) .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()); render_diagnostics(f, output_format, config, &context, &diagnostics) diff --git a/crates/ruff/src/printer.rs b/crates/ruff/src/printer.rs index d7c83a701e..4048cd198a 100644 --- a/crates/ruff/src/printer.rs +++ b/crates/ruff/src/printer.rs @@ -240,8 +240,7 @@ impl Printer { .hide_severity(true) .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) - .with_fix_applicability(self.unsafe_fixes.required_applicability()) - .show_fix_diff(true); + .with_fix_applicability(self.unsafe_fixes.required_applicability()); render_diagnostics(writer, self.format, config, &context, &diagnostics.inner)?; @@ -414,8 +413,7 @@ impl Printer { .hide_severity(true) .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) - .with_fix_applicability(self.unsafe_fixes.required_applicability()) - .show_fix_diff(true); + .with_fix_applicability(self.unsafe_fixes.required_applicability()); render_diagnostics(writer, self.format, config, &context, &diagnostics.inner)?; } writer.flush()?; diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index f93aa608ac..22404e435b 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -1438,10 +1438,6 @@ pub struct DisplayDiagnosticConfig { hide_severity: bool, /// Whether to show the availability of a fix in a diagnostic. show_fix_status: bool, - /// Whether to show the diff for an available fix after the main diagnostic. - /// - /// This currently only applies to `DiagnosticFormat::Full`. - show_fix_diff: bool, /// The lowest applicability that should be shown when reporting diagnostics. fix_applicability: Applicability, @@ -1461,7 +1457,6 @@ impl DisplayDiagnosticConfig { preview: false, hide_severity: false, show_fix_status: false, - show_fix_diff: false, fix_applicability: Applicability::Safe, cancellation_token: None, } @@ -1540,14 +1535,6 @@ impl DisplayDiagnosticConfig { } } - /// Whether to show a diff for an available fix after the main diagnostic. - pub fn show_fix_diff(self, yes: bool) -> DisplayDiagnosticConfig { - DisplayDiagnosticConfig { - show_fix_diff: yes, - ..self - } - } - /// Set the lowest fix applicability that should be shown. /// /// In other words, an applicability of `Safe` (the default) would suppress showing fixes or fix diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index 58adea92a2..058dc7eaa8 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -2637,12 +2637,6 @@ watermelon self.config = config.with_show_fix_status(yes); } - /// Show a diff for the fix when rendering. - pub(super) fn show_fix_diff(&mut self, yes: bool) { - let config = self.config.clone(); - self.config = config.show_fix_diff(yes); - } - /// The lowest fix applicability to show when rendering. pub(super) fn fix_applicability(&mut self, applicability: Applicability) { let config = self.config.clone(); diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 6c3b20c81e..4d2f1d7343 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -64,8 +64,7 @@ impl<'a> FullRenderer<'a> { writeln!(f, "{}", renderer.render(diag.to_annotate()))?; } - if self.config.show_fix_diff - && diag.has_applicable_fix(self.config.fix_applicability()) + if diag.has_applicable_fix(self.config.fix_applicability()) && let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver, self.config) { @@ -480,6 +479,11 @@ mod tests { | ^^ | help: Remove unused import: `os` + | + - import os + 1 | + | + note: This is an unsafe fix and may change runtime behavior F841 [*] Local variable `x` is assigned to but never used --> fib.py:6:5 @@ -492,6 +496,13 @@ mod tests { 8 | return 0 | help: Remove assignment to unused variable `x` + | + 5 | """Compute the nth number in the Fibonacci sequence.""" + - x = 1 + 6 + + 7 | if n == 0: + | + note: This is an unsafe fix and may change runtime behavior F821 Undefined name `a` --> undef.py:1:4 @@ -709,7 +720,7 @@ print() fn notebook_output() { let (mut env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full); env.show_fix_status(true); - insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r###" + insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @" error[F401][*]: `os` imported but unused --> notebook.ipynb:cell 1:2:8 | @@ -718,6 +729,11 @@ print() | ^^ | help: Remove unused import: `os` + ::: cell 1 + | + 1 | # cell 1 + - import os + | error[F401][*]: `math` imported but unused --> notebook.ipynb:cell 2:2:8 @@ -729,6 +745,12 @@ print() 4 | print('hello world') | help: Remove unused import: `math` + ::: cell 2 + | + 1 | # cell 2 + - import math + 2 | + | error[F841]: Local variable `x` is assigned to but never used --> notebook.ipynb:cell 3:4:5 @@ -739,7 +761,7 @@ print() | ^ | help: Remove assignment to unused variable `x` - "###); + "); } /// Check notebook handling for multiple annotations in a single diagnostic that span cells. @@ -821,7 +843,6 @@ print() #[test] fn notebook_output_with_diff() { let (mut env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full); - env.show_fix_diff(true); env.show_fix_status(true); env.fix_applicability(Applicability::DisplayOnly); @@ -831,7 +852,6 @@ print() #[test] fn notebook_output_with_diff_spanning_cells() { let (mut env, mut diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full); - env.show_fix_diff(true); env.show_fix_status(true); env.fix_applicability(Applicability::DisplayOnly); @@ -984,7 +1004,6 @@ line 10 "; env.add("example.py", contents); env.format(DiagnosticFormat::Full); - env.show_fix_diff(true); env.show_fix_status(true); env.fix_applicability(Applicability::DisplayOnly); @@ -1042,7 +1061,6 @@ line 13 env.format(DiagnosticFormat::Full); env.context(0); env.merge_window(2); - env.show_fix_diff(true); let replacement = |target: &str| { let start = contents.find(target).unwrap(); diff --git a/crates/ruff_linter/src/test.rs b/crates/ruff_linter/src/test.rs index d48155c728..2a54ac67e4 100644 --- a/crates/ruff_linter/src/test.rs +++ b/crates/ruff_linter/src/test.rs @@ -486,7 +486,6 @@ pub(crate) fn print_jupyter_messages( .format(DiagnosticFormat::Full) .hide_severity(true) .with_show_fix_status(true) - .show_fix_diff(true) .with_fix_applicability(Applicability::DisplayOnly); DisplayDiagnostics::new( @@ -505,7 +504,6 @@ pub(crate) fn print_messages(diagnostics: &[Diagnostic]) -> String { .format(DiagnosticFormat::Full) .hide_severity(true) .with_show_fix_status(true) - .show_fix_diff(true) .with_fix_applicability(Applicability::DisplayOnly); DisplayDiagnostics::new( diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index fe2bb4f507..4bfcf42d19 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -519,7 +519,6 @@ impl MainLoop { .format(terminal_settings.output_format.into()) .color(colored::control::SHOULD_COLORIZE.should_colorize()) .with_cancellation_token(Some(self.cancellation_token.clone())) - .show_fix_diff(true) .context(0); write!( diff --git a/crates/ty_ide/src/code_action.rs b/crates/ty_ide/src/code_action.rs index cacbcc7880..2f28043eec 100644 --- a/crates/ty_ide/src/code_action.rs +++ b/crates/ty_ide/src/code_action.rs @@ -965,7 +965,6 @@ mod tests { let config = DisplayDiagnosticConfig::new("ty") .color(false) - .show_fix_diff(true) .context(0) .format(DiagnosticFormat::Full); diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 242e89a913..4fcc167e2c 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -977,7 +977,6 @@ Source with applied edits: let config = DisplayDiagnosticConfig::new("ty") .color(false) - .show_fix_diff(true) .context(0) .format(DiagnosticFormat::Full); diff --git a/crates/ty_python_semantic/src/fixes.rs b/crates/ty_python_semantic/src/fixes.rs index 991416bde3..f6c28656c9 100644 --- a/crates/ty_python_semantic/src/fixes.rs +++ b/crates/ty_python_semantic/src/fixes.rs @@ -900,6 +900,11 @@ mod tests { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | help: Remove the unused suppression comment + | + 1 | import sys + - a = 5 + 10 # ty: ignore[unresolved-reference] + 2 + a = 5 + 10 + | "); } @@ -1146,6 +1151,12 @@ class B(A): 9 | b: str | help: Remove the unused suppression code + | + 6 | class B(A): + - def test( # ty:ignore[unresolved-reference, invalid-method-override] + 7 + def test( # ty:ignore[invalid-method-override] + 8 | self, + | "#); } @@ -1191,6 +1202,10 @@ class B(A): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | help: Remove the unused suppression comment + | + - value = missing # ty: ignore[] tracked by [123] # ty: ignore[unresolved-reference] + 1 + value = missing # ty: ignore[unresolved-reference] + | " ); } @@ -1233,6 +1248,11 @@ class B(A): 6 | ] | help: Remove the unused suppression comment + | + 3 | values = [ + - # ty: ignore[] tracked by [123] + 4 | missing, + | " ); } @@ -1269,6 +1289,12 @@ class B(A): 3 | value = 1 / 0 | help: Remove the unused suppression comment + | + 1 | seen_code = True + - # ty: ignore[ignore-comment-unknown-rule] # ty: ignore[not-a-rule] # ty: ignore[division-by-zero] + 2 + # ty: ignore[ignore-comment-unknown-rule] # ty: ignore[not-a-rule] + 3 | value = 1 / 0 + | " ); } @@ -1323,6 +1349,11 @@ class B(A): | ^^^^^^^^^^^^^^^^ | help: Remove the unused suppression code + | + 2 | + - result: int = f(missing) # ty: ignore[division-by-zero, invalid-assignment, too-many-positional-arguments, unresolved-reference] + 3 + result: int = f(missing) # ty: ignore[invalid-assignment, too-many-positional-arguments, unresolved-reference] + | "# ); } @@ -1548,6 +1579,12 @@ class B(A): 4 | # ty: ignore[invalid-argument-type, unresolved-reference] | help: Remove the unused suppression code + | + 1 | seen_code = True + - # ty: ignore[too-many-positional-arguments, unresolved-reference] + 2 + # ty: ignore[unresolved-reference] + 3 | values = [ + | warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-argument-type' --> test.py:4:18 @@ -1560,6 +1597,12 @@ class B(A): 6 | absent, | help: Remove the unused suppression code + | + 3 | values = [ + - # ty: ignore[invalid-argument-type, unresolved-reference] + 4 + # ty: ignore[unresolved-reference] + 5 | missing, + | " ); } From 1bb4bb0609717ea4c8891eca3b4fd0454b043fbe Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:38:02 -0400 Subject: [PATCH 016/390] Stabilize Markdown formatting (#27018) Summary -- This PR stabilizes the Markdown formatting initially added in #22470. It also reverts much of the error plumbing in the LSP that was added in #24150. I considered keeping the `FormatResult::PreviewOnly` variant but went ahead and dropped it since it was unused. Closes #26866. Test Plan -- Updated existing snapshots, dropping `--preview` in some cases --- crates/ruff/src/commands/format.rs | 32 +---------------- crates/ruff/tests/cli/format.rs | 34 ++++--------------- ...ires_python_extend_from_shared_config.snap | 1 + .../cli__lint__requires_python_no_tool.snap | 1 + ...ython_no_tool_target_version_override.snap | 1 + ..._requires_python_pyproject_toml_above.snap | 1 + ...python_pyproject_toml_above_with_tool.snap | 1 + ...nt__requires_python_ruff_toml_above-2.snap | 1 + ...lint__requires_python_ruff_toml_above.snap | 1 + ...s_python_ruff_toml_no_target_fallback.snap | 1 + ...ow_settings__display_default_settings.snap | 1 + ...isplay_settings_from_nested_directory.snap | 1 + crates/ruff_server/src/format.rs | 10 +----- .../server/api/requests/execute_command.rs | 2 +- .../src/server/api/requests/format.rs | 34 +++++-------------- .../ruff_server/tests/e2e/custom_extension.rs | 4 --- crates/ruff_workspace/src/configuration.rs | 30 +++++++--------- crates/ruff_workspace/src/settings.rs | 1 + docs/editors/features.md | 12 ------- docs/formatter.md | 2 -- 20 files changed, 41 insertions(+), 130 deletions(-) diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 685eac33cc..61fd5474da 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -489,12 +489,6 @@ pub(crate) fn format_source( ))) } SourceKind::Markdown(unformatted_document) => { - if !settings.preview.is_enabled() { - return Err(FormatCommandError::MarkdownExperimental( - path.map(Path::to_path_buf), - )); - } - if range.is_some() { return Err(FormatCommandError::RangeFormatNotSupported( path.map(Path::to_path_buf), @@ -829,7 +823,6 @@ pub(crate) enum FormatCommandError { Format(Option, FormatModuleError), Write(Option, SourceError), RangeFormatNotSupported(Option), - MarkdownExperimental(Option), } impl FormatCommandError { @@ -847,8 +840,7 @@ impl FormatCommandError { | Self::Read(path, _) | Self::Format(path, _) | Self::Write(path, _) - | Self::RangeFormatNotSupported(path) - | Self::MarkdownExperimental(path) => path.as_deref(), + | Self::RangeFormatNotSupported(path) => path.as_deref(), } } } @@ -885,11 +877,6 @@ impl From<&FormatCommandError> for Diagnostic { Severity::Error, "Range formatting is only supported for Python files.", ), - FormatCommandError::MarkdownExperimental(_) => Diagnostic::new( - DiagnosticId::PreviewFeature, - Severity::Warning, - "Markdown formatting is experimental, enable preview mode.", - ), }; if let Some(annotation) = annotation { @@ -984,23 +971,6 @@ impl Display for FormatCommandError { ) } } - Self::MarkdownExperimental(path) => { - if let Some(path) = path { - write!( - f, - "{header}{path}{colon} Markdown formatting is experimental, enable preview mode.", - header = "Failed to format ".bold(), - path = fs::relativize_path(path).bold(), - colon = ":".bold() - ) - } else { - write!( - f, - "{header} Markdown formatting is experimental, enable preview mode", - header = "Failed to format:".bold() - ) - } - } Self::Panic(path, err) => { let message = r"This indicates a bug in Ruff. If you could open an issue at: diff --git a/crates/ruff/tests/cli/format.rs b/crates/ruff/tests/cli/format.rs index 0395d10ff1..81e1ef4462 100644 --- a/crates/ruff/tests/cli/format.rs +++ b/crates/ruff/tests/cli/format.rs @@ -2504,31 +2504,12 @@ fn cookiecutter_globbing() -> Result<()> { } #[test] -fn markdown_formatting_preview_disabled() -> Result<()> { +fn markdown_formatting() -> Result<()> { let test = CliTest::new()?; let unformatted = test.fixture_path("unformatted.md"); assert_cmd_snapshot!(test.format_command() - .args(["--isolated", "--no-preview", "--diff"]) - .arg(unformatted), - @" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - error: Failed to format CRATE_ROOT/resources/test/fixtures/unformatted.md: Markdown formatting is experimental, enable preview mode. - "); - Ok(()) -} - -#[test] -fn markdown_formatting_preview_enabled() -> Result<()> { - let test = CliTest::new()?; - let unformatted = test.fixture_path("unformatted.md"); - - assert_cmd_snapshot!(test.format_command() - .args(["--isolated", "--preview", "--check"]) + .args(["--isolated", "--check"]) .arg(unformatted), @r#" success: false @@ -2570,7 +2551,7 @@ fn markdown_formatting_stdin() -> Result<()> { let unformatted = fs::read(test.fixture_path("unformatted.md")).unwrap(); assert_cmd_snapshot!(test.format_command() - .args(["--isolated", "--preview", "--stdin-filename", "unformatted.md"]) + .args(["--isolated", "--stdin-filename", "unformatted.md"]) .arg("-") .pass_stdin(unformatted), @r#" success: true @@ -2613,7 +2594,7 @@ print( 'hello' ) ])?; assert_cmd_snapshot!( - test.format_command().args(["--preview", "--diff", "test.qmd"]), + test.format_command().args(["--diff", "test.qmd"]), @r#" success: false exit_code: 1 @@ -2666,14 +2647,11 @@ print( 'hello' ) assert_cmd_snapshot!( test.format_command() - .args(["format", "--preview", "--check", "."]), + .args(["--check", "."]), @r#" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- - io: [TMP]/format: No such file or directory (os error 2) - --> format:1:1 - unformatted: File would be reformatted --> test.bar:1:1 | diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap index b4a34f4c7b..682462cd1d 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap @@ -61,6 +61,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap index fafd0ded8c..b45927e8df 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap @@ -63,6 +63,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap index 48c3d81fed..66a7714e49 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap @@ -65,6 +65,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap index 99f233b509..0b3f80819c 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap @@ -62,6 +62,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap index a9727552e0..5f376667d4 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap @@ -63,6 +63,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap index 72982b9649..0f9960f3fe 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap @@ -61,6 +61,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap index 7a50948dfc..cef5cf6988 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap @@ -61,6 +61,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap index 60137f66c8..14a16079b6 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap @@ -61,6 +61,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap index 882ccb119a..4f6d0ed870 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap @@ -58,6 +58,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap index f155e549bc..dafb9e2db8 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap @@ -58,6 +58,7 @@ file_resolver.include = [ "**/pyproject.toml", "**/ruff.toml", "**/.ruff.toml", + "*.md", ] file_resolver.extend_include = [] file_resolver.respect_gitignore = true diff --git a/crates/ruff_server/src/format.rs b/crates/ruff_server/src/format.rs index d5ddb2ecb0..770c2e6e85 100644 --- a/crates/ruff_server/src/format.rs +++ b/crates/ruff_server/src/format.rs @@ -33,14 +33,13 @@ pub(crate) enum FormatBackend { pub(crate) enum FormatResult { Formatted(String), Unchanged, - PreviewOnly { file_format: &'static str }, } impl FormatResult { fn into_formatted(self) -> Option { match self { Self::Formatted(formatted) => Some(formatted), - Self::Unchanged | Self::PreviewOnly { .. } => None, + Self::Unchanged => None, } } } @@ -91,13 +90,6 @@ fn format_internal( } } SourceType::Markdown => { - if !formatter_settings.preview.is_enabled() { - tracing::warn!("Markdown formatting is experimental, enable preview mode."); - return Ok(FormatResult::PreviewOnly { - file_format: "Markdown", - }); - } - match format_code_blocks(document.contents(), Some(path), formatter_settings) { MarkdownResult::Formatted(formatted) => Ok(FormatResult::Formatted(formatted)), MarkdownResult::Unchanged => Ok(FormatResult::Unchanged), diff --git a/crates/ruff_server/src/server/api/requests/execute_command.rs b/crates/ruff_server/src/server/api/requests/execute_command.rs index 9bfc8cec54..8fcb03e8c6 100644 --- a/crates/ruff_server/src/server/api/requests/execute_command.rs +++ b/crates/ruff_server/src/server/api/requests/execute_command.rs @@ -99,7 +99,7 @@ impl super::SyncRequestHandler for ExecuteCommand { .with_failure_code(ErrorCode::InternalError)?; } SupportedCommand::Format => { - let fixes = super::format::format_full_document(&snapshot, client)?; + let fixes = super::format::format_full_document(&snapshot)?; edit_tracker .set_fixes_for_document(fixes, version) .with_failure_code(ErrorCode::InternalError)?; diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index 4c2fb585cf..3482565ddc 100644 --- a/crates/ruff_server/src/server/api/requests/format.rs +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -24,7 +24,7 @@ impl super::BackgroundDocumentRequestHandler for Format { fn run_with_snapshot( snapshot: Self::Snapshot, - client: &Client, + _client: &Client, _params: types::DocumentFormattingParams, ) -> Result { let snapshot = match snapshot { @@ -37,12 +37,12 @@ impl super::BackgroundDocumentRequestHandler for Format { } }; - format_document(&snapshot, client) + format_document(&snapshot) } } /// Formats either a full text document or each individual cell in a single notebook document. -pub(super) fn format_full_document(snapshot: &DocumentSnapshot, client: &Client) -> Result { +pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result { let mut fixes = Fixes::default(); let query = snapshot.query(); let backend = snapshot @@ -56,21 +56,16 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot, client: &Client) .uris() .map(|uri| (uri.clone(), notebook.cell_document_by_uri(uri).unwrap())) { - if let Some(changes) = format_text_document( - text_document, - query, - snapshot.encoding(), - true, - backend, - client, - )? { + if let Some(changes) = + format_text_document(text_document, query, snapshot.encoding(), true, backend)? + { fixes.insert(uri, changes); } } } DocumentQuery::Text { document, .. } => { if let Some(changes) = - format_text_document(document, query, snapshot.encoding(), false, backend, client)? + format_text_document(document, query, snapshot.encoding(), false, backend)? { fixes.insert(snapshot.query().make_key().into_uri(), changes); } @@ -82,10 +77,7 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot, client: &Client) /// Formats either a full text document or an specific notebook cell. If the query within the snapshot is a notebook document /// with no selected cell, this will throw an error. -pub(super) fn format_document( - snapshot: &DocumentSnapshot, - client: &Client, -) -> Result { +pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result { let text_document = snapshot .query() .as_single_document() @@ -102,7 +94,6 @@ pub(super) fn format_document( snapshot.encoding(), query.as_notebook().is_some(), backend, - client, ) } @@ -112,7 +103,6 @@ fn format_text_document( encoding: PositionEncoding, is_notebook: bool, backend: crate::format::FormatBackend, - client: &Client, ) -> Result { let settings = query.settings(); let file_path = query.virtual_file_path(); @@ -140,14 +130,6 @@ fn format_text_document( let mut formatted = match formatted { FormatResult::Formatted(formatted) => formatted, FormatResult::Unchanged => return Ok(None), - FormatResult::PreviewOnly { file_format } => { - client.show_warning_message( - format_args!( - "{file_format} formatting is available only in preview mode. Enable `format.preview = true` in your Ruff configuration." - ), - ); - return Ok(None); - } }; // special case - avoid adding a newline to a notebook cell if it didn't already exist diff --git a/crates/ruff_server/tests/e2e/custom_extension.rs b/crates/ruff_server/tests/e2e/custom_extension.rs index 3d9ebad714..87ad69ba91 100644 --- a/crates/ruff_server/tests/e2e/custom_extension.rs +++ b/crates/ruff_server/tests/e2e/custom_extension.rs @@ -5,11 +5,7 @@ use lsp_types::{Position, Range}; use crate::TestServerBuilder; const CUSTOM_EXTENSION_CONFIG: &str = r#"[tool.ruff] -preview = true extension = { thing = "markdown" } - -[tool.ruff.format] -preview = true "#; const CUSTOM_EXTENSION_MARKDOWN: &str = "# title\n\n```python\nx='hi'\n```\n"; diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 75180b8b4e..bcd12ce60b 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -335,24 +335,20 @@ impl Configuration { extend_exclude: FilePatternSet::try_from_iter(self.extend_exclude)?, extend_include: FilePatternSet::try_from_iter(self.extend_include)?, force_exclude: self.force_exclude.unwrap_or(false), - include: match global_preview { - PreviewMode::Disabled => FilePatternSet::try_from_iter( - self.include.unwrap_or_else(|| INCLUDE.to_vec()), - )?, - PreviewMode::Enabled => { - FilePatternSet::try_from_iter(self.include.unwrap_or_else(|| { - let mut patterns = INCLUDE_PREVIEW.to_vec(); - if let Some(extension_map) = &self.extension { - patterns.extend( - extension_map - .extensions() - .map(|ext| FilePattern::Config(format!("*.{ext}"))), - ); - } - patterns - }))? + include: FilePatternSet::try_from_iter(self.include.unwrap_or_else(|| { + let mut patterns = match global_preview { + PreviewMode::Disabled => INCLUDE.to_vec(), + PreviewMode::Enabled => INCLUDE_PREVIEW.to_vec(), + }; + if let Some(extension_map) = &self.extension { + patterns.extend( + extension_map + .extensions() + .map(|ext| FilePattern::Config(format!("*.{ext}"))), + ); } - }, + patterns + }))?, respect_gitignore: self.respect_gitignore.unwrap_or(true), project_root: project_root.to_path_buf(), }, diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index a1c3dd4af2..a0b137ba87 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -145,6 +145,7 @@ pub(crate) static INCLUDE: &[FilePattern] = &[ FilePattern::Builtin("**/pyproject.toml"), FilePattern::Builtin("**/ruff.toml"), FilePattern::Builtin("**/.ruff.toml"), + FilePattern::Builtin("*.md"), ]; pub(crate) static INCLUDE_PREVIEW: &[FilePattern] = &[ FilePattern::Builtin("*.py"), diff --git a/docs/editors/features.md b/docs/editors/features.md index ef95a70601..164d049594 100644 --- a/docs/editors/features.md +++ b/docs/editors/features.md @@ -41,9 +41,6 @@ alt="Formatting a document in VS Code" ### Markdown code blocks -*This feature is currently only available in -[preview mode](https://docs.astral.sh/ruff/preview/#preview).* - The Ruff formatter can also format Python code blocks in Markdown files. The Ruff VS Code extension provides the `Format Document` command for Markdown files, which will then format the code blocks with the same settings @@ -55,15 +52,6 @@ then you will need to set one as the default in VS Code, and manually run the `Format Document With...` (or `Ruff: Format document`) command to run any other formatters separately. -To enable preview mode for formatting in VS Code, add the following to your -`settings.json`: - -```json -{ - "ruff.format.preview": true, -} -``` - To set Ruff as the default formatter for Markdown files in VS Code, add the following to your `settings.json`: diff --git a/docs/formatter.md b/docs/formatter.md index aee65ee133..c03343fe26 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -227,8 +227,6 @@ def f(x): ## Markdown code formatting -*This feature is currently only available in [preview mode](preview.md#preview).* - The Ruff formatter can also format Python code blocks in Markdown files. In these files, Ruff will format any CommonMark [fenced code blocks][] with the following info strings: `python`, `py`, `python3`, `py3`, or `pyi`. The From 9dcee31c3a34e40b080ee6a50a12261a137262a5 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:41:49 -0400 Subject: [PATCH 017/390] [`ruff`] Stabilize `duplicate-entry-in-dunder-all` (`RUF068`) (#26895) --- .../src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs index fceb6ddf74..faa75a9182 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs @@ -48,7 +48,7 @@ use crate::{FixAvailability, Violation}; /// ] /// ``` #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.14.14")] +#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] pub(crate) struct DuplicateEntryInDunderAll; impl Violation for DuplicateEntryInDunderAll { From 8fed943f13aff3eb8fa9837ffadcd97337daf481 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:42:04 -0400 Subject: [PATCH 018/390] [`flake8-implicit-str-concat`] Stabilize `implicit-string-concatenation-in-collection-literal` (`ISC004`) (#26901) Small tweak to the docs but otherwise looked good --- .../flake8_implicit_str_concat/rules/collection_literal.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs index 6d52ce7d2e..bb0eccc844 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs @@ -49,9 +49,9 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## Fix safety /// The fix is safe in that it does not change the semantics of your code. /// However, the issue is that you may often want to change semantics -/// by adding a missing comma. +/// by adding a missing comma. Thus, the fix is always marked as unsafe. #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.14.10")] +#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] pub(crate) struct ImplicitStringConcatenationInCollectionLiteral; impl Violation for ImplicitStringConcatenationInCollectionLiteral { From ff11ca00e7bf7380fb33b98b30cd8534f0f4e0a2 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:42:29 -0400 Subject: [PATCH 019/390] [`flake8-logging`] Stabilize `log-exception-outside-except-handler` (`LOG004`) (#26906) --- .../rules/log_exception_outside_except_handler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs index 31f5cbee39..3ded3afb3c 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs @@ -69,7 +69,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [The documentation]: https://docs.python.org/3/library/logging.html#logging.exception #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.9.5")] +#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] pub(crate) struct LogExceptionOutsideExceptHandler; impl Violation for LogExceptionOutsideExceptHandler { From 3e74e4480b4b043a8974260a5f0aca2c1df5462e Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:42:41 -0400 Subject: [PATCH 020/390] [`ruff`] Stabilize `none-not-at-end-of-union` (`RUF036`) (#26909) Very minor typo, otherwise looked good --- .../src/rules/ruff/rules/none_not_at_end_of_union.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs b/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs index a58c187987..4e359566c8 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs @@ -11,7 +11,7 @@ use crate::fix::edits::pad; use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does -/// Checks for type annotations where `None` is not at the end of an union. +/// Checks for type annotations where `None` is not at the end of a union. /// /// ## Why is this bad? /// Type annotation unions are commutative, meaning that the order of the elements @@ -33,7 +33,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `typing.Optional`](https://docs.python.org/3/library/typing.html#typing.Optional) /// - [Python documentation: `None`](https://docs.python.org/3/library/constants.html#None) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "0.7.4")] +#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] pub(crate) struct NoneNotAtEndOfUnion; impl Violation for NoneNotAtEndOfUnion { From 57236d6b9dacef87ba2c48501086ce692c2d70e5 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:43:11 -0400 Subject: [PATCH 021/390] [`refurb`] Stabilize `sorted-min-max` (`FURB192`) (#26910) https://docs.astral.sh/ruff/rules/sorted-min-max/ --- crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs b/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs index feb1670b20..ed36f55912 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs @@ -52,7 +52,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `min`](https://docs.python.org/3/library/functions.html#min) /// - [Python documentation: `max`](https://docs.python.org/3/library/functions.html#max) #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "v0.4.2")] +#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] pub(crate) struct SortedMinMax { min_max: MinMax, } From c1a11a0efc4020f0724f8675ba02e877daa2c346 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:43:34 -0400 Subject: [PATCH 022/390] [`pylint`] Stabilize `too-many-positional-arguments` (`PLR0917`) (#26915) Closes #16867 --- .../src/rules/pylint/rules/too_many_positional_arguments.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs index a0b0eaafe6..f241aa44a2 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs @@ -55,7 +55,7 @@ use crate::checkers::ast::Checker; /// /// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "v0.1.7")] +#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] pub(crate) struct TooManyPositionalArguments { c_pos: usize, max_pos: usize, From e2a0e570d864936246a588b1586caf05ce7530bc Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:47:02 -0400 Subject: [PATCH 023/390] [`flake8-copyright`] Stabilize `missing-copyright-notice` (`CPY001`) (#26921) This one required a couple of updates to existing tests because the rule was being used as a generic preview rule. I switched these tests to use `RUF911`, which is our `preview-test-rule`. I had also previously opened a tracking issue for this rule's stabilization in #19487, but as I noted in https://github.com/astral-sh/ruff/issues/19487#issuecomment-4454194086, I no longer feel like these issues really need to block stabilization. This rule is a commonly requested reason to enable specific preview rules and has been in preview for almost 3 years, so I think these other issues can be handled separately. I think we can close #19487 if we land this but keep the individual issues open. --- .../rules/missing_copyright_notice.rs | 2 +- crates/ruff_workspace/src/configuration.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs b/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs index b3f70a7f88..14ab6a6be6 100644 --- a/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs +++ b/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs @@ -20,7 +20,7 @@ use crate::settings::LinterSettings; /// - `lint.flake8-copyright.min-file-size` /// - `lint.flake8-copyright.notice-rgx` #[derive(ViolationMetadata)] -#[violation_metadata(preview_since = "v0.0.273")] +#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] pub(crate) struct MissingCopyrightNotice; impl Violation for MissingCopyrightNotice { diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index bcd12ce60b..9a558ddf49 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -2114,7 +2114,7 @@ mod tests { fn select_linter_preview() -> Result<()> { let actual = resolve_rules( [RuleSelection { - select: Some(vec![UnresolvedRuleSelector::cli("CPY")]), + select: Some(vec![UnresolvedRuleSelector::cli("RUF91")]), ..RuleSelection::default() }], Some(PreviewOptions { @@ -2127,7 +2127,7 @@ mod tests { let actual = resolve_rules( [RuleSelection { - select: Some(vec![UnresolvedRuleSelector::cli("CPY")]), + select: Some(vec![UnresolvedRuleSelector::cli("RUF91")]), ..RuleSelection::default() }], Some(PreviewOptions { @@ -2135,7 +2135,7 @@ mod tests { ..PreviewOptions::default() }), )?; - let expected = RuleSet::from_rule(Rule::MissingCopyrightNotice); + let expected = RuleSet::from_rule(Rule::PreviewTestRule); assert_eq!(actual, expected); Ok(()) } @@ -2144,7 +2144,7 @@ mod tests { fn select_prefix_preview() -> Result<()> { let actual = resolve_rules( [RuleSelection { - select: Some(vec![UnresolvedRuleSelector::cli("CPY0")]), + select: Some(vec![UnresolvedRuleSelector::cli("RUF91")]), ..RuleSelection::default() }], Some(PreviewOptions { @@ -2157,7 +2157,7 @@ mod tests { let actual = resolve_rules( [RuleSelection { - select: Some(vec![UnresolvedRuleSelector::cli("CPY0")]), + select: Some(vec![UnresolvedRuleSelector::cli("RUF91")]), ..RuleSelection::default() }], Some(PreviewOptions { @@ -2165,7 +2165,7 @@ mod tests { ..PreviewOptions::default() }), )?; - let expected = RuleSet::from_rule(Rule::MissingCopyrightNotice); + let expected = RuleSet::from_rule(Rule::PreviewTestRule); assert_eq!(actual, expected); Ok(()) } From 46d3e827fe0ad34329083d7b48f45ccc3f734c05 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:48:56 -0400 Subject: [PATCH 024/390] [`ruff`] Stabilize `ruff:ignore` suppressions (#26934) Codex tried to sneak in bug fixes for https://github.com/astral-sh/ruff/issues/26282, but I don't think these have to block stabilization since they haven't come up in the wild, and I plan to work on this issue after the minor release. --- crates/ruff/tests/cli/lint.rs | 3 +- .../resources/mdtest/suppression/ignore.md | 21 ----- crates/ruff_linter/src/noqa.rs | 2 +- crates/ruff_linter/src/preview.rs | 5 -- crates/ruff_linter/src/suppression.rs | 85 ++++++++----------- 5 files changed, 38 insertions(+), 78 deletions(-) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index e7a428edae..b5afa84b76 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -2685,7 +2685,6 @@ fn add_noqa_existing_ignore() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: #ruff:ignore comment found but not active, enable preview mode Added 1 noqa directive. ", ); @@ -2696,7 +2695,7 @@ fn add_noqa_existing_ignore() -> Result<()> { test_code, @" - def unused(x): # ruff:ignore[ANN001, ARG001, D103] # noqa: ANN001, ANN201, D103 + def unused(x): # ruff:ignore[ANN001, ARG001, D103] # noqa: ANN201 pass ", ); diff --git a/crates/ruff_linter/resources/mdtest/suppression/ignore.md b/crates/ruff_linter/resources/mdtest/suppression/ignore.md index 752f1dd757..2f9443e583 100644 --- a/crates/ruff_linter/resources/mdtest/suppression/ignore.md +++ b/crates/ruff_linter/resources/mdtest/suppression/ignore.md @@ -8,7 +8,6 @@ diagnostic range. ```toml [lint] -preview = true select = ["RUF015"] ``` @@ -29,7 +28,6 @@ entire class definition. ```toml [lint] -preview = true select = ["B903"] ``` @@ -46,7 +44,6 @@ Diagnostics with empty ranges should also be suppressible, as with `noqa`. ```toml [lint] -preview = true select = ["W292"] ``` @@ -61,7 +58,6 @@ after the matching `ruff:enable` comment: ```toml [lint] -preview = true select = ["RUF015"] ``` @@ -91,7 +87,6 @@ not_suppressed = [ ```toml [lint] -preview = true select = ["RUF015"] ``` @@ -153,7 +148,6 @@ x = ( ```toml [lint] -preview = true select = [ "F" ] ``` @@ -174,7 +168,6 @@ from foo import ( # ruff:ignore[F401] ```toml [lint] -preview = true select = [ "F401", "RUF100" ] ``` @@ -201,7 +194,6 @@ from sys import ( # ruff:ignore[F401] ```toml [lint] -preview = true select = [ "W291" ] ``` @@ -303,7 +295,6 @@ def f(): ```toml [lint] -preview = true select = ["F401", "RUF100", "RUF104"] ``` @@ -567,7 +558,6 @@ help: Remove unused suppression ```toml [lint] -preview = true select = ["F401", "RUF103", "RUF104"] ``` @@ -617,7 +607,6 @@ import foo ```toml [lint] -preview = true select = ["F401", "RUF103", "RUF104"] ``` @@ -667,7 +656,6 @@ import foo ```toml [lint] -preview = true select = ["F401", "RUF100", "FIX002"] ``` @@ -699,7 +687,6 @@ a = 10 ```toml [lint] -preview = true select = ["E501", "F821", "RUF100", "RUF103"] ``` @@ -773,7 +760,6 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] -preview = true select = ["E501", "RUF100", "FIX002"] ``` @@ -811,7 +797,6 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] -preview = true select = ["E501", "F401", "F821", "RUF100", "RUF103"] ``` @@ -845,7 +830,6 @@ help: Remove unused suppression ```toml [lint] -preview = true select = ["F821", "RUF102", "RUF103"] ``` @@ -881,7 +865,6 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] -preview = true select = ["F401", "F821", "RUF100", "RUF103"] ``` @@ -944,7 +927,6 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] -preview = true select = ["RUF103"] ``` @@ -993,7 +975,6 @@ note: This is an unsafe fix and may change runtime behavior ```toml [lint] -preview = true select = ["F401", "RUF103"] ``` @@ -1008,7 +989,6 @@ import os # before # ruff:ignore # ruff:ignore[F401] # after ```toml [lint] -preview = true select = ["RUF100"] ``` @@ -1061,7 +1041,6 @@ at offset zero, as with `noqa`. ```toml [lint] -preview = true select = ["D100"] ``` diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index 741f384c88..909e78e10a 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -3070,7 +3070,7 @@ mod tests { ## Fixed source ```py - def unused(x): # ruff:ignore[ANN001, ARG001, D103] # noqa: ANN001, ANN201, D103 + def unused(x): # ruff:ignore[ANN001, ARG001, D103] # noqa: ANN201 pass ``` " diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 7de6ef4411..03afb42290 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -356,11 +356,6 @@ pub(crate) const fn is_collapsible_if_fix_safe_enabled(settings: &LinterSettings settings.preview.is_enabled() } -// https://github.com/astral-sh/ruff/pull/23404 -pub(crate) const fn is_ruff_ignore_enabled(settings: &LinterSettings) -> bool { - settings.preview.is_enabled() -} - // https://github.com/astral-sh/ruff/pull/23259 pub(crate) const fn is_pep604_future_annotations_fix_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index fb04ee5b50..7d95fc5f5e 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -19,7 +19,7 @@ use crate::checkers::ast::{DiagnosticGuard, LintContext}; use crate::codes::Rule; use crate::comments::shebang::leading_shebang_range; use crate::fix::edits::delete_comment; -use crate::preview::{is_human_readable_names_enabled, is_ruff_ignore_enabled}; +use crate::preview::is_human_readable_names_enabled; use crate::rule_redirects::get_redirect_target; use crate::rules::ruff::rules::{ InvalidRuleCode, InvalidRuleCodeKind, InvalidSuppressionComment, InvalidSuppressionCommentKind, @@ -28,7 +28,7 @@ use crate::rules::ruff::rules::{ }; use crate::settings::LinterSettings; use crate::settings::types::PreviewMode; -use crate::{Locator, Violation, warn_user_once}; +use crate::{Locator, Violation}; #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] enum SuppressionAction { @@ -870,11 +870,9 @@ impl<'a> SuppressionsBuilder<'a> { ) -> bool { match suppression.action { SuppressionAction::Ignore => { - if is_ruff_ignore_enabled(self.settings) { - let (before, after) = tokens.split_at(suppression.token_range.start()); - let range = if indentation_at_offset(suppression.range.start(), self.source) - .is_some() - { + let (before, after) = tokens.split_at(suppression.token_range.start()); + let range = + if indentation_at_offset(suppression.range.start(), self.source).is_some() { // own-line ignore let mut range = Self::standalone_comment_range(suppression.range, before, after); @@ -891,55 +889,44 @@ impl<'a> SuppressionsBuilder<'a> { // trailing ignore self.trailing_comment_range(suppression.token_range, before) }; - for code in suppression.codes_as_str(self.source) { - self.valid.push(Suppression { - code: code.into(), - range, - used: false.into(), - comments: SuppressionComments::Single(suppression.clone()), - }); - } - } else { - warn_user_once!( - "#ruff:ignore comment found but not active, enable preview mode" - ); + for code in suppression.codes_as_str(self.source) { + self.valid.push(Suppression { + code: code.into(), + range, + used: false.into(), + comments: SuppressionComments::Single(suppression.clone()), + }); } true } SuppressionAction::FileIgnore => { - if is_ruff_ignore_enabled(self.settings) { - match indentation_at_offset(suppression.range.start(), self.source) { - // Module scope - Some("") => { - let range = TextRange::up_to(self.source.text_len()); - for code in suppression.codes_as_str(self.source) { - self.valid.push(Suppression { - code: code.into(), - range, - used: false.into(), - comments: SuppressionComments::Single(suppression.clone()), - }); - } - } - // Indented/inside block - Some(_) => { - self.invalid.push(InvalidSuppression { - kind: InvalidSuppressionKind::NotModuleScope, - comment: suppression.clone(), - }); - } - // Trailing - None => { - self.invalid.push(InvalidSuppression { - kind: InvalidSuppressionKind::Trailing, - comment: suppression.clone(), + match indentation_at_offset(suppression.range.start(), self.source) { + // Module scope + Some("") => { + let range = TextRange::up_to(self.source.text_len()); + for code in suppression.codes_as_str(self.source) { + self.valid.push(Suppression { + code: code.into(), + range, + used: false.into(), + comments: SuppressionComments::Single(suppression.clone()), }); } } - } else { - warn_user_once!( - "#ruff:file-ignore comment found but not active, enable preview mode" - ); + // Indented/inside block + Some(_) => { + self.invalid.push(InvalidSuppression { + kind: InvalidSuppressionKind::NotModuleScope, + comment: suppression.clone(), + }); + } + // Trailing + None => { + self.invalid.push(InvalidSuppression { + kind: InvalidSuppressionKind::Trailing, + comment: suppression.clone(), + }); + } } true } From ac74da7a22c0d768fed4bef9644c0a6e2a16ba09 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:37:47 -0400 Subject: [PATCH 025/390] [`flake8-blind-except`] Stabilize support for more logging methods (`BLE001`) (#26937) This change makes the [rule](https://docs.astral.sh/ruff/rules/blind-except/) a bit more permissive by allowing logging with more methods, as long as `exc_info` is attached. https://github.com/astral-sh/ruff/pull/22057 --- crates/ruff_linter/src/preview.rs | 5 -- .../src/rules/flake8_blind_except/mod.rs | 25 +------ .../flake8_blind_except/rules/blind_except.rs | 53 ++++----------- ...e8_blind_except__tests__BLE001_BLE.py.snap | 50 -------------- ..._except__tests__preview_BLE001_BLE.py.snap | 65 ------------------- 5 files changed, 12 insertions(+), 186 deletions(-) delete mode 100644 crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__preview_BLE001_BLE.py.snap diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 03afb42290..cd60591233 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -271,11 +271,6 @@ pub(crate) const fn is_s310_resolve_string_literal_bindings_enabled( settings.preview.is_enabled() } -// https://github.com/astral-sh/ruff/pull/22057 -pub(crate) const fn is_ble001_exc_info_suppression_enabled(settings: &LinterSettings) -> bool { - settings.preview.is_enabled() -} - // https://github.com/astral-sh/ruff/pull/22419 pub(crate) const fn is_py315_support_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() diff --git a/crates/ruff_linter/src/rules/flake8_blind_except/mod.rs b/crates/ruff_linter/src/rules/flake8_blind_except/mod.rs index 88f1c9099c..658908efb6 100644 --- a/crates/ruff_linter/src/rules/flake8_blind_except/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_blind_except/mod.rs @@ -9,9 +9,8 @@ mod tests { use test_case::test_case; use crate::registry::Rule; - use crate::settings::types::PreviewMode; use crate::test::test_path; - use crate::{assert_diagnostics, assert_diagnostics_diff, settings}; + use crate::{assert_diagnostics, settings}; #[test_case(Rule::BlindExcept, Path::new("BLE.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { @@ -23,26 +22,4 @@ mod tests { assert_diagnostics!(snapshot, diagnostics); Ok(()) } - - #[test_case(Rule::BlindExcept, Path::new("BLE.py"))] - fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { - let snapshot = format!( - "preview_{}_{}", - rule_code.noqa_code(), - path.to_string_lossy() - ); - assert_diagnostics_diff!( - snapshot, - Path::new("flake8_blind_except").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Disabled, - ..settings::LinterSettings::for_rule(rule_code) - }, - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, - ); - Ok(()) - } } diff --git a/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs b/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs index 1e85224bdf..f62788d800 100644 --- a/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs +++ b/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs @@ -8,9 +8,7 @@ use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; -use crate::preview::is_ble001_exc_info_suppression_enabled; use crate::rules::flake8_logging::helpers::is_logger_method_name; -use crate::settings::LinterSettings; /// ## What it does /// Checks for `except` clauses that catch all exceptions. This includes @@ -122,11 +120,7 @@ pub(crate) fn blind_except( } // If the exception is logged, don't flag an error. - let mut visitor = LogExceptionVisitor::new( - semantic, - &checker.settings().logger_objects, - checker.settings(), - ); + let mut visitor = LogExceptionVisitor::new(semantic, &checker.settings().logger_objects); visitor.visit_body(body); if visitor.seen() { return; @@ -191,43 +185,26 @@ impl<'a> StatementVisitor<'a> for ReraiseVisitor<'a> { } /// Returns `true` if the `exc_info` keyword argument is truthy. -fn is_exc_info_enabled( - method_name: &str, - arguments: &ast::Arguments, - semantic: &SemanticModel, - settings: &LinterSettings, -) -> bool { - if is_ble001_exc_info_suppression_enabled(settings) - || matches!(method_name, "error" | "critical") - { - arguments.find_keyword("exc_info").is_some_and(|keyword| { - Truthiness::from_expr(&keyword.value, |id| semantic.has_builtin_binding(id)).into_bool() - != Some(false) - }) - } else { - false - } +fn is_exc_info_enabled(arguments: &ast::Arguments, semantic: &SemanticModel) -> bool { + arguments.find_keyword("exc_info").is_some_and(|keyword| { + Truthiness::from_expr(&keyword.value, |id| semantic.has_builtin_binding(id)).into_bool() + != Some(false) + }) } /// A visitor to detect whether the exception was logged. struct LogExceptionVisitor<'a> { semantic: &'a SemanticModel<'a>, logger_objects: &'a [String], - settings: &'a LinterSettings, seen: bool, } impl<'a> LogExceptionVisitor<'a> { /// Create a new [`LogExceptionVisitor`] with the given exception name. - fn new( - semantic: &'a SemanticModel<'a>, - logger_objects: &'a [String], - settings: &'a LinterSettings, - ) -> Self { + fn new(semantic: &'a SemanticModel<'a>, logger_objects: &'a [String]) -> Self { Self { semantic, logger_objects, - settings, seen: false, } } @@ -257,12 +234,9 @@ impl<'a> StatementVisitor<'a> for LogExceptionVisitor<'a> { self.logger_objects, ) && match attr.as_str() { "exception" => true, - _ if is_logger_method_name(attr) => is_exc_info_enabled( - attr, - arguments, - self.semantic, - self.settings, - ), + _ if is_logger_method_name(attr) => { + is_exc_info_enabled(arguments, self.semantic) + } _ => false, } => { @@ -273,12 +247,7 @@ impl<'a> StatementVisitor<'a> for LogExceptionVisitor<'a> { |qualified_name| match qualified_name.segments() { ["logging", "exception"] => true, ["logging", method] if is_logger_method_name(method) => { - is_exc_info_enabled( - method, - arguments, - self.semantic, - self.settings, - ) + is_exc_info_enabled(arguments, self.semantic) } _ => false, }, diff --git a/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__BLE001_BLE.py.snap b/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__BLE001_BLE.py.snap index 5011575277..b8a84ab19b 100644 --- a/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__BLE001_BLE.py.snap +++ b/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__BLE001_BLE.py.snap @@ -263,53 +263,3 @@ BLE001 Do not catch blind exception: `BaseException` | ^^^^^^^^^^^^^ 221 | pass | - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:269:8 - | -267 | try: -268 | pass -269 | except Exception as e: - | ^^^^^^^^^ -270 | logging.debug("...", exc_info=e) # ok - | - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:275:8 - | -273 | try: -274 | pass -275 | except Exception: - | ^^^^^^^^^ -276 | logging.info("...", exc_info=True) # ok - | - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:280:8 - | -278 | try: -279 | pass -280 | except Exception as e: - | ^^^^^^^^^ -281 | logging.warn("...", exc_info=e) # ok - | - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:286:8 - | -284 | try: -285 | pass -286 | except Exception: - | ^^^^^^^^^ -287 | logging.warning("...", exc_info=True) # ok - | - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:292:8 - | -290 | try: -291 | pass -292 | except Exception as e: - | ^^^^^^^^^ -293 | logging.log(logging.INFO, "...", exc_info=e) # ok - | diff --git a/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__preview_BLE001_BLE.py.snap b/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__preview_BLE001_BLE.py.snap deleted file mode 100644 index 7fc7dd0407..0000000000 --- a/crates/ruff_linter/src/rules/flake8_blind_except/snapshots/ruff_linter__rules__flake8_blind_except__tests__preview_BLE001_BLE.py.snap +++ /dev/null @@ -1,65 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_blind_except/mod.rs ---- ---- Linter settings --- --linter.preview = disabled -+linter.preview = enabled - ---- Summary --- -Removed: 5 -Added: 0 - ---- Removed --- -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:269:8 - | -267 | try: -268 | pass -269 | except Exception as e: - | ^^^^^^^^^ -270 | logging.debug("...", exc_info=e) # ok - | - - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:275:8 - | -273 | try: -274 | pass -275 | except Exception: - | ^^^^^^^^^ -276 | logging.info("...", exc_info=True) # ok - | - - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:280:8 - | -278 | try: -279 | pass -280 | except Exception as e: - | ^^^^^^^^^ -281 | logging.warn("...", exc_info=e) # ok - | - - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:286:8 - | -284 | try: -285 | pass -286 | except Exception: - | ^^^^^^^^^ -287 | logging.warning("...", exc_info=True) # ok - | - - -BLE001 Do not catch blind exception: `Exception` - --> BLE.py:292:8 - | -290 | try: -291 | pass -292 | except Exception as e: - | ^^^^^^^^^ -293 | logging.log(logging.INFO, "...", exc_info=e) # ok - | From f1e00e3062111704d78136c6d035083206c82bab Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:38:05 -0400 Subject: [PATCH 026/390] [`pyupgrade`] Stabilize `typing_extensions.Text` detection (`UP019`) (#26943) Preview feature: https://github.com/astral-sh/ruff/pull/20825 Rule documentation: https://docs.astral.sh/ruff/rules/typing-text-str-alias/ --- crates/ruff_linter/src/preview.rs | 4 - crates/ruff_linter/src/rules/pyupgrade/mod.rs | 1 - .../pyupgrade/rules/typing_text_str_alias.rs | 11 +- ...er__rules__pyupgrade__tests__UP019.py.snap | 45 ++++++++ ...__pyupgrade__tests__UP019.py__preview.snap | 107 ------------------ 5 files changed, 47 insertions(+), 121 deletions(-) delete mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py__preview.snap diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index cd60591233..5199a61fc5 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -245,10 +245,6 @@ pub(crate) const fn is_b006_unsafe_fix_preserve_assignment_expr_enabled( settings.preview.is_enabled() } -pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSettings) -> bool { - settings.preview.is_enabled() -} - // https://github.com/astral-sh/ruff/pull/19045 pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index cdc8f10864..fa3ff8ff49 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -234,7 +234,6 @@ mod tests { Ok(()) } - #[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))] #[test_case(Rule::OSErrorAlias, Path::new("UP024_0.py"))] fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}__preview", path.to_string_lossy()); diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs index 4e69c888b6..175c760374 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs @@ -6,13 +6,10 @@ use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; -use crate::preview::is_typing_extensions_str_alias_enabled; use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does -/// Checks for uses of `typing.Text`. -/// -/// In preview mode, also checks for `typing_extensions.Text`. +/// Checks for uses of `typing.Text` and `typing_extensions.Text`. /// /// ## Why is this bad? /// `typing.Text` is an alias for `str`, and only exists for Python 2 @@ -65,11 +62,7 @@ pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) { let segments = qualified_name.segments(); let module = match segments { ["typing", "Text"] => TypingModule::Typing, - ["typing_extensions", "Text"] - if is_typing_extensions_str_alias_enabled(checker.settings()) => - { - TypingModule::TypingExtensions - } + ["typing_extensions", "Text"] => TypingModule::TypingExtensions, _ => return, }; diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py.snap index 13310b6a7b..142f093d4e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py.snap @@ -60,3 +60,48 @@ help: Replace with `str` 19 + def print_fourth_word(word: str) -> None: 20 | print(word) | + +UP019 [*] `typing_extensions.Text` is deprecated, use `str` + --> UP019.py:28:28 + | +28 | def print_fifth_word(word: typing_extensions.Text) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^ +29 | print(word) + | +help: Replace with `str` + | +27 | + - def print_fifth_word(word: typing_extensions.Text) -> None: +28 + def print_fifth_word(word: str) -> None: +29 | print(word) + | + +UP019 [*] `typing_extensions.Text` is deprecated, use `str` + --> UP019.py:32:28 + | +32 | def print_sixth_word(word: TypingExt.Text) -> None: + | ^^^^^^^^^^^^^^ +33 | print(word) + | +help: Replace with `str` + | +31 | + - def print_sixth_word(word: TypingExt.Text) -> None: +32 + def print_sixth_word(word: str) -> None: +33 | print(word) + | + +UP019 [*] `typing_extensions.Text` is deprecated, use `str` + --> UP019.py:36:30 + | +36 | def print_seventh_word(word: TextAlias) -> None: + | ^^^^^^^^^ +37 | print(word) + | +help: Replace with `str` + | +35 | + - def print_seventh_word(word: TextAlias) -> None: +36 + def print_seventh_word(word: str) -> None: +37 | print(word) + | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py__preview.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py__preview.snap deleted file mode 100644 index 142f093d4e..0000000000 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py__preview.snap +++ /dev/null @@ -1,107 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/pyupgrade/mod.rs ---- -UP019 [*] `typing.Text` is deprecated, use `str` - --> UP019.py:7:22 - | -7 | def print_word(word: Text) -> None: - | ^^^^ -8 | print(word) - | -help: Replace with `str` - | -6 | - - def print_word(word: Text) -> None: -7 + def print_word(word: str) -> None: -8 | print(word) - | - -UP019 [*] `typing.Text` is deprecated, use `str` - --> UP019.py:11:29 - | -11 | def print_second_word(word: typing.Text) -> None: - | ^^^^^^^^^^^ -12 | print(word) - | -help: Replace with `str` - | -10 | - - def print_second_word(word: typing.Text) -> None: -11 + def print_second_word(word: str) -> None: -12 | print(word) - | - -UP019 [*] `typing.Text` is deprecated, use `str` - --> UP019.py:15:28 - | -15 | def print_third_word(word: Hello.Text) -> None: - | ^^^^^^^^^^ -16 | print(word) - | -help: Replace with `str` - | -14 | - - def print_third_word(word: Hello.Text) -> None: -15 + def print_third_word(word: str) -> None: -16 | print(word) - | - -UP019 [*] `typing.Text` is deprecated, use `str` - --> UP019.py:19:29 - | -19 | def print_fourth_word(word: Goodbye) -> None: - | ^^^^^^^ -20 | print(word) - | -help: Replace with `str` - | -18 | - - def print_fourth_word(word: Goodbye) -> None: -19 + def print_fourth_word(word: str) -> None: -20 | print(word) - | - -UP019 [*] `typing_extensions.Text` is deprecated, use `str` - --> UP019.py:28:28 - | -28 | def print_fifth_word(word: typing_extensions.Text) -> None: - | ^^^^^^^^^^^^^^^^^^^^^^ -29 | print(word) - | -help: Replace with `str` - | -27 | - - def print_fifth_word(word: typing_extensions.Text) -> None: -28 + def print_fifth_word(word: str) -> None: -29 | print(word) - | - -UP019 [*] `typing_extensions.Text` is deprecated, use `str` - --> UP019.py:32:28 - | -32 | def print_sixth_word(word: TypingExt.Text) -> None: - | ^^^^^^^^^^^^^^ -33 | print(word) - | -help: Replace with `str` - | -31 | - - def print_sixth_word(word: TypingExt.Text) -> None: -32 + def print_sixth_word(word: str) -> None: -33 | print(word) - | - -UP019 [*] `typing_extensions.Text` is deprecated, use `str` - --> UP019.py:36:30 - | -36 | def print_seventh_word(word: TextAlias) -> None: - | ^^^^^^^^^ -37 | print(word) - | -help: Replace with `str` - | -35 | - - def print_seventh_word(word: TextAlias) -> None: -36 + def print_seventh_word(word: str) -> None: -37 | print(word) - | From aa63b6f247715b96ff3b0b604a89a2bf35894f3c Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:38:19 -0400 Subject: [PATCH 027/390] [`flake8-bandit`] Stabilize new PySNMP API paths (`S508`, `S509`) (#26945) Preview feature: https://github.com/astral-sh/ruff/pull/21374 Rule documentation: - https://docs.astral.sh/ruff/rules/snmp-insecure-version/ - https://docs.astral.sh/ruff/rules/snmp-weak-cryptography/ --- crates/ruff_linter/src/preview.rs | 5 - .../src/rules/flake8_bandit/mod.rs | 2 - .../rules/snmp_insecure_version.rs | 16 +-- .../rules/snmp_weak_cryptography.rs | 16 +-- ...s__flake8_bandit__tests__S508_S508.py.snap | 86 +++++++++++++++ ...s__flake8_bandit__tests__S509_S509.py.snap | 42 +++++++ ..._bandit__tests__preview__S508_S508.py.snap | 104 ------------------ ..._bandit__tests__preview__S509_S509.py.snap | 56 ---------- 8 files changed, 136 insertions(+), 191 deletions(-) delete mode 100644 crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S508_S508.py.snap delete mode 100644 crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S509_S509.py.snap diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 5199a61fc5..56ebaba4f6 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -250,11 +250,6 @@ pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &Linter settings.preview.is_enabled() } -// https://github.com/astral-sh/ruff/pull/21374 -pub(crate) const fn is_extended_snmp_api_path_detection_enabled(settings: &LinterSettings) -> bool { - settings.preview.is_enabled() -} - // https://github.com/astral-sh/ruff/pull/21395 pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() diff --git a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs index 69b93f868f..2b8423fb02 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs @@ -105,8 +105,6 @@ mod tests { #[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))] #[test_case(Rule::SuspiciousNonCryptographicRandomUsage, Path::new("S311.py"))] #[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))] - #[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"))] - #[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"))] #[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs index 8bf5830e31..44b90786e9 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_insecure_version.rs @@ -4,7 +4,6 @@ use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; -use crate::preview::is_extended_snmp_api_path_detection_enabled; /// ## What it does /// Checks for uses of SNMPv1 or SNMPv2. @@ -48,17 +47,10 @@ pub(crate) fn snmp_insecure_version(checker: &Checker, call: &ast::ExprCall) { .semantic() .resolve_qualified_name(&call.func) .is_some_and(|qualified_name| { - if is_extended_snmp_api_path_detection_enabled(checker.settings()) { - matches!( - qualified_name.segments(), - ["pysnmp", "hlapi", .., "CommunityData"] - ) - } else { - matches!( - qualified_name.segments(), - ["pysnmp", "hlapi", "CommunityData"] - ) - } + matches!( + qualified_name.segments(), + ["pysnmp", "hlapi", .., "CommunityData"] + ) }) { if let Some(keyword) = call.arguments.find_keyword("mpModel") { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs index 390f1bf13a..4e9297fe4a 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/snmp_weak_cryptography.rs @@ -4,7 +4,6 @@ use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; -use crate::preview::is_extended_snmp_api_path_detection_enabled; /// ## What it does /// Checks for uses of the SNMPv3 protocol without encryption. @@ -48,17 +47,10 @@ pub(crate) fn snmp_weak_cryptography(checker: &Checker, call: &ast::ExprCall) { .semantic() .resolve_qualified_name(&call.func) .is_some_and(|qualified_name| { - if is_extended_snmp_api_path_detection_enabled(checker.settings()) { - matches!( - qualified_name.segments(), - ["pysnmp", "hlapi", .., "UsmUserData"] - ) - } else { - matches!( - qualified_name.segments(), - ["pysnmp", "hlapi", "UsmUserData"] - ) - } + matches!( + qualified_name.segments(), + ["pysnmp", "hlapi", .., "UsmUserData"] + ) }) { checker.report_diagnostic(SnmpWeakCryptography, call.func.range()); diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S508_S508.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S508_S508.py.snap index acbb09666a..b47cdd7f77 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S508_S508.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S508_S508.py.snap @@ -20,3 +20,89 @@ S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. 5 | 6 | CommunityData("public", mpModel=2) # OK | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:18:46 + | +16 | import pysnmp.hlapi.auth +17 | +18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 +20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:19:58 + | +18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508 +19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 +21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:20:53 + | +18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508 +19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 +20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 +22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:21:45 + | +19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 +20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 +21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 +23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:22:58 + | +20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 +21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 +22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 +24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:23:53 + | +21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 +22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 +23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 +25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:24:45 + | +22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 +23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 +24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 + | + +S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. + --> S508.py:25:43 + | +23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 +24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 +25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 + | ^^^^^^^^^ +26 | +27 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK + | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S509_S509.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S509_S509.py.snap index c52b437891..da81c2a630 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S509_S509.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S509_S509.py.snap @@ -18,3 +18,45 @@ S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` 6 | 7 | less_insecure = UsmUserData("securityName", "authName", "privName") # OK | + +S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. + --> S509.py:15:1 + | +13 | import pysnmp.hlapi.auth +14 | +15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 +17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 + | + +S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. + --> S509.py:16:1 + | +15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509 +16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 +18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 + | + +S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. + --> S509.py:17:1 + | +15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509 +16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 +17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 + | + +S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. + --> S509.py:18:1 + | +16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 +17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 +18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +19 | +20 | pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK + | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S508_S508.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S508_S508.py.snap deleted file mode 100644 index f763850e17..0000000000 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S508_S508.py.snap +++ /dev/null @@ -1,104 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs ---- ---- Linter settings --- --linter.preview = disabled -+linter.preview = enabled - ---- Summary --- -Removed: 0 -Added: 8 - ---- Added --- -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:18:46 - | -16 | import pysnmp.hlapi.auth -17 | -18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 -20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:19:58 - | -18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508 -19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 -21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:20:53 - | -18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508 -19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 -20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 -22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:21:45 - | -19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 -20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 -21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 -23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:22:58 - | -20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508 -21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 -22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 -24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:23:53 - | -21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508 -22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 -23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 -25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:24:45 - | -22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508 -23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 -24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 - | - - -S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. - --> S508.py:25:43 - | -23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508 -24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508 -25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508 - | ^^^^^^^^^ -26 | -27 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S509_S509.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S509_S509.py.snap deleted file mode 100644 index 026e848351..0000000000 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S509_S509.py.snap +++ /dev/null @@ -1,56 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs ---- ---- Linter settings --- --linter.preview = disabled -+linter.preview = enabled - ---- Summary --- -Removed: 0 -Added: 4 - ---- Added --- -S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. - --> S509.py:15:1 - | -13 | import pysnmp.hlapi.auth -14 | -15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 -17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 - | - - -S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. - --> S509.py:16:1 - | -15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509 -16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 -18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 - | - - -S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. - --> S509.py:17:1 - | -15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509 -16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 -17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 - | - - -S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. - --> S509.py:18:1 - | -16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509 -17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509 -18 | pysnmp.hlapi.auth.UsmUserData("user") # S509 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -19 | -20 | pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK - | From 40e43bc8e5f551dd84ff7db207312953af0edc37 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:38:39 -0400 Subject: [PATCH 028/390] [`flake8-future-annotations`] Stabilize additional PEP 585-compatible APIs (`FA102`) (#26953) Preview feature: https://github.com/astral-sh/ruff/pull/20659 Rule documentation: https://docs.astral.sh/ruff/rules/future-required-type-annotation/ --- .../src/checkers/ast/analyze/expression.rs | 9 +- crates/ruff_linter/src/preview.rs | 5 - .../rules/flake8_future_annotations/mod.rs | 16 - ...uture_import_uses_preview_generics.py.snap | 815 +++++++++++++++++ ...uture_import_uses_preview_generics.py.snap | 851 ------------------ .../src/analyze/typing.rs | 94 +- 6 files changed, 873 insertions(+), 917 deletions(-) delete mode 100644 crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_preview_no_future_import_uses_preview_generics.py.snap diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 04b7108a21..66b5cf0e2f 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -8,8 +8,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::preview::{ - is_future_required_preview_generics_enabled, is_pep604_future_annotations_fix_enabled, - is_up006_future_annotations_fix_enabled, + is_pep604_future_annotations_fix_enabled, is_up006_future_annotations_fix_enabled, }; use crate::registry::Rule; use crate::rules::{ @@ -78,11 +77,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { && checker.semantic.in_annotation() && checker.semantic.in_runtime_evaluated_annotation() && !checker.semantic.in_string_type_definition() - && typing::is_pep585_generic( - value, - &checker.semantic, - is_future_required_preview_generics_enabled(checker.settings()), - ) + && typing::is_pep585_generic(value, &checker.semantic) { flake8_future_annotations::rules::future_required_type_annotation( checker, diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 56ebaba4f6..d29a0eb257 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -211,11 +211,6 @@ pub(crate) const fn is_allow_nested_roots_enabled(settings: &LinterSettings) -> settings.preview.is_enabled() } -// https://github.com/astral-sh/ruff/pull/20659 -pub(crate) const fn is_future_required_preview_generics_enabled(settings: &LinterSettings) -> bool { - settings.preview.is_enabled() -} - // https://github.com/astral-sh/ruff/pull/20169 pub(crate) const fn is_fix_builtin_open_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs b/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs index 6612ee086f..5e015e5428 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs @@ -9,7 +9,6 @@ mod tests { use test_case::test_case; use crate::registry::Rule; - use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_diagnostics, settings}; use ruff_python_ast::PythonVersion; @@ -58,19 +57,4 @@ mod tests { assert_diagnostics!(snapshot, diagnostics); Ok(()) } - - #[test_case(Path::new("no_future_import_uses_preview_generics.py"))] - fn fa102_preview(path: &Path) -> Result<()> { - let snapshot = format!("fa102_preview_{}", path.to_string_lossy()); - let diagnostics = test_path( - Path::new("flake8_future_annotations").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY37.into(), - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation) - }, - )?; - assert_diagnostics!(snapshot, diagnostics); - Ok(()) - } } diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_preview_generics.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_preview_generics.py.snap index eb7482447a..24dd3a986b 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_preview_generics.py.snap +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_no_future_import_uses_preview_generics.py.snap @@ -1,6 +1,39 @@ --- source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs --- +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:40:13 + | +39 | def takes_preview_generics( +40 | future: asyncio.Future[int], + | ^^^^^^^^^^^^^^^^^^^ +41 | task: asyncio.Task[str], +42 | deque_object: collections.deque[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:41:11 + | +39 | def takes_preview_generics( +40 | future: asyncio.Future[int], +41 | task: asyncio.Task[str], + | ^^^^^^^^^^^^^^^^^ +42 | deque_object: collections.deque[int], +43 | defaultdict_object: collections.defaultdict[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection --> no_future_import_uses_preview_generics.py:42:19 | @@ -34,3 +67,785 @@ help: Add `from __future__ import annotations` 2 | import asyncio | note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:44:19 + | +42 | deque_object: collections.deque[int], +43 | defaultdict_object: collections.defaultdict[str, int], +44 | ordered_dict: collections.OrderedDict[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +45 | counter_obj: collections.Counter[str], +46 | chain_map: collections.ChainMap[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:45:18 + | +43 | defaultdict_object: collections.defaultdict[str, int], +44 | ordered_dict: collections.OrderedDict[str, int], +45 | counter_obj: collections.Counter[str], + | ^^^^^^^^^^^^^^^^^^^^^^^^ +46 | chain_map: collections.ChainMap[str, int], +47 | context_manager: contextlib.AbstractContextManager[str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:46:16 + | +44 | ordered_dict: collections.OrderedDict[str, int], +45 | counter_obj: collections.Counter[str], +46 | chain_map: collections.ChainMap[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +47 | context_manager: contextlib.AbstractContextManager[str], +48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:47:22 + | +45 | counter_obj: collections.Counter[str], +46 | chain_map: collections.ChainMap[str, int], +47 | context_manager: contextlib.AbstractContextManager[str], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], +49 | dataclass_field: dataclasses.Field[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:48:28 + | +46 | chain_map: collections.ChainMap[str, int], +47 | context_manager: contextlib.AbstractContextManager[str], +48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +49 | dataclass_field: dataclasses.Field[int], +50 | cached_prop: functools.cached_property[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:49:22 + | +47 | context_manager: contextlib.AbstractContextManager[str], +48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], +49 | dataclass_field: dataclasses.Field[int], + | ^^^^^^^^^^^^^^^^^^^^^^ +50 | cached_prop: functools.cached_property[int], +51 | partial_method: functools.partialmethod[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:50:18 + | +48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], +49 | dataclass_field: dataclasses.Field[int], +50 | cached_prop: functools.cached_property[int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +51 | partial_method: functools.partialmethod[int], +52 | path_like: os.PathLike[str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:51:21 + | +49 | dataclass_field: dataclasses.Field[int], +50 | cached_prop: functools.cached_property[int], +51 | partial_method: functools.partialmethod[int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +52 | path_like: os.PathLike[str], +53 | lifo_queue: queue.LifoQueue[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:52:16 + | +50 | cached_prop: functools.cached_property[int], +51 | partial_method: functools.partialmethod[int], +52 | path_like: os.PathLike[str], + | ^^^^^^^^^^^^^^^^ +53 | lifo_queue: queue.LifoQueue[int], +54 | regular_queue: queue.Queue[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:53:17 + | +51 | partial_method: functools.partialmethod[int], +52 | path_like: os.PathLike[str], +53 | lifo_queue: queue.LifoQueue[int], + | ^^^^^^^^^^^^^^^^^^^^ +54 | regular_queue: queue.Queue[int], +55 | priority_queue: queue.PriorityQueue[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:54:20 + | +52 | path_like: os.PathLike[str], +53 | lifo_queue: queue.LifoQueue[int], +54 | regular_queue: queue.Queue[int], + | ^^^^^^^^^^^^^^^^ +55 | priority_queue: queue.PriorityQueue[int], +56 | simple_queue: queue.SimpleQueue[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:55:21 + | +53 | lifo_queue: queue.LifoQueue[int], +54 | regular_queue: queue.Queue[int], +55 | priority_queue: queue.PriorityQueue[int], + | ^^^^^^^^^^^^^^^^^^^^^^^^ +56 | simple_queue: queue.SimpleQueue[int], +57 | regex_pattern: re.Pattern[str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:56:19 + | +54 | regular_queue: queue.Queue[int], +55 | priority_queue: queue.PriorityQueue[int], +56 | simple_queue: queue.SimpleQueue[int], + | ^^^^^^^^^^^^^^^^^^^^^^ +57 | regex_pattern: re.Pattern[str], +58 | regex_match: re.Match[str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:57:20 + | +55 | priority_queue: queue.PriorityQueue[int], +56 | simple_queue: queue.SimpleQueue[int], +57 | regex_pattern: re.Pattern[str], + | ^^^^^^^^^^^^^^^ +58 | regex_match: re.Match[str], +59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:58:18 + | +56 | simple_queue: queue.SimpleQueue[int], +57 | regex_pattern: re.Pattern[str], +58 | regex_match: re.Match[str], + | ^^^^^^^^^^^^^ +59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], +60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:59:19 + | +57 | regex_pattern: re.Pattern[str], +58 | regex_match: re.Match[str], +59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], +61 | shelf_obj: shelve.Shelf[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:60:24 + | +58 | regex_match: re.Match[str], +59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], +60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +61 | shelf_obj: shelve.Shelf[str, int], +62 | mapping_proxy: types.MappingProxyType[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:61:16 + | +59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], +60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], +61 | shelf_obj: shelve.Shelf[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^ +62 | mapping_proxy: types.MappingProxyType[str, int], +63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:62:20 + | +60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], +61 | shelf_obj: shelve.Shelf[str, int], +62 | mapping_proxy: types.MappingProxyType[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], +64 | weak_method: weakref.WeakMethod[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:63:20 + | +61 | shelf_obj: shelve.Shelf[str, int], +62 | mapping_proxy: types.MappingProxyType[str, int], +63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +64 | weak_method: weakref.WeakMethod[int], +65 | weak_set: weakref.WeakSet[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:64:18 + | +62 | mapping_proxy: types.MappingProxyType[str, int], +63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], +64 | weak_method: weakref.WeakMethod[int], + | ^^^^^^^^^^^^^^^^^^^^^^^ +65 | weak_set: weakref.WeakSet[int], +66 | weak_value_dict: weakref.WeakValueDictionary[object, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:65:15 + | +63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], +64 | weak_method: weakref.WeakMethod[int], +65 | weak_set: weakref.WeakSet[int], + | ^^^^^^^^^^^^^^^^^^^^ +66 | weak_value_dict: weakref.WeakValueDictionary[object, int], +67 | awaitable: Awaitable[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:66:22 + | +64 | weak_method: weakref.WeakMethod[int], +65 | weak_set: weakref.WeakSet[int], +66 | weak_value_dict: weakref.WeakValueDictionary[object, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +67 | awaitable: Awaitable[int], +68 | coroutine: Coroutine[int, None, str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:67:16 + | +65 | weak_set: weakref.WeakSet[int], +66 | weak_value_dict: weakref.WeakValueDictionary[object, int], +67 | awaitable: Awaitable[int], + | ^^^^^^^^^^^^^^ +68 | coroutine: Coroutine[int, None, str], +69 | async_iterable: AsyncIterable[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:68:16 + | +66 | weak_value_dict: weakref.WeakValueDictionary[object, int], +67 | awaitable: Awaitable[int], +68 | coroutine: Coroutine[int, None, str], + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +69 | async_iterable: AsyncIterable[int], +70 | async_iterator: AsyncIterator[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:69:21 + | +67 | awaitable: Awaitable[int], +68 | coroutine: Coroutine[int, None, str], +69 | async_iterable: AsyncIterable[int], + | ^^^^^^^^^^^^^^^^^^ +70 | async_iterator: AsyncIterator[int], +71 | async_generator: AsyncGenerator[int, None], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:70:21 + | +68 | coroutine: Coroutine[int, None, str], +69 | async_iterable: AsyncIterable[int], +70 | async_iterator: AsyncIterator[int], + | ^^^^^^^^^^^^^^^^^^ +71 | async_generator: AsyncGenerator[int, None], +72 | iterable: Iterable[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:71:22 + | +69 | async_iterable: AsyncIterable[int], +70 | async_iterator: AsyncIterator[int], +71 | async_generator: AsyncGenerator[int, None], + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +72 | iterable: Iterable[int], +73 | iterator: Iterator[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:72:15 + | +70 | async_iterator: AsyncIterator[int], +71 | async_generator: AsyncGenerator[int, None], +72 | iterable: Iterable[int], + | ^^^^^^^^^^^^^ +73 | iterator: Iterator[int], +74 | generator: Generator[int, None, None], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:73:15 + | +71 | async_generator: AsyncGenerator[int, None], +72 | iterable: Iterable[int], +73 | iterator: Iterator[int], + | ^^^^^^^^^^^^^ +74 | generator: Generator[int, None, None], +75 | reversible: Reversible[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:74:16 + | +72 | iterable: Iterable[int], +73 | iterator: Iterator[int], +74 | generator: Generator[int, None, None], + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +75 | reversible: Reversible[int], +76 | container: Container[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:75:17 + | +73 | iterator: Iterator[int], +74 | generator: Generator[int, None, None], +75 | reversible: Reversible[int], + | ^^^^^^^^^^^^^^^ +76 | container: Container[int], +77 | collection: Collection[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:76:16 + | +74 | generator: Generator[int, None, None], +75 | reversible: Reversible[int], +76 | container: Container[int], + | ^^^^^^^^^^^^^^ +77 | collection: Collection[int], +78 | callable_obj: Callable[[int], str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:77:17 + | +75 | reversible: Reversible[int], +76 | container: Container[int], +77 | collection: Collection[int], + | ^^^^^^^^^^^^^^^ +78 | callable_obj: Callable[[int], str], +79 | set_obj: Set[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:78:19 + | +76 | container: Container[int], +77 | collection: Collection[int], +78 | callable_obj: Callable[[int], str], + | ^^^^^^^^^^^^^^^^^^^^ +79 | set_obj: Set[int], +80 | mutable_set: MutableSet[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:79:14 + | +77 | collection: Collection[int], +78 | callable_obj: Callable[[int], str], +79 | set_obj: Set[int], + | ^^^^^^^^ +80 | mutable_set: MutableSet[int], +81 | mapping: Mapping[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:80:18 + | +78 | callable_obj: Callable[[int], str], +79 | set_obj: Set[int], +80 | mutable_set: MutableSet[int], + | ^^^^^^^^^^^^^^^ +81 | mapping: Mapping[str, int], +82 | mutable_mapping: MutableMapping[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:81:14 + | +79 | set_obj: Set[int], +80 | mutable_set: MutableSet[int], +81 | mapping: Mapping[str, int], + | ^^^^^^^^^^^^^^^^^ +82 | mutable_mapping: MutableMapping[str, int], +83 | sequence: Sequence[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:82:22 + | +80 | mutable_set: MutableSet[int], +81 | mapping: Mapping[str, int], +82 | mutable_mapping: MutableMapping[str, int], + | ^^^^^^^^^^^^^^^^^^^^^^^^ +83 | sequence: Sequence[int], +84 | mutable_sequence: MutableSequence[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:83:15 + | +81 | mapping: Mapping[str, int], +82 | mutable_mapping: MutableMapping[str, int], +83 | sequence: Sequence[int], + | ^^^^^^^^^^^^^ +84 | mutable_sequence: MutableSequence[int], +85 | byte_string: ByteString[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:84:23 + | +82 | mutable_mapping: MutableMapping[str, int], +83 | sequence: Sequence[int], +84 | mutable_sequence: MutableSequence[int], + | ^^^^^^^^^^^^^^^^^^^^ +85 | byte_string: ByteString[int], +86 | mapping_view: MappingView[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:85:18 + | +83 | sequence: Sequence[int], +84 | mutable_sequence: MutableSequence[int], +85 | byte_string: ByteString[int], + | ^^^^^^^^^^^^^^^ +86 | mapping_view: MappingView[str, int], +87 | keys_view: KeysView[str], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:86:19 + | +84 | mutable_sequence: MutableSequence[int], +85 | byte_string: ByteString[int], +86 | mapping_view: MappingView[str, int], + | ^^^^^^^^^^^^^^^^^^^^^ +87 | keys_view: KeysView[str], +88 | items_view: ItemsView[str, int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:87:16 + | +85 | byte_string: ByteString[int], +86 | mapping_view: MappingView[str, int], +87 | keys_view: KeysView[str], + | ^^^^^^^^^^^^^ +88 | items_view: ItemsView[str, int], +89 | values_view: ValuesView[int], + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:88:17 + | +86 | mapping_view: MappingView[str, int], +87 | keys_view: KeysView[str], +88 | items_view: ItemsView[str, int], + | ^^^^^^^^^^^^^^^^^^^ +89 | values_view: ValuesView[int], +90 | ) -> None: + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior + +FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection + --> no_future_import_uses_preview_generics.py:89:18 + | +87 | keys_view: KeysView[str], +88 | items_view: ItemsView[str, int], +89 | values_view: ValuesView[int], + | ^^^^^^^^^^^^^^^ +90 | ) -> None: +91 | ... + | +help: Add `from __future__ import annotations` + | +1 + from __future__ import annotations +2 | import asyncio + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_preview_no_future_import_uses_preview_generics.py.snap b/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_preview_no_future_import_uses_preview_generics.py.snap deleted file mode 100644 index 24dd3a986b..0000000000 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/snapshots/ruff_linter__rules__flake8_future_annotations__tests__fa102_preview_no_future_import_uses_preview_generics.py.snap +++ /dev/null @@ -1,851 +0,0 @@ ---- -source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs ---- -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:40:13 - | -39 | def takes_preview_generics( -40 | future: asyncio.Future[int], - | ^^^^^^^^^^^^^^^^^^^ -41 | task: asyncio.Task[str], -42 | deque_object: collections.deque[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:41:11 - | -39 | def takes_preview_generics( -40 | future: asyncio.Future[int], -41 | task: asyncio.Task[str], - | ^^^^^^^^^^^^^^^^^ -42 | deque_object: collections.deque[int], -43 | defaultdict_object: collections.defaultdict[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:42:19 - | -40 | future: asyncio.Future[int], -41 | task: asyncio.Task[str], -42 | deque_object: collections.deque[int], - | ^^^^^^^^^^^^^^^^^^^^^^ -43 | defaultdict_object: collections.defaultdict[str, int], -44 | ordered_dict: collections.OrderedDict[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:43:25 - | -41 | task: asyncio.Task[str], -42 | deque_object: collections.deque[int], -43 | defaultdict_object: collections.defaultdict[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -44 | ordered_dict: collections.OrderedDict[str, int], -45 | counter_obj: collections.Counter[str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:44:19 - | -42 | deque_object: collections.deque[int], -43 | defaultdict_object: collections.defaultdict[str, int], -44 | ordered_dict: collections.OrderedDict[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -45 | counter_obj: collections.Counter[str], -46 | chain_map: collections.ChainMap[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:45:18 - | -43 | defaultdict_object: collections.defaultdict[str, int], -44 | ordered_dict: collections.OrderedDict[str, int], -45 | counter_obj: collections.Counter[str], - | ^^^^^^^^^^^^^^^^^^^^^^^^ -46 | chain_map: collections.ChainMap[str, int], -47 | context_manager: contextlib.AbstractContextManager[str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:46:16 - | -44 | ordered_dict: collections.OrderedDict[str, int], -45 | counter_obj: collections.Counter[str], -46 | chain_map: collections.ChainMap[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -47 | context_manager: contextlib.AbstractContextManager[str], -48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:47:22 - | -45 | counter_obj: collections.Counter[str], -46 | chain_map: collections.ChainMap[str, int], -47 | context_manager: contextlib.AbstractContextManager[str], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], -49 | dataclass_field: dataclasses.Field[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:48:28 - | -46 | chain_map: collections.ChainMap[str, int], -47 | context_manager: contextlib.AbstractContextManager[str], -48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -49 | dataclass_field: dataclasses.Field[int], -50 | cached_prop: functools.cached_property[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:49:22 - | -47 | context_manager: contextlib.AbstractContextManager[str], -48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], -49 | dataclass_field: dataclasses.Field[int], - | ^^^^^^^^^^^^^^^^^^^^^^ -50 | cached_prop: functools.cached_property[int], -51 | partial_method: functools.partialmethod[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:50:18 - | -48 | async_context_manager: contextlib.AbstractAsyncContextManager[int], -49 | dataclass_field: dataclasses.Field[int], -50 | cached_prop: functools.cached_property[int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -51 | partial_method: functools.partialmethod[int], -52 | path_like: os.PathLike[str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:51:21 - | -49 | dataclass_field: dataclasses.Field[int], -50 | cached_prop: functools.cached_property[int], -51 | partial_method: functools.partialmethod[int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -52 | path_like: os.PathLike[str], -53 | lifo_queue: queue.LifoQueue[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:52:16 - | -50 | cached_prop: functools.cached_property[int], -51 | partial_method: functools.partialmethod[int], -52 | path_like: os.PathLike[str], - | ^^^^^^^^^^^^^^^^ -53 | lifo_queue: queue.LifoQueue[int], -54 | regular_queue: queue.Queue[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:53:17 - | -51 | partial_method: functools.partialmethod[int], -52 | path_like: os.PathLike[str], -53 | lifo_queue: queue.LifoQueue[int], - | ^^^^^^^^^^^^^^^^^^^^ -54 | regular_queue: queue.Queue[int], -55 | priority_queue: queue.PriorityQueue[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:54:20 - | -52 | path_like: os.PathLike[str], -53 | lifo_queue: queue.LifoQueue[int], -54 | regular_queue: queue.Queue[int], - | ^^^^^^^^^^^^^^^^ -55 | priority_queue: queue.PriorityQueue[int], -56 | simple_queue: queue.SimpleQueue[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:55:21 - | -53 | lifo_queue: queue.LifoQueue[int], -54 | regular_queue: queue.Queue[int], -55 | priority_queue: queue.PriorityQueue[int], - | ^^^^^^^^^^^^^^^^^^^^^^^^ -56 | simple_queue: queue.SimpleQueue[int], -57 | regex_pattern: re.Pattern[str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:56:19 - | -54 | regular_queue: queue.Queue[int], -55 | priority_queue: queue.PriorityQueue[int], -56 | simple_queue: queue.SimpleQueue[int], - | ^^^^^^^^^^^^^^^^^^^^^^ -57 | regex_pattern: re.Pattern[str], -58 | regex_match: re.Match[str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:57:20 - | -55 | priority_queue: queue.PriorityQueue[int], -56 | simple_queue: queue.SimpleQueue[int], -57 | regex_pattern: re.Pattern[str], - | ^^^^^^^^^^^^^^^ -58 | regex_match: re.Match[str], -59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:58:18 - | -56 | simple_queue: queue.SimpleQueue[int], -57 | regex_pattern: re.Pattern[str], -58 | regex_match: re.Match[str], - | ^^^^^^^^^^^^^ -59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], -60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:59:19 - | -57 | regex_pattern: re.Pattern[str], -58 | regex_match: re.Match[str], -59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], -61 | shelf_obj: shelve.Shelf[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:60:24 - | -58 | regex_match: re.Match[str], -59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], -60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -61 | shelf_obj: shelve.Shelf[str, int], -62 | mapping_proxy: types.MappingProxyType[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:61:16 - | -59 | bsd_db_shelf: shelve.BsdDbShelf[str, int], -60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], -61 | shelf_obj: shelve.Shelf[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^ -62 | mapping_proxy: types.MappingProxyType[str, int], -63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:62:20 - | -60 | db_filename_shelf: shelve.DbfilenameShelf[str, int], -61 | shelf_obj: shelve.Shelf[str, int], -62 | mapping_proxy: types.MappingProxyType[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], -64 | weak_method: weakref.WeakMethod[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:63:20 - | -61 | shelf_obj: shelve.Shelf[str, int], -62 | mapping_proxy: types.MappingProxyType[str, int], -63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -64 | weak_method: weakref.WeakMethod[int], -65 | weak_set: weakref.WeakSet[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:64:18 - | -62 | mapping_proxy: types.MappingProxyType[str, int], -63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], -64 | weak_method: weakref.WeakMethod[int], - | ^^^^^^^^^^^^^^^^^^^^^^^ -65 | weak_set: weakref.WeakSet[int], -66 | weak_value_dict: weakref.WeakValueDictionary[object, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:65:15 - | -63 | weak_key_dict: weakref.WeakKeyDictionary[object, int], -64 | weak_method: weakref.WeakMethod[int], -65 | weak_set: weakref.WeakSet[int], - | ^^^^^^^^^^^^^^^^^^^^ -66 | weak_value_dict: weakref.WeakValueDictionary[object, int], -67 | awaitable: Awaitable[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:66:22 - | -64 | weak_method: weakref.WeakMethod[int], -65 | weak_set: weakref.WeakSet[int], -66 | weak_value_dict: weakref.WeakValueDictionary[object, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -67 | awaitable: Awaitable[int], -68 | coroutine: Coroutine[int, None, str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:67:16 - | -65 | weak_set: weakref.WeakSet[int], -66 | weak_value_dict: weakref.WeakValueDictionary[object, int], -67 | awaitable: Awaitable[int], - | ^^^^^^^^^^^^^^ -68 | coroutine: Coroutine[int, None, str], -69 | async_iterable: AsyncIterable[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:68:16 - | -66 | weak_value_dict: weakref.WeakValueDictionary[object, int], -67 | awaitable: Awaitable[int], -68 | coroutine: Coroutine[int, None, str], - | ^^^^^^^^^^^^^^^^^^^^^^^^^ -69 | async_iterable: AsyncIterable[int], -70 | async_iterator: AsyncIterator[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:69:21 - | -67 | awaitable: Awaitable[int], -68 | coroutine: Coroutine[int, None, str], -69 | async_iterable: AsyncIterable[int], - | ^^^^^^^^^^^^^^^^^^ -70 | async_iterator: AsyncIterator[int], -71 | async_generator: AsyncGenerator[int, None], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:70:21 - | -68 | coroutine: Coroutine[int, None, str], -69 | async_iterable: AsyncIterable[int], -70 | async_iterator: AsyncIterator[int], - | ^^^^^^^^^^^^^^^^^^ -71 | async_generator: AsyncGenerator[int, None], -72 | iterable: Iterable[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:71:22 - | -69 | async_iterable: AsyncIterable[int], -70 | async_iterator: AsyncIterator[int], -71 | async_generator: AsyncGenerator[int, None], - | ^^^^^^^^^^^^^^^^^^^^^^^^^ -72 | iterable: Iterable[int], -73 | iterator: Iterator[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:72:15 - | -70 | async_iterator: AsyncIterator[int], -71 | async_generator: AsyncGenerator[int, None], -72 | iterable: Iterable[int], - | ^^^^^^^^^^^^^ -73 | iterator: Iterator[int], -74 | generator: Generator[int, None, None], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:73:15 - | -71 | async_generator: AsyncGenerator[int, None], -72 | iterable: Iterable[int], -73 | iterator: Iterator[int], - | ^^^^^^^^^^^^^ -74 | generator: Generator[int, None, None], -75 | reversible: Reversible[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:74:16 - | -72 | iterable: Iterable[int], -73 | iterator: Iterator[int], -74 | generator: Generator[int, None, None], - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ -75 | reversible: Reversible[int], -76 | container: Container[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:75:17 - | -73 | iterator: Iterator[int], -74 | generator: Generator[int, None, None], -75 | reversible: Reversible[int], - | ^^^^^^^^^^^^^^^ -76 | container: Container[int], -77 | collection: Collection[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:76:16 - | -74 | generator: Generator[int, None, None], -75 | reversible: Reversible[int], -76 | container: Container[int], - | ^^^^^^^^^^^^^^ -77 | collection: Collection[int], -78 | callable_obj: Callable[[int], str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:77:17 - | -75 | reversible: Reversible[int], -76 | container: Container[int], -77 | collection: Collection[int], - | ^^^^^^^^^^^^^^^ -78 | callable_obj: Callable[[int], str], -79 | set_obj: Set[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:78:19 - | -76 | container: Container[int], -77 | collection: Collection[int], -78 | callable_obj: Callable[[int], str], - | ^^^^^^^^^^^^^^^^^^^^ -79 | set_obj: Set[int], -80 | mutable_set: MutableSet[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:79:14 - | -77 | collection: Collection[int], -78 | callable_obj: Callable[[int], str], -79 | set_obj: Set[int], - | ^^^^^^^^ -80 | mutable_set: MutableSet[int], -81 | mapping: Mapping[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:80:18 - | -78 | callable_obj: Callable[[int], str], -79 | set_obj: Set[int], -80 | mutable_set: MutableSet[int], - | ^^^^^^^^^^^^^^^ -81 | mapping: Mapping[str, int], -82 | mutable_mapping: MutableMapping[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:81:14 - | -79 | set_obj: Set[int], -80 | mutable_set: MutableSet[int], -81 | mapping: Mapping[str, int], - | ^^^^^^^^^^^^^^^^^ -82 | mutable_mapping: MutableMapping[str, int], -83 | sequence: Sequence[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:82:22 - | -80 | mutable_set: MutableSet[int], -81 | mapping: Mapping[str, int], -82 | mutable_mapping: MutableMapping[str, int], - | ^^^^^^^^^^^^^^^^^^^^^^^^ -83 | sequence: Sequence[int], -84 | mutable_sequence: MutableSequence[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:83:15 - | -81 | mapping: Mapping[str, int], -82 | mutable_mapping: MutableMapping[str, int], -83 | sequence: Sequence[int], - | ^^^^^^^^^^^^^ -84 | mutable_sequence: MutableSequence[int], -85 | byte_string: ByteString[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:84:23 - | -82 | mutable_mapping: MutableMapping[str, int], -83 | sequence: Sequence[int], -84 | mutable_sequence: MutableSequence[int], - | ^^^^^^^^^^^^^^^^^^^^ -85 | byte_string: ByteString[int], -86 | mapping_view: MappingView[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:85:18 - | -83 | sequence: Sequence[int], -84 | mutable_sequence: MutableSequence[int], -85 | byte_string: ByteString[int], - | ^^^^^^^^^^^^^^^ -86 | mapping_view: MappingView[str, int], -87 | keys_view: KeysView[str], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:86:19 - | -84 | mutable_sequence: MutableSequence[int], -85 | byte_string: ByteString[int], -86 | mapping_view: MappingView[str, int], - | ^^^^^^^^^^^^^^^^^^^^^ -87 | keys_view: KeysView[str], -88 | items_view: ItemsView[str, int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:87:16 - | -85 | byte_string: ByteString[int], -86 | mapping_view: MappingView[str, int], -87 | keys_view: KeysView[str], - | ^^^^^^^^^^^^^ -88 | items_view: ItemsView[str, int], -89 | values_view: ValuesView[int], - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:88:17 - | -86 | mapping_view: MappingView[str, int], -87 | keys_view: KeysView[str], -88 | items_view: ItemsView[str, int], - | ^^^^^^^^^^^^^^^^^^^ -89 | values_view: ValuesView[int], -90 | ) -> None: - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior - -FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection - --> no_future_import_uses_preview_generics.py:89:18 - | -87 | keys_view: KeysView[str], -88 | items_view: ItemsView[str, int], -89 | values_view: ValuesView[int], - | ^^^^^^^^^^^^^^^ -90 | ) -> None: -91 | ... - | -help: Add `from __future__ import annotations` - | -1 + from __future__ import annotations -2 | import asyncio - | -note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 916bba9bf2..5375235e87 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -147,46 +147,64 @@ pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option bool { +pub fn is_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(expr) - .is_some_and(|qualified_name| match qualified_name.segments() { - ["", "dict" | "frozenset" | "list" | "set" | "tuple" | "type"] - | ["collections", "deque" | "defaultdict"] => true, - ["asyncio", "Future" | "Task"] - | ["collections", "ChainMap" | "Counter" | "OrderedDict"] - | [ - "contextlib", - "AbstractAsyncContextManager" | "AbstractContextManager", - ] - | ["dataclasses", "Field"] - | ["functools", "cached_property" | "partialmethod"] - | ["os", "PathLike"] - | [ - "queue", - "LifoQueue" | "PriorityQueue" | "Queue" | "SimpleQueue", - ] - | ["re", "Match" | "Pattern"] - | ["shelve", "BsdDbShelf" | "DbfilenameShelf" | "Shelf"] - | ["types", "MappingProxyType"] - | [ - "weakref", - "WeakKeyDictionary" | "WeakMethod" | "WeakSet" | "WeakValueDictionary", - ] - | [ - "collections", - "abc", - "AsyncGenerator" | "AsyncIterable" | "AsyncIterator" | "Awaitable" | "ByteString" - | "Callable" | "Collection" | "Container" | "Coroutine" | "Generator" | "ItemsView" - | "Iterable" | "Iterator" | "KeysView" | "Mapping" | "MappingView" - | "MutableMapping" | "MutableSequence" | "MutableSet" | "Reversible" | "Sequence" - | "Set" | "ValuesView", - ] => include_preview_generics, - _ => false, + .is_some_and(|qualified_name| { + matches!( + qualified_name.segments(), + ["", "dict" | "frozenset" | "list" | "set" | "tuple" | "type"] + | [ + "collections", + "deque" | "defaultdict" | "ChainMap" | "Counter" | "OrderedDict" + ] + | ["asyncio", "Future" | "Task"] + | [ + "contextlib", + "AbstractAsyncContextManager" | "AbstractContextManager" + ] + | ["dataclasses", "Field"] + | ["functools", "cached_property" | "partialmethod"] + | ["os", "PathLike"] + | [ + "queue", + "LifoQueue" | "PriorityQueue" | "Queue" | "SimpleQueue" + ] + | ["re", "Match" | "Pattern"] + | ["shelve", "BsdDbShelf" | "DbfilenameShelf" | "Shelf"] + | ["types", "MappingProxyType"] + | [ + "weakref", + "WeakKeyDictionary" | "WeakMethod" | "WeakSet" | "WeakValueDictionary" + ] + | [ + "collections", + "abc", + "AsyncGenerator" + | "AsyncIterable" + | "AsyncIterator" + | "Awaitable" + | "ByteString" + | "Callable" + | "Collection" + | "Container" + | "Coroutine" + | "Generator" + | "ItemsView" + | "Iterable" + | "Iterator" + | "KeysView" + | "Mapping" + | "MappingView" + | "MutableMapping" + | "MutableSequence" + | "MutableSet" + | "Reversible" + | "Sequence" + | "Set" + | "ValuesView" + ] + ) }) } From a9702d8928344f77a41dbe535f655a69fb04e2df Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:49:28 -0400 Subject: [PATCH 029/390] [`flake8-bandit`] Stabilize string literal binding resolution (`S310`) (#26944) Preview feature: https://github.com/astral-sh/ruff/pull/21469 Rule documentation: https://docs.astral.sh/ruff/rules/suspicious-url-open-usage/ --- crates/ruff_linter/src/preview.rs | 7 -- .../rules/suspicious_function_call.rs | 32 ++----- ...s__flake8_bandit__tests__S310_S310.py.snap | 81 ---------------- ..._bandit__tests__preview__S310_S310.py.snap | 93 +------------------ 4 files changed, 7 insertions(+), 206 deletions(-) diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index d29a0eb257..a0e6abcdda 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -250,13 +250,6 @@ pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSet settings.preview.is_enabled() } -// https://github.com/astral-sh/ruff/pull/21469 -pub(crate) const fn is_s310_resolve_string_literal_bindings_enabled( - settings: &LinterSettings, -) -> bool { - settings.preview.is_enabled() -} - // https://github.com/astral-sh/ruff/pull/22419 pub(crate) const fn is_py315_support_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() 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 06e2e7e348..90c743c1da 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 @@ -10,10 +10,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::Violation; use crate::checkers::ast::Checker; -use crate::preview::{ - is_s310_resolve_string_literal_bindings_enabled, is_suspicious_function_reference_enabled, -}; -use crate::settings::LinterSettings; +use crate::preview::is_suspicious_function_reference_enabled; /// ## What it does /// Checks for calls to `pickle` functions or modules that wrap them. @@ -1021,13 +1018,8 @@ fn suspicious_function( } /// Resolves `expr` to its binding and checks if the resolved expression starts with an HTTP or HTTPS prefix. - fn expression_starts_with_http_prefix( - expr: &Expr, - semantic: &SemanticModel, - settings: &LinterSettings, - ) -> bool { - let resolved_expression = if is_s310_resolve_string_literal_bindings_enabled(settings) - && let Some(name_expr) = expr.as_name_expr() + fn expression_starts_with_http_prefix(expr: &Expr, semantic: &SemanticModel) -> bool { + let resolved_expression = if let Some(name_expr) = expr.as_name_expr() && let Some(binding_id) = semantic.only_binding(name_expr) && let Some(value) = find_binding_value(semantic.binding(binding_id), semantic) { @@ -1170,11 +1162,7 @@ fn suspicious_function( .all(|keyword| keyword.arg.is_some()) { if let Some(url_expr) = arguments.find_argument_value("url", 0) - && expression_starts_with_http_prefix( - url_expr, - checker.semantic(), - checker.settings(), - ) + && expression_starts_with_http_prefix(url_expr, checker.semantic()) { return; } @@ -1211,11 +1199,7 @@ fn suspicious_function( }) => { if let Some(url_expr) = arguments.find_argument_value("url", 0) - && expression_starts_with_http_prefix( - url_expr, - checker.semantic(), - checker.settings(), - ) + && expression_starts_with_http_prefix(url_expr, checker.semantic()) { return; } @@ -1223,11 +1207,7 @@ fn suspicious_function( // If the `url` argument is a string literal (including resolved bindings), allow `http` and `https` schemes. Some(expr) - if expression_starts_with_http_prefix( - expr, - checker.semantic(), - checker.settings(), - ) => + if expression_starts_with_http_prefix(expr, checker.semantic()) => { return; } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S310_S310.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S310_S310.py.snap index 7c73532b23..4a4b61876e 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S310_S310.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S310_S310.py.snap @@ -254,84 +254,3 @@ S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom sch 42 | urllib.request.urlopen(urllib.request.Request(url)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:51:1 - | -49 | # https://github.com/astral-sh/ruff/issues/21462 -50 | path = "https://example.com/data.csv" -51 | urllib.request.urlretrieve(path, "data.csv") - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -52 | url = "https://example.com/api" -53 | urllib.request.Request(url) - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:53:1 - | -51 | urllib.request.urlretrieve(path, "data.csv") -52 | url = "https://example.com/api" -53 | urllib.request.Request(url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -54 | -55 | # Test resolved f-strings and concatenated string literals - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:57:1 - | -55 | # Test resolved f-strings and concatenated string literals -56 | fstring_url = f"https://example.com/data.csv" -57 | urllib.request.urlopen(fstring_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -58 | urllib.request.Request(fstring_url) - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:58:1 - | -56 | fstring_url = f"https://example.com/data.csv" -57 | urllib.request.urlopen(fstring_url) -58 | urllib.request.Request(fstring_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -59 | -60 | concatenated_url = "https://" + "example.com/data.csv" - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:61:1 - | -60 | concatenated_url = "https://" + "example.com/data.csv" -61 | urllib.request.urlopen(concatenated_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -62 | urllib.request.Request(concatenated_url) - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:62:1 - | -60 | concatenated_url = "https://" + "example.com/data.csv" -61 | urllib.request.urlopen(concatenated_url) -62 | urllib.request.Request(concatenated_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -63 | -64 | nested_concatenated = "http://" + "example.com" + "/data.csv" - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:65:1 - | -64 | nested_concatenated = "http://" + "example.com" + "/data.csv" -65 | urllib.request.urlopen(nested_concatenated) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -66 | urllib.request.Request(nested_concatenated) - | - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:66:1 - | -64 | nested_concatenated = "http://" + "example.com" + "/data.csv" -65 | urllib.request.urlopen(nested_concatenated) -66 | urllib.request.Request(nested_concatenated) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S310_S310.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S310_S310.py.snap index 081c3afb49..e92475a2b2 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S310_S310.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__preview__S310_S310.py.snap @@ -6,100 +6,9 @@ source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs +linter.preview = enabled --- Summary --- -Removed: 8 +Removed: 0 Added: 2 ---- Removed --- -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:51:1 - | -49 | # https://github.com/astral-sh/ruff/issues/21462 -50 | path = "https://example.com/data.csv" -51 | urllib.request.urlretrieve(path, "data.csv") - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -52 | url = "https://example.com/api" -53 | urllib.request.Request(url) - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:53:1 - | -51 | urllib.request.urlretrieve(path, "data.csv") -52 | url = "https://example.com/api" -53 | urllib.request.Request(url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -54 | -55 | # Test resolved f-strings and concatenated string literals - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:57:1 - | -55 | # Test resolved f-strings and concatenated string literals -56 | fstring_url = f"https://example.com/data.csv" -57 | urllib.request.urlopen(fstring_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -58 | urllib.request.Request(fstring_url) - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:58:1 - | -56 | fstring_url = f"https://example.com/data.csv" -57 | urllib.request.urlopen(fstring_url) -58 | urllib.request.Request(fstring_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -59 | -60 | concatenated_url = "https://" + "example.com/data.csv" - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:61:1 - | -60 | concatenated_url = "https://" + "example.com/data.csv" -61 | urllib.request.urlopen(concatenated_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -62 | urllib.request.Request(concatenated_url) - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:62:1 - | -60 | concatenated_url = "https://" + "example.com/data.csv" -61 | urllib.request.urlopen(concatenated_url) -62 | urllib.request.Request(concatenated_url) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -63 | -64 | nested_concatenated = "http://" + "example.com" + "/data.csv" - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:65:1 - | -64 | nested_concatenated = "http://" + "example.com" + "/data.csv" -65 | urllib.request.urlopen(nested_concatenated) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -66 | urllib.request.Request(nested_concatenated) - | - - -S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. - --> S310.py:66:1 - | -64 | nested_concatenated = "http://" + "example.com" + "/data.csv" -65 | urllib.request.urlopen(nested_concatenated) -66 | urllib.request.Request(nested_concatenated) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - - - --- Added --- S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected. --> S310.py:46:5 From afe2723a348364ac7f4b9abd76fc67779490c05e Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:45:30 -0400 Subject: [PATCH 030/390] [`flake8-gettext`] Stabilize qualified-name and built-in binding resolution (`INT001`, `INT002`, `INT003`) (#26947) Preview feature: https://github.com/astral-sh/ruff/pull/19045 Rule documentation: - https://docs.astral.sh/ruff/rules/f-string-in-get-text-func-call/ - https://docs.astral.sh/ruff/rules/format-in-get-text-func-call/ - https://docs.astral.sh/ruff/rules/printf-in-get-text-func-call/ --- crates/ruff_linter/src/preview.rs | 5 -- .../src/rules/flake8_gettext/mod.rs | 8 +-- ...tring-in-get-text-func-call_INT001.py.snap | 69 +++++++++++++++++++ ...ormat-in-get-text-func-call_INT002.py.snap | 69 +++++++++++++++++++ ...rintf-in-get-text-func-call_INT003.py.snap | 69 +++++++++++++++++++ 5 files changed, 208 insertions(+), 12 deletions(-) diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index a0e6abcdda..57578452a9 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -240,11 +240,6 @@ pub(crate) const fn is_b006_unsafe_fix_preserve_assignment_expr_enabled( settings.preview.is_enabled() } -// https://github.com/astral-sh/ruff/pull/19045 -pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &LinterSettings) -> bool { - settings.preview.is_enabled() -} - // https://github.com/astral-sh/ruff/pull/21395 pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() diff --git a/crates/ruff_linter/src/rules/flake8_gettext/mod.rs b/crates/ruff_linter/src/rules/flake8_gettext/mod.rs index e0469f93d1..12705a7624 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/mod.rs @@ -1,8 +1,6 @@ //! Rules from [flake8-gettext](https://pypi.org/project/flake8-gettext/). use crate::checkers::ast::Checker; -use crate::preview::{ - is_extended_i18n_function_matching_enabled, is_plural_ngettext_check_enabled, -}; +use crate::preview::is_plural_ngettext_check_enabled; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::Modules; @@ -44,10 +42,6 @@ pub(crate) fn is_gettext_func_call( return true; } - if !is_extended_i18n_function_matching_enabled(checker.settings()) { - return false; - } - let semantic = checker.semantic(); let Some(qualified_name) = semantic.resolve_qualified_name(func) else { diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap index 0285458057..9cfb1b5242 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap @@ -30,6 +30,46 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg 9 | _gettext(f"{'value'}") # no lint | +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:22:21 + | +20 | name = "Guido" +21 | +22 | gettext_mod.gettext(f"Hello, {name}!") + | ^^^^^^^^^^^^^^^^^ +23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2) +24 | gettext_fn(f"Hello, {name}!") + | + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:23:22 + | +22 | gettext_mod.gettext(f"Hello, {name}!") +23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2) + | ^^^^^^^^^^^^^^^^^ +24 | gettext_fn(f"Hello, {name}!") +25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2) + | + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:24:12 + | +22 | gettext_mod.gettext(f"Hello, {name}!") +23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2) +24 | gettext_fn(f"Hello, {name}!") + | ^^^^^^^^^^^^^^^^^ +25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2) + | + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:25:13 + | +23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2) +24 | gettext_fn(f"Hello, {name}!") +25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2) + | ^^^^^^^^^^^^^^^^^ + | + INT001 f-string is resolved before function call; consider `_("string %s") % arg` --> INT001.py:31:14 | @@ -91,3 +131,32 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg 41 | print(_(f"{a}")) | ^^^^^^ | + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:51:12 + | +49 | builtins.__dict__["gettext"] = gettext_fn +50 | +51 | builtins._(f"{'value'}") + | ^^^^^^^^^^^^ +52 | builtins.gettext(f"{'value'}") +53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2) + | + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:52:18 + | +51 | builtins._(f"{'value'}") +52 | builtins.gettext(f"{'value'}") + | ^^^^^^^^^^^^ +53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2) + | + +INT001 f-string is resolved before function call; consider `_("string %s") % arg` + --> INT001.py:53:19 + | +51 | builtins._(f"{'value'}") +52 | builtins.gettext(f"{'value'}") +53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2) + | ^^^^^^^^^^^^ + | diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__format-in-get-text-func-call_INT002.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__format-in-get-text-func-call_INT002.py.snap index b1edf104de..b368f71065 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__format-in-get-text-func-call_INT002.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__format-in-get-text-func-call_INT002.py.snap @@ -30,6 +30,46 @@ INT002 `format` method argument is resolved before function call; consider `_("s 5 | _gettext("{}".format("line")) # no lint | +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:18:21 + | +16 | name = "Guido" +17 | +18 | gettext_mod.gettext("Hello, {}!".format(name)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) +20 | gettext_fn("Hello, {}!".format(name)) + | + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:19:22 + | +18 | gettext_mod.gettext("Hello, {}!".format(name)) +19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +20 | gettext_fn("Hello, {}!".format(name)) +21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) + | + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:20:12 + | +18 | gettext_mod.gettext("Hello, {}!".format(name)) +19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) +20 | gettext_fn("Hello, {}!".format(name)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) + | + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:21:13 + | +19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) +20 | gettext_fn("Hello, {}!".format(name)) +21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2) + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | + INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` --> INT002.py:27:14 | @@ -91,3 +131,32 @@ INT002 `format` method argument is resolved before function call; consider `_("s 37 | print(_("{}".format(a))) | ^^^^^^^^^^^^^^ | + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:47:12 + | +45 | builtins.__dict__["gettext"] = gettext_fn +46 | +47 | builtins._("{}".format("line")) + | ^^^^^^^^^^^^^^^^^^^ +48 | builtins.gettext("{}".format("line")) +49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2) + | + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:48:18 + | +47 | builtins._("{}".format("line")) +48 | builtins.gettext("{}".format("line")) + | ^^^^^^^^^^^^^^^^^^^ +49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2) + | + +INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg` + --> INT002.py:49:19 + | +47 | builtins._("{}".format("line")) +48 | builtins.gettext("{}".format("line")) +49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2) + | ^^^^^^^^^^^^^^^^^^^ + | diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__printf-in-get-text-func-call_INT003.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__printf-in-get-text-func-call_INT003.py.snap index 91260001b7..496422c556 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__printf-in-get-text-func-call_INT003.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__printf-in-get-text-func-call_INT003.py.snap @@ -30,6 +30,46 @@ INT003 printf-style format is resolved before function call; consider `_("string 5 | _gettext("%s" % "line") # no lint | +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:18:21 + | +16 | name = "Guido" +17 | +18 | gettext_mod.gettext("Hello, %s!" % name) + | ^^^^^^^^^^^^^^^^^^^ +19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2) +20 | gettext_fn("Hello, %s!" % name) + | + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:19:22 + | +18 | gettext_mod.gettext("Hello, %s!" % name) +19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2) + | ^^^^^^^^^^^^^^^^^^^ +20 | gettext_fn("Hello, %s!" % name) +21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2) + | + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:20:12 + | +18 | gettext_mod.gettext("Hello, %s!" % name) +19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2) +20 | gettext_fn("Hello, %s!" % name) + | ^^^^^^^^^^^^^^^^^^^ +21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2) + | + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:21:13 + | +19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2) +20 | gettext_fn("Hello, %s!" % name) +21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2) + | ^^^^^^^^^^^^^^^^^^^ + | + INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` --> INT003.py:27:14 | @@ -91,3 +131,32 @@ INT003 printf-style format is resolved before function call; consider `_("string 37 | print(_("%s" % a)) | ^^^^^^^^ | + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:47:12 + | +45 | builtins.__dict__["gettext"] = gettext_fn +46 | +47 | builtins._("%s" % "line") + | ^^^^^^^^^^^^^ +48 | builtins.gettext("%s" % "line") +49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2) + | + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:48:18 + | +47 | builtins._("%s" % "line") +48 | builtins.gettext("%s" % "line") + | ^^^^^^^^^^^^^ +49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2) + | + +INT003 printf-style format is resolved before function call; consider `_("string %s") % arg` + --> INT003.py:49:19 + | +47 | builtins._("%s" % "line") +48 | builtins.gettext("%s" % "line") +49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2) + | ^^^^^^^^^^^^^ + | From 87e51e2cbbaed376fc13dead40fd772361fa07c0 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:03:36 -0400 Subject: [PATCH 031/390] Fix `format --check` spans for syntax errors (#27045) Summary -- This PR fixes #24528 by attaching more information to `FormatCommandError::Parse` instances. I made a couple of attempts at Micha's suggestion ([1]) to convert to `Diagnostic`s much earlier, but doing this only for errors was pretty awkward while still producing a much larger diff. I think it would be better to use diagnostics more deeply both for the error and non-error cases eventually, which should also help with #8232, but I considered the limited fix here a better stopgap for the 0.16 release. [1]: https://github.com/astral-sh/ruff/issues/24528#issuecomment-4228856614 Test Plan -- Updated snapshots for two existing tests, plus an additional notebook test --- crates/ruff/src/commands/format.rs | 91 +++++++++++++++++++++--------- crates/ruff/tests/cli/format.rs | 48 ++++++++++++++-- 2 files changed, 107 insertions(+), 32 deletions(-) diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 61fd5474da..57f6351a0c 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -34,7 +34,7 @@ use ruff_linter::source_kind::{SourceError, SourceKind}; use ruff_linter::warn_user_once; use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_formatter::{FormatModuleError, QuoteStyle, format_module_source, format_range}; -use ruff_source_file::{LineIndex, LineRanges, OneIndexed, SourceFileBuilder}; +use ruff_source_file::{LineIndex, LineRanges, OneIndexed, SourceFile, SourceFileBuilder}; use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_workspace::FormatterSettings; use ruff_workspace::resolver::{ @@ -381,12 +381,7 @@ pub(crate) fn format_source( let formatted = formatted.map_err(|err| { if let FormatModuleError::ParseError(err) = err { - DisplayParseError::from_source_kind( - err, - path.map(Path::to_path_buf), - source_kind, - ) - .into() + FormatCommandError::parse(err, path, source_kind) } else { FormatCommandError::Format(path.map(Path::to_path_buf), err) } @@ -429,15 +424,14 @@ pub(crate) fn format_source( format_module_source(unformatted, options.clone()).map_err(|err| { if let FormatModuleError::ParseError(err) = err { // Offset the error by the start of the cell - DisplayParseError::from_source_kind( + FormatCommandError::parse( ParseError { error: err.error, location: err.location.checked_add(*start).unwrap(), }, - path.map(Path::to_path_buf), + path, source_kind, ) - .into() } else { FormatCommandError::Format(path.map(Path::to_path_buf), err) } @@ -584,7 +578,21 @@ impl<'a> FormatResults<'a> { output_format: OutputFormat, errors: &[FormatCommandError], ) -> io::Result<()> { - let mut notebook_index = FxHashMap::default(); + let mut notebook_index = errors + .iter() + .filter_map(|error| { + if let FormatCommandError::Parse { + source_file, + notebook_index: Some(notebook_index), + .. + } = error + { + Some((source_file.name().to_string(), notebook_index.clone())) + } else { + None + } + }) + .collect(); let diagnostics: Vec<_> = errors .iter() .map(Diagnostic::from) @@ -817,7 +825,11 @@ impl<'a> FormatResults<'a> { #[derive(Error, Debug)] pub(crate) enum FormatCommandError { Ignore(#[from] ignore::Error), - Parse(#[from] DisplayParseError), + Parse { + error: DisplayParseError, + source_file: SourceFile, + notebook_index: Option, + }, Panic(Option, Box), Read(Option, SourceError), Format(Option, FormatModuleError), @@ -826,6 +838,24 @@ pub(crate) enum FormatCommandError { } impl FormatCommandError { + fn parse(error: ParseError, path: Option<&Path>, source_kind: &SourceKind) -> Self { + let name = path.map_or_else(|| "-".into(), Path::to_string_lossy); + let source_file = SourceFileBuilder::new(name, source_kind.source_code()).finish(); + let notebook_index = source_kind + .as_ipy_notebook() + .map(|notebook| notebook.index().clone()); + + Self::Parse { + error: DisplayParseError::from_source_kind( + error, + path.map(Path::to_path_buf), + source_kind, + ), + source_file, + notebook_index, + } + } + fn path(&self) -> Option<&Path> { match self { Self::Ignore(err) => { @@ -835,7 +865,7 @@ impl FormatCommandError { None } } - Self::Parse(err) => err.path(), + Self::Parse { error, .. } => error.path(), Self::Panic(path, _) | Self::Read(path, _) | Self::Format(path, _) @@ -859,11 +889,15 @@ impl From<&FormatCommandError> for Diagnostic { FormatCommandError::Ignore(error) => { Diagnostic::new(DiagnosticId::Io, Severity::Error, error) } - FormatCommandError::Parse(display_parse_error) => Diagnostic::new( - DiagnosticId::InvalidSyntax, - Severity::Error, - &display_parse_error.error().error, - ), + FormatCommandError::Parse { + error, source_file, .. + } => { + return Diagnostic::invalid_syntax( + source_file.clone(), + &error.error().error, + error.error(), + ); + } FormatCommandError::Panic(path, panic_error) => { return create_panic_diagnostic(panic_error, path.as_deref()); } @@ -912,8 +946,8 @@ impl Display for FormatCommandError { ) } } - Self::Parse(err) => { - write!(f, "{err}") + Self::Parse { error, .. } => { + write!(f, "{error}") } Self::Read(path, err) => { if let Some(path) = path { @@ -1228,7 +1262,6 @@ mod tests { use insta::assert_snapshot; use ruff_db::panic::catch_unwind; - use ruff_linter::logging::DisplayParseError; use ruff_linter::source_kind::{SourceError, SourceKind}; use ruff_python_formatter::FormatModuleError; use ruff_python_parser::{ParseError, ParseErrorType}; @@ -1258,14 +1291,14 @@ mod tests { "Permission denied", ))), }), - FormatCommandError::Parse(DisplayParseError::from_source_kind( + FormatCommandError::parse( ParseError { error: ParseErrorType::UnexpectedIndentation, location: TextRange::default(), }, - Some(path.clone()), + Some(&path), &source_kind, - )), + ), FormatCommandError::Panic(Some(path.clone()), Box::new(panic_error)), FormatCommandError::Read( Some(path.clone()), @@ -1304,9 +1337,6 @@ mod tests { io: test.py: Permission denied --> test.py:1:1 - invalid-syntax: Unexpected indentation - --> test.py:1:1 - io: File not found --> test.py:1:1 @@ -1319,6 +1349,13 @@ mod tests { invalid-cli-option: Range formatting is only supported for Python files. --> test.py:1:1 + invalid-syntax: Unexpected indentation + --> test.py:1:1 + | + 1 | 1 + | ^ + | + panic: Panicked at when checking `test.py`: `Test panic for FormatCommandError` --> test.py:1:1 info: This indicates a bug in Ruff. diff --git a/crates/ruff/tests/cli/format.rs b/crates/ruff/tests/cli/format.rs index 81e1ef4462..efe7bd8175 100644 --- a/crates/ruff/tests/cli/format.rs +++ b/crates/ruff/tests/cli/format.rs @@ -577,7 +577,11 @@ from module import = exit_code: 2 ----- stdout ----- invalid-syntax: Expected an import name - --> main.py:1:1 + --> main.py:2:20 + | + 2 | from module import = + | ^ + | ----- stderr ----- @@ -1944,9 +1948,8 @@ fn test_notebook_trailing_semicolon() -> Result<()> { Ok(()) } -#[test] -fn syntax_error_in_notebooks() -> Result<()> { - let test = CliTest::with_files([ +fn notebook_with_syntax_error() -> Result { + CliTest::with_files([ ( "ruff.toml", r#" @@ -2004,7 +2007,12 @@ include = ["*.ipy"] } "#, ), - ])?; + ]) +} + +#[test] +fn syntax_error_in_notebooks() -> Result<()> { + let test = notebook_with_syntax_error()?; assert_cmd_snapshot!(test.format_command() .args(["--config", "ruff.toml"]) @@ -2020,6 +2028,36 @@ include = ["*.ipy"] Ok(()) } +#[test] +fn syntax_error_in_notebooks_check() -> Result<()> { + let test = notebook_with_syntax_error()?; + + assert_cmd_snapshot!( + test.format_command() + .args(["--config", "ruff.toml"]) + .args(["--extension", "ipy:ipynb"]) + .arg("--check") + .arg("."), + @" + success: false + exit_code: 2 + ----- stdout ----- + invalid-syntax: Expected an expression + --> main.ipy:cell 2:3:24 + | + 1 | for i in range(iterations): + 2 | # выберите случайный индекс в диапазон от 0 до len(X)-1 включительно при помощи функции random.randint + 3 | j = # ваш код здесь + | ^ + | + + + ----- stderr ----- + " + ); + Ok(()) +} + #[test] fn extension() -> Result<()> { let test = CliTest::with_files([ From bcd70c5f10ea97ed52a785d70e7f33b83b7c697a Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:12:51 -0400 Subject: [PATCH 032/390] Exclude Markdown files from `format-dev` runs (#27052) Summary -- This should fix the CI failures I was seeing in #27035: https://github.com/astral-sh/ruff/actions/runs/29854835643/job/88716871906?pr=27035 I guess this didn't appear in #27018 because no formatter files were modified. Test Plan -- CI on this PR, success: https://github.com/astral-sh/ruff/actions/runs/29861350490/job/88738651835?pr=27052 --- crates/ruff_dev/src/format_dev.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs index ac081bff1b..65583465db 100644 --- a/crates/ruff_dev/src/format_dev.rs +++ b/crates/ruff_dev/src/format_dev.rs @@ -40,7 +40,8 @@ fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, ConfigArgumen let args_matches = FormatCommand::command() .no_binary_name(true) .get_matches_from(dirs); - let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?; + let mut arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?; + arguments.extend_exclude = Some(vec![FilePattern::Builtin("*.md")]); let (cli, config_arguments) = arguments.partition(GlobalConfigArgs::default())?; Ok((cli, config_arguments)) } From b30f04023281b46f12011f13ce6b45c247e0d2e3 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:29:44 -0400 Subject: [PATCH 033/390] Stabilize new default rules (#27035) This PR stabilizes the expanded default rule set currently in preview and adds a new generated `Default Rules` page to the docs to avoid needing to include a lengthy selector block in multiple places. --- README.md | 20 +- crates/ruff/src/cache.rs | 4 +- crates/ruff/tests/cli/format.rs | 3 + crates/ruff/tests/cli/lint.rs | 22 +- ...ow_settings__display_default_settings.snap | 768 +++++++++++++++++- crates/ruff/tests/config.rs | 10 +- crates/ruff/tests/integration_test.rs | 9 +- crates/ruff_dev/src/generate_default_rules.rs | 66 ++ crates/ruff_dev/src/generate_options.rs | 6 +- crates/ruff_dev/src/main.rs | 4 + crates/ruff_linter/src/settings/mod.rs | 24 +- .../src/session/index/ruff_settings.rs | 6 +- crates/ruff_wasm/README.md | 2 +- crates/ruff_workspace/src/configuration.rs | 11 +- crates/ruff_workspace/src/options.rs | 10 +- docs/.gitignore | 1 + docs/configuration.md | 24 +- docs/tutorial.md | 8 +- ruff.schema.json | 4 +- scripts/generate_mkdocs.py | 7 + 20 files changed, 883 insertions(+), 126 deletions(-) create mode 100644 crates/ruff_dev/src/generate_default_rules.rs diff --git a/README.md b/README.md index e77738e943..8eb4311acd 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,8 @@ Ruff can be configured through a `pyproject.toml`, `ruff.toml`, or `.ruff.toml` [_Configuration_](https://docs.astral.sh/ruff/configuration/), or [_Settings_](https://docs.astral.sh/ruff/settings/) for a complete list of all configuration options). +For the complete list of enabled rules, see [_Default Rules_](https://docs.astral.sh/ruff/default-rules/). + If left unspecified, Ruff's default configuration is equivalent to the following `ruff.toml` file: ```toml @@ -258,8 +260,7 @@ indent-width = 4 target-version = "py310" [lint] -# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. -select = ["E4", "E7", "E9", "F"] +# select = [...] # See the Default Rules page for the full listing. ignore = [] # Allow fix for all enabled rules (when `--fix`) is provided. @@ -315,18 +316,13 @@ for more on the linting and formatting commands, respectively. isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implements every rule in Rust as a first-party feature. -By default, Ruff enables Flake8's `F` rules, along with a subset of the `E` rules, omitting any -stylistic rules that overlap with the use of a formatter, like `ruff format` or -[Black](https://github.com/psf/black). +By default, Ruff enables rules from the `F`, `E`, `B`, `UP`, and `RUF` categories, +as well as many more, omitting any stylistic rules that overlap with the use of a formatter, like +`ruff format` or [Black](https://github.com/psf/black). If you're just getting started with Ruff, **the default rule set is a great place to start**: it -catches a wide variety of common errors (like unused imports) with zero configuration. - -In [preview](https://docs.astral.sh/ruff/preview/), Ruff enables an expanded set of default rules -that includes rules from the `B`, `UP`, and `RUF` categories, as well as many more. If you give the -new defaults a try, feel free to leave feedback in the [GitHub -discussion](https://github.com/astral-sh/ruff/discussions/23203), where you can also find the new -rule set listed in full. +catches a wide variety of common errors (like unused imports) with zero configuration. See +[_Default Rules_](https://docs.astral.sh/ruff/default-rules/) for the complete list. diff --git a/crates/ruff/src/cache.rs b/crates/ruff/src/cache.rs index fc8d756b33..33e3721df7 100644 --- a/crates/ruff/src/cache.rs +++ b/crates/ruff/src/cache.rs @@ -514,6 +514,7 @@ mod tests { use ruff_cache::CACHE_DIR_NAME; use ruff_linter::package::PackageRoot; + use ruff_linter::registry::Rule; use ruff_linter::settings::LinterSettings; use ruff_linter::settings::flags; use ruff_linter::settings::types::UnsafeFixes; @@ -537,7 +538,7 @@ mod tests { cache_dir, linter: LinterSettings { unresolved_target_version: PythonVersion::latest().into(), - ..Default::default() + ..LinterSettings::for_rule(Rule::UnusedVariable) }, ..Settings::default() }; @@ -1031,6 +1032,7 @@ mod tests { let settings = Settings { cache_dir, + linter: LinterSettings::for_rule(Rule::UndefinedExport), ..Settings::default() }; diff --git a/crates/ruff/tests/cli/format.rs b/crates/ruff/tests/cli/format.rs index efe7bd8175..842f45b1a0 100644 --- a/crates/ruff/tests/cli/format.rs +++ b/crates/ruff/tests/cli/format.rs @@ -262,6 +262,9 @@ fn format_options() -> Result<()> { indent-width = 8 line-length = 84 +[lint] +isort.split-on-trailing-comma = false + [format] indent-style = "tab" quote-style = "single" diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index b5afa84b76..fac30767d2 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -46,7 +46,8 @@ inline-quotes = "single" test.py:1:5: Q000 [*] Double quotes found but single quotes preferred test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading test.py:1:19: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. + test.py:1:19: PLE1310 String `strip` call contains duplicate characters + Found 4 errors. [*] 2 fixable with the `--fix` option. ----- stderr ----- @@ -83,7 +84,8 @@ inline-quotes = "single" -:1:5: Q000 [*] Double quotes found but single quotes preferred -:1:5: B005 Using `.strip()` with multi-character strings is misleading -:1:19: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. + -:1:19: PLE1310 String `strip` call contains duplicate characters + Found 4 errors. [*] 2 fixable with the `--fix` option. ----- stderr ----- @@ -117,7 +119,8 @@ inline-quotes = "single" -:1:5: Q000 [*] Double quotes found but single quotes preferred -:1:5: B005 Using `.strip()` with multi-character strings is misleading -:1:19: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. + -:1:19: PLE1310 String `strip` call contains duplicate characters + Found 4 errors. [*] 2 fixable with the `--fix` option. ----- stderr ----- @@ -157,7 +160,8 @@ inline-quotes = "single" -:1:5: Q000 [*] Double quotes found but single quotes preferred -:1:5: B005 Using `.strip()` with multi-character strings is misleading -:1:19: Q000 [*] Double quotes found but single quotes preferred - Found 3 errors. + -:1:19: PLE1310 String `strip` call contains duplicate characters + Found 4 errors. [*] 2 fixable with the `--fix` option. ----- stderr ----- @@ -3980,7 +3984,7 @@ fn walrus_before_py38() { .args(["--stdin-filename", "test.py"]) .arg("--target-version=py38") .arg("-") - .pass_stdin(r#"(x := 1)"#), + .pass_stdin(r#"if (x := 1): ..."#), @" success: true exit_code: 0 @@ -3997,12 +4001,12 @@ fn walrus_before_py38() { .args(["--stdin-filename", "test.py"]) .arg("--target-version=py37") .arg("-") - .pass_stdin(r#"(x := 1)"#), + .pass_stdin(r#"if (x := 1): ..."#), @" success: false exit_code: 1 ----- stdout ----- - test.py:1:2: invalid-syntax: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8) + test.py:1:5: invalid-syntax: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8) Found 1 error. ----- stderr ----- @@ -4766,7 +4770,7 @@ fn supported_file_extensions_preview_enabled() -> Result<()> { } #[test] -fn preview_default_rules() -> Result<()> { +fn default_rules() -> Result<()> { let test = CliTest::with_settings(|_path, mut settings| { settings.add_filter(r"(?s).*(linter\.rules\.enabled[^]]+]).*", "$1"); settings @@ -4775,7 +4779,7 @@ fn preview_default_rules() -> Result<()> { test.write_file("try.py", "1")?; assert_cmd_snapshot!( - test.check_command().args(["--preview", "--show-settings"]), + test.check_command().arg("--show-settings"), @" linter.rules.enabled = [ sys-version-slice3 (YTT101), diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap index 4f6d0ed870..176e9fc71d 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap @@ -68,28 +68,207 @@ file_resolver.project_root = "[TMP]/" linter.exclude = [] linter.project_root = "[TMP]/" linter.rules.enabled = [ - multiple-imports-on-one-line (E401), - module-import-not-at-top-of-file (E402), - multiple-statements-on-one-line-colon (E701), - multiple-statements-on-one-line-semicolon (E702), - useless-semicolon (E703), - none-comparison (E711), - true-false-comparison (E712), - not-in-test (E713), - not-is-test (E714), - type-comparison (E721), + sys-version-slice3 (YTT101), + sys-version2 (YTT102), + sys-version-cmp-str3 (YTT103), + sys-version-info0-eq3 (YTT201), + six-py3 (YTT202), + sys-version-info1-cmp-int (YTT203), + sys-version-info-minor-cmp-int (YTT204), + sys-version0 (YTT301), + sys-version-cmp-str10 (YTT302), + sys-version-slice1 (YTT303), + cancel-scope-no-checkpoint (ASYNC100), + trio-sync-call (ASYNC105), + async-zero-sleep (ASYNC115), + long-sleep-not-forever (ASYNC116), + blocking-http-call-in-async-function (ASYNC210), + create-subprocess-in-async-function (ASYNC220), + run-process-in-async-function (ASYNC221), + wait-for-process-in-async-function (ASYNC222), + blocking-open-call-in-async-function (ASYNC230), + blocking-sleep-in-async-function (ASYNC251), + exec-builtin (S102), + try-except-pass (S110), + try-except-continue (S112), + blind-except (BLE001), + unary-prefix-increment-decrement (B002), + assignment-to-os-environ (B003), + unreliable-callable-check (B004), + strip-with-multi-characters (B005), + mutable-argument-default (B006), + function-call-in-default-argument (B008), + get-attr-with-constant (B009), + set-attr-with-constant (B010), + jump-statement-in-finally (B012), + redundant-tuple-in-exception-handler (B013), + duplicate-handler-exception (B014), + useless-comparison (B015), + raise-literal (B016), + assert-raises-exception (B017), + useless-expression (B018), + cached-instance-method (B019), + loop-variable-overrides-iterator (B020), + f-string-docstring (B021), + useless-contextlib-suppress (B022), + function-uses-loop-variable (B023), + duplicate-try-block-exception (B025), + star-arg-unpacking-after-keyword-arg (B026), + except-with-empty-tuple (B029), + except-with-non-exception-classes (B030), + reuse-of-groupby-generator (B031), + unintentional-type-annotation (B032), + duplicate-value (B033), + static-key-dict-comprehension (B035), + mutable-contextvar-default (B039), + unnecessary-generator-list (C400), + unnecessary-generator-set (C401), + unnecessary-generator-dict (C402), + unnecessary-list-comprehension-set (C403), + unnecessary-list-comprehension-dict (C404), + unnecessary-literal-set (C405), + unnecessary-literal-dict (C406), + unnecessary-collection-call (C408), + unnecessary-literal-within-tuple-call (C409), + unnecessary-literal-within-list-call (C410), + unnecessary-list-call (C411), + unnecessary-call-around-sorted (C413), + unnecessary-double-cast-or-process (C414), + unnecessary-subscript-reversal (C415), + unnecessary-map (C417), + unnecessary-literal-within-dict-call (C418), + unnecessary-comprehension-in-call (C419), + call-datetime-without-tzinfo (DTZ001), + call-datetime-today (DTZ002), + call-datetime-utcnow (DTZ003), + call-datetime-utcfromtimestamp (DTZ004), + call-datetime-now-without-tzinfo (DTZ005), + call-datetime-fromtimestamp (DTZ006), + call-datetime-strptime-without-zone (DTZ007), + call-date-today (DTZ011), + call-date-fromtimestamp (DTZ012), + datetime-min-max (DTZ901), + debugger (T100), + shebang-not-executable (EXE001), + shebang-missing-executable-file (EXE002), + shebang-leading-whitespace (EXE004), + shebang-not-first-line (EXE005), + future-rewritable-type-annotation (FA100), + future-required-type-annotation (FA102), + f-string-in-get-text-func-call (INT001), + format-in-get-text-func-call (INT002), + printf-in-get-text-func-call (INT003), + direct-logger-instantiation (LOG001), + invalid-get-logger-argument (LOG002), + undocumented-warn (LOG009), + exc-info-outside-except-handler (LOG014), + root-logger-call (LOG015), + logging-warn (G010), + logging-extra-attr-clash (G101), + logging-exc-info (G201), + logging-redundant-exc-info (G202), + unnecessary-placeholder (PIE790), + duplicate-class-field-definition (PIE794), + non-unique-enums (PIE796), + unnecessary-spread (PIE800), + unnecessary-dict-kwargs (PIE804), + reimplemented-container-builtin (PIE807), + unnecessary-range-start (PIE808), + multiple-starts-ends-with (PIE810), + unprefixed-type-param (PYI001), + complex-if-statement-in-stub (PYI002), + unrecognized-version-info-check (PYI003), + patch-version-comparison (PYI004), + wrong-tuple-length-version-comparison (PYI005), + bad-version-info-comparison (PYI006), + unrecognized-platform-check (PYI007), + unrecognized-platform-name (PYI008), + pass-statement-stub-body (PYI009), + non-empty-stub-body (PYI010), + pass-in-class-body (PYI012), + ellipsis-in-non-empty-class-body (PYI013), + assignment-default-in-stub (PYI015), + duplicate-union-member (PYI016), + complex-assignment-in-stub (PYI017), + unused-private-type-var (PYI018), + custom-type-var-for-self (PYI019), + quoted-annotation-in-stub (PYI020), + unaliased-collections-abc-set-import (PYI025), + type-alias-without-annotation (PYI026), + str-or-repr-defined-in-stub (PYI029), + unnecessary-literal-union (PYI030), + any-eq-ne-annotation (PYI032), + legacy-type-comment (PYI033), + non-self-return-type (PYI034), + unassigned-special-variable-in-stub (PYI035), + bad-exit-annotation (PYI036), + redundant-numeric-union (PYI041), + snake-case-type-alias (PYI042), + t-suffixed-type-alias (PYI043), + future-annotations-in-stub (PYI044), + iter-method-return-iterable (PYI045), + unused-private-protocol (PYI046), + unused-private-type-alias (PYI047), + stub-body-multiple-statements (PYI048), + unused-private-typed-dict (PYI049), + no-return-argument-annotation-in-stub (PYI050), + unannotated-assignment-in-stub (PYI052), + unnecessary-type-union (PYI055), + byte-string-usage (PYI057), + generator-return-from-iter-method (PYI058), + generic-not-last-base-class (PYI059), + redundant-none-literal (PYI061), + duplicate-literal-member (PYI062), + pep484-style-positional-only-parameter (PYI063), + redundant-final-literal (PYI064), + bad-version-info-order (PYI066), + pytest-raises-without-exception (PT010), + pytest-duplicate-parametrize-test-cases (PT014), + pytest-deprecated-yield-fixture (PT020), + pytest-erroneous-use-fixtures-on-fixture (PT025), + pytest-use-fixtures-without-parameters (PT026), + pytest-warns-with-multiple-statements (PT031), + unnecessary-return-none (RET501), + duplicate-isinstance-call (SIM101), + collapsible-if (SIM102), + needless-bool (SIM103), + return-in-try-except-finally (SIM107), + enumerate-for-loop (SIM113), + if-with-same-arms (SIM114), + open-file-with-context-handler (SIM115), + multiple-with-statements (SIM117), + in-dict-keys (SIM118), + negate-equal-op (SIM201), + negate-not-equal-op (SIM202), + double-negation (SIM208), + if-expr-with-true-false (SIM210), + if-expr-with-false-true (SIM211), + expr-and-not-expr (SIM220), + expr-or-not-expr (SIM221), + expr-or-true (SIM222), + expr-and-false (SIM223), + if-else-block-instead-of-dict-get (SIM401), + split-static-string (SIM905), + zip-dict-keys-and-values (SIM911), + runtime-import-in-type-checking-block (TC004), + empty-type-checking-block (TC005), + unquoted-type-alias (TC007), + runtime-string-union (TC010), + py-path (PTH124), + invalid-pathlib-with-suffix (PTH210), + static-join-to-f-string (FLY002), + unsorted-imports (I001), + invalid-module-name (N999), + unnecessary-list-cast (PERF101), + incorrect-dict-iterator (PERF102), + manual-list-copy (PERF402), bare-except (E722), - lambda-assignment (E731), - ambiguous-variable-name (E741), - ambiguous-class-name (E742), - ambiguous-function-name (E743), io-error (E902), + invalid-escape-sequence (W605), + empty-docstring (D419), unused-import (F401), import-shadowed-by-loop-var (F402), - undefined-local-with-import-star (F403), late-future-import (F404), - undefined-local-with-import-star-usage (F405), - undefined-local-with-nested-import-star-usage (F406), future-feature-not-defined (F407), percent-format-invalid-format (F501), percent-format-expected-mapping (F502), @@ -119,7 +298,6 @@ linter.rules.enabled = [ yield-outside-function (F704), return-outside-function (F706), default-except-not-last (F707), - forward-annotation-syntax-error (F722), redefined-while-unused (F811), undefined-name (F821), undefined-export (F822), @@ -127,30 +305,379 @@ linter.rules.enabled = [ unused-variable (F841), unused-annotation (F842), raise-not-implemented (F901), + invalid-mock-access (PGH005), + type-name-incorrect-variance (PLC0105), + type-bivariance (PLC0131), + type-param-name-mismatch (PLC0132), + single-string-slots (PLC0205), + dict-index-missing-items (PLC0206), + iteration-over-set (PLC0208), + useless-import-alias (PLC0414), + unnecessary-direct-lambda-call (PLC3002), + yield-in-init (PLE0100), + return-in-init (PLE0101), + nonlocal-and-global (PLE0115), + continue-in-finally (PLE0116), + nonlocal-without-binding (PLE0117), + load-before-global-declaration (PLE0118), + invalid-length-return-type (PLE0303), + invalid-index-return-type (PLE0305), + invalid-str-return-type (PLE0307), + invalid-bytes-return-type (PLE0308), + invalid-hash-return-type (PLE0309), + invalid-all-object (PLE0604), + invalid-all-format (PLE0605), + potential-index-error (PLE0643), + misplaced-bare-raise (PLE0704), + repeated-keyword-argument (PLE1132), + await-outside-async (PLE1142), + logging-too-many-args (PLE1205), + logging-too-few-args (PLE1206), + bad-string-format-character (PLE1300), + bad-string-format-type (PLE1307), + bad-str-strip-call (PLE1310), + invalid-envvar-value (PLE1507), + singledispatch-method (PLE1519), + singledispatchmethod-function (PLE1520), + yield-from-in-async-function (PLE1700), + bidirectional-unicode (PLE2502), + invalid-character-backspace (PLE2510), + invalid-character-sub (PLE2512), + invalid-character-esc (PLE2513), + invalid-character-nul (PLE2514), + invalid-character-zero-width-space (PLE2515), + comparison-with-itself (PLR0124), + comparison-of-constant (PLR0133), + property-with-parameters (PLR0206), + manual-from-import (PLR0402), + redefined-argument-from-local (PLR1704), + useless-return (PLR1711), + boolean-chained-comparison (PLR1716), + sys-exit-alias (PLR1722), + if-stmt-min-max (PLR1730), + unnecessary-dict-index-lookup (PLR1733), + unnecessary-list-index-lookup (PLR1736), + empty-comment (PLR2044), + useless-else-on-loop (PLW0120), + self-assigning-variable (PLW0127), + redeclared-assigned-name (PLW0128), + assert-on-string-literal (PLW0129), + named-expr-without-context (PLW0131), + useless-exception-statement (PLW0133), + nan-comparison (PLW0177), + bad-staticmethod-argument (PLW0211), + super-without-brackets (PLW0245), + import-self (PLW0406), + global-variable-not-assigned (PLW0602), + global-at-module-level (PLW0604), + self-or-cls-assignment (PLW0642), + binary-op-exception (PLW0711), + bad-open-mode (PLW1501), + shallow-copy-environ (PLW1507), + invalid-envvar-default (PLW1508), + subprocess-popen-preexec-fn (PLW1509), + subprocess-run-without-check (PLW1510), + useless-with-lock (PLW2101), + useless-metaclass-type (UP001), + type-of-primitive (UP003), + useless-object-inheritance (UP004), + deprecated-unittest-alias (UP005), + non-pep585-annotation (UP006), + non-pep604-annotation-union (UP007), + super-call-with-parameters (UP008), + utf8-encoding-declaration (UP009), + unnecessary-future-import (UP010), + lru-cache-without-parameters (UP011), + unnecessary-encode-utf8 (UP012), + convert-named-tuple-functional-to-class (UP014), + datetime-timezone-utc (UP017), + native-literals (UP018), + typing-text-str-alias (UP019), + open-alias (UP020), + replace-universal-newlines (UP021), + replace-stdout-stderr (UP022), + deprecated-c-element-tree (UP023), + os-error-alias (UP024), + unicode-kind-prefix (UP025), + deprecated-mock-import (UP026), + yield-in-for-loop (UP028), + unnecessary-builtin-import (UP029), + format-literals (UP030), + printf-string-formatting (UP031), + f-string (UP032), + lru-cache-with-maxsize-none (UP033), + extraneous-parentheses (UP034), + deprecated-import (UP035), + outdated-version-block (UP036), + quoted-annotation (UP037), + unnecessary-class-parentheses (UP039), + non-pep695-type-alias (UP040), + timeout-error-alias (UP041), + unnecessary-default-type-args (UP043), + non-pep646-unpack (UP044), + non-pep604-annotation-optional (UP045), + non-pep695-generic-class (UP046), + non-pep695-generic-function (UP047), + private-type-parameter (UP049), + useless-class-metaclass-type (UP050), + print-empty-string (FURB105), + for-loop-writes (FURB122), + readlines-in-for (FURB129), + check-and-remove-from-set (FURB132), + if-expr-min-max (FURB136), + verbose-decimal-constructor (FURB157), + bit-count (FURB161), + fromisoformat-replace-z (FURB162), + redundant-log-base (FURB163), + int-on-sliced-str (FURB166), + regex-flag-alias (FURB167), + isinstance-type-none (FURB168), + type-none-comparison (FURB169), + implicit-cwd (FURB177), + hashlib-digest-hex (FURB181), + slice-to-remove-prefix-or-suffix (FURB188), + zip-instead-of-pairwise (RUF007), + mutable-dataclass-default (RUF008), + function-call-in-dataclass-default-argument (RUF009), + explicit-f-string-type-conversion (RUF010), + mutable-class-default (RUF012), + implicit-optional (RUF013), + unnecessary-iterable-allocation-for-first-element (RUF015), + invalid-index-type (RUF016), + quadratic-list-summation (RUF017), + assignment-in-assert (RUF018), + unnecessary-key-check (RUF019), + never-union (RUF020), + unsorted-dunder-all (RUF022), + unsorted-dunder-slots (RUF023), + mutable-fromkeys-value (RUF024), + default-factory-kwarg (RUF026), + invalid-formatter-suppression-comment (RUF028), + assert-with-print-message (RUF030), + decimal-from-float-literal (RUF032), + post-init-default (RUF033), + useless-if-else (RUF034), + invalid-assert-message-literal-argument (RUF040), + unnecessary-nested-literal (RUF041), + unnecessary-cast-to-int (RUF046), + map-int-version-parsing (RUF048), + dataclass-enum (RUF049), + if-key-in-dict-del (RUF051), + class-with-mixed-type-vars (RUF053), + unnecessary-round (RUF057), + starmap-zip (RUF058), + unused-unpacked-variable (RUF059), + unused-noqa (RUF100), + redirected-noqa (RUF101), + invalid-pyproject-toml (RUF200), + raise-vanilla-class (TRY002), + type-check-without-type-error (TRY004), + verbose-raise (TRY201), + useless-try-except (TRY203), + verbose-log-message (TRY401), ] linter.rules.should_fix = [ - multiple-imports-on-one-line (E401), - module-import-not-at-top-of-file (E402), - multiple-statements-on-one-line-colon (E701), - multiple-statements-on-one-line-semicolon (E702), - useless-semicolon (E703), - none-comparison (E711), - true-false-comparison (E712), - not-in-test (E713), - not-is-test (E714), - type-comparison (E721), + sys-version-slice3 (YTT101), + sys-version2 (YTT102), + sys-version-cmp-str3 (YTT103), + sys-version-info0-eq3 (YTT201), + six-py3 (YTT202), + sys-version-info1-cmp-int (YTT203), + sys-version-info-minor-cmp-int (YTT204), + sys-version0 (YTT301), + sys-version-cmp-str10 (YTT302), + sys-version-slice1 (YTT303), + cancel-scope-no-checkpoint (ASYNC100), + trio-sync-call (ASYNC105), + async-zero-sleep (ASYNC115), + long-sleep-not-forever (ASYNC116), + blocking-http-call-in-async-function (ASYNC210), + create-subprocess-in-async-function (ASYNC220), + run-process-in-async-function (ASYNC221), + wait-for-process-in-async-function (ASYNC222), + blocking-open-call-in-async-function (ASYNC230), + blocking-sleep-in-async-function (ASYNC251), + exec-builtin (S102), + try-except-pass (S110), + try-except-continue (S112), + blind-except (BLE001), + unary-prefix-increment-decrement (B002), + assignment-to-os-environ (B003), + unreliable-callable-check (B004), + strip-with-multi-characters (B005), + mutable-argument-default (B006), + function-call-in-default-argument (B008), + get-attr-with-constant (B009), + set-attr-with-constant (B010), + jump-statement-in-finally (B012), + redundant-tuple-in-exception-handler (B013), + duplicate-handler-exception (B014), + useless-comparison (B015), + raise-literal (B016), + assert-raises-exception (B017), + useless-expression (B018), + cached-instance-method (B019), + loop-variable-overrides-iterator (B020), + f-string-docstring (B021), + useless-contextlib-suppress (B022), + function-uses-loop-variable (B023), + duplicate-try-block-exception (B025), + star-arg-unpacking-after-keyword-arg (B026), + except-with-empty-tuple (B029), + except-with-non-exception-classes (B030), + reuse-of-groupby-generator (B031), + unintentional-type-annotation (B032), + duplicate-value (B033), + static-key-dict-comprehension (B035), + mutable-contextvar-default (B039), + unnecessary-generator-list (C400), + unnecessary-generator-set (C401), + unnecessary-generator-dict (C402), + unnecessary-list-comprehension-set (C403), + unnecessary-list-comprehension-dict (C404), + unnecessary-literal-set (C405), + unnecessary-literal-dict (C406), + unnecessary-collection-call (C408), + unnecessary-literal-within-tuple-call (C409), + unnecessary-literal-within-list-call (C410), + unnecessary-list-call (C411), + unnecessary-call-around-sorted (C413), + unnecessary-double-cast-or-process (C414), + unnecessary-subscript-reversal (C415), + unnecessary-map (C417), + unnecessary-literal-within-dict-call (C418), + unnecessary-comprehension-in-call (C419), + call-datetime-without-tzinfo (DTZ001), + call-datetime-today (DTZ002), + call-datetime-utcnow (DTZ003), + call-datetime-utcfromtimestamp (DTZ004), + call-datetime-now-without-tzinfo (DTZ005), + call-datetime-fromtimestamp (DTZ006), + call-datetime-strptime-without-zone (DTZ007), + call-date-today (DTZ011), + call-date-fromtimestamp (DTZ012), + datetime-min-max (DTZ901), + debugger (T100), + shebang-not-executable (EXE001), + shebang-missing-executable-file (EXE002), + shebang-leading-whitespace (EXE004), + shebang-not-first-line (EXE005), + future-rewritable-type-annotation (FA100), + future-required-type-annotation (FA102), + f-string-in-get-text-func-call (INT001), + format-in-get-text-func-call (INT002), + printf-in-get-text-func-call (INT003), + direct-logger-instantiation (LOG001), + invalid-get-logger-argument (LOG002), + undocumented-warn (LOG009), + exc-info-outside-except-handler (LOG014), + root-logger-call (LOG015), + logging-warn (G010), + logging-extra-attr-clash (G101), + logging-exc-info (G201), + logging-redundant-exc-info (G202), + unnecessary-placeholder (PIE790), + duplicate-class-field-definition (PIE794), + non-unique-enums (PIE796), + unnecessary-spread (PIE800), + unnecessary-dict-kwargs (PIE804), + reimplemented-container-builtin (PIE807), + unnecessary-range-start (PIE808), + multiple-starts-ends-with (PIE810), + unprefixed-type-param (PYI001), + complex-if-statement-in-stub (PYI002), + unrecognized-version-info-check (PYI003), + patch-version-comparison (PYI004), + wrong-tuple-length-version-comparison (PYI005), + bad-version-info-comparison (PYI006), + unrecognized-platform-check (PYI007), + unrecognized-platform-name (PYI008), + pass-statement-stub-body (PYI009), + non-empty-stub-body (PYI010), + pass-in-class-body (PYI012), + ellipsis-in-non-empty-class-body (PYI013), + assignment-default-in-stub (PYI015), + duplicate-union-member (PYI016), + complex-assignment-in-stub (PYI017), + unused-private-type-var (PYI018), + custom-type-var-for-self (PYI019), + quoted-annotation-in-stub (PYI020), + unaliased-collections-abc-set-import (PYI025), + type-alias-without-annotation (PYI026), + str-or-repr-defined-in-stub (PYI029), + unnecessary-literal-union (PYI030), + any-eq-ne-annotation (PYI032), + legacy-type-comment (PYI033), + non-self-return-type (PYI034), + unassigned-special-variable-in-stub (PYI035), + bad-exit-annotation (PYI036), + redundant-numeric-union (PYI041), + snake-case-type-alias (PYI042), + t-suffixed-type-alias (PYI043), + future-annotations-in-stub (PYI044), + iter-method-return-iterable (PYI045), + unused-private-protocol (PYI046), + unused-private-type-alias (PYI047), + stub-body-multiple-statements (PYI048), + unused-private-typed-dict (PYI049), + no-return-argument-annotation-in-stub (PYI050), + unannotated-assignment-in-stub (PYI052), + unnecessary-type-union (PYI055), + byte-string-usage (PYI057), + generator-return-from-iter-method (PYI058), + generic-not-last-base-class (PYI059), + redundant-none-literal (PYI061), + duplicate-literal-member (PYI062), + pep484-style-positional-only-parameter (PYI063), + redundant-final-literal (PYI064), + bad-version-info-order (PYI066), + pytest-raises-without-exception (PT010), + pytest-duplicate-parametrize-test-cases (PT014), + pytest-deprecated-yield-fixture (PT020), + pytest-erroneous-use-fixtures-on-fixture (PT025), + pytest-use-fixtures-without-parameters (PT026), + pytest-warns-with-multiple-statements (PT031), + unnecessary-return-none (RET501), + duplicate-isinstance-call (SIM101), + collapsible-if (SIM102), + needless-bool (SIM103), + return-in-try-except-finally (SIM107), + enumerate-for-loop (SIM113), + if-with-same-arms (SIM114), + open-file-with-context-handler (SIM115), + multiple-with-statements (SIM117), + in-dict-keys (SIM118), + negate-equal-op (SIM201), + negate-not-equal-op (SIM202), + double-negation (SIM208), + if-expr-with-true-false (SIM210), + if-expr-with-false-true (SIM211), + expr-and-not-expr (SIM220), + expr-or-not-expr (SIM221), + expr-or-true (SIM222), + expr-and-false (SIM223), + if-else-block-instead-of-dict-get (SIM401), + split-static-string (SIM905), + zip-dict-keys-and-values (SIM911), + runtime-import-in-type-checking-block (TC004), + empty-type-checking-block (TC005), + unquoted-type-alias (TC007), + runtime-string-union (TC010), + py-path (PTH124), + invalid-pathlib-with-suffix (PTH210), + static-join-to-f-string (FLY002), + unsorted-imports (I001), + invalid-module-name (N999), + unnecessary-list-cast (PERF101), + incorrect-dict-iterator (PERF102), + manual-list-copy (PERF402), bare-except (E722), - lambda-assignment (E731), - ambiguous-variable-name (E741), - ambiguous-class-name (E742), - ambiguous-function-name (E743), io-error (E902), + invalid-escape-sequence (W605), + empty-docstring (D419), unused-import (F401), import-shadowed-by-loop-var (F402), - undefined-local-with-import-star (F403), late-future-import (F404), - undefined-local-with-import-star-usage (F405), - undefined-local-with-nested-import-star-usage (F406), future-feature-not-defined (F407), percent-format-invalid-format (F501), percent-format-expected-mapping (F502), @@ -180,7 +707,6 @@ linter.rules.should_fix = [ yield-outside-function (F704), return-outside-function (F706), default-except-not-last (F707), - forward-annotation-syntax-error (F722), redefined-while-unused (F811), undefined-name (F821), undefined-export (F822), @@ -188,6 +714,176 @@ linter.rules.should_fix = [ unused-variable (F841), unused-annotation (F842), raise-not-implemented (F901), + invalid-mock-access (PGH005), + type-name-incorrect-variance (PLC0105), + type-bivariance (PLC0131), + type-param-name-mismatch (PLC0132), + single-string-slots (PLC0205), + dict-index-missing-items (PLC0206), + iteration-over-set (PLC0208), + useless-import-alias (PLC0414), + unnecessary-direct-lambda-call (PLC3002), + yield-in-init (PLE0100), + return-in-init (PLE0101), + nonlocal-and-global (PLE0115), + continue-in-finally (PLE0116), + nonlocal-without-binding (PLE0117), + load-before-global-declaration (PLE0118), + invalid-length-return-type (PLE0303), + invalid-index-return-type (PLE0305), + invalid-str-return-type (PLE0307), + invalid-bytes-return-type (PLE0308), + invalid-hash-return-type (PLE0309), + invalid-all-object (PLE0604), + invalid-all-format (PLE0605), + potential-index-error (PLE0643), + misplaced-bare-raise (PLE0704), + repeated-keyword-argument (PLE1132), + await-outside-async (PLE1142), + logging-too-many-args (PLE1205), + logging-too-few-args (PLE1206), + bad-string-format-character (PLE1300), + bad-string-format-type (PLE1307), + bad-str-strip-call (PLE1310), + invalid-envvar-value (PLE1507), + singledispatch-method (PLE1519), + singledispatchmethod-function (PLE1520), + yield-from-in-async-function (PLE1700), + bidirectional-unicode (PLE2502), + invalid-character-backspace (PLE2510), + invalid-character-sub (PLE2512), + invalid-character-esc (PLE2513), + invalid-character-nul (PLE2514), + invalid-character-zero-width-space (PLE2515), + comparison-with-itself (PLR0124), + comparison-of-constant (PLR0133), + property-with-parameters (PLR0206), + manual-from-import (PLR0402), + redefined-argument-from-local (PLR1704), + useless-return (PLR1711), + boolean-chained-comparison (PLR1716), + sys-exit-alias (PLR1722), + if-stmt-min-max (PLR1730), + unnecessary-dict-index-lookup (PLR1733), + unnecessary-list-index-lookup (PLR1736), + empty-comment (PLR2044), + useless-else-on-loop (PLW0120), + self-assigning-variable (PLW0127), + redeclared-assigned-name (PLW0128), + assert-on-string-literal (PLW0129), + named-expr-without-context (PLW0131), + useless-exception-statement (PLW0133), + nan-comparison (PLW0177), + bad-staticmethod-argument (PLW0211), + super-without-brackets (PLW0245), + import-self (PLW0406), + global-variable-not-assigned (PLW0602), + global-at-module-level (PLW0604), + self-or-cls-assignment (PLW0642), + binary-op-exception (PLW0711), + bad-open-mode (PLW1501), + shallow-copy-environ (PLW1507), + invalid-envvar-default (PLW1508), + subprocess-popen-preexec-fn (PLW1509), + subprocess-run-without-check (PLW1510), + useless-with-lock (PLW2101), + useless-metaclass-type (UP001), + type-of-primitive (UP003), + useless-object-inheritance (UP004), + deprecated-unittest-alias (UP005), + non-pep585-annotation (UP006), + non-pep604-annotation-union (UP007), + super-call-with-parameters (UP008), + utf8-encoding-declaration (UP009), + unnecessary-future-import (UP010), + lru-cache-without-parameters (UP011), + unnecessary-encode-utf8 (UP012), + convert-named-tuple-functional-to-class (UP014), + datetime-timezone-utc (UP017), + native-literals (UP018), + typing-text-str-alias (UP019), + open-alias (UP020), + replace-universal-newlines (UP021), + replace-stdout-stderr (UP022), + deprecated-c-element-tree (UP023), + os-error-alias (UP024), + unicode-kind-prefix (UP025), + deprecated-mock-import (UP026), + yield-in-for-loop (UP028), + unnecessary-builtin-import (UP029), + format-literals (UP030), + printf-string-formatting (UP031), + f-string (UP032), + lru-cache-with-maxsize-none (UP033), + extraneous-parentheses (UP034), + deprecated-import (UP035), + outdated-version-block (UP036), + quoted-annotation (UP037), + unnecessary-class-parentheses (UP039), + non-pep695-type-alias (UP040), + timeout-error-alias (UP041), + unnecessary-default-type-args (UP043), + non-pep646-unpack (UP044), + non-pep604-annotation-optional (UP045), + non-pep695-generic-class (UP046), + non-pep695-generic-function (UP047), + private-type-parameter (UP049), + useless-class-metaclass-type (UP050), + print-empty-string (FURB105), + for-loop-writes (FURB122), + readlines-in-for (FURB129), + check-and-remove-from-set (FURB132), + if-expr-min-max (FURB136), + verbose-decimal-constructor (FURB157), + bit-count (FURB161), + fromisoformat-replace-z (FURB162), + redundant-log-base (FURB163), + int-on-sliced-str (FURB166), + regex-flag-alias (FURB167), + isinstance-type-none (FURB168), + type-none-comparison (FURB169), + implicit-cwd (FURB177), + hashlib-digest-hex (FURB181), + slice-to-remove-prefix-or-suffix (FURB188), + zip-instead-of-pairwise (RUF007), + mutable-dataclass-default (RUF008), + function-call-in-dataclass-default-argument (RUF009), + explicit-f-string-type-conversion (RUF010), + mutable-class-default (RUF012), + implicit-optional (RUF013), + unnecessary-iterable-allocation-for-first-element (RUF015), + invalid-index-type (RUF016), + quadratic-list-summation (RUF017), + assignment-in-assert (RUF018), + unnecessary-key-check (RUF019), + never-union (RUF020), + unsorted-dunder-all (RUF022), + unsorted-dunder-slots (RUF023), + mutable-fromkeys-value (RUF024), + default-factory-kwarg (RUF026), + invalid-formatter-suppression-comment (RUF028), + assert-with-print-message (RUF030), + decimal-from-float-literal (RUF032), + post-init-default (RUF033), + useless-if-else (RUF034), + invalid-assert-message-literal-argument (RUF040), + unnecessary-nested-literal (RUF041), + unnecessary-cast-to-int (RUF046), + map-int-version-parsing (RUF048), + dataclass-enum (RUF049), + if-key-in-dict-del (RUF051), + class-with-mixed-type-vars (RUF053), + unnecessary-round (RUF057), + starmap-zip (RUF058), + unused-unpacked-variable (RUF059), + unused-noqa (RUF100), + redirected-noqa (RUF101), + invalid-pyproject-toml (RUF200), + raise-vanilla-class (TRY002), + type-check-without-type-error (TRY004), + verbose-raise (TRY201), + useless-try-except (TRY203), + verbose-log-message (TRY401), ] linter.per_file_ignores = {} linter.safety_table.forced_safe = [] diff --git a/crates/ruff/tests/config.rs b/crates/ruff/tests/config.rs index 7ef338f68c..d41b996be9 100644 --- a/crates/ruff/tests/config.rs +++ b/crates/ruff/tests/config.rs @@ -21,12 +21,12 @@ fn lint_select() { specific prefixes. `ignore` takes precedence over `select` if the same prefix appears in both. - Default value: ["E4", "E7", "E9", "F"] + Default value: See https://docs.astral.sh/ruff/default-rules/ or run `ruff check --show-settings --isolated` Type: list[RuleSelector] Example usage: ```toml - # On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`). - select = ["E4", "E7", "E9", "F", "B", "Q"] + # On top of the defaults, enable flake8-bugbear (`B`) and flake8-quotes (`Q`). + extend-select = ["B", "Q"] ``` ----- stderr ----- @@ -43,10 +43,10 @@ fn lint_select_json() { ----- stdout ----- { "doc": "A list of rule codes or prefixes to enable. Prefixes can specify exact\nrules (like `F841`), entire categories (like `F`), or anything in\nbetween.\n\nWhen breaking ties between enabled and disabled rules (via `select` and\n`ignore`, respectively), more specific prefixes override less\nspecific prefixes. `ignore` takes precedence over `select` if the\nsame prefix appears in both.", - "default": "[\"E4\", \"E7\", \"E9\", \"F\"]", + "default": "See https://docs.astral.sh/ruff/default-rules/ or run `ruff check --show-settings --isolated`", "value_type": "list[RuleSelector]", "scope": null, - "example": "# On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).\nselect = [\"E4\", \"E7\", \"E9\", \"F\", \"B\", \"Q\"]", + "example": "# On top of the defaults, enable flake8-bugbear (`B`) and flake8-quotes (`Q`).\nextend-select = [\"B\", \"Q\"]", "deprecated": null } diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs index 33a3317b61..90e0337904 100644 --- a/crates/ruff/tests/integration_test.rs +++ b/crates/ruff/tests/integration_test.rs @@ -999,7 +999,10 @@ preview = true #[test] fn full_output_format() { - let mut cmd = RuffCheck::default().output_format("full").build(); + let mut cmd = RuffCheck::default() + .output_format("full") + .args(["--select=E741"]) + .build(); assert_cmd_snapshot!(cmd .pass_stdin("l = 1"), @" success: false @@ -2607,7 +2610,7 @@ fn pyproject_toml_stdin_schema_error() { #[test] fn pyproject_toml_stdin_no_applicable_rules_selected() { let mut cmd = RuffCheck::default() - .args(["--stdin-filename", "pyproject.toml"]) + .args(["--stdin-filename", "pyproject.toml", "--ignore=RUF200"]) .build(); assert_cmd_snapshot!( @@ -2649,7 +2652,7 @@ fn pyproject_toml_stdin_no_errors() { .build(); assert_cmd_snapshot!( - cmd.pass_stdin(r#"[project]\nname = "ruff"\nversion = "0.0.0""#), + cmd.pass_stdin("[project]\nname = 'ruff'\nversion = '0.0.0'"), @" success: true exit_code: 0 diff --git a/crates/ruff_dev/src/generate_default_rules.rs b/crates/ruff_dev/src/generate_default_rules.rs new file mode 100644 index 0000000000..4b2c4f0ce0 --- /dev/null +++ b/crates/ruff_dev/src/generate_default_rules.rs @@ -0,0 +1,66 @@ +//! Generate a Markdown-compatible listing of Ruff's default lint rules. + +use std::fmt::Write; + +use itertools::Itertools; +use strum::IntoEnumIterator; + +use ruff_linter::registry::{Linter, RuleNamespace}; +use ruff_linter::settings::LinterSettings; + +pub(crate) fn generate() -> String { + let default_rules = LinterSettings::default().rules; + let linters = Linter::iter() + .filter_map(|linter| { + let rules = linter + .all_rules() + .filter(|rule| default_rules.enabled(*rule)) + .collect_vec(); + (!rules.is_empty()).then_some((linter, rules)) + }) + .collect_vec(); + + let mut output = String::new(); + output.push_str("# Default Rules\n\n"); + output.push_str("Ruff enables the following rules by default:\n\n"); + + output.push_str("??? note \"Default `select` configuration\"\n\n"); + for (filename, section) in [ + ("pyproject.toml", "[tool.ruff.lint]"), + ("ruff.toml", "[lint]"), + ] { + let _ = writeln!(output, " === \"{filename}\"\n"); + output.push_str(" ```toml\n"); + let _ = writeln!(output, " {section}"); + output.push_str(" select = [\n"); + for (_, rules) in &linters { + for rule in rules { + let _ = writeln!(output, " \"{}\",", rule.noqa_code()); + } + } + output.push_str(" ]\n"); + output.push_str(" ```\n\n"); + } + + for (linter, rules) in &linters { + let codes = match linter.common_prefix() { + "" => linter + .upstream_categories() + .unwrap() + .iter() + .map(|category| category.prefix) + .join(", "), + prefix => prefix.to_string(), + }; + let _ = writeln!(output, "## {} ({codes})\n", linter.name()); + + for rule in rules { + let name = rule.name(); + let code = rule.noqa_code(); + let _ = writeln!(output, "- [`{name}`](rules/{name}.md) (`{code}`)"); + } + output.push('\n'); + } + + output +} diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index 5baa0a2a4d..40cc951798 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -131,7 +131,11 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S output.push_str(field.doc); output.push_str("\n\n"); - let _ = writeln!(output, "**Default value**: `{}`", field.default); + if parents_anchor == "lint" && name == "select" { + output.push_str("**Default value**: See [Default Rules](default-rules.md).\n"); + } else { + let _ = writeln!(output, "**Default value**: `{}`", field.default); + } output.push('\n'); let _ = writeln!(output, "**Type**: `{}`", field.value_type); output.push('\n'); diff --git a/crates/ruff_dev/src/main.rs b/crates/ruff_dev/src/main.rs index 93aac0ec93..c5af3add87 100644 --- a/crates/ruff_dev/src/main.rs +++ b/crates/ruff_dev/src/main.rs @@ -13,6 +13,7 @@ use std::process::ExitCode; mod format_dev; mod generate_all; mod generate_cli_help; +mod generate_default_rules; mod generate_docs; mod generate_json_schema; mod generate_options; @@ -50,6 +51,8 @@ enum Command { GenerateTySchema(generate_ty_schema::Args), /// Generate a Markdown-compatible table of supported lint rules. GenerateRulesTable, + /// Generate a Markdown-compatible listing of default lint rules. + GenerateDefaultRules, GenerateTyRules(generate_ty_rules::Args), /// Generate a Markdown-compatible listing of configuration options. GenerateOptions, @@ -98,6 +101,7 @@ fn main() -> Result { Command::GenerateJSONSchema(args) => generate_json_schema::main(&args)?, Command::GenerateTySchema(args) => generate_ty_schema::main(&args)?, Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()), + Command::GenerateDefaultRules => println!("{}", generate_default_rules::generate()), Command::GenerateTyRules(args) => generate_ty_rules::main(&args)?, Command::GenerateOptions => println!("{}", generate_options::generate()), Command::GenerateTyOptions(args) => generate_ty_options::main(&args)?, diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index 0f0c9aad3d..1d4b3e624b 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -9,12 +9,11 @@ use std::path::{Path, PathBuf}; use std::sync::LazyLock; use types::CompiledPerFileTargetVersionList; -use crate::codes::RuleCodePrefix; use ruff_macros::CacheKey; use ruff_python_ast::PythonVersion; use crate::line_width::LineLength; -use crate::registry::{Linter, Rule}; +use crate::registry::Rule; use crate::rules::{ flake8_annotations, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_copyright, flake8_errmsg, flake8_gettext, @@ -23,7 +22,7 @@ use crate::rules::{ pep8_naming, pycodestyle, pydoclint, pydocstyle, pyflakes, pylint, pyupgrade, ruff, }; use crate::settings::types::{CompiledPerFileIgnoreList, ExtensionMapping, FilePatternSet}; -use crate::{RuleSelector, codes, fs}; +use crate::{RuleSelector, fs}; use super::line_width::IndentWidth; @@ -354,25 +353,8 @@ impl Display for LinterSettings { } } -pub const DEFAULT_SELECTORS: &[RuleSelector] = &[ - RuleSelector::Linter(Linter::Pyflakes), - // Only include pycodestyle rules that do not overlap with the formatter - RuleSelector::Prefix { - prefix: RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E4), - redirected_from: None, - }, - RuleSelector::Prefix { - prefix: RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E7), - redirected_from: None, - }, - RuleSelector::Prefix { - prefix: RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E9), - redirected_from: None, - }, -]; - #[rustfmt::skip] -pub const PREVIEW_DEFAULT_SELECTORS: &[RuleSelector] = &[ +pub const DEFAULT_SELECTORS: &[RuleSelector] = &[ RuleSelector::rule(Rule::CancelScopeNoCheckpoint), // ASYNC100 RuleSelector::rule(Rule::TrioSyncCall), // ASYNC105 RuleSelector::rule(Rule::AsyncZeroSleep), // ASYNC115 diff --git a/crates/ruff_server/src/session/index/ruff_settings.rs b/crates/ruff_server/src/session/index/ruff_settings.rs index 64e7fbfe91..adec385343 100644 --- a/crates/ruff_server/src/session/index/ruff_settings.rs +++ b/crates/ruff_server/src/session/index/ruff_settings.rs @@ -617,13 +617,13 @@ mod tests { let configuration = toml::from_str( r#" [lint.isort] - required-imports = ["from collections.abc import Set"] + required-imports = ["import numpy"] "#, )?; let editor_settings = EditorSettings { configuration: Some(ResolvedConfiguration::Inline(Box::new(configuration))), select: Some(vec![UnresolvedRuleSelector::new( - "PYI025", + "ICN001", ValueSource::Editor, )]), ..Default::default() @@ -636,7 +636,7 @@ mod tests { !settings .linter .rules - .enabled(Rule::UnaliasedCollectionsAbcSetImport) + .enabled(Rule::UnconventionalImportAlias) ); Ok(()) } diff --git a/crates/ruff_wasm/README.md b/crates/ruff_wasm/README.md index b58015c3f6..1c2e7532fa 100644 --- a/crates/ruff_wasm/README.md +++ b/crates/ruff_wasm/README.md @@ -25,7 +25,7 @@ const exampleDocument = `print('hello'); print("world")`; await init(); // Initializes WASM module -// These are default settings just to illustrate configuring Ruff +// These settings illustrate configuring Ruff // Settings info: https://docs.astral.sh/ruff/settings const workspace = new Workspace( { diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 9a558ddf49..8c8f5491b4 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -34,8 +34,7 @@ use ruff_linter::settings::types::{ RequiredVersion, UnsafeFixes, }; use ruff_linter::settings::{ - DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, LinterSettings, PREVIEW_DEFAULT_SELECTORS, TASK_TAGS, - TargetVersion, + DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, LinterSettings, TASK_TAGS, TargetVersion, }; use ruff_linter::{ RuleSelector, UnresolvedRuleSelector, fs, warn_user_once, warn_user_once_by_id, @@ -897,14 +896,8 @@ impl LintConfiguration { require_explicit: self.explicit_preview_rules.unwrap_or_default(), }; - let selectors = if preview.mode.is_enabled() { - PREVIEW_DEFAULT_SELECTORS - } else { - DEFAULT_SELECTORS - }; - // The select_set keeps track of which rules have been selected. - let mut select_set: RuleSet = selectors + let mut select_set: RuleSet = DEFAULT_SELECTORS .iter() .flat_map(|selector| selector.rules(&preview)) .collect(); diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index cd19803f52..14120f9099 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -724,7 +724,7 @@ pub struct LintCommonOptions { /// /// ```toml /// [tool.ruff.lint] - /// # Adds flake8-bugbear on top of the default rules (E4, E7, E9, F). + /// # Adds flake8-bugbear on top of the default rules. /// extend-select = ["B"] /// ``` /// @@ -734,7 +734,7 @@ pub struct LintCommonOptions { default = "[]", value_type = "list[RuleSelector]", example = r#" - # On top of the default `select` (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`). + # On top of the default `select`, enable flake8-bugbear (`B`) and flake8-quotes (`Q`). extend-select = ["B", "Q"] "# )] @@ -882,11 +882,11 @@ pub struct LintCommonOptions { /// specific prefixes. `ignore` takes precedence over `select` if the /// same prefix appears in both. #[option( - default = r#"["E4", "E7", "E9", "F"]"#, + default = r#"See https://docs.astral.sh/ruff/default-rules/ or run `ruff check --show-settings --isolated`"#, value_type = "list[RuleSelector]", example = r#" - # On top of the defaults (`E4`, E7`, `E9`, and `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`). - select = ["E4", "E7", "E9", "F", "B", "Q"] + # On top of the defaults, enable flake8-bugbear (`B`) and flake8-quotes (`Q`). + extend-select = ["B", "Q"] "# )] pub select: Option>, diff --git a/docs/.gitignore b/docs/.gitignore index ec6ed14e2b..c782bce528 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,4 +1,5 @@ /contributing.md +/default-rules.md /index.md /rules.md /rules/ diff --git a/docs/configuration.md b/docs/configuration.md index 2af6928d1c..ea2aa0bd99 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -7,6 +7,8 @@ semantics are the same. For a complete enumeration of the available configuration options, see [_Settings_](settings.md). +For the complete list of enabled rules, see [_Default Rules_](default-rules.md). + If left unspecified, Ruff's default configuration is equivalent to: === "pyproject.toml" @@ -51,10 +53,7 @@ If left unspecified, Ruff's default configuration is equivalent to: target-version = "py310" [tool.ruff.lint] - # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. - # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or - # McCabe complexity (`C901`) by default. - select = ["E4", "E7", "E9", "F"] + # select = [...] # See the Default Rules page for the full listing. ignore = [] # Allow fix for all enabled rules (when `--fix`) is provided. @@ -133,10 +132,7 @@ If left unspecified, Ruff's default configuration is equivalent to: target-version = "py310" [lint] - # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. - # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or - # McCabe complexity (`C901`) by default. - select = ["E4", "E7", "E9", "F"] + # select = [...] # See the Default Rules page for the full listing. ignore = [] # Allow fix for all enabled rules (when `--fix`) is provided. @@ -180,8 +176,8 @@ As an example, the following would configure Ruff to: ```toml [tool.ruff.lint] - # 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults. - select = ["E4", "E7", "E9", "F", "B"] + # 1. Enable all flake8-bugbear (`B`) rules, in addition to the defaults. + extend-select = ["B"] # 2. Avoid enforcing line-length violations (`E501`) ignore = ["E501"] @@ -203,8 +199,8 @@ As an example, the following would configure Ruff to: ```toml [lint] - # 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults. - select = ["E4", "E7", "E9", "F", "B"] + # 1. Enable all flake8-bugbear (`B`) rules, in addition to the defaults. + extend-select = ["B"] # 2. Avoid enforcing line-length violations (`E501`) ignore = ["E501"] @@ -229,7 +225,7 @@ Linter plugin configurations are expressed as subsections, e.g.: ```toml [tool.ruff.lint] # Add "Q" to the list of enabled codes. - select = ["E4", "E7", "E9", "F", "Q"] + extend-select = ["Q"] [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" @@ -240,7 +236,7 @@ Linter plugin configurations are expressed as subsections, e.g.: ```toml [lint] # Add "Q" to the list of enabled codes. - select = ["E4", "E7", "E9", "F", "Q"] + extend-select = ["Q"] [lint.flake8-quotes] docstring-quotes = "double" diff --git a/docs/tutorial.md b/docs/tutorial.md index d57ee6ca5d..ac956fba14 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -201,13 +201,13 @@ Ruff supports [over 900 lint rules](rules.md) split across over 50 built-in plug determining the right set of rules will depend on your project's needs: some rules may be too strict, some are framework-specific, and so on. -By default, Ruff enables Flake8's `F` rules, along with a subset of the `E` rules, omitting any -stylistic rules that overlap with the use of a formatter, like `ruff format` or +By default, Ruff enables rules from the `F`, `E`, `B`, `UP`, and `RUF` categories, as well as many +more, omitting any stylistic rules that overlap with the use of a formatter, like `ruff format` or [Black](https://github.com/psf/black). If you're introducing a linter for the first time, **the default rule set is a great place to -start**: it's narrow and focused while catching a wide variety of common errors (like unused -imports) with zero configuration. +start**: it catches a wide variety of common errors (like unused imports) with zero configuration. +See [_Default Rules_](default-rules.md) for the complete list. If you're migrating to Ruff from another linter, you can enable rules that are equivalent to those enforced in your previous configuration. For example, if we want to enforce the pyupgrade diff --git a/ruff.schema.json b/ruff.schema.json index 7564b9c57f..4555a248f0 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -145,7 +145,7 @@ } }, "extend-select": { - "description": "A list of rule codes or prefixes to enable, in addition to those\nspecified by [`select`](#lint_select).\n\nUnlike [`select`](#lint_select), which _replaces_ the default rule set\nwhen specified, `extend-select` _adds_ to whatever rules are already\nactive. This makes `extend-select` the preferred option when you want\nto enable additional rules on top of the defaults without having to\nenumerate them.\n\nFor example, to enable the defaults plus flake8-bugbear:\n\n```toml\n[tool.ruff.lint]\n# Adds flake8-bugbear on top of the default rules (E4, E7, E9, F).\nextend-select = [\"B\"]\n```\n\nUsing `select = [\"B\"]` instead would _replace_ the defaults, enabling\nonly flake8-bugbear.", + "description": "A list of rule codes or prefixes to enable, in addition to those\nspecified by [`select`](#lint_select).\n\nUnlike [`select`](#lint_select), which _replaces_ the default rule set\nwhen specified, `extend-select` _adds_ to whatever rules are already\nactive. This makes `extend-select` the preferred option when you want\nto enable additional rules on top of the defaults without having to\nenumerate them.\n\nFor example, to enable the defaults plus flake8-bugbear:\n\n```toml\n[tool.ruff.lint]\n# Adds flake8-bugbear on top of the default rules.\nextend-select = [\"B\"]\n```\n\nUsing `select = [\"B\"]` instead would _replace_ the defaults, enabling\nonly flake8-bugbear.", "type": [ "array", "null" @@ -2199,7 +2199,7 @@ } }, "extend-select": { - "description": "A list of rule codes or prefixes to enable, in addition to those\nspecified by [`select`](#lint_select).\n\nUnlike [`select`](#lint_select), which _replaces_ the default rule set\nwhen specified, `extend-select` _adds_ to whatever rules are already\nactive. This makes `extend-select` the preferred option when you want\nto enable additional rules on top of the defaults without having to\nenumerate them.\n\nFor example, to enable the defaults plus flake8-bugbear:\n\n```toml\n[tool.ruff.lint]\n# Adds flake8-bugbear on top of the default rules (E4, E7, E9, F).\nextend-select = [\"B\"]\n```\n\nUsing `select = [\"B\"]` instead would _replace_ the defaults, enabling\nonly flake8-bugbear.", + "description": "A list of rule codes or prefixes to enable, in addition to those\nspecified by [`select`](#lint_select).\n\nUnlike [`select`](#lint_select), which _replaces_ the default rule set\nwhen specified, `extend-select` _adds_ to whatever rules are already\nactive. This makes `extend-select` the preferred option when you want\nto enable additional rules on top of the defaults without having to\nenumerate them.\n\nFor example, to enable the defaults plus flake8-bugbear:\n\n```toml\n[tool.ruff.lint]\n# Adds flake8-bugbear on top of the default rules.\nextend-select = [\"B\"]\n```\n\nUsing `select = [\"B\"]` instead would _replace_ the defaults, enabling\nonly flake8-bugbear.", "type": [ "array", "null" diff --git a/scripts/generate_mkdocs.py b/scripts/generate_mkdocs.py index 564d7c1a34..943ddc517e 100644 --- a/scripts/generate_mkdocs.py +++ b/scripts/generate_mkdocs.py @@ -47,6 +47,7 @@ class Section(NamedTuple): Section("Configuring Ruff", "configuration.md", generated=False), Section("Preview", "preview.md", generated=False), Section("Rules", "rules.md", generated=True), + Section("Default Rules", "default-rules.md", generated=True), Section("Settings", "settings.md", generated=True), Section("Versioning", "versioning.md", generated=False), Section("Integrations", "integrations.md", generated=False), @@ -74,6 +75,7 @@ class Section(NamedTuple): ), "https://docs.astral.sh/ruff/installation/": "installation.md", "https://docs.astral.sh/ruff/rules/": "rules.md", + "https://docs.astral.sh/ruff/default-rules/": "default-rules.md", "https://docs.astral.sh/ruff/settings/": "settings.md", "#whos-using-ruff": "https://github.com/astral-sh/ruff#whos-using-ruff", "https://docs.astral.sh/ruff/preview/": "preview.md", @@ -210,6 +212,11 @@ def main() -> None: ["cargo", "dev", "generate-options"], encoding="utf-8", ) + elif filename == "default-rules.md": + file_content = subprocess.check_output( + ["cargo", "dev", "generate-default-rules"], + encoding="utf-8", + ) else: block = content.split(f"\n\n") if len(block) != 2: From ef912bbbe466856aa4aac10ad2a8856eb3d5aef3 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Wed, 22 Jul 2026 11:44:29 -0400 Subject: [PATCH 034/390] Add newly stabilized rules to defaults (#27055) Summary -- I did a quick pass over the newly stabilized rules to see if any of them should be on by default, with these results: | Rule | Category | |--|--| | [PLR1708](https://docs.astral.sh/ruff/rules/PLR1708) | correctness | | [RUF068](https://docs.astral.sh/ruff/rules/RUF068) | correctness | | [ISC004](https://docs.astral.sh/ruff/rules/ISC004) | suspicious | | [PLE0304](https://docs.astral.sh/ruff/rules/PLE0304) | suspicious | | [RUF063](https://docs.astral.sh/ruff/rules/RUF063) | suspicious | | [FURB192](https://docs.astral.sh/ruff/rules/FURB192) | complexity | | [AIR303](https://docs.astral.sh/ruff/rules/AIR303) | pedantic | | [CPY001](https://docs.astral.sh/ruff/rules/CPY001) | pedantic | | [FURB164](https://docs.astral.sh/ruff/rules/FURB164) | pedantic | | [LOG004](https://docs.astral.sh/ruff/rules/LOG004) | pedantic | | [PLR0917](https://docs.astral.sh/ruff/rules/PLR0917) | pedantic | | [RUF036](https://docs.astral.sh/ruff/rules/RUF036) | pedantic | | [RUF045](https://docs.astral.sh/ruff/rules/RUF045) | pedantic | Obviously the categories themselves are still unused for now, but this suggested adding the first six to the default set. I initially included RUF036 in Style and RUF045 as Suspicious but Codex talked me down. LOG004 would also likely be higher if it were a bit more reliable, but we can't easily detect some cases that are relatively common in the ecosystem, such as defining some kind of custom logging function that itself is intended to be called from an exception handler. Test Plan -- Updated snapshots --- crates/ruff/tests/cli/lint.rs | 6 ++++++ ...cli__show_settings__display_default_settings.snap | 12 ++++++++++++ crates/ruff_linter/src/settings/mod.rs | 6 ++++++ 3 files changed, 24 insertions(+) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index fac30767d2..b40d7b0b9b 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -4872,6 +4872,7 @@ fn default_rules() -> Result<()> { f-string-in-get-text-func-call (INT001), format-in-get-text-func-call (INT002), printf-in-get-text-func-call (INT003), + implicit-string-concatenation-in-collection-literal (ISC004), direct-logger-instantiation (LOG001), invalid-get-logger-argument (LOG002), undocumented-warn (LOG009), @@ -5035,6 +5036,7 @@ fn default_rules() -> Result<()> { nonlocal-without-binding (PLE0117), load-before-global-declaration (PLE0118), invalid-length-return-type (PLE0303), + invalid-bool-return-type (PLE0304), invalid-index-return-type (PLE0305), invalid-str-return-type (PLE0307), invalid-bytes-return-type (PLE0308), @@ -5065,6 +5067,7 @@ fn default_rules() -> Result<()> { property-with-parameters (PLR0206), manual-from-import (PLR0402), redefined-argument-from-local (PLR1704), + stop-iteration-return (PLR1708), useless-return (PLR1711), boolean-chained-comparison (PLR1716), sys-exit-alias (PLR1722), @@ -5150,6 +5153,7 @@ fn default_rules() -> Result<()> { implicit-cwd (FURB177), hashlib-digest-hex (FURB181), slice-to-remove-prefix-or-suffix (FURB188), + sorted-min-max (FURB192), zip-instead-of-pairwise (RUF007), mutable-dataclass-default (RUF008), function-call-in-dataclass-default-argument (RUF009), @@ -5181,6 +5185,8 @@ fn default_rules() -> Result<()> { unnecessary-round (RUF057), starmap-zip (RUF058), unused-unpacked-variable (RUF059), + access-annotations-from-class-dict (RUF063), + duplicate-entry-in-dunder-all (RUF068), unused-noqa (RUF100), redirected-noqa (RUF101), invalid-pyproject-toml (RUF200), diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap index 176e9fc71d..43a4d2768e 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap @@ -158,6 +158,7 @@ linter.rules.enabled = [ f-string-in-get-text-func-call (INT001), format-in-get-text-func-call (INT002), printf-in-get-text-func-call (INT003), + implicit-string-concatenation-in-collection-literal (ISC004), direct-logger-instantiation (LOG001), invalid-get-logger-argument (LOG002), undocumented-warn (LOG009), @@ -321,6 +322,7 @@ linter.rules.enabled = [ nonlocal-without-binding (PLE0117), load-before-global-declaration (PLE0118), invalid-length-return-type (PLE0303), + invalid-bool-return-type (PLE0304), invalid-index-return-type (PLE0305), invalid-str-return-type (PLE0307), invalid-bytes-return-type (PLE0308), @@ -351,6 +353,7 @@ linter.rules.enabled = [ property-with-parameters (PLR0206), manual-from-import (PLR0402), redefined-argument-from-local (PLR1704), + stop-iteration-return (PLR1708), useless-return (PLR1711), boolean-chained-comparison (PLR1716), sys-exit-alias (PLR1722), @@ -436,6 +439,7 @@ linter.rules.enabled = [ implicit-cwd (FURB177), hashlib-digest-hex (FURB181), slice-to-remove-prefix-or-suffix (FURB188), + sorted-min-max (FURB192), zip-instead-of-pairwise (RUF007), mutable-dataclass-default (RUF008), function-call-in-dataclass-default-argument (RUF009), @@ -467,6 +471,8 @@ linter.rules.enabled = [ unnecessary-round (RUF057), starmap-zip (RUF058), unused-unpacked-variable (RUF059), + access-annotations-from-class-dict (RUF063), + duplicate-entry-in-dunder-all (RUF068), unused-noqa (RUF100), redirected-noqa (RUF101), invalid-pyproject-toml (RUF200), @@ -567,6 +573,7 @@ linter.rules.should_fix = [ f-string-in-get-text-func-call (INT001), format-in-get-text-func-call (INT002), printf-in-get-text-func-call (INT003), + implicit-string-concatenation-in-collection-literal (ISC004), direct-logger-instantiation (LOG001), invalid-get-logger-argument (LOG002), undocumented-warn (LOG009), @@ -730,6 +737,7 @@ linter.rules.should_fix = [ nonlocal-without-binding (PLE0117), load-before-global-declaration (PLE0118), invalid-length-return-type (PLE0303), + invalid-bool-return-type (PLE0304), invalid-index-return-type (PLE0305), invalid-str-return-type (PLE0307), invalid-bytes-return-type (PLE0308), @@ -760,6 +768,7 @@ linter.rules.should_fix = [ property-with-parameters (PLR0206), manual-from-import (PLR0402), redefined-argument-from-local (PLR1704), + stop-iteration-return (PLR1708), useless-return (PLR1711), boolean-chained-comparison (PLR1716), sys-exit-alias (PLR1722), @@ -845,6 +854,7 @@ linter.rules.should_fix = [ implicit-cwd (FURB177), hashlib-digest-hex (FURB181), slice-to-remove-prefix-or-suffix (FURB188), + sorted-min-max (FURB192), zip-instead-of-pairwise (RUF007), mutable-dataclass-default (RUF008), function-call-in-dataclass-default-argument (RUF009), @@ -876,6 +886,8 @@ linter.rules.should_fix = [ unnecessary-round (RUF057), starmap-zip (RUF058), unused-unpacked-variable (RUF059), + access-annotations-from-class-dict (RUF063), + duplicate-entry-in-dunder-all (RUF068), unused-noqa (RUF100), redirected-noqa (RUF101), invalid-pyproject-toml (RUF200), diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index 1d4b3e624b..75ead91fd2 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -487,6 +487,7 @@ pub const DEFAULT_SELECTORS: &[RuleSelector] = &[ RuleSelector::rule(Rule::ImplicitCwd), // FURB177 RuleSelector::rule(Rule::HashlibDigestHex), // FURB181 RuleSelector::rule(Rule::SliceToRemovePrefixOrSuffix), // FURB188 + RuleSelector::rule(Rule::SortedMinMax), // FURB192 RuleSelector::rule(Rule::LoggingWarn), // G010 RuleSelector::rule(Rule::LoggingExtraAttrClash), // G101 RuleSelector::rule(Rule::LoggingExcInfo), // G201 @@ -495,6 +496,7 @@ pub const DEFAULT_SELECTORS: &[RuleSelector] = &[ RuleSelector::rule(Rule::FStringInGetTextFuncCall), // INT001 RuleSelector::rule(Rule::FormatInGetTextFuncCall), // INT002 RuleSelector::rule(Rule::PrintfInGetTextFuncCall), // INT003 + RuleSelector::rule(Rule::ImplicitStringConcatenationInCollectionLiteral), // ISC004 RuleSelector::rule(Rule::DirectLoggerInstantiation), // LOG001 RuleSelector::rule(Rule::InvalidGetLoggerArgument), // LOG002 RuleSelector::rule(Rule::UndocumentedWarn), // LOG009 @@ -528,6 +530,7 @@ pub const DEFAULT_SELECTORS: &[RuleSelector] = &[ RuleSelector::rule(Rule::NonlocalWithoutBinding), // PLE0117 RuleSelector::rule(Rule::LoadBeforeGlobalDeclaration), // PLE0118 RuleSelector::rule(Rule::InvalidLengthReturnType), // PLE0303 + RuleSelector::rule(Rule::InvalidBoolReturnType), // PLE0304 RuleSelector::rule(Rule::InvalidIndexReturnType), // PLE0305 RuleSelector::rule(Rule::InvalidStrReturnType), // PLE0307 RuleSelector::rule(Rule::InvalidBytesReturnType), // PLE0308 @@ -558,6 +561,7 @@ pub const DEFAULT_SELECTORS: &[RuleSelector] = &[ RuleSelector::rule(Rule::PropertyWithParameters), // PLR0206 RuleSelector::rule(Rule::ManualFromImport), // PLR0402 RuleSelector::rule(Rule::RedefinedArgumentFromLocal), // PLR1704 + RuleSelector::rule(Rule::StopIterationReturn), // PLR1708 RuleSelector::rule(Rule::UselessReturn), // PLR1711 RuleSelector::rule(Rule::BooleanChainedComparison), // PLR1716 RuleSelector::rule(Rule::SysExitAlias), // PLR1722 @@ -672,6 +676,8 @@ pub const DEFAULT_SELECTORS: &[RuleSelector] = &[ RuleSelector::rule(Rule::UnnecessaryRound), // RUF057 RuleSelector::rule(Rule::StarmapZip), // RUF058 RuleSelector::rule(Rule::UnusedUnpackedVariable), // RUF059 + RuleSelector::rule(Rule::AccessAnnotationsFromClassDict), // RUF063 + RuleSelector::rule(Rule::DuplicateEntryInDunderAll), // RUF068 RuleSelector::rule(Rule::UnusedNOQA), // RUF100 RuleSelector::rule(Rule::RedirectedNOQA), // RUF101 RuleSelector::rule(Rule::InvalidPyprojectToml), // RUF200 From 17ef71142c52230b923dad46ee5554140fc3fd2e Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:45:07 -0400 Subject: [PATCH 035/390] Stabilize `--add-ignore` (#27125) Summary -- This makes sense to accompany `ruff: ignore` comments being stabilized. The preview gate has been pushed down to cover only the names vs codes decision. I'll flag this as a comment, but I wasn't totally sure about the LSP case. I left using `ruff: ignore` in its suppression edits preview-gated for now. Test Plan -- Existing tests, dropping one that showed the preview error with `--add-ignore`. I left `--preview` in many of the tests to continue testing the human-readable names but removed it from one. --- crates/ruff/src/args.rs | 4 ++-- crates/ruff/src/commands/add_noqa.rs | 11 +--------- crates/ruff/tests/cli/lint.rs | 30 +--------------------------- crates/ruff_linter/src/linter.rs | 1 + crates/ruff_linter/src/noqa.rs | 21 ++++++++++++++++++- crates/ruff_server/src/lint.rs | 1 + docs/configuration.md | 4 ++-- 7 files changed, 28 insertions(+), 44 deletions(-) diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index 7258722d82..de02e3dc8d 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -468,8 +468,8 @@ pub struct CheckCommand { )] pub add_noqa: Option, /// Enable automatic additions of `ruff: ignore` comments to failing lines. - /// Optionally provide a reason to append after the rule names. - /// Requires preview mode. + /// Optionally provide a reason to append after the codes. + /// In preview, add suppression comments with rule names instead. #[arg( long, value_name = "REASON", diff --git a/crates/ruff/src/commands/add_noqa.rs b/crates/ruff/src/commands/add_noqa.rs index 159a7fa0bb..1fef003c17 100644 --- a/crates/ruff/src/commands/add_noqa.rs +++ b/crates/ruff/src/commands/add_noqa.rs @@ -1,14 +1,13 @@ use std::path::PathBuf; use std::time::Instant; -use anyhow::{Result, bail}; +use anyhow::Result; use log::{debug, error}; #[cfg(not(target_family = "wasm"))] use rayon::prelude::*; use ruff_linter::SuppressionKind; use ruff_linter::linter::add_suppressions_to_path; -use ruff_linter::preview::is_human_readable_names_enabled; use ruff_linter::source_kind::SourceKind; use ruff_linter::warn_user_once; use ruff_python_ast::{PySourceType, SourceType}; @@ -80,14 +79,6 @@ pub(crate) fn add_noqa( { return Ok(0); } - if matches!(suppression_kind, SuppressionKind::Ignore) - && !is_human_readable_names_enabled(settings.linter.preview) - { - bail!( - "`--add-ignore` requires preview mode, but preview is disabled for `{}`", - path.display() - ); - } let source_kind = match SourceKind::from_path(path, source_type) { Ok(Some(source_kind)) => source_kind, Ok(None) => return Ok(0), diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index b40d7b0b9b..3f57bc14a7 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -2614,7 +2614,6 @@ fn add_ignore() -> Result<()> { fixture .check_command() .arg("--select=RUF015") - .arg("--preview") .arg("--add-ignore"), @" success: true @@ -2633,40 +2632,13 @@ fn add_ignore() -> Result<()> { @" def first_square(): - return [x * x for x in range(20)][0] # ruff: ignore[unnecessary-iterable-allocation-for-first-element] + return [x * x for x in range(20)][0] # ruff: ignore[RUF015] ", ); Ok(()) } -#[test] -fn add_ignore_requires_preview() -> Result<()> { - let fixture = CliTest::new()?; - fixture.write_file("noqa.py", "import os\n")?; - - assert_cmd_snapshot!( - fixture - .check_command() - .arg("--select=F401") - .arg("--add-ignore"), - @" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - ruff failed - Cause: `--add-ignore` requires preview mode, but preview is disabled for `[TMP]/noqa.py` - ", - ); - - let test_code = fixture.read_file("noqa.py")?; - insta::assert_snapshot!(test_code, @"import os"); - - Ok(()) -} - #[test] fn add_noqa_existing_ignore() -> Result<()> { let fixture = CliTest::new()?; diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index e83a57416d..3fd556aa02 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -436,6 +436,7 @@ pub fn add_suppressions_to_path( reason, &suppressions, suppression_kind, + settings.preview, ) } diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index 909e78e10a..22113ce06a 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -19,8 +19,10 @@ use rustc_hash::FxHashSet; use crate::Edit; use crate::Locator; use crate::fs::relativize_path; +use crate::preview::is_human_readable_names_enabled; use crate::registry::Rule; use crate::rule_redirects::get_redirect_target; +use crate::settings::types::PreviewMode; use crate::suppression::{self, Suppressions}; /// Generates an array of edits that matches the length of `diagnostics`. @@ -39,6 +41,7 @@ pub fn generate_suppression_edits( line_ending: LineEnding, suppressions: &Suppressions, suppression_kind: SuppressionKind, + preview: PreviewMode, ) -> Vec> { let file_directives = FileNoqaDirectives::extract(locator, comment_ranges, external, path); let exemption = FileExemption::from(&file_directives); @@ -51,6 +54,7 @@ pub fn generate_suppression_edits( noqa_line_for, suppressions, suppression_kind, + preview, ); build_suppression_edits_by_diagnostic(comments, locator, line_ending, None, suppression_kind) } @@ -780,6 +784,7 @@ pub(crate) fn add_suppression( reason: Option<&str>, suppressions: &Suppressions, suppression_kind: SuppressionKind, + preview: PreviewMode, ) -> Result { let (count, output) = add_suppression_inner( path, @@ -792,6 +797,7 @@ pub(crate) fn add_suppression( reason, suppressions, suppression_kind, + preview, ); fs::write(path, output)?; @@ -810,6 +816,7 @@ fn add_suppression_inner( reason: Option<&str>, suppressions: &Suppressions, suppression_kind: SuppressionKind, + preview: PreviewMode, ) -> (usize, String) { let mut count = 0; @@ -827,6 +834,7 @@ fn add_suppression_inner( noqa_line_for, suppressions, suppression_kind, + preview, ); let edits = @@ -938,6 +946,7 @@ impl Ranged for ExistingDirective<'_> { } } +#[expect(clippy::too_many_arguments)] fn find_suppression_comments<'a>( diagnostics: &'a [Diagnostic], locator: &'a Locator, @@ -946,6 +955,7 @@ fn find_suppression_comments<'a>( noqa_line_for: &NoqaMapping, suppressions: &'a Suppressions, suppression_kind: SuppressionKind, + preview: PreviewMode, ) -> Vec>> { // List of suppression comments, ordered to match up with `messages` let mut comments_by_line: Vec>> = vec![]; @@ -1023,7 +1033,8 @@ fn find_suppression_comments<'a>( let identifier = match suppression_kind { SuppressionKind::Noqa => code.as_str(), - SuppressionKind::Ignore => message.name(), + SuppressionKind::Ignore if is_human_readable_names_enabled(preview) => message.name(), + SuppressionKind::Ignore => code.as_str(), }; comments_by_line.push(Some(SuppressionComment { @@ -1368,6 +1379,7 @@ mod tests { use crate::rules::pycodestyle::rules::{AmbiguousVariableName, UselessSemicolon}; use crate::rules::pyflakes::rules::UnusedVariable; use crate::rules::pyupgrade::rules::PrintfStringFormatting; + use crate::settings::types::PreviewMode; use crate::settings::{LinterSettings, flags}; use crate::source_kind::SourceKind; use crate::suppression::Suppressions; @@ -1423,6 +1435,7 @@ mod tests { None, &suppressions, suppression_kind, + settings.preview, ) } @@ -3328,6 +3341,7 @@ mod tests { None, &Suppressions::default(), SuppressionKind::Noqa, + PreviewMode::Disabled, ); assert_eq!(count, 0); assert_eq!(output, format!("{contents}")); @@ -3354,6 +3368,7 @@ mod tests { None, &Suppressions::default(), SuppressionKind::Noqa, + PreviewMode::Disabled, ); assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: F841\n"); @@ -3387,6 +3402,7 @@ mod tests { None, &Suppressions::default(), SuppressionKind::Noqa, + PreviewMode::Disabled, ); assert_eq!(count, 1); assert_eq!(output, "x = 1 # noqa: E741, F841\n"); @@ -3420,6 +3436,7 @@ mod tests { None, &Suppressions::default(), SuppressionKind::Noqa, + PreviewMode::Disabled, ); assert_eq!(count, 0); assert_eq!(output, "x = 1 # noqa"); @@ -3453,6 +3470,7 @@ print( LineEnding::Lf, &suppressions, SuppressionKind::Noqa, + PreviewMode::Disabled, ); assert_eq!( edits, @@ -3487,6 +3505,7 @@ bar = LineEnding::Lf, &suppressions, SuppressionKind::Noqa, + PreviewMode::Disabled, ); assert_eq!( edits, diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index 5b2208a17f..2cc836cca8 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -162,6 +162,7 @@ pub(crate) fn check( } else { SuppressionKind::Noqa }, + settings.linter.preview, ); let context = LspDiagnosticContext { source_kind: &source_kind, diff --git a/docs/configuration.md b/docs/configuration.md index ea2aa0bd99..1a7babb3e9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -645,8 +645,8 @@ Options: Optionally provide a reason to append after the codes --add-ignore[=] Enable automatic additions of `ruff: ignore` comments to failing - lines. Optionally provide a reason to append after the rule names. - Requires preview mode + lines. Optionally provide a reason to append after the codes. In + preview, add suppression comments with rule names instead --show-files See the files Ruff will be run against with the current settings --show-settings From 22400709220931375e072ad5d7460b9fc781af78 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:41:30 -0400 Subject: [PATCH 036/390] Reflect `ruff: ignore` and `--add-ignore` stabilization in documentation (#27127) Summary -- Follow up to #27125 and #26934 to update the docs. --- docs/linter.md | 42 +++++++++++++++++++++--------------------- docs/tutorial.md | 4 ++-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/linter.md b/docs/linter.md index e4b1f73045..bdf7783629 100644 --- a/docs/linter.md +++ b/docs/linter.md @@ -292,8 +292,12 @@ see the [`lint.per-file-ignores`](settings.md#lint_per-file-ignores) setting. ### Comments -Ruff supports multiple forms of suppression comments, including inline and file-level `noqa` -comments, and range suppressions. +Ruff supports multiple forms of suppression comments, including inline and file-level `noqa` and +`ruff: ignore` comments, and range suppressions. + +In [`preview`](preview.md) mode, rule names (e.g. `unused-import`) can be used in `ruff: ignore`, +`ruff: file-ignore`, `ruff: disable`, and `ruff: enable` comments instead of rule codes (e.g. +`F401`). #### Line-level @@ -344,20 +348,18 @@ The full inline comment specification is as follows: missing delimiter (e.g. `F401F841`), though a warning will be emitted in this case. -*The following is currently only available in [preview mode](`preview.md`).* - To cover an entire "logical" line (a multi-line statement or suite header), an "ignore" comment may be placed above the first line: ```python -# ruff: ignore[unused-function-argument] # Covers the entire function signature +# ruff: ignore[ARG001] # Covers the entire function signature def foo( arg1, arg2, ): pass -# ruff: ignore[line-too-long] # Covers the entire list literal +# ruff: ignore[E501] # Covers the entire list literal things = [ "really long string literal ...", "really long string literal ...", @@ -371,13 +373,13 @@ of the multi-line statement or header uncovered: ```python def foo( arg1, - # ruff: ignore[unused-function-argument] # Only covers `arg2` + # ruff: ignore[ARG001] # Only covers `arg2` arg2, ): pass things = [ - "really long string literal ...", # ruff: ignore[line-too-long] # Only covers this line + "really long string literal ...", # ruff: ignore[E501] # Only covers this line "really long string literal ...", ] ``` @@ -386,8 +388,8 @@ Ignore comments can also be "stacked" with other comments or pragmas, and will still cover the next logical line: ```python -# ruff: ignore[ambiguous-variable-name] -# ruff: ignore[unused-variable] +# ruff: ignore[E741] +# ruff: ignore[F841] # I definitely know what I'm doing. i = 1 ``` @@ -454,9 +456,6 @@ be used to terminate a preceding "disable" comment with identical codes. Unlike `noqa` suppressions, range suppressions do not support "blanket" suppression of all violations. At least one violation code must be listed. -In [`preview`](preview.md) mode, rule names (e.g. `unused-import`) can be used in these comments -instead of rule codes (e.g. `F401`). - The full range suppression comment specification is as follows: - An own-line comment starting with case sensitive `#ruff:`, with optional whitespace @@ -496,12 +495,11 @@ The file-level suppression comment specification is as follows: optional whitespace and a case-insensitive match for `noqa`. After this, the specification is as in the inline `noqa` suppressions above. -In [`preview`](preview.md) mode, one or more rules can be ignored across an -entire file with a `file-ignore` comment on its own line, at global module scope, -and preferably near the top of the file: +One or more rules can also be ignored across an entire file with a `file-ignore` comment on its own +line, at global module scope, and preferably near the top of the file: ```python -# ruff: file-ignore[unused-import, unused-function-argument] +# ruff: file-ignore[F401, ARG001] ``` The full-level suppression comment specification is as follows: @@ -534,15 +532,17 @@ $ ruff check /path/to/file.py --extend-select RUF100 --fix ### Inserting necessary suppression comments Ruff can _automatically add_ suppression comments to all lines that contain violations, which is -useful when migrating a new codebase to Ruff. To add the appropriate comments to all relevant -lines, run Ruff with `--add-noqa`: +useful when migrating a new codebase to Ruff. To add the appropriate comments to all relevant lines, +run Ruff with `--add-noqa` to add `noqa` comments or with `--add-ignore` to add `ruff: ignore` +comments: ```shell-session $ ruff check /path/to/file.py --add-noqa +$ ruff check /path/to/file.py --add-ignore ``` -The `--add-noqa` flag adds `noqa` directives with rule codes. To add `ruff: ignore` comments with -human-readable rule names instead, use `--add-ignore` with preview mode enabled. +Both of these flags use rule codes on stable. To add `ruff: ignore` comments with human-readable +rule names instead, use `--add-ignore` with preview mode enabled. ### isort action comments diff --git a/docs/tutorial.md b/docs/tutorial.md index ac956fba14..0736dcceb4 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -361,8 +361,8 @@ index 71fca60c8d..e92d839f1b 100644 +from typing import Iterable # noqa: UP035 ``` -To add `# ruff: ignore[...]` comments with human-readable rule names instead, use the -`--add-ignore` flag with preview mode enabled. +To add `# ruff: ignore[...]` comments instead, use the `--add-ignore` flag. In preview mode, +`--add-ignore` uses human-readable rule names in place of rule codes. ## Integrations From 34334491652f8ceca5246d15c5c5afe0d6bc77ae Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 23 Jul 2026 11:01:03 -0700 Subject: [PATCH 037/390] [ty] Reuse full call diagnostics for implicit setter calls (#27115) ## Summary Reuse the full call-error diagnostic machinery for implicit descriptor `__set__` and custom `__setattr__` calls. The call reporter can override the main lint/message, retain the original call failure on the primary annotation, map synthesized arguments back to their source ranges, and add an info diagnostic explaining why the method was called. This PR also correctly re-classifies failed `__setattr__` assignments as `invalid-assignment` instead of `unresolved-attribute`. This PR currently only applies this system to `__set__` and `__setattr__` calls, but it can be extended to any diagnostic resulting from a failed implicit dunder call. Closes astral-sh/ty#4034 ## Test plan Added and updated mdtests covering invalid descriptor values, malformed `__set__` signatures, concise expected/found output, nested tuple incompatibilities, overloaded `__setattr__`, invalid attribute names and values reported together, and instance/metaclass `__setattr__` assignments. --- .../resources/mdtest/attributes.md | 131 +++++++++- .../resources/mdtest/descriptor_protocol.md | 12 +- .../diagnostics/attribute_assignment.md | 102 +++++++- .../resources/mdtest/properties.md | 21 +- crates/ty_python_semantic/src/types/call.rs | 14 +- .../ty_python_semantic/src/types/call/bind.rs | 247 +++++++++++++----- .../ty_python_semantic/src/types/context.rs | 36 ++- .../src/types/diagnostic.rs | 41 ++- .../infer/builder/attribute_assignment.rs | 72 +++-- 9 files changed, 543 insertions(+), 133 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 539995bba0..c0676959a3 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2980,6 +2980,75 @@ instance.callback = lambda number: ( instance.payload = {"value": 1} ``` +### Nested argument type + +```py +class C: + def __setattr__(self, name: str, value: tuple[int, str]): ... + +c = C() +c.x = (1, b"") # snapshot: invalid-assignment +``` + +```snapshot +error[invalid-assignment]: Cannot assign object of type `tuple[Literal[1], Literal[b""]]` to attribute `x` on type `C` + --> src/mdtest_snippet.py:5:7 + | +5 | c.x = (1, b"") # snapshot: invalid-assignment + | ^^^^^^^^ Expected `tuple[int, str]`, found `tuple[Literal[1], Literal[b""]]` + | +info: This assignment implicitly calls a custom `__setattr__` method +info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` +info: Method defined here + --> src/mdtest_snippet.py:2:9 + | +2 | def __setattr__(self, name: str, value: tuple[int, str]): ... + | ^^^^^^^^^^^ ---------------------- Parameter declared here + | +``` + +### Overloaded `__setattr__` + +```py +from typing import overload + +class D: + @overload + def __setattr__(self, name: str, value: tuple[int, str]): ... + @overload + def __setattr__(self, name: str, value: int): ... + def __setattr__(self, name: str, value: tuple[int, str] | int): ... + +d = D() +d.x = (1, b"") # snapshot: invalid-assignment +``` + +```snapshot +error[invalid-assignment]: Cannot assign object of type `tuple[Literal[1], Literal[b""]]` to attribute `x` on type `D` + --> src/mdtest_snippet.py:11:1 + | +11 | d.x = (1, b"") # snapshot: invalid-assignment + | ^^^ No overload of bound method `D.__setattr__` matches arguments + | +info: This assignment implicitly calls a custom `__setattr__` method +info: First overload defined here + --> src/mdtest_snippet.py:4:5 + | +4 | / @overload +5 | | def __setattr__(self, name: str, value: tuple[int, str]): ... + | |_________________________________________________________________^ First overload defined here + | +info: Possible overloads for bound method `__setattr__`: +info: (self, name: str, value: tuple[int, str]) -> Unknown +info: (self, name: str, value: int) -> Unknown +info: Overload implementation defined here + --> src/mdtest_snippet.py:8:9 + | +8 | def __setattr__(self, name: str, value: tuple[int, str] | int): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +``` + ### Type of the `name` parameter If the `name` parameter of the `__setattr__` method is annotated with a (union of) literal type(s), @@ -2998,10 +3067,58 @@ date.day = 8 date.month = 4 date.year = 2025 -# error: [unresolved-attribute] "Cannot assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` with custom `__setattr__` method." +date.month = "May" # snapshot: invalid-assignment +# snapshot: invalid-assignment +# snapshot: invalid-assignment date.tz = "UTC" ``` +```snapshot +error[invalid-assignment]: Cannot assign object of type `Literal["May"]` to attribute `month` on type `Date` + --> src/mdtest_snippet.py:13:14 + | +13 | date.month = "May" # snapshot: invalid-assignment + | ^^^^^ Expected `int`, found `Literal["May"]` + | +info: This assignment implicitly calls a custom `__setattr__` method +info: Method defined here + --> src/mdtest_snippet.py:5:9 + | +5 | def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None: + | ^^^^^^^^^^^ ---------- Parameter declared here + | + + +error[invalid-assignment]: Cannot assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` + --> src/mdtest_snippet.py:16:1 + | +16 | date.tz = "UTC" + | ^^^^^^^ Expected `Literal["day", "month", "year"]`, found `Literal["tz"]` + | +info: This assignment implicitly calls a custom `__setattr__` method +info: Method defined here + --> src/mdtest_snippet.py:5:9 + | +5 | def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None: + | ^^^^^^^^^^^ ------------------------------------- Parameter declared here + | + + +error[invalid-assignment]: Cannot assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` + --> src/mdtest_snippet.py:16:11 + | +16 | date.tz = "UTC" + | ^^^^^ Expected `int`, found `Literal["UTC"]` + | +info: This assignment implicitly calls a custom `__setattr__` method +info: Method defined here + --> src/mdtest_snippet.py:5:9 + | +5 | def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None: + | ^^^^^^^^^^^ ---------- Parameter declared here + | +``` + ### Return type of `__setattr__` If the return type of the `__setattr__` method is `Never`, we do not allow any attribute assignments @@ -3117,7 +3234,7 @@ def use_module(m: MyModule, param: int) -> None: # But assigning to an attribute that's not explicitly defined will still # use `__setattr__` for validation. - # error: [unresolved-attribute] "Cannot assign object of type `int` to attribute `undefined_param` on type `MyModule` with custom `__setattr__` method." + # error: [invalid-assignment] "Cannot assign object of type `int` to attribute `undefined_param` on type `MyModule`" m.undefined_param = param ``` @@ -3158,7 +3275,7 @@ class Meta(type): class Foo(metaclass=Meta): ... Foo.whatever = 42 -Foo.whatever = "invalid" # error: [unresolved-attribute] "with custom `__setattr__` method" +Foo.whatever = "invalid" # error: [invalid-assignment] ``` If both the metaclass and class define `__setattr__`, class-object assignments use the metaclass @@ -3169,11 +3286,11 @@ class WithSetAttr(metaclass=Meta): def __setattr__(self, name: str, value: str) -> None: ... WithSetAttr.class_attribute = 42 -WithSetAttr.class_attribute = "invalid" # error: [unresolved-attribute] "with custom `__setattr__` method" +WithSetAttr.class_attribute = "invalid" # error: [invalid-assignment] instance = WithSetAttr() instance.instance_attribute = "valid" -instance.instance_attribute = 42 # error: [unresolved-attribute] "with custom `__setattr__` method" +instance.instance_attribute = 42 # error: [invalid-assignment] ``` The same applies when the class object is annotated as `type[Foo]`: @@ -3181,7 +3298,7 @@ The same applies when the class object is annotated as `type[Foo]`: ```py def set_on_subclass(cls: type[Foo]) -> None: cls.whatever = 42 - cls.whatever = "invalid" # error: [unresolved-attribute] "with custom `__setattr__` method" + cls.whatever = "invalid" # error: [invalid-assignment] ``` The setter also provides the expected type when inferring the assigned value: @@ -3225,7 +3342,7 @@ OverloadedClass.callback = lambda number: ( number.missing ) OverloadedClass.payload = {"value": 1} -OverloadedClass.callback = {"value": 1} # error: [unresolved-attribute] "with custom `__setattr__` method" +OverloadedClass.callback = {"value": 1} # error: [invalid-assignment] ``` A metaclass `__setattr__` method returning `Never` prevents writes to undefined attributes: diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index b3b92fc7b3..9fd9f0c066 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -35,7 +35,7 @@ reveal_type(C.ten) # revealed: Literal[10] # This is fine: c.ten = 10 -# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `ten` on type `C` with custom `__set__` method" +# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `ten` on type `C`" c.ten = 11 ``` @@ -78,7 +78,7 @@ c.flexible_int = "42" # also okay! reveal_type(c.flexible_int) # revealed: int | None -# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `flexible_int` on type `C` with custom `__set__` method" +# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `flexible_int` on type `C`" c.flexible_int = None # not okay reveal_type(c.flexible_int) # revealed: int | None @@ -215,7 +215,7 @@ def f1(flag: bool): attr = DataDescriptor() def f(self): - # error: [invalid-assignment] "Invalid assignment to data descriptor attribute `attr` on type `Self@f` with custom `__set__` method" + # error: [invalid-assignment] "Invalid assignment to data descriptor attribute `attr` on type `Self@f`" self.attr = b"foo" reveal_type(C1().attr) # revealed: Literal["data"] | bytes @@ -480,7 +480,7 @@ on the metaclass: ```py C1.meta_data_descriptor = 1 -# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor` on type `` with custom `__set__` method" +# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor` on type ``" C1.meta_data_descriptor = "invalid" ``` @@ -586,7 +586,7 @@ def _(flag: bool): # TODO: We currently emit two diagnostics here, corresponding to the two states of `flag`. The diagnostics are not # wrong, but they could be subsumed under a higher-level diagnostic. - # error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor1` on type `` with custom `__set__` method" + # error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor1` on type ``" # error: [invalid-assignment] "Object of type `None` is not assignable to attribute `meta_data_descriptor1` of type `Literal["value on class"]`" C5.meta_data_descriptor1 = None @@ -735,7 +735,7 @@ reveal_type(C.name) # revealed: property c.name = "new" c.name = None -# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `name` on type `C` with custom `__set__` method" +# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `name` on type `C`" c.name = 42 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md index de60429349..a18c94b802 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md @@ -274,17 +274,27 @@ class C: instance = C() instance.attr = 1 # fine -# TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter) instance.attr = "wrong" # snapshot: invalid-assignment + +# Check that the concise diagnostic retains the useful expected and provided types. +# error: [invalid-assignment] "Expected `int`, found `Literal["also wrong"]`" +instance.attr = "also wrong" ``` ```snapshot -error[invalid-assignment]: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method - --> src/mdtest_snippet.py:12:1 +error[invalid-assignment]: Invalid assignment to data descriptor attribute `attr` on type `C` + --> src/mdtest_snippet.py:11:17 | -12 | instance.attr = "wrong" # snapshot: invalid-assignment - | ^^^^^^^^^^^^^ +11 | instance.attr = "wrong" # snapshot: invalid-assignment + | ^^^^^^^ Expected `int`, found `Literal["wrong"]` | +info: This assignment implicitly calls `__set__` on a descriptor of type `Descriptor` +info: Function defined here + --> src/mdtest_snippet.py:2:9 + | +2 | def __set__(self, instance: object, value: int) -> None: + | ^^^^^^^ ---------- Parameter declared here + | ``` ### Invalid `__set__` method signature @@ -299,17 +309,89 @@ class C: instance = C() -# TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`) instance.attr = 1 # snapshot: invalid-assignment ``` ```snapshot -error[invalid-assignment]: Invalid assignment to data descriptor attribute `attr` on type `C` with custom `__set__` method - --> src/mdtest_snippet.py:11:1 +error[invalid-assignment]: Invalid assignment to data descriptor attribute `attr` on type `C` + --> src/mdtest_snippet.py:10:1 + | +10 | instance.attr = 1 # snapshot: invalid-assignment + | ^^^^^^^^^^^^^ No argument provided for required parameter `extra` of function `WrongDescriptor.__set__` + | +info: This assignment implicitly calls `__set__` on a descriptor of type `WrongDescriptor` +info: Parameter declared here + --> src/mdtest_snippet.py:2:53 + | +2 | def __set__(self, instance: object, value: int, extra: int) -> None: + | ^^^^^^^^^^ + | +``` + +### Invalid property setter argument type + +```py +class Document: ... + +class HasDocumentRef: + @property + def document(self) -> Document | None: ... + @document.setter + def document(self, document: Document) -> None: ... + +class Model(HasDocumentRef): + def detach(self) -> None: + self.document = None # snapshot: invalid-assignment + + # Check that the concise diagnostic identifies the actual setter argument mismatch. + # error: [invalid-assignment] "Expected `Document`, found `None`" + self.document = None +``` + +```snapshot +error[invalid-assignment]: Invalid assignment to data descriptor attribute `document` on type `Self@detach` + --> src/mdtest_snippet.py:11:25 | -11 | instance.attr = 1 # snapshot: invalid-assignment - | ^^^^^^^^^^^^^ +11 | self.document = None # snapshot: invalid-assignment + | ^^^^ Expected `Document`, found `None` | +info: This assignment implicitly calls `__set__` on a descriptor of type `property` +info: Function defined here + --> src/mdtest_snippet.py:7:9 + | +7 | def document(self, document: Document) -> None: ... + | ^^^^^^^^ ------------------ Parameter declared here + | +``` + +### Nested argument type + +```py +class Descriptor: + def __set__(self, instance, value: tuple[int, str]) -> None: ... + +class C: + x = Descriptor() + +c = C() +c.x = (1, b"") # snapshot: invalid-assignment +``` + +```snapshot +error[invalid-assignment]: Invalid assignment to data descriptor attribute `x` on type `C` + --> src/mdtest_snippet.py:8:7 + | +8 | c.x = (1, b"") # snapshot: invalid-assignment + | ^^^^^^^^ Expected `tuple[int, str]`, found `tuple[Literal[1], Literal[b""]]` + | +info: This assignment implicitly calls `__set__` on a descriptor of type `Descriptor` +info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` +info: Function defined here + --> src/mdtest_snippet.py:2:9 + | +2 | def __set__(self, instance, value: tuple[int, str]) -> None: ... + | ^^^^^^^ ---------------------- Parameter declared here + | ``` ## Setting attributes on union types diff --git a/crates/ty_python_semantic/resources/mdtest/properties.md b/crates/ty_python_semantic/resources/mdtest/properties.md index f4f4802acc..44afc2c502 100644 --- a/crates/ty_python_semantic/resources/mdtest/properties.md +++ b/crates/ty_python_semantic/resources/mdtest/properties.md @@ -449,19 +449,34 @@ This attribute access desugars to ```py type(attr_property).__set__(attr_property, c, "a") -# error: [call-non-callable] "Call of wrapper descriptor `property.__set__` failed: calling the setter failed" +# snapshot: invalid-argument-type type(attr_property).__set__(attr_property, c, 1) ``` +```snapshot +error[invalid-argument-type]: Argument to function `C.attr` is incorrect + --> src/mdtest_snippet.py:31:47 + | +31 | type(attr_property).__set__(attr_property, c, 1) + | ^ Expected `str`, found `Literal[1]` + | +info: Function defined here + --> src/mdtest_snippet.py:10:9 + | +10 | def attr(self, value: str) -> None: + | ^^^^ ---------- Parameter declared here + | +``` + which is also equivalent to the following expressions: ```py attr_property.__set__(c, "a") -# error: [call-non-callable] +# error: [invalid-argument-type] attr_property.__set__(c, 1) C.attr.__set__(c, "a") -# error: [call-non-callable] +# error: [invalid-argument-type] C.attr.__set__(c, 1) ``` diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index 1e0d14e04b..4b6a9af792 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -9,7 +9,9 @@ use ruff_python_ast as ast; mod arguments; pub(crate) mod bind; pub(super) use arguments::{Argument, CallArguments}; -pub(super) use bind::{Binding, Bindings, CallableBinding, MatchedArgument}; +pub(super) use bind::{ + Binding, Bindings, CallDiagnosticOverride, CallableBinding, MatchedArgument, +}; /// Whether the right operand's reflected method has priority based on the possible runtime /// classes of both operands. @@ -271,6 +273,16 @@ impl<'db> CallError<'db> { _ => None, }) } + + pub(crate) fn report_diagnostics_with_override( + &self, + context: &InferContext<'db, '_>, + node: ast::AnyNodeRef, + overrides: &CallDiagnosticOverride<'_>, + ) { + self.1 + .report_diagnostics_with_override(context, node, overrides); + } } /// The reason why calling a type failed. diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 22eb24c573..5a7c6f6ac7 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -27,6 +27,7 @@ use self::constructor::{ConstructorBinding, ConstructorContext}; use super::{Argument, CallArguments, CallError, CallErrorKind, InferContext, Signature, Type}; use crate::db::Db; use crate::dunder_all::dunder_all_names; +use crate::lint::LintMetadata; use crate::place::{DefinedPlace, Definedness, Place}; use crate::subscript::PyIndex; use crate::types::call::arguments::{CallArgumentTypes, Expansion, is_expandable_type}; @@ -34,6 +35,7 @@ use crate::types::callable::CallableTypeKind; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, PathBound, PathBounds, Solutions, }; +use crate::types::context::LintDiagnosticGuardBuilder; use crate::types::dedicated::pydantic::{self, ConfigBoolean}; use crate::types::diagnostic::{ CALL_NON_CALLABLE, CALL_TOP_CALLABLE, INVALID_ARGUMENT_TYPE, INVALID_DATACLASS, @@ -78,6 +80,58 @@ use ty_python_core::semantic_index; pub(crate) use self::constructor::ConstructorCallableKind; +/// Overrides the lint and top-level message for a call diagnostic emitted from an implicit call. +/// +/// The original call-error message is retained on the primary annotation, while `info` explains +/// why the call happened. `argument_ranges` maps synthetic call arguments back to source ranges. +pub(crate) struct CallDiagnosticOverride<'a> { + pub(crate) lint: &'static LintMetadata, + pub(crate) message: String, + pub(crate) info: &'a str, + pub(crate) argument_ranges: &'a [TextRange], +} + +struct CallDiagnosticContext<'context, 'overrides, 'db, 'ast> { + context: &'context InferContext<'db, 'ast>, + overrides: Option<&'context CallDiagnosticOverride<'overrides>>, + argument_index_offset: usize, +} + +impl<'db> CallDiagnosticContext<'_, '_, 'db, '_> { + fn report_lint<'ctx, T: Ranged>( + &'ctx self, + lint: &'static LintMetadata, + ranged: T, + ) -> Option> { + let lint = self.overrides.map_or(lint, |overrides| overrides.lint); + self.context.report_lint(lint, ranged).map(|builder| { + if let Some(overrides) = self.overrides { + builder.with_message_override(overrides.message.clone(), overrides.info) + } else { + builder + } + }) + } + + fn get_range(&self, node: ast::AnyNodeRef<'_>, argument_index: Option) -> TextRange { + let argument_index = argument_index.map(|index| index + self.argument_index_offset); + self.overrides + .and_then(|overrides| { + argument_index.and_then(|index| overrides.argument_ranges.get(index)) + }) + .copied() + .unwrap_or_else(|| BindingError::get_node(node, argument_index).range()) + } +} + +impl<'db, 'ast> std::ops::Deref for CallDiagnosticContext<'_, '_, 'db, 'ast> { + type Target = InferContext<'db, 'ast>; + + fn deref(&self) -> &Self::Target { + self.context + } +} + fn generic_contexts_mentioned_in_type<'db>( db: &'db dyn Db, ty: Type<'db>, @@ -1260,6 +1314,37 @@ impl<'db> Bindings<'db> { &self, context: &InferContext<'db, '_>, node: ast::AnyNodeRef, + ) { + self.report_diagnostics_impl( + &CallDiagnosticContext { + context, + overrides: None, + argument_index_offset: 0, + }, + node, + ); + } + + pub(crate) fn report_diagnostics_with_override( + &self, + context: &InferContext<'db, '_>, + node: ast::AnyNodeRef, + overrides: &CallDiagnosticOverride<'_>, + ) { + self.report_diagnostics_impl( + &CallDiagnosticContext { + context, + overrides: Some(overrides), + argument_index_offset: 0, + }, + node, + ); + } + + fn report_diagnostics_impl( + &self, + context: &CallDiagnosticContext<'_, '_, 'db, '_>, + node: ast::AnyNodeRef, ) { // If all elements are not callable, report that the type as a whole is not callable. if self.elements.iter().all(|e| !e.is_callable()) { @@ -1294,7 +1379,7 @@ impl<'db> Bindings<'db> { if !reported_ctor_init_callables.insert(downstream_bindings.callable_type()) { continue; } - downstream_bindings.report_diagnostics(context, node); + downstream_bindings.report_diagnostics_impl(context, node); } } @@ -1302,7 +1387,7 @@ impl<'db> Bindings<'db> { /// If the element is an intersection where all bindings failed, use priority hierarchy. fn report_element_diagnostics( &self, - context: &InferContext<'db, '_>, + context: &CallDiagnosticContext<'_, '_, 'db, '_>, node: ast::AnyNodeRef, element: &BindingsElement<'db>, ) { @@ -1590,23 +1675,7 @@ impl<'db> Bindings<'db> { ] = overload.parameter_types() { if let Some(setter) = property.setter(db) { - if let Ok(return_ty) = setter - .try_call(db, &CallArguments::positional([*instance, *value])) - .map(|binding| binding.return_type(db)) - { - // `property.__set__` returns `None` for ordinary setters, but - // preserving `Never` keeps non-returning setters divergent. - overload.set_return_type(if return_ty.is_never() { - return_ty - } else { - Type::none(db) - }); - } else { - overload.errors.push(BindingError::InternalCallError( - "calling the setter failed", - )); - overload.set_return_type(Type::unknown()); - } + overload.check_property_setter(db, setter, *instance, *value, 1); } else { overload .errors @@ -1648,23 +1717,7 @@ impl<'db> Bindings<'db> { Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderSet(property)) => { if let [Some(instance), Some(value), ..] = overload.parameter_types() { if let Some(setter) = property.setter(db) { - if let Ok(return_ty) = setter - .try_call(db, &CallArguments::positional([*instance, *value])) - .map(|binding| binding.return_type(db)) - { - // `property.__set__` returns `None` for ordinary setters, but - // preserving `Never` keeps non-returning setters divergent. - overload.set_return_type(if return_ty.is_never() { - return_ty - } else { - Type::none(db) - }); - } else { - overload.errors.push(BindingError::InternalCallError( - "calling the setter failed", - )); - overload.set_return_type(Type::unknown()); - } + overload.check_property_setter(db, setter, *instance, *value, 0); } else { overload .errors @@ -4140,7 +4193,7 @@ impl<'db> CallableBinding<'db> { fn report_diagnostics( &self, - context: &InferContext<'db, '_>, + context: &CallDiagnosticContext<'_, '_, 'db, '_>, node: ast::AnyNodeRef, compound_diag: Option<&dyn CompoundDiagnostic>, ) { @@ -6237,6 +6290,37 @@ pub(crate) struct Binding<'db> { } impl<'db> Binding<'db> { + fn check_property_setter( + &mut self, + db: &'db dyn Db, + setter: Type<'db>, + instance: Type<'db>, + value: Type<'db>, + argument_index_offset: usize, + ) { + match setter.try_call(db, &CallArguments::positional([instance, value])) { + Ok(bindings) => { + let return_ty = bindings.return_type(db); + // `property.__set__` returns `None` for ordinary setters, but preserving `Never` + // keeps non-returning setters divergent. + self.set_return_type(if return_ty.is_never() { + return_ty + } else { + Type::none(db) + }); + } + Err(CallError(_, bindings)) => { + self.errors.push(BindingError::PropertySetterCallError( + PropertySetterCallError { + bindings, + argument_index_offset, + }, + )); + self.set_return_type(Type::unknown()); + } + } + } + pub(crate) fn single(signature_type: Type<'db>, signature: Signature<'db>) -> Binding<'db> { let return_ty = signature.return_ty; Binding { @@ -6938,7 +7022,7 @@ impl<'db> Binding<'db> { fn report_diagnostics( &self, - context: &InferContext<'db, '_>, + context: &CallDiagnosticContext<'_, '_, 'db, '_>, node: ast::AnyNodeRef, callable_ty: Type<'db>, callable_description: Option<&CallableDescription>, @@ -7349,9 +7433,10 @@ pub(crate) enum BindingError<'db> { }, PropertyHasNoSetter(PropertyInstanceType<'db>), PropertyHasNoDeleter(PropertyInstanceType<'db>), + PropertySetterCallError(PropertySetterCallError<'db>), /// 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. + /// We use this variant to report errors in `property.__get__` and `property.__delete__`, + /// which can occur when the call to the underlying getter/deleter fails. InternalCallError(&'static str), /// This overload binding of the callable does not match the arguments. // TODO: We could expand this with an enum to specify why the overload is unmatched. @@ -7366,6 +7451,31 @@ pub(crate) enum BindingError<'db> { InvalidDataclassArgument(InvalidDataclassArgument), } +#[derive(Clone, Debug)] +pub(crate) struct PropertySetterCallError<'db> { + bindings: Box>, + argument_index_offset: usize, +} + +impl PartialEq for PropertySetterCallError<'_> { + fn eq(&self, other: &Self) -> bool { + self.argument_index_offset == other.argument_index_offset + && self.bindings.callable_type() == other.bindings.callable_type() + && self + .bindings + .iter_flat() + .flatten() + .flat_map(Binding::errors) + .eq(other + .bindings + .iter_flat() + .flatten() + .flat_map(Binding::errors)) + } +} + +impl Eq for PropertySetterCallError<'_> {} + impl BindingError<'_> { /// Returns whether this error is relevant to `functools.partial(...)` construction. /// @@ -7451,7 +7561,8 @@ impl BindingError<'_> { | BindingError::MissingArguments { .. } | BindingError::UnmatchedOverload | BindingError::PropertyHasNoSetter(..) - | BindingError::PropertyHasNoDeleter(..) => {} + | BindingError::PropertyHasNoDeleter(..) + | BindingError::PropertySetterCallError(..) => {} } } @@ -7511,6 +7622,7 @@ impl<'db> BindingError<'db> { | Self::InvalidDataclassArgument(_) | Self::PropertyHasNoSetter(_) | Self::PropertyHasNoDeleter(_) + | Self::PropertySetterCallError(_) | Self::CalledTopCallable(_) | Self::InternalCallError(_) => false, @@ -7531,7 +7643,7 @@ impl<'db> BindingError<'db> { #[expect(clippy::too_many_arguments)] fn report_diagnostic( &self, - context: &InferContext<'db, '_>, + context: &CallDiagnosticContext<'_, '_, 'db, '_>, node: ast::AnyNodeRef, callable_ty: Type<'db>, callable_description: Option<&CallableDescription>, @@ -7558,7 +7670,7 @@ impl<'db> BindingError<'db> { // silenced diagnostics during overload evaluation, and rely on the assignability // diagnostic being emitted here. - let range = Self::get_node(node, *argument_index); + let range = context.get_range(node, *argument_index); let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else { return; }; @@ -7676,7 +7788,7 @@ impl<'db> BindingError<'db> { argument_index, provided_ty, } => { - let range = Self::get_node(node, *argument_index); + let range = context.get_range(node, *argument_index); let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else { return; }; @@ -7697,8 +7809,8 @@ impl<'db> BindingError<'db> { expected_positional_count, provided_positional_count, } => { - let node = Self::get_node(node, *first_excess_argument_index); - if let Some(builder) = context.report_lint(&TOO_MANY_POSITIONAL_ARGUMENTS, node) { + let range = context.get_range(node, *first_excess_argument_index); + if let Some(builder) = context.report_lint(&TOO_MANY_POSITIONAL_ARGUMENTS, range) { let mut diag = builder.into_diagnostic(format_args!( "Too many positional arguments{}: expected \ {expected_positional_count}, got {provided_positional_count}", @@ -7766,8 +7878,8 @@ impl<'db> BindingError<'db> { argument_name, argument_index, } => { - let node = Self::get_node(node, *argument_index); - if let Some(builder) = context.report_lint(&UNKNOWN_ARGUMENT, node) { + let range = context.get_range(node, *argument_index); + if let Some(builder) = context.report_lint(&UNKNOWN_ARGUMENT, range) { let mut diag = builder.into_diagnostic(format_args!( "Argument `{argument_name}` does not match any known parameter{}", callable_description @@ -7788,8 +7900,8 @@ impl<'db> BindingError<'db> { } Self::UnknownKeywordVariadicArgument { argument_index } => { - let node = Self::get_node(node, *argument_index); - if let Some(builder) = context.report_lint(&UNKNOWN_ARGUMENT, node) { + let range = context.get_range(node, *argument_index); + if let Some(builder) = context.report_lint(&UNKNOWN_ARGUMENT, range) { let mut diag = builder.into_diagnostic(format_args!( "Unpacked argument may contain keyword arguments that do not match any known parameter{}", callable_description @@ -7813,9 +7925,9 @@ impl<'db> BindingError<'db> { argument_index, parameter, } => { - let node = Self::get_node(node, *argument_index); + let range = context.get_range(node, *argument_index); if let Some(builder) = - context.report_lint(&POSITIONAL_ONLY_PARAMETER_AS_KWARG, node) + context.report_lint(&POSITIONAL_ONLY_PARAMETER_AS_KWARG, range) { let mut diag = builder.into_diagnostic(format_args!( "Positional-only parameter {parameter} passed as keyword argument{}", @@ -7840,8 +7952,8 @@ impl<'db> BindingError<'db> { argument_index, parameter, } => { - let node = Self::get_node(node, *argument_index); - if let Some(builder) = context.report_lint(&PARAMETER_ALREADY_ASSIGNED, node) { + let range = context.get_range(node, *argument_index); + if let Some(builder) = context.report_lint(&PARAMETER_ALREADY_ASSIGNED, range) { let mut diag = builder.into_diagnostic(format_args!( "Multiple values provided for parameter {parameter}{}", callable_description @@ -7858,7 +7970,7 @@ impl<'db> BindingError<'db> { error, argument_index, } => { - let range = Self::get_node(node, *argument_index); + let range = context.get_range(node, *argument_index); let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else { return; }; @@ -7953,9 +8065,18 @@ impl<'db> BindingError<'db> { ); } + Self::PropertySetterCallError(error) => { + let context = CallDiagnosticContext { + context: context.context, + overrides: context.overrides, + argument_index_offset: error.argument_index_offset, + }; + error.bindings.report_diagnostics_impl(&context, node); + } + Self::InternalCallError(reason) => { - let node = Self::get_node(node, None); - if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, node) { + let range = context.get_range(node, None); + if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, range) { let mut diag = builder.into_diagnostic(format_args!( "Call{} failed: {reason}", callable_description @@ -7971,8 +8092,8 @@ impl<'db> BindingError<'db> { Self::UnmatchedOverload => {} Self::CalledTopCallable(callable_ty) => { - let node = Self::get_node(node, None); - if let Some(builder) = context.report_lint(&CALL_TOP_CALLABLE, node) { + let range = context.get_range(node, None); + if let Some(builder) = context.report_lint(&CALL_TOP_CALLABLE, range) { let callable_ty_display = callable_ty.display(context.db()); let mut diag = builder.into_diagnostic(format_args!( "Object of type `{callable_ty_display}` is not safe to call; \ @@ -7989,8 +8110,8 @@ impl<'db> BindingError<'db> { } Self::InvalidDataclassApplication(target) => { - let node = Self::get_node(node, None); - if let Some(builder) = context.report_lint(&INVALID_DATACLASS, node) { + let range = context.get_range(node, None); + if let Some(builder) = context.report_lint(&INVALID_DATACLASS, range) { let (message, info) = match target { InvalidDataclassTarget::NamedTuple => ( "Cannot use `dataclass()` on a `NamedTuple` class", @@ -8015,8 +8136,8 @@ impl<'db> BindingError<'db> { } Self::InvalidDataclassArgument(argument) => { - let node = Self::get_node(node, None); - if let Some(builder) = context.report_lint(&INVALID_DATACLASS, node) { + let range = context.get_range(node, None); + if let Some(builder) = context.report_lint(&INVALID_DATACLASS, range) { builder.into_diagnostic(match argument { InvalidDataclassArgument::OrderRequiresEq => { "`order=True` requires `eq=True`" diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 0eaa06d914..36c03f2fcc 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -418,6 +418,7 @@ pub(super) struct LintDiagnosticGuardBuilder<'db, 'ctx> { severity: Severity, source: LintSource, primary_range: TextRange, + message_override: Option<(String, String)>, } impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { @@ -495,6 +496,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { severity, source, primary_range: range, + message_override: None, }) } @@ -508,25 +510,45 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { /// /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. + /// + /// If a message override is present, `message` is retained on the primary annotation. pub(super) fn into_diagnostic( self, message: impl std::fmt::Display, ) -> LintDiagnosticGuard<'db, 'ctx> { - let mut diag = Diagnostic::new(DiagnosticId::Lint(self.id.name()), self.severity, message); - diag.set_documentation_url(Some(self.id.documentation_url())); // This is why `LintDiagnosticGuard::set_primary_message` exists. - // We add the primary annotation here (because it's required), but - // the optional message can be added later. We could accept it here - // in this `build` method, but we already accept the main diagnostic - // message. So the messages are likely to be quite confusable. + // We add the primary annotation here (because it's required). Without a message + // override, its optional message can be added later via `set_primary_message`. let primary_span = Span::from(self.ctx.file()).with_range(self.primary_range); - diag.annotate(Annotation::primary(primary_span)); + let mut diag = if let Some((message_override, info)) = self.message_override { + let mut diag = Diagnostic::new( + DiagnosticId::Lint(self.id.name()), + self.severity, + message_override, + ); + diag.annotate(Annotation::primary(primary_span).message(message)); + diag.info(info); + diag + } else { + let mut diag = + Diagnostic::new(DiagnosticId::Lint(self.id.name()), self.severity, message); + diag.annotate(Annotation::primary(primary_span)); + diag + }; + diag.set_documentation_url(Some(self.id.documentation_url())); LintDiagnosticGuard { ctx: self.ctx, source: self.source, diag: Some(diag), } } + + /// Replace the top-level message and add an info sub-diagnostic while retaining the original + /// message on the primary annotation. + pub(super) fn with_message_override(mut self, message: String, info: &str) -> Self { + self.message_override = Some((message, info.to_string())); + self + } } /// A builder for constructing a diagnostic guard. diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 7deb63723b..e57f14d9a7 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -10,7 +10,7 @@ use crate::diagnostic::format_enumeration; use crate::lint::{Level, LintRegistryBuilder, LintStatus}; use crate::place::{DefinedPlace, Place, place_from_bindings}; use crate::suppression::FileSuppressionId; -use crate::types::call::CallError; +use crate::types::call::{CallDiagnosticOverride, CallError}; use crate::types::class::{ CodeGeneratorKind, DisjointBase, DisjointBaseKind, ExpandedClassBaseEntry, MethodDecorator, }; @@ -1712,15 +1712,18 @@ pub(super) fn report_invalid_attribute_assignment( pub(super) fn report_bad_dunder_set_call<'db>( context: &InferContext<'db, '_>, dunder_set_failure: &CallError<'db>, - attribute: &str, object_type: Type<'db>, + descriptor_type: Type<'db>, + includes_descriptor_argument: bool, target: &ast::ExprAttribute, + value: &ast::Expr, ) { - let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, target) else { - return; - }; let db = context.db(); + let attribute = target.attr.as_str(); if let Some(property) = dunder_set_failure.as_attempt_to_set_property_with_no_setter() { + let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, target) else { + return; + }; let object_type = object_type.display(db); let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot assign to read-only property `{attribute}` on object of type `{object_type}`", @@ -1738,13 +1741,27 @@ pub(super) fn report_bad_dunder_set_call<'db>( )); } } else { - // 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_type.display(db) - )); + let argument_ranges = if includes_descriptor_argument { + &[target.range(), target.value.range(), value.range()][..] + } else { + &[target.value.range(), value.range()][..] + }; + dunder_set_failure.report_diagnostics_with_override( + context, + target.into(), + &CallDiagnosticOverride { + lint: &INVALID_ASSIGNMENT, + message: format!( + "Invalid assignment to data descriptor attribute `{attribute}` on type `{}`", + object_type.display(db) + ), + info: &format!( + "This assignment implicitly calls `__set__` on a descriptor of type `{}`", + descriptor_type.display(db) + ), + argument_ranges, + }, + ); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs b/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs index a42942127f..e42f2db4a6 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs @@ -8,7 +8,7 @@ use crate::types::attribute_write::{ FallbackAttributeWriteRequirement, InstanceAttributeWriteMember, ProtocolMemberWriteRequirement, attribute_write_requirement, property_setter_returns_never, }; -use crate::types::call::{Bindings, CallArguments, CallError}; +use crate::types::call::{Bindings, CallArguments, CallDiagnosticOverride, CallError}; use crate::types::diagnostic::{ INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, UNRESOLVED_ATTRIBUTE, report_bad_dunder_set_call, report_invalid_attribute_assignment, report_possibly_missing_attribute, @@ -55,10 +55,15 @@ enum AssignmentAttributeWriteDiagnostic<'db> { is_setattr_synthesized: bool, }, TerminalDescriptor, - BadDunderSet(CallError<'db>), + BadDunderSet { + failure: CallError<'db>, + descriptor_ty: Type<'db>, + includes_descriptor_argument: bool, + }, PossiblyMissing, BadSetAttr { value_ty: Type<'db>, + failure: CallError<'db>, }, Unresolved { with_period: bool, @@ -410,9 +415,12 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { } InstanceAttributeWriteMember::SetAttr => match setattr_result { Ok(_) | Err(CallDunderError::PossiblyUnbound { .. }) => true, - Err(CallDunderError::CallError(..)) => { + Err(CallDunderError::CallError(kind, bindings, _)) => { if emit_diagnostics { - self.report(AssignmentAttributeWriteDiagnostic::BadSetAttr { value_ty }); + self.report(AssignmentAttributeWriteDiagnostic::BadSetAttr { + value_ty, + failure: CallError(kind, bindings), + }); } false } @@ -484,10 +492,11 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { match setattr_result { Ok(_) | Err(CallDunderError::PossiblyUnbound { .. }) => true, - Err(CallDunderError::CallError(..)) => { + Err(CallDunderError::CallError(kind, bindings, _)) => { if emit_diagnostics { self.report(AssignmentAttributeWriteDiagnostic::BadSetAttr { value_ty, + failure: CallError(kind, bindings), }); } false @@ -581,9 +590,11 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { Ok(_) => true, Err(CallDunderError::CallError(kind, bindings, _)) => { if emit_diagnostics { - self.report(AssignmentAttributeWriteDiagnostic::BadDunderSet(CallError( - kind, bindings, - ))); + self.report(AssignmentAttributeWriteDiagnostic::BadDunderSet { + failure: CallError(kind, bindings), + descriptor_ty, + includes_descriptor_argument: false, + }); } false } @@ -619,7 +630,11 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { Ok(_) => true, Err(error) => { if emit_diagnostics { - self.report(AssignmentAttributeWriteDiagnostic::BadDunderSet(error)); + self.report(AssignmentAttributeWriteDiagnostic::BadDunderSet { + failure: error, + descriptor_ty, + includes_descriptor_argument: true, + }); } false } @@ -778,13 +793,19 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { )); } } - AssignmentAttributeWriteDiagnostic::BadDunderSet(failure) => { + AssignmentAttributeWriteDiagnostic::BadDunderSet { + failure, + descriptor_ty, + includes_descriptor_argument, + } => { report_bad_dunder_set_call( &self.builder.context, &failure, - self.attribute, self.object_ty, + descriptor_ty, + includes_descriptor_argument, self.target, + self.value, ); } AssignmentAttributeWriteDiagnostic::PossiblyMissing => { @@ -795,19 +816,22 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { self.object_ty, ); } - AssignmentAttributeWriteDiagnostic::BadSetAttr { value_ty } => { - if let Some(builder) = self - .builder - .context - .report_lint(&UNRESOLVED_ATTRIBUTE, self.target) - { - builder.into_diagnostic(format_args!( - "Cannot assign object of type `{}` to attribute `{}` on type `{}` with custom `__setattr__` method.", - value_ty.display(db), - self.attribute, - self.object_ty.display(db) - )); - } + AssignmentAttributeWriteDiagnostic::BadSetAttr { value_ty, failure } => { + failure.report_diagnostics_with_override( + &self.builder.context, + self.target.into(), + &CallDiagnosticOverride { + lint: &INVALID_ASSIGNMENT, + message: format!( + "Cannot assign object of type `{}` to attribute `{}` on type `{}`", + value_ty.display(db), + self.attribute, + self.object_ty.display(db) + ), + info: "This assignment implicitly calls a custom `__setattr__` method", + argument_ranges: &[self.target.range(), self.value.range()], + }, + ); } AssignmentAttributeWriteDiagnostic::Unresolved { with_period } => { if let Some(builder) = self From a2635fd8f39e1d34ce8074cb486809426148f3e9 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:37:34 -0400 Subject: [PATCH 038/390] Bump 0.16.0 (#27136) --- BREAKING_CHANGES.md | 63 + CHANGELOG.md | 1379 ++--------------- Cargo.lock | 74 +- Cargo.toml | 72 +- README.md | 6 +- changelogs/0.15.x.md | 1329 ++++++++++++++++ crates/ruff/Cargo.toml | 2 +- crates/ruff/README.md | 2 +- crates/ruff_annotate_snippets/Cargo.toml | 2 +- crates/ruff_cache/Cargo.toml | 2 +- crates/ruff_cache/README.md | 4 +- crates/ruff_db/Cargo.toml | 2 +- crates/ruff_db/README.md | 4 +- crates/ruff_diagnostics/Cargo.toml | 2 +- crates/ruff_diagnostics/README.md | 4 +- crates/ruff_formatter/Cargo.toml | 2 +- crates/ruff_formatter/README.md | 4 +- crates/ruff_graph/Cargo.toml | 2 +- crates/ruff_graph/README.md | 4 +- crates/ruff_index/Cargo.toml | 2 +- crates/ruff_index/README.md | 4 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_linter/README.md | 4 +- .../rules/function_signature_change_in_3.rs | 2 +- .../rules/missing_copyright_notice.rs | 2 +- .../rules/collection_literal.rs | 2 +- .../log_exception_outside_except_handler.rs | 2 +- .../rules/pylint/rules/invalid_bool_return.rs | 2 +- .../pylint/rules/stop_iteration_return.rs | 2 +- .../rules/too_many_positional_arguments.rs | 2 +- .../src/rules/refurb/rules/sorted_min_max.rs | 2 +- .../refurb/rules/unnecessary_from_float.rs | 2 +- .../access_annotations_from_class_dict.rs | 2 +- .../rules/duplicate_entry_in_dunder_all.rs | 2 +- .../ruff/rules/none_not_at_end_of_union.rs | 2 +- crates/ruff_macros/Cargo.toml | 2 +- crates/ruff_macros/README.md | 4 +- crates/ruff_markdown/Cargo.toml | 2 +- crates/ruff_markdown/README.md | 4 +- crates/ruff_memory_usage/Cargo.toml | 2 +- crates/ruff_memory_usage/README.md | 4 +- crates/ruff_notebook/Cargo.toml | 2 +- crates/ruff_notebook/README.md | 4 +- crates/ruff_options_metadata/Cargo.toml | 2 +- crates/ruff_options_metadata/README.md | 4 +- crates/ruff_python_ast/Cargo.toml | 2 +- crates/ruff_python_ast/README.md | 4 +- crates/ruff_python_codegen/Cargo.toml | 2 +- crates/ruff_python_codegen/README.md | 4 +- crates/ruff_python_formatter/Cargo.toml | 2 +- crates/ruff_python_formatter/README.md | 4 +- crates/ruff_python_importer/Cargo.toml | 2 +- crates/ruff_python_importer/README.md | 4 +- crates/ruff_python_index/Cargo.toml | 2 +- crates/ruff_python_index/README.md | 4 +- crates/ruff_python_literal/Cargo.toml | 2 +- crates/ruff_python_literal/README.md | 4 +- crates/ruff_python_parser/Cargo.toml | 2 +- crates/ruff_python_parser/README.md | 4 +- crates/ruff_python_semantic/Cargo.toml | 2 +- crates/ruff_python_semantic/README.md | 4 +- crates/ruff_python_stdlib/Cargo.toml | 2 +- crates/ruff_python_stdlib/README.md | 4 +- crates/ruff_python_trivia/Cargo.toml | 2 +- crates/ruff_python_trivia/README.md | 4 +- crates/ruff_ranged_value/Cargo.toml | 2 +- crates/ruff_ranged_value/README.md | 4 +- crates/ruff_server/Cargo.toml | 2 +- crates/ruff_server/README.md | 4 +- crates/ruff_source_file/Cargo.toml | 2 +- crates/ruff_source_file/README.md | 4 +- crates/ruff_text_size/Cargo.toml | 2 +- crates/ruff_text_size/README.md | 4 +- crates/ruff_wasm/Cargo.toml | 2 +- crates/ruff_wasm/README.md | 4 +- crates/ruff_workspace/Cargo.toml | 2 +- crates/ruff_workspace/README.md | 4 +- crates/ty_combine/Cargo.toml | 2 +- crates/ty_combine/README.md | 4 +- crates/ty_module_resolver/Cargo.toml | 2 +- crates/ty_module_resolver/README.md | 4 +- crates/ty_python_core/Cargo.toml | 2 +- crates/ty_python_core/README.md | 4 +- crates/ty_python_semantic/Cargo.toml | 2 +- crates/ty_python_semantic/README.md | 4 +- crates/ty_site_packages/Cargo.toml | 2 +- crates/ty_site_packages/README.md | 4 +- crates/ty_static/Cargo.toml | 2 +- crates/ty_static/README.md | 4 +- crates/ty_vendored/Cargo.toml | 2 +- docs/formatter.md | 2 +- docs/integrations.md | 8 +- docs/tutorial.md | 2 +- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- uv.lock | 78 +- 96 files changed, 1728 insertions(+), 1525 deletions(-) create mode 100644 changelogs/0.15.x.md diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index d634cb306d..8d73f590af 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,5 +1,68 @@ # Breaking Changes +## 0.16.0 + +- **New default rules** + + Ruff now enables a much larger set of rules by default (413, up from 59). See the blog post for + more details and the new [Default Rules](https://docs.astral.sh/ruff/default-rules/) page for a + full listing of the enabled rules. + +- **Python code block formatting in Markdown files** + + Ruff can now format Python code blocks in Markdown files and will do this by default. See the + [documentation](https://docs.astral.sh/ruff/formatter/#markdown-code-formatting) for more details. + +- **`ruff: ignore` suppression comments** + + Ruff now supports `ruff: ignore` comments at the ends of lines, like `noqa` comments, or on the line preceding a diagnostic. For example, these both suppress an [`unused-import`](https://docs.astral.sh/ruff/rules/unused-import/) (`F401`) diagnostic: + + ```py + import math # ruff: ignore[F401] + + # ruff: ignore[F401] + import os + ``` + +- **Fix diffs in linter and formatter output** + + Fixes are now shown in `check` and `format --check` output: + + ````console + ❯ ruff format --check . + unformatted: File would be reformatted + --> try.md:1:1 + | + 1 | ```python + - import math + 2 + import math + 3 | ``` + | + + 1 file would be reformatted + ```` + + This example also shows off the Markdown formatting. + +- **Output format support in `format --check`** + + `format --check` now supports the same output formats as the linter, including the `github` and + `gitlab` outputs for rendering annotations in CI: + + ```console + ❯ ruff format --check --output-format github . + ::error title=ruff (unformatted),file=try.md,line=2,col=8,endLine=2,endColumn=10::try.md:2:8: unformatted: File would be reformatted + ``` + + See the CLI help or [documentation](https://docs.astral.sh/ruff/settings/#output-format) for the + full list of supported formats. + +- **Some fields are now optional in the JSON output** + + The `filename`, `location`, `end_location`, `fix.edits[].location`, and `fix.edits[].end_location` + fields in the JSON output format may now be `null` rather than defaulting to the empty string and + row 1, column 1, respectively. + ## 0.15.0 - **2026 formatter style guide** diff --git a/CHANGELOG.md b/CHANGELOG.md index e92d24f1b8..dace580a43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1334 +1,145 @@ # Changelog -## 0.15.22 +## 0.16.0 -Released on 2026-07-16. +Released on 2026-07-23. -### Preview features - -- \[`pycodestyle`\] Add an autofix for `E402` ([#22212](https://github.com/astral-sh/ruff/pull/22212)) -- \[`refurb`\] Allow subclassing builtins in stub files (`FURB189`) ([#26812](https://github.com/astral-sh/ruff/pull/26812)) -- \[`ruff`\] Add rule to replace `noqa` comments with `ruff:ignore` (`RUF105`) ([#26423](https://github.com/astral-sh/ruff/pull/26423)) -- \[`ruff`\] Add rule to use human-readable names in `ruff:ignore` comments (`RUF106`) ([#26682](https://github.com/astral-sh/ruff/pull/26682)) -- \[`ruff`\] Add rule to use human-readable names in configuration selectors (`RUF201`) ([#26772](https://github.com/astral-sh/ruff/pull/26772)) - -### Bug fixes - -- \[`flake8-pyi`\] Fix false positive in `__all__` (`PYI053`) ([#26872](https://github.com/astral-sh/ruff/pull/26872)) - -### Rule changes - -- \[`pylint`\] Ignore mutable type updates in `redefined-loop-name` (`PLW2901`) ([#25733](https://github.com/astral-sh/ruff/pull/25733)) - -### Performance - -- Avoid redundant lexer token bookkeeping ([#26765](https://github.com/astral-sh/ruff/pull/26765)) -- Avoid redundant pending-indentation writes ([#26774](https://github.com/astral-sh/ruff/pull/26774)) -- Avoid unnecessary identifier lookahead ([#26525](https://github.com/astral-sh/ruff/pull/26525)) -- Reuse parser scratch buffers ([#26798](https://github.com/astral-sh/ruff/pull/26798)) - -### Documentation - -- Document argfile support ([#26803](https://github.com/astral-sh/ruff/pull/26803)) -- \[`flake8-datetimez`\] Clarify naming guidance for `datetime.today` (`DTZ002`) ([#26658](https://github.com/astral-sh/ruff/pull/26658)) -- \[`pycodestyle`\] Document `E731` fix safety ([#26847](https://github.com/astral-sh/ruff/pull/26847)) -- \[`ruff`\] Clarify intentional async contexts for `unused-async` (`RUF029`) ([#26641](https://github.com/astral-sh/ruff/pull/26641)) - -### Contributors - -- [@dwego](https://github.com/dwego) -- [@MichaReiser](https://github.com/MichaReiser) -- [@Joosboy](https://github.com/Joosboy) -- [@KaufmanDmitriy](https://github.com/KaufmanDmitriy) -- [@PeterJCLaw](https://github.com/PeterJCLaw) -- [@ntBre](https://github.com/ntBre) -- [@charliermarsh](https://github.com/charliermarsh) - -## 0.15.21 - -Released on 2026-07-09. - -### Preview features - -- Add `--add-ignore` for adding `ruff:ignore` comments ([#26346](https://github.com/astral-sh/ruff/pull/26346)) -- \[`flake8-comprehensions`\] Drop `C409` tuple comprehension preview behavior ([#25707](https://github.com/astral-sh/ruff/pull/25707)) -- Avoid whitespace normalization when formatting comments ([#26455](https://github.com/astral-sh/ruff/pull/26455)) -- \[`pyupgrade`\] Lint and fix use of deprecated `abc` decorators (`UP051`) ([#26417](https://github.com/astral-sh/ruff/pull/26417)) - -### Bug fixes - -- Refine non-empty f-string detection ([#26526](https://github.com/astral-sh/ruff/pull/26526)) -- Detect syntax errors in individual notebook cells ([#26419](https://github.com/astral-sh/ruff/pull/26419)) -- \[`flake8-implicit-str-concat`\] Fix `ISC003` autofix incorrectly stripping `+` from comments ([#26554](https://github.com/astral-sh/ruff/pull/26554)) - -### Rule changes - -- \[`flake8-executable`\] Mark `EXE004` fix as unsafe ([#26033](https://github.com/astral-sh/ruff/pull/26033)) -- \[`flake8-pyi`\] Mark `PYI061` fixes as unsafe in Python files ([#26533](https://github.com/astral-sh/ruff/pull/26533)) -- \[`pydocstyle`\] Skip `overload-with-docstring` in stub files (`D418`) ([#26318](https://github.com/astral-sh/ruff/pull/26318)) - -### Performance - -- Avoid per-token source index visitor calls ([#26506](https://github.com/astral-sh/ruff/pull/26506)) -- Cache parenthesized expression boundaries in the formatter ([#26344](https://github.com/astral-sh/ruff/pull/26344)) -- Improve performance of rendering edits in preview mode ([#26565](https://github.com/astral-sh/ruff/pull/26565)) -- Inline `fits_element` in formatter ([#26429](https://github.com/astral-sh/ruff/pull/26429)) -- Inline formatter printing hot paths ([#26504](https://github.com/astral-sh/ruff/pull/26504)) -- Lazily create builtin bindings ([#26510](https://github.com/astral-sh/ruff/pull/26510)) -- Skip empty trivia scans in the source indexer ([#26507](https://github.com/astral-sh/ruff/pull/26507)) -- Use ICF for macOS release builds ([#25780](https://github.com/astral-sh/ruff/pull/25780)) - -### Formatter - -- Add `--extend-exclude` to `ruff format` ([#26372](https://github.com/astral-sh/ruff/pull/26372)) - -### Documentation - -- Add "How does Ruff's import sorting compare to isort?" link to README ([#26530](https://github.com/astral-sh/ruff/pull/26530)) -- Fix Mozilla Firefox repository link in README ([#26537](https://github.com/astral-sh/ruff/pull/26537)) -- \[`flake8-bandit`\] Fix misleading docstring for `mako-templates` (`S702`) ([#26432](https://github.com/astral-sh/ruff/pull/26432)) -- \[`ruff`\] Fix non-triggering example for `if-key-in-dict-del` (`RUF051`) ([#26433](https://github.com/astral-sh/ruff/pull/26433)) - -### Contributors - -- [@EkriirkE](https://github.com/EkriirkE) -- [@tingerrr](https://github.com/tingerrr) -- [@s-rigaud](https://github.com/s-rigaud) -- [@nikolauspschuetz](https://github.com/nikolauspschuetz) -- [@Avasam](https://github.com/Avasam) -- [@ntBre](https://github.com/ntBre) -- [@omar-y-abdi](https://github.com/omar-y-abdi) -- [@AlexWaygood](https://github.com/AlexWaygood) -- [@sylvestre](https://github.com/sylvestre) -- [@shaanmajid](https://github.com/shaanmajid) -- [@lerebear](https://github.com/lerebear) -- [@baltasarblanco](https://github.com/baltasarblanco) -- [@Sanjays2402](https://github.com/Sanjays2402) -- [@ZedThree](https://github.com/ZedThree) -- [@servusdei2018](https://github.com/servusdei2018) -- [@charliermarsh](https://github.com/charliermarsh) -- [@jesco-absolute](https://github.com/jesco-absolut) -- [@velikodniy](https://github.com/velikodniy) -- [@zaniebot](https://github.com/zaniebot) -- [@epage](https://github.com/epage) - -## 0.15.20 - -Released on 2026-06-25. - -### Preview features - -- Allow human-readable names in rule selectors ([#25887](https://github.com/astral-sh/ruff/pull/25887)) -- Emit a warning instead of an error for unknown rule selectors ([#26113](https://github.com/astral-sh/ruff/pull/26113)) -- Match `noqa` shebang handling in `ruff:ignore` comments ([#26286](https://github.com/astral-sh/ruff/pull/26286)) -- \[`ruff`\] Remove `pytest-fixture-autouse` (`RUF076`) ([#26240](https://github.com/astral-sh/ruff/pull/26240), [#26371](https://github.com/astral-sh/ruff/pull/26371)) - -### Documentation - -- Add versioning sections to custom crate READMEs ([#26317](https://github.com/astral-sh/ruff/pull/26317)) -- Update `ruff_python_parser` README for crates.io ([#26315](https://github.com/astral-sh/ruff/pull/26315)) -- \[`perflint`\] Clarify that `PERF402` applies to any iterable ([#26242](https://github.com/astral-sh/ruff/pull/26242)) - -### Contributors +Check out the [blog post](https://astral.sh/blog/ruff-v0.16.0) for a migration +guide and overview of the changes! -- [@dhruvmanila](https://github.com/dhruvmanila) -- [@MichaReiser](https://github.com/MichaReiser) -- [@ntBre](https://github.com/ntBre) -- [@trilamsr](https://github.com/trilamsr) +### Breaking changes -## 0.15.19 +- Ruff now enables a much larger set of rules by default (413, up from 59). See the blog post for + more details and the new [Default Rules](https://docs.astral.sh/ruff/default-rules/) page for a + full listing of the enabled rules. -Released on 2026-06-23. +- Ruff can now format Python code blocks in Markdown files and will do this by default. See the + [documentation](https://docs.astral.sh/ruff/formatter/#markdown-code-formatting) for more details. -### Preview features +- Ruff now supports `ruff: ignore` comments at the ends of lines, like `noqa` comments, or on the line preceding a diagnostic. For example, these both suppress an [`unused-import`](https://docs.astral.sh/ruff/rules/unused-import/) (`F401`) diagnostic: -- Support human-readable names when hovering suppression comments and in code actions ([#26114](https://github.com/astral-sh/ruff/pull/26114)) + ```py + import math # ruff: ignore[F401] -### Bug fixes + # ruff: ignore[F401] + import os + ``` -- Fall back to default settings when editor-only settings are invalid ([#26244](https://github.com/astral-sh/ruff/pull/26244)) -- Fix panic when inserting text at a notebook cell boundary ([#26111](https://github.com/astral-sh/ruff/pull/26111)) +- Fixes are now shown in `check` and `format --check` output: -### Rule changes + ````console + ❯ ruff format --check . + unformatted: File would be reformatted + --> try.md:1:1 + | + 1 | ```python + - import math + 2 + import math + 3 | ``` + | -- \[`pylint`\] Update fix suggestions for `__floor__`, `__trunc__`, `__length_hint__`, and `__matmul__` variants (`PLC2801`) ([#26239](https://github.com/astral-sh/ruff/pull/26239)) + 1 file would be reformatted + ```` -### Performance + This example also shows off the Markdown formatting. -- Avoid allocating when parsing single string literals ([#26200](https://github.com/astral-sh/ruff/pull/26200)) -- Avoid reallocating singleton call arguments ([#26223](https://github.com/astral-sh/ruff/pull/26223)) -- Lazily create source files for lint diagnostics ([#26226](https://github.com/astral-sh/ruff/pull/26226)) -- Optimize formatter text width and indentation ([#26236](https://github.com/astral-sh/ruff/pull/26236)) -- Reserve capacity for builtin bindings ([#26229](https://github.com/astral-sh/ruff/pull/26229)) -- Skip repeated-key checks for singleton dictionaries ([#26228](https://github.com/astral-sh/ruff/pull/26228)) -- Use ArrayVec for qualified name segments ([#26224](https://github.com/astral-sh/ruff/pull/26224)) +- `format --check` now supports the same output formats as the linter, including the `github` and + `gitlab` outputs for rendering annotations in CI: -### Documentation + ```console + ❯ ruff format --check --output-format github . + ::error title=ruff (unformatted),file=try.md,line=2,col=8,endLine=2,endColumn=10::try.md:2:8: unformatted: File would be reformatted + ``` -- \[`flake8-pyi`\] Note that `PYI051` is an opinionated stylistic rule ([#26179](https://github.com/astral-sh/ruff/pull/26179)) -- \[`pyupgrade`\] Clarify `UP029` as a Python 2 compatibility rule ([#26243](https://github.com/astral-sh/ruff/pull/26243)) + See the CLI help or [documentation](https://docs.astral.sh/ruff/settings/#output-format) for the + full list of supported formats. -### Other changes +- The `filename`, `location`, `end_location`, `fix.edits[].location`, and `fix.edits[].end_location` + fields in the JSON output format may now be `null` rather than defaulting to the empty string and + row 1, column 1, respectively. -- Publish Ruff crates to crates.io ([#26271](https://github.com/astral-sh/ruff/pull/26271)) +### Stabilization -### Contributors +The following rules have been stabilized and are no longer in preview: -- [@MakenRosa](https://github.com/MakenRosa) -- [@MichaReiser](https://github.com/MichaReiser) -- [@trilamsr](https://github.com/trilamsr) -- [@ntBre](https://github.com/ntBre) -- [@sanjibani](https://github.com/sanjibani) -- [@charliermarsh](https://github.com/charliermarsh) +- [`airflow3-incompatible-function-signature`](https://docs.astral.sh/ruff/rules/airflow3-incompatible-function-signature) + (`AIR303`) +- [`missing-copyright-notice`](https://docs.astral.sh/ruff/rules/missing-copyright-notice) + (`CPY001`) +- [`unnecessary-from-float`](https://docs.astral.sh/ruff/rules/unnecessary-from-float) (`FURB164`) +- [`sorted-min-max`](https://docs.astral.sh/ruff/rules/sorted-min-max) (`FURB192`) +- [`implicit-string-concatenation-in-collection-literal`](https://docs.astral.sh/ruff/rules/implicit-string-concatenation-in-collection-literal) + (`ISC004`) +- [`log-exception-outside-except-handler`](https://docs.astral.sh/ruff/rules/log-exception-outside-except-handler) + (`LOG004`) +- [`invalid-bool-return-type`](https://docs.astral.sh/ruff/rules/invalid-bool-return-type) + (`PLE0304`) +- [`too-many-positional-arguments`](https://docs.astral.sh/ruff/rules/too-many-positional-arguments) + (`PLR0917`) +- [`stop-iteration-return`](https://docs.astral.sh/ruff/rules/stop-iteration-return) (`PLR1708`) +- [`none-not-at-end-of-union`](https://docs.astral.sh/ruff/rules/none-not-at-end-of-union) + (`RUF036`) +- [`access-annotations-from-class-dict`](https://docs.astral.sh/ruff/rules/access-annotations-from-class-dict) + (`RUF063`) +- [`duplicate-entry-in-dunder-all`](https://docs.astral.sh/ruff/rules/duplicate-entry-in-dunder-all) + (`RUF068`) -## 0.15.18 +The following behaviors have been stabilized: -Released on 2026-06-18. +- [`blind-except`](https://docs.astral.sh/ruff/rules/blind-except) (`BLE001`) is now suppressed when + the exception is logged via `logging` methods other than `critical`, `error` and `exception`. +- [`future-required-type-annotation`](https://docs.astral.sh/ruff/rules/future-required-type-annotation) + (`FA102`) now checks for additional [PEP 585](https://peps.python.org/pep-0585/)-compatible + APIs, such as those from `collections.abc`. +- [`f-string-in-get-text-func-call`](https://docs.astral.sh/ruff/rules/f-string-in-get-text-func-call) + (`INT001`), + [`format-in-get-text-func-call`](https://docs.astral.sh/ruff/rules/format-in-get-text-func-call) + (`INT002`), and + [`printf-in-get-text-func-call`](https://docs.astral.sh/ruff/rules/printf-in-get-text-func-call) + (`INT003`) now check for additional common ways of using the `gettext` module, such as assigning + it to `builtins._`. +- [`suspicious-url-open-usage`](https://docs.astral.sh/ruff/rules/suspicious-url-open-usage) + (`S310`) now resolves local string literal bindings to avoid more false positives. +- [`snmp-insecure-version`](https://docs.astral.sh/ruff/rules/snmp-insecure-version) (`S508`) and + [`snmp-weak-cryptography`](https://docs.astral.sh/ruff/rules/snmp-weak-cryptography) (`S509`) now + support the recommended API from newer versions of PySNMP. +- [`typing-text-str-alias`](https://docs.astral.sh/ruff/rules/typing-text-str-alias) (`UP019`) now + recognizes `typing_extensions.Text` in addition to `typing.Text`. ### Preview features -- Handle nested `ruff:ignore` comments ([#25791](https://github.com/astral-sh/ruff/pull/25791)) -- Stop displaying severity in output ([#26050](https://github.com/astral-sh/ruff/pull/26050)) -- Use human-readable names in CLI output ([#25937](https://github.com/astral-sh/ruff/pull/25937)) -- Use human-readable names in LSP and playground diagnostics ([#26058](https://github.com/astral-sh/ruff/pull/26058)) -- \[`pydocstyle`\] Prevent property docstrings starting with verbs (`D421`) ([#23775](https://github.com/astral-sh/ruff/pull/23775)) -- \[`flake8-pyi`\] Extend `PYI033` to Python files ([#26129](https://github.com/astral-sh/ruff/pull/26129)) +- \[`pyupgrade`\] Fix false positive with `TypeVar` default before Python 3.13 (`UP040`) ([#26888](https://github.com/astral-sh/ruff/pull/26888)) ### Bug fixes -- Detect equivalent numeric mapping keys ([#26009](https://github.com/astral-sh/ruff/pull/26009)) -- Detect mapping keys equivalent to booleans ([#25982](https://github.com/astral-sh/ruff/pull/25982)) -- Detect repeated signed and complex dictionary keys ([#26007](https://github.com/astral-sh/ruff/pull/26007)) +- \[`ruff`\] Fix missing check on unrecognized early bound (`RUF016`) ([#26986](https://github.com/astral-sh/ruff/pull/26986)) ### Rule changes -- \[`flake8-pyi`\] Rename `PYI033` to `legacy-type-comment` ([#26131](https://github.com/astral-sh/ruff/pull/26131)) +- Insert a space after the colon in Ruff suppression comments ([#27123](https://github.com/astral-sh/ruff/pull/27123)) ### Performance -- Use `ThinVec` for call keywords ([#25999](https://github.com/astral-sh/ruff/pull/25999)) -- Inline parser recovery context checks ([#26038](https://github.com/astral-sh/ruff/pull/26038)) -- Match parser keywords as bytes ([#26037](https://github.com/astral-sh/ruff/pull/26037)) -- Move value parsing out of lexing ([#25360](https://github.com/astral-sh/ruff/pull/25360)) - -### Server - -- Render subdiagnostics and secondary annotations as related information ([#26011](https://github.com/astral-sh/ruff/pull/26011)) +- \[`pyupgrade`\] Speed up `unnecessary-future-import` (`UP010`) ([#27047](https://github.com/astral-sh/ruff/pull/27047)) ### Documentation -- Update fix availability for always-fixable rules ([#26091](https://github.com/astral-sh/ruff/pull/26091)) -- \[`flake8-tidy-imports`\] Add fix safety section (`TID252`) ([#17491](https://github.com/astral-sh/ruff/pull/17491)) - -### Parser - -- Reject `__debug__` lambda parameters ([#26022](https://github.com/astral-sh/ruff/pull/26022)) -- Reject `_` as a match-pattern target ([#25977](https://github.com/astral-sh/ruff/pull/25977)) -- Reject multiple starred names in sequence patterns ([#25976](https://github.com/astral-sh/ruff/pull/25976)) -- Reject parenthesized star imports ([#26021](https://github.com/astral-sh/ruff/pull/26021)) -- Reject starred comprehension targets ([#26023](https://github.com/astral-sh/ruff/pull/26023)) -- Reject unparenthesized generator expressions in class bases ([#25978](https://github.com/astral-sh/ruff/pull/25978)) -- Reject `yield` expressions after commas ([#26024](https://github.com/astral-sh/ruff/pull/26024)) -- Validate function type parameter default order ([#25981](https://github.com/astral-sh/ruff/pull/25981)) - -### Playground - -- Make diagnostic links clickable ([#26104](https://github.com/astral-sh/ruff/pull/26104)) -- Use diagnostic tags ([#26105](https://github.com/astral-sh/ruff/pull/26105)) - -### Contributors - -- [@AlexWaygood](https://github.com/AlexWaygood) -- [@ntBre](https://github.com/ntBre) -- [@gtkacz](https://github.com/gtkacz) -- [@MichaReiser](https://github.com/MichaReiser) -- [@charliermarsh](https://github.com/charliermarsh) -- [@Kalmaegi](https://github.com/Kalmaegi) - -## 0.15.17 - -Released on 2026-06-11. - -### Preview features - -- Allow human-readable names in suppression comments ([#25614](https://github.com/astral-sh/ruff/pull/25614)) -- Fix handling of `ignore` comments within a `disable`/`enable` pair ([#25845](https://github.com/astral-sh/ruff/pull/25845)) -- Prioritize human-readable names in CLI output ([#25869](https://github.com/astral-sh/ruff/pull/25869)) -- Respect diagnostic start and parent ranges and trailing comments in `ruff:ignore` suppressions ([#25673](https://github.com/astral-sh/ruff/pull/25673)) -- \[`flake8-async`\] Add `trio.as_safe_channel` to safe decorators (`ASYNC119`) ([#25775](https://github.com/astral-sh/ruff/pull/25775)) -- \[`flake8-pytest-style`\] Also check `pytest_asyncio` fixtures ([#25375](https://github.com/astral-sh/ruff/pull/25375)) -- \[`ruff`\] Ban `pytest` autouse fixtures (`RUF076`) ([#25477](https://github.com/astral-sh/ruff/pull/25477)) -- \[`pyupgrade`\] Add `from __future__ import annotations` automatically (`UP007`, `UP045`) ([#23259](https://github.com/astral-sh/ruff/pull/23259)) - -### Bug fixes - -- Fix diagnostic when `ruff:enable` or `ruff:disable` appears where `ruff:ignore` is expected ([#25700](https://github.com/astral-sh/ruff/pull/25700)) -- \[`pyupgrade`\] Preserve leading empty literals to avoid syntax errors (`UP032`) ([#25491](https://github.com/astral-sh/ruff/pull/25491)) - -### Rule changes - -- \[`flake8-pytest-style`\] Clarify diagnostic message for single parameters (`PT007`) ([#25592](https://github.com/astral-sh/ruff/pull/25592)) -- \[`numpy`\] Drop autofix for `np.in1d` (`NPY201`) ([#25612](https://github.com/astral-sh/ruff/pull/25612)) -- \[`pylint`\] Exempt Python version comparisons (`PLR2004`) ([#25743](https://github.com/astral-sh/ruff/pull/25743)) - -### Performance - -- Reserve AST `Vec`s with correct capacity for common cases ([#25451](https://github.com/astral-sh/ruff/pull/25451)) - -### Formatter - -- Preserve whitespace for Quarto cell option comments ([#25641](https://github.com/astral-sh/ruff/pull/25641)) - -### CLI - -- Allow rule names in `ruff rule` ([#25640](https://github.com/astral-sh/ruff/pull/25640)) - -### Other changes - -- Fix playground diagnostics scrollbars ([#25642](https://github.com/astral-sh/ruff/pull/25642)) +- \[`ruff`\] Add missing period in "Why is this bad?" section (`RUF200`) ([#26930](https://github.com/astral-sh/ruff/pull/26930)) +- \[`flake8-simplify`\] Clarify `os.environ` behavior on Windows (`SIM112`) ([#26972](https://github.com/astral-sh/ruff/pull/26972)) +- \[`pydocstyle`\] Document fix safety (`D400`) ([#26971](https://github.com/astral-sh/ruff/pull/26971)) ### Contributors -- [@SuryanshSS1011](https://github.com/SuryanshSS1011) -- [@anishgirianish](https://github.com/anishgirianish) -- [@romero-deshaw](https://github.com/romero-deshaw) -- [@karlhillx](https://github.com/karlhillx) -- [@carljm](https://github.com/carljm) -- [@ntBre](https://github.com/ntBre) -- [@11happy](https://github.com/11happy) -- [@Kilo59](https://github.com/Kilo59) -- [@oconnor663](https://github.com/oconnor663) -- [@LeonidasZhak](https://github.com/LeonidasZhak) -- [@DavisVaughan](https://github.com/DavisVaughan) -- [@MeGaGiGaGon](https://github.com/MeGaGiGaGon) - [@jonathandung](https://github.com/jonathandung) +- [@Joosboy](https://github.com/Joosboy) - [@MichaReiser](https://github.com/MichaReiser) -- [@brianmego](https://github.com/brianmego) - -## 0.15.16 - -Released on 2026-06-04. - -### Preview features - -- \[`flake8-async`\] Implement `yield-in-context-manager-in-async-generator` (`ASYNC119`) ([#24644](https://github.com/astral-sh/ruff/pull/24644)) -- \[`pylint`\] Narrow diagnostic range and exclude cases without exception handlers (`PLW0717`) ([#25440](https://github.com/astral-sh/ruff/pull/25440)) -- \[`ruff`\] Treat `yield` before `break` from a terminal loop as terminal (`RUF075`) ([#25447](https://github.com/astral-sh/ruff/pull/25447)) - -### Bug fixes - -- \[`eradicate`\] Avoid flagging `ruff:ignore` comments as code (`ERA001`) ([#25537](https://github.com/astral-sh/ruff/pull/25537)) -- \[`eradicate`\] Fix `ERA001`/`RUF100` conflict when `noqa` is on commented-out code ([#25414](https://github.com/astral-sh/ruff/pull/25414)) -- \[`pyflakes`\] Avoid removing the `format` call when it would change behavior (`F523`) ([#25320](https://github.com/astral-sh/ruff/pull/25320)) -- \[`pylint`\] Avoid syntax errors in invalid character replacements in f-strings before Python 3.12 (`PLE2510`, `PLE2512`, `PLE2513`, `PLE2514`, `PLE2515`) ([#25544](https://github.com/astral-sh/ruff/pull/25544)) -- \[`pyupgrade`\] Avoid converting `format` calls with more kinds of side effects (`UP032`) ([#25484](https://github.com/astral-sh/ruff/pull/25484)) - -### Rule changes - -- \[`flake8-pytest-style`\] Avoid fixes for ambiguous `argnames` and `argvalues` combinations (`PT006`) ([#24776](https://github.com/astral-sh/ruff/pull/24776)) - -### Performance - -- Drop excess capacity from statement suites during parsing ([#25368](https://github.com/astral-sh/ruff/pull/25368)) - -### Documentation - -- \[`pydocstyle`\] Improve discoverability of rules enabled for each convention ([#24973](https://github.com/astral-sh/ruff/pull/24973)) -- \[`ruff`\] Restore example code for Python versions before 3.15 (`RUF017`) ([#25439](https://github.com/astral-sh/ruff/pull/25439)) -- Fix typo `bin/active` → `bin/activate` in tutorial ([#25473](https://github.com/astral-sh/ruff/pull/25473)) - -### Other changes - -- Shrink additional parser AST collections ([#25465](https://github.com/astral-sh/ruff/pull/25465)) - -### Contributors - -- [@Redslayer112](https://github.com/Redslayer112) -- [@koriyoshi2041](https://github.com/koriyoshi2041) -- [@George-Ogden](https://github.com/George-Ogden) -- [@TejasAmle](https://github.com/TejasAmle) -- [@anishgirianish](https://github.com/anishgirianish) -- [@ntBre](https://github.com/ntBre) -- [@MichaReiser](https://github.com/MichaReiser) -- [@loganrosen](https://github.com/loganrosen) -- [@RafaelJohn9](https://github.com/RafaelJohn9) -- [@adityasingh2400](https://github.com/adityasingh2400) - -## 0.15.15 - -Released on 2026-05-28. - -### Preview features - -- Fix Markdown closing fence handling ([#25310](https://github.com/astral-sh/ruff/pull/25310)) -- \[`pyflakes`\] Report duplicate imports in `typing.TYPE_CHECKING` block (`F811`) ([#22560](https://github.com/astral-sh/ruff/pull/22560)) - -### Bug fixes - -- \[`pyflakes`\] Treat function-scope bare annotations as locals per PEP 526 (`F821`) ([#21540](https://github.com/astral-sh/ruff/pull/21540)) - -### Performance - -- Avoid redundant `TokenValue` drops in the lexer ([#25300](https://github.com/astral-sh/ruff/pull/25300)) -- Reduce memory usage by dropping token-excess capacity and improve performance by approximating the initial tokens `Vec` size ([#25354](https://github.com/astral-sh/ruff/pull/25354)) -- Use `ThinVec` in AST to shrink `Stmt` ([#25361](https://github.com/astral-sh/ruff/pull/25361)) - -### Documentation - -- Fix `line-length` example for `--config` option ([#25389](https://github.com/astral-sh/ruff/pull/25389)) -- \[`flake8-comprehensions`\] Document `RecursionError` edge case in `__len__` (`C416`) ([#25286](https://github.com/astral-sh/ruff/pull/25286)) -- \[`mccabe`\] Improve example (`C901`) ([#25287](https://github.com/astral-sh/ruff/pull/25287)) -- \[`pyupgrade`\] Clarify fix safety docs (`UP007`, `UP045`) ([#25288](https://github.com/astral-sh/ruff/pull/25288)) -- \[`refurb`\] Document `FURB192` exception change for empty sequences ([#25317](https://github.com/astral-sh/ruff/pull/25317)) -- \[`ruff`\] Document false negative for user-defined types (`RUF013`) ([#25289](https://github.com/astral-sh/ruff/pull/25289)) - -### Formatter - -- Fix formatting of lambdas nested within f-strings ([#25398](https://github.com/astral-sh/ruff/pull/25398)) - -### Server - -- Return code action for `codeAction/resolve` requests that contain no or no valid URL ([#25365](https://github.com/astral-sh/ruff/pull/25365)) - -### Other changes - -- Expand semantic syntax errors for invalid walruses ([#25415](https://github.com/astral-sh/ruff/pull/25415)) - -### Contributors - -- [@chirizxc](https://github.com/chirizxc) -- [@ntBre](https://github.com/ntBre) -- [@adityasingh2400](https://github.com/adityasingh2400) -- [@charliermarsh](https://github.com/charliermarsh) -- [@fallintoplace](https://github.com/fallintoplace) -- [@martin-schlossarek](https://github.com/martin-schlossarek) -- [@MichaReiser](https://github.com/MichaReiser) -- [@Ruchir28](https://github.com/Ruchir28) - -## 0.15.14 - -Released on 2026-05-21. - -### Preview features - -- \[`airflow`\] Implement `airflow-task-implicit-multiple-outputs` (`AIR202`) ([#25152](https://github.com/astral-sh/ruff/pull/25152)) -- \[`flake8-use-pathlib`\] Mark `PTH101` fix as unsafe when first argument is a class attribute annotated as `int` ([#25086](https://github.com/astral-sh/ruff/pull/25086)) -- \[`pylint`\] Implement `too-many-try-statements` (`W0717`) ([#23970](https://github.com/astral-sh/ruff/pull/23970)) -- \[`ruff`\] Add `incorrect-decorator-order` (`RUF074`) ([#23461](https://github.com/astral-sh/ruff/pull/23461)) -- \[`ruff`\] Add `fallible-context-manager` (`RUF075`) ([#22844](https://github.com/astral-sh/ruff/pull/22844)) - -### Bug fixes - -- Fix lambda formatting in interpolated string expressions ([#25144](https://github.com/astral-sh/ruff/pull/25144)) -- Treat generic `frozenset` annotations as immutable ([#25251](https://github.com/astral-sh/ruff/pull/25251)) -- \[`flake8-type-checking`\] Avoid `strict` behavior when `future-annotations` are enabled (`TC001`, `TC002`, `TC003`) ([#25035](https://github.com/astral-sh/ruff/pull/25035)) -- \[`pylint`\] Avoid false positives in `else` clause (`PLR1733`) ([#25177](https://github.com/astral-sh/ruff/pull/25177)) - -### Rule changes - -- \[`flake8-comprehensions`\] Skip `C417` for lambdas with positional-only parameters ([#25272](https://github.com/astral-sh/ruff/pull/25272)) -- \[`flake8-simplify`\] Preserve f-string source verbatim in `SIM101` fix ([#25061](https://github.com/astral-sh/ruff/pull/25061)) - -### Performance - -- Avoid unnecessary parser lookahead for operators ([#25290](https://github.com/astral-sh/ruff/pull/25290)) - -### Documentation - -- Update code example setting Neovim LSP log level ([#25284](https://github.com/astral-sh/ruff/pull/25284)) - -### Other changes - -- Add full PEP 798 support ([#25104](https://github.com/astral-sh/ruff/pull/25104)) -- Add a parser recursion limit ([#24810](https://github.com/astral-sh/ruff/pull/24810)) -- Update various `ruff_python_stdlib` APIs ([#25273](https://github.com/astral-sh/ruff/pull/25273)) - -### Contributors - -- [@ocaballeror](https://github.com/ocaballeror) -- [@lerebear](https://github.com/lerebear) -- [@samuelcolvin](https://github.com/samuelcolvin) -- [@baltasarblanco](https://github.com/baltasarblanco) -- [@aconal-com](https://github.com/aconal-com) -- [@anishgirianish](https://github.com/anishgirianish) -- [@JelleZijlstra](https://github.com/JelleZijlstra) -- [@AlexWaygood](https://github.com/AlexWaygood) -- [@ntBre](https://github.com/ntBre) -- [@adityasingh2400](https://github.com/adityasingh2400) -- [@charliermarsh](https://github.com/charliermarsh) -- [@Dev-iL](https://github.com/Dev-iL) -- [@neutrinoceros](https://github.com/neutrinoceros) -- [@shivamtiwari3](https://github.com/shivamtiwari3) -- [@Dev-X25874](https://github.com/Dev-X25874) - -## 0.15.13 - -Released on 2026-05-14. - -### Preview features - -- Add a rule to flag lazy imports that are eagerly evaluated ([#25016](https://github.com/astral-sh/ruff/pull/25016)) -- \[`pylint`\] Standardize diagnostic message (`PLR0914`, `PLR0917`) ([#24996](https://github.com/astral-sh/ruff/pull/24996)) - -### Bug fixes - -- Fix `F811` false positive for class methods ([#24933](https://github.com/astral-sh/ruff/pull/24933)) -- Fix setting selection for multi-folder workspace ([#24819](https://github.com/astral-sh/ruff/pull/24819)) -- \[`eradicate`\] Fix false positive for lines with leading whitespace (`ERA001`) ([#25122](https://github.com/astral-sh/ruff/pull/25122)) -- \[`flake8-pyi`\] Fix false positive for f-string debug specifier (`PYI016`) ([#24098](https://github.com/astral-sh/ruff/pull/24098)) - -### Rule changes - -- Always include panic payload in panic diagnostic message ([#24873](https://github.com/astral-sh/ruff/pull/24873)) -- Restrict `PYI034` for in-place operations to enclosing class ([#24511](https://github.com/astral-sh/ruff/pull/24511)) -- Improve error message for parameters that are declared `global` ([#24902](https://github.com/astral-sh/ruff/pull/24902)) -- Update known stdlib ([#25103](https://github.com/astral-sh/ruff/pull/25103)) - -### Performance - -- \[`isort`\] Avoid constructing `glob::Pattern`s for literal known modules ([#25123](https://github.com/astral-sh/ruff/pull/25123)) - -### CLI - -- Add TOML examples to `--config` help text ([#25013](https://github.com/astral-sh/ruff/pull/25013)) -- Colorize ruff check 'All checks passed' ([#25085](https://github.com/astral-sh/ruff/pull/25085)) - -### Configuration - -- Increase max allowed value of `line-length` setting ([#24962](https://github.com/astral-sh/ruff/pull/24962)) - -### Documentation - -- Add `D203` to rules that conflict with the formatter ([#25044](https://github.com/astral-sh/ruff/pull/25044)) -- Clarify `COM819` and formatter interaction ([#25045](https://github.com/astral-sh/ruff/pull/25045)) -- Clarify that `NotImplemented` is a value, not an exception (`F901`) ([#25054](https://github.com/astral-sh/ruff/pull/25054)) -- Update number of lint rules supported ([#24942](https://github.com/astral-sh/ruff/pull/24942)) - -### Other changes - -- Simplify the playground's markdown template ([#24924](https://github.com/astral-sh/ruff/pull/24924)) - -### Contributors - -- [@MichaReiser](https://github.com/MichaReiser) -- [@brian-c11](https://github.com/brian-c11) - [@Andrej730](https://github.com/Andrej730) -- [@denyszhak](https://github.com/denyszhak) -- [@darestack](https://github.com/darestack) -- [@sharkdp](https://github.com/sharkdp) -- [@charliermarsh](https://github.com/charliermarsh) -- [@EkriirkE](https://github.com/EkriirkE) -- [@eyupcanakman](https://github.com/eyupcanakman) -- [@Hrk84ya](https://github.com/Hrk84ya) -- [@thernstig](https://github.com/thernstig) -- [@ntBre](https://github.com/ntBre) - -## 0.15.12 - -Released on 2026-04-24. - -### Preview features - -- Implement `#ruff:file-ignore` file-level suppressions ([#23599](https://github.com/astral-sh/ruff/pull/23599)) -- Implement `#ruff:ignore` logical-line suppressions ([#23404](https://github.com/astral-sh/ruff/pull/23404)) -- Revert preview changes to displayed diagnostic severity in LSP ([#24789](https://github.com/astral-sh/ruff/pull/24789)) -- \[`airflow`\] Implement `task-branch-as-short-circuit` (`AIR004`) ([#23579](https://github.com/astral-sh/ruff/pull/23579)) -- \[`flake8-bugbear`\] Fix `break`/`continue` handling in `loop-iterator-mutation` (`B909`) ([#24440](https://github.com/astral-sh/ruff/pull/24440)) -- \[`pylint`\] Fix `PLC2701` for type parameter scopes ([#24576](https://github.com/astral-sh/ruff/pull/24576)) - -### Rule changes - -- \[`pandas-vet`\] Suggest `.array` as well in `PD011` ([#24805](https://github.com/astral-sh/ruff/pull/24805)) - -### CLI - -- Respect default Unix permissions for cache files ([#24794](https://github.com/astral-sh/ruff/pull/24794)) - -### Documentation - -- \[`pylint`\] Fix `PLR0124` description not to claim self-comparison always returns the same value ([#24749](https://github.com/astral-sh/ruff/pull/24749)) -- \[`pyupgrade`\] Expand docs on reusable `TypeVar`s and scoping (`UP046`) ([#24153](https://github.com/astral-sh/ruff/pull/24153)) -- Improve rules table accessibility ([#24711](https://github.com/astral-sh/ruff/pull/24711)) - -### Contributors - -- [@dylwil3](https://github.com/dylwil3) -- [@AlexWaygood](https://github.com/AlexWaygood) -- [@woodruffw](https://github.com/woodruffw) -- [@avasis-ai](https://github.com/avasis-ai) -- [@Dev-iL](https://github.com/Dev-iL) -- [@denyszhak](https://github.com/denyszhak) -- [@ShipItAndPray](https://github.com/ShipItAndPray) -- [@anishgirianish](https://github.com/anishgirianish) -- [@augustelalande](https://github.com/augustelalande) -- [@amyreese](https://github.com/amyreese) -- [@majiayu000](https://github.com/majiayu000) - -## 0.15.11 - -Released on 2026-04-16. - -### Preview features - -- \[`ruff`\] Ignore `RUF029` when function is decorated with `asynccontextmanager` ([#24642](https://github.com/astral-sh/ruff/pull/24642)) -- \[`airflow`\] Implement `airflow-xcom-pull-in-template-string` (`AIR201`) ([#23583](https://github.com/astral-sh/ruff/pull/23583)) -- \[`flake8-bandit`\] Fix `S103` false positives and negatives in mask analysis ([#24424](https://github.com/astral-sh/ruff/pull/24424)) - -### Bug fixes - -- \[`flake8-async`\] Omit overridden methods for `ASYNC109` ([#24648](https://github.com/astral-sh/ruff/pull/24648)) - -### Documentation - -- \[`flake8-async`\] Add override mention to `ASYNC109` docs ([#24666](https://github.com/astral-sh/ruff/pull/24666)) -- Update Neovim config examples to use `vim.lsp.config` ([#24577](https://github.com/astral-sh/ruff/pull/24577)) - -### Contributors - -- [@augustelalande](https://github.com/augustelalande) -- [@anishgirianish](https://github.com/anishgirianish) -- [@benberryallwood](https://github.com/benberryallwood) -- [@charliermarsh](https://github.com/charliermarsh) -- [@Dev-iL](https://github.com/Dev-iL) - -## 0.15.10 - -Released on 2026-04-09. - -### Preview features - -- \[`flake8-logging`\] Allow closures in except handlers (`LOG004`) ([#24464](https://github.com/astral-sh/ruff/pull/24464)) -- \[`flake8-self`\] Make `SLF` diagnostics robust to non-self-named variables ([#24281](https://github.com/astral-sh/ruff/pull/24281)) -- \[`flake8-simplify`\] Make the fix for `collapsible-if` safe in `preview` (`SIM102`) ([#24371](https://github.com/astral-sh/ruff/pull/24371)) - -### Bug fixes - -- Avoid emitting multi-line f-string elements before Python 3.12 ([#24377](https://github.com/astral-sh/ruff/pull/24377)) -- Avoid syntax error from `E502` fixes in f-strings and t-strings ([#24410](https://github.com/astral-sh/ruff/pull/24410)) -- Strip form feeds from indent passed to `dedent_to` ([#24381](https://github.com/astral-sh/ruff/pull/24381)) -- \[`pyupgrade`\] Fix panic caused by handling of octals (`UP012`) ([#24390](https://github.com/astral-sh/ruff/pull/24390)) -- Reject multi-line f-string elements before Python 3.12 ([#24355](https://github.com/astral-sh/ruff/pull/24355)) - -### Rule changes - -- \[`ruff`\] Treat f-string interpolation as potential side effect (`RUF019`) ([#24426](https://github.com/astral-sh/ruff/pull/24426)) - -### Server - -- Add support for custom file extensions ([#24463](https://github.com/astral-sh/ruff/pull/24463)) - -### Documentation - -- Document adding fixes in CONTRIBUTING.md ([#24393](https://github.com/astral-sh/ruff/pull/24393)) -- Fix JSON typo in settings example ([#24517](https://github.com/astral-sh/ruff/pull/24517)) - -### Contributors - -- [@charliermarsh](https://github.com/charliermarsh) -- [@dylwil3](https://github.com/dylwil3) -- [@silverstein](https://github.com/silverstein) -- [@anishgirianish](https://github.com/anishgirianish) -- [@shizukushq](https://github.com/shizukushq) -- [@zanieb](https://github.com/zanieb) -- [@AlexWaygood](https://github.com/AlexWaygood) - -## 0.15.9 - -Released on 2026-04-02. - -### Preview features - -- \[`pyflakes`\] Flag annotated variable redeclarations as `F811` in preview mode ([#24244](https://github.com/astral-sh/ruff/pull/24244)) -- \[`ruff`\] Allow dunder-named assignments in non-strict mode for `RUF067` ([#24089](https://github.com/astral-sh/ruff/pull/24089)) - -### Bug fixes - -- \[`flake8-errmsg`\] Avoid shadowing existing `msg` in fix for `EM101` ([#24363](https://github.com/astral-sh/ruff/pull/24363)) -- \[`flake8-simplify`\] Ignore pre-initialization references in `SIM113` ([#24235](https://github.com/astral-sh/ruff/pull/24235)) -- \[`pycodestyle`\] Fix `W391` fixes for consecutive empty notebook cells ([#24236](https://github.com/astral-sh/ruff/pull/24236)) -- \[`pyupgrade`\] Fix `UP008` nested class matching ([#24273](https://github.com/astral-sh/ruff/pull/24273)) -- \[`pyupgrade`\] Ignore strings with string-only escapes (`UP012`) ([#16058](https://github.com/astral-sh/ruff/pull/16058)) -- \[`ruff`\] `RUF072`: skip formfeeds on dedent ([#24308](https://github.com/astral-sh/ruff/pull/24308)) -- \[`ruff`\] Avoid re-using symbol in `RUF024` fix ([#24316](https://github.com/astral-sh/ruff/pull/24316)) -- \[`ruff`\] Parenthesize expression in `RUF050` fix ([#24234](https://github.com/astral-sh/ruff/pull/24234)) -- Disallow starred expressions as values of starred expressions ([#24280](https://github.com/astral-sh/ruff/pull/24280)) - -### Rule changes - -- \[`flake8-simplify`\] Suppress `SIM105` for `except*` before Python 3.12 ([#23869](https://github.com/astral-sh/ruff/pull/23869)) -- \[`pyflakes`\] Extend `F507` to flag `%`-format strings with zero placeholders ([#24215](https://github.com/astral-sh/ruff/pull/24215)) -- \[`pyupgrade`\] `UP018` should detect more unnecessarily wrapped literals (UP018) ([#24093](https://github.com/astral-sh/ruff/pull/24093)) -- \[`pyupgrade`\] Fix `UP008` callable scope handling to support lambdas ([#24274](https://github.com/astral-sh/ruff/pull/24274)) -- \[`ruff`\] `RUF010`: Mark fix as unsafe when it deletes a comment ([#24270](https://github.com/astral-sh/ruff/pull/24270)) - -### Formatter - -- Add `nested-string-quote-style` formatting option ([#24312](https://github.com/astral-sh/ruff/pull/24312)) - -### Documentation - -- \[`flake8-bugbear`\] Clarify RUF071 fix safety for non-path string comparisons ([#24149](https://github.com/astral-sh/ruff/pull/24149)) -- \[`flake8-type-checking`\] Clarify import cycle wording for `TC001`/`TC002`/`TC003` ([#24322](https://github.com/astral-sh/ruff/pull/24322)) - -### Other changes - -- Avoid rendering fix lines with trailing whitespace after `|` ([#24343](https://github.com/astral-sh/ruff/pull/24343)) - -### Contributors - -- [@charliermarsh](https://github.com/charliermarsh) -- [@MichaReiser](https://github.com/MichaReiser) -- [@tranhoangtu-it](https://github.com/tranhoangtu-it) -- [@dylwil3](https://github.com/dylwil3) -- [@zsol](https://github.com/zsol) -- [@renovate](https://github.com/renovate) -- [@bitloi](https://github.com/bitloi) -- [@danparizher](https://github.com/danparizher) -- [@chinar-amrutkar](https://github.com/chinar-amrutkar) -- [@second-ed](https://github.com/second-ed) -- [@getehen](https://github.com/getehen) -- [@Redovo1](https://github.com/Redovo1) -- [@matthewlloyd](https://github.com/matthewlloyd) -- [@zanieb](https://github.com/zanieb) -- [@InSyncWithFoo](https://github.com/InSyncWithFoo) -- [@RenzoMXD](https://github.com/RenzoMXD) - -## 0.15.8 - -Released on 2026-03-26. - -### Preview features - -- \[`ruff`\] New rule `unnecessary-if` (`RUF050`) ([#24114](https://github.com/astral-sh/ruff/pull/24114)) -- \[`ruff`\] New rule `useless-finally` (`RUF072`) ([#24165](https://github.com/astral-sh/ruff/pull/24165)) -- \[`ruff`\] New rule `f-string-percent-format` (`RUF073`): warn when using `%` operator on an f-string ([#24162](https://github.com/astral-sh/ruff/pull/24162)) -- \[`pyflakes`\] Recognize `frozendict` as a builtin for Python 3.15+ ([#24100](https://github.com/astral-sh/ruff/pull/24100)) - -### Bug fixes - -- \[`flake8-async`\] Use fully-qualified `anyio.lowlevel` import in autofix (`ASYNC115`) ([#24166](https://github.com/astral-sh/ruff/pull/24166)) -- \[`flake8-bandit`\] Check tuple arguments for partial paths in `S607` ([#24080](https://github.com/astral-sh/ruff/pull/24080)) -- \[`pyflakes`\] Skip `undefined-name` (`F821`) for conditionally deleted variables ([#24088](https://github.com/astral-sh/ruff/pull/24088)) -- `E501`/`W505`/formatter: Exclude nested pragma comments from line width calculation ([#24071](https://github.com/astral-sh/ruff/pull/24071)) -- Fix `%foo?` parsing in IPython assignment expressions ([#24152](https://github.com/astral-sh/ruff/pull/24152)) -- `analyze graph`: resolve string imports that reference attributes, not just modules ([#24058](https://github.com/astral-sh/ruff/pull/24058)) - -### Rule changes - -- \[`eradicate`\] ignore `ty: ignore` comments in `ERA001` ([#24192](https://github.com/astral-sh/ruff/pull/24192)) -- \[`flake8-bandit`\] Treat `sys.executable` as trusted input in `S603` ([#24106](https://github.com/astral-sh/ruff/pull/24106)) -- \[`flake8-self`\] Recognize `Self` annotation and `self` assignment in `SLF001` ([#24144](https://github.com/astral-sh/ruff/pull/24144)) -- \[`pyflakes`\] `F507`: Fix false negative for non-tuple RHS in `%`-formatting ([#24142](https://github.com/astral-sh/ruff/pull/24142)) -- \[`refurb`\] Parenthesize generator arguments in `FURB142` fixer ([#24200](https://github.com/astral-sh/ruff/pull/24200)) - -### Performance - -- Speed up diagnostic rendering ([#24146](https://github.com/astral-sh/ruff/pull/24146)) - -### Server - -- Warn when Markdown files are skipped due to preview being disabled ([#24150](https://github.com/astral-sh/ruff/pull/24150)) - -### Documentation - -- Clarify `extend-ignore` and `extend-select` settings documentation ([#24064](https://github.com/astral-sh/ruff/pull/24064)) -- Mention AI policy in PR template ([#24198](https://github.com/astral-sh/ruff/pull/24198)) - -### Other changes - -- Use trusted publishing for NPM packages ([#24171](https://github.com/astral-sh/ruff/pull/24171)) - -### Contributors - -- [@bitloi](https://github.com/bitloi) -- [@Sim-hu](https://github.com/Sim-hu) -- [@mvanhorn](https://github.com/mvanhorn) -- [@chinar-amrutkar](https://github.com/chinar-amrutkar) -- [@markjm](https://github.com/markjm) -- [@RenzoMXD](https://github.com/RenzoMXD) -- [@vivekkhimani](https://github.com/vivekkhimani) -- [@seroperson](https://github.com/seroperson) -- [@moktamd](https://github.com/moktamd) -- [@charliermarsh](https://github.com/charliermarsh) -- [@ntBre](https://github.com/ntBre) -- [@zanieb](https://github.com/zanieb) -- [@dylwil3](https://github.com/dylwil3) -- [@MichaReiser](https://github.com/MichaReiser) - -## 0.15.7 - -Released on 2026-03-19. - -### Preview features - -- Display output severity in preview ([#23845](https://github.com/astral-sh/ruff/pull/23845)) -- Don't show `noqa` hover for non-Python documents ([#24040](https://github.com/astral-sh/ruff/pull/24040)) - -### Rule changes - -- \[`pycodestyle`\] Recognize `pyrefly:` as a pragma comment (`E501`) ([#24019](https://github.com/astral-sh/ruff/pull/24019)) - -### Server - -- Don't return code actions for non-Python documents ([#23905](https://github.com/astral-sh/ruff/pull/23905)) - -### Documentation - -- Add company AI policy to contributing guide ([#24021](https://github.com/astral-sh/ruff/pull/24021)) -- Document editor features for Markdown code formatting ([#23924](https://github.com/astral-sh/ruff/pull/23924)) -- \[`pylint`\] Improve phrasing (`PLC0208`) ([#24033](https://github.com/astral-sh/ruff/pull/24033)) - -### Other changes - -- Use PEP 639 license information ([#19661](https://github.com/astral-sh/ruff/pull/19661)) - -### Contributors - -- [@tmimmanuel](https://github.com/tmimmanuel) -- [@DimitriPapadopoulos](https://github.com/DimitriPapadopoulos) -- [@amyreese](https://github.com/amyreese) -- [@statxc](https://github.com/statxc) -- [@dylwil3](https://github.com/dylwil3) -- [@hunterhogan](https://github.com/hunterhogan) -- [@renovate](https://github.com/renovate) - -## 0.15.6 - -Released on 2026-03-12. - -### Preview features - -- Add support for `lazy` import parsing ([#23755](https://github.com/astral-sh/ruff/pull/23755)) -- Add support for star-unpacking of comprehensions (PEP 798) ([#23788](https://github.com/astral-sh/ruff/pull/23788)) -- Reject semantic syntax errors for lazy imports ([#23757](https://github.com/astral-sh/ruff/pull/23757)) -- Drop a few rules from the preview default set ([#23879](https://github.com/astral-sh/ruff/pull/23879)) -- \[`airflow`\] Flag `Variable.get()` calls outside of task execution context (`AIR003`) ([#23584](https://github.com/astral-sh/ruff/pull/23584)) -- \[`airflow`\] Flag runtime-varying values in DAG/task constructor arguments (`AIR304`) ([#23631](https://github.com/astral-sh/ruff/pull/23631)) -- \[`flake8-bugbear`\] Implement `delattr-with-constant` (`B043`) ([#23737](https://github.com/astral-sh/ruff/pull/23737)) -- \[`flake8-tidy-imports`\] Add `TID254` to enforce lazy imports ([#23777](https://github.com/astral-sh/ruff/pull/23777)) -- \[`flake8-tidy-imports`\] Allow users to ban lazy imports with `TID254` ([#23847](https://github.com/astral-sh/ruff/pull/23847)) -- \[`isort`\] Retain `lazy` keyword when sorting imports ([#23762](https://github.com/astral-sh/ruff/pull/23762)) -- \[`pyupgrade`\] Add `from __future__ import annotations` automatically (`UP006`) ([#23260](https://github.com/astral-sh/ruff/pull/23260)) -- \[`refurb`\] Support `newline` parameter in `FURB101` for Python 3.13+ ([#23754](https://github.com/astral-sh/ruff/pull/23754)) -- \[`ruff`\] Add `os-path-commonprefix` (`RUF071`) ([#23814](https://github.com/astral-sh/ruff/pull/23814)) -- \[`ruff`\] Add unsafe fix for os-path-commonprefix (`RUF071`) ([#23852](https://github.com/astral-sh/ruff/pull/23852)) -- \[`ruff`\] Limit `RUF036` to typing contexts; make it unsafe for non-typing-only ([#23765](https://github.com/astral-sh/ruff/pull/23765)) -- \[`ruff`\] Use starred unpacking for `RUF017` in Python 3.15+ ([#23789](https://github.com/astral-sh/ruff/pull/23789)) - -### Bug fixes - -- Fix `--add-noqa` creating unwanted leading whitespace ([#23773](https://github.com/astral-sh/ruff/pull/23773)) -- Fix `--add-noqa` breaking shebangs ([#23577](https://github.com/astral-sh/ruff/pull/23577)) -- [formatter] Fix lambda body formatting for multiline calls and subscripts ([#23866](https://github.com/astral-sh/ruff/pull/23866)) -- [formatter] Preserve required annotation parentheses in annotated assignments ([#23865](https://github.com/astral-sh/ruff/pull/23865)) -- [formatter] Preserve type-expression parentheses in the formatter ([#23867](https://github.com/astral-sh/ruff/pull/23867)) -- \[`flake8-annotations`\] Fix stack overflow in `ANN401` on quoted annotations with escape sequences ([#23912](https://github.com/astral-sh/ruff/pull/23912)) -- \[`pep8-naming`\] Check naming conventions in `match` pattern bindings (`N806`, `N815`, `N816`) ([#23899](https://github.com/astral-sh/ruff/pull/23899)) -- \[`perflint`\] Fix comment duplication in fixes (`PERF401`, `PERF403`) ([#23729](https://github.com/astral-sh/ruff/pull/23729)) -- \[`pyupgrade`\] Properly trigger `super` change in nested class (`UP008`) ([#22677](https://github.com/astral-sh/ruff/pull/22677)) -- \[`ruff`\] Avoid syntax errors in `RUF036` fixes ([#23764](https://github.com/astral-sh/ruff/pull/23764)) - -### Rule changes - -- \[`flake8-bandit`\] Flag `S501` with `requests.request` ([#23873](https://github.com/astral-sh/ruff/pull/23873)) -- \[`flake8-executable`\] Fix WSL detection in non-Docker containers ([#22879](https://github.com/astral-sh/ruff/pull/22879)) -- \[`flake8-print`\] Ignore `pprint` calls with `stream=` ([#23787](https://github.com/astral-sh/ruff/pull/23787)) - -### Documentation - -- Update docs for Markdown code block formatting ([#23871](https://github.com/astral-sh/ruff/pull/23871)) -- \[`flake8-bugbear`\] Fix misleading description for `B904` ([#23731](https://github.com/astral-sh/ruff/pull/23731)) - -### Contributors - -- [@zsol](https://github.com/zsol) -- [@carljm](https://github.com/carljm) -- [@ntBre](https://github.com/ntBre) -- [@Bortlesboat](https://github.com/Bortlesboat) -- [@sososonia-cyber](https://github.com/sososonia-cyber) -- [@chirizxc](https://github.com/chirizxc) -- [@leandrobbraga](https://github.com/leandrobbraga) -- [@11happy](https://github.com/11happy) -- [@Acelogic](https://github.com/Acelogic) -- [@anishgirianish](https://github.com/anishgirianish) -- [@amyreese](https://github.com/amyreese) -- [@xvchris](https://github.com/xvchris) -- [@charliermarsh](https://github.com/charliermarsh) -- [@getehen](https://github.com/getehen) -- [@Dev-iL](https://github.com/Dev-iL) - -## 0.15.5 - -Released on 2026-03-05. - -### Preview features - -- Discover Markdown files by default in preview mode ([#23434](https://github.com/astral-sh/ruff/pull/23434)) -- \[`perflint`\] Extend `PERF102` to comprehensions and generators ([#23473](https://github.com/astral-sh/ruff/pull/23473)) -- \[`refurb`\] Fix `FURB101` and `FURB103` false positives when I/O variable is used later ([#23542](https://github.com/astral-sh/ruff/pull/23542)) -- \[`ruff`\] Add fix for `none-not-at-end-of-union` (`RUF036`) ([#22829](https://github.com/astral-sh/ruff/pull/22829)) -- \[`ruff`\] Fix false positive for `re.split` with empty string pattern (`RUF055`) ([#23634](https://github.com/astral-sh/ruff/pull/23634)) - -### Bug fixes - -- \[`fastapi`\] Handle callable class dependencies with `__call__` method (`FAST003`) ([#23553](https://github.com/astral-sh/ruff/pull/23553)) -- \[`pydocstyle`\] Fix numpy section ordering (`D420`) ([#23685](https://github.com/astral-sh/ruff/pull/23685)) -- \[`pyflakes`\] Fix false positive for names shadowing re-exports (`F811`) ([#23356](https://github.com/astral-sh/ruff/pull/23356)) -- \[`pyupgrade`\] Avoid inserting redundant `None` elements in `UP045` ([#23459](https://github.com/astral-sh/ruff/pull/23459)) - -### Documentation - -- Document extension mapping for Markdown code formatting ([#23574](https://github.com/astral-sh/ruff/pull/23574)) -- Update default Python version examples ([#23605](https://github.com/astral-sh/ruff/pull/23605)) - -### Other changes - -- Publish releases to Astral mirror ([#23616](https://github.com/astral-sh/ruff/pull/23616)) - -### Contributors - -- [@amyreese](https://github.com/amyreese) -- [@stakeswky](https://github.com/stakeswky) -- [@chirizxc](https://github.com/chirizxc) -- [@anishgirianish](https://github.com/anishgirianish) -- [@bxff](https://github.com/bxff) -- [@zsol](https://github.com/zsol) -- [@charliermarsh](https://github.com/charliermarsh) - [@ntBre](https://github.com/ntBre) -- [@kar-ganap](https://github.com/kar-ganap) - -## 0.15.4 - -Released on 2026-02-26. - -This is a follow-up release to 0.15.3 that resolves a panic when the new rule `PLR1712` was enabled with any rule that analyzes definitions, such as many of the `ANN` or `D` rules. - -### Bug fixes - -- Fix panic on access to definitions after analyzing definitions ([#23588](https://github.com/astral-sh/ruff/pull/23588)) -- \[`pyflakes`\] Suppress false positive in `F821` for names used before `del` in stub files ([#23550](https://github.com/astral-sh/ruff/pull/23550)) - -### Documentation - -- Clarify first-party import detection in Ruff ([#23591](https://github.com/astral-sh/ruff/pull/23591)) -- Fix incorrect `import-heading` example ([#23568](https://github.com/astral-sh/ruff/pull/23568)) - -### Contributors - -- [@stakeswky](https://github.com/stakeswky) -- [@ntBre](https://github.com/ntBre) -- [@thejcannon](https://github.com/thejcannon) -- [@GeObts](https://github.com/GeObts) - -## 0.15.3 - -Released on 2026-02-26. - -### Preview features - -- Drop explicit support for `.qmd` file extension ([#23572](https://github.com/astral-sh/ruff/pull/23572)) - - This can now be enabled instead by setting the [`extension`](https://docs.astral.sh/ruff/settings/#extension) option: - - ```toml - # ruff.toml - extension = { qmd = "markdown" } - - # pyproject.toml - [tool.ruff] - extension = { qmd = "markdown" } - ``` - -- Include configured extensions in file discovery ([#23400](https://github.com/astral-sh/ruff/pull/23400)) - -- \[`flake8-bandit`\] Allow suspicious imports in `TYPE_CHECKING` blocks (`S401`-`S415`) ([#23441](https://github.com/astral-sh/ruff/pull/23441)) - -- \[`flake8-bugbear`\] Allow `B901` in pytest hook wrappers ([#21931](https://github.com/astral-sh/ruff/pull/21931)) - -- \[`flake8-import-conventions`\] Add missing conventions from upstream (`ICN001`, `ICN002`) ([#21373](https://github.com/astral-sh/ruff/pull/21373)) - -- \[`pydocstyle`\] Add rule to enforce docstring section ordering (`D420`) ([#23537](https://github.com/astral-sh/ruff/pull/23537)) - -- \[`pylint`\] Implement `swap-with-temporary-variable` (`PLR1712`) ([#22205](https://github.com/astral-sh/ruff/pull/22205)) - -- \[`ruff`\] Add `unnecessary-assign-before-yield` (`RUF070`) ([#23300](https://github.com/astral-sh/ruff/pull/23300)) - -- \[`ruff`\] Support file-level noqa in `RUF102` ([#23535](https://github.com/astral-sh/ruff/pull/23535)) - -- \[`ruff`\] Suppress diagnostic for invalid f-strings before Python 3.12 (`RUF027`) ([#23480](https://github.com/astral-sh/ruff/pull/23480)) - -- \[`flake8-bandit`\] Don't flag `BaseLoader`/`CBaseLoader` as unsafe (`S506`) ([#23510](https://github.com/astral-sh/ruff/pull/23510)) - -### Bug fixes - -- Avoid infinite loop between `I002` and `PYI025` ([#23352](https://github.com/astral-sh/ruff/pull/23352)) -- \[`pyflakes`\] Fix false positive for `@overload` from `lint.typing-modules` (`F811`) ([#23357](https://github.com/astral-sh/ruff/pull/23357)) -- \[`pyupgrade`\] Fix false positive for `TypeVar` default before Python 3.12 (`UP046`) ([#23540](https://github.com/astral-sh/ruff/pull/23540)) -- \[`pyupgrade`\] Fix handling of `\N` in raw strings (`UP032`) ([#22149](https://github.com/astral-sh/ruff/pull/22149)) - -### Rule changes - -- Render sub-diagnostics in the GitHub output format ([#23455](https://github.com/astral-sh/ruff/pull/23455)) - -- \[`flake8-bugbear`\] Tag certain `B007` diagnostics as unnecessary ([#23453](https://github.com/astral-sh/ruff/pull/23453)) - -- \[`ruff`\] Ignore unknown rule codes in `RUF100` ([#23531](https://github.com/astral-sh/ruff/pull/23531)) - - These are now flagged by [`RUF102`](https://docs.astral.sh/ruff/rules/invalid-rule-code/) instead. - -### Documentation - -- Fix missing settings links for several linters ([#23519](https://github.com/astral-sh/ruff/pull/23519)) -- Update isort action comments heading ([#23515](https://github.com/astral-sh/ruff/pull/23515)) -- \[`pydocstyle`\] Fix double comma in description of `D404` ([#23440](https://github.com/astral-sh/ruff/pull/23440)) - -### Other changes - -- Update the Python module (notably `find_ruff_bin`) for parity with uv ([#23406](https://github.com/astral-sh/ruff/pull/23406)) - -### Contributors - -- [@zanieb](https://github.com/zanieb) -- [@o1x3](https://github.com/o1x3) -- [@assadyousuf](https://github.com/assadyousuf) -- [@kar-ganap](https://github.com/kar-ganap) -- [@denyszhak](https://github.com/denyszhak) -- [@amyreese](https://github.com/amyreese) -- [@carljm](https://github.com/carljm) -- [@anishgirianish](https://github.com/anishgirianish) -- [@Bnyro](https://github.com/Bnyro) -- [@danparizher](https://github.com/danparizher) -- [@ntBre](https://github.com/ntBre) -- [@gcomneno](https://github.com/gcomneno) -- [@jaap3](https://github.com/jaap3) -- [@stakeswky](https://github.com/stakeswky) - -## 0.15.2 - -Released on 2026-02-19. - -### Preview features - -- Expand the default rule set ([#23385](https://github.com/astral-sh/ruff/pull/23385)) - - In preview, Ruff now enables a significantly expanded default rule set of 412 - rules, up from the stable default set of 59 rules. The new rules are mostly a - superset of the stable defaults, with the exception of these rules, which are - removed from the preview defaults: - - - [`multiple-imports-on-one-line`](https://docs.astral.sh/ruff/rules/multiple-imports-on-one-line) (`E401`) - - [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file) (`E402`) - - [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file) (`E701`) - - [`multiple-statements-on-one-line-semicolon`](https://docs.astral.sh/ruff/rules/multiple-statements-on-one-line-semicolon) (`E702`) - - [`useless-semicolon`](https://docs.astral.sh/ruff/rules/useless-semicolon) (`E703`) - - [`none-comparison`](https://docs.astral.sh/ruff/rules/none-comparison) (`E711`) - - [`true-false-comparison`](https://docs.astral.sh/ruff/rules/true-false-comparison) (`E712`) - - [`not-in-test`](https://docs.astral.sh/ruff/rules/not-in-test) (`E713`) - - [`not-is-test`](https://docs.astral.sh/ruff/rules/not-is-test) (`E714`) - - [`type-comparison`](https://docs.astral.sh/ruff/rules/type-comparison) (`E721`) - - [`lambda-assignment`](https://docs.astral.sh/ruff/rules/lambda-assignment) (`E731`) - - [`ambiguous-variable-name`](https://docs.astral.sh/ruff/rules/ambiguous-variable-name) (`E741`) - - [`ambiguous-class-name`](https://docs.astral.sh/ruff/rules/ambiguous-class-name) (`E742`) - - [`ambiguous-function-name`](https://docs.astral.sh/ruff/rules/ambiguous-function-name) (`E743`) - - [`undefined-local-with-import-star`](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star) (`F403`) - - [`undefined-local-with-import-star-usage`](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star-usage) (`F405`) - - [`undefined-local-with-nested-import-star-usage`](https://docs.astral.sh/ruff/rules/undefined-local-with-nested-import-star-usage) (`F406`) - - [`forward-annotation-syntax-error`](https://docs.astral.sh/ruff/rules/forward-annotation-syntax-error) (`F722`) - - If you use preview and prefer the old defaults, you can restore them with - configuration like: - - ```toml - - # ruff.toml - - [lint] - select = ["E4", "E7", "E9", "F"] - - # pyproject.toml - - [tool.ruff.lint] - select = ["E4", "E7", "E9", "F"] - ``` - - If you do give them a try, feel free to share your feedback in the [GitHub - discussion](https://github.com/astral-sh/ruff/discussions/23203)! - -- \[`flake8-pyi`\] Also check string annotations (`PYI041`) ([#19023](https://github.com/astral-sh/ruff/pull/19023)) - -### Bug fixes - -- \[`flake8-async`\] Fix `in_async_context` logic ([#23426](https://github.com/astral-sh/ruff/pull/23426)) -- \[`ruff`\] Fix for `RUF102` should delete entire comment ([#23380](https://github.com/astral-sh/ruff/pull/23380)) -- \[`ruff`\] Suppress diagnostic for strings with backslashes in interpolations before Python 3.12 (`RUF027`) ([#21069](https://github.com/astral-sh/ruff/pull/21069)) -- \[`flake8-bugbear`\] Fix `B023` false positive for immediately-invoked lambdas ([#23294](https://github.com/astral-sh/ruff/pull/23294)) -- [parser] Fix false syntax error for match-like annotated assignments ([#23297](https://github.com/astral-sh/ruff/pull/23297)) -- [parser] Fix indentation tracking after line continuations ([#23417](https://github.com/astral-sh/ruff/pull/23417)) - -### Rule changes - -- \[`flake8-executable`\] Allow global flags in uv shebangs (`EXE003`) ([#22582](https://github.com/astral-sh/ruff/pull/22582)) -- \[`pyupgrade`\] Fix handling of `typing.{io,re}` (`UP035`) ([#23131](https://github.com/astral-sh/ruff/pull/23131)) -- \[`ruff`\] Detect `PLC0207` on chained `str.split()` calls ([#23275](https://github.com/astral-sh/ruff/pull/23275)) - -### CLI - -- Remove invalid inline `noqa` warning ([#23270](https://github.com/astral-sh/ruff/pull/23270)) - -### Configuration - -- Add extension mapping to configuration file options ([#23384](https://github.com/astral-sh/ruff/pull/23384)) - -### Documentation - -- Add `Q004` to the list of conflicting rules ([#23340](https://github.com/astral-sh/ruff/pull/23340)) -- \[`ruff`\] Expand `lint.external` docs and add sub-diagnostic (`RUF100`, `RUF102`) ([#23268](https://github.com/astral-sh/ruff/pull/23268)) - -### Contributors - -- [@dylwil3](https://github.com/dylwil3) -- [@Jkhall81](https://github.com/Jkhall81) -- [@danparizher](https://github.com/danparizher) -- [@dhruvmanila](https://github.com/dhruvmanila) -- [@harupy](https://github.com/harupy) -- [@ngnpope](https://github.com/ngnpope) -- [@amyreese](https://github.com/amyreese) -- [@kar-ganap](https://github.com/kar-ganap) -- [@robsdedude](https://github.com/robsdedude) -- [@shaanmajid](https://github.com/shaanmajid) -- [@ntBre](https://github.com/ntBre) -- [@toslunar](https://github.com/toslunar) - -## 0.15.1 - -Released on 2026-02-12. - -### Preview features - -- \[`airflow`\] Add ruff rules to catch deprecated Airflow imports for Airflow 3.1 (`AIR321`) ([#22376](https://github.com/astral-sh/ruff/pull/22376)) -- \[`airflow`\] Third positional parameter not named `ti_key` should be flagged for `BaseOperatorLink.get_link` (`AIR303`) ([#22828](https://github.com/astral-sh/ruff/pull/22828)) -- \[`flake8-gettext`\] Fix false negatives for plural argument of `ngettext` (`INT001`, `INT002`, `INT003`) ([#21078](https://github.com/astral-sh/ruff/pull/21078)) -- \[`pyflakes`\] Fix infinite loop in preview fix for `unused-import` (`F401`) ([#23038](https://github.com/astral-sh/ruff/pull/23038)) -- \[`pygrep-hooks`\] Detect non-existent mock methods in standalone expressions (`PGH005`) ([#22830](https://github.com/astral-sh/ruff/pull/22830)) -- \[`pylint`\] Allow dunder submodules and improve diagnostic range (`PLC2701`) ([#22804](https://github.com/astral-sh/ruff/pull/22804)) -- \[`pyupgrade`\] Improve diagnostic range for tuples (`UP024`) ([#23013](https://github.com/astral-sh/ruff/pull/23013)) -- \[`refurb`\] Check subscripts in tuple do not use lambda parameters in `reimplemented-operator` (`FURB118`) ([#23079](https://github.com/astral-sh/ruff/pull/23079)) -- \[`ruff`\] Detect mutable defaults in `field` calls (`RUF008`) ([#23046](https://github.com/astral-sh/ruff/pull/23046)) -- \[`ruff`\] Ignore std `cmath.inf` (`RUF069`) ([#23120](https://github.com/astral-sh/ruff/pull/23120)) -- \[`ruff`\] New rule `float-equality-comparison` (`RUF069`) ([#20585](https://github.com/astral-sh/ruff/pull/20585)) -- Don't format unlabeled Markdown code blocks ([#23106](https://github.com/astral-sh/ruff/pull/23106)) -- Markdown formatting support in LSP ([#23063](https://github.com/astral-sh/ruff/pull/23063)) -- Support Quarto Markdown language markers ([#22947](https://github.com/astral-sh/ruff/pull/22947)) -- Support formatting `pycon` Markdown code blocks ([#23112](https://github.com/astral-sh/ruff/pull/23112)) -- Use extension mapping to select Markdown code block language ([#22934](https://github.com/astral-sh/ruff/pull/22934)) - -### Bug fixes - -- Avoid false positive for undefined variables in `FAST001` ([#23224](https://github.com/astral-sh/ruff/pull/23224)) -- Avoid introducing syntax errors for `FAST003` autofix ([#23227](https://github.com/astral-sh/ruff/pull/23227)) -- Avoid suggesting `InitVar` for `__post_init__` that references PEP 695 type parameters ([#23226](https://github.com/astral-sh/ruff/pull/23226)) -- Deduplicate type variables in generic functions ([#23225](https://github.com/astral-sh/ruff/pull/23225)) -- Fix exception handler parenthesis removal for Python 3.14+ ([#23126](https://github.com/astral-sh/ruff/pull/23126)) -- Fix f-string middle panic when parsing t-strings ([#23232](https://github.com/astral-sh/ruff/pull/23232)) -- Wrap `RUF020` target for multiline fixes ([#23210](https://github.com/astral-sh/ruff/pull/23210)) -- Wrap `UP007` target for multiline fixes ([#23208](https://github.com/astral-sh/ruff/pull/23208)) -- Fix missing diagnostics for last range suppression in file ([#23242](https://github.com/astral-sh/ruff/pull/23242)) -- \[`pyupgrade`\] Fix syntax error on string with newline escape and comment (`UP037`) ([#22968](https://github.com/astral-sh/ruff/pull/22968)) - -### Rule changes - -- Use `ruff` instead of `Ruff` as the program name in GitHub output format ([#23240](https://github.com/astral-sh/ruff/pull/23240)) -- \[`PT006`\] Fix syntax error when unpacking nested tuples in `parametrize` fixes (#22441) ([#22464](https://github.com/astral-sh/ruff/pull/22464)) -- \[`airflow`\] Catch deprecated attribute access from context key for Airflow 3.0 (`AIR301`) ([#22850](https://github.com/astral-sh/ruff/pull/22850)) -- \[`airflow`\] Capture deprecated arguments and a decorator (`AIR301`) ([#23170](https://github.com/astral-sh/ruff/pull/23170)) -- \[`flake8-boolean-trap`\] Add `multiprocessing.Value` to excluded functions for `FBT003` ([#23010](https://github.com/astral-sh/ruff/pull/23010)) -- \[`flake8-bugbear`\] Add a secondary annotation showing the previous occurrence (`B033`) ([#22634](https://github.com/astral-sh/ruff/pull/22634)) -- \[`flake8-type-checking`\] Add sub-diagnostic showing the runtime use of an annotation (`TC004`) ([#23091](https://github.com/astral-sh/ruff/pull/23091)) -- \[`isort`\] Support configurable import section heading comments ([#23151](https://github.com/astral-sh/ruff/pull/23151)) -- \[`ruff`\] Improve the diagnostic for `RUF012` ([#23202](https://github.com/astral-sh/ruff/pull/23202)) - -### Formatter - -- Suppress diagnostic output for `format --check --silent` ([#17736](https://github.com/astral-sh/ruff/pull/17736)) - -### Documentation - -- Add tabbed shell completion documentation ([#23169](https://github.com/astral-sh/ruff/pull/23169)) -- Explain how to enable Markdown formatting for pre-commit hook ([#23077](https://github.com/astral-sh/ruff/pull/23077)) -- Fixed import in `runtime-evaluated-decorators` example ([#23187](https://github.com/astral-sh/ruff/pull/23187)) -- Update ruff server contributing guide ([#23060](https://github.com/astral-sh/ruff/pull/23060)) - -### Other changes - -- Exclude WASM artifacts from GitHub releases ([#23221](https://github.com/astral-sh/ruff/pull/23221)) - -### Contributors - -- [@mkniewallner](https://github.com/mkniewallner) -- [@bxff](https://github.com/bxff) -- [@dylwil3](https://github.com/dylwil3) -- [@Avasam](https://github.com/Avasam) -- [@amyreese](https://github.com/amyreese) -- [@charliermarsh](https://github.com/charliermarsh) -- [@Alex-ley-scrub](https://github.com/Alex-ley-scrub) -- [@Kalmaegi](https://github.com/Kalmaegi) -- [@danparizher](https://github.com/danparizher) -- [@AiyionPrime](https://github.com/AiyionPrime) -- [@eureka928](https://github.com/eureka928) -- [@11happy](https://github.com/11happy) -- [@Jkhall81](https://github.com/Jkhall81) -- [@chirizxc](https://github.com/chirizxc) -- [@leandrobbraga](https://github.com/leandrobbraga) -- [@tvatter](https://github.com/tvatter) -- [@anishgirianish](https://github.com/anishgirianish) -- [@shaanmajid](https://github.com/shaanmajid) -- [@ntBre](https://github.com/ntBre) -- [@sjyangkevin](https://github.com/sjyangkevin) - -## 0.15.0 - -Released on 2026-02-03. - -Check out the [blog post](https://astral.sh/blog/ruff-v0.15.0) for a migration -guide and overview of the changes! - -### Breaking changes - -- Ruff now formats your code according to the 2026 style guide. See the formatter section below or in the blog post for a detailed list of changes. - -- The linter now supports block suppression comments. For example, to suppress `N803` for all parameters in this function: - - ```python - # ruff: disable[N803] - def foo( - legacyArg1, - legacyArg2, - legacyArg3, - legacyArg4, - ): ... - # ruff: enable[N803] - ``` - - See the [documentation](https://docs.astral.sh/ruff/linter/#block-level) for more details. - -- The `ruff:alpine` Docker image is now based on Alpine 3.23 (up from 3.21). - -- The `ruff:debian` and `ruff:debian-slim` Docker images are now based on Debian 13 "Trixie" instead of Debian 12 "Bookworm." - -- Binaries for the `ppc64` (64-bit big-endian PowerPC) architecture are no longer included in our releases. It should still be possible to build Ruff manually for this platform, if needed. - -- Ruff now resolves all `extend`ed configuration files before falling back on a default Python version. - -### Stabilization - -The following rules have been stabilized and are no longer in preview: - -- [`blocking-http-call-httpx-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-http-call-httpx-in-async-function) - (`ASYNC212`) -- [`blocking-path-method-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-path-method-in-async-function) - (`ASYNC240`) -- [`blocking-input-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-input-in-async-function) - (`ASYNC250`) -- [`map-without-explicit-strict`](https://docs.astral.sh/ruff/rules/map-without-explicit-strict) - (`B912`) -- [`if-exp-instead-of-or-operator`](https://docs.astral.sh/ruff/rules/if-exp-instead-of-or-operator) - (`FURB110`) -- [`single-item-membership-test`](https://docs.astral.sh/ruff/rules/single-item-membership-test) - (`FURB171`) -- [`missing-maxsplit-arg`](https://docs.astral.sh/ruff/rules/missing-maxsplit-arg) (`PLC0207`) -- [`unnecessary-lambda`](https://docs.astral.sh/ruff/rules/unnecessary-lambda) (`PLW0108`) -- [`unnecessary-empty-iterable-within-deque-call`](https://docs.astral.sh/ruff/rules/unnecessary-empty-iterable-within-deque-call) - (`RUF037`) -- [`in-empty-collection`](https://docs.astral.sh/ruff/rules/in-empty-collection) (`RUF060`) -- [`legacy-form-pytest-raises`](https://docs.astral.sh/ruff/rules/legacy-form-pytest-raises) - (`RUF061`) -- [`non-octal-permissions`](https://docs.astral.sh/ruff/rules/non-octal-permissions) (`RUF064`) -- [`invalid-rule-code`](https://docs.astral.sh/ruff/rules/invalid-rule-code) (`RUF102`) -- [`invalid-suppression-comment`](https://docs.astral.sh/ruff/rules/invalid-suppression-comment) - (`RUF103`) -- [`unmatched-suppression-comment`](https://docs.astral.sh/ruff/rules/unmatched-suppression-comment) - (`RUF104`) -- [`replace-str-enum`](https://docs.astral.sh/ruff/rules/replace-str-enum) (`UP042`) - -The following behaviors have been stabilized: - -- The `--output-format` flag is now respected when running Ruff in `--watch` mode, and the `full` output format is now used by default, matching the regular CLI output. -- [`builtin-attribute-shadowing`](https://docs.astral.sh/ruff/rules/builtin-attribute-shadowing/) (`A003`) now detects the use of shadowed built-in names in additional contexts like decorators, default arguments, and other attribute definitions. -- [`duplicate-union-member`](https://docs.astral.sh/ruff/rules/duplicate-union-member/) (`PYI016`) now considers `typing.Optional` when searching for duplicate union members. -- [`split-static-string`](https://docs.astral.sh/ruff/rules/split-static-string/) (`SIM905`) now offers an autofix when the `maxsplit` argument is provided, even without a `sep` argument. -- [`dict-get-with-none-default`](https://docs.astral.sh/ruff/rules/dict-get-with-none-default/) (`SIM910`) now applies to more types of key expressions. -- [`super-call-with-parameters`](https://docs.astral.sh/ruff/rules/super-call-with-parameters/) (`UP008`) now has a safe fix when it will not delete comments. -- [`unnecessary-default-type-args`](https://docs.astral.sh/ruff/rules/unnecessary-default-type-args/) (`UP043`) now applies to stub (`.pyi`) files on Python versions before 3.13. - -### Formatter - -This release introduces the new 2026 style guide, with the following changes: - -- Lambda parameters are now kept on the same line and lambda bodies will be parenthesized to let - them break across multiple lines ([#21385](https://github.com/astral-sh/ruff/pull/21385)) -- Parentheses around tuples of exceptions in `except` clauses will now be removed on Python 3.14 and - later ([#20768](https://github.com/astral-sh/ruff/pull/20768)) -- A single empty line is now permitted at the beginning of function bodies ([#21110](https://github.com/astral-sh/ruff/pull/21110)) -- Parentheses are avoided for long `as` captures in `match` statements ([#21176](https://github.com/astral-sh/ruff/pull/21176)) -- Extra spaces between escaped quotes and ending triple quotes can now be omitted ([#17216](https://github.com/astral-sh/ruff/pull/17216)) -- Blank lines are now enforced before classes with decorators in stub files ([#18888](https://github.com/astral-sh/ruff/pull/18888)) - -### Preview features - -- Apply formatting to Markdown code blocks ([#22470](https://github.com/astral-sh/ruff/pull/22470), [#22990](https://github.com/astral-sh/ruff/pull/22990), [#22996](https://github.com/astral-sh/ruff/pull/22996)) - - See the [documentation](https://docs.astral.sh/ruff/formatter/#markdown-code-formatting) for more details. - -### Bug fixes - -- Fix suppression indentation matching ([#22903](https://github.com/astral-sh/ruff/pull/22903)) - -### Rule changes - -- Customize where the `fix_title` sub-diagnostic appears ([#23044](https://github.com/astral-sh/ruff/pull/23044)) -- \[`FastAPI`\] Add sub-diagnostic explaining why a fix was unavailable (`FAST002`) ([#22565](https://github.com/astral-sh/ruff/pull/22565)) -- \[`flake8-annotations`\] Don't suggest `NoReturn` for functions raising `NotImplementedError` (`ANN201`, `ANN202`, `ANN205`, `ANN206`) ([#21311](https://github.com/astral-sh/ruff/pull/21311)) -- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP017`) ([#22873](https://github.com/astral-sh/ruff/pull/22873)) -- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP020`) ([#22872](https://github.com/astral-sh/ruff/pull/22872)) -- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP033`) ([#22871](https://github.com/astral-sh/ruff/pull/22871)) -- \[`refurb`\] Do not add `abc.ABC` if already present (`FURB180`) ([#22234](https://github.com/astral-sh/ruff/pull/22234)) -- \[`refurb`\] Make fix unsafe if it deletes comments (`FURB110`) ([#22768](https://github.com/astral-sh/ruff/pull/22768)) -- \[`ruff`\] Add sub-diagnostics with permissions (`RUF064`) ([#22972](https://github.com/astral-sh/ruff/pull/22972)) - -### Server - -- Identify notebooks by LSP `didOpen` instead of `.ipynb` file extension ([#22810](https://github.com/astral-sh/ruff/pull/22810)) - -### CLI - -- Add `--color` CLI option to force colored output ([#22806](https://github.com/astral-sh/ruff/pull/22806)) - -### Documentation - -- Document `-` stdin convention in CLI help text ([#22817](https://github.com/astral-sh/ruff/pull/22817)) -- \[`refurb`\] Change example to `re.search` with `^` anchor (`FURB167`) ([#22984](https://github.com/astral-sh/ruff/pull/22984)) -- Fix link to Sphinx code block directives ([#23041](https://github.com/astral-sh/ruff/pull/23041)) -- \[`pydocstyle`\] Clarify which quote styles are allowed (`D300`) ([#22825](https://github.com/astral-sh/ruff/pull/22825)) -- \[`flake8-bugbear`\] Improve docs for `no-explicit-stacklevel` (`B028`) ([#22538](https://github.com/astral-sh/ruff/pull/22538)) - -### Other changes - -- Update MSRV to 1.91 ([#22874](https://github.com/astral-sh/ruff/pull/22874)) +- [@zaniebot](https://github.com/zaniebot) -### Contributors +## 0.15.x -- [@danparizher](https://github.com/danparizher) -- [@chirizxc](https://github.com/chirizxc) -- [@amyreese](https://github.com/amyreese) -- [@Jkhall81](https://github.com/Jkhall81) -- [@cwkang1998](https://github.com/cwkang1998) -- [@manzt](https://github.com/manzt) -- [@11happy](https://github.com/11happy) -- [@hugovk](https://github.com/hugovk) -- [@caiquejjx](https://github.com/caiquejjx) -- [@ntBre](https://github.com/ntBre) -- [@akawd](https://github.com/akawd) -- [@konstin](https://github.com/konstin) +See [changelogs/0.15.x](./changelogs/0.15.x.md) ## 0.14.x diff --git a/Cargo.lock b/Cargo.lock index 1147a4c8cb..1ec238c5fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3076,7 +3076,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.15.22" +version = "0.16.0" dependencies = [ "anyhow", "argfile", @@ -3140,7 +3140,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anstream 1.0.0", "anstyle", @@ -3181,7 +3181,7 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.5" +version = "0.0.6" dependencies = [ "char_str", "filetime", @@ -3195,7 +3195,7 @@ dependencies = [ [[package]] name = "ruff_db" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anstyle", "arc-swap", @@ -3286,7 +3286,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.5" +version = "0.0.6" dependencies = [ "get-size2", "is-macro", @@ -3296,7 +3296,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.5" +version = "0.0.6" dependencies = [ "drop_bomb", "ruff_cache", @@ -3312,7 +3312,7 @@ dependencies = [ [[package]] name = "ruff_graph" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "clap", @@ -3333,7 +3333,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.5" +version = "0.0.6" dependencies = [ "get-size2", "ruff_macros", @@ -3343,7 +3343,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.15.22" +version = "0.16.0" dependencies = [ "aho-corasick", "anyhow", @@ -3406,7 +3406,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.5" +version = "0.0.6" dependencies = [ "heck", "itertools 0.15.0", @@ -3419,7 +3419,7 @@ dependencies = [ [[package]] name = "ruff_markdown" -version = "0.0.5" +version = "0.0.6" dependencies = [ "insta", "regex", @@ -3450,14 +3450,14 @@ dependencies = [ [[package]] name = "ruff_memory_usage" -version = "0.0.5" +version = "0.0.6" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "rand 0.10.2", @@ -3473,14 +3473,14 @@ dependencies = [ [[package]] name = "ruff_options_metadata" -version = "0.0.5" +version = "0.0.6" dependencies = [ "serde", ] [[package]] name = "ruff_python_ast" -version = "0.0.5" +version = "0.0.6" dependencies = [ "aho-corasick", "arrayvec", @@ -3517,7 +3517,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.5" +version = "0.0.6" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -3529,7 +3529,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "clap", @@ -3562,7 +3562,7 @@ dependencies = [ [[package]] name = "ruff_python_importer" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "insta", @@ -3577,7 +3577,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.5" +version = "0.0.6" dependencies = [ "ruff_python_ast", "ruff_python_parser", @@ -3588,7 +3588,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.5" +version = "0.0.6" dependencies = [ "bitflags 2.13.0", "icu_properties", @@ -3598,7 +3598,7 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "bitflags 2.13.0", @@ -3627,7 +3627,7 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.5" +version = "0.0.6" dependencies = [ "bitflags 2.13.0", "insta", @@ -3648,7 +3648,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.5" +version = "0.0.6" dependencies = [ "bitflags 2.13.0", "unicode-ident", @@ -3656,7 +3656,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" -version = "0.0.5" +version = "0.0.6" dependencies = [ "itertools 0.15.0", "ruff_source_file", @@ -3677,7 +3677,7 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.5" +version = "0.0.6" dependencies = [ "get-size2", "ruff_db", @@ -3689,7 +3689,7 @@ dependencies = [ [[package]] name = "ruff_server" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "crossbeam", @@ -3732,7 +3732,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.5" +version = "0.0.6" dependencies = [ "get-size2", "memchr", @@ -3742,7 +3742,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.5" +version = "0.0.6" dependencies = [ "get-size2", "schemars", @@ -3753,7 +3753,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.15.22" +version = "0.16.0" dependencies = [ "console_error_panic_hook", "console_log", @@ -3780,7 +3780,7 @@ dependencies = [ [[package]] name = "ruff_workspace" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "colored", @@ -4665,7 +4665,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.5" +version = "0.0.6" dependencies = [ "ordermap", "ruff_db", @@ -4748,7 +4748,7 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "camino", @@ -4821,7 +4821,7 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "bitflags 2.13.0", @@ -4855,7 +4855,7 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.5" +version = "0.0.6" dependencies = [ "anyhow", "bitflags 2.13.0", @@ -4949,7 +4949,7 @@ dependencies = [ [[package]] name = "ty_site_packages" -version = "0.0.5" +version = "0.0.6" dependencies = [ "camino", "colored", @@ -4970,7 +4970,7 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.5" +version = "0.0.6" dependencies = [ "ruff_macros", ] @@ -5002,7 +5002,7 @@ dependencies = [ [[package]] name = "ty_vendored" -version = "0.0.5" +version = "0.0.6" dependencies = [ "path-slash", "ruff_db", diff --git a/Cargo.toml b/Cargo.toml index 4ffa870687..36a52565a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,51 +14,51 @@ license = "MIT" [workspace.dependencies] char_str = { version = "0.0.2" } -ruff = { version = "0.15.22", path = "crates/ruff" } -ruff_annotate_snippets = { version = "0.0.5", path = "crates/ruff_annotate_snippets" } -ruff_cache = { version = "0.0.5", path = "crates/ruff_cache" } -ruff_db = { version = "0.0.5", path = "crates/ruff_db", default-features = false } -ruff_diagnostics = { version = "0.0.5", path = "crates/ruff_diagnostics" } -ruff_formatter = { version = "0.0.5", path = "crates/ruff_formatter" } -ruff_graph = { version = "0.0.5", path = "crates/ruff_graph" } -ruff_index = { version = "0.0.5", path = "crates/ruff_index" } -ruff_linter = { version = "0.15.22", path = "crates/ruff_linter" } -ruff_macros = { version = "0.0.5", path = "crates/ruff_macros" } -ruff_markdown = { version = "0.0.5", path = "crates/ruff_markdown" } -ruff_memory_usage = { version = "0.0.5", path = "crates/ruff_memory_usage" } -ruff_notebook = { version = "0.0.5", path = "crates/ruff_notebook" } -ruff_options_metadata = { version = "0.0.5", path = "crates/ruff_options_metadata" } -ruff_python_ast = { version = "0.0.5", path = "crates/ruff_python_ast" } -ruff_python_codegen = { version = "0.0.5", path = "crates/ruff_python_codegen" } -ruff_python_formatter = { version = "0.0.5", path = "crates/ruff_python_formatter" } -ruff_python_importer = { version = "0.0.5", path = "crates/ruff_python_importer" } -ruff_python_index = { version = "0.0.5", path = "crates/ruff_python_index" } -ruff_python_literal = { version = "0.0.5", path = "crates/ruff_python_literal" } -ruff_python_parser = { version = "0.0.5", path = "crates/ruff_python_parser" } -ruff_python_semantic = { version = "0.0.5", path = "crates/ruff_python_semantic" } -ruff_python_stdlib = { version = "0.0.5", path = "crates/ruff_python_stdlib" } -ruff_python_trivia = { version = "0.0.5", path = "crates/ruff_python_trivia" } -ruff_server = { version = "0.0.5", path = "crates/ruff_server" } -ruff_source_file = { version = "0.0.5", path = "crates/ruff_source_file" } +ruff = { version = "0.16.0", path = "crates/ruff" } +ruff_annotate_snippets = { version = "0.0.6", path = "crates/ruff_annotate_snippets" } +ruff_cache = { version = "0.0.6", path = "crates/ruff_cache" } +ruff_db = { version = "0.0.6", path = "crates/ruff_db", default-features = false } +ruff_diagnostics = { version = "0.0.6", path = "crates/ruff_diagnostics" } +ruff_formatter = { version = "0.0.6", path = "crates/ruff_formatter" } +ruff_graph = { version = "0.0.6", path = "crates/ruff_graph" } +ruff_index = { version = "0.0.6", path = "crates/ruff_index" } +ruff_linter = { version = "0.16.0", path = "crates/ruff_linter" } +ruff_macros = { version = "0.0.6", path = "crates/ruff_macros" } +ruff_markdown = { version = "0.0.6", path = "crates/ruff_markdown" } +ruff_memory_usage = { version = "0.0.6", path = "crates/ruff_memory_usage" } +ruff_notebook = { version = "0.0.6", path = "crates/ruff_notebook" } +ruff_options_metadata = { version = "0.0.6", path = "crates/ruff_options_metadata" } +ruff_python_ast = { version = "0.0.6", path = "crates/ruff_python_ast" } +ruff_python_codegen = { version = "0.0.6", path = "crates/ruff_python_codegen" } +ruff_python_formatter = { version = "0.0.6", path = "crates/ruff_python_formatter" } +ruff_python_importer = { version = "0.0.6", path = "crates/ruff_python_importer" } +ruff_python_index = { version = "0.0.6", path = "crates/ruff_python_index" } +ruff_python_literal = { version = "0.0.6", path = "crates/ruff_python_literal" } +ruff_python_parser = { version = "0.0.6", path = "crates/ruff_python_parser" } +ruff_python_semantic = { version = "0.0.6", path = "crates/ruff_python_semantic" } +ruff_python_stdlib = { version = "0.0.6", path = "crates/ruff_python_stdlib" } +ruff_python_trivia = { version = "0.0.6", path = "crates/ruff_python_trivia" } +ruff_server = { version = "0.0.6", path = "crates/ruff_server" } +ruff_source_file = { version = "0.0.6", path = "crates/ruff_source_file" } ruff_mdtest = { path = "crates/ruff_mdtest" } -ruff_ranged_value = { version = "0.0.5", path = "crates/ruff_ranged_value" } -ruff_text_size = { version = "0.0.5", path = "crates/ruff_text_size" } -ruff_workspace = { version = "0.0.5", path = "crates/ruff_workspace" } +ruff_ranged_value = { version = "0.0.6", path = "crates/ruff_ranged_value" } +ruff_text_size = { version = "0.0.6", path = "crates/ruff_text_size" } +ruff_workspace = { version = "0.0.6", path = "crates/ruff_workspace" } ty = { path = "crates/ty" } -ty_combine = { version = "0.0.5", path = "crates/ty_combine" } +ty_combine = { version = "0.0.6", path = "crates/ty_combine" } ty_completion_bench = { path = "crates/ty_completion_bench" } ty_completion_eval = { path = "crates/ty_completion_eval" } ty_ide = { path = "crates/ty_ide" } -ty_module_resolver = { version = "0.0.5", path = "crates/ty_module_resolver" } +ty_module_resolver = { version = "0.0.6", path = "crates/ty_module_resolver" } ty_project = { path = "crates/ty_project", default-features = false } -ty_python_semantic = { version = "0.0.5", path = "crates/ty_python_semantic" } -ty_python_core = { version = "0.0.5", path = "crates/ty_python_core" } +ty_python_semantic = { version = "0.0.6", path = "crates/ty_python_semantic" } +ty_python_core = { version = "0.0.6", path = "crates/ty_python_core" } ty_server = { path = "crates/ty_server" } -ty_site_packages = { version = "0.0.5", path = "crates/ty_site_packages" } -ty_static = { version = "0.0.5", path = "crates/ty_static" } +ty_site_packages = { version = "0.0.6", path = "crates/ty_site_packages" } +ty_static = { version = "0.0.6", path = "crates/ty_static" } ty_test = { path = "crates/ty_test" } -ty_vendored = { version = "0.0.5", path = "crates/ty_vendored" } +ty_vendored = { version = "0.0.6", path = "crates/ty_vendored" } mdtest = { path = "crates/mdtest" } diff --git a/README.md b/README.md index 8eb4311acd..0d02aea570 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,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.15.22/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.15.22/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.16.0/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.16.0/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -186,7 +186,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.15.22 + rev: v0.16.0 hooks: # Run the linter. - id: ruff-check diff --git a/changelogs/0.15.x.md b/changelogs/0.15.x.md new file mode 100644 index 0000000000..4bb2e2060f --- /dev/null +++ b/changelogs/0.15.x.md @@ -0,0 +1,1329 @@ +## 0.15.0 + +Released on 2026-02-03. + +Check out the [blog post](https://astral.sh/blog/ruff-v0.15.0) for a migration +guide and overview of the changes! + +### Breaking changes + +- Ruff now formats your code according to the 2026 style guide. See the formatter section below or in the blog post for a detailed list of changes. + +- The linter now supports block suppression comments. For example, to suppress `N803` for all parameters in this function: + + ```python + # ruff: disable[N803] + def foo( + legacyArg1, + legacyArg2, + legacyArg3, + legacyArg4, + ): ... + # ruff: enable[N803] + ``` + + See the [documentation](https://docs.astral.sh/ruff/linter/#block-level) for more details. + +- The `ruff:alpine` Docker image is now based on Alpine 3.23 (up from 3.21). + +- The `ruff:debian` and `ruff:debian-slim` Docker images are now based on Debian 13 "Trixie" instead of Debian 12 "Bookworm." + +- Binaries for the `ppc64` (64-bit big-endian PowerPC) architecture are no longer included in our releases. It should still be possible to build Ruff manually for this platform, if needed. + +- Ruff now resolves all `extend`ed configuration files before falling back on a default Python version. + +### Stabilization + +The following rules have been stabilized and are no longer in preview: + +- [`blocking-http-call-httpx-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-http-call-httpx-in-async-function) + (`ASYNC212`) +- [`blocking-path-method-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-path-method-in-async-function) + (`ASYNC240`) +- [`blocking-input-in-async-function`](https://docs.astral.sh/ruff/rules/blocking-input-in-async-function) + (`ASYNC250`) +- [`map-without-explicit-strict`](https://docs.astral.sh/ruff/rules/map-without-explicit-strict) + (`B912`) +- [`if-exp-instead-of-or-operator`](https://docs.astral.sh/ruff/rules/if-exp-instead-of-or-operator) + (`FURB110`) +- [`single-item-membership-test`](https://docs.astral.sh/ruff/rules/single-item-membership-test) + (`FURB171`) +- [`missing-maxsplit-arg`](https://docs.astral.sh/ruff/rules/missing-maxsplit-arg) (`PLC0207`) +- [`unnecessary-lambda`](https://docs.astral.sh/ruff/rules/unnecessary-lambda) (`PLW0108`) +- [`unnecessary-empty-iterable-within-deque-call`](https://docs.astral.sh/ruff/rules/unnecessary-empty-iterable-within-deque-call) + (`RUF037`) +- [`in-empty-collection`](https://docs.astral.sh/ruff/rules/in-empty-collection) (`RUF060`) +- [`legacy-form-pytest-raises`](https://docs.astral.sh/ruff/rules/legacy-form-pytest-raises) + (`RUF061`) +- [`non-octal-permissions`](https://docs.astral.sh/ruff/rules/non-octal-permissions) (`RUF064`) +- [`invalid-rule-code`](https://docs.astral.sh/ruff/rules/invalid-rule-code) (`RUF102`) +- [`invalid-suppression-comment`](https://docs.astral.sh/ruff/rules/invalid-suppression-comment) + (`RUF103`) +- [`unmatched-suppression-comment`](https://docs.astral.sh/ruff/rules/unmatched-suppression-comment) + (`RUF104`) +- [`replace-str-enum`](https://docs.astral.sh/ruff/rules/replace-str-enum) (`UP042`) + +The following behaviors have been stabilized: + +- The `--output-format` flag is now respected when running Ruff in `--watch` mode, and the `full` output format is now used by default, matching the regular CLI output. +- [`builtin-attribute-shadowing`](https://docs.astral.sh/ruff/rules/builtin-attribute-shadowing/) (`A003`) now detects the use of shadowed built-in names in additional contexts like decorators, default arguments, and other attribute definitions. +- [`duplicate-union-member`](https://docs.astral.sh/ruff/rules/duplicate-union-member/) (`PYI016`) now considers `typing.Optional` when searching for duplicate union members. +- [`split-static-string`](https://docs.astral.sh/ruff/rules/split-static-string/) (`SIM905`) now offers an autofix when the `maxsplit` argument is provided, even without a `sep` argument. +- [`dict-get-with-none-default`](https://docs.astral.sh/ruff/rules/dict-get-with-none-default/) (`SIM910`) now applies to more types of key expressions. +- [`super-call-with-parameters`](https://docs.astral.sh/ruff/rules/super-call-with-parameters/) (`UP008`) now has a safe fix when it will not delete comments. +- [`unnecessary-default-type-args`](https://docs.astral.sh/ruff/rules/unnecessary-default-type-args/) (`UP043`) now applies to stub (`.pyi`) files on Python versions before 3.13. + +### Formatter + +This release introduces the new 2026 style guide, with the following changes: + +- Lambda parameters are now kept on the same line and lambda bodies will be parenthesized to let + them break across multiple lines ([#21385](https://github.com/astral-sh/ruff/pull/21385)) +- Parentheses around tuples of exceptions in `except` clauses will now be removed on Python 3.14 and + later ([#20768](https://github.com/astral-sh/ruff/pull/20768)) +- A single empty line is now permitted at the beginning of function bodies ([#21110](https://github.com/astral-sh/ruff/pull/21110)) +- Parentheses are avoided for long `as` captures in `match` statements ([#21176](https://github.com/astral-sh/ruff/pull/21176)) +- Extra spaces between escaped quotes and ending triple quotes can now be omitted ([#17216](https://github.com/astral-sh/ruff/pull/17216)) +- Blank lines are now enforced before classes with decorators in stub files ([#18888](https://github.com/astral-sh/ruff/pull/18888)) + +### Preview features + +- Apply formatting to Markdown code blocks ([#22470](https://github.com/astral-sh/ruff/pull/22470), [#22990](https://github.com/astral-sh/ruff/pull/22990), [#22996](https://github.com/astral-sh/ruff/pull/22996)) + + See the [documentation](https://docs.astral.sh/ruff/formatter/#markdown-code-formatting) for more details. + +### Bug fixes + +- Fix suppression indentation matching ([#22903](https://github.com/astral-sh/ruff/pull/22903)) + +### Rule changes + +- Customize where the `fix_title` sub-diagnostic appears ([#23044](https://github.com/astral-sh/ruff/pull/23044)) +- \[`FastAPI`\] Add sub-diagnostic explaining why a fix was unavailable (`FAST002`) ([#22565](https://github.com/astral-sh/ruff/pull/22565)) +- \[`flake8-annotations`\] Don't suggest `NoReturn` for functions raising `NotImplementedError` (`ANN201`, `ANN202`, `ANN205`, `ANN206`) ([#21311](https://github.com/astral-sh/ruff/pull/21311)) +- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP017`) ([#22873](https://github.com/astral-sh/ruff/pull/22873)) +- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP020`) ([#22872](https://github.com/astral-sh/ruff/pull/22872)) +- \[`pyupgrade`\] Make fix unsafe if it deletes comments (`UP033`) ([#22871](https://github.com/astral-sh/ruff/pull/22871)) +- \[`refurb`\] Do not add `abc.ABC` if already present (`FURB180`) ([#22234](https://github.com/astral-sh/ruff/pull/22234)) +- \[`refurb`\] Make fix unsafe if it deletes comments (`FURB110`) ([#22768](https://github.com/astral-sh/ruff/pull/22768)) +- \[`ruff`\] Add sub-diagnostics with permissions (`RUF064`) ([#22972](https://github.com/astral-sh/ruff/pull/22972)) + +### Server + +- Identify notebooks by LSP `didOpen` instead of `.ipynb` file extension ([#22810](https://github.com/astral-sh/ruff/pull/22810)) + +### CLI + +- Add `--color` CLI option to force colored output ([#22806](https://github.com/astral-sh/ruff/pull/22806)) + +### Documentation + +- Document `-` stdin convention in CLI help text ([#22817](https://github.com/astral-sh/ruff/pull/22817)) +- \[`refurb`\] Change example to `re.search` with `^` anchor (`FURB167`) ([#22984](https://github.com/astral-sh/ruff/pull/22984)) +- Fix link to Sphinx code block directives ([#23041](https://github.com/astral-sh/ruff/pull/23041)) +- \[`pydocstyle`\] Clarify which quote styles are allowed (`D300`) ([#22825](https://github.com/astral-sh/ruff/pull/22825)) +- \[`flake8-bugbear`\] Improve docs for `no-explicit-stacklevel` (`B028`) ([#22538](https://github.com/astral-sh/ruff/pull/22538)) + +### Other changes + +- Update MSRV to 1.91 ([#22874](https://github.com/astral-sh/ruff/pull/22874)) + +### Contributors + +- [@danparizher](https://github.com/danparizher) +- [@chirizxc](https://github.com/chirizxc) +- [@amyreese](https://github.com/amyreese) +- [@Jkhall81](https://github.com/Jkhall81) +- [@cwkang1998](https://github.com/cwkang1998) +- [@manzt](https://github.com/manzt) +- [@11happy](https://github.com/11happy) +- [@hugovk](https://github.com/hugovk) +- [@caiquejjx](https://github.com/caiquejjx) +- [@ntBre](https://github.com/ntBre) +- [@akawd](https://github.com/akawd) +- [@konstin](https://github.com/konstin) + +## 0.15.1 + +Released on 2026-02-12. + +### Preview features + +- \[`airflow`\] Add ruff rules to catch deprecated Airflow imports for Airflow 3.1 (`AIR321`) ([#22376](https://github.com/astral-sh/ruff/pull/22376)) +- \[`airflow`\] Third positional parameter not named `ti_key` should be flagged for `BaseOperatorLink.get_link` (`AIR303`) ([#22828](https://github.com/astral-sh/ruff/pull/22828)) +- \[`flake8-gettext`\] Fix false negatives for plural argument of `ngettext` (`INT001`, `INT002`, `INT003`) ([#21078](https://github.com/astral-sh/ruff/pull/21078)) +- \[`pyflakes`\] Fix infinite loop in preview fix for `unused-import` (`F401`) ([#23038](https://github.com/astral-sh/ruff/pull/23038)) +- \[`pygrep-hooks`\] Detect non-existent mock methods in standalone expressions (`PGH005`) ([#22830](https://github.com/astral-sh/ruff/pull/22830)) +- \[`pylint`\] Allow dunder submodules and improve diagnostic range (`PLC2701`) ([#22804](https://github.com/astral-sh/ruff/pull/22804)) +- \[`pyupgrade`\] Improve diagnostic range for tuples (`UP024`) ([#23013](https://github.com/astral-sh/ruff/pull/23013)) +- \[`refurb`\] Check subscripts in tuple do not use lambda parameters in `reimplemented-operator` (`FURB118`) ([#23079](https://github.com/astral-sh/ruff/pull/23079)) +- \[`ruff`\] Detect mutable defaults in `field` calls (`RUF008`) ([#23046](https://github.com/astral-sh/ruff/pull/23046)) +- \[`ruff`\] Ignore std `cmath.inf` (`RUF069`) ([#23120](https://github.com/astral-sh/ruff/pull/23120)) +- \[`ruff`\] New rule `float-equality-comparison` (`RUF069`) ([#20585](https://github.com/astral-sh/ruff/pull/20585)) +- Don't format unlabeled Markdown code blocks ([#23106](https://github.com/astral-sh/ruff/pull/23106)) +- Markdown formatting support in LSP ([#23063](https://github.com/astral-sh/ruff/pull/23063)) +- Support Quarto Markdown language markers ([#22947](https://github.com/astral-sh/ruff/pull/22947)) +- Support formatting `pycon` Markdown code blocks ([#23112](https://github.com/astral-sh/ruff/pull/23112)) +- Use extension mapping to select Markdown code block language ([#22934](https://github.com/astral-sh/ruff/pull/22934)) + +### Bug fixes + +- Avoid false positive for undefined variables in `FAST001` ([#23224](https://github.com/astral-sh/ruff/pull/23224)) +- Avoid introducing syntax errors for `FAST003` autofix ([#23227](https://github.com/astral-sh/ruff/pull/23227)) +- Avoid suggesting `InitVar` for `__post_init__` that references PEP 695 type parameters ([#23226](https://github.com/astral-sh/ruff/pull/23226)) +- Deduplicate type variables in generic functions ([#23225](https://github.com/astral-sh/ruff/pull/23225)) +- Fix exception handler parenthesis removal for Python 3.14+ ([#23126](https://github.com/astral-sh/ruff/pull/23126)) +- Fix f-string middle panic when parsing t-strings ([#23232](https://github.com/astral-sh/ruff/pull/23232)) +- Wrap `RUF020` target for multiline fixes ([#23210](https://github.com/astral-sh/ruff/pull/23210)) +- Wrap `UP007` target for multiline fixes ([#23208](https://github.com/astral-sh/ruff/pull/23208)) +- Fix missing diagnostics for last range suppression in file ([#23242](https://github.com/astral-sh/ruff/pull/23242)) +- \[`pyupgrade`\] Fix syntax error on string with newline escape and comment (`UP037`) ([#22968](https://github.com/astral-sh/ruff/pull/22968)) + +### Rule changes + +- Use `ruff` instead of `Ruff` as the program name in GitHub output format ([#23240](https://github.com/astral-sh/ruff/pull/23240)) +- \[`PT006`\] Fix syntax error when unpacking nested tuples in `parametrize` fixes (#22441) ([#22464](https://github.com/astral-sh/ruff/pull/22464)) +- \[`airflow`\] Catch deprecated attribute access from context key for Airflow 3.0 (`AIR301`) ([#22850](https://github.com/astral-sh/ruff/pull/22850)) +- \[`airflow`\] Capture deprecated arguments and a decorator (`AIR301`) ([#23170](https://github.com/astral-sh/ruff/pull/23170)) +- \[`flake8-boolean-trap`\] Add `multiprocessing.Value` to excluded functions for `FBT003` ([#23010](https://github.com/astral-sh/ruff/pull/23010)) +- \[`flake8-bugbear`\] Add a secondary annotation showing the previous occurrence (`B033`) ([#22634](https://github.com/astral-sh/ruff/pull/22634)) +- \[`flake8-type-checking`\] Add sub-diagnostic showing the runtime use of an annotation (`TC004`) ([#23091](https://github.com/astral-sh/ruff/pull/23091)) +- \[`isort`\] Support configurable import section heading comments ([#23151](https://github.com/astral-sh/ruff/pull/23151)) +- \[`ruff`\] Improve the diagnostic for `RUF012` ([#23202](https://github.com/astral-sh/ruff/pull/23202)) + +### Formatter + +- Suppress diagnostic output for `format --check --silent` ([#17736](https://github.com/astral-sh/ruff/pull/17736)) + +### Documentation + +- Add tabbed shell completion documentation ([#23169](https://github.com/astral-sh/ruff/pull/23169)) +- Explain how to enable Markdown formatting for pre-commit hook ([#23077](https://github.com/astral-sh/ruff/pull/23077)) +- Fixed import in `runtime-evaluated-decorators` example ([#23187](https://github.com/astral-sh/ruff/pull/23187)) +- Update ruff server contributing guide ([#23060](https://github.com/astral-sh/ruff/pull/23060)) + +### Other changes + +- Exclude WASM artifacts from GitHub releases ([#23221](https://github.com/astral-sh/ruff/pull/23221)) + +### Contributors + +- [@mkniewallner](https://github.com/mkniewallner) +- [@bxff](https://github.com/bxff) +- [@dylwil3](https://github.com/dylwil3) +- [@Avasam](https://github.com/Avasam) +- [@amyreese](https://github.com/amyreese) +- [@charliermarsh](https://github.com/charliermarsh) +- [@Alex-ley-scrub](https://github.com/Alex-ley-scrub) +- [@Kalmaegi](https://github.com/Kalmaegi) +- [@danparizher](https://github.com/danparizher) +- [@AiyionPrime](https://github.com/AiyionPrime) +- [@eureka928](https://github.com/eureka928) +- [@11happy](https://github.com/11happy) +- [@Jkhall81](https://github.com/Jkhall81) +- [@chirizxc](https://github.com/chirizxc) +- [@leandrobbraga](https://github.com/leandrobbraga) +- [@tvatter](https://github.com/tvatter) +- [@anishgirianish](https://github.com/anishgirianish) +- [@shaanmajid](https://github.com/shaanmajid) +- [@ntBre](https://github.com/ntBre) +- [@sjyangkevin](https://github.com/sjyangkevin) + +## 0.15.2 + +Released on 2026-02-19. + +### Preview features + +- Expand the default rule set ([#23385](https://github.com/astral-sh/ruff/pull/23385)) + + In preview, Ruff now enables a significantly expanded default rule set of 412 + rules, up from the stable default set of 59 rules. The new rules are mostly a + superset of the stable defaults, with the exception of these rules, which are + removed from the preview defaults: + + - [`multiple-imports-on-one-line`](https://docs.astral.sh/ruff/rules/multiple-imports-on-one-line) (`E401`) + - [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file) (`E402`) + - [`module-import-not-at-top-of-file`](https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file) (`E701`) + - [`multiple-statements-on-one-line-semicolon`](https://docs.astral.sh/ruff/rules/multiple-statements-on-one-line-semicolon) (`E702`) + - [`useless-semicolon`](https://docs.astral.sh/ruff/rules/useless-semicolon) (`E703`) + - [`none-comparison`](https://docs.astral.sh/ruff/rules/none-comparison) (`E711`) + - [`true-false-comparison`](https://docs.astral.sh/ruff/rules/true-false-comparison) (`E712`) + - [`not-in-test`](https://docs.astral.sh/ruff/rules/not-in-test) (`E713`) + - [`not-is-test`](https://docs.astral.sh/ruff/rules/not-is-test) (`E714`) + - [`type-comparison`](https://docs.astral.sh/ruff/rules/type-comparison) (`E721`) + - [`lambda-assignment`](https://docs.astral.sh/ruff/rules/lambda-assignment) (`E731`) + - [`ambiguous-variable-name`](https://docs.astral.sh/ruff/rules/ambiguous-variable-name) (`E741`) + - [`ambiguous-class-name`](https://docs.astral.sh/ruff/rules/ambiguous-class-name) (`E742`) + - [`ambiguous-function-name`](https://docs.astral.sh/ruff/rules/ambiguous-function-name) (`E743`) + - [`undefined-local-with-import-star`](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star) (`F403`) + - [`undefined-local-with-import-star-usage`](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star-usage) (`F405`) + - [`undefined-local-with-nested-import-star-usage`](https://docs.astral.sh/ruff/rules/undefined-local-with-nested-import-star-usage) (`F406`) + - [`forward-annotation-syntax-error`](https://docs.astral.sh/ruff/rules/forward-annotation-syntax-error) (`F722`) + + If you use preview and prefer the old defaults, you can restore them with + configuration like: + + ```toml + + # ruff.toml + + [lint] + select = ["E4", "E7", "E9", "F"] + + # pyproject.toml + + [tool.ruff.lint] + select = ["E4", "E7", "E9", "F"] + ``` + + If you do give them a try, feel free to share your feedback in the [GitHub + discussion](https://github.com/astral-sh/ruff/discussions/23203)! + +- \[`flake8-pyi`\] Also check string annotations (`PYI041`) ([#19023](https://github.com/astral-sh/ruff/pull/19023)) + +### Bug fixes + +- \[`flake8-async`\] Fix `in_async_context` logic ([#23426](https://github.com/astral-sh/ruff/pull/23426)) +- \[`ruff`\] Fix for `RUF102` should delete entire comment ([#23380](https://github.com/astral-sh/ruff/pull/23380)) +- \[`ruff`\] Suppress diagnostic for strings with backslashes in interpolations before Python 3.12 (`RUF027`) ([#21069](https://github.com/astral-sh/ruff/pull/21069)) +- \[`flake8-bugbear`\] Fix `B023` false positive for immediately-invoked lambdas ([#23294](https://github.com/astral-sh/ruff/pull/23294)) +- [parser] Fix false syntax error for match-like annotated assignments ([#23297](https://github.com/astral-sh/ruff/pull/23297)) +- [parser] Fix indentation tracking after line continuations ([#23417](https://github.com/astral-sh/ruff/pull/23417)) + +### Rule changes + +- \[`flake8-executable`\] Allow global flags in uv shebangs (`EXE003`) ([#22582](https://github.com/astral-sh/ruff/pull/22582)) +- \[`pyupgrade`\] Fix handling of `typing.{io,re}` (`UP035`) ([#23131](https://github.com/astral-sh/ruff/pull/23131)) +- \[`ruff`\] Detect `PLC0207` on chained `str.split()` calls ([#23275](https://github.com/astral-sh/ruff/pull/23275)) + +### CLI + +- Remove invalid inline `noqa` warning ([#23270](https://github.com/astral-sh/ruff/pull/23270)) + +### Configuration + +- Add extension mapping to configuration file options ([#23384](https://github.com/astral-sh/ruff/pull/23384)) + +### Documentation + +- Add `Q004` to the list of conflicting rules ([#23340](https://github.com/astral-sh/ruff/pull/23340)) +- \[`ruff`\] Expand `lint.external` docs and add sub-diagnostic (`RUF100`, `RUF102`) ([#23268](https://github.com/astral-sh/ruff/pull/23268)) + +### Contributors + +- [@dylwil3](https://github.com/dylwil3) +- [@Jkhall81](https://github.com/Jkhall81) +- [@danparizher](https://github.com/danparizher) +- [@dhruvmanila](https://github.com/dhruvmanila) +- [@harupy](https://github.com/harupy) +- [@ngnpope](https://github.com/ngnpope) +- [@amyreese](https://github.com/amyreese) +- [@kar-ganap](https://github.com/kar-ganap) +- [@robsdedude](https://github.com/robsdedude) +- [@shaanmajid](https://github.com/shaanmajid) +- [@ntBre](https://github.com/ntBre) +- [@toslunar](https://github.com/toslunar) + +## 0.15.3 + +Released on 2026-02-26. + +### Preview features + +- Drop explicit support for `.qmd` file extension ([#23572](https://github.com/astral-sh/ruff/pull/23572)) + + This can now be enabled instead by setting the [`extension`](https://docs.astral.sh/ruff/settings/#extension) option: + + ```toml + # ruff.toml + extension = { qmd = "markdown" } + + # pyproject.toml + [tool.ruff] + extension = { qmd = "markdown" } + ``` + +- Include configured extensions in file discovery ([#23400](https://github.com/astral-sh/ruff/pull/23400)) + +- \[`flake8-bandit`\] Allow suspicious imports in `TYPE_CHECKING` blocks (`S401`-`S415`) ([#23441](https://github.com/astral-sh/ruff/pull/23441)) + +- \[`flake8-bugbear`\] Allow `B901` in pytest hook wrappers ([#21931](https://github.com/astral-sh/ruff/pull/21931)) + +- \[`flake8-import-conventions`\] Add missing conventions from upstream (`ICN001`, `ICN002`) ([#21373](https://github.com/astral-sh/ruff/pull/21373)) + +- \[`pydocstyle`\] Add rule to enforce docstring section ordering (`D420`) ([#23537](https://github.com/astral-sh/ruff/pull/23537)) + +- \[`pylint`\] Implement `swap-with-temporary-variable` (`PLR1712`) ([#22205](https://github.com/astral-sh/ruff/pull/22205)) + +- \[`ruff`\] Add `unnecessary-assign-before-yield` (`RUF070`) ([#23300](https://github.com/astral-sh/ruff/pull/23300)) + +- \[`ruff`\] Support file-level noqa in `RUF102` ([#23535](https://github.com/astral-sh/ruff/pull/23535)) + +- \[`ruff`\] Suppress diagnostic for invalid f-strings before Python 3.12 (`RUF027`) ([#23480](https://github.com/astral-sh/ruff/pull/23480)) + +- \[`flake8-bandit`\] Don't flag `BaseLoader`/`CBaseLoader` as unsafe (`S506`) ([#23510](https://github.com/astral-sh/ruff/pull/23510)) + +### Bug fixes + +- Avoid infinite loop between `I002` and `PYI025` ([#23352](https://github.com/astral-sh/ruff/pull/23352)) +- \[`pyflakes`\] Fix false positive for `@overload` from `lint.typing-modules` (`F811`) ([#23357](https://github.com/astral-sh/ruff/pull/23357)) +- \[`pyupgrade`\] Fix false positive for `TypeVar` default before Python 3.12 (`UP046`) ([#23540](https://github.com/astral-sh/ruff/pull/23540)) +- \[`pyupgrade`\] Fix handling of `\N` in raw strings (`UP032`) ([#22149](https://github.com/astral-sh/ruff/pull/22149)) + +### Rule changes + +- Render sub-diagnostics in the GitHub output format ([#23455](https://github.com/astral-sh/ruff/pull/23455)) + +- \[`flake8-bugbear`\] Tag certain `B007` diagnostics as unnecessary ([#23453](https://github.com/astral-sh/ruff/pull/23453)) + +- \[`ruff`\] Ignore unknown rule codes in `RUF100` ([#23531](https://github.com/astral-sh/ruff/pull/23531)) + + These are now flagged by [`RUF102`](https://docs.astral.sh/ruff/rules/invalid-rule-code/) instead. + +### Documentation + +- Fix missing settings links for several linters ([#23519](https://github.com/astral-sh/ruff/pull/23519)) +- Update isort action comments heading ([#23515](https://github.com/astral-sh/ruff/pull/23515)) +- \[`pydocstyle`\] Fix double comma in description of `D404` ([#23440](https://github.com/astral-sh/ruff/pull/23440)) + +### Other changes + +- Update the Python module (notably `find_ruff_bin`) for parity with uv ([#23406](https://github.com/astral-sh/ruff/pull/23406)) + +### Contributors + +- [@zanieb](https://github.com/zanieb) +- [@o1x3](https://github.com/o1x3) +- [@assadyousuf](https://github.com/assadyousuf) +- [@kar-ganap](https://github.com/kar-ganap) +- [@denyszhak](https://github.com/denyszhak) +- [@amyreese](https://github.com/amyreese) +- [@carljm](https://github.com/carljm) +- [@anishgirianish](https://github.com/anishgirianish) +- [@Bnyro](https://github.com/Bnyro) +- [@danparizher](https://github.com/danparizher) +- [@ntBre](https://github.com/ntBre) +- [@gcomneno](https://github.com/gcomneno) +- [@jaap3](https://github.com/jaap3) +- [@stakeswky](https://github.com/stakeswky) + +## 0.15.4 + +Released on 2026-02-26. + +This is a follow-up release to 0.15.3 that resolves a panic when the new rule `PLR1712` was enabled with any rule that analyzes definitions, such as many of the `ANN` or `D` rules. + +### Bug fixes + +- Fix panic on access to definitions after analyzing definitions ([#23588](https://github.com/astral-sh/ruff/pull/23588)) +- \[`pyflakes`\] Suppress false positive in `F821` for names used before `del` in stub files ([#23550](https://github.com/astral-sh/ruff/pull/23550)) + +### Documentation + +- Clarify first-party import detection in Ruff ([#23591](https://github.com/astral-sh/ruff/pull/23591)) +- Fix incorrect `import-heading` example ([#23568](https://github.com/astral-sh/ruff/pull/23568)) + +### Contributors + +- [@stakeswky](https://github.com/stakeswky) +- [@ntBre](https://github.com/ntBre) +- [@thejcannon](https://github.com/thejcannon) +- [@GeObts](https://github.com/GeObts) + +## 0.15.5 + +Released on 2026-03-05. + +### Preview features + +- Discover Markdown files by default in preview mode ([#23434](https://github.com/astral-sh/ruff/pull/23434)) +- \[`perflint`\] Extend `PERF102` to comprehensions and generators ([#23473](https://github.com/astral-sh/ruff/pull/23473)) +- \[`refurb`\] Fix `FURB101` and `FURB103` false positives when I/O variable is used later ([#23542](https://github.com/astral-sh/ruff/pull/23542)) +- \[`ruff`\] Add fix for `none-not-at-end-of-union` (`RUF036`) ([#22829](https://github.com/astral-sh/ruff/pull/22829)) +- \[`ruff`\] Fix false positive for `re.split` with empty string pattern (`RUF055`) ([#23634](https://github.com/astral-sh/ruff/pull/23634)) + +### Bug fixes + +- \[`fastapi`\] Handle callable class dependencies with `__call__` method (`FAST003`) ([#23553](https://github.com/astral-sh/ruff/pull/23553)) +- \[`pydocstyle`\] Fix numpy section ordering (`D420`) ([#23685](https://github.com/astral-sh/ruff/pull/23685)) +- \[`pyflakes`\] Fix false positive for names shadowing re-exports (`F811`) ([#23356](https://github.com/astral-sh/ruff/pull/23356)) +- \[`pyupgrade`\] Avoid inserting redundant `None` elements in `UP045` ([#23459](https://github.com/astral-sh/ruff/pull/23459)) + +### Documentation + +- Document extension mapping for Markdown code formatting ([#23574](https://github.com/astral-sh/ruff/pull/23574)) +- Update default Python version examples ([#23605](https://github.com/astral-sh/ruff/pull/23605)) + +### Other changes + +- Publish releases to Astral mirror ([#23616](https://github.com/astral-sh/ruff/pull/23616)) + +### Contributors + +- [@amyreese](https://github.com/amyreese) +- [@stakeswky](https://github.com/stakeswky) +- [@chirizxc](https://github.com/chirizxc) +- [@anishgirianish](https://github.com/anishgirianish) +- [@bxff](https://github.com/bxff) +- [@zsol](https://github.com/zsol) +- [@charliermarsh](https://github.com/charliermarsh) +- [@ntBre](https://github.com/ntBre) +- [@kar-ganap](https://github.com/kar-ganap) + +## 0.15.6 + +Released on 2026-03-12. + +### Preview features + +- Add support for `lazy` import parsing ([#23755](https://github.com/astral-sh/ruff/pull/23755)) +- Add support for star-unpacking of comprehensions (PEP 798) ([#23788](https://github.com/astral-sh/ruff/pull/23788)) +- Reject semantic syntax errors for lazy imports ([#23757](https://github.com/astral-sh/ruff/pull/23757)) +- Drop a few rules from the preview default set ([#23879](https://github.com/astral-sh/ruff/pull/23879)) +- \[`airflow`\] Flag `Variable.get()` calls outside of task execution context (`AIR003`) ([#23584](https://github.com/astral-sh/ruff/pull/23584)) +- \[`airflow`\] Flag runtime-varying values in DAG/task constructor arguments (`AIR304`) ([#23631](https://github.com/astral-sh/ruff/pull/23631)) +- \[`flake8-bugbear`\] Implement `delattr-with-constant` (`B043`) ([#23737](https://github.com/astral-sh/ruff/pull/23737)) +- \[`flake8-tidy-imports`\] Add `TID254` to enforce lazy imports ([#23777](https://github.com/astral-sh/ruff/pull/23777)) +- \[`flake8-tidy-imports`\] Allow users to ban lazy imports with `TID254` ([#23847](https://github.com/astral-sh/ruff/pull/23847)) +- \[`isort`\] Retain `lazy` keyword when sorting imports ([#23762](https://github.com/astral-sh/ruff/pull/23762)) +- \[`pyupgrade`\] Add `from __future__ import annotations` automatically (`UP006`) ([#23260](https://github.com/astral-sh/ruff/pull/23260)) +- \[`refurb`\] Support `newline` parameter in `FURB101` for Python 3.13+ ([#23754](https://github.com/astral-sh/ruff/pull/23754)) +- \[`ruff`\] Add `os-path-commonprefix` (`RUF071`) ([#23814](https://github.com/astral-sh/ruff/pull/23814)) +- \[`ruff`\] Add unsafe fix for os-path-commonprefix (`RUF071`) ([#23852](https://github.com/astral-sh/ruff/pull/23852)) +- \[`ruff`\] Limit `RUF036` to typing contexts; make it unsafe for non-typing-only ([#23765](https://github.com/astral-sh/ruff/pull/23765)) +- \[`ruff`\] Use starred unpacking for `RUF017` in Python 3.15+ ([#23789](https://github.com/astral-sh/ruff/pull/23789)) + +### Bug fixes + +- Fix `--add-noqa` creating unwanted leading whitespace ([#23773](https://github.com/astral-sh/ruff/pull/23773)) +- Fix `--add-noqa` breaking shebangs ([#23577](https://github.com/astral-sh/ruff/pull/23577)) +- [formatter] Fix lambda body formatting for multiline calls and subscripts ([#23866](https://github.com/astral-sh/ruff/pull/23866)) +- [formatter] Preserve required annotation parentheses in annotated assignments ([#23865](https://github.com/astral-sh/ruff/pull/23865)) +- [formatter] Preserve type-expression parentheses in the formatter ([#23867](https://github.com/astral-sh/ruff/pull/23867)) +- \[`flake8-annotations`\] Fix stack overflow in `ANN401` on quoted annotations with escape sequences ([#23912](https://github.com/astral-sh/ruff/pull/23912)) +- \[`pep8-naming`\] Check naming conventions in `match` pattern bindings (`N806`, `N815`, `N816`) ([#23899](https://github.com/astral-sh/ruff/pull/23899)) +- \[`perflint`\] Fix comment duplication in fixes (`PERF401`, `PERF403`) ([#23729](https://github.com/astral-sh/ruff/pull/23729)) +- \[`pyupgrade`\] Properly trigger `super` change in nested class (`UP008`) ([#22677](https://github.com/astral-sh/ruff/pull/22677)) +- \[`ruff`\] Avoid syntax errors in `RUF036` fixes ([#23764](https://github.com/astral-sh/ruff/pull/23764)) + +### Rule changes + +- \[`flake8-bandit`\] Flag `S501` with `requests.request` ([#23873](https://github.com/astral-sh/ruff/pull/23873)) +- \[`flake8-executable`\] Fix WSL detection in non-Docker containers ([#22879](https://github.com/astral-sh/ruff/pull/22879)) +- \[`flake8-print`\] Ignore `pprint` calls with `stream=` ([#23787](https://github.com/astral-sh/ruff/pull/23787)) + +### Documentation + +- Update docs for Markdown code block formatting ([#23871](https://github.com/astral-sh/ruff/pull/23871)) +- \[`flake8-bugbear`\] Fix misleading description for `B904` ([#23731](https://github.com/astral-sh/ruff/pull/23731)) + +### Contributors + +- [@zsol](https://github.com/zsol) +- [@carljm](https://github.com/carljm) +- [@ntBre](https://github.com/ntBre) +- [@Bortlesboat](https://github.com/Bortlesboat) +- [@sososonia-cyber](https://github.com/sososonia-cyber) +- [@chirizxc](https://github.com/chirizxc) +- [@leandrobbraga](https://github.com/leandrobbraga) +- [@11happy](https://github.com/11happy) +- [@Acelogic](https://github.com/Acelogic) +- [@anishgirianish](https://github.com/anishgirianish) +- [@amyreese](https://github.com/amyreese) +- [@xvchris](https://github.com/xvchris) +- [@charliermarsh](https://github.com/charliermarsh) +- [@getehen](https://github.com/getehen) +- [@Dev-iL](https://github.com/Dev-iL) + +## 0.15.7 + +Released on 2026-03-19. + +### Preview features + +- Display output severity in preview ([#23845](https://github.com/astral-sh/ruff/pull/23845)) +- Don't show `noqa` hover for non-Python documents ([#24040](https://github.com/astral-sh/ruff/pull/24040)) + +### Rule changes + +- \[`pycodestyle`\] Recognize `pyrefly:` as a pragma comment (`E501`) ([#24019](https://github.com/astral-sh/ruff/pull/24019)) + +### Server + +- Don't return code actions for non-Python documents ([#23905](https://github.com/astral-sh/ruff/pull/23905)) + +### Documentation + +- Add company AI policy to contributing guide ([#24021](https://github.com/astral-sh/ruff/pull/24021)) +- Document editor features for Markdown code formatting ([#23924](https://github.com/astral-sh/ruff/pull/23924)) +- \[`pylint`\] Improve phrasing (`PLC0208`) ([#24033](https://github.com/astral-sh/ruff/pull/24033)) + +### Other changes + +- Use PEP 639 license information ([#19661](https://github.com/astral-sh/ruff/pull/19661)) + +### Contributors + +- [@tmimmanuel](https://github.com/tmimmanuel) +- [@DimitriPapadopoulos](https://github.com/DimitriPapadopoulos) +- [@amyreese](https://github.com/amyreese) +- [@statxc](https://github.com/statxc) +- [@dylwil3](https://github.com/dylwil3) +- [@hunterhogan](https://github.com/hunterhogan) +- [@renovate](https://github.com/renovate) + +## 0.15.8 + +Released on 2026-03-26. + +### Preview features + +- \[`ruff`\] New rule `unnecessary-if` (`RUF050`) ([#24114](https://github.com/astral-sh/ruff/pull/24114)) +- \[`ruff`\] New rule `useless-finally` (`RUF072`) ([#24165](https://github.com/astral-sh/ruff/pull/24165)) +- \[`ruff`\] New rule `f-string-percent-format` (`RUF073`): warn when using `%` operator on an f-string ([#24162](https://github.com/astral-sh/ruff/pull/24162)) +- \[`pyflakes`\] Recognize `frozendict` as a builtin for Python 3.15+ ([#24100](https://github.com/astral-sh/ruff/pull/24100)) + +### Bug fixes + +- \[`flake8-async`\] Use fully-qualified `anyio.lowlevel` import in autofix (`ASYNC115`) ([#24166](https://github.com/astral-sh/ruff/pull/24166)) +- \[`flake8-bandit`\] Check tuple arguments for partial paths in `S607` ([#24080](https://github.com/astral-sh/ruff/pull/24080)) +- \[`pyflakes`\] Skip `undefined-name` (`F821`) for conditionally deleted variables ([#24088](https://github.com/astral-sh/ruff/pull/24088)) +- `E501`/`W505`/formatter: Exclude nested pragma comments from line width calculation ([#24071](https://github.com/astral-sh/ruff/pull/24071)) +- Fix `%foo?` parsing in IPython assignment expressions ([#24152](https://github.com/astral-sh/ruff/pull/24152)) +- `analyze graph`: resolve string imports that reference attributes, not just modules ([#24058](https://github.com/astral-sh/ruff/pull/24058)) + +### Rule changes + +- \[`eradicate`\] ignore `ty: ignore` comments in `ERA001` ([#24192](https://github.com/astral-sh/ruff/pull/24192)) +- \[`flake8-bandit`\] Treat `sys.executable` as trusted input in `S603` ([#24106](https://github.com/astral-sh/ruff/pull/24106)) +- \[`flake8-self`\] Recognize `Self` annotation and `self` assignment in `SLF001` ([#24144](https://github.com/astral-sh/ruff/pull/24144)) +- \[`pyflakes`\] `F507`: Fix false negative for non-tuple RHS in `%`-formatting ([#24142](https://github.com/astral-sh/ruff/pull/24142)) +- \[`refurb`\] Parenthesize generator arguments in `FURB142` fixer ([#24200](https://github.com/astral-sh/ruff/pull/24200)) + +### Performance + +- Speed up diagnostic rendering ([#24146](https://github.com/astral-sh/ruff/pull/24146)) + +### Server + +- Warn when Markdown files are skipped due to preview being disabled ([#24150](https://github.com/astral-sh/ruff/pull/24150)) + +### Documentation + +- Clarify `extend-ignore` and `extend-select` settings documentation ([#24064](https://github.com/astral-sh/ruff/pull/24064)) +- Mention AI policy in PR template ([#24198](https://github.com/astral-sh/ruff/pull/24198)) + +### Other changes + +- Use trusted publishing for NPM packages ([#24171](https://github.com/astral-sh/ruff/pull/24171)) + +### Contributors + +- [@bitloi](https://github.com/bitloi) +- [@Sim-hu](https://github.com/Sim-hu) +- [@mvanhorn](https://github.com/mvanhorn) +- [@chinar-amrutkar](https://github.com/chinar-amrutkar) +- [@markjm](https://github.com/markjm) +- [@RenzoMXD](https://github.com/RenzoMXD) +- [@vivekkhimani](https://github.com/vivekkhimani) +- [@seroperson](https://github.com/seroperson) +- [@moktamd](https://github.com/moktamd) +- [@charliermarsh](https://github.com/charliermarsh) +- [@ntBre](https://github.com/ntBre) +- [@zanieb](https://github.com/zanieb) +- [@dylwil3](https://github.com/dylwil3) +- [@MichaReiser](https://github.com/MichaReiser) + +## 0.15.9 + +Released on 2026-04-02. + +### Preview features + +- \[`pyflakes`\] Flag annotated variable redeclarations as `F811` in preview mode ([#24244](https://github.com/astral-sh/ruff/pull/24244)) +- \[`ruff`\] Allow dunder-named assignments in non-strict mode for `RUF067` ([#24089](https://github.com/astral-sh/ruff/pull/24089)) + +### Bug fixes + +- \[`flake8-errmsg`\] Avoid shadowing existing `msg` in fix for `EM101` ([#24363](https://github.com/astral-sh/ruff/pull/24363)) +- \[`flake8-simplify`\] Ignore pre-initialization references in `SIM113` ([#24235](https://github.com/astral-sh/ruff/pull/24235)) +- \[`pycodestyle`\] Fix `W391` fixes for consecutive empty notebook cells ([#24236](https://github.com/astral-sh/ruff/pull/24236)) +- \[`pyupgrade`\] Fix `UP008` nested class matching ([#24273](https://github.com/astral-sh/ruff/pull/24273)) +- \[`pyupgrade`\] Ignore strings with string-only escapes (`UP012`) ([#16058](https://github.com/astral-sh/ruff/pull/16058)) +- \[`ruff`\] `RUF072`: skip formfeeds on dedent ([#24308](https://github.com/astral-sh/ruff/pull/24308)) +- \[`ruff`\] Avoid re-using symbol in `RUF024` fix ([#24316](https://github.com/astral-sh/ruff/pull/24316)) +- \[`ruff`\] Parenthesize expression in `RUF050` fix ([#24234](https://github.com/astral-sh/ruff/pull/24234)) +- Disallow starred expressions as values of starred expressions ([#24280](https://github.com/astral-sh/ruff/pull/24280)) + +### Rule changes + +- \[`flake8-simplify`\] Suppress `SIM105` for `except*` before Python 3.12 ([#23869](https://github.com/astral-sh/ruff/pull/23869)) +- \[`pyflakes`\] Extend `F507` to flag `%`-format strings with zero placeholders ([#24215](https://github.com/astral-sh/ruff/pull/24215)) +- \[`pyupgrade`\] `UP018` should detect more unnecessarily wrapped literals (UP018) ([#24093](https://github.com/astral-sh/ruff/pull/24093)) +- \[`pyupgrade`\] Fix `UP008` callable scope handling to support lambdas ([#24274](https://github.com/astral-sh/ruff/pull/24274)) +- \[`ruff`\] `RUF010`: Mark fix as unsafe when it deletes a comment ([#24270](https://github.com/astral-sh/ruff/pull/24270)) + +### Formatter + +- Add `nested-string-quote-style` formatting option ([#24312](https://github.com/astral-sh/ruff/pull/24312)) + +### Documentation + +- \[`flake8-bugbear`\] Clarify RUF071 fix safety for non-path string comparisons ([#24149](https://github.com/astral-sh/ruff/pull/24149)) +- \[`flake8-type-checking`\] Clarify import cycle wording for `TC001`/`TC002`/`TC003` ([#24322](https://github.com/astral-sh/ruff/pull/24322)) + +### Other changes + +- Avoid rendering fix lines with trailing whitespace after `|` ([#24343](https://github.com/astral-sh/ruff/pull/24343)) + +### Contributors + +- [@charliermarsh](https://github.com/charliermarsh) +- [@MichaReiser](https://github.com/MichaReiser) +- [@tranhoangtu-it](https://github.com/tranhoangtu-it) +- [@dylwil3](https://github.com/dylwil3) +- [@zsol](https://github.com/zsol) +- [@renovate](https://github.com/renovate) +- [@bitloi](https://github.com/bitloi) +- [@danparizher](https://github.com/danparizher) +- [@chinar-amrutkar](https://github.com/chinar-amrutkar) +- [@second-ed](https://github.com/second-ed) +- [@getehen](https://github.com/getehen) +- [@Redovo1](https://github.com/Redovo1) +- [@matthewlloyd](https://github.com/matthewlloyd) +- [@zanieb](https://github.com/zanieb) +- [@InSyncWithFoo](https://github.com/InSyncWithFoo) +- [@RenzoMXD](https://github.com/RenzoMXD) + +## 0.15.10 + +Released on 2026-04-09. + +### Preview features + +- \[`flake8-logging`\] Allow closures in except handlers (`LOG004`) ([#24464](https://github.com/astral-sh/ruff/pull/24464)) +- \[`flake8-self`\] Make `SLF` diagnostics robust to non-self-named variables ([#24281](https://github.com/astral-sh/ruff/pull/24281)) +- \[`flake8-simplify`\] Make the fix for `collapsible-if` safe in `preview` (`SIM102`) ([#24371](https://github.com/astral-sh/ruff/pull/24371)) + +### Bug fixes + +- Avoid emitting multi-line f-string elements before Python 3.12 ([#24377](https://github.com/astral-sh/ruff/pull/24377)) +- Avoid syntax error from `E502` fixes in f-strings and t-strings ([#24410](https://github.com/astral-sh/ruff/pull/24410)) +- Strip form feeds from indent passed to `dedent_to` ([#24381](https://github.com/astral-sh/ruff/pull/24381)) +- \[`pyupgrade`\] Fix panic caused by handling of octals (`UP012`) ([#24390](https://github.com/astral-sh/ruff/pull/24390)) +- Reject multi-line f-string elements before Python 3.12 ([#24355](https://github.com/astral-sh/ruff/pull/24355)) + +### Rule changes + +- \[`ruff`\] Treat f-string interpolation as potential side effect (`RUF019`) ([#24426](https://github.com/astral-sh/ruff/pull/24426)) + +### Server + +- Add support for custom file extensions ([#24463](https://github.com/astral-sh/ruff/pull/24463)) + +### Documentation + +- Document adding fixes in CONTRIBUTING.md ([#24393](https://github.com/astral-sh/ruff/pull/24393)) +- Fix JSON typo in settings example ([#24517](https://github.com/astral-sh/ruff/pull/24517)) + +### Contributors + +- [@charliermarsh](https://github.com/charliermarsh) +- [@dylwil3](https://github.com/dylwil3) +- [@silverstein](https://github.com/silverstein) +- [@anishgirianish](https://github.com/anishgirianish) +- [@shizukushq](https://github.com/shizukushq) +- [@zanieb](https://github.com/zanieb) +- [@AlexWaygood](https://github.com/AlexWaygood) + +## 0.15.11 + +Released on 2026-04-16. + +### Preview features + +- \[`ruff`\] Ignore `RUF029` when function is decorated with `asynccontextmanager` ([#24642](https://github.com/astral-sh/ruff/pull/24642)) +- \[`airflow`\] Implement `airflow-xcom-pull-in-template-string` (`AIR201`) ([#23583](https://github.com/astral-sh/ruff/pull/23583)) +- \[`flake8-bandit`\] Fix `S103` false positives and negatives in mask analysis ([#24424](https://github.com/astral-sh/ruff/pull/24424)) + +### Bug fixes + +- \[`flake8-async`\] Omit overridden methods for `ASYNC109` ([#24648](https://github.com/astral-sh/ruff/pull/24648)) + +### Documentation + +- \[`flake8-async`\] Add override mention to `ASYNC109` docs ([#24666](https://github.com/astral-sh/ruff/pull/24666)) +- Update Neovim config examples to use `vim.lsp.config` ([#24577](https://github.com/astral-sh/ruff/pull/24577)) + +### Contributors + +- [@augustelalande](https://github.com/augustelalande) +- [@anishgirianish](https://github.com/anishgirianish) +- [@benberryallwood](https://github.com/benberryallwood) +- [@charliermarsh](https://github.com/charliermarsh) +- [@Dev-iL](https://github.com/Dev-iL) + +## 0.15.12 + +Released on 2026-04-24. + +### Preview features + +- Implement `#ruff:file-ignore` file-level suppressions ([#23599](https://github.com/astral-sh/ruff/pull/23599)) +- Implement `#ruff:ignore` logical-line suppressions ([#23404](https://github.com/astral-sh/ruff/pull/23404)) +- Revert preview changes to displayed diagnostic severity in LSP ([#24789](https://github.com/astral-sh/ruff/pull/24789)) +- \[`airflow`\] Implement `task-branch-as-short-circuit` (`AIR004`) ([#23579](https://github.com/astral-sh/ruff/pull/23579)) +- \[`flake8-bugbear`\] Fix `break`/`continue` handling in `loop-iterator-mutation` (`B909`) ([#24440](https://github.com/astral-sh/ruff/pull/24440)) +- \[`pylint`\] Fix `PLC2701` for type parameter scopes ([#24576](https://github.com/astral-sh/ruff/pull/24576)) + +### Rule changes + +- \[`pandas-vet`\] Suggest `.array` as well in `PD011` ([#24805](https://github.com/astral-sh/ruff/pull/24805)) + +### CLI + +- Respect default Unix permissions for cache files ([#24794](https://github.com/astral-sh/ruff/pull/24794)) + +### Documentation + +- \[`pylint`\] Fix `PLR0124` description not to claim self-comparison always returns the same value ([#24749](https://github.com/astral-sh/ruff/pull/24749)) +- \[`pyupgrade`\] Expand docs on reusable `TypeVar`s and scoping (`UP046`) ([#24153](https://github.com/astral-sh/ruff/pull/24153)) +- Improve rules table accessibility ([#24711](https://github.com/astral-sh/ruff/pull/24711)) + +### Contributors + +- [@dylwil3](https://github.com/dylwil3) +- [@AlexWaygood](https://github.com/AlexWaygood) +- [@woodruffw](https://github.com/woodruffw) +- [@avasis-ai](https://github.com/avasis-ai) +- [@Dev-iL](https://github.com/Dev-iL) +- [@denyszhak](https://github.com/denyszhak) +- [@ShipItAndPray](https://github.com/ShipItAndPray) +- [@anishgirianish](https://github.com/anishgirianish) +- [@augustelalande](https://github.com/augustelalande) +- [@amyreese](https://github.com/amyreese) +- [@majiayu000](https://github.com/majiayu000) + +## 0.15.13 + +Released on 2026-05-14. + +### Preview features + +- Add a rule to flag lazy imports that are eagerly evaluated ([#25016](https://github.com/astral-sh/ruff/pull/25016)) +- \[`pylint`\] Standardize diagnostic message (`PLR0914`, `PLR0917`) ([#24996](https://github.com/astral-sh/ruff/pull/24996)) + +### Bug fixes + +- Fix `F811` false positive for class methods ([#24933](https://github.com/astral-sh/ruff/pull/24933)) +- Fix setting selection for multi-folder workspace ([#24819](https://github.com/astral-sh/ruff/pull/24819)) +- \[`eradicate`\] Fix false positive for lines with leading whitespace (`ERA001`) ([#25122](https://github.com/astral-sh/ruff/pull/25122)) +- \[`flake8-pyi`\] Fix false positive for f-string debug specifier (`PYI016`) ([#24098](https://github.com/astral-sh/ruff/pull/24098)) + +### Rule changes + +- Always include panic payload in panic diagnostic message ([#24873](https://github.com/astral-sh/ruff/pull/24873)) +- Restrict `PYI034` for in-place operations to enclosing class ([#24511](https://github.com/astral-sh/ruff/pull/24511)) +- Improve error message for parameters that are declared `global` ([#24902](https://github.com/astral-sh/ruff/pull/24902)) +- Update known stdlib ([#25103](https://github.com/astral-sh/ruff/pull/25103)) + +### Performance + +- \[`isort`\] Avoid constructing `glob::Pattern`s for literal known modules ([#25123](https://github.com/astral-sh/ruff/pull/25123)) + +### CLI + +- Add TOML examples to `--config` help text ([#25013](https://github.com/astral-sh/ruff/pull/25013)) +- Colorize ruff check 'All checks passed' ([#25085](https://github.com/astral-sh/ruff/pull/25085)) + +### Configuration + +- Increase max allowed value of `line-length` setting ([#24962](https://github.com/astral-sh/ruff/pull/24962)) + +### Documentation + +- Add `D203` to rules that conflict with the formatter ([#25044](https://github.com/astral-sh/ruff/pull/25044)) +- Clarify `COM819` and formatter interaction ([#25045](https://github.com/astral-sh/ruff/pull/25045)) +- Clarify that `NotImplemented` is a value, not an exception (`F901`) ([#25054](https://github.com/astral-sh/ruff/pull/25054)) +- Update number of lint rules supported ([#24942](https://github.com/astral-sh/ruff/pull/24942)) + +### Other changes + +- Simplify the playground's markdown template ([#24924](https://github.com/astral-sh/ruff/pull/24924)) + +### Contributors + +- [@MichaReiser](https://github.com/MichaReiser) +- [@brian-c11](https://github.com/brian-c11) +- [@Andrej730](https://github.com/Andrej730) +- [@denyszhak](https://github.com/denyszhak) +- [@darestack](https://github.com/darestack) +- [@sharkdp](https://github.com/sharkdp) +- [@charliermarsh](https://github.com/charliermarsh) +- [@EkriirkE](https://github.com/EkriirkE) +- [@eyupcanakman](https://github.com/eyupcanakman) +- [@Hrk84ya](https://github.com/Hrk84ya) +- [@thernstig](https://github.com/thernstig) +- [@ntBre](https://github.com/ntBre) + +## 0.15.14 + +Released on 2026-05-21. + +### Preview features + +- \[`airflow`\] Implement `airflow-task-implicit-multiple-outputs` (`AIR202`) ([#25152](https://github.com/astral-sh/ruff/pull/25152)) +- \[`flake8-use-pathlib`\] Mark `PTH101` fix as unsafe when first argument is a class attribute annotated as `int` ([#25086](https://github.com/astral-sh/ruff/pull/25086)) +- \[`pylint`\] Implement `too-many-try-statements` (`W0717`) ([#23970](https://github.com/astral-sh/ruff/pull/23970)) +- \[`ruff`\] Add `incorrect-decorator-order` (`RUF074`) ([#23461](https://github.com/astral-sh/ruff/pull/23461)) +- \[`ruff`\] Add `fallible-context-manager` (`RUF075`) ([#22844](https://github.com/astral-sh/ruff/pull/22844)) + +### Bug fixes + +- Fix lambda formatting in interpolated string expressions ([#25144](https://github.com/astral-sh/ruff/pull/25144)) +- Treat generic `frozenset` annotations as immutable ([#25251](https://github.com/astral-sh/ruff/pull/25251)) +- \[`flake8-type-checking`\] Avoid `strict` behavior when `future-annotations` are enabled (`TC001`, `TC002`, `TC003`) ([#25035](https://github.com/astral-sh/ruff/pull/25035)) +- \[`pylint`\] Avoid false positives in `else` clause (`PLR1733`) ([#25177](https://github.com/astral-sh/ruff/pull/25177)) + +### Rule changes + +- \[`flake8-comprehensions`\] Skip `C417` for lambdas with positional-only parameters ([#25272](https://github.com/astral-sh/ruff/pull/25272)) +- \[`flake8-simplify`\] Preserve f-string source verbatim in `SIM101` fix ([#25061](https://github.com/astral-sh/ruff/pull/25061)) + +### Performance + +- Avoid unnecessary parser lookahead for operators ([#25290](https://github.com/astral-sh/ruff/pull/25290)) + +### Documentation + +- Update code example setting Neovim LSP log level ([#25284](https://github.com/astral-sh/ruff/pull/25284)) + +### Other changes + +- Add full PEP 798 support ([#25104](https://github.com/astral-sh/ruff/pull/25104)) +- Add a parser recursion limit ([#24810](https://github.com/astral-sh/ruff/pull/24810)) +- Update various `ruff_python_stdlib` APIs ([#25273](https://github.com/astral-sh/ruff/pull/25273)) + +### Contributors + +- [@ocaballeror](https://github.com/ocaballeror) +- [@lerebear](https://github.com/lerebear) +- [@samuelcolvin](https://github.com/samuelcolvin) +- [@baltasarblanco](https://github.com/baltasarblanco) +- [@aconal-com](https://github.com/aconal-com) +- [@anishgirianish](https://github.com/anishgirianish) +- [@JelleZijlstra](https://github.com/JelleZijlstra) +- [@AlexWaygood](https://github.com/AlexWaygood) +- [@ntBre](https://github.com/ntBre) +- [@adityasingh2400](https://github.com/adityasingh2400) +- [@charliermarsh](https://github.com/charliermarsh) +- [@Dev-iL](https://github.com/Dev-iL) +- [@neutrinoceros](https://github.com/neutrinoceros) +- [@shivamtiwari3](https://github.com/shivamtiwari3) +- [@Dev-X25874](https://github.com/Dev-X25874) + +## 0.15.15 + +Released on 2026-05-28. + +### Preview features + +- Fix Markdown closing fence handling ([#25310](https://github.com/astral-sh/ruff/pull/25310)) +- \[`pyflakes`\] Report duplicate imports in `typing.TYPE_CHECKING` block (`F811`) ([#22560](https://github.com/astral-sh/ruff/pull/22560)) + +### Bug fixes + +- \[`pyflakes`\] Treat function-scope bare annotations as locals per PEP 526 (`F821`) ([#21540](https://github.com/astral-sh/ruff/pull/21540)) + +### Performance + +- Avoid redundant `TokenValue` drops in the lexer ([#25300](https://github.com/astral-sh/ruff/pull/25300)) +- Reduce memory usage by dropping token-excess capacity and improve performance by approximating the initial tokens `Vec` size ([#25354](https://github.com/astral-sh/ruff/pull/25354)) +- Use `ThinVec` in AST to shrink `Stmt` ([#25361](https://github.com/astral-sh/ruff/pull/25361)) + +### Documentation + +- Fix `line-length` example for `--config` option ([#25389](https://github.com/astral-sh/ruff/pull/25389)) +- \[`flake8-comprehensions`\] Document `RecursionError` edge case in `__len__` (`C416`) ([#25286](https://github.com/astral-sh/ruff/pull/25286)) +- \[`mccabe`\] Improve example (`C901`) ([#25287](https://github.com/astral-sh/ruff/pull/25287)) +- \[`pyupgrade`\] Clarify fix safety docs (`UP007`, `UP045`) ([#25288](https://github.com/astral-sh/ruff/pull/25288)) +- \[`refurb`\] Document `FURB192` exception change for empty sequences ([#25317](https://github.com/astral-sh/ruff/pull/25317)) +- \[`ruff`\] Document false negative for user-defined types (`RUF013`) ([#25289](https://github.com/astral-sh/ruff/pull/25289)) + +### Formatter + +- Fix formatting of lambdas nested within f-strings ([#25398](https://github.com/astral-sh/ruff/pull/25398)) + +### Server + +- Return code action for `codeAction/resolve` requests that contain no or no valid URL ([#25365](https://github.com/astral-sh/ruff/pull/25365)) + +### Other changes + +- Expand semantic syntax errors for invalid walruses ([#25415](https://github.com/astral-sh/ruff/pull/25415)) + +### Contributors + +- [@chirizxc](https://github.com/chirizxc) +- [@ntBre](https://github.com/ntBre) +- [@adityasingh2400](https://github.com/adityasingh2400) +- [@charliermarsh](https://github.com/charliermarsh) +- [@fallintoplace](https://github.com/fallintoplace) +- [@martin-schlossarek](https://github.com/martin-schlossarek) +- [@MichaReiser](https://github.com/MichaReiser) +- [@Ruchir28](https://github.com/Ruchir28) + +## 0.15.16 + +Released on 2026-06-04. + +### Preview features + +- \[`flake8-async`\] Implement `yield-in-context-manager-in-async-generator` (`ASYNC119`) ([#24644](https://github.com/astral-sh/ruff/pull/24644)) +- \[`pylint`\] Narrow diagnostic range and exclude cases without exception handlers (`PLW0717`) ([#25440](https://github.com/astral-sh/ruff/pull/25440)) +- \[`ruff`\] Treat `yield` before `break` from a terminal loop as terminal (`RUF075`) ([#25447](https://github.com/astral-sh/ruff/pull/25447)) + +### Bug fixes + +- \[`eradicate`\] Avoid flagging `ruff:ignore` comments as code (`ERA001`) ([#25537](https://github.com/astral-sh/ruff/pull/25537)) +- \[`eradicate`\] Fix `ERA001`/`RUF100` conflict when `noqa` is on commented-out code ([#25414](https://github.com/astral-sh/ruff/pull/25414)) +- \[`pyflakes`\] Avoid removing the `format` call when it would change behavior (`F523`) ([#25320](https://github.com/astral-sh/ruff/pull/25320)) +- \[`pylint`\] Avoid syntax errors in invalid character replacements in f-strings before Python 3.12 (`PLE2510`, `PLE2512`, `PLE2513`, `PLE2514`, `PLE2515`) ([#25544](https://github.com/astral-sh/ruff/pull/25544)) +- \[`pyupgrade`\] Avoid converting `format` calls with more kinds of side effects (`UP032`) ([#25484](https://github.com/astral-sh/ruff/pull/25484)) + +### Rule changes + +- \[`flake8-pytest-style`\] Avoid fixes for ambiguous `argnames` and `argvalues` combinations (`PT006`) ([#24776](https://github.com/astral-sh/ruff/pull/24776)) + +### Performance + +- Drop excess capacity from statement suites during parsing ([#25368](https://github.com/astral-sh/ruff/pull/25368)) + +### Documentation + +- \[`pydocstyle`\] Improve discoverability of rules enabled for each convention ([#24973](https://github.com/astral-sh/ruff/pull/24973)) +- \[`ruff`\] Restore example code for Python versions before 3.15 (`RUF017`) ([#25439](https://github.com/astral-sh/ruff/pull/25439)) +- Fix typo `bin/active` → `bin/activate` in tutorial ([#25473](https://github.com/astral-sh/ruff/pull/25473)) + +### Other changes + +- Shrink additional parser AST collections ([#25465](https://github.com/astral-sh/ruff/pull/25465)) + +### Contributors + +- [@Redslayer112](https://github.com/Redslayer112) +- [@koriyoshi2041](https://github.com/koriyoshi2041) +- [@George-Ogden](https://github.com/George-Ogden) +- [@TejasAmle](https://github.com/TejasAmle) +- [@anishgirianish](https://github.com/anishgirianish) +- [@ntBre](https://github.com/ntBre) +- [@MichaReiser](https://github.com/MichaReiser) +- [@loganrosen](https://github.com/loganrosen) +- [@RafaelJohn9](https://github.com/RafaelJohn9) +- [@adityasingh2400](https://github.com/adityasingh2400) + +## 0.15.17 + +Released on 2026-06-11. + +### Preview features + +- Allow human-readable names in suppression comments ([#25614](https://github.com/astral-sh/ruff/pull/25614)) +- Fix handling of `ignore` comments within a `disable`/`enable` pair ([#25845](https://github.com/astral-sh/ruff/pull/25845)) +- Prioritize human-readable names in CLI output ([#25869](https://github.com/astral-sh/ruff/pull/25869)) +- Respect diagnostic start and parent ranges and trailing comments in `ruff:ignore` suppressions ([#25673](https://github.com/astral-sh/ruff/pull/25673)) +- \[`flake8-async`\] Add `trio.as_safe_channel` to safe decorators (`ASYNC119`) ([#25775](https://github.com/astral-sh/ruff/pull/25775)) +- \[`flake8-pytest-style`\] Also check `pytest_asyncio` fixtures ([#25375](https://github.com/astral-sh/ruff/pull/25375)) +- \[`ruff`\] Ban `pytest` autouse fixtures (`RUF076`) ([#25477](https://github.com/astral-sh/ruff/pull/25477)) +- \[`pyupgrade`\] Add `from __future__ import annotations` automatically (`UP007`, `UP045`) ([#23259](https://github.com/astral-sh/ruff/pull/23259)) + +### Bug fixes + +- Fix diagnostic when `ruff:enable` or `ruff:disable` appears where `ruff:ignore` is expected ([#25700](https://github.com/astral-sh/ruff/pull/25700)) +- \[`pyupgrade`\] Preserve leading empty literals to avoid syntax errors (`UP032`) ([#25491](https://github.com/astral-sh/ruff/pull/25491)) + +### Rule changes + +- \[`flake8-pytest-style`\] Clarify diagnostic message for single parameters (`PT007`) ([#25592](https://github.com/astral-sh/ruff/pull/25592)) +- \[`numpy`\] Drop autofix for `np.in1d` (`NPY201`) ([#25612](https://github.com/astral-sh/ruff/pull/25612)) +- \[`pylint`\] Exempt Python version comparisons (`PLR2004`) ([#25743](https://github.com/astral-sh/ruff/pull/25743)) + +### Performance + +- Reserve AST `Vec`s with correct capacity for common cases ([#25451](https://github.com/astral-sh/ruff/pull/25451)) + +### Formatter + +- Preserve whitespace for Quarto cell option comments ([#25641](https://github.com/astral-sh/ruff/pull/25641)) + +### CLI + +- Allow rule names in `ruff rule` ([#25640](https://github.com/astral-sh/ruff/pull/25640)) + +### Other changes + +- Fix playground diagnostics scrollbars ([#25642](https://github.com/astral-sh/ruff/pull/25642)) + +### Contributors + +- [@SuryanshSS1011](https://github.com/SuryanshSS1011) +- [@anishgirianish](https://github.com/anishgirianish) +- [@romero-deshaw](https://github.com/romero-deshaw) +- [@karlhillx](https://github.com/karlhillx) +- [@carljm](https://github.com/carljm) +- [@ntBre](https://github.com/ntBre) +- [@11happy](https://github.com/11happy) +- [@Kilo59](https://github.com/Kilo59) +- [@oconnor663](https://github.com/oconnor663) +- [@LeonidasZhak](https://github.com/LeonidasZhak) +- [@DavisVaughan](https://github.com/DavisVaughan) +- [@MeGaGiGaGon](https://github.com/MeGaGiGaGon) +- [@jonathandung](https://github.com/jonathandung) +- [@MichaReiser](https://github.com/MichaReiser) +- [@brianmego](https://github.com/brianmego) + +## 0.15.18 + +Released on 2026-06-18. + +### Preview features + +- Handle nested `ruff:ignore` comments ([#25791](https://github.com/astral-sh/ruff/pull/25791)) +- Stop displaying severity in output ([#26050](https://github.com/astral-sh/ruff/pull/26050)) +- Use human-readable names in CLI output ([#25937](https://github.com/astral-sh/ruff/pull/25937)) +- Use human-readable names in LSP and playground diagnostics ([#26058](https://github.com/astral-sh/ruff/pull/26058)) +- \[`pydocstyle`\] Prevent property docstrings starting with verbs (`D421`) ([#23775](https://github.com/astral-sh/ruff/pull/23775)) +- \[`flake8-pyi`\] Extend `PYI033` to Python files ([#26129](https://github.com/astral-sh/ruff/pull/26129)) + +### Bug fixes + +- Detect equivalent numeric mapping keys ([#26009](https://github.com/astral-sh/ruff/pull/26009)) +- Detect mapping keys equivalent to booleans ([#25982](https://github.com/astral-sh/ruff/pull/25982)) +- Detect repeated signed and complex dictionary keys ([#26007](https://github.com/astral-sh/ruff/pull/26007)) + +### Rule changes + +- \[`flake8-pyi`\] Rename `PYI033` to `legacy-type-comment` ([#26131](https://github.com/astral-sh/ruff/pull/26131)) + +### Performance + +- Use `ThinVec` for call keywords ([#25999](https://github.com/astral-sh/ruff/pull/25999)) +- Inline parser recovery context checks ([#26038](https://github.com/astral-sh/ruff/pull/26038)) +- Match parser keywords as bytes ([#26037](https://github.com/astral-sh/ruff/pull/26037)) +- Move value parsing out of lexing ([#25360](https://github.com/astral-sh/ruff/pull/25360)) + +### Server + +- Render subdiagnostics and secondary annotations as related information ([#26011](https://github.com/astral-sh/ruff/pull/26011)) + +### Documentation + +- Update fix availability for always-fixable rules ([#26091](https://github.com/astral-sh/ruff/pull/26091)) +- \[`flake8-tidy-imports`\] Add fix safety section (`TID252`) ([#17491](https://github.com/astral-sh/ruff/pull/17491)) + +### Parser + +- Reject `__debug__` lambda parameters ([#26022](https://github.com/astral-sh/ruff/pull/26022)) +- Reject `_` as a match-pattern target ([#25977](https://github.com/astral-sh/ruff/pull/25977)) +- Reject multiple starred names in sequence patterns ([#25976](https://github.com/astral-sh/ruff/pull/25976)) +- Reject parenthesized star imports ([#26021](https://github.com/astral-sh/ruff/pull/26021)) +- Reject starred comprehension targets ([#26023](https://github.com/astral-sh/ruff/pull/26023)) +- Reject unparenthesized generator expressions in class bases ([#25978](https://github.com/astral-sh/ruff/pull/25978)) +- Reject `yield` expressions after commas ([#26024](https://github.com/astral-sh/ruff/pull/26024)) +- Validate function type parameter default order ([#25981](https://github.com/astral-sh/ruff/pull/25981)) + +### Playground + +- Make diagnostic links clickable ([#26104](https://github.com/astral-sh/ruff/pull/26104)) +- Use diagnostic tags ([#26105](https://github.com/astral-sh/ruff/pull/26105)) + +### Contributors + +- [@AlexWaygood](https://github.com/AlexWaygood) +- [@ntBre](https://github.com/ntBre) +- [@gtkacz](https://github.com/gtkacz) +- [@MichaReiser](https://github.com/MichaReiser) +- [@charliermarsh](https://github.com/charliermarsh) +- [@Kalmaegi](https://github.com/Kalmaegi) + +## 0.15.19 + +Released on 2026-06-23. + +### Preview features + +- Support human-readable names when hovering suppression comments and in code actions ([#26114](https://github.com/astral-sh/ruff/pull/26114)) + +### Bug fixes + +- Fall back to default settings when editor-only settings are invalid ([#26244](https://github.com/astral-sh/ruff/pull/26244)) +- Fix panic when inserting text at a notebook cell boundary ([#26111](https://github.com/astral-sh/ruff/pull/26111)) + +### Rule changes + +- \[`pylint`\] Update fix suggestions for `__floor__`, `__trunc__`, `__length_hint__`, and `__matmul__` variants (`PLC2801`) ([#26239](https://github.com/astral-sh/ruff/pull/26239)) + +### Performance + +- Avoid allocating when parsing single string literals ([#26200](https://github.com/astral-sh/ruff/pull/26200)) +- Avoid reallocating singleton call arguments ([#26223](https://github.com/astral-sh/ruff/pull/26223)) +- Lazily create source files for lint diagnostics ([#26226](https://github.com/astral-sh/ruff/pull/26226)) +- Optimize formatter text width and indentation ([#26236](https://github.com/astral-sh/ruff/pull/26236)) +- Reserve capacity for builtin bindings ([#26229](https://github.com/astral-sh/ruff/pull/26229)) +- Skip repeated-key checks for singleton dictionaries ([#26228](https://github.com/astral-sh/ruff/pull/26228)) +- Use ArrayVec for qualified name segments ([#26224](https://github.com/astral-sh/ruff/pull/26224)) + +### Documentation + +- \[`flake8-pyi`\] Note that `PYI051` is an opinionated stylistic rule ([#26179](https://github.com/astral-sh/ruff/pull/26179)) +- \[`pyupgrade`\] Clarify `UP029` as a Python 2 compatibility rule ([#26243](https://github.com/astral-sh/ruff/pull/26243)) + +### Other changes + +- Publish Ruff crates to crates.io ([#26271](https://github.com/astral-sh/ruff/pull/26271)) + +### Contributors + +- [@MakenRosa](https://github.com/MakenRosa) +- [@MichaReiser](https://github.com/MichaReiser) +- [@trilamsr](https://github.com/trilamsr) +- [@ntBre](https://github.com/ntBre) +- [@sanjibani](https://github.com/sanjibani) +- [@charliermarsh](https://github.com/charliermarsh) + +## 0.15.20 + +Released on 2026-06-25. + +### Preview features + +- Allow human-readable names in rule selectors ([#25887](https://github.com/astral-sh/ruff/pull/25887)) +- Emit a warning instead of an error for unknown rule selectors ([#26113](https://github.com/astral-sh/ruff/pull/26113)) +- Match `noqa` shebang handling in `ruff:ignore` comments ([#26286](https://github.com/astral-sh/ruff/pull/26286)) +- \[`ruff`\] Remove `pytest-fixture-autouse` (`RUF076`) ([#26240](https://github.com/astral-sh/ruff/pull/26240), [#26371](https://github.com/astral-sh/ruff/pull/26371)) + +### Documentation + +- Add versioning sections to custom crate READMEs ([#26317](https://github.com/astral-sh/ruff/pull/26317)) +- Update `ruff_python_parser` README for crates.io ([#26315](https://github.com/astral-sh/ruff/pull/26315)) +- \[`perflint`\] Clarify that `PERF402` applies to any iterable ([#26242](https://github.com/astral-sh/ruff/pull/26242)) + +### Contributors + +- [@dhruvmanila](https://github.com/dhruvmanila) +- [@MichaReiser](https://github.com/MichaReiser) +- [@ntBre](https://github.com/ntBre) +- [@trilamsr](https://github.com/trilamsr) + +## 0.15.21 + +Released on 2026-07-09. + +### Preview features + +- Add `--add-ignore` for adding `ruff:ignore` comments ([#26346](https://github.com/astral-sh/ruff/pull/26346)) +- \[`flake8-comprehensions`\] Drop `C409` tuple comprehension preview behavior ([#25707](https://github.com/astral-sh/ruff/pull/25707)) +- Avoid whitespace normalization when formatting comments ([#26455](https://github.com/astral-sh/ruff/pull/26455)) +- \[`pyupgrade`\] Lint and fix use of deprecated `abc` decorators (`UP051`) ([#26417](https://github.com/astral-sh/ruff/pull/26417)) + +### Bug fixes + +- Refine non-empty f-string detection ([#26526](https://github.com/astral-sh/ruff/pull/26526)) +- Detect syntax errors in individual notebook cells ([#26419](https://github.com/astral-sh/ruff/pull/26419)) +- \[`flake8-implicit-str-concat`\] Fix `ISC003` autofix incorrectly stripping `+` from comments ([#26554](https://github.com/astral-sh/ruff/pull/26554)) + +### Rule changes + +- \[`flake8-executable`\] Mark `EXE004` fix as unsafe ([#26033](https://github.com/astral-sh/ruff/pull/26033)) +- \[`flake8-pyi`\] Mark `PYI061` fixes as unsafe in Python files ([#26533](https://github.com/astral-sh/ruff/pull/26533)) +- \[`pydocstyle`\] Skip `overload-with-docstring` in stub files (`D418`) ([#26318](https://github.com/astral-sh/ruff/pull/26318)) + +### Performance + +- Avoid per-token source index visitor calls ([#26506](https://github.com/astral-sh/ruff/pull/26506)) +- Cache parenthesized expression boundaries in the formatter ([#26344](https://github.com/astral-sh/ruff/pull/26344)) +- Improve performance of rendering edits in preview mode ([#26565](https://github.com/astral-sh/ruff/pull/26565)) +- Inline `fits_element` in formatter ([#26429](https://github.com/astral-sh/ruff/pull/26429)) +- Inline formatter printing hot paths ([#26504](https://github.com/astral-sh/ruff/pull/26504)) +- Lazily create builtin bindings ([#26510](https://github.com/astral-sh/ruff/pull/26510)) +- Skip empty trivia scans in the source indexer ([#26507](https://github.com/astral-sh/ruff/pull/26507)) +- Use ICF for macOS release builds ([#25780](https://github.com/astral-sh/ruff/pull/25780)) + +### Formatter + +- Add `--extend-exclude` to `ruff format` ([#26372](https://github.com/astral-sh/ruff/pull/26372)) + +### Documentation + +- Add "How does Ruff's import sorting compare to isort?" link to README ([#26530](https://github.com/astral-sh/ruff/pull/26530)) +- Fix Mozilla Firefox repository link in README ([#26537](https://github.com/astral-sh/ruff/pull/26537)) +- \[`flake8-bandit`\] Fix misleading docstring for `mako-templates` (`S702`) ([#26432](https://github.com/astral-sh/ruff/pull/26432)) +- \[`ruff`\] Fix non-triggering example for `if-key-in-dict-del` (`RUF051`) ([#26433](https://github.com/astral-sh/ruff/pull/26433)) + +### Contributors + +- [@EkriirkE](https://github.com/EkriirkE) +- [@tingerrr](https://github.com/tingerrr) +- [@s-rigaud](https://github.com/s-rigaud) +- [@nikolauspschuetz](https://github.com/nikolauspschuetz) +- [@Avasam](https://github.com/Avasam) +- [@ntBre](https://github.com/ntBre) +- [@omar-y-abdi](https://github.com/omar-y-abdi) +- [@AlexWaygood](https://github.com/AlexWaygood) +- [@sylvestre](https://github.com/sylvestre) +- [@shaanmajid](https://github.com/shaanmajid) +- [@lerebear](https://github.com/lerebear) +- [@baltasarblanco](https://github.com/baltasarblanco) +- [@Sanjays2402](https://github.com/Sanjays2402) +- [@ZedThree](https://github.com/ZedThree) +- [@servusdei2018](https://github.com/servusdei2018) +- [@charliermarsh](https://github.com/charliermarsh) +- [@jesco-absolute](https://github.com/jesco-absolut) +- [@velikodniy](https://github.com/velikodniy) +- [@zaniebot](https://github.com/zaniebot) +- [@epage](https://github.com/epage) + +## 0.15.22 + +Released on 2026-07-16. + +### Preview features + +- \[`pycodestyle`\] Add an autofix for `E402` ([#22212](https://github.com/astral-sh/ruff/pull/22212)) +- \[`refurb`\] Allow subclassing builtins in stub files (`FURB189`) ([#26812](https://github.com/astral-sh/ruff/pull/26812)) +- \[`ruff`\] Add rule to replace `noqa` comments with `ruff:ignore` (`RUF105`) ([#26423](https://github.com/astral-sh/ruff/pull/26423)) +- \[`ruff`\] Add rule to use human-readable names in `ruff:ignore` comments (`RUF106`) ([#26682](https://github.com/astral-sh/ruff/pull/26682)) +- \[`ruff`\] Add rule to use human-readable names in configuration selectors (`RUF201`) ([#26772](https://github.com/astral-sh/ruff/pull/26772)) + +### Bug fixes + +- \[`flake8-pyi`\] Fix false positive in `__all__` (`PYI053`) ([#26872](https://github.com/astral-sh/ruff/pull/26872)) + +### Rule changes + +- \[`pylint`\] Ignore mutable type updates in `redefined-loop-name` (`PLW2901`) ([#25733](https://github.com/astral-sh/ruff/pull/25733)) + +### Performance + +- Avoid redundant lexer token bookkeeping ([#26765](https://github.com/astral-sh/ruff/pull/26765)) +- Avoid redundant pending-indentation writes ([#26774](https://github.com/astral-sh/ruff/pull/26774)) +- Avoid unnecessary identifier lookahead ([#26525](https://github.com/astral-sh/ruff/pull/26525)) +- Reuse parser scratch buffers ([#26798](https://github.com/astral-sh/ruff/pull/26798)) + +### Documentation + +- Document argfile support ([#26803](https://github.com/astral-sh/ruff/pull/26803)) +- \[`flake8-datetimez`\] Clarify naming guidance for `datetime.today` (`DTZ002`) ([#26658](https://github.com/astral-sh/ruff/pull/26658)) +- \[`pycodestyle`\] Document `E731` fix safety ([#26847](https://github.com/astral-sh/ruff/pull/26847)) +- \[`ruff`\] Clarify intentional async contexts for `unused-async` (`RUF029`) ([#26641](https://github.com/astral-sh/ruff/pull/26641)) + +### Contributors + +- [@dwego](https://github.com/dwego) +- [@MichaReiser](https://github.com/MichaReiser) +- [@Joosboy](https://github.com/Joosboy) +- [@KaufmanDmitriy](https://github.com/KaufmanDmitriy) +- [@PeterJCLaw](https://github.com/PeterJCLaw) +- [@ntBre](https://github.com/ntBre) +- [@charliermarsh](https://github.com/charliermarsh) diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index a1644ec9c1..825c93c37e 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.15.22" +version = "0.16.0" description = "An extremely fast Python linter and code formatter" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff/README.md b/crates/ruff/README.md index f9aa90fe1b..569e763e15 100644 --- a/crates/ruff/README.md +++ b/crates/ruff/README.md @@ -10,7 +10,7 @@ See the [documentation](https://docs.astral.sh/ruff/) or This crate is the entry point to the Ruff command-line interface. The Rust API exposed here is not considered public interface. -This is version 0.15.22. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff). +This is version 0.16.0. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff). The following Ruff workspace members are also available: diff --git a/crates/ruff_annotate_snippets/Cargo.toml b/crates/ruff_annotate_snippets/Cargo.toml index d40beddeec..560501ece5 100644 --- a/crates/ruff_annotate_snippets/Cargo.toml +++ b/crates/ruff_annotate_snippets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_annotate_snippets" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cache/Cargo.toml b/crates/ruff_cache/Cargo.toml index 933e85eae3..a54bfa2828 100644 --- a/crates/ruff_cache/Cargo.toml +++ b/crates/ruff_cache/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cache" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cache/README.md b/crates/ruff_cache/README.md index 2a8e9a65a2..4af95989f4 100644 --- a/crates/ruff_cache/README.md +++ b/crates/ruff_cache/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_cache). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_cache). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_db/Cargo.toml b/crates/ruff_db/Cargo.toml index 757321d855..61874819dc 100644 --- a/crates/ruff_db/Cargo.toml +++ b/crates/ruff_db/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_db" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_db/README.md b/crates/ruff_db/README.md index d755d0a8bd..7b2ed316a7 100644 --- a/crates/ruff_db/README.md +++ b/crates/ruff_db/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_db). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_db). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_diagnostics/Cargo.toml b/crates/ruff_diagnostics/Cargo.toml index 233189ed8e..9421b7233f 100644 --- a/crates/ruff_diagnostics/Cargo.toml +++ b/crates/ruff_diagnostics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_diagnostics" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_diagnostics/README.md b/crates/ruff_diagnostics/README.md index c606f2fa91..2d7b1de2e2 100644 --- a/crates/ruff_diagnostics/README.md +++ b/crates/ruff_diagnostics/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_diagnostics). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_diagnostics). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_formatter/Cargo.toml b/crates/ruff_formatter/Cargo.toml index fd5568e375..1fa86bdf38 100644 --- a/crates/ruff_formatter/Cargo.toml +++ b/crates/ruff_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_formatter" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_formatter/README.md b/crates/ruff_formatter/README.md index 4701c296b6..58590c2812 100644 --- a/crates/ruff_formatter/README.md +++ b/crates/ruff_formatter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_formatter). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_graph/Cargo.toml b/crates/ruff_graph/Cargo.toml index c239f328e8..60454d4910 100644 --- a/crates/ruff_graph/Cargo.toml +++ b/crates/ruff_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_graph" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" edition.workspace = true rust-version.workspace = true diff --git a/crates/ruff_graph/README.md b/crates/ruff_graph/README.md index 6100ed4cc0..d2672061e9 100644 --- a/crates/ruff_graph/README.md +++ b/crates/ruff_graph/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_graph). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_graph). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_index/Cargo.toml b/crates/ruff_index/Cargo.toml index b9817e75b5..5bc2fe3818 100644 --- a/crates/ruff_index/Cargo.toml +++ b/crates/ruff_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_index" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_index/README.md b/crates/ruff_index/README.md index d99bf84ffb..c57ffb1aa0 100644 --- a/crates/ruff_index/README.md +++ b/crates/ruff_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_index). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index c2decabbbc..70234e23f2 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.15.22" +version = "0.16.0" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/README.md b/crates/ruff_linter/README.md index ba776d0d57..06a64f5420 100644 --- a/crates/ruff_linter/README.md +++ b/crates/ruff_linter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.15.22) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_linter). +This version (0.16.0) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_linter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs b/crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs index 53c649def2..967ebbb101 100644 --- a/crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs +++ b/crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs @@ -36,7 +36,7 @@ use ruff_text_size::Ranged; /// collector.create_asset(uri="s3://bucket/key") /// ``` #[derive(ViolationMetadata)] -#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct Airflow3IncompatibleFunctionSignature { function_name: String, change: FunctionSignatureChange, diff --git a/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs b/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs index 14ab6a6be6..0b182c288d 100644 --- a/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs +++ b/crates/ruff_linter/src/rules/flake8_copyright/rules/missing_copyright_notice.rs @@ -20,7 +20,7 @@ use crate::settings::LinterSettings; /// - `lint.flake8-copyright.min-file-size` /// - `lint.flake8-copyright.notice-rgx` #[derive(ViolationMetadata)] -#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct MissingCopyrightNotice; impl Violation for MissingCopyrightNotice { diff --git a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs index bb0eccc844..bf68c8a671 100644 --- a/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_implicit_str_concat/rules/collection_literal.rs @@ -51,7 +51,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// However, the issue is that you may often want to change semantics /// by adding a missing comma. Thus, the fix is always marked as unsafe. #[derive(ViolationMetadata)] -#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct ImplicitStringConcatenationInCollectionLiteral; impl Violation for ImplicitStringConcatenationInCollectionLiteral { diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs index 3ded3afb3c..79d932e7b4 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs @@ -69,7 +69,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// /// [The documentation]: https://docs.python.org/3/library/logging.html#logging.exception #[derive(ViolationMetadata)] -#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct LogExceptionOutsideExceptHandler; impl Violation for LogExceptionOutsideExceptHandler { diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs index 1d8b8bf409..bb45dd993f 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_bool_return.rs @@ -35,7 +35,7 @@ use crate::checkers::ast::Checker; /// ## References /// - [Python documentation: The `__bool__` method](https://docs.python.org/3/reference/datamodel.html#object.__bool__) #[derive(ViolationMetadata)] -#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct InvalidBoolReturnType; impl Violation for InvalidBoolReturnType { diff --git a/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs b/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs index 0e09655130..8dbe97fd9c 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/stop_iteration_return.rs @@ -38,7 +38,7 @@ use crate::checkers::ast::Checker; /// - [PEP 479](https://peps.python.org/pep-0479/) /// - [Python documentation](https://docs.python.org/3/library/exceptions.html#StopIteration) #[derive(ViolationMetadata)] -#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct StopIterationReturn; impl Violation for StopIterationReturn { diff --git a/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs b/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs index f241aa44a2..7620505146 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/too_many_positional_arguments.rs @@ -55,7 +55,7 @@ use crate::checkers::ast::Checker; /// /// [override]: https://docs.python.org/3/library/typing.html#typing.override #[derive(ViolationMetadata)] -#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct TooManyPositionalArguments { c_pos: usize, max_pos: usize, diff --git a/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs b/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs index ed36f55912..a6b25f8c82 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs @@ -52,7 +52,7 @@ use crate::checkers::ast::Checker; /// - [Python documentation: `min`](https://docs.python.org/3/library/functions.html#min) /// - [Python documentation: `max`](https://docs.python.org/3/library/functions.html#max) #[derive(ViolationMetadata)] -#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct SortedMinMax { min_max: MinMax, } diff --git a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs index ec1abc3003..734618392b 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_from_float.rs @@ -61,7 +61,7 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `decimal`](https://docs.python.org/3/library/decimal.html) /// - [Python documentation: `fractions`](https://docs.python.org/3/library/fractions.html) #[derive(ViolationMetadata)] -#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct UnnecessaryFromFloat { method_name: MethodName, constructor: Constructor, diff --git a/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs b/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs index dcf2d218ea..a3ddb2b259 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/access_annotations_from_class_dict.rs @@ -73,7 +73,7 @@ use ruff_text_size::Ranged; /// ## References /// - [Python Annotations Best Practices](https://docs.python.org/3.14/howto/annotations.html) #[derive(ViolationMetadata)] -#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct AccessAnnotationsFromClassDict { python_version: PythonVersion, } diff --git a/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs index faa75a9182..dab957b18f 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs @@ -48,7 +48,7 @@ use crate::{FixAvailability, Violation}; /// ] /// ``` #[derive(ViolationMetadata)] -#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct DuplicateEntryInDunderAll; impl Violation for DuplicateEntryInDunderAll { diff --git a/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs b/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs index 4e359566c8..7d3e9c8609 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/none_not_at_end_of_union.rs @@ -33,7 +33,7 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// - [Python documentation: `typing.Optional`](https://docs.python.org/3/library/typing.html#typing.Optional) /// - [Python documentation: `None`](https://docs.python.org/3/library/constants.html#None) #[derive(ViolationMetadata)] -#[violation_metadata(stable_since = "NEXT_RUFF_VERSION")] +#[violation_metadata(stable_since = "0.16.0")] pub(crate) struct NoneNotAtEndOfUnion; impl Violation for NoneNotAtEndOfUnion { diff --git a/crates/ruff_macros/Cargo.toml b/crates/ruff_macros/Cargo.toml index 3b0320b491..70456d34f7 100644 --- a/crates/ruff_macros/Cargo.toml +++ b/crates/ruff_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_macros" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_macros/README.md b/crates/ruff_macros/README.md index 127dbd027c..3686c4d081 100644 --- a/crates/ruff_macros/README.md +++ b/crates/ruff_macros/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_macros). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_macros). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_markdown/Cargo.toml b/crates/ruff_markdown/Cargo.toml index e7106dcf34..e5c510c59c 100644 --- a/crates/ruff_markdown/Cargo.toml +++ b/crates/ruff_markdown/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_markdown" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ruff_markdown/README.md b/crates/ruff_markdown/README.md index f6007c1178..52234fd0f7 100644 --- a/crates/ruff_markdown/README.md +++ b/crates/ruff_markdown/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_markdown). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_markdown). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_memory_usage/Cargo.toml b/crates/ruff_memory_usage/Cargo.toml index 5758dc8979..6e4befc136 100644 --- a/crates/ruff_memory_usage/Cargo.toml +++ b/crates/ruff_memory_usage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_memory_usage" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_memory_usage/README.md b/crates/ruff_memory_usage/README.md index 75700ae5d3..1717f53d89 100644 --- a/crates/ruff_memory_usage/README.md +++ b/crates/ruff_memory_usage/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_memory_usage). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_memory_usage). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_notebook/Cargo.toml b/crates/ruff_notebook/Cargo.toml index c5216cb7e2..6c073f018f 100644 --- a/crates/ruff_notebook/Cargo.toml +++ b/crates/ruff_notebook/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_notebook" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_notebook/README.md b/crates/ruff_notebook/README.md index 1f01efbdc5..5c0d10bc01 100644 --- a/crates/ruff_notebook/README.md +++ b/crates/ruff_notebook/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_notebook). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_notebook). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_options_metadata/Cargo.toml b/crates/ruff_options_metadata/Cargo.toml index 4e6b438efe..db12da1970 100644 --- a/crates/ruff_options_metadata/Cargo.toml +++ b/crates/ruff_options_metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_options_metadata" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_options_metadata/README.md b/crates/ruff_options_metadata/README.md index 4a425586f4..e41074cf15 100644 --- a/crates/ruff_options_metadata/README.md +++ b/crates/ruff_options_metadata/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_options_metadata). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_options_metadata). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_ast/Cargo.toml b/crates/ruff_python_ast/Cargo.toml index f838d6cc4d..4bf7791f16 100644 --- a/crates/ruff_python_ast/Cargo.toml +++ b/crates/ruff_python_ast/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_ast" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_ast/README.md b/crates/ruff_python_ast/README.md index 246cbdc200..1fc24e18ef 100644 --- a/crates/ruff_python_ast/README.md +++ b/crates/ruff_python_ast/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_ast). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_ast). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_codegen/Cargo.toml b/crates/ruff_python_codegen/Cargo.toml index 14d83eaf54..f59bb46d40 100644 --- a/crates/ruff_python_codegen/Cargo.toml +++ b/crates/ruff_python_codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_codegen" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_codegen/README.md b/crates/ruff_python_codegen/README.md index c998c19eaf..9a9f81bafd 100644 --- a/crates/ruff_python_codegen/README.md +++ b/crates/ruff_python_codegen/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_codegen). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_codegen). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index c7fe7d8f8d..c5eb9472f5 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_formatter" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index af1d9cb289..0bb7b2eb28 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -32,8 +32,8 @@ Head to [The Ruff Formatter](https://docs.astral.sh/ruff/formatter/) for usage i This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_formatter). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_importer/Cargo.toml b/crates/ruff_python_importer/Cargo.toml index 2f65a28a62..9da5d1f818 100644 --- a/crates/ruff_python_importer/Cargo.toml +++ b/crates/ruff_python_importer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_importer" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_importer/README.md b/crates/ruff_python_importer/README.md index 594ef8b98b..f8a888d0a4 100644 --- a/crates/ruff_python_importer/README.md +++ b/crates/ruff_python_importer/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_importer). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_importer). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_index/Cargo.toml b/crates/ruff_python_index/Cargo.toml index 8278131bbd..1e5fd74c58 100644 --- a/crates/ruff_python_index/Cargo.toml +++ b/crates/ruff_python_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_index" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_index/README.md b/crates/ruff_python_index/README.md index 0a11540f61..3d7881599b 100644 --- a/crates/ruff_python_index/README.md +++ b/crates/ruff_python_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_index). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_literal/Cargo.toml b/crates/ruff_python_literal/Cargo.toml index 1fd8bebb7f..63dcc0a110 100644 --- a/crates/ruff_python_literal/Cargo.toml +++ b/crates/ruff_python_literal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_literal" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_literal/README.md b/crates/ruff_python_literal/README.md index afdcd0368b..2e2d89c756 100644 --- a/crates/ruff_python_literal/README.md +++ b/crates/ruff_python_literal/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_literal). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_literal). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_parser/Cargo.toml b/crates/ruff_python_parser/Cargo.toml index aebe2f1ae3..fae77341b7 100644 --- a/crates/ruff_python_parser/Cargo.toml +++ b/crates/ruff_python_parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_parser" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_parser/README.md b/crates/ruff_python_parser/README.md index e8a07ae1e2..33acbf61cc 100644 --- a/crates/ruff_python_parser/README.md +++ b/crates/ruff_python_parser/README.md @@ -19,8 +19,8 @@ Refer to the [contributing guidelines](./CONTRIBUTING.md) to get started and Git This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_parser). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_parser). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_semantic/Cargo.toml b/crates/ruff_python_semantic/Cargo.toml index ed753c2ed0..0b50dd1a01 100644 --- a/crates/ruff_python_semantic/Cargo.toml +++ b/crates/ruff_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_semantic" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_semantic/README.md b/crates/ruff_python_semantic/README.md index bd19e639c7..3c8a5c4540 100644 --- a/crates/ruff_python_semantic/README.md +++ b/crates/ruff_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_semantic). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_stdlib/Cargo.toml b/crates/ruff_python_stdlib/Cargo.toml index 61e9b6309f..889fb2e14a 100644 --- a/crates/ruff_python_stdlib/Cargo.toml +++ b/crates/ruff_python_stdlib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_stdlib" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_stdlib/README.md b/crates/ruff_python_stdlib/README.md index 720253982a..4609d18a54 100644 --- a/crates/ruff_python_stdlib/README.md +++ b/crates/ruff_python_stdlib/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_stdlib). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_stdlib). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_trivia/Cargo.toml b/crates/ruff_python_trivia/Cargo.toml index e00ccd86b4..a765f27a86 100644 --- a/crates/ruff_python_trivia/Cargo.toml +++ b/crates/ruff_python_trivia/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_trivia" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_trivia/README.md b/crates/ruff_python_trivia/README.md index ea990d2d33..e587085bc7 100644 --- a/crates/ruff_python_trivia/README.md +++ b/crates/ruff_python_trivia/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_python_trivia). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_trivia). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_ranged_value/Cargo.toml b/crates/ruff_ranged_value/Cargo.toml index df3e113867..69fb3ef962 100644 --- a/crates/ruff_ranged_value/Cargo.toml +++ b/crates/ruff_ranged_value/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_ranged_value" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_ranged_value/README.md b/crates/ruff_ranged_value/README.md index eb36cea302..cbcb301578 100644 --- a/crates/ruff_ranged_value/README.md +++ b/crates/ruff_ranged_value/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_ranged_value). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_ranged_value). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index 808f812876..fc48939e38 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_server" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_server/README.md b/crates/ruff_server/README.md index 9f1ed8c62f..47f7c363d0 100644 --- a/crates/ruff_server/README.md +++ b/crates/ruff_server/README.md @@ -24,8 +24,8 @@ You can also join us on [**Discord**](https://discord.com/invite/astral-sh). This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_server). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_server). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_source_file/Cargo.toml b/crates/ruff_source_file/Cargo.toml index b713313d5a..602e059d62 100644 --- a/crates/ruff_source_file/Cargo.toml +++ b/crates/ruff_source_file/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_source_file" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_source_file/README.md b/crates/ruff_source_file/README.md index 693ee2da7a..9f71dd12a9 100644 --- a/crates/ruff_source_file/README.md +++ b/crates/ruff_source_file/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_source_file). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_source_file). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_text_size/Cargo.toml b/crates/ruff_text_size/Cargo.toml index fcd8305cde..6a840ea237 100644 --- a/crates/ruff_text_size/Cargo.toml +++ b/crates/ruff_text_size/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_text_size" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_text_size/README.md b/crates/ruff_text_size/README.md index 21887ec343..5d9cfabf2f 100644 --- a/crates/ruff_text_size/README.md +++ b/crates/ruff_text_size/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_text_size). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_text_size). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index f7c7ac1871..cf0d777f6d 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.15.22" +version = "0.16.0" description = "WebAssembly bindings for Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/README.md b/crates/ruff_wasm/README.md index 1c2e7532fa..27ae8a6804 100644 --- a/crates/ruff_wasm/README.md +++ b/crates/ruff_wasm/README.md @@ -55,8 +55,8 @@ const formatted = workspace.format(exampleDocument); This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.15.22) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_wasm). +This version (0.16.0) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_wasm). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_workspace/Cargo.toml b/crates/ruff_workspace/Cargo.toml index 7aaae335ca..6296e2c2e6 100644 --- a/crates/ruff_workspace/Cargo.toml +++ b/crates/ruff_workspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_workspace" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_workspace/README.md b/crates/ruff_workspace/README.md index 6a9c1eb472..db10bd6d95 100644 --- a/crates/ruff_workspace/README.md +++ b/crates/ruff_workspace/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ruff_workspace). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_workspace). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_combine/Cargo.toml b/crates/ty_combine/Cargo.toml index acafd2457a..9dd6d17dfe 100644 --- a/crates/ty_combine/Cargo.toml +++ b/crates/ty_combine/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_combine" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" edition.workspace = true rust-version.workspace = true diff --git a/crates/ty_combine/README.md b/crates/ty_combine/README.md index 6542e860cc..b212ed1bd2 100644 --- a/crates/ty_combine/README.md +++ b/crates/ty_combine/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ty_combine). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ty_combine). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_module_resolver/Cargo.toml b/crates/ty_module_resolver/Cargo.toml index d75f8c4548..268067e6b8 100644 --- a/crates/ty_module_resolver/Cargo.toml +++ b/crates/ty_module_resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_module_resolver" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_module_resolver/README.md b/crates/ty_module_resolver/README.md index 3c34b69455..8bbf6015f5 100644 --- a/crates/ty_module_resolver/README.md +++ b/crates/ty_module_resolver/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ty_module_resolver). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ty_module_resolver). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_core/Cargo.toml b/crates/ty_python_core/Cargo.toml index 6f6240c877..9703595f66 100644 --- a/crates/ty_python_core/Cargo.toml +++ b/crates/ty_python_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_python_core" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_python_core/README.md b/crates/ty_python_core/README.md index 7b9226b17f..916d238fa5 100644 --- a/crates/ty_python_core/README.md +++ b/crates/ty_python_core/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ty_python_core). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ty_python_core). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml index 15df264183..8ddb877472 100644 --- a/crates/ty_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_python_semantic" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_python_semantic/README.md b/crates/ty_python_semantic/README.md index 0e52fd63d6..ab169b6860 100644 --- a/crates/ty_python_semantic/README.md +++ b/crates/ty_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ty_python_semantic). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ty_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_site_packages/Cargo.toml b/crates/ty_site_packages/Cargo.toml index 65dae5d5c4..27b7a649fb 100644 --- a/crates/ty_site_packages/Cargo.toml +++ b/crates/ty_site_packages/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_site_packages" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_site_packages/README.md b/crates/ty_site_packages/README.md index 935980dcbf..c133994592 100644 --- a/crates/ty_site_packages/README.md +++ b/crates/ty_site_packages/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ty_site_packages). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ty_site_packages). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_static/Cargo.toml b/crates/ty_static/Cargo.toml index dd649170e8..68f1627d0b 100644 --- a/crates/ty_static/Cargo.toml +++ b/crates/ty_static/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_static" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ty_static/README.md b/crates/ty_static/README.md index 34c43bf209..93fd1c573c 100644 --- a/crates/ty_static/README.md +++ b/crates/ty_static/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.5) is a component of [Ruff 0.15.22](https://crates.io/crates/ruff/0.15.22). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.15.22/crates/ty_static). +This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ty_static). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_vendored/Cargo.toml b/crates/ty_vendored/Cargo.toml index 2ba738ffc7..405cb0c71c 100644 --- a/crates/ty_vendored/Cargo.toml +++ b/crates/ty_vendored/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_vendored" -version = "0.0.5" +version = "0.0.6" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/docs/formatter.md b/docs/formatter.md index c03343fe26..6071b14bda 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -304,7 +304,7 @@ support needs to be explicitly included by adding it to `types_or`: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.22 + rev: v0.16.0 hooks: - id: ruff-format types_or: [python, pyi, jupyter, markdown] diff --git a/docs/integrations.md b/docs/integrations.md index 2645d5ab7a..25f1f6e418 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.15.22-alpine + name: ghcr.io/astral-sh/ruff:0.16.0-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.15.22 + rev: v0.16.0 hooks: # Run the linter. - id: ruff-check @@ -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.15.22 + rev: v0.16.0 hooks: # Run the linter. - id: ruff-check @@ -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.15.22 + rev: v0.16.0 hooks: # Run the linter. - id: ruff-check diff --git a/docs/tutorial.md b/docs/tutorial.md index 0736dcceb4..655d825a20 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -372,7 +372,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.15.22 + rev: v0.16.0 hooks: # Run the linter. - id: ruff-check diff --git a/pyproject.toml b/pyproject.toml index fff314f51c..d699d47859 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.15.22" +version = "0.16.0" 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 72d3f15a68..530e00ead8 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.15.22" +version = "0.16.0" description = "" authors = ["Charles Marsh "] diff --git a/uv.lock b/uv.lock index 0c4222d46f..76c49dd971 100644 --- a/uv.lock +++ b/uv.lock @@ -30,8 +30,8 @@ name = "anyio" version = "4.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "idna", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version == '3.12.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } wheels = [ @@ -43,7 +43,7 @@ name = "anysqlite" version = "0.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, + { name = "anyio", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/4b/cd5d66b9f87e773bc71344a368b9472987e33514e6627e28342b9c3e7c43/anysqlite-0.0.5.tar.gz", hash = "sha256:9dfcf87baf6b93426ad1d9118088c41dbf24ef01b445eea4a5d486bac2755cce", size = 3432, upload-time = "2023-10-02T13:49:25.135Z" } wheels = [ @@ -64,7 +64,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "python_full_version >= '3.12' and implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -176,11 +176,11 @@ name = "hishel" version = "0.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "anysqlite" }, - { name = "httpx" }, - { name = "msgpack" }, - { name = "typing-extensions" }, + { name = "anyio", marker = "python_full_version >= '3.12'" }, + { name = "anysqlite", marker = "python_full_version >= '3.12'" }, + { name = "httpx", marker = "python_full_version >= '3.12'" }, + { name = "msgpack", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/64/a104ccac48f123f853254483617b16e0efc1649bd7e35bcdc5a5a5ef0ae2/hishel-0.1.5.tar.gz", hash = "sha256:9d40c682cd94fd6e1394fb05713ae20a75ed8aeba6f5272380444039ce6257f2", size = 75468, upload-time = "2025-10-18T13:32:41.854Z" } wheels = [ @@ -192,8 +192,8 @@ name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "h11" }, + { name = "certifi", marker = "python_full_version >= '3.12'" }, + { name = "h11", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ @@ -205,10 +205,10 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, + { name = "anyio", marker = "python_full_version >= '3.12'" }, + { name = "certifi", marker = "python_full_version >= '3.12'" }, + { name = "httpcore", marker = "python_full_version >= '3.12'" }, + { name = "idna", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ @@ -229,7 +229,7 @@ name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mdurl" }, + { name = "mdurl", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ @@ -374,10 +374,10 @@ name = "pydantic" version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "annotated-types", marker = "python_full_version >= '3.12'" }, + { name = "pydantic-core", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ @@ -389,7 +389,7 @@ name = "pydantic-core" version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ @@ -519,7 +519,7 @@ name = "pygit2" version = "1.19.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi" }, + { name = "cffi", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/44/415aa93422b4bfc21a6448acb7e16280d5f33a9a3fae38a384e37b046ae4/pygit2-1.19.3.tar.gz", hash = "sha256:a543e6d4ebb43825564935758dc234e770016fed673b84370d46ae9580558831", size = 810489, upload-time = "2026-06-13T08:06:04.982Z" } wheels = [ @@ -594,8 +594,8 @@ name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, + { name = "markdown-it-py", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ @@ -607,14 +607,14 @@ name = "rooster" version = "0.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "hishel" }, - { name = "httpx" }, - { name = "marko" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pygit2" }, - { name = "tqdm" }, - { name = "typer" }, + { name = "hishel", marker = "python_full_version >= '3.12'" }, + { name = "httpx", marker = "python_full_version >= '3.12'" }, + { name = "marko", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pydantic", marker = "python_full_version >= '3.12'" }, + { name = "pygit2", marker = "python_full_version >= '3.12'" }, + { name = "tqdm", marker = "python_full_version >= '3.12'" }, + { name = "typer", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/02/8ce565271dc52bd0d0d812043b12ec60111d947f81dc30301d19d7bfd453/rooster-0.1.1.tar.gz", hash = "sha256:c9823122f0c2b035985e70384323cdd353477af988e0f065bc302646a49da482", size = 18608, upload-time = "2025-10-29T15:18:49.478Z" } wheels = [ @@ -623,7 +623,7 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.22" +version = "0.16.0" source = { editable = "." } [package.dev-dependencies] @@ -654,7 +654,7 @@ name = "tqdm" version = "4.68.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } wheels = [ @@ -666,10 +666,10 @@ name = "typer" version = "0.26.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "rich" }, - { name = "shellingham" }, + { name = "annotated-doc", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "rich", marker = "python_full_version >= '3.12'" }, + { name = "shellingham", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } wheels = [ @@ -690,7 +690,7 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ From 52d5c6155394d2cdcdcd1a6747c8a1682c0f2dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9r=C3=A8?= Date: Thu, 23 Jul 2026 12:22:30 -0700 Subject: [PATCH 039/390] [ty] Parse NumPy docstrings for parameter docs (#25924) ## Summary This refactors how we extract parameter documentation from NumPy-style docstrings. It replaces the existing regex-based scanner in `ty_ide/src/docstring.rs` with a parser that should strictly improve parameter documentation extraction along the following dimensions: - We will now recognize parameters from `Other Parameters` sections, where previously we only recognized parameters from `Parameters` sections. - We now respect PEP 257 indentation and container boundaries, thereby ignoring parameter-like text that appears inside Markdown fences/lists, doctests, and reStructuredText directives, field lists, and literal blocks. - We now recognize continuation prose and intentional blank lines in parameter documentation more consistently. - We now extract docs for all parameters in a comma-separated list. In addition to those immediate improvements, this new section visitor recognizes `Attributes`, `Returns`, `Yields`, and `Raises` sections and provides the source text range tracking that will allow us to render NumPy-style docstrings as Markdown in an [upcoming change](https://github.com/astral-sh/ruff/pull/25925). Please note that I have intentionally biased this parser towards successfully parsing shapes that were actually found in the wild during a corpus review of popular public repositories that use NumPy-style docstrings. As such, there are theoretically possible shapes that we do not bother to support because they are very unlikely to occur in real docstrings. I think this is an acceptable compromise in favour of maintainability. ## Test Plan See included tests. --- crates/ty_ide/src/docstring.rs | 188 +-- crates/ty_ide/src/docstring/document.rs | 8 +- crates/ty_ide/src/docstring/document/numpy.rs | 1382 +++++++++++++++++ .../src/docstring/document/preformatted.rs | 7 + .../ty_ide/src/docstring/document/syntax.rs | 12 +- 5 files changed, 1401 insertions(+), 196 deletions(-) create mode 100644 crates/ty_ide/src/docstring/document/numpy.rs diff --git a/crates/ty_ide/src/docstring.rs b/crates/ty_ide/src/docstring.rs index f61a733138..b434b506e5 100644 --- a/crates/ty_ide/src/docstring.rs +++ b/crates/ty_ide/src/docstring.rs @@ -10,20 +10,11 @@ mod document; mod markdown; use indexmap::IndexMap; -use regex::Regex; use ruff_python_trivia::{PythonWhitespace, expand_tabs, leading_indentation}; use ruff_source_file::UniversalNewlines; -use std::sync::LazyLock; use crate::MarkupKind; -static NUMPY_SECTION_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(r"(?i)^\s*Parameters\s*$").expect("NumPy section regex should be valid") -}); - -static NUMPY_UNDERLINE_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"^\s*-+\s*$").expect("NumPy underline regex should be valid")); - /// A docstring which hasn't yet been interpreted or rendered /// /// Used to ensure handlers of docstrings select a rendering mode. @@ -59,7 +50,7 @@ impl Docstring { /// Returns a map of parameter names to their documentation. pub fn parameter_documentation(&self) -> IndexMap { let normalized_source = documentation_trim(&self.0); - document::parameter_documentation(&normalized_source, extract_numpy_style_params(&self.0)) + document::parameter_documentation(&normalized_source) } } @@ -156,183 +147,6 @@ fn documentation_trim(docs: &str) -> String { output } -/// Calculate the indentation level of a line. -/// -/// Based on python's expandtabs (where tabs are considered 8 spaces). -fn get_indentation_level(line: &str) -> usize { - leading_indentation(line) - .chars() - .map(|s| if s == '\t' { 8 } else { 1 }) - .sum() -} - -/// Extract parameter documentation from NumPy-style docstrings. -fn extract_numpy_style_params(docstring: &str) -> IndexMap { - let mut param_docs = IndexMap::new(); - - let mut lines = docstring - .universal_newlines() - .map(|line| line.as_str()) - .peekable(); - let mut in_params_section = false; - let mut found_underline = false; - let mut current_param: Option = None; - let mut current_doc = String::new(); - let mut base_param_indent: Option = None; - let mut base_content_indent: Option = None; - - while let Some(line) = lines.next() { - if NUMPY_SECTION_REGEX.is_match(line) { - // Check if the next line is an underline - if let Some(next_line) = lines.peek() { - if NUMPY_UNDERLINE_REGEX.is_match(next_line) { - in_params_section = true; - found_underline = false; - base_param_indent = None; - base_content_indent = None; - continue; - } - } - } - - if in_params_section && !found_underline { - if NUMPY_UNDERLINE_REGEX.is_match(line) { - found_underline = true; - continue; - } - } - - if in_params_section && found_underline { - let current_indent = get_indentation_level(line); - let trimmed = line.trim(); - - // Skip empty lines - if trimmed.is_empty() { - continue; - } - - // Check if we hit another section - if current_indent == 0 { - if let Some(next_line) = lines.peek() { - if NUMPY_UNDERLINE_REGEX.is_match(next_line) { - // This is another section - if let Some(param_name) = current_param.take() { - param_docs.insert(param_name, current_doc.trim().to_string()); - current_doc.clear(); - } - in_params_section = false; - continue; - } - } - } - - // Determine if this could be a parameter line - let could_be_param = if let Some(base_indent) = base_param_indent { - // We've seen parameters before - check if this matches the expected parameter indentation - current_indent == base_indent - } else { - // First potential parameter - check if it has reasonable indentation and content - current_indent > 0 - && (trimmed.contains(':') - || trimmed.chars().all(|c| c.is_alphanumeric() || c == '_')) - }; - - if could_be_param { - // Check if this could be a section header by looking at the next line - if let Some(next_line) = lines.peek() { - if NUMPY_UNDERLINE_REGEX.is_match(next_line) { - // This is a section header, not a parameter - if let Some(param_name) = current_param.take() { - param_docs.insert(param_name, current_doc.trim().to_string()); - current_doc.clear(); - } - in_params_section = false; - continue; - } - } - - // Set base indentation levels on first parameter - if base_param_indent.is_none() { - base_param_indent = Some(current_indent); - } - - // Handle parameter with type annotation (param : type) - if trimmed.contains(':') { - // Save previous parameter if exists - if let Some(param_name) = current_param.take() { - param_docs.insert(param_name, current_doc.trim().to_string()); - current_doc.clear(); - } - - // Extract parameter name and description - let parts: Vec<&str> = trimmed.splitn(2, ':').collect(); - if parts.len() == 2 { - let param_name = parts[0].trim(); - - // Extract just the parameter name (before any type info) - let param_name = param_name.split_whitespace().next().unwrap_or(param_name); - current_param = Some(param_name.to_string()); - current_doc.clear(); // Description comes on following lines, not on this line - } - } else { - // Handle parameter without type annotation - // Save previous parameter if exists - if let Some(param_name) = current_param.take() { - param_docs.insert(param_name, current_doc.trim().to_string()); - current_doc.clear(); - } - - // This line is the parameter name - current_param = Some(trimmed.to_string()); - current_doc.clear(); - } - } else if current_param.is_some() { - // Determine if this is content for the current parameter - let is_content = if let Some(base_content) = base_content_indent { - // We've seen content before - check if this matches expected content indentation - current_indent >= base_content - } else { - // First potential content line - should be more indented than parameter - if let Some(base_param) = base_param_indent { - current_indent > base_param - } else { - // Fallback: any indented content - current_indent > 0 - } - }; - - if is_content { - // Set base content indentation on first content line - if base_content_indent.is_none() { - base_content_indent = Some(current_indent); - } - - // This is a continuation of the current parameter documentation - if !current_doc.is_empty() { - current_doc.push('\n'); - } - current_doc.push_str(trimmed); - } else { - // This line doesn't match our expected indentation patterns - // Save current parameter and stop processing - if let Some(param_name) = current_param.take() { - param_docs.insert(param_name, current_doc.trim().to_string()); - current_doc.clear(); - } - in_params_section = false; - } - } - } - } - - // Don't forget the last parameter - if let Some(param_name) = current_param { - param_docs.insert(param_name, current_doc.trim().to_string()); - } - - param_docs -} - #[cfg(test)] mod tests { use insta::Settings; diff --git a/crates/ty_ide/src/docstring/document.rs b/crates/ty_ide/src/docstring/document.rs index 3a233e222d..3ad3ce3314 100644 --- a/crates/ty_ide/src/docstring/document.rs +++ b/crates/ty_ide/src/docstring/document.rs @@ -5,6 +5,7 @@ use strum_macros::EnumIter; use self::syntax::{indentation, starts_with_markdown_list_item}; pub(super) mod google; +mod numpy; pub(super) mod preformatted; pub(super) mod rst; pub(in crate::docstring) mod syntax; @@ -13,12 +14,9 @@ pub(in crate::docstring) mod syntax; /// /// `normalized_source` must have already undergone PEP-257 trimming and universal newline /// normalization. -pub(super) fn parameter_documentation( - normalized_source: &str, - numpy_parameters: IndexMap, -) -> IndexMap { +pub(super) fn parameter_documentation(normalized_source: &str) -> IndexMap { let mut parameters = google::parameter_documentation(normalized_source); - parameters.extend(numpy_parameters); + parameters.extend(numpy::parameter_documentation(normalized_source)); parameters.extend(rst::parameter_documentation(normalized_source)); parameters } diff --git a/crates/ty_ide/src/docstring/document/numpy.rs b/crates/ty_ide/src/docstring/document/numpy.rs new file mode 100644 index 0000000000..f303a629da --- /dev/null +++ b/crates/ty_ide/src/docstring/document/numpy.rs @@ -0,0 +1,1382 @@ +//! Parsing for NumPy-style docstring sections. +//! +//! The [numpydoc style guide](https://numpydoc.readthedocs.io/en/latest/format.html) +//! organizes documentation into sections whose headings are underlined with hyphens. Item-oriented +//! sections conventionally use a `name : type` line followed by an indented description. This +//! parser recognizes `Parameters`, `Other Parameters`, `Attributes`, `Returns`, `Yields`, and +//! `Raises`. +//! +//! Example: +//! +//! ```text +//! Compute the mean of a sequence. +//! +//! Parameters +//! ---------- +//! values : sequence of float +//! Values to average. +//! axis : int, optional +//! Axis along which to compute the mean. +//! +//! Returns +//! ------- +//! float +//! The arithmetic mean. +//! ``` + +use indexmap::IndexMap; +use ruff_text_size::{TextRange, TextSize}; + +use super::preformatted::{PreformattedBlockScanner, starts_preformatted_block}; +use super::syntax::{ + ParsedLine, container_block_end, is_dotted_identifier, is_markdown_code_span, parsed_lines, + split_once_at_top_level_colon, starts_container_block, +}; +use super::{DescriptionBuilder, HeaderKind, SectionKind}; + +/// Returns parameter documentation from recognized NumPy-style parameter sections. +/// +/// `normalized_source` must have already undergone PEP-257 trimming and universal newline +/// normalization. +pub(super) fn parameter_documentation(normalized_source: &str) -> IndexMap { + let mut parameters = Parameters::default(); + + for section in sections(normalized_source) { + let Section { + kind, + range: _, + body, + } = section; + if matches!(kind, SectionKind::Parameters | SectionKind::OtherParameters) { + parameters.extend_fragments(body.into_fragments()); + } + } + + parameters.into_inner() +} + +#[derive(Default)] +struct Parameters(IndexMap); + +impl Parameters { + fn extend_fragments(&mut self, fragments: Vec) { + for fragment in fragments { + let BodyFragment::Item(item) = fragment else { + continue; + }; + let Item { + display_name, + ty: _, + description, + } = item; + let Some(display_name) = display_name else { + continue; + }; + let description = description.trim(); + if description.is_empty() { + continue; + } + let Some(names) = parameter_lookup_names(&display_name) else { + continue; + }; + for name in names { + self.0.insert(name, description.to_string()); + } + } + } + + fn into_inner(self) -> IndexMap { + self.0 + } +} + +fn parameter_lookup_names(display_name: &str) -> Option> { + let mut lookup_names = Vec::new(); + for name in display_name.split(',').map(str::trim) { + if name == "..." { + continue; + } + + if !is_item_name_part(name) { + return None; + } + lookup_names.push(name.to_string()); + } + + (!lookup_names.is_empty()).then_some(lookup_names) +} + +/// Returns recognized NumPy-style sections in source order. +/// +/// `source` must have already undergone PEP-257 trimming and universal newline normalization +/// (typically via `docstring::documentation_trim`). +fn sections(source: &str) -> Vec
{ + Parser::new(parsed_lines(source)).parse() +} + +/// A recognized NumPy-style docstring section. +type Section = super::Section>; + +type SectionBody = super::SectionBody>; + +/// One parsed fragment in a NumPy section body. +type BodyFragment = super::BodyFragment>; + +/// A named or anonymous item in a NumPy section. +type Item = super::Item>; + +struct Parser<'a> { + lines: Vec>, + current_line: usize, + sections: Vec
, + current_section: Option>, + scanner: PreformattedBlockScanner<'a>, +} + +impl<'a> Parser<'a> { + fn new(lines: Vec>) -> Self { + Self { + lines, + current_line: 0, + sections: Vec::new(), + current_section: None, + scanner: PreformattedBlockScanner::default(), + } + } + + fn parse(mut self) -> Vec
{ + while self.current_line < self.lines.len() { + self.parse_line(); + } + + if let Some(section) = self.current_section.take() { + self.finish_section(section); + } + + self.sections + } + + fn parse_line(&mut self) { + let line = self.lines[self.current_line]; + let line_header = self.parse_header(self.current_line); + let index = self.current_line; + self.current_line += 1; + + // First, attempt to add the current line to the current section. + if let Some(mut section) = self.current_section.take() { + if section.push_line(line, line_header, &self.lines[self.current_line..]) { + self.current_section = Some(section); + return; + } + + self.finish_section(section); + } + + // Second, skip content owned by a preformatted or container block, where nested headers + // are inert. + if self.scanner.consume_preformatted_line(line.text) { + return; + } + if let Some(end) = container_block_end(&self.lines, index) { + self.current_line = end; + return; + } + + // Finally, start a new section from a standalone header, or observe syntax that may + // introduce a preformatted block. + if let Some(header) = line_header { + self.current_section = Some(SectionBuilder::new(header)); + self.current_line += 1; + } else { + self.scanner + .observe_line_outside_preformatted_block(line.text); + } + } + + fn parse_header(&self, index: usize) -> Option
{ + let line = self.lines[index]; + let underline = self.lines.get(index + 1)?; + + if line.text.trim().is_empty() || !is_underline(underline.text) { + return None; + } + + let indent = if index == 0 { + // PEP 257 trimming strips the indentation from the first line, + // so instead use the underline to determine this section's indentation. + underline.indent + } else if underline.indent == line.indent { + line.indent + } else { + // After the first line, each underline must align with its section title. + return None; + }; + + Some(Header { + kind: section_kind(line.text) + .map(HeaderKind::Structured) + .unwrap_or(HeaderKind::Opaque), + indent, + range: TextRange::new(line.range.start(), underline.range.end()), + }) + } + + fn finish_section(&mut self, section: SectionBuilder<'a>) { + if let Some(section) = section.finish() { + self.sections.push(section); + } + } +} + +struct SectionBuilder<'a> { + section_header: Header, + range: TextRange, + pending_blank_lines: Vec>, + preformatted: PreformattedBlockScanner<'a>, + has_seen_item_block: bool, + body: BodyBuilder<'a>, +} + +impl<'a> SectionBuilder<'a> { + fn new(section_header: Header) -> Self { + Self { + range: section_header.range, + pending_blank_lines: Vec::new(), + preformatted: PreformattedBlockScanner::default(), + has_seen_item_block: false, + body: BodyBuilder::new(section_header.kind, section_header.indent), + section_header, + } + } + + /// Returns `false` when `line` belongs outside this section. + fn push_line( + &mut self, + line: ParsedLine<'a>, + line_header: Option
, + following_lines: &[ParsedLine<'_>], + ) -> bool { + // Let an active preformatted block consume the line before interpreting it. + let preformatted_block_is_active = self.preformatted.is_active(); + let line_is_preformatted = self.preformatted.consume_preformatted_line(line.text); + if preformatted_block_is_active && line_is_preformatted { + self.push_body_line(line, None); + return true; + } + + // Defer blank lines until the next content line determines their ownership. + if line.text.trim().is_empty() { + self.pending_blank_lines.push(line); + return true; + } + + // Omit a marker for a static substitution from extracted parameter + // documentation, but keep scanning explicit parameters and leave the + // section raw when rendering. + // + // This is an edge case, but static substitutions commonly appear in + // some popular libraries (e.g., SciPy and Matplotlib). + if self.section_header.kind.is_parameter_section() + && line_header.is_none() + && !line_is_preformatted + && is_static_substitution(line, self.section_header.indent) + { + self.push_static_substitution(line); + return true; + } + + // Parse the line as an item and determine whether it belongs to this section. + let item_line = ItemLine::parse(self.section_header, line, following_lines); + let starts_item_block = item_line.is_some(); + let has_leading_blank_lines = !self.pending_blank_lines.is_empty(); + if !self.line_belongs_to_section( + line, + line_header, + starts_item_block, + has_leading_blank_lines, + ) { + return false; + } + + // Finally, commit the accepted line and update the state used to classify later lines. + self.push_body_line(line, item_line); + self.has_seen_item_block |= starts_item_block; + if !line_is_preformatted { + self.preformatted + .observe_line_outside_preformatted_block(line.text); + } + + true + } + + fn line_belongs_to_section( + &self, + line: ParsedLine<'_>, + line_header: Option
, + starts_item_block: bool, + has_leading_blank_lines: bool, + ) -> bool { + // A sibling-level underlined header starts a new section. + // Every section, including an opaque one, ends at a sibling or shallower header. + if line_header.is_some_and(|header| header.indent <= self.section_header.indent) { + return false; + } + + // Items are not parsed in opaque sections so only the above header can end them. + if self.section_header.kind == HeaderKind::Opaque { + return true; + } + + match line.indent.cmp(&self.section_header.indent) { + std::cmp::Ordering::Less => false, + std::cmp::Ordering::Greater => true, + std::cmp::Ordering::Equal => { + if self.section_header.kind.is_parameter_section() { + // Parameter sections may contain leading prose and aligned continuations. + // After an item establishes the list, a blank line followed by an aligned + // non-item ends the section. + !self.has_seen_item_block || !has_leading_blank_lines || starts_item_block + } else { + starts_item_block + } + } + } + } + + fn push_static_substitution(&mut self, line: ParsedLine<'a>) { + self.commit_pending_blank_lines(); + self.range = self.range.cover(line.range); + + if let BodyBuilder::ItemList(builder) = &mut self.body { + // At item indentation, the substitution may expand into more items, so end the + // preceding item. An indented substitution remains within the current description. + if line.indent == self.section_header.indent { + builder.finish_current_item(); + self.has_seen_item_block = true; + } + + // The unknown expansion cannot be reproduced by structured rendering. + builder.has_structural_ambiguity = true; + } + } + + fn commit_pending_blank_lines(&mut self) { + for line in self.pending_blank_lines.drain(..) { + self.range = self.range.cover(line.range); + self.body.push_blank_line(); + } + } + + fn push_body_line(&mut self, line: ParsedLine<'a>, item_line: Option>) { + self.commit_pending_blank_lines(); + self.range = self.range.cover(line.range); + self.body.push_line(line, item_line); + } + + fn finish(self) -> Option
{ + let HeaderKind::Structured(kind) = self.section_header.kind else { + return None; + }; + + Some(Section { + kind, + range: self.range, + body: self.body.finish(), + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Header { + kind: HeaderKind, + indent: TextSize, + range: TextRange, +} + +fn section_kind(line: &str) -> Option { + match line.trim().to_ascii_lowercase().as_str() { + "parameters" => Some(SectionKind::Parameters), + "other parameters" => Some(SectionKind::OtherParameters), + "attributes" => Some(SectionKind::Attributes), + "returns" => Some(SectionKind::Returns), + "yields" => Some(SectionKind::Yields), + "raises" => Some(SectionKind::Raises), + _ => None, + } +} + +fn is_underline(line: &str) -> bool { + let line = line.trim(); + line.len() >= 3 && line.chars().all(|char| char == '-') +} + +/// Recognizes standalone percent- and dollar-style substitutions. +/// +/// Percent substitutions may use any nonempty name without parentheses: +/// +/// ```python +/// "%(name)s" +/// "%(Class:kwdoc)s" +/// ``` +/// +/// Dollar substitutions require a dotted Python identifier: +/// +/// ```python +/// "$name" +/// "${package.name}" +/// ``` +fn is_static_substitution(line: ParsedLine<'_>, section_indent: TextSize) -> bool { + let text = line.text.trim(); + let is_percent_marker = text + .strip_prefix("%(") + .and_then(|line| line.strip_suffix(")s")) + .is_some_and(|name| !name.is_empty() && !name.contains('(') && !name.contains(')')); + let is_dollar_marker = text + .strip_prefix("${") + .and_then(|line| line.strip_suffix('}')) + .or_else(|| text.strip_prefix('$')) + .is_some_and(is_dotted_identifier); + + line.indent >= section_indent && (is_percent_marker || is_dollar_marker) +} + +/// Accepts description-backed items, plus single-token types without one. +fn is_anonymous_return_item(line: &str, has_description: bool) -> bool { + !line.is_empty() + && !line.ends_with(['.', ':']) + && (has_description || !line.chars().any(char::is_whitespace)) +} + +enum BodyBuilder<'a> { + /// A recognized section whose body consists of named items and their descriptions. + ItemList(ItemListBuilder<'a>), + /// An underlined section that participates in boundary detection but is not parsed. + Opaque, +} + +impl<'a> BodyBuilder<'a> { + fn new(kind: HeaderKind, required_item_indent: TextSize) -> Self { + match kind { + HeaderKind::Structured(_) => Self::ItemList(ItemListBuilder::new( + kind.is_parameter_section(), + required_item_indent, + )), + HeaderKind::Opaque => Self::Opaque, + } + } + + fn push_blank_line(&mut self) { + if let Self::ItemList(builder) = self { + builder.push_blank_line(); + } + } + + fn push_line(&mut self, line: ParsedLine<'a>, item_line: Option>) { + if let Self::ItemList(builder) = self { + builder.push_line(line, item_line); + } + } + + fn finish(self) -> SectionBody { + match self { + Self::ItemList(builder) => builder.finish(), + Self::Opaque => SectionBody::Opaque, + } + } +} + +struct ItemListBuilder<'a> { + fragments: Vec, + current_item: Option>, + leading_prose: DescriptionBuilder<'a>, + required_item_indent: TextSize, + preserve_leading_prose: bool, + has_structural_ambiguity: bool, +} + +impl<'a> ItemListBuilder<'a> { + fn new(preserve_leading_prose: bool, required_item_indent: TextSize) -> Self { + Self { + fragments: Vec::new(), + current_item: None, + leading_prose: DescriptionBuilder::default(), + required_item_indent, + preserve_leading_prose, + has_structural_ambiguity: false, + } + } + + fn push_blank_line(&mut self) { + if let Some(item) = &mut self.current_item { + item.description.push_continuation(""); + } else if self.preserve_leading_prose { + self.leading_prose.push_continuation(""); + } + } + + fn push_line(&mut self, line: ParsedLine<'a>, item_line: Option>) { + let is_at_item_indent = line.indent == self.required_item_indent; + + // An item starts a new fragment; its description is collected from later lines. + if let Some(ItemLine { + item, + has_structural_ambiguity, + }) = item_line + { + self.finish_pending_fragments(); + self.current_item = Some(item); + self.has_structural_ambiguity |= has_structural_ambiguity; + return; + } + + // Record when preserving the remaining line as prose or a continuation loses structure. + if is_at_item_indent { + // Aligned prose after an item may instead be another, malformed item. + if self.current_item.is_some() { + self.has_structural_ambiguity = true; + } + } else if self.current_item.is_none() && self.preserve_leading_prose { + // Indented content before the first item may be nested content or code rather than + // section-level prose, so interpreting it as prose could discard meaningful structure. + self.has_structural_ambiguity = true; + } + + // Preserve the line as an item continuation or leading prose when supported. + if let Some(item) = &mut self.current_item { + item.description.push_continuation(line.text); + } else if self.preserve_leading_prose { + self.leading_prose.push_line(line.text); + } else { + self.has_structural_ambiguity = true; + } + } + + fn finish_pending_fragments(&mut self) { + self.finish_leading_prose(); + self.finish_current_item(); + } + + fn finish_leading_prose(&mut self) { + let prose = std::mem::take(&mut self.leading_prose).finish(); + if !prose.is_empty() { + self.fragments.push(BodyFragment::Prose(prose)); + } + } + + fn finish_current_item(&mut self) { + if let Some(item) = self.current_item.take() { + self.fragments.push(BodyFragment::Item(item.finish())); + } + } + + fn finish(mut self) -> SectionBody { + if self.preserve_leading_prose && self.current_item.is_none() && self.fragments.is_empty() { + return SectionBody::Opaque; + } + + self.finish_pending_fragments(); + SectionBody::Parsed { + fragments: self.fragments, + has_structural_ambiguity: self.has_structural_ambiguity, + } + } +} + +struct ItemLine<'a> { + item: ItemBuilder<'a>, + has_structural_ambiguity: bool, +} + +impl<'a> ItemLine<'a> { + fn parse( + section_header: Header, + line: ParsedLine<'a>, + following_lines: &[ParsedLine<'_>], + ) -> Option { + // Only aligned lines can start items. Other lines are prose or item continuations. + if line.indent != section_header.indent { + return None; + } + + // Each structured section has its own item grammar. Opaque sections only delimit content. + let HeaderKind::Structured(kind) = section_header.kind else { + return None; + }; + + match kind { + SectionKind::Parameters + | SectionKind::KeywordArguments + | SectionKind::OtherParameters + | SectionKind::Attributes => Self::parse_named_item(line, following_lines), + SectionKind::Returns | SectionKind::Yields => { + Self::parse_return_item(line, following_lines) + } + SectionKind::Raises => Self::parse_raise_item(line), + } + } + + fn parse_named_item(line: ParsedLine<'a>, following_lines: &[ParsedLine<'_>]) -> Option { + let text = line.text.trim(); + + let Some(separator) = parse_type_separator(text) else { + // Named items may omit their type. + return is_item_name(text) + .then(|| Self::new(ItemBuilder::new(Some(text), None, ""), false)); + }; + + let name_is_valid = is_item_name(separator.name); + let item = ItemBuilder::new(Some(separator.name), Some(separator.ty), ""); + + // Conventional `name : type` syntax establishes an item boundary even when the name is + // invalid, preventing it from absorbing adjacent items. + if separator.has_whitespace_before_colon { + return Some(Self::new(item, !name_is_valid)); + } + + // Compact syntax requires a valid name and either a type or description. + if name_is_valid + && (!separator.ty.is_empty() || has_indented_description(&line, following_lines)) + { + return Some(Self::new(item, separator.has_structural_ambiguity)); + } + + None + } + + fn parse_return_item(line: ParsedLine<'a>, following_lines: &[ParsedLine<'_>]) -> Option { + let text = line.text.trim(); + + // Block openers at item indentation belong outside the section, not to a return item. + if starts_preformatted_block(text) || starts_container_block(text) { + return None; + } + + // A complete code span is an anonymous type even when its contents contain a colon. + if is_markdown_code_span(text) { + return Some(Self::new(ItemBuilder::new(None, Some(text), ""), false)); + } + + // Next, prefer the named `name : type` form. A colon adjacent to the name needs a + // description block to distinguish it from prose. + let Some(separator) = + parse_type_separator(text).filter(|separator| !separator.name.is_empty()) + else { + // Otherwise, accept an anonymous type only when its shape or description + // distinguishes it from prose. + let has_description = has_indented_description(&line, following_lines); + return is_anonymous_return_item(text, has_description) + .then(|| Self::new(ItemBuilder::new(None, Some(text), ""), false)); + }; + + let item = ItemBuilder::new(Some(separator.name), Some(separator.ty), ""); + + // Conventional `name : type` syntax is sufficient on its own. + if separator.has_whitespace_before_colon { + return Some(Self::new(item, separator.has_structural_ambiguity)); + } + + // A compact separator needs a description to distinguish it from prose. + has_indented_description(&line, following_lines) + .then(|| Self::new(item, separator.has_structural_ambiguity)) + } + + fn parse_raise_item(line: ParsedLine<'a>) -> Option { + let text = line.text.trim(); + + // Raises use a named item, with an optional inline description after the first colon. + let (name, description) = text + .split_once(':') + .map_or((text, ""), |(name, description)| { + (name.trim(), description.trim()) + }); + if !is_item_name(name) && !is_markdown_code_span(name) { + return None; + } + + Some(Self::new( + ItemBuilder::new(Some(name), None, description), + false, + )) + } + + fn new(item: ItemBuilder<'a>, has_structural_ambiguity: bool) -> Self { + Self { + item, + has_structural_ambiguity, + } + } +} + +struct ItemBuilder<'a> { + display_name: Option<&'a str>, + ty: Option<&'a str>, + description: DescriptionBuilder<'a>, +} + +impl<'a> ItemBuilder<'a> { + fn new( + display_name: Option<&'a str>, + ty: Option<&'a str>, + inline_description: &'a str, + ) -> Self { + Self { + display_name, + ty, + description: DescriptionBuilder::with_inline(inline_description), + } + } + + fn finish(self) -> Item { + Item { + display_name: self.display_name.map(str::to_string), + ty: self.ty.map(str::to_string), + description: self.description.finish(), + } + } +} + +/// A parsed NumPy-style `name : type` separator. +struct TypeSeparator<'a> { + /// The documented item name. + name: &'a str, + /// The documented item type. + ty: &'a str, + /// Whether whitespace before the colon identifies conventional NumPy item syntax. + has_whitespace_before_colon: bool, + /// Whether the separator omits whitespace on both sides. + has_structural_ambiguity: bool, +} + +/// Parses a NumPy-style `name : type` separator. +fn parse_type_separator(line: &str) -> Option> { + let (name, ty) = split_once_at_top_level_colon(line)?; + let has_whitespace_before_colon = name.ends_with(char::is_whitespace); + let has_whitespace_after_colon = ty.starts_with(char::is_whitespace); + let has_structural_ambiguity = + !has_whitespace_before_colon && !has_whitespace_after_colon && !ty.is_empty(); + + Some(TypeSeparator { + name: name.trim(), + ty: ty.trim(), + has_whitespace_before_colon, + has_structural_ambiguity, + }) +} + +fn has_indented_description(line: &ParsedLine<'_>, following_lines: &[ParsedLine<'_>]) -> bool { + following_lines + .iter() + .find(|line| !line.text.trim().is_empty()) + .is_some_and(|next| next.indent > line.indent) +} + +/// Returns whether `name` is a valid NumPy-style item name or comma-separated name list. +fn is_item_name(name: &str) -> bool { + let mut has_lookup_name = false; + + for part in name.split(',') { + let part = part.trim(); + if part == "..." { + continue; + } + + if !is_item_name_part(part) { + return false; + } + + has_lookup_name = true; + } + + has_lookup_name +} + +fn is_item_name_part(name: &str) -> bool { + let name = name + .strip_prefix("**") + .or_else(|| name.strip_prefix('*')) + .unwrap_or(name); + + is_dotted_identifier(name) +} + +#[cfg(test)] +mod tests { + use insta::assert_snapshot; + use itertools::Itertools; + + use super::{BodyFragment, Item, SectionBody, parameter_documentation, sections}; + + #[test] + fn extracts_supported_numpy_parameter_items() { + let raw = r#" + This is a function description. + + Parameters + ---------- + param1 : str + The first parameter description + + This is a second paragraph. + This is a continuation of the first parameter description. + param2, param4, ... : int + The shared parameter description + param3 + A parameter without type annotation + *args : object + Extra positional arguments + **kwargs : object + Extra keyword arguments + options.mode : str + Nested field documentation + π : int + A Unicode parameter + override_repr: callable, optional + Replacement representation function + formats, names : + undocumented + copy : bool + Whether to copy the input + + Other Parameters + ---------------- + kw_only : str, optional + A less commonly used keyword-only parameter + "#; + + assert_snapshot!(display_parameters(raw), @" + param1: + │ The first parameter description + │ + │ This is a second paragraph. + │ This is a continuation of the first parameter description. + param2: + │ The shared parameter description + param4: + │ The shared parameter description + param3: + │ A parameter without type annotation + *args: + │ Extra positional arguments + **kwargs: + │ Extra keyword arguments + options.mode: + │ Nested field documentation + π: + │ A Unicode parameter + override_repr: + │ Replacement representation function + copy: + │ Whether to copy the input + kw_only: + │ A less commonly used keyword-only parameter + "); + } + + #[test] + fn uses_last_documentation_for_duplicate_parameter() { + let source = normalized( + r#" + Parameters + ---------- + value : str + First documentation. + value : str + Replacement documentation. + "#, + ); + + assert_eq!( + parameter_documentation(&source)["value"], + "Replacement documentation." + ); + } + + #[test] + fn extracts_shifted_top_level_numpy_sections() { + let raw = "\ +A decoded newline follows: +This line starts at column zero. + + Parameters + ---------- + shifted : int + Documentation in a shifted section. + + Returns + ------- + bool + Result."; + + assert_snapshot!(display_parameters(raw), @" + shifted: + │ Documentation in a shifted section. + "); + } + + #[test] + fn ignores_numpy_items_nested_in_section_preambles() { + let raw = "\ +Parameters +---------- +Choose one of the following. + nested : int + Example-only text. +beta : float + Useful documentation."; + + assert_snapshot!(display_parameters(raw), @" + beta: + │ Useful documentation. + "); + } + + #[test] + fn ignores_numpy_sections_in_containers() { + let raw = "\ +Summary. + +- Example data: + Parameters + ---------- + nested : int + Not parameter documentation."; + + assert_snapshot!(display_parameters(raw), @""); + } + + #[test] + fn ignores_numpy_sections_in_rest_literal_blocks() { + let raw = "\ +Summary. + +Example:: + + Other Parameters + ---------------- + nested : int + Literal content."; + + assert_snapshot!(display_parameters(raw), @""); + } + + #[test] + fn finds_numpy_section_after_first_line_rest_literal_block() { + let raw = "\ +Example:: + + sample output + + Parameters + ---------- + value : int + Parameter documentation."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ Parameter documentation. + "); + } + + #[test] + fn ignores_numpy_sections_nested_in_other_sections() { + let raw = "\ +Examples +-------- + Parameters + ---------- + nested : int + Not parameter documentation. + +Notes +----- +More details. + +Parameters +---------- +value : int + Parameter documentation."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ Parameter documentation. + "); + } + + #[test] + fn extracts_parameters_from_a_first_line_section() { + let raw = "\ +Parameters + ---------- + value : int + Description. + +Examples: + Example prose."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ Description. + "); + + let source = normalized(raw); + assert!( + sections(&source) + .first() + .is_some_and(|section| &source[section.range] + == "\ +Parameters + ---------- + value : int + Description.") + ); + } + + #[test] + fn preserves_blank_lines_in_preformatted_parameter_descriptions() { + let raw = "\ +Parameters +---------- +value : str + ```text + first + + second + ``` +other : int + Another value."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ ```text + │ first + │ + │ second + │ ``` + other: + │ Another value. + "); + } + + #[test] + fn leaves_misaligned_parameter_section_opaque() { + let raw = "\ +Parameters +---------- + value : int + Description. + other : str + Other."; + + let source = normalized(raw); + assert!( + sections(&source) + .first() + .is_some_and(|section| matches!(section.body, SectionBody::Opaque)) + ); + } + + #[test] + fn extracts_compact_parameters_without_rendering_them_structurally() { + let raw = "\ +Parameters +---------- +d:int + Parameter d."; + + assert_snapshot!(display_parameters(raw), @" + d: + │ Parameter d. + "); + + assert_section_is_structurally_ambiguous(raw); + } + + #[test] + fn leaves_indented_parameter_preambles_raw() { + let raw = "\ +Parameters +---------- +Choose one form. + foo() +beta : int + Useful documentation."; + + assert_snapshot!(display_parameters(raw), @" + beta: + │ Useful documentation. + "); + + assert_section_is_structurally_ambiguous(raw); + } + + #[test] + fn leaves_unconfirmed_parameter_item_opaque() { + let source = normalized( + "\ +Summary. + +Parameters +---------- +Note:", + ); + assert!( + sections(&source) + .first() + .is_some_and(|section| matches!(section.body, SectionBody::Opaque)) + ); + } + + #[test] + fn extracts_later_parameters_from_an_ambiguous_section() { + let raw = "\ +Parameters +---------- +value : int + Description. +Ambiguous prose. +other : str + Other."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ Description. + │ Ambiguous prose. + other: + │ Other. + "); + + assert_section_is_structurally_ambiguous(raw); + } + + #[test] + fn skips_an_invalid_item_without_ending_the_parameter_list() { + let raw = "\ +Parameters +---------- +value : int + Description. + +malformed name : str + Not value documentation. + +other : str + Other."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ Description. + other: + │ Other. + "); + + assert_section_is_structurally_ambiguous(raw); + } + + #[test] + fn skips_static_substitutions() { + let raw = "\ +Summary. + Parameters + ---------- + first : int + Before. + %(description)s + $DESCRIPTION + After. + + $ITEM + Expansion content. + ${OTHER} + second : int + Description. +%(OUTSIDE)s + outside : int + Not parameter documentation. + + Parameters + ---------- + %(boundary)s + + %(left)s or %(right)s + hidden : int + Also not parameter documentation."; + + assert_snapshot!(display_parameters(raw), @" + first: + │ Before. + │ After. + second: + │ Description. + "); + + let source = normalized(raw); + assert!( + sections(&source) + .into_iter() + .all(|section| section.into_renderable_fragments().is_none()) + ); + } + + #[test] + fn extracts_later_parameters_after_an_unconfirmed_item() { + let raw = "\ +Parameters +---------- +value : int + Description. +Note: +other : str + Other."; + + assert_snapshot!(display_parameters(raw), @" + value: + │ Description. + │ Note: + other: + │ Other. + "); + + assert_section_is_structurally_ambiguous(raw); + } + + #[test] + fn treats_indented_return_items_as_structurally_ambiguous() { + let raw = "\ +Returns +------- + foo() + result"; + + assert_section_is_structurally_ambiguous(raw); + } + + #[test] + fn ends_parameters_before_preformatted_block() { + let source = normalized( + "\ +Parameters +---------- +value : int + Description. + +```text +other : str +```", + ); + + assert!(sections(&source).first().is_some_and(|section| { + matches!( + section.body, + SectionBody::Parsed { + has_structural_ambiguity: false, + .. + } + ) && &source[section.range] + == "\ +Parameters +---------- +value : int + Description." + })); + } + + #[test] + fn parses_attributes_without_descriptions() { + let source = normalized( + "\ +Attributes +---------- +dtype : np.dtype +index", + ); + + assert_eq!( + sections(&source).first().map(|section| §ion.body), + Some(&SectionBody::Parsed { + fragments: vec![ + BodyFragment::Item(Item { + display_name: Some("dtype".to_string()), + ty: Some("np.dtype".to_string()), + description: String::new(), + }), + BodyFragment::Item(Item { + display_name: Some("index".to_string()), + ty: None, + description: String::new(), + }), + ], + has_structural_ambiguity: false, + }) + ); + } + + #[test] + fn parses_named_and_anonymous_return_items() { + let source = normalized( + "\ +Returns +------- +np.ndarray, bool + The values and a flag. +angular separation : Quantity + The angle between two points. +`module:Type`", + ); + + assert_eq!( + sections(&source).first().map(|section| §ion.body), + Some(&SectionBody::Parsed { + fragments: vec![ + BodyFragment::Item(Item { + display_name: None, + ty: Some("np.ndarray, bool".to_string()), + description: "The values and a flag.".to_string(), + }), + BodyFragment::Item(Item { + display_name: Some("angular separation".to_string()), + ty: Some("Quantity".to_string()), + description: "The angle between two points.".to_string(), + }), + BodyFragment::Item(Item { + display_name: None, + ty: Some("`module:Type`".to_string()), + description: String::new(), + }), + ], + has_structural_ambiguity: false, + }) + ); + } + + fn assert_section_is_structurally_ambiguous(raw: &str) { + let source = normalized(raw); + assert!(sections(&source).first().is_some_and(|section| matches!( + section.body, + SectionBody::Parsed { + has_structural_ambiguity: true, + .. + } + ))); + } + + fn display_parameters(raw: &str) -> String { + let normalized_source = crate::docstring::documentation_trim(raw); + parameter_documentation(&normalized_source) + .into_iter() + .map(|(name, documentation)| { + let documentation = documentation + .lines() + .map(|line| match line { + "" => " │".to_string(), + _ => format!(" │ {line}"), + }) + .join("\n"); + format!("{name}:\n{documentation}") + }) + .join("\n") + } + + fn normalized(raw: &str) -> String { + crate::docstring::documentation_trim(raw) + } +} diff --git a/crates/ty_ide/src/docstring/document/preformatted.rs b/crates/ty_ide/src/docstring/document/preformatted.rs index 40f6e30f55..7dde49b6c2 100644 --- a/crates/ty_ide/src/docstring/document/preformatted.rs +++ b/crates/ty_ide/src/docstring/document/preformatted.rs @@ -45,6 +45,13 @@ impl<'a> MarkdownFence<'a> { } } +/// Returns whether `line` starts a recognized preformatted block. +pub(super) fn starts_preformatted_block(line: &str) -> bool { + PreformattedBlockScanner::line_starts_doctest(line) + || MarkdownFence::find(line).is_some() + || RestLiteralBlockScanner::line_starts_literal_block(line.trim_start()) +} + /// Recognizes preformatted blocks that may occur within a docstring. #[derive(Debug, Default, Clone, PartialEq, Eq)] pub(super) struct PreformattedBlockScanner<'a> { diff --git a/crates/ty_ide/src/docstring/document/syntax.rs b/crates/ty_ide/src/docstring/document/syntax.rs index 401b7b5277..95a1a18c1d 100644 --- a/crates/ty_ide/src/docstring/document/syntax.rs +++ b/crates/ty_ide/src/docstring/document/syntax.rs @@ -120,10 +120,7 @@ pub(in crate::docstring) fn is_backtick_run_escaped(text: &str, index: usize) -> /// at index 3. pub(super) fn container_block_end(lines: &[ParsedLine<'_>], index: usize) -> Option { let marker = lines.get(index)?; - if !is_rest_directive_marker(marker.text) - && !is_field_list_marker(marker.text) - && !starts_with_markdown_list_item(marker.text.trim_start()) - { + if !starts_container_block(marker.text) { return None; } @@ -137,6 +134,13 @@ pub(super) fn container_block_end(lines: &[ParsedLine<'_>], index: usize) -> Opt ) } +/// Returns whether `line` starts a block that owns its indented contents. +pub(super) fn starts_container_block(line: &str) -> bool { + is_rest_directive_marker(line) + || is_field_list_marker(line) + || starts_with_markdown_list_item(line.trim_start()) +} + fn is_rest_directive_marker(line: &str) -> bool { let Some(directive) = line.trim_start().strip_prefix(".. ") else { return false; From 426049a89287381010573c44f0709fbb7c2a3fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9r=C3=A8?= Date: Thu, 23 Jul 2026 12:38:53 -0700 Subject: [PATCH 040/390] [ty] Use Fx hashing for docstring parameter documentation (#27135) ## Summary Following up on [this bit of review feedback](https://github.com/astral-sh/ruff/pull/25924#discussion_r3636552035), this switches the hash map implementation that we use for parameter documentation in hover and signature help responses from `IndexMap` to `FxIndexMap`. This does not change the behaviour of those language server responses. ## Test Plan This is a refactor that relies on existing test coverage. --- crates/ty_ide/src/docstring.rs | 5 ++--- crates/ty_ide/src/docstring/document.rs | 4 ++-- crates/ty_ide/src/docstring/document/google.rs | 8 ++++---- crates/ty_ide/src/docstring/document/numpy.rs | 8 ++++---- crates/ty_ide/src/docstring/document/rst.rs | 6 +++--- crates/ty_ide/src/lib.rs | 4 +++- crates/ty_ide/src/signature_help.rs | 3 ++- 7 files changed, 20 insertions(+), 18 deletions(-) diff --git a/crates/ty_ide/src/docstring.rs b/crates/ty_ide/src/docstring.rs index b434b506e5..39e74e9ab2 100644 --- a/crates/ty_ide/src/docstring.rs +++ b/crates/ty_ide/src/docstring.rs @@ -9,11 +9,10 @@ mod document; mod markdown; -use indexmap::IndexMap; use ruff_python_trivia::{PythonWhitespace, expand_tabs, leading_indentation}; use ruff_source_file::UniversalNewlines; -use crate::MarkupKind; +use crate::{FxIndexMap, MarkupKind}; /// A docstring which hasn't yet been interpreted or rendered /// @@ -48,7 +47,7 @@ impl Docstring { /// Extract parameter documentation from popular docstring formats. /// Returns a map of parameter names to their documentation. - pub fn parameter_documentation(&self) -> IndexMap { + pub fn parameter_documentation(&self) -> FxIndexMap { let normalized_source = documentation_trim(&self.0); document::parameter_documentation(&normalized_source) } diff --git a/crates/ty_ide/src/docstring/document.rs b/crates/ty_ide/src/docstring/document.rs index 3ad3ce3314..8f769cb47b 100644 --- a/crates/ty_ide/src/docstring/document.rs +++ b/crates/ty_ide/src/docstring/document.rs @@ -1,8 +1,8 @@ -use indexmap::IndexMap; use ruff_text_size::{TextRange, TextSize}; use strum_macros::EnumIter; use self::syntax::{indentation, starts_with_markdown_list_item}; +use crate::FxIndexMap; pub(super) mod google; mod numpy; @@ -14,7 +14,7 @@ pub(in crate::docstring) mod syntax; /// /// `normalized_source` must have already undergone PEP-257 trimming and universal newline /// normalization. -pub(super) fn parameter_documentation(normalized_source: &str) -> IndexMap { +pub(super) fn parameter_documentation(normalized_source: &str) -> FxIndexMap { let mut parameters = google::parameter_documentation(normalized_source); parameters.extend(numpy::parameter_documentation(normalized_source)); parameters.extend(rst::parameter_documentation(normalized_source)); diff --git a/crates/ty_ide/src/docstring/document/google.rs b/crates/ty_ide/src/docstring/document/google.rs index 85322bffbf..e3d56a0fcf 100644 --- a/crates/ty_ide/src/docstring/document/google.rs +++ b/crates/ty_ide/src/docstring/document/google.rs @@ -30,7 +30,6 @@ //! retries: Number of retries. //! ``` -use indexmap::IndexMap; use ruff_python_stdlib::identifiers::is_identifier; use ruff_python_trivia::Cursor; use ruff_text_size::{TextRange, TextSize}; @@ -41,12 +40,13 @@ use super::syntax::{ parsed_lines, split_once_at_top_level_colon, split_trailing_parenthetical, }; use super::{DescriptionBuilder, HeaderKind, SectionKind}; +use crate::FxIndexMap; /// Returns parameter documentation from recognized Google-style parameter sections. /// /// `normalized_source` must have already undergone PEP-257 trimming and universal newline /// normalization. -pub(super) fn parameter_documentation(normalized_source: &str) -> IndexMap { +pub(super) fn parameter_documentation(normalized_source: &str) -> FxIndexMap { let mut parameters = Parameters::default(); for section in sections(normalized_source) { let Section { @@ -184,7 +184,7 @@ impl<'a> ParameterDisplayName<'a> { } #[derive(Default)] -struct Parameters(IndexMap); +struct Parameters(FxIndexMap); impl Parameters { fn extend_fragments(&mut self, fragments: Vec) { @@ -209,7 +209,7 @@ impl Parameters { } } - fn into_inner(self) -> IndexMap { + fn into_inner(self) -> FxIndexMap { self.0 } } diff --git a/crates/ty_ide/src/docstring/document/numpy.rs b/crates/ty_ide/src/docstring/document/numpy.rs index f303a629da..9d00b59e6d 100644 --- a/crates/ty_ide/src/docstring/document/numpy.rs +++ b/crates/ty_ide/src/docstring/document/numpy.rs @@ -24,7 +24,6 @@ //! The arithmetic mean. //! ``` -use indexmap::IndexMap; use ruff_text_size::{TextRange, TextSize}; use super::preformatted::{PreformattedBlockScanner, starts_preformatted_block}; @@ -33,12 +32,13 @@ use super::syntax::{ split_once_at_top_level_colon, starts_container_block, }; use super::{DescriptionBuilder, HeaderKind, SectionKind}; +use crate::FxIndexMap; /// Returns parameter documentation from recognized NumPy-style parameter sections. /// /// `normalized_source` must have already undergone PEP-257 trimming and universal newline /// normalization. -pub(super) fn parameter_documentation(normalized_source: &str) -> IndexMap { +pub(super) fn parameter_documentation(normalized_source: &str) -> FxIndexMap { let mut parameters = Parameters::default(); for section in sections(normalized_source) { @@ -56,7 +56,7 @@ pub(super) fn parameter_documentation(normalized_source: &str) -> IndexMap); +struct Parameters(FxIndexMap); impl Parameters { fn extend_fragments(&mut self, fragments: Vec) { @@ -85,7 +85,7 @@ impl Parameters { } } - fn into_inner(self) -> IndexMap { + fn into_inner(self) -> FxIndexMap { self.0 } } diff --git a/crates/ty_ide/src/docstring/document/rst.rs b/crates/ty_ide/src/docstring/document/rst.rs index 8ccd608d3e..64bf275a91 100644 --- a/crates/ty_ide/src/docstring/document/rst.rs +++ b/crates/ty_ide/src/docstring/document/rst.rs @@ -1,12 +1,12 @@ use std::iter::{Enumerate, Peekable}; use compact_str::{CompactString, ToCompactString}; -use indexmap::IndexMap; use ruff_python_trivia::leading_indentation; use ruff_source_file::{Line as SourceLine, UniversalNewlineIterator, UniversalNewlines}; use ruff_text_size::{Ranged, TextRange, TextSize}; use super::preformatted::PreformattedBlockScanner; +use crate::FxIndexMap; /// Parses all reST field lists in a docstring. fn field_lists(raw: &str) -> Vec { @@ -35,8 +35,8 @@ pub(in crate::docstring) fn top_level_field_lists( /// /// `normalized_source` must have already undergone PEP-257 trimming and universal newline /// normalization. -pub(super) fn parameter_documentation(normalized_source: &str) -> IndexMap { - let mut parameters = IndexMap::new(); +pub(super) fn parameter_documentation(normalized_source: &str) -> FxIndexMap { + let mut parameters = FxIndexMap::default(); for field_list in top_level_field_lists(normalized_source) { for field in field_list.fields { diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index d0b27b0ca2..08f0c87bd1 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -69,11 +69,13 @@ use ruff_db::{ vendored::VendoredPath, }; use ruff_text_size::{Ranged, TextRange}; -use rustc_hash::FxHashSet; +use rustc_hash::{FxBuildHasher, FxHashSet}; use std::ops::{Deref, DerefMut}; use ty_project::Db; use ty_python_semantic::types::{Type, TypeDefinition}; +type FxIndexMap = indexmap::IndexMap; + /// Information associated with a text range. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct RangedValue { diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs index a077d0b3b6..e4455ea5ec 100644 --- a/crates/ty_ide/src/signature_help.rs +++ b/crates/ty_ide/src/signature_help.rs @@ -7,6 +7,7 @@ //! and overloads. use crate::Db; +use crate::FxIndexMap; use crate::docstring::Docstring; use crate::goto::docstring_for_call_definition; use ruff_db::files::File; @@ -236,7 +237,7 @@ fn create_parameters<'db>( let param_docs = if let Some(docstring) = docstring { docstring.parameter_documentation() } else { - indexmap::IndexMap::new() + FxIndexMap::default() }; parameters From 2510a58cbf19cbc7289b8aa519731227551a0d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9r=C3=A8?= Date: Thu, 23 Jul 2026 12:55:07 -0700 Subject: [PATCH 041/390] [ty] Render NumPy docstrings as structured Markdown (#25925) ## Summary This introduces Markdown rendering for NumPy-style sections in docstrings. Supported sections are rendered with canonical Markdown headings, bold item names, inline code spans for types and raised exceptions, and descriptions passed through the existing general Markdown renderer. Here's what it looks like in VSCode[^1]: [^1]: Note that even before this change (specifically, as of https://github.com/astral-sh/ruff/pull/26599) we already render section headers but we don't yet render section contents properly. | Before | After | | :--- | :--- | | numpy-before-1 numpy-before-2 numpy-before-3 | numpy-after-1 numpy-after-2 numpy-after-3 | The implementation includes the following judgement calls that I think are acceptable, but that we may want to revisit based on user feedback: - We only render Markdown headings for a fixed subset of NumPy sections that map cleanly to a shared document model: parameters, attributes, returns, yields, and raises. That list can be expanded in the future. - We carry forward a few of the rendering policies that were [established for the reStructuredText format](https://github.com/astral-sh/ruff/pull/25903): - Malformed, unsupported, or ambiguous content still causes us to leave an entire section raw (i.e. source content, not structured Markdown). Other well-formed sections in the same docstring are rendered as usual. - Types are rendered with inline code spans, and so do not receive syntax highlighting. - Physical line breaks in descriptions are preserved as Markdown hard breaks rather than being reflowed. Lastly, please note that rendering should improve further after these two additional fixes land: - https://github.com/astral-sh/ruff/pull/26923 - https://github.com/astral-sh/ruff/pull/26924 Those are both general fixes that affect multiple docstring formats, and so are intentionally separated from this change. Closes https://github.com/astral-sh/ty/issues/1667 ## Test Plan See included tests. --- crates/ty_ide/src/docstring.rs | 74 ++-- crates/ty_ide/src/docstring/document.rs | 2 +- crates/ty_ide/src/docstring/document/numpy.rs | 6 +- .../src/docstring/markdown/structured.rs | 4 +- .../docstring/markdown/structured/numpy.rs | 370 ++++++++++++++++++ 5 files changed, 414 insertions(+), 42 deletions(-) create mode 100644 crates/ty_ide/src/docstring/markdown/structured/numpy.rs diff --git a/crates/ty_ide/src/docstring.rs b/crates/ty_ide/src/docstring.rs index 39e74e9ab2..ca4f0ae042 100644 --- a/crates/ty_ide/src/docstring.rs +++ b/crates/ty_ide/src/docstring.rs @@ -1176,22 +1176,22 @@ Summary. "); assert_snapshot!(docstring.render_markdown(), @" - This is a function description. - - Parameters - ---------- - param1 : str -     The first parameter description - param2 : int -     The second parameter description -     This is a continuation of param2 description. - param3 -     A parameter without type annotation - - Returns - ------- - str -     The return value description + This is a function description. + + ## Parameters + **param1**: `str` + The first parameter description + + **param2**: `int` + The second parameter description + This is a continuation of param2 description. + + **param3** + A parameter without type annotation + + ## Returns + `str` + The return value description "); } @@ -1324,10 +1324,9 @@ Summary. **param2**: `int` Another Google-style parameter - Parameters - ---------- - param3 : bool -     NumPy-style parameter + ## Parameters + **param3**: `bool` + NumPy-style parameter "); } @@ -1488,12 +1487,12 @@ Summary. **param3** Another reST-style parameter - Parameters - ---------- - param3 : str -     NumPy-style duplicate parameter - param4 : bool -     NumPy-style parameter + ## Parameters + **param3**: `str` + NumPy-style duplicate parameter + + **param4**: `bool` + NumPy-style parameter "); } @@ -1547,17 +1546,18 @@ Summary. "); assert_snapshot!(docstring.render_markdown(), @" - This is a function description. - - Parameters - ---------- - param1 : str -         The first parameter description - param2 : int -         The second parameter description -         This is a continuation of param2 description. - param3 -         A parameter without type annotation + This is a function description. + + ## Parameters + **param1**: `str` + The first parameter description + + **param2**: `int` + The second parameter description + This is a continuation of param2 description. + + **param3** + A parameter without type annotation "); } diff --git a/crates/ty_ide/src/docstring/document.rs b/crates/ty_ide/src/docstring/document.rs index 8f769cb47b..ddb6635384 100644 --- a/crates/ty_ide/src/docstring/document.rs +++ b/crates/ty_ide/src/docstring/document.rs @@ -5,7 +5,7 @@ use self::syntax::{indentation, starts_with_markdown_list_item}; use crate::FxIndexMap; pub(super) mod google; -mod numpy; +pub(super) mod numpy; pub(super) mod preformatted; pub(super) mod rst; pub(in crate::docstring) mod syntax; diff --git a/crates/ty_ide/src/docstring/document/numpy.rs b/crates/ty_ide/src/docstring/document/numpy.rs index 9d00b59e6d..4f454bc570 100644 --- a/crates/ty_ide/src/docstring/document/numpy.rs +++ b/crates/ty_ide/src/docstring/document/numpy.rs @@ -110,17 +110,17 @@ fn parameter_lookup_names(display_name: &str) -> Option> { /// /// `source` must have already undergone PEP-257 trimming and universal newline normalization /// (typically via `docstring::documentation_trim`). -fn sections(source: &str) -> Vec
{ +pub(in crate::docstring) fn sections(source: &str) -> Vec
{ Parser::new(parsed_lines(source)).parse() } /// A recognized NumPy-style docstring section. -type Section = super::Section>; +pub(in crate::docstring) type Section = super::Section>; type SectionBody = super::SectionBody>; /// One parsed fragment in a NumPy section body. -type BodyFragment = super::BodyFragment>; +pub(in crate::docstring) type BodyFragment = super::BodyFragment>; /// A named or anonymous item in a NumPy section. type Item = super::Item>; diff --git a/crates/ty_ide/src/docstring/markdown/structured.rs b/crates/ty_ide/src/docstring/markdown/structured.rs index ac693750b8..f7df4c74cd 100644 --- a/crates/ty_ide/src/docstring/markdown/structured.rs +++ b/crates/ty_ide/src/docstring/markdown/structured.rs @@ -9,6 +9,7 @@ use crate::docstring::document::preformatted::MarkdownFence; use crate::docstring::document::syntax::{is_markdown_code_span, starts_with_markdown_list_item}; mod google; +mod numpy; mod rst; /// Renders a docstring as Markdown. @@ -18,6 +19,7 @@ mod rst; pub(super) fn render_into(output: &mut String, source: &str) { let mut sections = rst::structured_sections(source); sections.extend(google::structured_sections(source)); + sections.extend(numpy::structured_sections(source)); render_sections_into(output, source, sections); } @@ -249,7 +251,7 @@ impl SectionItem { if let Some(name) = self.display_name.as_deref() { if matches!(self.kind, SectionKind::Raises) { - render_code_span_into(output, name); + render_type_code_span_into(output, name); } else { render_bold_text_into(output, name); } diff --git a/crates/ty_ide/src/docstring/markdown/structured/numpy.rs b/crates/ty_ide/src/docstring/markdown/structured/numpy.rs new file mode 100644 index 0000000000..990086ae44 --- /dev/null +++ b/crates/ty_ide/src/docstring/markdown/structured/numpy.rs @@ -0,0 +1,370 @@ +use crate::docstring::document::numpy; + +use super::{Section, SectionItem, SectionKind}; + +/// Returns NumPy-style sections that can be rendered structurally. +pub(super) fn structured_sections(normalized_source: &str) -> Vec
{ + numpy::sections(normalized_source) + .into_iter() + .filter_map(section) + .collect() +} + +fn section(parsed: numpy::Section) -> Option
{ + let kind = parsed.kind(); + let range = parsed.range(); + let fragments = parsed.into_renderable_fragments()?; + + if fragments.is_empty() { + return None; + } + + let items = fragments + .into_iter() + .map(|fragment| section_item(kind, fragment)) + .collect(); + + Section::new(range, items) +} + +fn section_item(kind: SectionKind, fragment: numpy::BodyFragment) -> SectionItem { + match fragment { + numpy::BodyFragment::Prose(description) => { + SectionItem::from_owned_parts(kind, None, None, description) + } + numpy::BodyFragment::Item(item) => { + let (display_name, ty, description) = item.into_display_name_type_and_description(); + SectionItem::from_owned_parts(kind, display_name, ty, description) + } + } +} + +#[cfg(test)] +mod tests { + use insta::{Settings, assert_snapshot}; + + use super::super::render_sections_into; + use super::structured_sections; + + #[test] + fn renders_supported_sections() { + let _snap = bind_markdown_snapshot_filters(); + let docstring = "\ +Summary. + +Parameters +---------- +value, alias : str + The value. + + A second paragraph. +other + Another value. +*args : object + Extra positional arguments. +**kwargs : object + Extra keyword arguments. +options.mode : str + Nested field documentation. +π : int + A Unicode parameter. +a1, a2, ... : sequence of array_like + Arrays to combine. +override_repr: callable, optional + Replacement representation function. +formats, names : +undocumented + +Other Parameters +---------------- +kw_only: bool + Less common option. + +Attributes +---------- +name : str + Display name. + +Returns +------- +result : bool + Whether validation passed. + +Yields +------ +int + Next value. + +Raises +------ +ValueError + If invalid. +`TypeError` + If unsupported. +"; + + assert_snapshot!(render_numpy(docstring), @r" + Summary. + + ## Parameters + **value, alias**: `str` + The value. + + A second paragraph. + + **other** + Another value. + + **\*args**: `object` + Extra positional arguments. + + **\*\*kwargs**: `object` + Extra keyword arguments. + + **options.mode**: `str` + Nested field documentation. + + **π**: `int` + A Unicode parameter. + + **a1, a2, ...**: `sequence of array_like` + Arrays to combine. + + **override\_repr**: `callable, optional` + Replacement representation function. + + **formats, names** + + **undocumented** + + ## Other Parameters + **kw\_only**: `bool` + Less common option. + + ## Attributes + **name**: `str` + Display name. + + ## Returns + **result**: `bool` + Whether validation passed. + + ## Yields + `int` + Next value. + + ## Raises + `ValueError` + If invalid. + + `TypeError` + If unsupported. + "); + } + + #[test] + fn renders_preformatted_parameter_description() { + let _snap = bind_markdown_snapshot_filters(); + let docstring = "\ +Parameters +---------- +value : str + Example:: + ``` +other : int + Another value. +"; + + assert_snapshot!(render_numpy(docstring), @" + ## Parameters + **value**: `str` + Example: + + ```````````python + ``` + ``````````` + + **other**: `int` + Another value. + "); + } + + #[test] + fn renders_parameter_section_preamble() { + let _snap = bind_markdown_snapshot_filters(); + let docstring = "\ +Parameters +---------- +Either x or y must be provided. + +beta : float + Useful documentation. +"; + + assert_snapshot!(render_numpy(docstring), @" + ## Parameters + Either x or y must be provided. + + **beta**: `float` + Useful documentation. + "); + } + + #[test] + fn renders_shifted_top_level_sections() { + let _snap = bind_markdown_snapshot_filters(); + let docstring = "\ +A decoded newline follows: +This line starts at column zero. + + Parameters + ---------- + shifted : int + Documentation in a shifted section. +"; + + assert_snapshot!(render_numpy(docstring), @" + A decoded newline follows: + This line starts at column zero. + + ## Parameters + **shifted**: `int` + Documentation in a shifted section. + "); + } + + #[test] + fn renders_parenthesized_return_names() { + let _snap = bind_markdown_snapshot_filters(); + let docstring = "\ +Returns +------- +((node1, node2), ancestor) : tuple[tuple[object, object], object] + A node pair and its lowest common ancestor. +"; + + assert_snapshot!(render_numpy(docstring), @" + ## Returns + **((node1, node2), ancestor)**: `tuple[tuple[object, object], object]` + A node pair and its lowest common ancestor. + "); + } + + #[test] + fn renders_return_prose_outside_the_structured_section() { + let _snap = bind_markdown_snapshot_filters(); + let docstring = "\ +Returns +------- +list of nodes + The nodes in traversal order +necessarily returned in a stable order +"; + + assert_snapshot!(render_numpy(docstring), @" + ## Returns + `list of nodes` + The nodes in traversal order + + necessarily returned in a stable order + "); + } + + #[test] + fn declines_to_render_nested_parameter_items() { + let docstring = "\ +Parameters +---------- +Choose one of the following. + nested : int + Example-only text. +beta : float + Useful documentation. +"; + + assert!(parsed_sections(docstring).is_empty()); + } + + #[test] + fn declines_to_render_structurally_ambiguous_section() { + let docstring = "\ +Parameters +---------- + value : int + Description. + Ambiguous prose. + other : str + Other. +"; + + assert!(parsed_sections(docstring).is_empty()); + } + + #[test] + fn declines_to_render_section_nested_in_container() { + let docstring = "\ +Summary. + +- Example data: + Parameters + ---------- + nested : int + Not parameter documentation. +"; + + assert!(parsed_sections(docstring).is_empty()); + } + + #[test] + fn declines_to_render_prose_only_return_section() { + let docstring = "\ +Returns +------- + The created object. +"; + + assert!(parsed_sections(docstring).is_empty()); + } + + #[test] + fn declines_to_render_unclosed_return_fence() { + let docstring = "\ +Returns +------- +```python + result = 1 +"; + + assert!(parsed_sections(docstring).is_empty()); + } + + #[test] + fn declines_to_render_empty_return_section() { + let docstring = "\ +Returns +------- + +Notes +----- +Not a return value. +"; + + assert!(parsed_sections(docstring).is_empty()); + } + + fn render_numpy(source: &str) -> String { + let mut output = String::new(); + render_sections_into(&mut output, source, parsed_sections(source)); + output + } + + fn parsed_sections(source: &str) -> Vec { + structured_sections(source) + } + + fn bind_markdown_snapshot_filters() -> impl Drop { + let mut settings = Settings::clone_current(); + settings.add_filter(" \n", "\n"); + settings.bind_to_scope() + } +} From e212348aedf922c63ee6b54e715258ba37573b37 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 23 Jul 2026 16:02:51 -0400 Subject: [PATCH 042/390] [ty] Add test expectations for scoped quantifiers (#27102) This PR adds a suite of tests for the [scoped quantifier](https://gist.github.com/dcreager/679132607be4c7cfb08ddc8ad1076982) work. Many of these tests are currently failing; the ones that are passing are likely doing so for the wrong reasons. As part of adding these tests, we also add a new `Constraint.exists` extension method, to go along with the existing `ConstraintSet.for_all`. --------- Co-authored-by: Charlie Marsh --- .../mdtest/type_properties/constraints.md | 40 ++- .../mdtest/type_properties/quantification.md | 317 ++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 11 + .../ty_python_semantic/src/types/call/bind.rs | 16 +- .../ty_python_semantic/src/types/display.rs | 3 + crates/ty_python_semantic/src/types/method.rs | 13 +- .../ty_python_semantic/src/types/relation.rs | 1 + .../ty_vendored/ty_extensions/_internal.pyi | 5 + 8 files changed, 393 insertions(+), 13 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md index c4e8cd5c9e..ae66e75be4 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md @@ -861,10 +861,39 @@ def same_typevar[T](): static_assert(constraints == expected) ``` +## Existential quantification + +Existential quantification removes the listed typevars from a constraint set. Any constraints that +do not involve those typevars must remain in the result. The result holds whenever _at least one_ +valid assignment to the quantified variables satisfies the expression being quantified over. + +```py +from typing import Never +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +def preserves_remaining_conjunct[T, U]() -> None: + t_int = ConstraintSet.range(int, T, int) + u_str = ConstraintSet.range(str, U, str) + quantified = (t_int & u_str).exists(tuple[U]) + static_assert(quantified == t_int) + +def satisfies_uncertain_disjunct[T, U]() -> None: + t_int = ConstraintSet.range(int, T, int) + u_str = ConstraintSet.range(str, U, str) + quantified = (t_int | u_str).exists(tuple[U]) + static_assert(quantified == ConstraintSet.always()) + +def no_typevars_is_identity[T]() -> None: + constraints = ConstraintSet.range(Never, T, int) + static_assert(constraints.exists(tuple[()]) == constraints) +``` + ## Universal quantification Universal quantification removes the listed typevars from a constraint set. Any constraints that do -not involve those typevars must remain in the result, including constraints in an uncertain branch. +not involve those typevars must remain in the result. The result holds whenever _every_ valid +assignment to the quantified variables satisfies the expression being quantified over. ```py from typing import Never @@ -901,13 +930,12 @@ def quantifier_order[S, T]() -> None: target_is_int = ConstraintSet.range(int, T, int) equal = source_is_int.satisfies(target_is_int) & target_is_int.satisfies(source_is_int) - # ∀T.∃S.equal(S, T) = ∀T.¬∀S.¬equal(S, T) - forall_target_exists_source = (~((~equal).for_all(tuple[S]))).for_all(tuple[T]) + # ∀T.∃S.equal(S, T) + forall_target_exists_source = equal.exists(tuple[S]).for_all(tuple[T]) static_assert(forall_target_exists_source == ConstraintSet.always()) - # ∃S.∀T.equal(S, T) = ¬∀S.¬∀T.equal(S, T) - forall_target = equal.for_all(tuple[T]) - exists_source_forall_target = ~((~forall_target).for_all(tuple[S])) + # ∃S.∀T.equal(S, T) + exists_source_forall_target = equal.for_all(tuple[T]).exists(tuple[S]) static_assert(exists_source_forall_target == ConstraintSet.never()) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md b/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md new file mode 100644 index 0000000000..a6c44bd09a --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md @@ -0,0 +1,317 @@ +# Quantification + +Quantification removes typevars from a constraint set, returning a new equivalent constraint set +that only references the remaining typevars. With existential quantification (`exists`), the result +holds when there is _at least one_ valid assignment of the removed variables that satisfies the +quantified expression. With universal quantification (`for_all`), the result holds when _every_ +valid assignment satisfies the quantified expression. + +This file contains several baseline test cases that validate our implementation of quantification. + +| Case | Formula | Expected result | +| ---- | -------------------------------- | ------------------------------------------- | +| C0 | `∃X. X = int ∧ A ≤ Invariant[X]` | Equivalent to `A ≤ Invariant[int]` | +| E1 | `∃X. U ≤ X ∧ X = V` | Equivalent to `U ≤ V` | +| E2 | `∃X. A ≤ Invariant[X] ∧ X ≤ B` | `A` and `B` must admit a common `X` | +| E3 | `∃X. A ≤ X ∧ Invariant[X] ≤ B` | `A` and `B` must admit a common `X` | +| E4 | `∃X. C₁(X, Y) ∧ C₂(X, Z)` | Solutions for `Y` and `Z` remain correlated | +| E5 | `∃X ∈ {int, str}. C(X, Y, Z)` | Solutions remain paired with each choice | +| E6 | `∀Y ∈ Dᵧ. ∃X ∈ Dₓ. R(X, Y)` | `X` may depend on the choice of `Y` | + +```toml +[environment] +python-version = "3.13" +``` + +## C0: grounded invariant + +In `∃X. X = int ∧ A ≤ Invariant[X]`, every assignment of `X` is _valid_ (i.e., satisfies the +implicit upper bound of `object`), but the only _satisfying_ assignment is `X = int`. That means the +result should be equivalent to `A ≤ Invariant[int]`. + +```py +from typing import Never +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Invariant[T]: + def get(self) -> T: + raise NotImplementedError + + def set(self, value: T) -> None: ... + +def grounded[X, A]() -> None: + # ∃X. X = int ∧ A ≤ Invariant[X] + body = ConstraintSet.range(int, X, int) & ConstraintSet.range(Never, A, Invariant[X]) + quantified = body.exists(tuple[X]) + + # TODO: revealed: tuple[Solution[X=int, A=list[int]]] + # revealed: tuple[Solution[X=int, A=Never]] + reveal_type(body.solutions(inferable=tuple[X, A])) + # TODO: revealed: tuple[Solution[A=list[int]]] + # revealed: tuple[Solution[A=Never]] + reveal_type(quantified.solutions(inferable=tuple[A])) + + # A ≤ Invariant[int] + expected = ConstraintSet.range(Never, A, Invariant[int]) + static_assert(quantified == expected) + static_assert(~quantified == ~expected) +``` + +## E1: relational bridge + +There is an `X` satisfying `U ≤ X ∧ X = V` exactly when `U ≤ V`. + +```py +from typing import Never +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +def relational_bridge[X, U, V]() -> None: + # ∃X. U ≤ X ∧ X = V + body = ConstraintSet.range(Never, U, X) & ConstraintSet.range(V, X, V) + quantified = body.exists(tuple[X]) + + # TODO: revealed: tuple[Solution[V=object, U=object]] + # revealed: tuple[Solution[V=U@relational_bridge, U=Never]] + reveal_type(quantified.solutions(inferable=tuple[U, V])) + + # U ≤ V + expected = ConstraintSet.range(Never, U, V) + static_assert(quantified == expected) + static_assert(~quantified == ~expected) +``` + +## E2: open invariant inverse image + +A specialization satisfies `∃X. A ≤ Invariant[X] ∧ X ≤ B` only if there is some `X` compatible with +both `A` and `B`. `A = Invariant[str]` and `B ≤ int` cannot satisfy the expression, so they must +satisfy its negation. + +```py +from typing import Never +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Invariant[T]: + def get(self) -> T: + raise NotImplementedError + + def set(self, value: T) -> None: ... + +def inverse_image[X, A, B]() -> None: + # ∃X. A ≤ Invariant[X] ∧ X ≤ B + body = ConstraintSet.range(Never, A, Invariant[X]) & ConstraintSet.range(Never, X, B) + quantified = body.exists(tuple[X]) + + # TODO: revealed: tuple[Solution[A=Invariant[object], B=object, X=object]] + # revealed: tuple[Solution[A=Never, B=X@inverse_image, X=Never]] + reveal_type(body.solutions(inferable=tuple[X, A, B])) + # TODO: revealed: tuple[Solution[A=Invariant[object], B=object]] + # revealed: tuple[()] + reveal_type(quantified.solutions(inferable=tuple[A, B])) + + # Invariant[str] ≤ A ∧ B ≤ int + invalid = ConstraintSet.range(Invariant[str], A, object) & ConstraintSet.range(Never, B, int) + # revealed: None + reveal_type((body & invalid).solutions(inferable=tuple[X, A, B])) + # TODO: revealed: None + # revealed: tuple[Solution[A=Invariant[str], B=Never]] + reveal_type((quantified & invalid).solutions(inferable=tuple[A, B])) + + static_assert(not (quantified & invalid)) + # TODO: no error + # error: [static-assert-error] + static_assert((~quantified & invalid) == invalid) +``` + +## E3: witness-sensitive image + +For `∃X. A ≤ X ∧ Invariant[X] ≤ B`, each choice of `X` determines which values of `A` and `B` can +satisfy the expression. `A ≥ int` and `B ≤ Invariant[str]` cannot satisfy it, so they must satisfy +its negation. + +```py +from typing import Never +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Invariant[T]: + def get(self) -> T: + raise NotImplementedError + + def set(self, value: T) -> None: ... + +def witness_sensitive[X, A, B]() -> None: + # ∃X. A ≤ X ∧ Invariant[X] ≤ B + body = ConstraintSet.range(A, X, object) & ConstraintSet.range(Invariant[X], B, object) + quantified = body.exists(tuple[X]) + + # Each solution for A and B depends on the compatible choice of X. + # TODO: revealed: tuple[Solution[X=object, A=object, B=Invariant[object]]] + # revealed: tuple[Solution[X=A@witness_sensitive, A=X@witness_sensitive, B=Invariant[X@witness_sensitive]]] + reveal_type(body.solutions(inferable=tuple[X, A, B])) + # TODO: revealed: tuple[Solution[A=object, B=Invariant[object]]] + # revealed: tuple[()] + reveal_type(quantified.solutions(inferable=tuple[A, B])) + + # int ≤ A ∧ B ≤ Invariant[str] + invalid = ConstraintSet.range(int, A, object) & ConstraintSet.range(Never, B, Invariant[str]) + # revealed: None + reveal_type((body & invalid).solutions(inferable=tuple[X, A, B])) + # TODO: revealed: None + # revealed: tuple[Solution[A=int, B=Never]] + reveal_type((quantified & invalid).solutions(inferable=tuple[A, B])) + + static_assert(not (quantified & invalid)) + # TODO: no error + # error: [static-assert-error] + static_assert((~quantified & invalid) == invalid) +``` + +## E4: correlated visible outputs + +`C₁` relates `X` to `Y`, while `C₂` relates `X` to `Z`. Both constraints must hold for the same +choice of `X`. The two valid solution families are `(Y = int, Z = Invariant[int])` and +`(Y = str, Z = Invariant[str])`; the cross-pairing `(Y = int, Z = Invariant[str])` is invalid. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Invariant[T]: + def get(self) -> T: + raise NotImplementedError + + def set(self, value: T) -> None: ... + +def correlated_outputs[X, Y, Z]() -> None: + # C₁(X, Y) = (X = int ∧ Y = int) ∨ (X = str ∧ Y = str) + c1_int = ConstraintSet.range(int, X, int) & ConstraintSet.range(int, Y, int) + c1_str = ConstraintSet.range(str, X, str) & ConstraintSet.range(str, Y, str) + c1 = c1_int | c1_str + + # C₂(X, Z) = (Z = Invariant[X]) + c2 = ConstraintSet.range(Invariant[X], Z, Invariant[X]) + + # ∃X. C₁(X, Y) ∧ C₂(X, Z) + body = c1 & c2 + quantified = body.exists(tuple[X]) + + # TODO: revealed: tuple[Solution[X=int, Y=int, Z=Invariant[int]], Solution[X=str, Y=str, Z=Invariant[str]]] + # revealed: tuple[Solution[X=int | Y@correlated_outputs, Z=Invariant[X@correlated_outputs] | Invariant[int], Y=int], Solution[X=str | Y@correlated_outputs, Z=Invariant[X@correlated_outputs] | Invariant[str], Y=str]] + reveal_type(body.solutions(inferable=tuple[X, Y, Z])) + # revealed: tuple[Solution[Z=Invariant[int], Y=int], Solution[Z=Invariant[str], Y=str]] + reveal_type(quantified.solutions(inferable=tuple[Y, Z])) + + # (Y = int ∧ Z = Invariant[int]) ∨ (Y = str ∧ Z = Invariant[str]) + expected_int = ConstraintSet.range(int, Y, int) & ConstraintSet.range(Invariant[int], Z, Invariant[int]) + expected_str = ConstraintSet.range(str, Y, str) & ConstraintSet.range(Invariant[str], Z, Invariant[str]) + expected = expected_int | expected_str + static_assert(quantified == expected) + static_assert(~quantified == ~expected) + + # (Y = int ∧ Z = Invariant[str]) + invalid_cross = ConstraintSet.range(int, Y, int) & ConstraintSet.range(Invariant[str], Z, Invariant[str]) + static_assert(not (quantified & invalid_cross)) + # revealed: None + reveal_type((quantified & invalid_cross).solutions(inferable=tuple[Y, Z])) +``` + +## E5: finite domain + +The declaration of `X` constrains it to be either `int` or `str`. Each valid choice gives a separate +solution family. After `X` is quantified, `Y` and `Z` must remain correlated in each solution, and +specializations outside the declared domain must be rejected. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Invariant[T]: + def get(self) -> T: + raise NotImplementedError + + def set(self, value: T) -> None: ... + +def finite_domain[X: (int, str), Y, Z]() -> None: + # ∃X ∈ {int, str}. C(X, Y, Z) + # C(X, Y, Z) = (Y = X) ∧ (Z = Invariant[X]) + body = ConstraintSet.range(X, Y, X) & ConstraintSet.range(Invariant[X], Z, Invariant[X]) + quantified = body.exists(tuple[X]) + + # TODO: revealed: tuple[Solution[X=int, Y=int, Z=Invariant[int]], Solution[X=str, Y=str, Z=Invariant[str]]] + # revealed: tuple[Solution[X=Y@finite_domain, Y=X@finite_domain, Z=Invariant[X@finite_domain] | Invariant[Y@finite_domain]]] + reveal_type(body.solutions(inferable=tuple[X, Y, Z])) + # TODO: revealed: tuple[Solution[Y=int, Z=Invariant[int]], Solution[Y=str, Z=Invariant[str]]] + # revealed: tuple[Solution[Z=Invariant[Y@finite_domain]]] + reveal_type(quantified.solutions(inferable=tuple[Y, Z])) + + # (Y = int ∧ Z = Invariant[int]) ∨ (Y = str ∧ Z = Invariant[str]) + expected_int = ConstraintSet.range(int, Y, int) & ConstraintSet.range(Invariant[int], Z, Invariant[int]) + expected_str = ConstraintSet.range(str, Y, str) & ConstraintSet.range(Invariant[str], Z, Invariant[str]) + expected = expected_int | expected_str + # TODO: no error + # error: [static-assert-error] + static_assert(quantified == expected) + # TODO: no error + # error: [static-assert-error] + static_assert(~quantified == ~expected) + + # (Y = int ∧ Z = Invariant[str]) + invalid_cross = ConstraintSet.range(int, Y, int) & ConstraintSet.range(Invariant[str], Z, Invariant[str]) + static_assert(not (quantified & invalid_cross)) + # revealed: None + reveal_type((quantified & invalid_cross).solutions(inferable=tuple[Y, Z])) + + # (Y = bytes ∧ Z = Invariant[bytes]) + invalid_domain = ConstraintSet.range(bytes, Y, bytes) & ConstraintSet.range(Invariant[bytes], Z, Invariant[bytes]) + static_assert(not (quantified & invalid_domain)) + # TODO: revealed: None + # revealed: tuple[Solution[Z=Invariant[Y@finite_domain] | Invariant[bytes], Y=bytes]] + reveal_type((quantified & invalid_domain).solutions(inferable=tuple[Y, Z])) +``` + +## E6: alternation and negative polarity + +The declarations of `X` and `Y` constrain both variables to `int` or `str`. For every valid choice +of `Y`, there is a matching choice of `X`. Reversing the quantifiers would require one choice of `X` +to work for every `Y` and is therefore false. Negating the relation asks whether there is a `Y` with +no matching `X`; an `int`-only relation shows that a missing `str` case is rejected. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +def alternation[X: (int, str), Y: (int, str)]() -> None: + # R(X, Y) = (X = int ∧ Y = int) ∨ (X = str ∧ Y = str) + x_int = ConstraintSet.range(int, X, int) + x_str = ConstraintSet.range(str, X, str) + y_int = ConstraintSet.range(int, Y, int) + y_str = ConstraintSet.range(str, Y, str) + relation = (x_int & y_int) | (x_str & y_str) + + # ∀Y. ∃X. R(X, Y) + forall_y_exists_x = relation.exists(tuple[X]).for_all(tuple[Y]) + # TODO: no error + # error: [static-assert-error] + static_assert(forall_y_exists_x) + # TODO: no error + # error: [static-assert-error] + static_assert(not ~forall_y_exists_x) + + # ∃X. ∀Y. R(X, Y) + exists_x_forall_y = relation.for_all(tuple[Y]).exists(tuple[X]) + static_assert(not exists_x_forall_y) + + # ∃Y. ∀X. ¬R(X, Y) + counterexample = (~relation).for_all(tuple[X]).exists(tuple[Y]) + # TODO: no error + # error: [static-assert-error] + static_assert(not counterexample) + static_assert(counterexample == ~forall_y_exists_x) + + int_only = x_int & y_int + missing_str = int_only.exists(tuple[X]).for_all(tuple[Y]) + static_assert(not missing_str) +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index c6f1e0777c..516ebb065d 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4073,6 +4073,14 @@ impl<'db> Type<'db> { )) .into() } + Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked)) + if name == "exists" => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetExists(tracked), + )) + .into() + } Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked)) if name == "for_all" => { @@ -6538,6 +6546,7 @@ impl<'db> Type<'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -6905,6 +6914,7 @@ impl<'db> Type<'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -7173,6 +7183,7 @@ impl<'db> Type<'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 5a7c6f6ac7..62a8a5999c 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -2809,7 +2809,10 @@ impl<'db> Bindings<'db> { )); } - Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetForAll(tracked)) => { + Type::KnownBoundMethod( + method @ (KnownBoundMethodType::ConstraintSetExists(tracked) + | KnownBoundMethodType::ConstraintSetForAll(tracked)), + ) => { let [Some(typevars)] = overload.parameter_types() else { continue; }; @@ -2822,11 +2825,12 @@ impl<'db> Bindings<'db> { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - constraints.load(db, tracked.constraints(db)).for_all( - db, - constraints, - typevars, - ) + let set = constraints.load(db, tracked.constraints(db)); + if matches!(method, KnownBoundMethodType::ConstraintSetExists(_)) { + set.reduce_inferable(db, constraints, typevars) + } else { + set.for_all(db, constraints, typevars) + } }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index b52f3706c7..4c6cf9fe31 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1219,6 +1219,9 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { KnownBoundMethodType::ConstraintSetSatisfies(_) => { return f.write_str("bound method `ConstraintSet.satisfies`"); } + KnownBoundMethodType::ConstraintSetExists(_) => { + return f.write_str("bound method `ConstraintSet.exists`"); + } KnownBoundMethodType::ConstraintSetForAll(_) => { return f.write_str("bound method `ConstraintSet.for_all`"); } diff --git a/crates/ty_python_semantic/src/types/method.rs b/crates/ty_python_semantic/src/types/method.rs index d69e91c3f5..de7a17e2c1 100644 --- a/crates/ty_python_semantic/src/types/method.rs +++ b/crates/ty_python_semantic/src/types/method.rs @@ -222,6 +222,7 @@ pub enum KnownBoundMethodType<'db> { ConstraintSetNever, ConstraintSetImpliesSubtypeOf(InternedConstraintSet<'db>), ConstraintSetSatisfies(InternedConstraintSet<'db>), + ConstraintSetExists(InternedConstraintSet<'db>), ConstraintSetForAll(InternedConstraintSet<'db>), ConstraintSetSatisfiedByAllTypeVars(InternedConstraintSet<'db>), ConstraintSetSolutionsFor(InternedConstraintSet<'db>), @@ -261,6 +262,7 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -308,6 +310,7 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -330,6 +333,7 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -490,7 +494,8 @@ impl<'db> KnownBoundMethodType<'db> { ))) } - KnownBoundMethodType::ConstraintSetForAll(_) => { + KnownBoundMethodType::ConstraintSetExists(_) + | KnownBoundMethodType::ConstraintSetForAll(_) => { Either::Right(std::iter::once(Signature::new( Parameters::standard([Parameter::positional_only(Some(Name::new_static( "typevars", @@ -625,6 +630,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { KnownBoundMethodType::ConstraintSetSatisfies(_), KnownBoundMethodType::ConstraintSetSatisfies(_), ) + | ( + KnownBoundMethodType::ConstraintSetExists(_), + KnownBoundMethodType::ConstraintSetExists(_), + ) | ( KnownBoundMethodType::ConstraintSetForAll(_), KnownBoundMethodType::ConstraintSetForAll(_), @@ -658,6 +667,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) @@ -674,6 +684,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index b723c68e3e..61236bbadf 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -262,6 +262,7 @@ impl<'db> Type<'db> { | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetExists(_) | KnownBoundMethodType::ConstraintSetForAll(_) | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) diff --git a/crates/ty_vendored/ty_extensions/_internal.pyi b/crates/ty_vendored/ty_extensions/_internal.pyi index 11896f6b01..31146606b1 100644 --- a/crates/ty_vendored/ty_extensions/_internal.pyi +++ b/crates/ty_vendored/ty_extensions/_internal.pyi @@ -117,6 +117,11 @@ class ConstraintSet: `other`. """ + def exists(self, typevars: TypeForm[tuple[object, ...]]) -> Self: + """ + Existentially abstracts the given type variables from this constraint set. + """ + def for_all(self, typevars: TypeForm[tuple[object, ...]]) -> Self: """ Universally abstracts the given type variables from this constraint set. From 77b9cc565ddd21b901bbac100a518177ee267bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9r=C3=A8?= Date: Thu, 23 Jul 2026 20:12:09 -0700 Subject: [PATCH 043/390] [ty] Introduce shared primitives for parsing backticks in docstrings (#26928) ## Summary This refactors the code span parsing we do in docstrings around a cursor-based scanner and explicit ranged backtick runs and spans. Ultimately, this serves two purposes: 1. To remove duplicated offset arithmetic from inline-link and trailing-parenthetical parsing. 2. To expose an iterator over backtick-delimited fragments of text for [downstream normalization](https://github.com/astral-sh/ruff/pull/26923). ## Test plan See included tests. --- .../ty_ide/src/docstring/document/syntax.rs | 424 +++++++++++++++--- .../src/docstring/markdown/general/inline.rs | 43 +- 2 files changed, 397 insertions(+), 70 deletions(-) diff --git a/crates/ty_ide/src/docstring/document/syntax.rs b/crates/ty_ide/src/docstring/document/syntax.rs index 95a1a18c1d..9ad96adb9f 100644 --- a/crates/ty_ide/src/docstring/document/syntax.rs +++ b/crates/ty_ide/src/docstring/document/syntax.rs @@ -1,7 +1,7 @@ use ruff_python_stdlib::identifiers::is_identifier; use ruff_python_trivia::{Cursor, leading_indentation, tab_offset_u32}; use ruff_source_file::UniversalNewlines; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use super::rst::is_field_list_marker; @@ -62,56 +62,291 @@ pub(in crate::docstring) fn starts_with_markdown_list_item(line: &str) -> bool { /// /// For example, this returns `true` for ``"`value`"`` and `false` for /// ``"`value` trailing"``. -pub(in crate::docstring) fn is_markdown_code_span(text: &str) -> bool { - find_backtick_run(text, TextSize::ZERO).and_then(|opening| markdown_code_span(text, opening)) - == Some(TextRange::up_to(TextSize::of(text))) +pub(crate) fn is_markdown_code_span(text: &str) -> bool { + let mut tokens = InlineMarkupScanner::new(text); + let Some(InlineMarkupToken::Code(_)) = tokens.next() else { + return false; + }; + + tokens.next().is_none() } -/// Returns the byte range of the first consecutive backtick run at or after `from`. +/// Emits non-overlapping tokens that completely span the source text. /// -/// For example, searching ``"value `code`"`` from the start returns the range covering the -/// opening ``"`"``. -pub(in crate::docstring) fn find_backtick_run(text: &str, from: TextSize) -> Option { - let from = from.to_usize(); - let start = from + text.get(from..)?.find('`')?; - let len = text[start..] - .bytes() - .take_while(|byte| *byte == b'`') - .count(); - Some(TextRange::new( - TextSize::of(&text[..start]), - TextSize::of(&text[..start + len]), - )) +/// Currently supports `Code` for complete, unescaped backtick-delimited segments and `Text` +/// for everything else. +/// +/// For example: +/// +/// ```text +/// InlineMarkupScanner::new("before `code` after") +/// => Text("before "), Code("code"), Text(" after") +/// ``` +struct InlineMarkupScanner<'a> { + /// The scanner used to find complete code spans. + scanner: BacktickScanner<'a>, + /// The end of the last token returned to the caller. + last_token_end: TextSize, + /// A span saved while its preceding text is returned first. + pending_span: Option>, } -/// Returns the Markdown code span delimited by `opening`, if it has a matching closing run. +impl<'a> InlineMarkupScanner<'a> { + /// Creates a lossless iterator over plain text and complete backtick-delimited code spans. + /// + /// Escaped or unmatched backticks remain part of an [`InlineMarkupToken::Text`] token. + fn new(source: &'a str) -> Self { + Self { + scanner: BacktickScanner::new(source), + last_token_end: TextSize::ZERO, + pending_span: None, + } + } + + fn take_remaining_text(&mut self) -> Option> { + let source_end = self.scanner.source.text_len(); + let remaining = TextRange::new(self.last_token_end, source_end); + self.last_token_end = source_end; + (!remaining.is_empty()).then(|| InlineMarkupToken::Text(&self.scanner.source[remaining])) + } +} + +impl<'a> Iterator for InlineMarkupScanner<'a> { + type Item = InlineMarkupToken<'a>; + + fn next(&mut self) -> Option { + let span = if let Some(span) = self.pending_span.take() { + // Emit the span saved while returning its preceding text on the previous call. + span + } else { + loop { + // Without another backtick run, the remaining source is all plain text. + let Some(opening) = self.scanner.next() else { + return self.take_remaining_text(); + }; + + // Escaped runs are literal source text, so continue looking for the next possible + // opening without emitting a token boundary. + if opening.is_escaped() { + continue; + } + + // Without a closing delimiter, callers cannot treat the opening or any later runs as + // structured markup. Emit the remainder as one text token. + let Some(span) = self.scanner.eat_span(opening) else { + return self.take_remaining_text(); + }; + break span; + } + }; + + if self.last_token_end < span.start() { + let preceding_text = TextRange::new(self.last_token_end, span.start()); + self.last_token_end = span.start(); + self.pending_span = Some(span); + return Some(InlineMarkupToken::Text( + &self.scanner.source[preceding_text], + )); + } + + debug_assert_eq!(self.last_token_end, span.start()); + self.last_token_end = span.end(); + Some(InlineMarkupToken::Code(span)) + } +} + +/// One lossless token produced by [`InlineMarkupScanner`]. /// -/// For example, the opening run in "``value`with:ticks`` trailing" produces the range covering -/// "``value`with:ticks``". -pub(in crate::docstring) fn markdown_code_span( - text: &str, - opening: TextRange, -) -> Option { - let mut search_from = opening.end(); - loop { - let closing = find_backtick_run(text, search_from)?; - if closing.len() == opening.len() { - return Some(opening.cover(closing)); +/// For example: +/// +/// ```text +/// source "before `code` after" +/// tokens Text("before "), Code("code"), Text(" after") +/// ``` +/// +/// Escaped and unmatched backticks remain text. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum InlineMarkupToken<'a> { + /// Source text outside a complete, unescaped backtick span. + Text(&'a str), + /// A complete code span whose backtick delimiters have equal lengths. + Code(BacktickSpan<'a>), +} + +/// Source text delimited by ordered backtick runs of equal length. +/// +/// For example: +/// +/// ```text +/// source "before ``code`` after" +/// range() 7..15 +/// is_single() false +/// content() "code" +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct BacktickSpan<'a> { + /// The source between the opening and closing delimiters. + content: &'a str, + /// The byte range including both delimiters. + range: TextRange, + /// The byte length of either delimiter. + delimiter_len: TextSize, +} + +impl<'a> BacktickSpan<'a> { + /// Returns whether both delimiters consist of one backtick. + pub(crate) fn is_single(self) -> bool { + self.delimiter_len == TextSize::new(1) + } + + /// Returns the source between the opening and closing runs. + pub(crate) fn content(self) -> &'a str { + self.content + } +} + +impl Ranged for BacktickSpan<'_> { + fn range(&self) -> TextRange { + self.range + } +} + +/// Scans consecutive backtick runs in source order. +/// +/// The scanner can consume a complete span after returning its opening run. For example: +/// +/// ```text +/// source "prefix ``code`` suffix" +/// opening = next() Some(BacktickRun("``")) +/// as_str() "code`` suffix" +/// eat_span(opening) Some(BacktickSpan("``code``")) +/// as_str() " suffix" +/// ``` +#[derive(Clone)] +pub(crate) struct BacktickScanner<'a> { + /// The complete source whose runs are returned. + source: &'a str, + /// The current scan position within `source`. + cursor: Cursor<'a>, +} + +impl<'a> BacktickScanner<'a> { + /// Creates a scanner positioned at the start of `source`. + pub(crate) fn new(source: &'a str) -> Self { + Self { + source, + cursor: Cursor::new(source), + } + } + + /// Creates a scanner positioned at `offset` within `source`. + fn starts_at(offset: TextSize, source: &'a str) -> Self { + let mut scanner = Self::new(source); + scanner.cursor.skip_bytes(offset.to_usize()); + scanner + } + + /// Returns the remaining source. + pub(crate) fn as_str(&self) -> &'a str { + self.cursor.as_str() + } + + /// Consumes the closing run that matches the most recently returned `opening`. + /// + /// Returns `None` without advancing when no matching run exists. + pub(crate) fn eat_span(&mut self, opening: BacktickRun) -> Option> { + debug_assert_eq!(opening.end(), self.cursor.offset()); + + let mut lookahead = self.clone(); + while let Some(closing) = lookahead.next() { + if let Some(span) = self.span(opening, closing) { + *self = lookahead; + return Some(span); + } + } + None + } + + /// Creates a span from two ordered runs of equal length. + /// + /// Both runs must use ranges in this scanner's source. + pub(crate) fn span( + &self, + opening: BacktickRun, + closing: BacktickRun, + ) -> Option> { + debug_assert!(opening.end() <= closing.start()); + + if opening.range.len() != closing.range.len() { + return None; + } + + Some(BacktickSpan { + content: &self.source[TextRange::new(opening.end(), closing.start())], + range: opening.range.cover(closing.range), + delimiter_len: opening.range.len(), + }) + } +} + +impl Iterator for BacktickScanner<'_> { + type Item = BacktickRun; + + fn next(&mut self) -> Option { + self.cursor.eat_while(|character| character != '`'); + if self.cursor.is_eof() { + return None; } - search_from = closing.end(); + + let start = self.cursor.offset(); + self.cursor.eat_while(|character| character == '`'); + let range = TextRange::new(start, self.cursor.offset()); + + let preceding_backslashes = self.source[..start.to_usize()] + .bytes() + .rev() + .take_while(|byte| *byte == b'\\') + .count(); + let escaped = !preceding_backslashes.is_multiple_of(2); + + Some(BacktickRun { range, escaped }) } } -/// Returns whether the backtick run at `index` is escaped by a preceding backslash. +/// One consecutive run of backticks found by [`BacktickScanner`]. /// -/// For example, the backtick in ``"\`"`` is escaped, while the backtick in ``"\\`"`` is not. -pub(in crate::docstring) fn is_backtick_run_escaped(text: &str, index: usize) -> bool { - !text[..index] - .bytes() - .rev() - .take_while(|byte| *byte == b'\\') - .count() - .is_multiple_of(2) +/// For example: +/// +/// ```text +/// source "before \\`` after" +/// range() 8..10 +/// is_single() false +/// is_escaped() true +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct BacktickRun { + /// The byte range of the consecutive backticks. + range: TextRange, + /// Whether an odd-length backslash run escapes the first backtick. + escaped: bool, +} + +impl BacktickRun { + /// Returns whether this run consists of one backtick. + pub(crate) fn is_single(self) -> bool { + self.range.len() == TextSize::new(1) + } + + /// Returns whether a preceding odd-length backslash run escapes this run. + pub(crate) fn is_escaped(self) -> bool { + self.escaped + } +} + +impl Ranged for BacktickRun { + fn range(&self) -> TextRange { + self.range + } } /// Returns the end of an indented Markdown or reStructuredText container block. @@ -236,18 +471,31 @@ pub(super) fn split_trailing_parenthetical(value: &str) -> Option<(&str, &str)> let mut outermost_opening = None; let mut cursor = Cursor::new(value); - while let Some(character) = cursor.bump() { - let index = cursor.offset().to_usize() - character.len_utf8(); + loop { + let start = cursor.offset(); + let Some(character) = cursor.bump() else { + break; + }; + match character { - '\'' | '"' => consume_quoted_string(&mut cursor, character), - '`' if !is_backtick_run_escaped(value, index) => { - let opening = find_backtick_run(value, TextSize::of(&value[..index]))?; - let span = markdown_code_span(value, opening).unwrap_or(opening); - cursor.skip_bytes((span.end() - cursor.offset()).to_usize()); + quote @ ('\'' | '"') => consume_quoted_string(&mut cursor, quote), + '`' => { + let mut scanner = BacktickScanner::starts_at(start, value); + let opening = scanner.next()?; + if opening.is_escaped() { + // The loop has consumed only the first, escaped backtick. Leave the rest of + // the run for the next iteration, where it may open a shorter span. + continue; + } + + let end = scanner + .eat_span(opening) + .map_or_else(|| opening.end(), |span| span.end()); + cursor.skip_bytes((end - cursor.offset()).to_usize()); } '(' => { if depth == 0 { - outermost_opening = Some(index); + outermost_opening = Some(start); } depth += 1; } @@ -255,9 +503,9 @@ pub(super) fn split_trailing_parenthetical(value: &str) -> Option<(&str, &str)> depth = depth.checked_sub(1)?; if depth == 0 && cursor.is_eof() { let opening = outermost_opening?; - let prefix = value[..opening].trim(); - let contents = value[opening + '('.len_utf8()..index].trim(); - return Some((prefix, contents)); + let (prefix, parenthetical) = value.split_at(opening.to_usize()); + let contents = parenthetical.strip_prefix('(')?.strip_suffix(')')?; + return Some((prefix.trim(), contents.trim())); } } _ => {} @@ -282,9 +530,56 @@ pub(super) fn indentation(line: &str) -> TextSize { #[cfg(test)] mod tests { use super::{ - is_markdown_code_span, split_once_at_top_level_colon, split_trailing_parenthetical, + BacktickScanner, InlineMarkupScanner, InlineMarkupToken, TextSize, is_markdown_code_span, + split_once_at_top_level_colon, split_trailing_parenthetical, }; + #[test] + fn scans_backtick_runs_and_spans() { + let mut scanner = BacktickScanner::starts_at(TextSize::new(7), "prefix ``code`` suffix"); + let opening = scanner.next().expect("an opening backtick run"); + + assert!(!opening.is_single()); + assert!(!opening.is_escaped()); + assert_eq!(scanner.as_str(), "code`` suffix"); + + let span = scanner.eat_span(opening).expect("a matching backtick run"); + assert!(!span.is_single()); + assert_eq!(span.content(), "code"); + assert_eq!(scanner.as_str(), " suffix"); + } + + #[test] + fn scans_text_and_code_tokens() { + let source = "é :class:`~pkg.Widget` or ``literal`tick`` β"; + + assert_eq!( + token_contents(source), + vec![ + ("text", "é :class:"), + ("code", "~pkg.Widget"), + ("text", " or "), + ("code", "literal`tick"), + ("text", " β"), + ] + ); + } + + #[test] + fn scans_code_at_source_boundaries() { + assert_eq!( + token_contents("`first` and `last`"), + vec![("code", "first"), ("text", " and "), ("code", "last")] + ); + } + + #[test] + fn preserves_escaped_and_unmatched_backticks_as_text() { + let source = r"\`literal\` and `unfinished"; + + assert_eq!(token_contents(source), vec![("text", source)]); + } + #[test] fn recognizes_complete_markdown_code_spans() { for (text, expected) in [ @@ -375,6 +670,22 @@ mod tests { ); } + #[test] + fn ignores_parentheses_inside_code_spans_after_escaped_backtick() { + assert_eq!( + split_trailing_parenthetical(r"value (\``)`)"), + Some(("value", r"\``)`")) + ); + } + + #[test] + fn treats_unmatched_backticks_as_plain_parenthetical_text() { + assert_eq!( + split_trailing_parenthetical("value (`unfinished)"), + Some(("value", "`unfinished")) + ); + } + #[test] fn ignores_parentheses_after_escaped_quotes() { assert_eq!( @@ -392,4 +703,13 @@ mod tests { fn rejects_parenthesized_group_before_trailing_text() { assert_eq!(split_trailing_parenthetical("value (str) or None"), None); } + + fn token_contents(source: &str) -> Vec<(&'static str, &str)> { + InlineMarkupScanner::new(source) + .map(|token| match token { + InlineMarkupToken::Text(text) => ("text", text), + InlineMarkupToken::Code(code) => ("code", code.content()), + }) + .collect() + } } diff --git a/crates/ty_ide/src/docstring/markdown/general/inline.rs b/crates/ty_ide/src/docstring/markdown/general/inline.rs index 2c79fa8852..4c8d583051 100644 --- a/crates/ty_ide/src/docstring/markdown/general/inline.rs +++ b/crates/ty_ide/src/docstring/markdown/general/inline.rs @@ -49,11 +49,9 @@ use std::borrow::Cow; -use ruff_text_size::TextSize; +use ruff_text_size::{Ranged, TextSize}; -use crate::docstring::document::syntax::{ - find_backtick_run, is_backtick_run_escaped, markdown_code_span, -}; +use crate::docstring::document::syntax::BacktickScanner; /// Exposes an interface for rendering a line of prose that may contain a hyperlink. #[derive(Default)] @@ -278,28 +276,26 @@ enum Candidate<'a> { /// Finds the first complete hyperlink or plausible wrapped candidate in `input`. fn find_link(input: &str) -> Option<(usize, Candidate<'_>)> { - let mut offset = TextSize::ZERO; + let mut scanner = BacktickScanner::new(input); // Visit each backtick run that could delimit inline markup. - while let Some(run) = find_backtick_run(input, offset) { + while let Some(run) = scanner.next() { let index = run.start().to_usize(); // An escaped run is literal text, so continue immediately after it. - if is_backtick_run_escaped(input, index) { - offset = run.end(); + if run.is_escaped() { continue; } - // Try parsing a link only when a single backtick has valid surrounding characters. - if run.len() == TextSize::new(1) - && is_link_start(input, index) + // Try parsing a link only when the backtick run has valid surrounding characters. + if is_link_start(input, index) && let Some(candidate) = parse_candidate(&input[index..]) { return Some((index, candidate)); } // Skip other backtick-delimited spans rather than searching inside them. - offset = markdown_code_span(input, run)?.end(); + scanner.eat_span(run)?; } None @@ -310,7 +306,13 @@ fn find_link(input: &str) -> Option<(usize, Candidate<'_>)> { /// Plausible wrapped labels without a closing backtick remain pending; /// malformed or unsupported forms return `None`. fn parse_candidate(input: &str) -> Option> { - let after_opening = input.strip_prefix('`')?; + let mut scanner = BacktickScanner::new(input); + let opening = scanner.next()?; + if opening.start() != TextSize::ZERO { + return None; + } + + let after_opening = scanner.as_str(); if after_opening .chars() .next() @@ -319,7 +321,11 @@ fn parse_candidate(input: &str) -> Option> { return None; } - let Some(closing) = find_backtick_run(input, TextSize::new(1)) else { + let Some(closing) = scanner.next() else { + if !opening.is_single() { + return None; + } + // Eliminate candidates whose content already contains a disallowed // backslash or closing `>`, or whose target cannot become HTTP(S). A // partial URI scheme remains valid so it can wrap immediately after @@ -333,21 +339,22 @@ fn parse_candidate(input: &str) -> Option> { } return Some(Candidate::Pending); }; - if closing.len() != TextSize::new(1) { + let span = scanner.span(opening, closing)?; + if !span.is_single() { return None; } - let content = &input[1..closing.start().to_usize()]; + let content = span.content(); if content.contains('\\') { return None; } - let after_closing = &input[closing.end().to_usize()..]; + let after_closing = scanner.as_str(); let underscore_count = after_closing .bytes() .take_while(|byte| *byte == b'_') .count(); - let len = closing.end().to_usize() + underscore_count; + let len = span.end().to_usize() + underscore_count; if !(1..=2).contains(&underscore_count) || !is_link_suffix(&after_closing[underscore_count..]) { return None; } From 7e6e8b7dbd3240d4420e1cdd3ef990ef1897f076 Mon Sep 17 00:00:00 2001 From: Denys Zhak Date: Fri, 24 Jul 2026 13:05:06 +0200 Subject: [PATCH 044/390] [ty] Add a lint rule for combined abstract and final decorators (#26932) Closes https://github.com/astral-sh/ty/issues/3876 ## Summary Lint rule for combined abstract and final decorators ## Test Plan Md tests --- crates/ty/docs/rules.md | 262 ++++++++++-------- .../lint_docs/abstract-and-final-method.md | 22 ++ .../resources/mdtest/final.md | 59 ++++ .../src/types/diagnostic.rs | 10 + .../src/types/infer/builder/function.rs | 24 +- ty.schema.json | 10 + 6 files changed, 269 insertions(+), 118 deletions(-) create mode 100644 crates/ty_python_semantic/resources/lint_docs/abstract-and-final-method.md diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index f3f9f9d22f..5a3f80c9e6 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -2,13 +2,49 @@ # Rules +## `abstract-and-final-method` + + +Default level: error · +Added in 0.0.64 · +Related issues · +View source + + + +**What it does** + + +Checks for methods decorated with both `@abstractmethod` and `@final`. + +**Why is this bad?** + + +An abstract method must be overridden for a subclass to become concrete, but a final +method cannot be overridden. Combining the decorators therefore makes it impossible +for a subclass to provide a concrete implementation. + +**Example** + + +```python +from abc import ABC, abstractmethod +from typing import final + + +class Base(ABC): + @final + @abstractmethod + def method(self) -> None: ... # error +``` + ## `abstract-method-in-final-class` Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -54,7 +90,7 @@ class Derived(Base): # error Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -118,7 +154,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -201,7 +237,7 @@ value = unknown # ty: ignore[unresolved-reference] Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -256,7 +292,7 @@ Foo.method() # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -284,7 +320,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -319,7 +355,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -353,7 +389,7 @@ a = 1 # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -388,7 +424,7 @@ class C(A, B): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -424,7 +460,7 @@ class B(A): ... # error Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -460,7 +496,7 @@ type B = A # error Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -497,7 +533,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -536,7 +572,7 @@ old_func() # error: [deprecated] Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -569,7 +605,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -600,7 +636,7 @@ class B(A, A): ... # error Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -643,7 +679,7 @@ class A: # error Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -720,7 +756,7 @@ def foo() -> "intt\b": ... # error Default level: warn · Added in 0.0.50 · Related issues · -View source +View source @@ -760,7 +796,7 @@ def g(value: ~A) -> None: ... # error: [experimental-syntax] Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -795,7 +831,7 @@ def my_function() -> int: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -911,7 +947,7 @@ def test() -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -947,7 +983,7 @@ class C(A, B): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -977,7 +1013,7 @@ t[3] # error Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -1014,7 +1050,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -1115,7 +1151,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1147,7 +1183,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1178,7 +1214,7 @@ a: int = "" # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1236,7 +1272,7 @@ C.instance_only_var = 56 # error Default level: error · Added in 0.0.33 · Related issues · -View source +View source @@ -1282,7 +1318,7 @@ class Sub(Base): Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1324,7 +1360,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1351,7 +1387,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1381,7 +1417,7 @@ with 1: # error Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1434,7 +1470,7 @@ See: Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1470,7 +1506,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1502,7 +1538,7 @@ a: str # error Default level: warn · Added in 0.0.20 · Related issues · -View source +View source @@ -1559,7 +1595,7 @@ class Pet(Enum): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1623,7 +1659,7 @@ This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](h Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1676,7 +1712,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1727,7 +1763,7 @@ class NonFrozenChild(FrozenBase): # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1776,7 +1812,7 @@ class D(Generic[U, T]): ... # error Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1872,7 +1908,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1920,7 +1956,7 @@ carol = Person(name="Carol", aeg=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -1982,7 +2018,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2022,7 +2058,7 @@ def f(t: TypeVar("U")): ... # ty: ignore[invalid-type-form] Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -2072,7 +2108,7 @@ match object(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2107,7 +2143,7 @@ class B(metaclass=42): ... # error Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2225,7 +2261,7 @@ Correct use of `@override` is enforced by ty's [`invalid-explicit-override`](#in Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -2282,7 +2318,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict Default level: warn · Added in 0.0.31 · Related issues · -View source +View source @@ -2330,7 +2366,7 @@ admin[0] # "Alice" Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -2368,7 +2404,7 @@ Baz = NewType("Baz", int | str) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2425,7 +2461,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2454,7 +2490,7 @@ def f(a: int = ""): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2490,7 +2526,7 @@ P2 = ParamSpec() # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2526,7 +2562,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2597,7 +2633,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2629,7 +2665,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2740,7 +2776,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2791,7 +2827,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2837,7 +2873,7 @@ InvalidAlias = TypeAliasType("InvalidAlias", list[T], type_params=(list[T],)) # Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2904,7 +2940,7 @@ Bar[int] # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2937,7 +2973,7 @@ TYPE_CHECKING = "" # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2973,7 +3009,7 @@ b: Annotated[int] # error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -3030,7 +3066,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -3074,7 +3110,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3131,7 +3167,7 @@ V = TypeVar("V", list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -3173,7 +3209,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.28 · Related issues · -View source +View source @@ -3209,7 +3245,7 @@ class Child(Base): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -3252,7 +3288,7 @@ def f(options: dict[str, object]): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -3287,7 +3323,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.25 · Related issues · -View source +View source @@ -3322,7 +3358,7 @@ def gen() -> Iterator[int]: Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -3389,7 +3425,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -3439,7 +3475,7 @@ def g(arg: object): Default level: warn · Added in 0.0.30 · Related issues · -View source +View source @@ -3482,7 +3518,7 @@ Movie = TypedDict("Film", {"title": str}) # error: [mismatched-type-name] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3513,7 +3549,7 @@ func() # error Default level: ignore · Added in 0.0.41 · Related issues · -View source +View source @@ -3572,7 +3608,7 @@ class ExplicitChild(Parent): Default level: ignore · Added in 0.0.45 · Related issues · -View source +View source @@ -3611,7 +3647,7 @@ def handle(m: re.Match[str]) -> str: Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -3650,7 +3686,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3688,7 +3724,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.30 · Related issues · -View source +View source @@ -3726,7 +3762,7 @@ class Sub(Super): ... # error: [non-callable-init-subclass] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3755,7 +3791,7 @@ for i in 34: # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3783,7 +3819,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -3820,7 +3856,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -3857,7 +3893,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3888,7 +3924,7 @@ f(1, x=2) # error Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3919,7 +3955,7 @@ f(x=1) # error Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3958,7 +3994,7 @@ A.c # error Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3997,7 +4033,7 @@ A()[0] # error Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4043,7 +4079,7 @@ from module import a # error Default level: warn · Added in 0.0.23 · Related issues · -View source +View source @@ -4075,7 +4111,7 @@ html.parser # error Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4112,7 +4148,7 @@ print(x) # error Default level: warn · Added in 0.0.60 · Related issues · -View source +View source @@ -4187,7 +4223,7 @@ def test() -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4222,7 +4258,7 @@ cast(int, f()) # error Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -4260,7 +4296,7 @@ class C: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -4304,7 +4340,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4339,7 +4375,7 @@ static_assert(int(2.0 * 3.0) == 6) # error Default level: warn · Added in 0.0.39 · Related issues · -View source +View source @@ -4390,7 +4426,7 @@ Consider using [`functools.total_ordering`][total_ordering] instead, which does Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4424,7 +4460,7 @@ class B(A): ... # error Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -4464,7 +4500,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4494,7 +4530,7 @@ f("foo") # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4533,7 +4569,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4591,7 +4627,7 @@ class A: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -4635,7 +4671,7 @@ class C(Generic[T]): Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4664,7 +4700,7 @@ reveal_type(1) # revealed: Literal[1] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4695,7 +4731,7 @@ f(x=1, y=2) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4728,7 +4764,7 @@ A().foo # error Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -4803,7 +4839,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4832,7 +4868,7 @@ import foo # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4860,7 +4896,7 @@ print(x) # error Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -4907,7 +4943,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4956,7 +4992,7 @@ b1 < b2 < b1 # error Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -5003,7 +5039,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5036,7 +5072,7 @@ A() + A() # error Default level: warn · Added in 0.0.21 · Related issues · -View source +View source @@ -5156,7 +5192,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -5235,7 +5271,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/lint_docs/abstract-and-final-method.md b/crates/ty_python_semantic/resources/lint_docs/abstract-and-final-method.md new file mode 100644 index 0000000000..390c976af7 --- /dev/null +++ b/crates/ty_python_semantic/resources/lint_docs/abstract-and-final-method.md @@ -0,0 +1,22 @@ +## What it does + +Checks for methods decorated with both `@abstractmethod` and `@final`. + +## Why is this bad? + +An abstract method must be overridden for a subclass to become concrete, but a final +method cannot be overridden. Combining the decorators therefore makes it impossible +for a subclass to provide a concrete implementation. + +## Example + +```python +from abc import ABC, abstractmethod +from typing import final + + +class Base(ABC): + @final + @abstractmethod + def method(self) -> None: ... # error +``` diff --git a/crates/ty_python_semantic/resources/mdtest/final.md b/crates/ty_python_semantic/resources/mdtest/final.md index 3ffc42d09b..c24ab5d396 100644 --- a/crates/ty_python_semantic/resources/mdtest/final.md +++ b/crates/ty_python_semantic/resources/mdtest/final.md @@ -452,6 +452,65 @@ class F: def not_a_method(): ... ``` +## A method cannot be both abstract and final + +An abstract method must be overridden for a subclass to become concrete, but a final method cannot +be overridden. + +```py +from abc import abstractmethod +from typing import final + +class A: + @final + @abstractmethod + def first(self) -> None: ... # error: [abstract-and-final-method] + + # Decorator order does not matter. + @abstractmethod + @final + def second(self) -> None: ... # error: [abstract-and-final-method] + @abstractmethod + def abstract(self) -> None: ... + @final + def final(self) -> None: ... +``` + +## An overloaded method cannot be both abstract and final + +`runtime.py`: + +```py +from abc import ABC, abstractmethod +from typing import final, overload + +class A(ABC): + @overload + def method(self, value: int) -> int: ... + @overload + def method(self, value: str) -> str: ... + @final + @abstractmethod + def method(self, value: int | str) -> int | str: # error: [abstract-and-final-method] + raise NotImplementedError +``` + +`stub.pyi`: + +```pyi +from abc import abstractmethod +from typing import final, overload + +class A: + @overload + @final + @abstractmethod + def method(self, value: int) -> int: ... # error: [abstract-and-final-method] + @overload + @abstractmethod + def method(self, value: str) -> str: ... +``` + ## An `@final` method is overridden by an implicit instance attribute ```py diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index e57f14d9a7..06d3e5fbb4 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -134,6 +134,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INEFFECTIVE_FINAL); registry.register_lint(&FINAL_ON_NON_METHOD); registry.register_lint(&FINAL_WITHOUT_VALUE); + registry.register_lint(&ABSTRACT_AND_FINAL_METHOD); registry.register_lint(&ABSTRACT_METHOD_IN_FINAL_CLASS); registry.register_lint(&CALL_ABSTRACT_METHOD); registry.register_lint(&TYPE_ASSERTION_FAILURE); @@ -959,6 +960,15 @@ declare_lint! { } } +declare_lint! { + #[doc = include_str!("../../resources/lint_docs/abstract-and-final-method.md")] + pub(crate) static ABSTRACT_AND_FINAL_METHOD = { + summary: "detects methods that are both abstract and final", + status: LintStatus::stable("0.0.64"), + default_level: Level::Error, + } +} + declare_lint! { #[doc = include_str!("../../resources/lint_docs/abstract-method-in-final-class.md")] pub(crate) static ABSTRACT_METHOD_IN_FINAL_CLASS = { diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs index fb47f50f02..9e8f9ddfe7 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -5,11 +5,11 @@ use crate::{ KnownClass, KnownInstanceType, ParamSpecAttrKind, SubclassOfInner, SubclassOfType, Type, TypeContext, TypeVarKind, UnionType, diagnostic::{ - FINAL_ON_NON_METHOD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_TYPE_FORM, - USELESS_OVERLOAD_BODY, add_type_expression_reference_link, - is_invalid_typed_dict_literal, report_implicit_return_type, - report_invalid_generator_function_return_type, report_invalid_return_type, - report_shadowed_type_variable, + ABSTRACT_AND_FINAL_METHOD, FINAL_ON_NON_METHOD, INVALID_PARAMETER_DEFAULT, + INVALID_PARAMSPEC, INVALID_TYPE_FORM, USELESS_OVERLOAD_BODY, + add_type_expression_reference_link, is_invalid_typed_dict_literal, + report_implicit_return_type, report_invalid_generator_function_return_type, + report_invalid_return_type, report_shadowed_type_variable, }, function::{ FunctionBodyKind, FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, @@ -381,6 +381,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { diagnostic.info("`@final` is only meaningful on methods and classes"); } + if function_decorators + .contains(FunctionDecorators::ABSTRACT_METHOD | FunctionDecorators::FINAL) + && self + .index + .scope(self.scope().file_scope_id(db)) + .kind() + .is_class() + && let Some(builder) = self.context.report_lint(&ABSTRACT_AND_FINAL_METHOD, name) + { + builder.into_diagnostic(format_args!( + "Method `{name}` cannot be both `@abstractmethod` and `@final`", + )); + } + let has_defaults = parameters .iter_non_variadic_params() .any(|param| param.default.is_some()); diff --git a/ty.schema.json b/ty.schema.json index 4f5d00f4ad..ef68c1c4b0 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -328,6 +328,16 @@ "Rules": { "type": "object", "properties": { + "abstract-and-final-method": { + "title": "detects methods that are both abstract and final", + "description": "## What it does\n\nChecks for methods decorated with both `@abstractmethod` and `@final`.\n\n## Why is this bad?\n\nAn abstract method must be overridden for a subclass to become concrete, but a final\nmethod cannot be overridden. Combining the decorators therefore makes it impossible\nfor a subclass to provide a concrete implementation.\n\n## Example\n\n```python\nfrom abc import ABC, abstractmethod\nfrom typing import final\n\n\nclass Base(ABC):\n @final\n @abstractmethod\n def method(self) -> None: ... # error\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "abstract-method-in-final-class": { "title": "detects `@final` classes with unimplemented abstract methods", "description": "## What it does\n\nChecks for `@final` classes that have unimplemented abstract methods.\n\n## Why is this bad?\n\nA class decorated with `@final` cannot be subclassed. If such a class has abstract\nmethods that are not implemented, the class can never be properly instantiated, as\nthe abstract methods can never be implemented (since subclassing is prohibited).\n\nAt runtime, instantiation of classes with unimplemented abstract methods is only\nprevented for classes that have `ABCMeta` (or a subclass of it) as their metaclass.\nHowever, type checkers also enforce this for classes that do not use `ABCMeta`, since\nthe intent for the class to be abstract is clear from the use of `@abstractmethod`.\n\n## Example\n\n```python\nfrom abc import ABC, abstractmethod\nfrom typing import final\n\n\nclass Base(ABC):\n @abstractmethod\n def method(self) -> int: ...\n\n\n@final\n# `Derived` does not implement `method`\nclass Derived(Base): # error\n pass\n```", From 7c06333a2c38758d9650d6b5a8b32d5531293f14 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 24 Jul 2026 05:47:17 -0700 Subject: [PATCH 045/390] [ty] Fix identity narrowing for NewTypes (#26439) Co-authored-by: Alex Waygood --- .../resources/mdtest/annotations/new_types.md | 3 +- .../resources/mdtest/comparison/identity.md | 44 +++- .../mdtest/comparison/intersections.md | 38 ++- .../mdtest/narrow/conditionals/is.md | 217 ++++++++++++++++++ .../resources/mdtest/ty_extensions.md | 5 +- .../src/types/infer/comparisons.rs | 112 ++++++++- crates/ty_python_semantic/src/types/narrow.rs | 111 ++++++++- 7 files changed, 506 insertions(+), 24 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index 699dddfd58..08b419e34c 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -578,8 +578,7 @@ A = NewType("A", EllipsisType) static_assert(is_singleton(A)) static_assert(is_single_valued(A)) reveal_type(type(A(...)) is EllipsisType) # revealed: Literal[True] -# TODO: This should be `Literal[True]` also. -reveal_type(A(...) is ...) # revealed: bool +reveal_type(A(...) is ...) # revealed: Literal[True] B = NewType("B", int) static_assert(not is_singleton(B)) diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/identity.md b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md index 51e75fd529..d5ff379655 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/identity.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md @@ -1,4 +1,6 @@ -# Identity tests +# Identity comparisons + +## Basic comparisons ```py from typing_extensions import TypeAliasType @@ -40,3 +42,43 @@ def _(a1: TypeAliasType, a2: TypeAliasType): reveal_type(list[int] is list[int]) # revealed: bool reveal_type(list[int] is not list[int]) # revealed: bool ``` + +## Repeated identity comparisons after narrowing `Unknown` + +Once `value is None` has succeeded, the value can only be the `None` singleton even when its +original type is `Unknown`. + +```py +from ty_extensions import Unknown + +def f(value: Unknown) -> None: + if value is None: + reveal_type(value) # revealed: Unknown & None + reveal_type(value is not None) # revealed: Literal[False] +``` + +## Identity comparisons for the same constrained `TypeVar` + +All occurrences of the same constrained `TypeVar` use the same constraint. Here, each constraint +contains only one object, so two values with that `TypeVar` must be identical. This remains true +when one occurrence appears through a type alias. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from types import EllipsisType +from typing import TypeVar + +T = TypeVar("T", None, EllipsisType) + +def f(left: T, right: T) -> None: + reveal_type(left is right) # revealed: Literal[True] + +type Alias[X] = X + +def aliased(left: Alias[T], right: T) -> None: + reveal_type(left is right) # revealed: Literal[True] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md index bd85637b08..56515ecc86 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md @@ -92,19 +92,49 @@ def _(x: int): ### Identity comparisons -```py -class A: ... +The type `~None` excludes the `None` object, so its identity comparisons with `None` have definite +results. +```py def _(o: object): - a = A() n = None if o is not None: - reveal_type(o) # revealed: ~None + reveal_type(o) # revealed: ~None reveal_type(o is n) # revealed: Literal[False] reveal_type(o is not n) # revealed: Literal[True] ``` +A single-member enum contains only one object. A value excluded from `E` cannot be `E.ONLY`, so the +branch below is unreachable and must not emit an attribute error. + +```py +from enum import Enum +from ty_extensions import Not + +class E(Enum): + ONLY = 1 + +def f(value: Not[E]) -> None: + if value is E.ONLY: + reveal_type(value) # revealed: Never + value.does_not_exist # no error (unreachable branch) +``` + +After `not isinstance(value, B)`, `value` cannot be identical to a `B` instance. This remains true +when `value` has also been narrowed to `A`, so the inner branch is unreachable. + +```py +class A: ... +class B: ... + +def f(value: object, other_b: B) -> None: + if isinstance(value, A) and not isinstance(value, B): + if value is other_b: + reveal_type(value) # revealed: Never + value.does_not_exist # no error (unreachable branch) +``` + ## Diagnostics ### Unsupported operators for positive contributions diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md index 6661ca5db0..75268fc9af 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md @@ -190,6 +190,223 @@ def narrow_generic_alias[T: (Generic[int], Specialized)](klass: type[T]) -> None reveal_type(Generic[int]) # revealed: ``` +## Narrowing with a constrained `TypeVar` + +The `is` check below can discard `int` because it cannot be `None` or `...`. The `is not` check +cannot discard either remaining type: depending on the current constraint, either value could differ +from `other`. + +```py +from types import EllipsisType +from typing import TypeVar + +T = TypeVar("T", None, EllipsisType) + +def takes_singleton(value: None | EllipsisType) -> None: ... +def f(value: int | None | EllipsisType, other: T) -> None: + if value is other: + takes_singleton(value) + if value is not other: + reveal_type(value) # revealed: int | (None & ~T@f) | (EllipsisType & ~T@f) +``` + +## `is` with `NewType`s + +### Distinct `NewType`s with the same base + +Calling a `NewType` returns its argument unchanged. Values with distinct `NewType`s over `Foo` can +therefore be the same object even though their types are disjoint. The examples below cover direct +comparisons and narrowing through unions and intersections. + +```py +from typing import NewType +from ty_extensions import Intersection + +class Foo: ... +class FooSub(Foo): ... + +FooNewType1 = NewType("FooNewType1", Foo) +FooNewType2 = NewType("FooNewType2", Foo) + +def same_base(foo1: FooNewType1, foo2: FooNewType2) -> None: + reveal_type(foo1 is foo2) # revealed: bool + if foo1 is foo2: + reveal_type(foo1) # revealed: FooNewType1 + reveal_type(foo2) # revealed: FooNewType2 + +def union(value: FooNewType1 | None, other: FooNewType2) -> None: + if value is other: + reveal_type(value) # revealed: FooNewType1 + +def intersection(left: Intersection[FooNewType1, FooSub], right: FooNewType2) -> None: + if left is right: + reveal_type(right) # revealed: FooNewType2 & FooSub +``` + +### `NewType`s in `TypeVar` bounds and constraints + +`NewType`s inside `TypeVar` bounds and constraints can likewise refer to the same runtime object. +Comparing the distinct `TypeVar`s below is not always false, and a true branch keeps the original +`TypeVar`. + +```py +from typing import NewType, TypeVar + +class Foo: ... + +FooNewType1 = NewType("FooNewType1", Foo) +FooNewType2 = NewType("FooNewType2", Foo) +FooNewType3 = NewType("FooNewType3", Foo) +FooNewType4 = NewType("FooNewType4", Foo) + +BoundedT = TypeVar("BoundedT", bound=FooNewType1) +BoundedU = TypeVar("BoundedU", bound=FooNewType2) + +def bounded_typevars(left: BoundedT, right: BoundedU) -> tuple[BoundedU, BoundedU]: + reveal_type(left is right) # revealed: bool + if left is right: + # TODO: This should narrow to `BoundedT & BoundedU` and avoid the false positive below. + reveal_type(left) # revealed: BoundedT@bounded_typevars + return (left, left) # error: [invalid-return-type] + return (right, right) + +ConstrainedT = TypeVar("ConstrainedT", FooNewType1, FooNewType2) +ConstrainedU = TypeVar("ConstrainedU", FooNewType3, FooNewType4) + +def constrained_typevars(left: ConstrainedT, right: ConstrainedU) -> tuple[ConstrainedU, ConstrainedU]: + reveal_type(left is right) # revealed: bool + if left is right: + # TODO: This should narrow to `ConstrainedT & ConstrainedU` and avoid the false positive. + reveal_type(left) # revealed: ConstrainedT@constrained_typevars + return (left, left) # error: [invalid-return-type] + return (right, right) +``` + +Every constraint below is a `NewType` based on `EllipsisType`, so `other` always refers to the same +`...` object as a `SingletonC` value. After an `is not` check, repeating the opposite check must be +unreachable. + +```py +from types import EllipsisType +from typing import NewType, TypeVar +from typing_extensions import assert_never + +SingletonA = NewType("SingletonA", EllipsisType) +SingletonB = NewType("SingletonB", EllipsisType) +SingletonC = NewType("SingletonC", EllipsisType) + +SingletonT = TypeVar("SingletonT", SingletonA, SingletonB) + +def direct(value: SingletonC | int, other: SingletonT) -> None: + if value is not other: + if value is other: + assert_never(value) +``` + +### Narrowing an object to a `NewType` in the true branch + +If an object is identical to a value with a `NewType`, the true branch narrows the object to that +`NewType` rather than its underlying type. + +```py +from typing import NewType + +UserId = NewType("UserId", int) + +def preserve_newtype(x: object, user_id: UserId) -> None: + if x is user_id: + reveal_type(x) # revealed: UserId +``` + +### Comparing `NewType`s with literals + +Calls to `NewType` return their arguments unchanged. Comparisons with `bool` and `int` literals can +therefore succeed, so the true branches below remain reachable. + +```py +from typing import Literal, NewType + +BoolNewType = NewType("BoolNewType", bool) +IntNewType = NewType("IntNewType", int) + +def literals(true: Literal[True], b: BoolNewType, forty_two: Literal[42], i: IntNewType) -> None: + if b is true: + reveal_type(true) # revealed: Literal[True] + reveal_type(b) # revealed: BoolNewType + if i is forty_two: + reveal_type(forty_two) # revealed: Literal[42] + reveal_type(i) # revealed: IntNewType +``` + +### `is not` with singleton `NewType`s + +Both `NewType`s below are based on `EllipsisType`, which contains only the `...` object. The +`is not` branch therefore removes the `NewType` alternative. + +```py +from types import EllipsisType +from typing import NewType + +SingletonA = NewType("SingletonA", EllipsisType) +SingletonB = NewType("SingletonB", EllipsisType) + +def singleton_is_not(value: SingletonA | int, other: SingletonB) -> None: + if value is not other: + reveal_type(value) # revealed: int +``` + +### Static exclusions + +The type `~Literal[True]` excludes the literal type but accepts the distinct `BoolNewType`. However, +`BoolNewType(True)` returns `True` unchanged, so `value is True` can be either true or false. + +```py +from __future__ import annotations + +from typing import Literal, NewType + +BoolNewType = NewType("BoolNewType", bool) + +def excludes_true(value: ~Literal[True]) -> None: + reveal_type(value is True) # revealed: bool + +excludes_true(BoolNewType(True)) +``` + +Similarly, `int & ~Literal[1]` accepts `IntNewType(1)`, which returns the `1` object unchanged, so +the comparison remains possible. + +```py +from typing import Literal, NewType + +IntNewType = NewType("IntNewType", int) + +def excludes_one(value: int & ~Literal[1]) -> None: + reveal_type(value is 1) # revealed: bool + +excludes_one(IntNewType(1)) +``` + +### Comparisons that are always false + +An identity comparison is still always false when the two runtime types are distinct final classes. + +```py +from typing import NewType, final + +@final +class A: ... + +@final +class B: ... + +ANewType = NewType("ANewType", A) +BNewType = NewType("BNewType", B) + +def disjoint_bases(a: ANewType, b: BNewType) -> None: + reveal_type(a is b) # revealed: Literal[False] +``` + ## `is` where the other operand is a call expression ```py diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index 65e236130a..959ba055f8 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -31,9 +31,8 @@ o: Not[()] p: Not[(int,)] def static_truthiness(not_one: Not[Literal[1]]) -> None: - # TODO: `bool` is not incorrect, but these would ideally be `Literal[True]` and `Literal[False]` - # respectively, since all possible runtime objects that are created by the literal syntax `1` - # are members of the type `Literal[1]` + # A `NewType` over `int` is distinct from `Literal[1]` but can refer to the same runtime object, + # so neither identity comparison has a definite result. reveal_type(not_one is not 1) # revealed: bool reveal_type(not_one is 1) # revealed: bool diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index 5be62d22cf..b7b77dd9b7 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -13,11 +13,75 @@ use crate::types::equality::{ use crate::types::tuple::TupleSpec; use crate::types::{ DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, - LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, Type, TypeContext, + LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, Type, TypeContext, TypeTransformer, TypeVarBoundOrConstraints, UnionBuilder, }; use ty_python_core::Truthiness; +impl<'db> Type<'db> { + /// Upcast `self` to a type that conservatively describes its possible runtime objects in an + /// identity comparison. + /// + /// A `NewType` wrapper is an identity function at runtime, so it contributes its concrete base + /// type here while remaining distinct for ordinary type relations and intersections. + /// + /// Negative intersection elements are generally omitted. A static exclusion does not imply a + /// runtime exclusion: `NewType("N", bool)(True)` can inhabit `~Literal[True]`, but evaluates + /// to the `True` singleton at runtime. However, excluding an entire nominal instance type is + /// stable under `NewType` erasure, so constraints such as `~None` and `~SomeClass` are + /// preserved. + pub(crate) fn identity_comparison_type(self, db: &'db dyn Db) -> Type<'db> { + struct IdentityComparisonUpcasting; + + fn upcast<'db>( + db: &'db dyn Db, + ty: Type<'db>, + visitor: &TypeTransformer<'db, IdentityComparisonUpcasting>, + ) -> Type<'db> { + match ty { + Type::TypeAlias(alias) => { + visitor.visit_type(db, ty, || upcast(db, alias.value_type(db), visitor)) + } + Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db), + Type::TypeVar(typevar) => visitor.visit_type(db, ty, || { + match typevar.typevar(db).bound_or_constraints(db) { + Some(bound_or_constraints) => { + upcast(db, bound_or_constraints.as_type(db), visitor) + } + None => ty, + } + }), + Type::Union(union) => union.map(db, |element| upcast(db, *element, visitor)), + Type::Intersection(intersection) => { + let mut builder = IntersectionBuilder::new(db); + for element in intersection.positive(db) { + builder = builder.add_positive(upcast(db, *element, visitor)); + } + for element in intersection.negative(db) { + if element.resolve_type_alias(db).is_nominal_instance() { + builder = builder.add_negative(*element); + } + } + builder.build() + } + _ => ty, + } + } + + upcast( + db, + self, + &TypeTransformer::::default(), + ) + } + + /// Return `true` if `self` and `other` cannot describe the same runtime object. + pub(crate) fn is_disjoint_from_for_identity(self, db: &'db dyn Db, other: Type<'db>) -> bool { + self.identity_comparison_type(db) + .is_disjoint_from(db, other.identity_comparison_type(db)) + } +} + /// Whether the intersection type is on the left or right side of the comparison. #[derive(Debug, Clone, Copy)] enum IntersectionOn { @@ -132,10 +196,10 @@ pub(super) fn infer_binary_type_comparison<'db>( ) -> Result, UnsupportedComparisonError<'db>> { let db = context.db(); - // Note: identity (is, is not) for equal builtin types is unreliable and not part of the - // language spec. - // - `[ast::CompOp::Is]`: return `false` if unequal, `bool` if equal - // - `[ast::CompOp::IsNot]`: return `true` if unequal, `bool` if equal + // Identity comparisons between equal builtin types are unreliable and not guaranteed by the + // language specification: + // - `is` returns `false` if the types are disjoint and `bool` if they overlap. + // - `is not` returns `true` if the types are disjoint and `bool` if they overlap. let try_dunder = |policy: MemberLookupPolicy| { let rich_comparison = |op| infer_rich_comparison(db, left, right, op, policy); let membership_test_comparison = |op, range: TextRange| { @@ -174,6 +238,44 @@ pub(super) fn infer_binary_type_comparison<'db>( } }; + // Keep two occurrences of the same `TypeVar` symbolic. Replacing them with their bounds or + // constraints would lose their shared specialization: a `TypeVar` constrained to `None` and + // `EllipsisType` chooses the same singleton for both operands, not independent alternatives. + if matches!(op, ast::CmpOp::Is | ast::CmpOp::IsNot) + && !matches!( + ( + left.resolve_type_alias(db), + right.resolve_type_alias(db) + ), + (Type::TypeVar(left), Type::TypeVar(right)) if left.is_same_typevar_as(db, right) + ) + { + // `NewType` is an identity function at runtime, so distinct NewTypes can still contain the + // same object: + // + // UserId = NewType("UserId", int) + // OrderId = NewType("OrderId", int) + // user_id is order_id # possibly true + // + // Widen both operands to the types of their possible runtime objects before using the + // ordinary comparison logic. Keeping the usual recursive dispatch preserves facts carried + // by unions and intersections after widening. + let left_identity = left.identity_comparison_type(db); + let right_identity = right.identity_comparison_type(db); + if left_identity != left || right_identity != right { + return visitor.visit(db, (left, op, right), || { + infer_binary_type_comparison( + context, + left_identity, + op, + right_identity, + range, + visitor, + ) + }); + } + } + let soundness_policy = ComparisonSoundnessPolicy::from_analysis_settings(db.analysis_settings(context.file())); let comparison_truthiness = match op { diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 853c30f0bf..4a9b1e156c 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -3041,14 +3041,96 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { match op { ast::CmpOp::IsNot => { - if rhs_ty.is_singleton(self.db) { - Some(rhs_ty.negate(self.db)) + let rhs_identity_ty = rhs_ty.identity_comparison_type(self.db); + // An `is not` check can narrow the LHS only when the RHS identifies a single + // runtime object. There are two ways this can happen: + // + // 1. The RHS's runtime identity type is itself a singleton. This includes ordinary + // singleton types, such as `None`, and distinct `NewType`s that all wrap the + // same singleton object. Narrow against the runtime identity type so that every + // static type representing that object is excluded. + // + // 2. The RHS is a constrained `TypeVar` whose constraints are all singletons. For + // example, `T = TypeVar("T", None, EllipsisType)` can be specialized to either + // `None` or `EllipsisType` across different calls. Within one specialization, + // however, every occurrence of `T` resolves to the same constraint, so all + // values of type `T` are either `None` or all are `...`. Keep `T` symbolic so + // that excluding it preserves its relationship with subsequent occurrences of + // the same specialization. + // + // In every other case, the RHS might identify multiple objects even within a + // single specialization, so excluding its entire type would be unsound. + let rhs_constraint = if rhs_identity_ty.is_singleton(self.db) { + rhs_identity_ty + } else if matches!(rhs_ty.resolve_type_alias(self.db), Type::TypeVar(_)) + && rhs_ty.is_singleton(self.db) + { + rhs_ty } else { - // Non-singletons cannot be safely narrowed using `is not` - None + return None; + }; + Some(rhs_constraint.negate(self.db)) + } + ast::CmpOp::Is => { + // Preserve the nominal RHS constraint for ordinary overlaps. If a `NewType` + // creates additional runtime-only overlap, retain the corresponding part of the + // LHS as well so that applying the constraint does not erase that possibility. + let mut builder = UnionBuilder::new(self.db).add(rhs_ty); + let rhs_resolved = rhs_ty.resolve_type_alias(self.db); + let rhs_identity_ty = rhs_ty.identity_comparison_type(self.db); + let add_runtime_overlap = |builder: UnionBuilder<'db>, element: Type<'db>| { + let overlaps_only_at_runtime = |rhs_element| { + element.is_disjoint_from(self.db, rhs_element) + && !element.is_disjoint_from_for_identity(self.db, rhs_element) + }; + let has_runtime_only_overlap = match rhs_resolved { + Type::Union(union) => union + .elements(self.db) + .iter() + .copied() + .any(overlaps_only_at_runtime), + Type::TypeVar(typevar) => { + match typevar.typevar(self.db).bound_or_constraints(self.db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + overlaps_only_at_runtime(bound) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + constraints + .elements(self.db) + .iter() + .copied() + .any(overlaps_only_at_runtime) + } + None => overlaps_only_at_runtime(rhs_ty), + } + } + rhs_ty => overlaps_only_at_runtime(rhs_ty), + }; + if !has_runtime_only_overlap { + return builder; + } + + let runtime_overlap = + IntersectionType::from_two_elements(self.db, element, rhs_identity_ty); + builder.add(if runtime_overlap.is_never() { + element + } else { + runtime_overlap + }) + }; + + if let Type::Union(union) = lhs_ty.resolve_type_alias(self.db) { + builder = union + .elements(self.db) + .iter() + .copied() + .fold(builder, add_runtime_overlap); + } else { + builder = add_runtime_overlap(builder, lhs_ty); } + + Some(builder.build()) } - ast::CmpOp::Is => Some(rhs_ty), ast::CmpOp::In => self.evaluate_expr_in(lhs_ty, rhs_ty), ast::CmpOp::NotIn => self.evaluate_expr_not_in(lhs_ty, rhs_ty), _ => None, @@ -3181,6 +3263,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // if t[0] is not None: // reveal_type(t) # tuple[int, int] if matches!(&**ops, [ast::CmpOp::Is | ast::CmpOp::IsNot]) + && let is_positive_check = is_positive == (ops[0] == ast::CmpOp::Is) && let ast::Expr::Subscript(subscript) = left.expression_value() && let Type::Union(union) = inference .expression_type(&*subscript.value) @@ -3191,19 +3274,29 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .as_int_literal() && let Ok(index) = i32::try_from(index) && let rhs_ty = inference.expression_type(&comparators[0]) - && rhs_ty.is_singleton(self.db) + && let rhs_identity_ty = rhs_ty.identity_comparison_type(self.db) + && let rhs_identity_is_singleton = rhs_identity_ty.is_singleton(self.db) + && let rhs_is_correlated_singleton = (!rhs_identity_is_singleton + && matches!(rhs_ty.resolve_type_alias(self.db), Type::TypeVar(_)) + && rhs_ty.is_singleton(self.db)) + && (is_positive_check || rhs_is_correlated_singleton || rhs_identity_is_singleton) { - let is_positive_check = is_positive == (ops[0] == ast::CmpOp::Is); let filtered = union.filter(self.db, |elem| { elem.tuple_instance_spec(self.db) .and_then(|spec| spec.py_index(self.db, index).ok()) .is_none_or(|el_ty| { if is_positive_check { // `is X` context: keep tuples where element could be X - !el_ty.is_disjoint_from(self.db, rhs_ty) + !el_ty.is_disjoint_from_for_identity(self.db, rhs_ty) + } else if rhs_is_correlated_singleton { + // Preserve the shared specialization instead of excluding every + // constraint in the projected union. + !el_ty.is_subtype_of(self.db, rhs_ty) } else { // `is not X` context: keep tuples where element is not always X - !el_ty.is_subtype_of(self.db, rhs_ty) + !el_ty + .identity_comparison_type(self.db) + .is_subtype_of(self.db, rhs_identity_ty) } }) }); From f9d8ab4d372d8c85c1ecf8ec383f1255ed28d4a0 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 24 Jul 2026 13:59:00 +0100 Subject: [PATCH 046/390] [ty] Improve identity comparison inference for singleton types (#27126) ## Summary (Stacked on top of https://github.com/astral-sh/ruff/pull/26439) This PR pulls out identity comparison inference from the big `match` in `comparisons.rs` in favour of handling them at a higher level. Identity comparisons are fundamentally different to other kinds of comparisons: rather than falling back to any dunder methods, they work solely on the basis of memory-address comparison at runtime. Pulling them out of the big `match` in `comparisons.rs` both simplifies the code and improves the accuracy and precision of our inference. A result of this is, for example, that we now infer `Literal[True]` as the result of the comparison `True is True`. ## Test plan mdtests --- .../resources/mdtest/comparison/identity.md | 83 +++ crates/ty_python_semantic/src/types.rs | 7 + .../src/types/infer/builder.rs | 3 +- .../src/types/infer/comparisons.rs | 546 ++++++++++-------- 4 files changed, 384 insertions(+), 255 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/identity.md b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md index d5ff379655..9ae34717f5 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/identity.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md @@ -5,6 +5,19 @@ ```py from typing_extensions import TypeAliasType +reveal_type(False is False) # revealed: Literal[True] +reveal_type(False is True) # revealed: Literal[False] +reveal_type(1 is True) # revealed: Literal[False] +reveal_type(... is ...) # revealed: Literal[True] +reveal_type(NotImplemented is NotImplemented) # revealed: Literal[True] + +# two occurences of the same literal `1` do not necessarily share the +# same memory address, as `1` is not a singleton (but they also *might*!) +reveal_type(1 is 1) # revealed: bool + +# but two different integer literals definitely don't share the same memory address +reveal_type(1 is 2) # revealed: Literal[False] + class A: ... def _(a1: A, a2: A, o: object): @@ -43,6 +56,76 @@ reveal_type(list[int] is list[int]) # revealed: bool reveal_type(list[int] is not list[int]) # revealed: bool ``` +## Identity comparisons with NewTypes + +Two variables cannot share the same memory address if they have disjoint nominal-instance backing +types: + +```py +def f(x: str, y: int): + reveal_type(x is y) # revealed: Literal[False] + reveal_type(x is not y) # revealed: Literal[True] +``` + +but simple disjointness is not enough -- these two `NewType`s are disjoint, yet `B(True)` shares the +same memory address as `C(True)`. Disjointness of the nominal-instance types *backing* the `NewType` +is the necessary precondition: + +```py +from typing import NewType, Literal +from ty_extensions._internal import is_disjoint_from + +B = NewType("B", bool) +C = NewType("C", bool) + +reveal_type(is_disjoint_from(B, C)) # revealed: ConstraintSet[Literal[True]] +reveal_type(is_disjoint_from(B, Literal[True])) # revealed: ConstraintSet[Literal[True]] + +def f(x: B, y: C): + reveal_type(x is y) # revealed: bool + reveal_type(x is not y) # revealed: bool + reveal_type(x is True) # revealed: bool + reveal_type(x is False) # revealed: bool + reveal_type(x is not True) # revealed: bool + reveal_type(x is not False) # revealed: bool +``` + +Nonetheless, if the NewType's nominal backing type is disjoint from another type, `Literal` boolean +types can still be inferred as a result: + +```py +from typing import NewType, Literal + +N = NewType("N", str) +O = NewType("O", int) + +def f(x: N, y: int, z: O): + reveal_type(x is y) # revealed: Literal[False] + reveal_type(x is not y) # revealed: Literal[True] + reveal_type(x is z) # revealed: Literal[False] + reveal_type(x is not z) # revealed: Literal[True] +``` + +## Identity comparisons see through type aliases + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal + +type SoTrue = Literal[True] +type SoFalse = Literal[False] + +def f(x: SoTrue, y: SoFalse): + reveal_type(x is True) # revealed: Literal[True] + reveal_type(x is False) # revealed: Literal[False] + reveal_type(x is y) # revealed: Literal[False] + reveal_type(x is not y) # revealed: Literal[True] +``` + ## Repeated identity comparisons after narrowing `Unknown` Once `value is None` has succeeded, the value can only be the `None` singleton even when its diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 516ebb065d..bac4ea9d5e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1181,6 +1181,13 @@ impl<'db> Type<'db> { }) } + pub(crate) const fn as_intersection(self) -> Option> { + match self { + Type::Intersection(intersection) => Some(intersection), + _ => None, + } + } + pub const fn is_unknown(&self) -> bool { matches!( self, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 145b5e0b49..88a5686390 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -162,7 +162,7 @@ mod typed_dict; mod typeguard; mod typevar; -use super::comparisons::{self, BinaryComparisonVisitor}; +use super::comparisons; /// A helper to track if we already know that declared and inferred types are the same. #[derive(Debug, Clone, PartialEq, Eq)] @@ -10718,7 +10718,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { *op, right_ty, range, - &BinaryComparisonVisitor::new(Ok(Type::bool_literal(true))), ) .unwrap_or_else(|error| { report_unsupported_comparison( diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index b7b77dd9b7..93fc63e538 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -90,15 +90,15 @@ enum IntersectionOn { } /// A [`CycleDetector`] that is used in [`infer_binary_type_comparison`]. -pub(super) type BinaryComparisonVisitor<'db> = CycleDetector< +type BinaryComparisonVisitor<'db> = CycleDetector< 'db, ast::CmpOp, - (Type<'db>, ast::CmpOp, Type<'db>), + (Type<'db>, NonIdentityOperator, Type<'db>), Result, UnsupportedComparisonError<'db>>, 1, >; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum RichCompareOperator { Eq, Ne, @@ -147,17 +147,42 @@ impl RichCompareOperator { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum MembershipTestCompareOperator { +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum MembershipOperator { In, NotIn, } -impl From for ast::CmpOp { - fn from(value: MembershipTestCompareOperator) -> Self { +impl MembershipOperator { + const fn is_in(self) -> bool { + matches!(self, MembershipOperator::In) + } + + const fn is_not_in(self) -> bool { + matches!(self, MembershipOperator::NotIn) + } +} + +impl From for ast::CmpOp { + fn from(value: MembershipOperator) -> Self { match value { - MembershipTestCompareOperator::In => ast::CmpOp::In, - MembershipTestCompareOperator::NotIn => ast::CmpOp::NotIn, + MembershipOperator::In => ast::CmpOp::In, + MembershipOperator::NotIn => ast::CmpOp::NotIn, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum NonIdentityOperator { + Rich(RichCompareOperator), + Membership(MembershipOperator), +} + +impl From for ast::CmpOp { + fn from(value: NonIdentityOperator) -> Self { + match value { + NonIdentityOperator::Rich(rich_op) => rich_op.into(), + NonIdentityOperator::Membership(membership_op) => membership_op.into(), } } } @@ -192,14 +217,105 @@ pub(super) fn infer_binary_type_comparison<'db>( op: ast::CmpOp, right: Type<'db>, range: TextRange, +) -> Result, UnsupportedComparisonError<'db>> { + let db = context.db(); + + let op = match op { + ast::CmpOp::Is | ast::CmpOp::IsNot => { + let is_positive = op == ast::CmpOp::Is; + + let is_singleton_or_intersection_with_singleton = |ty: Type<'db>| { + ty.is_singleton(db) + || ty + .resolve_type_alias(db) + .as_intersection() + .is_some_and(|intersection| { + intersection + .positive(db) + .iter() + .any(|ty| ty.is_singleton(db)) + }) + }; + + // Keep two occurrences of the same `TypeVar` symbolic. Replacing them with their bounds or + // constraints would lose their shared specialization: a `TypeVar` constrained to `None` and + // `EllipsisType` chooses the same singleton for both operands, not independent alternatives. + if let Type::TypeVar(left) = left.resolve_type_alias(db) + && let Type::TypeVar(right) = right.resolve_type_alias(db) + && left.is_same_typevar_as(db, right) + && is_singleton_or_intersection_with_singleton(Type::TypeVar(left)) + { + return Ok(Type::bool_literal(is_positive)); + } + + // `NewType` is an identity function at runtime, so distinct NewTypes can still contain the + // same object: + // + // UserId = NewType("UserId", int) + // OrderId = NewType("OrderId", int) + // UserId(1) is OrderId(1) # true, even though the two NewTypes are disjoint types! + // + // Widen both operands to the types of their possible runtime objects before using the + // ordinary comparison logic. + let left_identity = left.identity_comparison_type(db); + let right_identity = right.identity_comparison_type(db); + + // If the identity types are disjoint, the operands cannot refer to the same + // runtime object. + // + // Otherwise, knowing that both types are non-disjoint singletons is still not enough + // to establish that they refer to the *same* singleton: `is_disjoint_from` can return + // false when disjointness cannot be proven. For example, two enum-literal types will + // always both be singletons, but if their aliases are unknown, we cannot tell whether + // they denote the same member or distinct members (one might be an alias to the other). + // + // We therefore require one singleton type to be a subtype of the other before inferring + // definite identity. Either direction suffices, which also handles cases like + // `None` and `Unknown & None`. + let result = if left_identity.is_disjoint_from(db, right_identity) { + Type::bool_literal(!is_positive) + } else if is_singleton_or_intersection_with_singleton(left_identity) + && is_singleton_or_intersection_with_singleton(right_identity) + && (left_identity.is_subtype_of(db, right_identity) + || right_identity.is_subtype_of(db, left_identity)) + { + Type::bool_literal(is_positive) + } else { + KnownClass::Bool.to_instance(db) + }; + + return Ok(result); + } + ast::CmpOp::Eq => NonIdentityOperator::Rich(RichCompareOperator::Eq), + ast::CmpOp::NotEq => NonIdentityOperator::Rich(RichCompareOperator::Ne), + ast::CmpOp::Lt => NonIdentityOperator::Rich(RichCompareOperator::Lt), + ast::CmpOp::LtE => NonIdentityOperator::Rich(RichCompareOperator::Le), + ast::CmpOp::Gt => NonIdentityOperator::Rich(RichCompareOperator::Gt), + ast::CmpOp::GtE => NonIdentityOperator::Rich(RichCompareOperator::Ge), + ast::CmpOp::In => NonIdentityOperator::Membership(MembershipOperator::In), + ast::CmpOp::NotIn => NonIdentityOperator::Membership(MembershipOperator::NotIn), + }; + + infer_binary_type_comparison_inner( + context, + left, + op, + right, + range, + &BinaryComparisonVisitor::new(Ok(Type::bool_literal(true))), + ) +} + +fn infer_binary_type_comparison_inner<'db>( + context: &InferContext<'db, '_>, + left: Type<'db>, + op: NonIdentityOperator, + right: Type<'db>, + range: TextRange, visitor: &BinaryComparisonVisitor<'db>, ) -> Result, UnsupportedComparisonError<'db>> { let db = context.db(); - // Identity comparisons between equal builtin types are unreliable and not guaranteed by the - // language specification: - // - `is` returns `false` if the types are disjoint and `bool` if they overlap. - // - `is not` returns `true` if the types are disjoint and `bool` if they overlap. let try_dunder = |policy: MemberLookupPolicy| { let rich_comparison = |op| infer_rich_comparison(db, left, right, op, policy); let membership_test_comparison = |op, range: TextRange| { @@ -207,80 +323,22 @@ pub(super) fn infer_binary_type_comparison<'db>( }; match op { - ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq), - ast::CmpOp::NotEq => rich_comparison(RichCompareOperator::Ne), - ast::CmpOp::Lt => rich_comparison(RichCompareOperator::Lt), - ast::CmpOp::LtE => rich_comparison(RichCompareOperator::Le), - ast::CmpOp::Gt => rich_comparison(RichCompareOperator::Gt), - ast::CmpOp::GtE => rich_comparison(RichCompareOperator::Ge), - ast::CmpOp::In => membership_test_comparison(MembershipTestCompareOperator::In, range), - ast::CmpOp::NotIn => { - membership_test_comparison(MembershipTestCompareOperator::NotIn, range) - } - ast::CmpOp::Is => { - if left.is_disjoint_from(db, right) { - Ok(Type::bool_literal(false)) - } else if left.is_singleton(db) && left.is_equivalent_to(db, right) { - Ok(Type::bool_literal(true)) - } else { - Ok(KnownClass::Bool.to_instance(db)) - } - } - ast::CmpOp::IsNot => { - if left.is_disjoint_from(db, right) { - Ok(Type::bool_literal(true)) - } else if left.is_singleton(db) && left.is_equivalent_to(db, right) { - Ok(Type::bool_literal(false)) - } else { - Ok(KnownClass::Bool.to_instance(db)) - } + NonIdentityOperator::Rich(rich_op) => rich_comparison(rich_op), + NonIdentityOperator::Membership(membership_op) => { + membership_test_comparison(membership_op, range) } } }; - // Keep two occurrences of the same `TypeVar` symbolic. Replacing them with their bounds or - // constraints would lose their shared specialization: a `TypeVar` constrained to `None` and - // `EllipsisType` chooses the same singleton for both operands, not independent alternatives. - if matches!(op, ast::CmpOp::Is | ast::CmpOp::IsNot) - && !matches!( - ( - left.resolve_type_alias(db), - right.resolve_type_alias(db) - ), - (Type::TypeVar(left), Type::TypeVar(right)) if left.is_same_typevar_as(db, right) - ) - { - // `NewType` is an identity function at runtime, so distinct NewTypes can still contain the - // same object: - // - // UserId = NewType("UserId", int) - // OrderId = NewType("OrderId", int) - // user_id is order_id # possibly true - // - // Widen both operands to the types of their possible runtime objects before using the - // ordinary comparison logic. Keeping the usual recursive dispatch preserves facts carried - // by unions and intersections after widening. - let left_identity = left.identity_comparison_type(db); - let right_identity = right.identity_comparison_type(db); - if left_identity != left || right_identity != right { - return visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison( - context, - left_identity, - op, - right_identity, - range, - visitor, - ) - }); - } - } - let soundness_policy = ComparisonSoundnessPolicy::from_analysis_settings(db.analysis_settings(context.file())); let comparison_truthiness = match op { - ast::CmpOp::Eq => equality_truthiness(db, left, right, soundness_policy), - ast::CmpOp::NotEq => inequality_truthiness(db, left, right, soundness_policy), + NonIdentityOperator::Rich(RichCompareOperator::Eq) => { + equality_truthiness(db, left, right, soundness_policy) + } + NonIdentityOperator::Rich(RichCompareOperator::Ne) => { + inequality_truthiness(db, left, right, soundness_policy) + } _ => Truthiness::Ambiguous, }; if comparison_truthiness != Truthiness::Ambiguous { @@ -288,7 +346,7 @@ pub(super) fn infer_binary_type_comparison<'db>( } let comparison_result = match (left, right) { - (Type::EnumComplement(complement), right) => Some(infer_binary_type_comparison( + (Type::EnumComplement(complement), right) => Some(infer_binary_type_comparison_inner( context, complement.remaining_literal_union(db), op, @@ -296,7 +354,7 @@ pub(super) fn infer_binary_type_comparison<'db>( range, visitor, )), - (left, Type::EnumComplement(complement)) => Some(infer_binary_type_comparison( + (left, Type::EnumComplement(complement)) => Some(infer_binary_type_comparison_inner( context, left, op, @@ -308,7 +366,7 @@ pub(super) fn infer_binary_type_comparison<'db>( (Type::Union(union), other) => { let mut builder = UnionBuilder::new(db); for element in union.elements(db) { - builder = builder.add(infer_binary_type_comparison( + builder = builder.add(infer_binary_type_comparison_inner( context, *element, op, other, range, visitor, )?); } @@ -317,7 +375,7 @@ pub(super) fn infer_binary_type_comparison<'db>( (other, Type::Union(union)) => { let mut builder = UnionBuilder::new(db); for element in union.elements(db) { - builder = builder.add(infer_binary_type_comparison( + builder = builder.add(infer_binary_type_comparison_inner( context, other, op, *element, range, visitor, )?); } @@ -331,7 +389,7 @@ pub(super) fn infer_binary_type_comparison<'db>( .copied() .any(Type::is_type_var) => { - Some(infer_binary_type_comparison( + Some(infer_binary_type_comparison_inner( context, intersection.with_expanded_typevars_and_newtypes(db), op, @@ -347,7 +405,7 @@ pub(super) fn infer_binary_type_comparison<'db>( .copied() .any(Type::is_type_var) => { - Some(infer_binary_type_comparison( + Some(infer_binary_type_comparison_inner( context, left, op, @@ -368,7 +426,7 @@ pub(super) fn infer_binary_type_comparison<'db>( visitor, ) .map_err(|err| UnsupportedComparisonError { - op, + op: op.into(), left_ty: left, right_ty: err.right_ty, }), @@ -384,18 +442,32 @@ pub(super) fn infer_binary_type_comparison<'db>( visitor, ) .map_err(|err| UnsupportedComparisonError { - op, + op: op.into(), left_ty: err.left_ty, right_ty: right, }), ), (Type::TypeAlias(alias), right) => Some(visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison(context, alias.value_type(db), op, right, range, visitor) + infer_binary_type_comparison_inner( + context, + alias.value_type(db), + op, + right, + range, + visitor, + ) })), (left, Type::TypeAlias(alias)) => Some(visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison(context, left, op, alias.value_type(db), range, visitor) + infer_binary_type_comparison_inner( + context, + left, + op, + alias.value_type(db), + range, + visitor, + ) })), // `try_dunder` works for almost all `NewType`s, but not for `NewType`s of `float` and @@ -407,7 +479,7 @@ pub(super) fn infer_binary_type_comparison<'db>( (Type::NewTypeInstance(newtype), right) => { Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison( + infer_binary_type_comparison_inner( context, newtype.concrete_base_type(db), op, @@ -421,7 +493,7 @@ pub(super) fn infer_binary_type_comparison<'db>( (left, Type::NewTypeInstance(newtype)) => { Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison( + infer_binary_type_comparison_inner( context, left, op, @@ -445,7 +517,9 @@ pub(super) fn infer_binary_type_comparison<'db>( Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison(context, bound, op, bound, range, visitor) + infer_binary_type_comparison_inner( + context, bound, op, bound, range, visitor, + ) }) })) } @@ -453,7 +527,7 @@ pub(super) fn infer_binary_type_comparison<'db>( // For constrained TypeVars, check each constraint paired with itself. let mut builder = UnionBuilder::new(db); for &constraint in constraints.elements(db) { - builder = builder.add(infer_binary_type_comparison( + builder = builder.add(infer_binary_type_comparison_inner( context, constraint, op, constraint, range, visitor, )?); } @@ -469,14 +543,16 @@ pub(super) fn infer_binary_type_comparison<'db>( Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison(context, bound, op, right, range, visitor) + infer_binary_type_comparison_inner( + context, bound, op, right, range, visitor, + ) }) })) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { let mut builder = UnionBuilder::new(db); for &constraint in constraints.elements(db) { - builder = builder.add(infer_binary_type_comparison( + builder = builder.add(infer_binary_type_comparison_inner( context, constraint, op, right, range, visitor, )?); } @@ -492,14 +568,16 @@ pub(super) fn infer_binary_type_comparison<'db>( Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison(context, left, op, bound, range, visitor) + infer_binary_type_comparison_inner( + context, left, op, bound, range, visitor, + ) }) })) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { let mut builder = UnionBuilder::new(db); for &constraint in constraints.elements(db) { - builder = builder.add(infer_binary_type_comparison( + builder = builder.add(infer_binary_type_comparison_inner( context, left, op, constraint, range, visitor, )?); } @@ -513,31 +591,27 @@ pub(super) fn infer_binary_type_comparison<'db>( match (left_literal.kind(), right_literal.kind()) { (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Int(m)) => { Some(match op { - ast::CmpOp::Eq => Ok(Type::bool_literal(n == m)), - ast::CmpOp::NotEq => Ok(Type::bool_literal(n != m)), - ast::CmpOp::Lt => Ok(Type::bool_literal(n < m)), - ast::CmpOp::LtE => Ok(Type::bool_literal(n <= m)), - ast::CmpOp::Gt => Ok(Type::bool_literal(n > m)), - ast::CmpOp::GtE => Ok(Type::bool_literal(n >= m)), - // We cannot say that two equal int Literals will return True from an `is` or `is not` comparison. - // Even if they are the same value, they may not be the same object. - ast::CmpOp::Is => { - if n == m { - Ok(KnownClass::Bool.to_instance(db)) - } else { - Ok(Type::bool_literal(false)) - } + NonIdentityOperator::Rich(RichCompareOperator::Eq) => { + Ok(Type::bool_literal(n == m)) + } + NonIdentityOperator::Rich(RichCompareOperator::Ne) => { + Ok(Type::bool_literal(n != m)) } - ast::CmpOp::IsNot => { - if n == m { - Ok(KnownClass::Bool.to_instance(db)) - } else { - Ok(Type::bool_literal(true)) - } + NonIdentityOperator::Rich(RichCompareOperator::Lt) => { + Ok(Type::bool_literal(n < m)) + } + NonIdentityOperator::Rich(RichCompareOperator::Le) => { + Ok(Type::bool_literal(n <= m)) + } + NonIdentityOperator::Rich(RichCompareOperator::Gt) => { + Ok(Type::bool_literal(n > m)) + } + NonIdentityOperator::Rich(RichCompareOperator::Ge) => { + Ok(Type::bool_literal(n >= m)) } // Undefined for (int, int) - ast::CmpOp::In | ast::CmpOp::NotIn => Err(UnsupportedComparisonError { - op, + NonIdentityOperator::Membership(_) => Err(UnsupportedComparisonError { + op: op.into(), left_ty: left, right_ty: right, }), @@ -545,7 +619,7 @@ pub(super) fn infer_binary_type_comparison<'db>( } // Booleans are coded as integers (False = 0, True = 1) (LiteralValueTypeKind::Int(n), LiteralValueTypeKind::Bool(b)) => Some( - infer_binary_type_comparison( + infer_binary_type_comparison_inner( context, Type::int_literal(n.as_i64()), op, @@ -554,13 +628,13 @@ pub(super) fn infer_binary_type_comparison<'db>( visitor, ) .map_err(|_| UnsupportedComparisonError { - op, + op: op.into(), left_ty: left, right_ty: right, }), ), (LiteralValueTypeKind::Bool(b), LiteralValueTypeKind::Int(m)) => Some( - infer_binary_type_comparison( + infer_binary_type_comparison_inner( context, Type::int_literal(i64::from(b)), op, @@ -569,13 +643,13 @@ pub(super) fn infer_binary_type_comparison<'db>( visitor, ) .map_err(|_| UnsupportedComparisonError { - op, + op: op.into(), left_ty: left, right_ty: right, }), ), (LiteralValueTypeKind::Bool(a), LiteralValueTypeKind::Bool(b)) => Some( - infer_binary_type_comparison( + infer_binary_type_comparison_inner( context, Type::int_literal(i64::from(a)), op, @@ -584,7 +658,7 @@ pub(super) fn infer_binary_type_comparison<'db>( visitor, ) .map_err(|_| UnsupportedComparisonError { - op, + op: op.into(), left_ty: left, right_ty: right, }), @@ -597,27 +671,29 @@ pub(super) fn infer_binary_type_comparison<'db>( let s1 = salsa_s1.value(db); let s2 = salsa_s2.value(db); let result = match op { - ast::CmpOp::Eq => Type::bool_literal(s1 == s2), - ast::CmpOp::NotEq => Type::bool_literal(s1 != s2), - ast::CmpOp::Lt => Type::bool_literal(s1 < s2), - ast::CmpOp::LtE => Type::bool_literal(s1 <= s2), - ast::CmpOp::Gt => Type::bool_literal(s1 > s2), - ast::CmpOp::GtE => Type::bool_literal(s1 >= s2), - ast::CmpOp::In => Type::bool_literal(s2.contains(s1)), - ast::CmpOp::NotIn => Type::bool_literal(!s2.contains(s1)), - ast::CmpOp::Is => { - if s1 == s2 { - KnownClass::Bool.to_instance(db) - } else { - Type::bool_literal(false) - } + NonIdentityOperator::Rich(RichCompareOperator::Eq) => { + Type::bool_literal(s1 == s2) + } + NonIdentityOperator::Rich(RichCompareOperator::Ne) => { + Type::bool_literal(s1 != s2) } - ast::CmpOp::IsNot => { - if s1 == s2 { - KnownClass::Bool.to_instance(db) - } else { - Type::bool_literal(true) - } + NonIdentityOperator::Rich(RichCompareOperator::Lt) => { + Type::bool_literal(s1 < s2) + } + NonIdentityOperator::Rich(RichCompareOperator::Le) => { + Type::bool_literal(s1 <= s2) + } + NonIdentityOperator::Rich(RichCompareOperator::Gt) => { + Type::bool_literal(s1 > s2) + } + NonIdentityOperator::Rich(RichCompareOperator::Ge) => { + Type::bool_literal(s1 >= s2) + } + NonIdentityOperator::Membership(MembershipOperator::In) => { + Type::bool_literal(s2.contains(s1)) + } + NonIdentityOperator::Membership(MembershipOperator::NotIn) => { + Type::bool_literal(!s2.contains(s1)) } }; Some(Ok(result)) @@ -627,31 +703,29 @@ pub(super) fn infer_binary_type_comparison<'db>( let b1 = salsa_b1.value(db); let b2 = salsa_b2.value(db); let result = match op { - ast::CmpOp::Eq => Type::bool_literal(b1 == b2), - ast::CmpOp::NotEq => Type::bool_literal(b1 != b2), - ast::CmpOp::Lt => Type::bool_literal(b1 < b2), - ast::CmpOp::LtE => Type::bool_literal(b1 <= b2), - ast::CmpOp::Gt => Type::bool_literal(b1 > b2), - ast::CmpOp::GtE => Type::bool_literal(b1 >= b2), - ast::CmpOp::In => { - Type::bool_literal(memchr::memmem::find(b2, b1).is_some()) + NonIdentityOperator::Rich(RichCompareOperator::Eq) => { + Type::bool_literal(b1 == b2) } - ast::CmpOp::NotIn => { - Type::bool_literal(memchr::memmem::find(b2, b1).is_none()) + NonIdentityOperator::Rich(RichCompareOperator::Ne) => { + Type::bool_literal(b1 != b2) } - ast::CmpOp::Is => { - if b1 == b2 { - KnownClass::Bool.to_instance(db) - } else { - Type::bool_literal(false) - } + NonIdentityOperator::Rich(RichCompareOperator::Lt) => { + Type::bool_literal(b1 < b2) } - ast::CmpOp::IsNot => { - if b1 == b2 { - KnownClass::Bool.to_instance(db) - } else { - Type::bool_literal(true) - } + NonIdentityOperator::Rich(RichCompareOperator::Le) => { + Type::bool_literal(b1 <= b2) + } + NonIdentityOperator::Rich(RichCompareOperator::Gt) => { + Type::bool_literal(b1 > b2) + } + NonIdentityOperator::Rich(RichCompareOperator::Ge) => { + Type::bool_literal(b1 >= b2) + } + NonIdentityOperator::Membership(MembershipOperator::In) => { + Type::bool_literal(memchr::memmem::find(b2, b1).is_some()) + } + NonIdentityOperator::Membership(MembershipOperator::NotIn) => { + Type::bool_literal(memchr::memmem::find(b2, b1).is_none()) } }; Some(Ok(result)) @@ -682,8 +756,11 @@ pub(super) fn infer_binary_type_comparison<'db>( | LiteralValueTypeKind::Bool(_) | LiteralValueTypeKind::Bytes(_), LiteralValueTypeKind::LiteralString, - ) if matches!(op, ast::CmpOp::Eq | ast::CmpOp::NotEq) => { - Some(Ok(Type::bool_literal(op == ast::CmpOp::NotEq))) + ) if let NonIdentityOperator::Rich( + rich @ (RichCompareOperator::Eq | RichCompareOperator::Ne), + ) = op => + { + Some(Ok(Type::bool_literal(rich == RichCompareOperator::Ne))) } _ => None, } @@ -699,8 +776,12 @@ pub(super) fn infer_binary_type_comparison<'db>( let result = left.iff(db, &constraints, right); let equivalent = result.is_always_satisfied(db); match op { - ast::CmpOp::Eq => Some(Ok(Type::bool_literal(equivalent))), - ast::CmpOp::NotEq => Some(Ok(Type::bool_literal(!equivalent))), + NonIdentityOperator::Rich(RichCompareOperator::Eq) => { + Some(Ok(Type::bool_literal(equivalent))) + } + NonIdentityOperator::Rich(RichCompareOperator::Ne) => { + Some(Ok(Type::bool_literal(!equivalent))) + } _ => None, } } @@ -718,28 +799,21 @@ pub(super) fn infer_binary_type_comparison<'db>( }; let result = match op { - ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq), - ast::CmpOp::NotEq => tuple_rich_comparison(RichCompareOperator::Ne), - ast::CmpOp::Lt => tuple_rich_comparison(RichCompareOperator::Lt), - ast::CmpOp::LtE => tuple_rich_comparison(RichCompareOperator::Le), - ast::CmpOp::Gt => tuple_rich_comparison(RichCompareOperator::Gt), - ast::CmpOp::GtE => tuple_rich_comparison(RichCompareOperator::Ge), - ast::CmpOp::In | ast::CmpOp::NotIn => { + NonIdentityOperator::Rich(rich_op) => tuple_rich_comparison(rich_op), + NonIdentityOperator::Membership(membership_op) => { let mut any_eq = false; let mut any_ambiguous = false; for ty in rhs_tuple.iter_element_types(db) { - let eq_result = infer_binary_type_comparison( + let eq_result = infer_binary_type_comparison_inner( context, left, - ast::CmpOp::Eq, + NonIdentityOperator::Rich(RichCompareOperator::Eq), ty, range, visitor, ) - .expect( - "infer_binary_type_comparison should never return None for `CmpOp::Eq`", - ); + .expect("infer_binary_type_comparison should never return None for `==`"); match eq_result { todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo), @@ -755,31 +829,13 @@ pub(super) fn infer_binary_type_comparison<'db>( } if any_eq { - Ok(Type::bool_literal(op.is_in())) + Ok(Type::bool_literal(membership_op.is_in())) } else if !any_ambiguous { - Ok(Type::bool_literal(op.is_not_in())) + Ok(Type::bool_literal(membership_op.is_not_in())) } else { Ok(KnownClass::Bool.to_instance(db)) } } - ast::CmpOp::Is | ast::CmpOp::IsNot => { - // - `[ast::CmpOp::Is]`: returns `false` if the elements are definitely unequal, otherwise `bool` - // - `[ast::CmpOp::IsNot]`: returns `true` if the elements are definitely unequal, otherwise `bool` - let eq_result = tuple_rich_comparison(RichCompareOperator::Eq).expect( - "infer_binary_type_comparison should never return None for `CmpOp::Eq`", - ); - - Ok(match eq_result { - todo @ Type::Dynamic(DynamicType::Todo(_)) => todo, - // It's okay to ignore errors here because Python doesn't call `__bool__` - // for `is` and `is not` comparisons. This is an implementation detail - // for how we determine the truthiness of a type. - ty => match ty.bool(db) { - Truthiness::AlwaysFalse => Type::bool_literal(op.is_is_not()), - _ => KnownClass::Bool.to_instance(db), - }, - }) - } }; Some(result) @@ -799,7 +855,7 @@ pub(super) fn infer_binary_type_comparison<'db>( fn infer_binary_intersection_type_comparison<'db>( context: &InferContext<'db, '_>, intersection: IntersectionType<'db>, - op: ast::CmpOp, + op: NonIdentityOperator, other: Type<'db>, intersection_on: IntersectionOn, range: TextRange, @@ -820,10 +876,10 @@ fn infer_binary_intersection_type_comparison<'db>( if let Some(alternatives) = intersection.finite_alternative_union(db) { return match intersection_on { IntersectionOn::Left => { - infer_binary_type_comparison(context, alternatives, op, other, range, visitor) + infer_binary_type_comparison_inner(context, alternatives, op, other, range, visitor) } IntersectionOn::Right => { - infer_binary_type_comparison(context, other, op, alternatives, range, visitor) + infer_binary_type_comparison_inner(context, other, op, alternatives, range, visitor) } }; } @@ -834,10 +890,10 @@ fn infer_binary_intersection_type_comparison<'db>( for pos in intersection.positive(db) { let result = match intersection_on { IntersectionOn::Left => { - infer_binary_type_comparison(context, *pos, op, other, range, visitor) + infer_binary_type_comparison_inner(context, *pos, op, other, range, visitor) } IntersectionOn::Right => { - infer_binary_type_comparison(context, other, op, *pos, range, visitor) + infer_binary_type_comparison_inner(context, other, op, *pos, range, visitor) } }; @@ -850,30 +906,6 @@ fn infer_binary_intersection_type_comparison<'db>( } } - // For negative contributions to the intersection type, there are only a few - // special cases that allow us to narrow down the result type of the comparison. - for neg in intersection.negative(db) { - let result = match intersection_on { - IntersectionOn::Left => { - infer_binary_type_comparison(context, *neg, op, other, range, visitor).ok() - } - IntersectionOn::Right => { - infer_binary_type_comparison(context, other, op, *neg, range, visitor).ok() - } - } - .and_then(Type::as_literal_value_kind); - - match (op, result) { - (ast::CmpOp::Is, Some(LiteralValueTypeKind::Bool(true))) => { - return Ok(Type::bool_literal(false)); - } - (ast::CmpOp::IsNot, Some(LiteralValueTypeKind::Bool(false))) => { - return Ok(Type::bool_literal(true)); - } - _ => {} - } - } - // If none of the simplifications above apply, we still need to return *some* // result type for the comparison 'T_inter `op` T_other' (or reversed), where // @@ -921,10 +953,10 @@ fn infer_binary_intersection_type_comparison<'db>( for pos in intersection.positive(db) { let result = match intersection_on { IntersectionOn::Left => { - infer_binary_type_comparison(context, *pos, op, other, range, visitor) + infer_binary_type_comparison_inner(context, *pos, op, other, range, visitor) } IntersectionOn::Right => { - infer_binary_type_comparison(context, other, op, *pos, range, visitor) + infer_binary_type_comparison_inner(context, other, op, *pos, range, visitor) } }; @@ -959,12 +991,22 @@ fn infer_binary_intersection_type_comparison<'db>( State::NoPositiveElements => { // We didn't see any positive elements, check if the operation is supported on `object`: match intersection_on { - IntersectionOn::Left => { - infer_binary_type_comparison(context, Type::object(), op, other, range, visitor) - } - IntersectionOn::Right => { - infer_binary_type_comparison(context, other, op, Type::object(), range, visitor) - } + IntersectionOn::Left => infer_binary_type_comparison_inner( + context, + Type::object(), + op, + other, + range, + visitor, + ), + IntersectionOn::Right => infer_binary_type_comparison_inner( + context, + other, + op, + Type::object(), + range, + visitor, + ), } } State::UnsupportedOnAllElements(error) => Err(error), @@ -1031,7 +1073,7 @@ fn infer_membership_test_comparison<'db>( context: &InferContext<'db, '_>, left: Type<'db>, right: Type<'db>, - op: MembershipTestCompareOperator, + op: MembershipOperator, range: TextRange, ) -> Result, UnsupportedComparisonError<'db>> { let db = context.db(); @@ -1065,10 +1107,8 @@ fn infer_membership_test_comparison<'db>( }); match op { - MembershipTestCompareOperator::In => Type::from_truthiness(db, truthiness), - MembershipTestCompareOperator::NotIn => { - Type::from_truthiness(db, truthiness.negate()) - } + MembershipOperator::In => Type::from_truthiness(db, truthiness), + MembershipOperator::NotIn => Type::from_truthiness(db, truthiness.negate()), } }) .ok_or_else(|| UnsupportedComparisonError { @@ -1101,15 +1141,15 @@ fn infer_tuple_rich_comparison<'db>( let mut builder = UnionBuilder::new(db); for (l_ty, r_ty) in left_iter.zip(right_iter) { - let pairwise_eq_result = infer_binary_type_comparison( + let pairwise_eq_result = infer_binary_type_comparison_inner( context, l_ty, - ast::CmpOp::Eq, + NonIdentityOperator::Rich(RichCompareOperator::Eq), r_ty, range, visitor, ) - .expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); + .expect("infer_binary_type_comparison should never return None for `==`"); match pairwise_eq_result.try_bool(db).unwrap_or_else(|err| { // TODO: We should, whenever possible, pass the range of the left and right elements @@ -1131,10 +1171,10 @@ fn infer_tuple_rich_comparison<'db>( RichCompareOperator::Lt | RichCompareOperator::Le | RichCompareOperator::Gt - | RichCompareOperator::Ge => infer_binary_type_comparison( + | RichCompareOperator::Ge => infer_binary_type_comparison_inner( context, l_ty, - op.into(), + NonIdentityOperator::Rich(op), r_ty, range, visitor, @@ -1190,10 +1230,10 @@ fn infer_tuple_rich_comparison<'db>( (left @ TupleSpec::Variable(_), right) | (left, right @ TupleSpec::Variable(_)) => { let mut results = SmallVec::<[Type<'db>; 8]>::new(); left.try_for_each_element_pair(db, right, |l_ty, r_ty| { - results.push(infer_binary_type_comparison( + results.push(infer_binary_type_comparison_inner( context, l_ty, - op.into(), + NonIdentityOperator::Rich(op), r_ty, range, visitor, From c8df968758c332682965112c160d992782ccf33d Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 24 Jul 2026 07:07:37 -0700 Subject: [PATCH 047/390] [ty] Clarify diagnostic message naming (#27140) ## Summary In main, we have some confusing/ambiguous naming in the diagnostic API; the term "primary" is sometimes used to refer to the "primary annotation" and sometimes to the headline diagnostic message. In fact, we currently have methods named `primary_message` and `set_primary_message`, but the latter sets the primary _annotation_ message, and the former fetches the headline message! This PR prefers clarity over conciseness, always using `primary_annotation` to refer to the primary annotation (the fact that this is the primary _annotation_ message and not some other "primary message" is IMO key context the caller should be aware of), and always using the term "headline" to refer to the main diagnostic message. Specifically: - Rename both diagnostic-guard `set_primary_message` methods to `set_primary_annotation_message` and update all call sites. - Rename `Diagnostic::primary_message` and `SubDiagnostic::primary_message` to `headline_message`, updating consumers and terminology in the diagnostic API documentation. This is a fully mechanical rename that should have no behavior impact. ## Test plan - Existing diagnostic-rendering and ty semantic coverage passes, including concise-message behavior; no snapshots changed. - All workspace targets, tests, and benchmarks compile with the renamed APIs, and repository hooks pass. --- crates/mdtest/src/matcher.rs | 6 +- crates/ruff_benchmark/benches/ty.rs | 2 +- crates/ruff_db/src/diagnostic/mod.rs | 30 +++-- crates/ruff_db/src/diagnostic/render.rs | 2 +- crates/ruff_linter/src/checkers/ast/mod.rs | 2 +- .../pyflakes/rules/redefined_while_unused.rs | 2 +- .../rules/duplicate_entry_in_dunder_all.rs | 2 +- crates/ruff_server/src/lint.rs | 4 +- crates/ty/tests/file_watching.rs | 6 +- crates/ty_project/src/lib.rs | 4 +- crates/ty_python_semantic/src/fixes.rs | 8 +- crates/ty_python_semantic/src/types.rs | 4 +- .../src/types/bound_super.rs | 2 +- .../ty_python_semantic/src/types/call/bind.rs | 12 +- .../ty_python_semantic/src/types/context.rs | 16 +-- .../src/types/diagnostic.rs | 113 +++++++++--------- .../ty_python_semantic/src/types/function.rs | 5 +- .../src/types/infer/builder.rs | 27 +++-- .../src/types/infer/builder/dynamic_class.rs | 18 +-- .../src/types/infer/builder/enum_call.rs | 4 +- .../types/infer/builder/final_attribute.rs | 6 +- .../src/types/infer/builder/function.rs | 8 +- .../src/types/infer/builder/named_tuple.rs | 29 ++--- .../src/types/infer/builder/new_class.rs | 2 +- .../infer/builder/post_inference/function.rs | 10 +- .../post_inference/overloaded_function.rs | 2 +- .../builder/post_inference/static_class.rs | 11 +- .../post_inference/type_param_validation.rs | 6 +- .../builder/post_inference/typed_dict.rs | 2 +- .../src/types/infer/builder/subscript.rs | 4 +- .../src/types/infer/builder/type_call.rs | 4 +- .../types/infer/builder/type_expression.rs | 27 +++-- .../src/types/infer/builder/typed_dict.rs | 10 +- .../src/types/infer/builder/typevar.rs | 28 +++-- .../src/types/infer/tests.rs | 2 +- .../ty_python_semantic/src/types/overrides.rs | 2 +- .../src/types/string_annotation.rs | 2 +- .../src/types/typed_dict.rs | 4 +- .../ty_python_semantic/src/types/unpacker.rs | 4 +- .../ty_server/src/server/api/diagnostics.rs | 4 +- .../api/requests/workspace_diagnostic.rs | 2 +- crates/ty_wasm/src/lib.rs | 2 +- 42 files changed, 230 insertions(+), 210 deletions(-) diff --git a/crates/mdtest/src/matcher.rs b/crates/mdtest/src/matcher.rs index 8f5eb6a3d0..88f70cf4b6 100644 --- a/crates/mdtest/src/matcher.rs +++ b/crates/mdtest/src/matcher.rs @@ -462,7 +462,7 @@ fn match_reveal_type_diagnostic( return false; } - let primary_message = diagnostic.primary_message(); + let headline_message = diagnostic.headline_message(); let Some(primary_annotation) = (diagnostic.primary_annotation()).and_then(|a| a.get_message()) else { @@ -473,7 +473,7 @@ fn match_reveal_type_diagnostic( // reveal_type, reveal_protocol_interface if matches!( - primary_message, + headline_message, "Revealed type" | "Revealed protocol interface" ) && expected_reveal_type_message.is_none_or(|expected_reveal_type_message| { primary_annotation == expected_reveal_type_message @@ -483,7 +483,7 @@ fn match_reveal_type_diagnostic( // reveal_when_assignable_to, reveal_when_subtype_of, reveal_mro if matches!( - primary_message, + headline_message, "Assignability holds" | "Subtyping holds" | "Revealed MRO" ) && expected_reveal_type .is_none_or(|expected_reveal_type| primary_annotation == expected_reveal_type) diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index 65b3449c1c..c2b5570a4c 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -212,7 +212,7 @@ fn assert_diagnostics(db: &dyn Db, diagnostics: &[Diagnostic], expected: &[KeyDi .primary_span() .and_then(|span| span.range()) .map(Range::::from), - diagnostic.primary_message(), + diagnostic.headline_message(), diagnostic.severity(), ) }) diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 22404e435b..a6ba4c0343 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -155,8 +155,8 @@ impl Diagnostic { /// /// An "info" diagnostic is useful when contextualizing or otherwise /// helpful information can be added to help end users understand the - /// main diagnostic message better. For example, if a the main diagnostic - /// message is about a function call being invalid, a useful "info" + /// headline message better. For example, if the headline message is about + /// a function call being invalid, a useful "info" /// sub-diagnostic could show the function definition (or only the relevant /// parts of it). /// @@ -203,18 +203,17 @@ impl Diagnostic { self.inner.id } - /// Returns the primary message for this diagnostic. + /// Returns the headline message for this diagnostic. /// /// A diagnostic always has a message, but it may be empty. - pub fn primary_message(&self) -> &str { + pub fn headline_message(&self) -> &str { self.inner.message.as_str() } - /// Introspects this diagnostic and returns what kind of "primary" message - /// it contains for concise formatting. + /// Introspects this diagnostic and returns its message for concise formatting. /// /// When we concisely format diagnostics, we likely want to not only - /// include the primary diagnostic message but also the message attached + /// include the headline message but also the message attached /// to the primary annotation. In particular, the primary annotation often /// contains *essential* information or context for understanding the /// diagnostic. @@ -242,7 +241,7 @@ impl Diagnostic { /// Set a custom message for the concise formatting of this diagnostic. /// /// This overrides the default behavior of generating a concise message - /// from the main diagnostic message and the primary annotation. + /// from the headline message and the primary annotation. pub fn set_concise_message(&mut self, message: impl IntoDiagnosticMessage) { Arc::make_mut(&mut self.inner).custom_concise_message = Some(message.into_diagnostic_message()); @@ -702,18 +701,17 @@ impl SubDiagnostic { self.primary_annotation().map(Annotation::get_span) } - /// Returns the primary message for this sub-diagnostic. + /// Returns the headline message for this sub-diagnostic. /// /// A sub-diagnostic always has a message, but it may be empty. - pub fn primary_message(&self) -> &str { + pub fn headline_message(&self) -> &str { self.inner.message.as_str() } - /// Introspects this diagnostic and returns what kind of "primary" message - /// it contains for concise formatting. + /// Introspects this sub-diagnostic and returns its message for concise formatting. /// /// When we concisely format diagnostics, we likely want to not only - /// include the primary diagnostic message but also the message attached + /// include the headline message but also the message attached /// to the primary annotation. In particular, the primary annotation often /// contains *essential* information or context for understanding the /// diagnostic. @@ -722,7 +720,7 @@ impl SubDiagnostic { /// cases, just converting it to a string (or printing it) will do what /// you want. pub fn concise_message(&self) -> ConciseMessage<'_> { - let main = self.primary_message(); + let main = self.headline_message(); let annotation = self .primary_annotation() .and_then(|ann| ann.get_message()) @@ -1633,10 +1631,10 @@ pub enum DiagnosticFormat { /// A representation of the kinds of messages inside a diagnostic. pub enum ConciseMessage<'a> { - /// A diagnostic contains a non-empty main message and an empty + /// A diagnostic contains a non-empty headline message and an empty /// primary annotation message. MainDiagnostic(&'a str), - /// A diagnostic contains a non-empty main message and a non-empty + /// A diagnostic contains a non-empty headline message and a non-empty /// primary annotation message. Both { main: &'a str, annotation: &'a str }, /// A custom concise message has been provided. diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index 058dc7eaa8..7629c9d778 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -2561,7 +2561,7 @@ watermelon assert_eq!( diagnostics .iter() - .map(Diagnostic::primary_message) + .map(Diagnostic::headline_message) .collect::>(), ["checking main.py", "checking mod.py"] ); diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 23d6f8f5d6..59a5a53530 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -3645,7 +3645,7 @@ impl DiagnosticGuard<'_, '_> { /// /// Callers can add additional primary or secondary annotations via the /// `DerefMut` trait implementation to a `Diagnostic`. - pub(crate) fn set_primary_message(&mut self, message: impl IntoDiagnosticMessage) { + pub(crate) fn set_primary_annotation_message(&mut self, message: impl IntoDiagnosticMessage) { // N.B. It is normally bad juju to define `self` methods // on types that implement `Deref`. Instead, it's idiomatic // to do `fn foo(this: &mut LintDiagnosticGuard)`, which in diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs b/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs index c2cfaa9dc8..7c7725b9f6 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/redefined_while_unused.rs @@ -282,7 +282,7 @@ pub(crate) fn redefined_while_unused(checker: &Checker, scope_id: ScopeId, scope info.shadowed, ); - diagnostic.set_primary_message(format_args!("`{name}` redefined here")); + diagnostic.set_primary_annotation_message(format_args!("`{name}` redefined here")); if let Some(range) = info.binding.parent_range(checker.semantic()) { diagnostic.set_parent(range.start()); diff --git a/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs b/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs index dab957b18f..a16edf98b7 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/duplicate_entry_in_dunder_all.rs @@ -163,7 +163,7 @@ fn duplicate_entry_in_dunder_all(checker: &Checker, target: &ast::Expr, value: & previous_expr, ); - diagnostic.set_primary_message(format_args!("`{name}` duplicated here")); + diagnostic.set_primary_annotation_message(format_args!("`{name}` duplicated here")); diagnostic.try_set_fix(|| { edits::remove_member(elts, index, source).map(|edit| { diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index 2cc836cca8..396e491b1b 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -376,9 +376,9 @@ fn to_lsp_diagnostic( .primary_annotation() .and_then(Annotation::get_message) { - format!("{}: {annotation_message}", diagnostic.primary_message()) + format!("{}: {annotation_message}", diagnostic.headline_message()) } else { - diagnostic.primary_message().to_string() + diagnostic.headline_message().to_string() } } else { diagnostic.concise_message().to_string() diff --git a/crates/ty/tests/file_watching.rs b/crates/ty/tests/file_watching.rs index 0810f2f847..39fbdb10c9 100644 --- a/crates/ty/tests/file_watching.rs +++ b/crates/ty/tests/file_watching.rs @@ -1395,11 +1395,11 @@ print(sys.last_exc, os.getegid()) assert_eq!(diagnostics.len(), 2); assert_eq!( - diagnostics[0].primary_message(), + diagnostics[0].headline_message(), "Module `sys` has no member `last_exc`" ); assert_eq!( - diagnostics[1].primary_message(), + diagnostics[1].headline_message(), "Module `os` has no member `getegid`" ); @@ -1453,7 +1453,7 @@ fn reloading_options_updates_inferred_python_version_diagnostics_when_metadata_i assert_eq!(diagnostics.len(), 1); assert_eq!( - diagnostics[0].primary_message(), + diagnostics[0].headline_message(), format!( "Ignoring unsupported inferred Python version `3.{unsupported_minor}`; ty will use Python {} instead.", PythonVersion::latest_ty() diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index 50635cad21..aa61f4fe3f 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -910,7 +910,7 @@ mod tests { check_file_impl(&db, file) .as_ref() .unwrap_err() - .primary_message() + .headline_message() .to_string(), "Failed to read file: No such file or directory".to_string() ); @@ -928,7 +928,7 @@ mod tests { .as_ref() .unwrap() .iter() - .map(|diagnostic| diagnostic.primary_message().to_string()) + .map(|diagnostic| diagnostic.headline_message().to_string()) .collect::>(), vec![] as Vec ); diff --git a/crates/ty_python_semantic/src/fixes.rs b/crates/ty_python_semantic/src/fixes.rs index f6c28656c9..f4ce815376 100644 --- a/crates/ty_python_semantic/src/fixes.rs +++ b/crates/ty_python_semantic/src/fixes.rs @@ -1736,12 +1736,12 @@ class B(A): assert_eq!(diagnostic.id(), LINT_ID); assert_eq!( - diagnostic.primary_message(), + diagnostic.headline_message(), "Variable `a` should be named `b`." ); assert_eq!(convergence_diagnostic.id(), DiagnosticId::InternalError); - assert_snapshot!(convergence_diagnostic.primary_message(), @"Fixes failed to converge after 10 iterations."); + assert_snapshot!(convergence_diagnostic.headline_message(), @"Fixes failed to converge after 10 iterations."); // It should keep the source text from the last allowed fix iteration. assert_eq!(&*source_text(&db, file), "a = 10"); @@ -1813,12 +1813,12 @@ class B(A): assert_eq!(diagnostic.id(), LINT_ID); assert_eq!( - diagnostic.primary_message(), + diagnostic.headline_message(), "Variable `b` should be named `c`." ); assert_eq!(syntax_error.id(), DiagnosticId::InternalError); - assert_snapshot!(syntax_error.primary_message(), @"Applying fixes introduced a syntax error. Reverting changes."); + assert_snapshot!(syntax_error.headline_message(), @"Applying fixes introduced a syntax error. Reverting changes."); // It should revert the source to the last known error free version. assert_eq!(&*source_text(&db, file), "b = 10"); diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index bac4ea9d5e..0642eb5fc0 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -8567,7 +8567,7 @@ impl<'db> InvalidTypeExpression<'db> { { return; } - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean to use the module's member \ `{module_name_final_part}.{module_name_final_part}`?" )); @@ -8593,7 +8593,7 @@ impl<'db> InvalidTypeExpression<'db> { .map(|parent| parent.to_scope_id(db, function_body_scope.file(db))) == builtins_module_scope(db) { - diagnostic.set_primary_message("Did you mean `collections.abc.Callable`?"); + diagnostic.set_primary_annotation_message("Did you mean `collections.abc.Callable`?"); } else if matches!(self, InvalidTypeExpression::InvalidBareParamSpec(_)) { diagnostic.info("A bare ParamSpec is only valid:"); diagnostic.info(" - as the first argument to `Callable`"); diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index ef054c4571..1fda7aafc6 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -128,7 +128,7 @@ impl<'db> BoundSuperError<'db> { _ => { let mut diagnostic = builder.into_diagnostic("Argument is not a valid class"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Argument has type `{}`", pivot_class.display(context.db()) )); diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 62a8a5999c..e61e303717 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -80,7 +80,7 @@ use ty_python_core::semantic_index; pub(crate) use self::constructor::ConstructorCallableKind; -/// Overrides the lint and top-level message for a call diagnostic emitted from an implicit call. +/// Overrides the lint and headline message for a call diagnostic emitted from an implicit call. /// /// The original call-error message is retained on the primary annotation, while `info` explains /// why the call happened. `argument_ranges` maps synthetic call arguments back to source ranges. @@ -7697,12 +7697,12 @@ impl<'db> BindingError<'db> { provenance, InvalidArgumentTypeProvenance::OpenTypedDictExtraItems ) { - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Possible extra items in unpacked open `TypedDict` have type \ `{provided_ty_display}`, expected `{expected_ty_display}`" )); } else { - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Expected `{expected_ty_display}`, found `{provided_ty_display}`" )); } @@ -7801,7 +7801,7 @@ impl<'db> BindingError<'db> { let mut diag = builder.into_diagnostic( "Argument expression after ** must be a mapping with `str` key type", ); - diag.set_primary_message(format_args!("Found `{provided_ty_display}`")); + diag.set_primary_annotation_message(format_args!("Found `{provided_ty_display}`")); if let Some(compound_diag) = compound_diag { compound_diag.add_context(context.db(), &mut diag); @@ -7993,7 +7993,7 @@ impl<'db> BindingError<'db> { SpecializationError::MismatchedBound { bound_typevar, .. } => { let typevar = bound_typevar.typevar(context.db()); let typevar_name = typevar.name(context.db()); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Argument type `{argument_ty_display}` does not \ satisfy upper bound `{}` of type variable `{typevar_name}`", typevar @@ -8007,7 +8007,7 @@ impl<'db> BindingError<'db> { SpecializationError::MismatchedConstraint { bound_typevar, .. } => { let typevar = bound_typevar.typevar(context.db()); let typevar_name = typevar.name(context.db()); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Argument type `{argument_ty_display}` does not \ satisfy constraints ({}) of type variable `{typevar_name}`", typevar diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 36c03f2fcc..93f7bc655d 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -129,7 +129,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { /// The severity of the diagnostic returned is automatically determined /// by the given lint and configuration. The message given to /// `LintDiagnosticGuardBuilder::to_diagnostic` is used to construct the - /// initial diagnostic and should be considered the "top-level message" of + /// initial diagnostic and should be considered the "headline message" of /// the diagnostic. (i.e., If nothing else about the diagnostic is seen, /// aside from its identifier, the message is probably the thing you'd pick /// to show.) @@ -139,7 +139,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { /// typing context. (That means the range given _must_ be valid for the /// `File` currently being type checked.) This primary annotation does /// not have a message attached to it, but callers can attach one via - /// `LintDiagnosticGuard::set_primary_message`. + /// `LintDiagnosticGuard::set_primary_annotation_message`. /// /// After using the builder to make a guard, once the guard is dropped, the /// diagnostic is added to the context, unless there is something in the @@ -263,7 +263,7 @@ impl fmt::Debug for InferContext<'_, '_> { /// /// * On `Drop`, the underlying diagnostic is added to the typing context. /// * Some convenience methods for mutating the underlying `Diagnostic` -/// in lint context. For example, `LintDiagnosticGuard::set_primary_message` +/// in lint context. For example, `LintDiagnosticGuard::set_primary_annotation_message` /// will attach a message to the primary span on the diagnostic. pub(super) struct LintDiagnosticGuard<'db, 'ctx> { /// The typing context. @@ -289,7 +289,7 @@ impl LintDiagnosticGuard<'_, '_> { /// /// Callers can add additional primary or secondary annotations via the /// `DerefMut` trait implementation to a `Diagnostic`. - pub(super) fn set_primary_message(&mut self, message: impl IntoDiagnosticMessage) { + pub(super) fn set_primary_annotation_message(&mut self, message: impl IntoDiagnosticMessage) { // N.B. It is normally bad juju to define `self` methods // on types that implement `Deref`. Instead, it's idiomatic // to do `fn foo(this: &mut LintDiagnosticGuard)`, which in @@ -506,7 +506,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { /// the ID and severity derived from the `LintMetadata` used to create /// this builder. The diagnostic also includes a primary annotation /// without a message. To add a message to this primary annotation, use - /// `LintDiagnosticGuard::set_primary_message`. + /// `LintDiagnosticGuard::set_primary_annotation_message`. /// /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. @@ -516,9 +516,9 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { self, message: impl std::fmt::Display, ) -> LintDiagnosticGuard<'db, 'ctx> { - // This is why `LintDiagnosticGuard::set_primary_message` exists. + // This is why `LintDiagnosticGuard::set_primary_annotation_message` exists. // We add the primary annotation here (because it's required). Without a message - // override, its optional message can be added later via `set_primary_message`. + // override, its optional message can be added later via `set_primary_annotation_message`. let primary_span = Span::from(self.ctx.file()).with_range(self.primary_range); let mut diag = if let Some((message_override, info)) = self.message_override { let mut diag = Diagnostic::new( @@ -543,7 +543,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { } } - /// Replace the top-level message and add an info sub-diagnostic while retaining the original + /// Replace the headline message and add an info sub-diagnostic while retaining the original /// message on the primary annotation. pub(super) fn with_message_override(mut self, message: String, info: &str) -> Self { self.message_override = Some((message, info.to_string())); diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 06d3e5fbb4..0c1db5eb0b 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -1300,11 +1300,11 @@ pub(crate) fn report_mismatched_type_name<'db>( "The name passed to `{constructor}` must match the variable it is assigned to" )); if let Some(actual_name) = actual_name { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected \"{expected_name}\", got \"{actual_name}\"" )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected \"{expected_name}\", got variable of type `{}`", actual_name_ty.display(context.db()) )); @@ -1673,7 +1673,7 @@ pub(super) fn report_invalid_assignment<'db>( } } - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Incompatible value of type `{}`", value_ty.display(context.db()), )); @@ -1682,7 +1682,7 @@ pub(super) fn report_invalid_assignment<'db>( error_context.attach_to(context.db(), &mut diag); // Overwrite the concise message to avoid showing the value type twice - let message = diag.primary_message().to_string(); + let message = diag.headline_message().to_string(); diag.set_concise_message(message); } @@ -1746,7 +1746,7 @@ pub(super) fn report_bad_dunder_set_call<'db>( diagnostic.annotate(Annotation::secondary(Span::from(file_range)).message( format_args!("Property `{object_type}.{attribute}` defined here with no setter"), )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Attempted assignment to `{object_type}.{attribute}` here" )); } @@ -1800,7 +1800,7 @@ pub(super) fn report_bad_dunder_delete_call<'db>( diagnostic.annotate(Annotation::secondary(Span::from(file_range)).message( format_args!("Property `{object_type}.{attribute}` defined here with no deleter"), )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Attempted deletion of `{object_type}.{attribute}` here" )); } @@ -1856,7 +1856,7 @@ pub(super) fn report_invalid_return_type( let return_type_span = context.span(return_type_range); let mut diag = builder.into_diagnostic("Return type does not match returned value"); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "expected `{expected_ty}`, found `{actual_ty}`", expected_ty = expected_ty.display_with(context.db(), settings.clone()), actual_ty = actual_ty.display_with(context.db(), settings.clone()), @@ -1884,7 +1884,7 @@ pub(super) fn report_invalid_generator_function_return_type( 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!( + diag.set_primary_annotation_message(format_args!( "expected `{expected_ty}`, found `{inferred_ty}`", expected_ty = expected_ty.display(context.db()), )); @@ -1957,7 +1957,7 @@ pub(super) fn report_invalid_generator_yield_type( format!("generator with send type `{actual_display}`, expected `{expected_display}`") } }; - diag.set_primary_message(primary); + diag.set_primary_annotation_message(primary); if let Some(return_type_span) = return_type_span { diag.annotate(Annotation::secondary(return_type_span).message(format!( @@ -2133,7 +2133,7 @@ pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast let mut diagnostic = if ty.is_notimplemented(context.db()) { let mut diag = builder.into_diagnostic("Cannot catch `NotImplemented` in an exception handler"); - diag.set_primary_message("Did you mean `NotImplementedError`?"); + diag.set_primary_annotation_message("Did you mean `NotImplementedError`?"); diag } else { let mut diag = builder.into_diagnostic(format_args!( @@ -2144,7 +2144,7 @@ pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast "object" }, )); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Object has type `{}`", ty.display(context.db()) )); @@ -2166,14 +2166,14 @@ pub(crate) fn report_invalid_exception_raised( }; if raise_type.is_notimplemented(context.db()) { let mut diagnostic = builder.into_diagnostic(format_args!("Cannot raise `NotImplemented`")); - diagnostic.set_primary_message("Did you mean `NotImplementedError`?"); + diagnostic.set_primary_annotation_message("Did you mean `NotImplementedError`?"); diagnostic.info("Can only raise an instance or subclass of `BaseException`"); } else { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot raise object of type `{}`", raise_type.display(context.db()) )); - diagnostic.set_primary_message("Not an instance or subclass of `BaseException`"); + diagnostic.set_primary_annotation_message("Not an instance or subclass of `BaseException`"); } } @@ -2185,7 +2185,7 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast: let mut diag = builder.into_diagnostic(format_args!( "Cannot use `NotImplemented` as an exception cause", )); - diag.set_primary_message("Did you mean `NotImplementedError`?"); + diag.set_primary_annotation_message("Did you mean `NotImplementedError`?"); diag } else { builder.into_diagnostic(format_args!( @@ -2216,7 +2216,7 @@ pub(crate) fn report_instance_layout_conflict( let mut diagnostic = builder .into_diagnostic("Class will raise `TypeError` at runtime due to incompatible bases"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Bases {} cannot be combined in multiple inheritance", disjoint_bases.describe_problematic_class_bases(db) )); @@ -2446,7 +2446,7 @@ pub(crate) fn report_bad_argument_to_get_protocol_members( }; 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.set_primary_annotation_message("This call will raise `TypeError` at runtime"); diagnostic.info("Only protocol classes can be passed to `get_protocol_members`"); let mut class_def_diagnostic = SubDiagnostic::new( @@ -2481,8 +2481,9 @@ pub(crate) fn report_bad_argument_to_protocol_interface( }; let db = context.db(); let mut diagnostic = builder.into_diagnostic("Invalid argument to `reveal_protocol_interface`"); - diagnostic - .set_primary_message("Only protocol classes can be passed to `reveal_protocol_interface`"); + diagnostic.set_primary_annotation_message( + "Only protocol classes can be passed to `reveal_protocol_interface`", + ); if let Some(class) = param_type.to_class_type(context.db()) { let mut class_def_diagnostic = SubDiagnostic::new( @@ -2531,7 +2532,7 @@ pub(crate) fn report_invalid_class_match_pattern( let mut diagnostic = builder.into_diagnostic(format_args!( "`{class_display}` cannot be used in a class pattern because it is not a type" )); - diagnostic.set_primary_message("This will raise `TypeError` at runtime"); + diagnostic.set_primary_annotation_message("This will raise `TypeError` at runtime"); } pub(crate) fn report_too_many_positional_patterns_for_class_pattern( @@ -2591,7 +2592,7 @@ pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol( 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"); + diagnostic.set_primary_annotation_message("This call will raise `TypeError` at runtime"); add_non_runtime_checkable_protocol_context(db, &mut diagnostic, protocol); diagnostic.info(format_args!( "A protocol class can only be used in `{function_name}` checks if it is decorated \ @@ -2618,7 +2619,7 @@ pub(crate) fn report_issubclass_check_against_protocol_with_non_method_members<' "`{class_name}` cannot be used as the second argument to `issubclass` \ as it is a protocol with non-method members" )); - diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + diagnostic.set_primary_annotation_message("This call will raise `TypeError` at runtime"); if let [single_member] = non_method_members { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -2677,7 +2678,7 @@ pub(crate) fn report_runtime_check_against_typed_dict( "`TypedDict` class `{class_name}` cannot be used as the second argument to `{function_name}`", function_name = function.name() )); - diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + diagnostic.set_primary_annotation_message("This call will raise `TypeError` at runtime"); } pub(crate) fn report_match_pattern_against_non_runtime_checkable_protocol( @@ -2693,7 +2694,7 @@ pub(crate) fn report_match_pattern_against_non_runtime_checkable_protocol( let mut diagnostic = builder.into_diagnostic(format_args!( "`TypedDict` class `{class_name}` cannot be used in a class pattern", )); - diagnostic.set_primary_message("This will raise `TypeError` at runtime"); + diagnostic.set_primary_annotation_message("This will raise `TypeError` at runtime"); } fn add_non_runtime_checkable_protocol_context<'db>( @@ -2750,7 +2751,7 @@ pub(crate) fn report_attempted_protocol_instantiation( 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"); + diagnostic.set_primary_annotation_message("This call will raise `TypeError` at runtime"); let mut class_def_diagnostic = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -2775,7 +2776,7 @@ pub(crate) fn report_call_to_abstract_method( let db = context.db(); let name = function.name(db); let mut diag = builder.into_diagnostic(format_args!("Cannot call `{name}` on class object")); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "`{name}` is an abstract {method_kind} with a trivial body" )); let span = abstract_method_span( @@ -2886,17 +2887,17 @@ pub(crate) fn report_undeclared_protocol_member( let suggestion = binding_type.promote(db); if should_give_hint(db, suggestion) { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Consider adding an annotation, e.g. `{symbol_name}: {} = ...`", suggestion.display(db) )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Consider adding an annotation for `{symbol_name}`" )); } } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{symbol_name}` is not declared as a protocol member" )); } @@ -2923,7 +2924,7 @@ pub(crate) fn report_undeclared_protocol_attribute( let symbol_name = target.attr.as_str(); let mut diagnostic = builder.into_diagnostic("Cannot assign to an undeclared attribute in a protocol method"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{symbol_name}` is not declared as a protocol member" )); @@ -3130,7 +3131,7 @@ pub(crate) fn report_unsupported_base( }; let db = context.db(); let mut diagnostic = builder.into_diagnostic("Unsupported class base"); - diagnostic.set_primary_message(format_args!("Has type `{}`", base_type.display(db))); + diagnostic.set_primary_annotation_message(format_args!("Has type `{}`", base_type.display(db))); diagnostic.set_concise_message(format_args!( "Unsupported class base with type `{}`", base_type.display(db) @@ -3206,14 +3207,15 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( "{quote}{suggestion}{quote}", quote = literal.value.first_literal_flags().quote_str() ); - diagnostic - .set_primary_message(format_args!("Did you mean {quoted_suggestion}?")); + diagnostic.set_primary_annotation_message(format_args!( + "Did you mean {quoted_suggestion}?" + )); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( quoted_suggestion, key_node.range(), ))); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Unknown key \"{key}\" - did you mean \"{suggestion}\"?", )); } @@ -3221,7 +3223,8 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( "Unknown key \"{key}\" for TypedDict `{typed_dict_name}` - did you mean \"{suggestion}\"?", )); } else { - diagnostic.set_primary_message(format_args!("Unknown key \"{key}\"")); + diagnostic + .set_primary_annotation_message(format_args!("Unknown key \"{key}\"")); if let Some(full_ty) = full_object_ty { diagnostic.set_concise_message(format_args!( "Unknown key \"{key}\" for TypedDict `{typed_dict_name}` (subscripted object has type `{full_ty}`)", @@ -3273,7 +3276,7 @@ pub(super) fn report_namedtuple_field_without_default_after_field_with_default<' "NamedTuple field without default value cannot follow field(s) with default value(s)", ); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Field `{field}` defined here without a default value", )); @@ -3322,11 +3325,11 @@ pub(super) fn report_named_tuple_field_with_leading_underscore<'db>( builder.into_diagnostic("NamedTuple field name cannot start with an underscore"); if field_definition.is_some() { - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "Class definition will raise `TypeError` at runtime due to this field", ); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Class definition will raise `TypeError` at runtime due to field `{field_name}`", )); } @@ -3494,14 +3497,14 @@ pub(crate) fn report_invalid_type_param_order<'db>( )); if let [single_typevar] = invalid_later_typevars { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Type variable `{}` does not have a default", single_typevar.name(db), )); } else { let later_typevars = format_enumeration(invalid_later_typevars.iter().map(|tv| tv.name(db))); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Type variables {later_typevars} do not have defaults", )); } @@ -3726,7 +3729,7 @@ pub(crate) fn report_shadowed_type_variable<'db>( diagnostic.set_concise_message(format_args!( "Generic {kind} `{name}` uses {typevar_kind} `{typevar_name}` already bound by an enclosing scope", )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{typevar_name}` used in {kind} definition here" )); let Some(other_definition) = other_typevar.binding_context(db).definition() else { @@ -3802,7 +3805,7 @@ pub(super) fn report_invalid_method_override<'db>( let mut diagnostic = builder.into_diagnostic(format_args!("Invalid override of method `{member}`")); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Definition is incompatible with `{overridden_method}`" )); @@ -3977,7 +3980,7 @@ pub(super) fn report_incompatible_base_method<'db>( "Base classes for class `{}` define method `{member}` incompatibly", class.name(db) )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{selected_name}.{member}` is incompatible with `{contract_name}.{member}`" )); if selected_decorator != contract_decorator { @@ -4046,7 +4049,7 @@ pub(super) fn report_overridden_final_method<'db>( let mut diagnostic = builder.into_diagnostic(format_args!("Cannot override `{superclass_name}.{member}`")); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Overrides a definition from superclass `{superclass_name}`" )); diagnostic.set_concise_message(format_args!( @@ -4214,7 +4217,7 @@ pub(super) fn report_overridden_final_variable<'db>( let mut diagnostic = builder.into_diagnostic(format_args!("Cannot override `{superclass_name}.{member}`")); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Overrides a final variable from superclass `{superclass_name}`" )); diagnostic.set_concise_message(format_args!( @@ -4267,7 +4270,7 @@ pub(super) fn report_unsupported_comparison<'db>( diagnostic_builder.into_diagnostic(format_args!("Unsupported `{}` operation", error.op)); if left_ty.is_equivalent_to(db, right_ty) { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Both operands have type `{}`", left_ty.display_with(db, display_settings.clone()) )); @@ -4424,7 +4427,7 @@ fn report_unsupported_binary_operation_impl<'a>( diagnostic_builder.into_diagnostic(format_args!("Unsupported `{operator}` operation")); if left_ty.is_equivalent_to(db, right_ty) { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Both operands have type `{}`", left_ty.display_with(db, display_settings.clone()) )); @@ -4475,7 +4478,7 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( class.name(db), base_class.name(db) )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Subclass `{}` is not frozen but base class `{}` is", class.name(db), base_class.name(db) @@ -4489,7 +4492,7 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( class.name(db), base_class.name(db) )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Subclass `{}` is frozen but base class `{}` is not", class.name(db), base_class.name(db) @@ -4553,7 +4556,7 @@ pub(super) fn report_invalid_total_ordering( let mut diagnostic = builder.into_diagnostic( "Class decorated with `@total_ordering` must define at least one ordering method", ); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{}` does not define `__lt__`, `__le__`, `__gt__`, or `__ge__`", class.name(db) )); @@ -4577,7 +4580,7 @@ pub(super) fn report_invalid_total_ordering_call( let mut diagnostic = builder.into_diagnostic( "`@functools.total_ordering` requires at least one ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`) to be defined", ); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{}` does not define `__lt__`, `__le__`, `__gt__`, or `__ge__`", class.name(db) )); @@ -4696,7 +4699,7 @@ pub(super) fn report_invalid_concatenate_last_arg<'db>( "The last argument to `typing.Concatenate` must be either `...` or a `ParamSpec` \ type variable", ); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Got `{}`", last_arg_type.display(context.db()) )); @@ -4741,7 +4744,7 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( if let Some((superclass, definition)) = class_and_def { let superclass_name = superclass.name(db); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Superclass `{superclass_name}` cannot be subclassed", )); let definition_module = parsed_module(db, definition.file(db)); @@ -4771,7 +4774,7 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( } diagnostic.annotate(annotation); } else if err_kind == CallErrorKind::NotCallable { - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "`class` statement will fail because `__init_subclass__` \ on a superclass is not callable", ); @@ -4780,7 +4783,7 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( `__init_subclass__` definition on a superclass", )); } else { - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "`class` statement may fail because `__init_subclass__` \ on a superclass may not be callable", ); diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 63154e4faa..c1928a671c 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1615,7 +1615,8 @@ fn check_classinfo_in_isinstance<'db>( let mut diagnostic = builder.into_diagnostic(format_args!( "`typing.Any` cannot be used with `isinstance()`" )); - diagnostic.set_primary_message("This call will raise `TypeError` at runtime"); + diagnostic + .set_primary_annotation_message("This call will raise `TypeError` at runtime"); } Type::KnownInstance(KnownInstanceType::UnionType(_)) => { report_invalid_union_type_elements( @@ -2563,7 +2564,7 @@ impl KnownFunction { }; let mut diagnostic = builder.into_diagnostic("Invalid argument to `reveal_mro`"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Can only pass a class object, generic alias or a union thereof" )); return; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 88a5686390..d4d4676fd0 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3627,7 +3627,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_NEWTYPE, &arguments.args[1]) { let mut diag = builder.into_diagnostic("invalid base for `typing.NewType`"); - diag.set_primary_message("A `NewType` base cannot be generic"); + diag.set_primary_annotation_message("A `NewType` base cannot be generic"); } return; } @@ -3653,7 +3653,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_NEWTYPE, &arguments.args[1]) { let mut diag = builder.into_diagnostic("invalid base for `typing.NewType`"); - diag.set_primary_message(format!("type `{}`", inferred.display(self.db()))); + diag.set_primary_annotation_message(format!("type `{}`", inferred.display(self.db()))); if matches!(inferred, Type::ProtocolInstance(_)) { diag.info("The base of a `NewType` is not allowed to be a protocol class."); } else if matches!(inferred, Type::TypedDict(_)) { @@ -4086,7 +4086,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .secondary(annotation.as_ref()) .message("Declared type"), ); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Incompatible value of type `{}`", value_ty.display(self.db()), )); @@ -4948,7 +4948,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let mut diag = builder.into_diagnostic(format_args!("Invalid global declaration of `{name}`")); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "`{name}` has no declarations or bindings in the global scope" )); diag.info("This limits ty's ability to make accurate inferences about the boundness and types of global-scope symbols"); @@ -7276,7 +7276,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diag = builder .into_diagnostic("Argument expression after ** must be a mapping type"); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Found `{}`", unpack_ty.display(self.db()) )); @@ -8256,7 +8256,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder .into_diagnostic("Argument expression after ** must be a mapping type") - .set_primary_message(format_args!("Found `{}`", mapping_type.display(self.db()))); + .set_primary_annotation_message(format_args!( + "Found `{}`", + mapping_type.display(self.db()) + )); } call_arguments @@ -9294,7 +9297,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diag = builder.into_diagnostic(format_args!(r#"The class `{class_name}` is deprecated"#)); if let Some(message) = deprecated.message { - diag.set_primary_message(message.value(self.db())); + diag.set_primary_annotation_message(message.value(self.db())); } diag.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated); return; @@ -9326,7 +9329,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diag = builder.into_diagnostic(format_args!(r#"The function `{func_name}` is deprecated"#)); if let Some(message) = deprecated.message { - diag.set_primary_message(message.value(self.db())); + diag.set_primary_annotation_message(message.value(self.db())); } diag.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated); } @@ -9878,7 +9881,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // because it's already caught by typing.Type. if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY39 { if let Some(("", builtin_name)) = as_pep_585_generic("typing", id) { - diagnostic.set_primary_message(format_args!("Did you mean `{builtin_name}`?")); + diagnostic + .set_primary_annotation_message(format_args!("Did you mean `{builtin_name}`?")); } } @@ -11969,7 +11973,7 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { "Reassignment of `Final` symbol `{place}` is not allowed" )); - diagnostic.set_primary_message("Reassignment of `Final` symbol"); + diagnostic.set_primary_annotation_message("Reassignment of `Final` symbol"); if let Some(previous_definition) = previous_definition { // It is not very helpful to show the previous definition if it results from @@ -11996,7 +12000,8 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { .message("Symbol declared as `Final` here"), ); } - diagnostic.set_primary_message("Symbol later reassigned here"); + diagnostic + .set_primary_annotation_message("Symbol later reassigned here"); } } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/dynamic_class.rs b/crates/ty_python_semantic/src/types/infer/builder/dynamic_class.rs index 1f34f5cd2c..18f8c325df 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/dynamic_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/dynamic_class.rs @@ -62,7 +62,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter 2 (`bases`) of `{fn_name}`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `{}`, found `{}`", formal_parameter_type.display(db), bases_type.display(db) @@ -112,8 +112,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid base for class created via `{fn_name}`" )); - diagnostic - .set_primary_message(format_args!("Has type `{}`", base.display(db))); + diagnostic.set_primary_annotation_message(format_args!( + "Has type `{}`", + base.display(db) + )); match class_base { ClassBase::Generic => { diagnostic.info(format_args!( @@ -143,8 +145,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Unsupported base for class created via `{fn_name}`" )); - diagnostic - .set_primary_message(format_args!("Has type `{}`", base.display(db))); + diagnostic.set_primary_annotation_message(format_args!( + "Has type `{}`", + base.display(db) + )); diagnostic.info(format_args!( "Classes created via `{fn_name}` cannot be protocols", )); @@ -179,7 +183,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let mut diagnostic = builder .into_diagnostic("Invalid base for class created via `type()`"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Has type `{}`", base.display(db) )); @@ -301,7 +305,7 @@ pub(super) fn report_mro_error_kind<'db>( context.report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_range) { let mut diagnostic = builder.into_diagnostic("Unsupported class base"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Has type `{}`", base_type.display(db) )); diff --git a/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs b/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs index 04d8ea70fa..9ba0dbe76b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs @@ -483,7 +483,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `value` of `{base_name}()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", name_type.display(db) )); @@ -906,7 +906,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `names` of `{base_name}()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `{}`, found `{}`", enum_names_type(db).display(db), names_ty.display(db), diff --git a/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs b/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs index 89220edb77..c338ae85d9 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs @@ -213,7 +213,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "Cannot assign to final attribute `{attribute}` on type `{}`", object_ty.display(db) )); - diagnostic.set_primary_message(if is_dataclass_like { + diagnostic.set_primary_annotation_message(if is_dataclass_like { "`Final` attributes can only be assigned in the class body, `__init__`, or `__post_init__` on dataclass-like classes" } else { "`Final` attributes can only be assigned in the class body or `__init__`" @@ -262,7 +262,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let mut diagnostic = diag_builder.into_diagnostic("Invalid assignment to final attribute"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{attribute}` already has a value in the class body" )); if let Some(final_declaration) = final_declaration { @@ -301,7 +301,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "Cannot delete final attribute `{attribute}` on type `{}`", object_ty.display(db) )); - diagnostic.set_primary_message("`Final` attributes cannot be deleted"); + diagnostic.set_primary_annotation_message("`Final` attributes cannot be deleted"); if let Some(final_declaration) = final_declaration { self.annotate_final_declaration(&mut diagnostic, final_declaration); } diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs index 9e8f9ddfe7..e2c6878a86 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -505,7 +505,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "Useless body for `@overload`-decorated function `{}`", function.name )); - diagnostic.set_primary_message("This statement will never be executed"); + diagnostic.set_primary_annotation_message("This statement will never be executed"); diagnostic.info( "`@overload`-decorated functions are solely for type checkers \ and must be overwritten at runtime by a non-`@overload`-decorated implementation", @@ -939,7 +939,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diag = builder.into_diagnostic(format_args!( "`{name}.kwargs` is valid only in `**kwargs` annotation", )); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Did you mean `{name}.args`?" )); add_type_expression_reference_link(diag); @@ -1068,7 +1068,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diag = builder.into_diagnostic(format_args!( "`{name}.args` is valid only in `*args` annotation", )); - diag.set_primary_message(format_args!("Did you mean `{name}.kwargs`?")); + diag.set_primary_annotation_message(format_args!( + "Did you mean `{name}.kwargs`?" + )); add_type_expression_reference_link(diag); } KnownClass::Dict.to_specialized_instance( diff --git a/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs b/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs index 16dcda2c89..aa69630a62 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs @@ -243,7 +243,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `defaults` of `namedtuple()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `Iterable[Any] | None`, found `{}`", kw_type.display(db) )); @@ -260,7 +260,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `rename` of `namedtuple()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `bool`, found `{}`", kw_type.display(db) )); @@ -280,7 +280,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `module` of `namedtuple()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str | None`, found `{}`", kw_type.display(db) )); @@ -335,7 +335,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `typename` of `{kind}()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", name_type.display(db) )); @@ -464,7 +464,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `field_names` of `namedtuple()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str` or an iterable of strings, found `{}`", fields_type.display(db) )); @@ -511,7 +511,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let mut diagnostic = builder.into_diagnostic(format_args!("Too many defaults for `namedtuple()`")); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Got {defaults_count} default values but only {num_fields} field names" )); diagnostic.info("This will raise `TypeError` at runtime"); @@ -564,7 +564,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic( "Invalid argument to parameter `fields` of `NamedTuple()`", ); - diagnostic.set_primary_message("`fields` must be a literal list or tuple"); + diagnostic + .set_primary_annotation_message("`fields` must be a literal list or tuple"); } return NamedTupleSpec::unknown(db); } @@ -600,7 +601,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic( "Invalid argument to parameter `fields` of `NamedTuple()`", ); - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "`fields` must be a sequence of literal lists or tuples", ); } @@ -626,7 +627,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic( "Invalid argument to parameter `fields` of `NamedTuple()`", ); - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "Each element in `fields` must be a length-2 tuple or list", ); } @@ -662,7 +663,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, name_expr) { let mut diagnostic = builder.into_diagnostic("Invalid `NamedTuple` field name definition"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected a string literal for the field name, found `{}`", name_type.display(db) )); @@ -707,7 +708,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Duplicate field name `{field_name}` in `{kind}()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Field `{field_name}` already defined; will raise `ValueError` at runtime" )); } @@ -718,21 +719,21 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Field name `{field_name}` in `{kind}()` cannot start with an underscore" )); - diagnostic.set_primary_message("Will raise `ValueError` at runtime"); + diagnostic.set_primary_annotation_message("Will raise `ValueError` at runtime"); } else if is_keyword(field_name) && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) { let mut diagnostic = builder.into_diagnostic(format_args!( "Field name `{field_name}` in `{kind}()` cannot be a Python keyword" )); - diagnostic.set_primary_message("Will raise `ValueError` at runtime"); + diagnostic.set_primary_annotation_message("Will raise `ValueError` at runtime"); } else if !is_identifier(field_name) && let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) { let mut diagnostic = builder.into_diagnostic(format_args!( "Field name `{field_name}` in `{kind}()` is not a valid identifier" )); - diagnostic.set_primary_message("Will raise `ValueError` at runtime"); + diagnostic.set_primary_annotation_message("Will raise `ValueError` at runtime"); } } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/new_class.rs b/crates/ty_python_semantic/src/types/infer/builder/new_class.rs index efdfecfe08..1d82ff6841 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/new_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/new_class.rs @@ -80,7 +80,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic( "Invalid argument to parameter 1 (`name`) of `types.new_class()`", ); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", name_type.display(db) )); diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/function.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/function.rs index e077088c70..1288f6ef65 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/function.rs @@ -162,7 +162,7 @@ fn check_legacy_positional_only_convention<'db>( "Invalid use of the legacy convention \ for positional-only parameters", ); - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "Parameter name begins with `__` but will not be treated as positional-only", ); diagnostic.info( @@ -253,7 +253,7 @@ fn check_legacy_typevar_defaults<'db>( )); if is_later_in_list { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Default of `{typevar_name}` references later type parameter `{}`", bad_typevar.name(db), )); @@ -263,7 +263,7 @@ fn check_legacy_typevar_defaults<'db>( bad_typevar.name(db) )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Default of `{typevar_name}` references out-of-scope type variable `{}`", bad_typevar.name(db), )); @@ -392,14 +392,14 @@ fn check_legacy_typevar_ordering<'db>( )); if let [single_typevar] = &*state.invalid_later_tvars { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Type variable `{}` does not have a default", single_typevar.name(db), )); } else { let later_typevars = format_enumeration(state.invalid_later_tvars.iter().map(|tv| tv.name(db))); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Type variables {later_typevars} do not have defaults", )); } diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs index 9a79acecfd..14270528b7 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs @@ -113,7 +113,7 @@ pub(crate) fn check_overloaded_function<'db>( "Overloaded function `{}` requires at least two overloads", function_node.name )); - diagnostic.set_primary_message("Only one overload defined here"); + diagnostic.set_primary_annotation_message("Only one overload defined here"); if let Some(decorator) = single_overload.find_known_decorator_span(db, KnownFunction::Overload) { diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs index a341fac9c4..0d502eed31 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs @@ -371,7 +371,7 @@ pub(crate) fn check_static_class_definitions<'db>( "TypedDict class `{}` can only inherit from TypedDict classes", class.name(db), )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{}` is not a `TypedDict` class", base_class.name(db) )); @@ -665,7 +665,7 @@ pub(crate) fn check_static_class_definitions<'db>( "Invalid argument to parameter `{arg_name}` \ in `TypedDict` definition", )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected either `True` or `False`, got object of type `{}`", passed_type.display(db) )); @@ -1270,7 +1270,8 @@ fn check_final_class_abstract_methods<'db>( "Final class `{class_name}` has unimplemented abstract method \ `{first_method_name}`", )); - diagnostic.set_primary_message(format_args!("`{first_method_name}` is unimplemented")); + diagnostic + .set_primary_annotation_message(format_args!("`{first_method_name}` is unimplemented")); } else { let verbose = db.verbose(); let max_abstract_methods_to_print = if verbose { num_abstract_methods } else { 3 }; @@ -1278,7 +1279,7 @@ fn check_final_class_abstract_methods<'db>( format_enumeration(abstract_methods.keys().take(max_abstract_methods_to_print)); if num_abstract_methods > max_abstract_methods_to_print { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "{num_abstract_methods} abstract methods are unimplemented, \ including {formatted_methods}", )); @@ -1295,7 +1296,7 @@ fn check_final_class_abstract_methods<'db>( "Final class `{class_name}` has unimplemented \ abstract methods {formatted_methods}", )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Abstract methods {formatted_methods} are unimplemented" )); } diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/type_param_validation.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/type_param_validation.rs index 472fdcb7cd..e223539cf3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/type_param_validation.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/type_param_validation.rs @@ -47,7 +47,7 @@ pub(crate) fn check_single_typevar_tuple_pep695( "{owner_kind} `{owner_name}` cannot have multiple `TypeVarTuple` type parameters" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{}` is an additional TypeVarTuple", typevar_tuple.name )); @@ -112,7 +112,7 @@ pub(crate) fn check_no_default_after_typevar_tuple_pep695( typevar_tuple.name )); - diagnostic.set_primary_message(format_args!("`{single_name}` has a default")); + diagnostic.set_primary_annotation_message(format_args!("`{single_name}` has a default")); } else { let names = format_enumeration(params_with_defaults.iter().map(|p| p.name())); @@ -121,7 +121,7 @@ pub(crate) fn check_no_default_after_typevar_tuple_pep695( typevar_tuple.name )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{}` has a default", params_with_defaults[0].name() )); diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/typed_dict.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/typed_dict.rs index ec861a367c..95a42c06cf 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/typed_dict.rs @@ -481,7 +481,7 @@ fn report_typed_dict_field_override<'db>( )) }; - diagnostic.set_primary_message(format_args!("{reason}")); + diagnostic.set_primary_annotation_message(format_args!("{reason}")); if own_field_definition.is_none() { add_definition_subdiagnostic( diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index a311cbcb65..140bd52a74 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -517,7 +517,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(builder) = self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) { let mut diagnostic = builder.into_diagnostic("Cannot specialize non-generic type alias"); - diagnostic.set_primary_message("Double specialization is not allowed"); + diagnostic.set_primary_annotation_message("Double specialization is not allowed"); } return Type::unknown(); } @@ -1767,7 +1767,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { slice_ty.display(db), object_ty.display(db), )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected value assignable to `{}`", expected_ty.display(db) )); diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_call.rs b/crates/ty_python_semantic/src/types/infer/builder/type_call.rs index ea4532f886..fecd8bf33c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_call.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_call.rs @@ -184,7 +184,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let mut diagnostic = builder .into_diagnostic("Invalid argument to parameter 3 (`namespace`) of `type()`"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `dict[str, Any]`, found `{}`", namespace_type.display(db) )); @@ -199,7 +199,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let mut diagnostic = builder.into_diagnostic("Invalid argument to parameter 1 (`name`) of `type()`"); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", name_type.display(db) )); diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 8addf06db2..6cb9f1704b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -298,7 +298,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.into_diagnostic("Unsupported `|` operation"); if left_type_value.is_equivalent_to(self.db(), right_type_value) { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Both operands have type `{}`", left_type_value.display(self.db()) )); @@ -444,7 +444,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(single_element) = bytes.as_single_part_bytestring() && let Ok(valid_string) = String::from_utf8(single_element.value.to_vec()) { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `typing.Literal[b\"{valid_string}\"]`?" )); } @@ -464,7 +464,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ), ) { if let Some(int) = int.as_i64() { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `typing.Literal[{int}]`?" )); } @@ -509,7 +509,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.type_expression_context() ), ) { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `typing.Literal[{}]`?", if bool_value.value { "True" } else { "False" } )); @@ -539,7 +539,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let hinted_type = KnownClass::List.to_specialized_instance(db, &[inner_type]); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `{}`?", hinted_type.display(self.db()), )); @@ -572,7 +572,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if inner_types.iter().all(|ty| ty.is_hintable(self.db())) { let hinted_type = Type::heterogeneous_tuple(self.db(), inner_types); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `{}`?", hinted_type.display(self.db()), )); @@ -716,7 +716,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if key_type.is_hintable(self.db()) && value_type.is_hintable(self.db()) { let hinted_type = KnownClass::Dict .to_specialized_instance(self.db(), &[key_type, value_type]); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `{}`?", hinted_type.display(self.db()), )); @@ -744,7 +744,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let hinted_type = KnownClass::Set.to_specialized_instance(self.db(), &[inner_type]); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `{}`?", hinted_type.display(self.db()), )); @@ -1082,8 +1082,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let mut diagnostic = builder.into_diagnostic("Invalid `tuple` specialization"); - diagnostic - .set_primary_message("`...` cannot be used after an unpacked element"); + diagnostic.set_primary_annotation_message( + "`...` cannot be used after an unpacked element", + ); } let result = TupleType::homogeneous(self.db(), element_ty); self.store_expression_type(&tuple.slice, Type::tuple(Some(result))); @@ -1099,7 +1100,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, tuple) { let mut diagnostic = builder.into_diagnostic("Invalid `tuple` specialization"); - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "`...` can only be used as the second element \ in a two-element `tuple` specialization", ); @@ -1188,7 +1189,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, tuple) { let mut diagnostic = builder.into_diagnostic("Invalid `tuple` specialization"); - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "`...` can only be used as the second element \ in a two-element `tuple` specialization", ); @@ -1911,7 +1912,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "`[...]` is not a valid parameter list for `Callable`", ) { if let Some(returns) = return_type { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Did you mean `Callable[..., {}]`?", returns.display(db) )); diff --git a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs index e786503ad1..6cd7934cfd 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs @@ -174,7 +174,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `{arg_name}` of `TypedDict()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected either `True` or `False`, got object of type `{}`", kw_type.display(db) )); @@ -275,7 +275,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `typename` of `TypedDict()`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", name_type.display(db) )); @@ -650,8 +650,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "Expected a string-literal key \ in the `fields` dict of `TypedDict()`", ); - diagnostic - .set_primary_message(format_args!("Found `{}`", key_type.display(db))); + diagnostic.set_primary_annotation_message(format_args!( + "Found `{}`", + key_type.display(db) + )); } } else { self.infer_expression(value, TypeContext::default()); diff --git a/crates/ty_python_semantic/src/types/infer/builder/typevar.rs b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs index 15b411278b..647120106d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typevar.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs @@ -261,7 +261,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(mut diagnostic) = not_assignable_to_upper_bound() { annotate_default_definition(&mut diagnostic); if let Some(name) = name { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Constraint `{constraint}` of default \ `{default_name}` is not assignable to upper \ bound of `{name}`", @@ -277,7 +277,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { constraint = constraint.display(db), )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Constraint `{constraint}` of `{default_name}` is \ not assignable to upper bound `{bound}` of \ outer TypeVar", @@ -303,7 +303,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(mut diagnostic) = not_assignable_to_upper_bound() { annotate_default_definition(&mut diagnostic); if let Some(name) = name { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Upper bound `{default_bound}` of default \ `{default_name}` is not assignable to upper \ bound of `{name}`", @@ -319,7 +319,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { default_bound = default_bound.display(db), )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Upper bound `{default_bound}` of default \ `{default_name}` is not assignable to upper \ bound of outer TypeVar", @@ -352,7 +352,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(mut diagnostic) = inconsistent_with_constraints() { annotate_default_definition(&mut diagnostic); if let Some(name) = name { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Constraint `{constraint}` of default \ `{default_name}` is not one of the constraints \ of `{name}`", @@ -367,7 +367,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { constraint = default_constraint.display(db), )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "Constraint `{constraint}` of outer TypeVar default \ `{default_name}` is not one of the constraints \ of the outer TypeVar", @@ -392,7 +392,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(mut diagnostic) = inconsistent_with_constraints() { annotate_default_definition(&mut diagnostic); if let Some(default_bound) = default_typevar.upper_bound(db) { - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "Bounded TypeVar cannot be used as the default \ for a constrained TypeVar", ); @@ -401,7 +401,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { default_bound = default_bound.display(db), )); } else { - diagnostic.set_primary_message( + diagnostic.set_primary_annotation_message( "Unbounded TypeVar cannot be used as the default \ for a constrained TypeVar", ); @@ -422,9 +422,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if !default_ty.is_assignable_to(db, bound) { if let Some(mut diagnostic) = not_assignable_to_upper_bound() { if let Some(name) = name { - diagnostic.set_primary_message(format_args!("Default of `{name}`")); + diagnostic.set_primary_annotation_message(format_args!( + "Default of `{name}`" + )); } else { - diagnostic.set_primary_message("TypeVar default"); + diagnostic.set_primary_annotation_message("TypeVar default"); } diagnostic.set_concise_message(not_assignable_message); } @@ -439,12 +441,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { if let Some(mut diagnostic) = inconsistent_with_constraints() { if let Some(name) = name { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{default}` is not one of the constraints of `{name}`", default = default_ty.display(db), )); } else { - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{default}` is not one of the constraints", default = default_ty.display(db), )); @@ -510,7 +512,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid default for type parameter `{typevar_name}`" )); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "`{outer_name}` is a type parameter bound in an outer scope" )); diagnostic.set_concise_message(format_args!( diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index 5075a5f283..7681e1c8c0 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -42,7 +42,7 @@ fn get_symbol<'db>( fn assert_diagnostic_messages(diagnostics: &[Diagnostic], expected: &[&str]) { let messages: Vec<&str> = diagnostics .iter() - .map(Diagnostic::primary_message) + .map(Diagnostic::headline_message) .collect(); assert_eq!(&messages, expected); } diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index c5ed14762b..8eb83f20d1 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -1289,7 +1289,7 @@ fn report_invalid_attribute_override<'db>( let mut diagnostic = builder.into_diagnostic(format_args!("Invalid override of attribute `{member}`")); - diagnostic.set_primary_message(format_args!( + diagnostic.set_primary_annotation_message(format_args!( "{subclass_kind} cannot override {superclass_kind} `{superclass_member}`" )); diagnostic.info("This violates the Liskov Substitution Principle"); diff --git a/crates/ty_python_semantic/src/types/string_annotation.rs b/crates/ty_python_semantic/src/types/string_annotation.rs index f51d386e11..a0ceb3e2bd 100644 --- a/crates/ty_python_semantic/src/types/string_annotation.rs +++ b/crates/ty_python_semantic/src/types/string_annotation.rs @@ -82,7 +82,7 @@ pub(crate) fn parse_string_annotation( let mut diagnostic = builder.into_diagnostic("Syntax error in forward annotation"); - diagnostic.set_primary_message(&error); + diagnostic.set_primary_annotation_message(&error); let possible_secondary = string_literal .range() diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index e10cd65df7..f30146338a 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -1462,7 +1462,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { self.key, )); - diagnostic.set_primary_message(format_args!("key is marked read-only")); + diagnostic.set_primary_annotation_message(format_args!("key is marked read-only")); self.add_object_type_annotation(db, &mut diagnostic); Self::add_item_definition_subdiagnostic( db, @@ -1502,7 +1502,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { self.key, )); - diagnostic.set_primary_message(format_args!("value of type `{value_d}`")); + diagnostic.set_primary_annotation_message(format_args!("value of type `{value_d}`")); diagnostic.annotate( self.context diff --git a/crates/ty_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs index 45afdab987..0469e22d70 100644 --- a/crates/ty_python_semantic/src/types/unpacker.rs +++ b/crates/ty_python_semantic/src/types/unpacker.rs @@ -230,7 +230,7 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { ResizeTupleError::TooManyValues => { let mut diag = builder.into_diagnostic("Too many values to unpack"); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Expected {}", target_len.display_minimum(), )); @@ -241,7 +241,7 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { ResizeTupleError::TooFewValues => { let mut diag = builder.into_diagnostic("Not enough values to unpack"); - diag.set_primary_message(format_args!( + diag.set_primary_annotation_message(format_args!( "Expected {}", target_len.display_minimum(), )); diff --git a/crates/ty_server/src/server/api/diagnostics.rs b/crates/ty_server/src/server/api/diagnostics.rs index 6c1236b2c7..3fe6808e75 100644 --- a/crates/ty_server/src/server/api/diagnostics.rs +++ b/crates/ty_server/src/server/api/diagnostics.rs @@ -540,9 +540,9 @@ pub(super) fn to_lsp_diagnostic( .primary_annotation() .and_then(|annotation| annotation.get_message()) { - format!("{}: {annotation_message}", diagnostic.primary_message()) + format!("{}: {annotation_message}", diagnostic.headline_message()) } else { - diagnostic.primary_message().to_string() + diagnostic.headline_message().to_string() } } else { diagnostic.concise_message().to_string() diff --git a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs index d64ec525f1..088244900c 100644 --- a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs +++ b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs @@ -280,7 +280,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> { } else { tracing::debug!( "Ignoring diagnostic without a file: {diagnostic}", - diagnostic = diagnostic.primary_message() + diagnostic = diagnostic.headline_message() ); } } diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index c5f4dde36f..6a69c4d60e 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -913,7 +913,7 @@ impl Diagnostic { SubDiagnostic { severity: sub_diagnostic.severity().into(), - message: sub_diagnostic.primary_message().to_string(), + message: sub_diagnostic.headline_message().to_string(), annotations, } }) From 660edc07a908a8faa92cfa50a940ac4276b707f5 Mon Sep 17 00:00:00 2001 From: Perfloop Agent Date: Fri, 24 Jul 2026 16:09:00 +0200 Subject: [PATCH 048/390] [ty] Improve importer performance (#27159) Signed-off-by: Perfloop Agent --- crates/ty_ide/src/importer.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/ty_ide/src/importer.rs b/crates/ty_ide/src/importer.rs index 1821696246..c2ed60d5ae 100644 --- a/crates/ty_ide/src/importer.rs +++ b/crates/ty_ide/src/importer.rs @@ -366,10 +366,7 @@ impl<'ast> MembersInScope<'ast> { } pub(crate) fn find_member(&self, symbol_name: &str) -> Option<&MemberInScope> { - self.map - .iter() - .find(|(name, _)| *name == symbol_name) - .map(|(_, member)| member) + self.map.get(symbol_name) } pub(crate) fn satisfies( From 77be1b6e533c0e8d1391f2b47c322d1cae0e14d1 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:45:26 -0400 Subject: [PATCH 049/390] Normalize rule selectors in ecosystem checks (#27158) Summary -- Now that we allow rule names in selectors in preview, we need to normalize them back to codes to allow running on stable. This PR implements this normalization as one of our config overrides, using the comparison executable to extract the rule mapping from `ruff rule`. Test Plan -- CI on this PR and then #27154 --- crates/ruff_linter/src/preview.rs | 2 + python/ruff-ecosystem/ruff_ecosystem/check.py | 9 ++- .../ruff-ecosystem/ruff_ecosystem/format.py | 19 +++++- .../ruff-ecosystem/ruff_ecosystem/projects.py | 65 ++++++++++++++++++- 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 57578452a9..69b886bc48 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -331,6 +331,8 @@ pub(crate) const fn is_pep604_future_annotations_fix_enabled(settings: &LinterSe } // https://github.com/astral-sh/ruff/pull/25614 +// TODO(brent) Remove ecosystem selector normalization when stabilizing human-readable rule names: +// https://github.com/astral-sh/ruff/pull/27158 pub const fn is_human_readable_names_enabled(preview: PreviewMode) -> bool { preview.is_enabled() } diff --git a/python/ruff-ecosystem/ruff_ecosystem/check.py b/python/ruff-ecosystem/ruff_ecosystem/check.py index 78fae8f162..9d92fd4187 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/check.py +++ b/python/ruff-ecosystem/ruff_ecosystem/check.py @@ -22,6 +22,7 @@ markdown_plus_minus, markdown_project_section, ) +from ruff_ecosystem.projects import rule_name_to_code from ruff_ecosystem.types import ( Comparison, Diff, @@ -508,7 +509,13 @@ async def compare_check( config_overrides: ConfigOverrides, cloned_repo: ClonedRepository, ) -> Comparison: - with config_overrides.patch_config(cloned_repo.path, options.preview): + # TODO(brent) Remove this workaround when human-readable rule names are stabilized. + rule_names = ( + rule_name_to_code(ruff_comparison_executable.resolve()) + if not options.preview + else {} + ) + with config_overrides.patch_config(cloned_repo.path, options.preview, rule_names): async with asyncio.TaskGroup() as tg: baseline_task = tg.create_task( ruff_check( diff --git a/python/ruff-ecosystem/ruff_ecosystem/format.py b/python/ruff-ecosystem/ruff_ecosystem/format.py index b503a51dc6..5415cecb77 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/format.py +++ b/python/ruff-ecosystem/ruff_ecosystem/format.py @@ -16,6 +16,7 @@ from ruff_ecosystem import logger from ruff_ecosystem.markdown import markdown_project_section +from ruff_ecosystem.projects import rule_name_to_code from ruff_ecosystem.types import Comparison, Diff, Result, ToolError if TYPE_CHECKING: @@ -173,7 +174,13 @@ async def format_then_format( config_overrides: ConfigOverrides, cloned_repo: ClonedRepository, ) -> Sequence[str]: - with config_overrides.patch_config(cloned_repo.path, options.preview): + # TODO(brent) Remove this workaround when human-readable rule names are stabilized. + rule_names = ( + rule_name_to_code(ruff_comparison_executable.resolve()) + if not options.preview + else {} + ) + with config_overrides.patch_config(cloned_repo.path, options.preview, rule_names): # Run format to get the baseline await format( formatter=baseline_formatter, @@ -201,7 +208,13 @@ async def format_and_format( config_overrides: ConfigOverrides, cloned_repo: ClonedRepository, ) -> Sequence[str]: - with config_overrides.patch_config(cloned_repo.path, options.preview): + # TODO(brent) Remove this workaround when human-readable rule names are stabilized. + rule_names = ( + rule_name_to_code(ruff_comparison_executable.resolve()) + if not options.preview + else {} + ) + with config_overrides.patch_config(cloned_repo.path, options.preview, rule_names): # Run format without diff to get the baseline await format( formatter=baseline_formatter, @@ -218,7 +231,7 @@ async def format_and_format( # Then reset await cloned_repo.reset() - with config_overrides.patch_config(cloned_repo.path, options.preview): + with config_overrides.patch_config(cloned_repo.path, options.preview, rule_names): # Then run format again await format( formatter=Formatter.ruff, diff --git a/python/ruff-ecosystem/ruff_ecosystem/projects.py b/python/ruff-ecosystem/ruff_ecosystem/projects.py index e25d2fd10f..2e86360817 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/projects.py +++ b/python/ruff-ecosystem/ruff_ecosystem/projects.py @@ -7,12 +7,13 @@ import abc import contextlib import dataclasses +import json from asyncio import create_subprocess_exec from dataclasses import dataclass, field from enum import Enum from functools import cache from pathlib import Path -from subprocess import DEVNULL, PIPE +from subprocess import DEVNULL, PIPE, check_output from typing import Any, Self import tomli @@ -55,6 +56,61 @@ def __post_init__(self): "required-version": None } +# TODO(brent) Remove selector normalization when human-readable rule names are stabilized. +RULE_SELECTOR_OPTIONS = ( + "select", + "extend-select", + "ignore", + "extend-ignore", + "fixable", + "extend-fixable", + "unfixable", + "extend-unfixable", + "extend-safe-fixes", + "extend-unsafe-fixes", +) + + +@cache +def rule_name_to_code(executable: Path) -> dict[str, str]: + rules = json.loads( + check_output( + [executable, "rule", "--all", "--output-format", "json"], + encoding="utf8", + ) + ) + return {rule["name"]: rule["code"] for rule in rules} + + +def normalize_rule_selectors( + config: dict[str, Any], rule_names: dict[str, str] +) -> None: + selector_lists: list[list[Any]] = [] + + for section in (config, config.get("lint")): + if not isinstance(section, dict): + continue + + for option in RULE_SELECTOR_OPTIONS: + if isinstance(selectors := section.get(option), list): + selector_lists.append(selectors) + + for option in ("per-file-ignores", "extend-per-file-ignores"): + if isinstance(per_file_ignores := section.get(option), dict): + selector_lists.extend( + selectors + for selectors in per_file_ignores.values() + if isinstance(selectors, list) + ) + + for selectors in selector_lists: + selectors[:] = [ + rule_names.get(selector, selector) + if isinstance(selector, str) + else selector + for selector in selectors + ] + @dataclass(frozen=True) class ConfigOverrides(Serializable): @@ -91,6 +147,7 @@ def patch_config( self, dirpath: Path, preview: bool, + rule_names: dict[str, str], ) -> None: """ Temporarily patch the Ruff configuration file in the given directory. @@ -153,6 +210,12 @@ def patch_config( else: target[names[-1]] = value + if not preview: + ruff_config = toml + for name in base: + ruff_config = ruff_config[name] + normalize_rule_selectors(ruff_config, rule_names) + tomli_w.dump(toml, path.open("wb")) try: From 7d3e61eebf33b97f2cebfd47c26668780eb79c1c Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Fri, 24 Jul 2026 11:31:09 -0400 Subject: [PATCH 050/390] Update uv to 0.11.32 in CI (#27154) Latest releases has updates to metadata and uv check that our tests want to rely on in https://github.com/astral-sh/ruff/pull/25551 --- .github/workflows/ci.yaml | 28 ++++++++++---------- .github/workflows/daily_fuzz.yaml | 2 +- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/sync_typeshed.yaml | 6 ++--- .github/workflows/ty-ecosystem-analyzer.yaml | 4 +-- .github/workflows/ty-ecosystem-report.yaml | 2 +- .pre-commit-config.yaml | 2 +- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a03c2fe203..c2576232eb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -347,7 +347,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" enable-cache: "true" - name: ty mdtests (GitHub annotations) if: ${{ needs.determine_changes.outputs.ty == 'true' }} @@ -411,7 +411,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" enable-cache: "true" - name: "Run tests" run: cargo nextest run --cargo-profile profiling --all-features @@ -450,7 +450,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" enable-cache: "true" - name: "Run tests" run: | @@ -560,7 +560,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: ruff-linux-debug @@ -602,7 +602,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - name: "Install Rust toolchain" run: rustup component add rustfmt # Run all code generation scripts, and verify that the current output is @@ -649,7 +649,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} activate-environment: true - version: "0.11.31" + version: "0.11.32" - name: "Install Rust toolchain" run: rustup show @@ -763,7 +763,7 @@ jobs: run: git fetch --no-tags --filter=blob:none --unshallow origin - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -829,7 +829,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -882,7 +882,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 @@ -920,7 +920,7 @@ jobs: with: python-version: 3.13 activate-environment: true - version: "0.11.31" + version: "0.11.32" - name: "Install dependencies" run: uv pip install -r docs/requirements.txt - name: "Update README File" @@ -1077,7 +1077,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - name: "Install Rust toolchain" run: rustup show @@ -1177,7 +1177,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - name: "Install codspeed" uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 @@ -1228,7 +1228,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - name: "Install Rust toolchain" run: rustup show @@ -1281,7 +1281,7 @@ jobs: - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - name: "Install codspeed" uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index d1b2f41a2d..3d7162646a 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -38,7 +38,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - name: "Install Rust toolchain" run: rustup show - name: "Install mold" diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 4058f1ff39..2fcd7819e5 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -24,7 +24,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: wheels-* diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index 851d117002..be9daeebcd 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -86,7 +86,7 @@ jobs: git config --global user.email '<>' - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - name: Sync typeshed stubs run: | rm -rf "ruff/${VENDORED_TYPESHED}" @@ -142,7 +142,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - name: Setup git run: | git config --global user.name typeshedbot @@ -184,7 +184,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" - name: Setup git run: | git config --global user.name typeshedbot diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index efb2d079ef..96068e766a 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -127,7 +127,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" ignore-empty-workdir: true # The default of `enable-cache: auto` leads to warnings from setup-uv in this job, # since its cache-invalidation keys (pyproject.toml, uv.lock, etc.) aren't available @@ -187,7 +187,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.31" + version: "0.11.32" ignore-empty-workdir: true # The default of `enable-cache: auto` leads to warnings from setup-uv in this job, # since its cache-invalidation keys (pyproject.toml, uv.lock, etc.) aren't available diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index e06ae6da2e..d8c9bb18b9 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -36,7 +36,7 @@ jobs: uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: enable-cache: true - version: "0.11.31" + version: "0.11.32" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e85a75a8e4..55e2acc3cc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -113,7 +113,7 @@ repos: priority: 0 - repo: https://github.com/astral-sh/uv-pre-commit - rev: 69e5d7b46d7a93b633431a498a46cf3a8a2181f4 # frozen: 0.11.31 + rev: e3e6ef7d9bda544b2e795782dbd7d2a4fbd7eb6d # frozen: 0.11.32 hooks: - id: uv-lock priority: 0 From d297b16535153a7c70e58b243d008de6dcb05bd4 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Fri, 24 Jul 2026 11:57:59 -0400 Subject: [PATCH 051/390] [ty] Discover uv workspace roots (#25551) This introduces minimal support for acquiring `uv workspace metadata` when, and only when, ty is invoked through `uv check`, and packages/workspaces are being requested (and not e.g. --script, see below). All other invocations of metadata are "future work". ## `uv check` Recap As a recap, `uv check` on current main will: * Apply standard package selection (usually default to the package that owns cwd, with `--package` to select a different one, and `--all-packages` to select all of them) * Run `uv sync` for the selected packages, and to fetch the appropriate version of ty * Set `VIRTUAL_ENV=/path/to/.venv/` * Set `UV=/path/to/uv.exe` * Set `TY_UV=1` * Set the working dir to either be the "right" one for the selected packages (generally the one package that will be worked on, or the workspace if there's multiple or the package lives outside the workspace) * Run something like `ty check --exclude=packages/a/subpkg/ -- packages/a/ packages/b/` * in this example invocation, `a` and `b` were selected and `subpkg` was not, and an explicit exclude was introduced to prevent it from being implicitly analyzed as part of `a`, under normal circumstances no exclude is needed, but any non-virtual workspace may need this kind of thing, and non-virtual workspaces are orthodox) The consequences of this are that `uv check` already "just works" without this PR, however there are a few corner cases that are debatable. ## What This Changes The following behaviours should *only* apply when TY_UV=1 mode is set, indicating we are being invoked through `uv check` (if you set env-var we simply assume you are `uv check` and GLHF if you're not; interactions with flags uv will never set are not guaranteed to make sense). * We will run `uv workspace metadata --frozen --active` in the cwd that uv told us to run in * `--frozen` because we assume apriori that `uv check` already ran lock+sync * `--active` to ensure we continue to use the VIRTUAL_ENV that uv told us to use (notably important for `--isolated`) * Notably we *do not* pass `--sync` because the new default main uv behaviour for `uv workspace metadata` is to opportunistically emit environment metadata (python interpreter, package-to-module mappings) even if `--sync` isn't requested. We *should not* pass `--sync` in this case because uv has performed package/extra/group selection and we don't know what that was, and we are trying to avoid defining a protocol for forwarding that for the MVP. * Corner Case 1: If the user runs `uv check --isolated` and *there is no uv.lock* then this will fail as the rerun with --frozen will not find any lock to use. This is fine for an MVP. * Corner Case 2: If the workspace has `conflicts` the package versions reported may be subtly incorrect. In general "how does ty operate in a world with conflicts" is "that's the neat part, it doesn't" for the forseeable future (uv check can maybe be made to work but how the heck does the LSP?) * Corner Case 3: flags like `--no-project` and `--no-sync` are whacky nonsense I am refusing to put anymore thought into for an MVP. * The relevant parts of the metadata will be stored on ProjectMetadata (currently only a few fields) * ty will preferentially read config from the uv workspace root that metadata reports (notably quite important for the member-outside-the-workspace-dir case, where otherwise ty could get lost) * ty will preferentially use the python version/interpreter the metadata reports, deferring to uv as "the authority" on these configs (and ignoring it's own). ## Future Work for `--watch` If TY_UV=1 mode is enabled, `--watch` is forbidden (which is fine uv will never pass that). In the future it would be nice to have `uv check --watch`, but this will require us to have a proper system for ty actually telling uv to resync with the proper package selection, and for uv metadata to have some way to report what members to watch. ## Future Work for `ty check` It would be nice if `ty check` could opportunistically detect that uv is available and auto-use TY_UV=1 mode. This is explicitly out of scope for the MVP as it causes endless UX arguments. I think under the current architecture it will probably "just" be ty instead opting to run `uv workspace metadata --sync` to sync the entire workspace (maybe with `--isolated` if we don't want to clobber the "real" venv... also it would then remember the venv location and pass `--active` for all subsequent invocations to keep reusing the same ephemeral venv). Note to self: `uv workspace metadata --isolated` exists but is fake because of the accursed "global deprecated --isolated flag that is different from the --isolated flag other commands have, so need to add that in the future if we decide we need it. ## Future Work For Scripts In the current implementation `TY_UV=1` mode is disabled if a path to a *file* is passed, as this should only happen if `uv check --script` is run. This is a temporary measure to keep scope tight for an MVP. `uv check --script` already works better than `ty check path/to/script.py` because `uv check` will set VIRTUAL_ENV to the script's isolated venv -- the above just means we won't immediately have richer features TY_UV mode adds. In "package mode" where TY_UV=1 applies, if a package contains random pep723 scripts, ty will attempt to analyze them with the workspace's venv, which is wrong in the same way ty is wrong already. In a followup we will make ty check if a file is a script, and if so, not analyze it unless the CLI was passed an explicit path to exactly that file (indicating --script). In a followup we can teach ty to run `uv workspace metadata --script=...` when it finds a script and considers it in-scope to analyze it. Some fiddly corners around whether VIRTUAL_ENV should be cleared for such invocations, in general script venvs work kinda like ephemeral venvs but consistently resolved to the same path so you don't need to try super hard to be consistent, uv will figure it out. ## Future Work For LSP LSP is out of scope for the MVP. The rough idea for how it *should* work is that it will initially start up as it does now and opportunistically kick off `uv workspace metadata --sync (--isolated?)` in the background. If that returns successfully, it's answers will then be respected and integrated in much the same way they are when invoked with `uv check`. This may cause mass invalidations but ty is fast so it's fine right? :) (The idea of this architecture is ty will give okish results early and fast and then as soon as uv is done "snap" into a richer form, in a similar way to how rust-analyzer can "snap to life" when `cargo check` completes.) After the first run it can then setup proper watchers and opportunistically invoke things like `uv workspace metadata --script` or reinvoke `uv workspace metadata --sync` as needed. See the future work for scripts and watchers for details of that. ## Future Work for meow Meow lints will opportunistically be enabled when the UvMetadata is attached to the ProjectMetadata and contains package-to-module maps. Meow and attaching more metadata will be implemented in a followup PR. --- crates/ruff_db/src/system.rs | 15 + crates/ruff_db/src/system/os.rs | 13 + crates/ruff_db/src/system/test.rs | 10 + crates/ruff_linter/src/rule_selector.rs | 1 + crates/ruff_ranged_value/src/lib.rs | 4 + crates/ty/Cargo.toml | 1 + crates/ty/src/lib.rs | 13 +- crates/ty/tests/cli/main.rs | 2 + crates/ty/tests/cli/uv_workspace.rs | 422 +++++++++++++ crates/ty_project/src/metadata.rs | 566 +++++++++++++++--- crates/ty_project/src/metadata/options.rs | 11 + crates/ty_project/src/metadata/uv.rs | 248 ++++++++ crates/ty_project/src/metadata/value.rs | 8 +- .../ty_python_semantic/src/diagnostic/mod.rs | 12 +- crates/ty_python_semantic/src/lint.rs | 3 + .../ty_python_semantic/src/types/context.rs | 3 + crates/ty_server/src/system.rs | 11 + crates/ty_site_packages/src/lib.rs | 22 +- crates/ty_site_packages/src/version.rs | 3 + crates/ty_static/src/env_vars.rs | 13 + 20 files changed, 1284 insertions(+), 97 deletions(-) create mode 100644 crates/ty/tests/cli/uv_workspace.rs create mode 100644 crates/ty_project/src/metadata/uv.rs diff --git a/crates/ruff_db/src/system.rs b/crates/ruff_db/src/system.rs index 4997fdd630..42554e7031 100644 --- a/crates/ruff_db/src/system.rs +++ b/crates/ruff_db/src/system.rs @@ -12,6 +12,7 @@ use ruff_python_ast::PySourceType; use std::error::Error; use std::fmt; use std::fmt::Debug; +use std::process::Output; pub use test::{DbWithTestSystem, DbWithWritableSystem, InMemorySystem, TestSystem}; use walk_directory::WalkDirectoryBuilder; @@ -100,6 +101,20 @@ pub trait System: Debug + Sync + Send { /// Find an executable binary's path by name. fn which(&self, binary_name: &str) -> WhichResult; + /// Runs a command in the given working directory, returning its output. + fn run_command( + &self, + program: &str, + args: &[&str], + current_directory: &SystemPath, + ) -> Result { + let _ = (program, args, current_directory); + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "running commands is not supported by this system", + )) + } + /// Reads the content of the file at `path` into a [`String`]. fn read_to_string(&self, path: &SystemPath) -> Result; diff --git a/crates/ruff_db/src/system/os.rs b/crates/ruff_db/src/system/os.rs index d2f88941ef..f13d778de8 100644 --- a/crates/ruff_db/src/system/os.rs +++ b/crates/ruff_db/src/system/os.rs @@ -15,6 +15,7 @@ use crate::system::{ use filetime::FileTime; use ruff_notebook::{Notebook, NotebookError}; use std::num::NonZeroUsize; +use std::process::{Command, Output}; use std::sync::Arc; use std::{any::Any, path::PathBuf}; @@ -129,6 +130,18 @@ impl System for OsSystem { } } + fn run_command( + &self, + program: &str, + args: &[&str], + current_directory: &SystemPath, + ) -> Result { + Command::new(program) + .args(args) + .current_dir(current_directory.as_std_path()) + .output() + } + fn current_directory(&self) -> &SystemPath { &self.inner.cwd } diff --git a/crates/ruff_db/src/system/test.rs b/crates/ruff_db/src/system/test.rs index 8b28e601d1..09071d7d7f 100644 --- a/crates/ruff_db/src/system/test.rs +++ b/crates/ruff_db/src/system/test.rs @@ -1,6 +1,7 @@ use ruff_notebook::{Notebook, NotebookError}; use rustc_hash::FxHashMap; use std::panic::RefUnwindSafe; +use std::process::Output; use std::sync::{Arc, Mutex}; use crate::Db; @@ -140,6 +141,15 @@ impl System for TestSystem { Err(WhichError::CannotFindBinaryPath) } + fn run_command( + &self, + program: &str, + args: &[&str], + current_directory: &SystemPath, + ) -> Result { + self.system().run_command(program, args, current_directory) + } + fn read_directory<'a>( &'a self, path: &SystemPath, diff --git a/crates/ruff_linter/src/rule_selector.rs b/crates/ruff_linter/src/rule_selector.rs index 70b6303c8d..d5c0295f91 100644 --- a/crates/ruff_linter/src/rule_selector.rs +++ b/crates/ruff_linter/src/rule_selector.rs @@ -106,6 +106,7 @@ impl std::fmt::Display for RuleResolutionError { ValueSource::File(path) => format_args!("`{}`", path.as_path()), ValueSource::Cli => format_args!("the CLI"), ValueSource::Editor => format_args!("the editor configuration"), + ValueSource::UvWorkspace => format_args!("uv workspace metadata"), }; match kind { RuleResolutionErrorKind::Removed => { diff --git a/crates/ruff_ranged_value/src/lib.rs b/crates/ruff_ranged_value/src/lib.rs index 9bd87ac955..7d9b5d64b6 100644 --- a/crates/ruff_ranged_value/src/lib.rs +++ b/crates/ruff_ranged_value/src/lib.rs @@ -29,6 +29,9 @@ pub enum ValueSource { /// or if the value was auto-discovered by the editor /// (e.g., the Python environment) Editor, + + /// The value was provided by `uv workspace metadata`. + UvWorkspace, } impl ValueSource { @@ -37,6 +40,7 @@ impl ValueSource { ValueSource::File(path) => Some(&**path), ValueSource::Cli => None, ValueSource::Editor => None, + ValueSource::UvWorkspace => None, } } diff --git a/crates/ty/Cargo.toml b/crates/ty/Cargo.toml index bac0e76bf9..e9bc06c209 100644 --- a/crates/ty/Cargo.toml +++ b/crates/ty/Cargo.toml @@ -66,6 +66,7 @@ toml = { workspace = true } [features] default = [] +test-uv = [] [lints] workspace = true diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index 4bfcf42d19..2b47acdb00 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -20,7 +20,7 @@ use ruff_db::diagnostic::{ Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, Severity, }; use ruff_db::files::File; -use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; +use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf}; use ruff_db::{STACK_SIZE, max_parallelism}; use ruff_diagnostics::Applicability; use salsa::Database; @@ -156,9 +156,20 @@ fn run_check(args: CheckCommand) -> anyhow::Result { Some(config_file) => { ProjectMetadata::from_config_file(config_file.clone(), &project_path, &system)? } + None if check_paths.iter().any(|path| system.is_file(path)) => { + // `uv check --script` passes a file as its check path. Disable uv workspace metadata + // for scripts until script integration is implemented in a follow-up. + ProjectMetadata::discover_without_uv(&project_path, &system)? + } None => ProjectMetadata::discover(&project_path, &system)?, }; + if watch && project_metadata.has_uv_workspace() { + return Err(anyhow!( + "`--watch` is not supported with uv workspace integration" + )); + } + project_metadata.apply_configuration_files(&system)?; project_metadata.apply_override_options(args.into_options()); diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs index c7f1e0bead..67d5672c38 100644 --- a/crates/ty/tests/cli/main.rs +++ b/crates/ty/tests/cli/main.rs @@ -7,6 +7,7 @@ mod python_environment; mod rule; mod rule_selection; mod scripts; +mod uv_workspace; use anyhow::Context as _; use insta::Settings; @@ -878,6 +879,7 @@ impl CliTest { let mut settings = insta::Settings::clone_current(); settings.add_filter(&tempdir_filter(&project_dir), "/"); + settings.add_filter(r"\bty\.exe\b", "ty"); settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1"); // 0.003s settings.add_filter(r"\d.\d\d\ds", "0.000s"); diff --git a/crates/ty/tests/cli/uv_workspace.rs b/crates/ty/tests/cli/uv_workspace.rs new file mode 100644 index 0000000000..b321e9213e --- /dev/null +++ b/crates/ty/tests/cli/uv_workspace.rs @@ -0,0 +1,422 @@ +//! Integration tests for ty's side of `uv check`. +//! +//! Corresponding uv-side workspace tests live at +//! . + +#[cfg(feature = "test-uv")] +use std::{path::Path, process::Command}; + +use insta_cmd::assert_cmd_snapshot; + +use crate::CliTest; + +fn workspace_case() -> anyhow::Result { + CliTest::with_files([ + ( + "pyproject.toml", + r#" +[tool.uv.workspace] +members = ["packages/*"] +"#, + ), + ( + "packages/member/pyproject.toml", + r#" +[project] +name = "member" +version = "0.1.0" +requires-python = ">=3.8" +"#, + ), + ( + "packages/member/member.py", + "value: int = 'selected-member'", + ), + ( + "packages/sibling/pyproject.toml", + r#" +[project] +name = "sibling" +version = "0.1.0" +requires-python = ">=3.8" +"#, + ), + ( + "packages/sibling/sibling.py", + "value: int = 'unselected-sibling'", + ), + ]) +} + +#[cfg(feature = "test-uv")] +fn command_with_uv(case: &CliTest, virtual_env: Option<&Path>) -> anyhow::Result { + let mut sync = Command::new("uv"); + sync.current_dir(case.root()) + .args(["workspace", "metadata", "--sync"]) + .env("UV_CACHE_DIR", case.root().join("cache")) + .env("UV_OFFLINE", "1") + .env("UV_PYTHON_DOWNLOADS", "never"); + if let Some(virtual_env) = virtual_env { + sync.arg("--active").env("VIRTUAL_ENV", virtual_env); + } + anyhow::ensure!( + sync.output()?.status.success(), + "failed to prepare uv workspace" + ); + + let mut command = case.command(); + command + .env("TY_UV", "1") + .env("UV", "uv") + .env("UV_CACHE_DIR", case.root().join("cache")) + .env("UV_OFFLINE", "1") + .env("UV_PYTHON_DOWNLOADS", "never") + .env("TY_OUTPUT_FORMAT", "concise") + .env("PATH", std::env::var_os("PATH").unwrap_or_default()); + if let Some(virtual_env) = virtual_env { + command.env("VIRTUAL_ENV", virtual_env); + } + + Ok(command) +} + +/// The workspace root provides first-party imports without expanding analysis to unselected +/// sibling members. +#[cfg(feature = "test-uv")] +#[test] +fn uses_uv_workspace_root_without_checking_siblings() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file("shared.py", "value: int = 'unselected-workspace-root'")?; + case.write_file( + "packages/member/member.py", + "import shared\nvalue: int = 'selected-member'", + )?; + + let mut command = command_with_uv(&case, None)?; + command + .current_dir(case.root().join("packages/member")) + .arg("."); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + member.py:2:14: error[invalid-assignment] Object of type `Literal["selected-member"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + assert!(case.root().join(".venv").is_dir()); + + Ok(()) +} + +/// An explicit file is treated as a script, so workspace discovery stays disabled even when +/// `TY_UV` is set. +#[cfg(feature = "test-uv")] +#[test] +fn explicit_file_path_disables_uv_workspace_discovery() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file("shared.py", "value: int = 'unselected-workspace-root'")?; + case.write_file( + "packages/member/member.py", + "import shared\nvalue: int = 'selected-script'", + )?; + + let mut command = command_with_uv(&case, None)?; + command + .current_dir(case.root().join("packages/member")) + .arg("member.py"); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + member.py:1:8: error[unresolved-import] Cannot resolve imported module `shared` + member.py:2:14: error[invalid-assignment] Object of type `Literal["selected-script"]` is not assignable to `int` + Found 2 diagnostics + + ----- stderr ----- + "#); + + Ok(()) +} + +/// An explicitly selected member inherits ty rule configuration from the uv workspace root. +#[cfg(feature = "test-uv")] +#[test] +fn explicit_workspace_member_directory_uses_workspace_configuration() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file( + "pyproject.toml", + r#" +[tool.uv.workspace] +members = ["packages/*"] + +[tool.ty.rules] +invalid-assignment = "ignore" +"#, + )?; + let mut command = command_with_uv(&case, None)?; + command.arg("packages/member"); + + assert_cmd_snapshot!(command, @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + "); + + Ok(()) +} + +/// Workspace configuration still applies when the selected member lives outside the workspace +/// root's directory tree. +#[cfg(feature = "test-uv")] +#[test] +fn external_workspace_member_uses_workspace_configuration() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" +[tool.uv.workspace] +members = ["../external-package"] + +[tool.ty.rules] +invalid-assignment = "ignore" +"#, + ), + ( + "../external-package/pyproject.toml", + r#" +[project] +name = "external-package" +version = "0.1.0" +requires-python = ">=3.8" +"#, + ), + ( + "../external-package/member.py", + "value: int = 'selected-external-member'", + ), + ])?; + + let mut command = command_with_uv(&case, None)?; + command + .args(["--project", "../external-package", "../external-package"]) + .env("UV_PROJECT", case.root()); + + assert_cmd_snapshot!(command, @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + "); + + Ok(()) +} + +/// Excludes passed by `uv check` prevent an unselected nested member from being analyzed. +#[cfg(feature = "test-uv")] +#[test] +fn selected_workspace_member_excludes_nested_member() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file( + "pyproject.toml", + r#" +[tool.uv.workspace] +members = ["packages/*", "packages/member/nested"] +"#, + )?; + case.write_file( + "packages/member/nested/pyproject.toml", + r#" +[project] +name = "nested" +version = "0.1.0" +requires-python = ">=3.8" +"#, + )?; + case.write_file( + "packages/member/nested/nested.py", + "value: int = 'unselected-nested-member'", + )?; + + let mut command = command_with_uv(&case, None)?; + command.args(["--exclude", "packages/member/nested", "packages/member"]); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + packages/member/member.py:1:14: error[invalid-assignment] Object of type `Literal["selected-member"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + + Ok(()) +} + +/// Metadata discovery preserves uv's active isolated environment instead of using an invalid +/// Python environment configured in the workspace. +#[cfg(feature = "test-uv")] +#[test] +fn forwards_active_environment_to_uv() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file( + "pyproject.toml", + r#" +[tool.uv.workspace] +members = ["packages/*"] + +[tool.ty.environment] +python = "missing-configured-environment" +"#, + )?; + let environment = case.root().join("isolated"); + let mut command = command_with_uv(&case, Some(&environment))?; + command + .current_dir(case.root().join("packages/member")) + .arg(".") + .env_remove("UV_PROJECT_ENVIRONMENT"); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + member.py:1:14: error[invalid-assignment] Object of type `Literal["selected-member"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + + assert!(environment.is_dir()); + assert!(!case.root().join(".venv").exists()); + + Ok(()) +} + +/// Merely exposing the uv executable must not change ordinary ty project discovery without +/// `TY_UV`. +#[test] +fn uv_workspace_discovery_is_opt_in() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file("shared.py", "value: int = 'unselected-workspace-root'")?; + case.write_file( + "packages/member/member.py", + "import shared\nvalue: int = 'selected-member'", + )?; + + let mut command = case.command(); + command + .current_dir(case.root().join("packages/member")) + .env("UV", "uv") + .env("TY_OUTPUT_FORMAT", "concise") + .env_remove("TY_UV"); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + member.py:1:8: error[unresolved-import] Cannot resolve imported module `shared` + member.py:2:14: error[invalid-assignment] Object of type `Literal["selected-member"]` is not assignable to `int` + Found 2 diagnostics + + ----- stderr ----- + "#); + + Ok(()) +} + +/// Failures to invoke uv are visible by default instead of silently disabling integration. +#[test] +fn warns_when_uv_workspace_metadata_cannot_be_loaded() -> anyhow::Result<()> { + let case = workspace_case()?.with_filter( + "program not found", + "No such file or directory (os error 2)", + ); + case.write_file("packages/member/member.py", "value: int = 1")?; + + let mut command = case.command(); + command + .current_dir(case.root().join("packages/member")) + .arg(".") + .env("TY_UV", "1") + .env("UV", "missing-uv-executable") + .env("TY_OUTPUT_FORMAT", "concise"); + + assert_cmd_snapshot!(command, @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN Failed to invoke `uv workspace metadata`: No such file or directory (os error 2) + "); + + Ok(()) +} + +/// Workspace discovery can find uv on `PATH` when the `UV` executable override is absent. +#[cfg(feature = "test-uv")] +#[test] +fn finds_uv_on_path_without_uv_environment_variable() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file("shared.py", "value: int = 'unselected-workspace-root'")?; + case.write_file( + "packages/member/member.py", + "import shared\nvalue: int = 'selected-member'", + )?; + + let mut command = command_with_uv(&case, None)?; + command + .current_dir(case.root().join("packages/member")) + .arg(".") + .env_remove("UV"); + + assert_cmd_snapshot!(command, @r#" + success: false + exit_code: 1 + ----- stdout ----- + member.py:2:14: error[invalid-assignment] Object of type `Literal["selected-member"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + + Ok(()) +} + +/// Version-sensitive diagnostics attribute their assumed Python version to workspace metadata, +/// not to a command-line override. +#[cfg(feature = "test-uv")] +#[test] +fn reports_uv_workspace_python_version_source() -> anyhow::Result<()> { + let case = workspace_case()?; + case.write_file("packages/member/member.py", "frozendict")?; + + for output_format in ["full", "concise"] { + let mut command = command_with_uv(&case, None)?; + command + .current_dir(case.root().join("packages/member")) + .arg(".") + .arg("--output-format") + .arg(output_format); + + let output = command.output()?; + let stdout = String::from_utf8(output.stdout)?; + assert!(!output.status.success()); + assert!(!stdout.contains("specified on the command line")); + if output_format == "full" { + assert!(stdout.contains("provided by uv workspace metadata")); + } + } + + Ok(()) +} diff --git a/crates/ty_project/src/metadata.rs b/crates/ty_project/src/metadata.rs index ea4774bb7c..b87c086ff4 100644 --- a/crates/ty_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -8,11 +8,15 @@ use std::sync::Arc; use thiserror::Error; use ty_combine::Combine; use ty_python_core::program::{FallibleStrategy, MisconfigurationStrategy, ProgramSettings}; +use ty_static::EnvVars; use crate::Db; -use crate::metadata::options::{OptionDiagnostic, ProgramSettingsDiagnostic, ToSettingsError}; +use crate::metadata::options::{ + EnvironmentOptions, OptionDiagnostic, ProgramSettingsDiagnostic, ToSettingsError, +}; use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError}; use crate::metadata::settings::Settings; +use crate::metadata::value::RelativePathBuf; pub use options::Options; use options::TyTomlError; @@ -22,6 +26,7 @@ pub mod pyproject; pub mod python_version; mod script; pub mod settings; +mod uv; pub mod value; #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] @@ -41,10 +46,16 @@ pub struct ProjectMetadata { /// the file specified by [`Self::config_file_override`] if it is `Some` (e.g. when using `--config-file `). pub(super) options: Options, + /// The Python version and interpreter path derived from uv workspace metadata. + /// + /// These options have higher precedence than project and user-level configuration. + #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] + uv_workspace_options: Option>, + /// The user-level configuration path and its options. /// - /// Its options have lower precedence than [`Self::override_options`] and [`Self::options`], - /// but higher precedence than [`Self::fallback_options`]. + /// Its options have lower precedence than [`Self::override_options`], [`Self::options`], and + /// [`Self::uv_workspace_options`], but higher precedence than [`Self::fallback_options`]. #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] user_configuration: Option>, @@ -58,6 +69,9 @@ pub struct ProjectMetadata { /// instead of from the project's `pyproject.toml` or `ty.toml` file. #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] config_file_override: Option, + + #[cfg_attr(test, serde(skip))] + uv_workspace: Option, } impl ProjectMetadata { @@ -67,10 +81,12 @@ impl ProjectMetadata { name: ProjectName::new(name), root, options: Options::default(), + uv_workspace_options: None, override_options: None, user_configuration: None, fallback_options: None, config_file_override: None, + uv_workspace: None, } } @@ -94,10 +110,12 @@ impl ProjectMetadata { name: ProjectName::new(root.file_name().unwrap_or("root")), root: root.to_path_buf(), options, + uv_workspace_options: None, override_options: None, user_configuration: None, fallback_options: None, config_file_override: Some(path), + uv_workspace: None, }) } @@ -152,24 +170,56 @@ impl ProjectMetadata { name, root, options, + uv_workspace_options: None, override_options: None, user_configuration: None, fallback_options: None, config_file_override: None, + uv_workspace: None, }) } /// Discovers the closest project at `path` and returns its metadata. /// /// The algorithm traverses upwards in the `path`'s ancestor chain and uses the following precedence - /// the resolve the project's root. + /// to resolve the project's root. /// /// 1. The closest `pyproject.toml` with a `tool.ty` section or `ty.toml`. + /// 1. The uv workspace root, if uv integration is enabled. /// 1. The closest `pyproject.toml`. /// 1. Fallback to use `path` as the root and use the default settings. pub fn discover( path: &SystemPath, system: &dyn System, + ) -> Result { + let uv_workspace = if matches!(system.env_var(EnvVars::TY_UV).as_deref(), Ok("1" | "true")) + { + match uv::UvWorkspace::discover(path, system) { + Ok(workspace) => Some(workspace), + Err(error) => { + tracing::warn!("{error}"); + None + } + } + } else { + None + }; + + Self::discover_with_uv_workspace(path, system, uv_workspace) + } + + /// Discovers the closest project without considering uv workspace metadata. + pub fn discover_without_uv( + path: &SystemPath, + system: &dyn System, + ) -> Result { + Self::discover_with_uv_workspace(path, system, None) + } + + fn discover_with_uv_workspace( + path: &SystemPath, + system: &dyn System, + uv_workspace: Option, ) -> Result { tracing::debug!("Searching for a project in '{path}'"); @@ -178,99 +228,55 @@ impl ProjectMetadata { } let mut closest_project: Option = None; + let mut uv_project: Option = None; + let uv_workspace_root = uv_workspace.as_ref().map(uv::UvWorkspace::root); for project_root in path.ancestors() { - let pyproject_path = project_root.join("pyproject.toml"); - - let pyproject = if let Ok(pyproject_str) = system.read_to_string(&pyproject_path) { - match PyProject::from_toml_str( - &pyproject_str, - ValueSource::File(Arc::new(pyproject_path.clone())), - ) { - Ok(pyproject) => Some(pyproject), - Err(error) => { - return Err(ProjectMetadataError::InvalidPyProject { - path: pyproject_path, - source: Box::new(error), - }); - } + let is_uv_workspace_root = uv_workspace_root == Some(project_root); + let Some((metadata, has_ty_configuration)) = Self::discover_in(project_root, system)? + else { + if is_uv_workspace_root { + uv_project = Some(Self::new( + project_root.file_name().unwrap_or("root"), + project_root.to_path_buf(), + )); } - } else { - None + continue; }; - // 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( - &ty_str, - ValueSource::File(Arc::new(ty_toml_path.clone())), - ) { - Ok(options) => options, - Err(error) => { - return Err(ProjectMetadataError::InvalidTyToml { - path: ty_toml_path, - source: Box::new(error), - }); - } - }; - - if pyproject - .as_ref() - .is_some_and(|project| project.ty().is_some()) - { - // TODO: Consider using a diagnostic here - tracing::warn!( - "Ignoring the `tool.ty` section in `{pyproject_path}` because `{ty_toml_path}` takes precedence." - ); - } - + if has_ty_configuration { tracing::debug!("Found project at '{}'", project_root); - - let metadata = ProjectMetadata::from_options( - options, - project_root.to_path_buf(), - pyproject - .as_ref() - .and_then(|pyproject| pyproject.project.as_ref()), - &FallibleStrategy, - ) - .map_err(|err| { - ProjectMetadataError::InvalidRequiresPythonConstraint { - source: err, - path: pyproject_path, - } - })?; - - return Ok(metadata); + return Ok(metadata.with_uv_workspace(uv_workspace)); } - if let Some(pyproject) = pyproject { - let has_ty_section = pyproject.ty().is_some(); - let metadata = - ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf()) - .map_err( - |err| ProjectMetadataError::InvalidRequiresPythonConstraint { - source: err, - path: pyproject_path, - }, - )?; - - if has_ty_section { - tracing::debug!("Found project at '{}'", project_root); - - return Ok(metadata); - } - - // Not a project itself, keep looking for an enclosing project. - if closest_project.is_none() { - closest_project = Some(metadata); - } + if is_uv_workspace_root { + uv_project = Some(metadata); + } else if closest_project.is_none() { + closest_project = Some(metadata); } } - // No project found, but maybe a pyproject.toml was found. - let metadata = if let Some(closest_project) = closest_project { + // Workspace members can live outside the workspace directory, so their ancestor chain may + // never include the workspace root. + if let Some(workspace_root) = uv_workspace_root + && !path.starts_with(workspace_root) + { + let metadata = Self::discover_in(workspace_root, system)? + .map(|(metadata, _)| metadata) + .unwrap_or_else(|| { + Self::new( + workspace_root.file_name().unwrap_or("root"), + workspace_root.to_path_buf(), + ) + }); + uv_project = Some(metadata); + } + + let metadata = if let Some(uv_project) = uv_project { + tracing::debug!("Using uv workspace at '{}'", uv_project.root()); + + uv_project + } else if let Some(closest_project) = closest_project { tracing::debug!( "Project without `tool.ty` section: '{}'", closest_project.root() @@ -286,7 +292,96 @@ impl ProjectMetadata { Self::new(path.file_name().unwrap_or("root"), path.to_path_buf()) }; - Ok(metadata) + Ok(metadata.with_uv_workspace(uv_workspace)) + } + + fn discover_in( + project_root: &SystemPath, + system: &dyn System, + ) -> Result, ProjectMetadataError> { + let pyproject_path = project_root.join("pyproject.toml"); + + let pyproject = if let Ok(pyproject_str) = system.read_to_string(&pyproject_path) { + match PyProject::from_toml_str( + &pyproject_str, + ValueSource::File(Arc::new(pyproject_path.clone())), + ) { + Ok(pyproject) => Some(pyproject), + Err(error) => { + return Err(ProjectMetadataError::InvalidPyProject { + path: pyproject_path, + source: Box::new(error), + }); + } + } + } else { + None + }; + + // 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( + &ty_str, + ValueSource::File(Arc::new(ty_toml_path.clone())), + ) { + Ok(options) => options, + Err(error) => { + return Err(ProjectMetadataError::InvalidTyToml { + path: ty_toml_path, + source: Box::new(error), + }); + } + }; + + if pyproject + .as_ref() + .is_some_and(|project| project.ty().is_some()) + { + // TODO: Consider using a diagnostic here + tracing::warn!( + "Ignoring the `tool.ty` section in `{pyproject_path}` because `{ty_toml_path}` takes precedence." + ); + } + + let metadata = ProjectMetadata::from_options( + options, + project_root.to_path_buf(), + pyproject + .as_ref() + .and_then(|pyproject| pyproject.project.as_ref()), + &FallibleStrategy, + ) + .map_err(|source| { + ProjectMetadataError::InvalidRequiresPythonConstraint { + source, + path: pyproject_path, + } + })?; + + return Ok(Some((metadata, true))); + } + + let Some(pyproject) = pyproject else { + return Ok(None); + }; + + let has_ty_configuration = pyproject.ty().is_some(); + let metadata = ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf()) + .map_err( + |source| ProjectMetadataError::InvalidRequiresPythonConstraint { + source, + path: pyproject_path, + }, + )?; + + Ok(Some((metadata, has_ty_configuration))) + } + + #[must_use] + fn with_uv_workspace(mut self, uv_workspace: Option) -> Self { + self.uv_workspace = uv_workspace; + self } /// Rediscovers the project, while preserving applied options. @@ -357,10 +452,14 @@ impl ProjectMetadata { } } + pub fn has_uv_workspace(&self) -> bool { + self.uv_workspace.is_some() + } + /// Applies lower-precedence options to this project. /// /// Options applied later take precedence over options applied earlier, but all fallback options - /// have lower precedence than the raw and user-level options. + /// have lower precedence than the raw, uv workspace, and user-level options. pub fn apply_fallback_options(&mut self, options: Options) { if let Some(existing) = self.fallback_options.as_mut() { let previous = std::mem::replace(existing.as_mut(), options); @@ -372,7 +471,7 @@ impl ProjectMetadata { /// Returns the project's option layers from highest to lowest precedence. /// - /// `options` is used as the raw base layer between the override and user-level options. + /// `options` is used as the raw base layer between the uv workspace and user-level options. /// Layers can be merged by passing them to [`Options::combine_with`] in iterator order: /// /// ```ignore @@ -388,6 +487,7 @@ impl ProjectMetadata { self.override_options .as_deref() .into_iter() + .chain(self.uv_workspace_options.as_deref()) .chain(std::iter::once(options)) .chain( self.user_configuration @@ -401,6 +501,7 @@ impl ProjectMetadata { /// /// This includes: /// + /// * The uv workspace configuration /// * The user-level configuration pub fn apply_configuration_files( &mut self, @@ -416,6 +517,19 @@ impl ProjectMetadata { self.user_configuration = Some(Box::new((user.path().to_owned(), user.into_options()))); } + self.uv_workspace_options = self.uv_workspace.as_ref().map(|uv_workspace| { + Box::new(Options { + environment: Some(EnvironmentOptions { + python_version: uv_workspace.python_version().cloned(), + python: uv_workspace + .environment() + .map(|path| RelativePathBuf::new(path, ValueSource::UvWorkspace)), + ..EnvironmentOptions::default() + }), + ..Options::default() + }) + }); + Ok(()) } @@ -523,7 +637,10 @@ mod tests { use insta::assert_ron_snapshot; use ruff_db::system::{SystemPathBuf, TestSystem}; use ruff_python_ast::PythonVersion; + use ruff_ranged_value::ValueSource; + use ty_static::EnvVars; + use crate::metadata::{Options, uv::UvWorkspace, value::RelativePathBuf}; use crate::{ProjectMetadata, ProjectMetadataError}; #[test] @@ -778,6 +895,266 @@ unclosed table, expected `]` Ok(()) } + #[test] + fn uv_workspace_precedes_plain_member_pyproject() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from("/app"); + let member = root.join("packages/member"); + + system.memory_file_system().write_files_all([ + (root.join("pyproject.toml"), "[tool.uv.workspace]"), + ( + member.join("pyproject.toml"), + r#" + [project] + name = "member" + "#, + ), + ])?; + + let uv_workspace = uv_workspace(&root, &system)?; + let project = + ProjectMetadata::discover_with_uv_workspace(&member, &system, Some(uv_workspace))?; + + assert_eq!(project.root(), &*root); + + Ok(()) + } + + #[test] + fn external_uv_workspace_precedes_plain_member_pyproject() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from("/app/workspace"); + let member = SystemPathBuf::from("/app/external-package"); + + system.memory_file_system().write_files_all([ + ( + root.join("pyproject.toml"), + r#" + [tool.uv.workspace] + members = ["../external-package"] + + [tool.ty.rules] + invalid-assignment = "ignore" + "#, + ), + ( + member.join("pyproject.toml"), + r#" + [project] + name = "external-package" + "#, + ), + ])?; + + let uv_workspace = uv_workspace(&root, &system)?; + let project = + ProjectMetadata::discover_with_uv_workspace(&member, &system, Some(uv_workspace))?; + + assert_eq!(project.root(), &*root); + + Ok(()) + } + + #[test] + fn uv_workspace_discovery_is_system_independent() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from("/app"); + let member = root.join("packages/member"); + + system.set_env_var(EnvVars::TY_UV, "1"); + system.set_env_var(EnvVars::UV, "uv"); + system + .memory_file_system() + .write_file_all(member.join("pyproject.toml"), "[project]\nname = 'member'")?; + + let project = ProjectMetadata::discover(&member, &system)?; + + assert_eq!(project.root(), &*member); + + Ok(()) + } + + #[test] + fn member_ty_configuration_selects_project_root() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from("/app"); + let member = root.join("packages/member"); + + system.memory_file_system().write_files_all([ + (root.join("uv.toml"), ""), + ( + member.join("pyproject.toml"), + r#" + [project] + name = "member" + + [tool.ty.environment] + python-version = "3.10" + "#, + ), + ])?; + + let uv_workspace = uv_workspace(&root, &system)?; + let mut project = + ProjectMetadata::discover_with_uv_workspace(&member, &system, Some(uv_workspace))?; + project.apply_configuration_files(&system)?; + + assert_eq!(project.root(), &*member); + assert_eq!( + project + .to_merged_options() + .options() + .environment + .as_ref() + .and_then(|environment| environment.python_version.as_deref()) + .copied() + .map(PythonVersion::from), + Some(PythonVersion::PY310) + ); + + Ok(()) + } + + #[test] + fn outer_ty_configuration_precedes_uv_workspace() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from("/app"); + let workspace = root.join("workspace"); + let member = workspace.join("packages/member"); + + system.memory_file_system().write_files_all([ + ( + root.join("ty.toml"), + r#" + [environment] + python-version = "3.10" + "#, + ), + (workspace.join("pyproject.toml"), "[tool.uv.workspace]"), + ( + member.join("pyproject.toml"), + r#" + [project] + name = "member" + "#, + ), + ])?; + + let uv_workspace = uv_workspace(&workspace, &system)?; + let project = + ProjectMetadata::discover_with_uv_workspace(&member, &system, Some(uv_workspace))?; + + assert_eq!(project.root(), &*root); + + Ok(()) + } + + #[test] + fn applies_uv_workspace_environment() -> anyhow::Result<()> { + let system = TestSystem::default(); + let root = SystemPathBuf::from("/app"); + let member = root.join("packages/member"); + let environment = root.join("uv-venv"); + + system.memory_file_system().write_files_all([ + ( + root.join("pyproject.toml"), + r#" + [tool.uv.workspace] + + [tool.ty.environment] + python = "/project-venv" + python-version = "3.10" + "#, + ), + (member.join("pyproject.toml"), "[project]\nname = 'member'"), + (environment.join("marker"), ""), + ])?; + + let metadata = serde_json::json!({ + "workspace_root": root, + "environment": { + "root": environment, + "python": { + "version": "3.13.5", + }, + }, + }); + let uv_workspace = UvWorkspace::from_metadata(metadata.to_string().as_bytes(), &system)?; + let mut project = + ProjectMetadata::discover_with_uv_workspace(&member, &system, Some(uv_workspace))?; + project.apply_fallback_options(Options::from_toml_str( + r#" + [environment] + python = "/editor-venv" + python-version = "3.10" + "#, + ValueSource::Editor, + )?); + project.apply_configuration_files(&system)?; + + let merged_options = project.to_merged_options(); + let project_environment = merged_options.options().environment.as_ref(); + assert_eq!( + project_environment + .and_then(|environment| environment.python_version.as_deref()) + .copied() + .map(PythonVersion::from), + Some(PythonVersion::PY313) + ); + assert_eq!( + project_environment + .and_then(|environment| environment.python.as_ref()) + .map(RelativePathBuf::path), + Some(environment.as_path()) + ); + assert!(matches!( + project_environment + .and_then(|environment| environment.python.as_ref()) + .map(RelativePathBuf::source), + Some(ValueSource::UvWorkspace) + )); + assert!(matches!( + project_environment + .and_then(|environment| environment.python_version.as_ref()) + .map(ruff_ranged_value::RangedValue::source), + Some(ValueSource::UvWorkspace) + )); + + let user_config_directory = root.join("config"); + system + .in_memory() + .set_user_configuration_directory(Some(user_config_directory.clone())); + system.memory_file_system().write_file_all( + user_config_directory.join("ty/ty.toml"), + r#" + [environment] + python = "/user-venv" + python-version = "3.12" + "#, + )?; + project.apply_configuration_files(&system)?; + + let merged_options = project.to_merged_options(); + let project_environment = merged_options.options().environment.as_ref(); + assert_eq!( + project_environment + .and_then(|environment| environment.python_version.as_deref()) + .copied() + .map(PythonVersion::from), + Some(PythonVersion::PY313) + ); + assert_eq!( + project_environment + .and_then(|environment| environment.python.as_ref()) + .map(|python| python.path().as_str()), + Some(environment.as_str()) + ); + + Ok(()) + } + #[test] fn nested_projects_with_outer_ty_section() -> anyhow::Result<()> { let system = TestSystem::default(); @@ -1237,6 +1614,17 @@ unclosed table, expected `]` assert_eq!(format!("{error:#}").replace('\\', "/"), message); } + fn uv_workspace(root: &SystemPathBuf, system: &TestSystem) -> anyhow::Result { + let metadata = serde_json::json!({ + "workspace_root": root, + }); + + Ok(UvWorkspace::from_metadata( + metadata.to_string().as_bytes(), + system, + )?) + } + fn with_escaped_paths(f: impl FnOnce() -> R) -> R { let mut settings = insta::Settings::clone_current(); settings.add_dynamic_redaction(".root", |content, _path| { diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index b78209494c..8c586ea8ce 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -197,6 +197,7 @@ impl Options { SysPrefixPathOrigin::ConfigFileSetting(path.clone(), python_path.range()) } ValueSource::Editor => SysPrefixPathOrigin::Editor, + ValueSource::UvWorkspace => SysPrefixPathOrigin::UvWorkspace, }; PythonEnvironment::new(python_path.absolute(project_root, system), origin, system) @@ -565,6 +566,7 @@ fn python_version_from_config( PythonVersionFileSource::new(path.clone(), ranged_version.range()), ), ValueSource::Editor => PythonVersionSource::Editor, + ValueSource::UvWorkspace => PythonVersionSource::UvWorkspace, }, } } @@ -686,6 +688,10 @@ fn unsupported_inferred_python_version_diagnostic( SubDiagnosticSeverity::Info, "The version was inferred from your editor.", )), + PythonVersionSource::UvWorkspace => diagnostic.sub(SubDiagnostic::new( + SubDiagnosticSeverity::Info, + "The version was provided by uv workspace metadata.", + )), PythonVersionSource::Default => diagnostic.sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, "ty fell back to its default Python version.", @@ -1101,6 +1107,7 @@ impl Rules { ValueSource::File(_) => LintSource::File, ValueSource::Cli => LintSource::Cli, ValueSource::Editor => LintSource::Editor, + ValueSource::UvWorkspace => LintSource::UvWorkspace, }; let mut set_lint_level = |lint| { @@ -2268,6 +2275,10 @@ impl OptionDiagnostic { SubDiagnosticSeverity::Info, "The {value_label} was specified in the editor settings.", )), + ValueSource::UvWorkspace => self.sub(SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format!("The {value_label} was provided by uv workspace metadata."), + )), } } diff --git a/crates/ty_project/src/metadata/uv.rs b/crates/ty_project/src/metadata/uv.rs new file mode 100644 index 0000000000..1d46104be7 --- /dev/null +++ b/crates/ty_project/src/metadata/uv.rs @@ -0,0 +1,248 @@ +use std::path::PathBuf; + +use pep440_rs::Version; +use ruff_db::system::{System, SystemPath, SystemPathBuf}; +use ruff_ranged_value::{RangedValue, ValueSource}; +use serde::Deserialize; +use thiserror::Error; +use ty_static::EnvVars; + +use super::python_version::SupportedPythonVersion; + +#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] +pub(super) struct UvWorkspace { + root: SystemPathBuf, + environment: Option, + python_version: Option>, +} + +impl UvWorkspace { + pub(super) fn discover( + path: &SystemPath, + system: &dyn System, + ) -> Result { + let uv = system + .env_var(EnvVars::UV) + .unwrap_or_else(|_| "uv".to_string()); + + // `uv check` has already selected and synchronized the environment. Keep this query + // read-only so package selection and `--isolated` aren't overwritten by a second sync. + let output = system + .run_command( + &uv, + &["workspace", "metadata", "--frozen", "--active"], + path, + ) + .map_err(UvWorkspaceError::Invocation)?; + + if !output.status.success() { + return Err(UvWorkspaceError::CommandFailed { + status: output.status, + stderr: String::from_utf8_lossy(&output.stderr).into_owned(), + }); + } + + Self::from_metadata(&output.stdout, system) + } + + pub(super) fn from_metadata( + metadata: &[u8], + system: &dyn System, + ) -> Result { + let metadata = serde_json::from_slice::(metadata) + .map_err(UvWorkspaceError::InvalidMetadata)?; + + let root = existing_directory(metadata.workspace_root, "workspace root", system)?; + + let (environment, python_version) = match metadata.environment { + Some(environment) => ( + Some(existing_directory( + environment.root, + "environment root", + system, + )?), + Some(resolve_python_version(&environment.python.version)?), + ), + None => (None, None), + }; + + Ok(Self { + root, + environment, + python_version, + }) + } + + pub(super) fn root(&self) -> &SystemPath { + &self.root + } + + pub(super) fn environment(&self) -> Option<&SystemPath> { + self.environment.as_deref() + } + + pub(super) fn python_version(&self) -> Option<&RangedValue> { + self.python_version.as_ref() + } +} + +fn resolve_python_version( + version: &Version, +) -> Result, UvWorkspaceError> { + let [major, minor, ..] = version.release() else { + return Err(UvWorkspaceError::InvalidPythonVersion(version.clone())); + }; + let version = format!("{major}.{minor}") + .parse::() + .map_err(|_| UvWorkspaceError::InvalidPythonVersion(version.clone()))?; + + Ok(RangedValue::new(version, ValueSource::UvWorkspace)) +} + +fn existing_directory( + path: PathBuf, + description: &'static str, + system: &dyn System, +) -> Result { + let path = match SystemPathBuf::from_path_buf(path) { + Ok(path) => path, + Err(path) => return Err(UvWorkspaceError::NonUnicodePath { description, path }), + }; + + if !system.is_directory(&path) { + return Err(UvWorkspaceError::MissingDirectory { description, path }); + } + + Ok(path) +} + +#[derive(Debug, Error)] +pub(super) enum UvWorkspaceError { + #[error("Failed to invoke `uv workspace metadata`: {0}")] + Invocation(#[source] std::io::Error), + + #[error("`uv workspace metadata` failed with status {status}: {stderr}")] + CommandFailed { + status: std::process::ExitStatus, + stderr: String, + }, + + #[error("invalid `uv workspace metadata` JSON: {0}")] + InvalidMetadata(serde_json::Error), + + #[error("unsupported Python version `{0}` returned by `uv workspace metadata`")] + InvalidPythonVersion(Version), + + #[error("non-Unicode {description} returned by `uv workspace metadata`: `{path}`", path = path.display())] + NonUnicodePath { + description: &'static str, + path: PathBuf, + }, + + #[error("missing {description} returned by `uv workspace metadata`: `{path}`")] + MissingDirectory { + description: &'static str, + path: SystemPathBuf, + }, +} + +#[derive(Deserialize)] +struct WorkspaceMetadata { + workspace_root: PathBuf, + environment: Option, +} + +#[derive(Deserialize)] +struct WorkspaceEnvironment { + root: PathBuf, + python: WorkspacePython, +} + +#[derive(Deserialize)] +struct WorkspacePython { + version: Version, +} + +#[cfg(test)] +mod tests { + use ruff_db::system::{SystemPath, TestSystem}; + + use super::{UvWorkspace, UvWorkspaceError}; + + #[test] + fn rejects_invalid_metadata() { + let system = TestSystem::default(); + + assert!(matches!( + UvWorkspace::from_metadata(b"{", &system), + Err(UvWorkspaceError::InvalidMetadata(_)) + )); + } + + #[test] + fn environment_can_be_omitted() -> anyhow::Result<()> { + let system = TestSystem::default(); + system + .memory_file_system() + .write_file_all("/app/pyproject.toml", "[tool.uv.workspace]")?; + let metadata = br#"{ + "workspace_root": "/app" + }"#; + + let workspace = UvWorkspace::from_metadata(metadata, &system)?; + + assert!(workspace.environment().is_none()); + assert!(workspace.python_version().is_none()); + + Ok(()) + } + + #[test] + fn uses_environment_python_version() -> anyhow::Result<()> { + let system = TestSystem::default(); + system.memory_file_system().write_files_all([ + ("/app/pyproject.toml", "[tool.uv.workspace]"), + ("/env/marker", ""), + ])?; + let metadata = br#"{ + "workspace_root": "/app", + "environment": { + "root": "/env", + "python": { "version": "3.13.5" } + } + }"#; + + let workspace = UvWorkspace::from_metadata(metadata, &system)?; + + assert_eq!(workspace.environment(), Some(SystemPath::new("/env"))); + assert_eq!( + workspace.python_version().map(ToString::to_string), + Some("3.13".to_string()) + ); + + Ok(()) + } + + #[test] + fn rejects_unsupported_environment_python_version() -> anyhow::Result<()> { + let system = TestSystem::default(); + system.memory_file_system().write_files_all([ + ("/app/pyproject.toml", "[tool.uv.workspace]"), + ("/env/marker", ""), + ])?; + let metadata = br#"{ + "workspace_root": "/app", + "environment": { + "root": "/env", + "python": { "version": "3.16.0" } + } + }"#; + + assert!(matches!( + UvWorkspace::from_metadata(metadata, &system), + Err(UvWorkspaceError::InvalidPythonVersion(_)) + )); + + Ok(()) + } +} diff --git a/crates/ty_project/src/metadata/value.rs b/crates/ty_project/src/metadata/value.rs index 29dc903302..d118dfde7e 100644 --- a/crates/ty_project/src/metadata/value.rs +++ b/crates/ty_project/src/metadata/value.rs @@ -76,7 +76,9 @@ impl RelativePathBuf { pub fn absolute(&self, project_root: &SystemPath, system: &dyn System) -> SystemPathBuf { let relative_to = match self.0.source() { ValueSource::File(_) => project_root, - ValueSource::Cli | ValueSource::Editor => system.current_directory(), + ValueSource::Cli | ValueSource::Editor | ValueSource::UvWorkspace => { + system.current_directory() + } }; // Expand tildes and environment variables in the path (e.g. `~/.cache/foo`). @@ -146,7 +148,9 @@ impl RelativeGlobPattern { ) -> Result { let relative_to = match self.0.source() { ValueSource::File(_) => project_root, - ValueSource::Cli | ValueSource::Editor => system.current_directory(), + ValueSource::Cli | ValueSource::Editor | ValueSource::UvWorkspace => { + system.current_directory() + } }; let pattern = PortableGlobPattern::parse(&self.0, kind)?; diff --git a/crates/ty_python_semantic/src/diagnostic/mod.rs b/crates/ty_python_semantic/src/diagnostic/mod.rs index fcc86256fb..395a60b6fc 100644 --- a/crates/ty_python_semantic/src/diagnostic/mod.rs +++ b/crates/ty_python_semantic/src/diagnostic/mod.rs @@ -33,9 +33,10 @@ pub fn inferred_python_version_source_annotation( .as_ref() .and_then(|source| source.span(db)) .map(Annotation::primary), - PythonVersionSource::Cli | PythonVersionSource::Editor | PythonVersionSource::Default => { - None - } + PythonVersionSource::Cli + | PythonVersionSource::Editor + | PythonVersionSource::UvWorkspace + | PythonVersionSource::Default => None, } } @@ -100,6 +101,11 @@ pub fn add_inferred_python_version_hint_to_diagnostic( because it's the version of the selected Python interpreter in your editor", )); } + crate::PythonVersionSource::UvWorkspace => { + diagnostic.info(format_args!( + "Python {version} was assumed when {action} because it was provided by uv workspace metadata", + )); + } crate::PythonVersionSource::InstallationDirectoryLayout { site_packages_parent_dir, source: _, diff --git a/crates/ty_python_semantic/src/lint.rs b/crates/ty_python_semantic/src/lint.rs index f182f422af..52b6909d70 100644 --- a/crates/ty_python_semantic/src/lint.rs +++ b/crates/ty_python_semantic/src/lint.rs @@ -662,4 +662,7 @@ pub enum LintSource { /// The rule was enabled from the configuration in the editor. Editor, + + /// The rule was enabled by uv workspace metadata. + UvWorkspace, } diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 93f7bc655d..9dcfc63a25 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -379,6 +379,9 @@ impl Drop for LintDiagnosticGuard<'_, '_> { LintSource::Editor => { format!("rule `{rule}` was selected in the editor settings") } + LintSource::UvWorkspace => { + format!("rule `{rule}` was selected by uv workspace metadata") + } }); } diff --git a/crates/ty_server/src/system.rs b/crates/ty_server/src/system.rs index e35fb030cb..998f0e1fa2 100644 --- a/crates/ty_server/src/system.rs +++ b/crates/ty_server/src/system.rs @@ -3,6 +3,7 @@ use std::fmt; use std::fmt::Display; use std::hash::{DefaultHasher, Hash, Hasher as _}; use std::panic::RefUnwindSafe; +use std::process::Output; use std::sync::Arc; use crate::Db; @@ -181,6 +182,16 @@ impl System for LSPSystem { self.native_system.is_same_file(first, second) } + fn run_command( + &self, + program: &str, + args: &[&str], + current_directory: &SystemPath, + ) -> Result { + self.native_system + .run_command(program, args, current_directory) + } + fn source_type(&self, path: &SystemPath) -> Option { let document = self.system_path_to_document(path)?; Self::source_type_from_document(document, path.extension()) diff --git a/crates/ty_site_packages/src/lib.rs b/crates/ty_site_packages/src/lib.rs index 880797e1fb..22388883a9 100644 --- a/crates/ty_site_packages/src/lib.rs +++ b/crates/ty_site_packages/src/lib.rs @@ -2107,6 +2107,8 @@ pub enum SysPrefixPathOrigin { PythonCliFlag, /// The selected interpreter in the user's editor. Editor, + /// The `sys.prefix` path was provided by `uv workspace metadata`. + UvWorkspace, /// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable VirtualEnvVar, /// The `sys.prefix` path came from the `CONDA_PREFIX` environment variable @@ -2136,6 +2138,7 @@ impl SysPrefixPathOrigin { | Self::DerivedFromPyvenvCfg | Self::CondaPrefixVar | Self::PythonBinary + | Self::UvWorkspace | Self::SelfEnvironment => false, } } @@ -2154,7 +2157,8 @@ impl SysPrefixPathOrigin { Self::VirtualEnvVar | Self::CondaPrefixVar | Self::DerivedFromPyvenvCfg - | Self::LocalVenv => true, + | Self::LocalVenv + | Self::UvWorkspace => true, } } @@ -2169,7 +2173,8 @@ impl SysPrefixPathOrigin { | Self::DerivedFromPyvenvCfg | Self::ConfigFileSetting(..) | Self::PythonCliFlag - | Self::PythonBinary => false, + | Self::PythonBinary + | Self::UvWorkspace => false, Self::LocalVenv => true, } } @@ -2185,6 +2190,7 @@ impl std::fmt::Display for SysPrefixPathOrigin { Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"), Self::LocalVenv => f.write_str("local virtual environment"), Self::Editor => f.write_str("selected interpreter in your editor"), + Self::UvWorkspace => f.write_str("uv workspace environment"), Self::SelfEnvironment => f.write_str("ty environment"), Self::PythonBinary => f.write_str("Python binary discovered in $PATH"), } @@ -2625,6 +2631,18 @@ mod tests { test.run(); } + #[test] + fn can_find_site_packages_directory_no_virtual_env_at_origin_uv_workspace() { + let test = PythonEnvironmentTestCase { + system: TestSystem::default(), + minor_version: 12, + free_threaded: false, + origin: SysPrefixPathOrigin::UvWorkspace, + virtual_env: None, + }; + test.run(); + } + #[test] fn can_find_site_packages_directory_no_virtual_env_freethreaded() { // Shouldn't be converted to an mdtest because mdtest automatically creates a diff --git a/crates/ty_site_packages/src/version.rs b/crates/ty_site_packages/src/version.rs index b64df135e2..0abe0db9d8 100644 --- a/crates/ty_site_packages/src/version.rs +++ b/crates/ty_site_packages/src/version.rs @@ -39,6 +39,9 @@ pub enum PythonVersionSource { /// (e.g., the Python environment) Editor, + /// The value was provided by `uv workspace metadata`. + UvWorkspace, + /// We fell back to a default value because the value was not specified via the CLI or a config file. #[default] Default, diff --git a/crates/ty_static/src/env_vars.rs b/crates/ty_static/src/env_vars.rs index 49701a39fd..e85a1dd379 100644 --- a/crates/ty_static/src/env_vars.rs +++ b/crates/ty_static/src/env_vars.rs @@ -61,6 +61,19 @@ impl EnvVars { /// Accepts the same values as the `--output-format` command-line argument. pub const TY_OUTPUT_FORMAT: &'static str = "TY_OUTPUT_FORMAT"; + /// Enable uv integration. + /// + /// When set to `"1"` or `"true"`, ty invokes `uv workspace metadata` to discover the workspace + /// root. + #[attr_hidden] + pub const TY_UV: &'static str = "TY_UV"; + + /// The path to the uv executable to use for workspace discovery. + /// + /// ty uses this path when uv integration is enabled by `TY_UV`. + #[attr_hidden] + pub const UV: &'static str = "UV"; + /// Used to detect an activated virtual environment. pub const VIRTUAL_ENV: &'static str = "VIRTUAL_ENV"; From 71eb5e5a489a6854058f331607e192ac4c69697f Mon Sep 17 00:00:00 2001 From: Harshal Patel <106813066+HarshalPatel1972@users.noreply.github.com> Date: Fri, 24 Jul 2026 22:43:08 +0530 Subject: [PATCH 052/390] [`ruff`] Fix false positive with unpacked arguments (`RUF065`) (#26959) Fixes #26912 RUF065 assumes a strict 1:1 positional mapping between format string specifiers (`%s`) and `logging` arguments. That breaks down entirely when a starred expression (like `*args`) is passed, because it unpacks an unknown number of items at runtime. When we hit a starred argument, the linter gets misaligned and starts flagging unrelated eager conversions (`str(x)`) that don't actually correspond to a `%s` specifier. (This one's annoying because it means completely valid logging calls get lint errors). To fix this, we just bail out of the argument loop as soon as we see `arg.is_starred_expr()`. I think this is right for the common case, but happy to adjust if there's a better way to handle variadics in this context. ## Test Plan Added test scenarios to `RUF065_0.py` covering: - Eager conversion *after* a starred argument (shouldn't trigger) - Eager conversion *before* a starred argument (still triggers) - Multiple starred arguments in the same call - Mixed format specifiers before a starred argument --------- Co-authored-by: Brent Westbrook --- .../mdtest/ruff/logging-eager-conversion.md | 43 +++++++++++++++++++ .../ruff/rules/logging_eager_conversion.rs | 8 +++- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_linter/resources/mdtest/ruff/logging-eager-conversion.md diff --git a/crates/ruff_linter/resources/mdtest/ruff/logging-eager-conversion.md b/crates/ruff_linter/resources/mdtest/ruff/logging-eager-conversion.md new file mode 100644 index 0000000000..800d2776e6 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/ruff/logging-eager-conversion.md @@ -0,0 +1,43 @@ +# `logging-eager-conversion` (`RUF065`) + +```toml +lint.preview = true +lint.select = ["RUF065"] +``` + +## Unpacked arguments + +The presence of a starred expression (`*args`) breaks the positional mapping between format string specifiers and variadic logging arguments. Ensure eager conversions *before* the starred argument are still flagged, but bail out on ambiguous cases *after* it. + +```py +import logging + +# 1. Starred before eager conversion (should not trigger for repr("5") because the mapping is broken) +logging.warning("%s%s%s%s %s", *"1234", repr("5")) + +# 2. Eager conversion before starred (should trigger for repr("1") because it maps reliably) +logging.warning("%s %s", repr("1"), *["1234"]) # snapshot: logging-eager-conversion + +# 3. Multiple starred arguments (should not trigger anywhere) +logging.warning("%s %s %s", *["1"], *["2"], repr("3")) + +# 4. Mixed specifiers and eager conversion before starred (should trigger for repr("1")) +logging.warning("%s %s %s", repr("1"), *["2", "3"]) # snapshot: logging-eager-conversion +``` + +```snapshot +error[RUF065]: Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s` + --> src/mdtest_snippet.py:7:26 + | +7 | logging.warning("%s %s", repr("1"), *["1234"]) # snapshot: logging-eager-conversion + | ^^^^^^^^^ + | + + +error[RUF065]: Unnecessary `repr()` conversion when formatting with `%s`. Use `%r` instead of `%s` + --> src/mdtest_snippet.py:13:29 + | +13 | logging.warning("%s %s %s", repr("1"), *["2", "3"]) # snapshot: logging-eager-conversion + | ^^^^^^^^^ + | +``` diff --git a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs index 920f6c73a5..861ee00c3d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs @@ -137,7 +137,13 @@ pub(crate) fn logging_eager_conversion(checker: &Checker, call: &ast::ExprCall) None } }) - .zip(call.arguments.args.iter().skip(msg_pos + 1)) + .zip( + call.arguments + .args + .iter() + .skip(msg_pos + 1) + .take_while(|arg| !arg.is_starred_expr()), + ) { // Check if the argument is a call to eagerly format a value if let Expr::Call(ast::ExprCall { From 5539406c160ce2fe6b2b55e9d2b75091cd8a9bb4 Mon Sep 17 00:00:00 2001 From: Martijn Pieters Date: Fri, 24 Jul 2026 18:17:33 +0100 Subject: [PATCH 053/390] Cover `pycon` Markdown formatting (#27153) ## Summary Add a section on `pycon` handling to the *Markdown code formatting* section. Fixes #27151 ## Test Plan - verified markdown output locally - run `scripts/check_docs_formatted.py --generate-docs` - run `mkdocs build --strict` - run `mkdocs serve` and inspect the site preview --------- Co-authored-by: Brent Westbrook --- docs/formatter.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/formatter.md b/docs/formatter.md index 6071b14bda..2cff4f1040 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -229,13 +229,12 @@ def f(x): The Ruff formatter can also format Python code blocks in Markdown files. In these files, Ruff will format any CommonMark [fenced code blocks][] with -the following info strings: `python`, `py`, `python3`, `py3`, or `pyi`. The -formatter will automatically skip a code block if the code does not parse as +the following info strings: `python`, `py`, `python3`, `py3`, `pyi`, or `pycon`. +The formatter will automatically skip a code block if the code does not parse as valid Python or if the reformatted code would produce an invalid Python program. -Code blocks marked as `python`, `py`, `python3`, or `py3` will be formatted with -the normal Python code formatting style, while any code blocks marked with -`pyi` will be formatted like Python type stub files: +Code blocks marked as `pyi` are formatted like stub files, `pycon` blocks as +REPL sessions, and the others use normal Python file formatting. For example: ````markdown ```py From f69d1210dc47138f6cc6e81a67921c31cc2c2ce3 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 24 Jul 2026 11:01:11 -0700 Subject: [PATCH 054/390] [ty] Fix `missing-override-decorator` suggestion before Python 3.12 (#27166) ## Summary `missing-override-decorator` always suggested `@typing.override`, even when checking code targeting Python versions earlier than 3.12, where that decorator is unavailable. Select the suggested module from the configured Python version so older targets are directed to `@typing_extensions.override` while Python 3.12+ continues to use `@typing.override`. Closes https://github.com/astral-sh/ty/issues/4085. ## Test plan - Add mdtests for Python 3.11 and 3.12 that snapshot the version-appropriate suggestion. - Cover explicit overrides imported from `typing_extensions` on Python 3.11 and from `typing` on Python 3.12, confirming both satisfy the rule. - Verify concise diagnostic output remains clear. --- .../resources/mdtest/override.md | 76 +++++++++++++++++++ .../ty_python_semantic/src/types/overrides.rs | 9 ++- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/override.md b/crates/ty_python_semantic/resources/mdtest/override.md index 15e048ee99..44c4044bee 100644 --- a/crates/ty_python_semantic/resources/mdtest/override.md +++ b/crates/ty_python_semantic/resources/mdtest/override.md @@ -558,6 +558,82 @@ class StubAbstractImplementation(StubAbstractInterface): def method(self) -> int: ... # error: [missing-override-decorator] ``` +## Missing `@override` decorator on Python 3.11 + +```toml +[environment] +python-version = "3.11" + +[rules] +missing-override-decorator = "error" +``` + +```py +from typing_extensions import override + +class Parent: + def method(self) -> None: ... + +class Child(Parent): + def method(self) -> None: ... # snapshot: missing-override-decorator + +class ExplicitChild(Parent): + @override + def method(self) -> None: ... +``` + +```snapshot +error[missing-override-decorator]: Method `method` overrides `Parent.method` but is not decorated with `@override` + --> src/mdtest_snippet.py:4:9 + | +4 | def method(self) -> None: ... + | ------ `Parent.method` defined here +5 | +6 | class Child(Parent): +7 | def method(self) -> None: ... # snapshot: missing-override-decorator + | ^^^^^^ + | +info: Decorate the method with `@typing_extensions.override` to make the override explicit +``` + +## Missing `@override` decorator on Python 3.12 + +```toml +[environment] +python-version = "3.12" + +[rules] +missing-override-decorator = "error" +``` + +```py +from typing import override + +class Parent: + def method(self) -> None: ... + +class Child(Parent): + def method(self) -> None: ... # snapshot: missing-override-decorator + +class ExplicitChild(Parent): + @override + def method(self) -> None: ... +``` + +```snapshot +error[missing-override-decorator]: Method `method` overrides `Parent.method` but is not decorated with `@override` + --> src/mdtest_snippet.py:4:9 + | +4 | def method(self) -> None: ... + | ------ `Parent.method` defined here +5 | +6 | class Child(Parent): +7 | def method(self) -> None: ... # snapshot: missing-override-decorator + | ^^^^^^ + | +info: Decorate the method with `@typing.override` to make the override explicit +``` + ## Possibly-unbound definitions ```py diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index 8eb83f20d1..209919b9d7 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -1553,7 +1553,14 @@ fn check_missing_overrides<'db>( "Method `{}` overrides `{superclass_member}` but is not decorated with `@override`", member.name )); - diagnostic.info("Decorate the method with `@typing.override` to make the override explicit"); + let override_module = if Program::get(db).python_version(db) >= PythonVersion::PY312 { + "typing" + } else { + "typing_extensions" + }; + diagnostic.info(format_args!( + "Decorate the method with `@{override_module}.override` to make the override explicit" + )); if let Some(superclass_definition) = superclass_definition && superclass_definition.file(db) == context.file() From 8db33c239878a83d254a2c1a3ad363e9646283c4 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 24 Jul 2026 14:18:07 -0400 Subject: [PATCH 055/390] [ty] Use trivial sat checks for constraint set short circuits (#27161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There are many places where we check whether a constraint set is always true or never true as a short circuit, since `false ∧ C == false` and `true ∨ C == true` for all `C`. Previously, we did a "deep" satisfiability check, which gives a 100% accurate answer, but requires walking through the entire BDD, using the sequent map to calculate derived facts and detect contradictions. Since these checks are only used for optimizations, we can instead use a simpler check against the `ALWAYS_TRUE` and `ALWAYS_FALSE` terminals. These "trivial" checks can return false negatives, but that's okay; we'll just fall back on calculating the actual AND or OR, which is fine. To be extra careful, before implementing this I had Codex collect some data about how often the previous short-circuit check would engage for a non-terminal-but-always/never-satisfied BDD (data aggregated over `DateType`, `Expression`, `scipy`, `ibis`, `scipy-stubs`, `pandas`, `pandas-stubs`, `pydantic`, `sympy`, `mypy`, `jax`, `beartype`, and `attrs`): | Short-circuit location | Engaged | Missed by terminal comparison | Terminal coverage | |---|---:|---:|---:| | `ConstraintSet::and` / `or` | 13,057,040 | 42 | 99.9997% | | Iterator `when_all` / `when_any` | 7,569,371 | 177 | 99.9977% | | Post-`intersect`: `is_never_satisfied` | 1,022,067 | 367 | 99.9641% | | Post-`union`: `is_always_satisfied` | 585,184 | 125 | 99.9786% | The answer: not often at all. --- .../mdtest/diagnostics/error_context.md | 89 ++++++++ .../src/types/bound_super.rs | 2 +- .../src/types/constraints.rs | 191 +++++++++++++++--- .../ty_python_semantic/src/types/instance.rs | 4 +- .../src/types/protocol_class.rs | 6 +- .../ty_python_semantic/src/types/relation.rs | 2 +- .../src/types/signatures.rs | 2 + crates/ty_python_semantic/src/types/tuple.rs | 12 +- .../src/types/typed_dict.rs | 8 +- 9 files changed, 270 insertions(+), 46 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md index ef61f31d7d..eda449b7bb 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md @@ -693,6 +693,95 @@ help: A TypedDict is not usually assignable to any `dict[..]` type; `dict` types help: Consider using `Mapping[..]` instead of `dict[..]`. ``` +## Generic `TypedDict` field conflicts in overload diagnostics + +A generic `TypedDict` relation can be unsatisfiable without being the `never` terminal. The +resulting overload diagnostic should still explain which field introduced the conflicting +constraints. + +```py +from typing import Generic, Self, TypeVar, TypedDict, overload + +T = TypeVar("T") + +class Pair(TypedDict, Generic[T]): + first: T + second: T + +class Fixed(TypedDict): + first: int + second: str + +class OverloadedSelf: + @overload + def method(self, value: Fixed) -> None: ... # snapshot: invalid-overload + @overload + def method(self, value: str) -> None: ... + def method(self, value: Pair[Self] | str) -> None: ... +``` + +```snapshot +error[invalid-overload]: Implementation does not accept all arguments of this overload + --> src/mdtest_snippet.py:15:9 + | +15 | def method(self, value: Fixed) -> None: ... # snapshot: invalid-overload + | ^^^^^^ +16 | @overload +17 | def method(self, value: str) -> None: ... +18 | def method(self, value: Pair[Self] | str) -> None: ... + | ------ Implementation defined here + | +info: Implementation signature `(self, value: Pair[Self@method] | str) -> None` is not assignable to overload signature `(self, value: Fixed) -> None` +info: parameter `value` has an incompatible type: `Fixed` is not assignable to `Pair[Self@method] | str` +info: └── type `Fixed` is not assignable to any element of the union `Pair[Self@method] | str` +info: ├── field "second" on TypedDict `Fixed` has type `str` which is not assignable to type `Self@method` expected by TypedDict `Pair` +info: └── ... omitted 1 union element without additional context +``` + +## Stop checking callable parameters after incompatible generic constraints + +Once earlier parameters produce an unsatisfiable nonterminal constraint set, continuing to a later +parameter must not replace the diagnostic context that explains the original incompatibility. + +```py +from typing import Generic, Self, TypeVar, TypedDict, overload + +T = TypeVar("T") + +class Pair(TypedDict, Generic[T]): + first: T + second: T + +class Fixed(TypedDict): + first: int + second: str + +class OverloadedSelf: + @overload + def method(self, value: Fixed, later: int) -> None: ... # snapshot: invalid-overload + @overload + def method(self, value: str, later: str) -> None: ... + def method(self, value: Pair[Self] | str, later: str) -> None: ... +``` + +```snapshot +error[invalid-overload]: Implementation does not accept all arguments of this overload + --> src/mdtest_snippet.py:15:9 + | +15 | def method(self, value: Fixed, later: int) -> None: ... # snapshot: invalid-overload + | ^^^^^^ +16 | @overload +17 | def method(self, value: str, later: str) -> None: ... +18 | def method(self, value: Pair[Self] | str, later: str) -> None: ... + | ------ Implementation defined here + | +info: Implementation signature `(self, value: Pair[Self@method] | str, later: str) -> None` is not assignable to overload signature `(self, value: Fixed, later: int) -> None` +info: parameter `value` has an incompatible type: `Fixed` is not assignable to `Pair[Self@method] | str` +info: └── type `Fixed` is not assignable to any element of the union `Pair[Self@method] | str` +info: ├── field "second" on TypedDict `Fixed` has type `str` which is not assignable to type `Self@method` expected by TypedDict `Pair` +info: └── ... omitted 1 union element without additional context +``` + ## Type variable upper bounds Assignability context is included when an explicit type argument does not satisfy a type variable's diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 1fda7aafc6..a129548ec4 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -1009,7 +1009,7 @@ impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { } (ClassBase::TypedDict(_), _) => self.never(), }; - if class_equivalence.is_never_satisfied(db) { + if class_equivalence.is_trivially_never_satisfied() { return self.never(); } let owner_equivalence = match (left.owner(db), right.owner(db)) { diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 085d8e4a6a..91bc1b56fc 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -170,8 +170,8 @@ pub(crate) trait IteratorConstraintsExtension { /// Returns the constraints under which any element of the iterator holds. /// /// This method short-circuits; if we encounter any element that - /// [`is_always_satisfied`][ConstraintSet::is_always_satisfied], then the overall result - /// must be as well, and we stop consuming elements from the iterator. + /// [`is_trivially_always_satisfied`][ConstraintSet::is_trivially_always_satisfied], then the + /// overall result must be as well, and we stop consuming elements from the iterator. fn when_any<'db, 'c>( self, db: &'db dyn Db, @@ -182,8 +182,8 @@ pub(crate) trait IteratorConstraintsExtension { /// Returns the constraints under which every element of the iterator holds. /// /// This method short-circuits; if we encounter any element that - /// [`is_never_satisfied`][ConstraintSet::is_never_satisfied], then the overall result - /// must be as well, and we stop consuming elements from the iterator. + /// [`is_trivially_never_satisfied`][ConstraintSet::is_trivially_never_satisfied], then the + /// overall result must be as well, and we stop consuming elements from the iterator. fn when_all<'db, 'c>( self, db: &'db dyn Db, @@ -198,12 +198,11 @@ where { fn when_any<'db, 'c>( self, - db: &'db dyn Db, + _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, mut f: impl FnMut(T) -> ConstraintSet<'db, 'c>, ) -> ConstraintSet<'db, 'c> { let node = NodeId::distributed_or( - db, builder, self.map(|element| { let constraint = f(element); @@ -216,12 +215,11 @@ where fn when_all<'db, 'c>( self, - db: &'db dyn Db, + _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, mut f: impl FnMut(T) -> ConstraintSet<'db, 'c>, ) -> ConstraintSet<'db, 'c> { let node = NodeId::distributed_and( - db, builder, self.map(|element| { let constraint = f(element); @@ -437,16 +435,34 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { debug_assert!(std::ptr::eq(self.builder, builder)); } - /// Returns whether this constraint set never holds + /// Returns whether this constraint set never holds. pub(crate) fn is_never_satisfied(self, db: &'db dyn Db) -> bool { self.node.is_never_satisfied(db, self.builder) } - /// Returns whether this constraint set always holds + /// Returns whether this constraint set is the `never` terminal. + /// + /// A nonterminal constraint set can also never be satisfied, so `false` does not prove that + /// the set is satisfiable. Use [`Self::is_never_satisfied`] when false negatives are not + /// acceptable. + pub(crate) fn is_trivially_never_satisfied(self) -> bool { + self.node == ALWAYS_FALSE + } + + /// Returns whether this constraint set always holds. pub(crate) fn is_always_satisfied(self, db: &'db dyn Db) -> bool { self.node.is_always_satisfied(db, self.builder) } + /// Returns whether this constraint set is the `always` terminal. + /// + /// A nonterminal constraint set can also always be satisfied, so `false` does not prove that + /// the set is not always satisfied. Use [`Self::is_always_satisfied`] when false negatives are + /// not acceptable. + pub(crate) fn is_trivially_always_satisfied(self) -> bool { + self.node == ALWAYS_TRUE + } + /// Returns the constraints under which `lhs` is a subtype of `rhs`, assuming that the /// constraints in this constraint set hold. Panics if neither of the types being compared are /// a typevar. (That case is handled by `Type::has_relation_to`.) @@ -535,7 +551,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: impl FnOnce() -> Self, ) -> Self { self.verify_builder(builder); - if !self.is_never_satisfied(db) { + if !self.is_trivially_never_satisfied() { let other = other(); other.verify_builder(builder); self.intersect(db, builder, other); @@ -556,7 +572,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: impl FnOnce() -> Self, ) -> Self { self.verify_builder(builder); - if !self.is_always_satisfied(db) { + if !self.is_trivially_always_satisfied() { let other = other(); other.verify_builder(builder); self.union(db, builder, other); @@ -2548,14 +2564,13 @@ impl NodeId { /// You must also provide the "zero" and "one" units of the operator. The "zero" is the value /// that has no effect (`0 ∨ a = a`). It is returned if the iterator is empty. The "one" is the /// value that saturates (`1 ∨ a = 1`). We use this to short-circuit; if any element BDD or any - /// intermediate result evaluates to "one", we can return early. - fn tree_fold<'db>( - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + /// intermediate result is the "one" terminal, we can return early. + fn tree_fold( + builder: &ConstraintSetBuilder<'_>, nodes: impl Iterator, zero: Self, - is_one: impl Fn(Self, &'db dyn Db, &ConstraintSetBuilder<'db>) -> bool, - mut combine: impl FnMut(Self, &ConstraintSetBuilder<'db>, Self) -> Self, + one: Self, + mut combine: impl FnMut(Self, &ConstraintSetBuilder<'_>, Self) -> Self, ) -> Self { // To implement the "linear" shape described above, we could collect the iterator elements // into a vector, and then use the fold at the bottom of this method to combine the @@ -2583,7 +2598,7 @@ impl NodeId { // until the iterator passes 256 elements. let mut accumulator: SmallVec<[(NodeId, u8); 8]> = SmallVec::default(); for node in nodes { - if is_one(node, db, builder) { + if node == one { return node; } @@ -2594,7 +2609,7 @@ impl NodeId { { let (existing, _) = accumulator.pop().expect("accumulator should not be empty"); node = combine(existing, builder, node); - if is_one(node, db, builder) { + if node == one { return node; } depth += 1; @@ -2610,32 +2625,28 @@ impl NodeId { .fold(zero, |result, (node, _)| combine(result, builder, node)) } - fn distributed_or<'db>( - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + fn distributed_or( + builder: &ConstraintSetBuilder<'_>, nodes: impl Iterator, ) -> Self { Self::tree_fold( - db, builder, nodes, ALWAYS_FALSE, - Self::is_always_satisfied, + ALWAYS_TRUE, Self::or_with_offset, ) } - fn distributed_and<'db>( - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + fn distributed_and( + builder: &ConstraintSetBuilder<'_>, nodes: impl Iterator, ) -> Self { Self::tree_fold( - db, builder, nodes, ALWAYS_TRUE, - Self::is_never_satisfied, + ALWAYS_FALSE, Self::and_with_offset, ) } @@ -7888,6 +7899,126 @@ mod tests { assert_eq!(storage.constraint_implication_cache.len(), 2); } + #[test] + fn trivial_satisfaction_only_recognizes_terminals() { + let db = setup_db(); + let t = create_typevar(&db, "T"); + let builder = ConstraintSetBuilder::new(); + let t_int = create_constraint(&db, &builder, t, KnownClass::Int); + let t_str = create_constraint(&db, &builder, t, KnownClass::Str); + let impossible = t_int.and(&db, &builder, || t_str); + + assert!(ConstraintSet::always(&builder).is_trivially_always_satisfied()); + assert!(!ConstraintSet::always(&builder).is_trivially_never_satisfied()); + assert!(ConstraintSet::never(&builder).is_trivially_never_satisfied()); + assert!(!ConstraintSet::never(&builder).is_trivially_always_satisfied()); + assert!(!t_int.is_trivially_always_satisfied()); + assert!(!t_int.is_trivially_never_satisfied()); + assert!(impossible.is_never_satisfied(&db)); + assert!(!impossible.is_trivially_never_satisfied()); + + let t_bool_upper = ConstraintSet::constrain_typevar_upper_bound( + &db, + &builder, + t, + KnownClass::Bool.to_instance(&db), + ); + let t_int_upper = ConstraintSet::constrain_typevar_upper_bound( + &db, + &builder, + t, + KnownClass::Int.to_instance(&db), + ); + let tautology = t_bool_upper + .negate(&db, &builder) + .or(&db, &builder, || t_int_upper); + + assert!(tautology.is_always_satisfied(&db)); + assert!(!tautology.is_trivially_always_satisfied()); + } + + #[test] + fn combinators_only_short_circuit_on_terminal_saturation() { + let db = setup_db(); + let t = create_typevar(&db, "T"); + let builder = ConstraintSetBuilder::new(); + let t_int = create_constraint(&db, &builder, t, KnownClass::Int); + let t_str = create_constraint(&db, &builder, t, KnownClass::Str); + let impossible = t_int.and(&db, &builder, || t_str); + let t_bool_upper = ConstraintSet::constrain_typevar_upper_bound( + &db, + &builder, + t, + KnownClass::Bool.to_instance(&db), + ); + let t_int_upper = ConstraintSet::constrain_typevar_upper_bound( + &db, + &builder, + t, + KnownClass::Int.to_instance(&db), + ); + let tautology = t_bool_upper + .negate(&db, &builder) + .or(&db, &builder, || t_int_upper); + + let forced = Cell::new(0); + ConstraintSet::never(&builder).and(&db, &builder, || { + forced.set(forced.get() + 1); + t_int + }); + ConstraintSet::always(&builder).or(&db, &builder, || { + forced.set(forced.get() + 1); + t_int + }); + assert_eq!(forced.get(), 0); + + impossible.and(&db, &builder, || { + forced.set(forced.get() + 1); + t_int + }); + tautology.or(&db, &builder, || { + forced.set(forced.get() + 1); + t_int + }); + assert_eq!(forced.get(), 2); + + let visited = Cell::new(0); + [impossible, t_int] + .into_iter() + .when_all(&db, &builder, |set| { + visited.set(visited.get() + 1); + set + }); + assert_eq!(visited.get(), 2); + + visited.set(0); + [tautology, t_int] + .into_iter() + .when_any(&db, &builder, |set| { + visited.set(visited.get() + 1); + set + }); + assert_eq!(visited.get(), 2); + + visited.set(0); + [ConstraintSet::never(&builder), t_int] + .into_iter() + .when_all(&db, &builder, |set| { + visited.set(visited.get() + 1); + set + }); + assert_eq!(visited.get(), 1); + + visited.set(0); + [ConstraintSet::always(&builder), t_int] + .into_iter() + .when_any(&db, &builder, |set| { + visited.set(visited.get() + 1); + set + }); + assert_eq!(visited.get(), 1); + } + #[test] fn never_satisfied_results_are_cached() { let db = setup_db(); diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index fd9e33354e..02bd459023 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -528,7 +528,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { if result .union(db, self.constraints, nominally_satisfied) - .is_always_satisfied(db) + .is_trivially_always_satisfied() { return result; } @@ -800,7 +800,7 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { let compatible = self.check_tuple_spec_pair(db, &left_spec, &right_spec); if result .union(db, self.constraints, compatible) - .is_always_satisfied(db) + .is_trivially_always_satisfied() { return result; } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index afdca82752..06c25e6311 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -2044,7 +2044,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { value_ty, ); result = result.and(db, self.constraints, || element_result); - if result.is_never_satisfied(db) { + if result.is_trivially_never_satisfied() { break; } } @@ -2061,7 +2061,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { value_ty, ); result = result.or(db, self.constraints, || element_result); - if result.is_always_satisfied(db) { + if result.is_trivially_always_satisfied() { break; } } @@ -2152,7 +2152,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ClassAttributeWriteMember::Explicit { member, fallback } => { let member_result = self.check_explicit_property_write(db, object_ty, member, value_ty); - if member_result.is_never_satisfied(db) { + if member_result.is_trivially_never_satisfied() { return member_result; } if let Some(fallback) = fallback { diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 61236bbadf..828ed75de3 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -1590,7 +1590,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // check each literal individually. let supertype_result = self .without_context_collection(|| self.check_type_pair(db, supertype, target)); - if supertype_result.is_always_satisfied(db) { + if supertype_result.is_trivially_always_satisfied() { return supertype_result; } } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 12f84b34fd..09d5e9f4ac 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -2281,6 +2281,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { parameter, }); } + // Continuing past a nonterminal contradiction can bind later `ParamSpec`s or + // replace the diagnostic context that explains the incompatible parameter. !result .intersect(db, self.constraints, constraint_set) .is_never_satisfied(db) diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index cdfe8d19c1..029493ff07 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -387,7 +387,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let element_constraints = self.check_type_pair(db, source_ty, target_ty); if result .intersect(db, self.constraints, element_constraints) - .is_never_satisfied(db) + .is_trivially_never_satisfied() { return result; } @@ -399,7 +399,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let element_constraints = self.check_type_pair(db, source_ty, target_ty); if result .intersect(db, self.constraints, element_constraints) - .is_never_satisfied(db) + .is_trivially_never_satisfied() { return result; } @@ -465,7 +465,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let element_constraints = self.check_type_pair(db, source_ty, target_ty); if result .intersect(db, self.constraints, element_constraints) - .is_never_satisfied(db) + .is_trivially_never_satisfied() { return result; } @@ -478,7 +478,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let element_constraints = self.check_type_pair(db, source_ty, target_ty); if result .intersect(db, self.constraints, element_constraints) - .is_never_satisfied(db) + .is_trivially_never_satisfied() { return result; } @@ -601,7 +601,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }; if result .intersect(db, self.constraints, pair_constraints) - .is_never_satisfied(db) + .is_trivially_never_satisfied() { return result; } @@ -638,7 +638,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }; if result .intersect(db, self.constraints, pair_constraints) - .is_never_satisfied(db) + .is_trivially_never_satisfied() { return result; } diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index f30146338a..2f863ce13b 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -661,7 +661,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { self.check_type_pair(db, source_item_field.declared_ty, target_ty), ); - if result.is_never_satisfied(db) { + if result.is_trivially_never_satisfied() { return result; } } @@ -685,7 +685,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { target_item_field.declared_ty, ), ); - if result.is_never_satisfied(db) { + if result.is_trivially_never_satisfied() { return result; } } @@ -880,7 +880,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } }; result.intersect(db, self.constraints, field_constraints); - if result.is_never_satisfied(db) { + if result.is_trivially_never_satisfied() + || (self.is_context_collection_enabled() && result.is_never_satisfied(db)) + { if let Some(context) = self.report_context() && let Some(source_item_field) = source_items.get(target_item_name) { From 995db7ce3d6102e7d696e0aa4bc403a2328edbda Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Fri, 24 Jul 2026 15:16:39 -0400 Subject: [PATCH 056/390] [ty] Add `--exclude-scripts` and `--include-scripts` (#27169) This is a 3rd pseudo-alternative to * https://github.com/astral-sh/uv/pull/20676 * https://github.com/astral-sh/ruff/pull/27165 which @MichaReiser suggested as a way to ask for the desired behaviour that becomes "standard API" and not like, some random "oh are we doing uv?" escape-hatch. Notably here a PEP 723 script is only excluded if it's "implicitly selected" in much the same way that this still checks `bar.py`: `ty check --exclude foo/ -- foo/bar.py` `--include-scripts` is the default and exists only to negate `--exclude-scripts` in e.g. config-vs-cli-arg fights (and maybe will be nice if we ever change the default). --- crates/ty/docs/cli.md | 1 + crates/ty/docs/configuration.md | 27 ++++++ crates/ty/src/args.rs | 18 ++++ crates/ty/tests/cli/file_selection.rs | 88 +++++++++++++++++++ crates/ty_project/src/metadata.rs | 2 +- crates/ty_project/src/metadata/options.rs | 13 +++ crates/ty_project/src/metadata/settings.rs | 2 + crates/ty_project/src/walk.rs | 14 +++ .../e2e__commands__debug_command.snap | 1 + ty.schema.json | 7 ++ 10 files changed, 172 insertions(+), 1 deletion(-) diff --git a/crates/ty/docs/cli.md b/crates/ty/docs/cli.md index d68791569d..c8306663a3 100644 --- a/crates/ty/docs/cli.md +++ b/crates/ty/docs/cli.md @@ -56,6 +56,7 @@ over all configuration files.

Cannot be used in combination with --exit-zero or --exit-zero-on-warning.

--exclude exclude

Glob patterns for files to exclude from type checking.

Uses gitignore-style syntax to exclude files and directories from type checking. Supports patterns like tests/, *.tmp, **/__pycache__/**.

+
--exclude-scripts

Exclude files containing PEP 723 inline script metadata unless passed explicitly. Use --include-scripts to disable

--exit-zero

Always use exit code 0, even when there are error-level diagnostics.

Cannot be used in combination with --error-on-warning.

--exit-zero-on-warning

Use exit code 0 if there are no error-level diagnostics.

diff --git a/crates/ty/docs/configuration.md b/crates/ty/docs/configuration.md index 8f2950fdc8..fcd90c42b1 100644 --- a/crates/ty/docs/configuration.md +++ b/crates/ty/docs/configuration.md @@ -948,6 +948,33 @@ to re-include `dist` use `exclude = ["!dist"]` --- +### `exclude-scripts` + +Whether to exclude files containing PEP 723 inline script metadata unless they are +explicitly passed on the command line. + +**Default value**: `false` + +**Type**: `bool` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.ty.src] + exclude-scripts = true + ``` + +=== "ty.toml" + + ```toml + [src] + exclude-scripts = true + ``` + +--- + ### `include` A list of files and directories to check. The `include` option diff --git a/crates/ty/src/args.rs b/crates/ty/src/args.rs index d3a576211d..f954ed3390 100644 --- a/crates/ty/src/args.rs +++ b/crates/ty/src/args.rs @@ -204,6 +204,19 @@ pub(crate) struct CheckCommand { #[clap(long, overrides_with("force_exclude"), hide = true)] no_force_exclude: bool, + /// Exclude files containing PEP 723 inline script metadata unless passed explicitly. + /// Use `--include-scripts` to disable. + #[arg( + long, + overrides_with("include_scripts"), + help_heading = "File selection", + default_missing_value = "true", + num_args = 0..1 + )] + exclude_scripts: Option, + #[clap(long, overrides_with("exclude_scripts"), hide = true)] + include_scripts: bool, + /// Glob patterns for files to exclude from type checking. /// /// Uses gitignore-style syntax to exclude files and directories from type checking. @@ -251,6 +264,10 @@ impl CheckCommand { .no_respect_ignore_files .then_some(false) .or(self.respect_ignore_files); + let exclude_scripts = self + .include_scripts + .then_some(false) + .or(self.exclude_scripts); let error_on_warning = self .exit_zero_on_warning .then_some(false) @@ -279,6 +296,7 @@ impl CheckCommand { }), src: Some(SrcOptions { respect_ignore_files, + exclude_scripts, exclude: self.exclude.map(|excludes| { RangedValue::cli(excludes.iter().map(RelativeGlobPattern::cli).collect()) }), diff --git a/crates/ty/tests/cli/file_selection.rs b/crates/ty/tests/cli/file_selection.rs index 47b68548c2..7046d1deb1 100644 --- a/crates/ty/tests/cli/file_selection.rs +++ b/crates/ty/tests/cli/file_selection.rs @@ -2,6 +2,94 @@ use insta_cmd::assert_cmd_snapshot; use crate::CliTest; +#[test] +fn exclude_scripts_only_applies_to_implicitly_discovered_files() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("main.py", "value: int = 'project'"), + ( + "script.py", + r#" + # /// script + # dependencies = [] + # /// + value: int = "script" + "#, + ), + ( + "nested/script.py", + r#" + # /// script + # dependencies = [] + # /// + value: int = "nested-script" + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise"), @r#" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int` + nested/script.py:5:14: error[invalid-assignment] Object of type `Literal["nested-script"]` is not assignable to `int` + script.py:5:14: error[invalid-assignment] Object of type `Literal["script"]` is not assignable to `int` + Found 3 diagnostics + + ----- stderr ----- + "#); + + assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise").arg("--exclude-scripts"), @r#" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + + assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise").arg("--exclude-scripts").arg("script.py"), @r#" + success: false + exit_code: 1 + ----- stdout ----- + script.py:5:14: error[invalid-assignment] Object of type `Literal["script"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + + case.write_file( + "ty.toml", + r#" + [src] + exclude-scripts = true + "#, + )?; + + assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise"), @r#" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int` + Found 1 diagnostic + + ----- stderr ----- + "#); + assert_cmd_snapshot!(case.command().env("TY_OUTPUT_FORMAT", "concise").arg("--include-scripts"), @r#" + success: false + exit_code: 1 + ----- stdout ----- + main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int` + nested/script.py:5:14: error[invalid-assignment] Object of type `Literal["nested-script"]` is not assignable to `int` + script.py:5:14: error[invalid-assignment] Object of type `Literal["script"]` is not assignable to `int` + Found 3 diagnostics + + ----- stderr ----- + "#); + + Ok(()) +} + /// Test exclude CLI argument functionality #[test] fn exclude_argument() -> anyhow::Result<()> { diff --git a/crates/ty_project/src/metadata.rs b/crates/ty_project/src/metadata.rs index b87c086ff4..e6439cf14d 100644 --- a/crates/ty_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -24,7 +24,7 @@ mod configuration_file; pub mod options; pub mod pyproject; pub mod python_version; -mod script; +pub(crate) mod script; pub mod settings; mod uv; pub mod value; diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 8c586ea8ce..14cf81fcfb 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -943,6 +943,18 @@ pub struct SrcOptions { #[serde(skip_serializing_if = "Option::is_none")] pub respect_ignore_files: Option, + /// Whether to exclude files containing PEP 723 inline script metadata unless they are + /// explicitly passed on the command line. + #[option( + default = r#"false"#, + value_type = r#"bool"#, + example = r#" + exclude-scripts = true + "# + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude_scripts: Option, + /// A list of files and directories to check. The `include` option /// follows a similar syntax to `.gitignore` but reversed: /// Including a file or directory will make it so that it (and its contents) @@ -1062,6 +1074,7 @@ impl SrcOptions { Ok(SrcSettings { respect_ignore_files: self.respect_ignore_files.unwrap_or(true), + exclude_scripts: self.exclude_scripts.unwrap_or(false), files, }) } diff --git a/crates/ty_project/src/metadata/settings.rs b/crates/ty_project/src/metadata/settings.rs index 9ba362b57d..e2e4b0c74b 100644 --- a/crates/ty_project/src/metadata/settings.rs +++ b/crates/ty_project/src/metadata/settings.rs @@ -81,12 +81,14 @@ impl Default for TerminalSettings { #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] pub struct SrcSettings { pub respect_ignore_files: bool, + pub exclude_scripts: bool, pub files: IncludeExcludeFilter, } impl SrcSettings { pub(crate) fn default() -> Self { Self { respect_ignore_files: true, + exclude_scripts: false, files: IncludeExcludeFilter::default(), } } diff --git a/crates/ty_project/src/walk.rs b/crates/ty_project/src/walk.rs index 6efea29574..91e775544a 100644 --- a/crates/ty_project/src/walk.rs +++ b/crates/ty_project/src/walk.rs @@ -1,4 +1,5 @@ use crate::glob::IncludeExcludeFilter; +use crate::metadata::script::script_metadata; use crate::{Db, GlobFilterCheckMode, IncludeResult, Project}; use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity}; use ruff_db::files::{File, system_path_to_file}; @@ -167,6 +168,7 @@ impl ProjectFilesWalker { }; let filter = ProjectFilesFilter::from_project(db, project); + let exclude_scripts = project.settings(db).src().exclude_scripts; let files = std::sync::Mutex::new(Vec::new()); let diagnostics = std::sync::Mutex::new(Vec::new()); @@ -259,6 +261,18 @@ impl ProjectFilesWalker { // If this returns `Err`, then the file was deleted between now and when the walk callback was called. // We can ignore this. if let Ok(file) = system_path_to_file(&*db, entry.path()) { + if entry.depth() > 0 + && exclude_scripts + && script_metadata(&*db, file).is_some() + { + tracing::debug!( + "Ignoring implicitly discovered PEP 723 script `{path}` \ + because `exclude-scripts` is enabled.", + path = entry.path() + ); + return WalkState::Skip; + } + files.lock().unwrap().push(file); } } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index b570aef0b4..464020fa4f 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -46,6 +46,7 @@ Settings: Settings { }, src: SrcSettings { respect_ignore_files: true, + exclude_scripts: false, files: IncludeExcludeFilter { include: IncludeFilter( [ diff --git a/ty.schema.json b/ty.schema.json index ef68c1c4b0..be1fa4be1c 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -1586,6 +1586,13 @@ } ] }, + "exclude-scripts": { + "description": "Whether to exclude files containing PEP 723 inline script metadata unless they are\nexplicitly passed on the command line.", + "type": [ + "boolean", + "null" + ] + }, "include": { "description": "A list of files and directories to check. The `include` option\nfollows a similar syntax to `.gitignore` but reversed:\nIncluding a file or directory will make it so that it (and its contents)\nare type checked.\n\n- `./src/` matches only a directory\n- `./src` matches both files and directories\n- `src` matches a file or directory named `src`\n- `*` matches any (possibly empty) sequence of characters (except `/`).\n- `**` matches zero or more path components.\n This sequence **must** form a single path component, so both `**a` and `b**` are invalid and will result in an error.\n A sequence of more than two consecutive `*` characters is also invalid.\n- `?` matches any single character except `/`\n- `[abc]` matches any character inside the brackets. Character sequences can also specify ranges of characters, as ordered by Unicode,\n so e.g. `[0-9]` specifies any character between `0` and `9` inclusive. An unclosed bracket is invalid.\n\nAll paths are anchored relative to the project root (`src` only\nmatches `/src` and not `/test/src`).\n\n`exclude` takes precedence over `include`.", "anyOf": [ From cfd9f05ef9de09b1039f5562744e710c9196b7bf Mon Sep 17 00:00:00 2001 From: Baltasar Blanco Date: Fri, 24 Jul 2026 16:26:03 -0300 Subject: [PATCH 057/390] [`flake8-comprehensions`] NFKC-normalize keyword names in `C408` fix (#26813) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python normalizes identifiers to NFKC, but does not normalize string literals. The fix for `unnecessary-collection-call` (C408) reparses the call with libcst, which — unlike Ruff's own parser — does not normalize identifiers, and then emits the raw source text of each keyword argument as a dictionary key. This changed the key at runtime: `dict(ℼ=3.14)` has the key `π`, but was rewritten to `{"ℼ": 3.14}`. Normalize the keyword name before quoting it, matching what `fix::codemods` already does for qualified names built from libCST nodes. Fixes #16234 ## Summary Python normalizes identifiers to NFKC, but not string literals. The C408 fix re-parses the call with libcst, and that parser has a particular quirk: it does not normalize (Ruff's own parser does). So libcst hands over the raw source text of the kwarg as the key. `dict(ℼ=3.14)` has the key `π` at RUNTIME, but was rewritten to `{"ℼ": 3.14}` > this changes the behavior. My fix normalizes the kwarg name before quoting it. I'm not creating anything new, because `fix::codemods` already does the same thing. I decided not to mark it as unsafe, because the C408 fix is already unsafe. And unlike B009/B010/B043 (string > identifier, where preserving the behavior is impossible), C408 goes identifier > string. So the correct key can be computed :) ## Test Plan - 4 new cases at the end of `C408.py`: `ℼ`→`π`, `ſ`→`s`, `𝕒`→`a`, plus an ASCII control with no changes. - The snapshot diff is essentially additive. No diagnostic was altered. - The `allow_dict_calls_with_keyword_arguments` snapshot stayed INTACT. - `cargo test -p ruff_linter`: 2811 passed, 0 failed :) - `clippy --workspace --all-features -D warnings`: 100% clean. `generate-all`: up-to-date. And `prek run -a`: 14/14. Fixes #16234 --------- Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../fixtures/flake8_comprehensions/C408.py | 9 +++ .../src/rules/flake8_comprehensions/fixes.rs | 8 +- ...8_comprehensions__tests__C408_C408.py.snap | 78 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C408.py b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C408.py index c1ac839e27..d2ffaa7352 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C408.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C408.py @@ -37,3 +37,12 @@ def list(): t"{ dict(x='y') | dict(y='z') }" t"a {dict(x='y') | dict(y='z')} b" t"a { dict(x='y') | dict(y='z') } b" + +# https://github.com/astral-sh/ruff/issues/16234 +# Python normalizes identifiers to NFKC, but does not normalize string literals, so the fix has to +# normalize the keyword name to preserve the dictionary key at runtime. The character "ℼ" normalizes +# to "π", and "ſ" normalizes to "s". +dict(ℼ=3.14) +dict(ſ=1) +dict(𝕒=1, b=2) +dict(a=1, b=2) # already NFKC-normalized: unchanged diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs index 32d850820b..e04d218d90 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs @@ -14,6 +14,7 @@ use ruff_python_ast::{self as ast, Expr, ExprCall}; use ruff_python_codegen::Stylist; use ruff_python_semantic::SemanticModel; use ruff_text_size::{Ranged, TextRange}; +use unicode_normalization::UnicodeNormalization; use crate::Locator; use crate::cst::helpers::{negate, space}; @@ -241,6 +242,10 @@ pub(crate) fn fix_unnecessary_collection_call( .unwrap_or(stylist.quote()); // Quote each argument. + // + // Python normalizes identifiers to NFKC, but string literals are not normalized. Emitting the + // raw source text of a keyword argument would change the dictionary key at runtime, so the + // name has to be normalized. See https://github.com/astral-sh/ruff/issues/16234. for arg in &call.args { let quoted = format!( "{}{}{}", @@ -248,7 +253,8 @@ pub(crate) fn fix_unnecessary_collection_call( arg.keyword .as_ref() .expect("Expected dictionary argument to be kwarg") - .value, + .value + .nfkc(), quote, ); arena.push(quoted); diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py.snap b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py.snap index d3fa03663c..0002bcc838 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py.snap +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/snapshots/ruff_linter__rules__flake8_comprehensions__tests__C408_C408.py.snap @@ -529,12 +529,15 @@ C408 [*] Unnecessary `dict()` call (rewrite as a literal) 38 | t"a {dict(x='y') | dict(y='z')} b" 39 | t"a { dict(x='y') | dict(y='z') } b" | ^^^^^^^^^^^ +40 | +41 | # https://github.com/astral-sh/ruff/issues/16234 | help: Rewrite as a literal | 38 | t"a {dict(x='y') | dict(y='z')} b" - t"a { dict(x='y') | dict(y='z') } b" 39 + t"a { {'x': 'y'} | dict(y='z') } b" +40 | | note: This is an unsafe fix and may change runtime behavior @@ -545,11 +548,86 @@ C408 [*] Unnecessary `dict()` call (rewrite as a literal) 38 | t"a {dict(x='y') | dict(y='z')} b" 39 | t"a { dict(x='y') | dict(y='z') } b" | ^^^^^^^^^^^ +40 | +41 | # https://github.com/astral-sh/ruff/issues/16234 | help: Rewrite as a literal | 38 | t"a {dict(x='y') | dict(y='z')} b" - t"a { dict(x='y') | dict(y='z') } b" 39 + t"a { dict(x='y') | {'y': 'z'} } b" +40 | + | +note: This is an unsafe fix and may change runtime behavior + +C408 [*] Unnecessary `dict()` call (rewrite as a literal) + --> C408.py:45:1 + | +43 | # normalize the keyword name to preserve the dictionary key at runtime. The character "ℼ" normalizes +44 | # to "π", and "ſ" normalizes to "s". +45 | dict(ℼ=3.14) + | ^^^^^^^^^^^^ +46 | dict(ſ=1) +47 | dict(𝕒=1, b=2) + | +help: Rewrite as a literal + | +44 | # to "π", and "ſ" normalizes to "s". + - dict(ℼ=3.14) +45 + {"π": 3.14} +46 | dict(ſ=1) + | +note: This is an unsafe fix and may change runtime behavior + +C408 [*] Unnecessary `dict()` call (rewrite as a literal) + --> C408.py:46:1 + | +44 | # to "π", and "ſ" normalizes to "s". +45 | dict(ℼ=3.14) +46 | dict(ſ=1) + | ^^^^^^^^^ +47 | dict(𝕒=1, b=2) +48 | dict(a=1, b=2) # already NFKC-normalized: unchanged + | +help: Rewrite as a literal + | +45 | dict(ℼ=3.14) + - dict(ſ=1) +46 + {"s": 1} +47 | dict(𝕒=1, b=2) + | +note: This is an unsafe fix and may change runtime behavior + +C408 [*] Unnecessary `dict()` call (rewrite as a literal) + --> C408.py:47:1 + | +45 | dict(ℼ=3.14) +46 | dict(ſ=1) +47 | dict(𝕒=1, b=2) + | ^^^^^^^^^^^^^^ +48 | dict(a=1, b=2) # already NFKC-normalized: unchanged + | +help: Rewrite as a literal + | +46 | dict(ſ=1) + - dict(𝕒=1, b=2) +47 + {"a": 1, "b": 2} +48 | dict(a=1, b=2) # already NFKC-normalized: unchanged + | +note: This is an unsafe fix and may change runtime behavior + +C408 [*] Unnecessary `dict()` call (rewrite as a literal) + --> C408.py:48:1 + | +46 | dict(ſ=1) +47 | dict(𝕒=1, b=2) +48 | dict(a=1, b=2) # already NFKC-normalized: unchanged + | ^^^^^^^^^^^^^^ + | +help: Rewrite as a literal + | +47 | dict(𝕒=1, b=2) + - dict(a=1, b=2) # already NFKC-normalized: unchanged +48 + {"a": 1, "b": 2} # already NFKC-normalized: unchanged | note: This is an unsafe fix and may change runtime behavior From 57fb39b364f9d710bfcd475f9f11214960cbe1b4 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 24 Jul 2026 13:51:42 -0500 Subject: [PATCH 058/390] Test more indentation cases --- .../invalid/statements/if_extra_indent.py | 42 ++ ...syntax@statements__if_extra_indent.py.snap | 574 +++++++++++++++++- 2 files changed, 609 insertions(+), 7 deletions(-) diff --git a/crates/ruff_python_parser/resources/invalid/statements/if_extra_indent.py b/crates/ruff_python_parser/resources/invalid/statements/if_extra_indent.py index 647626cb5d..55cc8886e5 100644 --- a/crates/ruff_python_parser/resources/invalid/statements/if_extra_indent.py +++ b/crates/ruff_python_parser/resources/invalid/statements/if_extra_indent.py @@ -6,3 +6,45 @@ pass a = 10 + +# Multiple nested unexpected indents. +if True: + before_nested + first_nested + second_nested + after_nested + +outside_nested + +# A valid compound statement inside recovered indentation. +if True: + before_compound + if condition: + nested_compound + recovered_compound + after_compound + +outside_compound + +# Multiple independent unexpected-indent regions in the same body. +if True: + before_regions + first_region + middle_region + second_region + after_region + +outside_regions + +# An independent syntax error inside recovered indentation stays visible. +if True: + before_error + broken(,) + after_error + +outside_error + +# Outstanding unexpected indents are flushed at EOF. +if True: + before_eof + final_eof diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap index 6a36fcb277..800e52f16b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap @@ -8,7 +8,7 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/if_extra_inde Module( ModModule { node_index: NodeIndex(None), - range: 0..153, + range: 0..939, body: [ If( StmtIf { @@ -92,6 +92,423 @@ Module( ), }, ), + If( + StmtIf { + node_index: NodeIndex(None), + range: 192..265, + test: BooleanLiteral( + ExprBooleanLiteral { + node_index: NodeIndex(None), + range: 195..199, + value: true, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 205..218, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 205..218, + id: Name("before_nested"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 227..239, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 227..239, + id: Name("first_nested"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 252..265, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 252..265, + id: Name("second_nested"), + ctx: Load, + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 270..282, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 270..282, + id: Name("after_nested"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 284..298, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 284..298, + id: Name("outside_nested"), + ctx: Load, + }, + ), + }, + ), + If( + StmtIf { + node_index: NodeIndex(None), + range: 359..464, + test: BooleanLiteral( + ExprBooleanLiteral { + node_index: NodeIndex(None), + range: 362..366, + value: true, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 372..387, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 372..387, + id: Name("before_compound"), + ctx: Load, + }, + ), + }, + ), + If( + StmtIf { + node_index: NodeIndex(None), + range: 396..437, + test: Name( + ExprName { + node_index: NodeIndex(None), + range: 399..408, + id: Name("condition"), + ctx: Load, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 422..437, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 422..437, + id: Name("nested_compound"), + ctx: Load, + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 446..464, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 446..464, + id: Name("recovered_compound"), + ctx: Load, + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 469..483, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 469..483, + id: Name("after_compound"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 485..501, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 485..501, + id: Name("outside_compound"), + ctx: Load, + }, + ), + }, + ), + If( + StmtIf { + node_index: NodeIndex(None), + range: 570..618, + test: BooleanLiteral( + ExprBooleanLiteral { + node_index: NodeIndex(None), + range: 573..577, + value: true, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 583..597, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 583..597, + id: Name("before_regions"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 606..618, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 606..618, + id: Name("first_region"), + ctx: Load, + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 623..636, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 623..636, + id: Name("middle_region"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 645..658, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 645..658, + id: Name("second_region"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 663..675, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 663..675, + id: Name("after_region"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 677..692, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 677..692, + id: Name("outside_regions"), + ctx: Load, + }, + ), + }, + ), + If( + StmtIf { + node_index: NodeIndex(None), + range: 768..811, + test: BooleanLiteral( + ExprBooleanLiteral { + node_index: NodeIndex(None), + range: 771..775, + value: true, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 781..793, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 781..793, + id: Name("before_error"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 802..811, + value: Call( + ExprCall { + node_index: NodeIndex(None), + range: 802..811, + func: Name( + ExprName { + node_index: NodeIndex(None), + range: 802..808, + id: Name("broken"), + ctx: Load, + }, + ), + arguments: Arguments { + range: 808..811, + node_index: NodeIndex(None), + args: [], + keywords: [], + }, + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 816..827, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 816..827, + id: Name("after_error"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 829..842, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 829..842, + id: Name("outside_error"), + ctx: Load, + }, + ), + }, + ), + If( + StmtIf { + node_index: NodeIndex(None), + range: 897..938, + test: BooleanLiteral( + ExprBooleanLiteral { + node_index: NodeIndex(None), + range: 900..904, + value: true, + }, + ), + body: [ + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 910..920, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 910..920, + id: Name("before_eof"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 929..938, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 929..938, + id: Name("final_eof"), + ctx: Load, + }, + ), + }, + ), + ], + elif_else_clauses: [], + }, + ), ], }, ) @@ -108,9 +525,152 @@ Module( | - | -6 | pass -7 | - | ^ Syntax Error: Expected a statement -8 | a = 10 - | + | + 6 | pass + 7 | + | ^ Syntax Error: Expected a statement + 8 | a = 10 + 9 | +10 | # Multiple nested unexpected indents. + | + + + | +11 | if True: +12 | before_nested +13 | first_nested + | ^^^^^^^^ Syntax Error: Unexpected indentation +14 | second_nested +15 | after_nested + | + + + | +12 | before_nested +13 | first_nested +14 | second_nested + | ^^^^^^^^^^^^ Syntax Error: Unexpected indentation +15 | after_nested + | + + + | +13 | first_nested +14 | second_nested +15 | after_nested + | ^ Syntax Error: Expected a statement +16 | +17 | outside_nested + | + + + | +15 | after_nested +16 | + | ^ Syntax Error: Expected a statement +17 | outside_nested +18 | +19 | # A valid compound statement inside recovered indentation. + | + + + | +20 | if True: +21 | before_compound +22 | if condition: + | ^^^^^^^^ Syntax Error: Unexpected indentation +23 | nested_compound +24 | recovered_compound + | + + + | +25 | after_compound +26 | + | ^ Syntax Error: Expected a statement +27 | outside_compound +28 | +29 | # Multiple independent unexpected-indent regions in the same body. + | + + + | +30 | if True: +31 | before_regions +32 | first_region + | ^^^^^^^^ Syntax Error: Unexpected indentation +33 | middle_region +34 | second_region + | + + + | +32 | first_region +33 | middle_region +34 | second_region + | ^^^^^^^^ Syntax Error: Unexpected indentation +35 | after_region + | + + + | +33 | middle_region +34 | second_region +35 | after_region + | ^ Syntax Error: Expected a statement +36 | +37 | outside_regions + | + + + | +35 | after_region +36 | + | ^ Syntax Error: Expected a statement +37 | outside_regions +38 | +39 | # An independent syntax error inside recovered indentation stays visible. + | + + + | +40 | if True: +41 | before_error +42 | broken(,) + | ^^^^^^^^ Syntax Error: Unexpected indentation +43 | after_error + | + + + | +40 | if True: +41 | before_error +42 | broken(,) + | ^ Syntax Error: Expected an expression or a ')' +43 | after_error + | + + + | +43 | after_error +44 | + | ^ Syntax Error: Expected a statement +45 | outside_error +46 | +47 | # Outstanding unexpected indents are flushed at EOF. + | + + + | +48 | if True: +49 | before_eof +50 | final_eof + | ^^^^^^^^ Syntax Error: Unexpected indentation + | + + + | +49 | before_eof +50 | final_eof + | ^ Syntax Error: Expected a statement + | From 34d0944f3b1d0ce51db6a583e17faeb933df9be6 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 24 Jul 2026 12:49:05 -0500 Subject: [PATCH 059/390] Reduce syntax error noise by swallowing dedents like indents --- ...ules__pycodestyle__tests__E111_E11.py.snap | 11 - ...ules__pycodestyle__tests__E112_E11.py.snap | 11 - ...ules__pycodestyle__tests__E113_E11.py.snap | 11 - ...ules__pycodestyle__tests__E114_E11.py.snap | 11 - ...ules__pycodestyle__tests__E115_E11.py.snap | 11 - ...ules__pycodestyle__tests__E116_E11.py.snap | 11 - ...ules__pycodestyle__tests__E117_E11.py.snap | 11 - ...ules__pycodestyle__tests__W191_W19.py.snap | 10 - .../invalid/statements/if_extra_indent.py | 2 +- crates/ruff_python_parser/src/parser/mod.rs | 16 +- ...ash_continuation_indentation_error.py.snap | 7 - ...lid_syntax@if_stmt_misspelled_elif.py.snap | 9 +- ...x@re_lexing__fstring_format_spec_1.py.snap | 9 - ...ents__if_extra_closing_parentheses.py.snap | 7 - ...syntax@statements__if_extra_indent.py.snap | 371 +++++++----------- ...alid_syntax@try_stmt_invalid_order.py.snap | 7 - ..._syntax@try_stmt_misspelled_except.py.snap | 20 +- .../mdtest/generics/pep695/paramspec.md | 1 - .../resources/mdtest/invalid_syntax.md | 2 - 19 files changed, 165 insertions(+), 373 deletions(-) diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E111_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E111_E11.py.snap index 45ab478836..23a8aedbd7 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E111_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E111_E11.py.snap @@ -45,17 +45,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - invalid-syntax: Expected an indented block after `if` statement --> E11.py:45:1 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap index b2544e4212..791dea7b0f 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E112_E11.py.snap @@ -34,17 +34,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - E112 Expected an indented block --> E11.py:45:1 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E113_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E113_E11.py.snap index 4fdbcf5e3c..2684210981 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E113_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E113_E11.py.snap @@ -34,17 +34,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - invalid-syntax: Expected an indented block after `if` statement --> E11.py:45:1 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E114_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E114_E11.py.snap index 06eb01f682..5829e15a10 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E114_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E114_E11.py.snap @@ -23,17 +23,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - E114 Indentation is not a multiple of 4 (comment) --> E11.py:15:1 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap index b4655dd131..223bfca4b0 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E115_E11.py.snap @@ -23,17 +23,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - E115 Expected an indented block (comment) --> E11.py:30:1 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E116_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E116_E11.py.snap index f70307eaa6..33ae7f29b7 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E116_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E116_E11.py.snap @@ -23,17 +23,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - E116 Unexpected indentation (comment) --> E11.py:15:1 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E117_E11.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E117_E11.py.snap index 10f126e602..aa9f692840 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E117_E11.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__E117_E11.py.snap @@ -34,17 +34,6 @@ invalid-syntax: Unexpected indentation 14 | mimetype = 'application/x-directory' | -invalid-syntax: Expected a statement - --> E11.py:14:1 - | -12 | print() -13 | #: E114 E116 -14 | mimetype = 'application/x-directory' - | ^ -15 | # 'httpd/unix-directory' -16 | create_date = False - | - E117 Over-indented --> E11.py:39:1 | diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W191_W19.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W191_W19.py.snap index 8d88eef99b..1dd6489e64 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W191_W19.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W191_W19.py.snap @@ -17,16 +17,6 @@ invalid-syntax: Unexpected indentation 2 | multiline string with tab in it''' | -invalid-syntax: Expected a statement - --> W19.py:5:1 - | -4 | #: W191 -5 | if False: - | ^ -6 | print # indented with 1 tab -7 | #: - | - W191 Indentation contains tabs --> W19.py:6:1 | diff --git a/crates/ruff_python_parser/resources/invalid/statements/if_extra_indent.py b/crates/ruff_python_parser/resources/invalid/statements/if_extra_indent.py index 55cc8886e5..2ab639c6cb 100644 --- a/crates/ruff_python_parser/resources/invalid/statements/if_extra_indent.py +++ b/crates/ruff_python_parser/resources/invalid/statements/if_extra_indent.py @@ -1,4 +1,4 @@ -# Improving the recovery would require changing the lexer to emit an extra dedent token after `a + b`. +# On invalid indentation, recover as if the indentation wasn't there if True: pass a + b diff --git a/crates/ruff_python_parser/src/parser/mod.rs b/crates/ruff_python_parser/src/parser/mod.rs index e3e9af569f..6719c0ab86 100644 --- a/crates/ruff_python_parser/src/parser/mod.rs +++ b/crates/ruff_python_parser/src/parser/mod.rs @@ -736,6 +736,7 @@ impl<'src> Parser<'src> { mut parse_element: impl FnMut(&mut Parser<'src>), ) { let mut progress = ParserProgress::default(); + let mut unexpected_indents = 0; let saved_context = self.recovery_context; self.recovery_context = self @@ -745,7 +746,12 @@ impl<'src> Parser<'src> { loop { progress.assert_progressing(self); - if recovery_context_kind.is_list_element(self) { + if 0 < unexpected_indents && self.at(TokenKind::Dedent) { + // Ignore this `Dedent` like we ignored the `Indent`, avoiding extra errors from + // being imbalanced + unexpected_indents -= 1; + self.bump(TokenKind::Dedent); + } else if recovery_context_kind.is_list_element(self) { parse_element(self); } else if recovery_context_kind.is_regular_list_terminator(self) { break; @@ -763,6 +769,14 @@ impl<'src> Parser<'src> { self.current_token_range(), ); + if matches!( + recovery_context_kind, + RecoveryContextKind::ModuleStatements | RecoveryContextKind::BlockStatements + ) && self.at(TokenKind::Indent) + { + // For this invalid `Indent`, ensure the matching `Dedent` gets consumed as well + unexpected_indents += 1; + } self.bump_any(); } } diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap index cb857a67d4..c8a556e09b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap @@ -69,10 +69,3 @@ Module( 4 | | 2 | |____^ Syntax Error: Unexpected indentation | - - - | -3 | \ -4 | 2 - | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_misspelled_elif.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_misspelled_elif.py.snap index e932a526c4..4a0f4d0afa 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_misspelled_elif.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_misspelled_elif.py.snap @@ -98,8 +98,8 @@ Module( | 3 | elf: 4 | pass - | ^ Syntax Error: Expected a statement 5 | else: + | ^^^^ Syntax Error: Expected a statement 6 | pass | @@ -128,10 +128,3 @@ Module( 6 | pass | ^^^^ Syntax Error: Unexpected indentation | - - - | -5 | else: -6 | pass - | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap index d836318aea..f0d3b3104c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap @@ -480,15 +480,6 @@ Module( | - | - 9 | 'format spec'} -10 | - | ^ Syntax Error: Expected a statement -11 | f'middle {'string':\\\ -12 | 'format spec'} - | - - | 11 | f'middle {'string':\\\ 12 | 'format spec'} diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap index 780f943a96..126138f28a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap @@ -75,10 +75,3 @@ Module( 3 | pass | ^^^^ Syntax Error: Unexpected indentation | - - - | -2 | if True)): -3 | pass - | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap index 800e52f16b..e3796a8d1f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap @@ -8,16 +8,16 @@ input_file: crates/ruff_python_parser/resources/invalid/statements/if_extra_inde Module( ModModule { node_index: NodeIndex(None), - range: 0..939, + range: 0..905, body: [ If( StmtIf { node_index: NodeIndex(None), - range: 103..134, + range: 69..110, test: BooleanLiteral( ExprBooleanLiteral { node_index: NodeIndex(None), - range: 106..110, + range: 72..76, value: true, }, ), @@ -25,21 +25,21 @@ Module( Pass( StmtPass { node_index: NodeIndex(None), - range: 116..120, + range: 82..86, }, ), Expr( StmtExpr { node_index: NodeIndex(None), - range: 129..134, + range: 95..100, value: BinOp( ExprBinOp { node_index: NodeIndex(None), - range: 129..134, + range: 95..100, left: Name( ExprName { node_index: NodeIndex(None), - range: 129..130, + range: 95..96, id: Name("a"), ctx: Load, }, @@ -48,7 +48,7 @@ Module( right: Name( ExprName { node_index: NodeIndex(None), - range: 133..134, + range: 99..100, id: Name("b"), ctx: Load, }, @@ -57,25 +57,25 @@ Module( ), }, ), + Pass( + StmtPass { + node_index: NodeIndex(None), + range: 106..110, + }, + ), ], elif_else_clauses: [], }, ), - Pass( - StmtPass { - node_index: NodeIndex(None), - range: 140..144, - }, - ), Assign( StmtAssign { node_index: NodeIndex(None), - range: 146..152, + range: 112..118, targets: [ Name( ExprName { node_index: NodeIndex(None), - range: 146..147, + range: 112..113, id: Name("a"), ctx: Store, }, @@ -84,7 +84,7 @@ Module( value: NumberLiteral( ExprNumberLiteral { node_index: NodeIndex(None), - range: 150..152, + range: 116..118, value: Int( 10, ), @@ -95,11 +95,11 @@ Module( If( StmtIf { node_index: NodeIndex(None), - range: 192..265, + range: 158..248, test: BooleanLiteral( ExprBooleanLiteral { node_index: NodeIndex(None), - range: 195..199, + range: 161..165, value: true, }, ), @@ -107,11 +107,11 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 205..218, + range: 171..184, value: Name( ExprName { node_index: NodeIndex(None), - range: 205..218, + range: 171..184, id: Name("before_nested"), ctx: Load, }, @@ -121,11 +121,11 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 227..239, + range: 193..205, value: Name( ExprName { node_index: NodeIndex(None), - range: 227..239, + range: 193..205, id: Name("first_nested"), ctx: Load, }, @@ -135,17 +135,31 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 252..265, + range: 218..231, value: Name( ExprName { node_index: NodeIndex(None), - range: 252..265, + range: 218..231, id: Name("second_nested"), ctx: Load, }, ), }, ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 236..248, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 236..248, + id: Name("after_nested"), + ctx: Load, + }, + ), + }, + ), ], elif_else_clauses: [], }, @@ -153,25 +167,11 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 270..282, - value: Name( - ExprName { - node_index: NodeIndex(None), - range: 270..282, - id: Name("after_nested"), - ctx: Load, - }, - ), - }, - ), - Expr( - StmtExpr { - node_index: NodeIndex(None), - range: 284..298, + range: 250..264, value: Name( ExprName { node_index: NodeIndex(None), - range: 284..298, + range: 250..264, id: Name("outside_nested"), ctx: Load, }, @@ -181,11 +181,11 @@ Module( If( StmtIf { node_index: NodeIndex(None), - range: 359..464, + range: 325..449, test: BooleanLiteral( ExprBooleanLiteral { node_index: NodeIndex(None), - range: 362..366, + range: 328..332, value: true, }, ), @@ -193,11 +193,11 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 372..387, + range: 338..353, value: Name( ExprName { node_index: NodeIndex(None), - range: 372..387, + range: 338..353, id: Name("before_compound"), ctx: Load, }, @@ -207,11 +207,11 @@ Module( If( StmtIf { node_index: NodeIndex(None), - range: 396..437, + range: 362..403, test: Name( ExprName { node_index: NodeIndex(None), - range: 399..408, + range: 365..374, id: Name("condition"), ctx: Load, }, @@ -220,11 +220,11 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 422..437, + range: 388..403, value: Name( ExprName { node_index: NodeIndex(None), - range: 422..437, + range: 388..403, id: Name("nested_compound"), ctx: Load, }, @@ -238,17 +238,31 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 446..464, + range: 412..430, value: Name( ExprName { node_index: NodeIndex(None), - range: 446..464, + range: 412..430, id: Name("recovered_compound"), ctx: Load, }, ), }, ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 435..449, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 435..449, + id: Name("after_compound"), + ctx: Load, + }, + ), + }, + ), ], elif_else_clauses: [], }, @@ -256,25 +270,11 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 469..483, - value: Name( - ExprName { - node_index: NodeIndex(None), - range: 469..483, - id: Name("after_compound"), - ctx: Load, - }, - ), - }, - ), - Expr( - StmtExpr { - node_index: NodeIndex(None), - range: 485..501, + range: 451..467, value: Name( ExprName { node_index: NodeIndex(None), - range: 485..501, + range: 451..467, id: Name("outside_compound"), ctx: Load, }, @@ -284,11 +284,11 @@ Module( If( StmtIf { node_index: NodeIndex(None), - range: 570..618, + range: 536..641, test: BooleanLiteral( ExprBooleanLiteral { node_index: NodeIndex(None), - range: 573..577, + range: 539..543, value: true, }, ), @@ -296,11 +296,11 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 583..597, + range: 549..563, value: Name( ExprName { node_index: NodeIndex(None), - range: 583..597, + range: 549..563, id: Name("before_regions"), ctx: Load, }, @@ -310,17 +310,59 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 606..618, + range: 572..584, value: Name( ExprName { node_index: NodeIndex(None), - range: 606..618, + range: 572..584, id: Name("first_region"), ctx: Load, }, ), }, ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 589..602, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 589..602, + id: Name("middle_region"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 611..624, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 611..624, + id: Name("second_region"), + ctx: Load, + }, + ), + }, + ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 629..641, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 629..641, + id: Name("after_region"), + ctx: Load, + }, + ), + }, + ), ], elif_else_clauses: [], }, @@ -328,53 +370,11 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 623..636, + range: 643..658, value: Name( ExprName { node_index: NodeIndex(None), - range: 623..636, - id: Name("middle_region"), - ctx: Load, - }, - ), - }, - ), - Expr( - StmtExpr { - node_index: NodeIndex(None), - range: 645..658, - value: Name( - ExprName { - node_index: NodeIndex(None), - range: 645..658, - id: Name("second_region"), - ctx: Load, - }, - ), - }, - ), - Expr( - StmtExpr { - node_index: NodeIndex(None), - range: 663..675, - value: Name( - ExprName { - node_index: NodeIndex(None), - range: 663..675, - id: Name("after_region"), - ctx: Load, - }, - ), - }, - ), - Expr( - StmtExpr { - node_index: NodeIndex(None), - range: 677..692, - value: Name( - ExprName { - node_index: NodeIndex(None), - range: 677..692, + range: 643..658, id: Name("outside_regions"), ctx: Load, }, @@ -384,11 +384,11 @@ Module( If( StmtIf { node_index: NodeIndex(None), - range: 768..811, + range: 734..793, test: BooleanLiteral( ExprBooleanLiteral { node_index: NodeIndex(None), - range: 771..775, + range: 737..741, value: true, }, ), @@ -396,11 +396,11 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 781..793, + range: 747..759, value: Name( ExprName { node_index: NodeIndex(None), - range: 781..793, + range: 747..759, id: Name("before_error"), ctx: Load, }, @@ -410,21 +410,21 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 802..811, + range: 768..777, value: Call( ExprCall { node_index: NodeIndex(None), - range: 802..811, + range: 768..777, func: Name( ExprName { node_index: NodeIndex(None), - range: 802..808, + range: 768..774, id: Name("broken"), ctx: Load, }, ), arguments: Arguments { - range: 808..811, + range: 774..777, node_index: NodeIndex(None), args: [], keywords: [], @@ -433,6 +433,20 @@ Module( ), }, ), + Expr( + StmtExpr { + node_index: NodeIndex(None), + range: 782..793, + value: Name( + ExprName { + node_index: NodeIndex(None), + range: 782..793, + id: Name("after_error"), + ctx: Load, + }, + ), + }, + ), ], elif_else_clauses: [], }, @@ -440,25 +454,11 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 816..827, + range: 795..808, value: Name( ExprName { node_index: NodeIndex(None), - range: 816..827, - id: Name("after_error"), - ctx: Load, - }, - ), - }, - ), - Expr( - StmtExpr { - node_index: NodeIndex(None), - range: 829..842, - value: Name( - ExprName { - node_index: NodeIndex(None), - range: 829..842, + range: 795..808, id: Name("outside_error"), ctx: Load, }, @@ -468,11 +468,11 @@ Module( If( StmtIf { node_index: NodeIndex(None), - range: 897..938, + range: 863..904, test: BooleanLiteral( ExprBooleanLiteral { node_index: NodeIndex(None), - range: 900..904, + range: 866..870, value: true, }, ), @@ -480,11 +480,11 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 910..920, + range: 876..886, value: Name( ExprName { node_index: NodeIndex(None), - range: 910..920, + range: 876..886, id: Name("before_eof"), ctx: Load, }, @@ -494,11 +494,11 @@ Module( Expr( StmtExpr { node_index: NodeIndex(None), - range: 929..938, + range: 895..904, value: Name( ExprName { node_index: NodeIndex(None), - range: 929..938, + range: 895..904, id: Name("final_eof"), ctx: Load, }, @@ -525,16 +525,6 @@ Module( | - | - 6 | pass - 7 | - | ^ Syntax Error: Expected a statement - 8 | a = 10 - 9 | -10 | # Multiple nested unexpected indents. - | - - | 11 | if True: 12 | before_nested @@ -554,26 +544,6 @@ Module( | - | -13 | first_nested -14 | second_nested -15 | after_nested - | ^ Syntax Error: Expected a statement -16 | -17 | outside_nested - | - - - | -15 | after_nested -16 | - | ^ Syntax Error: Expected a statement -17 | outside_nested -18 | -19 | # A valid compound statement inside recovered indentation. - | - - | 20 | if True: 21 | before_compound @@ -584,16 +554,6 @@ Module( | - | -25 | after_compound -26 | - | ^ Syntax Error: Expected a statement -27 | outside_compound -28 | -29 | # Multiple independent unexpected-indent regions in the same body. - | - - | 30 | if True: 31 | before_regions @@ -613,26 +573,6 @@ Module( | - | -33 | middle_region -34 | second_region -35 | after_region - | ^ Syntax Error: Expected a statement -36 | -37 | outside_regions - | - - - | -35 | after_region -36 | - | ^ Syntax Error: Expected a statement -37 | outside_regions -38 | -39 | # An independent syntax error inside recovered indentation stays visible. - | - - | 40 | if True: 41 | before_error @@ -651,26 +591,9 @@ Module( | - | -43 | after_error -44 | - | ^ Syntax Error: Expected a statement -45 | outside_error -46 | -47 | # Outstanding unexpected indents are flushed at EOF. - | - - | 48 | if True: 49 | before_eof 50 | final_eof | ^^^^^^^^ Syntax Error: Unexpected indentation | - - - | -49 | before_eof -50 | final_eof - | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_invalid_order.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_invalid_order.py.snap index 0bcf4355aa..9f55c08251 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_invalid_order.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_invalid_order.py.snap @@ -80,10 +80,3 @@ Module( 6 | pass | ^^^^ Syntax Error: Unexpected indentation | - - - | -5 | else: -6 | pass - | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_misspelled_except.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_misspelled_except.py.snap index 1803efb100..e65db427d0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_misspelled_except.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_misspelled_except.py.snap @@ -219,8 +219,8 @@ Module( | 3 | exept: # spellchecker:disable-line 4 | pass - | ^ Syntax Error: Expected a statement 5 | finally: + | ^^^^^^^ Syntax Error: Expected a statement 6 | pass 7 | a = 1 | @@ -257,16 +257,6 @@ Module( | - | -5 | finally: -6 | pass - | ^ Syntax Error: Expected a statement -7 | a = 1 -8 | try: -9 | pass - | - - | 10 | except: 11 | pass @@ -284,11 +274,3 @@ Module( | ^^^^ Syntax Error: Unexpected indentation 14 | b = 1 | - - - | -12 | exept: # spellchecker:disable-line -13 | pass - | ^ Syntax Error: Expected a statement -14 | b = 1 - | diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index 5d0dae7b3d..c714ce6181 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -32,7 +32,6 @@ position. The parser could do a better job in recovering from these errors. # error: [invalid-syntax] # error: [invalid-syntax] def foo[**P: int]() -> None: - # error: [invalid-syntax] # error: [invalid-syntax] pass ``` diff --git a/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md index 53acc4fbe3..7a88c70b62 100644 --- a/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md +++ b/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md @@ -40,7 +40,6 @@ type pass = 1 # error: [invalid-syntax] # error: [invalid-syntax] def True(for): - # error: [invalid-syntax] # error: [invalid-syntax] pass ``` @@ -76,7 +75,6 @@ match while: # error: [invalid-syntax] # error: [unresolved-reference] "Name `case` used when not defined" case in: - # error: [invalid-syntax] # error: [invalid-syntax] pass ``` From 3ad98a8dc820b805965076ecca4d27eebde70755 Mon Sep 17 00:00:00 2001 From: Martin Kuntz Jacobsen <56176095+TrapsterDK@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:30:51 +0200 Subject: [PATCH 060/390] [ty] Allow unresolved unused venv home paths (#27162) ## Summary Allow ty to use local virtual environment when `home` value in `pyvenv.cfg` cannot be resolved and system site-packages are disabled. Implements the suggested fix by @MichaReiser for the issue https://github.com/astral-sh/ty/issues/455. ## Test Plan Added testing for discovering local `site-packages` with an unresolved `home`. Added testing for preserving the erorr when system site-packages are enabled. --- crates/ty_site_packages/src/lib.rs | 79 ++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/crates/ty_site_packages/src/lib.rs b/crates/ty_site_packages/src/lib.rs index 22388883a9..77a593e12a 100644 --- a/crates/ty_site_packages/src/lib.rs +++ b/crates/ty_site_packages/src/lib.rs @@ -685,7 +685,7 @@ impl PythonBuildVariant { #[derive(Debug)] pub struct VirtualEnvironment { root_path: SysPrefixPath, - base_executable_home_path: PythonHomePath, + base_executable_home_path: Option, include_system_site_packages: bool, /// The version of the Python executable that was used to create this virtual environment. @@ -746,10 +746,9 @@ impl VirtualEnvironment { parent_environment, } = parsed_pyvenv_cfg; - // The `home` key is read by the standard library's `site.py` module, - // so if it's missing from the `pyvenv.cfg` file - // (or the provided value is invalid), - // it's reasonable to consider the virtual environment irredeemably broken. + // The `home` key is read by the standard library's `site.py` module, so a missing + // key indicates an irredeemably broken virtual environment. An unresolvable value + // can still be tolerated when the base interpreter is not needed. let Some(base_executable_home_path) = base_executable_home_path else { return Err(SitePackagesDiscoveryError::PyvenvCfgParseError( pyvenv_cfg_path, @@ -757,13 +756,24 @@ impl VirtualEnvironment { )); }; - let base_executable_home_path = PythonHomePath::new(base_executable_home_path, system) - .map_err(|io_err| { - SitePackagesDiscoveryError::PyvenvCfgParseError( + let base_executable_home_path = match PythonHomePath::new(base_executable_home_path, system) + { + Ok(home_path) => Some(home_path), + Err(io_err) if !include_system_site_packages => { + tracing::warn!( + "Failed to resolve the `home` value in the `pyvenv.cfg` file at \ + `{pyvenv_cfg_path}`. Goto-definition for stdlib-defined items will not \ + be able to jump to the real implementation. Underlying error: {io_err}" + ); + None + } + Err(io_err) => { + return Err(SitePackagesDiscoveryError::PyvenvCfgParseError( pyvenv_cfg_path.clone(), PyvenvCfgParseErrorKind::InvalidHomeValue(io_err), - ) - })?; + )); + } + }; // Since the `extends-environment` key is nonstandard, // for now we only trust it if the virtual environment was created with `uv`. @@ -853,8 +863,9 @@ impl VirtualEnvironment { } if *include_system_site_packages { - let system_sys_prefix = - SysPrefixPath::from_executable_home_path(base_executable_home_path); + let system_sys_prefix = base_executable_home_path + .as_ref() + .and_then(SysPrefixPath::from_executable_home_path); // If we fail to resolve the `sys.prefix` path from the base executable home path, // or if we fail to resolve the `site-packages` from the `sys.prefix` path, @@ -909,8 +920,9 @@ System site-packages will not be used for module resolution.", // of the dir we're looking for. let version = version.as_ref().map(|v| v.version); let layout = PythonInterpreterLayout::unknown(*implementation, version); - if let Some(system_sys_prefix) = - SysPrefixPath::from_executable_home_path_real(system, base_executable_home_path) + if let Some(system_sys_prefix) = base_executable_home_path + .as_ref() + .and_then(|home_path| SysPrefixPath::from_executable_home_path_real(system, home_path)) { let real_stdlib_directory = real_stdlib_directory_from_sys_prefix(&system_sys_prefix, layout, system); @@ -2473,7 +2485,10 @@ mod tests { } else { SystemPathBuf::from(&*format!("/Python3.{}/bin", self.minor_version)) }; - assert_eq!(venv.base_executable_home_path, expected_home); + assert_eq!( + venv.base_executable_home_path.as_deref(), + Some(&*expected_home) + ); let site_packages_directories = venv.site_packages_directories(&self.system).unwrap(); let expected_venv_site_packages = if cfg!(target_os = "windows") { @@ -2990,15 +3005,47 @@ mod tests { } #[test] - fn parsing_pyvenv_cfg_with_invalid_home_key_fails() { + fn unresolved_pyvenv_cfg_home_is_nonfatal_without_system_site_packages() { let system = TestSystem::default(); let memory_fs = system.memory_file_system(); let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); memory_fs .write_file_all(&pyvenv_cfg_path, "home = foo") .unwrap(); + let site_packages = if cfg!(target_os = "windows") { + SystemPathBuf::from(r"\.venv\Lib\site-packages") + } else { + SystemPathBuf::from("/.venv/lib/python3.13/site-packages") + }; + memory_fs.create_directory_all(&site_packages).unwrap(); + + let venv = PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system) + .unwrap() + .expect_venv(); + + assert_eq!(venv.base_executable_home_path, None); + let expected = [site_packages]; + assert_eq!( + venv.site_packages_directories(&system).unwrap(), + &expected[..] + ); + } + + #[test] + fn unresolved_pyvenv_cfg_home_with_system_site_packages_fails() { + let system = TestSystem::default(); + let memory_fs = system.memory_file_system(); + let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); + memory_fs + .write_file_all( + &pyvenv_cfg_path, + "home = foo\ninclude-system-site-packages = true", + ) + .unwrap(); + let venv_result = PythonEnvironment::new("/.venv", SysPrefixPathOrigin::VirtualEnvVar, &system); + assert!(matches!( venv_result, Err(SitePackagesDiscoveryError::PyvenvCfgParseError( From a8c4a236ced1ac137b66bb88ddb49178495836b3 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 25 Jul 2026 01:02:55 +0100 Subject: [PATCH 061/390] [ty] Narrow tagged unions using identity comparisons (#27130) (Stacked on top of https://github.com/astral-sh/ruff/pull/27126) ## Summary - Narrow nominal tagged unions when discriminator attributes are compared with `is` or `is not`, including reversed comparisons, boolean and enum literals, `None`, and `NewType` runtime identities. - Share identity-comparison truthiness between inference and narrowing while preserving singleton guarantees and constrained-`TypeVar` correlation. - Avoid stale narrowing when an assignment expression directly rebinds the tagged-union value. N.B. Codex kept pointing out unrelated walrus narrowing bugs when I asked it to review this branch. But when I challenged it on this it conceded that these were all pre-existing bugs on `main`. I think it only started flagging them because this PR refactors our identity narrowing for tagged unions so that they use a shared helper, so it thought everything to do with identity narrowing for tagged unions was in scope. --- crates/ty_python_core/src/place.rs | 11 +- .../mdtest/narrow/conditionals/eq.md | 22 ++++ .../mdtest/narrow/conditionals/is.md | 120 +++++++++++++++++ .../src/types/infer/comparisons.rs | 121 ++++++++---------- crates/ty_python_semantic/src/types/narrow.rs | 81 +++++++----- 5 files changed, 252 insertions(+), 103 deletions(-) diff --git a/crates/ty_python_core/src/place.rs b/crates/ty_python_core/src/place.rs index b74fdd0903..b74bc6666d 100644 --- a/crates/ty_python_core/src/place.rs +++ b/crates/ty_python_core/src/place.rs @@ -687,24 +687,21 @@ impl<'db, 'a> PossiblyNarrowedPlacesBuilder<'db, 'a> { self.add_narrowing_target(comparator, &mut places); } - let can_narrow_attribute_base = - matches!(&*expr_compare.ops, [ast::CmpOp::Eq | ast::CmpOp::NotEq]); - let can_narrow_subscript_base = matches!( + let can_narrow_tagged_union_base = matches!( &*expr_compare.ops, [ast::CmpOp::Eq | ast::CmpOp::NotEq | ast::CmpOp::Is | ast::CmpOp::IsNot] ); - // For subscript expressions on either side, the subscript base can also be narrowed. - // (TypedDict and tuple discriminated union narrowing.) + // Tagged-union checks can also narrow the base of a subscript or attribute on either side. for expr in std::iter::once(&*expr_compare.left).chain(&expr_compare.comparators) { - if can_narrow_subscript_base + if can_narrow_tagged_union_base && let ast::Expr::Subscript(subscript) = expr.expression_value() && let Some(place_expr) = PlaceExpr::try_from_expr(&subscript.value) && let Some(place) = self.places.place_id((&place_expr).into()) { places.insert(place); } - if can_narrow_attribute_base + if can_narrow_tagged_union_base && let ast::Expr::Attribute(attribute) = expr && let Some(place_expr) = PlaceExpr::try_from_expr(&attribute.value) && let Some(place) = self.places.place_id((&place_expr).into()) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index d759f0669c..e6450c4867 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -1430,6 +1430,28 @@ def overwritten_tagged_union(value: A | B | bool): reveal_type(value) # revealed: Literal[True] else: reveal_type(value) # revealed: Literal[False] + +def overwritten_tagged_union_attribute(value: A | B | str): + if isinstance(value, (A, B)): + if (value := value.tag) == "a": + reveal_type(value) # revealed: Literal["a"] + else: + reveal_type(value) # revealed: Literal["b"] + +def tagged_union_rebound_by_comparator(value: A | B | str): + if isinstance(value, (A, B)): + if value.tag == (value := "a"): + reveal_type(value) # revealed: Literal["a"] + else: + reveal_type(value) # revealed: Literal["a"] + +def tagged_union_with_unrelated_assignment(value: A | B): + if value.tag == (tag := "a"): + reveal_type(value) # revealed: A + reveal_type(tag) # revealed: Literal["a"] + else: + reveal_type(value) # revealed: B + reveal_type(tag) # revealed: Literal["a"] ``` ## Union with `Any` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md index 75268fc9af..e38eaaa816 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md @@ -28,6 +28,126 @@ def _(x: A, y: A | None): reveal_type(y) # revealed: A | None ``` +## Narrowing tagged unions of nominal classes by attribute identity + +```py +from dataclasses import dataclass +from enum import Enum +from typing import Literal, NewType + +@dataclass +class Foo: + tag: Literal[False] + +@dataclass +class Bar: + tag: Literal[True] + +@dataclass +class UnknownTag: + tag: bool + +def boolean_tags(value: Foo | Bar): + if value.tag is True: + reveal_type(value) # revealed: Bar + else: + reveal_type(value) # revealed: Foo + + if value.tag is not True: + reveal_type(value) # revealed: Foo + else: + reveal_type(value) # revealed: Bar + + if True is value.tag: + reveal_type(value) # revealed: Bar + else: + reveal_type(value) # revealed: Foo + + if True is not value.tag: + reveal_type(value) # revealed: Foo + else: + reveal_type(value) # revealed: Bar + +def ambiguous_tag(value: Foo | Bar | UnknownTag): + if value.tag is True: + reveal_type(value) # revealed: Bar | UnknownTag + else: + reveal_type(value) # revealed: Foo | UnknownTag + +def nonsingleton_tag(value: Foo | Bar, tag: bool): + if value.tag is tag: + reveal_type(value) # revealed: Foo | Bar + else: + reveal_type(value) # revealed: Foo | Bar + +def overwritten_tagged_union(value: Foo | Bar | bool): + if isinstance(value, (Foo, Bar)): + if (value := value.tag) is True: + reveal_type(value) # revealed: Literal[True] + else: + reveal_type(value) # revealed: Literal[False] + +def tagged_union_rebound_by_comparator(value: Foo | Bar | bool): + if isinstance(value, (Foo, Bar)): + if value.tag is (value := True): + reveal_type(value) # revealed: Literal[True] + else: + reveal_type(value) # revealed: Literal[True] + +def tagged_union_with_unrelated_assignment(value: Foo | Bar): + if value.tag is (tag := True): + reveal_type(value) # revealed: Bar + reveal_type(tag) # revealed: Literal[True] + else: + reveal_type(value) # revealed: Foo + reveal_type(tag) # revealed: Literal[True] + +class MissingTag: + tag: None + +class PresentTag: + tag: str + +def optional_tags(value: MissingTag | PresentTag): + if value.tag is None: + reveal_type(value) # revealed: MissingTag + else: + reveal_type(value) # revealed: PresentTag + +class Tag(Enum): + FOO = 1 + BAR = 2 + +class EnumFoo: + tag: Literal[Tag.FOO] + +class EnumBar: + tag: Literal[Tag.BAR] + +def enum_tags(value: EnumFoo | EnumBar): + if value.tag is Tag.FOO: + reveal_type(value) # revealed: EnumFoo + else: + reveal_type(value) # revealed: EnumBar + +BoolTag = NewType("BoolTag", bool) + +class NewTypeTag: + tag: BoolTag + +def newtype_tags(value: Foo | Bar | NewTypeTag): + if value.tag is True: + reveal_type(value) # revealed: Bar | NewTypeTag + else: + reveal_type(value) # revealed: Foo | NewTypeTag + +def nonsingleton_newtype_tag(value: Foo | Bar, tag: BoolTag): + if value.tag is tag: + reveal_type(value) # revealed: Foo | Bar + else: + reveal_type(value) # revealed: Foo | Bar +``` + ## `is` in chained comparisons ```py diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index 93fc63e538..b0416bb585 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -75,10 +75,56 @@ impl<'db> Type<'db> { ) } - /// Return `true` if `self` and `other` cannot describe the same runtime object. - pub(crate) fn is_disjoint_from_for_identity(self, db: &'db dyn Db, other: Type<'db>) -> bool { - self.identity_comparison_type(db) - .is_disjoint_from(db, other.identity_comparison_type(db)) + /// Return whether values of these types always, never, or possibly identify the same object. + pub(crate) fn identity_comparison_truthiness( + self, + db: &'db dyn Db, + other: Type<'db>, + ) -> Truthiness { + let is_singleton_or_intersection_with_singleton = |ty: Type<'db>| { + ty.is_singleton(db) + || ty + .resolve_type_alias(db) + .as_intersection() + .is_some_and(|intersection| { + intersection + .positive(db) + .iter() + .any(|ty| ty.is_singleton(db)) + }) + }; + + // Two occurrences of the same constrained `TypeVar` require separate handling. Although + // different specializations can choose different singleton constraints, every occurrence in + // one specialization shares the same selected constraint and therefore the same object. + if let Type::TypeVar(left) = self.resolve_type_alias(db) + && let Type::TypeVar(right) = other.resolve_type_alias(db) + && left.is_same_typevar_as(db, right) + && is_singleton_or_intersection_with_singleton(Type::TypeVar(left)) + { + return Truthiness::AlwaysTrue; + } + + // `NewType` instances are identity functions at runtime, so distinct static types can still + // identify the same object. Compare the types of their possible runtime objects instead. + let left_identity = self.identity_comparison_type(db); + let right_identity = other.identity_comparison_type(db); + + // Non-disjoint singleton types do not necessarily identify the same object: disjointness can + // be inconclusive, for example when aliases between enum members cannot be determined. + // Require one singleton type to be a subtype of the other before concluding that they are + // definitely identical. + if left_identity.is_disjoint_from(db, right_identity) { + Truthiness::AlwaysFalse + } else if is_singleton_or_intersection_with_singleton(left_identity) + && is_singleton_or_intersection_with_singleton(right_identity) + && (left_identity.is_subtype_of(db, right_identity) + || right_identity.is_subtype_of(db, left_identity)) + { + Truthiness::AlwaysTrue + } else { + Truthiness::Ambiguous + } } } @@ -222,69 +268,10 @@ pub(super) fn infer_binary_type_comparison<'db>( let op = match op { ast::CmpOp::Is | ast::CmpOp::IsNot => { - let is_positive = op == ast::CmpOp::Is; - - let is_singleton_or_intersection_with_singleton = |ty: Type<'db>| { - ty.is_singleton(db) - || ty - .resolve_type_alias(db) - .as_intersection() - .is_some_and(|intersection| { - intersection - .positive(db) - .iter() - .any(|ty| ty.is_singleton(db)) - }) - }; - - // Keep two occurrences of the same `TypeVar` symbolic. Replacing them with their bounds or - // constraints would lose their shared specialization: a `TypeVar` constrained to `None` and - // `EllipsisType` chooses the same singleton for both operands, not independent alternatives. - if let Type::TypeVar(left) = left.resolve_type_alias(db) - && let Type::TypeVar(right) = right.resolve_type_alias(db) - && left.is_same_typevar_as(db, right) - && is_singleton_or_intersection_with_singleton(Type::TypeVar(left)) - { - return Ok(Type::bool_literal(is_positive)); - } - - // `NewType` is an identity function at runtime, so distinct NewTypes can still contain the - // same object: - // - // UserId = NewType("UserId", int) - // OrderId = NewType("OrderId", int) - // UserId(1) is OrderId(1) # true, even though the two NewTypes are disjoint types! - // - // Widen both operands to the types of their possible runtime objects before using the - // ordinary comparison logic. - let left_identity = left.identity_comparison_type(db); - let right_identity = right.identity_comparison_type(db); - - // If the identity types are disjoint, the operands cannot refer to the same - // runtime object. - // - // Otherwise, knowing that both types are non-disjoint singletons is still not enough - // to establish that they refer to the *same* singleton: `is_disjoint_from` can return - // false when disjointness cannot be proven. For example, two enum-literal types will - // always both be singletons, but if their aliases are unknown, we cannot tell whether - // they denote the same member or distinct members (one might be an alias to the other). - // - // We therefore require one singleton type to be a subtype of the other before inferring - // definite identity. Either direction suffices, which also handles cases like - // `None` and `Unknown & None`. - let result = if left_identity.is_disjoint_from(db, right_identity) { - Type::bool_literal(!is_positive) - } else if is_singleton_or_intersection_with_singleton(left_identity) - && is_singleton_or_intersection_with_singleton(right_identity) - && (left_identity.is_subtype_of(db, right_identity) - || right_identity.is_subtype_of(db, left_identity)) - { - Type::bool_literal(is_positive) - } else { - KnownClass::Bool.to_instance(db) - }; - - return Ok(result); + let truthiness = left + .identity_comparison_truthiness(db, right) + .negate_if(op == ast::CmpOp::IsNot); + return Ok(Type::from_truthiness(db, truthiness)); } ast::CmpOp::Eq => NonIdentityOperator::Rich(RichCompareOperator::Eq), ast::CmpOp::NotEq => NonIdentityOperator::Rich(RichCompareOperator::Ne), diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 4a9b1e156c..ccf162da05 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -1044,6 +1044,12 @@ fn necessary_sequence_pattern_type<'db>( } } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum NominalAttributeComparison { + Equality, + Identity, +} + struct NarrowingConstraintsBuilder<'db, 'ast> { db: &'db dyn Db, module: &'ast ParsedModuleRef, @@ -3081,7 +3087,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let add_runtime_overlap = |builder: UnionBuilder<'db>, element: Type<'db>| { let overlaps_only_at_runtime = |rhs_element| { element.is_disjoint_from(self.db, rhs_element) - && !element.is_disjoint_from_for_identity(self.db, rhs_element) + && element + .identity_comparison_truthiness(self.db, rhs_element) + .may_be_true() }; let has_runtime_only_overlap = match rhs_resolved { Type::Union(union) => union @@ -3274,30 +3282,15 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .as_int_literal() && let Ok(index) = i32::try_from(index) && let rhs_ty = inference.expression_type(&comparators[0]) - && let rhs_identity_ty = rhs_ty.identity_comparison_type(self.db) - && let rhs_identity_is_singleton = rhs_identity_ty.is_singleton(self.db) - && let rhs_is_correlated_singleton = (!rhs_identity_is_singleton - && matches!(rhs_ty.resolve_type_alias(self.db), Type::TypeVar(_)) - && rhs_ty.is_singleton(self.db)) - && (is_positive_check || rhs_is_correlated_singleton || rhs_identity_is_singleton) { let filtered = union.filter(self.db, |elem| { elem.tuple_instance_spec(self.db) .and_then(|spec| spec.py_index(self.db, index).ok()) .is_none_or(|el_ty| { - if is_positive_check { - // `is X` context: keep tuples where element could be X - !el_ty.is_disjoint_from_for_identity(self.db, rhs_ty) - } else if rhs_is_correlated_singleton { - // Preserve the shared specialization instead of excluding every - // constraint in the projected union. - !el_ty.is_subtype_of(self.db, rhs_ty) - } else { - // `is not X` context: keep tuples where element is not always X - !el_ty - .identity_comparison_type(self.db) - .is_subtype_of(self.db, rhs_identity_ty) - } + el_ty + .identity_comparison_truthiness(self.db, rhs_ty) + .negate_if(!is_positive_check) + .may_be_true() }) }); if filtered != Type::Union(union) { @@ -3401,6 +3394,19 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { if let ast::Expr::Subscript(subscript) = comparators[0].expression_value() { narrow_subscript(subscript, inference.expression_type(&**left)); } + } + + if let [ + operator @ (ast::CmpOp::Eq | ast::CmpOp::NotEq | ast::CmpOp::Is | ast::CmpOp::IsNot), + ] = &**ops + { + let comparison = if matches!(operator, ast::CmpOp::Is | ast::CmpOp::IsNot) { + NominalAttributeComparison::Identity + } else { + NominalAttributeComparison::Equality + }; + let is_positive_comparison = + is_positive == matches!(operator, ast::CmpOp::Eq | ast::CmpOp::Is); let mut narrow_attribute = |attribute: &ast::ExprAttribute, other_type: Type<'db>| { let value_type = inference.expression_type(&*attribute.value); @@ -3410,13 +3416,19 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { &attribute.value, attribute.attr.id(), other_type, - is_equality, + comparison, + is_positive_comparison, ) { insert_narrowing_constraint(&mut constraints, place, constraint); } }; - if let ast::Expr::Attribute(attribute) = &**left { + if let ast::Expr::Attribute(attribute) = &**left + && comparators[0].as_named_expr().is_none_or(|named| { + PlaceExpr::try_from_expr(&named.target) + != PlaceExpr::try_from_expr(&attribute.value) + }) + { narrow_attribute(attribute, inference.expression_type(&comparators[0])); } @@ -4037,6 +4049,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { &attribute.value, attribute.attr.id(), value_ty, + NominalAttributeComparison::Equality, is_positive, ) { constraints.insert(place, constraint); @@ -4294,22 +4307,32 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { attribute_value_expr: &ast::Expr, attribute_name: &str, rhs_type: Type<'db>, - is_equality: bool, + comparison: NominalAttributeComparison, + is_positive: bool, ) -> Option<(ScopedPlaceId, NarrowingConstraint<'db>)> { let Type::Union(union) = attribute_value_type.resolve_type_alias(self.db) else { return None; }; - if !is_supported_tag_literal(rhs_type) { + + if comparison == NominalAttributeComparison::Equality && !is_supported_tag_literal(rhs_type) + { return None; } let narrowed = union.filter(self.db, |element| { nominal_attribute_type(self.db, *element, attribute_name).is_none_or(|attribute_type| { - if is_equality { - !is_supported_tag_literal(attribute_type) - || !attribute_type.is_disjoint_from(self.db, rhs_type) - } else { - !attribute_type.is_subtype_of(self.db, rhs_type) + match (comparison, is_positive) { + (NominalAttributeComparison::Equality, true) => { + !is_supported_tag_literal(attribute_type) + || !attribute_type.is_disjoint_from(self.db, rhs_type) + } + (NominalAttributeComparison::Equality, false) => { + !attribute_type.is_subtype_of(self.db, rhs_type) + } + (NominalAttributeComparison::Identity, is_positive) => attribute_type + .identity_comparison_truthiness(self.db, rhs_type) + .negate_if(!is_positive) + .may_be_true(), } }) }); From a32cdce84b0028059da0573893c2b5ff66c693f7 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sat, 25 Jul 2026 02:16:41 +0200 Subject: [PATCH 062/390] [ty] Improve union and intersection builder performance (#27167) ## Summary The `add` operations move the `Union`/`IntersectionBuilder`. This can be noticeable in hot mappings and normalization where the fluent API also isn't required. This PR uses `add_in_place` in more places and also adds such an API to `IntersectionBuilder`. This improves performance by about 1%. I extracted this out of the scripts PR, so please remind that when considering the regression of this PR. I discovered this optimization because the scripts refactor PR increased the size of `UnionBuilder`, making the move more expensive. Since I'm out Monday/Tuesday, feel free to merge. --------- Co-authored-by: Carl Meyer --- crates/ty_python_semantic/src/types.rs | 14 ++- .../src/types/bound_super.rs | 4 +- crates/ty_python_semantic/src/types/enums.rs | 2 +- .../ty_python_semantic/src/types/equality.rs | 2 +- .../ty_python_semantic/src/types/function.rs | 10 +- .../src/types/narrow/containment.rs | 2 +- .../types/property_tests/type_generation.rs | 4 +- .../src/types/set_theoretic.rs | 108 ++++++++--------- .../src/types/set_theoretic/builder.rs | 112 ++++++++---------- .../ty_python_semantic/src/types/subscript.rs | 4 +- .../src/types/typed_dict.rs | 4 +- 11 files changed, 126 insertions(+), 140 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 0642eb5fc0..60cbb17891 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -5877,10 +5877,10 @@ impl<'db> Type<'db> { // Flatten each positive element and rebuild through the intersection builder. let mut builder = IntersectionBuilder::new(db); for pos in intersection.positive(db) { - builder = builder.add_positive(pos.flatten_typevars(db)); + builder.add_positive_in_place(pos.flatten_typevars(db)); } for neg in intersection.negative(db) { - builder = builder.add_negative(neg.flatten_typevars(db)); + builder.add_negative_in_place(neg.flatten_typevars(db)); } builder.build() } @@ -6758,8 +6758,12 @@ impl<'db> Type<'db> { Type::Intersection(intersection) => { let mut builder = IntersectionBuilder::new(db); for positive in intersection.positive(db) { - builder = - builder.add_positive(positive.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + builder.add_positive_in_place(positive.apply_type_mapping_impl( + db, + type_mapping, + tcx, + visitor, + )); } // Regular promotion should remove negative contributions from intersections, // so we don't preserve them here when regular promotion is enabled. @@ -6768,7 +6772,7 @@ impl<'db> Type<'db> { TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular) ) { for negative in intersection.negative(db) { - builder = builder.add_negative( + builder.add_negative_in_place( negative.apply_type_mapping_impl(db, &type_mapping.flip(), tcx, visitor), ); } diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index a129548ec4..8a85f07df3 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -726,7 +726,7 @@ impl<'db> BoundSuperType<'db> { for positive in intersection.positive(db) { if let Ok(good_element) = delegate_to(*positive) { one_good_element_found = true; - builder = builder.add_positive(good_element); + builder.add_positive_in_place(good_element); } } if !one_good_element_found { @@ -738,7 +738,7 @@ impl<'db> BoundSuperType<'db> { } for negative in intersection.negative(db) { if let Ok(good_element) = delegate_to(*negative) { - builder = builder.add_negative(good_element); + builder.add_negative_in_place(good_element); } } return Ok(builder.build()); diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index 43631e738b..b19d440ffe 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -780,7 +780,7 @@ impl<'db> EnumComplementType<'db> { let mut builder = IntersectionBuilder::new(db).add_positive(literal); for rest in self.rest(db) { - builder = builder.add_positive(*rest); + builder.add_positive_in_place(*rest); } builder.build() } diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index 5e66a4b979..4ab7f0947f 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -1121,7 +1121,7 @@ fn evaluate_intersection_left<'db>( ComparisonResult::AlwaysFalse => any_false = true, ComparisonResult::CanNarrow(narrowed) => { any_narrowing = true; - builder = builder.add_positive(narrowed); + builder.add_positive_in_place(narrowed); } ComparisonResult::Ambiguous => any_ambiguous = true, } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index c1928a671c..c0af80717b 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1789,10 +1789,10 @@ fn is_instance_truthiness<'db>( } else if let Type::TypeVar(tvar) = positive { match tvar.typevar(db).bound_or_constraints(db) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - effective = effective.add_positive(bound); + effective.add_positive_in_place(bound); } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - effective = effective.add_positive(constraints.as_type(db)); + effective.add_positive_in_place(constraints.as_type(db)); } // A typevar without bounds/constraints has `object` as its implicit upper bound, // and adding `object` to an intersection is a no-op @@ -1801,9 +1801,9 @@ fn is_instance_truthiness<'db>( found_tvars_or_newtypes = true; } else if let Type::NewTypeInstance(newtype) = positive { found_tvars_or_newtypes = true; - effective = effective.add_positive(newtype.concrete_base_type(db)); + effective.add_positive_in_place(newtype.concrete_base_type(db)); } else { - effective = effective.add_positive(positive); + effective.add_positive_in_place(positive); } } @@ -1815,7 +1815,7 @@ fn is_instance_truthiness<'db>( if is_instance_truthiness(db, negative, class).is_always_true() { return Truthiness::AlwaysFalse; } - effective = effective.add_negative(negative); + effective.add_negative_in_place(negative); } let effective = effective.build(); diff --git a/crates/ty_python_semantic/src/types/narrow/containment.rs b/crates/ty_python_semantic/src/types/narrow/containment.rs index 9f39b85e36..057f40b588 100644 --- a/crates/ty_python_semantic/src/types/narrow/containment.rs +++ b/crates/ty_python_semantic/src/types/narrow/containment.rs @@ -175,7 +175,7 @@ pub(super) fn narrow_string_membership<'db>( { let mut builder = IntersectionBuilder::new(db).add_positive(narrowed); for character in haystack.chars() { - builder = builder.add_negative(Type::single_char_string_literal(db, character)); + builder.add_negative_in_place(Type::single_char_string_literal(db, character)); } narrowed = builder.build(); } diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index 2fef4cc9c7..caeec4f18a 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -208,10 +208,10 @@ impl Ty { Ty::Intersection { pos, neg } => { let mut builder = IntersectionBuilder::new(db); for p in pos { - builder = builder.add_positive(p.into_type(db)); + builder.add_positive_in_place(p.into_type(db)); } for n in neg { - builder = builder.add_negative(n.into_type(db)); + builder.add_negative_in_place(n.into_type(db)); } builder.build() } diff --git a/crates/ty_python_semantic/src/types/set_theoretic.rs b/crates/ty_python_semantic/src/types/set_theoretic.rs index 17c89643b4..cd1d66b655 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic.rs @@ -55,10 +55,13 @@ impl<'db> UnionType<'db> { if let Some(first) = iter_elements.next() { if let Some(second) = iter_elements.next() { - let builder = UnionBuilder::new(db).add(first.into()).add(second.into()); - iter_elements - .fold(builder, |builder, element| builder.add(element.into())) - .build() + let mut builder = UnionBuilder::new(db); + builder.add_in_place(first.into()); + builder.add_in_place(second.into()); + for element in iter_elements { + builder.add_in_place(element.into()); + } + builder.build() } else { first.into() } @@ -93,13 +96,11 @@ impl<'db> UnionType<'db> { I: IntoIterator, T: Into>, { - elements - .into_iter() - .fold( - UnionBuilder::new(db).unpack_aliases(false), - |builder, element| builder.add(element.into()), - ) - .build() + let mut builder = UnionBuilder::new(db).unpack_aliases(false); + for element in elements { + builder.add_in_place(element.into()); + } + builder.build() } /// Returns `true` if any direct element of this union is a type alias. @@ -122,13 +123,11 @@ impl<'db> UnionType<'db> { I: IntoIterator, T: Into>, { - elements - .into_iter() - .fold( - UnionBuilder::new(db).cycle_recovery(true), - |builder, element| builder.add(element.into()), - ) - .build() + let mut builder = UnionBuilder::new(db).cycle_recovery(true); + for element in elements { + builder.add_in_place(element.into()); + } + builder.build() } /// A fallible version of [`UnionType::from_elements`]. @@ -143,7 +142,7 @@ impl<'db> UnionType<'db> { { let mut builder = UnionBuilder::new(db); for element in elements { - builder = builder.add(element?.into()); + builder.add_in_place(element?.into()); } Some(builder.build()) } @@ -173,11 +172,11 @@ impl<'db> UnionType<'db> { if &new_ty != ty { let mut builder = UnionBuilder::new(db).unpack_aliases(false); for prev in &elements[..i] { - builder = builder.add(*prev); + builder.add_in_place(*prev); } - builder = builder.add(new_ty); + builder.add_in_place(new_ty); for (_, element) in iter { - builder = builder.add(transform_fn(element)); + builder.add_in_place(transform_fn(element)); } return builder .recursively_defined(self.recursively_defined(db)) @@ -214,13 +213,13 @@ impl<'db> UnionType<'db> { while let Some((i, ty)) = iter.next() { let new_ty = transform_fn(ty)?; if &new_ty != ty || matches!(new_ty, Type::TypeAlias(_)) { - let mut builder = elements[..i] - .iter() - .copied() - .fold(UnionBuilder::new(db), UnionBuilder::add); - builder = builder.add(new_ty); + let mut builder = UnionBuilder::new(db); + for prev in &elements[..i] { + builder.add_in_place(*prev); + } + builder.add_in_place(new_ty); for (_, element) in iter { - builder = builder.add(transform_fn(element)?); + builder.add_in_place(transform_fn(element)?); } return Ok(builder .recursively_defined(self.recursively_defined(db)) @@ -307,7 +306,7 @@ impl<'db> UnionType<'db> { provenance = provenance.or(member_provenance); all_unbound = false; - builder = builder.add(ty_member); + builder.add_in_place(ty_member); } } } @@ -367,7 +366,7 @@ impl<'db> UnionType<'db> { provenance = provenance.or(member_provenance); all_unbound = false; - builder = builder.add(ty_member); + builder.add_in_place(ty_member); } } } @@ -411,7 +410,7 @@ impl<'db> UnionType<'db> { if ty.same_divergent_marker(div) { return Some(ty); } - builder = builder.add(ty); + builder.add_in_place(ty); empty = false; } else { // `Divergent` in a union type does not mean true divergence, so we skip it if not nested. @@ -420,7 +419,7 @@ impl<'db> UnionType<'db> { builder = builder.recursively_defined(RecursivelyDefined::Yes); continue; } - builder = builder.add( + builder.add_in_place( ty.recursive_type_normalized_impl(db, div, nested) .unwrap_or(div), ); @@ -428,7 +427,7 @@ impl<'db> UnionType<'db> { } } if empty { - builder = builder.add(div); + builder.add_in_place(div); } Some(builder.build()) } @@ -768,13 +767,12 @@ impl<'db> IntersectionType<'db> { if let Some(first) = elements_iter.next() { if let Some(second) = elements_iter.next() { - let builder = + let mut builder = IntersectionBuilder::new(db).positive_elements([first.into(), second.into()]); - elements_iter - .fold(builder, |builder, element| { - builder.add_positive(element.into()) - }) - .build() + for element in elements_iter { + builder.add_positive_in_place(element.into()); + } + builder.build() } else { first.into() } @@ -936,10 +934,10 @@ impl<'db> IntersectionType<'db> { ) -> Type<'db> { let mut builder = IntersectionBuilder::new(db); for ty in self.positive(db) { - builder = builder.add_positive(transform_fn(ty)); + builder.add_positive_in_place(transform_fn(ty)); } for ty in self.negative(db) { - builder = builder.add_negative(*ty); + builder.add_negative_in_place(*ty); } builder.build() } @@ -959,13 +957,11 @@ impl<'db> IntersectionType<'db> { return None; } - Some( - self.iter_positive(db) - .fold(IntersectionBuilder::new(db), |builder, positive| { - builder.add_positive(positive.dunder_class(db)) - }) - .build(), - ) + let mut builder = IntersectionBuilder::new(db); + for positive in self.iter_positive(db) { + builder.add_positive_in_place(positive.dunder_class(db)); + } + Some(builder.build()) } pub(crate) fn map_with_boundness( @@ -997,7 +993,7 @@ impl<'db> IntersectionType<'db> { } provenance = provenance.or(member_provenance); - builder = builder.add_positive(ty_member); + builder.add_positive_in_place(ty_member); } } } @@ -1053,7 +1049,7 @@ impl<'db> IntersectionType<'db> { } provenance = provenance.or(member_provenance); - builder = builder.add_positive(ty_member); + builder.add_positive_in_place(ty_member); } } } @@ -1127,7 +1123,7 @@ impl<'db> IntersectionType<'db> { if let Some(projection) = positive.to_instance(db) { has_projected_positive = true; is_exact &= projection.is_exact(); - builder = builder.add_positive(projection.into_inner()); + builder.add_positive_in_place(projection.into_inner()); } else { is_exact = false; } @@ -1159,10 +1155,10 @@ fn expand_intersection_typevars_and_newtypes<'db>( Type::TypeVar(tvar) => { match tvar.typevar(db).bound_or_constraints(db) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - builder = builder.add_positive(bound); + builder.add_positive_in_place(bound); } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - builder = builder.add_positive(constraints.as_type(db)); + builder.add_positive_in_place(constraints.as_type(db)); } // Type variables without bounds or constraints implicitly have `object` // as their upper bound, and adding `object` to an intersection is always a no-op @@ -1170,14 +1166,14 @@ fn expand_intersection_typevars_and_newtypes<'db>( } } Type::NewTypeInstance(newtype) => { - builder = builder.add_positive(newtype.concrete_base_type(db)); + builder.add_positive_in_place(newtype.concrete_base_type(db)); } - _ => builder = builder.add_positive(element), + _ => builder.add_positive_in_place(element), } } for &element in negative { - builder = builder.add_negative(element); + builder.add_negative_in_place(element); } builder.build() diff --git a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs index f672fa73bb..d3836d858c 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs @@ -267,13 +267,13 @@ fn normalize_enum_complement_unions<'db>(db: &'db dyn Db, types: &mut Vec IntersectionBuilder<'db> { ); } - pub(crate) fn add_positive(self, ty: Type<'db>) -> Self { - self.add_positive_impl(ty, &mut vec![]) + pub(crate) fn add_positive(mut self, ty: Type<'db>) -> Self { + self.add_positive_in_place(ty); + self + } + + pub(crate) fn add_positive_in_place(&mut self, ty: Type<'db>) { + self.add_positive_impl(ty, &mut vec![]); } - pub(crate) fn add_positive_impl( - mut self, - ty: Type<'db>, - seen_aliases: &mut Vec>, - ) -> Self { + fn add_positive_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { match ty { Type::TypeAlias(alias) => { if seen_aliases.contains(&ty) { @@ -1244,11 +1245,11 @@ impl<'db> IntersectionBuilder<'db> { for inner in &mut self.intersections { inner.positive.insert(ty); } - return self; + return; } seen_aliases.push(ty); let value_type = alias.value_type(self.db); - self.add_positive_impl(value_type, seen_aliases) + self.add_positive_impl(value_type, seen_aliases); } Type::Union(union) => { // Distribute ourself over this union: for each union element, clone ourself and @@ -1259,29 +1260,27 @@ impl<'db> IntersectionBuilder<'db> { // (T2 & T4)`. If `self` is already a union-of-intersections `(T1 & T2) | (T3 & T4)` // and we add `T5 | T6` to it, that flattens all the way out to `(T1 & T2 & T5) | (T1 & // T2 & T6) | (T3 & T4 & T5) ...` -- you get the idea. - union - .elements(self.db) - .iter() - .map(|elem| self.clone().add_positive_impl(*elem, seen_aliases)) - .fold(IntersectionBuilder::empty(self.db), |mut builder, sub| { - builder.extend(sub); - builder - }) + let mut distributed = IntersectionBuilder::empty(self.db); + for elem in union.elements(self.db) { + let mut branch = self.clone(); + branch.add_positive_impl(*elem, seen_aliases); + distributed.extend(branch); + } + self.intersections = distributed.intersections; } // `(A & B & ~C) & (D & E & ~F)` -> `A & B & D & E & ~C & ~F` Type::Intersection(other) => { let db = self.db; for pos in other.positive(db) { - self = self.add_positive_impl(*pos, seen_aliases); + self.add_positive_impl(*pos, seen_aliases); } for neg in other.negative(db) { - self = self.add_negative_impl(*neg, seen_aliases); + self.add_negative_impl(*neg, seen_aliases); } - self } Type::EnumComplement(complement) => { let db = self.db; - self.add_positive_impl(complement.to_intersection(db), seen_aliases) + self.add_positive_impl(complement.to_intersection(db), seen_aliases); } _ => { // If we are already a union-of-intersections, distribute the new intersected element @@ -1289,20 +1288,20 @@ impl<'db> IntersectionBuilder<'db> { for inner in &mut self.intersections { inner.add_positive(self.db, ty); } - self } } } - pub(crate) fn add_negative(self, ty: Type<'db>) -> Self { - self.add_negative_impl(ty, &mut vec![]) + pub(crate) fn add_negative(mut self, ty: Type<'db>) -> Self { + self.add_negative_in_place(ty); + self + } + + pub(crate) fn add_negative_in_place(&mut self, ty: Type<'db>) { + self.add_negative_impl(ty, &mut vec![]); } - pub(crate) fn add_negative_impl( - mut self, - ty: Type<'db>, - seen_aliases: &mut Vec>, - ) -> Self { + fn add_negative_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { // See comments above in `add_positive`; this is just the negated version. match ty { Type::TypeAlias(alias) => { @@ -1311,17 +1310,16 @@ impl<'db> IntersectionBuilder<'db> { for inner in &mut self.intersections { inner.negative.insert(ty); } - return self; + return; } seen_aliases.push(ty); let value_type = alias.value_type(self.db); - self.add_negative_impl(value_type, seen_aliases) + self.add_negative_impl(value_type, seen_aliases); } Type::Union(union) => { for elem in union.elements(self.db) { - self = self.add_negative_impl(*elem, seen_aliases); + self.add_negative_impl(*elem, seen_aliases); } - self } Type::Intersection(intersection) => { // (A | B) & ~(C & ~D) @@ -1331,41 +1329,29 @@ impl<'db> IntersectionBuilder<'db> { // and negative constraints D, then our new intersection // is (existing & ~C) | (existing & D) - let positive_side = intersection - .positive(self.db) - .iter() - // we negate all the positive constraints while distributing - .map(|elem| { - self.clone() - .add_negative_impl(*elem, &mut seen_aliases.clone()) - }); - - let negative_side = intersection - .negative(self.db) - .iter() - // all negative constraints end up becoming positive constraints - .map(|elem| { - self.clone() - .add_positive_impl(*elem, &mut seen_aliases.clone()) - }); - - positive_side.chain(negative_side).fold( - IntersectionBuilder::empty(self.db), - |mut builder, sub| { - builder.extend(sub); - builder - }, - ) + let mut distributed = IntersectionBuilder::empty(self.db); + // We negate all the positive constraints while distributing. + for elem in intersection.positive(self.db) { + let mut branch = self.clone(); + branch.add_negative_impl(*elem, &mut seen_aliases.clone()); + distributed.extend(branch); + } + // All negative constraints end up becoming positive constraints. + for elem in intersection.negative(self.db) { + let mut branch = self.clone(); + branch.add_positive_impl(*elem, &mut seen_aliases.clone()); + distributed.extend(branch); + } + self.intersections = distributed.intersections; } Type::EnumComplement(complement) => { let db = self.db; - self.add_negative_impl(complement.to_intersection(db), seen_aliases) + self.add_negative_impl(complement.to_intersection(db), seen_aliases); } _ => { for inner in &mut self.intersections { inner.add_negative(self.db, ty); } - self } } } @@ -1376,7 +1362,7 @@ impl<'db> IntersectionBuilder<'db> { T: Into>, { for element in elements { - self = self.add_positive(element.into()); + self.add_positive_in_place(element.into()); } self } diff --git a/crates/ty_python_semantic/src/types/subscript.rs b/crates/ty_python_semantic/src/types/subscript.rs index d2bbc9acce..5d030131e8 100644 --- a/crates/ty_python_semantic/src/types/subscript.rs +++ b/crates/ty_python_semantic/src/types/subscript.rs @@ -441,7 +441,7 @@ where if !results.is_empty() { let mut builder = IntersectionBuilder::new(db); for result in results { - builder = builder.add_positive(result); + builder.add_positive_in_place(result); } return Ok(builder.build()); } @@ -457,7 +457,7 @@ where for error in errors { if !any_has_method || error.any_method_available() { - builder = builder.add_positive(error.result_type()); + builder.add_positive_in_place(error.result_type()); let error_iter = error.into_errors().into_iter(); if any_has_method { collected_errors.extend( diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index 2f863ce13b..0161984f1f 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -1826,7 +1826,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( for unpacked in &unpacked_elements { if let Some(unpacked_key) = unpacked.keys.get(&key) { saw_key = true; - value_ty = value_ty.add(unpacked_key.value_ty); + value_ty.add_in_place(unpacked_key.value_ty); is_required &= unpacked_key.is_required; definition = Some(if let Some(definition) = definition { merge_unpacked_key_definitions(definition, unpacked_key.definition) @@ -1835,7 +1835,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( }); } else if let Some(extra_items) = unpacked.openness.effective_extra_items() { saw_key = true; - value_ty = value_ty.add(extra_items.declared_ty); + value_ty.add_in_place(extra_items.declared_ty); is_required = false; definition = Some(None); } else { From 8576b6f024fc96da0cc92dae1b83869ab7b90699 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 25 Jul 2026 01:22:51 -0700 Subject: [PATCH 063/390] [ty] treat bivariance as covariant (#24319) ## Summary Fixes https://github.com/astral-sh/ty/issues/1728. Bivariance is confusing, not useful, and not in the spec. Fall back to covariant when we would otherwise infer bivariance. This is primarily a revival of https://github.com/astral-sh/ruff/pull/23779, with a few preliminary fixes in prior PRs. --------- Co-authored-by: Carl Meyer --- crates/ty_ide/src/hover.rs | 4 +- .../resources/mdtest/annotations/self.md | 3 +- .../resources/mdtest/bidirectional.md | 23 ++--- .../mdtest/generics/pep695/classes.md | 25 ++---- .../mdtest/generics/pep695/typevartuple.md | 7 +- .../mdtest/generics/pep695/variance.md | 90 +++++++++---------- .../resources/mdtest/narrow/type.md | 4 +- .../resources/mdtest/protocols.md | 2 +- .../resources/mdtest/type_of/basic.md | 37 -------- .../resources/mdtest/type_of/generics.md | 11 ++- .../mdtest/type_properties/is_subtype_of.md | 8 -- .../resources/mdtest/union_types.md | 6 -- crates/ty_python_semantic/src/types/tests.rs | 60 ++++++++++++- .../ty_python_semantic/src/types/typevar.rs | 11 ++- 14 files changed, 145 insertions(+), 146 deletions(-) diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index a26550de3f..a7b5f3cd04 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -4272,10 +4272,10 @@ def function(): // TODO: Should this be constravariant instead? assert_snapshot!(test.hover(), @" - P@Alias (bivariant) + P@Alias (covariant) --------------------------------------------- ```python - P@Alias (bivariant) + P@Alias (covariant) ``` --------------------------------------------- info[hover]: Hovered content is diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index b488fceced..f7fdebaf0a 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -413,6 +413,7 @@ class GenericShape[T]: @classmethod def baz[U](cls, u: U) -> "GenericShape[U]": reveal_type(cls) # revealed: type[Self@baz] + # error: [invalid-return-type] return cls() class GenericCircle[T](GenericShape[T]): ... @@ -1091,7 +1092,7 @@ class ExplicitGeneric[T]: ExplicitGeneric[int]().special() -# TODO: this should be an `invalid-argument-type` error +# error: [invalid-argument-type] "Argument to bound method `ExplicitGeneric.special` is incorrect: Expected `ExplicitGeneric[int]`, found `ExplicitGeneric[str]`" ExplicitGeneric[str]().special() ``` diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index 8c8b9801d1..082d908c33 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -704,13 +704,14 @@ def _(): ## Prefer the declared type of generic classes and callables When inferring a generic call, we only use the declared type as type context if it is in -non-covariant position. The final annotated assignment binding still uses the declared type if the -inferred and declared types are mutually assignable: +non-covariant position. Unused type parameters are inferred as covariant. The final annotated +assignment binding still uses the declared type if the inferred and declared types are mutually +assignable: ```py from typing import Any -class Bivariant[T]: +class UnusedTypeParameter[T]: pass class Covariant[T]: @@ -724,8 +725,8 @@ class Contravariant[T]: class Invariant[T]: x: T -def bivariant[T](x: T) -> Bivariant[T]: - return Bivariant() +def unused_type_parameter[T](x: T) -> UnusedTypeParameter[T]: + return UnusedTypeParameter() def covariant[T](x: T) -> Covariant[T]: return Covariant() @@ -736,32 +737,32 @@ def contravariant[T](x: T) -> Contravariant[T]: def invariant[T](x: T) -> Invariant[T]: return Invariant() -x1 = bivariant(1) +x1 = unused_type_parameter(1) x2 = covariant(1) x3 = contravariant(1) x4 = invariant(1) -reveal_type(x1) # revealed: Bivariant[Literal[1]] +reveal_type(x1) # revealed: UnusedTypeParameter[Literal[1]] reveal_type(x2) # revealed: Covariant[Literal[1]] reveal_type(x3) # revealed: Contravariant[int] reveal_type(x4) # revealed: Invariant[int] -x5: Bivariant[int | None] = bivariant(1) +x5: UnusedTypeParameter[int | None] = unused_type_parameter(1) x6: Covariant[int | None] = covariant(1) x7: Contravariant[int | None] = contravariant(1) x8: Invariant[int | None] = invariant(1) -reveal_type(x5) # revealed: Bivariant[int | None] +reveal_type(x5) # revealed: UnusedTypeParameter[Literal[1]] reveal_type(x6) # revealed: Covariant[Literal[1]] reveal_type(x7) # revealed: Contravariant[int | None] reveal_type(x8) # revealed: Invariant[int | None] -x9: Bivariant[Any] = bivariant(1) +x9: UnusedTypeParameter[Any] = unused_type_parameter(1) x10: Covariant[Any] = covariant(1) x11: Contravariant[Any] = contravariant(1) x12: Invariant[Any] = invariant(1) -reveal_type(x9) # revealed: Bivariant[Any] +reveal_type(x9) # revealed: UnusedTypeParameter[Any] reveal_type(x10) # revealed: Covariant[Any] reveal_type(x11) # revealed: Contravariant[Any] reveal_type(x12) # revealed: Invariant[Any] 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 60925489a0..15a03316b8 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -313,17 +313,12 @@ 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. -We have to add `x: T` to the classes to ensure they're not bivariant in `T` (__new__ and __init__ -signatures don't count towards variance). - ### `__new__` only ```py from ty_extensions._internal import generic_context, into_regular_callable class C[T]: - x: T - def __new__(cls, x: T) -> "C[T]": return object.__new__(cls) @@ -332,9 +327,9 @@ reveal_type(generic_context(C)) # revealed: ty_extensions._internal.GenericContext[T@C] reveal_type(generic_context(into_regular_callable(C))) -reveal_type(C(1)) # revealed: C[int] +reveal_type(C(1)) # revealed: C[Literal[1]] -# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") ``` @@ -344,8 +339,6 @@ wrong_innards: C[int] = C("five") from ty_extensions._internal import generic_context, into_regular_callable class C[T]: - x: T - def __init__(self, x: T) -> None: ... # revealed: ty_extensions._internal.GenericContext[T@C] @@ -353,9 +346,9 @@ reveal_type(generic_context(C)) # revealed: ty_extensions._internal.GenericContext[T@C] reveal_type(generic_context(into_regular_callable(C))) -reveal_type(C(1)) # revealed: C[int] +reveal_type(C(1)) # revealed: C[Literal[1]] -# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`" +# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`" wrong_innards: C[int] = C("five") ``` @@ -553,10 +546,6 @@ from typing import overload from ty_extensions._internal import generic_context, into_regular_callable class C[T]: - # we need to use the type variable or else the class is bivariant in T, and - # specializations become meaningless - x: T - @overload def __init__(self: C[str], x: str) -> None: ... @overload @@ -593,10 +582,6 @@ C[None](b"bytes") # error: [no-matching-overload] C[None](12) class D[T, U]: - # we need to use the type variable or else the class is bivariant in T, and - # specializations become meaningless - x: T - @overload def __init__(self: "D[str, U]", u: U) -> None: ... @overload @@ -610,7 +595,7 @@ reveal_type(generic_context(into_regular_callable(D))) reveal_type(D("string")) # revealed: D[str, Literal["string"]] reveal_type(D(1)) # revealed: D[str, Literal[1]] -reveal_type(D(1, "string")) # revealed: D[int, Literal["string"]] +reveal_type(D(1, "string")) # revealed: D[Literal[1], Literal["string"]] ``` ### Synthesized methods with dataclasses diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md index 2f00ed577f..a3919da77f 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md @@ -729,11 +729,11 @@ reveal_type(add_letters(Array[B, D]())) # revealed: Array[A, B, D, C] reveal_type(add_letter_a(Array[B, C]())) # revealed: Array[A, B, C] reveal_type(del_letter_a(Array[A, B]())) # revealed: Array[B] -# TODO: error: [invalid-argument-type] +# error: [invalid-argument-type] "Argument to function `del_letter_a` is incorrect: Expected `Array[A, C]`, found `Array[B, C]`" reveal_type(del_letter_a(Array[B, C]())) # revealed: Array[C] reveal_type(del_letter_c(Array[A, B, C]())) # revealed: Array[A, B] -# TODO: error: [invalid-argument-type] +# error: [invalid-argument-type] "Argument to function `del_letter_c` is incorrect: Expected `Array[A, C]`, found `Array[A, B]`" reveal_type(del_letter_c(Array[A, B]())) # revealed: Array[A] reveal_type(generic(A(), Array[B, D]())) # revealed: Array[A, B, D] @@ -1026,8 +1026,7 @@ class Row[*Cells]: def f(pair: Row[int, str], triple: Row[int, str, bytes]) -> None: reveal_type(pair.get()) # revealed: Row[str, int] - # TODO: Should reveal `Row[str, bytes, int]`. - reveal_type(triple.get()) # revealed: Row[Unknown, Unknown] + reveal_type(triple.get()) # revealed: Row[str, bytes, int] ``` ## Invalid Forms diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md index 623421b8ab..497f1b1741 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md @@ -6,9 +6,11 @@ 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.) +relations. Much more detail can be found in the [spec]. PEP 695 defines inferred variance as +**covariant**, **contravariant**, or **invariant**. We also represent **bivariance** internally, for +cases where varying a type parameter does not change the type. For PEP 695 parameters, we report +these cases as covariant, matching the spec's inference algorithm when assignment is valid in both +directions. For all of the examples below, we will consider typevars `T` and `U`, two generic classes using those typevars `C[T]` and `D[U]`, and two types `A` and `B`. @@ -284,15 +286,10 @@ static_assert(not is_equivalent_to(D[Any], C[Any])) static_assert(not is_equivalent_to(D[Any], C[Unknown])) ``` -## Bivariance +## Bivariant Fallback -With a bivariant typevar, _all_ specializations of the generic class are assignable to (and in fact, -gradually equivalent to) each other, and all specializations are subtypes of (and 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.) +If inference for a PEP 695 type parameter would otherwise conclude bivariance because the type +parameter is unused, we fall back to covariance instead. ```py from ty_extensions import static_assert, Unknown @@ -309,7 +306,7 @@ class D[U](C[U]): pass static_assert(is_assignable_to(C[B], C[A])) -static_assert(is_assignable_to(C[A], C[B])) +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])) @@ -317,37 +314,37 @@ static_assert(is_assignable_to(C[Any], C[B])) static_assert(is_assignable_to(D[B], C[A])) static_assert(is_subtype_of(C[A], C[A])) -static_assert(is_assignable_to(D[A], C[B])) +static_assert(not is_assignable_to(D[A], C[B])) static_assert(is_assignable_to(D[A], C[Any])) static_assert(is_assignable_to(D[B], C[Any])) static_assert(is_assignable_to(D[Any], C[A])) static_assert(is_assignable_to(D[Any], C[B])) static_assert(is_subtype_of(C[B], C[A])) -static_assert(is_subtype_of(C[A], C[B])) -static_assert(is_subtype_of(C[A], C[Any])) -static_assert(is_subtype_of(C[B], C[Any])) -static_assert(is_subtype_of(C[Any], C[A])) -static_assert(is_subtype_of(C[Any], C[B])) -static_assert(is_subtype_of(C[Any], C[Any])) -static_assert(is_subtype_of(C[object], C[Any])) -static_assert(is_subtype_of(C[Any], C[Never])) +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(not is_subtype_of(C[Any], C[Any])) +static_assert(not is_subtype_of(C[object], C[Any])) +static_assert(not is_subtype_of(C[Any], C[Never])) static_assert(is_subtype_of(D[B], C[A])) -static_assert(is_subtype_of(D[A], C[B])) -static_assert(is_subtype_of(D[A], C[Any])) -static_assert(is_subtype_of(D[B], C[Any])) -static_assert(is_subtype_of(D[Any], C[A])) -static_assert(is_subtype_of(D[Any], C[B])) +static_assert(not is_subtype_of(D[A], C[B])) +static_assert(not is_subtype_of(D[A], C[Any])) +static_assert(not is_subtype_of(D[B], C[Any])) +static_assert(not is_subtype_of(D[Any], C[A])) +static_assert(not is_subtype_of(D[Any], C[B])) static_assert(is_equivalent_to(C[A], C[A])) static_assert(is_equivalent_to(C[B], C[B])) -static_assert(is_equivalent_to(C[B], C[A])) -static_assert(is_equivalent_to(C[A], C[B])) -static_assert(is_equivalent_to(C[A], C[Any])) -static_assert(is_equivalent_to(C[B], C[Any])) -static_assert(is_equivalent_to(C[Any], C[A])) -static_assert(is_equivalent_to(C[Any], 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(not is_equivalent_to(D[A], C[A])) static_assert(not is_equivalent_to(D[B], C[B])) @@ -379,11 +376,11 @@ of that instance affect its variance. from ty_extensions import static_assert from ty_extensions._internal import is_subtype_of -class Bivariant[T]: - def takes_int_self(self, value: Bivariant[int]): ... +class WouldBeBivariant[T]: + def takes_int_self(self, value: WouldBeBivariant[int]): ... -static_assert(is_subtype_of(Bivariant[int], Bivariant[object])) -static_assert(is_subtype_of(Bivariant[object], Bivariant[int])) +static_assert(is_subtype_of(WouldBeBivariant[int], WouldBeBivariant[object])) +static_assert(not is_subtype_of(WouldBeBivariant[object], WouldBeBivariant[int])) class Covariant[T]: def get(self) -> T: @@ -767,10 +764,11 @@ class C[T]: def __new__(self, x: T): ... static_assert(is_subtype_of(C[B], C[A])) -static_assert(is_subtype_of(C[A], C[B])) +static_assert(not is_subtype_of(C[A], C[B])) ``` -This example is then bivariant because it doesn't use `T` outside of the two exempted methods. +This example would otherwise be bivariant because it doesn't use `T` outside of the two exempted +methods, so we fall back to covariance. This holds likewise for dataclasses with synthesized `__init__`: @@ -1006,17 +1004,17 @@ static_assert(not is_subtype_of(InvariantLiteral1, InvariantInt)) static_assert(not is_subtype_of(MyInvariant[Literal[1]], MyInvariant[int])) static_assert(not is_subtype_of(MyInvariant[int], MyInvariant[Literal[1]])) -class Bivariant[T]: +class WouldBeBivariant[T]: pass -type BivariantLiteral1 = Bivariant[Literal[1]] -type BivariantInt = Bivariant[int] -type MyBivariant[T] = Bivariant[T] +type WouldBeBivariantLiteral1 = WouldBeBivariant[Literal[1]] +type WouldBeBivariantInt = WouldBeBivariant[int] +type MyWouldBeBivariant[T] = WouldBeBivariant[T] -static_assert(is_subtype_of(BivariantInt, BivariantLiteral1)) -static_assert(is_subtype_of(BivariantLiteral1, BivariantInt)) -static_assert(is_subtype_of(MyBivariant[Literal[1]], MyBivariant[int])) -static_assert(is_subtype_of(MyBivariant[int], MyBivariant[Literal[1]])) +static_assert(not is_subtype_of(WouldBeBivariantInt, WouldBeBivariantLiteral1)) +static_assert(is_subtype_of(WouldBeBivariantLiteral1, WouldBeBivariantInt)) +static_assert(is_subtype_of(MyWouldBeBivariant[Literal[1]], MyWouldBeBivariant[int])) +static_assert(not is_subtype_of(MyWouldBeBivariant[int], MyWouldBeBivariant[Literal[1]])) ``` ## Inheriting from generic classes with inferred variance diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type.md b/crates/ty_python_semantic/resources/mdtest/narrow/type.md index 080e9a03e6..e039974c75 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type.md @@ -212,7 +212,7 @@ def f(x: A[int] | B): reveal_type(x) # revealed: A[int] | B if type(x) is A: - reveal_type(x) # revealed: A[int] + reveal_type(x) # revealed: A[int] | (B & A[object]) else: reveal_type(x) # revealed: A[int] | B @@ -230,7 +230,7 @@ def f(x: A[int] | B): if type(x) is not A: reveal_type(x) # revealed: A[int] | B else: - reveal_type(x) # revealed: A[int] + reveal_type(x) # revealed: A[int] | (B & A[object]) if type(x) is not B: reveal_type(x) # revealed: A[int] | B diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index adaabb4c47..8979965595 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -5766,7 +5766,7 @@ def f(c: C[int]) -> None: # The key thing is that we don't stack overflow while checking this. # The cycle detection assumes compatibility when it detects potential # infinite recursion between protocol specializations. - takes_c(c) + takes_c(c) # error: [invalid-argument-type] class Left[T](Protocol): @property diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md index 00c38b31b5..ef443eb97e 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md @@ -319,8 +319,6 @@ from typing import final, Any from ty_extensions import static_assert from ty_extensions._internal import is_assignable_to, is_subtype_of, is_disjoint_from -class Biv[T]: ... - class Cov[T]: def pop(self) -> T: raise NotImplementedError @@ -332,9 +330,6 @@ class Contra[T]: class Inv[T]: x: T -@final -class BivSub[T](Biv[T]): ... - @final class CovSub[T](Cov[T]): ... @@ -345,9 +340,6 @@ class ContraSub[T](Contra[T]): ... class InvSub[T](Inv[T]): ... def _[T, U](): - static_assert(is_subtype_of(type[BivSub[T]], type[BivSub[U]])) - static_assert(not is_disjoint_from(type[BivSub[U]], type[BivSub[T]])) - # `T` and `U` could specialize to the same type. static_assert(not is_subtype_of(type[CovSub[T]], type[CovSub[U]])) static_assert(not is_disjoint_from(type[CovSub[U]], type[CovSub[T]])) @@ -359,12 +351,6 @@ def _[T, U](): static_assert(not is_disjoint_from(type[InvSub[U]], type[InvSub[T]])) def _(): - static_assert(is_subtype_of(type[BivSub[bool]], type[BivSub[int]])) - static_assert(is_subtype_of(type[BivSub[int]], type[BivSub[bool]])) - static_assert(not is_disjoint_from(type[BivSub[bool]], type[BivSub[int]])) - # `BivSub[int]` and `BivSub[str]` are mutual subtypes. - static_assert(not is_disjoint_from(type[BivSub[int]], type[BivSub[str]])) - static_assert(is_subtype_of(type[CovSub[bool]], type[CovSub[int]])) static_assert(not is_subtype_of(type[CovSub[int]], type[CovSub[bool]])) static_assert(not is_disjoint_from(type[CovSub[bool]], type[CovSub[int]])) @@ -383,12 +369,6 @@ def _(): static_assert(is_disjoint_from(type[InvSub[bool]], type[InvSub[int]])) def _[T](): - static_assert(is_subtype_of(type[BivSub[T]], type[BivSub[Any]])) - static_assert(is_subtype_of(type[BivSub[Any]], type[BivSub[T]])) - static_assert(is_assignable_to(type[BivSub[T]], type[BivSub[Any]])) - static_assert(is_assignable_to(type[BivSub[Any]], type[BivSub[T]])) - static_assert(not is_disjoint_from(type[BivSub[T]], type[BivSub[Any]])) - static_assert(not is_subtype_of(type[CovSub[T]], type[CovSub[Any]])) static_assert(not is_subtype_of(type[CovSub[Any]], type[CovSub[T]])) static_assert(is_assignable_to(type[CovSub[T]], type[CovSub[Any]])) @@ -408,12 +388,6 @@ def _[T](): static_assert(not is_disjoint_from(type[InvSub[T]], type[InvSub[Any]])) def _[T, U](): - static_assert(is_subtype_of(type[BivSub[T]], type[Biv[T]])) - static_assert(not is_subtype_of(type[Biv[T]], type[BivSub[T]])) - static_assert(not is_disjoint_from(type[BivSub[T]], type[Biv[T]])) - static_assert(not is_disjoint_from(type[BivSub[U]], type[Biv[T]])) - static_assert(not is_disjoint_from(type[BivSub[U]], type[Biv[U]])) - static_assert(is_subtype_of(type[CovSub[T]], type[Cov[T]])) static_assert(not is_subtype_of(type[Cov[T]], type[CovSub[T]])) static_assert(not is_disjoint_from(type[CovSub[T]], type[Cov[T]])) @@ -433,11 +407,6 @@ def _[T, U](): static_assert(not is_disjoint_from(type[InvSub[U]], type[Inv[U]])) def _(): - static_assert(is_subtype_of(type[BivSub[bool]], type[Biv[int]])) - static_assert(is_subtype_of(type[BivSub[int]], type[Biv[bool]])) - static_assert(not is_disjoint_from(type[BivSub[bool]], type[Biv[int]])) - static_assert(not is_disjoint_from(type[BivSub[int]], type[Biv[bool]])) - static_assert(is_subtype_of(type[CovSub[bool]], type[Cov[int]])) static_assert(not is_subtype_of(type[CovSub[int]], type[Cov[bool]])) static_assert(not is_disjoint_from(type[CovSub[bool]], type[Cov[int]])) @@ -454,12 +423,6 @@ def _(): static_assert(is_disjoint_from(type[InvSub[int]], type[Inv[bool]])) def _[T](): - static_assert(is_subtype_of(type[BivSub[T]], type[Biv[Any]])) - static_assert(is_subtype_of(type[BivSub[Any]], type[Biv[T]])) - static_assert(is_assignable_to(type[BivSub[T]], type[Biv[Any]])) - static_assert(is_assignable_to(type[BivSub[Any]], type[Biv[T]])) - static_assert(not is_disjoint_from(type[BivSub[T]], type[Biv[Any]])) - static_assert(not is_subtype_of(type[CovSub[T]], type[Cov[Any]])) static_assert(not is_subtype_of(type[CovSub[Any]], type[Cov[T]])) static_assert(is_assignable_to(type[CovSub[T]], type[Cov[Any]])) diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md index d434e974e4..37be37f7f7 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md @@ -601,9 +601,11 @@ expects_type_c_of_int_and_str(C) # Also OK, the specialized `C[int, str]` is assignable to `type[C[int, str]]` expects_type_c_of_int_and_str(C[int, str]) -# TODO: these should be errors +# error: [invalid-argument-type] expects_type_c_of_int_and_str(C[str]) +# error: [invalid-argument-type] expects_type_c_of_int_and_str(C[int, str, bytes]) +# error: [invalid-argument-type] expects_type_c_of_int_and_str(C[str, int]) ``` @@ -619,14 +621,17 @@ def expects_type_c_default_of_int_str(f: type[C[int, str]]): ... expects_type_c_default(C) expects_type_c_default(C[int, str]) -expects_type_c_default_of_int(C) expects_type_c_default_of_int(C[int]) expects_type_c_default_of_int_str(C) expects_type_c_default_of_int_str(C[int, str]) -# TODO: these should be errors +# error: [invalid-argument-type] expects_type_c_default(C[int]) +# error: [invalid-argument-type] +expects_type_c_default_of_int(C) +# error: [invalid-argument-type] expects_type_c_default_of_int(C[str]) +# error: [invalid-argument-type] expects_type_c_default_of_int_str(C[str, int]) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index e128771c27..bb901fe2da 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -931,14 +931,6 @@ static_assert(not is_subtype_of(Invariant[Any], Invariant[int])) static_assert(not is_subtype_of(Invariant[int], Invariant[Any])) static_assert(not is_subtype_of(Invariant[Any], Invariant[object])) static_assert(not is_subtype_of(Invariant[object], Invariant[Any])) - -class Bivariant[T]: ... - -static_assert(is_subtype_of(Bivariant[Any], Bivariant[Any])) -static_assert(is_subtype_of(Bivariant[Any], Bivariant[int])) -static_assert(is_subtype_of(Bivariant[int], Bivariant[Any])) -static_assert(is_subtype_of(Bivariant[Any], Bivariant[object])) -static_assert(is_subtype_of(Bivariant[object], Bivariant[Any])) ``` The same for `Unknown`: diff --git a/crates/ty_python_semantic/resources/mdtest/union_types.md b/crates/ty_python_semantic/resources/mdtest/union_types.md index 2825a7e032..0c9016d19b 100644 --- a/crates/ty_python_semantic/resources/mdtest/union_types.md +++ b/crates/ty_python_semantic/resources/mdtest/union_types.md @@ -348,8 +348,6 @@ python-version = "3.12" ```py from typing import Any -class Bivariant[T]: ... - class Covariant[T]: def get(self) -> T: raise NotImplementedError @@ -361,8 +359,6 @@ class Invariant[T]: mutable_attribute: T def _( - a: Bivariant[Any] | Bivariant[Any | str], - b: Bivariant[Any | str] | Bivariant[Any], c: Covariant[Any] | Covariant[Any | str], d: Covariant[Any | str] | Covariant[Any], e: Contravariant[Any | str] | Contravariant[Any], @@ -370,8 +366,6 @@ def _( g: Invariant[Any] | Invariant[Any | str], h: Invariant[Any | str] | Invariant[Any], ): - reveal_type(a) # revealed: Bivariant[Any] - reveal_type(b) # revealed: Bivariant[Any | str] reveal_type(c) # revealed: Covariant[Any | str] reveal_type(d) # revealed: Covariant[Any | str] reveal_type(e) # revealed: Contravariant[Any] diff --git a/crates/ty_python_semantic/src/types/tests.rs b/crates/ty_python_semantic/src/types/tests.rs index 22b9f51137..7ca4c4726f 100644 --- a/crates/ty_python_semantic/src/types/tests.rs +++ b/crates/ty_python_semantic/src/types/tests.rs @@ -357,12 +357,28 @@ fn type_alias_variance() { }; type_alias } + fn get_bound_typevar_instance<'db>( + db: &'db TestDb, + type_alias: PEP695TypeAliasType<'db>, + ) -> BoundTypeVarInstance<'db> { + let generic_context = type_alias.generic_context(db).unwrap(); + generic_context.variables(db).next().unwrap() + } + fn get_bound_typevar<'db>( db: &'db TestDb, type_alias: PEP695TypeAliasType<'db>, ) -> BoundTypeVarIdentity<'db> { - let generic_context = type_alias.generic_context(db).unwrap(); - generic_context.variables(db).next().unwrap().identity(db) + get_bound_typevar_instance(db, type_alias).identity(db) + } + + fn assert_effective_variance<'db>( + db: &'db TestDb, + type_alias: PEP695TypeAliasType<'db>, + expected: TypeVarVariance, + ) { + let typevar = get_bound_typevar_instance(db, type_alias); + assert_eq!(typevar.variance(db), expected); } let mut db = setup_db(); @@ -397,6 +413,7 @@ type ContravariantAliasAlias[T] = ContravariantAlias[T] type InvariantAliasAlias[T] = InvariantAlias[T] type BivariantAliasAlias[T] = BivariantAlias[T] type ParamSpecContravariantAlias[**P] = Callable[P, None] +type ParamSpecDefaultContravariantAlias[**P = [int, str]] = Callable[P, None] type ParamSpecConcatenateAlias[**P] = Callable[Concatenate[int, P], None] type ParamSpecBivariantAlias[**P] = int @@ -468,6 +485,13 @@ type RecursiveAlias2[T] = None | list[T] | list[RecursiveAlias2[T]] TypeVarVariance::Contravariant ); + let paramspec_default_contravariant = get_type_alias(&db, "ParamSpecDefaultContravariantAlias"); + assert_eq!( + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_default_contravariant)) + .variance_of(&db, get_bound_typevar(&db, paramspec_default_contravariant)), + TypeVarVariance::Contravariant + ); + let paramspec_concatenate = get_type_alias(&db, "ParamSpecConcatenateAlias"); assert_eq!( KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_concatenate)) @@ -495,6 +519,38 @@ type RecursiveAlias2[T] = None | list[T] | list[RecursiveAlias2[T]] .variance_of(&db, get_bound_typevar(&db, recursive2)), TypeVarVariance::Invariant ); + + assert_effective_variance(&db, covariant, TypeVarVariance::Covariant); + assert_effective_variance(&db, contravariant, TypeVarVariance::Contravariant); + assert_effective_variance(&db, invariant, TypeVarVariance::Invariant); + assert_effective_variance(&db, bivariant, TypeVarVariance::Covariant); + assert_effective_variance(&db, covariant_alias, TypeVarVariance::Covariant); + assert_effective_variance(&db, contravariant_alias, TypeVarVariance::Contravariant); + assert_effective_variance(&db, invariant_alias, TypeVarVariance::Invariant); + assert_effective_variance(&db, bivariant_alias, TypeVarVariance::Covariant); + assert_effective_variance(&db, paramspec_contravariant, TypeVarVariance::Contravariant); + assert_effective_variance( + &db, + paramspec_default_contravariant, + TypeVarVariance::Contravariant, + ); + assert_effective_variance(&db, paramspec_concatenate, TypeVarVariance::Contravariant); + assert_effective_variance(&db, paramspec_bivariant, TypeVarVariance::Covariant); + assert_effective_variance(&db, recursive, TypeVarVariance::Covariant); + assert_effective_variance(&db, recursive2, TypeVarVariance::Invariant); + + let bivariant_typevar = get_bound_typevar_instance(&db, bivariant); + for polarity in [ + TypeVarVariance::Covariant, + TypeVarVariance::Contravariant, + TypeVarVariance::Invariant, + TypeVarVariance::Bivariant, + ] { + assert_eq!( + bivariant_typevar.variance_with_polarity(&db, polarity), + polarity + ); + } } #[test] diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index 2bc2b3edef..44653c4d8e 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -1102,12 +1102,17 @@ impl<'db> BoundTypeVarInstance<'db> { polarity: TypeVarVariance, ) -> TypeVarVariance { let _span = tracing::trace_span!("variance_with_polarity").entered(); + match self.typevar(db).explicit_variance(db) { Some(explicit_variance) => explicit_variance.compose(polarity), None => match self.binding_context(db) { - BindingContext::Definition(definition) => binding_type(db, definition) - .with_polarity(polarity) - .variance_of(db, self.identity(db)), + BindingContext::Definition(definition) => polarity.compose_thunk(|| { + match binding_type(db, definition).variance_of(db, self.identity(db)) { + // When both directions are valid, the typing spec selects covariance. + TypeVarVariance::Bivariant => TypeVarVariance::Covariant, + variance => variance, + } + }), BindingContext::Synthetic => TypeVarVariance::Invariant, }, } From 39f006c761884ca9ed14304ead4d55d50ac6aaf9 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:47:50 +0900 Subject: [PATCH 064/390] [ty] Avoid cycle when resolving `ModuleType` globals (#27182) ## Summary This PR provides a natural expectation that salsa cycle events will not fire for trivial scripts. ## Test Plan --- crates/ty_python_semantic/src/place.rs | 115 +++++++++++++----- .../src/types/infer/tests.rs | 31 +++-- 2 files changed, 108 insertions(+), 38 deletions(-) diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 201d34cdb9..7e3e81a26d 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -1989,21 +1989,81 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { pub(crate) mod implicit_globals { use ruff_db::files::File; + use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_python_ast::name::Name; + use ty_module_resolver::KnownModule; use crate::Program; use crate::db::Db; use crate::module_docstring; use crate::place::{Definedness, PlaceAndQualifiers}; - use crate::types::{ - ClassLiteral, KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type, - }; + use crate::reachability::evaluate_reachability; + use crate::types::{KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type}; use ruff_python_ast::PythonVersion; + use ty_python_core::definition::{DefinitionKind, DefinitionState}; + use ty_python_core::scope::{NodeWithScopeRef, ScopeId}; use ty_python_core::symbol::Symbol; - use ty_python_core::{place_table, use_def_map}; + use ty_python_core::{place_table, semantic_index, use_def_map}; + + use super::{DefinedPlace, Place, core_module_scope, is_reexported, place_from_declarations}; + + /// Returns the body scope when all reachable, exported definitions of `name` + /// in a vendored module are the same direct class definition. + /// + /// This can be used as a fast-path to avoid query cycles. + fn try_vendored_class_scope<'db>( + db: &'db dyn Db, + module_scope: ScopeId<'db>, + name: &str, + ) -> Option> { + let file = module_scope.file(db); + if !file.path(db).is_vendored_path() { + return None; + } + let symbol_id = place_table(db, module_scope).symbol_id(name)?; + let use_def = use_def_map(db, module_scope); + let module = parsed_module(db, file).load(db); + let index = semantic_index(db, file); + let mut body_scope = None; + + for binding in use_def.end_of_scope_symbol_bindings(symbol_id) { + let DefinitionState::Defined(definition) = binding.binding else { + continue; + }; + if file.is_stub(db) && !is_reexported(db, definition) { + continue; + } + if evaluate_reachability(db, use_def, binding.reachability_constraint).is_always_false() + { + continue; + } - use super::{DefinedPlace, Place, place_from_declarations}; + let DefinitionKind::Class(class) = definition.kind(db) else { + return None; + }; + let class_scope = index + .node_scope(NodeWithScopeRef::Class(class.node(&module))) + .to_scope_id(db, file); + if body_scope.is_some_and(|body_scope| body_scope != class_scope) { + return None; + } + body_scope = Some(class_scope); + } + + body_scope + } + + /// Return the body scope of the canonical `types.ModuleType` class. + #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] + fn module_type_body_scope(db: &dyn Db) -> Option> { + let module_scope = core_module_scope(db, KnownModule::Types)?; + try_vendored_class_scope(db, module_scope, "ModuleType").or_else(|| { + KnownClass::ModuleType + .try_to_class_literal(db) + .map(|class| class.body_scope(db)) + }) + } pub(crate) fn module_type_implicit_global_declaration<'db>( db: &'db dyn Db, @@ -2015,14 +2075,9 @@ pub(crate) mod implicit_globals { { return Place::Undefined.into(); } - let Type::ClassLiteral(module_type_class) = KnownClass::ModuleType.to_class_literal(db) - else { - return Place::Undefined.into(); - }; - let Some(class) = module_type_class.as_static() else { + let Some(module_type_scope) = module_type_body_scope(db) else { return Place::Undefined.into(); }; - let module_type_scope = class.body_scope(db); let place_table = place_table(db, module_type_scope); let Some(symbol_id) = place_table.symbol_id(name) else { return Place::Undefined.into(); @@ -2141,25 +2196,11 @@ pub(crate) mod implicit_globals { /// Conceptually this function could be a `Set` rather than a list, /// but the number of symbols declared in this scope is likely to be very small, /// so the cost of hashing the names is likely to be more expensive than it's worth. - #[salsa::tracked( - returns(deref), - cycle_initial=|_, _| smallvec::SmallVec::default(), - heap_size=ruff_memory_usage::heap_size - )] - fn module_type_symbols(db: &dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> { - let Some(module_type) = KnownClass::ModuleType - .to_class_literal(db) - .as_class_literal() - else { - // The most likely way we get here is if a user specified a `--custom-typeshed-dir` - // without a `types.pyi` stub in the `stdlib/` directory - return smallvec::SmallVec::default(); - }; - - let ClassLiteral::Static(module_type) = module_type else { - return smallvec::SmallVec::default(); - }; - let module_type_symbol_table = place_table(db, module_type.body_scope(db)); + fn module_type_symbols_from_scope( + db: &dyn Db, + module_type_scope: ScopeId<'_>, + ) -> smallvec::SmallVec<[ast::name::Name; 8]> { + let module_type_symbol_table = place_table(db, module_type_scope); module_type_symbol_table .symbols() @@ -2175,6 +2216,20 @@ pub(crate) mod implicit_globals { .collect() } + #[salsa::tracked( + returns(deref), + cycle_initial=|_, _| smallvec::SmallVec::default(), + heap_size=ruff_memory_usage::heap_size + )] + fn module_type_symbols(db: &dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> { + let Some(module_type_scope) = module_type_body_scope(db) else { + // The most likely way we get here is if a user specified a `--custom-typeshed-dir` + // without a resolvable `ModuleType` class in the `stdlib/types.pyi` stub. + return smallvec::SmallVec::default(); + }; + module_type_symbols_from_scope(db, module_type_scope) + } + /// Returns an iterator over all implicit module global symbols and their types. /// /// This is used for completions in the global scope of a module. It returns diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index 7681e1c8c0..9a4ccf1dd8 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -333,20 +333,35 @@ fn pep695_type_params() { check_typevar("Y", "TypeVar", None, None, None); } +#[test] +fn simple_assignment_does_not_enter_salsa_cycle() { + let mut db = setup_db(); + db.write_dedented("src/a.py", "x = 1; y = x + 1").unwrap(); + + assert_file_diagnostics(&db, "src/a.py", &[]); + + let events = db.take_salsa_events(); + let cycles = salsa::attach(&db, || { + events + .iter() + .filter_map(|event| { + if let salsa::EventKind::WillIterateCycle { database_key, .. } = event.kind { + Some(format!("{database_key:?}")) + } else { + None + } + }) + .collect::>() + }); + assert_eq!(cycles, Vec::::new()); +} + /// Test that a symbol known to be unbound in a scope does not still trigger cycle-causing /// reachability-constraint checks in that scope. #[test] fn unbound_symbol_no_reachability_constraint_check() { let mut db = setup_db(); - // First, type-check a random other file so that we cache a result for the `module_type_symbols` - // query (which often encounters cycles due to `types.pyi` importing `typing_extensions` and - // `typing_extensions.pyi` importing `types`). Clear the events afterwards so that unrelated - // cycles from that query don't interfere with our test. - db.write_dedented("src/wherever.py", "print(x)").unwrap(); - assert_file_diagnostics(&db, "src/wherever.py", &["Name `x` used when not defined"]); - db.clear_salsa_events(); - // If the bug we are testing for is not fixed, what happens is that when inferring the // `flag: bool = True` definitions, we look up `bool` as a deferred name (thus from end of // scope), and because of the early return its "unbound" binding has a reachability From 53958952ac32ce18d8d97dbbd9b865e13f1c709c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Sat, 25 Jul 2026 21:23:51 +0500 Subject: [PATCH 065/390] [`ruff`] Drop `ruff-ecosystem` override for `apache/airflow` (#27174) ## Summary This reverts commit dd16d689abd9d0fa1caf4316e70479fd422b6142 (#23921), since issue upstream is fixed in https://github.com/apache/airflow/pull/64920 and workaround is no longer needed. https://patch-diff.githubusercontent.com/raw/apache/airflow/pull/64920.diff ```diff diff --git a/task-sdk/src/airflow/sdk/_shared/AGENTS.md b/task-sdk/src/airflow/sdk/_shared/AGENTS.md index 8d2c7cf9d3367..57a3a75046e45 120000 --- a/task-sdk/src/airflow/sdk/_shared/AGENTS.md +++ b/task-sdk/src/airflow/sdk/_shared/AGENTS.md @@ -1 +1 @@ -../../../../../airflow-core/src/airflow/_shared/AGENTS..md \ No newline at end of file +../../../../../airflow-core/src/airflow/_shared/AGENTS.md \ No newline at end of file ``` So workaround is no longer needed. ## Test Plan `ruff-ecosystem` runs without issues. --- python/ruff-ecosystem/ruff_ecosystem/defaults.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/python/ruff-ecosystem/ruff_ecosystem/defaults.py b/python/ruff-ecosystem/ruff_ecosystem/defaults.py index a5dc724b19..58ce1c8a7d 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/defaults.py +++ b/python/ruff-ecosystem/ruff_ecosystem/defaults.py @@ -23,12 +23,6 @@ Project( repo=Repository(owner="apache", name="airflow", ref="main"), check_options=CheckOptions(select="ALL"), - config_overrides={ - # Broken symlink - "exclude": [ - "task-sdk/src/airflow/sdk/_shared/AGENTS.md", - ] - }, ), Project( repo=Repository(owner="apache", name="superset", ref="master"), From 03f3c04d1b9f900b09986b2bd86c50ffc7f92f22 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Sat, 25 Jul 2026 21:24:01 +0500 Subject: [PATCH 066/390] [`ruff`] reintroduce `sphinx-doc/sphinx` to `ruff-ecosystem` (#27175) ## Summary Previously disabled in https://github.com/astral-sh/ruff/pull/9854 Not sure which preview rule was the blocker exactly, but apparently it's now stabilized and we can use `sphinx` in `ruff-ecosystem`. ## Test Plan I ran `ruff-ecosystem` locally with and without `--force-preview` and it worked without issues. --- .../ruff-ecosystem/ruff_ecosystem/defaults.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/python/ruff-ecosystem/ruff_ecosystem/defaults.py b/python/ruff-ecosystem/ruff_ecosystem/defaults.py index 58ce1c8a7d..14b7787090 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/defaults.py +++ b/python/ruff-ecosystem/ruff_ecosystem/defaults.py @@ -82,19 +82,17 @@ Project( repo=Repository(owner="scikit-build", name="scikit-build-core", ref="main") ), - # TODO(charlie): Ecosystem check fails in non-preview due to the direct - # selection of preview rules. - # Project( - # repo=Repository( - # owner="sphinx-doc", - # name="sphinx", - # ref="master", - # ), - # format_options=FormatOptions( - # # Does not contain valid UTF-8 - # exclude="tests/roots/test-pycode/cp_1251_coded.py" - # ), - # ), + Project( + repo=Repository( + owner="sphinx-doc", + name="sphinx", + ref="master", + ), + format_options=FormatOptions( + # Does not contain valid UTF-8 + exclude="tests/roots/test-pycode/cp_1251_coded.py" + ), + ), Project(repo=Repository(owner="spruceid", name="siwe-py", ref="main")), Project(repo=Repository(owner="tiangolo", name="fastapi", ref="master")), Project(repo=Repository(owner="yandex", name="ch-backup", ref="main")), From 5cc6fb1dd91a085f56caa557c133052e60beeeb0 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 25 Jul 2026 09:30:42 -0700 Subject: [PATCH 067/390] [ty] Avoid exponential inference when copying mixed TypedDict unions (#27108) ## Summary Prior to this change, we handled unions of `TypedDict` instances efficiently when inferring the type of `dict(value)`. But adding a single ordinary dictionary to the union disabled that optimization: ```python class A(TypedDict): kind: Literal["a"] class B(TypedDict): kind: Literal["b"] def copy(value: A | B | dict[str, Any]) -> None: dict(value) ``` For sufficiently large unions, combining the protocol constraints for each member became exponential. In #27096, we tried to replace the constraints for the `TypedDict` members with the shared constraints for `Mapping[str, object]`, then combine the remaining union members separately. This fixes the performance issue, but assumes that constraints that are equivalent for assignability also produce the same inferred solutions. Turns out that assumption doesn;t hold in general. For example, a `TypedDict` with additional `Any` items must retain that information: ```python class Extras(TypedDict, extra_items=Any): ... def copy(value: Extras | dict[str, str]) -> None: reveal_type(dict(value)) # dict[str, Any | str] ``` Unconditionally replacing `Extras` with `Mapping[str, object]` would instead infer `dict[str, object]`. Similar problems arise with bounded type variables, custom protocols, and intersections introduced by narrowing. This PR applies the same optimization, but only when it is safe to reuse the `TypedDict` constraints. We require both equivalent constraints and equivalent inferred solutions, preserve the original constraints for ordinary mapping members, and limit the optimization to the `SupportsKeysAndGetItem` protocol used by dictionary constructors. We also retain the optimization after `isinstance(value, dict)`: ```python def copy(value: A | B | dict[str, Any]) -> None: if isinstance(value, dict): dict(value) ``` --------- Co-authored-by: David Zbarsky --- crates/ruff_benchmark/benches/ty.rs | 74 ++++++++++ .../resources/mdtest/typed_dict.md | 125 +++++++++++++++- .../src/types/class/known.rs | 27 +++- .../ty_python_semantic/src/types/generics.rs | 137 +++++++++++++++--- 4 files changed, 342 insertions(+), 21 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index c2b5570a4c..08834524a2 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -1542,6 +1542,79 @@ fn benchmark_pandas_tdd(criterion: &mut Criterion) { }); } +fn benchmark_mixed_typed_dict_union_copy(criterion: &mut Criterion) { + const NUM_VARIANTS: usize = 12; + + setup_rayon(); + + let mut code = concat!( + "from collections import ChainMap, OrderedDict, defaultdict\n", + "from collections.abc import Mapping, MutableMapping\n", + "from typing import Any, Literal, TypedDict\n\n", + ) + .to_string(); + + for i in 0..NUM_VARIANTS { + writeln!( + &mut code, + "class Item{i}(TypedDict):\n type: Literal[{i}]" + ) + .ok(); + if i == 0 { + code.push_str(" other: Any\n"); + } + code.push('\n'); + } + + code.push_str("type Item = "); + for i in 0..NUM_VARIANTS { + if i > 0 { + code.push_str(" | "); + } + write!(&mut code, "Item{i}").ok(); + } + + code.push_str( + r#" + +def copy_dict(value: Item | dict[str, Any]) -> dict[str, object]: + return dict(value) + +def copy_mapping(value: Item | Mapping[str, Any]) -> dict[str, object]: + return dict(value) + +def copy_mutable_mapping(value: Item | MutableMapping[str, Any]) -> dict[str, object]: + return dict(value) + +def copy_ordered_dict(value: Item | OrderedDict[str, Any]) -> dict[str, object]: + return dict(value) + +def copy_default_dict(value: Item | defaultdict[str, Any]) -> dict[str, object]: + return dict(value) + +def copy_chain_map(value: Item | ChainMap[str, Any]) -> dict[str, object]: + return dict(value) + +def copy_narrowed_mapping(value: Item | Mapping[str, Any]) -> dict[str, object] | None: + if isinstance(value, dict): + return dict(value) + return None +"#, + ); + + criterion.bench_function("ty_micro[mixed_typed_dict_union_copy]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db, .. } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + fn benchmark_recursive_typed_dict_union_contextual_inference(criterion: &mut Criterion) { const NUM_BRANCHES: usize = 11; @@ -1978,6 +2051,7 @@ criterion_group!( benchmark_repeated_statement_calls, benchmark_factored_upper_bounds, benchmark_pandas_tdd, + benchmark_mixed_typed_dict_union_copy, benchmark_recursive_typed_dict_union_contextual_inference, benchmark_invariant_generic_return_union, benchmark_invariant_generic_union_bound, diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index f94d8e2d9b..ba010f468a 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -2610,7 +2610,9 @@ python-version = "3.12" ``` ```py -from typing import Literal, TypedDict +from collections import ChainMap, OrderedDict, defaultdict +from collections.abc import Mapping, MutableMapping +from typing import Any, Literal, TypedDict A = TypedDict("A", {"type": Literal["a"]}) B = TypedDict("B", {"type": Literal["b"]}) @@ -2648,7 +2650,58 @@ def _(item: Item) -> None: def _(item: Item | str) -> None: if isinstance(item, dict): reveal_type(dict(item)) # revealed: dict[str, object] +``` + +Adding a regular dictionary to the union should not make copying it slow: + +```py +def _(item: Item | dict[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] + if isinstance(item, dict): + reveal_type(dict(item)) # revealed: dict[str, object] +``` + +An unrelated `Any` field on a `TypedDict` should not disable this optimization: + +```py +class ItemWithAny(TypedDict): + type: Literal["any"] + other: Any + +def _(item: Item | ItemWithAny | dict[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] +``` + +`Mapping`, `MutableMapping`, and other standard-library mappings should also be copied efficiently: + +```py +def _(item: Item | Mapping[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] +def _(item: Item | MutableMapping[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] + +def _(item: Item | OrderedDict[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] + +def _(item: Item | defaultdict[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] + +def _(item: Item | ChainMap[str, Any]) -> None: + reveal_type(dict(item)) # revealed: dict[str, object] +``` + +A mapping should still be copied efficiently after `isinstance()` narrows it to a dictionary: + +```py +def _(item: Item | Mapping[str, Any]) -> None: + if isinstance(item, dict): + reveal_type(dict(item)) # revealed: dict[str, object] +``` + +The union can also be assembled from type aliases: + +```py type FirstGroup = A | B | C | D | E | F | G | H type SecondGroup = I | J | K | L | M | N | O | P type AliasedItem = FirstGroup | SecondGroup | Q | R | S | T | U | V | W | X @@ -2762,6 +2815,76 @@ def _(value: ClearA | ClearB) -> None: reveal_type(clear_result(value)) # revealed: None ``` +An `isinstance()` check against a protocol can establish that `__getitem__()` returns `Any`. That +return type must be preserved for unions containing a `TypedDict`: + +```py +from typing import Any, Literal, Protocol, TypeVar, TypedDict, runtime_checkable + +ValueT = TypeVar("ValueT", covariant=True) + +class GetValue(Protocol[ValueT]): + def __getitem__(self, key: Literal["value"], /) -> ValueT: ... + +class StringValue(TypedDict): + value: str + +@runtime_checkable +class GetAnyValue(Protocol): + def __getitem__(self, key: Literal["value"], /) -> Any: ... + +def get_value(value: GetValue[ValueT]) -> ValueT: + raise NotImplementedError + +def _(value: StringValue | dict[str, Any]) -> None: + if isinstance(value, GetAnyValue): + reveal_type(get_value(value)) # revealed: Any +``` + +The same `Any` result must remain valid when the mapping protocol uses a bounded type variable: + +```py +from _typeshed import SupportsKeysAndGetItem +from collections.abc import Iterable + +BoundedValueT = TypeVar("BoundedValueT", bound=str) + +@runtime_checkable +class AnyValueMapping(Protocol): + def keys(self) -> Iterable[str]: ... + def __getitem__(self, key: str, /) -> Any: ... + +def get_bounded_mapping(value: SupportsKeysAndGetItem[str, BoundedValueT]) -> BoundedValueT: + raise NotImplementedError + +def _(value: StringValue | dict[str, Any]) -> None: + if isinstance(value, AnyValueMapping): + reveal_type(get_bounded_mapping(value)) # revealed: Any +``` + +A `TypedDict` that permits extra items of type `Any` keeps that type when copied: + +```py +from typing_extensions import TypedDict as ExtensionsTypedDict + +class AnyExtraItems(ExtensionsTypedDict, extra_items=Any): ... + +def _(value: AnyExtraItems | dict[str, str]) -> None: + reveal_type(dict(value)) # revealed: dict[str, Any | str] +``` + +A union of two such `TypedDict`s must also preserve `Any` when copied or passed to a mapping +protocol with a bounded type variable: + +```py +class OtherAnyExtraItems(ExtensionsTypedDict, extra_items=Any): ... + +def _(value: AnyExtraItems | OtherAnyExtraItems) -> None: + reveal_type(dict(value)) # revealed: dict[str, Any] + dict(value)["x"].strip() + reveal_type(get_bounded_mapping(value)) # revealed: Any +``` + Rejected common-constraint probes must not affect fallback protocol inference: ```py diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs index 17bba090de..490d647449 100644 --- a/crates/ty_python_semantic/src/types/class/known.rs +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -95,6 +95,7 @@ pub enum KnownClass { EllipsisType, // Typeshed NoneType, // Part of `types` for Python >= 3.10 + SupportsKeysAndGetItem, // Typing Awaitable, Generator, @@ -121,6 +122,7 @@ pub enum KnownClass { AsyncIterator, Sequence, Mapping, + MutableMapping, // typing_extensions ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features ExtensionTypedDictFallback, @@ -268,6 +270,8 @@ impl KnownClass { | Self::AsyncIterator | Self::Sequence | Self::Mapping + | Self::MutableMapping + | Self::SupportsKeysAndGetItem // 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) @@ -382,6 +386,8 @@ impl KnownClass { | KnownClass::AsyncIterator | KnownClass::Sequence | KnownClass::Mapping + | KnownClass::MutableMapping + | KnownClass::SupportsKeysAndGetItem | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict @@ -494,6 +500,8 @@ impl KnownClass { | KnownClass::AsyncIterator | KnownClass::Sequence | KnownClass::Mapping + | KnownClass::MutableMapping + | KnownClass::SupportsKeysAndGetItem | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict @@ -607,6 +615,8 @@ impl KnownClass { | KnownClass::AsyncIterator | KnownClass::Sequence | KnownClass::Mapping + | KnownClass::MutableMapping + | KnownClass::SupportsKeysAndGetItem | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict @@ -653,6 +663,7 @@ impl KnownClass { match self { Self::Hashable | Self::SupportsIndex + | Self::SupportsKeysAndGetItem | Self::Iterable | Self::TyExtensionsAsyncIterable | Self::TyExtensionsAsyncIterator @@ -752,6 +763,7 @@ impl KnownClass { | Self::Path | Self::FunctoolsPartial | Self::Mapping + | Self::MutableMapping | Self::Sequence | Self::PydanticBaseModel | Self::PydanticBaseSettings @@ -850,6 +862,8 @@ impl KnownClass { | KnownClass::AsyncIterator | KnownClass::Sequence | KnownClass::Mapping + | KnownClass::MutableMapping + | KnownClass::SupportsKeysAndGetItem | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict @@ -921,6 +935,7 @@ impl KnownClass { Self::AsyncGeneratorType => "AsyncGeneratorType", Self::CoroutineType => "CoroutineType", Self::NoneType => "NoneType", + Self::SupportsKeysAndGetItem => "SupportsKeysAndGetItem", Self::SpecialForm => "_SpecialForm", Self::TypeVar => "TypeVar", Self::ExtensionsTypeVar => "TypeVar", @@ -968,6 +983,7 @@ impl KnownClass { Self::AsyncIterator => "AsyncIterator", Self::Sequence => "Sequence", Self::Mapping => "Mapping", + Self::MutableMapping => "MutableMapping", // For example, `typing.List` is defined as `List = _Alias()` in typeshed Self::StdlibAlias => "_Alias", // This is the name the type of `sys.version_info` has in typeshed, @@ -1323,7 +1339,7 @@ impl KnownClass { | Self::EllipsisType | Self::NotImplementedType | Self::WrapperDescriptorType => KnownModule::Types, - Self::NoneType => KnownModule::Typeshed, + Self::NoneType | Self::SupportsKeysAndGetItem => KnownModule::Typeshed, Self::Awaitable | Self::Generator | Self::AsyncGenerator @@ -1335,6 +1351,7 @@ impl KnownClass { | Self::AsyncIterator | Self::Sequence | Self::Mapping + | Self::MutableMapping | Self::ProtocolMeta | Self::ParamSpec | Self::Hashable @@ -1493,6 +1510,8 @@ impl KnownClass { | Self::AsyncIterator | Self::Sequence | Self::Mapping + | Self::MutableMapping + | Self::SupportsKeysAndGetItem | Self::NamedTupleFallback | Self::NamedTupleLike | Self::ConstraintSet @@ -1611,6 +1630,8 @@ impl KnownClass { | Self::AsyncIterator | Self::Sequence | Self::Mapping + | Self::MutableMapping + | Self::SupportsKeysAndGetItem | Self::NamedTupleFallback | Self::NamedTupleLike | Self::ConstraintSet @@ -1672,6 +1693,7 @@ impl KnownClass { "deprecated" => &[Self::Deprecated], "GenericAlias" => &[Self::GenericAlias], "NoneType" => &[Self::NoneType], + "SupportsKeysAndGetItem" => &[Self::SupportsKeysAndGetItem], "ModuleType" => &[Self::ModuleType], "GeneratorType" => &[Self::GeneratorType], "AsyncGeneratorType" => &[Self::AsyncGeneratorType], @@ -1691,6 +1713,7 @@ impl KnownClass { "AsyncIterator" => &[Self::AsyncIterator, Self::TyExtensionsAsyncIterator], "Sequence" => &[Self::Sequence], "Mapping" => &[Self::Mapping], + "MutableMapping" => &[Self::MutableMapping], "ParamSpec" => &[Self::ParamSpec, Self::ExtensionsParamSpec], "ParamSpecArgs" => &[Self::ParamSpecArgs], "ParamSpecKwargs" => &[Self::ParamSpecKwargs], @@ -1821,6 +1844,7 @@ impl KnownClass { | Self::Field | Self::KwOnly | Self::NamedTupleFallback + | Self::SupportsKeysAndGetItem | Self::TypedDictFallback | Self::ExtensionTypedDictFallback | Self::TypeVar @@ -1863,6 +1887,7 @@ impl KnownClass { | Self::AsyncIterator | Self::Sequence | Self::Mapping + | Self::MutableMapping | Self::ProtocolMeta | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions), diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 7284b37158..c7e3c49f95 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -2651,19 +2651,46 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } } - /// Returns common protocol constraints for a union containing only `TypedDict`s when every + /// Returns common protocol constraints for the `TypedDict` members of a union when every such /// member has the same constraints as their shared `Mapping[str, object]` fallback. fn common_typed_dict_protocol_constraints( &self, formal: Type<'db>, actual: UnionType<'db>, ) -> Option> { + fn is_string_keyed_mapping<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { + let Type::NominalInstance(instance) = ty.resolve_type_alias(db) else { + return false; + }; + + matches!( + instance.class(db).known(db), + Some( + KnownClass::Dict + | KnownClass::Mapping + | KnownClass::MutableMapping + | KnownClass::DefaultDict + | KnownClass::ChainMap + | KnownClass::OrderedDict + ) + ) && instance + .class(db) + .into_generic_alias() + .is_some_and(|alias| { + matches!( + alias.specialization(db).types(db), + [key, _] if key.resolve_type_alias(db) == KnownClass::Str.to_instance(db) + ) + }) + } + fn collect_typed_dicts<'db>( db: &'db dyn Db, ty: Type<'db>, resolving: &mut FxHashSet>, completed: &mut FxHashMap, bool>, typed_dicts: &mut FxHashSet>, + other_types: &mut FxOrderSet>, ) -> bool { let ty = ty.resolve_type_alias(db); if let Some(result) = completed.get(&ty) { @@ -2680,22 +2707,62 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { return false; } let result = union.elements(db).iter().all(|element| { - collect_typed_dicts(db, *element, resolving, completed, typed_dicts) + collect_typed_dicts( + db, + *element, + resolving, + completed, + typed_dicts, + other_types, + ) }); resolving.remove(&ty); result } Type::Intersection(intersection) - if intersection - .iter_positive(db) - .any(|element| element.resolve_type_alias(db).is_typed_dict()) => + if intersection.negative(db).is_empty() + && intersection + .iter_positive(db) + .any(|element| element.resolve_type_alias(db).is_typed_dict()) + && intersection.iter_positive(db).all(|element| { + let element = element.resolve_type_alias(db); + element.is_typed_dict() + || element + == KnownClass::Dict + .to_instance_unknown(db) + .top_materialization(db) + }) => { // `isinstance(value, dict)` narrows a `TypedDict` to an intersection with - // `Top[dict[Unknown, Unknown]]`. Keep the full intersection so the normal - // constraint-equivalence check below remains authoritative. + // `Top[dict[Unknown, Unknown]]`. Other conjuncts may contribute gradual + // constraints that the shared mapping would erase. typed_dicts.insert(ty); true } + Type::Intersection(intersection) + if intersection.negative(db).is_empty() + && intersection + .iter_positive(db) + .any(|element| is_string_keyed_mapping(db, element)) + && intersection.iter_positive(db).all(|element| { + let element = element.resolve_type_alias(db); + is_string_keyed_mapping(db, element) + || element + == KnownClass::Dict + .to_instance_unknown(db) + .top_materialization(db) + }) => + { + // `isinstance(value, dict)` can also narrow a mapping to an intersection with + // `Top[dict[Unknown, Unknown]]`. Retain the full intersection so its original + // key and value constraints are preserved. + other_types.insert(ty); + true + } + Type::NominalInstance(_) if is_string_keyed_mapping(db, ty) => { + other_types.insert(ty); + true + } _ => false, }; completed.insert(ty, result); @@ -2705,6 +2772,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { let mut resolving = FxHashSet::default(); let mut completed = FxHashMap::default(); let mut typed_dicts = FxHashSet::default(); + let mut other_types = FxOrderSet::default(); if !actual.elements(self.db).iter().all(|element| { collect_typed_dicts( self.db, @@ -2712,10 +2780,24 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { &mut resolving, &mut completed, &mut typed_dicts, + &mut other_types, ) }) { return None; } + if typed_dicts.is_empty() { + return None; + } + // Other protocols can observe key-specific or gradual evidence that the shared mapping + // fallback erases; restrict mixed unions to the protocol used by dictionary constructors. + if !other_types.is_empty() + && !matches!(formal, Type::ProtocolInstance(protocol) + if protocol.class_origin().is_some_and(|class| { + class.is_known(self.db, KnownClass::SupportsKeysAndGetItem) + })) + { + return None; + } // Use the read-only `Mapping[str, object]` as the fallback rather than `dict[str, object]`. // The current constraint solver can consider mutable protocol constraints equivalent even @@ -2724,18 +2806,35 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { let mapping = KnownClass::Mapping.to_specialized_instance(self.db, spec); let mapping_when = mapping.when_constraint_set_assignable_to_owned(self.db, formal); let mapping_when = self.constraints.load(self.db, &mapping_when); - typed_dicts - .into_iter() - .all(|element| { - let element_when = self.constraints.load( - self.db, - &element.when_constraint_set_assignable_to_owned(self.db, formal), - ); - element_when - .iff(self.db, self.constraints, mapping_when) - .is_always_satisfied(self.db) - }) - .then_some(mapping_when) + // Logically equivalent constraints can still infer different solutions, such as `Any` + // instead of `object`; preserve the original constraints when gradual evidence differs. + let mapping_solutions = mapping_when.solutions(self.db, self.constraints, self.inferable); + if !typed_dicts.into_iter().all(|element| { + let element_when = self.constraints.load( + self.db, + &element.when_constraint_set_assignable_to_owned(self.db, formal), + ); + element_when + .iff(self.db, self.constraints, mapping_when) + .is_always_satisfied(self.db) + && element_when.solutions(self.db, self.constraints, self.inferable) + == mapping_solutions + }) { + return None; + } + + // Reuse one constraint for all equivalent TypedDicts, but retain each mapping arm's + // original constraints. + Some(mapping_when.and(self.db, self.constraints, || { + other_types + .into_iter() + .when_all(self.db, self.constraints, |element| { + self.constraints.load( + self.db, + &element.when_constraint_set_assignable_to_owned(self.db, formal), + ) + }) + })) } /// Infer type mappings by comparing formal callable signatures against actual callables. From 329789399758564fba2a557f1d0eba923ed57887 Mon Sep 17 00:00:00 2001 From: Zayan Khan <108294002+ZayanKhan-12@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:52:09 -0400 Subject: [PATCH 068/390] [ty] docs: fix setup_primer_project.py link in ty CONTRIBUTING (#27184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `crates/ty/CONTRIBUTING.md` links `[\`setup_primer_project.py\`](./scripts/setup_primer_project.py)`, which resolves to `crates/ty/scripts/setup_primer_project.py` — a path that doesn't exist. The script lives at the repository root under `scripts/`, so from `crates/ty/` the link should be `../../scripts/setup_primer_project.py`. Verified the target exists. Docs-only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Zayan Khan Co-authored-by: Claude Fable 5 --- crates/ty/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty/CONTRIBUTING.md b/crates/ty/CONTRIBUTING.md index 293a5a1718..0313be6d4e 100644 --- a/crates/ty/CONTRIBUTING.md +++ b/crates/ty/CONTRIBUTING.md @@ -144,7 +144,7 @@ ensure that the changes do not break any of the properties. ## Ecosystem CI (`ecosystem-analyzer`) GitHub Actions will run your changes against a number of real-world projects from GitHub and report -any differences in ty's diagnostic output. You can use [`setup_primer_project.py`](./scripts/setup_primer_project.py) +any differences in ty's diagnostic output. You can use [`setup_primer_project.py`](../../scripts/setup_primer_project.py) to reproduce the same testing conditions locally. ## Coding guidelines From b087bfd74784b9f850ef50e7aaea9eb78d39662c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 25 Jul 2026 09:54:45 -0700 Subject: [PATCH 069/390] [ty] Model walrus bindings from comprehensions (#26466) ## Summary PEP 572 specifies that an assignment-expression target inside a comprehension binds in the scope containing the outermost comprehension. Before this change, we left the target inside the comprehension scope, so it was unresolved when referenced afterward. For example, these two cases raised false positives on `main`: ```py def first_comment(lines: list[str]): if any((comment := line).startswith("#") for line in lines): reveal_type(comment) # str def partial_sums(values: list[int]): total = 0 sums = [total := total + value for value in values] reveal_type(total) # int ``` This PR makes the walrus target visible in the containing scope while keeping its value expression in the comprehension scope, where it can reference iteration variables. Specifically, the semantic index now records the real assignment in the comprehension and adds a synthetic binding in the containing scope. If the comprehension always assigns the target, that binding replaces an earlier value; if it may not, the earlier value remains possible. Paths skipped by a filter and assignments passed through nested comprehensions are kept in order, and type inference reads the bindings left at the end of the comprehension to determine the exported type. ## What's included vs. excluded I went through several iterations of this change over the past few months, and each time they ballooned in scope. Here, I'm trying to intentionally limit the scope, so this section articulates cases that came up in Codex review but were explicitly rejected as out-of-scope. We do not model the zero-iteration path or whether a generator expression has been consumed: ```py def empty_comprehension(): items: list[int] = [] [(last := item) for item in items] print(last) # Runtime NameError; this PR treats `last` as bound. def deferred_generator(items: list[int]): pending = ((last := item) for item in items) print(last) # Runtime NameError until `pending` is consumed; modeled eagerly here. ``` We also conservatively promote exported values because later iterations can change them. We do not try to retain single-iteration literal precision: ```py [(last := 1) for _ in [0]] reveal_type(last) # int, not Literal[1] ``` We do not compute a fixed point for a walrus whose value changes type across repeated iterations. We currently infer the exported type from the first modeled iteration: ```py value = 0 [value := "" if isinstance(value, int) else 0 for _ in [0, 1]] reveal_type(value) # str; the runtime value after the second iteration is int value.upper() # Currently accepted; a fixed-point model would infer str | int ``` (This requires treating the comprehension body as a loop and iterating its use-def state to a fixed point.) We also defer all the IDE-specific navigation, references, rename, and unused-binding behavior to a separate PR (#26476). Closes https://github.com/astral-sh/ty/issues/162. --- crates/ty_python_core/src/builder.rs | 282 ++++++++++++++++-- crates/ty_python_core/src/definition.rs | 34 ++- crates/ty_python_core/src/use_def.rs | 42 +++ .../resources/mdtest/comprehensions/basic.md | 227 ++++++++++++++ .../diagnostics/semantic_syntax_errors.md | 6 + .../resources/mdtest/import/star.md | 106 +++---- .../src/types/infer/builder.rs | 61 ++-- 7 files changed, 649 insertions(+), 109 deletions(-) diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index 9cf72470ab..bd97790d2b 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -32,8 +32,8 @@ use crate::definition::{ ExceptHandlerDefinitionNodeRef, ForStmtDefinitionNodeRef, ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, ImportFromSubmoduleDefinitionNodeRef, LambdaParameterDefinitionNodeRef, LoopHeaderDefinitionNodeRef, LoopStmtRef, - MatchPatternDefinitionNodeRef, NestedBindingsDefinitionKind, ParameterDefinitionNodeRef, - StarImportDefinitionNodeRef, WithItemDefinitionNodeRef, + MatchPatternDefinitionNodeRef, NestedBindingExecution, NestedBindingsDefinitionKind, + ParameterDefinitionNodeRef, StarImportDefinitionNodeRef, WithItemDefinitionNodeRef, }; use crate::expression::{Expression, ExpressionKind}; use crate::frozen::{FrozenMap, FrozenSet}; @@ -61,8 +61,8 @@ use crate::statement::StatementInner; use crate::symbol::{ScopedSymbolId, Symbol}; use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue}; use crate::use_def::{ - EnclosingSnapshotKey, FlowSnapshot, FutureDefinitions, LiveBinding, PreviousDefinitions, - ScopedDefinitionId, ScopedEnclosingSnapshotId, UseDefMapBuilder, + EnclosingSnapshotKey, FlowSnapshot, FutureDefinitions, LiveBinding, LiveBindingStatus, + PreviousDefinitions, ScopedDefinitionId, ScopedEnclosingSnapshotId, UseDefMapBuilder, }; use crate::{Db, Statement, StatementNodeKey}; use crate::{ @@ -1438,9 +1438,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { ); } Some(CurrentAssignment::Named(named)) => { - // TODO(dhruvmanila): If the current scope is a comprehension, then the - // named expression is implicitly nonlocal. This is yet to be - // implemented. + self.mark_comprehension_named_target(place_id, named.target.range()); self.add_definition(place_id, named); } Some(CurrentAssignment::Comprehension { @@ -1555,13 +1553,23 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { definitions.len() }; - self.record_definition(place, definition); + self.record_definition(place, definition, None); (definition, num_definitions) } /// Records an already-created definition in the current scope. - fn record_definition(&mut self, place: ScopedPlaceId, definition: Definition<'db>) { + /// + /// `previous_definitions` controls whether a new binding replaces earlier bindings. By + /// default, ordinary assignments replace them and loop headers keep them. Comprehension + /// bindings choose explicitly because an assignment that only runs on some paths must keep + /// the earlier binding. + fn record_definition( + &mut self, + place: ScopedPlaceId, + definition: Definition<'db>, + previous_definitions: Option, + ) { let kind = definition.kind(self.db); let is_loop_header = kind.is_loop_header(); let category = kind.category(self.source_type.is_stub(), self.module); @@ -1591,16 +1599,15 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { } DefinitionCategory::Declaration => use_def.record_declaration(place, definition), DefinitionCategory::Binding => { - // Loop-header bindings don't shadow prior bindings. - let previous_definitions = if is_loop_header { + let previous = previous_definitions.unwrap_or(if is_loop_header { PreviousDefinitions::AreKept } else { PreviousDefinitions::AreShadowed - }; + }); use_def.record_binding( place, definition, - previous_definitions, + previous, FutureDefinitions::ShadowThisOne, ); if !is_loop_header { @@ -1806,6 +1813,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { place, DefinitionKind::NestedBindings(Box::new(NestedBindingsDefinitionKind { name, + execution: NestedBindingExecution::Lazy, nested_declarations: declarations, })), false, @@ -1847,6 +1855,209 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { } } + /// Records assignment-expression bindings from a comprehension in its containing scope. + /// + /// The value expression still belongs to the comprehension scope, so the real definition + /// stays there. The synthetic definition lets the containing scope observe that binding while + /// retaining the comprehension's scope for type inference. + /// + /// ```python + /// [(last := item) for item in items] + /// print(last) # `last` is owned by this containing scope. + /// ``` + fn synthesize_comprehension_binding_definitions( + &mut self, + nested_bindings: NestedGlobalOrNonlocalDeclarations, + ) { + let mut nested_bindings = nested_bindings.into_iter().collect::>(); + nested_bindings.sort_unstable_by(|(left, _), (right, _)| left.cmp(right)); + + for (name, mut declarations) in nested_bindings { + // Ignore declarations used only to validate `nonlocal` syntax. + declarations.retain(|d| d.is_bound); + declarations.shrink_to_fit(); + let Some(first_declaration) = declarations.first().copied() else { + continue; + }; + + let binding_status = self.comprehension_binding_status(&name, &declarations); + + let symbol = self.add_symbol(name.clone()); + debug_assert!( + declarations + .iter() + .all(|declaration| declaration.is_global() == first_declaration.is_global()) + ); + self.forward_comprehension_binding(&name, first_declaration, symbol); + + let place: ScopedPlaceId = symbol.into(); + if binding_status == LiveBindingStatus::Unbound { + self.mark_place_bound(place); + continue; + } + + let definition = Definition::new( + self.db, + self.current_scope_id(), + place, + DefinitionKind::NestedBindings(Box::new(NestedBindingsDefinitionKind { + name, + execution: NestedBindingExecution::Eager, + nested_declarations: declarations, + })), + false, + ); + let previous = if binding_status == LiveBindingStatus::Bound { + PreviousDefinitions::AreShadowed + } else { + PreviousDefinitions::AreKept + }; + self.record_definition(place, definition, Some(previous)); + } + } + + /// Summarizes whether the comprehension's live exit paths bind `name`. + /// + /// For example, `value` is only possibly bound after this comprehension because the walrus is + /// skipped when `flag` is false: + /// + /// ```python + /// [(value := item) if flag else None for item in items] + /// ``` + fn comprehension_binding_status( + &mut self, + name: &str, + declarations: &[NestedDeclaration], + ) -> LiveBindingStatus { + let mut status = LiveBindingStatus::Unbound; + for declaration in declarations { + let scope_id = declaration.file_scope_id; + let Some(symbol) = self.place_tables[scope_id].symbol_id(name) else { + continue; + }; + match self.use_def_maps[scope_id].symbol_live_binding_status(symbol) { + LiveBindingStatus::Bound => return LiveBindingStatus::Bound, + LiveBindingStatus::PossiblyBound => status = LiveBindingStatus::PossiblyBound, + LiveBindingStatus::Unbound => {} + } + } + status + } + + /// Passes a walrus binding out through nested comprehensions. + /// + /// ```python + /// [[(last := item) for item in row] for row in rows] + /// print(last) # `last` belongs to the scope outside both comprehensions. + /// ``` + /// + /// Each comprehension passes the binding out one level. This preserves the order and + /// conditions under which the assignment is evaluated. + fn forward_comprehension_binding( + &mut self, + name: &Name, + first_declaration: NestedDeclaration, + symbol: ScopedSymbolId, + ) { + if self.scopes[self.current_scope()].kind() != ScopeKind::Comprehension { + return; + } + + self.current_scope_info_mut() + .nested_global_or_nonlocal_declarations + .remove(name); + + if first_declaration.is_global() { + self.current_place_table_mut() + .symbol_mut(symbol) + .mark_global(); + } else { + self.current_place_table_mut() + .symbol_mut(symbol) + .mark_nonlocal(); + } + self.current_scope_info_mut() + .this_scope_global_or_nonlocal_declarations + .entry(name.clone()) + .or_insert(first_declaration.range); + } + + /// Marks a comprehension walrus target as a write to the containing Python scope. + /// + /// The iteration variable remains local to the comprehension, while the walrus target does + /// not: + /// + /// ```python + /// [(result := item) for item in items] + /// print(result) # valid + /// print(item) # `item` is not defined here + /// ``` + fn mark_comprehension_named_target(&mut self, place: ScopedPlaceId, range: TextRange) { + if self.scopes[self.current_scope()].kind() != ScopeKind::Comprehension { + return; + } + if self.semantic_syntax_errors.borrow().iter().any(|error| { + matches!( + error.kind, + SemanticSyntaxErrorKind::ReboundComprehensionVariable + | SemanticSyntaxErrorKind::NamedExpressionInComprehensionIterable + ) && error.range.contains_range(range) + }) { + return; + } + + let Some(symbol) = place.as_symbol() else { + return; + }; + let name = self.current_place_table().symbol(symbol).name().clone(); + let Some(containing_scope) = self.scope_stack.iter().rev().find(|scope_info| { + self.scopes[scope_info.file_scope_id].kind() != ScopeKind::Comprehension + }) else { + return; + }; + + let containing_scope_id = containing_scope.file_scope_id; + let is_global = match self.scopes[containing_scope_id].kind() { + ScopeKind::Module => true, + ScopeKind::Function | ScopeKind::Lambda => self.place_tables[containing_scope_id] + .symbol_id(&name) + .is_some_and(|symbol| { + self.place_tables[containing_scope_id] + .symbol(symbol) + .is_global() + }), + // Assignment expressions are invalid in comprehensions directly contained by these + // scopes. Leave the recovered target local to the comprehension. + ScopeKind::Class | ScopeKind::TypeAlias | ScopeKind::TypeParams => return, + ScopeKind::Comprehension => return, + }; + + if is_global { + self.current_place_table_mut() + .symbol_mut(symbol) + .mark_global(); + } else { + let (containing_symbol, added) = + self.place_tables[containing_scope_id].add_symbol(Symbol::new(name.clone())); + if added { + self.use_def_maps[containing_scope_id].add_place(containing_symbol.into()); + } + + let containing_symbol = + self.place_tables[containing_scope_id].symbol_mut(containing_symbol); + if !containing_symbol.is_nonlocal() && !containing_symbol.is_bound() { + containing_symbol.mark_bound(); + } + + self.current_place_table_mut() + .symbol_mut(symbol) + .mark_nonlocal(); + } + self.current_scope_info_mut() + .this_scope_global_or_nonlocal_declarations + .insert(name, range); + } + fn record_expression_narrowing_constraint( &mut self, predicate_node: &'ast ast::Expr, @@ -2434,8 +2645,9 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { value, ); + let mut filtered_out_paths = Vec::new(); for if_expr in &generator.ifs { - self.visit_comprehension_filter(if_expr); + filtered_out_paths.push(self.visit_comprehension_filter(if_expr)); } for generator in generators_iter { @@ -2452,25 +2664,52 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { ); for if_expr in &generator.ifs { - self.visit_comprehension_filter(if_expr); + filtered_out_paths.push(self.visit_comprehension_filter(if_expr)); } } visit_outer_elt(self); - self.pop_scope(); + for filtered_out_path in filtered_out_paths { + self.flow_merge(filtered_out_path); + } + let nested_bindings = self.pop_scope(); + self.synthesize_comprehension_binding_definitions(nested_bindings); self.current_assignments = saved_assignments; comprehension_scope } - fn visit_comprehension_filter(&mut self, if_expr: &'ast ast::Expr) { + /// Visits a comprehension filter on its truthy path and returns the filtered-out path. + /// + /// A false filter skips the rest of the current iteration, but assignments performed while + /// evaluating the filter remain observable: + /// + /// ```python + /// [item for item in items if (last := item)] + /// print(last) + /// ``` + fn visit_comprehension_filter(&mut self, if_expr: &'ast ast::Expr) -> FlowSnapshot { self.visit_expr(if_expr); let condition_flow_snapshot = self.flow_snapshot_for_condition(if_expr); - if let Some(truthy) = condition_flow_snapshot.into_truthy() { - self.flow_restore(truthy); - } - let _ = self.record_expression_narrowing_constraint(if_expr); + let filtered_out = if let Some(snapshots) = condition_flow_snapshot.into_branches() { + self.flow_restore(snapshots.truthy); + snapshots.falsy + } else { + self.flow_snapshot() + }; + + let (predicate, narrowing_id) = self.record_expression_narrowing_constraint(if_expr); + let reachability_constraint = self.record_reachability_constraint(predicate); + let included_path = self.flow_snapshot(); + + self.flow_restore(filtered_out); + self.record_negated_narrowing_constraint(predicate, narrowing_id); + self.record_negated_reachability_constraint(reachability_constraint); + let filtered_out = self.flow_snapshot(); + + self.flow_restore(included_path); + filtered_out } fn declare_parameters(&mut self, parameters: &'ast ast::Parameters) { @@ -4444,7 +4683,6 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { } } ast::Expr::Named(node) => { - // TODO walrus in comprehensions is implicitly nonlocal self.visit_expr(&node.value); // See https://peps.python.org/pep-0572/#differences-between-assignment-expressions-and-assignment-statements diff --git a/crates/ty_python_core/src/definition.rs b/crates/ty_python_core/src/definition.rs index 5e5f4a8936..f1f2fe7767 100644 --- a/crates/ty_python_core/src/definition.rs +++ b/crates/ty_python_core/src/definition.rs @@ -9,7 +9,6 @@ use ruff_python_ast::{self as ast, AnyNodeRef, Expr}; use ruff_text_size::{Ranged, TextRange, TextSize}; use smallvec::SmallVec; -use crate::Db; use crate::LoopHeaderId; use crate::ast_node_ref::AstNodeRef; use crate::member::ScopedMemberId; @@ -19,6 +18,8 @@ use crate::predicate::PatternPredicate; use crate::scope::{FileScopeId, ScopeId}; use crate::symbol::ScopedSymbolId; use crate::unpack::{Unpack, UnpackPosition}; +use crate::use_def::BindingWithConstraintsIterator; +use crate::{Db, SemanticIndex}; /// A definition of a place. /// @@ -1594,12 +1595,43 @@ impl LoopHeaderDefinitionKind { #[derive(Clone, Debug, get_size2::GetSize)] pub struct NestedBindingsDefinitionKind { pub name: Name, + pub execution: NestedBindingExecution, // Note that in general this can include both `global` and `nonlocal` declarations from // different nested scopes, because we don't necessarily know at synthesis time which of those // kind will be visible in the current scope. pub nested_declarations: SmallVec<[crate::builder::NestedDeclaration; 1]>, } +impl NestedBindingsDefinitionKind { + /// Returns the binding source for each nested declaration, along with whether it is global. + pub fn binding_sources<'index, 'db>( + &'index self, + index: &'index SemanticIndex<'db>, + ) -> impl Iterator)> + 'index { + self.nested_declarations.iter().filter_map(|declaration| { + debug_assert!(declaration.is_bound); + let symbol = index + .place_table(declaration.file_scope_id) + .symbol_id(&self.name)?; + let use_def = index.use_def_map(declaration.file_scope_id); + let bindings = match self.execution { + NestedBindingExecution::Lazy => use_def.reachable_bindings(symbol.into()), + NestedBindingExecution::Eager => use_def.end_of_scope_bindings(symbol.into()), + }; + Some((declaration.is_global(), bindings)) + }) + } +} + +/// Describes when writes from a nested scope can affect its containing scope. +#[derive(Copy, Clone, Debug, Eq, PartialEq, get_size2::GetSize)] +pub enum NestedBindingExecution { + /// The nested scope can run later or repeatedly, as with a function body. + Lazy, + /// The nested scope is modeled as running while evaluating the containing expression. + Eager, +} + #[derive( Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, get_size2::GetSize, salsa::SalsaValue, )] diff --git a/crates/ty_python_core/src/use_def.rs b/crates/ty_python_core/src/use_def.rs index 3caac2daa5..963873dc6f 100644 --- a/crates/ty_python_core/src/use_def.rs +++ b/crates/ty_python_core/src/use_def.rs @@ -278,6 +278,17 @@ pub use place_state::LiveBinding; pub use place_state::ScopedDefinitionId; pub(super) use place_state::{FutureDefinitions, PreviousDefinitions}; +/// Summarizes whether the live control-flow paths leave a symbol bound. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(super) enum LiveBindingStatus { + /// No live path contains a binding. + Unbound, + /// Some live paths contain a binding and others leave the symbol unbound. + PossiblyBound, + /// Every live path contains a binding. + Bound, +} + /// Identifies a [`LoopHeader`] within a single scope's [`UseDefMap`]. #[newtype_index] #[derive(get_size2::GetSize)] @@ -2475,6 +2486,37 @@ impl<'db> UseDefMapBuilder<'db> { .map(LiveBinding::binding) } + /// Returns the current boundness of `symbol` after applying pending reachability constraints. + /// + /// Bindings on statically unreachable paths do not contribute to the result. This is stricter + /// than [`Symbol::is_bound`](crate::symbol::Symbol::is_bound), which records whether the symbol + /// is bound anywhere in the scope without considering control flow. + pub(super) fn symbol_live_binding_status( + &mut self, + symbol: ScopedSymbolId, + ) -> LiveBindingStatus { + let mut has_binding = false; + let mut has_unbound = false; + + for binding in self.current_bindings(symbol.into()) { + if binding.reachability_constraint() == ScopedReachabilityConstraintId::ALWAYS_FALSE { + continue; + } + + if binding.binding().is_unbound() { + has_unbound = true; + } else { + has_binding = true; + } + } + + match (has_binding, has_unbound) { + (true, true) => LiveBindingStatus::PossiblyBound, + (true, false) => LiveBindingStatus::Bound, + (false, _) => LiveBindingStatus::Unbound, + } + } + pub(super) fn mark_binding_definitions_used( &mut self, binding_definition_ids: impl IntoIterator, diff --git a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md index c99289b60e..c03c1185db 100644 --- a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md @@ -31,6 +31,231 @@ class Table: [[reveal_type((x, y)) for x in range(3)] for y in range(3)] ``` +## Assignment expressions in comprehensions + +[PEP 572] specifies that an assignment expression in a comprehension binds its target in the scope +containing the outermost comprehension. + +ty currently assumes that a comprehension runs at least once and that a generator expression is +consumed immediately. + +### Basic forms + +Assignment expressions can appear in the element of a list comprehension and in the key or value of +a dictionary comprehension: + +```py +[(list_value := item) for item in [1]] +{(dict_key := item): (dict_value := item) for item in [1]} + +reveal_type(list_value) # revealed: int +reveal_type(dict_key) # revealed: int +reveal_type(dict_value) # revealed: int +``` + +### Generator expressions + +The target also binds in the containing scope when the assignment is in a generator expression. PEP +572 uses this `any` pattern as a motivating example: + +```py +def find_comment(lines: list[str]): + if any((comment := line).startswith("#") for line in lines): + reveal_type(comment) # revealed: str +``` + +### Assignment order + +If an iteration assigns the same target more than once, the last assignment determines its value +after the comprehension: + +```py +[(ordered := item, ordered := "") for item in [1]] +reveal_type(ordered) # revealed: str +``` + +### Branches that do not assign + +A target in a branch known not to run remains unbound, while the other target is available after the +comprehension: + +```py +[(dead := 1) if False else (live := 2) for _ in [0]] + +dead # error: [unresolved-reference] +reveal_type(live) # revealed: int +``` + +### Assignments on only some paths + +When the assignment only runs on one possible path, an earlier value remains possible: + +```py +def conditional_with_previous_value(flag: bool): + value = "old" + [(value := 1) if flag else 0 for _ in [0]] + reveal_type(value) # revealed: Literal["old"] | int +``` + +Without an earlier value, the target may be unbound: + +```py +def conditional_without_previous_value(flag: bool): + [(value := 1) if flag else 0 for _ in [0]] + # error: [possibly-unresolved-reference] + reveal_type(value) # revealed: int +``` + +ty conservatively keeps the type of an assignment that is unreachable on the first iteration, since +a later iteration may take a different branch. Even though `0 == 1` is always false, the target is +therefore possibly unbound, and checking must continue after the read: + +```py +def statically_false_condition(): + [(value := 1) if 0 == 1 else 0 for _ in [0]] + # error: [possibly-unresolved-reference] + reveal_type(value) # revealed: int + still_reachable # error: [unresolved-reference] +``` + +### Comprehension filters + +A false filter skips the element, but an assignment made while evaluating that filter still takes +effect: + +```py +[value for value in [True, False] if (last_value := value)] +reveal_type(last_value) # revealed: bool +``` + +If short-circuit evaluation skips the assignment, the target may be unbound: + +```py +def conditional_filter(flag: bool): + [0 for _ in [0] if flag and (value := 1)] + # error: [possibly-unresolved-reference] + reveal_type(value) # revealed: int +``` + +An assignment in the element only runs when every preceding filter succeeds: + +```py +def assignment_after_filter(flag: bool): + [(value := 1) for _ in [0] if flag] + # error: [possibly-unresolved-reference] + reveal_type(value) # revealed: int +``` + +### Assignments that depend on earlier iterations + +An assignment can read the value left by an earlier iteration. In this example, the final value is +`3`, so retaining only the first iteration's literal values would be incorrect: + +```py +def partial_sum(): + total = 0 + [total := total + value for value in [1, 2]] + reveal_type(total) # revealed: int +``` + +ty does not yet account for a type that changes between iterations. The second iteration below +assigns `int`, so the final type should be `str | int` and `value.upper()` should report an error: + +```py +def type_changes_across_iterations(): + value = 0 + [value := "" if isinstance(value, int) else 0 for _ in [0, 1]] + reveal_type(value) # revealed: str + value.upper() +``` + +The same applies when two targets depend on values from earlier iterations: + +```py +def two_dependent_targets(): + x = 0 + y = 0 + [(y := x, x := y + 1) for _ in [1, 2]] + reveal_type(x) # revealed: int + reveal_type(y) # revealed: int +``` + +A guard can also depend on a value changed by a later assignment in the same iteration. The first +iteration below sets `flag`, so the second iteration assigns `value`: + +```py +def loop_carried_guard(): + flag = False + [((value := 1) if flag else 0, (flag := True)) for _ in [0, 1]] + # error: [possibly-unresolved-reference] + reveal_type(value) # revealed: int +``` + +### Function-local targets + +An assignment in a branch known not to run still makes its target local to the containing function. +A read must not fall back to a global variable with the same name: + +```py +local_target = "global" + +def read_local_target(): + [(local_target := 1) if False else 0 for _ in [0]] + local_target # error: [unresolved-reference] +``` + +A walrus also makes its target local before the first iteration. Its first assignment must not read +a global with the same name. Explicit `global` and `nonlocal` declarations still refer to the +existing outer variable: + +```py +total = 0 + +def sums(values: list[int]) -> list[int]: + return [total := total + value for value in values] # error: [unresolved-reference] + +def sums_global(values: list[int]) -> list[int]: + global total + return [total := total + value for value in values] + +def sums_nonlocal(values: list[int]) -> list[int]: + total = 0 + + def add_values() -> list[int]: + nonlocal total + return [total := total + value for value in values] + + return add_values() +``` + +### Nested comprehensions + +An assignment in an inner comprehension still binds outside the outermost comprehension. A later +assignment in the outer comprehension replaces the inner value: + +```py +[([nested_order := 1 for _ in [0]], (nested_order := "")) for _ in [0]] +reveal_type(nested_order) # revealed: str +``` + +These are controls for an inner comprehension that is never evaluated. It must not replace an +earlier value: + +```py +def unreachable_nested_assignment_with_previous_value(): + value = "old" + [[value := 1 for _ in [0]] if False else [] for _ in [0]] + reveal_type(value) # revealed: Literal["old"] +``` + +Nor should it create a new value: + +```py +def unreachable_nested_assignment_without_previous_value(): + [[value := 1 for _ in [0]] if False else [] for _ in [0]] + value # error: [unresolved-reference] +``` + ## Comprehension referencing outer comprehension ```py @@ -262,3 +487,5 @@ reveal_type(dict_with_literal_values) # revealed: dict[str, Literal[1, 2, 3]] set_with_literals: set[Literal[1, 2, 3]] = {k for k in (1, 2, 3)} reveal_type(set_with_literals) # revealed: set[Literal[1, 2, 3]] ``` + +[pep 572]: https://peps.python.org/pep-0572/#scope-of-the-target 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 b01b151c02..a118b3af0f 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 @@ -266,6 +266,12 @@ def returns_list() -> list[int]: # error: [invalid-syntax] "assignment expression cannot be used in a comprehension iterable expression" [x for x in (z := returns_list()).copy()] +def invalid_later_iterable(): + # error: [invalid-syntax] "assignment expression cannot be used in a comprehension iterable expression" + [item for item in [0] for _ in (escaped := [1])] + # error: [unresolved-reference] + reveal_type(escaped) # revealed: Unknown + # error: [invalid-syntax] "assignment expression cannot be used in a comprehension iterable expression" # error: [invalid-syntax] "assignment expression cannot rebind comprehension variable" [a for a in [(b := 1) for b in [1]]] diff --git a/crates/ty_python_semantic/resources/mdtest/import/star.md b/crates/ty_python_semantic/resources/mdtest/import/star.md index 519d54c995..dfe766c389 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/star.md +++ b/crates/ty_python_semantic/resources/mdtest/import/star.md @@ -425,40 +425,51 @@ print(K) print(L) ``` -### Definitions in function-like scopes are not global definitions +### Comprehension and lambda locals are not global definitions -Except for some cases involving walrus expressions inside comprehension scopes. +Comprehension iteration variables, lambda parameters, and assignments inside lambdas are not module +globals and are therefore not available to a wildcard import. `exporter.py`: ```py -class Iterator: - def __next__(self) -> int: - return 42 +[a for a in [1]] +{b for b in [1]} +{c: c for c in [1]} +(d for d in [1]) +lambda e: (f := 42) +[(lambda s=s: (t := 42))() for s in [1]] +``` -class Iterable: - def __iter__(self) -> Iterator: - return Iterator() +`importer.py`: -[a for a in Iterable()] -{b for b in Iterable()} -{c: c for c in Iterable()} -(d for d in Iterable()) -lambda e: (f := 42) +```py +from exporter import * + +a # error: [unresolved-reference] +b # error: [unresolved-reference] +c # error: [unresolved-reference] +d # error: [unresolved-reference] +e # error: [unresolved-reference] +f # error: [unresolved-reference] +s # error: [unresolved-reference] +t # error: [unresolved-reference] +``` -# Definitions created by walruses in a comprehension scope are unique; -# they "leak out" of the scope and are stored in the surrounding scope -[(g := h * 2) for h in Iterable()] -[i for j in Iterable() if (i := j - 10) > 0] -{(k := l * 2): (m := l * 3) for l in Iterable()} -list(((o := p * 2) for p in Iterable())) +### Assignment-expression targets in comprehensions are global definitions -# A walrus expression nested inside several scopes *still* leaks out -# to the global scope: -[[[[(q := r) for r in Iterable()]] for _ in range(42)] for _ in range(42)] +Assignment-expression targets bind in the scope containing the comprehension. At module level, +targets in an element, filter, dictionary key or value, generator expression, or nested +comprehension are all available to a wildcard import. -# A walrus inside a lambda inside a comprehension does not leak out -[(lambda s=s: (t := 42))() for s in Iterable()] +`exporter.py`: + +```py +[(list_value := item) for item in [1]] +[item for item in [1] if (filtered_value := item - 10) > 0] +{(dict_key := item * 2): (dict_value := item * 3) for item in [1]} +list((generator_value := item * 2) for item in [1]) +[[[[(nested_value := item) for item in [1]]] for _ in [1]] for _ in [1]] ``` `importer.py`: @@ -466,47 +477,12 @@ list(((o := p * 2) for p in Iterable())) ```py from exporter import * -# error: [unresolved-reference] -reveal_type(a) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(b) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(c) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(d) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(e) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(f) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(h) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(j) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(p) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(r) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(s) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(t) # revealed: Unknown - -# TODO: these should all reveal `Unknown | int` and should not emit errors. -# (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] -reveal_type(g) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(i) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(k) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(m) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(o) # revealed: Unknown -# error: [unresolved-reference] -reveal_type(q) # revealed: Unknown +reveal_type(list_value) # revealed: int +reveal_type(filtered_value) # revealed: int +reveal_type(dict_key) # revealed: int +reveal_type(dict_value) # revealed: int +reveal_type(generator_value) # revealed: int +reveal_type(nested_value) # revealed: int ``` ### An annotation without a value is a definition in a stub but not a `.py` file diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index d4d4676fd0..6cccb9f29b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -125,7 +125,8 @@ use ty_python_core::definition::{ AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, ComprehensionDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey, DefinitionState, ExceptHandlerDefinitionKind, ForStmtDefinitionKind, LambdaParameterDefinitionNodeKind, LoopHeaderDefinitionKind, - NestedBindingsDefinitionKind, ParameterDefinitionNodeKind, TargetKind, WithItemDefinitionKind, + NestedBindingExecution, NestedBindingsDefinitionKind, ParameterDefinitionNodeKind, TargetKind, + WithItemDefinitionKind, }; use ty_python_core::expression::{Expression, ExpressionKind}; use ty_python_core::narrowing_constraints::ConstraintKey; @@ -2486,18 +2487,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let this_scope_sees_nonlocal_bindings = !(this_scope_sees_global_bindings || (scope.scope(db).kind().is_class() && symbol.is_local())); - let mut visible_nested_declarations = nested_bindings_kind - .nested_declarations - .iter() - .filter(|declaration| { - if declaration.is_global() { + let mut binding_sources = nested_bindings_kind + .binding_sources(self.index) + .filter_map(|(is_global, bindings)| { + (if is_global { this_scope_sees_global_bindings } else { this_scope_sees_nonlocal_bindings - } + }) + .then_some(bindings) }) .peekable(); - if visible_nested_declarations.peek().is_some() + if binding_sources.peek().is_some() && self .index .use_def_map(scope_id) @@ -2512,20 +2513,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return; } - let mut union = UnionBuilder::new(db).recursively_defined(RecursivelyDefined::Yes); - for declaration in visible_nested_declarations { - assert!( - declaration.is_bound, - "nested declarations without bindings shouldn't be recorded here", - ); - let nested_place_table = self.index.place_table(declaration.file_scope_id); - let nested_symbol_id = nested_place_table - .symbol_id(&nested_bindings_kind.name) - .unwrap(); - let use_def = self.index.use_def_map(declaration.file_scope_id); + let recursively_defined = match nested_bindings_kind.execution { + NestedBindingExecution::Lazy => RecursivelyDefined::Yes, + NestedBindingExecution::Eager => RecursivelyDefined::No, + }; + let mut union = UnionBuilder::new(db).recursively_defined(recursively_defined); + for bindings in binding_sources { + if nested_bindings_kind.execution == NestedBindingExecution::Eager { + // A comprehension can execute repeatedly, so a source that is unreachable in the + // first modeled iteration may become reachable in a later one. Preserve each + // source's narrowed type and let the proxy's outer use-def state track boundness. + for binding in bindings { + let DefinitionState::Defined(source) = binding.binding else { + continue; + }; + let ty = binding_type(db, source); + union.add_in_place(binding.narrowing_constraint.narrow( + db, + ty, + source.place(db), + )); + } + continue; + } + let Some(ty) = place_from_bindings_with_reachability_cache( db, - use_def.reachable_bindings(nested_symbol_id.into()), + bindings, self.reachability_cache(), ) .place @@ -2534,7 +2548,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; union.add_in_place(ty); } - self.bindings.insert(definition, union.build()); + let ty = union.build(); + let ty = match nested_bindings_kind.execution { + NestedBindingExecution::Lazy => ty, + NestedBindingExecution::Eager => ty.promote(db), + }; + self.bindings.insert(definition, ty); } fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) { From 6c660d8d06fdea421b45686e54c8f759ae68b323 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 25 Jul 2026 10:49:04 -0700 Subject: [PATCH 070/390] [ty] Preserve receiver constraints when binding overloaded methods (#27038) ## Summary Follow-up to [the receiver-constraint review discussion in #24707](https://github.com/astral-sh/ruff/pull/24707#discussion_r3326577303). Prior to this change, we reduced overload receiver matching to a boolean before binding the method. That kept overloads with satisfiable generic receivers, but discarded the constraints needed to specialize the remainder of the signature; structural receivers with contradictory constraints could also remain visible. We now reuse the receiver constraint set produced during binding, existentially prune impossible overloads, and solve exact receiver bounds into the bound signature while retaining one-sided constraints for later callable comparisons. For example: ```py from typing import overload class Box[T]: value: T @overload def method[S](self: "Box[S]", value: S) -> S: ... @overload def method(self, value: bytes) -> bytes: ... def method(self, value: object) -> object: ... reveal_type(Box[str]().method) # Overload[(value: str) -> str, (value: bytes) -> bytes] ``` --------- Co-authored-by: Carl Meyer --- .../resources/mdtest/enums.md | 6 +- .../resources/mdtest/liskov.md | 60 +++++ .../resources/mdtest/overloads.md | 240 +++++++++++++++++- .../src/types/constraints.rs | 2 +- .../ty_python_semantic/src/types/generics.rs | 9 + crates/ty_python_semantic/src/types/method.rs | 14 +- .../src/types/signatures.rs | 82 +++++- 7 files changed, 382 insertions(+), 31 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 177fecdfc1..3a4e4b8fd9 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -3674,13 +3674,13 @@ dynamic construction of enums using the functional syntax: from enum import Enum, IntEnum, StrEnum from ty_extensions._internal import into_regular_callable -# revealed: Overload[[_EnumMemberT](value: Any, names: None = None) -> _EnumMemberT, (value: str, names: Iterable[Iterable[str | Any]], *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]] +# revealed: Overload[(value: Any, names: None = None) -> Enum, (value: str, names: Iterable[Iterable[str | Any]], *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]] reveal_type(into_regular_callable(Enum)) -# revealed: Overload[[_EnumMemberT](value: Any, names: None = None) -> _EnumMemberT, (value: str, names: Iterable[Iterable[str | Any]], *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]] +# revealed: Overload[(value: Any, names: None = None) -> IntEnum, (value: str, names: Iterable[Iterable[str | Any]], *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]] reveal_type(into_regular_callable(IntEnum)) -# revealed: Overload[[_EnumMemberT](value: Any, names: None = None) -> _EnumMemberT, (value: str, names: Iterable[Iterable[str | Any]], *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]] +# revealed: Overload[(value: Any, names: None = None) -> StrEnum, (value: str, names: Iterable[Iterable[str | Any]], *, module: str | None = None, qualname: str | None = None, type: type | None = None, start: int = 1, boundary: FlagBoundary | None = None) -> type[Enum]] reveal_type(into_regular_callable(StrEnum)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/liskov.md b/crates/ty_python_semantic/resources/mdtest/liskov.md index 0d2ef15b27..395b671083 100644 --- a/crates/ty_python_semantic/resources/mdtest/liskov.md +++ b/crates/ty_python_semantic/resources/mdtest/liskov.md @@ -2159,3 +2159,63 @@ class MaybeEqWhile: def __eq__(self, other: MaybeEqWhile) -> bool: return True ``` + +## Overloaded generic receivers remain visible to override checks + +An override must still be checked against every applicable receiver-specialized overload when the +subclass retains a covariant type parameter. A `str` receiver matches both the `str` and `object` +overloads, so accepting only `str` is invalid. A two-item receiver excludes the one-item overload, +so matching only that excluded overload is also invalid. + +```toml +[environment] +python-version = "3.12" +``` + +```pyi +from typing import Any, Generic, TypeVar, overload + +ValueCo = TypeVar("ValueCo", covariant=True) +ShapeCo = TypeVar("ShapeCo", covariant=True) + +class Receiver(Generic[ValueCo, ShapeCo]): + @overload + def by_value(self: "Receiver[str, Any]", value: str) -> None: ... + @overload + def by_value(self: "Receiver[object, Any]", value: object) -> None: ... + @overload + def by_shape(self: "Receiver[Any, tuple[str]]", value: str) -> None: ... + @overload + def by_shape(self: "Receiver[Any, tuple[str, str]]", value: bytes) -> None: ... + +class NarrowValueOverride(Receiver[str, ShapeCo], Generic[ShapeCo]): + def by_value(self, value: str) -> None: ... # error: [invalid-method-override] + +class WrongShapeOverride(Receiver[ValueCo, tuple[str, str]], Generic[ValueCo]): + def by_shape(self, value: str) -> None: ... # error: [invalid-method-override] +``` + +## Equivalent overloaded protocol receivers are valid overrides + +A generic implementation can restate a protocol's receiver-specialized overload set using its own +receiver type without changing the method contract. + +```py +from typing import Generic, Protocol, TypeVar, overload + +T = TypeVar("T") +TContra = TypeVar("TContra", contravariant=True) + +class TaskStatus(Protocol[TContra]): + @overload + def started(self: "TaskStatus[None]") -> None: ... + @overload + def started(self, value: TContra) -> None: ... + +class ConcreteStatus(Generic[T], TaskStatus[T]): + @overload + def started(self: "ConcreteStatus[None]") -> None: ... + @overload + def started(self: "ConcreteStatus[T]", value: T) -> None: ... + def started(self, value: T | None = None) -> None: ... +``` diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index f3afc268dc..2ca9d3b223 100644 --- a/crates/ty_python_semantic/resources/mdtest/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -275,9 +275,7 @@ def union_receiver(reader: Reader[int | str]): ## Method type variables inferred from `self` Binding an overload whose explicit receiver introduces a method type variable should infer that -variable from the concrete receiver and apply it to the remainder of the signature. At present, -receiver matching retains the overload, but does not yet apply the inferred `S = str` -specialization. +variable from the concrete receiver and apply it to the remainder of the signature. ```toml [environment] @@ -285,9 +283,11 @@ python-version = "3.12" ``` ```py -from typing import overload +from typing import Any, Callable, overload class ReceiverGeneric[T]: + value: T + @overload def method[S](self: "ReceiverGeneric[S]", value: S) -> S: ... @overload @@ -295,18 +295,147 @@ class ReceiverGeneric[T]: def method(self, value: object) -> object: return value -# Receiver constraints are preserved for later relation checks, but are not yet solved into the -# displayed bound signature. -# TODO: revealed: Overload[(value: str) -> str, (value: bytes) -> bytes] -reveal_type(ReceiverGeneric[str]().method) # revealed: Overload[[S](value: S) -> S, (value: bytes) -> bytes] +reveal_type(ReceiverGeneric[str]().method) # revealed: Overload[(value: str) -> str, (value: bytes) -> bytes] + +def takes_callable(fn: Callable[..., Any]) -> None: ... +def use_generic_receiver[T](value: ReceiverGeneric[T]) -> None: + # revealed: Overload[(value: T@use_generic_receiver) -> T@use_generic_receiver, (value: bytes) -> bytes] + reveal_type(value.method) + takes_callable(value.method) +``` + +## Constrained method type variables inferred from `self` + +Matching a receiver against a value-constrained method type variable must reject values outside that +variable's constraints. A subclass of an allowed value must be promoted to the declared constraint +rather than appearing as the specialized return type. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, Generic, TypeVar, overload + +BoxT = TypeVar("BoxT", covariant=True) +Constrained = TypeVar("Constrained", str, bytes) + +class ConstrainedReceiverBox(Generic[BoxT]): + @overload + def method(self: "ConstrainedReceiverBox[Constrained]", value: Constrained) -> Constrained: ... + @overload + def method(self: "ConstrainedReceiverBox[Constrained]", value: Constrained, repeat: int = ...) -> Constrained: ... + def method(self, value: str | bytes, repeat: int = 1) -> str | bytes: + return value + +invalid_receiver = ConstrainedReceiverBox[int]() +invalid_method = invalid_receiver.method +reveal_type(invalid_method) # revealed: Overload[] + +# error: [no-matching-overload] +reveal_type(invalid_method(1)) # revealed: Unknown + +# error: [invalid-assignment] +invalid_callback: Callable[[int], int] = invalid_method + +class SubStr(str): ... + +subclass_receiver = ConstrainedReceiverBox[SubStr]() +reveal_type(subclass_receiver.method(SubStr())) # revealed: str +promoted_callback: Callable[[SubStr], str] = subclass_receiver.method +``` + +## Disjunctive generic receivers + +A receiver may satisfy both sides of a union without constraining both sides' type variables on the +same path. In particular, matching `Left[int]` leaves the `Right` type variable available to +specialize from the method arguments. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, overload + +class Left[T]: + left: T + +class Right[T]: + right: T + +class BaseWithDisjunctiveReceiver: + @overload + def method[S, U](self: "Left[S] | Right[U]", first: S, second: U) -> tuple[S, U]: ... + @overload + def method(self, first: bytes, second: bytes) -> tuple[bytes, bytes]: ... + def method(self, first: object, second: object) -> tuple[object, object]: + return first, second + +class Both(BaseWithDisjunctiveReceiver, Left[int], Right[str]): ... + +receiver = Both() +receiver.method(1, b"value") +valid_callback: Callable[[int, bytes], tuple[int, bytes]] = receiver.method +``` + +## Receiver type variables alongside variadic type parameters + +A method's `ParamSpec` or `TypeVarTuple` must not prevent an ordinary type variable from being +specialized by its receiver. The variadic parameters remain available for argument inference; +neither method can be converted into a callback with a return type incompatible with the receiver. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, overload + +class VariadicReceiverBox[T]: + value: T + + @overload + def with_paramspec[**P, S](self: "VariadicReceiverBox[S]", callback: Callable[P, object]) -> S: ... + @overload + def with_paramspec(self, callback: bytes) -> bytes: ... + def with_paramspec(self, callback: object) -> object: + return callback + + @overload + def with_typevartuple[*Ts, S](self: "VariadicReceiverBox[S]", first: int, *values: *Ts) -> S: ... + @overload + def with_typevartuple(self, first: bytes) -> bytes: ... + def with_typevartuple(self, first: object, *values: object) -> object: + return first + +def accepts_int(value: int) -> object: + return value + +receiver = VariadicReceiverBox[str]() + +# revealed: Overload[[**P](callback: (**P) -> object) -> str, (callback: bytes) -> bytes] +reveal_type(receiver.with_paramspec) +reveal_type(receiver.with_paramspec(accepts_int)) # revealed: str + +# error: [invalid-assignment] +bad_paramspec_callback: Callable[[Callable[[int], object]], int] = receiver.with_paramspec + +reveal_type(receiver.with_typevartuple(1, b"value")) # revealed: str +typevartuple_callback: Callable[[int, bytes], str] = receiver.with_typevartuple + +# error: [invalid-assignment] +bad_typevartuple_callback: Callable[[int, bytes], int] = receiver.with_typevartuple ``` ## Structural protocol receivers Checking a generic protocol receiver requires solving all uses of its type variable together. Here `get()` would require `int` to be assignable to `T`, while `put()` would require `T` to be -assignable to `str`, so no `T` can satisfy `ProtocolSelf[T]`. At present, the incompatible overload -is retained because structural receiver specialization is not yet supported. +assignable to `str`, so no `T` can satisfy `ProtocolSelf[T]`. ```py from typing import Callable, Protocol, TypeVar, overload @@ -331,14 +460,51 @@ class ProtocolSelfImplementation(BaseWithProtocolSelf): def put(self, x: str) -> None: ... -# TODO: The first overload should be eliminated, leaving `bound method -# BaseWithProtocolSelf.method() -> bytes`. -reveal_type(ProtocolSelfImplementation().method) # revealed: Overload[[ProtocolSelfT]() -> ProtocolSelfT, () -> bytes] +reveal_type(ProtocolSelfImplementation().method) # revealed: bound method ProtocolSelfImplementation.method() -> bytes good_protocol_receiver: Callable[[], bytes] = ProtocolSelfImplementation().method bad_protocol_receiver: Callable[[], int] = ProtocolSelfImplementation().method # error: [invalid-assignment] ``` +## One-sided constraints from protocol receivers + +An explicit protocol receiver can constrain a method type variable without determining an exact +specialization. Keep that type variable generic so that compatible callbacks remain valid while the +receiver constraint rejects incompatible ones. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, Protocol, overload + +class Producer[T](Protocol): + def get(self) -> T: ... + +class BaseWithProducer: + @overload + def method[S](self: Producer[S], value: S) -> S: ... + @overload + def method(self, value: bytes) -> bytes: ... + def method(self, value: object) -> object: + return value + +class ProducerImplementation(BaseWithProducer): + def get(self) -> str: + return "" + +# `Producer` is covariant, so binding records `str <: S` without specializing `S` to `str`. +reveal_type(ProducerImplementation().method) # revealed: Overload[[S](value: S) -> S, (value: bytes) -> bytes] +# `S = object` satisfies the receiver constraint. +producer_callback: Callable[[object], object] = ProducerImplementation().method +# `S = int` violates the receiver constraint, and the `bytes` overload is also incompatible. +bad_producer_callback: Callable[[int], int] = ProducerImplementation().method # error: [invalid-assignment] +# The argument adds `Literal[1] <: S`, so the combined lower bound is `str | Literal[1]`. +reveal_type(ProducerImplementation().method(1)) # revealed: str | Literal[1] +``` + ## Constructor ```py @@ -1350,3 +1516,51 @@ def baz(x, y, z=None) -> bytes | list[str]: # revealed: Overload[(x, y) -> bytes, (x, y, z) -> list[str]] reveal_type(baz) ``` + +## Generic overloaded protocol members preserve receiver relationships + +An overloaded method used to satisfy a protocol receiver can relate a method-scoped type variable to +a concrete generic receiver. Binding that member must retain the scalar return type. + +```toml +[environment] +python-version = "3.12" +``` + +```pyi +from typing import Any, Generic, Protocol, TypeVar, assert_type, overload, reveal_type + +class ScalarBase: ... +class Scalar(ScalarBase): ... + +ScalarCo = TypeVar("ScalarCo", bound=ScalarBase, covariant=True, default=ScalarBase) +ShapeCo = TypeVar("ShapeCo", bound=tuple[int, ...], covariant=True, default=tuple[Any, ...]) + +class HasPhantom[T](Protocol): + def phantom(self) -> T: ... + +class Phantom(Generic[ShapeCo, ScalarCo]): + # An empty shape selects the scalar overload and relates its return type to the receiver. + @overload + def phantom[T: ScalarBase](self: "Phantom[tuple[()], T]") -> T: ... + # A non-empty shape selects the list-valued overload instead. + @overload + def phantom[Shape: tuple[int, *tuple[int, ...]], T: ScalarBase]( + self: "Phantom[Shape, T]", + ) -> list[T]: ... + +class Normal(Phantom[ShapeCo, ScalarCo], Generic[ShapeCo, ScalarCo]): + # Matching this protocol receiver requires binding the inherited `phantom` overloads. + @property + def value[T](self: "HasPhantom[T]") -> T: ... + +# The empty shape selects `phantom() -> Scalar`, so the protocol and property type is `Scalar`. +normal: Normal[tuple[()], Scalar] +assert_type(normal.value, Scalar) + +# A non-empty shape selects `phantom() -> list[Scalar]`, so the property type is `list[Scalar]`. +shaped: Normal[tuple[int], Scalar] +assert_type(shaped.phantom(), list[Scalar]) +# TODO: The receiver constraint `Scalar <: T` should propagate through invariant `list[T]`. +reveal_type(shaped.value) # revealed: Unknown +``` diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 91bc1b56fc..0b640281fa 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -1519,7 +1519,7 @@ impl<'db> UpperBound<'db> { !self.is_empty() } - fn as_single_bound(&self) -> Option> { + pub(crate) fn as_single_bound(&self) -> Option> { if self.clauses.len() != 1 { return None; } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index c7e3c49f95..53c3074f36 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -2123,6 +2123,15 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } } + /// Adds a constraint set to the pending specialization and projects its valid solutions into + /// the legacy type mappings. + pub(crate) fn add_constraint_set( + &mut self, + set: ConstraintSet<'db, 'c>, + ) -> Result<(), SpecializationError<'db>> { + self.infer_from_constraint_set(set) + } + /// Build a specialization, using a caller-provided hook to select the solution for each /// typevar. /// diff --git a/crates/ty_python_semantic/src/types/method.rs b/crates/ty_python_semantic/src/types/method.rs index de7a17e2c1..b6c5c28381 100644 --- a/crates/ty_python_semantic/src/types/method.rs +++ b/crates/ty_python_semantic/src/types/method.rs @@ -138,17 +138,9 @@ impl<'db> BoundMethodType<'db> { } return CallableSignature::from_overloads( - function_signature - .overloads - .iter() - .filter(|signature| signature.can_bind_self_to(db, receiver_type)) - .map(|signature| { - signature.bind_self_with_receiver( - db, - Some(receiver_type), - Some(typing_self_type), - ) - }), + function_signature.overloads.iter().filter_map(|signature| { + signature.bind_self_if_compatible(db, receiver_type, typing_self_type) + }), ); }; diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 09d5e9f4ac..cc93caac40 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -22,11 +22,12 @@ use super::{DynamicType, Type, TypeVarVariance, UnionType, semantic_index}; use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, OwnedConstraintSet, + PathBounds, Solutions, }; use crate::types::cyclic::ActiveRecursionDetector; use crate::types::generics::{ - ApplySpecialization, GenericContext, InferableTypeVars, Specialization, TypeVarInference, - walk_generic_context, + ApplySpecialization, GenericContext, InferableTypeVars, Specialization, SpecializationBuilder, + TypeVarInference, walk_generic_context, }; use crate::types::infer::{TypeExpressionFlags, infer_deferred_types}; use crate::types::relation::{ @@ -1150,11 +1151,86 @@ impl<'db> Signature<'db> { } } + /// Returns this signature bound to `receiver_type` if its explicit receiver annotation is + /// compatible with the bound receiver. + /// + /// Matching the receiver can constrain type variables that occur elsewhere in the signature. + /// Exact bounds determine an unambiguous specialization; one-sided constraints remain attached + /// to the bound signature for later relation checks. + pub(crate) fn bind_self_if_compatible( + &self, + db: &'db dyn Db, + receiver_type: Type<'db>, + typing_self_type: Type<'db>, + ) -> Option { + if !self.can_bind_self_to(db, receiver_type) { + return None; + } + + let bound_signature = + self.bind_self_with_receiver(db, Some(receiver_type), Some(typing_self_type)); + let Some(receiver_constraints) = bound_signature.receiver_constraints.as_ref() else { + return Some(bound_signature); + }; + + let constraints = ConstraintSetBuilder::new(); + let when = constraints.load(db, receiver_constraints); + let inferable = self.inferable_typevars(db); + + match when.solutions(db, &constraints, inferable) { + Solutions::Unsatisfiable => return None, + Solutions::Unconstrained => return Some(bound_signature), + // Each receiver path can leave a different type variable unconstrained. Preserve the + // original relation instead of combining those independent solutions. + Solutions::Constrained(solutions) if solutions.len() > 1 => { + return Some(bound_signature); + } + Solutions::Constrained(_) => {} + } + + let Some(generic_context) = self.generic_context else { + return Some(bound_signature); + }; + + let mut builder = SpecializationBuilder::new(db, &constraints, inferable); + builder.add_constraint_set(when).ok()?; + let concrete_class_receiver = + matches!(receiver_type, Type::ClassLiteral(_) | Type::GenericAlias(_)); + let specialization = builder.build_with(generic_context, |typevar, bounds| { + if let Some(bounds) = bounds + && let Some(lower) = bounds.lower + && let Some(upper) = bounds.upper.as_single_bound() + && lower.is_equivalent_to(db, upper) + && let Ok(Some(solution)) = PathBounds::default_solve(db, &constraints, bounds) + { + return Some(solution); + } + + if let Some(bounds) = bounds + && concrete_class_receiver + && bound_signature + .variance_of(db, typevar.identity(db)) + .is_covariant() + && bounds.lower.is_some_and(|lower| !lower.is_never()) + && let Ok(Some(solution)) = PathBounds::default_solve(db, &constraints, bounds) + { + return Some(solution); + } + + Some(Type::TypeVar(typevar)) + }); + + Some( + self.apply_specialization(db, specialization) + .bind_self_with_receiver(db, Some(receiver_type), Some(typing_self_type)), + ) + } + /// Returns `true` if this signature's first parameter can accept the bound `self` type. /// /// This is used to prune impossible overloads when a method is bound to a concrete receiver. /// If a signature has no positional first parameter, we conservatively keep it. - pub(crate) fn can_bind_self_to(&self, db: &'db dyn Db, self_type: Type<'db>) -> bool { + fn can_bind_self_to(&self, db: &'db dyn Db, self_type: Type<'db>) -> bool { // A dynamic receiver might be compatible with any explicit receiver annotation. if self_type.is_dynamic() { return true; From 13c5f8389a632fd7107e6208eed94acb9e74008b Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 25 Jul 2026 14:23:15 -0700 Subject: [PATCH 071/390] [ty] Make reachability analysis idempotent (#27163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes https://github.com/astral-sh/ty/issues/4080 and the missing range-cycle recovery reported in https://github.com/astral-sh/ty/issues/4077. The non-terminal-call prefix pass used thread-local recursion state to decide whether to warm preceding calls. Because the pass runs inside tracked queries, re-entry on different threads could record different Salsa dependencies and make reachability analysis non-idempotent. Remove the thread-local guard and make complete-prefix warming unconditional for large scopes, with no-op Salsa cycle recovery on cached call ranges. Crucially, leave the incomplete final call block demand-driven: warming that tail inline bypasses the range query's recovery boundary and can create a divergent inference cycle. The tail contains at most 15 calls, so the reverse query chain stays bounded without retaining per-call Salsa memos or eagerly inferring future calls. Small scopes retain the performance threshold and evaluate their calls on demand. ## Test plan - Added a focused cross-file implicit-attribute regression with more than one call block; it enters the cached range directly and reproduces the original `analyze_non_terminal_call_range` Salsa-cycle panic without recovery. - Added an incremental invalidation regression that changes a repeatedly called dependency from returning `None` to `NoReturn` and verifies end-of-scope reachability is recomputed. - Extended the deterministic corpus regression minimized from py-fuzzer seed 945 to exactly 17 module-scope call predicates, exercising the large-scope path. The prior implementation reliably fails with `infer_definition_types(...): execute: too many cycle iterations`; the corrected implementation completes immediately. - Verified the existing large implicit-attribute/stack-depth and terminal-narrowing scenarios, repeated-call performance shapes, the full `ty_python_semantic` suite, crate Clippy/hooks, and the complete CPython-3.14 differential fuzzer range (seeds 0–1000). --- .../ty_4080_fuzzed_reachability_cycle.py | 47 +++++ crates/ty_python_semantic/src/reachability.rs | 188 ++++++++++++------ crates/ty_python_semantic/src/types.rs | 2 +- 3 files changed, 176 insertions(+), 61 deletions(-) create mode 100644 crates/ty_python_semantic/resources/corpus/ty_4080_fuzzed_reachability_cycle.py diff --git a/crates/ty_python_semantic/resources/corpus/ty_4080_fuzzed_reachability_cycle.py b/crates/ty_python_semantic/resources/corpus/ty_4080_fuzzed_reachability_cycle.py new file mode 100644 index 0000000000..b7f6806056 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/ty_4080_fuzzed_reachability_cycle.py @@ -0,0 +1,47 @@ +# Regression test for https://github.com/astral-sh/ty/issues/4080 +# Minimized from py-fuzzer seed 945. Prefix warming must not cause this cycle to diverge. + +lambda: name_3 + +for name_0 in {lambda: name_0: 0}: + pass +else: + try: + while name_0: + pass + unique_name_0() + except* 0: + pass + finally: + with 0 as name_0: + pass + +try: + assert lambda: name_0 + unique_name_1() +except: + while unique_name_2: + pass +finally: + import name_3 + +match 0: + case {**name_0}: + pass + +# Together with the two calls above, keep this scope just above the prefix-warming threshold. +extra_00() +extra_01() +extra_02() +extra_03() +extra_04() +extra_05() +extra_06() +extra_07() +extra_08() +extra_09() +extra_10() +extra_11() +extra_12() +extra_13() +extra_14() diff --git a/crates/ty_python_semantic/src/reachability.rs b/crates/ty_python_semantic/src/reachability.rs index 74685355a8..51a59d608c 100644 --- a/crates/ty_python_semantic/src/reachability.rs +++ b/crates/ty_python_semantic/src/reachability.rs @@ -200,19 +200,18 @@ use crate::{ dunder_all::dunder_all_names, place::{DefinedPlace, Definedness, Place, RequiresExplicitReExport, imported_symbol}, types::{ - ActiveRecursionDetector, CallableTypes, ComparisonSoundnessPolicy, EnumClassLiteral, - KnownInstanceType, NarrowingConstraint, SpecialFormType, Type, TypeContext, UnionType, - callable_pattern_type, definite_match_pattern_type, - definite_match_pattern_type_for_subject, equality_truthiness, expand_type, - infer_narrowing_constraints, infer_same_file_expression_type, mapping_pattern_type, - pattern_binding_fallthrough_type, sequence_pattern_type_builder, singleton_pattern_type, + CallableTypes, ComparisonSoundnessPolicy, EnumClassLiteral, KnownInstanceType, + NarrowingConstraint, SpecialFormType, Type, TypeContext, UnionType, callable_pattern_type, + definite_match_pattern_type, definite_match_pattern_type_for_subject, equality_truthiness, + expand_type, infer_narrowing_constraints, infer_same_file_expression_type, + mapping_pattern_type, pattern_binding_fallthrough_type, sequence_pattern_type_builder, + singleton_pattern_type, }, }; use ruff_index::{Idx, IndexSlice}; use ruff_python_ast::name::Name; use ruff_text_size::TextRange; use rustc_hash::{FxHashMap, FxHashSet}; -use salsa::plumbing::AsId; use smallvec::SmallVec; use ty_python_core::{ BindingWithConstraints, DeclarationWithConstraint, DeclarationsIterator, FileScopeId, @@ -523,10 +522,6 @@ fn accumulate_constraint<'db>( } } -std::thread_local! { - static ACTIVE_NON_TERMINAL_CALL_PREFIXES: ActiveRecursionDetector = ActiveRecursionDetector::default(); -} - const NON_TERMINAL_CALL_CHUNK_SIZE: usize = 16; const REACHABILITY_EVALUATION_CHUNK_SIZE: usize = 256; @@ -543,7 +538,7 @@ fn predicate_scope<'db>(db: &'db dyn Db, predicate: &Predicate<'db>) -> ScopeId< } } -/// Infers preceding call predicates in source order. +/// Infers complete preceding blocks of call predicates in source order. /// /// Predicate IDs are assigned in source order, but the decision diagrams intentionally order /// predicates in reverse to reduce their size. Inferring a later call can depend on the @@ -557,10 +552,12 @@ fn predicate_scope<'db>(db: &'db dyn Db, predicate: &Predicate<'db>) -> ScopeId< /// accept the broader eager pass because it keeps the ordering simple, and checking a scope will /// typically exercise most of its predicates eventually. /// -/// Reentrant analysis of the same predicate graph skips the prefix pass: because the outer pass is -/// proceeding in source order, any preceding call needed by the current expression has already -/// been inferred. A different predicate graph performs its own pass, which is necessary when -/// inferring a call crosses into another large scope. +/// Reentrant analysis is handled by Salsa cycle recovery on the cached-range queries. The final +/// incomplete block is left for the reachability walk: it can add at most 15 nested call queries, +/// and analyzing it eagerly would bypass the range query's cycle recovery and could introduce a +/// divergent inference cycle. For large scopes, keeping the complete-block pass unconditional +/// ensures that tracked callers record the same dependencies on every thread. Small scopes do not +/// need prefix warming to bound the Salsa stack, so their calls are evaluated entirely on demand. fn analyze_non_terminal_call_prefix<'db>( db: &'db dyn Db, predicates: &IndexSlice>, @@ -573,51 +570,26 @@ fn analyze_non_terminal_call_prefix<'db>( .nth(NON_TERMINAL_CALL_CHUNK_SIZE) .is_some(); - ACTIVE_NON_TERMINAL_CALL_PREFIXES.with(|active| { - active.visit( - &scope.as_id(), - || {}, - || { - if !has_many_calls { - for predicate in &predicates.raw[..=root_predicate.index()] { - if matches!(predicate.node, PredicateNode::IsNonTerminalCall(_)) { - analyze_single(db, predicate); - } - } - return; - } - - let call_predicates = non_terminal_call_predicates(db, scope); - let call_count = - call_predicates.partition_point(|predicate| *predicate <= root_predicate); - if call_count <= NON_TERMINAL_CALL_CHUNK_SIZE { - analyze_non_terminal_calls(db, predicates, &call_predicates[..call_count]); - return; - } - - let mut start = 0; - let mut remaining = call_count / NON_TERMINAL_CALL_CHUNK_SIZE; - - while remaining > 0 { - let level = remaining.ilog2(); - let length = 1 << level; - analyze_non_terminal_call_range(db, scope, level, start >> level); - start += length; - remaining -= length; - } + if !has_many_calls { + return false; + } - let tail_start = - call_count / NON_TERMINAL_CALL_CHUNK_SIZE * NON_TERMINAL_CALL_CHUNK_SIZE; - analyze_non_terminal_calls( - db, - predicates, - &call_predicates[tail_start..call_count], - ); - }, - ); - }); + let call_predicates = non_terminal_call_predicates(db, scope); + let call_count = call_predicates.partition_point(|predicate| *predicate <= root_predicate); + let mut start = 0; + // Leave the incomplete final block demand-driven. Its reverse dependency chain is bounded by + // the block size, and every eagerly analyzed call remains behind a recoverable range query. + let mut remaining = call_count / NON_TERMINAL_CALL_CHUNK_SIZE; + + while remaining > 0 { + let level = remaining.ilog2(); + let length = 1 << level; + analyze_non_terminal_call_range(db, scope, level, start >> level); + start += length; + remaining -= length; + } - has_many_calls + true } /// Returns the statement-call predicates for `scope` in source order. @@ -654,7 +626,15 @@ fn analyze_non_terminal_calls<'db>( /// queries. Splitting ranges in half keeps the Salsa query stack logarithmic even when the first /// requested prefix contains thousands of calls. Each leaf handles multiple calls iteratively to /// avoid retaining a Salsa argument and query result for every individual predicate. -#[salsa::tracked(returns(copy), heap_size = get_size2::GetSize::get_heap_size)] +/// +/// Analyzing a call can re-enter reachability through expression inference and request this same +/// range. Recovery is a no-op because the range only warms call queries; any call still needed for +/// reachability is evaluated directly by the decision-diagram walk. +#[salsa::tracked( + returns(copy), + cycle_initial = |_, _, _, _, _| (), + heap_size = get_size2::GetSize::get_heap_size +)] fn analyze_non_terminal_call_range<'db>( db: &'db dyn Db, scope: ScopeId<'db>, @@ -1779,6 +1759,94 @@ mod tests { use ty_python_core::predicate::Predicates; use ty_python_core::semantic_index; + #[test] + fn non_terminal_call_range_recovers_cross_file_cycle() -> anyhow::Result<()> { + let mut db = setup_db(); + let calls = " other.target.ping()\n".repeat(NON_TERMINAL_CALL_CHUNK_SIZE + 1); + let a = format!( + r#"from b import B + +class A: + def setup(self, other: B) -> None: +{calls} self.target = TargetA() + +class TargetA: + def ping(self) -> None: ... +"# + ); + let b = format!( + r#"from a import A + +class B: + def setup(self, other: A) -> None: +{calls} self.target = TargetB() + +class TargetB: + def ping(self) -> None: ... +"# + ); + db.write_files([("/src/a.py", a.as_str()), ("/src/b.py", b.as_str())])?; + + let file = system_path_to_file(&db, "/src/a.py").unwrap(); + let index = semantic_index(&db, file); + let class_scope = index + .child_scopes(FileScopeId::global()) + .find(|(_, scope)| scope.node().as_class().is_some()) + .unwrap() + .0; + let setup_scope = index + .child_scopes(class_scope) + .find(|(_, scope)| scope.node().as_function().is_some()) + .unwrap() + .0 + .to_scope_id(&db, file); + + // Enter the range directly so it becomes the cycle head when inferring `other.target` + // reaches the other module and then re-enters this scope. + analyze_non_terminal_call_range(&db, setup_scope, 0, 0); + Ok(()) + } + + #[test] + fn non_terminal_call_range_invalidates_when_callable_changes() -> anyhow::Result<()> { + let mut db = setup_db(); + let source = format!( + "from dependency import callback\n\ndef f() -> None:\n{}", + " callback()\n".repeat(NON_TERMINAL_CALL_CHUNK_SIZE + 1) + ); + db.write_files([ + ("/src/dependency.py", "def callback() -> None: ..."), + ("/src/test.py", source.as_str()), + ])?; + + let file = system_path_to_file(&db, "/src/test.py").unwrap(); + let function_scope = { + let index = semantic_index(&db, file); + index.child_scopes(FileScopeId::global()).next().unwrap().0 + }; + { + let scope = function_scope.to_scope_id(&db, file); + let use_def = use_def_map(&db, scope); + assert!( + evaluate_reachability_constraint(&db, scope, use_def.end_of_scope_reachability()) + .may_be_true() + ); + } + + db.write_file( + "/src/dependency.py", + "from typing import NoReturn\ndef callback() -> NoReturn: ...", + )?; + + let scope = function_scope.to_scope_id(&db, file); + let use_def = use_def_map(&db, scope); + assert!( + evaluate_reachability_constraint(&db, scope, use_def.end_of_scope_reachability()) + .is_always_false() + ); + Ok(()) + } + #[test] fn deep_constraint_projection_does_not_overflow() -> anyhow::Result<()> { const DEPTH: usize = 100_000; diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 60cbb17891..b5116ca786 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -24,7 +24,7 @@ use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module}; pub(crate) use self::callable::UpcastPolicy; pub use self::cyclic::CycleDetector; -pub(crate) use self::cyclic::{ActiveRecursionDetector, TypeTransformer}; +pub(crate) use self::cyclic::TypeTransformer; pub(crate) use self::diagnostic::register_lints; pub use self::diagnostic::{TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_REFERENCE}; pub(crate) use self::infer::{ From a5cdc6d5813b68f6d0cd3b0da015b273cd444620 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 25 Jul 2026 14:23:39 -0700 Subject: [PATCH 072/390] [ty] Fix ParamSpec declaration hover and type navigation (#27183) ## Summary Fix hover and go-to-type-definition on PEP 695 `ParamSpec` declarations by resolving the declared parameter's inferred type in the shared IDE target resolver. ## Test plan - Existing hover regression confirms a declared `ParamSpec` now displays its inferred type and variance. - Existing navigation regression confirms go-to-type-definition resolves the declared `ParamSpec`. --- crates/ty_ide/src/goto.rs | 2 +- crates/ty_ide/src/goto_type_definition.rs | 15 ++++++++++++++- crates/ty_ide/src/hover.rs | 18 +++++++++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 3bcd0d0adc..09192764b7 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -460,6 +460,7 @@ impl GotoTarget<'_> { // (i.e. the type of `MyClass` in `MyClass()` is `` and not `() -> MyClass`) GotoTarget::Call { callable, .. } => callable.inferred_type(model), GotoTarget::TypeParamTypeVarName(typevar) => typevar.inferred_type(model), + GotoTarget::TypeParamParamSpecName(typevar) => typevar.inferred_type(model), GotoTarget::ImportModuleComponent { module_name, component_index, @@ -521,7 +522,6 @@ impl GotoTarget<'_> { | GotoTarget::PatternKeywordArgument(_) | GotoTarget::PatternMatchStarName(_) | GotoTarget::PatternMatchAsName(_) - | GotoTarget::TypeParamParamSpecName(_) | GotoTarget::TypeParamTypeVarTupleName(_) | GotoTarget::NonLocal { .. } | GotoTarget::Globals { .. } => None, diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index d9ea73e894..9e4512b1b0 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -1546,7 +1546,20 @@ mod tests { "#, ); - assert_snapshot!(test.goto_type_definition(), @"No goto target found"); + assert_snapshot!(test.goto_type_definition(), @" + info[goto-type definition]: Go to type definition + --> main.py:3:15 + | + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^^ Clicking here + | + info: Found 1 type definition + --> main.py:3:15 + | + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | -- + | + "); } #[test] diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index a7b5f3cd04..a0086cc2af 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -4083,7 +4083,23 @@ def function(): "#, ); - assert_snapshot!(test.hover(), @"Hover provided no content"); + assert_snapshot!(test.hover(), @" + AB@Alias2 (contravariant) + --------------------------------------------- + ```python + AB@Alias2 (contravariant) + ``` + --------------------------------------------- + info[hover]: Hovered content is + --> main.py:3:15 + | + 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] + | ^- + | || + | |Cursor offset + | source + | + "); } #[test] From 986ea67d68f7fef6c377f5c98b6906873aaabd72 Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 26 Jul 2026 19:42:51 -0400 Subject: [PATCH 073/390] [ty] Implement LSP `textDocument/implementation` request (#25410) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary https://github.com/astral-sh/ty/issues/3514 Implements support for `textDocument/implementation` 1. Finds one or more class roots: - for an attribute access, normalize the receiver type into class roots - for a method declaration, use the containing class as the root 2. For each class root, collect: - the method definition selected by normal MRO lookup for that root - same-named methods defined directly on known transitive subclasses 3. Deduplicate targets + preserve discovery order ## Test Plan - New snapshot tests - E2E in VSCode and Neovim Screenshot 2026-05-26 at 23 16 45 Screenshot 2026-05-26 at 23 16 28 --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Lérè --- Cargo.lock | 1 - crates/ty_ide/Cargo.toml | 1 - .../src/call_hierarchy/incoming_calls.rs | 6 +- crates/ty_ide/src/goto.rs | 59 +- crates/ty_ide/src/goto_definition.rs | 89 + crates/ty_ide/src/goto_implementation.rs | 2242 +++++++++++++++++ crates/ty_ide/src/lib.rs | 2 + crates/ty_ide/src/references.rs | 41 +- crates/ty_ide/src/stub_mapping.rs | 11 +- crates/ty_python_semantic/src/lib.rs | 7 +- .../src/types/ide_support.rs | 779 +++++- crates/ty_server/src/capabilities.rs | 14 + crates/ty_server/src/server/api.rs | 5 + crates/ty_server/src/server/api/requests.rs | 2 + .../api/requests/goto_implementation.rs | 77 + crates/ty_server/tests/e2e/implementation.rs | 116 + crates/ty_server/tests/e2e/main.rs | 12 + .../e2e__initialize__initialization.snap | 1 + ...ialize__initialization_with_workspace.snap | 1 + 19 files changed, 3382 insertions(+), 84 deletions(-) create mode 100644 crates/ty_ide/src/goto_implementation.rs create mode 100644 crates/ty_server/src/server/api/requests/goto_implementation.rs create mode 100644 crates/ty_server/tests/e2e/implementation.rs diff --git a/Cargo.lock b/Cargo.lock index 1ec238c5fa..e2ac903157 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4717,7 +4717,6 @@ dependencies = [ "indexmap", "insta", "itertools 0.15.0", - "memchr", "rayon", "regex", "ruff_db", diff --git a/crates/ty_ide/Cargo.toml b/crates/ty_ide/Cargo.toml index 17d16d76a4..e57aef33ec 100644 --- a/crates/ty_ide/Cargo.toml +++ b/crates/ty_ide/Cargo.toml @@ -37,7 +37,6 @@ compact_str = { workspace = true } get-size2 = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } -memchr = { workspace = true } rayon = { workspace = true } regex = { workspace = true } rustc-hash = { workspace = true } diff --git a/crates/ty_ide/src/call_hierarchy/incoming_calls.rs b/crates/ty_ide/src/call_hierarchy/incoming_calls.rs index 8d2c2e04c3..69e0d9fae0 100644 --- a/crates/ty_ide/src/call_hierarchy/incoming_calls.rs +++ b/crates/ty_ide/src/call_hierarchy/incoming_calls.rs @@ -1,6 +1,6 @@ use crate::call_hierarchy::{CalleeLeaf, module_detail}; use crate::goto::{Definitions, GotoTarget, find_goto_target}; -use crate::references::{contains_identifier, has_any_external_visible_definitions}; +use crate::references::has_any_external_visible_definitions; use crate::{CallHierarchyItem, Db, SymbolKind}; use rayon::prelude::*; use ruff_db::files::File; @@ -16,7 +16,9 @@ use ty_project::parallel::{ParallelIteratorExt, minimum_parallel_job_len}; use ty_python_core::scope::{NodeWithScopeKind, ScopeKind}; use ty_python_semantic::types::ide_support::static_member_type_for_attribute; use ty_python_semantic::types::{PropertyAccessorRole, Type}; -use ty_python_semantic::{HasDefinition as _, HasType as _, ImportAliasResolution, SemanticModel}; +use ty_python_semantic::{ + HasDefinition as _, HasType as _, ImportAliasResolution, SemanticModel, contains_identifier, +}; /// Salsa snapshots coordinate clone and drop through shared state. For ordinary targets, most /// files are rejected by the text prefilter, so process enough files per job to amortize that diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 09192764b7..ef0160f3fc 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -246,7 +246,7 @@ pub(crate) enum GotoTarget<'a> { pub(crate) struct Definitions<'db>(Vec>); impl<'db> Definitions<'db> { - fn new(mut resolved: Vec>) -> Self { + pub(crate) fn new(mut resolved: Vec>) -> Self { for index in (1..resolved.len()).rev() { if resolved[..index].contains(&resolved[index]) { resolved.remove(index); @@ -339,8 +339,43 @@ impl<'db> Definitions<'db> { goto_target: &GotoTarget<'_>, ) -> Option> { let definitions = self.goto_declaration(model, goto_target)?; - let resolved = StubMapper::new(model.db()).map_definitions(definitions.0); - Some(Self::new(resolved)) + Some(definitions.map_stubs(model.db())) + } + + /// Map definitions from stub files to corresponding source implementations. + pub(crate) fn map_stubs(self, db: &'db dyn ty_python_semantic::Db) -> Definitions<'db> { + let resolved = StubMapper::new(db).map_definitions(self.0); + Self::new(resolved) + } + + /// Map stub definitions to corresponding source implementations for implementation lookup. + /// + /// Stub definitions without source mappings are discarded. Returns `None` if no definitions + /// remain. + pub(crate) fn map_stubs_for_implementation( + self, + db: &'db dyn ty_python_semantic::Db, + ) -> Option> { + let stub_mapper = StubMapper::new(db); + let resolved: Vec<_> = self + .0 + .into_iter() + .flat_map(|definition| { + if definition.focus_range(db).file().is_stub(db) { + stub_mapper + .map_definition_to_source(&definition) + .unwrap_or_default() + } else { + vec![definition] + } + }) + .collect(); + + if resolved.is_empty() { + None + } else { + Some(Self::new(resolved)) + } } /// Convert these semantic definitions to editor-facing navigation targets. @@ -546,6 +581,24 @@ impl GotoTarget<'_> { } } + /// Gets definitions for the underlying expression, excluding call dispatch targets. + pub(crate) fn expression_definitions<'db>( + &self, + model: &SemanticModel<'db>, + alias_resolution: ImportAliasResolution, + ) -> Option> { + let expression = match self { + GotoTarget::Expression(expression) + | GotoTarget::Call { + callable: expression, + .. + } => *expression, + _ => return None, + }; + + definitions_for_expression(model, expression, alias_resolution).map(Definitions::new) + } + /// Gets the definitions for this goto target. /// /// The `alias_resolution` parameter controls whether import aliases diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index 2022851567..4a88b34d56 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -594,6 +594,91 @@ class MyOtherClass: "); } + /// goto-definition on a class attribute should go to the .py not the .pyi + #[test] + fn goto_definition_stub_map_class_attribute() { + let test = CursorTest::builder() + .source( + "main.py", + " +from mymodule import MyClass +def f(x: MyClass): + x.sound +", + ) + .source( + "mymodule.py", + r#" +class MyClass: + sound: str = "generic" +"#, + ) + .source( + "mymodule.pyi", + r#" +class MyClass: + sound: str +"#, + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r#" + info[goto-definition]: Go to definition + --> main.py:4:7 + | + 4 | x.sound + | ^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:3:5 + | + 3 | sound: str = "generic" + | ----- + | + "#); + } + + /// goto-definition on a module-level variable should go to the .py not the .pyi + #[test] + fn goto_definition_stub_map_module_variable() { + let test = CursorTest::builder() + .source( + "main.py", + " +import mymodule +mymodule.COUNT +", + ) + .source( + "mymodule.py", + r#" +COUNT = 0 +"#, + ) + .source( + "mymodule.pyi", + r#" +COUNT: int +"#, + ) + .build(); + + assert_snapshot!(test.goto_definition(), @r" + info[goto-definition]: Go to definition + --> main.py:3:10 + | + 3 | mymodule.COUNT + | ^^^^^ Clicking here + | + info: Found 1 definition + --> mymodule.py:2:1 + | + 2 | COUNT = 0 + | ----- + | + "); + } + /// goto-definition on a class function should go to the .py not the .pyi #[test] fn goto_definition_stub_map_class_function() { @@ -2613,6 +2698,7 @@ class GenericFoo[T](Base): Definition, Declaration, TypeDefinition, + Implementation, } impl GotoAction { @@ -2621,6 +2707,7 @@ class GenericFoo[T](Base): GotoAction::Definition => "goto-definition", GotoAction::Declaration => "goto-declaration", GotoAction::TypeDefinition => "goto-type definition", + GotoAction::Implementation => "goto-implementation", } } @@ -2629,6 +2716,7 @@ class GenericFoo[T](Base): GotoAction::Definition => "Go to definition", GotoAction::Declaration => "Go to declaration", GotoAction::TypeDefinition => "Go to type definition", + GotoAction::Implementation => "Go to implementation", } } @@ -2637,6 +2725,7 @@ class GenericFoo[T](Base): GotoAction::Definition => "definition", GotoAction::Declaration => "declaration", GotoAction::TypeDefinition => "type definition", + GotoAction::Implementation => "implementation", } } } diff --git a/crates/ty_ide/src/goto_implementation.rs b/crates/ty_ide/src/goto_implementation.rs new file mode 100644 index 0000000000..5038ac6c6e --- /dev/null +++ b/crates/ty_ide/src/goto_implementation.rs @@ -0,0 +1,2242 @@ +//! Finds known implementations of classes and class members. +//! +//! This module implements the `textDocument/implementation` request, commonly exposed as **Go to +//! Implementation** in an editor. It follows nominal inheritance and uses receiver type to decide +//! where to start searching. +//! +//! For example, consider this class hierarchy: +//! +//! ```python +//! class Animal: +//! sound = "unknown" +//! +//! def speak(self) -> str: +//! return self.sound +//! +//! class Dog(Animal): +//! sound = "woof" +//! +//! def speak(self) -> str: +//! return self.sound +//! +//! def make_sound(animal: Animal) -> str: +//! return animal.speak() +//! ``` +//! +//! A request on `animal.speak()` starts from `Animal`, so it returns both `Animal.speak` and +//! `Dog.speak`. A request on a value known to be a `Dog` returns only the implementation selected +//! for `Dog`. +//! +//! # Supported request locations +//! +//! - A method or data attribute use, such as `animal.speak()` or `animal.sound`. +//! - The name in a method declaration, such as `speak` in the definition of `Animal` above. The +//! containing class becomes the starting point. +//! - The name in a class declaration. The result includes that class and its known subclasses. +//! - A class name used as a base class, type annotation, or constructor call. Qualified names such +//! as `module.Animal` and `Outer.Inner` are also supported. +//! +//! # Selecting results +//! +//! - An overloaded method resolves to its implementation body when one is available. +//! - Reading, assigning, or deleting a property selects its getter, setter, or deleter (the +//! corresponding property accessor). +//! - A declaration in a `.pyi` stub file maps to the corresponding source definition when +//! possible. +//! - A class or member definition that cannot run in the configured Python environment is +//! excluded. A request directly on an unreachable class, method, property getter, setter, or +//! deleter returns no result. +//! +//! # Limits +//! +//! - Classes are not discovered just because they provide the methods required by a +//! `typing.Protocol` (structural subtyping); they must explicitly inherit from that protocol. +//! - Properties created with Python's built-in `property` are recognized. Other objects that +//! customize what happens when an attribute is read, assigned, or deleted (descriptors) are not +//! interpreted as properties. + +use crate::goto::{Definitions, GotoTarget, find_goto_target}; +use crate::{Db, NavigationTarget, NavigationTargets, RangedValue}; +use rayon::prelude::*; +use ruff_db::files::{File, FileRange}; +use ruff_db::parsed::parsed_module; +use ruff_text_size::{Ranged, TextSize}; +use ty_project::parallel::ParallelIteratorExt; +use ty_python_semantic::{ + ImplementationsFinder, ImportAliasResolution, ResolvedDefinition, SemanticModel, +}; + +/// Returns the known implementations for the supported target at `offset`. +/// +/// Returns `None` when the cursor is not on a supported target or no implementation can be +/// identified. +pub fn goto_implementation( + db: &dyn Db, + file: File, + offset: TextSize, +) -> Option> { + let module = parsed_module(db, file).load(db); + let model = SemanticModel::new(db, file); + let goto_target = find_goto_target(&model, &module, offset)?; + let finder = prepare_implementations_finder_for_goto_target(&model, &goto_target)?; + + let mut candidate_files: Vec = db + .project() + .files(db) + .iter() + .copied() + .filter(|candidate| *candidate != file) + .collect(); + candidate_files.push(file); + + let batches = candidate_files + .into_par_iter() + .map_with_db(db, |db, file| { + let definitions = finder.implementations_for_file(db, file); + definitions_to_implementation_targets(db, definitions) + }) + .collect::>(); + + let mut implementation_targets = + definitions_to_implementation_targets(db, finder.into_initial_definitions()); + implementation_targets.extend(batches.into_iter().flatten()); + + if implementation_targets.is_empty() { + return None; + } + + let implementation_targets = implementation_targets.into_iter().collect(); + + Some(RangedValue { + range: FileRange::new(file, goto_target.range()), + value: implementation_targets, + }) +} + +/// Select and prepare the appropriate `ImplementationsFinder` for `goto_target`. +fn prepare_implementations_finder_for_goto_target<'db>( + model: &SemanticModel<'db>, + goto_target: &GotoTarget<'_>, +) -> Option> { + match goto_target { + GotoTarget::Expression(expression) + | GotoTarget::Call { + callable: expression, + .. + } if matches!( + expression, + ruff_python_ast::ExprRef::Name(_) | ruff_python_ast::ExprRef::Attribute(_) + ) => + { + goto_target + .expression_definitions(model, ImportAliasResolution::ResolveAliases) + .and_then(|definitions| { + ImplementationsFinder::for_class_reference( + model.db(), + definitions.iter().as_slice(), + ) + }) + .or_else(|| match expression { + ruff_python_ast::ExprRef::Attribute(attribute) => { + ImplementationsFinder::for_attribute(model, attribute) + } + _ => None, + }) + } + GotoTarget::StringAnnotationSubexpr { .. } => goto_target + .definitions(model, ImportAliasResolution::ResolveAliases) + .and_then(|definitions| { + ImplementationsFinder::for_class_reference( + model.db(), + definitions.iter().as_slice(), + ) + }), + GotoTarget::FunctionDef(function) => ImplementationsFinder::for_method(model, function), + GotoTarget::ClassDef(class) => ImplementationsFinder::for_class(model, class), + _ => None, + } +} + +fn definitions_to_implementation_targets( + db: &dyn Db, + definitions: Vec, +) -> Vec { + Definitions::new(definitions) + .map_stubs_for_implementation(db) + .map(|definitions| { + definitions + .into_navigation_targets(db) + .into_iter() + .collect() + }) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use crate::goto_implementation; + use crate::tests::{CursorTest, cursor_test}; + use insta::assert_snapshot; + use ruff_db::system::SystemPathBuf; + use ty_project::Db as _; + + #[test] + fn implementation_method_family_from_attribute() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + def speak(self): ... + + class Cat(Animal): + def speak(self): ... + + def f(animal: Animal): + animal.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:12:12 + | + 12 | animal.speak() + | ^^^^^ Clicking here + | + info: Found 3 implementations + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + 4 | + 5 | class Dog(Animal): + 6 | def speak(self): ... + | ----- + 7 | + 8 | class Cat(Animal): + 9 | def speak(self): ... + | ----- + | + "); + } + + #[test] + fn implementation_abstract_root_method_is_included() { + let test = cursor_test( + r#" + from abc import ABC, abstractmethod + + class Animal(ABC): + @abstractmethod + def speak(self) -> str: ... + + class Dog(Animal): + def speak(self) -> str: + return "woof" + + class Cat(Animal): + def speak(self) -> str: + return "meow" + + def f(animal: Animal): + animal.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:17:12 + | + 17 | animal.speak() + | ^^^^^ Clicking here + | + info: Found 3 implementations + --> main.py:6:9 + | + 6 | def speak(self) -> str: ... + | ----- + 7 | + 8 | class Dog(Animal): + 9 | def speak(self) -> str: + | ----- + | + ::: main.py:13:9 + | + 13 | def speak(self) -> str: + | ----- + | + "); + } + + #[test] + fn implementation_transitive_subclass_overrides() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Mammal(Animal): + pass + + class Dog(Mammal): + def speak(self): ... + + def f(animal: Animal): + animal.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:12:12 + | + 12 | animal.speak() + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + | + ::: main.py:9:9 + | + 9 | def speak(self): ... + | ----- + | + "); + } + + #[test] + fn implementation_inherited_method_from_concrete_receiver() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + pass + + dog = Dog() + dog.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:9:5 + | + 9 | dog.speak() + | ^^^^^ Clicking here + | + info: Found 1 implementation + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + | + "); + } + + #[test] + fn implementation_overridden_method_from_concrete_receiver() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + def speak(self): ... + + class Cat(Animal): + def speak(self): ... + + def f(dog: Dog): + dog.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:12:9 + | + 12 | dog.speak() + | ^^^^^ Clicking here + | + info: Found 1 implementation + --> main.py:6:9 + | + 6 | def speak(self): ... + | ----- + | + "); + } + + #[test] + fn implementation_shadowed_inherited_method_from_concrete_receiver() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + speak = 1 + + dog = Dog() + dog.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:9:5 + | + 9 | dog.speak() + | ^^^^^ Clicking here + | + info: Found 1 implementation + --> main.py:6:5 + | + 6 | speak = 1 + | ----- + | + "); + } + + #[test] + fn implementation_unresolved_root_does_not_scan_subclasses() { + let test = cursor_test( + r#" + class Dog: + def speak(self): ... + + def f(value: object): + value.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_overloaded_method_returns_implementation() { + let test = cursor_test( + r#" + from typing import overload + + class Animal: + @overload + def speak(self, volume: int) -> int: ... + @overload + def speak(self, volume: str) -> str: ... + def speak(self, volume: int | str) -> int | str: + return volume + + def f(animal: Animal): + animal.speak(1) + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:13:12 + | + 13 | animal.speak(1) + | ^^^^^ Clicking here + | + info: Found 1 implementation + --> main.py:9:9 + | + 9 | def speak(self, volume: int | str) -> int | str: + | ----- + | + "); + } + + #[test] + fn implementation_overload_only_root_scans_subclasses() { + let test = cursor_test( + r#" + from typing import overload + + class Animal: + @overload + def speak(self, volume: int) -> int: ... + @overload + def speak(self, volume: str) -> str: ... + + class Dog(Animal): + def speak(self, volume: int | str) -> int | str: + return volume + + def f(animal: Animal): + animal.speak(1) + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:15:12 + | + 15 | animal.speak(1) + | ^^^^^ Clicking here + | + info: Found 1 implementation + --> main.py:11:9 + | + 11 | def speak(self, volume: int | str) -> int | str: + | ----- + | + "); + } + + #[test] + fn implementation_property_setter_definition() { + let test = cursor_test( + r#" + class Base: + @property + def value(self) -> int: ... + + @value.setter + def value(self, value: int) -> None: ... + + @value.deleter + def value(self) -> None: ... + + class Child(Base): + @property + def value(self) -> int: ... + + @value.setter + def value(self, value: int) -> None: ... + + @value.deleter + def value(self) -> None: ... + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:7:9 + | + 7 | def value(self, value: int) -> None: ... + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:7:9 + | + 7 | def value(self, value: int) -> None: ... + | ----- + | + ::: main.py:17:9 + | + 17 | def value(self, value: int) -> None: ... + | ----- + | + "); + } + + #[test] + fn implementation_property_read() { + let test = cursor_test( + r#" + class Base: + @property + def value(self) -> int: ... + + @value.setter + def value(self, value: int) -> None: ... + + @value.deleter + def value(self) -> None: ... + + class Child(Base): + @property + def value(self) -> int: ... + + @value.setter + def value(self, value: int) -> None: ... + + @value.deleter + def value(self) -> None: ... + + def f(base: Base): + return base.value + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:23:17 + | + 23 | return base.value + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:4:9 + | + 4 | def value(self) -> int: ... + | ----- + | + ::: main.py:14:9 + | + 14 | def value(self) -> int: ... + | ----- + | + "); + } + + #[test] + fn implementation_property_write() { + let test = cursor_test( + r#" + class Base: + @property + def value(self) -> int: ... + + @value.setter + def value(self, value: int) -> None: ... + + @value.deleter + def value(self) -> None: ... + + class Child(Base): + @property + def value(self) -> int: ... + + @value.setter + def value(self, value: int) -> None: ... + + @value.deleter + def value(self) -> None: ... + + def f(base: Base, value: int): + base.value = value + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:23:10 + | + 23 | base.value = value + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:7:9 + | + 7 | def value(self, value: int) -> None: ... + | ----- + | + ::: main.py:17:9 + | + 17 | def value(self, value: int) -> None: ... + | ----- + | + "); + } + + #[test] + fn implementation_inherited_method_from_union_receivers_deduplicates() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + pass + + class Cat(Animal): + pass + + def f(pet: Dog | Cat): + pet.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:12:9 + | + 12 | pet.speak() + | ^^^^^ Clicking here + | + info: Found 1 implementation + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + | + "); + } + + #[test] + fn implementation_typevar_bound_receiver() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + def speak(self): ... + + def f[T: Animal](animal: T): + animal.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:9:12 + | + 9 | animal.speak() + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + 4 | + 5 | class Dog(Animal): + 6 | def speak(self): ... + | ----- + | + "); + } + + #[test] + fn implementation_classmethod_receiver() { + let test = cursor_test( + r#" + class Animal: + @classmethod + def speak(cls): ... + + @classmethod + def call(cls): + cls.speak() + + class Dog(Animal): + @classmethod + def speak(cls): ... + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:8:13 + | + 8 | cls.speak() + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:4:9 + | + 4 | def speak(cls): ... + | ----- + | + ::: main.py:12:9 + | + 12 | def speak(cls): ... + | ----- + | + "); + } + + #[test] + fn implementation_typevar_bound_class_object_receiver() { + let test = cursor_test( + r#" + class Animal: + @classmethod + def speak(cls): ... + + class Dog(Animal): + @classmethod + def speak(cls): ... + + def f[T: Animal](cls: type[T]): + cls.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:11:9 + | + 11 | cls.speak() + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:4:9 + | + 4 | def speak(cls): ... + | ----- + 5 | + 6 | class Dog(Animal): + 7 | @classmethod + 8 | def speak(cls): ... + | ----- + | + "); + } + + #[test] + fn implementation_subclass_through_import_alias() { + let test = CursorTest::builder() + .source( + "base.py", + r#" + class Base: + def method(self): ... + "#, + ) + .source( + "aliases.py", + r#" + from base import Base as B + "#, + ) + .source( + "child.py", + r#" + from aliases import B + + class Child(B): + def method(self): ... + "#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> base.py:3:9 + | + 3 | def method(self): ... + | ^^^^^^ Clicking here + | + info: Found 2 implementations + --> base.py:3:9 + | + 3 | def method(self): ... + | ------ + | + ::: child.py:5:9 + | + 5 | def method(self): ... + | ------ + | + "); + } + + #[test] + fn implementation_parallel_candidate_batches_preserve_order() { + let test = CursorTest::builder() + .source( + "base.py", + r#" + class Base: + def method(self): ... + "#, + ) + .source( + "z_child.py", + r#" + from base import Base + + class ZChild(Base): + def method(self): ... + "#, + ) + .source( + "a_child.py", + r#" + from base import Base + + class AChild(Base): + def method(self): ... + "#, + ) + .build(); + + let targets = salsa::attach(&test.db, || { + goto_implementation(&test.db, test.cursor.file, test.cursor.offset) + .expect("implementation targets") + }); + let paths = targets + .into_iter() + .map(|target| target.file().path(&test.db).to_string()) + .collect::>(); + + assert_eq!(paths, ["/base.py", "/z_child.py", "/a_child.py"]); + } + + #[test] + fn implementation_stub_map_class_method() { + let test = CursorTest::builder() + .source( + "main.py", + " +from mymodule import MyClass +x = MyClass(0) +x.action() +", + ) + .source( + "mymodule.py", + r#" +class MyClass: + def __init__(self, val): + self.val = val + def action(self): + print(self.val) +"#, + ) + .source( + "mymodule.pyi", + r#" +class MyClass: + def __init__(self, val: bool): ... + def action(self): ... +"#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:4:3 + | + 4 | x.action() + | ^^^^^^ Clicking here + | + info: Found 1 implementation + --> mymodule.py:5:9 + | + 5 | def action(self): + | ------ + | + "); + } + + #[test] + fn implementation_stub_map_overloaded_class_method() { + let test = CursorTest::builder() + .source( + "main.py", + " +from mymodule import MyClass +x = MyClass(0) +x.action(1) +", + ) + .source( + "mymodule.py", + r#" +class MyClass: + def __init__(self, val): + self.val = val + def action(self, value): + return value +"#, + ) + .source( + "mymodule.pyi", + r#" +from typing import overload + +class MyClass: + def __init__(self, val: bool): ... + @overload + def action(self, value: int) -> int: ... + @overload + def action(self, value: str) -> str: ... +"#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:4:3 + | + 4 | x.action(1) + | ^^^^^^ Clicking here + | + info: Found 1 implementation + --> mymodule.py:5:9 + | + 5 | def action(self, value): + | ------ + | + "); + } + + #[test] + fn implementation_stub_only_overloaded_class_method() { + let test = CursorTest::builder() + .source( + "main.py", + " +from mymodule import MyClass +x = MyClass(0) +x.action(1) +", + ) + .source( + "mymodule.pyi", + r#" +from typing import overload + +class MyClass: + def __init__(self, val: bool): ... + @overload + def action(self, value: int) -> int: ... + @overload + def action(self, value: str) -> str: ... +"#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_method_declaration_root() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + def speak(self): ... + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:3:9 + | + 3 | def speak(self): ... + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + 4 | + 5 | class Dog(Animal): + 6 | def speak(self): ... + | ----- + | + "); + } + + #[test] + fn implementation_unsupported_target() { + let test = cursor_test( + r#" + def function(): ... + + function() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_class_family() { + let test = cursor_test( + r#" + from abc import ABC + + class Animal(ABC): + pass + + class Dog(Animal): + pass + + class Cat(Animal): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:4:7 + | + 4 | class Animal(ABC): + | ^^^^^^ Clicking here + | + info: Found 3 implementations + --> main.py:4:7 + | + 4 | class Animal(ABC): + | ------ + 5 | pass + 6 | + 7 | class Dog(Animal): + | --- + 8 | pass + 9 | + 10 | class Cat(Animal): + | --- + | + "); + } + + #[test] + fn implementation_class_family_in_request_file_excluded_from_project() { + let mut test = CursorTest::builder() + .source( + "main.py", + r#" + class Animal: + pass + + class Dog(Animal): + pass + "#, + ) + .source("included.py", "") + .build(); + + test.db + .project() + .set_included_paths(&mut test.db, vec![SystemPathBuf::from("/included.py")]); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:2:7 + | + 2 | class Animal: + | ^^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:2:7 + | + 2 | class Animal: + | ------ + 3 | pass + 4 | + 5 | class Dog(Animal): + | --- + | + "); + } + + #[test] + fn implementation_class_no_subclasses() { + let test = cursor_test( + r#" + class Widget: + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:2:7 + | + 2 | class Widget: + | ^^^^^^ Clicking here + | + info: Found 1 implementation + --> main.py:2:7 + | + 2 | class Widget: + | ------ + | + "); + } + + #[test] + fn implementation_class_intermediate_root() { + let test = cursor_test( + r#" + class Animal: + pass + + class Mammal(Animal): + pass + + class Dog(Mammal): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:5:7 + | + 5 | class Mammal(Animal): + | ^^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:5:7 + | + 5 | class Mammal(Animal): + | ------ + 6 | pass + 7 | + 8 | class Dog(Mammal): + | --- + | + "); + } + + #[test] + fn implementation_class_diamond_dedup() { + let test = cursor_test( + r#" + class Base: + pass + + class Left(Base): + pass + + class Right(Base): + pass + + class Diamond(Left, Right): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:2:7 + | + 2 | class Base: + | ^^^^ Clicking here + | + info: Found 4 implementations + --> main.py:2:7 + | + 2 | class Base: + | ---- + 3 | pass + 4 | + 5 | class Left(Base): + | ---- + 6 | pass + 7 | + 8 | class Right(Base): + | ----- + 9 | pass + 10 | + 11 | class Diamond(Left, Right): + | ------- + | + "); + } + + #[test] + fn implementation_class_generic_base() { + let test = cursor_test( + r#" + class Container[T]: + pass + + class IntContainer(Container[int]): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:2:7 + | + 2 | class Container[T]: + | ^^^^^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:2:7 + | + 2 | class Container[T]: + | --------- + 3 | pass + 4 | + 5 | class IntContainer(Container[int]): + | ------------ + | + "); + } + + #[test] + fn implementation_class_reference_in_annotation() { + let test = cursor_test( + r#" + class Animal: + pass + + class Dog(Animal): + pass + + def f(x: Animal): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:8:10 + | + 8 | def f(x: Animal): + | ^^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:2:7 + | + 2 | class Animal: + | ------ + 3 | pass + 4 | + 5 | class Dog(Animal): + | --- + | + "); + } + + #[test] + fn implementation_class_reference_in_string_annotation() { + let test = cursor_test( + r#" + class Animal: + pass + + class Dog(Animal): + pass + + def f(x: "Animal"): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:8:11 + | + 8 | def f(x: "Animal"): + | ^^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:2:7 + | + 2 | class Animal: + | ------ + 3 | pass + 4 | + 5 | class Dog(Animal): + | --- + | + "#); + } + + #[test] + fn implementation_qualified_class_reference_in_base_list() { + let test = CursorTest::builder() + .source( + "animals.py", + r#" + class Animal: + pass + "#, + ) + .source( + "main.py", + r#" + import animals + + class Dog(animals.Animal): + pass + "#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:4:19 + | + 4 | class Dog(animals.Animal): + | ^^^^^^ Clicking here + | + info: Found 2 implementations + --> animals.py:2:7 + | + 2 | class Animal: + | ------ + | + ::: main.py:4:7 + | + 4 | class Dog(animals.Animal): + | --- + | + "); + } + + #[test] + fn implementation_qualified_class_reference_in_instantiation() { + let test = CursorTest::builder() + .source( + "animals.py", + r#" + class Animal: + pass + "#, + ) + .source( + "main.py", + r#" + import animals + + class Dog(animals.Animal): + pass + + animals.Animal() + "#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:7:9 + | + 7 | animals.Animal() + | ^^^^^^ Clicking here + | + info: Found 2 implementations + --> animals.py:2:7 + | + 2 | class Animal: + | ------ + | + ::: main.py:4:7 + | + 4 | class Dog(animals.Animal): + | --- + | + "); + } + + #[test] + fn implementation_class_call_with_assigned_constructor() { + let test = cursor_test( + r#" + def init(self): + pass + + class Base: + __init__ = init + + class Child(Base): + pass + + Base() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:11:1 + | + 11 | Base() + | ^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:5:7 + | + 5 | class Base: + | ---- + 6 | __init__ = init + 7 | + 8 | class Child(Base): + | ----- + | + "); + } + + #[test] + fn implementation_nested_class_reference() { + let test = cursor_test( + r#" + class Outer: + class Inner: + pass + + class SubInner(Outer.Inner): + pass + + def f(x: Outer.Inner): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:9:16 + | + 9 | def f(x: Outer.Inner): + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:3:11 + | + 3 | class Inner: + | ----- + 4 | pass + 5 | + 6 | class SubInner(Outer.Inner): + | -------- + | + "); + } + + #[test] + fn implementation_attribute_bound_to_class() { + // An attribute that resolves to a class object is a class reference, not a member + // lookup, matching how a bare name bound to a class behaves. + let test = cursor_test( + r#" + class Dog: + pass + + class Factory: + dog_cls = Dog + + def f(factory: Factory): + factory.dog_cls + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:9:13 + | + 9 | factory.dog_cls + | ^^^^^^^ Clicking here + | + info: Found 1 implementation + --> main.py:2:7 + | + 2 | class Dog: + | --- + | + "); + } + + #[test] + fn implementation_mixed_class_value_attribute_uses_member_bindings() { + let test = cursor_test( + r#" + flag: bool + + class Dog: + pass + + class Puppy(Dog): + pass + + class Factory: + item: type[Dog] | int + if flag: + item = Dog + else: + item = 0 + + factory = Factory() + factory.item + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:18:9 + | + 18 | factory.item + | ^^^^ Clicking here + | + info: Found 3 implementations + --> main.py:11:5 + | + 11 | item: type[Dog] | int + | ---- + 12 | if flag: + 13 | item = Dog + | ---- + 14 | else: + 15 | item = 0 + | ---- + | + "); + } + + #[test] + fn implementation_mixed_class_and_module_binding_is_unsupported() { + let test = CursorTest::builder() + .source("flag_source.py", "flag: bool") + .source("helper.py", "value = 1") + .source( + "main.py", + r#" + import flag_source + + class Dog: + pass + + class Puppy(Dog): + pass + + if flag_source.flag: + import helper as Item + else: + Item = Dog + + Item + "#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_class_instance_reference_is_unsupported() { + // A bare reference to an instance is not a class reference, so it does not resolve to the + // class implementation family. + let test = cursor_test( + r#" + class Animal: + pass + + class Dog(Animal): + pass + + def f(animal: Animal): + animal + "#, + ); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_class_stub_mapped_subclass() { + let test = CursorTest::builder() + .source( + "main.py", + r#" + class Base: + pass + "#, + ) + .source( + "mymodule.py", + r#" + from main import Base + + class Derived(Base): + pass + "#, + ) + .source( + "mymodule.pyi", + r#" + from main import Base + + class Derived(Base): ... + "#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:2:7 + | + 2 | class Base: + | ^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:2:7 + | + 2 | class Base: + | ---- + | + ::: mymodule.py:4:7 + | + 4 | class Derived(Base): + | ------- + | + "); + } + + #[test] + fn implementation_attribute_family_from_base_receiver() { + let test = cursor_test( + r#" + class Animal: + sound: str = "generic" + + class Dog(Animal): + sound: str = "woof" + + class Cat(Animal): + sound: str = "meow" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:12:12 + | + 12 | animal.sound + | ^^^^^ Clicking here + | + info: Found 3 implementations + --> main.py:3:5 + | + 3 | sound: str = "generic" + | ----- + 4 | + 5 | class Dog(Animal): + 6 | sound: str = "woof" + | ----- + 7 | + 8 | class Cat(Animal): + 9 | sound: str = "meow" + | ----- + | + "#); + } + + #[test] + fn implementation_attribute_plain_assignment() { + let test = cursor_test( + r#" + class Animal: + sound = "generic" + + class Dog(Animal): + sound = "woof" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:9:12 + | + 9 | animal.sound + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:3:5 + | + 3 | sound = "generic" + | ----- + 4 | + 5 | class Dog(Animal): + 6 | sound = "woof" + | ----- + | + "#); + } + + #[test] + fn implementation_attribute_bare_annotation_declaration() { + let test = cursor_test( + r#" + class Animal: + sound: str + + class Dog(Animal): + sound: str = "woof" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:9:12 + | + 9 | animal.sound + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:3:5 + | + 3 | sound: str + | ----- + 4 | + 5 | class Dog(Animal): + 6 | sound: str = "woof" + | ----- + | + "#); + } + + #[test] + fn implementation_attribute_method_and_data_mixed() { + let test = cursor_test( + r#" + class Animal: + def speak(self): ... + + class Dog(Animal): + speak = 1 + + def f(animal: Animal): + animal.speak + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:9:12 + | + 9 | animal.speak + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:3:9 + | + 3 | def speak(self): ... + | ----- + 4 | + 5 | class Dog(Animal): + 6 | speak = 1 + | ----- + | + "); + } + + #[test] + fn implementation_attribute_instance_attribute_family() { + let test = cursor_test( + r#" + class Animal: + def __init__(self): + self.sound = "generic" + + class Dog(Animal): + def __init__(self): + self.sound = "woof" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:11:12 + | + 11 | animal.sound + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:4:9 + | + 4 | self.sound = "generic" + | ---------- + 5 | + 6 | class Dog(Animal): + 7 | def __init__(self): + 8 | self.sound = "woof" + | ---------- + | + "#); + } + + #[test] + fn implementation_attribute_instance_attribute_from_concrete_receiver() { + let test = cursor_test( + r#" + class Animal: + def __init__(self): + self.sound = "generic" + + class Dog(Animal): + pass + + class Cat(Animal): + def __init__(self): + self.sound = "meow" + + def f(dog: Dog): + dog.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:14:9 + | + 14 | dog.sound + | ^^^^^ Clicking here + | + info: Found 1 implementation + --> main.py:4:9 + | + 4 | self.sound = "generic" + | ---------- + | + "#); + } + + #[test] + fn implementation_attribute_class_body_and_instance_mixed() { + let test = cursor_test( + r#" + class Animal: + sound: str = "generic" + + class Dog(Animal): + def __init__(self): + self.sound = "woof" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:10:12 + | + 10 | animal.sound + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:3:5 + | + 3 | sound: str = "generic" + | ----- + 4 | + 5 | class Dog(Animal): + 6 | def __init__(self): + 7 | self.sound = "woof" + | ---------- + | + "#); + } + + #[test] + fn implementation_attribute_class_body_takes_priority_over_instance() { + // When a class defines the attribute both in its body and on `self`, the class-body + // definition wins for that class, matching the goto-definition lookup. + let test = cursor_test( + r#" + class Animal: + sound: str = "generic" + def __init__(self): + self.sound = "override" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:8:12 + | + 8 | animal.sound + | ^^^^^ Clicking here + | + info: Found 1 implementation + --> main.py:3:5 + | + 3 | sound: str = "generic" + | ----- + | + "#); + } + + #[test] + fn implementation_attribute_stub_mapped() { + let test = CursorTest::builder() + .source( + "main.py", + " +from mymodule import MyClass +def f(x: MyClass): + x.sound +", + ) + .source( + "mymodule.py", + r#" +class MyClass: + sound: str = "generic" +"#, + ) + .source( + "mymodule.pyi", + r#" +class MyClass: + sound: str +"#, + ) + .build(); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:4:7 + | + 4 | x.sound + | ^^^^^ Clicking here + | + info: Found 1 implementation + --> mymodule.py:3:5 + | + 3 | sound: str = "generic" + | ----- + | + "#); + } + + #[test] + fn implementation_attribute_protocol_method_nominal_only() { + // TODO: the receiver is a `Protocol`, so implementations should be determined by structural + // subtyping and return all three `speak` definitions (`Speaker`, `Dog`, and `Cat`). We + // currently use nominal inheritance only and return `Speaker.speak` and `Cat.speak`. See + // https://github.com/astral-sh/ruff/pull/25410#discussion_r3344203732. + let test = cursor_test( + r#" + from typing import Protocol + + class Speaker(Protocol): + def speak(self) -> None: ... + + class Dog: + def speak(self) -> None: ... + + class Cat(Speaker): + def speak(self) -> None: ... + + def f(speaker: Speaker): + speaker.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:14:13 + | + 14 | speaker.speak() + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:5:9 + | + 5 | def speak(self) -> None: ... + | ----- + | + ::: main.py:11:9 + | + 11 | def speak(self) -> None: ... + | ----- + | + "); + } + + #[test] + fn implementation_attribute_unreachable_override_excluded() { + // `FutureDog.speak` is defined in an unreachable block, so member lookup must not return + // it as an override. + let test = cursor_test( + r#" + import sys + + class Animal: + def speak(self): ... + + if sys.version_info >= (3, 5): + class Dog(Animal): + def speak(self): ... + + if sys.version_info >= (3, 999): + class FutureDog(Animal): + def speak(self): ... + + def f(animal: Animal): + animal.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r" + info[goto-implementation]: Go to implementation + --> main.py:16:12 + | + 16 | animal.speak() + | ^^^^^ Clicking here + | + info: Found 2 implementations + --> main.py:5:9 + | + 5 | def speak(self): ... + | ----- + 6 | + 7 | if sys.version_info >= (3, 5): + 8 | class Dog(Animal): + 9 | def speak(self): ... + | ----- + | + "); + } + + #[test] + fn implementation_attribute_unreachable_method_in_reachable_class_excluded() { + let test = cursor_test( + r#" + import sys + + class Animal: + def speak(self): ... + + class Dog(Animal): + if sys.version_info >= (3, 999): + def speak(self): ... + + def f(animal: Animal): + animal.speak() + "#, + ); + + assert_snapshot!(test.goto_implementation(), @" + info[goto-implementation]: Go to implementation + --> main.py:12:12 + | + 12 | animal.speak() + | ^^^^^ Clicking here + | + info: Found 1 implementation + --> main.py:5:9 + | + 5 | def speak(self): ... + | ----- + | + "); + } + + #[test] + fn implementation_attribute_unreachable_data_in_reachable_class_excluded() { + let test = cursor_test( + r#" + import sys + + class Animal: + sound: str = "generic" + + class Dog(Animal): + def __init__(self): + if sys.version_info >= (3, 999): + self.sound = "woof" + + def f(animal: Animal): + animal.sound + "#, + ); + + assert_snapshot!(test.goto_implementation(), @r#" + info[goto-implementation]: Go to implementation + --> main.py:13:12 + | + 13 | animal.sound + | ^^^^^ Clicking here + | + info: Found 1 implementation + --> main.py:5:5 + | + 5 | sound: str = "generic" + | ----- + | + "#); + } + + #[test] + fn implementation_unreachable_class_declaration_is_unsupported() { + let test = cursor_test( + r#" + import sys + + if sys.version_info >= (3, 999): + class Animal: + pass + + class Child(Animal): + pass + "#, + ); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_unreachable_class_reference_is_unsupported() { + let test = cursor_test( + r#" + import sys + + if sys.version_info >= (3, 999): + class Animal: + pass + + class Child(Animal): + pass + + value: Animal + "#, + ); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + #[test] + fn implementation_unreachable_method_declaration_is_unsupported() { + let test = cursor_test( + r#" + import sys + + class Animal: + def speak(self): ... + + class Dog(Animal): + if sys.version_info >= (3, 999): + def speak(self): ... + + class Pup(Dog): + def speak(self): ... + "#, + ); + + assert_snapshot!(test.goto_implementation(), @"No goto target found"); + } + + impl CursorTest { + fn goto_implementation(&self) -> String { + let Some(targets) = salsa::attach(&self.db, || { + goto_implementation(&self.db, self.cursor.file, self.cursor.offset) + }) else { + return "No goto target found".to_string(); + }; + + self.render_diagnostics([crate::goto_definition::test::GotoDiagnostic::new( + crate::goto_definition::test::GotoAction::Implementation, + targets, + )]) + } + } +} diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 08f0c87bd1..5309a9190b 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -14,6 +14,7 @@ mod folding_range; mod goto; mod goto_declaration; mod goto_definition; +mod goto_implementation; mod goto_type_definition; mod hints; mod hover; @@ -44,6 +45,7 @@ pub use document_symbols::document_symbols; pub use find_references::find_references; pub use folding_range::{FoldingRange, FoldingRangeKind, folding_ranges}; pub use goto::{goto_declaration, goto_definition, goto_type_definition}; +pub use goto_implementation::goto_implementation; pub use hints::{Hint, HintKind, hints}; pub use hover::hover; pub use inlay_hints::{ diff --git a/crates/ty_ide/src/references.rs b/crates/ty_ide/src/references.rs index 7d42fbfa1c..5e4204b0b5 100644 --- a/crates/ty_ide/src/references.rs +++ b/crates/ty_ide/src/references.rs @@ -24,7 +24,9 @@ use ruff_text_size::Ranged; use ty_project::parallel::{ParallelIteratorExt, minimum_parallel_job_len}; use ty_python_core::definition::{Definition, DefinitionKind, DefinitionState}; use ty_python_core::scope::{FileScopeId, NodeWithScopeKind, ScopeKind}; -use ty_python_semantic::{ImportAliasResolution, ResolvedDefinition, SemanticModel}; +use ty_python_semantic::{ + ImportAliasResolution, ResolvedDefinition, SemanticModel, contains_identifier, +}; /// Salsa snapshots coordinate clone and drop through shared state. For cached files that don't /// contain the target, that coordination can cost more than the file scan and scales poorly when @@ -189,36 +191,6 @@ fn references_for_keyword_arguments_in_file( references } -/// Cheap text prefilter for identifier references before AST/semantic validation. -/// -/// Heuristically matches an ASCII approximation of `\b{name}\b`. -pub(crate) fn contains_identifier(source: &str, name: &str) -> bool { - if name.is_empty() { - return false; - } - - let bytes = source.as_bytes(); - let needle = name.as_bytes(); - - memchr::memmem::find_iter(bytes, needle).any(move |pos| { - let after = pos + needle.len(); - - // Skip this entry if it is within an identifier. E.g. skip - // this entry when searching for `x` and this is a match - // within `exclude = 10` - let boundary_before = pos == 0 || !is_ascii_identifier_continue(bytes[pos - 1]); - let boundary_after = bytes - .get(after) - .is_none_or(|byte| !is_ascii_identifier_continue(*byte)); - - boundary_before && boundary_after - }) -} - -fn is_ascii_identifier_continue(byte: u8) -> bool { - byte.is_ascii_alphanumeric() || byte == b'_' -} - /// Returns whether `node` assigns `value` to the sole target `__slots__`, e.g. /// `__slots__ = (...)` or `__slots__: tuple = (...)`. fn is_slots_assignment(node: AnyNodeRef<'_>, value: AnyNodeRef<'_>) -> bool { @@ -872,11 +844,4 @@ def f(): assert!(!cursor_target_is_externally_visible(&test), "{case}"); } } - - #[test] - fn source_candidate_prefilters_use_identifier_boundaries() { - for (source, name) in [("x = 1", "x"), ("obj.x", "x"), ("x()", "x")] { - assert!(contains_identifier(source, name)); - } - } } diff --git a/crates/ty_ide/src/stub_mapping.rs b/crates/ty_ide/src/stub_mapping.rs index e69f8b0e77..7e67e5e2b5 100644 --- a/crates/ty_ide/src/stub_mapping.rs +++ b/crates/ty_ide/src/stub_mapping.rs @@ -32,14 +32,19 @@ impl<'db> StubMapper<'db> { &self, def: ResolvedDefinition<'db>, ) -> impl Iterator> { - if let Some(definitions) = - map_stub_definition(self.db, &def, self.cached_vendored_root.as_deref()) - { + if let Some(definitions) = self.map_definition_to_source(&def) { return Either::Left(definitions.into_iter()); } Either::Right(std::iter::once(def)) } + pub(crate) fn map_definition_to_source( + &self, + def: &ResolvedDefinition<'db>, + ) -> Option>> { + map_stub_definition(self.db, def, self.cached_vendored_root.as_deref()) + } + /// Map multiple `ResolvedDefinitions`, applying stub-to-source mapping to each. /// /// This is a convenience method that applies `map_definition` to each element diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 72ab6581ab..5b9b39d362 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -41,9 +41,10 @@ pub use ty_site_packages::{ SitePackagesPaths, SysPrefixPathOrigin, }; pub use types::ide_support::{ - ImportAliasResolution, ResolvedDefinition, TypeHierarchyClass, definitions_for_attribute, - definitions_for_bin_op, definitions_for_imported_symbol, definitions_for_name, - definitions_for_unary_op, map_stub_definition, type_hierarchy_prepare, type_hierarchy_subtypes, + ImplementationsFinder, ImportAliasResolution, ResolvedDefinition, TypeHierarchyClass, + contains_identifier, definitions_for_attribute, definitions_for_bin_op, + definitions_for_imported_symbol, definitions_for_name, definitions_for_unary_op, + map_stub_definition, type_hierarchy_prepare, type_hierarchy_subtypes, type_hierarchy_supertypes, }; pub use types::{DisplaySettings, TypeQualifiers}; diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 2375a4545d..8d48d09081 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -10,11 +10,12 @@ use crate::types::constraints::ConstraintSetBuilder; use crate::types::signatures::{ParametersKind, Signature}; use crate::types::{ CallDunderError, CallableTypes, ClassBase, ClassLiteral, ClassType, KnownClass, KnownFunction, - KnownUnion, SubclassOfInner, Type, TypeContext, + KnownUnion, PropertyAccessorRole, SubclassOfInner, Type, TypeContext, + TypeVarBoundOrConstraints, binding_type, }; use crate::{Db, DisplaySettings, HasDefinition, HasType, SemanticModel}; use itertools::Either; -use ruff_db::files::FileRange; +use ruff_db::files::{File, FileRange}; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_python_ast::{self as ast, AnyNodeRef, name::Name}; @@ -324,6 +325,316 @@ pub fn definitions_for_attribute<'db>( resolved } +/// A prepared implementation search. +/// +/// Preparing the finder resolves the class roots and the implementations selected for those roots. +/// Candidate subclasses can then be scanned one file at a time. +pub struct ImplementationsFinder<'db> { + /// Definitions selected directly for the goto target's roots: + /// - Root class definitions for a class-family search + /// - Definitions found through each root's MRO for a member-family search. + initial_definitions: Vec>, + + /// Classes whose known subclasses should be scanned for additional implementations. + roots: FxHashSet>, + + /// Whether scanning should return subclass definitions or same-named members on subclasses. + kind: ImplementationsFinderKind, +} + +enum ImplementationsFinderKind { + ClassFamily, + MemberFamily { + name: Name, + accessor_role: Option, + }, +} + +impl<'db> ImplementationsFinder<'db> { + /// Creates a class-family finder from resolved class roots. + fn for_class_roots(db: &'db dyn Db, roots: Vec>) -> Self { + let mut initial_definitions = Vec::new(); + for root in &roots { + if let Some(definition) = root.definition(db) { + let resolved = ResolvedDefinition::Definition(definition); + if !initial_definitions.contains(&resolved) { + initial_definitions.push(resolved); + } + } + } + + Self { + initial_definitions, + roots: roots.into_iter().collect(), + kind: ImplementationsFinderKind::ClassFamily, + } + } + + /// Creates a member-family finder for roots that resolve the member through their MRO. + fn for_member_roots( + db: &'db dyn Db, + roots: Vec>, + member_name: Name, + accessor_role: Option, + ) -> Option { + let mut initial_definitions = Vec::new(); + let mut family_roots = FxHashSet::default(); + + for root in roots { + // Avoid scanning every known subclass when the member doesn't resolve on this root. + let Some(root_definitions) = + mro_member_definitions(db, root, member_name.as_str(), accessor_role) + else { + continue; + }; + + for definition in root_definitions { + if !initial_definitions.contains(&definition) { + initial_definitions.push(definition); + } + } + + family_roots.insert(root); + } + + if family_roots.is_empty() { + return None; + } + + Some(Self { + initial_definitions, + roots: family_roots, + kind: ImplementationsFinderKind::MemberFamily { + name: member_name, + accessor_role, + }, + }) + } + + /// Returns implementations contributed by classes defined in `file`. + pub fn implementations_for_file<'scan>( + &'scan self, + db: &'scan dyn Db, + file: File, + ) -> Vec> + where + 'db: 'scan, + { + let roots: &FxHashSet> = &self.roots; + match &self.kind { + ImplementationsFinderKind::ClassFamily => { + class_implementations_for_file(db, file, roots) + } + ImplementationsFinderKind::MemberFamily { + name, + accessor_role, + } => member_implementations_for_file(db, file, roots, name.as_str(), *accessor_role), + } + } + + /// Returns the definitions selected directly for the finder's roots. + pub fn into_initial_definitions(self) -> Vec> { + self.initial_definitions + } + + /// Creates an `ImplementationsFinder` for an attribute expression `x.y`. + /// + /// ```py + /// def f(animal: Animal): + /// animal.sound + /// ^^^^^ + /// ``` + /// + /// For a receiver of type `Animal`, this includes the member definition selected through + /// `Animal`'s MRO plus same-named definitions on known subclasses such as `Dog` or `Cat`. For a + /// receiver of type `Dog`, the root is `Dog`: inherited behavior resolves through `Dog`'s MRO, + /// and sibling classes such as `Cat` are not included. + /// + /// Both `def`-style methods and attribute definitions are returned, whether the attribute is + /// declared in the class body (`sound: str = ...`, `sound = ...`, or a bare `sound: str`) or + /// assigned to `self` in a method body (`self.sound = ...`). + pub fn for_attribute( + model: &SemanticModel<'db>, + attribute: &ast::ExprAttribute, + ) -> Option { + let db = model.db(); + let lhs_ty = attribute.value.inferred_type(model)?; + let mut roots = Vec::new(); + let mut seen = FxHashSet::default(); + collect_implementation_root_classes(db, lhs_ty, &mut seen, &mut roots); + + let accessor_role = match attribute.ctx { + ast::ExprContext::Load => Some(PropertyAccessorRole::Getter), + ast::ExprContext::Store => Some(PropertyAccessorRole::Setter), + ast::ExprContext::Del => Some(PropertyAccessorRole::Deleter), + ast::ExprContext::Invalid => None, + }; + + ImplementationsFinder::for_member_roots(db, roots, attribute.attr.id.clone(), accessor_role) + } + + /// Creates an `ImplementationsFinder` for a method declaration. + /// + /// ```py + /// class Animal: + /// def speak(self): ... + /// ^^^^^ + /// + /// class Dog(Animal): + /// def speak(self): ... + /// ``` + /// + /// The containing class is used as the root. The method's implementation, if present, is returned + /// along with same-named methods defined on known transitive subclasses. This does not walk to + /// parent classes: on `Dog.speak`, the root is `Dog`, so `Animal.speak` is not included. + pub fn for_method(model: &SemanticModel<'db>, function: &ast::StmtFunctionDef) -> Option { + let db = model.db(); + let function_definition = function.definition(model); + if !is_reachable_implementation_definition(db, function_definition) { + return None; + } + + let containing_scope = function_definition.scope(db); + let accessor_role = function + .inferred_type(model) + .and_then(Type::as_property_instance) + .and_then(|property| property.accessor_role(db, function_definition)); + let class_node = containing_scope.node(db).as_class()?; + let class_definition = + semantic_index(db, containing_scope.file(db)).expect_single_definition(class_node); + let class_ty = binding_type(db, class_definition); + let root = extract_class_literal(db, class_ty)?; + + ImplementationsFinder::for_member_roots( + db, + vec![root], + function.name.id.clone(), + accessor_role, + ) + } + + /// Creates an `ImplementationsFinder` for a class declaration. + /// + /// ```py + /// class Animal: + /// ^^^^^^ + /// pass + /// + /// class Dog(Animal): ... + /// class Cat(Animal): ... + /// ``` + /// + /// The clicked class is the root and is returned first, followed by its known transitive + /// subclasses such as `Dog` and `Cat`. This walks down the hierarchy only: clicking a subclass + /// returns that class and its own subclasses, not its parents. + pub fn for_class(model: &SemanticModel<'db>, class: &ast::StmtClassDef) -> Option { + let db = model.db(); + let class_definition = class.definition(model); + if !is_reachable_implementation_definition(db, class_definition) { + return None; + } + let root = extract_class_literal(db, binding_type(db, class_definition))?; + + Some(ImplementationsFinder::for_class_roots(db, vec![root])) + } + + /// Creates an `ImplementationsFinder` for classes referred to by `resolved`, covering class + /// references such as a base class, an annotation, or a constructor call. + /// + /// ```py + /// class Animal: ... + /// + /// class Dog(Animal): ... + /// ^^^^^^ + /// ``` + /// + /// The referenced class is the root and is returned first, followed by its known transitive + /// subclasses, just like clicking the class declaration. + /// + /// The resolved definitions' binding types are used rather than the reference's inferred value + /// type, because a class used as an annotation (`x: Animal`) infers as an instance of that class, + /// which is indistinguishable from an actual instance variable. The resolved definition's type is + /// a class object precisely when the reference refers to a class. + /// + /// Returns `None` when no binding refers to a class object or any binding refers to a non-class + /// object (for example an instance variable or method), so callers can fall back to member + /// handling. + pub fn for_class_reference( + db: &'db dyn Db, + resolved_definitions: &[ResolvedDefinition<'db>], + ) -> Option { + let mut roots = Vec::new(); + let mut seen = FxHashSet::default(); + + for def in resolved_definitions { + let ResolvedDefinition::Definition(definition) = def else { + return None; + }; + + if !is_reachable_implementation_definition(db, *definition) { + continue; + } + + // Declaration-only definitions such as a bare `sound: str` annotation have no binding + // type and cannot refer to a class object. + if !def.category(db).is_binding() { + continue; + } + + // Only references that resolve to a class object (a base class, annotation, `Animal()`, or + // a name bound to a class) are class implementation requests; instances resolve to their + // own definitions, whose type is the instance rather than the class object. + let ty = binding_type(db, *definition); + + let root = match ty { + Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::GenericAlias(_) => { + extract_class_literal(db, ty) + } + _ => None, + }; + + let root = root?; + + if seen.insert(root) { + roots.push(root); + } + } + + if roots.is_empty() { + return None; + } + + Some(ImplementationsFinder::for_class_roots(db, roots)) + } +} + +/// Finds subclasses of `roots` defined in `file`. +fn class_implementations_for_file<'db>( + db: &'db dyn Db, + file: File, + roots: &FxHashSet>, +) -> Vec> { + if !contains_identifier(&source_text(db, file), "class") { + return Vec::new(); + } + + let mut definitions = Vec::new(); + + for candidate in reachable_class_literals_in_file(db, file) { + if roots.contains(&candidate) || !class_mro_intersects(db, candidate, roots) { + continue; + } + if let Some(definition) = candidate.definition(db) { + let resolved = ResolvedDefinition::Definition(definition); + if !definitions.contains(&resolved) { + definitions.push(resolved); + } + } + } + + definitions +} + /// Returns the descriptor object type for an attribute expression `x.y`, without invoking the /// descriptor protocol. This corresponds to `inspect.getattr_static(x, "y")` at the type level. pub fn static_member_type_for_attribute<'db>( @@ -405,6 +716,296 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( resolved } +/// Finds member implementations contributed by subclasses of `roots` defined in `file`. +fn member_implementations_for_file<'db>( + db: &'db dyn Db, + file: File, + roots: &FxHashSet>, + member_name: &str, + accessor_role: Option, +) -> Vec> { + let mut definitions = Vec::new(); + + // A file can only contribute an override if it contains a class and spells the member name, + // whether as a method name, a class-body target, or a `self.member` assignment. + let source = source_text(db, file); + if !contains_identifier(&source, "class") || !contains_identifier(&source, member_name) { + return definitions; + } + + for candidate in reachable_class_literals_in_file(db, file) { + // The implementations selected for the roots were collected during finder preparation. + if roots.contains(&candidate) { + continue; + } + + if !class_mro_intersects(db, candidate, roots) { + continue; + } + + for definition in + own_member_definitions(db, candidate, member_name, accessor_role).unwrap_or_default() + { + if !definitions.contains(&definition) { + definitions.push(definition); + } + } + } + + definitions +} + +/// Returns whether any class in `class`'s MRO is one of `roots`. +fn class_mro_intersects<'db>( + db: &'db dyn Db, + class: ClassLiteral<'db>, + roots: &FxHashSet>, +) -> bool { + class + .iter_mro(db) + .filter_map(ClassBase::into_class) + .any(|ancestor| roots.contains(&ancestor.class_literal(db))) +} + +/// Finds the member definitions selected by normal Python MRO lookup for `class`. +/// +/// This intentionally stops at the first class in the MRO that defines `member_name`; inherited +/// members should navigate to the definition that actually provides the behavior for the receiver. +/// The returned vector can be empty when the selected member has no implementation definition, +/// such as an overload-only method. +fn mro_member_definitions<'db>( + db: &'db dyn Db, + class: ClassLiteral<'db>, + member_name: &str, + accessor_role: Option, +) -> Option>> { + class + .iter_mro(db) + .filter_map(ClassBase::into_class) + .find_map(|class| { + own_member_definitions(db, class.class_literal(db), member_name, accessor_role) + }) +} + +/// Returns member definitions for `member_name` that are declared directly in `class`. +/// +/// ```py +/// class Animal: +/// def speak(self): ... +/// +/// class Dog(Animal): +/// pass +/// +/// class Cat(Animal): +/// def speak(self): ... +/// ``` +/// +/// For member `speak`, this returns nothing for `Dog` because it only inherits the member, but it +/// returns `Cat.speak` for `Cat` because it is defined directly in `Cat`. +/// +/// A class-body definition (method or attribute) takes priority and determines this class's +/// contribution when present, mirroring the goto-definition lookup in +/// [`definitions_for_attribute_in_class_hierarchy`]. Otherwise, instance attributes assigned in the +/// class's own method bodies (`self.member = ...`) are used. +/// +/// Subclasses that only inherit the member do not add a new implementation target. The inherited +/// definition is already represented by the ancestor that defines it; this only finds subclasses +/// that define a new method body or attribute. +/// +/// Returns `None` if `class` has no reachable user-visible definitions for `member_name`. Returns +/// `Some` with an empty vector if the class defines the symbol but none of its reachable definitions +/// produce a navigable implementation matching `accessor_role`. +fn own_member_definitions<'db>( + db: &'db dyn Db, + class: ClassLiteral<'db>, + member_name: &str, + accessor_role: Option, +) -> Option>> { + let class = class.as_static()?; + let class_scope = class.body_scope(db); + + let class_place_table = ty_python_core::place_table(db, class_scope); + if let Some(place_id) = class_place_table.symbol_id(member_name) { + let use_def = use_def_map(db, class_scope); + let definitions = reachable_implementation_definitions( + db, + use_def + .reachable_symbol_declarations(place_id) + .filter_map(|declaration| declaration.declaration.definition()) + .chain( + use_def + .reachable_symbol_bindings(place_id) + .filter_map(|binding| binding.binding.definition()), + ), + ); + if !definitions.is_empty() { + return Some( + definitions + .into_iter() + .filter(|definition| { + property_accessor_role_matches(db, *definition, accessor_role) + }) + .filter_map(|definition| member_implementation_definition(db, definition)) + .collect(), + ); + } + } + + let file = class_scope.file(db); + let index = semantic_index(db, file); + let mut instance_definitions = Vec::new(); + for function_scope_id in attribute_scopes(db, class_scope) { + let Some(place_id) = index + .place_table(function_scope_id) + .member_id_by_instance_attribute_name(member_name) + else { + continue; + }; + let use_def = index.use_def_map(function_scope_id); + instance_definitions.extend( + use_def + .reachable_member_declarations(place_id) + .filter_map(|declaration| declaration.declaration.definition()) + .chain( + use_def + .reachable_member_bindings(place_id) + .filter_map(|binding| binding.binding.definition()), + ), + ); + } + + let instance_definitions = reachable_implementation_definitions(db, instance_definitions); + if instance_definitions.is_empty() { + return None; + } + Some( + instance_definitions + .into_iter() + .filter_map(|definition| member_implementation_definition(db, definition)) + .collect(), + ) +} + +/// Returns whether `definition` is either not a property accessor or has the requested role. +fn property_accessor_role_matches( + db: &dyn Db, + definition: Definition<'_>, + requested_role: Option, +) -> bool { + if !matches!(definition.kind(db), DefinitionKind::Function(_)) { + return true; + } + + requested_role.is_none_or(|requested_role| { + binding_type(db, definition) + .as_property_instance() + .and_then(|property| property.accessor_role(db, definition)) + .is_none_or(|definition_role| definition_role == requested_role) + }) +} + +/// Normalize a member definition to the implementation target that should be navigated to. +fn member_implementation_definition<'db>( + db: &'db dyn Db, + definition: Definition<'db>, +) -> Option> { + match definition.kind(db) { + // `def` statements collapse overload declarations to their concrete implementation below. + DefinitionKind::Function(_) => {} + // Attribute definitions (`sound: str = ...`, `sound = ...`, a bare `sound: str` + // declaration, or `self.sound = ...`) are implementation targets as-is. + DefinitionKind::Assignment(_) | DefinitionKind::AnnotatedAssignment(_) => { + return Some(ResolvedDefinition::Definition(definition)); + } + // Other kinds (imports, comprehension targets, ...) can stop MRO lookup, but should not + // themselves become implementation targets. + _ => return None, + } + + // Use the inferred function type to collapse overload declarations to their concrete + // implementation. If inference cannot produce a function literal, keep the original `def` as a + // conservative fallback. + let Some(function) = binding_type(db, definition).as_function_literal() else { + return Some(ResolvedDefinition::Definition(definition)); + }; + + let (_, implementation) = function.overloads_and_implementation(db); + if implementation.is_some() { + return Some(ResolvedDefinition::Definition(function.last_definition(db))); + } + + // Stub overload declarations can still map to a real source implementation later. + if definition.file(db).is_stub(db) { + return Some(ResolvedDefinition::Definition(definition)); + } + + // Non-stub overload-only groups have no runtime implementation to navigate to. + None +} + +/// Normalizes a receiver type into the class roots used for implementation lookup. +fn collect_implementation_root_classes<'db>( + db: &'db dyn Db, + ty: Type<'db>, + seen: &mut FxHashSet>, + roots: &mut Vec>, +) { + match ty.resolve_type_alias(db) { + Type::Union(union) => { + // `pet: Dog | Cat` can dispatch through either `Dog` or `Cat`. + for element in union.elements(db) { + collect_implementation_root_classes(db, *element, seen, roots); + } + } + Type::Intersection(intersection) => { + // Finite intersections can stand for alternatives like `Dog` or `Cat`. + if let Some(alternatives) = intersection.finite_alternatives(db) { + for alternative in alternatives { + collect_implementation_root_classes(db, alternative, seen, roots); + } + } + } + Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db) { + // `T: Animal` can dispatch through the `Animal` bound. + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + collect_implementation_root_classes(db, bound, seen, roots); + } + // `T: (Dog, Cat)` can dispatch through either constraint. + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + collect_implementation_root_classes(db, constraints.as_type(db), seen, roots); + } + None => {} + }, + Type::SubclassOf(subclass_of) if subclass_of.is_type_var() => { + // Both `type[T]` and the implicit `cls` parameter of a classmethod are represented as + // `SubclassOf(TypeVar)`. Normalize them through the existing TypeVar handling above. + collect_implementation_root_classes(db, subclass_of.to_instance(db), seen, roots); + } + ty => { + // `dog: Dog` maps directly to the `Dog` class root. + let root = match ty { + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { + extract_class_literal(db, ty) + } + Type::NominalInstance(_) + | Type::ProtocolInstance(_) + | Type::KnownInstance(_) + | Type::LiteralValue(_) + | Type::TypedDict(_) + | Type::NewTypeInstance(_) => extract_class_literal(db, ty) + .or_else(|| extract_class_literal(db, ty.to_meta_type(db))), + _ => None, + }; + + if let Some(root) = root + && seen.insert(root) + { + roots.push(root); + } + } + } +} + fn reachable_definitions<'db>( db: &'db dyn Db, definitions: impl IntoIterator>, @@ -415,6 +1016,58 @@ fn reachable_definitions<'db>( .collect() } +fn reachable_implementation_definitions<'db>( + db: &'db dyn Db, + definitions: impl IntoIterator>, +) -> FxIndexSet> { + definitions + .into_iter() + .filter(|definition| definition.kind(db).is_user_visible()) + .filter(|definition| is_reachable_implementation_definition(db, *definition)) + .collect() +} + +fn is_reachable_implementation_definition(db: &dyn Db, definition: Definition<'_>) -> bool { + let file = definition.file(db); + let parsed = parsed_module(db, file).load(db); + is_range_reachable( + db, + semantic_index(db, file), + definition.file_scope(db), + definition.full_range(db, &parsed).range(), + ) +} + +/// Cheap text prefilter for identifier references before AST/semantic validation. +/// +/// Heuristically matches an ASCII approximation of `\b{name}\b`. +pub fn contains_identifier(source: &str, name: &str) -> bool { + if name.is_empty() { + return false; + } + + let bytes = source.as_bytes(); + let needle = name.as_bytes(); + + memchr::memmem::find_iter(bytes, needle).any(move |pos| { + let after = pos + needle.len(); + + // Skip this entry if it is within an identifier. E.g. skip + // this entry when searching for `x` and this is a match + // within `exclude = 10`. + let boundary_before = pos == 0 || !is_ascii_identifier_continue(bytes[pos - 1]); + let boundary_after = bytes + .get(after) + .is_none_or(|byte| !is_ascii_identifier_continue(*byte)); + + boundary_before && boundary_after + }) +} + +fn is_ascii_identifier_continue(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || byte == b'_' +} + fn resolve_reachable_definitions<'db>( db: &'db dyn Db, symbol_name: &str, @@ -1890,14 +2543,24 @@ mod resolve_definition { let component = match definition.kind(db) { DefinitionKind::Function(func) => func.node(parsed).name.as_str(), DefinitionKind::Class(class) => class.node(parsed).name.as_str(), + DefinitionKind::Assignment(assignment) => { + let ast::Expr::Name(name) = assignment.target(parsed) else { + return Err(()); + }; + name.id.as_str() + } + DefinitionKind::AnnotatedAssignment(assignment) => { + let ast::Expr::Name(name) = assignment.target(parsed) else { + return Err(()); + }; + name.id.as_str() + } DefinitionKind::TypeAlias(_) | DefinitionKind::Import(_) | DefinitionKind::ImportFrom(_) | DefinitionKind::ImportFromSubmodule(_) | DefinitionKind::StarImport(_) | DefinitionKind::NamedExpression(_) - | DefinitionKind::Assignment(_) - | DefinitionKind::AnnotatedAssignment(_) | DefinitionKind::AugmentedAssignment(_) | DefinitionKind::DictKeyAssignment(_) | DefinitionKind::For(_) @@ -1990,6 +2653,26 @@ pub fn type_hierarchy_subtypes( let Some(target_class) = extract_class_literal(db, ty) else { return vec![]; }; + direct_subtypes(db, target_class, modules) + .into_iter() + .map(|class_literal| class_literal_to_hierarchy_info(db, class_literal)) + .collect() +} + +/// Finds classes that directly inherit from `target_class`. +/// +/// ```py +/// class Animal: ... +/// class Dog(Animal): ... +/// class LoudDog(Dog): ... +/// ``` +/// +/// For `Animal`, this returns `Dog`, but not `LoudDog`. +fn direct_subtypes<'db>( + db: &'db dyn Db, + target_class: ClassLiteral<'db>, + modules: &[Module<'db>], +) -> Vec> { let target_name = target_class.name(db); let target_is_object = target_class.is_known(db, KnownClass::Object); let mut subtypes = vec![]; @@ -2011,37 +2694,23 @@ pub fn type_hierarchy_subtypes( continue; } - // Skip files that don't contain the class name. This avoids expensive - // semantic analysis for files that can't possibly contain a subclass - // of the target. We can't do this when looking for subtypes of - // `object` since `object` can be implicit. - if !target_is_object && !source_text(db, file).contains(target_name.as_str()) { + let source = source_text(db, file); + if !contains_identifier(&source, "class") { continue; } - let index = semantic_index(db, file); - for scope_id in index.scope_ids() { - let scope = scope_id.node(db); - let Some(class_node) = scope.as_class() else { - continue; - }; - - let def = index.expect_single_definition(class_node); - if !matches!(def.kind(db), DefinitionKind::Class(_)) { - continue; - } - - let file_scope_id = scope_id.file_scope_id(db); - let parsed = parsed_module(db, file).load(db); - if !is_range_reachable(db, index, file_scope_id, class_node.node(&parsed).range()) { - continue; - } - - let ty = crate::types::binding_type(db, def); - let Some(class_ty) = extract_class_literal(db, ty) else { - continue; - }; + // Keep the cheap name-based prefilter for non-first-party modules, which includes the + // vendored stdlib. First-party modules may inherit through local import aliases, e.g. + // `from a import Base as B; class Child(B): ...`, so they need semantic analysis even + // when they do not mention the target class's original name. + if is_non_first_party + && !target_is_object + && !contains_identifier(&source, target_name.as_str()) + { + continue; + } + for class_ty in reachable_class_literals_in_file(db, file) { let bases = class_ty.explicit_bases(db); let is_subtype = if target_is_object && bases.is_empty() @@ -2055,13 +2724,46 @@ pub fn type_hierarchy_subtypes( }) }; if is_subtype { - subtypes.push(class_literal_to_hierarchy_info(db, class_ty)); + subtypes.push(class_ty); } } } subtypes } +/// Enumerates the reachable class definitions in `file`. +fn reachable_class_literals_in_file(db: &dyn Db, file: File) -> Vec> { + let index = semantic_index(db, file); + let parsed = parsed_module(db, file).load(db); + let mut classes = Vec::new(); + + for scope_id in index.scope_ids() { + let scope = scope_id.node(db); + let Some(class_node) = scope.as_class() else { + continue; + }; + + // Map AST class node to its definition in the semantic index. + let definition = index.expect_single_definition(class_node); + if !matches!(definition.kind(db), DefinitionKind::Class(_)) { + continue; + } + + // Drop classes in dead code — e.g. a class under if sys.version_info < (3, 9): on a newer Python. + let file_scope_id = scope_id.file_scope_id(db); + if !is_range_reachable(db, index, file_scope_id, class_node.node(&parsed).range()) { + continue; + } + + // Convert the definition's type into a ClassLiteral, dropping anything that doesn't produce a usable class object. + if let Some(class) = extract_class_literal(db, binding_type(db, definition)) { + classes.push(class); + } + } + + classes +} + /// Extract a `ClassLiteral` from a `Type`, handling various type forms. fn extract_class_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { match ty { @@ -2221,12 +2923,23 @@ pub fn constructor_signature(model: &SemanticModel, call_expr: &ast::ExprCall) - #[cfg(test)] mod tests { - use super::{CallArgumentForm, call_argument_forms}; + use super::{CallArgumentForm, call_argument_forms, contains_identifier}; use crate::SemanticModel; use crate::db::tests::TestDbBuilder; use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; + #[test] + fn source_candidate_prefilters_use_identifier_boundaries() { + for (source, name) in [("x = 1", "x"), ("obj.x", "x"), ("x()", "x")] { + assert!(contains_identifier(source, name)); + } + + for (source, name) in [("exclude = 10", "x"), ("Database", "Base"), ("", "x")] { + assert!(!contains_identifier(source, name)); + } + } + #[test] fn keyword_call_argument_forms_follow_source_order() -> anyhow::Result<()> { let db = TestDbBuilder::new() diff --git a/crates/ty_server/src/capabilities.rs b/crates/ty_server/src/capabilities.rs index 5b0a838622..c70eaae144 100644 --- a/crates/ty_server/src/capabilities.rs +++ b/crates/ty_server/src/capabilities.rs @@ -36,6 +36,7 @@ bitflags::bitflags! { const PREFER_MARKDOWN_IN_COMPLETION = 1 << 18; const COMPLETION_ITEM_SNIPPET_SUPPORT = 1 << 19; const FULL_DIAGNOSTIC_OUTPUT = 1 << 20; + const IMPLEMENTATION_LINK_SUPPORT = 1 << 21; } } @@ -122,6 +123,11 @@ impl ResolvedClientCapabilities { self.contains(Self::DECLARATION_LINK_SUPPORT) } + /// Returns `true` if the client supports location links in goto implementation. + pub(crate) const fn supports_implementation_link(self) -> bool { + self.contains(Self::IMPLEMENTATION_LINK_SUPPORT) + } + /// Returns `true` if the client prefers markdown in hover responses. pub(crate) const fn prefers_markdown_in_hover(self) -> bool { self.contains(Self::PREFER_MARKDOWN_IN_HOVER) @@ -287,6 +293,13 @@ impl ResolvedClientCapabilities { flags |= Self::DECLARATION_LINK_SUPPORT; } + if text_document + .and_then(|text_document| text_document.implementation?.link_support) + .unwrap_or_default() + { + flags |= Self::IMPLEMENTATION_LINK_SUPPORT; + } + if text_document .and_then(|document| document.hover.as_ref()) .and_then(|hover| preferred_markup_kind(hover.content_format.as_deref()?)) @@ -443,6 +456,7 @@ pub(crate) fn server_capabilities( type_definition_provider: Some(true.into()), definition_provider: Some(true.into()), declaration_provider: Some(true.into()), + implementation_provider: Some(true.into()), references_provider: Some(true.into()), rename_provider: Some(server_rename_options().into()), document_highlight_provider: Some(true.into()), diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index bf21af7d32..a3aa75e09e 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -55,6 +55,11 @@ pub(super) fn request(req: server::Request) -> Task { >( req, BackgroundSchedule::Worker ), + requests::GotoImplementationRequestHandler::METHOD => background_document_request_task::< + requests::GotoImplementationRequestHandler, + >( + req, BackgroundSchedule::Worker + ), requests::GotoDefinitionRequestHandler::METHOD => background_document_request_task::< requests::GotoDefinitionRequestHandler, >(req, BackgroundSchedule::Worker), diff --git a/crates/ty_server/src/server/api/requests.rs b/crates/ty_server/src/server/api/requests.rs index 47ee0409bf..973d6783d4 100644 --- a/crates/ty_server/src/server/api/requests.rs +++ b/crates/ty_server/src/server/api/requests.rs @@ -23,6 +23,7 @@ mod execute_command; mod folding_range; mod goto_declaration; mod goto_definition; +mod goto_implementation; mod goto_type_definition; mod hover; mod inlay_hints; @@ -52,6 +53,7 @@ pub(super) use execute_command::ExecuteCommand; pub(super) use folding_range::FoldingRangeRequestHandler; pub(super) use goto_declaration::GotoDeclarationRequestHandler; pub(super) use goto_definition::GotoDefinitionRequestHandler; +pub(super) use goto_implementation::GotoImplementationRequestHandler; pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler; pub(super) use hover::HoverRequestHandler; pub(super) use inlay_hints::InlayHintRequestHandler; diff --git a/crates/ty_server/src/server/api/requests/goto_implementation.rs b/crates/ty_server/src/server/api/requests/goto_implementation.rs new file mode 100644 index 0000000000..fad3fc7519 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/goto_implementation.rs @@ -0,0 +1,77 @@ +use std::borrow::Cow; + +use lsp_types::{ImplementationParams, ImplementationRequest, ImplementationResponse, Uri}; +use ty_ide::goto_implementation; +use ty_project::ProjectDatabase; + +use crate::document::{PositionExt, ToLink}; +use crate::server::api::traits::{ + BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, +}; +use crate::session::DocumentSnapshot; +use crate::session::client::Client; + +pub(crate) struct GotoImplementationRequestHandler; + +impl RequestHandler for GotoImplementationRequestHandler { + type RequestType = ImplementationRequest; +} + +impl BackgroundDocumentRequestHandler for GotoImplementationRequestHandler { + fn document_uri(params: &ImplementationParams) -> Cow<'_, Uri> { + Cow::Borrowed(¶ms.text_document_position_params.text_document.uri) + } + + fn run_with_snapshot( + db: &ProjectDatabase, + snapshot: &DocumentSnapshot, + _client: &Client, + params: ImplementationParams, + ) -> crate::server::Result> { + if snapshot + .workspace_settings() + .is_language_services_disabled() + { + return Ok(None); + } + + let Some(file) = snapshot.to_notebook_or_file(db) else { + return Ok(None); + }; + + let Some(offset) = params.text_document_position_params.position.to_text_size( + db, + file, + snapshot.uri(), + snapshot.encoding(), + ) else { + return Ok(None); + }; + + let Some(ranged) = goto_implementation(db, file, offset) else { + return Ok(None); + }; + + if snapshot + .resolved_client_capabilities() + .supports_implementation_link() + { + let src = Some(ranged.range); + let links: Vec<_> = ranged + .into_iter() + .filter_map(|target| target.to_link(db, src, snapshot.encoding())) + .collect(); + + Ok(Some(links.into())) + } else { + let locations: Vec<_> = ranged + .into_iter() + .filter_map(|target| target.to_location(db, snapshot.encoding())) + .collect(); + + Ok(Some(ImplementationResponse::Definition(locations.into()))) + } + } +} + +impl RetriableRequestHandler for GotoImplementationRequestHandler {} diff --git a/crates/ty_server/tests/e2e/implementation.rs b/crates/ty_server/tests/e2e/implementation.rs new file mode 100644 index 0000000000..2fc6564c08 --- /dev/null +++ b/crates/ty_server/tests/e2e/implementation.rs @@ -0,0 +1,116 @@ +use anyhow::Result; +use lsp_types::{ + Definition, ImplementationParams, ImplementationProvider, ImplementationRequest, + ImplementationResponse, PartialResultParams, Position, Range, TextDocumentIdentifier, + TextDocumentPositionParams, WorkDoneProgressParams, +}; + +use crate::TestServerBuilder; + +const CONTENT: &str = r#"class Animal: + def speak(self): ... + +class Dog(Animal): + def speak(self): ... + +class Cat(Animal): + def speak(self): ... + +def f(animal: Animal): + animal.speak() +"#; + +#[test] +fn implementation_provider_is_advertised() -> Result<()> { + let server = TestServerBuilder::new()? + .build() + .wait_until_workspaces_are_initialized(); + + let initialization_result = server.initialization_result().unwrap(); + assert_eq!( + initialization_result.capabilities.implementation_provider, + Some(ImplementationProvider::Bool(true)) + ); + + Ok(()) +} + +#[test] +fn implementation_locations_without_link_support() -> Result<()> { + let mut server = TestServerBuilder::new()? + .with_file("foo.py", CONTENT)? + .enable_implementations_link_support(false) + .build() + .wait_until_workspaces_are_initialized(); + + server.open_text_document("foo.py", CONTENT, 1); + + let response = implementation(&mut server, "foo.py", Position::new(10, 13)).unwrap(); + let ImplementationResponse::Definition(Definition::LocationList(locations)) = response else { + panic!("Expected Location[] response, got {response:#?}"); + }; + + let ranges: Vec<_> = locations.iter().map(|location| location.range).collect(); + assert_eq!( + ranges, + vec![ + Range::new(Position::new(1, 8), Position::new(1, 13)), + Range::new(Position::new(4, 8), Position::new(4, 13)), + Range::new(Position::new(7, 8), Position::new(7, 13)), + ] + ); + + Ok(()) +} + +#[test] +fn implementation_location_links_with_link_support() -> Result<()> { + let mut server = TestServerBuilder::new()? + .with_file("foo.py", CONTENT)? + .enable_implementations_link_support(true) + .build() + .wait_until_workspaces_are_initialized(); + + server.open_text_document("foo.py", CONTENT, 1); + + let response = implementation(&mut server, "foo.py", Position::new(10, 13)).unwrap(); + let ImplementationResponse::DefinitionLinkList(links) = response else { + panic!("Expected LocationLink[] response, got {response:#?}"); + }; + + let selection_ranges: Vec<_> = links + .iter() + .map(|link| link.target_selection_range) + .collect(); + assert_eq!( + selection_ranges, + vec![ + Range::new(Position::new(1, 8), Position::new(1, 13)), + Range::new(Position::new(4, 8), Position::new(4, 13)), + Range::new(Position::new(7, 8), Position::new(7, 13)), + ] + ); + assert!(links.iter().all(|link| { + link.origin_selection_range + == Some(Range::new(Position::new(10, 11), Position::new(10, 16))) + })); + + Ok(()) +} + +fn implementation( + server: &mut crate::TestServer, + path: impl AsRef, + position: Position, +) -> Option { + server.send_request_await::(ImplementationParams { + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { + uri: server.file_uri(path), + }, + position, + }, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }) +} diff --git a/crates/ty_server/tests/e2e/main.rs b/crates/ty_server/tests/e2e/main.rs index 79f7f85e97..84b3881185 100644 --- a/crates/ty_server/tests/e2e/main.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -34,6 +34,7 @@ mod completions; mod configuration; mod folding_range; mod hover; +mod implementation; mod initialize; mod inlay_hints; mod notebook; @@ -1383,6 +1384,17 @@ impl TestServerBuilder { self } + /// Enable or disable location link support for goto implementations + pub(crate) fn enable_implementations_link_support(mut self, enabled: bool) -> Self { + self.client_capabilities + .text_document + .get_or_insert_default() + .implementation + .get_or_insert_default() + .link_support = Some(enabled); + self + } + /// Set custom client capabilities (overrides any previously set capabilities) #[expect(dead_code)] pub(crate) fn with_client_capabilities(mut self, capabilities: ClientCapabilities) -> Self { diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap index 2fbcab3bb6..20630e3208 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap @@ -44,6 +44,7 @@ expression: initialization_result "declarationProvider": true, "definitionProvider": true, "typeDefinitionProvider": true, + "implementationProvider": true, "referencesProvider": true, "documentHighlightProvider": true, "documentSymbolProvider": true, diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap index 2fbcab3bb6..20630e3208 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap @@ -44,6 +44,7 @@ expression: initialization_result "declarationProvider": true, "definitionProvider": true, "typeDefinitionProvider": true, + "implementationProvider": true, "referencesProvider": true, "documentHighlightProvider": true, "documentSymbolProvider": true, From 847ed6fca271c899245bdb44dab2c63c168652d5 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sun, 26 Jul 2026 22:58:03 -0700 Subject: [PATCH 074/390] [ty] Make `with_message_override` diagnostic API more future-proof (#27180) ## Summary Follow-up to #27140 that addresses a couple latent risks in the new `with_message_override` API. - Always retain the original headline message: move it to an empty primary annotation if possible (as we did before), or prepend it as the first info subdiagnostic (instead of dropping it, as we did before) when the primary annotation already has a message. This avoids silently losing existing headline messages. In the current cases where do lose the prior headline message, it wasn't critical info -- but I also think it's useful to preserve it. In potential future cases it might be more important to preserve it. - Clear a stale custom concise messages when replacing the headline. This currently isn't relevant because no call error diagnostic uses a custom concise message, but in general if the headline is being replaced, that most likely indicates the existing custom concise message is also no longer correct. Instead we'll just construct a default concise message from the new headline and primary annotation. If we have a future case where that's not adequate, the `with_message_override` API can also accept a new concise diagnostic. ## Test plan - Updated descriptor `__set__`, property-setter, and custom `__setattr__` mdtest snapshots to cover invalid arguments, nested incompatibilities, and multiple argument errors, confirming the preserved call headline is the first info message. - Existing missing-argument and overload scenarios continue to retain their original headline on the primary annotation, and concise diagnostic expectations remain unchanged. --- crates/ruff_db/src/diagnostic/mod.rs | 18 ++++++++ .../resources/mdtest/attributes.md | 4 ++ .../diagnostics/attribute_assignment.md | 3 ++ .../ty_python_semantic/src/types/call/bind.rs | 6 ++- .../ty_python_semantic/src/types/context.rs | 46 ++++++++++++------- 5 files changed, 58 insertions(+), 19 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index a6ba4c0343..32870c586a 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -169,6 +169,13 @@ impl Diagnostic { self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Info, message)); } + /// Adds an "info" sub-diagnostic before any existing sub-diagnostics. + pub fn prepend_info<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) { + Arc::make_mut(&mut self.inner) + .subs + .insert(0, SubDiagnostic::new(SubDiagnosticSeverity::Info, message)); + } + /// Adds a "help" sub-diagnostic with the given message. /// /// See the closely related [`Diagnostic::info`] method for more details. @@ -210,6 +217,11 @@ impl Diagnostic { self.inner.message.as_str() } + /// Sets the headline message for this diagnostic. + pub fn set_headline_message(&mut self, message: impl IntoDiagnosticMessage) { + Arc::make_mut(&mut self.inner).message = message.into_diagnostic_message(); + } + /// Introspects this diagnostic and returns its message for concise formatting. /// /// When we concisely format diagnostics, we likely want to not only @@ -247,6 +259,12 @@ impl Diagnostic { Some(message.into_diagnostic_message()); } + /// Remove the custom concise message, restoring the default behavior of generating a concise + /// message from the headline message and the primary annotation. + pub fn clear_concise_message(&mut self) { + Arc::make_mut(&mut self.inner).custom_concise_message = None; + } + /// Returns the severity of this diagnostic. /// /// Note that this may be different than the severity of sub-diagnostics. diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index c0676959a3..8d92813802 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2997,6 +2997,7 @@ error[invalid-assignment]: Cannot assign object of type `tuple[Literal[1], Liter 5 | c.x = (1, b"") # snapshot: invalid-assignment | ^^^^^^^^ Expected `tuple[int, str]`, found `tuple[Literal[1], Literal[b""]]` | +info: Argument to bound method `C.__setattr__` is incorrect info: This assignment implicitly calls a custom `__setattr__` method info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` info: Method defined here @@ -3080,6 +3081,7 @@ error[invalid-assignment]: Cannot assign object of type `Literal["May"]` to attr 13 | date.month = "May" # snapshot: invalid-assignment | ^^^^^ Expected `int`, found `Literal["May"]` | +info: Argument to bound method `Date.__setattr__` is incorrect info: This assignment implicitly calls a custom `__setattr__` method info: Method defined here --> src/mdtest_snippet.py:5:9 @@ -3095,6 +3097,7 @@ error[invalid-assignment]: Cannot assign object of type `Literal["UTC"]` to attr 16 | date.tz = "UTC" | ^^^^^^^ Expected `Literal["day", "month", "year"]`, found `Literal["tz"]` | +info: Argument to bound method `Date.__setattr__` is incorrect info: This assignment implicitly calls a custom `__setattr__` method info: Method defined here --> src/mdtest_snippet.py:5:9 @@ -3110,6 +3113,7 @@ error[invalid-assignment]: Cannot assign object of type `Literal["UTC"]` to attr 16 | date.tz = "UTC" | ^^^^^ Expected `int`, found `Literal["UTC"]` | +info: Argument to bound method `Date.__setattr__` is incorrect info: This assignment implicitly calls a custom `__setattr__` method info: Method defined here --> src/mdtest_snippet.py:5:9 diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md index a18c94b802..d903903d93 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md @@ -288,6 +288,7 @@ error[invalid-assignment]: Invalid assignment to data descriptor attribute `attr 11 | instance.attr = "wrong" # snapshot: invalid-assignment | ^^^^^^^ Expected `int`, found `Literal["wrong"]` | +info: Argument to function `Descriptor.__set__` is incorrect info: This assignment implicitly calls `__set__` on a descriptor of type `Descriptor` info: Function defined here --> src/mdtest_snippet.py:2:9 @@ -355,6 +356,7 @@ error[invalid-assignment]: Invalid assignment to data descriptor attribute `docu 11 | self.document = None # snapshot: invalid-assignment | ^^^^ Expected `Document`, found `None` | +info: Argument to function `HasDocumentRef.document` is incorrect info: This assignment implicitly calls `__set__` on a descriptor of type `property` info: Function defined here --> src/mdtest_snippet.py:7:9 @@ -384,6 +386,7 @@ error[invalid-assignment]: Invalid assignment to data descriptor attribute `x` o 8 | c.x = (1, b"") # snapshot: invalid-assignment | ^^^^^^^^ Expected `tuple[int, str]`, found `tuple[Literal[1], Literal[b""]]` | +info: Argument to function `Descriptor.__set__` is incorrect info: This assignment implicitly calls `__set__` on a descriptor of type `Descriptor` info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` info: Function defined here diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index e61e303717..e3cb203cfb 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -82,8 +82,10 @@ pub(crate) use self::constructor::ConstructorCallableKind; /// Overrides the lint and headline message for a call diagnostic emitted from an implicit call. /// -/// The original call-error message is retained on the primary annotation, while `info` explains -/// why the call happened. `argument_ranges` maps synthetic call arguments back to source ranges. +/// The original call-error message is retained on the primary annotation if the call reporter +/// does not supply its own annotation message, or as an info sub-diagnostic otherwise. `info` +/// explains why the call happened. `argument_ranges` maps synthetic call arguments back to source +/// ranges. pub(crate) struct CallDiagnosticOverride<'a> { pub(crate) lint: &'static LintMetadata, pub(crate) message: String, diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 9dcfc63a25..112b189758 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -274,6 +274,7 @@ pub(super) struct LintDiagnosticGuard<'db, 'ctx> { diag: Option, source: LintSource, + message_override: Option, } impl LintDiagnosticGuard<'_, '_> { @@ -363,6 +364,22 @@ impl Drop for LintDiagnosticGuard<'_, '_> { // once. let mut diag = self.diag.take().unwrap(); + if let Some(message_override) = self.message_override.take() { + let primary_annotation_has_message = diag + .primary_annotation() + .and_then(Annotation::get_message) + .is_some_and(|message| !message.is_empty()); + let original_message = diag.headline_message().to_string(); + if primary_annotation_has_message { + diag.prepend_info(original_message); + } else if let Some(annotation) = diag.primary_annotation_mut() { + annotation.set_message(original_message); + } + + diag.set_headline_message(message_override); + diag.clear_concise_message(); + } + if self.ctx.db().verbose() { let rule = diag.id(); @@ -514,7 +531,9 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. /// - /// If a message override is present, `message` is retained on the primary annotation. + /// If a message override is present, it is applied when the diagnostic is finalized. `message` + /// is retained on the primary annotation if the annotation has no message, or as an info + /// sub-diagnostic otherwise. Any custom concise message is discarded. pub(super) fn into_diagnostic( self, message: impl std::fmt::Display, @@ -523,31 +542,24 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { // We add the primary annotation here (because it's required). Without a message // override, its optional message can be added later via `set_primary_annotation_message`. let primary_span = Span::from(self.ctx.file()).with_range(self.primary_range); - let mut diag = if let Some((message_override, info)) = self.message_override { - let mut diag = Diagnostic::new( - DiagnosticId::Lint(self.id.name()), - self.severity, - message_override, - ); - diag.annotate(Annotation::primary(primary_span).message(message)); + let mut diag = Diagnostic::new(DiagnosticId::Lint(self.id.name()), self.severity, message); + diag.annotate(Annotation::primary(primary_span)); + let message_override = self.message_override.map(|(message, info)| { diag.info(info); - diag - } else { - let mut diag = - Diagnostic::new(DiagnosticId::Lint(self.id.name()), self.severity, message); - diag.annotate(Annotation::primary(primary_span)); - diag - }; + message + }); diag.set_documentation_url(Some(self.id.documentation_url())); LintDiagnosticGuard { ctx: self.ctx, source: self.source, diag: Some(diag), + message_override, } } - /// Replace the headline message and add an info sub-diagnostic while retaining the original - /// message on the primary annotation. + /// Replace the headline message when the diagnostic is finalized and add an info + /// sub-diagnostic. The original message is retained on the primary annotation if it has no + /// message, or as an info sub-diagnostic otherwise. pub(super) fn with_message_override(mut self, message: String, info: &str) -> Self { self.message_override = Some((message, info.to_string())); self From 8e12b40050117605737280c7c949529c76bb93c6 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sun, 26 Jul 2026 23:06:57 -0700 Subject: [PATCH 075/390] [ty] Decorate only overload implementation signatures (#27147) ## Summary The typing specification says that decorators applied to an overloaded function implementation are applied only to that implementation signature (not to the entire overloaded signature), and this happens before consistency is checked with the overload signatures. External callers only see overload signatures as written (with the effect of any decorators applied to the _overload_), they don't see the effect of decorators applied to the implementation. On main we do this wrong; we apply implementation decorators to the entire overloaded signature. This PR fixes that, and also fixes application of decorators to specific overloads, notably handling the case where application of a decorator to an overload results in a `Callable` type instead of a `FunctionLiteral` type -- previously that broke the chain of overloads. Fixes https://github.com/astral-sh/ty/issues/3859, relates to https://github.com/astral-sh/ty/issues/2278 and https://github.com/astral-sh/ty/issues/2057 ## Test plan - Added mdtests for return widening, parameter narrowing, callable-protocol and callable-union alternatives, non-callable decorator results, and function-literal replacement with preserved deprecation metadata. - Updated `Concatenate`, `ParamSpec`, async-overload, and override fixtures to distinguish implementation decoration from regular application to an entire overloaded callable and to verify caller-visible overloads and consistency diagnostics. --- .../mdtest/generics/pep695/concatenate.md | 13 +- .../mdtest/generics/pep695/paramspec.md | 3 +- .../resources/mdtest/overloads.md | 229 ++++++++++++++++++ .../resources/mdtest/override.md | 1 + .../ty_python_semantic/src/types/function.rs | 202 ++++++++++++--- .../src/types/infer/builder/function.rs | 43 +++- .../post_inference/overloaded_function.rs | 87 +++++-- 7 files changed, 509 insertions(+), 69 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md index 82c9f371c2..6cdf4ee465 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md @@ -649,12 +649,11 @@ def remove_param[**P, R](func: Callable[Concatenate[int, P], R]) -> Callable[P, def f1(x: int, y: str) -> str: ... @overload def f1(x: int, y: int) -> int: ... -@remove_param def f1(x: int, y: str | int) -> str | int: return y # TODO: Should reveal `Overloaded[(y: str) -> str, (y: int) -> int]` -reveal_type(f1) # revealed: (y: str) -> str | int +reveal_type(remove_param(f1)) # revealed: (y: str) -> str | int ``` But, it's not possible to _add_ a parameter to an overloaded function using `Concatenate` because @@ -666,18 +665,17 @@ def add_param[**P, R](func: Callable[P, R]) -> Callable[Concatenate[int, P], R]: return func(*args, **kwargs) return wrapper -# TODO: Raise a diagnostic stating that the signature of the implementation doesn't match the -# overloads because the overloads don't have the extra `int` parameter. @overload +# error: [invalid-overload] "Implementation does not accept all arguments of this overload" def f2(y: str) -> str: ... @overload +# error: [invalid-overload] "Implementation does not accept all arguments of this overload" def f2(y: int) -> int: ... @add_param def f2(y: str | int) -> str | int: return y -# TODO: Should this reveal `Overloaded[(int, /, y: str) -> str, (int, /, y: int) -> int]` ? -reveal_type(f2) # revealed: Overload[(int, /, y: str) -> str | int, (int, /, y: int) -> str | int] +reveal_type(f2) # revealed: Overload[(y: str) -> str, (y: int) -> int] ``` But, it's possible to add the additional parameter just to the overload signatures and not the @@ -692,8 +690,7 @@ def f3(x: int, /, y: int) -> int: ... def f3(y: str | int) -> str | int: return y -# TODO: Should reveal `Overloaded[(int, /, y: str) -> str, (int, /, y: int) -> int]` -reveal_type(f3) # revealed: Overload[(int, x: int, /, y: str) -> str | int, (int, x: int, /, y: int) -> str | int] +reveal_type(f3) # revealed: Overload[(x: int, /, y: str) -> str, (x: int, /, y: int) -> int] ``` ## `Concatenate` with protocol classes diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index c714ce6181..965dff1cdb 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -1102,11 +1102,10 @@ def unwrap_awaitable(function: Callable[P, Awaitable[R]], /) -> Callable[P, R]: async def unwrapped(value: int) -> int: ... @overload async def unwrapped(value: str) -> str: ... -@unwrap_awaitable async def unwrapped(value: int | str) -> int | str: raise NotImplementedError -reveal_type(unwrapped(1)) # revealed: int | str +reveal_type(unwrap_awaitable(unwrapped)(1)) # revealed: int | str ``` The selected decorator overload can use an `Awaitable` return type. diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index 2ca9d3b223..52630e8662 100644 --- a/crates/ty_python_semantic/resources/mdtest/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -972,6 +972,235 @@ def generic_parameter_type(x: int) -> int | str: return x ``` +### Decorated implementation consistency + +Decorators on an overload implementation apply only to the implementation signature. The decorated +signature is checked against the overloads, while callers continue to see only the overloads. + +```py +from typing import Callable, overload + +def widen_return(func: Callable[[int | str], int]) -> Callable[[int | str], int | str]: + raise NotImplementedError + +@overload +def widened(x: int, /) -> int: ... +@overload +def widened(x: str, /) -> str: ... +@widen_return +def widened(x: int | str) -> int: + return 1 + +reveal_type(widened) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +reveal_type(widened(1)) # revealed: int +reveal_type(widened("one")) # revealed: str + +def narrow_parameter(func: Callable[[int | str], int | str]) -> Callable[[int], int | str]: + raise NotImplementedError + +@overload +def narrowed(x: int, /) -> int: ... +@overload +# error: [invalid-overload] "Implementation does not accept all arguments of this overload" +def narrowed(x: str, /) -> str: ... +@narrow_parameter +def narrowed(x: int | str) -> int | str: + return x + +reveal_type(narrowed) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +reveal_type(narrowed(1)) # revealed: int +reveal_type(narrowed("one")) # revealed: str +``` + +### Decorated overload consistency + +Decorators on individual overloads transform those overload signatures before implementation +consistency is checked. The transformed signatures remain visible to callers. + +```py +from typing import Callable, overload + +def decorate_overload(func: Callable[..., object]) -> Callable[[int], int]: + raise NotImplementedError + +def decorate_implementation(func: Callable[..., object]) -> Callable[[int | str], int | str]: + raise NotImplementedError + +@overload +@decorate_overload +def decorated() -> None: ... +@overload +def decorated(x: str, /) -> str: ... +@decorate_implementation +def decorated(y: bytes, z: bytes) -> bytes: + raise NotImplementedError + +reveal_type(decorated) # revealed: Overload[(int, /) -> int, (x: str, /) -> str] +reveal_type(decorated(1)) # revealed: int +reveal_type(decorated("one")) # revealed: str +``` + +### Decorated overloads with `Concatenate` + +Each decorated overload applies its decorator to its own signature, without including any preceding +overloads in the decorator call. + +```py +from collections.abc import Callable +from typing import Any, Concatenate, ParamSpec, TypeVar, overload + +P = ParamSpec("P") +A = TypeVar("A") +R = TypeVar("R") + +def curry1(func: Callable[Concatenate[A, P], R]) -> Callable[[A], Callable[P, R]]: + raise NotImplementedError + +@curry1 +@overload +def starmap(mapper: Callable[[int, int], int], parser: int) -> int: ... +@curry1 +@overload +def starmap(mapper: Callable[[str, str, str], str], parser: str) -> str: ... +@curry1 +def starmap(mapper: Callable[..., Any], parser: Any) -> Any: + raise NotImplementedError + +def add(x: int, y: int) -> int: + return x + y + +# revealed: Overload[((int, int, /) -> int, /) -> ((parser: int) -> int), ((str, str, str, /) -> str, /) -> ((parser: str) -> str)] +reveal_type(starmap) +reveal_type(starmap(add)) # revealed: (parser: int) -> int +``` + +### Decorated implementation replaced by a function + +A decorator can replace an overload implementation with another function. The overload set remains +visible to callers, the replacement signature is checked for consistency, and an outer `@deprecated` +decorator still applies to the overload set. + +```py +from collections.abc import Callable +from typing import Any, TypeVar, overload +from typing_extensions import deprecated + +R = TypeVar("R") + +def replacement(x: int, /) -> int: + return x + +def replace_with(value: R) -> Callable[[Callable[..., Any]], R]: + def decorator(_function: Callable[..., Any]) -> R: + return value + return decorator + +@overload +def replaced(x: int, /) -> int: ... +@overload +# error: [invalid-overload] "Overload signature is not consistent with implementation" +def replaced(x: str, /) -> str: ... +@deprecated("use replacement directly") +@replace_with(replacement) +def replaced(x: int | str) -> int | str: + return x + +# error: [deprecated] "use replacement directly" +reveal_type(replaced) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +# error: [deprecated] "use replacement directly" +reveal_type(replaced("one")) # revealed: str +``` + +### Decorated implementation with multiple callable signatures + +An overloaded callback protocol can provide one implementation signature for each overload. Every +callable in a union must support every overload. A decorator that returns a non-callable cannot +implement any overload. + +```py +from typing import Callable, Protocol, overload + +class ValidCallback(Protocol): + @overload + def __call__(self, x: int, /) -> int: ... + @overload + def __call__(self, x: str, /) -> str: ... + +class NarrowCallback(Protocol): + @overload + def __call__(self, x: int, /) -> int: ... + @overload + def __call__(self, x: bytes, /) -> bytes: ... + +def valid_callback(func: Callable[[int | str], int | str]) -> ValidCallback: + raise NotImplementedError + +def narrow_callback(func: Callable[[int | str], int | str]) -> NarrowCallback: + raise NotImplementedError + +def valid_union( + func: Callable[[int | str], int | str], +) -> Callable[[int | str], int | str] | Callable[[object], object]: + raise NotImplementedError + +def narrow_union( + func: Callable[[int | str], int | str], +) -> Callable[[int | str], int | str] | Callable[[int], int]: + raise NotImplementedError + +def noncallable(func: Callable[[int | str], int | str]) -> int: + raise NotImplementedError + +@overload +def callback_valid(x: int, /) -> int: ... +@overload +def callback_valid(x: str, /) -> str: ... +@valid_callback +def callback_valid(x: int | str) -> int | str: + return x + +@overload +def callback_narrowed(x: int, /) -> int: ... +@overload +# error: [invalid-overload] "Overload signature is not consistent with implementation" +def callback_narrowed(x: str, /) -> str: ... +@narrow_callback +def callback_narrowed(x: int | str) -> int | str: + return x + +@overload +def union_valid(x: int, /) -> int: ... +@overload +def union_valid(x: str, /) -> str: ... +@valid_union +def union_valid(x: int | str) -> int | str: + return x + +@overload +def union_narrowed(x: int, /) -> int: ... +@overload +# error: [invalid-overload] "Overload signature is not consistent with implementation" +def union_narrowed(x: str, /) -> str: ... +@narrow_union +def union_narrowed(x: int | str) -> int | str: + return x + +@overload +def not_callable(x: int, /) -> int: ... +@overload +def not_callable(x: str, /) -> str: ... +@noncallable +# error: [invalid-overload] "Overload implementation is not callable after applying decorators" +def not_callable(x: int | str) -> int | str: + return x + +reveal_type(callback_valid) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +reveal_type(callback_narrowed) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +reveal_type(union_valid) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +reveal_type(union_narrowed) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +reveal_type(not_callable) # revealed: Overload[(x: int, /) -> int, (x: str, /) -> str] +``` + ### Implementation consistency parameter mismatch diagnostics Non-generic implementation checks require parameter names and positional-only forms to line up with diff --git a/crates/ty_python_semantic/resources/mdtest/override.md b/crates/ty_python_semantic/resources/mdtest/override.md index 44c4044bee..6ba91b5c4a 100644 --- a/crates/ty_python_semantic/resources/mdtest/override.md +++ b/crates/ty_python_semantic/resources/mdtest/override.md @@ -859,6 +859,7 @@ class Spam: @overload @override + # error: [invalid-overload] "`@override` decorator should be applied only to the overload implementation" def quux(self, x: str) -> str: ... @overload def quux(self, x: int) -> int: ... diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index c0af80717b..d65622e8b7 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -49,9 +49,10 @@ //! the public type of `f` is resolved at position 3, correctly giving you all of the overloads //! (and the implementation). -use std::str::FromStr; +use std::{borrow::Cow, str::FromStr}; use bitflags::bitflags; +use itertools::Either; use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; @@ -79,7 +80,7 @@ use crate::types::diagnostic::{ }; use crate::types::display::DisplaySettings; use crate::types::generics::{ApplySpecialization, GenericContext, typing_self}; -use crate::types::infer::{nearest_enclosing_class, original_class_type}; +use crate::types::infer::{infer_definition_types, nearest_enclosing_class, original_class_type}; use crate::types::known_instance::DeprecatedInstance; use crate::types::list_members::all_members; use crate::types::narrow::ClassInfoConstraintFunction; @@ -93,7 +94,7 @@ use crate::types::{ CallableType, ClassBase, ClassLiteral, ClassType, FindLegacyTypeVarsVisitor, IntersectionBuilder, KnownClass, KnownInstanceType, SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, - UnionBuilder, UnionType, definition_expression_type, walk_signature, + UnionBuilder, UnionType, binding_type, definition_expression_type, walk_signature, }; use crate::{Db, FxOrderSet}; use ty_python_core::ast_ids::HasScopedUseId; @@ -452,15 +453,25 @@ impl<'db> OverloadLiteral<'db> { .scoped_use_id(db, self.file(db)); let Place::Defined(DefinedPlace { - ty: Type::FunctionLiteral(previous_type), + ty: previous_type, definedness: Definedness::AlwaysDefined, + provenance, .. }) = place_from_bindings(db, use_def.bindings_at_use(use_id)).place else { return None; }; - let previous_literal = previous_type.literal(db); + let previous_literal = match previous_type { + Type::FunctionLiteral(previous_type) => previous_type.literal(db), + Type::Callable(_) => { + let definition = provenance.definition()?; + infer_definition_types(db, definition) + .function_type(definition)? + .literal(db) + } + _ => return None, + }; let previous_overload = previous_literal.last_definition; if !previous_overload.is_overload(db) { return None; @@ -506,6 +517,19 @@ impl<'db> OverloadLiteral<'db> { signature } + /// Returns the effective signatures of this overload after applying decorators. + pub(crate) fn decorated_signatures( + self, + db: &'db dyn Db, + ) -> impl Iterator> + Clone + 'db { + match binding_type(db, self.definition(db)) { + Type::Callable(callable) => { + Either::Left(callable.signatures(db).overloads.iter().cloned()) + } + _ => Either::Right(std::iter::once(self.signature(db))), + } + } + /// Typed internally-visible "raw" signature for this function. /// That is, the return types of async functions are not wrapped in `CoroutineType[...]`. /// The `return_callable_typevar_scope` controls whether type variables that only appear in a @@ -740,6 +764,36 @@ impl<'db> FunctionLiteral<'db> { } } + /// Ignore previous overloads when applying decorators to an individual definition. + pub(super) const fn without_overloads(self) -> Self { + Self { + overloaded: false, + ..self + } + } + + /// Preserve the overload set and last-definition identity while updating decorator metadata. + pub(super) fn with_last_definition_metadata( + self, + db: &'db dyn Db, + decorated: OverloadLiteral<'db>, + ) -> Self { + let definition = self.last_definition; + Self { + last_definition: OverloadLiteral::new( + db, + definition.name(db), + definition.known(db), + definition.body_scope(db), + definition.decorators(db), + decorated.deprecated(db), + decorated.dataclass_transformer_params(db), + definition.has_explicit_return_annotation(db), + ), + ..self + } + } + fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { // All of the overloads of a function literal should have the same name. self.last_definition.name(db) @@ -820,9 +874,8 @@ impl<'db> FunctionLiteral<'db> { (overloads.as_ref(), *implementation) } - fn has_separate_implementation(self, db: &'db dyn Db) -> bool { - !self.last_definition.is_overload(db) - && self.last_definition.previous_overload(db).is_some() + pub(super) fn has_separate_implementation(self, db: &'db dyn Db) -> bool { + self.overloaded && !self.last_definition.is_overload(db) } fn iter_overloads_and_implementation( @@ -854,7 +907,14 @@ impl<'db> FunctionLiteral<'db> { return CallableSignature::single(implementation.signature(db)); } - CallableSignature::from_overloads(overloads.iter().map(|overload| overload.signature(db))) + CallableSignature::from_overloads(overloads.iter().flat_map(|overload| { + // The last overload may still be inferred, so querying its binding would create a cycle. + if *overload == self.last_definition { + Either::Left(std::iter::once(overload.signature(db))) + } else { + Either::Right(overload.decorated_signatures(db)) + } + })) } /// Typed externally-visible signature of the last overload or implementation of this function. @@ -1013,22 +1073,23 @@ pub struct UpdatedFunctionSignatures<'db> { /// See also: [`FunctionLiteral::signature`]. signature: Option>, - /// Contains a potentially modified signature for the implementation of an overloaded function, - /// in case certain operations (like type mappings) have been applied to it. + /// Contains the potentially modified callables for the implementation of an overloaded + /// function, in case decorators or type mappings have been applied to it. Each callable can + /// itself be overloaded. /// /// See also: [`FunctionLiteral::last_definition_signature`]. - implementation_signature: Option>, + implementation_callables: Option]>>, } impl<'db> UpdatedFunctionSignatures<'db> { fn new( signature: Option>, - implementation_signature: Option>, + implementation_callables: Option]>>, ) -> Option> { - (signature.is_some() || implementation_signature.is_some()).then(|| { + (signature.is_some() || implementation_callables.is_some()).then(|| { Box::new(Self { signature, - implementation_signature, + implementation_callables, }) }) } @@ -1058,8 +1119,10 @@ pub(super) fn walk_function_type<'db, V: super::visitor::TypeVisitor<'db> + ?Siz walk_signature(db, signature, visitor); } } - if let Some(signature) = function.updated_implementation_signature(db) { - walk_signature(db, signature, visitor); + if let Some(callables) = function.updated_implementation_callables(db) { + for callable in callables { + visitor.visit_callable_type(db, *callable); + } } } @@ -1072,9 +1135,48 @@ impl<'db> FunctionType<'db> { } fn updated_implementation_signature(self, db: &'db dyn Db) -> Option<&'db Signature<'db>> { + let [callable] = self.updated_implementation_callables(db)? else { + return None; + }; + let [signature] = callable.signatures(db).overloads.as_slice() else { + return None; + }; + Some(signature) + } + + fn updated_implementation_callables(self, db: &'db dyn Db) -> Option<&'db [CallableType<'db>]> { self.updated_signatures(db) .as_deref() - .and_then(|updated| updated.implementation_signature.as_ref()) + .and_then(|updated| updated.implementation_callables.as_deref()) + } + + /// Return all effective implementation callables, falling back to the raw implementation. + pub(super) fn implementation_callables(self, db: &'db dyn Db) -> Cow<'db, [CallableType<'db>]> { + self.updated_implementation_callables(db).map_or_else( + || { + Cow::Owned(vec![CallableType::single( + db, + self.last_definition_signature(db).clone(), + )]) + }, + Cow::Borrowed, + ) + } + + /// Retain decorated implementation callables without changing the caller-visible overloads. + pub(super) fn with_implementation_callables( + self, + db: &'db dyn Db, + implementation_callables: Box<[CallableType<'db>]>, + ) -> Self { + Self::new( + db, + self.literal(db), + UpdatedFunctionSignatures::new( + self.updated_signature(db).cloned(), + Some(implementation_callables), + ), + ) } pub(crate) fn with_inherited_generic_context( @@ -1086,17 +1188,27 @@ impl<'db> FunctionType<'db> { .signature(db) .with_inherited_generic_context(db, inherited_generic_context); let literal = self.literal(db); - let updated_implementation_signature = literal.has_separate_implementation(db).then(|| { - self.last_definition_signature(db) - .clone() - .with_inherited_generic_context(db, inherited_generic_context) + let updated_implementation_callables = literal.has_separate_implementation(db).then(|| { + self.implementation_callables(db) + .iter() + .map(|callable| { + CallableType::new( + db, + callable + .signatures(db) + .with_inherited_generic_context(db, inherited_generic_context), + callable.kind(db), + callable.provenance(db), + ) + }) + .collect() }); Self::new( db, literal, UpdatedFunctionSignatures::new( Some(updated_signature), - updated_implementation_signature, + updated_implementation_callables, ), ) } @@ -1111,7 +1223,7 @@ impl<'db> FunctionType<'db> { // Returned-callable rescoping and type-alias specialization should not rebuild signatures from the // function literal; doing so can re-enter recursive `TypeOf` evaluation. let literal = self.literal(db); - let (updated_signature, updated_implementation_signature) = if matches!( + let (updated_signature, updated_implementation_callables) = if matches!( type_mapping, TypeMapping::ApplySpecialization( ApplySpecialization::ReturnCallables(_) | ApplySpecialization::TypeAlias(_) @@ -1125,8 +1237,13 @@ impl<'db> FunctionType<'db> { self.updated_signature(db).map(|signature| { signature.apply_type_mapping_impl(db, type_mapping, tcx, visitor) }), - self.updated_implementation_signature(db).map(|signature| { - signature.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + self.updated_implementation_callables(db).map(|callables| { + callables + .iter() + .map(|callable| { + callable.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + }) + .collect() }), ) } else { @@ -1136,23 +1253,23 @@ impl<'db> FunctionType<'db> { .apply_type_mapping_impl(db, type_mapping, tcx, visitor), ), literal.has_separate_implementation(db).then(|| { - self.last_definition_signature(db).apply_type_mapping_impl( - db, - type_mapping, - tcx, - visitor, - ) + self.implementation_callables(db) + .iter() + .map(|callable| { + callable.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + }) + .collect() }), ) }; - if updated_signature.is_none() && updated_implementation_signature.is_none() { + if updated_signature.is_none() && updated_implementation_callables.is_none() { self } else { Self::new( db, literal, - UpdatedFunctionSignatures::new(updated_signature, updated_implementation_signature), + UpdatedFunctionSignatures::new(updated_signature, updated_implementation_callables), ) } } @@ -1524,11 +1641,16 @@ impl<'db> FunctionType<'db> { } None => None, }; - let updated_implementation_signature = - match self.updated_implementation_signature(db) { - Some(signature) => { - Some(signature.recursive_type_normalized_impl(db, div, nested)?) - } + let updated_implementation_callables = + match self.updated_implementation_callables(db) { + Some(callables) => Some( + callables + .iter() + .map(|callable| { + callable.recursive_type_normalized_impl(db, div, nested) + }) + .collect::>>()?, + ), None => None, }; Some(Self::new( @@ -1536,7 +1658,7 @@ impl<'db> FunctionType<'db> { literal, UpdatedFunctionSignatures::new( updated_signature, - updated_implementation_signature, + updated_implementation_callables, ), )) }, diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs index e2c6878a86..bd8cfcedf2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -434,8 +434,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { function.returns.is_some(), ); let function_literal = FunctionLiteral::new(db, overload_literal); - - let mut inferred_ty = Type::FunctionLiteral(FunctionType::new(db, function_literal, None)); + let function_type = FunctionType::new(db, function_literal, None); + let is_decorated_overload_implementation = !decorator_types_and_nodes.is_empty() + && function_literal.has_separate_implementation(db); + let is_decorated_overload = + !decorator_types_and_nodes.is_empty() && overload_literal.is_overload(db); + + let mut inferred_ty = Type::FunctionLiteral( + if is_decorated_overload_implementation || is_decorated_overload { + FunctionType::new(db, function_literal.without_overloads(), None) + } else { + function_type + }, + ); if !decorator_list.is_empty() { self.undecorated_type = Some(inferred_ty); } @@ -478,6 +489,34 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; } + if is_decorated_overload_implementation { + let function_type = if let Type::FunctionLiteral(function) = inferred_ty { + FunctionType::new( + db, + function_literal + .with_last_definition_metadata(db, function.literal(db).last_definition), + None, + ) + } else { + function_type + }; + let implementation_callables = inferred_ty + .try_upcast_to_callable(db) + .map_or_else(Box::default, |callables| { + callables.iter().copied().collect() + }); + inferred_ty = Type::FunctionLiteral( + function_type.with_implementation_callables(db, implementation_callables), + ); + } else if is_decorated_overload && let Type::FunctionLiteral(function) = inferred_ty { + inferred_ty = Type::FunctionLiteral(FunctionType::new( + db, + function_literal + .with_last_definition_metadata(db, function.literal(db).last_definition), + None, + )); + } + self.add_declaration_with_binding( function.into(), definition, diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs index 14270528b7..8a6ad9d270 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs @@ -9,7 +9,7 @@ use crate::{ Db, place::{DefinedPlace, Definedness, Place, place_from_bindings}, types::{ - KnownClass, Type, + CallableType, KnownClass, Type, context::InferContext, diagnostic::INVALID_OVERLOAD, function::{FunctionDecorators, FunctionType, KnownFunction, OverloadLiteral}, @@ -101,8 +101,15 @@ pub(crate) fn check_overloaded_function<'db>( if let Some(implementation) = implementation && binding_decorator_inconsistencies.is_empty() + && context.is_lint_enabled(&INVALID_OVERLOAD) { - check_non_generic_overload_implementation_consistency(context, overloads, implementation); + let implementation_callables = function.implementation_callables(db); + check_non_generic_overload_implementation_consistency( + context, + overloads, + implementation, + &implementation_callables, + ); } // Check that the overloaded function has at least two overloads @@ -272,30 +279,46 @@ pub(crate) fn check_overloaded_function<'db>( /// Check non-generic overload signatures against their implementation. /// -/// This is the first, deliberately narrow pass at overload implementation consistency. It reports -/// only when the overloads and implementation are all non-generic; generic signatures require -/// careful treatment of type-variable domains. +/// This is the first, deliberately narrow pass at overload implementation consistency. Signature +/// compatibility is checked only when the overloads and implementation are all non-generic; +/// generic signatures require careful treatment of type-variable domains. Each callable +/// alternative of the implementation must contain a signature consistent with each overload. fn check_non_generic_overload_implementation_consistency<'db>( context: &InferContext<'db, '_>, overloads: &'db [OverloadLiteral<'db>], implementation: OverloadLiteral<'db>, + implementation_callables: &[CallableType<'db>], ) { - if !context.is_lint_enabled(&INVALID_OVERLOAD) { + let db = context.db(); + if implementation_callables.is_empty() + || implementation_callables + .iter() + .any(|callable| callable.signatures(db).overloads.is_empty()) + { + let function_node = implementation.node(db, context.file(), context.module()); + if let Some(builder) = context.report_lint(&INVALID_OVERLOAD, &function_node.name) { + builder.into_diagnostic(format_args!( + "Overload implementation is not callable after applying decorators" + )); + } return; } - let db = context.db(); - let implementation_signature = implementation.signature(db); - // TODO: Remove this temporary non-generic restriction once overload implementation consistency // handles type-variable domains. - if !implementation_signature.is_non_generic() { + if implementation_callables + .iter() + .flat_map(|callable| &callable.signatures(db).overloads) + .any(|signature| !signature.is_non_generic()) + { return; } - let overload_signatures = overloads - .iter() - .map(|overload| (overload, overload.signature(db))); + let overload_signatures = overloads.iter().flat_map(|overload| { + overload + .decorated_signatures(db) + .map(move |signature| (overload, signature)) + }); if overload_signatures .clone() @@ -306,10 +329,40 @@ fn check_non_generic_overload_implementation_consistency<'db>( for (overload, overload_signature) in overload_signatures { let function_node = overload.node(db, context.file(), context.module()); - let parameter_consistency = implementation_signature - .non_generic_implementation_parameters_consistency_with(db, &overload_signature); - let return_type_consistency = implementation_signature - .non_generic_implementation_return_type_consistency_with(db, &overload_signature); + let Some((implementation_signature, parameter_consistency, return_type_consistency)) = + implementation_callables.iter().find_map(|callable| { + let mut inconsistency = None; + for implementation_signature in &callable.signatures(db).overloads { + let parameter_consistency = implementation_signature + .non_generic_implementation_parameters_consistency_with( + db, + &overload_signature, + ); + let return_type_consistency = implementation_signature + .non_generic_implementation_return_type_consistency_with( + db, + &overload_signature, + ); + if matches!( + (¶meter_consistency, &return_type_consistency), + ( + ParameterConsistency::Consistent, + ReturnTypeConsistency::Consistent + ) + ) { + return None; + } + inconsistency = Some(( + implementation_signature, + parameter_consistency, + return_type_consistency, + )); + } + inconsistency + }) + else { + continue; + }; let (parameter_error_context, return_type_error_context, message) = match (parameter_consistency, return_type_consistency) { From 1ea0f4c8547c6b430a4fa78b4dbb62dd90cea9a1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 26 Jul 2026 23:29:17 -0700 Subject: [PATCH 076/390] [ty] Avoid expanding optional enum comparisons (#27105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary We already have an efficient way to compare enums as a whole. However, this fast path only applies when both sides contain nothing but enum values. An extra value such as `None` causes `ModelSlug | None` to miss it: ```python def matches(slug: ModelSlug, candidate: ModelSlug | None) -> bool: return candidate == slug ``` Instead of comparing `ModelSlug` once, we expanded it into individual members. Repeated comparisons in a boolean chain made large enums especially expensive. We now group the enum values on each side, compare the groups once using the existing enum comparison logic, and evaluate any other union members normally. This fixes optional enums without special-casing `None`; the same approach handles integers, strings, `Any`, `Unknown`, and unions containing several enum classes. For an optional enum, separate the enum from the other possible value: ```text ModelSlug | None → grouped enum: ModelSlug → other value: None ``` The same approach works for several enum classes: ```text LeftEnum | RightEnum | None → grouped enums: LeftEnum | RightEnum → other value: None ``` Specifically, we evaluate comparisons in this order: 1. Compare enum values, including enums inside unions. 2. Handle `Any` and `Unknown`. 3. Expand finite alternatives only when necessary. 4. Fall back to ordinary structural comparison. The 406-member cross-enum case improves from approximately 4.95 seconds to 0.05 seconds. Closes https://github.com/astral-sh/ty/issues/4069. --------- Co-authored-by: Carl Meyer --- crates/ruff_benchmark/benches/ty.rs | 39 ++ .../mdtest/narrow/conditionals/eq.md | 393 +++++++++++++++++- .../resources/mdtest/narrow/match.md | 7 + .../ty_python_semantic/src/types/equality.rs | 110 +++-- .../src/types/equality/enums.rs | 310 +++++++++++++- 5 files changed, 816 insertions(+), 43 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index 08834524a2..d908b0612a 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -665,6 +665,44 @@ fn benchmark_narrowed_str_enum_comparison(criterion: &mut Criterion) { benchmark_enum_comparison(criterion, "ty_micro[narrowed_str_enum_comparison]", &code); } +/// Regression benchmark for . +/// +/// Compare a large enum with optional enum fields in a chain of conditions. +fn benchmark_optional_str_enum_comparison(criterion: &mut Criterion) { + const NUM_ENUM_MEMBERS: usize = 256; + + let mut code = + "from dataclasses import dataclass\nfrom enum import StrEnum\n\nclass ModelSlug(StrEnum):\n" + .to_string(); + for index in 0..NUM_ENUM_MEMBERS { + writeln!(&mut code, " M{index} = \"m{index}\"").ok(); + } + code.push_str( + r#" + +@dataclass +class Category: + default_model: ModelSlug | None = None + browsing_model: ModelSlug | None = None + code_interpreter_model: ModelSlug | None = None + plugins_model: ModelSlug | None = None + dalle_model: ModelSlug | None = None + + +def belongs(slug: ModelSlug, category: Category) -> bool: + return ( + category.default_model == slug + or category.browsing_model == slug + or category.code_interpreter_model == slug + or category.plugins_model == slug + or category.dalle_model == slug + ) +"#, + ); + + benchmark_enum_comparison(criterion, "ty_micro[optional_str_enum_comparison]", &code); +} + /// Ensure explicit enum-literal unions are compared as value sets, not member pairs. fn benchmark_enum_literal_union_comparison(criterion: &mut Criterion) { const NUM_ENUM_MEMBERS: usize = 256; @@ -2031,6 +2069,7 @@ criterion_group!( benchmark_large_enum_membership, benchmark_many_enum_members, benchmark_narrowed_str_enum_comparison, + benchmark_optional_str_enum_comparison, benchmark_enum_literal_union_comparison, benchmark_repeated_str_enum_comparisons, benchmark_cross_str_enum_comparison, diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index e6450c4867..902e4a1df9 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -170,6 +170,31 @@ def compare_non_overlapping_literal_unions( reveal_type(left == right) # revealed: Literal[False] ``` +Adding `None` to either side must not change which enum values can match: + +```py +def compare_optional_left(left: Choice | None, right: Choice): + if left == right: + reveal_type(left) # revealed: Choice + else: + reveal_type(left) # revealed: Choice | None + +def compare_optional_right(left: Choice, right: Choice | None): + if left == right: + reveal_type(right) # revealed: Choice +``` + +With ty's default builtin-equality assumptions, neither an integer nor `None` matches a +string-valued enum member: + +```py +def compare_enum_with_integer(left: Choice | int | None, right: Choice): + if left == right: + reveal_type(left) # revealed: Choice + else: + reveal_type(left) # revealed: Choice | int | None +``` + Members with the same known value are aliases, even when one value comes from a function call. Comparisons between their canonical members are always true: @@ -402,7 +427,8 @@ member. Exact member comparisons are true or false when both values are known: ```py from enum import StrEnum -from typing import Literal +from typing import Any, Literal +from typing_extensions import assert_type class Left(StrEnum): A = "a" @@ -448,12 +474,117 @@ def compare_subsets( reveal_type(right) # revealed: Literal[Right.SHARED] ``` +When only one side can be `None`, equality still narrows both enums to their shared value: + +```py +def compare_optional_cross_enum_left(left: Left | None, right: Right): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] + reveal_type(right) # revealed: Literal[Right.SHARED] + +def compare_optional_cross_enum_right(left: Left, right: Right | None): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] + reveal_type(right) # revealed: Literal[Right.SHARED] +``` + +When both sides can be `None`, equality can match `None` or the shared string: + +```py +def compare_both_optional_cross_enums(left: Left | None, right: Right | None): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] | None + reveal_type(right) # revealed: Literal[Right.SHARED] | None +``` + +Under the same assumptions, an unrelated integer does not change which enum members match, whether +the condition uses `==` or `!=`: + +```py +def compare_cross_enums_with_integer(left: Left | None, right: Right | int): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] + reveal_type(right) # revealed: Literal[Right.SHARED] + + if left != right: + reveal_type(left) # revealed: Left | None + reveal_type(right) # revealed: Right | int + else: + reveal_type(left) # revealed: Literal[Left.SHARED] + reveal_type(right) # revealed: Literal[Right.SHARED] +``` + +A plain string can also match a member of the other enum. The string and every matching enum member +must remain possible: + +```py +def compare_left_string_against_enum_members(left: Left | Literal["b"], right: Right): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED, "b"] + reveal_type(right) # revealed: Literal[Right.SHARED, Right.B] + +def compare_right_string_against_enum_members(left: Left, right: Right | Literal["a"]): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED, Left.A] + assert_type(right, Literal[Right.SHARED, "a"]) +``` + +A `dict[str, Any]` is treated as having dictionary equality, so it cannot match a string-valued enum +member: + +```py +def compare_cross_enum_with_dictionary(left: Left | dict[str, Any], right: Right | None): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] + reveal_type(right) # revealed: Literal[Right.SHARED] +``` + +By contrast, `Any` can match any enum member. It must not exclude `None` from the other side: + +```py +def compare_optional_enum_against_any(left: Left | None, right: Right | Any): + if left == right: + reveal_type(left) # revealed: Left | None + reveal_type(right) # revealed: Literal[Right.SHARED] | Any + +def compare_any_against_optional_enum(left: Left | Any, right: Right | None): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] | Any + reveal_type(right) # revealed: Right | None +``` + +If the two sides have no matching values, `==` is always false and `!=` is always true. A shared +`None` makes `==` uncertain: + +```py +def compare_disjoint_cross_enum_alternatives( + left: Literal[Left.A] | None, + disjoint: Literal[Right.B] | Literal[1], + overlapping: Literal[Right.B] | None, +): + reveal_type(left == disjoint) # revealed: Literal[False] + reveal_type(left != disjoint) # revealed: Literal[True] + reveal_type(left == overlapping) # revealed: bool +``` + +When all possible values match, `==` is always true: + +```py +def compare_matching_cross_enum_alternatives( + left: Literal[Left.SHARED] | Literal["shared"], + right: Literal[Right.SHARED], +): + reveal_type(left == right) # revealed: Literal[True] + reveal_type(left != right) # revealed: Literal[False] +``` + The same comparison-key projection applies when each operand spans several enum classes. This example represents 18 possible values on each side, which would otherwise require 324 pairwise comparisons: ```py from enum import IntEnum +from typing import Literal class MixedLeft0(IntEnum): A = 0 @@ -508,6 +639,29 @@ def compare_mixed_domains( reveal_type(right) # revealed: MixedRight0 ``` +Treating `str` as having builtin equality, adding `None` or `str` does not prevent matches between +integer-valued enum classes: + +```py +def compare_multiple_integer_enums_with_other_values( + left: MixedLeft0 | MixedLeft1 | None, + right: MixedRight0 | MixedRight1 | str, +): + if left == right: + reveal_type(left) # revealed: MixedLeft0 + reveal_type(right) # revealed: MixedRight0 +``` + +Python considers `False` equal to `0`, so a `False` alternative can match an integer-valued enum +member even when the other enum has no matching members: + +```py +def compare_false_to_integer_enum(left: MixedLeft1 | Literal[False], right: MixedRight0): + if left == right: + reveal_type(left) # revealed: Literal[False] + reveal_type(right) # revealed: Literal[MixedRight0.A] +``` + An open identity-comparing enum can still be narrowed to all of its declared members. Undeclared runtime members are not retained merely because every declared member matches: @@ -604,6 +758,23 @@ def compare_open(left: OpenLeft, right: CustomRight): reveal_type(left) # revealed: OpenLeft ``` +A custom equality method must still determine the result when the enum is combined with `None`: + +```py +def compare_optional_custom(left: CustomLeft | None, right: CustomRight): + if left == right: + reveal_type(left) # revealed: CustomLeft +``` + +An enum with `_missing_` may have members that do not appear in its definition. Adding `None` must +not cause the comparison to assume that its declared member is the only possible match: + +```py +def compare_optional_open(left: OpenLeft | None, right: CustomRight): + if left == right: + reveal_type(left) # revealed: OpenLeft +``` + The same narrowing applies when comparing enum members directly with their inherited integer or string values. The negative constraint excludes both the builtin literal and every enum member known to compare equal to it: @@ -1459,7 +1630,10 @@ def tagged_union_with_unrelated_assignment(value: A | B): ```py import sys from enum import Enum, IntEnum -from typing import Any, Literal, TypeVar +from typing import Any, Literal, TypeAlias, TypeVar + +from ty_extensions import Unknown +from typing_extensions import assert_never, assert_type T = TypeVar("T", bound=object) U = TypeVar("U") @@ -1470,6 +1644,9 @@ class Color(Enum): RED = 1 BLUE = 2 +class OtherColor(Enum): + RED = 1 + class NonReflexive(Enum): VALUE = 1 @@ -1560,6 +1737,147 @@ def _(x: Any): reveal_type(x) # revealed: Any & ~TypeVar ``` +`Any` must stay `Any` when compared with an enum, on either side of the comparison: + +```py +def enum_against_any(value: Color, other: Any): + if value != other: + reveal_type(other) # revealed: Any + +def any_against_enum(value: Any, other: Color): + if value != other: + reveal_type(value) # revealed: Any +``` + +`Any` must also stay `Any` when the enum can be `None`: + +```py +def optional_enum_against_any(value: Color | None, other: Any): + if value != other: + reveal_type(other) # revealed: Any + +def any_against_optional_enum(value: Any, other: Color | None): + if value != other: + reveal_type(value) # revealed: Any +``` + +`Any` must also stay `Any` when compared with `bool | None`: + +```py +def optional_bool_against_any(value: bool | None, other: Any): + if value != other: + reveal_type(other) # revealed: Any +``` + +Comparing `Color | Any` with `Color | None` must keep both `Color` and `Any`: + +```py +def gradual_enum_union(value: Color | Any, other: Color | None): + if value != other: + reveal_type(value) # revealed: Color | Any +``` + +`Color | Any` must stay unchanged when the other value can be an enum member or `None`. This applies +to `!=` and the false branch of `==`: + +```py +def any_union_against_optional_enum_member(value: Color | Any, other: Literal[Color.RED] | None): + if value != other: + reveal_type(value) # revealed: Color | Any + assert_type(value, Color | Any) + +def any_union_against_optional_enum_member_equality_else(value: Color | Any, other: Literal[Color.RED] | None): + if value == other: + return + reveal_type(value) # revealed: Color | Any +``` + +An alias for `Any` must preserve the same result: + +```py +AnyAlias: TypeAlias = Any + +def any_alias_union_against_optional_enum_member(value: Color | AnyAlias, other: Literal[Color.RED] | None): + if value != other: + reveal_type(value) # revealed: Color | Any +``` + +The same comparisons also preserve `Unknown`: + +```py +def unknown_union_against_optional_enum_member(value: Color | Unknown, other: Literal[Color.RED] | None): + if value != other: + reveal_type(value) # revealed: Color | Unknown + assert_type(value, Color | Unknown) + +def unknown_union_against_optional_enum_member_equality_else(value: Color | Unknown, other: Literal[Color.RED] | None): + if value == other: + return + reveal_type(value) # revealed: Color | Unknown +``` + +When an enum check and a comparison are combined with `and`, either condition can be false. The +original union must therefore be preserved: + +```py +def any_union_after_enum_check(value: Color | Any, other: Color | Any): + if isinstance(value, Color) and value == other: + return + reveal_type(value) # revealed: Color | Any + assert_type(value, Color | Any) + +def unknown_union_after_enum_check(value: Color | Unknown, other: Color | Unknown): + if isinstance(value, Color) and value == other: + return + reveal_type(value) # revealed: Color | Unknown + assert_type(value, Color | Unknown) +``` + +The second comparison can fail even when the first one matches, so both possible types must remain: + +```py +def any_union_after_failed_comparisons(value: Color | Any, other: OtherColor | None): + if value == Color.RED and value == other: + return + reveal_type(value) # revealed: Color | Any + assert_type(value, Color | Any) + +def unknown_union_after_failed_comparisons(value: Color | Unknown, other: OtherColor | None): + if value == Color.RED and value == other: + return + reveal_type(value) # revealed: Color | Unknown + assert_type(value, Color | Unknown) +``` + +These `Enum` classes compare by identity, so their members are not equal even when their underlying +values match. Comparing with `OtherColor.RED` must therefore exclude every `Color` member: + +```py +def any_comparison_with_other_enum(value: Color | OtherColor | Any): + if value == OtherColor.RED: + reveal_type(value) # revealed: OtherColor | (Any & ~Color) + if isinstance(value, Color): + assert_never(value) + +def unknown_comparison_with_other_enum(value: Color | OtherColor | Unknown): + if value == OtherColor.RED: + reveal_type(value) # revealed: OtherColor | (Unknown & ~Color) + if isinstance(value, Color): + assert_never(value) +``` + +`Color | Any` must also stay unchanged after either `==` or `!=`: + +```py +def gradual_enum_union_against_enum(value: Color | Any, other: Color): + if value == other: + reveal_type(value) # revealed: Color | Any + +def gradual_enum_union_inequality(value: Color | Any, other: Color): + if value != other: + reveal_type(value) # revealed: Color | Any +``` + ## Booleans and integers ```py @@ -1772,17 +2090,22 @@ def _(x: A | B): ## Enabling strict equality narrowing -The `strict-equality-semantics` option can be enabled to preserve broad builtin types and union -members that a subclass could compare equal to. Narrowing types that are already literal unions -remains safe and is unaffected. This also applies to tuples, whose subclasses can override equality. +Enabling `strict-equality-semantics` accounts for builtin subclasses that override `__eq__` or +compare equal to a literal without belonging to its `Literal` type. It preserves broad builtin types +and union alternatives that could compare equal, including tuples. Literal unions and enum members +are still narrowed when it is safe. ```toml +[environment] +python-version = "3.11" + [analysis] strict-equality-semantics = true ``` ```py -from typing import Literal +from enum import IntEnum, StrEnum +from typing import Any, Literal def broad(value: str): if value == "a": @@ -1800,6 +2123,64 @@ def literal(value: Literal["a", "b"]): if value == "a": reveal_type(value) # revealed: Literal["a"] +class Left(StrEnum): + A = "a" + SHARED = "shared" + +class Right(StrEnum): + SHARED = "shared" + B = "b" + +def compare_enum_with_integer(left: Left | int | None, right: Left): + if left == right: + reveal_type(left) # revealed: Left | int + +def compare_cross_enums_with_integer(left: Left | None, right: Right | int): + if left == right: + reveal_type(left) # revealed: Left | None + reveal_type(right) # revealed: Literal[Right.SHARED] | int + + if left != right: + reveal_type(left) # revealed: Left | None + reveal_type(right) # revealed: Right | int + else: + reveal_type(left) # revealed: Left | None + reveal_type(right) # revealed: Literal[Right.SHARED] | int + +def compare_cross_enum_with_dictionary(left: Left | dict[str, Any], right: Right | None): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] | dict[str, Any] + reveal_type(right) # revealed: Right | None + +def compare_both_optional_cross_enums(left: Left | None, right: Right | None): + if left == right: + reveal_type(left) # revealed: Literal[Left.SHARED] | None + reveal_type(right) # revealed: Literal[Right.SHARED] | None + +class MixedLeft0(IntEnum): + ZERO = 0 + ONE = 1 + +class MixedLeft1(IntEnum): + TWO = 2 + THREE = 3 + +class MixedRight0(IntEnum): + ZERO = 0 + ONE = 1 + +class MixedRight1(IntEnum): + FOUR = 4 + FIVE = 5 + +def compare_multiple_integer_enums_with_other_values( + left: MixedLeft0 | MixedLeft1 | None, + right: MixedRight0 | MixedRight1 | str, +): + if left == right: + reveal_type(left) # revealed: MixedLeft0 | MixedLeft1 | None + reveal_type(right) # revealed: MixedRight0 | str + class Foo: ... def union(value: Foo | None, other: Foo): diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index 04f7e2fbca..2fce07e47a 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -3101,6 +3101,13 @@ def cross_int_enum_members(value: First | Second) -> None: case _: reveal_type(value) # revealed: Literal[First.TWO, Second.TWO] +def optional_cross_int_enum_members(value: First | Second | None) -> None: + match value: + case First.ONE: + reveal_type(value) # revealed: Literal[First.ONE, Second.ONE] + case _: + reveal_type(value) # revealed: Literal[First.TWO, Second.TWO] | None + class Warning(Enum): W1 = auto() diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index 4ab7f0947f..e239ea7f6f 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -17,7 +17,7 @@ use super::{ mod enums; -use self::enums::evaluate_enum_domains; +use self::enums::evaluate_enum_comparison; /// The result of evaluating a runtime comparison between two types. /// @@ -166,8 +166,14 @@ pub(super) fn evaluate_type_equality<'db>( ) }) .or_else(|| { - evaluate_enum_domains(db, left, right, branch, ComparisonOperator::Equality) - .and_then(|result| result.constraint(branch)) + evaluate_enum_comparison( + &mut ComparisonEvaluator::new(db, soundness_policy), + left, + right, + branch, + ComparisonOperator::Equality, + ) + .and_then(|result| result.constraint(branch)) }) .or_else(|| { if comparison_domain( @@ -474,7 +480,10 @@ impl<'db> ComparisonEvaluator<'db> { } } -/// Evaluate a comparison whose aliases are resolved and whose key is registered as active. +/// Evaluate one comparison after resolving aliases and checking for recursion. +/// +/// Handle enums and dynamic values such as `Any` before checking individual enum members. +/// Otherwise, checking each member separately can incorrectly narrow `Any`. /// /// Recursive comparisons must use [`ComparisonEvaluator::evaluate`] so cycles are detected. fn evaluate_comparison_once<'db>( @@ -484,19 +493,78 @@ fn evaluate_comparison_once<'db>( branch: ComparisonBranch, operator: ComparisonOperator, ) -> ComparisonResult<'db> { - let db = evaluator.db; + evaluate_enum_comparison(evaluator, left, right, branch, operator) + .or_else(|| evaluate_dynamic_comparison(evaluator, left, right, branch, operator)) + .or_else(|| evaluate_finite_comparison(evaluator, left, right, branch, operator)) + .unwrap_or_else(|| evaluate_structural_comparison(evaluator, left, right, branch, operator)) +} - if let Some(result) = evaluate_enum_domains(db, left, right, branch, operator) { - return result; +/// Handle dynamic values such as `Any` before checking individual enum members. +/// +/// A one-member enum can exclude that member from `Any`. An enum with several members must not +/// exclude all of its members one at a time. +fn evaluate_dynamic_comparison<'db>( + evaluator: &mut ComparisonEvaluator<'db>, + left: Type<'db>, + right: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, +) -> Option> { + let db = evaluator.db; + match (left, right) { + (Type::Dynamic(_), other) + if !operator.condition_expects_equality(branch) + && all_values_compare_equal(evaluator, other, operator) => + { + let excluded = if other.is_enum(db) + && let Some(alternatives) = finite_alternatives(db, other, operator) + && let [alternative] = alternatives.as_slice() + { + *alternative + } else { + other + }; + Some(ComparisonResult::CanNarrow( + IntersectionBuilder::new(db) + .add_positive(left) + .add_negative(excluded) + .build(), + )) + } + (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => Some(ComparisonResult::Ambiguous), + _ => None, } +} - if let Some(alternatives) = finite_alternatives(db, left, operator) { - return evaluate_union_left(evaluator, &alternatives, right, branch, operator); - } - if let Some(alternatives) = finite_alternatives(db, right, operator) { - return evaluate_union_right(evaluator, left, &alternatives, branch, operator); - } +/// Compare finite sets of values after handling enums and dynamic values. +/// +/// Start with the side being narrowed so its restrictions are not applied to the other side. +fn evaluate_finite_comparison<'db>( + evaluator: &mut ComparisonEvaluator<'db>, + left: Type<'db>, + right: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, +) -> Option> { + let db = evaluator.db; + finite_alternatives(db, left, operator) + .map(|alternatives| evaluate_union_left(evaluator, &alternatives, right, branch, operator)) + .or_else(|| { + finite_alternatives(db, right, operator).map(|alternatives| { + evaluate_union_right(evaluator, left, &alternatives, branch, operator) + }) + }) +} +/// Compare values not handled by the enum, dynamic, or finite-value stages. +fn evaluate_structural_comparison<'db>( + evaluator: &mut ComparisonEvaluator<'db>, + left: Type<'db>, + right: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, +) -> ComparisonResult<'db> { + let db = evaluator.db; match (left, right) { ( Type::Never @@ -521,22 +589,6 @@ fn evaluate_comparison_once<'db>( | Type::TypeIs(_), ) => ComparisonResult::Ambiguous, - (Type::Dynamic(_), other) => { - if !operator.condition_expects_equality(branch) - && all_values_compare_equal(evaluator, other, operator) - { - ComparisonResult::CanNarrow( - IntersectionBuilder::new(db) - .add_positive(left) - .add_negative(other) - .build(), - ) - } else { - ComparisonResult::Ambiguous - } - } - (_, Type::Dynamic(_)) => ComparisonResult::Ambiguous, - (Type::TypeVar(var), other) => match var.typevar(db).bound_or_constraints(db) { None => ComparisonResult::Ambiguous, Some(TypeVarBoundOrConstraints::UpperBound(_)) => { diff --git a/crates/ty_python_semantic/src/types/equality/enums.rs b/crates/ty_python_semantic/src/types/equality/enums.rs index cae8937677..f32b3dc9cd 100644 --- a/crates/ty_python_semantic/src/types/equality/enums.rs +++ b/crates/ty_python_semantic/src/types/equality/enums.rs @@ -12,17 +12,42 @@ use crate::types::{ use crate::{Db, FxOrderMap, FxOrderSet}; use super::{ - ComparisonBranch, ComparisonOperator, ComparisonResult, KnownComparisonSemantics, - enum_literal_value, + ComparisonBranch, ComparisonEvaluator, ComparisonGoal, ComparisonOperator, ComparisonResult, + KnownComparisonSemantics, combine_definite_truthiness, enum_literal_value, + evaluate_against_results, evaluate_target_union, }; -/// Compare two enum value domains without comparing every pair of members. +/// Compare enum values without checking every pair of members. /// -/// Any narrowing constraint produced here contains only enum-membership facts. In particular, -/// equality never transfers gradual or nominal intersection state from one operand to the other. -/// Same-class domains compare compact member sets directly, while comparisons spanning multiple -/// classes project their members onto runtime comparison keys. -pub(super) fn evaluate_enum_domains<'db>( +/// If either side also contains other values, compare those values normally. +/// +/// Return `None` when the enum comparison does not apply. +pub(super) fn evaluate_enum_comparison<'db>( + evaluator: &mut ComparisonEvaluator<'db>, + target: Type<'db>, + other: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, +) -> Option> { + evaluate_enum_domains(evaluator.db, target, other, branch, operator).or_else(|| { + PartitionedEnumComparison::new(evaluator.db, target, other, branch, operator).map( + |comparison| match comparison.evaluate(evaluator, branch, operator) { + ComparisonResult::CanNarrow(narrowed) + if narrowed == target.resolve_type_alias(evaluator.db) => + { + ComparisonResult::Ambiguous + } + result => result, + }, + ) + }) +} + +/// Compare values that are all enum members. +/// +/// Describe the result using enum members only. Do not copy other restrictions from one side to +/// the other. +fn evaluate_enum_domains<'db>( db: &'db dyn Db, target: Type<'db>, other: Type<'db>, @@ -41,6 +66,195 @@ pub(super) fn evaluate_enum_domains<'db>( ProjectedEnumComparison::new(db, target, &other, operator)?.evaluate(db, branch, operator) } +/// Compare unions that contain enums and other values. +/// +/// Compare enum members together and compare other values normally. Values such as `None`, +/// `Any`, or a matching string can also affect which values match. +/// +/// Compare the enum members only once. +/// +/// ```python +/// from enum import StrEnum +/// +/// class Left(StrEnum): +/// SHARED = "shared" +/// LEFT = "left" +/// +/// class Right(StrEnum): +/// SHARED = "shared" +/// RIGHT = "right" +/// +/// def compare(left: Left | None, right: Right | None): +/// if left == right: +/// reveal_type(left) # Literal[Left.SHARED] | None +/// reveal_type(right) # Literal[Right.SHARED] | None +/// ``` +struct PartitionedEnumComparison<'db> { + target: EnumDomainPartition<'db>, + other: EnumDomainPartition<'db>, + other_type: Type<'db>, + enum_result: ComparisonResult<'db>, +} + +impl<'db> PartitionedEnumComparison<'db> { + /// Prepare a comparison when both sides contain enums and at least one side also contains + /// another value. + /// + /// Return `None` if either enum has unsupported comparison behavior. + fn new( + db: &'db dyn Db, + target: Type<'db>, + other: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, + ) -> Option { + if !matches!(target.resolve_type_alias(db), Type::Union(_)) + && !matches!(other.resolve_type_alias(db), Type::Union(_)) + { + return None; + } + + let target = EnumDomainPartition::from_type(db, target)?; + let other_type = other; + let other = EnumDomainPartition::from_type(db, other)?; + + if !target.has_other_values() && !other.has_other_values() { + return None; + } + + let enum_result = + evaluate_enum_domains(db, target.enum_type, other.enum_type, branch, operator)?; + + Some(Self { + target, + other, + other_type, + enum_result, + }) + } + + /// Reuse the saved enum result and compare all other values normally. + fn evaluate_pair( + &self, + evaluator: &mut ComparisonEvaluator<'db>, + target: Type<'db>, + other: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, + ) -> ComparisonResult<'db> { + if target == self.target.enum_type && other == self.other.enum_type { + self.enum_result + } else { + evaluator.evaluate(target, other, branch, operator) + } + } + + /// Compare one possible value with the other side. + /// + /// Compare `Any` and `Unknown` with the whole union so neither is narrowed by separate values. + /// + /// Return whether the result is certain or which values can still match. + fn evaluate_against_other( + &self, + evaluator: &mut ComparisonEvaluator<'db>, + target: Type<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, + ) -> ComparisonResult<'db> { + if let [other] = self.other.alternatives.as_slice() { + return self.evaluate_pair(evaluator, target, *other, branch, operator); + } + + if evaluator.goal == ComparisonGoal::Truthiness { + return combine_definite_truthiness( + self.other + .alternatives + .iter() + .map(|other| self.evaluate_pair(evaluator, target, *other, branch, operator)), + ); + } + + if matches!(target.resolve_type_alias(evaluator.db), Type::Dynamic(_)) { + return evaluator.evaluate(target, self.other_type, branch, operator); + } + + evaluate_against_results( + evaluator.db, + target, + branch, + self.other + .alternatives + .iter() + .map(|other| self.evaluate_pair(evaluator, target, *other, branch, operator)), + ) + } + + /// Compare all possible values. + /// + /// If no member of an enum can match, exclude it from the other possible values. + /// + /// Return whether the result is certain or which values can still match. + fn evaluate( + &self, + evaluator: &mut ComparisonEvaluator<'db>, + branch: ComparisonBranch, + operator: ComparisonOperator, + ) -> ComparisonResult<'db> { + if let [target] = self.target.alternatives.as_slice() { + return self.evaluate_against_other(evaluator, *target, branch, operator); + } + + if evaluator.goal == ComparisonGoal::Truthiness { + return combine_definite_truthiness( + self.target.alternatives.iter().map(|target| { + self.evaluate_against_other(evaluator, *target, branch, operator) + }), + ); + } + + let mut narrowed_enum = None; + let result = + evaluate_target_union(evaluator.db, &self.target.alternatives, branch, |target| { + let result = self.evaluate_against_other(evaluator, target, branch, operator); + if target == self.target.enum_type + && let ComparisonResult::CanNarrow(narrowed) = result + && narrowed != target + { + narrowed_enum = Some(narrowed); + } + result + }); + + if let ComparisonResult::CanNarrow(narrowed) = result + && let Some(narrowed_enum) = narrowed_enum + && let Some(domains) = EnumDomainSet::from_type(evaluator.db, self.target.enum_type) + { + let excluded = domains + .domains + .iter() + .fold(UnionBuilder::new(evaluator.db), |builder, domain| { + let domain_type = domain.restriction_type(evaluator.db); + if domain_type.is_disjoint_from(evaluator.db, narrowed_enum) { + builder.add(domain_type) + } else { + builder + } + }) + .build(); + if !excluded.is_never() { + return ComparisonResult::CanNarrow( + IntersectionBuilder::new(evaluator.db) + .add_positive(narrowed) + .add_negative(excluded) + .build(), + ); + } + } + + result + } +} + /// Two non-empty value domains from the same enum and the semantics used to compare them. /// /// This representation avoids constructing and pairwise comparing unions of every declared @@ -449,6 +663,86 @@ impl<'db> EnumValueSet<'db> { } } +/// The enum members and other values on one side of a comparison. +/// +/// Place enum members at the first enum's position and keep other values in their original order. +struct EnumDomainPartition<'db> { + enum_type: Type<'db>, + alternatives: Vec>, +} + +impl<'db> EnumDomainPartition<'db> { + /// Combine enum values while keeping other values in their original order. + /// + /// Return `None` when there is no enum or a type alias refers to itself. + fn from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { + fn collect<'db>( + db: &'db dyn Db, + ty: Type<'db>, + enum_types: &mut Vec>, + alternatives: &mut Vec>, + enum_position: &mut Option, + active_types: &mut FxHashSet>, + ) -> Option<()> { + if EnumValueSet::from_type(db, ty, active_types).is_some() { + enum_position.get_or_insert(alternatives.len()); + enum_types.push(ty); + return Some(()); + } + + let Type::Union(union) = ty.resolve_type_alias(db) else { + alternatives.push(ty); + return Some(()); + }; + + if !active_types.insert(ty) { + return None; + } + + let result = union.elements(db).iter().try_for_each(|element| { + collect( + db, + *element, + enum_types, + alternatives, + enum_position, + active_types, + ) + }); + active_types.remove(&ty); + result + } + + let mut enum_types = Vec::new(); + let mut alternatives = Vec::new(); + let mut enum_position = None; + let mut active_types = FxHashSet::default(); + collect( + db, + ty, + &mut enum_types, + &mut alternatives, + &mut enum_position, + &mut active_types, + )?; + let enum_position = enum_position?; + let enum_type = enum_types + .into_iter() + .fold(UnionBuilder::new(db), UnionBuilder::add) + .build(); + alternatives.insert(enum_position, enum_type); + + Some(Self { + enum_type, + alternatives, + }) + } + + fn has_other_values(&self) -> bool { + self.alternatives.len() > 1 + } +} + /// One or more enum-class domains represented by an operand. struct EnumDomainSet<'db> { domains: Vec>, From 8214287de811422aec2b591c866e34b8c12f61bc Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:40:31 -0400 Subject: [PATCH 077/390] Document removed default rules (#27212) Summary -- Fixes https://github.com/astral-sh/ruff/issues/27199. I'll also add a variation of this to the release notes and blog post. --- BREAKING_CHANGES.md | 5 ++++- CHANGELOG.md | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 8d73f590af..e21770e927 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -6,7 +6,10 @@ Ruff now enables a much larger set of rules by default (413, up from 59). See the blog post for more details and the new [Default Rules](https://docs.astral.sh/ruff/default-rules/) page for a - full listing of the enabled rules. + full listing of the enabled rules. Note that this is primarily an expansion, but 18 of the more + opinionated pycodestyle (`E`) and pyflakes (`F`) rules have been removed from the default set: + `E401`, `E402`, `E701`, `E702`, `E703`, `E711`, `E712`, `E713`, `E714`, `E721`, `E731`, `E741`, + `E742`, `E743`, `F403`, `F405`, `F406`, and `F722`. - **Python code block formatting in Markdown files** diff --git a/CHANGELOG.md b/CHANGELOG.md index dace580a43..cf61cd779d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,10 @@ guide and overview of the changes! - Ruff now enables a much larger set of rules by default (413, up from 59). See the blog post for more details and the new [Default Rules](https://docs.astral.sh/ruff/default-rules/) page for a - full listing of the enabled rules. + full listing of the enabled rules. Note that this is primarily an expansion, but 18 of the more + opinionated pycodestyle (`E`) and pyflakes (`F`) rules have been removed from the default set: + `E401`, `E402`, `E701`, `E702`, `E703`, `E711`, `E712`, `E713`, `E714`, `E721`, `E731`, `E741`, + `E742`, `E743`, `F403`, `F405`, `F406`, and `F722`. - Ruff can now format Python code blocks in Markdown files and will do this by default. See the [documentation](https://docs.astral.sh/ruff/formatter/#markdown-code-formatting) for more details. From 904feab5a36c21601f765cdd416476fb0c7eeca9 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Mon, 27 Jul 2026 10:49:46 -0400 Subject: [PATCH 078/390] [ty] Store typevar instances in `InferableTypeVars` (#27113) This is a pure refactoring PR in support of the [scoped quantifier](https://gist.github.com/dcreager/679132607be4c7cfb08ddc8ad1076982) work. For the new existential atoms, we will need to calculate the valid specializations of the quantified typevars, so that we can include that in the quantified domain. (This needs to be tracked separately so that we handle it correctly in both existential and universal quantifications.) That requires having a `BoundTypeVarInstance`, rather than a `BoundTypeVarIdentity`. This PR updates our `InferableTypeVars` type to store instances. (We actually have to store both, so that we can continue to deduplicate based on identity.) While we're here we also rename it to `TypeVarSet`, since we're using it for more than just inferable sets now. This should be a pure refactoring PR with no behavioral changes. --- crates/ty_python_semantic/src/types.rs | 8 +- .../ty_python_semantic/src/types/call/bind.rs | 76 ++---- crates/ty_python_semantic/src/types/class.rs | 13 +- .../src/types/constraints.rs | 58 ++--- .../ty_python_semantic/src/types/generics.rs | 137 +++------- .../src/types/infer/builder.rs | 9 +- .../src/types/infer/builder/subscript.rs | 12 +- .../ty_python_semantic/src/types/instance.rs | 5 +- .../ty_python_semantic/src/types/relation.rs | 50 ++-- .../src/types/signatures.rs | 10 +- .../ty_python_semantic/src/types/typevar.rs | 244 +++++++++++++++++- 11 files changed, 378 insertions(+), 244 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index b5116ca786..e8c2fe0b17 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -75,9 +75,7 @@ use crate::types::function::{ FunctionType, KnownFunction, }; pub(crate) use crate::types::generics::GenericContext; -use crate::types::generics::{ - ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, -}; +use crate::types::generics::{ApplySpecialization, Specialization, bind_typevar}; use crate::types::infer::InferenceFlags; use crate::types::known_instance::{ InternedConstraintSet, InternedType, SentinelInstance, UnionTypeInstance, @@ -93,11 +91,11 @@ use crate::types::tuple::TupleSpec; pub use crate::types::type_alias::TypeAliasType; pub use crate::types::type_form::TypeFormType; pub(crate) use crate::types::typed_dict::TypedDictType; -use crate::types::typevar::TypeVarInstance; pub use crate::types::typevar::{ BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, ParamSpecAttrKind, TypeVarBoundOrConstraints, TypeVarKind, TypeVarNonce, }; +use crate::types::typevar::{TypeVarInstance, TypeVarSet}; pub use crate::types::variance::TypeVarVariance; use crate::types::variance::VarianceInferable; use crate::types::visitor::any_over_type; @@ -2128,7 +2126,7 @@ impl<'db> Type<'db> { self, db: &'db dyn Db, target: Type<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> Type<'db> { let constraints = ConstraintSetBuilder::new(); self.filter_union(db, |elem| { diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index e3cb203cfb..76bba05579 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -49,8 +49,7 @@ use crate::types::function::{ OverloadLiteral, }; use crate::types::generics::{ - GenericContext, InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError, - TypeVarInference, + GenericContext, Specialization, SpecializationBuilder, SpecializationError, TypeVarInference, }; use crate::types::infer::original_class_type; use crate::types::known_instance::{FieldInstance, InternedConstraintSetSolution}; @@ -60,7 +59,7 @@ use crate::types::signatures::{ }; use crate::types::tuple::{TupleLength, TupleSpec, TupleType, VariableSegment}; use crate::types::typed_dict::{TypedDictOpenness, extract_unpacked_typed_dict_from_value_type}; -use crate::types::typevar::{BoundTypeVarIdentity, TypeVarNonceGenerator}; +use crate::types::typevar::{BoundTypeVarIdentity, TypeVarNonceGenerator, TypeVarSet}; use crate::types::visitor::{ TypeCollector, TypeKind, TypeVisitor, any_over_type, walk_non_atomic_type, walk_type_with_recursion_guard, @@ -212,16 +211,13 @@ fn freshen_generic_contexts_in_type<'db>( fn inferable_typevars_from_tuple<'db>( db: &'db dyn Db, instance: &NominalInstanceType<'db>, -) -> Option> { - let typevars: Option> = instance +) -> Option> { + let typevars: Option> = instance .tuple_spec(db)? .fixed_elements() - .map(|ty| { - ty.as_typevar() - .map(|bound_typevar| bound_typevar.identity(db)) - }) + .map(|ty| ty.as_typevar()) .collect(); - typevars.map(|typevars| InferableTypeVars::from_typevars(db, typevars)) + typevars.map(|typevars| TypeVarSet::from_typevars(db, typevars)) } /// Priority levels for call errors in intersection types. @@ -2058,12 +2054,7 @@ impl<'db> Bindings<'db> { let ty_b = ty_b.project_type_form(db); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - ty_a.when_subtype_of( - db, - ty_b, - constraints, - InferableTypeVars::None, - ) + ty_a.when_subtype_of(db, ty_b, constraints, TypeVarSet::None) }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -2078,12 +2069,7 @@ impl<'db> Bindings<'db> { let ty_b = ty_b.project_type_form(db); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - ty_a.when_assignable_to( - db, - ty_b, - constraints, - InferableTypeVars::None, - ) + ty_a.when_assignable_to(db, ty_b, constraints, TypeVarSet::None) }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -2113,12 +2099,7 @@ impl<'db> Bindings<'db> { let ty_b = ty_b.project_type_form(db); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - ty_a.when_disjoint_from( - db, - ty_b, - constraints, - InferableTypeVars::None, - ) + ty_a.when_disjoint_from(db, ty_b, constraints, TypeVarSet::None) }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -2779,7 +2760,7 @@ impl<'db> Bindings<'db> { ty_b, constraints.load(db, tracked.constraints(db)), constraints, - InferableTypeVars::None, + TypeVarSet::None, ) }); let tracked = InternedConstraintSet::new(db, result); @@ -2846,14 +2827,14 @@ impl<'db> Bindings<'db> { let extract_inferable = |instance: &NominalInstanceType<'db>| { if instance.has_known_class(db, KnownClass::NoneType) { // Caller explicitly passed None, so no typevars are inferable. - return Some(InferableTypeVars::None); + return Some(TypeVarSet::None); } inferable_typevars_from_tuple(db, instance) }; let inferable = match overload.parameter_types() { // Caller did not provide argument, so no typevars are inferable. - [None] => InferableTypeVars::None, + [None] => TypeVarSet::None, [Some(ty)] => { let Type::NominalInstance(instance) = ty.project_type_form(db) else { @@ -5056,7 +5037,7 @@ struct ArgumentTypeChecker<'a, 'db> { return_ty: Type<'db>, errors: &'a mut Vec>, - inferable_typevars: InferableTypeVars<'db>, + inferable_typevars: TypeVarSet<'db>, inference: Option>, /// Argument indices for which specialization inference has already produced a sufficiently @@ -5083,7 +5064,7 @@ fn validate_keyword_unpack_key_type<'db>( db: &'db dyn Db, constraints: &ConstraintSetBuilder<'db>, argument_type: Type<'db>, - inferable_typevars: InferableTypeVars<'db>, + inferable_typevars: TypeVarSet<'db>, ) -> KeywordUnpackKeyTypeCheck<'db> { if matches!(argument_type, Type::TypedDict(_)) || argument_type.as_paramspec_typevar(db).is_some() @@ -5134,7 +5115,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { call_expression_tcx, return_ty, errors, - inferable_typevars: InferableTypeVars::None, + inferable_typevars: TypeVarSet::None, inference: None, constraint_set_errors: vec![false; arguments.len()], } @@ -6013,13 +5994,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } } - fn finish( - self, - ) -> ( - InferableTypeVars<'db>, - Option>, - Type<'db>, - ) { + fn finish(self) -> (TypeVarSet<'db>, Option>, Type<'db>) { for (parameter_ty, builder) in self .parameter_tys .iter_mut() @@ -6193,10 +6168,10 @@ struct ParamSpecArgumentContext<'a, 'call, 'db> { fn inferable_typevar_occurrences<'db>( db: &'db dyn Db, ty: Type<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> usize { struct InferableTypeVarVisitor<'db> { - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, count: Cell, stack: RefCell; 8]>>, } @@ -6274,7 +6249,7 @@ pub(crate) struct Binding<'db> { constructor_context: Option>, /// The inferable typevars in this signature. - inferable_typevars: InferableTypeVars<'db>, + inferable_typevars: TypeVarSet<'db>, /// The type-variable inference result for this binding, if the callable is generic. inference: Option>, @@ -6337,7 +6312,7 @@ impl<'db> Binding<'db> { signature_type, return_ty, constructor_context: None, - inferable_typevars: InferableTypeVars::None, + inferable_typevars: TypeVarSet::None, inference: None, argument_matches: Box::from([]), variadic_argument_matched_to_variadic_parameter: false, @@ -6811,12 +6786,7 @@ impl<'db> Binding<'db> { let argument_type = argument_types.get_default().unwrap_or(Type::unknown()); if let KeywordUnpackKeyTypeCheck::Invalid(provided_ty) = - validate_keyword_unpack_key_type( - db, - constraints, - argument_type, - InferableTypeVars::None, - ) + validate_keyword_unpack_key_type(db, constraints, argument_type, TypeVarSet::None) { self.errors.push(BindingError::InvalidKeyType { argument_index: adjusted_argument_index, @@ -7100,7 +7070,7 @@ impl<'db> Binding<'db> { /// Resets the state of this binding to its initial state. fn reset(&mut self, db: &'db dyn Db) { self.return_ty = self.initial_return_type(db); - self.inferable_typevars = InferableTypeVars::None; + self.inferable_typevars = TypeVarSet::None; self.inference = None; self.argument_matches = Box::from([]); self.parameter_tys = Box::from([]); @@ -7111,7 +7081,7 @@ impl<'db> Binding<'db> { #[derive(Clone, Debug)] struct BindingSnapshot<'db> { return_ty: Type<'db>, - inferable_typevars: InferableTypeVars<'db>, + inferable_typevars: TypeVarSet<'db>, inference: Option>, argument_matches: Box<[MatchedArgument<'db>]>, parameter_tys: Box<[Option>]>, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index b1d0e7b2b2..23b524b8c6 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -26,9 +26,7 @@ use crate::types::constraints::{ }; use crate::types::enums::enum_metadata; use crate::types::function::{AbstractMethodKind, DataclassTransformerParams}; -use crate::types::generics::{ - GenericContext, InferableTypeVars, Specialization, walk_specialization, -}; +use crate::types::generics::{GenericContext, Specialization, walk_specialization}; use crate::types::known_instance::DeprecatedInstance; use crate::types::member::Member; use crate::types::relation::{ @@ -38,6 +36,7 @@ use crate::types::signatures::{ CallableSignature, Parameter, Parameters, Signature, SignatureRelationVisitor, }; use crate::types::tuple::TupleSpec; +use crate::types::typevar::TypeVarSet; use crate::types::{ ApplyTypeMappingVisitor, CallableType, CallableTypes, DataclassParams, FindLegacyTypeVarsVisitor, IntersectionType, TypeContext, TypeMapping, TypedDictModule, @@ -1381,7 +1380,7 @@ impl<'db> ClassType<'db> { let materialization_visitor = ApplyTypeMappingVisitor::default(); let checker = TypeRelationChecker::subtyping( &constraints, - InferableTypeVars::None, + TypeVarSet::None, &relation_visitor, &disjointness_visitor, &signature_relation_visitor, @@ -1425,7 +1424,7 @@ impl<'db> ClassType<'db> { constraints: &ConstraintSetBuilder<'db>, ) -> bool { self.could_exist_in_mro_of_impl(db, other, |this, other| { - this.is_disjoint_from(db, other, constraints, InferableTypeVars::None) + this.is_disjoint_from(db, other, constraints, TypeVarSet::None) .is_always_satisfied(db) }) } @@ -1512,11 +1511,11 @@ impl<'db> ClassType<'db> { other, |this, other| this.could_exist_in_mro_of(db, other, constraints), |this, other| { - this.is_disjoint_from(db, other, constraints, InferableTypeVars::None) + this.is_disjoint_from(db, other, constraints, TypeVarSet::None) .is_always_satisfied(db) }, |this, other| { - this.when_disjoint_from(db, other, constraints, InferableTypeVars::None) + this.when_disjoint_from(db, other, constraints, TypeVarSet::None) .is_always_satisfied(db) }, ) diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 0b640281fa..514297ecb8 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -105,8 +105,7 @@ use ty_python_core::rank::RankBitBox; use ty_static::EnvVars; use crate::types::class::GenericAlias; -use crate::types::generics::InferableTypeVars; -use crate::types::typevar::{BoundTypeVarIdentity, walk_bound_type_var_type}; +use crate::types::typevar::{BoundTypeVarIdentity, TypeVarSet, walk_bound_type_var_type}; use crate::types::variance::VarianceInferable; use crate::types::visitor::{ TypeCollector, TypeKind, TypeVisitor, any_over_type, walk_non_atomic_type, @@ -495,7 +494,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { &self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> bool { self.verify_builder(builder); self.node.satisfied_by_all_typevars(db, builder, inferable) @@ -615,7 +614,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, - to_remove: InferableTypeVars<'db>, + to_remove: TypeVarSet<'db>, ) -> Self { self.verify_builder(builder); Self::from_node(builder, self.node.exists(db, builder, to_remove)) @@ -742,10 +741,10 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, - to_remove: InferableTypeVars<'db>, + to_remove: TypeVarSet<'db>, ) -> Self { self.verify_builder(builder); - if to_remove == InferableTypeVars::None { + if to_remove == TypeVarSet::None { return self; } @@ -773,7 +772,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> Solutions<'db> { self.solutions_with(db, builder, inferable, |_variance, path_bound| { PathBounds::default_solve(db, builder, path_bound) @@ -784,7 +783,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, choose: impl FnMut(TypeVarVariance, &PathBound<'db>) -> Result>, ()>, ) -> Solutions<'db> { self.verify_builder(builder); @@ -887,7 +886,7 @@ struct ConstraintSetStorage<'db> { negate_cache: FxHashMap, or_cache: FxHashMap<(NodeId, NodeId, usize), NodeId>, and_cache: FxHashMap<(NodeId, NodeId, usize), NodeId>, - exists_cache: FxHashMap<(NodeId, InferableTypeVars<'db>), NodeId>, + exists_cache: FxHashMap<(NodeId, TypeVarSet<'db>), NodeId>, restrict_one_cache: FxHashMap<(NodeId, ConstraintAssignment), (NodeId, bool)>, simplify_cache: FxHashMap, @@ -2469,7 +2468,7 @@ impl NodeId { self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, choose: impl FnMut(TypeVarVariance, &PathBound<'db>) -> Result>, ()>, ) -> Solutions<'db> { let path_bounds = PathBounds::compute(db, builder, self, inferable); @@ -2839,7 +2838,7 @@ impl NodeId { self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> bool { match self.node() { Node::AlwaysTrue => return true, @@ -2918,9 +2917,9 @@ impl NodeId { self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, - bound_typevars: InferableTypeVars<'db>, + bound_typevars: TypeVarSet<'db>, ) -> Self { - if bound_typevars == InferableTypeVars::None { + if bound_typevars == TypeVarSet::None { return self; } @@ -2946,7 +2945,7 @@ impl NodeId { self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> Self { match self.node() { Node::AlwaysTrue => ALWAYS_TRUE, @@ -3520,7 +3519,7 @@ impl<'db> Type<'db> { self, db: &'db dyn Db, target: Type<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> &'db PathBounds<'db> { #[salsa::tracked( returns(ref), @@ -3531,7 +3530,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, source: Type<'db>, target: Type<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> PathBounds<'db> { let when = source.when_constraint_set_assignable_to_owned(db, target); when.query(|builder, when| PathBounds::compute(db, builder, when.node, inferable)) @@ -3570,7 +3569,7 @@ impl<'db> PathBounds<'db> { db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, node: NodeId, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> Self { #[derive(Default)] struct CollectVisitor { @@ -3700,7 +3699,7 @@ impl<'db> PathBounds<'db> { db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, node: NodeId, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> Option { match node.node() { Node::AlwaysTrue => return Some(PathBounds::Unconstrained), @@ -4203,7 +4202,7 @@ impl InteriorNode { self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, - bound_typevars: InferableTypeVars<'db>, + bound_typevars: TypeVarSet<'db>, ) -> NodeId { let mentions_typevar = |ty: Type<'db>| match ty { Type::TypeVar(typevar) => typevar.is_inferable(db, bound_typevars), @@ -4235,7 +4234,7 @@ impl InteriorNode { self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> NodeId { let is_bare_inferable_typevar = |ty: Type<'db>| { ty.as_typevar() @@ -7718,8 +7717,7 @@ mod tests { &builder, || ConstraintSet::constrain_typevar_lower_bound(&db, &builder, t, str), ); - let inferable = - InferableTypeVars::from_typevars(&db, std::iter::once(t.identity(&db)).collect()); + let inferable = TypeVarSet::from_typevars(&db, [t]); let (single_sequents, pair_sequents) = { let storage = builder.storage.borrow(); ( @@ -7753,10 +7751,7 @@ mod tests { ConstraintSet::constrain_typevar(&db, &builder, t, int, int).and(&db, &builder, || { ConstraintSet::constrain_typevar(&db, &builder, u, int, int) }); - let inferable = InferableTypeVars::from_typevars( - &db, - [t.identity(&db), u.identity(&db)].into_iter().collect(), - ); + let inferable = TypeVarSet::from_typevars(&db, [t, u]); let (single_sequents, pair_sequents) = { let storage = builder.storage.borrow(); ( @@ -7795,8 +7790,7 @@ mod tests { ConstraintSet::constrain_typevar(&db, &builder, t, int, int).and(&db, &builder, || { ConstraintSet::constrain_typevar(&db, &builder, t, str, str) }); - let inferable = - InferableTypeVars::from_typevars(&db, std::iter::once(t.identity(&db)).collect()); + let inferable = TypeVarSet::from_typevars(&db, [t]); let (single_sequents, pair_sequents) = { let storage = builder.storage.borrow(); ( @@ -8090,13 +8084,7 @@ mod tests { build_bdd: impl Fn(&ConstraintSetBuilder<'db>) -> NodeId, expected: impl IntoIterator, ) { - let inferable = InferableTypeVars::from_typevars( - db, - typevars - .iter() - .map(|typevar| typevar.identity(db)) - .collect(), - ); + let inferable = TypeVarSet::from_typevars(db, typevars.iter().copied()); let mut signatures = FxIndexSet::default(); for constraint_order in (0..atoms.len()).permutations(atoms.len()) { diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 53c3074f36..da53309f0d 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1,9 +1,8 @@ use std::borrow::Cow; use std::cell::{Cell, RefCell}; use std::collections::hash_map::Entry; -use std::fmt::Display; -use itertools::{Either, Itertools}; +use itertools::Itertools; use ruff_python_ast as ast; use rustc_hash::{FxHashMap, FxHashSet}; @@ -25,7 +24,7 @@ use crate::types::tuple::{ }; use crate::types::type_alias::{walk_manual_pep_695_type_alias, walk_pep_695_type_alias}; use crate::types::typevar::{ - BoundTypeVarIdentity, TypeVarIdentity, TypeVarInstance, walk_type_var_bounds, + BoundTypeVarIdentity, TypeVarIdentity, TypeVarInstance, TypeVarSet, walk_type_var_bounds, }; use crate::types::visitor::{ TypeCollector, TypeVisitor, any_over_type, walk_type_with_recursion_guard, @@ -242,92 +241,6 @@ pub(crate) fn typing_self<'db>( ) } -/// The set of bound typevar occurrences that can be solved by the current inference context. -/// -/// Membership is keyed by [`BoundTypeVarIdentity`], including any freshness nonce. This lets a -/// fresh generic-callable occurrence be inferable without making the surrounding source-level -/// typevar inferable. -#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, get_size2::GetSize, salsa::SalsaValue)] -pub(crate) enum InferableTypeVars<'db> { - None, - Some(InferableTypeVarsInner<'db>), -} - -impl<'db> InferableTypeVars<'db> { - pub(crate) fn from_typevars( - db: &'db dyn Db, - mut typevars: FxOrderSet>, - ) -> Self { - if typevars.is_empty() { - return InferableTypeVars::None; - } - - typevars.shrink_to_fit(); - Self::Some(InferableTypeVarsInner::new_internal(db, typevars)) - } -} - -#[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] -pub(crate) struct InferableTypeVarsInner<'db> { - #[returns(ref)] - inferable: FxOrderSet>, -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for InferableTypeVarsInner<'_> {} - -impl<'db> BoundTypeVarIdentity<'db> { - pub(crate) fn is_inferable(self, db: &'db dyn Db, inferable: InferableTypeVars<'db>) -> bool { - match inferable { - InferableTypeVars::None => false, - InferableTypeVars::Some(inner) => inner.inferable(db).contains(&self), - } - } -} - -impl<'db> BoundTypeVarInstance<'db> { - pub(crate) fn is_inferable(self, db: &'db dyn Db, inferable: InferableTypeVars<'db>) -> bool { - self.identity(db).is_inferable(db, inferable) - } -} - -#[salsa::tracked] -impl<'db> InferableTypeVars<'db> { - #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn merge(self, db: &'db dyn Db, other: Self) -> Self { - match (self, other) { - (InferableTypeVars::None, other) | (other, InferableTypeVars::None) => other, - (InferableTypeVars::Some(self_inner), InferableTypeVars::Some(other_inner)) => { - let merged = self_inner.inferable(db) | other_inner.inferable(db); - Self::Some(InferableTypeVarsInner::new_internal(db, merged)) - } - } - } - - // This is not an IntoIterator implementation because I have no desire to try to name the - // iterator type. - pub(crate) fn iter( - self, - db: &'db dyn Db, - ) -> impl Iterator> + 'db { - match self { - InferableTypeVars::None => Either::Left(std::iter::empty()), - InferableTypeVars::Some(inner) => Either::Right(inner.inferable(db).iter().copied()), - } - } - - // Keep this around for debugging purposes - #[expect(dead_code)] - pub(crate) fn display(&self, db: &'db dyn Db) -> impl Display { - format!( - "[{}]", - self.iter(db) - .map(|identity| identity.display(db)) - .format(", ") - ) - } -} - /// A list of formal type variables for a generic function, class, type alias, or fresh callable /// occurrence. /// @@ -470,10 +383,10 @@ impl<'db> GenericContext<'db> { /// In this example, `method`'s generic context binds `Self` and `T`, but its inferable set /// also includes `A@C`. This is needed because at each call site, we need to infer the /// specialized class instance type whose method is being invoked. - pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> InferableTypeVars<'db> { + pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> TypeVarSet<'db> { #[derive(Default)] struct CollectTypeVars<'db> { - typevars: RefCell>>, + typevars: RefCell, BoundTypeVarInstance<'db>>>, recursion_guard: TypeCollector<'db>, } @@ -489,7 +402,8 @@ impl<'db> GenericContext<'db> { ) { self.typevars .borrow_mut() - .insert(bound_typevar.identity(db)); + .entry(bound_typevar.identity(db)) + .or_insert(bound_typevar); let typevar = bound_typevar.typevar(db); if let Some(bound_or_constraints) = typevar.bound_or_constraints(db) { walk_type_var_bounds(db, bound_or_constraints, self); @@ -503,18 +417,18 @@ impl<'db> GenericContext<'db> { #[salsa::tracked( returns(copy), - cycle_initial=|_, _, _| InferableTypeVars::None, + cycle_initial=|_, _, _| TypeVarSet::None, heap_size=ruff_memory_usage::heap_size, )] fn inferable_typevars_inner<'db>( db: &'db dyn Db, generic_context: GenericContext<'db>, - ) -> InferableTypeVars<'db> { + ) -> TypeVarSet<'db> { let visitor = CollectTypeVars::default(); for bound_typevar in generic_context.variables(db) { visitor.visit_bound_type_var_type(db, bound_typevar); } - InferableTypeVars::from_typevars(db, visitor.typevars.into_inner()) + TypeVarSet::from_typevars(db, visitor.typevars.into_inner().into_values()) } inferable_typevars_inner(db, self) @@ -1563,7 +1477,7 @@ impl<'db> Specialization<'db> { db: &'db dyn Db, other: Self, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); @@ -2041,7 +1955,7 @@ impl<'db> Type<'db> { pub(crate) struct SpecializationBuilder<'db, 'c> { db: &'db dyn Db, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, pending: ConstraintSet<'db, 'c>, types: FxHashMap, UnionAccumulator<'db>>, paramspec_seen: FxHashSet>, @@ -2111,7 +2025,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { pub(crate) fn new( db: &'db dyn Db, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> Self { Self { db, @@ -3617,3 +3531,30 @@ impl<'db> SpecializationError<'db> { } } } + +#[cfg(test)] +mod tests { + use super::*; + + use ruff_python_ast::name::Name; + + use crate::db::tests::setup_db; + + #[test] + fn generic_context_inferable_typevars_retain_instances_from_bounds() { + let db = setup_db(); + let u = + BoundTypeVarInstance::synthetic(&db, Name::new_static("U"), TypeVarVariance::Invariant); + let t = + BoundTypeVarInstance::synthetic(&db, Name::new_static("T"), TypeVarVariance::Invariant) + .map_bound_or_constraints(&db, |_| { + Some(TypeVarBoundOrConstraints::UpperBound(Type::TypeVar(u))) + }); + let context = GenericContext::from_typevar_instances(&db, [t]); + + let inferable = context.inferable_typevars(&db); + assert_eq!(inferable.iter(&db).collect::>(), [t, u]); + assert!(t.is_inferable(&db, inferable)); + assert!(u.is_inferable(&db, inferable)); + } +} diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6cccb9f29b..04b64581f3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -81,8 +81,7 @@ use crate::types::function::{ same_module_uncached_raw_signature, }; use crate::types::generics::{ - GenericContext, InferableTypeVars, Specialization, SpecializationBuilder, bind_typevar, - enclosing_binding_contexts, + GenericContext, Specialization, SpecializationBuilder, bind_typevar, enclosing_binding_contexts, }; use crate::types::infer::builder::named_tuple::NamedTupleKind; use crate::types::infer::builder::paramspec_validation::validate_paramspec_components; @@ -105,7 +104,7 @@ use crate::types::tuple::{Tuple, TupleLength, TupleSpecBuilder, TupleType, Varia use crate::types::type_alias::{ManualPEP695TypeAliasType, PEP695TypeAliasType}; use crate::types::typed_dict::{TypedDictAssignmentKind, TypedDictKeyAssignment}; use crate::types::typevar::{ - BoundTypeVarIdentity, TypeVarConstraints, TypeVarIdentity, TypeVarInstance, + BoundTypeVarIdentity, TypeVarConstraints, TypeVarIdentity, TypeVarInstance, TypeVarSet, }; use crate::types::unpacker::UnpackResult; use crate::types::{ @@ -5277,7 +5276,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .signature .generic_context .map(|generic_context| generic_context.inferable_typevars(db)) - .unwrap_or(InferableTypeVars::None); + .unwrap_or(TypeVarSet::None); !overload .return_ty @@ -6569,7 +6568,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .try_to_class_literal(self.db()) .and_then(|class| class.generic_context(self.db())) .map(|generic_context| generic_context.inferable_typevars(self.db())) - .unwrap_or(InferableTypeVars::None); + .unwrap_or(TypeVarSet::None); annotation.filter_disjoint_elements( self.db(), Type::homogeneous_tuple(self.db(), Type::unknown()), diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index 140bd52a74..7baadc86f7 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -16,7 +16,7 @@ use crate::types::diagnostic::{ TypedDictDeleteErrorKind, report_cannot_delete_typed_dict_key, report_invalid_arguments_to_annotated, report_not_subscriptable, }; -use crate::types::generics::{GenericContext, InferableTypeVars, bind_typevar}; +use crate::types::generics::{GenericContext, bind_typevar}; use crate::types::infer::builder::annotation_expression::PEP613Policy; use crate::types::infer::builder::{ArgExpr, ArgumentsIter, MultiInferenceGuard}; use crate::types::infer::{InferenceFlags, TypeExpressionFlags}; @@ -24,6 +24,7 @@ use crate::types::special_form::AliasSpec; use crate::types::subscript::{LegacyGenericOrigin, SubscriptError, SubscriptErrorKind}; use crate::types::tuple::{Tuple, TupleSpecBuilder, TupleType, VariableSegment}; use crate::types::typed_dict::{TypedDictAssignmentKind, TypedDictKeyAssignment}; +use crate::types::typevar::TypeVarSet; use crate::types::{ BoundTypeVarInstance, CallArguments, CallDunderError, CallableBinding, CycleDetector, DynamicType, InternedType, KnownClass, KnownInstanceType, LintDiagnosticGuard, @@ -972,12 +973,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match typevar.typevar(db).bound_or_constraints(db) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { if provided_type - .when_assignable_to( - db, - bound, - &constraints, - InferableTypeVars::None, - ) + .when_assignable_to(db, bound, &constraints, TypeVarSet::None) .is_never_satisfied(db) { if let Some(builder) = self @@ -1012,7 +1008,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { db, typevar_constraints.as_type(db), &constraints, - InferableTypeVars::None, + TypeVarSet::None, ) .is_never_satisfied(db) { diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 02bd459023..af6363d1b3 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -17,7 +17,7 @@ use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, }; use crate::types::enums::is_single_member_enum; -use crate::types::generics::{InferableTypeVars, walk_specialization}; +use crate::types::generics::walk_specialization; use crate::types::protocol_class::{ ProtocolClass, has_all_protocol_members_defined, walk_protocol_instance_member, walk_protocol_interface, @@ -28,6 +28,7 @@ use crate::types::relation::{ }; use crate::types::signatures::SignatureRelationVisitor; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; +use crate::types::typevar::TypeVarSet; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ ApplyTypeMappingVisitor, CallableType, ClassBase, ClassLiteral, ErrorContext, @@ -1049,7 +1050,7 @@ impl<'db> ProtocolInstanceType<'db> { let materialization_visitor = ApplyTypeMappingVisitor::default(); let checker = TypeRelationChecker::subtyping( &constraints, - InferableTypeVars::None, + TypeVarSet::None, &relation_visitor, &disjointness_visitor, &signature_relation_visitor, diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 828ed75de3..a20b9b411c 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -25,7 +25,7 @@ use crate::{ Db, types::{ ErrorContext, ErrorContextTree, Type, TypePair, constraints::ConstraintSet, - generics::InferableTypeVars, + typevar::TypeVarSet, }, }; @@ -314,7 +314,7 @@ impl<'db> Type<'db> { /// See [`TypeRelation::Subtyping`] for more details. pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_subtype_of(db, target, &constraints, InferableTypeVars::None) + self.when_subtype_of(db, target, &constraints, TypeVarSet::None) .is_always_satisfied(db) } @@ -323,7 +323,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> ConstraintSet<'db, 'c> { self.has_relation_to(db, target, constraints, inferable, TypeRelation::Subtyping) } @@ -338,7 +338,7 @@ impl<'db> Type<'db> { target: Type<'db>, assuming: ConstraintSet<'db, 'c>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); @@ -364,7 +364,7 @@ impl<'db> Type<'db> { /// See `TypeRelation::Assignability` for more details. pub fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_assignable_to(db, target, &constraints, InferableTypeVars::None) + self.when_assignable_to(db, target, &constraints, TypeVarSet::None) .is_always_satisfied(db) } @@ -383,7 +383,7 @@ impl<'db> Type<'db> { let builder = ConstraintSetBuilder::new(); let checker = TypeRelationChecker { constraints: &builder, - inferable: InferableTypeVars::None, + inferable: TypeVarSet::None, relation: TypeRelation::Assignability, typevar_evaluation: TypeVarEvaluation::Eager, context_tree: Some(ErrorContextTree::new()), @@ -416,7 +416,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> ConstraintSet<'db, 'c> { self.has_relation_to( db, @@ -483,7 +483,7 @@ impl<'db> Type<'db> { db, target, constraints, - InferableTypeVars::None, + TypeVarSet::None, TypeRelation::Assignability, TypeVarEvaluation::Lazy, ) @@ -510,7 +510,7 @@ impl<'db> Type<'db> { db, target, constraints, - InferableTypeVars::None, + TypeVarSet::None, TypeRelation::Assignability, TypeVarEvaluation::Lazy, ) @@ -526,7 +526,7 @@ impl<'db> Type<'db> { db, target, constraints, - InferableTypeVars::None, + TypeVarSet::None, TypeRelation::Subtyping, TypeVarEvaluation::Lazy, ) @@ -544,7 +544,7 @@ impl<'db> Type<'db> { db, types.second(db), &ConstraintSetBuilder::new(), - InferableTypeVars::None, + TypeVarSet::None, TypeRelation::Redundancy { pure: false }, ) .is_always_satisfied(db) @@ -562,7 +562,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, relation: TypeRelation, ) -> ConstraintSet<'db, 'c> { self.has_relation_to_with_typevar_evaluation( @@ -580,7 +580,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, relation: TypeRelation, typevar_evaluation: TypeVarEvaluation, ) -> ConstraintSet<'db, 'c> { @@ -688,7 +688,7 @@ impl<'db> Type<'db> { /// `false` answers in some cases. pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_disjoint_from(db, other, &constraints, InferableTypeVars::None) + self.when_disjoint_from(db, other, &constraints, TypeVarSet::None) .is_always_satisfied(db) } @@ -697,7 +697,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, other: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); @@ -771,7 +771,7 @@ impl<'db, 'c> IsDisjointVisitor<'db, 'c> { #[derive(Clone)] pub(super) struct TypeRelationChecker<'a, 'c, 'db> { pub(super) constraints: &'c ConstraintSetBuilder<'db>, - pub(super) inferable: InferableTypeVars<'db>, + pub(super) inferable: TypeVarSet<'db>, pub(super) relation: TypeRelation, pub(super) typevar_evaluation: TypeVarEvaluation, context_tree: Option>, @@ -792,7 +792,7 @@ pub(super) struct TypeRelationChecker<'a, 'c, 'db> { impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { pub(super) fn subtyping( constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, @@ -821,7 +821,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { ) -> Self { Self { constraints, - inferable: InferableTypeVars::None, + inferable: TypeVarSet::None, relation: TypeRelation::Assignability, typevar_evaluation: TypeVarEvaluation::Lazy, context_tree: None, @@ -842,7 +842,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { ) -> Self { Self { constraints, - inferable: InferableTypeVars::None, + inferable: TypeVarSet::None, relation: TypeRelation::Assignability, typevar_evaluation: TypeVarEvaluation::Lazy, context_tree: Some(ErrorContextTree::new()), @@ -863,7 +863,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { ) -> Self { Self { constraints, - inferable: InferableTypeVars::None, + inferable: TypeVarSet::None, relation: TypeRelation::Assignability, typevar_evaluation: TypeVarEvaluation::Eager, context_tree: Some(ErrorContextTree::new()), @@ -875,7 +875,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } } - pub(super) fn with_inferable_typevars(&self, inferable: InferableTypeVars<'db>) -> Self { + pub(super) fn with_inferable_typevars(&self, inferable: TypeVarSet<'db>) -> Self { Self { inferable, ..self.clone() @@ -891,7 +891,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { ) -> bool { Self::subtyping( self.constraints, - InferableTypeVars::None, + TypeVarSet::None, self.relation_visitor, self.disjointness_visitor, self.signature_relation_visitor, @@ -2468,7 +2468,7 @@ impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { constraints: self.constraints, context_tree: None, given: self.given, - inferable: InferableTypeVars::None, + inferable: TypeVarSet::None, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, signature_relation_visitor: self.signature_relation_visitor, @@ -2508,7 +2508,7 @@ impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { pub(super) struct DisjointnessChecker<'a, 'c, 'db> { pub(super) constraints: &'c ConstraintSetBuilder<'db>, - pub(super) inferable: InferableTypeVars<'db>, + pub(super) inferable: TypeVarSet<'db>, given: ConstraintSet<'db, 'c>, // N.B. these fields are private to reduce the risk of @@ -2526,7 +2526,7 @@ pub(super) struct DisjointnessChecker<'a, 'c, 'db> { impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { pub(super) fn new( constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'db>, + inferable: TypeVarSet<'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index cc93caac40..e9c110dddd 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -26,8 +26,8 @@ use crate::types::constraints::{ }; use crate::types::cyclic::ActiveRecursionDetector; use crate::types::generics::{ - ApplySpecialization, GenericContext, InferableTypeVars, Specialization, SpecializationBuilder, - TypeVarInference, walk_generic_context, + ApplySpecialization, GenericContext, Specialization, SpecializationBuilder, TypeVarInference, + walk_generic_context, }; use crate::types::infer::{TypeExpressionFlags, infer_deferred_types}; use crate::types::relation::{ @@ -35,7 +35,7 @@ use crate::types::relation::{ }; use crate::types::tuple::{Tuple, TupleType, VariableSegment}; use crate::types::typed_dict::extract_unpacked_typed_dict_keys_from_kwargs_annotation; -use crate::types::typevar::max_typevar_freshness_matching_generic_context; +use crate::types::typevar::{TypeVarSet, max_typevar_freshness_matching_generic_context}; use crate::types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, CallableType, ErrorContext, ErrorContextTree, FindLegacyTypeVarsVisitor, KnownClass, @@ -1575,10 +1575,10 @@ impl<'db> Signature<'db> { .any(|(_, parameter)| parameter.annotated_type().contains_self(db)) } - fn inferable_typevars(&self, db: &'db dyn Db) -> InferableTypeVars<'db> { + fn inferable_typevars(&self, db: &'db dyn Db) -> TypeVarSet<'db> { match self.generic_context { Some(generic_context) => generic_context.inferable_typevars(db), - None => InferableTypeVars::None, + None => TypeVarSet::None, } } diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index 44653c4d8e..b4ef76bf2e 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -1,13 +1,14 @@ use std::cell::{Cell, RefCell}; use std::rc::Rc; +use itertools::{Either, Itertools}; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; use rustc_hash::FxHashSet; use smallvec::SmallVec; use crate::{ - Db, TypeQualifiers, + Db, FxOrderMap, TypeQualifiers, place::{ DefinedPlace, Definedness, Place, PlaceAndQualifiers, Provenance, PublicTypePolicy, TypeOrigin, @@ -1515,6 +1516,110 @@ impl<'db> BoundTypeVarIdentity<'db> { } } +/// A set of bound typevar occurrences. +/// +/// Membership is keyed by [`BoundTypeVarIdentity`], including any freshness nonce, while the first +/// bound instance encountered for each identity is retained. This lets a fresh generic-callable +/// occurrence be inferable without making the surrounding source-level typevar inferable. +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, get_size2::GetSize, salsa::SalsaValue)] +pub(crate) enum TypeVarSet<'db> { + None, + Some(TypeVarSetInner<'db>), +} + +impl<'db> TypeVarSet<'db> { + pub(crate) fn from_typevars( + db: &'db dyn Db, + typevars: impl IntoIterator>, + ) -> Self { + let mut typevars = typevars.into_iter().peekable(); + if typevars.peek().is_none() { + return TypeVarSet::None; + } + + let mut set = FxOrderMap::default(); + for typevar in typevars { + set.entry(typevar.identity(db)).or_insert(typevar); + } + set.shrink_to_fit(); + Self::Some(TypeVarSetInner::new_internal(db, set)) + } +} + +#[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] +pub(crate) struct TypeVarSetInner<'db> { + #[returns(ref)] + typevars: FxOrderMap, BoundTypeVarInstance<'db>>, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for TypeVarSetInner<'_> {} + +impl<'db> BoundTypeVarIdentity<'db> { + pub(crate) fn is_inferable(self, db: &'db dyn Db, inferable: TypeVarSet<'db>) -> bool { + match inferable { + TypeVarSet::None => false, + TypeVarSet::Some(inner) => inner.typevars(db).contains_key(&self), + } + } +} + +impl<'db> BoundTypeVarInstance<'db> { + pub(crate) fn is_inferable(self, db: &'db dyn Db, inferable: TypeVarSet<'db>) -> bool { + self.identity(db).is_inferable(db, inferable) + } +} + +impl<'db> TypeVarSet<'db> { + pub(crate) fn merge(self, db: &'db dyn Db, other: Self) -> Self { + #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] + fn merge_inner<'db>( + db: &'db dyn Db, + self_inner: TypeVarSetInner<'db>, + other_inner: TypeVarSetInner<'db>, + ) -> TypeVarSet<'db> { + TypeVarSet::from_typevars( + db, + self_inner + .typevars(db) + .values() + .chain(other_inner.typevars(db).values()) + .copied(), + ) + } + + match (self, other) { + (TypeVarSet::None, other) | (other, TypeVarSet::None) => other, + (TypeVarSet::Some(self_inner), TypeVarSet::Some(other_inner)) => { + merge_inner(db, self_inner, other_inner) + } + } + } + + // This is not an IntoIterator implementation because I have no desire to try to name the + // iterator type. + pub(crate) fn iter( + self, + db: &'db dyn Db, + ) -> impl Iterator> + 'db { + match self { + TypeVarSet::None => Either::Left(std::iter::empty()), + TypeVarSet::Some(inner) => Either::Right(inner.typevars(db).values().copied()), + } + } + + // Keep this around for debugging purposes + #[cfg_attr(not(test), expect(dead_code))] + pub(crate) fn display(self, db: &'db dyn Db) -> String { + format!( + "[{}]", + self.iter(db) + .map(|typevar| typevar.identity(db).display(db)) + .format(", ") + ) + } +} + #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Some(Type::divergent(id)), @@ -1830,3 +1935,140 @@ impl<'db> super::cyclic::HasIdentity<'db> for TypeVarInstance<'db> { *self } } + +#[cfg(test)] +mod tests { + use super::*; + + use ruff_db::testing::assert_function_query_was_not_run_by_name; + + use crate::db::tests::setup_db; + + fn bound_typevar<'db>( + db: &'db dyn Db, + name: &'static str, + kind: TypeVarKind, + bound_or_constraints: Option>, + freshness: TypeVarNonce, + ) -> BoundTypeVarInstance<'db> { + let identity = TypeVarIdentity::new(db, Name::new_static(name), None, kind); + let typevar = TypeVarInstance::new( + db, + identity, + bound_or_constraints, + Some(TypeVarVariance::Invariant), + None, + ); + BoundTypeVarInstance::new(db, typevar, BindingContext::Synthetic, None, freshness) + } + + #[test] + fn typevar_set_empty_set_is_none() { + let db = setup_db(); + let typevar = + BoundTypeVarInstance::synthetic(&db, Name::new_static("T"), TypeVarVariance::Invariant); + let inferable = TypeVarSet::from_typevars(&db, []); + + assert_eq!(inferable, TypeVarSet::None); + assert_eq!(inferable.iter(&db).count(), 0); + assert!(!typevar.is_inferable(&db, inferable)); + assert!(!typevar.identity(&db).is_inferable(&db, inferable)); + } + + #[test] + fn typevar_set_keeps_first_instance_for_each_identity() { + let mut db = setup_db(); + db.clear_salsa_events(); + + // The synthetic lazy bound has no definition, so it is equivalent to the implicit + // `object` upper bound represented eagerly below. + let lazy = bound_typevar( + &db, + "T", + TypeVarKind::Pep695TypeVar, + Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound), + TypeVarNonce::NONE, + ); + let eager = bound_typevar( + &db, + "T", + TypeVarKind::Pep695TypeVar, + Some(TypeVarBoundOrConstraints::UpperBound(Type::object()).into()), + TypeVarNonce::NONE, + ); + let u = + BoundTypeVarInstance::synthetic(&db, Name::new_static("U"), TypeVarVariance::Invariant); + let v = + BoundTypeVarInstance::synthetic(&db, Name::new_static("V"), TypeVarVariance::Invariant); + + assert_ne!(lazy, eager); + assert_eq!(lazy.identity(&db), eager.identity(&db)); + + let left = TypeVarSet::from_typevars(&db, [lazy, u, eager]); + let right = TypeVarSet::from_typevars(&db, [eager, v, lazy]); + let merged = left.merge(&db, right); + + assert_eq!(left.iter(&db).collect::>(), [lazy, u]); + assert_eq!(right.iter(&db).collect::>(), [eager, v]); + assert_eq!(merged.iter(&db).collect::>(), [lazy, u, v]); + assert_eq!(merged, TypeVarSet::from_typevars(&db, [lazy, u, v])); + assert!(lazy.is_inferable(&db, merged)); + assert!(eager.is_inferable(&db, merged)); + assert_eq!(merged.display(&db), "[T, U, V]"); + + let events = db.take_salsa_events(); + assert_function_query_was_not_run_by_name(&db, "lazy_bound_unchecked", None, &events); + } + + #[test] + fn typevar_set_distinguishes_fresh_and_paramspec_identities() { + let db = setup_db(); + let typevar = bound_typevar( + &db, + "T", + TypeVarKind::Pep695TypeVar, + None, + TypeVarNonce::NONE, + ); + let fresh = bound_typevar( + &db, + "T", + TypeVarKind::Pep695TypeVar, + None, + TypeVarNonce::NONE.increment(), + ); + let paramspec = bound_typevar( + &db, + "P", + TypeVarKind::Pep695ParamSpec, + None, + TypeVarNonce::NONE, + ); + let args = paramspec.with_paramspec_attr(&db, ParamSpecAttrKind::Args); + let kwargs = paramspec.with_paramspec_attr(&db, ParamSpecAttrKind::Kwargs); + + let inferable = TypeVarSet::from_typevars(&db, [typevar, fresh, args, kwargs]); + assert_eq!( + inferable.iter(&db).collect::>(), + [typevar, fresh, args, kwargs] + ); + assert!(typevar.is_inferable(&db, inferable)); + assert!(fresh.is_inferable(&db, inferable)); + assert!(args.is_inferable(&db, inferable)); + assert!(kwargs.is_inferable(&db, inferable)); + assert!(!paramspec.is_inferable(&db, inferable)); + + let paramspec_only = TypeVarSet::from_typevars(&db, [paramspec]); + assert!( + args.identity(&db) + .without_paramspec_attr(&db) + .is_inferable(&db, paramspec_only) + ); + assert!( + kwargs + .identity(&db) + .without_paramspec_attr(&db) + .is_inferable(&db, paramspec_only) + ); + } +} From 4ab610a5eb22c2a6106b7f61839e498ca2dd5cdc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 27 Jul 2026 11:35:36 -0400 Subject: [PATCH 079/390] [ty] Preserve Self in `__new__` calls (#27003) ## Summary Prior to this change, calling an inherited `__new__` implementation could lose `Self` and return `Unknown`. This caused us to miss invalid attribute access and broke chains of custom `__new__` implementations: ```py from typing import Self class Foo: ... class Bar(Foo): def __new__(cls) -> Self: return Foo.__new__(cls) class Baz(Bar): def __new__(cls) -> Self: result = Bar.__new__(cls) # Previously: Unknown result.nonexistent() # Previously: no diagnostic return result ``` This gives every unannotated `__new__` receiver an implicit `type[Self]`, matching the constructor typing specification and preserving `Self` through inherited calls. It also preserves existing generic constructor calls where the synthetic `cls` receiver would otherwise be specialized and checked twice. The regression coverage includes overloaded callbacks, signature-preserving and callable-protocol decorators, `Concatenate`, and aliased receiver types; the intentionally odd nested and `Concatenate` inference results remain documented as pre-existing limitations. Closes https://github.com/astral-sh/ty/issues/3965. --------- Co-authored-by: Carl Meyer --- crates/ruff_benchmark/benches/ty_walltime.rs | 2 +- .../resources/mdtest/call/constructor.md | 99 ++++++++++++++++++- .../resources/mdtest/call/methods.md | 42 ++++++++ .../resources/mdtest/enums.md | 9 ++ .../mdtest/type_properties/is_subtype_of.md | 6 +- .../ty_python_semantic/src/types/call/bind.rs | 49 ++++++++- .../src/types/call/bind/constructor.rs | 4 +- .../ty_python_semantic/src/types/function.rs | 8 +- .../src/types/infer/builder/function.rs | 2 +- 9 files changed, 209 insertions(+), 12 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index a6eee29bca..9588fb055e 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -198,7 +198,7 @@ static SYMPY: Benchmark = Benchmark::new( max_dep_date: TY_ECOSYSTEM_PIN, python_version: SupportedPythonVersion::Py311, }, - 16617, + 16700, ); static TANJUN: Benchmark = Benchmark::new( diff --git a/crates/ty_python_semantic/resources/mdtest/call/constructor.md b/crates/ty_python_semantic/resources/mdtest/call/constructor.md index c50d97c62b..030bd9e091 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/constructor.md +++ b/crates/ty_python_semantic/resources/mdtest/call/constructor.md @@ -277,6 +277,103 @@ class Foo: reveal_type(Foo(1)) # revealed: Foo ``` +## Implicit `__new__` receivers + +An unannotated `cls` parameter on `__new__` is inferred as `type[Self]`. Constructor calls must be +accepted when a generic callback determines the class's type argument. Here, the correlated callback +overloads, covariant `frozenset`, and fully dynamic `values` exercise a path-merged specialization. +Reapplying that specialization to the synthetic `cls` would introduce an extra `frozenset` layer and +incorrectly reject both ordinary and signature-preserving `Callable` constructors. + +```pyi +from collections.abc import Callable +from typing import Any, Generic, ParamSpec, Protocol, TypeVar, overload +from typing_extensions import Self + +P = ParamSpec("P") +R = TypeVar("R") +R_co = TypeVar("R_co", covariant=True) +T = TypeVar("T") + +@overload +def callback(value: frozenset[T]) -> T: ... +@overload +def callback(value: T) -> T: ... + +class Mapper(Generic[R]): + def __new__(cls, callback: Callable[[T], R], values: list[T], /) -> Self: ... + +values: Any + +# TODO: Preserve correlated overload solutions so dynamic values do not infer an extra +# `frozenset` layer or an element type of `Never`. +reveal_type(Mapper(callback, values)) # revealed: Mapper[frozenset[frozenset[Never]]] + +def wrap(function: Callable[P, R]) -> Callable[P, R]: ... + +class Wrapped(Generic[R]): + @wrap + def __new__(cls, callback: Callable[[T], R], values: list[T]) -> Self: ... + +# TODO: Preserve the callback's correlated overload solutions through the decorator. +reveal_type(Wrapped(callback, values)) # revealed: Wrapped[frozenset[frozenset[Never]]] +``` + +A decorator can preserve `cls` explicitly with `Concatenate`, re-expressing the receiver with its +own type variable. Constraint inference checks the synthetic receiver; the later assignability pass +must not reject it merely because that decorator-scoped type variable remains unsolved: + +```pyi +from typing_extensions import Concatenate + +def wrap_cls(function: Callable[Concatenate[type[T], P], R]) -> Callable[Concatenate[type[T], P], R]: ... + +class WrappedCls: + @wrap_cls + def __new__(cls) -> Self: ... + +reveal_type(WrappedCls()) # revealed: WrappedCls +``` + +A decorator can also return a callback protocol instead of `Callable`. Its inferred `type[Self]` +receiver must likewise not be rejected by the later assignability pass: + +```pyi +class CallableObject(Protocol[P, R_co]): + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R_co: ... + +def wrap_object(function: Callable[P, R_co]) -> CallableObject[P, R_co]: ... + +class WrappedObject: + @wrap_object + def __new__(cls) -> Self: ... + +reveal_type(WrappedObject()) # revealed: WrappedObject +``` + +The explicit `cls` type in a signature-preserving decorator can also be expressed with a generic +type alias. The alias must be resolved when identifying the constructor receiver, or the later +assignability pass rejects the synthetic argument against the decorator's unsolved receiver type: + +```toml +[environment] +python-version = "3.12" +``` + +```pyi +type Receiver[X] = type[X] + +def preserve[U, **P, R]( + function: Callable[Concatenate[Receiver[U], P], R], +) -> Callable[Concatenate[Receiver[U], P], R]: ... + +class Simple: + @preserve + def __new__(cls) -> Self: ... + +reveal_type(Simple()) # revealed: Simple +``` + ## `__new__` defined as a classmethod Marking it as a classmethod, on the other hand, breaks at runtime. @@ -762,7 +859,7 @@ class C[T]: x: T def __new__[S](cls, x: S) -> "C[tuple[S, S]]": - return object.__new__(cls) + raise NotImplementedError() reveal_type(C(1)) # revealed: C[tuple[int, int]] reveal_type(C("hello")) # revealed: C[tuple[str, str]] diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index 803a591488..2a6a46926b 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -1019,6 +1019,48 @@ class X: return self.__new__(type(self)) ``` +Calling `object.__new__` from an overriding `__new__` method preserves `Self`, so an invalid +attribute access on the result is reported: + +```py +class Item: + def __new__(cls) -> Self: + result = object.__new__(cls) + reveal_type(result) # revealed: Self@__new__ + # error: [unresolved-attribute] + result.nonexistent() + return result +``` + +Explicitly marking `__new__` as a static method does not change the inferred result: + +```py +class StaticItem: + @staticmethod + def __new__(cls) -> Self: + result = object.__new__(cls) + reveal_type(result) # revealed: Self@__new__ + return result +``` + +`Self` is also preserved through a chain of inherited `__new__` calls: + +```py +class Foo: ... + +class Bar(Foo): + def __new__(cls) -> Self: + return Foo.__new__(cls) + +class Baz(Bar): + def __new__(cls) -> Self: + result = Bar.__new__(cls) + reveal_type(result) # revealed: Self@__new__ + # error: [unresolved-attribute] + result.nonexistent() + return result +``` + ## Bound-method attribute fallback Bound-method attributes are resolved first on `types.MethodType`, then, if absent, on the underlying diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 3a4e4b8fd9..943162c595 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -527,6 +527,8 @@ to `Any`: from enum import Enum class Connector(Enum): + connector_id: int + def __new__(cls, value: str, connector_id: int) -> "Connector": obj = object.__new__(cls) obj._value_ = value @@ -546,6 +548,7 @@ from enum import Enum class AnnotatedConnector(Enum): _value_: str + connector_id: int def __new__(cls, value: str, connector_id: int = 0) -> "AnnotatedConnector": obj = object.__new__(cls) @@ -661,6 +664,8 @@ annotation, subclass member values remain dynamic: from enum import Enum class Base(Enum): + connector_id: int + def __new__(cls, value: str, connector_id: int) -> "Base": obj = object.__new__(cls) obj._value_ = value @@ -680,6 +685,8 @@ An explicit `_value_` annotation on the subclass still takes precedence: from enum import Enum class Base(Enum): + connector_id: int + def __new__(cls, value: str, connector_id: int = 0) -> "Base": obj = object.__new__(cls) obj._value_ = value @@ -702,6 +709,8 @@ explicitly annotated: from enum import Enum class Base(Enum): + connector_id: int + def __new__(cls, value: int, connector_id: int = 0) -> "Base": obj = object.__new__(cls) obj._value_ = value diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index bb901fe2da..e9756bb3f4 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -1873,7 +1873,7 @@ class MetaWithIntReturn(type): class F(metaclass=MetaWithIntReturn): def __new__(cls) -> str: - return super().__new__(cls) + return "" class Returns[T](Protocol): def __call__(self) -> T: ... @@ -1952,7 +1952,7 @@ static_assert(not is_subtype_of(TypeOf[A], Returns[A])) class B: def __new__(cls, a: int) -> int: - return super().__new__(cls) + return 0 def __init__(self, a: str) -> None: ... @@ -2023,7 +2023,7 @@ class MetaWithIntReturn(type): class F(metaclass=MetaWithIntReturn): def __new__(cls) -> str: - return super().__new__(cls) + return "" def __init__(self, x: int) -> None: ... diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 76bba05579..d8ad60c669 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -5028,6 +5028,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { struct ArgumentTypeChecker<'a, 'db> { db: &'db dyn Db, signature_type: Type<'db>, + constructor_kind: Option, signature: &'a Signature<'db>, arguments: &'a CallArguments<'a, 'db>, argument_matches: &'a [MatchedArgument<'db>], @@ -5096,6 +5097,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn new( db: &'db dyn Db, signature_type: Type<'db>, + constructor_kind: Option, signature: &'a Signature<'db>, arguments: &'a CallArguments<'a, 'db>, argument_matches: &'a [MatchedArgument<'db>], @@ -5107,6 +5109,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { Self { db, signature_type, + constructor_kind, signature, arguments, argument_matches, @@ -5517,9 +5520,51 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { return; } + // Constraint inference has already checked the synthetic `cls`. For example: + // + // ```py + // from collections.abc import Callable + // from typing import Any, Self, overload + // + // @overload + // def callback[T](value: frozenset[T]) -> T: ... + // @overload + // def callback[T](value: T) -> T: ... + // + // class Mapper[R]: + // def __new__[T](cls, callback: Callable[[T], R], values: list[T]) -> Self: ... + // + // values: Any + // Mapper(callback, values) + // ``` + // + // The overloads provide alternative, correlated solutions for `T` and `R`. The current + // solver merges each TypeVar's solutions separately, losing that correlation. Applying + // the merged specialization to `cls` a second time then changes the receiver from + // `type[Mapper[frozenset[Never]]]` to + // `type[Mapper[frozenset[frozenset[Never]]]]`, incorrectly rejecting the valid call. + // + // A decorator using `Concatenate[type[U], P]` can also replace the inferred `type[Self]` + // receiver with its own `type[U]`, possibly through a type alias. Constraint inference has + // already checked the actual class against `type[U]`. If the other arguments cannot solve + // `U`, independently checking the class against `type[U]` again would reject the call + // solely because `U` remains unsolved. + // + // TODO: Remove this special case once solution extraction preserves correlations between + // TypeVars across alternative inference paths and constructor calls are solved in a single + // constraint set, so decorator-scoped receiver variables are not rechecked independently. + let constructor_receiver = matches!(argument, Argument::Synthetic) + && self.constructor_kind == Some(ConstructorCallableKind::New) + && matches!( + parameter.annotated_type().resolve_type_alias(self.db), + Type::SubclassOf(subclass_of) if subclass_of.into_type_var().is_some() + ); + let mut expected_ty = parameter.annotated_type(); if let Some(specialization) = self.specialization() { - argument_type = argument_type.apply_specialization(self.db, specialization); + if !constructor_receiver { + argument_type = argument_type.apply_specialization(self.db, specialization); + } expected_ty = expected_ty.apply_specialization(self.db, specialization); } @@ -5554,6 +5599,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // // TODO: handle starred annotations, e.g. `*args: *Ts` or `*args: *tuple[int, *tuple[str, ...]]` if !self.constraint_set_errors[argument_index] + && !constructor_receiver && !parameter.has_starred_annotation() && !is_valid_isinstance_target() && argument_type @@ -6747,6 +6793,7 @@ impl<'db> Binding<'db> { let mut checker = ArgumentTypeChecker::new( db, self.signature_type, + self.constructor_context.map(ConstructorContext::kind), &self.signature, arguments, &self.argument_matches, diff --git a/crates/ty_python_semantic/src/types/call/bind/constructor.rs b/crates/ty_python_semantic/src/types/call/bind/constructor.rs index 6b922afa01..b55b131e9f 100644 --- a/crates/ty_python_semantic/src/types/call/bind/constructor.rs +++ b/crates/ty_python_semantic/src/types/call/bind/constructor.rs @@ -553,7 +553,7 @@ impl<'db> ConstructorContext<'db> { self.instance_type } - fn kind(self) -> ConstructorCallableKind { + pub(super) fn kind(self) -> ConstructorCallableKind { self.kind } } @@ -635,7 +635,7 @@ impl<'db> Binding<'db> { return false; }; - let Type::SubclassOf(subclass_of) = cls_parameter_ty else { + let Type::SubclassOf(subclass_of) = cls_parameter_ty.resolve_type_alias(db) else { return false; }; let Some(cls_typevar) = subclass_of.into_type_var() else { diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index d65622e8b7..0486a2b92a 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -630,7 +630,9 @@ impl<'db> OverloadLiteral<'db> { let generic_context = raw_signature.generic_context; raw_signature.add_implicit_self_annotation(db, || { - if self.is_staticmethod(db) { + let is_staticmethod = self.is_staticmethod(db); + let is_dunder_new = self.name(db) == "__new__"; + if is_staticmethod && !is_dunder_new { return None; } @@ -678,7 +680,7 @@ impl<'db> OverloadLiteral<'db> { for an implicit self: Self annotation", ); - if self.is_classmethod(db) { + if self.is_classmethod(db) || is_dunder_new { Some(SubclassOfType::from( db, SubclassOfInner::TypeVar(typing_self), @@ -689,7 +691,7 @@ impl<'db> OverloadLiteral<'db> { } else { // If skip creating the typevar, we use "instance of class" or "subclass of // class" as the implicit annotation instead. - if self.is_classmethod(db) { + if self.is_classmethod(db) || is_dunder_new { Some(SubclassOfType::from( db, SubclassOfInner::Class(ClassType::NonGeneric(class_literal)), diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs index bd8cfcedf2..7690042950 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -1057,7 +1057,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .as_class_literal() .and_then(|class| class.known(db)) { - if known_class == KnownClass::Staticmethod { + if known_class == KnownClass::Staticmethod && function_name != "__new__" { return None; } From 5fa6d662954153ddfc008f26e4278bc96d38ab16 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 27 Jul 2026 18:21:03 +0100 Subject: [PATCH 080/390] [ty] Don't consider known-instance types, generic aliases or non-singleton special-form types to be single-valued (#27137) ## Summary Stop assuming that special-cased typing objects and generic aliases compare equal whenever they share the same internal type representation. Distinct objects that compare unequal at runtime can often have identical representations in ty's model. Examples include `list[int]` and `typing.List[int]`, `Annotated` aliases with different metadata, and special forms from `typing` and `typing_extensions`. Treat these objects conservatively during equality narrowing and pattern matching, while preserving precise behavior for sentinels and special forms guaranteed to be singletons. Remove unnecessary special-casing for empty ranges. As well as fixing several bugs and simplifying our implementation, this also makes it easier to remove the troublesome `Type::is_single_valued()` API altogether in a followup PR. ## Test Plan mdtests updated --- .../resources/mdtest/loops/for.md | 21 ---- .../mdtest/narrow/conditionals/eq.md | 99 ++++++++++++++++++- .../resources/mdtest/narrow/match.md | 39 ++++++-- .../type_properties/is_single_valued.md | 1 - crates/ty_python_semantic/src/types.rs | 30 +++--- .../ty_python_semantic/src/types/equality.rs | 8 +- .../src/types/special_form.rs | 14 ++- 7 files changed, 152 insertions(+), 60 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index 20e3e8caa4..ec2d673fdf 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -86,27 +86,6 @@ def non_empty_first(flag: bool) -> None: reveal_type(value) # revealed: range ``` -Empty ranges all compare equal, but non-empty ranges with the same type refinement may contain -different values: - -```py -empty_left = range(0) -empty_right = range(1, 1) - -if empty_left == empty_right: - reveal_type(empty_left) # revealed: range -else: - reveal_type(empty_left) # revealed: Never - -non_empty_left = range(1) -non_empty_right = range(2) - -if non_empty_left == non_empty_right: - reveal_type(non_empty_left) # revealed: range -else: - reveal_type(non_empty_left) # revealed: range -``` - ## With shadowed `range` ```py diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index 902e4a1df9..390f77a488 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -1206,6 +1206,101 @@ def narrow_different_equality_implementations(value: FinalObject | FinalInt, oth reveal_type(value) # revealed: FinalObject ``` +## Sentinels + +Sentinels always compare equal to themselves, since they are singletons: + +```py +from typing_extensions import Sentinel + +MISSING = Sentinel("MISSING") + +reveal_type(MISSING == MISSING) # revealed: Literal[True] +``` + +## Known typing-object equality behavior + +Certain typing APIs are heavily special-cased by ty, which makes it tempting to special case +equality inference for these symbols. This, however, is error-prone: for example, ty currently +infers the same type for `typing_extensions.Literal` as it does for `typing.Literal`, even though +these may not be the same runtime object and may not compare equal. There's also no known use case +for precisely inferring equality comparisons between these objects. + +For most special-cased typing APIs, therefore, we simply fallback to the nominal instance that the +typing symbol is known to be an instance of: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from functools import partial +from typing import Literal, NamedTuple +from typing_extensions import NamedTuple as ExtensionsNamedTuple +from ty_extensions._internal import generic_context + +type Alias = int + +class GenericClass[T]: ... + +reveal_type(Alias == Alias) # revealed: bool +reveal_type(generic_context(GenericClass) == generic_context(GenericClass)) # revealed: bool +reveal_type((int | str) == (int | str)) # revealed: bool +reveal_type(Literal[1] == Literal[1]) # revealed: bool + +def target(value: int) -> int: + return value + +# The bound `__call__` methods belong to distinct `partial` objects. +reveal_type(partial(target, 1).__call__ == partial(target, 1).__call__) # revealed: bool + +reveal_type(NamedTuple == ExtensionsNamedTuple) # revealed: bool +reveal_type(NamedTuple != ExtensionsNamedTuple) # revealed: bool +``` + +Repeated construction of `dataclasses.Field` and `typing_extensions.deprecated` produces distinct +objects that will compare unequal, even when their inferred payloads are identical: + +```py +from dataclasses import dataclass, field +from typing_extensions import deprecated + +@dataclass +class FieldComparisons: + # False at runtime! + equals: bool = reveal_type(field(default=1) == field(default=1)) # revealed: bool + # True at runtime! + not_equals: bool = reveal_type(field(default=1) != field(default=1)) # revealed: bool + +# False at runtime! +reveal_type(deprecated("gone") == deprecated("gone")) # revealed: bool +# True at runtime! +reveal_type(deprecated("gone") != deprecated("gone")) # revealed: bool +``` + +Runtime-significant metadata, spelling, and origin can be erased from the types that ty records for +many of these APIs. Just because ty infers two of these objects as being of the same type does not +therefore mean that they are equal: + +```py +import builtins +from collections.abc import Callable as AbcCallable +from typing import Annotated, Callable, List, Type, TypeAlias + +A: TypeAlias = "int" +B: TypeAlias = "builtins.int" + +# The `Annotated[]` metadata is discarded and ignored by ty, so these are inferred +# as having the same type, but they will compare unequal at runtime +reveal_type(Annotated[int, "a"] == Annotated[int, "b"]) # revealed: bool + +reveal_type(A == B) # revealed: bool +reveal_type(Callable[[int], str] == AbcCallable[[int], str]) # revealed: bool +reveal_type(List[int] == list[int]) # revealed: bool +reveal_type(Type[int] == type[int]) # revealed: bool +``` + ## Constrained type variables Equality analysis expands the constraints of a constrained type variable in either operand position. @@ -1717,7 +1812,7 @@ def _(x: Any, y: Any | str): def _(x: Any): if x != list[Any]: - reveal_type(x) # revealed: Any & ~ + reveal_type(x) # revealed: Any def _(x: Any, y: SingleIntEnum): if x == y: @@ -1734,7 +1829,7 @@ def _(x: Any): if x == RUNTIME_TYPE_VAR: pass else: - reveal_type(x) # revealed: Any & ~TypeVar + reveal_type(x) # revealed: Any ``` `Any` must stay `Any` when compared with an enum, on either side of the comparison: diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index 2fce07e47a..8f6a3593cf 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -2932,31 +2932,57 @@ def _(value: FinalPatternInt): reveal_type(value) # revealed: FinalPatternInt ``` -Some precisely modeled objects compare equal to themselves, so an equivalent value pattern is -exhaustive: +We don't attempt to precisely model equality behaviour between special-cased typing-API objects. As +described in `narrow/conditionals/eq.md`, doing so would be possible in some cases, but it would be +error-prone, and there are few known use cases for doing this. ```py +from functools import partial from types import FunctionType -from typing import NewType, TypeVar +from typing import List, Literal, NewType, Optional, TypeVar +from typing_extensions import Literal as ExtensionsLiteral T = TypeVar("T") UserId = NewType("UserId", int) class ReflexivePatternValues: LIST_INT = list[int] + LEGACY_LIST_INT = List[int] + EXTENSIONS_LITERAL = ExtensionsLiteral + OPTIONAL = Optional TYPE_VAR = T NEW_TYPE = UserId +# error: [invalid-return-type] def generic_alias_value_pattern() -> int: match list[int]: case ReflexivePatternValues.LIST_INT: return 1 +# error: [invalid-return-type] +def cross_origin_generic_alias_value_pattern() -> int: + match list[int]: + case ReflexivePatternValues.LEGACY_LIST_INT: + return 1 + +# error: [invalid-return-type] +def cross_origin_special_form_value_pattern() -> int: + match Literal: + case ReflexivePatternValues.EXTENSIONS_LITERAL: + return 1 + +def singleton_special_form_value_pattern() -> int: + match Optional: + case ReflexivePatternValues.OPTIONAL: + return 1 + +# error: [invalid-return-type] def type_var_value_pattern() -> int: match T: case ReflexivePatternValues.TYPE_VAR: return 1 +# error: [invalid-return-type] def new_type_value_pattern() -> int: match UserId: case ReflexivePatternValues.NEW_TYPE: @@ -2972,13 +2998,6 @@ def bound_method_value_pattern() -> int: match helper.__get__: case helper.__get__: return 1 -``` - -Two calls that construct equivalent objects need not produce equal values. For example, separate -`partial` objects do not compare equal, so this match is not exhaustive: - -```py -from functools import partial def target(value: int) -> int: return value diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md index 82dcfb33e2..9ff48c25a3 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md @@ -48,7 +48,6 @@ static_assert(not is_single_valued(Callable[[int, str], None])) static_assert(not is_single_valued(TypeAliasType)) static_assert(not is_single_valued(UnionType)) -static_assert(is_single_valued(TypeOf[list[int]])) class A: def method(self): ... diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e8c2fe0b17..87f32bca33 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2484,15 +2484,7 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(..) | Type::ClassLiteral(..) | Type::ModuleLiteral(..) => true, - Type::SpecialForm(special_form) => { - // Nearly all `SpecialForm` types are singletons, but if a symbol could validly - // originate from either `typing` or `typing_extensions` then this is not guaranteed. - // E.g. `typing.TypeGuard` is equivalent to `typing_extensions.TypeGuard`, so both are treated - // as inhabiting the type `SpecialFormType::TypeGuard` in our model, but they are actually - // distinct symbols at different memory addresses at runtime. - !(special_form.check_module(KnownModule::Typing) - && special_form.check_module(KnownModule::TypingExtensions)) - } + Type::SpecialForm(special_form) => special_form.is_guaranteed_singleton(), Type::KnownInstance(KnownInstanceType::Sentinel(_)) => true, Type::KnownInstance(_) => false, Type::Callable(_) => { @@ -2543,22 +2535,22 @@ impl<'db> Type<'db> { /// Return true if this type is non-empty and all inhabitants of this type compare equal. pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { match self { - // All empty ranges compare equal, but non-empty ranges can contain different values. - Type::KnownInstance(KnownInstanceType::Range { is_non_empty }) => !is_non_empty, + Type::KnownInstance(KnownInstanceType::Sentinel(_)) => true, + Type::SpecialForm(special_form) => special_form.is_guaranteed_singleton(), - // Each `partial()` call creates a distinct object at runtime. - Type::KnownInstance( - KnownInstanceType::FunctoolsPartial(_) | KnownInstanceType::FunctoolsPartialCall(_), - ) => false, + // It's tempting to add extensive special casing here, but it would be very error-prone, + // and there's no known use case. For example, `Annotated[int, ""]` does not compare + // equal to `Annotated[int, "fooo"]` (but for us they have identical types); `list[int]` + // does not compare equal to `typing.List[int]` (but for us they have identical types); + // `typing.Literal` will not necessarily be the same object as `typing_extensions.Literal` + // even on Python versions where typeshed says they are the same symbol; etc. etc. + Type::KnownInstance(_) | Type::GenericAlias(_) => false, Type::FunctionLiteral(..) | Type::WrapperDescriptor(_) | Type::KnownBoundMethod(_) | Type::ModuleLiteral(..) - | Type::ClassLiteral(..) - | Type::GenericAlias(..) - | Type::SpecialForm(..) - | Type::KnownInstance(..) => true, + | Type::ClassLiteral(..) => true, Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Enum(..) => !self.overrides_equality(db), diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index e239ea7f6f..ebc85a049c 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -677,11 +677,6 @@ fn evaluate_structural_comparison<'db>( (Type::ModuleLiteral(left_module), Type::ModuleLiteral(right_module)) => { operator.result_from_equality(left_module.module(db) == right_module.module(db)) } - (Type::GenericAlias(left_alias), Type::GenericAlias(right_alias)) - if left_alias == right_alias => - { - operator.result_from_equality(true) - } (Type::WrapperDescriptor(left_descriptor), Type::WrapperDescriptor(right_descriptor)) if left_descriptor == right_descriptor => { @@ -1624,7 +1619,8 @@ fn has_known_identity_comparison_semantics<'db>( operator: ComparisonOperator, ) -> bool { match ty { - Type::FunctionLiteral(_) | Type::ModuleLiteral(_) | Type::SpecialForm(_) => true, + Type::FunctionLiteral(_) | Type::ModuleLiteral(_) => true, + Type::SpecialForm(special_form) => special_form.is_guaranteed_singleton(), Type::ClassLiteral(class) => { KnownComparisonSemantics::of_instance(db, class.metaclass_instance_type(db), operator) == Some(KnownComparisonSemantics::Object) diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 97100f8c53..c168bca8b8 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -234,6 +234,18 @@ impl SpecialFormType { self.class().to_instance(db) } + /// Return `true` if this special form is guaranteed to be a singleton at runtime. + /// + /// Nearly all `SpecialForm` types are singletons, but if a symbol could validly + /// originate from either `typing` or `typing_extensions` then this is not guaranteed. + /// E.g. `typing.TypeGuard` is equivalent to `typing_extensions.TypeGuard`, so both are treated + /// as inhabiting the type `SpecialFormType::TypeGuard` in our model, but they are actually + /// distinct symbols at different memory addresses at runtime. + pub(super) const fn is_guaranteed_singleton(self) -> bool { + !(self.check_module(KnownModule::Typing) + && self.check_module(KnownModule::TypingExtensions)) + } + /// Return the type denoted by this retained special-form value when it is valid without /// parameters or a surrounding inference scope. pub(crate) fn type_form_argument(self, db: &dyn Db) -> Option> { @@ -517,7 +529,7 @@ impl SpecialFormType { /// /// Most variants can only exist in one module, which is the same as `self.class().canonical_module(db)`. /// Some variants could validly be defined in either `typing` or `typing_extensions`, however. - pub(super) fn check_module(self, module: KnownModule) -> bool { + pub(super) const fn check_module(self, module: KnownModule) -> bool { match self { Self::TypeQualifier(qualifier) => qualifier.check_module(module), Self::LegacyStdlibAlias(_) From 552eda14df4d804a5f2d4bdd431f23a379e763f2 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 27 Jul 2026 18:21:03 +0100 Subject: [PATCH 081/390] [ty] Remove `Type::is_single_valued()` (#26992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Stacked on #27137. Apologies, there's a few separate things going on here, but @carljm encouraged me to submit this as-is rather than taking the time to try to split it up more, so... here it is! This PR removes the confused `Type::is_single_valued()` concept and the corresponding `ty_extensions._internal.is_single_valued` API. “Single-valued” conflated several subtly different questions: whether a type represents one runtime object, whether all represented values compare equal, whether `==` and `!=` have predictable behavior, and whether a comparison can safely narrow a type. Those properties are not interchangeable, especially for overridden comparison methods, tuples, constrained `TypeVar`s, and erased `NewType` wrappers. Instead, equality, inequality, and membership narrowing now ask the comparison evaluator the specific question they need. ### Remove the single-valued API and helper chain Deletes: - `Type::is_single_valued()` and its supporting implementations for known classes, instances, tuples, enums, and functions; - `ty_extensions._internal.is_single_valued`, including its call-binding support and dedicated tests; - the associated property tests, comments, and prose describing “single-valued types”. Existing tests are reframed in terms of the observable behavior they actually exercise, such as literal equality narrowing, singleton-ness, or statically known branches. ### Consolidate equality and inequality analysis Routes equality and inequality narrowing through a shared comparison path, eliminating their duplicated constraint-evaluation logic and the redundant comparison-domain pre-classification. In particular: - literal constraints no longer rely on a separate single-valued check; - constrained `TypeVar` correlation is preserved by checking whether the narrowed constraint actually compares equal, instead of requiring type equivalence; - `not in` narrowing uses the comparison evaluator to determine whether every value represented by a tuple slot compares equal, while respecting the configured comparison-soundness policy; - equality and inequality continue to account for their distinct comparison methods where necessary. This also improves cases such as: ```py def inequality_else(value: str | tuple[str | None, str | None, str] | None) -> None: if value == "files": pass elif value != "response": return reveal_type(value) # revealed: Literal["files", "response"] ``` ### Consolidate tuple-comparison semantics Adds fixed-length tuple reasoning to the equality evaluator and shares tuple-element equality analysis with comparison inference. Tuple comparisons can now use the information that: - tuples with different fixed lengths cannot compare equal; - corresponding elements are compared using identity before equality; - a definite unequal element makes the tuples unequal; - all elements must compare equal before the tuples can be considered definitely equal; - recursive tuple comparisons must remain cycle-safe. For example: ```py def fixed_tuple_slot(x: tuple[Literal[1], Literal["x"]] | None) -> None: if x not in ((1, "x"),): reveal_type(x) # revealed: None def incompatible_tuple_key( key: tuple[str, bool, bool], values: dict[tuple[str, bool], int], ) -> int | None: if key in values: reveal_type(key) # revealed: Never return values[key] return None ``` Comparison inference still owns operator calls and diagnostics, but now delegates tuple-element truthiness to the equality evaluator. This preserves `unsupported-bool-conversion` diagnostics even when another element or a length mismatch makes the overall tuple result definite, avoids attempting element-wise boolean conversion for `is` and `is not`, and avoids unnecessary lexicographic ordering calls when preceding elements are definitely equal. ### Cover subtle comparison cases Adds and updates regression coverage for: - fixed, empty, subclassed, `NewType`-wrapped, and recursive tuple slots; - tuple equality, inequality, membership, identity, and lexicographic comparison behavior; - tuple elements with unsupported boolean conversion; - constrained literal `TypeVar`s and correlated narrowing; - enums with custom `__eq__` or `__ne__` methods; - erased `NewType` identities, where static disjointness does not imply distinct runtime objects; - equality and inequality branches involving literals, tuples, and broad types. Together, these changes remove the broad, footgun-prone type property while keeping comparison semantics localized in the comparison evaluator and making the relevant narrowing decisions explicit. --- .../resources/mdtest/annotations/new_types.md | 6 +- .../resources/mdtest/comparison/tuples.md | 28 +- .../mdtest/generics/pep695/variables.md | 13 +- .../mdtest/narrow/conditionals/eq.md | 156 ++++++- .../mdtest/narrow/conditionals/in.md | 65 ++- .../resources/mdtest/protocols.md | 5 +- ...lemen\342\200\246_(39b614d4707c0661).snap" | 37 +- .../mdtest/statically_known_branches.md | 14 +- .../resources/mdtest/ty_extensions.md | 15 - .../type_compendium/integer_literals.md | 17 +- .../type_properties/is_single_valued.md | 142 ------ .../mdtest/type_properties/truthiness.md | 2 +- crates/ty_python_semantic/src/reachability.rs | 2 +- crates/ty_python_semantic/src/types.rs | 126 +----- .../ty_python_semantic/src/types/call/bind.rs | 8 - .../src/types/class/known.rs | 118 ----- crates/ty_python_semantic/src/types/enums.rs | 12 - .../ty_python_semantic/src/types/equality.rs | 421 ++++++++++-------- .../ty_python_semantic/src/types/function.rs | 4 - .../src/types/infer/comparisons.rs | 127 +++--- .../ty_python_semantic/src/types/instance.rs | 14 - crates/ty_python_semantic/src/types/narrow.rs | 6 +- .../src/types/property_tests.rs | 6 - .../ty_python_semantic/src/types/relation.rs | 5 +- crates/ty_python_semantic/src/types/tuple.rs | 15 - .../ty_vendored/ty_extensions/_internal.pyi | 3 - 26 files changed, 600 insertions(+), 767 deletions(-) delete mode 100644 crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index 08b419e34c..c2e1c85b9d 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -566,23 +566,21 @@ E(["foo"]) # error: [invalid-argument-type] E(E(E(["foo"]))) # error: [invalid-argument-type] ``` -## `NewType` wrapping preserves singleton-ness and single-valued-ness +## `NewType` wrapping preserves singleton-ness ```py from typing_extensions import NewType from ty_extensions import static_assert -from ty_extensions._internal import is_singleton, is_single_valued +from ty_extensions._internal import is_singleton from types import EllipsisType A = NewType("A", EllipsisType) static_assert(is_singleton(A)) -static_assert(is_single_valued(A)) reveal_type(type(A(...)) is EllipsisType) # revealed: Literal[True] reveal_type(A(...) is ...) # revealed: Literal[True] B = NewType("B", int) static_assert(not is_singleton(B)) -static_assert(not is_single_valued(B)) ``` ## `NewType`s of tuples can be iterated/unpacked diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md b/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md index 6a70f1e5ea..8fd77c122c 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md @@ -149,6 +149,7 @@ of the dunder methods.) ```py from __future__ import annotations +from typing import Literal class EqReturnType: ... class NeReturnType: ... @@ -203,6 +204,18 @@ class B: return LtReturnTypeOnB() reveal_type((A(), B()) < (A(), B())) # revealed: LtReturnType | LtReturnTypeOnB | Literal[False] + +class LaterLtReturnType: ... + +class CustomEq: + def __eq__(self, other: object) -> Literal[True]: + return True + +class Later: + def __lt__(self, other: Later) -> LaterLtReturnType: + return LaterLtReturnType() + +reveal_type((CustomEq(), Later()) < (CustomEq(), Later())) # revealed: LaterLtReturnType | Literal[False] ``` #### Special Handling of Eq and NotEq in Lexicographic Comparisons @@ -496,7 +509,20 @@ class A: return NotBoolable() # error: [unsupported-bool-conversion] -(A(),) == (A(),) +reveal_type((A(),) == (A(),)) # revealed: bool +# error: [unsupported-bool-conversion] +reveal_type((A(), "x") == (A(), "y")) # revealed: Literal[False] +# error: [unsupported-bool-conversion] +reveal_type((A(),) != (A(), 0)) # revealed: Literal[True] +``` + +Tuple identity comparisons do not compare elements and therefore do not coerce their equality +results to `bool`: + +```py +def tuple_identity(left: tuple[A], right: tuple[A]) -> None: + reveal_type(left is right) # revealed: bool + reveal_type(left is not right) # revealed: bool ``` ## Recursive NamedTuple diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md index 9ced4016d0..3ab315003e 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md @@ -604,21 +604,17 @@ def f[ # fmt: on ``` -## Singletons and single-valued types - -(Note: for simplicity, all of the prose in this section refers to _singleton_ types, but all of the -claims also apply to _single-valued_ types.) +## Singletons An unbounded, unconstrained typevar is not a singleton, because it can be specialized to a non-singleton type. ```py from ty_extensions import static_assert -from ty_extensions._internal import is_singleton, is_single_valued +from ty_extensions._internal import is_singleton def unbounded_unconstrained[T](t: T) -> None: static_assert(not is_singleton(T)) - static_assert(not is_single_valued(T)) ``` A bounded typevar is not a singleton, even if its bound is a singleton, since it can still be @@ -627,7 +623,6 @@ specialized to `Never`. ```py def bounded[T: None](t: T) -> None: static_assert(not is_singleton(T)) - static_assert(not is_single_valued(T)) ``` A constrained typevar is a singleton if all of its constraints are singletons. (Note that you cannot @@ -638,13 +633,9 @@ from typing_extensions import Literal 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: T) -> None: static_assert(is_singleton(T)) - -def constrained_single_valued[T: (Literal[True], tuple[()])](t: T) -> None: - static_assert(is_single_valued(T)) ``` ## Unions involving typevars diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index 390f77a488..5281395459 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -1308,7 +1308,8 @@ The resulting constraint is intersected with the type variable, preserving its i ```py from enum import Enum -from typing import Literal, TypeVar, final +from typing import Any, Generic, Literal, TypeVar, final +from ty_extensions import Intersection, Top @final class ConstraintA: ... @@ -1347,6 +1348,32 @@ def correlated_typevar_ne(value: E, other: EnumT) -> EnumT: return other reveal_type(value) # revealed: EnumT@correlated_typevar_ne return value + +LiteralT = TypeVar("LiteralT", Literal[1], Literal[2]) + +def correlated_literal_typevar_eq(value: Literal[1, 2], other: LiteralT) -> LiteralT: + if value == other: + return value + return other + +def correlated_literal_typevar_ne(value: Literal[1, 2], other: LiteralT) -> LiteralT: + if value != other: + return other + return value + +MaterializedT = TypeVar("MaterializedT", Literal[1], Intersection[Literal[2], Any]) + +HolderT = TypeVar("HolderT") + +class Holder(Generic[HolderT]): + def __init__(self, value: HolderT) -> None: + self.value = value + +def correlated_materialized_pattern(left: Top[MaterializedT], right: MaterializedT) -> int: + holder = Holder(right) + match left: + case holder.value: + return 1 ``` ## `LiteralString` and string-valued enums @@ -1578,7 +1605,7 @@ def _(x: Literal[1, 2]): reveal_type(x) # revealed: Literal[2] ``` -## `x != y` where `y` is a single-valued type +## `x != y` where `y` is a class literal ```py def _(flag: bool): @@ -1592,7 +1619,7 @@ def _(flag: bool): reveal_type(C) # revealed: ``` -## `x != y` where `y` has multiple single-valued options +## `x != y` where `y` has multiple literal options ```py from typing import Literal @@ -1623,9 +1650,9 @@ def _(x: Literal[1, 2], y: Y): reveal_type(x) # revealed: Literal[1, 2] ``` -## `!=` for non-single-valued types +## `!=` for broad types -Only single-valued types should narrow the type: +A broad right-hand type cannot narrow `x`: ```py def _(x: int | None, y: int): @@ -1633,7 +1660,7 @@ def _(x: int | None, y: int): reveal_type(x) # revealed: int | None ``` -## Mix of single-valued and non-single-valued types +## Mix of literal and broad types ```py from typing import Literal @@ -2071,7 +2098,7 @@ We assume that tuple subclasses don't override `tuple.__eq__`, which only return tuples. So they are excluded from the narrowed type when comparing to non-tuple values. ```py -from typing import Literal +from typing import Literal, cast def _(x: Literal["a", "b"] | tuple[int, int]): if x == "a": @@ -2080,6 +2107,121 @@ def _(x: Literal["a", "b"] | tuple[int, int]): else: # tuple type remains in the else branch reveal_type(x) # revealed: Literal["b"] | tuple[int, int] + +class OpenTupleSubclass(tuple[int, int]): ... + +def _(x: Literal["a", "b"] | OpenTupleSubclass): + if x == "a": + reveal_type(x) # revealed: Literal["a"] + else: + reveal_type(x) # revealed: Literal["b"] | OpenTupleSubclass + +def inequality_else(value: str | tuple[str | None, str | None, str] | None) -> None: + if value == "files": + pass + elif value != "response": + return + + reveal_type(value) # revealed: Literal["files", "response"] + cast(Literal["files", "response"], value) # error: [redundant-cast] +``` + +Fixed-length tuples compare corresponding elements using identity before equality, so distinct +inferred element types can still make the result definite. Different lengths cannot compare equal: + +```py +from enum import Enum +from typing import Final, Literal, NewType + +class TupleValues: + TRUE: Final = (True,) + LONGER: Final = (True, 0) + +def equivalent_tuple_pattern(value: tuple[Literal[1]]) -> int: + match value: + case TupleValues.TRUE: + return 1 + +def different_length_tuple_pattern(value: tuple[Literal[1]]) -> None: + match value: + case TupleValues.LONGER: + reveal_type(value) # revealed: Never + +class NeverEqualTupleElement(Enum): + A = 1 + B = 2 + + def __eq__(self, other: object) -> Literal[False]: + return False + +reveal_type((NeverEqualTupleElement.A,) == (NeverEqualTupleElement.A,)) # revealed: Literal[True] +reveal_type((NeverEqualTupleElement.A,) != (NeverEqualTupleElement.A,)) # revealed: Literal[False] + +def tuple_with_non_reflexive_elements(left: NeverEqualTupleElement, right: NeverEqualTupleElement) -> None: + reveal_type((left,) == (right,)) # revealed: bool + reveal_type((left,) != (right,)) # revealed: bool + +LeftElement = NewType("LeftElement", NeverEqualTupleElement) +RightElement = NewType("RightElement", NeverEqualTupleElement) + +def tuple_with_erased_element_identity(value: NeverEqualTupleElement) -> None: + reveal_type((LeftElement(value),) == (RightElement(value),)) # revealed: bool + reveal_type((LeftElement(value),) != (RightElement(value),)) # revealed: bool +``` + +## Narrowing with NewTypes + +`NewType` wrappers erase their distinction at runtime, so comparisons with an identity-based enum +literal remain ambiguous: + +```py +from enum import Enum +from typing import NewType + +class IdentityEnum(Enum): + A = 1 + B = 2 + +WrappedIdentityEnum = NewType("WrappedIdentityEnum", IdentityEnum) + +def literal_with_erased_identity(value: WrappedIdentityEnum) -> None: + reveal_type(IdentityEnum.A == value) # revealed: bool + reveal_type(IdentityEnum.A != value) # revealed: bool +``` + +## Narrowing with enums that have custom `__eq__` methods + +Custom enum comparison methods with definite return types determine equality and inequality +independently: + +```py +from enum import Enum +from typing import Any, Literal + +class AlwaysEqualEnum(Enum): + A = 1 + B = 2 + + def __eq__(self, other: object) -> Literal[True]: + return True + +class NeverUnequalEnum(Enum): + A = 1 + B = 2 + + def __ne__(self, other: object) -> Literal[False]: + return False + +reveal_type(AlwaysEqualEnum.A == AlwaysEqualEnum.B) # revealed: Literal[True] +reveal_type(NeverUnequalEnum.A != NeverUnequalEnum.B) # revealed: Literal[False] + +def tuple_with_custom_equality(left: AlwaysEqualEnum, right: AlwaysEqualEnum) -> None: + reveal_type((left,) == (right,)) # revealed: Literal[True] + reveal_type((left,) != (right,)) # revealed: Literal[False] + +def never_unequal_narrowing(x: Any, value: Literal[NeverUnequalEnum.A]) -> None: + if x != value: + reveal_type(x) # revealed: Any & ~Literal[NeverUnequalEnum.A] ``` ## Narrowing tagged unions of nominal classes by attribute diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md index 770fee1c36..78a6ded3ee 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md @@ -543,7 +543,8 @@ def unrelated_typevar(x: AlwaysEqual, y: U) -> U: ## Direct `not in` conditional ```py -from typing import Any, Literal, TypeVar +from enum import Enum +from typing import Any, Literal, NewType, TypeVar T = TypeVar("T", Literal[1], Literal[2]) @@ -595,6 +596,44 @@ def correlated_typevar(x: T | None, y: T) -> None: if x not in (y,): reveal_type(x) # revealed: None +def empty_tuple_slot(x: tuple[()] | None) -> None: + if x not in ((),): + reveal_type(x) # revealed: None + +def fixed_tuple_slot(x: tuple[Literal[1], Literal["x"]] | None) -> None: + if x not in ((1, "x"),): + reveal_type(x) # revealed: None + +# We optimistically assume that an unseen runtime subclass does not override `tuple.__eq__`. +class OpenTupleSubclass(tuple[Literal[1], Literal["x"]]): ... + +def tuple_subclass_slot(x: OpenTupleSubclass | None, value: OpenTupleSubclass) -> None: + if x not in (value,): + reveal_type(x) # revealed: None + +WrappedTuple = NewType("WrappedTuple", tuple[Literal[1], Literal["x"]]) + +def newtype_tuple_slot(x: WrappedTuple | None, value: WrappedTuple) -> None: + if x not in (value,): + reveal_type(x) # revealed: None + +class ReflexiveEnum(Enum): + A = 1 + B = 2 + + def __eq__(self, other: object) -> Literal[True]: + return True + +E = TypeVar("E", Literal[ReflexiveEnum.A], Literal[ReflexiveEnum.B]) + +def reflexive_enum_literal_slot(x: Literal[ReflexiveEnum.A] | None, value: Literal[ReflexiveEnum.A]) -> None: + if x not in (value,): + reveal_type(x) # revealed: None + +def reflexive_enum_typevar_slot(x: E | None, value: E) -> None: + if x not in (value,): + reveal_type(x) # revealed: None + def tuple_with_any_slot(x: str | None, missing: Any) -> None: if x not in (missing, None): reveal_type(x) # revealed: str @@ -617,6 +656,21 @@ def mutable_global_rhs(x: str | None, unavailable: set[str | None]) -> None: reveal_type(x) # revealed: str | None ``` +## Recursive tuple slots + +```toml +[environment] +python-version = "3.12" +``` + +```py +type Recursive = tuple[Recursive, int] + +def recursive_tuple_slot(x: Recursive | None, value: Recursive) -> None: + if x not in (value,): + reveal_type(x) # revealed: tuple[Recursive, int] | None +``` + ## Membership and equality When containment is known to compare items using equality, we can remove a union member that cannot @@ -707,6 +761,15 @@ def custom_equality(x: AlwaysEqual | Literal[1]): def empty_tuple(x: Payload | Literal["missing"], values: tuple[()]): if x in values: reveal_type(x) # revealed: Never + +def incompatible_tuple_key( + key: tuple[str, bool, bool], + values: dict[tuple[str, bool], int], +) -> int | None: + if key in values: + reveal_type(key) # revealed: Never + return values[key] + return None ``` ## Custom containment methods diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 8979965595..0bfa70f44b 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -5163,7 +5163,7 @@ def _(x: Foo): pass ``` -## Protocols are never singleton types, and are never single-valued types +## Protocols are never singleton types It *might* be possible to have a singleton protocol-instance type...? @@ -5173,14 +5173,13 @@ worth it. Such cases should anyway be exceedingly rare and/or contrived. ```py from typing import Protocol, Callable -from ty_extensions._internal import is_singleton, is_single_valued +from ty_extensions._internal import is_singleton 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] ``` ## Integration test: `typing.SupportsIndex` and `typing.Sized` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" index d8946d351b..5704deda5d 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" @@ -22,7 +22,14 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md 7 | return NotBoolable() 8 | 9 | # error: [unsupported-bool-conversion] -10 | (A(),) == (A(),) +10 | reveal_type((A(),) == (A(),)) # revealed: bool +11 | # error: [unsupported-bool-conversion] +12 | reveal_type((A(), "x") == (A(), "y")) # revealed: Literal[False] +13 | # error: [unsupported-bool-conversion] +14 | reveal_type((A(),) != (A(), 0)) # revealed: Literal[True] +15 | def tuple_identity(left: tuple[A], right: tuple[A]) -> None: +16 | reveal_type(left is right) # revealed: bool +17 | reveal_type(left is not right) # revealed: bool ``` # Diagnostics @@ -53,10 +60,32 @@ help ``` error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` - --> src/mdtest_snippet.py:10:1 + --> src/mdtest_snippet.py:10:13 | -10 | (A(),) == (A(),) - | ^^^^^^^^^^^^^^^^ +10 | reveal_type((A(),) == (A(),)) # revealed: bool + | ^^^^^^^^^^^^^^^^ + | +info: `__bool__` on `NotBoolable` must be callable + +``` + +``` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` + --> src/mdtest_snippet.py:12:13 + | +12 | reveal_type((A(), "x") == (A(), "y")) # revealed: Literal[False] + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | +info: `__bool__` on `NotBoolable` must be callable + +``` + +``` +error[unsupported-bool-conversion]: Boolean conversion is not supported for type `NotBoolable` + --> src/mdtest_snippet.py:14:13 + | +14 | reveal_type((A(),) != (A(), 0)) # revealed: Literal[True] + | ^^^^^^^^^^^^^^^^^^ | info: `__bool__` on `NotBoolable` must be callable diff --git a/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md b/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md index e48db14919..8df7cfac3a 100644 --- a/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md +++ b/crates/ty_python_semantic/resources/mdtest/statically_known_branches.md @@ -1034,7 +1034,7 @@ reveal_type(c) # revealed: Literal[1] python-version = "3.10" ``` -### Single-valued types, always true +### Literal subject, always true ```py x = 1 @@ -1048,7 +1048,7 @@ match "a": reveal_type(x) # revealed: Literal[2] ``` -### Single-valued types, always true, with wildcard pattern +### Literal subject, always true, with wildcard pattern ```py x = 1 @@ -1064,7 +1064,7 @@ match "a": reveal_type(x) # revealed: Literal[2] ``` -### Single-valued types, always true, with guard +### Literal subject, always true, with guard Make sure we don't infer a static truthiness in case there is a case guard: @@ -1085,7 +1085,7 @@ match "a": reveal_type(x) # revealed: Literal[1, 2] ``` -### Single-valued types, always false +### Literal subject, always false ```py x = 1 @@ -1099,7 +1099,7 @@ match "something else": reveal_type(x) # revealed: Literal[1] ``` -### Single-valued types, always false, with wildcard pattern +### Literal subject, always false, with wildcard pattern ```py x = 1 @@ -1115,7 +1115,7 @@ match "something else": reveal_type(x) # revealed: Literal[1] ``` -### Single-valued types, always false, with guard +### Literal subject, always false, with guard For definitely-false cases, the presence of a guard has no influence: @@ -1136,7 +1136,7 @@ match "something else": reveal_type(x) # revealed: Literal[1] ``` -### Non-single-valued types +### Broad subject type ```py def _(s: str): diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index 959ba055f8..bc5a1b4212 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -433,21 +433,6 @@ static_assert(not is_singleton(int)) static_assert(not is_singleton(Literal["a"])) ``` -### Single-valued types - -```py -from ty_extensions import static_assert -from ty_extensions._internal import is_single_valued -from typing import Literal - -static_assert(is_single_valued(None)) -static_assert(is_single_valued(Literal[True])) -static_assert(is_single_valued(Literal["a"])) - -static_assert(not is_single_valued(int)) -static_assert(not is_single_valued(Literal["a"] | Literal["b"])) -``` - ## `TypeOf` We use `TypeOf` to get the inferred type of an expression. This is useful when we want to refer to diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md index 98af644f8f..396d7dff34 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/integer_literals.md @@ -50,22 +50,9 @@ def f(x: int): reveal_type(x) # revealed: int ``` -## Integer `Literal`s are single-valued types +## Equality narrowing for integer `Literal`s -There is a slightly weaker property that integer literals have. They are single-valued types, which -means that all objects of the type have the same value, i.e. they compare equal to each other: - -```py -from ty_extensions import static_assert -from ty_extensions._internal import is_single_valued -from typing import Literal - -static_assert(is_single_valued(Literal[0])) -static_assert(is_single_valued(Literal[1])) -static_assert(is_single_valued(Literal[54165])) -``` - -And this can be used for type-narrowing using equality comparisons: +Integer literals can narrow types in equality comparisons: ```py def f(x: int): diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md deleted file mode 100644 index 9ff48c25a3..0000000000 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_single_valued.md +++ /dev/null @@ -1,142 +0,0 @@ -## Single-valued types - -A type is single-valued iff it is not empty and all inhabitants of it compare equal. - -```pyi -import types -from types import UnionType -from typing_extensions import Any, Literal, LiteralString, Never, Callable, TypeAliasType -from ty_extensions import static_assert -from ty_extensions._internal import TypeOf, is_single_valued - -static_assert(is_single_valued(None)) -static_assert(is_single_valued(Literal[True])) -static_assert(is_single_valued(Literal[1])) -static_assert(is_single_valued(Literal["abc"])) -static_assert(is_single_valued(Literal[b"abc"])) - -static_assert(is_single_valued(tuple[()])) -static_assert(is_single_valued(tuple[Literal[True], Literal[1]])) - -class EmptyTupleSubclass(tuple[()]): ... -class HeterogeneousTupleSubclass(tuple[Literal[True], Literal[1]]): ... - -# N.B. this follows from the fact that `EmptyTupleSubclass` is a subtype of `tuple[()]`, -# and any property recognised for `tuple[()]` should therefore also be recognised for -# `EmptyTupleSubclass` since an `EmptyTupleSubclass` instance can be used anywhere where -# `tuple[()]` is accepted. This is only sound, however, if we ban `__eq__` and `__ne__` -# from being overridden on a tuple subclass. This is something we plan to do as part of -# our implementation of the Liskov Substitution Principle -# (https://github.com/astral-sh/ty/issues/166) -static_assert(is_single_valued(EmptyTupleSubclass)) -static_assert(is_single_valued(HeterogeneousTupleSubclass)) - -static_assert(not is_single_valued(str)) -static_assert(not is_single_valued(Never)) -static_assert(not is_single_valued(Any)) - -static_assert(not is_single_valued(Literal[1, 2])) - -static_assert(not is_single_valued(tuple[None, int])) - -class MultiValuedHeterogeneousTupleSubclass(tuple[None, int]): ... - -static_assert(not is_single_valued(MultiValuedHeterogeneousTupleSubclass)) - -static_assert(not is_single_valued(Callable[..., None])) -static_assert(not is_single_valued(Callable[[int, str], None])) - -static_assert(not is_single_valued(TypeAliasType)) -static_assert(not is_single_valued(UnionType)) - -class A: - def method(self): ... - -# Binding the same method to different instances yields different objects: `[].sort != [].sort` -static_assert(not is_single_valued(TypeOf[A().method])) -static_assert(is_single_valued(TypeOf[types.FunctionType.__get__])) -static_assert(is_single_valued(TypeOf[A.method.__get__])) -``` - -An enum literal is only considered single-valued if it has no custom `__eq__`/`__ne__` method, or if -these methods always return `True`/`False`, respectively. Otherwise, the single member of the enum -literal type might not compare equal to itself. - -```pyi -from ty_extensions import static_assert -from ty_extensions._internal import TypeOf, is_single_valued -from enum import Enum - -class NormalEnum(Enum): - NO = 0 - YES = 1 - -class SingleValuedEnum(Enum): - VALUE = 1 - -class ComparesEqualEnum(Enum): - NO = 0 - YES = 1 - - def __eq__(self, other: object) -> Literal[True]: - return True - -class CustomEqEnum(Enum): - NO = 0 - YES = 1 - - def __eq__(self, other: object) -> bool: - return False - -class CustomNeEnum(Enum): - NO = 0 - YES = 1 - - def __ne__(self, other: object) -> bool: - return False - -class StrEnum(str, Enum): - A = "a" - B = "b" - -class IntEnum(int, Enum): - A = 1 - B = 2 - -static_assert(is_single_valued(Literal[NormalEnum.NO])) -static_assert(is_single_valued(Literal[NormalEnum.YES])) -static_assert(not is_single_valued(NormalEnum)) - -def _(value: NormalEnum) -> None: - if value is NormalEnum.NO: - return - static_assert(is_single_valued(TypeOf[value])) - -def _(value: NormalEnum & Any) -> None: - if value is NormalEnum.NO: - return - static_assert(not is_single_valued(TypeOf[value])) - -static_assert(is_single_valued(Literal[SingleValuedEnum.VALUE])) -static_assert(is_single_valued(SingleValuedEnum)) - -static_assert(is_single_valued(Literal[ComparesEqualEnum.NO])) -static_assert(is_single_valued(Literal[ComparesEqualEnum.YES])) -static_assert(not is_single_valued(ComparesEqualEnum)) - -static_assert(not is_single_valued(Literal[CustomEqEnum.NO])) -static_assert(not is_single_valued(Literal[CustomEqEnum.YES])) -static_assert(not is_single_valued(CustomEqEnum)) - -static_assert(not is_single_valued(Literal[CustomNeEnum.NO])) -static_assert(not is_single_valued(Literal[CustomNeEnum.YES])) -static_assert(not is_single_valued(CustomNeEnum)) - -static_assert(is_single_valued(Literal[StrEnum.A])) -static_assert(is_single_valued(Literal[StrEnum.B])) -static_assert(not is_single_valued(StrEnum)) - -static_assert(is_single_valued(Literal[IntEnum.A])) -static_assert(is_single_valued(Literal[IntEnum.B])) -static_assert(not is_single_valued(IntEnum)) -``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md index a2109f7a4f..3804903ef6 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md @@ -132,7 +132,7 @@ def f(x: Callable[..., Any], y: Callable[[int], str]): reveal_type(bool(y)) # revealed: bool ``` -But certain callable single-valued types are known to be always truthy: +But certain callable objects are known to be always truthy: ```py from types import FunctionType diff --git a/crates/ty_python_semantic/src/reachability.rs b/crates/ty_python_semantic/src/reachability.rs index 51a59d608c..34775a41d5 100644 --- a/crates/ty_python_semantic/src/reachability.rs +++ b/crates/ty_python_semantic/src/reachability.rs @@ -335,7 +335,7 @@ fn enum_literal_subject_names<'db>( /// Return the canonical enum-member name matched by a single value pattern. /// /// This recognizes patterns like `case Color.RED:` only when the pattern expression is -/// single-valued and belongs to the expected enum class. Enum aliases are resolved to their +/// an enum member belonging to the expected enum class. Enum aliases are resolved to their /// canonical member names before returning. fn enum_member_pattern_name<'db>( db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 87f32bca33..7e1e2fcd30 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1322,34 +1322,6 @@ impl<'db> Type<'db> { matches!(self, Type::SpecialForm(SpecialFormType::TypeAlias)) } - /// Return true if this type overrides __eq__ or __ne__ methods - fn overrides_equality(&self, db: &'db dyn Db) -> bool { - let check_dunder = |dunder_name, allowed_return_value| { - // Note that we do explicitly exclude dunder methods on `object`, `int` and `str` here. - // The reason for this is that we know that these dunder methods behave in a predictable way. - // Only custom dunder methods need to be examined here, as they might break single-valuedness - // by always returning `False`, for example. - let call_result = self.try_call_dunder_with_policy( - db, - dunder_name, - &mut CallArguments::positional([Type::unknown()]), - TypeContext::default(), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK - | MemberLookupPolicy::MRO_NO_INT_OR_STR_LOOKUP, - ); - let call_result = call_result.as_ref(); - call_result.is_ok_and(|bindings| { - bindings - .return_type(db) - .as_literal_value() - .and_then(literal::LiteralValueType::as_bool) - == Some(allowed_return_value) - }) || call_result.is_err_and(|err| matches!(err, CallDunderError::MethodNotAvailable)) - }; - - !(check_dunder("__eq__", true) && check_dunder("__ne__", false)) - } - pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { self.is_instance_of(db, KnownClass::NotImplementedType) } @@ -2494,7 +2466,7 @@ impl<'db> Type<'db> { false } Type::BoundMethod(..) => { - // `BoundMethod` types are single-valued types, but not singleton types: + // `BoundMethod` types are not singleton types: // ```pycon // >>> class Foo: // ... def bar(self): pass @@ -2532,102 +2504,6 @@ impl<'db> Type<'db> { } } - /// Return true if this type is non-empty and all inhabitants of this type compare equal. - pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { - match self { - Type::KnownInstance(KnownInstanceType::Sentinel(_)) => true, - Type::SpecialForm(special_form) => special_form.is_guaranteed_singleton(), - - // It's tempting to add extensive special casing here, but it would be very error-prone, - // and there's no known use case. For example, `Annotated[int, ""]` does not compare - // equal to `Annotated[int, "fooo"]` (but for us they have identical types); `list[int]` - // does not compare equal to `typing.List[int]` (but for us they have identical types); - // `typing.Literal` will not necessarily be the same object as `typing_extensions.Literal` - // even on Python versions where typeshed says they are the same symbol; etc. etc. - Type::KnownInstance(_) | Type::GenericAlias(_) => false, - - Type::FunctionLiteral(..) - | Type::WrapperDescriptor(_) - | Type::KnownBoundMethod(_) - | Type::ModuleLiteral(..) - | Type::ClassLiteral(..) => true, - - Type::LiteralValue(literal) => match literal.kind() { - LiteralValueTypeKind::Enum(..) => !self.overrides_equality(db), - - LiteralValueTypeKind::Int(..) - | LiteralValueTypeKind::String(..) - | LiteralValueTypeKind::Bytes(..) - | LiteralValueTypeKind::Bool(_) => true, - - LiteralValueTypeKind::LiteralString => false, - }, - - 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 - // `Never`. A constrained typevar is single-valued if all of its constraints are - // single-valued. (Note that you cannot specialize a constrained typevar to a subtype - // of a constraint.) - Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { - None => false, - Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| constraint.is_single_valued(db)), - } - } - - Type::SubclassOf(..) => { - // TODO: Same comment as above for `is_singleton` - false - } - - Type::NominalInstance(instance) => instance.is_single_valued(db), - Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).is_single_valued(db), - - Type::BoundSuper(_) => { - // At runtime two super instances never compare equal, even if their arguments are identical. - false - } - - Type::BoundMethod(_) => { - // Binding the same method to different instances yields different objects: `[].sort != [].sort` - false - } - - Type::TypeIs(type_is) => type_is.is_bound(db), - Type::TypeGuard(type_guard) => type_guard.is_bound(db), - Type::TypeForm(_) => false, - - Type::TypeAlias(alias) => alias.value_type(db).is_single_valued(db), - - Type::Dynamic(_) - | Type::Divergent(_) - | Type::Never - | Type::Union(..) - | Type::AlwaysTruthy - | Type::AlwaysFalsy - | Type::Callable(_) - | Type::PropertyInstance(_) - | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::TypedDict(_) => false, - - Type::Intersection(intersection) => intersection - .enum_complement(db) - .is_some_and(|complement| complement.is_single_valued(db)), - Type::EnumComplement(complement) => complement.is_single_valued(db), - } - } - /// This function is roughly equivalent to `find_name_in_mro` as defined in the [descriptor guide] or /// [`_PyType_Lookup`] in CPython's `Objects/typeobject.c`. It should typically be called through /// [`Type::class_member`], unless it is known that `self` is a class-like type. This function returns diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index d8ad60c669..5af16dac6f 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -2116,14 +2116,6 @@ impl<'db> Bindings<'db> { } } - Some(KnownFunction::IsSingleValued) => { - if let [Some(ty)] = overload.parameter_types() { - overload.set_return_type(Type::bool_literal( - ty.project_type_form(db).is_single_valued(db), - )); - } - } - Some(KnownFunction::GenericContext) => { if let [Some(ty)] = overload.parameter_types() { let wrap_generic_context = |generic_context| { diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs index 490d647449..90ff4f51d3 100644 --- a/crates/ty_python_semantic/src/types/class/known.rs +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -1418,124 +1418,6 @@ impl KnownClass { } } - /// Returns `Some(true)` if all instances of this `KnownClass` compare equal. - /// Returns `None` for `KnownClass::Tuple`, since whether or not a tuple type - /// is single-valued depends on the tuple spec. - pub(crate) const fn is_single_valued(self) -> Option { - match self { - Self::NoneType - | Self::NoDefaultType - | Self::EllipsisType - | Self::NotImplementedType => Some(true), - - Self::Bool - | Self::Object - | Self::Bytes - | Self::Bytearray - | Self::Memoryview - | Self::Range - | Self::Type - | Self::Int - | Self::Float - | Self::Complex - | Self::Str - | Self::List - | Self::Set - | Self::FrozenSet - | Self::Dict - | Self::Slice - | Self::Property - | Self::BaseException - | Self::BaseExceptionGroup - | Self::Exception - | Self::Warning - | Self::NotImplementedError - | Self::ExceptionGroup - | Self::Staticmethod - | Self::Classmethod - | Self::Awaitable - | Self::Generator - | Self::AsyncGenerator - | Self::Deprecated - | Self::GenericAlias - | Self::ModuleType - | Self::FunctionType - | Self::GeneratorType - | Self::AsyncGeneratorType - | Self::CoroutineType - | Self::MethodType - | Self::MethodWrapperType - | Self::WrapperDescriptorType - | Self::SpecialForm - | Self::ChainMap - | Self::Counter - | Self::DefaultDict - | Self::Deque - | Self::OrderedDict - | Self::VersionInfo - | Self::Hashable - | Self::SupportsIndex - | Self::StdlibAlias - | Self::TypeAliasType - | Self::TypeVar - | Self::ExtensionsTypeVar - | Self::ParamSpec - | Self::ExtensionsParamSpec - | Self::ParamSpecArgs - | Self::ParamSpecKwargs - | Self::TypeVarTuple - | Self::ExtensionsTypeVarTuple - | Self::Sentinel - | Self::Enum - | Self::EnumProperty - | Self::EnumType - | Self::Auto - | Self::Member - | Self::Nonmember - | Self::StrEnum - | Self::IntEnum - | Self::Flag - | Self::IntFlag - | Self::ABCMeta - | Self::Super - | Self::NewType - | Self::Field - | Self::KwOnly - | Self::Iterable - | Self::TyExtensionsAsyncIterable - | Self::TyExtensionsAsyncIterator - | Self::TyExtensionsIterable - | Self::Iterator - | Self::TyExtensionsIterator - | Self::AsyncIterator - | Self::Sequence - | Self::Mapping - | Self::MutableMapping - | Self::SupportsKeysAndGetItem - | Self::NamedTupleFallback - | Self::NamedTupleLike - | Self::ConstraintSet - | Self::ConstraintSetSolution - | Self::GenericContext - | Self::Specialization - | Self::TypedDictFallback - | Self::ExtensionTypedDictFallback - | Self::BuiltinFunctionType - | Self::ProtocolMeta - | Self::Template - | Self::Path - | Self::UnionType - | Self::FunctoolsPartial - | Self::PydanticBaseModel - | Self::PydanticBaseSettings - | Self::PydanticConfigDict - | Self::PydanticRootModel - | Self::PydanticStrict => Some(false), - - Self::Tuple => None, - } - } - /// Is this class a singleton class? /// /// A singleton class is a class where it is known that only one instance can ever exist at runtime. diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index b19d440ffe..66ea4a2d61 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -734,18 +734,6 @@ impl<'db> EnumComplementType<'db> { self.rest(db).is_empty() && self.remaining_member_count(db) == 1 } - /// Return `true` when this complement is a single value under equality narrowing. - /// - /// Enums that override equality are excluded because one remaining enum literal can still - /// compare equal to non-identical values. - pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { - self.is_singleton(db) - && !self - .enum_class(db) - .to_non_generic_instance(db) - .overrides_equality(db) - } - /// Expand this complement to the enum literals that remain possible. pub fn remaining_literal_types(self, db: &'db dyn Db) -> Vec> { self.remaining_member_names(db) diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index ebc85a049c..078871eb52 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -9,9 +9,9 @@ use rustc_hash::FxHashSet; use crate::{AnalysisSettings, Db, place::PlaceAndQualifiers}; use super::{ - EnumLiteralType, IntersectionBuilder, KnownBoundMethodType, KnownClass, LiteralValueType, - LiteralValueTypeKind, MemberLookupPolicy, Truthiness, Type, TypeVarBoundOrConstraints, - UnionBuilder, + CallArguments, EnumLiteralType, IntersectionBuilder, KnownBoundMethodType, KnownClass, + LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, Truthiness, Type, TypeContext, + TypeVarBoundOrConstraints, UnionBuilder, enums::{enum_member_literals, enum_metadata}, }; @@ -130,86 +130,28 @@ pub(super) fn evaluate_type_equality<'db>( is_positive: bool, soundness_policy: ComparisonSoundnessPolicy, ) -> Option> { - let right = right.resolve_type_alias(db); - - // Preserve the shared specialization of a constrained TypeVar. Expanding the TypeVar before - // comparing it with `left` would lose the correlation with other occurrences in the function. - if is_positive - && let Type::TypeVar(typevar) = right - && let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = - typevar.typevar(db).bound_or_constraints(db) - && constraints.elements(db).iter().all(|constraint| { - evaluate_type_equality(db, left, *constraint, true, soundness_policy) - .is_some_and(|narrowed| narrowed.is_equivalent_to(db, *constraint)) - }) - { - return Some(right); - } - - let branch = ComparisonBranch::from(is_positive); - let condition_expects_equality = - ComparisonOperator::Equality.condition_expects_equality(branch); - enum_literal_constraint( + evaluate_type_comparison( db, left, right, + is_positive, ComparisonOperator::Equality, - condition_expects_equality, + soundness_policy, ) - .or_else(|| { - builtin_literal_constraint( - db, - left, - right, - ComparisonOperator::Equality, - condition_expects_equality, - ) - }) - .or_else(|| { - evaluate_enum_comparison( - &mut ComparisonEvaluator::new(db, soundness_policy), - left, - right, - branch, - ComparisonOperator::Equality, - ) - .and_then(|result| result.constraint(branch)) - }) - .or_else(|| { - if comparison_domain( - db, - left, - right, - ComparisonOperator::Equality, - soundness_policy, - ) == ComparisonDomain::Known - { - ComparisonEvaluator::new(db, soundness_policy) - .evaluate(left, right, branch, ComparisonOperator::Equality) - .constraint(branch) - } else { - None - } - }) } /// Return a constraint excluding every value known to compare equal to `ty`. pub(super) fn equality_exclusion_constraint<'db>( db: &'db dyn Db, ty: Type<'db>, + soundness_policy: ComparisonSoundnessPolicy, ) -> Option> { let ty = ty.resolve_type_alias(db); - builtin_literal_constraint(db, ty, ty, ComparisonOperator::Equality, false) - .or_else(|| ty.is_single_valued(db).then(|| ty.negate(db))) - .or_else(|| { - (ComparisonEvaluator::conservative(db).evaluate( - ty, - ty, - ComparisonBranch::Positive, - ComparisonOperator::Equality, - ) == ComparisonResult::AlwaysTrue) - .then(|| ty.negate(db)) - }) + builtin_literal_constraint(db, ty, ty, ComparisonOperator::Equality, false).or_else(|| { + let mut evaluator = ComparisonEvaluator::new(db, soundness_policy); + all_values_compare_equal(&mut evaluator, ty, ComparisonOperator::Equality) + .then(|| ty.negate(db)) + }) } /// Return a constraint for `left` in a branch where `left != right` has the given truthiness. @@ -238,46 +180,63 @@ pub(super) fn evaluate_type_inequality<'db>( right: Type<'db>, is_positive: bool, soundness_policy: ComparisonSoundnessPolicy, +) -> Option> { + evaluate_type_comparison( + db, + left, + right, + is_positive, + ComparisonOperator::Inequality, + soundness_policy, + ) +} + +/// Return a constraint for `left` in the selected branch of an equality or inequality comparison. +fn evaluate_type_comparison<'db>( + db: &'db dyn Db, + left: Type<'db>, + right: Type<'db>, + is_positive: bool, + operator: ComparisonOperator, + soundness_policy: ComparisonSoundnessPolicy, ) -> Option> { let right = right.resolve_type_alias(db); + let branch = ComparisonBranch::from(is_positive); + let condition_expects_equality = operator.condition_expects_equality(branch); - // Preserve the shared specialization of a constrained TypeVar when `left != right` is false. - if !is_positive + // Preserve the shared specialization of a constrained TypeVar. Expanding it before comparing + // with `left` would lose the correlation with other occurrences in the function. + if condition_expects_equality && let Type::TypeVar(typevar) = right && let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = typevar.typevar(db).bound_or_constraints(db) && constraints.elements(db).iter().all(|constraint| { - evaluate_type_inequality(db, left, *constraint, false, soundness_policy) - .is_some_and(|narrowed| narrowed.is_equivalent_to(db, *constraint)) + evaluate_type_comparison( + db, + left, + *constraint, + is_positive, + operator, + soundness_policy, + ) + .is_some_and(|narrowed| { + equality_truthiness(db, narrowed, *constraint, soundness_policy) + == Truthiness::AlwaysTrue + }) }) { return Some(right); } - let branch = ComparisonBranch::from(is_positive); - let condition_expects_equality = - ComparisonOperator::Inequality.condition_expects_equality(branch); - enum_literal_constraint( - db, - left, - right, - ComparisonOperator::Inequality, - condition_expects_equality, - ) - .or_else(|| { - builtin_literal_constraint( - db, - left, - right, - ComparisonOperator::Inequality, - condition_expects_equality, - ) - }) - .or_else(|| { - ComparisonEvaluator::new(db, soundness_policy) - .evaluate(left, right, branch, ComparisonOperator::Inequality) - .constraint(branch) - }) + enum_literal_constraint(db, left, right, operator, condition_expects_equality) + .or_else(|| { + builtin_literal_constraint(db, left, right, operator, condition_expects_equality) + }) + .or_else(|| { + ComparisonEvaluator::new(db, soundness_policy) + .evaluate(left, right, branch, operator) + .constraint(branch) + }) } /// Return the truthiness of `left == right` when it is known for every represented runtime value. @@ -316,6 +275,35 @@ pub(super) fn inequality_truthiness<'db>( ) } +/// Evaluates tuple-element equality while reusing the active-comparison-set allocation across a +/// tuple walk. The set only detects recursive comparisons; results are not cached between +/// elements. +pub(super) struct TupleEqualityEvaluator<'db> { + evaluator: ComparisonEvaluator<'db>, +} + +impl<'db> TupleEqualityEvaluator<'db> { + pub(super) fn new(db: &'db dyn Db, soundness_policy: ComparisonSoundnessPolicy) -> Self { + Self { + evaluator: ComparisonEvaluator::for_truthiness(db, soundness_policy), + } + } + + pub(super) fn element_truthiness( + &mut self, + left: Type<'db>, + right: Type<'db>, + inferred_truthiness: Truthiness, + ) -> Truthiness { + // Identity can turn a false equality result true, but cannot turn a true result false. + if inferred_truthiness == Truthiness::AlwaysTrue { + return Truthiness::AlwaysTrue; + } + + evaluate_tuple_element_equality(&mut self.evaluator, left, right) + } +} + fn comparison_truthiness<'db>( db: &'db dyn Db, left: Type<'db>, @@ -371,7 +359,7 @@ pub(crate) struct ComparisonSoundnessPolicy { } impl ComparisonSoundnessPolicy { - const CONSERVATIVE: Self = Self { + pub(crate) const CONSERVATIVE: Self = Self { allow_unsafe_equality: false, }; @@ -411,10 +399,6 @@ impl<'db> ComparisonEvaluator<'db> { } } - fn conservative(db: &'db dyn Db) -> Self { - Self::new(db, ComparisonSoundnessPolicy::CONSERVATIVE) - } - fn for_truthiness(db: &'db dyn Db, soundness_policy: ComparisonSoundnessPolicy) -> Self { Self { db, @@ -589,6 +573,34 @@ fn evaluate_structural_comparison<'db>( | Type::TypeIs(_), ) => ComparisonResult::Ambiguous, + (Type::Dynamic(_), other) => { + if !operator.condition_expects_equality(branch) + && all_values_compare_equal(evaluator, other, operator) + { + ComparisonResult::CanNarrow( + IntersectionBuilder::new(db) + .add_positive(left) + .add_negative(other) + .build(), + ) + } else { + ComparisonResult::Ambiguous + } + } + (_, Type::Dynamic(_)) => ComparisonResult::Ambiguous, + + // A constrained TypeVar selects one constraint for the entire specialization, so each + // alternative can be checked independently without losing that correlation. + (Type::TypeVar(left_var), Type::TypeVar(right_var)) + if left_var.is_same_typevar_as(db, right_var) + && let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = + left_var.typevar(db).bound_or_constraints(db) + && constraints.elements(db).iter().all(|constraint| { + all_values_compare_equal(evaluator, *constraint, operator) + }) => + { + operator.result_from_equality(true) + } (Type::TypeVar(var), other) => match var.typevar(db).bound_or_constraints(db) { None => ComparisonResult::Ambiguous, Some(TypeVarBoundOrConstraints::UpperBound(_)) => { @@ -690,13 +702,6 @@ fn evaluate_structural_comparison<'db>( Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderCall(left_function)), Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderCall(right_function)), ) if left_function == right_function => operator.result_from_equality(true), - (Type::KnownInstance(left_instance), Type::KnownInstance(right_instance)) - if left_instance == right_instance - && left.is_single_valued(db) - && operator == ComparisonOperator::Equality => - { - ComparisonResult::AlwaysTrue - } (left, right) if has_known_identity_comparison_semantics(db, left, operator) && has_known_identity_comparison_semantics(db, right, operator) => @@ -708,6 +713,15 @@ fn evaluate_structural_comparison<'db>( compare_nominal_instances(evaluator, left_instance, right_instance, operator) } + (left, right) + if left.is_singleton(db) + && left.is_equivalent_to(db, right) + && KnownComparisonSemantics::of_type(db, left, operator) + == Some(KnownComparisonSemantics::Object) => + { + operator.result_from_equality(true) + } + _ => ComparisonResult::Ambiguous, } } @@ -1340,6 +1354,9 @@ fn compare_literal_to_other<'db>( Some(other_semantics) if literal_semantics != other_semantics => { ComparisonResult::from_bool(operator == ComparisonOperator::Inequality) } + // Object equality compares identity. `NewType` operands are evaluated using their concrete + // base before reaching this arm, so erased identities cannot make these types appear + // disjoint here. Some(KnownComparisonSemantics::Object) if literal_semantics == KnownComparisonSemantics::Object && other.is_disjoint_from(db, literal_type) => @@ -1350,16 +1367,12 @@ fn compare_literal_to_other<'db>( // `int` subclass can compare equal to `1` despite being disjoint from `Literal[1]`. Some(_) if literal_operand == LiteralOperand::Other - && literal_type.is_single_valued(db) && !other.is_disjoint_from(db, literal_type) => { ComparisonResult::CanNarrow(literal_type.negate_if(db, !condition_expects_equality)) } Some(_) => ComparisonResult::Ambiguous, - None if literal_operand == LiteralOperand::Other - && !condition_expects_equality - && literal_type.is_single_valued(db) => - { + None if literal_operand == LiteralOperand::Other && !condition_expects_equality => { ComparisonResult::CanNarrow(literal_type.negate(db)) } None => ComparisonResult::Ambiguous, @@ -1369,9 +1382,9 @@ fn compare_literal_to_other<'db>( /// Compare nominal instances when their inherited comparison implementations are known. /// /// The result is definite only when the implementations cannot compare equal, or when both types -/// denote the same singleton. +/// denote the same singleton, or when their fixed tuple elements have a definite comparison. fn compare_nominal_instances<'db>( - evaluator: &ComparisonEvaluator<'db>, + evaluator: &mut ComparisonEvaluator<'db>, left_instance: super::NominalInstanceType<'db>, right_instance: super::NominalInstanceType<'db>, operator: ComparisonOperator, @@ -1394,11 +1407,69 @@ fn compare_nominal_instances<'db>( if left == right && left.is_singleton(db) { ComparisonResult::from_bool(operator == ComparisonOperator::Equality) + } else if left_semantics == KnownComparisonSemantics::Tuple + && let Some(left_tuple) = left_instance.tuple_spec(db) + && let Some(right_tuple) = right_instance.tuple_spec(db) + && let Some(left_tuple) = left_tuple.as_fixed_length() + && let Some(right_tuple) = right_tuple.as_fixed_length() + { + let left_elements = left_tuple.all_elements(); + let right_elements = right_tuple.all_elements(); + if left_elements.len() != right_elements.len() { + return operator.result_from_equality(false); + } + + let mut all_equal = true; + for (&left, &right) in left_elements.iter().zip(right_elements) { + match evaluate_tuple_element_equality(evaluator, left, right) { + Truthiness::AlwaysTrue => {} + Truthiness::AlwaysFalse => return operator.result_from_equality(false), + Truthiness::Ambiguous => all_equal = false, + } + } + + if all_equal { + operator.result_from_equality(true) + } else { + ComparisonResult::Ambiguous + } } else { ComparisonResult::Ambiguous } } +fn evaluate_tuple_element_equality<'db>( + evaluator: &mut ComparisonEvaluator<'db>, + left: Type<'db>, + right: Type<'db>, +) -> Truthiness { + let db = evaluator.db; + + if left == right && left.is_singleton(db) { + return Truthiness::AlwaysTrue; + } + + match evaluator.evaluate( + left, + right, + ComparisonBranch::Positive, + ComparisonOperator::Equality, + ) { + ComparisonResult::AlwaysTrue => Truthiness::AlwaysTrue, + // Known comparison semantics are reflexive, so a false result rules out shared runtime + // identity. Static disjointness alone is insufficient because `NewType` and similar + // wrappers can erase their distinction at runtime. + ComparisonResult::AlwaysFalse + if [left, right] + .into_iter() + .all(|ty| has_reflexive_equality_semantics(evaluator, ty)) => + { + Truthiness::AlwaysFalse + } + _ => Truthiness::Ambiguous, + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] enum ComparisonOperator { Equality, @@ -1492,6 +1563,15 @@ impl KnownComparisonSemantics { { Self::of_instance(db, ty, operator) } + Type::SpecialForm(special_form) => KnownComparisonSemantics::of_type_with_policy( + db, + special_form.instance_fallback(db), + operator, + soundness_policy, + ), + Type::KnownInstance(instance) => { + KnownComparisonSemantics::of_instance(db, instance.instance_fallback(db), operator) + } _ => None, } } @@ -1526,10 +1606,15 @@ impl KnownComparisonSemantics { let dunder = lookup_dunder(db, class, operator.dunder()); if dunder.place.is_undefined() { - if operator == ComparisonOperator::Inequality - && !lookup_dunder(db, class, "__eq__").place.is_undefined() - { - return None; + if operator == ComparisonOperator::Inequality { + let equality = lookup_dunder(db, class, "__eq__"); + // `tuple.__ne__` delegates to its builtin equality implementation. + if equality == lookup_dunder(db, KnownClass::Tuple.to_class_literal(db), "__eq__") { + return Some(Self::Tuple); + } + if !equality.place.is_undefined() { + return None; + } } return Some(Self::Object); } @@ -1549,67 +1634,14 @@ impl KnownComparisonSemantics { } } -/// Whether the non-target operand has a comparison domain that can safely constrain the target. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum ComparisonDomain { - /// The operand may use comparison behavior that `ty` does not model. - Unknown, - /// The operand can be handled by `ty`'s equality-narrowing evaluator. - Known, -} - -/// Classify whether `ty` has comparison behavior that can constrain `target`. -/// -/// Unions only have a known domain if every arm does. Broad nominal types require full dunder -/// analysis, which is only useful here when it can eliminate an arm from a union target. -fn comparison_domain<'db>( - db: &'db dyn Db, - target: Type<'db>, +/// Return whether equality on `ty` is reflexive and therefore rules out shared identity when false. +fn has_reflexive_equality_semantics<'db>( + evaluator: &ComparisonEvaluator<'db>, ty: Type<'db>, - operator: ComparisonOperator, - soundness_policy: ComparisonSoundnessPolicy, -) -> ComparisonDomain { - let target = target.resolve_type_alias(db); - let ty = ty.resolve_type_alias(db); - - match ty { - Type::Union(union) => { - if union.elements(db).iter().all(|element| { - comparison_domain(db, target, *element, operator, soundness_policy) - == ComparisonDomain::Known - }) { - ComparisonDomain::Known - } else { - ComparisonDomain::Unknown - } - } - Type::LiteralValue(_) | Type::EnumComplement(_) | Type::TypedDict(_) => { - ComparisonDomain::Known - } - Type::Intersection(intersection) if intersection.enum_complement(db).is_some() => { - ComparisonDomain::Known - } - Type::NominalInstance(instance) => { - if instance.tuple_spec(db).is_some() - || ty.is_singleton(db) - || instance.has_known_class(db, KnownClass::Bool) - || target.is_union() - && KnownComparisonSemantics::of_type_with_policy( - db, - ty, - operator, - soundness_policy, - ) - .is_some() - { - ComparisonDomain::Known - } else { - ComparisonDomain::Unknown - } - } - _ if ty.is_single_valued(db) => ComparisonDomain::Known, - _ => ComparisonDomain::Unknown, - } +) -> bool { + evaluator + .comparison_semantics(ty, ComparisonOperator::Equality) + .is_some() } /// Return whether `ty` is a singleton whose comparison uses object identity semantics. @@ -1620,7 +1652,6 @@ fn has_known_identity_comparison_semantics<'db>( ) -> bool { match ty { Type::FunctionLiteral(_) | Type::ModuleLiteral(_) => true, - Type::SpecialForm(special_form) => special_form.is_guaranteed_singleton(), Type::ClassLiteral(class) => { KnownComparisonSemantics::of_instance(db, class.metaclass_instance_type(db), operator) == Some(KnownComparisonSemantics::Object) @@ -1644,14 +1675,36 @@ fn lookup_dunder<'db>( /// Return the comparison result for two literals when their runtime values determine it. /// -/// This accounts for integer/boolean equality and enum aliases or enum values. `None` means custom -/// or insufficiently known comparison behavior prevents a definitive result. +/// This accounts for integer/boolean equality, enum aliases or enum values, and reflexive custom +/// enum comparison methods with a definite return type. `None` means comparison behavior is +/// insufficiently known to produce a definitive result. fn known_literal_equality<'db>( db: &'db dyn Db, left: LiteralValueTypeKind<'db>, right: LiteralValueTypeKind<'db>, operator: ComparisonOperator, ) -> Option { + if let (LiteralValueTypeKind::Enum(left_enum), LiteralValueTypeKind::Enum(right_enum)) = + (left, right) + && same_enum_member(db, left_enum, right_enum) + && KnownComparisonSemantics::of_instance(db, left_enum.enum_class_instance(db), operator) + .is_none() + && let Ok(bindings) = Type::enum_literal(left_enum).try_call_dunder_with_policy( + db, + operator.dunder(), + &mut CallArguments::positional([Type::unknown()]), + TypeContext::default(), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK + | MemberLookupPolicy::MRO_NO_INT_OR_STR_LOOKUP, + ) + && let Some(result) = bindings + .return_type(db) + .as_literal_value() + .and_then(LiteralValueType::as_bool) + { + return Some(result == (operator == ComparisonOperator::Equality)); + } + match (left, right) { (LiteralValueTypeKind::Int(left), LiteralValueTypeKind::Int(right)) => { Some(left.as_i64() == right.as_i64()) diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 0486a2b92a..ab6bffe67e 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -2244,8 +2244,6 @@ pub enum KnownFunction { IsDisjointFrom, /// `ty_extensions._internal.is_singleton` IsSingleton, - /// `ty_extensions._internal.is_single_valued` - IsSingleValued, /// `ty_extensions._internal.generic_context` GenericContext, /// `ty_extensions._internal.into_callable` @@ -2353,7 +2351,6 @@ impl KnownFunction { | Self::IsConstraintSetAssignableTo | Self::IsDisjointFrom | Self::IsEquivalentTo - | Self::IsSingleValued | Self::IsSingleton | Self::IsSubtypeOf | Self::GenericContext @@ -2918,7 +2915,6 @@ pub(crate) mod tests { | KnownFunction::DunderAllNames | KnownFunction::EnumMembers | KnownFunction::IsDisjointFrom - | KnownFunction::IsSingleValued | KnownFunction::IsAssignableTo | KnownFunction::IsConstraintSetAssignableTo | KnownFunction::IsEquivalentTo diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index b0416bb585..4ae0e82246 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -8,7 +8,7 @@ use crate::types::constraints::ConstraintSetBuilder; use crate::types::context::InferContext; use crate::types::cyclic::CycleDetector; use crate::types::equality::{ - ComparisonSoundnessPolicy, equality_truthiness, inequality_truthiness, + ComparisonSoundnessPolicy, TupleEqualityEvaluator, equality_truthiness, inequality_truthiness, }; use crate::types::tuple::TupleSpec; use crate::types::{ @@ -319,6 +319,56 @@ fn infer_binary_type_comparison_inner<'db>( let soundness_policy = ComparisonSoundnessPolicy::from_analysis_settings(db.analysis_settings(context.file())); + + if let NonIdentityOperator::Rich(rich_op) = op + && let Some(left_tuple) = left.tuple_instance_spec(db) + && let Some(right_tuple) = right.tuple_instance_spec(db) + { + return visitor.visit(db, (left, op, right), || { + infer_tuple_rich_comparison(context, &left_tuple, rich_op, &right_tuple, range, visitor) + }); + } + + if let NonIdentityOperator::Membership(op) = op + && left.tuple_instance_spec(db).is_some() + && let Some(right_tuple) = right.tuple_instance_spec(db) + { + let mut any_eq = false; + let mut any_ambiguous = false; + + for ty in right_tuple.iter_element_types(db) { + let eq_result = infer_binary_type_comparison_inner( + context, + left, + NonIdentityOperator::Rich(RichCompareOperator::Eq), + ty, + range, + visitor, + ) + .expect("equality comparisons should always be supported"); + + match eq_result { + todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo), + // It's okay to ignore errors here because Python doesn't call `__bool__` + // for different union variants. Instead, this is just for us to + // evaluate a possibly truthy value to `false` or `true`. + ty => match ty.bool(db) { + Truthiness::AlwaysTrue => any_eq = true, + Truthiness::AlwaysFalse => (), + Truthiness::Ambiguous => any_ambiguous = true, + }, + } + } + + return Ok(if any_eq { + Type::bool_literal(op.is_in()) + } else if !any_ambiguous { + Type::bool_literal(op.is_not_in()) + } else { + KnownClass::Bool.to_instance(db) + }); + } + let comparison_truthiness = match op { NonIdentityOperator::Rich(RichCompareOperator::Eq) => { equality_truthiness(db, left, right, soundness_policy) @@ -760,8 +810,7 @@ fn infer_binary_type_comparison_inner<'db>( let constraints = ConstraintSetBuilder::new(); let left = constraints.load(db, left.constraints(db)); let right = constraints.load(db, right.constraints(db)); - let result = left.iff(db, &constraints, right); - let equivalent = result.is_always_satisfied(db); + let equivalent = left.iff(db, &constraints, right).is_always_satisfied(db); match op { NonIdentityOperator::Rich(RichCompareOperator::Eq) => { Some(Ok(Type::bool_literal(equivalent))) @@ -773,61 +822,6 @@ fn infer_binary_type_comparison_inner<'db>( } } - (Type::NominalInstance(nominal1), Type::NominalInstance(nominal2)) - if let Some(lhs_tuple) = nominal1.tuple_spec(db) - && let Some(rhs_tuple) = nominal2.tuple_spec(db) => - { - let tuple_rich_comparison = |rich_op| { - visitor.visit(db, (left, op, right), || { - infer_tuple_rich_comparison( - context, &lhs_tuple, rich_op, &rhs_tuple, range, visitor, - ) - }) - }; - - let result = match op { - NonIdentityOperator::Rich(rich_op) => tuple_rich_comparison(rich_op), - NonIdentityOperator::Membership(membership_op) => { - let mut any_eq = false; - let mut any_ambiguous = false; - - for ty in rhs_tuple.iter_element_types(db) { - let eq_result = infer_binary_type_comparison_inner( - context, - left, - NonIdentityOperator::Rich(RichCompareOperator::Eq), - ty, - range, - visitor, - ) - .expect("infer_binary_type_comparison should never return None for `==`"); - - match eq_result { - todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo), - // It's okay to ignore errors here because Python doesn't call `__bool__` - // for different union variants. Instead, this is just for us to - // evaluate a possibly truthy value to `false` or `true`. - ty => match ty.bool(db) { - Truthiness::AlwaysTrue => any_eq = true, - Truthiness::AlwaysFalse => (), - Truthiness::Ambiguous => any_ambiguous = true, - }, - } - } - - if any_eq { - Ok(Type::bool_literal(membership_op.is_in())) - } else if !any_ambiguous { - Ok(Type::bool_literal(membership_op.is_not_in())) - } else { - Ok(KnownClass::Bool.to_instance(db)) - } - } - }; - - Some(result) - } - _ => None, }; @@ -1126,6 +1120,10 @@ fn infer_tuple_rich_comparison<'db>( let right_iter = right.iter_all_elements(); let mut builder = UnionBuilder::new(db); + let soundness_policy = ComparisonSoundnessPolicy::from_analysis_settings( + db.analysis_settings(context.file()), + ); + let mut equality = TupleEqualityEvaluator::new(db, soundness_policy); for (l_ty, r_ty) in left_iter.zip(right_iter) { let pairwise_eq_result = infer_binary_type_comparison_inner( @@ -1138,12 +1136,16 @@ fn infer_tuple_rich_comparison<'db>( ) .expect("infer_binary_type_comparison should never return None for `==`"); - match pairwise_eq_result.try_bool(db).unwrap_or_else(|err| { + let inferred_truthiness = pairwise_eq_result.try_bool(db).unwrap_or_else(|err| { // TODO: We should, whenever possible, pass the range of the left and right elements // instead of the range of the whole tuple. err.report_diagnostic(context, range); - err.fallback_truthiness() - }) { + Truthiness::Ambiguous + }); + + let eq_truthiness = equality.element_truthiness(l_ty, r_ty, inferred_truthiness); + + match eq_truthiness { // - AlwaysTrue : Continue to the next pair for lexicographic comparison Truthiness::AlwaysTrue => continue, // - AlwaysFalse: @@ -1166,7 +1168,8 @@ fn infer_tuple_rich_comparison<'db>( range, visitor, )?, - // For `==` and `!=`, we already figure out the result from `pairwise_eq_result` + // For `==` and `!=`, the equality evaluator has already determined + // that these elements may differ. // NOTE: The CPython implementation does not account for non-boolean return types // or cases where `!=` is not the negation of `==`, we also do not consider these cases. RichCompareOperator::Eq => Type::bool_literal(false), diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index af6363d1b3..d56e4e899a 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -428,20 +428,6 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn is_single_valued(self, db: &'db dyn Db) -> bool { - match self.0 { - NominalInstanceInner::ExactTuple(tuple) => tuple.is_single_valued(db), - NominalInstanceInner::Object => false, - NominalInstanceInner::SysVersionInfo => true, - NominalInstanceInner::NonTuple(class) => class - .class(db) - .known(db) - .and_then(KnownClass::is_single_valued) - .or_else(|| Some(self.tuple_spec(db)?.is_single_valued(db))) - .unwrap_or_else(|| is_single_member_enum(db, class.class(db).class_literal(db))), - } - } - pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { SubclassOfType::from(db, self.class(db)) } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index ccf162da05..148e3efb16 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -2981,7 +2981,11 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // `not in` negates equality with every element; it does not use `__ne__`. Only add an // exclusion when every value represented by a slot is known to compare equal. for element_ty in fixed_length.all_elements().iter().copied() { - if let Some(constraint) = equality_exclusion_constraint(self.db, element_ty) { + if let Some(constraint) = equality_exclusion_constraint( + self.db, + element_ty, + self.comparison_soundness_policy(), + ) { builder = builder.add_positive(constraint); constrained = true; } diff --git a/crates/ty_python_semantic/src/types/property_tests.rs b/crates/ty_python_semantic/src/types/property_tests.rs index cf7d63fcbb..c24231235c 100644 --- a/crates/ty_python_semantic/src/types/property_tests.rs +++ b/crates/ty_python_semantic/src/types/property_tests.rs @@ -145,12 +145,6 @@ mod stable { forall types s, t. s.is_subtype_of(db, t) => s.is_assignable_to(db, t) ); - // If `T` is a singleton, it is also single-valued. - type_property_test!( - singleton_implies_single_valued, db, - forall types t. t.is_singleton(db) => t.is_single_valued(db) - ); - // All types should be assignable to `object` type_property_test!( all_types_assignable_to_object, db, diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index a20b9b411c..0f4ef6cfbf 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -2857,10 +2857,9 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { !left_sentinel.is_same_sentinel(db, right_sentinel), ), - // any single-valued type is disjoint from another single-valued type - // iff the two types are nonequal + // These types are disjoint whenever their represented objects differ. ( - // note `LiteralString` is not single-valued, but we handle the special case above + // `LiteralString` can represent different strings and is handled above. left @ (Type::FunctionLiteral(..) | Type::KnownBoundMethod(..) | Type::WrapperDescriptor(..) diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 029493ff07..6ad3c50045 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -297,10 +297,6 @@ impl<'db> TupleType<'db> { self.tuple(db) .find_legacy_typevars_impl(db, binding_context, typevars, visitor); } - - pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { - self.tuple(db).is_single_valued(db) - } } impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { @@ -942,10 +938,6 @@ impl<'db> FixedLengthTuple> { ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); } } - - fn is_single_valued(&self, db: &'db dyn Db) -> bool { - self.0.iter().all(|ty| ty.is_single_valued(db)) - } } impl<'db> PyIndex<'db> for &FixedLengthTuple> { @@ -2431,13 +2423,6 @@ impl<'db> Tuple, VariableSegment<'db>> { } } - pub(crate) fn is_single_valued(&self, db: &'db dyn Db) -> bool { - match self { - Tuple::Fixed(tuple) => tuple.is_single_valued(db), - Tuple::Variable(_) => false, - } - } - /// Calls a closure for each pair of elements that could potentially be compared at runtime /// between `self` and `other`. /// diff --git a/crates/ty_vendored/ty_extensions/_internal.pyi b/crates/ty_vendored/ty_extensions/_internal.pyi index 31146606b1..9a612c4d81 100644 --- a/crates/ty_vendored/ty_extensions/_internal.pyi +++ b/crates/ty_vendored/ty_extensions/_internal.pyi @@ -239,9 +239,6 @@ def is_disjoint_from( def is_singleton(ty: TypeForm[object]) -> bool: """Returns `True` if `ty` is a singleton type with exactly one inhabitant.""" -def is_single_valued(ty: TypeForm[object]) -> bool: - """Returns `True` if `ty` is non-empty and all inhabitants compare equal to each other.""" - # ------------------- # Operations on types # ------------------- From e057c213e54cfdd8320d959bc9fe4a25e3304d7b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 27 Jul 2026 18:21:04 +0100 Subject: [PATCH 082/390] [ty] Improve tuple membership and rich comparison inference (#27164) Stacked on #26992. ## Summary - Infer fixed-length tuple membership for scalar operands and empty tuples while keeping variable-length tuples conservative. - Reuse tuple-element equality evaluation for membership and lexicographic comparisons, accounting for identity-before-equality, custom comparison methods, and correlated enum/type-variable values. - Centralize rich-comparison dispatch and preserve both possible return types when reflected subclass methods may take precedence. --- .../comparison/instances/rich_comparison.md | 16 ++-- .../resources/mdtest/comparison/tuples.md | 90 ++++++++++++++++++ .../mdtest/narrow/conditionals/in.md | 4 +- crates/ty_python_semantic/src/types/call.rs | 46 +++++++++ .../ty_python_semantic/src/types/equality.rs | 28 ++++-- .../src/types/infer/comparisons.rs | 93 +++++++------------ 6 files changed, 200 insertions(+), 77 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md index f880106c0f..6eed9d7338 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md @@ -165,7 +165,9 @@ reveal_type(C() <= C()) # revealed: NeReturnType When subclasses override comparison methods, these overridden methods take precedence over those in the parent class. Class `B` inherits from `A` and redefines comparison methods to return types other -than `A`. +than `A`. However, because `A` is not final, its instances could have runtime classes other than +`A`. The reflected method therefore has priority only for some possible operands, so both methods' +return types are included in the result. ```py from __future__ import annotations @@ -215,14 +217,14 @@ class B(A): def __ge__(self, other: A) -> GeReturnType: # error: [invalid-method-override] return GeReturnType() -reveal_type(A() == B()) # revealed: EqReturnType -reveal_type(A() != B()) # revealed: NeReturnType +reveal_type(A() == B()) # revealed: A | EqReturnType +reveal_type(A() != B()) # revealed: A | NeReturnType -reveal_type(A() < B()) # revealed: GtReturnType -reveal_type(A() <= B()) # revealed: GeReturnType +reveal_type(A() < B()) # revealed: A | GtReturnType +reveal_type(A() <= B()) # revealed: A | GeReturnType -reveal_type(A() > B()) # revealed: LtReturnType -reveal_type(A() >= B()) # revealed: LeReturnType +reveal_type(A() > B()) # revealed: A | LtReturnType +reveal_type(A() >= B()) # revealed: A | LeReturnType ``` ## Reflected Comparisons with Subclass But Falls Back to LHS diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md b/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md index 8fd77c122c..5196cbfba9 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md @@ -324,6 +324,96 @@ def _(n: int): reveal_type(a not in d) # revealed: bool ``` +Membership in a fixed-length tuple compares each element with the needle, regardless of whether the +needle is itself a tuple: + +```py +from typing import Literal + +def scalar_membership(value: int, values: tuple[Literal[1], Literal[2]]): + reveal_type(1 in values) # revealed: Literal[True] + reveal_type(3 in values) # revealed: Literal[False] + reveal_type(value in values) # revealed: bool + + reveal_type(1 not in values) # revealed: Literal[False] + reveal_type(3 not in values) # revealed: Literal[True] + reveal_type(value not in values) # revealed: bool + +def empty_tuple(value: object, values: tuple[()]): + reveal_type(value in values) # revealed: Literal[False] + reveal_type(value not in values) # revealed: Literal[True] +``` + +A variable-length tuple might be empty, even if all its possible elements would compare equal to the +needle: + +```py +from typing import Literal + +def variable_length( + nested: tuple[tuple[()], ...], + values: tuple[Literal[1], ...], +): + reveal_type(() in nested) # revealed: bool + reveal_type(() not in nested) # revealed: bool + + reveal_type(1 in values) # revealed: bool + reveal_type(1 not in values) # revealed: bool +``` + +Tuple membership checks whether the needle is the same object as an element before comparing the +objects for equality. A non-reflexive equality method therefore cannot establish that membership is +always false: + +```py +from typing import Literal + +class NeverEqual: + def __eq__(self, other: object) -> Literal[False]: + return False + +class AlwaysEqual: + def __eq__(self, other: object) -> Literal[True]: + return True + +def identity_before_equality(value: NeverEqual): + reveal_type(value == value) # revealed: Literal[False] + reveal_type(value in (value,)) # revealed: bool + reveal_type(value not in (value,)) # revealed: bool + +def custom_equality(value: AlwaysEqual): + reveal_type(value in (1,)) # revealed: bool + reveal_type(value not in (1,)) # revealed: bool + reveal_type((value,) == (1,)) # revealed: Literal[True] + reveal_type((value,) != (1,)) # revealed: Literal[False] + +def custom_equality_union(value: AlwaysEqual | None): + reveal_type(value in (1,)) # revealed: bool + reveal_type(value not in (1,)) # revealed: bool + +def custom_equality_union_member(value: AlwaysEqual | None, member: AlwaysEqual): + reveal_type(value.__eq__(member)) # revealed: bool + reveal_type(member.__eq__(value)) # revealed: Literal[True] + reveal_type(value in (member,)) # revealed: Literal[True] + reveal_type(value not in (member,)) # revealed: Literal[False] + reveal_type((value,) == (member,)) # revealed: bool + reveal_type((value,) != (member,)) # revealed: bool + +class Base: + def __eq__(self, other: object) -> bool: + return False + +class AlwaysEqualChild(Base): + def __eq__(self, other: object) -> Literal[True]: + return True + +def reflected_custom_equality(value: Base, child: AlwaysEqualChild): + reveal_type(value == child) # revealed: bool + reveal_type(child == value) # revealed: Literal[True] + reveal_type(value in (child,)) # revealed: Literal[True] + reveal_type(value not in (child,)) # revealed: Literal[False] +``` + ### Identity Comparisons "Identity Comparisons" refers to `is` and `is not`. diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md index 78a6ded3ee..f6ae86bf6e 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md @@ -628,11 +628,11 @@ E = TypeVar("E", Literal[ReflexiveEnum.A], Literal[ReflexiveEnum.B]) def reflexive_enum_literal_slot(x: Literal[ReflexiveEnum.A] | None, value: Literal[ReflexiveEnum.A]) -> None: if x not in (value,): - reveal_type(x) # revealed: None + reveal_type(x) # revealed: Never def reflexive_enum_typevar_slot(x: E | None, value: E) -> None: if x not in (value,): - reveal_type(x) # revealed: None + reveal_type(x) # revealed: Never def tuple_with_any_slot(x: str | None, missing: Any) -> None: if x not in (missing, None): diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index 4b6a9af792..b8e8638de1 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -91,6 +91,52 @@ fn reflected_method_priority<'db>( } impl<'db> Type<'db> { + /// Return the result of dispatching a rich comparison method between two operands. + /// + /// A strict subclass on the right takes precedence over the normal method on the left. + /// The caller remains responsible for operator-specific fallbacks such as identity-based + /// equality when neither comparison method is available. + pub(super) fn try_call_rich_comparison_dunder( + db: &'db dyn Db, + left: Type<'db>, + right: Type<'db>, + dunder: &'static str, + reflected_dunder: &'static str, + policy: MemberLookupPolicy, + ) -> Option> { + let call_dunder = |name, receiver: Type<'db>, argument: Type<'db>| { + receiver + .try_call_dunder_with_policy( + db, + name, + &mut CallArguments::positional([argument]), + TypeContext::default(), + policy, + ) + .map(|outcome| outcome.return_type(db)) + .ok() + }; + + match reflected_method_priority(db, left, right) { + ReflectedMethodPriority::Never => call_dunder(dunder, left, right) + .or_else(|| call_dunder(reflected_dunder, right, left)), + ReflectedMethodPriority::Possibly => { + match ( + call_dunder(dunder, left, right), + call_dunder(reflected_dunder, right, left), + ) { + (Some(normal), Some(reflected)) => { + Some(UnionType::from_two_elements(db, normal, reflected)) + } + (Some(result), None) | (None, Some(result)) => Some(result), + (None, None) => None, + } + } + ReflectedMethodPriority::Definitely => call_dunder(reflected_dunder, right, left) + .or_else(|| call_dunder(dunder, left, right)), + } + } + /// Memoize the pure return-type part of binary dunder resolution so repeated identical /// expressions don't re-run overload selection at every call site. pub(crate) fn try_call_bin_op_return_type( diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index 078871eb52..6bd92a1e18 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -12,6 +12,7 @@ use super::{ CallArguments, EnumLiteralType, IntersectionBuilder, KnownBoundMethodType, KnownClass, LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, Truthiness, Type, TypeContext, TypeVarBoundOrConstraints, UnionBuilder, + bool::BoolError, enums::{enum_member_literals, enum_metadata}, }; @@ -293,14 +294,29 @@ impl<'db> TupleEqualityEvaluator<'db> { &mut self, left: Type<'db>, right: Type<'db>, - inferred_truthiness: Truthiness, - ) -> Truthiness { - // Identity can turn a false equality result true, but cannot turn a true result false. - if inferred_truthiness == Truthiness::AlwaysTrue { - return Truthiness::AlwaysTrue; + ) -> Result> { + let db = self.evaluator.db; + let truthiness = evaluate_tuple_element_equality(&mut self.evaluator, left, right); + if !truthiness.is_ambiguous() { + return Ok(truthiness); } - evaluate_tuple_element_equality(&mut self.evaluator, left, right) + let Some(result) = Type::try_call_rich_comparison_dunder( + db, + left, + right, + "__eq__", + "__eq__", + MemberLookupPolicy::default(), + ) else { + return Ok(Truthiness::Ambiguous); + }; + + // Identity can turn a false equality result true, but cannot turn a true result false. + Ok(match result.try_bool(db)? { + Truthiness::AlwaysTrue => Truthiness::AlwaysTrue, + Truthiness::AlwaysFalse | Truthiness::Ambiguous => Truthiness::Ambiguous, + }) } } diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index 4ae0e82246..5b376bf1b2 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -10,7 +10,7 @@ use crate::types::cyclic::CycleDetector; use crate::types::equality::{ ComparisonSoundnessPolicy, TupleEqualityEvaluator, equality_truthiness, inequality_truthiness, }; -use crate::types::tuple::TupleSpec; +use crate::types::tuple::{Tuple, TupleSpec}; use crate::types::{ DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, Type, TypeContext, TypeTransformer, @@ -330,33 +330,24 @@ fn infer_binary_type_comparison_inner<'db>( } if let NonIdentityOperator::Membership(op) = op - && left.tuple_instance_spec(db).is_some() && let Some(right_tuple) = right.tuple_instance_spec(db) + && let Tuple::Fixed(right_tuple) = &*right_tuple { let mut any_eq = false; let mut any_ambiguous = false; - - for ty in right_tuple.iter_element_types(db) { - let eq_result = infer_binary_type_comparison_inner( - context, - left, - NonIdentityOperator::Rich(RichCompareOperator::Eq), - ty, - range, - visitor, - ) - .expect("equality comparisons should always be supported"); - - match eq_result { - todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo), - // It's okay to ignore errors here because Python doesn't call `__bool__` - // for different union variants. Instead, this is just for us to - // evaluate a possibly truthy value to `false` or `true`. - ty => match ty.bool(db) { - Truthiness::AlwaysTrue => any_eq = true, - Truthiness::AlwaysFalse => (), - Truthiness::Ambiguous => any_ambiguous = true, - }, + let mut equality = TupleEqualityEvaluator::new(db, soundness_policy); + + for &element_ty in right_tuple.elements_slice() { + // It's okay to ignore errors here because Python doesn't call `__bool__` + // for different union variants. Instead, this is just for us to + // evaluate a possibly truthy value to `false` or `true`. + match equality + .element_truthiness(element_ty, left) + .unwrap_or_else(|error| error.fallback_truthiness()) + { + Truthiness::AlwaysTrue => any_eq = true, + Truthiness::AlwaysFalse => (), + Truthiness::Ambiguous => any_ambiguous = true, } } @@ -1005,26 +996,14 @@ fn infer_rich_comparison<'db>( op: RichCompareOperator, policy: MemberLookupPolicy, ) -> Result, UnsupportedComparisonError<'db>> { - // The following resource has details about the rich comparison algorithm: - // https://snarky.ca/unravelling-rich-comparison-operators/ - let call_dunder = |op: RichCompareOperator, left: Type<'db>, right: Type<'db>| { - left.try_call_dunder_with_policy( - db, - op.dunder(), - &mut CallArguments::positional([right]), - TypeContext::default(), - policy, - ) - .map(|outcome| outcome.return_type(db)) - .ok() - }; - - // The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side. - if left != right && right.is_subtype_of(db, left) { - call_dunder(op.reflect(), right, left).or_else(|| call_dunder(op, left, right)) - } else { - call_dunder(op, left, right).or_else(|| call_dunder(op.reflect(), right, left)) - } + Type::try_call_rich_comparison_dunder( + db, + left, + right, + op.dunder(), + op.reflect().dunder(), + policy, + ) .or_else(|| { // When no appropriate method returns any value other than NotImplemented, // the `==` and `!=` operators will fall back to `is` and `is not`, respectively. @@ -1126,24 +1105,14 @@ fn infer_tuple_rich_comparison<'db>( let mut equality = TupleEqualityEvaluator::new(db, soundness_policy); for (l_ty, r_ty) in left_iter.zip(right_iter) { - let pairwise_eq_result = infer_binary_type_comparison_inner( - context, - l_ty, - NonIdentityOperator::Rich(RichCompareOperator::Eq), - r_ty, - range, - visitor, - ) - .expect("infer_binary_type_comparison should never return None for `==`"); - - let inferred_truthiness = pairwise_eq_result.try_bool(db).unwrap_or_else(|err| { - // TODO: We should, whenever possible, pass the range of the left and right elements - // instead of the range of the whole tuple. - err.report_diagnostic(context, range); - Truthiness::Ambiguous - }); - - let eq_truthiness = equality.element_truthiness(l_ty, r_ty, inferred_truthiness); + let eq_truthiness = equality + .element_truthiness(l_ty, r_ty) + .unwrap_or_else(|err| { + // TODO: We should, whenever possible, pass the range of the left and right elements + // instead of the range of the whole tuple. + err.report_diagnostic(context, range); + Truthiness::Ambiguous + }); match eq_truthiness { // - AlwaysTrue : Continue to the next pair for lexicographic comparison From 1673aa73d4dbe8124f9098607d675a943d273951 Mon Sep 17 00:00:00 2001 From: jesco Date: Mon, 27 Jul 2026 15:45:38 -0400 Subject: [PATCH 083/390] [`refurb`] Mark fixes that remove unknown separators as unsafe (`FURB105`) (#27200) ## Summary Fixes #20919. The main learning point after working on this was, that python validates sep even when it is unused; removing an unknown value can suppress a TypeError; therefore only known-valid separators receive a safe fix. FURB105 can suggest removing an unused `sep` argument from a `print` call. The fix was previously considered safe unless evaluating the separator had observable side effects. Python still validates `sep` even when the call only prints one value. If the separator is neither `None` nor a string, the original call raises a `TypeError`. Removing an unknown separator can therefore change the program's behavior even when evaluating its expression has no side effects. This change makes the fix conservative: removing `sep` is considered safe when its value is statically known to be a string literal or `None`. Other separator expressions retain the FURB105 diagnostic, but their fix is marked unsafe. The fixtures cover both a known-valid `None` separator and a separator supplied through an unknown variable. The rule documentation and snapshots are updated to reflect the safety distinction. ## Test Plan - Ran the focused FURB105 fixture and snapshot test: `cargo test -p ruff_linter --lib rule_printemptystring_path_new_furb105_py_expects` - Ran `cargo fmt --all --check` --- .../resources/test/fixtures/refurb/FURB105.py | 4 ++ .../rules/refurb/rules/print_empty_string.rs | 17 +++--- ...es__refurb__tests__FURB105_FURB105.py.snap | 56 +++++++++++++++---- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB105.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB105.py index 007d5822de..51eae16737 100644 --- a/crates/ruff_linter/resources/test/fixtures/refurb/FURB105.py +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB105.py @@ -22,6 +22,10 @@ print(f"") print(f"", sep=",") print(f"", end="bar") +print(1, sep=None) + +def p(sep): + print(1, sep=sep) # OK. diff --git a/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs b/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs index 9e228bf483..ee803ba0e0 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/print_empty_string.rs @@ -1,8 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::helpers::{contains_effect, is_empty_f_string}; +use ruff_python_ast::helpers::is_empty_f_string; use ruff_python_ast::{self as ast, Expr}; use ruff_python_codegen::Generator; -use ruff_python_semantic::SemanticModel; use ruff_python_trivia::CommentRanges; use ruff_text_size::Ranged; @@ -33,8 +32,8 @@ use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; /// /// ## Fix safety /// This fix is marked as unsafe if it removes comments or an unused `sep` keyword argument -/// that may have side effects. Removing such arguments may change the program's -/// behavior by skipping the execution of those side effects. +/// that is not known to be a valid separator. Removing such arguments may change the +/// program's behavior by skipping their evaluation or hiding a `TypeError`. /// /// ## References /// - [Python documentation: `print`](https://docs.python.org/3/library/functions.html#print) @@ -98,7 +97,6 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) { EmptyStringFix::from_call( call, Separator::Remove, - checker.semantic(), checker.generator(), checker.comment_ranges(), ) @@ -125,7 +123,6 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) { EmptyStringFix::from_call( call, Separator::Remove, - checker.semantic(), checker.generator(), checker.comment_ranges(), ) @@ -191,7 +188,6 @@ pub(crate) fn print_empty_string(checker: &Checker, call: &ast::ExprCall) { EmptyStringFix::from_call( call, separator, - checker.semantic(), checker.generator(), checker.comment_ranges(), ) @@ -210,6 +206,10 @@ fn is_empty_string(expr: &Expr) -> bool { } } +fn is_known_valid_separator(expr: &Expr) -> bool { + expr.is_string_literal_expr() || expr.is_none_literal_expr() +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Separator { Remove, @@ -225,7 +225,6 @@ impl EmptyStringFix { fn from_call( call: &ast::ExprCall, separator: Separator, - semantic: &SemanticModel, generator: Generator, comment_ranges: &CommentRanges, ) -> Self { @@ -262,7 +261,7 @@ impl EmptyStringFix { return true; } - if contains_effect(&keyword.value, |id| semantic.has_builtin_binding(id)) { + if !is_known_valid_separator(&keyword.value) { applicability = Applicability::Unsafe; } diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB105_FURB105.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB105_FURB105.py.snap index 15b5cea41c..81628ae9e2 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB105_FURB105.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB105_FURB105.py.snap @@ -351,6 +351,7 @@ FURB105 [*] Unnecessary empty string and separator passed to `print` 23 | print(f"", sep=",") | ^^^^^^^^^^^^^^^^^^^ 24 | print(f"", end="bar") +25 | print(1, sep=None) | help: Remove empty string and separator | @@ -367,33 +368,68 @@ FURB105 [*] Unnecessary empty string passed to `print` 23 | print(f"", sep=",") 24 | print(f"", end="bar") | ^^^^^^^^^^^^^^^^^^^^^ -25 | -26 | # OK. +25 | print(1, sep=None) | help: Remove empty string | 23 | print(f"", sep=",") - print(f"", end="bar") 24 + print(end="bar") -25 | +25 | print(1, sep=None) | +FURB105 [*] Unnecessary separator passed to `print` + --> FURB105.py:25:1 + | +23 | print(f"", sep=",") +24 | print(f"", end="bar") +25 | print(1, sep=None) + | ^^^^^^^^^^^^^^^^^^ +26 | +27 | def p(sep): + | +help: Remove separator + | +24 | print(f"", end="bar") + - print(1, sep=None) +25 + print(1) +26 | + | + +FURB105 [*] Unnecessary separator passed to `print` + --> FURB105.py:28:5 + | +27 | def p(sep): +28 | print(1, sep=sep) + | ^^^^^^^^^^^^^^^^^ +29 | +30 | # OK. + | +help: Remove separator + | +27 | def p(sep): + - print(1, sep=sep) +28 + print(1) +29 | + | +note: This is an unsafe fix and may change runtime behavior + FURB105 [*] Unnecessary empty string passed to `print` - --> FURB105.py:42:1 + --> FURB105.py:46:1 | -42 | / print( -43 | | # text -44 | | "" -45 | | ) +46 | / print( +47 | | # text +48 | | "" +49 | | ) | |_^ | help: Remove empty string | -41 | +45 | - print( - # text - "" - ) -42 + print() +46 + print() | note: This is an unsafe fix and may change runtime behavior From f4e9979942d8cff19dadc783cadcbbabec943bbc Mon Sep 17 00:00:00 2001 From: vidigoat Date: Tue, 28 Jul 2026 01:59:29 +0530 Subject: [PATCH 084/390] [`refurb`] Parenthesize `yield` arguments in the `FURB192` fixer (#27192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The `FURB192` fix rebuilds the call by slicing the source text of the `sorted()` argument node. That range doesn't include surrounding parentheses, so a parenthesized `yield` loses them and the result is invalid syntax: ```python def f(): x = sorted((yield))[0] ``` ``` $ ruff check --isolated --select FURB192 --fix --unsafe-fixes t.py error: Fix introduced a syntax error. Reverting all changes. This indicates a bug in Ruff. ``` The replacement it tries to write is `x = min(yield)`. A `yield` expression is only valid as a call argument when it's parenthesized, so the fix gets discarded and the diagnostic is left unfixable. All four forms are affected on `main` (a5cdc6d): | source | fix produced today | | --- | --- | | `sorted((yield))[0]` | `min(yield)` | | `sorted((yield x))[-1]` | `max(yield x)` | | `sorted((yield), key=k)[0]` | `min(yield, key=k)` | | `sorted((yield from g()))[0]` | `min(yield from g())` | This slices `parenthesized_range()` for the argument and falls back to the node range, which is what `quadratic-list-summation` and a few other rules already do. #24200 fixed the same kind of thing in the `FURB142` fixer. One deliberate side effect worth flagging: parentheses are now kept for *any* parenthesized argument, so `sorted((a := b))[0]` becomes `min((a := b))` rather than `min(a := b)`. Both are valid; keeping them seemed more consistent than special-casing `yield`. Happy to narrow it to just `Expr::Yield`/`Expr::YieldFrom` if you'd rather not change the other cases. ## Test Plan Added `FURB192_1.py`. It has to be a separate fixture: `FURB192.py` shadows `sorted` with a module-level `def sorted()` at the bottom, and because function bodies are resolved against the final module scope, the rule doesn't fire inside *any* function in that file — so `yield` cases can't go there. - `cargo test -p ruff_linter` — 2807 passed, 0 failed - `cargo clippy -p ruff_linter --all-targets --all-features -- -D warnings` — clean - `prek run --files ` — clean I also ran `--select FURB192 --fix --unsafe-fixes` over all 1599 files under `crates/ruff_linter/resources/test/fixtures`, once with 0.16.0 and once with this branch, and diffed the output. The only file whose result changes is the new fixture, and neither build produces unparseable output anywhere else. --- .../test/fixtures/refurb/FURB192_1.py | 17 ++++ crates/ruff_linter/src/rules/refurb/mod.rs | 1 + .../src/rules/refurb/rules/sorted_min_max.rs | 16 ++-- ...__refurb__tests__FURB192_FURB192_1.py.snap | 93 +++++++++++++++++++ 4 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/refurb/FURB192_1.py create mode 100644 crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB192_FURB192_1.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB192_1.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB192_1.py new file mode 100644 index 0000000000..70b90d271b --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB192_1.py @@ -0,0 +1,17 @@ +# A `yield` expression is only valid as a call argument when it is +# parenthesized, so the parentheses have to be preserved by the fix. +# +# These live in their own fixture because `FURB192.py` shadows `sorted` with a +# module-level function, which suppresses the rule inside function bodies. + + +def f(l, key_fn): + sorted((yield))[0] + + sorted((yield l))[-1] + + sorted((yield), key=key_fn)[0] + + sorted((yield from l))[0] + + sorted((yield), reverse=True)[-1] diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index 72a30f6760..a1338ad22b 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -53,6 +53,7 @@ mod tests { #[test_case(Rule::WriteWholeFile, Path::new("FURB103_2.py"))] #[test_case(Rule::FStringNumberFormat, Path::new("FURB116.py"))] #[test_case(Rule::SortedMinMax, Path::new("FURB192.py"))] + #[test_case(Rule::SortedMinMax, Path::new("FURB192_1.py"))] #[test_case(Rule::SliceToRemovePrefixOrSuffix, Path::new("FURB188.py"))] #[test_case(Rule::SubclassBuiltin, Path::new("FURB189.py"))] #[test_case(Rule::FromisoformatReplaceZ, Path::new("FURB162.py"))] diff --git a/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs b/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs index a6b25f8c82..5a3f8ad36c 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/sorted_min_max.rs @@ -1,5 +1,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::Number; +use ruff_python_ast::token::parenthesized_range; use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; @@ -187,14 +188,17 @@ pub(crate) fn sorted_min_max(checker: &Checker, subscript: &ast::ExprSubscript) if checker.semantic().has_builtin_binding(min_max.as_str()) { diagnostic.set_fix({ + // Preserve any parentheses around the argument. Some expressions are + // only valid as a call argument when parenthesized (e.g., `yield`), + // so slicing the bare node would produce invalid syntax. + let list_expr = checker.locator().slice( + parenthesized_range(list_expr.into(), arguments.into(), checker.tokens()) + .unwrap_or(list_expr.range()), + ); let replacement = if let Some(key) = key_keyword_expr { - format!( - "{min_max}({}, {})", - checker.locator().slice(list_expr), - checker.locator().slice(key), - ) + format!("{min_max}({list_expr}, {})", checker.locator().slice(key)) } else { - format!("{min_max}({})", checker.locator().slice(list_expr)) + format!("{min_max}({list_expr})") }; let replacement = Edit::range_replacement(replacement, subscript.range()); diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB192_FURB192_1.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB192_FURB192_1.py.snap new file mode 100644 index 0000000000..df3c7a5b99 --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB192_FURB192_1.py.snap @@ -0,0 +1,93 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +--- +FURB192 [*] Prefer `min` over `sorted()` to compute the minimum value in a sequence + --> FURB192_1.py:9:5 + | + 8 | def f(l, key_fn): + 9 | sorted((yield))[0] + | ^^^^^^^^^^^^^^^^^^ +10 | +11 | sorted((yield l))[-1] + | +help: Replace with `min` + | +8 | def f(l, key_fn): + - sorted((yield))[0] +9 + min((yield)) +10 | + | +note: This is an unsafe fix and may change runtime behavior + +FURB192 [*] Prefer `max` over `sorted()` to compute the maximum value in a sequence + --> FURB192_1.py:11:5 + | + 9 | sorted((yield))[0] +10 | +11 | sorted((yield l))[-1] + | ^^^^^^^^^^^^^^^^^^^^^ +12 | +13 | sorted((yield), key=key_fn)[0] + | +help: Replace with `max` + | +10 | + - sorted((yield l))[-1] +11 + max((yield l)) +12 | + | +note: This is an unsafe fix and may change runtime behavior + +FURB192 [*] Prefer `min` over `sorted()` to compute the minimum value in a sequence + --> FURB192_1.py:13:5 + | +11 | sorted((yield l))[-1] +12 | +13 | sorted((yield), key=key_fn)[0] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +14 | +15 | sorted((yield from l))[0] + | +help: Replace with `min` + | +12 | + - sorted((yield), key=key_fn)[0] +13 + min((yield), key=key_fn) +14 | + | +note: This is an unsafe fix and may change runtime behavior + +FURB192 [*] Prefer `min` over `sorted()` to compute the minimum value in a sequence + --> FURB192_1.py:15:5 + | +13 | sorted((yield), key=key_fn)[0] +14 | +15 | sorted((yield from l))[0] + | ^^^^^^^^^^^^^^^^^^^^^^^^^ +16 | +17 | sorted((yield), reverse=True)[-1] + | +help: Replace with `min` + | +14 | + - sorted((yield from l))[0] +15 + min((yield from l)) +16 | + | +note: This is an unsafe fix and may change runtime behavior + +FURB192 [*] Prefer `min` over `sorted()` to compute the minimum value in a sequence + --> FURB192_1.py:17:5 + | +15 | sorted((yield from l))[0] +16 | +17 | sorted((yield), reverse=True)[-1] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Replace with `min` + | +16 | + - sorted((yield), reverse=True)[-1] +17 + min((yield)) + | +note: This is an unsafe fix and may change runtime behavior From 8fbdca7abde4ba992c63854ed0a433c2af0eda0c Mon Sep 17 00:00:00 2001 From: Jayashanker Padishala Date: Mon, 27 Jul 2026 13:43:21 -0700 Subject: [PATCH 085/390] [`pyupgrade`] Skip fix when a defaulted `TypeVar` precedes a non-defaulted one (`UP040`, `UP046`, `UP047`) (#27133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #27021. In a PEP 695 type-parameter list, a non-defaulted parameter cannot follow a defaulted one, but the UP040/UP046/UP047 fixes generated exactly that when the source declared e.g. `T = TypeVar("T", default=int)` before `S = TypeVar("S")` — producing `type Pair[T = int, S] = tuple[T, S]`, which ruff itself then rejects as invalid syntax. Reordering the parameters would not be an equivalent fix: parameter order determines how positional arguments bind when subscripting the alias/class/function (`Pair[int, str]`). So, per the issue's suggestion and following the existing precedent in these rules (duplicate TypeVars and defaults on unsupported targets also suppress the diagnostic; `NonPEP695TypeAlias` declares `FixAvailability::Always`, so a fixless diagnostic isn't an option), the diagnostic is skipped when a non-defaulted TypeVar follows a defaulted one. The guard lives at both choke points (`check_type_vars` for UP046/UP047 and `create_diagnostic` for both UP040 paths, including `TypeAliasType(..., type_params=...)`), so all three rules are covered by one helper. ## Test Plan - New fixture cases in `UP040.py`, `UP046_0.py`, `UP047_0.py`: defaulted-then-non-defaulted (no diagnostic), non-defaulted-first and all-defaulted (still fixed correctly); snapshots regenerated. - `cargo test -p ruff_linter` — all pass; `cargo fmt --check` and `cargo clippy -p ruff_linter --no-deps -- -D warnings` clean. - Manually verified the issue's repro: before, ruff emitted the invalid fix and then flagged its own output; after, no diagnostic on the bad ordering while valid orderings still convert. --- Developed with AI assistance (Claude Code); I reviewed the change and can speak to it. --- .../resources/mdtest/pyupgrade/pep-695.md | 170 ++++++++++++++++++ .../src/rules/pyupgrade/rules/pep695/mod.rs | 18 ++ .../rules/pep695/non_pep695_type_alias.rs | 5 + 3 files changed, 193 insertions(+) create mode 100644 crates/ruff_linter/resources/mdtest/pyupgrade/pep-695.md diff --git a/crates/ruff_linter/resources/mdtest/pyupgrade/pep-695.md b/crates/ruff_linter/resources/mdtest/pyupgrade/pep-695.md new file mode 100644 index 0000000000..e39472a989 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/pyupgrade/pep-695.md @@ -0,0 +1,170 @@ +# PEP 695 rules (`UP040`, `UP046`, `UP047`) + +```toml +target-version = "py313" + +[lint] +preview = true +select = [ + "non-pep695-type-alias", + "non-pep695-generic-class", + "non-pep695-generic-function", +] +``` + +## Defaulted `TypeVar` before a non-defaulted one + +In a PEP 695 type parameter list, a non-defaulted type parameter cannot follow a defaulted one, and +reordering the parameters would change how positional arguments bind when subscripting the alias, +class, or function. There is no valid, equivalent type parameter list in this case, so no diagnostic +is emitted (see [#27021](https://github.com/astral-sh/ruff/issues/27021)). + +### `non-pep695-type-alias` (`UP040`) + +#### No diagnostic for a `TypeAlias` annotation + +```py +from typing import TypeAlias, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +Pair: TypeAlias = tuple[T, S] +``` + +#### No diagnostic for a `TypeAliasType` call + +```py +from typing import TypeAliasType, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +Pair = TypeAliasType("Pair", tuple[T, S], type_params=(T, S)) +``` + +#### The fix is still offered when the non-defaulted `TypeVar` comes first + +```py +from typing import TypeAlias, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +Pair: TypeAlias = tuple[S, T] # snapshot: non-pep695-type-alias +``` + +```snapshot +error[UP040]: Type alias `Pair` uses `TypeAlias` annotation instead of the `type` keyword + --> src/mdtest_snippet.py:6:1 + | +6 | Pair: TypeAlias = tuple[S, T] # snapshot: non-pep695-type-alias + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Use the `type` keyword + | +5 | + - Pair: TypeAlias = tuple[S, T] # snapshot: non-pep695-type-alias +6 + type Pair[S, T = int] = tuple[S, T] # snapshot: non-pep695-type-alias + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### The fix is still offered when all of the type variables have defaults + +```py +from typing import TypeAlias, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S", default=str) + +Pair: TypeAlias = tuple[T, S] # error: [non-pep695-type-alias] +``` + +### `non-pep695-generic-class` (`UP046`) + +#### No diagnostic when a defaulted `TypeVar` precedes a non-defaulted one + +```py +from typing import Generic, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +class Pair(Generic[T, S]): + t: T + s: S +``` + +#### The fix is still offered when the non-defaulted `TypeVar` comes first + +```py +from typing import Generic, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +class Pair(Generic[S, T]): # snapshot: non-pep695-generic-class + t: T + s: S +``` + +```snapshot +error[UP046]: Generic class `Pair` uses `Generic` subclass instead of type parameters + --> src/mdtest_snippet.py:6:12 + | +6 | class Pair(Generic[S, T]): # snapshot: non-pep695-generic-class + | ^^^^^^^^^^^^^ + | +help: Use type parameters + | +5 | + - class Pair(Generic[S, T]): # snapshot: non-pep695-generic-class +6 + class Pair[S, T = int]: # snapshot: non-pep695-generic-class +7 | t: T + | +note: This is an unsafe fix and may change runtime behavior +``` + +### `non-pep695-generic-function` (`UP047`) + +#### No diagnostic when a defaulted `TypeVar` precedes a non-defaulted one + +```py +from typing import TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +def pair(t: T, s: S) -> tuple[T, S]: + return (t, s) +``` + +#### The fix is still offered when the non-defaulted `TypeVar` comes first + +```py +from typing import TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +def pair(s: S, t: T) -> tuple[S, T]: # snapshot: non-pep695-generic-function + return (s, t) +``` + +```snapshot +error[UP047]: Generic function `pair` should use type parameters + --> src/mdtest_snippet.py:6:5 + | +6 | def pair(s: S, t: T) -> tuple[S, T]: # snapshot: non-pep695-generic-function + | ^^^^^^^^^^^^^^^^ + | +help: Use type parameters + | +5 | + - def pair(s: S, t: T) -> tuple[S, T]: # snapshot: non-pep695-generic-function +6 + def pair[S, T = int](s: S, t: T) -> tuple[S, T]: # snapshot: non-pep695-generic-function +7 | return (s, t) + | +note: This is an unsafe fix and may change runtime behavior +``` diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index c2c6f00fde..109ccd762d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -367,6 +367,20 @@ fn in_nested_context(checker: &Checker) -> bool { .any(|stmt| matches!(stmt, Stmt::ClassDef(_) | Stmt::FunctionDef(_))) } +/// Returns `true` if a type variable without a default follows a type variable with a default. +/// +/// In a PEP 695 type parameter list this is a syntax error: +/// +/// ```python +/// type Pair[T = int, S] = tuple[T, S] # non-default type parameter `S` follows default type parameter +/// ``` +fn non_default_follows_default(type_vars: &[TypeVar]) -> bool { + type_vars + .iter() + .skip_while(|tv| tv.default.is_none()) + .any(|tv| tv.default.is_none()) +} + /// Deduplicate `vars`, returning `None` if `vars` is empty or any duplicates are found. /// Also returns `None` if any `TypeVar` has a default value and the target Python version /// is below 3.13 or preview mode is not enabled. Note that `typing_extensions` backports @@ -385,6 +399,10 @@ fn check_type_vars<'a>(vars: Vec>, checker: &Checker) -> Option Date: Tue, 28 Jul 2026 04:52:55 +0800 Subject: [PATCH 086/390] [`flake8-bandit`] Document `TYPE_CHECKING` exception (`S101`) (#27004) ## Summary Adds documentation for the S101 rule to mention `TYPE_CHECKING` blocks as a way to avoid the rule when asserts are used for type narrowing. This was discussed in the issue and aligns with the existing behavior (introduced in PR #3960) where asserts inside `TYPE_CHECKING` blocks are already allowed. ## Test Plan - Existing S101 tests pass (behavior unchanged) - `cargo fmt --check` clean Refs #26819 --------- Co-authored-by: Claude Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs index 606630bccb..f208658e29 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/assert_used.rs @@ -18,6 +18,9 @@ use crate::checkers::ast::Checker; /// /// Consider raising a meaningful error instead of using `assert`. /// +/// The rule exempts assertions within a `TYPE_CHECKING` block, assuming these are needed to satisfy +/// a type checker. +/// /// ## Example /// ```python /// assert x > 0, "Expected positive value." From 165c8c05b3aab05821e747e5c2c7fb5cdae9cecf Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 27 Jul 2026 15:55:47 -0700 Subject: [PATCH 087/390] [ty] Synthesize __replace__ for Pydantic models (#27220) ## Summary Fixes astral-sh/ty#4095. Synthesize Python 3.13+ `__replace__` methods for Pydantic models while preserving their dedicated field handling. Exclude private and internal Pydantic attributes and use model field names rather than constructor aliases for replacements. Include the synthesized method in class and instance member discovery without exposing dataclass-only attributes. Update the existing pinned Pydantic mdtest suite and its lockfile from Python 3.12 to Python 3.13, where `__replace__` was added. ## Test plan Add real-Pydantic mdtests for frozen and mutable models, private attributes, field aliases, inherited and generic fields, `RootModel`, and `BaseSettings`. Verify direct `__replace__` calls, `copy.replace`, concrete and generic return types, invalid replacement arguments, and class/instance completion. Add ordinary base-class and metaclass `dataclass_transform` replacement mdtests and verify replacement is not synthesized before Python 3.13. --- .../resources/mdtest/call/replace.md | 57 ++++++ .../resources/mdtest/external/pydantic.lock | 48 +++-- .../resources/mdtest/external/pydantic.md | 183 +++++++++++++++++- .../src/types/class/static_literal.rs | 15 +- .../src/types/list_members.rs | 13 +- 5 files changed, 279 insertions(+), 37 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/replace.md b/crates/ty_python_semantic/resources/mdtest/call/replace.md index 26ec7f1749..3b962a9ed8 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/replace.md +++ b/crates/ty_python_semantic/resources/mdtest/call/replace.md @@ -69,6 +69,41 @@ e = a.__replace__(x="wrong") # error: [invalid-argument-type] e = replace(a, x="wrong") ``` +### Dataclass transforms + +Classes transformed through a base class or metaclass also support the `__replace__` protocol. + +```py +from copy import replace +from typing import dataclass_transform + +@dataclass_transform() +class ModelBase: ... + +class BaseModel(ModelBase): + value: int + +# revealed: (self: BaseModel, *, value: int = ...) -> BaseModel +reveal_type(BaseModel.__replace__) + +base_model = BaseModel(value=1) +reveal_type(base_model.__replace__(value=2)) # revealed: BaseModel +reveal_type(replace(base_model, value=2)) # revealed: BaseModel + +@dataclass_transform() +class ModelMetaclass(type): ... + +class MetaclassModel(metaclass=ModelMetaclass): + value: int + +# revealed: (self: MetaclassModel, *, value: int = ...) -> MetaclassModel +reveal_type(MetaclassModel.__replace__) + +metaclass_model = MetaclassModel(value=1) +reveal_type(metaclass_model.__replace__(value=2)) # revealed: MetaclassModel +reveal_type(replace(metaclass_model, value=2)) # revealed: MetaclassModel +``` + ### NamedTuples NamedTuples also support the `__replace__` protocol: @@ -102,3 +137,25 @@ Invalid calls to `__replace__` will raise an error: # error: [unknown-argument] "Argument `z` does not match any known parameter" a.__replace__(z=42) ``` + +## Before Python 3.13 + +Dataclass transforms do not synthesize `__replace__` before the replacement protocol exists. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import dataclass_transform + +@dataclass_transform() +class ModelBase: ... + +class Model(ModelBase): + value: int + +Model.__replace__ # error: [unresolved-attribute] +Model(value=1).__replace__ # error: [unresolved-attribute] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/external/pydantic.lock b/crates/ty_python_semantic/resources/mdtest/external/pydantic.lock index 87f6bd7e46..fb7a5c2fb8 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/pydantic.lock +++ b/crates/ty_python_semantic/resources/mdtest/external/pydantic.lock @@ -1,14 +1,14 @@ version = 1 revision = 3 -requires-python = "==3.12.*" +requires-python = "==3.13.*" [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] [[package]] @@ -50,25 +50,21 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, ] [[package]] @@ -96,11 +92,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] diff --git a/crates/ty_python_semantic/resources/mdtest/external/pydantic.md b/crates/ty_python_semantic/resources/mdtest/external/pydantic.md index c26885d740..7bafd34307 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/pydantic.md +++ b/crates/ty_python_semantic/resources/mdtest/external/pydantic.md @@ -2,7 +2,7 @@ ```toml [environment] -python-version = "3.12" +python-version = "3.13" python-platform = "linux" [project] @@ -1499,6 +1499,187 @@ class InvalidFieldQualifiers(BaseModel): required: Required[int] ``` +## Replacement + +Pydantic models support `copy.replace` and expose a synthesized `__replace__` method on Python 3.13 +and later. + +### Frozen models + +```py +from copy import replace + +from pydantic import BaseModel + +class Model(BaseModel, frozen=True): + value: int + +model = Model(value=1) + +# revealed: (self: Model, *, value: int = ...) -> Model +reveal_type(Model.__replace__) + +reveal_type(model.__replace__(value=2)) # revealed: Model +reveal_type(replace(model, value=2)) # revealed: Model +``` + +### Mutable models + +Replacement is available on mutable models and accepts only real model fields. + +```py +from copy import replace + +from pydantic import BaseModel + +class Model(BaseModel): + value: int + _private: int = 0 + +model = Model(value=1) + +# revealed: (self: Model, *, value: int = ...) -> Model +reveal_type(Model.__replace__) + +reveal_type(model.__replace__(value=2)) # revealed: Model +reveal_type(replace(model, value=2)) # revealed: Model + +model.__replace__(value="two") # error: [invalid-argument-type] +model.__replace__(_private=2) # error: [unknown-argument] +model.__replace__(missing=2) # error: [unknown-argument] +``` + +### Field aliases + +Replacement updates model fields by name, even when initialization uses an alias. + +```py +from copy import replace + +from pydantic import BaseModel, Field + +class Model(BaseModel): + value: int = Field(alias="external_value") + +model = Model(external_value=1) + +# revealed: (self: Model, *, value: int = ...) -> Model +reveal_type(Model.__replace__) + +reveal_type(model.__replace__(value=2)) # revealed: Model +reveal_type(replace(model, value=2)) # revealed: Model + +model.__replace__(external_value=2) # error: [unknown-argument] +``` + +### Member discovery + +The synthesized method is available in completions for both a model class and its instances. Models +do not expose attributes that belong only to standard-library dataclasses. + +```py +from pydantic import BaseModel +from ty_extensions import static_assert +from ty_extensions._internal import has_member + +class Model(BaseModel): + value: int + +model = Model(value=1) + +static_assert(has_member(Model, "__replace__")) +static_assert(has_member(model, "__replace__")) +static_assert(not has_member(Model, "__dataclass_fields__")) +static_assert(not has_member(Model, "__dataclass_params__")) +static_assert(not has_member(Model, "__match_args__")) +``` + +### Inherited fields + +```py +from copy import replace + +from pydantic import BaseModel + +class Parent(BaseModel): + inherited: int + +class Child(Parent): + own: str + +model = Child(inherited=1, own="first") + +# revealed: (self: Child, *, inherited: int = ..., own: str = ...) -> Child +reveal_type(Child.__replace__) + +reveal_type(model.__replace__(inherited=2)) # revealed: Child +reveal_type(model.__replace__(own="second")) # revealed: Child +reveal_type(replace(model, inherited=2, own="second")) # revealed: Child + +model.__replace__(inherited="two") # error: [invalid-argument-type] +model.__replace__(own=2) # error: [invalid-argument-type] +``` + +### Generic models + +```py +from copy import replace + +from pydantic import BaseModel + +class Model[T](BaseModel): + value: T + +model = Model[int](value=1) + +reveal_type(model.__replace__(value=2)) # revealed: Model[int] +reveal_type(replace(model, value=2)) # revealed: Model[int] + +model.__replace__(value="two") # error: [invalid-argument-type] +``` + +### Root models + +```py +from copy import replace + +from pydantic import RootModel + +class Model(RootModel[int]): ... + +model = Model(1) + +# revealed: (self: Model, *, root: int = ...) -> Model +reveal_type(Model.__replace__) + +reveal_type(model.__replace__(root=2)) # revealed: Model +reveal_type(replace(model, root=2)) # revealed: Model + +model.__replace__(root="two") # error: [invalid-argument-type] +``` + +### Settings models + +```py +from copy import replace + +from pydantic_settings import BaseSettings + +class Model(BaseSettings): + value: int + +model = Model(value=1) + +# revealed: (self: Model, *, value: int = ...) -> Model +reveal_type(Model.__replace__) + +reveal_type(model.__replace__(value=2)) # revealed: Model +reveal_type(replace(model, value=2)) # revealed: Model + +model.__replace__(value="two") # error: [invalid-argument-type] +model.__replace__(_secrets_dir=".") # error: [unknown-argument] +``` + ## Pydantic dataclasses Pydantic's dataclasses are similar to the standard library dataclasses: diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index 38553e3847..13142a2997 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -1436,8 +1436,9 @@ impl<'db> StaticClassLiteral<'db> { }; let mut field_ty = field.declared_ty; - if name == "__init__" && !init { - // Skip fields with `init=False` + if !init && (name == "__init__" || field_policy.is_pydantic()) { + // Fields with `init=False` are excluded from constructors. Pydantic's private + // and internal fields are also excluded from replacement. continue; } @@ -1575,6 +1576,9 @@ impl<'db> StaticClassLiteral<'db> { } (false, false) => {} } + } else if name == "__replace__" && field_policy.is_pydantic() { + // Pydantic updates model fields by name rather than by initialization alias. + add_parameter_with_name(field_name.clone(), default_ty); } else { // Use the alias name if provided, otherwise use the field name. let parameter_name = @@ -1778,9 +1782,10 @@ impl<'db> StaticClassLiteral<'db> { ) }) } - (CodeGeneratorKind::DataclassLike(_), "__replace__") - if Program::get(db).python_version(db) >= PythonVersion::PY313 => - { + ( + CodeGeneratorKind::DataclassLike(_) | CodeGeneratorKind::Pydantic(_), + "__replace__", + ) if Program::get(db).python_version(db) >= PythonVersion::PY313 => { let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) .with_annotated_type(instance_ty); diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index 11a2969f04..d4f0ebaed8 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -581,8 +581,14 @@ impl<'db> AllMembers<'db> { } } Some(CodeGeneratorKind::TypedDict) => {} - Some(CodeGeneratorKind::DataclassLike(_)) => { - for attr in SYNTHETIC_DATACLASS_ATTRIBUTES { + Some(kind @ (CodeGeneratorKind::DataclassLike(_) | CodeGeneratorKind::Pydantic(_))) => { + let synthetic_attributes: &[&str] = if kind.is_pydantic() { + &["__replace__"] + } else { + SYNTHETIC_DATACLASS_ATTRIBUTES + }; + + for attr in synthetic_attributes { if let Place::Defined(DefinedPlace { ty: synthetic_member, .. @@ -595,9 +601,6 @@ impl<'db> AllMembers<'db> { } } } - Some(CodeGeneratorKind::Pydantic(_)) => { - // Pydantic's special attributes are declared on and inherited from `BaseModel`. - } None => {} } } From ae323c75678b6586308cc116b39083d307852c66 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 27 Jul 2026 18:37:48 -0700 Subject: [PATCH 088/390] [ty] Fix gradual class assignability with generic receivers (#27223) ## Summary Fix assignability between concrete and gradual class types when a generic method receiver switches callable comparison into lazy constraint evaluation. Previously, `type[int]` and `type[Any]` were incorrectly treated as incompatible in either direction because class-type comparison required eager assignability. Base the decision on the assignability relation instead, preserving strict subtyping behavior and handling `type[Unknown]` as well. (This bug came up as part of sample user code for the otherwise unrelated issue https://github.com/astral-sh/ty/issues/4078.) ## Test plan - Add focused mdtests for generic bound methods returning concrete and gradual class types. - Cover `type[Any]` and `type[Unknown]`, including concrete-to-gradual and gradual-to-concrete callable assignments. - Verify the complete `ty_python_semantic` test suite and file-scoped repository hooks. --- .../type_properties/is_assignable_to.md | 26 +++++++++++++++++++ .../src/types/subclass_of.rs | 4 +-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 1f5f3a80ce..2410a01459 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -1146,6 +1146,32 @@ c: Callable[[Any], str] = A().f c: Callable[[Any], str] = A().g ``` +### Generic method types with gradual class return types + +A generic receiver makes signature comparison lazy without changing whether gradual class types are +assignable. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, Callable +from ty_extensions import Unknown + +class C: + def concrete[T](self: T) -> type[int]: + return int + + def gradual[T](self: T) -> type[Any]: + return int + +accepts_any: Callable[[], type[Any]] = C().concrete +accepts_unknown: Callable[[], type[Unknown]] = C().concrete +accepts_concrete: Callable[[], type[int]] = C().gradual +``` + ### Class literal types ```py diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index bf2fd0e0c4..d214863959 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -374,11 +374,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (SubclassOfInner::Dynamic(_), SubclassOfInner::Class(target_class)) => { ConstraintSet::from_bool( self.constraints, - target_class.is_object(db) || self.is_eager_assignability(), + target_class.is_object(db) || self.relation.is_assignability(), ) } (SubclassOfInner::Class(_), SubclassOfInner::Dynamic(_)) => { - ConstraintSet::from_bool(self.constraints, self.is_eager_assignability()) + ConstraintSet::from_bool(self.constraints, self.relation.is_assignability()) } // For example, `type[bool]` describes all possible runtime subclasses of the class `bool`, From 6a80a014227a971e5584ea1859486fc254441843 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 27 Jul 2026 21:38:06 -0400 Subject: [PATCH 089/390] [ty] Prefer static constrained TypeVar solutions (#27057) ## Summary Follow-up to [#26965](https://github.com/astral-sh/ruff/pull/26965). After accepting gradual constrained-TypeVar solutions, a gradual constraint can match before a more-specific constraint. The existing subtype-based tie-breaker cannot order these candidates, so inference depends on declaration order and can discard members of the concrete type: ```py from typing import Any, TypeVar class Row(tuple[Any, ...]): def asDict(self) -> dict[str, Any]: ... RowLike = TypeVar("RowLike", list[Any], tuple[Any, ...], Row) def identity(value: RowLike) -> RowLike: ... result = identity(Row()) result.asDict() # Previously: unresolved-attribute on tuple[Any, ...] ``` We now compare compatible constraints using assignability. When only one direction is assignable, lower-bound inference prefers the more-specific constraint and upper-bound-only inference preserves its existing preference for the more-general one. When both directions are assignable, we prefer a fully static constraint over a gradual constraint, which also makes `TypeVar("T", Any, int)` select `int` for a concrete integer argument. Incomparable constraints retain declaration order. --- .../mdtest/generics/legacy/functions.md | 21 +++++++++++----- .../src/types/constraints.rs | 24 ++++++++++++++----- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index e367b55b2f..5d603d3ab7 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -440,10 +440,10 @@ def consume_callback(callback: Callable[[Row], None]) -> Row: reveal_type(consume_callback(callback)) # revealed: tuple[Any, ...] ``` -## Gradual constraints can obscure a more specific constraint +## Prefer specific compatible constraints over gradual constraints -A gradual constraint that is compatible with a concrete argument can be selected before a more -specific constraint. This makes inference depend on the order in which the constraints are declared. +A gradual constraint can be compatible with a concrete argument and a more specific declared +constraint. We prefer the more specific constraint regardless of declaration order. ```py from typing import Any, TypeVar @@ -454,6 +454,8 @@ class Row(tuple[Any, ...]): GradualFirst = TypeVar("GradualFirst", list[Any], tuple[Any, ...], Row) RowFirst = TypeVar("RowFirst", Row, tuple[Any, ...], list[Any]) +AnyFirst = TypeVar("AnyFirst", Any, int) +IntFirst = TypeVar("IntFirst", int, Any) def gradual_first(row: GradualFirst) -> GradualFirst: return row @@ -461,15 +463,22 @@ def gradual_first(row: GradualFirst) -> GradualFirst: def row_first(row: RowFirst) -> RowFirst: return row +def any_first(value: AnyFirst) -> AnyFirst: + return value + +def int_first(value: IntFirst) -> IntFirst: + return value + gradual = gradual_first(Row()) -# TODO: revealed: Row -reveal_type(gradual) # revealed: tuple[Any, ...] -# error: [unresolved-attribute] "Object of type `tuple[Any, ...]` has no attribute `asDict`" +reveal_type(gradual) # revealed: Row gradual.asDict() specific = row_first(Row()) reveal_type(specific) # revealed: Row specific.asDict() + +reveal_type(any_first(1)) # revealed: int +reveal_type(int_first(1)) # revealed: int ``` ## Typevar inference is a unification problem diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 514297ecb8..43801c6c5e 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -3909,14 +3909,26 @@ impl<'db> PathBounds<'db> { // Lower-bound evidence asks for the narrowest compatible declared constraint // above the lower bound. With only upper-bound evidence, ask for the widest // compatible declared constraint below the upper bound. If the candidates are - // equivalent or incomparable, keep the current best to preserve the TypeVar's + // assignable in both directions, prefer a fully static constraint over a + // gradual one. Otherwise, keep the current best to preserve the TypeVar's // declared constraint order. - if path_bound.lower.is_some() { - candidate.is_subtype_of(db, current_best) - && !current_best.is_subtype_of(db, candidate) + let candidate_assignable_to_best = candidate.is_assignable_to(db, current_best); + let best_assignable_to_candidate = current_best.is_assignable_to(db, candidate); + + if candidate_assignable_to_best != best_assignable_to_candidate { + if path_bound.lower.is_some() { + candidate_assignable_to_best + } else { + best_assignable_to_candidate + } + } else if candidate_assignable_to_best { + let candidate_is_static = candidate.bottom_materialization(db) + == candidate.top_materialization(db); + let best_is_static = current_best.bottom_materialization(db) + == current_best.top_materialization(db); + candidate_is_static && !best_is_static } else { - current_best.is_subtype_of(db, candidate) - && !candidate.is_subtype_of(db, current_best) + false } }; From b2a35c351be0929c47802dca90d745ca74aef834 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 27 Jul 2026 18:41:44 -0700 Subject: [PATCH 090/390] [ty] Narrow tagged unions through all type kinds (#27226) ## Summary Closes astral-sh/ty#4100. That report wants attribute tagged-union narrowing on a union containing truthiness-guarded intersections (e.g. `A & ~AlwaysFalsy`). It was currently explicit in the attribute-tagged-union narrowing that it only operated on union elements that were nominal instances, but I couldn't find any soundness rationale for this restriction anywhere in the issues or PRs involved in adding this support, and I don't see why it can't apply to attribute access on any type. So this PR narrows tagged unions using the discriminator looked up on each complete union member, rather than limiting narrowing to nominal instances, which of course naturally supports the requested use case of truthiness-narrowed intersections. ## Test plan - Extend the existing equality mdtest with truthiness-guarded inherited and nested discriminants, one positive intersection, and one read-only structural protocol. - Extend the existing identity and attribute-truthiness mdtests with one prior-truthiness-guard regression each. --- .../mdtest/narrow/conditionals/eq.md | 66 ++++++++++++++++++- .../mdtest/narrow/conditionals/is.md | 9 +++ .../resources/mdtest/narrow/truthiness.md | 11 ++++ crates/ty_python_semantic/src/types/narrow.rs | 48 ++++++-------- 4 files changed, 103 insertions(+), 31 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index 5281395459..49e7ebb59b 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -2224,19 +2224,41 @@ def never_unequal_narrowing(x: Any, value: Literal[NeverUnequalEnum.A]) -> None: reveal_type(x) # revealed: Any & ~Literal[NeverUnequalEnum.A] ``` -## Narrowing tagged unions of nominal classes by attribute +## Narrowing tagged unions by attribute ```py -from typing import Literal +from typing import Literal, Protocol -class A: +from ty_extensions import Intersection + +class BaseA: tag: Literal["a"] + +class A(BaseA): field_a: int class B: tag: Literal["b"] field_b: str +class Marker(Protocol): + marked: bool + +class TaggedA(Protocol): + field_a: int + + @property + def tag(self) -> Literal["a"]: ... + +class TaggedB(Protocol): + field_b: str + + @property + def tag(self) -> Literal["b"]: ... + +class Container: + value: A | B | None + def _(x: A | B): if x.tag == "a": reveal_type(x) # revealed: A @@ -2254,6 +2276,44 @@ def _(x: A | B): reveal_type(x) # revealed: B else: reveal_type(x) # revealed: A + +def truthiness_guard(value: A | B | None): + if not value: + return + + reveal_type(value) # revealed: (A & ~AlwaysFalsy) | (B & ~AlwaysFalsy) + + if value.tag == "a": + reveal_type(value) # revealed: A & ~AlwaysFalsy + reveal_type(value.field_a) # revealed: int + else: + reveal_type(value) # revealed: B & ~AlwaysFalsy + reveal_type(value.field_b) # revealed: str + +def nested_attribute_after_truthiness_guard(container: Container): + if not container.value: + return + + if container.value.tag == "a": + reveal_type(container.value) # revealed: A & ~AlwaysFalsy + reveal_type(container.value.field_a) # revealed: int + else: + reveal_type(container.value) # revealed: B & ~AlwaysFalsy + reveal_type(container.value.field_b) # revealed: str + +def positive_intersection(value: Intersection[A, Marker] | Intersection[B, Marker]): + if value.tag == "a": + reveal_type(value) # revealed: A & Marker + else: + reveal_type(value) # revealed: B & Marker + +def protocol_union(value: TaggedA | TaggedB): + if value.tag == "a": + reveal_type(value) # revealed: TaggedA + reveal_type(value.field_a) # revealed: int + else: + reveal_type(value) # revealed: TaggedB + reveal_type(value.field_b) # revealed: str ``` Enum literals are also supported as attribute tags: diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md index e38eaaa816..76c7c2f863 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md @@ -146,6 +146,15 @@ def nonsingleton_newtype_tag(value: Foo | Bar, tag: BoolTag): reveal_type(value) # revealed: Foo | Bar else: reveal_type(value) # revealed: Foo | Bar + +def boolean_tags_after_truthiness(value: Foo | Bar | None): + if not value: + return + + if value.tag is True: + reveal_type(value) # revealed: Bar & ~AlwaysFalsy + else: + reveal_type(value) # revealed: Foo & ~AlwaysFalsy ``` ## `is` in chained comparisons diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md index cbea64c83d..ce737c8ea3 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md @@ -111,6 +111,17 @@ def _(response: Success | Failure | TruthyIntTag | FalsyIntTag | AmbiguousTag): reveal_type(response) # revealed: Success | TruthyIntTag | AmbiguousTag else: reveal_type(response) # revealed: Failure | FalsyIntTag | AmbiguousTag + +def truthiness_after_value_guard(response: Success | Failure | None): + if not response: + return + + if response.success: + reveal_type(response) # revealed: Success & ~AlwaysFalsy + reveal_type(response.result) # revealed: int + else: + reveal_type(response) # revealed: Failure & ~AlwaysFalsy + reveal_type(response.errors) # revealed: list[str] ``` ## Function Literals diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 148e3efb16..3a217abf09 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -4324,8 +4324,12 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } let narrowed = union.filter(self.db, |element| { - nominal_attribute_type(self.db, *element, attribute_name).is_none_or(|attribute_type| { - match (comparison, is_positive) { + element + .resolve_type_alias(self.db) + .member(self.db, attribute_name) + .place + .ignore_possibly_undefined() + .is_none_or(|attribute_type| match (comparison, is_positive) { (NominalAttributeComparison::Equality, true) => { !is_supported_tag_literal(attribute_type) || !attribute_type.is_disjoint_from(self.db, rhs_type) @@ -4337,8 +4341,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .identity_comparison_truthiness(self.db, rhs_type) .negate_if(!is_positive) .may_be_true(), - } - }) + }) }); if narrowed == Type::Union(union) { @@ -4362,14 +4365,19 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { }; let narrowed = union.filter(self.db, |element| { - nominal_attribute_type(self.db, *element, attribute_name).is_none_or(|attribute_type| { - let truthiness = attribute_type.bool(self.db); - if is_positive { - !truthiness.is_always_false() - } else { - !truthiness.is_always_true() - } - }) + element + .resolve_type_alias(self.db) + .member(self.db, attribute_name) + .place + .ignore_possibly_undefined() + .is_none_or(|attribute_type| { + let truthiness = attribute_type.bool(self.db); + if is_positive { + !truthiness.is_always_false() + } else { + !truthiness.is_always_true() + } + }) }); if narrowed == Type::Union(union) { @@ -4518,22 +4526,6 @@ fn is_supported_tag_literal(ty: Type) -> bool { ) } -fn nominal_attribute_type<'db>( - db: &'db dyn Db, - ty: Type<'db>, - attribute_name: &str, -) -> Option> { - let resolved_ty = ty.resolve_type_alias(db); - if resolved_ty.is_nominal_instance() { - resolved_ty - .member(db, attribute_name) - .place - .ignore_possibly_undefined() - } else { - None - } -} - // Return true if the given type is a `TypedDict` whose `field_name` field has a supported tag literal // type, or a union in which all elements that are `TypedDict`s have a supported tag literal type // for that field, or an intersection in which all positive elements that are `TypedDict`s have a From 62768b9af86cef49442eeffb9abcf2acd18239c8 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Tue, 28 Jul 2026 02:35:12 -0700 Subject: [PATCH 091/390] chore: bump gen-lsp-types to v0.11.0 (#27230) ## Summary Bumps gen-lsp-types to v0.11.0, which enables all LSP enumeration types to accept custom values. Fixes https://github.com/astral-sh/ty/issues/4082. ## Test Plan Tests are kept the same, only changed where necessary due to the library change. --- Cargo.lock | 4 +-- Cargo.toml | 2 +- crates/ruff_server/src/edit/notebook.rs | 31 ++++++++++------- crates/ty_server/src/document/notebook.rs | 33 +++++++++++-------- .../notifications/did_change_watched_files.rs | 2 ++ .../ty_server/tests/e2e/workspace_folders.rs | 2 +- 6 files changed, 45 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2ac903157..868cb2053a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1275,9 +1275,9 @@ dependencies = [ [[package]] name = "gen-lsp-types" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd635c5206acd03ea024d6b5902539e5c903de3afa220fdb5c94b583af77f4f" +checksum = "b64887ac3a8083427ae935a7296db876871582cd57eac077564f8bc18fa49116" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 36a52565a6..d0775eb890 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,7 +131,7 @@ libc = { version = "0.2.153" } libcst = { version = "1.8.4", default-features = false } log = { version = "0.4.17" } lsp-server = { version = "0.10.0" } -lsp-types = { package = "gen-lsp-types", version = "0.10.0", features = ["url"] } +lsp-types = { package = "gen-lsp-types", version = "0.11.0", features = ["url"] } matchit = { version = "0.9.0" } memchr = { version = "2.7.1" } mimalloc = { version = "0.1.49", features = ["v2"] } diff --git a/crates/ruff_server/src/edit/notebook.rs b/crates/ruff_server/src/edit/notebook.rs index ba15031389..d3a6c3f32d 100644 --- a/crates/ruff_server/src/edit/notebook.rs +++ b/crates/ruff_server/src/edit/notebook.rs @@ -63,25 +63,32 @@ impl NotebookDocument { let cells = self .cells .iter() - .map(|cell| match cell.kind { - NotebookCellKind::Code => ruff_notebook::Cell::Code(ruff_notebook::CodeCell { - execution_count: None, - id: None, - metadata: CellMetadata::default(), - outputs: vec![], - source: ruff_notebook::SourceValue::String( - cell.document.contents().to_string(), - ), - }), + .filter_map(|cell| match cell.kind { + NotebookCellKind::Code => { + Some(ruff_notebook::Cell::Code(ruff_notebook::CodeCell { + execution_count: None, + id: None, + metadata: CellMetadata::default(), + outputs: vec![], + source: ruff_notebook::SourceValue::String( + cell.document.contents().to_string(), + ), + })) + } NotebookCellKind::Markup => { - ruff_notebook::Cell::Markdown(ruff_notebook::MarkdownCell { + Some(ruff_notebook::Cell::Markdown(ruff_notebook::MarkdownCell { attachments: None, id: None, metadata: CellMetadata::default(), source: ruff_notebook::SourceValue::String( cell.document.contents().to_string(), ), - }) + })) + } + NotebookCellKind::Custom(_) => { + // Ignore unsupported cell kinds. This arm should never be reached unless a + // client sends a value which is not mentioned/supported in the LSP. + None } }) .collect(); diff --git a/crates/ty_server/src/document/notebook.rs b/crates/ty_server/src/document/notebook.rs index c538a7b7c6..946bdbfe20 100644 --- a/crates/ty_server/src/document/notebook.rs +++ b/crates/ty_server/src/document/notebook.rs @@ -72,7 +72,7 @@ impl NotebookDocument { let cells = self .cells .iter() - .map(|cell| { + .filter_map(|cell| { let cell_text = if let Ok(document) = index.document(&DocumentKey::from_uri(&cell.uri)) { if let Some(text_document) = document.as_text() { @@ -89,23 +89,30 @@ impl NotebookDocument { let source = ruff_notebook::SourceValue::String(cell_text); match cell.kind { - NotebookCellKind::Code => ruff_notebook::Cell::Code(ruff_notebook::CodeCell { - execution_count: cell - .execution_summary - .as_ref() - .map(|summary| i64::from(summary.execution_order)), - id: None, - metadata: CellMetadata::default(), - outputs: vec![], - source, - }), + NotebookCellKind::Code => { + Some(ruff_notebook::Cell::Code(ruff_notebook::CodeCell { + execution_count: cell + .execution_summary + .as_ref() + .map(|summary| i64::from(summary.execution_order)), + id: None, + metadata: CellMetadata::default(), + outputs: vec![], + source, + })) + } NotebookCellKind::Markup => { - ruff_notebook::Cell::Markdown(ruff_notebook::MarkdownCell { + Some(ruff_notebook::Cell::Markdown(ruff_notebook::MarkdownCell { attachments: None, id: None, metadata: CellMetadata::default(), source, - }) + })) + } + NotebookCellKind::Custom(_) => { + // Ignore unsupported cell kinds. This arm should never be reached unless a + // client sends a value which is not mentioned/supported in the LSP. + None } } }) diff --git a/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs b/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs index a04b19cdad..b0e866838d 100644 --- a/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs +++ b/crates/ty_server/src/server/api/notifications/did_change_watched_files.rs @@ -59,6 +59,8 @@ impl SyncNotificationHandler for DidChangeWatchedFiles { path: system_path, kind: DeletedKind::Any, }, + // Custom file change types are not supported and should be ignored. + FileChangeType::Custom(_) => continue, }; changes.push(change_event); diff --git a/crates/ty_server/tests/e2e/workspace_folders.rs b/crates/ty_server/tests/e2e/workspace_folders.rs index 0fa0fc9839..874e72a4ab 100644 --- a/crates/ty_server/tests/e2e/workspace_folders.rs +++ b/crates/ty_server/tests/e2e/workspace_folders.rs @@ -759,7 +759,7 @@ fn condensed_full_document_diagnostic_report(report: FullDocumentDiagnosticRepor Some(DiagnosticSeverity::Warning) => "WARNING", Some(DiagnosticSeverity::Information) => "INFORMATION", Some(DiagnosticSeverity::Hint) => "HINT", - None => "unknown", + Some(DiagnosticSeverity::Custom(_)) | None => "unknown", }; let Message::String(message) = d.message else { panic!( From 9d07aa093a50c19fe453c7fdf04b344a36452d8c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 28 Jul 2026 07:35:02 -0400 Subject: [PATCH 092/390] [ty] Support comprehension walruses in IDE features (#26476) ## Summary In #26466, we added semantic modeling for walrus targets from comprehensions in the containing semantic scope. This follow-up teaches IDE features to resolve the synthetic eager bindings back to their user-visible walrus definitions. This makes go-to-definition select the walrus target and allows references and rename to follow module/global comprehension walruses across files. --- crates/ty_ide/src/find_references.rs | 113 +++++++++++++ crates/ty_ide/src/goto_definition.rs | 158 +++++++++++++++++- crates/ty_ide/src/references.rs | 57 ++++++- crates/ty_ide/src/rename.rs | 91 ++++++++++ crates/ty_python_core/src/definition.rs | 51 +++++- .../src/types/ide_support.rs | 86 ++++++++-- .../src/types/ide_support/unused_bindings.rs | 88 +++++++++- .../src/types/infer/builder.rs | 56 +------ 8 files changed, 629 insertions(+), 71 deletions(-) diff --git a/crates/ty_ide/src/find_references.rs b/crates/ty_ide/src/find_references.rs index a490398d08..bac9fb84b3 100644 --- a/crates/ty_ide/src/find_references.rs +++ b/crates/ty_ide/src/find_references.rs @@ -89,6 +89,119 @@ mod tests { } } + #[test] + fn references_do_not_mix_global_and_nonlocal_comprehension_walruses() { + let test = cursor_test( + " +last = 0 + +def outer(): + last = 1 + + def write_global(): + global last + [(last := global_item) for global_item in [2]] + + def write_nonlocal(): + nonlocal last + [(last := nonlocal_item) for nonlocal_item in [3]] + + write_global() + write_nonlocal() + return last +", + ); + + assert_snapshot!(test.references(), @" + info[references]: Found 4 references + --> main.py:5:5 + | + 5 | last = 1 + | ---- + | + ::: main.py:12:18 + | + 12 | nonlocal last + | ---- + 13 | [(last := nonlocal_item) for nonlocal_item in [3]] + | ---- + 14 | + 15 | write_global() + 16 | write_nonlocal() + 17 | return last + | ---- + | + "); + } + + #[test] + fn comprehension_walrus_references_in_function() { + let test = cursor_test( + " +def f(items): + [(last := item) for item in items] + return last +", + ); + + assert_snapshot!(test.references(), @" + info[references]: Found 2 references + --> main.py:3:7 + | + 3 | [(last := item) for item in items] + | ---- + 4 | return last + | ---- + | + "); + } + + #[test] + fn nested_comprehension_walrus_references_in_function() { + let test = cursor_test( + " +def f(items): + [[(last := item) for item in items] for _ in [1]] + return last +", + ); + + assert_snapshot!(test.references(), @" + info[references]: Found 2 references + --> main.py:3:8 + | + 3 | [[(last := item) for item in items] for _ in [1]] + | ---- + 4 | return last + | ---- + | + "); + } + + #[test] + fn comprehension_walrus_references_across_files() { + let test = CursorTest::builder() + .source("lib.py", "[(last := item) for item in [1]]\n") + .source("main.py", "from lib import last\nprint(last)\n") + .build(); + + assert_snapshot!(test.references(), @" + info[references]: Found 3 references + --> lib.py:1:3 + | + 1 | [(last := item) for item in [1]] + | ---- + | + ::: main.py:1:17 + | + 1 | from lib import last + | ---- + 2 | print(last) + | ---- + | + "); + } + #[test] fn parameter_references_in_function() { let test = cursor_test( diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index 4a88b34d56..9f6295b0ef 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -33,7 +33,7 @@ pub fn goto_definition( #[cfg(test)] pub(super) mod test { - use crate::tests::{CursorTest, IntoDiagnostic}; + use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; use crate::{NavigationTargets, RangedValue, goto_definition}; use insta::assert_snapshot; use ruff_db::diagnostic::{ @@ -42,6 +42,162 @@ pub(super) mod test { }; use ruff_text_size::Ranged; + #[test] + fn goto_definition_does_not_mix_global_and_nonlocal_comprehension_walruses() { + let test = cursor_test( + " +last = 0 + +def outer(): + last = 1 + + def write_global(): + global last + [(last := global_item) for global_item in [2]] + + def write_nonlocal(): + nonlocal last + [(last := nonlocal_item) for nonlocal_item in [3]] + + write_global() + write_nonlocal() + return last +", + ); + + assert_snapshot!(test.goto_definition(), @" + info[goto-definition]: Go to definition + --> main.py:17:12 + | + 17 | return last + | ^^^^ Clicking here + | + info: Found 2 definitions + --> main.py:5:5 + | + 5 | last = 1 + | ---- + | + ::: main.py:13:11 + | + 13 | [(last := nonlocal_item) for nonlocal_item in [3]] + | ---- + | + "); + } + + #[test] + fn goto_definition_comprehension_walrus_in_function() { + let test = cursor_test( + " +def f(items): + [(last := item) for item in items] + return last +", + ); + + assert_snapshot!(test.goto_definition(), @" + info[goto-definition]: Go to definition + --> main.py:4:12 + | + 4 | return last + | ^^^^ Clicking here + | + info: Found 1 definition + --> main.py:3:7 + | + 3 | [(last := item) for item in items] + | ---- + | + "); + } + + #[test] + fn goto_definition_nested_comprehension_walrus_in_function() { + let test = cursor_test( + " +def f(items): + [[(last := item) for item in items] for _ in [1]] + return last +", + ); + + assert_snapshot!(test.goto_definition(), @" + info[goto-definition]: Go to definition + --> main.py:4:12 + | + 4 | return last + | ^^^^ Clicking here + | + info: Found 1 definition + --> main.py:3:8 + | + 3 | [[(last := item) for item in items] for _ in [1]] + | ---- + | + "); + } + + #[test] + fn goto_definition_imported_comprehension_walrus() { + let test = CursorTest::builder() + .source("lib.py", "[(last := item) for item in [1]]\n") + .source("main.py", "from lib import last\nprint(last)\n") + .build(); + + assert_snapshot!(test.goto_definition(), @" + info[goto-definition]: Go to definition + --> main.py:2:7 + | + 2 | print(last) + | ^^^^ Clicking here + | + info: Found 1 definition + --> lib.py:1:3 + | + 1 | [(last := item) for item in [1]] + | ---- + | + "); + } + + #[test] + fn goto_definition_nonlocal_comprehension_walrus() { + let test = cursor_test( + " +def outer(items): + last = 0 + + def inner(): + nonlocal last + [(last := item) for item in items] + return last + + return inner() +", + ); + + assert_snapshot!(test.goto_definition(), @" + info[goto-definition]: Go to definition + --> main.py:8:16 + | + 8 | return last + | ^^^^ Clicking here + | + info: Found 2 definitions + --> main.py:3:5 + | + 3 | last = 0 + | ---- + 4 | + 5 | def inner(): + 6 | nonlocal last + 7 | [(last := item) for item in items] + | ---- + | + "); + } + #[test] fn goto_definition_relative_import() { let test = CursorTest::builder() diff --git a/crates/ty_ide/src/references.rs b/crates/ty_ide/src/references.rs index 5e4204b0b5..4af8da9033 100644 --- a/crates/ty_ide/src/references.rs +++ b/crates/ty_ide/src/references.rs @@ -253,10 +253,16 @@ pub(crate) fn has_any_external_visible_definitions( definitions.iter().any(|definition| match definition { ResolvedDefinition::Definition(definition) => match definition.scope(db).scope(db).kind() { ScopeKind::Module | ScopeKind::Class => true, + ScopeKind::Comprehension => { + matches!(definition.kind(db), DefinitionKind::NamedExpression(_)) + && definition.place(db).as_symbol().is_some_and(|symbol_id| { + ty_python_core::semantic_index(db, definition.file(db)) + .symbol_resolves_to_global_scope(symbol_id, definition.file_scope(db)) + }) + } ScopeKind::TypeParams | ScopeKind::Function | ScopeKind::Lambda - | ScopeKind::Comprehension | ScopeKind::TypeAlias => false, }, ResolvedDefinition::Module(_) | ResolvedDefinition::FileWithRange(_) => true, @@ -812,6 +818,23 @@ mod tests { fn externally_visible_definitions_can_have_cross_file_references() { for (case, source) in [ ("module-global", "x = 1"), + ( + "module comprehension walrus", + "[(x := item) for item in [1]]", + ), + ( + "nested module comprehension walrus", + "[[(x := item) for item in [1]] for _ in [1]]", + ), + ( + "explicit global comprehension walrus", + " +x = 0 +def f(): + global x + [(x := item) for item in [1]] +", + ), ( "class", " @@ -838,6 +861,38 @@ def f(): ), ("lambda", "f = lambda x: x"), ("comprehension", "xs = [x for x in range(3)]"), + ( + "function comprehension walrus", + " +def f(): + [(x := item) for item in [1]] + return x +", + ), + ( + "nested function comprehension walrus", + " +def f(): + [[(x := item) for item in [1]] for _ in [1]] + return x +", + ), + ( + "lambda comprehension walrus", + "f = lambda: [(x := item) for item in [1]]", + ), + ( + "explicit nonlocal comprehension walrus", + " +def outer(): + x = 0 + def inner(): + nonlocal x + [(x := item) for item in [1]] + inner() + return x +", + ), ("type parameters", "type Alias[T] = list[T]"), ] { let test = cursor_test(source); diff --git a/crates/ty_ide/src/rename.rs b/crates/ty_ide/src/rename.rs index dea7353930..050bcd30b7 100644 --- a/crates/ty_ide/src/rename.rs +++ b/crates/ty_ide/src/rename.rs @@ -165,6 +165,97 @@ mod tests { } } + #[test] + fn rename_does_not_mix_global_and_nonlocal_comprehension_walruses() { + let test = cursor_test( + " +last = 0 + +def outer(): + last = 1 + + def write_global(): + global last + [(last := global_item) for global_item in [2]] + + def write_nonlocal(): + nonlocal last + [(last := nonlocal_item) for nonlocal_item in [3]] + + write_global() + write_nonlocal() + return last +", + ); + + assert_snapshot!(test.rename("result"), @" + info[rename]: Rename symbol (found 4 locations) + --> main.py:5:5 + | + 5 | last = 1 + | ^^^^ + | + ::: main.py:12:18 + | + 12 | nonlocal last + | ---- + 13 | [(last := nonlocal_item) for nonlocal_item in [3]] + | ---- + 14 | + 15 | write_global() + 16 | write_nonlocal() + 17 | return last + | ---- + | + "); + } + + #[test] + fn rename_comprehension_walrus_in_function() { + let test = cursor_test( + " +def f(items): + [(last := item) for item in items] + return last +", + ); + + assert_snapshot!(test.rename("result"), @" + info[rename]: Rename symbol (found 2 locations) + --> main.py:3:7 + | + 3 | [(last := item) for item in items] + | ^^^^ + 4 | return last + | ---- + | + "); + } + + #[test] + fn rename_comprehension_walrus_across_files() { + let test = CursorTest::builder() + .source("lib.py", "[(last := item) for item in [1]]\n") + .source("main.py", "from lib import last\nprint(last)\n") + .build(); + + assert_snapshot!(test.rename("result"), @" + info[rename]: Rename symbol (found 3 locations) + --> lib.py:1:3 + | + 1 | [(last := item) for item in [1]] + | ^^^^ + | + ::: main.py:1:17 + | + 1 | from lib import last + | ---- + 2 | print(last) + | ---- + | + "); + } + #[test] fn prepare_rename_parameter() { let test = cursor_test( diff --git a/crates/ty_python_core/src/definition.rs b/crates/ty_python_core/src/definition.rs index f1f2fe7767..df1d8a6aee 100644 --- a/crates/ty_python_core/src/definition.rs +++ b/crates/ty_python_core/src/definition.rs @@ -1603,7 +1603,9 @@ pub struct NestedBindingsDefinitionKind { } impl NestedBindingsDefinitionKind { - /// Returns the binding source for each nested declaration, along with whether it is global. + /// Returns every nested binding source and whether it was declared `global`. + /// + /// Use [`Self::visible_binding_sources`] when resolving the binding in a particular scope. pub fn binding_sources<'index, 'db>( &'index self, index: &'index SemanticIndex<'db>, @@ -1621,6 +1623,53 @@ impl NestedBindingsDefinitionKind { Some((declaration.is_global(), bindings)) }) } + + /// Returns nested binding sources that can update the same variable as `scope`. + /// + /// A synthetic binding can collect both `global` and `nonlocal` writes to one name: + /// + /// ```python + /// x = 0 + /// + /// def outer(): + /// x = 1 + /// + /// def change_global(): + /// global x + /// x = 2 + /// + /// def change_nonlocal(): + /// nonlocal x + /// x = 3 + /// ``` + /// + /// Only `change_nonlocal` can update `outer`'s local `x`. Nested functions also cannot + /// capture a class-local variable, so class scopes do not see nonlocal writes to their + /// own bindings. + pub fn visible_binding_sources<'index, 'db>( + &'index self, + index: &'index SemanticIndex<'db>, + scope: FileScopeId, + ) -> impl Iterator> + 'index { + let symbol_id = index.place_table(scope).symbol_id(&self.name); + let sees_global = symbol_id + .is_some_and(|symbol_id| index.symbol_resolves_to_global_scope(symbol_id, scope)); + let sees_nonlocal = !sees_global + && symbol_id.is_some_and(|symbol_id| { + !(index.scope(scope).kind().is_class() + && index.place_table(scope).symbol(symbol_id).is_local()) + }); + + self.binding_sources(index) + .filter_map(move |(is_global, bindings)| { + (if is_global { + sees_global + } else { + sees_nonlocal + }) + .then_some(bindings) + }) + } } /// Describes when writes from a nested scope can affect its containing scope. diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 8d48d09081..e62845d9b8 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use crate::FxIndexSet; use crate::place::builtins_module_scope; @@ -22,7 +22,7 @@ use ruff_python_ast::{self as ast, AnyNodeRef, name::Name}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; use ty_module_resolver::Module; -use ty_python_core::definition::{Definition, DefinitionKind}; +use ty_python_core::definition::{Definition, DefinitionKind, NestedBindingExecution}; use ty_python_core::{attribute_scopes, global_scope, semantic_index, use_def_map}; mod unreachable_code; @@ -82,11 +82,31 @@ pub fn definitions_for_name<'db>( continue; // Name not found in this scope, try parent scope }; + let use_def_map = index.use_def_map(scope_id); + // Check if this place is marked as global or nonlocal let place_expr = place_table.symbol(symbol_id); let is_global = place_expr.is_global(); let is_nonlocal = place_expr.is_nonlocal(); + if is_global || is_nonlocal { + // Assignments in a forwarding scope remain valid navigation targets, including eager + // walrus bindings exported from comprehensions. + all_definitions.extend(user_visible_definitions( + db, + use_def_map + .reachable_symbol_bindings(symbol_id) + .filter_map(|binding| binding.binding.definition()) + .filter(|definition| match definition.kind(db) { + DefinitionKind::NamedExpression(_) => true, + DefinitionKind::NestedBindings(nested) => { + nested.execution == NestedBindingExecution::Eager + } + _ => false, + }), + )); + } + // TODO: The current algorithm doesn't return definitions or bindings // for other scopes that are outside of this scope hierarchy that target // this name using a nonlocal or global binding. The semantic analyzer @@ -100,7 +120,7 @@ pub fn definitions_for_name<'db>( if let Some(global_symbol_id) = global_place_table.symbol_id(name_str) { let global_use_def_map = ty_python_core::use_def_map(db, global_scope_id); - all_definitions.extend(reachable_definitions( + all_definitions.extend(user_visible_definitions( db, global_use_def_map .reachable_symbol_bindings(global_symbol_id) @@ -121,10 +141,8 @@ pub fn definitions_for_name<'db>( continue; } - let use_def_map = index.use_def_map(scope_id); - // Get all definitions (both bindings and declarations) for this place - all_definitions.extend(reachable_definitions( + all_definitions.extend(user_visible_definitions( db, use_def_map .reachable_symbol_bindings(symbol_id) @@ -1006,14 +1024,54 @@ fn collect_implementation_root_classes<'db>( } } -fn reachable_definitions<'db>( +/// Returns the user-visible definitions represented by a use-def binding. +/// +/// Comprehension walruses are represented in the containing scope by synthetic eager bindings: +/// +/// ```python +/// [(last := item) for item in items] +/// print(last) # Go to definition should select `last := item` above. +/// ``` +/// +/// The binding for the use in `print` is synthetic, so follow it into the comprehension's +/// end-of-scope bindings. Nested comprehensions can produce a chain of these proxies. Only +/// follow sources that resolve to the same variable, so `global` and `nonlocal` writes do not +/// become definitions of each other. +fn user_visible_definitions<'db>( db: &'db dyn Db, definitions: impl IntoIterator>, ) -> FxIndexSet> { - definitions - .into_iter() - .filter(|definition| definition.kind(db).is_user_visible()) - .collect() + let mut pending = definitions.into_iter().collect::>(); + let mut seen = FxHashSet::default(); + let mut result = FxIndexSet::default(); + + while let Some(definition) = pending.pop_front() { + if !seen.insert(definition) { + continue; + } + + match definition.kind(db) { + DefinitionKind::NestedBindings(nested) => { + let index = semantic_index(db, definition.file(db)); + let sources = nested + .visible_binding_sources(index, definition.file_scope(db)) + .flatten() + .filter_map(|binding| binding.binding.definition()); + // A lazy function proxy can lead to an eager comprehension proxy. Follow that + // proxy-only chain without exposing ordinary lazy nested assignments. + pending.extend(sources.filter(|source| { + nested.execution == NestedBindingExecution::Eager + || matches!(source.kind(db), DefinitionKind::NestedBindings(_)) + })); + } + kind if kind.is_user_visible() => { + result.insert(definition); + } + _ => {} + } + } + + result } fn reachable_implementation_definitions<'db>( @@ -1073,7 +1131,7 @@ fn resolve_reachable_definitions<'db>( symbol_name: &str, definitions: impl IntoIterator>, ) -> Vec> { - reachable_definitions(db, definitions) + user_visible_definitions(db, definitions) .into_iter() .flat_map(|definition| { resolve_definition( @@ -2320,7 +2378,9 @@ mod resolve_definition { } } - definitions + super::user_visible_definitions(db, definitions) + .into_iter() + .collect() } /// Given a definition that may be in a stub file, find the "real" definition in a non-stub. diff --git a/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs b/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs index 3f2aa11dd5..6f318a9029 100644 --- a/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs +++ b/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs @@ -44,6 +44,36 @@ fn should_consider_definition(kind: &DefinitionKind<'_>) -> bool { } } +/// Returns whether a comprehension walrus belongs to an enclosing function or lambda. +/// +/// ```python +/// def last_item(items): +/// [(last := item) for item in items] +/// return last +/// ``` +/// +/// A module-level walrus, or one declared `global` or `nonlocal` in its containing +/// function, is not a local binding and must not receive an unused-binding diagnostic. +fn comprehension_named_expression_is_local( + index: &SemanticIndex<'_>, + comprehension_scope: FileScopeId, + name: &str, +) -> bool { + index + .ancestor_scopes(comprehension_scope) + .skip(1) + .find(|(_, scope)| scope.kind() != ScopeKind::Comprehension) + .is_some_and(|(scope_id, scope)| { + matches!(scope.kind(), ScopeKind::Function | ScopeKind::Lambda) + && index + .place_table(scope_id) + .symbol_id(name) + .is_some_and(|symbol_id| { + index.place_table(scope_id).symbol(symbol_id).is_local() + }) + }) +} + fn function_scope_is_overload_declaration( db: &dyn Db, index: &SemanticIndex<'_>, @@ -77,6 +107,14 @@ pub fn unused_bindings(db: &dyn Db, file: ruff_db::files::File) -> Box<[UnusedBi let is_stub_file = file.is_stub(db); let index = semantic_index(db, file); let mut unused = Vec::new(); + // A used synthetic definition counts as a use of the user-visible definitions it represents. + let used_definitions = index.scope_ids().flat_map(|scope_id| { + index + .use_def_map(scope_id.file_scope_id(db)) + .all_definitions_with_usage() + .filter_map(|(_, state, is_used)| is_used.then_some(state.definition()).flatten()) + }); + let used_user_visible_definitions = super::user_visible_definitions(db, used_definitions); for scope_id in index.scope_ids() { let file_scope_id = scope_id.file_scope_id(db); @@ -107,6 +145,7 @@ pub fn unused_bindings(db: &dyn Db, file: ruff_db::files::File) -> Box<[UnusedBi let DefinitionState::Defined(definition) = state else { continue; }; + let is_used = is_used || used_user_visible_definitions.contains(&definition); if is_used { let DefinitionKind::LoopHeader(loop_header_definition) = definition.kind(db) else { @@ -158,7 +197,12 @@ pub fn unused_bindings(db: &dyn Db, file: ruff_db::files::File) -> Box<[UnusedBi // Global and nonlocal assignments target bindings from outer scopes. // Treat them as externally managed to avoid false positives here. - if symbol.is_global() || symbol.is_nonlocal() { + let is_local_comprehension_named_expression = scope_kind == ScopeKind::Comprehension + && matches!(kind, DefinitionKind::NamedExpression(_)) + && comprehension_named_expression_is_local(index, file_scope_id, name); + if (symbol.is_global() || symbol.is_nonlocal()) + && !is_local_comprehension_named_expression + { continue; } @@ -278,6 +322,8 @@ mod tests { let source = dedent( " module_dead = 1 + [(module_walrus := item) for item in [1]] + [[(nested_module_walrus := item) for item in [1]] for _ in [1]] class C: class_dead = 1 @@ -320,6 +366,7 @@ mod tests { def mutate_global(): global global_value global_value = 1 + [(global_value := item) for item in [1]] local_dead = 1 def outer(): @@ -328,6 +375,7 @@ mod tests { def inner(): nonlocal captured captured = 1 + [(captured := item) for item in [1]] inner() return captured @@ -339,6 +387,44 @@ mod tests { Ok(()) } + #[test] + fn tracks_comprehension_walruses_in_local_scopes() -> anyhow::Result<()> { + let source = dedent( + " + def used(items): + [(used_walrus := item) for item in items] + return used_walrus + + def unused(items): + [(unused_walrus := item) for item in items] + + def nested_used(items): + [[(nested_used_walrus := item) for item in items] for _ in [1]] + return nested_used_walrus + + def nested_unused(items): + [[(nested_unused_walrus := item) for item in items] for _ in [1]] + + used_lambda = lambda items: ( + [(used_lambda_walrus := item) for item in items], + used_lambda_walrus, + ) + unused_lambda = lambda items: [(unused_lambda_walrus := item) for item in items] + ", + ); + + let names = collect_unused_names(&source)?; + assert_eq!( + names, + vec![ + "nested_unused_walrus", + "unused_lambda_walrus", + "unused_walrus", + ] + ); + Ok(()) + } + #[test] fn reports_unused_parameter_for_overriding_method() -> anyhow::Result<()> { let source = dedent( diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 04b64581f3..3f02a43b18 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2441,61 +2441,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { const MAX_EXACT_NESTED_BINDING_REACHABILITY_NODES: usize = 2048; let db = self.db(); - let scope = definition.scope(db); - let scope_id = scope.file_scope_id(db); - let symbol_id = definition - .place(db) - .as_symbol() - .expect("nested bindings definition should be a symbol"); - let symbol = self.index.place_table(scope_id).symbol(symbol_id); - - // At the point where a nested bindings definition is first synthesized, we don't - // necessarily know whether the current scope will see global or nonlocal bindings. - // Consider this example: - // - // def outer(): - // def inner1(): - // global x - // x = 1 - // def inner2(): - // nonlocal x - // x = 2 - // # Nested bindings of both kinds are potentially visible here, but we can't - // # actually use both kinds in the same scope. If `print(x)` comes next, we should - // # only see 2. But if `global x; print(x)` comes next, we should only see 1. - // ... - // - // By the time we get here in type inference, though, we can ask the semantic index whether - // the symbol resolves to the global scope or not. (For free variables, this currently - // requires walking ancestor scopes.) If so, we see any nested `global` bindings that were - // recorded. If not, we see any nested `nonlocal` ones. - // - // Note that if `x` is a free variable in this scope, then this synthetic binding will not - // shadow `UNBOUND`, and `infer_place_load` will walk to the defining scope and see all the - // nested bindings from there via the symbol's public type. However, we still want to - // respect locally visible nested bindings in that case, because there might be narrowing - // constraints that apply to the public type but not these nested bindings. - let this_scope_sees_global_bindings = self - .index - .symbol_resolves_to_global_scope(symbol_id, scope_id); - - // If a function body binds `x`, it's interested in nested `nonlocal` bindings of `x` too, - // because those resolve to the same variable. But if a *class* body binds `x`, it does - // *not* want to consider nested bindings of `x`, because those do *not* resolve to the - // same variable. - let this_scope_sees_nonlocal_bindings = !(this_scope_sees_global_bindings - || (scope.scope(db).kind().is_class() && symbol.is_local())); - + let scope_id = definition.file_scope(db); let mut binding_sources = nested_bindings_kind - .binding_sources(self.index) - .filter_map(|(is_global, bindings)| { - (if is_global { - this_scope_sees_global_bindings - } else { - this_scope_sees_nonlocal_bindings - }) - .then_some(bindings) - }) + .visible_binding_sources(self.index, scope_id) .peekable(); if binding_sources.peek().is_some() && self From b6457d54f893d4127a9585a09c9e3a73f5bda94d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 28 Jul 2026 10:00:25 -0400 Subject: [PATCH 093/390] [ty] Preserve frozen-dataclass setter delegation (#27217) ## Summary Prior to this change, we modeled a frozen dataclass's inherited `__setattr__` as an overload that rejects known frozen fields and returns `None` for every other attribute. That catch-all loses the actual setter that Python calls after the frozen base. For example, this assignment must be rejected because the next base's setter never returns: ```python from dataclasses import dataclass from typing import Never @dataclass(frozen=True) class Frozen: x: int = 1 class RejectsAssignment: y: int = 1 def __setattr__(self, name: str, value: object) -> Never: raise AttributeError(name) class Child(Frozen, RejectsAssignment): ... Child().y = 2 ``` Previously, we accepted it. We also rejected valid assignments when a later setter explicitly accepts a value for an otherwise read-only property. We now collect fields from every reachable frozen base, distinguish frozen fields from attributes that are delegated through `super()`, and validate delegated assignments against the actual next setter. This preserves setter argument checking and declared attribute types, handles both generic syntaxes, excludes `InitVar` arguments, and keeps the existing behavior for slotted frozen dataclasses. This is a prerequisite for #27001: it fixes the existing assignment behavior and establishes the shared frozen-method dispatch that the deletion change can reuse without expanding its scope. --- .../mdtest/dataclasses/dataclasses.md | 209 ++++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 3 +- .../src/types/class/static_literal.rs | 153 ++++++++++++- .../infer/builder/attribute_assignment.rs | 83 ++++++- 4 files changed, 424 insertions(+), 24 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index d5f9e2341d..3ba577ef18 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -810,6 +810,215 @@ grandchild.z = 2 grandchild.unknown = 2 ``` +When another base class rejects assignment, a frozen dataclass must not hide its `__setattr__` +method: + +```py +from dataclasses import dataclass +from typing import NoReturn + +@dataclass(frozen=True) +class Frozen: + x: int = 1 + +class RejectsAssignment: + y: int = 1 + + def __setattr__(self, name: str, value: object) -> NoReturn: + raise AttributeError(name) + +class ChildWithRejectingAssignmentBase(Frozen, RejectsAssignment): ... + +# error: [invalid-assignment] "Cannot assign to attribute `y` on type `ChildWithRejectingAssignmentBase` whose `__setattr__` method returns `Never`/`NoReturn`" +ChildWithRejectingAssignmentBase().y = 2 +``` + +A later base class can customize assignment to an ordinary attribute. The value must satisfy both +the later `__setattr__` and the attribute declaration: + +```py +class AllowsAssignment: + y: object = 1 + + def __setattr__(self, name: str, value: int) -> None: ... + +class ChildWithAllowingAssignmentBase(Frozen, AllowsAssignment): ... + +allowed = ChildWithAllowingAssignmentBase() +allowed.y = 2 + +# error: [invalid-assignment] "Cannot assign object of type" +allowed.y = "invalid" +``` + +A later `__setattr__` can forward to `object.__setattr__`, which still invokes data descriptors: + +```py +class ForwardsAssignment: + def __setattr__(self, name: str, value: object) -> None: + super().__setattr__(name, value) +``` + +A read-only property therefore remains read-only: + +```py +class ReadOnlyPropertyBase(ForwardsAssignment): + @property + def y(self) -> int: + return 1 + +class ChildWithReadOnlyProperty(Frozen, ReadOnlyPropertyBase): ... + +# error: [invalid-assignment] "Cannot assign to read-only property `y` on object of type `ChildWithReadOnlyProperty`" +ChildWithReadOnlyProperty().y = 2 +``` + +The property's setter still determines which values it accepts: + +```py +class TypedPropertyBase(ForwardsAssignment): + @property + def y(self) -> int: + return 1 + + @y.setter + def y(self, value: int) -> None: ... + +class ChildWithTypedProperty(Frozen, TypedPropertyBase): ... + +# error: [invalid-assignment] "Expected `int`, found `Literal["invalid"]`" +ChildWithTypedProperty().y = "invalid" +``` + +A property setter that never returns prevents assignment: + +```py +class TerminalPropertyBase(ForwardsAssignment): + @property + def y(self) -> int: + return 1 + + @y.setter + def y(self, value: int) -> NoReturn: + raise AttributeError + +class ChildWithTerminalProperty(Frozen, TerminalPropertyBase): ... + +# error: [invalid-assignment] "Cannot assign to attribute `y` on type `ChildWithTerminalProperty` whose `__set__` method returns `Never`/`NoReturn`" +ChildWithTerminalProperty().y = 2 +``` + +The same rule applies to a custom descriptor whose setter never returns: + +```py +class TerminalDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> int: + return 1 + + def __set__(self, instance: object, value: int) -> NoReturn: + raise AttributeError + +class TerminalDescriptorBase(ForwardsAssignment): + y: TerminalDescriptor = TerminalDescriptor() + +class ChildWithTerminalDescriptor(Frozen, TerminalDescriptorBase): ... + +# error: [invalid-assignment] "Cannot assign to attribute `y` on type `ChildWithTerminalDescriptor` whose `__set__` method returns `Never`/`NoReturn`" +ChildWithTerminalDescriptor().y = 2 +``` + +A later `__setattr__` does not make the declared type of an ordinary attribute disappear: + +```py +class AllowsUntypedAssignment: + y: int = 1 + + def __setattr__(self, name: str, value: object) -> None: ... + +class ChildWithUntypedAssignmentBase(Frozen, AllowsUntypedAssignment): ... + +# error: [invalid-assignment] +ChildWithUntypedAssignmentBase().y = "invalid" +``` + +A specialized `Generic[T]` frozen base must also preserve the next `__setattr__` in the MRO: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +@dataclass(frozen=True) +class GenericFrozen(Generic[T]): + value: T + +class RejectingGenericAssignmentChild(GenericFrozen[int], RejectsAssignment): ... + +# error: [invalid-assignment] "Cannot assign to attribute `y` on type `RejectingGenericAssignmentChild` whose `__setattr__` method returns `Never`/`NoReturn`" +RejectingGenericAssignmentChild(1).y = 2 +``` + +The same behavior applies to Python 3.12 type-parameter syntax: + +```py +@dataclass(frozen=True) +class TypeParameterFrozen[T]: + value: T + +class RejectingTypeParameterAssignmentChild(TypeParameterFrozen[int], RejectsAssignment): ... + +# error: [invalid-assignment] "Cannot assign to attribute `y` on type `RejectingTypeParameterAssignmentChild` whose `__setattr__` method returns `Never`/`NoReturn`" +RejectingTypeParameterAssignmentChild(1).y = 2 +``` + +When a subclass inherits from two frozen dataclasses, assignments to fields from both bases remain +frozen: + +```py +@dataclass(frozen=True) +class FirstFrozen: + first: int = 1 + +@dataclass(frozen=True) +class SecondFrozen: + second: int = 1 + +class ChildWithTwoFrozenBases(FirstFrozen, SecondFrozen): ... + +multiple = ChildWithTwoFrozenBases() +# revealed: Overload[(name: Literal["first"], value) -> Never, (name: Literal["second"], value) -> Never, (name: str, value) -> None] +reveal_type(multiple.__setattr__) + +multiple.second = 2 # error: [invalid-assignment] +``` + +An `InitVar` is a constructor argument, not a frozen field. A subclass can assign to an attribute +with the same name: + +```py +from dataclasses import InitVar + +@dataclass(frozen=True) +class FrozenWithInitVar: + temporary: InitVar[int] = 0 + +class ChildWithInitVar(FrozenWithInitVar): + temporary: int = 1 + +init_var_child = ChildWithInitVar() +init_var_child.temporary = 4 +``` + +The same rule applies when the `InitVar` belongs to a second frozen base: + +```py +class ChildWithSecondBaseInitVar(Frozen, FrozenWithInitVar): + temporary: int = 1 + +second_init_var_child = ChildWithSecondBaseInitVar() +second_init_var_child.temporary = 4 +``` + Non-field attributes on subclasses of slotted frozen dataclasses are still rejected. This correctly models the runtime behavior, but is somewhat surprising and may be a CPython bug, as subclasses of slotted classes usually allow arbitrary attributes to be set on them unless the subclass also diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 23b524b8c6..4453e10703 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -10,7 +10,8 @@ pub(super) use self::named_tuple::{ DynamicNamedTupleAnchor, DynamicNamedTupleLiteral, NamedTupleField, NamedTupleSpec, }; pub(crate) use self::static_literal::{ - ExpandedClassBaseEntry, StaticClassLiteral, expanded_class_base_entries, + ExpandedClassBaseEntry, FrozenDataclassDispatch, StaticClassLiteral, + expanded_class_base_entries, }; pub(super) use self::typed_dict::{DynamicTypedDictAnchor, DynamicTypedDictLiteral}; use super::dedicated::pydantic; diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index 13142a2997..7b76f3c7bc 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -24,6 +24,7 @@ use crate::{ Parameter, Parameters, PropertyInstanceType, Signature, SpecialFormType, StaticMroError, SubclassOfType, Truthiness, Type, TypeContext, TypeMapping, TypeVarVariance, TypedDictModule, UnionBuilder, UnionType, + bound_super::BoundSuperType, call::{CallError, CallErrorKind}, callable::{CallableFunctionProvenance, CallableTypeKind}, class::{ @@ -119,6 +120,48 @@ pub struct StaticClassLiteral<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for StaticClassLiteral<'_> {} +/// The result of [`StaticClassLiteral::inherited_frozen_dataclass_dispatch`]. +/// +/// See that method for details on how generated frozen-dataclass methods handle fields and +/// non-fields on subclass instances. +#[derive(Clone, Copy)] +pub(crate) enum FrozenDataclassDispatch<'db> { + /// A reachable frozen dataclass rejects modification of one of its fields. + FrozenField, + /// Every reachable frozen method delegates, with lookup resuming after this base. + Delegate(StaticClassLiteral<'db>), +} + +impl<'db> FrozenDataclassDispatch<'db> { + /// Returns the receiver for the next step of assignment or deletion validation. + /// + /// Validation stays on `object_ty` for a frozen field because the generated method rejects the + /// mutation. For a non-field, the generated method calls `super(frozen_base, object_ty)`, so + /// lookup must resume after the last frozen base. For example, assigning `Child().y` for + /// `class Child(Frozen, Later)` uses `super(Frozen, child)` when `y` is not a field of `Frozen`; + /// this preserves a later `__setattr__` or a descriptor for `y`. + pub(crate) fn receiver(self, db: &'db dyn Db, object_ty: Type<'db>) -> Type<'db> { + match self { + Self::FrozenField => object_ty, + Self::Delegate(frozen_base) => BoundSuperType::build( + db, + Type::ClassLiteral(ClassLiteral::Static(frozen_base)), + object_ty, + ) + .unwrap_or(object_ty), + } + } +} + +/// Fields protected by reachable frozen-dataclass methods. +struct InheritedFrozenDataclassFields<'db> { + names: Box<[Name]>, + /// The final frozen dataclass whose generated method participates in dispatch. + /// + /// For a non-field, mutation validation resumes after this class in the MRO. + last_frozen_base: StaticClassLiteral<'db>, +} + #[salsa::tracked] impl<'db> StaticClassLiteral<'db> { /// Return `true` if this class represents `known_class` @@ -1850,7 +1893,7 @@ impl<'db> StaticClassLiteral<'db> { } let frozen_base_fields = - self.inherited_non_slotted_frozen_dataclass_fields(db, specialization)?; + self.inherited_non_slotted_frozen_dataclass_fields(db, specialization, "__setattr__")?; let instance_ty = Type::instance(db, self.apply_optional_specialization(db, specialization)); @@ -1868,7 +1911,8 @@ impl<'db> StaticClassLiteral<'db> { }; let overloads = frozen_base_fields - .keys() + .names + .iter() .map(|field| setattr_signature(Type::string_literal(db, field), Type::Never)) .chain([setattr_signature( KnownClass::Str.to_instance(db), @@ -1883,15 +1927,82 @@ impl<'db> StaticClassLiteral<'db> { ))) } - /// Return the inherited frozen dataclass fields whose generated `__setattr__` still controls - /// assignments on this class. + /// Determines how an inherited generated frozen-dataclass `method` handles `name`. + /// + /// CPython's generated `__setattr__` and `__delattr__` reject every mutation when called on an + /// instance of the exact frozen class. On an ordinary subclass instance, they reject only + /// dataclass fields and delegate other names with `super(frozen_class, instance)`. + /// + /// For example: + /// + /// ```python + /// @dataclass(frozen=True) + /// class Frozen: + /// x: int + /// + /// class Later: + /// y: int + /// + /// class Child(Frozen, Later): ... + /// ``` + /// + /// Assigning to `Child().x` is rejected because `x` is a field of `Frozen`. Assigning to + /// `Child().y` instead delegates to `super(Frozen, child).__setattr__`, where a later + /// `__setattr__` or the descriptor for `y` can still reject the assignment. + /// + /// If multiple frozen dataclasses are reachable before an explicit implementation of + /// `method`, a non-field delegates past each generated method. [`FrozenDataclassDispatch::Delegate`] + /// stores the last frozen base so the caller can perform the equivalent lookup once, after all + /// of them. + pub(crate) fn inherited_frozen_dataclass_dispatch( + self, + db: &'db dyn Db, + specialization: Option>, + method: &str, + name: &str, + ) -> Option> { + if CodeGeneratorKind::from_static_class(db, self).is_some() + || class_member(db, self.body_scope(db), method) + .ignore_possibly_undefined() + .is_some() + { + return None; + } + + let frozen_base_fields = + self.inherited_non_slotted_frozen_dataclass_fields(db, specialization, method)?; + + if frozen_base_fields + .names + .iter() + .any(|field| field.as_str() == name) + { + Some(FrozenDataclassDispatch::FrozenField) + } else { + Some(FrozenDataclassDispatch::Delegate( + frozen_base_fields.last_frozen_base, + )) + } + } + + /// Returns the inherited fields protected by a generated frozen-dataclass method. fn inherited_non_slotted_frozen_dataclass_fields( self, db: &'db dyn Db, specialization: Option>, - ) -> Option<&'db FxIndexMap>> { + method: &str, + ) -> Option> { + let mut names = FxIndexSet::default(); + let mut last_frozen_base = None; + for base in self.iter_mro(db, specialization).skip(1) { - let (base_class, base_specialization) = base.into_class()?.static_class_literal(db)?; + let Some(base_class_type) = base.into_class() else { + break; + }; + let Some((base_class, base_specialization)) = base_class_type.static_class_literal(db) + else { + break; + }; // Stop if another class in the MRO replaces the generated frozen setter: // @@ -1905,29 +2016,47 @@ impl<'db> StaticClassLiteral<'db> { // // Writes to `Child().x` dispatch to `Mutable.__setattr__`, not to the synthesized // `Frozen.__setattr__`. - if class_member(db, base_class.body_scope(db), "__setattr__") + if class_member(db, base_class.body_scope(db), method) .ignore_possibly_undefined() .is_some() { - return None; + break; } if base_class.is_frozen_dataclass(db) == Some(true) { let field_policy @ CodeGeneratorKind::DataclassLike(_) = CodeGeneratorKind::from_static_class(db, base_class)? else { - return None; + break; }; if base_class.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) { - return None; + break; } - return Some(base_class.fields(db, base_specialization, field_policy)); + names.extend( + base_class + .fields(db, base_specialization, field_policy) + .iter() + .filter(|(_, field)| { + !matches!( + field.kind, + FieldKind::Dataclass { + init_only: true, + .. + } + ) + }) + .map(|(name, _)| name.clone()), + ); + last_frozen_base = Some(base_class); } } - None + Some(InheritedFrozenDataclassFields { + names: names.into_iter().collect(), + last_frozen_base: last_frozen_base?, + }) } /// Member lookup for classes that inherit from `typing.TypedDict`. diff --git a/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs b/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs index e42f2db4a6..b46e2fa2f6 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs @@ -9,6 +9,7 @@ use crate::types::attribute_write::{ ProtocolMemberWriteRequirement, attribute_write_requirement, property_setter_returns_never, }; use crate::types::call::{Bindings, CallArguments, CallDiagnosticOverride, CallError}; +use crate::types::class::FrozenDataclassDispatch; use crate::types::diagnostic::{ INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, UNRESOLVED_ATTRIBUTE, report_bad_dunder_set_call, report_invalid_attribute_assignment, report_possibly_missing_attribute, @@ -126,11 +127,17 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { ast::ArgOrKeyword::Arg(self.value), ]; let mut call_arguments = CallArguments::positional([name_ty, Type::unknown()]); + // A bound `super` must use its own MRO lookup rather than the normal instance fallback. + let lookup_policy = if matches!(object_ty, Type::BoundSuper(_)) { + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK + } else { + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::NO_INSTANCE_FALLBACK + }; let setattr_result = self.builder.infer_and_try_call_dunder( db, object_ty, "__setattr__", - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::NO_INSTANCE_FALLBACK, + lookup_policy, ArgumentsIter::synthesized(&ast_arguments), &mut call_arguments, &mut |builder, (argument_index, _, tcx)| { @@ -343,12 +350,29 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { emit_diagnostics: bool, ) -> bool { let db = self.builder.db(); + let frozen_dataclass_dispatch = object_ty + .nominal_class(db) + .and_then(|class| class.static_class_literal(db)) + .and_then(|(class, specialization)| { + class.inherited_frozen_dataclass_dispatch( + db, + specialization, + "__setattr__", + self.attribute, + ) + }); + let setattr_receiver = frozen_dataclass_dispatch + .map_or(object_ty, |dispatch| dispatch.receiver(db, object_ty)); + let (setattr_result, value_ty) = if matches!(member, InstanceAttributeWriteMember::SetAttr) - { - self.infer_and_try_call_setattr(object_ty, emit_diagnostics) + || matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::Delegate(_)) + ) { + self.infer_and_try_call_setattr(setattr_receiver, emit_diagnostics) } else { let value_ty = self.infer_value(TypeContext::default(), emit_diagnostics); - let setattr_result = object_ty.try_call_dunder_with_policy( + let setattr_result = setattr_receiver.try_call_dunder_with_policy( db, "__setattr__", &mut CallArguments::positional([ @@ -362,13 +386,19 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { }; // A terminal `__setattr__` blocks even explicitly declared attributes. - let setattr_returns_never = match &setattr_result { + let setattr_returns_never = matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::FrozenField) + ) || match &setattr_result { Ok(bindings) => bindings.return_type(db).is_never(), Err(error) => error.return_type(db).is_some_and(|ty| ty.is_never()), }; if setattr_returns_never { if emit_diagnostics { - let is_setattr_synthesized = match object_ty.class_member_with_policy( + let is_setattr_synthesized = !matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::Delegate(_)) + ) && match object_ty.class_member_with_policy( db, "__setattr__", MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, @@ -400,6 +430,19 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { { return false; } + if matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::Delegate(_)) + ) && let Err(CallDunderError::CallError(kind, bindings, _)) = setattr_result + { + if emit_diagnostics { + self.report(AssignmentAttributeWriteDiagnostic::BadSetAttr { + value_ty, + failure: CallError(kind, bindings), + }); + } + return false; + } let member_valid = self.evaluate_explicit_member(object_ty, member, value_ty, emit_diagnostics); if let Some(fallback) = fallback { @@ -424,6 +467,14 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { } false } + Err(CallDunderError::MethodNotAvailable) + if matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::Delegate(_)) + ) => + { + true + } Err(CallDunderError::MethodNotAvailable) => { if emit_diagnostics { self.report(AssignmentAttributeWriteDiagnostic::Unresolved { @@ -616,17 +667,27 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { emit_diagnostics: bool, ) -> bool { let db = self.builder.db(); - if property_setter_returns_never(db, descriptor_ty, object_ty, value_ty) { + let setter_result = setter_ty.try_call( + db, + &CallArguments::positional([descriptor_ty, object_ty, value_ty]), + ); + // `Never` supports arbitrary operations only because there can be no runtime value to + // mutate; it is not a concrete descriptor with a terminal setter. + let setter_returns_never = !descriptor_ty.is_never() + && match &setter_result { + Ok(bindings) => bindings.return_type(db).is_never(), + Err(error) => error.return_type(db).is_never(), + }; + if setter_returns_never + || property_setter_returns_never(db, descriptor_ty, object_ty, value_ty) + { if emit_diagnostics { self.report(AssignmentAttributeWriteDiagnostic::TerminalDescriptor); } return false; } - match setter_ty.try_call( - db, - &CallArguments::positional([descriptor_ty, object_ty, value_ty]), - ) { + match setter_result { Ok(_) => true, Err(error) => { if emit_diagnostics { From 20a45c3dd5b9f78169d914d7c70acdf1bac2290f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 28 Jul 2026 10:00:26 -0400 Subject: [PATCH 094/390] [ty] Reject frozen-dataclass field deletion through subclasses (#27001) ## Summary Stacked on #27217. Prior to this change, we rejected assignments to fields inherited from a frozen dataclass but allowed the equivalent deletion through an ordinary subclass, even though it raises `FrozenInstanceError` at runtime: ```python from dataclasses import dataclass @dataclass(frozen=True) class Frozen: x: int = 1 class Child(Frozen): ... del Child().x ``` We now synthesize frozen `__delattr__` for both frozen dataclasses and their ordinary subclasses, reuse the frozen-method dispatch from #27217, and continue through `super()` for non-field deletions. This preserves descriptor and later-MRO checks, supports multiple frozen bases and generic specializations, excludes `InitVar`, and retains the existing Python 3.12 behavior for slotted frozen dataclasses. The generated signatures also make direct and intermediate method overrides consistent with frozen `__setattr__`. --- .../mdtest/dataclasses/dataclasses.md | 205 +++++++++++++++++- .../src/types/class/static_literal.rs | 105 ++++++--- .../src/types/infer/builder.rs | 90 +++++++- 3 files changed, 353 insertions(+), 47 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 3ba577ef18..66d70c5c69 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -659,7 +659,7 @@ reveal_type(WithUnsafeHash.__hash__) # revealed: (self: WithUnsafeHash) -> int ### `frozen` -If true (the default is False), assigning to fields will generate a diagnostic. +When `frozen=True`, a dataclass does not allow its fields to be assigned or deleted. ```py from dataclasses import dataclass @@ -670,6 +670,11 @@ class MyFrozenClass: frozen_instance = MyFrozenClass(1) frozen_instance.x = 2 # error: [invalid-assignment] + +reveal_type(frozen_instance.__delattr__) # revealed: (name) -> Never + +# error: [invalid-assignment] "Cannot delete attribute `x` on type `MyFrozenClass` whose `__delattr__` method returns `Never`/`NoReturn`" +del frozen_instance.x ``` If `__setattr__()` or `__delattr__()` is defined in the class, a diagnostic is emitted. @@ -971,8 +976,7 @@ class RejectingTypeParameterAssignmentChild(TypeParameterFrozen[int], RejectsAss RejectingTypeParameterAssignmentChild(1).y = 2 ``` -When a subclass inherits from two frozen dataclasses, assignments to fields from both bases remain -frozen: +When a subclass inherits from two frozen dataclasses, fields from both bases remain frozen: ```py @dataclass(frozen=True) @@ -988,12 +992,15 @@ class ChildWithTwoFrozenBases(FirstFrozen, SecondFrozen): ... multiple = ChildWithTwoFrozenBases() # revealed: Overload[(name: Literal["first"], value) -> Never, (name: Literal["second"], value) -> Never, (name: str, value) -> None] reveal_type(multiple.__setattr__) +# revealed: Overload[(name: Literal["first"]) -> Never, (name: Literal["second"]) -> Never, (name: str) -> None] +reveal_type(multiple.__delattr__) multiple.second = 2 # error: [invalid-assignment] +del multiple.second # error: [invalid-assignment] ``` -An `InitVar` is a constructor argument, not a frozen field. A subclass can assign to an attribute -with the same name: +An `InitVar` is a constructor argument, not a frozen field. A subclass can assign and delete an +attribute with the same name: ```py from dataclasses import InitVar @@ -1007,6 +1014,7 @@ class ChildWithInitVar(FrozenWithInitVar): init_var_child = ChildWithInitVar() init_var_child.temporary = 4 +del init_var_child.temporary ``` The same rule applies when the `InitVar` belongs to a second frozen base: @@ -1017,13 +1025,14 @@ class ChildWithSecondBaseInitVar(Frozen, FrozenWithInitVar): second_init_var_child = ChildWithSecondBaseInitVar() second_init_var_child.temporary = 4 +del second_init_var_child.temporary ``` Non-field attributes on subclasses of slotted frozen dataclasses are still rejected. This correctly models the runtime behavior, but is somewhat surprising and may be a CPython bug, as subclasses of slotted classes usually allow arbitrary attributes to be set on them unless the subclass also explicitly declares `__slots__`. We should change our behavior here to follow CPython, if they "fix" -it. +it. The same limitation applies when deleting an attribute. ```py from dataclasses import dataclass @@ -1043,6 +1052,9 @@ frozen.x = 2 # error: [invalid-assignment] frozen.y = 2 # error: [invalid-assignment] frozen.z = 2 # error: [invalid-assignment] +del frozen.x # error: [invalid-assignment] +del frozen.y # error: [invalid-assignment] + grandchild = MySlottedFrozenGrandchildClass() grandchild.x = 2 # error: [invalid-assignment] grandchild.y = 2 # error: [invalid-assignment] @@ -1050,8 +1062,7 @@ grandchild.z = 2 # error: [invalid-assignment] grandchild.unknown = 2 # error: [invalid-assignment] ``` -The same diagnostic is emitted if a frozen dataclass is inherited, and an attempt is made to delete -an attribute: +A frozen dataclass also prevents an ordinary subclass from deleting an inherited field: ```py from dataclasses import dataclass @@ -1063,7 +1074,183 @@ class MyFrozenClass: class MyFrozenChildClass(MyFrozenClass): ... frozen = MyFrozenChildClass() -del frozen.x # TODO this should emit an [invalid-assignment] + +# revealed: Overload[(name: Literal["x"]) -> Never, (name: str) -> None] +reveal_type(frozen.__delattr__) + +del frozen.x # error: [invalid-assignment] +``` + +A frozen dataclass does not make a subclass's read-only property safe to delete: + +```py +from dataclasses import dataclass + +@dataclass(frozen=True) +class Frozen: + x: int = 1 + +class ReadOnlyChild(Frozen): + @property + def y(self) -> int: + return 1 + +# error: [invalid-assignment] "Cannot delete read-only property `y` on object of type `ReadOnlyChild`" +del ReadOnlyChild().y +``` + +Deleting a property is also invalid when its deleter never returns: + +```py +from typing import NoReturn + +class RejectingPropertyChild(Frozen): + @property + def y(self) -> int: + return 1 + + @y.deleter + def y(self) -> NoReturn: + raise AttributeError("y") + +# error: [invalid-assignment] "Cannot delete attribute `y` on type `RejectingPropertyChild` whose `__delete__` method returns `Never`/`NoReturn`" +del RejectingPropertyChild().y +``` + +When another base class rejects deletion, the frozen dataclass must not hide its `__delattr__` +method: + +```py +class RejectsDeletion: + y: int = 1 + + def __delattr__(self, name: str) -> NoReturn: + raise AttributeError(name) + +class ChildWithRejectingBase(Frozen, RejectsDeletion): ... + +# error: [invalid-assignment] "Cannot delete attribute `y` on type `ChildWithRejectingBase` whose `__delattr__` method returns `Never`/`NoReturn`" +del ChildWithRejectingBase().y +``` + +A second base class can customize deletion of an ordinary attribute: + +```py +class AllowsDeletion: + y: int = 1 + + def __delattr__(self, name: str) -> None: ... + +class ChildWithAllowingBase(Frozen, AllowsDeletion): ... + +del ChildWithAllowingBase().y +``` + +A later `__delattr__` can forward to `object.__delattr__`, which still invokes data descriptors: + +```py +class ForwardsDeletion: + def __delattr__(self, name: str) -> None: + super().__delattr__(name) +``` + +A read-only property therefore remains read-only: + +```py +class ReadOnlyDeletionBase(ForwardsDeletion): + @property + def y(self) -> int: + return 1 + +class ChildWithReadOnlyDeletion(Frozen, ReadOnlyDeletionBase): ... + +# error: [invalid-assignment] "Cannot delete read-only property `y` on object of type `ChildWithReadOnlyDeletion`" +del ChildWithReadOnlyDeletion().y +``` + +A property deleter that never returns also prevents deletion: + +```py +class TerminalDeletionBase(ForwardsDeletion): + @property + def y(self) -> int: + return 1 + + @y.deleter + def y(self) -> NoReturn: + raise AttributeError + +class ChildWithTerminalDeletion(Frozen, TerminalDeletionBase): ... + +# error: [invalid-assignment] "Cannot delete attribute `y` on type `ChildWithTerminalDeletion` whose `__delete__` method returns `Never`/`NoReturn`" +del ChildWithTerminalDeletion().y +``` + +The same rule applies to a custom descriptor whose deleter never returns: + +```py +class TerminalDeleteDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> int: + return 1 + + def __delete__(self, instance: object) -> NoReturn: + raise AttributeError + +class TerminalDescriptorDeletionBase(ForwardsDeletion): + y: TerminalDeleteDescriptor = TerminalDeleteDescriptor() + +class ChildWithTerminalDescriptorDeletion(Frozen, TerminalDescriptorDeletionBase): ... + +# error: [invalid-assignment] "Cannot delete attribute `y` on type `ChildWithTerminalDescriptorDeletion` whose `__delete__` method returns `Never`/`NoReturn`" +del ChildWithTerminalDescriptorDeletion().y +``` + +An ordinary attribute defined on a subclass can also be deleted: + +```py +class ChildWithOwnAttribute(Frozen): + y: int = 1 + +deletable = ChildWithOwnAttribute() +deletable.y = 2 +del deletable.y +``` + +A subclass can replace the inherited `__delattr__`, but a method that returns `None` is an invalid +override of the frozen base's method, which returns `Never`. The overriding method still controls +deletion on both that subclass and its subclasses: + +```py +class ChildWithDeletionOverride(Frozen): + # error: [invalid-method-override] + def __delattr__(self, name: str) -> None: ... + +class GrandchildWithDeletionOverride(ChildWithDeletionOverride): ... + +del ChildWithDeletionOverride().x +del GrandchildWithDeletionOverride().x +``` + +A read-only property remains protected when the frozen base is a specialized `Generic[T]` dataclass: + +```py +class ReadOnlyGenericChild(GenericFrozen[int]): + @property + def y(self) -> int: + return 1 + +# error: [invalid-assignment] "Cannot delete read-only property `y` on object of type `ReadOnlyGenericChild`" +del ReadOnlyGenericChild(1).y +``` + +The Python 3.12 type-parameter syntax must also preserve a `__delattr__` method defined by another +base class: + +```py +class RejectingTypeParameterChild(TypeParameterFrozen[int], RejectsDeletion): ... + +# error: [invalid-assignment] "Cannot delete attribute `y` on type `RejectingTypeParameterChild` whose `__delattr__` method returns `Never`/`NoReturn`" +del RejectingTypeParameterChild(1).y ``` ### frozen/non-frozen inheritance diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index 7b76f3c7bc..2527d9cafe 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -126,7 +126,7 @@ impl get_size2::GetSize for StaticClassLiteral<'_> {} /// non-fields on subclass instances. #[derive(Clone, Copy)] pub(crate) enum FrozenDataclassDispatch<'db> { - /// A reachable frozen dataclass rejects modification of one of its fields. + /// A reachable frozen dataclass rejects assignment to or deletion of one of its fields. FrozenField, /// Every reachable frozen method delegates, with lookup resuming after this base. Delegate(StaticClassLiteral<'db>), @@ -153,6 +153,32 @@ impl<'db> FrozenDataclassDispatch<'db> { } } +/// A method synthesized for a frozen dataclass. +#[derive(Clone, Copy)] +enum FrozenDataclassMethod { + SetAttr, + DelAttr, +} + +impl FrozenDataclassMethod { + /// Returns the frozen-dataclass method for `name`, if it is `__setattr__` or `__delattr__`. + fn from_name(name: &str) -> Option { + match name { + "__setattr__" => Some(Self::SetAttr), + "__delattr__" => Some(Self::DelAttr), + _ => None, + } + } + + /// Returns the corresponding Python special-method name. + const fn name(self) -> &'static str { + match self { + Self::SetAttr => "__setattr__", + Self::DelAttr => "__delattr__", + } + } +} + /// Fields protected by reachable frozen-dataclass methods. struct InheritedFrozenDataclassFields<'db> { names: Box<[Name]>, @@ -1421,12 +1447,13 @@ impl<'db> StaticClassLiteral<'db> { // An ordinary subclass of a frozen dataclass is not itself dataclass-like, so the // `CodeGeneratorKind::from_class` check below would return `None` before dataclass-like // synthesis runs. Still, an instance of such a subclass inherits the frozen dataclass's - // generated `__setattr__`, which rejects writes to frozen base fields. - if name == "__setattr__" - && let Some(synthesized_setattr) = - self.own_frozen_dataclass_subclass_setattr(db, specialization) + // generated `__setattr__` and `__delattr__`, which reject assignments and deletions of + // frozen base fields. + if let Some(method) = FrozenDataclassMethod::from_name(name) + && let Some(synthesized_method) = + self.own_frozen_dataclass_subclass_method(db, specialization, method) { - return Some(synthesized_setattr); + return Some(synthesized_method); } let field_policy = CodeGeneratorKind::from_class(db, self.into())?; @@ -1856,6 +1883,20 @@ impl<'db> StaticClassLiteral<'db> { } None } + (CodeGeneratorKind::DataclassLike(_), "__delattr__") + if self.is_frozen_dataclass(db) == Some(true) => + { + let signature = Signature::new( + Parameters::standard([ + Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty), + Parameter::positional_or_keyword(Name::new_static("name")), + ]), + Type::Never, + ); + + Some(Type::function_like_callable(db, signature)) + } (field_policy @ CodeGeneratorKind::DataclassLike(_), "__slots__") if Program::get(db).python_version(db) >= PythonVersion::PY310 => { @@ -1878,43 +1919,51 @@ impl<'db> StaticClassLiteral<'db> { } } - /// Synthesize a `__setattr__` view for an ordinary subclass of a frozen dataclass. + /// Synthesize a `__setattr__` or `__delattr__` view for an ordinary subclass of a frozen + /// dataclass. /// - /// CPython's generated frozen-dataclass `__setattr__` rejects all writes on exact instances of - /// the frozen dataclass, but on subclass instances it only rejects writes to that dataclass's - /// fields before delegating to the next `__setattr__` in the MRO. - fn own_frozen_dataclass_subclass_setattr( + /// CPython's generated frozen-dataclass `__setattr__` and `__delattr__` reject all assignments + /// and deletions on exact instances of the frozen dataclass, but on subclass instances they + /// only reject assignments and deletions of that dataclass's fields before delegating to the + /// next method in the MRO. + fn own_frozen_dataclass_subclass_method( self, db: &'db dyn Db, specialization: Option>, + method: FrozenDataclassMethod, ) -> Option> { if CodeGeneratorKind::from_static_class(db, self).is_some() { return None; } let frozen_base_fields = - self.inherited_non_slotted_frozen_dataclass_fields(db, specialization, "__setattr__")?; + self.inherited_non_slotted_frozen_dataclass_fields(db, specialization, method.name())?; let instance_ty = Type::instance(db, self.apply_optional_specialization(db, specialization)); - let setattr_signature = |name_ty, return_ty| { - Signature::new( - Parameters::standard([ - Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(instance_ty), - Parameter::positional_or_keyword(Name::new_static("name")) - .with_annotated_type(name_ty), + let method_signature = |name_ty, return_ty| { + let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty); + let name_parameter = Parameter::positional_or_keyword(Name::new_static("name")) + .with_annotated_type(name_ty); + let parameters = match method { + FrozenDataclassMethod::SetAttr => Parameters::standard([ + self_parameter, + name_parameter, Parameter::positional_or_keyword(Name::new_static("value")), ]), - return_ty, - ) + FrozenDataclassMethod::DelAttr => { + Parameters::standard([self_parameter, name_parameter]) + } + }; + Signature::new(parameters, return_ty) }; let overloads = frozen_base_fields .names .iter() - .map(|field| setattr_signature(Type::string_literal(db, field), Type::Never)) - .chain([setattr_signature( + .map(|field| method_signature(Type::string_literal(db, field), Type::Never)) + .chain([method_signature( KnownClass::Str.to_instance(db), Type::none(db), )]); @@ -1949,6 +1998,7 @@ impl<'db> StaticClassLiteral<'db> { /// Assigning to `Child().x` is rejected because `x` is a field of `Frozen`. Assigning to /// `Child().y` instead delegates to `super(Frozen, child).__setattr__`, where a later /// `__setattr__` or the descriptor for `y` can still reject the assignment. + /// Deletion follows the same lookup through `__delattr__` and `__delete__`. /// /// If multiple frozen dataclasses are reachable before an explicit implementation of /// `method`, a non-field delegates past each generated method. [`FrozenDataclassDispatch::Delegate`] @@ -1985,7 +2035,7 @@ impl<'db> StaticClassLiteral<'db> { } } - /// Returns the inherited fields protected by a generated frozen-dataclass method. + /// Returns the inherited fields whose generated `__setattr__` or `__delattr__` still applies. fn inherited_non_slotted_frozen_dataclass_fields( self, db: &'db dyn Db, @@ -2004,18 +2054,19 @@ impl<'db> StaticClassLiteral<'db> { break; }; - // Stop if another class in the MRO replaces the generated frozen setter: + // Stop if another class in the MRO replaces the relevant generated frozen method: // // @dataclass(frozen=True) // class Frozen: x: int // // class Mutable(Frozen): // def __setattr__(self, name: str, value: object) -> None: ... + // def __delattr__(self, name: str) -> None: ... // // class Child(Mutable): ... // - // Writes to `Child().x` dispatch to `Mutable.__setattr__`, not to the synthesized - // `Frozen.__setattr__`. + // Writes and deletions of `Child().x` dispatch to the corresponding `Mutable` method, + // not to the synthesized `Frozen` method. if class_member(db, base_class.body_scope(db), method) .ignore_possibly_undefined() .is_some() diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 3f02a43b18..32d128b8f5 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -48,7 +48,9 @@ use crate::types::call::bind::{ }; use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind}; use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; -use crate::types::class::{ClassLiteral, CodeGeneratorKind, MethodDecorator}; +use crate::types::class::{ + ClassLiteral, CodeGeneratorKind, FrozenDataclassDispatch, MethodDecorator, +}; use crate::types::constraints::{ConstraintSetBuilder, PathBounds, Solutions}; use crate::types::context::InferContext; use crate::types::dedicated::pydantic; @@ -2887,15 +2889,66 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::TypeForm(_) | Type::TypedDict(_) | Type::NewTypeInstance(_) => { - let delattr_dunder_call_result = object_ty.try_call_dunder_with_policy( - db, - "__delattr__", - &mut CallArguments::positional([Type::string_literal(db, attribute)]), - TypeContext::default(), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, - ); + let frozen_dataclass_dispatch = object_ty + .nominal_class(db) + .and_then(|class| class.static_class_literal(db)) + .and_then(|(class, specialization)| { + class.inherited_frozen_dataclass_dispatch( + db, + specialization, + "__delattr__", + attribute, + ) + }); + + let delattr_receiver = frozen_dataclass_dispatch + .map_or(object_ty, |dispatch| dispatch.receiver(db, object_ty)); + + let mut delattr_arguments = + CallArguments::positional([Type::string_literal(db, attribute)]); + let delattr_dunder_call_result = if matches!(delattr_receiver, Type::BoundSuper(_)) + { + match delattr_receiver + .member_lookup_with_policy( + db, + "__delattr__", + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ) + .place + { + Place::Defined(DefinedPlace { + ty: delattr, + definedness, + provenance, + .. + }) => match delattr.try_call(db, &delattr_arguments) { + Ok(bindings) if definedness == Definedness::PossiblyUndefined => { + Err(CallDunderError::PossiblyUnbound { + bindings: Box::new(bindings), + unbound_on: None, + }) + } + Ok(bindings) => Ok(bindings), + Err(CallError(kind, bindings)) => { + Err(CallDunderError::CallError(kind, bindings, provenance)) + } + }, + Place::Undefined => Err(CallDunderError::MethodNotAvailable), + } + } else { + delattr_receiver.try_call_dunder_with_policy( + db, + "__delattr__", + &mut delattr_arguments, + TypeContext::default(), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ) + }; - let returns_never = match &delattr_dunder_call_result { + let returns_never = matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::FrozenField) + ) || match &delattr_dunder_call_result { Ok(result) => result.return_type(db).is_never(), Err(err) => err.return_type(db).is_some_and(|ty| ty.is_never()), }; @@ -2913,7 +2966,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } match delattr_dunder_call_result { - Ok(_) | Err(CallDunderError::PossiblyUnbound { .. }) => { + Ok(_) | Err(CallDunderError::PossiblyUnbound { .. }) + if !matches!( + frozen_dataclass_dispatch, + Some(FrozenDataclassDispatch::Delegate(_)) + ) => + { if self.validate_final_attribute_deletion( target, object_ty, @@ -2924,6 +2982,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } return true; } + Ok(_) | Err(CallDunderError::PossiblyUnbound { .. }) => {} Err(CallDunderError::CallError(kind, _bindings, _)) => { if emit_diagnostics { report_bad_dunder_delattr_call( @@ -2967,7 +3026,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { TypeContext::default(), ); - if self.property_deleter_returns_never(attr_ty, object_ty) { + // `Never` supports arbitrary operations only because there can be no runtime + // value to mutate; it is not a concrete descriptor with a terminal deleter. + let deleter_returns_never = !attr_ty.is_never() + && match &delete_dunder_call_result { + Ok(bindings) => bindings.return_type(db).is_never(), + Err(error) => error.return_type(db).is_some_and(|ty| ty.is_never()), + }; + if deleter_returns_never + || self.property_deleter_returns_never(attr_ty, object_ty) + { if emit_diagnostics && let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, target) From 10824452d018e6ea5160f9ba7cbab583b647f20d Mon Sep 17 00:00:00 2001 From: jesco Date: Tue, 28 Jul 2026 11:01:16 -0400 Subject: [PATCH 095/390] [`flake8-pytest-style`] Make fixes safe by default and unsafe only when comments are present (`PT018`) (#27201) ## Summary Fixes #20698. * **Default Fix Safety:** The `PT018` rule splits composite assertions into individual `assert` statements. Because standard transformations (without custom messages) maintain short-circuiting and original runtime behavior, these fixes are now classified as **safe** by default, rather than marking every fix as unsafe. * **Handling Comments:** Previously, Ruff completely disabled fixes for assertions containing comments. Now, assertions with internal comments will still offer a fix, but marked with an **unsafe** classification to ensure user verification while preserving functionality. Proposed fixed will now composite assertion with an internal comment. Ordinary transformations are safe while the comment-bearing case receives an unsafe fix and this is verified. ## Test Plan - Ran the focused PT018 fixture and snapshot test: `cargo test -p ruff_linter --lib rule_pytestcompositeassertion_path_new_pt018_py_settings_default_pt018_expects` - Ran `cargo fmt --all --check` --------- Co-authored-by: Brent Westbrook --- .../fixtures/flake8_pytest_style/PT018.py | 7 + crates/ruff_linter/src/preview.rs | 5 + .../src/rules/flake8_pytest_style/mod.rs | 1 + .../flake8_pytest_style/rules/assertion.rs | 25 +- ...es__flake8_pytest_style__tests__PT018.snap | 13 + ...style__tests__preview__PT018_PT018.py.snap | 630 ++++++++++++++++++ 6 files changed, 677 insertions(+), 4 deletions(-) create mode 100644 crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__preview__PT018_PT018.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT018.py b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT018.py index 0ccec6f759..77efd8a097 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT018.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT018.py @@ -70,3 +70,10 @@ def test_parenthesized_not(): assert (not self.find_graph_output(node.output[0]) or self.find_graph_input(node.input[0])) + + +def test_comments(): + assert ( + # comment + something and something_else + ) diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 69b886bc48..853063ec23 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -56,6 +56,11 @@ pub(crate) const fn is_fix_f_string_logging_enabled(settings: &LinterSettings) - settings.preview.is_enabled() } +// https://github.com/astral-sh/ruff/pull/27201 +pub(crate) const fn is_fix_pytest_composite_assertion_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() diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs index f5db3b1f52..e97adff100 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs @@ -379,6 +379,7 @@ mod tests { } #[test_case(Rule::PytestExtraneousScopeFunction, Path::new("PT003.py"))] + #[test_case(Rule::PytestCompositeAssertion, Path::new("PT018.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs index 9b82d8eb95..b529d8485c 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs @@ -28,7 +28,8 @@ use crate::cst::matchers::match_indented_block; use crate::cst::matchers::match_module; use crate::fix::codemods::CodegenStylist; use crate::importer::ImportRequest; -use crate::{Edit, Fix, FixAvailability, Violation}; +use crate::preview::is_fix_pytest_composite_assertion_enabled; +use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; use super::unittest_assert::UnittestAssert; @@ -60,6 +61,14 @@ use super::unittest_assert::UnittestAssert; /// assert not something /// assert not something_else /// ``` +/// +/// ## Fix safety +/// +/// On stable, the rule's fix is always unsafe and not offered when it would remove comments in the +/// compound assertion. In [preview], the fix is only unsafe when it would delete such comments and +/// safe otherwise. +/// +/// [preview]: https://docs.astral.sh/ruff/preview/ #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.208")] pub(crate) struct PytestCompositeAssertion; @@ -829,16 +838,24 @@ pub(crate) fn composite_condition(checker: &Checker, stmt: &Stmt, test: &Expr, m let composite = is_composite_condition(test); if matches!(composite, CompositionKind::Simple | CompositionKind::Mixed) { let mut diagnostic = checker.report_diagnostic(PytestCompositeAssertion, stmt.range()); + let preview_fix = is_fix_pytest_composite_assertion_enabled(checker.settings()); if matches!(composite, CompositionKind::Simple) && msg.is_none() - && !checker.comment_ranges().intersects(stmt.range()) + && (preview_fix || !checker.comment_ranges().intersects(stmt.range())) && !checker .indexer() .in_multi_statement_line(stmt, checker.source()) { diagnostic.try_set_fix(|| { - fix_composite_condition(stmt, checker.locator(), checker.stylist()) - .map(Fix::unsafe_edit) + fix_composite_condition(stmt, checker.locator(), checker.stylist()).map(|edit| { + let applicability = + if preview_fix && !checker.comment_ranges().intersects(edit.range()) { + Applicability::Safe + } else { + Applicability::Unsafe + }; + Fix::applicable_edit(edit, applicability) + }) }); } } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT018.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT018.snap index 3c27f91e47..92f42410e9 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT018.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT018.snap @@ -1,5 +1,6 @@ --- source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs +assertion_line: 358 --- PT018 [*] Assertion should be broken down into multiple parts --> PT018.py:14:5 @@ -385,3 +386,15 @@ help: Break down assertion into multiple parts 72 | | note: This is an unsafe fix and may change runtime behavior + +PT018 Assertion should be broken down into multiple parts + --> PT018.py:76:5 + | +75 | def test_comments(): +76 | / assert ( +77 | | # comment +78 | | something and something_else +79 | | ) + | |_____^ + | +help: Break down assertion into multiple parts diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__preview__PT018_PT018.py.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__preview__PT018_PT018.py.snap new file mode 100644 index 0000000000..8f9feb7a6c --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__preview__PT018_PT018.py.snap @@ -0,0 +1,630 @@ +--- +source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs +assertion_line: 389 +--- +--- Linter settings --- +-linter.preview = disabled ++linter.preview = enabled + +--- Summary --- +Removed: 14 +Added: 14 + +--- Removed --- +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:14:5 + | +13 | def test_error(): +14 | assert something and something_else + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +15 | assert something and something_else and something_third +16 | assert something and not something_else + | +help: Break down assertion into multiple parts + | +13 | def test_error(): + - assert something and something_else +14 + assert something +15 + assert something_else +16 | assert something and something_else and something_third + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:15:5 + | +13 | def test_error(): +14 | assert something and something_else +15 | assert something and something_else and something_third + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +16 | assert something and not something_else +17 | assert something and (something_else or something_third) + | +help: Break down assertion into multiple parts + | +14 | assert something and something_else + - assert something and something_else and something_third +15 + assert something and something_else +16 + assert something_third +17 | assert something and not something_else + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:16:5 + | +14 | assert something and something_else +15 | assert something and something_else and something_third +16 | assert something and not something_else + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | assert something and (something_else or something_third) +18 | assert not something and something_else + | +help: Break down assertion into multiple parts + | +15 | assert something and something_else and something_third + - assert something and not something_else +16 + assert something +17 + assert not something_else +18 | assert something and (something_else or something_third) + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:17:5 + | +15 | assert something and something_else and something_third +16 | assert something and not something_else +17 | assert something and (something_else or something_third) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +18 | assert not something and something_else +19 | assert not (something or something_else) + | +help: Break down assertion into multiple parts + | +16 | assert something and not something_else + - assert something and (something_else or something_third) +17 + assert something +18 + assert (something_else or something_third) +19 | assert not something and something_else + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:18:5 + | +16 | assert something and not something_else +17 | assert something and (something_else or something_third) +18 | assert not something and something_else + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +19 | assert not (something or something_else) +20 | assert not (something or something_else or something_third) + | +help: Break down assertion into multiple parts + | +17 | assert something and (something_else or something_third) + - assert not something and something_else +18 + assert not something +19 + assert something_else +20 | assert not (something or something_else) + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:19:5 + | +17 | assert something and (something_else or something_third) +18 | assert not something and something_else +19 | assert not (something or something_else) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +20 | assert not (something or something_else or something_third) +21 | assert something and something_else == """error + | +help: Break down assertion into multiple parts + | +18 | assert not something and something_else + - assert not (something or something_else) +19 + assert not something +20 + assert not something_else +21 | assert not (something or something_else or something_third) + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:20:5 + | +18 | assert not something and something_else +19 | assert not (something or something_else) +20 | assert not (something or something_else or something_third) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +21 | assert something and something_else == """error +22 | message + | +help: Break down assertion into multiple parts + | +19 | assert not (something or something_else) + - assert not (something or something_else or something_third) +20 + assert not (something or something_else) +21 + assert not something_third +22 | assert something and something_else == """error + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:21:5 + | +19 | assert not (something or something_else) +20 | assert not (something or something_else or something_third) +21 | / assert something and something_else == """error +22 | | message +23 | | """ + | |_______^ +24 | assert ( +25 | something + | +help: Break down assertion into multiple parts + | +20 | assert not (something or something_else or something_third) + - assert something and something_else == """error +21 + assert something +22 + assert something_else == """error +23 | message + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:24:5 + | +22 | message +23 | """ +24 | / assert ( +25 | | something +26 | | and something_else +27 | | == """error +28 | | message +29 | | """ +30 | | ) + | |_____^ +31 | +32 | # recursive case + | +help: Break down assertion into multiple parts + | +23 | """ +24 + assert something +25 | assert ( + - something + - and something_else +26 + something_else +27 | == """error + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:33:5 + | +32 | # recursive case +33 | assert not (a or not (b or c)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +34 | assert not (a or not (b and c)) + | +help: Break down assertion into multiple parts + | +32 | # recursive case + - assert not (a or not (b or c)) +33 + assert not a +34 + assert (b or c) +35 | assert not (a or not (b and c)) + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:34:5 + | +32 | # recursive case +33 | assert not (a or not (b or c)) +34 | assert not (a or not (b and c)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +35 | +36 | # detected, but no fix for messages + | +help: Break down assertion into multiple parts + | +33 | assert not (a or not (b or c)) + - assert not (a or not (b and c)) +34 + assert not a +35 + assert (b and c) +36 | + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:59:5 + | +57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7143 +58 | def test_parenthesized_not(): +59 | / assert not ( +60 | | self.find_graph_output(node.output[0]) +61 | | or self.find_graph_input(node.input[0]) +62 | | or self.find_graph_output(node.input[0]) +63 | | ) + | |_____^ +64 | +65 | assert (not ( + | +help: Break down assertion into multiple parts + | +61 | or self.find_graph_input(node.input[0]) + - or self.find_graph_output(node.input[0]) +62 | ) +63 + assert not ( +64 + self.find_graph_output(node.input[0]) +65 + ) +66 | + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:65:5 + | +63 | ) +64 | +65 | / assert (not ( +66 | | self.find_graph_output(node.output[0]) +67 | | or self.find_graph_input(node.input[0]) +68 | | or self.find_graph_output(node.input[0]) +69 | | )) + | |______^ +70 | +71 | assert (not self.find_graph_output(node.output[0]) or + | +help: Break down assertion into multiple parts + | +64 | + - assert (not ( +65 + assert not ( +66 | self.find_graph_output(node.output[0]) +67 | or self.find_graph_input(node.input[0]) + - or self.find_graph_output(node.input[0]) + - )) +68 + ) +69 + assert not ( +70 + self.find_graph_output(node.input[0]) +71 + ) +72 | + | +note: This is an unsafe fix and may change runtime behavior + + +PT018 Assertion should be broken down into multiple parts + --> PT018.py:76:5 + | +75 | def test_comments(): +76 | / assert ( +77 | | # comment +78 | | something and something_else +79 | | ) + | |_____^ + | +help: Break down assertion into multiple parts + + + +--- Added --- +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:14:5 + | +13 | def test_error(): +14 | assert something and something_else + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +15 | assert something and something_else and something_third +16 | assert something and not something_else + | +help: Break down assertion into multiple parts + | +13 | def test_error(): + - assert something and something_else +14 + assert something +15 + assert something_else +16 | assert something and something_else and something_third + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:15:5 + | +13 | def test_error(): +14 | assert something and something_else +15 | assert something and something_else and something_third + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +16 | assert something and not something_else +17 | assert something and (something_else or something_third) + | +help: Break down assertion into multiple parts + | +14 | assert something and something_else + - assert something and something_else and something_third +15 + assert something and something_else +16 + assert something_third +17 | assert something and not something_else + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:16:5 + | +14 | assert something and something_else +15 | assert something and something_else and something_third +16 | assert something and not something_else + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | assert something and (something_else or something_third) +18 | assert not something and something_else + | +help: Break down assertion into multiple parts + | +15 | assert something and something_else and something_third + - assert something and not something_else +16 + assert something +17 + assert not something_else +18 | assert something and (something_else or something_third) + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:17:5 + | +15 | assert something and something_else and something_third +16 | assert something and not something_else +17 | assert something and (something_else or something_third) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +18 | assert not something and something_else +19 | assert not (something or something_else) + | +help: Break down assertion into multiple parts + | +16 | assert something and not something_else + - assert something and (something_else or something_third) +17 + assert something +18 + assert (something_else or something_third) +19 | assert not something and something_else + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:18:5 + | +16 | assert something and not something_else +17 | assert something and (something_else or something_third) +18 | assert not something and something_else + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +19 | assert not (something or something_else) +20 | assert not (something or something_else or something_third) + | +help: Break down assertion into multiple parts + | +17 | assert something and (something_else or something_third) + - assert not something and something_else +18 + assert not something +19 + assert something_else +20 | assert not (something or something_else) + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:19:5 + | +17 | assert something and (something_else or something_third) +18 | assert not something and something_else +19 | assert not (something or something_else) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +20 | assert not (something or something_else or something_third) +21 | assert something and something_else == """error + | +help: Break down assertion into multiple parts + | +18 | assert not something and something_else + - assert not (something or something_else) +19 + assert not something +20 + assert not something_else +21 | assert not (something or something_else or something_third) + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:20:5 + | +18 | assert not something and something_else +19 | assert not (something or something_else) +20 | assert not (something or something_else or something_third) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +21 | assert something and something_else == """error +22 | message + | +help: Break down assertion into multiple parts + | +19 | assert not (something or something_else) + - assert not (something or something_else or something_third) +20 + assert not (something or something_else) +21 + assert not something_third +22 | assert something and something_else == """error + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:21:5 + | +19 | assert not (something or something_else) +20 | assert not (something or something_else or something_third) +21 | / assert something and something_else == """error +22 | | message +23 | | """ + | |_______^ +24 | assert ( +25 | something + | +help: Break down assertion into multiple parts + | +20 | assert not (something or something_else or something_third) + - assert something and something_else == """error +21 + assert something +22 + assert something_else == """error +23 | message + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:24:5 + | +22 | message +23 | """ +24 | / assert ( +25 | | something +26 | | and something_else +27 | | == """error +28 | | message +29 | | """ +30 | | ) + | |_____^ +31 | +32 | # recursive case + | +help: Break down assertion into multiple parts + | +23 | """ +24 + assert something +25 | assert ( + - something + - and something_else +26 + something_else +27 | == """error + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:33:5 + | +32 | # recursive case +33 | assert not (a or not (b or c)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +34 | assert not (a or not (b and c)) + | +help: Break down assertion into multiple parts + | +32 | # recursive case + - assert not (a or not (b or c)) +33 + assert not a +34 + assert (b or c) +35 | assert not (a or not (b and c)) + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:34:5 + | +32 | # recursive case +33 | assert not (a or not (b or c)) +34 | assert not (a or not (b and c)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +35 | +36 | # detected, but no fix for messages + | +help: Break down assertion into multiple parts + | +33 | assert not (a or not (b or c)) + - assert not (a or not (b and c)) +34 + assert not a +35 + assert (b and c) +36 | + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:59:5 + | +57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7143 +58 | def test_parenthesized_not(): +59 | / assert not ( +60 | | self.find_graph_output(node.output[0]) +61 | | or self.find_graph_input(node.input[0]) +62 | | or self.find_graph_output(node.input[0]) +63 | | ) + | |_____^ +64 | +65 | assert (not ( + | +help: Break down assertion into multiple parts + | +61 | or self.find_graph_input(node.input[0]) + - or self.find_graph_output(node.input[0]) +62 | ) +63 + assert not ( +64 + self.find_graph_output(node.input[0]) +65 + ) +66 | + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:65:5 + | +63 | ) +64 | +65 | / assert (not ( +66 | | self.find_graph_output(node.output[0]) +67 | | or self.find_graph_input(node.input[0]) +68 | | or self.find_graph_output(node.input[0]) +69 | | )) + | |______^ +70 | +71 | assert (not self.find_graph_output(node.output[0]) or + | +help: Break down assertion into multiple parts + | +64 | + - assert (not ( +65 + assert not ( +66 | self.find_graph_output(node.output[0]) +67 | or self.find_graph_input(node.input[0]) + - or self.find_graph_output(node.input[0]) + - )) +68 + ) +69 + assert not ( +70 + self.find_graph_output(node.input[0]) +71 + ) +72 | + | + + +PT018 [*] Assertion should be broken down into multiple parts + --> PT018.py:76:5 + | +75 | def test_comments(): +76 | / assert ( +77 | | # comment +78 | | something and something_else +79 | | ) + | |_____^ + | +help: Break down assertion into multiple parts + | +75 | def test_comments(): + - assert ( + - # comment + - something and something_else + - ) +76 + assert something +77 + assert something_else + | +note: This is an unsafe fix and may change runtime behavior From 023b1db30e6ec3e0d2fef1c42878a8588589252f Mon Sep 17 00:00:00 2001 From: LHMQ878 <72402929@cityu-dg.edu.cn> Date: Tue, 28 Jul 2026 23:14:36 +0800 Subject: [PATCH 096/390] [`flake8-import-conventions`] Document that `extend-aliases` can override default aliases (#27191) Closes #17097 ## Summary The `extend-aliases` documentation only stated that entries "will be added to the `aliases` mapping", which does not convey that an entry for a module that already has a default alias *replaces* that default. As noted in the issue, this makes it possible to change or opt out of a default alias without restating the whole `aliases` mapping. This is a documentation-only change; the behaviour already works this way because the settings resolver applies `extend-aliases` on top of `aliases` via `HashMap::extend`, so matching keys are overwritten. ## Test Plan Verified the documented behaviour against `ruff 0.12.4` using this sample file: ```python import numpy import numpy as np ``` | Config | Result | | --- | --- | | default | `ICN001 `numpy` should be imported as `np`` on line 1 | | `numpy = "numpy"` under `extend-aliases` | `ICN001 `numpy` should be imported as `numpy`` on line 2, i.e. the aliased import is now the violation | | `numpy = "nmp"` under `extend-aliases` | both lines flagged, requiring `nmp`, so the default `np` is fully replaced | Also ran: - `cargo fmt --check` - passes. - `cargo dev generate-all` - regenerated `ruff.schema.json` so the new description is reflected there; confirmed the file is still valid JSON and contains the updated text. --------- Co-authored-by: Claude Opus 5 --- crates/ruff_workspace/src/options.rs | 3 ++- ruff.schema.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 14120f9099..321bb99c50 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -1623,7 +1623,8 @@ pub struct Flake8ImportConventionsOptions { pub aliases: Option>, /// A mapping from module to conventional import alias. These aliases will - /// be added to the [`aliases`](#lint_flake8-import-conventions_aliases) mapping. + /// be added to the [`aliases`](#lint_flake8-import-conventions_aliases) mapping + /// and will override any existing `aliases` if the two settings overlap. #[option( default = r#"{}"#, value_type = "dict[str, str]", diff --git a/ruff.schema.json b/ruff.schema.json index 4555a248f0..11b83aec21 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1254,7 +1254,7 @@ "uniqueItems": true }, "extend-aliases": { - "description": "A mapping from module to conventional import alias. These aliases will\nbe added to the [`aliases`](#lint_flake8-import-conventions_aliases) mapping.", + "description": "A mapping from module to conventional import alias. These aliases will\nbe added to the [`aliases`](#lint_flake8-import-conventions_aliases) mapping\nand will override any existing `aliases` if the two settings overlap.", "type": [ "object", "null" From 154d1c10adfaf139eb83e7cd44814c531d8286f2 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:15:12 -0400 Subject: [PATCH 097/390] [`flake8-bugbear`] Mark `range` as immutable (`B008`) (#27247) Summary -- Fixes #27245. Test Plan -- Updated existing snapshots. These were previously emitting both B006 on the outer comprehensions and B008 on the inner `range` call, but now the inner diagnostic is omitted because `range` is immutable. --- .../test/fixtures/flake8_bugbear/B006_B008.py | 2 +- ...ke8_bugbear__tests__B006_B006_B008.py.snap | 4 +-- ...ke8_bugbear__tests__B008_B006_B008.py.snap | 25 ------------------- ...ar__tests__preview__B006_B006_B008.py.snap | 4 +-- crates/ruff_python_stdlib/src/typing.rs | 1 + 5 files changed, 6 insertions(+), 30 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B006_B008.py b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B006_B008.py index 8e55d5b340..8f461b4dbb 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B006_B008.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B006_B008.py @@ -98,7 +98,7 @@ def dont_forget_me(value=collections.deque()): ... -# N.B. we're also flagging the function call in the comprehension +# B006 still flags mutable comprehension defaults. def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): pass diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snap index 9ee2465a7f..7bdf721a3f 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snap @@ -149,14 +149,14 @@ note: This is an unsafe fix and may change runtime behavior B006 [*] Do not use mutable data structures for argument defaults --> B006_B008.py:102:46 | -101 | # N.B. we're also flagging the function call in the comprehension +101 | # B006 still flags mutable comprehension defaults. 102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): | ^^^^^^^^^^^^^^^^^^^^^^^^ 103 | pass | help: Replace with `None`; initialize within function | -101 | # N.B. we're also flagging the function call in the comprehension +101 | # B006 still flags mutable comprehension defaults. - def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): 102 + def list_comprehension_also_not_okay(default=None): 103 | pass diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B008_B006_B008.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B008_B006_B008.py.snap index edaaadb944..3fbd47e194 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B008_B006_B008.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B008_B006_B008.py.snap @@ -1,31 +1,6 @@ --- source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs --- -B008 Do not perform function call `range` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable - --> B006_B008.py:102:61 - | -101 | # N.B. we're also flagging the function call in the comprehension -102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): - | ^^^^^^^^ -103 | pass - | - -B008 Do not perform function call `range` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable - --> B006_B008.py:106:64 - | -106 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}): - | ^^^^^^^^ -107 | pass - | - -B008 Do not perform function call `range` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable - --> B006_B008.py:110:60 - | -110 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}): - | ^^^^^^^^ -111 | pass - | - B008 Do not perform function call `time.time` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable --> B006_B008.py:126:39 | diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_B008.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_B008.py.snap index 9ee2465a7f..7bdf721a3f 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_B008.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__preview__B006_B006_B008.py.snap @@ -149,14 +149,14 @@ note: This is an unsafe fix and may change runtime behavior B006 [*] Do not use mutable data structures for argument defaults --> B006_B008.py:102:46 | -101 | # N.B. we're also flagging the function call in the comprehension +101 | # B006 still flags mutable comprehension defaults. 102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): | ^^^^^^^^^^^^^^^^^^^^^^^^ 103 | pass | help: Replace with `None`; initialize within function | -101 | # N.B. we're also flagging the function call in the comprehension +101 | # B006 still flags mutable comprehension defaults. - def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): 102 + def list_comprehension_also_not_okay(default=None): 103 | pass diff --git a/crates/ruff_python_stdlib/src/typing.rs b/crates/ruff_python_stdlib/src/typing.rs index d3c97889fe..7e80065446 100644 --- a/crates/ruff_python_stdlib/src/typing.rs +++ b/crates/ruff_python_stdlib/src/typing.rs @@ -324,6 +324,7 @@ pub fn is_immutable_return_type(qualified_name: &[&str]) -> bool { | "float" | "frozenset" | "int" + | "range" | "str" | "tuple" | "slice" From 958efb1ff40d8a433bde48ce84b9e35e99332350 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 28 Jul 2026 11:57:09 -0500 Subject: [PATCH 098/390] Vendor latest annotate-snippets (#27033) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This is a first step towards #20411. After this, we would work to either upstream changes or adjust things within Ruff. Performance improvements have already been upstreamed (https://github.com/rust-lang/annotate-snippets-rs/pull/437, https://github.com/rust-lang/annotate-snippets-rs/pull/440) Re-implemented the following from the fork: - Hidden snippets (https://github.com/rust-lang/annotate-snippets-rs/issues/448) - Renderer-level disabling of hyperlinks - Jupyter cell indices (https://github.com/rust-lang/annotate-snippets-rs/issues/449) - Fixable mark (https://github.com/rust-lang/annotate-snippets-rs/issues/450) - `lineno_offset` (maybe not needed if #20648 is taken care of) - anonymized lines affects origin (https://github.com/rust-lang/annotate-snippets-rs/issues/451) - cut indicator (maybe not needed if we can switch to unicode renderer) Additional hacks to minimize behavior change include - Adjust upstream's hidden severity behavior to match Ruff's (https://github.com/rust-lang/annotate-snippets-rs/issues/452) - How newlines are annotated (https://github.com/rust-lang/annotate-snippets-rs/issues/454) - To not indent for line numbers when none will be present (https://github.com/rust-lang/annotate-snippets-rs/issues/453) - Enabling `simd` by default All of the above represents work that needs to be resolved to address #20411. `lib.name` hack is used to simplify maintenance with the assumption that we'll continue working towards #20411. Behavior changes that were included - Tweak when implicit padding lines are present - Fix Origin's line/column is now derived from the primary annotation - Make consistent the rendering of primary/context annotations - Fix snippets to show up for empty source (but may degrade #24528) - Fix handling of term width - Fix rendering of annotations between newline and first character (granted, maybe the caller is wrong) - Tweak multiline annotation markers New or upcoming features annotate-snippets features that we will be able to leverage by moving this forward - `AnnotationKind::Visible` to improve the quality of built-in code-folding if Ruff were to switch to it - `DecorStyle::Unicode` - *(upcoming)* No-graphiscs mode for screen readers (may also be useful for AIs) Deferred: - Moving related `Group`s into the same `Report` - Using `secondary_title` for any `Group` after the first in a `Report` - Using a `Snippet`, instead of titles, for "virtual code" like https://github.com/astral-sh/ruff/pull/27033#discussion_r3622727236 - Switching hidden snippets to `Origin` ## Test Plan Within cpython, I ran ```console $ hyperfine -i "../../ruff/target-baseline/release/ruff check" "../../ruff/target-update/release/ruff check" -N -m20 --output=null Benchmark 1: ../../ruff/target-baseline/release/ruff check Time (mean ± σ): 27.5 ms ± 1.1 ms [User: 33.0 ms, System: 108.4 ms] Range (min … max): 25.0 ms … 31.7 ms 94 runs Warning: Ignoring non-zero exit code. Benchmark 2: ../../ruff/target-update/release/ruff check Time (mean ± σ): 28.1 ms ± 2.0 ms [User: 33.4 ms, System: 105.7 ms] Range (min … max): 24.8 ms … 38.9 ms 102 runs Warning: Ignoring non-zero exit code. Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use t he '--warmup' or '--prepare' options. Summary ../../ruff/target-baseline/release/ruff check ran 1.03 ± 0.08 times faster than ../../ruff/target-update/release/ruff check ``` Before: image After: image --- Cargo.lock | 56 +- Cargo.toml | 1 - crates/ruff/src/commands/format.rs | 1 - crates/ruff/tests/cli/format.rs | 20 +- crates/ruff/tests/cli/lint.rs | 2 - crates/ruff/tests/integration_test.rs | 23 - crates/ruff_annotate_snippets/Cargo.toml | 87 +- .../examples/custom_error.rs | 32 + .../custom_error.svg} | 14 +- .../examples/custom_level.rs | 64 + .../examples/custom_level.svg | 62 + .../examples/elide_header.rs | 21 + .../examples/elide_header.svg | 44 + .../examples/expected_type.rs | 34 +- .../examples/expected_type.svg | 26 +- .../ruff_annotate_snippets/examples/footer.rs | 27 +- .../examples/footer.svg | 18 +- .../ruff_annotate_snippets/examples/format.rs | 40 +- .../examples/format.svg | 63 +- .../examples/highlight_message.rs | 67 + .../examples/highlight_message.svg | 64 + .../examples/highlight_source.rs | 31 + .../examples/highlight_source.svg | 40 + .../examples/id_hyperlink.rs | 32 + .../examples/id_hyperlink.svg | 40 + .../examples/multi_suggestion.rs | 75 + .../examples/multi_suggestion.svg | 82 + .../examples/multislice.rs | 26 +- .../examples/multislice.svg | 20 +- .../examples/struct_name_as_context.rs | 27 + .../examples/struct_name_as_context.svg | 44 + crates/ruff_annotate_snippets/src/level.rs | 235 + crates/ruff_annotate_snippets/src/lib.rs | 133 +- .../src/renderer/display_list.rs | 1946 ------ .../src/renderer/margin.rs | 21 +- .../src/renderer/mod.rs | 409 +- .../src/renderer/render.rs | 2900 +++++++++ .../src/renderer/source_map.rs | 828 +++ .../src/renderer/styled_buffer.rs | 118 +- .../src/renderer/stylesheet.rs | 46 +- crates/ruff_annotate_snippets/src/snippet.rs | 641 +- .../ann_eof.ascii.term.svg} | 10 +- .../tests/color/ann_eof.rs | 21 + .../tests/color/ann_eof.unicode.term.svg | 34 + .../ann_insertion.ascii.term.svg} | 10 +- .../tests/color/ann_insertion.rs | 21 + .../color/ann_insertion.unicode.term.svg | 34 + .../ann_multiline.ascii.term.svg} | 16 +- .../tests/color/ann_multiline.rs | 34 + .../color/ann_multiline.unicode.term.svg | 40 + .../ann_multiline2.ascii.term.svg} | 10 +- .../tests/color/ann_multiline2.rs | 34 + .../color/ann_multiline2.unicode.term.svg | 40 + .../ann_removed_nl.ascii.term.svg} | 10 +- .../tests/color/ann_removed_nl.rs | 21 + .../color/ann_removed_nl.unicode.term.svg | 34 + ...sure_emoji_highlight_width.ascii.term.svg} | 10 +- .../color/ensure_emoji_highlight_width.rs | 25 + ...ure_emoji_highlight_width.unicode.term.svg | 34 + .../first_snippet_is_primary.ascii.term.svg | 54 + .../tests/color/first_snippet_is_primary.rs | 52 + .../first_snippet_is_primary.unicode.term.svg | 54 + .../fold_ann_multiline.ascii.term.svg} | 23 +- .../tests/color/fold_ann_multiline.rs | 55 + .../color/fold_ann_multiline.unicode.term.svg | 48 + .../fold_bad_origin_line.ascii.term.svg} | 13 +- .../tests/color/fold_bad_origin_line.rs | 26 + .../fold_bad_origin_line.unicode.term.svg | 34 + .../fold_leading.ascii.term.svg} | 10 +- .../tests/color/fold_leading.rs | 37 + .../fold_leading.unicode.term.svg} | 14 +- .../fold_trailing.ascii.term.svg} | 10 +- .../tests/color/fold_trailing.rs | 36 + .../color/fold_trailing.unicode.term.svg | 34 + ...f_line_with_wide_characters.ascii.term.svg | 54 + ...ighlight_diff_line_with_wide_characters.rs | 44 + ...line_with_wide_characters.unicode.term.svg | 54 + ...light_duplicated_diff_lines.ascii.term.svg | 62 + .../color/highlight_duplicated_diff_lines.rs | 75 + ...ght_duplicated_diff_lines.unicode.term.svg | 62 + ...ighlight_first_line_tab_371.ascii.term.svg | 41 + .../color/highlight_first_line_tab_371.rs | 21 + .../highlight_source.ascii.term.svg} | 12 +- .../tests/color/highlight_source.rs | 43 + .../color/highlight_source.unicode.term.svg | 38 + ...ht_source_multi_width_chars.ascii.term.svg | 30 + .../highlight_source_multi_width_chars.rs | 20 + ..._source_multi_width_chars.unicode.term.svg | 30 + ...ght_source_zero_width_chars.ascii.term.svg | 30 + .../highlight_source_zero_width_chars.rs | 21 + ...t_source_zero_width_chars.unicode.term.svg | 30 + .../issue_9.ascii.term.svg} | 23 +- .../tests/color/issue_9.rs | 33 + .../tests/color/issue_9.unicode.term.svg | 46 + .../tests/color/main.rs | 32 + .../multiline_removal_indent.ascii.term.svg | 34 + .../tests/color/multiline_removal_indent.rs | 19 + .../multiline_removal_indent.unicode.term.svg | 34 + ...line_removal_last_line_tabs.ascii.term.svg | 37 + .../color/multiline_removal_last_line_tabs.rs | 18 + ...ne_removal_last_line_tabs.unicode.term.svg | 37 + ...ultiline_removal_suggestion.ascii.term.svg | 68 + .../color/multiline_removal_suggestion.rs | 110 + ...tiline_removal_suggestion.unicode.term.svg | 68 + .../multiple_annotations.ascii.term.svg} | 20 +- .../tests/color/multiple_annotations.rs | 46 + .../multiple_annotations.unicode.term.svg | 54 + ...ltiple_highlight_duplicated.ascii.term.svg | 78 + .../color/multiple_highlight_duplicated.rs | 93 + ...iple_highlight_duplicated.unicode.term.svg | 78 + .../multiple_multiline_removal.ascii.term.svg | 120 + .../tests/color/multiple_multiline_removal.rs | 93 + ...ultiple_multiline_removal.unicode.term.svg | 120 + .../primary_title_second_group.ascii.term.svg | 41 + .../tests/color/primary_title_second_group.rs | 31 + ...rimary_title_second_group.unicode.term.svg | 41 + ...leading_tab_label_alignment.ascii.term.svg | 34 + .../regression_leading_tab_label_alignment.rs | 27 + ...ading_tab_label_alignment.unicode.term.svg | 34 + ...ssion_leading_tab_long_line.ascii.term.svg | 34 + .../color/regression_leading_tab_long_line.rs | 26 + ...ion_leading_tab_long_line.unicode.term.svg | 34 + .../simple.ascii.term.svg} | 17 +- .../tests/color/simple.rs | 37 + .../tests/color/simple.unicode.term.svg | 40 + .../tests/color/strip_line.ascii.term.svg | 34 + .../tests/color/strip_line.rs | 30 + .../tests/color/strip_line.unicode.term.svg | 34 + .../color/strip_line_char.ascii.term.svg | 34 + .../tests/color/strip_line_char.rs | 30 + .../color/strip_line_char.unicode.term.svg | 34 + .../color/strip_line_non_ws.ascii.term.svg | 38 + .../tests/color/strip_line_non_ws.rs | 36 + .../color/strip_line_non_ws.unicode.term.svg | 38 + .../tests/color/styled_title.ascii.term.svg | 56 + .../tests/color/styled_title.rs | 50 + .../tests/color/styled_title.unicode.term.svg | 56 + .../ruff_annotate_snippets/tests/examples.rs | 95 + .../tests/fixtures/color/ann_eof.toml | 15 - .../tests/fixtures/color/ann_insertion.toml | 15 - .../tests/fixtures/color/ann_multiline.toml | 21 - .../tests/fixtures/color/ann_multiline2.toml | 21 - .../tests/fixtures/color/ann_removed_nl.toml | 15 - .../color/ensure-emoji-highlight-width.toml | 18 - .../fixtures/color/fold_ann_multiline.toml | 44 - .../fixtures/color/fold_bad_origin_line.toml | 20 - .../tests/fixtures/color/fold_leading.toml | 29 - .../tests/fixtures/color/fold_trailing.toml | 28 - .../tests/fixtures/color/issue_9.toml | 31 - .../fixtures/color/multiple_annotations.toml | 32 - ...egression_leading_tab_label_alignment.toml | 45 - .../regression_leading_tab_long_line.svg | 36 - .../regression_leading_tab_long_line.toml | 20 - .../tests/fixtures/color/simple.toml | 22 - .../tests/fixtures/color/strip_line.toml | 18 - .../tests/fixtures/color/strip_line_char.toml | 18 - .../fixtures/color/strip_line_non_ws.svg | 40 - .../fixtures/color/strip_line_non_ws.toml | 26 - .../tests/fixtures/deserialize.rs | 130 - .../tests/fixtures/main.rs | 41 - .../ruff_annotate_snippets/tests/formatter.rs | 5532 ++++++++++++++-- .../tests/rustc_tests.rs | 5540 ++++++++++++++++- crates/ruff_db/src/diagnostic/mod.rs | 24 +- crates/ruff_db/src/diagnostic/render.rs | 103 +- crates/ruff_db/src/diagnostic/render/full.rs | 30 +- ...ull__tests__notebook_output_with_diff.snap | 2 - ...ebook_output_with_diff_spanning_cells.snap | 1 - .../mdtest/flake8-bandit/unsafe-markup-use.md | 1 - .../pytest-parametrize-names-wrong-type.md | 1 - .../mdtest/notebook/cell-boundaries.md | 1 - ...g-dot-format-extra-positional-arguments.md | 2 - .../pylint/invalid-character-backspace.md | 8 - .../mdtest/pylint/redefined-loop-name.md | 5 - .../too-many-statements-in-try-clause.md | 1 - .../pyupgrade/deprecated-abc-decorator.md | 3 - .../resources/mdtest/pyupgrade/f-string.md | 6 - .../resources/mdtest/pyupgrade/pep-695.md | 3 - .../mdtest/ruff/fallible-context-manager.md | 1 - .../mdtest/ruff/invalid-pyproject-toml.md | 1 - .../mdtest/ruff/logging-eager-conversion.md | 2 - .../resources/mdtest/ruff/noqa-comments.md | 13 - .../mdtest/ruff/rule-codes-in-selectors.md | 6 - .../rule-codes-in-suppression-comments.md | 12 - .../resources/mdtest/suppression/ignore.md | 20 - ...les__airflow__tests__AIR003_AIR003.py.snap | 1 - ...tests__AIR003_AIR003_dag_decorator.py.snap | 1 - ...les__airflow__tests__AIR004_AIR004.py.snap | 7 - ..._airflow__tests__AIR004_AIR004_sdk.py.snap | 1 - ...airflow__tests__AIR301_AIR301_args.py.snap | 1 - ...sts__AIR301_AIR301_class_attribute.py.snap | 1 - ...flow__tests__AIR301_AIR301_context.py.snap | 8 - ...irflow__tests__AIR301_AIR301_names.py.snap | 7 - ...ow__tests__AIR301_AIR301_names_fix.py.snap | 4 - ...__AIR301_AIR301_provider_names_fix.py.snap | 1 - ...rflow__tests__AIR302_AIR302_amazon.py.snap | 1 - ...rflow__tests__AIR302_AIR302_celery.py.snap | 1 - ...w__tests__AIR302_AIR302_common_sql.py.snap | 13 - ..._tests__AIR302_AIR302_daskexecutor.py.snap | 1 - ...irflow__tests__AIR302_AIR302_druid.py.snap | 1 - ..._airflow__tests__AIR302_AIR302_fab.py.snap | 1 - ...airflow__tests__AIR302_AIR302_hdfs.py.snap | 1 - ...airflow__tests__AIR302_AIR302_hive.py.snap | 2 - ...airflow__tests__AIR302_AIR302_http.py.snap | 1 - ...airflow__tests__AIR302_AIR302_jdbc.py.snap | 1 - ...w__tests__AIR302_AIR302_kubernetes.py.snap | 2 - ...irflow__tests__AIR302_AIR302_mysql.py.snap | 1 - ...rflow__tests__AIR302_AIR302_oracle.py.snap | 1 - ...ow__tests__AIR302_AIR302_papermill.py.snap | 1 - ..._airflow__tests__AIR302_AIR302_pig.py.snap | 1 - ...rflow__tests__AIR302_AIR302_presto.py.snap | 1 - ...irflow__tests__AIR302_AIR302_samba.py.snap | 1 - ...irflow__tests__AIR302_AIR302_slack.py.snap | 1 - ...airflow__tests__AIR302_AIR302_smtp.py.snap | 1 - ...rflow__tests__AIR302_AIR302_sqlite.py.snap | 1 - ...low__tests__AIR302_AIR302_standard.py.snap | 2 - ...flow__tests__AIR302_AIR302_zendesk.py.snap | 1 - ...les__airflow__tests__AIR304_AIR304.py.snap | 2 - ...airflow__tests__AIR311_AIR311_args.py.snap | 2 - ...irflow__tests__AIR311_AIR311_names.py.snap | 4 - ...les__airflow__tests__AIR312_AIR312.py.snap | 1 - ...irflow__tests__AIR321_AIR321_names.py.snap | 1 - ...s__eradicate__tests__ERA001_ERA001.py.snap | 2 - ..._flake8_2020__tests__YTT102_YTT102.py.snap | 1 - ..._flake8_2020__tests__YTT103_YTT103.py.snap | 1 - ..._flake8_2020__tests__YTT201_YTT201.py.snap | 1 - ..._flake8_2020__tests__YTT203_YTT203.py.snap | 1 - ..._flake8_2020__tests__YTT204_YTT204.py.snap | 1 - ..._flake8_2020__tests__YTT301_YTT301.py.snap | 1 - ..._flake8_2020__tests__YTT302_YTT302.py.snap | 1 - ..._flake8_2020__tests__YTT303_YTT303.py.snap | 1 - ...__flake8_annotations__tests__defaults.snap | 3 - ...e8_async__tests__ASYNC100_ASYNC100.py.snap | 11 - ...e8_async__tests__ASYNC105_ASYNC105.py.snap | 1 - ...s__flake8_async__tests__ASYNC109_0.py.snap | 1 - ..._async__tests__ASYNC109_ASYNC109_0.py.snap | 1 - ...e8_async__tests__ASYNC110_ASYNC110.py.snap | 5 - ...e8_async__tests__ASYNC115_ASYNC115.py.snap | 5 - ...e8_async__tests__ASYNC116_ASYNC116.py.snap | 5 - ...e8_async__tests__ASYNC119_ASYNC119.py.snap | 5 - ...e8_async__tests__ASYNC210_ASYNC210.py.snap | 5 - ...e8_async__tests__ASYNC221_ASYNC22x.py.snap | 2 - ...e8_async__tests__ASYNC222_ASYNC22x.py.snap | 2 - ...e8_async__tests__ASYNC230_ASYNC230.py.snap | 5 - ...e8_async__tests__ASYNC240_ASYNC240.py.snap | 1 - ...e8_async__tests__ASYNC250_ASYNC250.py.snap | 1 - ...e8_async__tests__ASYNC251_ASYNC251.py.snap | 1 - ...s__flake8_bandit__tests__S101_S101.py.snap | 2 - ...s__flake8_bandit__tests__S102_S102.py.snap | 1 - ...s__flake8_bandit__tests__S103_S103.py.snap | 1 - ...s__flake8_bandit__tests__S104_S104.py.snap | 2 - ...s__flake8_bandit__tests__S105_S105.py.snap | 1 - ...s__flake8_bandit__tests__S106_S106.py.snap | 1 - ...les__flake8_bandit__tests__S110_typed.snap | 1 - ...s__flake8_bandit__tests__S301_S301.py.snap | 1 - ...s__flake8_bandit__tests__S307_S307.py.snap | 1 - ...s__flake8_bandit__tests__S310_S310.py.snap | 2 - ...s__flake8_bandit__tests__S312_S312.py.snap | 1 - ...s__flake8_bandit__tests__S401_S401.py.snap | 1 - ...s__flake8_bandit__tests__S402_S402.py.snap | 1 - ...s__flake8_bandit__tests__S403_S403.py.snap | 1 - ...s__flake8_bandit__tests__S404_S404.py.snap | 1 - ...s__flake8_bandit__tests__S405_S405.py.snap | 1 - ...s__flake8_bandit__tests__S406_S406.py.snap | 1 - ...s__flake8_bandit__tests__S407_S407.py.snap | 1 - ...s__flake8_bandit__tests__S408_S408.py.snap | 1 - ...s__flake8_bandit__tests__S409_S409.py.snap | 1 - ...s__flake8_bandit__tests__S410_S410.py.snap | 1 - ...s__flake8_bandit__tests__S411_S411.py.snap | 1 - ...s__flake8_bandit__tests__S412_S412.py.snap | 1 - ...s__flake8_bandit__tests__S413_S413.py.snap | 1 - ...s__flake8_bandit__tests__S415_S415.py.snap | 1 - ...s__flake8_bandit__tests__S501_S501.py.snap | 1 - ...s__flake8_bandit__tests__S506_S506.py.snap | 1 - ...s__flake8_bandit__tests__S601_S601.py.snap | 1 - ...s__flake8_bandit__tests__S602_S602.py.snap | 1 - ...s__flake8_bandit__tests__S603_S603.py.snap | 1 - ...s__flake8_bandit__tests__S604_S604.py.snap | 1 - ...s__flake8_bandit__tests__S605_S605.py.snap | 2 - ...s__flake8_bandit__tests__S606_S606.py.snap | 1 - ...s__flake8_bandit__tests__S607_S607.py.snap | 1 - ...s__flake8_bandit__tests__S609_S609.py.snap | 1 - ...s__flake8_bandit__tests__S611_S611.py.snap | 1 - ...s__flake8_bandit__tests__S701_S701.py.snap | 1 - ...s__flake8_bandit__tests__S702_S702.py.snap | 1 - ..._bandit__tests__preview__S301_S301.py.snap | 1 - ..._bandit__tests__preview__S307_S307.py.snap | 1 - ..._bandit__tests__preview__S308_S308.py.snap | 1 - ..._bandit__tests__preview__S311_S311.py.snap | 1 - ..._bandit__tests__preview__S506_S506.py.snap | 1 - ...e8_boolean_trap__tests__FBT003_FBT.py.snap | 2 - ..._trap__tests__extend_allowed_callable.snap | 1 - ...__flake8_bugbear__tests__B002_B002.py.snap | 2 - ...flake8_bugbear__tests__B006_B006_4.py.snap | 4 +- ...flake8_bugbear__tests__B006_B006_5.py.snap | 2 - ...ke8_bugbear__tests__B006_B006_B008.py.snap | 1 - ...ke8_bugbear__tests__B009_B009_B010.py.snap | 2 - ...ke8_bugbear__tests__B010_B009_B010.py.snap | 1 - ...__flake8_bugbear__tests__B011_B011.py.snap | 1 - ...__flake8_bugbear__tests__B012_B012.py.snap | 5 - ...__flake8_bugbear__tests__B015_B015.py.snap | 3 - ...flake8_bugbear__tests__B017_B017_1.py.snap | 1 - ...__flake8_bugbear__tests__B018_B018.py.snap | 5 - ...__flake8_bugbear__tests__B021_B021.py.snap | 8 - ...__flake8_bugbear__tests__B023_B023.py.snap | 6 - ...__flake8_bugbear__tests__B026_B026.py.snap | 1 - ...__flake8_bugbear__tests__B029_B029.py.snap | 1 - ...__flake8_bugbear__tests__B031_B031.py.snap | 1 - ...__flake8_bugbear__tests__B033_B033.py.snap | 20 +- ...__flake8_bugbear__tests__B035_B035.py.snap | 1 - ...__flake8_bugbear__tests__B039_B039.py.snap | 1 - ...__flake8_bugbear__tests__B043_B043.py.snap | 1 - ...__flake8_bugbear__tests__B901_B901.py.snap | 8 - ...ests__B903_class_as_data_structure.py.snap | 4 - ..._B903_py39_class_as_data_structure.py.snap | 4 - ...__flake8_bugbear__tests__B904_B904.py.snap | 1 - ...rules__flake8_bugbear__tests__B905.py.snap | 1 - ...__flake8_bugbear__tests__B909_B909.py.snap | 2 - ...__flake8_bugbear__tests__B912_B912.py.snap | 1 - ...s__extend_immutable_calls_arg_default.snap | 1 - ...ts__extend_mutable_contextvar_default.snap | 1 - ...gbear__tests__preview__B006_B006_4.py.snap | 4 +- ...gbear__tests__preview__B006_B006_5.py.snap | 2 - ...ar__tests__preview__B006_B006_B008.py.snap | 1 - ..._flake8_builtins__tests__A001_A001.py.snap | 1 - ...sts__A001_A001.py_builtins_ignorelist.snap | 1 - ..._flake8_builtins__tests__A003_A003.py.snap | 1 - ...sts__A003_A003.py_builtins_ignorelist.snap | 1 - ..._flake8_builtins__tests__A004_A004.py.snap | 2 - ...sts__A004_A004.py_builtins_ignorelist.snap | 2 - ...rules__flake8_commas__tests__COM81.py.snap | 1 - ...8_comprehensions__tests__C400_C400.py.snap | 2 - ...rehensions__tests__C400_C400_py315.py.snap | 1 - ...8_comprehensions__tests__C401_C401.py.snap | 3 - ...rehensions__tests__C401_C401_py315.py.snap | 1 - ...8_comprehensions__tests__C403_C403.py.snap | 2 - ...rehensions__tests__C403_C403_py315.py.snap | 1 - ...8_comprehensions__tests__C404_C404.py.snap | 1 - ...8_comprehensions__tests__C405_C405.py.snap | 2 - ...8_comprehensions__tests__C408_C408.py.snap | 1 - ...8_comprehensions__tests__C409_C409.py.snap | 1 - ...8_comprehensions__tests__C410_C410.py.snap | 1 - ...rehensions__tests__C411_C411_py315.py.snap | 1 - ...8_comprehensions__tests__C413_C413.py.snap | 1 - ...8_comprehensions__tests__C414_C414.py.snap | 1 - ...8_comprehensions__tests__C417_C417.py.snap | 2 - ...comprehensions__tests__C417_C417_1.py.snap | 1 - ...rehensions__tests__C418_C418_py315.py.snap | 1 - ...rehensions__tests__C419_C419_py315.py.snap | 1 - ...8_comprehensions__tests__C420_C420.py.snap | 11 - ...comprehensions__tests__C420_C420_1.py.snap | 1 - ...comprehensions__tests__C420_C420_2.py.snap | 1 - ...e8_datetimez__tests__DTZ011_DTZ011.py.snap | 1 - ...e8_datetimez__tests__DTZ012_DTZ012.py.snap | 1 - ...e8_datetimez__tests__DTZ901_DTZ901.py.snap | 2 - ..._flake8_debugger__tests__T100_T100.py.snap | 2 - ..._flake8_django__tests__DJ001_DJ001.py.snap | 4 - ..._flake8_django__tests__DJ003_DJ003.py.snap | 2 - ..._flake8_django__tests__DJ006_DJ006.py.snap | 1 - ..._flake8_django__tests__DJ007_DJ007.py.snap | 2 - ..._flake8_django__tests__DJ012_DJ012.py.snap | 6 - ...__rules__flake8_errmsg__tests__custom.snap | 9 - ...rules__flake8_errmsg__tests__defaults.snap | 11 - ...lake8_errmsg__tests__string_exception.snap | 2 - ...flake8_executable__tests__EXE004_1.py.snap | 1 - ...flake8_executable__tests__EXE004_4.py.snap | 1 - ...flake8_executable__tests__EXE005_1.py.snap | 1 - ...flake8_executable__tests__EXE005_2.py.snap | 1 - ...flake8_executable__tests__EXE005_3.py.snap | 1 - ...xme__tests__line-contains-todo_T00.py.snap | 1 - ...tring-in-get-text-func-call_INT001.py.snap | 3 - ...ormat-in-get-text-func-call_INT002.py.snap | 3 - ...tring-in-get-text-func-call_INT001.py.snap | 5 - ...ormat-in-get-text-func-call_INT002.py.snap | 5 - ...rintf-in-get-text-func-call_INT003.py.snap | 5 - ...rintf-in-get-text-func-call_INT003.py.snap | 3 - ...icit_str_concat__tests__ISC001_ISC.py.snap | 1 - ...at__tests__ISC001_ISC_syntax_error.py.snap | 4 +- ...__tests__ISC001_ISC_syntax_error_2.py.snap | 2 - ...icit_str_concat__tests__ISC002_ISC.py.snap | 1 - ...at__tests__ISC002_ISC_syntax_error.py.snap | 4 +- ...__tests__ISC002_ISC_syntax_error_2.py.snap | 1 - ...icit_str_concat__tests__ISC003_ISC.py.snap | 1 - ...oncat__tests__multiline_ISC001_ISC.py.snap | 1 - ...oncat__tests__multiline_ISC002_ISC.py.snap | 1 - ...8_import_conventions__tests__defaults.snap | 2 - ..._import_conventions__tests__same_name.snap | 1 - ...ake8_logging__tests__LOG002_LOG002.py.snap | 1 - ...e8_logging__tests__LOG004_LOG004_0.py.snap | 2 - ...e8_logging__tests__LOG004_LOG004_1.py.snap | 1 - ...e8_logging__tests__LOG014_LOG014_0.py.snap | 3 - ...e8_logging__tests__LOG014_LOG014_1.py.snap | 1 - ...ake8_logging__tests__LOG015_LOG015.py.snap | 2 - ...flake8_logging_format__tests__G001.py.snap | 1 - ...flake8_logging_format__tests__G002.py.snap | 1 - ...flake8_logging_format__tests__G003.py.snap | 1 - ...flake8_logging_format__tests__G004.py.snap | 3 - ...ging_format__tests__G004_arg_order.py.snap | 1 - ...ormat__tests__G004_implicit_concat.py.snap | 1 - ..._format__tests__preview__G004_G004.py.snap | 3 - ...ests__preview__G004_G004_arg_order.py.snap | 1 - ...preview__G004_G004_implicit_concat.py.snap | 1 - ...__flake8_pie__tests__PIE790_PIE790.py.snap | 19 - ...__flake8_pie__tests__PIE794_PIE794.py.snap | 5 - ...__flake8_pie__tests__PIE796_PIE796.py.snap | 9 - ...__flake8_pie__tests__PIE800_PIE800.py.snap | 1 - ...__flake8_pie__tests__PIE807_PIE807.py.snap | 3 - ...__flake8_pie__tests__PIE808_PIE808.py.snap | 2 - ..._flake8_pyi__tests__PYI002_PYI002.pyi.snap | 1 - ..._flake8_pyi__tests__PYI005_PYI005.pyi.snap | 1 - ...__flake8_pyi__tests__PYI006_PYI006.py.snap | 1 - ..._flake8_pyi__tests__PYI006_PYI006.pyi.snap | 1 - ..._flake8_pyi__tests__PYI007_PYI007.pyi.snap | 1 - ..._flake8_pyi__tests__PYI009_PYI009.pyi.snap | 1 - ...__flake8_pyi__tests__PYI013_PYI013.py.snap | 5 - ...__flake8_pyi__tests__PYI016_PYI016.py.snap | 1 - ..._flake8_pyi__tests__PYI016_PYI016.pyi.snap | 2 - ...flake8_pyi__tests__PYI019_PYI019_0.py.snap | 14 - ...lake8_pyi__tests__PYI019_PYI019_0.pyi.snap | 17 - ...lake8_pyi__tests__PYI019_PYI019_1.pyi.snap | 1 - ...flake8_pyi__tests__PYI025_PYI025_1.py.snap | 1 - ...flake8_pyi__tests__PYI025_PYI025_3.py.snap | 1 - ...lake8_pyi__tests__PYI025_PYI025_3.pyi.snap | 1 - ...__flake8_pyi__tests__PYI032_PYI032.py.snap | 2 - ..._flake8_pyi__tests__PYI032_PYI032.pyi.snap | 2 - ...__flake8_pyi__tests__PYI034_PYI034.py.snap | 3 - ..._flake8_pyi__tests__PYI034_PYI034.pyi.snap | 2 - ..._flake8_pyi__tests__PYI036_PYI036.pyi.snap | 2 - ...lake8_pyi__tests__PYI041_PYI041_1.pyi.snap | 10 - ..._flake8_pyi__tests__PYI045_PYI045.pyi.snap | 1 - ..._flake8_pyi__tests__PYI052_PYI052.pyi.snap | 1 - ..._flake8_pyi__tests__PYI054_PYI054.pyi.snap | 1 - ...__flake8_pyi__tests__PYI055_PYI055.py.snap | 4 - ..._flake8_pyi__tests__PYI055_PYI055.pyi.snap | 1 - ...__flake8_pyi__tests__PYI059_PYI059.py.snap | 1 - ..._flake8_pyi__tests__PYI059_PYI059.pyi.snap | 2 - ...__flake8_pyi__tests__PYI061_PYI061.py.snap | 3 - ..._flake8_pyi__tests__PYI061_PYI061.pyi.snap | 10 - ...__flake8_pyi__tests__PYI062_PYI062.py.snap | 1 - ..._flake8_pyi__tests__PYI062_PYI062.pyi.snap | 1 - ...yi__tests__preview_PYI041_PYI041_4.py.snap | 1 - ...ke8_pyi__tests__py38_PYI061_PYI061.py.snap | 3 - ...e8_pyi__tests__py38_PYI061_PYI061.pyi.snap | 10 - ..._tests__pyi021_pie790_isolation_check.snap | 1 - ...es__flake8_pytest_style__tests__PT008.snap | 1 - ...es__flake8_pytest_style__tests__PT009.snap | 1 - ...es__flake8_pytest_style__tests__PT012.snap | 4 - ...es__flake8_pytest_style__tests__PT013.snap | 1 - ...es__flake8_pytest_style__tests__PT017.snap | 1 - ...es__flake8_pytest_style__tests__PT018.snap | 4 - ...es__flake8_pytest_style__tests__PT022.snap | 3 - ...es__flake8_pytest_style__tests__PT028.snap | 3 - ...es__flake8_pytest_style__tests__PT031.snap | 4 - ...8_pytest_style__tests__is_pytest_test.snap | 1 - ...style__tests__preview__PT018_PT018.py.snap | 2 - ..._tests__only_multiline_doubles_all.py.snap | 1 - ...docstring_doubles_module_multiline.py.snap | 1 - ...ocstring_doubles_module_singleline.py.snap | 1 - ...ubles_over_docstring_singles_class.py.snap | 1 - ...es_over_docstring_singles_function.py.snap | 1 - ...g_singles_mixed_quotes_class_var_1.py.snap | 2 - ...g_singles_mixed_quotes_class_var_2.py.snap | 2 - ...xed_quotes_module_singleline_var_1.py.snap | 1 - ...xed_quotes_module_singleline_var_2.py.snap | 1 - ...ngles_over_docstring_doubles_class.py.snap | 1 - ...es_over_docstring_doubles_function.py.snap | 1 - ...g_doubles_mixed_quotes_class_var_1.py.snap | 1 - ...g_doubles_mixed_quotes_class_var_2.py.snap | 1 - ...docstring_singles_module_multiline.py.snap | 1 - ...ocstring_singles_module_singleline.py.snap | 1 - ...quire_doubles_over_singles_escaped.py.snap | 2 - ...re_doubles_over_singles_escaped_py311.snap | 1 - ...s_over_singles_escaped_unnecessary.py.snap | 1 - ...uire_doubles_over_singles_implicit.py.snap | 1 - ...ver_singles_would_be_triple_quotes.py.snap | 2 - ...quire_singles_over_doubles_escaped.py.snap | 2 - ...re_singles_over_doubles_escaped_py311.snap | 1 - ...s_over_doubles_escaped_unnecessary.py.snap | 1 - ...uire_singles_over_doubles_implicit.py.snap | 1 - ...ver_doubles_would_be_triple_quotes.py.snap | 1 - ...ry-paren-on-raise-exception_RSE102.py.snap | 2 - ...lake8_return__tests__RET501_RET501.py.snap | 2 - ...lake8_return__tests__RET503_RET503.py.snap | 19 - ...lake8_return__tests__RET504_RET504.py.snap | 14 - ...lake8_return__tests__RET505_RET505.py.snap | 2 - ...self__tests__custom_method_decorators.snap | 1 - ...les__flake8_self__tests__ignore_names.snap | 2 - ...ts__private-member-access_SLF001_1.py.snap | 1 - ...ke8_simplify__tests__SIM101_SIM101.py.snap | 1 - ...ke8_simplify__tests__SIM103_SIM103.py.snap | 17 - ...8_simplify__tests__SIM105_SIM105_0.py.snap | 4 - ...8_simplify__tests__SIM105_SIM105_1.py.snap | 1 - ...8_simplify__tests__SIM105_SIM105_2.py.snap | 1 - ...8_simplify__tests__SIM105_SIM105_3.py.snap | 1 - ...8_simplify__tests__SIM105_SIM105_4.py.snap | 1 - ...ke8_simplify__tests__SIM107_SIM107.py.snap | 1 - ...ke8_simplify__tests__SIM108_SIM108.py.snap | 8 - ...ke8_simplify__tests__SIM110_SIM110.py.snap | 10 - ...ke8_simplify__tests__SIM110_SIM111.py.snap | 12 - ...ke8_simplify__tests__SIM113_SIM113.py.snap | 4 - ...ke8_simplify__tests__SIM114_SIM114.py.snap | 6 - ...ke8_simplify__tests__SIM118_SIM118.py.snap | 1 - ...ke8_simplify__tests__SIM210_SIM210.py.snap | 2 - ...ke8_simplify__tests__SIM222_SIM222.py.snap | 2 - ...ke8_simplify__tests__SIM223_SIM223.py.snap | 1 - ...ke8_simplify__tests__SIM905_SIM905.py.snap | 2 - ...ke8_simplify__tests__SIM910_SIM910.py.snap | 3 - ...ify__tests__preview__SIM113_SIM113.py.snap | 4 - ..._tidy_imports__tests__ban_all_imports.snap | 1 - ...dy_imports__tests__ban_parent_imports.snap | 1 - ...ts__tests__ban_parent_imports_package.snap | 1 - ...dy_imports__tests__banned_api_package.snap | 1 - ...__tests__preview_lazy_import_mismatch.snap | 1 - ...sts__preview_lazy_import_mismatch_all.snap | 1 - ..._invalid-todo-capitalization_TD006.py.snap | 1 - ...dos__tests__invalid-todo-tag_TD001.py.snap | 1 - ...__tests__missing-todo-author_TD002.py.snap | 1 - ...s__tests__missing-todo-colon_TD004.py.snap | 1 - ...ts__missing-todo-description_TD005.py.snap | 1 - ...os__tests__missing-todo-link_TD003.py.snap | 1 - ...__TC001-TC002-TC003_TC001-3_future.py.snap | 1 - ...import__TC001_TC001_future_present.py.snap | 1 - ...s__empty-type-checking-block_TC005.py.snap | 1 - ...mport-in-type-checking-block_quote.py.snap | 1 - ...ng__tests__quoted-type-alias_TC008.py.snap | 2 - ...ias_TC008_typing_execution_context.py.snap | 1 - ...g__tests__runtime-cast-value_TC006.py.snap | 10 - ...ort-in-type-checking-block_TC004_1.py.snap | 1 - ...rt-in-type-checking-block_TC004_11.py.snap | 1 - ...rt-in-type-checking-block_TC004_12.py.snap | 1 - ...rt-in-type-checking-block_TC004_17.py.snap | 1 - ...ort-in-type-checking-block_TC004_2.py.snap | 1 - ...mport-in-type-checking-block_quote.py.snap | 1 - ...k_runtime_evaluated_base_classes_1.py.snap | 3 - ...ock_runtime_evaluated_decorators_1.py.snap | 3 - ...-in-type-checking-block_whitespace.py.snap | 3 +- ...ests__runtime-string-union_TC010_1.py.snap | 1 - ...ests__runtime-string-union_TC010_2.py.snap | 1 - ...y-standard-library-import_init_var.py.snap | 1 - ...ly-standard-library-import_kw_only.py.snap | 1 - ...g__tests__tc004_precedence_over_tc007.snap | 1 - ...g__tests__tc010_precedence_over_tc008.snap | 1 - ...y-standard-library-import_init_var.py.snap | 1 - ...d-library-import_module__undefined.py.snap | 1 - ...ort_runtime_evaluated_decorators_2.py.snap | 1 - ...e_pathlib__tests__PTH124_py_path_1.py.snap | 1 - ...e_pathlib__tests__PTH124_py_path_2.py.snap | 1 - ..._use_pathlib__tests__PTH202_PTH202.py.snap | 2 - ...se_pathlib__tests__PTH202_PTH202_2.py.snap | 1 - ..._use_pathlib__tests__PTH203_PTH203.py.snap | 3 - ..._use_pathlib__tests__PTH204_PTH204.py.snap | 2 - ..._use_pathlib__tests__PTH205_PTH205.py.snap | 1 - ..._use_pathlib__tests__PTH208_PTH208.py.snap | 2 - ..._use_pathlib__tests__PTH211_PTH211.py.snap | 1 - ...ake8_use_pathlib__tests__full_name.py.snap | 1 - ...ake8_use_pathlib__tests__import_as.py.snap | 1 - ...e8_use_pathlib__tests__import_from.py.snap | 2 - ...use_pathlib__tests__import_from_as.py.snap | 1 - ...lib__tests__preview__PTH201_PTH201.py.snap | 1 - ...lib__tests__preview__PTH202_PTH202.py.snap | 2 - ...b__tests__preview__PTH202_PTH202_2.py.snap | 1 - ...lib__tests__preview__PTH203_PTH203.py.snap | 3 - ...lib__tests__preview__PTH204_PTH204.py.snap | 2 - ...lib__tests__preview__PTH205_PTH205.py.snap | 1 - ..._pathlib__tests__preview_full_name.py.snap | 1 - ..._pathlib__tests__preview_import_as.py.snap | 1 - ...athlib__tests__preview_import_from.py.snap | 2 - ...lib__tests__preview_import_from_as.py.snap | 1 - ...kage_first_and_third_party_imports.py.snap | 1 - ...kage_first_and_third_party_imports.py.snap | 1 - ...tests__add_newline_before_comments.py.snap | 1 - ..._isort__tests__as_imports_comments.py.snap | 1 - ..._rules__isort__tests__bom_unsorted.py.snap | 1 - ...sts__case_sensitive_case_sensitive.py.snap | 1 - ...to_furthest_relative_imports_order.py.snap | 1 - ...__isort__tests__combine_as_imports.py.snap | 1 - ...bine_as_imports_combine_as_imports.py.snap | 1 - ..._isort__tests__combine_import_from.py.snap | 1 - ...ter__rules__isort__tests__comments.py.snap | 1 - ..._isort__tests__deduplicate_imports.py.snap | 1 - ...es__isort__tests__detect_same_package.snap | 1 - ...les__isort__tests__fit_line_length.py.snap | 1 - ...rt__tests__fit_line_length_comment.py.snap | 1 - ...orce_single_line_force_single_line.py.snap | 1 - ..._tests__force_sort_within_sections.py.snap | 1 - ...ections_force_sort_within_sections.py.snap | 1 - ..._force_sort_within_sections_future.py.snap | 1 - ...sort_within_sections_with_as_names.py.snap | 1 - ...ns_lazy_force_sort_within_sections.py.snap | 1 - ..._rules__isort__tests__force_to_top.py.snap | 1 - ...__tests__force_to_top_force_to_top.py.snap | 1 - ...__isort__tests__force_wrap_aliases.py.snap | 1 - ...ce_wrap_aliases_force_wrap_aliases.py.snap | 1 - ...les__isort__tests__forced_separate.py.snap | 1 - ..._tests__from_first_lazy_from_first.py.snap | 1 - ...__rules__isort__tests__future_from.py.snap | 1 - ...kage_first_and_third_party_imports.py.snap | 1 - ..._rules__isort__tests__if_elif_else.py.snap | 1 - ...t__tests__import_from_after_import.py.snap | 1 - ...heading_force_sort_within_sections.py.snap | 1 - ...sts__import_heading_import_heading.py.snap | 1 - ...ing_import_heading_already_present.py.snap | 1 - ...t_heading_import_heading_duplicate.py.snap | 1 - ...rt_heading_import_heading_unsorted.py.snap | 1 - ...ing_partial_import_heading_partial.py.snap | 1 - ...tion_import_heading_single_section.py.snap | 1 - ...mport_heading_with_no_lines_before.py.snap | 1 - ...ading_import_heading_wrong_heading.py.snap | 1 - ...les__isort__tests__inline_comments.py.snap | 1 - ...sest_separate_local_folder_imports.py.snap | 1 - ...lder_separate_local_folder_imports.py.snap | 1 - ..._rules__isort__tests__lazy_imports.py.snap | 1 - ...ules__isort__tests__leading_prefix.py.snap | 1 - ...gth_sort__length_sort_from_imports.py.snap | 1 - ...ort__length_sort_non_ascii_members.py.snap | 1 - ...ort__length_sort_non_ascii_modules.py.snap | 1 - ...gth_sort_straight_and_from_imports.py.snap | 1 - ...sort__length_sort_straight_imports.py.snap | 1 - ..._length_sort_with_relative_imports.py.snap | 1 - ...straight__length_sort_from_imports.py.snap | 1 - ...gth_sort_straight_and_from_imports.py.snap | 1 - ...ight__length_sort_straight_imports.py.snap | 1 - ...es__isort__tests__line_ending_crlf.py.snap | 1 - ...ules__isort__tests__line_ending_lf.py.snap | 1 - ...isort__tests__lines_after_imports.pyi.snap | 1 - ...ts__lines_after_imports_func_after.py.snap | 1 - ...after_imports_lines_after_imports.pyi.snap | 1 - ...rts_lines_after_imports_func_after.py.snap | 1 - ..._lines_after_imports_nothing_after.py.snap | 1 - ...s_between_typeslines_between_types.py.snap | 1 - ...isort__tests__magic_trailing_comma.py.snap | 1 - ...r__rules__isort__tests__match_case.py.snap | 1 - ...rules__isort__tests__natural_order.py.snap | 1 - ..._isort__tests__no_detect_same_package.snap | 1 - ...les__isort__tests__no_lines_before.py.snap | 1 - ...no_lines_before.py_no_lines_before.py.snap | 1 - ...o_lines_before_with_empty_sections.py.snap | 1 - ...andard_library_no_standard_library.py.snap | 1 - ..._rules__isort__tests__no_wrap_star.py.snap | 1 - ...rules__isort__tests__order_by_type.py.snap | 1 - ..._order_by_type_false_order_by_type.py.snap | 1 - ..._order_by_type_with_custom_classes.py.snap | 1 - ..._order_by_type_with_custom_classes.py.snap | 1 - ...rder_by_type_with_custom_constants.py.snap | 1 - ...rder_by_type_with_custom_constants.py.snap | 1 - ...rder_by_type_with_custom_variables.py.snap | 1 - ...rder_by_type_with_custom_variables.py.snap | 1 - ...s__order_relative_imports_by_level.py.snap | 1 - ...ort__tests__preserve_comment_order.py.snap | 1 - ...isort__tests__preserve_import_star.py.snap | 1 - ...isort__tests__preserve_indentation.py.snap | 1 - ...comments_propagate_inline_comments.py.snap | 1 - ...ort__tests__reorder_within_section.py.snap | 1 - ...mport_with_useless_alias_this_this.py.snap | 1 - ..._with_useless_alias_this_this_from.py.snap | 1 - ...ort__tests__section_order_sections.py.snap | 1 - ...s__isort__tests__sections_sections.py.snap | 1 - ...ests__separate_first_party_imports.py.snap | 1 - ...rt__tests__separate_future_imports.py.snap | 1 - ...sts__separate_local_folder_imports.py.snap | 1 - ...ests__separate_third_party_imports.py.snap | 1 - ..._linter__rules__isort__tests__skip.py.snap | 3 - ...isort__tests__sort_similar_imports.py.snap | 1 - ...linter__rules__isort__tests__split.py.snap | 2 - ...railing_comma_magic_trailing_comma.py.snap | 1 - ...__isort__tests__star_before_others.py.snap | 1 - ...es__isort__tests__trailing_comment.py.snap | 1 - ...les__isort__tests__trailing_suffix.py.snap | 1 - ...__numpy-deprecated-function_NPY003.py.snap | 2 - ...numpy-deprecated-type-alias_NPY001.py.snap | 1 - ..._tests__numpy-legacy-random_NPY002.py.snap | 1 - ...tests__numpy2-deprecation_NPY201_3.py.snap | 1 - ..._rules__pandas_vet__tests__PD002_fail.snap | 1 - ..._rules__pandas_vet__tests__PD003_fail.snap | 1 - ..._rules__pandas_vet__tests__PD004_fail.snap | 1 - ..._rules__pandas_vet__tests__PD007_fail.snap | 1 - ..._rules__pandas_vet__tests__PD008_fail.snap | 1 - ..._rules__pandas_vet__tests__PD009_fail.snap | 1 - ..._pandas_vet__tests__PD011_fail_values.snap | 1 - ...__pandas_vet__tests__PD013_fail_stack.snap | 1 - ...ts__PD015_fail_merge_on_pandas_object.snap | 1 - ..._pandas_vet__tests__PD901_fail_df_var.snap | 1 - ...les__pep8_naming__tests__N803_N803.py.snap | 3 - ...les__pep8_naming__tests__N806_N806.py.snap | 1 - ...les__pep8_naming__tests__N812_N812.py.snap | 1 - ...les__pep8_naming__tests__N813_N813.py.snap | 1 - ...les__pep8_naming__tests__N817_N817.py.snap | 2 - ...case_imported_as_incorrect_convention.snap | 2 - ...ing__tests__ignore_names_N806_N806.py.snap | 1 - ...ing__tests__ignore_names_N811_N811.py.snap | 1 - ...ing__tests__ignore_names_N812_N812.py.snap | 1 - ...ing__tests__ignore_names_N813_N813.py.snap | 1 - ...ing__tests__ignore_names_N814_N814.py.snap | 1 - ...ing__tests__ignore_names_N815_N815.py.snap | 1 - ...ing__tests__ignore_names_N816_N816.py.snap | 1 - ...ing__tests__ignore_names_N817_N817.py.snap | 1 - ...__perflint__tests__PERF401_PERF401.py.snap | 18 - ...__perflint__tests__PERF402_PERF402.py.snap | 1 - ...__perflint__tests__PERF403_PERF403.py.snap | 16 - ...t__tests__preview__PERF401_PERF401.py.snap | 18 - ...t__tests__preview__PERF403_PERF403.py.snap | 16 - ...les__pycodestyle__tests__E101_E101.py.snap | 1 - ...ules__pycodestyle__tests__E203_E20.py.snap | 1 - ...ules__pycodestyle__tests__E231_E23.py.snap | 1 - ...ules__pycodestyle__tests__E241_E24.py.snap | 1 - ...ules__pycodestyle__tests__E262_E26.py.snap | 1 - ...tyle__tests__E301_E30_syntax_error.py.snap | 1 - ...tyle__tests__E302_E30_syntax_error.py.snap | 1 - ...ules__pycodestyle__tests__E303_E30.py.snap | 2 - ...tyle__tests__E303_E30_syntax_error.py.snap | 1 - ...tyle__tests__E305_E30_syntax_error.py.snap | 2 - ...tyle__tests__E306_E30_syntax_error.py.snap | 1 - ...ules__pycodestyle__tests__E401_E40.py.snap | 2 - ...ules__pycodestyle__tests__E402_E40.py.snap | 1 - ...s__pycodestyle__tests__E402_E402_0.py.snap | 3 - ...s__pycodestyle__tests__E402_E402_1.py.snap | 1 - ...les__pycodestyle__tests__E501_E501.py.snap | 1 - ...s__pycodestyle__tests__E501_E501_3.py.snap | 1 - ...s__pycodestyle__tests__E501_E501_4.py.snap | Bin 9132 -> 10805 bytes ...les__pycodestyle__tests__E713_E713.py.snap | 1 - ...les__pycodestyle__tests__E714_E714.py.snap | 1 - ...les__pycodestyle__tests__E731_E731.py.snap | 16 - ...les__pycodestyle__tests__E741_E741.py.snap | 1 - ...ules__pycodestyle__tests__W191_W19.py.snap | 2 - ...s__pycodestyle__tests__W292_W292_0.py.snap | 1 - ...les__pycodestyle__tests__W293_W293.py.snap | 1 - ...s__pycodestyle__tests__W605_W605_0.py.snap | 2 - ...s__pycodestyle__tests__W605_W605_1.py.snap | 4 - ...__tests__blank_lines_E304_typing_stub.snap | 1 - ...patibility-lines-after(-1)-between(0).snap | 3 - ...mpatibility-lines-after(0)-between(0).snap | 2 - ...mpatibility-lines-after(1)-between(1).snap | 2 - ...mpatibility-lines-after(4)-between(4).snap | 4 - ..._tests__blank_lines_typing_stub_isort.snap | 10 - ...s__pycodestyle__tests__max_doc_length.snap | 3 - ...yle__tests__max_doc_length_with_utf_8.snap | 3 - ...odestyle__tests__preview__E402_E40.py.snap | 1 - ...style__tests__preview__E402_E402_0.py.snap | 3 - ...style__tests__preview__E402_E402_1.py.snap | 1 - ...tests__preview__E402_E402_comments.py.snap | 1 - ...ests__preview__E402_E402_docstring.py.snap | 1 - ...__tests__preview__E402_E402_future.py.snap | 1 - ..._tests__preview__E402_E402_shebang.py.snap | 1 - ..._E402_shebang_docstring_and_future.py.snap | 1 - ...style__tests__preview__E501_E501_5.py.snap | 1 - ...tyle__tests__preview__W391_W391.ipynb.snap | 7 +- ...style__tests__preview__W391_W391_0.py.snap | 1 - ...style__tests__preview__W391_W391_2.py.snap | 3 +- ...rules__pycodestyle__tests__tab_size_1.snap | 1 - ...rules__pycodestyle__tests__tab_size_2.snap | 1 - ...rules__pycodestyle__tests__tab_size_4.snap | 1 - ...rules__pycodestyle__tests__tab_size_8.snap | 1 - ...__pycodestyle__tests__task_tags_false.snap | 1 - ...patibility-lines-after(-1)-between(0).snap | 4 - ...mpatibility-lines-after(0)-between(0).snap | 6 - ...mpatibility-lines-after(1)-between(1).snap | 5 - ...mpatibility-lines-after(4)-between(4).snap | 2 - ...er__rules__pycodestyle__tests__w292_4.snap | 1 - ...hite_space_syntax_error_compatibility.snap | 1 - ...__rules__pydocstyle__tests__D103_D.py.snap | 1 - ...__rules__pydocstyle__tests__D200_D.py.snap | 5 - ...ules__pydocstyle__tests__D200_D200.py.snap | 3 - ...__rules__pydocstyle__tests__D201_D.py.snap | 2 - ...ules__pydocstyle__tests__D202_D202.py.snap | 2 - ...__rules__pydocstyle__tests__D203_D.py.snap | 2 - ...__rules__pydocstyle__tests__D204_D.py.snap | 1 - ...__rules__pydocstyle__tests__D205_D.py.snap | 2 - ...__rules__pydocstyle__tests__D207_D.py.snap | 2 - ...__rules__pydocstyle__tests__D208_D.py.snap | 1 - ...ules__pydocstyle__tests__D208_D208.py.snap | 2 - ...__rules__pydocstyle__tests__D209_D.py.snap | 2 - ...__rules__pydocstyle__tests__D210_D.py.snap | 4 - ...__rules__pydocstyle__tests__D211_D.py.snap | 1 - ...__rules__pydocstyle__tests__D212_D.py.snap | 3 - ...__rules__pydocstyle__tests__D213_D.py.snap | 23 - ...__rules__pydocstyle__tests__D300_D.py.snap | 7 - ...ules__pydocstyle__tests__D300_D300.py.snap | 2 - ...__rules__pydocstyle__tests__D301_D.py.snap | 1 - ...ules__pydocstyle__tests__D301_D301.py.snap | 4 - ...__rules__pydocstyle__tests__D400_D.py.snap | 10 - ...ules__pydocstyle__tests__D400_D400.py.snap | 1 - ...ules__pydocstyle__tests__D401_D401.py.snap | 4 - ...__rules__pydocstyle__tests__D402_D.py.snap | 1 - ...ules__pydocstyle__tests__D402_D402.py.snap | 1 - ...ules__pydocstyle__tests__D403_D403.py.snap | 1 - ...__rules__pydocstyle__tests__D404_D.py.snap | 3 - ...__pydocstyle__tests__D407_sections.py.snap | 1 - ...es__pydocstyle__tests__D412_sphinx.py.snap | 2 - ...__pydocstyle__tests__D413_sections.py.snap | 1 - ...__pydocstyle__tests__D414_sections.py.snap | 1 - ...__rules__pydocstyle__tests__D415_D.py.snap | 9 - ...__rules__pydocstyle__tests__D419_D.py.snap | 1 - ...linter__rules__pydocstyle__tests__bom.snap | 1 - ...__rules__pydocstyle__tests__d209_d400.snap | 2 - ...ules__pyflakes__tests__F401_F401_0.py.snap | 5 - ...les__pyflakes__tests__F401_F401_11.py.snap | 1 - ...les__pyflakes__tests__F401_F401_15.py.snap | 1 - ...les__pyflakes__tests__F401_F401_18.py.snap | 1 - ...ules__pyflakes__tests__F401_F401_5.py.snap | 1 - ...ules__pyflakes__tests__F401_F401_6.py.snap | 1 - ...ules__pyflakes__tests__F401_F401_7.py.snap | 2 - ...ules__pyflakes__tests__F401_F401_9.py.snap | 1 - ...eprecated_option_F401_24____init__.py.snap | 3 - ...on_F401_25__all_nonempty____init__.py.snap | 3 - ...ption_F401_26__all_empty____init__.py.snap | 2 - ...on_F401_27__all_mistyped____init__.py.snap | 2 - ...on_F401_28__all_multiple____init__.py.snap | 2 - ...ts__F401_deprecated_option_F401_30.py.snap | 1 - ...sts__F401_stable_F401_24____init__.py.snap | 3 - ...le_F401_25__all_nonempty____init__.py.snap | 3 - ...table_F401_26__all_empty____init__.py.snap | 2 - ...le_F401_27__all_mistyped____init__.py.snap | 2 - ...le_F401_28__all_multiple____init__.py.snap | 2 - ...ules__pyflakes__tests__F404_F404_1.py.snap | 1 - ..._rules__pyflakes__tests__F405_F405.py.snap | 2 - ..._rules__pyflakes__tests__F406_F406.py.snap | 2 - ..._rules__pyflakes__tests__F407_F407.py.snap | 1 - ..._rules__pyflakes__tests__F502_F502.py.snap | 1 - ..._rules__pyflakes__tests__F504_F504.py.snap | 1 - ..._rules__pyflakes__tests__F522_F522.py.snap | 1 - ..._rules__pyflakes__tests__F523_F523.py.snap | 1 - ..._rules__pyflakes__tests__F524_F524.py.snap | 1 - ..._rules__pyflakes__tests__F525_F525.py.snap | 1 - ..._rules__pyflakes__tests__F541_F541.py.snap | 1 - ..._rules__pyflakes__tests__F601_F601.py.snap | 1 - ..._rules__pyflakes__tests__F602_F602.py.snap | 1 - ..._rules__pyflakes__tests__F632_F632.py.snap | 1 - ..._rules__pyflakes__tests__F633_F633.py.snap | 1 - ..._rules__pyflakes__tests__F701_F701.py.snap | 3 - ..._rules__pyflakes__tests__F702_F702.py.snap | 3 - ..._rules__pyflakes__tests__F704_F704.py.snap | 1 - ..._rules__pyflakes__tests__F706_F706.py.snap | 2 - ..._rules__pyflakes__tests__F722_F722.py.snap | 1 - ...ules__pyflakes__tests__F722_F722_1.py.snap | 1 - ...ules__pyflakes__tests__F811_F811_1.py.snap | 3 +- ...les__pyflakes__tests__F811_F811_12.py.snap | 2 +- ...les__pyflakes__tests__F811_F811_15.py.snap | 1 - ...les__pyflakes__tests__F811_F811_16.py.snap | 1 - ...les__pyflakes__tests__F811_F811_17.py.snap | 3 +- ...ules__pyflakes__tests__F811_F811_2.py.snap | 3 +- ...les__pyflakes__tests__F811_F811_23.py.snap | 3 +- ...les__pyflakes__tests__F811_F811_26.py.snap | 2 +- ...les__pyflakes__tests__F811_F811_28.py.snap | 2 +- ...es__pyflakes__tests__F811_F811_29.pyi.snap | 3 +- ...ules__pyflakes__tests__F811_F811_3.py.snap | 3 +- ...les__pyflakes__tests__F811_F811_30.py.snap | 8 +- ...les__pyflakes__tests__F811_F811_31.py.snap | 2 +- ...les__pyflakes__tests__F811_F811_32.py.snap | 2 +- ...les__pyflakes__tests__F811_F811_35.py.snap | 4 +- ...ules__pyflakes__tests__F811_F811_4.py.snap | 3 +- ...ules__pyflakes__tests__F811_F811_5.py.snap | 3 +- ...ules__pyflakes__tests__F811_F811_6.py.snap | 2 +- ...ules__pyflakes__tests__F811_F811_8.py.snap | 2 +- ...ules__pyflakes__tests__F821_F821_0.py.snap | 4 - ...ules__pyflakes__tests__F821_F821_1.py.snap | 4 - ...les__pyflakes__tests__F821_F821_11.py.snap | 1 - ...les__pyflakes__tests__F821_F821_12.py.snap | 1 - ...les__pyflakes__tests__F821_F821_13.py.snap | 1 - ...les__pyflakes__tests__F821_F821_17.py.snap | 1 - ...les__pyflakes__tests__F821_F821_18.py.snap | 1 - ...les__pyflakes__tests__F821_F821_19.py.snap | 1 - ...les__pyflakes__tests__F821_F821_21.py.snap | 1 - ...les__pyflakes__tests__F821_F821_28.py.snap | 1 - ...les__pyflakes__tests__F821_F821_31.py.snap | 2 - ...les__pyflakes__tests__F821_F821_34.py.snap | 1 - ...es__pyflakes__tests__F821_F821_34.pyi.snap | 1 - ...ules__pyflakes__tests__F821_F821_4.py.snap | 5 - ...ules__pyflakes__tests__F821_F821_7.py.snap | 1 - ...ules__pyflakes__tests__F821_F821_9.py.snap | 1 - ...ules__pyflakes__tests__F822_F822_0.py.snap | 1 - ...les__pyflakes__tests__F822_F822_0.pyi.snap | 1 - ...ules__pyflakes__tests__F822_F822_1.py.snap | 1 - ...les__pyflakes__tests__F822_F822_1b.py.snap | 1 - ..._rules__pyflakes__tests__F823_F823.py.snap | 1 - ...ules__pyflakes__tests__F841_F841_0.py.snap | 5 - ...ules__pyflakes__tests__F841_F841_1.py.snap | 8 - ...ules__pyflakes__tests__F841_F841_3.py.snap | 10 - ..._rules__pyflakes__tests__F842_F842.py.snap | 1 - ..._rules__pyflakes__tests__F901_F901.py.snap | 3 - ...tests__augmented_assignment_after_del.snap | 2 - ...es__pyflakes__tests__default_builtins.snap | 1 - ...shadowed_global_import_in_local_scope.snap | 2 +- ...shadowed_import_shadow_in_local_scope.snap | 2 +- ..._shadowed_local_import_in_local_scope.snap | 2 +- ...r__rules__pyflakes__tests__double_del.snap | 1 - ...pyflakes__tests__extra_typing_modules.snap | 1 - ...s__f401_allowed_unused_imports_option.snap | 1 - ...ests__f401_multiple_unused_submodules.snap | 1 - ...w_first_party_submodule_no_dunder_all.snap | 1 - ...__pyflakes__tests__f401_type_checking.snap | 1 - ...811_annotated_assignment_redefinition.snap | 4 +- ...__f821_frozendict_pre_py315_undefined.snap | 1 - ...sion_but_old_target_version_specified.snap | 1 - ...lakes__tests__f841_dummy_variable_rgx.snap | 5 - ...s__load_after_unbind_from_class_scope.snap | 1 - ...yflakes__tests__multi_statement_lines.snap | 3 - ..._tests__nested_relative_typing_module.snap | 2 - ...s__preview__F401_F401_24____init__.py.snap | 3 - ...01_F401_25__all_nonempty____init__.py.snap | 3 - ..._F401_F401_26__all_empty____init__.py.snap | 2 - ...01_F401_27__all_mistyped____init__.py.snap | 2 - ...01_F401_28__all_multiple____init__.py.snap | 2 - ...s__preview__F401_F401_33____init__.py.snap | 1 - ...akes__tests__preview__F811_F811_36.py.snap | 2 +- ...kes__tests__preview__F822___init__.py.snap | 3 - ...flakes__tests__relative_typing_module.snap | 1 - ...grep_hooks__tests__PGH004_PGH004_1.py.snap | 1 - ...ests__PLC0207_missing_maxsplit_arg.py.snap | 5 - ...__PLC0415_import_outside_top_level.py.snap | 2 - ...t__tests__PLC1802_len_as_condition.py.snap | 1 - ...int__tests__PLC2401_non_ascii_name.py.snap | 1 - ..._private_name__submodule____main__.py.snap | 1 - ...s__PLC2801_unnecessary_dunder_call.py.snap | 2 - ...002_unnecessary_direct_lambda_call.py.snap | 2 - ...lint__tests__PLE0100_yield_in_init.py.snap | 1 - ...tests__PLE0115_nonlocal_and_global.py.snap | 1 - ...__PLE0117_nonlocal_without_binding.py.snap | 3 - ...PLE0303_invalid_return_type_length.py.snap | 4 - ...__PLE0304_invalid_return_type_bool.py.snap | 1 - ..._PLE0305_invalid_return_type_index.py.snap | 4 - ...s__PLE0307_invalid_return_type_str.py.snap | 4 - ..._PLE0308_invalid_return_type_bytes.py.snap | 3 - ...__PLE0309_invalid_return_type_hash.py.snap | 3 - ..._tests__PLE0604_invalid_all_object.py.snap | 1 - ...ests__PLE0704_misplaced_bare_raise.py.snap | 1 - ..._PLE1132_repeated_keyword_argument.py.snap | 1 - ...ts__PLE1142_await_outside_async.ipynb.snap | 1 - ...tests__PLE1142_await_outside_async.py.snap | 11 - ..._tests__PLE1310_bad_str_strip_call.py.snap | 1 - ...E1700_yield_from_in_async_function.py.snap | 1 - ...sts__PLE2502_bidirectional_unicode.py.snap | 12 +- ..._tests__PLE2510_invalid_characters.py.snap | Bin 3165 -> 3200 bytes ...10_invalid_characters_syntax_error.py.snap | 4 - ..._tests__PLE2512_invalid_characters.py.snap | Bin 3724 -> 3769 bytes ..._tests__PLE2513_invalid_characters.py.snap | Bin 4029 -> 4073 bytes ..._tests__PLE2514_invalid_characters.py.snap | Bin 1616 -> 1657 bytes ..._tests__PLE2515_invalid_characters.py.snap | Bin 6520 -> 6562 bytes ...sts__PLR1708_stop_iteration_return.py.snap | 7 - ...int__tests__PLR1711_useless_return.py.snap | 6 - ...R1712_swap_with_temporary_variable.py.snap | 3 - ...R1714_repeated_equality_comparison.py.snap | 1 - ...PLR1716_boolean_chained_comparison.py.snap | 1 - ...t__tests__PLR1722_sys_exit_alias_0.py.snap | 2 - ...t__tests__PLR1722_sys_exit_alias_1.py.snap | 3 - ...__tests__PLR1722_sys_exit_alias_10.py.snap | 1 - ...__tests__PLR1722_sys_exit_alias_11.py.snap | 1 - ...__tests__PLR1722_sys_exit_alias_12.py.snap | 1 - ...__tests__PLR1722_sys_exit_alias_13.py.snap | 1 - ...__tests__PLR1722_sys_exit_alias_14.py.snap | 1 - ...__tests__PLR1722_sys_exit_alias_15.py.snap | 1 - ...__tests__PLR1722_sys_exit_alias_16.py.snap | 2 - ...t__tests__PLR1722_sys_exit_alias_2.py.snap | 2 - ...t__tests__PLR1722_sys_exit_alias_3.py.snap | 3 - ...t__tests__PLR1722_sys_exit_alias_4.py.snap | 2 - ...t__tests__PLR1722_sys_exit_alias_5.py.snap | 2 - ...t__tests__PLR1722_sys_exit_alias_6.py.snap | 1 - ...t__tests__PLR1722_sys_exit_alias_7.py.snap | 1 - ...t__tests__PLR1722_sys_exit_alias_8.py.snap | 1 - ...t__tests__PLR1722_sys_exit_alias_9.py.snap | 1 - ...nt__tests__PLR1730_if_stmt_min_max.py.snap | 5 - ...1733_unnecessary_dict_index_lookup.py.snap | 1 - ...1736_unnecessary_list_index_lookup.py.snap | 3 - ...lint__tests__PLR2044_empty_comment.py.snap | 2 - ...__PLR6104_non_augmented_assignment.py.snap | 4 - ...pylint__tests__PLW0101_unreachable.py.snap | 1 - ..._tests__PLW0108_unnecessary_lambda.py.snap | 1 - ...__PLW0129_assert_on_string_literal.py.snap | 1 - ...PLW0131_named_expr_without_context.py.snap | 2 - ...int__tests__PLW0177_nan_comparison.py.snap | 1 - ...LW0244_redefined_slots_in_subclass.py.snap | 2 - ...tests__PLW0406_import_self__module.py.snap | 1 - ...W0602_global_variable_not_assigned.py.snap | 1 - ...ts__PLW0642_self_or_cls_assignment.py.snap | 1 - ...lint__tests__PLW1501_bad_open_mode.py.snap | 1 - ...ests__PLW1507_shallow_copy_environ.py.snap | 1 - ...tests__PLW2901_redefined_loop_name.py.snap | 1 - ...int__tests__PLW3301_nested_min_max.py.snap | 1 - ...tests__conflict_with_definition_rules.snap | 1 - ...s__pylint__tests__continue_in_finally.snap | 2 - ..._import_outside_top_level_with_banned.snap | 1 - ...LW0133_useless_exception_statement.py.snap | 10 - ...ylint__tests__too_many_public_methods.snap | 1 - crates/ruff_linter/src/rules/pyupgrade/mod.rs | 1 - ...er__rules__pyupgrade__tests__UP001.py.snap | 1 - ...er__rules__pyupgrade__tests__UP003.py.snap | 1 - ...er__rules__pyupgrade__tests__UP005.py.snap | 1 - ...__rules__pyupgrade__tests__UP007_1.py.snap | 2 - ...er__rules__pyupgrade__tests__UP008.py.snap | 11 - ...__rules__pyupgrade__tests__UP010_0.py.snap | 1 - ...er__rules__pyupgrade__tests__UP012.py.snap | 2 - ...er__rules__pyupgrade__tests__UP014.py.snap | 1 - ...__rules__pyupgrade__tests__UP015_1.py.snap | 1 - ...er__rules__pyupgrade__tests__UP018.py.snap | 5 - ..._rules__pyupgrade__tests__UP018_CR.py.snap | 1 - ..._rules__pyupgrade__tests__UP018_LF.py.snap | 1 - ...__rules__pyupgrade__tests__UP024_2.py.snap | 1 - ...er__rules__pyupgrade__tests__UP025.py.snap | 1 - ...er__rules__pyupgrade__tests__UP026.py.snap | 2 - ...__rules__pyupgrade__tests__UP028_0.py.snap | 11 - ...sts__UP029_1.py_skip_required_imports.snap | 1 - ...__rules__pyupgrade__tests__UP030_0.py.snap | 1 - ...__rules__pyupgrade__tests__UP031_0.py.snap | 5 +- ...__rules__pyupgrade__tests__UP032_0.py.snap | 9 - ...__rules__pyupgrade__tests__UP032_1.py.snap | 1 - ...__rules__pyupgrade__tests__UP032_2.py.snap | 1 - ...er__rules__pyupgrade__tests__UP035.py.snap | 1 - ...__rules__pyupgrade__tests__UP037_0.py.snap | 6 - ...__rules__pyupgrade__tests__UP037_1.py.snap | 1 - ..._rules__pyupgrade__tests__UP037_2.pyi.snap | 10 - ...__rules__pyupgrade__tests__UP037_3.py.snap | 1 - ...er__rules__pyupgrade__tests__UP039.py.snap | 1 - ...er__rules__pyupgrade__tests__UP040.py.snap | 2 - ...pgrade__tests__UP040.py__preview_diff.snap | 1 - ...r__rules__pyupgrade__tests__UP040.pyi.snap | 2 - ...er__rules__pyupgrade__tests__UP042.py.snap | 4 - ...__rules__pyupgrade__tests__UP045_1.py.snap | 9 - ...__rules__pyupgrade__tests__UP046_0.py.snap | 1 - ...__rules__pyupgrade__tests__UP049_1.py.snap | 3 - ...sts__add_future_annotation_UP037_0.py.snap | 6 - ...sts__add_future_annotation_UP037_1.py.snap | 1 - ...ts__add_future_annotation_UP037_2.pyi.snap | 10 - ...rade__tests__datetime_utc_alias_py311.snap | 4 - ..._annotations_keep_runtime_typing_p310.snap | 2 - ...sts__future_annotations_pep_585_py310.snap | 2 - ...sts__future_annotations_pep_604_py310.snap | 1 - ...es__refurb__tests__FURB105_FURB105.py.snap | 1 - ...es__refurb__tests__FURB113_FURB113.py.snap | 10 - ...es__refurb__tests__FURB116_FURB116.py.snap | 1 - ...es__refurb__tests__FURB118_FURB118.py.snap | 2 - ...es__refurb__tests__FURB122_FURB122.py.snap | 18 - ...es__refurb__tests__FURB129_FURB129.py.snap | 1 - ...es__refurb__tests__FURB131_FURB131.py.snap | 8 - ...es__refurb__tests__FURB132_FURB132.py.snap | 4 - ...es__refurb__tests__FURB136_FURB136.py.snap | 1 - ...es__refurb__tests__FURB140_FURB140.py.snap | 1 - ...es__refurb__tests__FURB142_FURB142.py.snap | 2 - ...es__refurb__tests__FURB145_FURB145.py.snap | 1 - ...es__refurb__tests__FURB152_FURB152.py.snap | 1 - ...es__refurb__tests__FURB154_FURB154.py.snap | 6 - ...es__refurb__tests__FURB157_FURB157.py.snap | 1 - ...es__refurb__tests__FURB162_FURB162.py.snap | 1 - ...es__refurb__tests__FURB163_FURB163.py.snap | 2 - ...es__refurb__tests__FURB164_FURB164.py.snap | 1 - ...es__refurb__tests__FURB166_FURB166.py.snap | 1 - ...es__refurb__tests__FURB169_FURB169.py.snap | 1 - ...es__refurb__tests__FURB187_FURB187.py.snap | 3 - ...es__refurb__tests__FURB188_FURB188.py.snap | 5 - ...__refurb__tests__FURB192_FURB192_1.py.snap | 1 - ...sts__fstring_number_format_python_311.snap | 1 - ...__tests__PY315_RUF015_RUF015_py315.py.snap | 1 - ...ruff__tests__PY315_RUF017_RUF017_0.py.snap | 2 - ..._rules__ruff__tests__RUF005_RUF005.py.snap | 1 - ..._rules__ruff__tests__RUF006_RUF006.py.snap | 5 - ..._rules__ruff__tests__RUF007_RUF007.py.snap | 1 - ..._rules__ruff__tests__RUF008_RUF008.py.snap | 1 - ...__ruff__tests__RUF009_RUF009_attrs.py.snap | 5 - ..._rules__ruff__tests__RUF010_RUF010.py.snap | 1 - ..._rules__ruff__tests__RUF012_RUF012.py.snap | 1 - ...ules__ruff__tests__RUF013_RUF013_4.py.snap | 1 - ..._rules__ruff__tests__RUF015_RUF015.py.snap | 1 - ..._rules__ruff__tests__RUF016_RUF016.py.snap | 1 - ...ules__ruff__tests__RUF017_RUF017_0.py.snap | 2 - ...ules__ruff__tests__RUF017_RUF017_1.py.snap | 1 - ..._rules__ruff__tests__RUF020_RUF020.py.snap | 3 - ..._rules__ruff__tests__RUF023_RUF023.py.snap | 1 - ..._rules__ruff__tests__RUF024_RUF024.py.snap | 1 - ..._rules__ruff__tests__RUF026_RUF026.py.snap | 14 - ...ules__ruff__tests__RUF027_RUF027_0.py.snap | 9 - ..._rules__ruff__tests__RUF028_RUF028.py.snap | 1 - ..._rules__ruff__tests__RUF030_RUF030.py.snap | 1 - ..._rules__ruff__tests__RUF032_RUF032.py.snap | 1 - ..._rules__ruff__tests__RUF033_RUF033.py.snap | 10 - ..._rules__ruff__tests__RUF034_RUF034.py.snap | 1 - ..._rules__ruff__tests__RUF037_RUF037.py.snap | 11 - ..._rules__ruff__tests__RUF043_RUF043.py.snap | 3 - ..._rules__ruff__tests__RUF046_RUF046.py.snap | 3 - ...les__ruff__tests__RUF046_RUF046_CR.py.snap | 1 - ...les__ruff__tests__RUF046_RUF046_LF.py.snap | 1 - ...es__ruff__tests__RUF047_RUF047_for.py.snap | 2 - ...les__ruff__tests__RUF047_RUF047_if.py.snap | 7 - ...es__ruff__tests__RUF047_RUF047_try.py.snap | 2 - ...__ruff__tests__RUF047_RUF047_while.py.snap | 2 - ..._rules__ruff__tests__RUF050_RUF050.py.snap | 2 - ..._rules__ruff__tests__RUF051_RUF051.py.snap | 1 - ..._rules__ruff__tests__RUF053_RUF053.py.snap | 13 - ..._rules__ruff__tests__RUF056_RUF056.py.snap | 1 - ...ules__ruff__tests__RUF058_RUF058_0.py.snap | 3 - ...ules__ruff__tests__RUF059_RUF059_0.py.snap | 2 - ...ules__ruff__tests__RUF059_RUF059_1.py.snap | 6 - ...ules__ruff__tests__RUF059_RUF059_2.py.snap | 6 - ...ules__ruff__tests__RUF059_RUF059_3.py.snap | 2 - ...sts__RUF061_RUF061_deprecated_call.py.snap | 2 - ..._ruff__tests__RUF061_RUF061_raises.py.snap | 5 - ...__ruff__tests__RUF061_RUF061_warns.py.snap | 2 - ...ules__ruff__tests__RUF065_RUF065_0.py.snap | 2 - ...ules__ruff__tests__RUF065_RUF065_1.py.snap | 1 - ...__RUF067_RUF067__modules____init__.py.snap | 1 - ..._rules__ruff__tests__RUF068_RUF068.py.snap | 24 +- ...ules__ruff__tests__RUF101_RUF101_0.py.snap | 2 - ..._rules__ruff__tests__RUF102_RUF102.py.snap | 1 - ...er__rules__ruff__tests__RUF200_bleach.snap | 1 - ...s__ruff__tests__RUF200_invalid_author.snap | 1 - ..._tests__add_future_import_RUF013_4.py.snap | 1 - ...confusables_deferred_annotations_diff.snap | 1 - ...e_code_external_rules_ruff__RUF102.py.snap | 1 - ...issing_fstring_syntax_backslash_py311.snap | 2 - ...ruff_linter__rules__ruff__tests__noqa.snap | 2 - ...uff__tests__preview__RUF008_RUF008.py.snap | 1 - ...uff__tests__preview__RUF039_RUF039.py.snap | 2 - ...sts__preview__RUF039_RUF039_concat.py.snap | 4 - ...uff__tests__preview__RUF054_RUF054.py.snap | 10 +- ...f__tests__preview__RUF055_RUF055_0.py.snap | 1 - ...f__tests__preview__RUF055_RUF055_1.py.snap | 1 - ...f__tests__preview__RUF055_RUF055_2.py.snap | 6 - ...uff__tests__preview__RUF069_RUF069.py.snap | 2 - ...uff__tests__preview__RUF070_RUF070.py.snap | 1 - ...uff__tests__preview__RUF072_RUF072.py.snap | 6 +- ...RUF039_RUF039_py_version_sensitive.py.snap | 1 - ...RUF039_RUF039_py_version_sensitive.py.snap | 1 - ...uff__tests__py314__RUF058_RUF058_2.py.snap | 1 - ...ules__ruff__tests__range_suppressions.snap | 7 - ..._linter__rules__ruff__tests__ruf100_0.snap | 4 - ...__rules__ruff__tests__ruf100_0_prefix.snap | 4 - ..._linter__rules__ruff__tests__ruf100_1.snap | 3 - ..._linter__rules__ruff__tests__ruf100_2.snap | 1 - ..._linter__rules__ruff__tests__ruf100_3.snap | 1 - ..._linter__rules__ruff__tests__ruf100_5.snap | 1 - ...__rules__ruff__tests__ruff_noqa_codes.snap | 1 - ...tests__ruff_noqa_filedirective_unused.snap | 1 - ...oqa_filedirective_unused_last_of_many.snap | 1 - ...rules__ruff__tests__ruff_noqa_invalid.snap | 2 - ...s__strictly_empty_init_modules_ruf067.snap | 3 - ...sts__unnecessary_if_and_needless_else.snap | 1 - ...ts__useless_finally_and_needless_else.snap | 1 - ...dless_else_and_suppressible_exception.snap | 1 - ...ss_finally_and_suppressible_exception.snap | 1 - ..._error-instead-of-exception_TRY400.py.snap | 6 - ...__tests__raise-vanilla-args_TRY003.py.snap | 3 - ..._tests__raise-vanilla-class_TRY002.py.snap | 2 - ...pe-check-without-type-error_TRY004.py.snap | 34 - ..._tests__verbose-log-message_TRY401.py.snap | 12 - ...atops__tests__verbose-raise_TRY201.py.snap | 3 - ...linter__tests__async_comprehension.py.snap | 1 - ...n_in_sync_comprehension_notebook_3.10.snap | 1 - ...__linter__tests__await_scope_notebook.snap | 1 - ...linter__linter__tests__import_sorting.snap | 2 - ...er__linter__tests__ipy_escape_command.snap | 1 - ..._linter__tests__late_future_import.py.snap | 1 - ...linter__tests__return_in_generator.py.snap | 1 - ...er__tests__return_outside_function.py.snap | 3 - ...syntax_error_annotated_global.py_3.14.snap | 1 - ...rror_duplicate_type_parameter.py_3.12.snap | 1 - ...error_invalid_star_expression.py_3.10.snap | 1 - ...x_error_rebound_comprehension.py_3.10.snap | 1 - ...ror_single_starred_assignment.py_3.10.snap | 1 - ...linter__linter__tests__undefined_name.snap | 1 - ...inter__linter__tests__unused_variable.snap | 2 - ...ests__yield_from_in_async_function.py.snap | 1 - ...linter__linter__tests__yield_scope.py.snap | 3 - crates/ruff_python_parser/tests/fixtures.rs | 8 +- ...ann_assign_stmt_invalid_annotation.py.snap | 1 - ...tax@ann_assign_stmt_invalid_target.py.snap | 1 - ...ntax@ann_assign_stmt_invalid_value.py.snap | 1 - ...syntax@ann_assign_stmt_missing_rhs.py.snap | 1 - ..._assign_stmt_type_alias_annotation.py.snap | 1 - ...tax@args_unparenthesized_generator.py.snap | 1 - .../invalid_syntax@assert_empty_msg.py.snap | 1 - .../invalid_syntax@assert_empty_test.py.snap | 1 - ...lid_syntax@assert_invalid_msg_expr.py.snap | 1 - ...id_syntax@assert_invalid_test_expr.py.snap | 1 - ..._syntax@assign_stmt_invalid_target.py.snap | 2 - ...tax@assign_stmt_invalid_value_expr.py.snap | 1 - ...tax@assign_stmt_starred_expr_value.py.snap | 1 - ...tax@aug_assign_stmt_invalid_target.py.snap | 1 - ...ntax@aug_assign_stmt_invalid_value.py.snap | 1 - ...ash_continuation_indentation_error.py.snap | 1 - ..._syntax@case_expect_indented_block.py.snap | 1 - ...nvalid_syntax@class_def_empty_body.py.snap | 1 - ...alid_syntax@class_def_missing_name.py.snap | 1 - ...unparenthesized_generator_argument.py.snap | 1 - ...lid_syntax@class_type_params_py311.py.snap | 2 - ...yntax@clause_expect_indented_block.py.snap | 1 - ...tax@clause_expect_single_statement.py.snap | 1 - ...ntax@comma_separated_missing_comma.py.snap | 2 - ...ted_missing_comma_between_elements.py.snap | 1 - ...ted_missing_element_between_commas.py.snap | 1 - ...ma_separated_missing_first_element.py.snap | 1 - ...prehension_missing_for_after_async.py.snap | 1 - .../invalid_syntax@debug_shadow_class.py.snap | 1 - ...valid_syntax@debug_shadow_function.py.snap | 1 - ...invalid_syntax@debug_shadow_import.py.snap | 1 - .../invalid_syntax@debug_shadow_match.py.snap | 1 - .../invalid_syntax@debug_shadow_try.py.snap | 1 - ...lid_syntax@debug_shadow_type_alias.py.snap | 1 - .../invalid_syntax@debug_shadow_with.py.snap | 1 - ...yntax@decorator_missing_expression.py.snap | 1 - ...d_syntax@decorator_missing_newline.py.snap | 1 - ..._syntax@decorator_unexpected_token.py.snap | 1 - .../invalid_syntax@del_debug_py39.py.snap | 1 - ...valid_syntax@del_incomplete_target.py.snap | 1 - .../invalid_syntax@del_stmt_empty.py.snap | 1 - ...x@different_match_pattern_bindings.py.snap | 1 - ...d_syntax@dotted_name_multiple_dots.py.snap | 2 - ..._syntax@duplicate_match_class_attr.py.snap | 2 - ...tax@duplicate_type_parameter_names.py.snap | 1 - .../invalid_syntax@except_star_py310.py.snap | 1 - ...essions__arguments__double_starred.py.snap | 2 - ...ments__duplicate_keyword_arguments.py.snap | 2 - ...ons__arguments__invalid_expression.py.snap | 1 - ...uments__invalid_keyword_expression.py.snap | 1 - ...ressions__arguments__invalid_order.py.snap | 1 - ...sions__arguments__missing_argument.py.snap | 1 - ...ressions__arguments__missing_comma.py.snap | 1 - ...ax@expressions__arguments__starred.py.snap | 1 - ...essions__attribute__invalid_member.py.snap | 1 - ...ressions__attribute__multiple_dots.py.snap | 2 - ...@expressions__attribute__no_member.py.snap | 1 - ...syntax@expressions__await__recover.py.snap | 1 - ...ns__bin_op__invalid_rhs_expression.py.snap | 1 - ...ressions__bin_op__named_expression.py.snap | 1 - ...ssions__bin_op__starred_expression.py.snap | 1 - ...s__bool_op__invalid_rhs_expression.py.snap | 1 - ...@expressions__bool_op__missing_lhs.py.snap | 1 - ...essions__bool_op__named_expression.py.snap | 1 - ...sions__bool_op__starred_expression.py.snap | 1 - ...xpressions__compare__invalid_order.py.snap | 3 - ...s__compare__invalid_rhs_expression.py.snap | 1 - ...ressions__compare__multiple_equals.py.snap | 2 - ...essions__compare__named_expression.py.snap | 1 - ...sions__compare__starred_expression.py.snap | 1 - ...x@expressions__dict__comprehension.py.snap | 1 - ...tax@expressions__dict__double_star.py.snap | 1 - ...ons__dict__missing_closing_brace_0.py.snap | 2 - ...ons__dict__missing_closing_brace_1.py.snap | 1 - ..._syntax@expressions__dict__recover.py.snap | 2 - ...tax@expressions__emoji_identifiers.py.snap | 3 - ...yntax@expressions__emoji_statement.py.snap | 1 - ...id_syntax@expressions__if__recover.py.snap | 1 - ...essions__lambda_default_parameters.py.snap | 1 - ...sions__lambda_duplicate_parameters.py.snap | 2 - ...x@expressions__list__comprehension.py.snap | 1 - ...s__list__missing_closing_bracket_0.py.snap | 1 - ...s__list__missing_closing_bracket_1.py.snap | 1 - ...s__list__missing_closing_bracket_2.py.snap | 1 - ..._syntax@expressions__list__recover.py.snap | 1 - ...__list__star_expression_precedence.py.snap | 1 - ...expressions__named__invalid_target.py.snap | 1 - ...sions__named__missing_expression_0.py.snap | 2 - ...sions__named__missing_expression_1.py.snap | 1 - ...sions__named__missing_expression_2.py.snap | 2 - ...sions__named__missing_expression_3.py.snap | 1 - ...ressions__parenthesized__generator.py.snap | 3 - ...nthesized__missing_closing_paren_0.py.snap | 1 - ...nthesized__missing_closing_paren_1.py.snap | 1 - ...nthesized__missing_closing_paren_2.py.snap | 1 - ...ions__parenthesized__parenthesized.py.snap | 1 - ...@expressions__parenthesized__tuple.py.snap | 1 - ..._parenthesized__tuple_starred_expr.py.snap | 4 - ...ax@expressions__set__comprehension.py.snap | 1 - ...set__missing_closing_curly_brace_0.py.snap | 1 - ...set__missing_closing_curly_brace_1.py.snap | 1 - ...set__missing_closing_curly_brace_2.py.snap | 1 - ...d_syntax@expressions__set__recover.py.snap | 1 - ...s__set__star_expression_precedence.py.snap | 1 - ...__subscript__invalid_slice_element.py.snap | 1 - ...sions__subscript__unclosed_slice_0.py.snap | 1 - ...sions__subscript__unclosed_slice_1.py.snap | 2 - .../invalid_syntax@expressions__unary.py.snap | 1 - ...pressions__unary__named_expression.py.snap | 1 - ...pressions__yield__named_expression.py.snap | 1 - ...xpressions__yield__star_expression.py.snap | 1 - ...ns__yield_from__starred_expression.py.snap | 1 - ...sions__yield_from__unparenthesized.py.snap | 1 - ...ing_conversion_follows_exclamation.py.snap | 2 - ...d_syntax@f_string_empty_expression.py.snap | 1 - ...g_invalid_conversion_flag_name_tok.py.snap | 1 - ..._invalid_conversion_flag_other_tok.py.snap | 1 - ...ntax@f_string_invalid_starred_expr.py.snap | 1 - ..._string_lambda_without_parentheses.py.snap | 4 - ...id_syntax@f_string_unclosed_lbrace.py.snap | 1 - ...ing_unclosed_lbrace_in_format_spec.py.snap | 1 - ...nvalid_syntax@for_iter_unpack_py38.py.snap | 2 - ..._syntax@for_stmt_invalid_iter_expr.py.snap | 2 - ...lid_syntax@for_stmt_invalid_target.py.snap | 2 - ...or_stmt_invalid_target_binary_expr.py.snap | 1 - ...for_stmt_invalid_target_in_keyword.py.snap | 1 - ...syntax@for_stmt_missing_in_keyword.py.snap | 1 - ...lid_syntax@for_stmt_missing_target.py.snap | 1 - ...id_syntax@from_import_dotted_names.py.snap | 2 - ...lid_syntax@from_import_empty_names.py.snap | 3 - ..._syntax@from_import_missing_module.py.snap | 1 - ...tax@from_import_parenthesized_star.py.snap | 1 - ...@from_import_star_with_other_names.py.snap | 1 - ...ort_unparenthesized_trailing_comma.py.snap | 1 - ...lid_syntax@function_def_empty_body.py.snap | 1 - ...x@function_def_invalid_return_expr.py.snap | 2 - ...ax@function_def_missing_identifier.py.snap | 1 - ...x@function_def_missing_return_type.py.snap | 1 - ...nction_def_unclosed_parameter_list.py.snap | 2 - ...n_def_unparenthesized_return_types.py.snap | 1 - ..._syntax@function_type_params_py311.py.snap | 2 - .../invalid_syntax@global_stmt_empty.py.snap | 1 - ...alid_syntax@global_stmt_expression.py.snap | 1 - ..._syntax@global_stmt_trailing_comma.py.snap | 1 - .../invalid_syntax@if_stmt_empty_body.py.snap | 1 - ...d_syntax@if_stmt_invalid_test_expr.py.snap | 1 - ...nvalid_syntax@if_stmt_missing_test.py.snap | 1 - ...lid_syntax@if_stmt_misspelled_elif.py.snap | 1 - ...syntax@import_alias_missing_asname.py.snap | 1 - .../invalid_syntax@import_from_star.py.snap | 2 - .../invalid_syntax@import_stmt_empty.py.snap | 1 - ...ax@import_stmt_parenthesized_names.py.snap | 1 - ...lid_syntax@import_stmt_star_import.py.snap | 3 - ..._syntax@import_stmt_trailing_comma.py.snap | 1 - ..._attribute_before_for_in_delimiter.py.snap | 1 - ...id_syntax@invalid_annotation_class.py.snap | 1 - ...syntax@invalid_annotation_function.py.snap | 1 - ...@invalid_annotation_function_py314.py.snap | 1 - ...id_syntax@invalid_annotation_py314.py.snap | 1 - ...ntax@invalid_annotation_type_alias.py.snap | 1 - ...nvalid_syntax@invalid_byte_literal.py.snap | 1 - .../invalid_syntax@invalid_del_target.py.snap | 6 - ...ax@invalid_fstring_literal_element.py.snap | 1 - ...alid_syntax@invalid_future_feature.py.snap | 2 - ...alid_syntax@invalid_string_literal.py.snap | 1 - ...lp_escape_command_error_recovery_1.py.snap | 1 - ...lp_escape_command_error_recovery_2.py.snap | 2 - ...lid_syntax@iter_unpack_return_py37.py.snap | 1 - ...alid_syntax@iter_unpack_yield_py37.py.snap | 1 - ...ntax@lambda_body_with_starred_expr.py.snap | 1 - ...syntax@lambda_body_with_yield_expr.py.snap | 1 - ...@lazy_import_invalid_context_py315.py.snap | 1 - ...tax@lazy_import_invalid_from_py315.py.snap | 2 - ...alid_syntax@lazy_import_stmt_py314.py.snap | 1 - ...x@match_stmt_expect_indented_block.py.snap | 2 - ...tax@match_stmt_expected_case_block.py.snap | 3 +- ...ntax@match_stmt_invalid_guard_expr.py.snap | 1 - ...ntax@match_stmt_missing_guard_expr.py.snap | 1 - ..._syntax@match_stmt_missing_pattern.py.snap | 1 - ...@match_stmt_no_newline_before_case.py.snap | 2 - ...mixed_bytes_and_non_bytes_literals.py.snap | 1 - ...ultiple_assignment_in_case_pattern.py.snap | 1 - ...ntax@multiple_clauses_on_same_line.py.snap | 3 - ...multiple_starred_assignment_target.py.snap | 2 - ..._starred_names_in_sequence_pattern.py.snap | 2 - .../invalid_syntax@named_expr_slice.py.snap | 3 - ...yntax@named_expr_slice_parse_error.py.snap | 1 - ...x@nested_async_comprehension_py310.py.snap | 1 - ...@nested_quote_in_format_spec_py312.py.snap | 1 - ...nvalid_syntax@node_range_with_gaps.py.snap | 2 - ...nlocal_declaration_at_module_level.py.snap | 1 - ...invalid_syntax@nonlocal_stmt_empty.py.snap | 1 - ...id_syntax@nonlocal_stmt_expression.py.snap | 1 - ...yntax@nonlocal_stmt_trailing_comma.py.snap | 1 - ...id_syntax@param_missing_annotation.py.snap | 1 - ...valid_syntax@param_missing_default.py.snap | 1 - ...ntax@param_with_invalid_annotation.py.snap | 1 - ..._syntax@param_with_invalid_default.py.snap | 1 - ...x@param_with_star_annotation_py310.py.snap | 1 - ...alid_syntax@params_duplicate_names.py.snap | 5 - ...rams_expected_after_star_separator.py.snap | 1 - ...x@params_follows_var_keyword_param.py.snap | 5 - ...@params_kwarg_after_star_separator.py.snap | 1 - ...alid_syntax@params_multiple_kwargs.py.snap | 1 - ...ax@params_multiple_slash_separator.py.snap | 1 - ...tax@params_multiple_star_separator.py.snap | 1 - ...lid_syntax@params_multiple_varargs.py.snap | 1 - ..._syntax@params_no_arg_before_slash.py.snap | 1 - ...x@params_non_default_after_default.py.snap | 2 - ...lid_syntax@params_star_after_slash.py.snap | 1 - ...ms_star_separator_after_star_param.py.snap | 1 - ...ax@params_var_keyword_with_default.py.snap | 4 - ...params_var_positional_with_default.py.snap | 4 - ...parenthesized_context_manager_py38.py.snap | 1 - ...id_syntax@parenthesized_kwarg_py38.py.snap | 1 - ...valid_syntax@pep701_f_string_py311.py.snap | 4 - ...@pep701_nested_interpolation_py311.py.snap | 1 - ...ict_unpacking_comprehensions_py315.py.snap | 1 - ...798_unpacking_comprehensions_py314.py.snap | 1 - .../invalid_syntax@pos_only_py37.py.snap | 1 - ...syntax@raise_stmt_from_without_exc.py.snap | 1 - ...id_syntax@raise_stmt_invalid_cause.py.snap | 1 - ...alid_syntax@raise_stmt_invalid_exc.py.snap | 1 - ...e_stmt_unparenthesized_tuple_cause.py.snap | 1 - ...ise_stmt_unparenthesized_tuple_exc.py.snap | 1 - ...nvalid_syntax@re_lex_logical_token.py.snap | 1 - ...yntax@re_lex_logical_token_mac_eol.py.snap | 8 +- ...x@re_lexing__fstring_format_spec_1.py.snap | 4 - ...re_lexing__triple_quoted_fstring_1.py.snap | 4 - ...re_lexing__triple_quoted_fstring_2.py.snap | 1 - .../invalid_syntax@re_lexing__ty_1828.py.snap | 3 - ...tax@rebound_comprehension_variable.py.snap | 2 - ...id_syntax@return_stmt_invalid_expr.py.snap | 2 - ...ple_and_compound_stmt_on_same_line.py.snap | 1 - ...ompound_stmt_on_same_line_in_block.py.snap | 1 - ...d_syntax@simple_stmts_on_same_line.py.snap | 2 - ...simple_stmts_on_same_line_in_block.py.snap | 2 - .../invalid_syntax@single_star_for.py.snap | 1 - .../invalid_syntax@single_star_return.py.snap | 1 - .../invalid_syntax@single_star_yield.py.snap | 1 - ...x@single_starred_assignment_target.py.snap | 1 - .../invalid_syntax@star_index_py310.py.snap | 1 - .../invalid_syntax@star_slices.py.snap | 2 - ...yntax@starred_comprehension_target.py.snap | 1 - ...lid_syntax@starred_list_comp_py314.py.snap | 1 - ..._syntax@starred_starred_expression.py.snap | 1 - ...atements__function_type_parameters.py.snap | 4 - ...ents__if_extra_closing_parentheses.py.snap | 1 - ...syntax@statements__if_extra_indent.py.snap | 1 - ...ements__invalid_assignment_targets.py.snap | 1 - ...nvalid_augmented_assignment_target.py.snap | 1 - ...ax@statements__match__as_pattern_2.py.snap | 2 - ...ax@statements__match__as_pattern_3.py.snap | 2 - ...ts__match__invalid_mapping_pattern.py.snap | 1 - ...s__with__ambiguous_lpar_with_items.py.snap | 1 - ...nts__with__unclosed_ambiguous_lpar.py.snap | 1 - ..._with__unclosed_ambiguous_lpar_eof.py.snap | 1 - ...__with__unparenthesized_with_items.py.snap | 1 - ...d_syntax@t_string_empty_expression.py.snap | 1 - ...g_invalid_conversion_flag_name_tok.py.snap | 1 - ..._invalid_conversion_flag_other_tok.py.snap | 1 - ...ntax@t_string_invalid_starred_expr.py.snap | 1 - ..._string_lambda_without_parentheses.py.snap | 4 - ...id_syntax@t_string_unclosed_lbrace.py.snap | 1 - ...ing_unclosed_lbrace_in_format_spec.py.snap | 1 - ...alid_syntax@template_strings_py313.py.snap | 1 - ...alid_syntax@try_stmt_invalid_order.py.snap | 1 - ...ax@try_stmt_missing_except_finally.py.snap | 1 - ..._syntax@try_stmt_mixed_except_kind.py.snap | 1 - ..._syntax@tuple_context_manager_py38.py.snap | 1 - ..._syntax@type_alias_incomplete_stmt.py.snap | 1 - ...ntax@type_alias_invalid_value_expr.py.snap | 1 - ...id_syntax@type_param_default_py312.py.snap | 2 - ...ntax@type_param_invalid_bound_expr.py.snap | 1 - ...id_syntax@type_param_missing_bound.py.snap | 1 - ...syntax@type_param_param_spec_bound.py.snap | 3 - ...am_param_spec_invalid_default_expr.py.snap | 1 - ...e_param_param_spec_missing_default.py.snap | 1 - ...aram_type_var_invalid_default_expr.py.snap | 1 - ...ype_param_type_var_missing_default.py.snap | 1 - ...ax@type_param_type_var_tuple_bound.py.snap | 3 - ...ype_var_tuple_invalid_default_expr.py.snap | 2 - ...ram_type_var_tuple_missing_default.py.snap | 1 - ...yntax@type_parameter_default_order.py.snap | 1 - .../invalid_syntax@type_params_empty.py.snap | 1 - .../invalid_syntax@type_stmt_py311.py.snap | 1 - ...arenthesized_named_expr_index_py38.py.snap | 1 - ...nthesized_named_expr_set_comp_py38.py.snap | 1 - ...esized_named_expr_set_literal_py38.py.snap | 1 - .../invalid_syntax@walrus_py37.py.snap | 1 - ...yntax@while_stmt_invalid_test_expr.py.snap | 1 - ..._items_parenthesized_missing_comma.py.snap | 1 - ...invalid_syntax@write_to_debug_expr.py.snap | 1 - .../invalid_syntax@yield_after_comma.py.snap | 1 - ...yntax@yield_from_in_async_function.py.snap | 1 - crates/ty/tests/cli/analysis_options.rs | 5 - crates/ty/tests/cli/config_option.rs | 7 - crates/ty/tests/cli/exit_code.rs | 11 - crates/ty/tests/cli/file_selection.rs | 42 - crates/ty/tests/cli/fixes.rs | 10 +- crates/ty/tests/cli/main.rs | 19 +- crates/ty/tests/cli/python_environment.rs | 70 - crates/ty/tests/cli/rule_selection.rs | 42 +- crates/ty/tests/cli/scripts.rs | 12 +- crates/ty_ide/src/all_symbols.rs | 22 - crates/ty_ide/src/call_hierarchy.rs | 10 - .../src/call_hierarchy/incoming_calls.rs | 40 - .../src/call_hierarchy/outgoing_calls.rs | 36 - crates/ty_ide/src/code_action.rs | 36 - crates/ty_ide/src/doc_highlights.rs | 15 - crates/ty_ide/src/document_symbols.rs | 23 - crates/ty_ide/src/find_references.rs | 67 - crates/ty_ide/src/folding_range.rs | 123 - crates/ty_ide/src/goto_declaration.rs | 172 +- crates/ty_ide/src/goto_definition.rs | 142 - crates/ty_ide/src/goto_implementation.rs | 94 - crates/ty_ide/src/goto_type_definition.rs | 150 +- crates/ty_ide/src/hover.rs | 172 +- crates/ty_ide/src/inlay_hints.rs | 624 -- crates/ty_ide/src/rename.rs | 81 +- crates/ty_ide/src/selection_range.rs | 34 - ...tests__add_ignore_trailing_whitespace.snap | 1 - ...over_subscript_literal_index_variants.snap | 9 - ...ts__hover_subscript_non_literal_index.snap | 1 - ...pt_slice_literal_bounds_list_variants.snap | 6 - ..._slice_literal_bounds_string_variants.snap | 3 - crates/ty_ide/src/workspace_symbols.rs | 6 - .../resources/mdtest/annotations/any.md | 3 +- .../mdtest/annotations/literal_string.md | 2 - .../resources/mdtest/annotations/new_types.md | 5 - .../resources/mdtest/annotations/string.md | 19 +- .../mdtest/assignment/annotations.md | 3 +- .../resources/mdtest/assignment/augmented.md | 1 - .../resources/mdtest/attributes.md | 19 - .../resources/mdtest/binary/custom.md | 4 - .../resources/mdtest/binary/instances.md | 1 - .../resources/mdtest/call/abstract_method.md | 3 +- .../resources/mdtest/call/builtins.md | 2 - .../resources/mdtest/call/function.md | 4 - .../resources/mdtest/call/methods.md | 11 +- .../resources/mdtest/call/overloads.md | 2 - .../resources/mdtest/call/type.md | 7 - .../resources/mdtest/call/union.md | 6 - .../comparison/instances/membership_test.md | 2 - .../comparison/instances/rich_comparison.md | 2 - .../mdtest/comparison/intersections.md | 2 - .../resources/mdtest/comparison/unions.md | 5 - .../mdtest/comparison/unsupported.md | 7 - .../mdtest/dataclasses/dataclasses.md | 8 +- .../resources/mdtest/del.md | 5 - .../resources/mdtest/descriptor_protocol.md | 3 +- .../diagnostics/attribute_assignment.md | 23 +- .../mdtest/diagnostics/error_context.md | 196 +- .../diagnostics/invalid_argument_type.md | 48 - .../invalid_assignment_syntactic_variants.md | 18 +- .../mdtest/diagnostics/missing_argument.md | 8 - .../diagnostics/semantic_syntax_errors.md | 13 - .../resources/mdtest/diagnostics/shadowing.md | 7 +- .../diagnostics/too_many_positionals.md | 6 - .../resources/mdtest/diagnostics/unpacking.md | 4 - .../mdtest/directives/assert_never.md | 7 - .../mdtest/directives/assert_type.md | 5 - .../resources/mdtest/directives/cast.md | 5 - .../resources/mdtest/enums.md | 2 - .../mdtest/expression/yield_and_yield_from.md | 9 +- .../mdtest/generics/legacy/classes.md | 3 +- .../mdtest/generics/legacy/functions.md | 4 - .../mdtest/generics/legacy/variables.md | 1 - .../mdtest/generics/legacy/variance.md | 4 - .../mdtest/generics/pep695/aliases.md | 14 +- .../mdtest/generics/pep695/functions.md | 4 - .../resources/mdtest/generics/scoping.md | 1 - .../resources/mdtest/implicit_type_aliases.md | 3 - .../resources/mdtest/liskov.md | 62 +- .../resources/mdtest/loops/async_for.md | 7 - .../resources/mdtest/loops/for.md | 24 - .../resources/mdtest/metaclass.md | 1 - .../resources/mdtest/narrow/isinstance.md | 6 - .../resources/mdtest/narrow/issubclass.md | 4 - .../resources/mdtest/notebook.md | 1 - .../resources/mdtest/overloads.md | 3 - .../resources/mdtest/override.md | 6 +- .../paramspec_subcall_error_location.md | 15 - .../resources/mdtest/pep613_type_aliases.md | 1 - .../resources/mdtest/pep695_type_aliases.md | 1 - .../resources/mdtest/properties.md | 3 - .../resources/mdtest/protocols.md | 5 +- .../3525_character_split_notebook.md | 1 - ...ript_\342\200\246_(98082f2161ea366f).snap" | 2 - ...pe_-_For_a_`dict`_(4aa9d1d82d07fcf1).snap" | 1 - ...ype_-_For_a_`list`_(752cfa73fb34c1c).snap" | 1 - ...e_for\342\200\246_(815dae276e2fd2b7).snap" | 1 - ...pe_-_For_a_`dict`_(177872afa1956fef).snap" | 1 - ...pe_-_For_a_`list`_(e7ebbd4af387837c).snap" | 1 - ...ype_f\342\200\246_(155d53762388f9ad).snap" | 4 +- ...for_`\342\200\246_(7cf0fa634e2a2d59).snap" | 3 +- ...`_met\342\200\246_(468f62a3bdd1d60c).snap" | 1 - ...g_`__\342\200\246_(efd3f0c02e9b89e9).snap" | 1 - ..._all_\342\200\246_(8a0f0e8ceccc51b2).snap" | 6 +- ..._one_\342\200\246_(b515711c0a451a86).snap" | 3 +- ...e_for\342\200\246_(57372b65e30392a8).snap" | 1 - ...e_for\342\200\246_(ffe39a3bae68cfe4).snap" | 2 - ...ion_w\342\200\246_(28ef812089a32e6a).snap" | 1 - ...on_ha\342\200\246_(d394c561bdd35078).snap" | 6 - ...iagno\342\200\246_(a97274530a7f61c1).snap" | 4 - ...mport\342\200\246_(2fcfcf567587a056).snap" | 3 - ...mport\342\200\246_(c14954eefd15211f).snap" | 2 - ...mport\342\200\246_(dba22bd97137ee38).snap" | 4 - ...s_imp\342\200\246_(cbfbf5ff94e6e104).snap" | 1 - ...dule_\342\200\246_(846453deaca1071c).snap" | 1 - ...bmodu\342\200\246_(4fad4be9778578b7).snap" | 2 - ..._bad_\342\200\246_(2ceba7b720e21b8b).snap" | 2 - ...nsist\342\200\246_(557742f3cd2464b2).snap" | 17 - ...neric\342\200\246_(5a066394f338af48).snap" | 8 - ...aramet\342\200\246_(6bb09b09c131074).snap" | 24 +- ..._bad_\342\200\246_(cf706b07cf0ec31f).snap" | 2 - ...o_back-references_(9051beb16a623d36).snap" | 5 - ...ust_b\342\200\246_(dc429fc3e8c18eaf).snap" | 6 - ...ted_`Concatenate`_(86093b62e6e6874c).snap" | 3 - ...otatio\342\200\246_(bb5fe70ded875e4).snap" | 8 - ...Too_few_arguments_(efcf77cdbde3ff86).snap" | 17 - ...46_-_Introduction_(cff2724f4c9d28c4).snap" | 4 - ...\200\246_-_Syntax_(142fa2948c3c6cf1).snap" | 9 - ..._in_g\342\200\246_(6d8b024dda7ced11).snap" | 3 +- ...sic_case_with_ABC_(21e412599c45972a).snap" | 3 +- ..._ther\342\200\246_(ecae0f4510696c95).snap" | 5 +- ..._ther\342\200\246_(f807ff3716d8ab0d).snap" | 5 +- ...ct_me\342\200\246_(feafee9a4abbe8d1).snap" | 6 +- ...mplic\342\200\246_(e373f31c7a7d88e7).snap" | 64 +- ...fined\342\200\246_(fc7b496fd1986deb).snap" | 16 - ..._a_me\342\200\246_(338615109711a91b).snap" | 25 - ..._case\342\200\246_(2389d52c5ecfa2bd).snap" | 2 - ...`@fin\342\200\246_(9863b583f4c651c5).snap" | 4 - ...ods_d\342\200\246_(861757f48340ed92).snap" | 36 +- ...atica\342\200\246_(29a698d9deaf7318).snap" | 2 - ...final\342\200\246_(c004aaab38745318).snap" | 2 - ...ion_f\342\200\246_(ee99fadd6476677e).snap" | 24 +- ..._unio\342\200\246_(5396a8f9e7f88f71).snap" | 8 - ...mplic\342\200\246_(4c3d127986a58f11).snap" | 14 - ...compa\342\200\246_(98b54233987eb654).snap" | 2 - ...are_o\342\200\246_(58a3839a9bc7026d).snap" | 6 +- ..._set-\342\200\246_(15737b0beb194b0e).snap" | 2 - ...ed_wh\342\200\246_(ba5cb09eaa3715d8).snap" | 4 - ...used_\342\200\246_(652fec4fd4a6c63a).snap" | 2 - ...iagno\342\200\246_(a4b698196d337a3f).snap" | 2 - ...sed_w\342\200\246_(f61204fc81905069).snap" | 6 - ...d_exp\342\200\246_(3fbab22ead236138).snap" | 6 - ...2\200\246_-_Basic_(f15db7dc447d0795).snap" | 1 - ...h_mis\342\200\246_(9ce1ee3cd1c9c8d1).snap" | 1 - ...th_pos\342\200\246_(a028edbafe180ca).snap" | 1 - ...eturn\342\200\246_(fedf62ffaca0f2d7).snap" | 1 - ..._awai\342\200\246_(9db6c457a98cde25).snap" | 1 - ..._awai\342\200\246_(d78580fb6720e4ea).snap" | 5 +- ...re_one\342\200\246_(ef7c2c0c8d9b1f0).snap" | 1 - ...initi\342\200\246_(15b05c126b6ae968).snap" | 1 - ...initi\342\200\246_(ccb69f512135dd61).snap" | 1 - ...f_Leg\342\200\246_(eaa359e8d6b3031d).snap" | 6 - ...ers_m\342\200\246_(3edf97b20f58fa11).snap" | 3 - ...covar\342\200\246_(b7b0976739681470).snap" | 1 - ...h_bou\342\200\246_(4ca5f13621915554).snap" | 1 - ...y_one\342\200\246_(8b0258f5188209c6).snap" | 1 - ..._for_\342\200\246_(72827c64b5c73d05).snap" | 1 - ..._argu\342\200\246_(39164266ada3dc2f).snap" | 1 - ...y_ass\342\200\246_(c2e3e46852bb268f).snap" | 2 - ..._Must_have_a_name_(79a4ce09338e666b).snap" | 1 - ...efine\342\200\246_(b1be57970f924722).snap" | 3 +- ...iven_\342\200\246_(8f6aed0dba79e995).snap" | 1 - ...ument\342\200\246_(9d57505425233fd8).snap" | 2 - ...eter_\342\200\246_(8424f2b8bc4351f9).snap" | 1 - ...t_for\342\200\246_(b632d61c1d75f9fb).snap" | 3 - ...teral\342\200\246_(4ee237f49e7ac736).snap" | 2 - ...Os_in\342\200\246_(e2b355c09a967862).snap" | 1 - ...ludes\342\200\246_(d2532518c44112c8).snap" | 2 - ...ts_th\342\200\246_(6f8d0bf648c4b305).snap" | 5 - ...ts_wi\342\200\246_(ea7ebc83ec359b54).snap" | 7 - ...iple_\342\200\246_(f30babd05c89dce9).snap" | 3 - ...not_h\342\200\246_(e2ed186fe2b2fc35).snap" | 5 - ...uple`_-_Definition_(bbf79630502e65e9).snap | 14 +- ...ltiple_Inheritance_(82ed33d1b3b433d8).snap | 3 - ...iagno\342\200\246_(8ca723b970e370d0).snap" | 1 - ...uctor_\342\200\246_(dd9f8a8f736a329).snap" | 1 - ...ith_u\342\200\246_(31cb5f881221158e).snap" | 3 - ...get__\342\200\246_(9ecd21d0927ee1ff).snap" | 3 - ...n_wit\342\200\246_(dd80c593d9136f35).snap" | 3 - ...n_wit\342\200\246_(f66e3a8a3977c472).snap" | 3 - ...aded_\342\200\246_(3553d085684e16a0).snap" | 3 - ...aded_\342\200\246_(36814b28492c01d2).snap" | 3 - ...verloa\342\200\246_(84dadf8abd8f2f2).snap" | 6 +- ..._-_`@classmethod`_(aaa04d4cfa3adaba).snap" | 11 +- ...00\246_-_`@final`_(f8e529ec23a61665).snap" | 15 +- ...246_-_`@override`_(2df210735ca532f9).snap" | 9 +- ...-_Regular_modules_(5c8e81664d1c7470).snap" | 2 - ...orate\342\200\246_(d17a1580f99a6402).snap" | 2 - ...override`_-_Basics_(b7c220f8171f11f0).snap | 32 +- ...amSpe\342\200\246_(648be2a43987ffd8).snap" | 18 - ...not_s\342\200\246_(c9dbdc7b13b704a4).snap" | 2 - ...ramSpe\342\200\246_(327594c6dacd8ad).snap" | 17 - ...not_s\342\200\246_(8243f67799c93e3c).snap" | 5 +- ...ol_cl\342\200\246_(288988036f34ddcf).snap" | 5 - ..._auto\342\200\246_(310665856cfe2424).snap" | 4 - ..._prot\342\200\246_(585a3e9545d41b64).snap" | 8 - ...o_`ge\342\200\246_(3d0c4ee818c4d8d5).snap" | 4 - ...tterns\342\200\246_(8ae0e231033b78e).snap" | 4 - ...rotoco\342\200\246_(98257e7c2300373).snap" | 26 - ...s_in_\342\200\246_(21be5d9bdab1c844).snap" | 2 - ..._`emp\342\200\246_(f44e56404a51ca26).snap" | 2 - ...ons_-_Asynchronous_(408134055c24a538).snap | 2 - ...ions_-_Synchronous_(6a32ec69d15117b8).snap | 11 +- ...onal_\342\200\246_(94c036c5d3803ab2).snap" | 7 +- ...t_ret\342\200\246_(393cb38bf7119649).snap" | 3 - ...t_ret\342\200\246_(3d2d19aa49b28f1c).snap" | 1 - ...nvalid_return_type_(a91e0c67519cd77f).snap | 14 +- ...type_\342\200\246_(c3a523878447af6b).snap" | 5 +- ...ithin\342\200\246_(3259718bf20b45a2).snap" | 6 +- ...ithin\342\200\246_(711fb86287c4d87b).snap" | 6 +- ...n_wit\342\200\246_(f58a51442a16371e).snap" | 1 - ...withi\342\200\246_(c19e9277cf9fafb5).snap" | 1 - ..._in_c\342\200\246_(1a50b4ccb10b95dd).snap" | 3 +- ...in_me\342\200\246_(2ed4c18a38ed9090).snap" | 3 +- ...in_ne\342\200\246_(a1aca17ea750ffdd).snap" | 3 +- ...order\342\200\246_(d075a45828c9dbc5).snap" | 9 +- ...with_\342\200\246_(ce8defbeaf54e06c).snap" | 3 +- ..._Nested_functions_(3f2ee9fa81da0177).snap" | 3 +- ...ed_in\342\200\246_(de027dcc5360f252).snap" | 3 +- ...erloa\342\200\246_(39e892ccb644ee63).snap" | 3 - ...n_wit\342\200\246_(8fdf5a06afc7d4fe).snap" | 2 - ...of_ov\342\200\246_(93e9a157fdca3ab2).snap" | 2 - ..._inva\342\200\246_(249d635e74a41c9e).snap" | 4 - ...n_3.1\342\200\246_(5e6477d05ddea33f).snap" | 10 - ...Objec\342\200\246_(b753048091f275c0).snap" | 9 - ...Objec\342\200\246_(f9e5e48e3a4a4c12).snap" | 5 - ...sage_-_Metaclasses_(faeb52a8cd1533b3).snap | 2 - ..._the_\342\200\246_(93e8ab913ead83b2).snap" | 1 - ...ion_w\342\200\246_(3d4f2229d00f8d86).snap" | 1 - ...ion_w\342\200\246_(718dcfd7e6ed9829).snap" | 1 - ...sion_w\342\200\246_(8686e7748a7c975).snap" | 1 - ...sons_\342\200\246_(f45f1da2f8ca693d).snap" | 1 - ...lemen\342\200\246_(39b614d4707c0661).snap" | 8 +- ...pport\342\200\246_(966dd82bd3668d0e).snap" | 6 - ...fixes\342\200\246_(c25079c01f6d8eb3).snap" | 1 - ...paris\342\200\246_(400a427b33d53e00).snap" | 7 - ...lidat\342\200\246_(25381f371caa1401).snap" | 11 - ...ict`_-_Diagnostics_(e5289abf5c570c29).snap | 33 +- ...ct`_i\342\200\246_(9df67eb93e3df341).snap" | 1 - ..._with\342\200\246_(4b18755412dfaff1).snap" | 32 - ...decla\342\200\246_(bef70731cae5b8af).snap" | 4 - ...warni\342\200\246_(75ac240a2d1f7108).snap" | 2 - ..._exam\342\200\246_(c24ecd8582e5eb2f).snap" | 3 - ...ts_bu\342\200\246_(d840ac443ca8ec7f).snap" | 2 - ...s_on_\342\200\246_(7bdb97302c27c412).snap" | 4 - ...rgume\342\200\246_(ad1d489710ee2a34).snap" | 2 - ...rd_re\342\200\246_(707b284610419a54).snap" | 13 - ...long_\342\200\246_(ec94b5e857284ef3).snap" | 2 - ...loade\342\200\246_(4408ade1316b97c0).snap" | 3 - ...t_dia\342\200\246_(f419c2a8e2ce2412).snap" | 6 - ..._impo\342\200\246_(72d090df51ea97b8).snap" | 1 - ...th_a_\342\200\246_(12d4a70b7fc67cc6).snap" | 1 - ...th_an\342\200\246_(6cff507dc64a1bff).snap" | 1 - ...th_an\342\200\246_(9da56616d6332a83).snap" | 1 - ...th_an\342\200\246_(9fa713dfa17cc404).snap" | 1 - ...th_to\342\200\246_(4b8ba6ee48180cdd).snap" | 1 - ...d_on_\342\200\246_(51edda0b1aebc2bf).snap" | 1 - ...t_bef\342\200\246_(41702a6f6d20b082).snap" | 2 - ..._Pyth\342\200\246_(1028a80959504fc9).snap" | 2 - ...00\246_-_Enum_base_(4873196c8b48364).snap" | 1 - ...Enum_with_members_(81bef9a8e1230854).snap" | 1 - ..._-_`@final`_class_(ea69d237256b3762).snap" | 1 - ..._-_`Generic`_base_(d455f46a27cec685).snap" | 1 - ...-_`Protocol`_base_(99c9bde73664dd51).snap" | 1 - ..._`TypedDict`_base_(6f76171c88fc8760).snap" | 1 - ...`_att\342\200\246_(2721d40bf12fe8b7).snap" | 1 - ...`_met\342\200\246_(15636dc4074e5335).snap" | 4 +- ...`_met\342\200\246_(ce8b8da49eaf4cda).snap" | 4 +- ...n_wher\342\200\246_(7cca8063ea43c1a).snap" | 1 - ...defaul\342\200\246_(b62ed1f409042cc).snap" | 3 +- ...efaul\342\200\246_(d9ffda7fd9cdf840).snap" | 1 - ...ned_de\342\200\246_(ff24930259abfb3).snap" | 6 +- ...fault\342\200\246_(a2759fd9d2731a7d).snap" | 3 +- ...t_wit\342\200\246_(30284a6490652e58).snap" | 6 +- ...t_wit\342\200\246_(37f9b6583c0633f5).snap" | 3 +- ...'s_bo\342\200\246_(fcd7ad5416c91629).snap" | 1 - ...s_use\342\200\246_(7e6bb178099059fe).snap" | 7 +- ...ent_-_Before_3.10_(2545eaa83b635b8b).snap" | 1 - .../resources/mdtest/subscript/instance.md | 1 - .../mdtest/suppressions/ty_ignore.md | 10 - .../mdtest/suppressions/type_ignore.md | 5 - .../resources/mdtest/ty_extensions.md | 4 - .../resources/mdtest/type_qualifiers/final.md | 3 - .../resources/mdtest/unary/custom.md | 3 - .../resources/mdtest/unary/not.md | 1 - .../resources/mdtest/unreachable.md | 1 - .../resources/mdtest/with/async.md | 1 - .../resources/mdtest/with/sync.md | 1 - crates/ty_python_semantic/src/fixes.rs | 5 - .../src/types/ide_support/unreachable_code.rs | 34 - ...h_diagnostics__full_diagnostic_output.snap | 2 +- ...gnostic_caching_rendered_source_after.snap | 2 +- ...nostic_caching_rendered_source_before.snap | 2 +- crates/ty_site_packages/src/lib.rs | 11 +- 1758 files changed, 20503 insertions(+), 10265 deletions(-) create mode 100644 crates/ruff_annotate_snippets/examples/custom_error.rs rename crates/ruff_annotate_snippets/{tests/fixtures/color/strip_line.svg => examples/custom_error.svg} (57%) create mode 100644 crates/ruff_annotate_snippets/examples/custom_level.rs create mode 100644 crates/ruff_annotate_snippets/examples/custom_level.svg create mode 100644 crates/ruff_annotate_snippets/examples/elide_header.rs create mode 100644 crates/ruff_annotate_snippets/examples/elide_header.svg create mode 100644 crates/ruff_annotate_snippets/examples/highlight_message.rs create mode 100644 crates/ruff_annotate_snippets/examples/highlight_message.svg create mode 100644 crates/ruff_annotate_snippets/examples/highlight_source.rs create mode 100644 crates/ruff_annotate_snippets/examples/highlight_source.svg create mode 100644 crates/ruff_annotate_snippets/examples/id_hyperlink.rs create mode 100644 crates/ruff_annotate_snippets/examples/id_hyperlink.svg create mode 100644 crates/ruff_annotate_snippets/examples/multi_suggestion.rs create mode 100644 crates/ruff_annotate_snippets/examples/multi_suggestion.svg create mode 100644 crates/ruff_annotate_snippets/examples/struct_name_as_context.rs create mode 100644 crates/ruff_annotate_snippets/examples/struct_name_as_context.svg create mode 100644 crates/ruff_annotate_snippets/src/level.rs delete mode 100644 crates/ruff_annotate_snippets/src/renderer/display_list.rs create mode 100644 crates/ruff_annotate_snippets/src/renderer/render.rs create mode 100644 crates/ruff_annotate_snippets/src/renderer/source_map.rs rename crates/ruff_annotate_snippets/tests/{fixtures/color/ann_removed_nl.svg => color/ann_eof.ascii.term.svg} (75%) create mode 100644 crates/ruff_annotate_snippets/tests/color/ann_eof.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/ann_eof.unicode.term.svg rename crates/ruff_annotate_snippets/tests/{fixtures/color/ann_insertion.svg => color/ann_insertion.ascii.term.svg} (76%) create mode 100644 crates/ruff_annotate_snippets/tests/color/ann_insertion.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/ann_insertion.unicode.term.svg rename crates/ruff_annotate_snippets/tests/{fixtures/color/ann_multiline.svg => color/ann_multiline.ascii.term.svg} (60%) create mode 100644 crates/ruff_annotate_snippets/tests/color/ann_multiline.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/ann_multiline.unicode.term.svg rename crates/ruff_annotate_snippets/tests/{fixtures/color/ann_multiline2.svg => color/ann_multiline2.ascii.term.svg} (74%) create mode 100644 crates/ruff_annotate_snippets/tests/color/ann_multiline2.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/ann_multiline2.unicode.term.svg rename crates/ruff_annotate_snippets/tests/{fixtures/color/ann_eof.svg => color/ann_removed_nl.ascii.term.svg} (75%) create mode 100644 crates/ruff_annotate_snippets/tests/color/ann_removed_nl.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/ann_removed_nl.unicode.term.svg rename crates/ruff_annotate_snippets/tests/{fixtures/color/ensure-emoji-highlight-width.svg => color/ensure_emoji_highlight_width.ascii.term.svg} (65%) create mode 100644 crates/ruff_annotate_snippets/tests/color/ensure_emoji_highlight_width.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/ensure_emoji_highlight_width.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/first_snippet_is_primary.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/first_snippet_is_primary.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/first_snippet_is_primary.unicode.term.svg rename crates/ruff_annotate_snippets/tests/{fixtures/color/fold_ann_multiline.svg => color/fold_ann_multiline.ascii.term.svg} (55%) create mode 100644 crates/ruff_annotate_snippets/tests/color/fold_ann_multiline.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/fold_ann_multiline.unicode.term.svg rename crates/ruff_annotate_snippets/tests/{fixtures/color/fold_bad_origin_line.svg => color/fold_bad_origin_line.ascii.term.svg} (66%) create mode 100644 crates/ruff_annotate_snippets/tests/color/fold_bad_origin_line.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/fold_bad_origin_line.unicode.term.svg rename crates/ruff_annotate_snippets/tests/{fixtures/color/fold_leading.svg => color/fold_leading.ascii.term.svg} (73%) create mode 100644 crates/ruff_annotate_snippets/tests/color/fold_leading.rs rename crates/ruff_annotate_snippets/tests/{fixtures/color/strip_line_char.svg => color/fold_leading.unicode.term.svg} (56%) rename crates/ruff_annotate_snippets/tests/{fixtures/color/fold_trailing.svg => color/fold_trailing.ascii.term.svg} (73%) create mode 100644 crates/ruff_annotate_snippets/tests/color/fold_trailing.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/fold_trailing.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_diff_line_with_wide_characters.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_diff_line_with_wide_characters.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_diff_line_with_wide_characters.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_duplicated_diff_lines.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_duplicated_diff_lines.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_duplicated_diff_lines.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_first_line_tab_371.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_first_line_tab_371.rs rename crates/ruff_annotate_snippets/tests/{fixtures/color/regression_leading_tab_label_alignment.svg => color/highlight_source.ascii.term.svg} (59%) create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_source.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_source.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_source_multi_width_chars.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_source_multi_width_chars.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_source_multi_width_chars.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_source_zero_width_chars.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_source_zero_width_chars.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/highlight_source_zero_width_chars.unicode.term.svg rename crates/ruff_annotate_snippets/tests/{fixtures/color/issue_9.svg => color/issue_9.ascii.term.svg} (59%) create mode 100644 crates/ruff_annotate_snippets/tests/color/issue_9.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/issue_9.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/main.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/multiline_removal_indent.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/multiline_removal_indent.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/multiline_removal_indent.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/multiline_removal_last_line_tabs.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/multiline_removal_last_line_tabs.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/multiline_removal_last_line_tabs.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/multiline_removal_suggestion.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/multiline_removal_suggestion.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/multiline_removal_suggestion.unicode.term.svg rename crates/ruff_annotate_snippets/tests/{fixtures/color/multiple_annotations.svg => color/multiple_annotations.ascii.term.svg} (67%) create mode 100644 crates/ruff_annotate_snippets/tests/color/multiple_annotations.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/multiple_annotations.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/multiple_highlight_duplicated.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/multiple_highlight_duplicated.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/multiple_highlight_duplicated.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/multiple_multiline_removal.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/multiple_multiline_removal.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/multiple_multiline_removal.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/primary_title_second_group.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/primary_title_second_group.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/primary_title_second_group.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/regression_leading_tab_label_alignment.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/regression_leading_tab_label_alignment.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/regression_leading_tab_label_alignment.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/regression_leading_tab_long_line.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/regression_leading_tab_long_line.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/regression_leading_tab_long_line.unicode.term.svg rename crates/ruff_annotate_snippets/tests/{fixtures/color/simple.svg => color/simple.ascii.term.svg} (68%) create mode 100644 crates/ruff_annotate_snippets/tests/color/simple.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/simple.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/strip_line.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/strip_line.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/strip_line.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/strip_line_char.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/strip_line_char.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/strip_line_char.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/strip_line_non_ws.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/strip_line_non_ws.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/strip_line_non_ws.unicode.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/styled_title.ascii.term.svg create mode 100644 crates/ruff_annotate_snippets/tests/color/styled_title.rs create mode 100644 crates/ruff_annotate_snippets/tests/color/styled_title.unicode.term.svg delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/ann_eof.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/ann_insertion.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/ann_multiline.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/ann_multiline2.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/ann_removed_nl.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/ensure-emoji-highlight-width.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/fold_ann_multiline.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/fold_bad_origin_line.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/fold_leading.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/fold_trailing.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/issue_9.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/multiple_annotations.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_label_alignment.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_long_line.svg delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/regression_leading_tab_long_line.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/simple.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/strip_line.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/strip_line_char.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/strip_line_non_ws.svg delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/color/strip_line_non_ws.toml delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/deserialize.rs delete mode 100644 crates/ruff_annotate_snippets/tests/fixtures/main.rs diff --git a/Cargo.lock b/Cargo.lock index 868cb2053a..d34a10c5b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -948,7 +948,7 @@ checksum = "a867d7322eb69cf3a68a5426387a25b45cb3b9c5ee41023ee6cea92e2afadd82" dependencies = [ "camino", "fancy-regex", - "libtest-mimic 0.8.1", + "libtest-mimic", "walkdir", ] @@ -1968,18 +1968,6 @@ dependencies = [ "libc", ] -[[package]] -name = "libtest-mimic" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc0bda45ed5b3a2904262c1bb91e526127aa70e7ef3758aba2ef93cf896b9b58" -dependencies = [ - "clap", - "escape8259", - "termcolor", - "threadpool", -] - [[package]] name = "libtest-mimic" version = "0.8.1" @@ -2286,16 +2274,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "objc2" version = "0.6.3" @@ -3146,10 +3124,7 @@ dependencies = [ "anstyle", "memchr", "ruff_annotate_snippets", - "serde", "snapbox", - "toml 1.1.3+spec-1.1.0", - "tryfn", "unicode-width", ] @@ -4255,15 +4230,6 @@ dependencies = [ "windows-sys 0.61.0", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" version = "0.4.3" @@ -4383,15 +4349,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - [[package]] name = "tikv-jemalloc-sys" version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" @@ -4608,17 +4565,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "tryfn" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f68b00518dd6c69ee2289900b140e55dad068cb925678603bfa8d539f61ef6c1" -dependencies = [ - "ignore", - "libtest-mimic 0.7.3", - "snapbox", -] - [[package]] name = "ty" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index d0775eb890..e209367e60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -202,7 +202,6 @@ tracing-subscriber = { version = "0.3.18", default-features = false, features = "ansi", "smallvec", ] } -tryfn = { version = "1.0.0" } typed-arena = { version = "2.0.2" } unicode-ident = { version = "1.0.12" } unicode-normalization = { version = "0.1.23" } diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index 57f6351a0c..27d045291a 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -1354,7 +1354,6 @@ mod tests { | 1 | 1 | ^ - | panic: Panicked at when checking `test.py`: `Test panic for FormatCommandError` --> test.py:1:1 diff --git a/crates/ruff/tests/cli/format.rs b/crates/ruff/tests/cli/format.rs index 842f45b1a0..08549a9fa1 100644 --- a/crates/ruff/tests/cli/format.rs +++ b/crates/ruff/tests/cli/format.rs @@ -56,14 +56,14 @@ fn default_files() -> Result<()> { exit_code: 1 ----- stdout ----- unformatted: File would be reformatted - --> bar.py:1:1 + --> bar.py:1:7 | - bar = "needs formatting" 1 + bar = "needs formatting" | unformatted: File would be reformatted - --> foo.py:1:1 + --> foo.py:1:7 | - foo = "needs formatting" 1 + foo = "needs formatting" @@ -520,7 +520,7 @@ exclude = ["format_excluded.py"] exit_code: 1 ----- stdout ----- unformatted: File would be reformatted - --> main.py:1:1 + --> main.py:1:3 | - x = 1 1 + x = 1 @@ -548,7 +548,7 @@ fn deduplicate_directory_and_explicit_file() -> Result<()> { exit_code: 1 ----- stdout ----- unformatted: File would be reformatted - --> main.py:1:1 + --> main.py:1:3 | - x = 1 1 + x = 1 @@ -584,7 +584,6 @@ from module import = | 2 | from module import = | ^ - | ----- stderr ----- @@ -700,7 +699,7 @@ fn output_format_notebook() -> Result<()> { exit_code: 1 ----- stdout ----- unformatted: File would be reformatted - --> CRATE_ROOT/resources/test/fixtures/unformatted.ipynb:cell 1:1:1 + --> CRATE_ROOT/resources/test/fixtures/unformatted.ipynb:cell 1:2:1 ::: cell 1 | 1 | import numpy @@ -835,7 +834,7 @@ fn check_quiet_mode_shows_diagnostics_only() -> Result<()> { exit_code: 1 ----- stdout ----- unformatted: File would be reformatted - --> main.py:1:1 + --> main.py:1:5 | - def foo(): - pass @@ -859,7 +858,7 @@ fn check_default_mode_shows_diagnostics_and_summary() -> Result<()> { exit_code: 1 ----- stdout ----- unformatted: File would be reformatted - --> main.py:1:1 + --> main.py:1:5 | - def foo(): - pass @@ -2052,7 +2051,6 @@ fn syntax_error_in_notebooks_check() -> Result<()> { 2 | # выберите случайный индекс в диапазон от 0 до len(X)-1 включительно при помощи функции random.randint 3 | j = # ваш код здесь | ^ - | ----- stderr ----- @@ -2557,7 +2555,7 @@ fn markdown_formatting() -> Result<()> { exit_code: 1 ----- stdout ----- unformatted: File would be reformatted - --> CRATE_ROOT/resources/test/fixtures/unformatted.md:1:1 + --> CRATE_ROOT/resources/test/fixtures/unformatted.md:4:7 | 3 | ```py - print( "hello" ) @@ -2694,7 +2692,7 @@ print( 'hello' ) exit_code: 1 ----- stdout ----- unformatted: File would be reformatted - --> test.bar:1:1 + --> test.bar:5:7 | 4 | ```py - print( 'hello' ) diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 3f57bc14a7..0955917992 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -4475,7 +4475,6 @@ fn show_fixes_in_full_output_with_preview_enabled() { | 1 | import math | ^^^^ - | help: Remove unused import: `math` | - import math @@ -5195,7 +5194,6 @@ fn ruff_toml_is_linted() -> Result<()> { | 1 | lint.select = ["F401"] | ^^^^ - | help: Replace rule code with `unused-import` | - lint.select = ["F401"] diff --git a/crates/ruff/tests/integration_test.rs b/crates/ruff/tests/integration_test.rs index 90e0337904..08d53501db 100644 --- a/crates/ruff/tests/integration_test.rs +++ b/crates/ruff/tests/integration_test.rs @@ -120,7 +120,6 @@ fn stdin_error() { | 1 | import os | ^^ - | help: Remove unused import: `os` | - import os @@ -148,7 +147,6 @@ fn stdin_filename() { | 1 | import os | ^^ - | help: Remove unused import: `os` | - import os @@ -187,7 +185,6 @@ import bar # unused import | 2 | import bar # unused import | ^^^ - | help: Remove unused import: `bar` | 1 | @@ -199,7 +196,6 @@ import bar # unused import | 2 | import foo # unused import | ^^^ - | help: Remove unused import: `foo` | 1 | @@ -231,7 +227,6 @@ fn check_warn_stdin_filename_with_files() { | 1 | import os | ^^ - | help: Remove unused import: `os` | - import os @@ -261,7 +256,6 @@ fn stdin_source_type_py() { | 1 | import os | ^^ - | help: Remove unused import: `os` | - import os @@ -502,7 +496,6 @@ fn stdin_fix_jupyter() { | 1 | print(x) | ^ - | Found 3 errors (2 fixed, 1 remaining). "#); @@ -601,7 +594,6 @@ fn stdin_override_parser_ipynb() { | 1 | import os | ^^ - | help: Remove unused import: `os` ::: cell 1 | @@ -613,7 +605,6 @@ fn stdin_override_parser_ipynb() { | 1 | import sys | ^^^ - | help: Remove unused import: `sys` ::: cell 3 | @@ -647,7 +638,6 @@ fn stdin_override_parser_py() { | 1 | import os | ^^ - | help: Remove unused import: `os` | - import os @@ -685,7 +675,6 @@ extension = {ipynb="python"} | 1 | import os | ^^ - | help: Remove unused import: `os` | - import os @@ -884,7 +873,6 @@ fn stdin_parse_error() { | 1 | from foo import | ^ - | Found 1 error. @@ -914,7 +902,6 @@ fn stdin_multiple_parse_error() { 1 | from foo import 2 | bar = | ^ - | Found 2 errors. @@ -936,7 +923,6 @@ fn parse_error_not_included() { | 1 | foo = | ^ - | Found 1 error. @@ -959,7 +945,6 @@ fn full_output_preview() { | 1 | l = 1 | ^ - | Found 1 error. @@ -988,7 +973,6 @@ preview = true | 1 | l = 1 | ^ - | Found 1 error. @@ -1013,7 +997,6 @@ fn full_output_format() { | 1 | l = 1 | ^ - | Found 1 error. @@ -1870,7 +1853,6 @@ fn check_input_from_argfile() -> Result<()> { | 1 | import os | ^^ - | help: Remove unused import: `os` | - import os @@ -2502,7 +2484,6 @@ select = ["RUF017"] 2 | y = [4, 5, 6] 3 | sum([x, y], []) | ^^^^^^^^^^^^^^^ - | help: Replace with `functools.reduce` Found 1 error. @@ -2543,7 +2524,6 @@ unfixable = ["RUF"] 2 | y = [4, 5, 6] 3 | sum([x, y], []) | ^^^^^^^^^^^^^^^ - | help: Replace with `functools.reduce` Found 1 error. @@ -2571,7 +2551,6 @@ fn pyproject_toml_stdin_syntax_error() { | 1 | [project | ^ - | Found 1 error. @@ -2598,7 +2577,6 @@ fn pyproject_toml_stdin_schema_error() { 1 | [project] 2 | name = 1 | ^ - | Found 1 error. @@ -2691,7 +2669,6 @@ fn pyproject_toml_stdin_schema_error_fix() { 1 | [project] 2 | name = 1 | ^ - | Found 1 error. " diff --git a/crates/ruff_annotate_snippets/Cargo.toml b/crates/ruff_annotate_snippets/Cargo.toml index 560501ece5..b84edcd631 100644 --- a/crates/ruff_annotate_snippets/Cargo.toml +++ b/crates/ruff_annotate_snippets/Cargo.toml @@ -12,28 +12,91 @@ license = "MIT OR Apache-2.0" [dependencies] anstyle = { workspace = true } -memchr = { workspace = true } +memchr = { workspace = true, optional = true } unicode-width = { workspace = true } [dev-dependencies] ruff_annotate_snippets = { path = ".", features = ["testing-colors"] } - anstream = { workspace = true } -serde = { workspace = true, features = ["derive"] } -snapbox = { workspace = true, features = ["diff", "term-svg", "cmd", "examples"] } -toml = { workspace = true } -tryfn = { workspace = true } +snapbox = { workspace = true } [features] -default = [] +default = ["std", "simd"] +std = ["anstyle/std", "memchr?/std"] +simd = ["dep:memchr"] testing-colors = [] -[[test]] -name = "fixtures" -harness = false +# Using upstream lints +[lints.rust] +rust_2018_idioms = { level = "warn", priority = -1 } +unnameable_types = "warn" +unreachable_pub = "warn" +unsafe_op_in_unsafe_fn = "warn" +unused_lifetimes = "warn" +unused_macro_rules = "warn" +unused_qualifications = "warn" -[lints] -workspace = true +[lints.clippy] +disallowed_methods = "allow" # HACK: minimize changes from upstream +bool_assert_comparison = "allow" +branches_sharing_code = "allow" +checked_conversions = "warn" +collapsible_else_if = "allow" +create_dir = "warn" +dbg_macro = "warn" +debug_assert_with_mut_call = "warn" +doc_markdown = "warn" +empty_enums = "warn" +enum_glob_use = "warn" +expl_impl_clone_on_copy = "warn" +explicit_deref_methods = "warn" +explicit_into_iter_loop = "warn" +fallible_impl_from = "warn" +filter_map_next = "warn" +flat_map_option = "warn" +float_cmp_const = "warn" +fn_params_excessive_bools = "warn" +from_iter_instead_of_collect = "warn" +if_same_then_else = "allow" +implicit_clone = "warn" +imprecise_flops = "warn" +inconsistent_struct_constructor = "warn" +inefficient_to_string = "warn" +infinite_loop = "warn" +invalid_upcast_comparisons = "warn" +large_digit_groups = "warn" +large_stack_arrays = "warn" +large_types_passed_by_value = "warn" +let_and_return = "allow" # sometimes good to name what you are returning +linkedlist = "warn" +lossy_float_literal = "warn" +macro_use_imports = "warn" +mem_forget = "warn" +mutex_integer = "warn" +needless_continue = "allow" +needless_for_each = "warn" +negative_feature_names = "warn" +path_buf_push_overwrite = "warn" +ptr_as_ptr = "warn" +rc_mutex = "warn" +redundant_feature_names = "warn" +ref_option_ref = "warn" +rest_pat_in_fully_bound_structs = "warn" +result_large_err = "allow" +same_functions_in_if_condition = "warn" +self_named_module_files = "warn" +semicolon_if_nothing_returned = "warn" +str_to_string = "warn" +string_add = "warn" +string_add_assign = "warn" +string_lit_as_bytes = "warn" +todo = "warn" +trait_duplication_in_bounds = "warn" +uninlined_format_args = "warn" +verbose_file_reads = "warn" +wildcard_imports = "warn" +zero_sized_map_values = "warn" [lib] +name = "annotate_snippets" test = false diff --git a/crates/ruff_annotate_snippets/examples/custom_error.rs b/crates/ruff_annotate_snippets/examples/custom_error.rs new file mode 100644 index 0000000000..1618d3f0cf --- /dev/null +++ b/crates/ruff_annotate_snippets/examples/custom_error.rs @@ -0,0 +1,32 @@ +use annotate_snippets::renderer::DecorStyle; +use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet}; + +fn main() { + let source = r#"//@ compile-flags: -Ztreat-err-as-bug +//@ failure-status: 101 +//@ error-pattern: aborting due to `-Z treat-err-as-bug=1` +//@ error-pattern: [eval_static_initializer] evaluating initializer of static `C` +//@ normalize-stderr: "note: .*\n\n" -> "" +//@ normalize-stderr: "thread 'rustc' panicked.*:\n.*\n" -> "" +//@ rustc-env:RUST_BACKTRACE=0 + +#![crate_type = "rlib"] + +pub static C: u32 = 0 - 1; +//~^ ERROR could not evaluate static initializer +"#; + let report = &[Level::ERROR + .with_name(Some("error: internal compiler error")) + .primary_title("could not evaluate static initializer") + .id("E0080") + .element( + Snippet::source(source).path("$DIR/err.rs").annotation( + AnnotationKind::Primary + .span(386..391) + .label("attempt to compute `0_u32 - 1_u32`, which would overflow"), + ), + )]; + + let renderer = Renderer::styled().decor_style(DecorStyle::Unicode); + anstream::println!("{}", renderer.render(report)); +} diff --git a/crates/ruff_annotate_snippets/tests/fixtures/color/strip_line.svg b/crates/ruff_annotate_snippets/examples/custom_error.svg similarity index 57% rename from crates/ruff_annotate_snippets/tests/fixtures/color/strip_line.svg rename to crates/ruff_annotate_snippets/examples/custom_error.svg index 75709d703a..8c05a6c47a 100644 --- a/crates/ruff_annotate_snippets/tests/fixtures/color/strip_line.svg +++ b/crates/ruff_annotate_snippets/examples/custom_error.svg @@ -1,4 +1,4 @@ - +
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn primary_title(self, text: impl Into>) -> Title<'a> { + Title { + level: self, + id: None, + text: text.into(), + allows_styling: false, + is_fixable: false, + } + } + + /// For any secondary, or context, [`Group`][crate::Group]s (subsequent) in a [`Report`][crate::Report] + /// + /// See [`Group::with_title`][crate::Group::with_title] + /// + ///
+ /// + /// Text passed to this function is allowed to be styled, as such all + /// text is considered "trusted input" and has no normalizations applied to + /// it. [`normalize_untrusted_str`](crate::normalize_untrusted_str) can be + /// used to normalize untrusted text before it is passed to this function. + /// + ///
+ pub fn secondary_title(self, text: impl Into>) -> Title<'a> { + Title { + level: self, + id: None, + text: text.into(), + allows_styling: true, + is_fixable: false, + } + } + + /// A text [`Element`][crate::Element] in a [`Group`][crate::Group] + /// + ///
+ /// + /// Text passed to this function is allowed to be styled, as such all + /// text is considered "trusted input" and has no normalizations applied to + /// it. [`normalize_untrusted_str`](crate::normalize_untrusted_str) can be + /// used to normalize untrusted text before it is passed to this function. + /// + ///
+ pub fn message(self, text: impl Into>) -> Message<'a> { + Message { + level: self, + text: text.into(), + } + } + + pub(crate) fn as_str(&'a self) -> &'a str { + match (&self.name, self.level) { + (Some(Some(name)), _) => name.as_ref(), + (Some(None), _) => "", + (None, LevelInner::Error) => ERROR_TXT, + (None, LevelInner::Warning) => WARNING_TXT, + (None, LevelInner::Info) => INFO_TXT, + (None, LevelInner::Note) => NOTE_TXT, + (None, LevelInner::Help) => HELP_TXT, + } + } + + pub(crate) fn style(&self, stylesheet: &Stylesheet) -> Style { + self.level.style(stylesheet) + } +} + +/// # Customize the `Level` +impl<'a> Level<'a> { + /// Replace the name describing this [`Level`] + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ /// + /// # Example + /// + /// ```rust + /// # #[allow(clippy::needless_doctest_main)] + #[doc = include_str!("../examples/custom_level.rs")] + /// ``` + #[doc = include_str!("../examples/custom_level.svg")] + pub fn with_name(self, name: impl Into>) -> Level<'a> { + Level { + name: Some(name.into().0), + level: self.level, + } + } + + /// Do not show the [`Level`]s name + /// + /// Useful for: + /// - Another layer of the application will include the level (e.g. when rendering errors) + /// - [`Message`]s that are part of a previous [`Group`][crate::Group] [`Element`][crate::Element]s + /// + /// # Example + /// + /// ```rust + /// # use annotate_snippets::{Group, Snippet, AnnotationKind, Level}; + ///let source = r#"fn main() { + /// let b: &[u8] = include_str!("file.txt"); //~ ERROR mismatched types + /// let s: &str = include_bytes!("file.txt"); //~ ERROR mismatched types + /// }"#; + /// let report = &[ + /// Level::ERROR.primary_title("mismatched types").id("E0308") + /// .element( + /// Snippet::source(source) + /// .path("$DIR/mismatched-types.rs") + /// .annotation( + /// AnnotationKind::Primary + /// .span(105..131) + /// .label("expected `&str`, found `&[u8; 0]`"), + /// ) + /// .annotation( + /// AnnotationKind::Context + /// .span(98..102) + /// .label("expected due to this"), + /// ), + /// ) + /// .element( + /// Level::NOTE + /// .no_name() + /// .message("expected reference `&str`\nfound reference `&'static [u8; 0]`"), + /// ), + /// ]; + /// ``` + pub fn no_name(self) -> Level<'a> { + self.with_name(None::<&str>) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum LevelInner { + Error, + Warning, + Info, + Note, + Help, +} + +impl LevelInner { + pub(crate) fn style(self, stylesheet: &Stylesheet) -> Style { + match self { + LevelInner::Error => stylesheet.error, + LevelInner::Warning => stylesheet.warning, + LevelInner::Info => stylesheet.info, + LevelInner::Note => stylesheet.note, + LevelInner::Help => stylesheet.help, + } + } +} diff --git a/crates/ruff_annotate_snippets/src/lib.rs b/crates/ruff_annotate_snippets/src/lib.rs index f8d0e1e5c6..e47083464a 100644 --- a/crates/ruff_annotate_snippets/src/lib.rs +++ b/crates/ruff_annotate_snippets/src/lib.rs @@ -1,35 +1,93 @@ -//! A library for formatting of text or programming code snippets. -//! -//! It's primary purpose is to build an ASCII-graphical representation of the snippet -//! with annotations. +//! Format [diagnostic reports][Report], including highlighting snippets of text //! //! # Example //! //! ```rust +//! # #[allow(clippy::needless_doctest_main)] #![doc = include_str!("../examples/expected_type.rs")] //! ``` //! #![doc = include_str!("../examples/expected_type.svg")] //! -//! The crate uses a three stage process with two conversions between states: +//! # Visual overview +//! +//! [`Report`] +//! +#![doc = include_str!("../examples/multi_suggestion.svg")] +//! +//! ### Primary group +//! +//! [`Title`] +//! ```text +//! error: cannot construct `Box<_, _>` with struct literal syntax due to private fields +//! ``` +//! +//! +//! [`Annotation`] on a [`Snippet`] +//! ```text +//! ╭▸ $DIR/multi-suggestion.rs:17:13 +//! │ +//! 17 │ let _ = Box {}; +//! │ ━━━ +//! │ +//! ``` +//! +//! [`Message`] +//! ```text +//! ╰ note: private fields `0` and `1` that were not provided +//! ``` +//! +//! +//! +//! ### Secondary group: suggested fix +//! +//! [`Title`] (proposed solution) +//! ```text +//! help: you might have meant to use an associated function to build this type +//! ``` +//! +//! [`Patch`] Option 1 on a [`Snippet`] +//! ```text +//! ╭╴ +//! 21 - let _ = Box {}; +//! 21 + let _ = Box::new(_); +//! ├╴ +//! ``` //! +//! [`Patch`] Option 2 on a [`Snippet`] //! ```text -//! Message --> Renderer --> impl Display +//! ├╴ +//! 17 - let _ = Box {}; +//! 17 + let _ = Box::new_uninit(); +//! ├╴ //! ``` //! -//! The input type - [Message] is a structure designed -//! to align with likely output from any parser whose code snippet is to be -//! annotated. +//! *etc for Options 3 and 4* +//! +//! [`Message`] +//! ```text +//! ╰ and 12 other candidates +//! ``` //! -//! The middle structure - [Renderer] is a structure designed -//! to convert a snippet into an internal structure that is designed to store -//! the snippet data in a way that is easy to format. -//! [Renderer] also handles the user-configurable formatting -//! options, such as color, or margins. +//! ### Secondary group: alternative suggested fix //! -//! Finally, `impl Display` into a final `String` output. +//! [`Title`] (proposed solution) +//! ```text +//! help: consider using the `Default` trait +//! ``` +//! +//! Only [`Patch`] on a [`Snippet`] +//! ```text +//! ╭╴ +//! 17 - let _ = Box {}; +//! 17 + let _ = ::default(); +//! ╰╴ +//! ``` +//! +//! # Cargo `features` +//! +//! - `simd` - Speeds up folding //! -//! # features //! - `testing-colors` - Makes [Renderer::styled] colors OS independent, which //! allows for easier testing when testing colored output. It should be added as //! a feature in `[dev-dependencies]`, which can be done with the following command: @@ -37,33 +95,36 @@ //! cargo add annotate-snippets --dev --feature testing-colors //! ``` -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(all(not(feature = "std"), not(test)), no_std)] +#![cfg_attr(docsrs, feature(doc_cfg))] #![warn(clippy::print_stderr)] #![warn(clippy::print_stdout)] +#![warn(clippy::std_instead_of_alloc)] +#![warn(clippy::std_instead_of_core)] #![warn(missing_debug_implementations)] -// Since this is a vendored copy of `annotate-snippets`, we squash Clippy -// warnings from upstream in order to the reduce the diff. If our copy drifts -// far from upstream such that patches become impractical to apply in both -// places, then we can get rid of these suppressions and fix the lints. -#![allow( - clippy::return_self_not_must_use, - clippy::cast_possible_truncation, - clippy::cast_precision_loss, - clippy::explicit_iter_loop, - clippy::unused_self, - clippy::unnecessary_wraps, - clippy::range_plus_one, - clippy::redundant_closure_for_method_calls, - clippy::struct_field_names, - clippy::cloned_instead_of_copied, - clippy::cast_sign_loss, - clippy::needless_as_bytes, - clippy::unnecessary_map_or -)] +extern crate alloc; + +use alloc::string::String; + +pub mod level; pub mod renderer; mod snippet; +/// Normalize the string to avoid any unicode control characters. +/// +/// This is important for untrusted input, as it can contain +/// invalid unicode sequences. +pub fn normalize_untrusted_str(s: &str) -> String { + renderer::normalize_whitespace(s).into_owned() +} + +#[doc(inline)] +pub use level::Level; #[doc(inline)] pub use renderer::Renderer; pub use snippet::*; + +#[doc = include_str!("../README.md")] +#[cfg(doctest)] +pub struct ReadmeDoctests; diff --git a/crates/ruff_annotate_snippets/src/renderer/display_list.rs b/crates/ruff_annotate_snippets/src/renderer/display_list.rs deleted file mode 100644 index 85396c0a30..0000000000 --- a/crates/ruff_annotate_snippets/src/renderer/display_list.rs +++ /dev/null @@ -1,1946 +0,0 @@ -//! `display_list` module stores the output model for the snippet. -//! -//! `DisplayList` is a central structure in the crate, which contains -//! the structured list of lines to be displayed. -//! -//! It is made of two types of lines: `Source` and `Raw`. All `Source` lines -//! are structured using four columns: -//! -//! ```text -//! /------------ (1) Line number column. -//! | /--------- (2) Line number column delimiter. -//! | | /------- (3) Inline marks column. -//! | | | /--- (4) Content column with the source and annotations for slices. -//! | | | | -//! ============================================================================= -//! error[E0308]: mismatched types -//! --> src/format.rs:51:5 -//! | -//! 151 | / fn test() -> String { -//! 152 | | return "test"; -//! 153 | | } -//! | |___^ error: expected `String`, for `&str`. -//! | -//! ``` -//! -//! The first two lines of the example above are `Raw` lines, while the rest -//! are `Source` lines. -//! -//! `DisplayList` does not store column alignment information, and those are -//! only calculated by the implementation of `std::fmt::Display` using information such as -//! styling. -//! -//! The above snippet has been built out of the following structure: -use crate::{Id, snippet}; -use std::borrow::Cow; -use std::cmp::{Reverse, max, min}; -use std::collections::HashMap; -use std::fmt::Display; -use std::ops::Range; -use std::{cmp, fmt}; - -use unicode_width::UnicodeWidthStr; - -use crate::renderer::styled_buffer::StyledBuffer; -use crate::renderer::{DEFAULT_TERM_WIDTH, Margin, Style, stylesheet::Stylesheet}; - -const ANONYMIZED_LINE_NUM: &str = "LL"; -const ERROR_TXT: &str = "error"; -const HELP_TXT: &str = "help"; -const INFO_TXT: &str = "info"; -const NOTE_TXT: &str = "note"; -const WARNING_TXT: &str = "warning"; - -/// List of lines to be displayed. -pub(crate) struct DisplayList<'a> { - pub(crate) body: Vec>, - pub(crate) stylesheet: &'a Stylesheet, - pub(crate) anonymized_line_numbers: bool, - pub(crate) cut_indicator: &'static str, - pub(crate) lineno_offset: usize, -} - -impl PartialEq for DisplayList<'_> { - fn eq(&self, other: &Self) -> bool { - self.body == other.body && self.anonymized_line_numbers == other.anonymized_line_numbers - } -} - -impl fmt::Debug for DisplayList<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("DisplayList") - .field("body", &self.body) - .field("anonymized_line_numbers", &self.anonymized_line_numbers) - .finish() - } -} - -impl Display for DisplayList<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let lineno_width = self.body.iter().fold(0, |max, set| { - set.display_lines.iter().fold(max, |max, line| match line { - DisplayLine::Source { lineno, .. } => cmp::max(lineno.unwrap_or(0), max), - _ => max, - }) - }); - let lineno_width = self.lineno_offset - + if lineno_width == 0 { - lineno_width - } else if self.anonymized_line_numbers { - ANONYMIZED_LINE_NUM.len() - } else { - ((lineno_width as f64).log10().floor() as usize) + 1 - }; - - let multiline_depth = self.body.iter().fold(0, |max, set| { - set.display_lines.iter().fold(max, |max2, line| match line { - DisplayLine::Source { annotations, .. } => cmp::max( - annotations.iter().fold(max2, |max3, line| { - cmp::max( - match line.annotation_part { - DisplayAnnotationPart::Standalone => 0, - DisplayAnnotationPart::LabelContinuation => 0, - DisplayAnnotationPart::MultilineStart(depth) => depth + 1, - DisplayAnnotationPart::MultilineEnd(depth) => depth + 1, - }, - max3, - ) - }), - max, - ), - _ => max2, - }) - }); - let mut buffer = StyledBuffer::new(); - for set in self.body.iter() { - self.format_set(set, lineno_width, multiline_depth, &mut buffer)?; - } - write!(f, "{}", buffer.render(self.stylesheet)?) - } -} - -impl<'a> DisplayList<'a> { - pub(crate) fn new( - message: snippet::Message<'a>, - stylesheet: &'a Stylesheet, - anonymized_line_numbers: bool, - term_width: usize, - cut_indicator: &'static str, - ) -> DisplayList<'a> { - let lineno_offset = message.lineno_offset; - let body = format_message( - message, - term_width, - anonymized_line_numbers, - cut_indicator, - true, - ); - - Self { - body, - stylesheet, - anonymized_line_numbers, - cut_indicator, - lineno_offset, - } - } - - fn format_set( - &self, - set: &DisplaySet<'_>, - lineno_width: usize, - multiline_depth: usize, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - for line in &set.display_lines { - set.format_line( - line, - lineno_width, - multiline_depth, - self.stylesheet, - self.anonymized_line_numbers, - self.cut_indicator, - buffer, - )?; - } - Ok(()) - } -} - -#[derive(Debug, PartialEq)] -pub(crate) struct DisplaySet<'a> { - pub(crate) display_lines: Vec>, - pub(crate) margin: Margin, -} - -impl DisplaySet<'_> { - fn format_label( - &self, - line_offset: usize, - label: &[DisplayTextFragment<'_>], - stylesheet: &Stylesheet, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - for fragment in label { - let style = match fragment.style { - DisplayTextStyle::Regular => stylesheet.none(), - DisplayTextStyle::Emphasis => stylesheet.emphasis(), - }; - buffer.append(line_offset, fragment.content, *style); - } - Ok(()) - } - - fn format_annotation( - &self, - line_offset: usize, - annotation: &Annotation<'_>, - continuation: bool, - stylesheet: &Stylesheet, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - let hide_severity = annotation.annotation_type.is_none(); - let color = get_annotation_style(&annotation.annotation_type, stylesheet); - - let formatted_len = if let Some(id) = &annotation.id { - let id_len = id.id.len(); - if hide_severity { - id_len - } else { - 2 + id_len + annotation_type_len(&annotation.annotation_type) - } - } else { - annotation_type_len(&annotation.annotation_type) - }; - - if continuation { - for _ in 0..formatted_len + 2 { - buffer.append(line_offset, " ", Style::new()); - } - return self.format_label(line_offset, &annotation.label, stylesheet, buffer); - } - if formatted_len == 0 { - self.format_label(line_offset, &annotation.label, stylesheet, buffer) - } else { - // TODO(brent) All of this complicated checking of `hide_severity` should be reverted - // once we have real severities in Ruff. This code is trying to account for two - // different cases: - // - // - main diagnostic message - // - subdiagnostic message - // - // In the first case, signaled by `hide_severity = true`, we want to print the ID (the - // noqa code for a ruff lint diagnostic, e.g. `F401`, or `invalid-syntax` for a syntax - // error) without brackets. Instead, for subdiagnostics, we actually want to print the - // severity (usually `help`) regardless of the `hide_severity` setting. This is signaled - // by an ID of `None`. - // - // With real severities these should be reported more like in ty: - // - // ``` - // error[F401]: `math` imported but unused - // error[invalid-syntax]: Cannot use `match` statement on Python 3.9... - // ``` - // - // instead of the current versions intended to mimic the old Ruff output format: - // - // ``` - // F401 `math` imported but unused - // invalid-syntax: Cannot use `match` statement on Python 3.9... - // ``` - // - // Note that the `invalid-syntax` colon is added manually in `ruff_db`, not here. We - // could eventually add a colon to Ruff lint diagnostics (`F401:`) and then make the - // colon below unconditional again. - // - // This also applies to the hard-coded `stylesheet.error()` styling of the - // hidden-severity `id`. This should just be `*color` again later, but for now we don't - // want an unformatted `id`, which is what `get_annotation_style` returns for - // `DisplayAnnotationType::None`. - let annotation_type = annotation_type_str(&annotation.annotation_type); - if let Some(id) = annotation.id { - if hide_severity { - buffer.append( - line_offset, - &format!("{id} ", id = fmt_with_hyperlink(id.id, id.url, stylesheet)), - *stylesheet.error(), - ); - } else { - buffer.append( - line_offset, - &format!( - "{annotation_type}[{id}]", - id = fmt_with_hyperlink(id.id, id.url, stylesheet) - ), - *color, - ); - } - } else { - buffer.append(line_offset, annotation_type, *color); - } - - if annotation.is_fixable { - buffer.append(line_offset, "[", stylesheet.none); - buffer.append(line_offset, "*", stylesheet.help); - buffer.append(line_offset, "]", stylesheet.none); - // In the hide-severity case, we need a space instead of the colon and space below. - if hide_severity { - buffer.append(line_offset, " ", stylesheet.none); - } - } - - if !is_annotation_empty(annotation) { - if annotation.id.is_none() || !hide_severity { - buffer.append(line_offset, ": ", stylesheet.none); - } - self.format_label(line_offset, &annotation.label, stylesheet, buffer)?; - } - Ok(()) - } - } - - #[inline] - fn format_raw_line( - &self, - line_offset: usize, - line: &DisplayRawLine<'_>, - lineno_width: usize, - stylesheet: &Stylesheet, - anonymized_line_numbers: bool, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - match line { - DisplayRawLine::Origin { - path, - pos, - header_type, - } => { - let header_sigil = match header_type { - DisplayHeaderType::Initial => "-->", - DisplayHeaderType::Continuation => ":::", - }; - let lineno_color = stylesheet.line_no(); - buffer.puts(line_offset, lineno_width, header_sigil, *lineno_color); - buffer.puts(line_offset, lineno_width + 4, path, stylesheet.none); - if let Some(Position { row, col, cell }) = pos { - if let Some(cell) = cell { - buffer.append(line_offset, ":", stylesheet.none); - buffer.append(line_offset, &format!("cell {cell}"), stylesheet.none); - } - buffer.append(line_offset, ":", stylesheet.none); - if anonymized_line_numbers { - buffer.append(line_offset, ANONYMIZED_LINE_NUM, stylesheet.none); - } else { - buffer.append(line_offset, row.to_string().as_str(), stylesheet.none); - } - buffer.append(line_offset, ":", stylesheet.none); - buffer.append(line_offset, col.to_string().as_str(), stylesheet.none); - } - Ok(()) - } - DisplayRawLine::Annotation { - annotation, - source_aligned, - continuation, - } => { - if *source_aligned { - if *continuation { - for _ in 0..lineno_width + 3 { - buffer.append(line_offset, " ", stylesheet.none); - } - } else { - let lineno_color = stylesheet.line_no(); - for _ in 0..lineno_width + 1 { - buffer.append(line_offset, " ", stylesheet.none); - } - buffer.append(line_offset, "=", *lineno_color); - buffer.append(line_offset, " ", *lineno_color); - } - } - self.format_annotation(line_offset, annotation, *continuation, stylesheet, buffer) - } - } - } - - // Adapted from https://github.com/rust-lang/rust/blob/d371d17496f2ce3a56da76aa083f4ef157572c20/compiler/rustc_errors/src/emitter.rs#L706-L1211 - #[expect(clippy::too_many_arguments)] - #[inline] - fn format_line( - &self, - dl: &DisplayLine<'_>, - lineno_width: usize, - multiline_depth: usize, - stylesheet: &Stylesheet, - anonymized_line_numbers: bool, - cut_indicator: &'static str, - buffer: &mut StyledBuffer, - ) -> fmt::Result { - let line_offset = buffer.num_lines(); - match dl { - DisplayLine::Source { - lineno, - inline_marks, - line, - annotations, - } => { - let lineno_color = stylesheet.line_no(); - if anonymized_line_numbers && lineno.is_some() { - let num = format!("{ANONYMIZED_LINE_NUM:>lineno_width$} |"); - buffer.puts(line_offset, 0, &num, *lineno_color); - } else { - match lineno { - Some(n) => { - let num = format!("{n:>lineno_width$} |"); - buffer.puts(line_offset, 0, &num, *lineno_color); - } - None => { - buffer.putc(line_offset, lineno_width + 1, '|', *lineno_color); - } - } - } - if let DisplaySourceLine::Content { text, .. } = line { - // The width of the line number, a space, pipe, and a space - // `123 | ` is `lineno_width + 3`. - let width_offset = lineno_width + 3; - let code_offset = if multiline_depth == 0 { - width_offset - } else { - width_offset + multiline_depth + 1 - }; - - // Add any inline marks to the code line - if !inline_marks.is_empty() || 0 < multiline_depth { - format_inline_marks( - line_offset, - inline_marks, - lineno_width, - stylesheet, - buffer, - )?; - } - - let text = normalize_whitespace(text); - let line_len = text.as_bytes().len(); - let left = self.margin.left(line_len); - let right = self.margin.right(line_len); - - // On long lines, we strip the source line, accounting for unicode. - let mut taken = 0; - let mut was_cut_right = false; - let mut code = String::new(); - for ch in text.chars().skip(left) { - // Make sure that the trimming on the right will fall within the terminal width. - // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` - // is. For now, just accept that sometimes the code line will be longer than - // desired. - let next = char_width(ch).unwrap_or(1); - if taken + next > right - left { - was_cut_right = true; - break; - } - taken += next; - code.push(ch); - } - buffer.puts(line_offset, code_offset, &code, Style::new()); - if self.margin.was_cut_left() { - // We have stripped some code/whitespace from the beginning, make it clear. - buffer.puts(line_offset, code_offset, cut_indicator, *lineno_color); - } - if was_cut_right { - buffer.puts( - line_offset, - code_offset + taken - cut_indicator.width(), - cut_indicator, - *lineno_color, - ); - } - - let left: usize = text - .chars() - .take(left) - .map(|ch| char_width(ch).unwrap_or(1)) - .sum(); - - let mut annotations = annotations.clone(); - annotations.sort_by_key(|a| Reverse(a.range.0)); - - let mut annotations_positions = vec![]; - let mut line_len: usize = 0; - let mut p = 0; - for (i, annotation) in annotations.iter().enumerate() { - for (j, next) in annotations.iter().enumerate() { - // This label overlaps with another one and both take space ( - // they have text and are not multiline lines). - if overlaps(next, annotation, 0) - && annotation.has_label() - && j > i - && p == 0 - // We're currently on the first line, move the label one line down - { - // If we're overlapping with an un-labelled annotation with the same span - // we can just merge them in the output - if next.range.0 == annotation.range.0 - && next.range.1 == annotation.range.1 - && !next.has_label() - { - continue; - } - - // This annotation needs a new line in the output. - p += 1; - break; - } - } - annotations_positions.push((p, annotation)); - for (j, next) in annotations.iter().enumerate() { - if j > i { - let l = next - .annotation - .label - .iter() - .map(|label| label.content) - .collect::>() - .join("") - .len() - + 2; - // Do not allow two labels to be in the same line if they - // overlap including padding, to avoid situations like: - // - // fn foo(x: u32) { - // -------^------ - // | | - // fn_spanx_span - // - // Both labels must have some text, otherwise they are not - // overlapping. Do not add a new line if this annotation or - // the next are vertical line placeholders. If either this - // or the next annotation is multiline start/end, move it - // to a new line so as not to overlap the horizontal lines. - if (overlaps(next, annotation, l) - && annotation.has_label() - && next.has_label()) - || (annotation.takes_space() && next.has_label()) - || (annotation.has_label() && next.takes_space()) - || (annotation.takes_space() && next.takes_space()) - || (overlaps(next, annotation, l) - && next.range.1 <= annotation.range.1 - && next.has_label() - && p == 0) - // Avoid #42595. - { - // This annotation needs a new line in the output. - p += 1; - break; - } - } - } - line_len = max(line_len, p); - } - - if line_len != 0 { - line_len += 1; - } - - if annotations_positions.iter().all(|(_, ann)| { - matches!( - ann.annotation_part, - DisplayAnnotationPart::MultilineStart(_) - ) - }) { - if let Some(max_pos) = - annotations_positions.iter().map(|(pos, _)| *pos).max() - { - // Special case the following, so that we minimize overlapping multiline spans. - // - // 3 │ X0 Y0 Z0 - // │ ┏━━━━━┛ │ │ < We are writing these lines - // │ ┃┌───────┘ │ < by reverting the "depth" of - // │ ┃│┌─────────┘ < their multiline spans. - // 4 │ ┃││ X1 Y1 Z1 - // 5 │ ┃││ X2 Y2 Z2 - // │ ┃│└────╿──│──┘ `Z` label - // │ ┃└─────│──┤ - // │ ┗━━━━━━┥ `Y` is a good letter too - // ╰╴ `X` is a good letter - for (pos, _) in &mut annotations_positions { - *pos = max_pos - *pos; - } - // We know then that we don't need an additional line for the span label, saving us - // one line of vertical space. - line_len = line_len.saturating_sub(1); - } - } - - // This is a special case where we have a multiline - // annotation that is at the start of the line disregarding - // any leading whitespace, and no other multiline - // annotations overlap it. In this case, we want to draw - // - // 2 | fn foo() { - // | _^ - // 3 | | - // 4 | | } - // | |_^ test - // - // we simplify the output to: - // - // 2 | / fn foo() { - // 3 | | - // 4 | | } - // | |_^ test - if multiline_depth == 1 - && annotations_positions.len() == 1 - && annotations_positions - .first() - .map_or(false, |(_, annotation)| { - matches!( - annotation.annotation_part, - DisplayAnnotationPart::MultilineStart(_) - ) && text - .chars() - .take(annotation.range.0) - .all(|c| c.is_whitespace()) - }) - { - let (_, ann) = annotations_positions.remove(0); - let style = get_annotation_style(&ann.annotation_type, stylesheet); - buffer.putc(line_offset, 3 + lineno_width, '/', *style); - } - - // Draw the column separator for any extra lines that were - // created - // - // After this we will have: - // - // 2 | fn foo() { - // | - // | - // | - // 3 | - // 4 | } - // | - if !annotations_positions.is_empty() { - for pos in 0..=line_len { - buffer.putc( - line_offset + pos + 1, - lineno_width + 1, - '|', - stylesheet.line_no, - ); - } - } - - // Write the horizontal lines for multiline annotations - // (only the first and last lines need this). - // - // After this we will have: - // - // 2 | fn foo() { - // | __________ - // | - // | - // 3 | - // 4 | } - // | _ - for &(pos, annotation) in &annotations_positions { - let style = get_annotation_style(&annotation.annotation_type, stylesheet); - let pos = pos + 1; - match annotation.annotation_part { - DisplayAnnotationPart::MultilineStart(depth) - | DisplayAnnotationPart::MultilineEnd(depth) => { - for col in width_offset + depth - ..(code_offset + annotation.range.0).saturating_sub(left) - { - buffer.putc(line_offset + pos, col + 1, '_', *style); - } - } - _ => {} - } - } - - // Write the vertical lines for labels that are on a different line as the underline. - // - // After this we will have: - // - // 2 | fn foo() { - // | __________ - // | | | - // | | - // 3 | | - // 4 | | } - // | |_ - for &(pos, annotation) in &annotations_positions { - let style = get_annotation_style(&annotation.annotation_type, stylesheet); - let pos = pos + 1; - if pos > 1 && (annotation.has_label() || annotation.takes_space()) { - for p in line_offset + 2..=line_offset + pos { - buffer.putc( - p, - (code_offset + annotation.range.0).saturating_sub(left), - '|', - *style, - ); - } - } - match annotation.annotation_part { - DisplayAnnotationPart::MultilineStart(depth) => { - for p in line_offset + pos + 1..line_offset + line_len + 2 { - buffer.putc(p, width_offset + depth, '|', *style); - } - } - DisplayAnnotationPart::MultilineEnd(depth) => { - for p in line_offset..=line_offset + pos { - buffer.putc(p, width_offset + depth, '|', *style); - } - } - _ => {} - } - } - - // Add in any inline marks for any extra lines that have - // been created. Output should look like above. - for inline_mark in inline_marks { - let DisplayMarkType::AnnotationThrough(depth) = inline_mark.mark_type; - let style = get_annotation_style(&inline_mark.annotation_type, stylesheet); - if annotations_positions.is_empty() { - buffer.putc(line_offset, width_offset + depth, '|', *style); - } else { - for p in line_offset..=line_offset + line_len + 1 { - buffer.putc(p, width_offset + depth, '|', *style); - } - } - } - - // Write the labels on the annotations that actually have a label. - // - // After this we will have: - // - // 2 | fn foo() { - // | __________ - // | | - // | something about `foo` - // 3 | - // 4 | } - // | _ test - for &(pos, annotation) in &annotations_positions { - if !is_annotation_empty(&annotation.annotation) { - let style = - get_annotation_style(&annotation.annotation_type, stylesheet); - let mut formatted_len = if let Some(id) = &annotation.annotation.id { - 2 + id.id.len() - + annotation_type_len(&annotation.annotation.annotation_type) - } else { - annotation_type_len(&annotation.annotation.annotation_type) - }; - let (pos, col) = if pos == 0 { - (pos + 1, (annotation.range.1 + 1).saturating_sub(left)) - } else { - (pos + 2, annotation.range.0.saturating_sub(left)) - }; - if annotation.annotation_part - == DisplayAnnotationPart::LabelContinuation - { - formatted_len = 0; - } else if formatted_len != 0 { - formatted_len += 2; - let id = match &annotation.annotation.id { - Some(id) => format!( - "[{id}]", - id = fmt_with_hyperlink(&id.id, id.url, stylesheet) - ), - None => String::new(), - }; - buffer.puts( - line_offset + pos, - col + code_offset, - &format!( - "{}{}: ", - annotation_type_str(&annotation.annotation_type), - id - ), - *style, - ); - } else { - formatted_len = 0; - } - let mut before = 0; - for fragment in &annotation.annotation.label { - let inner_col = before + formatted_len + col + code_offset; - buffer.puts(line_offset + pos, inner_col, fragment.content, *style); - before += fragment.content.len(); - } - } - } - - // Sort from biggest span to smallest span so that smaller spans are - // represented in the output: - // - // x | fn foo() - // | ^^^---^^ - // | | | - // | | something about `foo` - // | something about `fn foo()` - annotations_positions.sort_by_key(|(_, ann)| { - // Decreasing order. When annotations share the same length, prefer `Primary`. - Reverse(ann.len()) - }); - - // Write the underlines. - // - // After this we will have: - // - // 2 | fn foo() { - // | ____-_____^ - // | | - // | something about `foo` - // 3 | - // 4 | } - // | _^ test - for &(_, annotation) in &annotations_positions { - let mark = match annotation.annotation_type { - DisplayAnnotationType::Error => '^', - DisplayAnnotationType::Warning => '-', - DisplayAnnotationType::Info => '-', - DisplayAnnotationType::Note => '-', - DisplayAnnotationType::Help => '-', - DisplayAnnotationType::None => ' ', - }; - let style = get_annotation_style(&annotation.annotation_type, stylesheet); - for p in annotation.range.0..annotation.range.1 { - buffer.putc( - line_offset + 1, - (code_offset + p).saturating_sub(left), - mark, - *style, - ); - } - } - } else if !inline_marks.is_empty() { - format_inline_marks( - line_offset, - inline_marks, - lineno_width, - stylesheet, - buffer, - )?; - } - Ok(()) - } - DisplayLine::Fold { inline_marks } => { - buffer.puts(line_offset, 0, cut_indicator, *stylesheet.line_no()); - if !inline_marks.is_empty() || 0 < multiline_depth { - format_inline_marks( - line_offset, - inline_marks, - lineno_width, - stylesheet, - buffer, - )?; - } - Ok(()) - } - DisplayLine::Raw(line) => self.format_raw_line( - line_offset, - line, - lineno_width, - stylesheet, - anonymized_line_numbers, - buffer, - ), - } - } -} - -/// Inline annotation which can be used in either Raw or Source line. -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct Annotation<'a> { - pub(crate) annotation_type: DisplayAnnotationType, - pub(crate) id: Option>, - pub(crate) label: Vec>, - pub(crate) is_fixable: bool, -} - -/// A single line used in `DisplayList`. -#[derive(Debug, PartialEq)] -pub(crate) enum DisplayLine<'a> { - /// A line with `lineno` portion of the slice. - Source { - lineno: Option, - inline_marks: Vec, - line: DisplaySourceLine<'a>, - annotations: Vec>, - }, - - /// A line indicating a folded part of the slice. - Fold { inline_marks: Vec }, - - /// A line which is displayed outside of slices. - Raw(DisplayRawLine<'a>), -} - -/// A source line. -#[derive(Debug, PartialEq)] -pub(crate) enum DisplaySourceLine<'a> { - /// A line with the content of the Snippet. - Content { - text: &'a str, - range: (usize, usize), // meta information for annotation placement. - end_line: EndLine, - }, - /// An empty source line. - Empty, -} - -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct DisplaySourceAnnotation<'a> { - pub(crate) annotation: Annotation<'a>, - pub(crate) range: (usize, usize), - pub(crate) annotation_type: DisplayAnnotationType, - pub(crate) annotation_part: DisplayAnnotationPart, -} - -impl DisplaySourceAnnotation<'_> { - fn has_label(&self) -> bool { - !self - .annotation - .label - .iter() - .all(|label| label.content.is_empty()) - } - - // Length of this annotation as displayed in the stderr output - fn len(&self) -> usize { - // Account for usize underflows - self.range.1.abs_diff(self.range.0) - } - - fn takes_space(&self) -> bool { - // Multiline annotations always have to keep vertical space. - matches!( - self.annotation_part, - DisplayAnnotationPart::MultilineStart(_) | DisplayAnnotationPart::MultilineEnd(_) - ) - } -} - -#[derive(Debug, PartialEq)] -pub(crate) struct Position { - row: usize, - col: usize, - cell: Option, -} - -/// Raw line - a line which does not have the `lineno` part and is not considered -/// a part of the snippet. -#[derive(Debug, PartialEq)] -pub(crate) enum DisplayRawLine<'a> { - /// A line which provides information about the location of the given - /// slice in the project structure. - Origin { - path: &'a str, - pos: Option, - header_type: DisplayHeaderType, - }, - - /// An annotation line which is not part of any snippet. - Annotation { - annotation: Annotation<'a>, - - /// If set to `true`, the annotation will be aligned to the - /// lineno delimiter of the snippet. - source_aligned: bool, - /// If set to `true`, only the label of the `Annotation` will be - /// displayed. It allows for a multiline annotation to be aligned - /// without displaying the meta information (`type` and `id`) to be - /// displayed on each line. - continuation: bool, - }, -} - -/// An inline text fragment which any label is composed of. -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct DisplayTextFragment<'a> { - pub(crate) content: &'a str, - pub(crate) style: DisplayTextStyle, -} - -/// A style for the `DisplayTextFragment` which can be visually formatted. -/// -/// This information may be used to emphasis parts of the label. -#[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) enum DisplayTextStyle { - Regular, - Emphasis, -} - -/// An indicator of what part of the annotation a given `Annotation` is. -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DisplayAnnotationPart { - /// A standalone, single-line annotation. - Standalone, - /// A continuation of a multi-line label of an annotation. - LabelContinuation, - /// A line starting a multiline annotation. - MultilineStart(usize), - /// A line ending a multiline annotation. - MultilineEnd(usize), -} - -/// A visual mark used in `inline_marks` field of the `DisplaySourceLine`. -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct DisplayMark { - pub(crate) mark_type: DisplayMarkType, - pub(crate) annotation_type: DisplayAnnotationType, -} - -/// A type of the `DisplayMark`. -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DisplayMarkType { - /// A mark indicating a multiline annotation going through the current line. - AnnotationThrough(usize), -} - -/// A type of the `Annotation` which may impact the sigils, style or text displayed. -/// -/// There are several ways to uses this information when formatting the `DisplayList`: -/// -/// * An annotation may display the name of the type like `error` or `info`. -/// * An underline for `Error` may be `^^^` while for `Warning` it could be `---`. -/// * `ColorStylesheet` may use different colors for different annotations. -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DisplayAnnotationType { - None, - Error, - Warning, - Info, - Note, - Help, -} - -impl DisplayAnnotationType { - #[inline] - const fn is_none(&self) -> bool { - matches!(self, Self::None) - } -} - -impl From for DisplayAnnotationType { - fn from(at: snippet::Level) -> Self { - match at { - snippet::Level::None => DisplayAnnotationType::None, - snippet::Level::Error => DisplayAnnotationType::Error, - snippet::Level::Warning => DisplayAnnotationType::Warning, - snippet::Level::Info => DisplayAnnotationType::Info, - snippet::Level::Note => DisplayAnnotationType::Note, - snippet::Level::Help => DisplayAnnotationType::Help, - } - } -} - -/// Information whether the header is the initial one or a consecutive one -/// for multi-slice cases. -// TODO: private -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum DisplayHeaderType { - /// Initial header is the first header in the snippet. - Initial, - - /// Continuation marks all headers of following slices in the snippet. - Continuation, -} - -struct CursorLines<'a>(&'a str); - -impl CursorLines<'_> { - fn new(src: &str) -> CursorLines<'_> { - CursorLines(src) - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub(crate) enum EndLine { - Eof, - Lf, - Crlf, -} - -impl EndLine { - /// The number of characters this line ending occupies in bytes. - pub(crate) fn len(self) -> usize { - match self { - EndLine::Eof => 0, - EndLine::Lf => 1, - EndLine::Crlf => 2, - } - } -} - -impl<'a> Iterator for CursorLines<'a> { - type Item = (&'a str, EndLine); - - fn next(&mut self) -> Option { - if self.0.is_empty() { - None - } else { - self.0 - .find('\n') - .map(|x| { - let ret = if 0 < x { - if self.0.as_bytes()[x - 1] == b'\r' { - (&self.0[..x - 1], EndLine::Crlf) - } else { - (&self.0[..x], EndLine::Lf) - } - } else { - ("", EndLine::Lf) - }; - self.0 = &self.0[x + 1..]; - ret - }) - .or_else(|| { - let ret = Some((self.0, EndLine::Eof)); - self.0 = ""; - ret - }) - } - } -} - -fn format_message<'m>( - message: snippet::Message<'m>, - term_width: usize, - anonymized_line_numbers: bool, - cut_indicator: &'static str, - primary: bool, -) -> Vec> { - let snippet::Message { - level, - id, - title, - footer, - snippets, - is_fixable, - lineno_offset: _, - } = message; - - let mut sets = vec![]; - let body = if !snippets.is_empty() || primary { - vec![format_title(level, id, title, is_fixable)] - } else { - format_footer(level, id, title) - }; - - for (idx, snippet) in snippets.into_iter().enumerate() { - let snippet = fold_prefix_suffix(snippet); - sets.push(format_snippet( - snippet, - idx == 0, - !footer.is_empty(), - term_width, - anonymized_line_numbers, - cut_indicator, - )); - } - - if let Some(first) = sets.first_mut() { - for line in body { - first.display_lines.insert(0, line); - } - } else { - sets.push(DisplaySet { - display_lines: body, - margin: Margin::new(0, 0, 0, 0, DEFAULT_TERM_WIDTH, 0), - }); - } - - for annotation in footer { - sets.extend(format_message( - annotation, - term_width, - anonymized_line_numbers, - cut_indicator, - false, - )); - } - - sets -} - -fn format_title<'a>( - level: crate::Level, - id: Option>, - label: &'a str, - is_fixable: bool, -) -> DisplayLine<'a> { - DisplayLine::Raw(DisplayRawLine::Annotation { - annotation: Annotation { - annotation_type: DisplayAnnotationType::from(level), - id, - label: format_label(Some(label), Some(DisplayTextStyle::Emphasis)), - is_fixable, - }, - source_aligned: false, - continuation: false, - }) -} - -fn format_footer<'a>( - level: crate::Level, - id: Option>, - label: &'a str, -) -> Vec> { - let mut result = vec![]; - for (i, line) in label.lines().enumerate() { - result.push(DisplayLine::Raw(DisplayRawLine::Annotation { - annotation: Annotation { - annotation_type: DisplayAnnotationType::from(level), - id, - label: format_label(Some(line), None), - is_fixable: false, - }, - source_aligned: true, - continuation: i != 0, - })); - } - result -} - -fn format_label( - label: Option<&str>, - style: Option, -) -> Vec> { - let mut result = vec![]; - if let Some(label) = label { - let element_style = style.unwrap_or(DisplayTextStyle::Regular); - result.push(DisplayTextFragment { - content: label, - style: element_style, - }); - } - result -} - -fn format_snippet<'m>( - snippet: snippet::Snippet<'m>, - is_first: bool, - has_footer: bool, - term_width: usize, - anonymized_line_numbers: bool, - cut_indicator: &'static str, -) -> DisplaySet<'m> { - let main_range = snippet.annotations.first().map(|x| x.range.start); - let origin = snippet.origin; - let need_empty_header = origin.is_some() || is_first; - - let is_file_level = snippet.annotations.iter().any(|ann| ann.is_file_level); - if is_file_level { - // TODO(brent) enable this assertion again once we set `is_file_level` for individual rules. - // It's causing too many false positives currently when the default is to make any - // annotation with a default range file-level. See - // https://github.com/astral-sh/ruff/issues/19688. - // - // assert!( - // snippet.source.is_empty(), - // "Non-empty file-level snippet that won't be rendered: {:?}", - // snippet.source - // ); - let header = format_header(origin, main_range, &[], is_first, snippet.cell_index); - return DisplaySet { - display_lines: header.map_or_else(Vec::new, |header| vec![header]), - margin: Margin::new(0, 0, 0, 0, term_width, 0), - }; - } - - let cell_index = snippet.cell_index; - - let mut body = format_body( - snippet, - need_empty_header, - has_footer, - term_width, - anonymized_line_numbers, - cut_indicator, - ); - let header = format_header( - origin, - main_range, - &body.display_lines, - is_first, - cell_index, - ); - - if let Some(header) = header { - body.display_lines.insert(0, header); - } - - body -} - -fn format_header<'a>( - origin: Option<&'a str>, - main_range: Option, - body: &[DisplayLine<'_>], - is_first: bool, - cell_index: Option, -) -> Option> { - let display_header = if is_first { - DisplayHeaderType::Initial - } else { - DisplayHeaderType::Continuation - }; - - if let Some((main_range, path)) = main_range.zip(origin) { - let mut col = 1; - let mut line_offset = 1; - - for item in body { - if let DisplayLine::Source { - line: - DisplaySourceLine::Content { - text, - range, - end_line, - }, - lineno, - .. - } = item - { - // At the very end of the `main_range`, report the location as the first character - // in the next line instead of falling back to the default location of `1:1`. This - // is another divergence from upstream. - let end_of_range = range.1 + max(*end_line as usize, 1); - if main_range >= range.0 && main_range < end_of_range { - let char_column = text[0..(main_range - range.0).min(text.len())] - .chars() - .count(); - col = char_column + 1; - line_offset = lineno.unwrap_or(1); - break; - } else if main_range == end_of_range { - line_offset = lineno.map_or(1, |line| line + 1); - break; - } - } - } - - return Some(DisplayLine::Raw(DisplayRawLine::Origin { - path, - pos: Some(Position { - row: line_offset, - col, - cell: cell_index, - }), - header_type: display_header, - })); - } - - if let Some(path) = origin { - return Some(DisplayLine::Raw(DisplayRawLine::Origin { - path, - pos: None, - header_type: display_header, - })); - } - - None -} - -fn fold_prefix_suffix(mut snippet: snippet::Snippet<'_>) -> snippet::Snippet<'_> { - if !snippet.fold { - return snippet; - } - - let ann_start = snippet - .annotations - .iter() - .map(|ann| ann.range.start) - .min() - .unwrap_or(0); - if let Some(before_new_start) = snippet.source[0..ann_start].rfind('\n') { - let new_start = before_new_start + 1; - - let line_offset = newline_count(&snippet.source[..new_start]); - snippet.line_start += line_offset; - - snippet.source = &snippet.source[new_start..]; - - for ann in &mut snippet.annotations { - let range_start = ann.range.start - new_start; - let range_end = ann.range.end - new_start; - ann.range = range_start..range_end; - } - } - - let ann_end = snippet - .annotations - .iter() - .map(|ann| ann.range.end) - .max() - .unwrap_or(snippet.source.len()); - if let Some(end_offset) = snippet.source[ann_end..].find('\n') { - let new_end = ann_end + end_offset; - snippet.source = &snippet.source[..new_end]; - } - - snippet -} - -fn newline_count(body: &str) -> usize { - memchr::memchr_iter(b'\n', body.as_bytes()).count() -} - -fn fold_body(body: Vec>) -> Vec> { - const INNER_CONTEXT: usize = 1; - const INNER_UNFOLD_SIZE: usize = INNER_CONTEXT * 2 + 1; - - let mut lines = vec![]; - let mut unhighlighted_lines = vec![]; - for line in body { - match &line { - DisplayLine::Source { annotations, .. } => { - if annotations.is_empty() { - unhighlighted_lines.push(line); - } else { - if lines.is_empty() { - // Ignore leading unhighlighted lines - unhighlighted_lines.clear(); - } - match unhighlighted_lines.len() { - 0 => {} - n if n <= INNER_UNFOLD_SIZE => { - // Rather than render our cut indicator, don't fold - lines.append(&mut unhighlighted_lines); - } - _ => { - lines.extend(unhighlighted_lines.drain(..INNER_CONTEXT)); - let inline_marks = lines - .last() - .and_then(|line| { - if let DisplayLine::Source { inline_marks, .. } = line { - let inline_marks = inline_marks.clone(); - Some(inline_marks) - } else { - None - } - }) - .unwrap_or_default(); - lines.push(DisplayLine::Fold { - inline_marks: inline_marks.clone(), - }); - unhighlighted_lines - .drain(..unhighlighted_lines.len().saturating_sub(INNER_CONTEXT)); - lines.append(&mut unhighlighted_lines); - } - } - lines.push(line); - } - } - _ => { - unhighlighted_lines.push(line); - } - } - } - - lines -} - -fn format_body<'m>( - snippet: snippet::Snippet<'m>, - need_empty_header: bool, - has_footer: bool, - term_width: usize, - anonymized_line_numbers: bool, - cut_indicator: &'static str, -) -> DisplaySet<'m> { - let source_len = snippet.source.len(); - if let Some(bigger) = snippet.annotations.iter().find_map(|x| { - // Allow highlighting one past the last character in the source. - if source_len + 1 < x.range.end { - Some(&x.range) - } else { - None - } - }) { - panic!("SourceAnnotation range `{bigger:?}` is beyond the end of buffer `{source_len}`") - } - - let mut body = vec![]; - let mut current_line = snippet.line_start; - let mut current_index = 0; - - let mut whitespace_margin = usize::MAX; - let mut span_left_margin = usize::MAX; - let mut span_right_margin = 0; - let mut label_right_margin = 0; - let mut max_line_len = 0; - - let mut depth_map: HashMap = HashMap::new(); - let mut current_depth = 0; - let mut annotations = snippet.annotations; - let ranges = annotations - .iter() - .map(|a| a.range.clone()) - .collect::>(); - // We want to merge multiline annotations that have the same range into one - // multiline annotation to save space. This is done by making any duplicate - // multiline annotations into a single-line annotation pointing at the end - // of the range. - // - // 3 | X0 Y0 Z0 - // | _____^ - // | | ____| - // | || ___| - // | ||| - // 4 | ||| X1 Y1 Z1 - // 5 | ||| X2 Y2 Z2 - // | ||| ^ - // | |||____| - // | ||____`X` is a good letter - // | |____`Y` is a good letter too - // | `Z` label - // Should be - // error: foo - // --> test.rs:3:3 - // | - // 3 | / X0 Y0 Z0 - // 4 | | X1 Y1 Z1 - // 5 | | X2 Y2 Z2 - // | | ^ - // | |____| - // | `X` is a good letter - // | `Y` is a good letter too - // | `Z` label - // | - ranges.iter().enumerate().for_each(|(r_idx, range)| { - annotations - .iter_mut() - .enumerate() - .skip(r_idx + 1) - .for_each(|(ann_idx, ann)| { - // Skip if the annotation's index matches the range index - if ann_idx != r_idx - // We only want to merge multiline annotations - && snippet.source[ann.range.clone()].lines().count() > 1 - // We only want to merge annotations that have the same range - && ann.range.start == range.start - && ann.range.end == range.end - { - ann.range.start = ann.range.end.saturating_sub(1); - } - }); - }); - annotations.sort_by_key(|a| a.range.start); - let mut annotations = annotations.into_iter().enumerate().collect::>(); - - for (idx, (line, end_line)) in CursorLines::new(snippet.source).enumerate() { - let line_length: usize = line.len(); - let line_range = (current_index, current_index + line_length); - let end_line_size = end_line.len(); - - body.push(DisplayLine::Source { - lineno: Some(current_line), - inline_marks: vec![], - line: DisplaySourceLine::Content { - text: line, - range: line_range, - end_line, - }, - annotations: vec![], - }); - - let leading_whitespace = line - .chars() - .take_while(|c| c.is_whitespace()) - .map(|c| { - match c { - // Tabs are displayed as 4 spaces - '\t' => 4, - _ => 1, - } - }) - .sum(); - whitespace_margin = min(whitespace_margin, leading_whitespace); - max_line_len = max(max_line_len, line_length); - - let line_start_index = line_range.0; - let line_end_index = line_range.1; - current_line += 1; - current_index += line_length + end_line_size; - - // It would be nice to use filter_drain here once it's stable. - annotations.retain(|(key, annotation)| { - let body_idx = idx; - let annotation_type = match annotation.level { - snippet::Level::Error => DisplayAnnotationType::None, - snippet::Level::Warning => DisplayAnnotationType::None, - _ => DisplayAnnotationType::from(annotation.level), - }; - let label_right = annotation.label.map_or(0, |label| label.len() + 1); - match annotation.range { - // This handles if the annotation is on the next line. We add - // the `end_line_size` to account for annotating the line end. - Range { start, .. } if start > line_end_index + end_line_size => true, - // This handles the case where an annotation is contained - // within the current line including any line-end characters. - Range { start, end } - if start >= line_start_index - // We add at least one to `line_end_index` to allow - // highlighting the end of a file - && end <= line_end_index + max(end_line_size, 1) => - { - if let DisplayLine::Source { - ref mut annotations, - .. - } = body[body_idx] - { - let annotation_start_col = line - [0..(start - line_start_index).min(line_length)] - .chars() - .map(|c| char_width(c).unwrap_or(0)) - .sum::(); - let mut annotation_end_col = line - [0..(end - line_start_index).min(line_length)] - .chars() - .map(|c| char_width(c).unwrap_or(0)) - .sum::(); - if annotation_start_col == annotation_end_col { - // At least highlight something - annotation_end_col += 1; - } - - span_left_margin = min(span_left_margin, annotation_start_col); - span_right_margin = max(span_right_margin, annotation_end_col); - label_right_margin = - max(label_right_margin, annotation_end_col + label_right); - - let range = (annotation_start_col, annotation_end_col); - annotations.push(DisplaySourceAnnotation { - annotation: Annotation { - annotation_type, - id: None, - label: format_label(annotation.label, None), - is_fixable: false, - }, - range, - annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::Standalone, - }); - } - false - } - // This handles the case where a multiline annotation starts - // somewhere on the current line, including any line-end chars - Range { start, end } - if start >= line_start_index - // The annotation can start on a line ending - && start <= line_end_index + end_line_size.saturating_sub(1) - && end > line_end_index => - { - if let DisplayLine::Source { - ref mut annotations, - .. - } = body[body_idx] - { - let annotation_start_col = line - [0..(start - line_start_index).min(line_length)] - .chars() - .map(|c| char_width(c).unwrap_or(0)) - .sum::(); - let annotation_end_col = annotation_start_col + 1; - - span_left_margin = min(span_left_margin, annotation_start_col); - span_right_margin = max(span_right_margin, annotation_end_col); - label_right_margin = - max(label_right_margin, annotation_end_col + label_right); - - let range = (annotation_start_col, annotation_end_col); - annotations.push(DisplaySourceAnnotation { - annotation: Annotation { - annotation_type, - id: None, - label: vec![], - is_fixable: false, - }, - range, - annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::MultilineStart(current_depth), - }); - depth_map.insert(*key, current_depth); - current_depth += 1; - } - true - } - // This handles the case where a multiline annotation starts - // somewhere before this line and ends after it as well - Range { start, end } - if start < line_start_index && end > line_end_index + max(end_line_size, 1) => - { - if let DisplayLine::Source { - ref mut inline_marks, - .. - } = body[body_idx] - { - let depth = depth_map.get(key).cloned().unwrap_or_default(); - inline_marks.push(DisplayMark { - mark_type: DisplayMarkType::AnnotationThrough(depth), - annotation_type: DisplayAnnotationType::from(annotation.level), - }); - } - true - } - // This handles the case where a multiline annotation ends - // somewhere on the current line, including any line-end chars - Range { start, end } - if start < line_start_index - && end >= line_start_index - // We add at least one to `line_end_index` to allow - // highlighting the end of a file - && end <= line_end_index + max(end_line_size, 1) => - { - if let DisplayLine::Source { - ref mut annotations, - .. - } = body[body_idx] - { - let end_mark = line[0..(end - line_start_index).min(line_length)] - .chars() - .map(|c| char_width(c).unwrap_or(0)) - .sum::() - .saturating_sub(1); - // If the annotation ends on a line-end character, we - // need to annotate one past the end of the line - let (end_mark, end_plus_one) = if end > line_end_index - // Special case for highlighting the end of a file - || (end == line_end_index + 1 && end_line_size == 0) - { - (end_mark + 1, end_mark + 2) - } else { - (end_mark, end_mark + 1) - }; - - span_left_margin = min(span_left_margin, end_mark); - span_right_margin = max(span_right_margin, end_plus_one); - label_right_margin = max(label_right_margin, end_plus_one + label_right); - - let range = (end_mark, end_plus_one); - let depth = depth_map.remove(key).unwrap_or(0); - annotations.push(DisplaySourceAnnotation { - annotation: Annotation { - annotation_type, - id: None, - - label: format_label(annotation.label, None), - is_fixable: false, - }, - range, - annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::MultilineEnd(depth), - }); - } - false - } - _ => true, - } - }); - // Reset the depth counter, but only after we've processed all - // annotations for a given line. - let max = depth_map.len(); - if current_depth > max { - current_depth = max; - } - } - - if snippet.fold { - body = fold_body(body); - } - - if need_empty_header { - body.insert( - 0, - DisplayLine::Source { - lineno: None, - inline_marks: vec![], - line: DisplaySourceLine::Empty, - annotations: vec![], - }, - ); - } - - if has_footer { - body.push(DisplayLine::Source { - lineno: None, - inline_marks: vec![], - line: DisplaySourceLine::Empty, - annotations: vec![], - }); - } else if let Some(DisplayLine::Source { .. }) = body.last() { - body.push(DisplayLine::Source { - lineno: None, - inline_marks: vec![], - line: DisplaySourceLine::Empty, - annotations: vec![], - }); - } - let max_line_num_len = if anonymized_line_numbers { - ANONYMIZED_LINE_NUM.len() - } else { - current_line.to_string().len() - }; - - let width_offset = cut_indicator.len() + max_line_num_len; - - if span_left_margin == usize::MAX { - span_left_margin = 0; - } - - let margin = Margin::new( - whitespace_margin, - span_left_margin, - span_right_margin, - label_right_margin, - term_width.saturating_sub(width_offset), - max_line_len, - ); - - DisplaySet { - display_lines: body, - margin, - } -} - -#[inline] -fn annotation_type_str(annotation_type: &DisplayAnnotationType) -> &'static str { - match annotation_type { - DisplayAnnotationType::Error => ERROR_TXT, - DisplayAnnotationType::Help => HELP_TXT, - DisplayAnnotationType::Info => INFO_TXT, - DisplayAnnotationType::Note => NOTE_TXT, - DisplayAnnotationType::Warning => WARNING_TXT, - DisplayAnnotationType::None => "", - } -} - -fn annotation_type_len(annotation_type: &DisplayAnnotationType) -> usize { - match annotation_type { - DisplayAnnotationType::Error => ERROR_TXT.len(), - DisplayAnnotationType::Help => HELP_TXT.len(), - DisplayAnnotationType::Info => INFO_TXT.len(), - DisplayAnnotationType::Note => NOTE_TXT.len(), - DisplayAnnotationType::Warning => WARNING_TXT.len(), - DisplayAnnotationType::None => 0, - } -} - -fn get_annotation_style<'a>( - annotation_type: &DisplayAnnotationType, - stylesheet: &'a Stylesheet, -) -> &'a Style { - match annotation_type { - DisplayAnnotationType::Error => stylesheet.error(), - DisplayAnnotationType::Warning => stylesheet.warning(), - DisplayAnnotationType::Info => stylesheet.info(), - DisplayAnnotationType::Note => stylesheet.note(), - DisplayAnnotationType::Help => stylesheet.help(), - DisplayAnnotationType::None => stylesheet.none(), - } -} - -#[inline] -fn is_annotation_empty(annotation: &Annotation<'_>) -> bool { - annotation - .label - .iter() - .all(|fragment| fragment.content.is_empty()) -} - -// We replace some characters so the CLI output is always consistent and underlines aligned. -const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[ - ('\t', " "), // We do our own tab replacement - ('\u{200D}', ""), // Replace ZWJ with nothing for consistent terminal output of grapheme clusters. - ('\u{202A}', ""), // The following unicode text flow control characters are inconsistently - ('\u{202B}', ""), // supported across CLIs and can cause confusion due to the bytes on disk - ('\u{202D}', ""), // not corresponding to the visible source code, so we replace them always. - ('\u{202E}', ""), - ('\u{2066}', ""), - ('\u{2067}', ""), - ('\u{2068}', ""), - ('\u{202C}', ""), - ('\u{2069}', ""), -]; - -fn normalize_whitespace(str: &str) -> Cow<'_, str> { - // This is an optimization to avoid repeated `str::replace` calls in the typical case of no - // valid replacements. Note that this list needs to be kept in sync with `OUTPUT_REPLACEMENTS`. - if !str.contains([ - '\t', '\u{200d}', '\u{202a}', '\u{202b}', '\u{202d}', '\u{202e}', '\u{2066}', '\u{2067}', - '\u{2068}', '\u{202c}', '\u{2069}', - ]) { - return Cow::Borrowed(str); - } - - let mut s = str.to_owned(); - for (c, replacement) in OUTPUT_REPLACEMENTS { - s = s.replace(*c, replacement); - } - Cow::Owned(s) -} - -fn overlaps( - a1: &DisplaySourceAnnotation<'_>, - a2: &DisplaySourceAnnotation<'_>, - padding: usize, -) -> bool { - (a2.range.0..a2.range.1).contains(&a1.range.0) - || (a1.range.0..a1.range.1 + padding).contains(&a2.range.0) -} - -fn format_inline_marks( - line: usize, - inline_marks: &[DisplayMark], - lineno_width: usize, - stylesheet: &Stylesheet, - buf: &mut StyledBuffer, -) -> fmt::Result { - for mark in inline_marks.iter() { - let annotation_style = get_annotation_style(&mark.annotation_type, stylesheet); - match mark.mark_type { - DisplayMarkType::AnnotationThrough(depth) => { - buf.putc(line, 3 + lineno_width + depth, '|', *annotation_style); - } - } - } - Ok(()) -} - -fn char_width(c: char) -> Option { - if c == '\t' { - Some(4) - } else { - unicode_width::UnicodeWidthChar::width(c) - } -} - -pub(super) fn fmt_with_hyperlink<'a, T>( - content: T, - url: Option<&'a str>, - stylesheet: &Stylesheet, -) -> impl std::fmt::Display + 'a -where - T: std::fmt::Display + 'a, -{ - let url = if stylesheet.hyperlink { url } else { None }; - - fmt::from_fn(move |f| { - if let Some(url) = url { - write!(f, "\x1B]8;;{url}\x1B\\")?; - } - - content.fmt(f)?; - - if url.is_some() { - f.write_str("\x1B]8;;\x1B\\")?; - } - - Ok(()) - }) -} diff --git a/crates/ruff_annotate_snippets/src/renderer/margin.rs b/crates/ruff_annotate_snippets/src/renderer/margin.rs index 40e94e5048..32503f99e5 100644 --- a/crates/ruff_annotate_snippets/src/renderer/margin.rs +++ b/crates/ruff_annotate_snippets/src/renderer/margin.rs @@ -1,4 +1,4 @@ -use std::cmp::{max, min}; +use core::cmp::{max, min}; const ELLIPSIS_PASSING: usize = 6; const LONG_WHITESPACE: usize = 20; @@ -17,7 +17,7 @@ pub(crate) struct Margin { /// The end of the line to be displayed. computed_right: usize, /// The current width of the terminal. 140 by default and in tests. - term_width: usize, + pub(crate) term_width: usize, /// The end column of a span label, including the span. Doesn't account for labels not in the /// same line as the span. label_right: usize, @@ -41,15 +41,9 @@ impl Margin { // | ^^^^^^^^^ // ``` - let whitespace_left = whitespace_left.saturating_sub(ELLIPSIS_PASSING); - let span_left = span_left.saturating_sub(ELLIPSIS_PASSING); - let mut m = Margin { - // When an annotation points at leading whitespace (e.g. an indentation error), - // `whitespace_left` can exceed `span_left`. Clamp it so that trimming whitespace - // never hides the leftmost annotation. - whitespace_left: min(whitespace_left, span_left), - span_left, + whitespace_left: whitespace_left.saturating_sub(ELLIPSIS_PASSING), + span_left: span_left.saturating_sub(ELLIPSIS_PASSING), span_right: span_right + ELLIPSIS_PASSING, computed_left: 0, computed_right: 0, @@ -77,7 +71,12 @@ impl Margin { if self.computed_right - self.computed_left > self.term_width { // Trimming only whitespace isn't enough, let's get craftier. - if self.label_right - self.whitespace_left <= self.term_width { + if self.label_right.saturating_sub(self.whitespace_left) <= self.term_width + // Trimming whitespace when the right-most label is somewhrere + // within it would result in the label pointing to the wrong + // place + && self.label_right >= self.whitespace_left + { // Attempt to fit the code window only trimming whitespace. self.computed_left = self.whitespace_left; self.computed_right = self.computed_left + self.term_width; diff --git a/crates/ruff_annotate_snippets/src/renderer/mod.rs b/crates/ruff_annotate_snippets/src/renderer/mod.rs index a48af545b6..78db9b993c 100644 --- a/crates/ruff_annotate_snippets/src/renderer/mod.rs +++ b/crates/ruff_annotate_snippets/src/renderer/mod.rs @@ -1,37 +1,118 @@ -//! The renderer for [`Message`]s +//! The [Renderer] and its settings //! //! # Example +//! //! ``` -//! use ruff_annotate_snippets::{Renderer, Snippet, Level}; -//! let snippet = Level::Error.title("mismatched types") -//! .snippet(Snippet::source("Foo").line_start(51).origin("src/format.rs")) -//! .snippet(Snippet::source("Faa").line_start(129).origin("src/display.rs")); +//! # use annotate_snippets::*; +//! # use annotate_snippets::renderer::*; +//! # use annotate_snippets::Level; +//! let report = // ... +//! # &[Group::with_title( +//! # Level::ERROR +//! # .primary_title("unresolved import `baz::zed`") +//! # .id("E0432") +//! # )]; //! -//! let renderer = Renderer::styled(); -//! println!("{}", renderer.render(snippet)); +//! let renderer = Renderer::styled().decor_style(DecorStyle::Unicode); +//! let output = renderer.render(report); +//! anstream::println!("{output}"); //! ``` -mod display_list; +pub(crate) mod render; +pub(crate) mod source_map; +pub(crate) mod stylesheet; + mod margin; mod styled_buffer; -pub(crate) mod stylesheet; -use crate::snippet::Message; +use alloc::string::String; + +use crate::Report; + +pub(crate) use render::ElementStyle; +pub(crate) use render::UnderlineParts; +pub(crate) use render::normalize_whitespace; +pub(crate) use render::{LineAnnotation, LineAnnotationType, char_width, num_overlap}; +pub(crate) use stylesheet::Stylesheet; + pub use anstyle::*; -use display_list::DisplayList; -use margin::Margin; -use std::fmt::Display; -use stylesheet::Stylesheet; +/// See [`Renderer::term_width`] pub const DEFAULT_TERM_WIDTH: usize = 140; -/// A renderer for [`Message`]s +const USE_WINDOWS_COLORS: bool = cfg!(windows) && !cfg!(feature = "testing-colors"); +const BRIGHT_BLUE: Style = if USE_WINDOWS_COLORS { + AnsiColor::BrightCyan.on_default() +} else { + AnsiColor::BrightBlue.on_default() +}; +/// [`Renderer::error`] applied by [`Renderer::styled`] +pub const DEFAULT_ERROR_STYLE: Style = AnsiColor::BrightRed.on_default().effects(Effects::BOLD); +/// [`Renderer::warning`] applied by [`Renderer::styled`] +pub const DEFAULT_WARNING_STYLE: Style = if USE_WINDOWS_COLORS { + AnsiColor::BrightYellow.on_default() +} else { + AnsiColor::Yellow.on_default() +} +.effects(Effects::BOLD); +/// [`Renderer::info`] applied by [`Renderer::styled`] +pub const DEFAULT_INFO_STYLE: Style = BRIGHT_BLUE.effects(Effects::BOLD); +/// [`Renderer::note`] applied by [`Renderer::styled`] +pub const DEFAULT_NOTE_STYLE: Style = AnsiColor::BrightGreen.on_default().effects(Effects::BOLD); +/// [`Renderer::help`] applied by [`Renderer::styled`] +pub const DEFAULT_HELP_STYLE: Style = AnsiColor::BrightCyan.on_default().effects(Effects::BOLD); +/// [`Renderer::line_num`] applied by [`Renderer::styled`] +pub const DEFAULT_LINE_NUM_STYLE: Style = BRIGHT_BLUE.effects(Effects::BOLD); +/// [`Renderer::emphasis`] applied by [`Renderer::styled`] +pub const DEFAULT_EMPHASIS_STYLE: Style = if USE_WINDOWS_COLORS { + AnsiColor::BrightWhite.on_default() +} else { + Style::new() +} +.effects(Effects::BOLD); +/// [`Renderer::none`] applied by [`Renderer::styled`] +pub const DEFAULT_NONE_STYLE: Style = Style::new(); +/// [`Renderer::context`] applied by [`Renderer::styled`] +pub const DEFAULT_CONTEXT_STYLE: Style = BRIGHT_BLUE.effects(Effects::BOLD); +/// [`Renderer::addition`] applied by [`Renderer::styled`] +pub const DEFAULT_ADDITION_STYLE: Style = AnsiColor::BrightGreen.on_default(); +/// [`Renderer::removal`] applied by [`Renderer::styled`] +pub const DEFAULT_REMOVAL_STYLE: Style = AnsiColor::BrightRed.on_default(); + +/// The [Renderer] for a [`Report`] +/// +/// The caller is expected to detect any relevant terminal features and configure the renderer, +/// including +/// - ANSI Escape code support (always outputted with [`Renderer::styled`]) +/// - Terminal width ([`Renderer::term_width`]) +/// - Unicode support ([`Renderer::decor_style`]) +/// +/// # Example +/// +/// ``` +/// # use annotate_snippets::*; +/// # use annotate_snippets::renderer::*; +/// # use annotate_snippets::Level; +/// let report = // ... +/// # &[Group::with_title( +/// # Level::ERROR +/// # .primary_title("unresolved import `baz::zed`") +/// # .id("E0432") +/// # )]; +/// +/// let renderer = Renderer::styled(); +/// let output = renderer.render(report); +/// anstream::println!("{output}"); +/// ``` #[derive(Clone, Debug)] pub struct Renderer { anonymized_line_numbers: bool, term_width: usize, + decor_style: DecorStyle, stylesheet: Stylesheet, - cut_indicator: &'static str, + hyperlink: bool, + short_message: bool, + cut_indicator: Option<&'static str>, } impl Renderer { @@ -40,57 +121,71 @@ impl Renderer { Self { anonymized_line_numbers: false, term_width: DEFAULT_TERM_WIDTH, + decor_style: DecorStyle::Ascii, stylesheet: Stylesheet::plain(), - cut_indicator: "...", + hyperlink: false, + short_message: false, + cut_indicator: None, } } /// Default terminal styling /// + /// If ANSI escape codes are not supported, either + /// - Call [`Renderer::plain`] instead + /// - Strip them after the fact, like with [`anstream`](https://docs.rs/anstream/latest/anstream/) + /// /// # Note + /// /// When testing styled terminal output, see the [`testing-colors` feature](crate#features) pub const fn styled() -> Self { - const USE_WINDOWS_COLORS: bool = cfg!(windows) && !cfg!(feature = "testing-colors"); - const BRIGHT_BLUE: Style = if USE_WINDOWS_COLORS { - AnsiColor::BrightCyan.on_default() - } else { - AnsiColor::BrightBlue.on_default() - }; Self { stylesheet: Stylesheet { - error: AnsiColor::BrightRed.on_default().effects(Effects::BOLD), - warning: if USE_WINDOWS_COLORS { - AnsiColor::BrightYellow.on_default() - } else { - 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: if USE_WINDOWS_COLORS { - AnsiColor::BrightWhite.on_default() - } else { - Style::new() - } - .effects(Effects::BOLD), - none: Style::new(), - hyperlink: true, + error: DEFAULT_ERROR_STYLE, + warning: DEFAULT_WARNING_STYLE, + info: DEFAULT_INFO_STYLE, + note: DEFAULT_NOTE_STYLE, + help: DEFAULT_HELP_STYLE, + line_num: DEFAULT_LINE_NUM_STYLE, + emphasis: DEFAULT_EMPHASIS_STYLE, + none: DEFAULT_NONE_STYLE, + context: DEFAULT_CONTEXT_STYLE, + addition: DEFAULT_ADDITION_STYLE, + removal: DEFAULT_REMOVAL_STYLE, }, + hyperlink: true, ..Self::plain() } } + /// Abbreviate the message + pub const fn short_message(mut self, short_message: bool) -> Self { + self.short_message = short_message; + self + } + + /// Set the width to render within + /// + /// Affects the rendering of [`Snippet`][crate::Snippet]s + pub const fn term_width(mut self, term_width: usize) -> Self { + self.term_width = term_width; + self + } + + /// Set the character set used for rendering decor + pub const fn decor_style(mut self, decor_style: DecorStyle) -> Self { + self.decor_style = decor_style; + self + } + /// Anonymize line numbers /// - /// This enables (or disables) line number anonymization. When enabled, line numbers are replaced - /// with `LL`. + /// When enabled, line numbers are replaced with `LL` which is useful for tests. /// /// # Example /// /// ```text - /// --> $DIR/whitespace-trimming.rs:LL:193 + /// --> $DIR/whitespace-trimming.rs:4:193 /// | /// LL | ... let _: () = 42; /// | ^^ expected (), found integer @@ -100,82 +195,252 @@ impl Renderer { self.anonymized_line_numbers = anonymized_line_numbers; self } +} - /// Set the terminal width - pub const fn term_width(mut self, term_width: usize) -> Self { - self.term_width = term_width; - self +impl Renderer { + /// Render a diagnostic [`Report`] + pub fn render(&self, groups: Report<'_>) -> String { + render::render(self, groups) } +} - /// Set the output style for `error` +/// Customize [`Renderer::styled`] +impl Renderer { + /// Override the output style for [error][crate::Level::ERROR] pub const fn error(mut self, style: Style) -> Self { self.stylesheet.error = style; self } - /// Set the output style for `warning` + /// Override the output style for [warnings][crate::Level::WARNING] pub const fn warning(mut self, style: Style) -> Self { self.stylesheet.warning = style; self } - /// Set the output style for `info` + /// Override the output style for [info][crate::Level::INFO] pub const fn info(mut self, style: Style) -> Self { self.stylesheet.info = style; self } - /// Set the output style for `note` + /// Override the output style for [notes][crate::Level::NOTE] pub const fn note(mut self, style: Style) -> Self { self.stylesheet.note = style; self } - /// Set the output style for `help` + /// Override the output style for [help][crate::Level::HELP] pub const fn help(mut self, style: Style) -> Self { self.stylesheet.help = style; self } - /// Set the output style for line numbers - pub const fn line_no(mut self, style: Style) -> Self { - self.stylesheet.line_no = style; + /// Override the output style for line numbers in the [`Snippet`][crate::Snippet] gutter + pub const fn line_num(mut self, style: Style) -> Self { + self.stylesheet.line_num = style; self } - /// Set the output style for emphasis + /// Override the output style for emphasis for the + /// [`primary_title`][crate::Level::primary_title] pub const fn emphasis(mut self, style: Style) -> Self { self.stylesheet.emphasis = style; self } - /// Set the output style for none + /// Override the output style for [`AnnotationKind::Context`][crate::AnnotationKind::Context] + pub const fn context(mut self, style: Style) -> Self { + self.stylesheet.context = style; + self + } + + /// Override the output style for [`Patch`][crate::Patch] additions + pub const fn addition(mut self, style: Style) -> Self { + self.stylesheet.addition = style; + self + } + + /// Override the output style for [`Patch`][crate::Patch] removals + pub const fn removal(mut self, style: Style) -> Self { + self.stylesheet.removal = style; + self + } + + /// Override the output style for all other text pub const fn none(mut self, style: Style) -> Self { self.stylesheet.none = style; self } pub const fn hyperlink(mut self, hyperlink: bool) -> Self { - self.stylesheet.hyperlink = hyperlink; + self.hyperlink = hyperlink; self } /// Set the string used for when a long line is cut. /// - /// The default is `...` (three `U+002E` characters). - pub const fn cut_indicator(mut self, string: &'static str) -> Self { - self.cut_indicator = string; + /// The default for [`DecorStyle::Ascii`] is `...` (three `U+002E` characters). + pub const fn cut_indicator(mut self, cut: &'static str) -> Self { + self.cut_indicator = Some(cut); self } +} + +/// The character set for rendering for decor +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecorStyle { + Ascii, + Unicode, +} + +impl DecorStyle { + fn col_separator(&self) -> char { + match self { + DecorStyle::Ascii => '|', + DecorStyle::Unicode => '│', + } + } + + fn note_separator(&self, is_cont: bool) -> &str { + match self { + DecorStyle::Ascii => "= ", + DecorStyle::Unicode if is_cont => "├ ", + DecorStyle::Unicode => "╰ ", + } + } + + fn multi_suggestion_separator(&self) -> &'static str { + match self { + DecorStyle::Ascii => "|", + DecorStyle::Unicode => "├╴", + } + } + + fn file_start(&self, is_first: bool, alone: bool) -> &'static str { + match self { + DecorStyle::Ascii => "--> ", + DecorStyle::Unicode if is_first && alone => " ─▸ ", + DecorStyle::Unicode if is_first => " ╭▸ ", + DecorStyle::Unicode => " ├▸ ", + } + } - /// Render a snippet into a `Display`able object - pub fn render<'a>(&'a self, msg: Message<'a>) -> impl Display + 'a { - DisplayList::new( - msg, - &self.stylesheet, - self.anonymized_line_numbers, - self.term_width, - self.cut_indicator, - ) + fn secondary_file_start(&self) -> &'static str { + match self { + DecorStyle::Ascii => "::: ", + DecorStyle::Unicode => " ⸬ ", + } + } + + fn diff(&self) -> char { + match self { + DecorStyle::Ascii => '~', + DecorStyle::Unicode => '±', + } + } + + fn margin(&self) -> &'static str { + match self { + DecorStyle::Ascii => "...", + DecorStyle::Unicode => "…", + } + } + + fn underline(&self, is_primary: bool) -> UnderlineParts { + // X0 Y0 + // label_start > ┯━━━━ < underline + // │ < vertical_text_line + // text + + // multiline_start_down ⤷ X0 Y0 + // top_left > ┌───╿──┘ < top_right_flat + // top_left > ┏│━━━┙ < top_right + // multiline_vertical > ┃│ + // ┃│ X1 Y1 + // ┃│ X2 Y2 + // ┃└────╿──┘ < multiline_end_same_line + // bottom_left > ┗━━━━━┥ < bottom_right_with_text + // multiline_horizontal ^ `X` is a good letter + + // multiline_whole_line > ┏ X0 Y0 + // ┃ X1 Y1 + // ┗━━━━┛ < multiline_end_same_line + + // multiline_whole_line > ┏ X0 Y0 + // ┃ X1 Y1 + // ┃ ╿ < multiline_end_up + // ┗━━┛ < bottom_right + + match (self, is_primary) { + (DecorStyle::Ascii, true) => UnderlineParts { + style: ElementStyle::UnderlinePrimary, + underline: '^', + label_start: '^', + vertical_text_line: '|', + multiline_vertical: '|', + multiline_horizontal: '_', + multiline_whole_line: '/', + multiline_start_down: '^', + bottom_right: '|', + top_left: ' ', + top_right_flat: '^', + bottom_left: '|', + multiline_end_up: '^', + multiline_end_same_line: '^', + multiline_bottom_right_with_text: '|', + }, + (DecorStyle::Ascii, false) => UnderlineParts { + style: ElementStyle::UnderlineSecondary, + underline: '-', + label_start: '-', + vertical_text_line: '|', + multiline_vertical: '|', + multiline_horizontal: '_', + multiline_whole_line: '/', + multiline_start_down: '-', + bottom_right: '|', + top_left: ' ', + top_right_flat: '-', + bottom_left: '|', + multiline_end_up: '-', + multiline_end_same_line: '-', + multiline_bottom_right_with_text: '|', + }, + (DecorStyle::Unicode, true) => UnderlineParts { + style: ElementStyle::UnderlinePrimary, + underline: '━', + label_start: '┯', + vertical_text_line: '│', + multiline_vertical: '┃', + multiline_horizontal: '━', + multiline_whole_line: '┏', + multiline_start_down: '╿', + bottom_right: '┙', + top_left: '┏', + top_right_flat: '┛', + bottom_left: '┗', + multiline_end_up: '╿', + multiline_end_same_line: '┛', + multiline_bottom_right_with_text: '┥', + }, + (DecorStyle::Unicode, false) => UnderlineParts { + style: ElementStyle::UnderlineSecondary, + underline: '─', + label_start: '┬', + vertical_text_line: '│', + multiline_vertical: '│', + multiline_horizontal: '─', + multiline_whole_line: '┌', + multiline_start_down: '│', + bottom_right: '┘', + top_left: '┌', + top_right_flat: '┘', + bottom_left: '└', + multiline_end_up: '│', + multiline_end_same_line: '┘', + multiline_bottom_right_with_text: '┤', + }, + } } } diff --git a/crates/ruff_annotate_snippets/src/renderer/render.rs b/crates/ruff_annotate_snippets/src/renderer/render.rs new file mode 100644 index 0000000000..e17ef80039 --- /dev/null +++ b/crates/ruff_annotate_snippets/src/renderer/render.rs @@ -0,0 +1,2900 @@ +// Most of this file is adapted from https://github.com/rust-lang/rust/blob/160905b6253f42967ed4aef4b98002944c7df24c/compiler/rustc_errors/src/emitter.rs + +use alloc::borrow::Cow; +use alloc::collections::BTreeMap; +use alloc::string::{String, ToString}; +use alloc::{format, vec, vec::Vec}; +use core::cmp::{Ordering, Reverse, max, min}; +use core::fmt; + +use anstyle::Style; + +use super::DecorStyle; +use super::Renderer; +use super::margin::Margin; +use super::stylesheet::Stylesheet; +use crate::level::{Level, LevelInner}; +use crate::renderer::source_map::{ + AnnotatedLineInfo, LineInfo, Loc, SourceMap, SplicedLines, SubstitutionHighlight, TrimmedPatch, +}; +use crate::renderer::styled_buffer::StyledBuffer; +use crate::snippet::Id; +use crate::{ + Annotation, AnnotationKind, Element, Group, Message, Origin, Padding, Patch, Report, Snippet, + Title, +}; + +const ANONYMIZED_LINE_NUM: &str = "LL"; + +pub(crate) fn render(renderer: &Renderer, groups: Report<'_>) -> String { + if renderer.short_message { + render_short_message(renderer, groups).unwrap() + } else { + let lineno_offset = groups.iter().map(|g| g.lineno_offset).max().unwrap_or(0); + let (max_line_num, og_primary_path, groups) = pre_process(groups); + let max_line_num_len = lineno_offset + + if renderer.anonymized_line_numbers { + ANONYMIZED_LINE_NUM.len() + } else { + num_decimal_digits(max_line_num) + }; + let mut out_string = String::new(); + let group_len = groups.len(); + for ( + g, + PreProcessedGroup { + group, + elements, + primary_path, + max_depth, + }, + ) in groups.into_iter().enumerate() + { + let mut buffer = StyledBuffer::new(); + let level = group.primary_level.clone(); + let mut message_iter = elements.into_iter().enumerate().peekable(); + if let Some(title) = &group.title { + let peek = message_iter.peek().map(|(_, s)| s); + let title_style = if title.allows_styling { + TitleStyle::Header + } else { + TitleStyle::MainHeader + }; + let buffer_msg_line_offset = buffer.num_lines(); + render_title( + renderer, + &mut buffer, + title, + max_line_num_len, + title_style, + matches!(peek, Some(PreProcessedElement::Message(_))), + buffer_msg_line_offset, + ); + let buffer_msg_line_offset = buffer.num_lines(); + + if matches!(peek, Some(PreProcessedElement::Message(_))) { + draw_col_separator_no_space( + renderer, + &mut buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + } + if peek.is_none() + && title_style == TitleStyle::MainHeader + && g == 0 + && group_len > 1 + { + draw_col_separator_end( + renderer, + &mut buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + } + } + let mut seen_primary = false; + let mut last_suggestion_path = None; + while let Some((i, section)) = message_iter.next() { + let peek = message_iter.peek().map(|(_, s)| s); + let is_first = i == 0; + match section { + PreProcessedElement::Message(title) => { + let title_style = TitleStyle::Secondary; + let buffer_msg_line_offset = buffer.num_lines(); + render_title( + renderer, + &mut buffer, + title, + max_line_num_len, + title_style, + peek.is_some(), + buffer_msg_line_offset, + ); + } + PreProcessedElement::Cause((cause, source_map, annotated_lines)) => { + let is_primary = primary_path == cause.path.as_ref() && !seen_primary; + seen_primary |= is_primary; + render_snippet_annotations( + renderer, + &mut buffer, + max_line_num_len, + cause, + is_primary, + &source_map, + &annotated_lines, + max_depth, + peek.is_some() || (g == 0 && group_len > 1), + is_first, + ); + + if g == 0 { + let current_line = buffer.num_lines(); + match peek { + Some(PreProcessedElement::Message(_)) => { + draw_col_separator_no_space( + renderer, + &mut buffer, + current_line, + max_line_num_len + 1, + ); + } + None if group_len > 1 => draw_col_separator_end( + renderer, + &mut buffer, + current_line, + max_line_num_len + 1, + ), + _ => {} + } + } + } + PreProcessedElement::Suggestion(( + suggestion, + source_map, + spliced_lines, + display_suggestion, + )) => { + let matches_previous_suggestion = + last_suggestion_path == Some(suggestion.path.as_ref()); + emit_suggestion_default( + renderer, + &mut buffer, + suggestion, + spliced_lines, + display_suggestion, + max_line_num_len, + &source_map, + primary_path.or(og_primary_path), + matches_previous_suggestion, + is_first, + //matches!(peek, Some(Element::Message(_) | Element::Padding(_))), + peek.is_some(), + ); + + if matches!(peek, Some(PreProcessedElement::Suggestion(_))) { + last_suggestion_path = Some(suggestion.path.as_ref()); + } else { + last_suggestion_path = None; + } + } + + PreProcessedElement::Origin(origin) => { + let buffer_msg_line_offset = buffer.num_lines(); + let is_primary = primary_path == Some(&origin.path) && !seen_primary; + seen_primary |= is_primary; + render_origin( + renderer, + &mut buffer, + max_line_num_len, + origin, + is_primary, + is_first, + peek.is_none(), + buffer_msg_line_offset, + ); + let current_line = buffer.num_lines(); + if g == 0 && peek.is_none() && group_len > 1 { + draw_col_separator_end( + renderer, + &mut buffer, + current_line, + max_line_num_len + 1, + ); + } + } + PreProcessedElement::Padding(_) => { + let current_line = buffer.num_lines(); + if peek.is_none() { + draw_col_separator_end( + renderer, + &mut buffer, + current_line, + max_line_num_len + 1, + ); + } else { + draw_col_separator_no_space( + renderer, + &mut buffer, + current_line, + max_line_num_len + 1, + ); + } + } + } + } + buffer + .render(&level, &renderer.stylesheet, &mut out_string) + .unwrap(); + if g != group_len - 1 { + out_string.push('\n'); + } + } + out_string + } +} + +fn render_short_message(renderer: &Renderer, groups: &[Group<'_>]) -> Result { + let mut buffer = StyledBuffer::new(); + let mut labels = None; + let group = groups.first().expect("Expected at least one group"); + + let Some(title) = &group.title else { + panic!("Expected a Title"); + }; + + if let Some(Element::Cause(cause)) = group + .elements + .iter() + .find(|e| matches!(e, Element::Cause(_))) + { + let labels_inner = cause + .markers + .iter() + .filter_map(|ann| match &ann.label { + Some(msg) if ann.kind.is_primary() => { + if !msg.trim().is_empty() { + Some(msg.to_string()) + } else { + None + } + } + _ => None, + }) + .collect::>() + .join(", "); + if !labels_inner.is_empty() { + labels = Some(labels_inner); + } + + if let Some(path) = &cause.path { + let mut origin = Origin::path(path.as_ref()).cell_index(cause.cell_index); + + let source_map = SourceMap::new(&cause.source, cause.line_start); + let (_depth, annotated_lines) = + source_map.annotated_lines(cause.markers.clone(), cause.fold); + + if let Some(primary_line) = annotated_lines + .iter() + .find(|l| l.annotations.iter().any(LineAnnotation::is_primary)) + .or(annotated_lines.iter().find(|l| !l.annotations.is_empty())) + { + origin.line = Some(primary_line.line_index); + if let Some(first_annotation) = primary_line + .annotations + .iter() + .min_by_key(|a| (Reverse(a.is_primary()), a.start.char)) + { + origin.char_column = Some(first_annotation.start.char + 1); + } + } + + render_origin(renderer, &mut buffer, 0, &origin, true, true, true, 0); + buffer.append(0, ": ", ElementStyle::LineAndColumn); + } + } + + render_title( + renderer, + &mut buffer, + title, + 0, // No line numbers in short messages + TitleStyle::MainHeader, + false, + 0, + ); + + if let Some(labels) = labels { + buffer.append(0, &format!(": {labels}"), ElementStyle::NoStyle); + } + + let mut out_string = String::new(); + buffer.render(&title.level, &renderer.stylesheet, &mut out_string)?; + + Ok(out_string) +} + +#[allow(clippy::too_many_arguments)] +fn render_title( + renderer: &Renderer, + buffer: &mut StyledBuffer, + title: &dyn MessageOrTitle, + max_line_num_len: usize, + title_style: TitleStyle, + is_cont: bool, + buffer_msg_line_offset: usize, +) { + let (label_style, title_element_style) = match title_style { + TitleStyle::MainHeader => ( + ElementStyle::Level(title.level().level), + if renderer.short_message { + ElementStyle::NoStyle + } else { + ElementStyle::MainHeaderMsg + }, + ), + TitleStyle::Header => ( + ElementStyle::Level(title.level().level), + ElementStyle::HeaderMsg, + ), + TitleStyle::Secondary => { + for _ in 0..max_line_num_len { + buffer.append(buffer_msg_line_offset, " ", ElementStyle::NoStyle); + } + + draw_note_separator( + renderer, + buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + is_cont, + ); + (ElementStyle::MainHeaderMsg, ElementStyle::NoStyle) + } + }; + let mut label_width = 0; + + if title.level().name != Some(None) { + buffer.append(buffer_msg_line_offset, title.level().as_str(), label_style); + label_width += title.level().as_str().len(); + if let Some(Id { id: Some(id), url }) = &title.id() { + buffer.append(buffer_msg_line_offset, "[", label_style); + if renderer.hyperlink + && let Some(url) = url.as_ref() + { + buffer.append( + buffer_msg_line_offset, + &format!("\x1B]8;;{url}\x1B\\"), + label_style, + ); + } + buffer.append(buffer_msg_line_offset, id, label_style); + if renderer.hyperlink && url.is_some() { + buffer.append(buffer_msg_line_offset, "\x1B]8;;\x1B\\", label_style); + } + buffer.append(buffer_msg_line_offset, "]", label_style); + label_width += 2 + id.len(); + } + if title.is_fixable() { + buffer.append(buffer_msg_line_offset, "[", ElementStyle::NoStyle); + buffer.append( + buffer_msg_line_offset, + "*", + ElementStyle::Level(LevelInner::Help), + ); + buffer.append(buffer_msg_line_offset, "]", ElementStyle::NoStyle); + label_width += 3; + } + buffer.append(buffer_msg_line_offset, ": ", title_element_style); + label_width += 2; + } else { + if let Some(Id { id: Some(id), url }) = &title.id() { + if renderer.hyperlink + && let Some(url) = url.as_ref() + { + buffer.append( + buffer_msg_line_offset, + &format!("\x1B]8;;{url}\x1B\\"), + label_style, + ); + } + buffer.append(buffer_msg_line_offset, id, label_style); + if renderer.hyperlink && url.is_some() { + buffer.append(buffer_msg_line_offset, "\x1B]8;;\x1B\\", label_style); + } + label_width += id.len(); + if title.is_fixable() { + buffer.append(buffer_msg_line_offset, " [", ElementStyle::NoStyle); + buffer.append( + buffer_msg_line_offset, + "*", + ElementStyle::Level(LevelInner::Help), + ); + buffer.append(buffer_msg_line_offset, "]", ElementStyle::NoStyle); + label_width += 4; + } + buffer.append(buffer_msg_line_offset, " ", title_element_style); + label_width += 1; + } + } + + let padding = " ".repeat(if title_style == TitleStyle::Secondary { + // The extra 3 ` ` is padding that's always needed to align to the + // label i.e. `note: `: + // + // error: message + // --> file.rs:13:20 + // | + // 13 | + // | ^^^^ + // | + // = note: multiline + // message + // ++^^^------ + // | | | + // | | | + // | | width of label + // | magic `3` + // `max_line_num_len` + max_line_num_len + 3 + label_width + } else { + label_width + }); + + let (title_str, style) = if title.allows_styling() { + (Cow::Borrowed(title.text()), ElementStyle::NoStyle) + } else { + (normalize_whitespace(title.text()), title_element_style) + }; + for (i, text) in title_str.split('\n').enumerate() { + #[allow(clippy::collapsible_if, reason = "reduce upstream divergence")] + if i != 0 { + if title_style == TitleStyle::Secondary + && is_cont + && matches!(renderer.decor_style, DecorStyle::Unicode) + { + buffer.append(buffer_msg_line_offset + i, &padding, ElementStyle::NoStyle); + // There's another note after this one, associated to the subwindow above. + // We write additional vertical lines to join them: + // ╭▸ test.rs:3:3 + // │ + // 3 │ code + // │ ━━━━ + // │ + // ├ note: foo + // │ bar + // ╰ note: foo + // bar + draw_col_separator_no_space( + renderer, + buffer, + buffer_msg_line_offset + i, + max_line_num_len + 1, + ); + } + } + buffer.append(buffer_msg_line_offset + i, text, style); + } +} + +#[allow(clippy::too_many_arguments)] +fn render_origin( + renderer: &Renderer, + buffer: &mut StyledBuffer, + max_line_num_len: usize, + origin: &Origin<'_>, + is_primary: bool, + is_first: bool, + alone: bool, + buffer_msg_line_offset: usize, +) { + if !renderer.short_message { + for _ in 0..max_line_num_len { + buffer.append(buffer_msg_line_offset, " ", ElementStyle::NoStyle); + } + } + + if is_primary && !renderer.short_message { + buffer.append( + buffer_msg_line_offset, + renderer.decor_style.file_start(is_first, alone), + ElementStyle::LineNumber, + ); + } else if !renderer.short_message { + // if !origin.standalone { + // // Add spacing line, as shown: + // // --> $DIR/file:54:15 + // // | + // // LL | code + // // | ^^^^ + // // | (<- It prints *this* line) + // // ::: $DIR/other_file.rs:15:5 + // // | + // // LL | code + // // | ---- + // draw_col_separator_no_space(renderer, + // buffer, + // buffer_msg_line_offset, + // max_line_num_len + 1, + // ); + // + // buffer_msg_line_offset += 1; + // } + // Then, the secondary file indicator + buffer.append( + buffer_msg_line_offset, + renderer.decor_style.secondary_file_start(), + ElementStyle::LineNumber, + ); + } + + let str = { + use core::fmt::Write as _; + + let mut buffer = origin.path.as_ref().to_owned(); + if let Some(cell_index) = origin.cell_index { + write!(&mut buffer, ":cell {cell_index}").unwrap(); + } + if let Some(line) = origin.line { + if renderer.anonymized_line_numbers { + write!(&mut buffer, ":{ANONYMIZED_LINE_NUM}").unwrap(); + } else { + write!(&mut buffer, ":{line}").unwrap(); + } + if let Some(col) = origin.char_column { + write!(&mut buffer, ":{col}").unwrap(); + } + } + buffer + }; + buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn); +} + +#[allow(clippy::too_many_arguments)] +fn render_snippet_annotations( + renderer: &Renderer, + buffer: &mut StyledBuffer, + max_line_num_len: usize, + snippet: &Snippet<'_, Annotation<'_>>, + is_primary: bool, + sm: &SourceMap<'_>, + annotated_lines: &[AnnotatedLineInfo<'_>], + multiline_depth: usize, + is_cont: bool, + is_first: bool, +) { + let show_snippet = !snippet.markers.iter().any(|s| s.is_file_level); + + if let Some(path) = &snippet.path { + let mut origin = Origin::path(path.as_ref()).cell_index(snippet.cell_index); + // print out the span location and spacer before we print the annotated source + // to do this, we need to know if this span will be primary + //let is_primary = primary_path == Some(&origin.path); + + if is_primary { + if let Some(primary_line) = annotated_lines + .iter() + .find(|l| l.annotations.iter().any(LineAnnotation::is_primary)) + .or(annotated_lines.iter().find(|l| !l.annotations.is_empty())) + { + origin.line = Some(primary_line.line_index); + if let Some(first_annotation) = primary_line + .annotations + .iter() + .min_by_key(|a| (Reverse(a.is_primary()), a.start.char)) + { + origin.char_column = Some(first_annotation.start.char + 1); + } + } + } else { + let buffer_msg_line_offset = buffer.num_lines(); + // Add spacing line, as shown: + // --> $DIR/file:54:15 + // | + // LL | code + // | ^^^^ + // | (<- It prints *this* line) + // ::: $DIR/other_file.rs:15:5 + // | + // LL | code + // | ---- + draw_col_separator_no_space( + renderer, + buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + if let Some(first_line) = annotated_lines + .iter() + .find(|l| !l.annotations.is_empty()) + .or(annotated_lines.first()) + { + origin.line = Some(first_line.line_index); + if let Some(first_annotation) = first_line.annotations.first() { + origin.char_column = Some(first_annotation.start.char + 1); + } + } + } + let buffer_msg_line_offset = buffer.num_lines(); + render_origin( + renderer, + buffer, + max_line_num_len, + &origin, + is_primary, + is_first, + !(show_snippet || is_cont), + buffer_msg_line_offset, + ); + // Put in the spacer between the location and annotated source + if show_snippet { + draw_col_separator_no_space( + renderer, + buffer, + buffer_msg_line_offset + 1, + max_line_num_len + 1, + ); + } + } else { + let buffer_msg_line_offset = buffer.num_lines(); + if is_primary { + if renderer.decor_style == DecorStyle::Unicode { + buffer.puts( + buffer_msg_line_offset, + max_line_num_len, + renderer.decor_style.file_start(is_first, false), + ElementStyle::LineNumber, + ); + } else { + draw_col_separator_no_space( + renderer, + buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + } + } else { + // Add spacing line, as shown: + // --> $DIR/file:54:15 + // | + // LL | code + // | ^^^^ + // | (<- It prints *this* line) + // ::: $DIR/other_file.rs:15:5 + // | + // LL | code + // | ---- + draw_col_separator_no_space( + renderer, + buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + + buffer.puts( + buffer_msg_line_offset + 1, + max_line_num_len, + renderer.decor_style.secondary_file_start(), + ElementStyle::LineNumber, + ); + } + } + + if !show_snippet { + return; + } + + // Contains the vertical lines' positions for active multiline annotations + let mut multilines = Vec::new(); + + // Get the left-side margin to remove it + let mut whitespace_margin = usize::MAX; + for line_info in annotated_lines { + let leading_whitespace = line_info + .line + .chars() + .take_while(|c| c.is_whitespace()) + .map(|c| { + match c { + // Tabs are displayed as 4 spaces + '\t' => 4, + _ => 1, + } + }) + .sum(); + if line_info.line.chars().any(|c| !c.is_whitespace()) { + whitespace_margin = min(whitespace_margin, leading_whitespace); + } + } + if whitespace_margin == usize::MAX { + whitespace_margin = 0; + } + + // Left-most column any visible span points at. + let mut span_left_margin = usize::MAX; + for line_info in annotated_lines { + for ann in &line_info.annotations { + span_left_margin = min(span_left_margin, ann.start.display); + span_left_margin = min(span_left_margin, ann.end.display); + } + } + if span_left_margin == usize::MAX { + span_left_margin = 0; + } + + // Right-most column any visible span points at. + let mut span_right_margin = 0; + let mut label_right_margin = 0; + let mut max_line_len = 0; + for line_info in annotated_lines { + max_line_len = max(max_line_len, str_width(line_info.line)); + for ann in &line_info.annotations { + span_right_margin = max(span_right_margin, ann.start.display); + span_right_margin = max(span_right_margin, ann.end.display); + // FIXME: account for labels not in the same line + let label_right = ann.label.as_ref().map_or(0, |l| str_width(l) + 1); + label_right_margin = max(label_right_margin, ann.end.display + label_right); + } + } + let width_offset = 3 + max_line_num_len; + let code_offset = if multiline_depth == 0 { + width_offset + } else { + width_offset + multiline_depth + 1 + }; + + let column_width = renderer.term_width.saturating_sub(code_offset); + + let margin = Margin::new( + whitespace_margin, + span_left_margin, + span_right_margin, + label_right_margin, + column_width, + max_line_len, + ); + + // Next, output the annotate source for this file + for annotated_line_idx in 0..annotated_lines.len() { + let previous_buffer_line = buffer.num_lines(); + + let depths = render_source_line( + renderer, + &annotated_lines[annotated_line_idx], + buffer, + width_offset, + code_offset, + max_line_num_len, + margin, + !is_cont && annotated_line_idx + 1 == annotated_lines.len(), + ); + + let mut to_add = BTreeMap::new(); + + for (depth, style) in depths { + if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) { + multilines.swap_remove(index); + } else { + to_add.insert(depth, style); + } + } + + // Set the multiline annotation vertical lines to the left of + // the code in this line. + for (depth, style) in &multilines { + for line in previous_buffer_line..buffer.num_lines() { + draw_multiline_line(renderer, buffer, line, width_offset, *depth, *style, false); + } + } + // check to see if we need to print out or elide lines that come between + // this annotated line and the next one. + if annotated_line_idx < (annotated_lines.len() - 1) { + let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index + - annotated_lines[annotated_line_idx].line_index; + match line_idx_delta.cmp(&2) { + Ordering::Greater => { + let last_buffer_line_num = buffer.num_lines(); + + draw_line_separator(renderer, buffer, last_buffer_line_num, width_offset); + + // Set the multiline annotation vertical lines on `...` bridging line. + for (depth, style) in &multilines { + draw_multiline_line( + renderer, + buffer, + last_buffer_line_num, + width_offset, + *depth, + *style, + true, + ); + } + if let Some(line) = annotated_lines.get(annotated_line_idx) { + for ann in &line.annotations { + if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type { + // In the case where we have elided the entire start of the + // multispan because those lines were empty, we still need + // to draw the `|`s across the `...`. + draw_multiline_line( + renderer, + buffer, + last_buffer_line_num, + width_offset, + pos, + if ann.is_primary() { + ElementStyle::UnderlinePrimary + } else { + ElementStyle::UnderlineSecondary + }, + true, + ); + } + } + } + } + + Ordering::Equal => { + let unannotated_line = sm + .get_line(annotated_lines[annotated_line_idx].line_index + 1) + .unwrap_or(""); + + let last_buffer_line_num = buffer.num_lines(); + + draw_line( + renderer, + buffer, + &normalize_whitespace(unannotated_line), + annotated_lines[annotated_line_idx + 1].line_index - 1, + last_buffer_line_num, + width_offset, + code_offset, + max_line_num_len, + margin, + ); + + for (depth, style) in &multilines { + draw_multiline_line( + renderer, + buffer, + last_buffer_line_num, + width_offset, + *depth, + *style, + false, + ); + } + if let Some(line) = annotated_lines.get(annotated_line_idx) { + for ann in &line.annotations { + if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type { + draw_multiline_line( + renderer, + buffer, + last_buffer_line_num, + width_offset, + pos, + if ann.is_primary() { + ElementStyle::UnderlinePrimary + } else { + ElementStyle::UnderlineSecondary + }, + false, + ); + } + } + } + } + Ordering::Less => {} + } + } + + multilines.extend(to_add); + } +} + +#[allow(clippy::too_many_arguments)] +fn render_source_line( + renderer: &Renderer, + line_info: &AnnotatedLineInfo<'_>, + buffer: &mut StyledBuffer, + width_offset: usize, + code_offset: usize, + max_line_num_len: usize, + margin: Margin, + close_window: bool, +) -> Vec<(usize, ElementStyle)> { + // Draw: + // + // LL | ... code ... + // | ^^-^ span label + // | | + // | secondary span label + // + // ^^ ^ ^^^ ^^^^ ^^^ we don't care about code too far to the right of a span, we trim it + // | | | | + // | | | actual code found in your source code and the spans we use to mark it + // | | when there's too much wasted space to the left, trim it + // | vertical divider between the column number and the code + // column number + + let source_string = normalize_whitespace(line_info.line); + + let line_offset = buffer.num_lines(); + + let left = draw_line( + renderer, + buffer, + &source_string, + line_info.line_index, + line_offset, + width_offset, + code_offset, + max_line_num_len, + margin, + ); + + // If there are no annotations, we are done + if line_info.annotations.is_empty() { + // `close_window` normally gets handled later, but we are early + // returning, so it needs to be handled here + if close_window { + draw_col_separator_end(renderer, buffer, line_offset + 1, width_offset - 2); + } + return vec![]; + } + + // Special case when there's only one annotation involved, it is the start of a multiline + // span and there's no text at the beginning of the code line. Instead of doing the whole + // graph: + // + // 2 | fn foo() { + // | _^ + // 3 | | + // 4 | | } + // | |_^ test + // + // we simplify the output to: + // + // 2 | / fn foo() { + // 3 | | + // 4 | | } + // | |_^ test + let mut buffer_ops = vec![]; + let mut annotations = vec![]; + let mut short_start = true; + for ann in &line_info.annotations { + if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type { + if source_string + .chars() + .take(ann.start.display) + .all(char::is_whitespace) + { + let uline = renderer.decor_style.underline(ann.is_primary()); + let chr = uline.multiline_whole_line; + annotations.push((depth, uline.style)); + buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style)); + } else { + short_start = false; + break; + } + } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type { + } else { + short_start = false; + break; + } + } + if short_start { + for (y, x, c, s) in buffer_ops { + buffer.putc(y, x, c, s); + } + return annotations; + } + + // We want to display like this: + // + // vec.push(vec.pop().unwrap()); + // --- ^^^ - previous borrow ends here + // | | + // | error occurs here + // previous borrow of `vec` occurs here + // + // But there are some weird edge cases to be aware of: + // + // vec.push(vec.pop().unwrap()); + // -------- - previous borrow ends here + // || + // |this makes no sense + // previous borrow of `vec` occurs here + // + // For this reason, we group the lines into "highlight lines" + // and "annotations lines", where the highlight lines have the `^`. + + // Sort the annotations by (start, end col) + // The labels are reversed, sort and then reversed again. + // Consider a list of annotations (A1, A2, C1, C2, B1, B2) where + // the letter signifies the span. Here we are only sorting by the + // span and hence, the order of the elements with the same span will + // not change. On reversing the ordering (|a, b| but b.cmp(a)), you get + // (C1, C2, B1, B2, A1, A2). All the elements with the same span are + // still ordered first to last, but all the elements with different + // spans are ordered by their spans in last to first order. Last to + // first order is important, because the jiggly lines and | are on + // the left, so the rightmost span needs to be rendered first, + // otherwise the lines would end up needing to go over a message. + + let mut annotations = line_info.annotations.clone(); + annotations.sort_by_key(|a| Reverse((a.start.display, a.start.char))); + + // First, figure out where each label will be positioned. + // + // In the case where you have the following annotations: + // + // vec.push(vec.pop().unwrap()); + // -------- - previous borrow ends here [C] + // || + // |this makes no sense [B] + // previous borrow of `vec` occurs here [A] + // + // `annotations_position` will hold [(2, A), (1, B), (0, C)]. + // + // We try, when possible, to stick the rightmost annotation at the end + // of the highlight line: + // + // vec.push(vec.pop().unwrap()); + // --- --- - previous borrow ends here + // + // But sometimes that's not possible because one of the other + // annotations overlaps it. For example, from the test + // `span_overlap_label`, we have the following annotations + // (written on distinct lines for clarity): + // + // fn foo(x: u32) { + // -------------- + // - + // + // In this case, we can't stick the rightmost-most label on + // the highlight line, or we would get: + // + // fn foo(x: u32) { + // -------- x_span + // | + // fn_span + // + // which is totally weird. Instead we want: + // + // fn foo(x: u32) { + // -------------- + // | | + // | x_span + // fn_span + // + // which is...less weird, at least. In fact, in general, if + // the rightmost span overlaps with any other span, we should + // use the "hang below" version, so we can at least make it + // clear where the span *starts*. There's an exception for this + // logic, when the labels do not have a message: + // + // fn foo(x: u32) { + // -------------- + // | + // x_span + // + // instead of: + // + // fn foo(x: u32) { + // -------------- + // | | + // | x_span + // + // + let mut overlap = vec![false; annotations.len()]; + let mut annotations_position = vec![]; + let mut line_len: usize = 0; + let mut p = 0; + for (i, annotation) in annotations.iter().enumerate() { + for (j, next) in annotations.iter().enumerate() { + if overlaps(next, annotation, 0) && j > 1 { + overlap[i] = true; + overlap[j] = true; + } + if overlaps(next, annotation, 0) // This label overlaps with another one and both + && annotation.has_label() // take space (they have text and are not + && j > i // multiline lines). + && p == 0 + // We're currently on the first line, move the label one line down + { + // If we're overlapping with an un-labelled annotation with the same span + // we can just merge them in the output + if next.start.display == annotation.start.display + && next.start.char == annotation.start.char + && next.end.display == annotation.end.display + && next.end.char == annotation.end.char + && !next.has_label() + { + continue; + } + + // This annotation needs a new line in the output. + p += 1; + break; + } + } + annotations_position.push((p, annotation)); + for (j, next) in annotations.iter().enumerate() { + if j > i { + let l = next.label.as_ref().map_or(0, |label| label.len() + 2); + if (overlaps(next, annotation, l) // Do not allow two labels to be in the same + // line if they overlap including padding, to + // avoid situations like: + // + // fn foo(x: u32) { + // -------^------ + // | | + // fn_spanx_span + // + && annotation.has_label() // Both labels must have some text, otherwise + && next.has_label()) // they are not overlapping. + // Do not add a new line if this annotation + // or the next are vertical line placeholders. + || (annotation.takes_space() // If either this or the next annotation is + && next.has_label()) // multiline start/end, move it to a new line + || (annotation.has_label() // so as not to overlap the horizontal lines. + && next.takes_space()) + || (annotation.takes_space() && next.takes_space()) + || (overlaps(next, annotation, l) + && (next.end.display, next.end.char) <= (annotation.end.display, annotation.end.char) + && next.has_label() + && p == 0) + // Avoid #42595. + { + // This annotation needs a new line in the output. + p += 1; + break; + } + } + } + line_len = max(line_len, p); + } + + if line_len != 0 { + line_len += 1; + } + + // If there are no annotations or the only annotations on this line are + // MultilineLine, then there's only code being shown, stop processing. + if line_info.annotations.iter().all(LineAnnotation::is_line) { + return vec![]; + } + + if annotations_position + .iter() + .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_))) + && let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max() + { + // Special case the following, so that we minimize overlapping multiline spans. + // + // 3 │ X0 Y0 Z0 + // │ ┏━━━━━┛ │ │ < We are writing these lines + // │ ┃┌───────┘ │ < by reverting the "depth" of + // │ ┃│┌─────────┘ < their multiline spans. + // 4 │ ┃││ X1 Y1 Z1 + // 5 │ ┃││ X2 Y2 Z2 + // │ ┃│└────╿──│──┘ `Z` label + // │ ┃└─────│──┤ + // │ ┗━━━━━━┥ `Y` is a good letter too + // ╰╴ `X` is a good letter + for (pos, _) in &mut annotations_position { + *pos = max_pos - *pos; + } + // We know then that we don't need an additional line for the span label, saving us + // one line of vertical space. + line_len = line_len.saturating_sub(1); + } + + // Write the column separator. + // + // After this we will have: + // + // 2 | fn foo() { + // | + // | + // | + // 3 | + // 4 | } + // | + for pos in 0..=line_len { + draw_col_separator_no_space(renderer, buffer, line_offset + pos + 1, width_offset - 2); + } + if close_window { + draw_col_separator_end( + renderer, + buffer, + line_offset + line_len + 1, + width_offset - 2, + ); + } + // Write the horizontal lines for multiline annotations + // (only the first and last lines need this). + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | + // | + // 3 | + // 4 | } + // | _ + for &(pos, annotation) in &annotations_position { + let underline = renderer.decor_style.underline(annotation.is_primary()); + let pos = pos + 1; + match annotation.annotation_type { + LineAnnotationType::MultilineStart(depth) | LineAnnotationType::MultilineEnd(depth) => { + draw_range( + buffer, + underline.multiline_horizontal, + line_offset + pos, + width_offset + depth, + (code_offset + annotation.start.display).saturating_sub(left), + underline.style, + ); + } + _ if annotation.highlight_source => { + buffer.set_style_range( + line_offset, + (code_offset + annotation.start.char).saturating_sub(left), + (code_offset + annotation.end.char).saturating_sub(left), + underline.style, + annotation.is_primary(), + ); + } + _ => {} + } + } + + // Write the vertical lines for labels that are on a different line as the underline. + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | | | + // | | + // 3 | | + // 4 | | } + // | |_ + for &(pos, annotation) in &annotations_position { + let underline = renderer.decor_style.underline(annotation.is_primary()); + let pos = pos + 1; + + if pos > 1 && (annotation.has_label() || annotation.takes_space()) { + for p in line_offset + 1..=line_offset + pos { + buffer.putc( + p, + (code_offset + annotation.start.display).saturating_sub(left), + match annotation.annotation_type { + LineAnnotationType::MultilineLine(_) => underline.multiline_vertical, + _ => underline.vertical_text_line, + }, + underline.style, + ); + } + if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type { + buffer.putc( + line_offset + pos, + (code_offset + annotation.start.display).saturating_sub(left), + underline.bottom_right, + underline.style, + ); + } + if matches!( + annotation.annotation_type, + LineAnnotationType::MultilineEnd(_) + ) && annotation.has_label() + { + buffer.putc( + line_offset + pos, + (code_offset + annotation.start.display).saturating_sub(left), + underline.multiline_bottom_right_with_text, + underline.style, + ); + } + } + match annotation.annotation_type { + LineAnnotationType::MultilineStart(depth) => { + buffer.putc( + line_offset + pos, + width_offset + depth - 1, + underline.top_left, + underline.style, + ); + for p in line_offset + pos + 1..line_offset + line_len + 2 { + buffer.putc( + p, + width_offset + depth - 1, + underline.multiline_vertical, + underline.style, + ); + } + } + LineAnnotationType::MultilineEnd(depth) => { + for p in line_offset..line_offset + pos { + buffer.putc( + p, + width_offset + depth - 1, + underline.multiline_vertical, + underline.style, + ); + } + buffer.putc( + line_offset + pos, + width_offset + depth - 1, + underline.bottom_left, + underline.style, + ); + } + _ => (), + } + } + + // Write the labels on the annotations that actually have a label. + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | | + // | something about `foo` + // 3 | + // 4 | } + // | _ test + for &(pos, annotation) in &annotations_position { + let style = if annotation.is_primary() { + ElementStyle::LabelPrimary + } else { + ElementStyle::LabelSecondary + }; + let (pos, col) = if pos == 0 { + if annotation.end.display == 0 { + (pos + 1, (annotation.end.display + 2).saturating_sub(left)) + } else { + (pos + 1, (annotation.end.display + 1).saturating_sub(left)) + } + } else { + (pos + 2, annotation.start.display.saturating_sub(left)) + }; + if let Some(label) = &annotation.label { + buffer.puts(line_offset + pos, code_offset + col, label, style); + } + } + + // Sort from biggest span to smallest span so that smaller spans are + // represented in the output: + // + // x | fn foo() + // | ^^^---^^ + // | | | + // | | something about `foo` + // | something about `fn foo()` + annotations_position.sort_by_key(|(_, ann)| { + // Decreasing order. When annotations share the same length, prefer `Primary`. + (Reverse(ann.len()), ann.is_primary()) + }); + + // Write the underlines. + // + // After this we will have: + // + // 2 | fn foo() { + // | ____-_____^ + // | | + // | something about `foo` + // 3 | + // 4 | } + // | _^ test + for &(pos, annotation) in &annotations_position { + let uline = renderer.decor_style.underline(annotation.is_primary()); + for p in annotation.start.display..annotation.end.display { + // The default span label underline. + buffer.putc( + line_offset + 1, + (code_offset + p).saturating_sub(left), + uline.underline, + uline.style, + ); + } + + if pos == 0 + && matches!( + annotation.annotation_type, + LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_) + ) + { + // The beginning of a multiline span with its leftward moving line on the same line. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start.display).saturating_sub(left), + match annotation.annotation_type { + LineAnnotationType::MultilineStart(_) => uline.top_right_flat, + LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line, + _ => panic!("unexpected annotation type: {annotation:?}"), + }, + uline.style, + ); + } else if pos != 0 + && matches!( + annotation.annotation_type, + LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_) + ) + { + // The beginning of a multiline span with its leftward moving line on another line, + // so we start going down first. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start.display).saturating_sub(left), + match annotation.annotation_type { + LineAnnotationType::MultilineStart(_) => uline.multiline_start_down, + LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up, + _ => panic!("unexpected annotation type: {annotation:?}"), + }, + uline.style, + ); + } else if pos != 0 && annotation.has_label() { + // The beginning of a span label with an actual label, we'll point down. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start.display).saturating_sub(left), + uline.label_start, + uline.style, + ); + } + } + + // We look for individual *long* spans, and we trim the *middle*, so that we render + // LL | ...= [0, 0, 0, ..., 0, 0]; + // | ^^^^^^^^^^...^^^^^^^ expected `&[u8]`, found `[{integer}; 1680]` + for (i, (_pos, annotation)) in annotations_position.iter().enumerate() { + // Skip cases where multiple spans overlap eachother. + if overlap[i] { + continue; + }; + let LineAnnotationType::Singleline = annotation.annotation_type else { + continue; + }; + let width = annotation.end.display - annotation.start.display; + + static MIN_PAD: usize = 5; + let cut_indicator = renderer + .cut_indicator + .unwrap_or(renderer.decor_style.margin()); + let margin_width = str_width(cut_indicator); + if width > margin.term_width * 2 && width > (MIN_PAD * 2 + margin_width) { + // If the terminal is *too* small, we keep at least a tiny bit of the span for + // display. + let pad = max(margin.term_width / 3, MIN_PAD); + // Code line + buffer.replace( + line_offset, + code_offset + (annotation.start.display + pad).saturating_sub(left), + code_offset + (annotation.end.display - pad).saturating_sub(left), + cut_indicator, + ); + // Underline line + buffer.replace( + line_offset + 1, + code_offset + (annotation.start.display + pad).saturating_sub(left), + code_offset + (annotation.end.display - pad).saturating_sub(left), + cut_indicator, + ); + } + } + annotations_position + .iter() + .filter_map(|&(_, annotation)| match annotation.annotation_type { + LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => { + let style = if annotation.is_primary() { + ElementStyle::LabelPrimary + } else { + ElementStyle::LabelSecondary + }; + Some((p, style)) + } + _ => None, + }) + .collect::>() +} + +#[allow(clippy::too_many_arguments)] +fn emit_suggestion_default( + renderer: &Renderer, + buffer: &mut StyledBuffer, + suggestion: &Snippet<'_, Patch<'_>>, + spliced_lines: SplicedLines<'_>, + show_code_change: DisplaySuggestion, + max_line_num_len: usize, + sm: &SourceMap<'_>, + primary_path: Option<&Cow<'_, str>>, + matches_previous_suggestion: bool, + is_first: bool, + is_cont: bool, +) { + let buffer_offset = buffer.num_lines(); + let mut row_num = buffer_offset + usize::from(!matches_previous_suggestion); + let (complete, parts, highlights, replaced_highlights) = spliced_lines; + let is_multiline = complete.lines().count() > 1; + + if suggestion.path.as_ref() != primary_path + && let Some(path) = suggestion.path.as_ref() + && !matches_previous_suggestion + { + let (loc, _) = sm.span_to_locations(parts[0].span.clone()); + // --> file.rs:line:col + // | + for _ in 0..max_line_num_len { + buffer.append(row_num - 1, " ", ElementStyle::NoStyle); + } + let arrow = renderer.decor_style.file_start(is_first, false); + buffer.append(row_num - 1, arrow, ElementStyle::LineNumber); + let message = if renderer.anonymized_line_numbers { + format!("{}:{}:{}", path, loc.line, loc.char + 1) + } else { + format!("{}:{}:{}", path, ANONYMIZED_LINE_NUM, loc.char + 1) + }; + buffer.append(row_num - 1, &message, ElementStyle::LineAndColumn); + + draw_col_separator_no_space(renderer, buffer, row_num, max_line_num_len + 1); + row_num += 1; + } else if matches_previous_suggestion { + buffer.puts( + row_num - 1, + max_line_num_len + 1, + renderer.decor_style.multi_suggestion_separator(), + ElementStyle::LineNumber, + ); + } else { + draw_col_separator_start(renderer, buffer, row_num - 1, max_line_num_len + 1); + } + + if let DisplaySuggestion::Diff = show_code_change { + row_num += 1; + } + + let lo = parts.iter().map(|p| p.span.start).min().unwrap(); + let hi = parts.iter().map(|p| p.span.end).max().unwrap(); + + let file_lines = sm.span_to_lines(lo..hi); + let (line_start, line_end) = if suggestion.fold { + // We use the original span to get original line_start + sm.span_to_locations(parts[0].original_span.clone()) + } else { + sm.span_to_locations(0..sm.source.len()) + }; + let mut lines = complete.lines(); + if lines.clone().next().is_none() { + // Account for a suggestion to completely remove a line(s) with whitespace (#94192). + for line in line_start.line..=line_end.line { + buffer.puts( + row_num - 1 + line - line_start.line, + 0, + &maybe_anonymized(renderer, line, max_line_num_len), + ElementStyle::LineNumber, + ); + buffer.puts( + row_num - 1 + line - line_start.line, + max_line_num_len + 1, + "- ", + ElementStyle::Removal, + ); + buffer.puts( + row_num - 1 + line - line_start.line, + max_line_num_len + 3, + &normalize_whitespace(sm.get_line(line).unwrap()), + ElementStyle::Removal, + ); + } + row_num += line_end.line - line_start.line; + } + let mut unhighlighted_lines = Vec::new(); + for (line_pos, (line, highlight_parts)) in lines.by_ref().zip(highlights).enumerate() { + // Remember lines that are not highlighted to hide them if needed + if highlight_parts.is_empty() && suggestion.fold { + unhighlighted_lines.push((line_pos, line)); + continue; + } + + match unhighlighted_lines.len() { + 0 => (), + // Since we show first line, "..." line and last line, + // There is no reason to hide if there are 3 or less lines + // (because then we just replace a line with ... which is + // not helpful) + n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| { + draw_code_line( + renderer, + buffer, + &mut row_num, + &[], + &[], + p + line_start.line, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + }), + // Print first unhighlighted line, "..." and last unhighlighted line, like so: + // + // LL | this line was highlighted + // LL | this line is just for context + // ... + // LL | this line is just for context + // LL | this line was highlighted + _ => { + let last_line = unhighlighted_lines.pop(); + let first_line = unhighlighted_lines.drain(..).next(); + + if let Some((p, l)) = first_line { + draw_code_line( + renderer, + buffer, + &mut row_num, + &[], + &[], + p + line_start.line, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + + let cut_indicator = renderer + .cut_indicator + .unwrap_or(renderer.decor_style.margin()); + let padding = str_width(cut_indicator); + buffer.puts( + row_num, + max_line_num_len.saturating_sub(padding), + cut_indicator, + ElementStyle::LineNumber, + ); + row_num += 1; + + if let Some((p, l)) = last_line { + draw_code_line( + renderer, + buffer, + &mut row_num, + &[], + &[], + p + line_start.line, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + } + } + draw_code_line( + renderer, + buffer, + &mut row_num, + &highlight_parts, + &replaced_highlights, + line_pos + line_start.line, + line, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + + // This offset and the ones below need to be signed to account for replacement code + // that is shorter than the original code. + let mut offsets: Vec<(usize, isize)> = Vec::new(); + // Only show an underline in the suggestions if the suggestion is not the + // entirety of the code being shown and the displayed code is not multiline. + if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add = + show_code_change + { + for part in parts { + let (span_start, span_end) = sm.span_to_locations(part.span.clone()); + let span_start_pos = span_start.display; + let span_end_pos = span_end.display; + + // If this addition is _only_ whitespace, then don't trim it, + // or else we're just not rendering anything. + let is_whitespace_addition = part.replacement.trim().is_empty(); + + // Do not underline the leading... + let start = if is_whitespace_addition { + 0 + } else { + part.replacement + .len() + .saturating_sub(part.replacement.trim_start().len()) + }; + // ...or trailing spaces. Account for substitutions containing unicode + // characters. + let sub_len: usize = str_width(if is_whitespace_addition { + &part.replacement + } else { + part.replacement.trim() + }); + + let offset: isize = offsets + .iter() + .filter_map(|(start, v)| { + if span_start_pos < *start { + None + } else { + Some(v) + } + }) + .sum(); + let underline_start = (span_start_pos + start) as isize + offset; + let underline_end = (span_start_pos + start + sub_len) as isize + offset; + assert!(underline_start >= 0 && underline_end >= 0); + let padding: usize = max_line_num_len + 3; + for p in underline_start..underline_end { + if matches!(show_code_change, DisplaySuggestion::Underline) { + // If this is a replacement, underline with `~`, if this is an addition + // underline with `+`. + buffer.putc( + row_num, + (padding as isize + p) as usize, + if part.is_addition(sm) { + '+' + } else { + renderer.decor_style.diff() + }, + ElementStyle::Addition, + ); + } + } + + // length of the code after substitution + let full_sub_len = str_width(&part.replacement) as isize; + + // length of the code to be substituted + let snippet_len = span_end_pos as isize - span_start_pos as isize; + // For multiple substitutions, use the position *after* the previous + // substitutions have happened, only when further substitutions are + // located strictly after. + offsets.push((span_end_pos, full_sub_len - snippet_len)); + } + row_num += 1; + } + + // if we elided some lines, add an ellipsis + if lines.next().is_some() { + let cut_indicator = renderer + .cut_indicator + .unwrap_or(renderer.decor_style.margin()); + let padding = str_width(cut_indicator); + buffer.puts( + row_num, + max_line_num_len.saturating_sub(padding), + cut_indicator, + ElementStyle::LineNumber, + ); + } else { + let row = match show_code_change { + DisplaySuggestion::Diff | DisplaySuggestion::Add | DisplaySuggestion::Underline => { + row_num - 1 + } + DisplaySuggestion::None => row_num, + }; + if is_cont { + draw_col_separator_no_space(renderer, buffer, row, max_line_num_len + 1); + } else { + draw_col_separator_end(renderer, buffer, row, max_line_num_len + 1); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn draw_code_line( + renderer: &Renderer, + buffer: &mut StyledBuffer, + row_num: &mut usize, + highlight_parts: &[SubstitutionHighlight], + replaced_parts: &[Vec], + line_num: usize, + line_to_add: &str, + show_code_change: DisplaySuggestion, + max_line_num_len: usize, + file_lines: &[&LineInfo<'_>], + is_multiline: bool, +) { + if let DisplaySuggestion::Diff = show_code_change { + // We need to print more than one line if the span we need to remove is multiline. + // For more info: https://github.com/rust-lang/rust/issues/92741 + let lines_to_remove = file_lines.iter().take(file_lines.len() - 1); + for (index, (line_to_remove, parts)) in lines_to_remove.zip(replaced_parts).enumerate() { + buffer.puts( + *row_num - 1, + 0, + &maybe_anonymized(renderer, line_num + index, max_line_num_len), + ElementStyle::LineNumber, + ); + buffer.puts( + *row_num - 1, + max_line_num_len + 1, + "- ", + ElementStyle::Removal, + ); + let line = normalize_whitespace(line_to_remove.line); + buffer.puts( + *row_num - 1, + max_line_num_len + 3, + &line, + ElementStyle::NoStyle, + ); + style_substitution_highlights( + parts, + ElementStyle::Removal, + *row_num - 1, + line_to_remove.line, + max_line_num_len, + buffer, + ); + *row_num += 1; + } + // If the last line is exactly equal to the line we need to add, we can skip both of + // them. This allows us to avoid output like the following: + // 2 - & + // 2 + if true { true } else { false } + // 3 - if true { true } else { false } + // If those lines aren't equal, we print their diff + let last_line = &file_lines.last().unwrap(); + if last_line.line == line_to_add { + *row_num -= 2; + // The last original line collapses into the previous drawn row, so + // fold its replaced-code highlights onto that row too. + style_substitution_highlights( + replaced_parts.last().unwrap(), + ElementStyle::Removal, + *row_num, + last_line.line, + max_line_num_len, + buffer, + ); + } else { + buffer.puts( + *row_num - 1, + 0, + &maybe_anonymized(renderer, line_num + file_lines.len() - 1, max_line_num_len), + ElementStyle::LineNumber, + ); + buffer.puts( + *row_num - 1, + max_line_num_len + 1, + "- ", + ElementStyle::Removal, + ); + buffer.puts( + *row_num - 1, + max_line_num_len + 3, + &normalize_whitespace(last_line.line), + ElementStyle::NoStyle, + ); + style_substitution_highlights( + replaced_parts.last().unwrap(), + ElementStyle::Removal, + *row_num - 1, + last_line.line, + max_line_num_len, + buffer, + ); + + if line_to_add.trim().is_empty() { + *row_num -= 1; + } else { + // Check if after the removal, the line is left with only whitespace. If so, we + // will not show an "addition" line, as removing the whole line is what the user + // would really want. + // For example, for the following: + // | + // 2 - .await + // 2 + (note the left over whitespace) + // | + // We really want + // | + // 2 - .await + // | + // *row_num -= 1; + buffer.puts( + *row_num, + 0, + &maybe_anonymized(renderer, line_num, max_line_num_len), + ElementStyle::LineNumber, + ); + buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition); + buffer.append( + *row_num, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } + } + } else if is_multiline { + buffer.puts( + *row_num, + 0, + &maybe_anonymized(renderer, line_num, max_line_num_len), + ElementStyle::LineNumber, + ); + match &highlight_parts { + [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => { + buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition); + } + [] | [SubstitutionHighlight { start: 0, end: 0 }] => { + // FIXME: needed? Doesn't get exercised in any test. + draw_col_separator_no_space(renderer, buffer, *row_num, max_line_num_len + 1); + } + _ => { + let diff = renderer.decor_style.diff(); + buffer.puts( + *row_num, + max_line_num_len + 1, + &format!("{diff} "), + ElementStyle::Addition, + ); + } + } + // LL | line_to_add + // ++^^^ + // | | + // | magic `3` + // `max_line_num_len` + buffer.puts( + *row_num, + max_line_num_len + 3, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } else if let DisplaySuggestion::Add = show_code_change { + buffer.puts( + *row_num, + 0, + &maybe_anonymized(renderer, line_num, max_line_num_len), + ElementStyle::LineNumber, + ); + buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition); + buffer.append( + *row_num, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } else { + buffer.puts( + *row_num, + 0, + &maybe_anonymized(renderer, line_num, max_line_num_len), + ElementStyle::LineNumber, + ); + draw_col_separator(renderer, buffer, *row_num, max_line_num_len + 1); + buffer.append( + *row_num, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } + + style_substitution_highlights( + highlight_parts, + ElementStyle::Addition, + *row_num, + line_to_add, + max_line_num_len, + buffer, + ); + + *row_num += 1; +} + +fn style_substitution_highlights( + highlight_parts: &[SubstitutionHighlight], + style: ElementStyle, + row_num: usize, + unnormalized_line: &str, + max_line_num_len: usize, + buffer: &mut StyledBuffer, +) { + for &SubstitutionHighlight { start, end } in highlight_parts { + // This is a no-op for empty ranges + if start != end { + // We calculate the extra width from tabs for both the start and end + // of the span, as tabs could be present in the middle of the span + let extra_width_start: usize = extra_width_from_tabs(unnormalized_line, start); + let extra_width_end: usize = extra_width_from_tabs(unnormalized_line, end); + buffer.set_style_range( + row_num, + max_line_num_len + 3 + start + extra_width_start, + max_line_num_len + 3 + end + extra_width_end, + style, + true, + ); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn draw_line( + renderer: &Renderer, + buffer: &mut StyledBuffer, + source_string: &str, + line_index: usize, + line_offset: usize, + width_offset: usize, + code_offset: usize, + max_line_num_len: usize, + margin: Margin, +) -> usize { + // Tabs are assumed to have been replaced by spaces in calling code. + debug_assert!(!source_string.contains('\t')); + let line_len = str_width(source_string); + // Create the source line we will highlight. + let mut left = margin.left(line_len); + let right = margin.right(line_len); + + let mut taken = 0; + let mut skipped = 0; + let code: String = source_string + .chars() + .skip_while(|ch| { + let w = char_width(*ch); + // If `skipped` is less than `left`, always skip the next `ch`, + // even if `ch` is a multi-width char that would make `skipped` + // exceed `left`. This ensures that we do not exceed term width on + // source lines. + if skipped < left { + skipped += w; + true + } else { + false + } + }) + .take_while(|ch| { + // Make sure that the trimming on the right will fall within the terminal width. + taken += char_width(*ch); + taken <= (right - left) + }) + .collect(); + // If we skipped more than `left`, adjust `left` to account for it. + if skipped > left { + left += skipped - left; + } + let cut_indicator = renderer + .cut_indicator + .unwrap_or(renderer.decor_style.margin()); + let padding = str_width(cut_indicator); + let (width_taken, bytes_taken) = if margin.was_cut_left() { + // We have stripped some code/whitespace from the beginning, make it clear. + let mut bytes_taken = 0; + let mut width_taken = 0; + for ch in code.chars() { + width_taken += char_width(ch); + bytes_taken += ch.len_utf8(); + + if width_taken >= padding { + break; + } + } + + buffer.puts( + line_offset, + code_offset, + cut_indicator, + ElementStyle::LineNumber, + ); + (width_taken, bytes_taken) + } else { + (0, 0) + }; + + buffer.puts( + line_offset, + code_offset + width_taken, + &code[bytes_taken..], + ElementStyle::Quotation, + ); + + if line_len > right { + // We have stripped some code/whitespace from the beginning, make it clear. + let mut char_taken = 0; + let mut width_taken_inner = 0; + for ch in code.chars().rev() { + width_taken_inner += char_width(ch); + char_taken += 1; + + if width_taken_inner >= padding { + break; + } + } + + buffer.puts( + line_offset, + code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken, + cut_indicator, + ElementStyle::LineNumber, + ); + } + + buffer.puts( + line_offset, + 0, + &maybe_anonymized(renderer, line_index, max_line_num_len), + ElementStyle::LineNumber, + ); + + draw_col_separator_no_space(renderer, buffer, line_offset, width_offset - 2); + + left +} + +fn draw_range( + buffer: &mut StyledBuffer, + symbol: char, + line: usize, + col_from: usize, + col_to: usize, + style: ElementStyle, +) { + for col in col_from..col_to { + buffer.putc(line, col, symbol, style); + } +} + +fn draw_multiline_line( + renderer: &Renderer, + buffer: &mut StyledBuffer, + line: usize, + offset: usize, + depth: usize, + style: ElementStyle, + elided: bool, +) { + let chr = match (style, renderer.decor_style) { + (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Ascii) => '|', + (_, DecorStyle::Ascii) => '|', + (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Unicode) => { + if elided { + '┇' + } else { + '┃' + } + } + (_, DecorStyle::Unicode) => { + if elided { + '┆' + } else { + '│' + } + } + }; + buffer.putc(line, offset + depth - 1, chr, style); +} + +fn draw_col_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) { + let chr = renderer.decor_style.col_separator(); + buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber); +} + +fn draw_col_separator_no_space( + renderer: &Renderer, + buffer: &mut StyledBuffer, + line: usize, + col: usize, +) { + let chr = renderer.decor_style.col_separator(); + draw_col_separator_no_space_with_style(buffer, chr, line, col, ElementStyle::LineNumber); +} + +fn draw_col_separator_start( + renderer: &Renderer, + buffer: &mut StyledBuffer, + line: usize, + col: usize, +) { + match renderer.decor_style { + DecorStyle::Ascii => { + draw_col_separator_no_space_with_style( + buffer, + '|', + line, + col, + ElementStyle::LineNumber, + ); + } + DecorStyle::Unicode => { + draw_col_separator_no_space_with_style( + buffer, + '╭', + line, + col, + ElementStyle::LineNumber, + ); + draw_col_separator_no_space_with_style( + buffer, + '╴', + line, + col + 1, + ElementStyle::LineNumber, + ); + } + } +} + +fn draw_col_separator_end(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) { + match renderer.decor_style { + DecorStyle::Ascii => { + draw_col_separator_no_space_with_style( + buffer, + '|', + line, + col, + ElementStyle::LineNumber, + ); + } + DecorStyle::Unicode => { + draw_col_separator_no_space_with_style( + buffer, + '╰', + line, + col, + ElementStyle::LineNumber, + ); + draw_col_separator_no_space_with_style( + buffer, + '╴', + line, + col + 1, + ElementStyle::LineNumber, + ); + } + } +} + +fn draw_col_separator_no_space_with_style( + buffer: &mut StyledBuffer, + chr: char, + line: usize, + col: usize, + style: ElementStyle, +) { + buffer.putc(line, col, chr, style); +} + +fn maybe_anonymized(renderer: &Renderer, line_num: usize, max_line_num_len: usize) -> String { + format!( + "{:>max_line_num_len$}", + if renderer.anonymized_line_numbers { + Cow::Borrowed(ANONYMIZED_LINE_NUM) + } else { + Cow::Owned(line_num.to_string()) + } + ) +} + +fn draw_note_separator( + renderer: &Renderer, + buffer: &mut StyledBuffer, + line: usize, + col: usize, + is_cont: bool, +) { + let chr = renderer.decor_style.note_separator(is_cont); + buffer.puts(line, col, chr, ElementStyle::LineNumber); +} + +fn draw_line_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) { + let (column, dots) = match renderer.decor_style { + DecorStyle::Ascii => (0, "..."), + DecorStyle::Unicode => (col - 2, "┆"), + }; + buffer.puts(line, column, dots, ElementStyle::LineNumber); +} + +trait MessageOrTitle { + fn level(&self) -> &Level<'_>; + fn id(&self) -> Option<&Id<'_>>; + fn text(&self) -> &str; + fn allows_styling(&self) -> bool; + fn is_fixable(&self) -> bool; +} + +impl MessageOrTitle for Title<'_> { + fn level(&self) -> &Level<'_> { + &self.level + } + fn id(&self) -> Option<&Id<'_>> { + self.id.as_ref() + } + fn text(&self) -> &str { + self.text.as_ref() + } + fn allows_styling(&self) -> bool { + self.allows_styling + } + fn is_fixable(&self) -> bool { + self.is_fixable + } +} + +impl MessageOrTitle for Message<'_> { + fn level(&self) -> &Level<'_> { + &self.level + } + fn id(&self) -> Option<&Id<'_>> { + None + } + fn text(&self) -> &str { + self.text.as_ref() + } + fn allows_styling(&self) -> bool { + true + } + fn is_fixable(&self) -> bool { + false + } +} + +/// Count extra display columns from tabs in the first `n` chars of `s`. +/// Each tab is displayed as 4 spaces, so the extra width per tab is 3. +fn extra_width_from_tabs(s: &str, n: usize) -> usize { + s.chars().take(n).filter(|&ch| ch == '\t').count() * 3 +} + +// instead of taking the String length or dividing by 10 while > 0, we multiply a limit by 10 until +// we're higher. If the loop isn't exited by the `return`, the last multiplication will wrap, which +// is OK, because while we cannot fit a higher power of 10 in a usize, the loop will end anyway. +// This is also why we need the max number of decimal digits within a `usize`. +fn num_decimal_digits(num: Option) -> usize { + #[cfg(target_pointer_width = "64")] + const MAX_DIGITS: usize = 20; + + #[cfg(target_pointer_width = "32")] + const MAX_DIGITS: usize = 10; + + #[cfg(target_pointer_width = "16")] + const MAX_DIGITS: usize = 5; + + let Some(num) = num else { + return 0; + }; + + let mut lim = 10; + for num_digits in 1..MAX_DIGITS { + if num < lim { + return num_digits; + } + lim = lim.wrapping_mul(10); + } + MAX_DIGITS +} + +fn str_width(s: &str) -> usize { + s.chars().map(char_width).sum() +} + +pub(crate) fn char_width(ch: char) -> usize { + // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is. For now, + // just accept that sometimes the code line will be longer than desired. + match ch { + '\t' => 4, + // Keep the following list in sync with `rustc_errors::emitter::OUTPUT_REPLACEMENTS`. These + // are control points that we replace before printing with a visible codepoint for the sake + // of being able to point at them with underlines. + '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}' + | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}' + | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}' + | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}' + | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}' + | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}' + | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1, + _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1), + } +} + +pub(crate) fn num_overlap( + a_start: usize, + a_end: usize, + b_start: usize, + b_end: usize, + inclusive: bool, +) -> bool { + let extra = usize::from(inclusive); + (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start) +} + +fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool { + num_overlap( + a1.start.display, + a1.end.display + padding, + a2.start.display, + a2.end.display, + false, + ) +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) enum LineAnnotationType { + /// Annotation under a single line of code + Singleline, + + // The Multiline type above is replaced with the following three in order + // to reuse the current label drawing code. + // + // Each of these corresponds to one part of the following diagram: + // + // x | foo(1 + bar(x, + // | _________^ < MultilineStart + // x | | y), < MultilineLine + // | |______________^ label < MultilineEnd + // x | z); + /// Annotation marking the first character of a fully shown multiline span + MultilineStart(usize), + /// Annotation marking the last character of a fully shown multiline span + MultilineEnd(usize), + /// Line at the left enclosing the lines of a fully shown multiline span + // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4 + // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in + // `draw_multiline_line`. + MultilineLine(usize), +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct LineAnnotation<'a> { + /// Start column. + /// Note that it is important that this field goes + /// first, so that when we sort, we sort orderings by start + /// column. + pub start: Loc, + + /// End column within the line (exclusive) + pub end: Loc, + + /// level + pub kind: AnnotationKind, + + /// Optional label to display adjacent to the annotation. + pub label: Option>, + + /// Is this a single line, multiline or multiline span minimized down to a + /// smaller span. + pub annotation_type: LineAnnotationType, + + /// Whether the source code should be highlighted + pub highlight_source: bool, +} + +impl LineAnnotation<'_> { + pub(crate) fn is_primary(&self) -> bool { + self.kind == AnnotationKind::Primary + } + + /// Whether this annotation is a vertical line placeholder. + pub(crate) fn is_line(&self) -> bool { + matches!(self.annotation_type, LineAnnotationType::MultilineLine(_)) + } + + /// Length of this annotation as displayed in the stderr output + pub(crate) fn len(&self) -> usize { + // Account for usize underflows + self.end.display.abs_diff(self.start.display) + } + + pub(crate) fn has_label(&self) -> bool { + if let Some(label) = &self.label { + // Consider labels with no text as effectively not being there + // to avoid weird output with unnecessary vertical lines, like: + // + // X | fn foo(x: u32) { + // | -------^------ + // | | | + // | | + // | + // + // Note that this would be the complete output users would see. + !label.is_empty() + } else { + false + } + } + + pub(crate) fn takes_space(&self) -> bool { + // Multiline annotations always have to keep vertical space. + matches!( + self.annotation_type, + LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_) + ) + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum DisplaySuggestion { + Underline, + Diff, + None, + Add, +} + +impl DisplaySuggestion { + fn new(complete: &str, patches: &[TrimmedPatch<'_>], sm: &SourceMap<'_>) -> Self { + let has_deletion = patches + .iter() + .any(|p| p.is_deletion(sm) || p.is_destructive_replacement(sm)); + let is_multiline = complete.lines().count() > 1; + if has_deletion && !is_multiline { + DisplaySuggestion::Diff + } else if patches.len() == 1 + && patches.first().is_some_and(|p| { + p.replacement.ends_with('\n') && p.replacement.trim() == complete.trim() + }) + { + // We are adding a line(s) of code before code that was already there. + DisplaySuggestion::Add + } else if (patches.len() != 1 || patches[0].replacement.trim() != complete.trim()) + && !is_multiline + { + DisplaySuggestion::Underline + } else { + DisplaySuggestion::None + } + } +} + +// We replace some characters so the CLI output is always consistent and underlines aligned. +// Keep the following list in sync with `rustc_span::char_width`. +const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[ + // In terminals without Unicode support the following will be garbled, but in *all* terminals + // the underlying codepoint will be as well. We could gate this replacement behind a "unicode + // support" gate. + ('\0', "␀"), + ('\u{0001}', "␁"), + ('\u{0002}', "␂"), + ('\u{0003}', "␃"), + ('\u{0004}', "␄"), + ('\u{0005}', "␅"), + ('\u{0006}', "␆"), + ('\u{0007}', "␇"), + ('\u{0008}', "␈"), + ('\t', " "), // We do our own tab replacement + ('\u{000b}', "␋"), + ('\u{000c}', "␌"), + ('\u{000d}', "␍"), + ('\u{000e}', "␎"), + ('\u{000f}', "␏"), + ('\u{0010}', "␐"), + ('\u{0011}', "␑"), + ('\u{0012}', "␒"), + ('\u{0013}', "␓"), + ('\u{0014}', "␔"), + ('\u{0015}', "␕"), + ('\u{0016}', "␖"), + ('\u{0017}', "␗"), + ('\u{0018}', "␘"), + ('\u{0019}', "␙"), + ('\u{001a}', "␚"), + ('\u{001b}', "␛"), + ('\u{001c}', "␜"), + ('\u{001d}', "␝"), + ('\u{001e}', "␞"), + ('\u{001f}', "␟"), + ('\u{007f}', "␡"), + ('\u{200d}', ""), // Replace ZWJ for consistent terminal output of grapheme clusters. + ('\u{202a}', "�"), // The following unicode text flow control characters are inconsistently + ('\u{202b}', "�"), // supported across CLIs and can cause confusion due to the bytes on disk + ('\u{202c}', "�"), // not corresponding to the visible source code, so we replace them always. + ('\u{202d}', "�"), + ('\u{202e}', "�"), + ('\u{2066}', "�"), + ('\u{2067}', "�"), + ('\u{2068}', "�"), + ('\u{2069}', "�"), +]; + +pub(crate) fn normalize_whitespace(s: &str) -> Cow<'_, str> { + if !s + .chars() + .any(|user| OUTPUT_REPLACEMENTS.iter().any(|(bad, _)| user == *bad)) + { + return Cow::Borrowed(s); + } + + // Scan the input string for a character in the ordered table above. + // If it's present, replace it with its alternative string (it can be more than 1 char!). + // Otherwise, retain the input char. + let normalized = s.chars().fold(String::with_capacity(s.len()), |mut s, c| { + match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) { + Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1), + _ => s.push(c), + } + s + }); + Cow::Owned(normalized) +} + +#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) enum ElementStyle { + MainHeaderMsg, + HeaderMsg, + LineAndColumn, + LineNumber, + Quotation, + UnderlinePrimary, + UnderlineSecondary, + LabelPrimary, + LabelSecondary, + NoStyle, + Level(LevelInner), + Addition, + Removal, +} + +impl ElementStyle { + pub(crate) fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style { + match self { + ElementStyle::Addition => stylesheet.addition, + ElementStyle::Removal => stylesheet.removal, + ElementStyle::LineAndColumn => stylesheet.none, + ElementStyle::LineNumber => stylesheet.line_num, + ElementStyle::Quotation => stylesheet.none, + ElementStyle::MainHeaderMsg => stylesheet.emphasis, + ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary => level.style(stylesheet), + ElementStyle::UnderlineSecondary | ElementStyle::LabelSecondary => stylesheet.context, + ElementStyle::HeaderMsg | ElementStyle::NoStyle => stylesheet.none, + ElementStyle::Level(lvl) => lvl.style(stylesheet), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) struct UnderlineParts { + pub(crate) style: ElementStyle, + pub(crate) underline: char, + pub(crate) label_start: char, + pub(crate) vertical_text_line: char, + pub(crate) multiline_vertical: char, + pub(crate) multiline_horizontal: char, + pub(crate) multiline_whole_line: char, + pub(crate) multiline_start_down: char, + pub(crate) bottom_right: char, + pub(crate) top_left: char, + pub(crate) top_right_flat: char, + pub(crate) bottom_left: char, + pub(crate) multiline_end_up: char, + pub(crate) multiline_end_same_line: char, + pub(crate) multiline_bottom_right_with_text: char, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum TitleStyle { + MainHeader, + Header, + Secondary, +} + +struct PreProcessedGroup<'a> { + group: &'a Group<'a>, + elements: Vec>, + primary_path: Option<&'a Cow<'a, str>>, + max_depth: usize, +} + +enum PreProcessedElement<'a> { + Message(&'a Message<'a>), + Cause( + ( + &'a Snippet<'a, Annotation<'a>>, + SourceMap<'a>, + Vec>, + ), + ), + Suggestion( + ( + &'a Snippet<'a, Patch<'a>>, + SourceMap<'a>, + SplicedLines<'a>, + DisplaySuggestion, + ), + ), + Origin(&'a Origin<'a>), + Padding(Padding), +} + +fn pre_process<'a>( + groups: &'a [Group<'a>], +) -> ( + Option, + Option<&'a Cow<'a, str>>, + Vec>, +) { + let mut max_line_num = None; + let mut og_primary_path = None; + let mut out = Vec::with_capacity(groups.len()); + for group in groups { + let mut elements = Vec::with_capacity(group.elements.len()); + let mut primary_path = None; + let mut max_depth = 0; + for element in &group.elements { + match element { + Element::Message(message) => { + elements.push(PreProcessedElement::Message(message)); + } + Element::Cause(cause) => { + let sm = SourceMap::new(&cause.source, cause.line_start); + let (depth, annotated_lines) = + sm.annotated_lines(cause.markers.clone(), cause.fold); + + let show_snippet = !cause.markers.iter().any(|s| s.is_file_level); + if show_snippet { + if cause.fold { + let end = cause + .markers + .iter() + .map(|a| a.span.end) + .max() + .unwrap_or(cause.source.len()) + .min(cause.source.len()); + + max_line_num = Some(max( + cause.line_start + newline_count(&cause.source[..end]), + max_line_num.unwrap_or(0), + )); + } else { + max_line_num = Some(max( + cause.line_start + newline_count(&cause.source), + max_line_num.unwrap_or(0), + )); + } + max_depth = max(depth, max_depth); + } + + if primary_path.is_none() { + primary_path = Some(cause.path.as_ref()); + } + elements.push(PreProcessedElement::Cause((cause, sm, annotated_lines))); + } + Element::Suggestion(suggestion) => { + let sm = SourceMap::new(&suggestion.source, suggestion.line_start); + if let Some((complete, patches, highlights, replaced_highlights)) = + sm.splice_lines(suggestion.markers.clone(), suggestion.fold) + { + let display_suggestion = DisplaySuggestion::new(&complete, &patches, &sm); + + if suggestion.fold { + if let Some(first) = patches.first() { + let (l_start, _) = + sm.span_to_locations(first.original_span.clone()); + let nc = newline_count(&complete); + let sugg_max_line_num = match display_suggestion { + DisplaySuggestion::Underline => l_start.line, + DisplaySuggestion::Diff => { + let file_lines = sm.span_to_lines(first.span.clone()); + file_lines + .last() + .map_or(l_start.line + nc, |line| line.line_index) + } + DisplaySuggestion::None => l_start.line + nc, + DisplaySuggestion::Add => l_start.line + nc, + }; + max_line_num = + Some(max(sugg_max_line_num, max_line_num.unwrap_or(0))); + } + } else { + max_line_num = Some(max( + suggestion.line_start + newline_count(&complete), + max_line_num.unwrap_or(0), + )); + } + + elements.push(PreProcessedElement::Suggestion(( + suggestion, + sm, + (complete, patches, highlights, replaced_highlights), + display_suggestion, + ))); + } + } + Element::Origin(origin) => { + if primary_path.is_none() { + primary_path = Some(Some(&origin.path)); + } + elements.push(PreProcessedElement::Origin(origin)); + } + Element::Padding(padding) => { + elements.push(PreProcessedElement::Padding(padding.clone())); + } + } + } + let group = PreProcessedGroup { + group, + elements, + primary_path: primary_path.unwrap_or_default(), + max_depth, + }; + if og_primary_path.is_none() && group.primary_path.is_some() { + og_primary_path = group.primary_path; + } + out.push(group); + } + + (max_line_num, og_primary_path, out) +} + +fn newline_count(body: &str) -> usize { + #[cfg(feature = "simd")] + { + // Trailing newlines do not count towards the number of lines + // (this is based into `str::lines`) + let trailing_newline = body.ends_with('\n'); + memchr::memchr_iter(b'\n', body.as_bytes()).count() - usize::from(trailing_newline) + } + #[cfg(not(feature = "simd"))] + { + body.lines().count().saturating_sub(1) + } +} + +#[cfg(test)] +mod test { + use super::{OUTPUT_REPLACEMENTS, newline_count}; + use snapbox::IntoData; + + fn format_replacements(replacements: Vec<(char, &str)>) -> String { + replacements + .into_iter() + .map(|r| format!(" {r:?}")) + .collect::>() + .join("\n") + } + + #[test] + /// The [`OUTPUT_REPLACEMENTS`] array must be sorted (for binary search to + /// work) and must contain no duplicate entries + fn ensure_output_replacements_is_sorted() { + let mut expected = OUTPUT_REPLACEMENTS.to_owned(); + expected.sort_by_key(|r| r.0); + expected.dedup_by_key(|r| r.0); + let expected = format_replacements(expected); + let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned()); + snapbox::assert_data_eq!(actual, expected.into_data().raw()); + } + + #[test] + fn ensure_newline_count_correct() { + let source = r#" + cargo-features = ["path-bases"] + + [package] + name = "foo" + version = "0.5.0" + authors = ["wycats@example.com"] + + [dependencies] + bar = { base = '^^not-valid^^', path = 'bar' } + "#; + assert_eq!(newline_count(source), 10); + + assert_eq!(newline_count(""), 0); + + assert_eq!(newline_count("one"), 0); + + assert_eq!(newline_count("one\n"), 0); + + assert_eq!(newline_count("one\ntwo"), 1); + + assert_eq!(newline_count("one\ntwo\n"), 1); + + assert_eq!(newline_count("one\n\n"), 1); + + assert_eq!(newline_count("one\r\ntwo\r\n"), 1); + } +} diff --git a/crates/ruff_annotate_snippets/src/renderer/source_map.rs b/crates/ruff_annotate_snippets/src/renderer/source_map.rs new file mode 100644 index 0000000000..e0d9f21609 --- /dev/null +++ b/crates/ruff_annotate_snippets/src/renderer/source_map.rs @@ -0,0 +1,828 @@ +use alloc::borrow::Cow; +use alloc::string::String; +use alloc::{vec, vec::Vec}; +use core::cmp::{max, min}; +use core::ops::Range; + +use crate::renderer::{LineAnnotation, LineAnnotationType, char_width, num_overlap}; +use crate::{Annotation, AnnotationKind, Patch}; + +#[derive(Debug)] +pub(crate) struct SourceMap<'a> { + lines: Vec>, + pub(crate) source: &'a str, +} + +impl<'a> SourceMap<'a> { + pub(crate) fn new(source: &'a str, line_start: usize) -> Self { + // Empty sources do have a "line", but it is empty, so we need to add + // a line with an empty string to the source map. + if source.is_empty() { + return Self { + lines: vec![LineInfo { + line: "", + line_index: line_start, + start_byte: 0, + end_byte: 0, + end_line_size: 0, + }], + source, + }; + } + + let mut current_index = 0; + + let mut mapping = vec![]; + for (idx, (line, end_line)) in CursorLines::new(source).enumerate() { + let line_length = line.len(); + let line_range = current_index..current_index + line_length; + let end_line_size = end_line.len(); + + mapping.push(LineInfo { + line, + line_index: line_start + idx, + start_byte: line_range.start, + end_byte: line_range.end + end_line_size, + end_line_size, + }); + + current_index += line_length + end_line_size; + } + Self { + lines: mapping, + source, + } + } + + pub(crate) fn get_line(&self, idx: usize) -> Option<&'a str> { + self.lines + .iter() + .find(|l| l.line_index == idx) + .map(|info| info.line) + } + + pub(crate) fn span_to_locations(&self, span: Range) -> (Loc, Loc) { + let start_info = self + .lines + .iter() + .find(|info| span.start >= info.start_byte && span.start < info.end_byte) + .unwrap_or(self.lines.last().unwrap()); + let (mut start_char_pos, start_display_pos) = start_info.line + [0..(span.start - start_info.start_byte).min(start_info.line.len())] + .chars() + .fold((0, 0), |(char_pos, byte_pos), c| { + let display = char_width(c); + (char_pos + 1, byte_pos + display) + }); + // correct the char pos if we are highlighting the end of a line + if (span.start - start_info.start_byte).saturating_sub(start_info.line.len()) > 0 { + start_char_pos += 1; + } + let start = Loc { + line: start_info.line_index, + char: start_char_pos, + display: start_display_pos, + byte: span.start, + }; + + if span.start == span.end { + return (start, start); + } + + let (end_idx, end_info, eof) = self + .lines + .iter() + .enumerate() + .find(|(_, info)| span.end >= info.start_byte && span.end < info.end_byte) + .map(|(idx, info)| (idx, info, false)) + .unwrap_or((self.lines.len() - 1, self.lines.last().unwrap(), true)); + let (end_char_pos, end_display_pos) = end_info.line + [0..(span.end - end_info.start_byte).min(end_info.line.len())] + .chars() + .fold((0, 0), |(char_pos, byte_pos), c| { + let display = char_width(c); + (char_pos + 1, byte_pos + display) + }); + + let mut end = Loc { + line: end_info.line_index, + char: end_char_pos, + display: end_display_pos, + byte: span.end, + }; + if start.line < end.line && end.char == 0 && !eof { + let prev_line_info = &self.lines[end_idx - 1]; + let (end_char_pos, end_display_pos) = prev_line_info.line + [0..(span.end - prev_line_info.start_byte).min(prev_line_info.line.len())] + .chars() + .fold((0, 0), |(char_pos, byte_pos), c| { + let display = char_width(c); + (char_pos + 1, byte_pos + display) + }); + if prev_line_info.end_byte == start.byte { + end = Loc { + line: prev_line_info.line_index, + char: end_char_pos + 1, + display: end_display_pos + 1, + byte: span.end, + }; + } else { + end = Loc { + line: prev_line_info.line_index, + char: end_char_pos, + display: end_display_pos, + byte: span.end, + }; + } + } + if start.line != end.line && end.byte > end_info.end_byte - end_info.end_line_size { + end.char += 1; + end.display += 1; + } + + (start, end) + } + + pub(crate) fn span_to_snippet(&self, span: Range) -> Option<&str> { + self.source.get(span) + } + + pub(crate) fn span_to_lines(&self, span: Range) -> Vec<&LineInfo<'a>> { + let mut lines = vec![]; + let start = span.start; + let end = span.end; + for line_info in &self.lines { + if start >= line_info.end_byte { + continue; + } + if end < line_info.start_byte { + break; + } + lines.push(line_info); + } + + if lines.is_empty() && !self.lines.is_empty() { + lines.push(self.lines.last().unwrap()); + } + + lines + } + + pub(crate) fn annotated_lines( + &self, + annotations: Vec>, + fold: bool, + ) -> (usize, Vec>) { + let source_len = self.source.len(); + if let Some(bigger) = annotations.iter().find_map(|x| { + // Allow highlighting one past the last character in the source. + if source_len + 1 < x.span.end { + Some(&x.span) + } else { + None + } + }) { + panic!("Annotation range `{bigger:?}` is beyond the end of buffer `{source_len}`") + } + + let mut annotated_line_infos = self + .lines + .iter() + .map(|info| AnnotatedLineInfo { + line: info.line, + line_index: info.line_index, + annotations: vec![], + keep: false, + }) + .collect::>(); + let mut multiline_annotations = vec![]; + + for Annotation { + span, + label, + kind, + highlight_source, + is_file_level: _, + } in annotations + { + let (lo, mut hi) = self.span_to_locations(span.clone()); + if kind == AnnotationKind::Visible { + for line_idx in lo.line..=hi.line { + self.keep_line(&mut annotated_line_infos, line_idx); + } + continue; + } + // Watch out for "empty spans". If we get a span like 6..6, we + // want to just display a `^` at 6, so convert that to + // 6..7. This is degenerate input, but it's best to degrade + // gracefully -- and the parser likes to supply a span like + // that for EOF, in particular. + + if lo.display == hi.display && lo.line == hi.line { + hi.display += 1; + } + + if lo.line == hi.line { + let line_ann = LineAnnotation { + start: lo, + end: hi, + kind, + label, + annotation_type: LineAnnotationType::Singleline, + highlight_source, + }; + self.add_annotation_to_file(&mut annotated_line_infos, lo.line, line_ann); + } else { + multiline_annotations.push(MultilineAnnotation { + depth: 1, + start: lo, + end: hi, + kind, + label, + overlaps_exactly: false, + highlight_source, + }); + } + } + + let mut primary_spans = vec![]; + + // Find overlapping multiline annotations, put them at different depths + multiline_annotations.sort_by_key(|ml| (ml.start.line, usize::MAX - ml.end.line)); + for (outer_i, ann) in multiline_annotations.clone().into_iter().enumerate() { + if ann.kind.is_primary() { + primary_spans.push((ann.start, ann.end)); + } + for (inner_i, a) in &mut multiline_annotations.iter_mut().enumerate() { + // Move all other multiline annotations overlapping with this one + // one level to the right. + if !ann.same_span(a) + && num_overlap(ann.start.line, ann.end.line, a.start.line, a.end.line, true) + { + a.increase_depth(); + } else if ann.same_span(a) && outer_i != inner_i { + a.overlaps_exactly = true; + } else { + if primary_spans + .iter() + .any(|(s, e)| a.start == *s && a.end == *e) + { + a.kind = AnnotationKind::Primary; + } + break; + } + } + } + + let mut max_depth = 0; // max overlapping multiline spans + for ann in &multiline_annotations { + max_depth = max(max_depth, ann.depth); + } + // Change order of multispan depth to minimize the number of overlaps in the ASCII art. + for a in &mut multiline_annotations { + a.depth = max_depth - a.depth + 1; + } + for ann in multiline_annotations { + let mut end_ann = ann.as_end(); + if ann.overlaps_exactly { + end_ann.annotation_type = LineAnnotationType::Singleline; + } else { + // avoid output like + // + // | foo( + // | _____^ + // | |_____| + // | || bar, + // | || ); + // | || ^ + // | ||______| + // | |______foo + // | baz + // + // and instead get + // + // | foo( + // | _____^ + // | | bar, + // | | ); + // | | ^ + // | | | + // | |______foo + // | baz + self.add_annotation_to_file( + &mut annotated_line_infos, + ann.start.line, + ann.as_start(), + ); + // 4 is the minimum vertical length of a multiline span when presented: two lines + // of code and two lines of underline. This is not true for the special case where + // the beginning doesn't have an underline, but the current logic seems to be + // working correctly. + let middle = min(ann.start.line + 4, ann.end.line); + // We'll show up to 4 lines past the beginning of the multispan start. + // We will *not* include the tail of lines that are only whitespace, a comment or + // a bare delimiter. + let filter = |s: &str| { + let s = s.trim(); + // Consider comments as empty, but don't consider docstrings to be empty. + !(s.starts_with("//") && !(s.starts_with("///") || s.starts_with("//!"))) + // Consider lines with nothing but whitespace, a single delimiter as empty. + && !["", "{", "}", "(", ")", "[", "]"].contains(&s) + }; + let until = (ann.start.line..middle) + .rev() + .filter_map(|line| self.get_line(line).map(|s| (line + 1, s))) + .find(|(_, s)| filter(s)) + .map_or(ann.start.line, |(line, _)| line); + for line in ann.start.line + 1..until { + // Every `|` that joins the beginning of the span (`___^`) to the end (`|__^`). + self.add_annotation_to_file(&mut annotated_line_infos, line, ann.as_line()); + } + let line_end = ann.end.line - 1; + let end_is_empty = self.get_line(line_end).is_some_and(|s| !filter(s)); + if middle < line_end && !end_is_empty { + self.add_annotation_to_file(&mut annotated_line_infos, line_end, ann.as_line()); + } + } + self.add_annotation_to_file(&mut annotated_line_infos, end_ann.end.line, end_ann); + } + + if fold { + annotated_line_infos.retain(|l| !l.annotations.is_empty() || l.keep); + } + + (max_depth, annotated_line_infos) + } + + fn add_annotation_to_file( + &self, + annotated_line_infos: &mut Vec>, + line_index: usize, + line_ann: LineAnnotation<'a>, + ) { + if let Some(line_info) = annotated_line_infos + .iter_mut() + .find(|line_info| line_info.line_index == line_index) + { + line_info.annotations.push(line_ann); + } else { + let info = self + .lines + .iter() + .find(|l| l.line_index == line_index) + .unwrap(); + annotated_line_infos.push(AnnotatedLineInfo { + line: info.line, + line_index, + annotations: vec![line_ann], + keep: false, + }); + annotated_line_infos.sort_by_key(|l| l.line_index); + } + } + + fn keep_line(&self, annotated_line_infos: &mut Vec>, line_index: usize) { + if let Some(line_info) = annotated_line_infos + .iter_mut() + .find(|line_info| line_info.line_index == line_index) + { + line_info.keep = true; + } else { + let info = self + .lines + .iter() + .find(|l| l.line_index == line_index) + .unwrap(); + annotated_line_infos.push(AnnotatedLineInfo { + line: info.line, + line_index, + annotations: vec![], + keep: true, + }); + annotated_line_infos.sort_by_key(|l| l.line_index); + } + } + + pub(crate) fn splice_lines<'b>( + &'a self, + mut patches: Vec>, + fold: bool, + ) -> Option> { + fn push_trailing(buf: &mut String, line_opt: Option<&str>, lo: &Loc, hi_opt: Option<&Loc>) { + // Convert CharPos to Usize, as CharPose is character offset + // Extract low index and high index + let (lo, hi_opt) = (lo.char, hi_opt.map(|hi| hi.char)); + if let Some(line) = line_opt { + if let Some(lo) = line.char_indices().map(|(i, _)| i).nth(lo) { + // Get high index while account for rare unicode and emoji with char_indices + let hi_opt = hi_opt.and_then(|hi| line.char_indices().map(|(i, _)| i).nth(hi)); + match hi_opt { + // If high index exist, take string from low to high index + Some(hi) if hi > lo => buf.push_str(&line[lo..hi]), + Some(_) => (), + // If high index absence, take string from low index till end string.len + None => buf.push_str(&line[lo..]), + } + } + // If high index is None + if hi_opt.is_none() { + buf.push('\n'); + } + } + } + + let source_len = self.source.len(); + if let Some(bigger) = patches.iter().find_map(|x| { + // Allow patching one past the last character in the source. + if source_len + 1 < x.span.end { + Some(&x.span) + } else { + None + } + }) { + panic!("Patch span `{bigger:?}` is beyond the end of buffer `{source_len}`") + } + + // Assumption: all spans are in the same file, and all spans + // are disjoint. Sort in ascending order. + patches.sort_by_key(|p| p.span.start); + + // Find the bounding span. + let (lo, hi) = if fold { + let lo = patches.iter().map(|p| p.span.start).min()?; + let hi = patches.iter().map(|p| p.span.end).max()?; + (lo, hi) + } else { + (0, source_len) + }; + + let lines = self.span_to_lines(lo..hi); + + let mut highlights = vec![]; + // To build up the result, we do this for each span: + // - push the line segment trailing the previous span + // (at the beginning a "phantom" span pointing at the start of the line) + // - push lines between the previous and current span (if any) + // - if the previous and current span are not on the same line + // push the line segment leading up to the current span + // - splice in the span substitution + // + // Finally push the trailing line segment of the last span + let (mut prev_hi, _) = self.span_to_locations(lo..hi); + prev_hi.char = 0; + let mut prev_line = lines.first().map(|line| line.line); + let mut buf = String::new(); + + let trimmed_patches = patches + .into_iter() + // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the + // suggestion and snippet to look as if we just suggested to add + // `"b"`, which is typically much easier for the user to understand. + .map(|part| part.trim_trivial_replacements(self.source)) + .collect::>(); + let mut line_highlight = vec![]; + // We need to keep track of the difference between the existing code and the added + // or deleted code in order to point at the correct column *after* substitution. + let mut acc = 0; + for part in &trimmed_patches { + let (cur_lo, cur_hi) = self.span_to_locations(part.span.clone()); + if prev_hi.line == cur_lo.line { + push_trailing(&mut buf, prev_line, &prev_hi, Some(&cur_lo)); + } else { + acc = 0; + highlights.push(core::mem::take(&mut line_highlight)); + push_trailing(&mut buf, prev_line, &prev_hi, None); + // push lines between the previous and current span (if any) + for idx in prev_hi.line + 1..(cur_lo.line) { + if let Some(line) = self.get_line(idx) { + buf.push_str(line.as_ref()); + buf.push('\n'); + highlights.push(core::mem::take(&mut line_highlight)); + } + } + if let Some(cur_line) = self.get_line(cur_lo.line) { + let end = match cur_line.char_indices().nth(cur_lo.char) { + Some((i, _)) => i, + None => cur_line.len(), + }; + buf.push_str(&cur_line[..end]); + } + } + // Add a whole line highlight per line in the snippet. + let len: isize = part + .replacement + .split('\n') + .next() + .unwrap_or(&part.replacement) + .chars() + .map(|c| match c { + '\t' => 4, + _ => 1, + }) + .sum(); + line_highlight.push(SubstitutionHighlight { + start: (cur_lo.char as isize + acc) as usize, + end: (cur_lo.char as isize + acc + len) as usize, + }); + buf.push_str(&part.replacement); + // Account for the difference between the width of the current code and the + // snippet being suggested, so that the *later* suggestions are correctly + // aligned on the screen. Note that cur_hi and cur_lo can be on different + // lines, so cur_hi.col can be smaller than cur_lo.col + acc += len - (cur_hi.char as isize - cur_lo.char as isize); + prev_hi = cur_hi; + prev_line = self.get_line(prev_hi.line); + for line in part.replacement.split('\n').skip(1) { + acc = 0; + highlights.push(core::mem::take(&mut line_highlight)); + let end: usize = line + .chars() + .map(|c| match c { + '\t' => 4, + _ => 1, + }) + .sum(); + line_highlight.push(SubstitutionHighlight { start: 0, end }); + } + } + highlights.push(core::mem::take(&mut line_highlight)); + if fold { + // if the replacement already ends with a newline, don't print the next line + if !buf.ends_with('\n') { + push_trailing(&mut buf, prev_line, &prev_hi, None); + } + } else { + // Add the trailing part of the source after the last patch + if let Some(snippet) = self.span_to_snippet(prev_hi.byte..source_len) { + buf.push_str(snippet); + for _ in snippet.matches('\n') { + highlights.push(core::mem::take(&mut line_highlight)); + } + } + } + // remove trailing newlines + while buf.ends_with('\n') { + buf.pop(); + } + + let (bounding_lo, bounding_hi) = self.span_to_locations(lo..hi); + let line_count = bounding_hi.line.saturating_sub(bounding_lo.line) + 1; + let mut replaced_highlights: Vec> = vec![Vec::new(); line_count]; + for part in &trimmed_patches { + let (cur_lo, cur_hi) = self.span_to_locations(part.span.clone()); + for line in cur_lo.line..=cur_hi.line { + let start = if line == cur_lo.line { cur_lo.char } else { 0 }; + let end = if line == cur_hi.line { + cur_hi.char + } else { + self.get_line(line).unwrap_or_default().chars().count() + }; + replaced_highlights[line - bounding_lo.line] + .push(SubstitutionHighlight { start, end }); + } + } + + if highlights.iter().all(|parts| parts.is_empty()) { + None + } else { + Some((buf, trimmed_patches, highlights, replaced_highlights)) + } + } +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct MultilineAnnotation<'a> { + pub depth: usize, + pub start: Loc, + pub end: Loc, + pub kind: AnnotationKind, + pub label: Option>, + pub overlaps_exactly: bool, + pub highlight_source: bool, +} + +impl<'a> MultilineAnnotation<'a> { + pub(crate) fn increase_depth(&mut self) { + self.depth += 1; + } + + /// Compare two `MultilineAnnotation`s considering only the `Span` they cover. + pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool { + self.start == other.start && self.end == other.end + } + + pub(crate) fn as_start(&self) -> LineAnnotation<'a> { + LineAnnotation { + start: self.start, + end: Loc { + line: self.start.line, + char: self.start.char + 1, + display: self.start.display + 1, + byte: self.start.byte + 1, + }, + kind: self.kind, + label: None, + annotation_type: LineAnnotationType::MultilineStart(self.depth), + highlight_source: self.highlight_source, + } + } + + pub(crate) fn as_end(&self) -> LineAnnotation<'a> { + LineAnnotation { + start: Loc { + line: self.end.line, + char: self.end.char.saturating_sub(1), + display: self.end.display.saturating_sub(1), + byte: self.end.byte.saturating_sub(1), + }, + end: self.end, + kind: self.kind, + label: self.label.clone(), + annotation_type: LineAnnotationType::MultilineEnd(self.depth), + highlight_source: self.highlight_source, + } + } + + pub(crate) fn as_line(&self) -> LineAnnotation<'a> { + LineAnnotation { + start: Loc::default(), + end: Loc::default(), + kind: self.kind, + label: None, + annotation_type: LineAnnotationType::MultilineLine(self.depth), + highlight_source: self.highlight_source, + } + } +} + +#[derive(Debug)] +pub(crate) struct LineInfo<'a> { + pub(crate) line: &'a str, + pub(crate) line_index: usize, + pub(crate) start_byte: usize, + pub(crate) end_byte: usize, + end_line_size: usize, +} + +#[derive(Debug)] +pub(crate) struct AnnotatedLineInfo<'a> { + pub(crate) line: &'a str, + pub(crate) line_index: usize, + pub(crate) annotations: Vec>, + pub(crate) keep: bool, +} + +/// A source code location used for error reporting. +#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct Loc { + /// The (1-based) line number. + pub(crate) line: usize, + /// The (0-based) column offset. + pub(crate) char: usize, + /// The (0-based) column offset when displayed. + pub(crate) display: usize, + /// The (0-based) byte offset. + pub(crate) byte: usize, +} + +struct CursorLines<'a>(&'a str); + +impl CursorLines<'_> { + fn new(src: &str) -> CursorLines<'_> { + CursorLines(src) + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +enum EndLine { + Eof, + Lf, + Crlf, +} + +impl EndLine { + /// The number of characters this line ending occupies in bytes. + pub(crate) fn len(self) -> usize { + match self { + EndLine::Eof => 0, + EndLine::Lf => 1, + EndLine::Crlf => 2, + } + } +} + +impl<'a> Iterator for CursorLines<'a> { + type Item = (&'a str, EndLine); + + fn next(&mut self) -> Option { + if self.0.is_empty() { + None + } else { + self.0 + .find('\n') + .map(|x| { + let ret = if 0 < x { + if self.0.as_bytes()[x - 1] == b'\r' { + (&self.0[..x - 1], EndLine::Crlf) + } else { + (&self.0[..x], EndLine::Lf) + } + } else { + ("", EndLine::Lf) + }; + self.0 = &self.0[x + 1..]; + ret + }) + .or_else(|| { + let ret = Some((self.0, EndLine::Eof)); + self.0 = ""; + ret + }) + } + } +} + +pub(crate) type SplicedLines<'a> = ( + String, + Vec>, + // Char spans to highlight per line of the post-substitution output. + Vec>, + // Char spans of the replaced (original) code, per original line in the + // bounding range covered by the splice. + Vec>, +); + +/// Used to translate between `Span`s and byte positions within a single output line in highlighted +/// code of structured suggestions. +#[derive(Debug, Clone, Copy)] +pub(crate) struct SubstitutionHighlight { + pub(crate) start: usize, + pub(crate) end: usize, +} + +#[derive(Clone, Debug)] +pub(crate) struct TrimmedPatch<'a> { + pub(crate) original_span: Range, + pub(crate) span: Range, + pub(crate) replacement: Cow<'a, str>, +} + +impl<'a> TrimmedPatch<'a> { + pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool { + !self.replacement.is_empty() && !self.replaces_meaningful_content(sm) + } + + pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool { + self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm) + } + + pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool { + !self.replacement.is_empty() && self.replaces_meaningful_content(sm) + } + + /// Whether this is a replacement that overwrites source with a snippet + /// in a way that isn't a superset of the original string. For example, + /// replacing "abc" with "abcde" is not destructive, but replacing it + /// it with "abx" is, since the "c" character is lost. + pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool { + self.is_replacement(sm) + && sm + .span_to_snippet(self.span.clone()) + .is_none_or(|s| as_substr(s.trim(), self.replacement.trim()).is_none()) + } + + fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool { + sm.span_to_snippet(self.span.clone()) + .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty()) + } +} + +/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect +/// the case where a substring of the suggestion is "sandwiched" in the original, like +/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length +/// of the suffix. +pub(crate) fn as_substr<'a>( + original: &'a str, + suggestion: &'a str, +) -> Option<(usize, &'a str, usize)> { + if let Some(stripped) = suggestion.strip_prefix(original) { + Some((original.len(), stripped, 0)) + } else if let Some(stripped) = suggestion.strip_suffix(original) { + Some((0, stripped, original.len())) + } else { + let common_prefix = original + .chars() + .zip(suggestion.chars()) + .take_while(|(c1, c2)| c1 == c2) + .map(|(c, _)| c.len_utf8()) + .sum(); + let original = &original[common_prefix..]; + let suggestion = &suggestion[common_prefix..]; + if let Some(stripped) = suggestion.strip_suffix(original) { + let common_suffix = original.len(); + Some((common_prefix, stripped, common_suffix)) + } else { + None + } + } +} diff --git a/crates/ruff_annotate_snippets/src/renderer/styled_buffer.rs b/crates/ruff_annotate_snippets/src/renderer/styled_buffer.rs index 73b30cefca..ff51aa41ff 100644 --- a/crates/ruff_annotate_snippets/src/renderer/styled_buffer.rs +++ b/crates/ruff_annotate_snippets/src/renderer/styled_buffer.rs @@ -2,10 +2,13 @@ //! //! [styled_buffer]: https://github.com/rust-lang/rust/blob/894f7a4ba6554d3797404bbf550d9919df060b97/compiler/rustc_errors/src/styled_buffer.rs +use alloc::string::String; +use alloc::{vec, vec::Vec}; +use core::fmt::{self, Write}; + +use crate::Level; +use crate::renderer::ElementStyle; use crate::renderer::stylesheet::Stylesheet; -use anstyle::Style; -use std::fmt; -use std::fmt::Write; #[derive(Debug)] pub(crate) struct StyledBuffer { @@ -15,13 +18,13 @@ pub(crate) struct StyledBuffer { #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct StyledChar { ch: char, - style: Style, + style: ElementStyle, } impl StyledChar { - pub(crate) const SPACE: Self = StyledChar::new(' ', Style::new()); + pub(crate) const SPACE: Self = StyledChar::new(' ', ElementStyle::NoStyle); - pub(crate) const fn new(ch: char, style: Style) -> StyledChar { + pub(crate) const fn new(ch: char, style: ElementStyle) -> StyledChar { StyledChar { ch, style } } } @@ -37,33 +40,40 @@ impl StyledBuffer { } } - pub(crate) fn render(&self, stylesheet: &Stylesheet) -> Result { + pub(crate) fn render( + &self, + level: &Level<'_>, + stylesheet: &Stylesheet, + str: &mut String, + ) -> Result<(), fmt::Error> { let capacity = self.lines.iter().map(|line| line.len()).sum(); - let mut str = String::with_capacity(capacity); + str.reserve(capacity); + for (i, line) in self.lines.iter().enumerate() { let mut current_style = stylesheet.none; - for ch in line { - if ch.style != current_style { + for StyledChar { ch, style } in line { + let ch_style = style.color_spec(level, stylesheet); + if ch_style != current_style { if !line.is_empty() { - write!(str, "{}", current_style.render_reset())?; + write!(str, "{current_style:#}")?; } - current_style = ch.style; - write!(str, "{}", current_style.render())?; + current_style = ch_style; + write!(str, "{current_style}")?; } - str.push(ch.ch); + str.push(*ch); } - write!(str, "{}", current_style.render_reset())?; + write!(str, "{current_style:#}")?; if i != self.lines.len() - 1 { str.push('\n'); } } - Ok(str) + Ok(()) } /// Sets `chr` with `style` for given `line`, `col`. /// If `line` does not exist in our buffer, adds empty lines up to the given /// and fills the last line with unstyled whitespace. - pub(crate) fn putc(&mut self, line: usize, col: usize, chr: char, style: Style) { + pub(crate) fn putc(&mut self, line: usize, col: usize, chr: char, style: ElementStyle) { self.ensure_lines(line); if col >= self.lines[line].len() { self.lines[line].resize(col + 1, StyledChar::SPACE); @@ -74,24 +84,29 @@ impl StyledBuffer { /// Sets `string` with `style` for given `line`, starting from `col`. /// If `line` does not exist in our buffer, adds empty lines up to the given /// and fills the last line with unstyled whitespace. - pub(crate) fn puts(&mut self, line: usize, col: usize, string: &str, style: Style) { + pub(crate) fn puts(&mut self, line: usize, col: usize, string: &str, style: ElementStyle) { if string.is_empty() { + // don't add trailing whitespace (from column offset) for blank strings return; } + self.ensure_lines(line); - let char_count = string.chars().count(); - let needed = col + char_count; - if needed > self.lines[line].len() { - self.lines[line].resize(needed, StyledChar::SPACE); - } let line = &mut self.lines[line]; - for (i, c) in string.chars().enumerate() { - line[col + i] = StyledChar::new(c, style); + + let new_len = col + string.chars().count(); + if new_len > line.len() { + line.resize(new_len, StyledChar::SPACE); + } + + for (offset, chr) in string.chars().enumerate() { + let col = col + offset; + line[col] = StyledChar::new(chr, style); } } + /// For given `line` inserts `string` with `style` after old content of that line, /// adding lines if needed - pub(crate) fn append(&mut self, line: usize, string: &str, style: Style) { + pub(crate) fn append(&mut self, line: usize, string: &str, style: ElementStyle) { if line >= self.lines.len() { self.puts(line, 0, string, style); } else { @@ -100,7 +115,58 @@ impl StyledBuffer { } } + pub(crate) fn replace(&mut self, line: usize, start: usize, end: usize, string: &str) { + if start == end { + return; + } + // If the replacement range would be out of bounds, do nothing, as we + // can't replace things that don't exist. + if start > self.lines[line].len() || end > self.lines[line].len() { + return; + }; + self.lines[line].splice( + start..end, + string + .chars() + .map(|c| StyledChar::new(c, ElementStyle::LineNumber)), + ); + } + pub(crate) fn num_lines(&self) -> usize { self.lines.len() } + + /// Set `style` for `line`, `col_start..col_end` range if: + /// 1. That line and column range exist in `StyledBuffer` + /// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation` + pub(crate) fn set_style_range( + &mut self, + line: usize, + col_start: usize, + col_end: usize, + style: ElementStyle, + overwrite: bool, + ) { + for col in col_start..col_end { + self.set_style(line, col, style, overwrite); + } + } + + /// Set `style` for `line`, `col` if: + /// 1. That line and column exist in `StyledBuffer` + /// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation` + pub(crate) fn set_style( + &mut self, + line: usize, + col: usize, + style: ElementStyle, + overwrite: bool, + ) { + if let Some(ref mut line) = self.lines.get_mut(line) + && let Some(StyledChar { style: s, .. }) = line.get_mut(col) + && (overwrite || matches!(s, ElementStyle::NoStyle | ElementStyle::Quotation)) + { + *s = style; + } + } } diff --git a/crates/ruff_annotate_snippets/src/renderer/stylesheet.rs b/crates/ruff_annotate_snippets/src/renderer/stylesheet.rs index d9ec70d6d0..075cad42a9 100644 --- a/crates/ruff_annotate_snippets/src/renderer/stylesheet.rs +++ b/crates/ruff_annotate_snippets/src/renderer/stylesheet.rs @@ -7,10 +7,12 @@ pub(crate) struct Stylesheet { pub(crate) info: Style, pub(crate) note: Style, pub(crate) help: Style, - pub(crate) line_no: Style, + pub(crate) line_num: Style, pub(crate) emphasis: Style, pub(crate) none: Style, - pub(crate) hyperlink: bool, + pub(crate) context: Style, + pub(crate) addition: Style, + pub(crate) removal: Style, } impl Default for Stylesheet { @@ -27,44 +29,12 @@ impl Stylesheet { info: Style::new(), note: Style::new(), help: Style::new(), - line_no: Style::new(), + line_num: Style::new(), emphasis: Style::new(), none: Style::new(), - hyperlink: false, + context: Style::new(), + addition: Style::new(), + removal: Style::new(), } } } - -impl Stylesheet { - pub(crate) fn error(&self) -> &Style { - &self.error - } - - pub(crate) fn warning(&self) -> &Style { - &self.warning - } - - pub(crate) fn info(&self) -> &Style { - &self.info - } - - pub(crate) fn note(&self) -> &Style { - &self.note - } - - pub(crate) fn help(&self) -> &Style { - &self.help - } - - pub(crate) fn line_no(&self) -> &Style { - &self.line_no - } - - pub(crate) fn emphasis(&self) -> &Style { - &self.emphasis - } - - pub(crate) fn none(&self) -> &Style { - &self.none - } -} diff --git a/crates/ruff_annotate_snippets/src/snippet.rs b/crates/ruff_annotate_snippets/src/snippet.rs index 363f8b2558..b70a9364f4 100644 --- a/crates/ruff_annotate_snippets/src/snippet.rs +++ b/crates/ruff_annotate_snippets/src/snippet.rs @@ -1,68 +1,222 @@ //! Structures used as an input for the library. -//! -//! Example: -//! -//! ``` -//! use ruff_annotate_snippets::*; -//! -//! Level::Error.title("mismatched types") -//! .snippet(Snippet::source("Foo").line_start(51).origin("src/format.rs")) -//! .snippet(Snippet::source("Faa").line_start(129).origin("src/display.rs")); -//! ``` - -use std::ops::Range; - -#[derive(Copy, Clone, Debug, Default, PartialEq)] + +use alloc::borrow::{Cow, ToOwned}; +use alloc::string::String; +use alloc::{vec, vec::Vec}; +use core::ops::Range; + +use crate::Level; +use crate::renderer::source_map::{TrimmedPatch, as_substr}; + +pub(crate) const ERROR_TXT: &str = "error"; +pub(crate) const HELP_TXT: &str = "help"; +pub(crate) const INFO_TXT: &str = "info"; +pub(crate) const NOTE_TXT: &str = "note"; +pub(crate) const WARNING_TXT: &str = "warning"; + +/// A [diagnostic message][Title] and any associated [context][Element] to help users +/// understand it +/// +/// The first [`Group`] is the ["primary" group][Level::primary_title], ie it contains the diagnostic +/// message. +/// +/// All subsequent [`Group`]s are for distinct pieces of [context][Level::secondary_title]. +/// The primary group will be visually distinguished to help tell them apart. +pub type Report<'a> = &'a [Group<'a>]; + +#[derive(Clone, Debug, Default)] pub(crate) struct Id<'a> { - pub(crate) id: &'a str, - pub(crate) url: Option<&'a str>, + pub(crate) id: Option>, + pub(crate) url: Option>, } -/// Primary structure provided for formatting +/// A [`Title`] with supporting [context][Element] within a [`Report`] /// -/// See [`Level::title`] to create a [`Message`] -#[derive(Debug)] -pub struct Message<'a> { - pub(crate) level: Level, - pub(crate) id: Option>, - pub(crate) title: &'a str, - pub(crate) snippets: Vec>, - pub(crate) footer: Vec>, - pub(crate) is_fixable: bool, +/// [Decor][crate::renderer::DecorStyle] is used to visually connect [`Element`]s of a `Group`. +/// +/// Generally, you will create separate group's for: +/// - New [`Snippet`]s, especially if they need their own [`AnnotationKind::Primary`] +/// - Each logically distinct set of [suggestions][Patch`] +/// +/// # Example +/// +/// ```rust +/// # #[allow(clippy::needless_doctest_main)] +#[doc = include_str!("../examples/highlight_message.rs")] +/// ``` +#[doc = include_str!("../examples/highlight_message.svg")] +#[derive(Clone, Debug)] +pub struct Group<'a> { + pub(crate) primary_level: Level<'a>, + pub(crate) title: Option>, + pub(crate) elements: Vec>, pub(crate) lineno_offset: usize, } -impl<'a> Message<'a> { - pub fn id(mut self, id: &'a str) -> Self { - self.id = Some(Id { id, url: None }); - self +impl<'a> Group<'a> { + /// Create group with a [`Title`], deriving [`AnnotationKind::Primary`] from its [`Level`] + pub fn with_title(title: Title<'a>) -> Self { + let level = title.level.clone(); + let mut x = Self::with_level(level); + x.title = Some(title); + x } - pub fn id_with_url(mut self, id: &'a str, url: Option<&'a str>) -> Self { - self.id = Some(Id { id, url }); + /// Create a title-less group with a primary [`Level`] for [`AnnotationKind::Primary`] + /// + /// # Example + /// + /// ```rust + /// # #[allow(clippy::needless_doctest_main)] + #[doc = include_str!("../examples/elide_header.rs")] + /// ``` + #[doc = include_str!("../examples/elide_header.svg")] + pub fn with_level(level: Level<'a>) -> Self { + Self { + primary_level: level, + title: None, + elements: vec![], + lineno_offset: 0, + } + } + + /// Append an [`Element`] that adds context to the [`Title`] + pub fn element(mut self, section: impl Into>) -> Self { + self.elements.push(section.into()); self } - pub fn snippet(mut self, slice: Snippet<'a>) -> Self { - self.snippets.push(slice); + /// Append [`Element`]s that adds context to the [`Title`] + pub fn elements(mut self, sections: impl IntoIterator>>) -> Self { + self.elements.extend(sections.into_iter().map(Into::into)); self } - pub fn snippets(mut self, slice: impl IntoIterator>) -> Self { - self.snippets.extend(slice); + pub fn is_empty(&self) -> bool { + self.elements.is_empty() && self.title.is_none() + } + + /// Add an offset used for aligning the header sigil (`-->`) with the line number separators. + /// + /// For normal diagnostics this is computed automatically based on the lines to be rendered. + /// This is intended only for use in the formatter, where we don't render a snippet directly but + /// still want the header to align with the diff. + pub fn lineno_offset(mut self, offset: usize) -> Self { + self.lineno_offset = offset; self } +} + +/// A section of content within a [`Group`] +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Element<'a> { + Message(Message<'a>), + Cause(Snippet<'a, Annotation<'a>>), + Suggestion(Snippet<'a, Patch<'a>>), + Origin(Origin<'a>), + Padding(Padding), +} + +impl<'a> From> for Element<'a> { + fn from(value: Message<'a>) -> Self { + Element::Message(value) + } +} - pub fn footer(mut self, footer: Message<'a>) -> Self { - self.footer.push(footer); +impl<'a> From>> for Element<'a> { + fn from(value: Snippet<'a, Annotation<'a>>) -> Self { + Element::Cause(value) + } +} + +impl<'a> From>> for Element<'a> { + fn from(value: Snippet<'a, Patch<'a>>) -> Self { + Element::Suggestion(value) + } +} + +impl<'a> From> for Element<'a> { + fn from(value: Origin<'a>) -> Self { + Element::Origin(value) + } +} + +impl From for Element<'_> { + fn from(value: Padding) -> Self { + Self::Padding(value) + } +} + +/// A whitespace [`Element`] in a [`Group`] +#[derive(Clone, Debug)] +pub struct Padding; + +/// A title that introduces a [`Group`], describing the main point +/// +/// To create a `Title`, see [`Level::primary_title`] or [`Level::secondary_title`]. +/// +/// # Example +/// +/// ```rust +/// # use annotate_snippets::*; +/// let report = &[ +/// Group::with_title( +/// Level::ERROR.primary_title("mismatched types").id("E0308") +/// ), +/// Group::with_title( +/// Level::HELP.secondary_title("function defined here") +/// ), +/// ]; +/// ``` +#[derive(Clone, Debug)] +pub struct Title<'a> { + pub(crate) level: Level<'a>, + pub(crate) id: Option>, + pub(crate) text: Cow<'a, str>, + pub(crate) allows_styling: bool, + pub(crate) is_fixable: bool, +} + +impl<'a> Title<'a> { + /// The category for this [`Report`] + /// + /// Useful for looking searching for more information to resolve the diagnostic. + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn id(mut self, id: impl Into>) -> Self { + self.id.get_or_insert(Id::default()).id = Some(id.into()); self } - pub fn footers(mut self, footer: impl IntoIterator>) -> Self { - self.footer.extend(footer); + /// Provide a URL for [`Title::id`] for more information on this diagnostic + /// + ///
+ /// + /// This is only relevant if `id` is present + /// + ///
+ pub fn id_url(mut self, url: impl Into>) -> Self { + self.id.get_or_insert(Id::default()).url = Some(url.into()); self } + /// Append an [`Element`] that adds context to the [`Title`] + pub fn element(self, section: impl Into>) -> Group<'a> { + Group::with_title(self).element(section) + } + + /// Append [`Element`]s that adds context to the [`Title`] + pub fn elements(self, sections: impl IntoIterator>>) -> Group<'a> { + Group::with_title(self).elements(sections) + } + /// Whether or not the diagnostic for this message is fixable. /// /// This is rendered as a `[*]` indicator after the `id` in an annotation header, if the @@ -71,98 +225,166 @@ impl<'a> Message<'a> { self.is_fixable = yes; self } - - /// Add an offset used for aligning the header sigil (`-->`) with the line number separators. - /// - /// For normal diagnostics this is computed automatically based on the lines to be rendered. - /// This is intended only for use in the formatter, where we don't render a snippet directly but - /// still want the header to align with the diff. - pub fn lineno_offset(mut self, offset: usize) -> Self { - self.lineno_offset = offset; - self - } } -/// Structure containing the slice of text to be annotated and -/// basic information about the location of the slice. +/// A text [`Element`] in a [`Group`] /// -/// One `Snippet` is meant to represent a single, continuous, -/// slice of source code that you want to annotate. -#[derive(Debug)] -pub struct Snippet<'a> { - pub(crate) origin: Option<&'a str>, - pub(crate) line_start: usize, - - pub(crate) source: &'a str, - pub(crate) annotations: Vec>, - - pub(crate) fold: bool, +/// See [`Level::message`] to create this. +#[derive(Clone, Debug)] +pub struct Message<'a> { + pub(crate) level: Level<'a>, + pub(crate) text: Cow<'a, str>, +} +/// A source view [`Element`] in a [`Group`] +/// +/// If you do not have [source][Snippet::source] available, see instead [`Origin`] +/// +/// `Snippet`s come in the following styles (`T`): +/// - With [`Annotation`]s, see [`Snippet::annotation`] +/// - With [`Patch`]s, see [`Snippet::patch`] +#[derive(Clone, Debug)] +pub struct Snippet<'a, T> { + pub(crate) path: Option>, /// The optional cell index in a Jupyter notebook, used for reporting source locations along /// with the ranges on `annotations`. pub(crate) cell_index: Option, + pub(crate) line_start: usize, + pub(crate) source: Cow<'a, str>, + pub(crate) markers: Vec, + pub(crate) fold: bool, } -impl<'a> Snippet<'a> { - pub fn source(source: &'a str) -> Self { +impl<'a, T: Clone> Snippet<'a, T> { + /// The source code to be rendered + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn source(source: impl Into>) -> Self { Self { - origin: None, + path: None, line_start: 1, - source, - annotations: vec![], - fold: false, cell_index: None, + source: source.into(), + markers: vec![], + fold: true, } } + /// When manually [`fold`][Self::fold]ing, + /// the [`source`][Self::source]s line offset from the original start pub fn line_start(mut self, line_start: usize) -> Self { self.line_start = line_start; self } - pub fn origin(mut self, origin: &'a str) -> Self { - self.origin = Some(origin); + /// The location of the [`source`][Self::source] (e.g. a path) + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn path(mut self, path: impl Into>) -> Self { + self.path = path.into().0; + self + } + + /// Attach a Jupyter notebook cell index. + pub fn cell_index(mut self, index: Option) -> Self { + self.cell_index = index; + self + } + + /// Control whether lines without [`Annotation`]s are shown + /// + /// The default is `fold(true)`, collapsing uninteresting lines. + /// + /// See [`AnnotationKind::Visible`] to force specific spans to be shown. + pub fn fold(mut self, fold: bool) -> Self { + self.fold = fold; self } +} - pub fn annotation(mut self, annotation: Annotation<'a>) -> Self { - self.annotations.push(annotation); +impl<'a> Snippet<'a, Annotation<'a>> { + /// Highlight and describe a span of text within the [`source`][Self::source] + pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> { + self.markers.push(annotation); self } + /// Highlight and describe spans of text within the [`source`][Self::source] pub fn annotations(mut self, annotation: impl IntoIterator>) -> Self { - self.annotations.extend(annotation); + self.markers.extend(annotation); self } +} - /// Hide lines without [`Annotation`]s - pub fn fold(mut self, fold: bool) -> Self { - self.fold = fold; +impl<'a> Snippet<'a, Patch<'a>> { + /// Suggest to the user an edit to the [`source`][Self::source] + pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> { + self.markers.push(patch); self } - /// Attach a Jupyter notebook cell index. - pub fn cell_index(mut self, index: Option) -> Self { - self.cell_index = index; + /// Suggest to the user edits to the [`source`][Self::source] + pub fn patches(mut self, patches: impl IntoIterator>) -> Self { + self.markers.extend(patches); self } } -/// An annotation for a [`Snippet`]. +/// Highlight and describe a span of text within a [`Snippet`] /// -/// See [`Level::span`] to create a [`Annotation`] -#[derive(Debug)] +/// See [`AnnotationKind`] to create an annotation. +/// +/// # Example +/// +/// ```rust +/// # #[allow(clippy::needless_doctest_main)] +#[doc = include_str!("../examples/expected_type.rs")] +/// ``` +/// +#[doc = include_str!("../examples/expected_type.svg")] +#[derive(Clone, Debug)] pub struct Annotation<'a> { - /// The byte range of the annotation in the `source` string - pub(crate) range: Range, - pub(crate) label: Option<&'a str>, - pub(crate) level: Level, + pub(crate) span: Range, + pub(crate) label: Option>, + pub(crate) kind: AnnotationKind, + pub(crate) highlight_source: bool, pub(crate) is_file_level: bool, } impl<'a> Annotation<'a> { - pub fn label(mut self, label: &'a str) -> Self { - self.label = Some(label); + /// Describe the reason the span is highlighted + /// + /// This will be styled according to the [`AnnotationKind`] + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn label(mut self, label: impl Into>) -> Self { + self.label = label.into().0; + self + } + + /// Style the source according to the [`AnnotationKind`] + /// + /// This gives extra emphasis to this annotation + pub fn highlight_source(mut self, highlight_source: bool) -> Self { + self.highlight_source = highlight_source; self } @@ -172,40 +394,225 @@ impl<'a> Annotation<'a> { } } -/// Types of annotations. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Level { - /// Do not attach any annotation. - None, - /// Error annotations are displayed using red color and "^" character. - Error, - /// Warning annotations are displayed using blue color and "-" character. - Warning, - Info, - Note, - Help, +/// The type of [`Annotation`] being applied to a [`Snippet`] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum AnnotationKind { + /// For showing the source that the [Group's Title][Group::with_title] references + /// + /// For [`Title`]-less groups, see [`Group::with_level`] + Primary, + /// Additional context to better understand the [`Primary`][Self::Primary] + /// [`Annotation`] + /// + /// See also [`Renderer::context`]. + /// + /// [`Renderer::context`]: crate::renderer::Renderer + Context, + /// Prevents the annotated text from getting [folded][Snippet::fold] + /// + /// By default, [`Snippet`]s will [fold][`Snippet::fold`] (remove) lines + /// that do not contain any annotations. [`Visible`][Self::Visible] makes + /// it possible to selectively prevent this behavior for specific text, + /// allowing context to be preserved without adding any annotation + /// characters. + /// + /// # Example + /// + /// ```rust + /// # #[allow(clippy::needless_doctest_main)] + #[doc = include_str!("../examples/struct_name_as_context.rs")] + /// ``` + /// + #[doc = include_str!("../examples/struct_name_as_context.svg")] + /// + Visible, } -impl Level { - pub fn title(self, title: &str) -> Message<'_> { - Message { - level: self, - id: None, - title, - snippets: vec![], - footer: vec![], - is_fixable: false, - lineno_offset: 0, - } - } - - /// Create a [`Annotation`] with the given span for a [`Snippet`] +impl AnnotationKind { + /// Annotate a byte span within [`Snippet`] pub fn span<'a>(self, span: Range) -> Annotation<'a> { Annotation { - range: span, + span, label: None, - level: self, + kind: self, + highlight_source: false, is_file_level: false, } } + + pub(crate) fn is_primary(&self) -> bool { + matches!(self, AnnotationKind::Primary) + } +} + +/// Suggested edit to the [`Snippet`] +/// +/// See [`Snippet::patch`] +/// +/// # Example +/// +/// ```rust +/// # #[allow(clippy::needless_doctest_main)] +#[doc = include_str!("../examples/multi_suggestion.rs")] +/// ``` +/// +#[doc = include_str!("../examples/multi_suggestion.svg")] +#[derive(Clone, Debug)] +pub struct Patch<'a> { + pub(crate) span: Range, + pub(crate) replacement: Cow<'a, str>, +} + +impl<'a> Patch<'a> { + /// Splice `replacement` into the [`Snippet`] at the specified byte span + /// + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn new(span: Range, replacement: impl Into>) -> Self { + Self { + span, + replacement: replacement.into(), + } + } + + /// Try to turn a replacement into an addition when the span that is being + /// overwritten matches either the prefix or suffix of the replacement. + pub(crate) fn trim_trivial_replacements(self, source: &str) -> TrimmedPatch<'a> { + let mut trimmed = TrimmedPatch { + original_span: self.span.clone(), + span: self.span, + replacement: self.replacement, + }; + + if trimmed.replacement.is_empty() { + return trimmed; + } + let Some(snippet) = source.get(trimmed.original_span.clone()) else { + return trimmed; + }; + + if let Some((prefix, substr, suffix)) = as_substr(snippet, &trimmed.replacement) { + trimmed.span = trimmed.original_span.start + prefix + ..trimmed.original_span.end.saturating_sub(suffix); + trimmed.replacement = Cow::Owned(substr.to_owned()); + } + trimmed + } +} + +/// A source location [`Element`] in a [`Group`] +/// +/// If you have source available, see instead [`Snippet`] +/// +/// # Example +/// +/// ```rust +/// # use annotate_snippets::{Group, Snippet, AnnotationKind, Level, Origin}; +/// let report = &[ +/// Level::ERROR.primary_title("mismatched types").id("E0308") +/// .element( +/// Origin::path("$DIR/mismatched-types.rs") +/// ) +/// ]; +/// ``` +#[derive(Clone, Debug)] +pub struct Origin<'a> { + pub(crate) path: Cow<'a, str>, + /// The optional cell index in a Jupyter notebook, used for reporting source locations along + /// with the ranges on `annotations`. + pub(crate) cell_index: Option, + pub(crate) line: Option, + pub(crate) char_column: Option, +} + +impl<'a> Origin<'a> { + ///
+ /// + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + /// + ///
+ pub fn path(path: impl Into>) -> Self { + Self { + path: path.into(), + cell_index: None, + line: None, + char_column: None, + } + } + + /// Attach a Jupyter notebook cell index. + pub fn cell_index(mut self, index: Option) -> Self { + self.cell_index = index; + self + } + + /// Set the default line number to display + pub fn line(mut self, line: usize) -> Self { + self.line = Some(line); + self + } + + /// Set the default column to display + /// + ///
+ /// + /// `char_column` is only be respected if [`Origin::line`] is also set. + /// + ///
+ pub fn char_column(mut self, char_column: usize) -> Self { + self.char_column = Some(char_column); + self + } +} + +impl<'a> From> for Origin<'a> { + fn from(origin: Cow<'a, str>) -> Self { + Self::path(origin) + } +} + +#[derive(Debug)] +pub struct OptionCow<'a>(pub(crate) Option>); + +impl<'a, T: Into>> From> for OptionCow<'a> { + fn from(value: Option) -> Self { + Self(value.map(Into::into)) + } +} + +impl<'a> From<&'a Cow<'a, str>> for OptionCow<'a> { + fn from(value: &'a Cow<'a, str>) -> Self { + Self(Some(Cow::Borrowed(value))) + } +} + +impl<'a> From> for OptionCow<'a> { + fn from(value: Cow<'a, str>) -> Self { + Self(Some(value)) + } +} + +impl<'a> From<&'a str> for OptionCow<'a> { + fn from(value: &'a str) -> Self { + Self(Some(Cow::Borrowed(value))) + } +} +impl<'a> From for OptionCow<'a> { + fn from(value: String) -> Self { + Self(Some(Cow::Owned(value))) + } +} + +impl<'a> From<&'a String> for OptionCow<'a> { + fn from(value: &'a String) -> Self { + Self(Some(Cow::Borrowed(value.as_str()))) + } } diff --git a/crates/ruff_annotate_snippets/tests/fixtures/color/ann_removed_nl.svg b/crates/ruff_annotate_snippets/tests/color/ann_eof.ascii.term.svg similarity index 75% rename from crates/ruff_annotate_snippets/tests/fixtures/color/ann_removed_nl.svg rename to crates/ruff_annotate_snippets/tests/color/ann_eof.ascii.term.svg index 045b0ef413..dfc0c58739 100644 --- a/crates/ruff_annotate_snippets/tests/fixtures/color/ann_removed_nl.svg +++ b/crates/ruff_annotate_snippets/tests/color/ann_eof.ascii.term.svg @@ -1,4 +1,4 @@ - + invalid_characters_syntax_error.py:13:15 @@ -129,5 +126,4 @@ PLE2510 Invalid unescaped character backspace, use "\b" instead 12 | # Implicitly concatenated 13 | b = '␈' f'␈' '␈ | ^ - | help: Replace with escape sequence diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap index 377551595030bca2a6cd350803df9c88bc66ca09..a6244be8a55ab9a23b97fa78b81e6153c05aa001 100644 GIT binary patch delta 239 zcmeB?-6=caH1negvnHNVWdTy^lkc$VO;%*w4-%XFkWm;+Ph=L}%)wO02;yvRXTAwx zB(f?qaRNmiO_=>?Lc`=vc4H*oLH0#FaJfef-IM3B%OOekahM?S4zjmR_T`jC63ORO zMB?>vNK9VK`5bKhR4#t7^^+IyYEC}KtppbQ$lb`u2ehubL>=fF^;#gDe4bko?Dz$| OI+Gu91@cQ*kzDJ3fUL&z~vt`bWiqUmlHx3;8IXfs8N{g#-W3vt8MaM4p}4- zc}_)a+I%?=f*r}v<;((ffYRhTZq3PkoSc)jc|s=N;>rd~x^e4(=*`9435;NY3w-*M ggSZvIg37%05J9M_$rt!^nH1tC*K?>$R_C7v0Qm)TyZ`_I delta 213 zcmaDUzgK?43FgUjS$HRN@+xfp!JNp*C^b2TwPx}}4vo#fSuGiPr5;U~&G4w9n@d4K zp=PohyEZFWWO6S@+awMdC`Xk;5z6xC5S^ULArDuy7|OEbL{?GBxdmvC0hcoqgVN*) z97dC0v4l+i%auLZoQHSvT|R}$(tJXb6L>W@U*T3`1PTgH7T}keti`JYlGvQUn+Fnv SsRT*NGR6VT(VQI2KMeq_Uqf5~ diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2514_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2514_invalid_characters.py.snap index d2e39983d962c07d501724d769b27832bd908919..880cbc8ef3bf745ce9113ca960af360c72e5020e 100644 GIT binary patch delta 186 zcmcb>^OI-7cIHPD8Yb@008grs^778^Ad8IiDwhC!rz9p0ol;8Y-(Ug(% z(S%u#Cd>wMC(E%&PBvneK@y2zX_eP zprBAQS)W;l1117iKY_V~6)Z5Bmqigxu>*@Py5fk*zgT3U8WdR3H3YL>1zN7n<_7?r CK{LYu diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap index 8f5b94d9459dcb618d60eef3e37c796753ca0aa6..6d3205f333b41af2062c3233eed1463277217f7a 100644 GIT binary patch delta 164 zcmexiw8(hFHUZ{G6J~9GB^=DSnM>$CBb2Ai#0eCBG-39m2@R9&B*Z4Gi76n7B#KX+ z{6I_wNrX{c5s3%VVT~e^D87jYZsMbc?#XuIa%dWYB({NUpUf%g!UD8bX>z=TCIBr^ BO=JK7 delta 137 zcmZ2v{KII&HUUPd&F_SR88`C?-Dd=|m6>>@9!;3d@Tj4iOF==QX0nQy4l7t>a;^B( s$?wEupgdu5MKqP};<|8^o1iQU33OFC5{DS2CL2n+Ffk}iZV=Z502#q9&;S4c diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1708_stop_iteration_return.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1708_stop_iteration_return.py.snap index a6775f05d7..89d9672981 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1708_stop_iteration_return.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1708_stop_iteration_return.py.snap @@ -8,7 +8,6 @@ PLR1708 Explicit `raise StopIteration` in generator 37 | yield 2 38 | raise StopIteration # Should trigger | ^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead PLR1708 Explicit `raise StopIteration` in generator @@ -18,7 +17,6 @@ PLR1708 Explicit `raise StopIteration` in generator 43 | yield 2 44 | raise StopIteration("finished") # Should trigger | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead PLR1708 Explicit `raise StopIteration` in generator @@ -28,7 +26,6 @@ PLR1708 Explicit `raise StopIteration` in generator 49 | yield 2 50 | raise StopIteration(1 + 2) # Should trigger | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead PLR1708 Explicit `raise StopIteration` in generator @@ -38,7 +35,6 @@ PLR1708 Explicit `raise StopIteration` in generator 55 | yield 2 56 | raise StopIteration("async") # Should trigger | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead PLR1708 Explicit `raise StopIteration` in generator @@ -95,7 +91,6 @@ PLR1708 Explicit `raise StopIteration` in generator 97 | yield 1 98 | raise StopIteration # Should trigger (no arguments) | ^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead PLR1708 Explicit `raise StopIteration` in generator @@ -105,7 +100,6 @@ PLR1708 Explicit `raise StopIteration` in generator 104 | if i == 3: 105 | raise StopIteration("loop") # Should trigger | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead PLR1708 Explicit `raise StopIteration` in generator @@ -126,5 +120,4 @@ PLR1708 Explicit `raise StopIteration` in generator 153 | def foo(): 154 | raise StopIteration((yield 1)) # Should trigger | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `return` instead diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1711_useless_return.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1711_useless_return.py.snap index 8f605bc56a..bb66211d8f 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1711_useless_return.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1711_useless_return.py.snap @@ -8,7 +8,6 @@ PLR1711 [*] Useless `return` statement at end of function 5 | print(sys.version) 6 | return None # [useless-return] | ^^^^^^^^^^^ - | help: Remove useless `return` statement | 5 | print(sys.version) @@ -23,7 +22,6 @@ PLR1711 [*] Useless `return` statement at end of function 10 | print(sys.version) 11 | return None # [useless-return] | ^^^^^^^^^^^ - | help: Remove useless `return` statement | 10 | print(sys.version) @@ -38,7 +36,6 @@ PLR1711 [*] Useless `return` statement at end of function 15 | print(sys.version) 16 | return None # [useless-return] | ^^^^^^^^^^^ - | help: Remove useless `return` statement | 15 | print(sys.version) @@ -53,7 +50,6 @@ PLR1711 [*] Useless `return` statement at end of function 21 | print(sys.version) 22 | return None # [useless-return] | ^^^^^^^^^^^ - | help: Remove useless `return` statement | 21 | print(sys.version) @@ -68,7 +64,6 @@ PLR1711 [*] Useless `return` statement at end of function 49 | print(sys.version) 50 | return None # [useless-return] | ^^^^^^^^^^^ - | help: Remove useless `return` statement | 49 | print(sys.version) @@ -83,7 +78,6 @@ PLR1711 [*] Useless `return` statement at end of function 59 | print(f"{key} not found") 60 | return None | ^^^^^^^^^^^ - | help: Remove useless `return` statement | 59 | print(f"{key} not found") diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1712_swap_with_temporary_variable.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1712_swap_with_temporary_variable.py.snap index e24fdc6ec8..9df056a3ac 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1712_swap_with_temporary_variable.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1712_swap_with_temporary_variable.py.snap @@ -10,7 +10,6 @@ PLR1712 [*] Unnecessary temporary variable 4 | | x = y 5 | | y = temp | |____________^ - | help: Use `x, y = y, x` instead | 2 | def foo(x: int, y: int): @@ -30,7 +29,6 @@ PLR1712 [*] Unnecessary temporary variable 12 | | x = y 13 | | y = temp | |________________^ - | help: Use `x, y = y, x` instead | 10 | if x > 5: @@ -50,7 +48,6 @@ PLR1712 [*] Unnecessary temporary variable 19 | | x = y 20 | | y = temp | |____________^ - | help: Use `x, y = y, x` instead | 17 | def bar(x: int, y: int): diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1714_repeated_equality_comparison.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1714_repeated_equality_comparison.py.snap index fb21bca8fd..e1284b355c 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1714_repeated_equality_comparison.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1714_repeated_equality_comparison.py.snap @@ -425,7 +425,6 @@ PLR1714 [*] Consider merging multiple comparisons: `foo in {"bar", "bar", "buzz" 78 | 79 | foo == "bar" or foo == "bar" or foo == "buzz" # All but one members identical | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Merge multiple comparisons | 78 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1716_boolean_chained_comparison.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1716_boolean_chained_comparison.py.snap index 16032c7afb..3e6d4093a7 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1716_boolean_chained_comparison.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1716_boolean_chained_comparison.py.snap @@ -415,7 +415,6 @@ PLR1716 [*] Contains chained boolean comparison that can be simplified 147 | 148 | a < (b) and (((b)) < c) | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Use a single compare expression | 147 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_0.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_0.py.snap index de87067f7e..8b8dec2666 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_0.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_0.py.snap @@ -23,7 +23,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 1 | exit(0) 2 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 1 + import sys @@ -61,7 +60,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 6 | exit(2) 7 | quit(2) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 1 + import sys diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_1.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_1.py.snap index ac72b8fe8f..fd44455ca8 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_1.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_1.py.snap @@ -25,7 +25,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 3 | exit(0) 4 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 3 | exit(0) @@ -59,7 +58,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 8 | exit(1) 9 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 8 | exit(1) @@ -86,5 +84,4 @@ PLR1722 Use `sys.exit()` instead of `quit` 15 | exit(1) 16 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_10.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_10.py.snap index c6481fa743..cd58284dba 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_10.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_10.py.snap @@ -7,5 +7,4 @@ PLR1722 Use `sys.exit()` instead of `exit` 7 | def main(): 8 | exit(0) | ^^^^ - | help: Replace `exit` with `sys.exit()` diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_11.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_11.py.snap index 2f6f7be5ea..e6dd0ff9c4 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_11.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_11.py.snap @@ -8,7 +8,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 2 | 3 | exit(0) | ^^^^ - | help: Replace `exit` with `sys.exit()` | 1 | from sys import * diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_12.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_12.py.snap index 453d5bb264..29ae1851cc 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_12.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_12.py.snap @@ -8,7 +8,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 2 | 3 | exit(0) | ^^^^ - | help: Replace `exit` with `sys.exit()` | - import os \ diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_13.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_13.py.snap index 235983f89c..665bec6c9e 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_13.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_13.py.snap @@ -6,7 +6,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` | 1 | exit(code=2) | ^^^^ - | help: Replace `exit` with `sys.exit()` | - exit(code=2) diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_14.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_14.py.snap index e190005449..abd4d99f49 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_14.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_14.py.snap @@ -7,5 +7,4 @@ PLR1722 Use `sys.exit()` instead of `exit` 1 | code = {"code": 2} 2 | exit(**code) | ^^^^ - | help: Replace `exit` with `sys.exit()` diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_15.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_15.py.snap index 38cca3c033..a704ab290d 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_15.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_15.py.snap @@ -8,7 +8,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 5 | code = 1 6 | exit(code) | ^^^^ - | help: Replace `exit` with `sys.exit()` | 1 + import sys diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_16.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_16.py.snap index c2b96562d5..e5afb4da80 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_16.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_16.py.snap @@ -53,7 +53,6 @@ PLR1722 Use `sys.exit()` instead of `exit` 16 | # no diagnostic for multiple arguments 17 | exit(2, 3, 4) | ^^^^ - | help: Replace `exit` with `sys.exit()` PLR1722 [*] Use `sys.exit()` instead of `exit` @@ -63,7 +62,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 22 | codes = [1] 23 | exit(*codes) | ^^^^ - | help: Replace `exit` with `sys.exit()` | 1 + import sys diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_2.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_2.py.snap index 9d18e37bcf..9397b6aebb 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_2.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_2.py.snap @@ -25,7 +25,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 3 | exit(0) 4 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 3 | exit(0) @@ -59,7 +58,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 8 | exit(1) 9 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 8 | exit(1) diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_3.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_3.py.snap index 1764a2f47d..7ffc90f08f 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_3.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_3.py.snap @@ -7,7 +7,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 3 | exit(0) 4 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 3 | exit(0) @@ -24,7 +23,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 8 | exit(1) 9 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 8 | exit(1) @@ -40,5 +38,4 @@ PLR1722 Use `sys.exit()` instead of `quit` 15 | exit(1) 16 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_4.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_4.py.snap index 38e6973e88..4bdd3ae17d 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_4.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_4.py.snap @@ -25,7 +25,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 3 | exit(0) 4 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 3 | exit(0) @@ -59,7 +58,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 8 | exit(1) 9 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 8 | exit(1) diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_5.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_5.py.snap index 60af85491f..7d3d509cfa 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_5.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_5.py.snap @@ -27,7 +27,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 3 | exit(0) 4 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 1 | from sys import * @@ -68,7 +67,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 8 | exit(1) 9 | quit(1) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 1 | from sys import * diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_6.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_6.py.snap index 2d624b367e..440149c059 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_6.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_6.py.snap @@ -23,7 +23,6 @@ PLR1722 [*] Use `sys.exit()` instead of `quit` 1 | exit(0) 2 | quit(0) | ^^^^ - | help: Replace `quit` with `sys.exit()` | 1 + import sys diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_7.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_7.py.snap index 61d707e153..60d5d65e13 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_7.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_7.py.snap @@ -7,7 +7,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 1 | def main(): 2 | exit(0) | ^^^^ - | help: Replace `exit` with `sys.exit()` | 1 + import sys diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_8.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_8.py.snap index 7f52bd45a2..056475d8ec 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_8.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_8.py.snap @@ -7,7 +7,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 4 | def main(): 5 | exit(0) | ^^^^ - | help: Replace `exit` with `sys.exit()` | - from sys import argv diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_9.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_9.py.snap index f011793086..c88151f1f0 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_9.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1722_sys_exit_alias_9.py.snap @@ -7,7 +7,6 @@ PLR1722 [*] Use `sys.exit()` instead of `exit` 1 | def main(): 2 | exit(0) | ^^^^ - | help: Replace `exit` with `sys.exit()` | 1 + import sys 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 66fb42986f..4e1050ac1a 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 @@ -141,7 +141,6 @@ PLR1730 [*] Replace `if` statement with `b = max(b, a)` 49 | / if a > b: 50 | | b = a | |_________^ - | help: Replace with `b = max(b, a)` | 48 | # case 8: b = max(b, a) @@ -253,7 +252,6 @@ PLR1730 [*] Replace `if` statement with `value = min(value, value2)` 79 | / if value > value2: 80 | | value = value2 | |__________________^ - | help: Replace with `value = min(value, value2)` | 78 | # base case 5: value = min(value, value2) @@ -289,7 +287,6 @@ PLR1730 [*] Replace `if` statement with `A1.value = min(A1.value, 10)` 95 | / if A1.value > 10: 96 | | A1.value = 10 | |_________________^ - | help: Replace with `A1.value = min(A1.value, 10)` | 94 | @@ -556,7 +553,6 @@ PLR1730 [*] Replace `if` statement with `self._max = min(value, self._max)` 219 | / if self._max >= value: 220 | | self._max = value | |_____________________________^ - | help: Replace with `self._max = min(value, self._max)` | 218 | self._min = value @@ -672,7 +668,6 @@ PLR1730 [*] Replace `if` statement with `a = min(b, a)` 250 | / if a >= b: 251 | | a = b # very important comment | |_________^ - | help: Replace with `a = min(b, a)` | 249 | # fix marked safe as preserve comments diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1733_unnecessary_dict_index_lookup.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1733_unnecessary_dict_index_lookup.py.snap index 962e253b45..987539e90e 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1733_unnecessary_dict_index_lookup.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1733_unnecessary_dict_index_lookup.py.snap @@ -94,7 +94,6 @@ PLR1733 [*] Unnecessary lookup of dictionary value by key 10 | blah = FRUITS[fruit_name] # PLR1733 11 | assert FRUITS[fruit_name] == "pear" # PLR1733 | ^^^^^^^^^^^^^^^^^^ - | help: Use existing variable | 10 | blah = FRUITS[fruit_name] # PLR1733 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1736_unnecessary_list_index_lookup.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1736_unnecessary_list_index_lookup.py.snap index 790c9c7e36..837f1e65d4 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1736_unnecessary_list_index_lookup.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR1736_unnecessary_list_index_lookup.py.snap @@ -146,7 +146,6 @@ PLR1736 [*] List index lookup in `enumerate()` loop 18 | blah = letters[index] # PLR1736 19 | assert letters[index] == "d" # PLR1736 | ^^^^^^^^^^^^^^ - | help: Use the loop variable directly | 18 | blah = letters[index] # PLR1736 @@ -180,7 +179,6 @@ PLR1736 [*] List index lookup in `enumerate()` loop 77 | for index, list_item in enumerate(some_list): 78 | print(some_list[index]) | ^^^^^^^^^^^^^^^^ - | help: Use the loop variable directly | 77 | for index, list_item in enumerate(some_list): @@ -196,7 +194,6 @@ PLR1736 [*] List index lookup in `enumerate()` loop 84 | for index, column_name in enumerate(column_names): 85 | _ = data[column_names[index]] # PLR1736 | ^^^^^^^^^^^^^^^^^^^ - | help: Use the loop variable directly | 84 | for index, column_name in enumerate(column_names): diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR2044_empty_comment.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR2044_empty_comment.py.snap index 7637d1a560..da0769a521 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR2044_empty_comment.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR2044_empty_comment.py.snap @@ -57,7 +57,6 @@ PLR2044 [*] Line with empty comment 17 | def foo(): # this comment is OK, the one below is not 18 | pass # | ^ - | help: Delete the empty comment | 17 | def foo(): # this comment is OK, the one below is not @@ -104,7 +103,6 @@ PLR2044 [*] Line with empty comment 57 | α = 1 58 | α# | ^ - | help: Delete the empty comment | 57 | α = 1 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR6104_non_augmented_assignment.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR6104_non_augmented_assignment.py.snap index 60f426f661..f690326bdc 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR6104_non_augmented_assignment.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR6104_non_augmented_assignment.py.snap @@ -441,7 +441,6 @@ PLR6104 [*] Use `*=` to perform an augmented assignment directly 41 | 42 | index = index * (index + 10) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with augmented assignment | 41 | @@ -458,7 +457,6 @@ PLR6104 [*] Use `+=` to perform an augmented assignment directly 46 | def t(self): 47 | self.a = self.a + 1 | ^^^^^^^^^^^^^^^^^^^ - | help: Replace with augmented assignment | 46 | def t(self): @@ -474,7 +472,6 @@ PLR6104 [*] Use `+=` to perform an augmented assignment directly 50 | obj = T() 51 | obj.a = obj.a + 1 | ^^^^^^^^^^^^^^^^^ - | help: Replace with augmented assignment | 50 | obj = T() @@ -660,7 +657,6 @@ PLR6104 [*] Use `+=` to perform an augmented assignment directly 93 | | \ 94 | | test8 | |_________^ - | help: Replace with augmented assignment | 89 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0101_unreachable.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0101_unreachable.py.snap index 87c0c9c471..feb106dd80 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0101_unreachable.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0101_unreachable.py.snap @@ -34,4 +34,3 @@ PLW0101 Unreachable code in `multiple_returns` 29 | | return 2 30 | | print("unreachable range should include above return") | |__________________________________________________________^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0108_unnecessary_lambda.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0108_unnecessary_lambda.py.snap index 928ac24557..5fbe8caa9b 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0108_unnecessary_lambda.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0108_unnecessary_lambda.py.snap @@ -168,5 +168,4 @@ PLW0108 Lambda may be unnecessary; consider inlining inner function 62 | _ = lambda x: (string := str)(x) 63 | _ = lambda x: ((x := 1) and str)(x) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Inline function call diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0129_assert_on_string_literal.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0129_assert_on_string_literal.py.snap index f840a788e5..0f3bfbb781 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0129_assert_on_string_literal.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0129_assert_on_string_literal.py.snap @@ -8,7 +8,6 @@ PLW0129 Asserting on a non-empty string literal will always pass 2 | a = 9 / 3 3 | assert "No ZeroDivisionError were raised" # [assert-on-string-literal] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | PLW0129 Asserting on a non-empty string literal will always pass --> assert_on_string_literal.py:12:12 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0131_named_expr_without_context.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0131_named_expr_without_context.py.snap index 4306ba4fc7..193dc8f464 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0131_named_expr_without_context.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0131_named_expr_without_context.py.snap @@ -18,7 +18,6 @@ PLW0131 Named expression used without context 3 | if True: 4 | (b := 1) | ^^^^^^ - | PLW0131 Named expression used without context --> named_expr_without_context.py:8:6 @@ -26,4 +25,3 @@ PLW0131 Named expression used without context 7 | class Foo: 8 | (c := 1) | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0177_nan_comparison.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0177_nan_comparison.py.snap index 538f8dbb9c..b151cc566b 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0177_nan_comparison.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0177_nan_comparison.py.snap @@ -138,4 +138,3 @@ PLW0177 Comparing against a NaN value; use `math.isnan` instead 98 | assert x == float("-NaN ") 99 | assert x == float(" \n+nan \t") | ^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0244_redefined_slots_in_subclass.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0244_redefined_slots_in_subclass.py.snap index aca4d1ddf4..6b81a1b744 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0244_redefined_slots_in_subclass.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0244_redefined_slots_in_subclass.py.snap @@ -27,7 +27,6 @@ PLW0244 Slot `a` redefined from base class `AnotherBase` 22 | class AnotherChild(AnotherBase): 23 | __slots__ = ["a","b","e","f"] | ^^^ - | PLW0244 Slot `b` redefined from base class `AnotherBase` --> redefined_slots_in_subclass.py:23:22 @@ -35,4 +34,3 @@ PLW0244 Slot `b` redefined from base class `AnotherBase` 22 | class AnotherChild(AnotherBase): 23 | __slots__ = ["a","b","e","f"] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0406_import_self__module.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0406_import_self__module.py.snap index 741e604b57..cfab103c2d 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0406_import_self__module.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0406_import_self__module.py.snap @@ -26,4 +26,3 @@ PLW0406 Module `import_self.module` imports itself 2 | from import_self import module 3 | from . import module | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0602_global_variable_not_assigned.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0602_global_variable_not_assigned.py.snap index 8b06cc77f5..a5fdf4e1ce 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0602_global_variable_not_assigned.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0602_global_variable_not_assigned.py.snap @@ -8,7 +8,6 @@ PLW0602 Using global for `X` but no assignment is done 4 | def f(): 5 | global X | ^ - | PLW0602 Using global for `X` but no assignment is done --> global_variable_not_assigned.py:9:12 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0642_self_or_cls_assignment.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0642_self_or_cls_assignment.py.snap index 360984514f..27df39b7d2 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0642_self_or_cls_assignment.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW0642_self_or_cls_assignment.py.snap @@ -172,5 +172,4 @@ PLW0642 Reassigned `cls` variable in `__new__` method 49 | def __new__(cls): 50 | cls = "apple" # PLW0642 | ^^^ - | help: Consider using a different variable name diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1501_bad_open_mode.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1501_bad_open_mode.py.snap index 0566155cc3..85c95f9b1a 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1501_bad_open_mode.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1501_bad_open_mode.py.snap @@ -127,4 +127,3 @@ PLW1501 `Ua` is not a valid mode for `open` 36 | import builtins 37 | builtins.open(NAME, "Ua", encoding="utf-8") | ^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1507_shallow_copy_environ.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1507_shallow_copy_environ.py.snap index c5c4a85e61..c616da9775 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1507_shallow_copy_environ.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1507_shallow_copy_environ.py.snap @@ -8,7 +8,6 @@ PLW1507 [*] Shallow copy of `os.environ` via `copy.copy(os.environ)` 3 | 4 | copied_env = copy.copy(os.environ) # [shallow-copy-environ] | ^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `os.environ.copy()` | 3 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap index 4a37f5facd..e639eb3059 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap @@ -274,4 +274,3 @@ PLW2901 `for` loop variable `a.i` overwritten by assignment target 179 | for a. i in []: 180 | a.i = 2 # error | ^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW3301_nested_min_max.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW3301_nested_min_max.py.snap index 55c6aef741..564dee97b6 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW3301_nested_min_max.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW3301_nested_min_max.py.snap @@ -291,7 +291,6 @@ PLW3301 [*] Nested `min` calls can be flattened 43 | import builtins 44 | builtins.min(1, min(2, 3)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Flatten nested `min` calls | 43 | import builtins diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__conflict_with_definition_rules.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__conflict_with_definition_rules.snap index 41c104b036..b02e55bb9e 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__conflict_with_definition_rules.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__conflict_with_definition_rules.snap @@ -9,7 +9,6 @@ PLR1712 [*] Unnecessary temporary variable 7 | | x = y 8 | | y = temp | |________^ - | help: Use `x, y = y, x` instead | 5 | x, y = 1, 2 diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__continue_in_finally.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__continue_in_finally.snap index d76dcc4236..fc851db825 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__continue_in_finally.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__continue_in_finally.snap @@ -72,7 +72,6 @@ PLE0116 `continue` not supported inside `finally` clause 48 | finally: 49 | continue # [continue-in-finally] | ^^^^^^^^ - | PLE0116 `continue` not supported inside `finally` clause --> continue_in_finally.py:56:9 @@ -136,4 +135,3 @@ PLE0116 `continue` not supported inside `finally` clause 94 | else: 95 | continue # [continue-in-finally] | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__import_outside_top_level_with_banned.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__import_outside_top_level_with_banned.snap index f433c3ce4b..84ac47d441 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__import_outside_top_level_with_banned.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__import_outside_top_level_with_banned.snap @@ -101,4 +101,3 @@ PLC0415 `import` should be at the top-level of a file 39 | # this should still trigger an error due to multiple imports 40 | from pkg import foo_allowed, bar_banned # [import-outside-toplevel] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLW0133_useless_exception_statement.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLW0133_useless_exception_statement.py.snap index 5e10f7ca81..3bc3b056b2 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLW0133_useless_exception_statement.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__preview__PLW0133_useless_exception_statement.py.snap @@ -56,7 +56,6 @@ PLW0133 [*] Missing `raise` statement on exception 28 | MySubError("This is a custom error") # PLW0133 29 | MyValueError("This is a custom value error") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 28 | MySubError("This is a custom error") # PLW0133 @@ -173,7 +172,6 @@ PLW0133 [*] Missing `raise` statement on exception 48 | MySubError("This is an exception") # PLW0133 49 | MyValueError("This is an exception") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 48 | MySubError("This is an exception") # PLW0133 @@ -230,7 +228,6 @@ PLW0133 [*] Missing `raise` statement on exception 58 | MySubError("This is an exception") # PLW0133 59 | MyValueError("This is an exception") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 58 | MySubError("This is an exception") # PLW0133 @@ -346,7 +343,6 @@ PLW0133 [*] Missing `raise` statement on exception 78 | MySubError("This is an exception") # PLW0133 79 | MyValueError("This is an exception") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 78 | MySubError("This is an exception") # PLW0133 @@ -403,7 +399,6 @@ PLW0133 [*] Missing `raise` statement on exception 89 | MySubError("This is an exception") # PLW0133 90 | MyValueError("This is an exception") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 89 | MySubError("This is an exception") # PLW0133 @@ -460,7 +455,6 @@ PLW0133 [*] Missing `raise` statement on exception 98 | MySubError("This is an exception") # PLW0133 99 | MyValueError("This is an exception") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 98 | MySubError("This is an exception") # PLW0133 @@ -517,7 +511,6 @@ PLW0133 [*] Missing `raise` statement on exception 106 | (MySubError("This is an exception")) # PLW0133 107 | (MyValueError("This is an exception")) # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 106 | (MySubError("This is an exception")) # PLW0133 @@ -574,7 +567,6 @@ PLW0133 [*] Missing `raise` statement on exception 114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133 115 | x = 1; (MyValueError("This is an exception")); y = 2 # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 114 | x = 1; (MySubError("This is an exception")); y = 2 # PLW0133 @@ -592,7 +584,6 @@ PLW0133 [*] Missing `raise` statement on exception 120 | UserWarning("This is a user warning") # PLW0133 121 | MyUserWarning("This is a custom user warning") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 120 | UserWarning("This is a user warning") # PLW0133 @@ -670,7 +661,6 @@ PLW0133 [*] Missing `raise` statement on exception 138 | 139 | MyUserWarning("This is a custom user warning") # PLW0133 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `raise` keyword | 138 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_public_methods.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_public_methods.snap index 8fdb140f3f..531340ccd8 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_public_methods.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__too_many_public_methods.snap @@ -40,4 +40,3 @@ PLR0904 Too many public methods (10 > 7) 37 | | def method9(self): 38 | | pass | |____________^ - | diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index fa3ff8ff49..0faa253c6e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -482,7 +482,6 @@ mod tests { | 1 | from pipes import quote, Template | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Import from `shlex` | - from pipes import quote, Template diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP001.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP001.py.snap index 15420b2197..aad9e7efda 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP001.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP001.py.snap @@ -7,7 +7,6 @@ UP001 [*] `__metaclass__ = type` is implied 1 | class A: 2 | __metaclass__ = type | ^^^^^^^^^^^^^^^^^^^^ - | help: Remove `__metaclass__ = type` | 1 | class A: diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP003.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP003.py.snap index abcdc9c7f3..ecd903ccae 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP003.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP003.py.snap @@ -92,7 +92,6 @@ UP003 [*] Use `str` instead of `type(...)` 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459841 14 | assert isinstance(fullname, type("")is not True) | ^^^^^^^^ - | help: Replace `type(...)` with `str` | 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459841 diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP005.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP005.py.snap index 164ee92e96..14eddffa1e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP005.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP005.py.snap @@ -61,7 +61,6 @@ UP005 [*] `assertNotRegexpMatches` is deprecated, use `assertNotRegex` 9 | self.failUnlessAlmostEqual(1, 1.1) 10 | self.assertNotRegexpMatches("a", "b") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace `assertNotRegex` with `assertNotRegexpMatches` | 9 | self.failUnlessAlmostEqual(1, 1.1) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007_1.py.snap index bdd77d7776..de174b1ede 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP007_1.py.snap @@ -216,7 +216,6 @@ UP007 [*] Use `X | Y` for type annotations 48 | x: Union[str, int] 49 | x: Union["str", "int"] | ^^^^^^^^^^^^^^^^^^^ - | help: Convert to `X | Y` | 48 | x: Union[str, int] @@ -324,7 +323,6 @@ UP007 [*] Use `X | Y` for type annotations 154 | | | Literal["LongLiteralNumberThree"] 155 | | ] | |_____^ - | help: Convert to `X | Y` | 150 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py.snap index d84e4c2caa..ab341e4a98 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP008.py.snap @@ -47,7 +47,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 21 | | self, 22 | | ).method() # wrong | |_________^ - | help: Remove `super()` parameters | 18 | super(Child, self).method # wrong @@ -116,7 +115,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 115 | def bar(self): 116 | super(__class__, self).foo() | ^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 115 | def bar(self): @@ -193,7 +191,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 150 | | # also a comment 151 | | ).f() | |_________^ - | help: Remove `super()` parameters | 146 | ).f() @@ -426,7 +423,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 232 | super # Python injects __class__ into scope 233 | builtins.super(ChildD10, self).f() | ^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 232 | super # Python injects __class__ into scope @@ -442,7 +438,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 341 | def __init__(self, foo): 342 | super(Outer.Inner, self).__init__(foo) # UP008: matches enclosing class chain | ^^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 341 | def __init__(self, foo): @@ -476,7 +471,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 387 | def f(self): 388 | super (Whitespace, self).f() # can use super() | ^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 387 | def f(self): @@ -492,7 +486,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 394 | def f(self): 395 | super(LocalOuter.LocalInner, self).f() # can use super() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 394 | def f(self): @@ -507,7 +500,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 398 | class LambdaMethod(BaseClass): 399 | f = lambda self: super(LambdaMethod, self).f() # can use super() | ^^^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 398 | class LambdaMethod(BaseClass): @@ -523,7 +515,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 404 | def f(cls): 405 | super(ClassMethod, cls).f() # can use super() | ^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 404 | def f(cls): @@ -539,7 +530,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 409 | async def f(self): 410 | super(AsyncMethod, self).f() # can use super() | ^^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 409 | async def f(self): @@ -555,7 +545,6 @@ UP008 [*] Use `super()` instead of `super(__class__, self)` 415 | def f(self): 416 | super (OuterWithWhitespace.Inner, self).f() # can use super() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `super()` parameters | 415 | def f(self): diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP010_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP010_0.py.snap index 6515184e9c..1eba176457 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP010_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP010_0.py.snap @@ -171,7 +171,6 @@ UP010 [*] Unnecessary `__future__` import `generators` for target Python version 14 | from __future__ import invalid_module, generators 15 | from __future__ import generators # comment | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unnecessary `__future__` import | 14 | from __future__ import invalid_module, generators diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP012.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP012.py.snap index 39afd57a55..a0adc52d17 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP012.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP012.py.snap @@ -468,7 +468,6 @@ UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode` 76 | ("unicode text©").encode("utf-8") 77 | ("unicode text©").encode(encoding="utf-8") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unnecessary `encoding` argument | 76 | ("unicode text©").encode("utf-8") @@ -800,7 +799,6 @@ UP012 [*] Unnecessary call to `encode` as UTF-8 119 | 120 | '\\ u0000 '.encode() | ^^^^^^^^^^^^^^^^^^^^ - | help: Rewrite as bytes literal | 119 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP014.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP014.py.snap index 3a508e0f37..7bf610bcde 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP014.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP014.py.snap @@ -102,7 +102,6 @@ UP014 [*] Convert `X` from `NamedTuple` functional to class syntax 37 | | ("some_config", int), # important 38 | | ]) | |__^ - | help: Convert `X` to class syntax | 35 | # Unsafe fix if comments are present diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP015_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP015_1.py.snap index 2f519d3769..76b4fe2b95 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP015_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP015_1.py.snap @@ -8,7 +8,6 @@ UP015 [*] Unnecessary mode argument 2 | # Refer: https://github.com/astral-sh/ruff/issues/11736 3 | x: 'open("foo", "r")' | ^^^ - | help: Remove mode argument | 2 | # Refer: https://github.com/astral-sh/ruff/issues/11736 diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap index c81ba0a928..a331620e67 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap @@ -389,7 +389,6 @@ UP018 [*] Unnecessary `float` call (rewrite as a literal) 60 | float(+1.0) 61 | float(-1.0) | ^^^^^^^^^^^ - | help: Replace with float literal | 60 | float(+1.0) @@ -525,7 +524,6 @@ UP018 [*] Unnecessary `int` call (rewrite as a literal) 75 | 76 | await int(-1) # await (-1) | ^^^^^^^ - | help: Replace with integer literal | 75 | @@ -555,7 +553,6 @@ UP018 [*] Unnecessary `float` call (rewrite as a literal) 79 | int(+1) ** 0 80 | float(+1.0)() | ^^^^^^^^^^^ - | help: Replace with float literal | 79 | int(+1) ** 0 @@ -647,7 +644,6 @@ UP018 [*] Unnecessary `bool` call (rewrite as a literal) 91 | float(1.)and None 92 | bool(True)and() | ^^^^^^^^^^ - | help: Replace with boolean literal | 91 | float(1.)and None @@ -866,7 +862,6 @@ UP018 [*] Unnecessary `complex` call (rewrite as a literal) 112 | complex(1j).real 113 | complex(real=1j).real | ^^^^^^^^^^^^^^^^ - | help: Replace with complex literal | 112 | complex(1j).real 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 index cd05516752..b16bc7a8bf 100644 --- 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 @@ -23,7 +23,6 @@ UP018 [*] Unnecessary `int` call (rewrite as a literal) 4 | / int(+ 5 | | 1) | |______^ - | help: Replace with integer literal | 3 | 1) - int(+ 4 + (+ 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 index 5b3d30c93f..3f367e591f 100644 --- 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 @@ -28,7 +28,6 @@ UP018 [*] Unnecessary `int` call (rewrite as a literal) 6 | / int(+ 7 | | 1) | |______^ - | help: Replace with integer literal | 5 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP024_2.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP024_2.py.snap index 3f999e9d4e..bd02547a50 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP024_2.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP024_2.py.snap @@ -391,7 +391,6 @@ UP024 [*] Replace aliased errors with `OSError` 54 | raise EnvironmentError(1) 55 | raise IOError(1, 2) | ^^^^^^^ - | help: Replace `IOError` with builtin `OSError` | 54 | raise EnvironmentError(1) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP025.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP025.py.snap index 087db8f6f9..a3315333bf 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP025.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP025.py.snap @@ -323,7 +323,6 @@ UP025 [*] Remove unicode literals from strings 33 | """"""""""""""""""""u"hi" 34 | ""U"helloooo" | ^^^^^^^^^^^ - | help: Remove unicode prefix | 33 | """"""""""""""""""""u"hi" diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP026.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP026.py.snap index 6ffb0e6bf4..0a8c718d06 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP026.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP026.py.snap @@ -503,7 +503,6 @@ UP026 [*] `mock` is deprecated, use `unittest.mock` 85 | # This should yield multiple, aliased imports. 86 | from mock import mock as foo, mock as bar, mock | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Import from `unittest.mock` instead | 85 | # This should yield multiple, aliased imports. @@ -520,7 +519,6 @@ UP026 [*] `mock` is deprecated, use `unittest.mock` 92 | # Error (`mock.Mock()`). 93 | x = mock.mock.Mock() | ^^^^^^^^^ - | help: Replace `mock.mock` with `mock` | 92 | # Error (`mock.Mock()`). diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP028_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP028_0.py.snap index ad2c71231e..c8e7dca2fe 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP028_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP028_0.py.snap @@ -8,7 +8,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 2 | / for x in y: 3 | | yield x | |_______________^ - | help: Replace with `yield from` | 1 | def f(): @@ -26,7 +25,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 7 | / for x, y in z: 8 | | yield (x, y) | |____________________^ - | help: Replace with `yield from` | 6 | def g(): @@ -44,7 +42,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 12 | / for x in [1, 2, 3]: 13 | | yield x | |_______________^ - | help: Replace with `yield from` | 11 | def h(): @@ -62,7 +59,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 17 | / for x in {x for x in y}: 18 | | yield x | |_______________^ - | help: Replace with `yield from` | 16 | def i(): @@ -80,7 +76,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 22 | / for x in (1, 2, 3): 23 | | yield x | |_______________^ - | help: Replace with `yield from` | 21 | def j(): @@ -98,7 +93,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 27 | / for x, y in {3: "x", 6: "y"}: 28 | | yield x, y | |__________________^ - | help: Replace with `yield from` | 26 | def k(): @@ -147,7 +141,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 44 | / for x, y in [{3: (3, [44, "long ss"]), 6: "y"}]: 45 | | yield x, y | |__________________^ - | help: Replace with `yield from` | 43 | def f(): @@ -209,7 +202,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 67 | / for z in x: 68 | | yield z | |_______________^ - | help: Replace with `yield from` | 66 | yield x @@ -250,7 +242,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 82 | | ): 83 | | yield h | |_______________^ - | help: Replace with `yield from` | 78 | def _serve_method(fn): @@ -357,7 +348,6 @@ UP028 [*] Replace `yield` over `for` loop with `yield from` 170 | / for a in 1,: 171 | | yield a | |_______________^ - | help: Replace with `yield from` | 169 | def f(): @@ -389,5 +379,4 @@ UP028 Replace `yield` over `for` loop with `yield from` 187 | / for some_non_local in iterable: 188 | | yield some_non_local | |________________________________^ - | help: Replace with `yield from` diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP029_1.py_skip_required_imports.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP029_1.py_skip_required_imports.snap index 52bc289d48..69d538baef 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP029_1.py_skip_required_imports.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP029_1.py_skip_required_imports.snap @@ -6,7 +6,6 @@ UP029 [*] Unnecessary builtin import: `int` | 1 | from builtins import str, int | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unnecessary builtin import | - from builtins import str, int diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP030_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP030_0.py.snap index 207e3e6b2b..1a6e2d88d4 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP030_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP030_0.py.snap @@ -486,7 +486,6 @@ UP030 [*] Use implicit references for positional format fields 64 | 65 | "{{{0}}}".format(123) | ^^^^^^^^^^^^^^^^^^^^^ - | help: Remove explicit positional indices | 64 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP031_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP031_0.py.snap index e578ccf4a0..5251a71ac3 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP031_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP031_0.py.snap @@ -455,7 +455,6 @@ UP031 [*] Use format specifiers instead of percent format 57 | 58 | print("%(a)s" % {"a" : 1}) | ^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with format specifiers | 57 | @@ -815,7 +814,6 @@ UP031 [*] Use format specifiers instead of percent format 112 | | x, # comment 113 | | ) | |_^ - | help: Replace with format specifiers | 110 | @@ -830,7 +828,7 @@ UP031 [*] Use format specifiers instead of percent format | 116 | path = "%s-%s-%s.pem" % ( | ________^ -117 | | safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert ma… +117 | | safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert … 118 | | cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date 119 | | hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix 120 | | ) @@ -1214,5 +1212,4 @@ UP031 Use format specifiers instead of percent format 170 | 171 | "%(and)s" % {"and": 2} | ^^^^^^^^^ - | help: Replace with format specifiers diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap index 6173f60104..774ff321e9 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_0.py.snap @@ -713,7 +713,6 @@ UP032 [*] Use f-string instead of `format` call 106 | | "b" 107 | | ).format(a=1) | |_____________^ - | help: Convert to f-string | 104 | ( @@ -731,7 +730,6 @@ UP032 [*] Use f-string instead of `format` call 110 | def d(osname, version, release): 111 | return"{}-{}.{}".format(osname, version, release) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 110 | def d(osname, version, release): @@ -746,7 +744,6 @@ UP032 [*] Use f-string instead of `format` call 114 | def e(): 115 | yield"{}".format(1) | ^^^^^^^^^^^^^^ - | help: Convert to f-string | 114 | def e(): @@ -760,7 +757,6 @@ UP032 [*] Use f-string instead of `format` call | 118 | assert"{}".format(1) | ^^^^^^^^^^^^^^ - | help: Convert to f-string | 117 | @@ -775,7 +771,6 @@ UP032 [*] Use f-string instead of `format` call 121 | async def c(): 122 | return "{}".format(await 3) | ^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 121 | async def c(): @@ -790,7 +785,6 @@ UP032 [*] Use f-string instead of `format` call 125 | async def c(): 126 | return "{}".format(1 + await 3) | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 125 | async def c(): @@ -927,7 +921,6 @@ UP032 Use f-string instead of `format` call 202 | | 1 # comment 203 | | ) | |_^ - | help: Convert to f-string UP032 [*] Use f-string instead of `format` call @@ -1056,7 +1049,6 @@ UP032 [*] Use f-string instead of `format` call 234 | 235 | ("{}" "{{}}").format(a) | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 234 | @@ -1267,7 +1259,6 @@ UP032 [*] Use f-string instead of `format` call 282 | # Raw string with \N{...} 283 | r"\N{angle}AOB = {angle}°".format(angle=180) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 282 | # Raw string with \N{...} diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_1.py.snap index 51eeb10965..0480f71b84 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_1.py.snap @@ -6,7 +6,6 @@ UP032 [*] Use f-string instead of `format` call | 1 | "{} {}".format(a, b) # Intentionally at start-of-file, to ensure graceful handling. | ^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | - "{} {}".format(a, b) # Intentionally at start-of-file, to ensure graceful handling. diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_2.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_2.py.snap index d9e8d40d7a..3c6fa7aee7 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_2.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP032_2.py.snap @@ -426,7 +426,6 @@ UP032 [*] Use f-string instead of `format` call 32 | "{0.real}".format(1_2) 33 | "{a.real}".format(a=1_2) | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to f-string | 32 | "{0.real}".format(1_2) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP035.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP035.py.snap index fe100e44bf..b84ae75f6a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP035.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP035.py.snap @@ -1130,7 +1130,6 @@ UP035 [*] Import from `re` instead: `Pattern` 133 | # UP035 on py37+ only 134 | from typing.re import Pattern | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Import from `re` | 133 | # UP035 on py37+ only diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_0.py.snap index d0bce0c8a3..a9c1dba916 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_0.py.snap @@ -37,7 +37,6 @@ UP037 [*] Remove quotes from type annotation 18 | def foo(var: "MyClass") -> "MyClass": 19 | x: "MyClass" | ^^^^^^^^^ - | help: Remove quotes | 18 | def foo(var: "MyClass") -> "MyClass": @@ -114,7 +113,6 @@ UP037 [*] Remove quotes from type annotation 31 | 32 | x: Callable[["MyClass"], None] | ^^^^^^^^^ - | help: Remove quotes | 31 | @@ -129,7 +127,6 @@ UP037 [*] Remove quotes from type annotation 35 | class Foo(NamedTuple): 36 | x: "MyClass" | ^^^^^^^^^ - | help: Remove quotes | 35 | class Foo(NamedTuple): @@ -144,7 +141,6 @@ UP037 [*] Remove quotes from type annotation 39 | class D(TypedDict): 40 | E: TypedDict("E", foo="int", total=False) | ^^^^^ - | help: Remove quotes | 39 | class D(TypedDict): @@ -159,7 +155,6 @@ UP037 [*] Remove quotes from type annotation 43 | class D(TypedDict): 44 | E: TypedDict("E", {"foo": "int"}) | ^^^^^ - | help: Remove quotes | 43 | class D(TypedDict): @@ -559,7 +554,6 @@ UP037 [*] Remove quotes from type annotation 125 | def foo(bar: "A\n#"): ... 126 | def foo(bar: "A\n#\n"): ... | ^^^^^^^^ - | help: Remove quotes | 125 | def foo(bar: "A\n#"): ... diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_1.py.snap index 3e8587c3a4..09e59a880d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_1.py.snap @@ -24,7 +24,6 @@ UP037 [*] Remove quotes from type annotation 13 | # OK 14 | X: "Tuple[int, int]" = (0, 0) | ^^^^^^^^^^^^^^^^^ - | help: Remove quotes | 13 | # OK diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_2.pyi.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_2.pyi.snap index 645b89f483..5ef03783a7 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_2.pyi.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_2.pyi.snap @@ -8,7 +8,6 @@ UP037 [*] Remove quotes from type annotation 2 | 3 | def f(a: Foo['SingleLine # Comment']): ... | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove quotes | 2 | @@ -26,7 +25,6 @@ UP037 [*] Remove quotes from type annotation 7 | | Multi | 8 | | Line]''']): ... | |____________^ - | help: Remove quotes | 5 | @@ -47,7 +45,6 @@ UP037 [*] Remove quotes from type annotation 13 | | Line # Comment 14 | | ]''']): ... | |____^ - | help: Remove quotes | 10 | @@ -68,7 +65,6 @@ UP037 [*] Remove quotes from type annotation 18 | | Multi | 19 | | Line] # Comment''']): ... | |_______________________^ - | help: Remove quotes | 16 | @@ -90,7 +86,6 @@ UP037 [*] Remove quotes from type annotation 24 | | Multi | 25 | | Line] # Comment''']): ... | |_______________________^ - | help: Remove quotes | 21 | @@ -111,7 +106,6 @@ UP037 [*] Remove quotes from type annotation | __________^ 29 | | ''' = []): ... | |_______^ - | help: Remove quotes | 27 | @@ -129,7 +123,6 @@ UP037 [*] Remove quotes from type annotation | ____^ 33 | | list[int]''' = [42] | |____________^ - | help: Remove quotes | 31 | @@ -148,7 +141,6 @@ UP037 [*] Remove quotes from type annotation 37 | | list[int] 38 | | ''' = []): ... | |_______^ - | help: Remove quotes | 35 | @@ -171,7 +163,6 @@ UP037 [*] Remove quotes from type annotation 45 | | Line 46 | | ] # Comment''']): ... | |___________________^ - | help: Remove quotes | 40 | @@ -194,7 +185,6 @@ UP037 [*] Remove quotes from type annotation | ____^ 50 | | [int]''' = [42] | |________^ - | help: Remove quotes | 48 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_3.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_3.py.snap index 7094914f7f..f16c938d50 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_3.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP037_3.py.snap @@ -26,7 +26,6 @@ UP037 [*] Remove quotes from type annotation 16 | # the behavior of _singleton above should match a non-ClassVar 17 | _doubleton: "EmptyCell" | ^^^^^^^^^^^ - | help: Remove quotes | 16 | # the behavior of _singleton above should match a non-ClassVar diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP039.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP039.py.snap index be76b72ac4..29e0906293 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP039.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP039.py.snap @@ -89,7 +89,6 @@ UP039 [*] Unnecessary parentheses after class definition 47 | | # text 48 | | ): ... | |_^ - | help: Remove parentheses | 45 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap index e3acbbb7a5..2baabf904c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap @@ -215,7 +215,6 @@ UP040 [*] Type alias `Decorator` uses `TypeAlias` annotation instead of the `typ 56 | T = typing.TypeVar["T"] 57 | Decorator: TypeAlias = typing.Callable[[T], T] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use the `type` keyword | 56 | T = typing.TypeVar["T"] @@ -352,7 +351,6 @@ UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of t 105 | | "PositiveList", list[Annotated[T, Gt(0)]], type_params=(T,) 106 | | ) # this comment should be okay | |_^ - | help: Use the `type` keyword | 103 | T = TypeVar("T") diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap index f2eed99224..81fb3d356d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py__preview_diff.snap @@ -56,7 +56,6 @@ UP040 [*] Type alias `DefaultList` uses `TypeAlias` annotation instead of the `t 133 | T_default = TypeVar("T_default", default=int) 134 | DefaultList: TypeAlias = list[T_default] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use the `type` keyword | 133 | T_default = TypeVar("T_default", default=int) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.pyi.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.pyi.snap index 5ce1224285..c37220ce2a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.pyi.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.pyi.snap @@ -25,7 +25,6 @@ UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keywo 6 | x: typing.TypeAlias = int 7 | x: TypeAlias = int | ^^^^^^^^^^^^^^^^^^ - | help: Use the `type` keyword | 6 | x: typing.TypeAlias = int @@ -69,7 +68,6 @@ UP040 [*] Type alias `T` uses `TypeAlias` annotation instead of the `type` keywo 23 | | # comment7 24 | | ) # comment8 | |_^ - | help: Use the `type` keyword | 15 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap index 2016e4aea4..dae543a0e8 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap @@ -6,7 +6,6 @@ UP042 [*] Class A inherits from both `str` and `enum.Enum` | 4 | class A(str, Enum): ... | ^ - | help: Inherit from `enum.StrEnum` | - from enum import Enum @@ -24,7 +23,6 @@ UP042 [*] Class B inherits from both `str` and `enum.Enum` | 7 | class B(Enum, str): ... | ^ - | help: Inherit from `enum.StrEnum` | - from enum import Enum @@ -43,7 +41,6 @@ UP042 Class D inherits from both `str` and `enum.Enum` | 10 | class D(int, str, Enum): ... | ^ - | help: Inherit from `enum.StrEnum` UP042 Class E inherits from both `str` and `enum.Enum` @@ -51,5 +48,4 @@ UP042 Class E inherits from both `str` and `enum.Enum` | 13 | class E(str, int, Enum): ... | ^ - | help: Inherit from `enum.StrEnum` diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045_1.py.snap index 8dd4852157..55a22140f4 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045_1.py.snap @@ -54,7 +54,6 @@ UP045 Use `X | None` for type annotations 14 | x: Optional[str] 15 | x = Optional[str] | ^^^^^^^^^^^^^ - | help: Convert to `X | None` UP045 [*] Use `X | None` for type annotations @@ -110,7 +109,6 @@ UP045 [*] Use `X | None` for type annotations 38 | | | list[ServiceSpecification] 39 | | ] = None | |_____^ - | help: Convert to `X | None` | 35 | class ServiceRefOrValue: @@ -129,7 +127,6 @@ UP045 [*] Use `X | None` for type annotations 43 | class ServiceRefOrValue: 44 | service_specification: Optional[str]is not True = None | ^^^^^^^^^^^^^ - | help: Convert to `X | None` | 43 | class ServiceRefOrValue: @@ -145,7 +142,6 @@ UP045 Use `X | None` for type annotations 48 | # Optional[None] should not be offered a fix 49 | foo: Optional[None] = None | ^^^^^^^^^^^^^^ - | help: Convert to `X | None` UP045 [*] Use `X | None` for type annotations @@ -225,7 +221,6 @@ UP045 [*] Use `X | None` for type annotations 77 | nested_optional_typing: typing.Optional[Optional[int]] = None 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to `X | None` | 77 | nested_optional_typing: typing.Optional[Optional[int]] = None @@ -241,7 +236,6 @@ UP045 [*] Use `X | None` for type annotations 77 | nested_optional_typing: typing.Optional[Optional[int]] = None 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to `X | None` | 77 | nested_optional_typing: typing.Optional[Optional[int]] = None @@ -257,7 +251,6 @@ UP045 [*] Use `X | None` for type annotations 77 | nested_optional_typing: typing.Optional[Optional[int]] = None 78 | triple_nested_optional: Optional[Optional[Optional[str]]] = None | ^^^^^^^^^^^^^ - | help: Convert to `X | None` | 77 | nested_optional_typing: typing.Optional[Optional[int]] = None @@ -275,7 +268,6 @@ UP045 [*] Use `X | None` for type annotations 83 | | # text 84 | | ] = None | |_^ - | help: Convert to `X | None` | 80 | @@ -366,5 +358,4 @@ UP045 Use `X | None` for type annotations 92 | bar: Optional[None | int | str] = None 93 | bar: Optional[None | None] = None | ^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to `X | None` diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap index dc9ec51021..0dcc52d704 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap @@ -167,7 +167,6 @@ UP046 [*] Generic class `A` uses `Generic` subclass instead of type parameters | 73 | class A(Generic[T]): ... | ^^^^^^^^^^ - | help: Use type parameters | 72 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP049_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP049_1.py.snap index 3a59718287..36991658e0 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP049_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP049_1.py.snap @@ -203,7 +203,6 @@ UP049 Generic class uses private type parameters 38 | # offer a diagnostic 39 | class F[_async]: ... | ^^^^^^ - | help: Rename type parameter to remove leading underscores UP049 Generic class uses private type parameters @@ -223,7 +222,6 @@ UP049 Generic class uses private type parameters | 64 | class C[_0]: ... | ^^ - | help: Rename type parameter to remove leading underscores UP049 Generic class uses private type parameters @@ -241,7 +239,6 @@ UP049 Generic class uses private type parameters 67 | class C[T, _T]: ... 68 | class C[_T, T]: ... | ^^ - | help: Rename type parameter to remove leading underscores UP049 [*] Generic class uses private type parameters diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_0.py.snap index d0bce0c8a3..a9c1dba916 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_0.py.snap @@ -37,7 +37,6 @@ UP037 [*] Remove quotes from type annotation 18 | def foo(var: "MyClass") -> "MyClass": 19 | x: "MyClass" | ^^^^^^^^^ - | help: Remove quotes | 18 | def foo(var: "MyClass") -> "MyClass": @@ -114,7 +113,6 @@ UP037 [*] Remove quotes from type annotation 31 | 32 | x: Callable[["MyClass"], None] | ^^^^^^^^^ - | help: Remove quotes | 31 | @@ -129,7 +127,6 @@ UP037 [*] Remove quotes from type annotation 35 | class Foo(NamedTuple): 36 | x: "MyClass" | ^^^^^^^^^ - | help: Remove quotes | 35 | class Foo(NamedTuple): @@ -144,7 +141,6 @@ UP037 [*] Remove quotes from type annotation 39 | class D(TypedDict): 40 | E: TypedDict("E", foo="int", total=False) | ^^^^^ - | help: Remove quotes | 39 | class D(TypedDict): @@ -159,7 +155,6 @@ UP037 [*] Remove quotes from type annotation 43 | class D(TypedDict): 44 | E: TypedDict("E", {"foo": "int"}) | ^^^^^ - | help: Remove quotes | 43 | class D(TypedDict): @@ -559,7 +554,6 @@ UP037 [*] Remove quotes from type annotation 125 | def foo(bar: "A\n#"): ... 126 | def foo(bar: "A\n#\n"): ... | ^^^^^^^^ - | help: Remove quotes | 125 | def foo(bar: "A\n#"): ... diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_1.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_1.py.snap index 3e8587c3a4..09e59a880d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_1.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_1.py.snap @@ -24,7 +24,6 @@ UP037 [*] Remove quotes from type annotation 13 | # OK 14 | X: "Tuple[int, int]" = (0, 0) | ^^^^^^^^^^^^^^^^^ - | help: Remove quotes | 13 | # OK diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_2.pyi.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_2.pyi.snap index 645b89f483..5ef03783a7 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_2.pyi.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__add_future_annotation_UP037_2.pyi.snap @@ -8,7 +8,6 @@ UP037 [*] Remove quotes from type annotation 2 | 3 | def f(a: Foo['SingleLine # Comment']): ... | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove quotes | 2 | @@ -26,7 +25,6 @@ UP037 [*] Remove quotes from type annotation 7 | | Multi | 8 | | Line]''']): ... | |____________^ - | help: Remove quotes | 5 | @@ -47,7 +45,6 @@ UP037 [*] Remove quotes from type annotation 13 | | Line # Comment 14 | | ]''']): ... | |____^ - | help: Remove quotes | 10 | @@ -68,7 +65,6 @@ UP037 [*] Remove quotes from type annotation 18 | | Multi | 19 | | Line] # Comment''']): ... | |_______________________^ - | help: Remove quotes | 16 | @@ -90,7 +86,6 @@ UP037 [*] Remove quotes from type annotation 24 | | Multi | 25 | | Line] # Comment''']): ... | |_______________________^ - | help: Remove quotes | 21 | @@ -111,7 +106,6 @@ UP037 [*] Remove quotes from type annotation | __________^ 29 | | ''' = []): ... | |_______^ - | help: Remove quotes | 27 | @@ -129,7 +123,6 @@ UP037 [*] Remove quotes from type annotation | ____^ 33 | | list[int]''' = [42] | |____________^ - | help: Remove quotes | 31 | @@ -148,7 +141,6 @@ UP037 [*] Remove quotes from type annotation 37 | | list[int] 38 | | ''' = []): ... | |_______^ - | help: Remove quotes | 35 | @@ -171,7 +163,6 @@ UP037 [*] Remove quotes from type annotation 45 | | Line 46 | | ] # Comment''']): ... | |___________________^ - | help: Remove quotes | 40 | @@ -194,7 +185,6 @@ UP037 [*] Remove quotes from type annotation | ____^ 50 | | [int]''' = [42] | |________^ - | help: Remove quotes | 48 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__datetime_utc_alias_py311.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__datetime_utc_alias_py311.snap index 557f768046..1759a22bd9 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__datetime_utc_alias_py311.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__datetime_utc_alias_py311.snap @@ -8,7 +8,6 @@ UP017 [*] Use `datetime.UTC` alias 9 | 10 | print(timezone.utc) | ^^^^^^^^^^^^ - | help: Convert to `datetime.UTC` alias | 1 + from datetime import UTC @@ -27,7 +26,6 @@ UP017 [*] Use `datetime.UTC` alias 15 | 16 | print(tz.utc) | ^^^^^^ - | help: Convert to `datetime.UTC` alias | 1 + from datetime import UTC @@ -46,7 +44,6 @@ UP017 [*] Use `datetime.UTC` alias 21 | 22 | print(datetime.timezone.utc) | ^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to `datetime.UTC` alias | 21 | @@ -62,7 +59,6 @@ UP017 [*] Use `datetime.UTC` alias 27 | 28 | print(dt.timezone.utc) | ^^^^^^^^^^^^^^^ - | help: Convert to `datetime.UTC` alias | 27 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap index bc78e199be..408d6ee9cb 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap @@ -41,7 +41,6 @@ UP006 [*] Use `list` instead of `List` for type annotation 41 | 42 | MyList: TypeAlias = Union[List[int], List[str]] | ^^^^ - | help: Replace with `list` | 41 | @@ -56,7 +55,6 @@ UP006 [*] Use `list` instead of `List` for type annotation 41 | 42 | MyList: TypeAlias = Union[List[int], List[str]] | ^^^^ - | help: Replace with `list` | 41 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_py310.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_py310.snap index bc78e199be..408d6ee9cb 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_py310.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_585_py310.snap @@ -41,7 +41,6 @@ UP006 [*] Use `list` instead of `List` for type annotation 41 | 42 | MyList: TypeAlias = Union[List[int], List[str]] | ^^^^ - | help: Replace with `list` | 41 | @@ -56,7 +55,6 @@ UP006 [*] Use `list` instead of `List` for type annotation 41 | 42 | MyList: TypeAlias = Union[List[int], List[str]] | ^^^^ - | help: Replace with `list` | 41 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap index 1f3bf23257..f1291f2cf3 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__future_annotations_pep_604_py310.snap @@ -24,7 +24,6 @@ UP007 [*] Use `X | Y` for type annotations 41 | 42 | MyList: TypeAlias = Union[List[int], List[str]] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Convert to `X | Y` | 41 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB105_FURB105.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB105_FURB105.py.snap index 81628ae9e2..3b5583ab03 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB105_FURB105.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB105_FURB105.py.snap @@ -422,7 +422,6 @@ FURB105 [*] Unnecessary empty string passed to `print` 48 | | "" 49 | | ) | |_^ - | help: Remove empty string | 45 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB113_FURB113.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB113_FURB113.py.snap index c096f7886c..346998d67e 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB113_FURB113.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB113_FURB113.py.snap @@ -132,7 +132,6 @@ FURB113 [*] Use `nums.extend((1, 2, 3))` instead of repeatedly calling `nums.app 63 | | nums.append(2) 64 | | nums.append(3) | |______________^ - | help: Replace with `nums.extend((1, 2, 3))` | 61 | # FURB113 @@ -152,7 +151,6 @@ FURB113 [*] Use `nums.extend((1, 2))` instead of repeatedly calling `nums.append 69 | / nums.append(1) 70 | | nums.append(2) | |__________________^ - | help: Replace with `nums.extend((1, 2))` | 68 | # FURB113 @@ -193,7 +191,6 @@ FURB113 Use `nums.extend((1, 2, 3))` instead of repeatedly calling `nums.append( 84 | | nums.append(2) 85 | | nums.append(3) | |__________________^ - | help: Replace with `nums.extend((1, 2, 3))` FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` @@ -204,7 +201,6 @@ FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` 90 | / x.append(1) 91 | | x.append(2) | |_______________^ - | help: Replace with `x.extend((1, 2))` | 89 | # FURB113 @@ -223,7 +219,6 @@ FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` 96 | / x.append(1) 97 | | x.append(2) | |_______________^ - | help: Replace with `x.extend((1, 2))` | 95 | # FURB113 @@ -242,7 +237,6 @@ FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` 102 | / x.append(1) 103 | | x.append(2) | |_______________^ - | help: Replace with `x.extend((1, 2))` | 101 | # FURB113 @@ -261,7 +255,6 @@ FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` 108 | / x.append(1) 109 | | x.append(2) | |_______________^ - | help: Replace with `x.extend((1, 2))` | 107 | # FURB113 @@ -282,7 +275,6 @@ FURB113 Use `x.extend((1, 2, 3))` instead of repeatedly calling `x.append()` 116 | | y.append(1) 117 | | x.append(3) | |_______________^ - | help: Replace with `x.extend((1, 2, 3))` FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` @@ -293,7 +285,6 @@ FURB113 [*] Use `x.extend((1, 2))` instead of repeatedly calling `x.append()` 122 | / x.append(1) 123 | | x.append(2) | |_______________^ - | help: Replace with `x.extend((1, 2))` | 121 | # FURB113 @@ -315,5 +306,4 @@ FURB113 Use `nums.extend((1, 2, 3))` instead of repeatedly calling `nums.append( 131 | | # comment 132 | | nums.append(3) | |__________________^ - | help: Replace with `nums.extend((1, 2, 3))` diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB116_FURB116.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB116_FURB116.py.snap index 7b038caeef..fe95ed16e7 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB116_FURB116.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB116_FURB116.py.snap @@ -252,7 +252,6 @@ FURB116 [*] Replace `bin` call with `f"{-1:b}"` 43 | # for negatives numbers autofix is display-only 44 | print(bin(-1)[2:]) | ^^^^^^^^^^^ - | help: Replace with `f"{-1:b}"` | 43 | # for negatives numbers autofix is display-only diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB118_FURB118.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB118_FURB118.py.snap index fc66bbb6d3..306415bd82 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB118_FURB118.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB118_FURB118.py.snap @@ -709,7 +709,6 @@ FURB118 [*] Use `operator.itemgetter((0, 1))` instead of defining a lambda 34 | op_itemgetter = lambda x: x[0, 1] 35 | op_itemgetter = lambda x: x[(0, 1)] | ^^^^^^^^^^^^^^^^^^^ - | help: Replace with `operator.itemgetter((0, 1))` | 1 | # Errors. @@ -813,7 +812,6 @@ FURB118 [*] Use `operator.itemgetter((1, 2))` instead of defining a lam 94 | # Without a slice, trivia is retained 95 | op_itemgetter = lambda x: x[1, 2] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `operator.itemgetter((1, 2))` | 1 | # Errors. diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB122_FURB122.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB122_FURB122.py.snap index 78c5e04094..126ca05574 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB122_FURB122.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB122_FURB122.py.snap @@ -9,7 +9,6 @@ FURB122 [*] Use of `f.write` in a for loop 10 | / for line in lines: 11 | | f.write(line) | |_________________________^ - | help: Replace with `f.writelines` | 9 | with open("file", "w") as f: @@ -27,7 +26,6 @@ FURB122 [*] Use of `f.write` in a for loop 17 | / for line in lines: 18 | | f.write(other_line) | |_______________________________^ - | help: Replace with `f.writelines` | 16 | with Path("file").open("w") as f: @@ -45,7 +43,6 @@ FURB122 [*] Use of `f.write` in a for loop 23 | / for line in lines: 24 | | f.write(line) | |_________________________^ - | help: Replace with `f.writelines` | 22 | with Path("file").open("w") as f: @@ -63,7 +60,6 @@ FURB122 [*] Use of `f.write` in a for loop 29 | / for line in lines: 30 | | f.write(line.encode()) | |__________________________________^ - | help: Replace with `f.writelines` | 28 | with Path("file").open("wb") as f: @@ -81,7 +77,6 @@ FURB122 [*] Use of `f.write` in a for loop 35 | / for line in lines: 36 | | f.write(line.upper()) | |_________________________________^ - | help: Replace with `f.writelines` | 34 | with Path("file").open("w") as f: @@ -99,7 +94,6 @@ FURB122 [*] Use of `f.write` in a for loop 43 | / for line in lines: 44 | | f.write(line) | |_________________________^ - | help: Replace with `f.writelines` | 42 | @@ -118,7 +112,6 @@ FURB122 [*] Use of `f.write` in a for loop 51 | | # a really important comment 52 | | f.write(line) | |_________________________^ - | help: Replace with `f.writelines` | 49 | with open("file","w") as f: @@ -138,7 +131,6 @@ FURB122 [*] Use of `f.write` in a for loop 57 | / for () in a: 58 | | f.write(()) | |_______________________^ - | help: Replace with `f.writelines` | 56 | with open("file", "w") as f: @@ -156,7 +148,6 @@ FURB122 [*] Use of `f.write` in a for loop 63 | / for a, b, c in d: 64 | | f.write((a, b)) | |___________________________^ - | help: Replace with `f.writelines` | 62 | with open("file", "w") as f: @@ -174,7 +165,6 @@ FURB122 [*] Use of `f.write` in a for loop 69 | / for [(), [a.b], (c,)] in d: 70 | | f.write(()) | |_______________________^ - | help: Replace with `f.writelines` | 68 | with open("file", "w") as f: @@ -192,7 +182,6 @@ FURB122 [*] Use of `f.write` in a for loop 75 | / for [([([a[b]],)],), [], (c[d],)] in e: 76 | | f.write(()) | |_______________________^ - | help: Replace with `f.writelines` | 74 | with open("file", "w") as f: @@ -253,7 +242,6 @@ FURB122 [*] Use of `f.write` in a for loop 96 | | ): 97 | | f.write(f"{char}") | |______________________________^ - | help: Replace with `f.writelines` | 92 | with open("file", "w") as f: @@ -276,7 +264,6 @@ FURB122 [*] Use of `f.write` in a for loop 183 | / for l in lambda: 0: 184 | | f.write(f"[{l}]") | |_____________________________^ - | help: Replace with `f.writelines` | 182 | with Path("file.txt").open("w", encoding="utf-8") as f: @@ -294,7 +281,6 @@ FURB122 [*] Use of `f.write` in a for loop 189 | / for l in (1,) if True else (2,): 190 | | f.write(f"[{l}]") | |_____________________________^ - | help: Replace with `f.writelines` | 188 | with Path("file.txt").open("w", encoding="utf-8") as f: @@ -312,7 +298,6 @@ FURB122 [*] Use of `f.write` in a for loop 196 | / for line in lambda: 0: 197 | | f.write(line) | |_________________________^ - | help: Replace with `f.writelines` | 195 | with open("file", "w") as f: @@ -330,7 +315,6 @@ FURB122 [*] Use of `f.write` in a for loop 202 | / for line in (1,) if True else (2,): 203 | | f.write(line) | |_________________________^ - | help: Replace with `f.writelines` | 201 | with open("file", "w") as f: @@ -348,7 +332,6 @@ FURB122 [*] Use of `f.write` in a for loop 209 | / for line in (lambda: 0): 210 | | f.write(f"{line}") | |______________________________^ - | help: Replace with `f.writelines` | 208 | with open("file", "w") as f: @@ -366,7 +349,6 @@ FURB122 [*] Use of `f.write` in a for loop 215 | / for line in ((1,) if True else (2,)): 216 | | f.write(f"{line}") | |______________________________^ - | help: Replace with `f.writelines` | 214 | with open("file", "w") as f: diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB129_FURB129.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB129_FURB129.py.snap index 377dd38e57..8adcea8b87 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB129_FURB129.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB129_FURB129.py.snap @@ -229,7 +229,6 @@ FURB129 [*] Instead of calling `readlines()`, iterate over file object directly 96 | with open("furb129.py") as f: 97 | [line for line in (f).readlines()] | ^^^^^^^^^^^^^^^ - | help: Remove `readlines()` | 96 | with open("furb129.py") as f: diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB131_FURB131.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB131_FURB131.py.snap index 547da9b559..eed82175cc 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB131_FURB131.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB131_FURB131.py.snap @@ -7,7 +7,6 @@ FURB131 [*] Prefer `clear` over deleting a full slice 10 | # FURB131 11 | del nums[:] | ^^^^^^^^^^^ - | help: Replace with `clear()` | 10 | # FURB131 @@ -23,7 +22,6 @@ FURB131 [*] Prefer `clear` over deleting a full slice 14 | # FURB131 15 | del names[:] | ^^^^^^^^^^^^ - | help: Replace with `clear()` | 14 | # FURB131 @@ -39,7 +37,6 @@ FURB131 Prefer `clear` over deleting a full slice 18 | # FURB131 19 | del x, nums[:] | ^^^^^^^^^^^^^^ - | help: Replace with `clear()` FURB131 Prefer `clear` over deleting a full slice @@ -48,7 +45,6 @@ FURB131 Prefer `clear` over deleting a full slice 22 | # FURB131 23 | del y, names[:], x | ^^^^^^^^^^^^^^^^^^ - | help: Replace with `clear()` FURB131 [*] Prefer `clear` over deleting a full slice @@ -58,7 +54,6 @@ FURB131 [*] Prefer `clear` over deleting a full slice 27 | # FURB131 28 | del x[:] | ^^^^^^^^ - | help: Replace with `clear()` | 27 | # FURB131 @@ -75,7 +70,6 @@ FURB131 [*] Prefer `clear` over deleting a full slice 32 | # FURB131 33 | del x[:] | ^^^^^^^^ - | help: Replace with `clear()` | 32 | # FURB131 @@ -92,7 +86,6 @@ FURB131 [*] Prefer `clear` over deleting a full slice 37 | # FURB131 38 | del x[:] | ^^^^^^^^ - | help: Replace with `clear()` | 37 | # FURB131 @@ -109,7 +102,6 @@ FURB131 [*] Prefer `clear` over deleting a full slice 42 | # FURB131 43 | del x[:] | ^^^^^^^^ - | help: Replace with `clear()` | 42 | # FURB131 diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB132_FURB132.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB132_FURB132.py.snap index d58bfdb288..e7e76f38b7 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB132_FURB132.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB132_FURB132.py.snap @@ -8,7 +8,6 @@ FURB132 [*] Use `s.discard("x")` instead of check and `remove` 12 | / if "x" in s: 13 | | s.remove("x") | |_________________^ - | help: Replace with `s.discard("x")` | 11 | # FURB132 @@ -26,7 +25,6 @@ FURB132 [*] Use `s3.discard("x")` instead of check and `remove` 22 | / if "x" in s3: 23 | | s3.remove("x") | |__________________^ - | help: Replace with `s3.discard("x")` | 21 | # FURB132 @@ -45,7 +43,6 @@ FURB132 [*] Use `s.discard(var)` instead of check and `remove` 28 | / if var in s: 29 | | s.remove(var) | |_________________^ - | help: Replace with `s.discard(var)` | 27 | # FURB132 @@ -62,7 +59,6 @@ FURB132 [*] Use `s.discard(f"{var}:{var}")` instead of check and `remove` 32 | / if f"{var}:{var}" in s: 33 | | s.remove(f"{var}:{var}") | |____________________________^ - | help: Replace with `s.discard(f"{var}:{var}")` | 31 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB136_FURB136.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB136_FURB136.py.snap index 3cb5077164..64b885d173 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB136_FURB136.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB136_FURB136.py.snap @@ -155,7 +155,6 @@ FURB136 [*] Replace `if` expression with `max(y, x)` 24 | | > y 25 | | ) else y # FURB136 | |________^ - | help: Replace with `max(y, x)` | 21 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB140_FURB140.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB140_FURB140.py.snap index ad1625f575..85c6cb9a13 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB140_FURB140.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB140_FURB140.py.snap @@ -47,7 +47,6 @@ FURB140 [*] Use `itertools.starmap` instead of the generator 12 | # FURB140 13 | {print(x, y) for x, y in zipped()} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `itertools.starmap` | 1 + from itertools import starmap diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB142_FURB142.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB142_FURB142.py.snap index 8f8e6b03d1..3ed7c5780d 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB142_FURB142.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB142_FURB142.py.snap @@ -231,7 +231,6 @@ FURB142 [*] Use of `set.add()` in a for loop 44 | | ): 45 | | s.add(f"{x}") | |_________________^ - | help: Replace with `.update()` | 40 | @@ -390,7 +389,6 @@ FURB142 [*] Use of `set.add()` in a for loop 108 | / for x in ("abc", "def"): 109 | | s.add((c for c in x)) | |_________________________^ - | help: Replace with `.update()` | 107 | # don't add extra parens for already parenthesized generators diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB145_FURB145.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB145_FURB145.py.snap index a0e851b790..e0386246b4 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB145_FURB145.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB145_FURB145.py.snap @@ -116,7 +116,6 @@ FURB145 [*] Prefer `copy` method over slicing 26 | | : 27 | | ] | |_^ - | help: Replace with `copy()` | 23 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB152_FURB152.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB152_FURB152.py.snap index 2402ae2e96..e8285f3b89 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB152_FURB152.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB152_FURB152.py.snap @@ -302,7 +302,6 @@ FURB152 [*] Replace `2.7182000000000001` with `math.e` 44 | 45 | e = 2.7182000000000001 # FURB152 | ^^^^^^^^^^^^^^^^^^ - | help: Use `math.e` | 1 + import math diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB154_FURB154.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB154_FURB154.py.snap index d5c1416d79..61de91d468 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB154_FURB154.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB154_FURB154.py.snap @@ -8,7 +8,6 @@ FURB154 [*] Use of repeated consecutive `global` 4 | / global x 5 | | global y | |____________^ - | help: Merge `global` statements | 3 | def f1(): @@ -26,7 +25,6 @@ FURB154 [*] Use of repeated consecutive `global` 10 | | global y 11 | | global z | |____________^ - | help: Merge `global` statements | 8 | def f3(): @@ -64,7 +62,6 @@ FURB154 [*] Use of repeated consecutive `global` 18 | / global x 19 | | global y | |____________^ - | help: Merge `global` statements | 17 | pass @@ -141,7 +138,6 @@ FURB154 [*] Use of repeated consecutive `nonlocal` 38 | / nonlocal x 39 | | nonlocal y | |__________________^ - | help: Merge `nonlocal` statements | 37 | pass @@ -198,7 +194,6 @@ FURB154 [*] Use of repeated consecutive `nonlocal` 53 | / nonlocal y 54 | | nonlocal z | |__________________^ - | help: Merge `nonlocal` statements | 52 | global x @@ -216,7 +211,6 @@ FURB154 [*] Use of repeated consecutive `global` 59 | | global a, b, c 60 | | global d, e, f | |__________________^ - | help: Merge `global` statements | 57 | def f6(): diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap index 34b408d739..b23d38312d 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB157_FURB157.py.snap @@ -680,7 +680,6 @@ FURB157 [*] Verbose expression in `Decimal` constructor 92 | Decimal("_+1") # Should flag as verbose 93 | Decimal("_-1_000") # Should flag as verbose | ^^^^^^^^^ - | help: Replace with `-1_000` | 92 | Decimal("_+1") # Should flag as verbose diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB162_FURB162.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB162_FURB162.py.snap index eb507c8a99..4b040a8bb3 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB162_FURB162.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB162_FURB162.py.snap @@ -208,7 +208,6 @@ FURB162 [*] Unnecessary timezone replacement with zero offset 51 | # Edge case 52 | datetime.fromisoformat("Z2025-01-01T00:00:00Z".strip("Z") + "+00:00") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `.replace()` call | 51 | # Edge case diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB163_FURB163.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB163_FURB163.py.snap index 8c84dd51fe..05dc04cac2 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB163_FURB163.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB163_FURB163.py.snap @@ -156,7 +156,6 @@ FURB163 [*] Prefer `math.log(yield)` over `math.log` with a redundant base 48 | def log(): 49 | yield math.log((yield), math.e) | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `math.log(yield)` | 48 | def log(): @@ -301,7 +300,6 @@ FURB163 [*] Prefer `math.log10(4.14e223)` over `math.log` with a redundant base 74 | math.log(4.13e223, 2) 75 | math.log(4.14e223, 10) | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `math.log10(4.14e223)` | 74 | math.log(4.13e223, 2) diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap index 93d0384f59..12431abb9b 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB164_FURB164.py.snap @@ -699,7 +699,6 @@ FURB164 [*] Verbose method `from_float` in `Decimal` construction 81 | | float("inf") 82 | | ) | |_^ - | help: Replace with `Decimal` constructor | 78 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB166_FURB166.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB166_FURB166.py.snap index f87a780a89..b21c9ec5c1 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB166_FURB166.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB166_FURB166.py.snap @@ -100,7 +100,6 @@ FURB166 [*] Use of `int` with explicit `base=16` after removing prefix 11 | 12 | _ = int(b"0xFFFF"[2:], 16) | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `base=0` | 11 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB169_FURB169.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB169_FURB169.py.snap index 43cbe9f577..ba454f05e7 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB169_FURB169.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB169_FURB169.py.snap @@ -293,7 +293,6 @@ FURB169 [*] When checking against `None`, use `is not` instead of comparison wit 42 | | a for a in range(0) 43 | | ) is not type(None) | |___________________^ - | help: Replace with `is not None` | 40 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB187_FURB187.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB187_FURB187.py.snap index 662129dd06..24f1a54ecc 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB187_FURB187.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB187_FURB187.py.snap @@ -8,7 +8,6 @@ FURB187 [*] Use of assignment of `reversed` on list `l` 5 | l = [] 6 | l = reversed(l) | ^^^^^^^^^^^^^^^ - | help: Replace with `l.reverse()` | 5 | l = [] @@ -25,7 +24,6 @@ FURB187 [*] Use of assignment of `reversed` on list `l` 10 | l = [] 11 | l = list(reversed(l)) | ^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `l.reverse()` | 10 | l = [] @@ -42,7 +40,6 @@ FURB187 [*] Use of assignment of `reversed` on list `l` 15 | l = [] 16 | l = l[::-1] | ^^^^^^^^^^^ - | help: Replace with `l.reverse()` | 15 | l = [] diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB188_FURB188.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB188_FURB188.py.snap index c260010e79..67c26ad968 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB188_FURB188.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB188_FURB188.py.snap @@ -45,7 +45,6 @@ FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice. 20 | def remove_extension_via_ternary(filename: str) -> str: 21 | return filename[:-4] if filename.endswith(".txt") else filename | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use removesuffix instead of ternary expression conditional upon endswith. | 20 | def remove_extension_via_ternary(filename: str) -> str: @@ -60,7 +59,6 @@ FURB188 [*] Prefer `str.removesuffix()` over conditionally replacing with slice. 24 | def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str: 25 | return filename[:-len(extension)] if filename.endswith(extension) else filename | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use removesuffix instead of ternary expression conditional upon endswith. | 24 | def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str: @@ -75,7 +73,6 @@ FURB188 [*] Prefer `str.removeprefix()` over conditionally replacing with slice. 28 | def remove_prefix(filename: str) -> str: 29 | return filename[4:] if filename.startswith("abc-") else filename | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use removeprefix instead of ternary expression conditional upon startswith. | 28 | def remove_prefix(filename: str) -> str: @@ -90,7 +87,6 @@ FURB188 [*] Prefer `str.removeprefix()` over conditionally replacing with slice. 32 | def remove_prefix_via_len(filename: str, prefix: str) -> str: 33 | return filename[len(prefix):] if filename.startswith(prefix) else filename | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use removeprefix instead of ternary expression conditional upon startswith. | 32 | def remove_prefix_via_len(filename: str, prefix: str) -> str: @@ -219,7 +215,6 @@ FURB188 [*] Prefer `str.removeprefix()` over conditionally replacing with slice. 183 | / if text.startswith("ř"): 184 | | text = text[1:] | |_______________________^ - | help: Use removeprefix instead of assignment conditional upon startswith. | 182 | text = "řetězec" diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB192_FURB192_1.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB192_FURB192_1.py.snap index df3c7a5b99..5e5ec280ff 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB192_FURB192_1.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB192_FURB192_1.py.snap @@ -83,7 +83,6 @@ FURB192 [*] Prefer `min` over `sorted()` to compute the minimum value in a seque 16 | 17 | sorted((yield), reverse=True)[-1] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `min` | 16 | diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__fstring_number_format_python_311.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__fstring_number_format_python_311.snap index 25c70b7636..830f08dc4f 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__fstring_number_format_python_311.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__fstring_number_format_python_311.snap @@ -229,7 +229,6 @@ FURB116 [*] Replace `bin` call with `f"{-1:b}"` 43 | # for negatives numbers autofix is display-only 44 | print(bin(-1)[2:]) | ^^^^^^^^^^^ - | help: Replace with `f"{-1:b}"` | 43 | # for negatives numbers autofix is display-only diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF015_RUF015_py315.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF015_RUF015_py315.py.snap index dd82e47487..79c3527454 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF015_RUF015_py315.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF015_RUF015_py315.py.snap @@ -25,7 +25,6 @@ RUF015 [*] Prefer `next(*x for x in xs)` over single element slice 3 | [*x for x in xs][0] 4 | list(*x for x in xs)[0] | ^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `next(*x for x in xs)` | 3 | [*x for x in xs][0] diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF017_RUF017_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF017_RUF017_0.py.snap index 65191c8be0..0824980405 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF017_RUF017_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY315_RUF017_RUF017_0.py.snap @@ -104,7 +104,6 @@ RUF017 [*] Avoid quadratic list summation 20 | 21 | sum([x, y], []) | ^^^^^^^^^^^^^^^ - | help: Replace with a starred list comprehension | 20 | @@ -121,7 +120,6 @@ RUF017 [*] Avoid quadratic list summation 25 | def func(): 26 | sum((factor.dims for factor in bases), []) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with a starred list comprehension | 25 | def func(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF005_RUF005.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF005_RUF005.py.snap index bbe198bd82..f3c41721a4 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF005_RUF005.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF005_RUF005.py.snap @@ -42,7 +42,6 @@ RUF005 Consider `[*first, 4, 5, 6]` instead of concatenation 22 | | 6, 23 | | ] | |_^ - | help: Replace with `[*first, 4, 5, 6]` RUF005 [*] Consider `[1, 2, 3, *foo]` instead of concatenation diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF006_RUF006.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF006_RUF006.py.snap index 65efeca6a0..9fb29c3cc6 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF006_RUF006.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF006_RUF006.py.snap @@ -8,7 +8,6 @@ RUF006 Store a reference to the return value of `asyncio.create_task` 5 | def f(): 6 | asyncio.create_task(coordinator.ws_connect()) # Error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | RUF006 Store a reference to the return value of `asyncio.ensure_future` --> RUF006.py:11:5 @@ -17,7 +16,6 @@ RUF006 Store a reference to the return value of `asyncio.ensure_future` 10 | def f(): 11 | asyncio.ensure_future(coordinator.ws_connect()) # Error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | RUF006 Store a reference to the return value of `asyncio.create_task` --> RUF006.py:68:12 @@ -26,7 +24,6 @@ RUF006 Store a reference to the return value of `asyncio.create_task` 67 | def f(): 68 | task = asyncio.create_task(coordinator.ws_connect()) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | RUF006 Store a reference to the return value of `loop.create_task` --> RUF006.py:74:26 @@ -35,7 +32,6 @@ RUF006 Store a reference to the return value of `loop.create_task` 73 | loop = asyncio.get_running_loop() 74 | task: asyncio.Task = loop.create_task(coordinator.ws_connect()) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | RUF006 Store a reference to the return value of `loop.create_task` --> RUF006.py:97:5 @@ -44,7 +40,6 @@ RUF006 Store a reference to the return value of `loop.create_task` 96 | loop = asyncio.get_running_loop() 97 | loop.create_task(coordinator.ws_connect()) # Error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | RUF006 Store a reference to the return value of `asyncio.create_task` --> RUF006.py:152:13 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF007_RUF007.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF007_RUF007.py.snap index 02a04f3566..df6aeed6fe 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF007_RUF007.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF007_RUF007.py.snap @@ -204,7 +204,6 @@ RUF007 [*] Prefer `itertools.pairwise()` over `zip()` when iterating over succes 24 | zip(foo[:-1], foo[1:], strict=False) 25 | zip(foo[:-1], foo[1:], strict=bool(foo)) | ^^^ - | help: Replace `zip()` with `itertools.pairwise()` | 1 + import itertools diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF008_RUF008.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF008_RUF008.py.snap index 790e5ff0d5..4289a1e031 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF008_RUF008.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF008_RUF008.py.snap @@ -52,4 +52,3 @@ RUF008 Do not use mutable default values for dataclass attributes 35 | perfectly_fine: 'list[int]' = field(default_factory=list) 36 | class_variable: 'typing.ClassVar[list[int]]'= [] | ^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF009_RUF009_attrs.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF009_RUF009_attrs.py.snap index c50c1d506a..2e6fb79933 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF009_RUF009_attrs.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF009_RUF009_attrs.py.snap @@ -51,7 +51,6 @@ RUF009 Do not perform function call `G` in dataclass defaults 108 | f: F = F() 109 | g: G = G() | ^^^ - | RUF009 Do not perform function call `F` in dataclass defaults --> RUF009_attrs.py:114:12 @@ -70,7 +69,6 @@ RUF009 Do not perform function call `G` in dataclass defaults 114 | f: F = F() 115 | g: G = G() | ^^^ - | RUF009 Do not perform function call `F` in dataclass defaults --> RUF009_attrs.py:120:12 @@ -89,7 +87,6 @@ RUF009 Do not perform function call `G` in dataclass defaults 120 | f: F = F() 121 | g: G = G() | ^^^ - | RUF009 Do not perform function call `F` in dataclass defaults --> RUF009_attrs.py:126:12 @@ -108,7 +105,6 @@ RUF009 Do not perform function call `G` in dataclass defaults 126 | f: F = F() 127 | g: G = G() | ^^^ - | RUF009 Do not perform function call `list` in dataclass defaults --> RUF009_attrs.py:144:20 @@ -117,4 +113,3 @@ RUF009 Do not perform function call `list` in dataclass defaults 143 | class TestAttrAttributes: 144 | x: list[int] = list() # RUF009 | ^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF010_RUF010.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF010_RUF010.py.snap index d65283c1b6..a2064e5cc9 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF010_RUF010.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF010_RUF010.py.snap @@ -619,7 +619,6 @@ RUF010 [*] Use explicit conversion flag 121 | | 1 122 | | ))}" | |__^ - | help: Replace with conversion flag | 118 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF012_RUF012.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF012_RUF012.py.snap index 8d0a78445e..5cc2fd8c41 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF012_RUF012.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF012_RUF012.py.snap @@ -126,7 +126,6 @@ RUF012 Mutable default value for class attribute 133 | class_variable_without_subscript: 'ClassVar' = [] 134 | final_variable_without_subscript: 'Final' = [] | ^^ - | help: Consider initializing in `__init__` or annotating with `typing.ClassVar` RUF012 Mutable default value for class attribute diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF013_RUF013_4.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF013_RUF013_4.py.snap index ce92f8a321..745e2f89b2 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF013_RUF013_4.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF013_RUF013_4.py.snap @@ -6,7 +6,6 @@ RUF013 [*] PEP 484 prohibits implicit `Optional` | 15 | def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int = None): ... | ^^^ - | help: Convert to `T | None` | 14 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF015_RUF015.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF015_RUF015.py.snap index b95b8c76ee..87d7c0c429 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF015_RUF015.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF015_RUF015.py.snap @@ -430,7 +430,6 @@ RUF015 [*] Prefer `next(iter(zip(x, y)))` over single element slice 72 | zip = list # Overwrite the builtin zip 73 | list(zip(x, y))[0] | ^^^^^^^^^^^^^^^^^^ - | help: Replace with `next(iter(zip(x, y)))` | 72 | zip = list # Overwrite the builtin zip diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF016_RUF016.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF016_RUF016.py.snap index 089bac11e0..1f37eb3b4c 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF016_RUF016.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF016_RUF016.py.snap @@ -453,4 +453,3 @@ RUF016 Slice in indexed access to type `list` uses type `str` instead of an inte 133 | x = "x" 134 | var = [1, 2, 3][x:"y"] | ^^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_0.py.snap index 2bcd9ba8f8..601c205bbd 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_0.py.snap @@ -125,7 +125,6 @@ RUF017 [*] Avoid quadratic list summation 20 | 21 | sum([x, y], []) | ^^^^^^^^^^^^^^^ - | help: Replace with `functools.reduce` | 20 | @@ -142,7 +141,6 @@ RUF017 [*] Avoid quadratic list summation 25 | def func(): 26 | sum((factor.dims for factor in bases), []) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `functools.reduce` | 1 + import functools diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_1.py.snap index 66860484aa..28a251a13f 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_1.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF017_RUF017_1.py.snap @@ -6,7 +6,6 @@ RUF017 [*] Avoid quadratic list summation | 1 | sum((factor.dims for factor in bases), []) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `functools.reduce` | - sum((factor.dims for factor in bases), []) diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF020_RUF020.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF020_RUF020.py.snap index d247be7190..afecdbe964 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF020_RUF020.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF020_RUF020.py.snap @@ -113,7 +113,6 @@ RUF020 [*] `Union[NoReturn, T]` is equivalent to `T` 7 | Union[Union[Never, int], Union[NoReturn, int]] 8 | Union[NoReturn, int, float] | ^^^^^^^^ - | help: Remove `NoReturn` | 7 | Union[Union[Never, int], Union[NoReturn, int]] @@ -173,7 +172,6 @@ RUF020 `Never | T` is equivalent to `T` 16 | a: int | Never | None 17 | b: Never | Never | None | ^^^^^ - | help: Remove `Never` RUF020 `Never | T` is equivalent to `T` @@ -182,7 +180,6 @@ RUF020 `Never | T` is equivalent to `T` 16 | a: int | Never | None 17 | b: Never | Never | None | ^^^^^ - | help: Remove `Never` RUF020 [*] `Never | T` is equivalent to `T` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF023_RUF023.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF023_RUF023.py.snap index b9b9da498e..e798afe925 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF023_RUF023.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF023_RUF023.py.snap @@ -621,7 +621,6 @@ RUF023 [*] `BezierBuilder4.__slots__` is not sorted 192 | | "baz", "bingo" 193 | | } | |__________________^ - | help: Apply a natural sort to `BezierBuilder4.__slots__` | 190 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF024_RUF024.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF024_RUF024.py.snap index ab2924d139..8b12e431e1 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF024_RUF024.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF024_RUF024.py.snap @@ -140,7 +140,6 @@ RUF024 [*] Do not pass mutable objects as values to `dict.fromkeys` 38 | key_0 = "z" 39 | dict.fromkeys("ABC", list(key)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with comprehension | 38 | key_0 = "z" diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF026_RUF026.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF026_RUF026.py.snap index e8cc588cfa..b9a000ba51 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF026_RUF026.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF026_RUF026.py.snap @@ -7,7 +7,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 10 | def func(): 11 | defaultdict(default_factory=int) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=int)` | 10 | def func(): @@ -23,7 +22,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 14 | def func(): 15 | defaultdict(default_factory=float) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=float)` | 14 | def func(): @@ -39,7 +37,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 18 | def func(): 19 | defaultdict(default_factory=dict) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=dict)` | 18 | def func(): @@ -55,7 +52,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 22 | def func(): 23 | defaultdict(default_factory=list) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=list)` | 22 | def func(): @@ -71,7 +67,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 26 | def func(): 27 | defaultdict(default_factory=tuple) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=tuple)` | 26 | def func(): @@ -88,7 +83,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 33 | 34 | defaultdict(default_factory=foo) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=foo)` | 33 | @@ -104,7 +98,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 37 | def func(): 38 | defaultdict(default_factory=lambda: 1) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=lambda: 1)` | 37 | def func(): @@ -121,7 +114,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 43 | 44 | defaultdict(default_factory=deque) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=deque)` | 43 | @@ -138,7 +130,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 51 | 52 | defaultdict(default_factory=MyCallable()) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=MyCallable())` | 51 | @@ -154,7 +145,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 55 | def func(): 56 | defaultdict(default_factory=tuple, member=1) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=tuple)` | 55 | def func(): @@ -170,7 +160,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 59 | def func(): 60 | defaultdict(member=1, default_factory=tuple) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=tuple)` | 59 | def func(): @@ -186,7 +175,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 63 | def func(): 64 | defaultdict(member=1, default_factory=tuple,) # RUF026 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `defaultdict(default_factory=tuple)` | 63 | def func(): @@ -205,7 +193,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 70 | | default_factory=tuple, 71 | | ) # RUF026 | |_____^ - | help: Replace with `defaultdict(default_factory=tuple)` | 68 | defaultdict( @@ -225,7 +212,6 @@ RUF026 [*] `default_factory` is a positional-only argument to `defaultdict` 77 | | member=1, 78 | | ) # RUF026 | |_____^ - | help: Replace with `defaultdict(default_factory=tuple)` | 75 | defaultdict( diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap index 3327181c22..5c3d0ab42b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF027_RUF027_0.py.snap @@ -8,7 +8,6 @@ RUF027 [*] Possible f-string without an `f` prefix 4 | 5 | print("but don't ignore this: {val}") # RUF027 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `f` prefix | 4 | @@ -43,7 +42,6 @@ RUF027 [*] Possible f-string without an `f` prefix 10 | b = "{a}" # RUF027 11 | c = "{a} {b} f'{val}' " # RUF027 | ^^^^^^^^^^^^^^^^^^^ - | help: Add `f` prefix | 10 | b = "{a}" # RUF027 @@ -78,7 +76,6 @@ RUF027 [*] Possible f-string without an `f` prefix 21 | b = r"raw string with formatting: {a}" # RUF027 22 | c = r"raw string with \backslashes\ and \"escaped quotes\": {a}" # RUF027 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `f` prefix | 21 | b = r"raw string with formatting: {a}" # RUF027 @@ -113,7 +110,6 @@ RUF027 [*] Possible f-string without an `f` prefix 27 | print("Hello, {name}!") # RUF027 28 | print("The test value we're using today is {a}") # RUF027 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `f` prefix | 27 | print("Hello, {name}!") # RUF027 @@ -130,7 +126,6 @@ RUF027 [*] Possible f-string without an `f` prefix 32 | a = 4 33 | print(do_nothing(do_nothing("{a}"))) # RUF027 | ^^^^^ - | help: Add `f` prefix | 32 | a = 4 @@ -169,7 +164,6 @@ RUF027 [*] Possible f-string without an `f` prefix 42 | | c} d 43 | | """ | |_______^ - | help: Add `f` prefix | 40 | # RUF027 @@ -189,7 +183,6 @@ RUF027 [*] Possible f-string without an `f` prefix 50 | | a} \ 51 | | " | |_____^ - | help: Add `f` prefix | 48 | # RUF027 @@ -224,7 +217,6 @@ RUF027 [*] Possible f-string without an `f` prefix 56 | b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only 57 | print(f"{a}" "{a}" f"{b}") # RUF027 | ^^^^^ - | help: Add `f` prefix | 56 | b = "{a}" "+" "{b}" r" \\ " # RUF027 for the first part only @@ -241,7 +233,6 @@ RUF027 [*] Possible f-string without an `f` prefix 61 | a = 4 62 | b = "\"not escaped:\" '{a}' \"escaped:\": '{{c}}'" # RUF027 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Add `f` prefix | 61 | a = 4 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF028_RUF028.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF028_RUF028.py.snap index ae94b664fd..f32c717ea7 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF028_RUF028.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF028_RUF028.py.snap @@ -185,7 +185,6 @@ RUF028 [*] This suppression comment is invalid because it cannot be at the end o 62 | val = 5 # fmt: on 63 | pass # fmt: on | ^^^^^^^^^ - | help: Remove this comment | 62 | val = 5 # fmt: on diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF030_RUF030.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF030_RUF030.py.snap index 41b9366605..5da2b5d855 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF030_RUF030.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF030_RUF030.py.snap @@ -350,7 +350,6 @@ RUF030 [*] `print()` call in `assert` statement is likely unintentional 107 | # - single StringLiteral 108 | assert True, builtins.print("This print should be removed.") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `print` | 107 | # - single StringLiteral diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF032_RUF032.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF032_RUF032.py.snap index 7efec46017..386dbed26c 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF032_RUF032.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF032_RUF032.py.snap @@ -141,7 +141,6 @@ RUF032 [*] `Decimal()` called with float literal argument 57 | 58 | val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")` | ^^^^^^^^^^^ - | help: Replace with string literal | 57 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap index c920f3c4a3..4581b195a4 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap @@ -8,7 +8,6 @@ RUF033 `__post_init__` method with argument defaults 18 | 19 | def __post_init__(self, bar = 11, baz = 11) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead RUF033 `__post_init__` method with argument defaults @@ -18,7 +17,6 @@ RUF033 `__post_init__` method with argument defaults 18 | 19 | def __post_init__(self, bar = 11, baz = 11) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead RUF033 [*] `__post_init__` method with argument defaults @@ -28,7 +26,6 @@ RUF033 [*] `__post_init__` method with argument defaults 24 | class Foo: 25 | def __post_init__(self, bar = 11, baz = 11) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead | 24 | class Foo: @@ -46,7 +43,6 @@ RUF033 [*] `__post_init__` method with argument defaults 24 | class Foo: 25 | def __post_init__(self, bar = 11, baz = 11) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead | 24 | class Foo: @@ -64,7 +60,6 @@ RUF033 [*] `__post_init__` method with argument defaults 45 | class Foo: 46 | def __post_init__(self, bar: int = 11, baz: Something[Whatever | None] = 11) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead | 45 | class Foo: @@ -82,7 +77,6 @@ RUF033 [*] `__post_init__` method with argument defaults 45 | class Foo: 46 | def __post_init__(self, bar: int = 11, baz: Something[Whatever | None] = 11) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead | 45 | class Foo: @@ -100,7 +94,6 @@ RUF033 [*] `__post_init__` method with argument defaults 58 | 59 | def __post_init__(self, bar: int = 11, baz: int = 12) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead | 58 | @@ -118,7 +111,6 @@ RUF033 [*] `__post_init__` method with argument defaults 58 | 59 | def __post_init__(self, bar: int = 11, baz: int = 12) -> None: ... | ^^ - | help: Use `dataclasses.InitVar` instead | 58 | @@ -136,7 +128,6 @@ RUF033 `__post_init__` method with argument defaults 66 | 67 | def __post_init__(self, bar: str = "ahhh", baz: str = "hmm") -> None: ... | ^^^^^^ - | help: Use `dataclasses.InitVar` instead RUF033 `__post_init__` method with argument defaults @@ -146,7 +137,6 @@ RUF033 `__post_init__` method with argument defaults 66 | 67 | def __post_init__(self, bar: str = "ahhh", baz: str = "hmm") -> None: ... | ^^^^^ - | help: Use `dataclasses.InitVar` instead RUF033 [*] `__post_init__` method with argument defaults diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF034_RUF034.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF034_RUF034.py.snap index 1ab0762fc7..5557b6c13a 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF034_RUF034.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF034_RUF034.py.snap @@ -27,4 +27,3 @@ RUF034 Useless `if`-`else` condition 10 | # Invalid 11 | x = 0.1 if False else 0.1 | ^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap index fff7cd66a2..a5b357425c 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF037_RUF037.py.snap @@ -7,7 +7,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 5 | def f(): 6 | queue = collections.deque([]) # RUF037 | ^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `deque()` | 5 | def f(): @@ -22,7 +21,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 9 | def f(): 10 | queue = collections.deque([], maxlen=10) # RUF037 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `deque(maxlen=...)` | 9 | def f(): @@ -37,7 +35,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 13 | def f(): 14 | queue = deque([]) # RUF037 | ^^^^^^^^^ - | help: Replace with `deque()` | 13 | def f(): @@ -52,7 +49,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 17 | def f(): 18 | queue = deque(()) # RUF037 | ^^^^^^^^^ - | help: Replace with `deque()` | 17 | def f(): @@ -67,7 +63,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 21 | def f(): 22 | queue = deque({}) # RUF037 | ^^^^^^^^^ - | help: Replace with `deque()` | 21 | def f(): @@ -82,7 +77,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 25 | def f(): 26 | queue = deque(set()) # RUF037 | ^^^^^^^^^^^^ - | help: Replace with `deque()` | 25 | def f(): @@ -97,7 +91,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 29 | def f(): 30 | queue = collections.deque([], maxlen=10) # RUF037 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `deque(maxlen=...)` | 29 | def f(): @@ -112,7 +105,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 60 | def f(): 61 | x = 0 or(deque)([]) | ^^^^^^^^^^^ - | help: Replace with `deque()` | 60 | def f(): @@ -158,7 +150,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 67 | deque([], **{"maxlen": 10}) # RUF037 68 | deque([], foo=1) # RUF037 | ^^^^^^^^^^^^^^^^ - | help: Replace with `deque()` | 67 | deque([], **{"maxlen": 10}) # RUF037 @@ -179,7 +170,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 83 | | maxlen=10, # a comment on maxlen, deleted 84 | | ) # only this is preserved | |_________^ - | help: Replace with `deque(maxlen=...)` | 79 | def f(): @@ -200,7 +190,6 @@ RUF037 [*] Unnecessary empty iterable within a deque call 88 | def f(): 89 | deque([], 10) | ^^^^^^^^^^^^^ - | help: Replace with `deque(maxlen=...)` | 88 | def f(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF043_RUF043.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF043_RUF043.py.snap index f849446a0b..52f7c82d14 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF043_RUF043.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF043_RUF043.py.snap @@ -112,7 +112,6 @@ RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped 21 | # https://github.com/astral-sh/ruff/issues/15316 22 | with pytest.raises(ClosingParenthesis, match="foo)"): ... | ^^^^^^ - | help: Use a raw string or `re.escape()` to make the intention explicit RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw @@ -346,7 +345,6 @@ RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped 46 | with pytest.raises(NonWordCharacter2, match="foobar\\W"): ... 47 | with pytest.raises(EndOfInput2, match="foobar\\z"): ... | ^^^^^^^^^^^ - | help: Use a raw string or `re.escape()` to make the intention explicit RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw @@ -356,5 +354,4 @@ RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped 51 | 52 | with pytest.raises(NameEscape, match="\\N{EN DASH}"): ... | ^^^^^^^^^^^^^^ - | help: Use a raw string or `re.escape()` to make the intention explicit diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046.py.snap index 595a5ccc9f..cd45cd92f8 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF046_RUF046.py.snap @@ -622,7 +622,6 @@ RUF046 [*] Value being cast to `int` is already an integer 52 | int(1 and 0) 53 | int(0 or -1) | ^^^^^^^^^^^^ - | help: Remove unnecessary `int` call | 52 | int(1 and 0) @@ -833,7 +832,6 @@ RUF046 [*] Value being cast to `int` is already an integer 75 | int(round(unknown)) 76 | int(round(unknown, None)) | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unnecessary `int` call | 75 | int(round(unknown)) @@ -1081,7 +1079,6 @@ RUF046 [*] Value being cast to `int` is already an integer 207 | | # unsafe fix because of this comment 208 | | ) | |_^ - | help: Remove unnecessary `int` call | 202 | 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 index 4c9542624c..1b56a758d3 100644 --- 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 @@ -7,7 +7,6 @@ RUF046 [*] Value being cast to `int` is already an integer 1 | / int(- 2 | | 1) # Carriage return as newline | |______^ - | help: Remove unnecessary `int` call | - int(- 1 + (- 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 index 6c8e85a1ba..fc945a5b78 100644 --- 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 @@ -8,7 +8,6 @@ RUF046 [*] Value being cast to `int` is already an integer 2 | / int(- 3 | | 1) | |______^ - | help: Remove unnecessary `int` call | 1 | # \n as newline diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_for.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_for.py.snap index 7dba401011..8d32c11f2f 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_for.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_for.py.snap @@ -9,7 +9,6 @@ RUF047 [*] Empty `else` clause 6 | / else: 7 | | pass | |________^ - | help: Remove the `else` clause | 5 | break @@ -26,7 +25,6 @@ RUF047 [*] Empty `else` clause 12 | / else: 13 | | ... | |_______^ - | help: Remove the `else` clause | 11 | belongs_to() # `for` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_if.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_if.py.snap index 170cc86584..54b74d7ff3 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_if.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_if.py.snap @@ -9,7 +9,6 @@ RUF047 [*] Empty `else` clause 5 | / else: 6 | | pass | |________^ - | help: Remove the `else` clause | 4 | condition_is_not_evaluated() @@ -26,7 +25,6 @@ RUF047 [*] Empty `else` clause 11 | / else: 12 | | ... | |_______^ - | help: Remove the `else` clause | 10 | belongs_to() # `if` @@ -43,7 +41,6 @@ RUF047 [*] Empty `else` clause 19 | / else: 20 | | pass | |________^ - | help: Remove the `else` clause | 18 | as_if() @@ -79,7 +76,6 @@ RUF047 [*] Empty `else` clause 32 | / else: 33 | | pass | |________^ - | help: Remove the `else` clause | 31 | # `if` @@ -94,7 +90,6 @@ RUF047 [*] Empty `else` clause 43 | if of_course: this() 44 | else: ... | ^^^^^^^^^ - | help: Remove the `else` clause | 43 | if of_course: this() @@ -109,7 +104,6 @@ RUF047 [*] Empty `else` clause 48 | this() # comment 49 | else: ... | ^^^^^^^^^ - | help: Remove the `else` clause | 48 | this() # comment @@ -125,7 +119,6 @@ RUF047 [*] Empty `else` clause 55 | / else: 56 | | ... | |___________^ - | help: Remove the `else` clause | 54 | b() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_try.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_try.py.snap index 6a61b7c63a..8e40273a77 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_try.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_try.py.snap @@ -9,7 +9,6 @@ RUF047 [*] Empty `else` clause 7 | / else: 8 | | pass | |________^ - | help: Remove the `else` clause | 6 | pass @@ -26,7 +25,6 @@ RUF047 [*] Empty `else` clause 17 | / else: 18 | | ... | |_______^ - | help: Remove the `else` clause | 16 | to() # `except` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_while.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_while.py.snap index 58f2ea145c..e219f2ae89 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_while.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF047_RUF047_while.py.snap @@ -9,7 +9,6 @@ RUF047 [*] Empty `else` clause 6 | / else: 7 | | pass | |________^ - | help: Remove the `else` clause | 5 | break @@ -26,7 +25,6 @@ RUF047 [*] Empty `else` clause 12 | / else: 13 | | ... | |_______^ - | help: Remove the `else` clause | 11 | belongs_to() # `for` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF050_RUF050.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF050_RUF050.py.snap index 8ef5ca160a..e36bfb7344 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF050_RUF050.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF050_RUF050.py.snap @@ -174,7 +174,6 @@ RUF050 [*] Empty `if` statement 43 | / if obj1: 44 | | pass | |____________^ - | help: Remove the `if` statement | 42 | with pytest.raises(ValueError, match=msg): @@ -339,7 +338,6 @@ RUF050 [*] Empty `if` statement 85 | / if foo(): 86 | | pass | |____________^ - | help: Remove the `if` statement | 84 | class Foo: diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF051_RUF051.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF051_RUF051.py.snap index 2badb4f691..257e61a09e 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF051_RUF051.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF051_RUF051.py.snap @@ -514,7 +514,6 @@ RUF051 [*] Use `pop` instead of `key in dict` followed by `del dict[key]` 98 | if b'yt' b'es' in d: 99 | del d[rb"""ytes"""] # This should not make the fix unsafe | ^^^^^^^^^^^^^^^^^^^ - | help: Replace `if` statement with `.pop(..., None)` | 97 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF053_RUF053.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF053_RUF053.py.snap index 8ee126b30c..ec5fcd2256 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF053_RUF053.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF053_RUF053.py.snap @@ -147,7 +147,6 @@ RUF053 Class with type parameter list inherits from `Generic` 32 | class C[*Ts](Generic[Unpack[_Bs]], tuple[*Bs]): ... 33 | class C[*Ts](Callable[[*_Cs], tuple[*Ts]], Generic[_Cs]): ... # TODO: Type parameter defaults | ^^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 [*] Class with type parameter list inherits from `Generic` @@ -184,7 +183,6 @@ RUF053 Class with type parameter list inherits from `Generic` 37 | class C[**P](Generic[_P2]): ... 38 | class C[**P](Generic[_P3]): ... # TODO: Type parameter defaults | ^^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 [*] Class with type parameter list inherits from `Generic` @@ -192,7 +190,6 @@ RUF053 [*] Class with type parameter list inherits from `Generic` | 41 | class C[T](Generic[T, _A]): ... | ^^^^^^^^^^^^^^ - | help: Remove `Generic` base class | 40 | @@ -209,7 +206,6 @@ RUF053 Class with type parameter list inherits from `Generic` 46 | # only simple assignments, so there is no fix. 47 | class C[T: (_Z := TypeVar('_Z'))](Generic[_Z]): ... | ^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 [*] Class with type parameter list inherits from `Generic` @@ -218,7 +214,6 @@ RUF053 [*] Class with type parameter list inherits from `Generic` 50 | class C(Generic[_B]): 51 | class D[T](Generic[_B, T]): ... | ^^^^^^^^^^^^^^ - | help: Remove `Generic` base class | 50 | class C(Generic[_B]): @@ -234,7 +229,6 @@ RUF053 Class with type parameter list inherits from `Generic` 54 | class C[T]: 55 | class D[U](Generic[T, U]): ... | ^^^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 [*] Class with type parameter list inherits from `Generic` @@ -262,7 +256,6 @@ RUF053 Class with type parameter list inherits from `Generic` 60 | class C[T](Generic[_C], Generic[_D]): ... 61 | class C[T, _C: (str, bytes)](Generic[_D]): ... # TODO: Type parameter defaults | ^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 Class with type parameter list inherits from `Generic` @@ -272,7 +265,6 @@ RUF053 Class with type parameter list inherits from `Generic` 65 | T # Comment 66 | ](Generic[_E]): ... # TODO: Type parameter defaults | ^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 Class with type parameter list inherits from `Generic` @@ -338,7 +330,6 @@ RUF053 Class with type parameter list inherits from `Generic` 73 | class C[T](Generic[Unpack[*_As]]): ... 74 | class C[T](Generic[Unpack[_As, _Bs]]): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 [*] Class with type parameter list inherits from `Generic` @@ -382,7 +373,6 @@ RUF053 [*] Class with type parameter list inherits from `Generic` 78 | class C[T](Generic[_A, Unpack[_As]]): ... 79 | class C[T](Generic[*_As, _A]): ... | ^^^^^^^^^^^^^^^^^ - | help: Remove `Generic` base class | 78 | class C[T](Generic[_A, Unpack[_As]]): ... @@ -409,7 +399,6 @@ RUF053 Class with type parameter list inherits from `Generic` 83 | class C[T](Generic[APublicTypeVar]): ... 84 | class C[T](Generic[APublicTypeVar, _A]): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove `Generic` base class RUF053 [*] Class with type parameter list inherits from `Generic` @@ -419,7 +408,6 @@ RUF053 [*] Class with type parameter list inherits from `Generic` 90 | # See also the `_Z` example above. 91 | class C[T](Generic[_G]): ... # Should be moved down below eventually | ^^^^^^^^^^^ - | help: Remove `Generic` base class | 90 | # See also the `_Z` example above. @@ -453,7 +441,6 @@ RUF053 [*] Class with type parameter list inherits from `Generic` 95 | class C[T: (str,)](Generic[_A]): ... 96 | class C[T: [a]](Generic[_A]): ... | ^^^^^^^^^^^ - | help: Remove `Generic` base class | 95 | class C[T: (str,)](Generic[_A]): ... diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF056_RUF056.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF056_RUF056.py.snap index 2fa91d5f49..e5b79cbc85 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF056_RUF056.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF056_RUF056.py.snap @@ -438,7 +438,6 @@ RUF056 [*] Avoid providing a falsy fallback to `dict.get()` in boolean test posi 190 | d = {} 191 | not d.get("key", (False)) | ^^^^^ - | help: Remove falsy fallback from `dict.get()` | 190 | d = {} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF058_RUF058_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF058_RUF058_0.py.snap index 89bb8efba8..ef85a191e3 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF058_RUF058_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF058_RUF058_0.py.snap @@ -24,7 +24,6 @@ RUF058 [*] `itertools.starmap` called on `zip` iterable 7 | starmap(func, zip()) 8 | starmap(func, zip([])) | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `map` instead | 7 | starmap(func, zip()) @@ -38,7 +37,6 @@ RUF058 [*] `itertools.starmap` called on `zip` iterable | 11 | starmap(func, zip(a, b, c,),) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `map` instead | 10 | @@ -183,7 +181,6 @@ RUF058 [*] `itertools.starmap` called on `zip` iterable 59 | | ) 60 | | ) | |_^ - | help: Use `map` instead | 50 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_0.py.snap index 89691e783e..94b7f281df 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_0.py.snap @@ -44,7 +44,6 @@ RUF059 [*] Unpacked variable `x` is never used 25 | 26 | (x, y) = baz = bar | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 25 | @@ -61,7 +60,6 @@ RUF059 [*] Unpacked variable `y` is never used 25 | 26 | (x, y) = baz = bar | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 25 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_1.py.snap index cd37d69f41..4878184931 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_1.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_1.py.snap @@ -7,7 +7,6 @@ RUF059 [*] Unpacked variable `x` is never used 1 | def f(tup): 2 | x, y = tup | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 1 | def f(tup): @@ -23,7 +22,6 @@ RUF059 [*] Unpacked variable `y` is never used 1 | def f(tup): 2 | x, y = tup | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 1 | def f(tup): @@ -57,7 +55,6 @@ RUF059 [*] Unpacked variable `x` is never used 15 | def f(): 16 | (x, y) = coords = 1, 2 | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 15 | def f(): @@ -73,7 +70,6 @@ RUF059 [*] Unpacked variable `y` is never used 15 | def f(): 16 | (x, y) = coords = 1, 2 | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 15 | def f(): @@ -89,7 +85,6 @@ RUF059 [*] Unpacked variable `x` is never used 19 | def f(): 20 | coords = (x, y) = 1, 2 | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 19 | def f(): @@ -105,7 +100,6 @@ RUF059 [*] Unpacked variable `y` is never used 19 | def f(): 20 | coords = (x, y) = 1, 2 | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 19 | def f(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_2.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_2.py.snap index b8533e337a..02a790a6f2 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_2.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_2.py.snap @@ -80,7 +80,6 @@ RUF059 [*] Unpacked variable `x3` is never used 17 | (x2, y2) = coords2 = (1, 2) 18 | coords3 = (x3, y3) = (1, 2) | ^^ - | help: Prefix it with an underscore or any other dummy variable pattern | 17 | (x2, y2) = coords2 = (1, 2) @@ -97,7 +96,6 @@ RUF059 [*] Unpacked variable `y3` is never used 17 | (x2, y2) = coords2 = (1, 2) 18 | coords3 = (x3, y3) = (1, 2) | ^^ - | help: Prefix it with an underscore or any other dummy variable pattern | 17 | (x2, y2) = coords2 = (1, 2) @@ -147,7 +145,6 @@ RUF059 [*] Unpacked variable `a` is never used 26 | def f(): 27 | toplevel = (a, b) = lexer.get_token() | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 26 | def f(): @@ -163,7 +160,6 @@ RUF059 [*] Unpacked variable `b` is never used 26 | def f(): 27 | toplevel = (a, b) = lexer.get_token() | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 26 | def f(): @@ -179,7 +175,6 @@ RUF059 [*] Unpacked variable `a` is never used 30 | def f(): 31 | (a, b) = toplevel = lexer.get_token() | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 30 | def f(): @@ -194,7 +189,6 @@ RUF059 [*] Unpacked variable `b` is never used 30 | def f(): 31 | (a, b) = toplevel = lexer.get_token() | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 30 | def f(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_3.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_3.py.snap index aceb3e4c67..9897ceafd1 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_3.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF059_RUF059_3.py.snap @@ -8,7 +8,6 @@ RUF059 [*] Unpacked variable `b` is never used 12 | a = foo() 13 | b, c = foo() | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 12 | a = foo() @@ -25,7 +24,6 @@ RUF059 [*] Unpacked variable `c` is never used 12 | a = foo() 13 | b, c = foo() | ^ - | help: Prefix it with an underscore or any other dummy variable pattern | 12 | a = foo() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_deprecated_call.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_deprecated_call.py.snap index da3217bed8..e55888ceaf 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_deprecated_call.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_deprecated_call.py.snap @@ -7,7 +7,6 @@ RUF061 [*] Use context-manager form of `pytest.deprecated_call()` 15 | def test_error_trivial(): 16 | pytest.deprecated_call(raise_deprecation_warning, "deprecated") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.deprecated_call()` as a context-manager | 15 | def test_error_trivial(): @@ -42,7 +41,6 @@ RUF061 [*] Use context-manager form of `pytest.deprecated_call()` 24 | def test_error_lambda(): 25 | pytest.deprecated_call(lambda: warnings.warn("", DeprecationWarning)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.deprecated_call()` as a context-manager | 24 | def test_error_lambda(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_raises.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_raises.py.snap index 269443d770..64e072723c 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_raises.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_raises.py.snap @@ -7,7 +7,6 @@ RUF061 [*] Use context-manager form of `pytest.raises()` 18 | def test_error_trivial(): 19 | pytest.raises(ZeroDivisionError, func, 1, b=0) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.raises()` as a context-manager | 18 | def test_error_trivial(): @@ -24,7 +23,6 @@ RUF061 [*] Use context-manager form of `pytest.raises()` 22 | def test_error_match(): 23 | pytest.raises(ZeroDivisionError, func, 1, b=0).match("division by zero") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.raises()` as a context-manager | 22 | def test_error_match(): @@ -41,7 +39,6 @@ RUF061 [*] Use context-manager form of `pytest.raises()` 26 | def test_error_assign(): 27 | excinfo = pytest.raises(ZeroDivisionError, func, 1, b=0) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.raises()` as a context-manager | 26 | def test_error_assign(): @@ -58,7 +55,6 @@ RUF061 [*] Use context-manager form of `pytest.raises()` 30 | def test_error_kwargs(): 31 | pytest.raises(func=func, expected_exception=ZeroDivisionError) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.raises()` as a context-manager | 30 | def test_error_kwargs(): @@ -93,7 +89,6 @@ RUF061 [*] Use context-manager form of `pytest.raises()` 39 | def test_error_lambda(): 40 | pytest.raises(ZeroDivisionError, lambda: 1 / 0) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.raises()` as a context-manager | 39 | def test_error_lambda(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_warns.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_warns.py.snap index 41b1112254..36dbf41a5f 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_warns.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_warns.py.snap @@ -7,7 +7,6 @@ RUF061 [*] Use context-manager form of `pytest.warns()` 15 | def test_error_trivial(): 16 | pytest.warns(UserWarning, raise_user_warning, "warning") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.warns()` as a context-manager | 15 | def test_error_trivial(): @@ -42,7 +41,6 @@ RUF061 [*] Use context-manager form of `pytest.warns()` 24 | def test_error_lambda(): 25 | pytest.warns(UserWarning, lambda: warnings.warn("", UserWarning)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `pytest.warns()` as a context-manager | 24 | def test_error_lambda(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_0.py.snap index 9ac438216a..1d36fd3102 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_0.py.snap @@ -158,7 +158,6 @@ RUF065 Unnecessary `hex()` conversion when formatting with `%s`. Use `%#x` inste 56 | logging.info("Hex: %s", hex(42)) 57 | logging.warning("Hex: %s", hex(255)) | ^^^^^^^^ - | RUF065 Unnecessary `ascii()` conversion when formatting with `%s`. Use `%a` instead of `%s` --> RUF065_0.py:63:19 @@ -216,4 +215,3 @@ RUF065 Unnecessary `hex()` conversion when formatting with `%s`. Use `%#x` inste 69 | info("Hex: %s", hex(42)) 70 | log(logging.INFO, "Hex: %s", hex(255)) | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_1.py.snap index 56fa9ec243..a6d84c6e30 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_1.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF065_RUF065_1.py.snap @@ -7,4 +7,3 @@ RUF065 Unnecessary `str()` conversion when formatting with `%s` 16 | # str() with single keyword argument - should be flagged (equivalent to str("!")) 17 | logging.warning("%s", str(object="!")) | ^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF067_RUF067__modules____init__.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF067_RUF067__modules____init__.py.snap index e0318db251..00372ae7fa 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF067_RUF067__modules____init__.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF067_RUF067__modules____init__.py.snap @@ -18,7 +18,6 @@ RUF067 `__init__` module should only contain docstrings and re-exports 14 | 15 | os.environ["FOO"] = 1 | ^^^^^^^^^^^^^^^^^^^^^ - | RUF067 `__init__` module should only contain docstrings and re-exports --> __init__.py:18:1 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF068_RUF068.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF068_RUF068.py.snap index 8858058a32..a93caed1c4 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF068_RUF068.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF068_RUF068.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/ruff/mod.rs --- RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:15:15 + --> RUF068.py:15:20 | 13 | __all__: typing.Any = ("A", "B") 14 | __all__ = ["A", "B"] @@ -22,7 +22,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:20:23 + --> RUF068.py:20:33 | 19 | # Bad 20 | __all__: list[str] = ["A", "B", "A"] @@ -41,7 +41,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:21:29 + --> RUF068.py:21:34 | 19 | # Bad 20 | __all__: list[str] = ["A", "B", "A"] @@ -61,7 +61,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:22:12 + --> RUF068.py:22:22 | 20 | __all__: list[str] = ["A", "B", "A"] 21 | __all__: typing.Any = ("A", "B", "B") @@ -81,7 +81,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:23:12 + --> RUF068.py:23:17 | 21 | __all__: typing.Any = ("A", "B", "B") 22 | __all__ = ["A", "B", "A"] @@ -101,7 +101,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:23:22 + --> RUF068.py:23:27 | 21 | __all__: typing.Any = ("A", "B", "B") 22 | __all__ = ["A", "B", "A"] @@ -121,7 +121,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:25:5 + --> RUF068.py:26:5 | 23 | __all__ = ["A", "A", "B", "B"] 24 | __all__ = [ @@ -140,7 +140,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:27:5 + --> RUF068.py:28:5 | 25 | "A", 26 | "A", @@ -159,7 +159,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:30:13 + --> RUF068.py:30:18 | 28 | "B" 29 | ] @@ -178,7 +178,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:31:17 + --> RUF068.py:31:22 | 29 | ] 30 | __all__ += ["B", "B"] @@ -198,7 +198,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:35:5 + --> RUF068.py:36:5 | 33 | # Bad, unsafe 34 | __all__ = [ @@ -217,7 +217,7 @@ help: Remove duplicate entries from `__all__` | RUF068 [*] `__all__` contains duplicate entries - --> RUF068.py:37:5 + --> RUF068.py:39:5 | 35 | "A", 36 | "A", diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF101_RUF101_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF101_RUF101_0.py.snap index a028111f87..2a62da7cde 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF101_RUF101_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF101_RUF101_0.py.snap @@ -94,7 +94,6 @@ RUF101 [*] `RUF940` is a redirect to `RUF950` 5 | x = 2 # noqa: RUF940, RUF950, RUF940 6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950 | ^^^^^^ - | help: Replace with `RUF950` | 5 | x = 2 # noqa: RUF940, RUF950, RUF940 @@ -109,7 +108,6 @@ RUF101 [*] `RUF940` is a redirect to `RUF950` 5 | x = 2 # noqa: RUF940, RUF950, RUF940 6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950 | ^^^^^^ - | help: Replace with `RUF950` | 5 | x = 2 # noqa: RUF940, RUF950, RUF940 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102.py.snap index 80d1704213..1f70be69ba 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF102_RUF102.py.snap @@ -216,7 +216,6 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123 21 | # Invalid code with trailing reason (single comment) 22 | import pathlib # noqa: INVALID123 some reason | ^^^^^^^^^^^^^^^^^^ - | help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_bleach.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_bleach.snap index 069209c9aa..e3aee0ad72 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_bleach.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_bleach.snap @@ -13,4 +13,3 @@ tinycss2>=1.1.0<1.2 6 | | "tinycss2>=1.1.0<1.2", 7 | | ] | |_^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_invalid_author.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_invalid_author.snap index 112f023679..d5e4839215 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_invalid_author.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF200_invalid_author.snap @@ -11,4 +11,3 @@ RUF200 Failed to parse pyproject.toml: a table with 'name' and/or 'email' keys 6 | | { name = "Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘", email = 1 } 7 | | ] | |_^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_4.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_4.py.snap index 1f5dd54e86..b84b79359b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_4.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_4.py.snap @@ -6,7 +6,6 @@ RUF013 [*] PEP 484 prohibits implicit `Optional` | 15 | def multiple_2(arg1: Optional, arg2: Optional = None, arg3: int = None): ... | ^^^ - | help: Convert to `T | None` | 2 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__confusables_deferred_annotations_diff.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__confusables_deferred_annotations_diff.snap index 3411053a0d..7f7cbe7e4e 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__confusables_deferred_annotations_diff.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__confusables_deferred_annotations_diff.snap @@ -16,4 +16,3 @@ RUF001 String contains ambiguous `ﮨ` (ARABIC LETTER HEH GOAL INITIAL FORM). Di 60 | from typing import Literal 61 | x: '''"""'Literal["ﮨ"]'"""''' | ^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap index 430399d76a..2320e73ff9 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__invalid_rule_code_external_rules_ruff__RUF102.py.snap @@ -178,7 +178,6 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123 21 | # Invalid code with trailing reason (single comment) 22 | import pathlib # noqa: INVALID123 some reason | ^^^^^^^^^^^^^^^^^^ - | help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the `# noqa` comment | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap index f061820b4d..8b3cb6371b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__missing_fstring_syntax_backslash_py311.snap @@ -20,7 +20,6 @@ RUF027 [*] Possible f-string without an `f` prefix 42 | | c} d 43 | | """ | |_______^ - | help: Add `f` prefix | 40 | # RUF027 @@ -41,7 +40,6 @@ RUF027 [*] Possible f-string without an `f` prefix 50 | | a} \ 51 | | " | |_____^ - | help: Add `f` prefix | 48 | # RUF027 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__noqa.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__noqa.snap index 98bd42a03e..d8164e2f94 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__noqa.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__noqa.snap @@ -8,7 +8,6 @@ E741 Ambiguous variable name: `I` 23 | # logged to user 24 | I = 1 # noqa: E741.F841 | ^ - | F841 [*] Local variable `I` is assigned to but never used --> noqa.py:24:5 @@ -17,7 +16,6 @@ F841 [*] Local variable `I` is assigned to but never used 23 | # logged to user 24 | I = 1 # noqa: E741.F841 | ^ - | help: Remove assignment to unused variable `I` | 23 | # logged to user diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF008_RUF008.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF008_RUF008.py.snap index 04357c897d..075d151388 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF008_RUF008.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF008_RUF008.py.snap @@ -52,7 +52,6 @@ RUF008 Do not use mutable default values for dataclass attributes 35 | perfectly_fine: 'list[int]' = field(default_factory=list) 36 | class_variable: 'typing.ClassVar[list[int]]'= [] | ^^ - | RUF008 Do not use mutable default values for dataclass attributes --> RUF008.py:42:48 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039.py.snap index 38ceb0533e..81ec14c221 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039.py.snap @@ -315,7 +315,6 @@ RUF039 [*] First argument to `regex.template()` is not raw string 28 | | l(?i:ne) 29 | | """, flags = regex.X) | |___^ - | help: Replace with raw string | 24 | @@ -435,5 +434,4 @@ RUF039 First argument to `re.compile()` is not raw string | ____________^ 68 | | b") # without fix | |__^ - | help: Replace with raw string diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039_concat.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039_concat.py.snap index 8e808d77fc..a2cb085e40 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039_concat.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF039_RUF039_concat.py.snap @@ -189,7 +189,6 @@ RUF039 [*] First argument to `re.subn()` is not raw string 37 | ) 38 | re.subn("()"r' am I'"??") | ^^^^ - | help: Replace with raw string | 37 | ) @@ -205,7 +204,6 @@ RUF039 [*] First argument to `re.subn()` is not raw string 37 | ) 38 | re.subn("()"r' am I'"??") | ^^^^ - | help: Replace with raw string | 37 | ) @@ -402,7 +400,6 @@ RUF039 [*] First argument to `regex.subn()` is not raw string 77 | ) 78 | regex.subn("()"r' am I'"??") | ^^^^ - | help: Replace with raw string | 77 | ) @@ -418,7 +415,6 @@ RUF039 [*] First argument to `regex.subn()` is not raw string 77 | ) 78 | regex.subn("()"r' am I'"??") | ^^^^ - | help: Replace with raw string | 77 | ) diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF054_RUF054.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF054_RUF054.py.snap index 26ed4fcb97..487a17671c 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF054_RUF054.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF054_RUF054.py.snap @@ -6,15 +6,14 @@ RUF054 Indented form feed | 6 | # Errors 7 | -8 | +8 | ␌ | ^ - | help: Remove form feed RUF054 Indented form feed --> RUF054.py:10:3 | -10 | +10 | ␌ | ^ 11 | 12 | def _(): @@ -25,7 +24,7 @@ RUF054 Indented form feed --> RUF054.py:13:2 | 12 | def _(): -13 | pass +13 | ␌ pass | ^ 14 | 15 | if False: @@ -37,7 +36,6 @@ RUF054 Indented form feed | 15 | if False: 16 | print('F') -17 | print('T') +17 | ␌print('T') | ^ - | help: Remove form feed diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap index c1fd12536f..ac6a7f9132 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_0.py.snap @@ -7,7 +7,6 @@ RUF055 [*] Plain string pattern passed to `re` function 5 | # this should be replaced with `s.replace("abc", "")` 6 | re.sub("abc", "", s) | ^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `s.replace("abc", "")` | 5 | # this should be replaced with `s.replace("abc", "")` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_1.py.snap index d09b20edaf..2d02880aef 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_1.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_1.py.snap @@ -26,7 +26,6 @@ RUF055 [*] Plain string pattern passed to `re` function 16 | repl = "new" 17 | re.sub(r"abc", repl, haystack) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `haystack.replace(r"abc", repl)` | 16 | repl = "new" diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_2.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_2.py.snap index 9befa8492c..65f47a6a8d 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_2.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF055_RUF055_2.py.snap @@ -7,7 +7,6 @@ RUF055 [*] Plain string pattern passed to `re` function 6 | # this should be replaced with `"abc" not in s` 7 | re.search("abc", s) is None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `"abc" not in s` | 6 | # this should be replaced with `"abc" not in s` @@ -22,7 +21,6 @@ RUF055 [*] Plain string pattern passed to `re` function 10 | # this should be replaced with `"abc" in s` 11 | re.search("abc", s) is not None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `"abc" in s` | 10 | # this should be replaced with `"abc" in s` @@ -37,7 +35,6 @@ RUF055 [*] Plain string pattern passed to `re` function 14 | # this should be replaced with `not s.startswith("abc")` 15 | re.match("abc", s) is None | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `not s.startswith("abc")` | 14 | # this should be replaced with `not s.startswith("abc")` @@ -52,7 +49,6 @@ RUF055 [*] Plain string pattern passed to `re` function 18 | # this should be replaced with `s.startswith("abc")` 19 | re.match("abc", s) is not None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `s.startswith("abc")` | 18 | # this should be replaced with `s.startswith("abc")` @@ -67,7 +63,6 @@ RUF055 [*] Plain string pattern passed to `re` function 22 | # this should be replaced with `s != "abc"` 23 | re.fullmatch("abc", s) is None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `s != "abc"` | 22 | # this should be replaced with `s != "abc"` @@ -82,7 +77,6 @@ RUF055 [*] Plain string pattern passed to `re` function 26 | # this should be replaced with `s == "abc"` 27 | re.fullmatch("abc", s) is not None | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `s == "abc"` | 26 | # this should be replaced with `s == "abc"` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF069_RUF069.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF069_RUF069.py.snap index 0818caa012..39486a6364 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF069_RUF069.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF069_RUF069.py.snap @@ -39,7 +39,6 @@ RUF069 Unreliable floating point equality comparison `x == 0.42` 14 | if x == 0.3: ... 15 | if x == 0.42: ... | ^^^^^^^^^^^^^^^ - | RUF069 Unreliable floating point equality comparison `a == b - 0.1` --> RUF069.py:19:12 @@ -235,4 +234,3 @@ RUF069 Unreliable floating point equality comparison `0.3 if x > 0 else 1 == 0.1 47 | 48 | assert (0.3 if x > 0 else 1) == 0.1 + 0.2 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF070_RUF070.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF070_RUF070.py.snap index 1e156ce0dd..a7233c248b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF070_RUF070.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF070_RUF070.py.snap @@ -248,7 +248,6 @@ RUF070 [*] Unnecessary assignment to `x` before `yield` statement 62 | x = f.read() 63 | yield x # RUF070 | ^ - | help: Remove unnecessary assignment | 61 | with open("foo.txt") as f: diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF072_RUF072.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF072_RUF072.py.snap index 65b54304a8..fbb131c113 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF072_RUF072.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF072_RUF072.py.snap @@ -314,12 +314,11 @@ help: Remove the `finally` clause RUF072 [*] Empty `finally` clause --> RUF072.py:178:1 | -176 | 1 +176 | ␌ 1 177 | 2 178 | / finally: 179 | | pass | |________^ - | help: Remove the `finally` clause | 174 | # Bare try finally with line starting with a formfeed @@ -336,12 +335,11 @@ help: Remove the `finally` clause RUF072 [*] Empty `finally` clause --> RUF072.py:186:1 | -184 | try: +184 | ␌try: 185 | 1 186 | / finally: 187 | | pass | |________^ - | help: Remove the `finally` clause | 183 | # (`try` is preceded by a form feed below) diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py37__RUF039_RUF039_py_version_sensitive.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py37__RUF039_RUF039_py_version_sensitive.py.snap index 58f3127ce7..63463f809f 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py37__RUF039_RUF039_py_version_sensitive.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py37__RUF039_RUF039_py_version_sensitive.py.snap @@ -8,5 +8,4 @@ RUF039 First argument to `re.compile()` is not raw string 2 | 3 | re.compile("\N{Partial Differential}") # with unsafe fix if python target is 3.8 or higher, else without fix | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with raw string diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py38__RUF039_RUF039_py_version_sensitive.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py38__RUF039_RUF039_py_version_sensitive.py.snap index 2c96acbb9e..db5e3c79f8 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py38__RUF039_RUF039_py_version_sensitive.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__py38__RUF039_RUF039_py_version_sensitive.py.snap @@ -8,7 +8,6 @@ RUF039 [*] First argument to `re.compile()` is not raw string 2 | 3 | re.compile("\N{Partial Differential}") # with unsafe fix if python target is 3.8 or higher, else without fix | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with raw string | 2 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__py314__RUF058_RUF058_2.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__py314__RUF058_RUF058_2.py.snap index 298d4b2eb8..3762a50a11 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__py314__RUF058_RUF058_2.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__py314__RUF058_RUF058_2.py.snap @@ -42,7 +42,6 @@ RUF058 [*] `itertools.starmap` called on `zip` iterable 6 | starmap(func, zip(a, b, c, strict=False)) 7 | starmap(func, zip(a, b, c, strict=strict)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Use `map` instead | 6 | starmap(func, zip(a, b, c, strict=False)) diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap index 1c4cea3779..c65ef006ef 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__range_suppressions.snap @@ -46,7 +46,6 @@ RUF103 [*] Invalid suppression comment: no matching 'disable' comment 18 | I = 1 19 | # ruff: enable[E741, F841] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove suppression comment | 18 | I = 1 @@ -111,7 +110,6 @@ RUF100 [*] Unused suppression (non-enabled: `E501`) 47 | I = 1 48 | # ruff: enable[E501] | -------------------- - | help: Remove unused suppression | 45 | # An unused suppression diagnostic should also be logged. @@ -302,7 +300,6 @@ RUF102 [*] Invalid rule code in suppression: YF829 96 | # ruff: enable[F841, RQW320] 97 | # ruff: enable[YF829] | ----- - | help: Add non-Ruff rule codes to the `lint.external` configuration option help: Remove the suppression comment | @@ -381,7 +378,6 @@ F841 [*] Local variable `bar` is assigned to but never used 117 | foo = 0 118 | bar = 0 | ^^^ - | help: Remove assignment to unused variable `bar` | 117 | foo = 0 @@ -397,7 +393,6 @@ F841 [*] Local variable `bar` is assigned to but never used 123 | foo = 0 # ruff: ignore[F841] 124 | bar = 0 | ^^^ - | help: Remove assignment to unused variable `bar` | 123 | foo = 0 # ruff: ignore[F841] @@ -413,7 +408,6 @@ F841 [*] Local variable `bar` is assigned to but never used 131 | """ # ruff: ignore[F841] 132 | bar = 0 | ^^^ - | help: Remove assignment to unused variable `bar` | 131 | """ # ruff: ignore[F841] @@ -487,7 +481,6 @@ RUF100 [*] Unused suppression (non-enabled: `F401`) 176 | print("goodbye") 177 | # ruff:enable[F401] | ------------------- - | help: Remove unused suppression | 174 | # https://github.com/astral-sh/ruff/issues/23235 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0.snap index 1d99fa8755..9925381435 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0.snap @@ -209,7 +209,6 @@ E501 Line too long (89 > 88) 92 | 93 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401 | ^ - | RUF100 [*] Unused `noqa` directive (unused: `F401`) --> RUF100_0.py:93:92 @@ -218,7 +217,6 @@ RUF100 [*] Unused `noqa` directive (unused: `F401`) 92 | 93 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401 | ^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 92 | @@ -234,7 +232,6 @@ F841 [*] Local variable `e` is assigned to but never used 107 | d = 1 # …noqa: F841, E50 108 | e = 1 # …noqa: E50 | ^ - | help: Remove assignment to unused variable `e` | 107 | d = 1 # …noqa: F841, E50 @@ -339,7 +336,6 @@ RUF100 [*] Unused `noqa` directive (duplicated: `PGH001`, `S307`) 130 | x = eval(command) # noqa: PGH001, S307, PGH001 131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 130 | x = eval(command) # noqa: PGH001, S307, PGH001 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0_prefix.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0_prefix.snap index 1d99fa8755..9925381435 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0_prefix.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_0_prefix.snap @@ -209,7 +209,6 @@ E501 Line too long (89 > 88) 92 | 93 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401 | ^ - | RUF100 [*] Unused `noqa` directive (unused: `F401`) --> RUF100_0.py:93:92 @@ -218,7 +217,6 @@ RUF100 [*] Unused `noqa` directive (unused: `F401`) 92 | 93 | "shape: (6,)\nSeries: '' [duration[μs]]\n[\n\t0µs\n\t1µs\n\t2µs\n\t3µs\n\t4µs\n\t5µs\n]" # noqa: F401 | ^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 92 | @@ -234,7 +232,6 @@ F841 [*] Local variable `e` is assigned to but never used 107 | d = 1 # …noqa: F841, E50 108 | e = 1 # …noqa: E50 | ^ - | help: Remove assignment to unused variable `e` | 107 | d = 1 # …noqa: F841, E50 @@ -339,7 +336,6 @@ RUF100 [*] Unused `noqa` directive (duplicated: `PGH001`, `S307`) 130 | x = eval(command) # noqa: PGH001, S307, PGH001 131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 130 | x = eval(command) # noqa: PGH001, S307, PGH001 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_1.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_1.snap index a362758258..653a8ea302 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_1.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_1.snap @@ -95,7 +95,6 @@ F401 [*] `typing.Awaitable` imported but unused 88 | # This should mark F501 as unused. 89 | from typing import Awaitable, AwaitableGenerator # noqa: F501 | ^^^^^^^^^ - | help: Remove unused import | 88 | # This should mark F501 as unused. @@ -110,7 +109,6 @@ F401 [*] `typing.AwaitableGenerator` imported but unused 88 | # This should mark F501 as unused. 89 | from typing import Awaitable, AwaitableGenerator # noqa: F501 | ^^^^^^^^^^^^^^^^^^ - | help: Remove unused import | 88 | # This should mark F501 as unused. @@ -125,7 +123,6 @@ RUF100 [*] Unused `noqa` directive (non-enabled: `F501`) 88 | # This should mark F501 as unused. 89 | from typing import Awaitable, AwaitableGenerator # noqa: F501 | ^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 88 | # This should mark F501 as unused. diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_2.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_2.snap index 60decc6cd4..3b2f4a86dc 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_2.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_2.snap @@ -6,7 +6,6 @@ RUF100 [*] Unused `noqa` directive (non-enabled: `F401`) | 1 | import itertools # noqa: F401 | ^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | - import itertools # noqa: F401 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_3.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_3.snap index ae760882c1..aa8e5eb958 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_3.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_3.snap @@ -422,7 +422,6 @@ RUF100 [*] Unused `noqa` directive (unused: `E501`) 30 | print(a) # comment with unicode µ # noqa: E501 31 | print(a) # comment with unicode µ # noqa: E501, F821 | ^^^^^^^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 30 | print(a) # comment with unicode µ # noqa: E501 diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_5.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_5.snap index 6422ffed19..2edd512091 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_5.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruf100_5.snap @@ -75,7 +75,6 @@ RUF100 [*] Unused `noqa` directive (non-enabled: `RET504`) 20 | # line below should autofix to `return data` 21 | return data # noqa: RET504 - intentional incorrect noqa, will be removed | ^^^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 20 | # line below should autofix to `return data` diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_codes.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_codes.snap index 42f0dfd342..6faada6c7b 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_codes.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_codes.snap @@ -7,7 +7,6 @@ F841 [*] Local variable `x` is assigned to but never used 7 | def f(): 8 | x = 1 | ^ - | help: Remove assignment to unused variable `x` | 7 | def f(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused.snap index e22cbd8e0a..f1c33a1592 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused.snap @@ -6,7 +6,6 @@ RUF100 [*] Unused `noqa` directive (non-enabled: `F841`) | 1 | # ruff: noqa: F841 -- intentional unused file directive; will be removed | ^^^^^^^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | - # ruff: noqa: F841 -- intentional unused file directive; will be removed diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused_last_of_many.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused_last_of_many.snap index 4a0b525f25..3c138f9c1d 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused_last_of_many.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_filedirective_unused_last_of_many.snap @@ -21,7 +21,6 @@ RUF100 [*] Unused `noqa` directive (non-enabled: `E701`) 1 | # flake8: noqa: F841, E501 -- used followed by unused code 2 | # ruff: noqa: E701, F541 -- unused followed by used code | ^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove unused `noqa` directive | 1 | # flake8: noqa: F841, E501 -- used followed by unused code diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_invalid.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_invalid.snap index c127d4c20f..04b50369b3 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_invalid.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__ruff_noqa_invalid.snap @@ -6,7 +6,6 @@ F401 [*] `os` imported but unused | 1 | import os # ruff: noqa: F401 | ^^ - | help: Remove unused import: `os` | - import os # ruff: noqa: F401 @@ -19,7 +18,6 @@ F841 [*] Local variable `x` is assigned to but never used 4 | def f(): 5 | x = 1 | ^ - | help: Remove assignment to unused variable `x` | 4 | def f(): diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__strictly_empty_init_modules_ruf067.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__strictly_empty_init_modules_ruf067.snap index 682497466e..d5e32a443f 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__strictly_empty_init_modules_ruf067.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__strictly_empty_init_modules_ruf067.snap @@ -28,7 +28,6 @@ RUF067 `__init__` module should only contain docstrings and re-exports 14 | 15 | os.environ["FOO"] = 1 | ^^^^^^^^^^^^^^^^^^^^^ - | RUF067 `__init__` module should only contain docstrings and re-exports @@ -186,7 +185,6 @@ RUF067 `__init__` module should not contain any code 14 | 15 | os.environ["FOO"] = 1 | ^^^^^^^^^^^^^^^^^^^^^ - | RUF067 `__init__` module should not contain any code @@ -421,4 +419,3 @@ RUF067 `__init__` module should not contain any code 57 | # also allow `__author__` 58 | __author__ = "The Author" # ok | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__unnecessary_if_and_needless_else.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__unnecessary_if_and_needless_else.snap index 6f3f9fc005..e8ed80b873 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__unnecessary_if_and_needless_else.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__unnecessary_if_and_needless_else.snap @@ -47,7 +47,6 @@ RUF047 [*] Empty `else` clause 21 | / else: 22 | | pass | |________^ - | help: Remove the `else` clause | 20 | pass diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else.snap index c3a43a1770..b8824b9cea 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else.snap @@ -85,7 +85,6 @@ RUF072 [*] Empty `finally` clause 29 | / finally: 30 | | pass | |________^ - | help: Remove the `finally` clause | 28 | baz() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else_and_suppressible_exception.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else_and_suppressible_exception.snap index f619ab2c8a..d6b9448753 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else_and_suppressible_exception.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_needless_else_and_suppressible_exception.snap @@ -28,7 +28,6 @@ RUF072 [*] Empty `finally` clause 9 | / finally: 10 | | pass | |________^ - | help: Remove the `finally` clause | 8 | pass diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_suppressible_exception.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_suppressible_exception.snap index c0ff748c42..845be65e12 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_suppressible_exception.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__useless_finally_and_suppressible_exception.snap @@ -9,7 +9,6 @@ RUF072 [*] Empty `finally` clause 8 | / finally: 9 | | pass | |________^ - | help: Remove the `finally` clause | 7 | pass diff --git a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__error-instead-of-exception_TRY400.py.snap b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__error-instead-of-exception_TRY400.py.snap index 67c8f11db9..51a8680efa 100644 --- a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__error-instead-of-exception_TRY400.py.snap +++ b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__error-instead-of-exception_TRY400.py.snap @@ -25,7 +25,6 @@ TRY400 [*] Use `logging.exception` instead of `logging.error` 15 | if True: 16 | logging.error("Context message here") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `exception` | 15 | if True: @@ -59,7 +58,6 @@ TRY400 [*] Use `logging.exception` instead of `logging.error` 29 | if True: 30 | logger.error("Context message here") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `exception` | 29 | if True: @@ -94,7 +92,6 @@ TRY400 [*] Use `logging.exception` instead of `logging.error` 39 | if True: 40 | log.error("Context message here") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `exception` | 39 | if True: @@ -129,7 +126,6 @@ TRY400 [*] Use `logging.exception` instead of `logging.error` 49 | if True: 50 | self.logger.error("Context message here") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `exception` | 49 | if True: @@ -163,7 +159,6 @@ TRY400 [*] Use `logging.exception` instead of `logging.error` 102 | if True: 103 | error("Context message here") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `exception` | 102 | if True: @@ -179,7 +174,6 @@ TRY400 [*] Use `logging.exception` instead of `logging.error` 142 | except Exception: 143 | error("Context message here") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Replace with `exception` | 142 | except Exception: diff --git a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-args_TRY003.py.snap b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-args_TRY003.py.snap index c9cbe9dd20..b312410cec 100644 --- a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-args_TRY003.py.snap +++ b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-args_TRY003.py.snap @@ -19,7 +19,6 @@ TRY003 Avoid specifying long messages outside the exception class 33 | if a % 2 == 0: 34 | raise BadArgCantBeEven(f"The argument '{a}' should be even") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY003 Avoid specifying long messages outside the exception class --> TRY003.py:39:15 @@ -28,7 +27,6 @@ TRY003 Avoid specifying long messages outside the exception class 38 | if a % 2 == 0: 39 | raise BadArgCantBeEven(f"The argument {a} should not be odd.") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY003 Avoid specifying long messages outside the exception class --> TRY003.py:44:15 @@ -37,4 +35,3 @@ TRY003 Avoid specifying long messages outside the exception class 43 | if a % 2 == 0: 44 | raise BadArgCantBeEven("The argument `a` should not be odd.") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-class_TRY002.py.snap b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-class_TRY002.py.snap index a781966a17..87e6ca16ae 100644 --- a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-class_TRY002.py.snap +++ b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__raise-vanilla-class_TRY002.py.snap @@ -19,7 +19,6 @@ TRY002 Create your own exception 16 | if b == 1: 17 | raise Exception | ^^^^^^^^^ - | TRY002 Create your own exception --> TRY002.py:37:15 @@ -39,4 +38,3 @@ TRY002 Create your own exception 40 | if b == 1: 41 | raise BaseException | ^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__type-check-without-type-error_TRY004.py.snap b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__type-check-without-type-error_TRY004.py.snap index 893ee904d6..c44177b542 100644 --- a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__type-check-without-type-error_TRY004.py.snap +++ b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__type-check-without-type-error_TRY004.py.snap @@ -8,7 +8,6 @@ TRY004 Prefer `TypeError` exception for invalid type 11 | else: 12 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:19:9 @@ -17,7 +16,6 @@ TRY004 Prefer `TypeError` exception for invalid type 18 | else: 19 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:30:9 @@ -26,7 +24,6 @@ TRY004 Prefer `TypeError` exception for invalid type 29 | else: 30 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:37:9 @@ -35,7 +32,6 @@ TRY004 Prefer `TypeError` exception for invalid type 36 | else: 37 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:44:9 @@ -44,7 +40,6 @@ TRY004 Prefer `TypeError` exception for invalid type 43 | else: 44 | raise ArithmeticError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:51:9 @@ -53,7 +48,6 @@ TRY004 Prefer `TypeError` exception for invalid type 50 | else: 51 | raise AssertionError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:58:9 @@ -62,7 +56,6 @@ TRY004 Prefer `TypeError` exception for invalid type 57 | else: 58 | raise AttributeError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:65:9 @@ -71,7 +64,6 @@ TRY004 Prefer `TypeError` exception for invalid type 64 | else: 65 | raise BufferError # should be typeerror | ^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:72:9 @@ -80,7 +72,6 @@ TRY004 Prefer `TypeError` exception for invalid type 71 | else: 72 | raise EOFError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:79:9 @@ -89,7 +80,6 @@ TRY004 Prefer `TypeError` exception for invalid type 78 | else: 79 | raise ImportError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:86:9 @@ -98,7 +88,6 @@ TRY004 Prefer `TypeError` exception for invalid type 85 | else: 86 | raise LookupError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:95:9 @@ -109,7 +98,6 @@ TRY004 Prefer `TypeError` exception for invalid type 96 | | "..." 97 | | ) | |_________^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:104:9 @@ -118,7 +106,6 @@ TRY004 Prefer `TypeError` exception for invalid type 103 | else: 104 | raise NameError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:111:9 @@ -127,7 +114,6 @@ TRY004 Prefer `TypeError` exception for invalid type 110 | else: 111 | raise ReferenceError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:118:9 @@ -136,7 +122,6 @@ TRY004 Prefer `TypeError` exception for invalid type 117 | else: 118 | raise RuntimeError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:125:9 @@ -145,7 +130,6 @@ TRY004 Prefer `TypeError` exception for invalid type 124 | else: 125 | raise SyntaxError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:132:9 @@ -154,7 +138,6 @@ TRY004 Prefer `TypeError` exception for invalid type 131 | else: 132 | raise SystemError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:139:9 @@ -163,7 +146,6 @@ TRY004 Prefer `TypeError` exception for invalid type 138 | else: 139 | raise ValueError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:146:9 @@ -172,7 +154,6 @@ TRY004 Prefer `TypeError` exception for invalid type 145 | else: 146 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:153:9 @@ -181,7 +162,6 @@ TRY004 Prefer `TypeError` exception for invalid type 152 | else: 153 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:160:9 @@ -190,7 +170,6 @@ TRY004 Prefer `TypeError` exception for invalid type 159 | else: 160 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:167:9 @@ -199,7 +178,6 @@ TRY004 Prefer `TypeError` exception for invalid type 166 | else: 167 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:174:9 @@ -208,7 +186,6 @@ TRY004 Prefer `TypeError` exception for invalid type 173 | else: 174 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:181:9 @@ -217,7 +194,6 @@ TRY004 Prefer `TypeError` exception for invalid type 180 | else: 181 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:188:9 @@ -226,7 +202,6 @@ TRY004 Prefer `TypeError` exception for invalid type 187 | else: 188 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:195:9 @@ -235,7 +210,6 @@ TRY004 Prefer `TypeError` exception for invalid type 194 | else: 195 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:202:9 @@ -244,7 +218,6 @@ TRY004 Prefer `TypeError` exception for invalid type 201 | else: 202 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:209:9 @@ -253,7 +226,6 @@ TRY004 Prefer `TypeError` exception for invalid type 208 | else: 209 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:216:9 @@ -262,7 +234,6 @@ TRY004 Prefer `TypeError` exception for invalid type 215 | else: 216 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:223:9 @@ -271,7 +242,6 @@ TRY004 Prefer `TypeError` exception for invalid type 222 | else: 223 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:230:9 @@ -280,7 +250,6 @@ TRY004 Prefer `TypeError` exception for invalid type 229 | elif isinstance(arg2, int): 230 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:239:9 @@ -289,7 +258,6 @@ TRY004 Prefer `TypeError` exception for invalid type 238 | else: 239 | raise Exception("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:276:9 @@ -298,7 +266,6 @@ TRY004 Prefer `TypeError` exception for invalid type 275 | if isinstance(some_args, int): 276 | raise ValueError("...") # should be typeerror | ^^^^^^^^^^^^^^^^^^^^^^^ - | TRY004 Prefer `TypeError` exception for invalid type --> TRY004.py:286:9 @@ -329,4 +296,3 @@ TRY004 Prefer `TypeError` exception for invalid type 315 | else: 316 | raise Exception(f"Unknown object type: {obj.__class__.__name__}") | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-log-message_TRY401.py.snap b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-log-message_TRY401.py.snap index 87a8944016..6958981373 100644 --- a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-log-message_TRY401.py.snap +++ b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-log-message_TRY401.py.snap @@ -8,7 +8,6 @@ TRY401 Redundant exception object included in `logging.exception` call 7 | except Exception as ex: 8 | logger.exception(f"Found an error: {ex}") # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:19:53 @@ -70,7 +69,6 @@ TRY401 Redundant exception object included in `logging.exception` call 26 | if True: 27 | logger.exception(f"Found an error: {bad}") # TRY401 | ^^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:39:47 @@ -79,7 +77,6 @@ TRY401 Redundant exception object included in `logging.exception` call 38 | except Exception as ex: 39 | logger.exception(f"Logging an error: {ex}") # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:46:53 @@ -88,7 +85,6 @@ TRY401 Redundant exception object included in `logging.exception` call 45 | except Exception as ex: 46 | logger.exception("Logging an error: " + str(ex)) # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:53:47 @@ -97,7 +93,6 @@ TRY401 Redundant exception object included in `logging.exception` call 52 | except Exception as ex: 53 | logger.exception("Logging an error:", ex) # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:77:38 @@ -106,7 +101,6 @@ TRY401 Redundant exception object included in `logging.exception` call 76 | except Exception as ex: 77 | exception(f"Found an error: {ex}") # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:88:46 @@ -168,7 +162,6 @@ TRY401 Redundant exception object included in `logging.exception` call 95 | if True: 96 | exception(f"Found an error: {bad}") # TRY401 | ^^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:103:40 @@ -177,7 +170,6 @@ TRY401 Redundant exception object included in `logging.exception` call 102 | except Exception as ex: 103 | exception(f"Logging an error: {ex}") # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:110:46 @@ -186,7 +178,6 @@ TRY401 Redundant exception object included in `logging.exception` call 109 | except Exception as ex: 110 | exception("Logging an error: " + str(ex)) # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:117:40 @@ -195,7 +186,6 @@ TRY401 Redundant exception object included in `logging.exception` call 116 | except Exception as ex: 117 | exception("Logging an error:", ex) # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:139:49 @@ -204,7 +194,6 @@ TRY401 Redundant exception object included in `logging.exception` call 138 | except Exception as ex: 139 | logger.exception(f"Found an error: {ex}") # TRY401 | ^^ - | TRY401 Redundant exception object included in `logging.exception` call --> TRY401.py:150:49 @@ -213,4 +202,3 @@ TRY401 Redundant exception object included in `logging.exception` call 149 | except Exception: 150 | logger.exception(f"Found an error: {ex}") # TRY401 | ^^ - | diff --git a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-raise_TRY201.py.snap b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-raise_TRY201.py.snap index f40a9fda63..0560eb3a08 100644 --- a/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-raise_TRY201.py.snap +++ b/crates/ruff_linter/src/rules/tryceratops/snapshots/ruff_linter__rules__tryceratops__tests__verbose-raise_TRY201.py.snap @@ -8,7 +8,6 @@ TRY201 [*] Use `raise` without specifying exception name 19 | logger.exception("process failed") 20 | raise e | ^ - | help: Remove exception name | 19 | logger.exception("process failed") @@ -25,7 +24,6 @@ TRY201 [*] Use `raise` without specifying exception name 62 | if True: 63 | raise e | ^ - | help: Remove exception name | 62 | if True: @@ -41,7 +39,6 @@ TRY201 [*] Use `raise` without specifying exception name 73 | def foo(): 74 | raise e | ^ - | help: Remove exception name | 73 | def foo(): 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 index e39895d9c6..4d6b3dfb9b 100644 --- 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 @@ -31,4 +31,3 @@ PLE1142 `await` should be used within an async function 10 | / async for _ in elements(1): 11 | | pass | |____________^ - | 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 14c1cea362..fed430c44b 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 @@ -8,4 +8,3 @@ invalid-syntax: cannot use an asynchronous comprehension inside of a synchronous 2 | [x async for x in elements(5)] # okay, async at top level 3 | [[x async for x in elements(5)] for i in range(5)] # error on 3.10, okay after | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_scope_notebook.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_scope_notebook.snap index 2038653a11..858407cf45 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_scope_notebook.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__await_scope_notebook.snap @@ -7,4 +7,3 @@ F704 `await` statement outside of a function 1 | class _: 2 | await 1 # SyntaxError: await outside function | ^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__import_sorting.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__import_sorting.snap index b788f7b1fb..9d888d9697 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__import_sorting.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__import_sorting.snap @@ -8,7 +8,6 @@ I001 [*] Import block is un-sorted or un-formatted 2 | | import random 3 | | import math | |___________^ - | help: Organize imports ::: cell 1 | @@ -65,7 +64,6 @@ I001 [*] Import block is un-sorted or un-formatted 7 | / import math 8 | | import abc | |__________^ - | help: Organize imports ::: cell 3 | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__ipy_escape_command.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__ipy_escape_command.snap index 1ddd61a96a..646b1f26c4 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__ipy_escape_command.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__ipy_escape_command.snap @@ -25,7 +25,6 @@ F401 [*] `sys` imported but unused 1 | %%timeit 2 | import sys | ^^^ - | help: Remove unused import: `sys` ::: cell 2 | 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 index 0274914bac..8d7121eb3c 100644 --- 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 @@ -7,4 +7,3 @@ 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 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_in_generator.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_in_generator.py.snap index 2abd1fad09..093c1340f1 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_in_generator.py.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_in_generator.py.snap @@ -52,4 +52,3 @@ B901 Using `yield` and `return {value}` in a generator function can lead to conf 23 | yield 1 24 | return 10 | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_outside_function.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_outside_function.py.snap index dd8114a6dc..cab2c63612 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_outside_function.py.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__return_outside_function.py.snap @@ -15,7 +15,6 @@ F706 `return` statement outside of a function/method 13 | return 1 # error 14 | return # error | ^^^^^^ - | F706 `return` statement outside of a function/method --> resources/test/fixtures/syntax_errors/return_outside_function.py:18:5 @@ -23,7 +22,6 @@ F706 `return` statement outside of a function/method 17 | class C: 18 | return 1 # error | ^^^^^^^^ - | F706 `return` statement outside of a function/method --> resources/test/fixtures/syntax_errors/return_outside_function.py:23:9 @@ -32,4 +30,3 @@ F706 `return` statement outside of a function/method 22 | class C: 23 | return 1 # error | ^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap index bc8140b385..731ac3ee42 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_annotated_global.py_3.14.snap @@ -71,4 +71,3 @@ invalid-syntax: annotated name `x` can't be global 37 | global x # error 38 | x: str | ^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_type_parameter.py_3.12.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_type_parameter.py_3.12.snap index 9b3d052471..dc5d76e882 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_type_parameter.py_3.12.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_duplicate_type_parameter.py_3.12.snap @@ -6,4 +6,3 @@ invalid-syntax: duplicate type parameter | 1 | class C[T, T]: pass | ^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_star_expression.py_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_star_expression.py_3.10.snap index f26a2d73a7..f04472006e 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_star_expression.py_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_invalid_star_expression.py_3.10.snap @@ -27,4 +27,3 @@ invalid-syntax: Starred expression cannot be used here 7 | def func(): 8 | yield *x | ^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_rebound_comprehension.py_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_rebound_comprehension.py_3.10.snap index 91fa59a6fc..58b567adb0 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_rebound_comprehension.py_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_rebound_comprehension.py_3.10.snap @@ -25,4 +25,3 @@ invalid-syntax: assignment expression within a comprehension cannot be used in a 4 | class C: 5 | [(x := y) for y in range(3)] | ^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_single_starred_assignment.py_3.10.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_single_starred_assignment.py_3.10.snap index 7c3cc3916b..04ce5f307b 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_single_starred_assignment.py_3.10.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__semantic_syntax_error_single_starred_assignment.py_3.10.snap @@ -6,4 +6,3 @@ invalid-syntax: starred assignment target must be in a list or tuple | 1 | *a = [1, 2, 3, 4] | ^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__undefined_name.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__undefined_name.snap index 478ccbfead..949a99ac82 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__undefined_name.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__undefined_name.snap @@ -6,4 +6,3 @@ F821 Undefined name `undefined` | 1 | print(undefined) | ^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__unused_variable.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__unused_variable.snap index fc213dcdd2..de53bff608 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__unused_variable.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__unused_variable.snap @@ -26,7 +26,6 @@ F841 [*] Local variable `foo2` is assigned to but never used 2 | foo1 = %matplotlib --list 3 | foo2: list[str] = %matplotlib --list | ^^^^ - | help: Remove assignment to unused variable `foo2` ::: cell 1 | @@ -61,7 +60,6 @@ F841 [*] Local variable `bar2` is assigned to but never used 2 | bar1 = !pwd 3 | bar2: str = !pwd | ^^^^ - | help: Remove assignment to unused variable `bar2` ::: cell 2 | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_from_in_async_function.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_from_in_async_function.py.snap index ab60900419..a53ef73460 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_from_in_async_function.py.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_from_in_async_function.py.snap @@ -6,4 +6,3 @@ PLE1700 `yield from` statement in async function; use `async for` instead | 1 | async def f(): yield from x # error | ^^^^^^^^^^^^ - | diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_scope.py.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_scope.py.snap index e32c627fce..09a503ee67 100644 --- a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_scope.py.snap +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__yield_scope.py.snap @@ -48,7 +48,6 @@ F704 `yield` statement outside of a function 4 | await 1 # error 5 | [(yield x) for x in range(3)] # error | ^^^^^^^ - | F704 `yield` statement outside of a function --> resources/test/fixtures/syntax_errors/yield_scope.py:23:9 @@ -110,7 +109,6 @@ F704 `yield` statement outside of a function 28 | {(yield 1): 0 for x in range(3)} # error 29 | {0: (yield 1) for x in range(3)} # error | ^^^^^^^ - | F704 `await` statement outside of a function --> resources/test/fixtures/syntax_errors/yield_scope.py:36:10 @@ -127,4 +125,3 @@ F704 `await` statement outside of a function | 41 | await 1 # error | ^^^^^^^ - | diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 928defd32f..a08e6542c7 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -2,9 +2,9 @@ use std::cell::RefCell; use std::cmp::Ordering; use std::fmt::{Formatter, Write}; +use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet}; use datatest_stable::Utf8Path; use itertools::Itertools; -use ruff_annotate_snippets::{Level, Renderer, Snippet}; use ruff_python_ast::token::{Token, Tokens}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal, walk_module}; @@ -380,14 +380,14 @@ impl std::fmt::Display for CodeFrame<'_> { let label = format!("Syntax Error: {error}", error = self.error); let span = usize::from(annotation_range.start())..usize::from(annotation_range.end()); - let annotation = Level::Error.span(span).label(&label); + let annotation = AnnotationKind::Primary.span(span).label(&label); let snippet = Snippet::source(source) .line_start(start_index.get()) .annotation(annotation) .fold(false); - let message = Level::None.title("").snippet(snippet); + let message = Group::with_level(Level::ERROR).element(snippet); let renderer = Renderer::plain().cut_indicator("…"); - let rendered = renderer.render(message); + let rendered = renderer.render(&[message]); writeln!(f, "{rendered}") } } diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap index 21eb2b1619..32da2cab8d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap @@ -219,7 +219,6 @@ Module( 3 | x: yield from b = 1 4 | x: y := int = 1 | ^^ Syntax Error: Expected a statement - | ## Semantic Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_target.py.snap index 1e58991d17..12e797e812 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_target.py.snap @@ -563,4 +563,3 @@ Module( 9 | [x]: int = 1 10 | [x, y]: int = 1, 2 | ^^^^^^ Syntax Error: Only single target (not list) can be annotated - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_value.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_value.py.snap index f9ad11979a..2123f14a29 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_value.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_value.py.snap @@ -246,4 +246,3 @@ Module( 2 | x: Any = x := 1 3 | x: list = [x, *a | b, *a or b] | ^^^^^^ Syntax Error: Boolean expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_missing_rhs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_missing_rhs.py.snap index babd340987..db6d7e8c2d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_missing_rhs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_missing_rhs.py.snap @@ -43,4 +43,3 @@ Module( | 1 | x: int = | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_type_alias_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_type_alias_annotation.py.snap index 8c2e51d0cd..0b9892d0bf 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_type_alias_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_type_alias_annotation.py.snap @@ -120,4 +120,3 @@ Module( 1 | a: type X = int 2 | lambda: type X = int | ^ Syntax Error: Simple statements must be separated by newlines or semicolons - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@args_unparenthesized_generator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@args_unparenthesized_generator.py.snap index 13b2bd4292..49270f2b38 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@args_unparenthesized_generator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@args_unparenthesized_generator.py.snap @@ -337,4 +337,3 @@ Module( 2 | total(1, 2, x for x in range(5), 6) 3 | sum(x for x in range(10),) | ^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unparenthesized generator expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_msg.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_msg.py.snap index fd78d8760b..2b647a1887 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_msg.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_msg.py.snap @@ -34,4 +34,3 @@ Module( | 1 | assert x, | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_test.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_test.py.snap index 2c64815756..14a4dd1844 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_test.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_empty_test.py.snap @@ -34,4 +34,3 @@ Module( | 1 | assert | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_msg_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_msg_expr.py.snap index 2cc454ed48..b8edfb474b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_msg_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_msg_expr.py.snap @@ -175,4 +175,3 @@ Module( 3 | assert False, yield x 4 | assert False, x := 1 | ^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_test_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_test_expr.py.snap index 87c0dcf672..23311b018f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_test_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assert_invalid_test_expr.py.snap @@ -160,4 +160,3 @@ Module( 3 | assert yield x 4 | assert x := 1 | ^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_target.py.snap index 7516e2d842..1b06e85b42 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@assign_stmt_invalid_target.py.snap @@ -276,7 +276,6 @@ Module( 3 | x = 1 = y = 2 = z 4 | ["a", "b"] = ["a", "b"] | ^^^ Syntax Error: Invalid assignment target - | | @@ -284,4 +283,3 @@ Module( 3 | x = 1 = y = 2 = z 4 | ["a", "b"] = ["a", "b"] | ^^^ Syntax Error: Invalid assignment target - | 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 6fe406354b..65b3b02572 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 @@ -342,4 +342,3 @@ Module( 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 index 45bda59ff0..186eb41f69 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 @@ -217,4 +217,3 @@ Module( 3 | _ = *list() 4 | _ = *(p + q) | ^^^^^^^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_target.py.snap index dbe201539e..4fd519628a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_target.py.snap @@ -255,4 +255,3 @@ Module( 5 | x += pass 6 | (x + y) += 1 | ^^^^^ Syntax Error: Invalid augmented assignment target - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_value.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_value.py.snap index eeec973fc9..02e8bdd447 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_value.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@aug_assign_stmt_invalid_value.py.snap @@ -279,4 +279,3 @@ Module( 4 | x += *lambda x: x 5 | x += y := 1 | ^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap index c8a556e09b..d32ea7bf46 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@backslash_continuation_indentation_error.py.snap @@ -68,4 +68,3 @@ Module( 3 | / \ 4 | | 2 | |____^ Syntax Error: Unexpected indentation - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@case_expect_indented_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@case_expect_indented_block.py.snap index 952ba305f7..5b757520e6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@case_expect_indented_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@case_expect_indented_block.py.snap @@ -92,4 +92,3 @@ Module( 2 | case 1: 3 | case 2: ... | ^^^^ Syntax Error: Expected an indented block after `case` block - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_empty_body.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_empty_body.py.snap index b12cc050bf..7a88362450 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_empty_body.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_empty_body.py.snap @@ -91,4 +91,3 @@ Module( 2 | class Foo(): 3 | x = 42 | ^ Syntax Error: Expected an indented block after `class` definition - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_missing_name.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_missing_name.py.snap index 218b289dc4..ffb66e8ee6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_missing_name.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_missing_name.py.snap @@ -155,4 +155,3 @@ Module( 2 | class (): ... 3 | class (metaclass=ABC): ... | ^ Syntax Error: Expected an identifier - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unparenthesized_generator_argument.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unparenthesized_generator_argument.py.snap index 08770ff67f..6a5ebeb17c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unparenthesized_generator_argument.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_def_unparenthesized_generator_argument.py.snap @@ -94,4 +94,3 @@ Module( | 1 | class Foo(base for base in bases): ... | ^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Unparenthesized generator expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap index 69e2166048..b24904e9ac 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@class_type_params_py311.py.snap @@ -176,7 +176,6 @@ Module( 2 | class Foo[S: (str, bytes), T: float, *Ts, **P]: ... 3 | class Foo[]: ... | ^ Syntax Error: Type parameter list cannot be empty - | ## Unsupported Syntax Errors @@ -194,4 +193,3 @@ Module( 2 | class Foo[S: (str, bytes), T: float, *Ts, **P]: ... 3 | class Foo[]: ... | ^^ Syntax Error: Cannot use type parameter lists on Python 3.11 (syntax was added in Python 3.12) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_indented_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_indented_block.py.snap index 32d4fbf051..e38fc337b0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_indented_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_indented_block.py.snap @@ -67,4 +67,3 @@ Module( 5 | # at the newline token after `:` 6 | if True: | ^ Syntax Error: Expected an indented block after `if` statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_single_statement.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_single_statement.py.snap index f43fda3f90..fbcd78eef2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_single_statement.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@clause_expect_single_statement.py.snap @@ -56,4 +56,3 @@ Module( | 1 | if True: if True: pass | ^^ Syntax Error: Expected a simple statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma.py.snap index 84c88e8d1f..d5cb24b9ff 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma.py.snap @@ -69,10 +69,8 @@ Module( | 1 | call(**x := 1) | ^^ Syntax Error: Expected `,`, found `:=` - | | 1 | call(**x := 1) | ^ Syntax Error: Positional argument cannot follow keyword argument unpacking - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma_between_elements.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma_between_elements.py.snap index 4da8a91dea..69e493cafc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma_between_elements.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_comma_between_elements.py.snap @@ -62,4 +62,3 @@ Module( 1 | # The comma between the first two elements is expected in `parse_list_expression`. 2 | [0, 1 2] | ^ Syntax Error: Expected `,`, found int - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_element_between_commas.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_element_between_commas.py.snap index 07e9b5e272..2c50e8d7c5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_element_between_commas.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_element_between_commas.py.snap @@ -61,4 +61,3 @@ Module( | 1 | [0, 1, , 2] | ^ Syntax Error: Expected an expression or a ']' - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_first_element.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_first_element.py.snap index 5f23342613..48bd834ed4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_first_element.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comma_separated_missing_first_element.py.snap @@ -55,4 +55,3 @@ Module( | 1 | call(= 1) | ^ Syntax Error: Expected an expression or a ')' - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comprehension_missing_for_after_async.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comprehension_missing_for_after_async.py.snap index a31a055919..dc0abcd271 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comprehension_missing_for_after_async.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@comprehension_missing_for_after_async.py.snap @@ -86,4 +86,3 @@ Module( 1 | (async) 2 | (x async x in iter) | ^ Syntax Error: Expected `for`, found name - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_class.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_class.py.snap index d3f8cd86fd..b12ab21adc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_class.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_class.py.snap @@ -103,4 +103,3 @@ Module( 1 | class __debug__: ... # class name 2 | class C[__debug__]: ... # type parameter name | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_function.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_function.py.snap index 18d9869410..0b94a57f9d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_function.py.snap @@ -245,4 +245,3 @@ Module( 3 | def f(__debug__): ... # parameter name 4 | lambda __debug__: 0 # lambda parameter name | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_import.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_import.py.snap index c40da1652a..870b0b8793 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_import.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_import.py.snap @@ -151,4 +151,3 @@ Module( 3 | from x import __debug__ 4 | from x import debug as __debug__ | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_match.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_match.py.snap index 0fe9d38d82..6d47e009d4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_match.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_match.py.snap @@ -69,4 +69,3 @@ Module( 1 | match x: 2 | case __debug__: ... | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_try.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_try.py.snap index 1514d68600..827e2c62ac 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_try.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_try.py.snap @@ -82,4 +82,3 @@ Module( 1 | try: ... 2 | except Exception as __debug__: ... | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_type_alias.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_type_alias.py.snap index 89647fd3a3..e0d4739946 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_type_alias.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_type_alias.py.snap @@ -108,4 +108,3 @@ Module( 1 | type __debug__ = list[int] # visited as an Expr but still flagged 2 | type Debug[__debug__] = str | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_with.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_with.py.snap index 12c0003499..c0e68ac262 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_with.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@debug_shadow_with.py.snap @@ -98,4 +98,3 @@ Module( | 1 | with open("foo.txt") as __debug__: ... | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap index 43ca84eb06..dca904baa9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_expression.py.snap @@ -277,4 +277,3 @@ Module( 7 | @ 8 | class Test | ^ Syntax Error: Expected `:`, found newline - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_newline.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_newline.py.snap index ea573c4cde..30cf842b3c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_newline.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_missing_newline.py.snap @@ -180,4 +180,3 @@ Module( 2 | @x async def foo(): ... 3 | @x class Foo: ... | ^^^^^ Syntax Error: Expected newline, found `class` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_unexpected_token.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_unexpected_token.py.snap index df57c3024a..be98f7c3ad 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_unexpected_token.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@decorator_unexpected_token.py.snap @@ -167,4 +167,3 @@ Module( 3 | @foo 4 | x = 1 | ^ Syntax Error: Expected class, function definition or async function definition after decorator - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_debug_py39.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_debug_py39.py.snap index 83f2d82287..dbb86fe822 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_debug_py39.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_debug_py39.py.snap @@ -36,4 +36,3 @@ Module( 1 | # parse_options: {"target-version": "3.9"} 2 | del __debug__ | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.9 (syntax was removed in 3.9) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_incomplete_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_incomplete_target.py.snap index 1ea6b54337..e8a391037c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_incomplete_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_incomplete_target.py.snap @@ -137,4 +137,3 @@ Module( 3 | del x, y[ 4 | z | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_stmt_empty.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_stmt_empty.py.snap index a769520c8a..28619bfea2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_stmt_empty.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@del_stmt_empty.py.snap @@ -26,4 +26,3 @@ Module( | 1 | del | ^ Syntax Error: Delete statement must have at least one target - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@different_match_pattern_bindings.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@different_match_pattern_bindings.py.snap index 588dbe3cd8..9f689a303d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@different_match_pattern_bindings.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@different_match_pattern_bindings.py.snap @@ -1230,4 +1230,3 @@ Module( 12 | case [C(D(a))] | [C(D(b))]: ... 13 | case [(a, b)] | [(c, d)]: ... | ^^^^^^^^^^^^^^^^^^^ Syntax Error: alternative patterns bind different names - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@dotted_name_multiple_dots.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@dotted_name_multiple_dots.py.snap index dd982db907..ebad60ec0a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@dotted_name_multiple_dots.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@dotted_name_multiple_dots.py.snap @@ -91,11 +91,9 @@ Module( 1 | import a..b 2 | import a...b | ^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | 1 | import a..b 2 | import a...b | ^ Syntax Error: Simple statements must be separated by newlines or semicolons - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_class_attr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_class_attr.py.snap index 83e2d732da..94f9ad317c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_class_attr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_match_class_attr.py.snap @@ -820,7 +820,6 @@ Module( 5 | case [{}, {"x": x, "y": Foo(x=1, x=2)}]: ... 6 | case Class(x=1, d={"x": 1, "x": 2}, other=Class(x=1, x=2)): ... | ^^^ Syntax Error: mapping pattern checks duplicate key `"x"` - | | @@ -828,4 +827,3 @@ Module( 5 | case [{}, {"x": x, "y": Foo(x=1, x=2)}]: ... 6 | case Class(x=1, d={"x": 1, "x": 2}, other=Class(x=1, x=2)): ... | ^ Syntax Error: attribute name `x` repeated in class pattern - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap index 6630565e33..ffbe7baa87 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@duplicate_type_parameter_names.py.snap @@ -671,4 +671,3 @@ Module( 6 | def f[T, *T](): ... # star is still duplicate 7 | def f[T, **T](): ... # as is double star | ^^^ Syntax Error: duplicate type parameter - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_star_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_star_py310.py.snap index 668f37596b..56a1931678 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_star_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@except_star_py310.py.snap @@ -158,4 +158,3 @@ Module( 4 | except* KeyError: ... 5 | except * Error: ... | ^ Syntax Error: Cannot use `except*` on Python 3.10 (syntax was added in Python 3.11) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__double_starred.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__double_starred.py.snap index 8656ee03e8..58d3cd2c1c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__double_starred.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__double_starred.py.snap @@ -239,7 +239,6 @@ Module( 4 | 5 | call(**x := 1) | ^^ Syntax Error: Expected `,`, found `:=` - | | @@ -247,4 +246,3 @@ Module( 4 | 5 | call(**x := 1) | ^ Syntax Error: Positional argument cannot follow keyword argument unpacking - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__duplicate_keyword_arguments.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__duplicate_keyword_arguments.py.snap index 9f5a0cfa41..61195ea44c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__duplicate_keyword_arguments.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__duplicate_keyword_arguments.py.snap @@ -146,10 +146,8 @@ Module( | 1 | foo(a=1, b=2, c=3, b=4, a=5) | ^^^ Syntax Error: Duplicate keyword argument "b" - | | 1 | foo(a=1, b=2, c=3, b=4, a=5) | ^^^ Syntax Error: Duplicate keyword argument "a" - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_expression.py.snap index 1e7f4b6192..b444c4d79d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_expression.py.snap @@ -223,4 +223,3 @@ Module( 4 | call(yield x) 5 | call(yield from x) | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_keyword_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_keyword_expression.py.snap index 00502dd679..551521976d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_keyword_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_keyword_expression.py.snap @@ -259,4 +259,3 @@ Module( 3 | call(x = *y) 4 | call(x = (*y)) | ^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_order.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_order.py.snap index 90c44de37c..9ccc48a630 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_order.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__invalid_order.py.snap @@ -343,4 +343,3 @@ Module( 4 | call(**kwargs, *args) 5 | call(**kwargs, (*args)) | ^^^^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_argument.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_argument.py.snap index 42fd265361..9525b67f76 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_argument.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_argument.py.snap @@ -62,4 +62,3 @@ Module( | 1 | call(x,,y) | ^ Syntax Error: Expected an expression or a ')' - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_comma.py.snap index 0f781f1e53..33b4022841 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__missing_comma.py.snap @@ -62,4 +62,3 @@ Module( | 1 | call(x y) | ^ Syntax Error: Expected `,`, found name - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__starred.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__starred.py.snap index 7703db14a2..f74d95974c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__starred.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__arguments__starred.py.snap @@ -200,7 +200,6 @@ Module( 2 | call(*yield x) 3 | call(*yield from x) | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here - | ## Unsupported Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__invalid_member.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__invalid_member.py.snap index 67ca493c7d..40b4cb4fe1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__invalid_member.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__invalid_member.py.snap @@ -160,4 +160,3 @@ Module( 2 | x.1.0 3 | x.[0] | ^ Syntax Error: Expected an identifier - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__multiple_dots.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__multiple_dots.py.snap index cbbf28892a..ee5946cf59 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__multiple_dots.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__multiple_dots.py.snap @@ -163,7 +163,6 @@ Module( 2 | multiple....dots 3 | multiple.....dots | ^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | @@ -171,4 +170,3 @@ Module( 2 | multiple....dots 3 | multiple.....dots | ^ Syntax Error: Expected an identifier - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__no_member.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__no_member.py.snap index 04634b6766..f23c09e434 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__no_member.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__attribute__no_member.py.snap @@ -96,4 +96,3 @@ Module( 5 | # No member access after the dot. 6 | last. | ^ Syntax Error: Expected an identifier - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__recover.py.snap index 3eb3479394..e3f5cb4807 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__await__recover.py.snap @@ -365,4 +365,3 @@ Module( 16 | await ~x 17 | await not x | ^^^^^ Syntax Error: Boolean 'not' expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__invalid_rhs_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__invalid_rhs_expression.py.snap index 41d9904805..2d828a4ea5 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__invalid_rhs_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__invalid_rhs_expression.py.snap @@ -128,4 +128,3 @@ Module( 2 | 3 | x - yield y | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__named_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__named_expression.py.snap index 8293f4b759..47e26dcc60 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__named_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__named_expression.py.snap @@ -134,4 +134,3 @@ Module( 1 | x - y := (1, 2) 2 | x / y := 2 | ^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__starred_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__starred_expression.py.snap index 5080466f7f..2e97b80a96 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__starred_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bin_op__starred_expression.py.snap @@ -99,4 +99,3 @@ Module( 1 | x + *y 2 | x ** *y | ^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__invalid_rhs_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__invalid_rhs_expression.py.snap index 8f10231161..151ded3761 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__invalid_rhs_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__invalid_rhs_expression.py.snap @@ -132,4 +132,3 @@ Module( 2 | 3 | x or yield y | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__missing_lhs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__missing_lhs.py.snap index 7becb2d219..1a9fd28213 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__missing_lhs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__missing_lhs.py.snap @@ -33,4 +33,3 @@ Module( | 1 | and y | ^^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__named_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__named_expression.py.snap index c599119ee6..4b0e330e7f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__named_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__named_expression.py.snap @@ -117,4 +117,3 @@ Module( 1 | x and a := b 2 | x or a := b | ^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__starred_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__starred_expression.py.snap index a63aa3e177..c9ef42bbbd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__starred_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__bool_op__starred_expression.py.snap @@ -103,4 +103,3 @@ Module( 1 | x and *y 2 | x or *y | ^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_order.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_order.py.snap index ccc649ea7c..ff77fb79c9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_order.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_order.py.snap @@ -169,18 +169,15 @@ Module( 6 | # Same here as well, `not` without `in` is considered to be a unary operator 7 | x not is y | ^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | 6 | # Same here as well, `not` without `in` is considered to be a unary operator 7 | x not is y | ^^ Syntax Error: Expected an identifier, but found a keyword `is` that cannot be used here - | | 6 | # Same here as well, `not` without `in` is considered to be a unary operator 7 | x not is y | ^ Syntax Error: Simple statements must be separated by newlines or semicolons - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_rhs_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_rhs_expression.py.snap index 22a228bba8..49de016698 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_rhs_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__invalid_rhs_expression.py.snap @@ -136,4 +136,3 @@ Module( 2 | 3 | x == yield y | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__multiple_equals.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__multiple_equals.py.snap index 38297d62da..170fba27dc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__multiple_equals.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__multiple_equals.py.snap @@ -123,7 +123,6 @@ Module( 2 | x === y 3 | x !== y | ^ Syntax Error: Expected an expression - | | @@ -131,4 +130,3 @@ Module( 2 | x === y 3 | x !== y | ^^^^ Syntax Error: Invalid assignment target - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__named_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__named_expression.py.snap index 3bdec1455c..bbdd158554 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__named_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__named_expression.py.snap @@ -142,4 +142,3 @@ Module( 1 | x not in y := (1, 2) 2 | x > y := 2 | ^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__starred_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__starred_expression.py.snap index 0de6ae37e0..4b546bf98c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__starred_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__compare__starred_expression.py.snap @@ -205,4 +205,3 @@ Module( 4 | *x < y 5 | *x is not y | ^^^^^^^^^^ Syntax Error: Comparison expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__comprehension.py.snap index a0f372cea4..2bec92c3f3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__comprehension.py.snap @@ -959,4 +959,3 @@ Module( 16 | {x: y for x in data if yield from y} 17 | {x: y for x in data if lambda y: y} | ^^^^^^^^^^^ Syntax Error: Lambda expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star.py.snap index 64fb0233e4..0be18e9338 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__double_star.py.snap @@ -633,4 +633,3 @@ Module( 11 | {**x not in y} 12 | {**x < y} | ^^^^^ Syntax Error: Comparison expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_0.py.snap index 31bd7feb9f..a72d48175c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_0.py.snap @@ -104,11 +104,9 @@ Module( 3 | def foo(): 4 | pass | ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here - | | 3 | def foo(): 4 | pass | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_1.py.snap index c53bcf8e92..3fda1e1433 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__missing_closing_brace_1.py.snap @@ -72,4 +72,3 @@ Module( 2 | 3 | 1 + 2 | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap index b1ad5d8255..c26c1b247d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__dict__recover.py.snap @@ -558,7 +558,6 @@ Module( 23 | {*x: y, z: a, *b: c} 24 | {x: *y, z: *a} | ^^ Syntax Error: Starred expression cannot be used here - | | @@ -566,4 +565,3 @@ Module( 23 | {*x: y, z: a, *b: c} 24 | {x: *y, z: *a} | ^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_identifiers.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_identifiers.py.snap index ba444eb959..52729b5c7a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_identifiers.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_identifiers.py.snap @@ -129,7 +129,6 @@ Module( 6 | # comment 7 | 🐶) | ^^ Syntax Error: Got unexpected token 🐶 - | | @@ -137,7 +136,6 @@ Module( 6 | # comment 7 | 🐶) | ^ Syntax Error: Expected a statement - | | @@ -145,4 +143,3 @@ Module( 6 | # comment 7 | 🐶) | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_statement.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_statement.py.snap index 20d9f0812e..6b0e5ccbb9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_statement.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__emoji_statement.py.snap @@ -18,4 +18,3 @@ Module( | 1 | 👍 | ^^ Syntax Error: Got unexpected token 👍 - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__recover.py.snap index 38a8d3c530..8e25a9ff44 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__if__recover.py.snap @@ -404,4 +404,3 @@ Module( 9 | x if expr else yield y 10 | x if expr else yield from y | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_default_parameters.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_default_parameters.py.snap index 7d2bfdb631..73de71b4d0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_default_parameters.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_default_parameters.py.snap @@ -107,4 +107,3 @@ Module( | 1 | lambda a, b=20, c: 1 | ^ Syntax Error: Parameter without a default cannot follow a parameter with a default - | 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 39ddc3c4c5..b81032db42 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 @@ -339,7 +339,6 @@ Module( 8 | 9 | lambda a, *, **a: 1 | ^^^ Syntax Error: Expected one or more keyword parameter after `*` separator - | ## Semantic Syntax Errors @@ -387,4 +386,3 @@ Module( 8 | 9 | lambda a, *, **a: 1 | ^ Syntax Error: Duplicate parameter "a" - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__comprehension.py.snap index 10e7438362..63a9049b31 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__comprehension.py.snap @@ -1291,7 +1291,6 @@ Module( 21 | [*x if x else y for x in z] 22 | [x if x else *y for x in z] | ^^ Syntax Error: Starred expression cannot be used here - | ## Unsupported Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_0.py.snap index 27415fde73..1129c6f661 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_0.py.snap @@ -44,4 +44,3 @@ Module( 2 | 3 | [ | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_1.py.snap index d53b5a8291..8ac3aa77ce 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_1.py.snap @@ -59,4 +59,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_2.py.snap index 3bfaaa689e..347768333c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__missing_closing_bracket_2.py.snap @@ -68,4 +68,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__recover.py.snap index 3fa2a32578..8188090268 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__recover.py.snap @@ -344,4 +344,3 @@ Module( 19 | 20 | [*] | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__star_expression_precedence.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__star_expression_precedence.py.snap index 064ab37b92..13ec68acb8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__star_expression_precedence.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__list__star_expression_precedence.py.snap @@ -522,4 +522,3 @@ Module( 9 | [*lambda x: x, z] 10 | [*x := 2, z] | ^^ Syntax Error: Assignment expression target must be an identifier - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__invalid_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__invalid_target.py.snap index 80c4345b88..a7ba61211f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__invalid_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__invalid_target.py.snap @@ -234,4 +234,3 @@ Module( 5 | (*x := 1) 6 | ([x, y] := [1, 2]) | ^^^^^^ Syntax Error: Assignment expression target must be an identifier - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_0.py.snap index d434114a50..80360bc643 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_0.py.snap @@ -35,7 +35,6 @@ Module( 2 | 3 | x := | ^^ Syntax Error: Expected a statement - | | @@ -43,4 +42,3 @@ Module( 2 | 3 | x := | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_1.py.snap index d37bcfe97a..6adc7cf738 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_1.py.snap @@ -49,4 +49,3 @@ Module( 2 | 3 | (x := | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_2.py.snap index 057477b761..c5d280ce7a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_2.py.snap @@ -102,11 +102,9 @@ Module( 5 | def foo(): 6 | pass | ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here - | | 5 | def foo(): 6 | pass | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_3.py.snap index 181c17c668..56494fe2b3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__named__missing_expression_3.py.snap @@ -64,4 +64,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__generator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__generator.py.snap index 2c06c02bf4..10b1f719fa 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__generator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__generator.py.snap @@ -83,16 +83,13 @@ Module( | 1 | (x := 1, for x in y) | ^^^ Syntax Error: Expected `)`, found `for` - | | 1 | (x := 1, for x in y) | ^ Syntax Error: Expected `:`, found `)` - | | 1 | (x := 1, for x in y) | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_0.py.snap index ed9f13abc6..be74ea3c8a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_0.py.snap @@ -35,4 +35,3 @@ Module( 2 | 3 | ( | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_1.py.snap index 80fe213fc2..53ae176db3 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_1.py.snap @@ -50,4 +50,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_2.py.snap index fa0cdaade8..05c0bb1775 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__missing_closing_paren_2.py.snap @@ -69,4 +69,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__parenthesized.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__parenthesized.py.snap index 2dc3440241..9f0372217c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__parenthesized.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__parenthesized.py.snap @@ -79,4 +79,3 @@ Module( 4 | # Unparenthesized named expression is allowed. 5 | x := 1 | ^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple.py.snap index 7670ed0edd..9aa45988b6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple.py.snap @@ -421,4 +421,3 @@ Module( 20 | # Unparenthesized named expression is not allowed 21 | x, y := 2, z | ^^ Syntax Error: Expected `,`, found `:=` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap index 05cd9dbaca..fc2ce04a1c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__parenthesized__tuple_starred_expr.py.snap @@ -1403,7 +1403,6 @@ Module( 9 | (*lambda x: x, z, *lambda x: x) 10 | (*x := 2, z, *x := 2) | ^^ Syntax Error: Assignment expression target must be an identifier - | | @@ -1411,7 +1410,6 @@ Module( 9 | (*lambda x: x, z, *lambda x: x) 10 | (*x := 2, z, *x := 2) | ^^ Syntax Error: Assignment expression target must be an identifier - | | @@ -1535,7 +1533,6 @@ Module( 19 | *lambda x: x, z, *lambda x: x 20 | *x := 2, z, *x := 2 | ^^ Syntax Error: Expected a statement - | | @@ -1543,4 +1540,3 @@ Module( 19 | *lambda x: x, z, *lambda x: x 20 | *x := 2, z, *x := 2 | ^^ Syntax Error: Expected `,`, found `:=` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__comprehension.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__comprehension.py.snap index 22132cd301..fe53312824 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__comprehension.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__comprehension.py.snap @@ -839,4 +839,3 @@ Module( 16 | {x for x in data if yield from y} 17 | {x for x in data if lambda y: y} | ^^^^^^^^^^^ Syntax Error: Lambda expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_0.py.snap index 759f2c99c0..b520e3eea9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_0.py.snap @@ -43,4 +43,3 @@ Module( 2 | 3 | { | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_1.py.snap index 01bfdf4692..b746257d0b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_1.py.snap @@ -58,4 +58,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_2.py.snap index ead70d568e..ef03e861b0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__missing_closing_curly_brace_2.py.snap @@ -67,4 +67,3 @@ Module( 5 | 6 | x + y | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__recover.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__recover.py.snap index 74e95fe8c7..8954c554fa 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__recover.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__recover.py.snap @@ -332,4 +332,3 @@ Module( 21 | 22 | [*] | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__star_expression_precedence.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__star_expression_precedence.py.snap index d3c23a0ec3..20d61b1a4c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__star_expression_precedence.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__set__star_expression_precedence.py.snap @@ -514,4 +514,3 @@ Module( 9 | {*lambda x: x, z} 10 | {*x := 2, z} | ^^ Syntax Error: Assignment expression target must be an identifier - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__invalid_slice_element.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__invalid_slice_element.py.snap index 2e4321abd9..91295b506f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__invalid_slice_element.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__invalid_slice_element.py.snap @@ -337,4 +337,3 @@ Module( 11 | # Mixed starred expression and named expression 12 | x[*x := 1] | ^^ Syntax Error: Assignment expression target must be an identifier - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_0.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_0.py.snap index b8b02d2973..bb210f7665 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_0.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_0.py.snap @@ -75,4 +75,3 @@ Module( 2 | 3 | x + y | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_1.py.snap index 05de28c275..13d17537ca 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__subscript__unclosed_slice_1.py.snap @@ -113,11 +113,9 @@ Module( 3 | def foo(): 4 | pass | ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here - | | 3 | def foo(): 4 | pass | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary.py.snap index b8f917a7dd..3510eb8a3c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary.py.snap @@ -55,4 +55,3 @@ Module( | 1 | not x := 1 | ^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__named_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__named_expression.py.snap index 45a740ba23..a410a5cb73 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__named_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__unary__named_expression.py.snap @@ -99,4 +99,3 @@ Module( 1 | -x := 1 2 | not x := 1 | ^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__named_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__named_expression.py.snap index 49a4e5362b..c637ec5f6b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__named_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield__named_expression.py.snap @@ -126,4 +126,3 @@ Module( 3 | 4 | yield 1, x := 2, 3 | ^^ Syntax Error: Expected `,`, found `:=` - | 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 4238df7e8c..88317ef0ad 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 @@ -123,7 +123,6 @@ Module( 3 | 4 | yield *x and y, z | ^^^^^^^ Syntax Error: Boolean expression cannot be used here - | ## Semantic Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__starred_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__starred_expression.py.snap index 719d563ed2..b8f1723b79 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__starred_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__starred_expression.py.snap @@ -101,4 +101,3 @@ Module( 3 | yield from *x 4 | yield from *x, y | ^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__unparenthesized.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__unparenthesized.py.snap index d521191aa7..4ce0931063 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__unparenthesized.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__yield_from__unparenthesized.py.snap @@ -174,4 +174,3 @@ Module( 8 | # vvvvvvvvvvvvv 9 | yield from (x, *x and y) | ^^^^^^^ Syntax Error: Boolean expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_conversion_follows_exclamation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_conversion_follows_exclamation.py.snap index 63c3670334..aef6b79cbf 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_conversion_follows_exclamation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_conversion_follows_exclamation.py.snap @@ -176,7 +176,6 @@ Module( 2 | t"{x! s}" 3 | f"{x! z}" | ^ Syntax Error: f-string: conversion type must come right after the exclamation mark - | | @@ -184,4 +183,3 @@ Module( 2 | t"{x! s}" 3 | f"{x! z}" | ^ Syntax Error: f-string: invalid conversion character - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap index f18251146b..68b565cf3b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap @@ -121,4 +121,3 @@ Module( 1 | f"{}" 2 | f"{ }" | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap index a5603a2dd3..2086898197 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap @@ -66,4 +66,3 @@ Module( | 1 | f"{x!z}" | ^ Syntax Error: f-string: invalid conversion character - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap index f69dee9e5c..a2ad5f30d0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap @@ -121,4 +121,3 @@ Module( 1 | f"{x!123}" 2 | f"{x!'a'}" | ^^^ Syntax Error: f-string: invalid conversion character - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap index 20091bbe02..56eacd2d90 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap @@ -226,4 +226,3 @@ Module( 3 | f"{*x and y}" 4 | f"{*yield x}" | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap index 2f5d767448..97f372aa56 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap @@ -106,22 +106,18 @@ Module( | 1 | f"{lambda x: x}" | ^^ Syntax Error: Expected an expression - | | 1 | f"{lambda x: x}" | ^^^^^^^^^ Syntax Error: f-string: lambda expressions are not allowed without parentheses - | | 1 | f"{lambda x: x}" | ^^ Syntax Error: f-string: expecting `}` - | | 1 | f"{lambda x: x}" | ^ Syntax Error: Expected an element of or the end of the f-string - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap index 3fa3b46130..acff579347 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap @@ -297,4 +297,3 @@ Module( 4 | f"{" 5 | f"""{""" | ^^^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap index ac1d7c98f4..1ac023ab76 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap @@ -155,4 +155,3 @@ Module( 1 | f"hello {x:" 2 | f"hello {x:.3f" | ^ Syntax Error: f-string: expecting `}` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_iter_unpack_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_iter_unpack_py38.py.snap index 36e244899b..01aa0c3e56 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_iter_unpack_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_iter_unpack_py38.py.snap @@ -235,7 +235,6 @@ Module( 3 | for x in a, *b: ... 4 | for x in *a, *b: ... | ^^ Syntax Error: Cannot use iterable unpacking in `for` statements on Python 3.8 (syntax was added in Python 3.9) - | | @@ -243,4 +242,3 @@ Module( 3 | for x in a, *b: ... 4 | for x in *a, *b: ... | ^^ Syntax Error: Cannot use iterable unpacking in `for` statements on Python 3.8 (syntax was added in Python 3.9) - | 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 ab00df4afe..3479a2f1cd 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 @@ -193,7 +193,6 @@ Module( 2 | for x in yield a: ... 3 | for target in x := 1: ... | ^^ Syntax Error: Expected `:`, found `:=` - | | @@ -201,7 +200,6 @@ Module( 2 | for x in yield a: ... 3 | for target in x := 1: ... | ^ Syntax Error: Invalid annotated assignment target - | ## Semantic Syntax Errors 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 88050de12e..c8f400ccc8 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 @@ -508,7 +508,6 @@ Module( 6 | for yield x in y: ... 7 | for [x, 1, y, *["a"]] in z: ... | ^ Syntax Error: Invalid assignment target - | | @@ -516,7 +515,6 @@ Module( 6 | for yield x in y: ... 7 | for [x, 1, y, *["a"]] in z: ... | ^^^ Syntax Error: Invalid assignment target - | ## Semantic Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_binary_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_binary_expr.py.snap index 72cac3d156..dee71c15aa 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_binary_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_binary_expr.py.snap @@ -379,4 +379,3 @@ Module( 5 | for not x in y: ... 6 | for x | y in z: ... | ^^^^^ Syntax Error: Invalid assignment target - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_in_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_in_keyword.py.snap index d219ca921c..76ecd5aca0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_in_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_invalid_target_in_keyword.py.snap @@ -493,4 +493,3 @@ Module( 5 | for [x in y, z] in iter: ... 6 | for {x in y, z} in iter: ... | ^^^^^^^^^^^ Syntax Error: Invalid assignment target - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_in_keyword.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_in_keyword.py.snap index a2bf0f699d..256fc1f8bd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_in_keyword.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_in_keyword.py.snap @@ -103,4 +103,3 @@ Module( 1 | for a b: ... 2 | for a: ... | ^ Syntax Error: Expected `in`, found `:` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_target.py.snap index 6484c523fd..184d01f31c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@for_stmt_missing_target.py.snap @@ -57,4 +57,3 @@ Module( | 1 | for in x: ... | ^^ Syntax Error: Expected an identifier - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_dotted_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_dotted_names.py.snap index cae496aa76..e64b251ac4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_dotted_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_dotted_names.py.snap @@ -188,7 +188,6 @@ Module( 2 | from x import a.b 3 | from x import a, b.c, d, e.f, g | ^ Syntax Error: Expected `,`, found `.` - | | @@ -196,4 +195,3 @@ Module( 2 | from x import a.b 3 | from x import a, b.c, d, e.f, g | ^ Syntax Error: Expected `,`, found `.` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_empty_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_empty_names.py.snap index c4479ebffd..3940178886 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_empty_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_empty_names.py.snap @@ -85,7 +85,6 @@ Module( 2 | from x import () 3 | from x import ,, | ^ Syntax Error: Expected an import name - | | @@ -93,7 +92,6 @@ Module( 2 | from x import () 3 | from x import ,, | ^ Syntax Error: Expected an import name - | | @@ -101,4 +99,3 @@ Module( 2 | from x import () 3 | from x import ,, | ^ Syntax Error: Expected one or more symbol names after import - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_module.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_module.py.snap index c5a59ee6c7..6dcd1dcea0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_module.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_missing_module.py.snap @@ -58,4 +58,3 @@ Module( 1 | from 2 | from import x | ^^^^^^ Syntax Error: Expected a module name - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_parenthesized_star.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_parenthesized_star.py.snap index c58ee28937..4b7d807a7f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_parenthesized_star.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_parenthesized_star.py.snap @@ -46,4 +46,3 @@ Module( | 1 | from x import (*) | ^^ Syntax Error: Star import cannot be parenthesized - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_star_with_other_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_star_with_other_names.py.snap index fc9bb538a6..57dc8ea3c4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_star_with_other_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_star_with_other_names.py.snap @@ -221,4 +221,3 @@ Module( 3 | from x import *, a as b 4 | from x import *, *, a | ^^^^^^^ Syntax Error: Star import must be the only import - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_unparenthesized_trailing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_unparenthesized_trailing_comma.py.snap index 01701ed451..996f94b7d1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_unparenthesized_trailing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@from_import_unparenthesized_trailing_comma.py.snap @@ -134,4 +134,3 @@ Module( 2 | from a import b as c, 3 | from a import b, c, | ^ Syntax Error: Trailing comma not allowed - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_empty_body.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_empty_body.py.snap index 8774db9904..151d9f4b39 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_empty_body.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_empty_body.py.snap @@ -113,4 +113,3 @@ Module( 2 | def foo() -> int: 3 | x = 42 | ^ Syntax Error: Expected an indented block after function definition - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap index a3bef4f4b9..a807f4cf4f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap @@ -200,7 +200,6 @@ Module( 2 | def foo() -> (*int): ... 3 | def foo() -> yield x: ... | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | ## Semantic Syntax Errors @@ -210,4 +209,3 @@ Module( 2 | def foo() -> (*int): ... 3 | def foo() -> yield x: ... | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_identifier.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_identifier.py.snap index 1ee0f8c743..51228a1638 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_identifier.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_identifier.py.snap @@ -112,4 +112,3 @@ Module( 1 | def (): ... 2 | def () -> int: ... | ^ Syntax Error: Expected an identifier - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_return_type.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_return_type.py.snap index 36593a9900..acfd61b924 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_return_type.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_missing_return_type.py.snap @@ -57,4 +57,3 @@ Module( | 1 | def foo() -> : ... | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_parameter_list.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_parameter_list.py.snap index 9efb6d3fac..9d71866cf6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_parameter_list.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unclosed_parameter_list.py.snap @@ -255,11 +255,9 @@ Module( 4 | def foo(a: int, b: str 5 | x = 10 | ^ Syntax Error: Expected `,`, found name - | | 4 | def foo(a: int, b: str 5 | x = 10 | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unparenthesized_return_types.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unparenthesized_return_types.py.snap index fc72f4209f..e943085fc1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unparenthesized_return_types.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_unparenthesized_return_types.py.snap @@ -149,4 +149,3 @@ Module( 1 | def foo() -> int,: ... 2 | def foo() -> int, str: ... | ^^^^^^^^ Syntax Error: Multiple return types must be parenthesized - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap index 5ab2a0d364..020e464de8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_type_params_py311.py.snap @@ -123,7 +123,6 @@ Module( 2 | def foo[T](): ... 3 | def foo[](): ... | ^ Syntax Error: Type parameter list cannot be empty - | ## Unsupported Syntax Errors @@ -141,4 +140,3 @@ Module( 2 | def foo[T](): ... 3 | def foo[](): ... | ^^ Syntax Error: Cannot use type parameter lists on Python 3.11 (syntax was added in Python 3.12) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_empty.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_empty.py.snap index 5971aa62c7..92375a3dfb 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_empty.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_empty.py.snap @@ -26,4 +26,3 @@ Module( | 1 | global | ^ Syntax Error: Global statement must have at least one name - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_expression.py.snap index c80bc04505..018d9049ee 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_expression.py.snap @@ -54,4 +54,3 @@ Module( | 1 | global x + 1 | ^ Syntax Error: Simple statements must be separated by newlines or semicolons - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_trailing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_trailing_comma.py.snap index 6891697042..22cbfb0016 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_trailing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@global_stmt_trailing_comma.py.snap @@ -83,4 +83,3 @@ Module( 2 | global x, 3 | global x, y, | ^ Syntax Error: Trailing comma not allowed - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_empty_body.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_empty_body.py.snap index 1d74c36f48..ad3fe7c4ca 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_empty_body.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_empty_body.py.snap @@ -66,4 +66,3 @@ Module( 1 | if True: 2 | 1 + 1 | ^ Syntax Error: Expected an indented block after `if` statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_invalid_test_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_invalid_test_expr.py.snap index c06836fcdb..1840201849 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_invalid_test_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_invalid_test_expr.py.snap @@ -145,4 +145,3 @@ Module( 2 | if yield x: ... 3 | if yield from x: ... | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_test.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_test.py.snap index 421ce18c82..f0f87f8a4f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_test.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_missing_test.py.snap @@ -48,4 +48,3 @@ Module( | 1 | if : ... | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_misspelled_elif.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_misspelled_elif.py.snap index 4a0f4d0afa..3a360a8066 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_misspelled_elif.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@if_stmt_misspelled_elif.py.snap @@ -127,4 +127,3 @@ Module( 5 | else: 6 | pass | ^^^^ Syntax Error: Unexpected indentation - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_alias_missing_asname.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_alias_missing_asname.py.snap index 873a39cf58..47f3fd9311 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_alias_missing_asname.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_alias_missing_asname.py.snap @@ -38,4 +38,3 @@ Module( | 1 | import x as | ^ Syntax Error: Expected symbol after `as` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap index cc5f028404..c2e12781c9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_from_star.py.snap @@ -233,7 +233,6 @@ Module( 7 | def f3(): 8 | from module import *, * | ^^^^ Syntax Error: Star import must be the only import - | ## Semantic Syntax Errors @@ -272,4 +271,3 @@ Module( 7 | def f3(): 8 | from module import *, * | ^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: `from module import *` only allowed at module level - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_empty.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_empty.py.snap index 3743669828..6f254cafc4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_empty.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_empty.py.snap @@ -27,4 +27,3 @@ Module( | 1 | import | ^ Syntax Error: Expected one or more symbol names after import - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_parenthesized_names.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_parenthesized_names.py.snap index d410339d7d..e4acf74b90 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_parenthesized_names.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_parenthesized_names.py.snap @@ -89,4 +89,3 @@ Module( 1 | import (a) 2 | import (a, b) | ^ Syntax Error: Expected one or more symbol names after import - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_star_import.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_star_import.py.snap index 8abd54a19e..78b584adc6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_star_import.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_star_import.py.snap @@ -121,18 +121,15 @@ Module( 1 | import * 2 | import x, *, y | ^ Syntax Error: Trailing comma not allowed - | | 1 | import * 2 | import x, *, y | ^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | 1 | import * 2 | import x, *, y | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_trailing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_trailing_comma.py.snap index 7114dbf8d2..37a9842027 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_trailing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@import_stmt_trailing_comma.py.snap @@ -71,4 +71,3 @@ Module( 1 | import , 2 | import x, y, | ^ Syntax Error: Trailing comma not allowed - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@incomplete_attribute_before_for_in_delimiter.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@incomplete_attribute_before_for_in_delimiter.py.snap index c8b5eb4ab3..b59353d9ae 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@incomplete_attribute_before_for_in_delimiter.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@incomplete_attribute_before_for_in_delimiter.py.snap @@ -398,4 +398,3 @@ Module( 5 | [item for item. in xs] 6 | for item. in xs: ... | ^^ Syntax Error: Expected an identifier - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_class.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_class.py.snap index 3c8495804d..9876d8216b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_class.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_class.py.snap @@ -572,4 +572,3 @@ Module( 6 | class M[T]((await 1)): ... 7 | class N[T: (await 1)]: ... | ^^^^^^^ Syntax Error: await expression cannot be used within a TypeVar bound - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap index 682cbac08f..9ca7f7bbef 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap @@ -1805,4 +1805,3 @@ Module( 19 | def v[*Ts = (await 1)](): ... # await in TypeVarTuple default 20 | def w[**Ts = (await 1)](): ... # await in ParamSpec default | ^^^^^^^ Syntax Error: await expression cannot be used within a ParamSpec default - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function_py314.py.snap index 132507c7c7..ddf812502d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function_py314.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function_py314.py.snap @@ -657,4 +657,3 @@ Module( 10 | def f() -> (await 1): ... 11 | def g(arg: (await 1)): ... | ^^^^^^^ Syntax Error: await expression cannot be used within a type annotation - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_py314.py.snap index f3fdd11ffe..80f657c133 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_py314.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_py314.py.snap @@ -236,4 +236,3 @@ Module( 6 | async def outer(): 7 | d: (await 1) | ^^^^^^^ Syntax Error: await expression cannot be used within a type annotation - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_type_alias.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_type_alias.py.snap index 5a05ef64f6..a017409127 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_type_alias.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_type_alias.py.snap @@ -488,4 +488,3 @@ Module( 7 | type Y[T: (await 1)] = int # await in bound 8 | type Y = (await 1) # await in value | ^^^^^^^ Syntax Error: await expression cannot be used within a type alias - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_byte_literal.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_byte_literal.py.snap index a330b1ce96..0de2a88389 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_byte_literal.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_byte_literal.py.snap @@ -120,4 +120,3 @@ Module( 2 | rb"a𝐁c123" 3 | b"""123a𝐁c""" | ^ Syntax Error: bytes can only contain ASCII literal characters - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_del_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_del_target.py.snap index 5834d7311a..cc7fbd303a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_del_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_del_target.py.snap @@ -254,7 +254,6 @@ Module( 3 | del {'x', 'y'} 4 | del None, True, False, 1, 1.0, "abc" | ^^^^ Syntax Error: Invalid delete target - | | @@ -262,7 +261,6 @@ Module( 3 | del {'x', 'y'} 4 | del None, True, False, 1, 1.0, "abc" | ^^^^ Syntax Error: Invalid delete target - | | @@ -270,7 +268,6 @@ Module( 3 | del {'x', 'y'} 4 | del None, True, False, 1, 1.0, "abc" | ^^^^^ Syntax Error: Invalid delete target - | | @@ -278,7 +275,6 @@ Module( 3 | del {'x', 'y'} 4 | del None, True, False, 1, 1.0, "abc" | ^ Syntax Error: Invalid delete target - | | @@ -286,7 +282,6 @@ Module( 3 | del {'x', 'y'} 4 | del None, True, False, 1, 1.0, "abc" | ^^^ Syntax Error: Invalid delete target - | | @@ -294,4 +289,3 @@ Module( 3 | del {'x', 'y'} 4 | del None, True, False, 1, 1.0, "abc" | ^^^^^ Syntax Error: Invalid delete target - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap index 0d2f651c21..fc74da89bf 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap @@ -101,4 +101,3 @@ Module( 1 | f'hello \N{INVALID} world' 2 | f"""hello \N{INVALID} world""" | ^^^^^^^ Syntax Error: Got unexpected unicode - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap index ec4c8f6c56..121735f806 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_future_feature.py.snap @@ -138,7 +138,6 @@ Module( 2 | from __future__ import annotations, invalid_feature 3 | from __future__ import invalid_feature_1, invalid_feature_2 | ^^^^^^^^^^^^^^^^^ Syntax Error: Future feature `invalid_feature_1` is not defined - | | @@ -146,4 +145,3 @@ Module( 2 | from __future__ import annotations, invalid_feature 3 | from __future__ import invalid_feature_1, invalid_feature_2 | ^^^^^^^^^^^^^^^^^ Syntax Error: Future feature `invalid_feature_2` is not defined - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_string_literal.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_string_literal.py.snap index 72cdf3cf6a..4d20a34438 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_string_literal.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_string_literal.py.snap @@ -81,4 +81,3 @@ Module( 1 | 'hello \N{INVALID} world' 2 | """hello \N{INVALID} world""" | ^^^^^^^ Syntax Error: Got unexpected unicode - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_1.py.snap index aa33df6cbc..6f81271973 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_1.py.snap @@ -81,4 +81,3 @@ Module( 2 | with (a, ?b) 3 | ? | ^ Syntax Error: Expected an indented block after `with` statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_2.py.snap index 424b43d107..4f1baa69a7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ipython_help_escape_command_error_recovery_2.py.snap @@ -70,11 +70,9 @@ Module( 2 | with (a, ?b 3 | ? | ^ Syntax Error: Expected `,`, found `?` - | | 2 | with (a, ?b 3 | ? | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_return_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_return_py37.py.snap index cc4ffef040..34d46e2a2b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_return_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_return_py37.py.snap @@ -160,4 +160,3 @@ Module( 2 | rest = (4, 5, 6) 3 | def f(): return 1, 2, 3, *rest | ^^^^^ Syntax Error: Cannot use iterable unpacking in return statements on Python 3.7 (syntax was added in Python 3.8) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_yield_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_yield_py37.py.snap index 09bb355569..4110c28f15 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_yield_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@iter_unpack_yield_py37.py.snap @@ -285,4 +285,3 @@ Module( 3 | def g(): yield 1, 2, 3, *rest 4 | def h(): yield 1, (yield 2, *rest), 3 | ^^^^^ Syntax Error: Cannot use iterable unpacking in yield expressions on Python 3.7 (syntax was added in Python 3.8) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_starred_expr.py.snap index a438ef9c46..c06d9a8abc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_starred_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_starred_expr.py.snap @@ -308,4 +308,3 @@ Module( 3 | lambda x: *y, z 4 | lambda x: *y and z | ^^^^^^^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_yield_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_yield_expr.py.snap index 50eb38666e..d21b80f6d6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_yield_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lambda_body_with_yield_expr.py.snap @@ -135,4 +135,3 @@ Module( 1 | lambda x: yield y 2 | lambda x: yield from y | ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_context_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_context_py315.py.snap index 9d2f1d49f4..67abc8a22c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_context_py315.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_context_py315.py.snap @@ -375,4 +375,3 @@ Module( 22 | class Inner: 23 | lazy import json | ^^^^^^^^^^^^^^^^ Syntax Error: lazy import not allowed inside classes - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_from_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_from_py315.py.snap index 146d091c51..3e7266d918 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_from_py315.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_invalid_from_py315.py.snap @@ -145,11 +145,9 @@ Module( 5 | def func(): 6 | lazy from sys import * | ^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: lazy from ... import not allowed inside functions - | | 5 | def func(): 6 | lazy from sys import * | ^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: `from sys import *` only allowed at module level - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_stmt_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_stmt_py314.py.snap index c3e6a3fedf..14f80ce4ad 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_stmt_py314.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@lazy_import_stmt_py314.py.snap @@ -75,4 +75,3 @@ Module( 2 | lazy import foo 3 | lazy from bar import baz | ^^^^ Syntax Error: Cannot use `lazy` import statement on Python 3.14 (syntax was added in Python 3.15) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expect_indented_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expect_indented_block.py.snap index 60781de936..f8ff37f51a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expect_indented_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expect_indented_block.py.snap @@ -63,11 +63,9 @@ Module( 1 | match foo: 2 | case _: ... | ^^^^ Syntax Error: Expected an indented block after `match` statement - | | 1 | match foo: 2 | case _: ... | ^ Syntax Error: Expected dedent, found end of file - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expected_case_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expected_case_block.py.snap index 8aaaa11d01..561ee48a26 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expected_case_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_expected_case_block.py.snap @@ -126,8 +126,8 @@ Module( | 1 | match x: 2 | x = 1 - | ^ Syntax Error: Expected a statement 3 | match x: + | ^ Syntax Error: Expected a statement 4 | match y: 5 | case _: ... | @@ -146,4 +146,3 @@ Module( 4 | match y: 5 | case _: ... | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_guard_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_guard_expr.py.snap index 967dfc2d2e..5ae960f29c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_guard_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_invalid_guard_expr.py.snap @@ -236,4 +236,3 @@ Module( 5 | match x: 6 | case y if yield x: ... | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_guard_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_guard_expr.py.snap index bf66acae17..880d99610c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_guard_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_guard_expr.py.snap @@ -69,4 +69,3 @@ Module( 1 | match x: 2 | case y if: ... | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_pattern.py.snap index 84fad76972..17247bca62 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_missing_pattern.py.snap @@ -69,4 +69,3 @@ Module( 1 | match x: 2 | case : ... | ^ Syntax Error: Expected a pattern - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap index 7888bcd48e..7d49a7f558 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@match_stmt_no_newline_before_case.py.snap @@ -62,10 +62,8 @@ Module( | 1 | match foo: case _: ... | ^^^^ Syntax Error: Expected newline, found `case` - | | 1 | match foo: case _: ... | ^ Syntax Error: Expected dedent, found end of file - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap index 6ac75db9d5..829f518bed 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap @@ -197,4 +197,3 @@ Module( 2 | f'first' b'second' 3 | 'first' f'second' b'third' | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Syntax Error: Bytes literal cannot be mixed with non-bytes literals - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap index 3aed113557..234f5001e0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_assignment_in_case_pattern.py.snap @@ -819,4 +819,3 @@ Module( 9 | case [x] | {1: x} | Class(y=x, z=x): ... # MatchOr 10 | case x as x: ... # MatchAs | ^ Syntax Error: multiple assignments to name `x` in pattern - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_clauses_on_same_line.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_clauses_on_same_line.py.snap index be571b2cd2..cd8e98be1c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_clauses_on_same_line.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_clauses_on_same_line.py.snap @@ -410,7 +410,6 @@ Module( 5 | try: pass except exc: pass else: pass finally: pass 6 | try: pass; except exc: pass; else: pass; finally: pass | ^^^^^^ Syntax Error: Expected newline, found `except` - | | @@ -418,7 +417,6 @@ Module( 5 | try: pass except exc: pass else: pass finally: pass 6 | try: pass; except exc: pass; else: pass; finally: pass | ^^^^ Syntax Error: Expected newline, found `else` - | | @@ -426,4 +424,3 @@ Module( 5 | try: pass except exc: pass else: pass finally: pass 6 | try: pass; except exc: pass; else: pass; finally: pass | ^^^^^^^ Syntax Error: Expected newline, found `finally` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_assignment_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_assignment_target.py.snap index 01b96d2fc4..fe2d8393ce 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_assignment_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_assignment_target.py.snap @@ -509,7 +509,6 @@ Module( 4 | [*a, *b, c] = (1, 2, 3) 5 | (*a, *b, (*c, *d)) = (1, 2) | ^^^^^^^^^^^^^^^^^^ Syntax Error: Two starred expressions in assignment - | | @@ -517,4 +516,3 @@ Module( 4 | [*a, *b, c] = (1, 2, 3) 5 | (*a, *b, (*c, *d)) = (1, 2) | ^^^^^^^^ Syntax Error: Two starred expressions in assignment - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_names_in_sequence_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_names_in_sequence_pattern.py.snap index 62a1d0db00..c289a7895a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_names_in_sequence_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@multiple_starred_names_in_sequence_pattern.py.snap @@ -102,11 +102,9 @@ Module( 1 | match subject: 2 | case *first, *second, *third: ... | ^^^^^^^ Syntax Error: multiple starred names in sequence pattern - | | 1 | match subject: 2 | case *first, *second, *third: ... | ^^^^^^ Syntax Error: multiple starred names in sequence pattern - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap index ab0bcdf9ca..961dcf36e1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice.py.snap @@ -266,7 +266,6 @@ Module( 3 | lst[1:x:=1] 4 | lst[1:3:x:=1] | ^^ Syntax Error: Expected `]`, found `:=` - | | @@ -274,7 +273,6 @@ Module( 3 | lst[1:x:=1] 4 | lst[1:3:x:=1] | ^ Syntax Error: Expected a statement - | | @@ -282,4 +280,3 @@ Module( 3 | lst[1:x:=1] 4 | lst[1:3:x:=1] | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice_parse_error.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice_parse_error.py.snap index e282dc0cab..34c8840748 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice_parse_error.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@named_expr_slice_parse_error.py.snap @@ -92,4 +92,3 @@ Module( 2 | # before 3.9, only emit the parse error, not the unsupported syntax error 3 | lst[x:=1:-1] | ^^^^ Syntax Error: Unparenthesized named expression cannot be used here - | 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 94435420da..ff285092c2 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 @@ -937,4 +937,3 @@ Module( 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 inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_quote_in_format_spec_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_quote_in_format_spec_py312.py.snap index 7eec20f80b..187240a204 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_quote_in_format_spec_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@nested_quote_in_format_spec_py312.py.snap @@ -89,4 +89,3 @@ Module( 1 | # parse_options: {"target-version": "3.12"} 2 | f"{1:""}" # this is a ParseError on all versions | ^ Syntax Error: f-string: expecting `}` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@node_range_with_gaps.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@node_range_with_gaps.py.snap index 361fe1288b..a71728ff9d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@node_range_with_gaps.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@node_range_with_gaps.py.snap @@ -125,11 +125,9 @@ Module( 2 | def bar(): ... 3 | def baz | ^ Syntax Error: Expected `(`, found newline - | | 2 | def bar(): ... 3 | def baz | ^ Syntax Error: Expected `)`, found end of file - | 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 index ecfbbe06d2..5f81d3505d 100644 --- 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 @@ -58,4 +58,3 @@ Module( 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 e7c4c552bd..cba504e096 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 @@ -53,4 +53,3 @@ Module( 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 f82316bd46..ae92fb5c2b 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 @@ -81,4 +81,3 @@ Module( 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 d749245b98..5060d24003 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 @@ -112,4 +112,3 @@ Module( 3 | nonlocal x, 4 | nonlocal x, y, | ^ Syntax Error: Trailing comma not allowed - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_annotation.py.snap index 16efd0233e..97f3765313 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_annotation.py.snap @@ -135,4 +135,3 @@ Module( 1 | def foo(x:): ... 2 | def foo(x:,): ... | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_default.py.snap index fcb763f0a1..677e725f3a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_missing_default.py.snap @@ -144,4 +144,3 @@ Module( 1 | def foo(x=): ... 2 | def foo(x: int = ): ... | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap index d6b1a5944e..b81e6a9903 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap @@ -256,7 +256,6 @@ Module( 2 | def foo(arg: yield int): ... 3 | def foo(arg: x := int): ... | ^^ Syntax Error: Expected `,`, found `:=` - | ## Semantic Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_default.py.snap index fcc9686845..461ef54faa 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_default.py.snap @@ -248,4 +248,3 @@ Module( 2 | def foo(x=(*int)): ... 3 | def foo(x=yield y): ... | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_star_annotation_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_star_annotation_py310.py.snap index 0add20fe0e..20e6c382e4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_star_annotation_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_star_annotation_py310.py.snap @@ -85,4 +85,3 @@ Module( 1 | # parse_options: {"target-version": "3.10"} 2 | def foo(*args: *Ts): ... | ^^^ Syntax Error: Cannot use star annotation on Python 3.10 (syntax was added in Python 3.11) - | 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 54ade04fb2..cf8ca302d4 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 @@ -160,28 +160,23 @@ Module( | 1 | def foo(a, a=10, *a, a, a: str, **a): ... | ^ Syntax Error: Duplicate parameter "a" - | | 1 | def foo(a, a=10, *a, a, a: str, **a): ... | ^ Syntax Error: Duplicate parameter "a" - | | 1 | def foo(a, a=10, *a, a, a: str, **a): ... | ^ Syntax Error: Duplicate parameter "a" - | | 1 | def foo(a, a=10, *a, a, a: str, **a): ... | ^ Syntax Error: Duplicate parameter "a" - | | 1 | def foo(a, a=10, *a, a, a: str, **a): ... | ^ Syntax Error: Duplicate parameter "a" - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_expected_after_star_separator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_expected_after_star_separator.py.snap index f343b562c6..3a61d03ad0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_expected_after_star_separator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_expected_after_star_separator.py.snap @@ -290,4 +290,3 @@ Module( 4 | def foo(a, *,): ... 5 | def foo(*, **kwargs): ... | ^^^^^^^^ Syntax Error: Expected one or more keyword parameter after `*` separator - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_follows_var_keyword_param.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_follows_var_keyword_param.py.snap index 930431d525..f643b398d0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_follows_var_keyword_param.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_follows_var_keyword_param.py.snap @@ -121,28 +121,23 @@ Module( | 1 | def foo(**kwargs, a, /, b=10, *, *args): ... | ^ Syntax Error: Parameter cannot follow var-keyword parameter - | | 1 | def foo(**kwargs, a, /, b=10, *, *args): ... | ^ Syntax Error: Parameter cannot follow var-keyword parameter - | | 1 | def foo(**kwargs, a, /, b=10, *, *args): ... | ^ Syntax Error: Parameter cannot follow var-keyword parameter - | | 1 | def foo(**kwargs, a, /, b=10, *, *args): ... | ^ Syntax Error: Parameter cannot follow var-keyword parameter - | | 1 | def foo(**kwargs, a, /, b=10, *, *args): ... | ^ Syntax Error: Parameter cannot follow var-keyword parameter - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_kwarg_after_star_separator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_kwarg_after_star_separator.py.snap index 589d8905af..3c76f98da4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_kwarg_after_star_separator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_kwarg_after_star_separator.py.snap @@ -68,4 +68,3 @@ Module( | 1 | def foo(*, **kwargs): ... | ^^^^^^^^ Syntax Error: Expected one or more keyword parameter after `*` separator - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_kwargs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_kwargs.py.snap index 156a962746..b5c0c9d0aa 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_kwargs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_kwargs.py.snap @@ -84,4 +84,3 @@ Module( | 1 | def foo(a, **kwargs1, **kwargs2): ... | ^^ Syntax Error: Parameter cannot follow var-keyword parameter - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_slash_separator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_slash_separator.py.snap index 21a9f28820..8fe7404b23 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_slash_separator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_slash_separator.py.snap @@ -182,4 +182,3 @@ Module( 1 | def foo(a, /, /, b): ... 2 | def foo(a, /, b, c, /): ... | ^ Syntax Error: Only one '/' separator allowed - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_star_separator.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_star_separator.py.snap index 4daa7f27f2..642996446c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_star_separator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_star_separator.py.snap @@ -182,4 +182,3 @@ Module( 1 | def foo(a, *, *, b): ... 2 | def foo(a, *, b, c, *): ... | ^ Syntax Error: Only one '*' separator allowed - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_varargs.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_varargs.py.snap index 8fb783b1c1..a60de5d69e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_varargs.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_multiple_varargs.py.snap @@ -296,4 +296,3 @@ Module( 3 | def foo(a, *args1, *args2, b): ... 4 | def foo(a, *args1, b, c, *args2): ... | ^^^^^^ Syntax Error: Only one '*' parameter allowed - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_no_arg_before_slash.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_no_arg_before_slash.py.snap index 7444c8148a..f3fc1117e0 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_no_arg_before_slash.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_no_arg_before_slash.py.snap @@ -119,4 +119,3 @@ Module( 1 | def foo(/): ... 2 | def foo(/, a): ... | ^ Syntax Error: Position-only parameter separator not allowed as first parameter - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_non_default_after_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_non_default_after_default.py.snap index 4d23c7dde9..6dfcec4878 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_non_default_after_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_non_default_after_default.py.snap @@ -122,10 +122,8 @@ Module( | 1 | def foo(a=10, b, c: int): ... | ^ Syntax Error: Parameter without a default cannot follow a parameter with a default - | | 1 | def foo(a=10, b, c: int): ... | ^^^^^^ Syntax Error: Parameter without a default cannot follow a parameter with a default - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_after_slash.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_after_slash.py.snap index 2015f10090..485d7d49ba 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_after_slash.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_after_slash.py.snap @@ -347,4 +347,3 @@ Module( 3 | def foo(a, *, /, b): ... 4 | def foo(a, *, b, c, /, d): ... | ^ Syntax Error: '/' parameter must appear before '*' parameter - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_separator_after_star_param.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_separator_after_star_param.py.snap index fb13bd6095..6a75f864f7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_separator_after_star_param.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_star_separator_after_star_param.py.snap @@ -204,4 +204,3 @@ Module( 1 | def foo(a, *args, *, b): ... 2 | def foo(a, *args, b, c, *): ... | ^ Syntax Error: Keyword-only parameter separator not allowed after '*' parameter - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_keyword_with_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_keyword_with_default.py.snap index 87adc006a3..6a172bd47c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_keyword_with_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_keyword_with_default.py.snap @@ -166,22 +166,18 @@ Module( | 1 | def foo(a, **kwargs={'b': 1, 'c': 2}): ... | ^ Syntax Error: Parameter with `*` or `**` cannot have default value - | | 1 | def foo(a, **kwargs={'b': 1, 'c': 2}): ... | ^ Syntax Error: Expected `)`, found `{` - | | 1 | def foo(a, **kwargs={'b': 1, 'c': 2}): ... | ^ Syntax Error: Expected newline, found `)` - | | 1 | def foo(a, **kwargs={'b': 1, 'c': 2}): ... | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_positional_with_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_positional_with_default.py.snap index e965d4a0b4..076fbfe23b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_positional_with_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_var_positional_with_default.py.snap @@ -118,22 +118,18 @@ Module( | 1 | def foo(a, *args=(1, 2)): ... | ^ Syntax Error: Parameter with `*` or `**` cannot have default value - | | 1 | def foo(a, *args=(1, 2)): ... | ^ Syntax Error: Expected `)`, found `(` - | | 1 | def foo(a, *args=(1, 2)): ... | ^ Syntax Error: Expected newline, found `)` - | | 1 | def foo(a, *args=(1, 2)): ... | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_context_manager_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_context_manager_py38.py.snap index c9fce5f081..3185cb6e32 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_context_manager_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_context_manager_py38.py.snap @@ -222,4 +222,3 @@ Module( 3 | with (foo, bar as y): ... 4 | with (foo as x, bar): ... | ^ Syntax Error: Cannot use parentheses within a `with` statement on Python 3.8 (syntax was added in Python 3.9) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap index 149fba6e35..6a2b1560fd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@parenthesized_kwarg_py38.py.snap @@ -180,4 +180,3 @@ Module( 3 | f((a) = 1) 4 | f( ( a ) = 1) | ^^^^^ Syntax Error: Cannot use parenthesized keyword argument name on Python 3.8 (syntax was removed in Python 3.8) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap index 85b7abc8b4..b1f3dfc4fc 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap @@ -1078,7 +1078,6 @@ Module( 14 | f"""{f"""{x}"""}""" # mark the whole triple quote 15 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) - | | @@ -1086,7 +1085,6 @@ Module( 14 | f"""{f"""{x}"""}""" # mark the whole triple quote 15 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) - | | @@ -1094,7 +1092,6 @@ Module( 14 | f"""{f"""{x}"""}""" # mark the whole triple quote 15 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) - | | @@ -1102,4 +1099,3 @@ Module( 14 | f"""{f"""{x}"""}""" # mark the whole triple quote 15 | f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_nested_interpolation_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_nested_interpolation_py311.py.snap index 5699059e91..51ca8d4dda 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_nested_interpolation_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_nested_interpolation_py311.py.snap @@ -228,4 +228,3 @@ Module( 3 | f'{1: abcd "{'aa'}" }' 4 | f'{1: abcd "{"\n"}" }' | ^ Syntax Error: Cannot use an escape sequence (backslash) in f-strings on Python 3.11 (syntax was added in Python 3.12) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_invalid_dict_unpacking_comprehensions_py315.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_invalid_dict_unpacking_comprehensions_py315.py.snap index f4b8413ff6..cf6a44043b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_invalid_dict_unpacking_comprehensions_py315.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_invalid_dict_unpacking_comprehensions_py315.py.snap @@ -351,4 +351,3 @@ Module( 4 | {**k: v for k, v in items} 5 | {k: **v for k, v in items} | ^^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_unpacking_comprehensions_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_unpacking_comprehensions_py314.py.snap index f388ce2869..86297c504c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_unpacking_comprehensions_py314.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep_798_unpacking_comprehensions_py314.py.snap @@ -331,4 +331,3 @@ Module( 5 | (*x for x in y) 6 | f(*x for x in y) | ^^ Syntax Error: Cannot use iterable unpacking in a generator expression on Python 3.14 (syntax was added in Python 3.15) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap index 5328fcf7dd..5a0accedf7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pos_only_py37.py.snap @@ -299,7 +299,6 @@ Module( 4 | def foo(a, *args, /, b): ... 5 | def foo(a, //): ... | ^^ Syntax Error: Expected `,`, found `//` - | ## Unsupported Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_from_without_exc.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_from_without_exc.py.snap index 4e9bad06a4..a26ddcf6ec 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_from_without_exc.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_from_without_exc.py.snap @@ -59,4 +59,3 @@ Module( 1 | raise from exc 2 | raise from None | ^^^^ Syntax Error: Exception missing in `raise` statement with cause - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_cause.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_cause.py.snap index c9559c557f..5554450d32 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_cause.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_cause.py.snap @@ -145,4 +145,3 @@ Module( 2 | raise x from yield y 3 | raise x from y := 1 | ^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_exc.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_exc.py.snap index 6886f22bad..02dab5618e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_exc.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_invalid_exc.py.snap @@ -118,4 +118,3 @@ Module( 2 | raise yield x 3 | raise x := 1 | ^^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_cause.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_cause.py.snap index 40dc442afd..8920563325 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_cause.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_cause.py.snap @@ -107,4 +107,3 @@ Module( 1 | raise x from y, 2 | raise x from y, z | ^^^^ Syntax Error: Unparenthesized tuple expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_exc.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_exc.py.snap index 34510782dc..249774dde9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_exc.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@raise_stmt_unparenthesized_tuple_exc.py.snap @@ -143,4 +143,3 @@ Module( 2 | raise x, y 3 | raise x, y from z | ^^^^ Syntax Error: Unparenthesized tuple expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap index 93b1439a58..9410e6f20a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap @@ -949,4 +949,3 @@ Module( 56 | def bar(): 57 | pass | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap index 2eb9515848..11e80e4bd9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token_mac_eol.py.snap @@ -112,12 +112,10 @@ Module( ## Errors | -1 | if call(foo, [a, b def bar(): pass - | ^^^ Syntax Error: Expected `]`, found `def` - | +1 | if call(foo, [a, b␍ def bar():␍ pass + | ^^^ Syntax Error: Expected `]`, found `def` | -1 | if call(foo, [a, b def bar(): pass +1 | if call(foo, [a, b␍ def bar():␍ pass | ^ Syntax Error: Expected `)`, found newline - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap index f0d3b3104c..c9aa9e7ce4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap @@ -484,25 +484,21 @@ Module( 11 | f'middle {'string':\\\ 12 | 'format spec'} | ^ Syntax Error: f-string: expecting `}` - | | 11 | f'middle {'string':\\\ 12 | 'format spec'} | ^^^^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | 11 | f'middle {'string':\\\ 12 | 'format spec'} | ^^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | 11 | f'middle {'string':\\\ 12 | 'format spec'} | ^^ Syntax Error: missing closing quote in string literal - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap index f58010350b..8fcddba72a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap @@ -77,14 +77,12 @@ Module( | ___________________________^ 6 | | y = 1 | |_____^ Syntax Error: f-string: unterminated triple-quoted string - | | 5 | f"""hello {x # comment 6 | y = 1 | ^ Syntax Error: f-string: expecting `}` - | | @@ -94,11 +92,9 @@ Module( | ___________________________^ 6 | | y = 1 | |_____^ Syntax Error: Expected FStringEnd, found FStringMiddle - | | 5 | f"""hello {x # comment 6 | y = 1 | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap index 7a5f85ab4a..290b20e86a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap @@ -81,4 +81,3 @@ Module( 5 | f'''{foo:.3f 6 | ''' | ^^^ Syntax Error: f-string: expecting `}` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap index f46548da50..d0ee282cd7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__ty_1828.py.snap @@ -197,7 +197,6 @@ Module( 4 | | class A: 5 | | pass | |_________^ Syntax Error: f-string: unterminated triple-quoted string - | | @@ -217,11 +216,9 @@ Module( 4 | | class A: 5 | | pass | |_________^ Syntax Error: Expected a statement - | | 4 | class A: 5 | pass | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@rebound_comprehension_variable.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@rebound_comprehension_variable.py.snap index 51966592d8..932b4f7ac2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@rebound_comprehension_variable.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@rebound_comprehension_variable.py.snap @@ -1021,7 +1021,6 @@ Module( 8 | [(a := 0) for a in range (0) for b in range(0)] 9 | [((a := 0), (b := 1)) for a in range (0) for b in range(0)] | ^ Syntax Error: assignment expression cannot rebind comprehension variable - | | @@ -1029,4 +1028,3 @@ Module( 8 | [(a := 0) for a in range (0) for b in range(0)] 9 | [((a := 0), (b := 1)) for a in range (0) for b in range(0)] | ^ Syntax Error: assignment expression cannot rebind comprehension variable - | 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 c6e921af83..d00b8aadbf 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 @@ -198,7 +198,6 @@ Module( 4 | return x := 1 5 | return *x and y | ^^^^^^^ Syntax Error: Boolean expression cannot be used here - | ## Semantic Syntax Errors @@ -216,4 +215,3 @@ Module( 4 | return x := 1 5 | return *x and y | ^^^^^^^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line.py.snap index fe0e953b68..c7203ece65 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line.py.snap @@ -70,4 +70,3 @@ Module( | 1 | a; if b: pass; b | ^^ Syntax Error: Compound statements are not allowed on the same line as simple statements - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line_in_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line_in_block.py.snap index 9364793f00..8ca5568f51 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line_in_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_and_compound_stmt_on_same_line_in_block.py.snap @@ -115,4 +115,3 @@ Module( 1 | if True: pass if False: pass 2 | if True: pass; if False: pass | ^^ Syntax Error: Compound statements are not allowed on the same line as simple statements - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line.py.snap index 2fdd57a51a..4a6746ba53 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line.py.snap @@ -153,7 +153,6 @@ Module( 2 | a + b c + d 3 | break; continue pass; continue break | ^^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | @@ -161,4 +160,3 @@ Module( 2 | a + b c + d 3 | break; continue pass; continue break | ^^^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line_in_block.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line_in_block.py.snap index 99af157a8e..9f757bf464 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line_in_block.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@simple_stmts_on_same_line_in_block.py.snap @@ -65,10 +65,8 @@ Module( | 1 | if True: break; continue pass; continue break | ^^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | | 1 | if True: break; continue pass; continue break | ^^^^^ Syntax Error: Simple statements must be separated by newlines or semicolons - | 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 ad76a4b9a2..f1a7ec5d75 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 @@ -117,4 +117,3 @@ Module( 1 | for _ in *x: ... 2 | for *x in xs: ... | ^^ 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 c9515b946e..46af644324 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 @@ -68,4 +68,3 @@ Module( | 1 | def f(): return *x | ^^ 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 e4eaafb75c..e95f6482b7 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 @@ -74,4 +74,3 @@ Module( | 1 | def f(): yield *x | ^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_starred_assignment_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_starred_assignment_target.py.snap index c619b3d415..93661f3f07 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_starred_assignment_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@single_starred_assignment_target.py.snap @@ -61,4 +61,3 @@ Module( | 1 | *a = (1,) | ^^ Syntax Error: starred assignment target must be in a list or tuple - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_index_py310.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_index_py310.py.snap index 23e829c384..c10eb737d2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_index_py310.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_index_py310.py.snap @@ -468,4 +468,3 @@ Module( 6 | lst[*a, *b] # multiple unpacks 7 | array[3:5, *idxs] # mixed with slices | ^^^^^ Syntax Error: Cannot use star expression in index on Python 3.10 (syntax was added in Python 3.11) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_slices.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_slices.py.snap index d4d9df070f..48d71c4c72 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_slices.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@star_slices.py.snap @@ -81,10 +81,8 @@ Module( | 1 | array[*start:*end] | ^^^^^^ Syntax Error: Starred expression cannot be used here - | | 1 | array[*start:*end] | ^^^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_comprehension_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_comprehension_target.py.snap index 02d17ed47a..bf5f2b33eb 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_comprehension_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_comprehension_target.py.snap @@ -70,4 +70,3 @@ Module( | 1 | [item for *items in source] | ^^^^^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_list_comp_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_list_comp_py314.py.snap index 75daac8756..e567871623 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_list_comp_py314.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_list_comp_py314.py.snap @@ -71,4 +71,3 @@ Module( 1 | # parse_options: {"target-version": "3.14"} 2 | [*x for x in y] | ^^ Syntax Error: Cannot use iterable unpacking in a list comprehension on Python 3.14 (syntax was added in Python 3.15) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_starred_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_starred_expression.py.snap index 71cf34bfa1..5e7b922cfa 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_starred_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@starred_starred_expression.py.snap @@ -127,4 +127,3 @@ Module( 2 | *[]) 3 | print(* *[]) | ^^^ Syntax Error: Starred expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap index 18b90424fa..0b2e6cb57b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__function_type_parameters.py.snap @@ -426,7 +426,6 @@ Module( 18 | 19 | def multiple_commas_and_recovery[A,,100](): ... | ^ Syntax Error: Expected a type parameter or the end of the type parameter list - | | @@ -434,7 +433,6 @@ Module( 18 | 19 | def multiple_commas_and_recovery[A,,100](): ... | ^^^ Syntax Error: Expected `]`, found int - | | @@ -442,7 +440,6 @@ Module( 18 | 19 | def multiple_commas_and_recovery[A,,100](): ... | ^ Syntax Error: Expected newline, found `]` - | | @@ -450,4 +447,3 @@ Module( 18 | 19 | def multiple_commas_and_recovery[A,,100](): ... | ^^ Syntax Error: Only single target (not tuple) can be annotated - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap index 126138f28a..a7555f2ebf 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_closing_parentheses.py.snap @@ -74,4 +74,3 @@ Module( 2 | if True)): 3 | pass | ^^^^ Syntax Error: Unexpected indentation - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap index e3796a8d1f..b469e7418a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__if_extra_indent.py.snap @@ -596,4 +596,3 @@ Module( 49 | before_eof 50 | final_eof | ^^^^^^^^ Syntax Error: Unexpected indentation - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap index 7744b48010..510a3c189c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap @@ -1884,7 +1884,6 @@ Module( 41 | [[a, b], [[42]], d] = [[1, 2], [[3]], 4] 42 | (x, foo(), y) = (42, 42, 42) | ^^^^^ Syntax Error: Invalid assignment target - | ## Semantic Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap index 4c67e94278..1e0455a3d2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap @@ -1739,4 +1739,3 @@ Module( 33 | [[a, b], [[42]], d] += [[1, 2], [[3]], 4] 34 | (x, foo(), y) += (42, 42, 42) | ^^^^^^^^^^^^^ Syntax Error: Invalid augmented assignment target - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap index 4bd787208c..f02af5a34e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_2.py.snap @@ -130,11 +130,9 @@ Module( 4 | case x as y + 1j: 5 | pass | ^^^^^^^^ Syntax Error: Expected dedent, found indent - | | 4 | case x as y + 1j: 5 | pass | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap index ad4dbb0ec8..eb00a56bdd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__as_pattern_3.py.snap @@ -159,11 +159,9 @@ Module( 4 | case {(x as y): 1}: 5 | pass | ^^^^^^^^ Syntax Error: Unexpected indentation - | | 4 | case {(x as y): 1}: 5 | pass | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap index 1fbaa9df86..45178034fd 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__match__invalid_mapping_pattern.py.snap @@ -646,4 +646,3 @@ Module( 22 | match subject: 23 | case {Foo(a as b): 1}: ... | ^^^^^^^^^^^ Syntax Error: Invalid mapping pattern key - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__ambiguous_lpar_with_items.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__ambiguous_lpar_with_items.py.snap index b0cefbde73..441cae77b7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__ambiguous_lpar_with_items.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__ambiguous_lpar_with_items.py.snap @@ -1869,7 +1869,6 @@ Module( 31 | with (item as f1) as f2: ... 32 | with (item1 as f, item2 := 0): ... | ^^^^^^^^^^ Syntax Error: Unparenthesized named expression cannot be used here - | ## Unsupported Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar.py.snap index 9c651474b8..b1fee5d797 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar.py.snap @@ -82,4 +82,3 @@ Module( 2 | 3 | x + y | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar_eof.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar_eof.py.snap index 317469f900..6724cfec70 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar_eof.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unclosed_ambiguous_lpar_eof.py.snap @@ -42,4 +42,3 @@ Module( | 1 | with ( | ^ Syntax Error: unexpected EOF while parsing - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unparenthesized_with_items.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unparenthesized_with_items.py.snap index daebbd0a96..9f31d95dd8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unparenthesized_with_items.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__with__unparenthesized_with_items.py.snap @@ -402,4 +402,3 @@ Module( 8 | with item1 as f, *item2: pass 9 | with item := 0 as f: pass | ^^ Syntax Error: Expected `,`, found `:=` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap index de21eac870..7133e8b63f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap @@ -119,4 +119,3 @@ Module( 2 | t"{}" 3 | t"{ }" | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap index b1b3890a11..55f34a110b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap @@ -65,4 +65,3 @@ Module( 1 | # parse_options: {"target-version": "3.14"} 2 | t"{x!z}" | ^ Syntax Error: t-string: invalid conversion character - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap index ef9242f4f8..0c729b6c50 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap @@ -119,4 +119,3 @@ Module( 2 | t"{x!123}" 3 | t"{x!'a'}" | ^^^ Syntax Error: t-string: invalid conversion character - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap index d906c8c837..2939393293 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap @@ -221,4 +221,3 @@ Module( 4 | t"{*x and y}" 5 | t"{*yield x}" | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap index 0de7376e01..b26d9cf4db 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap @@ -105,25 +105,21 @@ Module( 1 | # parse_options: {"target-version": "3.14"} 2 | t"{lambda x: x}" | ^^ Syntax Error: Expected an expression - | | 1 | # parse_options: {"target-version": "3.14"} 2 | t"{lambda x: x}" | ^^^^^^^^^ Syntax Error: t-string: lambda expressions are not allowed without parentheses - | | 1 | # parse_options: {"target-version": "3.14"} 2 | t"{lambda x: x}" | ^^ Syntax Error: t-string: expecting `}` - | | 1 | # parse_options: {"target-version": "3.14"} 2 | t"{lambda x: x}" | ^ Syntax Error: Expected an element of or the end of the t-string - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap index f6537ec7ee..e755d104ba 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap @@ -289,4 +289,3 @@ Module( 5 | t"{" 6 | t"""{""" | ^^^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap index 9789ed8922..5efaaaebe1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap @@ -153,4 +153,3 @@ Module( 2 | t"hello {x:" 3 | t"hello {x:.3f" | ^ Syntax Error: t-string: expecting `}` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@template_strings_py313.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@template_strings_py313.py.snap index 1eed668827..aaa2aed20f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@template_strings_py313.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@template_strings_py313.py.snap @@ -166,4 +166,3 @@ Module( 4 | / t"""what's 5 | | happening?""" | |_____________^ Syntax Error: Cannot use t-strings on Python 3.13 (syntax was added in Python 3.14) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_invalid_order.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_invalid_order.py.snap index 9f55c08251..193c1c8269 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_invalid_order.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_invalid_order.py.snap @@ -79,4 +79,3 @@ Module( 5 | else: 6 | pass | ^^^^ Syntax Error: Unexpected indentation - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_missing_except_finally.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_missing_except_finally.py.snap index cdba080314..6368dc0cd1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_missing_except_finally.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_missing_except_finally.py.snap @@ -73,4 +73,3 @@ Module( 5 | else: 6 | pass | ^ Syntax Error: Expected `except` or `finally` after `try` block - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_mixed_except_kind.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_mixed_except_kind.py.snap index 98f032e8c9..62db5271c7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_mixed_except_kind.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@try_stmt_mixed_except_kind.py.snap @@ -276,4 +276,3 @@ Module( 21 | / except* ExceptionGroup: 22 | | pass | |________^ Syntax Error: Cannot have both 'except' and 'except*' on the same 'try' - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@tuple_context_manager_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@tuple_context_manager_py38.py.snap index be7cb746f5..059a866b5d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@tuple_context_manager_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@tuple_context_manager_py38.py.snap @@ -188,4 +188,3 @@ Module( 10 | ): ... 11 | with (foo,): ... | ^ Syntax Error: Cannot use parentheses within a `with` statement on Python 3.8 (syntax was added in Python 3.9) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_incomplete_stmt.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_incomplete_stmt.py.snap index b035de8a7f..eb26aa58f7 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_incomplete_stmt.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_incomplete_stmt.py.snap @@ -94,4 +94,3 @@ Module( 2 | type x 3 | type x = | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_invalid_value_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_invalid_value_expr.py.snap index 57d354e2fd..9e9d4f7153 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_invalid_value_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_alias_invalid_value_expr.py.snap @@ -175,7 +175,6 @@ Module( 3 | type x = yield from y 4 | type x = x := 1 | ^^ Syntax Error: Expected a statement - | ## Semantic Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap index 996be97aa4..d20f30583a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_default_py312.py.snap @@ -331,7 +331,6 @@ Module( 4 | class C[T = int](): ... 5 | class D[S, T = int, U = uint](): ... | ^^^^^ Syntax Error: Cannot set default type for a type parameter on Python 3.12 (syntax was added in Python 3.13) - | | @@ -339,4 +338,3 @@ Module( 4 | class C[T = int](): ... 5 | class D[S, T = int, U = uint](): ... | ^^^^^^ Syntax Error: Cannot set default type for a type parameter on Python 3.12 (syntax was added in Python 3.13) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap index 7876c6b6ec..56404f81d8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_invalid_bound_expr.py.snap @@ -289,7 +289,6 @@ Module( 3 | type X[T: yield from x] = int 4 | type X[T: x := int] = int | ^^ Syntax Error: Expected `,`, found `:=` - | ## Semantic Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_bound.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_bound.py.snap index d029828b20..4a12a66c5d 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_bound.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_missing_bound.py.snap @@ -126,4 +126,3 @@ Module( 1 | type X[T: ] = int 2 | type X[T1: , T2] = int | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap index 351a141b60..0a0d030f52 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_bound.py.snap @@ -89,16 +89,13 @@ Module( | 1 | type X[**T: int] = int | ^ Syntax Error: Expected `]`, found `:` - | | 1 | type X[**T: int] = int | ^ Syntax Error: Expected a statement - | | 1 | type X[**T: int] = int | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap index fd2b2f4714..6e215c846a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_invalid_default_expr.py.snap @@ -353,7 +353,6 @@ Module( 4 | type X[**P = x := int] = int 5 | type X[**P = *int] = int | ^^^^ Syntax Error: Starred expression cannot be used here - | ## Semantic Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_default.py.snap index 1036d8c59a..180df8a3ed 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_param_spec_missing_default.py.snap @@ -124,4 +124,3 @@ Module( 1 | type X[**P =] = int 2 | type X[**P =, T2] = int | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap index 0b4041088c..cc7032b9c6 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_invalid_default_expr.py.snap @@ -427,7 +427,6 @@ Module( 5 | type X[T = x := int] = int 6 | type X[T: int = *int] = int | ^^^^ Syntax Error: Starred expression cannot be used here - | ## Semantic Syntax Errors diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_missing_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_missing_default.py.snap index 55290b5e18..0b4f3f2183 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_missing_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_missing_default.py.snap @@ -188,4 +188,3 @@ Module( 2 | type X[T: int =] = int 3 | type X[T1 =, T2] = int | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap index ae228c0e30..edeed69d21 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_bound.py.snap @@ -89,16 +89,13 @@ Module( | 1 | type X[*T: int] = int | ^ Syntax Error: Expected `]`, found `:` - | | 1 | type X[*T: int] = int | ^ Syntax Error: Expected a statement - | | 1 | type X[*T: int] = int | ^ Syntax Error: Expected a statement - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap index 8cd49704ce..f5fd209fbe 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_invalid_default_expr.py.snap @@ -362,7 +362,6 @@ Module( 4 | type X[*Ts = yield from x] = int 5 | type X[*Ts = x := int] = int | ^^ Syntax Error: Expected `,`, found `:=` - | ## Semantic Syntax Errors @@ -391,4 +390,3 @@ Module( 4 | type X[*Ts = yield from x] = int 5 | type X[*Ts = x := int] = int | ^^^ Syntax Error: non default type parameter `int` follows default type parameter - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_missing_default.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_missing_default.py.snap index 313733b6e5..f31c1fae64 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_missing_default.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_param_type_var_tuple_missing_default.py.snap @@ -124,4 +124,3 @@ Module( 1 | type X[*Ts =] = int 2 | type X[*Ts =, T2] = int | ^ Syntax Error: Expected an expression - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_parameter_default_order.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_parameter_default_order.py.snap index 9ce4c11a84..6d6b2fe73e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_parameter_default_order.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_parameter_default_order.py.snap @@ -365,4 +365,3 @@ Module( 3 | def f[T = int, U](): ... 4 | type Alias[T = int, U] = ... | ^ Syntax Error: non default type parameter `U` follows default type parameter - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_params_empty.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_params_empty.py.snap index 967dce60f4..d28237e35f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_params_empty.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_params_empty.py.snap @@ -111,4 +111,3 @@ Module( 2 | pass 3 | type ListOrSet[] = list | set | ^ Syntax Error: Type parameter list cannot be empty - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_stmt_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_stmt_py311.py.snap index 05bad9f457..227ca0f7fe 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_stmt_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@type_stmt_py311.py.snap @@ -43,4 +43,3 @@ Module( 1 | # parse_options: {"target-version": "3.11"} 2 | type x = int | ^^^^ Syntax Error: Cannot use `type` alias statement on Python 3.11 (syntax was added in Python 3.12) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_index_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_index_py38.py.snap index 15c59e5cca..c144ff3d3a 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_index_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_index_py38.py.snap @@ -64,4 +64,3 @@ Module( 1 | # parse_options: {"target-version": "3.8"} 2 | lst[x:=1] | ^^^^ Syntax Error: Cannot use unparenthesized assignment expression in a sequence index on Python 3.8 (syntax was added in Python 3.9) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_comp_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_comp_py38.py.snap index f907721b0f..ecc5a6040e 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_comp_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_comp_py38.py.snap @@ -100,4 +100,3 @@ Module( 1 | # parse_options: {"target-version": "3.8"} 2 | {last := x for x in range(3)} | ^^^^^^^^^ Syntax Error: Cannot use unparenthesized assignment expression as an element in a set comprehension on Python 3.8 (syntax was added in Python 3.9) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_literal_py38.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_literal_py38.py.snap index bf277fe095..3d999f8721 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_literal_py38.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unparenthesized_named_expr_set_literal_py38.py.snap @@ -204,4 +204,3 @@ Module( 3 | {1, x := 2, 3} 4 | {1, 2, x := 3} | ^^^^^^ Syntax Error: Cannot use unparenthesized assignment expression as an element in a set literal on Python 3.8 (syntax was added in Python 3.9) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@walrus_py37.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@walrus_py37.py.snap index dc64a3105d..e712c56940 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@walrus_py37.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@walrus_py37.py.snap @@ -49,4 +49,3 @@ Module( 1 | # parse_options: { "target-version": "3.7" } 2 | (x := 1) | ^^^^^^ Syntax Error: Cannot use named assignment expression (`:=`) on Python 3.7 (syntax was added in Python 3.8) - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_invalid_test_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_invalid_test_expr.py.snap index acdd7532ad..e6637b1f49 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_invalid_test_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@while_stmt_invalid_test_expr.py.snap @@ -211,4 +211,3 @@ Module( 3 | while a, b: ... 4 | while a := 1, b: ... | ^ Syntax Error: Expected `:`, found `,` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_comma.py.snap index 0c4b117dcf..c756775e3b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@with_items_parenthesized_missing_comma.py.snap @@ -377,4 +377,3 @@ Module( 4 | with (item1, item2 as f1 item3, item4): ... 5 | with (item1, item2: ... | ^ Syntax Error: Expected `)`, found `:` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap index cb9bd384b5..b23970c545 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap @@ -224,4 +224,3 @@ Module( 3 | __debug__ = 1 4 | x, y, __debug__, z = 1, 2, 3, 4 | ^^^^^^^^^ Syntax Error: cannot assign to `__debug__` - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_after_comma.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_after_comma.py.snap index 327a30a0d3..78d8a6437b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_after_comma.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_after_comma.py.snap @@ -87,4 +87,3 @@ Module( | 1 | def f(): 1, yield 1 | ^^^^^^^ Syntax Error: Yield expression cannot be used here - | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_from_in_async_function.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_from_in_async_function.py.snap index 86372e56d9..f040e673c2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_from_in_async_function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@yield_from_in_async_function.py.snap @@ -65,4 +65,3 @@ Module( | 1 | async def f(): yield from x | ^^^^^^^^^^^^ Syntax Error: `yield from` statement in async function; use `async for` instead - | diff --git a/crates/ty/tests/cli/analysis_options.rs b/crates/ty/tests/cli/analysis_options.rs index 496b9f2b6d..fcc20d7ab6 100644 --- a/crates/ty/tests/cli/analysis_options.rs +++ b/crates/ty/tests/cli/analysis_options.rs @@ -31,7 +31,6 @@ fn respect_type_ignore_comments_is_turned_off() -> anyhow::Result<()> { | 2 | y = a + 5 # type: ignore | ^ - | Found 1 diagnostic @@ -81,7 +80,6 @@ fn overrides_basic() -> anyhow::Result<()> { | 2 | print(x) # type: ignore # ignore not-respected (override) | ^ - | Found 1 diagnostic @@ -137,7 +135,6 @@ fn overrides_precedence() -> anyhow::Result<()> { | 2 | print(y) # type: ignore (should be an error, because type ignores are disabled) | ^ - | Found 1 diagnostic @@ -189,14 +186,12 @@ fn overrides_inherit_global() -> anyhow::Result<()> { | 2 | print(y) # type: ignore ignore not-respected (global) | ^ - | error[unresolved-reference]: Name `y` used when not defined --> tests/test_main.py:2:7 | 2 | print(y) # type: ignore ignore respected (inherited from global) | ^ - | Found 2 diagnostics diff --git a/crates/ty/tests/cli/config_option.rs b/crates/ty/tests/cli/config_option.rs index 89e4c7266d..d89f5f34df 100644 --- a/crates/ty/tests/cli/config_option.rs +++ b/crates/ty/tests/cli/config_option.rs @@ -16,7 +16,6 @@ fn cli_config_args_toml_string_basic() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -33,7 +32,6 @@ fn cli_config_args_toml_string_basic() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -66,7 +64,6 @@ fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -83,7 +80,6 @@ fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -105,7 +101,6 @@ fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -164,7 +159,6 @@ fn config_file_override() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -181,7 +175,6 @@ fn config_file_override() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic diff --git a/crates/ty/tests/cli/exit_code.rs b/crates/ty/tests/cli/exit_code.rs index 784d7bbe84..85eceea528 100644 --- a/crates/ty/tests/cli/exit_code.rs +++ b/crates/ty/tests/cli/exit_code.rs @@ -15,7 +15,6 @@ fn only_warnings() -> anyhow::Result<()> { | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -85,7 +84,6 @@ fn only_info() -> anyhow::Result<()> { | 3 | reveal_type(1) | ^ `Literal[1]` - | Found 1 diagnostic @@ -114,7 +112,6 @@ fn only_info_and_error_on_warning_is_true() -> anyhow::Result<()> { | 3 | reveal_type(1) | ^ `Literal[1]` - | Found 1 diagnostic @@ -146,7 +143,6 @@ fn only_warnings_and_error_on_warning_overrides_configuration() -> anyhow::Resul | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -178,7 +174,6 @@ fn only_warnings_and_error_on_warning_is_disabled_in_configuration() -> anyhow:: | 1 | print(x) # [unresolved-reference] | ^ - | Found 1 diagnostic @@ -207,14 +202,12 @@ fn both_warnings_and_errors() -> anyhow::Result<()> { | 2 | print(x) # [unresolved-reference] | ^ - | error[not-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method --> test.py:3:7 | 3 | print(4[1]) # [not-subscriptable] | ^^^^ - | Found 2 diagnostics @@ -243,14 +236,12 @@ fn both_warnings_and_errors_and_exit_zero_on_warning() -> anyhow::Result<()> { | 2 | print(x) # [unresolved-reference] | ^ - | error[not-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method --> test.py:3:7 | 3 | print(4[1]) # [not-subscriptable] | ^^^^ - | Found 2 diagnostics @@ -279,14 +270,12 @@ fn exit_zero_is_true() -> anyhow::Result<()> { | 2 | print(x) # [unresolved-reference] | ^ - | error[not-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method --> test.py:3:7 | 3 | print(4[1]) # [not-subscriptable] | ^^^^ - | Found 2 diagnostics diff --git a/crates/ty/tests/cli/file_selection.rs b/crates/ty/tests/cli/file_selection.rs index 7046d1deb1..5d2a082659 100644 --- a/crates/ty/tests/cli/file_selection.rs +++ b/crates/ty/tests/cli/file_selection.rs @@ -124,14 +124,12 @@ fn exclude_argument() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `temp_undefined_var` used when not defined --> temp_file.py:2:7 | 2 | print(temp_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -148,7 +146,6 @@ fn exclude_argument() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -200,7 +197,6 @@ fn configuration_include() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -225,14 +221,12 @@ fn configuration_include() -> anyhow::Result<()> { | 2 | print(other_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> src/main.py:2:7 | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -290,7 +284,6 @@ fn configuration_include_no_extension() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -342,14 +335,12 @@ fn configuration_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `temp_undefined_var` used when not defined --> temp_file.py:2:7 | 2 | print(temp_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -374,7 +365,6 @@ fn configuration_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -427,7 +417,6 @@ fn exclude_precedence_over_include() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -479,7 +468,6 @@ fn exclude_argument_precedence_include_argument() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -517,7 +505,6 @@ fn remove_default_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -542,14 +529,12 @@ fn remove_default_exclude() -> anyhow::Result<()> { | 2 | print(another_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> src/main.py:2:7 | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -596,7 +581,6 @@ fn cli_removes_config_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -613,14 +597,12 @@ fn cli_removes_config_exclude() -> anyhow::Result<()> { | 2 | print(build_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> src/main.py:2:7 | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -671,7 +653,6 @@ fn explicit_path_overrides_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -688,7 +669,6 @@ fn explicit_path_overrides_exclude() -> anyhow::Result<()> { | 2 | print(dist_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -705,7 +685,6 @@ fn explicit_path_overrides_exclude() -> anyhow::Result<()> { | 2 | print(other_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -756,14 +735,12 @@ fn explicit_path_overrides_exclude_force_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `dist_undefined_var` used when not defined --> tests/generated.py:2:7 | 2 | print(dist_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -780,7 +757,6 @@ fn explicit_path_overrides_exclude_force_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -797,14 +773,12 @@ fn explicit_path_overrides_exclude_force_exclude() -> anyhow::Result<()> { | 2 | print(other_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> src/main.py:2:7 | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -821,7 +795,6 @@ fn explicit_path_overrides_exclude_force_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -868,14 +841,12 @@ fn force_exclude_directory_exclusion() -> anyhow::Result<()> { | 3 | if base_path not in CMAKE_PREFIX_PATH: | ^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `CMAKE_PREFIX_PATH` used when not defined --> out/amd64/install/_setup_util.py:4:5 | 4 | CMAKE_PREFIX_PATH.insert(0, base_path) | ^^^^^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -935,14 +906,12 @@ fn cli_and_configuration_exclude() -> anyhow::Result<()> { | 2 | print(other_undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> src/main.py:2:7 | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -958,7 +927,6 @@ fn cli_and_configuration_exclude() -> anyhow::Result<()> { | 2 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -1142,14 +1110,12 @@ print(other_undefined) # error: unresolved-reference | 3 | return missing_value # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> main.py:5:7 | 5 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 2 diagnostics @@ -1166,7 +1132,6 @@ print(other_undefined) # error: unresolved-reference | 5 | print(undefined_var) # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -1219,21 +1184,18 @@ print(regular_undefined) # error: unresolved-reference | 2 | print(regular_undefined) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `undefined_var` used when not defined --> src/module.py:3:12 | 3 | return undefined_var # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `missing_value` used when not defined --> src/utils.py:3:12 | 3 | return missing_value # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 3 diagnostics @@ -1250,21 +1212,18 @@ print(regular_undefined) # error: unresolved-reference | 3 | return undefined_var # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `missing_value` used when not defined --> generated_utils.py:3:12 | 3 | return missing_value # error: unresolved-reference | ^^^^^^^^^^^^^ - | error[unresolved-reference]: Name `regular_undefined` used when not defined --> regular.py:2:7 | 2 | print(regular_undefined) # error: unresolved-reference | ^^^^^^^^^^^^^^^^^ - | Found 3 diagnostics @@ -1281,7 +1240,6 @@ print(regular_undefined) # error: unresolved-reference | 3 | return undefined_var # error: unresolved-reference | ^^^^^^^^^^^^^ - | Found 1 diagnostic diff --git a/crates/ty/tests/cli/fixes.rs b/crates/ty/tests/cli/fixes.rs index 03cdb6abe0..21424a58e0 100644 --- a/crates/ty/tests/cli/fixes.rs +++ b/crates/ty/tests/cli/fixes.rs @@ -140,21 +140,18 @@ fn add_ignore_unfixable() -> anyhow::Result<()> { | 6 | reveal_type(x) # ty: ignore[undefined-reveal] | ^ `Unknown` - | error[unresolved-reference]: Name `x` used when not defined --> has_syntax_error.py:1:7 | 1 | print(x # [unresolved-reference] | ^ - | error[invalid-syntax]: unexpected EOF while parsing --> has_syntax_error.py:1:34 | 1 | print(x # [unresolved-reference] | ^ - | Found 3 diagnostics Added 5 ignore comments @@ -224,9 +221,10 @@ fn fix_unfixable() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error[invalid-syntax]: unexpected EOF while parsing - --> has_syntax_error.py:1:1 - | - | + --> has_syntax_error.py:2:1 + | + 2 | + | ^ Found 2 diagnostics (1 fixed, 1 remaining). diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs index 67d5672c38..b05ada1fbb 100644 --- a/crates/ty/tests/cli/main.rs +++ b/crates/ty/tests/cli/main.rs @@ -51,13 +51,12 @@ fn test_quiet_output() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error[invalid-assignment]: Object of type `Literal["foo"]` is not assignable to `int` - --> test.py:1:4 + --> test.py:1:10 | 1 | x: int = 'foo' | --- ^^^^^ Incompatible value of type `Literal["foo"]` | | | Declared type - | Found 1 diagnostic @@ -132,7 +131,6 @@ fn test_run_in_sub_directory() -> anyhow::Result<()> { | 1 | ~ | ^ - | Found 1 diagnostic @@ -153,7 +151,6 @@ fn test_include_hidden_files_by_default() -> anyhow::Result<()> { | 1 | ~ | ^ - | Found 1 diagnostic @@ -186,7 +183,6 @@ fn test_respect_ignore_files() -> anyhow::Result<()> { | 1 | ~ | ^ - | Found 1 diagnostic @@ -204,7 +200,6 @@ fn test_respect_ignore_files() -> anyhow::Result<()> { | 1 | ~ | ^ - | Found 1 diagnostic @@ -222,7 +217,6 @@ fn test_respect_ignore_files() -> anyhow::Result<()> { | 1 | ~ | ^ - | Found 1 diagnostic @@ -283,7 +277,6 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> { | 2 | from utils import add | ^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -396,7 +389,6 @@ fn user_configuration() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file error[unresolved-reference]: Name `prin` used when not defined @@ -404,7 +396,6 @@ fn user_configuration() -> anyhow::Result<()> { | 7 | prin(x) | ^^^^ - | info: rule `unresolved-reference` is enabled by default Found 2 diagnostics @@ -437,7 +428,6 @@ fn user_configuration() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file warning[unresolved-reference]: Name `prin` used when not defined @@ -445,7 +435,6 @@ fn user_configuration() -> anyhow::Result<()> { | 7 | prin(x) | ^^^^ - | info: rule `unresolved-reference` was selected in the configuration file Found 2 diagnostics @@ -494,7 +483,6 @@ fn check_specific_paths() -> anyhow::Result<()> { | 2 | from main2 import z # error: unresolved-import | ^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -505,7 +493,6 @@ fn check_specific_paths() -> anyhow::Result<()> { | 2 | import does_not_exist # error: unresolved-import | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -530,7 +517,6 @@ fn check_specific_paths() -> anyhow::Result<()> { | 2 | from main2 import z # error: unresolved-import | ^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -541,7 +527,6 @@ fn check_specific_paths() -> anyhow::Result<()> { | 2 | import does_not_exist # error: unresolved-import | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -602,7 +587,6 @@ fn check_file_without_extension() -> anyhow::Result<()> { | 1 | a = b | ^ - | Found 1 diagnostic @@ -841,7 +825,6 @@ fn can_handle_large_binop_expressions() -> anyhow::Result<()> { | 4 | reveal_type(total) | ^^^^^ `Literal[2000]` - | Found 1 diagnostic diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs index 18c04e9025..2f20a098af 100644 --- a/crates/ty/tests/cli/python_environment.rs +++ b/crates/ty/tests/cli/python_environment.rs @@ -35,14 +35,12 @@ fn config_override_python_version() -> anyhow::Result<()> { | 5 | print(sys.last_exc) | ^^^^^^^^^^^^ - | info: The member may be available on other Python versions or platforms info: Python 3.11 was assumed when resolving the `last_exc` attribute --> pyproject.toml:3:18 | 3 | python-version = "3.11" | ^^^^^^ Python version configuration - | Found 1 diagnostic @@ -92,7 +90,6 @@ fn config_override_python_platform() -> anyhow::Result<()> { | 5 | reveal_type(sys.platform) | ^^^^^^^^^^^^ `Literal["linux"]` - | Found 1 diagnostic @@ -108,7 +105,6 @@ fn config_override_python_platform() -> anyhow::Result<()> { | 5 | reveal_type(sys.platform) | ^^^^^^^^^^^^ `LiteralString` - | Found 1 diagnostic @@ -145,14 +141,12 @@ fn config_file_annotation_showing_where_python_version_set_typing_error() -> any | 2 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 was assumed when resolving types --> pyproject.toml:3:18 | 3 | python-version = "3.12" | ^^^^^^ Python version configuration - | Found 1 diagnostic @@ -168,7 +162,6 @@ fn config_file_annotation_showing_where_python_version_set_typing_error() -> any | 2 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 was assumed when resolving types because it was specified on the command line @@ -202,7 +195,6 @@ fn src_subdirectory_takes_precedence_over_repo_root() -> anyhow::Result<()> { | 1 | from . import nonexistent_submodule | ^^^^^^^^^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -266,7 +258,6 @@ fn python_version_inferred_from_system_installation() -> anyhow::Result<()> { | 1 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 was assumed when resolving types because of the layout of your Python installation info: The primary `site-packages` directory of your installation was found at `lib/python3.12/site-packages/` @@ -292,7 +283,6 @@ fn python_version_inferred_from_system_installation() -> anyhow::Result<()> { | 1 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 was assumed when resolving types because of the layout of your Python installation info: The primary `site-packages` directory of your installation was found at `lib/pypy3.12/site-packages/` @@ -321,7 +311,6 @@ fn python_version_inferred_from_system_installation() -> anyhow::Result<()> { | 1 | import string.templatelib | ^^^^^^^^^^^^^^^^^^ - | info: The stdlib module `string.templatelib` is only available on Python 3.14+ info: Python 3.13 was assumed when resolving modules because of the layout of your Python installation info: The primary `site-packages` directory of your installation was found at `lib/python3.13t/site-packages/` @@ -404,7 +393,6 @@ import colorama | 1 | import foo | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -416,7 +404,6 @@ import colorama | 3 | import colorama | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -439,7 +426,6 @@ import colorama | 2 | import bar | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -451,7 +437,6 @@ import colorama | 3 | import colorama | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -474,7 +459,6 @@ import colorama | 2 | import bar | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -486,7 +470,6 @@ import colorama | 3 | import colorama | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -509,7 +492,6 @@ import colorama | 2 | import bar | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -521,7 +503,6 @@ import colorama | 3 | import colorama | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -575,7 +556,6 @@ import bar", | 1 | import foo | ^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -622,7 +602,6 @@ fn lib64_site_packages_directory_on_unix() -> anyhow::Result<()> { | 1 | import foo, bar, baz | ^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -666,7 +645,6 @@ fn many_search_paths() -> anyhow::Result<()> { | 1 | import foo1, baz | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /extra1 (extra search path specified on the CLI or in your config file) info: 2. /extra2 (extra search path specified on the CLI or in your config file) @@ -699,7 +677,6 @@ fn many_search_paths() -> anyhow::Result<()> { | 1 | import foo1, baz | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /extra1 (extra search path specified on the CLI or in your config file) info: 2. /extra2 (extra search path specified on the CLI or in your config file) @@ -734,7 +711,6 @@ fn many_search_paths() -> anyhow::Result<()> { | 1 | import foo1, baz | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /extra1 (extra search path specified on the CLI or in your config file) info: 2. /extra2 (extra search path specified on the CLI or in your config file) @@ -794,14 +770,12 @@ fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Resu | 1 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 was assumed when resolving types because of your virtual environment --> venv/pyvenv.cfg:2:11 | 2 | version = 3.12 | ^^^^ Virtual environment metadata - | info: No Python version was specified on the command line or in a configuration file Found 1 diagnostic @@ -850,14 +824,12 @@ fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> { | 1 | PythonFinalizationError | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 was assumed when resolving types because of your virtual environment --> venv/pyvenv.cfg:3:23 | 3 | version = 3.12 | ^^^^ Virtual environment metadata - | info: No Python version was specified on the command line or in a configuration file Found 1 diagnostic @@ -899,13 +871,11 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any | 2 | match object(): | ^^^^^ - | info: Python 3.8 was assumed when parsing syntax --> pyproject.toml:3:19 | 3 | requires-python = ">=3.8" | ^^^^^^^ Python version configuration - | Found 1 diagnostic @@ -921,7 +891,6 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any | 2 | match object(): | ^^^^^ - | info: Python 3.9 was assumed when parsing syntax because it was specified on the command line Found 1 diagnostic @@ -1082,7 +1051,6 @@ fn config_file_broken_python_setting() -> anyhow::Result<()> { 10 | [tool.ty.environment] 11 | python = "not-a-directory-or-executable" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not point to a Python executable or a directory on disk - | Cause: No such file or directory (os error 2) "#); @@ -1172,7 +1140,6 @@ fn config_file_python_setting_directory_with_no_site_packages() -> anyhow::Resul 2 | [tool.ty.environment] 3 | python = "directory-but-no-site-packages" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not find a `site-packages` directory for this Python installation/executable - | "#); Ok(()) @@ -1217,7 +1184,6 @@ fn config_file_python_setting_directory_with_unsupported_python_version() -> any | 2 | version_info = 3.16.0 | ^^^^^^ - | info: Expected one of `3.7`, `3.8`, `3.9`, `3.10`, `3.11`, `3.12`, `3.13`, `3.14`, `3.15`. info: Set `environment.python-version` explicitly to override the inferred version. info: The version was inferred from your virtual environment metadata. @@ -1262,7 +1228,6 @@ fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> { 2 | [tool.ty.environment] 3 | python = "directory-but-no-site-packages" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | "#); Ok(()) @@ -1303,28 +1268,24 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> { | 4 | os.grantpt(1) # only available on unix, Python 3.13 or newer | ^^^^^^^^^^ - | info: The member may be available on other Python versions or platforms info: Python 3.10 was assumed when resolving the `grantpt` attribute --> ty.toml:3:18 | 3 | python-version = "3.10" | ^^^^^^ Python version configuration - | error[unresolved-import]: Module `typing` has no member `LiteralString` --> main.py:6:20 | 6 | from typing import LiteralString # added in Python 3.11 | ^^^^^^^^^^^^^ - | info: The member may be available on other Python versions or platforms info: Python 3.10 was assumed when resolving imports --> ty.toml:3:18 | 3 | python-version = "3.10" | ^^^^^^ Python version configuration - | Found 2 diagnostics @@ -1522,7 +1483,6 @@ home = ./ | 4 | from package1 import WorkingVenv | ^^^^^^^^^^^ - | Found 1 diagnostic @@ -1541,7 +1501,6 @@ home = ./ | 2 | from package1 import ActiveVenv | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1560,7 +1519,6 @@ home = ./ | 3 | from package1 import ChildConda | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1580,7 +1538,6 @@ home = ./ | 4 | from package1 import WorkingVenv | ^^^^^^^^^^^ - | Found 1 diagnostic @@ -1602,7 +1559,6 @@ home = ./ | 2 | from package1 import ActiveVenv | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1622,7 +1578,6 @@ home = ./ | 3 | from package1 import ChildConda | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1642,7 +1597,6 @@ home = ./ | 3 | from package1 import ChildConda | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1662,7 +1616,6 @@ home = ./ | 5 | from package1 import BaseConda | ^^^^^^^^^ - | Found 1 diagnostic @@ -1749,7 +1702,6 @@ home = ./ | 2 | from package1 import ActiveVenv | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -1760,7 +1712,6 @@ home = ./ | 3 | from package1 import ChildConda | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -1771,7 +1722,6 @@ home = ./ | 4 | from package1 import WorkingVenv | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -1782,7 +1732,6 @@ home = ./ | 5 | from package1 import BaseConda | ^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /project (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -1805,7 +1754,6 @@ home = ./ | 2 | from package1 import ActiveVenv | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1824,7 +1772,6 @@ home = ./ | 3 | from package1 import ChildConda | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1844,7 +1791,6 @@ home = ./ | 5 | from package1 import BaseConda | ^^^^^^^^^ - | Found 1 diagnostic @@ -1866,7 +1812,6 @@ home = ./ | 2 | from package1 import ActiveVenv | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1886,7 +1831,6 @@ home = ./ | 5 | from package1 import BaseConda | ^^^^^^^^^ - | Found 1 diagnostic @@ -1906,7 +1850,6 @@ home = ./ | 3 | from package1 import ChildConda | ^^^^^^^^^^ - | Found 1 diagnostic @@ -1926,7 +1869,6 @@ home = ./ | 5 | from package1 import BaseConda | ^^^^^^^^^ - | Found 1 diagnostic @@ -2058,7 +2000,6 @@ fn ty_environment_and_discovered_venv() -> anyhow::Result<()> { | 9 | from shared_package import FromLocalVenv | ^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -2132,7 +2073,6 @@ fn ty_environment_and_active_environment() -> anyhow::Result<()> { | 2 | from ty_package import TyEnvClass | ^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -2251,7 +2191,6 @@ fn ty_system_environment_and_local_venv() -> anyhow::Result<()> { | 3 | from system_package import SystemEnvClass | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -2288,7 +2227,6 @@ fn src_root_deprecation_warning() -> anyhow::Result<()> { | 3 | root = "./src" | ^^^^^^^ - | Found 1 diagnostic @@ -2323,7 +2261,6 @@ fn src_root_deprecation_warning_with_environment_root() -> anyhow::Result<()> { | 3 | root = "./src" | ^^^^^^^ - | info: The `src.root` setting was ignored in favor of the `environment.root` setting Found 1 diagnostic @@ -2365,7 +2302,6 @@ fn environment_root_takes_precedence_over_src_root() -> anyhow::Result<()> { | 3 | root = "./src" | ^^^^^^^ - | info: The `src.root` setting was ignored in favor of the `environment.root` setting Found 1 diagnostic @@ -2554,7 +2490,6 @@ fn default_root_tests_package() -> anyhow::Result<()> { | 3 | from bar import bar # expected unresolved import | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. / (first-party code) @@ -2624,7 +2559,6 @@ fn default_root_python_package() -> anyhow::Result<()> { | 3 | from bar import bar # expected unresolved import | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. / (first-party code) @@ -2666,7 +2600,6 @@ fn default_root_python_package_pyi() -> anyhow::Result<()> { | 3 | from bar import bar # expected unresolved import | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. / (first-party code) @@ -2704,7 +2637,6 @@ fn pythonpath_is_respected() -> anyhow::Result<()> { | 2 | import baz | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. / (first-party code) @@ -2757,7 +2689,6 @@ fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> { | 2 | import baz | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. / (first-party code) @@ -2769,7 +2700,6 @@ fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> { | 3 | import foo | ^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. / (first-party code) diff --git a/crates/ty/tests/cli/rule_selection.rs b/crates/ty/tests/cli/rule_selection.rs index 78f6743f47..19357d71c6 100644 --- a/crates/ty/tests/cli/rule_selection.rs +++ b/crates/ty/tests/cli/rule_selection.rs @@ -27,7 +27,6 @@ fn configuration_rule_severity() -> anyhow::Result<()> { | 7 | prin(x) # unresolved-reference | ^^^^ - | info: rule `unresolved-reference` is enabled by default Found 1 diagnostic @@ -54,7 +53,6 @@ fn configuration_rule_severity() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 1 diagnostic @@ -94,7 +92,6 @@ fn cli_rule_severity() -> anyhow::Result<()> { | 2 | import does_not_exit | ^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -106,7 +103,6 @@ fn cli_rule_severity() -> anyhow::Result<()> { | 9 | prin(x) # unresolved-reference | ^^^^ - | info: rule `unresolved-reference` is enabled by default Found 2 diagnostics @@ -134,7 +130,6 @@ fn cli_rule_severity() -> anyhow::Result<()> { | 2 | import does_not_exit | ^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. / (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -146,7 +141,6 @@ fn cli_rule_severity() -> anyhow::Result<()> { | 4 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected on the command line Found 2 diagnostics @@ -185,7 +179,6 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { | 7 | prin(x) # unresolved-reference | ^^^^ - | info: rule `unresolved-reference` is enabled by default Found 1 diagnostic @@ -213,7 +206,6 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected on the command line Found 1 diagnostic @@ -249,7 +241,6 @@ fn configuration_unknown_rules() -> anyhow::Result<()> { | 3 | division-by-zer = "warn" # incorrect rule name | ^^^^^^^^^^^^^^^ - | Found 1 diagnostic @@ -324,7 +315,6 @@ fn overrides_basic() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: error (global) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file error[unresolved-reference]: Name `prin` used when not defined @@ -332,7 +322,6 @@ fn overrides_basic() -> anyhow::Result<()> { | 4 | prin(x) # unresolved-reference: error (global) | ^^^^ - | info: rule `unresolved-reference` was selected in the configuration file warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero @@ -340,7 +329,6 @@ fn overrides_basic() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: warn (override) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 3 diagnostics @@ -398,7 +386,6 @@ fn overrides_precedence() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: warn (first override) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 1 diagnostic @@ -448,7 +435,6 @@ fn multiple_overrides_inherit_cli_rules() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | Found 1 diagnostic @@ -499,7 +485,6 @@ fn overrides_exclude() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: error (override excluded) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero @@ -507,7 +492,6 @@ fn overrides_exclude() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: warn (override applies) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 2 diagnostics @@ -563,7 +547,6 @@ fn overrides_inherit_global() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: warn (global) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file error[unresolved-reference]: Name `prin` used when not defined @@ -571,7 +554,6 @@ fn overrides_inherit_global() -> anyhow::Result<()> { | 3 | prin(y) # unresolved-reference: error (global) | ^^^^ - | info: rule `unresolved-reference` was selected in the configuration file error[unresolved-reference]: Name `prin` used when not defined @@ -579,7 +561,6 @@ fn overrides_inherit_global() -> anyhow::Result<()> { | 3 | prin(y) # unresolved-reference: error (inherited from global) | ^^^^ - | info: rule `unresolved-reference` was selected in the configuration file Found 3 diagnostics @@ -716,7 +697,6 @@ fn overrides_missing_include_exclude() -> anyhow::Result<()> { | 5 | [[tool.ty.overrides]] | ^^^^^^^^^^^^^^^^^^^^^ This overrides section applies to all files - | info: It has no `include` or `exclude` option restricting the files info: Restrict the files by adding a pattern to `include` or `exclude`... info: or remove the `[[overrides]]` section and merge the configuration into the root `[rules]` table if the configuration should apply to all files @@ -726,7 +706,6 @@ fn overrides_missing_include_exclude() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 2 diagnostics @@ -771,7 +750,6 @@ fn overrides_empty_include() -> anyhow::Result<()> { | 6 | include = [] # Empty include - won't match any files | ^^ This `include` list is empty - | info: Remove the `include` option to match all files or add a pattern to match specific files error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero @@ -779,7 +757,6 @@ fn overrides_empty_include() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 2 diagnostics @@ -823,7 +800,6 @@ fn overrides_no_actual_overrides() -> anyhow::Result<()> { | 5 | [[tool.ty.overrides]] | ^^^^^^^^^^^^^^^^^^^^^ This overrides section overrides no settings - | info: It has no `rules` or `analysis` table info: Add a `[overrides.rules]` or `[overrides.analysis]` table... info: or remove the `[[overrides]]` section if there's nothing to override @@ -833,7 +809,6 @@ fn overrides_no_actual_overrides() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 2 diagnostics @@ -886,7 +861,6 @@ fn overrides_unknown_rules() -> anyhow::Result<()> { | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file warning[unknown-rule]: Unknown rule `division-by-zer`. Did you mean `division-by-zero`? @@ -894,14 +868,12 @@ fn overrides_unknown_rules() -> anyhow::Result<()> { | 10 | division-by-zer = "error" # incorrect rule name | ^^^^^^^^^^^^^^^ - | warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero --> tests/test_main.py:2:5 | 2 | y = 4 / 0 | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file Found 3 diagnostics @@ -972,7 +944,6 @@ fn cli_all_rules_warn() -> anyhow::Result<()> { | 2 | prin(x) # unresolved-reference | ^^^^ - | info: rule `unresolved-reference` was selected on the command line warning[unresolved-reference]: Name `x` used when not defined @@ -980,7 +951,6 @@ fn cli_all_rules_warn() -> anyhow::Result<()> { | 2 | prin(x) # unresolved-reference | ^ - | info: rule `unresolved-reference` was selected on the command line Found 2 diagnostics @@ -1026,7 +996,6 @@ fn cli_all_rules_precedence() -> anyhow::Result<()> { | 6 | prin(y) # unresolved-reference | ^^^^ - | info: rule `unresolved-reference` was selected on the command line Found 1 diagnostic @@ -1106,7 +1075,6 @@ fn configuration_all_rules() -> anyhow::Result<()> { | 6 | prin(y) # unresolved-reference | ^^^^ - | info: rule `unresolved-reference` was selected in the configuration file Found 1 diagnostic @@ -1154,7 +1122,7 @@ fn configuration_all_rules_with_rule_sorting_before_all() -> anyhow::Result<()> exit_code: 1 ----- stdout ----- error[abstract-method-in-final-class]: Final class `Derived` has unimplemented abstract methods - --> test.py:6:5 + --> test.py:11:7 | 6 | / @abstractmethod 7 | | def foo(self) -> int: @@ -1165,7 +1133,6 @@ fn configuration_all_rules_with_rule_sorting_before_all() -> anyhow::Result<()> | ------ 11 | class Derived(Base): | ^^^^^^^ `foo` is unimplemented - | info: rule `abstract-method-in-final-class` was selected in the configuration file Found 1 diagnostic @@ -1217,7 +1184,7 @@ fn overrides_all_rules_with_rule_sorting_before_all() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error[abstract-method-in-final-class]: Final class `Derived` has unimplemented abstract methods - --> src/test.py:6:5 + --> src/test.py:11:7 | 6 | / @abstractmethod 7 | | def foo(self) -> int: @@ -1228,7 +1195,6 @@ fn overrides_all_rules_with_rule_sorting_before_all() -> anyhow::Result<()> { | ------ 11 | class Derived(Base): | ^^^^^^^ `foo` is unimplemented - | info: rule `abstract-method-in-final-class` was selected in the configuration file Found 1 diagnostic @@ -1284,7 +1250,6 @@ fn all_overrides() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: error (global) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file error[unresolved-reference]: Name `prin` used when not defined @@ -1292,7 +1257,6 @@ fn all_overrides() -> anyhow::Result<()> { | 4 | prin(x) # unresolved-reference: error (global) | ^^^^ - | info: rule `unresolved-reference` was selected in the configuration file error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero @@ -1300,7 +1264,6 @@ fn all_overrides() -> anyhow::Result<()> { | 2 | y = 4 / 0 # division-by-zero: error (global) | ^^^^^ - | info: rule `division-by-zero` was selected in the configuration file warning[unresolved-reference]: Name `prin` used when not defined @@ -1308,7 +1271,6 @@ fn all_overrides() -> anyhow::Result<()> { | 4 | prin(x) # unresolved-reference: warn (override) | ^^^^ - | info: rule `unresolved-reference` was selected in the configuration file Found 4 diagnostics diff --git a/crates/ty/tests/cli/scripts.rs b/crates/ty/tests/cli/scripts.rs index 885340644e..dc44976430 100644 --- a/crates/ty/tests/cli/scripts.rs +++ b/crates/ty/tests/cli/scripts.rs @@ -40,7 +40,6 @@ fn project_settings_and_overrides_do_not_apply() -> anyhow::Result<()> { | 7 | print(missing) | ^^^^^^^ - | Found 1 diagnostic @@ -81,13 +80,12 @@ fn metadata_without_tool_ty_uses_default_settings() -> anyhow::Result<()> { exit_code: 1 ----- stdout ----- error[invalid-assignment]: Object of type `Literal["not an int"]` is not assignable to `int` - --> script.py:6:8 + --> script.py:6:14 | 6 | value: int = "not an int" | --- ^^^^^^^^^^^^ Incompatible value of type `Literal["not an int"]` | | | Declared type - | Found 1 diagnostic @@ -135,7 +133,6 @@ fn environment_options() -> anyhow::Result<()> { | 12 | reveal_type(sys.version_info[:2] == (3, 12)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Literal[True]` - | Found 1 diagnostic @@ -175,7 +172,6 @@ fn inline_overrides_are_ignored() -> anyhow::Result<()> { | 13 | print(missing) | ^^^^^^^ - | Found 1 diagnostic @@ -212,7 +208,6 @@ fn inline_terminal_settings_do_not_apply() -> anyhow::Result<()> { | 10 | print(missing) | ^^^^^^^ - | Found 1 diagnostic @@ -258,7 +253,6 @@ fn inline_settings_override_user_configuration() -> anyhow::Result<()> { | 10 | print(missing) # type: ignore | ^^^^^^^ - | Found 1 diagnostic @@ -301,14 +295,12 @@ fn user_configuration_applies() -> anyhow::Result<()> { | 6 | print(missing) | ^^^^^^^ - | warning[unresolved-reference]: Name `suppressed` used when not defined --> script.py:7:7 | 7 | print(suppressed) # type: ignore | ^^^^^^^^^^ - | Found 2 diagnostics @@ -351,7 +343,6 @@ fn cli_arguments_override_script_options() -> anyhow::Result<()> { | 10 | print(missing) | ^^^^^^^ - | Found 1 diagnostic @@ -403,7 +394,6 @@ fn explicit_config_replaces_inline_metadata() -> anyhow::Result<()> { | 10 | print(missing) | ^^^^^^^ - | Found 1 diagnostic diff --git a/crates/ty_ide/src/all_symbols.rs b/crates/ty_ide/src/all_symbols.rs index 3b62813e0a..ab25c746fa 100644 --- a/crates/ty_ide/src/all_symbols.rs +++ b/crates/ty_ide/src/all_symbols.rs @@ -615,7 +615,6 @@ def zqzqzq(): | 2 | from pandas.io.api import zqzqzq | ^^^^^^ - | info: Function zqzqzq "); } @@ -660,7 +659,6 @@ def zqzqzq(): | 2 | from pandas.io.api import zqzqzq as zqzqzq | ^^^^^^ - | info: Function zqzqzq "); } @@ -705,7 +703,6 @@ def zqzqzq(): | 2 | from pandas.io.api import * | ^ - | info: Function zqzqzq "); @@ -763,7 +760,6 @@ def zqzqzq(): | 2 | from pandas.io.parsers import zqzqzq | ^^^^^^ - | info: Function zqzqzq info[all-symbols]: AllSymbolInfo @@ -771,7 +767,6 @@ def zqzqzq(): | 2 | from pandas.io.parsers.readers import zqzqzq | ^^^^^^ - | info: Function zqzqzq "); } @@ -824,7 +819,6 @@ __all__ = ['zqzqzq'] | 2 | def zqzqzq(): | ^^^^^^ - | info: Function zqzqzq "); } @@ -855,7 +849,6 @@ def zqzqzq(): | 2 | def zqzqzq(): | ^^^^^^ - | info: Function zqzqzq info[all-symbols]: AllSymbolInfo @@ -863,7 +856,6 @@ def zqzqzq(): | 1 | from pandas import zqzqzq as zqzqzq | ^^^^^^ - | info: Function zqzqzq "); } @@ -897,7 +889,6 @@ def zqzqzq(): | 2 | def zqzqzq(): | ^^^^^^ - | info: Function zqzqzq info[all-symbols]: AllSymbolInfo @@ -905,7 +896,6 @@ def zqzqzq(): | 1 | from pandas import zqzqzq as zqzqzq | ^^^^^^ - | info: Function zqzqzq info[all-symbols]: AllSymbolInfo @@ -913,7 +903,6 @@ def zqzqzq(): | 1 | from pandas import zqzqzq as zqzqzq | ^^^^^^ - | info: Function zqzqzq "); } @@ -955,7 +944,6 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com' | 2 | ABCDEFGHIJKLMNOP = 'https://api.example.com' | ^^^^^^^^^^^^^^^^ - | info: Constant ABCDEFGHIJKLMNOP info[all-symbols]: AllSymbolInfo @@ -963,7 +951,6 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com' | 2 | class Abcdefghijklmnop: | ^^^^^^^^^^^^^^^^ - | info: Class Abcdefghijklmnop info[all-symbols]: AllSymbolInfo @@ -971,7 +958,6 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com' | 2 | def abcdefghijklmnop(): | ^^^^^^^^^^^^^^^^ - | info: Function abcdefghijklmnop "); } @@ -1002,7 +988,6 @@ def test_helper_xyzxyzxyz(): | 2 | def test_helper_xyzxyzxyz(): | ^^^^^^^^^^^^^^^^^^^^^ - | info: Function test_helper_xyzxyzxyz "); } @@ -1038,7 +1023,6 @@ def test_helper_xyzxyzxyz(): | 1 | def helper_xyzxyzxyz(): pass | ^^^^^^^^^^^^^^^^ - | info: Function helper_xyzxyzxyz info[all-symbols]: AllSymbolInfo @@ -1046,7 +1030,6 @@ def test_helper_xyzxyzxyz(): | 1 | def useful_xyzxyzxyz(): pass | ^^^^^^^^^^^^^^^^ - | info: Function useful_xyzxyzxyz "); } @@ -1071,7 +1054,6 @@ def test_helper_xyzxyzxyz(): | 1 | ZQZQZQ = 1 | ^^^^^^ - | info: Constant ZQZQZQ info[all-symbols]: AllSymbolInfo @@ -1079,7 +1061,6 @@ def test_helper_xyzxyzxyz(): | 1 | ZQZQZQ = 1 | ^^^^^^ - | info: Constant ZQZQZQ info[all-symbols]: AllSymbolInfo @@ -1087,7 +1068,6 @@ def test_helper_xyzxyzxyz(): | 1 | ZQZQZQ = 1 | ^^^^^^ - | info: Constant ZQZQZQ info[all-symbols]: AllSymbolInfo @@ -1095,7 +1075,6 @@ def test_helper_xyzxyzxyz(): | 1 | ZQZQZQ = 1 | ^^^^^^ - | info: Constant ZQZQZQ "); } @@ -1119,7 +1098,6 @@ def test_helper_xyzxyzxyz(): | 1 | ZQZQZQ = 1 | ^^^^^^ - | info: Constant ZQZQZQ "); } diff --git a/crates/ty_ide/src/call_hierarchy.rs b/crates/ty_ide/src/call_hierarchy.rs index 7fd38b180c..6b1f0ec596 100644 --- a/crates/ty_ide/src/call_hierarchy.rs +++ b/crates/ty_ide/src/call_hierarchy.rs @@ -246,7 +246,6 @@ mod tests { | 2 | def foo(): | ^^^ - | "); } @@ -264,7 +263,6 @@ mod tests { | 2 | class MyClass: | ^^^^^^^ - | "); } @@ -283,7 +281,6 @@ mod tests { | 3 | def method(self): | ^^^^^^ - | "); } @@ -303,7 +300,6 @@ mod tests { | 2 | def foo(): | ^^^ - | "); } @@ -340,21 +336,18 @@ mod tests { | 5 | def foo(x: int) -> int: ... | ^^^ - | info[prepare-call-hierarchy]: Function: `foo` (`main`) --> main.py:7:5 | 7 | def foo(x: str) -> str: ... | ^^^ - | info[prepare-call-hierarchy]: Function: `foo` (`main`) --> main.py:8:5 | 8 | def foo(x): | ^^^ - | "); } @@ -374,7 +367,6 @@ mod tests { | 2 | async def foo(): | ^^^ - | "); } @@ -394,7 +386,6 @@ mod tests { | 4 | def method(): | ^^^^^^ - | "); } @@ -414,7 +405,6 @@ mod tests { | 4 | def method(cls): | ^^^^^^ - | "); } } diff --git a/crates/ty_ide/src/call_hierarchy/incoming_calls.rs b/crates/ty_ide/src/call_hierarchy/incoming_calls.rs index 69e0d9fae0..68516e8d34 100644 --- a/crates/ty_ide/src/call_hierarchy/incoming_calls.rs +++ b/crates/ty_ide/src/call_hierarchy/incoming_calls.rs @@ -579,13 +579,11 @@ mod tests { | 6 | foo() | ^^^ Call site - | info: Function: `caller` (`main`) --> main.py:5:5 | 5 | def caller(): | ^^^^^^ - | "); } @@ -607,13 +605,11 @@ mod tests { | 7 | foo() # this is a call — should appear once | ^^^ Call site - | info: Function: `caller` (`main`) --> main.py:5:5 | 5 | def caller(): | ^^^^^^ - | "); } @@ -643,13 +639,11 @@ def use(): | 5 | foo() | ^^^ Call site - | info: Function: `use` (`caller`) --> caller.py:4:5 | 4 | def use(): | ^^^ - | "); } @@ -679,13 +673,11 @@ def use(): | 5 | bar() | ^^^ Call site - | info: Function: `use` (`caller`) --> caller.py:4:5 | 4 | def use(): | ^^^ - | "); } @@ -716,13 +708,11 @@ def invoke(value: Callable) -> int: | 5 | return value() | ^^^^^ Call site - | info: Function: `invoke` (`caller`) --> caller.py:4:5 | 4 | def invoke(value: Callable) -> int: | ^^^^^^ - | "); } @@ -743,13 +733,11 @@ def invoke(value: Callable) -> int: | 6 | foo(x=1) | ^^^ Call site - | info: Function: `caller` (`main`) --> main.py:5:5 | 5 | def caller(): | ^^^^^^ - | "); } @@ -769,7 +757,6 @@ def invoke(value: Callable) -> int: | 5 | foo() | ^^^ Call site - | info: Module: `main` --> main.py:1:1 "); @@ -794,7 +781,6 @@ def invoke(value: Callable) -> int: | 5 | @foo | ^^^ Call site - | info: Module: `main` --> main.py:1:1 "); @@ -825,13 +811,11 @@ class C: | 9 | def method(self, value=default()): | ^^^^^^^ Call site - | info: Class: `C` (`main`) --> main.py:7:7 | 7 | class C: | ^ - | "); } @@ -859,13 +843,11 @@ class C: | 11 | a.foo() | ^^^ Call site - | info: Function: `use` (`main`) --> main.py:10:5 | 10 | def use(a: A, b: B): | ^^^ - | "); } @@ -890,13 +872,11 @@ class C: | 8 | super().m() | ^ Call site - | info: Method: `m` (`main`) --> main.py:7:9 | 7 | def m(self): | ^ - | "); } @@ -927,13 +907,11 @@ def make() -> C: | 5 | return C() | ^ Call site - | info: Function: `make` (`caller`) --> caller.py:4:5 | 4 | def make() -> C: | ^^^^ - | "); } @@ -962,13 +940,11 @@ def make() -> C: | 8 | return c.prop | ^^^^ Call site - | info: Function: `read` (`main`) --> main.py:7:5 | 7 | def read(c: C) -> int: | ^^^^ - | "); } @@ -1000,13 +976,11 @@ def make() -> C: | 12 | c.prop = 5 | ^^^^ Call site - | info: Function: `write` (`main`) --> main.py:11:5 | 11 | def write(c: C) -> None: | ^^^^^ - | "); } @@ -1036,13 +1010,11 @@ def make() -> C: | 12 | del c.prop | ^^^^ Call site - | info: Function: `remove` (`main`) --> main.py:11:5 | 11 | def remove(c: C) -> None: | ^^^^^^ - | "); } @@ -1099,13 +1071,11 @@ def make() -> C: | 7 | return c.method() | ^^^^^^ Call site - | info: Function: `use` (`main`) --> main.py:6:5 | 6 | def use(c: C) -> int: | ^^^ - | "); } @@ -1148,13 +1118,11 @@ def make() -> C: | 5 | f = lambda x: target(x) | ^^^^^^ Call site - | info: Function: `(lambda)` (`main`) --> main.py:5:5 | 5 | f = lambda x: target(x) | ^^^^^^^^ - | "); let Some(target) = test .prepare_calls() @@ -1191,26 +1159,22 @@ def make() -> C: | 5 | a = lambda x: target(x) | ^^^^^^ Call site - | info: Function: `(lambda)` (`main`) --> main.py:5:5 | 5 | a = lambda x: target(x) | ^^^^^^^^ - | info[incoming-calls]: Incoming calls to `target` --> main.py:6:13 | 6 | b = lambda: target(0) | ^^^^^^ Call site - | info: Function: `(lambda)` (`main`) --> main.py:6:5 | 6 | b = lambda: target(0) | ^^^^^^ - | "); } @@ -1235,13 +1199,11 @@ def make() -> C: | 6 | f = lambda x: target(x) | ^^^^^^ Call site - | info: Function: `(lambda)` (`main`) --> main.py:6:9 | 6 | f = lambda x: target(x) | ^^^^^^^^ - | "); } @@ -1265,13 +1227,11 @@ def make() -> C: | 6 | return [target(x) for x in xs] | ^^^^^^ Call site - | info: Function: `caller` (`main`) --> main.py:5:5 | 5 | def caller(xs): | ^^^^^^ - | "); } diff --git a/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs b/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs index 647dfd76d7..47d8b8e4e7 100644 --- a/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs +++ b/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs @@ -368,13 +368,11 @@ mod tests { | 6 | helper() | ^^^^^^ Call site - | info: Function: `helper` (`main`) --> main.py:2:5 | 2 | def helper(): | ^^^^^^ - | "); } @@ -396,13 +394,11 @@ mod tests { | 7 | c.m() | ^ Call site - | info: Method: `m` (`main`) --> main.py:3:9 | 3 | def m(self): | ^ - | "); } @@ -423,13 +419,11 @@ mod tests { | 6 | C() | ^ Call site - | info: Class: `C` (`main`) --> main.py:2:7 | 2 | class C: | ^ - | "); } @@ -453,13 +447,11 @@ mod tests { | ^^^^^^ Call site 7 | helper() | ^^^^^^ Call site - | info: Function: `helper` (`main`) --> main.py:2:5 | 2 | def helper(): | ^^^^^^ - | "); } @@ -521,65 +513,55 @@ mod tests { | 17 | @cls_deco | ^^^^^^^^ Call site - | info: Function: `cls_deco` (`main`) --> main.py:2:5 | 2 | def cls_deco(cls): | ^^^^^^^^ - | info[outgoing-calls]: Outgoing calls from `Cls` --> main.py:18:11 | 18 | class Cls(base_factory()): | ^^^^^^^^^^^^ Call site - | info: Function: `base_factory` (`main`) --> main.py:5:5 | 5 | def base_factory(): | ^^^^^^^^^^^^ - | info[outgoing-calls]: Outgoing calls from `Cls` --> main.py:19:12 | 19 | attr = class_body_helper() | ^^^^^^^^^^^^^^^^^ Call site - | info: Function: `class_body_helper` (`main`) --> main.py:8:5 | 8 | def class_body_helper(): | ^^^^^^^^^^^^^^^^^ - | info[outgoing-calls]: Outgoing calls from `Cls` --> main.py:21:6 | 21 | @method_deco | ^^^^^^^^^^^ Call site - | info: Function: `method_deco` (`main`) --> main.py:11:5 | 11 | def method_deco(fn): | ^^^^^^^^^^^ - | info[outgoing-calls]: Outgoing calls from `Cls` --> main.py:22:19 | 22 | def m(self, x=default_factory()): | ^^^^^^^^^^^^^^^ Call site - | info: Function: `default_factory` (`main`) --> main.py:14:5 | 14 | def default_factory(): | ^^^^^^^^^^^^^^^ - | "); } @@ -605,13 +587,11 @@ mod tests { | 8 | nested() | ^^^^^^ Call site - | info: Function: `nested` (`main`) --> main.py:6:9 | 6 | def nested(): | ^^^^^^ - | "); } @@ -634,13 +614,11 @@ mod tests { | 5 | def foo(x=default_factory()): | ^^^^^^^^^^^^^^^ Call site - | info: Function: `default_factory` (`main`) --> main.py:2:5 | 2 | def default_factory(): | ^^^^^^^^^^^^^^^ - | "); } @@ -663,13 +641,11 @@ mod tests { | 5 | class Derived(base_factory()): | ^^^^^^^^^^^^ Call site - | info: Function: `base_factory` (`main`) --> main.py:2:5 | 2 | def base_factory(): | ^^^^^^^^^^^^ - | "); } @@ -698,13 +674,11 @@ mod tests { | 9 | f = lambda x=default_factory(): lambda_body_helper() | ^^^^^^^^^^^^^^^ Call site - | info: Function: `default_factory` (`main`) --> main.py:2:5 | 2 | def default_factory(): | ^^^^^^^^^^^^^^^ - | "); } @@ -723,26 +697,22 @@ mod tests { | LL | print("hi") # builtins resolve via stubs, so this *does* appear | ^^^^^ Call site - | info: Function: `print` (`builtins`) --> stdlib/builtins.pyi:LL:5 | LL | def print( | ^^^^^ - | info[outgoing-calls]: Outgoing calls from `foo` --> main.py:LL:5 | LL | print("hi") # builtins resolve via stubs, so this *does* appear | ^^^^^ Call site - | info: Function: `print` (`builtins`) --> stdlib/builtins.pyi:LL:5 | LL | def print( | ^^^^^ - | "#); } @@ -767,26 +737,22 @@ mod tests { | 8 | super().m() | ^ Call site - | info: Method: `m` (`main`) --> main.py:3:9 | 3 | def m(self): | ^ - | info[outgoing-calls]: Outgoing calls from `m` --> main.py:LL:9 | LL | super().m() | ^^^^^ Call site - | info: Class: `super` (`builtins`) --> stdlib/builtins.pyi:LL:7 | LL | class super: | ^^^^^ - | "); } @@ -817,13 +783,11 @@ def foo(): | 5 | helper() | ^^^^^^ Call site - | info: Function: `helper` (`lib`) --> lib.py:2:5 | 2 | def helper(): | ^^^^^^ - | "); } } diff --git a/crates/ty_ide/src/code_action.rs b/crates/ty_ide/src/code_action.rs index 2f28043eec..0246b42469 100644 --- a/crates/ty_ide/src/code_action.rs +++ b/crates/ty_ide/src/code_action.rs @@ -104,7 +104,6 @@ mod tests { 1 | b = a / 10 | ^ | - | - b = a / 10 1 + b = a / 10 # ty: ignore[unresolved-reference] | @@ -122,7 +121,6 @@ mod tests { 1 | b = a / 10 # fmt: off | ^ | - | - b = a / 10 # fmt: off 1 + b = a / 10 # fmt: off # ty: ignore[unresolved-reference] | @@ -152,7 +150,6 @@ mod tests { 2 | b = a / 0 # ty:ignore[division-by-zero] | ^ | - | 1 | - b = a / 0 # ty:ignore[division-by-zero] 2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference] @@ -175,7 +172,6 @@ mod tests { 2 | b = a / 10 # ty:ignore[] | ^ | - | 1 | - b = a / 10 # ty:ignore[] 2 + b = a / 10 # ty:ignore[unresolved-reference] @@ -200,7 +196,6 @@ mod tests { 4 | b = a / 10 | ^ | - | 2 | seen_code = True - # ty:ignore[] 3 + # ty:ignore[unresolved-reference] @@ -231,7 +226,6 @@ mod tests { 3 | # ty:ignore[] # ty:ignore[not-a-rule] # ty:ignore[division-by-zero] | ^^^^^^^^^^ | - | 2 | seen_code = True - # ty:ignore[] # ty:ignore[not-a-rule] # ty:ignore[division-by-zero] 3 + # ty:ignore[ignore-comment-unknown-rule] # ty:ignore[not-a-rule] # ty:ignore[division-by-zero] @@ -261,7 +255,6 @@ mod tests { 7 | absent, | ^^^^^^ | - | 2 | seen_code = True - # ty:ignore[] 3 + # ty:ignore[unresolved-reference] @@ -293,7 +286,6 @@ mod tests { 9 | absent, | ^^^^^^ | - | 4 | seen_code = True - # ty:ignore[invalid-assignment] 5 + # ty:ignore[invalid-assignment, unresolved-reference] @@ -317,7 +309,6 @@ mod tests { 2 | b = a / 0 # type:ignore[ty:division-by-zero] | ^ | - | 1 | - b = a / 0 # type:ignore[ty:division-by-zero] 2 + b = a / 0 # type:ignore[ty:division-by-zero, ty:unresolved-reference] @@ -340,7 +331,6 @@ mod tests { 2 | b = a / 0 # type:ignore[mypy-code] | ^ | - | 1 | - b = a / 0 # type:ignore[mypy-code] 2 + b = a / 0 # type:ignore[mypy-code] # ty: ignore[unresolved-reference] @@ -365,7 +355,6 @@ mod tests { 4 | b = a / 0 | ^ | - | 3 | - b = a / 0 4 + b = a / 0 # ty: ignore[unresolved-reference] @@ -388,7 +377,6 @@ mod tests { 2 | b = a / 0 # ty:ignore[division-by-zero,] | ^ | - | 1 | - b = a / 0 # ty:ignore[division-by-zero,] 2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference] @@ -411,7 +399,6 @@ mod tests { 2 | b = a / 0 # ty:ignore[division-by-zero ] | ^ | - | 1 | - b = a / 0 # ty:ignore[division-by-zero ] 2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference ] @@ -434,7 +421,6 @@ mod tests { 2 | b = a / 0 # ty:ignore[division-by-zero] some explanation | ^ | - | 1 | - b = a / 0 # ty:ignore[division-by-zero] some explanation 2 + b = a / 0 # ty:ignore[division-by-zero] some explanation # ty: ignore[unresolved-reference] @@ -463,7 +449,6 @@ mod tests { 5 | | 0 | |_________^ | - | 2 | b = ( - a # ty:ignore[division-by-zero] 3 + a # ty:ignore[division-by-zero, unresolved-reference] @@ -493,7 +478,6 @@ mod tests { 5 | | 0 # ty:ignore[division-by-zero] | |_________^ | - | 4 | / - 0 # ty:ignore[division-by-zero] 5 + 0 # ty:ignore[division-by-zero, unresolved-reference] @@ -523,7 +507,6 @@ mod tests { 5 | | 0 # ty:ignore[division-by-zero] | |_________^ | - | 2 | b = ( - a # ty:ignore[division-by-zero] 3 + a # ty:ignore[division-by-zero, unresolved-reference] @@ -550,7 +533,6 @@ mod tests { 3 | {a} | ^ | - | 4 | more text - """ 5 + """ # ty: ignore[unresolved-reference] @@ -578,7 +560,6 @@ mod tests { 4 | a | ^ | - | 3 | { - a 4 + a # ty: ignore[unresolved-reference] @@ -604,7 +585,6 @@ mod tests { 2 | b = a + """ | ^ | - | 3 | more text - """ 4 + """ # ty: ignore[unresolved-reference] @@ -628,7 +608,6 @@ mod tests { 2 | b = a \ | ^ | - | 2 | b = a \ - + "test" 3 + + "test" # ty: ignore[unresolved-reference] @@ -655,7 +634,6 @@ mod tests { 4 | + ddd \ | ^^^ | - | 4 | + ddd \ - 5 + # ty: ignore[unresolved-reference] @@ -678,7 +656,6 @@ mod tests { | 2 | reveal_type(1) | ^^^^^^^^^^^ - | help: This is a preferred code action | 1 + from typing import reveal_type @@ -691,7 +668,6 @@ mod tests { 2 | reveal_type(1) | ^^^^^^^^^^^ | - | 1 | - reveal_type(1) 2 + reveal_type(1) # ty: ignore[undefined-reveal] @@ -714,7 +690,6 @@ mod tests { | 2 | @deprecated("do not use") | ^^^^^^^^^^ - | help: This is a preferred code action | 1 + from warnings import deprecated @@ -727,7 +702,6 @@ mod tests { 2 | @deprecated("do not use") | ^^^^^^^^^^ | - | 1 | - @deprecated("do not use") 2 + @deprecated("do not use") # ty: ignore[unresolved-reference] @@ -753,7 +727,6 @@ mod tests { | 4 | @deprecated("do not use") | ^^^^^^^^^^ - | help: This is a preferred code action | 1 + from warnings import deprecated @@ -765,7 +738,6 @@ mod tests { | 4 | @deprecated("do not use") | ^^^^^^^^^^ - | help: This is a preferred code action | 3 | @@ -780,7 +752,6 @@ mod tests { 4 | @deprecated("do not use") | ^^^^^^^^^^ | - | 3 | - @deprecated("do not use") 4 + @deprecated("do not use") # ty: ignore[unresolved-reference] @@ -804,7 +775,6 @@ mod tests { | 2 | ExecutionLoader | ^^^^^^^^^^^^^^^ - | help: This is a preferred code action | 1 + from importlib.abc import ExecutionLoader @@ -817,7 +787,6 @@ mod tests { 2 | ExecutionLoader | ^^^^^^^^^^^^^^^ | - | 1 | - ExecutionLoader 2 + ExecutionLoader # ty: ignore[unresolved-reference] @@ -844,7 +813,6 @@ mod tests { | 3 | ExecutionLoader | ^^^^^^^^^^^^^^^ - | help: This is a preferred code action | 1 + from importlib.abc import ExecutionLoader @@ -857,7 +825,6 @@ mod tests { 3 | ExecutionLoader | ^^^^^^^^^^^^^^^ | - | 2 | import importlib - ExecutionLoader 3 + ExecutionLoader # ty: ignore[unresolved-reference] @@ -881,7 +848,6 @@ mod tests { | 3 | ExecutionLoader | ^^^^^^^^^^^^^^^ - | help: This is a preferred code action | 1 + from importlib.abc import ExecutionLoader @@ -893,7 +859,6 @@ mod tests { | 3 | ExecutionLoader | ^^^^^^^^^^^^^^^ - | help: This is a preferred code action | 2 | import importlib.abc @@ -907,7 +872,6 @@ mod tests { 3 | ExecutionLoader | ^^^^^^^^^^^^^^^ | - | 2 | import importlib.abc - ExecutionLoader 3 + ExecutionLoader # ty: ignore[unresolved-reference] diff --git a/crates/ty_ide/src/doc_highlights.rs b/crates/ty_ide/src/doc_highlights.rs index 66375681e0..31ca4400ce 100644 --- a/crates/ty_ide/src/doc_highlights.rs +++ b/crates/ty_ide/src/doc_highlights.rs @@ -72,14 +72,12 @@ mod tests { | 1 | from . import module_a | ^^^^^^^^ - | info[document_highlights]: Highlight 2 (Read) --> mypackage/__init__.py:2:5 | 2 | x = module_a | ^^^^^^^^ - | "); } @@ -127,28 +125,24 @@ def calculate_sum(): | 3 | value = 10 | ^^^^^ - | info[document_highlights]: Highlight 2 (Read) --> main.py:4:15 | 4 | doubled = value * 2 | ^^^^^ - | info[document_highlights]: Highlight 3 (Read) --> main.py:5:14 | 5 | result = value + doubled | ^^^^^ - | info[document_highlights]: Highlight 4 (Read) --> main.py:6:12 | 6 | return value | ^^^^^ - | "); } @@ -170,28 +164,24 @@ def process_data(data): | 2 | def process_data(data): | ^^^^ - | info[document_highlights]: Highlight 2 (Read) --> main.py:3:8 | 3 | if data: | ^^^^ - | info[document_highlights]: Highlight 3 (Read) --> main.py:4:21 | 4 | processed = data.upper() | ^^^^ - | info[document_highlights]: Highlight 4 (Read) --> main.py:6:12 | 6 | return data | ^^^^ - | "); } @@ -213,14 +203,12 @@ calc = Calculator() | 2 | class Calculator: | ^^^^^^^^^^ - | info[document_highlights]: Highlight 2 (Read) --> main.py:6:8 | 6 | calc = Calculator() | ^^^^^^^^^^ - | "); } @@ -259,21 +247,18 @@ def test(): | 2 | a: str = "test" | ^ - | info[document_highlights]: Highlight 2 (Write) --> main.py:4:1 | 4 | a: int = 10 | ^ - | info[document_highlights]: Highlight 3 (Read) --> main.py:6:7 | 6 | print(a) | ^ - | "#); } } diff --git a/crates/ty_ide/src/document_symbols.rs b/crates/ty_ide/src/document_symbols.rs index b6bf4eee3a..23b7950345 100644 --- a/crates/ty_ide/src/document_symbols.rs +++ b/crates/ty_ide/src/document_symbols.rs @@ -37,7 +37,6 @@ class World: | 2 | def hello(): | ^^^^^ - | info: Function hello info[document-symbols]: SymbolInfo @@ -45,7 +44,6 @@ class World: | 5 | class World: | ^^^^^ - | info: Class World info[document-symbols]: SymbolInfo @@ -53,7 +51,6 @@ class World: | 6 | def method(self): | ^^^^^^ - | info: Method method "); } @@ -96,7 +93,6 @@ def standalone_function(): | 5 | CONSTANT = 42 | ^^^^^^^^ - | info: Constant CONSTANT info[document-symbols]: SymbolInfo @@ -104,7 +100,6 @@ def standalone_function(): | 6 | variable = 'hello' | ^^^^^^^^ - | info: Variable variable info[document-symbols]: SymbolInfo @@ -112,7 +107,6 @@ def standalone_function(): | 7 | typed_global: str = 'typed' | ^^^^^^^^^^^^ - | info: Variable typed_global info[document-symbols]: SymbolInfo @@ -120,7 +114,6 @@ def standalone_function(): | 8 | annotated_only: int | ^^^^^^^^^^^^^^ - | info: Variable annotated_only info[document-symbols]: SymbolInfo @@ -128,7 +121,6 @@ def standalone_function(): | 10 | class MyClass: | ^^^^^^^ - | info: Class MyClass info[document-symbols]: SymbolInfo @@ -136,7 +128,6 @@ def standalone_function(): | 11 | class_var = 100 | ^^^^^^^^^ - | info: Field class_var info[document-symbols]: SymbolInfo @@ -144,7 +135,6 @@ def standalone_function(): | 12 | typed_class_var: str = 'class_typed' | ^^^^^^^^^^^^^^^ - | info: Field typed_class_var info[document-symbols]: SymbolInfo @@ -152,7 +142,6 @@ def standalone_function(): | 13 | annotated_class_var: float | ^^^^^^^^^^^^^^^^^^^ - | info: Field annotated_class_var info[document-symbols]: SymbolInfo @@ -160,7 +149,6 @@ def standalone_function(): | 15 | def __init__(self): | ^^^^^^^^ - | info: Constructor __init__ info[document-symbols]: SymbolInfo @@ -168,7 +156,6 @@ def standalone_function(): | 18 | def public_method(self): | ^^^^^^^^^^^^^ - | info: Method public_method info[document-symbols]: SymbolInfo @@ -176,7 +163,6 @@ def standalone_function(): | 21 | def _private_method(self): | ^^^^^^^^^^^^^^^ - | info: Method _private_method info[document-symbols]: SymbolInfo @@ -184,7 +170,6 @@ def standalone_function(): | 24 | def standalone_function(): | ^^^^^^^^^^^^^^^^^^^ - | info: Function standalone_function "); } @@ -211,7 +196,6 @@ class OuterClass: | 2 | class OuterClass: | ^^^^^^^^^^ - | info: Class OuterClass info[document-symbols]: SymbolInfo @@ -219,7 +203,6 @@ class OuterClass: | 3 | OUTER_CONSTANT = 100 | ^^^^^^^^^^^^^^ - | info: Constant OUTER_CONSTANT info[document-symbols]: SymbolInfo @@ -227,7 +210,6 @@ class OuterClass: | 5 | def outer_method(self): | ^^^^^^^^^^^^ - | info: Method outer_method info[document-symbols]: SymbolInfo @@ -235,7 +217,6 @@ class OuterClass: | 8 | class InnerClass: | ^^^^^^^^^^ - | info: Class InnerClass info[document-symbols]: SymbolInfo @@ -243,7 +224,6 @@ class OuterClass: | 9 | def inner_method(self): | ^^^^^^^^^^^^ - | info: Method inner_method "); } @@ -265,7 +245,6 @@ class Aliases: | 2 | type IntList = list[int] | ^^^^^^^ - | info: Variable IntList info[document-symbols]: SymbolInfo @@ -273,7 +252,6 @@ class Aliases: | 4 | class Aliases: | ^^^^^^^ - | info: Class Aliases info[document-symbols]: SymbolInfo @@ -281,7 +259,6 @@ class Aliases: | 5 | type Item = int | ^^^^ - | info: Variable Item "); } diff --git a/crates/ty_ide/src/find_references.rs b/crates/ty_ide/src/find_references.rs index bac9fb84b3..0b331e17cb 100644 --- a/crates/ty_ide/src/find_references.rs +++ b/crates/ty_ide/src/find_references.rs @@ -130,7 +130,6 @@ def outer(): 16 | write_nonlocal() 17 | return last | ---- - | "); } @@ -152,7 +151,6 @@ def f(items): | ---- 4 | return last | ---- - | "); } @@ -174,7 +172,6 @@ def f(items): | ---- 4 | return last | ---- - | "); } @@ -198,7 +195,6 @@ def f(items): | ---- 2 | print(last) | ---- - | "); } @@ -232,7 +228,6 @@ result = calculate_sum(value=42) 7 | # Call with keyword argument 8 | result = calculate_sum(value=42) | ----- - | "); } @@ -293,7 +288,6 @@ def outer_function(): 18 | decrement() 19 | final = counter | ------- - | "); } @@ -351,7 +345,6 @@ final_value = global_counter 17 | decrement_global() 18 | final_value = global_counter | -------------- - | "); } @@ -389,7 +382,6 @@ except ValueError as err: | --- 11 | print(f'Different error: {err}') | --- - | "); } @@ -416,7 +408,6 @@ match x: | ------- 5 | return pattern | ------- - | "); } @@ -444,7 +435,6 @@ match data: | ---- 6 | return rest | ---- - | "); } @@ -491,7 +481,6 @@ value = my_function | ----------- 14 | value = my_function | ----------- - | "); } @@ -547,7 +536,6 @@ test("test") 3 | 4 | test("test") | ---- - | "#); } @@ -594,7 +582,6 @@ cls = MyClass | 15 | cls = MyClass | ------- - | "); } @@ -618,7 +605,6 @@ cls = MyClass 3 | 4 | class MyClass: | ------- - | "#); } @@ -639,7 +625,6 @@ cls = MyClass | 2 | a: "MyClass" = 1 | ------- - | "#); } @@ -663,7 +648,6 @@ cls = MyClass 3 | 4 | class MyClass: | ------- - | "#); } @@ -701,7 +685,6 @@ cls = MyClass 3 | 4 | class MyClass: | ------- - | "#); } @@ -753,7 +736,6 @@ cls = MyClass 3 | 4 | class MyClass: | ------- - | "#); } @@ -785,7 +767,6 @@ cls = MyClass | 2 | ab: "ab" | -- -- - | "#); } @@ -819,7 +800,6 @@ cls = MyClass | -- 5 | x = ab | -- - | "#); } @@ -842,7 +822,6 @@ cls = MyClass | -- 5 | x = ab | -- - | "#); } @@ -865,7 +844,6 @@ cls = MyClass | -- 5 | x = ab | -- - | "#); } @@ -888,7 +866,6 @@ cls = MyClass | -- 5 | x = ab | -- - | "#); } @@ -911,7 +888,6 @@ cls = MyClass | -- 5 | x = ab | -- - | "#); } @@ -934,7 +910,6 @@ cls = MyClass | -- 5 | x = ab | -- - | "#); } @@ -963,7 +938,6 @@ cls = MyClass | -- 11 | x = ab | -- - | "); } @@ -992,7 +966,6 @@ cls = MyClass | -- 11 | x = ab | -- - | "); } @@ -1027,7 +1000,6 @@ cls = MyClass 9 | match event: 10 | case Click(x, button=ab): | ----- - | "); } @@ -1065,7 +1037,6 @@ cls = MyClass | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | -- -- -- - | "); } @@ -1083,7 +1054,6 @@ cls = MyClass | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | -- -- -- - | "); } @@ -1102,7 +1072,6 @@ cls = MyClass | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | -- -- -- - | "); } @@ -1121,7 +1090,6 @@ cls = MyClass | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | -- -- -- - | "); } @@ -1139,7 +1107,6 @@ cls = MyClass | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | -- -- -- - | "); } @@ -1157,7 +1124,6 @@ cls = MyClass | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | -- -- -- - | "); } @@ -1224,7 +1190,6 @@ class DataProcessor: | 2 | def func(x): | ---- - | "); } @@ -1274,7 +1239,6 @@ def process_model(): 5 | def get_attribute(self): 6 | return MyModel.attr | ---- - | "); } @@ -1312,7 +1276,6 @@ instance = ExampleClass(old_name="test") | -------- 4 | self.old_name = old_name | -------- - | "#); } @@ -1340,7 +1303,6 @@ TD(f=1) 7 | 8 | TD(f=1) | - - | "); } @@ -1368,7 +1330,6 @@ TD(f=1) 7 | 8 | TD(f=1) | - - | "); } @@ -1396,7 +1357,6 @@ NT(f=1) 7 | 8 | NT(f=1) | - - | "); } @@ -1425,7 +1385,6 @@ DC(f=1) 8 | 9 | DC(f=1) | - - | "); } @@ -1462,7 +1421,6 @@ result = func(value=42) | ----- 3 | return value * 2 | ----- - | "); } @@ -1504,7 +1462,6 @@ result = func(value=1) 4 | 5 | result = func(value=42) | ----- - | "); } @@ -1542,7 +1499,6 @@ async def main(): | ----- 3 | return value * 2 | ----- - | "); } @@ -1573,7 +1529,6 @@ instance = ExampleClass(old_name="test") | 4 | self.old_name = old_name | -------- - | "); } @@ -1611,7 +1566,6 @@ result = func(value=10) | ----- 4 | return value * 2 | ----- - | "); } @@ -1653,7 +1607,6 @@ result = instance.method(old_name="world") | -------- 4 | self.old_name = old_name | -------- - | "#); } @@ -1690,7 +1643,6 @@ func_alias() 3 | 4 | func_alias() | ---------- - | "); } @@ -1736,7 +1688,6 @@ func_alias() | 2 | class Path: | ---- - | "#); } @@ -1764,7 +1715,6 @@ func_alias() 4 | 5 | x = abc | --- - | "); } @@ -1792,7 +1742,6 @@ func_alias() 4 | 5 | x = abc | --- - | "); } @@ -1821,7 +1770,6 @@ func_alias() 4 | 5 | y = xyz | --- - | "); } @@ -1850,7 +1798,6 @@ func_alias() 4 | 5 | y = xyz | --- - | "); } @@ -1881,7 +1828,6 @@ func_alias() | 4 | x = subpkg | ------ - | "); } @@ -2014,7 +1960,6 @@ func_alias() | 2 | subpkg: int = 10 | ------ - | "); } @@ -2051,7 +1996,6 @@ func_alias() | 2 | subpkg: int = 10 | ------ - | "); } @@ -2083,7 +2027,6 @@ func_alias() 5 | 6 | print(a) | - - | "#); } @@ -2102,7 +2045,6 @@ print(x) | 3 | print(x) | - - | "); } @@ -2124,7 +2066,6 @@ print(x) | - 4 | print(x) | - - | "); } @@ -2146,7 +2087,6 @@ print(x) | - 4 | print(x) | - - | "); } @@ -2166,7 +2106,6 @@ print(x) | 4 | print(x) | - - | "); } @@ -2185,7 +2124,6 @@ value: Box | 3 | value: Box | --- - | "); } @@ -2209,7 +2147,6 @@ def test(flag: bool): | 8 | print(x) | - - | "); } @@ -2233,7 +2170,6 @@ def f(flag: bool): | - 6 | print(x) | - - | "); } @@ -2255,7 +2191,6 @@ print(x) | 6 | print(x) | - - | "); } @@ -2278,7 +2213,6 @@ class C: | 7 | print(self.x) | - - | "); } @@ -2303,7 +2237,6 @@ class C: | 9 | print(self.x) | - - | "); } } diff --git a/crates/ty_ide/src/folding_range.rs b/crates/ty_ide/src/folding_range.rs index cdcb38efc7..6a6cefa8b7 100644 --- a/crates/ty_ide/src/folding_range.rs +++ b/crates/ty_ide/src/folding_range.rs @@ -792,7 +792,6 @@ class MyClass: 6 | | def method(self): 7 | | return self.value | |_________________________^ - | info[folding-range]: Folding Range --> main.py:3:24 @@ -801,7 +800,6 @@ class MyClass: | ________________________^ 4 | | self.value = 1 | |______________________^ - | info[folding-range]: Folding Range --> main.py:6:22 @@ -810,7 +808,6 @@ class MyClass: | ______________________^ 7 | | return self.value | |_________________________^ - | "); } @@ -845,7 +842,6 @@ class MyClass: 7 | | attribute comment. 8 | | """ | |___________^ - | info[folding-range]: Folding Range --> main.py:3:24 @@ -858,7 +854,6 @@ class MyClass: 7 | | attribute comment. 8 | | """ | |___________^ - | info[folding-range]: Folding Range --> main.py:5:9 @@ -868,7 +863,6 @@ class MyClass: 7 | | attribute comment. 8 | | """ | |___________^ - | "#); } @@ -896,7 +890,6 @@ def main(): 3 | | import sys 4 | | from typing import List, Dict | |_____________________________^ - | info[folding-range]: Folding Range --> main.py:6:12 @@ -905,7 +898,6 @@ def main(): | ____________^ 7 | | pass | |________^ - | "); } @@ -934,7 +926,6 @@ import requests 2 | / import os 3 | | import sys | |__________^ - | info[folding-range]: Folding Range (imports) --> main.py:5:1 @@ -943,7 +934,6 @@ import requests 6 | | import pandas 7 | | import requests | |_______________^ - | "); } @@ -978,7 +968,6 @@ from fastapi import FastAPI 2 | / import os 3 | | from math import prod | |_____________________^ - | info[folding-range]: Folding Range (imports) --> main.py:12:1 @@ -986,7 +975,6 @@ from fastapi import FastAPI 12 | / import requests 13 | | from fastapi import FastAPI | |___________________________^ - | info[folding-range]: Folding Range --> main.py:5:5 @@ -996,7 +984,6 @@ from fastapi import FastAPI 6 | | import foo 7 | | import bar | |______________^ - | info[folding-range]: Folding Range (imports) --> main.py:6:5 @@ -1004,7 +991,6 @@ from fastapi import FastAPI 6 | / import foo 7 | | import bar | |______________^ - | info[folding-range]: Folding Range --> main.py:8:20 @@ -1014,7 +1000,6 @@ from fastapi import FastAPI 9 | | first = None 10 | | bar = None | |______________^ - | "); } @@ -1056,7 +1041,6 @@ class MyClass: 8 | | 9 | | do_something() | |__________________^ - | info[folding-range]: Folding Range (imports) --> main.py:3:5 @@ -1064,7 +1048,6 @@ class MyClass: 3 | / import os 4 | | import sys | |______________^ - | info[folding-range]: Folding Range (imports) --> main.py:6:5 @@ -1072,7 +1055,6 @@ class MyClass: 6 | / import numpy 7 | | import pandas | |_________________^ - | info[folding-range]: Folding Range --> main.py:12:15 @@ -1082,7 +1064,6 @@ class MyClass: 13 | | import typing 14 | | import collections | |______________________^ - | info[folding-range]: Folding Range (imports) --> main.py:13:5 @@ -1090,7 +1071,6 @@ class MyClass: 13 | / import typing 14 | | import collections | |______________________^ - | "); } @@ -1130,7 +1110,6 @@ else: | ______________^ 3 | | do_something() | |__________________^ - | info[folding-range]: Folding Range --> main.py:4:12 @@ -1139,7 +1118,6 @@ else: | ____________^ 5 | | do_other() | |______________^ - | info[folding-range]: Folding Range --> main.py:6:6 @@ -1148,7 +1126,6 @@ else: | ______^ 7 | | default() | |_____________^ - | info[folding-range]: Folding Range --> main.py:9:19 @@ -1157,7 +1134,6 @@ else: | ___________________^ 10 | | process(item) | |_________________^ - | info[folding-range]: Folding Range --> main.py:11:6 @@ -1166,7 +1142,6 @@ else: | ______^ 12 | | okay() | |__________^ - | info[folding-range]: Folding Range --> main.py:14:15 @@ -1175,7 +1150,6 @@ else: | _______________^ 15 | | continue_work() | |___________________^ - | info[folding-range]: Folding Range --> main.py:16:6 @@ -1184,7 +1158,6 @@ else: | ______^ 17 | | doit() | |__________^ - | "); } @@ -1257,7 +1230,6 @@ match value: 14 | | ): 15 | | return fallback | |_______________________^ - | info[folding-range]: Folding Range --> main.py:5:3 @@ -1275,7 +1247,6 @@ match value: 14 | | ): 15 | | return fallback | |_______________________^ - | info[folding-range]: Folding Range --> main.py:6:12 @@ -1288,7 +1259,6 @@ match value: 10 | | ): 11 | | return value | |____________________^ - | info[folding-range]: Folding Range --> main.py:10:7 @@ -1297,7 +1267,6 @@ match value: | _______^ 11 | | return value | |____________________^ - | info[folding-range]: Folding Range --> main.py:7:10 @@ -1307,7 +1276,6 @@ match value: 8 | | value, 9 | | ] | |________^ - | info[folding-range]: Folding Range --> main.py:12:11 @@ -1318,7 +1286,6 @@ match value: 14 | | ): 15 | | return fallback | |_______________________^ - | info[folding-range]: Folding Range --> main.py:14:7 @@ -1327,7 +1294,6 @@ match value: | _______^ 15 | | return fallback | |_______________________^ - | info[folding-range]: Folding Range --> main.py:17:18 @@ -1342,7 +1308,6 @@ match value: 23 | | ): 24 | | pass | |________^ - | info[folding-range]: Folding Range --> main.py:23:3 @@ -1351,7 +1316,6 @@ match value: | ___^ 24 | | pass | |________^ - | info[folding-range]: Folding Range --> main.py:26:5 @@ -1360,7 +1324,6 @@ match value: | _____^ 27 | | pass | |________^ - | info[folding-range]: Folding Range --> main.py:28:9 @@ -1372,7 +1335,6 @@ match value: 31 | | ) as error: 32 | | raise error | |_______________^ - | info[folding-range]: Folding Range --> main.py:31:12 @@ -1381,7 +1343,6 @@ match value: | ____________^ 32 | | raise error | |_______________^ - | info[folding-range]: Folding Range --> main.py:34:13 @@ -1394,7 +1355,6 @@ match value: 38 | | }: 39 | | handle_mapping() | |________________________^ - | info[folding-range]: Folding Range --> main.py:35:11 @@ -1406,7 +1366,6 @@ match value: 38 | | }: 39 | | handle_mapping() | |________________________^ - | info[folding-range]: Folding Range --> main.py:38:7 @@ -1415,7 +1374,6 @@ match value: | _______^ 39 | | handle_mapping() | |________________________^ - | "#); } @@ -1448,7 +1406,6 @@ def foo(x=[ 6 | | qux = x[0] + 1 7 | | return qux | |______________^ - | info[folding-range]: Folding Range --> main.py:5:4 @@ -1458,7 +1415,6 @@ def foo(x=[ 6 | | qux = x[0] + 1 7 | | return qux | |______________^ - | "); } @@ -1483,7 +1439,6 @@ if condition: # why | _____________________^ 3 | | do_work() | |_____________^ - | "); } @@ -1523,7 +1478,6 @@ if condition: 9 | | and_maybe_this() 10 | | and_maybe_this() | |____________________________^ - | info[folding-range]: Folding Range --> main.py:3:19 @@ -1538,7 +1492,6 @@ if condition: 9 | | and_maybe_this() 10 | | and_maybe_this() | |____________________________^ - | info[folding-range]: Folding Range --> main.py:6:18 @@ -1550,7 +1503,6 @@ if condition: 9 | | and_maybe_this() 10 | | and_maybe_this() | |____________________________^ - | "); } @@ -1587,7 +1539,6 @@ else: 3 | | process(item) 4 | | validate(item) | |__________________^ - | info[folding-range]: Folding Range --> main.py:5:6 @@ -1597,7 +1548,6 @@ else: 6 | | log_success() 7 | | notify_complete() | |_____________________^ - | info[folding-range]: Folding Range --> main.py:9:17 @@ -1607,7 +1557,6 @@ else: 10 | | do_work() 11 | | check_status() | |__________________^ - | info[folding-range]: Folding Range --> main.py:12:6 @@ -1617,7 +1566,6 @@ else: 13 | | handle_done() 14 | | cleanup_resources() | |_______________________^ - | "); } @@ -1650,7 +1598,6 @@ finally: | _____^ 3 | | risky_operation() | |_____________________^ - | info[folding-range]: Folding Range --> main.py:8:6 @@ -1659,7 +1606,6 @@ finally: | ______^ 9 | | success_action() | |____________________^ - | info[folding-range]: Folding Range --> main.py:10:9 @@ -1668,7 +1614,6 @@ finally: | _________^ 11 | | cleanup() | |_____________^ - | info[folding-range]: Folding Range --> main.py:4:19 @@ -1677,7 +1622,6 @@ finally: | ___________________^ 5 | | handle_value_error() | |________________________^ - | info[folding-range]: Folding Range --> main.py:6:18 @@ -1686,7 +1630,6 @@ finally: | __________________^ 7 | | handle_type_error() | |_______________________^ - | "); } @@ -1734,7 +1677,6 @@ my_list_with_trailing_own_line_comment = [ 4 | | 2, 5 | | 3, | |_______^ - | info[folding-range]: Folding Range --> main.py:8:12 @@ -1744,7 +1686,6 @@ my_list_with_trailing_own_line_comment = [ 9 | | "a": 1, 10 | | "b": 2, | |____________^ - | info[folding-range]: Folding Range --> main.py:13:42 @@ -1755,7 +1696,6 @@ my_list_with_trailing_own_line_comment = [ 15 | | 2, 16 | | 3, # reason | |_________________^ - | info[folding-range]: Folding Range --> main.py:19:43 @@ -1767,7 +1707,6 @@ my_list_with_trailing_own_line_comment = [ 22 | | 3, 23 | | # comment | |______________^ - | "#); } @@ -1830,7 +1769,6 @@ type Alias[ 3 | | first, 4 | | second, | |____________^ - | info[folding-range]: Folding Range --> main.py:7:11 @@ -1840,7 +1778,6 @@ type Alias[ 8 | | "a", 9 | | "b", | |_________^ - | info[folding-range]: Folding Range --> main.py:12:13 @@ -1850,7 +1787,6 @@ type Alias[ 13 | | first, 14 | | second, | |____________^ - | info[folding-range]: Folding Range --> main.py:17:17 @@ -1860,7 +1796,6 @@ type Alias[ 18 | | item 19 | | for item in items | |______________________^ - | info[folding-range]: Folding Range --> main.py:22:17 @@ -1870,7 +1805,6 @@ type Alias[ 23 | | item 24 | | for item in items | |______________________^ - | info[folding-range]: Folding Range --> main.py:27:16 @@ -1880,7 +1814,6 @@ type Alias[ 28 | | item 29 | | for item in items | |______________________^ - | info[folding-range]: Folding Range --> main.py:32:17 @@ -1890,7 +1823,6 @@ type Alias[ 33 | | key: value 34 | | for key, value in items | |____________________________^ - | info[folding-range]: Folding Range --> main.py:37:12 @@ -1900,7 +1832,6 @@ type Alias[ 38 | | T, 39 | | U, | |_______^ - | "#); } @@ -1940,7 +1871,6 @@ chained_call = ( | ___________________________^ 3 | | factory | |____________^ - | info[folding-range]: Folding Range --> main.py:4:3 @@ -1949,7 +1879,6 @@ chained_call = ( | ___^ 5 | | arg, | |_________^ - | info[folding-range]: Folding Range --> main.py:8:34 @@ -1958,7 +1887,6 @@ chained_call = ( | __________________________________^ 9 | | factory | |____________^ - | info[folding-range]: Folding Range --> main.py:16:3 @@ -1967,7 +1895,6 @@ chained_call = ( | ___^ 17 | | second, | |____________^ - | info[folding-range]: Folding Range --> main.py:12:17 @@ -1976,7 +1903,6 @@ chained_call = ( | _________________^ 13 | | factory | |____________^ - | info[folding-range]: Folding Range --> main.py:14:3 @@ -1985,7 +1911,6 @@ chained_call = ( | ___^ 15 | | first, | |___________^ - | "); } @@ -2015,7 +1940,6 @@ parenthesized_subscript_value = ( | ____________________________^ 3 | | key | |________^ - | info[folding-range]: Folding Range --> main.py:6:34 @@ -2024,7 +1948,6 @@ parenthesized_subscript_value = ( | __________________________________^ 7 | | data | |_________^ - | "); } @@ -2069,7 +1992,6 @@ multiline t-string 4 | | multiline string 5 | | """ | |___^ - | info[folding-range]: Folding Range --> main.py:7:19 @@ -2080,7 +2002,6 @@ multiline t-string 9 | | multiline bytes 10 | | """ | |___^ - | info[folding-range]: Folding Range --> main.py:12:21 @@ -2091,7 +2012,6 @@ multiline t-string 14 | | multiline f-string 15 | | """ | |___^ - | info[folding-range]: Folding Range --> main.py:17:21 @@ -2102,7 +2022,6 @@ multiline t-string 19 | | multiline t-string 20 | | """ | |___^ - | "#); } @@ -2137,7 +2056,6 @@ match value: 7 | | case _: 8 | | default() | |_________________^ - | info[folding-range]: Folding Range --> main.py:3:12 @@ -2146,7 +2064,6 @@ match value: | ____________^ 4 | | one() | |_____________^ - | info[folding-range]: Folding Range --> main.py:5:12 @@ -2155,7 +2072,6 @@ match value: | ____________^ 6 | | two() | |_____________^ - | info[folding-range]: Folding Range --> main.py:7:12 @@ -2164,7 +2080,6 @@ match value: | ____________^ 8 | | default() | |_________________^ - | "); } @@ -2195,7 +2110,6 @@ def main(): 3 | / import os 4 | | import sys | |__________^ - | info[folding-range]: Folding Range --> main.py:8:12 @@ -2204,7 +2118,6 @@ def main(): | ____________^ 9 | | pass | |________^ - | info[folding-range]: Folding Range (region) --> main.py:2:1 @@ -2214,7 +2127,6 @@ def main(): 4 | | import sys 5 | | # endregion | |___________^ - | info[folding-range]: Folding Range (region) --> main.py:7:1 @@ -2224,7 +2136,6 @@ def main(): 9 | | pass 10 | | # endregion | |___________^ - | "); } @@ -2255,7 +2166,6 @@ message = f""" 5 | | # endregion 6 | | """ | |___^ - | "#); } @@ -2288,7 +2198,6 @@ def my_function(): 6 | | """ 7 | | pass | |________^ - | info[folding-range]: Folding Range (comment) --> main.py:3:5 @@ -2298,7 +2207,6 @@ def my_function(): 5 | | docstring. 6 | | """ | |_______^ - | info[folding-range]: Folding Range --> main.py:3:5 @@ -2308,7 +2216,6 @@ def my_function(): 5 | | docstring. 6 | | """ | |_______^ - | "#); } @@ -2358,7 +2265,6 @@ def with_rawstring_doc(): 6 | | """ 7 | | pass | |________^ - | info[folding-range]: Folding Range (comment) --> main.py:3:5 @@ -2368,7 +2274,6 @@ def with_rawstring_doc(): 5 | | used as a docstring. 6 | | """ | |_______^ - | info[folding-range]: Folding Range --> main.py:3:5 @@ -2378,7 +2283,6 @@ def with_rawstring_doc(): 5 | | used as a docstring. 6 | | """ | |_______^ - | info[folding-range]: Folding Range --> main.py:10:24 @@ -2391,7 +2295,6 @@ def with_rawstring_doc(): 14 | | """ 15 | | pass | |________^ - | info[folding-range]: Folding Range (comment) --> main.py:11:5 @@ -2401,7 +2304,6 @@ def with_rawstring_doc(): 13 | | used as a docstring. 14 | | """ | |_______^ - | info[folding-range]: Folding Range --> main.py:11:5 @@ -2411,7 +2313,6 @@ def with_rawstring_doc(): 13 | | used as a docstring. 14 | | """ | |_______^ - | info[folding-range]: Folding Range --> main.py:18:26 @@ -2424,7 +2325,6 @@ def with_rawstring_doc(): 22 | | """ 23 | | pass | |________^ - | info[folding-range]: Folding Range (comment) --> main.py:19:5 @@ -2434,7 +2334,6 @@ def with_rawstring_doc(): 21 | | used as a docstring. 22 | | """ | |_______^ - | info[folding-range]: Folding Range --> main.py:19:5 @@ -2444,7 +2343,6 @@ def with_rawstring_doc(): 21 | | used as a docstring. 22 | | """ | |_______^ - | "#); } @@ -2478,7 +2376,6 @@ def foo(): | ___________^ 7 | | pass | |________^ - | info[folding-range]: Folding Range (comment) --> main.py:2:1 @@ -2487,7 +2384,6 @@ def foo(): 3 | | # that spans multiple lines 4 | | # explaining something important | |________________________________^ - | info[folding-range]: Folding Range (comment) --> main.py:9:1 @@ -2495,7 +2391,6 @@ def foo(): 9 | / # Another comment block 10 | | # with more details | |___________________^ - | ", ); } @@ -2523,7 +2418,6 @@ with open("file.txt") as f: 3 | | content = f.read() 4 | | process(content) | |____________________^ - | "#); } @@ -2577,7 +2471,6 @@ with open("file.txt") as f: 16 | | # Don't exceed the overall end date or max_days limit 17 | | c = 30 | |______________________________^ - | info[folding-range]: Folding Range (comment) --> main.py:3:21 @@ -2588,7 +2481,6 @@ with open("file.txt") as f: 6 | | into smaller chunks that can be requested individually. 7 | | """ | |_______________________^ - | info[folding-range]: Folding Range --> main.py:3:21 @@ -2599,7 +2491,6 @@ with open("file.txt") as f: 6 | | into smaller chunks that can be requested individually. 7 | | """ | |_______________________^ - | info[folding-range]: Folding Range --> main.py:11:42 @@ -2613,7 +2504,6 @@ with open("file.txt") as f: 16 | | # Don't exceed the overall end date or max_days limit 17 | | c = 30 | |______________________________^ - | info[folding-range]: Folding Range (comment) --> main.py:12:1 @@ -2621,7 +2511,6 @@ with open("file.txt") as f: 12 | / # Calculate the end of the current chunk 13 | | # Go to the last day of the current month | |_________________________________________________________________^ - | "#); } @@ -2655,7 +2544,6 @@ with open("file.txt") as f: | _______________^ 2 | | pass | |________^ - | "); // So does a single CRLF new-line. @@ -2670,7 +2558,6 @@ with open("file.txt") as f: | _______________^ 2 | | pass | |________^ - | "); // And so to does a single CR new-line. @@ -2685,7 +2572,6 @@ with open("file.txt") as f: | _______________^ 2 | | pass | |________^ - | "); } @@ -2728,7 +2614,6 @@ def my_function(): | ___________________^ 4 | | pass | |________^ - | "); } @@ -2756,7 +2641,6 @@ def my_function(): | ___________________^ 6 | | pass | |________^ - | "); } @@ -2785,7 +2669,6 @@ class MyClass: 4 | | value: int 5 | | name: str | |_____________^ - | "); } @@ -2812,7 +2695,6 @@ class MyClass: | _______________^ 5 | | value: int | |______________^ - | "); } @@ -2838,7 +2720,6 @@ async def my_async_function(): | _______________________________^ 4 | | pass | |________^ - | "); } @@ -2867,7 +2748,6 @@ def outer_function(): 4 | | def inner_function(): 5 | | pass | |____________^ - | info[folding-range]: Folding Range --> main.py:4:26 @@ -2876,7 +2756,6 @@ def outer_function(): | __________________________^ 5 | | pass | |____________^ - | "); } @@ -2905,7 +2784,6 @@ class MyClass: 4 | | async def my_async_method(self): 5 | | pass | |____________^ - | info[folding-range]: Folding Range --> main.py:4:37 @@ -2914,7 +2792,6 @@ class MyClass: | _____________________________________^ 5 | | pass | |____________^ - | "); } diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index 86e7bb5351..21b591f97e 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -53,13 +53,11 @@ mod tests { | 5 | result = my_function(1, 2) | ^^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:2:5 | 2 | def my_function(x, y): | ----------- - | "); } @@ -78,13 +76,11 @@ mod tests { | 3 | y = x | ^ Clicking here - | info: Found 1 declaration --> main.py:2:1 | 2 | x = 42 | - - | "); } @@ -109,13 +105,11 @@ mod tests { | 9 | person["name"] | ^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:5:5 | 5 | name: str | ---- - | "#); } @@ -137,13 +131,11 @@ mod tests { | 6 | instance = MyClass() | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:2:7 | 2 | class MyClass: | ------- - | "); } @@ -162,13 +154,11 @@ mod tests { | 3 | return param * 2 | ^^^^^ Clicking here - | info: Found 1 declaration --> main.py:2:9 | 2 | def foo(param): | ----- - | "); } @@ -188,13 +178,11 @@ mod tests { | 3 | v: T = value | ^ Clicking here - | info: Found 1 declaration --> main.py:2:18 | 2 | def generic_func[T](value: T) -> T: | - - | "); } @@ -214,13 +202,11 @@ mod tests { | 3 | def __init__(self, value: T): | ^ Clicking here - | info: Found 1 declaration --> main.py:2:20 | 2 | class GenericClass[T]: | - - | "); } @@ -242,13 +228,11 @@ mod tests { | 5 | return x # Should find outer x | ^ Clicking here - | info: Found 1 declaration --> main.py:2:1 | 2 | x = "outer" | - - | "#); } @@ -297,13 +281,11 @@ variable = 42 | 3 | print(mymodule.function()) | ^^^^^^^^ Clicking here - | info: Found 1 declaration --> mymodule.py:1:1 | 1 | | - - | "); } @@ -335,13 +317,11 @@ def other_function(): | 3 | print(my_function()) | ^^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> mymodule.py:2:5 | 2 | def my_function(): | ----------- - | "); } @@ -376,13 +356,11 @@ FOO = 0 | 3 | print(sub.helper()) | ^^^ Clicking here - | info: Found 1 declaration --> mymodule/submodule.py:1:1 | 1 | | - - | "); } @@ -401,11 +379,11 @@ FOO = 0 | 1 | from lib import module | ^^^^^^ Clicking here - | info: Found 1 declaration - --> lib/module.py:1:1 - | - | + --> lib/module.py:1:1 + | + 1 | + | - "); } @@ -435,13 +413,11 @@ def func(arg): | 3 | print(h("test")) | ^ Clicking here - | info: Found 1 declaration --> utils.py:2:5 | 2 | def func(arg): | ---- - | "#); } @@ -477,13 +453,11 @@ def shared_function(): | 3 | print(shared_function()) | ^^^^^^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> original.py:2:5 | 2 | def shared_function(): | --------------- - | "); } @@ -517,13 +491,11 @@ def multiply_numbers(a, b): | 3 | result = add_numbers(5, 3) | ^^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> math_utils.py:2:5 | 2 | def add_numbers(a, b): | ----------- - | "); } @@ -564,13 +536,11 @@ def another_helper(): | 3 | result = helper_function("test") | ^^^^^^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> package/utils.py:2:5 | 2 | def helper_function(arg): | --------------- - | "#); } @@ -610,13 +580,11 @@ def another_helper(): | 3 | result = helper_function("test") | ^^^^^^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> package/utils.py:2:5 | 2 | def helper_function(arg): | --------------- - | "#); } @@ -650,13 +618,11 @@ FOO = 0 | 2 | import mymodule.submodule as sub | ^^^ Clicking here - | info: Found 1 declaration --> mymodule/submodule.py:1:1 | 1 | | - - | "); } @@ -690,13 +656,11 @@ FOO = 0 | 2 | import mymodule.submodule as sub | ^^^^^^^^^ Clicking here - | info: Found 1 declaration --> mymodule/submodule.py:1:1 | 1 | | - - | "); } @@ -734,13 +698,11 @@ def another_helper(path): | 2 | from mypackage.utils import helper as h | ^^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/utils.py:2:5 | 2 | def helper(a, b): | ------ - | "); } @@ -778,13 +740,11 @@ def another_helper(path): | 2 | from mypackage.utils import helper as h | ^ Clicking here - | info: Found 1 declaration --> mypackage/utils.py:2:5 | 2 | def helper(a, b): | ------ - | "); } @@ -822,13 +782,11 @@ def another_helper(path): | 2 | from mypackage.utils import helper as h | ^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/utils.py:1:1 | 1 | | - - | "); } @@ -851,13 +809,11 @@ def another_helper(path): | 7 | y = c.x | ^ Clicking here - | info: Found 1 declaration --> main.py:4:9 | 4 | self.x: int = 1 | ------ - | "); } @@ -878,13 +834,11 @@ def another_helper(path): | 2 | a: "MyClass" = 1 | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -905,13 +859,11 @@ def another_helper(path): | 2 | a: "None | MyClass" = 1 | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -946,13 +898,11 @@ def another_helper(path): | 2 | a: "None | MyClass" = 1 | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1001,13 +951,11 @@ def another_helper(path): | 2 | a: "MyClass | No" = 1 | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1039,13 +987,11 @@ def another_helper(path): | 2 | ab: "ab" | ^^ Clicking here - | info: Found 1 declaration --> main.py:2:1 | 2 | ab: "ab" | -- - | "#); } @@ -1077,13 +1023,11 @@ def another_helper(path): | 2 | x: "list['MyClass | int'] | None" | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1104,13 +1048,11 @@ def another_helper(path): | 2 | x: "list['int | MyClass'] | None" | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1131,13 +1073,11 @@ def another_helper(path): | 2 | x: "list['int | None'] | MyClass" | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1158,13 +1098,11 @@ def another_helper(path): | 2 | x: "list['int' | 'MyClass'] | None" | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1185,13 +1123,11 @@ def another_helper(path): | 2 | x: "list['MyClass' | 'str'] | None" | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1226,13 +1162,11 @@ def another_helper(path): | 2 | x: """'list["int" | "str"]' | MyClass""" | ^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1259,13 +1193,11 @@ def another_helper(path): | 11 | y = d.y.x | ^ Clicking here - | info: Found 1 declaration --> main.py:4:9 | 4 | self.x: int = 1 | ------ - | "); } @@ -1288,13 +1220,11 @@ def another_helper(path): | 7 | y = c.x | ^ Clicking here - | info: Found 1 declaration --> main.py:4:9 | 4 | self.x = 1 | ------ - | "); } @@ -1317,13 +1247,11 @@ def another_helper(path): | 7 | res = c.foo() | ^^^ Clicking here - | info: Found 1 declaration --> main.py:3:9 | 3 | def foo(self): | --- - | "); } @@ -1387,13 +1315,11 @@ def outer(): | 8 | return x # Should find the nonlocal x declaration in outer scope | ^ Clicking here - | info: Found 1 declaration --> main.py:3:5 | 3 | x = "outer_value" | - - | "#); } @@ -1420,13 +1346,11 @@ def outer(): | 6 | nonlocal xy | ^^ Clicking here - | info: Found 1 declaration --> main.py:3:5 | 3 | xy = "outer_value" | -- - | "#); } @@ -1450,13 +1374,11 @@ def function(): | 7 | return global_var # Should find the global variable declaration | ^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:2:1 | 2 | global_var = "global_value" | ---------- - | "#); } @@ -1480,13 +1402,11 @@ def function(): | 5 | global global_var | ^^^^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:2:1 | 2 | global_var = "global_value" | ---------- - | "#); } @@ -1511,13 +1431,11 @@ def function(): | 9 | y = b.x | ^ Clicking here - | info: Found 1 declaration --> main.py:3:5 | 3 | x = 10 | - - | "); } @@ -1538,13 +1456,11 @@ def function(): | 4 | case ["get", ab]: | ^^ Clicking here - | info: Found 1 declaration --> main.py:4:22 | 4 | case ["get", ab]: | -- - | "#); } @@ -1565,13 +1481,11 @@ def function(): | 5 | x = ab | ^^ Clicking here - | info: Found 1 declaration --> main.py:4:22 | 4 | case ["get", ab]: | -- - | "#); } @@ -1592,13 +1506,11 @@ def function(): | 4 | case ["get", *ab]: | ^^ Clicking here - | info: Found 1 declaration --> main.py:4:23 | 4 | case ["get", *ab]: | -- - | "#); } @@ -1619,13 +1531,11 @@ def function(): | 5 | x = ab | ^^ Clicking here - | info: Found 1 declaration --> main.py:4:23 | 4 | case ["get", *ab]: | -- - | "#); } @@ -1646,13 +1556,11 @@ def function(): | 4 | case ["get", ("a" | "b") as ab]: | ^^ Clicking here - | info: Found 1 declaration --> main.py:4:37 | 4 | case ["get", ("a" | "b") as ab]: | -- - | "#); } @@ -1673,13 +1581,11 @@ def function(): | 5 | x = ab | ^^ Clicking here - | info: Found 1 declaration --> main.py:4:37 | 4 | case ["get", ("a" | "b") as ab]: | -- - | "#); } @@ -1706,13 +1612,11 @@ def function(): | 10 | case Click(x, button=ab): | ^^ Clicking here - | info: Found 1 declaration --> main.py:10:30 | 10 | case Click(x, button=ab): | -- - | "); } @@ -1739,13 +1643,11 @@ def function(): | 11 | x = ab | ^^ Clicking here - | info: Found 1 declaration --> main.py:10:30 | 10 | case Click(x, button=ab): | -- - | "); } @@ -1772,13 +1674,11 @@ def function(): | 10 | case Click(x, button=ab): | ^^^^^ Clicking here - | info: Found 1 declaration --> main.py:2:7 | 2 | class Click: | ----- - | "); } @@ -1816,13 +1716,11 @@ def function(): | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | ^^ Clicking here - | info: Found 1 declaration --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | -- - | "); } @@ -1840,13 +1738,11 @@ def function(): | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | ^^ Clicking here - | info: Found 1 declaration --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | -- - | "); } @@ -1865,13 +1761,11 @@ def function(): | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | ^^ Clicking here - | info: Found 1 declaration --> main.py:3:15 | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | -- - | "); } @@ -1890,13 +1784,11 @@ def function(): | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | ^^ Clicking here - | info: Found 1 declaration --> main.py:3:15 | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | -- - | "); } @@ -1914,13 +1806,11 @@ def function(): | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | ^^ Clicking here - | info: Found 1 declaration --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | -- - | "); } @@ -1938,13 +1828,11 @@ def function(): | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | ^^ Clicking here - | info: Found 1 declaration --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | -- - | "); } @@ -1971,13 +1859,11 @@ def function(): | 11 | c.value = 42 | ^^^^^ Clicking here - | info: Found 1 declaration --> main.py:7:9 | 7 | def value(self): | ----- - | "); } @@ -2041,13 +1927,11 @@ def function(): | 9 | obj.name | ^^^^ Clicking here - | info: Found 1 declaration --> main.py:6:5 | 6 | name: str | ---- - | "); } @@ -2070,13 +1954,11 @@ class MyClass: | 5 | def generic_method[T](self, value: ClassType) -> T: | ^^^^^^^^^ Clicking here - | info: Found 1 declaration --> main.py:3:5 | 3 | ClassType = int | --------- - | "); } @@ -2097,13 +1979,11 @@ class MyClass: | 5 | result = my_function(1, y=2, z=3) | ^ Clicking here - | info: Found 1 declaration --> main.py:2:20 | 2 | def my_function(x, y, z=10): | - - | "); } @@ -2134,7 +2014,6 @@ class MyClass: | 14 | result = process("hello", format="json") | ^^^^^^ Clicking here - | info: Found 2 declarations --> main.py:5:24 | @@ -2144,7 +2023,6 @@ class MyClass: 7 | @overload 8 | def process(data: int, format: int) -> int: ... | ------ - | "#); } @@ -2168,13 +2046,11 @@ class MyClass: | 8 | TD(f=1) | ^ Clicking here - | info: Found 1 declaration --> main.py:5:5 | 5 | f: int | - - | "); } @@ -2198,13 +2074,11 @@ class MyClass: | 8 | NT(f=1) | ^ Clicking here - | info: Found 1 declaration --> main.py:5:5 | 5 | f: int | - - | "); } @@ -2229,13 +2103,11 @@ class MyClass: | 9 | DC(f=1) | ^ Clicking here - | info: Found 1 declaration --> main.py:6:5 | 6 | f: int | - - | "); } @@ -2262,13 +2134,11 @@ class MyClass: | 11 | DC(f=1) | ^ Clicking here - | info: Found 1 declaration --> main.py:9:24 | 9 | def __init__(self, f: int) -> None: ... | - - | "); } @@ -2296,13 +2166,11 @@ class MyClass: | 12 | DC(g=1) | ^ Clicking here - | info: Found 1 declaration --> main.py:10:5 | 10 | f: int = Field(alias='g') | - - | "); } @@ -2344,7 +2212,6 @@ def ab(a: str): ... | 4 | ab(1) | ^^ Clicking here - | info: Found 2 declarations --> mymodule.pyi:5:5 | @@ -2354,7 +2221,6 @@ def ab(a: str): ... 7 | @overload 8 | def ab(a: str): ... | -- - | "); } @@ -2396,7 +2262,6 @@ def ab(a: str): ... | 4 | ab("hello") | ^^ Clicking here - | info: Found 2 declarations --> mymodule.pyi:5:5 | @@ -2406,7 +2271,6 @@ def ab(a: str): ... 7 | @overload 8 | def ab(a: str): ... | -- - | "#); } @@ -2448,7 +2312,6 @@ def ab(a: int): ... | 4 | ab(1, 2) | ^^ Clicking here - | info: Found 2 declarations --> mymodule.pyi:5:5 | @@ -2458,7 +2321,6 @@ def ab(a: int): ... 7 | @overload 8 | def ab(a: int): ... | -- - | "); } @@ -2500,7 +2362,6 @@ def ab(a: int): ... | 4 | ab(1) | ^^ Clicking here - | info: Found 2 declarations --> mymodule.pyi:5:5 | @@ -2510,7 +2371,6 @@ def ab(a: int): ... 7 | @overload 8 | def ab(a: int): ... | -- - | "); } @@ -2555,7 +2415,6 @@ def ab(a: int, *, c: int): ... | 4 | ab(1, b=2) | ^^ Clicking here - | info: Found 3 declarations --> mymodule.pyi:5:5 | @@ -2569,7 +2428,6 @@ def ab(a: int, *, c: int): ... 10 | @overload 11 | def ab(a: int, *, c: int): ... | -- - | "); } @@ -2614,7 +2472,6 @@ def ab(a: int, *, c: int): ... | 4 | ab(1, c=2) | ^^ Clicking here - | info: Found 3 declarations --> mymodule.pyi:5:5 | @@ -2628,7 +2485,6 @@ def ab(a: int, *, c: int): ... 10 | @overload 11 | def ab(a: int, *, c: int): ... | -- - | "); } @@ -2658,13 +2514,11 @@ def ab(a: int, *, c: int): ... | 4 | x = subpkg | ^^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/__init__.py:2:7 | 2 | from .subpkg.submod import val | ------ - | "); } @@ -2698,11 +2552,11 @@ def ab(a: int, *, c: int): ... | 2 | from .subpkg.submod import val | ^^^^^^ Clicking here - | info: Found 1 declaration - --> mypackage/subpkg/__init__.py:1:1 - | - | + --> mypackage/subpkg/__init__.py:1:1 + | + 1 | + | - "); } @@ -2757,13 +2611,11 @@ def ab(a: int, *, c: int): ... | 2 | from .subpkg.submod import val | ^^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/subpkg/submod.py:1:1 | 1 | | - - | "); } @@ -2793,13 +2645,11 @@ def ab(a: int, *, c: int): ... | 2 | from .subpkg import subpkg | ^^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/subpkg/__init__.py:1:1 | 1 | | - - | "); } @@ -2829,13 +2679,11 @@ def ab(a: int, *, c: int): ... | 2 | from .subpkg import subpkg | ^^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/subpkg/__init__.py:2:1 | 2 | subpkg: int = 10 | ------ - | "); } @@ -2866,13 +2714,11 @@ def ab(a: int, *, c: int): ... | 4 | x = subpkg | ^^^^^^ Clicking here - | info: Found 1 declaration --> mypackage/subpkg/__init__.py:2:1 | 2 | subpkg: int = 10 | ------ - | "); } @@ -2900,7 +2746,6 @@ def ab(a: int, *, c: int): ... | 6 | print(a) | ^ Clicking here - | info: Found 3 declarations --> main.py:2:1 | @@ -2914,7 +2759,6 @@ def ab(a: int, *, c: int): ... 7 | 8 | a: bool = True | - - | "#); } diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index 9f6295b0ef..c4fdbf790a 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -71,7 +71,6 @@ def outer(): | 17 | return last | ^^^^ Clicking here - | info: Found 2 definitions --> main.py:5:5 | @@ -82,7 +81,6 @@ def outer(): | 13 | [(last := nonlocal_item) for nonlocal_item in [3]] | ---- - | "); } @@ -102,13 +100,11 @@ def f(items): | 4 | return last | ^^^^ Clicking here - | info: Found 1 definition --> main.py:3:7 | 3 | [(last := item) for item in items] | ---- - | "); } @@ -128,13 +124,11 @@ def f(items): | 4 | return last | ^^^^ Clicking here - | info: Found 1 definition --> main.py:3:8 | 3 | [[(last := item) for item in items] for _ in [1]] | ---- - | "); } @@ -151,13 +145,11 @@ def f(items): | 2 | print(last) | ^^^^ Clicking here - | info: Found 1 definition --> lib.py:1:3 | 1 | [(last := item) for item in [1]] | ---- - | "); } @@ -183,7 +175,6 @@ def outer(items): | 8 | return last | ^^^^ Clicking here - | info: Found 2 definitions --> main.py:3:5 | @@ -194,7 +185,6 @@ def outer(items): 6 | nonlocal last 7 | [(last := item) for item in items] | ---- - | "); } @@ -211,13 +201,11 @@ def outer(items): | 1 | from . import module_a | ^^^^^^^^ Clicking here - | info: Found 1 definition --> mypackage/module_a.py:1:1 | 1 | class Test: ... | - - | "); } @@ -237,13 +225,11 @@ def outer(items): | 2 | x = module_a | ^^^^^^^^ Clicking here - | info: Found 1 definition --> mypackage/module_a.py:1:1 | 1 | class Test: ... | - - | "); } @@ -264,13 +250,11 @@ def outer(items): | 2 | x = module_a | ^^^^^^^^ Clicking here - | info: Found 1 definition --> mypackage/module_a.py:1:1 | 1 | class Test: ... | - - | "); } @@ -308,13 +292,11 @@ def my_function(): ... | 2 | from mymodule import my_function | ^^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:1:1 | 1 | | - - | "); } @@ -350,13 +332,11 @@ def my_function(): ... | 3 | x = mymodule | ^^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:1:1 | 1 | | - - | "); } @@ -397,13 +377,11 @@ def other_function(): ... | 3 | print(my_function()) | ^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def my_function(): | ----------- - | "); } @@ -434,13 +412,11 @@ def bar() -> None: | 3 | bar() | ^^^ Clicking here - | info: Found 1 definition --> a/impl.py:2:5 | 2 | def bar() -> None: | --- - | "); } @@ -474,13 +450,11 @@ def other_function(): ... | 2 | def my_function(): ... | ^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def my_function(): | ----------- - | "); } @@ -531,7 +505,6 @@ def other_function(): ... | 3 | print(my_function()) | ^^^^^^^^^^^ Clicking here - | info: Found 3 definitions --> mymodule.py:2:5 | @@ -545,7 +518,6 @@ def other_function(): ... 7 | 8 | def my_function(): | ----------- - | "#); } @@ -590,13 +562,11 @@ class MyOtherClass: | 3 | x = MyClass | ^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:7 | 2 | class MyClass: | ------- - | "); } @@ -634,13 +604,11 @@ class MyOtherClass: | 2 | class MyClass: | ^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:7 | 2 | class MyClass: | ------- - | "); } @@ -685,13 +653,11 @@ class MyOtherClass: | 3 | x = MyClass(0) | ^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:7 | 2 | class MyClass: | ------- - | "); } @@ -740,13 +706,11 @@ class MyOtherClass: | 4 | x.action() | ^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:5:9 | 5 | def action(self): | ------ - | "); } @@ -784,13 +748,11 @@ class MyClass: | 4 | x.sound | ^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:3:5 | 3 | sound: str = "generic" | ----- - | "#); } @@ -825,13 +787,11 @@ COUNT: int | 3 | mymodule.COUNT | ^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:1 | 2 | COUNT = 0 | ----- - | "); } @@ -879,13 +839,11 @@ class MyOtherClass: | 3 | x = MyClass.action() | ^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:5:9 | 5 | def action(): | ------ - | "); } @@ -919,13 +877,11 @@ class MyClass: ... | 2 | from mymodule import MyClass | ^^^^^^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:7 | 2 | class MyClass: ... | ------- - | "); } @@ -953,13 +909,11 @@ my_func(my_other_func(ab=5, y=2), 0) | 5 | my_other_func(my_func(ab=5, y=2), 0) | ^^ Clicking here - | info: Found 1 definition --> main.py:2:13 | 2 | def my_func(ab, y, z = None): ... | -- - | "); } @@ -987,13 +941,11 @@ my_func(my_other_func(ab=5, y=2), 0) | 6 | my_func(my_other_func(ab=5, y=2), 0) | ^^ Clicking here - | info: Found 1 definition --> main.py:3:19 | 3 | def my_other_func(ab, y): ... | -- - | "); } @@ -1021,13 +973,11 @@ my_func(my_other_func(ab=5, y=2), 0) | 5 | my_other_func(my_func(ab=5, y=2), 0) | ^^ Clicking here - | info: Found 1 definition --> main.py:2:13 | 2 | def my_func(ab, y): ... | -- - | "); } @@ -1055,13 +1005,11 @@ my_func(my_other_func(ab=5, y=2), 0) | 6 | my_func(my_other_func(ab=5, y=2), 0) | ^^ Clicking here - | info: Found 1 definition --> main.py:3:19 | 3 | def my_other_func(ab, y): ... | -- - | "); } @@ -1103,13 +1051,11 @@ def ab(a: str): ... | 4 | ab(1) | ^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def ab(a): | -- - | "); } @@ -1151,13 +1097,11 @@ def ab(a: str): ... | 4 | ab("hello") | ^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def ab(a): | -- - | "#); } @@ -1199,13 +1143,11 @@ def ab(a: int): ... | 4 | ab(1, 2) | ^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def ab(a, b = None): | -- - | "); } @@ -1247,13 +1189,11 @@ def ab(a: int): ... | 4 | ab(1) | ^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def ab(a, b = None): | -- - | "); } @@ -1298,13 +1238,11 @@ def ab(a: int, *, c: int): ... | 4 | ab(1, b=2) | ^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def ab(a, *, b = None, c = None): | -- - | "); } @@ -1349,13 +1287,11 @@ def ab(a: int, *, c: int): ... | 4 | ab(1, c=2) | ^^ Clicking here - | info: Found 1 definition --> mymodule.py:2:5 | 2 | def ab(a, *, b = None, c = None): | -- - | "); } @@ -1384,13 +1320,11 @@ a + b | 10 | a + b | ^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __add__(self, other): | ------- - | "); } @@ -1417,13 +1351,11 @@ B() + A() | 8 | B() + A() | ^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __radd__(self, other) -> A: | -------- - | "); } @@ -1452,13 +1384,11 @@ a+b | 10 | a+b | ^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __add__(self, other): | ------- - | "); } @@ -1487,13 +1417,11 @@ a+b | 10 | a+b | ^ Clicking here - | info: Found 1 definition --> main.py:8:1 | 8 | b = Test() | - - | "); } @@ -1541,13 +1469,11 @@ a = Test() | 7 | ~a | ^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __invert__(self) -> 'Test': ... | ---------- - | "); } @@ -1574,13 +1500,11 @@ a = Test() | 7 | ~a | ^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __invert__(self, extra_arg) -> 'Test': ... | ---------- - | "); } @@ -1606,13 +1530,11 @@ a = Test() | 7 | ~ a | ^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __invert__(self) -> 'Test': ... | ---------- - | "); } @@ -1638,13 +1560,11 @@ a = Test() | 7 | -a | ^ Clicking here - | info: Found 1 definition --> main.py:5:1 | 5 | a = Test() | - - | "); } @@ -1670,13 +1590,11 @@ a = Test() | 7 | not a | ^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __bool__(self) -> bool: ... | -------- - | "); } @@ -1702,13 +1620,11 @@ a = Test() | 7 | not a | ^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __len__(self) -> 42: ... | ------- - | "); } @@ -1738,13 +1654,11 @@ a = Test() | 8 | not a | ^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __bool__(self, extra_arg) -> bool: ... | -------- - | "); } @@ -1774,13 +1688,11 @@ a = Test() | 7 | not a | ^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __len__(self, extra_arg) -> 42: ... | ------- - | "); } @@ -1801,7 +1713,6 @@ a: float = 3.14 | LL | a: float = 3.14 | ^^^^^ Clicking here - | info: Found 2 definitions --> stdlib/builtins.pyi:LL:7 | @@ -1812,7 +1723,6 @@ a: float = 3.14 | LL | class float: | ----- - | "); } @@ -1833,7 +1743,6 @@ a: complex = 3.14 | LL | a: complex = 3.14 | ^^^^^^^ Clicking here - | info: Found 3 definitions --> stdlib/builtins.pyi:LL:7 | @@ -1849,7 +1758,6 @@ a: complex = 3.14 | LL | class complex: | ------- - | "); } @@ -1891,13 +1799,11 @@ x = MyClass() | 5 | x = MyClass() | ^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __init__(self, val): | -------- - | "); } @@ -1922,13 +1828,11 @@ x = MyClass() | 5 | x = MyClass() | ^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __init__(self, val): | -------- - | "); } @@ -1979,13 +1883,11 @@ x = MyClass(foo) | 7 | x = MyClass(foo) | ^^^ Clicking here - | info: Found 1 definition --> main.py:2:1 | 2 | foo = 1 | --- - | ", ); } @@ -2013,7 +1915,6 @@ x = MyClass() | 7 | x = MyClass() | ^^^^^^^ Clicking here - | info: Found 2 definitions --> main.py:3:9 | @@ -2022,7 +1923,6 @@ x = MyClass() 4 | self.val = val 5 | def __new__(self, val): | ------- - | "); } @@ -2046,13 +1946,11 @@ x = DynClass() | 4 | x = DynClass() | ^^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:2:1 | 2 | DynClass = type("DynClass", (), {}) | -------- - | "#); } @@ -2080,13 +1978,11 @@ x = DynClass() | LL | x = DynClass() | ^^^^^^^^ Clicking here - | info: Found 1 definition --> stdlib/builtins.pyi:LL:9 | LL | def __new__(cls) -> Self: ... | ------- - | "); } @@ -2128,13 +2024,11 @@ p = Point(1, 2) | 6 | p = Point(1, 2) | ^^^^^ Clicking here - | info: Found 1 definition --> main.py:4:1 | 4 | Point = namedtuple("Point", ["x", "y"]) | ----- - | "#); } @@ -2166,13 +2060,11 @@ p = Point(1, 2) | 6 | p = Point(1, 2) | ^^^^^ Clicking here - | info: Found 1 definition --> main.py:4:1 | 4 | Point = namedtuple("Point", ["x", "y"]) | ----- - | "#); } @@ -2200,7 +2092,6 @@ p = Point(1, 2) | 6 | print(a) | ^ Clicking here - | info: Found 3 definitions --> main.py:2:1 | @@ -2214,7 +2105,6 @@ p = Point(1, 2) 7 | 8 | a: bool = True | - - | "#); } @@ -2241,7 +2131,6 @@ p = Point(1, 2) | 8 | test.a | ^ Clicking here - | info: Found 2 definitions --> main.py:3:5 | @@ -2249,7 +2138,6 @@ p = Point(1, 2) | - 4 | a: str | - - | "); } @@ -2281,7 +2169,6 @@ p = Point(1, 2) | 13 | test.a | ^ Clicking here - | info: Found 2 definitions --> main.py:4:9 | @@ -2292,7 +2179,6 @@ p = Point(1, 2) | 8 | def a(self, value: str) -> None: | - - | "); } @@ -2316,13 +2202,11 @@ p = Point(1, 2) | LL | Foo.__dictoffset__ | ^^^^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> stdlib/builtins.pyi:LL:9 | LL | def __dictoffset__(self) -> int: ... | -------------- - | "); } @@ -2348,13 +2232,11 @@ p = Point(1, 2) | 6 | Bar.a | ^ Clicking here - | info: Found 1 definition --> main.py:3:5 | 3 | a: int | - - | "); } @@ -2404,13 +2286,11 @@ p = Point(1, 2) | LL | type.__dictoffset__ | ^^^^^^^^^^^^^^ Clicking here - | info: Found 1 definition --> stdlib/builtins.pyi:LL:9 | LL | def __dictoffset__(self) -> int: ... | -------------- - | "); } @@ -2435,13 +2315,11 @@ while True: | 5 | variable | ^^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:3:5 | 3 | variable = 1 | -------- - | "); } @@ -2468,13 +2346,11 @@ TD(f=1) | 8 | TD(f=1) | ^ Clicking here - | info: Found 1 definition --> main.py:5:5 | 5 | f: int | - - | "); } @@ -2502,13 +2378,11 @@ td.update(f=2) | 9 | td.update(f=2) | ^ Clicking here - | info: Found 1 definition --> main.py:5:5 | 5 | f: int | - - | "); } @@ -2537,13 +2411,11 @@ func(f=1) | 10 | func(f=1) | ^ Clicking here - | info: Found 1 definition --> main.py:5:5 | 5 | f: int | - - | "); } @@ -2570,13 +2442,11 @@ NT(f=1) | 8 | NT(f=1) | ^ Clicking here - | info: Found 1 definition --> main.py:5:5 | 5 | f: int | - - | "); } @@ -2604,13 +2474,11 @@ DC(f=1) | 9 | DC(f=1) | ^ Clicking here - | info: Found 1 definition --> main.py:6:5 | 6 | f: int | - - | "); } @@ -2640,13 +2508,11 @@ DC(f=1) | 11 | DC(f=1) | ^ Clicking here - | info: Found 1 definition --> main.py:9:24 | 9 | def __init__(self, f: int) -> None: ... | - - | "); } @@ -2677,13 +2543,11 @@ DC(g=1) | 12 | DC(g=1) | ^ Clicking here - | info: Found 1 definition --> main.py:10:5 | 10 | f: int = Field(alias='g') | - - | "); } @@ -2708,13 +2572,11 @@ for x in range(10): | 5 | variable | ^^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:3:5 | 3 | variable = 1 | -------- - | "); } @@ -2742,13 +2604,11 @@ class Bar(Foo): | 8 | super().__init__(x) | ^^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __init__(self, x: int) -> None: | -------- - | "); } @@ -2777,13 +2637,11 @@ class GenericFoo[T](Base): | 8 | super().__init__(x) | ^^^^^^^^ Clicking here - | info: Found 1 definition --> main.py:3:9 | 3 | def __init__(self, x: int) -> None: | -------- - | "); } diff --git a/crates/ty_ide/src/goto_implementation.rs b/crates/ty_ide/src/goto_implementation.rs index 5038ac6c6e..175286ae00 100644 --- a/crates/ty_ide/src/goto_implementation.rs +++ b/crates/ty_ide/src/goto_implementation.rs @@ -204,7 +204,6 @@ mod tests { | 12 | animal.speak() | ^^^^^ Clicking here - | info: Found 3 implementations --> main.py:3:9 | @@ -218,7 +217,6 @@ mod tests { 8 | class Cat(Animal): 9 | def speak(self): ... | ----- - | "); } @@ -251,7 +249,6 @@ mod tests { | 17 | animal.speak() | ^^^^^ Clicking here - | info: Found 3 implementations --> main.py:6:9 | @@ -266,7 +263,6 @@ mod tests { | 13 | def speak(self) -> str: | ----- - | "); } @@ -294,7 +290,6 @@ mod tests { | 12 | animal.speak() | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:3:9 | @@ -305,7 +300,6 @@ mod tests { | 9 | def speak(self): ... | ----- - | "); } @@ -330,13 +324,11 @@ mod tests { | 9 | dog.speak() | ^^^^^ Clicking here - | info: Found 1 implementation --> main.py:3:9 | 3 | def speak(self): ... | ----- - | "); } @@ -364,13 +356,11 @@ mod tests { | 12 | dog.speak() | ^^^^^ Clicking here - | info: Found 1 implementation --> main.py:6:9 | 6 | def speak(self): ... | ----- - | "); } @@ -395,13 +385,11 @@ mod tests { | 9 | dog.speak() | ^^^^^ Clicking here - | info: Found 1 implementation --> main.py:6:5 | 6 | speak = 1 | ----- - | "); } @@ -445,13 +433,11 @@ mod tests { | 13 | animal.speak(1) | ^^^^^ Clicking here - | info: Found 1 implementation --> main.py:9:9 | 9 | def speak(self, volume: int | str) -> int | str: | ----- - | "); } @@ -482,13 +468,11 @@ mod tests { | 15 | animal.speak(1) | ^^^^^ Clicking here - | info: Found 1 implementation --> main.py:11:9 | 11 | def speak(self, volume: int | str) -> int | str: | ----- - | "); } @@ -524,7 +508,6 @@ mod tests { | 7 | def value(self, value: int) -> None: ... | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:7:9 | @@ -535,7 +518,6 @@ mod tests { | 17 | def value(self, value: int) -> None: ... | ----- - | "); } @@ -574,7 +556,6 @@ mod tests { | 23 | return base.value | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:4:9 | @@ -585,7 +566,6 @@ mod tests { | 14 | def value(self) -> int: ... | ----- - | "); } @@ -624,7 +604,6 @@ mod tests { | 23 | base.value = value | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:7:9 | @@ -635,7 +614,6 @@ mod tests { | 17 | def value(self, value: int) -> None: ... | ----- - | "); } @@ -663,13 +641,11 @@ mod tests { | 12 | pet.speak() | ^^^^^ Clicking here - | info: Found 1 implementation --> main.py:3:9 | 3 | def speak(self): ... | ----- - | "); } @@ -694,7 +670,6 @@ mod tests { | 9 | animal.speak() | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:3:9 | @@ -704,7 +679,6 @@ mod tests { 5 | class Dog(Animal): 6 | def speak(self): ... | ----- - | "); } @@ -732,7 +706,6 @@ mod tests { | 8 | cls.speak() | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:4:9 | @@ -743,7 +716,6 @@ mod tests { | 12 | def speak(cls): ... | ----- - | "); } @@ -770,7 +742,6 @@ mod tests { | 11 | cls.speak() | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:4:9 | @@ -781,7 +752,6 @@ mod tests { 7 | @classmethod 8 | def speak(cls): ... | ----- - | "); } @@ -818,7 +788,6 @@ mod tests { | 3 | def method(self): ... | ^^^^^^ Clicking here - | info: Found 2 implementations --> base.py:3:9 | @@ -829,7 +798,6 @@ mod tests { | 5 | def method(self): ... | ------ - | "); } @@ -912,13 +880,11 @@ class MyClass: | 4 | x.action() | ^^^^^^ Clicking here - | info: Found 1 implementation --> mymodule.py:5:9 | 5 | def action(self): | ------ - | "); } @@ -964,13 +930,11 @@ class MyClass: | 4 | x.action(1) | ^^^^^^ Clicking here - | info: Found 1 implementation --> mymodule.py:5:9 | 5 | def action(self, value): | ------ - | "); } @@ -1021,7 +985,6 @@ class MyClass: | 3 | def speak(self): ... | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:3:9 | @@ -1031,7 +994,6 @@ class MyClass: 5 | class Dog(Animal): 6 | def speak(self): ... | ----- - | "); } @@ -1071,7 +1033,6 @@ class MyClass: | 4 | class Animal(ABC): | ^^^^^^ Clicking here - | info: Found 3 implementations --> main.py:4:7 | @@ -1085,7 +1046,6 @@ class MyClass: 9 | 10 | class Cat(Animal): | --- - | "); } @@ -1115,7 +1075,6 @@ class MyClass: | 2 | class Animal: | ^^^^^^ Clicking here - | info: Found 2 implementations --> main.py:2:7 | @@ -1125,7 +1084,6 @@ class MyClass: 4 | 5 | class Dog(Animal): | --- - | "); } @@ -1144,13 +1102,11 @@ class MyClass: | 2 | class Widget: | ^^^^^^ Clicking here - | info: Found 1 implementation --> main.py:2:7 | 2 | class Widget: | ------ - | "); } @@ -1175,7 +1131,6 @@ class MyClass: | 5 | class Mammal(Animal): | ^^^^^^ Clicking here - | info: Found 2 implementations --> main.py:5:7 | @@ -1185,7 +1140,6 @@ class MyClass: 7 | 8 | class Dog(Mammal): | --- - | "); } @@ -1213,7 +1167,6 @@ class MyClass: | 2 | class Base: | ^^^^ Clicking here - | info: Found 4 implementations --> main.py:2:7 | @@ -1231,7 +1184,6 @@ class MyClass: 10 | 11 | class Diamond(Left, Right): | ------- - | "); } @@ -1253,7 +1205,6 @@ class MyClass: | 2 | class Container[T]: | ^^^^^^^^^ Clicking here - | info: Found 2 implementations --> main.py:2:7 | @@ -1263,7 +1214,6 @@ class MyClass: 4 | 5 | class IntContainer(Container[int]): | ------------ - | "); } @@ -1288,7 +1238,6 @@ class MyClass: | 8 | def f(x: Animal): | ^^^^^^ Clicking here - | info: Found 2 implementations --> main.py:2:7 | @@ -1298,7 +1247,6 @@ class MyClass: 4 | 5 | class Dog(Animal): | --- - | "); } @@ -1323,7 +1271,6 @@ class MyClass: | 8 | def f(x: "Animal"): | ^^^^^^ Clicking here - | info: Found 2 implementations --> main.py:2:7 | @@ -1333,7 +1280,6 @@ class MyClass: 4 | 5 | class Dog(Animal): | --- - | "#); } @@ -1364,7 +1310,6 @@ class MyClass: | 4 | class Dog(animals.Animal): | ^^^^^^ Clicking here - | info: Found 2 implementations --> animals.py:2:7 | @@ -1375,7 +1320,6 @@ class MyClass: | 4 | class Dog(animals.Animal): | --- - | "); } @@ -1408,7 +1352,6 @@ class MyClass: | 7 | animals.Animal() | ^^^^^^ Clicking here - | info: Found 2 implementations --> animals.py:2:7 | @@ -1419,7 +1362,6 @@ class MyClass: | 4 | class Dog(animals.Animal): | --- - | "); } @@ -1446,7 +1388,6 @@ class MyClass: | 11 | Base() | ^^^^ Clicking here - | info: Found 2 implementations --> main.py:5:7 | @@ -1456,7 +1397,6 @@ class MyClass: 7 | 8 | class Child(Base): | ----- - | "); } @@ -1482,7 +1422,6 @@ class MyClass: | 9 | def f(x: Outer.Inner): | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:3:11 | @@ -1492,7 +1431,6 @@ class MyClass: 5 | 6 | class SubInner(Outer.Inner): | -------- - | "); } @@ -1519,13 +1457,11 @@ class MyClass: | 9 | factory.dog_cls | ^^^^^^^ Clicking here - | info: Found 1 implementation --> main.py:2:7 | 2 | class Dog: | --- - | "); } @@ -1559,7 +1495,6 @@ class MyClass: | 18 | factory.item | ^^^^ Clicking here - | info: Found 3 implementations --> main.py:11:5 | @@ -1571,7 +1506,6 @@ class MyClass: 14 | else: 15 | item = 0 | ---- - | "); } @@ -1659,7 +1593,6 @@ class MyClass: | 2 | class Base: | ^^^^ Clicking here - | info: Found 2 implementations --> main.py:2:7 | @@ -1670,7 +1603,6 @@ class MyClass: | 4 | class Derived(Base): | ------- - | "); } @@ -1698,7 +1630,6 @@ class MyClass: | 12 | animal.sound | ^^^^^ Clicking here - | info: Found 3 implementations --> main.py:3:5 | @@ -1712,7 +1643,6 @@ class MyClass: 8 | class Cat(Animal): 9 | sound: str = "meow" | ----- - | "#); } @@ -1737,7 +1667,6 @@ class MyClass: | 9 | animal.sound | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:3:5 | @@ -1747,7 +1676,6 @@ class MyClass: 5 | class Dog(Animal): 6 | sound = "woof" | ----- - | "#); } @@ -1772,7 +1700,6 @@ class MyClass: | 9 | animal.sound | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:3:5 | @@ -1782,7 +1709,6 @@ class MyClass: 5 | class Dog(Animal): 6 | sound: str = "woof" | ----- - | "#); } @@ -1807,7 +1733,6 @@ class MyClass: | 9 | animal.speak | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:3:9 | @@ -1817,7 +1742,6 @@ class MyClass: 5 | class Dog(Animal): 6 | speak = 1 | ----- - | "); } @@ -1844,7 +1768,6 @@ class MyClass: | 11 | animal.sound | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:4:9 | @@ -1855,7 +1778,6 @@ class MyClass: 7 | def __init__(self): 8 | self.sound = "woof" | ---------- - | "#); } @@ -1885,13 +1807,11 @@ class MyClass: | 14 | dog.sound | ^^^^^ Clicking here - | info: Found 1 implementation --> main.py:4:9 | 4 | self.sound = "generic" | ---------- - | "#); } @@ -1917,7 +1837,6 @@ class MyClass: | 10 | animal.sound | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:3:5 | @@ -1928,7 +1847,6 @@ class MyClass: 6 | def __init__(self): 7 | self.sound = "woof" | ---------- - | "#); } @@ -1954,13 +1872,11 @@ class MyClass: | 8 | animal.sound | ^^^^^ Clicking here - | info: Found 1 implementation --> main.py:3:5 | 3 | sound: str = "generic" | ----- - | "#); } @@ -1997,13 +1913,11 @@ class MyClass: | 4 | x.sound | ^^^^^ Clicking here - | info: Found 1 implementation --> mymodule.py:3:5 | 3 | sound: str = "generic" | ----- - | "#); } @@ -2037,7 +1951,6 @@ class MyClass: | 14 | speaker.speak() | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:5:9 | @@ -2048,7 +1961,6 @@ class MyClass: | 11 | def speak(self) -> None: ... | ----- - | "); } @@ -2082,7 +1994,6 @@ class MyClass: | 16 | animal.speak() | ^^^^^ Clicking here - | info: Found 2 implementations --> main.py:5:9 | @@ -2093,7 +2004,6 @@ class MyClass: 8 | class Dog(Animal): 9 | def speak(self): ... | ----- - | "); } @@ -2121,13 +2031,11 @@ class MyClass: | 12 | animal.speak() | ^^^^^ Clicking here - | info: Found 1 implementation --> main.py:5:9 | 5 | def speak(self): ... | ----- - | "); } @@ -2156,13 +2064,11 @@ class MyClass: | 13 | animal.sound | ^^^^^ Clicking here - | info: Found 1 implementation --> main.py:5:5 | 5 | sound: str = "generic" | ----- - | "#); } diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index 9e4512b1b0..080b43f01e 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -48,13 +48,11 @@ mod tests { | 4 | ab = Test() | ^^ Clicking here - | info: Found 1 type definition --> main.py:2:7 | 2 | class Test: ... | ---- - | "); } @@ -74,13 +72,11 @@ mod tests { | LL | ab = Literal | ^^ Clicking here - | info: Found 1 type definition --> stdlib/typing.pyi:LL:1 | LL | Literal: _SpecialForm | ------- - | "); } @@ -102,13 +98,11 @@ mod tests { | LL | ab = Any | ^^ Clicking here - | info: Found 1 type definition --> stdlib/typing.pyi:LL:7 | LL | class Any: | --- - | "); } @@ -129,13 +123,11 @@ mod tests { | LL | ab = Generic | ^^ Clicking here - | info: Found 1 type definition --> stdlib/typing.pyi:LL:1 | LL | Generic: type[_Generic] | ------- - | "); } @@ -155,13 +147,11 @@ mod tests { | LL | ab = AlwaysTruthy | ^^ Clicking here - | info: Found 1 type definition --> stdlib/ty_extensions/__init__.pyi:LL:1 | LL | AlwaysTruthy: _SpecialForm | ------------ - | "); } @@ -183,13 +173,11 @@ mod tests { | LL | D().x | ^ Clicking here - | info: Found 1 type definition --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Divergent: _SpecialForm | --------- - | "); } @@ -211,13 +199,11 @@ mod tests { | 6 | ab | ^^ Clicking here - | info: Found 1 type definition --> main.py:2:5 | 2 | def foo(a, b): ... | --- - | "); } @@ -245,7 +231,6 @@ mod tests { | 12 | a | ^ Clicking here - | info: Found 2 type definitions --> main.py:3:5 | @@ -254,7 +239,6 @@ mod tests { 4 | 5 | def bar(a, b): ... | --- - | "); } @@ -282,13 +266,11 @@ mod tests { | 12 | color | ^^^^^ Clicking here - | info: Found 1 type definition --> main.py:6:5 | 6 | BLUE = 2 | ---- - | "#); } @@ -317,7 +299,6 @@ mod tests { | 13 | color | ^^^^^ Clicking here - | info: Found 2 type definitions --> main.py:6:5 | @@ -325,7 +306,6 @@ mod tests { | ----- 7 | BLUE = 3 | ---- - | "#); } @@ -345,13 +325,11 @@ mod tests { | 2 | import lib | ^^^ Clicking here - | info: Found 1 type definition --> lib.py:1:1 | 1 | a = 10 | ------ - | "); } @@ -372,13 +350,11 @@ mod tests { | 2 | import lib.submod | ^^^ Clicking here - | info: Found 1 type definition --> lib/__init__.py:1:1 | 1 | b = 7 | ----- - | "); } @@ -399,13 +375,11 @@ mod tests { | 2 | import lib.submod | ^^^^^^ Clicking here - | info: Found 1 type definition --> lib/submod.py:1:1 | 1 | a = 10 | ------ - | "); } @@ -425,13 +399,11 @@ mod tests { | 2 | from lib import a | ^^^ Clicking here - | info: Found 1 type definition --> lib.py:1:1 | 1 | a = 10 | ------ - | "); } @@ -452,13 +424,11 @@ mod tests { | 2 | from lib.submod import a | ^^^ Clicking here - | info: Found 1 type definition --> lib/__init__.py:1:1 | 1 | b = 7 | ----- - | "); } @@ -479,13 +449,11 @@ mod tests { | 2 | from lib.submod import a | ^^^^^^ Clicking here - | info: Found 1 type definition --> lib/submod.py:1:1 | 1 | a = 10 | ------ - | "); } @@ -515,13 +483,11 @@ mod tests { | 2 | from .bot.botmod import * | ^^^^^^ Clicking here - | info: Found 1 type definition --> lib/sub/bot/botmod.py:1:1 | 1 | botmod = 31 | ----------- - | "); } @@ -551,13 +517,11 @@ mod tests { | 2 | from .bot.botmod import * | ^^^ Clicking here - | info: Found 1 type definition --> lib/sub/bot/__init__.py:1:1 | 1 | bot = 3 | ------- - | "); } @@ -587,13 +551,11 @@ mod tests { | 2 | from .bot.botmod import * | ^^^ Clicking here - | info: Found 1 type definition --> lib/sub/bot/__init__.py:1:1 | 1 | bot = 3 | ------- - | "); } @@ -638,13 +600,11 @@ mod tests { | 4 | lib | ^^^ Clicking here - | info: Found 1 type definition --> lib.py:1:1 | 1 | a = 10 | ------ - | "); } @@ -664,13 +624,11 @@ mod tests { | LL | a | ^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } #[test] @@ -687,13 +645,11 @@ mod tests { | LL | a: str = "test" | ^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | --- - | "#); } @@ -711,13 +667,11 @@ mod tests { | 2 | type Alias[T: int = bool] = list[T] | ^ Clicking here - | info: Found 1 type definition --> main.py:2:12 | 2 | type Alias[T: int = bool] = list[T] | - - | "); } @@ -735,13 +689,11 @@ mod tests { | 2 | type Alias[**P = [int, str]] = Callable[P, int] | ^ Clicking here - | info: Found 1 type definition --> main.py:2:14 | 2 | type Alias[**P = [int, str]] = Callable[P, int] | - - | "); } @@ -759,13 +711,11 @@ mod tests { | 2 | type Alias[*Ts = ()] = tuple[*Ts] | ^^ Clicking here - | info: Found 1 type definition --> main.py:2:13 | 2 | type Alias[*Ts = ()] = tuple[*Ts] | -- - | "); } @@ -787,13 +737,11 @@ mod tests { | 6 | Alias | ^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:1 | 4 | Alias = TypeAliasType("Alias", tuple[int, int]) | ----- - | "#); } @@ -814,13 +762,11 @@ mod tests { | 2 | a: "MyClass" = 1 | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -841,13 +787,11 @@ mod tests { | 2 | a: "None | MyClass" = 1 | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -868,7 +812,6 @@ mod tests { | LL | a: "None | MyClass" = 1 | ^^^^^^^^^^^^^^^^ Clicking here - | info: Found 2 type definitions --> main.py:LL:7 | @@ -879,7 +822,6 @@ mod tests { | LL | class NoneType: | -------- - | "#); } @@ -900,13 +842,11 @@ mod tests { | 2 | a: "None | MyClass" = 1 | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -927,7 +867,6 @@ mod tests { | LL | a: "None | MyClass" = 1 | ^^^^^^^^^^^^^^^^ Clicking here - | info: Found 2 type definitions --> main.py:LL:7 | @@ -938,7 +877,6 @@ mod tests { | LL | class NoneType: | -------- - | "#); } @@ -959,13 +897,11 @@ mod tests { | LL | a: "MyClass |" = 1 | ^^^^^^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/ty_extensions/__init__.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- - | "#); } @@ -986,13 +922,11 @@ mod tests { | 2 | a: "MyClass | No" = 1 | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1013,13 +947,11 @@ mod tests { | LL | a: "MyClass | No" = 1 | ^^ Clicking here - | info: Found 1 type definition --> stdlib/ty_extensions/__init__.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- - | "#); } @@ -1037,13 +969,11 @@ mod tests { | LL | ab: "ab" | ^^ Clicking here - | info: Found 1 type definition --> stdlib/ty_extensions/__init__.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- - | "#); } @@ -1061,13 +991,11 @@ mod tests { | LL | x: "foobar" | ^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/ty_extensions/__init__.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- - | "#); } @@ -1088,13 +1016,11 @@ mod tests { | 2 | x: "list['MyClass | int'] | None" | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1115,13 +1041,11 @@ mod tests { | 2 | x: "list['int | MyClass'] | None" | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1142,13 +1066,11 @@ mod tests { | 2 | x: "list['int | None'] | MyClass" | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1169,13 +1091,11 @@ mod tests { | 2 | x: "list['int' | 'MyClass'] | None" | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1196,13 +1116,11 @@ mod tests { | 2 | x: "list['MyClass' | 'str'] | None" | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1223,13 +1141,11 @@ mod tests { | LL | x: """'list["MyClass" | "str"]' | None""" | ^^^^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/ty_extensions/__init__.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- - | "#); } @@ -1250,13 +1166,11 @@ mod tests { | 2 | x: """'list["int" | "str"]' | MyClass""" | ^^^^^^^ Clicking here - | info: Found 1 type definition --> main.py:4:7 | 4 | class MyClass: | ------- - | "#); } @@ -1291,13 +1205,11 @@ mod tests { | LL | x = ab | ^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | --- - | "#); } @@ -1332,13 +1244,11 @@ mod tests { | LL | x = ab | ^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ---- - | "#); } @@ -1373,13 +1283,11 @@ mod tests { | LL | x = ab | ^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | --- - | "#); } @@ -1426,13 +1334,11 @@ mod tests { | LL | x = ab | ^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } @@ -1459,13 +1365,11 @@ mod tests { | 10 | case Click(x, button=ab): | ^^^^^ Clicking here - | info: Found 1 type definition --> main.py:2:7 | 2 | class Click: | ----- - | "); } @@ -1503,13 +1407,11 @@ mod tests { | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | ^^ Clicking here - | info: Found 1 type definition --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | -- - | "); } @@ -1527,13 +1429,11 @@ mod tests { | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | ^^ Clicking here - | info: Found 1 type definition --> main.py:2:13 | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | -- - | "); } @@ -1552,13 +1452,11 @@ mod tests { | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | ^^ Clicking here - | info: Found 1 type definition --> main.py:3:15 | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | -- - | "); } @@ -1599,13 +1497,11 @@ mod tests { | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | ^^ Clicking here - | info: Found 1 type definition --> main.py:2:14 | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | -- - | "); } @@ -1625,13 +1521,11 @@ mod tests { | LL | test(a= "123") | ^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | --- - | "#); } @@ -1654,13 +1548,11 @@ mod tests { | LL | test(a= 123) | ^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class int: | --- - | "); } @@ -1682,13 +1574,11 @@ f(**kwargs) | LL | f(**kwargs) | ^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class dict(MutableMapping[_KT, _VT]): | ---- - | "); } @@ -1715,13 +1605,11 @@ def outer(): | LL | return x # Should find the nonlocal x declaration in outer scope | ^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } @@ -1765,13 +1653,11 @@ def function(): | LL | return global_var # Should find the global variable declaration | ^^^^^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } @@ -1807,13 +1693,11 @@ def function(): | LL | a | ^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } @@ -1836,13 +1720,11 @@ def function(): | 7 | x.foo() | ^ Clicking here - | info: Found 1 type definition --> main.py:2:7 | 2 | class X: | - - | "); } @@ -1862,13 +1744,11 @@ def function(): | 4 | foo() | ^^^ Clicking here - | info: Found 1 type definition --> main.py:2:5 | 2 | def foo(a, b): ... | --- - | "); } @@ -1888,13 +1768,11 @@ def function(): | LL | print(a) | ^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | --- - | "); } @@ -1913,7 +1791,6 @@ def function(): | LL | a | ^ Clicking here - | info: Found 2 type definitions --> stdlib/builtins.pyi:LL:7 | @@ -1924,7 +1801,6 @@ def function(): | LL | class NoneType: | -------- - | "); } @@ -1955,11 +1831,11 @@ def function(): | 4 | x = subpkg | ^^^^^^ Clicking here - | info: Found 1 type definition - --> mypackage/subpkg/__init__.py:1:1 - | - | + --> mypackage/subpkg/__init__.py:1:1 + | + 1 | + | - "); } @@ -1990,11 +1866,11 @@ def function(): | 2 | from .subpkg.submod import val | ^^^^^^ Clicking here - | info: Found 1 type definition - --> mypackage/subpkg/__init__.py:1:1 - | - | + --> mypackage/subpkg/__init__.py:1:1 + | + 1 | + | - "); } @@ -2025,13 +1901,11 @@ def function(): | LL | x = submod | ^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/ty_extensions/__init__.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- - | "); } @@ -2062,14 +1936,12 @@ def function(): | 2 | from .subpkg.submod import val | ^^^^^^ Clicking here - | info: Found 1 type definition --> mypackage/subpkg/submod.py:1:1 | 1 | / 2 | | val: int = 0 | |_____________- - | "); } @@ -2099,14 +1971,12 @@ def function(): | 2 | from .subpkg import subpkg | ^^^^^^ Clicking here - | info: Found 1 type definition --> mypackage/subpkg/__init__.py:1:1 | 1 | / 2 | | subpkg: int = 10 | |_________________- - | "); } @@ -2136,13 +2006,11 @@ def function(): | LL | from .subpkg import subpkg | ^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class int: | --- - | "); } @@ -2172,13 +2040,11 @@ def function(): | LL | x = subpkg | ^^^^^^ Clicking here - | info: Found 1 type definition --> stdlib/builtins.pyi:LL:7 | LL | class int: | --- - | "); } diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index a0086cc2af..689abf81a7 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -453,7 +453,6 @@ mod tests { | ^- Cursor offset | | | source - | "); } @@ -511,7 +510,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -567,7 +565,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -617,7 +614,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -679,7 +675,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -739,7 +734,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -793,7 +787,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -847,7 +840,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -908,7 +900,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -946,7 +937,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -994,7 +984,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1024,7 +1013,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1062,7 +1050,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1110,7 +1097,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1156,7 +1142,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1202,11 +1187,10 @@ mod tests { --> main.py:12:5 | 12 | x = S(1) - | - + | ^ | | | source | Cursor offset - | "); } @@ -1244,11 +1228,10 @@ mod tests { --> main.py:12:5 | 12 | x = S(1) - | - + | ^ | | | source | Cursor offset - | "); } @@ -1281,7 +1264,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1314,7 +1296,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1355,7 +1336,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1401,7 +1381,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1446,7 +1425,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1487,7 +1465,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1528,7 +1505,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1600,7 +1576,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -1644,7 +1619,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -1685,7 +1659,6 @@ mod tests { | | | | | Cursor offset | source - | "#); let literal_string = hover_test( @@ -1725,7 +1698,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -1771,7 +1743,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -1811,7 +1782,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -1860,7 +1830,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -1917,7 +1886,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -1970,7 +1938,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2022,7 +1989,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2075,7 +2041,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2110,11 +2075,10 @@ mod tests { --> main.py:14:5 | 14 | foo.a - | - + | ^ | | | source | Cursor offset - | "); } @@ -2148,7 +2112,6 @@ mod tests { | ^^^- Cursor offset | | | source - | "); } @@ -2176,7 +2139,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -2216,7 +2178,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2258,7 +2219,6 @@ mod tests { | ^^^^^- Cursor offset | | | source - | "); } @@ -2288,7 +2248,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2328,7 +2287,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2368,7 +2326,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -2418,7 +2375,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -2465,7 +2421,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -2502,7 +2457,6 @@ mod tests { | | | | | Cursor offset | source - | "); } @@ -2535,7 +2489,6 @@ mod tests { | || | |Cursor offset | source - | "); } @@ -2575,7 +2528,6 @@ mod tests { | ^- Cursor offset | | | source - | "); } @@ -2610,7 +2562,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -2645,7 +2596,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -2693,7 +2643,6 @@ mod tests { | ^^^^^^^- Cursor offset | | | source - | "#); } @@ -2756,7 +2705,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -2786,7 +2734,6 @@ mod tests { | || | |Cursor offset | source - | "#); } @@ -2813,7 +2760,6 @@ mod tests { | || | |Cursor offset | source - | "#); } @@ -2840,7 +2786,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -2875,7 +2820,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -2910,7 +2854,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -2945,7 +2888,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -2980,7 +2922,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3015,7 +2956,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3045,7 +2985,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3080,7 +3019,6 @@ mod tests { | | | | | Cursor offset | source - | "#); } @@ -3138,7 +3076,6 @@ def ab(a: str): ... | || | |Cursor offset | source - | "); } @@ -3184,7 +3121,6 @@ def bar() -> None: | | | | | Cursor offset | source - | "); } @@ -3242,7 +3178,6 @@ def ab(a: str): | || | |Cursor offset | source - | "#); } @@ -3306,7 +3241,6 @@ def ab(a: int): | || | |Cursor offset | source - | "); } @@ -3364,7 +3298,6 @@ def ab(a: int): | || | |Cursor offset | source - | "); } @@ -3434,7 +3367,6 @@ def ab(a: int, *, c: int): | || | |Cursor offset | source - | "); } @@ -3504,7 +3436,6 @@ def ab(a: int, *, c: int): | || | |Cursor offset | source - | "); } @@ -3566,7 +3497,6 @@ def ab(a: int, *, c: int): | ^^^- Cursor offset | | | source - | "); } @@ -3616,7 +3546,6 @@ def ab(a: int, *, c: int): | ^^^- Cursor offset | | | source - | "); } @@ -3667,7 +3596,6 @@ def ab(a: int, *, c: int): | | | | | Cursor offset | source - | "); } @@ -3702,7 +3630,6 @@ def outer(): | ^- Cursor offset | | | source - | "#); } @@ -3755,7 +3682,6 @@ def function(): | | | | | Cursor offset | source - | "#); } @@ -3816,7 +3742,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -3860,7 +3785,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -3904,7 +3828,6 @@ def function(): | || | |Cursor offset | source - | "#); } @@ -3960,7 +3883,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -3996,7 +3918,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -4043,7 +3964,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -4070,7 +3990,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -4098,7 +4017,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -4128,7 +4046,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -4166,7 +4083,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -4217,7 +4133,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -4274,7 +4189,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4301,7 +4215,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4327,7 +4240,6 @@ def function(): | ^^- Cursor offset | | | source - | "); } @@ -4366,7 +4278,6 @@ def function(): | ^^^^^- Cursor offset | | | source - | "); } @@ -4414,7 +4325,6 @@ def function(): | ^^^^^- Cursor offset | | | source - | "); } @@ -4460,7 +4370,6 @@ def function(): | ^^^^- Cursor offset | | | source - | "); } @@ -4508,7 +4417,6 @@ def function(): | ^^^^- Cursor offset | | | source - | "); } @@ -4548,7 +4456,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4588,7 +4495,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4631,7 +4537,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4672,7 +4577,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4716,7 +4620,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4746,7 +4649,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4774,7 +4676,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4803,7 +4704,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4835,7 +4735,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4867,7 +4766,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4901,7 +4799,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -4968,7 +4865,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -5137,7 +5033,6 @@ def function(): | | | | | Cursor offset | source - | "#); } @@ -5175,7 +5070,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5205,7 +5099,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5235,7 +5128,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5281,7 +5173,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5319,7 +5210,6 @@ def function(): | | | | | Cursor offset | source - | "#); } @@ -5348,7 +5238,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5383,7 +5272,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5418,7 +5306,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5454,7 +5341,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5491,7 +5377,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5529,7 +5414,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5558,7 +5442,6 @@ def function(): | || | |Cursor offset | source - | "); } @@ -5586,7 +5469,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -5611,7 +5493,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -5636,7 +5517,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -5661,7 +5541,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -5688,7 +5567,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -5712,7 +5590,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -5736,7 +5613,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -5760,7 +5636,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -5786,7 +5661,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -5809,7 +5683,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -5832,7 +5705,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -5855,7 +5727,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -5881,7 +5752,6 @@ def function(): | ^^^- Cursor offset | | | source - | "); } @@ -5918,7 +5788,6 @@ def function(): | ^^^^^^^- Cursor offset | | | source - | "); let test = hover_test( @@ -5943,7 +5812,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -5974,7 +5842,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6002,7 +5869,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6030,7 +5896,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6058,7 +5923,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -6086,11 +5950,10 @@ def function(): --> main.py:2:12 | 2 | result = 5 + 3 - | - + | ^ | | | source | Cursor offset - | "); } @@ -6130,11 +5993,10 @@ def function(): --> main.py:15:8 | 15 | Test() + Test() - | - + | ^ | | | source | Cursor offset - | "); } @@ -6171,7 +6033,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -6202,7 +6063,6 @@ def function(): | ^^^^^- Cursor offset | | | source - | "); } @@ -6266,7 +6126,6 @@ def function(): | ^^^^^- Cursor offset | | | source - | "); let test = hover_test( @@ -6291,7 +6150,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -6317,7 +6175,6 @@ def function(): | ^^^- Cursor offset | | | source - | "); let test = hover_test( @@ -6340,7 +6197,6 @@ def function(): | ^^^- Cursor offset | | | source - | "); } @@ -6370,7 +6226,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6394,7 +6249,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6421,7 +6275,6 @@ def function(): | ^- Cursor offset | | | source - | "); let test = hover_test( @@ -6445,7 +6298,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -6485,7 +6337,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -6525,7 +6376,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -6565,7 +6415,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -6605,7 +6454,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -6644,7 +6492,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -6683,7 +6530,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -6722,7 +6568,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -6766,7 +6611,6 @@ def function(): | ^- Cursor offset | | | source - | "); } @@ -6794,7 +6638,6 @@ def function(): | | | | | Cursor offset | source - | "); } @@ -6848,7 +6691,6 @@ class CoolType(str): | ^- Cursor offset | | | source - | "); } @@ -6884,7 +6726,6 @@ type U = MyType | ^- Cursor offset | | | source - | "); } @@ -6934,7 +6775,6 @@ type U = MyType | | | | | Cursor offset | source - | "); } @@ -6983,7 +6823,6 @@ type U = MyType | ^^^^^- Cursor offset | | | source - | "); } @@ -7030,7 +6869,6 @@ type U = MyType | ^^^^- Cursor offset | | | source - | "); } diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 4fcc167e2c..660bf3aa88 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -1023,78 +1023,66 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: Literal[1]] = x | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:13 | LL | y[: Literal[1]] = x | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:5 | LL | z[: int] = i(1) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:5 | LL | w[: int] = z | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | bb[: Literal[b"foo"]] = aa | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class bytes(Sequence[int]): | ^^^^^ - | info: Source --> main2.py:LL:14 | LL | bb[: Literal[b"foo"]] = aa | ^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -1151,104 +1139,88 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = (x1, y1) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:14 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = (x1, y1) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:24 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = (x1, y1) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:32 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = (x1, y1) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | x3[: int], y3[: str] = (i(1), s('abc')) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:17 | LL | x3[: int], y3[: str] = (i(1), s('abc')) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | x4[: int], y4[: str] = (x3, y3) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:17 | LL | x4[: int], y4[: str] = (x3, y3) | ^^^ - | "#); } @@ -1272,39 +1244,33 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | (a[: int], *b[: list[int]]) = x | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:21 | LL | (a[: int], *b[: list[int]]) = x | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:26 | LL | (a[: int], *b[: list[int]]) = x | ^^^ - | "); } @@ -1362,26 +1328,22 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:5 | LL | x[: int], _ignored = (i(1), s('abc')) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:16 | LL | __ignored, y[: str] = (i(1), s('abc')) | ^^^ - | "); } @@ -1409,13 +1371,11 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:15 | LL | __special__[: int] = i(1) | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -1462,104 +1422,88 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = x1, y1 | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:14 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = x1, y1 | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:24 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = x1, y1 | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:32 | LL | x2[: Literal[1]], y2[: Literal["abc"]] = x1, y1 | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | x3[: int], y3[: str] = i(1), s('abc') | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:17 | LL | x3[: int], y3[: str] = i(1), s('abc') | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | x4[: int], y4[: str] = x3, y3 | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:17 | LL | x4[: int], y4[: str] = x3, y3 | ^^^ - | "#); } @@ -1597,143 +1541,121 @@ Source with applied edits: | LL | class tuple(Sequence[_T_co]): | ^^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: tuple[Literal[1], Literal["abc"]]] = x | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:11 | LL | y[: tuple[Literal[1], Literal["abc"]]] = x | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:19 | LL | y[: tuple[Literal[1], Literal["abc"]]] = x | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:23 | LL | y[: tuple[Literal[1], Literal["abc"]]] = x | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:31 | LL | y[: tuple[Literal[1], Literal["abc"]]] = x | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class tuple(Sequence[_T_co]): | ^^^^^ - | info: Source --> main2.py:LL:5 | LL | z[: tuple[int, str]] = (i(1), s('abc')) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:11 | LL | z[: tuple[int, str]] = (i(1), s('abc')) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:16 | LL | z[: tuple[int, str]] = (i(1), s('abc')) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class tuple(Sequence[_T_co]): | ^^^^^ - | info: Source --> main2.py:LL:5 | LL | w[: tuple[int, str]] = z | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:11 | LL | w[: tuple[int, str]] = z | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:16 | LL | w[: tuple[int, str]] = z | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -1785,156 +1707,132 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1)) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:14 | LL | x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1)) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:25 | LL | x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1)) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:33 | LL | x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1)) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:47 | LL | x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1)) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:55 | LL | x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1)) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | x3[: int], (y3[: str], z3[: int]) = (i(1), (s('abc'), i(2))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:18 | LL | x3[: int], (y3[: str], z3[: int]) = (i(1), (s('abc'), i(2))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:29 | LL | x3[: int], (y3[: str], z3[: int]) = (i(1), (s('abc'), i(2))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:6 | LL | x4[: int], (y4[: str], z4[: int]) = (x3, (y3, z3)) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:18 | LL | x4[: int], (y4[: str], z4[: int]) = (x3, (y3, z3)) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:29 | LL | x4[: int], (y4[: str], z4[: int]) = (x3, (y3, z3)) | ^^^ - | "#); } @@ -1966,39 +1864,33 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: Literal[1]] = x | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:13 | LL | y[: Literal[1]] = x | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:5 | LL | w[: int] = z | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2039,13 +1931,11 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:5 | LL | x[: int] = i(1) | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2089,26 +1979,22 @@ Source with applied edits: | LL | Unknown: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:18 | LL | self.y[: Unknown] = y | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, y): | ^ - | info: Source --> main2.py:7:8 | 7 | a = A([y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2153,13 +2039,11 @@ Source with applied edits: | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:17 | LL | x[: str] = ab | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2196,26 +2080,22 @@ Source with applied edits: | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:17 | LL | x[: list[str]] = ab | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:22 | LL | x[: list[str]] = ab | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2252,39 +2132,33 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:17 | LL | x[: Literal["a", "b"]] = ab | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:25 | LL | x[: Literal["a", "b"]] = ab | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:30 | LL | x[: Literal["a", "b"]] = ab | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2337,13 +2211,11 @@ Source with applied edits: | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:17 | LL | x[: str] = ab | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2574,338 +2446,286 @@ Source with applied edits: | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | a[: list[int]] = [1, 2] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | a[: list[int]] = [1, 2] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | b[: list[int | float]] = [1.0, 2.0] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | b[: list[int | float]] = [1.0, 2.0] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class float: | ^^^^^ - | info: Source --> main2.py:LL:16 | LL | b[: list[int | float]] = [1.0, 2.0] | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | c[: list[bool]] = [True, False] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class bool(int): | ^^^^ - | info: Source --> main2.py:LL:10 | LL | c[: list[bool]] = [True, False] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | d[: list[None | Unknown]] = [None, None] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/types.pyi:LL:7 | LL | class NoneType: | ^^^^^^^^ - | info: Source --> main2.py:LL:10 | LL | d[: list[None | Unknown]] = [None, None] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/ty_extensions/__init__.pyi:LL:1 | LL | Unknown: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:17 | LL | d[: list[None | Unknown]] = [None, None] | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | e[: list[str]] = ["hel", "lo"] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:10 | LL | e[: list[str]] = ["hel", "lo"] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | f[: list[str]] = ['the', 're'] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:10 | LL | f[: list[str]] = ['the', 're'] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | g[: list[str]] = [f"{ft}", f"{ft}"] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:10 | LL | g[: list[str]] = [f"{ft}", f"{ft}"] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | h[: list[Template]] = [t"wow %d", t"wow %d"] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/string/templatelib.pyi:LL:7 | LL | class Template: # TODO: consider making `Template` generic on `TypeVarTuple` | ^^^^^^^^ - | info: Source --> main2.py:LL:10 | LL | h[: list[Template]] = [t"wow %d", t"wow %d"] | ^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | i[: list[bytes]] = [b'/x01', b'/x02'] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class bytes(Sequence[int]): | ^^^^^ - | info: Source --> main2.py:LL:10 | LL | i[: list[bytes]] = [b'/x01', b'/x02'] | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | j[: list[int | float]] = [+1, +2.0] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | j[: list[int | float]] = [+1, +2.0] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class float: | ^^^^^ - | info: Source --> main2.py:LL:16 | LL | j[: list[int | float]] = [+1, +2.0] | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | k[: list[int | float]] = [-1, -2.0] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | k[: list[int | float]] = [-1, -2.0] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class float: | ^^^^^ - | info: Source --> main2.py:LL:16 | LL | k[: list[int | float]] = [-1, -2.0] | ^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2970,39 +2790,33 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:5 | LL | x[: Literal[Color.RED]] = Color.RED | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:4:7 | 4 | class Color(Enum): | ^^^^^ - | info: Source --> main2.py:8:13 | 8 | x[: Literal[Color.RED]] = Color.RED | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:5 | 5 | RED = 1 | ^^^ - | info: Source --> main2.py:8:19 | 8 | x[: Literal[Color.RED]] = Color.RED | ^^^ - | "); } @@ -3038,91 +2852,77 @@ Source with applied edits: | LL | class tuple(Sequence[_T_co]): | ^^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass()) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | info: Source --> main2.py:7:11 | 7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass()) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | info: Source --> main2.py:7:20 | 7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass()) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | info: Source --> main2.py:8:5 | 8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass() | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | info: Source --> main2.py:8:19 | 8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass() | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | info: Source --> main2.py:9:5 | 9 | c[: MyClass], d[: MyClass] = (MyClass(), MyClass()) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | info: Source --> main2.py:9:19 | 9 | c[: MyClass], d[: MyClass] = (MyClass(), MyClass()) | ^^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3170,494 +2970,418 @@ Source with applied edits: | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:18 | LL | self.x[: list[T@MyClass]] = x | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class tuple(Sequence[_T_co]): | ^^^^^ - | info: Source --> main2.py:LL:18 | LL | self.y[: tuple[U@MyClass, U@MyClass]] = y | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:7:5 | 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:13 | LL | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:18 | LL | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:7:35 | 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:7:45 | 7 | x[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class tuple(Sequence[_T_co]): | ^^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:8:11 | 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:19 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:24 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:8:30 | 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:38 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:43 | LL | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:8:62 | 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:8:72 | 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:8:97 | 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:8:107 | 8 | y[: tuple[MyClass[int, str], MyClass[int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:9:5 | 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:13 | LL | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:18 | LL | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:9:29 | 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:37 | LL | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:42 | LL | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:9:59 | 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:9:69 | 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:9:94 | 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:9:104 | 9 | a[: MyClass[int, str]], b[: MyClass[int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:10:5 | 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:13 | LL | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:18 | LL | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | class MyClass[T, U]: | ^^^^^^^ - | info: Source --> main2.py:10:29 | 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:37 | LL | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:42 | LL | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:10:60 | 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:10:70 | 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:10:95 | 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:36 | 3 | def __init__(self, x: list[T], y: tuple[U, U]): | ^ - | info: Source --> main2.py:10:105 | 10 | c[: MyClass[int, str]], d[: MyClass[int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3720,13 +3444,11 @@ Source with applied edits: | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:3:6 | 3 | foo([x=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3763,13 +3485,11 @@ Source with applied edits: | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:6:6 | 6 | foo([x=]y) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3814,13 +3534,11 @@ Source with applied edits: | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:10:6 | 10 | foo([x=]val.y) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3866,13 +3584,11 @@ Source with applied edits: | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:10:6 | 10 | foo([x=]x.y) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3921,13 +3637,11 @@ Source with applied edits: | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:12:6 | 12 | foo([x=]val.y()) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -3980,13 +3694,11 @@ Source with applied edits: | 4 | def foo(x: int): pass | ^ - | info: Source --> main2.py:14:6 | 14 | foo([x=]val.y()[1]) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4025,65 +3737,55 @@ Source with applied edits: | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | x[: list[int]] = [1] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | x[: list[int]] = [1] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: list[int]] = [2] | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:10 | LL | y[: list[int]] = [2] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:7:6 | 7 | foo([x=]y[0]) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4185,13 +3887,11 @@ Source with applied edits: | 2 | def foo(a: str, b: int, c: int, d: str): ... | ^ - | info: Source --> main2.py:4:6 | 4 | foo([a=]'foo', *t, d='bar') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4225,39 +3925,33 @@ Source with applied edits: | 2 | def foo(a: str, b: int, c: str): ... | ^ - | info: Source --> main2.py:4:6 | 4 | foo([a=]'foo', [b=]*t, [c=]'bar') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 | 2 | def foo(a: str, b: int, c: str): ... | ^ - | info: Source --> main2.py:4:17 | 4 | foo([a=]'foo', [b=]*t, [c=]'bar') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:25 | 2 | def foo(a: str, b: int, c: str): ... | ^ - | info: Source --> main2.py:4:25 | 4 | foo([a=]'foo', [b=]*t, [c=]'bar') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4290,26 +3984,22 @@ Source with applied edits: | 2 | def foo(a: int, b: int): ... | ^ - | info: Source --> main2.py:4:6 | 4 | foo([a=]1, [b=]*t) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 | 2 | def foo(a: int, b: int): ... | ^ - | info: Source --> main2.py:4:13 | 4 | foo([a=]1, [b=]*t) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4342,13 +4032,11 @@ Source with applied edits: | 2 | def foo(a: int): ... | ^ - | info: Source --> main2.py:4:6 | 4 | foo([a=]*t) | ^ - | "); } @@ -4370,13 +4058,11 @@ Source with applied edits: | 2 | def foo(x: int, /, y: int): pass | ^ - | info: Source --> main2.py:3:9 | 3 | foo(1, [y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4441,26 +4127,22 @@ Source with applied edits: | 3 | def __init__(self, x: int): pass | ^ - | info: Source --> main2.py:4:6 | 4 | Foo([x=]1) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:24 | 3 | def __init__(self, x: int): pass | ^ - | info: Source --> main2.py:5:10 | 5 | f = Foo([x=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4503,26 +4185,22 @@ Source with applied edits: | 5 | x: int | ^ - | info: Source --> main2.py:8:6 | 8 | Foo([x=]1, [y=]'a') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:6:5 | 6 | y: str | ^ - | info: Source --> main2.py:8:13 | 8 | Foo([x=]1, [y=]'a') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4557,26 +4235,22 @@ Source with applied edits: | 3 | def __new__(cls, x: int): pass | ^ - | info: Source --> main2.py:4:6 | 4 | Foo([x=]1) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:22 | 3 | def __new__(cls, x: int): pass | ^ - | info: Source --> main2.py:5:10 | 5 | f = Foo([x=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4615,13 +4289,11 @@ Source with applied edits: | 3 | def __call__(self, x: int): pass | ^ - | info: Source --> main2.py:6:6 | 6 | Foo([x=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4671,13 +4343,11 @@ Source with applied edits: | 3 | def bar(self, y: int): pass | ^ - | info: Source --> main2.py:4:12 | 4 | Foo().bar([y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4732,26 +4402,22 @@ Source with applied edits: | 8 | def choose(self: "Parent", parent_value: int) -> None: ... | ^^^^^^^^^^^^ - | info: Source --> main2.py:14:20 | 14 | parent.choose([parent_value=]1) | ^^^^^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:6:31 | 6 | def choose(self: "Child", child_value: int) -> None: ... | ^^^^^^^^^^^ - | info: Source --> main2.py:15:19 | 15 | child.choose([child_value=]2) | ^^^^^^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4788,13 +4454,11 @@ Source with applied edits: | 4 | def bar(cls, y: int): pass | ^ - | info: Source --> main2.py:5:10 | 5 | Foo.bar([y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4829,13 +4493,11 @@ Source with applied edits: | 4 | def bar(y: int): pass | ^ - | info: Source --> main2.py:5:10 | 5 | Foo.bar([y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4868,26 +4530,22 @@ Source with applied edits: | 2 | def foo(x: int | str): pass | ^ - | info: Source --> main2.py:3:6 | 3 | foo([x=]1) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(x: int | str): pass | ^ - | info: Source --> main2.py:4:6 | 4 | foo([x=]'abc') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4920,39 +4578,33 @@ Source with applied edits: | 2 | def foo(x: int, y: str, z: bool): pass | ^ - | info: Source --> main2.py:3:6 | 3 | foo([x=]1, [y=]'hello', [z=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 | 2 | def foo(x: int, y: str, z: bool): pass | ^ - | info: Source --> main2.py:3:13 | 3 | foo([x=]1, [y=]'hello', [z=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:25 | 2 | def foo(x: int, y: str, z: bool): pass | ^ - | info: Source --> main2.py:3:26 | 3 | foo([x=]1, [y=]'hello', [z=]True) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -4987,39 +4639,33 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:9 | LL | total[: int] = add([x=]3, [b=]2, y=4) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def add(x: int, b, y: int) -> int: | ^ - | info: Source --> main2.py:5:21 | 5 | total[: int] = add([x=]3, [b=]2, y=4) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 | 2 | def add(x: int, b, y: int) -> int: | ^ - | info: Source --> main2.py:5:28 | 5 | total[: int] = add([x=]3, [b=]2, y=4) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5050,13 +4696,11 @@ Source with applied edits: | 2 | def foo(x: int, y: str, z: bool): pass | ^ - | info: Source --> main2.py:3:6 | 3 | foo([x=]1, z=True, y='hello') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5089,13 +4733,11 @@ Source with applied edits: | 2 | def foo(x: int, y: str): pass | ^ - | info: Source --> main2.py:3:17 | 3 | foo(y='hello', [y=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5130,78 +4772,66 @@ Source with applied edits: | 2 | def foo(x: int, y: str = 'default', z: bool = False): pass | ^ - | info: Source --> main2.py:3:6 | 3 | foo([x=]1) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(x: int, y: str = 'default', z: bool = False): pass | ^ - | info: Source --> main2.py:4:6 | 4 | foo([x=]1, [y=]'custom') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 | 2 | def foo(x: int, y: str = 'default', z: bool = False): pass | ^ - | info: Source --> main2.py:4:13 | 4 | foo([x=]1, [y=]'custom') | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(x: int, y: str = 'default', z: bool = False): pass | ^ - | info: Source --> main2.py:5:6 | 5 | foo([x=]1, [y=]'custom', [z=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:17 | 2 | def foo(x: int, y: str = 'default', z: bool = False): pass | ^ - | info: Source --> main2.py:5:13 | 5 | foo([x=]1, [y=]'custom', [z=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:37 | 2 | def foo(x: int, y: str = 'default', z: bool = False): pass | ^ - | info: Source --> main2.py:5:27 | 5 | foo([x=]1, [y=]'custom', [z=]True) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5250,78 +4880,66 @@ Source with applied edits: | 8 | def baz(a: int, b: str, c: bool): pass | ^ - | info: Source --> main2.py:10:6 | 10 | baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(x: int) -> int: | ^ - | info: Source --> main2.py:10:14 | 10 | baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:8:17 | 8 | def baz(a: int, b: str, c: bool): pass | ^ - | info: Source --> main2.py:10:22 | 10 | baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:9 | 5 | def bar(y: str) -> str: | ^ - | info: Source --> main2.py:10:30 | 10 | baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:9 | 5 | def bar(y: str) -> str: | ^ - | info: Source --> main2.py:10:38 | 10 | baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:8:25 | 8 | def baz(a: int, b: str, c: bool): pass | ^ - | info: Source --> main2.py:10:52 | 10 | baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5362,26 +4980,22 @@ Source with applied edits: | 3 | def foo(self, value: int) -> 'A': | ^^^^^ - | info: Source --> main2.py:8:10 | 8 | A().foo([value=]42).bar([name=]'test').baz() | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:19 | 5 | def bar(self, name: str) -> 'A': | ^^^^ - | info: Source --> main2.py:8:26 | 8 | A().foo([value=]42).bar([name=]'test').baz() | ^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5418,13 +5032,11 @@ Source with applied edits: | 2 | def foo(x: str) -> str: | ^ - | info: Source --> main2.py:5:12 | 5 | bar(y=foo([x=]'test')) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5459,26 +5071,22 @@ Source with applied edits: | LL | Unknown: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:14 | LL | foo[: (x) -> Unknown] = lambda x: x * 2 | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/ty_extensions/__init__.pyi:LL:1 | LL | Unknown: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:17 | LL | bar[: (a, b) -> Unknown] = lambda a, b: a + b | ^^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5515,13 +5123,11 @@ Source with applied edits: | LL | LiteralString as LiteralString, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info: Source --> main2.py:LL:9 | LL | y[: LiteralString] = x | ^^^^^^^^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5573,78 +5179,66 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:9 | LL | y[: Literal[1, 2, 3, "hello"] | None] = x | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:17 | LL | y[: Literal[1, 2, 3, "hello"] | None] = x | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:20 | LL | y[: Literal[1, 2, 3, "hello"] | None] = x | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:23 | LL | y[: Literal[1, 2, 3, "hello"] | None] = x | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:26 | LL | y[: Literal[1, 2, 3, "hello"] | None] = x | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/types.pyi:LL:7 | LL | class NoneType: | ^^^^^^^^ - | info: Source --> main2.py:LL:37 | LL | y[: Literal[1, 2, 3, "hello"] | None] = x | ^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5680,26 +5274,22 @@ Source with applied edits: | 2 | class Foo[T]: ... | ^^^ - | info: Source --> main2.py:4:13 | 4 | a[: ] = Foo[int] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:17 | LL | a[: ] = Foo[int] | ^^^ - | "); } @@ -5721,39 +5311,33 @@ Source with applied edits: | LL | class type: | ^^^^ - | info: Source --> main2.py:LL:9 | LL | y[: type[list[str]]] = type(x) | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:14 | LL | y[: type[list[str]]] = type(x) | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:19 | LL | y[: type[list[str]]] = type(x) | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5790,13 +5374,11 @@ Source with applied edits: | 4 | def whatever(self): ... | ^^^^^^^^ - | info: Source --> main2.py:6:6 | 6 | ab[: property] = F.whatever | ^^^^^^^^ - | "); } @@ -5820,39 +5402,33 @@ Source with applied edits: | 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass | ^ - | info: Source --> main2.py:3:16 | 3 | foo(1, 'pos', [c=]3.14, [d=]False, e=42) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:38 | 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass | ^ - | info: Source --> main2.py:3:26 | 3 | foo(1, 'pos', [c=]3.14, [d=]False, e=42) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:28 | 2 | def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass | ^ - | info: Source --> main2.py:4:16 | 4 | foo(1, 'pos', [c=]3.14, e=42, f='custom') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5894,13 +5470,11 @@ Source with applied edits: | 2 | def bar(x: int | str): | ^ - | info: Source --> main2.py:4:6 | 4 | bar([x=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -5949,26 +5523,22 @@ Source with applied edits: | 5 | def foo(x: int) -> str: ... | ^ - | info: Source --> main2.py:11:6 | 11 | foo([x=]42) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:7:9 | 7 | def foo(x: str) -> int: ... | ^ - | info: Source --> main2.py:12:6 | 12 | foo([x=]'hello') | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6021,26 +5591,22 @@ Source with applied edits: | LL | class Sequence(Reversible[_T_co], Collection[_T_co]): | ^^^^^^^^ - | info: Source --> main2.py:LL:5 | LL | b[: Sequence[str]] = S('x', 'y') | ^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:14 | LL | b[: Sequence[str]] = S('x', 'y') | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6092,13 +5658,11 @@ Source with applied edits: | 5 | def f(x: int) -> str: ... | ^ - | info: Source --> main2.py:11:4 | 11 | f([x=][]) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6230,39 +5794,33 @@ Source with applied edits: | 2 | def foo(param: int): pass | ^^^^^ - | info: Source --> main2.py:7:6 | 7 | foo([param=]param2) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(param: int): pass | ^^^^^ - | info: Source --> main2.py:8:6 | 8 | foo([param=]my_param2) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:9 | 2 | def foo(param: int): pass | ^^^^^ - | info: Source --> main2.py:9:6 | 9 | foo([param=]parameter) | ^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6317,13 +5875,11 @@ Source with applied edits: | 2 | def foo(focus_range: int): pass | ^^^^^^^^^^^ - | info: Source --> main2.py:13:6 | 13 | foo([focus_range=]focus_end_range) | ^^^^^^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6358,13 +5914,11 @@ Source with applied edits: | 2 | def foo(x: int): pass | ^ - | info: Source --> main2.py:4:6 | 4 | foo([x=]1) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6396,13 +5950,11 @@ Source with applied edits: | 2 | def foo(_x: int, y: int): pass | ^ - | info: Source --> main2.py:3:9 | 3 | foo(1, [y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6441,26 +5993,22 @@ Source with applied edits: | 3 | x: int, | ^ - | info: Source --> main2.py:7:6 | 7 | foo([x=]1, [y=]2) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:4:5 | 4 | y: int | ^ - | info: Source --> main2.py:7:13 | 7 | foo([x=]1, [y=]2) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6493,91 +6041,77 @@ Source with applied edits: | LL | class int: | ^^^ - | info: Source --> main2.py:LL:16 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class bool(int): | ^^^^ - | info: Source --> main2.py:LL:25 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:37 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:43 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:49 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:54 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/ty_extensions/__init__.pyi:LL:1 | LL | Unknown: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:63 | LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo | ^^^^^^^ - | "); } @@ -6603,26 +6137,22 @@ Source with applied edits: | LL | class ModuleType: | ^^^^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | a[: ] = foo | ^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:1:1 | 1 | '''Foo module''' | ^^^^^^^^^^^^^^^^ - | info: Source --> main2.py:4:14 | 4 | a[: ] = foo | ^^^ - | "); } @@ -6646,52 +6176,44 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:20 | LL | a[: ] = Literal['a', 'b', 'c'] | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:28 | LL | a[: ] = Literal['a', 'b', 'c'] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:33 | LL | a[: ] = Literal['a', 'b', 'c'] | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:38 | LL | a[: ] = Literal['a', 'b', 'c'] | ^^^ - | "#); } @@ -6715,26 +6237,22 @@ Source with applied edits: | LL | class WrapperDescriptorType: | ^^^^^^^^^^^^^^^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | a[: ] = FunctionType.__get__ | ^^^^^^^^^^^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/types.pyi:LL:7 | LL | class FunctionType: | ^^^^^^^^^^^^ - | info: Source --> main2.py:LL:39 | LL | a[: ] = FunctionType.__get__ | ^^^^^^^^ - | "); } @@ -6758,52 +6276,44 @@ Source with applied edits: | LL | class MethodWrapperType: | ^^^^^^^^^^^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | a[: ] = f.__call__ | ^^^^^^^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/types.pyi:LL:9 | LL | def __call__(self, *args: Any, **kwargs: Any) -> Any: | ^^^^^^^^ - | info: Source --> main2.py:LL:22 | LL | a[: ] = f.__call__ | ^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/types.pyi:LL:7 | LL | class FunctionType: | ^^^^^^^^^^^^ - | info: Source --> main2.py:LL:35 | LL | a[: ] = f.__call__ | ^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:5 | 2 | def f(): ... | ^ - | info: Source --> main2.py:4:45 | 4 | a[: ] = f.__call__ | ^ - | "); } @@ -6831,78 +6341,66 @@ Source with applied edits: | LL | class NewType: | ^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | N[: ] = NewType([name=]'N', [tp=]str) | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:4:1 | 4 | N = NewType('N', str) | ^ - | info: Source --> main2.py:4:28 | 4 | N[: ] = NewType([name=]'N', [tp=]str) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:24 | LL | def __init__(self, name: str, tp: Any) -> None: ... # AnnotationForm | ^^^^ - | info: Source --> main2.py:LL:44 | LL | N[: ] = NewType([name=]'N', [tp=]str) | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:35 | LL | def __init__(self, name: str, tp: Any) -> None: ... # AnnotationForm | ^^ - | info: Source --> main2.py:LL:56 | LL | N[: ] = NewType([name=]'N', [tp=]str) | ^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:7 | LL | class NewType: | ^^^^^^^ - | info: Source --> main2.py:LL:6 | LL | Y[: ] = N | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:4:1 | 4 | N = NewType('N', str) | ^ - | info: Source --> main2.py:6:28 | 6 | Y[: ] = N | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -6934,26 +6432,22 @@ Source with applied edits: | LL | class type: | ^^^^ - | info: Source --> main2.py:LL:9 | LL | y[: type[T@f]] = x | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | 2 | def f[T](x: type[T]): | ^ - | info: Source --> main2.py:3:14 | 3 | y[: type[T@f]] = x | ^^^ - | "); } @@ -6977,39 +6471,33 @@ Source with applied edits: | LL | name: str, | ^^^^ - | info: Source --> main2.py:LL:14 | LL | T = TypeVar([name=]'T') | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:1 | LL | Protocol: _SpecialForm | ^^^^^^^^ - | info: Source --> main2.py:LL:26 | LL | Strange[: ] = Protocol[T] | ^^^^^^^^^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:3:1 | 3 | T = TypeVar('T') | ^ - | info: Source --> main2.py:4:42 | 4 | Strange[: ] = Protocol[T] | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7041,13 +6529,11 @@ Source with applied edits: | LL | name: str, | ^^^^ - | info: Source --> main2.py:LL:16 | LL | P = ParamSpec([name=]'P') | ^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7078,26 +6564,22 @@ Source with applied edits: | LL | def __new__(cls, name: str, value: Any, *, type_params: tuple[_TypeParameter, ...] = ()) -> Self: ... | ^^^^ - | info: Source --> main2.py:LL:20 | LL | A = TypeAliasType([name=]'A', [value=]str) | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:37 | LL | def __new__(cls, name: str, value: Any, *, type_params: tuple[_TypeParameter, ...] = ()) -> Self: ... | ^^^^^ - | info: Source --> main2.py:LL:32 | LL | A = TypeAliasType([name=]'A', [value=]str) | ^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7128,13 +6610,11 @@ Source with applied edits: | LL | name: str, | ^^^^ - | info: Source --> main2.py:LL:20 | LL | Ts = TypeVarTuple([name=]'Ts') | ^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7173,39 +6653,33 @@ Source with applied edits: | LL | Top: _SpecialForm | ^^^ - | info: Source --> main2.py:LL:9 | LL | x[: Top[list[Any]]] = xyxy | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:13 | LL | x[: Top[list[Any]]] = xyxy | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:7 | LL | class Any: | ^^^ - | info: Source --> main2.py:LL:18 | LL | x[: Top[list[Any]]] = xyxy | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7262,117 +6736,99 @@ Source with applied edits: | 6 | class B[T]: ... | ^ - | info: Source --> main2.py:4:5 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:4:19 | 4 | class A[T]: ... | ^ - | info: Source --> main2.py:4:7 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> bar.py:2:19 | 2 | class D[T, U]: ... | ^ - | info: Source --> main2.py:4:9 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:11 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:16 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:21 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:4:19 | 4 | class A[T]: ... | ^ - | info: Source --> main2.py:4:27 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:6:19 | 6 | class B[T]: ... | ^ - | info: Source --> main2.py:4:29 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:31 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = foo.C().foo() | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7432,117 +6888,99 @@ Source with applied edits: | 6 | class B[T]: ... | ^ - | info: Source --> main2.py:4:5 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:4:19 | 4 | class A[T]: ... | ^ - | info: Source --> main2.py:4:7 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> bar.py:2:19 | 2 | class D[T, U]: ... | ^ - | info: Source --> main2.py:4:9 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:11 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:16 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:21 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:4:19 | 4 | class A[T]: ... | ^ - | info: Source --> main2.py:4:27 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:6:19 | 6 | class B[T]: ... | ^ - | info: Source --> main2.py:4:29 | 4 | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class int: | ^^^ - | info: Source --> main2.py:LL:31 | LL | a[: B[A[D[int, list[str | A[B[int]]]]]]] = C().foo() | ^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7601,39 +7039,33 @@ Source with applied edits: | 2 | class D[T]: | ^ - | info: Source --> main2.py:6:5 | 6 | a[: D[Baz]] = D([x=]Baz) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:4:7 | 4 | class Baz: ... | ^^^ - | info: Source --> main2.py:6:7 | 6 | a[: D[Baz]] = D([x=]Baz) | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> foo/bar.py:3:36 | 3 | def __init__(self, x: type[T]): | ^ - | info: Source --> main2.py:6:18 | 6 | a[: D[Baz]] = D([x=]Baz) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7670,39 +7102,33 @@ Source with applied edits: | LL | class Any: | ^^^ - | info: Source --> main2.py:LL:9 | LL | a[: Any | Literal["some"]] = getattr(x, 'foo', "some") | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:1 | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:15 | LL | a[: Any | Literal["some"]] = getattr(x, 'foo', "some") | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class str(Sequence[str]): | ^^^ - | info: Source --> main2.py:LL:23 | LL | a[: Any | Literal["some"]] = getattr(x, 'foo', "some") | ^^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7750,52 +7176,44 @@ Source with applied edits: | LL | class dict(MutableMapping[_KT, _VT]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | a[: dict[TypeVar, Any] | None] = foo() | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:7 | LL | class TypeVar: | ^^^^^^^ - | info: Source --> main2.py:LL:10 | LL | a[: dict[TypeVar, Any] | None] = foo() | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:LL:7 | LL | class Any: | ^^^ - | info: Source --> main2.py:LL:19 | LL | a[: dict[TypeVar, Any] | None] = foo() | ^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/types.pyi:LL:7 | LL | class NoneType: | ^^^^^^^^ - | info: Source --> main2.py:LL:26 | LL | a[: dict[TypeVar, Any] | None] = foo() | ^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7862,26 +7280,22 @@ Source with applied edits: | 2 | class A: ... | ^ - | info: Source --> main2.py:4:5 | 4 | a[: bar.A | baz.A] = foo() | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> baz.py:2:19 | 2 | class A: ... | ^ - | info: Source --> main2.py:4:13 | 4 | a[: bar.A | baz.A] = foo() | ^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -7953,65 +7367,55 @@ Source with applied edits: | 2 | class A: ... | ^ - | info: Source --> main2.py:5:5 | 5 | a[: bar.A | baz.A | list[bar.A | baz.A]] = foo() | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> baz.py:2:22 | 2 | class A: ... | ^ - | info: Source --> main2.py:5:13 | 5 | a[: bar.A | baz.A | list[bar.A | baz.A]] = foo() | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:21 | LL | a[: bar.A | baz.A | list[bar.A | baz.A]] = foo() | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> bar.py:2:22 | 2 | class A: ... | ^ - | info: Source --> main2.py:5:26 | 5 | a[: bar.A | baz.A | list[bar.A | baz.A]] = foo() | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> baz.py:2:22 | 2 | class A: ... | ^ - | info: Source --> main2.py:5:34 | 5 | a[: bar.A | baz.A | list[bar.A | baz.A]] = foo() | ^^^^^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -8075,39 +7479,33 @@ Source with applied edits: | 8 | class B[T]: | ^ - | info: Source --> main2.py:11:5 | 11 | b[: B[A]] = B([x=]foo.A()) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> foo.py:2:19 | 2 | class A: ... | ^ - | info: Source --> main2.py:11:7 | 11 | b[: B[A]] = B([x=]foo.A()) | ^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:9:5 | 9 | x: T | ^ - | info: Source --> main2.py:11:16 | 11 | b[: B[A]] = B([x=]foo.A()) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -8153,39 +7551,33 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:5 | LL | x[: Literal[Color.RED]] = Color.RED | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> test.py:4:19 | 4 | class Color(Enum): | ^^^^^ - | info: Source --> main2.py:4:13 | 4 | x[: Literal[Color.RED]] = Color.RED | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> test.py:5:17 | 5 | RED = 1 | ^^^ - | info: Source --> main2.py:4:19 | 4 | x[: Literal[Color.RED]] = Color.RED | ^^^ - | "); } @@ -8230,39 +7622,33 @@ Source with applied edits: | LL | class list(MutableSequence[_T]): | ^^^^ - | info: Source --> main2.py:LL:5 | LL | y[: list[Inner]] = wrap([x=]Outer.Inner()) | ^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> module.py:3:23 | 3 | class Inner: ... | ^^^^^ - | info: Source --> main2.py:8:10 | 8 | y[: list[Inner]] = wrap([x=]Outer.Inner()) | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> main.py:5:13 | 5 | def wrap[T](x: T) -> list[T]: | ^ - | info: Source --> main2.py:8:26 | 8 | y[: list[Inner]] = wrap([x=]Outer.Inner()) | ^ - | --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -8308,39 +7694,33 @@ Source with applied edits: | LL | Literal: _SpecialForm | ^^^^^^^ - | info: Source --> main2.py:LL:5 | LL | x[: Literal[Color.RED]] = test.Color.RED | ^^^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> test.py:4:19 | 4 | class Color(Enum): | ^^^^^ - | info: Source --> main2.py:4:13 | 4 | x[: Literal[Color.RED]] = test.Color.RED | ^^^^^ - | info[inlay-hint-location]: Inlay Hint Target --> test.py:5:17 | 5 | RED = 1 | ^^^ - | info: Source --> main2.py:4:19 | 4 | x[: Literal[Color.RED]] = test.Color.RED | ^^^ - | "); } @@ -8377,13 +7757,11 @@ Source with applied edits: | 3 | class Inner: ... | ^^^^^ - | info: Source --> main2.py:4:5 | 4 | x[: Inner] = Outer().make() | ^^^^^ - | "); } @@ -8417,13 +7795,11 @@ Source with applied edits: | 3 | class Inner: ... | ^^^^^ - | info: Source --> main2.py:8:5 | 8 | x[: Inner] = Outer().make() | ^^^^^ - | "#); } diff --git a/crates/ty_ide/src/rename.rs b/crates/ty_ide/src/rename.rs index 050bcd30b7..183125a5ea 100644 --- a/crates/ty_ide/src/rename.rs +++ b/crates/ty_ide/src/rename.rs @@ -206,7 +206,6 @@ def outer(): 16 | write_nonlocal() 17 | return last | ---- - | "); } @@ -228,7 +227,6 @@ def f(items): | ^^^^ 4 | return last | ---- - | "); } @@ -252,7 +250,6 @@ def f(items): | ---- 2 | print(last) | ---- - | "); } @@ -296,7 +293,6 @@ func(value=42) 5 | 6 | func(value=42) | ----- - | "); } @@ -324,7 +320,6 @@ x = func | ---- 6 | x = func | ---- - | "); } @@ -354,7 +349,6 @@ cls = MyClass | ------- 7 | cls = MyClass | ------- - | "); } @@ -374,7 +368,6 @@ def func(): | 2 | def func(): | ^^^^ - | "); } @@ -427,7 +420,6 @@ class DataProcessor: 4 | def test(data): 5 | return func(data) | ---- - | "); } @@ -465,7 +457,6 @@ instance = ExampleClass(old_name="test") | 4 | instance = ExampleClass(old_name="test") | -------- - | "#); } @@ -489,7 +480,6 @@ instance = ExampleClass(old_name="test") 3 | 4 | class MyClass: | ------- - | "#); } @@ -513,7 +503,6 @@ instance = ExampleClass(old_name="test") 3 | 4 | class MyClass: | ------- - | "#); } @@ -551,7 +540,6 @@ instance = ExampleClass(old_name="test") 3 | 4 | class MyClass: | ------- - | "#); } @@ -603,7 +591,6 @@ instance = ExampleClass(old_name="test") 3 | 4 | class MyClass: | ------- - | "#); } @@ -640,7 +627,6 @@ instance = ExampleClass(old_name="test") | ^^ 5 | x = ab | -- - | "#); } @@ -663,7 +649,6 @@ instance = ExampleClass(old_name="test") | ^^ 5 | x = ab | -- - | "#); } @@ -686,7 +671,6 @@ instance = ExampleClass(old_name="test") | ^^ 5 | x = ab | -- - | "#); } @@ -709,7 +693,6 @@ instance = ExampleClass(old_name="test") | ^^ 5 | x = ab | -- - | "#); } @@ -732,7 +715,6 @@ instance = ExampleClass(old_name="test") | ^^ 5 | x = ab | -- - | "#); } @@ -755,7 +737,6 @@ instance = ExampleClass(old_name="test") | ^^ 5 | x = ab | -- - | "#); } @@ -784,7 +765,6 @@ instance = ExampleClass(old_name="test") | ^^ 11 | x = ab | -- - | "); } @@ -813,7 +793,6 @@ instance = ExampleClass(old_name="test") | ^^ 11 | x = ab | -- - | "); } @@ -848,7 +827,6 @@ instance = ExampleClass(old_name="test") 9 | match event: 10 | case Click(x, button=ab): | ----- - | "); } @@ -886,7 +864,6 @@ instance = ExampleClass(old_name="test") | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | ^^ -- -- - | "); } @@ -904,7 +881,6 @@ instance = ExampleClass(old_name="test") | 2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]] | ^^ -- -- - | "); } @@ -923,7 +899,6 @@ instance = ExampleClass(old_name="test") | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | ^^ -- -- - | "); } @@ -942,7 +917,6 @@ instance = ExampleClass(old_name="test") | 3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]] | ^^ -- -- - | "); } @@ -960,7 +934,6 @@ instance = ExampleClass(old_name="test") | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | ^^ -- -- - | "); } @@ -978,7 +951,6 @@ instance = ExampleClass(old_name="test") | 2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]] | ^^ -- -- - | "); } @@ -1048,7 +1020,6 @@ result = alias() | ^^^^^ 3 | result = alias() | ----- - | "); } @@ -1079,7 +1050,6 @@ result = alias() | ^^^^^ 3 | result = alias() | ----- - | "); } @@ -1144,7 +1114,6 @@ value1 = func_alias() 6 | 7 | result = original_function() | ----------------- - | "); } @@ -1196,7 +1165,6 @@ class App: 3 | 4 | func2() | ----- - | "); } @@ -1251,7 +1219,6 @@ result = func(10, y=20) 4 | 5 | result = func(10, y=20) | - - | "); } @@ -1278,7 +1245,6 @@ result = func(10, y=20) 4 | 5 | result = func(10, y=20) | - - | "); } @@ -1306,7 +1272,6 @@ TD(f=1) 7 | 8 | TD(f=1) | - - | "); } @@ -1334,7 +1299,6 @@ TD(f=1) 7 | 8 | TD(f=1) | - - | "); } @@ -1362,7 +1326,6 @@ NT(f=1) 7 | 8 | NT(f=1) | - - | "); } @@ -1391,7 +1354,6 @@ DC(f=1) 8 | 9 | DC(f=1) | - - | "); } @@ -1419,7 +1381,6 @@ DC(f=1) 4 | 5 | x = abc | --- - | "); } @@ -1446,7 +1407,6 @@ DC(f=1) 3 | 4 | x = lib2 | ---- - | "); } @@ -1478,7 +1438,6 @@ DC(f=1) | 1 | def deprecated(): pass | ---------- - | "); } @@ -1506,7 +1465,6 @@ DC(f=1) 4 | 5 | x = abc | --- - | "); } @@ -1537,7 +1495,6 @@ DC(f=1) | 4 | x = subpkg | ^^^^^^ - | "); } @@ -1670,7 +1627,6 @@ DC(f=1) | 2 | subpkg: int = 10 | ------ - | "); } @@ -1708,7 +1664,6 @@ DC(f=1) | 2 | subpkg: int = 10 | ------ - | "); } @@ -1764,7 +1719,6 @@ DC(f=1) 3 | 4 | test("test") | ---- - | "#); } @@ -1819,7 +1773,6 @@ DC(f=1) | 4 | Test().test("test") | ---- - | "#); } @@ -1876,7 +1829,6 @@ DC(f=1) 10 | 11 | def test(a: Any) -> Any: | ---- - | "#); } @@ -1913,7 +1865,6 @@ DC(f=1) | 4 | print(Foo().my_property) | ----------- - | "); } @@ -1963,7 +1914,6 @@ DC(f=1) | ----------- 5 | Foo().my_property = 56 | ----------- - | "); } @@ -2013,7 +1963,6 @@ DC(f=1) | ----------- 5 | del Foo().my_property | ----------- - | "); } @@ -2076,7 +2025,6 @@ DC(f=1) | ----------- 6 | del Foo().my_property | ----------- - | "); } @@ -2128,7 +2076,6 @@ DC(f=1) | ----------- 5 | Foo().my_property = 56 | ----------- - | "); } @@ -2180,7 +2127,6 @@ DC(f=1) | ----------- 5 | Foo().my_property = 56 | ----------- - | "); } @@ -2232,7 +2178,6 @@ DC(f=1) | ----------- 8 | def my_property(self, value: int) -> None: | ----------- - | "); } @@ -2276,7 +2221,6 @@ DC(f=1) | ----- 8 | def alpha(self, value: int) -> None: | ----- - | "); } @@ -2316,7 +2260,6 @@ DC(f=1) 10 | 11 | @my_func.setter | ------- - | "); } @@ -2353,13 +2296,12 @@ DC(f=1) // position-aware binding resolution in `definitions_for_name`. assert_snapshot!(test.rename("better_name"), @" info[rename]: Rename symbol (found 2 locations) - --> lib.py:11:2 + --> lib.py:12:5 | 11 | @my_func.setter | ------- 12 | def my_func(): | ^^^^^^^ - | "); } @@ -2394,7 +2336,6 @@ DC(f=1) 6 | 7 | @my_getter.setter | --------- - | "); } @@ -2436,7 +2377,6 @@ DC(f=1) 11 | 12 | @f.register | - - | "#); } @@ -2481,7 +2421,6 @@ DC(f=1) 12 | 13 | @f.register(str) | - - | "#); } @@ -2524,7 +2463,6 @@ DC(f=1) 12 | 13 | @f.register | - - | "#); } @@ -2571,7 +2509,6 @@ DC(f=1) 14 | 15 | @f.register | - - | "#); } @@ -2621,7 +2558,6 @@ DC(f=1) | - 16 | @f.register(float) | - - | "#); } @@ -2672,7 +2608,6 @@ DC(f=1) | --------- 16 | c.attribute = "new_value" | --------- - | "#); } @@ -2722,7 +2657,6 @@ DC(f=1) | ----------- 8 | def my_property(self, value: int) -> None: | ----------- - | "); } @@ -2761,7 +2695,6 @@ DC(f=1) | 4 | self.attribute = value | ^^^^^^^^^ - | "); } @@ -2793,7 +2726,6 @@ DC(f=1) 5 | 6 | print(a) | - - | "#); } @@ -2819,7 +2751,6 @@ class C: 5 | def __init__(self): 6 | self.value = 1 | ----- - | "#); } @@ -2845,7 +2776,6 @@ class C: 5 | def __init__(self): 6 | self.value = 1 | ----- - | "#); } @@ -2871,7 +2801,6 @@ class C: 5 | def __init__(self): 6 | self.value = 1 | ----- - | "#); } @@ -2897,7 +2826,6 @@ class C: 5 | def __init__(self): 6 | self.value = 1 | ----- - | "#); } @@ -2943,7 +2871,6 @@ class D: 5 | def __init__(self): 6 | self.value = 1 | ----- - | "#); } @@ -2969,7 +2896,6 @@ class C: 5 | def __init__(self): 6 | self.value = 1 | ----- - | "#); } @@ -2991,7 +2917,6 @@ class C: | ^^^^^ 4 | value: int | ----- - | "#); } @@ -3013,7 +2938,6 @@ class C: | 6 | self.value = 1 | ^^^^^ - | "); } @@ -3038,7 +2962,6 @@ class C: | ^^^^^ 4 | value: int = ... | ----- - | "#); } @@ -3064,7 +2987,6 @@ class C: | ^^^^^ 6 | self.value = value | ----- - | "#); } @@ -3089,7 +3011,6 @@ class Outer: | 7 | self.value = 1 | ^^^^^ - | "#); } } diff --git a/crates/ty_ide/src/selection_range.rs b/crates/ty_ide/src/selection_range.rs index d1560c14c4..6cac9348e3 100644 --- a/crates/ty_ide/src/selection_range.rs +++ b/crates/ty_ide/src/selection_range.rs @@ -85,28 +85,24 @@ x = 1 + 2 1 | / 2 | | x = 1 + 2 | |__________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | x = 1 + 2 | ^^^^^^^^^ - | info[selection-range]: Selection Range 2 --> main.py:2:5 | 2 | x = 1 + 2 | ^^^^^ - | info[selection-range]: Selection Range 3 --> main.py:2:9 | 2 | x = 1 + 2 | ^ - | "); } @@ -129,35 +125,30 @@ print(\"hello\") 1 | / 2 | | print("hello") | |_______________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | print("hello") | ^^^^^^^^^^^^^^ - | info[selection-range]: Selection Range 2 --> main.py:2:6 | 2 | print("hello") | ^^^^^^^^^ - | info[selection-range]: Selection Range 3 --> main.py:2:7 | 2 | print("hello") | ^^^^^^^ - | info[selection-range]: Selection Range 4 --> main.py:2:8 | 2 | print("hello") | ^^^^^ - | "#); } @@ -180,14 +171,12 @@ r"hello" 1 | / 2 | | r"hello" | |_________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | r"hello" | ^^^^^^^^ - | "#); } @@ -204,14 +193,12 @@ r"hello" | 1 | f"foo" b"bar" | ^^^^^^^^^^^^^ - | info[selection-range]: Selection Range 1 --> main.py:1:8 | 1 | f"foo" b"bar" | ^^^^^^ - | "#); } @@ -236,7 +223,6 @@ def my_function(): 2 | | def my_function(): 3 | | return 42 | |______________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 @@ -244,14 +230,12 @@ def my_function(): 2 | / def my_function(): 3 | | return 42 | |_____________^ - | info[selection-range]: Selection Range 2 --> main.py:2:5 | 2 | def my_function(): | ^^^^^^^^^^^ - | "); } @@ -278,7 +262,6 @@ class MyClass: 3 | | def __init__(self): 4 | | self.value = 1 | |_______________________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 @@ -287,14 +270,12 @@ class MyClass: 3 | | def __init__(self): 4 | | self.value = 1 | |______________________^ - | info[selection-range]: Selection Range 2 --> main.py:2:7 | 2 | class MyClass: | ^^^^^^^ - | "); } @@ -317,56 +298,48 @@ result = [(lambda x: x[key.attr])(item) for item in data if item is not 1 | / 2 | | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | |______________________________________________________________________________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info[selection-range]: Selection Range 2 --> main.py:2:10 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info[selection-range]: Selection Range 3 --> main.py:2:11 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info[selection-range]: Selection Range 4 --> main.py:2:12 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^^^^^^^^^^^^^^^^^^ - | info[selection-range]: Selection Range 5 --> main.py:2:22 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^^^^^^^^ - | info[selection-range]: Selection Range 6 --> main.py:2:24 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^^^^^ - | info[selection-range]: Selection Range 7 --> main.py:2:28 | 2 | result = [(lambda x: x[key.attr])(item) for item in data if item is not None] | ^^^^ - | "); } @@ -389,14 +362,12 @@ result = [(lambda x: x[key.attr])(item) for item in data if item is not 1 | / 2 | | "" | |___^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | "" | ^^ - | "#); } @@ -419,21 +390,18 @@ b"hello" 1 | / 2 | | b"hello" | |_________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | b"hello" | ^^^^^^^^ - | info[selection-range]: Selection Range 2 --> main.py:2:3 | 2 | b"hello" | ^^^^^ - | "#); } @@ -456,14 +424,12 @@ b"123a𝐁c" 1 | / 2 | | b"123a𝐁c" | |__________^ - | info[selection-range]: Selection Range 1 --> main.py:2:1 | 2 | b"123a𝐁c" | ^^^^^^^^^ - | "#); } diff --git a/crates/ty_ide/src/snapshots/ty_ide__code_action__tests__add_ignore_trailing_whitespace.snap b/crates/ty_ide/src/snapshots/ty_ide__code_action__tests__add_ignore_trailing_whitespace.snap index e806867f13..40cf35facd 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__code_action__tests__add_ignore_trailing_whitespace.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__code_action__tests__add_ignore_trailing_whitespace.snap @@ -8,7 +8,6 @@ info[code-action]: Ignore 'unresolved-reference' for this line 1 | b = a / 10 | ^ | - | - b = a / 10 1 + b = a / 10 # ty: ignore[unresolved-reference] | diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_literal_index_variants.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_literal_index_variants.snap index 1547de6380..300ef6b9b1 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_literal_index_variants.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_literal_index_variants.snap @@ -17,7 +17,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -36,7 +35,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -55,7 +53,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -74,7 +71,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -93,7 +89,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -112,7 +107,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -131,7 +125,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -150,7 +143,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -169,4 +161,3 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_non_literal_index.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_non_literal_index.snap index 77c9274dc5..983b0d938a 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_non_literal_index.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_non_literal_index.snap @@ -16,4 +16,3 @@ info[hover]: Hovered content is | | | source | Cursor offset - | diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_list_variants.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_list_variants.snap index 4f94fb7b1b..4d84c5ff9e 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_list_variants.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_list_variants.snap @@ -17,7 +17,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -36,7 +35,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -55,7 +53,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -74,7 +71,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -93,7 +89,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -112,4 +107,3 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_string_variants.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_string_variants.snap index 527c00fc20..0c934022d6 100644 --- a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_string_variants.snap +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_subscript_slice_literal_bounds_string_variants.snap @@ -17,7 +17,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -36,7 +35,6 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | @@ -55,4 +53,3 @@ info[hover]: Hovered content is | | | | | Cursor offset | source - | diff --git a/crates/ty_ide/src/workspace_symbols.rs b/crates/ty_ide/src/workspace_symbols.rs index 6a4e2799a4..6d29743335 100644 --- a/crates/ty_ide/src/workspace_symbols.rs +++ b/crates/ty_ide/src/workspace_symbols.rs @@ -96,7 +96,6 @@ API_BASE_URL = 'https://api.example.com' | 2 | def utility_function(): | ^^^^^^^^^^^^^^^^ - | info: Function utility_function "); @@ -106,7 +105,6 @@ API_BASE_URL = 'https://api.example.com' | 2 | class DataModel: | ^^^^^^^^^ - | info: Class DataModel "); @@ -116,7 +114,6 @@ API_BASE_URL = 'https://api.example.com' | 2 | API_BASE_URL = 'https://api.example.com' | ^^^^^^^^^^^^ - | info: Constant API_BASE_URL "); } @@ -139,7 +136,6 @@ class Test: | 3 | def from_path(): ... | ^^^^^^^^^ - | info: Method from_path "); } @@ -163,7 +159,6 @@ class Test: | 4 | def from_path(): ... | ^^^^^^^^^ - | info: Method from_path "); } @@ -188,7 +183,6 @@ foo = 1 | 5 | foo = 1 | ^^^ - | info: Variable foo "); assert_snapshot!(test.workspace_symbols("re"), @"No symbols found"); diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/any.md b/crates/ty_python_semantic/resources/mdtest/annotations/any.md index a8ca54c892..b4b9a3576e 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/any.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/any.md @@ -244,13 +244,12 @@ def check_callable_union(value1: CallableSubclassOfAny | IncompatibleCallable): ```snapshot error[invalid-assignment]: Object of type `CallableSubclassOfAny | IncompatibleCallable` is not assignable to `(int, /) -> int` - --> src/mdtest_snippet.py:141:14 + --> src/mdtest_snippet.py:141:37 | 141 | target1: Callable[[int], int] = value1 # snapshot | -------------------- ^^^^^^ Incompatible value of type `CallableSubclassOfAny | IncompatibleCallable` | | | Declared type - | info: element `IncompatibleCallable` of union `CallableSubclassOfAny | IncompatibleCallable` is not assignable to `(int, /) -> int` info: └── type `IncompatibleCallable` has inferred callable type `(x: int) -> bytes` info: └── incompatible return types: `bytes` is not assignable to `int` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md index 5b951562e7..4d36fdf892 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/literal_string.md @@ -46,7 +46,6 @@ error[invalid-type-form]: `LiteralString` expects no type parameter | 4 | a: LiteralString[str] | ^^^^^^^^^^^^^^^^^^ - | ``` ```py @@ -62,7 +61,6 @@ error[invalid-type-form]: `LiteralString` expects no type parameter | -------------^^^^^^^ | | | Did you mean `Literal`? - | ``` ### As a base class diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index c2e1c85b9d..bdc89a0c9d 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -201,7 +201,6 @@ warning[mismatched-type-name]: The name passed to `NewType` must match the varia | 5 | UserId = NewType("Id", int) | ^^^^ Expected "UserId", got "Id" - | ``` ```py @@ -217,7 +216,6 @@ warning[mismatched-type-name]: The name passed to `NewType` must match the varia | 10 | UsesExistingId = NewType("Id", "Id") | ^^^^ Expected "UsesExistingId", got "Id" - | ``` ## The base must be a class type or another newtype @@ -642,7 +640,6 @@ error[invalid-base]: Cannot subclass an instance of NewType | 6 | class Foo(X): ... | ^ - | info: Perhaps you were looking for: `Foo = NewType('Foo', X)` info: Definition of class `Foo` will raise `TypeError` at runtime ``` @@ -689,7 +686,6 @@ error[invalid-newtype]: invalid base for `typing.NewType` | 7 | UserId = NewType("UserId", Id) | ^^ type `Id` - | info: The base of a `NewType` is not allowed to be a protocol class. ``` @@ -707,7 +703,6 @@ error[invalid-newtype]: invalid base for `typing.NewType` | 12 | Bar = NewType("Bar", Foo) | ^^^ type `Foo` - | info: The base of a `NewType` is not allowed to be a `TypedDict`. ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/string.md b/crates/ty_python_semantic/resources/mdtest/annotations/string.md index 1d927a8eb8..6fed825953 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/string.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/string.md @@ -307,7 +307,6 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | 4 | c: """'"int"'""" = 1 | ^^^^^ Too many levels of nested string annotations; remove the redundant nested quotes - | error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation @@ -315,7 +314,6 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | 9 | f: "'str | int | bool | Foo | Bar'" = 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Nested string annotation is too long; remove the redundant nested quotes - | ``` ## Parameter @@ -393,7 +391,6 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | 43 | m: "yield 1" | ^^^^^^^ Yield expression cannot be used here - | help: Did you mean `typing.Literal["yield 1"]`? @@ -402,29 +399,26 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | 45 | n: "yield from 1" | ^^^^^^^^^^^^ Yield expression cannot be used here - | help: Did you mean `typing.Literal["yield from 1"]`? error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation - --> src/mdtest_snippet.py:55:5 + --> src/mdtest_snippet.py:55:10 | 55 | t: "list[yield from 1]" | -----^^^^^^^^^^^^- | | | Yield expression cannot be used here - | help: Did you mean `typing.Literal["list[yield from 1]"]`? error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation - --> src/mdtest_snippet.py:57:5 + --> src/mdtest_snippet.py:57:9 | 57 | u: "type]" | ----^ | | | Unexpected token at the end of an expression - | help: Did you mean `typing.Literal["type]"]`? ``` @@ -467,7 +461,7 @@ str ```snapshot error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation - --> src/mdtest_snippet.py:17:12 + --> src/mdtest_snippet.py:19:4 | 17 | a1: """ | ____________- @@ -476,11 +470,10 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | |____^- | | | Unexpected token at the end of an expression - | error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation - --> src/mdtest_snippet.py:22:12 + --> src/mdtest_snippet.py:23:6 | 22 | a2: """ | ____________- @@ -488,11 +481,10 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | | ^ Unexpected token at the end of an expression 24 | | str | |____- - | error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation - --> src/mdtest_snippet.py:27:12 + --> src/mdtest_snippet.py:28:12 | 27 | a3: """ | ____________- @@ -500,5 +492,4 @@ error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation | |____________^- | | | Unexpected token at the end of an expression - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index 535b6844e3..3687f181ee 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -27,13 +27,12 @@ a: Number = 1 ```snapshot error[invalid-assignment]: Object of type `Literal[1]` is not assignable to `Number` - --> src/mdtest_snippet.py:4:4 + --> src/mdtest_snippet.py:4:13 | 4 | a: Number = 1 | ------ ^ Incompatible value of type `Literal[1]` | | | Declared type - | info: Types from the `numbers` module aren't supported for static type checking help: Consider using a protocol instead, such as `typing.SupportsFloat` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md index 1f79f39434..76172dba8e 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md @@ -68,7 +68,6 @@ error[unsupported-operator]: Unsupported `-=` operation | | | | | Has type `Literal[1]` | Has type `C` - | ``` ## Method union diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 8d92813802..ad7442e7c0 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -1898,7 +1898,6 @@ error[unresolved-reference]: Name `x` used when not defined | 5 | y = x # snapshot | ^ - | info: An attribute `x` is available: consider using `self.x` ``` @@ -1916,7 +1915,6 @@ error[unresolved-reference]: Name `x` used when not defined | 10 | y = x # snapshot | ^ - | info: An attribute `x` is available: consider using `self.x` ``` @@ -2996,7 +2994,6 @@ error[invalid-assignment]: Cannot assign object of type `tuple[Literal[1], Liter | 5 | c.x = (1, b"") # snapshot: invalid-assignment | ^^^^^^^^ Expected `tuple[int, str]`, found `tuple[Literal[1], Literal[b""]]` - | info: Argument to bound method `C.__setattr__` is incorrect info: This assignment implicitly calls a custom `__setattr__` method info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` @@ -3005,7 +3002,6 @@ info: Method defined here | 2 | def __setattr__(self, name: str, value: tuple[int, str]): ... | ^^^^^^^^^^^ ---------------------- Parameter declared here - | ``` ### Overloaded `__setattr__` @@ -3030,7 +3026,6 @@ error[invalid-assignment]: Cannot assign object of type `tuple[Literal[1], Liter | 11 | d.x = (1, b"") # snapshot: invalid-assignment | ^^^ No overload of bound method `D.__setattr__` matches arguments - | info: This assignment implicitly calls a custom `__setattr__` method info: First overload defined here --> src/mdtest_snippet.py:4:5 @@ -3038,7 +3033,6 @@ info: First overload defined here 4 | / @overload 5 | | def __setattr__(self, name: str, value: tuple[int, str]): ... | |_________________________________________________________________^ First overload defined here - | info: Possible overloads for bound method `__setattr__`: info: (self, name: str, value: tuple[int, str]) -> Unknown info: (self, name: str, value: int) -> Unknown @@ -3047,7 +3041,6 @@ info: Overload implementation defined here | 8 | def __setattr__(self, name: str, value: tuple[int, str] | int): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ### Type of the `name` parameter @@ -3080,7 +3073,6 @@ error[invalid-assignment]: Cannot assign object of type `Literal["May"]` to attr | 13 | date.month = "May" # snapshot: invalid-assignment | ^^^^^ Expected `int`, found `Literal["May"]` - | info: Argument to bound method `Date.__setattr__` is incorrect info: This assignment implicitly calls a custom `__setattr__` method info: Method defined here @@ -3088,7 +3080,6 @@ info: Method defined here | 5 | def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None: | ^^^^^^^^^^^ ---------- Parameter declared here - | error[invalid-assignment]: Cannot assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` @@ -3096,7 +3087,6 @@ error[invalid-assignment]: Cannot assign object of type `Literal["UTC"]` to attr | 16 | date.tz = "UTC" | ^^^^^^^ Expected `Literal["day", "month", "year"]`, found `Literal["tz"]` - | info: Argument to bound method `Date.__setattr__` is incorrect info: This assignment implicitly calls a custom `__setattr__` method info: Method defined here @@ -3104,7 +3094,6 @@ info: Method defined here | 5 | def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None: | ^^^^^^^^^^^ ------------------------------------- Parameter declared here - | error[invalid-assignment]: Cannot assign object of type `Literal["UTC"]` to attribute `tz` on type `Date` @@ -3112,7 +3101,6 @@ error[invalid-assignment]: Cannot assign object of type `Literal["UTC"]` to attr | 16 | date.tz = "UTC" | ^^^^^ Expected `int`, found `Literal["UTC"]` - | info: Argument to bound method `Date.__setattr__` is incorrect info: This assignment implicitly calls a custom `__setattr__` method info: Method defined here @@ -3120,7 +3108,6 @@ info: Method defined here | 5 | def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None: | ^^^^^^^^^^^ ---------- Parameter declared here - | ``` ### Return type of `__setattr__` @@ -4234,7 +4221,6 @@ error[unresolved-attribute]: Module `datetime` has no member `UTC` | 4 | reveal_type(datetime.UTC) # revealed: Unknown | ^^^^^^^^^^^^ - | info: The member may be available on other Python versions or platforms info: Python 3.10 was assumed when resolving the `UTC` attribute because it was specified on the command line ``` @@ -4256,7 +4242,6 @@ error[unresolved-attribute]: Module `datetime` has no member `fakenotreal` | 4 | reveal_type(datetime.fakenotreal) # revealed: Unknown | ^^^^^^^^^^^^^^^^^^^^ - | ``` ## Unimported submodule incorrectly accessed as attribute @@ -4293,7 +4278,6 @@ warning[possibly-missing-submodule]: Submodule `bar` might not have been importe | 4 | reveal_type(foo.bar) # revealed: Unknown | ^^^^^^^ - | help: Consider explicitly importing `foo.bar` ``` @@ -4312,7 +4296,6 @@ warning[possibly-missing-submodule]: Submodule `bar` might not have been importe | 4 | reveal_type(baz.bar) # revealed: Unknown | ^^^^^^^ - | help: Consider explicitly importing `baz.bar` ``` @@ -4338,7 +4321,6 @@ error[unresolved-attribute]: Object of type `(...) -> Any` has no attribute `__n | 4 | x.__name__ # snapshot: unresolved-attribute | ^^^^^^^^^^ - | help: Function objects have a `__name__` attribute, but not all callable objects are functions help: See this FAQ for more information: ``` @@ -4354,7 +4336,6 @@ error[unresolved-attribute]: Object of type `(...) -> Any` has no attribute `__a | 6 | x.__annotate__ # snapshot: unresolved-attribute | ^^^^^^^^^^^^^^ - | help: Function objects have an `__annotate__` attribute, but not all callable objects are functions help: See this FAQ for more information: ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/custom.md b/crates/ty_python_semantic/resources/mdtest/binary/custom.md index 9c42bb9893..4331130477 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/custom.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/custom.md @@ -320,7 +320,6 @@ error[unsupported-operator]: Unsupported `+` operation | ---^^^--- | | | Both operands have type `` - | ``` ```py @@ -336,7 +335,6 @@ error[unsupported-operator]: Unsupported `+` operation | ---^^^--- | | | Both operands have type `` - | ``` ```py @@ -352,7 +350,6 @@ error[unsupported-operator]: Unsupported `+` operation | --^^^-- | | | Both operands have type `` - | ``` ## Subclass @@ -449,5 +446,4 @@ error[unsupported-operator]: Unsupported `+` operation | | | | | Has type `mod1.A` | Has type `mod2.A` - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/instances.md b/crates/ty_python_semantic/resources/mdtest/binary/instances.md index f458b0871b..3b8839588f 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/instances.md @@ -494,7 +494,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 7 | 10 and a and True | ^ - | info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/abstract_method.md b/crates/ty_python_semantic/resources/mdtest/call/abstract_method.md index 6509b63edf..e0552d43b4 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/abstract_method.md +++ b/crates/ty_python_semantic/resources/mdtest/call/abstract_method.md @@ -18,7 +18,7 @@ Foo.method() ```snapshot error[call-abstract-method]: Cannot call `method` on class object - --> src/mdtest_snippet.py:4:5 + --> src/mdtest_snippet.py:9:1 | 4 | / @classmethod 5 | | @abstractmethod @@ -28,7 +28,6 @@ error[call-abstract-method]: Cannot call `method` on class object 8 | # snapshot: call-abstract-method 9 | Foo.method() | ^^^^^^^^^^^^ `method` is an abstract classmethod with a trivial body - | ``` ## Abstract staticmethod with trivial body on class literal diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index bbf49b9c09..923cbe87f8 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -460,7 +460,6 @@ error[call-non-callable]: `NotImplemented` is not callable | --------------^^ | | | Did you mean `NotImplementedError`? - | ``` ```py @@ -477,7 +476,6 @@ error[call-non-callable]: `NotImplemented` is not callable | --------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | | | Did you mean `NotImplementedError`? - | ``` ## `map` with generic callbacks diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index 8893ee159c..ebb48072dc 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -1471,13 +1471,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 15 | f(**Foo1(a=1, b="b")) | ^^^^^^^^^^^^^^^^^^ Expected `int`, found `str` - | info: Function defined here --> src/mdtest_snippet.py:11:5 | 11 | def f(**kwargs: int) -> None: ... | ^ ------------- Parameter declared here - | error[invalid-argument-type]: Argument to function `f` is incorrect @@ -1485,13 +1483,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 15 | f(**Foo1(a=1, b="b")) | ^^^^^^^^^^^^^^^^^^ Possible extra items in unpacked open `TypedDict` have type `object`, expected `int` - | info: Function defined here --> src/mdtest_snippet.py:11:5 | 11 | def f(**kwargs: int) -> None: ... | ^ ------------- Parameter declared here - | ``` ### TypedDict union diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index 2a6a46926b..9f413f6019 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -616,13 +616,11 @@ error[missing-argument]: No argument provided for required parameter `arg` of fu | 18 | class MissingArg(RequiresArg): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info: Parameter declared here --> src/mdtest_snippet.py:13:32 | 13 | def __init_subclass__(cls, arg: int): ... | ^^^^^^^^ - | ``` ```py @@ -637,13 +635,11 @@ error[invalid-argument-type]: Argument to function `RequiresArg.__init_subclass_ | 20 | class InvalidType(RequiresArg, arg="foo"): ... | ^^^^^^^^^ Expected `int`, found `Literal["foo"]` - | info: Function defined here --> src/mdtest_snippet.py:13:9 | 13 | def __init_subclass__(cls, arg: int): ... | ^^^^^^^^^^^^^^^^^ -------- Parameter declared here - | ``` ```py @@ -668,13 +664,11 @@ error[missing-argument]: No argument provided for required parameter `arg` of fu | 24 | class IncorrectArg(RequiresArg, not_arg="foo"): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info: Parameter declared here --> src/mdtest_snippet.py:13:32 | 13 | def __init_subclass__(cls, arg: int): ... | ^^^^^^^^ - | error[unknown-argument]: Argument `not_arg` does not match any known parameter of function `RequiresArg.__init_subclass__` @@ -682,13 +676,11 @@ error[unknown-argument]: Argument `not_arg` does not match any known parameter o | 24 | class IncorrectArg(RequiresArg, not_arg="foo"): | ^^^^^^^^^^^^^ - | info: Function signature here --> src/mdtest_snippet.py:13:9 | 13 | def __init_subclass__(cls, arg: int): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ```py @@ -704,7 +696,7 @@ class Bad(NotCallableInitSubclass): ```snapshot error[non-callable-init-subclass]: Invalid definition of class `Bad` - --> src/mdtest_snippet.py:36:5 + --> src/mdtest_snippet.py:39:7 | 36 | __init_subclass__ = None | ----------------- `NotCallableInitSubclass.__init_subclass__` has type `None | Unknown`, which may not be callable @@ -712,7 +704,6 @@ error[non-callable-init-subclass]: Invalid definition of class `Bad` 38 | # snapshot: non-callable-init-subclass 39 | class Bad(NotCallableInitSubclass): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Superclass `NotCallableInitSubclass` cannot be subclassed - | info: `__init_subclass__` on a superclass is implicitly called during creation of a class object info: See https://docs.python.org/3/reference/datamodel.html#customizing-class-creation ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md index 9bdfbc6145..7713f85ecf 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -1030,7 +1030,6 @@ error[no-matching-overload]: No overload of function `f` matches arguments 39 | | a30=a, 40 | | ) | |_________^ - | info: Limit of argument type expansion reached at argument 9 info: First overload defined here --> src/overloaded.pyi:7:1 @@ -1038,7 +1037,6 @@ info: First overload defined here 7 | / @overload 8 | | def f() -> None: ... | |____________________^ First overload defined here - | info: Possible overloads for function `f`: info: () -> None info: (**kwargs: int) -> C diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index bbd7518d44..397fd71037 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -691,7 +691,6 @@ error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO | 7 | class Foo1(Generic[K, V], dict): ... # snapshot: inconsistent-mro | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Move `Generic[K, V]` to the end of the bases list | 6 | # error: [missing-type-argument] @@ -729,7 +728,6 @@ error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO 16 | | # comment5 17 | | ): ... | |_^ - | help: Move `Generic[K, V]` to the end of the bases list | 11 | # comment1 @@ -754,7 +752,6 @@ error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO | 19 | class Foo3(Generic[K, V], dict, metaclass=type): ... # snapshot: inconsistent-mro | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Move `Generic[K, V]` to the end of the bases list | 18 | # error: [missing-type-argument] @@ -796,7 +793,6 @@ error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO 28 | | # comment7 29 | | ): ... | |_^ - | help: Move `Generic[K, V]` to the end of the bases list | 21 | # comment1 @@ -828,7 +824,6 @@ error[duplicate-base]: Duplicate base class in class `Dup` | 4 | Dup = type("Dup", (A, A), {}) | ^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ## Metaclass conflicts @@ -956,7 +951,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to | 8 | X = type("X", (A, B), {}) | ^^^^^^^^^^^^^^^^^^^^^ Bases `A` and `B` cannot be combined in multiple inheritance - | info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts --> src/mdtest_snippet.py:8:16 | @@ -964,7 +958,6 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | - - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__` | | | `A` instances have a distinct memory layout because `A` defines non-empty `__slots__` - | ``` When the bases are not a tuple literal (e.g., a variable), the diagnostic is emitted without diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md index b878e2d5b0..ff862e998f 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/union.md +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -966,13 +966,11 @@ error[invalid-argument-type]: Argument to bound method `BytesCaller.__call__` is | 21 | f(None) | ^^^^ Expected `bytes`, found `None` - | info: Method defined here --> src/mdtest_snippet.py:13:9 | 13 | def __call__(self, x: bytes) -> bytes: | ^^^^^^^^ -------- Parameter declared here - | info: Union variant `BytesCaller` is incompatible with this call site info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller` @@ -982,13 +980,11 @@ error[invalid-argument-type]: Argument to bound method `IntCaller.__call__` is i | 21 | f(None) | ^^^^ Expected `int`, found `None` - | info: Method defined here --> src/mdtest_snippet.py:5:9 | 5 | def __call__(self, x: int) -> int: | ^^^^^^^^ ------ Parameter declared here - | info: Intersection element `IntCaller` is incompatible with this call site info: Attempted to call intersection type `IntCaller & StrCaller` info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller` @@ -999,13 +995,11 @@ error[invalid-argument-type]: Argument to bound method `StrCaller.__call__` is i | 21 | f(None) | ^^^^ Expected `str`, found `None` - | info: Method defined here --> src/mdtest_snippet.py:9:9 | 9 | def __call__(self, x: str) -> str: | ^^^^^^^^ ------ Parameter declared here - | info: Intersection element `StrCaller` is incompatible with this call site info: Attempted to call intersection type `IntCaller & StrCaller` info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md index f75a458a26..42508636e0 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md @@ -226,7 +226,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 9 | 10 in WithContains() | ^^^^^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` @@ -241,6 +240,5 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 11 | 10 not in WithContains() | ^^^^^^^^^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md index 6eed9d7338..837592fb10 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md @@ -371,7 +371,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 12 | 10 < Comparable() < 20 | ^^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` @@ -388,7 +387,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 14 | 10 < Comparable() < Comparable() | ^^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md index 56515ecc86..8c0630a142 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md @@ -164,7 +164,6 @@ error[unsupported-operator]: Unsupported `in` operation | | | | | Has type `NonContainer1 & NonContainer2` | Has type `Literal[2]` - | ``` Do not raise an error if at least one of the positive contributions to the intersection type support @@ -206,7 +205,6 @@ error[unsupported-operator]: Unsupported `in` operation | | | | | Has type `~NonContainer1` | Has type `Literal[2]` - | ``` ### Unsupported operators for negative contributions diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/unions.md b/crates/ty_python_semantic/resources/mdtest/comparison/unions.md index a2418d71b1..990b036dfc 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/unions.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/unions.md @@ -92,7 +92,6 @@ error[unsupported-operator]: Unsupported `in` operation | | | | | Has type `list[int] | Literal[1]` | Has type `Literal[1]` - | info: Operation fails because operator `in` is not supported between two objects of type `Literal[1]` ``` @@ -109,7 +108,6 @@ error[unsupported-operator]: Unsupported `in` operation | -^^^^- | | | Both operands have type `list[int] | Literal[1]` - | info: Operation fails because operator `in` is not supported between objects of type `list[int]` and `Literal[1]` ``` @@ -126,7 +124,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[str] | tuple[str, str]` | Has type `tuple[int]` - | info: Operation fails because operator `<` is not supported between objects of type `int` and `str` ``` @@ -143,7 +140,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[int]` | Has type `tuple[str] | tuple[str, str]` - | info: Operation fails because operator `<` is not supported between objects of type `str` and `int` ``` @@ -160,6 +156,5 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[str] | tuple[str, str]` | Has type `tuple[int] | tuple[int, int]` - | info: Operation fails because operator `<` is not supported between objects of type `int` and `str` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md b/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md index 8e4fd03486..7e55fc27b6 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md @@ -17,7 +17,6 @@ error[unsupported-operator]: Unsupported `in` operation | | | | | Has type `Literal[7]` | Has type `Literal[1]` - | ``` ```py @@ -35,7 +34,6 @@ error[unsupported-operator]: Unsupported `not in` operation | | | | | Has type `Literal[10]` | Has type `Literal[0]` - | ``` ```py @@ -53,7 +51,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `Literal[5]` | Has type `object` - | ``` ```py @@ -71,7 +68,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `object` | Has type `Literal[5]` - | ``` ```py @@ -90,7 +86,6 @@ error[unsupported-operator]: Unsupported `in` operation | | | | | Has type `Literal[1, "foo"]` | Has type `Literal[42]` - | info: Operation fails because operator `in` is not supported between objects of type `Literal[42]` and `Literal[1]` ``` @@ -109,7 +104,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[Literal[1], Literal["hello"]]` | Has type `tuple[Literal[1], Literal[2]]` - | info: Operation fails because operator `<` is not supported between the tuple elements at index 2 (of type `Literal[2]` and `Literal["hello"]`) ``` @@ -127,6 +121,5 @@ error[unsupported-operator]: Unsupported `<` operation | ------------^^^------------ | | | Both operands have type `tuple[bool, A]` - | info: Operation fails because operator `<` is not supported between the tuple elements at index 2 (both of type `A`) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 66d70c5c69..9e31b72786 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -1275,23 +1275,21 @@ class Child(FrozenBase): ```snapshot error[invalid-frozen-dataclass-subclass]: Non-frozen dataclass cannot inherit from frozen dataclass - --> src/foo.py:7:1 + --> src/foo.py:9:7 | 7 | @dataclass | ---------- `Child` dataclass parameters 8 | # snapshot: invalid-frozen-dataclass-subclass 9 | class Child(FrozenBase): | ^^^^^^----------^ Subclass `Child` is not frozen but base class `FrozenBase` is - | info: This causes the class creation to fail info: Base class definition - --> src/foo.py:3:1 + --> src/foo.py:4:7 | 3 | @dataclass(frozen=True) | ----------------------- `FrozenBase` dataclass parameters 4 | class FrozenBase: | ^^^^^^^^^^ `FrozenBase` definition - | ``` Frozen dataclasses inheriting from non-frozen dataclasses are also illegal: @@ -2258,7 +2256,6 @@ error[missing-argument]: No argument provided for required parameter `y` | 13 | C(3, "") | ^^^^^^^^ - | error[too-many-positional-arguments]: Too many positional arguments: expected 1, got 2 @@ -2266,7 +2263,6 @@ error[too-many-positional-arguments]: Too many positional arguments: expected 1, | 13 | C(3, "") | ^^ - | ``` Declaration order still controls `KW_ONLY` when a later field name was already referenced by an diff --git a/crates/ty_python_semantic/resources/mdtest/del.md b/crates/ty_python_semantic/resources/mdtest/del.md index 6ac52f4138..6fad0dad95 100644 --- a/crates/ty_python_semantic/resources/mdtest/del.md +++ b/crates/ty_python_semantic/resources/mdtest/del.md @@ -448,7 +448,6 @@ error[invalid-argument-type]: Cannot delete required key "name" from TypedDict ` | 19 | del m["name"] | ^^^^^^ - | info: Field defined here --> src/mdtest_snippet.py:3:7 | @@ -459,7 +458,6 @@ info: Field defined here | | | `name` declared as required here | Consider making it `NotRequired` - | info: Only keys marked as `NotRequired` (or in a TypedDict with `total=False`) can be deleted ``` @@ -488,7 +486,6 @@ error[invalid-argument-type]: Cannot delete required key "name" from TypedDict ` | 23 | del mixed["name"] | ^^^^^^ - | info: Field defined here --> src/mdtest_snippet.py:11:7 | @@ -499,7 +496,6 @@ info: Field defined here | | | `name` declared as required here | Consider making it `NotRequired` - | info: Only keys marked as `NotRequired` (or in a TypedDict with `total=False`) can be deleted ``` @@ -516,5 +512,4 @@ error[invalid-argument-type]: Cannot delete unknown key "non_existent" from Type | 25 | del mixed["non_existent"] | ^^^^^^^^^^^^^^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index 9fd9f0c066..351da6f068 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -791,7 +791,7 @@ DontAssignToMe().immutable = "the properties, they are a-changing" ```snapshot error[invalid-assignment]: Cannot assign to read-only property `immutable` on object of type `DontAssignToMe` - --> src/mdtest_snippet.py:3:9 + --> src/mdtest_snippet.py:6:1 | 3 | def immutable(self): ... | --------- Property `DontAssignToMe.immutable` defined here with no setter @@ -799,7 +799,6 @@ error[invalid-assignment]: Cannot assign to read-only property `immutable` on ob 5 | # snapshot: invalid-assignment 6 | DontAssignToMe().immutable = "the properties, they are a-changing" | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Attempted assignment to `DontAssignToMe.immutable` here - | ``` ### Built-in `classmethod` descriptor diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md index d903903d93..e94d43dab8 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_assignment.md @@ -29,7 +29,6 @@ error[invalid-assignment]: Object of type `Literal["wrong"]` is not assignable t | 8 | instance.attr = "wrong" # snapshot: invalid-assignment | ^^^^^^^^^^^^^ - | ``` And on the class object: @@ -44,7 +43,6 @@ error[invalid-assignment]: Object of type `Literal["wrong"]` is not assignable t | 9 | C.attr = "wrong" # snapshot: invalid-assignment | ^^^^^^ - | ``` ## Pure instance attributes @@ -74,7 +72,6 @@ error[invalid-attribute-access]: Cannot assign to instance attribute `attr` from | 8 | C.attr = 1 # snapshot: invalid-attribute-access | ^^^^^^ - | ``` ## Invalid annotated assignment to attribute @@ -96,23 +93,21 @@ class C: ```snapshot error[invalid-assignment]: Object of type `None` is not assignable to `str` - --> src/mdtest_snippet.py:3:20 + --> src/mdtest_snippet.py:3:26 | 3 | self.attr: str = None # snapshot: invalid-assignment | --- ^^^^ Incompatible value of type `None` | | | Declared type - | error[invalid-assignment]: Object of type `None` is not assignable to `str` - --> src/mdtest_snippet.py:8:26 + --> src/mdtest_snippet.py:8:32 | 8 | cls.class_attr1: str = None # snapshot: invalid-assignment | --- ^^^^ Incompatible value of type `None` | | | Declared type - | ``` Annotations on other attribute targets are ignored, and the assignment is checked against the @@ -180,7 +175,6 @@ error[invalid-attribute-access]: Cannot assign to ClassVar `attr` from an instan | 9 | instance.attr = 1 # snapshot: invalid-attribute-access | ^^^^^^^^^^^^^ - | ``` ## Unknown attributes @@ -199,7 +193,6 @@ error[unresolved-attribute]: Unresolved attribute `non_existent` on type ` None: | ^^^^^^^ ---------- Parameter declared here - | ``` ### Invalid `__set__` method signature @@ -319,14 +307,12 @@ error[invalid-assignment]: Invalid assignment to data descriptor attribute `attr | 10 | instance.attr = 1 # snapshot: invalid-assignment | ^^^^^^^^^^^^^ No argument provided for required parameter `extra` of function `WrongDescriptor.__set__` - | info: This assignment implicitly calls `__set__` on a descriptor of type `WrongDescriptor` info: Parameter declared here --> src/mdtest_snippet.py:2:53 | 2 | def __set__(self, instance: object, value: int, extra: int) -> None: | ^^^^^^^^^^ - | ``` ### Invalid property setter argument type @@ -355,7 +341,6 @@ error[invalid-assignment]: Invalid assignment to data descriptor attribute `docu | 11 | self.document = None # snapshot: invalid-assignment | ^^^^ Expected `Document`, found `None` - | info: Argument to function `HasDocumentRef.document` is incorrect info: This assignment implicitly calls `__set__` on a descriptor of type `property` info: Function defined here @@ -363,7 +348,6 @@ info: Function defined here | 7 | def document(self, document: Document) -> None: ... | ^^^^^^^^ ------------------ Parameter declared here - | ``` ### Nested argument type @@ -385,7 +369,6 @@ error[invalid-assignment]: Invalid assignment to data descriptor attribute `x` o | 8 | c.x = (1, b"") # snapshot: invalid-assignment | ^^^^^^^^ Expected `tuple[int, str]`, found `tuple[Literal[1], Literal[b""]]` - | info: Argument to function `Descriptor.__set__` is incorrect info: This assignment implicitly calls `__set__` on a descriptor of type `Descriptor` info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` @@ -394,7 +377,6 @@ info: Function defined here | 2 | def __set__(self, instance, value: tuple[int, str]) -> None: ... | ^^^^^^^ ---------------------- Parameter declared here - | ``` ## Setting attributes on union types @@ -429,5 +411,4 @@ error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attr | 10 | C1.attr = 1 # snapshot: invalid-assignment | ^^^^^^^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md index eda449b7bb..bc5c02d0fb 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md @@ -21,13 +21,12 @@ def _(source: str): ```snapshot error[invalid-assignment]: Object of type `str` is not assignable to `bytes` - --> src/mdtest_snippet.py:2:13 + --> src/mdtest_snippet.py:2:21 | 2 | target: bytes = source # snapshot | ----- ^^^^^^ Incompatible value of type `str` | | | Declared type - | ``` ## Unions @@ -41,13 +40,12 @@ def _(source: str | None): ```snapshot error[invalid-assignment]: Object of type `str | None` is not assignable to `str` - --> src/mdtest_snippet.py:2:13 + --> src/mdtest_snippet.py:2:19 | 2 | target: str = source # snapshot | --- ^^^^^^ Incompatible value of type `str | None` | | | Declared type - | info: element `None` of union `str | None` is not assignable to `str` ``` @@ -60,13 +58,12 @@ def _(source: int): ```snapshot error[invalid-assignment]: Object of type `int` is not assignable to `str | None` - --> src/mdtest_snippet.py:4:13 + --> src/mdtest_snippet.py:4:26 | 4 | target: str | None = source # snapshot | ---------- ^^^^^^ Incompatible value of type `int` | | | Declared type - | ``` Assigning a union to a union: @@ -78,13 +75,12 @@ def _(source: str | None): ```snapshot error[invalid-assignment]: Object of type `str | None` is not assignable to `bytes | None` - --> src/mdtest_snippet.py:6:13 + --> src/mdtest_snippet.py:6:28 | 6 | target: bytes | None = source # snapshot | ------------ ^^^^^^ Incompatible value of type `str | None` | | | Declared type - | info: element `str` of union `str | None` is not assignable to `bytes | None` ``` @@ -120,13 +116,12 @@ def _(source: Intersection[HasBar, HasNeither]): ```snapshot error[invalid-assignment]: Object of type `HasBar & HasNeither` is not assignable to `SupportsFooAndBar` - --> src/mdtest_snippet.py:23:13 + --> src/mdtest_snippet.py:23:33 | 23 | target: SupportsFooAndBar = source # snapshot | ----------------- ^^^^^^ Incompatible value of type `HasBar & HasNeither` | | | Declared type - | info: no element of intersection `HasBar & HasNeither` is assignable to `SupportsFooAndBar` info: ├── type `HasBar` is not assignable to protocol `SupportsFooAndBar` info: │ └── protocol member `foo` is not defined on type `HasBar` @@ -143,13 +138,12 @@ def _(source: HasFoo): ```snapshot error[invalid-assignment]: Object of type `HasFoo` is not assignable to `SupportsFoo & SupportsBar` - --> src/mdtest_snippet.py:25:13 + --> src/mdtest_snippet.py:25:54 | 25 | target: Intersection[SupportsFoo, SupportsBar] = source # snapshot | -------------------------------------- ^^^^^^ Incompatible value of type `HasFoo` | | | Declared type - | info: type `HasFoo` is not assignable to element `SupportsBar` of intersection `SupportsFoo & SupportsBar` info: └── type `HasFoo` is not assignable to protocol `SupportsBar` info: └── protocol member `bar` is not defined on type `HasFoo` @@ -164,13 +158,12 @@ def _(source: Intersection[HasFoo, HasNeither]): ```snapshot error[invalid-assignment]: Object of type `HasFoo & HasNeither` is not assignable to `SupportsFoo & SupportsBar` - --> src/mdtest_snippet.py:27:13 + --> src/mdtest_snippet.py:27:54 | 27 | target: Intersection[SupportsFoo, SupportsBar] = source # snapshot | -------------------------------------- ^^^^^^ Incompatible value of type `HasFoo & HasNeither` | | | Declared type - | info: type `HasFoo & HasNeither` is not assignable to element `SupportsBar` of intersection `SupportsFoo & SupportsBar` info: └── no element of intersection `HasFoo & HasNeither` is assignable to `SupportsBar` info: ├── type `HasFoo` is not assignable to protocol `SupportsBar` @@ -190,13 +183,12 @@ def _(source: tuple[int, str, bool]): ```snapshot error[invalid-assignment]: Object of type `tuple[int, str, bool]` is not assignable to `tuple[int, bytes, bool]` - --> src/mdtest_snippet.py:2:13 + --> src/mdtest_snippet.py:2:39 | 2 | target: tuple[int, bytes, bool] = source # snapshot | ----------------------- ^^^^^^ Incompatible value of type `tuple[int, str, bool]` | | | Declared type - | info: the second tuple element is not compatible: `str` is not assignable to `bytes` ``` @@ -209,13 +201,12 @@ def _(source: tuple[int, str]): ```snapshot error[invalid-assignment]: Object of type `tuple[int, str]` is not assignable to `tuple[int, str, bool]` - --> src/mdtest_snippet.py:4:13 + --> src/mdtest_snippet.py:4:37 | 4 | target: tuple[int, str, bool] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `tuple[int, str]` | | | Declared type - | info: a tuple of length 2 is not assignable to a tuple of length 3 ``` @@ -234,13 +225,12 @@ target: Callable[[int, bytes], bool] = source # snapshot ```snapshot error[invalid-assignment]: Object of type `def source(x: int, y: str) -> None` is not assignable to `(int, bytes, /) -> bool` - --> src/mdtest_snippet.py:6:9 + --> src/mdtest_snippet.py:6:40 | 6 | target: Callable[[int, bytes], bool] = source # snapshot | ---------------------------- ^^^^^^ Incompatible value of type `def source(x: int, y: str) -> None` | | | Declared type - | info: incompatible return types: `None` is not assignable to `bool` ``` @@ -253,13 +243,12 @@ def _(source: Callable[[int, str], bool]): ```snapshot error[invalid-assignment]: Object of type `(int, str, /) -> bool` is not assignable to `(int, bytes, /) -> bool` - --> src/mdtest_snippet.py:8:13 + --> src/mdtest_snippet.py:8:44 | 8 | target: Callable[[int, bytes], bool] = source # snapshot | ---------------------------- ^^^^^^ Incompatible value of type `(int, str, /) -> bool` | | | Declared type - | info: the second parameter has an incompatible type: `bytes` is not assignable to `str` ``` @@ -272,13 +261,12 @@ def _(source: Callable[[int, bytes], None]): ```snapshot error[invalid-assignment]: Object of type `(int, bytes, /) -> None` is not assignable to `(int, bytes, /) -> bool` - --> src/mdtest_snippet.py:10:13 + --> src/mdtest_snippet.py:10:44 | 10 | target: Callable[[int, bytes], bool] = source # snapshot | ---------------------------- ^^^^^^ Incompatible value of type `(int, bytes, /) -> None` | | | Declared type - | info: incompatible return types: `None` is not assignable to `bool` ``` @@ -291,13 +279,12 @@ def _(source: Callable[[int, str], bool]): ```snapshot error[invalid-assignment]: Object of type `(int, str, /) -> bool` is not assignable to `(int, /) -> bool` - --> src/mdtest_snippet.py:12:13 + --> src/mdtest_snippet.py:12:37 | 12 | target: Callable[[int], bool] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `(int, str, /) -> bool` | | | Declared type - | info: unexpected extra parameter ``` @@ -312,13 +299,12 @@ target: Callable[[int], bool] = source # snapshot ```snapshot error[invalid-assignment]: Object of type `def source(x: int, extra: str) -> bool` is not assignable to `(int, /) -> bool` - --> src/mdtest_snippet.py:16:9 + --> src/mdtest_snippet.py:16:33 | 16 | target: Callable[[int], bool] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `def source(x: int, extra: str) -> bool` | | | Declared type - | info: unexpected extra parameter `extra` ``` @@ -333,13 +319,12 @@ target: Callable[[str], Any] = Number # snapshot ```snapshot error[invalid-assignment]: Object of type `` is not assignable to `(str, /) -> Any` - --> src/mdtest_snippet.py:20:9 + --> src/mdtest_snippet.py:20:32 | 20 | target: Callable[[str], Any] = Number # snapshot | -------------------- ^^^^^^ Incompatible value of type `` | | | Declared type - | info: type `` has inferred callable type `(value: int) -> Number` info: └── the first parameter has an incompatible type: `str` is not assignable to `int` ``` @@ -363,7 +348,6 @@ error[invalid-argument-type]: Argument to function `accepts_callable` is incorre | 28 | accepts_callable(Foo) # snapshot | ^^^ Expected `(Any, /) -> Any`, found `` - | info: type `` has inferred callable type `(x: Any, y: Any) -> Foo` info: └── unexpected extra parameter `y` info: Function defined here @@ -371,7 +355,6 @@ info: Function defined here | 23 | def accepts_callable(callback: Callable[[Any], Any]) -> None: ... | ^^^^^^^^^^^^^^^^ ------------------------------ Parameter declared here - | ``` Assigning a bound method to a `Callable`: @@ -387,13 +370,12 @@ bound_method_target: Callable[[int], str] = greeter.greet # snapshot ```snapshot error[invalid-assignment]: Object of type `bound method Greeter.greet(name: str, greeting: str = "Hello") -> str` is not assignable to `(int, /) -> str` - --> src/mdtest_snippet.py:34:22 + --> src/mdtest_snippet.py:34:45 | 34 | bound_method_target: Callable[[int], str] = greeter.greet # snapshot | -------------------- ^^^^^^^^^^^^^ Incompatible value of type `bound method Greeter.greet(name: str, greeting: str = "Hello") -> str` | | | Declared type - | info: the first parameter has an incompatible type: `int` is not assignable to `str` ``` @@ -408,13 +390,12 @@ known_bound_method_target: Callable[[str], bool] = callable_base.__call__ # sna ```snapshot error[invalid-assignment]: Object of type `` is not assignable to `(str, /) -> bool` - --> src/mdtest_snippet.py:38:28 + --> src/mdtest_snippet.py:38:52 | 38 | known_bound_method_target: Callable[[str], bool] = callable_base.__call__ # snapshot | --------------------- ^^^^^^^^^^^^^^^^^^^^^^ Incompatible value of type `` | | | Declared type - | info: type `` has inferred callable type `(x: int) -> bool` info: └── the first parameter has an incompatible type: `str` is not assignable to `int` ``` @@ -433,13 +414,12 @@ partial_target: Callable[[bytes], bool] = partial_predicate # snapshot ```snapshot error[invalid-assignment]: Object of type `partial[(y: str) -> bool]` is not assignable to `(bytes, /) -> bool` - --> src/mdtest_snippet.py:45:17 + --> src/mdtest_snippet.py:45:43 | 45 | partial_target: Callable[[bytes], bool] = partial_predicate # snapshot | ----------------------- ^^^^^^^^^^^^^^^^^ Incompatible value of type `partial[(y: str) -> bool]` | | | Declared type - | info: the first parameter has an incompatible type: `bytes` is not assignable to `str` ``` @@ -471,7 +451,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: str) -> bool: | ---------------------------- `Parent.method` defined here - | info: parameter `x` has an incompatible type: `str` is not assignable to `bytes` info: This violates the Liskov Substitution Principle ``` @@ -500,7 +479,6 @@ error[invalid-method-override]: Invalid override of method `method` | 10 | def method(self, *, x: str, y: int) -> bool: | --------------------------------------- `ParentXY.method` defined here - | info: parameter `x` has an incompatible type: `str` is not assignable to `bytes` info: This violates the Liskov Substitution Principle ``` @@ -525,7 +503,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: str) -> bool: | ---------------------------- `Parent.method` defined here - | info: incompatible return types: `None` is not assignable to `bool` info: This violates the Liskov Substitution Principle ``` @@ -550,7 +527,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: str) -> bool: | ---------------------------- `Parent.method` defined here - | info: the parameter named `y` does not match `x` (and can be used as a keyword parameter) info: This violates the Liskov Substitution Principle ``` @@ -574,13 +550,12 @@ def _(source: Person): ```snapshot error[invalid-assignment]: Object of type `Person` is not assignable to `Other` - --> src/mdtest_snippet.py:10:13 + --> src/mdtest_snippet.py:10:21 | 10 | target: Other = source # snapshot | ----- ^^^^^^ Incompatible value of type `Person` | | | Declared type - | info: field "name" on TypedDict `Person` has type `str` which is not assignable to type `bytes` expected by TypedDict `Other` ``` @@ -597,13 +572,12 @@ def _(source: Person): ```snapshot error[invalid-assignment]: Object of type `Person` is not assignable to `PersonWithAge` - --> src/mdtest_snippet.py:16:13 + --> src/mdtest_snippet.py:16:29 | 16 | target: PersonWithAge = source # snapshot | ------------- ^^^^^^ Incompatible value of type `Person` | | | Declared type - | info: required field "age" is not present in source TypedDict `Person` ``` @@ -620,13 +594,12 @@ def _(source: PersonWithOptionalAge): ```snapshot error[invalid-assignment]: Object of type `PersonWithOptionalAge` is not assignable to `PersonWithAge` - --> src/mdtest_snippet.py:22:13 + --> src/mdtest_snippet.py:22:29 | 22 | target: PersonWithAge = source # snapshot | ------------- ^^^^^^ Incompatible value of type `PersonWithOptionalAge` | | | Declared type - | info: field "age" is required in TypedDict `PersonWithAge` but not required in TypedDict `PersonWithOptionalAge` ``` @@ -642,13 +615,12 @@ def _(source: PersonWithReadOnlyName): ```snapshot error[invalid-assignment]: Object of type `PersonWithReadOnlyName` is not assignable to `Person` - --> src/mdtest_snippet.py:27:13 + --> src/mdtest_snippet.py:27:22 | 27 | target: Person = source # snapshot | ------ ^^^^^^ Incompatible value of type `PersonWithReadOnlyName` | | | Declared type - | info: field "name" is read-only in TypedDict `PersonWithReadOnlyName` but mutable in TypedDict `Person` ``` @@ -661,13 +633,12 @@ def _(source: PersonWithAge): ```snapshot error[invalid-assignment]: Object of type `PersonWithAge` is not assignable to `PersonWithOptionalAge` - --> src/mdtest_snippet.py:29:13 + --> src/mdtest_snippet.py:29:37 | 29 | target: PersonWithOptionalAge = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `PersonWithAge` | | | Declared type - | info: field "age" is required in TypedDict `PersonWithAge` but not required and mutable in TypedDict `PersonWithOptionalAge` help: The required field could be removed through a destructive operation like `del` on the target. ``` @@ -681,13 +652,12 @@ def _(source: Person): ```snapshot error[invalid-assignment]: Object of type `Person` is not assignable to `dict[str, Any]` - --> src/mdtest_snippet.py:31:13 + --> src/mdtest_snippet.py:31:30 | 31 | target: dict[str, Any] = source # snapshot | -------------- ^^^^^^ Incompatible value of type `Person` | | | Declared type - | info: TypedDict `Person` is not assignable to `dict` help: A TypedDict is not usually assignable to any `dict[..]` type; `dict` types allow destructive operations like `clear()`. help: Consider using `Mapping[..]` instead of `dict[..]`. @@ -730,7 +700,6 @@ error[invalid-overload]: Implementation does not accept all arguments of this ov 17 | def method(self, value: str) -> None: ... 18 | def method(self, value: Pair[Self] | str) -> None: ... | ------ Implementation defined here - | info: Implementation signature `(self, value: Pair[Self@method] | str) -> None` is not assignable to overload signature `(self, value: Fixed) -> None` info: parameter `value` has an incompatible type: `Fixed` is not assignable to `Pair[Self@method] | str` info: └── type `Fixed` is not assignable to any element of the union `Pair[Self@method] | str` @@ -774,7 +743,6 @@ error[invalid-overload]: Implementation does not accept all arguments of this ov 17 | def method(self, value: str, later: str) -> None: ... 18 | def method(self, value: Pair[Self] | str, later: str) -> None: ... | ------ Implementation defined here - | info: Implementation signature `(self, value: Pair[Self@method] | str, later: str) -> None` is not assignable to overload signature `(self, value: Fixed, later: int) -> None` info: parameter `value` has an incompatible type: `Fixed` is not assignable to `Pair[Self@method] | str` info: └── type `Fixed` is not assignable to any element of the union `Pair[Self@method] | str` @@ -799,7 +767,7 @@ bad: Box[tuple[int, str, bool]] # snapshot: invalid-type-arguments ```snapshot error[invalid-type-arguments]: Type `tuple[int, str, bool]` is not assignable to upper bound `tuple[int, bytes, bool]` of type variable `T@Box` - --> src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:7:10 | 3 | T = TypeVar("T", bound=tuple[int, bytes, bool]) | - Type variable defined here @@ -808,7 +776,6 @@ error[invalid-type-arguments]: Type `tuple[int, str, bool]` is not assignable to 6 | 7 | bad: Box[tuple[int, str, bool]] # snapshot: invalid-type-arguments | ^^^^^^^^^^^^^^^^^^^^^ - | info: the second tuple element is not compatible: `str` is not assignable to `bytes` ``` @@ -830,13 +797,12 @@ def _(source: DoesNotHaveCheck): ```snapshot error[invalid-assignment]: Object of type `DoesNotHaveCheck` is not assignable to `SupportsCheck` - --> src/mdtest_snippet.py:9:13 + --> src/mdtest_snippet.py:9:29 | 9 | target: SupportsCheck = source # snapshot | ------------- ^^^^^^ Incompatible value of type `DoesNotHaveCheck` | | | Declared type - | info: type `DoesNotHaveCheck` is not assignable to protocol `SupportsCheck` info: └── protocol member `check` is not defined on type `DoesNotHaveCheck` ``` @@ -854,13 +820,12 @@ def _(source: CheckWithWrongSignature): ```snapshot error[invalid-assignment]: Object of type `CheckWithWrongSignature` is not assignable to `SupportsCheck` - --> src/mdtest_snippet.py:15:13 + --> src/mdtest_snippet.py:15:29 | 15 | target: SupportsCheck = source # snapshot | ------------- ^^^^^^ Incompatible value of type `CheckWithWrongSignature` | | | Declared type - | info: type `CheckWithWrongSignature` is not assignable to protocol `SupportsCheck` info: └── protocol member `check` is incompatible info: └── parameter `y` has an incompatible type: `str` is not assignable to `bytes` @@ -881,13 +846,12 @@ def _(source: DoesNotHaveName): ```snapshot error[invalid-assignment]: Object of type `DoesNotHaveName` is not assignable to `SupportsName` - --> src/mdtest_snippet.py:23:13 + --> src/mdtest_snippet.py:23:28 | 23 | target: SupportsName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `DoesNotHaveName` | | | Declared type - | info: type `DoesNotHaveName` is not assignable to protocol `SupportsName` info: └── protocol member `name` is not defined on type `DoesNotHaveName` ``` @@ -904,13 +868,12 @@ def _(source: SupportsSomethingElse): ```snapshot error[invalid-assignment]: Object of type `SupportsSomethingElse` is not assignable to `SupportsCheck` - --> src/mdtest_snippet.py:28:13 + --> src/mdtest_snippet.py:28:29 | 28 | target: SupportsCheck = source # snapshot | ------------- ^^^^^^ Incompatible value of type `SupportsSomethingElse` | | | Declared type - | info: protocol `SupportsSomethingElse` is not assignable to protocol `SupportsCheck` info: └── protocol member `check` is not defined on type `SupportsSomethingElse` ``` @@ -951,13 +914,12 @@ def _(source: BytesName): ```snapshot error[invalid-assignment]: Object of type `BytesName` is not assignable to `ReadableName` - --> src/mdtest_snippet.py:54:13 + --> src/mdtest_snippet.py:54:28 | 54 | target: ReadableName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `BytesName` | | | Declared type - | info: type `BytesName` is not assignable to protocol `ReadableName` info: └── protocol member `name` is incompatible info: └── read type `bytes` is not assignable to `str` @@ -970,13 +932,12 @@ def _(source: ReadOnlyName): ```snapshot error[invalid-assignment]: Object of type `ReadOnlyName` is not assignable to `WritableName` - --> src/mdtest_snippet.py:56:13 + --> src/mdtest_snippet.py:56:28 | 56 | target: WritableName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `ReadOnlyName` | | | Declared type - | info: type `ReadOnlyName` is not assignable to protocol `WritableName` info: └── protocol member `name` is incompatible info: └── the member does not accept writes of type `str` @@ -989,13 +950,12 @@ def _(source: BytesSetterName): ```snapshot error[invalid-assignment]: Object of type `BytesSetterName` is not assignable to `WritableName` - --> src/mdtest_snippet.py:58:13 + --> src/mdtest_snippet.py:58:28 | 58 | target: WritableName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `BytesSetterName` | | | Declared type - | info: type `BytesSetterName` is not assignable to protocol `WritableName` info: └── protocol member `name` is incompatible info: └── the member does not accept writes of type `str` @@ -1025,13 +985,12 @@ def _(source: ReadOnlyNameProtocol): ```snapshot error[invalid-assignment]: Object of type `ReadOnlyNameProtocol` is not assignable to `WritableName` - --> src/mdtest_snippet.py:72:13 + --> src/mdtest_snippet.py:72:28 | 72 | target: WritableName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `ReadOnlyNameProtocol` | | | Declared type - | info: protocol `ReadOnlyNameProtocol` is not assignable to protocol `WritableName` info: └── protocol member `name` is incompatible info: └── the member is not writable @@ -1044,13 +1003,12 @@ def _(source: BytesNameProtocol): ```snapshot error[invalid-assignment]: Object of type `BytesNameProtocol` is not assignable to `WritableName` - --> src/mdtest_snippet.py:74:13 + --> src/mdtest_snippet.py:74:28 | 74 | target: WritableName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `BytesNameProtocol` | | | Declared type - | info: protocol `BytesNameProtocol` is not assignable to protocol `WritableName` info: └── protocol member `name` is incompatible info: └── read type `bytes` is not assignable to `str` @@ -1063,13 +1021,12 @@ def _(source: BytesSetterNameProtocol): ```snapshot error[invalid-assignment]: Object of type `BytesSetterNameProtocol` is not assignable to `WritableName` - --> src/mdtest_snippet.py:76:13 + --> src/mdtest_snippet.py:76:28 | 76 | target: WritableName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `BytesSetterNameProtocol` | | | Declared type - | info: protocol `BytesSetterNameProtocol` is not assignable to protocol `WritableName` info: └── protocol member `name` is incompatible info: └── the member does not accept writes of type `str` @@ -1087,13 +1044,12 @@ def _(source: SupportsCheckWithOtherSignature): ```snapshot error[invalid-assignment]: Object of type `SupportsCheckWithOtherSignature` is not assignable to `SupportsCheck` - --> src/mdtest_snippet.py:81:13 + --> src/mdtest_snippet.py:81:29 | 81 | target: SupportsCheck = source # snapshot | ------------- ^^^^^^ Incompatible value of type `SupportsCheckWithOtherSignature` | | | Declared type - | info: protocol `SupportsCheckWithOtherSignature` is not assignable to protocol `SupportsCheck` info: └── protocol member `check` is incompatible info: └── parameter `y` has an incompatible type: `str` is not assignable to `bytes` @@ -1121,13 +1077,12 @@ def _(source: HasName): ```snapshot error[invalid-assignment]: Object of type `HasName` is not assignable to `StringOrName` - --> src/mdtest_snippet.py:13:13 + --> src/mdtest_snippet.py:13:28 | 13 | target: StringOrName = source # snapshot | ------------ ^^^^^^ Incompatible value of type `HasName` | | | Declared type - | info: type `HasName` is not assignable to any element of the union `str | SupportsName` info: ├── type `HasName` is not assignable to protocol `SupportsName` info: │ └── protocol member `name` is incompatible @@ -1148,13 +1103,12 @@ target: Callable[[tuple[int, bytes]], bool] = source # snapshot ```snapshot error[invalid-assignment]: Object of type `def source(x: tuple[int, str]) -> bool` is not assignable to `(tuple[int, bytes], /) -> bool` - --> src/mdtest_snippet.py:6:9 + --> src/mdtest_snippet.py:6:47 | 6 | target: Callable[[tuple[int, bytes]], bool] = source # snapshot | ----------------------------------- ^^^^^^ Incompatible value of type `def source(x: tuple[int, str]) -> bool` | | | Declared type - | info: the first parameter has an incompatible type: `tuple[int, bytes]` is not assignable to `tuple[int, str]` info: └── the second tuple element is not compatible: `bytes` is not assignable to `str` ``` @@ -1178,13 +1132,12 @@ def _(source: Incompatible): ```snapshot error[invalid-assignment]: Object of type `Incompatible` is not assignable to `SupportsCheck` - --> src/mdtest_snippet.py:12:13 + --> src/mdtest_snippet.py:12:29 | 12 | target: SupportsCheck = source # snapshot | ------------- ^^^^^^ Incompatible value of type `Incompatible` | | | Declared type - | info: type `Incompatible` is not assignable to protocol `SupportsCheck` info: └── protocol member `check1` is incompatible info: └── parameter `x` has an incompatible type: `str` is not assignable to `bytes` @@ -1209,13 +1162,12 @@ def _(source: HasNeither): ```snapshot error[invalid-assignment]: Object of type `HasNeither` is not assignable to `SupportsFoo | SupportsBar` - --> src/mdtest_snippet.py:12:13 + --> src/mdtest_snippet.py:12:41 | 12 | target: SupportsFoo | SupportsBar = source # snapshot | ------------------------- ^^^^^^ Incompatible value of type `HasNeither` | | | Declared type - | info: type `HasNeither` is not assignable to any element of the union `SupportsFoo | SupportsBar` info: ├── type `HasNeither` is not assignable to protocol `SupportsFoo` info: │ └── protocol member `foo` is not defined on type `HasNeither` @@ -1232,13 +1184,12 @@ def _(source: int): ```snapshot error[invalid-assignment]: Object of type `int` is not assignable to `str | bytes | bool | None` - --> src/mdtest_snippet.py:2:13 + --> src/mdtest_snippet.py:2:41 | 2 | target: str | bytes | bool | None = source # snapshot | ------------------------- ^^^^^^ Incompatible value of type `int` | | | Declared type - | ``` ## Failures for multiple intersection elements @@ -1259,13 +1210,12 @@ def _(source: Intersection[DoesNotSupportFoo1, DoesNotSupportFoo2]): ```snapshot error[invalid-assignment]: Object of type `DoesNotSupportFoo1 & DoesNotSupportFoo2` is not assignable to `SupportsFoo` - --> src/mdtest_snippet.py:11:13 + --> src/mdtest_snippet.py:11:27 | 11 | target: SupportsFoo = source # snapshot | ----------- ^^^^^^ Incompatible value of type `DoesNotSupportFoo1 & DoesNotSupportFoo2` | | | Declared type - | info: no element of intersection `DoesNotSupportFoo1 & DoesNotSupportFoo2` is assignable to `SupportsFoo` info: ├── type `DoesNotSupportFoo1` is not assignable to protocol `SupportsFoo` info: │ └── protocol member `foo` is not defined on type `DoesNotSupportFoo1` @@ -1299,13 +1249,12 @@ def _(source: IncompatibleFoo): ```snapshot error[invalid-assignment]: Object of type `IncompatibleFoo` is not assignable to `SupportsFooAndBar` - --> src/mdtest_snippet.py:16:13 + --> src/mdtest_snippet.py:16:33 | 16 | target: SupportsFooAndBar = source # snapshot | ----------------- ^^^^^^ Incompatible value of type `IncompatibleFoo` | | | Declared type - | info: type `IncompatibleFoo` is not assignable to protocol `SupportsFooAndBar` info: └── protocol member `foo` is incompatible info: └── the parameter named `name_` does not match `name` (and can be used as a keyword parameter) @@ -1322,13 +1271,12 @@ def _(source: list[str]): ```snapshot error[invalid-assignment]: Object of type `list[str]` is not assignable to `Iterable[bytes]` - --> src/mdtest_snippet.py:4:13 + --> src/mdtest_snippet.py:4:31 | 4 | target: Iterable[bytes] = source # snapshot | --------------- ^^^^^^ Incompatible value of type `list[str]` | | | Declared type - | info: type `list[str]` is not assignable to protocol `Iterable[bytes]` info: └── protocol member `__iter__` is incompatible info: └── incompatible return types: `Iterator[str]` is not assignable to `Iterator[bytes]` @@ -1349,13 +1297,12 @@ def _(source: list[bool]): ```snapshot error[invalid-assignment]: Object of type `list[bool]` is not assignable to `list[int]` - --> src/mdtest_snippet.py:2:13 + --> src/mdtest_snippet.py:2:25 | 2 | target: list[int] = source # snapshot | --------- ^^^^^^ Incompatible value of type `list[bool]` | | | Declared type - | info: `list` is invariant in its type parameter info: Consider using the covariant supertype `collections.abc.Sequence` info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics @@ -1409,163 +1356,150 @@ def _(source: MutableSequence[bool]): ```snapshot error[invalid-assignment]: Object of type `set[bool]` is not assignable to `set[int]` - --> src/mdtest_snippet.py:7:13 + --> src/mdtest_snippet.py:7:24 | 7 | target: set[int] = source # snapshot | -------- ^^^^^^ Incompatible value of type `set[bool]` | | | Declared type - | info: `set` is invariant in its type parameter info: Consider using the covariant supertype `collections.abc.Set` info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `dict[str, bool]` is not assignable to `dict[str, int]` - --> src/mdtest_snippet.py:10:13 + --> src/mdtest_snippet.py:10:30 | 10 | target: dict[str, int] = source # snapshot | -------------- ^^^^^^ Incompatible value of type `dict[str, bool]` | | | Declared type - | info: `dict` is invariant in its second type parameter info: Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `dict[bool, str]` is not assignable to `dict[int, str]` - --> src/mdtest_snippet.py:13:13 + --> src/mdtest_snippet.py:13:30 | 13 | target: dict[int, str] = source # snapshot | -------------- ^^^^^^ Incompatible value of type `dict[bool, str]` | | | Declared type - | info: `dict` is invariant in its first type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `dict[bool, bool]` is not assignable to `dict[int, int]` - --> src/mdtest_snippet.py:16:13 + --> src/mdtest_snippet.py:16:30 | 16 | target: dict[int, int] = source # snapshot | -------------- ^^^^^^ Incompatible value of type `dict[bool, bool]` | | | Declared type - | info: `dict` is invariant in its first and second type parameters info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `defaultdict[str, bool]` is not assignable to `defaultdict[str, int]` - --> src/mdtest_snippet.py:19:13 + --> src/mdtest_snippet.py:19:37 | 19 | target: defaultdict[str, int] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `defaultdict[str, bool]` | | | Declared type - | info: `defaultdict` is invariant in its second type parameter info: Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `defaultdict[bool, str]` is not assignable to `defaultdict[int, str]` - --> src/mdtest_snippet.py:22:13 + --> src/mdtest_snippet.py:22:37 | 22 | target: defaultdict[int, str] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `defaultdict[bool, str]` | | | Declared type - | info: `defaultdict` is invariant in its first type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `OrderedDict[str, bool]` is not assignable to `OrderedDict[str, int]` - --> src/mdtest_snippet.py:25:13 + --> src/mdtest_snippet.py:25:37 | 25 | target: OrderedDict[str, int] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `OrderedDict[str, bool]` | | | Declared type - | info: `OrderedDict` is invariant in its second type parameter info: Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `OrderedDict[bool, str]` is not assignable to `OrderedDict[int, str]` - --> src/mdtest_snippet.py:28:13 + --> src/mdtest_snippet.py:28:37 | 28 | target: OrderedDict[int, str] = source # snapshot | --------------------- ^^^^^^ Incompatible value of type `OrderedDict[bool, str]` | | | Declared type - | info: `OrderedDict` is invariant in its first type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `ChainMap[str, bool]` is not assignable to `ChainMap[str, int]` - --> src/mdtest_snippet.py:31:13 + --> src/mdtest_snippet.py:31:34 | 31 | target: ChainMap[str, int] = source # snapshot | ------------------ ^^^^^^ Incompatible value of type `ChainMap[str, bool]` | | | Declared type - | info: `ChainMap` is invariant in its second type parameter info: Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `ChainMap[bool, str]` is not assignable to `ChainMap[int, str]` - --> src/mdtest_snippet.py:34:13 + --> src/mdtest_snippet.py:34:34 | 34 | target: ChainMap[int, str] = source # snapshot | ------------------ ^^^^^^ Incompatible value of type `ChainMap[bool, str]` | | | Declared type - | info: `ChainMap` is invariant in its first type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `deque[bool]` is not assignable to `deque[int]` - --> src/mdtest_snippet.py:37:13 + --> src/mdtest_snippet.py:37:26 | 37 | target: deque[int] = source # snapshot | ---------- ^^^^^^ Incompatible value of type `deque[bool]` | | | Declared type - | info: `deque` is invariant in its type parameter info: Consider using the covariant supertype `collections.abc.Sequence` info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `Counter[bool]` is not assignable to `Counter[int]` - --> src/mdtest_snippet.py:40:13 + --> src/mdtest_snippet.py:40:28 | 40 | target: Counter[int] = source # snapshot | ------------ ^^^^^^ Incompatible value of type `Counter[bool]` | | | Declared type - | info: `Counter` is invariant in its type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics error[invalid-assignment]: Object of type `MutableSequence[bool]` is not assignable to `MutableSequence[int]` - --> src/mdtest_snippet.py:43:13 + --> src/mdtest_snippet.py:43:36 | 43 | target: MutableSequence[int] = source # snapshot | -------------------- ^^^^^^ Incompatible value of type `MutableSequence[bool]` | | | Declared type - | info: `MutableSequence` is invariant in its type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics ``` @@ -1586,13 +1520,12 @@ def _(source: MyContainer[bool]): ```snapshot error[invalid-assignment]: Object of type `MyContainer[bool]` is not assignable to `MyContainer[int]` - --> src/mdtest_snippet.py:52:13 + --> src/mdtest_snippet.py:52:32 | 52 | target: MyContainer[int] = source # snapshot | ---------------- ^^^^^^ Incompatible value of type `MyContainer[bool]` | | | Declared type - | info: `MyContainer` is invariant in its type parameter info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics ``` @@ -1606,13 +1539,12 @@ def _(source: list[int]): ```snapshot error[invalid-assignment]: Object of type `list[int]` is not assignable to `list[str]` - --> src/mdtest_snippet.py:54:13 + --> src/mdtest_snippet.py:54:25 | 54 | target: list[str] = source # snapshot | --------- ^^^^^^ Incompatible value of type `list[int]` | | | Declared type - | ``` We do not emit any error if the collection types are covariant: @@ -1641,13 +1573,12 @@ def f() -> tuple[int, str]: ```snapshot error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:1:12 + --> src/mdtest_snippet.py:2:12 | 1 | def f() -> tuple[int, str]: | --------------- Expected `tuple[int, str]` because of return type 2 | return 1, b"" # snapshot: invalid-return-type | ^^^^^^ expected `tuple[int, str]`, found `tuple[Literal[1], Literal[b""]]` - | info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` ``` @@ -1667,7 +1598,6 @@ error[invalid-assignment]: Object of type `tuple[Literal[1], Literal[b""]]` is n | 5 | c.x = (1, b"") # snapshot | ^^^ - | info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` ``` @@ -1682,13 +1612,12 @@ def f() -> Generator[tuple[int, str], None, None]: ```snapshot error[invalid-yield]: Yield expression type does not match annotation - --> src/mdtest_snippet.py:3:12 + --> src/mdtest_snippet.py:4:11 | 3 | def f() -> Generator[tuple[int, str], None, None]: | -------------------------------------- Function annotated with yield type `tuple[int, str]` here 4 | yield (1, b"") # snapshot: invalid-yield | ^^^^^^^^ expression of type `tuple[Literal[1], Literal[b""]]`, expected `tuple[int, str]` - | info: the second tuple element is not compatible: `Literal[b""]` is not assignable to `str` ``` @@ -1716,7 +1645,6 @@ error[not-iterable]: Object of type `WrongIterable` is not iterable | 12 | for _ in WrongIterable(): | ^^^^^^^^^^^^^^^ - | info: Its `__iter__` method returns an object of type `WrongIterator`, which has an invalid `__next__` method info: type `WrongIterable` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md index beb1e9d0b9..dfe995cf9c 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md @@ -18,13 +18,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo("hello") # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int) -> int: | ^^^ ------ Parameter declared here - | ``` ## Different source order @@ -45,13 +43,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 2 | foo("hello") # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/mdtest_snippet.py:4:5 | 4 | def foo(x: int) -> int: | ^^^ ------ Parameter declared here - | ``` ## Different files @@ -78,13 +74,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 3 | package.foo("hello") # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/package.py:1:5 | 1 | def foo(x: int) -> int: | ^^^ ------ Parameter declared here - | ``` ## Many parameters @@ -104,13 +98,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(1, "hello", 3) # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, z: int) -> int: | ^^^ ------ Parameter declared here - | ``` ## Many parameters across multiple lines @@ -135,7 +127,6 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 8 | foo(1, "hello", 3) # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | @@ -144,7 +135,6 @@ info: Function defined here 2 | x: int, 3 | y: int, | ------ Parameter declared here - | ``` ## Many parameters with multiple invalid arguments @@ -172,13 +162,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 7 | foo("a", "b", "c") | ^^^ Expected `int`, found `Literal["a"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, z: int) -> int: | ^^^ ------ Parameter declared here - | error[invalid-argument-type]: Argument to function `foo` is incorrect @@ -186,13 +174,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 7 | foo("a", "b", "c") | ^^^ Expected `int`, found `Literal["b"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, z: int) -> int: | ^^^ ------ Parameter declared here - | error[invalid-argument-type]: Argument to function `foo` is incorrect @@ -200,13 +186,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 7 | foo("a", "b", "c") | ^^^ Expected `int`, found `Literal["c"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, z: int) -> int: | ^^^ ------ Parameter declared here - | ``` ## Test calling a function whose type is vendored from `typeshed` @@ -226,7 +210,6 @@ error[invalid-argument-type]: Argument to function `loads` is incorrect | 3 | json.loads(5) # snapshot: invalid-argument-type | ^ Expected `str | bytes | bytearray`, found `Literal[5]` - | info: Function defined here --> stdlib/json/__init__.pyi:320:9 | @@ -234,7 +217,6 @@ info: Function defined here | ^^^^^ 321 | s: str | bytes | bytearray, | -------------------------- Parameter declared here - | ``` ## Tests for a variety of argument types @@ -259,13 +241,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(1, "hello", 3) # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, z: int, /) -> int: | ^^^ ------ Parameter declared here - | ``` ### Variadic arguments @@ -285,13 +265,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(1, 2, 3, "hello", 5) # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def foo(*numbers: int) -> int: | ^^^ ------------- Parameter declared here - | ``` ### Keyword only arguments @@ -311,13 +289,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(1, 2, z="hello") # snapshot: invalid-argument-type | ^^^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, *, z: int = 0) -> int: | ^^^ ---------- Parameter declared here - | ``` ### One keyword argument @@ -337,13 +313,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(1, 2, "hello") # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, y: int, z: int = 0) -> int: | ^^^ ---------- Parameter declared here - | ``` ### Variadic keyword arguments @@ -361,13 +335,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(a=1, b=2, c=3, d="hello", e=5) # snapshot: invalid-argument-type | ^^^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def foo(**numbers: int) -> int: | ^^^ -------------- Parameter declared here - | ``` ### Mix of arguments @@ -387,13 +359,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 4 | foo(1, 2, z="hello") # snapshot: invalid-argument-type | ^^^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def foo(x: int, /, y: int, *, z: int = 0) -> int: | ^^^ ---------- Parameter declared here - | ``` ### Synthetic arguments @@ -415,13 +385,11 @@ error[invalid-argument-type]: Argument to bound method `C.__call__` is incorrect | 6 | c("wrong") # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["wrong"]` - | info: Method defined here --> src/mdtest_snippet.py:2:9 | 2 | def __call__(self, x: int) -> int: | ^^^^^^^^ ------ Parameter declared here - | ``` ## Calls to methods @@ -443,13 +411,11 @@ error[invalid-argument-type]: Argument to bound method `C.square` is incorrect | 6 | c.square("hello") # snapshot: invalid-argument-type | ^^^^^^^ Expected `int`, found `Literal["hello"]` - | info: Method defined here --> src/mdtest_snippet.py:2:9 | 2 | def square(self, x: int) -> int: | ^^^^^^ ------ Parameter declared here - | ``` ## Calls to protocol methods @@ -470,13 +436,11 @@ error[invalid-argument-type]: Argument to bound method `P.method` is incorrect | 7 | p.method("bad") # snapshot: invalid-argument-type | ^^^^^ Expected `int`, found `Literal["bad"]` - | info: Method defined here --> src/mdtest_snippet.py:4:9 | 4 | def method(self, value: int) -> None: ... | ^^^^^^ ---------- Parameter declared here - | ``` ## Calls to overloaded protocol methods @@ -500,13 +464,11 @@ error[invalid-argument-type]: Argument to bound method `P.method` is incorrect | 10 | p.method("bad") # snapshot: invalid-argument-type | ^^^^^ Expected `int`, found `Literal["bad"]` - | info: Matching overload defined here --> src/mdtest_snippet.py:5:9 | 5 | def method(self, value: int) -> None: ... | ^^^^^^ ---------- Parameter declared here - | info: Non-matching overloads for bound method `method`: info: (self, /, value: int, extra: int) -> None ``` @@ -537,13 +499,11 @@ error[invalid-argument-type]: Argument to function `needs_a_foo` is incorrect | 5 | needs_a_foo(Foo()) # snapshot: invalid-argument-type | ^^^^^ Expected `module.Foo`, found `main.Foo` - | info: Function defined here --> src/module.py:3:5 | 3 | def needs_a_foo(x: Foo): ... | ^^^^^^^^^^^ ------ Parameter declared here - | ``` ## TypeVars with bounds that have the same name but are from different files @@ -581,13 +541,11 @@ error[invalid-argument-type]: Argument to function `needs_a_foo` is incorrect | 6 | needs_a_foo(x) # snapshot: invalid-argument-type | ^ Expected `Foo`, found `T@f` - | info: Function defined here --> src/module.py:3:5 | 3 | def needs_a_foo(x: Foo): ... | ^^^^^^^^^^^ ------ Parameter declared here - | ``` ## Numbers special case @@ -609,13 +567,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 5 | f(5) # snapshot: invalid-argument-type | ^ Expected `Number`, found `Literal[5]` - | info: Function defined here --> src/mdtest_snippet.py:3:5 | 3 | def f(x: Number): ... | ^ --------- Parameter declared here - | info: Types from the `numbers` module aren't supported for static type checking help: Consider using a protocol instead, such as `typing.SupportsFloat` @@ -625,14 +581,12 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 8 | f(x) # snapshot: invalid-argument-type | ^ Expected `Number`, found `int | float` - | info: element `int` of union `int | float` is not assignable to `Number` info: Function defined here --> src/mdtest_snippet.py:3:5 | 3 | def f(x: Number): ... | ^ --------- Parameter declared here - | info: Types from the `numbers` module aren't supported for static type checking help: Consider using a protocol instead, such as `typing.SupportsFloat` ``` @@ -656,13 +610,11 @@ error[invalid-argument-type]: Argument to function `modify` is incorrect | 5 | modify(xs) # snapshot: invalid-argument-type | ^^ Expected `list[int]`, found `list[bool]` - | info: Function defined here --> src/mdtest_snippet.py:1:5 | 1 | def modify(xs: list[int]): | ^^^^^^ ------------- Parameter declared here - | info: `list` is invariant in its type parameter info: Consider using the covariant supertype `collections.abc.Sequence` info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md index 6e0d318842..ee3be82f45 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_syntactic_variants.md @@ -13,13 +13,12 @@ Here, we point to the type annotation directly: ```snapshot error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int` - --> src/mdtest_snippet.py:1:4 + --> src/mdtest_snippet.py:1:10 | 1 | x: int = "three" # snapshot: invalid-assignment | --- ^^^^^^^ Incompatible value of type `Literal["three"]` | | | Declared type - | ``` ## Unannotated assignment @@ -34,13 +33,12 @@ type in an annotation on the variable name: ```snapshot error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int` - --> src/mdtest_snippet.py:2:1 + --> src/mdtest_snippet.py:2:5 | 2 | x = "three" # snapshot: invalid-assignment | - ^^^^^^^ Incompatible value of type `Literal["three"]` | | | Declared type `int` - | ``` ## Named expression @@ -55,13 +53,12 @@ Similar here, we could ideally point to the type annotation: ```snapshot error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int` - --> src/mdtest_snippet.py:3:2 + --> src/mdtest_snippet.py:3:7 | 3 | (x := "three") # snapshot: invalid-assignment | - ^^^^^^^ Incompatible value of type `Literal["three"]` | | | Declared type `int` - | ``` ## Multiline expressions @@ -79,7 +76,7 @@ x: str = ( ```snapshot error[invalid-assignment]: Object of type `Literal[15]` is not assignable to `str` - --> src/mdtest_snippet.py:4:4 + --> src/mdtest_snippet.py:4:10 | 4 | x: str = ( | ____---___^ @@ -90,7 +87,6 @@ error[invalid-assignment]: Object of type `Literal[15]` is not assignable to `st 7 | | ) 8 | | ) | |_^ Incompatible value of type `Literal[15]` - | ``` ## Multiple targets @@ -109,23 +105,21 @@ tuple: ```snapshot error[invalid-assignment]: Object of type `Literal["a"]` is not assignable to `int` - --> src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:4:8 | 4 | x, y = ("a", "b") # snapshot: invalid-assignment | - ^^^^^^^^^^ Incompatible value of type `Literal["a"]` | | | Declared type `int` - | error[invalid-assignment]: Object of type `Literal[0]` is not assignable to `str` - --> src/mdtest_snippet.py:6:4 + --> src/mdtest_snippet.py:6:8 | 6 | x, y = (0, 0) # snapshot: invalid-assignment | - ^^^^^^ Incompatible value of type `Literal[0]` | | | Declared type `str` - | ``` ## Shadowing of classes and functions diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md index 484ca4ca20..3f44b76613 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md @@ -48,13 +48,11 @@ error[missing-argument]: No argument provided for required parameter `a` of func | 3 | f() # snapshot | ^^^ - | info: Parameter declared here --> src/module.py:1:7 | 1 | def f(a, b=42): ... | ^ - | error[missing-argument]: No argument provided for required parameter `a` of function `f` @@ -62,7 +60,6 @@ error[missing-argument]: No argument provided for required parameter `a` of func | 12 | h(b=56) | ^^^^^^^ - | info: Union variant `def f(a, b=42) -> Unknown` is incompatible with this call site info: Attempted to call union type `(def f(a, b=42) -> Unknown) | (def g(a, b) -> Unknown)` @@ -72,7 +69,6 @@ error[missing-argument]: No argument provided for required parameter `a` of func | 12 | h(b=56) | ^^^^^^^ - | info: Union variant `def g(a, b) -> Unknown` is incompatible with this call site info: Attempted to call union type `(def f(a, b=42) -> Unknown) | (def g(a, b) -> Unknown)` @@ -82,13 +78,11 @@ error[missing-argument]: No argument provided for required parameter `a` of boun | 14 | Foo().method() # snapshot: missing-argument | ^^^^^^^^^^^^^^ - | info: Parameter declared here --> src/module.py:5:22 | 5 | def method(self, a): ... | ^ - | error[missing-argument]: No argument provided for required parameter `value` of bound method `P.method` @@ -96,11 +90,9 @@ error[missing-argument]: No argument provided for required parameter `value` of | 22 | p.method() # snapshot: missing-argument | ^^^^^^^^^^ - | info: Parameter declared here --> src/main.py:19:22 | 19 | def method(self, value: int) -> None: ... | ^^^^^^^^^^ - | ``` 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 a118b3af0f..a12a65f664 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 @@ -27,7 +27,6 @@ error[invalid-syntax]: cannot use an asynchronous comprehension inside of a sync | 6 | 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: @@ -44,7 +43,6 @@ error[not-iterable]: Object of type `range` is not async-iterable | 9 | return [[x async for x in elements(n)] async for n in range(3)] | ^^^^^^^^ - | info: It has no `__aiter__` method ``` @@ -497,7 +495,6 @@ error[invalid-syntax]: `break` outside loop | 1 | break # snapshot: invalid-syntax | ^^^^^ - | error[invalid-syntax]: `continue` outside loop @@ -505,7 +502,6 @@ error[invalid-syntax]: `continue` outside loop | 2 | continue # snapshot: invalid-syntax | ^^^^^^^^ - | error[invalid-syntax]: `break` outside loop @@ -513,7 +509,6 @@ error[invalid-syntax]: `break` outside loop | 9 | break # snapshot: invalid-syntax | ^^^^^ - | error[invalid-syntax]: `continue` outside loop @@ -521,7 +516,6 @@ error[invalid-syntax]: `continue` outside loop | 10 | continue # snapshot: invalid-syntax | ^^^^^^^^ - | error[invalid-syntax]: `break` outside loop @@ -529,7 +523,6 @@ error[invalid-syntax]: `break` outside loop | 14 | break # snapshot: invalid-syntax | ^^^^^ - | error[invalid-syntax]: `continue` outside loop @@ -537,7 +530,6 @@ error[invalid-syntax]: `continue` outside loop | 15 | continue # snapshot: invalid-syntax | ^^^^^^^^ - | ``` ## name cannot refer to a parameter and a global variable @@ -582,7 +574,6 @@ error[invalid-syntax]: name `a` cannot refer to a parameter and a global variabl | 4 | global a # snapshot: invalid-syntax | ^ - | error[invalid-syntax]: name `a` cannot refer to a parameter and a global variable @@ -590,7 +581,6 @@ error[invalid-syntax]: name `a` cannot refer to a parameter and a global variabl | 8 | global a # snapshot: invalid-syntax | ^ - | error[invalid-syntax]: name `a` cannot refer to a parameter and a global variable @@ -598,7 +588,6 @@ error[invalid-syntax]: name `a` cannot refer to a parameter and a global variabl | 16 | global a # snapshot: invalid-syntax | ^ - | error[invalid-syntax]: name `a` cannot refer to a parameter and a global variable @@ -606,7 +595,6 @@ error[invalid-syntax]: name `a` cannot refer to a parameter and a global variabl | 22 | global a # snapshot: invalid-syntax | ^ - | error[invalid-syntax]: name `a` cannot refer to a parameter and a global variable @@ -614,5 +602,4 @@ error[invalid-syntax]: name `a` cannot refer to a parameter and a global variabl | 27 | global a # snapshot: invalid-syntax | ^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md index c4a7889e68..688eefbc00 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/shadowing.md @@ -13,13 +13,12 @@ C = 1 # snapshot: invalid-assignment ```snapshot error[invalid-assignment]: Object of type `Literal[1]` is not assignable to `` - --> src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:3:5 | 3 | C = 1 # snapshot: invalid-assignment | - ^ Incompatible value of type `Literal[1]` | | | Declared type `` - | info: Implicit shadowing of class `C`. Add an annotation to make it explicit if this is intentional ``` @@ -33,13 +32,12 @@ f = 1 # snapshot: invalid-assignment ```snapshot error[invalid-assignment]: Object of type `Literal[1]` is not assignable to `def f() -> Unknown` - --> src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:3:5 | 3 | f = 1 # snapshot: invalid-assignment | - ^ Incompatible value of type `Literal[1]` | | | Declared type `def f() -> Unknown` - | info: Implicit shadowing of function `f`. Add an annotation to make it explicit if this is intentional ``` @@ -58,5 +56,4 @@ error[invalid-assignment]: Object of type `` is not assignable to a | 4 | config.optionxform = str # snapshot: invalid-assignment | ^^^^^^^^^^^^^^^^^^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md index b1bc18eb48..e2fa955410 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md @@ -40,13 +40,11 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 3 | f(1, 2, 3) # snapshot: too-many-positional-arguments | ^ - | info: Function signature here --> src/module.py:1:5 | 1 | def f(a, b=42): ... | ^^^^^^^^^^ - | error[too-many-positional-arguments]: Too many positional arguments to function `f`: expected 2, got 3 @@ -54,7 +52,6 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 12 | h(1, 2, 3) | ^ - | info: Union variant `def f(a, b=42) -> Unknown` is incompatible with this call site info: Attempted to call union type `(def f(a, b=42) -> Unknown) | (def g(a, b) -> Unknown)` @@ -64,7 +61,6 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 12 | h(1, 2, 3) | ^ - | info: Union variant `def g(a, b) -> Unknown` is incompatible with this call site info: Attempted to call union type `(def f(a, b=42) -> Unknown) | (def g(a, b) -> Unknown)` @@ -74,11 +70,9 @@ error[too-many-positional-arguments]: Too many positional arguments to bound met | 14 | Foo().method(1, 2) # snapshot: too-many-positional-arguments | ^ - | info: Method signature here --> src/module.py:5:9 | 5 | def method(self, a): ... | ^^^^^^^^^^^^^^^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md index 205aafbd60..c616be1686 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unpacking.md @@ -12,7 +12,6 @@ error[not-iterable]: Object of type `Literal[1]` is not iterable | 1 | a, b = 1 # snapshot: not-iterable | ^ - | info: It doesn't have an `__iter__` method or a `__getitem__` method ``` @@ -30,7 +29,6 @@ error[invalid-assignment]: Too many values to unpack | ^^^^ --------- Got 3 | | | Expected 2 - | ``` ## Exactly too few values to unpack @@ -47,7 +45,6 @@ error[invalid-assignment]: Not enough values to unpack | ^^^^ ---- Got 1 | | | Expected 2 - | ``` ## Too few values to unpack @@ -64,5 +61,4 @@ error[invalid-assignment]: Not enough values to unpack | ^^^^^^^^^^^^^ ------ Got 2 | | | Expected at least 3 - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md index a7c63452ab..61ed08ddb6 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md @@ -34,7 +34,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^-^ | | | Inferred type of argument is `Literal[0]` - | info: `Never` and `Literal[0]` are not equivalent types ``` @@ -51,7 +50,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^--^ | | | Inferred type of argument is `Literal[""]` - | info: `Never` and `Literal[""]` are not equivalent types ``` @@ -68,7 +66,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^----^ | | | Inferred type of argument is `None` - | info: `Never` and `None` are not equivalent types ``` @@ -85,7 +82,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^--^ | | | Inferred type of argument is `tuple[()]` - | info: `Never` and `tuple[()]` are not equivalent types ``` @@ -102,7 +98,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^--------------------^ | | | Inferred type of argument is `Literal[1]` - | info: `Never` and `Literal[1]` are not equivalent types ``` @@ -119,7 +114,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^----^ | | | Inferred type of argument is `Any` - | info: `Never` and `Any` are not equivalent types ``` @@ -136,7 +130,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Never` | ^^^^^^^^^^^^^-------^ | | | Inferred type of argument is `Unknown` - | info: `Never` and `Unknown` are not equivalent types ``` diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md index 2b43c89685..ae51bba28f 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md @@ -19,7 +19,6 @@ error[type-assertion-failure]: Argument does not have asserted type `str` | ^^^^^^^^^^^^-^^^^^^ | | | Inferred type is `int` - | info: `str` and `int` are not equivalent types ``` @@ -38,7 +37,6 @@ error[type-assertion-failure]: Argument does not have asserted type `int` | ^^^^^^^^^^^^-^^^^^^ | | | Inferred type is `bool` - | info: `bool` is a subtype of `int`, but they are not equivalent ``` @@ -106,7 +104,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Bar` | ^^^^^^^^^^^^-^^^^^^ | | | Inferred type is `Foo` - | info: `Bar` and `Foo` are not equivalent types ``` @@ -123,7 +120,6 @@ error[assert-type-unspellable-subtype]: Argument does not have asserted type `Ba | ^^^^^^^^^^^^-^^^^^^ | | | Inferred type is `Foo & Bar` - | info: `Foo & Bar` is a subtype of `Bar`, but they are not equivalent ``` @@ -141,7 +137,6 @@ error[type-assertion-failure]: Argument does not have asserted type `Baz` | ^^^^^^^^^^^^-^^^^^^ | | | Inferred type is `Foo & Bar` - | info: `Baz` and `Foo & Bar` are not equivalent types ``` diff --git a/crates/ty_python_semantic/resources/mdtest/directives/cast.md b/crates/ty_python_semantic/resources/mdtest/directives/cast.md index 3a1837f0b2..d4a148d009 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/cast.md @@ -113,7 +113,6 @@ warning[redundant-cast]: Value is already of type `int` | 5 | cast(int, secrets.randbelow(10)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the redundant `cast` | 4 | # snapshot: redundant-cast @@ -134,7 +133,6 @@ warning[redundant-cast]: Value is already of type `int` | 7 | cast(val=secrets.randbelow(10), typ=int) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the redundant `cast` | 6 | # snapshot: redundant-cast @@ -156,7 +154,6 @@ warning[redundant-cast]: Value is already of type `int` | 10 | return cast(int, x + y) * z | ^^^^^^^^^^^^^^^^ - | help: Remove the redundant `cast` | 9 | # snapshot: redundant-cast @@ -178,7 +175,6 @@ warning[redundant-cast]: Value is already of type `int` | 13 | return -cast(int, x + y) | ^^^^^^^^^^^^^^^^ - | help: Remove the redundant `cast` | 12 | # snapshot: redundant-cast @@ -200,7 +196,6 @@ warning[redundant-cast]: Value is already of type `int` | 16 | print(cast(int, x + y)) | ^^^^^^^^^^^^^^^^ - | help: Remove the redundant `cast` | 15 | # snapshot: redundant-cast diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 943162c595..17830ccbed 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -2452,7 +2452,6 @@ warning[mismatched-type-name]: The name passed to `Enum` must match the variable | 8 | Mismatch = Enum("WrongName", "A B") | ^^^^^^^^^^^ Expected "Mismatch", got "WrongName" - | ``` If the name is not a string literal, we also emit a diagnostic: @@ -2469,7 +2468,6 @@ warning[mismatched-type-name]: The name passed to `Enum` must match the variable | 11 | DynamicMismatch = Enum(name, "A B") | ^^^^ Expected "DynamicMismatch", got variable of type `str` - | ``` ### List/tuple of tuples diff --git a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md index 930cd8b0b3..6889b128e2 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md @@ -209,14 +209,13 @@ def invalid_generator() -> Generator[int, None, None]: ```snapshot error[invalid-yield]: Yield expression type does not match annotation - --> src/mdtest_snippet.py:3:28 + --> src/mdtest_snippet.py:5:11 | 3 | def invalid_generator() -> Generator[int, None, None]: | -------------------------- Function annotated with yield type `int` here 4 | # snapshot: invalid-yield 5 | yield "" | ^^ expression of type `Literal[""]`, expected `int` - | ``` ### Invalid annotation @@ -277,14 +276,13 @@ def outer() -> Generator[int, str, None]: ```snapshot error[invalid-yield]: Send type does not match annotation - --> src/mdtest_snippet.py:6:16 + --> src/mdtest_snippet.py:8:16 | 6 | def outer() -> Generator[int, str, None]: | ------------------------- Function annotated with send type `str` here 7 | # snapshot: invalid-yield 8 | yield from inner() | ^^^^^^^ generator with send type `int`, expected `str` - | ``` ### Non generator function with `Generator` annotation @@ -301,14 +299,13 @@ reveal_type(non_gen) # revealed: def non_gen() -> Generator[int, int, None] ```snapshot error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:3:18 + --> src/mdtest_snippet.py:5:12 | 3 | def non_gen() -> Generator[int, int, None]: | ------------------------- Expected `Generator[int, int, None]` because of return type 4 | # snapshot: invalid-return-type 5 | return 1 | ^ expected `Generator[int, int, None]`, found `Literal[1]` - | info: type `Literal[1]` is not assignable to protocol `Generator[int, int, None]` info: └── protocol member `__iter__` is not defined on type `Literal[1]` ``` 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 a75580cb52..308043c34a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -117,14 +117,13 @@ class ParamSpecOuterClass(Generic[P]): ```snapshot error[shadowed-type-variable]: Generic class `InnerClass` uses ParamSpec `P` already bound by an enclosing scope - --> src/mdtest_snippet.py:70:7 + --> src/mdtest_snippet.py:72:11 | 70 | class ParamSpecOuterClass(Generic[P]): | ------------------------------- ParamSpec `P` is bound in this enclosing scope 71 | # snapshot: shadowed-type-variable 72 | class InnerClass(SingleParamSpec[P]): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `P` used in class definition here - | ``` If you don't specialize a generic base class, we use the default specialization, which maps each diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index 5d603d3ab7..f4b677b8ca 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -229,13 +229,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 11 | reveal_type(f("string")) # revealed: Unknown | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound `int` of type variable `T` - | info: Type variable defined here --> src/mdtest_snippet.py:3:1 | 3 | T = TypeVar("T", bound=int) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` A bound can also be a union of protocols. If inference produces a union for the type variable, each @@ -282,13 +280,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 12 | reveal_type(f("string")) # revealed: Unknown | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints (`int`, `None`) of type variable `T` - | info: Type variable defined here --> src/mdtest_snippet.py:3:1 | 3 | T = TypeVar("T", int, None) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ## Typevar constraints diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md index 20ef991b51..6be22a8e11 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -696,7 +696,6 @@ error[invalid-legacy-type-variable]: A `TypeVar` cannot specify variance when `i | 48 | CovariantAndInferred = TypeVar("CovariantAndInferred", covariant=True, infer_variance=True) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ### Boolean parameters must be unambiguous diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md index 0407d2af21..a047ae784c 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md @@ -365,7 +365,6 @@ error[invalid-generic-class]: Variance of type variable `T_co` is incompatible w | 18 | class BadInvariantCo(Invariant[T_co]): ... | ^^^^^^^^^^^^^^^ - | help: Type variable `T_co` is declared as covariant, but base class `Invariant` requires it to be invariant @@ -374,7 +373,6 @@ error[invalid-generic-class]: Variance of type variable `T_contra` is incompatib | 21 | class BadInvariantContra(Invariant[T_contra]): ... | ^^^^^^^^^^^^^^^^^^^ - | help: Type variable `T_contra` is declared as contravariant, but base class `Invariant` requires it to be invariant @@ -383,7 +381,6 @@ error[invalid-generic-class]: Variance of type variable `T_contra` is incompatib | 24 | class BadCovariant(Covariant[T_contra]): ... | ^^^^^^^^^^^^^^^^^^^ - | help: Type variable `T_contra` is declared as contravariant, but base class `Covariant` requires it to be covariant @@ -392,7 +389,6 @@ error[invalid-generic-class]: Variance of type variable `T_co` is incompatible w | 27 | class BadContravariant(Contravariant[T_co]): ... | ^^^^^^^^^^^^^^^^^^^ - | help: Type variable `T_co` is declared as covariant, but base class `Contravariant` requires it to be contravariant ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index 169ebff7d7..e6abd346ab 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -372,7 +372,6 @@ error[not-subscriptable]: Cannot specialize non-generic type alias `AliasA` | ------^^^^^ | | | Alias to `A`, which is not generic - | ``` ```py @@ -388,7 +387,6 @@ error[not-subscriptable]: Cannot specialize non-generic type alias `AliasB` | ------^^^^^ | | | Alias to `B[int]`, which is already specialized - | ``` ## Aliases are not callable @@ -706,13 +704,12 @@ type Alias1[*Ts, T = int] = tuple[*Ts, T] ```snapshot error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:2:13 + --> src/mdtest_snippet.py:2:18 | 2 | type Alias1[*Ts, T = int] = tuple[*Ts, T] | --- ^^^^^^^ `T` has a default | | | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` @@ -723,13 +720,12 @@ type Alias2[T1, *Ts, T2 = int] = tuple[T1, *Ts, T2] ```snapshot error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:4:17 + --> src/mdtest_snippet.py:4:22 | 4 | type Alias2[T1, *Ts, T2 = int] = tuple[T1, *Ts, T2] | --- ^^^^^^^^ `T2` has a default | | | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` @@ -740,14 +736,13 @@ type Alias3[*Ts, T1 = int, T2 = str] = tuple[*Ts, T1, T2] ```snapshot error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:6:13 + --> src/mdtest_snippet.py:6:18 | 6 | type Alias3[*Ts, T1 = int, T2 = str] = tuple[*Ts, T1, T2] | --- ^^^^^^^^ -------- `T2` also has a default | | | | | `T1` has a default | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` @@ -760,13 +755,12 @@ type Alias4[*Us, *Ts = *tuple[int, str]] = tuple[*Us, *Ts] ```snapshot error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:10:13 + --> src/mdtest_snippet.py:10:18 | 10 | type Alias4[*Us, *Ts = *tuple[int, str]] = tuple[*Us, *Ts] | --- ^^^^^^^^^^^^^^^^^^^^^^ `Ts` has a default | | | `Us` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 3b34f8ed4b..17f86e4402 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -271,13 +271,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 9 | reveal_type(f("string")) # revealed: Unknown | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound `int` of type variable `T` - | info: Type variable defined here --> src/mdtest_snippet.py:3:7 | 3 | def f[T: int](x: T) -> T: | ^^^^^^ - | ``` ## Inferring a constrained typevar @@ -301,13 +299,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 10 | reveal_type(f("string")) # revealed: Unknown | ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints (`int`, `None`) of type variable `T` - | info: Type variable defined here --> src/mdtest_snippet.py:3:7 | 3 | def f[T: (int, None)](x: T) -> T: | ^^^^^^^^^^^^^^ - | ``` ## Typevar constraints diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md index 814dc4af70..927cd71d32 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -310,7 +310,6 @@ error[shadowed-type-variable]: Generic function `bad` uses TypeVarTuple `Ts` alr | 1 | def outer[*Ts](*args: *Ts) -> None: | ------------------------------ TypeVarTuple `Ts` is bound in this enclosing scope - | ``` ### Generic method within generic class diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 6170ff29d5..5a714aaafc 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -881,7 +881,6 @@ error[not-subscriptable]: Cannot subscript non-generic type alias `ListOfInts2` | -----------^^^^^ | | | Alias to `list[int]`, which is already specialized - | ``` ```py @@ -899,7 +898,6 @@ error[not-subscriptable]: Cannot subscript non-generic type `` | ---------^^^^^ | | | Type is already specialized - | ``` ### Multiple definitions diff --git a/crates/ty_python_semantic/resources/mdtest/liskov.md b/crates/ty_python_semantic/resources/mdtest/liskov.md index 395b671083..68be19b918 100644 --- a/crates/ty_python_semantic/resources/mdtest/liskov.md +++ b/crates/ty_python_semantic/resources/mdtest/liskov.md @@ -53,7 +53,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self) -> int: ... | ------------------- `Super.method` defined here - | info: incompatible return types: `object` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -76,7 +75,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self) -> int: ... | ------------------- `Super.method` defined here - | info: incompatible return types: `str` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -171,7 +169,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: int, /): ... | ----------------------- `Super.method` defined here - | info: This violates the Liskov Substitution Principle ``` @@ -193,7 +190,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: int, /): ... | ----------------------- `Super.method` defined here - | info: unexpected extra parameter `y` info: This violates the Liskov Substitution Principle ``` @@ -216,7 +212,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: int, /): ... | ----------------------- `Super.method` defined here - | info: parameter `x` is keyword-only but must also accept positional arguments info: This violates the Liskov Substitution Principle ``` @@ -239,7 +234,6 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: int, /): ... | ----------------------- `Super.method` defined here - | info: parameter `x` has an incompatible type: `int` is not assignable to `bool` info: This violates the Liskov Substitution Principle ``` @@ -256,7 +250,7 @@ class Sub16(Super2): ```snapshot error[invalid-method-override]: Invalid override of method `method2` - --> src/mdtest_snippet.pyi:43:9 + --> src/mdtest_snippet.pyi:46:9 | 43 | def method2(self, x): ... | ---------------- `Super2.method2` defined here @@ -264,7 +258,6 @@ error[invalid-method-override]: Invalid override of method `method2` 45 | class Sub16(Super2): 46 | def method2(self, x, /): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super2.method2` - | info: parameter `x` is positional-only but must also accept keyword arguments info: This violates the Liskov Substitution Principle ``` @@ -278,7 +271,7 @@ class Sub17(Super2): ```snapshot error[invalid-method-override]: Invalid override of method `method2` - --> src/mdtest_snippet.pyi:43:9 + --> src/mdtest_snippet.pyi:48:9 | 43 | def method2(self, x): ... | ---------------- `Super2.method2` defined here @@ -288,7 +281,6 @@ error[invalid-method-override]: Invalid override of method `method2` 47 | class Sub17(Super2): 48 | def method2(self, *, x): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super2.method2` - | info: parameter `x` is keyword-only but must also accept positional arguments info: This violates the Liskov Substitution Principle ``` @@ -312,7 +304,7 @@ class Sub19(Super3): ```snapshot error[invalid-method-override]: Invalid override of method `method3` - --> src/mdtest_snippet.pyi:50:9 + --> src/mdtest_snippet.pyi:55:9 | 50 | def method3(self, *, x): ... | ------------------- `Super3.method3` defined here @@ -322,7 +314,6 @@ error[invalid-method-override]: Invalid override of method `method3` 54 | class Sub19(Super3): 55 | def method3(self, x, /): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super3.method3` - | info: This violates the Liskov Substitution Principle ``` @@ -345,7 +336,7 @@ class Sub21(Super4): ```snapshot error[invalid-method-override]: Invalid override of method `method` - --> src/mdtest_snippet.pyi:57:9 + --> src/mdtest_snippet.pyi:62:9 | 57 | def method(self, *args: int, **kwargs: str): ... | --------------------------------------- `Super4.method` defined here @@ -355,7 +346,6 @@ error[invalid-method-override]: Invalid override of method `method` 61 | class Sub21(Super4): 62 | def method(self, *args): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super4.method` - | info: This violates the Liskov Substitution Principle ``` @@ -377,7 +367,6 @@ error[invalid-method-override]: Invalid override of method `method` | 57 | def method(self, *args: int, **kwargs: str): ... | --------------------------------------- `Super4.method` defined here - | info: This violates the Liskov Substitution Principle ``` @@ -661,7 +650,7 @@ class Compatible(ReturnsBool, ReturnsInt): ... ```snapshot error[invalid-method-override]: Base classes for class `BasicConflict` define method `method` incompatibly - --> src/mdtest_snippet.pyi:2:9 + --> src/mdtest_snippet.pyi:10:7 | 2 | def method(self) -> str: ... | ------ `ReturnsStr.method` defined here @@ -675,7 +664,6 @@ error[invalid-method-override]: Base classes for class `BasicConflict` define me 9 | 10 | class BasicConflict(ReturnsStr, ReturnsInt): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `ReturnsStr.method` is incompatible with `ReturnsInt.method` - | info: incompatible return types: `str` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -914,7 +902,7 @@ class StaticClassConflict(StaticMethod, ClassMethod): ... # error: [invalid-met ```snapshot error[invalid-method-override]: Base classes for class `ClassInstanceConflict` define method `kind` incompatibly - --> src/mdtest_snippet.pyi:10:9 + --> src/mdtest_snippet.pyi:13:7 | 10 | def kind(cls, value: int) -> int: ... | ---- `ClassMethod.kind` defined here @@ -927,7 +915,6 @@ error[invalid-method-override]: Base classes for class `ClassInstanceConflict` d | 2 | def kind(self, value: int) -> int: ... | ---- `InstanceMethod.kind` defined here - | info: `ClassMethod.kind` is a classmethod but `InstanceMethod.kind` is an instance method info: This violates the Liskov Substitution Principle ``` @@ -1068,7 +1055,6 @@ error[invalid-method-override]: Base classes for class `Combined` define method | 2 | def method(self) -> int: ... | ------ `right.Base.method` defined here - | info: incompatible return types: `str` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -1156,7 +1142,7 @@ class ThirdChild(GradualParent): ```snapshot error[invalid-method-override]: Invalid override of method `method` - --> src/stub.pyi:4:9 + --> src/stub.pyi:7:9 | 4 | def method(self, x: int) -> None: ... | ---------------------------- `Grandparent.method` defined here @@ -1164,7 +1150,6 @@ error[invalid-method-override]: Invalid override of method `method` 6 | class Parent(Grandparent): 7 | def method(self, x: str) -> None: ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Grandparent.method` - | info: parameter `x` has an incompatible type: `int` is not assignable to `str` info: This violates the Liskov Substitution Principle @@ -1179,7 +1164,6 @@ error[invalid-method-override]: Invalid override of method `method` | 7 | def method(self, x: str) -> None: ... # snapshot: invalid-method-override | ---------------------------- `Parent.method` defined here - | info: parameter `x` has an incompatible type: `str` is not assignable to `int` info: This violates the Liskov Substitution Principle @@ -1194,13 +1178,12 @@ error[invalid-method-override]: Invalid override of method `method` | 7 | def method(self, x: str) -> None: ... # snapshot: invalid-method-override | ---------------------------- `Parent.method` defined here - | info: parameter `x` has an incompatible type: `str` is not assignable to `bytes` info: This violates the Liskov Substitution Principle error[invalid-method-override]: Invalid override of method `method` - --> src/stub.pyi:25:9 + --> src/stub.pyi:28:9 | 25 | def method(self) -> int: ... | ------------------- `GrandparentWithReturnType.method` defined here @@ -1208,13 +1191,12 @@ error[invalid-method-override]: Invalid override of method `method` 27 | class ParentWithReturnType(GrandparentWithReturnType): 28 | def method(self) -> str: ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `GrandparentWithReturnType.method` - | info: incompatible return types: `str` is not assignable to `int` info: This violates the Liskov Substitution Principle error[invalid-method-override]: Invalid override of method `method` - --> src/stub.pyi:28:9 + --> src/stub.pyi:33:9 | 28 | def method(self) -> str: ... # snapshot: invalid-method-override | ------------------- `ParentWithReturnType.method` defined here @@ -1224,7 +1206,6 @@ error[invalid-method-override]: Invalid override of method `method` 32 | # but not with `ParentWithReturnType.method`. We report against the immediate parent. 33 | def method(self) -> int: ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `ParentWithReturnType.method` - | info: incompatible return types: `int` is not assignable to `str` info: This violates the Liskov Substitution Principle @@ -1239,7 +1220,6 @@ error[invalid-method-override]: Invalid override of method `method` | 4 | def method(self, x: int) -> None: ... | ---------------------------- `Grandparent.method` defined here - | info: parameter `x` has an incompatible type: `int` is not assignable to `str` info: This violates the Liskov Substitution Principle ``` @@ -1268,7 +1248,7 @@ class D(C): ```snapshot error[invalid-method-override]: Invalid override of method `get` - --> src/other_stub.pyi:2:9 + --> src/other_stub.pyi:5:9 | 2 | def get(self, default): ... | ------------------ `A.get` defined here @@ -1276,7 +1256,6 @@ error[invalid-method-override]: Invalid override of method `get` 4 | class B(A): 5 | def get(self, default, /): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `A.get` - | info: parameter `default` is positional-only but must also accept keyword arguments info: This violates the Liskov Substitution Principle ``` @@ -1477,7 +1456,6 @@ error[invalid-method-override]: Invalid override of method `method` | 7 | def method(self: HasValue, argument: int) -> None: ... | --------------------------------------------- `Mixin.method` defined here - | info: parameter `argument` has an incompatible type: `int` is not assignable to `str` info: This violates the Liskov Substitution Principle ``` @@ -1613,7 +1591,6 @@ error[invalid-method-override]: Invalid override of method `foo` | 2 | def foo(self, x): ... | ------------ `one.A.foo` defined here - | info: the parameter named `y` does not match `x` (and can be used as a keyword parameter) info: This violates the Liskov Substitution Principle ``` @@ -1706,7 +1683,7 @@ class D(C): ```snapshot error[invalid-method-override]: Invalid override of method `x` - --> src/bar.pyi:4:9 + --> src/bar.pyi:7:5 | 4 | def x(self, y: int): ... | --------------- `A.x` defined here @@ -1719,13 +1696,12 @@ error[invalid-method-override]: Invalid override of method `x` | 1 | def x(self, y: str): ... | --------------- Signature of `B.x` - | info: parameter `y` has an incompatible type: `int` is not assignable to `str` info: This violates the Liskov Substitution Principle error[invalid-method-override]: Invalid override of method `x` - --> src/bar.pyi:10:5 + --> src/bar.pyi:13:9 | 10 | x = foo.x | --------- `C.x` defined here @@ -1738,7 +1714,6 @@ error[invalid-method-override]: Invalid override of method `x` | 1 | def x(self, y: str): ... | --------------- Signature of `C.x` - | info: parameter `y` has an incompatible type: `str` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -1763,16 +1738,15 @@ error[invalid-method-override]: Invalid override of method `__eq__` | 136 | def __eq__(self, value: object, /) -> bool: ... | -------------------------------------- `object.__eq__` defined here - | info: parameter `value` has an incompatible type: `object` is not assignable to `Bad` info: This violates the Liskov Substitution Principle help: It is recommended for `__eq__` to work with arbitrary objects, for example: -help +help: help: def __eq__(self, other: object) -> bool: help: if not isinstance(other, Bad): help: return False help: return -help +help: ``` ## Class-private names do not override @@ -1847,7 +1821,6 @@ error[invalid-method-override]: Invalid override of method `_asdict` | 41 | def _asdict(self) -> tuple[int, ...]: ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Baz._asdict` - | info: incompatible return types: `tuple[int, ...]` is not assignable to `dict[str, Any]` info: This violates the Liskov Substitution Principle info: `Baz._asdict` is a generated method created because `Baz` inherits from `typing.NamedTuple` @@ -1855,7 +1828,6 @@ info: `Baz._asdict` is a generated method created because `Baz` inherits from `t | 37 | class Baz(NamedTuple): | ^^^^^^^^^^^^^^^ Definition of `Baz` - | ``` ## Staticmethods and classmethods @@ -1903,7 +1875,6 @@ error[invalid-method-override]: Invalid override of method `class_method` | 4 | def class_method(cls, x: int) -> int: ... | -------------------------------- `Parent.class_method` defined here - | info: incompatible return types: `object` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -1925,7 +1896,6 @@ error[invalid-method-override]: Invalid override of method `static_method` | 6 | def static_method(x: int) -> int: ... | ---------------------------- `Parent.static_method` defined here - | info: incompatible return types: `object` is not assignable to `int` info: This violates the Liskov Substitution Principle ``` @@ -1949,7 +1919,6 @@ error[invalid-method-override]: Invalid override of method `instance_method` | 2 | def instance_method(self, x: int) -> int: ... | ------------------------------------ `Parent.instance_method` defined here - | info: `BadChild1A.instance_method` is a staticmethod but `Parent.instance_method` is an instance method info: This violates the Liskov Substitution Principle ``` @@ -1970,7 +1939,6 @@ error[invalid-method-override]: Invalid override of method `static_method` | 6 | def static_method(x: int) -> int: ... | ---------------------------- `Parent.static_method` defined here - | info: `BadChild1B.static_method` is an instance method but `Parent.static_method` is a staticmethod info: This violates the Liskov Substitution Principle ``` @@ -2019,7 +1987,6 @@ error[invalid-method-override]: Invalid override of method `class_method` | 4 | def class_method(cls, x: int) -> int: ... | -------------------------------- `Parent.class_method` defined here - | info: `BadChild3A.class_method` is a staticmethod but `Parent.class_method` is a classmethod info: This violates the Liskov Substitution Principle ``` @@ -2041,7 +2008,6 @@ error[invalid-method-override]: Invalid override of method `static_method` | 6 | def static_method(x: int) -> int: ... | ---------------------------- `Parent.static_method` defined here - | info: `BadChild3B.static_method` is a classmethod but `Parent.static_method` is a staticmethod info: This violates the Liskov Substitution Principle ``` diff --git a/crates/ty_python_semantic/resources/mdtest/loops/async_for.md b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md index 22ae4e977e..df30433d40 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/async_for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md @@ -80,7 +80,6 @@ error[not-iterable]: Object of type `NotAsyncIterable` is not async-iterable | 5 | async for x in NotAsyncIterable(): | ^^^^^^^^^^^^^^^^^^ - | info: It has no `__aiter__` method ``` @@ -107,7 +106,6 @@ error[not-iterable]: Object of type `Iterator` is not async-iterable | 11 | async for x in Iterator(): | ^^^^^^^^^^ - | info: It has no `__aiter__` method ``` @@ -132,7 +130,6 @@ error[not-iterable]: Object of type `AsyncIterable` is not async-iterable | 9 | async for x in AsyncIterable(): | ^^^^^^^^^^^^^^^ - | info: Its `__aiter__` method returns an object of type `NoAnext`, which has no `__anext__` method ``` @@ -160,7 +157,6 @@ error[not-iterable]: Object of type `AsyncIterable` may not be async-iterable | 12 | async for x in AsyncIterable(): | ^^^^^^^^^^^^^^^ - | info: Its `__aiter__` method returns an object of type `PossiblyUnboundAnext`, which may not have a `__anext__` method info: type `AsyncIterable` is not assignable to protocol `AsyncIterable[Unknown]` info: └── protocol member `__aiter__` is incompatible @@ -193,7 +189,6 @@ error[not-iterable]: Object of type `PossiblyUnboundAiter` may not be async-iter | 12 | async for x in PossiblyUnboundAiter(): | ^^^^^^^^^^^^^^^^^^^^^^ - | info: Its `__aiter__` attribute (with type `bound method PossiblyUnboundAiter.__aiter__() -> AsyncIterable`) may not be callable ``` @@ -220,7 +215,6 @@ error[not-iterable]: Object of type `AsyncIterable` is not async-iterable | 11 | async for x in AsyncIterable(): | ^^^^^^^^^^^^^^^ - | info: Its `__aiter__` method has an invalid signature info: type `AsyncIterable` is not assignable to protocol `AsyncIterable[Unknown]` info: └── protocol member `__aiter__` is incompatible @@ -251,7 +245,6 @@ error[not-iterable]: Object of type `AsyncIterable` is not async-iterable | 11 | async for x in AsyncIterable(): | ^^^^^^^^^^^^^^^ - | info: Its `__aiter__` method returns an object of type `AsyncIterator`, which has an invalid `__anext__` method info: type `AsyncIterable` is not assignable to protocol `AsyncIterable[Unknown]` info: └── protocol member `__aiter__` is incompatible diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index ec2d673fdf..2a4e3f7fa7 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -348,7 +348,6 @@ error[not-iterable]: Object of type `NotIterable` is not iterable | 9 | for x in NotIterable(): | ^^^^^^^^^^^^^ - | info: Its `__iter__` attribute has type `int | None`, which is not callable ``` @@ -366,7 +365,6 @@ error[not-iterable]: Object of type `Literal[123]` is not iterable | 2 | for x in nonsense: # snapshot: not-iterable | ^^^^^^^^ - | info: It doesn't have an `__iter__` method or a `__getitem__` method ``` @@ -388,7 +386,6 @@ error[not-iterable]: Object of type `NotIterable` is not iterable | 6 | for x in NotIterable(): # snapshot: not-iterable | ^^^^^^^^^^^^^ - | info: Its `__iter__` attribute has type `None`, which is not callable ``` @@ -577,7 +574,6 @@ error[not-iterable]: Object of type `Test | Literal[42]` may not be iterable | 13 | for x in iterable: | ^^^^^^^^ - | info: It may not have an `__iter__` method and it doesn't have a `__getitem__` method info: `Literal[42]` does not implement `__iter__` ``` @@ -610,7 +606,6 @@ error[not-iterable]: Object of type `Test | Test2` may not be iterable | 16 | for x in iterable: | ^^^^^^^^ - | info: Its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method info: element `Test2` of union `Test | Test2` is not assignable to `Iterable[Unknown]` info: └── type `Test2` is not assignable to protocol `Iterable[Unknown]` @@ -651,7 +646,6 @@ error[not-iterable]: Object of type `Test | NotIter` may not be iterable | 15 | for x in iterable: | ^^^^^^^^ - | info: Its `__iter__` attribute (with type `(bound method Test.__iter__() -> TestIter) | int`) may not be callable ``` @@ -894,7 +888,6 @@ error[not-iterable]: Object of type `Iterable` is not iterable | 10 | for x in Iterable(): | ^^^^^^^^^^ - | info: Its `__iter__` method has an invalid signature info: type `Iterable` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible @@ -920,7 +913,6 @@ error[not-iterable]: Object of type `Bad` is not iterable | 6 | for x in Bad(): | ^^^^^ - | info: Its `__iter__` method returns an object of type `int`, which has no `__next__` method ``` @@ -971,7 +963,6 @@ error[not-iterable]: Object of type `Iterable1` is not iterable | 17 | for x in Iterable1(): | ^^^^^^^^^^^ - | info: Its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method info: type `Iterable1` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible @@ -994,7 +985,6 @@ error[not-iterable]: Object of type `Iterable2` is not iterable | 20 | for y in Iterable2(): | ^^^^^^^^^^^ - | info: Its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable ``` @@ -1026,7 +1016,6 @@ error[not-iterable]: Object of type `Iterable` may not be iterable | 16 | for x in Iterable(): | ^^^^^^^^^^ - | 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 ``` @@ -1086,7 +1075,6 @@ error[not-iterable]: Object of type `Iterable` may not be iterable | 15 | for x in Iterable(): | ^^^^^^^^^^ - | info: It may not have an `__iter__` method or a `__getitem__` method ``` @@ -1107,7 +1095,6 @@ error[not-iterable]: Object of type `Bad` is not iterable | 5 | for x in Bad(): | ^^^^^ - | info: It has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable ``` @@ -1146,7 +1133,6 @@ error[not-iterable]: Object of type `Iterable1` may not be iterable | 22 | for x in Iterable1(): | ^^^^^^^^^^^ - | info: It has no `__iter__` method and its `__getitem__` attribute is invalid info: `__getitem__` has type `CustomCallable`, which is not callable ``` @@ -1164,7 +1150,6 @@ error[not-iterable]: Object of type `Iterable2` may not be iterable | 26 | for y in Iterable2(): | ^^^^^^^^^^^ - | 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 ``` @@ -1189,7 +1174,6 @@ error[not-iterable]: Object of type `Iterable` is not iterable | 8 | for x in Iterable(): | ^^^^^^^^^^ - | 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 ``` @@ -1245,7 +1229,6 @@ error[not-iterable]: Object of type `Iterable1` may not be iterable | 16 | for x in Iterable1(): | ^^^^^^^^^^^ - | info: Its `__iter__` method may have an invalid signature info: type `Iterable1` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible @@ -1275,7 +1258,6 @@ error[not-iterable]: Object of type `Iterable2` may not be iterable | 27 | for x in Iterable2(): | ^^^^^^^^^^^ - | info: Its `__iter__` attribute (with type `(bound method Iterable2.__iter__() -> Iterator) | None`) may not be callable ``` @@ -1319,7 +1301,6 @@ error[not-iterable]: Object of type `Iterable1` may not be iterable | 28 | for x in Iterable1(): | ^^^^^^^^^^^ - | info: Its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method info: type `Iterable1` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible @@ -1343,7 +1324,6 @@ error[not-iterable]: Object of type `Iterable2` may not be iterable | 31 | for y in Iterable2(): | ^^^^^^^^^^^ - | info: Its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable info: type `Iterable2` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible @@ -1385,7 +1365,6 @@ error[not-iterable]: Object of type `Iterable1` may not be iterable | 20 | for x in Iterable1(): | ^^^^^^^^^^^ - | 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 ``` @@ -1402,7 +1381,6 @@ error[not-iterable]: Object of type `Iterable2` may not be iterable | 24 | for y in Iterable2(): | ^^^^^^^^^^^ - | 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 ``` @@ -1451,7 +1429,6 @@ error[not-iterable]: Object of type `Iterable1` may not be iterable | 31 | for x in Iterable1(): | ^^^^^^^^^^^ - | 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 ``` @@ -1467,7 +1444,6 @@ error[not-iterable]: Object of type `Iterable2` may not be iterable | 35 | for y in Iterable2(): | ^^^^^^^^^^^ - | 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/ty_python_semantic/resources/mdtest/metaclass.md b/crates/ty_python_semantic/resources/mdtest/metaclass.md index 9cdb4bb03a..254771c4d2 100644 --- a/crates/ty_python_semantic/resources/mdtest/metaclass.md +++ b/crates/ty_python_semantic/resources/mdtest/metaclass.md @@ -787,7 +787,6 @@ error[invalid-metaclass]: Metaclass type `int` is not callable | 3 | class B(metaclass=n): | ^^^^^^^^^^^ - | ``` ## Cyclic diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 0777762912..bf3f23c2b2 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -125,7 +125,6 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` | ^^^^^^^^^^^^^^---------------^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects info: Element `` in the union is not a class object ``` @@ -144,7 +143,6 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` | ^^^^^^^^^^^^^^-------------------------------^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects info: Elements `` and `` in the union are not class objects ``` @@ -163,7 +161,6 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` | ^^^^^^^^^^^^^^----------------------------^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects info: Element `` in the union, and 2 more elements, are not class objects ``` @@ -192,7 +189,6 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` | ^^^^^^^^^^^^^^^^^^^^-----------------^^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects info: Element `` in the union is not a class object ``` @@ -216,7 +212,6 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` | ^^^^^^^^^^^^^^^^^^^^^^^^^^-----------------^^^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects info: Element `` in the union is not a class object ``` @@ -240,7 +235,6 @@ error[invalid-argument-type]: Invalid second argument to `isinstance` | 31 | if isinstance(x, classes): | ^^^^^^^^^^^^^^^^^^^^^^ - | info: A `UnionType` instance can only be used as the second argument to `isinstance` if all elements are class objects info: Element `` in the union `list[int] | bytes` is not a class object ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md index 31e8513940..3209b9fdbf 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md @@ -187,7 +187,6 @@ error[invalid-argument-type]: Invalid second argument to `issubclass` | ^^^^^^^^^^^^^^---------------^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects info: Element `` in the union is not a class object ``` @@ -211,7 +210,6 @@ error[invalid-argument-type]: Invalid second argument to `issubclass` | ^^^^^^^^^^^^^^^^^^^^-----------------^^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects info: Element `` in the union is not a class object ``` @@ -235,7 +233,6 @@ error[invalid-argument-type]: Invalid second argument to `issubclass` | ^^^^^^^^^^^^^^^^^^^^^^^^^^-----------------^^^ | | | This `UnionType` instance contains non-class elements - | info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects info: Element `` in the union is not a class object ``` @@ -259,7 +256,6 @@ error[invalid-argument-type]: Invalid second argument to `issubclass` | 23 | if issubclass(x, classes): | ^^^^^^^^^^^^^^^^^^^^^^ - | info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects info: Element `` in the union `list[int] | bytes` is not a class object ``` diff --git a/crates/ty_python_semantic/resources/mdtest/notebook.md b/crates/ty_python_semantic/resources/mdtest/notebook.md index 7d9f840af0..cd3dcee5cd 100644 --- a/crates/ty_python_semantic/resources/mdtest/notebook.md +++ b/crates/ty_python_semantic/resources/mdtest/notebook.md @@ -40,5 +40,4 @@ error[invalid-syntax]: Expected class, function definition or async function def | 2 | @staticmethod | ^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index 52630e8662..ec4e76c179 100644 --- a/crates/ty_python_semantic/resources/mdtest/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -1242,7 +1242,6 @@ error[invalid-overload]: Implementation does not accept all arguments of this ov | ^^^^^^^^ 10 | def _extract(self, row_key: int | None = None, column_key: int | None = None) -> object: | -------- Implementation defined here - | info: Implementation signature `(self, row_key: int | None = None, column_key: int | None = None) -> object` is not assignable to overload signature `(self, column_key: int) -> object` info: the parameter named `row_key` does not match `column_key` (and can be used as a keyword parameter) @@ -1254,7 +1253,6 @@ error[invalid-overload]: Implementation does not accept all arguments of this ov | ^^^^^^ 19 | def update(self, params=(), /, **kwds) -> None: | ------ Implementation defined here - | info: Implementation signature `(self, params=..., /, **kwds) -> None` is not assignable to overload signature `(self, **kwds: Iterable[str]) -> None` info: parameter `self` is positional-only but must also accept keyword arguments ``` @@ -1291,7 +1289,6 @@ error[invalid-overload]: Overload return type is not assignable to implementatio 7 | def return_tuple(x: str) -> tuple[int]: ... 8 | def return_tuple(x: int | str) -> tuple[int]: | ------------ Implementation defined here - | info: Overload returns `tuple[str]`, which is not assignable to implementation return type `tuple[int]` info: the first tuple element is not compatible: `str` is not assignable to `int` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/override.md b/crates/ty_python_semantic/resources/mdtest/override.md index 6ba91b5c4a..4ac37e9501 100644 --- a/crates/ty_python_semantic/resources/mdtest/override.md +++ b/crates/ty_python_semantic/resources/mdtest/override.md @@ -584,7 +584,7 @@ class ExplicitChild(Parent): ```snapshot error[missing-override-decorator]: Method `method` overrides `Parent.method` but is not decorated with `@override` - --> src/mdtest_snippet.py:4:9 + --> src/mdtest_snippet.py:7:9 | 4 | def method(self) -> None: ... | ------ `Parent.method` defined here @@ -592,7 +592,6 @@ error[missing-override-decorator]: Method `method` overrides `Parent.method` but 6 | class Child(Parent): 7 | def method(self) -> None: ... # snapshot: missing-override-decorator | ^^^^^^ - | info: Decorate the method with `@typing_extensions.override` to make the override explicit ``` @@ -622,7 +621,7 @@ class ExplicitChild(Parent): ```snapshot error[missing-override-decorator]: Method `method` overrides `Parent.method` but is not decorated with `@override` - --> src/mdtest_snippet.py:4:9 + --> src/mdtest_snippet.py:7:9 | 4 | def method(self) -> None: ... | ------ `Parent.method` defined here @@ -630,7 +629,6 @@ error[missing-override-decorator]: Method `method` overrides `Parent.method` but 6 | class Child(Parent): 7 | def method(self) -> None: ... # snapshot: missing-override-decorator | ^^^^^^ - | info: Decorate the method with `@typing.override` to make the override explicit ``` diff --git a/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md b/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md index f5b35cc3f6..284d97e487 100644 --- a/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md +++ b/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md @@ -30,13 +30,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 9 | foo(fn1, "a", 2, c="c", unknown=1) | ^^^ Expected `int`, found `Literal["a"]` - | info: Function defined here --> src/mdtest_snippet.py:3:5 | 3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... | ^^^ ------------------ Parameter declared here - | error[invalid-argument-type]: Argument to function `foo` is incorrect @@ -44,13 +42,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 9 | foo(fn1, "a", 2, c="c", unknown=1) | ^^^^^ Expected `int`, found `Literal["c"]` - | info: Function defined here --> src/mdtest_snippet.py:3:5 | 3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... | ^^^ ------------------ Parameter declared here - | error[unknown-argument]: Argument `unknown` does not match any known parameter of function `foo` @@ -58,13 +54,11 @@ error[unknown-argument]: Argument `unknown` does not match any known parameter o | 9 | foo(fn1, "a", 2, c="c", unknown=1) | ^^^^^^^^^ - | info: Function signature here --> src/mdtest_snippet.py:3:5 | 3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ```py @@ -80,13 +74,11 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 13 | foo(fn2, 1, 2, 3) | ^ - | info: Function signature here --> src/mdtest_snippet.py:3:5 | 3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ```py @@ -102,13 +94,11 @@ error[positional-only-parameter-as-kwarg]: Positional-only parameter 1 (`a`) pas | 17 | foo(fn3, a=1) | ^^^ - | info: Function signature here --> src/mdtest_snippet.py:3:5 | 3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ```py @@ -128,13 +118,11 @@ error[missing-argument]: No argument provided for required parameter `b` of func | 22 | foo(fn4, 1, a=2) | ^^^^^^^^^^^^^^^^ - | info: Parameter declared here --> src/mdtest_snippet.py:3:37 | 3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... | ^^^^^^^^^^^^^ - | error[parameter-already-assigned]: Multiple values provided for parameter `a` of function `foo` @@ -142,7 +130,6 @@ error[parameter-already-assigned]: Multiple values provided for parameter `a` of | 22 | foo(fn4, 1, a=2) | ^^^ - | error[missing-argument]: No arguments provided for required parameters `a`, `b` of function `foo` @@ -150,13 +137,11 @@ error[missing-argument]: No arguments provided for required parameters `a`, `b` | 25 | foo(fn4) | ^^^^^^^^ - | info: Parameters declared here --> src/mdtest_snippet.py:3:16 | 3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` ## Methods diff --git a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md index acb2b5726d..b7bc652d2f 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep613_type_aliases.md @@ -533,7 +533,6 @@ error[invalid-type-form]: `Unpack` is not allowed in type alias values | 14 | differently_bad: TypeAlias = Unpack[tuple[int, ...]] # snapshot: invalid-type-form | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions ``` diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index bdb917e8c6..882ceeb661 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -261,7 +261,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `` | Has type `Literal["int"]` - | info: A type alias scope is lazy but will be executed at runtime if the `__value__` property is accessed ``` diff --git a/crates/ty_python_semantic/resources/mdtest/properties.md b/crates/ty_python_semantic/resources/mdtest/properties.md index 44afc2c502..93edff1d38 100644 --- a/crates/ty_python_semantic/resources/mdtest/properties.md +++ b/crates/ty_python_semantic/resources/mdtest/properties.md @@ -313,7 +313,6 @@ error[invalid-assignment]: Cannot delete read-only property `attr` on object of | 3 | def attr(self) -> int: | ---- Property `C.attr` defined here with no deleter - | ``` ## Limitations @@ -459,13 +458,11 @@ error[invalid-argument-type]: Argument to function `C.attr` is incorrect | 31 | type(attr_property).__set__(attr_property, c, 1) | ^ Expected `str`, found `Literal[1]` - | info: Function defined here --> src/mdtest_snippet.py:10:9 | 10 | def attr(self, value: str) -> None: | ^^^^ ---------- Parameter declared here - | ``` which is also equivalent to the following expressions: diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 0bfa70f44b..ad50e24a50 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -1066,13 +1066,11 @@ warning[ambiguous-protocol-member]: Cannot assign to an undeclared attribute in | 326 | self.augmented += 1 # snapshot: ambiguous-protocol-member | ^^^^^^^^^^^^^^ `augmented` is not declared as a protocol member - | info: Assigning to an undeclared attribute in a protocol method leads to an ambiguous interface --> src/mdtest_snippet.py:318:7 | 318 | class AssignmentForms(Protocol): | ^^^^^^^^^^^^^^^^^^^^^^^^^ `AssignmentForms` declared as a protocol here - | info: No declarations found for `augmented` in the body of `AssignmentForms` or any of its superclasses ``` @@ -4235,13 +4233,12 @@ iterable: Iterable[int] = DirectIterable # snapshot ```snapshot error[invalid-assignment]: Object of type `` is not assignable to `Iterable[int]` - --> src/mdtest_snippet.py:20:11 + --> src/mdtest_snippet.py:20:27 | 20 | iterable: Iterable[int] = DirectIterable # snapshot | ------------- ^^^^^^^^^^^^^^ Incompatible value of type `` | | | Declared type - | info: type `` is not assignable to protocol `Iterable[int]` info: └── protocol member `__iter__` is not defined on type `` info: └── special methods must be defined on the meta-type when matching a protocol diff --git a/crates/ty_python_semantic/resources/mdtest/regression/3525_character_split_notebook.md b/crates/ty_python_semantic/resources/mdtest/regression/3525_character_split_notebook.md index b7afa71b24..e3909af220 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/3525_character_split_notebook.md +++ b/crates/ty_python_semantic/resources/mdtest/regression/3525_character_split_notebook.md @@ -36,7 +36,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 2 | x = 1 # ty: ignore[unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment ::: cell 2 | diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Annotated_subscript_\342\200\246_(98082f2161ea366f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Annotated_subscript_\342\200\246_(98082f2161ea366f).snap" index bba81a9627..5b6dd364dd 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Annotated_subscript_\342\200\246_(98082f2161ea366f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Annotated_subscript_\342\200\246_(98082f2161ea366f).snap" @@ -27,7 +27,6 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | 4 | numbers[0]: str = "three" | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` @@ -37,6 +36,5 @@ error[invalid-type-form]: Type annotations are not allowed on subscripted expres | 4 | numbers[0]: str = "three" | ^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`dict`_(4aa9d1d82d07fcf1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`dict`_(4aa9d1d82d07fcf1).snap" index 7305b144c3..d5facda9bf 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`dict`_(4aa9d1d82d07fcf1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`dict`_(4aa9d1d82d07fcf1).snap" @@ -27,6 +27,5 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | ^^^^^^^-^^^^^ | | | Expected key of type `str`, got `Literal[0]` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`list`_(752cfa73fb34c1c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`list`_(752cfa73fb34c1c).snap" index 0b0e770975..fd77ebe502 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`list`_(752cfa73fb34c1c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_-_For_a_`list`_(752cfa73fb34c1c).snap" @@ -25,6 +25,5 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | 2 | numbers["zero"] = 3 # error: [invalid-assignment] | ^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_for\342\200\246_(815dae276e2fd2b7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_for\342\200\246_(815dae276e2fd2b7).snap" index ba4a84aedf..f6b047e6fe 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_for\342\200\246_(815dae276e2fd2b7).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_key_type_for\342\200\246_(815dae276e2fd2b7).snap" @@ -30,6 +30,5 @@ error[invalid-key]: TypedDict `Config` can only be subscripted with a string lit | 7 | config[0] = 3 # error: [invalid-key] | ^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`dict`_(177872afa1956fef).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`dict`_(177872afa1956fef).snap" index e7464a9ce4..9670e1f60f 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`dict`_(177872afa1956fef).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`dict`_(177872afa1956fef).snap" @@ -27,6 +27,5 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | ^^^^^^^^^^^^^^^^^^^^------- | | | Expected value of type `int`, got `Literal["three"]` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`list`_(e7ebbd4af387837c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`list`_(e7ebbd4af387837c).snap" index a0362842b3..2f085bcce7 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`list`_(e7ebbd4af387837c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_-_For_a_`list`_(e7ebbd4af387837c).snap" @@ -25,6 +25,5 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | 2 | numbers[0] = "three" # error: [invalid-assignment] | ^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_f\342\200\246_(155d53762388f9ad).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_f\342\200\246_(155d53762388f9ad).snap" index 7b7a1ba07f..f5b7c6efe4 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_f\342\200\246_(155d53762388f9ad).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Invalid_value_type_f\342\200\246_(155d53762388f9ad).snap" @@ -26,19 +26,17 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia ``` error[invalid-assignment]: Invalid assignment to key "retries" with declared type `int` on TypedDict `Config` - --> src/mdtest_snippet.py:7:5 + --> src/mdtest_snippet.py:7:25 | 7 | config["retries"] = "three" # error: [invalid-assignment] | ------ --------- ^^^^^^^ value of type `Literal["three"]` | | | | | key has declared type `int` | TypedDict `Config` - | info: Item declaration --> src/mdtest_snippet.py:4:5 | 4 | retries: int | ------------ Item declared here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Misspelled_key_for_`\342\200\246_(7cf0fa634e2a2d59).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Misspelled_key_for_`\342\200\246_(7cf0fa634e2a2d59).snap" index bc656372fa..b902d9d89b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Misspelled_key_for_`\342\200\246_(7cf0fa634e2a2d59).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Misspelled_key_for_`\342\200\246_(7cf0fa634e2a2d59).snap" @@ -26,14 +26,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia ``` error[invalid-key]: Unknown key "Retries" for TypedDict `Config` - --> src/mdtest_snippet.py:7:5 + --> src/mdtest_snippet.py:7:12 | 7 | config["Retries"] = 30.0 # error: [invalid-key] | ------ ^^^^^^^^^ Did you mean "retries"? | | | TypedDict `Config` | - | 6 | def _(config: Config) -> None: - config["Retries"] = 30.0 # error: [invalid-key] 7 + config["retries"] = 30.0 # error: [invalid-key] diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_No_`__setitem__`_met\342\200\246_(468f62a3bdd1d60c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_No_`__setitem__`_met\342\200\246_(468f62a3bdd1d60c).snap" index 28293ef4ab..0304377faf 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_No_`__setitem__`_met\342\200\246_(468f62a3bdd1d60c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_No_`__setitem__`_met\342\200\246_(468f62a3bdd1d60c).snap" @@ -29,7 +29,6 @@ error[invalid-assignment]: Cannot assign to a subscript on an object of type `Re | 6 | config["retries"] = 3 # error: [invalid-assignment] | ^^^^^^^^^^^^^^^^^ - | help: Consider adding a `__setitem__` method to `ReadOnlyDict`. ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Possibly_missing_`__\342\200\246_(efd3f0c02e9b89e9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Possibly_missing_`__\342\200\246_(efd3f0c02e9b89e9).snap" index e7bc46be47..890d507ff6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Possibly_missing_`__\342\200\246_(efd3f0c02e9b89e9).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Possibly_missing_`__\342\200\246_(efd3f0c02e9b89e9).snap" @@ -25,7 +25,6 @@ error[invalid-assignment]: Cannot assign to a subscript on an object of type `No | 2 | config["retries"] = 3 # error: [invalid-assignment] | ^^^^^^^^^^^^^^^^^ - | info: The full type of the subscripted object is `dict[str, int] | None` info: `None` does not have a `__setitem__` method. diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_all_\342\200\246_(8a0f0e8ceccc51b2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_all_\342\200\246_(8a0f0e8ceccc51b2).snap" index e05945c548..d831b9bcbe 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_all_\342\200\246_(8a0f0e8ceccc51b2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_all_\342\200\246_(8a0f0e8ceccc51b2).snap" @@ -33,24 +33,22 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia ``` error[invalid-key]: Unknown key "nane" for TypedDict `Animal` - --> src/mdtest_snippet.py:14:5 + --> src/mdtest_snippet.py:14:11 | 14 | being["nane"] = "unknown" | ----- ^^^^^^ Unknown key "nane" | | | TypedDict `Animal` in union type `Person | Animal` - | ``` ``` error[invalid-key]: Unknown key "nane" for TypedDict `Person` - --> src/mdtest_snippet.py:14:5 + --> src/mdtest_snippet.py:14:11 | 14 | being["nane"] = "unknown" | ----- ^^^^^^ Unknown key "nane" | | | TypedDict `Person` in union type `Person | Animal` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_one_\342\200\246_(b515711c0a451a86).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_one_\342\200\246_(b515711c0a451a86).snap" index 6444938508..f79d5e454b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_one_\342\200\246_(b515711c0a451a86).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Unknown_key_for_one_\342\200\246_(b515711c0a451a86).snap" @@ -31,12 +31,11 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia ``` error[invalid-key]: Unknown key "legs" for TypedDict `Person` - --> src/mdtest_snippet.py:12:5 + --> src/mdtest_snippet.py:12:11 | 12 | being["legs"] = 4 # error: [invalid-key] | ----- ^^^^^^ Unknown key "legs" | | | TypedDict `Person` in union type `Person | Animal` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(57372b65e30392a8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(57372b65e30392a8).snap" index 09ccb445d4..5ab94cc973 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(57372b65e30392a8).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(57372b65e30392a8).snap" @@ -27,7 +27,6 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | ^^^^^^^^^^^^^^^^^^^^- | | | Expected value of type `str`, got `Literal[3]` - | info: The full type of the subscripted object is `dict[str, int] | dict[str, str]` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" index dd6482e433..60fc839161 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" @@ -29,7 +29,6 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | ^^^^^^^^^^^^^^^^^^^^--- | | | Expected value of type `int`, got `float` - | info: The full type of the subscripted object is `dict[str, int] | dict[str, str]` ``` @@ -42,7 +41,6 @@ error[invalid-assignment]: Invalid subscript assignment with key of type `Litera | ^^^^^^^^^^^^^^^^^^^^--- | | | Expected value of type `str`, got `float` - | info: The full type of the subscripted object is `dict[str, int] | dict[str, str]` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/async.md_-_Async_with_statement\342\200\246_-_Context_expression_w\342\200\246_(28ef812089a32e6a).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/async.md_-_Async_with_statement\342\200\246_-_Context_expression_w\342\200\246_(28ef812089a32e6a).snap" index 55ca624cf8..856f10ff3f 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/async.md_-_Async_with_statement\342\200\246_-_Context_expression_w\342\200\246_(28ef812089a32e6a).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/async.md_-_Async_with_statement\342\200\246_-_Context_expression_w\342\200\246_(28ef812089a32e6a).snap" @@ -35,7 +35,6 @@ error[invalid-context-manager]: Object of type `Manager1 | NotAContextManager` c | 11 | async with context_expr as f: | ^^^^^^^^^^^^ - | info: `NotAContextManager` does not implement `__aenter__` or `__aexit__` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Invalid_exception_ha\342\200\246_(d394c561bdd35078).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Invalid_exception_ha\342\200\246_(d394c561bdd35078).snap" index 3619f8434e..2f433a830b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Invalid_exception_ha\342\200\246_(d394c561bdd35078).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Invalid_exception_ha\342\200\246_(d394c561bdd35078).snap" @@ -57,7 +57,6 @@ error[invalid-exception-caught]: Invalid object caught in an exception handler | 4 | except 3 as e: | ^ Object has type `Literal[3]` - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` @@ -71,7 +70,6 @@ error[invalid-exception-caught]: Invalid tuple caught in an exception handler | | | | | Invalid element of type `Literal[b"bar"]` | Invalid element of type `Literal["foo"]` - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` @@ -82,7 +80,6 @@ error[invalid-exception-caught]: Invalid object caught in an exception handler | 21 | except x as e: | ^ Object has type `type[str]` - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` @@ -93,7 +90,6 @@ error[invalid-exception-caught]: Invalid tuple caught in an exception handler | 24 | except y as f: | ^ Object has type `tuple[type[OSError], type[RuntimeError], int]` - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` @@ -104,7 +100,6 @@ error[invalid-exception-caught]: Invalid tuple caught in an exception handler | 27 | except z as g: | ^ Object has type `tuple[type[str], ...]` - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` @@ -115,7 +110,6 @@ error[invalid-exception-caught]: Invalid object caught in an exception handler | 33 | except int: | ^^^ Object has type `` - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno\342\200\246_(a97274530a7f61c1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno\342\200\246_(a97274530a7f61c1).snap" index cb14d8e188..3984c55bae 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno\342\200\246_(a97274530a7f61c1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Exception_Handling_-_Special-cased_diagno\342\200\246_(a97274530a7f61c1).snap" @@ -33,7 +33,6 @@ error[invalid-raise]: Cannot raise `NotImplemented` | 4 | raise NotImplemented from NotImplemented | ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`? - | info: Can only raise an instance or subclass of `BaseException` ``` @@ -44,7 +43,6 @@ error[invalid-raise]: Cannot use `NotImplemented` as an exception cause | 4 | raise NotImplemented from NotImplemented | ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`? - | info: An exception cause must be an instance of `BaseException`, subclass of `BaseException`, or `None` ``` @@ -55,7 +53,6 @@ error[invalid-exception-caught]: Cannot catch `NotImplemented` in an exception h | 6 | except NotImplemented: | ^^^^^^^^^^^^^^ Did you mean `NotImplementedError`? - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` @@ -69,7 +66,6 @@ error[invalid-exception-caught]: Invalid tuple caught in an exception handler | | | Invalid element of type `NotImplementedType` | Did you mean `NotImplementedError`? - | info: Can only catch a subclass of `BaseException` or tuple of `BaseException` subclasses ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(2fcfcf567587a056).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(2fcfcf567587a056).snap" index ba7d471ac4..3da3fe923c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(2fcfcf567587a056).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(2fcfcf567587a056).snap" @@ -26,7 +26,6 @@ error[unresolved-import]: Cannot resolve imported module `tomllib` | 1 | import tomllib # error: [unresolved-import] | ^^^^^^^ - | info: The stdlib module `tomllib` is only available on Python 3.11+ info: Python 3.10 was assumed when resolving modules because it was specified on the command line @@ -38,7 +37,6 @@ error[unresolved-import]: Cannot resolve imported module `string.templatelib` | 2 | from string.templatelib import Template # error: [unresolved-import] | ^^^^^^^^^^^^^^^^^^ - | info: The stdlib module `string.templatelib` is only available on Python 3.14+ info: Python 3.10 was assumed when resolving modules because it was specified on the command line @@ -50,7 +48,6 @@ error[unresolved-import]: Module `importlib.resources` has no member `abc` | 3 | from importlib.resources import abc # error: [unresolved-import] | ^^^ - | info: The stdlib module `importlib.resources` only has a `abc` submodule on Python 3.11+ info: Python 3.10 was assumed when resolving modules because it was specified on the command line diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(c14954eefd15211f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(c14954eefd15211f).snap" index 2155b6dcf1..1c0e66e13e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(c14954eefd15211f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(c14954eefd15211f).snap" @@ -25,7 +25,6 @@ error[unresolved-import]: Cannot resolve imported module `aifc` | 1 | import aifc # error: [unresolved-import] | ^^^^ - | info: The stdlib module `aifc` is only available on Python <=3.12 info: Python 3.13 was assumed when resolving modules because it was specified on the command line @@ -37,7 +36,6 @@ error[unresolved-import]: Cannot resolve imported module `distutils` | 2 | from distutils import sysconfig # error: [unresolved-import] | ^^^^^^^^^ - | info: The stdlib module `distutils` is only available on Python <=3.11 info: Python 3.13 was assumed when resolving modules because it was specified on the command line diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(dba22bd97137ee38).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(dba22bd97137ee38).snap" index 068a1b4124..da2e29279d 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(dba22bd97137ee38).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Attempting_to_import\342\200\246_(dba22bd97137ee38).snap" @@ -27,7 +27,6 @@ error[unresolved-import]: Cannot resolve imported module `compression.zstd` | 1 | import compression.zstd # error: [unresolved-import] | ^^^^^^^^^^^^^^^^ - | info: The stdlib module `compression` is only available on Python 3.14+ info: Python 3.10 was assumed when resolving modules because it was specified on the command line @@ -39,7 +38,6 @@ error[unresolved-import]: Cannot resolve imported module `compression` | 2 | from compression import zstd # error: [unresolved-import] | ^^^^^^^^^^^ - | info: The stdlib module `compression` is only available on Python 3.14+ info: Python 3.10 was assumed when resolving modules because it was specified on the command line @@ -51,7 +49,6 @@ error[unresolved-import]: Cannot resolve imported module `compression.fakebutwho | 3 | import compression.fakebutwhocansay # error: [unresolved-import] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info: The stdlib module `compression` is only available on Python 3.14+ info: Python 3.10 was assumed when resolving modules because it was specified on the command line @@ -63,7 +60,6 @@ error[unresolved-import]: Cannot resolve imported module `compression` | 4 | from compression import fakebutwhocansay # error: [unresolved-import] | ^^^^^^^^^^^ - | info: The stdlib module `compression` is only available on Python 3.14+ info: Python 3.10 was assumed when resolving modules because it was specified on the command line diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Multiple_objects_imp\342\200\246_(cbfbf5ff94e6e104).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Multiple_objects_imp\342\200\246_(cbfbf5ff94e6e104).snap" index 47fdfa10bb..a2d0cc2dbc 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Multiple_objects_imp\342\200\246_(cbfbf5ff94e6e104).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Multiple_objects_imp\342\200\246_(cbfbf5ff94e6e104).snap" @@ -25,7 +25,6 @@ error[unresolved-import]: Cannot resolve imported module `does_not_exist` | 2 | from does_not_exist import foo, bar, baz | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_\342\200\246_(846453deaca1071c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_\342\200\246_(846453deaca1071c).snap" index 58dd439bb8..1894f0f5ac 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_\342\200\246_(846453deaca1071c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_module_\342\200\246_(846453deaca1071c).snap" @@ -24,7 +24,6 @@ error[unresolved-import]: Cannot resolve imported module `zqzqzqzqzqzqzq` | 1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve imported module `zqzqzqzqzqzqzq`" | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodu\342\200\246_(4fad4be9778578b7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodu\342\200\246_(4fad4be9778578b7).snap" index 63d9b19ef1..f607150f14 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodu\342\200\246_(4fad4be9778578b7).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/basic.md_-_Structures_-_Unresolvable_submodu\342\200\246_(4fad4be9778578b7).snap" @@ -33,7 +33,6 @@ error[unresolved-import]: Cannot resolve imported module `a.foo` | 2 | import a.foo # error: [unresolved-import] "Cannot resolve imported module `a.foo`" | ^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) @@ -47,7 +46,6 @@ error[unresolved-import]: Cannot resolve imported module `b.foo` | 5 | import b.foo # error: [unresolved-import] "Cannot resolve imported module `b.foo`" | ^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Diagnostics_for_bad_\342\200\246_(2ceba7b720e21b8b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Diagnostics_for_bad_\342\200\246_(2ceba7b720e21b8b).snap" index 47ae403330..32cdd45d55 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Diagnostics_for_bad_\342\200\246_(2ceba7b720e21b8b).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Diagnostics_for_bad_\342\200\246_(2ceba7b720e21b8b).snap" @@ -47,7 +47,6 @@ error[invalid-type-arguments]: Type `int` is not assignable to upper bound `str` | 3 | T = TypeVar("T", bound=str) | - Type variable defined here - | ``` @@ -62,6 +61,5 @@ error[invalid-type-arguments]: Type `str` does not satisfy constraints `int`, `b | 4 | U = TypeVar("U", int, bytes) | - Type variable defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Errors_for_inconsist\342\200\246_(557742f3cd2464b2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Errors_for_inconsist\342\200\246_(557742f3cd2464b2).snap" index a9dead94b7..2169103cd6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Errors_for_inconsist\342\200\246_(557742f3cd2464b2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Errors_for_inconsist\342\200\246_(557742f3cd2464b2).snap" @@ -85,7 +85,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base is `Grandparent[T2@BadChild, T1@BadChild]` | Earlier class base inherits from `Grandparent[T1@BadChild, T2@BadChild]` - | ``` @@ -98,7 +97,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base is `Grandparent[T2@BadChild2, int]` | Earlier class base inherits from `Grandparent[T1@BadChild2, T2@BadChild2]` - | ``` @@ -111,7 +109,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild3, T1@BadChild3]` | Earlier class base inherits from `Grandparent[T1@BadChild3, T2@BadChild3]` - | ``` @@ -121,7 +118,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent` ( | 29 | class Fine(Parent, Grandparent[T1, T2]): ... # error: [missing-type-argument] | ^^^^^^ - | ``` @@ -131,7 +127,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent3` | 30 | class AlsoFine(Parent3, Parent4[T1, T2]): ... # error: [missing-type-argument] | ^^^^^^^ - | ``` @@ -141,7 +136,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent` ( | 35 | class Dandy(Parent, Parent3, Parent4): ... | ^^^^^^ - | ``` @@ -151,7 +145,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent3` | 35 | class Dandy(Parent, Parent3, Parent4): ... | ^^^^^^^ - | ``` @@ -161,7 +154,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent4` | 35 | class Dandy(Parent, Parent3, Parent4): ... | ^^^^^^^ - | ``` @@ -174,7 +166,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild4, T1@BadChild4]` | Earlier class base inherits from `Grandparent[T1@BadChild4, T2@BadChild4]` - | ``` @@ -184,7 +175,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent` ( | 42 | class BadChild4(Parent, Parent3[T1, T2], Parent4[T2, T1]): ... | ^^^^^^ - | ``` @@ -197,7 +187,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild5, T1@BadChild5]` | Earlier class base inherits from `Grandparent[T1@BadChild5, T2@BadChild5]` - | ``` @@ -210,7 +199,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild6, T1@BadChild6]` | Earlier class base inherits from `Grandparent[T1@BadChild6, T2@BadChild6]` - | ``` @@ -220,7 +208,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent3` | 49 | class BadChild6(Parent[T1, T2], Parent3, Parent4[T2, T1]): ... | ^^^^^^^ - | ``` @@ -233,7 +220,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild7, T1@BadChild7]` | Earlier class base inherits from `Grandparent[T1@BadChild7, T2@BadChild7]` - | ``` @@ -246,7 +232,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild8, T1@BadChild8]` | Earlier class base inherits from `Grandparent[T1@BadChild8, T2@BadChild8]` - | ``` @@ -256,7 +241,6 @@ info[missing-type-argument]: Missing type arguments for generic class `Parent4` | 56 | class BadChild8(Parent[T1, T2], Parent3[T2, T1], Parent4): ... | ^^^^^^^ - | ``` @@ -269,6 +253,5 @@ error[invalid-generic-class]: Inconsistent type arguments for `Grandparent` amon | | | | | Later class base inherits from `Grandparent[T2@BadChild9, T1@BadChild9]` | Earlier class base inherits from `Grandparent[T1@BadChild9, T2@BadChild9]` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" index 5d28b0b431..627aa2e7b9 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___Leg\342\200\246_-_Specializing_generic\342\200\246_(5a066394f338af48).snap" @@ -108,7 +108,6 @@ error[invalid-type-arguments]: Too many type arguments to class `C`: expected 1, | 11 | reveal_type(C[int, int]()) # revealed: C[Unknown] | ^^^ - | ``` @@ -123,7 +122,6 @@ error[invalid-type-arguments]: Type `str` is not assignable to upper bound `int` | 14 | BoundedT = TypeVar("BoundedT", bound=int) | -------- Type variable defined here - | ``` @@ -138,7 +136,6 @@ error[invalid-type-arguments]: Type `int | str` is not assignable to upper bound | 14 | BoundedT = TypeVar("BoundedT", bound=int) | -------- Type variable defined here - | info: element `str` of union `int | str` is not assignable to `int` ``` @@ -154,7 +151,6 @@ error[invalid-type-arguments]: Type `object` does not satisfy constraints `int`, | 34 | ConstrainedT = TypeVar("ConstrainedT", int, str) | ------------ Type variable defined here - | ``` @@ -164,7 +160,6 @@ error[invalid-type-arguments]: Too many type arguments to class `WithDefault`: e | 60 | reveal_type(WithDefault[str, str, str]()) # revealed: WithDefault[Unknown, Unknown] | ^^^ - | ``` @@ -181,7 +176,6 @@ error[invalid-generic-class]: Default of `WithDefaultT2` cannot reference later | ----------------------------------------------------- `WithDefaultT1` defined here 64 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) | --------------------------------------------------------------- `WithDefaultT2` defined here - | ``` @@ -198,7 +192,6 @@ error[invalid-generic-class]: Default of `WithDefaultT2` cannot reference later | ----------------------------------------------------- `WithDefaultT1` defined here 64 | WithDefaultT2 = TypeVar("WithDefaultT2", default=WithDefaultT1) | --------------------------------------------------------------- `WithDefaultT2` defined here - | ``` @@ -213,6 +206,5 @@ error[invalid-generic-class]: Default of `Start2T` cannot reference out-of-scope | 81 | Start2T = TypeVar("Start2T", default="StopT") | --------------------------------------------- `Start2T` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Default_type_paramet\342\200\246_(6bb09b09c131074).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Default_type_paramet\342\200\246_(6bb09b09c131074).snap" index c77a605eb2..281636a2c5 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Default_type_paramet\342\200\246_(6bb09b09c131074).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Default_type_paramet\342\200\246_(6bb09b09c131074).snap" @@ -48,74 +48,69 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/classes. ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:2:11 + --> src/mdtest_snippet.py:2:16 | 2 | class Foo[*Ts, T = int]: ... | --- ^^^^^^^ `T` has a default | | | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:5:15 + --> src/mdtest_snippet.py:5:20 | 5 | class Bar[T1, *Ts, T2 = int]: ... | --- ^^^^^^^^ `T2` has a default | | | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:8:11 + --> src/mdtest_snippet.py:8:16 | 8 | class Baz[*Ts, T1 = int, T2 = str]: ... | --- ^^^^^^^^ -------- `T2` also has a default | | | | | `T1` has a default | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:15:11 + --> src/mdtest_snippet.py:15:16 | 15 | class Qux[*Ts, **P = [int, str]]: ... | --- ^^^^^^^^^^^^^^^^ `P` has a default | | | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:18:12 + --> src/mdtest_snippet.py:18:17 | 18 | class Quux[*Ts, T1 = int, **P = [int, str]]: ... | --- ^^^^^^^^ ---------------- `P` also has a default | | | | | `T1` has a default | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:21:13 + --> src/mdtest_snippet.py:21:18 | 21 | class Corge[*Ts, T1 = int, T2 = str, **P = [int, str]]: ... | --- ^^^^^^^^ -------- ---------------- `P` also has a default @@ -123,33 +118,30 @@ error[invalid-type-variable-default]: Type parameters with defaults cannot follo | | | `T2` also has a default | | `T1` has a default | `Ts` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` ``` error[invalid-type-form]: Generic class `Grault` cannot have multiple `TypeVarTuple` type parameters - --> src/mdtest_snippet.py:25:14 + --> src/mdtest_snippet.py:25:19 | 25 | class Grault[*Us, *Ts = *tuple[int, str]]: ... | --- ^^^^^^^^^^^^^^^^^^^^^^ `Ts` is an additional TypeVarTuple | | | `Us` is the first TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#multiple-type-variable-tuples-not-allowed ``` ``` error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter - --> src/mdtest_snippet.py:25:14 + --> src/mdtest_snippet.py:25:19 | 25 | class Grault[*Us, *Ts = *tuple[int, str]]: ... | --- ^^^^^^^^^^^^^^^^^^^^^^ `Ts` has a default | | | `Us` is a TypeVarTuple - | info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Diagnostics_for_bad_\342\200\246_(cf706b07cf0ec31f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Diagnostics_for_bad_\342\200\246_(cf706b07cf0ec31f).snap" index 8f16be3264..65e0a3b234 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Diagnostics_for_bad_\342\200\246_(cf706b07cf0ec31f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Diagnostics_for_bad_\342\200\246_(cf706b07cf0ec31f).snap" @@ -42,7 +42,6 @@ error[invalid-type-arguments]: Type `int` is not assignable to upper bound `str` | 1 | class Bounded[T: str]: | - Type variable defined here - | ``` @@ -57,6 +56,5 @@ error[invalid-type-arguments]: Type `str` does not satisfy constraints `int`, `b | 4 | class Constrained[U: (int, bytes)]: | - Type variable defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Scoping_of_typevars_-_No_back-references_(9051beb16a623d36).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Scoping_of_typevars_-_No_back-references_(9051beb16a623d36).snap" index 7841c6b034..a3573d2ea0 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Scoping_of_typevars_-_No_back-references_(9051beb16a623d36).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/classes.md_-_Generic_classes___PEP\342\200\246_-_Scoping_of_typevars_-_No_back-references_(9051beb16a623d36).snap" @@ -47,7 +47,6 @@ error[invalid-type-variable-bound]: TypeVar upper bound cannot be generic | 2 | class C[S: T, T]: | ^ - | ``` @@ -57,7 +56,6 @@ error[invalid-type-variable-bound]: TypeVar upper bound cannot be generic | 6 | class D[S, T: S]: | ^ - | ``` @@ -67,7 +65,6 @@ error[invalid-type-variable-constraints]: TypeVar constraint cannot be generic | 10 | class E[S: (int, T), T]: | ^ - | ``` @@ -79,7 +76,6 @@ error[invalid-generic-class]: Default of `S` cannot reference later type paramet | ^^^ ----- ------- `T` defined here | | | `S` defined here - | ``` @@ -91,6 +87,5 @@ error[invalid-generic-class]: Default of `S` cannot reference later type paramet | ^^^^^^^ ----------- ------- `T` defined here | | | `S` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Last_argument_must_b\342\200\246_(dc429fc3e8c18eaf).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Last_argument_must_b\342\200\246_(dc429fc3e8c18eaf).snap" index a1382a8091..9b98036efb 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Last_argument_must_b\342\200\246_(dc429fc3e8c18eaf).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Last_argument_must_b\342\200\246_(dc429fc3e8c18eaf).snap" @@ -45,7 +45,6 @@ error[invalid-type-arguments]: The last argument to `typing.Concatenate` must be | 7 | def _(c: Callable[Concatenate[int, str], bool]): ... | ^^^ Got `str` - | ``` @@ -55,7 +54,6 @@ error[invalid-type-arguments]: The last argument to `typing.Concatenate` must be | 10 | reveal_type(Foo[Concatenate[int, str]].attr) # revealed: (...) -> None | ^^^ Got `str` - | ``` @@ -65,7 +63,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 13 | reveal_type(Foo[Concatenate[int, Concatenate]].attr) # revealed: (...) -> None | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -78,7 +75,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 16 | reveal_type(Foo[Concatenate[int, Concatenate[()]]].attr) # revealed: (...) -> None | ^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -91,7 +87,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 19 | reveal_type(Foo[Concatenate[int, Concatenate[int]]].attr) # revealed: (...) -> None | ^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -104,7 +99,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 22 | reveal_type(Foo[Concatenate[int, Concatenate[int, str]]].attr) # revealed: (...) -> None | ^^^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Nested_`Concatenate`_(86093b62e6e6874c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Nested_`Concatenate`_(86093b62e6e6874c).snap" index 078e61c274..ea11c86add 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Nested_`Concatenate`_(86093b62e6e6874c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Nested_`Concatenate`_(86093b62e6e6874c).snap" @@ -34,7 +34,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 5 | c: Callable[Concatenate[Concatenate[int, ...], P], None], | ^^^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -47,7 +46,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 7 | d: Callable[Concatenate[Concatenate, P], int], | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -60,7 +58,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 9 | e: Callable[Concatenate[int, Concatenate[int, ...]], None], | ^^^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Standalone_annotatio\342\200\246_(bb5fe70ded875e4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Standalone_annotatio\342\200\246_(bb5fe70ded875e4).snap" index 51a5e5ae30..fd0155652e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Standalone_annotatio\342\200\246_(bb5fe70ded875e4).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Standalone_annotatio\342\200\246_(bb5fe70ded875e4).snap" @@ -51,7 +51,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 6 | def invalid0(x: Concatenate): ... | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -64,7 +63,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 9 | def invalid1(x: Concatenate[int]): ... | ^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -77,7 +75,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 12 | def invalid2(x: Concatenate[int, ...]) -> None: ... | ^^^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -90,7 +87,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 15 | def invalid3() -> Concatenate[int, ...]: ... | ^^^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -103,7 +99,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 18 | def invalid4() -> Concatenate[()]: ... | ^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -116,7 +111,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 21 | a: Concatenate | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -129,7 +123,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 25 | b: Concatenate[int, P] | ^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -142,7 +135,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 28 | def invalid5[**P](x: Foo[Concatenate[P, ...]]) -> None: ... | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Too_few_arguments_(efcf77cdbde3ff86).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Too_few_arguments_(efcf77cdbde3ff86).snap" index ee82397533..b39470307c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Too_few_arguments_(efcf77cdbde3ff86).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/concatenate.md_-_`typing.Concatenate`_-_Invalid_uses_of_`Con\342\200\246_-_Too_few_arguments_(efcf77cdbde3ff86).snap" @@ -74,7 +74,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least 2 arguments whe | 8 | a: Callable[Concatenate[()], int], | ^^^^^^^^^^^^^^^ - | ``` @@ -84,7 +83,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least 2 arguments whe | 10 | b: Callable[Concatenate[int], int], | ^^^^^^^^^^^^^^^^ - | ``` @@ -94,7 +92,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least 2 arguments whe | 12 | c: Callable[Concatenate[(int,)], int], | ^^^^^^^^^^^^^^^^^^^ - | ``` @@ -104,7 +101,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least two arguments w | 14 | d: Callable[Concatenate, int], | ^^^^^^^^^^^ - | ``` @@ -114,7 +110,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least 2 arguments whe | 21 | reveal_type(Foo[Concatenate[()]].attr) # revealed: (...) -> None | ^^^^^^^^^^^^^^^ - | ``` @@ -124,7 +119,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least 2 arguments whe | 23 | reveal_type(Foo[Concatenate[int]].attr) # revealed: (...) -> None | ^^^^^^^^^^^^^^^^ - | ``` @@ -134,7 +128,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least 2 arguments whe | 25 | reveal_type(Foo[Concatenate[(int,)]].attr) # revealed: (...) -> None | ^^^^^^^^^^^^^^^^^^^ - | ``` @@ -144,7 +137,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least two arguments w | 27 | reveal_type(Foo[Concatenate].attr) # revealed: (...) -> None | ^^^^^^^^^^^ - | ``` @@ -154,7 +146,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 29 | reveal_type(Foo[[Concatenate]].attr) # revealed: (Unknown, /) -> None | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -167,7 +158,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 31 | reveal_type(Foo[[Concatenate, int]].attr) # revealed: (Unknown, int, /) -> None | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -180,7 +170,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 34 | reveal_type(Foo[[Concatenate[int], str]].attr) # revealed: (Unknown, str, /) -> None | ^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -193,7 +182,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 36 | reveal_type(Foo[[Concatenate[int, str], str]].attr) # revealed: (Unknown, str, /) -> None | ^^^^^^^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -206,7 +194,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 38 | reveal_type(Foo[[Concatenate[()], str]].attr) # revealed: (Unknown, str, /) -> None | ^^^^^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -219,7 +206,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least two arguments w | 48 | reveal_type(Bar[Concatenate, Concatenate].a) # revealed: (...) -> int | ^^^^^^^^^^^ - | ``` @@ -229,7 +215,6 @@ error[invalid-type-form]: `typing.Concatenate` requires at least two arguments w | 48 | reveal_type(Bar[Concatenate, Concatenate].a) # revealed: (...) -> int | ^^^^^^^^^^^ - | ``` @@ -239,7 +224,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 51 | reveal_type(Bar[[Concatenate], [Concatenate]].a) # revealed: (Unknown, /) -> int | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter @@ -252,7 +236,6 @@ error[invalid-type-form]: `typing.Concatenate` is not allowed in this context in | 51 | reveal_type(Bar[[Concatenate], [Concatenate]].a) # revealed: (Unknown, /) -> int | ^^^^^^^^^^^ - | info: `typing.Concatenate` is only valid: info: - as the first argument to `Callable` info: - as a type argument for a `ParamSpec` parameter diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Introduction_(cff2724f4c9d28c4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Introduction_(cff2724f4c9d28c4).snap" index 45e9ea1687..4564dd5749 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Introduction_(cff2724f4c9d28c4).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Introduction_(cff2724f4c9d28c4).snap" @@ -45,7 +45,6 @@ warning[deprecated]: The function `myfunc` is deprecated | 6 | myfunc(1) # error: [deprecated] "use OtherClass" | ^^^^^^ use OtherClass - | ``` @@ -55,7 +54,6 @@ warning[deprecated]: The class `MyClass` is deprecated | 12 | MyClass() # error: [deprecated] "use BetterClass" | ^^^^^^^ use BetterClass - | ``` @@ -65,7 +63,6 @@ warning[deprecated]: The function `afunc` is deprecated | 21 | MyClass.afunc() # error: [deprecated] "use something else" | ^^^^^ use something else - | ``` @@ -75,6 +72,5 @@ warning[deprecated]: The function `amethod` is deprecated | 22 | MyClass().amethod() # error: [deprecated] "don't use this!" | ^^^^^^^ don't use this! - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Syntax_(142fa2948c3c6cf1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Syntax_(142fa2948c3c6cf1).snap" index d84fa4f302..624cc4b50a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Syntax_(142fa2948c3c6cf1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Syntax_(142fa2948c3c6cf1).snap" @@ -73,7 +73,6 @@ error[invalid-argument-type]: Argument to class `deprecated` is incorrect | 3 | @deprecated # error: [invalid-argument-type] "LiteralString" | ^^^^^^^^^^^ Expected `LiteralString`, found `def invalid_deco() -> Unknown` - | ``` @@ -83,13 +82,11 @@ error[missing-argument]: No argument provided for required parameter `arg` of bo | 6 | invalid_deco() # error: [missing-argument] | ^^^^^^^^^^^^^^ - | info: Parameter declared here --> stdlib/typing_extensions.pyi:1220:28 | 1220 | def __call__(self, arg: _T, /) -> _T: ... | ^^^^^^^ - | ``` @@ -99,7 +96,6 @@ error[missing-argument]: No argument provided for required parameter `message` o | 9 | @deprecated() # error: [missing-argument] "message" | ^^^^^^^^^^^^ - | ``` @@ -109,7 +105,6 @@ warning[deprecated]: The function `invalid_deco` is deprecated | 20 | invalid_deco() # error: [deprecated] "message" | ^^^^^^^^^^^^ message - | ``` @@ -119,7 +114,6 @@ warning[deprecated]: The function `valid_deco` is deprecated | 29 | valid_deco() # error: [deprecated] | ^^^^^^^^^^ - | ``` @@ -129,7 +123,6 @@ error[invalid-argument-type]: Argument to class `deprecated` is incorrect | 35 | @deprecated(opaque()) # error: [invalid-argument-type] "LiteralString" | ^^^^^^^^ Expected `LiteralString`, found `str` - | ``` @@ -139,7 +132,6 @@ error[unknown-argument]: Argument `dsfsdf` does not match any known parameter of | 41 | @deprecated("some message", dsfsdf="whatever") # error: [unknown-argument] "dsfsdf" | ^^^^^^^^^^^^^^^^^ - | ``` @@ -149,6 +141,5 @@ warning[deprecated]: The function `valid_deco` is deprecated | 50 | valid_deco() # error: [deprecated] "some message" | ^^^^^^^^^^ some message - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Abstract_method_in_g\342\200\246_(6d8b024dda7ced11).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Abstract_method_in_g\342\200\246_(6d8b024dda7ced11).snap" index 97d474c324..d5efd8ded3 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Abstract_method_in_g\342\200\246_(6d8b024dda7ced11).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Abstract_method_in_g\342\200\246_(6d8b024dda7ced11).snap" @@ -32,7 +32,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md ``` error[abstract-method-in-final-class]: Final class `Child` has unimplemented abstract methods - --> src/mdtest_snippet.py:5:5 + --> src/mdtest_snippet.py:12:7 | 5 | / @abstractmethod 6 | | def method(self) -> int: ... @@ -45,6 +45,5 @@ error[abstract-method-in-final-class]: Final class `Child` has unimplemented abs | ------ 12 | class Child(Parent): # error: [abstract-method-in-final-class] | ^^^^^ `method` is unimplemented - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Basic_case_with_ABC_(21e412599c45972a).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Basic_case_with_ABC_(21e412599c45972a).snap" index 4a35aa96b9..7252403011 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Basic_case_with_ABC_(21e412599c45972a).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Basic_case_with_ABC_(21e412599c45972a).snap" @@ -30,7 +30,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md ``` error[abstract-method-in-final-class]: Final class `Derived` has unimplemented abstract methods - --> src/mdtest_snippet.py:5:5 + --> src/mdtest_snippet.py:10:7 | 5 | / @abstractmethod 6 | | def foo(self) -> int: @@ -41,6 +41,5 @@ error[abstract-method-in-final-class]: Final class `Derived` has unimplemented a | ------ 10 | class Derived(Base): # error: [abstract-method-in-final-class] "Final class `Derived` has unimplemented abstract method `foo`" | ^^^^^^^ `foo` is unimplemented - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(ecae0f4510696c95).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(ecae0f4510696c95).snap" index 6d678dfab6..3ecf3bac7c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(ecae0f4510696c95).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(ecae0f4510696c95).snap" @@ -45,17 +45,16 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md ``` error[abstract-method-in-final-class]: Final class `Abstract` has unimplemented abstract methods - --> src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:6:7 | 4 | @final | ------ -5 | # error: [abstract-method-in-final-class] "Final class `Abstract` has unimplemented abstract methods `aaaaaaaaaa`, `bbbbbbbb`, `ccccccc… +5 | # error: [abstract-method-in-final-class] "Final class `Abstract` has unimplemented abstract methods `aaaaaaaaaa`, `bbbbbbbb`, `ccccc… 6 | class Abstract(ABC): | ^^^^^^^^ Abstract methods `aaaaaaaaaa`, `bbbbbbbb`, `cccccccc`, `ddddddddd`, `eeeeeeeee`, `ffffffff`, `ggggggg`, `hhhhhhhh`, `iiiiiiiii` and `kkkkkkkkkk` are unimplemented 7 | / @abstractmethod 8 | | def aaaaaaaaaa(self) -> int: ... | |____________________________________- `aaaaaaaaaa` declared as abstract - | info: rule `abstract-method-in-final-class` is enabled by default ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(f807ff3716d8ab0d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(f807ff3716d8ab0d).snap" index 68cb8fa1f5..f1323f512d 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(f807ff3716d8ab0d).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Diagnostic_when_ther\342\200\246_(f807ff3716d8ab0d).snap" @@ -45,17 +45,16 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md ``` error[abstract-method-in-final-class]: Final class `Abstract` has unimplemented abstract methods - --> src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:6:7 | 4 | @final | ------ -5 | # error: [abstract-method-in-final-class] "Final class `Abstract` has 10 unimplemented abstract methods, including `aaaaaaaaaa`, `bbbbb… +5 | # error: [abstract-method-in-final-class] "Final class `Abstract` has 10 unimplemented abstract methods, including `aaaaaaaaaa`, `bbb… 6 | class Abstract(ABC): | ^^^^^^^^ 10 abstract methods are unimplemented, including `aaaaaaaaaa`, `bbbbbbbb` and `cccccccc` 7 | / @abstractmethod 8 | | def aaaaaaaaaa(self) -> int: ... | |____________________________________- `aaaaaaaaaa` declared as abstract - | info: Use `--verbose` to see all 10 unimplemented abstract methods ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Multiple_abstract_me\342\200\246_(feafee9a4abbe8d1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Multiple_abstract_me\342\200\246_(feafee9a4abbe8d1).snap" index a48f5f60c8..05eaf2da1e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Multiple_abstract_me\342\200\246_(feafee9a4abbe8d1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Multiple_abstract_me\342\200\246_(feafee9a4abbe8d1).snap" @@ -41,7 +41,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md ``` error[abstract-method-in-final-class]: Final class `MissingAll` has unimplemented abstract methods - --> src/mdtest_snippet.py:12:1 + --> src/mdtest_snippet.py:13:7 | 12 | @final | ------ @@ -53,13 +53,12 @@ error[abstract-method-in-final-class]: Final class `MissingAll` has unimplemente 5 | / @abstractmethod 6 | | def foo(self) -> int: ... | |_____________________________- `foo` declared as abstract on superclass `Base` - | ``` ``` error[abstract-method-in-final-class]: Final class `PartiallyImplemented` has unimplemented abstract methods - --> src/mdtest_snippet.py:16:1 + --> src/mdtest_snippet.py:17:7 | 16 | @final | ------ @@ -71,6 +70,5 @@ error[abstract-method-in-final-class]: Final class `PartiallyImplemented` has un 9 | / @abstractmethod 10 | | def baz(self) -> None: ... | |______________________________- `baz` declared as abstract on superclass `Base` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Protocol_with_implic\342\200\246_(e373f31c7a7d88e7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Protocol_with_implic\342\200\246_(e373f31c7a7d88e7).snap" index 599aeabe42..e1896646dd 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Protocol_with_implic\342\200\246_(e373f31c7a7d88e7).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_`@final`_class_mus\342\200\246_-_Protocol_with_implic\342\200\246_(e373f31c7a7d88e7).snap" @@ -169,7 +169,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md ``` error[abstract-method-in-final-class]: Final class `Q` has unimplemented abstract methods - --> src/mdtest_snippet.py:11:5 + --> src/mdtest_snippet.py:14:7 | 11 | def still_abstractmethod(self): ... | ----------------------------------- `still_abstractmethod` declared as abstract on superclass `P` @@ -178,20 +178,18 @@ error[abstract-method-in-final-class]: Final class `Q` has unimplemented abstrac | ------ 14 | class Q(P): ... # error: [abstract-method-in-final-class] | ^ `still_abstractmethod` is unimplemented - | info: `P.still_abstractmethod` is implicitly abstract because `P` is a `Protocol` class and `still_abstractmethod` lacks an implementation --> src/mdtest_snippet.py:3:7 | 3 | class P(Protocol): | ----------- `P` declared here - | help: Change the body of `still_abstractmethod` to `return` or `return None` if it was not intended to be abstract ``` ``` error[abstract-method-in-final-class]: Final class `S` has unimplemented abstract methods - --> src/mdtest_snippet.py:18:5 + --> src/mdtest_snippet.py:21:7 | 18 | def also_still_abstractmethod(self) -> None: ... | ------------------------------------------------ `also_still_abstractmethod` declared as abstract on superclass `R` @@ -200,20 +198,18 @@ error[abstract-method-in-final-class]: Final class `S` has unimplemented abstrac | ------ 21 | class S(R): ... # error: [abstract-method-in-final-class] | ^ `also_still_abstractmethod` is unimplemented - | info: `R.also_still_abstractmethod` is implicitly abstract because `R` is a `Protocol` class and `also_still_abstractmethod` lacks an implementation --> src/mdtest_snippet.py:16:7 | 16 | class R(Protocol): | ----------- `R` declared here - | help: Change the body of `also_still_abstractmethod` to `return` or `return None` if it was not intended to be abstract ``` ``` error[abstract-method-in-final-class]: Final class `RaisesSub` has unimplemented abstract methods - --> src/mdtest_snippet.py:24:5 + --> src/mdtest_snippet.py:28:7 | 24 | / def even_this_is_abstract(self): 25 | | raise NotImplementedError @@ -223,19 +219,17 @@ error[abstract-method-in-final-class]: Final class `RaisesSub` has unimplemented | ------ 28 | class RaisesSub(Raises): ... # error: [abstract-method-in-final-class] | ^^^^^^^^^ `even_this_is_abstract` is unimplemented - | info: `Raises.even_this_is_abstract` is implicitly abstract because `Raises` is a `Protocol` class and `even_this_is_abstract` lacks an implementation --> src/mdtest_snippet.py:23:7 | 23 | class Raises(Protocol): | ---------------- `Raises` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `AlsoRaisesSub` has unimplemented abstract methods - --> src/mdtest_snippet.py:31:5 + --> src/mdtest_snippet.py:35:7 | 31 | / def also_abstractmethod(self) -> Never: 32 | | raise NotImplementedError @@ -245,19 +239,17 @@ error[abstract-method-in-final-class]: Final class `AlsoRaisesSub` has unimpleme | ------ 35 | class AlsoRaisesSub(AlsoRaises): ... # error: [abstract-method-in-final-class] | ^^^^^^^^^^^^^ `also_abstractmethod` is unimplemented - | info: `AlsoRaises.also_abstractmethod` is implicitly abstract because `AlsoRaises` is a `Protocol` class and `also_abstractmethod` lacks an implementation --> src/mdtest_snippet.py:30:7 | 30 | class AlsoRaises(Protocol): | -------------------- `AlsoRaises` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `StrangeSub` has unimplemented abstract methods - --> src/mdtest_snippet.py:41:9 + --> src/mdtest_snippet.py:45:11 | 41 | / def weird_abstractmethod(self): 42 | | raise x @@ -267,19 +259,17 @@ error[abstract-method-in-final-class]: Final class `StrangeSub` has unimplemente | ------ 45 | class StrangeSub(Strange): ... # error: [abstract-method-in-final-class] | ^^^^^^^^^^ `weird_abstractmethod` is unimplemented - | info: `Strange.weird_abstractmethod` is implicitly abstract because `Strange` is a `Protocol` class and `weird_abstractmethod` lacks an implementation --> src/mdtest_snippet.py:40:11 | 40 | class Strange(Protocol): | ----------------- `Strange` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasOverloadSub` has unimplemented abstract methods - --> src/mdtest_snippet.py:51:9 + --> src/mdtest_snippet.py:54:7 | 51 | def foo(self, x: int) -> str: ... | --- `foo` declared as abstract on superclass `HasOverloads` @@ -288,19 +278,17 @@ error[abstract-method-in-final-class]: Final class `HasOverloadSub` has unimplem | ------ 54 | class HasOverloadSub(HasOverloads): ... # error: [abstract-method-in-final-class] | ^^^^^^^^^^^^^^ `foo` is unimplemented - | info: `HasOverloads.foo` is implicitly abstract because `HasOverloads` is a `Protocol` class and `foo` lacks an implementation --> src/mdtest_snippet.py:47:7 | 47 | class HasOverloads(Protocol): | ---------------------- `HasOverloads` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstractSub` has unimplemented abstract methods - --> src/mdtest_snippet.py:122:1 + --> src/mdtest_snippet.py:123:7 | 122 | @final | ------ @@ -311,19 +299,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstractSub` has unimplem | 72 | def a(self) -> int: ... | ----------------------- `a` declared as abstract on superclass `HasAbstract` - | info: `HasAbstract.a` is implicitly abstract because `HasAbstract` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:71:7 | 71 | class HasAbstract(Protocol): | --------------------- `HasAbstract` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract2Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:125:1 + --> src/mdtest_snippet.py:126:7 | 125 | @final | ------ @@ -335,19 +321,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract2Sub` has unimple 75 | / def a(self) -> int: 76 | | pass | |____________- `a` declared as abstract on superclass `HasAbstract2` - | info: `HasAbstract2.a` is implicitly abstract because `HasAbstract2` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:74:7 | 74 | class HasAbstract2(Protocol): | ---------------------- `HasAbstract2` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract3Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:128:1 + --> src/mdtest_snippet.py:129:7 | 128 | @final | ------ @@ -358,19 +342,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract3Sub` has unimple | 83 | def a(self) -> int: | ------------------ `a` declared as abstract on superclass `HasAbstract4` - | info: `HasAbstract4.a` is implicitly abstract because `HasAbstract4` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:82:7 | 82 | class HasAbstract4(Protocol): | ---------------------- `HasAbstract4` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract4Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:131:1 + --> src/mdtest_snippet.py:132:7 | 131 | @final | ------ @@ -381,19 +363,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract4Sub` has unimple | 83 | def a(self) -> int: | ------------------ `a` declared as abstract on superclass `HasAbstract4` - | info: `HasAbstract4.a` is implicitly abstract because `HasAbstract4` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:82:7 | 82 | class HasAbstract4(Protocol): | ---------------------- `HasAbstract4` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract5Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:134:1 + --> src/mdtest_snippet.py:135:7 | 134 | @final | ------ @@ -404,19 +384,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract5Sub` has unimple | 88 | def a(self) -> int: | ------------------ `a` declared as abstract on superclass `HasAbstract5` - | info: `HasAbstract5.a` is implicitly abstract because `HasAbstract5` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:87:7 | 87 | class HasAbstract5(Protocol): | ---------------------- `HasAbstract5` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract6Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:137:1 + --> src/mdtest_snippet.py:138:7 | 137 | @final | ------ @@ -427,19 +405,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract6Sub` has unimple | 93 | def a(self) -> int: | ------------------ `a` declared as abstract on superclass `HasAbstract6` - | info: `HasAbstract6.a` is implicitly abstract because `HasAbstract6` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:92:7 | 92 | class HasAbstract6(Protocol): | ---------------------- `HasAbstract6` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract7Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:140:1 + --> src/mdtest_snippet.py:141:7 | 140 | @final | ------ @@ -451,19 +427,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract7Sub` has unimple 105 | / def a(self) -> int: 106 | | raise NotImplementedError | |_________________________________- `a` declared as abstract on superclass `HasAbstract7` - | info: `HasAbstract7.a` is implicitly abstract because `HasAbstract7` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:104:7 | 104 | class HasAbstract7(Protocol): | ---------------------- `HasAbstract7` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract8Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:143:1 + --> src/mdtest_snippet.py:144:7 | 143 | @final | ------ @@ -475,19 +449,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract8Sub` has unimple 109 | / def a(self) -> int: 110 | | raise NotImplementedError() | |___________________________________- `a` declared as abstract on superclass `HasAbstract8` - | info: `HasAbstract8.a` is implicitly abstract because `HasAbstract8` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:108:7 | 108 | class HasAbstract8(Protocol): | ---------------------- `HasAbstract8` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract9Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:146:1 + --> src/mdtest_snippet.py:147:7 | 146 | @final | ------ @@ -498,19 +470,17 @@ error[abstract-method-in-final-class]: Final class `HasAbstract9Sub` has unimple | 113 | def a(self) -> int: | ------------------ `a` declared as abstract on superclass `HasAbstract9` - | info: `HasAbstract9.a` is implicitly abstract because `HasAbstract9` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:112:7 | 112 | class HasAbstract9(Protocol): | ---------------------- `HasAbstract9` declared here - | ``` ``` error[abstract-method-in-final-class]: Final class `HasAbstract10Sub` has unimplemented abstract methods - --> src/mdtest_snippet.py:149:1 + --> src/mdtest_snippet.py:150:7 | 149 | @final | ------ @@ -521,12 +491,10 @@ error[abstract-method-in-final-class]: Final class `HasAbstract10Sub` has unimpl | 118 | def a(self) -> int: | ------------------ `a` declared as abstract on superclass `HasAbstract10` - | info: `HasAbstract10.a` is implicitly abstract because `HasAbstract10` is a `Protocol` class and `a` lacks an implementation --> src/mdtest_snippet.py:117:7 | 117 | class HasAbstract10(Protocol): | ----------------------- `HasAbstract10` declared here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_possibly-undefined\342\200\246_(fc7b496fd1986deb).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_possibly-undefined\342\200\246_(fc7b496fd1986deb).snap" index fe1a18dfe3..bde456cf82 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_possibly-undefined\342\200\246_(fc7b496fd1986deb).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_A_possibly-undefined\342\200\246_(fc7b496fd1986deb).snap" @@ -96,7 +96,6 @@ error[override-of-final-method]: Cannot override `A.method1` | 40 | def method1(self) -> None: ... # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:8:9 | @@ -104,7 +103,6 @@ info: `A.method1` is decorated with `@final`, forbidding overrides | ------ 9 | def method1(self) -> None: ... | ------- `A.method1` defined here - | help: Remove the override of `method1` | 39 | class B(A): @@ -122,7 +120,6 @@ error[override-of-final-method]: Cannot override `A.method2` | 41 | def method2(self) -> None: ... # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method2` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:18:9 | @@ -130,7 +127,6 @@ info: `A.method2` is decorated with `@final`, forbidding overrides | ------ 19 | def method2(self) -> None: ... | ------- `A.method2` defined here - | help: Remove the override of `method2` | 40 | def method1(self) -> None: ... # error: [override-of-final-method] @@ -148,7 +144,6 @@ error[override-of-final-method]: Cannot override `A.method3` | 42 | def method3(self) -> None: ... # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method3` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:22:9 | @@ -156,7 +151,6 @@ info: `A.method3` is decorated with `@final`, forbidding overrides | ------ 23 | def method3(self) -> None: ... | ------- `A.method3` defined here - | help: Remove the override of `method3` | 41 | def method2(self) -> None: ... # error: [override-of-final-method] @@ -174,7 +168,6 @@ error[override-of-final-method]: Cannot override `A.method4` | 49 | method4 = 42 | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method4` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:33:9 | @@ -182,7 +175,6 @@ info: `A.method4` is decorated with `@final`, forbidding overrides | ------ 34 | def method4(self) -> None: ... | ------- `A.method4` defined here - | help: Remove the override of `method4` ``` @@ -193,7 +185,6 @@ error[override-of-final-method]: Cannot override `A.method1` | 55 | def method1(self) -> None: ... # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:8:9 | @@ -201,7 +192,6 @@ info: `A.method1` is decorated with `@final`, forbidding overrides | ------ 9 | def method1(self) -> None: ... | ------- `A.method1` defined here - | help: Remove the override of `method1` ``` @@ -212,7 +202,6 @@ error[override-of-final-method]: Cannot override `A.method2` | 61 | def method2(self) -> None: ... # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method2` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:18:9 | @@ -220,7 +209,6 @@ info: `A.method2` is decorated with `@final`, forbidding overrides | ------ 19 | def method2(self) -> None: ... | ------- `A.method2` defined here - | help: Remove the override of `method2` ``` @@ -231,7 +219,6 @@ error[override-of-final-method]: Cannot override `A.method3` | 67 | def method3(self) -> None: ... # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method3` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:22:9 | @@ -239,7 +226,6 @@ info: `A.method3` is decorated with `@final`, forbidding overrides | ------ 23 | def method3(self) -> None: ... | ------- `A.method3` defined here - | help: Remove the override of `method3` ``` @@ -250,7 +236,6 @@ error[override-of-final-method]: Cannot override `A.method4` | 71 | method4 = 42 # error: [override-of-final-method] | ^^^^^^^ Overrides a definition from superclass `A` - | info: `A.method4` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:33:9 | @@ -258,7 +243,6 @@ info: `A.method4` is decorated with `@final`, forbidding overrides | ------ 34 | def method4(self) -> None: ... | ------- `A.method4` defined here - | help: Remove the override of `method4` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Cannot_override_a_me\342\200\246_(338615109711a91b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Cannot_override_a_me\342\200\246_(338615109711a91b).snap" index b744a5683d..4e28e306ed 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Cannot_override_a_me\342\200\246_(338615109711a91b).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Cannot_override_a_me\342\200\246_(338615109711a91b).snap" @@ -136,7 +136,6 @@ error[override-of-final-method]: Cannot override `Parent.foo` | 42 | def foo(self): ... | ^^^ Overrides a definition from superclass `Parent` - | info: `Parent.foo` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:7:5 | @@ -144,7 +143,6 @@ info: `Parent.foo` is decorated with `@final`, forbidding overrides | ------ 8 | def foo(self): ... | --- `Parent.foo` defined here - | help: Remove the override of `foo` | 41 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`" @@ -162,7 +160,6 @@ error[override-of-final-method]: Cannot override `Parent.my_property1` | 44 | def my_property1(self) -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.my_property1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:9:5 | @@ -171,7 +168,6 @@ info: `Parent.my_property1` is decorated with `@final`, forbidding overrides 10 | @property 11 | def my_property1(self) -> int: ... | ------------ `Parent.my_property1` defined here - | help: Remove the override of `my_property1` ``` @@ -182,7 +178,6 @@ error[override-of-final-method]: Cannot override `Parent.my_property2` | 46 | def my_property2(self) -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.my_property2` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:13:5 | @@ -190,7 +185,6 @@ info: `Parent.my_property2` is decorated with `@final`, forbidding overrides | ------ 14 | def my_property2(self) -> int: ... | ------------ `Parent.my_property2` defined here - | help: Remove the getter and setter for `my_property2` ``` @@ -201,7 +195,6 @@ error[override-of-final-method]: Cannot override `Parent.my_property3` | 50 | def my_property3(self) -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.my_property3` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:16:5 | @@ -209,7 +202,6 @@ info: `Parent.my_property3` is decorated with `@final`, forbidding overrides | ------ 17 | def my_property3(self) -> int: ... | ------------ `Parent.my_property3` defined here - | help: Remove the override of `my_property3` ``` @@ -220,7 +212,6 @@ error[override-of-final-method]: Cannot override `Parent.class_method1` | 54 | def class_method1(cls) -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.class_method1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:18:5 | @@ -229,7 +220,6 @@ info: `Parent.class_method1` is decorated with `@final`, forbidding overrides 19 | @classmethod 20 | def class_method1(cls) -> int: ... | ------------- `Parent.class_method1` defined here - | help: Remove the override of `class_method1` | 52 | def my_property3(self) -> None: ... @@ -248,7 +238,6 @@ error[override-of-final-method]: Cannot override `Parent.static_method1` | 56 | def static_method1() -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.static_method1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:24:5 | @@ -257,7 +246,6 @@ info: `Parent.static_method1` is decorated with `@final`, forbidding overrides 25 | @staticmethod 26 | def static_method1() -> int: ... | -------------- `Parent.static_method1` defined here - | help: Remove the override of `static_method1` | 54 | def class_method1(cls) -> int: ... # error: [override-of-final-method] @@ -276,7 +264,6 @@ error[override-of-final-method]: Cannot override `Parent.class_method2` | 58 | def class_method2(cls) -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.class_method2` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:22:5 | @@ -284,7 +271,6 @@ info: `Parent.class_method2` is decorated with `@final`, forbidding overrides | ------ 23 | def class_method2(cls) -> int: ... | ------------- `Parent.class_method2` defined here - | help: Remove the override of `class_method2` | 56 | def static_method1() -> int: ... # error: [override-of-final-method] @@ -303,7 +289,6 @@ error[override-of-final-method]: Cannot override `Parent.static_method2` | 60 | def static_method2() -> int: ... # error: [override-of-final-method] | ^^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.static_method2` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:28:5 | @@ -311,7 +296,6 @@ info: `Parent.static_method2` is decorated with `@final`, forbidding overrides | ------ 29 | def static_method2() -> int: ... | -------------- `Parent.static_method2` defined here - | help: Remove the override of `static_method2` | 58 | def class_method2(cls) -> int: ... # error: [override-of-final-method] @@ -335,7 +319,6 @@ error[invalid-method-override]: Invalid override of method `foo` | 8 | def foo(self): ... | --------- `Parent.foo` defined here - | info: `Grandchild.foo` is a staticmethod but `Parent.foo` is an instance method info: This violates the Liskov Substitution Principle @@ -347,7 +330,6 @@ error[override-of-final-method]: Cannot override `Parent.foo` | 75 | def foo(): ... | ^^^ Overrides a definition from superclass `Parent` - | info: `Parent.foo` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:7:5 | @@ -355,7 +337,6 @@ info: `Parent.foo` is decorated with `@final`, forbidding overrides | ------ 8 | def foo(self): ... | --- `Parent.foo` defined here - | help: Remove the override of `foo` | 71 | # concern of Liskov. @@ -376,7 +357,6 @@ error[override-of-final-method]: Cannot override `Parent.my_property1` | 79 | def my_property1(self) -> str: ... | ^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.my_property1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:9:5 | @@ -385,7 +365,6 @@ info: `Parent.my_property1` is decorated with `@final`, forbidding overrides 10 | @property 11 | def my_property1(self) -> int: ... | ------------ `Parent.my_property1` defined here - | help: Remove the override of `my_property1` ``` @@ -396,7 +375,6 @@ error[override-of-final-method]: Cannot override `Parent.class_method1` | 82 | class_method1 = None | ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent` - | info: `Parent.class_method1` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:18:5 | @@ -405,7 +383,6 @@ info: `Parent.class_method1` is decorated with `@final`, forbidding overrides 19 | @classmethod 20 | def class_method1(cls) -> int: ... | ------------- `Parent.class_method1` defined here - | help: Remove the override of `class_method1` ``` @@ -416,7 +393,6 @@ error[override-of-final-method]: Cannot override `Foo.bar` | 113 | def bar(self): ... # error: [override-of-final-method] | ^^^ Overrides a definition from superclass `Foo` - | info: `Foo.bar` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:91:5 | @@ -427,7 +403,6 @@ info: `Foo.bar` is decorated with `@final`, forbidding overrides | 110 | def bar(self): ... | --- `Foo.bar` defined here - | help: Remove the override of `bar` | 112 | class Baz(Foo): diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Diagnostic_edge_case\342\200\246_(2389d52c5ecfa2bd).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Diagnostic_edge_case\342\200\246_(2389d52c5ecfa2bd).snap" index 118991dde6..3e3f8481ac 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Diagnostic_edge_case\342\200\246_(2389d52c5ecfa2bd).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Diagnostic_edge_case\342\200\246_(2389d52c5ecfa2bd).snap" @@ -37,7 +37,6 @@ error[override-of-final-method]: Cannot override `module1.Foo.f` | 4 | def f(self): ... # error: [override-of-final-method] | ^ Overrides a definition from superclass `module1.Foo` - | info: `module1.Foo.f` is decorated with `@final`, forbidding overrides --> src/module1.py:4:5 | @@ -45,7 +44,6 @@ info: `module1.Foo.f` is decorated with `@final`, forbidding overrides | ------ 5 | def f(self): ... | - `module1.Foo.f` defined here - | help: Remove the override of `f` | 3 | class Foo(module1.Foo): diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Only_the_first_`@fin\342\200\246_(9863b583f4c651c5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Only_the_first_`@fin\342\200\246_(9863b583f4c651c5).snap" index 74f25111d2..47bd4c9ad4 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Only_the_first_`@fin\342\200\246_(9863b583f4c651c5).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Only_the_first_`@fin\342\200\246_(9863b583f4c651c5).snap" @@ -37,7 +37,6 @@ error[override-of-final-method]: Cannot override `A.f` | 9 | def f(self): ... # error: [override-of-final-method] | ^ Overrides a definition from superclass `A` - | info: `A.f` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:4:5 | @@ -45,7 +44,6 @@ info: `A.f` is decorated with `@final`, forbidding overrides | ------ 5 | def f(self): ... | - `A.f` defined here - | help: Remove the override of `f` | 7 | class B(A): @@ -64,7 +62,6 @@ error[override-of-final-method]: Cannot override `B.f` | 14 | def f(self): ... # error: [override-of-final-method] | ^ Overrides a definition from superclass `B` - | info: `B.f` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.py:8:5 | @@ -72,7 +69,6 @@ info: `B.f` is decorated with `@final`, forbidding overrides | ------ 9 | def f(self): ... # error: [override-of-final-method] | - `B.f` defined here - | help: Remove the override of `f` | 11 | class C(B): diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloaded_methods_d\342\200\246_(861757f48340ed92).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloaded_methods_d\342\200\246_(861757f48340ed92).snap" index 5bfde0fbfa..9fa15ee430 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloaded_methods_d\342\200\246_(861757f48340ed92).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloaded_methods_d\342\200\246_(861757f48340ed92).snap" @@ -135,7 +135,6 @@ error[override-of-final-method]: Cannot override `Good.bar` | 19 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] | ^^^ Overrides a definition from superclass `Good` - | info: `Good.bar` is decorated with `@final`, forbidding overrides --> src/stub.pyi:5:5 | @@ -143,7 +142,6 @@ info: `Good.bar` is decorated with `@final`, forbidding overrides | ------ 6 | def bar(self, x: str) -> str: ... | --- `Good.bar` defined here - | help: Remove all overloads for `bar` | 15 | class ChildOfGood(Good): @@ -165,7 +163,6 @@ error[override-of-final-method]: Cannot override `Good.baz` | 23 | def baz(self, x: int) -> int: ... # error: [override-of-final-method] | ^^^ Overrides a definition from superclass `Good` - | info: `Good.baz` is decorated with `@final`, forbidding overrides --> src/stub.pyi:9:5 | @@ -174,7 +171,6 @@ info: `Good.baz` is decorated with `@final`, forbidding overrides 10 | @overload 11 | def baz(self, x: str) -> str: ... | --- `Good.baz` defined here - | help: Remove all overloads for `baz` | 19 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] @@ -192,7 +188,7 @@ note: This is an unsafe fix and may change runtime behavior ``` error[invalid-overload]: `@final` decorator should be applied only to the first overload - --> src/stub.pyi:26:5 + --> src/stub.pyi:31:9 | 26 | / @overload 27 | | def bar(self, x: str) -> str: ... @@ -203,13 +199,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the first 30 | # error: [invalid-overload] 31 | def bar(self, x: int) -> int: ... | ^^^ - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the first overload - --> src/stub.pyi:32:5 + --> src/stub.pyi:37:9 | 32 | / @overload 33 | | def baz(self, x: str) -> str: ... @@ -220,7 +215,6 @@ error[invalid-overload]: `@final` decorator should be applied only to the first 36 | # error: [invalid-overload] 37 | def baz(self, x: int) -> int: ... | ^^^ - | ``` @@ -230,13 +224,11 @@ error[override-of-final-method]: Cannot override `Bad.bar` | 43 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] | ^^^ Overrides a definition from superclass `Bad` - | info: `Bad.bar` is decorated with `@final`, forbidding overrides --> src/stub.pyi:27:9 | 27 | def bar(self, x: str) -> str: ... | --- `Bad.bar` defined here - | help: Remove all overloads for `bar` | 39 | class ChildOfBad(Bad): @@ -258,13 +250,11 @@ error[override-of-final-method]: Cannot override `Bad.baz` | 47 | def baz(self, x: int) -> int: ... # error: [override-of-final-method] | ^^^ Overrides a definition from superclass `Bad` - | info: `Bad.baz` is decorated with `@final`, forbidding overrides --> src/stub.pyi:33:9 | 33 | def baz(self, x: str) -> str: ... | --- `Bad.baz` defined here - | help: Remove all overloads for `baz` | 43 | def bar(self, x: int) -> int: ... # error: [override-of-final-method] @@ -285,7 +275,6 @@ error[override-of-final-method]: Cannot override `Good.f` | 19 | def f(self, x: int | str) -> int | str: | ^ Overrides a definition from superclass `Good` - | info: `Good.f` is decorated with `@final`, forbidding overrides --> src/main.py:8:5 | @@ -293,7 +282,6 @@ info: `Good.f` is decorated with `@final`, forbidding overrides | ------ 9 | def f(self, x: int | str) -> int | str: | - `Good.f` defined here - | help: Remove all overloads and the implementation for `f` | 12 | class ChildOfGood(Good): @@ -316,7 +304,7 @@ note: This is an unsafe fix and may change runtime behavior ``` error[invalid-overload]: `@final` decorator should be applied only to the overload implementation - --> src/main.py:23:5 + --> src/main.py:25:9 | 23 | @overload | --------- @@ -328,13 +316,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the overlo 27 | def f(self, x: int) -> int: ... 28 | def f(self, x: int | str) -> int | str: | - Implementation defined here - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the overload implementation - --> src/main.py:31:5 + --> src/main.py:33:9 | 31 | @final | ------ @@ -346,13 +333,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the overlo 35 | def g(self, x: int) -> int: ... 36 | def g(self, x: int | str) -> int | str: | - Implementation defined here - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the overload implementation - --> src/main.py:41:5 + --> src/main.py:43:9 | 41 | @overload | --------- @@ -362,13 +348,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the overlo | ^ 44 | def h(self, x: int | str) -> int | str: | - Implementation defined here - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the overload implementation - --> src/main.py:49:5 + --> src/main.py:51:9 | 49 | @final | ------ @@ -378,7 +363,6 @@ error[invalid-overload]: `@final` decorator should be applied only to the overlo | ^ 52 | def i(self, x: int | str) -> int | str: | - Implementation defined here - | ``` @@ -388,13 +372,11 @@ error[override-of-final-method]: Cannot override `Bad.f` | 57 | f = None # error: [override-of-final-method] | ^ Overrides a definition from superclass `Bad` - | info: `Bad.f` is decorated with `@final`, forbidding overrides --> src/main.py:28:9 | 28 | def f(self, x: int | str) -> int | str: | - `Bad.f` defined here - | help: Remove the override of `f` ``` @@ -405,13 +387,11 @@ error[override-of-final-method]: Cannot override `Bad.g` | 58 | g = None # error: [override-of-final-method] | ^ Overrides a definition from superclass `Bad` - | info: `Bad.g` is decorated with `@final`, forbidding overrides --> src/main.py:36:9 | 36 | def g(self, x: int | str) -> int | str: | - `Bad.g` defined here - | help: Remove the override of `g` ``` @@ -422,13 +402,11 @@ error[override-of-final-method]: Cannot override `Bad.h` | 59 | h = None # error: [override-of-final-method] | ^ Overrides a definition from superclass `Bad` - | info: `Bad.h` is decorated with `@final`, forbidding overrides --> src/main.py:44:9 | 44 | def h(self, x: int | str) -> int | str: | - `Bad.h` defined here - | help: Remove the override of `h` ``` @@ -439,13 +417,11 @@ error[override-of-final-method]: Cannot override `Bad.i` | 60 | i = None # error: [override-of-final-method] | ^ Overrides a definition from superclass `Bad` - | info: `Bad.i` is decorated with `@final`, forbidding overrides --> src/main.py:52:9 | 52 | def i(self, x: int | str) -> int | str: | - `Bad.i` defined here - | help: Remove the override of `i` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloads_in_statica\342\200\246_(29a698d9deaf7318).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloads_in_statica\342\200\246_(29a698d9deaf7318).snap" index 48b157a182..4df9b8fbff 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloads_in_statica\342\200\246_(29a698d9deaf7318).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overloads_in_statica\342\200\246_(29a698d9deaf7318).snap" @@ -61,7 +61,6 @@ error[override-of-final-method]: Cannot override `Foo.method` | 31 | def method(self, x: str) -> str: ... # error: [override-of-final-method] | ^^^^^^ Overrides a definition from superclass `Foo` - | info: `Foo.method` is decorated with `@final`, forbidding overrides --> src/mdtest_snippet.pyi:7:9 | @@ -69,7 +68,6 @@ info: `Foo.method` is decorated with `@final`, forbidding overrides | ------ 8 | def method(self, x: int) -> int: ... | ------ `Foo.method` defined here - | help: Remove all overloads for `method` | 27 | class Bar(Foo): diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overriding_a_`@final\342\200\246_(c004aaab38745318).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overriding_a_`@final\342\200\246_(c004aaab38745318).snap" index ba4dbf7231..d4f5078b9d 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overriding_a_`@final\342\200\246_(c004aaab38745318).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/final.md_-_Tests_for_the_`@typi\342\200\246_-_Overriding_a_`@final\342\200\246_(c004aaab38745318).snap" @@ -44,7 +44,6 @@ error[override-of-final-method]: Cannot override `Base.method` | 5 | method = replacement_method # error: [override-of-final-method] | ^^^^^^ Overrides a definition from superclass `Base` - | info: `Base.method` is decorated with `@final`, forbidding overrides --> src/base.py:4:5 | @@ -52,7 +51,6 @@ info: `Base.method` is decorated with `@final`, forbidding overrides | ------ 5 | def method(self) -> None: ... | ------ `Base.method` defined here - | help: Remove the override of `method` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_PEP-484_convention_f\342\200\246_(ee99fadd6476677e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_PEP-484_convention_f\342\200\246_(ee99fadd6476677e).snap" index 420e29e2d4..5aa525fd6c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_PEP-484_convention_f\342\200\246_(ee99fadd6476677e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_PEP-484_convention_f\342\200\246_(ee99fadd6476677e).snap" @@ -89,90 +89,82 @@ error[positional-only-parameter-as-kwarg]: Positional-only parameter 1 (`__x`) p | 5 | f(__x=1) | ^^^^^ - | info: Function signature here --> src/mdtest_snippet.py:1:5 | 1 | def f(__x: int): ... | ^^^^^^^^^^^ - | ``` ``` warning[invalid-legacy-positional-parameter]: Invalid use of the legacy convention for positional-only parameters - --> src/mdtest_snippet.py:9:7 + --> src/mdtest_snippet.py:9:15 | 9 | def g(x: int, __y: str): ... | - ^^^ Parameter name begins with `__` but will not be treated as positional-only | | | Prior parameter here was positional-or-keyword - | info: A parameter can only be positional-only if it precedes all positional-or-keyword parameters ``` ``` warning[invalid-legacy-positional-parameter]: Invalid use of the legacy convention for positional-only parameters - --> src/mdtest_snippet.py:20:8 + --> src/mdtest_snippet.py:20:16 | 20 | def g2(x: int, __y: str): ... # error: [invalid-legacy-positional-parameter] | - ^^^ Parameter name begins with `__` but will not be treated as positional-only | | | Prior parameter here was positional-or-keyword - | info: A parameter can only be positional-only if it precedes all positional-or-keyword parameters ``` ``` warning[invalid-legacy-positional-parameter]: Invalid use of the legacy convention for positional-only parameters - --> src/mdtest_snippet.py:22:8 + --> src/mdtest_snippet.py:22:16 | 22 | def g2(x: str, __y: int): ... # error: [invalid-legacy-positional-parameter] | - ^^^ Parameter name begins with `__` but will not be treated as positional-only | | | Prior parameter here was positional-or-keyword - | info: A parameter can only be positional-only if it precedes all positional-or-keyword parameters ``` ``` warning[invalid-legacy-positional-parameter]: Invalid use of the legacy convention for positional-only parameters - --> src/mdtest_snippet.py:23:8 + --> src/mdtest_snippet.py:23:22 | 23 | def g2(x: str | int, __y: int | str): ... # error: [invalid-legacy-positional-parameter] | - ^^^ Parameter name begins with `__` but will not be treated as positional-only | | | Prior parameter here was positional-or-keyword - | info: A parameter can only be positional-only if it precedes all positional-or-keyword parameters ``` ``` warning[invalid-legacy-positional-parameter]: Invalid use of the legacy convention for positional-only parameters - --> src/mdtest_snippet.py:41:8 + --> src/mdtest_snippet.py:41:11 | 41 | def g4(a, __b): ... # error: [invalid-legacy-positional-parameter] | - ^^^ Parameter name begins with `__` but will not be treated as positional-only | | | Prior parameter here was positional-or-keyword - | info: A parameter can only be positional-only if it precedes all positional-or-keyword parameters ``` ``` warning[invalid-legacy-positional-parameter]: Invalid use of the legacy convention for positional-only parameters - --> src/mdtest_snippet.py:55:23 + --> src/mdtest_snippet.py:55:29 | 55 | def static_method(self, __x: int): ... # error: [invalid-legacy-positional-parameter] | ---- ^^^ Parameter name begins with `__` but will not be treated as positional-only | | | Prior parameter here was positional-or-keyword - | info: A parameter can only be positional-only if it precedes all positional-or-keyword parameters ``` @@ -183,13 +175,11 @@ error[positional-only-parameter-as-kwarg]: Positional-only parameter 2 (`__x`) p | 63 | C(42).method(__x=1) | ^^^^^ - | info: Method signature here --> src/mdtest_snippet.py:49:9 | 49 | def method(self, __x: int): ... | ^^^^^^^^^^^^^^^^^^^^^^ - | ``` @@ -199,12 +189,10 @@ error[positional-only-parameter-as-kwarg]: Positional-only parameter 2 (`__x`) p | 65 | C.class_method(__x="1") | ^^^^^^^ - | info: Method signature here --> src/mdtest_snippet.py:51:9 | 51 | def class_method(cls, __x: str): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_Wrong_argument_type_-_Diagnostics_for_unio\342\200\246_(5396a8f9e7f88f71).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_Wrong_argument_type_-_Diagnostics_for_unio\342\200\246_(5396a8f9e7f88f71).snap" index dd637fa1cf..3163e5ba02 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_Wrong_argument_type_-_Diagnostics_for_unio\342\200\246_(5396a8f9e7f88f71).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/function.md_-_Call_expression_-_Wrong_argument_type_-_Diagnostics_for_unio\342\200\246_(5396a8f9e7f88f71).snap" @@ -40,7 +40,6 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 14 | f(a) # error: [invalid-argument-type] | ^ Expected `Sized`, found `str | Foo` - | info: element `Foo` of union `str | Foo` is not assignable to `Sized` info: └── type `Foo` is not assignable to protocol `Sized` info: └── protocol member `__len__` is not defined on type `Foo` @@ -49,7 +48,6 @@ info: Function defined here | 7 | def f(x: Sized): ... | ^ -------- Parameter declared here - | ``` @@ -59,7 +57,6 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 15 | f(b) # error: [invalid-argument-type] | ^ Expected `Sized`, found `list[str] | str | dict[str, str] | ... omitted 5 union elements` - | info: element `Foo` of union `list[str] | str | dict[str, str] | ... omitted 5 union elements` is not assignable to `Sized` info: └── type `Foo` is not assignable to protocol `Sized` info: └── protocol member `__len__` is not defined on type `Foo` @@ -68,7 +65,6 @@ info: Function defined here | 7 | def f(x: Sized): ... | ^ -------- Parameter declared here - | ``` @@ -78,7 +74,6 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 16 | f(c) # error: [invalid-argument-type] | ^ Expected `Sized`, found `list[str] | str | dict[str, str] | ... omitted 6 union elements` - | info: element `Foo` of union `list[str] | str | dict[str, str] | ... omitted 6 union elements` is not assignable to `Sized` info: └── type `Foo` is not assignable to protocol `Sized` info: └── protocol member `__len__` is not defined on type `Foo` @@ -87,7 +82,6 @@ info: Function defined here | 7 | def f(x: Sized): ... | ^ -------- Parameter declared here - | ``` @@ -97,7 +91,6 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 17 | f(d) # error: [invalid-argument-type] | ^ Expected `Sized`, found `list[str] | str | dict[str, str] | ... omitted 7 union elements` - | info: element `Foo` of union `list[str] | str | dict[str, str] | ... omitted 7 union elements` is not assignable to `Sized` info: └── type `Foo` is not assignable to protocol `Sized` info: └── protocol member `__len__` is not defined on type `Foo` @@ -106,6 +99,5 @@ info: Function defined here | 7 | def f(x: Sized): ... | ^ -------- Parameter declared here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Builtins_with_implic\342\200\246_(4c3d127986a58f11).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Builtins_with_implic\342\200\246_(4c3d127986a58f11).snap" index 41ab66ec38..6e920e9688 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Builtins_with_implic\342\200\246_(4c3d127986a58f11).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_Builtins_with_implic\342\200\246_(4c3d127986a58f11).snap" @@ -70,7 +70,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to 7 | | str 8 | | ): ... | |_^ Bases `int` and `str` cannot be combined in multiple inheritance - | info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts --> src/mdtest_snippet.py:6:5 | @@ -78,7 +77,6 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension 7 | str | --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension - | ``` @@ -92,7 +90,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to 15 | | B, 16 | | ): ... | |_^ Bases `int` and `B` cannot be combined in multiple inheritance - | info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts --> src/mdtest_snippet.py:14:5 | @@ -100,7 +97,6 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | --- `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension 15 | B, | - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__` - | ``` @@ -114,7 +110,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to 21 | | str 22 | | ): ... | |_^ Bases `D` and `str` cannot be combined in multiple inheritance - | info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts --> src/mdtest_snippet.py:20:5 | @@ -125,7 +120,6 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension 21 | str | --- `str` instances have a distinct memory layout because of the way `str` is implemented in a C extension - | ``` @@ -135,7 +129,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to | 24 | class F(int, bytes, bytearray): ... # error: [instance-layout-conflict] | ^^^^^^^^^^^^^^^^^^^^^^^^ Bases `int`, `bytes` and `bytearray` cannot be combined in multiple inheritance - | info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts --> src/mdtest_snippet.py:24:9 | @@ -144,7 +137,6 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | | | | | `bytes` instances have a distinct memory layout because of the way `bytes` is implemented in a C extension | `int` instances have a distinct memory layout because of the way `int` is implemented in a C extension - | ``` @@ -154,7 +146,6 @@ error[invalid-typed-dict-header]: `@disjoint_base` cannot be used with `TypedDic | 31 | @disjoint_base # error: [invalid-typed-dict-header] "`@disjoint_base` cannot be used with `TypedDict` class `Movie`" | ^^^^^^^^^^^^^^ - | ``` @@ -164,7 +155,6 @@ error[invalid-protocol]: `@disjoint_base` cannot be used with protocol class `Su | 34 | @disjoint_base # error: [invalid-protocol] "`@disjoint_base` cannot be used with protocol class `SupportsClose`" | ^^^^^^^^^^^^^^ - | ``` @@ -178,7 +168,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to 39 | | H 40 | | ): ... | |_^ Bases `G` and `H` cannot be combined in multiple inheritance - | info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts --> src/mdtest_snippet.py:38:5 | @@ -186,7 +175,6 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | - `G` instances have a distinct memory layout because of the way `G` is implemented in a C extension 39 | H | - `H` instances have a distinct memory layout because of the way `H` is implemented in a C extension - | ``` @@ -199,7 +187,6 @@ error[invalid-generic-class]: Inconsistent type arguments for `Sequence` among c | | | | | Later class base inherits from `Sequence[str]` | Earlier class base inherits from `Sequence[int]` - | ``` @@ -209,6 +196,5 @@ error[subclass-of-final-class]: Class `Foo` cannot inherit from final class `ran | 43 | class Foo(range, str): ... # error: [subclass-of-final-class] | ^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_`__slots__`___incompa\342\200\246_(98b54233987eb654).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_`__slots__`___incompa\342\200\246_(98b54233987eb654).snap" index 16814de0fe..d1c6853a6c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_`__slots__`___incompa\342\200\246_(98b54233987eb654).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/instance_layout_conf\342\200\246_-_Tests_for_ty's_`inst\342\200\246_-_`__slots__`___incompa\342\200\246_(98b54233987eb654).snap" @@ -37,7 +37,6 @@ error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to 9 | | B, 10 | | ): ... | |_^ Bases `A` and `B` cannot be combined in multiple inheritance - | info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts --> src/mdtest_snippet.py:8:5 | @@ -45,6 +44,5 @@ info: Two classes cannot coexist in a class's MRO if their instances have incomp | - `A` instances have a distinct memory layout because `A` defines non-empty `__slots__` 9 | B, | - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_AST_nodes_that_are_o\342\200\246_(58a3839a9bc7026d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_AST_nodes_that_are_o\342\200\246_(58a3839a9bc7026d).snap" index 10f3407bcf..39dff0c499 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_AST_nodes_that_are_o\342\200\246_(58a3839a9bc7026d).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_AST_nodes_that_are_o\342\200\246_(58a3839a9bc7026d).snap" @@ -33,7 +33,6 @@ error[invalid-type-form]: Int literals are not allowed in this context in a para | 3 | a: 42, | ^^ Did you mean `typing.Literal[42]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -45,7 +44,6 @@ error[invalid-type-form]: Bytes literals are not allowed in this context in a pa | 5 | b: b"42", | ^^^^^ Did you mean `typing.Literal[b"42"]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -57,7 +55,6 @@ error[invalid-type-form]: Boolean literals are not allowed in this context in a | 7 | c: True, | ^^^^ Did you mean `typing.Literal[True]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -65,13 +62,12 @@ info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotat ``` error[invalid-syntax-in-forward-annotation]: Syntax error in forward annotation - --> src/mdtest_snippet.py:9:9 + --> src/mdtest_snippet.py:9:17 | 9 | d: "invalid syntax", | --------^^^^^^ | | | Unexpected token at the end of an expression - | help: Did you mean `typing.Literal["invalid syntax"]`? ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Dict-literal_or_set-\342\200\246_(15737b0beb194b0e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Dict-literal_or_set-\342\200\246_(15737b0beb194b0e).snap" index 1227652f2d..b0bbb1b6f8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Dict-literal_or_set-\342\200\246_(15737b0beb194b0e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Dict-literal_or_set-\342\200\246_(15737b0beb194b0e).snap" @@ -27,7 +27,6 @@ error[invalid-type-form]: Dict literals are not allowed in parameter annotations | 2 | x: {int: str}, # error: [invalid-type-form] | ^^^^^^^^^^ Did you mean `dict[int, str]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -39,7 +38,6 @@ error[invalid-type-form]: Set literals are not allowed in parameter annotations | 3 | y: {str}, # error: [invalid-type-form] | ^^^^^ Did you mean `set[str]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_List-literal_used_wh\342\200\246_(ba5cb09eaa3715d8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_List-literal_used_wh\342\200\246_(ba5cb09eaa3715d8).snap" index 77eeb58124..db6a0ecbd5 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_List-literal_used_wh\342\200\246_(ba5cb09eaa3715d8).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_List-literal_used_wh\342\200\246_(ba5cb09eaa3715d8).snap" @@ -33,7 +33,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a par | 2 | x: [int], # error: [invalid-type-form] | ^^^^^ Did you mean `list[int]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -45,7 +44,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a ret | 3 | ) -> [int]: # error: [invalid-type-form] | ^^^^^ Did you mean `list[int]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -57,7 +55,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a par | 8 | x: [int, str], # error: [invalid-type-form] | ^^^^^^^^^^ - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -69,7 +66,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a ret | 9 | ) -> [int, str]: # error: [invalid-type-form] | ^^^^^^^^^^ - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Module-literal_used_\342\200\246_(652fec4fd4a6c63a).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Module-literal_used_\342\200\246_(652fec4fd4a6c63a).snap" index 9ecce90a24..da667d81c7 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Module-literal_used_\342\200\246_(652fec4fd4a6c63a).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Module-literal_used_\342\200\246_(652fec4fd4a6c63a).snap" @@ -41,7 +41,6 @@ error[invalid-type-form]: Module `datetime` is not valid in a parameter annotati 3 | def f(x: datetime): ... # error: [invalid-type-form] | ^^^^^^^^ Did you mean to use the module's member `datetime.datetime`? | - | 2 | - def f(x: datetime): ... # error: [invalid-type-form] 3 + def f(x: datetime.datetime): ... # error: [invalid-type-form] @@ -57,7 +56,6 @@ error[invalid-type-form]: Module `PIL.Image` is not valid in a parameter annotat 3 | def g(x: Image): ... # error: [invalid-type-form] | ^^^^^ Did you mean to use the module's member `Image.Image`? | - | 2 | - def g(x: Image): ... # error: [invalid-type-form] 3 + def g(x: Image.Image): ... # error: [invalid-type-form] diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Special-cased_diagno\342\200\246_(a4b698196d337a3f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Special-cased_diagno\342\200\246_(a4b698196d337a3f).snap" index d575a1c0f9..06b07d4704 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Special-cased_diagno\342\200\246_(a4b698196d337a3f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Special-cased_diagno\342\200\246_(a4b698196d337a3f).snap" @@ -27,7 +27,6 @@ error[invalid-type-form]: Function `callable` is not valid in a parameter annota | 3 | def decorator(fn: callable) -> callable: | ^^^^^^^^ Did you mean `collections.abc.Callable`? - | ``` @@ -37,6 +36,5 @@ error[invalid-type-form]: Function `callable` is not valid in a return type anno | 3 | def decorator(fn: callable) -> callable: | ^^^^^^^^ Did you mean `collections.abc.Callable`? - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Tuple-literal_used_w\342\200\246_(f61204fc81905069).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Tuple-literal_used_w\342\200\246_(f61204fc81905069).snap" index 4a6890c747..68d261cc83 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Tuple-literal_used_w\342\200\246_(f61204fc81905069).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Diagnostics_for_comm\342\200\246_-_Tuple-literal_used_w\342\200\246_(f61204fc81905069).snap" @@ -35,7 +35,6 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a pa | 2 | x: (), # error: [invalid-type-form] | ^^ Did you mean `tuple[()]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -47,7 +46,6 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a re | 3 | ) -> (): # error: [invalid-type-form] | ^^ Did you mean `tuple[()]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -59,7 +57,6 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a pa | 6 | x: (int,), # error: [invalid-type-form] | ^^^^^^ Did you mean `tuple[int]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -71,7 +68,6 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a re | 7 | ) -> (int,): # error: [invalid-type-form] | ^^^^^^ Did you mean `tuple[int]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -83,7 +79,6 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a pa | 10 | x: (int, str), # error: [invalid-type-form] | ^^^^^^^^^^ Did you mean `tuple[int, str]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -95,7 +90,6 @@ error[invalid-type-form]: Tuple literals are not allowed in this context in a re | 11 | ) -> (int, str): # error: [invalid-type-form] | ^^^^^^^^^^ Did you mean `tuple[int, str]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Multiple_starred_exp\342\200\246_(3fbab22ead236138).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Multiple_starred_exp\342\200\246_(3fbab22ead236138).snap" index 374334e324..2ad9d772e7 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Multiple_starred_exp\342\200\246_(3fbab22ead236138).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid.md_-_Tests_for_invalid_ty\342\200\246_-_Multiple_starred_exp\342\200\246_(3fbab22ead236138).snap" @@ -53,7 +53,6 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - | ``` @@ -66,7 +65,6 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - | ``` @@ -79,7 +77,6 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - | ``` @@ -92,7 +89,6 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - | ``` @@ -105,7 +101,6 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - | ``` @@ -118,6 +113,5 @@ error[invalid-type-form]: Multiple unpacked variadic tuples are not allowed in a | | | | | Later unpacked variadic tuple | First unpacked variadic tuple - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Basic_(f15db7dc447d0795).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Basic_(f15db7dc447d0795).snap" index ac7a7f00dd..916e38516d 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Basic_(f15db7dc447d0795).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Basic_(f15db7dc447d0795).snap" @@ -30,7 +30,6 @@ error[invalid-await]: `Literal[1]` is not awaitable | 349 | class int: | --- type defined here - | info: `__await__` is missing ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_mis\342\200\246_(9ce1ee3cd1c9c8d1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_mis\342\200\246_(9ce1ee3cd1c9c8d1).snap" index a806e2605a..7e1ab9e6ec 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_mis\342\200\246_(9ce1ee3cd1c9c8d1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_mis\342\200\246_(9ce1ee3cd1c9c8d1).snap" @@ -33,7 +33,6 @@ error[invalid-await]: `MissingAwait` is not awaitable | 1 | class MissingAwait: | ------------ type defined here - | info: `__await__` is missing ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(a028edbafe180ca).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(a028edbafe180ca).snap" index ade20c220f..dddc76738f 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(a028edbafe180ca).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Custom_type_with_pos\342\200\246_(a028edbafe180ca).snap" @@ -37,7 +37,6 @@ error[invalid-await]: `PossiblyUnbound` is not awaitable | 5 | def __await__(self): | --------------- method defined here - | info: `__await__` may be missing ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Invalid_union_return\342\200\246_(fedf62ffaca0f2d7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Invalid_union_return\342\200\246_(fedf62ffaca0f2d7).snap" index ea978d87ed..3bddee84b7 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Invalid_union_return\342\200\246_(fedf62ffaca0f2d7).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Invalid_union_return\342\200\246_(fedf62ffaca0f2d7).snap" @@ -37,7 +37,6 @@ error[invalid-await]: `UnawaitableUnion` is not awaitable | 14 | await UnawaitableUnion() # error: [invalid-await] | ^^^^^^^^^^^^^^^^^^ - | info: `__await__` returns `Generator[Any, None, None] | int`, which is not a valid iterator ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(9db6c457a98cde25).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(9db6c457a98cde25).snap" index c339d15aa5..f9e68e6075 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(9db6c457a98cde25).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(9db6c457a98cde25).snap" @@ -39,7 +39,6 @@ error[invalid-await]: `HasBadAwait` is not awaitable | 2 | __await__ = 42 | --------- attribute defined here - | info: `__await__` is not callable ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(d78580fb6720e4ea).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(d78580fb6720e4ea).snap" index d05b772572..0dcb38a298 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(d78580fb6720e4ea).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Non-callable_`__awai\342\200\246_(d78580fb6720e4ea).snap" @@ -42,7 +42,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_awai ``` error[invalid-await]: `NonCallableAwait` is not awaitable - --> src/mdtest_snippet.py:2:5 + --> src/mdtest_snippet.py:5:11 | 2 | __await__ = 42 | --------- attribute defined here @@ -50,7 +50,6 @@ error[invalid-await]: `NonCallableAwait` is not awaitable 4 | async def main() -> None: 5 | await NonCallableAwait() # error: [invalid-await] | ^^^^^^^^^^^^^^^^^^ - | info: `__await__` is not callable ``` @@ -66,7 +65,6 @@ error[invalid-await]: `DeepInheritedNonCallableAwait` is not awaitable | 7 | __await__ = 42 | --------- attribute defined here - | info: `__await__` is not callable ``` @@ -77,7 +75,6 @@ error[invalid-await]: `A | B` is not awaitable | 23 | await x # error: [invalid-await] | ^ - | info: `__await__` is not callable ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Union_type_where_one\342\200\246_(ef7c2c0c8d9b1f0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Union_type_where_one\342\200\246_(ef7c2c0c8d9b1f0).snap" index 763a73ac3c..2b52cc1ef6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Union_type_where_one\342\200\246_(ef7c2c0c8d9b1f0).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Union_type_where_one\342\200\246_(ef7c2c0c8d9b1f0).snap" @@ -36,7 +36,6 @@ error[invalid-await]: `Awaitable | NotAwaitable` is not awaitable | 2 | def __await__(self): | --------------- method defined here - | info: `__await__` may be missing info: `NotAwaitable` does not implement `__await__` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(15b05c126b6ae968).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(15b05c126b6ae968).snap" index b59b837165..0a8188f067 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(15b05c126b6ae968).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(15b05c126b6ae968).snap" @@ -34,7 +34,6 @@ error[invalid-await]: `InvalidAwaitArgs` is not awaitable | 2 | def __await__(self, value: int): | ------------------ parameters here - | info: `__await__` requires arguments and cannot be called implicitly ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(ccb69f512135dd61).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(ccb69f512135dd61).snap" index 69d17b266a..66008853dd 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(ccb69f512135dd61).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_`__await__`_definiti\342\200\246_(ccb69f512135dd61).snap" @@ -34,7 +34,6 @@ error[invalid-await]: `InvalidAwaitReturn` is not awaitable | 2 | def __await__(self) -> int: | ---------------------- method defined here - | info: `__await__` returns `int`, which is not a valid iterator ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet\342\200\246_-_Invalid_Order_of_Leg\342\200\246_(eaa359e8d6b3031d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet\342\200\246_-_Invalid_Order_of_Leg\342\200\246_(eaa359e8d6b3031d).snap" index f8e178fed2..99ad2532ab 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet\342\200\246_-_Invalid_Order_of_Leg\342\200\246_(eaa359e8d6b3031d).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet\342\200\246_-_Invalid_Order_of_Leg\342\200\246_(eaa359e8d6b3031d).snap" @@ -68,7 +68,6 @@ error[invalid-generic-class]: Type parameters without defaults cannot follow typ 4 | 5 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | ``` @@ -90,7 +89,6 @@ error[invalid-generic-class]: Type parameters without defaults cannot follow typ 5 | T2 = TypeVar("T2") 6 | T3 = TypeVar("T3") | ------------------ `T3` defined here - | ``` @@ -111,7 +109,6 @@ error[invalid-generic-class]: Type parameters without defaults cannot follow typ 4 | 5 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | ``` @@ -132,7 +129,6 @@ error[invalid-generic-class]: Type parameters without defaults cannot follow typ 4 | 5 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | ``` @@ -142,7 +138,6 @@ error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` an | 32 | Protocol[T1, T2, DefaultStrT, T3], | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the type parameters from the `Protocol` base | 31 | # error: [invalid-generic-class] @@ -171,6 +166,5 @@ error[invalid-generic-class]: Type parameters without defaults cannot follow typ 4 | 5 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Boolean_parameters_m\342\200\246_(3edf97b20f58fa11).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Boolean_parameters_m\342\200\246_(3edf97b20f58fa11).snap" index f6d155f7e1..127f64be48 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Boolean_parameters_m\342\200\246_(3edf97b20f58fa11).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Boolean_parameters_m\342\200\246_(3edf97b20f58fa11).snap" @@ -36,7 +36,6 @@ error[invalid-legacy-type-variable]: The `covariant` parameter of `TypeVar` cann | 7 | T = TypeVar("T", covariant=cond()) | ^^^^^^ - | ``` @@ -46,7 +45,6 @@ error[invalid-legacy-type-variable]: The `contravariant` parameter of `TypeVar` | 10 | U = TypeVar("U", contravariant=cond()) | ^^^^^^ - | ``` @@ -56,6 +54,5 @@ error[invalid-legacy-type-variable]: The `infer_variance` parameter of `TypeVar` | 13 | V = TypeVar("V", infer_variance=cond()) | ^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_be_both_covar\342\200\246_(b7b0976739681470).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_be_both_covar\342\200\246_(b7b0976739681470).snap" index 95f919e749..954a6e2b46 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_be_both_covar\342\200\246_(b7b0976739681470).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_be_both_covar\342\200\246_(b7b0976739681470).snap" @@ -27,6 +27,5 @@ error[invalid-legacy-type-variable]: A `TypeVar` cannot be both covariant and co | 4 | T = TypeVar("T", covariant=True, contravariant=True) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_both_bou\342\200\246_(4ca5f13621915554).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_both_bou\342\200\246_(4ca5f13621915554).snap" index d225dc5f27..faa09184ff 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_both_bou\342\200\246_(4ca5f13621915554).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_both_bou\342\200\246_(4ca5f13621915554).snap" @@ -27,6 +27,5 @@ error[invalid-legacy-type-variable]: A `TypeVar` cannot have both a bound and co | 4 | T = TypeVar("T", int, str, bound=bytes) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_only_one\342\200\246_(8b0258f5188209c6).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_only_one\342\200\246_(8b0258f5188209c6).snap" index 1d57404085..177dd3cc20 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_only_one\342\200\246_(8b0258f5188209c6).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_only_one\342\200\246_(8b0258f5188209c6).snap" @@ -27,6 +27,5 @@ error[invalid-legacy-type-variable]: A `TypeVar` cannot have exactly one constra | 4 | T = TypeVar("T", int) | ^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_feature_for_\342\200\246_(72827c64b5c73d05).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_feature_for_\342\200\246_(72827c64b5c73d05).snap" index c9c31ef57f..38310e4d41 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_feature_for_\342\200\246_(72827c64b5c73d05).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_feature_for_\342\200\246_(72827c64b5c73d05).snap" @@ -27,6 +27,5 @@ error[invalid-legacy-type-variable]: The `default` parameter of `typing.TypeVar` | 4 | T = TypeVar("T", default=int) | ^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_keyword_argu\342\200\246_(39164266ada3dc2f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_keyword_argu\342\200\246_(39164266ada3dc2f).snap" index fb335e4fae..6cac2b9e9c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_keyword_argu\342\200\246_(39164266ada3dc2f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_keyword_argu\342\200\246_(39164266ada3dc2f).snap" @@ -27,6 +27,5 @@ error[invalid-legacy-type-variable]: Unknown keyword argument `invalid_keyword` | 4 | T = TypeVar("T", invalid_keyword=True) | ^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_be_directly_ass\342\200\246_(c2e3e46852bb268f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_be_directly_ass\342\200\246_(c2e3e46852bb268f).snap" index 359c8f9eda..8ce6f006e0 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_be_directly_ass\342\200\246_(c2e3e46852bb268f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_be_directly_ass\342\200\246_(c2e3e46852bb268f).snap" @@ -31,7 +31,6 @@ error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple var | 5 | U: TypeVar = TypeVar("U") | ^^^^^^^^^^^^ - | ``` @@ -41,6 +40,5 @@ error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple var | 8 | tuple_with_typevar = ("foo", TypeVar("W")) | ^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_have_a_name_(79a4ce09338e666b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_have_a_name_(79a4ce09338e666b).snap" index d24e221540..14a0c408e7 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_have_a_name_(79a4ce09338e666b).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_have_a_name_(79a4ce09338e666b).snap" @@ -27,6 +27,5 @@ error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` is requir | 4 | T = TypeVar() | ^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_not_be_redefine\342\200\246_(b1be57970f924722).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_not_be_redefine\342\200\246_(b1be57970f924722).snap" index 91c84bbc35..61620f6628 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_not_be_redefine\342\200\246_(b1be57970f924722).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_not_be_redefine\342\200\246_(b1be57970f924722).snap" @@ -25,7 +25,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typev ``` error[invalid-legacy-type-variable]: Cannot redefine `T` as a type variable - --> src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:6:1 | 3 | T = TypeVar("T") | - Previously defined here @@ -33,6 +33,5 @@ error[invalid-legacy-type-variable]: Cannot redefine `T` as a type variable 5 | # error: [invalid-legacy-type-variable] 6 | T = TypeVar("T") | ^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Name_can't_be_given_\342\200\246_(8f6aed0dba79e995).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Name_can't_be_given_\342\200\246_(8f6aed0dba79e995).snap" index 340c9ce49b..83a517ddae 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Name_can't_be_given_\342\200\246_(8f6aed0dba79e995).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Name_can't_be_given_\342\200\246_(8f6aed0dba79e995).snap" @@ -27,6 +27,5 @@ error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` can only | 4 | T = TypeVar("T", name="T") | ^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_No_variadic_argument\342\200\246_(9d57505425233fd8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_No_variadic_argument\342\200\246_(9d57505425233fd8).snap" index e998cbb5e7..a114575df7 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_No_variadic_argument\342\200\246_(9d57505425233fd8).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_No_variadic_argument\342\200\246_(9d57505425233fd8).snap" @@ -32,7 +32,6 @@ error[invalid-legacy-type-variable]: Starred arguments are not supported in `Typ | 6 | T = TypeVar("T", *types) | ^^^^^^ - | ``` @@ -42,6 +41,5 @@ error[invalid-legacy-type-variable]: Starred arguments are not supported in `Typ | 9 | S = TypeVar("S", **{"bound": int}) | ^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_`TypeVar`_parameter_\342\200\246_(8424f2b8bc4351f9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_`TypeVar`_parameter_\342\200\246_(8424f2b8bc4351f9).snap" index 6e440a5f14..e6e4e1d4a9 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_`TypeVar`_parameter_\342\200\246_(8424f2b8bc4351f9).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_`TypeVar`_parameter_\342\200\246_(8424f2b8bc4351f9).snap" @@ -27,6 +27,5 @@ warning[mismatched-type-name]: The name passed to `TypeVar` must match the varia | 4 | T = TypeVar("Q") | ^^^ Expected "T", got "Q" - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument_par\342\200\246_-_Missing_argument_for\342\200\246_(b632d61c1d75f9fb).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument_par\342\200\246_-_Missing_argument_for\342\200\246_(b632d61c1d75f9fb).snap" index c24f7e5519..5d14c8d9d8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument_par\342\200\246_-_Missing_argument_for\342\200\246_(b632d61c1d75f9fb).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument_par\342\200\246_-_Missing_argument_for\342\200\246_(b632d61c1d75f9fb).snap" @@ -31,7 +31,6 @@ error[missing-argument]: No arguments provided for required parameters `*args`, | 5 | func() # error: [missing-argument] | ^^^^^^ - | info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime ``` @@ -42,7 +41,6 @@ error[missing-argument]: No argument provided for required parameter `**kwargs` | 6 | func(*args) # error: [missing-argument] | ^^^^^^^^^^^ - | info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime ``` @@ -53,7 +51,6 @@ error[missing-argument]: No argument provided for required parameter `*args` | 7 | func(**kwargs) # error: [missing-argument] | ^^^^^^^^^^^^^^ - | info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Inline_tuple-literal\342\200\246_(4ee237f49e7ac736).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Inline_tuple-literal\342\200\246_(4ee237f49e7ac736).snap" index dc192bc259..12071a81a8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Inline_tuple-literal\342\200\246_(4ee237f49e7ac736).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Inline_tuple-literal\342\200\246_(4ee237f49e7ac736).snap" @@ -30,7 +30,6 @@ error[duplicate-base]: Duplicate base class `int` | ^^^^^^^^^^^^^^^^^^^^^^^^^ --- ^^^ Class `int` later repeated here | | | Class `int` first included in bases list here - | info: Definition of class `InlineTupleDuplicateBases` will raise `TypeError` at runtime ``` @@ -41,7 +40,6 @@ error[invalid-base]: Invalid class base with type `Literal[1]` | 5 | class InlineTupleInvalidBases(*(int, 1)): ... | ^ - | info: Definition of class `InlineTupleInvalidBases` will raise `TypeError` at runtime ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Unresolvable_MROs_in\342\200\246_(e2b355c09a967862).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Unresolvable_MROs_in\342\200\246_(e2b355c09a967862).snap" index c3824309d7..353c2c160a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Unresolvable_MROs_in\342\200\246_(e2b355c09a967862).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_Unresolvable_MROs_in\342\200\246_(e2b355c09a967862).snap" @@ -30,6 +30,5 @@ error[inconsistent-mro]: Cannot create a consistent method resolution order (MRO | 7 | class Baz(Protocol[T], Foo, Bar[T]): ... # error: [inconsistent-mro] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_includes\342\200\246_(d2532518c44112c8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_includes\342\200\246_(d2532518c44112c8).snap" index fae0fef2f0..3577953f28 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_includes\342\200\246_(d2532518c44112c8).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_includes\342\200\246_(d2532518c44112c8).snap" @@ -51,7 +51,6 @@ warning[unsupported-base]: Unsupported class base | 17 | class Foo(x): ... | ^ Has type ` | ` - | info: ty cannot resolve a consistent method resolution order (MRO) for class `Foo` due to this base info: Only class objects or `Any` are supported as class bases @@ -63,7 +62,6 @@ warning[unsupported-base]: Unsupported class base | 28 | class D(C): ... # error: [unsupported-base] | ^ Has type `.C @ src/mdtest_snippet.py:23:15'> | .C @ src/mdtest_snippet.py:26:15'>` - | info: ty cannot resolve a consistent method resolution order (MRO) for class `D` due to this base info: Only class objects or `Any` are supported as class bases diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_th\342\200\246_(6f8d0bf648c4b305).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_th\342\200\246_(6f8d0bf648c4b305).snap" index 2f460e3e2c..b9469efc22 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_th\342\200\246_(6f8d0bf648c4b305).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_th\342\200\246_(6f8d0bf648c4b305).snap" @@ -47,7 +47,6 @@ error[invalid-base]: Invalid class base with type `Literal[2]` | 1 | class Foo(2): ... # error: [invalid-base] | ^ - | info: Definition of class `Foo` will raise `TypeError` at runtime ``` @@ -58,7 +57,6 @@ warning[unsupported-base]: Unsupported class base | 6 | class Bar(Foo()): ... # error: [unsupported-base] | ^^^^^ Has type `Foo` - | info: ty cannot resolve a consistent method resolution order (MRO) for class `Bar` due to this base info: Only class objects or `Any` are supported as class bases @@ -70,7 +68,6 @@ error[invalid-base]: Invalid class base with type `Bad1` | 15 | class BadSub1(Bad1()): ... # error: [invalid-base] | ^^^^^^ - | info: Definition of class `BadSub1` will raise `TypeError` at runtime info: An instance type is only a valid class base if it has a valid `__mro_entries__` method info: Type `Bad1` has an `__mro_entries__` method, but it cannot be called with the expected arguments @@ -84,7 +81,6 @@ error[invalid-base]: Invalid class base with type `Bad2` | 16 | class BadSub2(Bad2()): ... # error: [invalid-base] | ^^^^^^ - | info: Definition of class `BadSub2` will raise `TypeError` at runtime info: An instance type is only a valid class base if it has a valid `__mro_entries__` method info: Type `Bad2` has an `__mro_entries__` method, but it does not return a tuple of types @@ -97,7 +93,6 @@ error[invalid-base]: Invalid class base with type `HasMroEntries | NoMroEntries` | 24 | class Foo(base): ... # error: [invalid-base] | ^^^^ - | info: Definition of class `Foo` will raise `TypeError` at runtime info: An instance type is only a valid class base if it has a valid `__mro_entries__` method info: Type `HasMroEntries | NoMroEntries` may have an `__mro_entries__` attribute, but it may be missing diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_wi\342\200\246_(ea7ebc83ec359b54).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_wi\342\200\246_(ea7ebc83ec359b54).snap" index 6ed04adbfb..4cdfd9adc0 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_wi\342\200\246_(ea7ebc83ec359b54).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/mro.md_-_Method_Resolution_Or\342\200\246_-_`__bases__`_lists_wi\342\200\246_(ea7ebc83ec359b54).snap" @@ -91,7 +91,6 @@ error[duplicate-base]: Duplicate base class `str` | ^^^ --- ^^^ Class `str` later repeated here | | | Class `str` first included in bases list here - | info: Definition of class `Foo` will raise `TypeError` at runtime ``` @@ -110,7 +109,6 @@ error[duplicate-base]: Duplicate base class `Eggs` 21 | Spam, 22 | Eggs, | ^^^^ Class `Eggs` later repeated here - | info: Definition of class `Ham` will raise `TypeError` at runtime ``` @@ -128,7 +126,6 @@ error[duplicate-base]: Duplicate base class `Spam` 20 | Baz, 21 | Spam, | ^^^^ Class `Spam` later repeated here - | info: Definition of class `Ham` will raise `TypeError` at runtime ``` @@ -141,7 +138,6 @@ error[duplicate-base]: Duplicate base class `Mushrooms` | ^^^^^^^^ --------- ^^^^^^^^^ Class `Mushrooms` later repeated here | | | Class `Mushrooms` first included in bases list here - | info: Definition of class `Omelette` will raise `TypeError` at runtime ``` @@ -165,7 +161,6 @@ error[duplicate-base]: Duplicate base class `Eggs` 45 | Baz, 46 | Eggs, | ^^^^ Class `Eggs` later repeated here - | info: Definition of class `VeryEggyOmelette` will raise `TypeError` at runtime ``` @@ -180,7 +175,6 @@ error[duplicate-base]: Duplicate base class `A` | - Class `A` first included in bases list here 61 | A | ^ Class `A` later repeated here - | info: Definition of class `C` will raise `TypeError` at runtime ``` @@ -191,7 +185,6 @@ warning[unused-type-ignore-comment]: Unused `type: ignore` directive | 63 | ): # type: ignore[ty:duplicate-base] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 62 | # error: [unused-type-ignore-comment] diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_Edge_case___multiple_\342\200\246_(f30babd05c89dce9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_Edge_case___multiple_\342\200\246_(f30babd05c89dce9).snap" index 1dce9a6fbc..1c4e2074c6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_Edge_case___multiple_\342\200\246_(f30babd05c89dce9).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_Edge_case___multiple_\342\200\246_(f30babd05c89dce9).snap" @@ -37,7 +37,6 @@ error[invalid-named-tuple]: NamedTuple field name cannot start with an underscor | 8 | _asdict: bool # error: [invalid-named-tuple] "NamedTuple field `_asdict` cannot start with an underscore" | ^^^^^^^^^^^^^ Class definition will raise `TypeError` at runtime due to this field - | ``` @@ -47,7 +46,6 @@ error[invalid-named-tuple]: Cannot overwrite NamedTuple attribute `_asdict` | 14 | _asdict = True | ^^^^^^^ - | info: This will cause the class creation to fail at runtime ``` @@ -58,7 +56,6 @@ error[invalid-named-tuple]: Cannot overwrite NamedTuple attribute `_asdict` | 14 | _asdict = True | ^^^^^^^ - | info: This will cause the class creation to fail at runtime ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_NamedTuples_cannot_h\342\200\246_(e2ed186fe2b2fc35).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_NamedTuples_cannot_h\342\200\246_(e2ed186fe2b2fc35).snap" index 00c621984e..84df168350 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_NamedTuples_cannot_h\342\200\246_(e2ed186fe2b2fc35).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_NamedTuples_cannot_h\342\200\246_(e2ed186fe2b2fc35).snap" @@ -51,7 +51,6 @@ error[invalid-named-tuple]: NamedTuple field name cannot start with an underscor | 5 | _bar: int | ^^^^^^^^^ Class definition will raise `TypeError` at runtime due to this field - | ``` @@ -61,7 +60,6 @@ error[invalid-named-tuple]: Field name `_x` in `NamedTuple()` cannot start with | 15 | Underscore = NamedTuple("Underscore", [("_x", int), ("y", str)]) | ^^^^^^^^^^^^^^^^^^^^^^^^^ Will raise `ValueError` at runtime - | ``` @@ -71,7 +69,6 @@ error[invalid-named-tuple]: Field name `class` in `NamedTuple()` cannot be a Pyt | 19 | Keyword = NamedTuple("Keyword", [("x", int), ("class", str)]) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Will raise `ValueError` at runtime - | ``` @@ -81,7 +78,6 @@ error[invalid-named-tuple]: Duplicate field name `x` in `NamedTuple()` | 23 | Duplicate = NamedTuple("Duplicate", [("x", int), ("y", str), ("x", float)]) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Field `x` already defined; will raise `ValueError` at runtime - | ``` @@ -91,6 +87,5 @@ error[invalid-named-tuple]: Field name `not valid` in `NamedTuple()` is not a va | 27 | Invalid = NamedTuple("Invalid", [("not valid", int), ("ok", str)]) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Will raise `ValueError` at runtime - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Definition_(bbf79630502e65e9).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Definition_(bbf79630502e65e9).snap index 441d9f1d73..57be141c45 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Definition_(bbf79630502e65e9).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Definition_(bbf79630502e65e9).snap @@ -41,20 +41,19 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/named_tuple.md ``` error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s) - --> src/mdtest_snippet.py:4:5 + --> src/mdtest_snippet.py:6:5 | 4 | altitude: float = 0.0 | --------------------- Earlier field `altitude` defined here with a default value 5 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `latitud… 6 | latitude: float | ^^^^^^^^^^^^^^^ Field `latitude` defined here without a default value - | ``` ``` error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s) - --> src/mdtest_snippet.py:4:5 + --> src/mdtest_snippet.py:8:5 | 4 | altitude: float = 0.0 | --------------------- Earlier field `altitude` defined here with a default value @@ -63,32 +62,29 @@ error[invalid-named-tuple]: NamedTuple field without default value cannot follow 7 | # error: [invalid-named-tuple] "NamedTuple field without default value cannot follow field(s) with default value(s): Field `longitu… 8 | longitude: float | ^^^^^^^^^^^^^^^^ Field `longitude` defined here without a default value - | ``` ``` error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s) - --> src/mdtest_snippet.py:14:5 + --> src/mdtest_snippet.py:15:5 | 14 | altitude: float = 0.0 | --------------------- Earlier field `altitude` defined here with a default value 15 | latitude: float # error: [invalid-named-tuple] | ^^^^^^^^^^^^^^^ Field `latitude` defined here without a default value - | ``` ``` error[invalid-named-tuple]: NamedTuple field without default value cannot follow field(s) with default value(s) - --> src/mdtest_snippet.py:14:5 + --> src/mdtest_snippet.py:16:5 | 14 | altitude: float = 0.0 | --------------------- Earlier field `altitude` defined here with a default value 15 | latitude: float # error: [invalid-named-tuple] 16 | longitude: float # error: [invalid-named-tuple] | ^^^^^^^^^^^^^^^^ Field `longitude` defined here without a default value - | ``` @@ -98,7 +94,6 @@ error[invalid-named-tuple]: NamedTuple field without default value cannot follow | 20 | latitude: float # error: [invalid-named-tuple] | ^^^^^^^^^^^^^^^ Field `latitude` defined here without a default value - | info: Earlier field `altitude` was defined with a default value ``` @@ -109,7 +104,6 @@ error[invalid-named-tuple]: NamedTuple field without default value cannot follow | 21 | longitude: float # error: [invalid-named-tuple] | ^^^^^^^^^^^^^^^^ Field `longitude` defined here without a default value - | info: Earlier field `altitude` was defined with a default value ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Multiple_Inheritance_(82ed33d1b3b433d8).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Multiple_Inheritance_(82ed33d1b3b433d8).snap index e184e4e373..89fb704a2a 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Multiple_Inheritance_(82ed33d1b3b433d8).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Multiple_Inheritance_(82ed33d1b3b433d8).snap @@ -57,7 +57,6 @@ error[invalid-named-tuple]: NamedTuple class `C` cannot use multiple inheritance | 4 | class C(NamedTuple, object): | ^^^^^^ - | ``` @@ -67,7 +66,6 @@ error[invalid-named-tuple]: NamedTuple class `D` cannot use multiple inheritance | 10 | int, # error: [invalid-named-tuple] | ^^^ - | ``` @@ -77,6 +75,5 @@ error[invalid-named-tuple]: NamedTuple class `E` cannot use multiple inheritance | 17 | class E(NamedTuple, Protocol): ... | ^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Name_mismatch_diagno\342\200\246_(8ca723b970e370d0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Name_mismatch_diagno\342\200\246_(8ca723b970e370d0).snap" index b40529677c..3912a521a6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Name_mismatch_diagno\342\200\246_(8ca723b970e370d0).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/named_tuple.md_-_`NamedTuple`_-_`typing.NamedTuple`_-_Name_mismatch_diagno\342\200\246_(8ca723b970e370d0).snap" @@ -30,6 +30,5 @@ warning[mismatched-type-name]: The name passed to `NamedTuple` must match the va | 5 | Mismatch = NamedTuple("WrongName", [("x", int)]) | ^^^^^^^^^^^ Expected "Mismatch", got "WrongName" - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_class_constructor_\342\200\246_(dd9f8a8f736a329).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_class_constructor_\342\200\246_(dd9f8a8f736a329).snap" index 4657987302..914ada6c79 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_class_constructor_\342\200\246_(dd9f8a8f736a329).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_class_constructor_\342\200\246_(dd9f8a8f736a329).snap" @@ -24,7 +24,6 @@ error[no-matching-overload]: No overload of class `type` matches arguments | 1 | type() # error: [no-matching-overload] | ^^^^^^ - | help: `builtins.type()` can either be called with one or three positional arguments (got 0) ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_method_call_with_u\342\200\246_(31cb5f881221158e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_method_call_with_u\342\200\246_(31cb5f881221158e).snap" index d9ec2d4d0a..892d856520 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_method_call_with_u\342\200\246_(31cb5f881221158e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_A_method_call_with_u\342\200\246_(31cb5f881221158e).snap" @@ -35,14 +35,12 @@ error[no-matching-overload]: No overload of bound method `Foo.bar` matches argum | 12 | foo.bar(b"wat") # error: [no-matching-overload] | ^^^^^^^^^^^^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:4:5 | 4 | / @overload 5 | | def bar(self, x: int) -> int: ... | |_____________________________________^ First overload defined here - | info: Possible overloads for bound method `bar`: info: (self, x: int) -> int info: (self, x: str) -> str @@ -51,6 +49,5 @@ info: Overload implementation defined here | 8 | def bar(self, x: int | str) -> int | str: | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_An_explicit_`__get__\342\200\246_(9ecd21d0927ee1ff).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_An_explicit_`__get__\342\200\246_(9ecd21d0927ee1ff).snap" index 27e1354f70..d7aab53c57 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_An_explicit_`__get__\342\200\246_(9ecd21d0927ee1ff).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_An_explicit_`__get__\342\200\246_(9ecd21d0927ee1ff).snap" @@ -35,14 +35,12 @@ error[no-matching-overload]: No overload of method wrapper `__get__` of function | 12 | f.__get__() # error: [no-matching-overload] | ^^^^^^^^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:3:1 | 3 | / @overload 4 | | def f(x: int) -> int: ... | |_________________________^ First overload defined here - | info: Possible overloads for method wrapper `__get__` of function `f`: info: (x: int) -> int info: (x: str) -> str @@ -52,6 +50,5 @@ info: Overload implementation defined here | 9 | def f(x: int | str | bytes) -> int | str | bytes: | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(dd80c593d9136f35).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(dd80c593d9136f35).snap" index 48bd14faf8..a5452d1ef7 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(dd80c593d9136f35).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(dd80c593d9136f35).snap" @@ -72,14 +72,12 @@ error[no-matching-overload]: No overload of function `foo` matches arguments | 49 | foo(Foo(), Foo()) # error: [no-matching-overload] | ^^^^^^^^^^^^^^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:5:1 | 5 | / @overload 6 | | def foo(a: int, b: int, c: int): ... | |____________________________________^ First overload defined here - | info: Possible overloads for function `foo`: info: (a: int, b: int, c: int) -> Unknown info: (a: str, b: int, c: int) -> Unknown @@ -107,6 +105,5 @@ info: Overload implementation defined here | 47 | def foo(a, b, c): ... | ^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(f66e3a8a3977c472).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(f66e3a8a3977c472).snap" index fc4dfeafb6..8d1907ec94 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(f66e3a8a3977c472).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(f66e3a8a3977c472).snap" @@ -152,14 +152,12 @@ error[no-matching-overload]: No overload of function `foo` matches arguments | 129 | foo(Foo(), Foo()) # error: [no-matching-overload] | ^^^^^^^^^^^^^^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:5:1 | 5 | / @overload 6 | | def foo(a: int, b: int, c: int): ... | |____________________________________^ First overload defined here - | info: Possible overloads for function `foo`: info: (a: int, b: int, c: int) -> Unknown info: (a: str, b: int, c: int) -> Unknown @@ -217,6 +215,5 @@ info: Overload implementation defined here | 127 | def foo(a, b, c): ... | ^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(3553d085684e16a0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(3553d085684e16a0).snap" index 63d8843741..91fc4c1e35 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(3553d085684e16a0).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(3553d085684e16a0).snap" @@ -33,14 +33,12 @@ error[no-matching-overload]: No overload of function `f` matches arguments | 10 | f(b"foo") # error: [no-matching-overload] | ^^^^^^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:3:1 | 3 | / @overload 4 | | def f(x: int) -> int: ... | |_________________________^ First overload defined here - | info: Possible overloads for function `f`: info: (x: int) -> int info: (x: str) -> str @@ -49,6 +47,5 @@ info: Overload implementation defined here | 7 | def f(x: int | str) -> int | str: | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(36814b28492c01d2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(36814b28492c01d2).snap" index e641681188..ac0bde43f0 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(36814b28492c01d2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Calls_to_overloaded_\342\200\246_(36814b28492c01d2).snap" @@ -84,7 +84,6 @@ error[no-matching-overload]: No overload of function `f` matches arguments | 61 | f(b"foo") # error: [no-matching-overload] | ^^^^^^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:3:1 | @@ -108,7 +107,6 @@ info: First overload defined here 20 | | hyena: int, 21 | | ) -> int: ... | |_____________^ First overload defined here - | info: Possible overloads for function `f`: info: (lion: int, turtle: int, tortoise: int, goat: int, capybara: int, chicken: int, ostrich: int, gorilla: int, giraffe: int, condor: int, kangaroo: int, anaconda: int, tarantula: int, millipede: int, leopard: int, hyena: int) -> int info: (lion: str, turtle: str, tortoise: str, goat: str, capybara: str, chicken: str, ostrich: str, gorilla: str, giraffe: str, condor: str, kangaroo: str, anaconda: str, tarantula: str, millipede: str, leopard: str, hyena: str) -> str @@ -135,6 +133,5 @@ info: Overload implementation defined here 57 | | hyena: int | str, 58 | | ) -> int | str: | |______________^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloa\342\200\246_(84dadf8abd8f2f2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloa\342\200\246_(84dadf8abd8f2f2).snap" index f5cac57e3e..be70aba4af 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloa\342\200\246_(84dadf8abd8f2f2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_At_least_two_overloa\342\200\246_(84dadf8abd8f2f2).snap" @@ -36,26 +36,24 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md ``` error[invalid-overload]: Overloaded function `func` requires at least two overloads - --> src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:5:5 | 3 | @overload | --------- 4 | # error: [invalid-overload] 5 | def func(x: int) -> int: ... | ^^^^ Only one overload defined here - | ``` ``` error[invalid-overload]: Overloaded function `func` requires at least two overloads - --> src/mdtest_snippet.pyi:3:1 + --> 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/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@classmethod`_(aaa04d4cfa3adaba).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@classmethod`_(aaa04d4cfa3adaba).snap" index 5d1efa29cd..6d5aa76712 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@classmethod`_(aaa04d4cfa3adaba).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@classmethod`_(aaa04d4cfa3adaba).snap" @@ -95,7 +95,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md ``` error[invalid-overload]: Overloaded function `try_from1` does not use the `@classmethod` decorator consistently - --> src/mdtest_snippet.py:12:5 + --> src/mdtest_snippet.py:16:9 | 12 | @overload | --------- @@ -105,7 +105,6 @@ error[invalid-overload]: Overloaded function `try_from1` does not use the `@clas 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: | ^^^^^^^^^ - | ``` @@ -122,7 +121,6 @@ error[invalid-overload]: Overloaded function `try_from2` does not use the `@clas | --------- 22 | def try_from2(cls, x: int) -> CheckClassMethod: ... | --------- Missing here - | ``` @@ -131,10 +129,9 @@ error[invalid-overload]: Overloaded function `try_from3` does not use the `@clas --> src/mdtest_snippet.py:40:9 | 40 | def try_from3(cls, x: int | str) -> CheckClassMethod | None: - | --------- + | ^^^^^^^^^ | | | Missing here - | ``` @@ -144,19 +141,17 @@ error[call-non-callable]: Object of type `CheckClassMethod` is not callable | 43 | return cls(x) | ^^^^^^ - | ``` ``` error[invalid-assignment]: Object of type `bound method .from_value(x: int) -> int` is not assignable to `(str, /) -> str` - --> src/mdtest_snippet.py:76:6 + --> src/mdtest_snippet.py:76:29 | 76 | bad: Callable[[str], str] = Base.from_value | -------------------- ^^^^^^^^^^^^^^^ Incompatible value of type `bound method .from_value(x: int) -> int` | | | Declared type - | info: incompatible return types: `int` is not assignable to `str` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@final`_(f8e529ec23a61665).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@final`_(f8e529ec23a61665).snap" index bcade81649..91268b403d 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@final`_(f8e529ec23a61665).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@final`_(f8e529ec23a61665).snap" @@ -76,7 +76,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md ``` error[invalid-overload]: `@final` decorator should be applied only to the overload implementation - --> src/mdtest_snippet.py:12:5 + --> src/mdtest_snippet.py:15:9 | 12 | @overload | --------- @@ -89,13 +89,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the overlo 17 | def method2(self, x: str) -> str: ... 18 | def method2(self, x: int | str) -> int | str: | ------- Implementation defined here - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the overload implementation - --> src/mdtest_snippet.py:23:5 + --> src/mdtest_snippet.py:26:9 | 23 | @overload | --------- @@ -106,13 +105,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the overlo | ^^^^^^^ 27 | def method3(self, x: int | str) -> int | str: | ------- Implementation defined here - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the first overload - --> src/mdtest_snippet.pyi:9:5 + --> src/mdtest_snippet.pyi:14:9 | 9 | / @overload 10 | | def method2(self, x: int) -> int: ... @@ -123,13 +121,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the first 13 | # error: [invalid-overload] 14 | def method2(self, x: str) -> str: ... | ^^^^^^^ - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the first overload - --> src/mdtest_snippet.pyi:15:5 + --> src/mdtest_snippet.pyi:19:9 | 15 | / @overload 16 | | def method3(self, x: int) -> int: ... @@ -139,13 +136,12 @@ error[invalid-overload]: `@final` decorator should be applied only to the first 18 | @overload 19 | def method3(self, x: str) -> int: ... # error: [invalid-overload] | ^^^^^^^ - | ``` ``` error[invalid-overload]: `@final` decorator should be applied only to the first overload - --> src/mdtest_snippet.pyi:15:5 + --> src/mdtest_snippet.pyi:22:9 | 15 | / @overload 16 | | def method3(self, x: int) -> int: ... @@ -158,6 +154,5 @@ error[invalid-overload]: `@final` decorator should be applied only to the first | ------ 22 | def method3(self, x: bytes) -> bytes: ... # error: [invalid-overload] | ^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@override`_(2df210735ca532f9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@override`_(2df210735ca532f9).snap" index a6ac3a452e..349cfd46e3 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@override`_(2df210735ca532f9).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Inconsistent_decorat\342\200\246_-_`@override`_(2df210735ca532f9).snap" @@ -84,7 +84,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/overloads.md ``` error[invalid-overload]: `@override` decorator should be applied only to the overload implementation - --> src/mdtest_snippet.py:23:5 + --> src/mdtest_snippet.py:26:9 | 23 | @overload | --------- @@ -95,13 +95,12 @@ error[invalid-overload]: `@override` decorator should be applied only to the ove | ^^^^^^ 27 | def method(self, x: int | str) -> int | str: | ------ Implementation defined here - | ``` ``` error[invalid-overload]: `@override` decorator should be applied only to the overload implementation - --> src/mdtest_snippet.py:31:5 + --> src/mdtest_snippet.py:34:9 | 31 | @overload | --------- @@ -114,13 +113,12 @@ error[invalid-overload]: `@override` decorator should be applied only to the ove 36 | def method(self, x: str) -> str: ... 37 | def method(self, x: int | str) -> int | str: | ------ Implementation defined here - | ``` ``` error[invalid-overload]: `@override` decorator should be applied only to the first overload - --> src/mdtest_snippet.pyi:17:5 + --> src/mdtest_snippet.pyi:22:9 | 17 | / @overload 18 | | def method(self, x: int) -> int: ... @@ -131,6 +129,5 @@ error[invalid-overload]: `@override` decorator should be applied only to the fir 21 | # error: [invalid-overload] 22 | def method(self, x: str) -> str: ... | ^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_\342\200\246_-_Regular_modules_(5c8e81664d1c7470).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_\342\200\246_-_Regular_modules_(5c8e81664d1c7470).snap" index 7901a7572e..693a5564d6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_\342\200\246_-_Regular_modules_(5c8e81664d1c7470).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_Overload_without_an_\342\200\246_-_Regular_modules_(5c8e81664d1c7470).snap" @@ -37,7 +37,6 @@ error[invalid-overload]: Overloads for function `func` must be followed by a non | 5 | def func(x: int) -> int: ... | ^^^^ - | info: Attempting to call `func` will raise `TypeError` at runtime info: Overloaded functions without implementations are only permitted: info: - in stub files @@ -54,7 +53,6 @@ error[invalid-overload]: Overloads for function `method` must be followed by a n | 12 | def method(self, x: int) -> int: ... | ^^^^^^ - | info: Attempting to call `method` will raise `TypeError` at runtime info: Overloaded functions without implementations are only permitted: info: - in stub files diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_`@overload`-decorate\342\200\246_(d17a1580f99a6402).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_`@overload`-decorate\342\200\246_(d17a1580f99a6402).snap" index 36d556d1c2..53b47f9c38 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_`@overload`-decorate\342\200\246_(d17a1580f99a6402).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/overloads.md_-_Overloads_-_Invalid_-_`@overload`-decorate\342\200\246_(d17a1580f99a6402).snap" @@ -55,7 +55,6 @@ warning[useless-overload-body]: Useless body for `@overload`-decorated function | 23 | return x # error: [useless-overload-body] | ^^^^^^^^ This statement will never be executed - | info: `@overload`-decorated functions are solely for type checkers and must be overwritten at runtime by a non-`@overload`-decorated implementation help: Consider replacing this function body with `...` or `pass` @@ -67,7 +66,6 @@ warning[useless-overload-body]: Useless body for `@overload`-decorated function | 29 | print("oh no, a string") # error: [useless-overload-body] | ^^^^^^^^^^^^^^^^^^^^^^^^ This statement will never be executed - | info: `@overload`-decorated functions are solely for type checkers and must be overwritten at runtime by a non-`@overload`-decorated implementation help: Consider replacing this function body with `...` or `pass` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-_`typing.override`_-_Basics_(b7c220f8171f11f0).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-_`typing.override`_-_Basics_(b7c220f8171f11f0).snap index 2fb6d32fdf..bfd766091c 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-_`typing.override`_-_Basics_(b7c220f8171f11f0).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-_`typing.override`_-_Basics_(b7c220f8171f11f0).snap @@ -194,134 +194,124 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/override.md ``` error[invalid-explicit-override]: Method `___reprrr__` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:98:5 + --> src/mdtest_snippet.pyi:99:9 | 98 | @override | --------- 99 | def ___reprrr__(self): ... # error: [invalid-explicit-override] | ^^^^^^^^^^^ - | info: No `___reprrr__` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `foo` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:100:5 + --> src/mdtest_snippet.pyi:102:9 | 100 | @override | --------- 101 | @classmethod 102 | def foo(self): ... # error: [invalid-explicit-override] | ^^^ - | info: No `foo` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `bar` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:104:5 + --> src/mdtest_snippet.pyi:105:9 | 104 | @override | --------- 105 | def bar(self): ... # error: [invalid-explicit-override] | ^^^ - | info: No `bar` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `baz` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:107:5 + --> src/mdtest_snippet.pyi:108:9 | 107 | @override | --------- 108 | def baz(): ... # error: [invalid-explicit-override] | ^^^ - | info: No `baz` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `eggs` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:109:5 + --> src/mdtest_snippet.pyi:111:9 | 109 | @override | --------- 110 | @staticmethod 111 | def eggs(): ... # error: [invalid-explicit-override] | ^^^^ - | info: No `eggs` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `bad_property1` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:113:5 + --> src/mdtest_snippet.pyi:114:9 | 113 | @override | --------- 114 | def bad_property1(self) -> int: ... # error: [invalid-explicit-override] | ^^^^^^^^^^^^^ - | info: No `bad_property1` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `bad_property2` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:115:5 + --> src/mdtest_snippet.pyi:117:9 | 115 | @override | --------- 116 | @property 117 | def bad_property2(self) -> int: ... # error: [invalid-explicit-override] | ^^^^^^^^^^^^^ - | info: No `bad_property2` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `bad_settable_property` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:119:5 + --> src/mdtest_snippet.pyi:120:9 | 119 | @override | --------- 120 | def bad_settable_property(self) -> int: ... # error: [invalid-explicit-override] | ^^^^^^^^^^^^^^^^^^^^^ - | info: No `bad_settable_property` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `lossy` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:124:5 + --> src/mdtest_snippet.pyi:125:9 | 124 | @override | --------- 125 | def lossy(self): ... # error: [invalid-explicit-override] | ^^^^^ - | info: No `lossy` definitions were found on any superclasses of `Invalid` ``` ``` error[invalid-explicit-override]: Method `lossy2` is decorated with `@override` but does not override anything - --> src/mdtest_snippet.pyi:126:5 + --> src/mdtest_snippet.pyi:128:9 | 126 | @override | --------- 127 | @lossy_decorator 128 | def lossy2(self): ... # error: [invalid-explicit-override] | ^^^^^^ - | info: No `lossy2` definitions were found on any superclasses of `Invalid` ``` @@ -337,7 +327,6 @@ error[invalid-method-override]: Invalid override of method `class_method1` | 20 | def class_method1(cls) -> int: ... | ------------------------- `Parent.class_method1` defined here - | info: `LiskovViolatingButNotOverrideViolating.class_method1` is a staticmethod but `Parent.class_method1` is a classmethod info: This violates the Liskov Substitution Principle @@ -354,7 +343,6 @@ error[invalid-explicit-override]: Method `bar` is decorated with `@override` but | 156 | @override | --------- - | info: No `bar` definitions were found on any superclasses of `Foo` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" index 1b284285a5..34060323b9 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(648be2a43987ffd8).snap" @@ -108,7 +108,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 24 | a1: P, | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -124,7 +123,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 26 | a3: Callable[[P], int], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -140,7 +138,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 28 | a4: Callable[..., P], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -156,7 +153,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 30 | a5: Callable[Concatenate[P, ...], int], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -172,7 +168,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 32 | a6: P | int, | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -188,7 +183,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 34 | a7: Union[P, int], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -204,7 +198,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 36 | a8: Optional[P], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -220,7 +213,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 38 | a9: Annotated[P, "metadata"], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -236,7 +228,6 @@ error[invalid-type-form]: The first argument to `Callable` must be either a list | 40 | a10: Callable["[int, str]", str], | ^^^^^^^^^^^^ - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -248,7 +239,6 @@ error[invalid-type-form]: The first argument to `Callable` must be either a list | 42 | a11: Callable["...", int], | ^^^^^ - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -260,7 +250,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a r | 46 | def invalid_return() -> P: | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -276,7 +265,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 51 | x: P = y | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -292,7 +280,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 55 | x: Final[P] = y | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -308,7 +295,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a r | 58 | def invalid_stringified_return() -> "P": | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -324,7 +310,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 63 | a: "P", | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -340,7 +325,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 67 | x: "P" = y | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -356,7 +340,6 @@ error[invalid-type-form]: Bare ParamSpec `Q` is not valid in this context in a p | 74 | a: InvalidSpecializationTarget[[Q]], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -372,7 +355,6 @@ error[invalid-type-form]: Bare ParamSpec `Q` is not valid in this context in a p | 76 | b: InvalidSpecializationTarget[Q,], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(c9dbdc7b13b704a4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(c9dbdc7b13b704a4).snap" index 30d66b02d2..31e331a2c6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(c9dbdc7b13b704a4).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_Legacy_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(c9dbdc7b13b704a4).snap" @@ -56,7 +56,6 @@ error[invalid-type-arguments]: ParamSpec `P` cannot be used to specialize type v | - Type variable `T` defined here 4 | P = ParamSpec("P") | - ParamSpec `P` defined here - | ``` @@ -66,6 +65,5 @@ error[invalid-type-arguments]: Type argument for `ParamSpec` must be either a li | 26 | def func3(c: ParamSpecAndTypeVar[T, int], other: T): ... | ^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" index 99303e6579..170d8af1e6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_Validating_`ParamSpe\342\200\246_(327594c6dacd8ad).snap" @@ -86,7 +86,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 11 | a1: P, | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -102,7 +101,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 13 | a3: Callable[[P], int], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -118,7 +116,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 15 | a4: Callable[..., P], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -134,7 +131,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 17 | a5: Callable[Concatenate[P, ...], int], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -150,7 +146,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 19 | a6: P | int, | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -166,7 +161,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 21 | a7: Union[P, int], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -182,7 +176,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 23 | a8: Optional[P], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -198,7 +191,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 25 | a9: Annotated[P, "metadata"], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -214,7 +206,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a r | 29 | def invalid_return[**P]() -> P: | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -230,7 +221,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 33 | type Alias[**P] = P | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -246,7 +236,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 37 | x: P = y | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -262,7 +251,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 41 | x: Final[P] = y | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -278,7 +266,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a r | 44 | def invalid_stringified_return[**P]() -> "P": | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -294,7 +281,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a p | 49 | a: "P", | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -310,7 +296,6 @@ error[invalid-type-form]: Bare ParamSpec `P` is not valid in this context in a t | 53 | x: "P" = y | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -326,7 +311,6 @@ error[invalid-type-form]: Bare ParamSpec `Q` is not valid in this context in a p | 60 | a: InvalidSpecializationTarget[[Q]], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` @@ -342,7 +326,6 @@ error[invalid-type-form]: Bare ParamSpec `Q` is not valid in this context in a p | 62 | b: InvalidSpecializationTarget[Q,], | ^ - | info: A bare ParamSpec is only valid: info: - as the first argument to `Callable` info: - as the last argument to `Concatenate` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(8243f67799c93e3c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(8243f67799c93e3c).snap" index 7c51e0f626..4216032909 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(8243f67799c93e3c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/paramspec.md_-_PEP_695_`ParamSpec`_-_`ParamSpec`_cannot_s\342\200\246_(8243f67799c93e3c).snap" @@ -48,7 +48,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspe ``` error[invalid-type-arguments]: ParamSpec `P` cannot be used to specialize type variable `T` - --> src/mdtest_snippet.py:9:9 + --> src/mdtest_snippet.py:11:20 | 9 | def f[**P, T](): | - ParamSpec `P` defined here @@ -60,7 +60,6 @@ error[invalid-type-arguments]: ParamSpec `P` cannot be used to specialize type v | 3 | class OnlyTypeVar[T]: | - Type variable `T` defined here - | ``` @@ -79,7 +78,6 @@ error[invalid-type-arguments]: ParamSpec `P` cannot be used to specialize type v 8 | 9 | def f[**P, T](): | - ParamSpec `P` defined here - | ``` @@ -89,6 +87,5 @@ error[invalid-type-arguments]: Type argument for `ParamSpec` must be either a li | 29 | def func3[T](c: ParamSpecAndTypeVar[T, int], other: T): ... | ^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_cl\342\200\246_(288988036f34ddcf).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_cl\342\200\246_(288988036f34ddcf).snap" index 83895eae63..413c1927b8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_cl\342\200\246_(288988036f34ddcf).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_cl\342\200\246_(288988036f34ddcf).snap" @@ -48,7 +48,6 @@ error[call-non-callable]: Object of type `` is n | 4 | reveal_type(Protocol()) # revealed: Unknown | ^^^^^^^^^^ - | ``` @@ -58,13 +57,11 @@ error[call-non-callable]: Cannot instantiate class `MyProtocol` | 10 | reveal_type(MyProtocol()) # revealed: MyProtocol | ^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: Protocol classes cannot be instantiated --> src/mdtest_snippet.py:6:7 | 6 | class MyProtocol(Protocol): | ^^^^^^^^^^^^^^^^^^^^ `MyProtocol` declared as a protocol here - | ``` @@ -74,12 +71,10 @@ error[call-non-callable]: Cannot instantiate class `GenericProtocol` | 16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int] | ^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: Protocol classes cannot be instantiated --> src/mdtest_snippet.py:12:7 | 12 | class GenericProtocol[T](Protocol): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `GenericProtocol` declared as a protocol here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_and_auto\342\200\246_(310665856cfe2424).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_and_auto\342\200\246_(310665856cfe2424).snap" index fb5b8f5be2..b04e1b5ef8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_and_auto\342\200\246_(310665856cfe2424).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_and_auto\342\200\246_(310665856cfe2424).snap" @@ -55,7 +55,6 @@ error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` an | 5 | class Foo(Protocol[T], Generic[T]): ... # error: [invalid-generic-class] | ^^^^^^^^^^^ - | help: Remove the type parameters from the `Protocol` base | 4 | @@ -76,7 +75,6 @@ error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` an 11 | | T, 12 | | ], Generic[T]): ... | |_^ - | help: Remove the type parameters from the `Protocol` base | 9 | # error: [invalid-generic-class] @@ -100,7 +98,6 @@ error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` an 19 | | # very well documented code 20 | | ], # important comma! | |_^ - | help: Remove the type parameters from the `Protocol` base | 15 | # error: [invalid-generic-class] @@ -122,7 +119,6 @@ error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` an | 32 | class Foo[T](Protocol[T]): ... # error: [invalid-generic-class] | ^^^^^^^^^^^ - | help: Remove the type parameters from the `Protocol` base | 31 | diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_for_prot\342\200\246_(585a3e9545d41b64).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_for_prot\342\200\246_(585a3e9545d41b64).snap" index c62928847e..50abe4a3a5 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_for_prot\342\200\246_(585a3e9545d41b64).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Diagnostics_for_prot\342\200\246_(585a3e9545d41b64).snap" @@ -68,13 +68,11 @@ warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the | 12 | a = None # type: int | ^^^^^^^^ Consider adding an annotation for `a` - | info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface --> src/a.py:6:7 | 6 | class A(Protocol): | ^^^^^^^^^^^ `A` declared as a protocol here - | info: No declarations found for `a` in the body of `A` or any of its superclasses ``` @@ -85,13 +83,11 @@ warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the | 14 | b = ... # type: str | ^^^^^^^ Consider adding an annotation for `b` - | info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface --> src/a.py:6:7 | 6 | class A(Protocol): | ^^^^^^^^^^^ `A` declared as a protocol here - | info: No declarations found for `b` in the body of `A` or any of its superclasses ``` @@ -102,13 +98,11 @@ warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the | 17 | c = 1 # error: [ambiguous-protocol-member] | ^^^^^ Consider adding an annotation, e.g. `c: int = ...` - | info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface --> src/a.py:6:7 | 6 | class A(Protocol): | ^^^^^^^^^^^ `A` declared as a protocol here - | info: No declarations found for `c` in the body of `A` or any of its superclasses ``` @@ -119,13 +113,11 @@ warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the | 22 | for d in range(42): | ^ `d` is not declared as a protocol member - | info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface --> src/a.py:6:7 | 6 | class A(Protocol): | ^^^^^^^^^^^ `A` declared as a protocol here - | info: No declarations found for `d` in the body of `A` or any of its superclasses ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`ge\342\200\246_(3d0c4ee818c4d8d5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`ge\342\200\246_(3d0c4ee818c4d8d5).snap" index 4b8f21004d..cfc81b9343 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`ge\342\200\246_(3d0c4ee818c4d8d5).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Invalid_calls_to_`ge\342\200\246_(3d0c4ee818c4d8d5).snap" @@ -35,14 +35,12 @@ error[invalid-argument-type]: Invalid argument to `get_protocol_members` | 5 | get_protocol_members(NotAProtocol) # error: [invalid-argument-type] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | 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 | 3 | class NotAProtocol: ... | ^^^^^^^^^^^^ - | 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# @@ -54,14 +52,12 @@ error[invalid-argument-type]: Invalid argument to `get_protocol_members` | 9 | get_protocol_members(AlsoNotAProtocol) # error: [invalid-argument-type] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | 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 | 7 | class AlsoNotAProtocol(NotAProtocol, object): ... | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | 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/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Match_class_patterns\342\200\246_(8ae0e231033b78e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Match_class_patterns\342\200\246_(8ae0e231033b78e).snap" index 017fdee477..62f4a2a204 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Match_class_patterns\342\200\246_(8ae0e231033b78e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Match_class_patterns\342\200\246_(8ae0e231033b78e).snap" @@ -52,13 +52,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used in a class patte | 12 | case HasX(): # error: [isinstance-against-protocol] | ^^^^ This will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | info: A protocol class can only be used in a match class pattern 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 @@ -70,13 +68,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used in a class patte | 28 | case Wrapper(inner=HasX()): # error: [isinstance-against-protocol] | ^^^^ This will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | info: A protocol class can only be used in a match class pattern 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 diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protoco\342\200\246_(98257e7c2300373).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protoco\342\200\246_(98257e7c2300373).snap" index 13a0b9d177..0c5ec01140 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protoco\342\200\246_(98257e7c2300373).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Narrowing_of_protoco\342\200\246_(98257e7c2300373).snap" @@ -103,13 +103,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 7 | if isinstance(arg, HasX): # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -121,13 +119,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 12 | if issubclass(arg2, HasX): # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -139,13 +135,11 @@ error[isinstance-against-protocol]: Class `RuntimeCheckableHasX` cannot be used | 43 | if issubclass(arg1, RuntimeCheckableHasX): | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: A protocol class cannot be used in `issubclass` checks if it has non-method members --> src/mdtest_snippet.py:20:5 | 20 | x: int | ^ Non-method member `x` declared here - | ``` @@ -155,14 +149,12 @@ error[isinstance-against-protocol]: Class `MultipleNonMethodMembers` cannot be u | 48 | if issubclass(arg1, MultipleNonMethodMembers): # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: A protocol class cannot be used in `issubclass` checks if it has non-method members info: `MultipleNonMethodMembers` has non-method members `a` and `b` --> src/mdtest_snippet.py:39:5 | 39 | a: int | ^ Non-method member `a` declared here - | ``` @@ -172,13 +164,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 63 | isinstance(arg, (HasX, RuntimeCheckableHasX)) # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -190,13 +180,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 64 | isinstance(arg, (HasX, int)) # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -208,13 +196,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 68 | issubclass(arg2, (HasX, RuntimeCheckableHasX)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -226,13 +212,11 @@ error[isinstance-against-protocol]: Class `RuntimeCheckableHasX` cannot be used | 68 | issubclass(arg2, (HasX, RuntimeCheckableHasX)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: A protocol class cannot be used in `issubclass` checks if it has non-method members --> src/mdtest_snippet.py:20:5 | 20 | x: int | ^ Non-method member `x` declared here - | ``` @@ -242,13 +226,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 70 | issubclass(arg2, (HasX, OnlyMethodMembers)) # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -260,13 +242,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 72 | isinstance(arg, (int, (HasX, str))) # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -278,13 +258,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 76 | issubclass(arg2, (int, (HasX, RuntimeCheckableHasX))) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 @@ -296,13 +274,11 @@ error[isinstance-against-protocol]: Class `RuntimeCheckableHasX` cannot be used | 76 | issubclass(arg2, (int, (HasX, RuntimeCheckableHasX))) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: A protocol class cannot be used in `issubclass` checks if it has non-method members --> src/mdtest_snippet.py:20:5 | 20 | x: int | ^ Non-method member `x` declared here - | ``` @@ -312,13 +288,11 @@ error[isinstance-against-protocol]: Class `HasX` cannot be used as the second ar | 80 | isinstance(arg, classes) # error: [isinstance-against-protocol] | ^^^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime - | info: `HasX` is declared as a protocol class, but it is not declared as runtime-checkable --> src/mdtest_snippet.py:3:7 | 3 | class HasX(Protocol): | ^^^^^^^^^^^^^^ `HasX` declared here - | 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 diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Protocol_members_in_\342\200\246_(21be5d9bdab1c844).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Protocol_members_in_\342\200\246_(21be5d9bdab1c844).snap" index 7596d930a6..88ad76c836 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Protocol_members_in_\342\200\246_(21be5d9bdab1c844).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Protocol_members_in_\342\200\246_(21be5d9bdab1c844).snap" @@ -38,13 +38,11 @@ warning[ambiguous-protocol-member]: Cannot assign to undeclared variable in the | 12 | e = 56 # error: [ambiguous-protocol-member] | ^^^^^^ Consider adding an annotation, e.g. `e: int = ...` - | info: Assigning to an undeclared variable in a protocol class leads to an ambiguous interface --> src/mdtest_snippet.py:4:7 | 4 | class Foo(Protocol): | ^^^^^^^^^^^^^ `Foo` declared as a protocol here - | info: No declarations found for `e` in the body of `Foo` or any of its superclasses ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Diagnostics_for_`emp\342\200\246_(f44e56404a51ca26).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Diagnostics_for_`emp\342\200\246_(f44e56404a51ca26).snap" index 3b2a930a99..064faaad29 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Diagnostics_for_`emp\342\200\246_(f44e56404a51ca26).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Diagnostics_for_`emp\342\200\246_(f44e56404a51ca26).snap" @@ -30,7 +30,6 @@ error[empty-body]: Function always implicitly returns `None`, which is not assig | 7 | def method(self) -> str: ... # error: [empty-body] | ^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement info: Functions with empty bodies and non-`None` return types are only permitted: info: - in stub files @@ -43,7 +42,6 @@ info: Only classes that directly inherit from `typing.Protocol` or `typing_exten | 6 | class Concrete(Abstract): | ^^^^^^^^^^^^^^^^^^ `Protocol` not present in `Concrete`'s immediate bases - | info: See https://typing.python.org/en/latest/spec/protocol.html# ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Asynchronous_(408134055c24a538).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Asynchronous_(408134055c24a538).snap index c930f1b4a3..5f4aa0fd81 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Asynchronous_(408134055c24a538).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Asynchronous_(408134055c24a538).snap @@ -44,7 +44,6 @@ error[invalid-return-type]: Return type does not match returned value | 16 | async def j() -> str: # error: [invalid-return-type] | ^^^ expected `str`, found `types.AsyncGeneratorType` - | 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 @@ -56,6 +55,5 @@ error[invalid-syntax]: `return` with value in async generator | 21 | return 2 # error: [invalid-syntax] "`return` with value in async generator" | ^^^^^^^^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Synchronous_(6a32ec69d15117b8).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Synchronous_(6a32ec69d15117b8).snap index d684e31c9d..563566ebf3 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Synchronous_(6a32ec69d15117b8).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Generator_functions_-_Synchronous_(6a32ec69d15117b8).snap @@ -59,7 +59,6 @@ error[invalid-return-type]: Return type does not match returned value | 19 | def j() -> str: # error: [invalid-return-type] | ^^^ expected `str`, found `types.GeneratorType` - | 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 @@ -67,27 +66,25 @@ info: See https://docs.python.org/3/glossary.html#term-generator for more detail ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:22:30 + --> src/mdtest_snippet.py:24:12 | 22 | def invalid_return_type() -> typing.Generator[None, None, None]: | ---------------------------------- Expected `None` because of return type 23 | yield 24 | return "" # error: [invalid-return-type] | ^^ expected `None`, found `Literal[""]` - | ``` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:25:23 + --> src/mdtest_snippet.py:27:12 | 25 | def wrong_return() -> typing.Generator[int, int, int]: | ------------------------------- Expected `int` because of return type 26 | yield 1 27 | return "" # error: [invalid-return-type] | ^^ expected `int`, found `Literal[""]` - | ``` @@ -97,14 +94,13 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is | 31 | def missing_return() -> typing.Generator[int, int, int]: # error: [invalid-return-type] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement ``` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:33:35 + --> src/mdtest_snippet.py:36:12 | 33 | def iterator_must_not_return() -> typing.Iterator[int]: | -------------------- Expected `None` because of return type @@ -112,6 +108,5 @@ error[invalid-return-type]: Return type does not match returned value 35 | # error: [invalid-return-type] 36 | return "foo" | ^^^^^ expected `None`, found `Literal["foo"]` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_\342\200\246_(94c036c5d3803ab2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_\342\200\246_(94c036c5d3803ab2).snap" index 16e9f47210..6588600c7e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_\342\200\246_(94c036c5d3803ab2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_\342\200\246_(94c036c5d3803ab2).snap" @@ -33,7 +33,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:1:22 + --> src/mdtest_snippet.py:6:16 | 1 | def f(cond: bool) -> str: | --- Expected `str` because of return type @@ -43,13 +43,12 @@ error[invalid-return-type]: Return type does not match returned value 5 | # error: [invalid-return-type] 6 | return 1 | ^ expected `str`, found `Literal[1]` - | ``` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:8:22 + --> src/mdtest_snippet.py:11:16 | 8 | def f(cond: bool) -> str: | --- Expected `str` because of return type @@ -57,7 +56,6 @@ error[invalid-return-type]: Return type does not match returned value 10 | # error: [invalid-return-type] 11 | return 1 | ^ expected `str`, found `Literal[1]` - | ``` @@ -72,6 +70,5 @@ error[invalid-return-type]: Return type does not match returned value | 8 | def f(cond: bool) -> str: | --- Expected `str` because of return type - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(393cb38bf7119649).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(393cb38bf7119649).snap" index 98918bef02..07945ff967 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(393cb38bf7119649).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(393cb38bf7119649).snap" @@ -45,7 +45,6 @@ error[invalid-return-type]: Function can implicitly return `None`, which is not | 6 | def f(cond: bool) -> int: | ^^^ - | ``` @@ -55,7 +54,6 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is | 11 | def f(cond: bool) -> int: | ^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement ``` @@ -66,6 +64,5 @@ error[invalid-return-type]: Function can implicitly return `None`, which is not | 16 | def f(cond: bool) -> int: | ^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(3d2d19aa49b28f1c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(3d2d19aa49b28f1c).snap" index 778f3b10d0..48783ffacb 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(3d2d19aa49b28f1c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_ret\342\200\246_(3d2d19aa49b28f1c).snap" @@ -26,7 +26,6 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is | 2 | def f() -> int: | ^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_(a91e0c67519cd77f).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_(a91e0c67519cd77f).snap index 2edd69296d..fe4187e42b 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_(a91e0c67519cd77f).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_(a91e0c67519cd77f).snap @@ -53,34 +53,31 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is | 2 | def f() -> int: | ^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement ``` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:5:12 + --> src/mdtest_snippet.py:7:12 | 5 | def f() -> str: | --- Expected `str` because of return type 6 | # error: [invalid-return-type] 7 | return 1 | ^ expected `str`, found `Literal[1]` - | ``` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:9:12 + --> src/mdtest_snippet.py:11:5 | 9 | def f() -> int: | --- Expected `int` because of return type 10 | # error: [invalid-return-type] 11 | return | ^^^^^^ expected `int`, found `None` - | ``` @@ -90,7 +87,6 @@ error[empty-body]: Function always implicitly returns `None`, which is not assig | 18 | def m(x: T) -> T: ... | ^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement info: Functions with empty bodies and non-`None` return types are only permitted: info: - in stub files @@ -102,26 +98,24 @@ info: - or as `@abstractmethod`-decorated methods on abstract classes ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:22:12 + --> src/mdtest_snippet.py:24:12 | 22 | def f() -> A[int]: | ------ Expected `mdtest_snippet.A[int]` because of return type 23 | class A[T]: ... 24 | return A[int]() # error: [invalid-return-type] | ^^^^^^^^ expected `mdtest_snippet.A[int]`, found `mdtest_snippet..A[int]` - | ``` ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.py:28:12 + --> src/mdtest_snippet.py:30:12 | 28 | def g() -> B: | - Expected `mdtest_snippet.B` because of return type 29 | class B: ... 30 | return B() # error: [invalid-return-type] | ^^^ expected `mdtest_snippet.B`, found `mdtest_snippet..B` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_\342\200\246_(c3a523878447af6b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_\342\200\246_(c3a523878447af6b).snap" index 1c43d901aa..68ae2d8b0a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_\342\200\246_(c3a523878447af6b).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_\342\200\246_(c3a523878447af6b).snap" @@ -32,14 +32,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/function/return_type.md ``` error[invalid-return-type]: Return type does not match returned value - --> src/mdtest_snippet.pyi:1:12 + --> src/mdtest_snippet.pyi:3:12 | 1 | def f() -> int: | --- Expected `int` because of return type 2 | # error: [invalid-return-type] 3 | return ... | ^^^ expected `int`, found `EllipsisType` - | ``` @@ -49,7 +48,6 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is | 6 | def foo() -> int: | ^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement ``` @@ -60,7 +58,6 @@ error[invalid-return-type]: Function always implicitly returns `None`, which is | 11 | def foo() -> int: | ^^^ - | info: Consider changing the return annotation to `-> None` or adding a `return` statement ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" index 4e584caec4..6f2552df1a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(3259718bf20b45a2).snap" @@ -27,7 +27,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[shadowed-type-variable]: Generic class `Bad1` uses type variable `T` already bound by an enclosing scope - --> src/mdtest_snippet.py:3:5 + --> src/mdtest_snippet.py:6:11 | 3 | def f[T](x: T, y: T) -> None: | ------------------------ Type variable `T` is bound in this enclosing scope @@ -35,13 +35,12 @@ error[shadowed-type-variable]: Generic class `Bad1` uses type variable `T` alrea 5 | # error: [shadowed-type-variable] 6 | class Bad1[T]: ... | ^^^^ `T` used in class definition here - | ``` ``` error[shadowed-type-variable]: Generic class `Bad2` uses type variable `T` already bound by an enclosing scope - --> src/mdtest_snippet.py:3:5 + --> src/mdtest_snippet.py:8:11 | 3 | def f[T](x: T, y: T) -> None: | ------------------------ Type variable `T` is bound in this enclosing scope @@ -51,6 +50,5 @@ error[shadowed-type-variable]: Generic class `Bad2` uses type variable `T` alrea 7 | # error: [shadowed-type-variable] 8 | class Bad2(Iterable[T]): ... | ^^^^^^^^^^^^^^^^^ `T` used in class definition here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" index 55db2ec578..702a9a71ed 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_class_within\342\200\246_(711fb86287c4d87b).snap" @@ -27,7 +27,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[shadowed-type-variable]: Generic class `Bad1` uses type variable `T` already bound by an enclosing scope - --> src/mdtest_snippet.py:3:7 + --> src/mdtest_snippet.py:6:11 | 3 | class C[T]: | - Type variable `T` is bound in this enclosing scope @@ -35,13 +35,12 @@ error[shadowed-type-variable]: Generic class `Bad1` uses type variable `T` alrea 5 | # error: [shadowed-type-variable] 6 | class Bad1[T]: ... | ^^^^ `T` used in class definition here - | ``` ``` error[shadowed-type-variable]: Generic class `Bad2` uses type variable `T` already bound by an enclosing scope - --> src/mdtest_snippet.py:3:7 + --> src/mdtest_snippet.py:8:11 | 3 | class C[T]: | - Type variable `T` is bound in this enclosing scope @@ -51,6 +50,5 @@ error[shadowed-type-variable]: Generic class `Bad2` uses type variable `T` alrea 7 | # error: [shadowed-type-variable] 8 | class Bad2(Iterable[T]): ... | ^^^^^^^^^^^^^^^^^ `T` used in class definition here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" index a2a37ae67a..85cd276233 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_function_wit\342\200\246_(f58a51442a16371e).snap" @@ -33,6 +33,5 @@ error[shadowed-type-variable]: Generic function `bad` uses type variable `T` alr | 1 | def f[T](x: T, y: T) -> None: | ------------------------ Type variable `T` is bound in this enclosing scope - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" index 29725bea77..1b5754cbb5 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Nested_formal_typeva\342\200\246_-_Generic_method_withi\342\200\246_(c19e9277cf9fafb5).snap" @@ -33,6 +33,5 @@ error[shadowed-type-variable]: Generic function `bad` uses type variable `T` alr | 1 | class C[T]: | - Type variable `T` is bound in this enclosing scope - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" index 9f7d87d5b2..4cd746fe2a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Function_nested_in_c\342\200\246_(1a50b4ccb10b95dd).snap" @@ -23,14 +23,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Invalid default for type parameter `U` - --> src/mdtest_snippet.py:1:9 + --> src/mdtest_snippet.py:3:15 | 1 | class C[T]: | - `T` defined here 2 | # error: [invalid-type-variable-default] 3 | def f[U = T](self): ... | ^ `T` is a type parameter bound in an outer scope - | info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" index 82324be658..2a31bc02c2 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_me\342\200\246_(2ed4c18a38ed9090).snap" @@ -28,7 +28,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Invalid use of type variable `T2` - --> src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:8:25 | 4 | T2 = TypeVar("T2", default=T1) | ------------------------------ `T2` defined here @@ -37,7 +37,6 @@ error[invalid-type-variable-default]: Invalid use of type variable `T2` 7 | # error: [invalid-type-variable-default] "Invalid use of type variable `T2`: default of `T2` refers to out-of-scope type variable `… 8 | def method(self, x: T2) -> T2: | ^^ Default of `T2` references out-of-scope type variable `T1` - | info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" index 37b3cf576e..8e30747b70 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_in_ne\342\200\246_(a1aca17ea750ffdd).snap" @@ -29,7 +29,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Invalid use of type variable `U` - --> src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:8:18 | 4 | U = TypeVar("U", default=T) | --------------------------- `U` defined here @@ -38,7 +38,6 @@ error[invalid-type-variable-default]: Invalid use of type variable `U` 7 | # error: [invalid-type-variable-default] 8 | def inner(y: U) -> U: | ^ Default of `U` references out-of-scope type variable `T` - | info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" index 946a5aee3d..b188fdd233 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_order\342\200\246_(d075a45828c9dbc5).snap" @@ -43,7 +43,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Type parameters without defaults cannot follow type parameters with defaults - --> src/mdtest_snippet.py:9:10 + --> src/mdtest_snippet.py:9:17 | 9 | def f(x: T1, y: T2) -> tuple[T1, T2]: | -- ^^ Type variable `T2` does not have a default @@ -56,13 +56,12 @@ error[invalid-type-variable-default]: Type parameters without defaults cannot fo | ------------------------------- `T1` defined here 4 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | ``` ``` error[invalid-type-variable-default]: Type parameters without defaults cannot follow type parameters with defaults - --> src/mdtest_snippet.py:13:17 + --> src/mdtest_snippet.py:13:24 | 13 | def g(x: T2, y: T1, z: T3) -> tuple[T2, T1, T3]: | -- ^^ Type variable `T3` does not have a default @@ -76,13 +75,12 @@ error[invalid-type-variable-default]: Type parameters without defaults cannot fo 4 | T2 = TypeVar("T2") 5 | T3 = TypeVar("T3") | ------------------ `T3` defined here - | ``` ``` error[invalid-type-variable-default]: Type parameters without defaults cannot follow type parameters with defaults - --> src/mdtest_snippet.py:17:10 + --> src/mdtest_snippet.py:17:17 | 17 | def h(x: T1, y: T2, z: DefaultStrT, w: T3) -> tuple[T1, T2, DefaultStrT, T3]: | -- ^^ Type variables `T2` and `T3` do not have defaults @@ -95,6 +93,5 @@ error[invalid-type-variable-default]: Type parameters without defaults cannot fo | ------------------------------- `T1` defined here 4 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" index 98aa8d2eef..8e2a4102a5 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Legacy_TypeVar_with_\342\200\246_(ce8defbeaf54e06c).snap" @@ -31,7 +31,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Invalid use of type variable `U` - --> src/mdtest_snippet.py:4:1 + --> src/mdtest_snippet.py:7:12 | 4 | U = TypeVar("U", default=T) | --------------------------- `U` defined here @@ -39,7 +39,6 @@ error[invalid-type-variable-default]: Invalid use of type variable `U` 6 | # error: [invalid-type-variable-default] 7 | def bad(y: U, z: T) -> tuple[U, T]: | ^ Default of `U` references later type parameter `T` - | info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" index 62bd9c1a5c..d461955cf6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Nested_functions_(3f2ee9fa81da0177).snap" @@ -23,14 +23,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Invalid default for type parameter `U` - --> src/mdtest_snippet.py:1:11 + --> src/mdtest_snippet.py:3:19 | 1 | def outer[T](): | - `T` defined here 2 | # error: [invalid-type-variable-default] "Type parameter `U` cannot use outer-scope type parameter `T` as its default" 3 | def inner[U = T](): ... | ^ `T` is a type parameter bound in an outer scope - | info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" index 55e25a7604..7b56d2abb1 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/scoping.md_-_Scoping_rules_for_ty\342\200\246_-_Type_parameter_defau\342\200\246_-_Type_alias_nested_in\342\200\246_(de027dcc5360f252).snap" @@ -24,14 +24,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/scoping.md ``` error[invalid-type-variable-default]: Invalid default for type parameter `U` - --> src/mdtest_snippet.py:1:9 + --> src/mdtest_snippet.py:3:20 | 1 | class C[T]: | - `T` defined here 2 | # error: [invalid-type-variable-default] 3 | type Alias[U = T] = list[U] | ^ `T` is a type parameter bound in an outer scope - | info: See https://typing.python.org/en/latest/spec/generics.html#scoping-rules ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Bound_method_overloa\342\200\246_(39e892ccb644ee63).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Bound_method_overloa\342\200\246_(39e892ccb644ee63).snap" index 6af0555664..3c7bc0e068 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Bound_method_overloa\342\200\246_(39e892ccb644ee63).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Bound_method_overloa\342\200\246_(39e892ccb644ee63).snap" @@ -49,7 +49,6 @@ error[invalid-overload]: Implementation does not accept all arguments of this ov 12 | def f(self, x: str, y: int) -> str: ... 13 | def f( | - Implementation defined here - | info: Implementation signature `(self, x: bytes | int | str, y: int = 0) -> bytes | int | str` is not assignable to overload signature `(self: Other, x: bytes) -> bytes` info: parameter `self` has an incompatible type: `Other` is not assignable to `Base` @@ -61,13 +60,11 @@ error[invalid-argument-type]: Argument to bound method `Base.f` is incorrect | 20 | Base().f("ok", "bad") # error: [invalid-argument-type] | ^^^^^ Expected `int`, found `Literal["bad"]` - | info: Matching overload defined here --> src/mdtest_snippet.py:12:9 | 12 | def f(self, x: str, y: int) -> str: ... | ^ ------ Parameter declared here - | info: Non-matching overloads for bound method `f`: info: (self: Other, x: bytes) -> bytes info: (self, x: int) -> int diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Call_to_function_wit\342\200\246_(8fdf5a06afc7d4fe).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Call_to_function_wit\342\200\246_(8fdf5a06afc7d4fe).snap" index 3ada6ceb14..930e95da09 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Call_to_function_wit\342\200\246_(8fdf5a06afc7d4fe).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Call_to_function_wit\342\200\246_(8fdf5a06afc7d4fe).snap" @@ -159,13 +159,11 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect | 5 | foo("foo") # error: [invalid-argument-type] | ^^^^^ Expected `int`, found `Literal["foo"]` - | info: Matching overload defined here --> src/overloaded.pyi:4:5 | 4 | def foo(a: int): ... | ^^^ ------ Parameter declared here - | info: Non-matching overloads for function `foo`: info: (a: int, b: int, c: int) -> Unknown info: (a: str, b: int, c: int) -> Unknown diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Limited_number_of_ov\342\200\246_(93e9a157fdca3ab2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Limited_number_of_ov\342\200\246_(93e9a157fdca3ab2).snap" index c2f43cfdf0..2b6bf4efe4 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Limited_number_of_ov\342\200\246_(93e9a157fdca3ab2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Limited_number_of_ov\342\200\246_(93e9a157fdca3ab2).snap" @@ -39,13 +39,11 @@ error[invalid-argument-type]: Argument to function `f` is incorrect | 3 | f("a") # error: [invalid-argument-type] | ^^^ Expected `int`, found `Literal["a"]` - | info: Matching overload defined here --> src/overloaded.pyi:6:5 | 6 | def f(x: int) -> int: ... | ^ ------ Parameter declared here - | info: Non-matching overloads for function `f`: info: () -> None info: (x: int, y: int) -> int diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/special_form_attribu\342\200\246_-_Diagnostics_for_inva\342\200\246_(249d635e74a41c9e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/special_form_attribu\342\200\246_-_Diagnostics_for_inva\342\200\246_(249d635e74a41c9e).snap" index 653c37cc17..bc70f34a72 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/special_form_attribu\342\200\246_-_Diagnostics_for_inva\342\200\246_(249d635e74a41c9e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/special_form_attribu\342\200\246_-_Diagnostics_for_inva\342\200\246_(249d635e74a41c9e).snap" @@ -43,7 +43,6 @@ error[unresolved-attribute]: Special form `typing.Any` has no attribute `foo` | 14 | X.foo # error: [unresolved-attribute] | ^^^^^ - | help: Objects with type `Any` have a `foo` attribute, but the symbol `typing.Any` does not itself inhabit the type `Any` help: This error may indicate that `X` was defined as `X = typing.Any` when `X: typing.Any` was intended @@ -55,7 +54,6 @@ error[unresolved-attribute]: Special form `typing.Any` has no attribute `aaaaooo | 15 | X.aaaaooooooo # error: [unresolved-attribute] | ^^^^^^^^^^^^^ - | help: Objects with type `Any` have an `aaaaooooooo` attribute, but the symbol `typing.Any` does not itself inhabit the type `Any` help: This error may indicate that `X` was defined as `X = typing.Any` when `X: typing.Any` was intended @@ -67,7 +65,6 @@ error[unresolved-attribute]: Special form `typing.LiteralString` has no attribut | 16 | Foo.X.startswith # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^^ - | help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString` help: This error may indicate that `Foo.X` was defined as `Foo.X = typing.LiteralString` when `Foo.X: typing.LiteralString` was intended @@ -79,7 +76,6 @@ error[unresolved-attribute]: Special form `typing.LiteralString` has no attribut | 17 | Foo.Bar().y.startswith # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^^^^^^^^ - | help: Objects with type `LiteralString` have a `startswith` attribute, but the symbol `typing.LiteralString` does not itself inhabit the type `LiteralString` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" index f784c93ac8..ad9488ac5f 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/string.md_-_String_annotations_-_Partially_deferred_a\342\200\246_-_Python_less_than_3.1\342\200\246_(5e6477d05ddea33f).snap" @@ -91,7 +91,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `Literal["Foo"]` | Has type `` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements @@ -107,7 +106,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `Literal["memoryview"]` | Has type `` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements @@ -123,7 +121,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `None` | Has type `Literal["TD"]` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements @@ -139,7 +136,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `None` | Has type `Literal["P"]` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements @@ -152,7 +148,6 @@ error[unsupported-operator]: Unsupported `|` operation | 33 | h: None | None, | ^^^^^^^^^^^ Both operands have type `None` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line @@ -164,7 +159,6 @@ error[unresolved-reference]: Name `SomethingUndefined` used when not defined | 36 | i: SomethingUndefined | SomethingAlsoUndefined, | ^^^^^^^^^^^^^^^^^^ - | ``` @@ -174,7 +168,6 @@ error[unresolved-reference]: Name `SomethingAlsoUndefined` used when not defined | 36 | i: SomethingUndefined | SomethingAlsoUndefined, | ^^^^^^^^^^^^^^^^^^^^^^ - | ``` @@ -187,7 +180,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `Literal["bytes"]` | Has type `` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements @@ -203,7 +195,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `None` | Has type `Literal["int"]` - | info: All parameter annotations are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements @@ -219,7 +210,6 @@ error[unsupported-operator]: Unsupported `|` operation | | | | | Has type `None` | Has type `Literal["int"]` - | info: All type expressions are evaluated at runtime by default on Python <3.14 info: Python 3.13 was assumed when inferring types because it was specified on the command line help: Put quotes around the whole union rather than just certain elements diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec\342\200\246_(b753048091f275c0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec\342\200\246_(b753048091f275c0).snap" index 9d59d177b6..6415a40aa3 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec\342\200\246_(b753048091f275c0).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec\342\200\246_(b753048091f275c0).snap" @@ -129,7 +129,6 @@ error[unresolved-attribute]: Object of type `, C>` has no att | 20 | super(C, C()).c # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ - | ``` @@ -139,7 +138,6 @@ error[unresolved-attribute]: Object of type `, C>` has no att | 23 | super(B, C()).b # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ - | ``` @@ -149,7 +147,6 @@ error[unresolved-attribute]: Object of type `, C>` has no att | 24 | super(B, C()).c # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ - | ``` @@ -159,7 +156,6 @@ error[unresolved-attribute]: Object of type `, C>` has no att | 26 | super(A, C()).a # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ - | ``` @@ -169,7 +165,6 @@ error[unresolved-attribute]: Object of type `, C>` has no att | 27 | super(A, C()).b # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ - | ``` @@ -179,7 +174,6 @@ error[unresolved-attribute]: Object of type `, C>` has no att | 28 | super(A, C()).c # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ - | ``` @@ -189,7 +183,6 @@ error[invalid-super-argument]: `` is an abstract/st | 78 | reveal_type(super(object, x)) | ^^^^^^^^^^^^^^^^ - | ``` @@ -199,7 +192,6 @@ error[invalid-super-argument]: `(int, str, /) -> bool` is an abstract/structural | 82 | reveal_type(super(object, z)) | ^^^^^^^^^^^^^^^^ - | ``` @@ -209,6 +201,5 @@ error[invalid-super-argument]: `types.GenericAlias` instance `list[int]` is not | 98 | reveal_type(super(list[int], [])) | ^^^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec\342\200\246_(f9e5e48e3a4a4c12).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec\342\200\246_(f9e5e48e3a4a4c12).snap" index 675b898a76..b4eb19178b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec\342\200\246_(f9e5e48e3a4a4c12).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec\342\200\246_(f9e5e48e3a4a4c12).snap" @@ -166,7 +166,6 @@ info[missing-type-argument]: Missing type argument for generic class `Foo` (expe | 61 | def method3(self: Foo): # error: [missing-type-argument] | ^^^ - | ``` @@ -176,7 +175,6 @@ error[invalid-super-argument]: `S@method7` is not an instance or subclass of `` help: Consider adding an upper bound to type variable `S` @@ -189,7 +187,6 @@ error[invalid-super-argument]: `S@method8` is not an instance or subclass of `` @@ -201,7 +198,6 @@ error[invalid-super-argument]: `S@method9` is not an instance or subclass of `` @@ -213,7 +209,6 @@ error[invalid-super-argument]: `S@method10` is a type variable with an abstract/ | 100 | reveal_type(super()) | ^^^^^^^ - | info: Type variable `S` has upper bound `(...) -> str` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Metaclasses_(faeb52a8cd1533b3).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Metaclasses_(faeb52a8cd1533b3).snap index d4c42f028e..7b1b9425eb 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Metaclasses_(faeb52a8cd1533b3).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Metaclasses_(faeb52a8cd1533b3).snap @@ -63,7 +63,6 @@ error[invalid-super-argument]: `` is not an instance or subcl | 34 | super(Meta, OtherBase) # error: [invalid-super-argument] | ^^^^^^^^^^^^^^^^^^^^^^ - | ``` @@ -73,7 +72,6 @@ error[invalid-super-argument]: `type[T@__call__]` is not an instance or subclass | 40 | return super(BoundIntMeta, cls).__call__() # error: [invalid-super-argument] | ^^^^^^^^^^^^^^^^^^^^^^^^ - | info: Type variable `T` has upper bound `int` info: `type[int]` is not an instance or subclass of `` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Invalid_Usages_-_Diagnostic_when_the_\342\200\246_(93e8ab913ead83b2).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Invalid_Usages_-_Diagnostic_when_the_\342\200\246_(93e8ab913ead83b2).snap" index 456d3f18be..094e39d4ef 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Invalid_Usages_-_Diagnostic_when_the_\342\200\246_(93e8ab913ead83b2).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Invalid_Usages_-_Diagnostic_when_the_\342\200\246_(93e8ab913ead83b2).snap" @@ -34,6 +34,5 @@ error[invalid-super-argument]: Argument is not a valid class | 11 | super(A, A()) # error: [invalid-super-argument] | ^^^^^^^^^^^^^ Argument has type `.A @ src/mdtest_snippet.py:6:15'> | .A @ src/mdtest_snippet.py:9:15'>` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(3d4f2229d00f8d86).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(3d4f2229d00f8d86).snap" index 036133bd83..1a5b7bf330 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(3d4f2229d00f8d86).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(3d4f2229d00f8d86).snap" @@ -40,6 +40,5 @@ error[invalid-context-manager]: Object of type `GoodManager | BadManager` cannot | 16 | with context_expr as f: | ^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(718dcfd7e6ed9829).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(718dcfd7e6ed9829).snap" index f845368fbe..1e75bdf2b2 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(718dcfd7e6ed9829).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(718dcfd7e6ed9829).snap" @@ -39,7 +39,6 @@ error[invalid-context-manager]: Object of type `GoodManager | MissingExitManager | 15 | with context_expr as f: | ^^^^^^^^^^^^ - | info: `NotAContextManager` does not implement `__enter__` or `__exit__` info: `MissingExitManager` does not implement `__exit__` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(8686e7748a7c975).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(8686e7748a7c975).snap" index 02cebd2c08..c53e9081f6 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(8686e7748a7c975).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/sync.md_-_With_statements_-_Context_expression_w\342\200\246_(8686e7748a7c975).snap" @@ -35,7 +35,6 @@ error[invalid-context-manager]: Object of type `Manager1 | NotAContextManager` c | 11 | with context_expr as f: | ^^^^^^^^^^^^ - | info: `NotAContextManager` does not implement `__enter__` or `__exit__` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_\342\200\246_(f45f1da2f8ca693d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_\342\200\246_(f45f1da2f8ca693d).snap" index 4929923292..8fff86797c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_\342\200\246_(f45f1da2f8ca693d).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_\342\200\246_(f45f1da2f8ca693d).snap" @@ -40,7 +40,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 15 | a < b < b | ^^^^^ - | info: `__bool__` on `NotBoolable | Literal[False]` must be callable ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" index 5704deda5d..f6b01187be 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" @@ -45,16 +45,15 @@ error[invalid-method-override]: Invalid override of method `__eq__` | 136 | def __eq__(self, value: object, /) -> bool: ... | -------------------------------------- `object.__eq__` defined here - | info: incompatible return types: `NotBoolable` is not assignable to `bool` info: This violates the Liskov Substitution Principle help: It is recommended for `__eq__` to work with arbitrary objects, for example: -help +help: help: def __eq__(self, other: object) -> bool: help: if not isinstance(other, A): help: return False help: return -help +help: ``` @@ -64,7 +63,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 10 | reveal_type((A(),) == (A(),)) # revealed: bool | ^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` @@ -75,7 +73,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 12 | reveal_type((A(), "x") == (A(), "y")) # revealed: Literal[False] | ^^^^^^^^^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` @@ -86,7 +83,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 14 | reveal_type((A(),) != (A(), 0)) # revealed: Literal[True] | ^^^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Heterogeneous_-_Value_Comparisons_-_Comparison_Unsupport\342\200\246_(966dd82bd3668d0e).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Heterogeneous_-_Value_Comparisons_-_Comparison_Unsupport\342\200\246_(966dd82bd3668d0e).snap" index 4c94dacd08..379e13c47c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Heterogeneous_-_Value_Comparisons_-_Comparison_Unsupport\342\200\246_(966dd82bd3668d0e).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Heterogeneous_-_Value_Comparisons_-_Comparison_Unsupport\342\200\246_(966dd82bd3668d0e).snap" @@ -53,7 +53,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[Literal[1], Literal["hello"]]` | Has type `tuple[Literal[1], Literal[2]]` - | info: Operation fails because operator `<` is not supported between the tuple elements at index 2 (of type `Literal[2]` and `Literal["hello"]`) ``` @@ -67,7 +66,6 @@ error[unsupported-operator]: Unsupported `<=` operation | | | | | Has type `tuple[Literal[1], Literal["hello"]]` | Has type `tuple[Literal[1], Literal[2]]` - | info: Operation fails because operator `<=` is not supported between the tuple elements at index 2 (of type `Literal[2]` and `Literal["hello"]`) ``` @@ -81,7 +79,6 @@ error[unsupported-operator]: Unsupported `>` operation | | | | | Has type `tuple[Literal[1], Literal["hello"]]` | Has type `tuple[Literal[1], Literal[2]]` - | info: Operation fails because operator `>` is not supported between the tuple elements at index 2 (of type `Literal[2]` and `Literal["hello"]`) ``` @@ -95,7 +92,6 @@ error[unsupported-operator]: Unsupported `>=` operation | | | | | Has type `tuple[Literal[1], Literal["hello"]]` | Has type `tuple[Literal[1], Literal[2]]` - | info: Operation fails because operator `>=` is not supported between the tuple elements at index 2 (of type `Literal[2]` and `Literal["hello"]`) ``` @@ -108,7 +104,6 @@ error[unsupported-operator]: Unsupported `<` operation | -----------^^^----------- | | | Both operands have type `tuple[object]` - | info: Operation fails because operator `<` is not supported between the tuple elements at index 1 (both of type `object`) ``` @@ -121,7 +116,6 @@ error[unsupported-operator]: Unsupported `<` operation | -----------^^^----------- | | | Both operands have type `tuple[object]` - | info: Operation fails because operator `<` is not supported between the tuple elements at index 1 (both of type `object`) ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Tuples_with_Prefixes\342\200\246_(c25079c01f6d8eb3).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Tuples_with_Prefixes\342\200\246_(c25079c01f6d8eb3).snap" index 254cb309ea..0eae1e0107 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Tuples_with_Prefixes\342\200\246_(c25079c01f6d8eb3).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Tuples_with_Prefixes\342\200\246_(c25079c01f6d8eb3).snap" @@ -39,7 +39,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[str, *tuple[int, ...]]` | Has type `tuple[int, *tuple[str, ...]]` - | info: Operation fails because operator `<` is not supported between objects of type `int` and `str` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Unsupported_Comparis\342\200\246_(400a427b33d53e00).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Unsupported_Comparis\342\200\246_(400a427b33d53e00).snap" index 6d81e32027..144450cf1e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Unsupported_Comparis\342\200\246_(400a427b33d53e00).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Unsupported_Comparis\342\200\246_(400a427b33d53e00).snap" @@ -65,7 +65,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[str, ...]` | Has type `tuple[int, ...]` - | info: Operation fails because operator `<` is not supported between objects of type `int` and `str` ``` @@ -79,7 +78,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[int, ...]` | Has type `tuple[str, ...]` - | info: Operation fails because operator `<` is not supported between objects of type `str` and `int` ``` @@ -93,7 +91,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[str]` | Has type `tuple[int, ...]` - | info: Operation fails because operator `<` is not supported between objects of type `int` and `str` ``` @@ -107,7 +104,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[int, ...]` | Has type `tuple[str]` - | info: Operation fails because operator `<` is not supported between objects of type `str` and `int` ``` @@ -121,7 +117,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[int, ...]` | Has type `tuple[int, str]` - | info: Operation fails because operator `<` is not supported between objects of type `str` and `int` ``` @@ -135,7 +130,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[int, str]` | Has type `tuple[int, ...]` - | info: Operation fails because operator `<` is not supported between objects of type `int` and `str` ``` @@ -149,7 +143,6 @@ error[unsupported-operator]: Unsupported `<` operation | | | | | Has type `tuple[int, str]` | Has type `tuple[str, ...]` - | info: Operation fails because operator `<` is not supported between objects of type `str` and `int` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Class_header_validat\342\200\246_(25381f371caa1401).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Class_header_validat\342\200\246_(25381f371caa1401).snap" index 954fafa461..0c16ade179 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Class_header_validat\342\200\246_(25381f371caa1401).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Class_header_validat\342\200\246_(25381f371caa1401).snap" @@ -50,7 +50,6 @@ error[invalid-typed-dict-header]: TypedDict class `Foo` can only inherit from Ty | 349 | class int: | --- `int` defined here - | ``` @@ -65,7 +64,6 @@ error[invalid-typed-dict-header]: TypedDict class `Foo2` can only inherit from T | 113 | class object: | ------ `object` defined here - | ``` @@ -75,7 +73,6 @@ error[invalid-argument-type]: Invalid argument to parameter `total` in `TypedDic | 7 | class Bar(TypedDict, total=42): ... # error: [invalid-argument-type] | ^^^^^^^^ Expected either `True` or `False`, got object of type `Literal[42]` - | ``` @@ -85,7 +82,6 @@ error[invalid-argument-type]: Invalid argument to parameter `closed` in `TypedDi | 8 | class Baz(TypedDict, closed=None): ... # error: [invalid-argument-type] | ^^^^^^^^^^^ Expected either `True` or `False`, got object of type `None` - | ``` @@ -95,7 +91,6 @@ error[invalid-argument-type]: Invalid argument to parameter `total` in `TypedDic | 10 | class VeryDynamic(TypedDict, total=is_total): ... # error: [invalid-argument-type] | ^^^^^^^^^^^^^^ Expected either `True` or `False`, got object of type `bool` - | ``` @@ -105,7 +100,6 @@ error[unknown-argument]: Unknown keyword argument `weird` in `TypedDict` definit | 11 | class Bazzzz(TypedDict, weird=56): ... # error: [unknown-argument] | ^^^^^^^^ - | ``` @@ -115,7 +109,6 @@ error[invalid-typed-dict-header]: Custom metaclasses are not supported in `Typed | 14 | class Spam(TypedDict, metaclass=ABCMeta): ... # error: [invalid-typed-dict-header] | ^^^^^^^^^^^^^^^^^ - | ``` @@ -125,7 +118,6 @@ error[invalid-typed-dict-header]: Custom metaclasses are not supported in `Typed | 18 | class Ham(TypedDict, metaclass=type): ... # error: [invalid-typed-dict-header] | ^^^^^^^^^^^^^^ - | ``` @@ -135,7 +127,6 @@ error[invalid-typed-dict-header]: Keyword-variadic arguments are not supported i | 20 | class Eggs(TypedDict, **kwargs): ... # error: [invalid-typed-dict-header] | ^^^^^^^^ - | ``` @@ -145,7 +136,6 @@ error[invalid-argument-type]: Invalid argument to parameter `total` in `TypedDic | 21 | class Qux(TypedDict, total=1 == 1): ... # error: [invalid-argument-type] | ^^^^^^^^^^^^ Expected either `True` or `False`, got object of type `Literal[True]` - | ``` @@ -155,6 +145,5 @@ error[invalid-argument-type]: Invalid argument to parameter `closed` in `TypedDi | 22 | class Quux(TypedDict, closed=1 == 1): ... # error: [invalid-argument-type] | ^^^^^^^^^^^^^ Expected either `True` or `False`, got object of type `Literal[True]` - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap index 33c4c3732b..aedd64b990 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Diagnostics_(e5289abf5c570c29).snap @@ -76,14 +76,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md ``` error[invalid-key]: Unknown key "nane" for TypedDict `Person` - --> src/mdtest_snippet.py:8:5 + --> src/mdtest_snippet.py:8:12 | 8 | person["nane"] # error: [invalid-key] | ------ ^^^^^^ Did you mean "name"? | | | TypedDict `Person` | - | 7 | def access_invalid_literal_string_key(person: Person): - person["nane"] # error: [invalid-key] 8 + person["name"] # error: [invalid-key] @@ -95,13 +94,12 @@ note: This is an unsafe fix and may change runtime behavior ``` error[invalid-key]: Unknown key "nane" for TypedDict `Person` - --> src/mdtest_snippet.py:13:5 + --> src/mdtest_snippet.py:13:12 | 13 | person[NAME_KEY] # error: [invalid-key] | ------ ^^^^^^^^ Unknown key "nane" - did you mean "name"? | | | TypedDict `Person` - | ``` @@ -111,39 +109,35 @@ error[invalid-key]: TypedDict `Person` can only be subscripted with a string lit | 16 | person[str_key] # error: [invalid-key] | ^^^^^^^ - | ``` ``` error[invalid-assignment]: Invalid assignment to key "age" with declared type `int | None` on TypedDict `Person` - --> src/mdtest_snippet.py:19:5 + --> src/mdtest_snippet.py:19:21 | 19 | person["age"] = "42" # error: [invalid-assignment] | ------ ----- ^^^^ value of type `Literal["42"]` | | | | | key has declared type `int | None` | TypedDict `Person` - | info: Item declaration --> src/mdtest_snippet.py:5:5 | 5 | age: int | None | --------------- Item declared here - | ``` ``` error[invalid-key]: Unknown key "nane" for TypedDict `Person` - --> src/mdtest_snippet.py:22:5 + --> src/mdtest_snippet.py:22:12 | 22 | person["nane"] = "Alice" # error: [invalid-key] | ------ ^^^^^^ Did you mean "name"? | | | TypedDict `Person` | - | 21 | def write_to_non_existing_key(person: Person): - person["nane"] = "Alice" # error: [invalid-key] 22 + person["name"] = "Alice" # error: [invalid-key] @@ -159,61 +153,55 @@ error[invalid-key]: TypedDict `Person` can only be subscripted with a string lit | 25 | person[str_key] = "Alice" # error: [invalid-key] | ^^^^^^^ - | ``` ``` error[invalid-key]: Unknown key "unknown" for TypedDict `Person` - --> src/mdtest_snippet.py:29:21 + --> src/mdtest_snippet.py:29:50 | 29 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} | -----------------------------^^^^^^^^^-------- | | | | | Unknown key "unknown" | TypedDict `Person` - | ``` ``` error[invalid-key]: Unknown key "unknown" for TypedDict `Person` - --> src/mdtest_snippet.py:32:11 + --> src/mdtest_snippet.py:32:38 | 32 | bob = Person(name="Bob", age=25, unknown="Bar") | ------ TypedDict `Person` ^^^^^^^^^^^^^ Unknown key "unknown" - | ``` ``` error[invalid-assignment]: Cannot assign to key "id" on TypedDict `Employee` - --> src/mdtest_snippet.py:40:5 + --> src/mdtest_snippet.py:40:14 | 40 | employee["id"] = 42 # error: [invalid-assignment] | -------- ^^^^ key is marked read-only | | | TypedDict `Employee` - | info: Item declaration --> src/mdtest_snippet.py:36:5 | 36 | id: ReadOnly[int] | ----------------- Read-only item declared here - | ``` ``` error[invalid-key]: Unknown key "nane" for TypedDict `Person` - --> src/mdtest_snippet.py:43:5 + --> src/mdtest_snippet.py:43:12 | 43 | person['nane'] = "Alice" # fmt: skip | ------ ^^^^^^ Did you mean 'name'? | | | TypedDict `Person` | - | 42 | # error: [invalid-key] - person['nane'] = "Alice" # fmt: skip 43 + person['name'] = "Alice" # fmt: skip @@ -229,13 +217,11 @@ error[invalid-typed-dict-field]: Cannot overwrite TypedDict field `name` | 48 | name: int # error: [invalid-typed-dict-field] | ^^^^^^^^^ Inherited mutable field type `str` is incompatible with `int` - | info: Field declaration --> src/mdtest_snippet.py:45:5 | 45 | name: str | --------- Inherited field `name` declared here on base `MovieBase` - | ``` @@ -245,18 +231,15 @@ error[invalid-typed-dict-field]: Cannot overwrite TypedDict field `value` while | 56 | class BadMerge(LeftBase, RightBase): # error: [invalid-typed-dict-field] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inherited mutable field type `str` is incompatible with `int` - | info: Field declaration --> src/mdtest_snippet.py:51:5 | 51 | value: int | ---------- Field `value` already inherited from another base here - | info: Field declaration --> src/mdtest_snippet.py:54:5 | 54 | value: str | ---------- Inherited field `value` declared here on base `RightBase` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Error_cases_-_`typing.TypedDict`_i\342\200\246_(9df67eb93e3df341).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Error_cases_-_`typing.TypedDict`_i\342\200\246_(9df67eb93e3df341).snap" index 75b6c4ca3f..647fabb729 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Error_cases_-_`typing.TypedDict`_i\342\200\246_(9df67eb93e3df341).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Error_cases_-_`typing.TypedDict`_i\342\200\246_(9df67eb93e3df341).snap" @@ -27,7 +27,6 @@ error[invalid-type-form]: The special form `typing.TypedDict` is not allowed in | 4 | x: TypedDict = {"name": "Alice"} | ^^^^^^^^^ - | help: You might have meant to use a concrete TypedDict or `collections.abc.Mapping[str, object]` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with\342\200\246_(4b18755412dfaff1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with\342\200\246_(4b18755412dfaff1).snap" index 11ff93b1a4..35fa56551c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with\342\200\246_(4b18755412dfaff1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Function_syntax_with\342\200\246_(4b18755412dfaff1).snap" @@ -119,7 +119,6 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 4 | TypedDict("Foo", {}, {}) | ^^ - | ``` @@ -129,7 +128,6 @@ error[missing-argument]: No arguments provided for required parameters `typename | 6 | TypedDict() | ^^^^^^^^^^^ - | ``` @@ -139,7 +137,6 @@ error[missing-argument]: No argument provided for required parameter `fields` of | 8 | TypedDict("Foo") | ^^^^^^^^^^^^^^^^ - | ``` @@ -149,7 +146,6 @@ error[invalid-argument-type]: Invalid argument to parameter `typename` of `Typed | 11 | Bad1 = TypedDict(123, {"name": str}) | ^^^ Expected `str`, found `Literal[123]` - | ``` @@ -159,7 +155,6 @@ warning[mismatched-type-name]: The name passed to `TypedDict` must match the var | 14 | BadTypedDict3 = TypedDict("WrongName", {"name": str}) | ^^^^^^^^^^^ Expected "BadTypedDict3", got "WrongName" - | ``` @@ -169,7 +164,6 @@ warning[mismatched-type-name]: The name passed to `TypedDict` must match the var | 19 | Y = TypedDict(x, {}) | ^ Expected "Y", got variable of type `str` - | ``` @@ -179,7 +173,6 @@ error[invalid-argument-type]: Expected a dict literal for parameter `fields` of | 28 | Bad2 = TypedDict("Bad2", "not a dict") | ^^^^^^^^^^^^ - | ``` @@ -189,7 +182,6 @@ error[invalid-argument-type]: Expected a dict literal for parameter `fields` of | 30 | TypedDict("Bad2", "not a dict") | ^^^^^^^^^^^^ - | ``` @@ -199,7 +191,6 @@ error[invalid-argument-type]: Expected a dict literal for parameter `fields` of | 36 | Bad2b = TypedDict("Bad2b", get_fields()) | ^^^^^^^^^^^^ - | ``` @@ -209,7 +200,6 @@ error[invalid-argument-type]: Invalid argument to parameter `total` of `TypedDic | 39 | Bad3 = TypedDict("Bad3", {"name": str}, total="not a bool") | ^^^^^^^^^^^^ Expected either `True` or `False`, got object of type `Literal["not a bool"]` - | ``` @@ -219,7 +209,6 @@ error[invalid-argument-type]: Invalid argument to parameter `closed` of `TypedDi | 42 | Bad4 = TypedDict("Bad4", {"name": str}, closed=123) | ^^^ Expected either `True` or `False`, got object of type `Literal[123]` - | ``` @@ -229,7 +218,6 @@ error[invalid-argument-type]: Variadic positional arguments are not supported in | 48 | Bad5 = TypedDict(*tup) | ^^^^ - | ``` @@ -239,7 +227,6 @@ error[invalid-argument-type]: Variadic keyword arguments are not supported in `T | 51 | Bad6 = TypedDict("Bad6", {"name": str}, **kw) | ^^^^ - | ``` @@ -249,7 +236,6 @@ error[invalid-argument-type]: Variadic positional and keyword arguments are not | 54 | Bad7 = TypedDict(*tup, "foo", "bar", **kw) | ^^^^ ---- - | ``` @@ -259,7 +245,6 @@ error[invalid-argument-type]: Variadic keyword arguments are not supported in `T | 58 | Bad7b = TypedDict("Bad7b", **kw, random_other_arg=56) | ^^^^ - | ``` @@ -269,7 +254,6 @@ error[unknown-argument]: Argument `random_other_arg` does not match any known pa | 58 | Bad7b = TypedDict("Bad7b", **kw, random_other_arg=56) | ^^^^^^^^^^^^^^^^^^^ - | ``` @@ -279,7 +263,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 63 | Bad8 = TypedDict("Bad8", {**kwargs}) | ^^^^^^ - | ``` @@ -289,7 +272,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 65 | TypedDict("Bad8", {**kwargs}) | ^^^^^^ - | ``` @@ -299,7 +281,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 68 | Bad81 = TypedDict("Bad81", {**kwargs, **kwargs}) | ^^^^^^ - | ``` @@ -309,7 +290,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 68 | Bad81 = TypedDict("Bad81", {**kwargs, **kwargs}) | ^^^^^^ - | ``` @@ -319,7 +299,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 71 | TypedDict("Bad81", {**kwargs, **kwargs}) | ^^^^^^ - | ``` @@ -329,7 +308,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 71 | TypedDict("Bad81", {**kwargs, **kwargs}) | ^^^^^^ - | ``` @@ -339,7 +317,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 74 | Bad82 = TypedDict("Bad82", {**kwargs, "foo": []}) | ^^^^^^ - | ``` @@ -349,7 +326,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a typ | 74 | Bad82 = TypedDict("Bad82", {**kwargs, "foo": []}) | ^^ - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -361,7 +337,6 @@ error[invalid-argument-type]: Keyword splats are not allowed in the `fields` par | 77 | TypedDict("Bad82", {**kwargs, "foo": []}) | ^^^^^^ - | ``` @@ -371,7 +346,6 @@ error[invalid-type-form]: List literals are not allowed in this context in a typ | 77 | TypedDict("Bad82", {**kwargs, "foo": []}) | ^^ - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -383,7 +357,6 @@ error[invalid-argument-type]: Expected a string-literal key in the `fields` dict | 85 | Bad9 = TypedDict("Bad9", {name: int}) | ^^^^ Found `str` - | ``` @@ -393,7 +366,6 @@ error[invalid-argument-type]: Expected a string-literal key in the `fields` dict | 89 | Bad10 = TypedDict("Bad10", {name: 42}) | ^^^^ Found `str` - | ``` @@ -403,7 +375,6 @@ error[invalid-type-form]: Int literals are not allowed in this context in a type | 89 | Bad10 = TypedDict("Bad10", {name: 42}) | ^^ Did you mean `typing.Literal[42]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -415,7 +386,6 @@ error[invalid-argument-type]: Expected a string-literal key in the `fields` dict | 93 | class Bad11(TypedDict("Bad11", {name: 42})): ... | ^^^^ Found `str` - | ``` @@ -425,7 +395,6 @@ error[invalid-type-form]: Int literals are not allowed in this context in a type | 93 | class Bad11(TypedDict("Bad11", {name: 42})): ... | ^^ Did you mean `typing.Literal[42]`? - | info: See the following page for a reference on valid type expressions: info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions @@ -437,6 +406,5 @@ error[invalid-argument-type]: Invalid argument to parameter `typename` of `Typed | 96 | class Bad12(TypedDict(123, {"field": int})): ... | ^^^ Expected `str`, found `Literal[123]` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Only_annotated_decla\342\200\246_(bef70731cae5b8af).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Only_annotated_decla\342\200\246_(bef70731cae5b8af).snap" index 664164e876..73aa2e5512 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Only_annotated_decla\342\200\246_(bef70731cae5b8af).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Only_annotated_decla\342\200\246_(bef70731cae5b8af).snap" @@ -48,7 +48,6 @@ error[invalid-typed-dict-statement]: invalid statement in TypedDict class body | 17 | 42 | ^^ - | info: Only annotated declarations (`: `) are allowed. ``` @@ -59,7 +58,6 @@ error[invalid-typed-dict-statement]: TypedDict item cannot have a value | 19 | b: str = "hello" | ^^^^^^^ - | ``` @@ -69,7 +67,6 @@ error[invalid-typed-dict-statement]: TypedDict class cannot have methods | 21 | def bar(self): ... | ^^^^^^^^^^^^^^^^^^ - | ``` @@ -80,6 +77,5 @@ error[invalid-typed-dict-statement]: TypedDict class cannot have methods 24 | / def baz(self): 25 | | pass | |____________^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni\342\200\246_(75ac240a2d1f7108).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni\342\200\246_(75ac240a2d1f7108).snap" index 4dee562cbc..11b404c26e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni\342\200\246_(75ac240a2d1f7108).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni\342\200\246_(75ac240a2d1f7108).snap" @@ -34,7 +34,6 @@ warning[redundant-cast]: Value is already of type `Foo2` | 10 | _ = cast(Foo2, foo) # error: [redundant-cast] | ^^^^^^^^^^^^^^^ - | help: Remove the redundant `cast` | 9 | foo: Foo2 = {"x": 1} @@ -51,7 +50,6 @@ warning[redundant-cast]: Value is already of type `Bar2` | 11 | _ = cast(Bar2, foo) # error: [redundant-cast] | ^^^^^^^^^^^^^^^ - | info: `Bar2` is equivalent to `Foo2` help: Remove the redundant `cast` | diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_A_smaller_scale_exam\342\200\246_(c24ecd8582e5eb2f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_A_smaller_scale_exam\342\200\246_(c24ecd8582e5eb2f).snap" index d3cd4f9d36..3c6c138089 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_A_smaller_scale_exam\342\200\246_(c24ecd8582e5eb2f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_A_smaller_scale_exam\342\200\246_(c24ecd8582e5eb2f).snap" @@ -37,13 +37,11 @@ error[invalid-argument-type]: Argument to function `f2` is incorrect | 14 | x = f(3) | ^ Expected `str`, found `Literal[3]` - | info: Function defined here --> src/mdtest_snippet.py:4:5 | 4 | def f2(name: str) -> int: | ^^ --------- Parameter declared here - | info: Union variant `def f2(name: str) -> int` is incompatible with this call site info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int)` @@ -55,7 +53,6 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 14 | x = f(3) | ^ - | info: Union variant `def f1() -> int` is incompatible with this call site info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int)` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Multiple_variants_bu\342\200\246_(d840ac443ca8ec7f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Multiple_variants_bu\342\200\246_(d840ac443ca8ec7f).snap" index afc0cac027..ab68afe402 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Multiple_variants_bu\342\200\246_(d840ac443ca8ec7f).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Multiple_variants_bu\342\200\246_(d840ac443ca8ec7f).snap" @@ -36,13 +36,11 @@ error[invalid-argument-type]: Argument to function `f2` is incorrect | 13 | x = f(3) | ^ Expected `str`, found `Literal[3]` - | info: Function defined here --> src/mdtest_snippet.py:4:5 | 4 | def f2(name: str) -> int: | ^^ --------- Parameter declared here - | info: Union variant `def f2(name: str) -> int` is incompatible with this call site info: Attempted to call union type `(def f1(a: int) -> int) | (def f2(name: str) -> int)` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Attribute_access_on_\342\200\246_(7bdb97302c27c412).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Attribute_access_on_\342\200\246_(7bdb97302c27c412).snap" index c42fc43dc8..7d4a0a1fe8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Attribute_access_on_\342\200\246_(7bdb97302c27c412).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Attribute_access_on_\342\200\246_(7bdb97302c27c412).snap" @@ -40,7 +40,6 @@ error[invalid-argument-type]: Argument to bound method `A.foo` is incorrect | 17 | return x.foo(y) | ^^^^^^^^ Argument type `T@_` does not satisfy upper bound `A` of type variable `Self` - | info: Union variant `bound method T@_.foo(x: int) -> T@_` is incompatible with this call site info: Attempted to call union type `(bound method T@_.foo(x: int) -> T@_) | (bound method T@_.foo(x: str) -> T@_)` @@ -52,7 +51,6 @@ error[invalid-argument-type]: Argument to bound method `B.foo` is incorrect | 17 | return x.foo(y) | ^^^^^^^^ Argument type `T@_` does not satisfy upper bound `B` of type variable `Self` - | info: Union variant `bound method T@_.foo(x: str) -> T@_` is incompatible with this call site info: Attempted to call union type `(bound method T@_.foo(x: int) -> T@_) | (bound method T@_.foo(x: str) -> T@_)` @@ -64,13 +62,11 @@ error[invalid-argument-type]: Argument to bound method `B.foo` is incorrect | 17 | return x.foo(y) | ^ Expected `str`, found `int` - | info: Method defined here --> src/mdtest_snippet.py:8:9 | 8 | def foo(self, x: str) -> Self: | ^^^ ------ Parameter declared here - | info: Union variant `bound method T@_.foo(x: str) -> T@_` is incompatible with this call site info: Attempted to call union type `(bound method T@_.foo(x: int) -> T@_) | (bound method T@_.foo(x: str) -> T@_)` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_keyword_argume\342\200\246_(ad1d489710ee2a34).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_keyword_argume\342\200\246_(ad1d489710ee2a34).snap" index 43cc83213d..f5fd256433 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_keyword_argume\342\200\246_(ad1d489710ee2a34).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_keyword_argume\342\200\246_(ad1d489710ee2a34).snap" @@ -37,7 +37,6 @@ error[parameter-already-assigned]: Multiple values provided for parameter `name` | 14 | y = f("foo", name="bar", unknown="quux") | ^^^^^^^^^^ - | info: Union variant `def f1(name: str) -> int` is incompatible with this call site info: Attempted to call union type `(def f1(name: str) -> int) | (def any(...) -> int)` @@ -49,7 +48,6 @@ error[unknown-argument]: Argument `unknown` does not match any known parameter o | 14 | y = f("foo", name="bar", unknown="quux") | ^^^^^^^^^^^^^^ - | info: Union variant `def f1(name: str) -> int` is incompatible with this call site info: Attempted to call union type `(def f1(name: str) -> int) | (def any(...) -> int)` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_non-keyword_re\342\200\246_(707b284610419a54).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_non-keyword_re\342\200\246_(707b284610419a54).snap" index fc22cf876d..1081975618 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_non-keyword_re\342\200\246_(707b284610419a54).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Cover_non-keyword_re\342\200\246_(707b284610419a54).snap" @@ -83,7 +83,6 @@ error[call-non-callable]: Object of type `Literal[5]` is not callable | 60 | x = f(3) | ^^^^ - | info: Union variant `Literal[5]` is incompatible with this call site info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | ... omitted 5 union elements` @@ -95,7 +94,6 @@ error[call-non-callable]: Object of type `PossiblyNotCallable` is not callable ( | 60 | x = f(3) | ^^^^ - | info: Union variant `PossiblyNotCallable` is incompatible with this call site info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | ... omitted 5 union elements` @@ -107,7 +105,6 @@ error[missing-argument]: No argument provided for required parameter `b` of func | 60 | x = f(3) | ^^^^ - | info: Union variant `def f3(a: int, b: int) -> int` is incompatible with this call site info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | ... omitted 5 union elements` @@ -119,14 +116,12 @@ error[no-matching-overload]: No overload of function `f6` matches arguments | 60 | x = f(3) | ^^^^ - | info: First overload defined here --> src/mdtest_snippet.py:23:1 | 23 | / @overload 24 | | def f6() -> None: ... | |_____________________^ First overload defined here - | info: Possible overloads for function `f6`: info: () -> None info: (x: str, y: str) -> str @@ -135,7 +130,6 @@ info: Overload implementation defined here | 27 | def f6(x: str | None = None, y: str | None = None) -> str | None: | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info: Union variant `Overload[() -> None, (x: str, y: str) -> str]` is incompatible with this call site info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | ... omitted 5 union elements` @@ -147,13 +141,11 @@ error[invalid-argument-type]: Argument to function `f2` is incorrect | 60 | x = f(3) | ^ Expected `str`, found `Literal[3]` - | info: Function defined here --> src/mdtest_snippet.py:7:5 | 7 | def f2(name: str) -> int: | ^^ --------- Parameter declared here - | info: Union variant `def f2(name: str) -> int` is incompatible with this call site info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | ... omitted 5 union elements` @@ -165,13 +157,11 @@ error[invalid-argument-type]: Argument to function `f4` is incorrect | 60 | x = f(3) | ^ Argument type `Literal[3]` does not satisfy upper bound `str` of type variable `T` - | info: Type variable defined here --> src/mdtest_snippet.py:13:8 | 13 | def f4[T: str](x: T) -> int: | ^^^^^^ - | info: Union variant `def f4[T](x: T) -> int` is incompatible with this call site info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | ... omitted 5 union elements` @@ -183,13 +173,11 @@ error[invalid-argument-type]: Argument to function `f5` is incorrect | 60 | x = f(3) | ^ Expected `str`, found `Literal[3]` - | info: Matching overload defined here --> src/mdtest_snippet.py:19:5 | 19 | def f5(x: str) -> str: ... | ^^ ------ Parameter declared here - | info: Non-matching overloads for function `f5`: info: () -> None info: Union variant `Overload[() -> None, (x: str) -> str]` is incompatible with this call site @@ -203,7 +191,6 @@ error[too-many-positional-arguments]: Too many positional arguments to function | 60 | x = f(3) | ^ - | info: Union variant `def f1() -> int` is incompatible with this call site info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | ... omitted 5 union elements` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Truncation_for_long_\342\200\246_(ec94b5e857284ef3).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Truncation_for_long_\342\200\246_(ec94b5e857284ef3).snap" index 1f809315b5..50eecabcce 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Truncation_for_long_\342\200\246_(ec94b5e857284ef3).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Try_to_cover_all_pos\342\200\246_-_Truncation_for_long_\342\200\246_(ec94b5e857284ef3).snap" @@ -39,12 +39,10 @@ error[invalid-argument-type]: Argument to function `f1` is incorrect | 16 | f1(x) | ^ Expected `Literal[1, 2, 3, 4, 5, ... omitted 3 literals] | A | B | ... omitted 4 union elements`, found `int` - | info: Function defined here --> src/mdtest_snippet.py:10:5 | 10 | def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) -> int: | ^^ ----------------------------------------------------------- Parameter declared here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Union_with_overloade\342\200\246_(4408ade1316b97c0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Union_with_overloade\342\200\246_(4408ade1316b97c0).snap" index cdec573149..cbaf244dbc 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Union_with_overloade\342\200\246_(4408ade1316b97c0).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Union_with_overloade\342\200\246_(4408ade1316b97c0).snap" @@ -27,7 +27,6 @@ error[unresolved-attribute]: Attribute `split` is not defined on `int` in union | 4 | x.split(" ") | ^^^^^^^ - | ``` @@ -37,7 +36,6 @@ error[invalid-argument-type]: Argument to bound method `bytes.split` is incorrec | 4 | x.split(" ") | ^^^ Expected `Buffer | None`, found `Literal[" "]` - | info: type `Literal[" "]` is not assignable to any element of the union `Buffer | None` info: ├── type `Literal[" "]` is not assignable to protocol `Buffer` info: │ └── protocol member `__buffer__` is not defined on type `Literal[" "]` @@ -47,7 +45,6 @@ info: Method defined here | 1843 | def split(self, sep: ReadableBuffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]: | ^^^^^ --------------------------------- Parameter declared here - | info: Union variant `bound method bytes.split(sep: Buffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]` is incompatible with this call site info: Attempted to call union type `(bound method bytes.split(sep: Buffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]) | (bound method str.split(sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str])` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unknown_argument.md_-_Unknown_argument_dia\342\200\246_(f419c2a8e2ce2412).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unknown_argument.md_-_Unknown_argument_dia\342\200\246_(f419c2a8e2ce2412).snap" index 30e8f26de4..9ce0cfb6fe 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unknown_argument.md_-_Unknown_argument_dia\342\200\246_(f419c2a8e2ce2412).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unknown_argument.md_-_Unknown_argument_dia\342\200\246_(f419c2a8e2ce2412).snap" @@ -47,13 +47,11 @@ error[unknown-argument]: Argument `d` does not match any known parameter of func | 3 | f(a=1, b=2, c=3, d=42) # error: [unknown-argument] | ^^^^ - | info: Function signature here --> src/module.py:1:5 | 1 | def f(a, b, c=42): ... | ^^^^^^^^^^^^^ - | ``` @@ -63,7 +61,6 @@ error[unknown-argument]: Argument `d` does not match any known parameter of func | 12 | h(a=1, b=2, d=42) | ^^^^ - | info: Union variant `def f(a, b, c=42) -> Unknown` is incompatible with this call site info: Attempted to call union type `(def f(a, b, c=42) -> Unknown) | (def g(a, b) -> Unknown)` @@ -75,7 +72,6 @@ error[unknown-argument]: Argument `d` does not match any known parameter of func | 12 | h(a=1, b=2, d=42) | ^^^^ - | info: Union variant `def g(a, b) -> Unknown` is incompatible with this call site info: Attempted to call union type `(def f(a, b, c=42) -> Unknown) | (def g(a, b) -> Unknown)` @@ -87,12 +83,10 @@ error[unknown-argument]: Argument `c` does not match any known parameter of boun | 14 | Foo().method(a=1, b=2, c=3) # error: [unknown-argument] | ^^^ - | info: Method signature here --> src/module.py:5:9 | 5 | def method(self, a, b): ... | ^^^^^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_An_unresolvable_impo\342\200\246_(72d090df51ea97b8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_An_unresolvable_impo\342\200\246_(72d090df51ea97b8).snap" index 561207fe58..51d6e37be9 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_An_unresolvable_impo\342\200\246_(72d090df51ea97b8).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_An_unresolvable_impo\342\200\246_(72d090df51ea97b8).snap" @@ -26,7 +26,6 @@ error[unresolved-import]: Cannot resolve imported module `does_not_exist` | 1 | import does_not_exist # error: [unresolved-import] | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_a_\342\200\246_(12d4a70b7fc67cc6).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_a_\342\200\246_(12d4a70b7fc67cc6).snap" index 8aa1e73adc..1d2b3fe720 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_a_\342\200\246_(12d4a70b7fc67cc6).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_a_\342\200\246_(12d4a70b7fc67cc6).snap" @@ -31,6 +31,5 @@ error[unresolved-import]: Module `a` has no member `does_not_exist` | 1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import] | ^^^^^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(6cff507dc64a1bff).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(6cff507dc64a1bff).snap" index 7ff5d67069..3675b71844 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(6cff507dc64a1bff).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(6cff507dc64a1bff).snap" @@ -26,7 +26,6 @@ error[unresolved-import]: Cannot resolve imported module `.does_not_exist.foo.ba | 1 | from .does_not_exist.foo.bar import add # error: [unresolved-import] | ^^^^^^^^^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9da56616d6332a83).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9da56616d6332a83).snap" index 308a61eedc..8ac259aae2 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9da56616d6332a83).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9da56616d6332a83).snap" @@ -26,7 +26,6 @@ error[unresolved-import]: Cannot resolve imported module `.does_not_exist` | 1 | from .does_not_exist import add # error: [unresolved-import] | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9fa713dfa17cc404).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9fa713dfa17cc404).snap" index d8cc9b5c31..5a7816e521 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9fa713dfa17cc404).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_an\342\200\246_(9fa713dfa17cc404).snap" @@ -26,7 +26,6 @@ error[unresolved-import]: Cannot resolve imported module `does_not_exist` | 1 | from does_not_exist import add # error: [unresolved-import] | ^^^^^^^^^^^^^^ - | info: Searched in the following paths during module resolution: info: 1. /src (first-party code) info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty) diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_to\342\200\246_(4b8ba6ee48180cdd).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_to\342\200\246_(4b8ba6ee48180cdd).snap" index b6eb52cd63..31c9ed5c0c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_to\342\200\246_(4b8ba6ee48180cdd).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_import.md_-_Unresolved_import_di\342\200\246_-_Using_`from`_with_to\342\200\246_(4b8ba6ee48180cdd).snap" @@ -38,7 +38,6 @@ error[unresolved-import]: Cannot resolve imported module `....foo` | 1 | from ....foo import add # error: [unresolved-import] | ^^^ - | help: The module can be resolved if the number of leading dots is reduced help: Did you mean `...foo`? info: Searched in the following paths during module resolution: diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_New_builtin_used_on_\342\200\246_(51edda0b1aebc2bf).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_New_builtin_used_on_\342\200\246_(51edda0b1aebc2bf).snap" index 703f7b3257..d6e34c335c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_New_builtin_used_on_\342\200\246_(51edda0b1aebc2bf).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_New_builtin_used_on_\342\200\246_(51edda0b1aebc2bf).snap" @@ -24,7 +24,6 @@ error[unresolved-reference]: Name `PythonFinalizationError` used when not define | 1 | PythonFinalizationError # error: [unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^ - | info: `PythonFinalizationError` was added as a builtin in Python 3.13 info: Python 3.12 was assumed when resolving types because it was specified on the command line diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_not_present_bef\342\200\246_(41702a6f6d20b082).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_not_present_bef\342\200\246_(41702a6f6d20b082).snap" index a1d39ac44b..0fcc75f48a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_not_present_bef\342\200\246_(41702a6f6d20b082).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_not_present_bef\342\200\246_(41702a6f6d20b082).snap" @@ -25,7 +25,6 @@ error[unresolved-reference]: Name `List` used when not defined | 1 | foo: List[int] # error: [unresolved-reference] | ^^^^ - | ``` @@ -35,6 +34,5 @@ error[unresolved-reference]: Name `Type` used when not defined | 2 | bar: Type # error: [unresolved-reference] | ^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_present_in_Pyth\342\200\246_(1028a80959504fc9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_present_in_Pyth\342\200\246_(1028a80959504fc9).snap" index ff80a77106..cc2b3758fd 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_present_in_Pyth\342\200\246_(1028a80959504fc9).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unresolved_reference\342\200\246_-_Diagnostics_for_unre\342\200\246_-_Typing_builtin_has_I\342\200\246_-_Info_present_in_Pyth\342\200\246_(1028a80959504fc9).snap" @@ -25,7 +25,6 @@ error[unresolved-reference]: Name `List` used when not defined | 1 | foo: List[int] # error: [unresolved-reference] | ^^^^ Did you mean `list`? - | ``` @@ -35,6 +34,5 @@ error[unresolved-reference]: Name `Type` used when not defined | 2 | bar: Type # error: [unresolved-reference] | ^^^^ Did you mean `type`? - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_base_(4873196c8b48364).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_base_(4873196c8b48364).snap" index 16ebf6b6af..31ef1a61df 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_base_(4873196c8b48364).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_base_(4873196c8b48364).snap" @@ -29,7 +29,6 @@ error[invalid-base]: Invalid base for class created via `type()` | 6 | X = type("X", (MyEnum,), {}) # error: [invalid-base] | ^^^^^^ Has type `` - | info: Creating an enum class via `type()` is not supported info: Consider using `Enum("X", [])` instead diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_with_members_(81bef9a8e1230854).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_with_members_(81bef9a8e1230854).snap" index f971b932d3..8485e0f5ba 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_with_members_(81bef9a8e1230854).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_Enum_with_members_(81bef9a8e1230854).snap" @@ -30,6 +30,5 @@ error[subclass-of-final-class]: Class `X` cannot inherit from final class `Color | 7 | X = type("X", (Color,), {}) # error: [subclass-of-final-class] | ^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`@final`_class_(ea69d237256b3762).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`@final`_class_(ea69d237256b3762).snap" index 9ab5159345..e87d71ccbb 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`@final`_class_(ea69d237256b3762).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`@final`_class_(ea69d237256b3762).snap" @@ -30,6 +30,5 @@ error[subclass-of-final-class]: Class `X` cannot inherit from final class `Final | 7 | X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class] | ^^^^^^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Generic`_base_(d455f46a27cec685).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Generic`_base_(d455f46a27cec685).snap" index 2ad2cc4044..c6ef194401 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Generic`_base_(d455f46a27cec685).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Generic`_base_(d455f46a27cec685).snap" @@ -28,7 +28,6 @@ error[invalid-base]: Invalid base for class created via `type()` | 5 | X = type("X", (Generic[T],), {}) # error: [invalid-base] | ^^^^^^^^^^ Has type `` - | info: Classes created via `type()` cannot be generic info: Consider using `class X(Generic[...]): ...` instead diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Protocol`_base_(99c9bde73664dd51).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Protocol`_base_(99c9bde73664dd51).snap" index a10d89d6a8..395f228b5f 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Protocol`_base_(99c9bde73664dd51).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`Protocol`_base_(99c9bde73664dd51).snap" @@ -26,7 +26,6 @@ info[unsupported-dynamic-base]: Unsupported base for class created via `type()` | 3 | X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base] | ^^^^^^^^ Has type `` - | info: Classes created via `type()` cannot be protocols info: Consider using `class X(Protocol): ...` instead diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`TypedDict`_base_(6f76171c88fc8760).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`TypedDict`_base_(6f76171c88fc8760).snap" index 67179b87da..03d01d0604 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`TypedDict`_base_(6f76171c88fc8760).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_base_dyn\342\200\246_-_Unsupported_base_for\342\200\246_-_`TypedDict`_base_(6f76171c88fc8760).snap" @@ -26,7 +26,6 @@ error[invalid-base]: Invalid base for class created via `type()` | 3 | X = type("X", (TypedDict,), {}) # error: [invalid-base] | ^^^^^^^^^ Has type `` - | info: Classes created via `type()` cannot be TypedDicts info: Consider using `TypedDict("X", {})` instead diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_att\342\200\246_(2721d40bf12fe8b7).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_att\342\200\246_(2721d40bf12fe8b7).snap" index b26e706834..97865ae1a4 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_att\342\200\246_(2721d40bf12fe8b7).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_att\342\200\246_(2721d40bf12fe8b7).snap" @@ -30,7 +30,6 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 7 | 10 and a and True | ^ - | info: `__bool__` on `NotBoolable` must be callable ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(15636dc4074e5335).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(15636dc4074e5335).snap" index 2f05042fde..787dad66d8 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(15636dc4074e5335).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(15636dc4074e5335).snap" @@ -31,14 +31,12 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 8 | 10 and a and True | ^ - | info: `str` is not assignable to `bool` - --> src/mdtest_snippet.py:2:9 + --> src/mdtest_snippet.py:2:27 | 2 | def __bool__(self) -> str: | -------- ^^^ Incorrect return type | | | Method defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(ce8b8da49eaf4cda).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(ce8b8da49eaf4cda).snap" index 44e0c7eb04..aea2c2c523 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(ce8b8da49eaf4cda).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Has_a_`__bool__`_met\342\200\246_(ce8b8da49eaf4cda).snap" @@ -31,14 +31,12 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 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:17 | 2 | def __bool__(self, foo): | --------^^^^^^^^^^^ Incorrect parameters | | | Method defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Part_of_a_union_wher\342\200\246_(7cca8063ea43c1a).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Part_of_a_union_wher\342\200\246_(7cca8063ea43c1a).snap" index 90026b8d2e..991e95d41b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Part_of_a_union_wher\342\200\246_(7cca8063ea43c1a).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/unsupported_bool_con\342\200\246_-_Different_ways_that_\342\200\246_-_Part_of_a_union_wher\342\200\246_(7cca8063ea43c1a).snap" @@ -38,6 +38,5 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for unio | 15 | 10 and get() and True | ^^^^^ - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(b62ed1f409042cc).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(b62ed1f409042cc).snap" index f40a34c7b4..77083e0d90 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(b62ed1f409042cc).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(b62ed1f409042cc).snap" @@ -30,7 +30,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/variable ``` error[invalid-type-variable-default]: TypeVar default is inconsistent with the TypeVar's constraints - --> src/mdtest_snippet.py:11:18 + --> src/mdtest_snippet.py:11:41 | 11 | U = TypeVar("U", bool, complex, default=T1) | ------------- ^^ Constraint `int` of default `T1` is not one of the constraints of `U` @@ -41,6 +41,5 @@ error[invalid-type-variable-default]: TypeVar default is inconsistent with the T | 3 | T1 = TypeVar("T1", int, str) | ---------------------------- `T1` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(d9ffda7fd9cdf840).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(d9ffda7fd9cdf840).snap" index b4ed239ad5..6d6f1e60b5 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(d9ffda7fd9cdf840).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_constrained_defaul\342\200\246_(d9ffda7fd9cdf840).snap" @@ -45,6 +45,5 @@ error[invalid-type-variable-default]: TypeVar default is not assignable to the T | 3 | T1 = TypeVar("T1", int, str) | ---------------------------- `T1` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_non-constrained_de\342\200\246_(ff24930259abfb3).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_non-constrained_de\342\200\246_(ff24930259abfb3).snap" index 82a9b1532d..4b0d93355b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_non-constrained_de\342\200\246_(ff24930259abfb3).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_A_non-constrained_de\342\200\246_(ff24930259abfb3).snap" @@ -29,7 +29,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/variable ``` error[invalid-type-variable-default]: TypeVar default is inconsistent with the TypeVar's constraints - --> src/mdtest_snippet.py:7:18 + --> src/mdtest_snippet.py:7:38 | 7 | S = TypeVar("S", float, str, default=T1) | ---------- ^^ Bounded TypeVar cannot be used as the default for a constrained TypeVar @@ -40,14 +40,13 @@ error[invalid-type-variable-default]: TypeVar default is inconsistent with the T | 3 | T1 = TypeVar("T1", bound=int) | ----------------------------- `T1` defined here - | info: `T1` has bound `int` but is not constrained ``` ``` error[invalid-type-variable-default]: TypeVar default is inconsistent with the TypeVar's constraints - --> src/mdtest_snippet.py:10:18 + --> src/mdtest_snippet.py:10:38 | 10 | U = TypeVar("U", str, bytes, default=T2) | ---------- ^^ Unbounded TypeVar cannot be used as the default for a constrained TypeVar @@ -58,7 +57,6 @@ error[invalid-type-variable-default]: TypeVar default is inconsistent with the T | 4 | T2 = TypeVar("T2") | ------------------ `T2` defined here - | info: `T2` has no bound or constraints ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_An_unbounded_default\342\200\246_(a2759fd9d2731a7d).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_An_unbounded_default\342\200\246_(a2759fd9d2731a7d).snap" index 6543b7f86a..52ce5e3590 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_An_unbounded_default\342\200\246_(a2759fd9d2731a7d).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_An_unbounded_default\342\200\246_(a2759fd9d2731a7d).snap" @@ -25,7 +25,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/variable ``` error[invalid-type-variable-default]: TypeVar default is not assignable to the TypeVar's upper bound - --> src/mdtest_snippet.py:3:1 + --> src/mdtest_snippet.py:6:26 | 3 | T1 = TypeVar("T1") | ------------------ `T1` defined here @@ -35,6 +35,5 @@ error[invalid-type-variable-default]: TypeVar default is not assignable to the T | ^^ --- Upper bound of `S` | | | Upper bound `object` of default `T1` is not assignable to upper bound of `S` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(30284a6490652e58).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(30284a6490652e58).snap" index 71aaa269d4..3e0cff3a6b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(30284a6490652e58).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(30284a6490652e58).snap" @@ -32,24 +32,22 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/variable ``` error[invalid-type-variable-default]: TypeVar default is inconsistent with the TypeVar's constraints - --> src/mdtest_snippet.py:4:18 + --> src/mdtest_snippet.py:4:36 | 4 | T = TypeVar("T", int, str, default=bytes) | -------- ^^^^^ `bytes` is not one of the constraints of `T` | | | Constraints of `T` - | ``` ``` error[invalid-type-variable-default]: TypeVar default is inconsistent with the TypeVar's constraints - --> src/mdtest_snippet.py:10:18 + --> src/mdtest_snippet.py:10:36 | 10 | U = TypeVar("U", int, str, default=bool) | -------- ^^^^ `bool` is not one of the constraints of `U` | | | Constraints of `U` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(37f9b6583c0633f5).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(37f9b6583c0633f5).snap" index 4e7343b87d..b46e4d503a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(37f9b6583c0633f5).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Concrete_default_wit\342\200\246_(37f9b6583c0633f5).snap" @@ -25,12 +25,11 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/variable ``` error[invalid-type-variable-default]: TypeVar default is not assignable to the TypeVar's upper bound - --> src/mdtest_snippet.py:4:24 + --> src/mdtest_snippet.py:4:37 | 4 | T = TypeVar("T", bound=str, default=int) | --- ^^^ Default of `T` | | | Upper bound of `T` - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Default_TypeVar's_bo\342\200\246_(fcd7ad5416c91629).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Default_TypeVar's_bo\342\200\246_(fcd7ad5416c91629).snap" index 909fb658f0..4e0073ed49 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Default_TypeVar's_bo\342\200\246_(fcd7ad5416c91629).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Default_TypeVar's_bo\342\200\246_(fcd7ad5416c91629).snap" @@ -42,6 +42,5 @@ error[invalid-type-variable-default]: TypeVar default is not assignable to the T | 5 | T3 = TypeVar("T3", bound=str) | ----------------------------- `T3` defined here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Shadowing_checks_use\342\200\246_(7e6bb178099059fe).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Shadowing_checks_use\342\200\246_(7e6bb178099059fe).snap" index 26499c34cd..e254c1e653 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Shadowing_checks_use\342\200\246_(7e6bb178099059fe).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Shadowing_checks_use\342\200\246_(7e6bb178099059fe).snap" @@ -37,13 +37,12 @@ warning[mismatched-type-name]: The name passed to `TypeVar` must match the varia | 8 | Q = TypeVar("T") | ^^^ Expected "Q", got "T" - | ``` ``` error[shadowed-type-variable]: Generic class `Bad` uses type variable `Q` already bound by an enclosing scope - --> src/mdtest_snippet.py:10:7 + --> src/mdtest_snippet.py:14:11 | 10 | class Outer(Generic[Q]): | ----------------- Type variable `Q` is bound in this enclosing scope @@ -52,13 +51,12 @@ error[shadowed-type-variable]: Generic class `Bad` uses type variable `Q` alread 13 | # error: [shadowed-type-variable] 14 | class Bad(Generic[Q]): ... | ^^^^^^^^^^^^^^^ `Q` used in class definition here - | ``` ``` error[shadowed-type-variable]: Generic class `Bad` uses type variable `Q` already bound by an enclosing scope - --> src/mdtest_snippet.py:10:7 + --> src/mdtest_snippet.py:14:11 | 10 | class Outer(Generic[Q]): | ----------------- Type variable `Q` is bound in this enclosing scope @@ -67,6 +65,5 @@ error[shadowed-type-variable]: Generic class `Bad` uses type variable `Q` alread 13 | # error: [shadowed-type-variable] 14 | class Bad(Generic[Q]): ... | ^^^^^^^^^^^^^^^ `Q` used in class definition here - | ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/version_related_synt\342\200\246_-_Version-related_synt\342\200\246_-_`match`_statement_-_Before_3.10_(2545eaa83b635b8b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/version_related_synt\342\200\246_-_Version-related_synt\342\200\246_-_`match`_statement_-_Before_3.10_(2545eaa83b635b8b).snap" index dc72f19035..36b35780ec 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/version_related_synt\342\200\246_-_Version-related_synt\342\200\246_-_`match`_statement_-_Before_3.10_(2545eaa83b635b8b).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/version_related_synt\342\200\246_-_Version-related_synt\342\200\246_-_`match`_statement_-_Before_3.10_(2545eaa83b635b8b).snap" @@ -26,7 +26,6 @@ error[invalid-syntax]: Cannot use `match` statement on Python 3.9 (syntax was ad | 1 | match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)" | ^^^^^ - | info: Python 3.9 was assumed when parsing syntax because it was specified on the command line ``` diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/instance.md b/crates/ty_python_semantic/resources/mdtest/subscript/instance.md index f0490f831e..abe0367ade 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/instance.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/instance.md @@ -15,7 +15,6 @@ error[not-subscriptable]: Cannot subscript object of type `NotSubscriptable` wit | 4 | a = NotSubscriptable()[0] | ^^^^^^^^^^^^^^^^^^^^^ - | ``` ## `__getitem__` not callable diff --git a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md index 7faafc6698..29b8829eaa 100644 --- a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md @@ -126,7 +126,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 3 | a = test + 3 # ty: ignore[possibly-unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 2 | # snapshot @@ -150,7 +149,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 3 | a = test + 3 # ty: ignore[possibly-unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 2 | # error: [unresolved-reference] @@ -184,7 +182,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'unused-ignore-co | 2 | a = 10 / 0 # ty: ignore[division-by-zero, unused-ignore-comment] | ^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression code | 1 | # snapshot @@ -208,7 +205,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 2 | a = 10 / 2 # ty: ignore[division-by-zero, unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 1 | # snapshot @@ -230,7 +226,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignme | 5 | a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference] | ^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression code | 4 | # snapshot @@ -245,7 +240,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'unresolved-refer | 5 | a = 10 / 0 # ty: ignore[invalid-assignment, division-by-zero, unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression code | 4 | # snapshot @@ -266,7 +260,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignme | 7 | a = 10 / 0 # ty: ignore[invalid-assignment, unresolved-reference, division-by-zero] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression codes | 6 | # snapshot @@ -312,7 +305,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 9 | # fmt: off # ty: ignore[division-by-zero] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 8 | # snapshot @@ -337,7 +329,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 15 | # ty: ignore[division-by-zero] # fmt: off | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 14 | # snapshot @@ -475,7 +466,6 @@ warning[ignore-comment-unknown-rule]: Unknown rule `division-by-zer`. Did you me | 2 | a = 10 + 4 # ty: ignore[division-by-zer] | ^^^^^^^^^^^^^^^ - | ``` ## Code with `lint:` prefix diff --git a/crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md b/crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md index fdf33f588a..6ba5bdbc6f 100644 --- a/crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md +++ b/crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md @@ -170,7 +170,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 9 | + 2) # ty:ignore[division-by-zero] # fmt: skip | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 8 | # snapshot @@ -192,7 +191,6 @@ warning[unused-ignore-comment]: Unused `ty: ignore` directive | 12 | + 2) # fmt: skip # ty:ignore[division-by-zero] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 11 | # snapshot @@ -319,7 +317,6 @@ warning[unused-type-ignore-comment]: Unused `type: ignore` directive: 'division- | 2 | a = 10 / 2 # type: ignore[mypy-code, ty:division-by-zero] | ^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression code | 1 | # snapshot @@ -341,7 +338,6 @@ warning[unused-type-ignore-comment]: Unused `type: ignore` directive | 2 | a = 10 / 2 # type: ignore[ty:division-by-zero] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 1 | # snapshot @@ -363,5 +359,4 @@ warning[ignore-comment-unknown-rule]: Unknown rule `division-by`. Did you mean ` | 2 | a = 10 / 2 # type: ignore[ty:division-by] | ^^^^^^^^^^^^^^ - | ``` diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index bc5a1b4212..64436541b8 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -293,7 +293,6 @@ error[static-assert-error]: Static assertion error: argument evaluates to `False | ^^^^^^^^^^^^^^-----^ | | | Inferred type of argument is `Literal[False]` - | ``` With a custom message: @@ -311,7 +310,6 @@ error[static-assert-error]: Static assertion error: with a message | ^^^^^^^^^^^^^^-----^^^^^^^^^^^^^^^^^^^ | | | Inferred type of argument is `Literal[False]` - | ``` When it evaluates to something falsy: @@ -329,7 +327,6 @@ error[static-assert-error]: Static assertion error: argument of type `Literal["" | ^^^^^^^^^^^^^^--^ | | | Inferred type of argument is `Literal[""]` - | ``` When it evaluates to something that is not statically known to be truthy or falsy: @@ -347,7 +344,6 @@ error[static-assert-error]: Static assertion error: argument of type `int` has a | ^^^^^^^^^^^^^^--------------------^ | | | Inferred type of argument is `int` - | ``` ## Type predicates diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md index dc86e1d631..6c004c6660 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -627,13 +627,11 @@ error[override-of-final-variable]: Cannot override `module_a.Foo.X` | 5 | X = 2 | ^ Overrides a final variable from superclass `module_a.Foo` - | info: `module_a.Foo.X` is declared as `Final`, forbidding overrides --> src/module_a.py:4:5 | 4 | X: Final[int] = 1 | - `module_a.Foo.X` defined here - | ``` ### `Final` declaration without a value @@ -1312,7 +1310,6 @@ error[invalid-assignment]: Reassignment of `Final` symbol `MY_CONSTANT` is not a | 3 | MY_CONSTANT: Final[int] = 1 | ---------- Symbol declared as `Final` here - | ``` Imported `Final` symbol: diff --git a/crates/ty_python_semantic/resources/mdtest/unary/custom.md b/crates/ty_python_semantic/resources/mdtest/unary/custom.md index 1db66e8607..eefeea2244 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/custom.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/custom.md @@ -167,7 +167,6 @@ error[unsupported-operator]: Unary operator `+` is not supported for object of t | 15 | reveal_type(+x) # revealed: bool | ^^ - | info: `No` does not implement `__pos__` @@ -176,7 +175,6 @@ error[unsupported-operator]: Unary operator `-` is not supported for object of t | 18 | reveal_type(-x) # revealed: str | ^^ - | info: `No` does not implement `__neg__` @@ -185,7 +183,6 @@ error[unsupported-operator]: Unary operator `~` is not supported for object of t | 21 | reveal_type(~x) # revealed: int | ^^ - | info: `No` does not implement `__invert__` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/unary/not.md b/crates/ty_python_semantic/resources/mdtest/unary/not.md index 10ee1fccc8..98f8056931 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/not.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/not.md @@ -231,6 +231,5 @@ error[unsupported-bool-conversion]: Boolean conversion is not supported for type | 5 | not NotBoolable() | ^^^^^^^^^^^^^^^^^ - | info: `__bool__` on `NotBoolable` must be callable ``` diff --git a/crates/ty_python_semantic/resources/mdtest/unreachable.md b/crates/ty_python_semantic/resources/mdtest/unreachable.md index 7576ca63b5..e73fbb98bc 100644 --- a/crates/ty_python_semantic/resources/mdtest/unreachable.md +++ b/crates/ty_python_semantic/resources/mdtest/unreachable.md @@ -642,6 +642,5 @@ error[invalid-type-form]: Variable of type `Never` is not allowed in a parameter | 4 | def f(x: module.AwesomeAPI): ... | ^^^^^^^^^^^^^^^^^ - | help: The variable may have been inferred as `Never` because its definition was inferred as being unreachable ``` diff --git a/crates/ty_python_semantic/resources/mdtest/with/async.md b/crates/ty_python_semantic/resources/mdtest/with/async.md index a99cdafecf..01ff963076 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/async.md +++ b/crates/ty_python_semantic/resources/mdtest/with/async.md @@ -174,7 +174,6 @@ error[invalid-context-manager]: Object of type `Manager` cannot be used with `as | 7 | async with Manager(): | ^^^^^^^^^ - | info: Objects of type `Manager` can be used as sync context managers info: Consider using `with` here ``` diff --git a/crates/ty_python_semantic/resources/mdtest/with/sync.md b/crates/ty_python_semantic/resources/mdtest/with/sync.md index 733c7db405..0669d941dd 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/sync.md +++ b/crates/ty_python_semantic/resources/mdtest/with/sync.md @@ -261,7 +261,6 @@ error[invalid-context-manager]: Object of type `Manager` cannot be used with `wi | 6 | with Manager(): | ^^^^^^^^^ - | info: Objects of type `Manager` can be used as async context managers info: Consider using `async with` here ``` diff --git a/crates/ty_python_semantic/src/fixes.rs b/crates/ty_python_semantic/src/fixes.rs index f4ce815376..6616b9cde9 100644 --- a/crates/ty_python_semantic/src/fixes.rs +++ b/crates/ty_python_semantic/src/fixes.rs @@ -898,7 +898,6 @@ mod tests { 1 | import sys 2 | a = 5 + 10 # ty: ignore[unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | 1 | import sys @@ -934,7 +933,6 @@ mod tests { 1 | import sys 2 | a = x + | ^ - | error[invalid-syntax]: Expected an expression --> test.py:2:8 @@ -942,7 +940,6 @@ mod tests { 1 | import sys 2 | a = x + | ^ - | "); } @@ -1200,7 +1197,6 @@ class B(A): | 1 | value = missing # ty: ignore[] tracked by [123] # ty: ignore[unresolved-reference] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression comment | - value = missing # ty: ignore[] tracked by [123] # ty: ignore[unresolved-reference] @@ -1347,7 +1343,6 @@ class B(A): 2 | 3 | result: int = f(missing) # ty: ignore[division-by-zero, invalid-assignment, too-many-positional-arguments, unresolved-reference] | ^^^^^^^^^^^^^^^^ - | help: Remove the unused suppression code | 2 | diff --git a/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs b/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs index ac5b0d15a1..9ca9ce3810 100644 --- a/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs +++ b/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs @@ -188,7 +188,6 @@ mod tests { | 4 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -208,7 +207,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -233,7 +231,6 @@ mod tests { | 7 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -254,7 +251,6 @@ mod tests { 4 | / print("dead") 5 | | print("still dead") | |_______________________^ - | "#); Ok(()) } @@ -273,7 +269,6 @@ mod tests { | 4 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -295,7 +290,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -314,7 +308,6 @@ mod tests { | 4 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -334,7 +327,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -354,7 +346,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -374,7 +365,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -392,7 +382,6 @@ mod tests { | 3 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -413,14 +402,12 @@ mod tests { | 3 | print("dead") | ^^^^^^^^^^^^^ - | info[unreachable-code]: Code is always unreachable --> src/main.py:6:5 | 6 | print("also dead") | ^^^^^^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -438,7 +425,6 @@ mod tests { | 3 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -458,7 +444,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -478,7 +463,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -502,7 +486,6 @@ mod tests { | 4 | return | ^^^^^^ - | info[unreachable-code]: Code is always unreachable --> src/main.py:8:9 @@ -510,7 +493,6 @@ mod tests { 8 | / pass 9 | | print("dead") | |_________________^ - | "#); Ok(()) } @@ -530,7 +512,6 @@ mod tests { | 5 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -547,7 +528,6 @@ mod tests { | 2 | x = "yes" if True else "no" | ^^^^ - | "#); Ok(()) } @@ -568,14 +548,12 @@ mod tests { | 3 | x = 1 | ^^^^^ - | info[unreachable-code]: Code is always unreachable --> src/main.py:6:5 | 6 | y = 2 | ^^^^^ - | "); Ok(()) } @@ -593,7 +571,6 @@ mod tests { | 3 | x = lambda: 1 | ^^^^^^^^^^^^^ - | "); Ok(()) } @@ -613,7 +590,6 @@ mod tests { 3 | / def f(): 4 | | pass | |____________^ - | "); Ok(()) } @@ -633,7 +609,6 @@ mod tests { 3 | / class Foo: 4 | | pass | |____________^ - | "); Ok(()) } @@ -651,7 +626,6 @@ mod tests { | 3 | x = [i for i in range(10)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | "); Ok(()) } @@ -675,21 +649,18 @@ mod tests { | 3 | x = {k: v for k, v in {}.items()} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info[unreachable-code]: Code is always unreachable --> src/main.py:6:5 | 6 | y = {i for i in range(10)} | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | info[unreachable-code]: Code is always unreachable --> src/main.py:9:5 | 9 | z = (i for i in range(10)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - | "); Ok(()) } @@ -710,7 +681,6 @@ mod tests { | 3 | type Alias[T] = list[T] | ^^^^^^^^^^^^^^^^^^^^^^^ - | "); Ok(()) } @@ -733,7 +703,6 @@ mod tests { | 5 | from typing import Self | ^^^^^^^^^^^^^^^^^^^^^^^ - | "); Ok(()) } @@ -756,7 +725,6 @@ mod tests { | 5 | import winreg | ^^^^^^^^^^^^^ - | "); Ok(()) } @@ -780,7 +748,6 @@ mod tests { | 9 | print("dead") | ^^^^^^^^^^^^^ - | "#); Ok(()) } @@ -826,7 +793,6 @@ mod tests { 5 | / if False: 6 | | x = lambda: 1 | |_____________________^ - | "); Ok(()) } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__full_diagnostic_output.snap b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__full_diagnostic_output.snap index 02d34942a0..a5a61422da 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__full_diagnostic_output.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__publish_diagnostics__full_diagnostic_output.snap @@ -69,7 +69,7 @@ PublishDiagnosticsParams { data: Some( Object { "diagnostic_id": String("invalid-return-type"), - "rendered": String("\u{1b}[1m\u{1b}[91merror[invalid-return-type]\u{1b}[0m: \u{1b}[1mReturn type does not match returned value\u{1b}[0m\n \u{1b}[1m\u{1b}[94m-->\u{1b}[0m src/foo.py:1:14\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\u{1b}[1m\u{1b}[94m1 |\u{1b}[0m def foo() -> str:\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[33m---\u{1b}[0m \u{1b}[1m\u{1b}[33mExpected `str` because of return type\u{1b}[0m\n\u{1b}[1m\u{1b}[94m2 |\u{1b}[0m return 42\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[91m^^\u{1b}[0m \u{1b}[1m\u{1b}[91mexpected `str`, found `Literal[42]`\u{1b}[0m\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\n"), + "rendered": String("\u{1b}[1m\u{1b}[91merror[invalid-return-type]\u{1b}[0m\u{1b}[1m: Return type does not match returned value\u{1b}[0m\n \u{1b}[1m\u{1b}[94m--> \u{1b}[0msrc/foo.py:2:12\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\u{1b}[1m\u{1b}[94m1\u{1b}[0m \u{1b}[1m\u{1b}[94m|\u{1b}[0m def foo() -> str:\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[94m---\u{1b}[0m \u{1b}[1m\u{1b}[94mExpected `str` because of return type\u{1b}[0m\n\u{1b}[1m\u{1b}[94m2\u{1b}[0m \u{1b}[1m\u{1b}[94m|\u{1b}[0m return 42\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[91m^^\u{1b}[0m \u{1b}[1m\u{1b}[91mexpected `str`, found `Literal[42]`\u{1b}[0m\n\n"), }, ), }, diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_after.snap b/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_after.snap index ff9985b20d..62b4eacc15 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_after.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_after.snap @@ -61,7 +61,7 @@ RelatedFullDocumentDiagnosticReport( data: Some( Object { "diagnostic_id": String("invalid-return-type"), - "rendered": String("\u{1b}[1m\u{1b}[91merror[invalid-return-type]\u{1b}[0m: \u{1b}[1mReturn type does not match returned value\u{1b}[0m\n \u{1b}[1m\u{1b}[94m-->\u{1b}[0m src/foo.py:1:14\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\u{1b}[1m\u{1b}[94m1 |\u{1b}[0m def foo() -> str:\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[33m---\u{1b}[0m \u{1b}[1m\u{1b}[33mExpected `str` because of return type\u{1b}[0m\n\u{1b}[1m\u{1b}[94m2 |\u{1b}[0m return 42 # after!\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[91m^^\u{1b}[0m \u{1b}[1m\u{1b}[91mexpected `str`, found `Literal[42]`\u{1b}[0m\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\n"), + "rendered": String("\u{1b}[1m\u{1b}[91merror[invalid-return-type]\u{1b}[0m\u{1b}[1m: Return type does not match returned value\u{1b}[0m\n \u{1b}[1m\u{1b}[94m--> \u{1b}[0msrc/foo.py:2:12\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\u{1b}[1m\u{1b}[94m1\u{1b}[0m \u{1b}[1m\u{1b}[94m|\u{1b}[0m def foo() -> str:\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[94m---\u{1b}[0m \u{1b}[1m\u{1b}[94mExpected `str` because of return type\u{1b}[0m\n\u{1b}[1m\u{1b}[94m2\u{1b}[0m \u{1b}[1m\u{1b}[94m|\u{1b}[0m return 42 # after!\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[91m^^\u{1b}[0m \u{1b}[1m\u{1b}[91mexpected `str`, found `Literal[42]`\u{1b}[0m\n\n"), }, ), }, diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_before.snap b/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_before.snap index 81151c4ae3..686521e8a5 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_before.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__pull_diagnostics__document_diagnostic_caching_rendered_source_before.snap @@ -61,7 +61,7 @@ RelatedFullDocumentDiagnosticReport( data: Some( Object { "diagnostic_id": String("invalid-return-type"), - "rendered": String("\u{1b}[1m\u{1b}[91merror[invalid-return-type]\u{1b}[0m: \u{1b}[1mReturn type does not match returned value\u{1b}[0m\n \u{1b}[1m\u{1b}[94m-->\u{1b}[0m src/foo.py:1:14\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\u{1b}[1m\u{1b}[94m1 |\u{1b}[0m def foo() -> str:\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[33m---\u{1b}[0m \u{1b}[1m\u{1b}[33mExpected `str` because of return type\u{1b}[0m\n\u{1b}[1m\u{1b}[94m2 |\u{1b}[0m return 42 # before\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[91m^^\u{1b}[0m \u{1b}[1m\u{1b}[91mexpected `str`, found `Literal[42]`\u{1b}[0m\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\n"), + "rendered": String("\u{1b}[1m\u{1b}[91merror[invalid-return-type]\u{1b}[0m\u{1b}[1m: Return type does not match returned value\u{1b}[0m\n \u{1b}[1m\u{1b}[94m--> \u{1b}[0msrc/foo.py:2:12\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m\n\u{1b}[1m\u{1b}[94m1\u{1b}[0m \u{1b}[1m\u{1b}[94m|\u{1b}[0m def foo() -> str:\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[94m---\u{1b}[0m \u{1b}[1m\u{1b}[94mExpected `str` because of return type\u{1b}[0m\n\u{1b}[1m\u{1b}[94m2\u{1b}[0m \u{1b}[1m\u{1b}[94m|\u{1b}[0m return 42 # before\n \u{1b}[1m\u{1b}[94m|\u{1b}[0m \u{1b}[1m\u{1b}[91m^^\u{1b}[0m \u{1b}[1m\u{1b}[91mexpected `str`, found `Literal[42]`\u{1b}[0m\n\n"), }, ), }, diff --git a/crates/ty_site_packages/src/lib.rs b/crates/ty_site_packages/src/lib.rs index 77a593e12a..d52a37cac3 100644 --- a/crates/ty_site_packages/src/lib.rs +++ b/crates/ty_site_packages/src/lib.rs @@ -17,9 +17,9 @@ use std::ops::Deref; use std::str::FromStr; use std::{fmt, sync::Arc}; +use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet}; use camino::Utf8Component; use indexmap::IndexSet; -use ruff_annotate_snippets::{Level, Renderer, Snippet}; use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_python_ast::PythonVersion; use ruff_python_trivia::Cursor; @@ -1417,7 +1417,7 @@ fn display_error( let start_offset = source.line_start(start_index); let end_offset = source.line_end(end_index); - let mut annotation = Level::Error.span((setting_range - start_offset).into()); + let mut annotation = AnnotationKind::Primary.span((setting_range - start_offset).into()); if let Some(secondary_message) = secondary_message { annotation = annotation.label(secondary_message); @@ -1428,7 +1428,10 @@ fn display_error( .line_start(start_index.get()) .fold(false); - let message = Level::None.title(&primary_message).snippet(snippet); + let message = Level::ERROR + .no_name() + .primary_title(&primary_message) + .element(snippet); let renderer = if colored::control::SHOULD_COLORIZE.should_colorize() { Renderer::styled() @@ -1437,7 +1440,7 @@ fn display_error( }; let renderer = renderer.cut_indicator("…"); - writeln!(f, "{}", renderer.render(message)) + writeln!(f, "{}", renderer.render(&[message])) } /// The various ways in which parsing a `pyvenv.cfg` file could fail From 231eb3389b632866721c50beef0f0e1bd933562f Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:04:20 -0400 Subject: [PATCH 099/390] Add an option to opt out of human-readable names (#27160) Summary -- This PR closes #26320 by adding the new `lint.prefer-rule-codes-in-output` option that defaults to `false` and disables the use of human-readable rule names in most output formats, even in preview. This does not affect formats like JSON that include the name and code as separate fields. Test Plan -- New CLI test enabling the option, as well as manual testing in the playground: image and in VS Code: image Note the human-readable name in the `ruff: ignore` comment, which shows that preview is enabled, while the `(F401)` in the output shows that the setting is being used. --- crates/ruff/src/lib.rs | 12 ++++++--- crates/ruff/src/printer.rs | 17 ++++++++++--- crates/ruff/tests/cli/lint.rs | 25 +++++++++++++++++++ ...ires_python_extend_from_shared_config.snap | 1 + .../cli__lint__requires_python_no_tool.snap | 1 + ...quires_python_no_tool_preview_enabled.snap | 1 + ...ython_no_tool_target_version_override.snap | 1 + ..._requires_python_pyproject_toml_above.snap | 1 + ...python_pyproject_toml_above_with_tool.snap | 1 + ...nt__requires_python_ruff_toml_above-2.snap | 1 + ...lint__requires_python_ruff_toml_above.snap | 1 + ...s_python_ruff_toml_no_target_fallback.snap | 1 + ...ow_settings__display_default_settings.snap | 1 + ...isplay_settings_from_nested_directory.snap | 1 + crates/ruff_db/src/diagnostic/mod.rs | 16 ++++++++++++ crates/ruff_db/src/diagnostic/render.rs | 5 ++-- crates/ruff_db/src/diagnostic/render/azure.rs | 2 +- .../ruff_db/src/diagnostic/render/concise.rs | 7 +++--- .../ruff_db/src/diagnostic/render/github.rs | 7 +++--- .../ruff_db/src/diagnostic/render/gitlab.rs | 2 +- crates/ruff_db/src/diagnostic/render/junit.rs | 2 +- .../ruff_db/src/diagnostic/render/pylint.rs | 2 +- .../ruff_db/src/diagnostic/render/rdjson.rs | 2 +- crates/ruff_linter/src/message/grouped.rs | 19 +++++++++++--- crates/ruff_linter/src/message/mod.rs | 1 + crates/ruff_linter/src/message/sarif.rs | 6 ++++- crates/ruff_server/src/lint.rs | 17 ++++++++----- crates/ruff_wasm/src/lib.rs | 3 ++- crates/ruff_workspace/src/configuration.rs | 6 +++++ crates/ruff_workspace/src/options.rs | 24 ++++++++++++++++++ crates/ruff_workspace/src/settings.rs | 5 ++++ ruff.schema.json | 7 ++++++ 32 files changed, 163 insertions(+), 35 deletions(-) diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 350e57f3b4..f64359e567 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -375,6 +375,7 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result Result Result Result Result<()> { if matches!(self.log_level, LogLevel::Silent) { return Ok(()); @@ -223,7 +224,7 @@ impl Printer { if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) { if !diagnostics.fixed.is_empty() { writeln!(writer)?; - print_fix_summary(writer, &diagnostics.fixed, preview)?; + print_fix_summary(writer, &diagnostics.fixed, preview, prefer_rule_codes)?; writeln!(writer)?; } } @@ -237,6 +238,7 @@ impl Printer { let config = DisplayDiagnosticConfig::new("ruff") .preview(preview.is_enabled()) + .prefer_rule_codes(prefer_rule_codes) .hide_severity(true) .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) @@ -251,7 +253,7 @@ impl Printer { if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) { if !diagnostics.fixed.is_empty() { writeln!(writer)?; - print_fix_summary(writer, &diagnostics.fixed, preview)?; + print_fix_summary(writer, &diagnostics.fixed, preview, prefer_rule_codes)?; writeln!(writer)?; } } @@ -383,6 +385,7 @@ impl Printer { writer: &mut dyn Write, diagnostics: &Diagnostics, preview: PreviewMode, + prefer_rule_codes: bool, ) -> Result<()> { if matches!(self.log_level, LogLevel::Silent) { return Ok(()); @@ -410,6 +413,7 @@ impl Printer { let context = EmitterContext::new(&diagnostics.notebook_indexes); let config = DisplayDiagnosticConfig::new("ruff") .preview(preview.is_enabled()) + .prefer_rule_codes(prefer_rule_codes) .hide_severity(true) .color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()) .with_show_fix_status(show_fix_status(self.fix_mode, fixables.as_ref())) @@ -445,7 +449,12 @@ fn show_fix_status(fix_mode: flags::FixMode, fixables: Option<&FixableStatistics (!fix_mode.is_apply()) && fixables.is_some_and(FixableStatistics::any_applicable_fixes) } -fn print_fix_summary(writer: &mut dyn Write, fixed: &FixMap, preview: PreviewMode) -> Result<()> { +fn print_fix_summary( + writer: &mut dyn Write, + fixed: &FixMap, + preview: PreviewMode, + prefer_rule_codes: bool, +) -> Result<()> { let total = fixed .values() .map(|table| table.counts().sum::()) @@ -475,7 +484,7 @@ fn print_fix_summary(writer: &mut dyn Write, fixed: &FixMap, preview: PreviewMod ":".cyan() )?; for (code, name, count) in table.iter().sorted_by_key(|(.., count)| Reverse(*count)) { - if is_human_readable_names_enabled(preview) { + if is_human_readable_names_enabled(preview) && !prefer_rule_codes { writeln!( writer, " {count:>num_digits$} × {name} ({code})", diff --git a/crates/ruff/tests/cli/lint.rs b/crates/ruff/tests/cli/lint.rs index 0955917992..d8e394cfb2 100644 --- a/crates/ruff/tests/cli/lint.rs +++ b/crates/ruff/tests/cli/lint.rs @@ -4273,6 +4273,31 @@ class Foo: ); } +#[test] +fn prefer_rule_codes_in_output() { + assert_cmd_snapshot!( + Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args([ + "--preview", + "--config", + "output-prefer-rule-codes = true", + "--select=A001", + "-", + ]) + .pass_stdin("print = 1\n"), + @" + success: false + exit_code: 1 + ----- stdout ----- + -:1:1: A001 Variable `print` is shadowing a Python builtin + Found 1 error. + + ----- stderr ----- + " + ); +} + #[test_case::test_case("concise")] #[test_case::test_case("full")] #[test_case::test_case("json")] diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap index 682462cd1d..dd44815b42 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_extend_from_shared_config.snap @@ -21,6 +21,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap index b45927e8df..297f9e61ab 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool.snap @@ -23,6 +23,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap index 0a24b5e1d8..4fa30c77cf 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_preview_enabled.snap @@ -24,6 +24,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap index 66a7714e49..c0243ce499 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_no_tool_target_version_override.snap @@ -25,6 +25,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap index 0b3f80819c..0b134a091a 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above.snap @@ -22,6 +22,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap index 5f376667d4..94590c2c2c 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_pyproject_toml_above_with_tool.snap @@ -23,6 +23,7 @@ cache_dir = "[TMP]/foo/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap index 0f9960f3fe..ddc9873c88 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above-2.snap @@ -21,6 +21,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap index cef5cf6988..f0e415ce41 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_above.snap @@ -21,6 +21,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint diff --git a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap index 14a16079b6..a0f9602894 100644 --- a/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap +++ b/crates/ruff/tests/cli/snapshots/cli__lint__requires_python_ruff_toml_no_target_fallback.snap @@ -21,6 +21,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = concise +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap index 43a4d2768e..915a5a077e 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_default_settings.snap @@ -18,6 +18,7 @@ cache_dir = "[TMP]/.ruff_cache" fix = false fix_only = false output_format = full +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint diff --git a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap index dafb9e2db8..c991e1f61f 100644 --- a/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap +++ b/crates/ruff/tests/cli/snapshots/cli__show_settings__display_settings_from_nested_directory.snap @@ -18,6 +18,7 @@ cache_dir = "[TMP]/subdir/.ruff_cache" fix = false fix_only = false output_format = full +output_prefer_rule_codes = false show_fixes = false unsafe_fixes = hint diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index e45020770a..03d9319a2c 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -1447,6 +1447,8 @@ pub struct DisplayDiagnosticConfig { merge_window: usize, /// Whether to use preview formatting for Ruff diagnostics. preview: bool, + /// Whether to prefer rule codes over human-readable rule names in Ruff diagnostic output. + prefer_rule_codes: bool, /// Whether to hide the real `Severity` of diagnostics. /// /// This is intended for temporary use by Ruff, which only has a single `error` severity at the @@ -1471,6 +1473,7 @@ impl DisplayDiagnosticConfig { context: 2, merge_window: 2, preview: false, + prefer_rule_codes: false, hide_severity: false, show_fix_status: false, fix_applicability: Applicability::Safe, @@ -1535,6 +1538,19 @@ impl DisplayDiagnosticConfig { self.preview } + /// Whether to prefer rule codes over human-readable rule names, even in preview mode. + pub fn prefer_rule_codes(self, yes: bool) -> DisplayDiagnosticConfig { + DisplayDiagnosticConfig { + prefer_rule_codes: yes, + ..self + } + } + + /// Whether rule codes are explicitly preferred over human-readable rule names. + pub fn is_prefer_rule_codes_enabled(&self) -> bool { + self.prefer_rule_codes + } + /// Whether to hide a diagnostic's severity or not. pub fn hide_severity(self, yes: bool) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig { diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index 8621883ad8..e40ca5594d 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -238,9 +238,8 @@ impl<'a> ResolvedDiagnostic<'a> { }) .collect(); - let id = if !config.preview - && let Some(code) = diag.secondary_code() - { + let use_code = !config.preview || config.prefer_rule_codes; + let id = if use_code && let Some(code) = diag.secondary_code() { code.to_string() } else if config.hide_severity { // When Ruff gets real severities, we should put the colon back in diff --git a/crates/ruff_db/src/diagnostic/render/azure.rs b/crates/ruff_db/src/diagnostic/render/azure.rs index 4183c2702c..c1724aa6a5 100644 --- a/crates/ruff_db/src/diagnostic/render/azure.rs +++ b/crates/ruff_db/src/diagnostic/render/azure.rs @@ -49,7 +49,7 @@ impl AzureRenderer<'_> { )?; } } - let code = if self.config.preview { + let code = if self.config.preview && !self.config.prefer_rule_codes { diag.id().as_str() } else { diag.secondary_code_or_id() diff --git a/crates/ruff_db/src/diagnostic/render/concise.rs b/crates/ruff_db/src/diagnostic/render/concise.rs index 2fa922a900..0e437011d3 100644 --- a/crates/ruff_db/src/diagnostic/render/concise.rs +++ b/crates/ruff_db/src/diagnostic/render/concise.rs @@ -67,10 +67,9 @@ impl<'a> ConciseRenderer<'a> { write!(f, "{sep} ")?; } + let use_name = self.config.preview && !self.config.prefer_rule_codes; if self.config.hide_severity { - if !self.config.preview - && let Some(code) = diag.secondary_code() - { + if !use_name && let Some(code) = diag.secondary_code() { write!( f, "{code} ", @@ -100,7 +99,7 @@ impl<'a> ConciseRenderer<'a> { Severity::Error => ("error", stylesheet.error), Severity::Fatal => ("fatal", stylesheet.error), }; - let id = if self.config.preview { + let id = if use_name { diag.id().as_str() } else { diag.secondary_code_or_id() diff --git a/crates/ruff_db/src/diagnostic/render/github.rs b/crates/ruff_db/src/diagnostic/render/github.rs index 052cfeb21f..0d8b862e42 100644 --- a/crates/ruff_db/src/diagnostic/render/github.rs +++ b/crates/ruff_db/src/diagnostic/render/github.rs @@ -26,7 +26,8 @@ impl<'a> GithubRenderer<'a> { Severity::Warning => "warning", Severity::Error | Severity::Fatal => "error", }; - let code = if self.config.preview { + let use_name = self.config.preview && !self.config.prefer_rule_codes; + let code = if use_name { diagnostic.id().as_str() } else { diagnostic.secondary_code_or_id() @@ -90,9 +91,7 @@ impl<'a> GithubRenderer<'a> { write!(f, "::")?; } - if !self.config.preview - && let Some(code) = diagnostic.secondary_code() - { + if !use_name && let Some(code) = diagnostic.secondary_code() { write!(f, "{code}")?; } else { write!(f, "{id}:", id = diagnostic.id())?; diff --git a/crates/ruff_db/src/diagnostic/render/gitlab.rs b/crates/ruff_db/src/diagnostic/render/gitlab.rs index 88eab11857..f330e268dc 100644 --- a/crates/ruff_db/src/diagnostic/render/gitlab.rs +++ b/crates/ruff_db/src/diagnostic/render/gitlab.rs @@ -102,7 +102,7 @@ impl Serialize for SerializedMessages<'_> { fingerprints.insert(message_fingerprint); let description = diagnostic.concise_message(); - let check_name = if self.config.preview { + let check_name = if self.config.preview && !self.config.prefer_rule_codes { diagnostic.id().as_str() } else { diagnostic.secondary_code_or_id() diff --git a/crates/ruff_db/src/diagnostic/render/junit.rs b/crates/ruff_db/src/diagnostic/render/junit.rs index ddb7c2e8f6..f52f4b1950 100644 --- a/crates/ruff_db/src/diagnostic/render/junit.rs +++ b/crates/ruff_db/src/diagnostic/render/junit.rs @@ -58,7 +58,7 @@ impl<'a> JunitRenderer<'a> { start_location: location, } = diagnostic; - let code = if self.config.preview { + let code = if self.config.preview && !self.config.prefer_rule_codes { diagnostic.id().as_str() } else { diagnostic.secondary_code_or_id() diff --git a/crates/ruff_db/src/diagnostic/render/pylint.rs b/crates/ruff_db/src/diagnostic/render/pylint.rs index d6a4f5d56d..2dd4b9f8e6 100644 --- a/crates/ruff_db/src/diagnostic/render/pylint.rs +++ b/crates/ruff_db/src/diagnostic/render/pylint.rs @@ -46,7 +46,7 @@ impl PylintRenderer<'_> { }) .unwrap_or_default(); - let code = if self.config.preview { + let code = if self.config.preview && !self.config.prefer_rule_codes { diagnostic.id().as_str() } else { diagnostic.secondary_code_or_id() diff --git a/crates/ruff_db/src/diagnostic/render/rdjson.rs b/crates/ruff_db/src/diagnostic/render/rdjson.rs index 620d8e8f79..5ab30cbf86 100644 --- a/crates/ruff_db/src/diagnostic/render/rdjson.rs +++ b/crates/ruff_db/src/diagnostic/render/rdjson.rs @@ -86,7 +86,7 @@ fn diagnostic_to_rdjson<'a>( message: diagnostic.concise_message(), location, code: RdjsonCode { - value: if config.preview { + value: if config.preview && !config.prefer_rule_codes { diagnostic.id().as_str() } else { diagnostic.secondary_code_or_id() diff --git a/crates/ruff_linter/src/message/grouped.rs b/crates/ruff_linter/src/message/grouped.rs index 6c7752b939..b22e661012 100644 --- a/crates/ruff_linter/src/message/grouped.rs +++ b/crates/ruff_linter/src/message/grouped.rs @@ -17,6 +17,7 @@ pub struct GroupedEmitter { show_fix_status: bool, applicability: Applicability, preview: bool, + prefer_rule_codes: bool, } impl Default for GroupedEmitter { @@ -25,6 +26,7 @@ impl Default for GroupedEmitter { show_fix_status: false, applicability: Applicability::Safe, preview: false, + prefer_rule_codes: false, } } } @@ -47,6 +49,12 @@ impl GroupedEmitter { self.preview = preview; self } + + #[must_use] + pub fn with_prefer_rule_codes(mut self, prefer_rule_codes: bool) -> Self { + self.prefer_rule_codes = prefer_rule_codes; + self + } } impl Emitter for GroupedEmitter { @@ -87,6 +95,7 @@ impl Emitter for GroupedEmitter { row_length, column_length, preview: self.preview, + prefer_rule_codes: self.prefer_rule_codes, } )?; } @@ -136,6 +145,7 @@ struct DisplayGroupedMessage<'a> { column_length: NonZeroUsize, notebook_index: Option<&'a NotebookIndex>, preview: bool, + prefer_rule_codes: bool, } impl Display for DisplayGroupedMessage<'_> { @@ -182,6 +192,7 @@ impl Display for DisplayGroupedMessage<'_> { show_fix_status: self.show_fix_status, applicability: self.applicability, preview: self.preview, + prefer_rule_codes: self.prefer_rule_codes, }, )?; @@ -194,15 +205,17 @@ pub(super) struct RuleCodeAndBody<'a> { pub(crate) show_fix_status: bool, pub(crate) applicability: Applicability, pub(crate) preview: bool, + pub(crate) prefer_rule_codes: bool, } impl Display for RuleCodeAndBody<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let use_name = self.preview && !self.prefer_rule_codes; if self.show_fix_status { if let Some(fix) = self.message.fix() { // Do not display an indicator for inapplicable fixes if fix.applies(self.applicability) { - let code = if self.preview { + let code = if use_name { self.message.id().as_str() } else { self.message.secondary_code_or_id() @@ -218,9 +231,7 @@ impl Display for RuleCodeAndBody<'_> { } } - if !self.preview - && let Some(code) = self.message.secondary_code() - { + if !use_name && let Some(code) = self.message.secondary_code() { write!( f, "{code} {body}", diff --git a/crates/ruff_linter/src/message/mod.rs b/crates/ruff_linter/src/message/mod.rs index 9959c42f75..a25ad43b22 100644 --- a/crates/ruff_linter/src/message/mod.rs +++ b/crates/ruff_linter/src/message/mod.rs @@ -213,6 +213,7 @@ pub fn render_diagnostics( .with_show_fix_status(config.show_fix_status()) .with_applicability(config.fix_applicability()) .with_preview(config.preview_enabled()) + .with_prefer_rule_codes(config.is_prefer_rule_codes_enabled()) .emit(writer, diagnostics, context) .map_err(std::io::Error::other)?; } diff --git a/crates/ruff_linter/src/message/sarif.rs b/crates/ruff_linter/src/message/sarif.rs index 1d6a6edf86..375e544f1c 100644 --- a/crates/ruff_linter/src/message/sarif.rs +++ b/crates/ruff_linter/src/message/sarif.rs @@ -194,7 +194,11 @@ impl Serialize for RuleCode<'_> { impl<'a> RuleCode<'a> { fn from_diagnostic(code: &'a Diagnostic, config: &'a DisplayDiagnosticConfig) -> Self { match code.secondary_code() { - Some(diagnostic) if !config.preview_enabled() => Self::SecondaryCode(diagnostic), + Some(diagnostic) + if !config.preview_enabled() || config.is_prefer_rule_codes_enabled() => + { + Self::SecondaryCode(diagnostic) + } _ => Self::LintId(code.id().as_str()), } } diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index 396e491b1b..8579673c5c 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -4,6 +4,7 @@ use std::fmt::Write; use std::path::Path; use ruff_python_ast::SourceType; +use ruff_workspace::Settings; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; @@ -23,7 +24,7 @@ use ruff_linter::{ package::PackageRoot, packaging::detect_package_root, preview::is_human_readable_names_enabled, - settings::{LinterSettings, flags}, + settings::flags, source_kind::SourceKind, suppression::Suppressions, }; @@ -157,7 +158,9 @@ pub(crate) fn check( &directives.noqa_line_for, stylist.line_ending(), &suppressions, - if is_human_readable_names_enabled(settings.linter.preview) { + if is_human_readable_names_enabled(settings.linter.preview) + && !settings.output_prefer_rule_codes + { SuppressionKind::Ignore } else { SuppressionKind::Noqa @@ -172,7 +175,7 @@ pub(crate) fn check( document_uri: &document_uri, notebook, supports_related_information, - settings: &settings.linter, + settings, }; let mut diagnostics_map = DiagnosticsMap::default(); @@ -258,7 +261,7 @@ struct LspDiagnosticContext<'a> { document_uri: &'a lsp_types::Uri, notebook: Option<&'a NotebookDocument>, supports_related_information: bool, - settings: &'a LinterSettings, + settings: &'a Settings, } /// Generates an LSP diagnostic with an associated cell index for the diagnostic to go in. @@ -276,7 +279,9 @@ fn to_lsp_diagnostic( let (severity, code) = if let Some(code) = diagnostic.secondary_code() { let severity = severity(code); - let code = if is_human_readable_names_enabled(context.settings.preview) { + let code = if is_human_readable_names_enabled(context.settings.linter.preview) + && !context.settings.output_prefer_rule_codes + { name.to_string() } else { code.to_string() @@ -565,7 +570,7 @@ mod tests { }; let index = LineIndex::from_source_text(source); let uri = lsp_types::Uri::parse("file:///test.py").expect("URI to be valid"); - let settings = LinterSettings::default(); + let settings = Settings::default(); let context = LspDiagnosticContext { source_kind: &source_kind, index: &index, diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index d6f9560e55..8b7267a607 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -429,7 +429,8 @@ impl Workspace { }) .collect(); - let code = if !is_human_readable_names_enabled(self.settings.linter.preview) + let code = if (!is_human_readable_names_enabled(self.settings.linter.preview) + || self.settings.output_prefer_rule_codes) && let Some(code) = msg.secondary_code() { code.as_str() diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 8c8f5491b4..3790c9bb4c 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -179,6 +179,7 @@ pub struct Configuration { pub fix_only: Option, pub unsafe_fixes: Option, pub output_format: Option, + pub output_prefer_rule_codes: Option, pub preview: Option, pub required_version: Option, pub extension: Option, @@ -325,6 +326,7 @@ impl Configuration { fix_only: self.fix_only.unwrap_or(false), unsafe_fixes: self.unsafe_fixes.unwrap_or_default(), output_format: self.output_format.unwrap_or_default(), + output_prefer_rule_codes: self.output_prefer_rule_codes.unwrap_or_default(), show_fixes: self.show_fixes.unwrap_or(false), file_resolver: FileResolverSettings { @@ -604,6 +606,7 @@ impl Configuration { fix_only: options.fix_only, unsafe_fixes: options.unsafe_fixes.map(UnsafeFixes::from), output_format: options.output_format, + output_prefer_rule_codes: options.output_prefer_rule_codes, force_exclude: options.force_exclude, line_length: options.line_length, indent_width: options.indent_width, @@ -667,6 +670,9 @@ impl Configuration { fix_only: self.fix_only.or(config.fix_only), unsafe_fixes: self.unsafe_fixes.or(config.unsafe_fixes), output_format: self.output_format.or(config.output_format), + output_prefer_rule_codes: self + .output_prefer_rule_codes + .or(config.output_prefer_rule_codes), force_exclude: self.force_exclude.or(config.force_exclude), line_length: self.line_length.or(config.line_length), indent_width: self.indent_width.or(config.indent_width), diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 321bb99c50..e1403f98e3 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -107,6 +107,30 @@ pub struct Options { )] pub output_format: Option, + /// Whether to prefer rule codes over human-readable rule names in diagnostic output, even + /// when preview mode is enabled. + /// + /// Diagnostics without rule codes, such as syntax errors and formatting diagnostics, will + /// continue to use the human-readable name, but those corresponding to lint rules will use the + /// rule's code. For example, the concise diagnostic for an unused import will use the code + /// `F401` instead of the name `unused-import`: + /// + /// ```console + /// $ ruff check --preview --config 'output-prefer-rule-codes = true' --output-format=concise example.py + /// example.py:1:8: F401 [*] `math` imported but unused + /// $ ruff check --preview --config 'output-prefer-rule-codes = false' --output-format=concise example.py + /// example.py:1:8: unused-import: [*] `math` imported but unused + /// ``` + #[option( + default = "false", + value_type = "bool", + example = r#" + # Display rule codes instead of human-readable rule names. + output-prefer-rule-codes = true + "# + )] + pub output_prefer_rule_codes: Option, + /// Enable fix behavior by-default when running `ruff` (overridden /// by the `--fix` and `--no-fix` command-line flags). /// Only includes automatic fixes unless `--unsafe-fixes` is provided. diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index a0b137ba87..8527e17aff 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -18,6 +18,7 @@ use ruff_source_file::find_newline; use std::fmt; use std::path::{Path, PathBuf}; +#[expect(clippy::struct_excessive_bools)] #[derive(Debug, CacheKey)] pub struct Settings { #[cache_key(ignore)] @@ -31,6 +32,8 @@ pub struct Settings { #[cache_key(ignore)] pub output_format: OutputFormat, #[cache_key(ignore)] + pub output_prefer_rule_codes: bool, + #[cache_key(ignore)] pub show_fixes: bool, pub file_resolver: FileResolverSettings, @@ -47,6 +50,7 @@ impl Default for Settings { fix: false, fix_only: false, output_format: OutputFormat::default(), + output_prefer_rule_codes: false, show_fixes: false, unsafe_fixes: UnsafeFixes::default(), linter: LinterSettings::new(project_root), @@ -67,6 +71,7 @@ impl fmt::Display for Settings { self.fix, self.fix_only, self.output_format, + self.output_prefer_rule_codes, self.show_fixes, self.unsafe_fixes, self.file_resolver | nested, diff --git a/ruff.schema.json b/ruff.schema.json index 11b83aec21..6ecca20f03 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -562,6 +562,13 @@ } ] }, + "output-prefer-rule-codes": { + "description": "Whether to prefer rule codes over human-readable rule names in diagnostic output, even\nwhen preview mode is enabled.\n\nDiagnostics without rule codes, such as syntax errors and formatting diagnostics, will\ncontinue to use the human-readable name, but those corresponding to lint rules will use the\nrule's code. For example, the concise diagnostic for an unused import will use the code\n`F401` instead of the name `unused-import`:\n\n```console\n$ ruff check --preview --config 'output-prefer-rule-codes = true' --output-format=concise example.py\nexample.py:1:8: F401 [*] `math` imported but unused\n$ ruff check --preview --config 'output-prefer-rule-codes = false' --output-format=concise example.py\nexample.py:1:8: unused-import: [*] `math` imported but unused\n```", + "type": [ + "boolean", + "null" + ] + }, "pep8-naming": { "description": "Options for the `pep8-naming` plugin.", "anyOf": [ From 6ce19e795b55ae0d11fd8e616942e4fefd138c09 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 28 Jul 2026 14:42:48 -0400 Subject: [PATCH 100/390] [`pylint`] Add missing fix safety gotchas for `non-augmented-assignment` (`PLR6104`) (#27250) ## Summary Add missing fix safety gotchas for [non-augmented-assignment (PLR6104)](https://docs.astral.sh/ruff/rules/non-augmented-assignment/#non-augmented-assignment-plr6104) Extracted from https://github.com/astral-sh/ruff/pull/27188 ## Test Plan Look at generated doc --- .../src/rules/pylint/rules/non_augmented_assignment.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs b/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs index cbe51cdd8d..ad7d85f406 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs @@ -68,6 +68,13 @@ use crate::{AlwaysFixableViolation, Edit, Fix}; /// foo += [2] /// assert (foo, bar) == ([1, 2], [1, 2]) /// ``` +/// +/// An augmented assignment can also fail where the plain form succeeds. NumPy +/// writes the result into the target's buffer, so `a *= b` raises where +/// `a = a * b` would broadcast to a new shape or promote the dtype. The same +/// applies to `a @= b`, which requires the product to have the target's shape. +/// +/// The fix replaces the whole statement, so any comments inside it are lost. #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "v0.3.7")] pub(crate) struct NonAugmentedAssignment { From 810b772cd855d5acb27d68fdc3b0a008e52be465 Mon Sep 17 00:00:00 2001 From: Dex Devlon Date: Wed, 29 Jul 2026 00:42:05 +0530 Subject: [PATCH 101/390] [`pydocstyle`] Skip section detection inside RST directive bodies (`D214`, `D405`, `D413`) (#23635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #23562. Content inside reStructuredText directives (e.g., `.. code-block:: yaml`) was incorrectly identified as docstring section headers. For example, `references:` inside a code-block would trigger D405 (capitalization), D214 (over-indentation), and D413 (missing blank line). The root cause is that the section parser in `from_docstring` (`docstrings/sections.rs`) iterates docstring lines and calls `is_docstring_section` with no awareness of RST directive blocks. The word `references` matches `SectionKind::References`, the suffix `:` passes the section name check, and the preceding blank line (required by RST after the directive declaration) satisfies the end-of-paragraph heuristic. There is already existing RST awareness in `blanks_and_section_underline` (the `is_sphinx` check at lines 1593 and 1691), but that serves a different purpose — it preserves blank lines when a *real* section header like `Example:` has a `.. code-block::` directive as its body content. This fix is complementary: it prevents content *inside* a directive body from being misidentified as section headers in the first place. This adds RST directive body tracking to `from_docstring`. When a line starting with `.. ` is detected (an RST directive), all subsequent lines indented deeper than the directive are skipped from section detection. Real sections after directives continue to be detected correctly. ## Test Plan Added `sphinx_directive.py` test fixture with cases for: - Module-level docstring with `.. code-block:: yaml` containing `references:` (the exact case from #23562) - Function-level docstring with directive followed by real sections (`Returns:`) - Single-colon directive variant (`.. code-block: yaml`) - Nested directives (`.. note::` containing `.. code-block::`) - Real section (`Notes:`) following a directive — verifies sections after directives are still detected Registered test cases for `D214`, `D405`, and `D413` against the new fixture. All 72 pydocstyle tests pass: ``` cargo test -p ruff_linter -- "pydocstyle::tests::rules" test result: ok. 72 passed; 0 failed; 0 ignored ``` Also manually verified the original reproduction case no longer triggers D405/D214/D413 false positives. --------- Co-authored-by: Brent Westbrook --- .../fixtures/pydocstyle/sphinx_directive.py | 78 +++++++++++++++++++ crates/ruff_linter/src/docstrings/sections.rs | 26 ++++++- .../ruff_linter/src/rules/pydocstyle/mod.rs | 6 ++ ...tyle__tests__D214_sphinx_directive.py.snap | 4 + ...tyle__tests__D405_sphinx_directive.py.snap | 4 + ...tyle__tests__D413_sphinx_directive.py.snap | 53 +++++++++++++ 6 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pydocstyle/sphinx_directive.py create mode 100644 crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sphinx_directive.py.snap create mode 100644 crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D405_sphinx_directive.py.snap create mode 100644 crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sphinx_directive.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pydocstyle/sphinx_directive.py b/crates/ruff_linter/resources/test/fixtures/pydocstyle/sphinx_directive.py new file mode 100644 index 0000000000..4a79396323 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pydocstyle/sphinx_directive.py @@ -0,0 +1,78 @@ +"""A module-level docstring with a Sphinx directive containing section-like content. + +.. code-block:: yaml + + references: + - ref: Bibliographic citation in your favorite format. + refType: open literature + +This is more text after the directive. +""" + + +def func(): + """A function-level docstring with a Sphinx directive. + + Examples: + This is an example. + + .. code-block:: python + + returns = "not a section" + notes = "also not a section" + + Returns: + None + """ + + +def func2(): + """A function-level docstring with single-colon directive (invalid RST but still common). + + .. code-block: yaml + + references: + - ref: Some reference. + + More text. + """ + + +def func3(): + """A function-level docstring with nested directives. + + .. note:: + + .. code-block:: python + + warnings = "not a section" + + Returns: + None + """ + + +def func4(): + """A function-level docstring where a real section follows a directive. + + .. code-block:: python + + example = "code" + + Notes: + This IS a real section and should still be detected. + """ +# Regression test for nested directive state. +def func5(): + """A nested directive whose outer body contains section-like content. + + .. note:: + + .. code-block:: yaml + + references: + - ref: Some reference. + + references: + Still part of the note body, not a section. + """ diff --git a/crates/ruff_linter/src/docstrings/sections.rs b/crates/ruff_linter/src/docstrings/sections.rs index a4151c67aa..85c4474106 100644 --- a/crates/ruff_linter/src/docstrings/sections.rs +++ b/crates/ruff_linter/src/docstrings/sections.rs @@ -155,11 +155,31 @@ impl<'a> SectionContexts<'a> { // Skip the first line, which is the summary. let mut previous_line = lines.next(); + // Track only the outermost RST directive. Nested directives remain in its body + // until a non-blank line dedents to the outermost indentation. + // See: https://github.com/astral-sh/ruff/issues/23562 + let mut directive_indent = None; + while let Some(line) = lines.next() { - if let Some(section_kind) = suspected_as_section(&line, style) { - let indent = leading_space(&line); - let indent_size = indent.text_len(); + let indent = leading_space(&line); + let indent_size = indent.text_len(); + + if let Some(active_indent) = directive_indent + && (line.trim().is_empty() || indent_size > active_indent) + { + previous_line = Some(line); + continue; + } + + directive_indent = None; + if line.trim_start().starts_with(".. ") { + directive_indent = Some(indent_size); + previous_line = Some(line); + continue; + } + + if let Some(section_kind) = suspected_as_section(&line, style) { let section_name = leading_words(&line); let section_name_size = section_name.text_len(); diff --git a/crates/ruff_linter/src/rules/pydocstyle/mod.rs b/crates/ruff_linter/src/rules/pydocstyle/mod.rs index e94315db80..e3490be532 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/mod.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/mod.rs @@ -85,6 +85,12 @@ mod tests { #[test_case(Rule::MissingSectionNameColon, Path::new("D.py"))] #[test_case(Rule::OverindentedSection, Path::new("sections.py"))] #[test_case(Rule::OverindentedSection, Path::new("D214_module.py"))] + #[test_case(Rule::OverindentedSection, Path::new("sphinx_directive.py"))] + #[test_case(Rule::NonCapitalizedSectionName, Path::new("sphinx_directive.py"))] + #[test_case( + Rule::MissingBlankLineAfterLastSection, + Path::new("sphinx_directive.py") + )] #[test_case(Rule::OverindentedSectionUnderline, Path::new("D215.py"))] #[test_case(Rule::MissingSectionUnderlineAfterName, Path::new("sections.py"))] #[test_case(Rule::MismatchedSectionUnderlineLength, Path::new("sections.py"))] diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sphinx_directive.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sphinx_directive.py.snap new file mode 100644 index 0000000000..724d6e7d20 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D214_sphinx_directive.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D405_sphinx_directive.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D405_sphinx_directive.py.snap new file mode 100644 index 0000000000..724d6e7d20 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D405_sphinx_directive.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +--- + diff --git a/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sphinx_directive.py.snap b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sphinx_directive.py.snap new file mode 100644 index 0000000000..7c9ba10d96 --- /dev/null +++ b/crates/ruff_linter/src/rules/pydocstyle/snapshots/ruff_linter__rules__pydocstyle__tests__D413_sphinx_directive.py.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_linter/src/rules/pydocstyle/mod.rs +--- +D413 [*] Missing blank line after last section ("Returns") + --> sphinx_directive.py:24:5 + | +22 | notes = "also not a section" +23 | +24 | Returns: + | ^^^^^^^ +25 | None +26 | """ + | +help: Add blank line after "Returns" + | +25 | None +26 + +27 | """ + | + +D413 [*] Missing blank line after last section ("Returns") + --> sphinx_directive.py:50:5 + | +48 | warnings = "not a section" +49 | +50 | Returns: + | ^^^^^^^ +51 | None +52 | """ + | +help: Add blank line after "Returns" + | +51 | None +52 + +53 | """ + | + +D413 [*] Missing blank line after last section ("Notes") + --> sphinx_directive.py:62:5 + | +60 | example = "code" +61 | +62 | Notes: + | ^^^^^ +63 | This IS a real section and should still be detected. +64 | """ + | +help: Add blank line after "Notes" + | +63 | This IS a real section and should still be detected. +64 + +65 | """ + | From 84be0c934bf8c26d1ec4a579542196db4b0dc091 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 28 Jul 2026 15:19:15 -0400 Subject: [PATCH 102/390] [ty] Remove obsolete protocol Todo guards (#27252) ## Summary Prior to #26574, class and static protocol methods were represented as `Todo`-typed attributes, so protocol member handling excluded `Todo` attributes from non-method classification, write requirements, and `type[Protocol]` lookup. #26574 gave these members an explicit method representation, leaving the exclusions obsolete. We now process `Todo`-typed attributes through the same capability paths as other attributes and remove the redundant `meta_access` and `has_todo_type` helpers. --- .../src/types/protocol_class.rs | 41 ++++++------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 06c25e6311..6ffefd7d6b 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -469,7 +469,7 @@ impl<'db> ProtocolInterface<'db> { pub(super) fn non_method_members(self, db: &'db dyn Db) -> Vec> { self.members(db) - .filter(|member| !member.is_method() && !member.has_todo_type()) + .filter(|member| !member.is_method()) .collect() } @@ -539,14 +539,15 @@ impl<'db> ProtocolInterface<'db> { receiver_ty: Type<'db>, name: &str, ) -> Option<(Option>, TypeQualifiers)> { - self.member_by_name(db, name).and_then(|member| { - Some(( + self.member_by_name(db, name).map(|member| { + ( member - .meta_access(db)? + .capabilities(db) + .class .write .and_then(|write| write.bind_compatibility_type(db, receiver_ty)), member.qualifiers(), - )) + ) }) } @@ -600,16 +601,16 @@ impl<'db> ProtocolInterface<'db> { db: &'db dyn Db, name: &str, ) -> Option> { - self.member_by_name(db, name).and_then(|member| { - let read = member.meta_access(db)?.read; - Some(PlaceAndQualifiers { + self.member_by_name(db, name).map(|member| { + let read = member.capabilities(db).class.read; + PlaceAndQualifiers { place: read .and_then(|read| read.resolve(db)) .map(|read| Place::bound(read.ty())) .unwrap_or(Place::Undefined) .with_provenance(Provenance::from_definition(member.definition())), qualifiers: member.qualifiers(), - }) + } }) } @@ -1148,20 +1149,16 @@ impl<'db> ProtocolMemberData<'db> { ProtocolMemberKind::Attribute(member_ty) => { let is_class_var = self.qualifiers.contains(TypeQualifiers::CLASS_VAR); let is_final = self.qualifiers.contains(TypeQualifiers::FINAL); - // A `Todo` records a protocol member form that is not modeled yet; do not infer a - // write requirement from that temporary representation. - let is_todo = member_ty.ty().is_todo(); ProtocolMemberCapabilities { instance: ProtocolMemberAccess::new( Some(member_ty), - (!is_class_var && !is_final && !is_todo) + (!is_class_var && !is_final) .then_some(ProtocolMemberWrite::from_type(member_ty)), ), class: if is_class_var { ProtocolMemberAccess::new( Some(member_ty), - (!is_final && !is_todo) - .then_some(ProtocolMemberWrite::from_type(member_ty)), + (!is_final).then_some(ProtocolMemberWrite::from_type(member_ty)), ) } else { ProtocolMemberAccess::NONE @@ -1671,20 +1668,6 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { capabilities } } - - fn meta_access(&self, db: &'db dyn Db) -> Option> { - if self.has_todo_type() { - return None; - } - Some(self.capabilities(db).class) - } - - fn has_todo_type(&self) -> bool { - self.data - .kind - .member_types() - .any(|ty| matches!(ty, ProtocolMemberType::Value { ty, .. } if ty.is_todo())) - } } fn property_get_member_type<'db>( From 475d11aeed5ca174bef2e0e8a59336be6adc8259 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 28 Jul 2026 12:38:30 -0700 Subject: [PATCH 103/390] [ty] Preserve inference when filtering constructor overloads (#27254) ## Summary Fixes astral-sh/ty#4106. A concrete call to an overloaded generic constructor should select the first matching overload, unless there is real ambiguity due to gradual arguments. In main, the following instead incorrectly resolves to `Unknown`: ```python class MixedSelf[T]: @overload def __new__(cls, value: list[T]) -> Self: ... @overload def __new__(cls, value: T) -> T: ... reveal_type(MixedSelf([1])) # should be MixedSelf[int], but is Unknown in main ``` During step 5 of overload evaluation, the `cls` argument is `type[MixedSelf[T]]`, while the first matching overload has already specialized its receiver to `type[MixedSelf[int]]`. The previous step 5 check accidentally treated `T` as fixed instead of inferable, so it could not recognize that `type[MixedSelf[T]]` is assignable to `type[MixedSelf[int]]`. This caused step 5 to wrongly consider the overloads gradually ambiguous and infer `Unknown`. In this PR, we pass the current overload's inferable type variables into the assignability check to fix this. The receiver must still participate in overload filtering (Codex's first fix attempt here was to simply exclude it, which is wrong): it can be the only gradual argument. For example: ```python class Foo[T]: @overload def __new__(cls: type[Foo[int]]) -> int: ... @overload def __new__(cls: type[Foo[str]]) -> str: ... reveal_type(Foo[int]()) # int reveal_type(Foo[str]()) # str reveal_type(Foo[Any]()) # Unknown: genuinely ambiguous ``` ## Test plan - Cover the original overlapping `Self`/`T` constructor regression and a non-instance-return variant. - Cover concrete receiver-specific overload selection for `Foo[int]()` and `Foo[str]()`. - Cover `Foo[Any]()` as a genuine ambiguity when the synthetic receiver is the only gradual argument. ### All ecosystem changes are improvements. Bokeh is the only suspicious-looking case, but this is just exposing an existing unsupported metaclass-provides-property-descriptors thing that Bokeh does, so these errors are expected. --- .../resources/mdtest/call/constructor.md | 69 +++++++++++++++++++ .../ty_python_semantic/src/types/call/bind.rs | 10 ++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/constructor.md b/crates/ty_python_semantic/resources/mdtest/call/constructor.md index 030bd9e091..b79d67143d 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/constructor.md +++ b/crates/ty_python_semantic/resources/mdtest/call/constructor.md @@ -991,6 +991,75 @@ reveal_type(SimpleMixed(1)) # revealed: int reveal_type(SimpleMixed("foo")) # revealed: SimpleMixed ``` +### Overlapping generic `__new__` overloads preserve first-match selection + +A synthetic constructor receiver can still contain inferable class type variables, even though each +overload specializes `cls` differently. Step 5 of the overload evaluation algorithm must preserve +the overload's inferable variables when checking whether the argument types are covered; otherwise a +concrete constructor call appears ambiguous. In particular, both overloads below accept `list[int]`, +but the first one must win. The non-instance return case verifies that this is not specific to +`Self` or to returning the constructed class. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from __future__ import annotations + +from typing import Self, overload + +class MixedSelf[T]: + @overload + def __new__(cls, value: list[T]) -> Self: ... + @overload + def __new__(cls, value: T) -> T: ... + def __new__(cls, value: object) -> object: + return object.__new__(cls) + +reveal_type(MixedSelf([1])) # revealed: MixedSelf[int] +reveal_type(MixedSelf(1)) # revealed: Literal[1] + +class DistinctNonInstanceReturns[T]: + @overload + def __new__(cls, value: list[T]) -> str: ... + @overload + def __new__(cls, value: T) -> T: ... + def __new__(cls, value: object) -> object: + return object.__new__(cls) + +reveal_type(DistinctNonInstanceReturns([1])) # revealed: str +``` + +### A gradual constructor receiver participates in overload filtering + +The synthetic `cls` argument must participate in overload filtering because it can be the only +gradual argument. A concrete class specialization selects its matching receiver overload, but an +`Any` specialization can match receivers with different return types and must remain ambiguous. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from __future__ import annotations + +from typing import Any, overload + +class Foo[T]: + @overload + def __new__(cls: type[Foo[int]]) -> int: ... + @overload + def __new__(cls: type[Foo[str]]) -> str: ... + def __new__(cls) -> object: ... + +reveal_type(Foo[int]()) # revealed: int +reveal_type(Foo[str]()) # revealed: str +reveal_type(Foo[Any]()) # revealed: Unknown +``` + ### Multiple matching `__new__` overloads If overload resolution for `__new__` falls back to `Unknown` because the argument is `Any` or diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 5af16dac6f..6524162ad8 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -3992,7 +3992,15 @@ impl<'db> CallableBinding<'db> { }), ); - if top_materialized_argument_type.is_assignable_to(db, parameter_types) { + if top_materialized_argument_type + .when_assignable_to( + db, + parameter_types, + constraints, + self.overloads[*current_index].inferable_typevars, + ) + .is_always_satisfied(db) + { filter_remaining_overloads = true; } } From 1d1d437d93e4997657642aceff1e8a3b03b92fce Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 28 Jul 2026 13:36:57 -0700 Subject: [PATCH 104/390] Document mdtest writing guidelines (#27255) I tried to distill the items that keep coming up when I review Codex-authored mdtests into some AGENTS guidance. This could go in an mdtest-writing skill, but since mdtests are used across both ruff and ty, almost every change involves modifying or adding mdtests, and this guidance is reasonably short, I think it's fine to have it directly in the repo AGENTS file. --- AGENTS.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index d41e7f7dae..b334d20b8c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,6 +63,14 @@ When running tests with `INSTA_FORCE_PASS=1`, check for `.pending-snap` files if Never edit snapshot files or inline snapshot bodies manually. Regenerate them by running the relevant tests with the snapshot-update environment variables documented above, then review the generated diff. +## Writing mdtests + +- Write mdtests as readable, literate specifications, and minimize the context a reader must hold in mind. Prefer short, focused code blocks, and define types, fixtures, and helpers close to the assertions that use them. Give independent scenarios separate Markdown test headings; when scenarios need shared setup, interleave short prose-and-code blocks under the same heading. Code blocks for the same file within a section are concatenated, so do not repeat imports or definitions. +- Introduce each scenario with a short prose paragraph explaining the code immediately below. Use clear, precise terminology. Avoid long paragraphs covering multiple scenarios followed by a single long code block. +- Minimize regression examples to the behavior under test. When adapting real-world code or an issue reproducer, remove incidental types, methods, type parameters, imports, and domain-specific details. Preserve complexity only when necessary to reproduce the regression or distinguish the intended behavior, and reuse nearby fixtures or simple built-in types when doing so keeps the test easy to understand. +- Prefer a minimal, purpose-built custom type over a standard-library type when a regression depends on particular attributes, methods, bounds, or constraints. Define the relevant behavior in the test so readers do not need to look up the standard-library type to understand the scenario. For commonly used standard-library types, consider adding a separate regression using the real type to protect against changes in typeshed. +- Place each mdtest in a file for the behavior it actually tests, and assert that behavior directly. Prefer an existing file when one already covers that behavior; create a new file when no existing file is a good fit. Do not choose a file solely because its directive or helper can express the assertion. + ## Running Clippy ```sh From 645499fed69d8997278b81562522d0b261452303 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 28 Jul 2026 16:28:01 -0700 Subject: [PATCH 105/390] [ty] Support materialized class type expressions (#27258) ## Summary Accept `type[Top[...]]` and `type[Bottom[...]]` in type expressions. ## Test plan - Mdtests cover `type[Top[list[Any]]]` and `type[Bottom[list[Any]]]`, including revealed class-object and constructed instance types. - Mdtests cover PEP 695 generic aliases, aliases of fully materialized types, and invalid nested `Top`/`Bottom` arity. --- .../mdtest/type_properties/materialization.md | 65 +++++++++++++++++++ .../types/infer/builder/type_expression.rs | 5 +- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md index f16ba7d56d..b4e4e99f48 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -501,6 +501,71 @@ def _(top: Top[list[type[Any]]], bottom: Bottom[list[type[Any]]]): reveal_type(bottom) # revealed: Bottom[list[type[Any]]] ``` +## Materialized class annotations and constructors + +A class-object annotation can name either materialization of an invariant generic. Calling the +annotated class produces an instance with the same materialization. + +```py +from typing import Any +from ty_extensions import Bottom, Top + +def materialized_list_classes( + top: type[Top[list[Any]]], + bottom: type[Bottom[list[Any]]], +) -> None: + reveal_type(top) # revealed: type[Top[list[Any]]] + reveal_type(bottom) # revealed: type[Bottom[list[Any]]] + reveal_type(top()) # revealed: Top[list[Any]] + reveal_type(bottom()) # revealed: Bottom[list[Any]] +``` + +## Generic aliases of materialized classes + +A generic class alias can be materialized inside `type[...]`. Aliasing the complete materialized +type also preserves its polarity, and both alias forms resolve to the underlying class. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any +from ty_extensions import Bottom, Top + +type ListAlias[T] = list[T] +type TopList = Top[ListAlias[Any]] +type BottomList = Bottom[ListAlias[Any]] + +def aliased_materialized_list_classes( + generic_top: type[Top[ListAlias[Any]]], + generic_bottom: type[Bottom[ListAlias[Any]]], + aliased_top: type[TopList], + aliased_bottom: type[BottomList], +) -> None: + reveal_type(generic_top) # revealed: type[Top[list[Any]]] + reveal_type(generic_bottom) # revealed: type[Bottom[list[Any]]] + reveal_type(aliased_top) # revealed: type[Top[list[Any]]] + reveal_type(aliased_bottom) # revealed: type[Bottom[list[Any]]] + reveal_type(aliased_top()) # revealed: Top[list[Any]] + reveal_type(aliased_bottom()) # revealed: Bottom[list[Any]] +``` + +## Invalid materialization arity in class annotations + +`Top` and `Bottom` each require exactly one type argument, even when they are nested inside a +class-object annotation. + +```py +from ty_extensions import Bottom, Top + +def invalid_materialized_list_classes( + top: type[Top[int, str]], # error: [invalid-type-form] + bottom: type[Bottom[int, str]], # error: [invalid-type-form] +) -> None: ... +``` + ## Type variables ```toml diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 6cb9f1704b..623888fb5a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1358,7 +1358,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); invalid_type_argument(self, slice) } - value_ty @ Type::KnownInstance(KnownInstanceType::TypeAliasType(_)) => { + value_ty @ (Type::SpecialForm( + SpecialFormType::Top | SpecialFormType::Bottom, + ) + | Type::KnownInstance(KnownInstanceType::TypeAliasType(_))) => { let slice_ty = self.infer_subscript_type_expression(subscript, value_ty); subclass_of_type_argument(self, slice, slice_ty) } From 40538b6df48376a6e1284213775b2b4b2379c69f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:32:34 +0200 Subject: [PATCH 106/390] Update Rust crate console_log to v1.1.0 (#27294) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d34a10c5b9..b9021ed5eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -732,9 +732,9 @@ dependencies = [ [[package]] name = "console_log" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +checksum = "86919cef3e37b9356ccf54d4421208c17ecfda01beae61393e7ffd72916c0ef1" dependencies = [ "log", "web-sys", From c747076605eda9d6aac5c310cd166ac6cf6a1c28 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:33:09 +0200 Subject: [PATCH 107/390] Update Rust crate serde_json to v1.0.151 (#27285) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9021ed5eb..d9099c37c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3993,9 +3993,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", From 4e597128db0c681da221aa35fcfc4a7ea62e2536 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:33:33 +0200 Subject: [PATCH 108/390] Update Rust crate libc to v0.2.189 (#27279) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9099c37c3..7b144d0644 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1914,9 +1914,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libcst" From c18947e20e65a0548c2ba2b6e2e48b926097b974 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:34:16 +0200 Subject: [PATCH 109/390] Update Rust crate syn to v3 (#27300) --- Cargo.lock | 103 +++++++++++++++++++++++++++++------------------------ Cargo.toml | 2 +- 2 files changed, 58 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b144d0644..e4b84aad01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,7 +215,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -229,7 +229,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -245,7 +245,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn", + "syn 2.0.119", ] [[package]] @@ -357,7 +357,7 @@ checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -547,7 +547,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -649,7 +649,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -972,7 +972,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -992,7 +992,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1058,7 +1058,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1069,7 +1069,7 @@ checksum = "8dc51d98e636f5e3b0759a39257458b22619cac7e96d932da6eeb052891bb67c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1302,7 +1302,7 @@ checksum = "c736d226c32e496b8377813b52269e11ad3a48d8373b68862d0364f04fd1229d" dependencies = [ "attribute-derive", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1749,7 +1749,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1835,7 +1835,7 @@ checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1940,7 +1940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0903173ea316c34a44d0497161e04d9210af44f5f5e89bf2f55d9a254c9a0e8d" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2038,7 +2038,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2185,7 +2185,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2508,7 +2508,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2667,7 +2667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.119", ] [[package]] @@ -2698,7 +2698,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2738,7 +2738,7 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2796,7 +2796,7 @@ checksum = "a9a28b8493dd664c8b171dd944da82d933f7d456b829bfb236738e1fe06c5ba4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2827,7 +2827,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2961,7 +2961,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3035,7 +3035,7 @@ checksum = "c0ed1a78a1b19d184b0daa629dd9a024573173ec7d485b287cb369fb3607cc1c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3389,7 +3389,7 @@ dependencies = [ "quote", "regex", "ruff_python_trivia", - "syn", + "syn 3.0.3", ] [[package]] @@ -3884,7 +3884,7 @@ checksum = "ec5c48c5a4a53a6e2be9762f56566f1325d4394c011c00b3ea86ba2d13411e71" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3918,7 +3918,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.119", ] [[package]] @@ -3977,7 +3977,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3988,7 +3988,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4180,7 +4180,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4200,6 +4200,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -4208,7 +4219,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4276,7 +4287,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4287,7 +4298,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "test-case-core", ] @@ -4326,7 +4337,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4337,7 +4348,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4499,7 +4510,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5260,7 +5271,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -5303,7 +5314,7 @@ checksum = "ee997551ce1ad5adda03f7ce37ec34b4140fe9f547fd07b46d55901d1ba1a06b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5436,7 +5447,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5447,7 +5458,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5696,7 +5707,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.119", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -5712,7 +5723,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -5794,7 +5805,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -5815,7 +5826,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5835,7 +5846,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -5869,7 +5880,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e209367e60..2dd480e63c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -184,7 +184,7 @@ static_assertions = "1.1.0" strum = { version = "0.28.0", features = ["strum_macros"] } strum_macros = { version = "0.28.0" } supports-hyperlinks = { version = "3.1.0" } -syn = { version = "2.0.55" } +syn = { version = "3.0.0" } tempfile = { version = "3.9.0" } test-case = { version = "3.3.1" } thiserror = { version = "2.0.0" } From cdee3d496d878173b372c4c088e720712135db29 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:34:58 +0200 Subject: [PATCH 110/390] Update Rust crate jiff to v0.2.34 (#27278) --- Cargo.lock | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4b84aad01..e3ce5aa647 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1813,11 +1813,12 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" +checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16" dependencies = [ "defmt", + "jiff-core", "jiff-static", "jiff-tzdb-platform", "log", @@ -1827,12 +1828,22 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + [[package]] name = "jiff-static" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" +checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de" dependencies = [ + "jiff-core", "proc-macro2", "quote", "syn 2.0.119", From 573be94e78c90fff2925d6362ff0b2fc0920baa8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:35:21 +0200 Subject: [PATCH 111/390] Update actions/setup-go action to v7 (#27298) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c2576232eb..dc20bfff79 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -263,7 +263,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: "1.26.5" - name: "Run ShellCheck" From 0e8c303fab059b5e01e86736c46895bae9af4bf3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:35:36 +0200 Subject: [PATCH 112/390] Update Rust crate proc-macro2 to v1.0.107 (#27280) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3ce5aa647..562746d897 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2725,9 +2725,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] From 8789e4745e7d462c9fc601de6f7d0b3072541275 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:36:20 +0200 Subject: [PATCH 113/390] Update Rust crate ignore to v0.4.31 (#27277) --- Cargo.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 562746d897..3386f87467 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -566,7 +566,7 @@ dependencies = [ "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -685,7 +685,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -1035,7 +1035,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -1115,7 +1115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -1598,9 +1598,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b009b6744c1445efd7244084e25e498636412effb6760b55067553baa925cc7" +checksum = "7f8a7b8211e695a1d0cd91cace480d4d0bd57667ab10277cc412c5f7f4884f83" dependencies = [ "crossbeam-deque", "globset", @@ -3839,7 +3839,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -4249,7 +4249,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -5428,7 +5428,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] From 159ff80dceb2ec34550680c729eeaed6dc2a8063 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:36:56 +0200 Subject: [PATCH 114/390] Update Rust crate which to v8.0.5 (#27287) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3386f87467..d92a9403ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5390,9 +5390,9 @@ dependencies = [ [[package]] name = "which" -version = "8.0.4" +version = "8.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d7cd18d4acb58fb3cdfe9ea54e6cd96a4e7d4cc45c56338b236e82dad47248" +checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" dependencies = [ "libc", ] From 365840bf56a1aa813a049b55cd49a703f3bd46b3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:37:29 +0200 Subject: [PATCH 115/390] Update Rust crate uuid to v1.24.0 (#27296) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d92a9403ce..fff345b888 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5138,9 +5138,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.5" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "js-sys", "wasm-bindgen", From 3f3b8f467df0996e2040e544c125541c94876fde Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:37:44 +0200 Subject: [PATCH 116/390] Update Rust crate regex to v1.13.1 (#27282) --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fff345b888..c7e143a05e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2977,9 +2977,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -2989,9 +2989,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", From 3e955ba2f96f8f44dddd6b65f5466d0f675ec4e1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:38:00 +0200 Subject: [PATCH 117/390] Update Rust crate regex-automata to v0.4.16 (#27283) From 35474c4a1ed0c1b14705ce78bf6ab6edccd57993 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:38:14 +0200 Subject: [PATCH 118/390] Update Rust crate globset to v0.4.19 (#27276) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7e143a05e..3551a6a7e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1375,9 +1375,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" dependencies = [ "aho-corasick", "bstr", From e3652b7ada74f92e8b9b1b81c7da7be8cafd3fcb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:38:25 +0200 Subject: [PATCH 119/390] Update Rust crate quote to v1.0.47 (#27281) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3551a6a7e5..847c626f2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2812,9 +2812,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] From a2ab24dba0f4469763c21ed422c0e21be9d8ad54 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:38:54 +0200 Subject: [PATCH 120/390] Update Rust crate glob to v0.3.4 (#27275) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 847c626f2d..c201bcff5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1369,9 +1369,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "globset" From 55f00f811f66ebc78391b3b8d21d1d5351ea476b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:40:10 +0200 Subject: [PATCH 121/390] Update Rust crate bitflags to v2.13.1 (#27273) --- Cargo.lock | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c201bcff5a..bfe940245f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,9 +277,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -1044,7 +1044,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -1392,7 +1392,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "ignore", "walkdir", ] @@ -1673,7 +1673,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "inotify-sys", "libc", ] @@ -1975,7 +1975,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "libc", ] @@ -2220,7 +2220,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -2248,7 +2248,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "fsevent-sys", "inotify", "kqueue", @@ -2941,7 +2941,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -3055,7 +3055,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "once_cell", "serde", "serde_derive", @@ -3070,7 +3070,7 @@ dependencies = [ "anyhow", "argfile", "assert_fs", - "bitflags 2.13.0", + "bitflags 2.13.1", "cachedir", "clap", "clap_complete_command", @@ -3333,7 +3333,7 @@ version = "0.16.0" dependencies = [ "aho-corasick", "anyhow", - "bitflags 2.13.0", + "bitflags 2.13.1", "clap", "colored", "compact_str", @@ -3470,7 +3470,7 @@ version = "0.0.6" dependencies = [ "aho-corasick", "arrayvec", - "bitflags 2.13.0", + "bitflags 2.13.1", "char_str", "compact_str", "get-size2", @@ -3576,7 +3576,7 @@ dependencies = [ name = "ruff_python_literal" version = "0.0.6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "icu_properties", "itertools 0.15.0", "ruff_python_ast", @@ -3587,7 +3587,7 @@ name = "ruff_python_parser" version = "0.0.6" dependencies = [ "anyhow", - "bitflags 2.13.0", + "bitflags 2.13.1", "bstr", "datatest-stable", "drop_bomb", @@ -3615,7 +3615,7 @@ dependencies = [ name = "ruff_python_semantic" version = "0.0.6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "insta", "is-macro", "ruff_cache", @@ -3636,7 +3636,7 @@ dependencies = [ name = "ruff_python_stdlib" version = "0.0.6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "unicode-ident", ] @@ -3835,7 +3835,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -4678,7 +4678,7 @@ dependencies = [ name = "ty_ide" version = "0.0.0" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "camino", "compact_str", "get-size2", @@ -4791,7 +4791,7 @@ name = "ty_python_core" version = "0.0.6" dependencies = [ "anyhow", - "bitflags 2.13.0", + "bitflags 2.13.1", "bitvec", "char_str", "get-size2", @@ -4825,7 +4825,7 @@ name = "ty_python_semantic" version = "0.0.6" dependencies = [ "anyhow", - "bitflags 2.13.0", + "bitflags 2.13.1", "camino", "char_str", "compact_str", @@ -4879,7 +4879,7 @@ name = "ty_server" version = "0.0.0" dependencies = [ "anyhow", - "bitflags 2.13.0", + "bitflags 2.13.1", "crossbeam", "dunce", "gen-lsp-types", @@ -5362,7 +5362,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -5746,7 +5746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.13.0", + "bitflags 2.13.1", "indexmap", "log", "serde", From 2ebd87af0f0e4b7fb938355a209e3588d44d5d5e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:40:29 +0200 Subject: [PATCH 122/390] Update Rust crate anyhow to v1.0.104 (#27272) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bfe940245f..c88adac60a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,9 +155,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "approx" From 3d56a82670a0fed3550bffa60617edb0f21abe03 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:40:45 +0200 Subject: [PATCH 123/390] Update actions/checkout action to v7.0.1 (#27269) --- .github/workflows/build-binaries.yml | 16 +++--- .github/workflows/build-docker.yml | 2 +- .github/workflows/build-wasm.yml | 2 +- .github/workflows/ci.yaml | 60 ++++++++++---------- .github/workflows/daily_fuzz.yaml | 2 +- .github/workflows/memory_report.yaml | 2 +- .github/workflows/publish-crates.yml | 2 +- .github/workflows/publish-docs.yml | 2 +- .github/workflows/publish-playground.yml | 2 +- .github/workflows/publish-ty-playground.yml | 2 +- .github/workflows/publish-versions.yml | 2 +- .github/workflows/sync_typeshed.yaml | 10 ++-- .github/workflows/ty-ecosystem-analyzer.yaml | 2 +- .github/workflows/ty-ecosystem-report.yaml | 2 +- .github/workflows/typing_conformance.yaml | 4 +- 15 files changed, 56 insertions(+), 56 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 5bed4e7d45..be9855cf26 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -39,7 +39,7 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false @@ -69,7 +69,7 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-macos-15' || 'macos-15' }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false @@ -116,7 +116,7 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-macos-15' || 'macos-15' }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false @@ -177,7 +177,7 @@ jobs: - target: aarch64-pc-windows-msvc arch: x64 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false @@ -231,7 +231,7 @@ jobs: - x86_64-unknown-linux-gnu - i686-unknown-linux-gnu steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false @@ -313,7 +313,7 @@ jobs: manylinux: 2_31 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false @@ -379,7 +379,7 @@ jobs: - x86_64-unknown-linux-musl - i686-unknown-linux-musl steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false @@ -444,7 +444,7 @@ jobs: arch: armv7 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index d60b9c497b..06f39d7d98 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -39,7 +39,7 @@ jobs: - linux/amd64 - linux/arm64 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive persist-credentials: false diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml index e615b31d64..982e42aa56 100644 --- a/.github/workflows/build-wasm.yml +++ b/.github/workflows/build-wasm.yml @@ -35,7 +35,7 @@ jobs: target: [web, bundler, nodejs] fail-fast: false steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Install Rust toolchain" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dc20bfff79..9b89a96c05 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -52,7 +52,7 @@ jobs: # Flag that is set to "true" when code related to the benchmarks changes. benchmarks: ${{ steps.check_benchmarks.outputs.changed }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -248,7 +248,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Install Rust toolchain" @@ -260,7 +260,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 @@ -286,7 +286,7 @@ jobs: if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }} timeout-minutes: 20 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -308,7 +308,7 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 20 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -327,7 +327,7 @@ jobs: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_DEV_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -394,7 +394,7 @@ jobs: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_PROFILING_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -433,7 +433,7 @@ jobs: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_DEV_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -464,7 +464,7 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -499,7 +499,7 @@ jobs: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_DEV_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: SebRollen/toml-action@b1b3628f55fc3a28208d4203ada8b737e9687876 # v1.2.0 @@ -528,7 +528,7 @@ jobs: if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' || needs.determine_changes.outputs.code == 'true' }} timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -555,7 +555,7 @@ jobs: env: FORCE_COLOR: 1 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 @@ -594,7 +594,7 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 5 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -640,7 +640,7 @@ jobs: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_DEV_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.pull_request.base.ref }} persist-credentials: false @@ -667,7 +667,7 @@ jobs: cargo build --bin ruff mv target/debug/ruff target/debug/ruff-baseline - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false clean: false @@ -755,7 +755,7 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && (needs.determine_changes.outputs.ty == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }} timeout-minutes: ${{ github.repository == 'astral-sh/ruff' && 10 || 20 }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # Faster to do this separately than to use `fetch-depth: 0` with `actions/checkout` @@ -808,7 +808,7 @@ jobs: needs: determine_changes if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: cargo-bins/cargo-binstall@ead08b90bd7b2e6d81963fb9cf0b7239f66d5db4 # v1.21.0 @@ -824,7 +824,7 @@ jobs: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_PROFILING_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 @@ -848,7 +848,7 @@ jobs: timeout-minutes: 20 if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 @@ -877,7 +877,7 @@ jobs: runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 @@ -907,7 +907,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -939,7 +939,7 @@ jobs: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -964,7 +964,7 @@ jobs: # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_DEV_DEBUG: line-tables-only steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: "Checkout ruff source" with: persist-credentials: false @@ -983,7 +983,7 @@ jobs: - name: Build Ruff binary run: cargo build -p ruff --bin ruff - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: "Checkout ruff-lsp source" with: persist-credentials: false @@ -1022,7 +1022,7 @@ jobs: - determine_changes if: ${{ (needs.determine_changes.outputs.playground == 'true') }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Install Rust toolchain" @@ -1068,7 +1068,7 @@ jobs: id-token: write # required for OIDC authentication with CodSpeed steps: - name: "Checkout Branch" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -1110,7 +1110,7 @@ jobs: timeout-minutes: 20 steps: - name: "Checkout Branch" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -1172,7 +1172,7 @@ jobs: mode: simulation steps: - name: "Checkout Branch" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 @@ -1219,7 +1219,7 @@ jobs: timeout-minutes: 20 steps: - name: "Checkout Branch" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -1275,7 +1275,7 @@ jobs: filter: "pydantic|freqtrade|multithreaded|altair" steps: - name: "Checkout Branch" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index 3d7162646a..aa5f85f692 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -33,7 +33,7 @@ jobs: # Don't run the cron job on forks: if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 diff --git a/.github/workflows/memory_report.yaml b/.github/workflows/memory_report.yaml index ca6d22b0e5..23c39675ed 100644 --- a/.github/workflows/memory_report.yaml +++ b/.github/workflows/memory_report.yaml @@ -44,7 +44,7 @@ jobs: runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }} timeout-minutes: 20 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: ruff persist-credentials: false diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml index b2e086d6ba..d16b76b5af 100644 --- a/.github/workflows/publish-crates.yml +++ b/.github/workflows/publish-crates.yml @@ -21,7 +21,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 2094b6ec8a..eb49873b93 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -26,7 +26,7 @@ jobs: name: release runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ inputs.ref }} persist-credentials: true diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml index 32b0f8f1cb..4f407c0618 100644 --- a/.github/workflows/publish-playground.yml +++ b/.github/workflows/publish-playground.yml @@ -27,7 +27,7 @@ jobs: env: CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Install Rust toolchain" diff --git a/.github/workflows/publish-ty-playground.yml b/.github/workflows/publish-ty-playground.yml index e9ab1ff682..0dee4e2491 100644 --- a/.github/workflows/publish-ty-playground.yml +++ b/.github/workflows/publish-ty-playground.yml @@ -31,7 +31,7 @@ jobs: env: CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Install Rust toolchain" diff --git a/.github/workflows/publish-versions.yml b/.github/workflows/publish-versions.yml index 52e7ed28ec..fadeb2df69 100644 --- a/.github/workflows/publish-versions.yml +++ b/.github/workflows/publish-versions.yml @@ -20,7 +20,7 @@ jobs: env: VERSION: ${{ fromJson(inputs.plan).announcement_tag }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index be9daeebcd..d2065351e4 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -69,12 +69,12 @@ jobs: permissions: contents: write # to push back to the repository steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout Ruff with: path: ruff persist-credentials: true - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout typeshed with: repository: python/typeshed @@ -135,7 +135,7 @@ jobs: permissions: contents: write # to push back to the repository steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout Ruff with: persist-credentials: true @@ -177,7 +177,7 @@ jobs: permissions: contents: write # to push back to the repository steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout Ruff with: persist-credentials: true @@ -248,7 +248,7 @@ jobs: permissions: contents: write # to push back to the repository steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 name: Checkout Ruff with: persist-credentials: true diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index 96068e766a..ad25aa140b 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -53,7 +53,7 @@ jobs: runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }} timeout-minutes: 5 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false ref: ${{ github.sha }} diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index d8c9bb18b9..42b4421779 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -28,7 +28,7 @@ jobs: runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }} timeout-minutes: 40 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml index 538b17c47e..99dc57b01e 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -45,7 +45,7 @@ jobs: runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: ruff persist-credentials: false @@ -54,7 +54,7 @@ jobs: - name: Fetch full history without tags run: git -C ruff fetch --no-tags --filter=blob:none --unshallow origin - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: python/typing ref: ${{ env.CONFORMANCE_SUITE_COMMIT }} From 837f3fcab4d897ce806dac740df0dc22cae10037 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:41:52 +0200 Subject: [PATCH 124/390] Update dependency ruff to v0.16.0 (#27291) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index a545725cd9..7b1244ee63 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ PyYAML==6.0.3 -ruff==0.15.22 +ruff==0.16.0 mkdocs==1.6.1 mkdocs-material==9.7.6 mkdocs-redirects==1.2.3 From b36e72bfdc363419a2b9c97fd13e66b2b5aee6c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:42:52 +0200 Subject: [PATCH 125/390] Update Rust to v1.97.1 (#27288) --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 398f3f015c..7243604323 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.97.0" +channel = "1.97.1" From beeefe95d8bf58fb012cb42feab6b02b4d53daac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:43:05 +0200 Subject: [PATCH 126/390] Update taiki-e/install-action action to v2.84.0 (#27297) --- .github/workflows/ci.yaml | 16 ++++++++-------- .github/workflows/sync_typeshed.yaml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9b89a96c05..66b38fa70b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -339,7 +339,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest and insta" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 with: tool: | cargo-nextest @@ -405,7 +405,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 with: tool: cargo-nextest - name: "Install uv" @@ -444,7 +444,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 with: tool: cargo-nextest - name: "Install uv" @@ -1083,7 +1083,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 with: tool: cargo-codspeed @@ -1122,7 +1122,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 with: tool: cargo-codspeed @@ -1180,7 +1180,7 @@ jobs: version: "0.11.32" - name: "Install codspeed" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 with: tool: cargo-codspeed @@ -1234,7 +1234,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 with: tool: cargo-codspeed @@ -1284,7 +1284,7 @@ jobs: version: "0.11.32" - name: "Install codspeed" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 with: tool: cargo-codspeed diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index d2065351e4..c60d52aa15 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -268,7 +268,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest and insta" - uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2.83.2 + uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 with: tool: | cargo-nextest From 6447e32ed326dd5957d14299fb69cc4c55092ffd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:43:32 +0200 Subject: [PATCH 127/390] Update dependency astral-sh/uv to v0.12.0 (#27289) --- .github/workflows/ci.yaml | 28 ++++++++++---------- .github/workflows/daily_fuzz.yaml | 2 +- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/sync_typeshed.yaml | 6 ++--- .github/workflows/ty-ecosystem-analyzer.yaml | 4 +-- .github/workflows/ty-ecosystem-report.yaml | 2 +- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 66b38fa70b..056d038258 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -347,7 +347,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" enable-cache: "true" - name: ty mdtests (GitHub annotations) if: ${{ needs.determine_changes.outputs.ty == 'true' }} @@ -411,7 +411,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" enable-cache: "true" - name: "Run tests" run: cargo nextest run --cargo-profile profiling --all-features @@ -450,7 +450,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" enable-cache: "true" - name: "Run tests" run: | @@ -560,7 +560,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: ruff-linux-debug @@ -602,7 +602,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - name: "Install Rust toolchain" run: rustup component add rustfmt # Run all code generation scripts, and verify that the current output is @@ -649,7 +649,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} activate-environment: true - version: "0.11.32" + version: "0.12.0" - name: "Install Rust toolchain" run: rustup show @@ -763,7 +763,7 @@ jobs: run: git fetch --no-tags --filter=blob:none --unshallow origin - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -829,7 +829,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -882,7 +882,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 @@ -920,7 +920,7 @@ jobs: with: python-version: 3.13 activate-environment: true - version: "0.11.32" + version: "0.12.0" - name: "Install dependencies" run: uv pip install -r docs/requirements.txt - name: "Update README File" @@ -1077,7 +1077,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - name: "Install Rust toolchain" run: rustup show @@ -1177,7 +1177,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - name: "Install codspeed" uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 @@ -1228,7 +1228,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - name: "Install Rust toolchain" run: rustup show @@ -1281,7 +1281,7 @@ jobs: - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - name: "Install codspeed" uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index aa5f85f692..c430ccfb75 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -38,7 +38,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - name: "Install Rust toolchain" run: rustup show - name: "Install mold" diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 2fcd7819e5..cec7e0b981 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -24,7 +24,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: wheels-* diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index c60d52aa15..d992d92beb 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -86,7 +86,7 @@ jobs: git config --global user.email '<>' - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - name: Sync typeshed stubs run: | rm -rf "ruff/${VENDORED_TYPESHED}" @@ -142,7 +142,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - name: Setup git run: | git config --global user.name typeshedbot @@ -184,7 +184,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" - name: Setup git run: | git config --global user.name typeshedbot diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index ad25aa140b..2174f274fd 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -127,7 +127,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" ignore-empty-workdir: true # The default of `enable-cache: auto` leads to warnings from setup-uv in this job, # since its cache-invalidation keys (pyproject.toml, uv.lock, etc.) aren't available @@ -187,7 +187,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.11.32" + version: "0.12.0" ignore-empty-workdir: true # The default of `enable-cache: auto` leads to warnings from setup-uv in this job, # since its cache-invalidation keys (pyproject.toml, uv.lock, etc.) aren't available diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 42b4421779..97998b6682 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -36,7 +36,7 @@ jobs: uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: enable-cache: true - version: "0.11.32" + version: "0.12.0" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: From c1e3133523dcfb574b31238cdf7ae1934a15adbf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:44:37 +0200 Subject: [PATCH 128/390] Update dependency monaco-editor to ^0.56.0 (#27290) --- playground/package-lock.json | 20 ++++++++++---------- playground/ruff/package.json | 2 +- playground/shared/package.json | 2 +- playground/ty/package.json | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/playground/package-lock.json b/playground/package-lock.json index b8e9cb0329..49b483e592 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -4673,9 +4673,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", + "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -6636,12 +6636,12 @@ } }, "node_modules/monaco-editor": { - "version": "0.55.1", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", - "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz", + "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==", "license": "MIT", "dependencies": { - "dompurify": "3.2.7", + "dompurify": "3.4.8", "marked": "14.0.0" } }, @@ -8461,7 +8461,7 @@ "@monaco-editor/react": "^4.4.6", "classnames": "^2.3.2", "lz-string": "^1.5.0", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.56.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^4.0.0", @@ -8480,7 +8480,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.3.2", "fflate": "^0.8.2", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.56.0", "react": "^19.0.0", "react-aria-components": "^1.16.0", "react-resizable-panels": "^4.0.0" @@ -8493,7 +8493,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.5.1", "lz-string": "^1.5.0", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.56.0", "pyodide": "^314.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/playground/ruff/package.json b/playground/ruff/package.json index ebae6503ca..a89da89c4e 100644 --- a/playground/ruff/package.json +++ b/playground/ruff/package.json @@ -18,7 +18,7 @@ "@monaco-editor/react": "^4.4.6", "classnames": "^2.3.2", "lz-string": "^1.5.0", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.56.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^4.0.0", diff --git a/playground/shared/package.json b/playground/shared/package.json index 7d9c0dd61c..4706fa58b4 100644 --- a/playground/shared/package.json +++ b/playground/shared/package.json @@ -7,7 +7,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.3.2", "fflate": "^0.8.2", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.56.0", "react-aria-components": "^1.16.0", "react": "^19.0.0", "react-resizable-panels": "^4.0.0" diff --git a/playground/ty/package.json b/playground/ty/package.json index c1e586ea3e..e96e136b79 100644 --- a/playground/ty/package.json +++ b/playground/ty/package.json @@ -18,7 +18,7 @@ "@monaco-editor/react": "^4.7.0", "classnames": "^2.5.1", "lz-string": "^1.5.0", - "monaco-editor": "^0.55.0", + "monaco-editor": "^0.56.0", "pyodide": "^314.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", From c76580935b5c3592fb1f702ef4f5436ce5f91fb4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:45:05 +0200 Subject: [PATCH 129/390] Update dependency mkdocs-material to v9.7.7 (#27270) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 7b1244ee63..8dba234ac0 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ PyYAML==6.0.3 ruff==0.16.0 mkdocs==1.6.1 -mkdocs-material==9.7.6 +mkdocs-material==9.7.7 mkdocs-redirects==1.2.3 mdformat==1.0.0 mdformat-mkdocs==5.2.1 From fca24d89adecbc52d34f2589e820267c350c22c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:45:24 +0200 Subject: [PATCH 130/390] Update dependency prek to v0.4.10 (#27271) --- pyproject.toml | 2 +- uv.lock | 114 ++++++++++++++++++++++++------------------------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d699d47859..323606cc39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ exclude = [ [dependency-groups] dev = [ - "prek==0.4.9", + "prek==0.4.10", ] release = [ "rooster==0.1.1", diff --git a/uv.lock b/uv.lock index 76c49dd971..44b52b16b6 100644 --- a/uv.lock +++ b/uv.lock @@ -30,8 +30,8 @@ name = "anyio" version = "4.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", marker = "python_full_version == '3.12.*'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } wheels = [ @@ -43,7 +43,7 @@ name = "anysqlite" version = "0.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.12'" }, + { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/4b/cd5d66b9f87e773bc71344a368b9472987e33514e6627e28342b9c3e7c43/anysqlite-0.0.5.tar.gz", hash = "sha256:9dfcf87baf6b93426ad1d9118088c41dbf24ef01b445eea4a5d486bac2755cce", size = 3432, upload-time = "2023-10-02T13:49:25.135Z" } wheels = [ @@ -64,7 +64,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "python_full_version >= '3.12' and implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -176,11 +176,11 @@ name = "hishel" version = "0.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.12'" }, - { name = "anysqlite", marker = "python_full_version >= '3.12'" }, - { name = "httpx", marker = "python_full_version >= '3.12'" }, - { name = "msgpack", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "anyio" }, + { name = "anysqlite" }, + { name = "httpx" }, + { name = "msgpack" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/64/a104ccac48f123f853254483617b16e0efc1649bd7e35bcdc5a5a5ef0ae2/hishel-0.1.5.tar.gz", hash = "sha256:9d40c682cd94fd6e1394fb05713ae20a75ed8aeba6f5272380444039ce6257f2", size = 75468, upload-time = "2025-10-18T13:32:41.854Z" } wheels = [ @@ -192,8 +192,8 @@ name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi", marker = "python_full_version >= '3.12'" }, - { name = "h11", marker = "python_full_version >= '3.12'" }, + { name = "certifi" }, + { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ @@ -205,10 +205,10 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.12'" }, - { name = "certifi", marker = "python_full_version >= '3.12'" }, - { name = "httpcore", marker = "python_full_version >= '3.12'" }, - { name = "idna", marker = "python_full_version >= '3.12'" }, + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ @@ -229,7 +229,7 @@ name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mdurl", marker = "python_full_version >= '3.12'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ @@ -338,26 +338,26 @@ wheels = [ [[package]] name = "prek" -version = "0.4.9" +version = "0.4.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/df/94ed29398576e03494c5aacda8bfed9536edf348bed29cd09f382f2b9b23/prek-0.4.9.tar.gz", hash = "sha256:f8b86441484a5756f3fdb6f3b201d3d448f8845902a84653d78cbf6f875c424f", size = 492711, upload-time = "2026-07-11T11:04:04.332Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/54/edc21e275f9fa3540d4d98cf349c2de11621d6729cc401bb7aedf563609e/prek-0.4.10.tar.gz", hash = "sha256:db3122f4e780eb4587635e6a83df881caf2dbb1eb7799d1cca51158216d6f33b", size = 502565, upload-time = "2026-07-16T10:13:00.788Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/02/632446b72103daf92526203bf83cb76043ebce70588632aa2aea3741da07/prek-0.4.9-py3-none-linux_armv6l.whl", hash = "sha256:7b240ad6f679104309a944c4dd427ccc46d9aaf4f53ee07379c02bf7578c2750", size = 5637604, upload-time = "2026-07-11T11:03:38.985Z" }, - { url = "https://files.pythonhosted.org/packages/57/bf/89daefc85a1db9b8c525731c77121de0a8f57731e81c99d823e919e1f6a5/prek-0.4.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5cbb220d6d77cb047747dcabf421375fdfdd958e01a998a854758698d38fb239", size = 5960396, upload-time = "2026-07-11T11:03:40.953Z" }, - { url = "https://files.pythonhosted.org/packages/42/39/0e448da00671e77740be32cca96530ef64bcdbed178acce7f155b87f6057/prek-0.4.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fd85df4c186becdc47b2e6155de8cace99133c7517387e30f08ca15b93ead11f", size = 5524982, upload-time = "2026-07-11T11:03:42.605Z" }, - { url = "https://files.pythonhosted.org/packages/71/e5/1beceff9cfcf02817c06f38e726c9875cd003f62cbfa516f282fb3c3109b/prek-0.4.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ba40511145e948d461d6641b556b6ff671b186b0c90743600b9dcb788dd5eb5c", size = 5793691, upload-time = "2026-07-11T11:03:44.106Z" }, - { url = "https://files.pythonhosted.org/packages/7e/17/99e4884c45c46be90e81e220d2bdd5020716963707ef6702361cc398dd91/prek-0.4.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0cc5c06ae448568d076bb5e7ce4d630879d6467b2a89fd79061c349a47826efa", size = 5544549, upload-time = "2026-07-11T11:03:45.564Z" }, - { url = "https://files.pythonhosted.org/packages/0c/4b/63f5fe22d867a6e07b12e8a7887a0f3b193c2ef0fa9ed80649f2d257f4ac/prek-0.4.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31af616c216ec7e47913802b082ca816d707d24738fa58eae0463ae296cbe71e", size = 5948798, upload-time = "2026-07-11T11:03:47.424Z" }, - { url = "https://files.pythonhosted.org/packages/1f/89/90d5005436afb6ab99d3ba6599820fe0f0fcb776f5e9e362b5a19e10cbea/prek-0.4.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1ef842d7f19879fac2135c42ba15508f2677346ba800bc8bb82e5f43fe6a6ec", size = 6696938, upload-time = "2026-07-11T11:03:49Z" }, - { url = "https://files.pythonhosted.org/packages/2a/62/068dd25e1106e262b0ce69ffcdc594cf52d85aa13f64628f851e458980d4/prek-0.4.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:442ecf6a454c692bc8a2d5a16936a0fe8e3419727ff208e15818932acca9923a", size = 6170870, upload-time = "2026-07-11T11:03:50.407Z" }, - { url = "https://files.pythonhosted.org/packages/8e/42/bb1f3f3d84af4d12bb675ff071305fa776d3525c10626465d9960ad93c21/prek-0.4.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5b3c590252e3d5724ebed3774695712ff7cb554743b25184808aa5f42b06bd4c", size = 5800355, upload-time = "2026-07-11T11:03:51.882Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ca/dfc8312b4a7ce8fa100ce37a843279d108eec7e7fe2ddbe069448acd1642/prek-0.4.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dd283b15dec4da29caa3910bd72c8c9d7df93209770503465ad109d6370cb8e", size = 5655126, upload-time = "2026-07-11T11:03:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/25/8a/b19413cb64b81c18502ea7bbef32897f9126b8e53b58cd656996573dc53c/prek-0.4.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:cc25e30e1700a5c7dd9bad665c321e5589b51502bb1bbc6ada45e326d08b428b", size = 5517839, upload-time = "2026-07-11T11:03:54.884Z" }, - { url = "https://files.pythonhosted.org/packages/94/af/900df3f7535e87045df331646c6a01bef6e77dba7f2bdb53483f30cbb988/prek-0.4.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4ff5947deeb9a92e6508dfba8b27962dfb927bf60fd36472c9ae862df96fb38c", size = 5802556, upload-time = "2026-07-11T11:03:56.787Z" }, - { url = "https://files.pythonhosted.org/packages/e9/7b/80e560cbe396d0f8687012769cb2d7d7f3428dd019549225ebf205dea806/prek-0.4.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8320ca167d41855d9c4fed66df599f31f96307cbb0da1311a9fe465152e20bd5", size = 6285747, upload-time = "2026-07-11T11:03:58.099Z" }, - { url = "https://files.pythonhosted.org/packages/a3/d4/01ae3b99d09559a69befd128859d0036c604608dd9c6c99986592dde3c21/prek-0.4.9-py3-none-win32.whl", hash = "sha256:b1e8d3bc88ddce6414853468ed8126f45d4ae20f2f4677801ade20ad67a826fa", size = 5320862, upload-time = "2026-07-11T11:03:59.803Z" }, - { url = "https://files.pythonhosted.org/packages/28/68/d038b14f0220fed197be8bc93229e6ea7ad460803ff8a26e4b14a8c81f66/prek-0.4.9-py3-none-win_amd64.whl", hash = "sha256:ed1b4f87a13d1565e8731c60db7fa058966049cbb4d8872d160add510a286558", size = 5706850, upload-time = "2026-07-11T11:04:01.207Z" }, - { url = "https://files.pythonhosted.org/packages/02/4a/57d04de49f591088901794cc22f36563a102681e3512ae17ee6085cd2f30/prek-0.4.9-py3-none-win_arm64.whl", hash = "sha256:7eab3900d9ea614c8ea0d0d55a8b708f0c88e43c966dc8b13a4e36c1e398dd16", size = 5540477, upload-time = "2026-07-11T11:04:02.815Z" }, + { url = "https://files.pythonhosted.org/packages/db/e7/5a63528ba7b95b64f38db3e253aed49ee8e5e8ba16589889d2b7f809edb7/prek-0.4.10-py3-none-linux_armv6l.whl", hash = "sha256:023f302741d79301346c3088ba43a9592aff0ecdbe5ddc3019fa9b1183319c5e", size = 5694609, upload-time = "2026-07-16T10:12:26.352Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ef/ee9e6bf9a5ce242e9e4e66ac4e2e9042a0f6fd9f367cee18ad404456e93d/prek-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:72adc707e16f97564bbae08d22b222ac3bb2491f8fbfb5a0754f80d472c28a71", size = 6044037, upload-time = "2026-07-16T10:12:28.539Z" }, + { url = "https://files.pythonhosted.org/packages/68/7e/da08cc39e5348ccb9234e63a21ee56861f72e8497d6a78f0db1ccae6515d/prek-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:04c9321957e1b32e1fc7cf60bb4f90bba3761f8659d5551ed04f96e25596de49", size = 5535983, upload-time = "2026-07-16T10:12:30.691Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/0486a35bb687a9beac7a5810bd1104c6da56d469b30b1eeaeefd03c99da2/prek-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e66ccf6c5e4ebadd05cd98cb338d7f553e4d27aa243cf91279c5a569b3cdccc7", size = 5862085, upload-time = "2026-07-16T10:12:33.042Z" }, + { url = "https://files.pythonhosted.org/packages/52/39/277fe17ae1f121e532e3942456f5a6d01ddacfbc550e481dcb359be7a1b0/prek-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63f9061d75a50ef0ca92c4b596ad352937a845df80758244950e513b27e9e18f", size = 5605697, upload-time = "2026-07-16T10:12:35.498Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a1/08354af3e000f2656fad086690d834eab6c04631ff41313a219ea6232199/prek-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c2ff7110e4bfaafbbab13c2893a337081aca61ed797f14b6b224d2ea9741eef", size = 6034111, upload-time = "2026-07-16T10:12:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/e4/74/4702396c8d486132e5ce009ab56a0b37f50cb6866830d371f2617b7bdfdc/prek-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b696a05542e79aa27bcce68d1792e77f4fe6f9c6b012b34d74d62f964f3c72d", size = 6787203, upload-time = "2026-07-16T10:12:40.031Z" }, + { url = "https://files.pythonhosted.org/packages/90/29/b5d5d6fb87ebd64b37471e3e79761de9983f85e14d69c522efe7af6620ce/prek-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:431b44d6054e72815b4b05e1173596dfd02a7f7461211d40a2e3117e414642ad", size = 6261333, upload-time = "2026-07-16T10:12:42.216Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/54ba696d19f7efdc184093353cce713a850aef9c3556e23faeecafa22e94/prek-0.4.10-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ccbd2b4fd1df790087ba18b4506f680471922a5f13714f19801568434a040dee", size = 5867761, upload-time = "2026-07-16T10:12:44.329Z" }, + { url = "https://files.pythonhosted.org/packages/be/7d/3975098aa2baaabfc10f99f9fcf78045c4f10851beed8e9812b6a2688eab/prek-0.4.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:479e7480b447191aa5c6ed67e80f081d0f5ee4e878b140f4d2cee44165395f1c", size = 5714412, upload-time = "2026-07-16T10:12:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/97/c0/3e0aac190fe95fdef98526343559b61d4d9fd54444c8c9137ba02412afe1/prek-0.4.10-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0bb7451025cbd2b68e480a13cf665d7a5c87c8b87bf18549a78985c17df817ed", size = 5578145, upload-time = "2026-07-16T10:12:48.261Z" }, + { url = "https://files.pythonhosted.org/packages/d7/44/7b26035534204b8b8a9d5e625479201e616413d287262f557cb32e1f8d77/prek-0.4.10-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4fb047e5776676805794574b2d7b178cb3ab536793aadf172419fcda56b34a57", size = 5889245, upload-time = "2026-07-16T10:12:50.818Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6c/178a9d768876b4211a1bf63907fe308ae02d173639bcf41cea3c5eed35c1/prek-0.4.10-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:08318818d19caf79643babb89f872c92fda134a622b4731df1d6ed61e29d2d26", size = 6372849, upload-time = "2026-07-16T10:12:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/4d/84/d5f5ac8193602883f9dd1d675d9d4084e34fbe3ed2ef50a0c336d8a53d8f/prek-0.4.10-py3-none-win32.whl", hash = "sha256:092872714dcde480a662bbdd98b980b248c2d3e10543d4d53a3a58cc9e5b35b0", size = 5413005, upload-time = "2026-07-16T10:12:55.113Z" }, + { url = "https://files.pythonhosted.org/packages/41/63/9e648fda10bc02c9b6ba305f93b6a6e4fd37d23d13a269a9d2d6bb44eaa1/prek-0.4.10-py3-none-win_amd64.whl", hash = "sha256:3d323a18d0f8c50e474a8fa29fb93bd2db680116d8afb19b76e72ad4667f58e6", size = 5799075, upload-time = "2026-07-16T10:12:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/22/74/b34d8c80cec8dccc7b922c75b9dca62b18b603b5ed2eea93c9d7c2928d2d/prek-0.4.10-py3-none-win_arm64.whl", hash = "sha256:5e93865ef96756c4a26f37ece04ad514abbc19ae6a23ed1a507b6314e6a0d2fb", size = 5563955, upload-time = "2026-07-16T10:12:59.07Z" }, ] [[package]] @@ -374,10 +374,10 @@ name = "pydantic" version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types", marker = "python_full_version >= '3.12'" }, - { name = "pydantic-core", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.12'" }, + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ @@ -389,7 +389,7 @@ name = "pydantic-core" version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ @@ -519,7 +519,7 @@ name = "pygit2" version = "1.19.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "python_full_version >= '3.12'" }, + { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/44/415aa93422b4bfc21a6448acb7e16280d5f33a9a3fae38a384e37b046ae4/pygit2-1.19.3.tar.gz", hash = "sha256:a543e6d4ebb43825564935758dc234e770016fed673b84370d46ae9580558831", size = 810489, upload-time = "2026-06-13T08:06:04.982Z" } wheels = [ @@ -594,8 +594,8 @@ name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "markdown-it-py" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ @@ -607,14 +607,14 @@ name = "rooster" version = "0.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "hishel", marker = "python_full_version >= '3.12'" }, - { name = "httpx", marker = "python_full_version >= '3.12'" }, - { name = "marko", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pydantic", marker = "python_full_version >= '3.12'" }, - { name = "pygit2", marker = "python_full_version >= '3.12'" }, - { name = "tqdm", marker = "python_full_version >= '3.12'" }, - { name = "typer", marker = "python_full_version >= '3.12'" }, + { name = "hishel" }, + { name = "httpx" }, + { name = "marko" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pygit2" }, + { name = "tqdm" }, + { name = "typer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/02/8ce565271dc52bd0d0d812043b12ec60111d947f81dc30301d19d7bfd453/rooster-0.1.1.tar.gz", hash = "sha256:c9823122f0c2b035985e70384323cdd353477af988e0f065bc302646a49da482", size = 18608, upload-time = "2025-10-29T15:18:49.478Z" } wheels = [ @@ -637,7 +637,7 @@ release = [ [package.metadata] [package.metadata.requires-dev] -dev = [{ name = "prek", marker = "python_full_version >= '3.12'", specifier = "==0.4.9" }] +dev = [{ name = "prek", marker = "python_full_version >= '3.12'", specifier = "==0.4.10" }] release = [{ name = "rooster", marker = "python_full_version >= '3.12'", specifier = "==0.1.1" }] [[package]] @@ -654,7 +654,7 @@ name = "tqdm" version = "4.68.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } wheels = [ @@ -666,10 +666,10 @@ name = "typer" version = "0.26.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "rich", marker = "python_full_version >= '3.12'" }, - { name = "shellingham", marker = "python_full_version >= '3.12'" }, + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } wheels = [ @@ -690,7 +690,7 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ From a977d63c24b0de6bdf71d1e1f350ecb433734752 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 06:45:26 +0000 Subject: [PATCH 131/390] Update Rust crate thiserror to v2.0.19 (#27286) --- Cargo.lock | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c88adac60a..38d9b59b69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -564,7 +564,7 @@ checksum = "d669bb552908e336ad5681789752033b45566b7e591aeaac7a614e58e5d6d8f2" dependencies = [ "nix", "terminfo", - "thiserror 2.0.18", + "thiserror 2.0.19", "which", "windows-sys 0.59.0", ] @@ -981,7 +981,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1941,7 +1941,7 @@ dependencies = [ "paste", "peg", "regex", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -2112,7 +2112,7 @@ dependencies = [ "serde", "similar 3.1.1", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml 1.1.3+spec-1.1.0", "toml_parser", "tracing", @@ -2495,7 +2495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror 2.0.18", + "thiserror 2.0.19", "ucd-trie", ] @@ -2762,7 +2762,7 @@ dependencies = [ "pep440_rs", "pep508_rs", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml 0.9.12+spec-1.1.0", ] @@ -2777,7 +2777,7 @@ dependencies = [ "newtype-uuid", "quick-xml", "strip-ansi-escapes", - "thiserror 2.0.18", + "thiserror 2.0.19", "uuid", ] @@ -2952,7 +2952,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -3119,7 +3119,7 @@ dependencies = [ "strum", "tempfile", "test-case", - "thiserror 2.0.18", + "thiserror 2.0.19", "tikv-jemallocator", "toml 1.1.3+spec-1.1.0", "tracing", @@ -3217,7 +3217,7 @@ dependencies = [ "similar 3.1.1", "supports-hyperlinks", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "tracing-subscriber", "ty_static", @@ -3381,7 +3381,7 @@ dependencies = [ "strum_macros", "tempfile", "test-case", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml 1.1.3+spec-1.1.0", "typed-arena", "unicode-normalization", @@ -3453,7 +3453,7 @@ dependencies = [ "serde", "serde_json", "test-case", - "thiserror 2.0.18", + "thiserror 2.0.19", "uuid", ] @@ -3487,7 +3487,7 @@ dependencies = [ "serde", "serde_json", "thin-vec", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -3542,7 +3542,7 @@ dependencies = [ "similar 3.1.1", "smallvec", "static_assertions", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", ] @@ -3709,7 +3709,7 @@ dependencies = [ "shellexpand", "smallvec", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml 1.1.3+spec-1.1.0", "tracing", "tracing-log", @@ -4333,11 +4333,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -4353,13 +4353,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -4733,7 +4733,7 @@ dependencies = [ "strum", "strum_macros", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "ty_vendored", ] @@ -4775,7 +4775,7 @@ dependencies = [ "shellexpand", "strum", "strum_macros", - "thiserror 2.0.18", + "thiserror 2.0.19", "toml 1.1.3+spec-1.1.0", "tracing", "ty_combine", @@ -4864,7 +4864,7 @@ dependencies = [ "strum", "strum_macros", "test-case", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "ty_module_resolver", "ty_python_core", @@ -4904,7 +4904,7 @@ dependencies = [ "smallvec", "strum", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "tracing-subscriber", "ty_combine", From d0b988994c214e23724c04ca7f8a91cb6253ee95 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:45:57 +0200 Subject: [PATCH 132/390] Update NPM Development dependencies (#27292) --- playground/api/package-lock.json | 72 +++--- playground/package-lock.json | 430 +++++++++++++++---------------- 2 files changed, 251 insertions(+), 251 deletions(-) diff --git a/playground/api/package-lock.json b/playground/api/package-lock.json index de40873be0..2aebfef42e 100644 --- a/playground/api/package-lock.json +++ b/playground/api/package-lock.json @@ -46,9 +46,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260710.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260710.1.tgz", - "integrity": "sha512-OqJl2eWF5+y9jarMm3YqqCTUe7Hd4ihogX5jyRU8iaAgOVyDr/Bk6aXpPCVUi1/MHzO93a18R/TmSTtzmB0sQw==", + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260721.1.tgz", + "integrity": "sha512-VivNMhiEdZIB4JBWxf1RMJGROErv53qmQ+dvhjA1evrCouvqRYW718VqDideU3PSV7Ythl5Df48NqZYWoaEHpQ==", "cpu": [ "x64" ], @@ -63,9 +63,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260710.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260710.1.tgz", - "integrity": "sha512-MYBqWgUblO+VlGvO73zYsH3hB9tdRj+yLyt5IHDFWryipb2l1efmNiWtAOkIhSRfypqLYGFrfpaDm2Hg00XVKw==", + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260721.1.tgz", + "integrity": "sha512-k7oye1ZiuwnnBBA2eTMduconr/ud5ZxFtRNTsYwMdmJeeeislw2+M72otrHxxvybCP7JWPPlJ38uhfajpcyhOA==", "cpu": [ "arm64" ], @@ -80,9 +80,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260710.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260710.1.tgz", - "integrity": "sha512-lVWUgqI8qrkqvaCBGElu1kdaUFdAvaS2RD8K4qkCFP9hI3f5TCXumEs5qWSeZkvKum0+X/uJZ5hBFWsYI5SmoQ==", + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260721.1.tgz", + "integrity": "sha512-hon0lW4ZQ4boAVgaw+0ZFTNS8v5MWPWvK0HZnt4tDpKYnDUviLZawtUW3KqvFmCQTipVHl1S34j3J8Eqb93hGQ==", "cpu": [ "x64" ], @@ -97,9 +97,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260710.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260710.1.tgz", - "integrity": "sha512-kDwDPItBjAI4JL0df9Fma2N+Qggbm77IB/DnroAkEGQ79fpR80sYMyuB/ZQKyjEk9f48Ocq7HCCLq59qVSyNqA==", + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260721.1.tgz", + "integrity": "sha512-nAl+HRQqpX5b7xVwWcvLPZmCk8NQ2yjI0yvJTWcHiRswbMEg1ZZckVmjJUAn0PHzZARbCSyIV7v3UjM+SPRmIQ==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260710.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260710.1.tgz", - "integrity": "sha512-GcLHy1oN1dfK6g1Z7UDV9f5xMGyTfPwcjWQ0sfWKH31IsoEVCRapnj3IC0PoIrDbnoo6irGPP0CwVs3WzdTajw==", + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260721.1.tgz", + "integrity": "sha512-9paFG5cMTKz/CRixnEEnZbe5uvFPBFSDthxJHANfCWhUtBj49GSL1FPIokIg+Q+H8DGJEExU0lL92LtxD0lTxQ==", "cpu": [ "x64" ], @@ -131,9 +131,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "5.20260715.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260715.1.tgz", - "integrity": "sha512-saxo/nMqQJ1dKDUXp1a2y/+IjKENFVD9+QRefHg5EjJZY20OG3xcEge4PGljbqZiF3AiU4o5ZLS3Vm7cayQIxg==", + "version": "5.20260722.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260722.1.tgz", + "integrity": "sha512-8+kivCgFGzwrAfNOWgSpzy/VDvmT/i5KWBgQhnygv3d1kajNn6mCYTbLKpouG0aY8mXjhv+IQm1a8r2K/H4pqQ==", "dev": true, "license": "MIT OR Apache-2.0" }, @@ -1754,16 +1754,16 @@ } }, "node_modules/miniflare": { - "version": "4.20260710.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260710.0.tgz", - "integrity": "sha512-x1LLRkU6o1p7hiKrB0TRnL0MJn6xFOT+/vrlEQINz5cRDKLP8ru4hBqWTIvXAetzr1acKAnmAaG84pQ4W/K14g==", + "version": "4.20260721.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260721.0.tgz", + "integrity": "sha512-fBLaCxZ2i/nPH8iyLzvza0C8/sSF4sjD1ma1Skf+pkZVK0TlaW5ujHJlUHwcwR66v2JZt+Q28d4DCX/oaLG0cA==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", - "workerd": "1.20260710.1", + "workerd": "1.20260721.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, @@ -2069,9 +2069,9 @@ } }, "node_modules/workerd": { - "version": "1.20260710.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260710.1.tgz", - "integrity": "sha512-U2sBPPrb9U97sBKnnMN6Kv8p65903P35nwMkPE9vSH/bRuRqkZ3a1EjUw3jV28RhiyXpkLF77Evzw8XimFxyTw==", + "version": "1.20260721.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260721.1.tgz", + "integrity": "sha512-b/DWhpV0jTudzQpLhDovcOgBz233386q+3Hbari7CLCNT9UXxjQziSTZ9yCoKdT2K3TSx5jrwlOisq8hlLWXYg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -2082,17 +2082,17 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260710.1", - "@cloudflare/workerd-darwin-arm64": "1.20260710.1", - "@cloudflare/workerd-linux-64": "1.20260710.1", - "@cloudflare/workerd-linux-arm64": "1.20260710.1", - "@cloudflare/workerd-windows-64": "1.20260710.1" + "@cloudflare/workerd-darwin-64": "1.20260721.1", + "@cloudflare/workerd-darwin-arm64": "1.20260721.1", + "@cloudflare/workerd-linux-64": "1.20260721.1", + "@cloudflare/workerd-linux-arm64": "1.20260721.1", + "@cloudflare/workerd-windows-64": "1.20260721.1" } }, "node_modules/wrangler": { - "version": "4.111.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.111.0.tgz", - "integrity": "sha512-bffpI9EyrnpKkF/1S+RaIv8oRD93GtbsA7TlfWwOsGJGB7VO3jVbdGzpC9TU7Bqom3z7jUxcte4Z9MPhaQ4HoQ==", + "version": "4.113.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.113.0.tgz", + "integrity": "sha512-ROGzSloJv0y21It6Oc9LaruNcu1tdiQ/XzL3Jc3YkFjzXEMXzTqVhA8vQaGMTdZHTjFP0PVcwAHNgaw3gXu4wA==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { @@ -2100,10 +2100,10 @@ "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "4.20260710.0", + "miniflare": "4.20260721.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260710.1" + "workerd": "1.20260721.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -2117,7 +2117,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^5.20260710.1" + "@cloudflare/workers-types": "^5.20260721.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { diff --git a/playground/package-lock.json b/playground/package-lock.json index 49b483e592..31f3c3538f 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -732,9 +732,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.137.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", - "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", "funding": { @@ -2540,9 +2540,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", - "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -2557,9 +2557,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", - "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -2574,9 +2574,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", - "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -2591,9 +2591,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", - "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -2608,9 +2608,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", - "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -2625,9 +2625,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", - "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], @@ -2645,9 +2645,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", - "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ "arm64" ], @@ -2665,9 +2665,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", - "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], @@ -2685,9 +2685,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", - "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], @@ -2705,9 +2705,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", - "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], @@ -2725,9 +2725,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", - "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], @@ -2745,9 +2745,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", - "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -2762,9 +2762,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", - "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ "wasm32" ], @@ -2781,9 +2781,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", - "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ "arm64" ], @@ -2798,9 +2798,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", - "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -2838,49 +2838,49 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", - "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "5.21.6", + "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.2" + "tailwindcss": "4.3.3" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", - "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-x64": "4.3.2", - "@tailwindcss/oxide-freebsd-x64": "4.3.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-x64-musl": "4.3.2", - "@tailwindcss/oxide-wasm32-wasi": "4.3.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", - "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", "cpu": [ "arm64" ], @@ -2895,9 +2895,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", - "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", "cpu": [ "arm64" ], @@ -2912,9 +2912,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", - "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", "cpu": [ "x64" ], @@ -2929,9 +2929,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", - "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", "cpu": [ "x64" ], @@ -2946,9 +2946,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", - "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", "cpu": [ "arm" ], @@ -2963,9 +2963,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", - "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", "cpu": [ "arm64" ], @@ -2983,9 +2983,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", - "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", "cpu": [ "arm64" ], @@ -3003,9 +3003,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", - "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", "cpu": [ "x64" ], @@ -3023,9 +3023,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", - "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", "cpu": [ "x64" ], @@ -3043,9 +3043,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", - "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -3139,9 +3139,9 @@ "optional": true }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", - "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", "cpu": [ "arm64" ], @@ -3156,9 +3156,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", - "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", "cpu": [ "x64" ], @@ -3173,15 +3173,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", - "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.3.2", - "@tailwindcss/oxide": "4.3.2", - "tailwindcss": "4.3.2" + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" @@ -3253,17 +3253,17 @@ "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz", - "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/type-utils": "8.63.0", - "@typescript-eslint/utils": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -3276,7 +3276,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.63.0", + "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -3292,16 +3292,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz", - "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" }, "engines": { @@ -3317,14 +3317,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz", - "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.63.0", - "@typescript-eslint/types": "^8.63.0", + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", "debug": "^4.4.3" }, "engines": { @@ -3339,14 +3339,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz", - "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3357,9 +3357,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz", - "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", "dev": true, "license": "MIT", "engines": { @@ -3374,15 +3374,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz", - "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/utils": "8.63.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -3399,9 +3399,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz", - "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", "dev": true, "license": "MIT", "engines": { @@ -3413,16 +3413,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz", - "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.63.0", - "@typescript-eslint/tsconfig-utils": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/visitor-keys": "8.63.0", + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -3493,16 +3493,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz", - "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.63.0", - "@typescript-eslint/types": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0" + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3517,13 +3517,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz", - "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.63.0", + "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3939,9 +3939,9 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", - "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", + "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", "dev": true, "license": "MIT", "dependencies": { @@ -4704,9 +4704,9 @@ "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.21.6", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", - "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6653,9 +6653,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -6970,9 +6970,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.21.tgz", + "integrity": "sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ==", "dev": true, "funding": [ { @@ -6990,7 +6990,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7009,9 +7009,9 @@ } }, "node_modules/prettier": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", - "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", "bin": { @@ -7318,13 +7318,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", - "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.137.0", + "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -7334,21 +7334,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.3", - "@rolldown/binding-darwin-arm64": "1.1.3", - "@rolldown/binding-darwin-x64": "1.1.3", - "@rolldown/binding-freebsd-x64": "1.1.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", - "@rolldown/binding-linux-arm64-gnu": "1.1.3", - "@rolldown/binding-linux-arm64-musl": "1.1.3", - "@rolldown/binding-linux-ppc64-gnu": "1.1.3", - "@rolldown/binding-linux-s390x-gnu": "1.1.3", - "@rolldown/binding-linux-x64-gnu": "1.1.3", - "@rolldown/binding-linux-x64-musl": "1.1.3", - "@rolldown/binding-openharmony-arm64": "1.1.3", - "@rolldown/binding-wasm32-wasi": "1.1.3", - "@rolldown/binding-win32-arm64-msvc": "1.1.3", - "@rolldown/binding-win32-x64-msvc": "1.1.3" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, "node_modules/ruff_wasm": { @@ -7772,9 +7772,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", - "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", "dev": true, "license": "MIT" }, @@ -7999,16 +7999,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.63.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz", - "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.63.0", - "@typescript-eslint/parser": "8.63.0", - "@typescript-eslint/typescript-estree": "8.63.0", - "@typescript-eslint/utils": "8.63.0" + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8092,16 +8092,16 @@ } }, "node_modules/vite": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.16", - "rolldown": "~1.1.3", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "bin": { From a31757509f5aa6d30b144c3c689c3695743c86f7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:46:47 +0200 Subject: [PATCH 133/390] Update actions/setup-python action to v7 (#27299) --- .github/workflows/build-binaries.yml | 16 ++++++++-------- .github/workflows/ci.yaml | 4 ++-- .github/workflows/memory_report.yaml | 2 +- .github/workflows/publish-docs.yml | 2 +- .github/workflows/typing_conformance.yaml | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index be9855cf26..8ecb1fa0cf 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@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" @@ -73,7 +73,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 @@ -120,7 +120,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: arm64 @@ -181,7 +181,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: ${{ matrix.platform.arch }} @@ -235,7 +235,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 @@ -317,7 +317,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" @@ -383,7 +383,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 @@ -448,7 +448,7 @@ jobs: with: submodules: recursive persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: "Prep README.md" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 056d038258..d9b7e3130a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -851,7 +851,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 @@ -990,7 +990,7 @@ jobs: repository: "astral-sh/ruff-lsp" path: ruff-lsp - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: # installation fails on 3.13 and newer python-version: "3.12" diff --git a/.github/workflows/memory_report.yaml b/.github/workflows/memory_report.yaml index 23c39675ed..ebb76a1ae9 100644 --- a/.github/workflows/memory_report.yaml +++ b/.github/workflows/memory_report.yaml @@ -57,7 +57,7 @@ jobs: with: workspaces: "ruff" - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index eb49873b93..47f670b459 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -31,7 +31,7 @@ jobs: ref: ${{ inputs.ref }} persist-credentials: true - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: 3.12 diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml index 99dc57b01e..f694aa1e3d 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -65,7 +65,7 @@ jobs: with: workspaces: "ruff" - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} From 792937e0ea1dfd0a273ef23d055fafd316a5d0ac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 06:48:37 +0000 Subject: [PATCH 134/390] Update Rust crate clap to v4.6.4 (#27274) --- Cargo.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38d9b59b69..f85a5cb5e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,9 +487,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" dependencies = [ "clap_builder", "clap_derive", @@ -497,9 +497,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream 1.0.0", "anstyle", @@ -540,14 +540,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -4254,12 +4254,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] From c071a864558406748a060c06a0b92e868e30eb56 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 06:49:58 +0000 Subject: [PATCH 135/390] Update Rust crate serde to v1.0.229 (#27284) --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f85a5cb5e2..8259e1f6f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3952,9 +3952,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -3973,22 +3973,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] From d03c6a0b0b4b917f3aebed1636d1ec6ffe910855 Mon Sep 17 00:00:00 2001 From: justin Date: Wed, 29 Jul 2026 04:22:30 -0400 Subject: [PATCH 136/390] [ty] Allow mutation of private attributes on frozen Pydantic models (#27257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Pydantic tracking issue: https://github.com/astral-sh/ty/issues/3959 Related issue here on Pydantic PyCharm plugin: https://github.com/koxudaxi/pydantic-pycharm-plugin/issues/1149 Happy to open a separate issue as well, wasn't sure if it would be preferable to just track in the main thread above. This is apparently valid at runtime. The closest thing I could find to docs are [here](https://pydantic.dev/docs/validation/dev/concepts/models/#private-model-attributes): > Attributes whose name has a leading underscore are not treated as fields by Pydantic, and are not included in the model schema. Instead, these are converted into a “private attribute” which is not validated or even set during calls to __init__, model_validate, etc. Repro: https://github.com/thejchap/pydantic-private-ty ```bash uv run main.py && uv run ty check # 1 # error[[invalid-assignment](https://ty.dev/rules#invalid-assignment)]: Property `_a` defined in `A` is read-only # --> main.py:10:1 # | # 10 | a._a = 1 # | ^^^^ # | # Found 1 diagnostic ``` ## Test Plan Added new TODO test --------- Co-authored-by: David Peter --- .../resources/mdtest/external/pydantic.md | 19 ++++++++++++++++++- .../src/types/class/static_literal.rs | 5 ++--- .../src/types/dedicated/pydantic.rs | 12 ++++++++++++ .../infer/builder/attribute_assignment.rs | 12 +++++++++++- 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/external/pydantic.md b/crates/ty_python_semantic/resources/mdtest/external/pydantic.md index 7bafd34307..c93f6f4413 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/pydantic.md +++ b/crates/ty_python_semantic/resources/mdtest/external/pydantic.md @@ -1131,7 +1131,7 @@ There are various ways to make a field immutable. A model can be globally frozen parameter: ```py -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr class PersonFrozenName1(BaseModel, frozen=True): name: str @@ -1178,6 +1178,23 @@ derived = Derived(value=1) derived.value = 2 # error: [invalid-assignment] ``` +Private attributes on models with `frozen=True` can be mutated: + +```py +class FrozenPerson(BaseModel): + model_config = ConfigDict(frozen=True) + + _implicit_private: int + _private_with_default: int = 1 + _explicit_private: int = PrivateAttr(default=0) + +person = FrozenPerson() + +person._implicit_private = 2 +person._private_with_default = 2 +person._explicit_private = 2 +``` + ## Validation of default values At runtime, default values are *not* validated against the field type annotation, unless diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index 2527d9cafe..d81fb85728 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -2444,9 +2444,8 @@ impl<'db> StaticClassLiteral<'db> { }, CodeGeneratorKind::Pydantic(_) => FieldKind::Pydantic { default_ty, - // Pydantic treats underscore-prefixed annotations as private attributes, - // which are instance attributes but never constructor parameters. - init: init && !symbol.name().starts_with('_'), + // Private attributes are instance attributes but never constructor parameters. + init: init && !pydantic::is_private_attribute(symbol.name()), alias, strict, }, diff --git a/crates/ty_python_semantic/src/types/dedicated/pydantic.rs b/crates/ty_python_semantic/src/types/dedicated/pydantic.rs index 4644d4ea4b..258bde82b2 100644 --- a/crates/ty_python_semantic/src/types/dedicated/pydantic.rs +++ b/crates/ty_python_semantic/src/types/dedicated/pydantic.rs @@ -27,6 +27,11 @@ use crate::types::{ }; use crate::{Db, SemanticModel}; +/// Pydantic treats underscore-prefixed annotations as private instance attributes. +pub(in crate::types) fn is_private_attribute(name: &str) -> bool { + name.starts_with('_') +} + /// Metadata that controls Pydantic-specific model synthesis. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub(crate) struct ModelMetadata<'db> { @@ -489,6 +494,13 @@ pub(in crate::types) fn is_model(db: &dyn Db, class: StaticClassLiteral<'_>) -> .any(|base| base.is_known(db, KnownClass::PydanticBaseModel)) } +/// Return whether `ty` is an instance of a Pydantic model. +pub(in crate::types) fn is_model_instance(db: &dyn Db, ty: Type<'_>) -> bool { + ty.nominal_class(db) + .and_then(|class| class.static_class_literal(db)) + .is_some_and(|(class, _)| is_model(db, class)) +} + /// Return whether a field specifier's `default` argument provides a default value. /// /// Pydantic's `Field(...)` uses the ellipsis as a required-field sentinel, so it does not provide diff --git a/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs b/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs index b46e2fa2f6..516d3e321a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs @@ -10,6 +10,7 @@ use crate::types::attribute_write::{ }; use crate::types::call::{Bindings, CallArguments, CallDiagnosticOverride, CallError}; use crate::types::class::FrozenDataclassDispatch; +use crate::types::dedicated::pydantic; use crate::types::diagnostic::{ INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, UNRESOLVED_ATTRIBUTE, report_bad_dunder_set_call, report_invalid_attribute_assignment, report_possibly_missing_attribute, @@ -393,7 +394,16 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { Ok(bindings) => bindings.return_type(db).is_never(), Err(error) => error.return_type(db).is_some_and(|ty| ty.is_never()), }; - if setattr_returns_never { + + // We could also model this more precisely by synthesizing a `__setattr__`overload set + // that only disallows mutation on non-private fields, but for now, we just suppress the + // diagnostic here. This is much easier and faster. + let is_private_pydantic_attribute = + matches!(member, InstanceAttributeWriteMember::Explicit { .. }) + && pydantic::is_private_attribute(self.attribute) + && pydantic::is_model_instance(db, object_ty); + + if setattr_returns_never && !is_private_pydantic_attribute { if emit_diagnostics { let is_setattr_synthesized = !matches!( frozen_dataclass_dispatch, From a2203732fc5f8b7036756ffbaee4ff0f5ddb8f18 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 29 Jul 2026 07:20:31 -0700 Subject: [PATCH 137/390] [ty] Cache protocol receiver binding (#27301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Protocol-member compatibility repeatedly specializes the same callable for the same runtime receiver and typing `Self`, rebuilding signatures and constraint sets on each structural comparison. - Add a private Salsa-tracked protocol receiver-binding cache keyed by the callable, receiver type, and `Self` type. - Route all four existing protocol compatibility binding sites through the cache, including protocol-to-protocol member comparisons. - Preserve distinct receiver and `Self` bindings and reuse `apply_self` when they are identical. This is independently useful on `main` and becomes even more relevant with #27267. ## Performance DateType on clean `main` and this PR, with 40 samples per version: | Version | Estimate | 95% confidence interval | | --- | ---: | ---: | | Baseline | 38.701 ms | 38.336–39.125 ms | | Protocol receiver cache with fixed-point recovery | 33.887 ms | 33.742–34.033 ms | The change is **12.4% faster**, with non-overlapping confidence intervals. ## Test plan - Add a minimized mdtest for a generic protocol whose receiver-specific method and classmethod produce a recursive receiver-binding query; verify that the expected return-type diagnostic is reported instead of a Salsa panic. - Reproduce the exact ecosystem-analyzer configuration for DateType, scipy-stubs, and trio, and verify that all three projects produce byte-for-byte identical diagnostics to `main`. --- .../resources/mdtest/protocols.md | 28 +++++++++++++++ .../src/types/protocol_class.rs | 35 ++++++++++++++++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index ad50e24a50..9d9cb05de8 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -3771,6 +3771,34 @@ static_assert(is_subtype_of(Text, ConsoleRenderable)) static_assert(is_assignable_to(Text, ConsoleRenderable)) ``` +## Recursive protocol receiver binding + +A classmethod on a generic protocol can cause receiver binding for another method to depend on +itself. The cached receiver-binding query must reach a fixed point and report the ordinary +return-type error instead of panicking. + +```toml +[environment] +python-version = "3.11" +``` + +```py +from __future__ import annotations + +from datetime import datetime, timedelta, tzinfo +from typing import ClassVar, Optional, Protocol, TypeVar + +T = TypeVar("T", bound=Optional[tzinfo], covariant=True) + +class DateTime(Protocol[T]): + resolution: ClassVar[timedelta] + + def __sub__(self: DateTime[tzinfo], other: DateTime[tzinfo]) -> timedelta: ... + @classmethod + def now(cls, tz: Optional[tzinfo] = None) -> DateTime[Optional[tzinfo]]: + return datetime.now(tz) # error: [invalid-return-type] +``` + ## Subtyping of protocols with generic method members Protocol method members can be generic. They can have generic contexts scoped to the class: diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 6ffefd7d6b..948d8c628c 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -2355,14 +2355,16 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { self.check_callables_vs_callable( db, &callables.map(|callable| { - callable.apply_self_with_receiver( + protocol_apply_self_with_receiver( db, + callable, implementation_receiver_binding_ty, implementation_self_binding_ty, ) }), - required_callable.apply_self_with_receiver( + protocol_apply_self_with_receiver( db, + required_callable, protocol_receiver_binding_ty, protocol_self_binding_ty, ), @@ -2404,8 +2406,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { self.check_type_pair( db, attribute_type, - Type::Callable(required_callable.apply_self_with_receiver( + Type::Callable(protocol_apply_self_with_receiver( db, + required_callable, protocol_receiver_binding_ty, protocol_self_binding_ty, )), @@ -2678,7 +2681,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { if member.is_method() && let Type::Callable(callable) = member_type.ty() { - Some(Type::Callable(callable.apply_self(db, source_type))) + Some(Type::Callable(protocol_apply_self_with_receiver( + db, + callable, + source_type, + source_type, + ))) } else { member_type.bind_self(db, source_type) } @@ -3086,6 +3094,25 @@ fn protocol_bind_self<'db>( callable.bind_self(db, self_type).into_regular(db) } +/// Cache receiver and `Self` binding only for protocol-member compatibility checks. +#[salsa::tracked( + returns(copy), + cycle_initial=|db, _, _, _, _| CallableType::bottom(db), + heap_size=ruff_memory_usage::heap_size +)] +fn protocol_apply_self_with_receiver<'db>( + db: &'db dyn Db, + callable: CallableType<'db>, + receiver_type: Type<'db>, + self_type: Type<'db>, +) -> CallableType<'db> { + if receiver_type == self_type { + callable.apply_self(db, self_type) + } else { + callable.apply_self_with_receiver(db, receiver_type, self_type) + } +} + /// Return `true` if a callable has at least one overload and none return `Never`. /// /// Return-type disjointness is a pragmatic approximation for method members: a callable returning From 08d91681485d18c9c0791952e7400d976fb3be6f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 29 Jul 2026 10:21:18 -0400 Subject: [PATCH 138/390] [ty] Correct ParamSpec forwarded-argument diagnostic locations (#27263) ## Summary Prior to this change, we checked forwarded `ParamSpec` arguments against the callback but reported their parameter locations on the forwarding function. For example, this incorrectly highlighted `asyncio.to_thread`'s `func` parameter instead of the callback's `value` parameter: ```py import asyncio def callback(*, value: int) -> None: ... async def run() -> None: await asyncio.to_thread(callback, value="incorrect") ``` We now point to the callback parameter for functions, bound methods, overloads, `Concatenate`, and callable unions. When that parameter cannot be identified reliably, we fall back to the forwarding function's actual `*args` or `**kwargs` parameter rather than highlighting an unrelated parameter or overload. Closes https://github.com/astral-sh/ty/issues/2198. --- .../paramspec_subcall_error_location.md | 451 +++++++++++++++++- .../ty_python_semantic/src/types/call/bind.rs | 255 +++++++++- 2 files changed, 679 insertions(+), 27 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md b/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md index 284d97e487..d04d2c93ae 100644 --- a/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md +++ b/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md @@ -1,9 +1,9 @@ # `ParamSpec` error locations -When a free `ParamSpec` is available in a parameter before the ones representing it's components -(`P.args` and `P.kwargs`), ty invokes a sub-call logic where it performs a separate call to the -function with the arguments that are resolved from the `ParamSpec`. In this case, the diagnostic -location need to be offset based on the position of the `ParamSpec` components. +A callable can accept another callable and forward its positional and keyword arguments using a +`ParamSpec`. These tests check that argument errors identify the callback parameter that rejected +the argument, or fall back to the forwarding function's `*args` or `**kwargs` when that parameter +cannot be identified. ```toml [environment] @@ -31,10 +31,10 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect 9 | foo(fn1, "a", 2, c="c", unknown=1) | ^^^ Expected `int`, found `Literal["a"]` info: Function defined here - --> src/mdtest_snippet.py:3:5 + --> src/mdtest_snippet.py:4:5 | -3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... - | ^^^ ------------------ Parameter declared here +4 | def fn1(a: int, b: int, c: int) -> None: ... + | ^^^ ------ Parameter declared here error[invalid-argument-type]: Argument to function `foo` is incorrect @@ -43,10 +43,10 @@ error[invalid-argument-type]: Argument to function `foo` is incorrect 9 | foo(fn1, "a", 2, c="c", unknown=1) | ^^^^^ Expected `int`, found `Literal["c"]` info: Function defined here - --> src/mdtest_snippet.py:3:5 + --> src/mdtest_snippet.py:4:5 | -3 | def foo[**P, T](fn: Callable[P, T], *args: P.args, **kwargs: P.kwargs): ... - | ^^^ ------------------ Parameter declared here +4 | def fn1(a: int, b: int, c: int) -> None: ... + | ^^^ ------ Parameter declared here error[unknown-argument]: Argument `unknown` does not match any known parameter of function `foo` @@ -164,3 +164,434 @@ foo = Foo() # error: [unknown-argument] foo.method(fn1, "a", 2, c="c", unknown=1) ``` + +## Forwarded keyword arguments + +A forwarded keyword argument should identify the matching callback parameter, not the parameter that +accepts the callback. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback(*, value: int) -> None: ... + +wrapper(callback, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:6:19 + | +6 | wrapper(callback, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:4:5 + | +4 | def callback(*, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Forwarded bound methods + +A bound method still includes `self` in its source signature. The diagnostic should skip that +parameter and identify the argument that actually failed. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Handler: + def callback(self, *, value: int) -> None: ... + +def run(handler: Handler) -> None: + wrapper(handler.callback, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:9:31 + | +9 | wrapper(handler.callback, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:6:9 + | +6 | def callback(self, *, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Callbacks without a source definition + +A `Callable` annotation describes the accepted arguments but does not identify the function that +declared them. In that case, point to the forwarding function's `*args` parameter. The expanded +keyword parameters below cover the corresponding `**kwargs` fallback. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def run(callback: Callable[[int], None]) -> None: + wrapper(callback, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:5:23 + | +5 | wrapper(callback, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:3:5 + | +3 | def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + | ^^^^^^^ ------------- Parameter declared here +``` + +## Parameters consumed by Concatenate + +`Concatenate` lets a forwarding function provide the first argument itself. The diagnostic still +needs to account for that argument when locating the callback's remaining parameter. + +```py +from typing import Callable, Concatenate + +def wrapper[**P](callback: Callable[Concatenate[int, P], None], *args: P.args, **kwargs: P.kwargs) -> None: + callback(0, *args, **kwargs) + +def callback(prefix: int, *, value: int) -> None: ... + +wrapper(callback, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:8:19 + | +8 | wrapper(callback, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:6:5 + | +6 | def callback(prefix: int, *, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Overloaded callbacks + +When a callback has multiple overloads, the diagnostic should identify the parameter on the overload +that accepted the other arguments. + +```py +from typing import Callable, overload + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +@overload +def callback(value: int) -> None: ... +@overload +def callback(value: str, *, flag: str) -> None: ... +def callback(value: int | str, *, flag: str | None = None) -> None: ... + +wrapper(callback, "value", flag=1) # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:10:28 + | +10 | wrapper(callback, "value", flag=1) # snapshot: invalid-argument-type + | ^^^^^^ Expected `str`, found `Literal[1]` +info: Function defined here + --> src/mdtest_snippet.py:7:5 + | +7 | def callback(value: str, *, flag: str) -> None: ... + | ^^^^^^^^ --------- Parameter declared here +``` + +## Overloads selected by Concatenate + +The first callback overload accepts a `str` prefix, so it cannot match a forwarding function that +always supplies an `int`. An error in the remaining arguments should point to the second overload. + +```py +from typing import Callable, Concatenate, overload + +def wrapper[**P](callback: Callable[Concatenate[int, P], None], *args: P.args, **kwargs: P.kwargs) -> None: + callback(1, *args, **kwargs) + +@overload +def callback(prefix: str, value: str) -> None: ... +@overload +def callback(prefix: int, value: int) -> None: ... +def callback(prefix: str | int, value: str | int) -> None: ... + +wrapper(callback, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:12:19 + | +12 | wrapper(callback, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:9:5 + | +9 | def callback(prefix: int, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Overloads selected by a bound receiver + +A generic method can have separate overloads for different receiver types. A method on +`Receiver[int]` should point to the overload declared for `Receiver[int]`. + +```py +from typing import Callable, overload + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Receiver[T]: + value: T + + @overload + def method(self: "Receiver[str]", value: str) -> None: ... + @overload + def method(self: "Receiver[int]", value: int) -> None: ... + def method(self, value: str | int) -> None: ... + +def run(receiver: Receiver[int]) -> None: + wrapper(receiver.method, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:15:30 + | +15 | wrapper(receiver.method, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:11:9 + | +11 | def method(self: "Receiver[int]", value: int) -> None: ... + | ^^^^^^ ---------- Parameter declared here +``` + +## Callback annotations with multiple callable alternatives + +The callback below matches the second union alternative, which does not consume a leading argument. +The diagnostic should therefore identify `first`, not `second`. + +```py +from typing import Callable, Concatenate + +def wrapper[**P](callback: Callable[Concatenate[int, P], None] | Callable[P, str], *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback(first: int, second: str) -> str: + return second + +wrapper(callback, "incorrect", "valid") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:7:19 + | +7 | wrapper(callback, "incorrect", "valid") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:4:5 + | +4 | def callback(first: int, second: str) -> str: + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Optional callbacks + +A union may also contain a value that is not callable. The presence of `None` should not prevent the +diagnostic from identifying the callback's parameter. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, None] | None, *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback(value: int) -> None: ... + +wrapper(callback, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:6:19 + | +6 | wrapper(callback, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:4:5 + | +4 | def callback(value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Overloaded forwarding functions + +An overloaded forwarding function should retain the note identifying its matching overload as well +as the note identifying the callback parameter. + +```py +from typing import Callable, overload + +@overload +def wrap[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +@overload +def wrap(value: int, first: int, second: int) -> None: ... +def wrap(callback: Callable[..., None] | int, *args: object, **kwargs: object) -> None: ... +def keyword_callback(*, value: int) -> None: ... +def positional_callback(*values: int) -> None: ... +``` + +A keyword argument belongs to the forwarding function's `**kwargs` parameter. + +```py +wrap(keyword_callback, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrap` is incorrect + --> src/mdtest_snippet.py:10:24 + | +10 | wrap(keyword_callback, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:8:5 + | +8 | def keyword_callback(*, value: int) -> None: ... + | ^^^^^^^^^^^^^^^^ ---------- Parameter declared here +info: Matching overload defined here + --> src/mdtest_snippet.py:4:5 + | +4 | def wrap[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + | ^^^^ ------------------ Parameter declared here +info: Non-matching overloads for function `wrap`: +info: (value: int, first: int, second: int) -> None +``` + +A positional argument belongs to `*args`, even when the callback accepts it through `*values`. + +```py +wrap(positional_callback, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrap` is incorrect + --> src/mdtest_snippet.py:11:27 + | +11 | wrap(positional_callback, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:9:5 + | +9 | def positional_callback(*values: int) -> None: ... + | ^^^^^^^^^^^^^^^^^^^ ------------ Parameter declared here +info: Matching overload defined here + --> src/mdtest_snippet.py:4:5 + | +4 | def wrap[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + | ^^^^ ------------- Parameter declared here +info: Non-matching overloads for function `wrap`: +info: (value: int, first: int, second: int) -> None +``` + +## Forwarding through a callable object + +The forwarding object's own `self` parameter is not the callback. The diagnostic should point to the +argument accepted by `callback`. + +```py +from typing import Callable + +class Wrapper: + def __call__[**P](self, callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + +def callback(value: int) -> None: ... + +Wrapper()(callback, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to bound method `Wrapper.__call__` is incorrect + --> src/mdtest_snippet.py:8:21 + | +8 | Wrapper()(callback, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:6:5 + | +6 | def callback(value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Overloads without parameter definitions + +Expanding `Unpack[tuple[int]]` can remove the information linking an overload's parameter back to +its declaration. Until that link is restored, point to the forwarding function's `*args` rather than +an unrelated overload. + +```py +from typing import Callable, Concatenate, Unpack, overload + +def wrapper[**P](callback: Callable[Concatenate[int, P], None], *args: P.args, **kwargs: P.kwargs) -> None: ... +@overload +def callback(prefix: str, *values: Unpack[tuple[str]]) -> None: ... +@overload +def callback(prefix: int, *values: Unpack[tuple[int]]) -> None: ... +def callback(prefix: str | int, *values: Unpack[tuple[str | int]]) -> None: ... + +wrapper(callback, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:10:19 + | +10 | wrapper(callback, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:3:5 + | +3 | def wrapper[**P](callback: Callable[Concatenate[int, P], None], *args: P.args, **kwargs: P.kwargs) -> None: ... + | ^^^^^^^ ------------- Parameter declared here +``` + +## Expanded keyword parameters + +`Unpack[Config]` creates separate keyword parameters for `alpha` and `beta`, even though the +callback declares only `**options`. Without a reliable link to that declaration, point to the +forwarding function's `**kwargs` parameter. + +```py +from typing import Callable, TypedDict, Unpack + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Config(TypedDict): + alpha: int + beta: int + +def callback(**options: Unpack[Config]) -> None: ... + +wrapper(callback, alpha=1, beta="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:11:28 + | +11 | wrapper(callback, alpha=1, beta="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:3:5 + | +3 | def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + | ^^^^^^^ ------------------ Parameter declared here +``` diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 6524162ad8..4b612967d3 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -5175,6 +5175,81 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .collect() } + /// Returns the source callable whose parameters supplied a `ParamSpec` specialization. + /// + /// Forwarded arguments are checked against the wrapped callable, not the wrapper's + /// `Callable[P, R]` parameter. Retaining that callable also lets diagnostics account for bound + /// receivers and leading parameters consumed by `Concatenate`. + /// + /// Return `None` when the callback has no source declaration, so diagnostics can fall back to + /// the forwarding function's actual variadic parameter instead of an unrelated parameter. + /// + /// ```python + /// from typing import Callable + /// + /// def wrapper[**P](fn: Callable[P, None], *args: P.args, **kwargs: P.kwargs): ... + /// def target(*, value: int) -> None: ... + /// wrapper(target, value="bad") # The parameter source is `target`. + /// ``` + fn paramspec_parameter_source( + &self, + paramspec: BoundTypeVarInstance<'db>, + overload_index: usize, + ) -> Option> { + self.enumerate_argument_types() + .find_map(|(argument_index, _, argument, argument_types)| { + if matches!(argument, Argument::Synthetic) { + return None; + } + + self.argument_matches[argument_index] + .iter() + .find_map(|matched_parameter| { + let declared_type = + self.signature.parameters()[matched_parameter.index].annotated_type(); + let argument_type = argument_types.get_for_declared_type(declared_type); + let paramspec_prefix_len = |candidate: Type<'db>| { + candidate + .try_upcast_to_callable(self.db)? + .iter() + .find_map(|callable| { + callable.signatures(self.db).iter().find_map(|signature| { + let (prefix, declared_paramspec) = + signature.parameters().as_paramspec_with_prefix()?; + (declared_paramspec == paramspec).then_some(prefix.len()) + }) + }) + }; + let prefix_len = if let Type::Union(union) = + declared_type.resolve_type_alias(self.db) + { + union.elements(self.db).iter().find_map(|candidate| { + let specialized_candidate = candidate + .apply_optional_specialization(self.db, self.specialization()); + argument_type + .is_assignable_to(self.db, specialized_candidate) + .then_some(*candidate) + .and_then(paramspec_prefix_len) + }) + } else { + paramspec_prefix_len(declared_type) + }?; + let (function, is_bound_method) = match argument_type { + Type::FunctionLiteral(function) => (function, false), + Type::BoundMethod(method) => (method.function(self.db), true), + _ => return None, + }; + + Some(ForwardedParameterSource { + function, + is_bound_method, + parameter_index_offset: prefix_len + usize::from(is_bound_method), + overload_index, + }) + }) + }) + } + fn specialization(&self) -> Option> { self.inference .map(|inference| inference.specialization(self.db)) @@ -5619,6 +5694,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { expected_ty, provided_ty: argument_type, provenance: matched_parameter.provenance, + parameter_source: None, }); } // We still update the actual type of the parameter in this binding to match the argument, @@ -5876,7 +5952,6 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { }; let error_argument_indices = error_argument_indices.as_deref(); - // Create Bindings with all overloads and perform full overload resolution let callable_binding = CallableBinding::from_overloads(self.signature_type, signatures.iter().cloned()); let bindings = match Bindings::from(callable_binding) @@ -5897,13 +5972,49 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .single_element() .expect("ParamSpec sub-call should only contain a single CallableBinding"); - let mut extend_errors = |errors: &[BindingError<'db>]| { - self.errors.extend( - errors - .iter() - .cloned() - .map(|err| err.maybe_remap_argument_indices(error_argument_indices)), - ); + let mut extend_errors = |binding: &Binding<'db>| { + let parameter_source = binding + .errors + .iter() + .find(|error| matches!(error, BindingError::InvalidArgumentType { .. })) + .and_then(|_| { + self.paramspec_parameter_source(paramspec, binding.source_overload_index()) + }) + .and_then(|source| { + let overload_index = + source.source_overload_index(self.db, &binding.signature)?; + Some(ForwardedParameterSource { + overload_index, + ..source + }) + }); + let argument_matches = self.argument_matches; + + self.errors + .extend(binding.errors.iter().cloned().map(|mut error| { + if let BindingError::InvalidArgumentType { + parameter, + argument_index, + parameter_source: error_parameter_source, + .. + } = &mut error + && error_parameter_source.is_none() + { + if let Some(parameter_source) = parameter_source + && parameter_source.contains_parameter(self.db, parameter.index) + { + *error_parameter_source = Some(parameter_source); + } else if let Some(parameter_index) = argument_index + .and_then(|index| paramspec_arguments?.get(index)) + .and_then(|(index, _)| argument_matches[*index].parameters.first()) + .map(|parameter| parameter.index) + { + parameter.index = parameter_index; + } + } + + error.maybe_remap_argument_indices(error_argument_indices) + })); }; let mut matching_overloads = callable_binding.matching_overloads(); @@ -5912,7 +6023,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { if let [binding] = callable_binding.overloads() { // This is not an overloaded function, so we can propagate its errors to the // outer bindings. - extend_errors(&binding.errors); + extend_errors(binding); } else { let index = callable_binding .best_failing_overload_index( @@ -5921,20 +6032,20 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .unwrap_or(0); // TODO: We should also update the specialization for the `ParamSpec` to reflect // the matching overload here. - extend_errors(&callable_binding.overloads()[index].errors); + extend_errors(&callable_binding.overloads()[index]); } } (Some((_, binding)), None) => { // TODO: We should also update the specialization for the `ParamSpec` to reflect the // matching overload here. - extend_errors(&binding.errors); + extend_errors(binding); } (Some(_), Some(_)) => { if !matches!( callable_binding.overload_call_return_type, Some(OverloadCallReturnType::ArgumentTypeExpansion(_)) ) { - extend_errors(&callable_binding.overloads()[0].errors); + extend_errors(&callable_binding.overloads()[0]); } } } @@ -7382,6 +7493,69 @@ impl std::fmt::Display for ParameterContexts { } } +/// The function and source offsets used to locate a forwarded `ParamSpec` parameter. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct ForwardedParameterSource<'db> { + function: FunctionType<'db>, + is_bound_method: bool, + parameter_index_offset: usize, + overload_index: usize, +} + +impl<'db> ForwardedParameterSource<'db> { + /// Recovers an overload's original index after specialization filters earlier declarations. + /// + /// `overload_index` initially refers to the specialized overload list. This method finds the + /// corresponding position in the original function, which can differ after filtering. + fn source_overload_index(self, db: &'db dyn Db, signature: &Signature<'db>) -> Option { + let parameter_definition = signature + .parameters() + .iter() + .find_map(Parameter::definition)?; + + self.function + .signature(db) + .overloads + .iter() + .position(|source_signature| { + source_signature + .parameters() + .iter() + .any(|parameter| parameter.definition() == Some(parameter_definition)) + }) + } + + /// Returns whether the forwarded parameter maps to a specific source parameter. + /// + /// Out-of-range indices otherwise resolve to the entire signature, which would produce a + /// misleading diagnostic annotation. + fn contains_parameter(self, db: &'db dyn Db, parameter_index: usize) -> bool { + let parameter_index = parameter_index + self.parameter_index_offset; + let (overloads, implementation) = self.function.overloads_and_implementation(db); + let Some(overload) = overloads + .get(self.overload_index) + .copied() + .or(implementation) + else { + return false; + }; + + let (_, parameter_span) = overload.parameter_span(db, Some(parameter_index)); + let (_, all_parameters_span) = overload.parameter_span(db, None); + parameter_span != all_parameters_span + } + + /// Locates the matched source overload after restoring omitted receiver and prefix parameters. + fn parameter_span(self, db: &'db dyn Db, parameter_index: usize) -> (Span, Span) { + let parameter_index = parameter_index + self.parameter_index_offset; + let (overloads, _) = self.function.overloads_and_implementation(db); + overloads + .get(self.overload_index) + .map(|overload| overload.parameter_span(db, Some(parameter_index))) + .unwrap_or_else(|| self.function.parameter_span(db, Some(parameter_index))) + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum InvalidArgumentTypeProvenance { Argument, @@ -7410,6 +7584,8 @@ pub(crate) enum BindingError<'db> { expected_ty: Type<'db>, provided_ty: Type<'db>, provenance: InvalidArgumentTypeProvenance, + /// The callable that actually declared the parameter, when reached through a `ParamSpec`. + parameter_source: Option>, }, /// The type of the keyword-variadic argument's key is not `str`. InvalidKeyType { @@ -7687,6 +7863,7 @@ impl<'db> BindingError<'db> { expected_ty, provided_ty, provenance, + parameter_source, } => { // TODO: Ideally we would not emit diagnostics for `TypedDict` literal arguments // here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have @@ -7730,15 +7907,57 @@ impl<'db> BindingError<'db> { provided_ty.assignability_error_context(context.db(), *expected_ty); error_context.attach_to(context.db(), &mut diag); + if let Some(parameter_source) = parameter_source { + let (name_span, parameter_span) = + parameter_source.parameter_span(context.db(), parameter.index); + let callable_kind = if parameter_source.is_bound_method { + "Method" + } else { + "Function" + }; + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!("{callable_kind} defined here"), + ); + sub.annotate(Annotation::primary(name_span)); + sub.annotate( + Annotation::secondary(parameter_span).message("Parameter declared here"), + ); + diag.sub(sub); + } + if let Some(matching_overload) = matching_overload { if let Some(overload_literal) = matching_overload.get(context.db()) { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, "Matching overload defined here", ); + let parameter_index = if parameter_source.is_some() { + let argument_is_positional = Self::get_argument_node( + node, + argument_index.map(|index| index + context.argument_index_offset), + ) + .map_or(parameter.positional, |argument| { + matches!(argument, ArgOrKeyword::Arg(_)) + }); + overload_literal + .signature(context.db()) + .parameters() + .iter() + .position(|candidate| { + if argument_is_positional { + candidate.is_variadic() + } else { + candidate.is_keyword_variadic() + } + }) + .unwrap_or(parameter.index) + } else { + parameter.index + }; let (name_span, parameter_span) = overload_literal.parameter_span( context.db(), - Some(parameter.index + source_parameter_index_offset), + Some(parameter_index + source_parameter_index_offset), ); sub.annotate(Annotation::primary(name_span)); sub.annotate( @@ -7770,10 +7989,12 @@ impl<'db> BindingError<'db> { )); } } - } else if let Some((name_span, parameter_span)) = callable_ty.parameter_span( - context.db(), - Some(parameter.index + source_parameter_index_offset), - ) { + } else if parameter_source.is_none() + && let Some((name_span, parameter_span)) = callable_ty.parameter_span( + context.db(), + Some(parameter.index + source_parameter_index_offset), + ) + { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!("{callable_kind} defined here"), From 2f0c1acecf293e80bec605b49e1f89859dc763d7 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 29 Jul 2026 10:21:19 -0400 Subject: [PATCH 139/390] [ty] Recover forwarded callable object and constructor sources (#27264) ## Summary When a `ParamSpec` forwarding function receives a callable object, callback protocol, or class, we currently fall back to the forwarding function's `*args` or `**kwargs` parameter even though the underlying callable has a source definition: ```py import asyncio class Callback: def __call__(self, *, value: int) -> None: ... class Factory: def __init__(self, value: int) -> None: ... async def run() -> None: await asyncio.to_thread(Callback(), value="incorrect") await asyncio.to_thread(Factory, "incorrect") ``` We now reuse the existing callable-object and constructor bindings to highlight the appropriate `__call__`, metaclass `__call__`, `__init__`, or `__new__` parameter. This preserves the active constructor stage, the selected overload, and receivers such as `self` and `cls`. --- .../paramspec_subcall_error_location.md | 185 ++++++++++++++++++ .../ty_python_semantic/src/types/call/bind.rs | 14 +- 2 files changed, 197 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md b/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md index d04d2c93ae..792c0b1cc7 100644 --- a/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md +++ b/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md @@ -595,3 +595,188 @@ info: Function defined here 3 | def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... | ^^^^^^^ ------------------ Parameter declared here ``` + +## Callback protocols + +Unlike a plain `Callable` annotation, a callback protocol includes a declaration for `__call__`. The +diagnostic should point to the parameter on that method. + +```py +from typing import Callable, Protocol + +def wrapper[**P](callback: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Callback(Protocol): + def __call__(self, *, value: int) -> None: ... + +def run(callback: Callback) -> None: + wrapper(callback, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:9:23 + | +9 | wrapper(callback, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:6:9 + | +6 | def __call__(self, *, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Callable objects + +A callable object declares its accepted arguments on `__call__`. Point to that method when the +object is passed to a forwarding function. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Callback: + def __call__(self, *, value: int) -> None: ... + +wrapper(Callback(), value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:8:21 + | +8 | wrapper(Callback(), value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:6:9 + | +6 | def __call__(self, *, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Constructors defined by __init__ + +A class passed as the callback receives the forwarded arguments in its constructor. A class that +declares `__init__` should identify the matching constructor parameter. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Factory: + def __init__(self, value: int) -> None: ... + +wrapper(Factory, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:8:18 + | +8 | wrapper(Factory, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:6:9 + | +6 | def __init__(self, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Constructors defined by a metaclass + +A custom metaclass can determine the accepted constructor arguments through its own `__call__`. That +declaration takes precedence over the class's `__init__` method. + +```py +from typing import Callable + +def wrapper[**P](callback: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Meta(type): + def __call__(cls, value: int) -> object: + return object() + +class Factory(metaclass=Meta): + def __init__(self, value: str) -> None: ... + +wrapper(Factory, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:12:18 + | +12 | wrapper(Factory, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:6:9 + | +6 | def __call__(cls, value: int) -> object: + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Constructors defined by __new__ + +A constructor defined by `__new__` consumes `cls` before checking the forwarded arguments. The +diagnostic should point to `value`, not `cls`. + +```py +from typing import Callable, Self + +def wrapper[**P](callback: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Factory: + def __new__(cls, value: int) -> Self: + return super().__new__(cls) + +wrapper(Factory, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:9:18 + | +9 | wrapper(Factory, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:6:9 + | +6 | def __new__(cls, value: int) -> Self: + | ^^^^^^^ ---------- Parameter declared here +``` + +## Overloaded constructors defined by __new__ + +When `__new__` is overloaded, the diagnostic must both select the matching overload and account for +its `cls` parameter. + +```py +from typing import Callable, Self, overload + +def wrapper[**P](callback: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Factory: + @overload + def __new__(cls, value: str) -> Self: ... + @overload + def __new__(cls, value: int, flag: int) -> Self: ... + def __new__(cls, value: str | int, flag: int | None = None) -> Self: + return super().__new__(cls) + +wrapper(Factory, 1, "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:13:21 + | +13 | wrapper(Factory, 1, "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:9:9 + | +9 | def __new__(cls, value: int, flag: int) -> Self: ... + | ^^^^^^^ --------- Parameter declared here +``` diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 4b612967d3..abf7937af6 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -5183,6 +5183,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { /// /// Return `None` when the callback has no source declaration, so diagnostics can fall back to /// the forwarding function's actual variadic parameter instead of an unrelated parameter. + /// Callable objects and constructors use their existing bindings to preserve the active + /// constructor stage and any consumed receiver. /// /// ```python /// from typing import Callable @@ -5234,16 +5236,24 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } else { paramspec_prefix_len(declared_type) }?; - let (function, is_bound_method) = match argument_type { + let argument_bindings = argument_type.bindings(self.db); + let callable = argument_bindings.single_item()?.callable(); + let (function, is_bound_method) = match callable.signature_type { Type::FunctionLiteral(function) => (function, false), Type::BoundMethod(method) => (method.function(self.db), true), _ => return None, }; + let source_parameter_index_offset = callable + .overloads() + .get(overload_index) + .or_else(|| callable.overloads().first()) + .map_or(0, |binding| binding.source_parameter_index_offset) + + usize::from(callable.bound_type.is_some()); Some(ForwardedParameterSource { function, is_bound_method, - parameter_index_offset: prefix_len + usize::from(is_bound_method), + parameter_index_offset: prefix_len + source_parameter_index_offset, overload_index, }) }) From 03e0a2bcd4dd17023193533a2603d85fe9f06ac7 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 29 Jul 2026 10:21:19 -0400 Subject: [PATCH 140/390] [ty] Recover forwarded functools.partial diagnostic sources (#27265) ## Summary Prior to this change, forwarding a `functools.partial` caused us to point at the forwarding function's `*args` parameter instead of the remaining parameter on the original callable: ```py import asyncio from functools import partial def callback(prefix: int, value: int) -> None: ... async def run() -> None: await asyncio.to_thread(partial(callback, 1), "incorrect") ``` We now recover the function or bound method wrapped by `partial` and use its reduced signature to account for arguments that were already supplied. --- .../paramspec_subcall_error_location.md | 59 +++++++++++++++++++ .../ty_python_semantic/src/types/call/bind.rs | 38 +++++++++++- 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md b/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md index 792c0b1cc7..d724aa9418 100644 --- a/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md +++ b/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md @@ -780,3 +780,62 @@ info: Function defined here 9 | def __new__(cls, value: int, flag: int) -> Self: ... | ^^^^^^^ --------- Parameter declared here ``` + +## Functions wrapped by functools.partial + +`functools.partial` supplies the first argument before the callback is passed to the forwarding +function. An invalid forwarded argument should point to the next parameter on the original function. + +```py +from functools import partial +from typing import Callable + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback(prefix: int, value: int) -> None: ... + +wrapper(partial(callback, 1), "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:7:31 + | +7 | wrapper(partial(callback, 1), "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:5:5 + | +5 | def callback(prefix: int, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` + +## Bound methods wrapped by functools.partial + +When `functools.partial` wraps a bound method, both `self` and the argument supplied by `partial` +come before the forwarded argument. + +```py +from functools import partial +from typing import Callable + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Handler: + def callback(self, prefix: int, value: int) -> None: ... + +def run(handler: Handler) -> None: + wrapper(partial(handler.callback, 1), "incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:10:43 + | +10 | wrapper(partial(handler.callback, 1), "incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:7:9 + | +7 | def callback(self, prefix: int, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +``` diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index abf7937af6..48eb314fd7 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -5185,6 +5185,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { /// the forwarding function's actual variadic parameter instead of an unrelated parameter. /// Callable objects and constructors use their existing bindings to preserve the active /// constructor stage and any consumed receiver. + /// A `functools.partial` also requires accounting for arguments that were supplied before the + /// remaining signature was forwarded. /// /// ```python /// from typing import Callable @@ -5236,19 +5238,49 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } else { paramspec_prefix_len(declared_type) }?; - let argument_bindings = argument_type.bindings(self.db); + let (source_type, partial_signature) = match argument_type { + Type::KnownInstance( + KnownInstanceType::FunctoolsPartial(partial) + | KnownInstanceType::FunctoolsPartialCall(partial), + ) => ( + partial.wrapped(self.db).inner(self.db), + partial + .partial(self.db) + .signatures(self.db) + .overloads + .get(overload_index), + ), + _ => (argument_type, None), + }; + let argument_bindings = source_type.bindings(self.db); let callable = argument_bindings.single_item()?.callable(); let (function, is_bound_method) = match callable.signature_type { Type::FunctionLiteral(function) => (function, false), Type::BoundMethod(method) => (method.function(self.db), true), _ => return None, }; - let source_parameter_index_offset = callable + let source_binding = callable .overloads() .get(overload_index) - .or_else(|| callable.overloads().first()) + .or_else(|| callable.overloads().first()); + let source_parameter_index_offset = source_binding .map_or(0, |binding| binding.source_parameter_index_offset) + usize::from(callable.bound_type.is_some()); + let source_parameter_index_offset = + partial_signature + .zip(source_binding) + .and_then(|(partial_signature, source_binding)| { + let definition = partial_signature + .parameters() + .iter() + .find_map(Parameter::definition)?; + source_binding.signature.parameters().iter().position( + |parameter| parameter.definition() == Some(definition), + ) + }) + .map_or(source_parameter_index_offset, |index| { + index.max(source_parameter_index_offset) + }); Some(ForwardedParameterSource { function, From c6eea387ed6f7e2f9592db55b4475d84a386fd30 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 29 Jul 2026 07:48:16 -0700 Subject: [PATCH 141/390] [ty] Lazily materialize protocol attributes (#27267) ## Summary Support materialization of class-based protocols via lazy materialization of attribute types. - Represent a materialized protocol as its original protocol class plus its `Top` or `Bottom` polarity, materializing individual read and write requirements only when they are observed. - Preserve recursive protocols, nominal inheritance, descriptors, properties, class variables, and class-object member access without eagerly rebuilding protocol interfaces. - Propagate materialized member requirements through structural relations, generic inference, bounds, constraints, invariance, and overload resolution. - Infer constructors using the materialized receiver while preserving explicit `__new__` return types, custom metaclass returns, and mixed `Self`/non-instance overloads. - Fix `dataclasses.is_dataclass` narrowing. ## Test plan - Add mdtests for top and bottom reads and writes, properties, descriptors, class variables, class-object access, and nominal versus structural protocol relationships. - Cover recursive protocols, recursive structural inference, inherited and structural generic inference, bounds, constraints, invariant arguments, overload selection, generic aliases, `Self`, legacy type variables, and generator delegation. - Cover constructors that return protocol instances, non-instance values, custom metaclass values, and mixed overloaded return types. - Verify `dataclasses.is_dataclass` narrows an impossible branch to `Never`. ### Ecosystem All added/removed diagnostics are favorable. Some diagnostics change to reflect top-materialized protocols explicitly. Fixes astral-sh/ty#3443. Co-authored-by: Charlie Marsh --- .../resources/mdtest/async.md | 8 +- .../mdtest/dataclasses/dataclasses.md | 17 + .../mdtest/type_properties/materialization.md | 1011 +++++++++++++++++ .../resources/mdtest/type_qualifiers/final.md | 26 + crates/ty_python_semantic/src/types.rs | 106 +- .../src/types/attribute_write.rs | 13 +- .../src/types/bound_super.rs | 16 +- .../ty_python_semantic/src/types/callable.rs | 13 +- crates/ty_python_semantic/src/types/class.rs | 75 +- crates/ty_python_semantic/src/types/cyclic.rs | 8 +- .../src/types/diagnostic.rs | 11 +- .../ty_python_semantic/src/types/display.rs | 37 +- .../ty_python_semantic/src/types/generics.rs | 40 +- .../src/types/ide_support.rs | 2 +- .../src/types/infer/builder.rs | 7 +- .../infer/builder/attribute_assignment.rs | 5 + .../types/infer/builder/final_attribute.rs | 10 +- .../ty_python_semantic/src/types/instance.rs | 448 ++++++-- .../src/types/list_members.rs | 2 +- .../src/types/protocol_class.rs | 609 +++++++--- .../src/types/set_theoretic.rs | 2 +- .../src/types/subclass_of.rs | 2 +- .../ty_python_semantic/src/types/visitor.rs | 2 +- 23 files changed, 2076 insertions(+), 394 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/async.md b/crates/ty_python_semantic/resources/mdtest/async.md index f8171110f0..09b858714b 100644 --- a/crates/ty_python_semantic/resources/mdtest/async.md +++ b/crates/ty_python_semantic/resources/mdtest/async.md @@ -150,7 +150,7 @@ def get_any() -> Any: async def test(): x = get_any() if inspect.isawaitable(x): - reveal_type(x) # revealed: Any & Awaitable[object] + reveal_type(x) # revealed: Any & Top[Awaitable[object]] y = await x reveal_type(y) # revealed: Any ``` @@ -239,10 +239,10 @@ def is_async_callable(x: object) -> TypeIs[Top[Callable[..., Awaitable[object]]] async def f(fn: Callable[[int], int | Awaitable[int]]) -> None: if is_async_callable(fn): - reveal_type(fn) # revealed: ((int, /) -> int | Awaitable[int]) & Top[(...) -> Awaitable[object]] + reveal_type(fn) # revealed: ((int, /) -> int | Awaitable[int]) & Top[(...) -> Top[Awaitable[object]]] result = fn(1) - # This includes `int & Awaitable[object]`: an `int` subtype could define `__await__`. - reveal_type(result) # revealed: (int & Awaitable[object]) | Awaitable[int] + # This includes `int & Top[Awaitable[object]]`: an `int` subtype could define `__await__`. + reveal_type(result) # revealed: (int & Top[Awaitable[object]]) | Awaitable[int] reveal_type(await result) # revealed: object ``` diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 9e31b72786..ed90a0a7fe 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -2220,6 +2220,23 @@ But calling `asdict` on the class object is not allowed: asdict(Foo) ``` +## `dataclasses.is_dataclass` + +`is_dataclass` recognizes both dataclass instances and dataclass classes. A concrete dataclass +instance always satisfies the `DataclassInstance` protocol, so the negative branch is unreachable: + +```py +from dataclasses import dataclass, is_dataclass + +@dataclass +class Event: + x: int + +def check(event: Event) -> None: + if not is_dataclass(event): + reveal_type(event) # revealed: Never +``` + ## `dataclasses.KW_ONLY` If an attribute is annotated with `dataclasses.KW_ONLY`, it is not added to the synthesized diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md index b4e4e99f48..03406f2865 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -1001,3 +1001,1014 @@ def _(top: Top[FunctionHolder[Any]], bottom: Bottom[FunctionHolder[Any]]) -> Non # revealed: (def shared(self, value: Never) -> object, /) -> def shared(self, value: object) -> Never reveal_type(bottom.nested) ``` + +## Protocols + +Materializing a protocol maps each member according to how it is used. Reads are covariant and +writes are contravariant. + +```toml +[environment] +python-version = "3.12" +``` + +### Instance attributes + +For a mutable `Any` attribute, `Top` reads `object` and writes `Never`; `Bottom` does the reverse: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class MutableAny(Protocol): + value: Any + +def mutable_top_attributes(top: Top[MutableAny]) -> None: + reveal_type(top) # revealed: Top[MutableAny] + reveal_type(top.value) # revealed: object + top.value = 1 # error: [invalid-assignment] + +def mutable_bottom_attributes(bottom: Bottom[MutableAny]) -> None: + reveal_type(bottom) # revealed: Bottom[MutableAny] + bottom.value = object() + reveal_type(bottom.value) # revealed: Never +``` + +The class object of a materialized protocol preserves its instance type when called directly or +passed through a generic callable: + +```py +from typing import Callable + +def invoke[T](factory: Callable[[], T]) -> T: + return factory() + +def constructors(top: Top[MutableAny]) -> None: + reveal_type(type(top)) # revealed: type[Top[MutableAny]] + reveal_type(type(top)()) # revealed: Top[MutableAny] + reveal_type(invoke(type(top))) # revealed: Top[MutableAny] + +def annotated_constructors(top: type[Top[MutableAny]], bottom: type[Bottom[MutableAny]]) -> None: + reveal_type(top) # revealed: type[Top[MutableAny]] + reveal_type(bottom) # revealed: type[Bottom[MutableAny]] + reveal_type(top()) # revealed: Top[MutableAny] + reveal_type(bottom()) # revealed: Bottom[MutableAny] + reveal_type(invoke(top)) # revealed: Top[MutableAny] + reveal_type(invoke(bottom)) # revealed: Bottom[MutableAny] +``` + +A protocol's constructor can explicitly return a value that is not an instance of the protocol. +Materializing the protocol must preserve that return type, including when its class object is +converted to a callable: + +```py +class IntConstructor(Protocol): + value: Any + + def __new__(cls) -> int: + return 1 + +def non_instance_constructors( + plain: type[IntConstructor], + top: type[Top[IntConstructor]], + bottom: type[Bottom[IntConstructor]], +) -> None: + reveal_type(invoke(plain)) # revealed: int + reveal_type(invoke(top)) # revealed: int + reveal_type(invoke(bottom)) # revealed: int +``` + +A custom protocol metaclass can likewise construct a value that is not a protocol instance. Its +`__call__` return type is preserved when the protocol is materialized. + +```py +class IntConstructorMetaclass(type(Protocol)): + def __call__(cls) -> int: + return 1 + +class MetaclassConstructor(Protocol, metaclass=IntConstructorMetaclass): + value: Any + +def metaclass_constructors( + plain: type[MetaclassConstructor], + top: type[Top[MetaclassConstructor]], + bottom: type[Bottom[MetaclassConstructor]], +) -> None: + reveal_type(invoke(plain)) # revealed: int + reveal_type(invoke(top)) # revealed: int + reveal_type(invoke(bottom)) # revealed: int +``` + +Overloaded constructors preserve each return type separately: an instance-returning overload uses +the materialized protocol, while an overload returning a different type retains that type. + +```py +from typing import Self, overload + +class MixedConstructor(Protocol): + value: Any + + @overload + def __new__(cls) -> Self: ... + @overload + def __new__(cls, value: int) -> int: ... + def __new__(cls, value: int | None = None) -> Self | int: + raise NotImplementedError + +def invoke_with_int[T](factory: Callable[[int], T]) -> T: + return factory(1) + +def mixed_constructors( + top: type[Top[MixedConstructor]], + bottom: type[Bottom[MixedConstructor]], +) -> None: + reveal_type(invoke(top)) # revealed: Top[MixedConstructor] + reveal_type(invoke(bottom)) # revealed: Bottom[MixedConstructor] + reveal_type(invoke_with_int(top)) # revealed: int + reveal_type(invoke_with_int(bottom)) # revealed: int +``` + +Materialization preserves sound class-member access: an ordinary instance attribute is not available +on `type[Top[MutableAny]]` or `type[Bottom[MutableAny]]`. + +```py +def class_instance_attributes(top: Top[MutableAny], bottom: Bottom[MutableAny]) -> None: + type(top).value # error: [unresolved-attribute] + type(bottom).value # error: [unresolved-attribute] + +def annotated_class_instance_attributes(top: type[Top[MutableAny]], bottom: type[Bottom[MutableAny]]) -> None: + top.value # error: [unresolved-attribute] + bottom.value # error: [unresolved-attribute] +``` + +### Writable properties + +A property setter is already a write, so its parameter is mapped only once: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class WritableAny(Protocol): + @property + def value(self) -> Any: ... + @value.setter + def value(self, value: Any) -> None: ... + +def writable_top_property(top: Top[WritableAny]) -> None: + reveal_type(top.value) # revealed: object + top.value = 1 # error: [invalid-assignment] + +def writable_bottom_property(bottom: Bottom[WritableAny]) -> None: + bottom.value = object() + reveal_type(bottom.value) # revealed: Never +``` + +### Protocol relations + +`MutableAny` and `Top[MutableAny]` refer to the same protocol class, but they do not have the same +read and write requirements. Subtyping and union simplification must use those requirements: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_subtype_of + +class MutableAny(Protocol): + value: Any + +static_assert(is_subtype_of(Bottom[MutableAny], MutableAny)) +static_assert(is_subtype_of(Bottom[MutableAny], Top[MutableAny])) +static_assert(is_subtype_of(MutableAny, Top[MutableAny])) +static_assert(not is_subtype_of(MutableAny, Bottom[MutableAny])) +static_assert(not is_subtype_of(Top[MutableAny], Bottom[MutableAny])) +static_assert(not is_subtype_of(Top[MutableAny], MutableAny)) + +def union_order( + plain_first: MutableAny | Top[MutableAny], + top_first: Top[MutableAny] | MutableAny, +) -> None: + reveal_type(plain_first) # revealed: Top[MutableAny] + reveal_type(top_first) # revealed: Top[MutableAny] + reveal_type(plain_first.value) # revealed: object + reveal_type(top_first.value) # revealed: object +``` + +Inheriting from a protocol must not bypass its materialized write requirement. A nominal subclass +and a structurally identical class therefore have the same result here: + +```py +class MutableAnySubclass(MutableAny): + value: int + +class StructuralMutableAny: + value: int + +static_assert(not is_subtype_of(MutableAnySubclass, Bottom[MutableAny])) +static_assert(not is_subtype_of(StructuralMutableAny, Bottom[MutableAny])) +``` + +An inherited `Any` member is materialized along with members declared directly on the protocol, so +it cannot satisfy a more specific inherited protocol: + +```py +class GenericBase[T](Protocol): + item: T + +class InheritedAny(GenericBase[Any], Protocol): + marker: Any + +def requires_int_base(value: GenericBase[int]) -> None: ... +def _(top: Top[InheritedAny]) -> None: + requires_int_base(top) # error: [invalid-argument-type] +``` + +Materializing an unrelated member does not erase explicit protocol inheritance, even when an +override is structurally incompatible with the base protocol. Materializing the fully static base +also preserves the nominal relationship: + +```py +class BaseProtocol(Protocol): + @property + def value(self) -> int: ... + +class ChildProtocol(BaseProtocol, Protocol): + marker: Any + + @property + def value(self) -> str: ... + +static_assert(is_subtype_of(Top[ChildProtocol], BaseProtocol)) +static_assert(is_subtype_of(ChildProtocol, Top[BaseProtocol])) +static_assert(is_subtype_of(ChildProtocol, Bottom[BaseProtocol])) +``` + +A covariant `Awaitable[int]` satisfies the top-materialized `Awaitable[object]` protocol. Narrowing +to that protocol must therefore preserve `Awaitable[int]` without retaining a redundant +intersection: + +```py +from typing import Awaitable +from typing_extensions import TypeIs + +static_assert(is_subtype_of(Awaitable[int], Top[Awaitable[object]])) + +def is_top_awaitable(value: object) -> TypeIs[Top[Awaitable[object]]]: + return True + +def narrow_awaitable(value: Awaitable[int]) -> None: + if is_top_awaitable(value): + reveal_type(value) # revealed: Awaitable[int] +``` + +### Class variables + +Class variables have separate read and write types. `Top` reads `object` and writes `Never`, while +`Bottom` reads `Never` and writes `object`. These requirements are preserved on both inferred and +explicitly annotated class objects: + +```py +from typing import Any, ClassVar, Protocol +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_subtype_of + +class ClassVarAny(Protocol): + value: ClassVar[Any] + +def class_writes(top: Top[ClassVarAny], bottom: Bottom[ClassVarAny]) -> None: + type(top).value = 1 # error: [invalid-assignment] + type(bottom).value = object() + +def class_reads(top: Top[ClassVarAny], bottom: Bottom[ClassVarAny]) -> None: + reveal_type(type(top).value) # revealed: object + reveal_type(type(bottom).value) # revealed: Never + +def annotated_class_writes(top: type[Top[ClassVarAny]], bottom: type[Bottom[ClassVarAny]]) -> None: + reveal_type(top) # revealed: type[Top[ClassVarAny]] + reveal_type(bottom) # revealed: type[Bottom[ClassVarAny]] + top.value = 1 # error: [invalid-assignment] + bottom.value = object() + +def annotated_class_reads(top: type[Top[ClassVarAny]], bottom: type[Bottom[ClassVarAny]]) -> None: + reveal_type(top.value) # revealed: object + reveal_type(bottom.value) # revealed: Never +``` + +Structural protocol checks use the mapped read and write types as well. `ClassVarInt` satisfies the +top-materialized protocol, but not the bottom-materialized one; a class missing the class variable +does not satisfy the top-materialized protocol: + +```py +class ClassVarInt: + value: ClassVar[int] = 1 + +class MissingClassVar: ... + +static_assert(is_subtype_of(ClassVarInt, Top[ClassVarAny])) +static_assert(not is_subtype_of(ClassVarInt, Bottom[ClassVarAny])) +top_class: type[Top[ClassVarAny]] = ClassVarInt +missing_top_class: type[Top[ClassVarAny]] = MissingClassVar # error: [invalid-assignment] +invalid_bottom_class: type[Bottom[ClassVarAny]] = ClassVarInt # error: [invalid-assignment] + +def materialized_bottom_class(bottom: Bottom[ClassVarAny]) -> None: + valid_bottom_class: type[Bottom[ClassVarAny]] = type(bottom) + reveal_type(valid_bottom_class) # revealed: type[Bottom[ClassVarAny]] +``` + +Union simplification preserves the materialized class variable regardless of operand order: + +```py +def class_union_order( + plain: ClassVarAny, + top: Top[ClassVarAny], + flag: bool, +) -> None: + plain_first = type(plain) if flag else type(top) + top_first = type(top) if flag else type(plain) + reveal_type(plain_first.value) # revealed: object + reveal_type(top_first.value) # revealed: object +``` + +### Methods through the class object + +Ordinary, static, and class methods use their materialized signatures when accessed through the +class object. Ordinary methods remain unbound: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class DecoratedAny(Protocol): + def transform(self, value: Any) -> Any: ... + @staticmethod + def parse(value: Any) -> Any: ... + @classmethod + def create(cls, value: Any) -> Any: ... + +def decorated_class_access( + top: Top[DecoratedAny], + bottom: Bottom[DecoratedAny], +) -> None: + reveal_type(type(top).transform) # revealed: (self, /, value: Never) -> object + reveal_type(type(top).parse) # revealed: (value: Never) -> object + reveal_type(type(top).create) # revealed: (value: Never) -> object + reveal_type(type(bottom).transform) # revealed: (self, /, value: object) -> Never + reveal_type(type(bottom).parse) # revealed: (value: object) -> Never + reveal_type(type(bottom).create) # revealed: (value: object) -> Never +``` + +### Members outside the protocol interface + +`__init__` is not a protocol requirement, but accessing it on a materialized value still uses the +declaration on the protocol class: + +```py +from typing import Any, Protocol +from ty_extensions import Top + +class ProtocolWithInit(Protocol): + value: Any + + def __init__(self, value: int) -> None: ... + +def constructor(top: Top[ProtocolWithInit]) -> None: + reveal_type(top.__init__) # revealed: bound method Top[ProtocolWithInit].__init__(value: int) -> None +``` + +### Read-only property deletion + +Materializing a read-only property must not make it deletable: + +```py +from typing import Any, Protocol +from typing_extensions import TypeIs +from ty_extensions import Top + +class ReadOnlyProperty(Protocol): + @property + def property(self) -> Any: ... + +def is_read_only_property(value: object) -> TypeIs[Top[ReadOnlyProperty]]: + return True + +def property_deletion( + top: Top[ReadOnlyProperty], + value: object, +) -> None: + del top.property # error: [invalid-assignment] + if is_read_only_property(value): + del value.property # error: [invalid-assignment] +``` + +### Descriptor-decorated properties + +A descriptor can expose separate read and write types. `Top` maps an `Any` read to `object` and an +`Any` write to `Never`; `Bottom` maps them in the opposite direction: + +```py +from typing import Any, Callable, Never, Protocol +from typing_extensions import TypeIs +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_subtype_of + +class Descriptor: + def __get__(self, instance: object, owner: type[object] | None = None) -> Any: ... + def __set__(self, instance: object, value: Any) -> None: ... + +def descriptor(function: Callable[..., Any]) -> Descriptor: + raise NotImplementedError + +class DescriptorProperty(Protocol): + @descriptor + def value(self) -> Any: ... + +class TopDescriptorProperty: + @property + def value(self) -> object: + return object() + + @value.setter + def value(self, value: Never) -> None: ... + +class NarrowBottomDescriptorProperty: + @property + def value(self) -> Never: + raise RuntimeError + + @value.setter + def value(self, value: int) -> None: ... + +static_assert(is_subtype_of(TopDescriptorProperty, Top[DescriptorProperty])) +static_assert(not is_subtype_of(NarrowBottomDescriptorProperty, Bottom[DescriptorProperty])) + +def top_descriptor_write(top: Top[DescriptorProperty]) -> None: + top.value = 1 # error: [invalid-assignment] + +def bottom_descriptor_write(bottom: Bottom[DescriptorProperty]) -> None: + bottom.value = object() + +def plain_descriptor_write(plain: DescriptorProperty) -> None: + plain.value = object() + +def is_descriptor_property(value: object) -> TypeIs[Top[DescriptorProperty]]: + return True + +def narrowed_descriptor_write(value: object) -> None: + if is_descriptor_property(value): + reveal_type(value) # revealed: Top[DescriptorProperty] + value.value = 1 # error: [invalid-assignment] +``` + +### Property accessor types + +Materializing a property with fully static exposed types is a no-op. The accessor's implicit +receiver and the setter's return type do not contribute to the property requirement: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class FullyStaticProperty(Protocol): + @property + def value(self) -> int: ... + @value.setter + def value(self, value: int) -> Any: ... + +def fully_static_property( + top: Top[FullyStaticProperty], + bottom: Bottom[FullyStaticProperty], +) -> None: + reveal_type(top) # revealed: FullyStaticProperty + reveal_type(bottom) # revealed: FullyStaticProperty +``` + +### Assignment narrowing of materialized properties + +A materialized protocol exposes a property's return type, not the underlying descriptor, when +reading that property. Assignment narrowing must still recover the descriptor: its setter can +transform the assigned value, so the next read must not narrow to the assigned literal. + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class TransformingProperty(Protocol): + marker: Any + + @property + def value(self) -> int: ... + @value.setter + def value(self, value: int) -> None: ... + +def materialized_property_assignment_narrowing( + top: Top[TransformingProperty], + bottom: Bottom[TransformingProperty], +) -> None: + top.value = 1 + reveal_type(top.value) # revealed: int + bottom.value = 2 + reveal_type(bottom.value) # revealed: int +``` + +### Generic inference through inherited and structural protocols + +Generic inference uses a member's materialized type, not its original `Any`. This applies both to +inherited members and to the finite requirements of independently declared structural protocols. +Bounds, constraints, and invariant requirements must still reject an incompatible materialized +member instead of accepting an invalid call or selecting the wrong overload: + +```py +from typing import Any, Literal, Protocol, TypeVar, overload +from ty_extensions import Top + +class InferenceBase[T](Protocol): + @property + def item(self) -> T: ... + +class InheritedInferenceAny(InferenceBase[Any], Protocol): + marker: Any + +class StructuralInferenceAny(Protocol): + @property + def item(self) -> Any: ... + +def infer_item[T](value: InferenceBase[T]) -> T: + raise NotImplementedError + +def materialized_inference(inherited: Top[InheritedInferenceAny]) -> None: + reveal_type(infer_item(inherited)) # revealed: object + +def materialized_structural_inference(structural: Top[StructuralInferenceAny]) -> None: + reveal_type(infer_item(structural)) # revealed: object + +def bounded_item[T: str](value: InferenceBase[T]) -> T: + raise NotImplementedError + +def union_bounded_item[T: str | bytes](value: InferenceBase[T]) -> T: + raise NotImplementedError + +def constrained_item[T: (str, bytes)](value: InferenceBase[T]) -> T: + raise NotImplementedError + +LegacyConstrained = TypeVar("LegacyConstrained", str, bytes) + +def legacy_constrained_item(value: InferenceBase[LegacyConstrained]) -> LegacyConstrained: + raise NotImplementedError + +def invalid_materialized_bounds( + inherited: Top[InheritedInferenceAny], + structural: Top[StructuralInferenceAny], +) -> None: + bounded_item(inherited) # error: [invalid-argument-type] + bounded_item(structural) # error: [invalid-argument-type] + union_bounded_item(inherited) # error: [invalid-argument-type] + union_bounded_item(structural) # error: [invalid-argument-type] + constrained_item(inherited) # error: [invalid-argument-type] + constrained_item(structural) # error: [invalid-argument-type] + legacy_constrained_item(inherited) # error: [invalid-argument-type] + legacy_constrained_item(structural) # error: [invalid-argument-type] + +def consistent_item[T](value: InferenceBase[T], values: list[T]) -> T: + raise NotImplementedError + +def invalid_materialized_invariant_arguments( + inherited: Top[InheritedInferenceAny], + structural: Top[StructuralInferenceAny], + values: list[int], +) -> None: + consistent_item(inherited, values) # error: [invalid-argument-type] + consistent_item(structural, values) # error: [invalid-argument-type] + +class InvariantInferenceBase[T](Protocol): + item: T + +class InheritedInvariantAny(InvariantInferenceBase[Any], Protocol): + marker: Any + +class StructuralInvariantAny(Protocol): + item: Any + +def invariant_item[T](value: InvariantInferenceBase[T], required: T) -> T: + raise NotImplementedError + +def invalid_materialized_invariant_members( + inherited: Top[InheritedInvariantAny], + structural: Top[StructuralInvariantAny], +) -> None: + invariant_item(inherited, "required") # error: [invalid-argument-type] + invariant_item(structural, "required") # error: [invalid-argument-type] + +@overload +def select_item[T: str](value: InferenceBase[T]) -> Literal["bounded"]: ... +@overload +def select_item(value: object) -> Literal["fallback"]: ... +def select_item(value: object) -> Literal["bounded", "fallback"]: + return "fallback" + +@overload +def select_specific_item(value: InferenceBase[str]) -> Literal["str"]: ... +@overload +def select_specific_item(value: InferenceBase[bytes]) -> Literal["bytes"]: ... +def select_specific_item(value: object) -> str: + raise NotImplementedError + +def materialized_overload_resolution( + inherited: Top[InheritedInferenceAny], + structural: Top[StructuralInferenceAny], + valid: InferenceBase[str], +) -> None: + reveal_type(select_item(inherited)) # revealed: Literal["fallback"] + reveal_type(select_item(structural)) # revealed: Literal["fallback"] + reveal_type(select_item(valid)) # revealed: Literal["bounded"] + select_specific_item(inherited) # error: [no-matching-overload] + select_specific_item(structural) # error: [no-matching-overload] + +@overload +def select_consistent_item[T](value: InferenceBase[T], values: list[T]) -> T: ... +@overload +def select_consistent_item(value: object, values: list[int]) -> object: ... +def select_consistent_item(value: object, values: object) -> object: + raise NotImplementedError + +def materialized_invariant_overload_resolution( + inherited: Top[InheritedInferenceAny], + structural: Top[StructuralInferenceAny], + valid: InferenceBase[int], + values: list[int], +) -> None: + reveal_type(select_consistent_item(inherited, values)) # revealed: object + reveal_type(select_consistent_item(structural, values)) # revealed: object + reveal_type(select_consistent_item(valid, values)) # revealed: int +``` + +### Generic inference through recursive structural protocols + +A recursive protocol requirement must not cause inference to discard a structurally matching +protocol's materialization. The nonrecursive property establishes the correct specialization without +expanding the recursive property. + +```py +from __future__ import annotations + +from typing import Any, Literal, Protocol, overload +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_subtype_of + +class RecursiveValue[T](Protocol): + @property + def value(self) -> T: ... + @property + def child(self) -> RecursiveValue[T]: ... + +class RecursiveAny(Protocol): + @property + def value(self) -> Any: ... + @property + def child(self) -> RecursiveAny: ... + +static_assert(is_subtype_of(Top[RecursiveAny], RecursiveValue[object])) +static_assert(not is_subtype_of(Top[RecursiveAny], RecursiveValue[str])) +static_assert(is_subtype_of(Bottom[RecursiveAny], RecursiveValue[str])) +``` + +Inference preserves both materialization polarities: the top-materialized property infers `object`, +while the bottom-materialized property infers `Never`. + +```py +def infer_recursive_value[T](value: RecursiveValue[T]) -> T: + raise NotImplementedError + +def recursive_materialized_inference( + top: Top[RecursiveAny], + bottom: Bottom[RecursiveAny], + valid: RecursiveValue[str], +) -> None: + reveal_type(top.value) # revealed: object + reveal_type(infer_recursive_value(top)) # revealed: object + reveal_type(infer_recursive_value(valid)) # revealed: str + reveal_type(infer_recursive_value(bottom)) # revealed: Never +``` + +The nonrecursive property is used only to infer the specialization. The complete protocol must still +be checked, so a matching `value` cannot hide an incompatible `child`. + +```py +class WrongRecursiveAny(Protocol): + @property + def value(self) -> Any: ... + @property + def child(self) -> int: ... + +static_assert(not is_subtype_of(Top[WrongRecursiveAny], RecursiveValue[object])) + +def reject_incompatible_recursive_child(wrong: Top[WrongRecursiveAny]) -> None: + infer_recursive_value(wrong) # error: [invalid-argument-type] +``` + +A top-materialized `object` cannot satisfy a `str` bound or a `str`/`bytes` constraint. An ordinary +`RecursiveValue[str]` still satisfies both. + +```py +def bounded_recursive_value[T: str](value: RecursiveValue[T]) -> T: + raise NotImplementedError + +def constrained_recursive_value[T: (str, bytes)](value: RecursiveValue[T]) -> T: + raise NotImplementedError + +def recursive_materialized_bounds( + top: Top[RecursiveAny], + valid: RecursiveValue[str], +) -> None: + bounded_recursive_value(top) # error: [invalid-argument-type] + constrained_recursive_value(top) # error: [invalid-argument-type] + reveal_type(bounded_recursive_value(valid)) # revealed: str + reveal_type(constrained_recursive_value(valid)) # revealed: str +``` + +An invariant `list[T]` cannot narrow the materialized `object` property to `int`. + +```py +def infer_recursive_with_list[T](value: RecursiveValue[T], values: list[T]) -> T: + raise NotImplementedError + +def recursive_materialized_invariant_arguments( + top: Top[RecursiveAny], + valid: RecursiveValue[str], + ints: list[int], + strings: list[str], +) -> None: + infer_recursive_with_list(top, ints) # error: [invalid-argument-type] + reveal_type(infer_recursive_with_list(valid, strings)) # revealed: str +``` + +Overload resolution also respects the bound and the complete recursive requirement. Both an +incompatible materialized property and an incompatible child select the fallback or fail when no +fallback is available; the valid `str` specialization selects the bounded overload. + +```py +@overload +def select_recursive_value[T: str](value: RecursiveValue[T]) -> Literal["bounded"]: ... +@overload +def select_recursive_value(value: object) -> Literal["fallback"]: ... +def select_recursive_value(value: object) -> Literal["bounded", "fallback"]: + return "fallback" + +@overload +def select_specific_recursive_value(value: RecursiveValue[str]) -> Literal["str"]: ... +@overload +def select_specific_recursive_value(value: RecursiveValue[bytes]) -> Literal["bytes"]: ... +def select_specific_recursive_value(value: object) -> str: + raise NotImplementedError + +def recursive_materialized_overload_resolution( + top: Top[RecursiveAny], + wrong: Top[WrongRecursiveAny], + valid: RecursiveValue[str], +) -> None: + reveal_type(select_recursive_value(top)) # revealed: Literal["fallback"] + reveal_type(select_recursive_value(wrong)) # revealed: Literal["fallback"] + reveal_type(select_recursive_value(valid)) # revealed: Literal["bounded"] + select_specific_recursive_value(top) # error: [no-matching-overload] + select_specific_recursive_value(wrong) # error: [no-matching-overload] + reveal_type(select_specific_recursive_value(valid)) # revealed: Literal["str"] +``` + +### Generator delegation + +`yield from` uses the same materialized yield and return types as direct generator methods. Applying +another materialization must not change a result that no longer contains `Any`: + +```py +from collections.abc import Generator +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class MaterializedGenerator(Generator[Any, Any, Any], Protocol): + marker: Any + +def generator_delegation( + generator: Top[MaterializedGenerator], + nested: Bottom[Top[MaterializedGenerator]], +): + reveal_type(generator.__next__()) # revealed: object + result = yield from generator + reveal_type(result) # revealed: object + nested_result = yield from nested + reveal_type(nested_result) # revealed: object +``` + +The send type is contravariant. A top-materialized generator cannot accept values sent by a +`Generator[object, object, object]`, while a bottom-materialized generator can: + +```py +def top_generator_send( + generator: Top[MaterializedGenerator], +) -> Generator[object, object, object]: + result = yield from generator # error: [invalid-yield] + return result + +def bottom_generator_send( + generator: Bottom[MaterializedGenerator], +) -> Generator[object, object, object]: + result = yield from generator + return result +``` + +### `Self` binding + +`Self` may appear in `Top[GenericProtocol[Self]]` even when the protocol member itself is `Any`. It +must still bind to the class through which the attribute is accessed: + +```py +from typing import Any, Protocol, Self +from ty_extensions import Top + +class GenericProtocol[T](Protocol): + value: Any + +class SelfContainer: + member: Top[GenericProtocol[Self]] + +class SelfContainerChild(SelfContainer): + pass + +reveal_type(SelfContainerChild().member) # revealed: Top[GenericProtocol[SelfContainerChild]] +``` + +### Legacy type variables + +A legacy type variable in the protocol's type arguments still makes the enclosing function generic: + +```py +from typing import Any, Protocol, TypeVar +from ty_extensions import Top + +T = TypeVar("T") + +class LegacyProtocol(Protocol[T]): + value: Any + +def accepts_legacy(value: Top[LegacyProtocol[T]]) -> None: ... + +reveal_type(accepts_legacy) # revealed: def accepts_legacy[T](value: Top[LegacyProtocol[T]]) -> None +``` + +### Generic aliases + +Expanding a generic alias preserves the materialized write type: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class GenericMutable[T](Protocol): + value: T + +type MutableAlias[T] = GenericMutable[T] + +def alias_writes( + top: Top[MutableAlias[Any]], + bottom: Bottom[MutableAlias[Any]], +) -> None: + top.value = 1 # error: [invalid-assignment] + bottom.value = object() + +def annotated_generic_protocol_classes( + top: type[Top[GenericMutable[Any]]], + bottom: type[Bottom[GenericMutable[Any]]], + aliased_top: type[Top[MutableAlias[Any]]], + aliased_bottom: type[Bottom[MutableAlias[Any]]], +) -> None: + reveal_type(top) # revealed: type[Top[GenericMutable[Any]]] + reveal_type(bottom) # revealed: type[Bottom[GenericMutable[Any]]] + reveal_type(aliased_top) # revealed: type[Top[GenericMutable[Any]]] + reveal_type(aliased_bottom) # revealed: type[Bottom[GenericMutable[Any]]] +``` + +### Nested generic protocols + +A protocol nested inside another generic type preserves its separate read and write requirements +after materialization: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class Leaf[T](Protocol): + value: T + +class Outer[T](Protocol): + leaf: Leaf[T] + +class ReadHolder[T]: + @property + def outer(self) -> Outer[T]: + raise NotImplementedError + +def nested_specialization( + holder: Top[ReadHolder[Any]], + top_leaf: Top[Leaf[Any]], + bottom_leaf: Bottom[Leaf[Any]], +) -> None: + reveal_type(holder.outer) # revealed: Top[Outer[Any]] + holder.outer.leaf = bottom_leaf + holder.outer.leaf = top_leaf # error: [invalid-assignment] +``` + +### Class-backed protocol specialization during interface construction + +An ordinary specialization of a class-backed protocol only maps its class specialization. It must +not inspect the protocol interface, because the specialization can occur while that same interface +is being constructed: + +```py +from __future__ import annotations + +from typing import Generic, Protocol, TypeVar, overload + +S = TypeVar("S") +T = TypeVar("T") + +class Unit(Protocol): + def __mul__(self, other: S | Quantity[S]): ... + +class Vector(Protocol): ... + +class Quantity(Generic[T], Protocol): + @overload + def __mul__(self, other: Unit | Quantity[S]): ... + @overload + def __mul__(self, other: Vector) -> Vector: ... +``` + +### Recursive protocols + +Materializing a recursive protocol preserves its wrapper without eagerly expanding its recursive +interface. Nonrecursive members are still materialized, and following the recursive child preserves +both the protocol and its materialization polarity. + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_equivalent_to + +type RecursiveAlias = RecursiveProtocol + +class RecursiveProtocol(Protocol): + marker: Any + + @property + def child(self) -> RecursiveAlias: ... + +static_assert(is_equivalent_to(Top[RecursiveProtocol], Top[Top[RecursiveProtocol]])) +static_assert(is_equivalent_to(Bottom[RecursiveProtocol], Bottom[Bottom[RecursiveProtocol]])) +static_assert(is_equivalent_to(Top[RecursiveProtocol], Bottom[Top[RecursiveProtocol]])) +static_assert(is_equivalent_to(Bottom[RecursiveProtocol], Top[Bottom[RecursiveProtocol]])) + +def recursive_top_materialization(top: Top[RecursiveProtocol]) -> None: + reveal_type(top) # revealed: Top[RecursiveProtocol] + reveal_type(top.marker) # revealed: object + top.marker = 1 # error: [invalid-assignment] + + reveal_type(top.child) # revealed: Top[RecursiveProtocol] + reveal_type(top.child.child) # revealed: Top[RecursiveProtocol] + reveal_type(top.child.marker) # revealed: object + top.child.marker = 1 # error: [invalid-assignment] + +def recursive_bottom_children(bottom: Bottom[RecursiveProtocol]) -> None: + reveal_type(bottom) # revealed: Bottom[RecursiveProtocol] + reveal_type(bottom.child) # revealed: Bottom[RecursiveProtocol] + reveal_type(bottom.child.child) # revealed: Bottom[RecursiveProtocol] + bottom.child.marker = object() + reveal_type(bottom.child.marker) # revealed: Never + +def recursive_bottom_marker(bottom: Bottom[RecursiveProtocol]) -> None: + bottom.marker = object() + reveal_type(bottom.marker) # revealed: Never + +def recursive_nested_materialization( + nested_top: Top[Top[RecursiveProtocol]], + nested_bottom: Bottom[Bottom[RecursiveProtocol]], +) -> None: + reveal_type(nested_top) # revealed: Top[RecursiveProtocol] + reveal_type(nested_top.marker) # revealed: object + reveal_type(nested_bottom) # revealed: Bottom[RecursiveProtocol] + reveal_type(nested_bottom.marker) # revealed: Never +``` + +### Display + +Materialized protocols display `Top` and `Bottom` around the protocol class: + +```py +from typing import Any, Protocol +from ty_extensions import Bottom, Top + +class ReadAny(Protocol): + @property + def value(self) -> Any: ... + +def _(top: Top[ReadAny], bottom: Bottom[ReadAny]) -> None: + reveal_type(top) # revealed: Top[ReadAny] + reveal_type(bottom) # revealed: Bottom[ReadAny] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md index 6c004c6660..61c646efdd 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -842,6 +842,32 @@ def bar(x: Foo, value: int): x.value = value ``` +### Protocol members initialized in `__init__` + +A protocol may initialize its own `Final` member in `__init__`, even if another method specializes +the protocol's `self` type. That specialization must not make the initializer appear to belong to a +different class. Assignments to another instance or outside the initializer remain invalid. + +```py +from __future__ import annotations + +from typing import Final, Protocol, TypeVar + +T = TypeVar("T", covariant=True) + +class Owned(Protocol[T]): + owner: Final[T] + + def __init__(self, owner: T, other: Owned[T] | None = None) -> None: + self.owner = owner + if other is not None: + other.owner = owner # error: [invalid-assignment] + + def progress(self: Owned[int]) -> None: ... + def replace(self, owner: T) -> None: + self.owner = owner # error: [invalid-assignment] +``` + ### Explicit `Final` redeclaration Explicit `Final` redeclaration in the same scope is accepted (shadowing). diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 7e1e2fcd30..18c56a96d3 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1112,6 +1112,20 @@ struct GeneratorTypes<'db> { return_ty: Option>, } +impl<'db> GeneratorTypes<'db> { + /// Apply a generator's materialization with the variance of each operation. + fn materialize(self, db: &'db dyn Db, kind: MaterializationKind) -> Self { + let visitor = ApplyTypeMappingVisitor::default(); + Self { + yield_ty: self.yield_ty.map(|ty| ty.materialize(db, kind, &visitor)), + send_ty: self + .send_ty + .map(|ty| ty.materialize(db, kind.flip(), &visitor)), + return_ty: self.return_ty.map(|ty| ty.materialize(db, kind, &visitor)), + } + } +} + fn object_type_form(db: &dyn Db) -> Type<'_> { TypeFormType::from_type_expression(db, Type::object()) } @@ -1363,9 +1377,9 @@ impl<'db> Type<'db> { .any(|ty| ty.is_specialized_generic(db)) } Type::NominalInstance(instance_type) => instance_type.is_definition_generic(db), - Type::ProtocolInstance(protocol) => { - matches!(protocol.inner, Protocol::FromClass(class) if class.is_generic()) - } + Type::ProtocolInstance(protocol) => protocol + .class_origin(db) + .is_some_and(|class| class.is_generic()), Type::TypedDict(typed_dict) => typed_dict .defining_class() .is_some_and(ClassType::is_generic), @@ -1474,7 +1488,7 @@ impl<'db> Type<'db> { pub(crate) fn nominal_class(self, db: &'db dyn Db) -> Option> { match self { Type::NominalInstance(instance) => Some(instance.class(db)), - Type::ProtocolInstance(instance) => instance.to_nominal_instance().map(|i| i.class(db)), + Type::ProtocolInstance(instance) => instance.class_origin(db).map(|class| *class), Type::TypeAlias(alias) => alias.value_type(db).nominal_class(db), Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).nominal_class(db), Type::TypeVar(typevar) => { @@ -2061,7 +2075,7 @@ impl<'db> Type<'db> { Type::TypedDict(td) => td.defining_class().is_some(), - Type::ProtocolInstance(ProtocolInstanceType { inner, .. }) => !inner.is_synthesized(), + Type::ProtocolInstance(protocol) => protocol.class_origin(db).is_some(), Type::Dynamic(dynamic) => match dynamic { DynamicType::Any => true, @@ -2702,6 +2716,16 @@ impl<'db> Type<'db> { if let Some(fallback) = ty.materialized_divergent_fallback() { return fallback.class_member_with_policy(db, name, policy); } + if let Type::ProtocolInstance(protocol) = ty + && let Some(origin) = protocol.materialized_origin(db) + { + let interface = protocol.interface(db); + return if interface.includes_member(db, name) { + interface.instance_member(db, name) + } else { + Type::instance(db, *origin).class_member_with_policy(db, name, policy) + }; + } match ty { Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| { @@ -2710,11 +2734,10 @@ impl<'db> Type<'db> { Type::Intersection(inter) => inter.map_with_boundness_and_qualifiers(db, |elem| { elem.class_member_with_policy(db, name, policy) }), - // TODO: Once `to_meta_type` for the synthesized protocol is fully implemented, this handling should be removed. - Type::ProtocolInstance(ProtocolInstanceType { - inner: Protocol::Synthesized(_), - .. - }) => ty.instance_member(db, name), + // TODO: Remove this once synthesized protocols have a precise meta-type. + Type::ProtocolInstance(protocol) if protocol.class_origin(db).is_none() => { + ty.instance_member(db, name) + } Type::LiteralValue(literal) if name == "__len__" @@ -2816,7 +2839,7 @@ impl<'db> Type<'db> { let own_class = match self { Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { SubclassOfInner::Protocol(protocol) => { - protocol.class_origin().map(|origin| *origin) + protocol.class_origin(db).map(|origin| *origin) } subclass_of => subclass_of.into_class(db), }, @@ -4115,11 +4138,10 @@ impl<'db> Type<'db> { // // Note that we could do this for *all* protocols, but it's only *necessary* for synthesized // ones, and the standard logic is *probably* more performant for class-based protocols? - Type::ProtocolInstance(ProtocolInstanceType { - inner: Protocol::Synthesized(protocol), - .. - }) if policy.mro_no_object_fallback() - && !protocol.interface().includes_member(db, name_str) => + Type::ProtocolInstance(protocol) + if protocol.class_origin(db).is_none() + && policy.mro_no_object_fallback() + && !protocol.interface(db).includes_member(db, name_str) => { Place::Undefined.into() } @@ -4558,7 +4580,7 @@ impl<'db> Type<'db> { // checking it structurally again during call inference. if self_instance .as_protocol_instance() - .is_some_and(|protocol| protocol.to_nominal_instance().is_some()) + .is_some_and(|protocol| protocol.class_origin(db).is_some()) && signature .overloads .iter() @@ -4750,9 +4772,19 @@ impl<'db> Type<'db> { Binding::single(self, Signature::dynamic(Type::Dynamic(dynamic_type))).into() } SubclassOfInner::Class(class) => self.constructor_bindings(db, class), - SubclassOfInner::Protocol(protocol) => protocol.class_origin().map_or_else( + SubclassOfInner::Protocol(protocol) => protocol.class_origin(db).map_or_else( || Binding::single(self, Signature::dynamic(Type::unknown())).into(), - |origin| self.constructor_bindings(db, *origin), + |origin| { + let bindings = self.constructor_bindings(db, *origin); + if protocol.materialization_kind(db).is_some() { + bindings.with_constructed_instance_type( + db, + Type::ProtocolInstance(protocol), + ) + } else { + bindings + } + }, ), SubclassOfInner::TypeVar(tvar) => { let constructor_instance_type = Type::TypeVar(tvar); @@ -5825,10 +5857,14 @@ impl<'db> Type<'db> { Type::NominalInstance(instance) => { instance.class(db).iter_mro(db).find_map(from_class_base) } - Type::ProtocolInstance(ProtocolInstanceType { - inner: Protocol::FromClass(class), - .. - }) => class.iter_mro(db).find_map(from_class_base), + Type::ProtocolInstance(protocol) => protocol + .class_origin(db) + .and_then(|class| class.iter_mro(db).find_map(from_class_base)) + .map(|types| { + protocol + .materialization_kind(db) + .map_or(types, |kind| types.materialize(db, kind)) + }), Type::Union(union) => { let mut yield_builder = Some(UnionBuilder::new(db)); let mut send_builder = Some(UnionBuilder::new(db)); @@ -6560,16 +6596,9 @@ impl<'db> Type<'db> { })) }), - Type::ProtocolInstance(instance) => { - // TODO: Add tests for materialization once subtyping/assignability is implemented for - // protocols. It _might_ require changing the logic here because: - // - // > Subtyping for protocol instances involves taking account of the fact that - // > read-only property members, and method members, on protocols act covariantly; - // > write-only property members act contravariantly; and read/write attribute - // > members on protocols act invariantly - Type::ProtocolInstance(instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) - } + Type::ProtocolInstance(instance) => Type::ProtocolInstance( + instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + ), Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderGet(function)) => { Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderGet( @@ -7231,7 +7260,9 @@ impl<'db> Type<'db> { Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { SubclassOfInner::Dynamic(_) => None, SubclassOfInner::Class(class) => class.type_definition(db), - SubclassOfInner::Protocol(protocol) => protocol.class_origin()?.type_definition(db), + SubclassOfInner::Protocol(protocol) => { + protocol.class_origin(db)?.type_definition(db) + } SubclassOfInner::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar( bound_typevar.typevar(db).definition(db)?, )), @@ -7266,10 +7297,9 @@ impl<'db> Type<'db> { bound_typevar.typevar(db).definition(db)?, )), - Self::ProtocolInstance(protocol) => match protocol.inner { - Protocol::FromClass(class) => class.type_definition(db), - Protocol::Synthesized(_) => None, - }, + Self::ProtocolInstance(protocol) => protocol + .class_origin(db) + .and_then(|class| class.type_definition(db)), Self::TypedDict(typed_dict) => typed_dict.type_definition(db), diff --git a/crates/ty_python_semantic/src/types/attribute_write.rs b/crates/ty_python_semantic/src/types/attribute_write.rs index 990467172e..7adea613a2 100644 --- a/crates/ty_python_semantic/src/types/attribute_write.rs +++ b/crates/ty_python_semantic/src/types/attribute_write.rs @@ -68,10 +68,11 @@ pub(super) enum ProtocolMemberWriteRequirement<'db> { AssignableTo(Type<'db>), /// Invoke every possible descriptor setter with the assigned value. /// - /// `domain` is the precisely derived write type when that domain fits in [`Type`]. It is used - /// for contextual inference and protocol compatibility, while descriptor calls remain the - /// authority for real assignments. `None` preserves a known write capability whose generic or - /// set-theoretic domain cannot be represented precisely. + /// `domain` is the precisely derived write type when that domain fits in [`Type`]. A + /// representable domain constrains contextual inference, assignment, and protocol + /// compatibility. Calling the original descriptor still validates the complete setter + /// contract. `None` preserves a known write capability whose generic or set-theoretic domain + /// cannot be represented precisely. Descriptor { descriptor_ty: Type<'db>, receiver_ty: Type<'db>, @@ -645,6 +646,10 @@ pub(super) fn assignment_attribute_members<'db>( Type::KnownInstance(KnownInstanceType::FunctoolsPartial(_)) ) { object_ty.member(db, attribute) + } else if let Type::ProtocolInstance(protocol) = object_ty + && let Some(origin) = protocol.materialized_origin_property(db, attribute) + { + Type::instance(db, *origin).class_member(db, attribute) } else { object_ty.class_member(db, attribute) }; diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 8a85f07df3..a6490c9fab 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -636,9 +636,9 @@ impl<'db> BoundSuperType<'db> { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let class = match bound { Type::NominalInstance(instance) => Some(instance.class(db)), - Type::ProtocolInstance(protocol) => protocol - .to_nominal_instance() - .map(|instance| instance.class(db)), + Type::ProtocolInstance(protocol) => { + protocol.class_origin(db).map(|class| *class) + } _ => None, }; if let Some(class) = class { @@ -693,13 +693,13 @@ impl<'db> BoundSuperType<'db> { } Type::ProtocolInstance(protocol) => { - if let Some(nominal_instance) = protocol.to_nominal_instance() { + if let Some(class) = protocol.class_origin(db) { SuperOwnerKind::Resolved(Self::resolve_instance_super_owner( db, pivot_class, pivot_class_type, owner_type, - nominal_instance.class(db), + *class, None, )?) } else { @@ -755,9 +755,9 @@ impl<'db> BoundSuperType<'db> { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let class = match bound { Type::NominalInstance(instance) => Some(instance.class(db)), - Type::ProtocolInstance(protocol) => protocol - .to_nominal_instance() - .map(|instance| instance.class(db)), + Type::ProtocolInstance(protocol) => { + protocol.class_origin(db).map(|class| *class) + } _ => None, }; if let Some(class) = class { diff --git a/crates/ty_python_semantic/src/types/callable.rs b/crates/ty_python_semantic/src/types/callable.rs index 79015caf05..d21261a71a 100644 --- a/crates/ty_python_semantic/src/types/callable.rs +++ b/crates/ty_python_semantic/src/types/callable.rs @@ -151,9 +151,16 @@ impl<'db> Type<'db> { // TODO: This is unsound so in future we can consider an opt-in option to disable it. Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { SubclassOfInner::Class(class) => Some(class.into_callable(db)), - SubclassOfInner::Protocol(protocol) => protocol - .class_origin() - .map(|origin| (*origin).into_callable(db)), + SubclassOfInner::Protocol(protocol) => protocol.class_origin(db).map(|origin| { + if protocol.materialization_kind(db).is_some() { + // The origin supplies the constructor, but the actual receiver retains + // `Top[P]` or `Bottom[P]`. Infer with both so instance-returning overloads + // are materialized without replacing explicit non-instance returns. + (*origin).into_callable_with_receiver(db, self) + } else { + (*origin).into_callable(db) + } + }), SubclassOfInner::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let upcast_callables = bound diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 4453e10703..13a1c5d900 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2051,12 +2051,25 @@ impl<'db> ClassType<'db> { /// Return a callable type (or union of callable types) that represents the callable /// constructor signature of this class. + pub(super) fn into_callable(self, db: &'db dyn Db) -> CallableTypes<'db> { + self.into_callable_with_receiver(db, Type::from(self)) + } + + /// Infer this class's constructor using the actual class-object receiver. + /// + /// A materialized protocol uses its class origin for constructor lookup, but `Self` must be + /// bound to the materialized receiver. Keeping lookup and receiver separate preserves both + /// instance-returning constructors and constructors that explicitly return another type. #[salsa::tracked( returns(clone), - cycle_initial=|db, _, _| CallableTypes::one(CallableType::bottom(db)), + cycle_initial=|db, _, _, _| CallableTypes::one(CallableType::bottom(db)), heap_size=ruff_memory_usage::heap_size )] - pub(super) fn into_callable(self, db: &'db dyn Db) -> CallableTypes<'db> { + pub(super) fn into_callable_with_receiver( + self, + db: &'db dyn Db, + receiver: Type<'db>, + ) -> CallableTypes<'db> { // TODO: This mimics a lot of the logic in Type::try_call_from_constructor. Can we // consolidate the two? Can we invoke a class by upcasting the class into a Callable, and // then relying on the call binding machinery to Just Work™? @@ -2066,8 +2079,12 @@ impl<'db> ClassType<'db> { .static_class_literal(db) .and_then(|(class_literal, _)| class_literal.generic_context(db)); - let self_ty = Type::from(self); - let metaclass_dunder_call_function_symbol = self_ty + let lookup_type = Type::from(self); + let instance_type = receiver + .to_instance_approximation(db) + .unwrap_or_else(Type::unknown); + + let metaclass_dunder_call_function_symbol = lookup_type .member_lookup_with_policy( db, "__call__", @@ -2093,11 +2110,17 @@ impl<'db> ClassType<'db> { // for dynamic Enum creation. let is_actual_enum = enum_metadata(db, self.class_literal(db)).is_some(); if !is_actual_enum { - return CallableTypes::one(metaclass_dunder_call_function.into_callable_type(db)); + let callable = if receiver == lookup_type { + metaclass_dunder_call_function.into_callable_type(db) + } else { + metaclass_dunder_call_function + .into_callable_type_with_receiver(db, receiver, receiver) + }; + return CallableTypes::one(callable); } } - let dunder_new_function_symbol = self_ty.lookup_dunder_new(db); + let dunder_new_function_symbol = lookup_type.lookup_dunder_new(db); let dunder_new_signature = dunder_new_function_symbol .and_then(|place_and_quals| place_and_quals.ignore_possibly_undefined()) @@ -2108,21 +2131,22 @@ impl<'db> ClassType<'db> { }); let dunder_new_function = if let Some(dunder_new_signature) = dunder_new_signature { + let bound_signature = dunder_new_signature.bind_self_with_receiver( + db, + Some(receiver), + Some(instance_type), + ); + // Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class, // then we should ignore the `__init__` and just return the `__new__` method. - let returns_non_subclass = dunder_new_signature.overloads.iter().any(|signature| { - !signature.return_ty.is_assignable_to( - db, - self_ty - .to_instance_approximation(db) - .expect("ClassType should be instantiable"), - ) - }); + let returns_non_subclass = bound_signature + .overloads + .iter() + .any(|signature| !signature.return_ty.is_assignable_to(db, instance_type)); - let instance_ty = Type::instance(db, self); let dunder_new_bound_method = CallableType::new( db, - dunder_new_signature.bind_self_with_receiver(db, Some(self_ty), Some(instance_ty)), + bound_signature, CallableTypeKind::Regular, CallableFunctionProvenance::None, ); @@ -2135,7 +2159,7 @@ impl<'db> ClassType<'db> { None }; - let dunder_init_function_symbol = self_ty + let dunder_init_function_symbol = lookup_type .member_lookup_with_policy( db, "__init__", @@ -2144,10 +2168,6 @@ impl<'db> ClassType<'db> { ) .place; - let correct_return_type = self_ty - .to_instance_approximation(db) - .unwrap_or_else(Type::unknown); - // If the class defines an `__init__` method, then we synthesize a callable type with the // same parameters as the `__init__` method after it is bound, and with the return type of // the concrete type of `Self`. @@ -2173,8 +2193,7 @@ impl<'db> ClassType<'db> { ty.as_typevar() .is_none_or(|bound_typevar| !bound_typevar.typevar(db).is_self(db)) }); - let return_type = self_annotation.unwrap_or(correct_return_type); - let instance_ty = Type::instance(db, self); + let return_type = self_annotation.unwrap_or(instance_type); let generic_context = GenericContext::merge_optional( db, class_generic_context, @@ -2188,8 +2207,8 @@ impl<'db> ClassType<'db> { .with_definition(signature.definition()) .bind_self_with_receiver( db, - Some(instance_ty), - Some(instance_ty), + Some(instance_type), + Some(instance_type), ) }; @@ -2223,7 +2242,7 @@ impl<'db> ClassType<'db> { (None, None) => { // If no `__new__` or `__init__` method is found, then we fall back to looking for // an `object.__new__` method. - let new_function_symbol = self_ty + let new_function_symbol = lookup_type .member_lookup_with_policy( db, "__new__", @@ -2242,7 +2261,7 @@ impl<'db> ClassType<'db> { } CallableTypes::one( new_function - .into_bound_method_type(db, correct_return_type) + .into_bound_method_type(db, instance_type) .into_callable_type(db), ) } else { @@ -2252,7 +2271,7 @@ impl<'db> ClassType<'db> { Signature::new_generic( class_generic_context, Parameters::empty(), - correct_return_type, + instance_type, ), )) } diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs index f4dc8343d0..3b99b53e8b 100644 --- a/crates/ty_python_semantic/src/types/cyclic.rs +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -138,7 +138,7 @@ impl<'db> DefinitionReferenceVisitor<'db> { } let class = match ty { - Type::ProtocolInstance(protocol) => *protocol.class_origin()?, + Type::ProtocolInstance(protocol) => *protocol.class_origin(db)?, Type::TypedDict(typed_dict) => typed_dict.defining_class()?, _ => return None, }; @@ -198,7 +198,7 @@ impl<'db> TypeVisitor<'db> for DefinitionReferenceVisitor<'db> { } fn visit_protocol_instance_type(&self, db: &'db dyn Db, protocol: ProtocolInstanceType<'db>) { - if let Some(class) = protocol.class_origin() { + if let Some(class) = protocol.class_origin(db) { class.walk_recursive_member_types(db, self); } } @@ -229,12 +229,12 @@ impl<'db> TypeAliasType<'db> { impl<'db> ProtocolInstanceType<'db> { fn definition(self, db: &'db dyn Db) -> Option> { - let (origin, _) = self.class_origin()?.static_class_literal(db)?; + let (origin, _) = self.class_origin(db)?.static_class_literal(db)?; Some(origin.definition(db)) } fn is_recursive(self, db: &'db dyn Db) -> bool { - let Some(class) = self.class_origin() else { + let Some(class) = self.class_origin(db) else { return false; }; let Some((origin, _)) = class.static_class_literal(db) else { diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 0c1db5eb0b..a0c1cbd5ce 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -26,9 +26,9 @@ use crate::types::tuple::TupleSpec; use crate::types::typed_dict::TypedDictSchema; use crate::types::typevar::TypeVarInstance; use crate::types::{ - BoundTypeVarInstance, ClassType, DynamicType, ErrorContextTree, LintDiagnosticGuard, Protocol, - ProtocolInstanceType, SpecialFormType, SubclassOfInner, Type, TypeContext, TypeVarVariance, - binding_type, protocol_class::ProtocolClass, + BoundTypeVarInstance, ClassType, DynamicType, ErrorContextTree, LintDiagnosticGuard, + SpecialFormType, SubclassOfInner, Type, TypeContext, TypeVarVariance, binding_type, + protocol_class::ProtocolClass, }; use crate::types::{KnownInstanceType, MemberLookupPolicy, TypeVarKind, TypedDictType, UnionType}; use crate::{Db, DisplaySettings, FxIndexMap, Program, declare_lint}; @@ -2837,10 +2837,7 @@ pub(crate) fn report_undeclared_protocol_member( /// We also want to avoid suggesting invalid syntax such as `x: = int`. fn should_give_hint<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { let class = match ty { - Type::ProtocolInstance(ProtocolInstanceType { - inner: Protocol::FromClass(_), - .. - }) => return true, + Type::ProtocolInstance(protocol) if protocol.class_origin(db).is_some() => return true, Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { SubclassOfInner::Class(class) => class, SubclassOfInner::Protocol(_) => return true, diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 4c6cf9fe31..c2bc80164b 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -33,9 +33,8 @@ use crate::types::visitor::TypeVisitor; use crate::types::{ CallableType, IntersectionType, KnownBoundMethodType, KnownClass, KnownInstanceType, LiteralValueType, LiteralValueTypeKind, MaterializationKind, PropertyInstanceType, Protocol, - ProtocolInstanceType, SpecialFormType, StringLiteralType, SubclassOfInner, SubclassOfType, - Type, TypeAliasType, TypeGuardLike, TypedDictModule, TypedDictType, UnionType, - WrapperDescriptorKind, visitor, + SpecialFormType, StringLiteralType, SubclassOfInner, SubclassOfType, Type, TypeAliasType, + TypeGuardLike, TypedDictModule, TypedDictType, UnionType, WrapperDescriptorKind, visitor, }; use ty_python_core::definition::Definition; use ty_python_core::scope::{FileScopeId, ScopeKind}; @@ -582,10 +581,9 @@ impl<'db> TypeVisitor<'db> for AmbiguousNameCollector<'db> { // Visit the class (as if it were a nominal-instance type) // rather than the protocol members, if it is a class-based protocol. // (For the purposes of displaying the type, we'll use the class name.) - Type::ProtocolInstance(ProtocolInstanceType { - inner: Protocol::FromClass(class), - .. - }) => return self.visit_type(db, Type::from(class)), + Type::ProtocolInstance(protocol) if let Some(class) = protocol.class_origin(db) => { + return self.visit_type(db, Type::from(class)); + } // no need to recurse into TypeVar bounds/constraints Type::TypeVar(_) => return, _ => {} @@ -985,6 +983,31 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { .display_with(self.db, self.settings.clone()) .fmt_detailed(f), }, + Protocol::Materialized(materialized) => { + let materialization_kind = protocol.display_materialization_kind(self.db); + if let Some(kind) = materialization_kind { + let (name, form) = match kind { + MaterializationKind::Top => ("Top", SpecialFormType::Top), + MaterializationKind::Bottom => ("Bottom", SpecialFormType::Bottom), + }; + f.with_type(Type::SpecialForm(form)).write_str(name)?; + f.write_char('[')?; + } + + match *materialized.origin(self.db) { + ClassType::NonGeneric(class) => class + .display_with(self.db, self.settings.clone()) + .fmt_detailed(f), + ClassType::Generic(alias) => alias + .display_with(self.db, self.settings.clone()) + .fmt_detailed(f), + }?; + + if materialization_kind.is_some() { + f.write_char(']')?; + } + Ok(()) + } Protocol::Synthesized(synthetic) => { f.set_invalid_type_annotation(); f.write_char('<')?; diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index da53309f0d..55e6ad95e4 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -2715,7 +2715,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // fallback erases; restrict mixed unions to the protocol used by dictionary constructors. if !other_types.is_empty() && !matches!(formal, Type::ProtocolInstance(protocol) - if protocol.class_origin().is_some_and(|class| { + if protocol.class_origin(self.db).is_some_and(|class| { class.is_known(self.db, KnownClass::SupportsKeysAndGetItem) })) { @@ -3284,12 +3284,48 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } (formal, Type::ProtocolInstance(actual_protocol)) => { + if let Type::ProtocolInstance(formal_protocol) = formal + && let Some(actual_origin) = actual_protocol.materialized_origin(self.db) + && let Some(formal_origin) = formal_protocol.class_origin(self.db) + { + let nominally_inherited = actual_origin + .iter_mro(self.db) + .filter_map(ClassBase::into_class) + .any(|base| { + base.class_literal(self.db) == formal_origin.class_literal(self.db) + }); + let when = if nominally_inherited + || formal_protocol + .interface(self.db) + .has_only_finite_members(self.db) + { + Some(actual.when_constraint_set_assignable_to_owned(self.db, formal)) + } else { + actual_protocol + .when_non_recursive_members_assignable_to_owned( + self.db, + formal_protocol, + ) + .map(Cow::Borrowed) + }; + + // Materialized protocols cannot be replaced by their nominal origin: doing + // so would recover the original `Any` requirements. Infer from the complete + // interface when doing so is cycle-safe; otherwise use its nonrecursive + // requirements and leave full recursive compatibility to argument checking. + if let Some(when) = when { + let when = self.constraints.load(self.db, &when); + self.infer_from_constraint_set(when)?; + return Ok(()); + } + } + // TODO: This will only handle protocol classes that explicit inherit // from other generic protocol classes by listing it as a base class. // To handle classes that implicitly implement a generic protocol, we // will need to check the types of the protocol members to be able to // infer the specialization of the protocol that the class implements. - if let Some(actual_nominal) = actual_protocol.to_nominal_instance() { + if let Some(actual_nominal) = actual_protocol.nominal_origin_instance(self.db) { return self.infer_map_impl( formal, Type::NominalInstance(actual_nominal), diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index e62845d9b8..8368b2706b 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -239,7 +239,7 @@ pub fn definitions_for_attribute<'db>( // location for go-to-definition, even though the origin is not a nominal upper bound. let subclass_origin = |subclass_of: SubclassOfInner<'db>| { let class = match subclass_of { - SubclassOfInner::Protocol(protocol) => protocol.class_origin().map(|origin| *origin), + SubclassOfInner::Protocol(protocol) => protocol.class_origin(db).map(|origin| *origin), subclass_of => subclass_of.into_class(db), }?; class diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 32d128b8f5..1b13ea1260 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -12061,10 +12061,9 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { builder.infer_maybe_standalone_expression(value, TypeContext::default()) }); // If the member is a data descriptor, the RHS value may differ from the value actually assigned. - if value_ty - .class_member(db, &attr.id) - .place - .ignore_possibly_undefined() + if assignment_attribute_members(db, value_ty, &attr.id) + .and_then(AssignmentAttributeMembers::type_member) + .and_then(|member| member.place.ignore_possibly_undefined()) .is_some_and(|ty| ty.may_be_data_descriptor(db)) { builder.discard_dict_key_assignments_for(self.binding); diff --git a/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs b/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs index 516d3e321a..f05ec5e804 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs @@ -260,6 +260,11 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { TypeContext::new(Some(domain.unwrap_or_else(Type::unknown))), emit_diagnostics, ); + if let Some(domain) = domain + && !self.check_type_pair(value_ty, *domain, emit_diagnostics) + { + return false; + } self.evaluate_protocol_descriptor_write( *descriptor_ty, *receiver_ty, diff --git a/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs b/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs index c338ae85d9..7228d92b80 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs @@ -238,10 +238,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // that happens to have the right type. let is_self_parameter = self.is_instance_attribute_assignment(target); - let class_instance_ty = Type::instance(db, class_ty).top_materialization(db); - let object_instance_ty = object_ty.bind_self_typevars(db, class_instance_ty); - let is_current_class_instance = - is_self_parameter && object_instance_ty.is_subtype_of(db, class_instance_ty); + // Final ownership is nominal: checking structural protocol requirements can + // incorrectly reject the declaring class's own receiver. + let is_current_class_instance = is_self_parameter + && object_ty.nominal_class(db).is_some_and(|object_class| { + object_class.is_subtype_of_class_literal(db, class_ty.class_literal(db)) + }); if !is_current_class_instance { report_not_in_init(); return true; diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index d56e4e899a..386c84c977 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -7,14 +7,14 @@ use std::marker::PhantomData; use ruff_python_ast::name::Name; use ty_module_resolver::{ModuleName, file_to_module}; -use super::protocol_class::ProtocolInterface; +use super::protocol_class::{ProtocolInterface, ProtocolInterfaceView}; use super::{ BoundTypeVarIdentity, BoundTypeVarInstance, ClassType, DivergentType, KnownClass, MaterializationKind, SubclassOfType, Type, TypeAliasType, TypeVarVariance, }; use crate::place::PlaceAndQualifiers; use crate::types::constraints::{ - ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, + ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, OwnedConstraintSet, }; use crate::types::enums::is_single_member_enum; use crate::types::generics::walk_specialization; @@ -492,16 +492,36 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ty: Type<'db>, protocol: ProtocolInstanceType<'db>, ) -> ConstraintSet<'db, 'c> { - // `ty` might satisfy the protocol nominally, if `protocol` is a class-based protocol and - // `ty` has the protocol class in its MRO. This is a much cheaper check than the - // structural check we perform below, so we do it first to avoid the structural check when - // we can. + // Explicit protocol inheritance is nominal, but materializing a protocol can change + // the requirements represented by that same class. The nominal shortcut is therefore + // valid only when materialization leaves the target's members unchanged. let mut result = self.never(); + let source_protocol = ty.as_protocol_instance(); + + // Every gradual type lies between its bottom and top materializations. Comparing the + // exact same class specialization can therefore settle these directions without expanding + // a recursive protocol's members or confusing opposite materialization requirements. + if let Some(source) = source_protocol + && matches!( + ( + source.materialization_kind(db), + protocol.materialization_kind(db) + ), + ( + None | Some(MaterializationKind::Bottom), + Some(MaterializationKind::Top) + ) | (Some(MaterializationKind::Bottom), None) + ) + && let (Some(source_origin), Some(target_origin)) = + (source.class_origin(db), protocol.class_origin(db)) + && source_origin == target_origin + { + return self.always(); + } - if let Some(nominal_instance) = protocol.to_nominal_instance() { - let source_protocol_as_nominal = ty - .as_protocol_instance() - .and_then(ProtocolInstanceType::to_nominal_instance); + let source_protocol_as_nominal = + source_protocol.and_then(|source| source.nominal_origin_instance(db)); + if let Some(nominal_instance) = protocol.nominal_origin_instance(db) { // if `ty` and `protocol` are *both* protocols, we also need to treat `ty` as if it // were a nominal type, or we won't consider a protocol `P` that explicitly inherits // from a protocol `Q` to be a subtype of `Q` to be a subtype of `Q` if it overrides @@ -513,53 +533,60 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let nominally_satisfied = self.check_type_pair(db, type_to_test, Type::NominalInstance(nominal_instance)); - if result - .union(db, self.constraints, nominally_satisfied) - .is_trivially_always_satisfied() - { - return result; - } - - // `Generator` special case: compare the type parameters nominally. Prior to 3.13, - // its return type does not appear non-recursively in the protocol; from 3.13 onward, - // structurally inferring through `close() -> ReturnT | None` can spuriously infer - // `None`. - // TODO: Remove the Python 3.13+ extension of this special case once + // `Generator` parameters must be compared nominally. The class specialization + // already materializes each parameter according to its variance, while structural + // inference through `close() -> ReturnT | None` can infer a spurious `None` on + // Python 3.13 and newer. + // TODO: Remove the Python 3.13+ extension once // https://github.com/astral-sh/ty/issues/3596 is fixed. - if let Some(source_protocol) = ty.as_protocol_instance() - && let Protocol::FromClass(source_class) = source_protocol.inner - && let Protocol::FromClass(proto_class) = protocol.inner - && source_class.is_known(db, KnownClass::Generator) - && proto_class.is_known(db, KnownClass::Generator) + if nominal_instance.has_known_class(db, KnownClass::Generator) + && source_protocol_as_nominal + .is_some_and(|source| source.has_known_class(db, KnownClass::Generator)) { - return result; + return nominally_satisfied; } - if let Some(structurally_satisfied) = self.try_check_non_recursive_protocol_members( - db, - ty, - protocol, - source_protocol_as_nominal, - nominal_instance, - ) { - return result.or(db, self.constraints, || structurally_satisfied); - } + // A nominal relation that cannot succeed cannot bypass any materialized requirement. + // Check that inexpensive case first: comparing every requirement of an unrelated + // recursive protocol can expand its interface before structural member ordering gets + // a chance to reject an incompatible finite member. + let nominal_is_safe = nominally_satisfied.is_never_satisfied(db) + || (!protocol.materialization_changes_requirements(db, protocol) + && !source_protocol.is_some_and(|source| { + source.materialization_changes_requirements(db, protocol) + })); + + if nominal_is_safe { + if result + .union(db, self.constraints, nominally_satisfied) + .is_trivially_always_satisfied() + { + return result; + } + + if let Some(structurally_satisfied) = self.try_check_non_recursive_protocol_members( + db, + ty, + protocol, + source_protocol_as_nominal, + nominal_instance, + ) { + return result.or(db, self.constraints, || structurally_satisfied); + } - // For union simplification, failing the nominal relation between two - // specializations of the same protocol class is enough to keep both union elements. - // Falling back to the structural relation can recursively compare every protocol - // member even though a failed redundancy check only means that we preserve a - // potentially redundant union arm. - if matches!(self.relation, TypeRelation::Redundancy { pure: false }) - && ty - .as_protocol_instance() - .and_then(ProtocolInstanceType::to_nominal_instance) - .is_some_and(|source_instance| { + // For union simplification, failing the nominal relation between two + // specializations of the same protocol class is enough to keep both union elements. + // Falling back to the structural relation can recursively compare every protocol + // member even though a failed redundancy check only means that we preserve a + // potentially redundant union arm. + if matches!(self.relation, TypeRelation::Redundancy { pure: false }) + && source_protocol_as_nominal.is_some_and(|source_instance| { source_instance.class(db).class_literal(db) == nominal_instance.class(db).class_literal(db) }) - { - return nominally_satisfied; + { + return nominally_satisfied; + } } } @@ -581,7 +608,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ) } else { protocol - .inner .interface(db) .members(db) .when_all(db, self.constraints, |member| { @@ -637,19 +663,32 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let source_interface = source_protocol.interface(db); let target_interface = protocol.interface(db); let source_non_recursive = - non_recursive_protocol_interface(db, source_interface, identity_protocol, ty); + non_recursive_protocol_interface(db, source_interface.base(), identity_protocol, ty); let target_non_recursive = non_recursive_protocol_interface( db, - target_interface, + target_interface.base(), identity_protocol, Type::ProtocolInstance(protocol), ); - if source_non_recursive == source_interface && target_non_recursive == target_interface { + if source_non_recursive == source_interface.base() + && target_non_recursive == target_interface.base() + { return None; } - Some(self.check_protocol_interface_pair(db, ty, source_non_recursive, target_non_recursive)) + Some(self.check_protocol_interface_pair( + db, + ty, + ProtocolInterfaceView::new( + source_non_recursive, + source_interface.materialization_kind(), + ), + ProtocolInterfaceView::new( + target_non_recursive, + target_interface.materialization_kind(), + ), + )) } /// Return whether a class-object type inhabits `type[protocol]`. @@ -735,7 +774,7 @@ fn non_recursive_protocol_interface<'db>( if ty .as_protocol_instance() - .and_then(ProtocolInstanceType::to_nominal_instance) + .and_then(|protocol| protocol.nominal_origin_instance(db)) .is_some_and(|instance| instance.class_literal(db) == self.origin) { self.found.set(true); @@ -757,6 +796,43 @@ fn non_recursive_protocol_interface<'db>( }) } +/// Infers protocol constraints without expanding recursive member requirements. +/// +/// The target view retains its materialization, so readable and writable members are still +/// materialized in their respective variance positions. The complete target protocol must be +/// checked separately after generic inference. +#[salsa::tracked( + returns(ref), + cycle_initial = |_, _, _, _| OwnedConstraintSet::always(), + heap_size = ruff_memory_usage::heap_size, +)] +fn non_recursive_protocol_constraints<'db>( + db: &'db dyn Db, + source: ProtocolInstanceType<'db>, + target: ProtocolInterfaceView<'db>, +) -> OwnedConstraintSet<'db> { + let constraints = ConstraintSetBuilder::new(); + constraints.into_owned(|constraints| { + let relation_visitor = HasRelationToVisitor::default(constraints); + let disjointness_visitor = IsDisjointVisitor::default(constraints); + let signature_relation_visitor = SignatureRelationVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::default(); + let checker = TypeRelationChecker::constraint_set_assignability( + constraints, + &relation_visitor, + &disjointness_visitor, + &signature_relation_visitor, + &materialization_visitor, + ); + checker.check_protocol_interface_pair( + db, + Type::ProtocolInstance(source), + source.interface(db), + target, + ) + }) +} + impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { /// Return `true` if this protocol type is disjoint from the protocol `other`. /// @@ -916,16 +992,23 @@ pub(super) fn walk_protocol_instance_type<'db, V: super::visitor::TypeVisitor<'d visitor: &V, ) { if visitor.should_visit_lazy_type_attributes() { - walk_protocol_interface(db, protocol.inner.interface(db), visitor); + walk_protocol_interface(db, protocol.interface(db), visitor); } else { match protocol.inner { - Protocol::FromClass(class) => { - if let Some((_, Some(specialization))) = class.static_class_literal(db) { + Protocol::FromClass(_) | Protocol::Materialized(_) => { + if let Some((_, Some(specialization))) = protocol + .class_origin(db) + .and_then(|class| class.static_class_literal(db)) + { walk_specialization(db, specialization, visitor); } } Protocol::Synthesized(synthesized) => { - walk_protocol_interface(db, synthesized.interface(), visitor); + walk_protocol_interface( + db, + ProtocolInterfaceView::new(synthesized.interface(), None), + visitor, + ); } } } @@ -934,8 +1017,8 @@ pub(super) fn walk_protocol_instance_type<'db, V: super::visitor::TypeVisitor<'d impl<'db> ProtocolInstanceType<'db> { /// Return `true` if this is the standard-library `Hashable` protocol. pub(super) fn is_hashable(self, db: &'db dyn Db) -> bool { - self.to_nominal_instance() - .is_some_and(|instance| instance.class(db).is_known(db, KnownClass::Hashable)) + self.class_origin(db) + .is_some_and(|class| class.is_known(db, KnownClass::Hashable)) } // Keep this method private, so that the only way of constructing `ProtocolInstanceType` @@ -956,40 +1039,123 @@ impl<'db> ProtocolInstanceType<'db> { } } - /// Return the class backing a class-based protocol instance. - pub(super) fn as_class_based(self) -> Option> { + /// Preserves a class-based protocol and the polarity of its pending materialization. + /// + /// Member requirements are materialized only when an operation observes them. + fn materialized( + db: &'db dyn Db, + origin: ProtocolClass<'db>, + materialization_kind: MaterializationKind, + ) -> Self { + Self { + inner: Protocol::Materialized(MaterializedProtocolType::new( + db, + origin, + materialization_kind, + )), + _phantom: PhantomData, + } + } + + /// Returns the nominal instance of a protocol's origin without asserting nominal subtyping. + pub(super) fn nominal_origin_instance( + self, + db: &'db dyn Db, + ) -> Option> { + self.class_origin(db).map(|origin| { + NominalInstanceType(NominalInstanceInner::NonTuple(NominalInstanceClass::Plain( + *origin, + ))) + }) + } + + /// Return the class that defines this protocol, if it is class-backed. + pub(super) fn class_origin(self, db: &'db dyn Db) -> Option> { match self.inner { Protocol::FromClass(class) => Some(class), Protocol::Synthesized(_) => None, + Protocol::Materialized(materialized) => Some(materialized.origin(db)), } } - /// If this is a class-based protocol, convert the protocol-instance into a nominal instance. - /// - /// If this is a synthesized protocol that does not correspond to a class definition - /// in source code, return `None`. These are "pure" abstract types, that cannot be - /// treated in a nominal way. - pub(super) fn to_nominal_instance(self) -> Option> { + /// Returns the pending materialization of a class-based protocol, if any. + pub(super) fn materialization_kind(self, db: &'db dyn Db) -> Option { match self.inner { - Protocol::FromClass(class) => Some(NominalInstanceType( - NominalInstanceInner::NonTuple(NominalInstanceClass::Plain(*class)), - )), - Protocol::Synthesized(_) => None, + Protocol::Materialized(materialized) => Some(materialized.materialization_kind(db)), + Protocol::FromClass(_) | Protocol::Synthesized(_) => None, } } - /// Return the class that defines this protocol, if it is class-backed. - pub(super) const fn class_origin(self) -> Option> { + /// Returns the class origin of a protocol with a pending materialization. + pub(super) fn materialized_origin(self, db: &'db dyn Db) -> Option> { match self.inner { - Protocol::FromClass(class) => Some(class), - Protocol::Synthesized(_) => None, + Protocol::Materialized(materialized) => Some(materialized.origin(db)), + Protocol::FromClass(_) | Protocol::Synthesized(_) => None, } } + /// Returns the nominal origin when a materialized requirement is a property descriptor. + /// + /// Descriptor lookup needs the original property object even though ordinary reads expose + /// its lazily materialized value. + pub(super) fn materialized_origin_property( + self, + db: &'db dyn Db, + name: &str, + ) -> Option> { + self.materialized_origin(db) + .filter(|_| self.interface(db).member_is_property(db, name)) + } + + /// Returns whether a materialization changes any member required by `target`. + /// + /// An unrelated changed member must not prevent an explicitly inherited protocol from + /// satisfying its base nominally. + fn materialization_changes_requirements( + self, + db: &'db dyn Db, + target: ProtocolInstanceType<'db>, + ) -> bool { + self.materialization_kind(db).is_some() + && self + .interface(db) + .differs_for_members_required_by(db, target.interface(db)) + } + + /// Returns the materialization wrapper needed for displaying this protocol. + /// + /// Fully static requirements need no wrapper. A generic specialization can already display + /// its materialization, in which case adding another wrapper would duplicate `Top` or + /// `Bottom`. + pub(super) fn display_materialization_kind( + self, + db: &'db dyn Db, + ) -> Option { + let Protocol::Materialized(materialized) = self.inner else { + return None; + }; + let origin = materialized.origin(db); + if origin + .static_class_literal(db) + .and_then(|(_, specialization)| specialization) + .and_then(|specialization| specialization.materialization_kind(db)) + .is_some() + { + return None; + } + + let interface = self.interface(db); + interface + .differs_for_members_required_by(db, interface) + .then_some(materialized.materialization_kind(db)) + } + /// Return the structural meta-type of this protocol-instance type. pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { match self.inner { - Protocol::FromClass(_) => SubclassOfType::from_protocol(self), + Protocol::FromClass(_) | Protocol::Materialized(_) => { + SubclassOfType::from_protocol(self) + } // TODO: we can and should do better here. // @@ -1010,10 +1176,10 @@ impl<'db> ProtocolInstanceType<'db> { /// Return the nominal meta-type used for internal class-member lookup on a protocol instance. pub(super) fn to_nominal_meta_type(self, db: &'db dyn Db) -> Type<'db> { - match self.inner { - Protocol::FromClass(class) => SubclassOfType::from(db, *class), - Protocol::Synthesized(_) => self.to_meta_type(db), - } + self.class_origin(db).map_or_else( + || self.to_meta_type(db), + |origin| SubclassOfType::from(db, *origin), + ) } /// Return `true` if this protocol is a supertype of `object`. @@ -1062,10 +1228,26 @@ impl<'db> ProtocolInstanceType<'db> { }) } + /// Returns an effective materialized member without applying the nominal class fallback. + pub(super) fn materialized_interface_member( + self, + db: &'db dyn Db, + name: &str, + ) -> Option> { + self.materialization_kind(db)?; + let interface = self.interface(db); + interface + .includes_member(db, name) + .then(|| interface.instance_member(db, name)) + } + pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { match self.inner { Protocol::FromClass(class) => class.instance_member(db, name), Protocol::Synthesized(synthesized) => synthesized.interface().instance_member(db, name), + Protocol::Materialized(materialized) => self + .materialized_interface_member(db, name) + .unwrap_or_else(|| materialized.origin(db).instance_member(db, name)), } } @@ -1078,11 +1260,32 @@ impl<'db> ProtocolInstanceType<'db> { ) -> Self { match self.inner { Protocol::FromClass(class) => { - Self::from_class(class.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + let mapped_class = class.apply_type_mapping_impl(db, type_mapping, tcx, visitor); + if let TypeMapping::Materialize(materialization_kind) = type_mapping { + Self::materialized(db, mapped_class, *materialization_kind) + } else { + Self::from_class(mapped_class) + } } Protocol::Synthesized(synthesized) => Self::synthesized( synthesized.apply_type_mapping_impl(db, type_mapping, tcx, visitor), ), + Protocol::Materialized(materialized) => { + if matches!(type_mapping, TypeMapping::Materialize(_)) { + self + } else { + Self::materialized( + db, + materialized.origin(db).apply_type_mapping_impl( + db, + type_mapping, + tcx, + visitor, + ), + materialized.materialization_kind(db), + ) + } + } } } @@ -1100,12 +1303,45 @@ impl<'db> ProtocolInstanceType<'db> { Protocol::Synthesized(synthesized) => { synthesized.find_legacy_typevars_impl(db, binding_context, typevars, visitor); } + Protocol::Materialized(materialized) => { + materialized.origin(db).find_legacy_typevars_impl( + db, + binding_context, + typevars, + visitor, + ); + } } } - pub(super) fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { + pub(super) fn interface(self, db: &'db dyn Db) -> ProtocolInterfaceView<'db> { self.inner.interface(db) } + + /// Returns constraints inferred from the nonrecursive requirements of `target`. + /// + /// Recursive requirements are omitted only while inferring a generic specialization. The + /// eventual argument check must still compare against the complete protocol interface. + pub(super) fn when_non_recursive_members_assignable_to_owned( + self, + db: &'db dyn Db, + target: Self, + ) -> Option<&'db OwnedConstraintSet<'db>> { + let origin = target.class_origin(db)?; + let interface = target.interface(db); + let non_recursive = non_recursive_protocol_interface( + db, + interface.base(), + origin, + Type::ProtocolInstance(target), + ); + let target = ProtocolInterfaceView::new(non_recursive, interface.materialization_kind()); + if target.member_count(db) == 0 { + return None; + } + + Some(non_recursive_protocol_constraints(db, self, target)) + } } impl<'db> VarianceInferable<'db> for ProtocolInstanceType<'db> { @@ -1114,20 +1350,38 @@ impl<'db> VarianceInferable<'db> for 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. +/// A class-backed protocol materialization whose member requirements remain lazy. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +pub(super) struct MaterializedProtocolType<'db> { + #[returns(copy)] + pub(super) origin: ProtocolClass<'db>, + #[returns(copy)] + pub(super) materialization_kind: MaterializationKind, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for MaterializedProtocolType<'_> {} + +/// A class-backed, synthesized, or lazily materialized protocol. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize, salsa::SalsaValue)] pub(super) enum Protocol<'db> { FromClass(ProtocolClass<'db>), Synthesized(SynthesizedProtocolType<'db>), + Materialized(MaterializedProtocolType<'db>), } impl<'db> Protocol<'db> { /// Return the members of this protocol type - fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { + fn interface(self, db: &'db dyn Db) -> ProtocolInterfaceView<'db> { match self { - Self::FromClass(class) => class.interface(db), - Self::Synthesized(synthesized) => synthesized.interface(), + Self::FromClass(class) => ProtocolInterfaceView::new(class.interface(db), None), + Self::Synthesized(synthesized) => { + ProtocolInterfaceView::new(synthesized.interface(), None) + } + Self::Materialized(materialized) => ProtocolInterfaceView::new( + materialized.origin(db).unmaterialized_interface(db), + Some(materialized.materialization_kind(db)), + ), } } @@ -1144,12 +1398,17 @@ impl<'db> Protocol<'db> { Self::Synthesized(synthesized) => Some(Self::Synthesized( synthesized.recursive_type_normalized_impl(db, div, nested)?, )), + Self::Materialized(materialized) => { + Some(Self::Materialized(MaterializedProtocolType::new( + db, + materialized + .origin(db) + .recursive_type_normalized_impl(db, div, nested)?, + materialized.materialization_kind(db), + ))) + } } } - - pub(super) const fn is_synthesized(self) -> bool { - matches!(self, Self::Synthesized(_)) - } } impl<'db> VarianceInferable<'db> for Protocol<'db> { @@ -1159,6 +1418,9 @@ impl<'db> VarianceInferable<'db> for Protocol<'db> { Protocol::Synthesized(synthesized_protocol_type) => { synthesized_protocol_type.variance_of(db, typevar) } + Protocol::Materialized(materialized) => { + materialized.origin(db).variance_of(db, typevar) + } } } } diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index d4f0ebaed8..0f9e76a435 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -251,7 +251,7 @@ impl<'db> AllMembers<'db> { } SubclassOfInner::Protocol(protocol) => { if let Some((class_literal, _)) = protocol - .class_origin() + .class_origin(db) .and_then(|origin| origin.static_class_literal(db)) { self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal)); diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 948d8c628c..e5d07f1469 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -23,11 +23,11 @@ use crate::{ }, types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, - CallableType, ClassBase, ClassType, ErrorContext, FindLegacyTypeVarsVisitor, + CallableType, ClassBase, ClassType, ErrorContext, FindLegacyTypeVarsVisitor, GenericAlias, GenericContext, InstanceFallbackShadowsNonDataDescriptor, IntersectionType, KnownFunction, - MemberLookupKey, MemberLookupPolicy, Parameter, PropertyInstanceType, ProtocolInstanceType, - SelfBinding, Signature, StaticClassLiteral, Type, TypeMapping, TypeQualifiers, - TypeVarBoundOrConstraints, TypeVarVariance, UnionType, VarianceInferable, + MaterializationKind, MemberLookupKey, MemberLookupPolicy, Parameter, PropertyInstanceType, + ProtocolInstanceType, SelfBinding, Signature, StaticClassLiteral, Type, TypeMapping, + TypeQualifiers, TypeVarBoundOrConstraints, TypeVarVariance, UnionType, VarianceInferable, constraints::{ConstraintSet, IteratorConstraintsExtension, OptionConstraintsExtension}, context::InferContext, diagnostic::report_undeclared_protocol_member, @@ -76,6 +76,30 @@ impl<'db> ProtocolClass<'db> { cached_protocol_interface(db, *self) } + /// Returns the interface before an invariant specialization is materialized. + /// + /// A materialized generic origin retains its specialization for nominal identity and display. + /// Building member requirements from that specialization, however, would first materialize an + /// invariant type variable as a read and then reuse that result as its write. Strip only the + /// pending marker while constructing the shared interface so reads and writes can each apply + /// the original materialization in their own variance position. + pub(super) fn unmaterialized_interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { + let ClassType::Generic(alias) = *self else { + return self.interface(db); + }; + let specialization = alias.specialization(db); + if specialization.materialization_kind(db).is_none() { + return self.interface(db); + } + + let alias = GenericAlias::new( + db, + alias.origin(db), + specialization.with_materialization_kind(db, None), + ); + ProtocolClass(ClassType::Generic(alias)).interface(db) + } + /// Walk the effective non-method member types declared by this protocol. /// /// Method relations have their own declaration-based recursion guard. Keeping them out of this @@ -298,9 +322,233 @@ pub(super) struct ProtocolInterface<'db> { impl get_size2::GetSize for ProtocolInterface<'_> {} +/// A protocol interface together with the materialization applied to its requirements. +/// +/// The original interface remains shared. A member's readable and writable types are +/// materialized only when that member is accessed or compared. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize, salsa::SalsaValue)] +pub(super) struct ProtocolInterfaceView<'db> { + interface: ProtocolInterface<'db>, + materialization: Option, +} + +impl<'db> ProtocolInterfaceView<'db> { + pub(super) const fn new( + interface: ProtocolInterface<'db>, + materialization: Option, + ) -> Self { + Self { + interface, + materialization, + } + } + + pub(super) const fn base(self) -> ProtocolInterface<'db> { + self.interface + } + + pub(super) const fn materialization_kind(self) -> Option { + self.materialization + } + + pub(super) fn members<'a>( + self, + db: &'db dyn Db, + ) -> impl ExactSizeIterator> + where + 'db: 'a, + { + self.interface + .inner(db) + .iter() + .map(move |(name, data)| ProtocolMember { + name, + data, + materialization: self.materialization, + }) + } + + pub(super) fn member_count(self, db: &'db dyn Db) -> usize { + self.interface.member_count(db) + } + + /// Returns whether structural comparison can avoid recursive member expansion. + pub(super) fn has_only_finite_members(self, db: &'db dyn Db) -> bool { + self.members(db).all(|member| { + !matches!( + member.structural_member_priority(db), + StructuralMemberPriority::Recursive + ) + }) + } + + fn member_by_name<'a>(self, db: &'db dyn Db, name: &'a str) -> Option> { + self.interface + .inner(db) + .get(name) + .map(|data| ProtocolMember { + name, + data, + materialization: self.materialization, + }) + } + + pub(super) fn includes_member(self, db: &'db dyn Db, name: &str) -> bool { + self.interface.includes_member(db, name) + } + + /// Compare the original and materialized forms of members required by `required`. + /// + /// An unrelated materialized member must not prevent a protocol from retaining its + /// nominal relationship to one of its bases. + pub(super) fn differs_for_members_required_by(self, db: &'db dyn Db, required: Self) -> bool { + required.members(db).any(|required_member| { + let Some(materialized) = self.member_by_name(db, required_member.name()) else { + return false; + }; + let original = ProtocolMember { + name: materialized.name, + data: materialized.data, + materialization: None, + }; + + if materialized + .access(db, ProtocolMemberAccessMode::Instance) + .resolved(db) + != original + .access(db, ProtocolMemberAccessMode::Instance) + .resolved(db) + { + return true; + } + + // Class access to an ordinary instance method requires only that the method + // exists. Its unbound `self` is not part of structural compatibility and can + // recursively refer to this protocol, so do not materialize that signature. + if materialized.is_instance_method() { + return false; + } + + materialized + .access(db, ProtocolMemberAccessMode::Class) + .resolved(db) + != original + .access(db, ProtocolMemberAccessMode::Class) + .resolved(db) + }) + } + + /// Returns the declared instance-write requirement for a protocol member. + /// + /// `None` means that the protocol does not declare `name`; `Some((None, _))` means that the + /// member exists but is read-only. A writable member's requirement is bound to `receiver_ty` + /// before it is returned. + pub(super) fn instance_write_requirement( + self, + db: &'db dyn Db, + receiver_ty: Type<'db>, + name: &str, + ) -> Option<(Option>, TypeQualifiers)> { + self.member_by_name(db, name).map(|member| { + ( + member + .access(db, ProtocolMemberAccessMode::Instance) + .write + .and_then(|write| write.bind_requirement(db, receiver_ty)), + member.qualifiers(), + ) + }) + } + + /// Returns the write requirement exposed through `type[Protocol]` lookup. + /// + /// Only members required on every class object that satisfies the meta-protocol are available. + /// Ordinary instance attributes are required on the constructed object instead. + pub(super) fn meta_write_requirement( + self, + db: &'db dyn Db, + receiver_ty: Type<'db>, + name: &str, + ) -> Option<(Option>, TypeQualifiers)> { + self.member_by_name(db, name).map(|member| { + ( + member + .access(db, ProtocolMemberAccessMode::Class) + .write + .and_then(|write| write.bind_compatibility_type(db, receiver_ty)), + member.qualifiers(), + ) + }) + } + + /// Returns the callable signature exposed by instance access to a protocol's `__call__` + /// method. + /// + /// The callable is already in its instance-bound form, so callers must not bind it again. + pub(super) fn call_method(self, db: &'db dyn Db) -> Option> { + self.member_by_name(db, "__call__").and_then(|member| { + if !member.is_method() { + return None; + } + match member + .access(db, ProtocolMemberAccessMode::Instance) + .read + .and_then(|read| read.resolve(db)) + .map(ProtocolMemberType::ty) + { + Some(Type::Callable(callable)) => Some(callable), + _ => None, + } + }) + } + + pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + self.member_by_name(db, name) + .map(|member| PlaceAndQualifiers { + place: member + .access(db, ProtocolMemberAccessMode::Instance) + .read + .and_then(|read| read.resolve(db)) + .map(|read| Place::bound(read.ty())) + .unwrap_or(Place::Undefined) + .with_provenance(Provenance::from_definition(member.definition())), + qualifiers: member.qualifiers(), + }) + .unwrap_or_else(|| Type::object().member(db, name)) + } + + /// Looks up a member guaranteed to exist on every inhabitant of `type[Protocol]`. + /// + /// Methods retain their unbound signatures and `ClassVar`s retain their class-side types. + /// Properties are only required on the constructed instance, so they are undefined even when + /// the nominal protocol origin provides a property descriptor. + pub(super) fn meta_member( + self, + db: &'db dyn Db, + name: &str, + ) -> Option> { + self.member_by_name(db, name).map(|member| { + let read = member.access(db, ProtocolMemberAccessMode::Class).read; + PlaceAndQualifiers { + place: read + .and_then(|read| read.resolve(db)) + .map(|read| Place::bound(read.ty())) + .unwrap_or(Place::Undefined) + .with_provenance(Provenance::from_definition(member.definition())), + qualifiers: member.qualifiers(), + } + }) + } + + pub(super) fn member_is_property(self, db: &'db dyn Db, name: &str) -> bool { + self.member_by_name(db, name) + .is_some_and(|member| member.is_property()) + } +} + pub(super) fn walk_protocol_interface<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( db: &'db dyn Db, - interface: ProtocolInterface<'db>, + interface: ProtocolInterfaceView<'db>, visitor: &V, ) { for member in interface.members(db) { @@ -323,7 +571,7 @@ pub(super) fn walk_protocol_instance_interface< V: super::visitor::TypeVisitor<'db> + ?Sized, >( db: &'db dyn Db, - interface: ProtocolInterface<'db>, + interface: ProtocolInterfaceView<'db>, receiver_ty: Type<'db>, visitor: &V, ) { @@ -341,6 +589,9 @@ pub(super) fn walk_protocol_instance_member<'db, V: super::visitor::TypeVisitor< ) { match member.data.kind { ProtocolMemberKind::Method(method, _) => { + let method = member + .materialization + .map_or(method, |kind| method.materialize(db, kind)); let Type::Callable(callable) = method.ty() else { visitor.visit_type(db, method.ty()); return; @@ -354,11 +605,12 @@ pub(super) fn walk_protocol_instance_member<'db, V: super::visitor::TypeVisitor< } } } - ProtocolMemberKind::Property { read, write } => { + ProtocolMemberKind::Property { .. } => { + let access = member.access(db, ProtocolMemberAccessMode::Instance); for member_type in [ - read, - write.and_then(ProtocolMemberWrite::domain), - write.and_then(ProtocolMemberWrite::descriptor_type), + access.read, + access.write.and_then(ProtocolMemberWrite::domain), + access.write.and_then(ProtocolMemberWrite::descriptor_type), ] .into_iter() .flatten() @@ -369,6 +621,9 @@ pub(super) fn walk_protocol_instance_member<'db, V: super::visitor::TypeVisitor< } } ProtocolMemberKind::Attribute(attribute) => { + let attribute = member + .materialization + .map_or(attribute, |kind| attribute.materialize(db, kind)); if let Some(ty) = attribute.bind_self(db, receiver_ty) { visitor.visit_type(db, ty); } @@ -443,9 +698,11 @@ impl<'db> ProtocolInterface<'db> { where 'db: 'a, { - self.inner(db) - .iter() - .map(|(name, data)| ProtocolMember { name, data }) + self.inner(db).iter().map(|(name, data)| ProtocolMember { + name, + data, + materialization: None, + }) } pub(super) fn filter_members( @@ -457,7 +714,13 @@ impl<'db> ProtocolInterface<'db> { db, self.inner(db) .iter() - .filter(|&(name, data)| predicate(&ProtocolMember { name, data })) + .filter(|&(name, data)| { + predicate(&ProtocolMember { + name, + data, + materialization: None, + }) + }) .map(|(name, data)| (name.clone(), data.clone())) .collect::>(), ) @@ -473,12 +736,6 @@ impl<'db> ProtocolInterface<'db> { .collect() } - fn member_by_name<'a>(self, db: &'db dyn Db, name: &'a str) -> Option> { - self.inner(db) - .get(name) - .map(|data| ProtocolMember { name, data }) - } - pub(super) fn includes_member(self, db: &'db dyn Db, name: &str) -> bool { self.inner(db).contains_key(name) } @@ -491,8 +748,9 @@ impl<'db> ProtocolInterface<'db> { name: &str, generic_context: GenericContext<'db>, ) -> bool { - self.member_by_name(db, name) - .and_then(|member| member.capabilities(db).instance.write) + self.inner(db) + .get(name) + .and_then(|data| data.capabilities(db).instance.write) .and_then(ProtocolMemberWrite::domain) .and_then(|write| write.resolve(db)) .is_some_and(|write| { @@ -506,112 +764,8 @@ impl<'db> ProtocolInterface<'db> { }) } - /// Returns the declared instance-write requirement for a protocol member. - /// - /// `None` means that the protocol does not declare `name`; `Some((None, _))` means that the - /// member exists but is read-only. A writable member's requirement is bound to `receiver_ty` - /// before it is returned. - pub(super) fn instance_write_requirement( - self, - db: &'db dyn Db, - receiver_ty: Type<'db>, - name: &str, - ) -> Option<(Option>, TypeQualifiers)> { - self.member_by_name(db, name).map(|member| { - let capabilities = member.capabilities(db); - ( - capabilities - .instance - .write - .and_then(|write| write.bind_requirement(db, receiver_ty)), - member.qualifiers(), - ) - }) - } - - /// Returns the write requirement exposed through `type[Protocol]` lookup. - /// - /// Only members required on every class object that satisfies the meta-protocol are available. - /// Ordinary instance attributes are required on the constructed object instead. - pub(super) fn meta_write_requirement( - self, - db: &'db dyn Db, - receiver_ty: Type<'db>, - name: &str, - ) -> Option<(Option>, TypeQualifiers)> { - self.member_by_name(db, name).map(|member| { - ( - member - .capabilities(db) - .class - .write - .and_then(|write| write.bind_compatibility_type(db, receiver_ty)), - member.qualifiers(), - ) - }) - } - - /// Returns the callable signature exposed by instance access to a protocol's `__call__` - /// method. - /// - /// The callable is already in its instance-bound form, so callers must not bind it again. - pub(super) fn call_method(self, db: &'db dyn Db) -> Option> { - self.member_by_name(db, "__call__").and_then(|member| { - if !member.is_method() { - return None; - } - match member - .capabilities(db) - .instance - .read - .and_then(|read| read.resolve(db)) - .map(ProtocolMemberType::ty) - { - Some(Type::Callable(callable)) => Some(callable), - _ => None, - } - }) - } - pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - self.member_by_name(db, name) - .map(|member| { - let capabilities = member.capabilities(db); - PlaceAndQualifiers { - place: capabilities - .instance - .read - .and_then(|read| read.resolve(db)) - .map(|read| Place::bound(read.ty())) - .unwrap_or(Place::Undefined) - .with_provenance(Provenance::from_definition(member.definition())), - qualifiers: member.qualifiers(), - } - }) - .unwrap_or_else(|| Type::object().member(db, name)) - } - - /// Looks up a member guaranteed to exist on every inhabitant of `type[Protocol]`. - /// - /// Methods retain their unbound signatures and `ClassVar`s retain their class-side types. - /// Properties are only required on the constructed instance, so they are undefined even when - /// the nominal protocol origin provides a property descriptor. - pub(super) fn meta_member( - self, - db: &'db dyn Db, - name: &str, - ) -> Option> { - self.member_by_name(db, name).map(|member| { - let read = member.capabilities(db).class.read; - PlaceAndQualifiers { - place: read - .and_then(|read| read.resolve(db)) - .map(|read| Place::bound(read.ty())) - .unwrap_or(Place::Undefined) - .with_provenance(Provenance::from_definition(member.definition())), - qualifiers: member.qualifiers(), - } - }) + ProtocolInterfaceView::new(self, None).instance_member(db, name) } pub(super) fn recursive_type_normalized_impl( @@ -778,6 +932,31 @@ impl<'db> ProtocolMemberWrite<'db> { } } + /// Materialize an exposed write domain in its contravariant position. + /// + /// The descriptor itself remains unchanged so normal descriptor dispatch and deletion + /// continue to use the declaration on the original protocol class. + fn materialize(self, db: &'db dyn Db, kind: MaterializationKind) -> Self { + match self { + Self::Type(member) => Self::Type(member.materialize(db, kind.flip())), + Self::Descriptor { descriptor, domain } => Self::Descriptor { + descriptor, + domain: domain.map(|domain| domain.materialize(db, kind.flip())), + }, + } + } + + /// Resolve accessor representations without changing descriptor identity. + fn resolved(self, db: &'db dyn Db) -> Self { + match self { + Self::Type(member) => Self::Type(member.resolve(db).unwrap_or(member)), + Self::Descriptor { descriptor, domain } => Self::Descriptor { + descriptor: descriptor.resolve(db).unwrap_or(descriptor), + domain: domain.map(|member| member.resolve(db).unwrap_or(member)), + }, + } + } + fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { match (self, previous) { (Self::Type(current), Self::Type(previous)) => { @@ -940,6 +1119,21 @@ impl<'db> ProtocolMemberType<'db> { } } + /// Materialize the value exposed by a member, not the accessor implementing it. + /// + /// In particular, resolving a property setter before materialization prevents its callable + /// parameter from introducing a second contravariant flip. + fn materialize(self, db: &'db dyn Db, kind: MaterializationKind) -> Self { + let Some(resolved) = self.resolve(db) else { + return self; + }; + let ty = match kind { + MaterializationKind::Top => resolved.ty().top_materialization(db), + MaterializationKind::Bottom => resolved.ty().bottom_materialization(db), + }; + resolved.with_ty(ty) + } + /// Resolves this member type and binds member-local `Self` occurrences to `self_type`. fn bind_self(self, db: &'db dyn Db, self_type: Type<'db>) -> Option> { let Self::Value { @@ -1018,6 +1212,21 @@ impl<'db> ProtocolMemberAccess<'db> { Self { read, write } } + fn materialize(self, db: &'db dyn Db, kind: MaterializationKind) -> Self { + Self { + read: self.read.map(|read| read.materialize(db, kind)), + write: self.write.map(|write| write.materialize(db, kind)), + } + } + + /// Resolve readable and writable accessor representations without losing descriptor identity. + fn resolved(self, db: &'db dyn Db) -> Self { + Self { + read: self.read.map(|member| member.resolve(db).unwrap_or(member)), + write: self.write.map(|write| write.resolved(db)), + } + } + fn variances(self, db: &'db dyn Db) -> impl Iterator, TypeVarVariance)> { self.read .and_then(|member| member.resolve(db)) @@ -1043,6 +1252,15 @@ struct ProtocolMemberCapabilities<'db> { class: ProtocolMemberAccess<'db>, } +impl<'db> ProtocolMemberCapabilities<'db> { + fn materialize(self, db: &'db dyn Db, kind: MaterializationKind) -> Self { + Self { + instance: self.instance.materialize(db, kind), + class: self.class.materialize(db, kind), + } + } +} + #[derive(Copy, Clone, Eq, PartialEq)] enum ProtocolMemberAccessMode { Instance, @@ -1402,6 +1620,7 @@ impl<'db> ProtocolMemberKind<'db> { pub(super) struct ProtocolMember<'a, 'db> { name: &'a str, data: &'a ProtocolMemberData<'db>, + materialization: Option, } /// Orders protocol members so that finite constraints are established before recursive relations. @@ -1423,6 +1642,23 @@ fn walk_protocol_member<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( member: &ProtocolMember<'_, 'db>, visitor: &V, ) { + if member.materialization.is_some() { + let capabilities = member.capabilities(db); + for access in [capabilities.instance, capabilities.class] { + for member_type in [ + access.read, + access.write.and_then(ProtocolMemberWrite::domain), + access.write.and_then(ProtocolMemberWrite::descriptor_type), + ] + .into_iter() + .flatten() + { + visitor.visit_type(db, member_type.ty()); + } + } + return; + } + for member_type in member.data.kind.member_types() { visitor.visit_type(db, member_type.ty()); } @@ -1634,38 +1870,52 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { } fn capabilities(&self, db: &'db dyn Db) -> ProtocolMemberCapabilities<'db> { - self.data.capabilities(db) + let capabilities = self.data.capabilities(db); + self.materialization + .map_or(capabilities, |kind| capabilities.materialize(db, kind)) } - /// Returns the accesses that a candidate value must provide for this member. + /// Materialize only the access that an operation actually observes. + /// + /// In particular, an instance-method relation must not materialize the class-side callable: + /// its unbound receiver can recursively refer to the very protocol being compared. + fn access(&self, db: &'db dyn Db, mode: ProtocolMemberAccessMode) -> ProtocolMemberAccess<'db> { + let capabilities = self.data.capabilities(db); + let access = match mode { + ProtocolMemberAccessMode::Instance => capabilities.instance, + ProtocolMemberAccessMode::Class => capabilities.class, + }; + self.materialization + .map_or(access, |kind| access.materialize(db, kind)) + } + + /// Returns the access that a candidate value must provide for this member. /// /// A module-level callable can satisfy an ordinary or static method through direct member /// access. A class object can likewise satisfy a class, static, or ordinary instance method; /// special instance methods instead use special-method lookup through the meta-type. Neither /// case needs a separate class-side check for the same member. - fn implementation_capabilities( + fn implementation_access( &self, db: &'db dyn Db, ty: Type<'db>, - ) -> ProtocolMemberCapabilities<'db> { - let capabilities = self.capabilities(db); - if matches!( - (ty, self.data.kind), - ( - Type::ModuleLiteral(_), - ProtocolMemberKind::Method( - _, - ProtocolMethodKind::Instance | ProtocolMethodKind::Static + mode: ProtocolMemberAccessMode, + ) -> ProtocolMemberAccess<'db> { + if mode == ProtocolMemberAccessMode::Class + && (matches!( + (ty, self.data.kind), + ( + Type::ModuleLiteral(_), + ProtocolMemberKind::Method( + _, + ProtocolMethodKind::Instance | ProtocolMethodKind::Static + ) ) - ) - ) || (is_class_object_type(ty) && self.is_method()) + ) || (is_class_object_type(ty) && self.is_method())) { - ProtocolMemberCapabilities { - class: ProtocolMemberAccess::NONE, - ..capabilities - } + ProtocolMemberAccess::NONE } else { - capabilities + self.access(db, mode) } } } @@ -2511,9 +2761,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ty: Type<'db>, member: &ProtocolMember<'_, 'db>, ) -> ConstraintSet<'db, 'c> { - let capabilities = member.implementation_capabilities(db, ty); + let instance_access = + member.implementation_access(db, ty, ProtocolMemberAccessMode::Instance); if let Some(context) = self.report_context() { - let instance_read_missing = capabilities.instance.read.is_some() + let instance_read_missing = instance_access.read.is_some() && protocol_member_read_type( db, ty, @@ -2522,7 +2773,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ProtocolMemberAccessMode::Instance, ) .is_none(); - let class_read_missing = capabilities.class.read.is_some() + let class_access = + member.implementation_access(db, ty, ProtocolMemberAccessMode::Class); + let class_read_missing = class_access.read.is_some() && !(member.is_instance_method() && member.name == "__call__") && protocol_member_read_type( db, @@ -2554,16 +2807,18 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ty, ty, member, - capabilities.instance, + instance_access, ProtocolMemberAccessMode::Instance, ) .and(db, self.constraints, || { + let class_access = + member.implementation_access(db, ty, ProtocolMemberAccessMode::Class); self.type_satisfies_protocol_member_access( db, ty, ty.to_meta_type(db), member, - capabilities.class, + class_access, ProtocolMemberAccessMode::Class, ) }); @@ -2594,7 +2849,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .interface(db) .members(db) .when_all(db, self.constraints, |member| { - let required = member.capabilities(db).class; + let required = member.access(db, ProtocolMemberAccessMode::Class); if required.read.is_none() && required.write.is_none() { return self.always(); } @@ -2647,8 +2902,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { target_member: &ProtocolMember<'_, 'db>, access: ProtocolMemberAccessMode, ) -> ConstraintSet<'db, 'c> { - let source_capabilities = source_member.capabilities(db); - let target_capabilities = target_member.capabilities(db); + let source = source_member.access(db, access); if access == ProtocolMemberAccessMode::Class && source_member.is_method() @@ -2656,20 +2910,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { { // The instance-side check is authoritative for an ordinary method's signature. Class // access only establishes that the source member is also present on the class. - return ConstraintSet::from_bool( - self.constraints, - source_capabilities.class.read.is_some(), - ); + return ConstraintSet::from_bool(self.constraints, source.read.is_some()); } - - let (source, target) = match access { - ProtocolMemberAccessMode::Instance => { - (source_capabilities.instance, target_capabilities.instance) - } - ProtocolMemberAccessMode::Class => { - (source_capabilities.class, target_capabilities.class) - } - }; + let target = target_member.access(db, access); let read_result = match (source.read, target.read) { (_, None) => self.always(), @@ -2741,8 +2984,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { &self, db: &'db dyn Db, source_type: Type<'db>, - source: ProtocolInterface<'db>, - target: ProtocolInterface<'db>, + source: ProtocolInterfaceView<'db>, + target: ProtocolInterfaceView<'db>, ) -> ConstraintSet<'db, 'c> { if source.member_count(db) < target.member_count(db) && !self.is_context_collection_enabled() @@ -2807,7 +3050,11 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { member: &ProtocolMember<'_, 'db>, ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { - if member.capabilities(db).instance.write.is_none() { + if member + .access(db, ProtocolMemberAccessMode::Instance) + .write + .is_none() + { return self.never(); } @@ -2838,21 +3085,17 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { if member.is_property() && matches!(ty, Type::PropertyInstance(_)) { return self.never(); } - let capabilities = member.capabilities(db); + let access = member.access(db, ProtocolMemberAccessMode::Instance); if !member.is_method() { - capabilities - .instance - .read - .when_some_and(db, self.constraints, |read_ty| { - read_ty - .resolve(db) - .when_some_and(db, self.constraints, |read_ty| { - self.check_type_pair(db, ty, read_ty.ty()) - }) - }) + access.read.when_some_and(db, self.constraints, |read_ty| { + read_ty + .resolve(db) + .when_some_and(db, self.constraints, |read_ty| { + self.check_type_pair(db, ty, read_ty.ty()) + }) + }) } else { - let Some(Type::Callable(method)) = capabilities - .instance + let Some(Type::Callable(method)) = access .read .and_then(|read| read.resolve(db)) .map(ProtocolMemberType::ty) diff --git a/crates/ty_python_semantic/src/types/set_theoretic.rs b/crates/ty_python_semantic/src/types/set_theoretic.rs index cd1d66b655..4704818407 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic.rs @@ -951,7 +951,7 @@ impl<'db> IntersectionType<'db> { if !self.iter_positive(db).any(|positive| { matches!( positive, - Type::ProtocolInstance(protocol) if protocol.class_origin().is_some() + Type::ProtocolInstance(protocol) if protocol.class_origin(db).is_some() ) }) { return None; diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index d214863959..462cfd21f2 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -246,7 +246,7 @@ impl<'db> SubclassOfType<'db> { let class_like = match self.subclass_of.with_transposed_type_var(db) { SubclassOfInner::Class(class) => Type::from(class), SubclassOfInner::Dynamic(dynamic) => Type::Dynamic(dynamic), - SubclassOfInner::Protocol(protocol) => Type::from(*protocol.class_origin()?), + SubclassOfInner::Protocol(protocol) => Type::from(*protocol.class_origin(db)?), SubclassOfInner::TypeVar(bound_typevar) => { match bound_typevar.typevar(db).bound_or_constraints(db) { None => unreachable!(), diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index 72a3754fd7..c48404b969 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -444,7 +444,7 @@ pub(super) fn non_any_dynamic_content<'db>(db: &'db dyn Db, ty: Type<'db>) -> Dy protocol: ProtocolInstanceType<'db>, ) { let protocol_ty = Type::ProtocolInstance(protocol); - let Some(class) = protocol.as_class_based() else { + let Some(class) = protocol.class_origin(db) else { walk_protocol_instance_interface(db, protocol.interface(db), protocol_ty, self); return; }; From c6682d2b52bcb81d9f30783264966e01417f21f6 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 29 Jul 2026 08:05:12 -0700 Subject: [PATCH 142/390] [ty] Don't store `source_order` in BDD nodes (#27176) ## Summary Rebases and continues #26245 on the latest `main`, keeping constraint source ordering in a sidecar instead of TDD nodes and integrating it with current abstraction, type-mapping, and path-traversal code. Normalizes owned source-order sidecars into a dense, canonical representation, preventing irrelevant construction history from affecting Salsa equality or cycle convergence. Brings over the behavior-focused regressions from #27141 (astral-sh/ty#4073), covering union-order-independent callable inference and constraint absorption without adding the shape-cache implementation that the sidecar representation makes unnecessary. Fixes astral-sh/ty#4073 ## Test plan - Adds legacy-TypeVar and PEP 695 mdtests covering callable-return inference across actual/formal union orders, nested tuples, covariant generic members, and gradual `Container[Any]` constraints. - Adds constraint-set regressions covering absorption, compound and partitioned constraints, preserved binding order, distinct solutions, type mapping, and source-ordered sequent initialization across constraint traversals. - Adds owned-constraint-set regression coverage for canonical source-order identity across different construction histories and source-order reuse in compacted overlays. - Adds a Steam corpus reproducer covering the previously observed constraint-cycle panic. - Updates revealed-type ordering expectations for quantification and high-fanout constraints. ### Ecosystem - Reproduces all the same favorable ecosystem changes as #27141, plus a bunch of ordering changes in diagnostics. - There is one regression relative to main, in `static-frame`. This shows up because of a subtle difference in the way constraints are ordered in this PR compared to main -- the sidecar's reconstructed ordering is not always identical to main's effective ordering. That exposes this bug, but the real bug is protocol inference collecting constraints from all overloads without accounting for overlapping-overload precedence. This should be a separate fix. (Or alternately, the real bug is that the constraint solver is not commutative -- this is also out of scope :) ). --------- Co-authored-by: Douglas Creager --- .../recursive_bound_method_source_order.py | 28 + .../mdtest/generics/legacy/functions.md | 62 + .../mdtest/generics/pep695/functions.md | 59 + .../regression/constraint_set_ordering.md | 41 +- .../mdtest/type_properties/quantification.md | 8 +- .../src/types/constraints.rs | 1883 +++++++++-------- 6 files changed, 1227 insertions(+), 854 deletions(-) create mode 100644 crates/ty_python_semantic/resources/corpus/recursive_bound_method_source_order.py diff --git a/crates/ty_python_semantic/resources/corpus/recursive_bound_method_source_order.py b/crates/ty_python_semantic/resources/corpus/recursive_bound_method_source_order.py new file mode 100644 index 0000000000..42ac7392bb --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/recursive_bound_method_source_order.py @@ -0,0 +1,28 @@ +# Regression test for the steam.py ecosystem failure in +# https://github.com/astral-sh/ruff/pull/27176. + +from __future__ import annotations + +from typing import Protocol, TypeVar + + +class PartialApp: + pass + + +AppT = TypeVar("AppT", bound=PartialApp, covariant=True) + + +class BaseOwnedBadge(Protocol[AppT]): + app: AppT + + def __init__(self, app: AppT) -> None: + pass + + async def progress(self: BaseOwnedBadge[PartialApp]) -> None: + pass + + +class FavouriteBadge(BaseOwnedBadge[AppT]): + def __init__(self, app: AppT) -> None: + super().__init__(app) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index f4b677b8ca..2e15f2b689 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -1195,6 +1195,68 @@ class MyCallable: reveal_type(call(MyCallable())) # revealed: int ``` +## Callable return union order does not affect inference + +```py +from typing import Callable, Generic, TypeVar + +T = TypeVar("T") +T_co = TypeVar("T_co", covariant=True) + +class Box(Generic[T_co]): ... + +def ensure_tuple(func: Callable[[], tuple[T, ...] | T]) -> tuple[T, ...]: + raise NotImplementedError + +def ensure_tuple_reversed(func: Callable[[], T | tuple[T, ...]]) -> tuple[T, ...]: + raise NotImplementedError + +def ensure_box(func: Callable[[], Box[T] | T]) -> Box[T]: + raise NotImplementedError + +def ensure_box_reversed(func: Callable[[], T | Box[T]]) -> Box[T]: + raise NotImplementedError + +def check( + scalar_first: Callable[[], str | tuple[str, ...]], + tuple_first: Callable[[], tuple[str, ...] | str], + nested_member_first: Callable[[], Box[str] | tuple[Box[str], ...]], + nested_tuple_first: Callable[[], tuple[Box[str], ...] | Box[str]], + box_scalar_first: Callable[[], str | Box[str]], + box_first: Callable[[], Box[str] | str], +) -> None: + reveal_type(ensure_tuple(scalar_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple(tuple_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple_reversed(scalar_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple_reversed(tuple_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple(nested_member_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_tuple(nested_tuple_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_tuple_reversed(nested_member_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_tuple_reversed(nested_tuple_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_box(box_scalar_first)) # revealed: Box[str] + reveal_type(ensure_box(box_first)) # revealed: Box[str] + reveal_type(ensure_box_reversed(box_scalar_first)) # revealed: Box[str] + reveal_type(ensure_box_reversed(box_first)) # revealed: Box[str] +``` + +## Gradual container constraints preserve inference evidence + +`Collection` inherits from `Container[Any]`, so inferring a type variable from a collection passed +to a contravariant `Container` must preserve the gradual constraint. + +```py +from collections.abc import Container +from typing import Any, TypeVar + +T = TypeVar("T") + +def value(items: Container[T]) -> T: + raise NotImplementedError + +items: list[str] = [] +reveal_type(value(items)) # revealed: Any +``` + ## Passing a constrained TypeVar to a function expecting a compatible constrained TypeVar A constrained TypeVar should be assignable to a different constrained TypeVar if each constraint of diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 17f86e4402..50cb67d32b 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -1222,6 +1222,65 @@ def get_int() -> int | None: ... reveal_type(my_iter(get_int)) # revealed: Box[int] ``` +### Callable return union order does not affect inference + +```py +from typing import Callable + +class Box[T]: + def get(self) -> T: + raise NotImplementedError + +def ensure_tuple[T](func: Callable[[], tuple[T, ...] | T]) -> tuple[T, ...]: + raise NotImplementedError + +def ensure_tuple_reversed[T](func: Callable[[], T | tuple[T, ...]]) -> tuple[T, ...]: + raise NotImplementedError + +def ensure_box[T](func: Callable[[], Box[T] | T]) -> Box[T]: + raise NotImplementedError + +def ensure_box_reversed[T](func: Callable[[], T | Box[T]]) -> Box[T]: + raise NotImplementedError + +def check( + scalar_first: Callable[[], str | tuple[str, ...]], + tuple_first: Callable[[], tuple[str, ...] | str], + nested_member_first: Callable[[], Box[str] | tuple[Box[str], ...]], + nested_tuple_first: Callable[[], tuple[Box[str], ...] | Box[str]], + box_scalar_first: Callable[[], str | Box[str]], + box_first: Callable[[], Box[str] | str], +) -> None: + reveal_type(ensure_tuple(scalar_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple(tuple_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple_reversed(scalar_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple_reversed(tuple_first)) # revealed: tuple[str, ...] + reveal_type(ensure_tuple(nested_member_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_tuple(nested_tuple_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_tuple_reversed(nested_member_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_tuple_reversed(nested_tuple_first)) # revealed: tuple[Box[str], ...] + reveal_type(ensure_box(box_scalar_first)) # revealed: Box[str] + reveal_type(ensure_box(box_first)) # revealed: Box[str] + reveal_type(ensure_box_reversed(box_scalar_first)) # revealed: Box[str] + reveal_type(ensure_box_reversed(box_first)) # revealed: Box[str] +``` + +### Gradual container constraints preserve inference evidence + +`Collection` inherits from `Container[Any]`, so inferring a type variable from a collection passed +to a contravariant `Container` must preserve the gradual constraint. + +```py +from collections.abc import Container +from typing import Any + +def value[T](items: Container[T]) -> T: + raise NotImplementedError + +items: list[str] = [] +reveal_type(value(items)) # revealed: Any +``` + ### Don't include identical lower/upper bounds in type mapping multiple times This is was a performance regression reported in diff --git a/crates/ty_python_semantic/resources/mdtest/regression/constraint_set_ordering.md b/crates/ty_python_semantic/resources/mdtest/regression/constraint_set_ordering.md index 1b402f6a1d..044d66affd 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/constraint_set_ordering.md +++ b/crates/ty_python_semantic/resources/mdtest/regression/constraint_set_ordering.md @@ -23,6 +23,25 @@ the `wobbling-ty-constraint-order` agent skill to automate this process. python-version = "3.13" ``` +## Constraint absorption is independent of source order + +```py +from ty_extensions._internal import ConstraintSet + +def absorption[T]() -> None: + scalar = ConstraintSet.range(str, T, object) + tuple_ = ConstraintSet.range(tuple[str, ...], T, object) + + # revealed: tuple[Solution[T=str]] + reveal_type((scalar & (scalar | tuple_)).solutions_for(T, inferable=tuple[T])) + # revealed: tuple[Solution[T=str]] + reveal_type(((scalar | tuple_) & scalar).solutions_for(T, inferable=tuple[T])) + + # A genuine alternative still produces both solutions; absorption does not prefer one match. + # revealed: tuple[Solution[T=str], Solution[T=tuple[str, ...]]] + reveal_type((scalar | tuple_).solutions_for(T, inferable=tuple[T])) +``` + ## Solution binding order follows constraint source order The order of bindings within a path must follow the first constraint that introduced each typevar. @@ -49,6 +68,16 @@ def bindings_reverse_source[T, U, V]() -> None: constraints = ConstraintSet.range(bytes, V, bytes) & ConstraintSet.range(str, U, str) & ConstraintSet.range(int, T, int) # revealed: tuple[Solution[V=bytes, U=str, T=int]] reveal_type(constraints.solutions(inferable=tuple[T, U, V])) + +def bindings_absorbed[T, U, X]() -> None: + t = ConstraintSet.range(str, T, object) + u = ConstraintSet.range(bytes, U, object) + x = ConstraintSet.range(int, X, object) + + # ((X ≥ int) ∧ (T ≥ str) ∧ (U ≥ bytes)) | ((U ≥ bytes) ∧ (T ≥ str)) + constraints = (x & t & u) | (u & t) + # revealed: tuple[Solution[T=str, U=bytes]] + reveal_type(constraints.solutions(inferable=tuple[T, U, X])) ``` ## Nested transitive constraints and an unrelated alternative @@ -189,11 +218,11 @@ def chain_stu[S, T, U]() -> None: constraints = chain & ConstraintSet.range(int, S, object) & ConstraintSet.range(Never, U, int) # TODO: inferable typevars should not remain in these concrete solutions. # TODO: sometimes: revealed tuple[Solution[S=int | U@chain_stu | T@chain_stu]] - # revealed: tuple[Solution[S=T@chain_stu | int | U@chain_stu]] + # revealed: tuple[Solution[S=int | T@chain_stu | U@chain_stu]] reveal_type(constraints.solutions_for(S, inferable=tuple[S, T, U])) # revealed: tuple[Solution[T=S@chain_stu | int | U@chain_stu]] reveal_type(constraints.solutions_for(T, inferable=tuple[S, T, U])) - # revealed: tuple[Solution[U=T@chain_stu | S@chain_stu | int]] + # revealed: tuple[Solution[U=S@chain_stu | int | T@chain_stu]] reveal_type(constraints.solutions_for(U, inferable=tuple[S, T, U])) def chain_uts[U, T, S]() -> None: @@ -205,11 +234,11 @@ def chain_uts[U, T, S]() -> None: constraints = chain & ConstraintSet.range(int, S, object) & ConstraintSet.range(Never, U, int) # TODO: inferable typevars should not remain in these concrete solutions. # TODO: sometimes: revealed tuple[Solution[S=int | U@chain_uts | T@chain_uts]] - # revealed: tuple[Solution[S=T@chain_uts | int | U@chain_uts]] + # revealed: tuple[Solution[S=int | T@chain_uts | U@chain_uts]] reveal_type(constraints.solutions_for(S, inferable=tuple[S, T, U])) # revealed: tuple[Solution[T=S@chain_uts | int | U@chain_uts]] reveal_type(constraints.solutions_for(T, inferable=tuple[S, T, U])) - # revealed: tuple[Solution[U=T@chain_uts | S@chain_uts | int]] + # revealed: tuple[Solution[U=S@chain_uts | int | T@chain_uts]] reveal_type(constraints.solutions_for(U, inferable=tuple[S, T, U])) ``` @@ -465,7 +494,7 @@ def high_fanout[ # TODO: sometimes: revealed tuple[Solution[P=L0@high_fanout | Literal[0, 1, 2, 5, 6, 7, 8, 9, 10, 11] | L1@high_fanout | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout]] # TODO: sometimes: revealed tuple[Solution[P=L0@high_fanout | Literal[0, 1, 2, 3, 6, 7, 8, 9, 10, 11] | L1@high_fanout | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout]] # TODO: sometimes: revealed tuple[Solution[P=L0@high_fanout | Literal[0, 1, 2, 3, 4, 5, 6, 9, 10, 11] | L1@high_fanout | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout]] - # revealed: tuple[Solution[P=L0@high_fanout | L1@high_fanout | L2@high_fanout | Literal[2, 3, 4, 5, 6, 7, 8, 9, 10, 11] | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout]] + # revealed: tuple[Solution[P=L0@high_fanout | L1@high_fanout | Literal[2, 3, 4, 5, 6, 7, 8, 9, 10, 11] | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout]] reveal_type(pivot) # TODO: sometimes: revealed tuple[Solution[R11=P@high_fanout]] @@ -475,7 +504,7 @@ def high_fanout[ # TODO: sometimes: revealed tuple[Solution[R11=L0@high_fanout | Literal[0, 1, 5, 6, 7, 8, 9, 10, 11] | L1@high_fanout | L2@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout | P@high_fanout]] # TODO: sometimes: revealed tuple[Solution[R11=L0@high_fanout | Literal[0, 1, 2, 3, 6, 7, 8, 9, 10, 11] | L1@high_fanout | L2@high_fanout | L3@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout | P@high_fanout]] # TODO: sometimes: revealed tuple[Solution[R11=L0@high_fanout | Literal[0, 1, 2, 3, 4, 5, 6, 9, 10, 11] | L1@high_fanout | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout | P@high_fanout]] - # revealed: tuple[Solution[R11=L1@high_fanout | L2@high_fanout | Literal[2, 3, 4, 5, 6, 7, 8, 9, 10, 11] | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout | P@high_fanout]] + # revealed: tuple[Solution[R11=L1@high_fanout | Literal[2, 3, 4, 5, 6, 7, 8, 9, 10, 11] | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout | P@high_fanout]] reveal_type(result) impossible = constraints & ConstraintSet.range(Never, R11, Literal[0]) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md b/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md index a6c44bd09a..f4d4c1a1d9 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md @@ -73,7 +73,7 @@ def relational_bridge[X, U, V]() -> None: quantified = body.exists(tuple[X]) # TODO: revealed: tuple[Solution[V=object, U=object]] - # revealed: tuple[Solution[V=U@relational_bridge, U=Never]] + # revealed: tuple[Solution[U=Never, V=U@relational_bridge]] reveal_type(quantified.solutions(inferable=tuple[U, V])) # U ≤ V @@ -105,7 +105,7 @@ def inverse_image[X, A, B]() -> None: quantified = body.exists(tuple[X]) # TODO: revealed: tuple[Solution[A=Invariant[object], B=object, X=object]] - # revealed: tuple[Solution[A=Never, B=X@inverse_image, X=Never]] + # revealed: tuple[Solution[A=Never, X=Never, B=X@inverse_image]] reveal_type(body.solutions(inferable=tuple[X, A, B])) # TODO: revealed: tuple[Solution[A=Invariant[object], B=object]] # revealed: tuple[()] @@ -149,7 +149,7 @@ def witness_sensitive[X, A, B]() -> None: # Each solution for A and B depends on the compatible choice of X. # TODO: revealed: tuple[Solution[X=object, A=object, B=Invariant[object]]] - # revealed: tuple[Solution[X=A@witness_sensitive, A=X@witness_sensitive, B=Invariant[X@witness_sensitive]]] + # revealed: tuple[Solution[A=X@witness_sensitive, X=A@witness_sensitive, B=Invariant[X@witness_sensitive]]] reveal_type(body.solutions(inferable=tuple[X, A, B])) # TODO: revealed: tuple[Solution[A=object, B=Invariant[object]]] # revealed: tuple[()] @@ -201,7 +201,7 @@ def correlated_outputs[X, Y, Z]() -> None: # TODO: revealed: tuple[Solution[X=int, Y=int, Z=Invariant[int]], Solution[X=str, Y=str, Z=Invariant[str]]] # revealed: tuple[Solution[X=int | Y@correlated_outputs, Z=Invariant[X@correlated_outputs] | Invariant[int], Y=int], Solution[X=str | Y@correlated_outputs, Z=Invariant[X@correlated_outputs] | Invariant[str], Y=str]] reveal_type(body.solutions(inferable=tuple[X, Y, Z])) - # revealed: tuple[Solution[Z=Invariant[int], Y=int], Solution[Z=Invariant[str], Y=str]] + # revealed: tuple[Solution[Y=int, Z=Invariant[int]], Solution[Y=str, Z=Invariant[str]]] reveal_type(quantified.solutions(inferable=tuple[Y, Z])) # (Y = int ∧ Z = Invariant[int]) ∨ (Y = str ∧ Z = Invariant[str]) diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 43801c6c5e..db5967de44 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -201,15 +201,15 @@ where builder: &'c ConstraintSetBuilder<'db>, mut f: impl FnMut(T) -> ConstraintSet<'db, 'c>, ) -> ConstraintSet<'db, 'c> { - let node = NodeId::distributed_or( + let (node, source_order) = NodeId::distributed_or( builder, self.map(|element| { let constraint = f(element); constraint.verify_builder(builder); - constraint.node + (constraint.node, constraint.source_order) }), ); - ConstraintSet::from_node(builder, node) + ConstraintSet::from_node(builder, node, source_order) } fn when_all<'db, 'c>( @@ -218,15 +218,15 @@ where builder: &'c ConstraintSetBuilder<'db>, mut f: impl FnMut(T) -> ConstraintSet<'db, 'c>, ) -> ConstraintSet<'db, 'c> { - let node = NodeId::distributed_and( + let (node, source_order) = NodeId::distributed_and( builder, self.map(|element| { let constraint = f(element); constraint.verify_builder(builder); - constraint.node + (constraint.node, constraint.source_order) }), ); - ConstraintSet::from_node(builder, node) + ConstraintSet::from_node(builder, node, source_order) } } @@ -244,6 +244,7 @@ where #[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] pub struct OwnedConstraintSet<'db> { node: NodeId, + source_order: Option, inner: Option>>, } @@ -254,12 +255,16 @@ struct OwnedConstraintSetInner<'db> { typevars: IndexVec>, nodes: Box<[InteriorNodeData]>, node_indices: RankBitBox, + /// A dense, canonical source-order tree whose IDs are independent of sidecar construction + /// history. + source_orders: Box<[SourceOrder]>, } impl Default for OwnedConstraintSet<'_> { fn default() -> Self { Self { node: ALWAYS_FALSE, + source_order: None, inner: None, } } @@ -269,6 +274,7 @@ impl<'db> OwnedConstraintSet<'db> { pub(crate) fn always() -> Self { Self { node: ALWAYS_TRUE, + source_order: None, inner: None, } } @@ -299,7 +305,7 @@ impl<'db> OwnedConstraintSet<'db> { let builder = ConstraintSetBuilder { storage: RefCell::new(storage), }; - let set = ConstraintSet::from_node(&builder, self.node); + let set = ConstraintSet::from_node(&builder, self.node, self.source_order); f(&builder, set) } @@ -351,6 +357,10 @@ pub struct ConstraintSet<'db, 'c> { /// The BDD representing this constraint set node: NodeId, + /// The source ordering of the constraints in this constraint set. Will be `None` for terminal + /// nodes. + source_order: Option, + /// A reference to the builder that holds the storage for this constraint set's BDD builder: &'c ConstraintSetBuilder<'db>, @@ -359,20 +369,25 @@ pub struct ConstraintSet<'db, 'c> { } impl<'db, 'c> ConstraintSet<'db, 'c> { - fn from_node(builder: &'c ConstraintSetBuilder<'db>, node: NodeId) -> Self { + fn from_node( + builder: &'c ConstraintSetBuilder<'db>, + node: NodeId, + source_order: Option, + ) -> Self { Self { node, + source_order, builder, _invariant: PhantomData, } } fn never(builder: &'c ConstraintSetBuilder<'db>) -> Self { - Self::from_node(builder, ALWAYS_FALSE) + Self::from_node(builder, ALWAYS_FALSE, None) } fn always(builder: &'c ConstraintSetBuilder<'db>) -> Self { - Self::from_node(builder, ALWAYS_TRUE) + Self::from_node(builder, ALWAYS_TRUE, None) } pub(crate) fn from_bool(builder: &'c ConstraintSetBuilder<'db>, b: bool) -> Self { @@ -402,10 +417,9 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { lower: Option>, upper: Option>, ) -> Self { - Self::from_node( - builder, - Constraint::new_node_with_bounds(db, builder, typevar, lower, upper), - ) + let (node, source_order) = + Constraint::new_node_with_bounds(db, builder, typevar, lower, upper); + Self::from_node(builder, node, source_order) } /// Returns a constraint set that constrains a typevar to be a supertype of `lower`. @@ -436,7 +450,8 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// Returns whether this constraint set never holds. pub(crate) fn is_never_satisfied(self, db: &'db dyn Db) -> bool { - self.node.is_never_satisfied(db, self.builder) + self.node + .is_never_satisfied(db, self.builder, self.source_order) } /// Returns whether this constraint set is the `never` terminal. @@ -450,7 +465,8 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// Returns whether this constraint set always holds. pub(crate) fn is_always_satisfied(self, db: &'db dyn Db) -> bool { - self.node.is_always_satisfied(db, self.builder) + self.node + .is_always_satisfied(db, self.builder, self.source_order) } /// Returns whether this constraint set is the `always` terminal. @@ -473,7 +489,9 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { rhs: Type<'db>, ) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.implies_subtype_of(db, builder, lhs, rhs)) + let (node, extra_source_order) = self.node.implies_subtype_of(db, builder, lhs, rhs); + let source_order = builder.ordered_source_order(self.source_order, extra_source_order); + Self::from_node(builder, node, source_order) } /// Returns whether this constraint set is satisfied by all of the typevars that it mentions. @@ -497,13 +515,13 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { inferable: TypeVarSet<'db>, ) -> bool { self.verify_builder(builder); - self.node.satisfied_by_all_typevars(db, builder, inferable) + self.node + .satisfied_by_all_typevars(db, builder, inferable, self.source_order) } /// Updates this constraint set to hold the union of itself and another constraint set. /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. + /// In the result's source order, `self` will appear before `other`. pub(crate) fn union( &mut self, _db: &'db dyn Db, @@ -511,14 +529,14 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: Self, ) -> Self { self.verify_builder(builder); - self.node = self.node.or_with_offset(builder, other.node); + self.node = self.node.or(builder, other.node); + self.source_order = builder.ordered_source_order(self.source_order, other.source_order); *self } /// Updates this constraint set to hold the intersection of itself and another constraint set. /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. + /// In the result's source order, `self` will appear before `other`. pub(crate) fn intersect( &mut self, _db: &'db dyn Db, @@ -526,22 +544,22 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: Self, ) -> Self { self.verify_builder(builder); - self.node = self.node.and_with_offset(builder, other.node); + self.node = self.node.and(builder, other.node); + self.source_order = builder.ordered_source_order(self.source_order, other.source_order); *self } /// Returns the negation of this constraint set. pub(crate) fn negate(self, _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.negate(builder)) + Self::from_node(builder, self.node.negate(builder), self.source_order) } /// Returns the intersection of this constraint set and another. The other constraint set is /// provided as a thunk, to implement short-circuiting: the thunk is not forced if the /// constraint set is already saturated. /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. + /// In the result's source order, `self` will appear before `other`. #[inline] pub(crate) fn and( mut self, @@ -562,8 +580,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// as a thunk, to implement short-circuiting: the thunk is not forced if the constraint set is /// already saturated. /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. + /// In the result's source order, `self` will appear before `other`. pub(crate) fn or( mut self, db: &'db dyn Db, @@ -581,8 +598,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// Returns a constraint set encoding that this constraint set implies another. /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. + /// In the result's source order, `self` will appear before `other`. pub(crate) fn implies( self, db: &'db dyn Db, @@ -594,8 +610,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// Returns a constraint set encoding that this constraint set is equivalent to another. /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. + /// In the result's source order, `self` will appear before `other`. pub(crate) fn iff( self, _db: &'db dyn Db, @@ -603,7 +618,9 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: Self, ) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.iff_with_offset(builder, other.node)) + let node = self.node.iff(builder, other.node); + let source_order = builder.ordered_source_order(self.source_order, other.source_order); + Self::from_node(builder, node, source_order) } /// Reduces the set of inferable typevars for this constraint set. You provide the typevars that @@ -617,7 +634,10 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { to_remove: TypeVarSet<'db>, ) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.exists(db, builder, to_remove)) + let (node, derived_source_order) = + self.node.exists(db, builder, to_remove, self.source_order); + let source_order = builder.ordered_source_order(self.source_order, derived_source_order); + Self::from_node(builder, node, source_order) } /// Applies a type mapping to every constraint in this constraint set. @@ -631,7 +651,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { fn rebuild_node( builder: &ConstraintSetBuilder<'_>, old_node: NodeId, - mapped_constraints: &FxHashMap, + mapped_constraints: &FxHashMap)>, mapped_nodes: &mut FxHashMap, ) -> NodeId { if old_node.is_terminal() { @@ -642,8 +662,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { } let old_interior = builder.interior_node_data(old_node); - let condition = mapped_constraints[&old_interior.constraint] - .with_adjusted_source_order(builder, old_interior.source_order.saturating_sub(1)); + let (condition, _) = mapped_constraints[&old_interior.constraint]; let if_true = rebuild_node( builder, old_interior.if_true, @@ -670,7 +689,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { let builder = self.builder; let mut mapped_constraints = FxHashMap::default(); self.node - .for_each_unique_constraint(builder, &mut |constraint_id, _| { + .for_each_unique_constraint(builder, &mut |constraint_id| { if mapped_constraints.contains_key(&constraint_id) { return; } @@ -694,27 +713,46 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { let mapped = if let Type::TypeVar(typevar) = subject { Constraint::new_node_with_bounds(db, builder, typevar, lower, upper) } else { - let lower_holds = lower.map_or(ALWAYS_TRUE, |lower| { - builder - .load( + let lower_holds = lower.map_or_else( + || ConstraintSet::always(builder), + |lower| { + builder.load( db, &lower.when_constraint_set_assignable_to_owned(db, subject), ) - .node - }); - let upper_holds = upper.map_or(ALWAYS_TRUE, |upper| { - builder - .load( + }, + ); + let upper_holds = upper.map_or_else( + || ConstraintSet::always(builder), + |upper| { + builder.load( db, &subject.when_constraint_set_assignable_to_owned(db, upper), ) - .node - }); - lower_holds.and_with_offset(builder, upper_holds) + }, + ); + ( + lower_holds.node.and(builder, upper_holds.node), + builder.ordered_source_order( + lower_holds.source_order, + upper_holds.source_order, + ), + ) }; mapped_constraints.insert(constraint_id, mapped); }); + let source_order = builder + .calculate_source_orders(self.source_order) + .into_iter() + .fold(None, |source_order, constraint| { + mapped_constraints.get(&constraint).map_or( + source_order, + |(_, mapped_source_order)| { + builder.ordered_source_order(source_order, *mapped_source_order) + }, + ) + }); Self::from_node( builder, rebuild_node( @@ -723,6 +761,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { &mapped_constraints, &mut FxHashMap::default(), ), + source_order, ) } @@ -750,13 +789,9 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { // Universal and existential quantification are duals. Reusing existential abstraction // also keeps this operation on its cached, single-pass implementation. - Self::from_node( - builder, - self.node - .negate(builder) - .exists(db, builder, to_remove) - .negate(builder), - ) + self.negate(db, builder) + .reduce_inferable(db, builder, to_remove) + .negate(db, builder) } /// Computes solutions for each BDD path, using a caller-provided hook to select solutions. @@ -787,7 +822,8 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { choose: impl FnMut(TypeVarVariance, &PathBound<'db>) -> Result>, ()>, ) -> Solutions<'db> { self.verify_builder(builder); - self.node.solutions_with(db, builder, inferable, choose) + self.node + .solutions_with(db, builder, inferable, self.source_order, choose) } pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { @@ -843,6 +879,8 @@ pub(crate) struct ConstraintSetBuilder<'db> { storage: RefCell>, } +type ExistsCacheKey<'db> = (NodeId, TypeVarSet<'db>, Option); + #[derive(Debug, Default)] struct ConstraintSetStorage<'db> { /// Compacted owned storage overlaid onto this builder. This is used by @@ -871,6 +909,15 @@ struct ConstraintSetStorage<'db> { /// The BDD nodes that appear in any of the constraint sets constructed in this builder. nodes: IndexVec, + /// Encodes an ordering on the constraints in a constraint set, which is based on the order + /// that the constraints (or more accurately, the Python expressions they're derived from) + /// appear in the source code. This ensures that any union and intersections types that appear + /// in solutions are constructed in a stable (and source-consistent) order. + /// + /// This is encoded as a binary tree over [`ConstraintId`]s. A preorder traversal of that tree + /// defines the ordering. + source_orders: IndexVec, + // Everything below are the memoization tables for the arenas and for our BDD operations. constraint_cache: FxHashMap, ConstraintId>, typevar_cache: FxHashMap, TypeVarId>, @@ -878,15 +925,19 @@ struct ConstraintSetStorage<'db> { /// Avoid repeatedly walking deep constraint bounds without imposing Salsa-query overhead on /// the many shallow bounds that are cheap to walk once. constraint_bound_depth_cache: FxHashMap, + source_order_cache: FxHashMap, constraint_implication_cache: FxHashMap<(ConstraintId, ConstraintId), bool>, /// Only caches completed top-level results. Recursive results depend on active path - /// assignments and must not use this cache. + /// assignments and must not use this cache. A BDD's satisfiability does not depend on the + /// source order used to traverse it. never_satisfied_cache: FxHashMap, negate_cache: FxHashMap, - or_cache: FxHashMap<(NodeId, NodeId, usize), NodeId>, - and_cache: FxHashMap<(NodeId, NodeId, usize), NodeId>, - exists_cache: FxHashMap<(NodeId, TypeVarSet<'db>), NodeId>, + or_cache: FxHashMap<(NodeId, NodeId), NodeId>, + and_cache: FxHashMap<(NodeId, NodeId), NodeId>, + /// Existential abstraction derives new constraints in source order and returns their + /// source-order sidecar, so distinct orderings of the same BDD must not share a cache entry. + exists_cache: FxHashMap, (NodeId, Option)>, restrict_one_cache: FxHashMap<(NodeId, ConstraintAssignment), (NodeId, bool)>, simplify_cache: FxHashMap, @@ -924,6 +975,14 @@ impl ConstraintSetStorage<'_> { .zip(compacted.nodes.iter().copied()) .map(|(old_index, node)| (node, NodeId::from_usize(old_index))), ); + self.source_order_cache.extend( + compacted + .source_orders + .iter() + .copied() + .enumerate() + .map(|(index, source_order)| (source_order, SourceOrderId::from_usize(index))), + ); } fn adjusted_node_id(&self, id: NodeId) -> NodeId { @@ -940,6 +999,13 @@ impl ConstraintSetStorage<'_> { id } + fn adjusted_source_order_id(&self, id: SourceOrderId) -> SourceOrderId { + if let Some(compacted) = &self.compacted { + return id + compacted.source_orders.len(); + } + id + } + fn adjusted_typevar_id(&self, id: TypeVarId) -> TypeVarId { if let Some(compacted) = &self.compacted { return id + compacted.typevars.len(); @@ -966,12 +1032,27 @@ impl<'db> ConstraintSetBuilder<'db> { let constraint = f(&self); let node = constraint.node; if node.is_terminal() { - return OwnedConstraintSet { node, inner: None }; + return OwnedConstraintSet { + node, + source_order: None, + inner: None, + }; } + let source_order = constraint + .source_order + .expect("non-terminal BDD should have source_order"); + + // Combining constraint sets can allocate a new source-order tree even when the BDD is + // unchanged. Preserve each constraint's first source position, but rebuild the persisted + // sidecar densely so redundant combinations cannot affect its IDs or owned-set equality. + // Unlike node and constraint IDs, source-order IDs are not embedded in the BDD, so the + // sidecar can be rebuilt without remapping the BDD. + let source_constraints = self.calculate_source_orders(Some(source_order)); let mut storage = self.storage.into_inner(); let mut used_nodes = RankBitBox::bits_with_capacity(storage.nodes.len()); let mut used_constraints = RankBitBox::bits_with_capacity(storage.constraints.len()); + let mut stack = vec![node]; while let Some(node) = stack.pop() { if node.is_terminal() || used_nodes[node.index()] { @@ -984,6 +1065,22 @@ impl<'db> ConstraintSetBuilder<'db> { stack.push(interior.if_uncertain); stack.push(interior.if_false); } + + let mut source_orders: IndexVec = + IndexVec::with_capacity(source_constraints.len().saturating_mul(2).saturating_sub(1)); + let source_order = source_constraints + .into_iter() + .fold(None, |left, source_constraint| { + used_constraints.set(source_constraint.index(), true); + let right = source_orders.push(SourceOrder::Constraint(source_constraint)); + + Some(match left { + Some(left) => source_orders.push(SourceOrder::Ordered(left, right)), + None => right, + }) + }) + .expect("non-terminal BDD should have source_order"); + used_nodes.truncate(used_nodes.last_one().map_or(0, |last| last + 1)); used_constraints.truncate(used_constraints.last_one().map_or(0, |last| last + 1)); @@ -994,6 +1091,7 @@ impl<'db> ConstraintSetBuilder<'db> { .filter_map(|(node, used)| used.then_some(node)) .collect(); let node_indices = RankBitBox::from_bits(used_nodes); + let constraints = storage .constraints .into_iter() @@ -1001,16 +1099,19 @@ impl<'db> ConstraintSetBuilder<'db> { .filter_map(|(constraint, used)| used.then_some(constraint)) .collect(); let constraint_indices = RankBitBox::from_bits(used_constraints); + storage.typevars.shrink_to_fit(); OwnedConstraintSet { node, + source_order: Some(source_order), inner: Some(Arc::new(OwnedConstraintSetInner { constraints, constraint_indices, typevars: storage.typevars, nodes, node_indices, + source_orders: source_orders.raw.into_boxed_slice(), })), } } @@ -1033,7 +1134,7 @@ impl<'db> ConstraintSetBuilder<'db> { fn rebuild_node<'db>( builder: &ConstraintSetBuilder<'db>, inner: &OwnedConstraintSetInner<'db>, - constraints: &[NodeId], + constraints: &[(NodeId, Option)], cache: &mut FxHashMap, old_node: NodeId, ) -> NodeId { @@ -1055,12 +1156,8 @@ impl<'db> ConstraintSetBuilder<'db> { old_interior.if_uncertain, ); let if_false = rebuild_node(builder, inner, constraints, cache, old_interior.if_false); - // `Constraint::new_node` creates standalone nodes whose source order starts at 1. - // Shift the reloaded condition back to the source order recorded in the owned set; - // solution extraction uses this order for deterministic unions and intersections. let old_constraint_index = inner.retained_constraint_index(old_interior.constraint); - let condition = constraints[old_constraint_index] - .with_adjusted_source_order(builder, old_interior.source_order.saturating_sub(1)); + let (condition, _) = constraints[old_constraint_index]; let remapped = condition.ite_uncertain(builder, if_true, if_uncertain, if_false); cache.insert(old_node, remapped); @@ -1068,34 +1165,13 @@ impl<'db> ConstraintSetBuilder<'db> { } if other.node.is_terminal() { - return ConstraintSet::from_node(self, other.node); + return ConstraintSet::from_node(self, other.node, None); } let inner = other .inner .as_ref() .expect("storage-free owned constraint sets must have terminal roots"); - if inner.nodes.len() == 1 { - let old_interior = inner.nodes[inner.retained_node_index(other.node)]; - let old_constraint = - inner.constraints[inner.retained_constraint_index(old_interior.constraint)]; - let condition = Constraint::new_node_with_bounds( - db, - self, - old_constraint.typevar, - old_constraint.bounds.lower, - old_constraint.bounds.upper, - ) - .with_adjusted_source_order(self, old_interior.source_order.saturating_sub(1)); - let node = condition.ite_uncertain( - self, - old_interior.if_true, - old_interior.if_uncertain, - old_interior.if_false, - ); - return ConstraintSet::from_node(self, node); - } - // Load all of the constraints into the this builder first, to maximize the chance that the // constraints and typevars will appear in the same order. (This is important because many // of our mdtests try to force a particular ordering, to test that our algorithms are all @@ -1114,10 +1190,30 @@ impl<'db> ConstraintSetBuilder<'db> { }) .collect(); + let mut source_orders = vec![None; inner.source_orders.len()]; + for (i, old_source_order) in inner.source_orders.iter().copied().enumerate() { + match old_source_order { + SourceOrder::Ordered(old_left, old_right) => { + let new_left = source_orders[old_left.index()]; + let new_right = source_orders[old_right.index()]; + source_orders[i] = self.ordered_source_order(new_left, new_right); + } + SourceOrder::Constraint(old_constraint) => { + let old_constraint_index = inner.retained_constraint_index(old_constraint); + let (_, constraint_source_order) = constraints[old_constraint_index]; + source_orders[i] = constraint_source_order; + } + } + } + // Maps NodeIds in the OwnedConstraintSet to the corresponding NodeIds in this builder. let mut cache = FxHashMap::default(); let node = rebuild_node(self, inner, &constraints, &mut cache, other.node); - ConstraintSet::from_node(self, node) + let old_source_order = other + .source_order + .expect("non-terminal constraint set should have a source_order"); + let source_order = source_orders[old_source_order.index()]; + ConstraintSet::from_node(self, node, source_order) } /// Interns a single typevar, giving it a stable order in this builder @@ -1337,6 +1433,79 @@ impl<'db> ConstraintSetBuilder<'db> { } storage.nodes[node] } + + fn intern_source_order(&self, data: SourceOrder) -> SourceOrderId { + let mut storage = self.storage.borrow_mut(); + storage.ensure_overlay_identity_caches(); + if let Some(id) = storage.source_order_cache.get(&data) { + return *id; + } + let id = storage.source_orders.push(data); + let id = storage.adjusted_source_order_id(id); + storage.source_order_cache.insert(data, id); + id + } + + /// Repeating a source-order tree cannot change the first occurrence of any constraint, so + /// combining identical trees must reuse their existing sidecar. + fn ordered_source_order( + &self, + left: Option, + right: Option, + ) -> Option { + match (left, right) { + (None, None) => None, + (None, other) | (other, None) => other, + (Some(left), Some(right)) if left == right => Some(left), + (Some(left), Some(right)) => { + Some(self.intern_source_order(SourceOrder::Ordered(left, right))) + } + } + } + + fn constraint_source_order(&self, constraint: ConstraintId) -> SourceOrderId { + self.intern_source_order(SourceOrder::Constraint(constraint)) + } + + fn source_order_data(&self, source_order: SourceOrderId) -> SourceOrder { + let storage = self.storage.borrow(); + if let Some(compacted) = &storage.compacted { + let index = source_order.index(); + let split = compacted.source_orders.len(); + if index < split { + return compacted.source_orders[index]; + } + return storage.source_orders[SourceOrderId::from_usize(index - split)]; + } + storage.source_orders[source_order] + } + + fn calculate_source_orders( + &self, + source_order: Option, + ) -> FxIndexSet { + fn walk( + builder: &ConstraintSetBuilder, + current: SourceOrderId, + result: &mut FxIndexSet, + ) { + match builder.source_order_data(current) { + SourceOrder::Ordered(left, right) => { + walk(builder, left, result); + walk(builder, right, result); + } + SourceOrder::Constraint(constraint) => { + result.insert(constraint); + } + } + } + + let mut result = FxIndexSet::default(); + if let Some(source_order) = source_order { + walk(self, source_order, &mut result); + } + result + } } impl<'db> BoundTypeVarInstance<'db> { @@ -1420,6 +1589,17 @@ pub struct TypeVarId; #[derive(get_size2::GetSize)] pub struct ConstraintId; +#[newtype_index] +#[derive(get_size2::GetSize)] +struct SourceOrderId; + +/// The nodes of the tree that defines source ordering for a constraint set. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] +enum SourceOrder { + Ordered(SourceOrderId, SourceOrderId), + Constraint(ConstraintId), +} + /// An individual constraint in a constraint set. This restricts a single typevar to be within a /// lower and upper bound. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] @@ -1724,19 +1904,6 @@ impl<'db> Constraint<'db> { keeps_lower || keeps_upper } - /// Returns a new range constraint. - /// - /// Panics if `lower` and `upper` are not both fully static. - fn new_node( - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - typevar: BoundTypeVarInstance<'db>, - lower: Type<'db>, - upper: Type<'db>, - ) -> NodeId { - Self::new_node_with_bounds(db, builder, typevar, Some(lower), Some(upper)) - } - /// Returns a new range constraint, preserving whether each bound was present explicitly. /// /// Panics if present `lower` and `upper` bounds are not fully static. @@ -1746,9 +1913,9 @@ impl<'db> Constraint<'db> { typevar: BoundTypeVarInstance<'db>, mut lower: Option>, mut upper: Option>, - ) -> NodeId { + ) -> (NodeId, Option) { if lower.is_none() && upper.is_none() { - return ALWAYS_TRUE; + return (ALWAYS_TRUE, None); } // It's not useful for an upper bound to be an intersection type, or for a lower bound to @@ -1761,19 +1928,19 @@ impl<'db> Constraint<'db> { // (α | β) ≤ T ⇔ (α ≤ T) ∧ (β ≤ T) if let Some(Type::Union(lower_union)) = lower { let mut result = ALWAYS_TRUE; + let mut source_order = None; for lower_element in lower_union.elements(db) { - result = result.and_with_offset( + let (element_node, element_source_order) = Constraint::new_node_with_bounds( + db, builder, - Constraint::new_node_with_bounds( - db, - builder, - typevar, - Some(*lower_element), - upper, - ), + typevar, + Some(*lower_element), + upper, ); + result = result.and(builder, element_node); + source_order = builder.ordered_source_order(source_order, element_source_order); } - return result; + return (result, source_order); } // A negated type ¬α is represented as an intersection with no positive elements, and a // single negative element. We _don't_ want to treat that an "intersection" for the @@ -1782,31 +1949,30 @@ impl<'db> Constraint<'db> { && !upper_intersection.is_simple_negation(db) { let mut result = ALWAYS_TRUE; + let mut source_order = None; for upper_element in upper_intersection.iter_positive(db) { - result = result.and_with_offset( + let (element_node, element_source_order) = Constraint::new_node_with_bounds( + db, builder, - Constraint::new_node_with_bounds( - db, - builder, - typevar, - lower, - Some(upper_element), - ), + typevar, + lower, + Some(upper_element), ); + result = result.and(builder, element_node); + source_order = builder.ordered_source_order(source_order, element_source_order); } for upper_element in upper_intersection.iter_negative(db) { - result = result.and_with_offset( + let (element_node, element_source_order) = Constraint::new_node_with_bounds( + db, builder, - Constraint::new_node_with_bounds( - db, - builder, - typevar, - lower, - Some(upper_element.negate(db)), - ), + typevar, + lower, + Some(upper_element.negate(db)), ); + result = result.and(builder, element_node); + source_order = builder.ordered_source_order(source_order, element_source_order); } - return result; + return (result, source_order); } // Two identical typevars must always solve to the same type, so it is not useful to have @@ -1833,12 +1999,11 @@ impl<'db> Constraint<'db> { }) }) => { - return Node::new_constraint( - builder, - ConstraintId::new(db, builder, typevar, Type::Never, Type::object()), - 1, - ) - .negate(builder); + let constraint = + ConstraintId::new(db, builder, typevar, Type::Never, Type::object()); + let (node, source_order) = Node::new_constraint(builder, constraint); + let node = node.negate(builder); + return (node, source_order); } _ => {} } @@ -1872,7 +2037,7 @@ impl<'db> Constraint<'db> { let when = effective_lower.when_constraint_set_assignable_to_owned(db, effective_upper); let is_never_satisfied = when.query(|_builder, when| when.is_never_satisfied(db)); if is_never_satisfied { - return ALWAYS_FALSE; + return (ALWAYS_FALSE, None); } // We have an (arbitrary) ordering for typevars. If the upper and/or lower bounds are @@ -1889,17 +2054,14 @@ impl<'db> Constraint<'db> { } else { (typevar, lower) }; - Node::new_constraint( + let constraint = ConstraintId::new( + db, builder, - ConstraintId::new( - db, - builder, - typevar, - Type::TypeVar(bound), - Type::TypeVar(bound), - ), - 1, - ) + typevar, + Type::TypeVar(bound), + Type::TypeVar(bound), + ); + Node::new_constraint(builder, constraint) } // L ≤ T ≤ U == ([L] ≤ T) && (T ≤ [U]) @@ -1907,78 +2069,78 @@ impl<'db> Constraint<'db> { if typevar.can_be_bound_for(db, builder, lower) && typevar.can_be_bound_for(db, builder, upper) => { - let lower = Node::new_constraint( + let lower_constraint = ConstraintId::new_with_bounds( + db, builder, - ConstraintId::new_with_bounds( - db, - builder, - lower, - None, - Some(Type::TypeVar(typevar)), - ), - 1, + lower, + None, + Some(Type::TypeVar(typevar)), ); - let upper = Node::new_constraint( + let (lower_node, lower_source_order) = + Node::new_constraint(builder, lower_constraint); + let upper_constraint = ConstraintId::new_with_bounds( + db, builder, - ConstraintId::new_with_bounds( - db, - builder, - upper, - Some(Type::TypeVar(typevar)), - None, - ), - 1, + upper, + Some(Type::TypeVar(typevar)), + None, ); - lower.and(builder, upper) + let (upper_node, upper_source_order) = + Node::new_constraint(builder, upper_constraint); + let node = lower_node.and(builder, upper_node); + let source_order = + builder.ordered_source_order(lower_source_order, upper_source_order); + (node, source_order) } // L ≤ T ≤ U == ([L] ≤ T) && ([T] ≤ U) (Type::TypeVar(lower), _) if typevar.can_be_bound_for(db, builder, lower) => { - let lower = Node::new_constraint( + let lower_constraint = ConstraintId::new_with_bounds( + db, builder, - ConstraintId::new_with_bounds( - db, - builder, - lower, - None, - Some(Type::TypeVar(typevar)), - ), - 1, + lower, + None, + Some(Type::TypeVar(typevar)), ); - let upper = if upper.is_none() { - ALWAYS_TRUE + let (lower_node, lower_source_order) = + Node::new_constraint(builder, lower_constraint); + let (upper_node, upper_source_order) = if upper.is_none() { + (ALWAYS_TRUE, None) } else { Constraint::new_node_with_bounds(db, builder, typevar, None, upper) }; - lower.and(builder, upper) + let node = lower_node.and(builder, upper_node); + let source_order = + builder.ordered_source_order(lower_source_order, upper_source_order); + (node, source_order) } // L ≤ T ≤ U == (L ≤ [T]) && (T ≤ [U]) (_, Type::TypeVar(upper)) if typevar.can_be_bound_for(db, builder, upper) => { - let lower = if lower.is_none() { - ALWAYS_TRUE + let (lower_node, lower_source_order) = if lower.is_none() { + (ALWAYS_TRUE, None) } else { Constraint::new_node_with_bounds(db, builder, typevar, lower, None) }; - let upper = Node::new_constraint( + let upper_constraint = ConstraintId::new_with_bounds( + db, builder, - ConstraintId::new_with_bounds( - db, - builder, - upper, - Some(Type::TypeVar(typevar)), - None, - ), - 1, + upper, + Some(Type::TypeVar(typevar)), + None, ); - lower.and(builder, upper) + let (upper_node, upper_source_order) = + Node::new_constraint(builder, upper_constraint); + let node = lower_node.and(builder, upper_node); + let source_order = + builder.ordered_source_order(lower_source_order, upper_source_order); + (node, source_order) } - _ => Node::new_constraint( - builder, - ConstraintId::new_with_bounds(db, builder, typevar, lower, upper), - 1, - ), + _ => { + let constraint = ConstraintId::new_with_bounds(db, builder, typevar, lower, upper); + Node::new_constraint(builder, constraint) + } } } } @@ -2167,16 +2329,8 @@ impl NodeId { constraint: ConstraintId, if_true: NodeId, if_false: NodeId, - source_order: usize, ) -> NodeId { - Self::with_uncertain( - builder, - constraint, - if_true, - ALWAYS_FALSE, - if_false, - source_order, - ) + Self::with_uncertain(builder, constraint, if_true, ALWAYS_FALSE, if_false) } /// Creates a new TDD node with an explicit `if_uncertain` branch, applying local reductions. @@ -2186,7 +2340,6 @@ impl NodeId { if_true: NodeId, if_uncertain: NodeId, if_false: NodeId, - source_order: usize, ) -> NodeId { debug_assert!( if_true @@ -2238,17 +2391,11 @@ impl NodeId { return if_uncertain; } - let max_source_order = source_order - .max(if_true.max_source_order(builder)) - .max(if_uncertain.max_source_order(builder)) - .max(if_false.max_source_order(builder)); builder.intern_interior_node(InteriorNodeData { constraint, if_true, if_uncertain, if_false, - source_order, - max_source_order, }) } } @@ -2259,15 +2406,10 @@ impl Node { fn new_constraint( builder: &ConstraintSetBuilder<'_>, constraint: ConstraintId, - source_order: usize, - ) -> NodeId { - NodeId::with_uncertain( - builder, - constraint, - ALWAYS_TRUE, - ALWAYS_FALSE, - ALWAYS_FALSE, - source_order, + ) -> (NodeId, Option) { + ( + NodeId::with_uncertain(builder, constraint, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_FALSE), + Some(builder.constraint_source_order(constraint)), ) } @@ -2279,40 +2421,24 @@ impl Node { fn new_satisfied_constraint( builder: &ConstraintSetBuilder<'_>, constraint: ConstraintAssignment, - source_order: usize, - ) -> NodeId { - match constraint { - ConstraintAssignment::Positive(constraint) => NodeId::with_uncertain( - builder, - constraint, - ALWAYS_TRUE, - ALWAYS_FALSE, - ALWAYS_FALSE, - source_order, - ), - ConstraintAssignment::Negative(constraint) => NodeId::with_uncertain( - builder, - constraint, - ALWAYS_FALSE, - ALWAYS_FALSE, - ALWAYS_TRUE, - source_order, - ), + ) -> (NodeId, Option) { + let constraint_id = constraint.constraint(); + let node = match constraint { + ConstraintAssignment::Positive(constraint) => { + NodeId::with_uncertain(builder, constraint, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_FALSE) + } + ConstraintAssignment::Negative(constraint) => { + NodeId::with_uncertain(builder, constraint, ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_TRUE) + } + // The result holds regardless of the constraint's truth value, so only + // `if_uncertain` needs to be `ALWAYS_TRUE` — `n? 0: 1: 0`. It would also be + // correct to use `n? 1: 1: 1` (i.e., `ALWAYS_TRUE` for all outgoing edges), but + // that would throw away some of the efficiency gains this representation gives us. ConstraintAssignment::Unconstrained(constraint) => { - // The result holds regardless of the constraint's truth value, so only - // `if_uncertain` needs to be `ALWAYS_TRUE` — `n? 0: 1: 0`. It would also be - // correct to use `n? 1: 1: 1` (i.e., `ALWAYS_TRUE` for all outgoing edges), but - // that would throw away some of the efficiency gains this representation gives us. - NodeId::with_uncertain( - builder, - constraint, - ALWAYS_FALSE, - ALWAYS_TRUE, - ALWAYS_FALSE, - source_order, - ) + NodeId::with_uncertain(builder, constraint, ALWAYS_FALSE, ALWAYS_TRUE, ALWAYS_FALSE) } - } + }; + (node, Some(builder.constraint_source_order(constraint_id))) } } @@ -2347,37 +2473,6 @@ impl NodeId { Some(interior.constraint) } - fn max_source_order(self, builder: &ConstraintSetBuilder<'_>) -> usize { - if self.is_terminal() { - return 0; - } - let interior = builder.interior_node_data(self); - interior.max_source_order - } - - /// Returns a copy of this BDD node with all `source_order`s adjusted by the given amount. - fn with_adjusted_source_order(self, builder: &ConstraintSetBuilder<'_>, delta: usize) -> Self { - if delta == 0 { - return self; - } - match self.node() { - Node::AlwaysTrue | Node::AlwaysFalse => self, - Node::Interior(_) => { - let interior = builder.interior_node_data(self); - NodeId::with_uncertain( - builder, - interior.constraint, - interior.if_true.with_adjusted_source_order(builder, delta), - interior - .if_uncertain - .with_adjusted_source_order(builder, delta), - interior.if_false.with_adjusted_source_order(builder, delta), - interior.source_order + delta, - ) - } - } - } - /// Checks whether this BDD represents a single conjunction (of an arbitrary number of /// positive or negative constraints). fn is_single_conjunction(self, builder: &ConstraintSetBuilder<'_>) -> bool { @@ -2428,12 +2523,13 @@ impl NodeId { self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, + source_order: Option, ) -> bool { match self.node() { Node::AlwaysTrue => true, Node::AlwaysFalse => false, Node::Interior(interior) => { - let mut path = interior.path_assignments(builder); + let mut path = interior.path_assignments(builder, source_order); path.visit_negated(db, builder, self, &mut IsNeverSatisfiedVisitor) .is_continue() } @@ -2441,7 +2537,12 @@ impl NodeId { } /// Returns whether this BDD represent the constant function `false`. - fn is_never_satisfied<'db>(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> bool { + fn is_never_satisfied<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + source_order: Option, + ) -> bool { match self.node() { Node::AlwaysTrue => false, Node::AlwaysFalse => true, @@ -2450,7 +2551,7 @@ impl NodeId { return *result; } - let mut path = interior.path_assignments(builder); + let mut path = interior.path_assignments(builder, source_order); let result = path .visit(db, builder, self, &mut IsNeverSatisfiedVisitor) .is_continue(); @@ -2469,9 +2570,10 @@ impl NodeId { db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, + source_order: Option, choose: impl FnMut(TypeVarVariance, &PathBound<'db>) -> Result>, ()>, ) -> Solutions<'db> { - let path_bounds = PathBounds::compute(db, builder, self, inferable); + let path_bounds = PathBounds::compute(db, builder, self, inferable, source_order); path_bounds.solve_with(choose) } @@ -2485,63 +2587,13 @@ impl NodeId { } /// Returns the `or` or union of two BDDs. - /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. - fn or_with_offset(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { - // To ensure that `self` appears before `other` in `source_order`, we add the maximum - // `source_order` of the lhs to all of the `source_order`s in the rhs. - // - // TODO: If we store `other_offset` as a new field on InteriorNode, we might be able to - // avoid all of the extra work in the calls to with_adjusted_source_order, and apply the - // adjustment lazily when walking a BDD tree. (ditto below in the other _with_offset - // methods) - let other_offset = self.max_source_order(builder); - self.or_inner(builder, other, other_offset) - } - fn or(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { - self.or_inner(builder, other, 0) - } - - fn or_inner( - self, - builder: &ConstraintSetBuilder<'_>, - other: Self, - other_offset: usize, - ) -> Self { match (self.node(), other.node()) { - (Node::AlwaysTrue, Node::AlwaysTrue) => ALWAYS_TRUE, - (Node::AlwaysTrue, Node::Interior(_)) => { - let other_interior = builder.interior_node_data(other); - // If lhs is always true, then the overall result is true for any assignment of - // rhs. - NodeId::with_uncertain( - builder, - other_interior.constraint, - ALWAYS_FALSE, - ALWAYS_TRUE, - ALWAYS_FALSE, - other_interior.source_order + other_offset, - ) - } - (Node::Interior(_), Node::AlwaysTrue) => { - let self_interior = builder.interior_node_data(self); - // If rhs is always true, then the overall result is true for any assignment of - // lhs. - NodeId::with_uncertain( - builder, - self_interior.constraint, - ALWAYS_FALSE, - ALWAYS_TRUE, - ALWAYS_FALSE, - self_interior.source_order, - ) - } - (Node::AlwaysFalse, _) => other.with_adjusted_source_order(builder, other_offset), + (Node::AlwaysTrue, _) | (_, Node::AlwaysTrue) => ALWAYS_TRUE, + (Node::AlwaysFalse, _) => other, (_, Node::AlwaysFalse) => self, (Node::Interior(self_interior), Node::Interior(other_interior)) => { - self_interior.or(builder, other_interior, other_offset) + self_interior.or(builder, other_interior) } } } @@ -2566,11 +2618,11 @@ impl NodeId { /// intermediate result is the "one" terminal, we can return early. fn tree_fold( builder: &ConstraintSetBuilder<'_>, - nodes: impl Iterator, + nodes: impl Iterator)>, zero: Self, one: Self, mut combine: impl FnMut(Self, &ConstraintSetBuilder<'_>, Self) -> Self, - ) -> Self { + ) -> (Self, Option) { // To implement the "linear" shape described above, we could collect the iterator elements // into a vector, and then use the fold at the bottom of this method to combine the // elements using the operator. @@ -2595,108 +2647,66 @@ impl NodeId { // // We use a SmallVec for the accumulator so that we don't have to spill over to the heap // until the iterator passes 256 elements. - let mut accumulator: SmallVec<[(NodeId, u8); 8]> = SmallVec::default(); - for node in nodes { + let mut accumulator: SmallVec<[(NodeId, Option, u8); 8]> = + SmallVec::default(); + for (node, source_order) in nodes { if node == one { - return node; + return (node, source_order); } - let (mut node, mut depth) = (node, 0); + let (mut node, mut source_order, mut depth) = (node, source_order, 0); while accumulator .last() - .is_some_and(|(_, existing)| *existing == depth) + .is_some_and(|(_, _, existing)| *existing == depth) { - let (existing, _) = accumulator.pop().expect("accumulator should not be empty"); - node = combine(existing, builder, node); + let (existing_node, existing_source_order, _) = + accumulator.pop().expect("accumulator should not be empty"); + node = combine(existing_node, builder, node); + source_order = builder.ordered_source_order(existing_source_order, source_order); if node == one { - return node; + return (node, source_order); } depth += 1; } - accumulator.push((node, depth)); + accumulator.push((node, source_order, depth)); } // At this point, we've consumed all of the iterator. The length of the accumulator will be // the same as the number of 1 bits in the length of the iterator. We do a final fold to // produce the overall result. - accumulator - .into_iter() - .fold(zero, |result, (node, _)| combine(result, builder, node)) + accumulator.into_iter().fold( + (zero, None), + |(result_node, result_source_order), (node, source_order, _)| { + ( + combine(result_node, builder, node), + builder.ordered_source_order(result_source_order, source_order), + ) + }, + ) } fn distributed_or( builder: &ConstraintSetBuilder<'_>, - nodes: impl Iterator, - ) -> Self { - Self::tree_fold( - builder, - nodes, - ALWAYS_FALSE, - ALWAYS_TRUE, - Self::or_with_offset, - ) + nodes: impl Iterator)>, + ) -> (Self, Option) { + Self::tree_fold(builder, nodes, ALWAYS_FALSE, ALWAYS_TRUE, Self::or) } fn distributed_and( builder: &ConstraintSetBuilder<'_>, - nodes: impl Iterator, - ) -> Self { - Self::tree_fold( - builder, - nodes, - ALWAYS_TRUE, - ALWAYS_FALSE, - Self::and_with_offset, - ) + nodes: impl Iterator)>, + ) -> (Self, Option) { + Self::tree_fold(builder, nodes, ALWAYS_TRUE, ALWAYS_FALSE, Self::and) } /// Returns the `and` or intersection of two BDDs. - /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. - fn and_with_offset(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { - // To ensure that `self` appears before `other` in `source_order`, we add the maximum - // `source_order` of the lhs to all of the `source_order`s in the rhs. - let other_offset = self.max_source_order(builder); - self.and_inner(builder, other, other_offset) - } - fn and(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { - self.and_inner(builder, other, 0) - } - - fn and_inner( - self, - builder: &ConstraintSetBuilder<'_>, - other: Self, - other_offset: usize, - ) -> Self { match (self.node(), other.node()) { - (Node::AlwaysFalse, Node::AlwaysFalse) => ALWAYS_FALSE, - (Node::AlwaysFalse, Node::Interior(_)) => { - let other_interior = builder.interior_node_data(other); - NodeId::new( - builder, - other_interior.constraint, - ALWAYS_FALSE, - ALWAYS_FALSE, - other_interior.source_order + other_offset, - ) - } - (Node::Interior(_), Node::AlwaysFalse) => { - let self_interior = builder.interior_node_data(self); - NodeId::new( - builder, - self_interior.constraint, - ALWAYS_FALSE, - ALWAYS_FALSE, - self_interior.source_order, - ) - } - (Node::AlwaysTrue, _) => other.with_adjusted_source_order(builder, other_offset), + (Node::AlwaysFalse, _) | (_, Node::AlwaysFalse) => ALWAYS_FALSE, + (Node::AlwaysTrue, _) => other, (_, Node::AlwaysTrue) => self, (Node::Interior(self_interior), Node::Interior(other_interior)) => { - self_interior.and(builder, other_interior, other_offset) + self_interior.and(builder, other_interior) } } } @@ -2708,31 +2718,10 @@ impl NodeId { /// Returns a new BDD that evaluates to `true` when both input BDDs evaluate to the same /// result. - /// - /// In the result, `self` will appear before `other` according to the `source_order` of the BDD - /// nodes. - fn iff_with_offset(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { - // To ensure that `self` appears before `other` in `source_order`, we add the maximum - // `source_order` of the lhs to all of the `source_order`s in the rhs. - let other_offset = self.max_source_order(builder); - self.iff_inner(builder, other, other_offset) - } - fn iff(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { - self.iff_inner(builder, other, 0) - } - - fn iff_inner( - self, - builder: &ConstraintSetBuilder<'_>, - other: Self, - other_offset: usize, - ) -> Self { // iff(a, b) = (a ∧ b) ∨ (¬a ∧ ¬b) - let a_and_b = self.and_inner(builder, other, other_offset); - let not_a_and_not_b = - self.negate(builder) - .and_inner(builder, other.negate(builder), other_offset); + let a_and_b = self.and(builder, other); + let not_a_and_not_b = self.negate(builder).and(builder, other.negate(builder)); a_and_b.or(builder, not_a_and_not_b) } @@ -2784,7 +2773,6 @@ impl NodeId { then_node, uncertain_node, else_node, - interior.source_order, ); } @@ -2804,7 +2792,7 @@ impl NodeId { builder: &ConstraintSetBuilder<'db>, lhs: Type<'db>, rhs: Type<'db>, - ) -> Self { + ) -> (Self, Option) { // When checking subtyping involving a typevar, we can turn the subtyping check into a // constraint (i.e, "is `T` a subtype of `int` becomes the constraint `T ≤ int`), and then // check when the BDD implies that constraint. @@ -2813,7 +2801,7 @@ impl NodeId { // these types are coming in from arbitrary subtyping checks that the caller might want to // perform. So we have to take the appropriate materialization when translating the check // into a constraint. - let constraint = match (lhs, rhs) { + let (constraint, constraint_source_order) = match (lhs, rhs) { (Type::TypeVar(bound_typevar), _) => Constraint::new_node_with_bounds( db, builder, @@ -2831,14 +2819,16 @@ impl NodeId { _ => panic!("at least one type should be a typevar"), }; - self.implies(builder, constraint) + let node = self.implies(builder, constraint); + (node, constraint_source_order) } - fn satisfied_by_all_typevars<'db>( + fn satisfied_by_all_typevars<'db, 'c>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + builder: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, + source_order: Option, ) -> bool { match self.node() { Node::AlwaysTrue => return true, @@ -2847,27 +2837,34 @@ impl NodeId { } let mut typevars = FxHashSet::default(); - self.for_each_unique_constraint(builder, &mut |constraint, _| { + self.for_each_unique_constraint(builder, &mut |constraint| { let constraint = builder.constraint_data(constraint); typevars.insert(constraint.typevar); }); + // Specializations can introduce constraints that do not appear in the original BDD. + // Compose full constraint sets so those constraints retain their source orders when the + // resulting BDD is traversed. + let original = ConstraintSet::from_node(builder, self, source_order); + // Returns if some specialization satisfies this constraint set. - let some_specialization_satisfies = move |specializations: NodeId| { - let when_satisfied = specializations - .implies(builder, self) - .and(builder, specializations); - !when_satisfied.is_never_satisfied(db, builder) + let some_specialization_satisfies = move |specializations: ConstraintSet<'db, 'c>| { + let when_satisfied = + specializations + .implies(db, builder, || original) + .and(db, builder, || specializations); + !when_satisfied.is_never_satisfied(db) }; // Returns if all specializations satisfy this constraint set. - let all_specializations_satisfy = move |specializations: NodeId| { - let when_satisfied = specializations - .implies(builder, self) - .and(builder, specializations); + let all_specializations_satisfy = move |specializations: ConstraintSet<'db, 'c>| { + let when_satisfied = + specializations + .implies(db, builder, || original) + .and(db, builder, || specializations); when_satisfied - .iff(builder, specializations) - .is_always_satisfied(db, builder) + .iff(db, builder, specializations) + .is_always_satisfied(db) }; #[expect( @@ -2918,23 +2915,24 @@ impl NodeId { db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, bound_typevars: TypeVarSet<'db>, - ) -> Self { + source_order: Option, + ) -> (Self, Option) { if bound_typevars == TypeVarSet::None { - return self; + return (self, None); } let Node::Interior(interior) = self.node() else { - return self; + return (self, None); }; - let key = (self, bound_typevars); + let key = (self, bound_typevars, source_order); let storage = builder.storage.borrow(); if let Some(result) = storage.exists_cache.get(&key) { return *result; } drop(storage); - let result = interior.exists_inner(db, builder, bound_typevars); + let result = interior.exists_inner(db, builder, bound_typevars, source_order); let mut storage = builder.storage.borrow_mut(); storage.exists_cache.insert(key, result); @@ -2946,11 +2944,14 @@ impl NodeId { db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, - ) -> Self { + source_order: Option, + ) -> (Self, Option) { match self.node() { - Node::AlwaysTrue => ALWAYS_TRUE, - Node::AlwaysFalse => ALWAYS_FALSE, - Node::Interior(interior) => interior.remove_noninferable(db, builder, inferable), + Node::AlwaysTrue => (ALWAYS_TRUE, None), + Node::AlwaysFalse => (ALWAYS_FALSE, None), + Node::Interior(interior) => { + interior.remove_noninferable(db, builder, inferable, source_order) + } } } @@ -2991,15 +2992,12 @@ impl NodeId { } /// Returns a new BDD with any occurrence of `left ∧ right` replaced with `replacement`. - #[expect(clippy::too_many_arguments)] fn substitute_intersection<'db>( self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, left: ConstraintAssignment, - left_source_order: usize, right: ConstraintAssignment, - right_source_order: usize, replacement: NodeId, ) -> Self { // We perform a Shannon expansion to find out what the input BDD evaluates to when: @@ -3033,8 +3031,8 @@ impl NodeId { // false // // (Note that the `else` branch shouldn't be reachable, but we have to provide something!) - let left_node = Node::new_satisfied_constraint(builder, left, left_source_order); - let right_node = Node::new_satisfied_constraint(builder, right, right_source_order); + let (left_node, _) = Node::new_satisfied_constraint(builder, left); + let (right_node, _) = Node::new_satisfied_constraint(builder, right); let right_result = right_node.ite(builder, ALWAYS_FALSE, when_left_but_not_right); let left_result = left_node.ite(builder, right_result, when_not_left); let result = replacement.ite(builder, when_left_and_right, left_result); @@ -3053,15 +3051,12 @@ impl NodeId { } /// Returns a new BDD with any occurrence of `left ∨ right` replaced with `replacement`. - #[expect(clippy::too_many_arguments)] fn substitute_union<'db>( self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, left: ConstraintAssignment, - left_source_order: usize, right: ConstraintAssignment, - right_source_order: usize, replacement: NodeId, ) -> Self { // We perform a Shannon expansion to find out what the input BDD evaluates to when: @@ -3101,8 +3096,8 @@ impl NodeId { // Lastly, verify that the result is consistent with the input. (It must produce the same // results when `left ∨ right`.) If it doesn't, the substitution isn't valid, and we should // return the original BDD unmodified. - let left_node = Node::new_satisfied_constraint(builder, left, left_source_order); - let right_node = Node::new_satisfied_constraint(builder, right, right_source_order); + let (left_node, _) = Node::new_satisfied_constraint(builder, left); + let (right_node, _) = Node::new_satisfied_constraint(builder, right); let validity = replacement.iff(builder, left_node.or(builder, right_node)); let constrained_original = self.and(builder, validity); let constrained_replacement = result.and(builder, validity); @@ -3121,19 +3116,19 @@ impl NodeId { fn for_each_unique_constraint( self, builder: &ConstraintSetBuilder<'_>, - f: &mut dyn FnMut(ConstraintId, usize), + f: &mut dyn FnMut(ConstraintId), ) { fn walk( node: NodeId, builder: &ConstraintSetBuilder<'_>, seen: &mut FxHashSet, - f: &mut dyn FnMut(ConstraintId, usize), + f: &mut dyn FnMut(ConstraintId), ) { if node.is_terminal() || !seen.insert(node) { return; } let interior = builder.interior_node_data(node); - f(interior.constraint, interior.source_order); + f(interior.constraint); walk(interior.if_true, builder, seen, f); walk(interior.if_uncertain, builder, seen, f); walk(interior.if_false, builder, seen, f); @@ -3294,13 +3289,7 @@ impl NodeId { return write!(f, "<{index}> SHARED"); } let interior = builder.interior_node_data(node); - write!( - f, - "<{index}> {} {}/{}", - interior.constraint.display(db, builder), - interior.source_order, - interior.max_source_order, - )?; + write!(f, "<{index}> {}", interior.constraint.display(db, builder))?; // Calling display_graph recursively here causes rustc to claim that the // expect(unused) up above is unfulfilled! write!(f, "\n{prefix}┡━₁ ")?; @@ -3398,16 +3387,6 @@ struct InteriorNodeData { if_true: NodeId, if_uncertain: NodeId, if_false: NodeId, - - /// Represents the order in which this node's constraint was added to the containing constraint - /// set, relative to all of the other constraints in the set. This starts off at 1 for a simple - /// single-constraint set (e.g. created with [`Node::new_constraint`] or - /// [`Node::new_satisfied_constraint`]). It will get incremented, if needed, as that simple BDD - /// is combined into larger BDDs. - source_order: usize, - - /// The maximum `source_order` across this node and all of its descendants. - max_source_order: usize, } /// Accumulates lower and upper bounds for a single typevar on a single BDD path. @@ -3533,7 +3512,9 @@ impl<'db> Type<'db> { inferable: TypeVarSet<'db>, ) -> PathBounds<'db> { let when = source.when_constraint_set_assignable_to_owned(db, target); - when.query(|builder, when| PathBounds::compute(db, builder, when.node, inferable)) + when.query(|builder, when| { + PathBounds::compute(db, builder, when.node, inferable, when.source_order) + }) } assignable_solutions_impl(db, self, target, inferable) @@ -3570,13 +3551,14 @@ impl<'db> PathBounds<'db> { builder: &ConstraintSetBuilder<'db>, node: NodeId, inferable: TypeVarSet<'db>, + source_order: Option, ) -> Self { - #[derive(Default)] - struct CollectVisitor { + struct CollectVisitor<'a> { + source_orders: &'a FxIndexSet, sorted_paths: Vec>, } - impl PathFold for CollectVisitor { + impl PathFold for CollectVisitor<'_> { type Result = (); type Break = Infallible; @@ -3586,7 +3568,16 @@ impl<'db> PathBounds<'db> { _builder: &ConstraintSetBuilder<'db>, path: &PathAssignments, ) -> ControlFlow { - let mut path: Vec<_> = path.positive_constraints().collect(); + let mut path: Vec<_> = path + .positive_constraints() + .map(|(constraint, source_constraint)| { + let source_order = self + .source_orders + .get_index_of(&source_constraint) + .expect("every TDD constraint should have a source order"); + (constraint, source_order) + }) + .collect(); path.sort_by_key(|(_, source_order)| *source_order); self.sorted_paths.push(path); ControlFlow::Continue(()) @@ -3622,13 +3613,16 @@ impl<'db> PathBounds<'db> { } } + let mut source_orders = builder.calculate_source_orders(source_order); if let Some(path_bounds) = - Self::compute_simple_bound_conjunction(db, builder, node, inferable) + Self::compute_simple_bound_conjunction(db, builder, &source_orders, node, inferable) { return path_bounds; } - let node = node.remove_noninferable(db, builder, inferable); + let (node, derived_source_order) = + node.remove_noninferable(db, builder, inferable, source_order); + source_orders.extend(builder.calculate_source_orders(derived_source_order)); let interior = match node.node() { Node::AlwaysTrue => return PathBounds::Unconstrained, Node::AlwaysFalse => return PathBounds::Unsatisfiable, @@ -3640,8 +3634,15 @@ impl<'db> PathBounds<'db> { // come out of `PathAssignment`s with identical `source_order`s, but if they do, those // "tied" constraints will still be ordered in a stable way. So we need a stable sort to // retain that stable per-tie ordering. - let mut collect_visitor = CollectVisitor::default(); - let mut path = interior.path_assignments(builder); + let mut collect_visitor = CollectVisitor { + source_orders: &source_orders, + sorted_paths: Vec::new(), + }; + // Sequent discovery must also happen in source order. Sorting the collected paths below + // is too late: sequent pairs are not commutative, and TDD traversal order can otherwise + // discard gradual evidence before solution extraction. + let path_source_order = builder.ordered_source_order(source_order, derived_source_order); + let mut path = interior.path_assignments(builder, path_source_order); let _ = path.visit(db, builder, node, &mut collect_visitor); collect_visitor.sorted_paths.sort_by(|path1, path2| { let source_orders1 = path1.iter().map(|(_, source_order)| *source_order); @@ -3698,6 +3699,7 @@ impl<'db> PathBounds<'db> { fn compute_simple_bound_conjunction( db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, + source_orders: &FxIndexSet, node: NodeId, inferable: TypeVarSet<'db>, ) -> Option { @@ -3734,7 +3736,9 @@ impl<'db> PathBounds<'db> { constraints.push(( constraint.typevar, constraint.bounds, - interior.source_order, + source_orders + .get_index_of(&interior.constraint) + .expect("every TDD constraint should have a source order"), )); } } @@ -4033,7 +4037,6 @@ impl InteriorNode { interior.constraint, not_true.and(builder, not_uncertain), not_false.and(builder, not_uncertain), - interior.source_order, ); let mut storage = builder.storage.borrow_mut(); @@ -4041,8 +4044,8 @@ impl InteriorNode { result } - fn or(self, builder: &ConstraintSetBuilder<'_>, other: Self, other_offset: usize) -> NodeId { - let key = (self.node(), other.node(), other_offset); + fn or(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> NodeId { + let key = (self.node(), other.node()); let storage = builder.storage.borrow(); if let Some(result) = storage.or_cache.get(&key) { return *result; @@ -4057,18 +4060,11 @@ impl InteriorNode { Ordering::Equal => NodeId::with_uncertain( builder, self_interior.constraint, + self_interior.if_true.or(builder, other_interior.if_true), self_interior - .if_true - .or_inner(builder, other_interior.if_true, other_offset), - self_interior.if_uncertain.or_inner( - builder, - other_interior.if_uncertain, - other_offset, - ), - self_interior - .if_false - .or_inner(builder, other_interior.if_false, other_offset), - self_interior.source_order, + .if_uncertain + .or(builder, other_interior.if_uncertain), + self_interior.if_false.or(builder, other_interior.if_false), ), // This is from Frisch's original description of TDDs. If self < other, we check self // first. Instead of distributing other into the if_true and if_false branches, we @@ -4077,22 +4073,17 @@ impl InteriorNode { Ordering::Less => NodeId::with_uncertain( builder, self_interior.constraint, - self_interior.if_true, - self_interior - .if_uncertain - .or_inner(builder, other.node(), other_offset), + self_interior.if_true, + self_interior.if_uncertain.or(builder, other.node()), self_interior.if_false, - self_interior.source_order, ), // Ditto above but for the other variable ordering Ordering::Greater => NodeId::with_uncertain( builder, other_interior.constraint, other_interior.if_true, - self.node() - .or_inner(builder, other_interior.if_uncertain, other_offset), + self.node().or(builder, other_interior.if_uncertain), other_interior.if_false, - other_interior.source_order + other_offset, ), }; @@ -4101,8 +4092,8 @@ impl InteriorNode { result } - fn and(self, builder: &ConstraintSetBuilder<'_>, other: Self, other_offset: usize) -> NodeId { - let key = (self.node(), other.node(), other_offset); + fn and(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> NodeId { + let key = (self.node(), other.node()); let storage = builder.storage.borrow(); if let Some(result) = storage.and_cache.get(&key) { return *result; @@ -4126,48 +4117,34 @@ impl InteriorNode { Ordering::Equal => { let if_true = self_interior .if_true - .and_inner( + .and( builder, - other_interior.if_true.or_inner( - builder, - other_interior.if_uncertain, - other_offset, - ), - other_offset, + other_interior + .if_true + .or(builder, other_interior.if_uncertain), ) - .or_inner( + .or( builder, - self_interior.if_uncertain.and_inner( - builder, - other_interior.if_true, - other_offset, - ), - 0, + self_interior + .if_uncertain + .and(builder, other_interior.if_true), ); - let if_uncertain = self_interior.if_uncertain.and_inner( - builder, - other_interior.if_uncertain, - other_offset, - ); + let if_uncertain = self_interior + .if_uncertain + .and(builder, other_interior.if_uncertain); let if_false = self_interior .if_false - .and_inner( + .and( builder, - other_interior.if_uncertain.or_inner( - builder, - other_interior.if_false, - other_offset, - ), - other_offset, + other_interior + .if_uncertain + .or(builder, other_interior.if_false), ) - .or_inner( + .or( builder, - self_interior.if_uncertain.and_inner( - builder, - other_interior.if_false, - other_offset, - ), - 0, + self_interior + .if_uncertain + .and(builder, other_interior.if_false), ); NodeId::with_uncertain( builder, @@ -4175,33 +4152,21 @@ impl InteriorNode { if_true, if_uncertain, if_false, - self_interior.source_order, ) } Ordering::Less => NodeId::with_uncertain( builder, self_interior.constraint, - self_interior - .if_true - .and_inner(builder, other.node(), other_offset), - self_interior - .if_uncertain - .and_inner(builder, other.node(), other_offset), - self_interior - .if_false - .and_inner(builder, other.node(), other_offset), - self_interior.source_order, + self_interior.if_true.and(builder, other.node()), + self_interior.if_uncertain.and(builder, other.node()), + self_interior.if_false.and(builder, other.node()), ), Ordering::Greater => NodeId::with_uncertain( builder, other_interior.constraint, - self.node() - .and_inner(builder, other_interior.if_true, other_offset), - self.node() - .and_inner(builder, other_interior.if_uncertain, other_offset), - self.node() - .and_inner(builder, other_interior.if_false, other_offset), - other_interior.source_order + other_offset, + self.node().and(builder, other_interior.if_true), + self.node().and(builder, other_interior.if_uncertain), + self.node().and(builder, other_interior.if_false), ), }; @@ -4215,7 +4180,8 @@ impl InteriorNode { db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, bound_typevars: TypeVarSet<'db>, - ) -> NodeId { + source_order: Option, + ) -> (NodeId, Option) { let mentions_typevar = |ty: Type<'db>| match ty { Type::TypeVar(typevar) => typevar.is_inferable(db, bound_typevars), _ => false, @@ -4223,6 +4189,7 @@ impl InteriorNode { self.abstract_inner( db, builder, + source_order, // Remove any node that constrains one of `bound_typevars`, or that has a lower/upper // bound that mentions one of them. Removed constraints are still added to `path`, so // the sequent map can propagate any derived constraints that do not mention the @@ -4247,7 +4214,8 @@ impl InteriorNode { db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, - ) -> NodeId { + source_order: Option, + ) -> (NodeId, Option) { let is_bare_inferable_typevar = |ty: Type<'db>| { ty.as_typevar() .is_some_and(|bound_typevar| bound_typevar.is_inferable(db, inferable)) @@ -4255,6 +4223,7 @@ impl InteriorNode { self.abstract_inner( db, builder, + source_order, // We only want to keep constraints on inferable typevars. If the constraint's typevar // is itself inferable, we keep it. We also need to keep some constraints in // non-inferable typevars, if their lower or upper bound is a bare inferable typevar. @@ -4283,8 +4252,9 @@ impl InteriorNode { self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, + source_order: Option, should_remove: F, - ) -> NodeId + ) -> (NodeId, Option) where F: FnMut(ConstraintId) -> bool, { @@ -4302,8 +4272,8 @@ impl InteriorNode { where F: FnMut(ConstraintId) -> bool, { - type Result = NodeId; - type Interior = (Disposition, ConstraintId, usize); + type Result = (NodeId, Option); + type Interior = (Disposition, ConstraintId); type Break = Infallible; fn visit_satisfied<'db>( @@ -4312,7 +4282,7 @@ impl InteriorNode { _builder: &ConstraintSetBuilder<'db>, _path: &PathAssignments, ) -> ControlFlow { - ControlFlow::Continue(ALWAYS_TRUE) + ControlFlow::Continue((ALWAYS_TRUE, None)) } fn visit_unsatisfied<'db>( @@ -4321,7 +4291,7 @@ impl InteriorNode { _builder: &ConstraintSetBuilder<'db>, _path: &PathAssignments, ) -> ControlFlow { - ControlFlow::Continue(ALWAYS_FALSE) + ControlFlow::Continue((ALWAYS_FALSE, None)) } fn visit_impossible<'db>( @@ -4330,7 +4300,7 @@ impl InteriorNode { _builder: &ConstraintSetBuilder<'db>, _path: &PathAssignments, ) -> ControlFlow { - ControlFlow::Continue(ALWAYS_FALSE) + ControlFlow::Continue((ALWAYS_FALSE, None)) } fn enter_interior<'db>( @@ -4345,7 +4315,7 @@ impl InteriorNode { } else { Disposition::Keep }; - ControlFlow::Continue((disposition, interior.constraint, interior.source_order)) + ControlFlow::Continue((disposition, interior.constraint)) } fn visit_edge<'db>( @@ -4357,7 +4327,7 @@ impl InteriorNode { path: &PathAssignments, new_range: Range, ) -> ControlFlow { - let (disposition, _, _) = interior; + let (disposition, _) = interior; match disposition { // If we are keeping this node, we don't need to add any derived facts to the // result; we can always re-derive them later. @@ -4365,9 +4335,7 @@ impl InteriorNode { // If we are removing this node, we have to check if there are any derived facts // that depend on the constraint we're about to remove. If so, we need to - // "remember" them by AND-ing them in with the corresponding branch. We currently - // reuse the `source_order` of the constraint being removed when we add these - // derived facts. + // "remember" them by AND-ing them in with the corresponding branch. Disposition::Remove => { ControlFlow::Continue( path.assignments[new_range] @@ -4377,16 +4345,20 @@ impl InteriorNode { // removed! !(self.should_remove)(assignment.constraint()) }) - .fold(subtree, |subtree, (assignment, (source_order, _))| { - subtree.and( - builder, - Node::new_satisfied_constraint( - builder, - *assignment, - *source_order, - ), - ) - }), + .fold( + subtree, + |(subtree, subtree_source_order), (assignment, _)| { + let (assignment, assignment_source_order) = + Node::new_satisfied_constraint(builder, *assignment); + ( + subtree.and(builder, assignment), + builder.ordered_source_order( + subtree_source_order, + assignment_source_order, + ), + ) + }, + ), ) } } @@ -4401,7 +4373,7 @@ impl InteriorNode { if_uncertain: Self::Result, if_false: Self::Result, ) -> ControlFlow { - let (disposition, constraint, source_order) = interior; + let (disposition, constraint) = interior; match disposition { // If we are keeping this node, absorb the uncertain branch into both the true // and false branches before constructing the ITE, matching TDD semantics: when @@ -4412,11 +4384,23 @@ impl InteriorNode { // derived constraints into the result, and those constraints might appear before this // one in the BDD ordering. Disposition::Keep => { - let guard = Node::new_constraint(builder, *constraint, *source_order); - ControlFlow::Continue(guard.ite( + let (guard, guard_source_order) = + Node::new_constraint(builder, *constraint); + let (if_true, if_true_source_order) = if_true; + let (if_uncertain, if_uncertain_source_order) = if_uncertain; + let (if_false, if_false_source_order) = if_false; + let node = guard.ite( builder, if_true.or(builder, if_uncertain), if_false.or(builder, if_uncertain), + ); + let left_source_order = + builder.ordered_source_order(guard_source_order, if_true_source_order); + let right_source_order = builder + .ordered_source_order(if_uncertain_source_order, if_false_source_order); + ControlFlow::Continue(( + node, + builder.ordered_source_order(left_source_order, right_source_order), )) } @@ -4424,14 +4408,23 @@ impl InteriorNode { // outgoing edges. That is, the result is true if there's any assignment of // this node's constraint that is true. (We will have already added any // necessary derived facts in the `visit_edge` method.) - Disposition::Remove => ControlFlow::Continue( - if_true.or(builder, if_uncertain).or(builder, if_false), - ), + Disposition::Remove => { + let (if_true, if_true_source_order) = if_true; + let (if_uncertain, if_uncertain_source_order) = if_uncertain; + let (if_false, if_false_source_order) = if_false; + let node = if_true.or(builder, if_uncertain).or(builder, if_false); + let source_order = builder + .ordered_source_order(if_true_source_order, if_uncertain_source_order); + ControlFlow::Continue(( + node, + builder.ordered_source_order(source_order, if_false_source_order), + )) + } } } } - let mut path = self.path_assignments(builder); + let mut path = self.path_assignments(builder, source_order); let mut visitor = AbstractVisitor { should_remove }; let ControlFlow::Continue(result) = path.visit(db, builder, self.node(), &mut visitor); result @@ -4501,7 +4494,6 @@ impl InteriorNode { if_true, if_uncertain, if_false, - self_interior.source_order, ), found_in_true || found_in_uncertain || found_in_false, ) @@ -4513,19 +4505,28 @@ impl InteriorNode { result } - fn path_assignments(self, builder: &ConstraintSetBuilder<'_>) -> PathAssignments { - // Sort the constraints in this BDD by their `source_order`s before adding them to the - // sequent map. This ensures that constraints appear in the sequent map in a stable order. - // The constraints mentioned in a BDD should all have distinct `source_order`s, so an - // unstable sort is fine. + fn path_assignments( + self, + builder: &ConstraintSetBuilder<'_>, + source_order: Option, + ) -> PathAssignments { let mut constraints: SmallVec<[_; 8]> = SmallVec::new(); self.node() - .for_each_unique_constraint(builder, &mut |constraint, source_order| { - constraints.push((constraint, source_order)); + .for_each_unique_constraint(builder, &mut |constraint| { + constraints.push(constraint); }); - constraints.sort_unstable_by_key(|(_, source_order)| *source_order); - - PathAssignments::new(constraints.into_iter().map(|(constraint, _)| constraint)) + let source_orders = builder.calculate_source_orders(source_order); + // `PathAssignments` seeds its insertion-ordered discovered-constraint map from this list, + // and uses that order when constructing non-commutative sequent pairs. Do not replace this + // with TDD traversal order: doing so can change inference and lose gradual constraints. + // Every constraint in the TDD must appear in the sidecar. If an operation introduces new + // constraints, it must preserve their source orders rather than invent an order here. + constraints.sort_by_key(|constraint| { + source_orders + .get_index_of(constraint) + .expect("every BDD constraint should have a source-order entry") + }); + PathAssignments::new(constraints) } /// Returns a simplified version of a BDD. @@ -4557,14 +4558,9 @@ impl InteriorNode { // visit queue with all pairs of those constraints. (We use "combinations" because we don't // need to compare a constraint against itself, and because ordering doesn't matter.) let mut seen_constraints = FxHashSet::default(); - let mut source_orders = FxHashMap::default(); self.node() - .for_each_unique_constraint(builder, &mut |constraint, source_order| { + .for_each_unique_constraint(builder, &mut |constraint| { seen_constraints.insert(constraint); - source_orders - .entry(constraint) - .and_modify(|existing: &mut usize| *existing = (*existing).min(source_order)) - .or_insert(source_order); }); let mut to_visit: Vec<(_, _)> = (seen_constraints.iter().copied()) .array_combinations() @@ -4572,17 +4568,9 @@ impl InteriorNode { .collect(); // Repeatedly pop constraint pairs off of the visit queue, checking whether each pair can - // be simplified. If we add any derived constraints, we will place them at the end in - // source order. (We do not have any test cases that depend on constraint sets being - // displayed in a consistent ordering, so we don't need to be clever in assigning these - // `source_order`s.) + // be simplified. let mut simplified = self.node(); - let self_interior = builder.interior_node_data(self.node()); - let mut next_source_order = self_interior.max_source_order + 1; while let Some((left_constraint, right_constraint)) = to_visit.pop() { - let left_source_order = source_orders[&left_constraint]; - let right_source_order = source_orders[&right_constraint]; - // If the constraints refer to different typevars, the only simplifications we can make // are of the form `S ≤ T ∧ T ≤ int → S ≤ int`. let left_constraint_data = builder.constraint_data(left_constraint); @@ -4654,18 +4642,11 @@ impl InteriorNode { if seen_constraints.contains(&new_constraint) { continue; } - let new_node = Node::new_constraint(builder, new_constraint, next_source_order); - next_source_order += 1; - let positive_left_node = Node::new_satisfied_constraint( - builder, - left_constraint.when_true(), - left_source_order, - ); - let positive_right_node = Node::new_satisfied_constraint( - builder, - right_constraint.when_true(), - right_source_order, - ); + let (new_node, _) = Node::new_constraint(builder, new_constraint); + let (positive_left_node, _) = + Node::new_satisfied_constraint(builder, left_constraint.when_true()); + let (positive_right_node, _) = + Node::new_satisfied_constraint(builder, right_constraint.when_true()); let lhs = positive_left_node.and(builder, positive_right_node); let intersection = new_node.ite(builder, lhs, ALWAYS_FALSE); simplified = simplified.and(builder, intersection); @@ -4700,48 +4681,24 @@ impl InteriorNode { // Containment: The range of one constraint might completely contain the range of the // other. If so, there are several potential simplifications. let larger_smaller = if left_constraint.implies(db, builder, right_constraint) { - Some(( - right_constraint, - right_source_order, - left_constraint, - left_source_order, - )) + Some((right_constraint, left_constraint)) } else if right_constraint.implies(db, builder, left_constraint) { - Some(( - left_constraint, - left_source_order, - right_constraint, - right_source_order, - )) + Some((left_constraint, right_constraint)) } else { None }; - if let Some(( - larger_constraint, - larger_source_order, - smaller_constraint, - smaller_source_order, - )) = larger_smaller - { - let positive_larger_node = Node::new_satisfied_constraint( - builder, - larger_constraint.when_true(), - larger_source_order, - ); - let negative_larger_node = Node::new_satisfied_constraint( - builder, - larger_constraint.when_false(), - larger_source_order, - ); + if let Some((larger_constraint, smaller_constraint)) = larger_smaller { + let (positive_larger_node, _) = + Node::new_satisfied_constraint(builder, larger_constraint.when_true()); + let (negative_larger_node, _) = + Node::new_satisfied_constraint(builder, larger_constraint.when_false()); // larger ∨ smaller = larger simplified = simplified.substitute_union( db, builder, larger_constraint.when_true(), - larger_source_order, smaller_constraint.when_true(), - smaller_source_order, positive_larger_node, ); @@ -4750,9 +4707,7 @@ impl InteriorNode { db, builder, larger_constraint.when_false(), - larger_source_order, smaller_constraint.when_false(), - smaller_source_order, negative_larger_node, ); @@ -4762,9 +4717,7 @@ impl InteriorNode { db, builder, larger_constraint.when_false(), - larger_source_order, smaller_constraint.when_true(), - smaller_source_order, ALWAYS_FALSE, ); @@ -4774,9 +4727,7 @@ impl InteriorNode { db, builder, larger_constraint.when_true(), - larger_source_order, smaller_constraint.when_false(), - smaller_source_order, ALWAYS_TRUE, ); } @@ -4793,55 +4744,37 @@ impl InteriorNode { // represent that intersection. We also need to add the new constraint to our // seen set and (if we haven't already seen it) to the to-visit queue. if seen_constraints.insert(intersection_constraint) { - source_orders.insert(intersection_constraint, next_source_order); to_visit.extend( (seen_constraints.iter().copied()) .filter(|seen| *seen != intersection_constraint) .map(|seen| (seen, intersection_constraint)), ); } - let positive_intersection_node = Node::new_satisfied_constraint( + let (positive_intersection_node, _) = Node::new_satisfied_constraint( builder, intersection_constraint.when_true(), - next_source_order, ); - let negative_intersection_node = Node::new_satisfied_constraint( + let (negative_intersection_node, _) = Node::new_satisfied_constraint( builder, intersection_constraint.when_false(), - next_source_order, ); - next_source_order += 1; - let positive_left_node = Node::new_satisfied_constraint( - builder, - left_constraint.when_true(), - left_source_order, - ); - let negative_left_node = Node::new_satisfied_constraint( - builder, - left_constraint.when_false(), - left_source_order, - ); + let (positive_left_node, _) = + Node::new_satisfied_constraint(builder, left_constraint.when_true()); + let (negative_left_node, _) = + Node::new_satisfied_constraint(builder, left_constraint.when_false()); - let positive_right_node = Node::new_satisfied_constraint( - builder, - right_constraint.when_true(), - right_source_order, - ); - let negative_right_node = Node::new_satisfied_constraint( - builder, - right_constraint.when_false(), - right_source_order, - ); + let (positive_right_node, _) = + Node::new_satisfied_constraint(builder, right_constraint.when_true()); + let (negative_right_node, _) = + Node::new_satisfied_constraint(builder, right_constraint.when_false()); // left ∧ right = intersection simplified = simplified.substitute_intersection( db, builder, left_constraint.when_true(), - left_source_order, right_constraint.when_true(), - right_source_order, positive_intersection_node, ); @@ -4850,9 +4783,7 @@ impl InteriorNode { db, builder, left_constraint.when_false(), - left_source_order, right_constraint.when_false(), - right_source_order, negative_intersection_node, ); @@ -4863,9 +4794,7 @@ impl InteriorNode { db, builder, left_constraint.when_true(), - left_source_order, right_constraint.when_false(), - right_source_order, positive_left_node.and(builder, negative_intersection_node), ); @@ -4875,9 +4804,7 @@ impl InteriorNode { db, builder, left_constraint.when_false(), - left_source_order, right_constraint.when_true(), - right_source_order, positive_right_node.and(builder, negative_intersection_node), ); @@ -4888,9 +4815,7 @@ impl InteriorNode { db, builder, left_constraint.when_true(), - left_source_order, right_constraint.when_false(), - right_source_order, negative_right_node.or(builder, positive_intersection_node), ); @@ -4900,9 +4825,7 @@ impl InteriorNode { db, builder, left_constraint.when_false(), - left_source_order, right_constraint.when_true(), - right_source_order, negative_left_node.or(builder, positive_intersection_node), ); } @@ -4915,25 +4838,17 @@ impl InteriorNode { // All of the below hold because we just proved that the intersection of left // and right is empty. - let positive_left_node = Node::new_satisfied_constraint( - builder, - left_constraint.when_true(), - left_source_order, - ); - let positive_right_node = Node::new_satisfied_constraint( - builder, - right_constraint.when_true(), - right_source_order, - ); + let (positive_left_node, _) = + Node::new_satisfied_constraint(builder, left_constraint.when_true()); + let (positive_right_node, _) = + Node::new_satisfied_constraint(builder, right_constraint.when_true()); // left ∧ right = false simplified = simplified.substitute_intersection( db, builder, left_constraint.when_true(), - left_source_order, right_constraint.when_true(), - right_source_order, ALWAYS_FALSE, ); @@ -4942,9 +4857,7 @@ impl InteriorNode { db, builder, left_constraint.when_false(), - left_source_order, right_constraint.when_false(), - right_source_order, ALWAYS_TRUE, ); @@ -4954,9 +4867,7 @@ impl InteriorNode { db, builder, left_constraint.when_true(), - left_source_order, right_constraint.when_false(), - right_source_order, positive_left_node, ); @@ -4966,9 +4877,7 @@ impl InteriorNode { db, builder, left_constraint.when_false(), - left_source_order, right_constraint.when_true(), - right_source_order, positive_right_node, ); } @@ -6632,8 +6541,9 @@ impl PathFold for IsNeverSatisfiedVisitor { pub(crate) struct PathAssignments { /// All of the rules that we know for inferring derived constraints on the current path. sequents: Vec, - /// Each assignment's source order and the first per-path fuel value with which it was derived. - assignments: FxIndexMap, + /// Each assignment's source constraint and the first per-path fuel value with which it was + /// derived. + assignments: FxIndexMap, /// Additional per-path fuel values that can derive an assignment, keyed by its index in /// `assignments`. These are stored separately so that branch-local additions can be rolled /// back by truncating the set. Only the greatest fuel value participates in further @@ -6784,7 +6694,6 @@ impl PathAssignments { db, builder, interior.constraint.when_true(), - interior.source_order, |path, new_range, found_conflict| { let subtree = if found_conflict { visitor.visit_impossible(db, builder, path) @@ -6813,7 +6722,6 @@ impl PathAssignments { db, builder, interior.constraint.when_unconstrained(), - interior.source_order, |path, new_range, found_conflict| { let subtree = if found_conflict { visitor.visit_impossible(db, builder, path) @@ -6844,7 +6752,6 @@ impl PathAssignments { db, builder, interior.constraint.when_false(), - interior.source_order, |path, new_range, found_conflict| { let subtree = if found_conflict { visitor.visit_impossible(db, builder, path) @@ -6904,7 +6811,6 @@ impl PathAssignments { db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, assignment: ConstraintAssignment, - source_order: usize, f: impl FnOnce(&mut Self, Range, bool) -> R, ) -> R { // Record a snapshot of the assignments that we already knew held — both so that we can @@ -6929,8 +6835,9 @@ impl PathAssignments { debug_assert!(self.assignment_queue.is_empty()); self.assignment_queue .push_back((assignment, AssignmentFuel::origin())); + let source_constraint = assignment.constraint(); let found_conflict = self - .drain_assignment_queue(db, builder, source_order) + .drain_assignment_queue(db, builder, source_constraint) .is_err(); if !found_conflict { tracing::trace!( @@ -6961,13 +6868,17 @@ impl PathAssignments { result } - pub(crate) fn positive_constraints(&self) -> impl Iterator + '_ { - self.assignments - .iter() - .filter_map(|(assignment, (source_order, _))| match assignment { - ConstraintAssignment::Positive(constraint) => Some((*constraint, *source_order)), + pub(crate) fn positive_constraints( + &self, + ) -> impl Iterator + '_ { + self.assignments.iter().filter_map( + |(assignment, (source_constraint, _))| match assignment { + ConstraintAssignment::Positive(constraint) => { + Some((*constraint, *source_constraint)) + } ConstraintAssignment::Negative(_) | ConstraintAssignment::Unconstrained(_) => None, - }) + }, + ) } fn assignment_holds(&self, assignment: ConstraintAssignment) -> bool { @@ -7024,10 +6935,10 @@ impl PathAssignments { &mut self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, - source_order: usize, + source_constraint: ConstraintId, ) -> Result<(), PathAssignmentConflict> { while let Some((assignment, fuel)) = self.assignment_queue.pop_front() { - self.add_assignment(db, builder, assignment, source_order, fuel)?; + self.add_assignment(db, builder, assignment, source_constraint, fuel)?; } Ok(()) } @@ -7040,7 +6951,7 @@ impl PathAssignments { db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, assignment: ConstraintAssignment, - source_order: usize, + source_constraint: ConstraintId, fuel: AssignmentFuel, ) -> Result<(), PathAssignmentConflict> { if matches!(assignment, ConstraintAssignment::Unconstrained(_)) { @@ -7056,7 +6967,7 @@ impl PathAssignments { // assignment, but as an optimization we can return early without actually querying the // sequent map. self.assignments - .insert(assignment, (source_order, fuel.remaining)); + .insert(assignment, (source_constraint, fuel.remaining)); return Ok(()); } @@ -7085,19 +6996,19 @@ impl PathAssignments { None => return Ok(()), }; } - entry.insert((source_order, fuel.remaining)); + entry.insert((source_constraint, fuel.remaining)); } Entry::Occupied(mut entry) => { let index = entry.index(); - let (existing_source_order, existing_fuel) = entry.get_mut(); + let (existing_source_constraint, existing_fuel) = entry.get_mut(); // If a constraint appears both as an "origin" constraint (it actually appears in // the BDD structure) and as a "derived" constraint (we infer it from other - // constraints), we should prefer the origin source_order, regardless of which + // constraints), we should prefer the origin source constraint, regardless of which // order we encounter the various constraints in the BDD. if !fuel.is_derived() { - *existing_source_order = source_order; + *existing_source_constraint = source_constraint; } // We've already seen this assignment, and in theory have already queried the @@ -7128,13 +7039,7 @@ impl PathAssignments { } } - // Then use our sequents to add additional facts that we know to be true. We currently - // reuse the `source_order` of the "real" constraint passed into `walk_edge` when we add - // these derived facts. - // - // TODO: This might not be stable enough, if we add more than one derived fact for this - // constraint. If we still see inconsistent test output, we might need a more complex - // way of tracking source order for derived facts. + // Then use our sequents to add additional facts that we know to be true. // // TODO: This is very naive at the moment, partly for expediency, and partly because we // don't anticipate the sequent maps to be very large. We might consider avoiding the @@ -7518,10 +7423,14 @@ impl<'db> BoundTypeVarInstance<'db> { /// Returns the valid specializations of a typevar. This is used when checking a constraint set /// when this typevar is in inferable position, where we only need _some_ specialization to /// satisfy the constraint set. - fn valid_specializations(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> NodeId { + fn valid_specializations<'c>( + self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + ) -> ConstraintSet<'db, 'c> { if self.paramspec_attr(db).is_some() { // P.args and P.kwargs are variadic, and do not have an upper bound or constraints. - return ALWAYS_TRUE; + return ConstraintSet::always(builder); } // For gradual upper bounds and constraints, we are free to choose any materialization that @@ -7535,20 +7444,24 @@ impl<'db> BoundTypeVarInstance<'db> { // that _some_ valid specialization satisfies the constraint set, it's correct for us to // return the range of valid materializations that we can choose from. match self.typevar(db).bound_or_constraints(db) { - None => ALWAYS_TRUE, + None => ConstraintSet::always(builder), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let bound = bound.top_materialization(db); - Constraint::new_node_with_bounds(db, builder, self, None, Some(bound)) + ConstraintSet::constrain_typevar_upper_bound(db, builder, self, bound) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut specializations = ALWAYS_FALSE; + let mut specializations = ConstraintSet::never(builder); for constraint in constraints.elements(db) { let constraint_lower = constraint.bottom_materialization(db); let constraint_upper = constraint.top_materialization(db); - specializations = specializations.or_with_offset( + let constraint = ConstraintSet::constrain_typevar( + db, builder, - Constraint::new_node(db, builder, self, constraint_lower, constraint_upper), + self, + constraint_lower, + constraint_upper, ); + specializations.union(db, builder, constraint); } specializations } @@ -7568,36 +7481,40 @@ impl<'db> BoundTypeVarInstance<'db> { /// specifies the required specializations, and the iterator will be empty. For a constrained /// typevar, the primary result will include the fully static constraints, and the iterator /// will include an entry for each non-fully-static constraint. - fn required_specializations( + fn required_specializations<'c>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - ) -> (NodeId, Vec) { + builder: &'c ConstraintSetBuilder<'db>, + ) -> (ConstraintSet<'db, 'c>, Vec>) { // For upper bounds and constraints, we are free to choose any materialization that makes // the check succeed. In non-inferable positions, it is most helpful to choose a // materialization that is as restrictive as possible, since that minimizes the number of // valid specializations that must satisfy the check. We therefore take the bottom // materialization of the bound or constraints. match self.typevar(db).bound_or_constraints(db) { - None => (ALWAYS_TRUE, Vec::new()), + None => (ConstraintSet::always(builder), Vec::new()), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let bound = bound.bottom_materialization(db); ( - Constraint::new_node_with_bounds(db, builder, self, None, Some(bound)), + ConstraintSet::constrain_typevar_upper_bound(db, builder, self, bound), Vec::new(), ) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut non_gradual_constraints = ALWAYS_FALSE; + let mut non_gradual_constraints = ConstraintSet::never(builder); let mut gradual_constraints = Vec::new(); for constraint in constraints.elements(db) { let constraint_lower = constraint.bottom_materialization(db); let constraint_upper = constraint.top_materialization(db); - let constraint = - Constraint::new_node(db, builder, self, constraint_lower, constraint_upper); + let constraint = ConstraintSet::constrain_typevar( + db, + builder, + self, + constraint_lower, + constraint_upper, + ); if constraint_lower == constraint_upper { - non_gradual_constraints = - non_gradual_constraints.or_with_offset(builder, constraint); + non_gradual_constraints.union(db, builder, constraint); } else { gradual_constraints.push(constraint); } @@ -7684,6 +7601,28 @@ mod tests { assert!(mapped.is_always_satisfied(&db)); } + #[test] + fn type_mapping_handles_absorbed_constraints_in_source_order() { + let db = setup_db(); + let t = create_typevar(&db, "T"); + let builder = ConstraintSetBuilder::new(); + let str = create_constraint(&db, &builder, t, KnownClass::Str); + let int = create_constraint(&db, &builder, t, KnownClass::Int); + let set = str.or(&db, &builder, || int).and(&db, &builder, || str); + + let mapped = set.apply_type_mapping_impl( + &db, + &TypeMapping::ApplySpecialization(ApplySpecialization::Single( + t, + KnownClass::Str.to_instance(&db), + )), + TypeContext::default(), + &ApplyTypeMappingVisitor::default(), + ); + + assert!(mapped.is_always_satisfied(&db)); + } + #[test] fn upper_bound_prunes_duplicates_and_redundant_supertypes() { let db = setup_db(); @@ -8066,6 +8005,25 @@ mod tests { }); } + #[test] + fn never_satisfied_cache_is_shared_across_source_orders() { + let db = setup_db(); + let t = create_typevar(&db, "T"); + let u = create_typevar(&db, "U"); + let builder = ConstraintSetBuilder::new(); + let t_int = create_constraint(&db, &builder, t, KnownClass::Int); + let u_str = create_constraint(&db, &builder, u, KnownClass::Str); + + let first = t_int.and(&db, &builder, || u_str); + let second = u_str.and(&db, &builder, || t_int); + + assert_eq!(first.node, second.node); + assert_ne!(first.source_order, second.source_order); + assert!(!first.is_never_satisfied(&db)); + assert!(!second.is_never_satisfied(&db)); + assert_eq!(builder.storage.borrow().never_satisfied_cache.len(), 1); + } + #[derive(Clone, Copy)] struct PermutedConstraint<'db>( BoundTypeVarInstance<'db>, @@ -8076,7 +8034,7 @@ mod tests { impl<'db> PermutedConstraint<'db> { fn node(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> NodeId { let PermutedConstraint(typevar, lower, upper) = self; - Constraint::new_node_with_bounds(db, builder, typevar, lower, upper) + Constraint::new_node_with_bounds(db, builder, typevar, lower, upper).0 } } @@ -8115,7 +8073,22 @@ mod tests { ); } - let set = ConstraintSet::from_node(&builder, build_bdd(&builder)); + let node = build_bdd(&builder); + let source_order = atoms.iter().fold(None, |source_order, atom| { + let PermutedConstraint(typevar, lower, upper) = *atom; + let constraint = builder.intern_constraint( + db, + Constraint { + typevar, + bounds: ConstraintBounds::new(lower, upper), + }, + ); + builder.ordered_source_order( + source_order, + Some(builder.constraint_source_order(constraint)), + ) + }); + let set = ConstraintSet::from_node(&builder, node, source_order); let solutions = set.solutions(db, &builder, inferable); let mut merged = FxHashMap::default(); if let Solutions::Constrained(paths) = &solutions { @@ -8168,6 +8141,121 @@ mod tests { assert_eq!(signatures, expected); } + #[test] + fn constraint_absorption_is_independent_of_constraint_order() { + let db = setup_db(); + let t = create_typevar(&db, "T"); + let str = KnownClass::Str.to_instance(&db); + let int = KnownClass::Int.to_instance(&db); + let atoms = [ + PermutedConstraint(t, Some(str), None), + PermutedConstraint(t, Some(int), None), + ]; + + check_solutions_for_constraint_orderings( + &db, + &[t], + &atoms, + |builder| { + let [str_t, int_t] = atoms.map(|atom| atom.node(&db, builder)); + str_t.or(builder, int_t).and(builder, str_t) + }, + ["never=false always=false merged=[T=str] paths=[T=str]"], + ); + + check_solutions_for_constraint_orderings( + &db, + &[t], + &atoms, + |builder| { + let [str_t, int_t] = atoms.map(|atom| atom.node(&db, builder)); + str_t.or(builder, int_t) + }, + ["never=false always=false merged=[T=str | int] paths=[T=str; T=int]"], + ); + } + + #[test] + fn compound_constraint_absorption_is_independent_of_constraint_order() { + let db = setup_db(); + let t = create_typevar(&db, "T"); + let u = create_typevar(&db, "U"); + let str = KnownClass::Str.to_instance(&db); + let bytes = KnownClass::Bytes.to_instance(&db); + let int = KnownClass::Int.to_instance(&db); + let atoms = [ + PermutedConstraint(t, Some(str), None), + PermutedConstraint(u, Some(bytes), None), + PermutedConstraint(t, Some(int), None), + ]; + + check_solutions_for_constraint_orderings( + &db, + &[t, u], + &atoms, + |builder| { + let [str_t, bytes_u, int_t] = atoms.map(|atom| atom.node(&db, builder)); + let compound = str_t.and(builder, bytes_u); + compound.or(builder, int_t).and(builder, compound) + }, + ["never=false always=false merged=[T=str, U=bytes] paths=[T=str, U=bytes]"], + ); + } + + #[test] + fn compound_constraint_absorption_preserves_binding_source_order() { + let db = setup_db(); + let t = create_typevar(&db, "T"); + let u = create_typevar(&db, "U"); + let x = create_typevar(&db, "X"); + let str = KnownClass::Str.to_instance(&db); + let bytes = KnownClass::Bytes.to_instance(&db); + let int = KnownClass::Int.to_instance(&db); + let atoms = [ + PermutedConstraint(t, Some(str), None), + PermutedConstraint(u, Some(bytes), None), + PermutedConstraint(x, Some(int), None), + ]; + + check_solutions_for_constraint_orderings( + &db, + &[t, u, x], + &atoms, + |builder| { + let [str_t, bytes_u, int_x] = atoms.map(|atom| atom.node(&db, builder)); + let early = int_x.and(builder, str_t).and(builder, bytes_u); + let late = bytes_u.and(builder, str_t); + early.or(builder, late) + }, + ["never=false always=false merged=[T=str, U=bytes] paths=[T=str, U=bytes]"], + ); + } + + #[test] + fn constraint_partition_is_independent_of_constraint_order() { + let db = setup_db(); + let t = create_typevar(&db, "T"); + let str = KnownClass::Str.to_instance(&db); + let int = KnownClass::Int.to_instance(&db); + let atoms = [ + PermutedConstraint(t, Some(str), None), + PermutedConstraint(t, Some(int), None), + ]; + + check_solutions_for_constraint_orderings( + &db, + &[t], + &atoms, + |builder| { + let [str_t, int_t] = atoms.map(|atom| atom.node(&db, builder)); + let true_path = int_t.and(builder, str_t); + let false_path = int_t.negate(builder).and(builder, str_t); + true_path.or(builder, false_path) + }, + ["never=false always=false merged=[T=str] paths=[T=str]"], + ); + } + #[test] fn constraint_ordering_changes_nested_transitive_solutions() { let db = setup_db(); @@ -8193,9 +8281,9 @@ mod tests { let [t_list_u, u_int, list_int_t, bytes_v] = atoms.map(|atom| atom.node(&db, builder)); t_list_u - .and_with_offset(builder, u_int) - .and_with_offset(builder, list_int_t) - .or_with_offset(builder, bytes_v) + .and(builder, u_int) + .and(builder, list_int_t) + .or(builder, bytes_v) }, // TODO: All permutations should produce the first result. TDD traversal currently // leaks irrelevant positive constraints onto the `V = bytes` alternative. @@ -8229,9 +8317,9 @@ mod tests { |builder| { let [t_int, t_str, bytes_u] = atoms.map(|atom| atom.node(&db, builder)); t_int - .or_with_offset(builder, t_str) + .or(builder, t_str) .negate(builder) - .or_with_offset(builder, bytes_u) + .or(builder, bytes_u) }, // TODO: All permutations should produce the first result. A satisfied alternative // should not infer `T` from unrelated positive decisions made earlier in a BDD path. @@ -8264,13 +8352,12 @@ mod tests { |builder| { let [t_int, t_str, int_t, u_int] = atoms.map(|atom| atom.node(&db, builder)); t_int - .or_with_offset(builder, t_str) - .and_with_offset(builder, int_t) - .and_with_offset(builder, u_int) + .or(builder, t_str) + .and(builder, int_t) + .and(builder, u_int) }, - // TODO: `SequentMap::for_constraint_pair` can receive its inputs in BDD order, not - // source order. That changes which equivalent upper-bound intersection is constructed - // first. + // TODO: Constraint-ID permutations can still change which equivalent upper-bound + // intersection is constructed first. [ "never=false always=false merged=[T=int | U, U=T & int] paths=[T=int | U, U=T & int]", "never=false always=false merged=[T=int | U, U=int & T] paths=[T=int | U, U=int & T]", @@ -8309,15 +8396,15 @@ mod tests { &constraints, set, indoc! {r#" - <0> (U = bool) 2/4 - ┡━₁ <1> (T = bool) 4/4 + <0> (U = bool) + ┡━₁ <1> (T = bool) │ ┡━₁ always - │ ├─? <2> (T = str) 3/3 + │ ├─? <2> (T = str) │ │ ┡━₁ always │ │ ├─? never │ │ └─₀ never │ └─₀ never - ├─? <3> (U = str) 1/4 + ├─? <3> (U = str) │ ┡━₁ <1> SHARED │ ├─? never │ └─₀ never @@ -8340,7 +8427,7 @@ mod tests { &builder, t_int, indoc! {r#" - <0> (T = int) 1/1 + <0> (T = int) ┡━₁ always ├─? never └─₀ never @@ -8368,9 +8455,9 @@ mod tests { &builder, union, indoc! {r#" - <0> (U = str) 2/2 + <0> (U = str) ┡━₁ always - ├─? <1> (T = int) 1/1 + ├─? <1> (T = int) │ ┡━₁ always │ ├─? never │ └─₀ never @@ -8402,15 +8489,15 @@ mod tests { &builder, intersection, indoc! {r#" - <0> (U = int) 4/4 - ┡━₁ <1> (U = str) 2/2 + <0> (U = int) + ┡━₁ <1> (U = str) │ ┡━₁ always - │ ├─? <2> (T = int) 1/1 + │ ├─? <2> (T = int) │ │ ┡━₁ always │ │ ├─? never │ │ └─₀ never │ └─₀ never - ├─? <3> (T = bool) 3/3 + ├─? <3> (T = bool) │ ┡━₁ <1> SHARED │ ├─? never │ └─₀ never @@ -8435,10 +8522,10 @@ mod tests { &builder, negated, indoc! {r#" - <0> (U = str) 2/2 + <0> (U = str) ┡━₁ never ├─? never - └─₀ <1> (T = int) 1/1 + └─₀ <1> (T = int) ┡━₁ never ├─? never └─₀ always @@ -8521,7 +8608,11 @@ mod tests { } impl ReconstructPathFold { - fn result(&self, at: PathFoldBreak, result: NodeId) -> ControlFlow { + fn result( + &self, + at: PathFoldBreak, + result: (NodeId, Option), + ) -> ControlFlow)> { if self.break_at == Some(at) { ControlFlow::Break(at) } else { @@ -8531,7 +8622,7 @@ mod tests { } impl PathFold for ReconstructPathFold { - type Result = NodeId; + type Result = (NodeId, Option); type Break = PathFoldBreak; fn satisfied<'db>( @@ -8540,15 +8631,18 @@ mod tests { builder: &ConstraintSetBuilder<'db>, path: &PathAssignments, ) -> ControlFlow { - let result = path.assignments.iter().fold( - ALWAYS_TRUE, - |result, (assignment, (source_order, _))| { - result.and( - builder, - Node::new_satisfied_constraint(builder, *assignment, *source_order), - ) - }, - ); + let result = + path.assignments + .iter() + .fold((ALWAYS_TRUE, None), |result, (assignment, _)| { + let (node, source_order) = result; + let (assignment, assignment_source_order) = + Node::new_satisfied_constraint(builder, *assignment); + ( + node.and(builder, assignment), + builder.ordered_source_order(source_order, assignment_source_order), + ) + }); self.result(PathFoldBreak::Satisfied, result) } @@ -8558,7 +8652,7 @@ mod tests { _builder: &ConstraintSetBuilder<'db>, _path: &PathAssignments, ) -> ControlFlow { - self.result(PathFoldBreak::Unsatisfied, ALWAYS_FALSE) + self.result(PathFoldBreak::Unsatisfied, (ALWAYS_FALSE, None)) } fn impossible<'db>( @@ -8567,7 +8661,7 @@ mod tests { _builder: &ConstraintSetBuilder<'db>, _path: &PathAssignments, ) -> ControlFlow { - self.result(PathFoldBreak::Impossible, ALWAYS_FALSE) + self.result(PathFoldBreak::Impossible, (ALWAYS_FALSE, None)) } fn combine<'db>( @@ -8578,18 +8672,48 @@ mod tests { if_uncertain: Self::Result, if_false: Self::Result, ) -> ControlFlow { - let result = if_true.or(builder, if_uncertain).or(builder, if_false); - self.result(PathFoldBreak::Combine, result) + let (if_true, if_true_source_order) = if_true; + let (if_uncertain, if_uncertain_source_order) = if_uncertain; + let (if_false, if_false_source_order) = if_false; + let node = if_true.or(builder, if_uncertain).or(builder, if_false); + let source_order = + builder.ordered_source_order(if_true_source_order, if_uncertain_source_order); + let source_order = builder.ordered_source_order(source_order, if_false_source_order); + self.result(PathFoldBreak::Combine, (node, source_order)) } } - fn path_assignments_for(builder: &ConstraintSetBuilder<'_>, node: NodeId) -> PathAssignments { + fn path_assignments_for( + builder: &ConstraintSetBuilder<'_>, + node: NodeId, + source_order: Option, + ) -> PathAssignments { match node.node() { Node::AlwaysTrue | Node::AlwaysFalse => PathAssignments::new([]), - Node::Interior(interior) => interior.path_assignments(builder), + Node::Interior(interior) => interior.path_assignments(builder, source_order), } } + #[test] + fn path_assignments_follow_constraint_source_order() { + let db = setup_db(); + let t = create_typevar(&db, "T"); + let u = create_typevar(&db, "U"); + let builder = ConstraintSetBuilder::new(); + let t_int = create_constraint(&db, &builder, t, KnownClass::Int); + let u_str = create_constraint(&db, &builder, u, KnownClass::Str); + + // Construct the set in the opposite order from constraint creation. This ensures the + // initializer follows the sidecar rather than either TDD traversal or constraint IDs. + let set = u_str.and(&db, &builder, || t_int); + let path = path_assignments_for(&builder, set.node, set.source_order); + let expected = + [u_str.node, t_int.node].map(|node| builder.interior_node_data(node).constraint); + let actual: Vec<_> = path.discovered.keys().copied().collect(); + + assert_eq!(actual, expected); + } + #[test] fn path_fold_reconstructs_constraint_sets() { let db = setup_db(); @@ -8634,14 +8758,15 @@ mod tests { tautology, transitive, ] { - let mut path = path_assignments_for(&builder, set.node); + let mut path = path_assignments_for(&builder, set.node, set.source_order); let mut fold = ReconstructPathFold { break_at: None }; - let ControlFlow::Continue(reconstructed) = + let ControlFlow::Continue((reconstructed, reconstructed_source_order)) = path.visit(&db, &builder, set.node, &mut fold) else { panic!("reconstruction unexpectedly aborted"); }; - let reconstructed = ConstraintSet::from_node(&builder, reconstructed); + let reconstructed = + ConstraintSet::from_node(&builder, reconstructed, reconstructed_source_order); assert!( set.iff(&db, &builder, reconstructed) .is_always_satisfied(&db) @@ -8668,7 +8793,7 @@ mod tests { PathFoldBreak::Impossible, PathFoldBreak::Combine, ] { - let mut path = path_assignments_for(&builder, set.node); + let mut path = path_assignments_for(&builder, set.node, set.source_order); let mut aborting_fold = ReconstructPathFold { break_at: Some(break_at), }; @@ -8678,12 +8803,13 @@ mod tests { ); let mut completing_fold = ReconstructPathFold { break_at: None }; - let ControlFlow::Continue(reconstructed) = + let ControlFlow::Continue((reconstructed, reconstructed_source_order)) = path.visit(&db, &builder, set.node, &mut completing_fold) else { panic!("reconstruction unexpectedly aborted after {break_at:?}"); }; - let reconstructed = ConstraintSet::from_node(&builder, reconstructed); + let reconstructed = + ConstraintSet::from_node(&builder, reconstructed, reconstructed_source_order); assert!( set.iff(&db, &builder, reconstructed) .is_always_satisfied(&db) @@ -8727,6 +8853,32 @@ mod tests { assert!(tdd.iff(&db, &builder, negated).is_never_satisfied(&db)); } + #[test] + fn constraint_set_source_order_combination_is_idempotent() { + let db = setup_db(); + let t = create_typevar(&db, "T"); + let u = create_typevar(&db, "U"); + let builder = ConstraintSetBuilder::new(); + let t_int = create_constraint(&db, &builder, t, KnownClass::Int); + let u_str = create_constraint(&db, &builder, u, KnownClass::Str); + let combined = t_int.and(&db, &builder, || u_str); + + for original in [t_int, combined] { + let original_source_order_count = builder.storage.borrow().source_orders.len(); + let intersection = original.and(&db, &builder, || original); + let union = original.or(&db, &builder, || original); + + assert_eq!(intersection.node, original.node); + assert_eq!(intersection.source_order, original.source_order); + assert_eq!(union.node, original.node); + assert_eq!(union.source_order, original.source_order); + assert_eq!( + builder.storage.borrow().source_orders.len(), + original_source_order_count + ); + } + } + fn create_compacted_owned_set(db: &dyn Db) -> OwnedConstraintSet<'_> { let t = create_typevar(db, "T"); let u = create_typevar(db, "U"); @@ -8749,8 +8901,10 @@ mod tests { .expect("nonterminal root should retain storage"); assert_eq!(owned.node.index(), 2); + assert_eq!(owned.source_order.map(SourceOrderId::index), Some(0)); assert_eq!(inner.nodes.len(), 1); assert_eq!(inner.constraints.len(), 1); + assert_eq!(inner.source_orders.len(), 1); assert_eq!(inner.node_indices.len(), 3); assert_eq!(inner.constraint_indices.len(), 3); assert_eq!(inner.node_indices.iter_ones().collect::>(), vec![2]); @@ -8762,6 +8916,35 @@ mod tests { assert!(owned.node.index() >= inner.nodes.len()); } + #[test] + fn owned_constraint_set_source_order_ignores_construction_history() { + let db = setup_db(); + let t = create_typevar(&db, "T"); + let u = create_typevar(&db, "U"); + + let build = |include_redundant_combination| { + ConstraintSetBuilder::new().into_owned(|builder| { + let t_int = create_constraint(&db, builder, t, KnownClass::Int); + let u_str = create_constraint(&db, builder, u, KnownClass::Str); + let combined = t_int.and(&db, builder, || u_str); + + if include_redundant_combination { + // Repeating one constraint leaves the BDD and first-occurrence source order + // unchanged, but creates a distinct, reachable source-order tree. Both trees + // must compact to the same owned set. + let redundant = combined.and(&db, builder, || t_int); + assert_eq!(redundant.node, combined.node); + assert_ne!(redundant.source_order, combined.source_order); + redundant + } else { + combined + } + }) + }; + + assert_eq!(build(false), build(true)); + } + #[test] fn owned_constraint_set_query_reads_compacted_overlay() { let db = setup_db(); @@ -8773,7 +8956,7 @@ mod tests { builder, set, indoc! {r#" - <0> (V = bool) 1/1 + <0> (V = bool) ┡━₁ always ├─? never └─₀ never @@ -8794,7 +8977,7 @@ mod tests { let owned = create_compacted_owned_set(&db); owned.query(|builder, set| { - let (node_split, constraint_split, typevar_split) = { + let (node_split, constraint_split, typevar_split, source_order_split) = { let storage = builder.storage.borrow(); let compacted = storage .compacted @@ -8804,9 +8987,16 @@ mod tests { compacted.node_indices.len(), compacted.constraint_indices.len(), compacted.typevars.len(), + compacted.source_orders.len(), ) }; + let existing_constraint = builder.interior_node_data(set.node).constraint; + assert_eq!( + Some(builder.constraint_source_order(existing_constraint)), + set.source_order + ); + let w = create_typevar(&db, "W"); let w_str = create_constraint(&db, builder, w, KnownClass::Str); let new_constraint = w_str @@ -8817,6 +9007,11 @@ mod tests { assert!(w_str.node.index() >= node_split); assert!(new_constraint.index() >= constraint_split); assert!(builder.typevar_id(&db, w).index() >= typevar_split); + assert!( + w_str + .source_order + .is_some_and(|source_order| source_order.index() >= source_order_split) + ); let combined = set.and(&db, builder, || w_str); assert!(!combined.is_never_satisfied(&db)); @@ -8840,7 +9035,7 @@ mod tests { &builder, loaded, indoc! {r#" - <0> (V = bool) 1/1 + <0> (V = bool) ┡━₁ always ├─? never └─₀ never @@ -8892,9 +9087,9 @@ mod tests { builder, result, indoc! {r#" - <0> (U = str) 2/2 + <0> (U = str) ┡━₁ always - ├─? <1> (T = int) 1/1 + ├─? <1> (T = int) │ ┡━₁ always │ ├─? never │ └─₀ never @@ -8912,9 +9107,9 @@ mod tests { &builder, loaded, indoc! {r#" - <0> (U = str) 2/2 + <0> (U = str) ┡━₁ always - ├─? <1> (T = int) 1/1 + ├─? <1> (T = int) │ ┡━₁ always │ ├─? never │ └─₀ never From 8e5cd421366ce34503db3787e41628cf0f384e01 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:34:07 +0200 Subject: [PATCH 143/390] [ty] Decide satisfiability of simple typevar-free conjunctions directly (#27178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes astral-sh/ty#4089, where `ty check` took O(n²) time in the size of a literal union such as `mypy_boto3_ec2`'s `InstanceTypeType` (~1200 string literals). Checking iteration over `Sequence[Literal[...]]` builds a constraint set that is a single conjunction with one lower-bound constraint per union element. Deciding its satisfiability walks the BDD with `PathAssignments`, whose `discover_constraint` computes sequents for every pair of constraints — quadratic in the number of constraints. For lower-bound-only pairs, each pair computation also eagerly builds a `Literal[a] | Literal[b]` union in `ConstraintId::intersect`, only for the result to be discarded as `CannotSimplify`, so the entire quadratic pass produces empty sequent maps. This PR adds a linear fast path to `is_never_satisfied` for BDDs that are a single all-positive conjunction with typevar-free bounds: per typevar occurrence, check that the union of the lower bounds is assignable to each upper-bound clause. Assignability distributes over the union on the left and the intersection clauses on the right, so this finds exactly the contradictions that the walk's pairwise disjointness sequents detect. Bounds are grouped by occurrence identity so differently materialized instances of the same typevar are handled together. Type aliases and protocols can hide typevars in lazy attributes, so bounds containing either conservatively fall back to the general walk; this also avoids expanding recursively specialized aliases in the fast path. The existing `compute_simple_bound_conjunction` fast path used for solution extraction is updated to use the same identity and lazy-bound handling. For typevars with only upper-bound evidence, it skips quadratic per-clause redundancy pruning and lets the final intersection determine the solution. Timings for the issue's reproducer (debug build): 4.6s → 0.03s against an installed `mypy_boto3_ec2`, 2.3s → 0.03s for a synthetic 1200-literal union, and the runtime is now flat in the union size (5000 literals also check in ~0.03s). ## Test plan - Added benchmarks covering `Sequence[Literal[...]]` access and many contravariant callback arguments that produce upper-bound-only constraints. - Added unit tests covering satisfiable and contradictory simple conjunctions without sequent-cache growth, equivalence with the general path walk, differently materialized instances of the same typevar, hidden typevars in lazy aliases, and large upper-bound-only conjunctions. - The `ty_python_semantic` test suite passes. - Stable type property tests pass with 2000 generated cases. - Manually verified the issue's reproducer and upper-bound-only scaling. --------- Co-authored-by: Carl Meyer Co-authored-by: Douglas Creager --- crates/ruff_benchmark/benches/ty.rs | 74 ++ .../mdtest/generics/legacy/functions.md | 67 ++ .../mdtest/generics/pep695/functions.md | 84 +++ .../src/types/constraints.rs | 377 ++++++++-- .../ty_python_semantic/src/types/relation.rs | 668 +++++++++++------- .../src/types/signatures.rs | 2 +- 6 files changed, 952 insertions(+), 320 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index d908b0612a..e4e2322ee6 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -1551,6 +1551,41 @@ fn benchmark_factored_upper_bounds(criterion: &mut Criterion) { }); } +/// Guards against quadratic pruning when contravariant callbacks contribute many upper-only bounds. +fn benchmark_many_upper_bound_callbacks(criterion: &mut Criterion) { + const NUM_CALLBACKS: usize = 1_200; + + setup_rayon(); + + let mut code = String::from( + "from collections.abc import Callable\nfrom typing import Literal\n\ndef accepts[T](\n", + ); + for i in 0..NUM_CALLBACKS { + writeln!(&mut code, " cb{i}: Callable[[T], None],").ok(); + } + code.push_str(") -> None: ...\n\ndef call_many(\n"); + for i in 0..NUM_CALLBACKS { + writeln!(&mut code, " cb{i}: Callable[[Literal[{i}]], None],").ok(); + } + code.push_str(") -> None:\n accepts(\n"); + for i in 0..NUM_CALLBACKS { + writeln!(&mut code, " cb{i},").ok(); + } + code.push_str(" )\n"); + + criterion.bench_function("ty_micro[many_upper_bound_callbacks]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db, .. } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + fn benchmark_pandas_tdd(criterion: &mut Criterion) { setup_rayon(); let venv_path = setup_micro_case_venv("pandas_tdd", &["pandas-stubs"]); @@ -1758,6 +1793,43 @@ def perform(rows: Rows) -> AllResults: }); } +fn benchmark_sequence_literal_union_access(criterion: &mut Criterion) { + const NUM_LITERALS: usize = 1_200; + + setup_rayon(); + + // Regression benchmark for https://github.com/astral-sh/ty/issues/4089. + let mut code = String::from( + "from collections.abc import Sequence\nfrom typing import Literal\n\nItem = Literal[\n", + ); + for i in 0..NUM_LITERALS { + writeln!(&mut code, " 'value-{i}',").ok(); + } + code.push_str( + r#"] + +def iterate(items: Sequence[Item]) -> None: + for item in items: + pass + +def access(items: Sequence[Item]) -> None: + items[0] +"#, + ); + + criterion.bench_function("ty_micro[sequence_literal_union_access]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db, .. } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + fn benchmark_invariant_generic_union_bound(criterion: &mut Criterion) { const NUM_ALIASES: usize = 64; @@ -2089,10 +2161,12 @@ criterion_group!( benchmark_typeis_narrowing, benchmark_repeated_statement_calls, benchmark_factored_upper_bounds, + benchmark_many_upper_bound_callbacks, benchmark_pandas_tdd, benchmark_mixed_typed_dict_union_copy, benchmark_recursive_typed_dict_union_contextual_inference, benchmark_invariant_generic_return_union, + benchmark_sequence_literal_union_access, benchmark_invariant_generic_union_bound, benchmark_many_invariant_typevars, benchmark_pydantic_core_schema_dict, diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index 2e15f2b689..502e6fa6b0 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -1300,6 +1300,73 @@ reveal_type(narrow(1)) # revealed: int reveal_type(narrow("hello")) # revealed: str ``` +## Redundant callback bounds preserve constrained type-variable relationships + +A contravariant callback can contribute both another constrained type variable and a redundant +`object` upper bound. The inferred result must retain the other type variable in either callback +order. + +```py +from collections.abc import Callable +from typing import TypeVar + +T = TypeVar("T", int, str) +S = TypeVar("S", int, str) + +def select(first: Callable[[T], None], second: Callable[[T], None]) -> T: + raise NotImplementedError + +def forward_object(specific: Callable[[S], None], redundant: Callable[[object], None]) -> S: + result = select(specific, redundant) + reveal_type(result) # revealed: S@forward_object + return result + +def forward_object_reversed(specific: Callable[[S], None], redundant: Callable[[object], None]) -> S: + result = select(redundant, specific) + reveal_type(result) # revealed: S@forward_object_reversed + return result +``` + +A union of the type variable's constraints is also a redundant upper bound, even though it is not +`object`. + +```py +def forward_union(specific: Callable[[S], None], redundant: Callable[[int | str], None]) -> S: + result = select(specific, redundant) + reveal_type(result) # revealed: S@forward_union + return result + +def forward_union_reversed(specific: Callable[[S], None], redundant: Callable[[int | str], None]) -> S: + result = select(redundant, specific) + reveal_type(result) # revealed: S@forward_union_reversed + return result +``` + +The same relationship must survive a redundant, non-`object` nominal superclass shared by both +constraints. + +```py +class Base: ... +class Left(Base): ... +class Right(Base): ... + +TNominal = TypeVar("TNominal", Left, Right) +SNominal = TypeVar("SNominal", Left, Right) + +def select_nominal(first: Callable[[TNominal], None], second: Callable[[TNominal], None]) -> TNominal: + raise NotImplementedError + +def forward_nominal(specific: Callable[[SNominal], None], redundant: Callable[[Base], None]) -> SNominal: + result = select_nominal(specific, redundant) + reveal_type(result) # revealed: SNominal@forward_nominal + return result + +def forward_nominal_reversed(specific: Callable[[SNominal], None], redundant: Callable[[Base], None]) -> SNominal: + result = select_nominal(redundant, specific) + reveal_type(result) # revealed: SNominal@forward_nominal_reversed + return result +``` + ## Incompatible constraint sets But a constrained TypeVar with constraints not satisfied by the formal TypeVar should still error: diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 50cb67d32b..a611e9d970 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -1405,6 +1405,90 @@ def g[S: (bool, str)](x: S) -> S: return f(x) # error: [invalid-argument-type] ``` +## Redundant callback bounds preserve constrained type-variable relationships + +A contravariant callback can contribute both another constrained type variable and a redundant +`object` upper bound. The inferred result must retain the other type variable in either callback +order. + +```py +from collections.abc import Callable + +def select[T: (int, str)]( + first: Callable[[T], None], + second: Callable[[T], None], +) -> T: + raise NotImplementedError + +def forward_object[S: (int, str)]( + specific: Callable[[S], None], + redundant: Callable[[object], None], +) -> S: + result = select(specific, redundant) + reveal_type(result) # revealed: S@forward_object + return result + +def forward_object_reversed[S: (int, str)]( + specific: Callable[[S], None], + redundant: Callable[[object], None], +) -> S: + result = select(redundant, specific) + reveal_type(result) # revealed: S@forward_object_reversed + return result +``` + +A union of the type variable's constraints is also a redundant upper bound, even though it is not +`object`. + +```py +def forward_union[S: (int, str)]( + specific: Callable[[S], None], + redundant: Callable[[int | str], None], +) -> S: + result = select(specific, redundant) + reveal_type(result) # revealed: S@forward_union + return result + +def forward_union_reversed[S: (int, str)]( + specific: Callable[[S], None], + redundant: Callable[[int | str], None], +) -> S: + result = select(redundant, specific) + reveal_type(result) # revealed: S@forward_union_reversed + return result +``` + +The same relationship must survive a redundant, non-`object` nominal superclass shared by both +constraints. + +```py +class Base: ... +class Left(Base): ... +class Right(Base): ... + +def select_nominal[T: (Left, Right)]( + first: Callable[[T], None], + second: Callable[[T], None], +) -> T: + raise NotImplementedError + +def forward_nominal[S: (Left, Right)]( + specific: Callable[[S], None], + redundant: Callable[[Base], None], +) -> S: + result = select_nominal(specific, redundant) + reveal_type(result) # revealed: S@forward_nominal + return result + +def forward_nominal_reversed[S: (Left, Right)]( + specific: Callable[[S], None], + redundant: Callable[[Base], None], +) -> S: + result = select_nominal(redundant, specific) + reveal_type(result) # revealed: S@forward_nominal_reversed + return result +``` + ## Display ordering Where possible, we want the types that appear in inferred specializations to line up with the types diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index db5967de44..723347363a 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -1655,9 +1655,9 @@ impl<'db> ConstraintBounds<'db> { /// constraints) we solve to `Unknown`. An upper bound of `object` is treated as an explicit /// request for "any type" as a solution, so we solve it to `object`. /// -/// As an optimization, we will remove redundant clauses as we build up an `UpperBound`. This -/// reduces the amount of work `IntersectionBuilder` needs to do when producing the solution for -/// this upper bound. +/// Redundant clauses are retained while accumulating the bound, avoiding repeated relation checks +/// for every newly discovered clause. Consumers that require one effective bound can recover it +/// with [`UpperBound::as_single_bound`] without eagerly expanding large intersections of unions. #[derive(Clone, Debug, Default, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] pub(crate) struct UpperBound<'db> { clauses: FxOrderSet>, @@ -1671,25 +1671,12 @@ impl<'db> UpperBound<'db> { /// Creates an upper bound from one explicit clause. /// /// This preserves an explicit `object` clause so callers can distinguish `T <= object` from a - /// missing upper bound. Use [`UpperBound::add_clause`] when accumulating clauses that should - /// be canonicalized by redundancy pruning. + /// missing upper bound. Use [`UpperBound::add_clause`] when accumulating multiple clauses. pub(crate) fn from_clause(clause: Type<'db>) -> Self { let clauses = FxOrderSet::from_iter([clause]); Self { clauses } } - #[cfg(test)] - pub(crate) fn from_clauses( - db: &'db dyn Db, - clauses: impl IntoIterator>, - ) -> Self { - let mut upper = Self::none(); - for clause in clauses { - upper.add_clause(db, clause); - } - upper - } - pub(crate) fn is_empty(&self) -> bool { self.clauses.is_empty() } @@ -1698,21 +1685,34 @@ impl<'db> UpperBound<'db> { !self.is_empty() } - pub(crate) fn as_single_bound(&self) -> Option> { - if self.clauses.len() != 1 { - return None; - } - self.clauses.first().copied() + /// Returns an existing upper-bound clause if every other clause is redundant with it. + /// + /// This preserves constrained type variables without distributing unions: expanding + /// `S & (int | str)` into `(S & int) | (S & str)` would otherwise lose `S` as the single + /// effective bound. Returns `None` instead of materializing intersections when no existing + /// clause dominates the others. A missing bound remains distinct from an explicit `object`. + pub(crate) fn as_single_bound(&self, db: &'db dyn Db) -> Option> { + let mut clauses = self.clauses.iter().copied(); + let first = clauses.next()?; + let candidate = clauses.fold(first, |candidate, clause| { + if candidate.is_redundant_with(db, clause) { + candidate + } else { + clause + } + }); + + self.clauses + .iter() + .all(|clause| candidate.is_redundant_with(db, *clause)) + .then_some(candidate) } fn is_never(&self) -> bool { self.clauses.len() == 1 && self.clauses.contains(&Type::Never) } - pub(crate) fn add_clause(&mut self, db: &'db dyn Db, clause: Type<'db>) { - // This `Never` fast path is an optimization. The general redundancy-pruning loop below - // should also handle it correctly, but spelling it out avoids unnecessary relation checks - // and keeps the stored representation canonical. + pub(crate) fn add_clause(&mut self, clause: Type<'db>) { if self.is_never() { return; } @@ -1723,26 +1723,6 @@ impl<'db> UpperBound<'db> { return; } - // Do not special-case `object` here. An explicit `object` clause should be preserved when - // it is the only clause, so `T <= object` remains distinguishable from a missing upper - // bound. If another clause already exists, the general redundancy check below treats - // `object` as redundant; if a narrower clause is added later, the retain step removes the - // existing `object` clause. - // - // First check if there's an existing upper bound clause that is a subtype of the new type. - // If so, adding the new type does nothing to the intersection. - if self - .clauses - .iter() - .any(|existing| existing.is_redundant_with(db, clause)) - { - return; - } - - // Otherwise remove any existing clauses that are a supertype of the new type, since the - // intersection will clip them to the new type. - self.clauses - .retain(|existing| !clause.is_redundant_with(db, *existing)); self.clauses.insert(clause); } @@ -2232,10 +2212,10 @@ impl ConstraintId { }; let mut merged_upper = UpperBound::none(); if let Some(upper) = self_constraint.bounds.upper { - merged_upper.add_clause(db, upper); + merged_upper.add_clause(upper); } if let Some(upper) = other_constraint.bounds.upper { - merged_upper.add_clause(db, upper); + merged_upper.add_clause(upper); } let effective_lower = lower.unwrap_or(Type::Never); @@ -2543,6 +2523,46 @@ impl NodeId { builder: &ConstraintSetBuilder<'db>, source_order: Option, ) -> bool { + /// Checks whether this BDD is a single conjunction, where either (a) every constraint is + /// positive lower-bound-only, or (b) every constraint is a positive upper-bound-only. If + /// so, `object` or `Never` respectively is a valid solution regardless of the contents of + /// the constraints. + fn simple_conjunction_is_satisfiable( + builder: &ConstraintSetBuilder<'_>, + mut node: NodeId, + ) -> bool { + let mut found_lower = false; + let mut found_upper = false; + loop { + match node.node() { + Node::AlwaysTrue => return true, + Node::AlwaysFalse => return false, + + Node::Interior(_) => { + let interior = builder.interior_node_data(node); + + if interior.if_false != ALWAYS_FALSE + || interior.if_uncertain != ALWAYS_FALSE + { + // Not a single conjunction + return false; + } + + let constraint = builder.constraint_data(interior.constraint); + found_lower |= constraint.bounds.lower.is_some(); + found_upper |= constraint.bounds.upper.is_some(); + if found_lower && found_upper { + // Might be a single conjunction, but doesn't contain _only_ + // lower-bound-only or upper-bound-only constraints + return false; + } + + node = interior.if_true; + } + } + } + } + match self.node() { Node::AlwaysTrue => false, Node::AlwaysFalse => true, @@ -2551,10 +2571,13 @@ impl NodeId { return *result; } - let mut path = interior.path_assignments(builder, source_order); - let result = path - .visit(db, builder, self, &mut IsNeverSatisfiedVisitor) - .is_continue(); + let result = if simple_conjunction_is_satisfiable(builder, self) { + false + } else { + let mut path = interior.path_assignments(builder, source_order); + path.visit(db, builder, self, &mut IsNeverSatisfiedVisitor) + .is_continue() + }; builder .storage .borrow_mut() @@ -3428,7 +3451,7 @@ impl<'db> ConstraintBoundsBuilder<'db> { fn add_upper(&mut self, db: &'db dyn Db, ty: Type<'db>) { self.classify_evidence(db, ty); - self.upper.add_clause(db, ty); + self.upper.add_clause(ty); } fn finish(self, db: &'db dyn Db, bound_typevar: BoundTypeVarInstance<'db>) -> PathBound<'db> { @@ -3973,7 +3996,7 @@ impl<'db> PathBounds<'db> { }; if let (Some(ty @ Type::TypeVar(_)), _) | (_, Some(ty @ Type::TypeVar(_))) = - (path_bound.lower, path_bound.upper.as_single_bound()) + (path_bound.lower, path_bound.upper.as_single_bound(db)) { // This path relates two TypeVars, such as passing `S` to a parameter typed as // `T: (int, str)`. The compatibility check above has verified that at least @@ -5225,6 +5248,46 @@ impl SequentMap { Ref::map(storage, |storage| &storage.pair_sequent_cache[&key]) } + /// Quickly determines whether two constraints cannot possibly produce any sequents when passed + /// to [`for_constraint_pair`][Self::for_constraint_pair]. If this returns `true`, it is safe + /// to skip calling `for_constraint_pair` for this pair of constraints. + fn pair_cannot_produce_sequents<'db>( + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + left: ConstraintId, + right: ConstraintId, + ) -> bool { + // Currently, the only pattern we look for is when two constraints that have _only_ lower + // bounds, where those lower bounds are disjoint. Given `l₁ ≤ T ∧ l₂ ≤ T`, the only + // sequent we could theoretically produce is `(l₁ | l₂) ≤ T`. But we don't store that as a + // single constraint; we always break that apart into the two smaller constraints that we + // started with. + + let left = builder.constraint_data(left); + let right = builder.constraint_data(right); + if !left.typevar.is_same_typevar_as(db, right.typevar) { + return false; + } + + let ( + ConstraintBounds { + lower: Some(left_lower), + upper: None, + }, + ConstraintBounds { + lower: Some(right_lower), + upper: None, + }, + ) = (left.bounds, right.bounds) + else { + return false; + }; + + left_lower + .when_trivially_disjoint_from(db, right_lower, builder, TypeVarSet::None) + .is_trivially_always_satisfied() + } + fn add_single_tautology(&mut self, ante: ConstraintId) { self.sequents.push(Sequent::SingleTautology { ante }); } @@ -6555,6 +6618,8 @@ pub(crate) struct PathAssignments { /// ensures a stable order for all of the derived constraints that we create, while still /// letting us create them lazily.) discovered: FxIndexMap, + /// Constraint pairs that we have already checked and added to `sequents`. + elaborated_pairs: FxHashSet<(ConstraintId, ConstraintId)>, /// Derived assignments that have been queued up to be added to the current path. assignment_queue: VecDeque<(ConstraintAssignment, AssignmentFuel)>, @@ -6630,6 +6695,7 @@ impl PathAssignments { assignments: FxIndexMap::default(), additional_fuels: Vec::default(), discovered, + elaborated_pairs: FxHashSet::default(), remaining_overall_fuel: OVERALL_FUEL_BUDGET, assignment_queue: VecDeque::default(), new_assignments: FxIndexMap::default(), @@ -6915,7 +6981,7 @@ impl PathAssignments { constraint: ConstraintId, ) { // If we've already processed this constraint, we can skip it. - let existing = self.discovered.insert(constraint, true); + let (constraint_index, existing) = self.discovered.insert_full(constraint, true); let already_processed = existing.is_some_and(|existing| existing); if already_processed { return; @@ -6925,8 +6991,26 @@ impl PathAssignments { self.sequents.extend_from_slice(&single_map.sequents); drop(single_map); - for existing in self.discovered.keys().dropping_back(1) { - let pair_map = SequentMap::for_constraint_pair(db, builder, *existing, constraint); + for (existing_index, (existing, _)) in self.discovered.iter().enumerate() { + if *existing == constraint { + continue; + } + + if SequentMap::pair_cannot_produce_sequents(db, builder, *existing, constraint) { + continue; + } + + let (a, b) = if existing_index < constraint_index { + (*existing, constraint) + } else { + (constraint, *existing) + }; + if !self.elaborated_pairs.insert((a, b)) { + // We've already elaborated this pair of constraints. + continue; + } + + let pair_map = SequentMap::for_constraint_pair(db, builder, a, b); self.sequents.extend_from_slice(&pair_map.sequents); } } @@ -7534,7 +7618,7 @@ mod tests { use crate::db::tests::setup_db; use crate::types::generics::ApplySpecialization; - use crate::types::{BoundTypeVarInstance, KnownClass, TypeVarVariance}; + use crate::types::{BoundTypeVarInstance, KnownClass, SubclassOfType, TypeVarVariance}; use ruff_python_ast::name::Name; fn create_typevar<'db>(db: &'db dyn Db, name: &'static str) -> BoundTypeVarInstance<'db> { @@ -7624,36 +7708,181 @@ mod tests { } #[test] - fn upper_bound_prunes_duplicates_and_redundant_supertypes() { + fn upper_bound_collapses_never() { + let db = setup_db(); + let int = known_instance(&db, KnownClass::Int); + + let mut upper = UpperBound::from_clause(int); + upper.add_clause(Type::Never); + assert_eq!(upper.clauses, FxOrderSet::from_iter([Type::Never])); + assert_eq!(upper.materialize_exact(&db), Type::Never); + + upper.add_clause(int); + assert_eq!(upper.clauses, FxOrderSet::from_iter([Type::Never])); + } + + #[test] + fn upper_bound_recovers_redundant_single_bounds() { let db = setup_db(); let int = known_instance(&db, KnownClass::Int); let bool = known_instance(&db, KnownClass::Bool); let str = known_instance(&db, KnownClass::Str); + let int_or_str = UnionType::from_two_elements(&db, int, str); + let u = create_typevar(&db, "U").map_bound_or_constraints(&db, |_| { + Some(TypeVarBoundOrConstraints::UpperBound(int_or_str)) + }); + let u = Type::TypeVar(u); + + for (clauses, expected) in [ + ([Type::object(), int], int), + ([int, Type::object()], int), + ([int, bool], bool), + ([bool, int], bool), + ([int_or_str, u], u), + ([u, int_or_str], u), + ] { + let mut upper = UpperBound::none(); + for clause in clauses { + upper.add_clause(clause); + } - let mut upper = UpperBound::from_clauses(&db, [int, str, int]); - assert_eq!(upper.clauses, FxOrderSet::from_iter([int, str])); + assert_eq!(upper.clauses.len(), 2); + assert_eq!(upper.as_single_bound(&db), Some(expected)); + } + } - // `bool` is narrower than `int`, so it replaces the redundant `int` clause while - // preserving the relative order of the remaining clauses. - upper.add_clause(&db, bool); - assert_eq!(upper.clauses, FxOrderSet::from_iter([str, bool])); + #[test] + fn upper_bound_distinguishes_missing_bound_from_explicit_object() { + let db = setup_db(); - upper.add_clause(&db, int); - assert_eq!(upper.clauses, FxOrderSet::from_iter([str, bool])); + assert_eq!(UpperBound::none().as_single_bound(&db), None); + assert_eq!( + UpperBound::from_clause(Type::object()).as_single_bound(&db), + Some(Type::object()) + ); } #[test] - fn upper_bound_collapses_never() { + fn upper_bound_does_not_materialize_overlapping_union_clauses() { let db = setup_db(); let int = known_instance(&db, KnownClass::Int); + let str = known_instance(&db, KnownClass::Str); + let bytes = known_instance(&db, KnownClass::Bytes); + let int_or_str = UnionType::from_two_elements(&db, int, str); + let int_or_bytes = UnionType::from_two_elements(&db, int, bytes); - let mut upper = UpperBound::from_clause(int); - upper.add_clause(&db, Type::Never); - assert_eq!(upper.clauses, FxOrderSet::from_iter([Type::Never])); - assert_eq!(upper.materialize_exact(&db), Type::Never); + for clauses in [[int_or_str, int_or_bytes], [int_or_bytes, int_or_str]] { + let mut upper = UpperBound::none(); + for clause in clauses { + upper.add_clause(clause); + } - upper.add_clause(&db, int); - assert_eq!(upper.clauses, FxOrderSet::from_iter([Type::Never])); + assert_eq!(upper.materialize_exact(&db), int); + assert_eq!(upper.as_single_bound(&db), None); + } + } + + #[test] + fn upper_bound_does_not_treat_nontrivial_intersection_as_single_bound() { + let db = setup_db(); + let int = known_instance(&db, KnownClass::Int); + let u = Type::TypeVar(create_typevar(&db, "U")); + let mut upper = UpperBound::from_clause(u); + upper.add_clause(int); + + assert!(upper.materialize_exact(&db).is_nontrivial_intersection(&db)); + assert_eq!(upper.as_single_bound(&db), None); + } + + #[test] + fn trivial_disjointness_does_not_claim_bounded_typevar_class_is_disjoint() { + let db = setup_db(); + let builder = ConstraintSetBuilder::new(); + let bool = known_instance(&db, KnownClass::Bool); + let u = create_typevar(&db, "U") + .map_bound_or_constraints(&db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); + let type_of_u = SubclassOfType::from(&db, u); + let bool_class = KnownClass::Bool.to_class_literal(&db); + + for (left, right) in [(type_of_u, bool_class), (bool_class, type_of_u)] { + let trivial = left.when_trivially_disjoint_from(&db, right, &builder, TypeVarSet::None); + let full = left.when_disjoint_from(&db, right, &builder, TypeVarSet::None); + + assert!(trivial.is_trivially_never_satisfied()); + assert!(!full.is_always_satisfied(&db)); + } + } + + #[test] + fn trivial_disjointness_implies_full_disjointness() { + let db = setup_db(); + let builder = ConstraintSetBuilder::new(); + let bool = known_instance(&db, KnownClass::Bool); + let u = create_typevar(&db, "U") + .map_bound_or_constraints(&db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); + let types = [ + Type::Never, + Type::object(), + bool, + known_instance(&db, KnownClass::Int), + known_instance(&db, KnownClass::Str), + Type::int_literal(0), + Type::int_literal(1), + Type::bool_literal(true), + Type::bool_literal(false), + Type::string_literal(&db, "value"), + KnownClass::Bool.to_class_literal(&db), + KnownClass::Int.to_class_literal(&db), + SubclassOfType::from(&db, u), + ]; + let mut positive_results = 0; + + for left in types { + for right in types { + let trivial = + left.when_trivially_disjoint_from(&db, right, &builder, TypeVarSet::None); + if trivial.is_trivially_always_satisfied() { + positive_results += 1; + assert!( + left.when_disjoint_from(&db, right, &builder, TypeVarSet::None) + .is_always_satisfied(&db), + "cheap disjointness incorrectly accepts `{}` and `{}`", + left.display(&db), + right.display(&db) + ); + } + } + } + + assert!(positive_results > 0); + } + + #[test] + fn overlapping_lower_bounds_do_not_skip_nonempty_sequent_map() { + let db = setup_db(); + let builder = ConstraintSetBuilder::new(); + let t = create_typevar(&db, "T"); + let bool = known_instance(&db, KnownClass::Bool); + let u = create_typevar(&db, "U") + .map_bound_or_constraints(&db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); + let type_of_u = SubclassOfType::from(&db, u); + let bool_class = KnownClass::Bool.to_class_literal(&db); + let left = ConstraintId::new_with_bounds(&db, &builder, t, Some(type_of_u), None); + let right = ConstraintId::new_with_bounds(&db, &builder, t, Some(bool_class), None); + + for (left, right) in [(left, right), (right, left)] { + let sequents = SequentMap::for_constraint_pair(&db, &builder, left, right); + + assert!( + sequents + .sequents + .iter() + .any(|sequent| matches!(sequent, Sequent::SingleImplication { .. })) + ); + assert!(!SequentMap::pair_cannot_produce_sequents( + &db, &builder, left, right + )); + } } #[test] diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 0f4ef6cfbf..049b0cfd2b 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -351,6 +351,7 @@ impl<'db> Type<'db> { typevar_evaluation: TypeVarEvaluation::Eager, context_tree: None, given: assuming, + perform_expensive_checks: true, relation_visitor: &relation_visitor, disjointness_visitor: &disjointness_visitor, signature_relation_visitor: &signature_relation_visitor, @@ -388,6 +389,7 @@ impl<'db> Type<'db> { typevar_evaluation: TypeVarEvaluation::Eager, context_tree: Some(ErrorContextTree::new()), given: ConstraintSet::from_bool(&builder, false), + perform_expensive_checks: true, relation_visitor: &HasRelationToVisitor::default(&builder), disjointness_visitor: &IsDisjointVisitor::default(&builder), signature_relation_visitor: &SignatureRelationVisitor::default(), @@ -595,6 +597,7 @@ impl<'db> Type<'db> { typevar_evaluation, context_tree: None, given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, relation_visitor: &relation_visitor, disjointness_visitor: &disjointness_visitor, signature_relation_visitor: &signature_relation_visitor, @@ -663,6 +666,7 @@ impl<'db> Type<'db> { let checker = EquivalenceChecker { constraints, given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, relation_visitor: &relation_visitor, disjointness_visitor: &disjointness_visitor, signature_relation_visitor: &signature_relation_visitor, @@ -707,6 +711,34 @@ impl<'db> Type<'db> { constraints, inferable, given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, + disjointness_visitor: &disjointness_visitor, + relation_visitor: &relation_visitor, + signature_relation_visitor: &signature_relation_visitor, + materialization_visitor: &materialization_visitor, + }; + checker.check_type_pair(db, self, other) + } + + /// Checks whether `self` is disjoint from `other`, while being more accepting of false + /// negatives. Use this when you want to _quickly_ check whether two types are _definitely_ + /// disjoint, typically for engaging a fast path in some algorithm. + pub(crate) fn when_trivially_disjoint_from<'c>( + self, + db: &'db dyn Db, + other: Type<'db>, + constraints: &'c ConstraintSetBuilder<'db>, + inferable: TypeVarSet<'db>, + ) -> ConstraintSet<'db, 'c> { + let relation_visitor = HasRelationToVisitor::default(constraints); + let disjointness_visitor = IsDisjointVisitor::default(constraints); + let signature_relation_visitor = SignatureRelationVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::default(); + let checker = DisjointnessChecker { + constraints, + inferable, + given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: false, disjointness_visitor: &disjointness_visitor, relation_visitor: &relation_visitor, signature_relation_visitor: &signature_relation_visitor, @@ -776,6 +808,7 @@ pub(super) struct TypeRelationChecker<'a, 'c, 'db> { pub(super) typevar_evaluation: TypeVarEvaluation, context_tree: Option>, pub(super) given: ConstraintSet<'db, 'c>, + perform_expensive_checks: bool, // N.B. these fields are private to reduce the risk of // "double-visiting" a given pair of types. You should @@ -805,6 +838,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { typevar_evaluation: TypeVarEvaluation::Eager, context_tree: None, given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, relation_visitor, disjointness_visitor, signature_relation_visitor, @@ -826,6 +860,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { typevar_evaluation: TypeVarEvaluation::Lazy, context_tree: None, given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, relation_visitor, disjointness_visitor, signature_relation_visitor, @@ -847,6 +882,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { typevar_evaluation: TypeVarEvaluation::Lazy, context_tree: Some(ErrorContextTree::new()), given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, relation_visitor, disjointness_visitor, signature_relation_visitor, @@ -868,6 +904,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { typevar_evaluation: TypeVarEvaluation::Eager, context_tree: Some(ErrorContextTree::new()), given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, relation_visitor, disjointness_visitor, signature_relation_visitor, @@ -2394,6 +2431,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { EquivalenceChecker { constraints: self.constraints, given: self.given, + perform_expensive_checks: self.perform_expensive_checks, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, signature_relation_visitor: self.signature_relation_visitor, @@ -2406,6 +2444,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { constraints: self.constraints, inferable: self.inferable, given: self.given, + perform_expensive_checks: self.perform_expensive_checks, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, signature_relation_visitor: self.signature_relation_visitor, @@ -2444,6 +2483,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { pub(super) struct EquivalenceChecker<'a, 'c, 'db> { pub(super) constraints: &'c ConstraintSetBuilder<'db>, given: ConstraintSet<'db, 'c>, + perform_expensive_checks: bool, // N.B. these fields are private to reduce the risk of // "double-visiting" a given pair of types. You should @@ -2468,6 +2508,7 @@ impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { constraints: self.constraints, context_tree: None, given: self.given, + perform_expensive_checks: self.perform_expensive_checks, inferable: TypeVarSet::None, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, @@ -2510,6 +2551,7 @@ pub(super) struct DisjointnessChecker<'a, 'c, 'db> { pub(super) constraints: &'c ConstraintSetBuilder<'db>, pub(super) inferable: TypeVarSet<'db>, given: ConstraintSet<'db, 'c>, + perform_expensive_checks: bool, // N.B. these fields are private to reduce the risk of // "double-visiting" a given pair of types. You should @@ -2536,6 +2578,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { constraints, inferable, given: ConstraintSet::from_bool(constraints, false), + perform_expensive_checks: true, disjointness_visitor, relation_visitor, signature_relation_visitor, @@ -2554,6 +2597,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { inferable: self.inferable, context_tree: None, given: self.given, + perform_expensive_checks: self.perform_expensive_checks, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, signature_relation_visitor: self.signature_relation_visitor, @@ -2565,6 +2609,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { EquivalenceChecker { constraints: self.constraints, given: self.given, + perform_expensive_checks: self.perform_expensive_checks, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, signature_relation_visitor: self.signature_relation_visitor, @@ -2653,6 +2698,21 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { left: Type<'db>, right: Type<'db>, ) -> ConstraintSet<'db, 'c> { + /// This lets us clearly mark below which match arms require a non-trivial amount of work + /// to calculate, without sacrificing match guard exhaustiveness checks. If we are not + /// performing expensive checks, then we will conservatively report that the two types are + /// not disjoint. + fn nontrivial_check<'db, 'c>( + checker: &DisjointnessChecker<'_, 'c, 'db>, + check: impl FnOnce() -> ConstraintSet<'db, 'c>, + ) -> ConstraintSet<'db, 'c> { + if checker.perform_expensive_checks { + check() + } else { + checker.never() + } + } + if let Some(left) = left.materialized_divergent_fallback() { return self.check_type_pair(db, left, right); } @@ -2667,32 +2727,38 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { (Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => self.never(), (Type::Divergent(_), _) | (_, Type::Divergent(_)) => self.never(), - (Type::TypeAlias(alias), _) => { + (Type::TypeAlias(alias), _) => nontrivial_check(self, || { let left_alias_ty = alias.value_type(db); self.with_recursion_guard(db, left, right, || { self.check_type_pair(db, left_alias_ty, right) }) - } + }), - (_, Type::TypeAlias(alias)) => { + (_, Type::TypeAlias(alias)) => nontrivial_check(self, || { let right_alias_ty = alias.value_type(db); self.with_recursion_guard(db, left, right, || { self.check_type_pair(db, left, right_alias_ty) }) - } + }), - (Type::EnumComplement(complement), other) => { + (Type::EnumComplement(complement), other) => nontrivial_check(self, || { self.check_type_pair(db, complement.remaining_literal_union(db), other) - } + }), - (other, Type::EnumComplement(complement)) => { + (other, Type::EnumComplement(complement)) => nontrivial_check(self, || { self.check_type_pair(db, other, complement.remaining_literal_union(db)) - } + }), // `type[T]` and `TypeForm[S]` overlap whenever their represented instance types do. (Type::SubclassOf(subclass_of), Type::TypeForm(typeform)) | (Type::TypeForm(typeform), Type::SubclassOf(subclass_of)) => { - self.check_type_pair(db, subclass_of.to_instance(db), typeform.type_argument(db)) + nontrivial_check(self, || { + self.check_type_pair( + db, + subclass_of.to_instance(db), + typeform.type_argument(db), + ) + }) } // `type[T]` is disjoint from a callable or protocol instance if its upper bound or constraints are. @@ -2708,7 +2774,9 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { .with_transposed_type_var(db) .into_type_var() => { - self.check_type_pair(db, Type::TypeVar(type_var), other) + nontrivial_check(self, || { + self.check_type_pair(db, Type::TypeVar(type_var), other) + }) } // `type[T]` is disjoint from a class object `A` if every instance of `T` is disjoint from an instance of `A`. @@ -2716,7 +2784,9 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { if let Some(type_var) = subclass_of.into_type_var() && let Some(instance) = other.to_instance_approximation(db) => { - self.check_type_pair(db, Type::TypeVar(type_var), instance) + nontrivial_check(self, || { + self.check_type_pair(db, Type::TypeVar(type_var), instance) + }) } // A typevar is never disjoint from itself, since all occurrences of the typevar must @@ -2745,7 +2815,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { (Type::TypeVar(tvar), other) | (other, Type::TypeVar(tvar)) if !tvar.is_inferable(db, self.inferable) => { - match tvar.typevar(db).bound_or_constraints(db) { + nontrivial_check(self, || match tvar.typevar(db).bound_or_constraints(db) { None => self.never(), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { self.check_type_pair(db, bound, other) @@ -2757,61 +2827,69 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { |constraint| self.check_type_pair(db, *constraint, other), ) } - } + }) } // TODO: Infer specializations here (Type::TypeVar(_), _) | (_, Type::TypeVar(_)) => self.never(), - (Type::Union(union), other) | (other, Type::Union(union)) => union - .elements(db) - .iter() - .when_all(db, self.constraints, |e| { - self.check_type_pair(db, *e, other) - }), + (Type::Union(union), other) | (other, Type::Union(union)) => { + nontrivial_check(self, || { + union + .elements(db) + .iter() + .when_all(db, self.constraints, |e| { + self.check_type_pair(db, *e, other) + }) + }) + } // If we have two intersections, we test the positive elements of each one against the other intersection // Negative elements need a positive element on the other side in order to be disjoint. // This is similar to what would happen if we tried to build a new intersection that combines the two (Type::Intersection(left_intersection), Type::Intersection(right_intersection)) => { - if let Some(alternatives) = left_intersection.finite_alternative_union(db) { - self.check_type_pair(db, alternatives, right) - } else if let Some(alternatives) = right_intersection.finite_alternative_union(db) { - self.check_type_pair(db, left, alternatives) - } else { - self.with_recursion_guard(db, left, right, || { - left_intersection - .positive(db) - .iter() - .when_any(db, self.constraints, |&pos_ty| { - self.check_type_pair(db, pos_ty, right) - }) - .or(db, self.constraints, || { - right_intersection.positive(db).iter().when_any( - db, - self.constraints, - |&pos_ty| self.check_type_pair(db, pos_ty, left), - ) - }) - }) - } + nontrivial_check(self, || { + if let Some(alternatives) = left_intersection.finite_alternative_union(db) { + self.check_type_pair(db, alternatives, right) + } else if let Some(alternatives) = + right_intersection.finite_alternative_union(db) + { + self.check_type_pair(db, left, alternatives) + } else { + self.with_recursion_guard(db, left, right, || { + left_intersection + .positive(db) + .iter() + .when_any(db, self.constraints, |&pos_ty| { + self.check_type_pair(db, pos_ty, right) + }) + .or(db, self.constraints, || { + right_intersection.positive(db).iter().when_any( + db, + self.constraints, + |&pos_ty| self.check_type_pair(db, pos_ty, left), + ) + }) + }) + } + }) } - (Type::Intersection(intersection), other) => { + (Type::Intersection(intersection), other) => nontrivial_check(self, || { if let Some(alternatives) = intersection.finite_alternative_union(db) { self.check_type_pair(db, alternatives, other) } else { self.check_intersection_pair_via_elements(db, left, right, intersection, other) } - } + }), - (other, Type::Intersection(intersection)) => { + (other, Type::Intersection(intersection)) => nontrivial_check(self, || { if let Some(alternatives) = intersection.finite_alternative_union(db) { self.check_type_pair(db, other, alternatives) } else { self.check_intersection_pair_via_elements(db, left, right, intersection, other) } - } + }), (Type::LiteralValue(left), Type::LiteralValue(right)) if left.is_literal_string() && right.is_literal_string() @@ -2833,7 +2911,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { } (Type::PropertyInstance(left), Type::PropertyInstance(right)) => { - self.check_property_instance_pair(db, left, right) + nontrivial_check(self, || self.check_property_instance_pair(db, left, right)) } ( @@ -2847,7 +2925,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { | ( Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderDelete(left)), Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderDelete(right)), - ) => self.check_property_instance_pair(db, left, right), + ) => nontrivial_check(self, || self.check_property_instance_pair(db, left, right)), ( Type::KnownInstance(KnownInstanceType::Sentinel(left_sentinel)), @@ -2898,37 +2976,50 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { (Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => { // `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint. // Thus, they are only disjoint if `ty.bool() == AlwaysFalse`. - ConstraintSet::from_bool(self.constraints, ty.bool(db).is_always_false()) + nontrivial_check(self, || { + ConstraintSet::from_bool(self.constraints, ty.bool(db).is_always_false()) + }) } (Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => { // Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`. - ConstraintSet::from_bool(self.constraints, ty.bool(db).is_always_true()) + nontrivial_check(self, || { + ConstraintSet::from_bool(self.constraints, ty.bool(db).is_always_true()) + }) } - (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => self - .with_recursion_guard(db, left, right, || { - self.check_protocol_instance_pair(db, left_proto, right_proto) - }), + (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => { + nontrivial_check(self, || { + self.with_recursion_guard(db, left, right, || { + self.check_protocol_instance_pair(db, left_proto, right_proto) + }) + }) + } (Type::ProtocolInstance(protocol), Type::SpecialForm(special_form)) - | (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => self - .with_recursion_guard(db, left, right, || { - self.any_protocol_members_absent_or_disjoint( - db, - protocol, - special_form.instance_fallback(db), - ) - }), + | (Type::SpecialForm(special_form), Type::ProtocolInstance(protocol)) => { + nontrivial_check(self, || { + self.with_recursion_guard(db, left, right, || { + self.any_protocol_members_absent_or_disjoint( + db, + protocol, + special_form.instance_fallback(db), + ) + }) + }) + } (Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance)) - | (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => self - .with_recursion_guard(db, left, right, || { - self.any_protocol_members_absent_or_disjoint( - db, - protocol, - known_instance.instance_fallback(db), - ) - }), + | (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => { + nontrivial_check(self, || { + self.with_recursion_guard(db, left, right, || { + self.any_protocol_members_absent_or_disjoint( + db, + protocol, + known_instance.instance_fallback(db), + ) + }) + }) + } // The absence of a protocol member on one of these types guarantees // that the type will be disjoint from the protocol, @@ -2972,8 +3063,10 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { | Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::GenericAlias(..)), - ) => self.with_recursion_guard(db, left, right, || { - self.any_protocol_members_absent_or_disjoint(db, protocol, ty) + ) => nontrivial_check(self, || { + self.with_recursion_guard(db, left, right, || { + self.any_protocol_members_absent_or_disjoint(db, protocol, ty) + }) }), // This is the same as the branch above -- @@ -2981,19 +3074,21 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // () (Type::ProtocolInstance(protocol), Type::NominalInstance(nominal)) | (Type::NominalInstance(nominal), Type::ProtocolInstance(protocol)) - if nominal.class(db).is_final(db) => + if self.perform_expensive_checks && nominal.class(db).is_final(db) => { - self.with_recursion_guard(db, left, right, || { - self.any_protocol_members_absent_or_disjoint( - db, - protocol, - Type::NominalInstance(nominal), - ) + nontrivial_check(self, || { + self.with_recursion_guard(db, left, right, || { + self.any_protocol_members_absent_or_disjoint( + db, + protocol, + Type::NominalInstance(nominal), + ) + }) }) } (Type::ProtocolInstance(protocol), other) - | (other, Type::ProtocolInstance(protocol)) => { + | (other, Type::ProtocolInstance(protocol)) => nontrivial_check(self, || { self.with_recursion_guard(db, left, right, || { protocol .interface(db) @@ -3011,7 +3106,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { } }) }) - } + }), (Type::SubclassOf(subclass_of_ty), _) | (_, Type::SubclassOf(subclass_of_ty)) if subclass_of_ty.is_type_var() => @@ -3025,35 +3120,47 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { left_alias.origin(db) != right_alias.origin(db), ) .or(db, self.constraints, || { - self.check_specialization_pair( - db, - left_alias.specialization(db), - right_alias.specialization(db), - ) + nontrivial_check(self, || { + self.check_specialization_pair( + db, + left_alias.specialization(db), + right_alias.specialization(db), + ) + }) }) } (Type::ClassLiteral(class), Type::GenericAlias(alias_b)) - | (Type::GenericAlias(alias_b), Type::ClassLiteral(class)) => class - .default_specialization(db) - .into_generic_alias() - .when_none_or(db, self.constraints, |alias| { - self.check_type_pair(db, Type::GenericAlias(alias_b), Type::GenericAlias(alias)) - }), + | (Type::GenericAlias(alias_b), Type::ClassLiteral(class)) => { + nontrivial_check(self, || { + class + .default_specialization(db) + .into_generic_alias() + .when_none_or(db, self.constraints, |alias| { + self.check_type_pair( + db, + Type::GenericAlias(alias_b), + Type::GenericAlias(alias), + ) + }) + }) + } (Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b)) | (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => { match subclass_of_ty.subclass_of() { SubclassOfInner::Dynamic(_) => self.never(), SubclassOfInner::Protocol(_) => self.never(), - SubclassOfInner::Class(class_a) => ConstraintSet::from_bool( - self.constraints, - !class_a.could_exist_in_mro_of_with_disjointness_checker( - db, - ClassType::NonGeneric(class_b), - self, - ), - ), + SubclassOfInner::Class(class_a) => nontrivial_check(self, || { + ConstraintSet::from_bool( + self.constraints, + !class_a.could_exist_in_mro_of_with_disjointness_checker( + db, + ClassType::NonGeneric(class_b), + self, + ), + ) + }), SubclassOfInner::TypeVar(_) => unreachable!(), } } @@ -3063,87 +3170,109 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { match subclass_of_ty.subclass_of() { SubclassOfInner::Dynamic(_) => self.never(), SubclassOfInner::Protocol(_) => self.never(), - SubclassOfInner::Class(class_a) => ConstraintSet::from_bool( - self.constraints, - !class_a.could_exist_in_mro_of_with_disjointness_checker( - db, - ClassType::Generic(alias_b), - self, - ), - ), + SubclassOfInner::Class(class_a) => nontrivial_check(self, || { + ConstraintSet::from_bool( + self.constraints, + !class_a.could_exist_in_mro_of_with_disjointness_checker( + db, + ClassType::Generic(alias_b), + self, + ), + ) + }), SubclassOfInner::TypeVar(_) => unreachable!(), } } (Type::SubclassOf(left), Type::SubclassOf(right)) => { - self.check_subclassof_pair(db, left, right) + nontrivial_check(self, || self.check_subclassof_pair(db, left, right)) } // 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) - | (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() { - SubclassOfInner::Dynamic(_) => { - self.check_type_pair(db, KnownClass::Type.to_instance(db), other) - } - SubclassOfInner::Class(class) => { - self.check_type_pair(db, class.metaclass_instance_type(db), other) - } - SubclassOfInner::Protocol(_) => { - self.check_type_pair(db, KnownClass::Type.to_instance(db), other) - } - SubclassOfInner::TypeVar(_) => unreachable!(), - }, + | (other, Type::SubclassOf(subclass_of_ty)) => { + nontrivial_check(self, || match subclass_of_ty.subclass_of() { + SubclassOfInner::Dynamic(_) => { + self.check_type_pair(db, KnownClass::Type.to_instance(db), other) + } + SubclassOfInner::Class(class) => { + self.check_type_pair(db, class.metaclass_instance_type(db), other) + } + SubclassOfInner::Protocol(_) => { + self.check_type_pair(db, KnownClass::Type.to_instance(db), other) + } + SubclassOfInner::TypeVar(_) => unreachable!(), + }) + } (Type::SpecialForm(special_form), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::SpecialForm(special_form)) => { - ConstraintSet::from_bool( - self.constraints, - !special_form.is_instance_of(db, instance.class(db)), - ) + nontrivial_check(self, || { + ConstraintSet::from_bool( + self.constraints, + !special_form.is_instance_of(db, instance.class(db)), + ) + }) } (Type::KnownInstance(known_instance), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::KnownInstance(known_instance)) => { - ConstraintSet::from_bool( - self.constraints, - !known_instance.is_instance_of(db, instance.class(db)), - ) + nontrivial_check(self, || { + ConstraintSet::from_bool( + self.constraints, + !known_instance.is_instance_of(db, instance.class(db)), + ) + }) } (Type::LiteralValue(literal), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::LiteralValue(literal)) => { - let positive_relation_holds = match literal.kind() { - LiteralValueTypeKind::Int(_) => { - KnownClass::Int.when_subclass_of(db, instance.class(db), self.constraints) - } - LiteralValueTypeKind::Bool(_) => { - KnownClass::Bool.when_subclass_of(db, instance.class(db), self.constraints) - } - LiteralValueTypeKind::LiteralString | LiteralValueTypeKind::String(_) => { - KnownClass::Str.when_subclass_of(db, instance.class(db), self.constraints) - } - LiteralValueTypeKind::Bytes(_) => { - KnownClass::Bytes.when_subclass_of(db, instance.class(db), self.constraints) - } - LiteralValueTypeKind::Enum(enum_literal) => self - .as_relation_checker(TypeRelation::Subtyping) - .check_type_pair( + nontrivial_check(self, || { + let positive_relation_holds = match literal.kind() { + LiteralValueTypeKind::Int(_) => KnownClass::Int.when_subclass_of( db, - enum_literal.enum_class_instance(db), - Type::NominalInstance(instance), + instance.class(db), + self.constraints, ), - }; - positive_relation_holds.negate(db, self.constraints) + LiteralValueTypeKind::Bool(_) => KnownClass::Bool.when_subclass_of( + db, + instance.class(db), + self.constraints, + ), + LiteralValueTypeKind::LiteralString | LiteralValueTypeKind::String(_) => { + KnownClass::Str.when_subclass_of( + db, + instance.class(db), + self.constraints, + ) + } + LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes.when_subclass_of( + db, + instance.class(db), + self.constraints, + ), + LiteralValueTypeKind::Enum(enum_literal) => self + .as_relation_checker(TypeRelation::Subtyping) + .check_type_pair( + db, + enum_literal.enum_class_instance(db), + Type::NominalInstance(instance), + ), + }; + positive_relation_holds.negate(db, self.constraints) + }) } (Type::TypeIs(_) | Type::TypeGuard(_), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::TypeIs(_) | Type::TypeGuard(_)) => { // A boolean literal must be an instance of exactly `bool` // (it cannot be an instance of a `bool` subclass) - KnownClass::Bool - .when_subclass_of(db, instance.class(db), self.constraints) - .negate(db, self.constraints) + nontrivial_check(self, || { + KnownClass::Bool + .when_subclass_of(db, instance.class(db), self.constraints) + .negate(db, self.constraints) + }) } (Type::TypeIs(_) | Type::TypeGuard(_), _) @@ -3155,33 +3284,42 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // unless the type expressing "all instances of `Z`" is a subtype of of `Y`, // where `Z` is `X`'s metaclass. (Type::ClassLiteral(class), Type::NominalInstance(instance)) - | (Type::NominalInstance(instance), Type::ClassLiteral(class)) => class - .metaclass_instance_type(db) - .when_subtype_of( - db, - Type::NominalInstance(instance), - self.constraints, - self.inferable, - ) - .negate(db, self.constraints), + | (Type::NominalInstance(instance), Type::ClassLiteral(class)) => { + nontrivial_check(self, || { + class + .metaclass_instance_type(db) + .when_subtype_of( + db, + Type::NominalInstance(instance), + self.constraints, + self.inferable, + ) + .negate(db, self.constraints) + }) + } (Type::GenericAlias(alias), Type::NominalInstance(instance)) - | (Type::NominalInstance(instance), Type::GenericAlias(alias)) => self - .as_relation_checker(TypeRelation::Subtyping) - .check_type_pair( - db, - ClassType::Generic(alias).metaclass_instance_type(db), - Type::NominalInstance(instance), - ) - .negate(db, self.constraints), + | (Type::NominalInstance(instance), Type::GenericAlias(alias)) => { + nontrivial_check(self, || { + self.as_relation_checker(TypeRelation::Subtyping) + .check_type_pair( + db, + ClassType::Generic(alias).metaclass_instance_type(db), + Type::NominalInstance(instance), + ) + .negate(db, self.constraints) + }) + } (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 - .when_subclass_of(db, instance.class(db), self.constraints) - .negate(db, self.constraints) + nontrivial_check(self, || { + KnownClass::FunctionType + .when_subclass_of(db, instance.class(db), self.constraints) + .negate(db, self.constraints) + }) } // A `BoundMethod` type includes instances of the same method bound to a @@ -3195,57 +3333,71 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // method name would show up on both sides of this check. However for // completeness, if we're ever comparing `BoundMethod` types with different // method names, then they're clearly disjoint. - self.always() - } else if a_function != b_function - && a_function.has_known_decorator(db, FunctionDecorators::FINAL) - && b_function.has_known_decorator(db, FunctionDecorators::FINAL) - { - // If *both* methods are `@final` (and they're not literally the same - // definition), they must be disjoint. - // - // Note that we can't establish disjointness when only one side is `@final`, - // because we have to worry about cases like this: - // - // ``` - // class A: - // def f(self): ... - // class B: - // @final - // def f(self): ... - // # Valid in this order, though `C(A, B)` would be invalid. - // class C(B, A): ... - // ``` - self.always() - } else { - // The names match, so `BoundMethod` disjointness depends on whether the bound - // self types are disjoint. Note that this can produce confusing results in the - // face of Liskov violations. For example: - // ``` - // class A: - // def f(self) -> int: ... - // class B: - // def f(self) -> str: ... - // def _(x: Intersection[A, B]): - // x.f() - // ``` - // `class C(A, B)` could inhabit that intersection, but `int` and `str` are - // disjoint, so the type of `x.f()` there is going to be inferred as `Never`. - // That's probably not correct in practice, but the right way to address it is - // to emit a diagnostic on the definition of `C.f`. - self.check_type_pair(db, a.self_instance(db), b.self_instance(db)) + return self.always(); } + + nontrivial_check(self, || { + if a_function != b_function + && a_function.has_known_decorator(db, FunctionDecorators::FINAL) + && b_function.has_known_decorator(db, FunctionDecorators::FINAL) + { + // If *both* methods are `@final` (and they're not literally the same + // definition), they must be disjoint. + // + // Note that we can't establish disjointness when only one side is `@final`, + // because we have to worry about cases like this: + // + // ``` + // class A: + // def f(self): ... + // class B: + // @final + // def f(self): ... + // # Valid in this order, though `C(A, B)` would be invalid. + // class C(B, A): ... + // ``` + self.always() + } else { + // The names match, so `BoundMethod` disjointness depends on whether the bound + // self types are disjoint. Note that this can produce confusing results in the + // face of Liskov violations. For example: + // ``` + // class A: + // def f(self) -> int: ... + // class B: + // def f(self) -> str: ... + // def _(x: Intersection[A, B]): + // x.f() + // ``` + // `class C(A, B)` could inhabit that intersection, but `int` and `str` are + // disjoint, so the type of `x.f()` there is going to be inferred as `Never`. + // That's probably not correct in practice, but the right way to address it is + // to emit a diagnostic on the definition of `C.f`. + self.check_type_pair(db, a.self_instance(db), b.self_instance(db)) + } + }) } (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => { - self.check_type_pair(db, KnownClass::MethodType.to_instance(db), other) + nontrivial_check(self, || { + self.check_type_pair(db, KnownClass::MethodType.to_instance(db), other) + }) } (Type::KnownBoundMethod(method), other) | (other, Type::KnownBoundMethod(method)) => { - self.check_type_pair(db, method.class().to_instance(db), other) + nontrivial_check(self, || { + self.check_type_pair(db, method.class().to_instance(db), other) + }) } (Type::WrapperDescriptor(_), other) | (other, Type::WrapperDescriptor(_)) => { - self.check_type_pair(db, KnownClass::WrapperDescriptorType.to_instance(db), other) + nontrivial_check(self, || { + self.check_type_pair( + db, + KnownClass::WrapperDescriptorType.to_instance(db), + other, + ) + }) } (Type::Callable(_) | Type::FunctionLiteral(_), Type::Callable(_)) @@ -3272,15 +3424,27 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { | ( Type::NominalInstance(nominal), Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), - ) if nominal.class(db).is_final(db) => Type::NominalInstance(nominal) - .member_lookup_with_policy(db, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK) - .place - .ignore_possibly_undefined() - .when_none_or(db, self.constraints, |dunder_call| { - self.as_relation_checker(TypeRelation::Assignability) - .check_type_pair(db, dunder_call, Type::Callable(CallableType::unknown(db))) - .negate(db, self.constraints) - }), + ) if self.perform_expensive_checks && nominal.class(db).is_final(db) => { + nontrivial_check(self, || { + Type::NominalInstance(nominal) + .member_lookup_with_policy( + db, + "__call__", + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place + .ignore_possibly_undefined() + .when_none_or(db, self.constraints, |dunder_call| { + self.as_relation_checker(TypeRelation::Assignability) + .check_type_pair( + db, + dunder_call, + Type::Callable(CallableType::unknown(db)), + ) + .negate(db, self.constraints) + }) + }) + } ( Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), @@ -3297,60 +3461,74 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { (Type::ModuleLiteral(..), Type::NominalInstance(instance)) | (Type::NominalInstance(instance), Type::ModuleLiteral(..)) => { // Modules *can* actually be instances of `ModuleType` subclasses - self.check_type_pair( - db, - Type::NominalInstance(instance), - KnownClass::ModuleType.to_instance(db), - ) + nontrivial_check(self, || { + self.check_type_pair( + db, + Type::NominalInstance(instance), + KnownClass::ModuleType.to_instance(db), + ) + }) } - (Type::NominalInstance(left_i), Type::NominalInstance(right_i)) => self - .with_recursion_guard(db, left, right, || { - self.check_nominal_instance_pair(db, left_i, right_i) - }), + (Type::NominalInstance(left_i), Type::NominalInstance(right_i)) => { + nontrivial_check(self, || { + self.with_recursion_guard(db, left, right, || { + self.check_nominal_instance_pair(db, left_i, right_i) + }) + }) + } (Type::NewTypeInstance(left), Type::NewTypeInstance(right)) => { - self.check_newtype_pair(db, left, right) + nontrivial_check(self, || self.check_newtype_pair(db, left, right)) } (Type::NewTypeInstance(newtype), other) | (other, Type::NewTypeInstance(newtype)) => { - self.check_type_pair(db, newtype.concrete_base_type(db), other) + nontrivial_check(self, || { + self.check_type_pair(db, newtype.concrete_base_type(db), other) + }) } (Type::PropertyInstance(property), other) - | (other, Type::PropertyInstance(property)) => { + | (other, Type::PropertyInstance(property)) => nontrivial_check(self, || { self.check_type_pair(db, property.instance_fallback(db), other) - } + }), - (Type::BoundSuper(left), Type::BoundSuper(right)) => self - .as_equivalence_checker() - .check_bound_super_pair(db, left, right) - .negate(db, self.constraints), + (Type::BoundSuper(left), Type::BoundSuper(right)) => nontrivial_check(self, || { + self.as_equivalence_checker() + .check_bound_super_pair(db, left, right) + .negate(db, self.constraints) + }), (Type::BoundSuper(_), other) | (other, Type::BoundSuper(_)) => { - self.check_type_pair(db, KnownClass::Super.to_instance(db), other) + nontrivial_check(self, || { + self.check_type_pair(db, KnownClass::Super.to_instance(db), other) + }) } (Type::TypeForm(_), _) | (_, Type::TypeForm(_)) => self.never(), (Type::GenericAlias(_), _) | (_, Type::GenericAlias(_)) => self.always(), - (Type::TypedDict(left_td), Type::TypedDict(right_td)) => { + (Type::TypedDict(left_td), Type::TypedDict(right_td)) => nontrivial_check(self, || { self.with_recursion_guard(db, left, right, || { self.check_typeddict_pair(db, left_td, right_td) }) - } + }), // For any type `T`, if `dict[str, Any]` is not assignable to `T`, then all `TypedDict` // types will always be disjoint from `T`. This doesn't cover all cases -- in fact // `dict` *itself* is almost always disjoint from `TypedDict` -- but it's a good // approximation, and some false negatives are acceptable. (Type::TypedDict(_), other) | (other, Type::TypedDict(_)) => { - let dict_str_any = KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]); + nontrivial_check(self, || { + let dict_str_any = KnownClass::Dict.to_specialized_instance( + db, + &[KnownClass::Str.to_instance(db), Type::any()], + ); - self.as_relation_checker(TypeRelation::Assignability) - .check_type_pair(db, dict_str_any, other) - .negate(db, self.constraints) + self.as_relation_checker(TypeRelation::Assignability) + .check_type_pair(db, dict_str_any, other) + .negate(db, self.constraints) + }) } } } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index e9c110dddd..fcd6f8ea4e 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1199,7 +1199,7 @@ impl<'db> Signature<'db> { let specialization = builder.build_with(generic_context, |typevar, bounds| { if let Some(bounds) = bounds && let Some(lower) = bounds.lower - && let Some(upper) = bounds.upper.as_single_bound() + && let Some(upper) = bounds.upper.as_single_bound(db) && lower.is_equivalent_to(db, upper) && let Ok(Some(solution)) = PathBounds::default_solve(db, &constraints, bounds) { From 7de420ecd242fdecf859c65b8ffdaaae339c9a54 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 29 Jul 2026 19:37:24 +0200 Subject: [PATCH 144/390] Fix indexing of excluded nested Ruff workspaces (#27303) --- .../src/session/index/ruff_settings.rs | 8 +- crates/ruff_server/tests/e2e/workspace.rs | 183 +++++++++++++++++- 2 files changed, 186 insertions(+), 5 deletions(-) diff --git a/crates/ruff_server/src/session/index/ruff_settings.rs b/crates/ruff_server/src/session/index/ruff_settings.rs index adec385343..d1ab2c317c 100644 --- a/crates/ruff_server/src/session/index/ruff_settings.rs +++ b/crates/ruff_server/src/session/index/ruff_settings.rs @@ -295,10 +295,14 @@ impl RuffSettingsIndex { return WalkState::Continue; } + let depth = entry.depth(); let directory = entry.into_path(); - // If the directory is excluded from the workspace, skip it. - if let Some(file_name) = directory.file_name() { + // An explicitly opened workspace root must be indexed even if an ancestor + // configuration excludes it. Excluded descendants can still be skipped. + if depth > 0 + && let Some(file_name) = directory.file_name() + { let settings = index .read() .unwrap() diff --git a/crates/ruff_server/tests/e2e/workspace.rs b/crates/ruff_server/tests/e2e/workspace.rs index 4e862e97c7..242901fd6b 100644 --- a/crates/ruff_server/tests/e2e/workspace.rs +++ b/crates/ruff_server/tests/e2e/workspace.rs @@ -1,7 +1,9 @@ -use anyhow::Result; -use insta::assert_json_snapshot; +use anyhow::{Context, Result}; +use insta::{assert_json_snapshot, assert_snapshot}; -use crate::TestServerBuilder; +use crate::{TestServer, TestServerBuilder}; + +const SOURCE: &str = "value= \"hello\"\n"; #[test] fn selects_the_correct_workspace_settings_for_multi_root_workspaces() -> Result<()> { @@ -104,6 +106,181 @@ ignore = ["F401"] Ok(()) } +#[test] +fn nested_workspace_root_is_not_excluded_by_an_ancestor() -> Result<()> { + let mut server = nested_workspace_server(&["sub"], WorkspaceExclusion::Exclude)?; + + assert_snapshot!( + open_and_format(&mut server, "sub/test.py", SOURCE) + .context("nested workspace should be formatted")?, + @"value = 'hello'" + ); + // Explicitly opening `sub` does not override its own exclusion of `foo`. + assert!(open_and_format(&mut server, "sub/foo/test.py", SOURCE).is_none()); + + Ok(()) +} + +#[test] +fn nested_workspace_root_is_not_excluded_by_an_ancestor_in_a_multi_root_workspace() -> Result<()> { + const ISSUE_SOURCE: &str = r#"print("This line is long enough to wrap.") +"#; + + let mut server = TestServerBuilder::new()? + .with_workspace(".")? + .with_workspace("sub")? + .with_file( + ".ruff.toml", + r#"target-version = "py312" +line-length = 40 + +extend-exclude = [ + "sub", +] +"#, + )? + .with_file( + "sub/.ruff.toml", + r#"target-version = "py312" +line-length = 40 + +extend-exclude = [ + "foo", +] +"#, + )? + .with_file("test.py", ISSUE_SOURCE)? + .with_file("sub/test.py", ISSUE_SOURCE)? + .with_file("sub/foo/test.py", ISSUE_SOURCE)? + .build(); + + assert_snapshot!( + open_and_format(&mut server, "test.py", ISSUE_SOURCE) + .context("parent workspace should be formatted")?, + @r#" + print( + "This line is long enough to wrap." + ) + "# + ); + assert_snapshot!( + open_and_format(&mut server, "sub/test.py", ISSUE_SOURCE) + .context("nested workspace should be formatted")?, + @r#" + print( + "This line is long enough to wrap." + ) + "# + ); + assert!(open_and_format(&mut server, "sub/foo/test.py", ISSUE_SOURCE).is_none()); + + Ok(()) +} + +#[test] +fn nested_workspace_remains_excluded_without_explicit_registration() -> Result<()> { + let mut server = nested_workspace_server(&["."], WorkspaceExclusion::ExtendExclude)?; + + assert!(open_and_format(&mut server, "sub/test.py", SOURCE).is_none()); + assert!(open_and_format(&mut server, "sub/foo/test.py", SOURCE).is_none()); + + Ok(()) +} + +#[test] +fn unrelated_file_outside_workspace_uses_fallback_configuration() -> Result<()> { + let mut server = nested_workspace_server(&["sub"], WorkspaceExclusion::ExtendExclude)?; + + assert_snapshot!( + open_and_format(&mut server, "unrelated/test.py", SOURCE) + .context("unrelated file should use fallback formatting")?, + @r#"value = "hello""# + ); + + Ok(()) +} + +#[test] +fn single_file_mode_does_not_index_nested_configuration() -> Result<()> { + let mut server = TestServerBuilder::new()? + .with_file("nested/.ruff.toml", "[format]\nquote-style = \"single\"\n")? + .with_file("nested/test.py", SOURCE)? + .with_file("unrelated/test.py", SOURCE)? + .build(); + + assert_snapshot!( + open_and_format(&mut server, "nested/test.py", SOURCE) + .context("nested file should use fallback formatting")?, + @r#"value = "hello""# + ); + assert_snapshot!( + open_and_format(&mut server, "unrelated/test.py", SOURCE) + .context("unrelated file should use fallback formatting")?, + @r#"value = "hello""# + ); + + Ok(()) +} + +#[derive(Clone, Copy)] +enum WorkspaceExclusion { + Exclude, + ExtendExclude, +} + +/// Creates a test server for the following temporary workspace: +/// +/// ```text +/// / +/// ├── .ruff.toml # exclude or extend-exclude = ["sub"] +/// ├── test.py +/// ├── sub/ +/// │ ├── .ruff.toml # extend-exclude = ["foo"] +/// │ │ # format.quote-style = "single" +/// │ ├── test.py +/// │ └── foo/ +/// │ └── test.py +/// └── unrelated/ +/// └── test.py +/// ``` +fn nested_workspace_server( + workspaces: &[&str], + exclusion: WorkspaceExclusion, +) -> Result { + let mut builder = TestServerBuilder::new()?; + for workspace in workspaces { + builder = builder.with_workspace(workspace)?; + } + + let server = builder + .with_file( + ".ruff.toml", + match exclusion { + WorkspaceExclusion::Exclude => "exclude = [\"sub\"]\n", + WorkspaceExclusion::ExtendExclude => "extend-exclude = [\"sub\"]\n", + }, + )? + .with_file( + "sub/.ruff.toml", + "extend-exclude = [\"foo\"]\n[format]\nquote-style = \"single\"\n", + )? + .with_file("test.py", SOURCE)? + .with_file("sub/test.py", SOURCE)? + .with_file("sub/foo/test.py", SOURCE)? + .with_file("unrelated/test.py", SOURCE)? + .build(); + + Ok(server) +} + +fn open_and_format(server: &mut TestServer, path: &str, source: &str) -> Option { + server.open_text_document(path, source, 1); + server + .format_request(path) + .and_then(|edits| edits.into_iter().next()) + .map(|edit| edit.new_text) +} + #[test] fn unavailable_document_diagnostic_returns_empty_response() -> Result<()> { let mut server = TestServerBuilder::new()?.with_workspace(".")?.build(); From f3c869da1b4962ff17d3f1674fb887e0d065a7df Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 30 Jul 2026 00:42:43 +0500 Subject: [PATCH 145/390] [`ruff`] reintroduce `demisto/content` to `ruff-ecosystem` (#27305) ## Summary Reintroducing `demisto/content` back to `ruff-ecosystem`, it's a huge repo with 4.5K py file and 2.5M loc, so may have catch some cases for ecosystem report. It was previously commented out in https://github.com/astral-sh/ruff/pull/12129 due to the use of removed `E999` Issue with the use of removed rules upstream is resolved, though apparently `E999` was dropped from selection a while ago and new issue with the use of removed `UP038` occurred, but it's resolved now too - https://github.com/demisto/content/pull/45227 Syntax error in still present in https://github.com/demisto/content/blob/master/Packs/ThreatQ/Integrations/ThreatQ/ThreatQ.py, so keeping `exclude`. Though apparently it doesn't break `ruff-ecosystem` anymore, but keeping it safe for now. I've submitted a fix upstream https://github.com/demisto/content/pull/45275, so `exclude` can be removed later too. ## Test Plan Tested running `ruff-ecosystem` locally, no issues found. --- python/ruff-ecosystem/ruff_ecosystem/defaults.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/python/ruff-ecosystem/ruff_ecosystem/defaults.py b/python/ruff-ecosystem/ruff_ecosystem/defaults.py index 14b7787090..683c6864ea 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/defaults.py +++ b/python/ruff-ecosystem/ruff_ecosystem/defaults.py @@ -35,15 +35,7 @@ repo=Repository(owner="bokeh", name="bokeh", ref="branch-3.10"), check_options=CheckOptions(select="ALL"), ), - # Disabled due to use of explicit `select` with `E999`, which has been removed. - # See: https://github.com/astral-sh/ruff/pull/12129 - # Project( - # repo=Repository(owner="demisto", name="content", ref="master"), - # format_options=FormatOptions( - # # Syntax errors in this file - # exclude="Packs/ThreatQ/Integrations/ThreatQ/ThreatQ.py" - # ), - # ), + Project(repo=Repository(owner="demisto", name="content", ref="master")), Project(repo=Repository(owner="docker", name="docker-py", ref="main")), Project(repo=Repository(owner="facebookresearch", name="chameleon", ref="main")), Project(repo=Repository(owner="freedomofpress", name="securedrop", ref="develop")), From a4f873dfc599124dc97cf6999da4a0e4822d2ef7 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 29 Jul 2026 12:55:05 -0700 Subject: [PATCH 146/390] [ty] Preserve exact numeric types in covariant collections (#27311) ## Summary Fixes astral-sh/ty#4111. Collection inference promotes exact runtime `float` and `complex` elements to their numeric-tower unions, which can introduce types that violate a covariant `Sequence` or `Iterable` context. Preserve the original inferred element when it satisfies that context but the promoted type does not. This fixes the original `Sequence[str | Just[float]]` false positive without changing `Just` protocol matching, invariant collection inference, or ordinary mutable-list numeric widening. ## Test plan Added focused bidirectional mdtests covering exact-float `Sequence` and `Iterable` contexts, exact-complex sequence contexts, the original `Just[float]` union, tuples, invariant and explicitly annotated lists, rejection of actual integer and float mismatches, and preserved widening for mutable float lists. The full `ty_python_semantic` test suite and all applicable file-scoped repository hooks pass. --- .../resources/mdtest/bidirectional.md | 89 +++++++++++++++++++ .../src/types/infer/builder.rs | 17 +++- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index 082d908c33..a8d86627b4 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -137,6 +137,95 @@ reveal_type(s) # revealed: dict[int | str, int | str] reveal_type(s) # revealed: dict[int | str, int | str] ``` +### Exact float types in covariant contexts + +A covariant collection context must preserve an exact float when numeric promotion would introduce +an `int` that the expected element type rejects. + +```py +from collections.abc import Iterable, Sequence +from ty_extensions import JustFloat + +def takes_exact_sequence(values: Sequence[JustFloat]) -> None: ... +def takes_exact_iterable(values: Iterable[JustFloat]) -> None: ... +def takes_exact_list(values: list[JustFloat]) -> None: ... + +takes_exact_sequence([1.0]) +takes_exact_sequence((1.0,)) +takes_exact_sequence([1]) # error: [invalid-argument-type] + +takes_exact_iterable([1.0]) +takes_exact_iterable((1.0,)) +takes_exact_iterable([1]) # error: [invalid-argument-type] + +takes_exact_list([1.0]) + +annotated: list[JustFloat] = [1.0] +takes_exact_sequence(annotated) +``` + +Ordinary `float` contexts and unannotated mutable lists must retain numeric promotion. + +```py +def takes_float_sequence(values: Sequence[float]) -> None: ... + +takes_float_sequence([1.0]) +takes_float_sequence([1]) + +mutable_floats = [1.0] +mutable_floats.append(1) +reveal_type(mutable_floats) # revealed: list[int | float] +``` + +### Exact complex types in covariant contexts + +The same contextual restriction applies when promoting an exact complex number would introduce `int` +and `float`. + +```py +from collections.abc import Sequence +from ty_extensions import JustComplex + +def takes_exact_complexes(values: Sequence[JustComplex]) -> None: ... + +takes_exact_complexes([1j]) +takes_exact_complexes((1j,)) +takes_exact_complexes([1]) # error: [invalid-argument-type] +takes_exact_complexes([1.0]) # error: [invalid-argument-type] +``` + +### Exact-type protocols in covariant contexts + +A writable `__class__` property allows an invariant protocol to distinguish a runtime float from an +integer. A covariant sequence of a union containing this protocol must preserve that distinction. + +```py +from collections.abc import Sequence +from typing import Generic, Protocol, TypeVar + +T = TypeVar("T") + +class Just(Protocol, Generic[T]): + @property + def __class__(self, /) -> type[T]: ... + @__class__.setter + def __class__(self, value: type[T], /) -> None: ... + +def takes_exact_float(value: Just[float]) -> None: ... +def takes_exact_values(values: Sequence[str | Just[float]]) -> None: ... + +takes_exact_float(1.0) +takes_exact_float(1) # error: [invalid-argument-type] + +takes_exact_values(["1", 1.0]) +takes_exact_values(["1", float("nan")]) +takes_exact_values(("1", 1.0)) +takes_exact_values(["1", 1]) # error: [invalid-argument-type] + +annotated: list[str | Just[float]] = ["1", 1.0] +takes_exact_values(annotated) +``` + ### Optional unions ```py diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 1b13ea1260..04a4d4c82e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -7380,9 +7380,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } - // We promote element literal types in invariant position by default, unless they were - // inferred with an explicit literal annotation. - let inferred_elt_ty = inferred_elt_ty.promote(self.db()); + // A covariant context is an upper bound, so promotion must not widen an otherwise + // compatible element beyond that bound. In particular, promoting an exact float + // introduces `int`, which is not assignable to an exact-float context. + let promoted_elt_ty = inferred_elt_ty.promote(self.db()); + let inferred_elt_ty = if let Some(elt_tcx) = elt_tcx + && elt_tcx_variance[&elt_ty_identity].is_covariant() + && promoted_elt_ty != inferred_elt_ty + && !promoted_elt_ty.is_assignable_to(self.db(), elt_tcx) + && inferred_elt_ty.is_assignable_to(self.db(), elt_tcx) + { + inferred_elt_ty + } else { + promoted_elt_ty + }; let inferred_type_for_typevar = if elt.is_starred_expr() { inferred_elt_ty From 64bc5a97fd1aae1b86aa8afebf381178a02e3dcb Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 29 Jul 2026 15:09:15 -0500 Subject: [PATCH 147/390] [`flake8-pytest-style`] Mark `PT022` fixes as unsafe (#26440) ## Summary Changes the diagnostic fix applicability for rule `PT022` (missing yield types / old-style yield fixtures) from a safe edit to an `unsafe_edit`. Converting a `yield` to a `return` fundamentally alters execution flow, scope lifetimes, and teardown behavior. In certain contexts (e.g., when interacting with bindings like GDAL), this transformation can cause critical runtime failures such as segmentation faults. Forcing this to be an unsafe fix ensures that it will not be executed blindly during a standard `--fix` pass without explicit opt-in via `--allow-unsafe-fixes`. Fixes #26332 ## Test Plan Updated the companion snapshot test (`PT022.snap`) using `insta` to verify that the generated diagnostic now correctly appends the trailing note: `note: This is an unsafe fix and may change runtime behavior`. All relevant linter tests now pass cleanly. --------- Co-authored-by: Brent Westbrook --- .../src/rules/flake8_pytest_style/rules/fixture.rs | 9 +++++++-- ...linter__rules__flake8_pytest_style__tests__PT022.snap | 3 +++ 2 files changed, 10 insertions(+), 2 deletions(-) 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 0b2be586d7..285ea7d35a 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 @@ -518,6 +518,11 @@ impl Violation for PytestFixtureFinalizerCallback { /// return resource /// ``` /// +/// ## Fix safety +/// +/// This rule's fix is always marked unsafe because removing the `yield` can change the behavior of +/// code that relies on implicit cleanup, such as when a value is garbage-collected. +/// /// ## References /// - [`pytest` documentation: Teardown/Cleanup](https://docs.pytest.org/en/latest/how-to/fixtures.html#teardown-cleanup-aka-fixture-finalization) #[derive(ViolationMetadata)] @@ -840,9 +845,9 @@ fn check_fixture_returns(checker: &Checker, name: &str, body: &[Stmt], returns: )) }); if let Some(return_type_edit) = return_type_edit { - diagnostic.set_fix(Fix::safe_edits(yield_edit, [return_type_edit])); + diagnostic.set_fix(Fix::unsafe_edits(yield_edit, [return_type_edit])); } else { - diagnostic.set_fix(Fix::safe_edit(yield_edit)); + diagnostic.set_fix(Fix::unsafe_edit(yield_edit)); } } } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT022.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT022.snap index fcd8705d8b..b460eb6e9e 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT022.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT022.snap @@ -15,6 +15,7 @@ help: Replace `yield` with `return` 17 + return resource 18 | | +note: This is an unsafe fix and may change runtime behavior PT022 [*] No teardown in fixture `error`, use `return` instead of `yield` --> PT022.py:37:5 @@ -33,6 +34,7 @@ help: Replace `yield` with `return` 37 + return resource 38 | | +note: This is an unsafe fix and may change runtime behavior PT022 [*] No teardown in fixture `error`, use `return` instead of `yield` --> PT022.py:43:5 @@ -50,3 +52,4 @@ help: Replace `yield` with `return` - yield resource 43 + return resource | +note: This is an unsafe fix and may change runtime behavior From d5ef97fcd03e108f7510f84b8ed85bb4051311fe Mon Sep 17 00:00:00 2001 From: Anish Giri <161533316+anishgirianish@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:38:33 -0500 Subject: [PATCH 148/390] [`flake8-return`] Fix false positive when variable is read in `finally` clause (`RET504`) (#25441) ## Summary Fixes #17292 RET504 flagged assignments as unnecessary even when the variable was read in `finally`/`except`, breaking runtime behavior on fix. Checks that the binding has only one reference (the return itself) before flagging ## Test Plan - Fixture cases for finally, except, nested try, and the still-fires case. - ran ecosystem checks locally and verified expected results --------- Co-authored-by: Brent Westbrook --- .../flake8-return/unnecessary-assign.md | 282 ++++++++++++++++++ .../src/rules/flake8_return/rules/function.rs | 18 +- .../src/rules/flake8_return/visitor.rs | 38 ++- 3 files changed, 331 insertions(+), 7 deletions(-) create mode 100644 crates/ruff_linter/resources/mdtest/flake8-return/unnecessary-assign.md diff --git a/crates/ruff_linter/resources/mdtest/flake8-return/unnecessary-assign.md b/crates/ruff_linter/resources/mdtest/flake8-return/unnecessary-assign.md new file mode 100644 index 0000000000..27b9d05511 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/flake8-return/unnecessary-assign.md @@ -0,0 +1,282 @@ +# `unnecessary-assign` (`RET504`) + +```toml +lint.select = ["RET504"] +``` + +RET504 is suppressed only when the assigned name is read in an enclosing `finally` suite, which +runs after the `return`. Reads elsewhere (sibling branches, `except` handlers) don't run after the +`return`, so they don't keep the assignment alive. + +## Variable read in the enclosing `finally` + +```py +def f(): + out = "" + try: + out = foo() + return out + except Exception as e: + out = str(e) + finally: + log(out) +``` + +A closure captured in `finally` reads the name from another scope: + +```py +def f(): + try: + x = foo() + return x + finally: + def _cleanup(): + log(x) + _cleanup() +``` + +Outer `finally` reads the name across a nested `try`: + +```py +def f(): + x = "" + try: + try: + x = foo() + return x + except: + pass + finally: + log(x) +``` + +The outer `finally` runs after a `return` in an inner `finally`: + +```py +def f(): + x = "" + try: + try: + pass + finally: + x = foo() + return x + finally: + log(x) +``` + +The `finally` also runs after a `return` in the `else` clause: + +```py +def f(): + try: + pass + except Exception: + pass + else: + x = compute() + return x + finally: + log(x) +``` + +And after a `return` in an `except` handler: + +```py +def f(): + try: + pass + except Exception: + x = recover() + return x + finally: + log(x) +``` + +The assignment may also come from a `with` body inside the `try`: + +```py +def f(): + try: + with open("f") as fh: + x = fh.read() + return x + finally: + log(x) +``` + +## Augmented assignment in `finally` reads the name + +```py +def f(): + try: + x = foo() + return x + finally: + x += 1 +``` + +```py +def f(): + try: + x = foo() + return x + finally: + if cond(): + x += 1 +``` + +## `del` of the name in `finally` + +Removing the assignment would leave the name unbound, so `del x` would raise `UnboundLocalError`: + +```py +def f(): + try: + x = foo() + return x + finally: + del x +``` + +```py +def f(): + try: + x = foo() + return x + finally: + if cond(): + del x +``` + +## A read after a `finally` rebind still suppresses + +A rebind in `finally` makes the assignment redundant, but distinguishing a rebind that kills the +value from a plain read needs control-flow analysis we don't do here. We conservatively treat the +later read as observing the assignment. + +```py +def f(): + try: + x = foo() + return x + finally: + x = "done" + log(x) +``` + +```py +def f(): + try: + x = foo() + return x + finally: + x: str = "done" + log(x) +``` + +```py +def f(): + try: + x = foo() + return x + finally: + x, _ = ("done", 0) + log(x) +``` + +```py +def f(): + try: + x = foo() + return x + finally: + if cond(): + x = "done" + log(x) +``` + +## A read in an `except` handler fires + +An `except` handler is an alternative path: if it runs, the `try` assignment never completed, so +removing the assignment doesn't change what the handler reads. + +```py +def f(): + result = None + try: + result = compute() + return result # error: [unnecessary-assign] + except Exception as e: + log(result) +``` + +## `finally` doesn't read the name + +```py +def f(): + try: + x = foo() + return x # error: [unnecessary-assign] + finally: + log("done") +``` + +```py +def f(): + try: + x = foo() + return x # error: [unnecessary-assign] + finally: + x = "done" +``` + +## Assignment and return both inside `finally` + +```py +def f(): + try: + pass + finally: + x = foo() + return x # error: [unnecessary-assign] +``` + +## `return` in an `except` handler with no later read + +```py +def f(): + try: + entry = fetch() + except AlreadyExists: + entry = lookup() + result = to_dict(entry) + return result # error: [unnecessary-assign] +``` + +## Same name assigned and returned in sibling branches + +Each branch's assignment is independently redundant. A later branch reusing the name doesn't +observe an earlier branch's value, so both fire. + +```py +def f(cond): + if cond: + x = compute() + return x # error: [unnecessary-assign] + else: + x = other() + return x # error: [unnecessary-assign] +``` + +The same holds when the branches are `try` arms without a `finally`: + +```py +def f(): + try: + x = compute() + return x # error: [unnecessary-assign] + except Exception: + x = fallback() + return x # error: [unnecessary-assign] +``` 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 9db7dc3a4e..af6b9259a6 100644 --- a/crates/ruff_linter/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff_linter/src/rules/flake8_return/rules/function.rs @@ -568,7 +568,7 @@ pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) { let Some(function_scope) = checker.semantic().function_scope(function_def) else { return; }; - for (assign, return_, stmt) in &stack.assignment_return { + for (assign, return_, stmt, enclosing_finally) in &stack.assignment_return { // Identify, e.g., `return x`. let Some(value) = return_.value.as_ref() else { continue; @@ -617,6 +617,22 @@ pub(crate) fn unnecessary_assign(checker: &Checker, function_stmt: &Stmt) { else { continue; }; + // Ignore assignments whose name is read or deleted in an enclosing `finally`, which runs + // after the `return`. A reference resolving to a later rebinding in the `finally` counts + // too, so check every binding of the name. + if !enclosing_finally.is_empty() + && function_scope + .get_all(assigned_id) + .flat_map(|binding_id| checker.semantic().binding(binding_id).references()) + .map(|reference_id| checker.semantic().reference(reference_id)) + .any(|reference| { + enclosing_finally + .iter() + .any(|finally_range| finally_range.contains_range(reference.range())) + }) + { + continue; + } // Check if there's any reference made to `assigned_binding` in another scope, e.g, nested // functions. If there is, ignore them. if assigned_binding diff --git a/crates/ruff_linter/src/rules/flake8_return/visitor.rs b/crates/ruff_linter/src/rules/flake8_return/visitor.rs index f86bd0e57e..2a7dc234b0 100644 --- a/crates/ruff_linter/src/rules/flake8_return/visitor.rs +++ b/crates/ruff_linter/src/rules/flake8_return/visitor.rs @@ -4,6 +4,7 @@ use rustc_hash::FxHashSet; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_semantic::SemanticModel; +use ruff_text_size::{Ranged, TextRange}; #[derive(Default)] pub(super) struct Stack<'data> { @@ -30,11 +31,16 @@ pub(super) struct Stack<'data> { pub(super) annotations: FxHashSet<&'data str>, /// Whether the current function is a generator. pub(super) is_generator: bool, - /// The `assignment`-to-`return` statement pairs in the current function. + /// The `assignment`-to-`return` statement pairs in the current function, each paired with the + /// ranges of any enclosing `finally` suites that run after the `return`. /// TODO(charlie): Remove the extra [`Stmt`] here, which is necessary to support statement /// removal for the `return` statement. - pub(super) assignment_return: - Vec<(&'data ast::StmtAssign, &'data ast::StmtReturn, &'data Stmt)>, + pub(super) assignment_return: Vec<( + &'data ast::StmtAssign, + &'data ast::StmtReturn, + &'data Stmt, + Vec, + )>, } pub(super) struct ReturnVisitor<'semantic, 'data> { @@ -57,6 +63,20 @@ impl<'semantic, 'data> ReturnVisitor<'semantic, 'data> { parents: Vec::new(), } } + + /// Return the enclosing `finally` suites that run after this `return`. + fn enclosing_finally(&self, stmt_return: &ast::StmtReturn) -> Vec { + self.parents + .iter() + .filter_map(|parent| parent.as_try_stmt()) + .filter_map(|stmt_try| { + let first = stmt_try.finalbody.first()?; + let last = stmt_try.finalbody.last()?; + Some(TextRange::new(first.start(), last.end())) + }) + .filter(|finally_range| !finally_range.contains_range(stmt_return.range())) + .collect() + } } impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> { @@ -128,9 +148,13 @@ impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> { // return x // ``` Stmt::Assign(stmt_assign) => { - self.stack - .assignment_return - .push((stmt_assign, stmt_return, stmt)); + let enclosing_finally = self.enclosing_finally(stmt_return); + self.stack.assignment_return.push(( + stmt_assign, + stmt_return, + stmt, + enclosing_finally, + )); } // Example: // ```python @@ -144,10 +168,12 @@ impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> { with.body.last().and_then(Stmt::as_assign_stmt) { if !has_conditional_body(with, self.semantic) { + let enclosing_finally = self.enclosing_finally(stmt_return); self.stack.assignment_return.push(( stmt_assign, stmt_return, stmt, + enclosing_finally, )); } } From 7c3e2db97deb5c5edd6e4303403ef6caf2bde8be Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 29 Jul 2026 15:17:04 -0700 Subject: [PATCH 149/390] [ty] Fix enum class container assignability (#27318) ## Summary Fixes https://github.com/astral-sh/ty/issues/4114. Enum classes were incorrectly rejected as `Container[T]` even though `EnumMeta.__contains__` accepts `object`. The underlying issue was that concrete class objects and generic aliases were considered assignable to `type[Any]` during eager relation checks, but not while lazily solving explicitly annotated metaclass-receiver constraints. This produced an unsatisfiable hidden constraint on an otherwise correctly bound `__contains__` method. Make class literals and generic aliases consistently assignable to gradual `type[...]` targets in both eager and lazy assignability checks, while preserving stricter subtyping and incompatible protocol signatures. ## Test plan - Added enum mdtests covering `Enum`, `IntEnum`, and `StrEnum`, including unparameterized containers, `Container[Any]`, `Container[object]`, enum-member and unrelated-element container types, and iterable, reversible, and collection protocols. - Added protocol mdtests covering explicitly typed metaclass receivers, conflicting class-level special methods, structural membership protocols, and rejection of incompatible membership parameters and return types. - Verified the original issue reproducer on Python 3.11 through 3.14. --- .../resources/mdtest/enums.md | 43 ++++++++++++ .../resources/mdtest/protocols.md | 69 +++++++++++++++++++ .../ty_python_semantic/src/types/relation.rs | 4 +- 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 17830ccbed..44fcbeba02 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -1995,6 +1995,49 @@ reveal_type(Answer.name) # revealed: Literal[Answer.name] reveal_type(Answer.value) # revealed: Literal[Answer.value] ``` +## Enum classes as collection protocols + +An enum class is a container because `EnumMeta.__contains__` accepts any object. Consequently, the +class satisfies `Container[T]` for every `T`, including types unrelated to its members. Its +metaclass also provides the iteration, reversal, and length methods required by the corresponding +collection protocols. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from collections.abc import Collection, Container, Iterable, Reversible +from enum import Enum, IntEnum, StrEnum, auto +from typing import Any + +class Color(Enum): + RED = auto() + +unparameterized_container: Container = Color +any_container: Container[Any] = Color +object_container: Container[object] = Color +member_container: Container[Color] = Color +integer_container: Container[int] = Color +string_container: Container[str] = Color +iterable: Iterable[Color] = Color +reversible: Reversible[Color] = Color +collection: Collection[Color] = Color + +class Number(IntEnum): + ONE = 1 + +integer_enum_container: Container[int] = Number +integer_enum_iterable: Iterable[int] = Number + +class Word(StrEnum): + HELLO = "hello" + +string_enum_container: Container[str] = Word +string_enum_iterable: Iterable[str] = Word +``` + ## Iterating over enum members ```py diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 9d9cb05de8..ba3c923c02 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -4291,6 +4291,75 @@ class Custom: static_assert(is_assignable_to(TypeOf[Custom], CustomProtocol)) ``` +## Class objects with explicitly typed special-method receivers + +A special method defined on a metaclass receives the class object, not an instance of that class. An +explicitly annotated metaclass receiver must therefore be checked against the class object when +matching a collection protocol. Special-method lookup must also ignore conflicting methods defined +on the class itself. + +```py +from collections.abc import Collection, Container, Iterable, Iterator, Reversible +from typing import Any, Protocol +from ty_extensions import static_assert +from ty_extensions._internal import TypeOf, is_assignable_to, is_subtype_of + +class Membership(Protocol): + def __contains__(self, value: int, /) -> bool: ... + +class CollectionMeta(type): + def __contains__(self: type[Any], value: object, /) -> bool: + return True + + def __iter__(self: type[Any]) -> Iterator[int]: + return iter((1,)) + + def __reversed__(self: type[Any]) -> Iterator[int]: + return iter((1,)) + + def __len__(self: type[Any]) -> int: + return 1 + +class ClassCollection(metaclass=CollectionMeta): + def __contains__(self, value: str, /) -> bool: + return True + + def __iter__(self) -> Iterator[str]: + return iter(("member",)) + + def __reversed__(self) -> Iterator[str]: + return iter(("member",)) + +static_assert(is_assignable_to(TypeOf[ClassCollection], Membership)) +static_assert(is_assignable_to(TypeOf[ClassCollection], Container[int])) +static_assert(is_assignable_to(TypeOf[ClassCollection], Container[str])) +static_assert(is_subtype_of(TypeOf[ClassCollection], Container[int])) +static_assert(is_assignable_to(TypeOf[ClassCollection], Iterable[int])) +static_assert(is_assignable_to(TypeOf[ClassCollection], Reversible[int])) +static_assert(is_assignable_to(TypeOf[ClassCollection], Collection[int])) +``` + +The explicit receiver must not hide an incompatible membership parameter or return type. + +```py +class StringMembershipMeta(type): + def __contains__(self: type[Any], value: str, /) -> bool: + return True + +class StringMembership(metaclass=StringMembershipMeta): + pass + +class NonBooleanMembershipMeta(type): + def __contains__(self: type[Any], value: object, /) -> int: + return 1 + +class NonBooleanMembership(metaclass=NonBooleanMembershipMeta): + pass + +static_assert(not is_assignable_to(TypeOf[StringMembership], Container[int])) +static_assert(not is_assignable_to(TypeOf[NonBooleanMembership], Container[int])) +``` + ## Subtyping of protocols with `@classmethod` or `@staticmethod` members The typing spec states that protocols may have `@classmethod` or `@staticmethod` method members. diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 049b0cfd2b..0eb46402d2 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -2259,7 +2259,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .unwrap_or_else(|| { ConstraintSet::from_bool( self.constraints, - self.is_eager_assignability(), + self.relation.is_assignability(), ) }), } @@ -2300,7 +2300,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .unwrap_or_else(|| { ConstraintSet::from_bool( self.constraints, - self.is_eager_assignability(), + self.relation.is_assignability(), ) }), } From 4d4c8fa1c75b00561ea24eefefb13eb5ff80e01f Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 30 Jul 2026 10:48:07 +0530 Subject: [PATCH 150/390] [ty] Emit diagnostic when specializing a non-generic class (#26883) ## Summary Emit `not-subscriptable` when a non-generic class is specialized in a type expression, and recover as `Unknown` instead of an internal `@Todo` type. This highlighted some scenarios in the ecosystem result where `not-subscriptable` is being emitted but it might not be ideal, refer to my inline comments. I've added mdtest cases for these looking at the ecosystem result. Closes https://github.com/astral-sh/ty/issues/2439. ## Test plan Update the mdtest --- .../mdtest/generics/legacy/classes.md | 110 ++++++++++++++++++ .../mdtest/generics/pep695/aliases.md | 1 + .../mdtest/generics/pep695/classes.md | 38 ++++++ .../resources/mdtest/protocols.md | 2 +- .../types/infer/builder/type_expression.rs | 22 +++- 5 files changed, 168 insertions(+), 5 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 308043c34a..a079176aae 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -158,6 +158,116 @@ reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecialized)) reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTypevar)) ``` +## Specializing classes with unavailable generic context + +When an earlier error prevents ty from determining a class's generic context, specializing the class +can emit a cascading `not-subscriptable` diagnostic. + +### Conditional typing compatibility imports + +Libraries support multiple Python versions by importing generic machinery from either +`typing_extensions` or `typing`. ty does not yet recognize the resulting union as the corresponding +typing special form. + +```py +try: + import typing_extensions as typing +except ImportError: + import typing + +T = typing.TypeVar("T") + +# TODO: Fix the conditional typing import in https://github.com/astral-sh/ty/issues/1585. +# error: [invalid-argument-type] "`typing_extensions.TypeVar | typing.TypeVar` is not a valid argument to `Generic`" +class Parser(typing.Generic[T]): ... + +# TODO: Remove this cascading error when https://github.com/astral-sh/ty/issues/1585 is fixed. +parser: Parser[int] # error: [not-subscriptable] "Cannot subscript non-generic type ``" +``` + +### Decorated generic bases + +A decorator that ty cannot fully understand can obscure the generic context of a base class. A +subclass that forwards type variables to that base remains possibly generic. + +```py +import collections.abc +from typing import Generic, TypeVar +from ty_extensions._internal import generic_context + +K = TypeVar("K") +V = TypeVar("V") + +# error: [unresolved-attribute] "Class `Mapping` has no attribute `register`" +@collections.abc.Mapping.register +class Mapping(Generic[K, V]): ... + +# TODO: Invalid decorator causes us to lose the generic context from the class... +reveal_type(generic_context(Mapping)) # revealed: None + +class FrozenDict(Mapping[K, V]): ... + +# TODO: ...which then causes us to emit this +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +mapping: FrozenDict[str, int] +``` + +### Unresolved generic bases + +```py +from typing import TypeVar + +from missing import Base # error: [unresolved-import] + +reveal_type(Base) # revealed: Unknown + +T = TypeVar("T") + +class Child(Base[T]): ... + +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +child: Child[int] +``` + +### Conditional generic bases + +`base1.py`: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Base(Generic[T]): ... +``` + +`base2.py`: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Base(Generic[T]): ... +``` + +```py +from typing import TypeVar + +try: + from base1 import Base +except ImportError: + from base2 import Base + +T = TypeVar("T") + +# error: [unsupported-base] +class Child(Base[T]): ... + +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +child: Child[int] +``` + ## Errors for inconsistent type arguments diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index e6abd346ab..22e205f270 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -152,6 +152,7 @@ class LegacyDict(TypedDict[T]): # error: [unbound-type-variable] x: T +# error: [not-subscriptable] "Cannot subscript non-generic type ``" type LegacyDictInt = LegacyDict[int] # error: [not-subscriptable] "Cannot specialize non-generic type alias `LegacyDictInt`" 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 15a03316b8..37288530fd 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -919,6 +919,44 @@ reveal_type(generic_context(A.merge)) # revealed: ty_extensions._internal.Gener reveal_type(generic_context(Impl.foo)) # revealed: ty_extensions._internal.GenericContext[Self@foo] ``` +## Subscripting non-generic classes + +Subscripting a non-generic class in a type expression is an error. The invalid type expression +recovers to `Unknown`. + +```py +class NonGeneric: ... + +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +def direct(value: NonGeneric[int]) -> None: + reveal_type(value) # revealed: Unknown +``` + +The same diagnostic applies when the specialization is nested inside `type[...]`. + +```py +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +def nested(value: type[NonGeneric[int]]) -> None: + reveal_type(value) # revealed: Unknown +``` + +Inheriting from a non-generic class, or from a specialization of a generic class, does not make the +subclass generic. + +```py +class Child(NonGeneric): ... +class Generic[T]: ... +class SpecializedChild(Generic[int]): ... + +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +def child(value: Child[str]) -> None: + reveal_type(value) # revealed: Unknown + +# error: [not-subscriptable] "Cannot subscript non-generic type ``" +def specialized_child(value: SpecializedChild[bytes]) -> None: + reveal_type(value) # revealed: Unknown +``` + ## Tuple as a PEP-695 generic class Our special handling for `tuple` does not break if `tuple` is defined as a PEP-695 generic class in diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index ba3c923c02..43c2b7f9f4 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -6563,7 +6563,7 @@ class B1(A1[T3], Protocol[T3]): ... class B2(A2[T4], Protocol[T4]): ... # TODO should just be `B2[Any]` -reveal_type(T3.__bound__) # revealed: B2[Any] | @Todo(specialized non-generic class) +reveal_type(T3.__bound__) # revealed: B2[Any] | Unknown # TODO error: [invalid-type-arguments] def f(x: B1[int]): diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 623888fb5a..00edef8fa1 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1341,9 +1341,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } None => { - // TODO: emit a diagnostic if you try to specialize a non-generic class. self.infer_expression(parameters, TypeContext::default()); - todo_type!("specialized non-generic class") + if let Some(builder) = + self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) + { + builder.into_diagnostic(format_args!( + "Cannot subscript non-generic type `{}`", + value_ty.display(self.db()) + )); + } + Type::unknown() } } } @@ -1776,9 +1783,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .unwrap_or(Type::unknown()) } _ => { - // TODO: emit a diagnostic if you try to specialize a non-generic class. self.infer_expression(slice, TypeContext::default()); - todo_type!("specialized non-generic class") + if let Some(builder) = + self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) + { + builder.into_diagnostic(format_args!( + "Cannot subscript non-generic type `{}`", + value_ty.display(self.db()) + )); + } + Type::unknown() } } } From b20daf741241a6829ff8963a203f7847561deb62 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Thu, 30 Jul 2026 00:02:16 -0700 Subject: [PATCH 151/390] [ty] refactor: add helper function to send partial results (#27249) --- .../api/requests/workspace_diagnostic.rs | 24 +++++++++++-------- crates/ty_server/src/session/client.rs | 15 ++++++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs index 088244900c..a04267a6ca 100644 --- a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs +++ b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs @@ -5,11 +5,10 @@ use std::time::{Duration, Instant}; use lsp_server::RequestId; use lsp_types::WorkspaceDiagnosticRequest; use lsp_types::{ - FullDocumentDiagnosticReport, PreviousResultId, ProgressNotification, ProgressParams, - ProgressToken, UnchangedDocumentDiagnosticReport, Uri, WorkspaceDiagnosticParams, - WorkspaceDiagnosticReport, WorkspaceDiagnosticReportPartialResult, - WorkspaceDocumentDiagnosticReport, WorkspaceFullDocumentDiagnosticReport, - WorkspaceUnchangedDocumentDiagnosticReport, + FullDocumentDiagnosticReport, PreviousResultId, ProgressToken, + UnchangedDocumentDiagnosticReport, Uri, WorkspaceDiagnosticParams, WorkspaceDiagnosticReport, + WorkspaceDiagnosticReportPartialResult, WorkspaceDocumentDiagnosticReport, + WorkspaceFullDocumentDiagnosticReport, WorkspaceUnchangedDocumentDiagnosticReport, }; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::File; @@ -624,12 +623,17 @@ impl Streaming { .map(WorkspaceDocumentDiagnosticReport::WorkspaceFullDocumentDiagnosticReport) .collect(); - let report = self.create_result(items); + let partial_result = match self.create_result(items) { + WorkspaceDiagnosticReportResult::PartialReport(partial_report) => partial_report, + WorkspaceDiagnosticReportResult::Report(WorkspaceDiagnosticReport { items }) => { + // WorkspaceDiagnosticReport and WorkspaceDiagnosticReportPartialResult have the + // same serialization in the LSP. + // https://github.com/microsoft/language-server-protocol/issues/2281 + WorkspaceDiagnosticReportPartialResult { items } + } + }; self.client - .send_notification::(ProgressParams { - token: self.token.clone(), - value: json!(report), - }); + .send_partial_result::(self.token.clone(), partial_result); self.last_flush = Instant::now(); } diff --git a/crates/ty_server/src/session/client.rs b/crates/ty_server/src/session/client.rs index fb16e2106f..32baa04c40 100644 --- a/crates/ty_server/src/session/client.rs +++ b/crates/ty_server/src/session/client.rs @@ -194,6 +194,21 @@ impl Client { self.show_message(message, lsp_types::MessageType::Error); } + /// Sends a notification of partial result progress to the client, via a `$/progress` + /// notification. + pub(crate) fn send_partial_result( + &self, + token: lsp_types::ProgressToken, + partial_result: R::PartialResult, + ) where + R: lsp_types::RequestWithPartialResults, + { + self.send_notification::(lsp_types::ProgressParams { + token, + value: serde_json::to_value(partial_result).expect("Partial result to be serializable"), + }); + } + /// Re-queues this request after a salsa cancellation for a retry. /// /// The main loop will skip the retry if the client cancelled the request in the meantime. From 7da4b8b8d78fd6df2b3e06d8466d9cd49822900d Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 30 Jul 2026 00:37:49 -0700 Subject: [PATCH 152/390] [ty] Respect bounds and constraints in generic materializations (#27228) ## Summary Respect declared upper bounds and constraints when materializing generic type arguments across covariance, contravariance, and invariance. Preserve constrained top and bottom materializations during type-relation checks, filter valid alternatives safely, and avoid recursively forcing lazy bounds. Narrow runtime class checks using unknown specializations instead of type-parameter defaults, fixing incorrect `Never`-default narrowing. Closes astral-sh/ty#1109. ## Test plan - Add mdtests for PEP 695 and legacy bounded and constrained generics across all variance directions, subtype and assignability relations, overlapping and gradual constraints, and partial unions and intersections. - Cover invariant attributes, getter and setter polarity, unrelated `Any`, mixed constrained and invariant type arguments, and recursive bounds. - Cover positive and negative bounded and constrained `isinstance` narrowing, tuple class information, `issubclass`, class-pattern fallthrough, and `Never`-default regressions. - Cover equivalence for gradual bounded shape aliases, invariant and covariant specializations, and defaulted nested generics. ## Ecosystem Ecosystem changes are all positive and in line with the intended semantics of this PR. --- .../resources/mdtest/narrow/isinstance.md | 201 ++++++++- .../resources/mdtest/narrow/match.md | 57 +++ .../type_properties/is_equivalent_to.md | 90 ++++ .../mdtest/type_properties/materialization.md | 400 ++++++++++++++++++ .../resources/mdtest/union_types.md | 40 ++ crates/ty_python_semantic/src/types.rs | 6 +- .../src/types/class/static_literal.rs | 4 +- .../ty_python_semantic/src/types/generics.rs | 252 +++++++++-- .../ty_python_semantic/src/types/typevar.rs | 53 ++- 9 files changed, 1065 insertions(+), 38 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index bf3f23c2b2..ebcee31192 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -625,6 +625,73 @@ def _(x: object): reveal_type(x.get()) # revealed: object ``` +A bounded covariant generic uses its declared upper bound rather than `object`: + +```py +class BoundedCovariant[T: int]: + def get(self) -> T: + raise NotImplementedError + +def _(x: object): + if isinstance(x, BoundedCovariant): + reveal_type(x) # revealed: BoundedCovariant[int] + reveal_type(x.get()) # revealed: int +``` + +Negative narrowing must exclude every specialization of a bounded generic, including a gradual one. + +```py +from typing import Any + +def excludes_bounded_generic(value: BoundedCovariant[Any] | bool) -> bool: + if isinstance(value, BoundedCovariant): + reveal_type(value) # revealed: BoundedCovariant[Any] + return False + + reveal_type(value) # revealed: bool + return value +``` + +The same exclusion applies when the generic appears in a tuple of runtime classes. + +```py +def excludes_bounded_generic_tuple( + value: BoundedCovariant[Any] | bool | bytes, +) -> bool: + if isinstance(value, (BoundedCovariant, bytes)): + reveal_type(value) # revealed: BoundedCovariant[Any] | bytes + return False + + reveal_type(value) # revealed: bool + return value +``` + +Constrained type parameters preserve the materialization of the generic class while making the union +of valid constraints available when reading a covariant attribute: + +```py +class ConstrainedCovariant[T: (int, str)]: + def get(self) -> T: + raise NotImplementedError + +def _(x: object): + if isinstance(x, ConstrainedCovariant): + reveal_type(x) # revealed: Top[ConstrainedCovariant[Unknown]] + reveal_type(x.get()) # revealed: int | str +``` + +Constrained generics must also be excluded by negative narrowing. + +```py +def excludes_constrained_generic(value: ConstrainedCovariant[Any] | bool) -> bool: + if isinstance(value, ConstrainedCovariant): + reveal_type(value) # revealed: ConstrainedCovariant[Any] + return False + + reveal_type(value) # revealed: bool + return value +``` + Similarly, contravariant type parameters use their lower bound of `Never`: ```py @@ -686,7 +753,7 @@ class InvariantWithAny[T: int]: def _(x: object): if isinstance(x, InvariantWithAny): reveal_type(x) # revealed: Top[InvariantWithAny[Unknown]] - reveal_type(x.a) # revealed: object + reveal_type(x.a) # revealed: int reveal_type(x.b) # revealed: Any ``` @@ -726,6 +793,28 @@ def _(x: Invariant[int] | Covariant[str]): reveal_type(x) # revealed: Covariant[str] & ~Top[Invariant[Unknown]] ``` +The built-in `tuple` stores its variable-length shape separately from its generic type argument. +Narrowing must preserve and materialize that shape. + +```py +def narrow_tuple(value: object) -> None: + if isinstance(value, tuple): + reveal_type(value) # revealed: tuple[object, ...] +``` + +A tuple subclass retains its nominal type and inherits its tuple shape from its specialized base. +The subclass's own type parameter is still materialized using its declared bound. + +```py +class BoundedTuple[T: int](tuple[T, str]): ... + +def narrow_tuple_subclass(value: object) -> None: + if isinstance(value, BoundedTuple): + reveal_type(value) # revealed: BoundedTuple[int] + reveal_type(value[0]) # revealed: int + reveal_type(value[1]) # revealed: str +``` + The behavior of `issubclass()` is similar. ```py @@ -738,6 +827,65 @@ def _(x: type[object], y: type[object], z: type[object]): reveal_type(z) # revealed: type[Top[Invariant[Unknown]]] ``` +Negative `issubclass()` narrowing also excludes every specialization of a bounded generic. + +```py +def excludes_bounded_generic_subclass( + cls: type[BoundedCovariant[Any]] | type[bool], +) -> type[bool]: + if issubclass(cls, BoundedCovariant): + reveal_type(cls) # revealed: type[BoundedCovariant[Any]] + return bool + + reveal_type(cls) # revealed: + return cls +``` + +## Narrowing recursively bounded generics + +An `isinstance()` check must not recurse indefinitely when a generic bound refers to its own class. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any + +class Recursive[T: "Recursive[Any]"]: ... + +def narrow(value: object) -> None: + if isinstance(value, Recursive): + reveal_type(value) # revealed: Recursive[object] +``` + +A self-referential bound must also be safe when its recursion is hidden behind a type alias. + +```py +class AliasedRecursive[T: "RecursiveAlias"]: ... + +type RecursiveAlias = AliasedRecursive[Any] + +def narrow_alias(value: object) -> None: + if isinstance(value, AliasedRecursive): + reveal_type(value) # revealed: AliasedRecursive[object] +``` + +The same cycle recovery must handle bounds shared by mutually recursive generic classes. + +```py +class Left[T: "Right[Any]"]: ... +class Right[U: Left[Any]]: ... + +def narrow_mutual(value: object) -> None: + if isinstance(value, Left): + reveal_type(value) # revealed: Left[object] + + if isinstance(value, Right): + reveal_type(value) # revealed: Right[object] +``` + ## Narrowing generic defaults in Python 3.13 When a type parameter has a bare `Any` default, narrowing still materializes the substituted @@ -775,6 +923,57 @@ def _(x: object): reveal_type(x.y) # revealed: tuple[A, object] ``` +`isinstance(value, Box)` checks the runtime class, not the type argument used to specialize it. +Narrowing must therefore preserve the original type argument instead of substituting `Box`'s +default. + +```py +from typing import assert_never + +class Box[T: str = str]: + value: T + + def __init__(self, value: T) -> None: ... + +def box_with_default[T: str = str](value: Box[T] | T) -> Box[T]: + if isinstance(value, Box): + reveal_type(value) # revealed: Box[T@box_with_default] + return value + + if not isinstance(value, Box): + reveal_type(value) # revealed: T@box_with_default & ~Top[Box[Unknown]] + return Box[T](value) + + assert_never(value) +``` + +When `isinstance()` narrows an unknown value to a tuple subclass, its type argument comes from the +declared upper bound, not the default. Its element types are inherited from the specialized base. + +```py +class DefaultedTuple[T: int = bool](tuple[T, str]): ... + +def narrow_defaulted_tuple(value: object) -> None: + if isinstance(value, DefaultedTuple): + reveal_type(value) # revealed: DefaultedTuple[int] + reveal_type(value[0]) # revealed: int + reveal_type(value[1]) # revealed: str +``` + +Negative narrowing also excludes gradual specializations of the defaulted tuple subclass. + +```py +def excludes_defaulted_tuple(value: DefaultedTuple[Any] | bool) -> bool: + if isinstance(value, DefaultedTuple): + reveal_type(value) # revealed: DefaultedTuple[Any] + reveal_type(value[0]) # revealed: Any + reveal_type(value[1]) # revealed: str + return False + + reveal_type(value) # revealed: bool + return value +``` + ## Narrowing generic `classmethod` After an `isinstance(..., classmethod)` branch unwraps and replaces a generic `classmethod`, the diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index 8f6a3593cf..af988bb001 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -112,6 +112,63 @@ def f(x: Covariant[int]): assert_never(x) ``` +## Generic patterns ignore type parameter defaults + +A generic class pattern matches every runtime specialization, not only the specialization described +by its type parameter's default. + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import Any + +class Box[T: str = str]: + value: T + + def __init__(self, value: T) -> None: ... + +def box_with_default[T: str = str](value: Box[T] | T) -> Box[T]: + match value: + case Box(): + reveal_type(value) # revealed: Box[T@box_with_default] + return value + case remaining: + reveal_type(remaining) # revealed: T@box_with_default & ~Top[Box[Unknown]] + return Box[T](remaining) +``` + +When a class pattern matches a tuple subclass, its type argument comes from the declared upper +bound, not the default. Its element types are inherited from the specialized base. + +```py +class DefaultedTuple[T: int = bool](tuple[T, str]): ... + +def match_defaulted_tuple(value: object) -> None: + match value: + case DefaultedTuple(): + reveal_type(value) # revealed: DefaultedTuple[int] + reveal_type(value[0]) # revealed: int + reveal_type(value[1]) # revealed: str +``` + +The same pattern excludes gradual specializations from the remaining match arms. + +```py +def excludes_defaulted_tuple(value: DefaultedTuple[Any] | bool) -> bool: + match value: + case DefaultedTuple(): + reveal_type(value) # revealed: DefaultedTuple[Any] + reveal_type(value[0]) # revealed: Any + reveal_type(value[1]) # revealed: str + return False + case remaining: + reveal_type(remaining) # revealed: bool + return remaining +``` + ## Class patterns with generic `@final` classes These work the same as non-`@final` classes. diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index 43109c179f..cb20fd5a4b 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -84,6 +84,96 @@ static_assert(not is_equivalent_to(type, type[Any])) static_assert(not is_equivalent_to(type[object], type[Any])) ``` +## Equivalent bounded gradual specializations + +A bounded generic specialized with a gradual type alias is equivalent to the same generic +specialized with the expanded alias. + +```toml +[environment] +python-version = "3.13" +``` + +For a covariant bounded type parameter, this applies to aliases containing either `Any` or +`Unknown`. + +```py +from typing import Any + +from ty_extensions import Unknown, static_assert +from ty_extensions._internal import is_equivalent_to + +type AnyTuple = tuple[Any, ...] +type UnknownTuple = tuple[Unknown, ...] + +class BoundedCovariant[T: tuple[int, ...]]: + def get(self) -> T: + raise NotImplementedError + +static_assert(is_equivalent_to(BoundedCovariant[AnyTuple], BoundedCovariant[tuple[Any, ...]])) +static_assert(is_equivalent_to(BoundedCovariant[UnknownTuple], BoundedCovariant[AnyTuple])) +``` + +The same gradual tuple alias remains equivalent when the bounded type parameter is invariant. + +```py +class BoundedInvariant[T: tuple[int, ...]]: + value: T + +static_assert(is_equivalent_to(BoundedInvariant[AnyTuple], BoundedInvariant[tuple[Any, ...]])) +``` + +`Outer[int, Inner]` is equivalent to `Outer[int, Inner[Any]]` because `Inner` defaults to `Any`. +`Outer[int]` is equivalent to the same explicit specialization because `Outer` defaults to +`Inner[Any]`. + +```py +class Inner[T: int = Any]: + def get(self) -> T: + raise NotImplementedError + +class Outer[T: int, U: Inner[Any] = Inner[Any]]: + def get(self) -> U: + raise NotImplementedError + +static_assert(is_equivalent_to(Outer[int, Inner[Any]], Outer[int, Inner])) +static_assert(is_equivalent_to(Outer[int, Inner[Any]], Outer[int])) +``` + +## Bounded gradual specializations are distinct from upper bounds + +A generic specialized with a gradual type argument is not equivalent to the same generic specialized +with the type parameter's upper bound. + +```toml +[environment] +python-version = "3.13" +``` + +For a covariant type parameter: + +```py +from typing import Any + +from ty_extensions import static_assert +from ty_extensions._internal import is_equivalent_to + +class BoundedCovariant[T: tuple[int, ...]]: + def get(self) -> T: + raise NotImplementedError + +static_assert(not is_equivalent_to(BoundedCovariant[tuple[Any, ...]], BoundedCovariant[tuple[int, ...]])) +``` + +The same distinction applies to an invariant type parameter. + +```py +class BoundedInvariant[T: tuple[int, ...]]: + value: T + +static_assert(not is_equivalent_to(BoundedInvariant[tuple[Any, ...]], BoundedInvariant[tuple[int, ...]])) +``` + ## Unions and intersections ```pyi diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md index 03406f2865..84692f51b0 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -681,6 +681,406 @@ def contravariant(top: Top[ContravariantCallable], bottom: Bottom[ContravariantC reveal_type(bottom) # revealed: (GenericContravariant[Never], /) -> None ``` +## Bounded generic type parameters + +Top materialization of a covariant generic uses the type parameter's declared upper bound. Bottom +materialization uses its lower bound, `Never`. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, Generic, Never, TypeVar +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_equivalent_to, is_subtype_of + +class BoundedCovariant[T: int]: + def get(self) -> T: + raise NotImplementedError + +static_assert(is_equivalent_to(Top[BoundedCovariant[Any]], BoundedCovariant[int])) +static_assert(is_equivalent_to(Bottom[BoundedCovariant[Any]], BoundedCovariant[Never])) +static_assert(is_subtype_of(BoundedCovariant[Any], Top[BoundedCovariant[Any]])) +static_assert(is_subtype_of(BoundedCovariant[Any], BoundedCovariant[int])) +``` + +A type alias can conceal a gradual argument; the same subtype relationships still apply. + +```py +type AliasedAny = Any + +static_assert(is_subtype_of(BoundedCovariant[AliasedAny], Top[BoundedCovariant[AliasedAny]])) +static_assert(is_subtype_of(BoundedCovariant[AliasedAny], BoundedCovariant[int])) +``` + +An alias for a static upper bound remains static. It absorbs a bounded gradual specialization in +either union order. + +```py +type AliasedInt = int + +def aliased_static_bound( + gradual_first: BoundedCovariant[Any] | BoundedCovariant[AliasedInt], + gradual_last: BoundedCovariant[AliasedInt] | BoundedCovariant[Any], +) -> None: + reveal_type(gradual_first) # revealed: BoundedCovariant[AliasedInt] + reveal_type(gradual_last) # revealed: BoundedCovariant[AliasedInt] +``` + +Contravariance reverses which bound is used by top and bottom materialization. + +```py +class BoundedContravariant[T: int]: + def put(self, value: T) -> None: ... + +static_assert(is_equivalent_to(Top[BoundedContravariant[Any]], BoundedContravariant[Never])) +static_assert(is_equivalent_to(Bottom[BoundedContravariant[Any]], BoundedContravariant[int])) +``` + +For an invariant generic, materialize attributes and method parameters according to their own +variance. An unrelated `Any` attribute must remain gradual. + +```py +class BoundedInvariant[T: int]: + value: T + unrelated: Any + + def get(self) -> T: + raise NotImplementedError + + def put(self, value: T) -> None: ... + +def bounded_invariant( + top: Top[BoundedInvariant[Any]], + bottom: Bottom[BoundedInvariant[Any]], +) -> None: + reveal_type(top.value) # revealed: int + reveal_type(top.unrelated) # revealed: Any + reveal_type(top.get) # revealed: bound method Top[BoundedInvariant[Any]].get() -> int + reveal_type(top.put) # revealed: bound method Top[BoundedInvariant[Any]].put(value: Never) -> None + + reveal_type(bottom.unrelated) # revealed: Any + reveal_type(bottom.get) # revealed: bound method Bottom[BoundedInvariant[Any]].get() -> Never + reveal_type(bottom.put) # revealed: bound method Bottom[BoundedInvariant[Any]].put(value: int) -> None + reveal_type(bottom.value) # revealed: Never +``` + +Explicitly covariant and contravariant legacy `TypeVar` declarations obey the same bounded +materialization rules. + +```py +BoundedT_co = TypeVar("BoundedT_co", bound=int, covariant=True) + +class LegacyBoundedCovariant(Generic[BoundedT_co]): ... + +static_assert(is_equivalent_to(Top[LegacyBoundedCovariant[Any]], LegacyBoundedCovariant[int])) +static_assert(is_equivalent_to(Bottom[LegacyBoundedCovariant[Any]], LegacyBoundedCovariant[Never])) + +BoundedT_contra = TypeVar("BoundedT_contra", bound=int, contravariant=True) + +class LegacyBoundedContravariant(Generic[BoundedT_contra]): ... + +static_assert(is_equivalent_to(Top[LegacyBoundedContravariant[Any]], LegacyBoundedContravariant[Never])) +static_assert(is_equivalent_to(Bottom[LegacyBoundedContravariant[Any]], LegacyBoundedContravariant[int])) +``` + +Reading an attribute of a top-materialized legacy invariant generic yields the type parameter's +upper bound; reading the same attribute from its bottom materialization yields the lower bound. + +```py +BoundedT = TypeVar("BoundedT", bound=int) + +class LegacyBoundedInvariant(Generic[BoundedT]): + value: BoundedT + +def legacy_bounded_invariant( + legacy_top: Top[LegacyBoundedInvariant[Any]], + legacy_bottom: Bottom[LegacyBoundedInvariant[Any]], +) -> None: + reveal_type(legacy_top.value) # revealed: int + reveal_type(legacy_bottom.value) # revealed: Never +``` + +## Constrained generic type parameters + +A constrained type parameter cannot generally be replaced by the union of its constraints: the union +need not itself be a valid specialization. Top and bottom materialization must instead retain the +covariant generic and its valid specializations. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, Generic, Never, TypeVar +from ty_extensions import Bottom, Intersection, Not, Top, static_assert +from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of + +class ConstrainedCovariant[T: (int, str)]: + def get(self) -> T: + raise NotImplementedError + +def constrained_covariant( + top: Top[ConstrainedCovariant[Any]], + bottom: Bottom[ConstrainedCovariant[Any]], +) -> None: + reveal_type(top) # revealed: Top[ConstrainedCovariant[Any]] + reveal_type(bottom) # revealed: Bottom[ConstrainedCovariant[Any]] + +static_assert(is_subtype_of(ConstrainedCovariant[int], Top[ConstrainedCovariant[Any]])) +static_assert(is_subtype_of(ConstrainedCovariant[str], Top[ConstrainedCovariant[Any]])) +static_assert(is_subtype_of(ConstrainedCovariant[Any], Top[ConstrainedCovariant[Any]])) +static_assert(not is_subtype_of(Top[ConstrainedCovariant[Any]], ConstrainedCovariant[int])) +static_assert(not is_subtype_of(Top[ConstrainedCovariant[Any]], ConstrainedCovariant[str])) +static_assert(is_subtype_of(Bottom[ConstrainedCovariant[Any]], ConstrainedCovariant[int])) +static_assert(is_subtype_of(Bottom[ConstrainedCovariant[Any]], ConstrainedCovariant[str])) +static_assert(is_subtype_of(Bottom[ConstrainedCovariant[Any]], Top[ConstrainedCovariant[Any]])) +static_assert(not is_equivalent_to(Intersection[ConstrainedCovariant[str], Not[ConstrainedCovariant[int]]], Never)) + +static_assert(is_assignable_to(ConstrainedCovariant[int], Top[ConstrainedCovariant[Any]])) +static_assert(not is_assignable_to(Top[ConstrainedCovariant[Any]], ConstrainedCovariant[int])) +static_assert(is_assignable_to(Bottom[ConstrainedCovariant[Any]], ConstrainedCovariant[int])) +``` + +Contravariant constrained generics likewise preserve their materializations while reversing the +relationship between input positions and top or bottom types. + +```py +class ConstrainedContravariant[T: (int, str)]: + def put(self, value: T) -> None: ... + +def constrained_contravariant( + top: Top[ConstrainedContravariant[Any]], + bottom: Bottom[ConstrainedContravariant[Any]], +) -> None: + reveal_type(top) # revealed: Top[ConstrainedContravariant[Any]] + reveal_type(bottom) # revealed: Bottom[ConstrainedContravariant[Any]] + +static_assert(is_subtype_of(ConstrainedContravariant[int], Top[ConstrainedContravariant[Any]])) +static_assert(is_subtype_of(ConstrainedContravariant[str], Top[ConstrainedContravariant[Any]])) +static_assert(not is_subtype_of(Top[ConstrainedContravariant[Any]], ConstrainedContravariant[int])) +static_assert(not is_subtype_of(Top[ConstrainedContravariant[Any]], ConstrainedContravariant[str])) +static_assert(is_subtype_of(Bottom[ConstrainedContravariant[Any]], ConstrainedContravariant[int])) +static_assert(is_subtype_of(Bottom[ConstrainedContravariant[Any]], ConstrainedContravariant[str])) +static_assert(is_subtype_of(Bottom[ConstrainedContravariant[Any]], Top[ConstrainedContravariant[Any]])) + +static_assert(is_assignable_to(ConstrainedContravariant[int], Top[ConstrainedContravariant[Any]])) +static_assert(not is_assignable_to(Top[ConstrainedContravariant[Any]], ConstrainedContravariant[int])) +static_assert(is_assignable_to(Bottom[ConstrainedContravariant[Any]], ConstrainedContravariant[int])) +``` + +An invariant constrained parameter materializes readable values to the union of valid constraints +and writable parameters to `Never`. Unrelated gradual attributes remain `Any`. + +```py +class ConstrainedInvariant[T: (int, str)]: + value: T + unrelated: Any + + def get(self) -> T: + raise NotImplementedError + + def put(self, value: T) -> None: ... + +def constrained_invariant( + top: Top[ConstrainedInvariant[Any]], + bottom: Bottom[ConstrainedInvariant[Any]], +) -> None: + reveal_type(top.value) # revealed: int | str + reveal_type(top.unrelated) # revealed: Any + reveal_type(top.get) # revealed: bound method Top[ConstrainedInvariant[Any]].get() -> int | str + reveal_type(top.put) # revealed: bound method Top[ConstrainedInvariant[Any]].put(value: Never) -> None + + reveal_type(bottom.unrelated) # revealed: Any + reveal_type(bottom.get) # revealed: bound method Bottom[ConstrainedInvariant[Any]].get() -> Never + reveal_type(bottom.put) # revealed: bound method Bottom[ConstrainedInvariant[Any]].put(value: int | str) -> None + reveal_type(bottom.value) # revealed: Never +``` + +Direct attribute writes are currently checked against the readable union rather than the safe +`Never` parameter used for setters. + +```py +def constrained_invariant_writes(top: Top[ConstrainedInvariant[Any]]) -> None: + # TODO: Reject these writes; neither value is safe for every specialization. + top.value = 1 + top.value = "value" + top.value = 1.5 # error: [invalid-assignment] +``` + +Legacy constrained type variables preserve the same covariant and contravariant subtype +relationships. + +```py +ConstrainedT_co = TypeVar("ConstrainedT_co", int, str, covariant=True) + +class LegacyConstrainedCovariant(Generic[ConstrainedT_co]): ... + +static_assert(is_subtype_of(LegacyConstrainedCovariant[int], Top[LegacyConstrainedCovariant[Any]])) +static_assert(is_subtype_of(Bottom[LegacyConstrainedCovariant[Any]], LegacyConstrainedCovariant[str])) + +ConstrainedT_contra = TypeVar("ConstrainedT_contra", int, str, contravariant=True) + +class LegacyConstrainedContravariant(Generic[ConstrainedT_contra]): ... + +static_assert(is_subtype_of(LegacyConstrainedContravariant[int], Top[LegacyConstrainedContravariant[Any]])) +static_assert(is_subtype_of(Bottom[LegacyConstrainedContravariant[Any]], LegacyConstrainedContravariant[str])) +``` + +A partially gradual type argument filters out constraints incompatible with its static `int` arm. + +```py +static_assert(is_equivalent_to(Bottom[ConstrainedCovariant[Any | int]], ConstrainedCovariant[int])) +static_assert(not is_subtype_of(ConstrainedCovariant[str], Top[ConstrainedCovariant[Any | int]])) +static_assert(is_equivalent_to(Top[ConstrainedContravariant[Any | int]], ConstrainedContravariant[int])) +static_assert(not is_subtype_of(Bottom[ConstrainedContravariant[Any | int]], ConstrainedContravariant[str])) +``` + +An intersection of `int` and `Any` likewise retains only the compatible `int` constraint for both +variances. + +```py +type GradualInt = Intersection[int, Any] + +static_assert(is_subtype_of(ConstrainedCovariant[int], Top[ConstrainedCovariant[GradualInt]])) +static_assert(not is_subtype_of(ConstrainedCovariant[str], Top[ConstrainedCovariant[GradualInt]])) +static_assert(is_subtype_of(Bottom[ConstrainedCovariant[GradualInt]], ConstrainedCovariant[int])) +static_assert(not is_subtype_of(Bottom[ConstrainedCovariant[GradualInt]], ConstrainedCovariant[str])) +static_assert(is_subtype_of(ConstrainedContravariant[int], Top[ConstrainedContravariant[GradualInt]])) +static_assert(not is_subtype_of(ConstrainedContravariant[str], Top[ConstrainedContravariant[GradualInt]])) +static_assert(is_subtype_of(Bottom[ConstrainedContravariant[GradualInt]], ConstrainedContravariant[int])) +static_assert(not is_subtype_of(Bottom[ConstrainedContravariant[GradualInt]], ConstrainedContravariant[str])) +``` + +## Gradual generic constraints + +When `Any` is itself a constraint, static specializations outside the other constraint must remain +valid. Reading a top-materialized covariant value produces `object`. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_subtype_of + +class GradualConstrainedCovariant[T: (int, Any)]: + def get(self) -> T: + raise NotImplementedError + +class GradualConstrainedContravariant[T: (int, Any)]: + def put(self, value: T) -> None: ... + +def gradual_constraints(value: Top[GradualConstrainedCovariant[Any]]) -> None: + reveal_type(value) # revealed: Top[GradualConstrainedCovariant[Any]] + reveal_type(value.get()) # revealed: object + +static_assert(is_subtype_of(GradualConstrainedCovariant[int], Top[GradualConstrainedCovariant[Any]])) +static_assert(is_subtype_of(GradualConstrainedCovariant[str], Top[GradualConstrainedCovariant[Any]])) +static_assert(is_subtype_of(Bottom[GradualConstrainedCovariant[Any]], GradualConstrainedCovariant[int])) +static_assert(is_subtype_of(Bottom[GradualConstrainedCovariant[Any]], GradualConstrainedCovariant[str])) +static_assert(is_subtype_of(GradualConstrainedContravariant[int], Top[GradualConstrainedContravariant[Any]])) +static_assert(is_subtype_of(GradualConstrainedContravariant[str], Top[GradualConstrainedContravariant[Any]])) +static_assert(is_subtype_of(Bottom[GradualConstrainedContravariant[Any]], GradualConstrainedContravariant[int])) +static_assert(is_subtype_of(Bottom[GradualConstrainedContravariant[Any]], GradualConstrainedContravariant[str])) +``` + +## Overlapping generic constraints + +When one valid constraint is a subtype of another, the broader constraint supplies the upper bound +and the narrower constraint supplies the lower bound. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_equivalent_to + +class OverlappingCovariant[T: (int, bool)]: + def get(self) -> T: + raise NotImplementedError + +class OverlappingContravariant[T: (int, bool)]: + def put(self, value: T) -> None: ... + +static_assert(is_equivalent_to(Top[OverlappingCovariant[Any]], OverlappingCovariant[int])) +static_assert(is_equivalent_to(Bottom[OverlappingCovariant[Any]], OverlappingCovariant[bool])) +static_assert(is_equivalent_to(Top[OverlappingContravariant[Any]], OverlappingContravariant[bool])) +static_assert(is_equivalent_to(Bottom[OverlappingContravariant[Any]], OverlappingContravariant[int])) +``` + +## Mixed constrained and unconstrained type parameters + +A generic with both constrained and unconstrained parameters materializes each parameter +independently. Filtering the constrained parameter must not change the unconstrained parameter. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any +from ty_extensions import Bottom, Intersection, Top, static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +type GradualInt = Intersection[int, Any] + +class MixedConstrained[T: (int, str), U]: + value: T + items: list[U] + +def mixed_constrained( + top: Top[MixedConstrained[Any, Any]], + bottom: Bottom[MixedConstrained[Any, Any]], +) -> None: + reveal_type(top) # revealed: Top[MixedConstrained[Any, Any]] + reveal_type(bottom) # revealed: Bottom[MixedConstrained[Any, Any]] + reveal_type(top.value) # revealed: int | str + reveal_type(top.items) # revealed: Top[list[Any]] + reveal_type(bottom.items) # revealed: Bottom[list[Any]] + reveal_type(bottom.value) # revealed: Never + +static_assert(is_subtype_of(MixedConstrained[int, int], Top[MixedConstrained[Any, Any]])) +static_assert(is_subtype_of(MixedConstrained[str, int], Top[MixedConstrained[Any, Any]])) +static_assert(is_subtype_of(Bottom[MixedConstrained[Any, Any]], MixedConstrained[int, int])) +static_assert(is_subtype_of(Bottom[MixedConstrained[Any, Any]], MixedConstrained[str, int])) +static_assert(is_subtype_of(MixedConstrained[int, int], Top[MixedConstrained[Any, int]])) +static_assert(is_assignable_to(MixedConstrained[int, str], Top[MixedConstrained[GradualInt, Any]])) +static_assert(not is_assignable_to(MixedConstrained[str, str], Top[MixedConstrained[GradualInt, Any]])) +static_assert(is_assignable_to(Bottom[MixedConstrained[GradualInt, Any]], MixedConstrained[int, int])) +static_assert(not is_assignable_to(Bottom[MixedConstrained[GradualInt, Any]], MixedConstrained[str, int])) +``` + +## Materialization does not force invalid recursive specializations + +An invalid self-referential bound must produce the expected diagnostics without forcing recursive +materialization. Invalid specializations recover as `Unknown`. + +```toml +[environment] +python-version = "3.12" +``` + +```py +# error: [invalid-type-arguments] +class RecursiveSpecialization[T: "RecursiveSpecialization[int]"]: ... + +# error: [invalid-type-arguments] +def recursive_specialization(value: RecursiveSpecialization[str]) -> None: + reveal_type(value) # revealed: RecursiveSpecialization[Unknown] +``` + ## Invalid use `Top[]` and `Bottom[]` are special forms that take a single argument. diff --git a/crates/ty_python_semantic/resources/mdtest/union_types.md b/crates/ty_python_semantic/resources/mdtest/union_types.md index 0c9016d19b..67539036d7 100644 --- a/crates/ty_python_semantic/resources/mdtest/union_types.md +++ b/crates/ty_python_semantic/resources/mdtest/union_types.md @@ -373,3 +373,43 @@ def _( reveal_type(g) # revealed: Invariant[Any] | Invariant[Any | str] reveal_type(h) # revealed: Invariant[Any | str] | Invariant[Any] ``` + +A type alias does not make a gradual type argument static. Covariant unions simplify the same way +whether the gradual argument is written directly or hidden behind one or more aliases. + +```py +type GradualAlias = Any | str +type NestedGradualAlias = GradualAlias + +def gradual_aliases( + direct_first: Covariant[Any] | Covariant[GradualAlias], + direct_last: Covariant[GradualAlias] | Covariant[Any], + nested_first: Covariant[Any] | Covariant[NestedGradualAlias], + nested_last: Covariant[NestedGradualAlias] | Covariant[Any], +) -> None: + reveal_type(direct_first) # revealed: Covariant[GradualAlias] + reveal_type(direct_last) # revealed: Covariant[GradualAlias] + reveal_type(nested_first) # revealed: Covariant[NestedGradualAlias] + reveal_type(nested_last) # revealed: Covariant[NestedGradualAlias] +``` + +Matching materialization endpoints do not establish that gradual tuple arguments have the same +shape. A bounded generic must preserve which tuple position contains the gradual element. + +```py +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import is_equivalent_to + +type L = tuple[Any, int] +type R = tuple[int, Any] + +class C[T: tuple[int, int]]: + def get(self) -> T: + raise NotImplementedError + +static_assert(is_equivalent_to(Top[C[L]], Top[C[R]])) +static_assert(is_equivalent_to(Bottom[C[L]], Bottom[C[R]])) +static_assert(not is_equivalent_to(C[L], C[R])) +static_assert(not is_equivalent_to(C[L] | C[R], C[L])) +static_assert(not is_equivalent_to(C[R] | C[L], C[R])) +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 18c56a96d3..3d4576eebe 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -383,7 +383,7 @@ pub(crate) struct VisitSpecialization; /// How a generic type has been specialized. /// -/// This matters only if there is at least one invariant type parameter. +/// This matters only if there is at least one invariant or constrained type parameter. /// For example, we represent `Top[list[Any]]` as a `GenericAlias` with /// `MaterializationKind` set to Top, which we denote as `Top[list[Any]]`. /// A type `Top[list[T]]` includes all fully static list types `list[U]` where `U` is @@ -1579,8 +1579,8 @@ impl<'db> Type<'db> { /// More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of /// the dynamic types (`Any`, `Unknown`, `Todo`) replaced as follows: /// - /// - In covariant position, it's replaced with `object` (TODO: it should be the `TypeVar`'s upper - /// bound, if any) + /// - In covariant position, it's replaced with `object`, or the type variable's upper bound + /// when the dynamic type is a bounded generic argument /// - In contravariant position, it's replaced with `Never` /// - In invariant position, we replace the object with a special form recording that it's the top /// or bottom materialization. diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index d81fb85728..cdd45ce7ee 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -525,7 +525,7 @@ impl<'db> StaticClassLiteral<'db> { pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { self.apply_specialization(db, |generic_context| { generic_context - .default_specialization(db, self.known(db)) + .unknown_specialization(db, self.known(db)) .materialize_impl( db, MaterializationKind::Top, @@ -548,7 +548,7 @@ impl<'db> StaticClassLiteral<'db> { /// maps each of the class's typevars to `Unknown`. pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { self.apply_specialization(db, |generic_context| { - generic_context.unknown_specialization(db) + generic_context.unknown_specialization(db, self.known(db)) }) } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 55e6ad95e4..e5776e7ff0 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -815,9 +815,18 @@ impl<'db> GenericContext<'db> { self.specialize(db, types) } - pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> Specialization<'db> { - self.specialize( + /// Specializes every type parameter to its unknown form. + /// + /// The built-in `tuple` also needs an explicit variable-length tuple shape so that + /// materialization can preserve its element type. + pub(crate) fn unknown_specialization( + self, + db: &'db dyn Db, + known_class: Option, + ) -> Specialization<'db> { + Specialization::new( db, + self, self.variables(db) .map(|typevar| match typevar.kind(db) { TypeVarKind::LegacyTypeVarTuple | TypeVarKind::Pep695TypeVarTuple => { @@ -828,7 +837,10 @@ impl<'db> GenericContext<'db> { } _ => Type::unknown(), }) - .collect::>(), + .collect::>(), + None, + (known_class == Some(KnownClass::Tuple)) + .then(|| TupleType::homogeneous(db, Type::unknown())), ) } @@ -1035,7 +1047,7 @@ pub struct Specialization<'db> { /// `Bottom[A[Any]]` is a subtype of all materializations of `A[Any]`, and is represented /// with `Some(MaterializationKind::Bottom)`. /// The `materialization_kind` field may be non-`None` only if the specialization contains - /// dynamic types in invariant positions. + /// dynamic types in invariant positions or positions with constrained type variables. #[returns(copy)] pub(crate) materialization_kind: Option, @@ -1416,26 +1428,46 @@ impl<'db> Specialization<'db> { if self.materialization_kind(db).is_some() { return self; } - let mut has_dynamic_invariant_typevar = false; + let mut has_unsimplified_dynamic_typevar = false; let types = self.map_types(db, |_, bound_typevar, vartype| { - match specialization_variance(db, bound_typevar) { + let variance = specialization_variance(db, bound_typevar); + let top_materialization = vartype.materialize(db, MaterializationKind::Top, visitor); + let has_dynamic_type = + !visitor.is_equivalent_to_materialization(db, vartype, top_materialization); + + match variance { TypeVarVariance::Bivariant => { // With bivariance, all specializations are subtypes of each other, // so any materialization is acceptable. - vartype.materialize(db, MaterializationKind::Top, visitor) + top_materialization } - TypeVarVariance::Covariant => { - vartype.materialize(db, materialization_kind, visitor) + TypeVarVariance::Covariant | TypeVarVariance::Contravariant + if has_dynamic_type && bound_typevar.typevar(db).is_constrained(db) => + { + has_unsimplified_dynamic_typevar = true; + vartype } - TypeVarVariance::Contravariant => { - vartype.materialize(db, materialization_kind.flip(), visitor) + TypeVarVariance::Covariant | TypeVarVariance::Contravariant => { + let effective_materialization_kind = if variance.is_covariant() { + materialization_kind + } else { + materialization_kind.flip() + }; + let materialized = + vartype.materialize(db, effective_materialization_kind, visitor); + + if has_dynamic_type + && effective_materialization_kind == MaterializationKind::Top + && let Some(upper_bound) = + bound_typevar.typevar(db).top_materialized_upper_bound(db) + { + IntersectionType::from_two_elements(db, materialized, upper_bound) + } else { + materialized + } } TypeVarVariance::Invariant => { - let top_materialization = - vartype.materialize(db, MaterializationKind::Top, visitor); - if !visitor.is_equivalent_to_materialization(db, vartype, top_materialization) { - has_dynamic_invariant_typevar = true; - } + has_unsimplified_dynamic_typevar |= has_dynamic_type; vartype } } @@ -1450,11 +1482,8 @@ impl<'db> Specialization<'db> { visitor, ) }); - let new_materialization_kind = if has_dynamic_invariant_typevar { - Some(materialization_kind) - } else { - None - }; + let new_materialization_kind = + has_unsimplified_dynamic_typevar.then_some(materialization_kind); // Keep this check in sync with every field that can be transformed above. let specialization_unchanged = matches!(&types, Cow::Borrowed(_)) && tuple_inner == original_tuple_inner @@ -1529,6 +1558,62 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return self.check_tuple_type_pair(db, source_tuple, target_tuple); } + // A gradual specialization is a subtype of a fully static specialization when all its + // valid materializations are subtypes. Materializing the source applies declared bounds + // and constraints before comparing arguments. This establishes `C[Any] <: Top[C[Any]]` + // and lets negative `isinstance` narrowing exclude every specialization of `C`. + // + // This transformation is sound for directional subtyping and non-pure redundancy. + // Assignability and pure redundancy must retain the source's gradual semantics. + if matches!( + self.relation, + TypeRelation::Subtyping + | TypeRelation::SubtypingAssuming + | TypeRelation::Redundancy { pure: false } + ) + // Explicitly materialized sources are already static and cannot advance further. + && source.materialization_kind(db).is_none() + // Performance only: `source_top != source` below already handles unchanged + // arguments. Without expanding aliases, treat them as potentially gradual. + && source.types(db).iter().any(|ty| { + any_over_type(db, *ty, false, |ty| { + ty.is_dynamic() || matches!(ty, Type::TypeAlias(_)) + }) + }) + // Avoid the `self.always()` type-variable shortcut in + // `check_subtyping_in_invariant_position`: it would incorrectly conclude + // that `Top[Inv[Any]] <: Inv[T]` for an unresolved `T`. + // TODO: remove this once that shortcut is removed. + && target + .types(db) + .iter() + .all(|ty| !ty.has_typevar_or_typevar_instance(db)) + // Only non-pure redundancy needs a target already equal to its top. + // Materializing the source otherwise loses the bottom needed to + // simplify `Covariant[Any] | Covariant[Any | str]`. Comparing both + // top and bottom is a possible alternative, but it gets more complex + // due to the need to preserve Divergent markers. Also the fact that we currently + // simplify tuples containing `Never` to `Never` means that for + // `class C[T: tuple[int, int]]`, `C[tuple[Any, int]]` and `C[tuple[int, Any]]` + // have the same top and bottom but expose `Any` in different tuple positions. + // TODO: Try resolving the above issues so we can compare top/bottom subtyping here. + && (!matches!(self.relation, TypeRelation::Redundancy { pure: false }) + || target + == target.materialize_impl( + db, + MaterializationKind::Top, + self.materialization_visitor, + )) + { + let source_top = + source.materialize_impl(db, MaterializationKind::Top, self.materialization_visitor); + // Dynamic arguments can still be unchanged by top materialization; retrying + // the same pair would recurse indefinitely. + if source_top != source { + return self.check_specialization_pair(db, source_top, target); + } + } + let source_materialization_kind = source.materialization_kind(db); let target_materialization_kind = target.materialization_kind(db); @@ -1542,13 +1627,15 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { db, self.constraints, |(bound_typevar, source_type, target_type)| { + let variance = specialization_variance(db, bound_typevar); + // Subtyping/assignability of each type in the specialization depends on the variance // of the corresponding typevar: // - covariant: verify that source_type <: target_type // - contravariant: verify that target_type <: source_type // - invariant: verify that source_type <: target_type AND target_type <: source_type // - bivariant: skip, can't make subtyping/assignability false - match specialization_variance(db, bound_typevar) { + match variance { TypeVarVariance::Invariant => self.check_relation_in_invariant_position( db, *source_type, @@ -1556,11 +1643,43 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { *target_type, target_materialization_kind, ), - TypeVarVariance::Covariant => { - self.check_type_pair(db, *source_type, *target_type) - } - TypeVarVariance::Contravariant => { - self.check_type_pair(db, *target_type, *source_type) + TypeVarVariance::Covariant | TypeVarVariance::Contravariant => { + let ( + source_type, + source_materialization, + target_type, + target_materialization, + ) = if variance.is_covariant() { + ( + *source_type, + source_materialization_kind, + *target_type, + target_materialization_kind, + ) + } else { + ( + *target_type, + target_materialization_kind.map(MaterializationKind::flip), + *source_type, + source_materialization_kind.map(MaterializationKind::flip), + ) + }; + + self.check_type_pair( + db, + self.materialize_constrained_type_argument( + db, + bound_typevar, + source_type, + source_materialization, + ), + self.materialize_constrained_type_argument( + db, + bound_typevar, + target_type, + target_materialization, + ), + ) } TypeVarVariance::Bivariant => self.always(), } @@ -1568,6 +1687,85 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ) } + /// Materializes a constrained covariant or contravariant argument for a relation check. + /// + /// A constrained type variable can only take one of its declared alternatives. For example, + /// replacing `Any` with `int | str` for `class C[T: (int, str)]` would create the invalid + /// specialization `C[int | str]`. The caller preserves the enclosing `Top[C[Any]]`; this + /// helper combines the reachable constraints into `int | str` only for the relation check, + /// without constructing `C[int | str]`. + fn materialize_constrained_type_argument( + &self, + db: &'db dyn Db, + bound_typevar: BoundTypeVarInstance<'db>, + ty: Type<'db>, + materialization: Option, + ) -> Type<'db> { + let Some(materialization) = materialization else { + return ty; + }; + + // A lazy upper bound may refer back to the enclosing specialization. Check whether this + // type variable is constrained before evaluating its bounds or constraints. + let typevar = bound_typevar.typevar(db); + if !typevar.is_constrained(db) { + return ty; + } + + let argument_top = + ty.materialize(db, MaterializationKind::Top, self.materialization_visitor); + if self + .materialization_visitor + .is_equivalent_to_materialization(db, ty, argument_top) + { + return ty; + } + let Some(constraints) = typevar.constraints(db) else { + return ty; + }; + let argument_bottom = ty.materialize( + db, + MaterializationKind::Bottom, + self.materialization_visitor, + ); + + let viable_constraints = constraints.iter().filter_map(|constraint| { + let constraint_top = + constraint.materialize(db, MaterializationKind::Top, self.materialization_visitor); + + // A viable constraint must overlap the argument's upper materialization and contain + // its lower materialization. The upper check matters for `Intersection[int, Any]`, + // and the lower check matters for `Any | int`. + if argument_top.is_disjoint_from(db, constraint_top) + || !argument_bottom.is_subtype_of(db, constraint_top) + { + return None; + } + + Some(match materialization { + MaterializationKind::Top => constraint_top, + MaterializationKind::Bottom => constraint.materialize( + db, + MaterializationKind::Bottom, + self.materialization_visitor, + ), + }) + }); + + match materialization { + MaterializationKind::Top => IntersectionType::from_two_elements( + db, + argument_top, + UnionType::from_elements(db, viable_constraints), + ), + MaterializationKind::Bottom => UnionType::from_two_elements( + db, + argument_bottom, + IntersectionType::from_elements(db, viable_constraints), + ), + } + } + /// Whether two types encountered in an invariant position /// have a relation (subtyping or assignability), taking into account /// that the two types may come from a top or bottom materialization. diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index b4ef76bf2e..70217c4935 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -15,9 +15,9 @@ use crate::{ }, types::{ ApplySpecialization, ApplyTypeMappingVisitor, CycleDetector, DynamicType, GenericContext, - InstanceProjection, KnownClass, KnownInstanceType, MaterializationKind, Parameter, - Parameters, Type, TypeAliasType, TypeContext, TypeMapping, TypeVarVariance, UnionBuilder, - UnionType, any_over_type, binding_type, definition_expression_type, + InstanceProjection, IntersectionType, KnownClass, KnownInstanceType, MaterializationKind, + Parameter, Parameters, Type, TypeAliasType, TypeContext, TypeMapping, TypeVarVariance, + UnionBuilder, UnionType, any_over_type, binding_type, definition_expression_type, tuple::Tuple, variance::VarianceInferable, visitor::{self, TypeCollector, TypeVisitor, walk_type_with_recursion_guard}, @@ -238,6 +238,34 @@ impl<'db> TypeVarInstance<'db> { } } + /// Returns the static upper bound used when materializing a gradual type argument. + /// + /// Constraints are unioned only when materializing an exposed member, where their union is a + /// valid conservative upper bound. A bound may recursively refer to its own generic class, + /// either directly or through other bounds. Such a bound has no finite static top + /// materialization, so recover from its cycle without applying an upper bound. + #[salsa::tracked( + returns(copy), + cycle_result=|_, _, _| None, + heap_size=ruff_memory_usage::heap_size + )] + pub(super) fn top_materialized_upper_bound(self, db: &'db dyn Db) -> Option> { + self.bound_or_constraints(db) + .map(|bound_or_constraints| bound_or_constraints.as_type(db).top_materialization(db)) + } + + /// Returns whether this type variable has constraints without evaluating a lazy bound. + pub(super) fn is_constrained(self, db: &'db dyn Db) -> bool { + matches!( + self._bound_or_constraints(db), + Some( + TypeVarBoundOrConstraintsEvaluation::Eager(TypeVarBoundOrConstraints::Constraints( + _ + )) | TypeVarBoundOrConstraintsEvaluation::LazyConstraints + ) + ) + } + 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)) @@ -1164,8 +1192,23 @@ impl<'db> BoundTypeVarInstance<'db> { } else { // Materialization uses a different mapping mode. Reuse of the outer // visitor can incorrectly hit a cache entry from specialization. - let materialization_visitor = ApplyTypeMappingVisitor::default(); - mapped.materialize(db, *materialization_kind, &materialization_visitor) + let materialization_visitor = visitor.for_new_materialization_root(); + let materialized = + mapped.materialize(db, *materialization_kind, &materialization_visitor); + + if *materialization_kind == MaterializationKind::Top + && !materialization_visitor.is_equivalent_to_materialization( + db, + mapped, + materialized, + ) + && let Some(upper_bound) = + self.typevar(db).top_materialized_upper_bound(db) + { + IntersectionType::from_two_elements(db, materialized, upper_bound) + } else { + materialized + } } }) .unwrap_or(Type::TypeVar(self)), From d91586bd5b30f77f6614d57b73fc7dbea9051a0e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:09:25 -0400 Subject: [PATCH 153/390] Update prek dependencies (#27293) 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/ruff-pre-commit](https://redirect.github.com/astral-sh/ruff-pre-commit) | repository | minor | `v0.15.22` → `v0.16.0` | | [astral-sh/uv-pre-commit](https://redirect.github.com/astral-sh/uv-pre-commit) | repository | minor | `0.11.32` → `0.12.0` | | [rbubley/mirrors-prettier](https://redirect.github.com/rbubley/mirrors-prettier) | repository | patch | `v3.9.5` → `v3.9.6` | | [zizmorcore/zizmor-pre-commit](https://redirect.github.com/zizmorcore/zizmor-pre-commit) | repository | minor | `v1.27.0` → `v1.28.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://redirect.github.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
astral-sh/ruff-pre-commit (astral-sh/ruff-pre-commit) ### [`v0.16.0`](https://redirect.github.com/astral-sh/ruff-pre-commit/releases/tag/v0.16.0) [Compare Source](https://redirect.github.com/astral-sh/ruff-pre-commit/compare/v0.15.22...v0.16.0) See:
astral-sh/uv-pre-commit (astral-sh/uv-pre-commit) ### [`v0.12.0`](https://redirect.github.com/astral-sh/uv-pre-commit/releases/tag/0.12.0) [Compare Source](https://redirect.github.com/astral-sh/uv-pre-commit/compare/0.11.33...0.12.0) See: ### [`v0.11.33`](https://redirect.github.com/astral-sh/uv-pre-commit/compare/0.11.32...0.11.33) [Compare Source](https://redirect.github.com/astral-sh/uv-pre-commit/compare/0.11.32...0.11.33)
rbubley/mirrors-prettier (rbubley/mirrors-prettier) ### [`v3.9.6`](https://redirect.github.com/rbubley/mirrors-prettier/compare/v3.9.5...v3.9.6) [Compare Source](https://redirect.github.com/rbubley/mirrors-prettier/compare/v3.9.5...v3.9.6)
zizmorcore/zizmor-pre-commit (zizmorcore/zizmor-pre-commit) ### [`v1.28.0`](https://redirect.github.com/zizmorcore/zizmor-pre-commit/releases/tag/v1.28.0) [Compare Source](https://redirect.github.com/zizmorcore/zizmor-pre-commit/compare/v1.27.0...v1.28.0) See:
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - "before 4am on Wednesday" - 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. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] 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> Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .pre-commit-config.yaml | 12 ++++++------ scripts/ty_benchmark/src/benchmark/__init__.py | 10 +++++++--- .../ty_benchmark/src/benchmark/lsp_client.py | 18 +++++++++--------- scripts/ty_benchmark/src/benchmark/projects.py | 11 +++++++---- scripts/ty_benchmark/src/benchmark/run.py | 8 ++++---- scripts/ty_benchmark/src/benchmark/snapshot.py | 10 +++++++--- .../src/benchmark/test_lsp_diagnostics.py | 4 ++-- scripts/ty_benchmark/src/benchmark/venv.py | 4 +++- 8 files changed, 45 insertions(+), 32 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55e2acc3cc..ea178f587f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,7 +54,7 @@ repos: priority: 0 # Prettier - repo: https://github.com/rbubley/mirrors-prettier - rev: 9337a74165b178ae2c766f60bee7252a0f06f3e8 # frozen: v3.9.5 + rev: 0ee178619d696787ca73d210cc191d720868c631 # frozen: v3.9.6 hooks: - id: prettier types: [yaml] @@ -63,7 +63,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/zizmorcore/zizmor-pre-commit - rev: 64a97fb7fa63188393d3215c6e312f5f9c6d0f78 # frozen: v1.27.0 + rev: 067260dc5fe6ea86b7551bfd6f8b3ba4e6c93129 # frozen: v1.28.0 hooks: - id: zizmor priority: 0 @@ -113,13 +113,13 @@ repos: priority: 0 - repo: https://github.com/astral-sh/uv-pre-commit - rev: e3e6ef7d9bda544b2e795782dbd7d2a4fbd7eb6d # frozen: 0.11.32 + rev: 5900cba2cfe6d20f562458d4d308ac55569f92eb # frozen: 0.12.0 hooks: - id: uv-lock priority: 0 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 2700fd5671c633760d912769c041bfcde2b9a01b # frozen: v0.15.22 + rev: cb8c523fd4835aba42af70f4cad5568db4df0b6c # frozen: v0.16.0 hooks: - id: ruff-format exclude: crates/ty_python_semantic/resources/corpus/ @@ -127,7 +127,7 @@ repos: # Priority 1: Second-pass fixers (e.g., markdownlint-fix runs after mdformat). - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 2700fd5671c633760d912769c041bfcde2b9a01b # frozen: v0.15.22 + rev: cb8c523fd4835aba42af70f4cad5568db4df0b6c # frozen: v0.16.0 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix] @@ -150,7 +150,7 @@ repos: # Priority 2: ruffen-docs runs after markdownlint-fix (both modify markdown). - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 2700fd5671c633760d912769c041bfcde2b9a01b # frozen: v0.15.22 + rev: cb8c523fd4835aba42af70f4cad5568db4df0b6c # frozen: v0.16.0 hooks: - id: ruff-format name: mdtest format diff --git a/scripts/ty_benchmark/src/benchmark/__init__.py b/scripts/ty_benchmark/src/benchmark/__init__.py index f3dae1e83d..02462a4575 100644 --- a/scripts/ty_benchmark/src/benchmark/__init__.py +++ b/scripts/ty_benchmark/src/benchmark/__init__.py @@ -3,8 +3,9 @@ import logging import subprocess import sys +from collections.abc import Mapping from pathlib import Path -from typing import Mapping, NamedTuple +from typing import NamedTuple if sys.platform == "win32": import mslex as shlex @@ -12,6 +13,9 @@ import shlex +logger = logging.getLogger(__name__) + + class Command(NamedTuple): name: str """The name of the command to benchmark.""" @@ -76,6 +80,6 @@ def run(self, *, cwd: Path | None = None, env: Mapping[str, str]) -> None: for command in self.commands: args.append(shlex.join(command.command)) - logging.info(f"Running {args}") + logger.info(f"Running {args}") - subprocess.run(args, cwd=cwd, env=env) + subprocess.run(args, cwd=cwd, env=env, check=True) diff --git a/scripts/ty_benchmark/src/benchmark/lsp_client.py b/scripts/ty_benchmark/src/benchmark/lsp_client.py index d51f61fa09..9dc202f627 100644 --- a/scripts/ty_benchmark/src/benchmark/lsp_client.py +++ b/scripts/ty_benchmark/src/benchmark/lsp_client.py @@ -9,6 +9,8 @@ from lsprotocol import types as lsp from pygls.lsp.client import LanguageClient +logger = logging.getLogger(__name__) + def _register_notebook_structure_hooks(converter): """Register structure hooks for notebook document types to work around cattrs deserialization issues.""" @@ -64,7 +66,7 @@ def __init__( def publish_diagnostics( client: LSPClient, params: lsp.PublishDiagnosticsParams ): - logging.info( + logger.info( f"Received publish_diagnostics for {params.uri} with version={params.version}, diagnostics count={len(params.diagnostics)}" ) future = self.diagnostics.get(params.uri, None) @@ -78,11 +80,11 @@ def publish_diagnostics( @self.feature(lsp.WINDOW_LOG_MESSAGE) def log_message(client: LSPClient, params: lsp.LogMessageParams): if params.type == lsp.MessageType.Error: - logging.error(f"server error: {params.message}") + logger.error(f"server error: {params.message}") elif params.type == lsp.MessageType.Warning: - logging.warning(f"server warning: {params.message}") + logger.warning(f"server warning: {params.message}") else: - logging.info(f"server info: {params.message}") + logger.info(f"server info: {params.message}") @override async def initialize_async( @@ -92,9 +94,7 @@ async def initialize_async( self.server_capabilities = result.capabilities - logging.info( - f"Pull diagnostic support: {self.server_supports_pull_diagnostics}" - ) + logger.info(f"Pull diagnostic support: {self.server_supports_pull_diagnostics}") return result @@ -154,9 +154,9 @@ async def wait_for_push_diagnostics_async( self.diagnostics[path.as_uri()] = future try: - logging.info(f"Waiting for push diagnostics for {path}") + logger.info(f"Waiting for push diagnostics for {path}") result = await asyncio.wait_for(future, timeout) - logging.info(f"Awaited push diagnostics for {path}") + logger.info(f"Awaited push diagnostics for {path}") finally: self.diagnostics.pop(path.as_uri()) diff --git a/scripts/ty_benchmark/src/benchmark/projects.py b/scripts/ty_benchmark/src/benchmark/projects.py index e8b40db73a..710556d2c7 100644 --- a/scripts/ty_benchmark/src/benchmark/projects.py +++ b/scripts/ty_benchmark/src/benchmark/projects.py @@ -1,9 +1,12 @@ import logging import subprocess import sys +from collections.abc import Sequence from pathlib import Path from typing import Final, Literal, NamedTuple +logger = logging.getLogger(__name__) + class Project(NamedTuple): name: str @@ -26,10 +29,10 @@ class Project(NamedTuple): skip: str | None = None """The project is skipped from benchmarking if not `None`.""" - include: list[str] = [] + include: Sequence[str] = () """The directories and files to check. If empty, checks the current directory""" - exclude: list[str] = [] + exclude: Sequence[str] = () """The directories and files to exclude from checks.""" edit: IncrementalEdit | None = None @@ -39,7 +42,7 @@ def clone(self, checkout_dir: Path) -> None: if (checkout_dir / ".git").exists(): return - logging.debug(f"Cloning {self.repository} to {checkout_dir}") + logger.debug(f"Cloning {self.repository} to {checkout_dir}") try: # git doesn't support cloning a specific revision. @@ -94,7 +97,7 @@ def clone(self, checkout_dir: Path) -> None: except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to clone {self.name}:\n\n{e.stderr}") from e - logging.info(f"Cloned {self.name} to {checkout_dir}.") + logger.info(f"Cloned {self.name} to {checkout_dir}.") class IncrementalEdit(NamedTuple): diff --git a/scripts/ty_benchmark/src/benchmark/run.py b/scripts/ty_benchmark/src/benchmark/run.py index 9b673ae159..e299aeb5eb 100644 --- a/scripts/ty_benchmark/src/benchmark/run.py +++ b/scripts/ty_benchmark/src/benchmark/run.py @@ -87,7 +87,7 @@ def main() -> None: args = parser.parse_args() logging.basicConfig( - level=logging.INFO if args.verbose else logging.WARN, + level=logging.INFO if args.verbose else logging.WARNING, format="%(asctime)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) @@ -165,15 +165,15 @@ def main() -> None: continue if not first: - print("") + print() print( "-------------------------------------------------------------------------------" ) - print("") + print() print(f"{project.name}") print("-" * len(project.name)) - print("") + print() if args.snapshot: # Get the directory where run.py is located to find snapshots directory. diff --git a/scripts/ty_benchmark/src/benchmark/snapshot.py b/scripts/ty_benchmark/src/benchmark/snapshot.py index cb5e5f1c3c..15b8d2a96d 100644 --- a/scripts/ty_benchmark/src/benchmark/snapshot.py +++ b/scripts/ty_benchmark/src/benchmark/snapshot.py @@ -4,11 +4,14 @@ import logging import re import subprocess +from collections.abc import Mapping from pathlib import Path -from typing import Mapping, NamedTuple +from typing import NamedTuple from benchmark import Command +logger = logging.getLogger(__name__) + def normalize_output(output: str, cwd: Path) -> str: """Normalize output by replacing absolute paths with relative placeholders.""" @@ -66,7 +69,7 @@ def run(self, *, cwd: Path, env: Mapping[str, str]): # Run the prepare command if provided. if command.prepare: - logging.info(f"Running prepare: {command.prepare}") + logger.info(f"Running prepare: {command.prepare}") subprocess.run( command.prepare, cwd=cwd, @@ -76,13 +79,14 @@ def run(self, *, cwd: Path, env: Mapping[str, str]): ) # Run the actual command and capture output. - logging.info(f"Running {command.command}") + logger.info(f"Running {command.command}") result = subprocess.run( command.command, cwd=cwd, env=env, capture_output=True, text=True, + check=False, ) # Get the actual output and combine stdout and stderr for the snapshot. diff --git a/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py b/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py index d5d94ffbba..7bde5edc58 100644 --- a/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py +++ b/scripts/ty_benchmark/src/benchmark/test_lsp_diagnostics.py @@ -40,7 +40,7 @@ @pytest.fixture(scope="module", params=ALL_PROJECTS, ids=lambda p: p.name) def project_setup( request, -) -> Generator[tuple[Project, Venv], None, None]: +) -> Generator[tuple[Project, Venv]]: """Set up a project and its venv once per module (shared across all tests for this project).""" project: Project = request.param @@ -187,7 +187,7 @@ def edited_file_path(self) -> Path: def absolute_file_path(self, file_path: str) -> Path: return self.cwd / file_path - def files_to_check(self) -> Generator[Path, None, None]: + def files_to_check(self) -> Generator[Path]: yield self.edited_file_path for file in self.edit.affected_files: diff --git a/scripts/ty_benchmark/src/benchmark/venv.py b/scripts/ty_benchmark/src/benchmark/venv.py index f128d37eb1..a8488c6cca 100644 --- a/scripts/ty_benchmark/src/benchmark/venv.py +++ b/scripts/ty_benchmark/src/benchmark/venv.py @@ -6,6 +6,8 @@ from dataclasses import dataclass from pathlib import Path +logger = logging.getLogger(__name__) + @dataclass(frozen=True, kw_only=True, slots=True) class Venv: @@ -63,7 +65,7 @@ def install( ) -> None: """Installs the dependencies required to type check the project.""" - logging.debug(f"Installing dependencies: {', '.join(pip_install_args)}") + logger.debug(f"Installing dependencies: {', '.join(pip_install_args)}") mypy_overrides = Path(__file__).with_name("mypy-overrides.txt") command = [ From 0d804975a2645d6e4a795cf501fd764b88aa47e3 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:09:34 -0400 Subject: [PATCH 154/390] Lint TOML files in the LSP (#26862) Summary -- This PR adds TOML linting and fixing support to the LSP following https://github.com/astral-sh/ruff/pull/26772. It's organized as 3 initial refactoring commits followed by the two commits actually adding TOML support. After this lands, we'll also need to add TOML support in the VS Code extension, like we did for Markdown files in https://github.com/astral-sh/ruff-vscode/pull/950. Test Plan -- New e2e tests --- crates/ruff_server/src/fix.rs | 100 +++++-- crates/ruff_server/src/lint.rs | 271 ++++++++++++------ .../src/server/api/requests/code_action.rs | 14 +- crates/ruff_server/tests/e2e/code_action.rs | 104 +++++++ crates/ruff_server/tests/e2e/diagnostics.rs | 108 +++++++ 5 files changed, 471 insertions(+), 126 deletions(-) diff --git a/crates/ruff_server/src/fix.rs b/crates/ruff_server/src/fix.rs index 82dd1654cc..04b6bee64e 100644 --- a/crates/ruff_server/src/fix.rs +++ b/crates/ruff_server/src/fix.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use ruff_python_ast::SourceType; +use ruff_python_ast::{SourceType, TomlSourceType}; use rustc_hash::FxHashMap; use crate::{ @@ -14,6 +14,7 @@ use ruff_linter::{ linter::FixerResult, packaging::detect_package_root, settings::{LinterSettings, flags}, + toml::lint_fix_toml, }; use ruff_notebook::SourceValue; use ruff_source_file::LineIndex; @@ -30,11 +31,6 @@ pub(crate) fn fix_all( let settings = query.settings(); let document_path = query.virtual_file_path(); - let SourceType::Python(source_type) = query.source_type_for_lint() else { - return Ok(Fixes::default()); - }; - let source_kind = query.make_python_source_kind(source_type); - // If the document is excluded, return an empty list of fixes. if is_document_excluded_for_linting( &document_path, @@ -45,6 +41,15 @@ pub(crate) fn fix_all( return Ok(Fixes::default()); } + let source_type = match query.source_type_for_lint() { + SourceType::Python(source_type) => source_type, + SourceType::Toml(source_type @ (TomlSourceType::Pyproject | TomlSourceType::Ruff)) => { + return fix_toml(query, linter_settings, source_type, encoding); + } + SourceType::Toml(_) | SourceType::Markdown => return Ok(Fixes::default()), + }; + let source_kind = query.make_python_source_kind(source_type); + let file_path = query.file_path(); let package = if let Some(file_path) = &file_path { detect_package_root( @@ -132,28 +137,69 @@ pub(crate) fn fix_all( } Ok(fixes) } else { - let source_index = LineIndex::from_source_text(source_kind.source_code()); + Ok(text_document_fixes( + query, + source_kind.source_code(), + transformed.source_code(), + encoding, + )) + } +} - let modified = transformed.source_code(); - let modified_index = LineIndex::from_source_text(modified); +fn fix_toml( + query: &DocumentQuery, + linter_settings: &LinterSettings, + source_type: TomlSourceType, + encoding: PositionEncoding, +) -> crate::Result { + let document = query.as_single_document()?; + let transformed = lint_fix_toml( + &query.virtual_file_path(), + document.contents(), + linter_settings, + source_type, + query.settings().unsafe_fixes, + ) + .transformed; - let Replacement { - source_range, - modified_range, - } = Replacement::between( - source_kind.source_code(), - source_index.line_starts(), - modified, - modified_index.line_starts(), - ); - Ok([( - query.make_key().into_uri(), - vec![lsp_types::TextEdit { - range: source_range.to_range(source_kind.source_code(), &source_index, encoding), - new_text: modified[modified_range].to_owned(), - }], - )] - .into_iter() - .collect()) + if let Cow::Borrowed(_) = transformed { + return Ok(Fixes::default()); } + + Ok(text_document_fixes( + query, + document.contents(), + transformed.as_ref(), + encoding, + )) +} + +fn text_document_fixes( + query: &DocumentQuery, + source: &str, + modified: &str, + encoding: PositionEncoding, +) -> Fixes { + let source_index = LineIndex::from_source_text(source); + let modified_index = LineIndex::from_source_text(modified); + + let Replacement { + source_range, + modified_range, + } = Replacement::between( + source, + source_index.line_starts(), + modified, + modified_index.line_starts(), + ); + + [( + query.make_key().into_uri(), + vec![lsp_types::TextEdit { + range: source_range.to_range(source, &source_index, encoding), + new_text: modified[modified_range].to_owned(), + }], + )] + .into_iter() + .collect() } diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index 8579673c5c..0946eb6b83 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -3,13 +3,13 @@ use std::fmt::Write; use std::path::Path; -use ruff_python_ast::SourceType; +use ruff_python_ast::{SourceType, TomlSourceType}; use ruff_workspace::Settings; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use crate::{ - DIAGNOSTIC_NAME, PositionEncoding, + DIAGNOSTIC_NAME, PositionEncoding, TextDocument, edit::{NotebookDocument, NotebookRange, ToRangeExt}, resolve::is_document_excluded_for_linting, session::DocumentQuery, @@ -27,8 +27,9 @@ use ruff_linter::{ settings::flags, source_kind::SourceKind, suppression::Suppressions, + toml::lint_toml, }; -use ruff_notebook::Notebook; +use ruff_notebook::{Notebook, NotebookIndex}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_source_file::LineIndex; @@ -77,13 +78,6 @@ pub(crate) fn check( let settings = query.settings(); let document_path = query.virtual_file_path(); - let SourceType::Python(source_type) = query.source_type_for_lint() else { - return DiagnosticsMap::default(); - }; - let source_kind = query.make_python_source_kind(source_type); - let document_uri = query.make_key().into_uri(); - let notebook = query.as_notebook(); - // If the document is excluded, return an empty list of diagnostics. if is_document_excluded_for_linting( &document_path, @@ -94,6 +88,88 @@ pub(crate) fn check( return DiagnosticsMap::default(); } + let result = match query.source_type_for_lint() { + SourceType::Python(source_type) => check_python(query, source_type), + SourceType::Toml(source_type @ (TomlSourceType::Pyproject | TomlSourceType::Ruff)) => { + let Ok(document) = query.as_single_document() else { + return DiagnosticsMap::default(); + }; + check_toml(query, document, source_type) + } + SourceType::Toml(_) | SourceType::Markdown => return DiagnosticsMap::default(), + }; + + let CheckResult { + diagnostics, + suppression_edits, + document, + } = result; + let document_uri = query.make_key().into_uri(); + let context = LspDiagnosticContext { + source: document.source(), + index: document.index(), + notebook_index: document.notebook_index(), + encoding, + document_path: &document_path, + document_uri: &document_uri, + notebook: query.as_notebook(), + supports_related_information, + settings, + }; + + let mut diagnostics_map = DiagnosticsMap::default(); + + // Populates all relevant URLs with an empty diagnostic list. + // This ensures that documents without diagnostics still get updated. + if let Some(notebook) = query.as_notebook() { + for uri in notebook.uris() { + diagnostics_map.entry(uri.clone()).or_default(); + } + } else { + diagnostics_map + .entry(query.make_key().into_uri()) + .or_default(); + } + + let mut suppression_edits = suppression_edits.into_iter(); + let lsp_diagnostics = diagnostics.into_iter().filter_map(|message| { + let suppression_edit = suppression_edits.next().flatten(); + if message.is_invalid_syntax() && !show_syntax_errors { + None + } else { + Some(to_lsp_diagnostic(&message, suppression_edit, &context)) + } + }); + + if let Some(notebook) = query.as_notebook() { + for (index, diagnostic) in lsp_diagnostics { + let Some(uri) = notebook.cell_uri_by_index(index) else { + tracing::warn!("Unable to find notebook cell at index {index}."); + continue; + }; + diagnostics_map + .entry(uri.clone()) + .or_default() + .push(diagnostic); + } + } else { + diagnostics_map + .entry(query.make_key().into_uri()) + .or_default() + .extend(lsp_diagnostics.map(|(_, diagnostic)| diagnostic)); + } + + diagnostics_map +} + +fn check_python( + query: &DocumentQuery, + source_type: ruff_python_ast::PySourceType, +) -> CheckResult<'_> { + let settings = query.settings(); + let document_path = query.virtual_file_path(); + let source_kind = query.make_python_source_kind(source_type); + let file_path = query.file_path(); let package = if let Some(file_path) = &file_path { detect_package_root( @@ -167,62 +243,45 @@ pub(crate) fn check( }, settings.linter.preview, ); - let context = LspDiagnosticContext { - source_kind: &source_kind, - index: locator.to_index(), - encoding, - document_path: document_path.as_ref(), - document_uri: &document_uri, - notebook, - supports_related_information, - settings, - }; - - let mut diagnostics_map = DiagnosticsMap::default(); - - // Populates all relevant URLs with an empty diagnostic list. - // This ensures that documents without diagnostics still get updated. - if let Some(notebook) = query.as_notebook() { - for uri in notebook.uris() { - diagnostics_map.entry(uri.clone()).or_default(); - } - } else { - diagnostics_map - .entry(query.make_key().into_uri()) - .or_default(); + let index = locator.to_index().clone(); + + CheckResult { + diagnostics, + suppression_edits, + document: CheckedDocument::Python { + source: source_kind, + index, + }, } +} - let lsp_diagnostics = - diagnostics - .into_iter() - .zip(suppression_edits) - .filter_map(|(message, noqa_edit)| { - if message.is_invalid_syntax() && !show_syntax_errors { - None - } else { - Some(to_lsp_diagnostic(&message, noqa_edit, &context)) - } - }); - - if let Some(notebook) = query.as_notebook() { - for (index, diagnostic) in lsp_diagnostics { - let Some(uri) = notebook.cell_uri_by_index(index) else { - tracing::warn!("Unable to find notebook cell at index {index}."); - continue; - }; - diagnostics_map - .entry(uri.clone()) - .or_default() - .push(diagnostic); - } +fn check_toml<'a>( + query: &DocumentQuery, + document: &'a TextDocument, + source_type: TomlSourceType, +) -> CheckResult<'a> { + let settings = query.settings(); + let diagnostics = if settings + .linter + .rules + .iter_enabled() + .any(|rule| rule.lint_source().is_toml()) + { + lint_toml( + &query.virtual_file_path(), + document.contents(), + &settings.linter, + source_type, + ) } else { - diagnostics_map - .entry(query.make_key().into_uri()) - .or_default() - .extend(lsp_diagnostics.map(|(_, diagnostic)| diagnostic)); - } + Vec::new() + }; - diagnostics_map + CheckResult { + diagnostics, + suppression_edits: Vec::new(), + document: CheckedDocument::Toml(document), + } } /// Converts LSP diagnostics to a list of `DiagnosticFix`es by deserializing associated data on each diagnostic. @@ -253,9 +312,47 @@ pub(crate) fn fixes_for_diagnostics( .collect() } +enum CheckedDocument<'a> { + Python { + source: SourceKind, + index: LineIndex, + }, + Toml(&'a TextDocument), +} + +impl CheckedDocument<'_> { + fn source(&self) -> &str { + match self { + Self::Python { source, .. } => source.source_code(), + Self::Toml(document) => document.contents(), + } + } + + fn index(&self) -> &LineIndex { + match self { + Self::Python { index, .. } => index, + Self::Toml(document) => document.index(), + } + } + + fn notebook_index(&self) -> Option<&NotebookIndex> { + match self { + Self::Python { source, .. } => source.as_ipy_notebook().map(Notebook::index), + Self::Toml(_) => None, + } + } +} + +struct CheckResult<'a> { + diagnostics: Vec, + suppression_edits: Vec>, + document: CheckedDocument<'a>, +} + struct LspDiagnosticContext<'a> { - source_kind: &'a SourceKind, + source: &'a str, index: &'a LineIndex, + notebook_index: Option<&'a NotebookIndex>, encoding: PositionEncoding, document_path: &'a Path, document_uri: &'a lsp_types::Uri, @@ -305,22 +402,12 @@ fn to_lsp_diagnostic( .into_iter() .flat_map(Fix::edits) .map(|edit| lsp_types::TextEdit { - range: diagnostic_edit_range( - edit.range(), - context.source_kind, - context.index, - context.encoding, - ), + range: diagnostic_edit_range(edit.range(), context), new_text: edit.content().unwrap_or_default().to_string(), }) .collect(); let noqa_edit = noqa_edit.map(|noqa_edit| lsp_types::TextEdit { - range: diagnostic_edit_range( - noqa_edit.range(), - context.source_kind, - context.index, - context.encoding, - ), + range: diagnostic_edit_range(noqa_edit.range(), context), new_text: noqa_edit.into_content().unwrap_or_default().into_string(), }); serde_json::to_value(AssociatedDiagnosticData { @@ -336,20 +423,16 @@ fn to_lsp_diagnostic( let range: lsp_types::Range; let cell: usize; - if let Some(notebook_index) = context.source_kind.as_ipy_notebook().map(Notebook::index) { + if let Some(notebook_index) = context.notebook_index { NotebookRange { cell, range } = diagnostic_range.to_notebook_range( - context.source_kind.source_code(), + context.source, context.index, notebook_index, context.encoding, ); } else { cell = usize::default(); - range = diagnostic_range.to_range( - context.source_kind.source_code(), - context.index, - context.encoding, - ); + range = diagnostic_range.to_range(context.source, context.index, context.encoding); } let related_information = @@ -456,7 +539,7 @@ fn span_to_location(span: &Span, context: &LspDiagnosticContext) -> Option Option lsp_types::Range { - if let Some(notebook_index) = source_kind.as_ipy_notebook().map(Notebook::index) { +fn diagnostic_edit_range(range: TextRange, context: &LspDiagnosticContext) -> lsp_types::Range { + if let Some(notebook_index) = context.notebook_index { range - .to_notebook_range(source_kind.source_code(), index, notebook_index, encoding) + .to_notebook_range( + context.source, + context.index, + notebook_index, + context.encoding, + ) .range } else { - range.to_range(source_kind.source_code(), index, encoding) + range.to_range(context.source, context.index, context.encoding) } } @@ -521,6 +604,7 @@ fn tags(diagnostic: &Diagnostic) -> Option> { #[cfg(test)] mod tests { use ruff_db::diagnostic::{DiagnosticId, Severity, SubDiagnosticSeverity}; + use ruff_linter::source_kind::SourceKind; use ruff_source_file::SourceFileBuilder; use ruff_text_size::{TextRange, TextSize}; @@ -572,8 +656,9 @@ mod tests { let uri = lsp_types::Uri::parse("file:///test.py").expect("URI to be valid"); let settings = Settings::default(); let context = LspDiagnosticContext { - source_kind: &source_kind, + source: source_kind.source_code(), index: &index, + notebook_index: None, encoding: PositionEncoding::UTF8, document_path: Path::new("test.py"), document_uri: &uri, diff --git a/crates/ruff_server/src/server/api/requests/code_action.rs b/crates/ruff_server/src/server/api/requests/code_action.rs index 5f280b8629..fedf2239bd 100644 --- a/crates/ruff_server/src/server/api/requests/code_action.rs +++ b/crates/ruff_server/src/server/api/requests/code_action.rs @@ -1,6 +1,6 @@ use lsp_server::ErrorCode; use lsp_types::{self as types, CodeActionRequest, CodeActionResponse}; -use ruff_python_ast::SourceType; +use ruff_python_ast::{SourceType, TomlSourceType}; use rustc_hash::FxHashSet; use types::CodeActionKind; @@ -41,9 +41,10 @@ impl super::BackgroundDocumentRequestHandler for CodeActions { let query = snapshot.query(); - // Don't provide code actions for non-Python documents (e.g., markdown files). - let SourceType::Python(_) = query.source_type_for_lint() else { - return Ok(Some(response)); + let is_python = match query.source_type_for_lint() { + SourceType::Python(_) => true, + SourceType::Toml(TomlSourceType::Pyproject | TomlSourceType::Ruff) => false, + SourceType::Toml(_) | SourceType::Markdown => return Ok(Some(response)), }; let document_path = query.virtual_file_path(); @@ -70,7 +71,8 @@ impl super::BackgroundDocumentRequestHandler for CodeActions { .extend(quick_fix(&snapshot, &fixes).with_failure_code(ErrorCode::InternalError)?); } - if snapshot.client_settings().noqa_comments() + if is_python + && snapshot.client_settings().noqa_comments() && supported_code_actions.contains(&SupportedCodeAction::QuickFix) { response.extend(noqa_comments(&snapshot, &fixes)); @@ -93,7 +95,7 @@ impl super::BackgroundDocumentRequestHandler for CodeActions { } } - if snapshot.client_settings().organize_imports() { + if is_python && snapshot.client_settings().organize_imports() { if supported_code_actions.contains(&SupportedCodeAction::SourceOrganizeImports) { if snapshot.is_notebook_cell() { // This is ignore here because the client requests this code action for each diff --git a/crates/ruff_server/tests/e2e/code_action.rs b/crates/ruff_server/tests/e2e/code_action.rs index 68c43ee24a..3952c248c2 100644 --- a/crates/ruff_server/tests/e2e/code_action.rs +++ b/crates/ruff_server/tests/e2e/code_action.rs @@ -81,6 +81,110 @@ fn code_actions_for_python() -> Result<()> { Ok(()) } +#[test] +fn code_actions_for_toml() -> Result<()> { + let source = r#" +[lint] +preview = true +select = ["rule-codes-in-selectors"] +extend-select = ["F401"] +"#; + let mut server = TestServerBuilder::new()? + .with_workspace(".")? + .with_file("ruff.toml", source)? + .build(); + + server.open_text_document_with_language_id("ruff.toml", "toml", source, 1); + + let diagnostics = match server.document_diagnostic_request("ruff.toml", None) { + DocumentDiagnosticReport::RelatedFullDocumentDiagnosticReport(report) => { + report.full_document_diagnostic_report.items + } + DocumentDiagnosticReport::RelatedUnchangedDocumentDiagnosticReport(_) => { + panic!("Expected a full diagnostic report"); + } + }; + let actions = server + .code_action_request("ruff.toml", diagnostics) + .expect("Expected code actions"); + + assert_json_snapshot!(actions, @r#" + [ + { + "title": "Ruff (rule-codes-in-selectors): Replace rule code with `unused-import`", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 4, + "character": 18 + }, + "end": { + "line": 4, + "character": 22 + } + }, + "severity": 2, + "code": "rule-codes-in-selectors", + "codeDescription": { + "href": "https://docs.astral.sh/ruff/rules/rule-codes-in-selectors" + }, + "source": "Ruff", + "message": "Rule code used instead of name in `lint.extend-select`\n\nhelp: Replace rule code with `unused-import`", + "tags": [] + } + ], + "edit": { + "changes": { + "file:///ruff.toml": [ + { + "range": { + "start": { + "line": 4, + "character": 18 + }, + "end": { + "line": 4, + "character": 22 + } + }, + "newText": "unused-import" + } + ] + } + }, + "data": "file:///ruff.toml" + }, + { + "title": "Ruff: Fix all auto-fixable problems", + "kind": "source.fixAll.ruff", + "edit": { + "changes": { + "file:///ruff.toml": [ + { + "range": { + "start": { + "line": 4, + "character": 0 + }, + "end": { + "line": 5, + "character": 0 + } + }, + "newText": "extend-select = [\"unused-import\"]\n" + } + ] + } + } + } + ] + "#); + + Ok(()) +} + #[test] fn human_readable_rule_names() -> Result<()> { let mut server = TestServerBuilder::new()? diff --git a/crates/ruff_server/tests/e2e/diagnostics.rs b/crates/ruff_server/tests/e2e/diagnostics.rs index 4fb75272c8..be6cf48ee6 100644 --- a/crates/ruff_server/tests/e2e/diagnostics.rs +++ b/crates/ruff_server/tests/e2e/diagnostics.rs @@ -81,3 +81,111 @@ fn uses_human_readable_names_in_preview() -> Result<()> { Ok(()) } + +#[test] +fn toml_diagnostics() -> Result<()> { + let source = r#" +[lint] +preview = true +select = ["rule-codes-in-selectors"] +extend-select = ["F401"] +"#; + let mut server = TestServerBuilder::new()? + .with_workspace(".")? + .with_file("ruff.toml", source)? + .build(); + + server.open_text_document_with_language_id("ruff.toml", "toml", source, 1); + + let diagnostics = server.document_diagnostic_request("ruff.toml", None); + + assert_json_snapshot!(diagnostics, @r#" + { + "items": [ + { + "range": { + "start": { + "line": 4, + "character": 18 + }, + "end": { + "line": 4, + "character": 22 + } + }, + "severity": 2, + "code": "rule-codes-in-selectors", + "codeDescription": { + "href": "https://docs.astral.sh/ruff/rules/rule-codes-in-selectors" + }, + "source": "Ruff", + "message": "Rule code used instead of name in `lint.extend-select`\n\nhelp: Replace rule code with `unused-import`", + "tags": [], + "data": { + "code": "rule-codes-in-selectors", + "edits": [ + { + "newText": "unused-import", + "range": { + "end": { + "character": 22, + "line": 4 + }, + "start": { + "character": 18, + "line": 4 + } + } + } + ], + "noqa_edit": null, + "title": "Replace rule code with `unused-import`" + } + } + ], + "kind": "full" + } + "#); + + Ok(()) +} + +#[test] +fn invalid_pyproject_toml_diagnostic() -> Result<()> { + let source = "[project]\nname = 1\n"; + let mut server = TestServerBuilder::new()?.with_workspace(".")?.build(); + + server.open_text_document_with_language_id("pyproject.toml", "toml", source, 1); + + let diagnostics = server.document_diagnostic_request("pyproject.toml", None); + + assert_json_snapshot!(diagnostics, @r#" + { + "items": [ + { + "range": { + "start": { + "line": 1, + "character": 7 + }, + "end": { + "line": 1, + "character": 8 + } + }, + "severity": 2, + "code": "RUF200", + "codeDescription": { + "href": "https://docs.astral.sh/ruff/rules/invalid-pyproject-toml" + }, + "source": "Ruff", + "message": "Failed to parse pyproject.toml: invalid type: integer `1`, expected a string", + "tags": [] + } + ], + "kind": "full" + } + "#); + + Ok(()) +} From f40dca98a7f1b6fff0cee5ecf1e6364d5a8bdc15 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 30 Jul 2026 11:08:54 -0400 Subject: [PATCH 155/390] [ty] Preserve forwarded expanded-variadic diagnostic sources (#27266) ## Summary A callback using `Unpack[Config]` or `Unpack[tuple[...]]` can accept several arguments through a single `**kwargs` or `*args` declaration. Forwarded `ParamSpec` diagnostics currently fall back to the forwarding function because they cannot reliably map those arguments to the callback's source parameter: ```py import asyncio from typing import TypedDict, Unpack class Config(TypedDict): alpha: int beta: int def callback(**options: Unpack[Config]) -> None: ... async def run() -> None: await asyncio.to_thread(callback, alpha=1, beta="incorrect") ``` We now preserve parameter definitions while expanding tuple annotations, select the correct overload after `Concatenate` filtering, and map expanded arguments back to the original `*args` or `**kwargs` declaration. --- .../paramspec_subcall_error_location.md | 146 ++++++++++- .../ty_python_semantic/src/types/call/bind.rs | 147 ++++++----- .../ty_python_semantic/src/types/callable.rs | 5 +- crates/ty_python_semantic/src/types/class.rs | 1 + .../ty_python_semantic/src/types/function.rs | 24 +- .../src/types/signatures.rs | 240 +++++++++++++----- 6 files changed, 408 insertions(+), 155 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md b/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md index d724aa9418..20623b2a97 100644 --- a/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md +++ b/crates/ty_python_semantic/resources/mdtest/paramspec_subcall_error_location.md @@ -531,11 +531,11 @@ info: Function defined here | ^^^^^^^^ ---------- Parameter declared here ``` -## Overloads without parameter definitions +## Overloads with expanded positional parameters -Expanding `Unpack[tuple[int]]` can remove the information linking an overload's parameter back to -its declaration. Until that link is restored, point to the forwarding function's `*args` rather than -an unrelated overload. +Expanding `Unpack[tuple[int]]` must preserve the link to the callback's `*values` declaration. The +diagnostic can then identify the matching overload instead of falling back to the forwarding +function's `*args` parameter. ```py from typing import Callable, Concatenate, Unpack, overload @@ -557,17 +557,17 @@ error[invalid-argument-type]: Argument to function `wrapper` is incorrect 10 | wrapper(callback, "incorrect") # snapshot: invalid-argument-type | ^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` info: Function defined here - --> src/mdtest_snippet.py:3:5 + --> src/mdtest_snippet.py:7:5 | -3 | def wrapper[**P](callback: Callable[Concatenate[int, P], None], *args: P.args, **kwargs: P.kwargs) -> None: ... - | ^^^^^^^ ------------- Parameter declared here +7 | def callback(prefix: int, *values: Unpack[tuple[int]]) -> None: ... + | ^^^^^^^^ --------------------------- Parameter declared here ``` ## Expanded keyword parameters `Unpack[Config]` creates separate keyword parameters for `alpha` and `beta`, even though the -callback declares only `**options`. Without a reliable link to that declaration, point to the -forwarding function's `**kwargs` parameter. +callback declares only `**options`. An error for `beta` should point to that `**options` +declaration. ```py from typing import Callable, TypedDict, Unpack @@ -590,10 +590,95 @@ error[invalid-argument-type]: Argument to function `wrapper` is incorrect 11 | wrapper(callback, alpha=1, beta="incorrect") # snapshot: invalid-argument-type | ^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` info: Function defined here - --> src/mdtest_snippet.py:3:5 + --> src/mdtest_snippet.py:9:5 | -3 | def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... - | ^^^^^^^ ------------------ Parameter declared here +9 | def callback(**options: Unpack[Config]) -> None: ... + | ^^^^^^^^ ------------------------- Parameter declared here +``` + +## Overloads with expanded keyword parameters + +Both callback overloads unpack the same `TypedDict`, so their expanded parameters refer to the same +field declarations. The diagnostic should still identify the overload selected by its `int` prefix. + +```py +from typing import Callable, Concatenate, TypedDict, Unpack, overload + +class Config(TypedDict): + value: int + +def wrapper[**P](callback: Callable[Concatenate[int, P], None], *args: P.args, **kwargs: P.kwargs) -> None: ... +@overload +def callback(prefix: str, **options: Unpack[Config]) -> None: ... +@overload +def callback(prefix: int, **options: Unpack[Config]) -> None: ... +def callback(prefix: str | int, **options: Unpack[Config]) -> None: ... + +wrapper(callback, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:13:19 + | +13 | wrapper(callback, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:10:5 + | +10 | def callback(prefix: int, **options: Unpack[Config]) -> None: ... + | ^^^^^^^^ ------------------------- Parameter declared here +``` + +The same overload identity must survive `functools.partial`, which removes the bound prefix before +forwarding the remaining arguments. + +```py +from functools import partial + +def forward[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + +forward(partial(callback, 1), value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `forward` is incorrect + --> src/mdtest_snippet.py:18:31 + | +18 | forward(partial(callback, 1), value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Function defined here + --> src/mdtest_snippet.py:10:5 + | +10 | def callback(prefix: int, **options: Unpack[Config]) -> None: ... + | ^^^^^^^^ ------------------------- Parameter declared here +``` + +## Expanded positional parameters + +`Unpack[tuple[int, str]]` creates two positional parameters from one `*values` declaration. An error +in the second argument should point to that declaration. + +```py +from typing import Callable, Unpack + +def wrapper[**P](callback: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback(*values: Unpack[tuple[int, str]]) -> None: ... + +wrapper(callback, 1, 2) # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:6:22 + | +6 | wrapper(callback, 1, 2) # snapshot: invalid-argument-type + | ^ Expected `str`, found `Literal[2]` +info: Function defined here + --> src/mdtest_snippet.py:4:5 + | +4 | def callback(*values: Unpack[tuple[int, str]]) -> None: ... + | ^^^^^^^^ -------------------------------- Parameter declared here ``` ## Callback protocols @@ -684,6 +769,43 @@ info: Method defined here | ^^^^^^^^ ---------- Parameter declared here ``` +## Overloaded constructors defined by __init__ + +Synthesized constructor signatures should preserve the overload selected by `Concatenate`, even when +both overloads unpack the same `TypedDict` fields. + +```py +from typing import Callable, Concatenate, TypedDict, Unpack, overload + +class Options(TypedDict): + value: int + +def wrapper[**P, T](callback: Callable[Concatenate[int, P], T], *args: P.args, **kwargs: P.kwargs) -> T: + return callback(1, *args, **kwargs) + +class Factory: + @overload + def __init__(self, prefix: str, **options: Unpack[Options]) -> None: ... + @overload + def __init__(self, prefix: int, **options: Unpack[Options]) -> None: ... + def __init__(self, prefix: str | int, **options: Unpack[Options]) -> None: ... + +wrapper(Factory, value="incorrect") # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper` is incorrect + --> src/mdtest_snippet.py:16:18 + | +16 | wrapper(Factory, value="incorrect") # snapshot: invalid-argument-type + | ^^^^^^^^^^^^^^^^^ Expected `int`, found `Literal["incorrect"]` +info: Method defined here + --> src/mdtest_snippet.py:13:9 + | +13 | def __init__(self, prefix: int, **options: Unpack[Options]) -> None: ... + | ^^^^^^^^ -------------------------- Parameter declared here +``` + ## Constructors defined by a metaclass A custom metaclass can determine the accepted constructor arguments through its own `__call__`. That diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 48eb314fd7..c34a7f018b 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -3128,7 +3128,15 @@ impl<'db> CallableBinding<'db> { signature_type: Type<'db>, overloads: impl IntoIterator>, ) -> Self { - Self::from_indexed_overloads(signature_type, overloads.into_iter().enumerate()) + Self::from_indexed_overloads( + signature_type, + overloads.into_iter().enumerate().map(|(index, signature)| { + ( + signature.source_overload_index().unwrap_or(index), + signature, + ) + }), + ) } /// Constructs a callable binding from overloads while preserving each overload's position in @@ -5242,14 +5250,20 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { Type::KnownInstance( KnownInstanceType::FunctoolsPartial(partial) | KnownInstanceType::FunctoolsPartialCall(partial), - ) => ( - partial.wrapped(self.db).inner(self.db), - partial - .partial(self.db) - .signatures(self.db) - .overloads - .get(overload_index), - ), + ) => { + let signatures = + &partial.partial(self.db).signatures(self.db).overloads; + ( + partial.wrapped(self.db).inner(self.db), + signatures + .iter() + .find(|signature| { + signature.source_overload_index() + == Some(overload_index) + }) + .or_else(|| signatures.get(overload_index)), + ) + } _ => (argument_type, None), }; let argument_bindings = source_type.bindings(self.db); @@ -5261,8 +5275,12 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { }; let source_binding = callable .overloads() - .get(overload_index) + .iter() + .find(|binding| binding.source_overload_index() == overload_index) + .or_else(|| callable.overloads().get(overload_index)) .or_else(|| callable.overloads().first()); + let overload_index = + source_binding.map_or(overload_index, Binding::source_overload_index); let source_parameter_index_offset = source_binding .map_or(0, |binding| binding.source_parameter_index_offset) + usize::from(callable.bound_type.is_some()); @@ -6021,14 +6039,6 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .find(|error| matches!(error, BindingError::InvalidArgumentType { .. })) .and_then(|_| { self.paramspec_parameter_source(paramspec, binding.source_overload_index()) - }) - .and_then(|source| { - let overload_index = - source.source_overload_index(self.db, &binding.signature)?; - Some(ForwardedParameterSource { - overload_index, - ..source - }) }); let argument_matches = self.argument_matches; @@ -6043,7 +6053,9 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { && error_parameter_source.is_none() { if let Some(parameter_source) = parameter_source - && parameter_source.contains_parameter(self.db, parameter.index) + && parameter_source + .source_parameter_index(self.db, parameter) + .is_some() { *error_parameter_source = Some(parameter_source); } else if let Some(parameter_index) = argument_index @@ -6051,7 +6063,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .and_then(|(index, _)| argument_matches[*index].parameters.first()) .map(|parameter| parameter.index) { - parameter.index = parameter_index; + parameter.signature_parameter_index = parameter_index; } } @@ -7484,7 +7496,12 @@ impl std::fmt::Display for CallableDescription<'_> { #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct ParameterContext { name: Option>, - index: usize, + + /// Position in the current, possibly specialized or expanded signature. + signature_parameter_index: usize, + + /// Position of the original source declaration, before any parameter expansion. + source_parameter_index: Option, /// Was the argument for this parameter passed positionally, and matched to a non-variadic /// positional parameter? (If so, we will provide the index in the diagnostic, not just the @@ -7498,7 +7515,8 @@ impl ParameterContext { name: parameter .display_name() .map(ParameterDisplayName::into_owned), - index, + signature_parameter_index: index, + source_parameter_index: parameter.source_parameter_index(), positional, } } @@ -7508,12 +7526,12 @@ impl std::fmt::Display for ParameterContext { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(name) = &self.name { if self.positional { - write!(f, "{} (`{name}`)", self.index + 1) + write!(f, "{} (`{name}`)", self.signature_parameter_index + 1) } else { write!(f, "`{name}`") } } else { - write!(f, "{}", self.index + 1) + write!(f, "{}", self.signature_parameter_index + 1) } } } @@ -7545,54 +7563,49 @@ pub(crate) struct ForwardedParameterSource<'db> { } impl<'db> ForwardedParameterSource<'db> { - /// Recovers an overload's original index after specialization filters earlier declarations. + /// Locates the source parameter that accepted a forwarded argument. /// - /// `overload_index` initially refers to the specialized overload list. This method finds the - /// corresponding position in the original function, which can differ after filtering. - fn source_overload_index(self, db: &'db dyn Db, signature: &Signature<'db>) -> Option { - let parameter_definition = signature - .parameters() - .iter() - .find_map(Parameter::definition)?; - + /// An unpacked variadic annotation can expand one source declaration into several callable + /// parameters. Map each expanded parameter back to its shared `*args` or `**kwargs` + /// declaration instead of interpreting its position as a source parameter index. Looking up + /// the original position through the cached signature also avoids making the caller depend on + /// the callback's entire AST. + /// + /// ```python + /// from typing import Unpack + /// + /// def callback(*args: Unpack[tuple[int, str]]) -> None: ... + /// ``` + fn source_parameter_index( + self, + db: &'db dyn Db, + parameter: &ParameterContext, + ) -> Option { + let parameter_index = parameter + .source_parameter_index + .unwrap_or(parameter.signature_parameter_index + self.parameter_index_offset); self.function .signature(db) .overloads + .get(self.overload_index)? + .parameters() .iter() - .position(|source_signature| { - source_signature - .parameters() - .iter() - .any(|parameter| parameter.definition() == Some(parameter_definition)) + .any(|source_parameter| { + source_parameter.source_parameter_index() == Some(parameter_index) }) + .then_some(parameter_index) } - /// Returns whether the forwarded parameter maps to a specific source parameter. - /// - /// Out-of-range indices otherwise resolve to the entire signature, which would produce a - /// misleading diagnostic annotation. - fn contains_parameter(self, db: &'db dyn Db, parameter_index: usize) -> bool { - let parameter_index = parameter_index + self.parameter_index_offset; + /// Locates the matched source overload after restoring omitted receiver and prefix parameters. + fn parameter_span(self, db: &'db dyn Db, parameter: &ParameterContext) -> (Span, Span) { + let parameter_index = self + .source_parameter_index(db, parameter) + .unwrap_or(parameter.signature_parameter_index + self.parameter_index_offset); let (overloads, implementation) = self.function.overloads_and_implementation(db); - let Some(overload) = overloads + overloads .get(self.overload_index) .copied() .or(implementation) - else { - return false; - }; - - let (_, parameter_span) = overload.parameter_span(db, Some(parameter_index)); - let (_, all_parameters_span) = overload.parameter_span(db, None); - parameter_span != all_parameters_span - } - - /// Locates the matched source overload after restoring omitted receiver and prefix parameters. - fn parameter_span(self, db: &'db dyn Db, parameter_index: usize) -> (Span, Span) { - let parameter_index = parameter_index + self.parameter_index_offset; - let (overloads, _) = self.function.overloads_and_implementation(db); - overloads - .get(self.overload_index) .map(|overload| overload.parameter_span(db, Some(parameter_index))) .unwrap_or_else(|| self.function.parameter_span(db, Some(parameter_index))) } @@ -7951,7 +7964,7 @@ impl<'db> BindingError<'db> { if let Some(parameter_source) = parameter_source { let (name_span, parameter_span) = - parameter_source.parameter_span(context.db(), parameter.index); + parameter_source.parameter_span(context.db(), parameter); let callable_kind = if parameter_source.is_bound_method { "Method" } else { @@ -7993,9 +8006,9 @@ impl<'db> BindingError<'db> { candidate.is_keyword_variadic() } }) - .unwrap_or(parameter.index) + .unwrap_or(parameter.signature_parameter_index) } else { - parameter.index + parameter.signature_parameter_index }; let (name_span, parameter_span) = overload_literal.parameter_span( context.db(), @@ -8034,7 +8047,7 @@ impl<'db> BindingError<'db> { } else if parameter_source.is_none() && let Some((name_span, parameter_span)) = callable_ty.parameter_span( context.db(), - Some(parameter.index + source_parameter_index_offset), + Some(parameter.signature_parameter_index + source_parameter_index_offset), ) { let mut sub = SubDiagnostic::new( @@ -8135,8 +8148,10 @@ impl<'db> BindingError<'db> { } else { let span = callable_ty.parameter_span( context.db(), - (parameters.0.len() == 1) - .then(|| parameters.0[0].index + source_parameter_index_offset), + (parameters.0.len() == 1).then(|| { + parameters.0[0].signature_parameter_index + + source_parameter_index_offset + }), ); if let Some((_, parameter_span)) = span { let mut sub = SubDiagnostic::new( diff --git a/crates/ty_python_semantic/src/types/callable.rs b/crates/ty_python_semantic/src/types/callable.rs index d21261a71a..91f92dcf19 100644 --- a/crates/ty_python_semantic/src/types/callable.rs +++ b/crates/ty_python_semantic/src/types/callable.rs @@ -742,7 +742,10 @@ impl<'db> CallableTypes<'db> { for callable in self.0 { for signature in callable.signatures(db) { let signature = signature.clone(); - let dedup_key = signature.clone().with_definition(None); + let dedup_key = signature + .clone() + .with_definition(None) + .with_source_overload_index(None); if seen_overloads.insert(dedup_key) { overloads.push(signature); } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 13a1c5d900..be8ea4acdc 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2205,6 +2205,7 @@ impl<'db> ClassType<'db> { return_type, ) .with_definition(signature.definition()) + .with_source_overload_index(signature.source_overload_index()) .bind_self_with_receiver( db, Some(instance_type), diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index ab6bffe67e..b6d9e6f32e 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -909,14 +909,22 @@ impl<'db> FunctionLiteral<'db> { return CallableSignature::single(implementation.signature(db)); } - CallableSignature::from_overloads(overloads.iter().flat_map(|overload| { - // The last overload may still be inferred, so querying its binding would create a cycle. - if *overload == self.last_definition { - Either::Left(std::iter::once(overload.signature(db))) - } else { - Either::Right(overload.decorated_signatures(db)) - } - })) + CallableSignature::from_overloads(overloads.iter().enumerate().flat_map( + |(source_overload_index, overload)| { + // The last overload may still be inferred, so querying its binding would create a cycle. + if *overload == self.last_definition { + Either::Left(std::iter::once( + overload + .signature(db) + .with_source_overload_index(Some(source_overload_index)), + )) + } else { + Either::Right(overload.decorated_signatures(db).map(move |signature| { + signature.with_source_overload_index(Some(source_overload_index)) + })) + } + }, + )) } /// Typed externally-visible signature of the last overload or implementation of this function. diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index fcd6f8ea4e..49a532c00d 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -11,6 +11,7 @@ //! arguments must match _at least one_ overload. use std::fmt; +use std::num::NonZeroU32; use std::slice::Iter; use std::sync::Arc; @@ -242,7 +243,10 @@ impl<'db> CallableSignature<'db> { overload.inference, overload.unspecialized_return_ty, ); - let dedup_key = signature.clone().with_definition(None); + let dedup_key = signature + .clone() + .with_definition(None) + .with_source_overload_index(None); if seen_overloads.insert(dedup_key) { new_overloads.push(signature); } @@ -324,6 +328,7 @@ impl<'db> CallableSignature<'db> { type_mapping.update_signature_generic_context(db, context) }), definition: self_signature.definition, + source_overload_index: self_signature.source_overload_index, receiver_constraints: self_signature.map_receiver_constraints( db, type_mapping, @@ -352,6 +357,7 @@ impl<'db> CallableSignature<'db> { }), ), definition: signature.definition, + source_overload_index: signature.source_overload_index, receiver_constraints: { let mapped = self_signature.map_receiver_constraints( db, @@ -553,6 +559,12 @@ pub struct Signature<'db> { /// This is useful for locating and extracting docstring information for the signature. pub(crate) definition: Option>, + /// Position of this overload in the original function definition. + /// + /// Filtering, receiver binding, and partial application can leave a signature at a different + /// position in the active overload list. Preserve its source position for call diagnostics. + source_overload_index: Option, + /// The constraint introduced by binding an explicitly annotated receiver, if any. receiver_constraints: Option>, @@ -693,6 +705,7 @@ impl<'db> Signature<'db> { Self { generic_context: None, definition: None, + source_overload_index: None, receiver_constraints: None, parameters, return_ty, @@ -707,6 +720,7 @@ impl<'db> Signature<'db> { Self { generic_context, definition: None, + source_overload_index: None, receiver_constraints: None, parameters, return_ty, @@ -718,6 +732,7 @@ impl<'db> Signature<'db> { Signature { generic_context: None, definition: None, + source_overload_index: None, receiver_constraints: None, parameters: Parameters::gradual_form(), return_ty: signature_type, @@ -771,6 +786,7 @@ impl<'db> Signature<'db> { Self { generic_context, definition: Some(definition), + source_overload_index: None, receiver_constraints: None, parameters, return_ty, @@ -844,6 +860,7 @@ impl<'db> Signature<'db> { Self { generic_context: self.generic_context, definition: self.definition, + source_overload_index: self.source_overload_index, receiver_constraints: self.receiver_constraints.clone(), parameters, return_ty, @@ -874,6 +891,7 @@ impl<'db> Signature<'db> { Some(Self { generic_context: self.generic_context, definition: self.definition, + source_overload_index: self.source_overload_index, receiver_constraints: self.receiver_constraints.clone(), parameters, return_ty, @@ -892,6 +910,7 @@ impl<'db> Signature<'db> { .generic_context .map(|context| type_mapping.update_signature_generic_context(db, context)), definition: self.definition, + source_overload_index: self.source_overload_index, receiver_constraints: self.map_receiver_constraints(db, type_mapping, tcx, visitor), parameters: self .parameters @@ -1111,6 +1130,7 @@ impl<'db> Signature<'db> { .generic_context .map(|generic_context| generic_context.remove_self(db, binding_context)), definition: self.definition, + source_overload_index: self.source_overload_index, receiver_constraints, parameters, return_ty, @@ -1361,6 +1381,7 @@ impl<'db> Signature<'db> { Self { generic_context: self.generic_context, definition: self.definition, + source_overload_index: self.source_overload_index, receiver_constraints, parameters, return_ty, @@ -1735,6 +1756,21 @@ impl<'db> Signature<'db> { Self { definition, ..self } } + /// Records this signature's position in its defining function's overload list. + pub(crate) fn with_source_overload_index(mut self, index: Option) -> Self { + self.source_overload_index = index + .and_then(|index| u32::try_from(index).ok()) + .and_then(|index| index.checked_add(1)) + .and_then(NonZeroU32::new); + self + } + + /// Returns this signature's position in its defining function's overload list. + pub(crate) fn source_overload_index(&self) -> Option { + self.source_overload_index + .map(|index| index.get() as usize - 1) + } + /// Create a new signature with the given parameters. pub(crate) fn with_parameters(self, parameters: Parameters<'db>) -> Self { Self { parameters, ..self } @@ -1927,6 +1963,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { signature.parameters().clone(), Type::unknown(), ) + .with_source_overload_index(signature.source_overload_index()) }, )), CallableTypeKind::ParamSpecValue, @@ -1979,6 +2016,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { signature.parameters().clone(), Type::unknown(), ) + .with_source_overload_index(signature.source_overload_index()) }), ), CallableTypeKind::ParamSpecValue, @@ -2549,17 +2587,20 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { if let Some(source_param) = source_params.next() { let lower = Type::Callable(CallableType::new( db, - CallableSignature::single(Signature::new_generic( - source.generic_context, - Parameters::concatenate( - db, - std::iter::once(source_param.clone()) - .chain(source_params.cloned()) - .collect(), - ConcatenateTail::ParamSpec(source_bound_typevar), - ), - Type::unknown(), - )), + CallableSignature::single( + Signature::new_generic( + source.generic_context, + Parameters::concatenate( + db, + std::iter::once(source_param.clone()) + .chain(source_params.cloned()) + .collect(), + ConcatenateTail::ParamSpec(source_bound_typevar), + ), + Type::unknown(), + ) + .with_source_overload_index(source.source_overload_index()), + ), CallableTypeKind::ParamSpecValue, CallableFunctionProvenance::None, )); @@ -2574,17 +2615,20 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } else if let Some(target_param) = target_params.next() { let upper = Type::Callable(CallableType::new( db, - CallableSignature::single(Signature::new_generic( - target.generic_context, - Parameters::concatenate( - db, - std::iter::once(target_param.clone()) - .chain(target_params.cloned()) - .collect(), - ConcatenateTail::ParamSpec(target_bound_typevar), - ), - Type::unknown(), - )), + CallableSignature::single( + Signature::new_generic( + target.generic_context, + Parameters::concatenate( + db, + std::iter::once(target_param.clone()) + .chain(target_params.cloned()) + .collect(), + ConcatenateTail::ParamSpec(target_bound_typevar), + ), + Type::unknown(), + ) + .with_source_overload_index(target.source_overload_index()), + ), CallableTypeKind::ParamSpecValue, CallableFunctionProvenance::None, )); @@ -2615,11 +2659,14 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (None, Some(([], target_bound_typevar))) => { let lower = Type::Callable(CallableType::new( db, - CallableSignature::single(Signature::new_generic( - source.generic_context, - source.parameters.clone(), - Type::unknown(), - )), + CallableSignature::single( + Signature::new_generic( + source.generic_context, + source.parameters.clone(), + Type::unknown(), + ) + .with_source_overload_index(source.source_overload_index()), + ), CallableTypeKind::ParamSpecValue, CallableFunctionProvenance::None, )); @@ -2761,11 +2808,14 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .with_transformed_parameters(source_params.cloned()); let lower = Type::Callable(CallableType::new( db, - CallableSignature::single(Signature::new_generic( - source.generic_context, - source_params, - Type::unknown(), - )), + CallableSignature::single( + Signature::new_generic( + source.generic_context, + source_params, + Type::unknown(), + ) + .with_source_overload_index(source.source_overload_index()), + ), CallableTypeKind::ParamSpecValue, CallableFunctionProvenance::None, )); @@ -2785,11 +2835,14 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (Some(([], source_bound_typevar)), None) => { let upper = Type::Callable(CallableType::new( db, - CallableSignature::single(Signature::new_generic( - target.generic_context, - target.parameters.clone(), - Type::unknown(), - )), + CallableSignature::single( + Signature::new_generic( + target.generic_context, + target.parameters.clone(), + Type::unknown(), + ) + .with_source_overload_index(target.source_overload_index()), + ), CallableTypeKind::ParamSpecValue, CallableFunctionProvenance::None, )); @@ -2899,11 +2952,14 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .with_transformed_parameters(target_params.cloned()); let upper = Type::Callable(CallableType::new( db, - CallableSignature::single(Signature::new_generic( - target.generic_context, - target_params, - Type::unknown(), - )), + CallableSignature::single( + Signature::new_generic( + target.generic_context, + target_params, + Type::unknown(), + ) + .with_source_overload_index(target.source_overload_index()), + ), CallableTypeKind::ParamSpecValue, CallableFunctionProvenance::None, )); @@ -3846,14 +3902,16 @@ impl<'db> Parameters<'db> { Parameter::keyword_only(name.clone()) .with_annotated_type(field.declared_ty) .with_optional_default_type((!field.is_required()).then_some(Type::unknown())) - .with_definition(field.first_declaration()), + .with_definition(field.first_declaration()) + .with_source_parameter_index(parameter.source_parameter_index()), ); } if let Some(extra_items) = unpacked_typed_dict.openness(db).effective_extra_items() { value.push( Parameter::keyword_variadic(kwargs_name) - .with_annotated_type(extra_items.declared_ty), + .with_annotated_type(extra_items.declared_ty) + .with_source_parameter_index(parameter.source_parameter_index()), ); } } @@ -4349,7 +4407,9 @@ impl<'db> Parameters<'db> { .chain(positional_or_keyword) .chain(variadic) .chain(keyword_only) - .chain(keywords), + .chain(keywords) + .enumerate() + .map(|(index, parameter)| parameter.with_source_parameter_index(Some(index))), ) } @@ -4458,6 +4518,15 @@ impl<'db> Parameters<'db> { } /// Expands an unpacked `*args` annotation into its logical callable parameters. + /// + /// Preserve the original `*args` definition and source position on every expanded parameter + /// so diagnostics can identify its declaration after specialization or overload filtering. + /// + /// ```python + /// from typing import Unpack + /// + /// def callback(*args: Unpack[tuple[int, str]]) -> None: ... + /// ``` fn expand_starred_variadic_annotations(&self, db: &'db dyn Db) -> Self { if !self .data @@ -4476,37 +4545,36 @@ impl<'db> Parameters<'db> { && let Some(tuple) = parameter.annotated_type().exact_tuple_instance_spec(db) { expanded = true; + let positional_parameter = |ty| { + Parameter::positional_only(None) + .with_annotated_type(ty) + .with_definition(parameter.definition()) + .with_source_parameter_index(parameter.source_parameter_index()) + }; match tuple.as_ref() { Tuple::Fixed(tuple) => { - parameters.extend( - tuple - .iter_all_elements() - .map(|ty| Parameter::positional_only(None).with_annotated_type(ty)), - ); + parameters.extend(tuple.iter_all_elements().map(positional_parameter)); } Tuple::Variable(variable) => { - parameters.extend( - variable - .iter_prefix_elements() - .map(|ty| Parameter::positional_only(None).with_annotated_type(ty)), - ); + parameters + .extend(variable.iter_prefix_elements().map(positional_parameter)); let name = parameter .name() .cloned() .unwrap_or_else(|| Name::new_static("args")); - parameters.push(Parameter::variadic(name).with_annotated_type( - match variable.variable() { - VariableSegment::Homogeneous(element) => element, - VariableSegment::TypeVarTuple(typevartuple) => { - Type::TypeVar(typevartuple) - } - }, - )); - parameters.extend( - variable - .iter_suffix_elements() - .map(|ty| Parameter::positional_only(None).with_annotated_type(ty)), + parameters.push( + Parameter::variadic(name) + .with_annotated_type(match variable.variable() { + VariableSegment::Homogeneous(element) => element, + VariableSegment::TypeVarTuple(typevartuple) => { + Type::TypeVar(typevartuple) + } + }) + .with_definition(parameter.definition()) + .with_source_parameter_index(parameter.source_parameter_index()), ); + parameters + .extend(variable.iter_suffix_elements().map(positional_parameter)); } } } else { @@ -4664,6 +4732,13 @@ pub(crate) struct Parameter<'db> { /// Syntax-level annotation kind for cases where the annotation has special parameter semantics. annotation_kind: ParameterAnnotationKind, + /// Position of the source parameter that owns this logical parameter. + /// + /// Expanded tuple and `TypedDict` parameters retain the position of their original `*args` or + /// `**kwargs` declaration. Store positions one-based so `None` does not increase the size of + /// this struct. + source_parameter_index: Option, + kind: ParameterKind<'db>, } @@ -4691,6 +4766,7 @@ impl<'db> Parameter<'db> { definition: None, inferred_annotation: true, annotation_kind: ParameterAnnotationKind::Normal, + source_parameter_index: None, kind: ParameterKind::PositionalOnly { name, default_type: None, @@ -4704,6 +4780,7 @@ impl<'db> Parameter<'db> { definition: None, inferred_annotation: true, annotation_kind: ParameterAnnotationKind::Normal, + source_parameter_index: None, kind: ParameterKind::PositionalOrKeyword { name, default_type: None, @@ -4717,6 +4794,7 @@ impl<'db> Parameter<'db> { definition: None, inferred_annotation: true, annotation_kind: ParameterAnnotationKind::Normal, + source_parameter_index: None, kind: ParameterKind::Variadic { name }, } } @@ -4727,6 +4805,7 @@ impl<'db> Parameter<'db> { definition: None, inferred_annotation: true, annotation_kind: ParameterAnnotationKind::Normal, + source_parameter_index: None, kind: ParameterKind::KeywordOnly { name, default_type: None, @@ -4740,6 +4819,7 @@ impl<'db> Parameter<'db> { definition: None, inferred_annotation: true, annotation_kind: ParameterAnnotationKind::Normal, + source_parameter_index: None, kind: ParameterKind::KeywordVariadic { name }, } } @@ -4783,6 +4863,24 @@ impl<'db> Parameter<'db> { self } + /// Records the source position without replacing a synthesized parameter's IDE definition. + /// + /// A `TypedDict` field can then keep its declaration for navigation while diagnostics refer + /// to the enclosing `**kwargs` parameter. + fn with_source_parameter_index(mut self, index: Option) -> Self { + self.source_parameter_index = index + .and_then(|index| u32::try_from(index).ok()) + .and_then(|index| index.checked_add(1)) + .and_then(NonZeroU32::new); + self + } + + /// Returns the original source parameter's position before variadic expansion. + pub(crate) fn source_parameter_index(&self) -> Option { + self.source_parameter_index + .map(|index| index.get() as usize - 1) + } + fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, @@ -4803,6 +4901,7 @@ impl<'db> Parameter<'db> { .apply_type_mapping_impl(db, type_mapping, tcx, visitor), inferred_annotation: self.inferred_annotation, annotation_kind: self.annotation_kind, + source_parameter_index: self.source_parameter_index, } } @@ -4818,6 +4917,7 @@ impl<'db> Parameter<'db> { definition: self.definition, inferred_annotation: self.inferred_annotation, annotation_kind: self.annotation_kind, + source_parameter_index: self.source_parameter_index, kind, } } @@ -4833,6 +4933,7 @@ impl<'db> Parameter<'db> { definition, annotation_kind, inferred_annotation, + source_parameter_index, kind, } = self; @@ -4893,6 +4994,7 @@ impl<'db> Parameter<'db> { definition: *definition, inferred_annotation: *inferred_annotation, annotation_kind: *annotation_kind, + source_parameter_index: *source_parameter_index, kind, }) } @@ -4938,6 +5040,7 @@ impl<'db> Parameter<'db> { definition, inferred_annotation, annotation_kind, + source_parameter_index: None, kind, } } @@ -5249,6 +5352,7 @@ mod tests { definition: _, annotation_kind, inferred_annotation, + source_parameter_index: _, kind, } = parameter; From 63830f3e97b56ca3be0dd8f1092f76c4acc63213 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 30 Jul 2026 13:45:26 -0400 Subject: [PATCH 156/390] [ty] Borrow from constraint set storage less often (#27328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a mechanical change that updates the _internal_ constraint set APIs to take in `ConstraintSetStorage`, instead of the `RefCell`-carrying `ConstraintSetBuilder`. That means we only have to `borrow` or `borrow_mut` at the public API boundary, instead of multiple times throughout the internals. I'm not sure if this will have a huge performance impact, but that's not the goal — rather it's to simplify the internal APIs. --- .../src/types/constraints.rs | 2274 +++++++++-------- 1 file changed, 1177 insertions(+), 1097 deletions(-) diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 723347363a..2a3c657e9b 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -86,7 +86,7 @@ //! //! [duboc]: https://gldubc.github.io/#thesis -use std::cell::{Cell, Ref, RefCell}; +use std::cell::{Cell, RefCell}; use std::cmp::Ordering; use std::collections::VecDeque; use std::convert::Infallible; @@ -417,8 +417,9 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { lower: Option>, upper: Option>, ) -> Self { + let mut storage = builder.storage.borrow_mut(); let (node, source_order) = - Constraint::new_node_with_bounds(db, builder, typevar, lower, upper); + Constraint::new_node_with_bounds(db, &mut storage, typevar, lower, upper); Self::from_node(builder, node, source_order) } @@ -450,8 +451,9 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// Returns whether this constraint set never holds. pub(crate) fn is_never_satisfied(self, db: &'db dyn Db) -> bool { + let mut storage = self.builder.storage.borrow_mut(); self.node - .is_never_satisfied(db, self.builder, self.source_order) + .is_never_satisfied(db, &mut storage, self.source_order) } /// Returns whether this constraint set is the `never` terminal. @@ -465,8 +467,9 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// Returns whether this constraint set always holds. pub(crate) fn is_always_satisfied(self, db: &'db dyn Db) -> bool { + let mut storage = self.builder.storage.borrow_mut(); self.node - .is_always_satisfied(db, self.builder, self.source_order) + .is_always_satisfied(db, &mut storage, self.source_order) } /// Returns whether this constraint set is the `always` terminal. @@ -489,8 +492,9 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { rhs: Type<'db>, ) -> Self { self.verify_builder(builder); - let (node, extra_source_order) = self.node.implies_subtype_of(db, builder, lhs, rhs); - let source_order = builder.ordered_source_order(self.source_order, extra_source_order); + let mut storage = builder.storage.borrow_mut(); + let (node, extra_source_order) = self.node.implies_subtype_of(db, &mut storage, lhs, rhs); + let source_order = storage.ordered_source_order(self.source_order, extra_source_order); Self::from_node(builder, node, source_order) } @@ -515,8 +519,9 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { inferable: TypeVarSet<'db>, ) -> bool { self.verify_builder(builder); + let mut storage = builder.storage.borrow_mut(); self.node - .satisfied_by_all_typevars(db, builder, inferable, self.source_order) + .satisfied_by_all_typevars(db, &mut storage, inferable, self.source_order) } /// Updates this constraint set to hold the union of itself and another constraint set. @@ -529,8 +534,9 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: Self, ) -> Self { self.verify_builder(builder); - self.node = self.node.or(builder, other.node); - self.source_order = builder.ordered_source_order(self.source_order, other.source_order); + let mut storage = builder.storage.borrow_mut(); + self.node = self.node.or(&mut storage, other.node); + self.source_order = storage.ordered_source_order(self.source_order, other.source_order); *self } @@ -544,15 +550,17 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: Self, ) -> Self { self.verify_builder(builder); - self.node = self.node.and(builder, other.node); - self.source_order = builder.ordered_source_order(self.source_order, other.source_order); + let mut storage = builder.storage.borrow_mut(); + self.node = self.node.and(&mut storage, other.node); + self.source_order = storage.ordered_source_order(self.source_order, other.source_order); *self } /// Returns the negation of this constraint set. pub(crate) fn negate(self, _db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>) -> Self { self.verify_builder(builder); - Self::from_node(builder, self.node.negate(builder), self.source_order) + let mut storage = builder.storage.borrow_mut(); + Self::from_node(builder, self.node.negate(&mut storage), self.source_order) } /// Returns the intersection of this constraint set and another. The other constraint set is @@ -618,8 +626,9 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { other: Self, ) -> Self { self.verify_builder(builder); - let node = self.node.iff(builder, other.node); - let source_order = builder.ordered_source_order(self.source_order, other.source_order); + let mut storage = builder.storage.borrow_mut(); + let node = self.node.iff(&mut storage, other.node); + let source_order = storage.ordered_source_order(self.source_order, other.source_order); Self::from_node(builder, node, source_order) } @@ -634,9 +643,11 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { to_remove: TypeVarSet<'db>, ) -> Self { self.verify_builder(builder); + let mut storage = builder.storage.borrow_mut(); let (node, derived_source_order) = - self.node.exists(db, builder, to_remove, self.source_order); - let source_order = builder.ordered_source_order(self.source_order, derived_source_order); + self.node + .exists(db, &mut storage, to_remove, self.source_order); + let source_order = storage.ordered_source_order(self.source_order, derived_source_order); Self::from_node(builder, node, source_order) } @@ -649,7 +660,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { fn rebuild_node( - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, old_node: NodeId, mapped_constraints: &FxHashMap)>, mapped_nodes: &mut FxHashMap, @@ -661,102 +672,107 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { return *mapped; } - let old_interior = builder.interior_node_data(old_node); + let old_interior = storage.interior_node_data(old_node); let (condition, _) = mapped_constraints[&old_interior.constraint]; let if_true = rebuild_node( - builder, + storage, old_interior.if_true, mapped_constraints, mapped_nodes, ); let if_uncertain = rebuild_node( - builder, + storage, old_interior.if_uncertain, mapped_constraints, mapped_nodes, ); let if_false = rebuild_node( - builder, + storage, old_interior.if_false, mapped_constraints, mapped_nodes, ); - let mapped = condition.ite_uncertain(builder, if_true, if_uncertain, if_false); + let mapped = condition.ite_uncertain(storage, if_true, if_uncertain, if_false); mapped_nodes.insert(old_node, mapped); mapped } - let builder = self.builder; - let mut mapped_constraints = FxHashMap::default(); + // We have to collect this into a temporary vec since we can't hold an open borrow on the + // storage during the apply_type_mapping calls below, since they also need to borrow the + // storage. + let storage = self.builder.storage.borrow(); + let mut constraints = SmallVec::<[_; 8]>::new(); self.node - .for_each_unique_constraint(builder, &mut |constraint_id| { - if mapped_constraints.contains_key(&constraint_id) { - return; - } + .for_each_unique_constraint(&storage, &mut |constraint_id| { + let constraint = storage.constraint_data(constraint_id); + constraints.push((constraint_id, constraint)); + }); + drop(storage); - let constraint = builder.constraint_data(constraint_id); - let subject = Type::TypeVar(constraint.typevar).apply_type_mapping_impl( - db, - type_mapping, - tcx, - visitor, - ); - let lower = constraint - .bounds - .lower - .map(|lower| lower.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); - let upper = constraint - .bounds - .upper - .map(|upper| upper.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + let mut mapped_constraints = FxHashMap::default(); + for (constraint_id, constraint) in constraints { + if mapped_constraints.contains_key(&constraint_id) { + continue; + } - let mapped = if let Type::TypeVar(typevar) = subject { - Constraint::new_node_with_bounds(db, builder, typevar, lower, upper) - } else { - let lower_holds = lower.map_or_else( - || ConstraintSet::always(builder), - |lower| { - builder.load( - db, - &lower.when_constraint_set_assignable_to_owned(db, subject), - ) - }, - ); - let upper_holds = upper.map_or_else( - || ConstraintSet::always(builder), - |upper| { - builder.load( - db, - &subject.when_constraint_set_assignable_to_owned(db, upper), - ) - }, - ); - ( - lower_holds.node.and(builder, upper_holds.node), - builder.ordered_source_order( - lower_holds.source_order, - upper_holds.source_order, - ), - ) + let subject = Type::TypeVar(constraint.typevar).apply_type_mapping_impl( + db, + type_mapping, + tcx, + visitor, + ); + let lower = constraint + .bounds + .lower + .map(|lower| lower.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + let upper = constraint + .bounds + .upper + .map(|upper| upper.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + + let mut storage = self.builder.storage.borrow_mut(); + let mapped = if let Type::TypeVar(typevar) = subject { + Constraint::new_node_with_bounds(db, &mut storage, typevar, lower, upper) + } else { + let (lower_holds, lower_holds_source_order) = match lower { + Some(lower) => storage.load( + db, + &lower.when_constraint_set_assignable_to_owned(db, subject), + ), + None => (ALWAYS_TRUE, None), }; - mapped_constraints.insert(constraint_id, mapped); - }); + let (upper_holds, upper_holds_source_order) = match upper { + Some(upper) => storage.load( + db, + &subject.when_constraint_set_assignable_to_owned(db, upper), + ), + None => (ALWAYS_TRUE, None), + }; + ( + lower_holds.and(&mut storage, upper_holds), + storage + .ordered_source_order(lower_holds_source_order, upper_holds_source_order), + ) + }; + mapped_constraints.insert(constraint_id, mapped); + } - let source_order = builder + let mut storage = self.builder.storage.borrow_mut(); + let source_order = storage .calculate_source_orders(self.source_order) .into_iter() .fold(None, |source_order, constraint| { mapped_constraints.get(&constraint).map_or( source_order, |(_, mapped_source_order)| { - builder.ordered_source_order(source_order, *mapped_source_order) + storage.ordered_source_order(source_order, *mapped_source_order) }, ) }); Self::from_node( - builder, + self.builder, rebuild_node( - builder, + &mut storage, self.node, &mapped_constraints, &mut FxHashMap::default(), @@ -822,14 +838,33 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { choose: impl FnMut(TypeVarVariance, &PathBound<'db>) -> Result>, ()>, ) -> Solutions<'db> { self.verify_builder(builder); - self.node - .solutions_with(db, builder, inferable, self.source_order, choose) + let mut storage = builder.storage.borrow_mut(); + let path_bounds = + PathBounds::compute(db, &mut storage, self.node, inferable, self.source_order); + drop(storage); + path_bounds.solve_with(choose) } pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { - self.node - .simplify_for_display(db, self.builder) - .display(db, self.builder) + struct DisplayConstraintSet<'c, 'db> { + node: NodeId, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + } + + impl Display for DisplayConstraintSet<'_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut storage = self.builder.storage.borrow_mut(); + let node = self.node.simplify_for_display(self.db, &mut storage); + Display::fmt(&node.display(self.db, &mut storage), f) + } + } + + DisplayConstraintSet { + node: self.node, + db, + builder: self.builder, + } } #[expect(dead_code)] // Keep this around for debugging purposes @@ -837,12 +872,28 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { self, db: &'db dyn Db, prefix: &'a dyn Display, - ) -> impl Display + 'a - where - 'db: 'a, - 'c: 'a, - { - self.node.display_graph(db, self.builder, prefix) + ) -> impl Display { + struct DisplayConstraintSet<'a, 'c, 'db> { + node: NodeId, + prefix: &'a dyn Display, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + } + + impl Display for DisplayConstraintSet<'_, '_, '_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut storage = self.builder.storage.borrow_mut(); + let node = self.node.simplify_for_display(self.db, &mut storage); + Display::fmt(&node.display_graph(self.db, &storage, self.prefix), f) + } + } + + DisplayConstraintSet { + node: self.node, + prefix, + db, + builder: self.builder, + } } } @@ -1047,9 +1098,9 @@ impl<'db> ConstraintSetBuilder<'db> { // sidecar densely so redundant combinations cannot affect its IDs or owned-set equality. // Unlike node and constraint IDs, source-order IDs are not embedded in the BDD, so the // sidecar can be rebuilt without remapping the BDD. - let source_constraints = self.calculate_source_orders(Some(source_order)); - let mut storage = self.storage.into_inner(); + let source_constraints = storage.calculate_source_orders(Some(source_order)); + let mut used_nodes = RankBitBox::bits_with_capacity(storage.nodes.len()); let mut used_constraints = RankBitBox::bits_with_capacity(storage.constraints.len()); @@ -1131,109 +1182,30 @@ impl<'db> ConstraintSetBuilder<'db> { db: &'db dyn Db, other: &OwnedConstraintSet<'db>, ) -> ConstraintSet<'db, 'c> { - fn rebuild_node<'db>( - builder: &ConstraintSetBuilder<'db>, - inner: &OwnedConstraintSetInner<'db>, - constraints: &[(NodeId, Option)], - cache: &mut FxHashMap, - old_node: NodeId, - ) -> NodeId { - if old_node.is_terminal() { - return old_node; - } - if let Some(remapped) = cache.get(&old_node) { - return *remapped; - } - - let old_node_index = inner.retained_node_index(old_node); - let old_interior = inner.nodes[old_node_index]; - let if_true = rebuild_node(builder, inner, constraints, cache, old_interior.if_true); - let if_uncertain = rebuild_node( - builder, - inner, - constraints, - cache, - old_interior.if_uncertain, - ); - let if_false = rebuild_node(builder, inner, constraints, cache, old_interior.if_false); - let old_constraint_index = inner.retained_constraint_index(old_interior.constraint); - let (condition, _) = constraints[old_constraint_index]; - let remapped = condition.ite_uncertain(builder, if_true, if_uncertain, if_false); - - cache.insert(old_node, remapped); - remapped - } - - if other.node.is_terminal() { - return ConstraintSet::from_node(self, other.node, None); - } - let inner = other - .inner - .as_ref() - .expect("storage-free owned constraint sets must have terminal roots"); - - // Load all of the constraints into the this builder first, to maximize the chance that the - // constraints and typevars will appear in the same order. (This is important because many - // of our mdtests try to force a particular ordering, to test that our algorithms are all - // order-independent.) - let constraints: Box<[_]> = inner - .constraints - .iter() - .map(|old_constraint| { - Constraint::new_node_with_bounds( - db, - self, - old_constraint.typevar, - old_constraint.bounds.lower, - old_constraint.bounds.upper, - ) - }) - .collect(); - - let mut source_orders = vec![None; inner.source_orders.len()]; - for (i, old_source_order) in inner.source_orders.iter().copied().enumerate() { - match old_source_order { - SourceOrder::Ordered(old_left, old_right) => { - let new_left = source_orders[old_left.index()]; - let new_right = source_orders[old_right.index()]; - source_orders[i] = self.ordered_source_order(new_left, new_right); - } - SourceOrder::Constraint(old_constraint) => { - let old_constraint_index = inner.retained_constraint_index(old_constraint); - let (_, constraint_source_order) = constraints[old_constraint_index]; - source_orders[i] = constraint_source_order; - } - } - } - - // Maps NodeIds in the OwnedConstraintSet to the corresponding NodeIds in this builder. - let mut cache = FxHashMap::default(); - let node = rebuild_node(self, inner, &constraints, &mut cache, other.node); - let old_source_order = other - .source_order - .expect("non-terminal constraint set should have a source_order"); - let source_order = source_orders[old_source_order.index()]; + let mut storage = self.storage.borrow_mut(); + let (node, source_order) = storage.load(db, other); ConstraintSet::from_node(self, node, source_order) } +} +impl<'db> ConstraintSetStorage<'db> { /// Interns a single typevar, giving it a stable order in this builder - fn intern_typevar(&self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarId { + fn intern_typevar(&mut self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarId { let identity = typevar.identity(db); - let mut storage = self.storage.borrow_mut(); - storage.ensure_overlay_identity_caches(); - if let Some(id) = storage.typevar_cache.get(&identity) { + self.ensure_overlay_identity_caches(); + if let Some(id) = self.typevar_cache.get(&identity) { return *id; } - let id = storage.typevars.push(identity); - let id = storage.adjusted_typevar_id(id); - storage.typevar_cache.insert(identity, id); + let id = self.typevars.push(identity); + let id = self.adjusted_typevar_id(id); + self.typevar_cache.insert(identity, id); id } /// Interns all of the typevars mentioned in a type in a stable order. - fn intern_mentioned_typevars_in_type(&self, db: &'db dyn Db, ty: Type<'db>) { + fn intern_mentioned_typevars_in_type(&mut self, db: &'db dyn Db, ty: Type<'db>) { struct InternMentionedTypevars<'a, 'db> { - builder: &'a ConstraintSetBuilder<'db>, + storage: RefCell<&'a mut ConstraintSetStorage<'db>>, recursion_guard: TypeCollector<'db>, } @@ -1247,7 +1219,9 @@ impl<'db> ConstraintSetBuilder<'db> { db: &'db dyn Db, bound_typevar: BoundTypeVarInstance<'db>, ) { - self.builder.intern_typevar(db, bound_typevar); + let mut storage = self.storage.borrow_mut(); + storage.intern_typevar(db, bound_typevar); + drop(storage); walk_bound_type_var_type(db, bound_typevar, self); } @@ -1263,7 +1237,7 @@ impl<'db> ConstraintSetBuilder<'db> { } InternMentionedTypevars { - builder: self, + storage: RefCell::new(self), recursion_guard: TypeCollector::default(), } .visit_type(db, ty); @@ -1271,7 +1245,7 @@ impl<'db> ConstraintSetBuilder<'db> { /// Interns all of the typevars mentioned in a constraint in a stable order. fn intern_constraint_typevars( - &self, + &mut self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>, bounds: ConstraintBounds<'db>, @@ -1285,76 +1259,63 @@ impl<'db> ConstraintSetBuilder<'db> { } } - fn intern_constraint(&self, db: &'db dyn Db, data: Constraint<'db>) -> ConstraintId { + fn intern_constraint(&mut self, db: &'db dyn Db, data: Constraint<'db>) -> ConstraintId { self.intern_constraint_typevars(db, data.typevar, data.bounds); - let mut storage = self.storage.borrow_mut(); - storage.ensure_overlay_identity_caches(); - if let Some(id) = storage.constraint_cache.get(&data) { + self.ensure_overlay_identity_caches(); + if let Some(id) = self.constraint_cache.get(&data) { return *id; } - let id = storage.constraints.push(data); - let id = storage.adjusted_constraint_id(id); - storage.constraint_cache.insert(data, id); + let id = self.constraints.push(data); + let id = self.adjusted_constraint_id(id); + self.constraint_cache.insert(data, id); id } - fn intern_interior_node(&self, data: InteriorNodeData) -> NodeId { - let mut storage = self.storage.borrow_mut(); - storage.ensure_overlay_identity_caches(); - if let Some(id) = storage.node_cache.get(&data) { + fn intern_interior_node(&mut self, data: InteriorNodeData) -> NodeId { + self.ensure_overlay_identity_caches(); + if let Some(id) = self.node_cache.get(&data) { return *id; } - let id = storage.nodes.push(data); - let id = storage.adjusted_node_id(id); - storage.node_cache.insert(data, id); + let id = self.nodes.push(data); + let id = self.adjusted_node_id(id); + self.node_cache.insert(data, id); id } - fn typevar_id(&self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarId { + fn typevar_id(&mut self, db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> TypeVarId { let identity = typevar.identity(db); - let mut storage = self.storage.borrow_mut(); - storage.ensure_overlay_identity_caches(); - storage - .typevar_cache + self.ensure_overlay_identity_caches(); + self.typevar_cache .get(&identity) .copied() .expect("typevar should be interned before ordering") } fn constraint_data(&self, constraint: ConstraintId) -> Constraint<'db> { - let storage = self.storage.borrow(); - if let Some(compacted) = &storage.compacted { + if let Some(compacted) = &self.compacted { let index = constraint.index(); let split = compacted.constraint_indices.len(); if index < split { let compacted_index = compacted.retained_constraint_index(constraint); return compacted.constraints[compacted_index]; } - return storage.constraints[ConstraintId::from_usize(index - split)]; + return self.constraints[ConstraintId::from_usize(index - split)]; } - storage.constraints[constraint] + self.constraints[constraint] } fn cached_constraint_bound_depth( - &self, + &mut self, db: &'db dyn Db, constraint: ConstraintId, ) -> (u16, u16) { - if let Some(depth) = self - .storage - .borrow() - .constraint_bound_depth_cache - .get(&constraint) - { + if let Some(depth) = self.constraint_bound_depth_cache.get(&constraint) { return *depth; } let depth = self.constraint_data(constraint).bound_depth(db); - self.storage - .borrow_mut() - .constraint_bound_depth_cache - .insert(constraint, depth); + self.constraint_bound_depth_cache.insert(constraint, depth); depth } @@ -1372,7 +1333,7 @@ impl<'db> ConstraintSetBuilder<'db> { /// antecedents and its consequent. (Measuring growth rather than absolute depth avoids /// penalizing a complex concrete bound that is merely propagated unchanged.) fn sequent_fuel_cost( - &self, + &mut self, db: &'db dyn Db, constraint: ConstraintId, antecedent_constructor_depth: u16, @@ -1383,73 +1344,65 @@ impl<'db> ConstraintSetBuilder<'db> { } fn cached_constraint_implies( - &self, + &mut self, db: &'db dyn Db, ante: ConstraintId, post: ConstraintId, ) -> bool { let key = (ante, post); - if let Some(result) = self.storage.borrow().constraint_implication_cache.get(&key) { + if let Some(result) = self.constraint_implication_cache.get(&key) { return *result; } let result = ante.implies(db, self, post); - self.storage - .borrow_mut() - .constraint_implication_cache - .insert(key, result); + self.constraint_implication_cache.insert(key, result); result } fn cached_is_constraint_set_subtype_of( - &self, + &mut self, db: &'db dyn Db, source: Type<'db>, target: Type<'db>, ) -> bool { let key = (source, target); - if let Some(result) = self.storage.borrow().constraint_set_subtype_cache.get(&key) { + if let Some(result) = self.constraint_set_subtype_cache.get(&key) { return *result; } let result = source.is_constraint_set_subtype_of(db, target); - self.storage - .borrow_mut() - .constraint_set_subtype_cache - .insert(key, result); + self.constraint_set_subtype_cache.insert(key, result); result } fn interior_node_data(&self, node: NodeId) -> InteriorNodeData { - let storage = self.storage.borrow(); - if let Some(compacted) = &storage.compacted { + if let Some(compacted) = &self.compacted { let index = node.index(); let split = compacted.node_indices.len(); if index < split { let compacted_index = compacted.retained_node_index(node); return compacted.nodes[compacted_index]; } - return storage.nodes[NodeId::from_usize(index - split)]; + return self.nodes[NodeId::from_usize(index - split)]; } - storage.nodes[node] + self.nodes[node] } - fn intern_source_order(&self, data: SourceOrder) -> SourceOrderId { - let mut storage = self.storage.borrow_mut(); - storage.ensure_overlay_identity_caches(); - if let Some(id) = storage.source_order_cache.get(&data) { + fn intern_source_order(&mut self, data: SourceOrder) -> SourceOrderId { + self.ensure_overlay_identity_caches(); + if let Some(id) = self.source_order_cache.get(&data) { return *id; } - let id = storage.source_orders.push(data); - let id = storage.adjusted_source_order_id(id); - storage.source_order_cache.insert(data, id); + let id = self.source_orders.push(data); + let id = self.adjusted_source_order_id(id); + self.source_order_cache.insert(data, id); id } /// Repeating a source-order tree cannot change the first occurrence of any constraint, so /// combining identical trees must reuse their existing sidecar. fn ordered_source_order( - &self, + &mut self, left: Option, right: Option, ) -> Option { @@ -1463,21 +1416,20 @@ impl<'db> ConstraintSetBuilder<'db> { } } - fn constraint_source_order(&self, constraint: ConstraintId) -> SourceOrderId { + fn constraint_source_order(&mut self, constraint: ConstraintId) -> SourceOrderId { self.intern_source_order(SourceOrder::Constraint(constraint)) } fn source_order_data(&self, source_order: SourceOrderId) -> SourceOrder { - let storage = self.storage.borrow(); - if let Some(compacted) = &storage.compacted { + if let Some(compacted) = &self.compacted { let index = source_order.index(); let split = compacted.source_orders.len(); if index < split { return compacted.source_orders[index]; } - return storage.source_orders[SourceOrderId::from_usize(index - split)]; + return self.source_orders[SourceOrderId::from_usize(index - split)]; } - storage.source_orders[source_order] + self.source_orders[source_order] } fn calculate_source_orders( @@ -1485,14 +1437,14 @@ impl<'db> ConstraintSetBuilder<'db> { source_order: Option, ) -> FxIndexSet { fn walk( - builder: &ConstraintSetBuilder, + storage: &ConstraintSetStorage, current: SourceOrderId, result: &mut FxIndexSet, ) { - match builder.source_order_data(current) { + match storage.source_order_data(current) { SourceOrder::Ordered(left, right) => { - walk(builder, left, result); - walk(builder, right, result); + walk(storage, left, result); + walk(storage, right, result); } SourceOrder::Constraint(constraint) => { result.insert(constraint); @@ -1506,6 +1458,97 @@ impl<'db> ConstraintSetBuilder<'db> { } result } + + /// Loads an [`OwnedConstraintSet`] into this storage. + fn load( + &mut self, + db: &'db dyn Db, + other: &OwnedConstraintSet<'db>, + ) -> (NodeId, Option) { + fn rebuild_node<'db>( + storage: &mut ConstraintSetStorage<'db>, + inner: &OwnedConstraintSetInner<'db>, + constraints: &[(NodeId, Option)], + cache: &mut FxHashMap, + old_node: NodeId, + ) -> NodeId { + if old_node.is_terminal() { + return old_node; + } + if let Some(remapped) = cache.get(&old_node) { + return *remapped; + } + + let old_node_index = inner.retained_node_index(old_node); + let old_interior = inner.nodes[old_node_index]; + let if_true = rebuild_node(storage, inner, constraints, cache, old_interior.if_true); + let if_uncertain = rebuild_node( + storage, + inner, + constraints, + cache, + old_interior.if_uncertain, + ); + let if_false = rebuild_node(storage, inner, constraints, cache, old_interior.if_false); + let old_constraint_index = inner.retained_constraint_index(old_interior.constraint); + let (condition, _) = constraints[old_constraint_index]; + let remapped = condition.ite_uncertain(storage, if_true, if_uncertain, if_false); + + cache.insert(old_node, remapped); + remapped + } + + if other.node.is_terminal() { + return (other.node, None); + } + let inner = other + .inner + .as_ref() + .expect("storage-free owned constraint sets must have terminal roots"); + + // Load all of the constraints into the this storage first, to maximize the chance that the + // constraints and typevars will appear in the same order. (This is important because many + // of our mdtests try to force a particular ordering, to test that our algorithms are all + // order-independent.) + let constraints: Box<[_]> = inner + .constraints + .iter() + .map(|old_constraint| { + Constraint::new_node_with_bounds( + db, + self, + old_constraint.typevar, + old_constraint.bounds.lower, + old_constraint.bounds.upper, + ) + }) + .collect(); + + let mut source_orders = vec![None; inner.source_orders.len()]; + for (i, old_source_order) in inner.source_orders.iter().copied().enumerate() { + match old_source_order { + SourceOrder::Ordered(old_left, old_right) => { + let new_left = source_orders[old_left.index()]; + let new_right = source_orders[old_right.index()]; + source_orders[i] = self.ordered_source_order(new_left, new_right); + } + SourceOrder::Constraint(old_constraint) => { + let old_constraint_index = inner.retained_constraint_index(old_constraint); + let (_, constraint_source_order) = constraints[old_constraint_index]; + source_orders[i] = constraint_source_order; + } + } + } + + // Maps NodeIds in the OwnedConstraintSet to the corresponding NodeIds in this builder. + let mut cache = FxHashMap::default(); + let node = rebuild_node(self, inner, &constraints, &mut cache, other.node); + let old_source_order = other + .source_order + .expect("non-terminal constraint set should have a source_order"); + let source_order = source_orders[old_source_order.index()]; + (node, source_order) + } } impl<'db> BoundTypeVarInstance<'db> { @@ -1521,11 +1564,11 @@ impl<'db> BoundTypeVarInstance<'db> { fn can_be_bound_for( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, typevar: Self, ) -> bool { - wobble_index(builder.typevar_id(db, self).index()) - < wobble_index(builder.typevar_id(db, typevar).index()) + wobble_index(storage.typevar_id(db, self).index()) + < wobble_index(storage.typevar_id(db, typevar).index()) } } @@ -1748,38 +1791,46 @@ impl<'db> UpperBound<'db> { } /// Returns the constraints under which `lower` is assignable to every stored upper clause. - fn when_satisfied_by<'c>( + fn when_satisfied_by( &self, db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, lower: Type<'db>, - ) -> ConstraintSet<'db, 'c> { - self.clauses.iter().when_all(db, builder, |clause| { + ) -> (NodeId, Option) { + let mut node = ALWAYS_TRUE; + let mut source_order = None; + for clause in &self.clauses { let when_clause = lower.when_constraint_set_assignable_to_owned(db, *clause); - builder.load(db, &when_clause) - }) + let (clause_node, clause_source_order) = storage.load(db, &when_clause); + node = node.and(storage, clause_node); + source_order = storage.ordered_source_order(source_order, clause_source_order); + if node == ALWAYS_FALSE { + break; + } + } + (node, source_order) } } impl ConstraintId { fn new<'db>( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, typevar: BoundTypeVarInstance<'db>, lower: Type<'db>, upper: Type<'db>, ) -> ConstraintId { - Self::new_with_bounds(db, builder, typevar, Some(lower), Some(upper)) + Self::new_with_bounds(db, storage, typevar, Some(lower), Some(upper)) } fn new_with_bounds<'db>( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, typevar: BoundTypeVarInstance<'db>, lower: Option>, upper: Option>, ) -> ConstraintId { - builder.intern_constraint( + storage.intern_constraint( db, Constraint { typevar, @@ -1889,7 +1940,7 @@ impl<'db> Constraint<'db> { /// Panics if present `lower` and `upper` bounds are not fully static. fn new_node_with_bounds( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, typevar: BoundTypeVarInstance<'db>, mut lower: Option>, mut upper: Option>, @@ -1912,13 +1963,13 @@ impl<'db> Constraint<'db> { for lower_element in lower_union.elements(db) { let (element_node, element_source_order) = Constraint::new_node_with_bounds( db, - builder, + storage, typevar, Some(*lower_element), upper, ); - result = result.and(builder, element_node); - source_order = builder.ordered_source_order(source_order, element_source_order); + result = result.and(storage, element_node); + source_order = storage.ordered_source_order(source_order, element_source_order); } return (result, source_order); } @@ -1933,24 +1984,24 @@ impl<'db> Constraint<'db> { for upper_element in upper_intersection.iter_positive(db) { let (element_node, element_source_order) = Constraint::new_node_with_bounds( db, - builder, + storage, typevar, lower, Some(upper_element), ); - result = result.and(builder, element_node); - source_order = builder.ordered_source_order(source_order, element_source_order); + result = result.and(storage, element_node); + source_order = storage.ordered_source_order(source_order, element_source_order); } for upper_element in upper_intersection.iter_negative(db) { let (element_node, element_source_order) = Constraint::new_node_with_bounds( db, - builder, + storage, typevar, lower, Some(upper_element.negate(db)), ); - result = result.and(builder, element_node); - source_order = builder.ordered_source_order(source_order, element_source_order); + result = result.and(storage, element_node); + source_order = storage.ordered_source_order(source_order, element_source_order); } return (result, source_order); } @@ -1980,9 +2031,9 @@ impl<'db> Constraint<'db> { }) => { let constraint = - ConstraintId::new(db, builder, typevar, Type::Never, Type::object()); - let (node, source_order) = Node::new_constraint(builder, constraint); - let node = node.negate(builder); + ConstraintId::new(db, storage, typevar, Type::Never, Type::object()); + let (node, source_order) = Node::new_constraint(storage, constraint); + let node = node.negate(storage); return (node, source_order); } _ => {} @@ -2005,7 +2056,7 @@ impl<'db> Constraint<'db> { _ => {} } - builder.intern_constraint_typevars(db, typevar, ConstraintBounds::new(lower, upper)); + storage.intern_constraint_typevars(db, typevar, ConstraintBounds::new(lower, upper)); // If `lower ≰ upper` for every possible assignment of typevars, then the constraint cannot // be satisfied, since there is no type that is both greater than `lower`, and less than @@ -2015,7 +2066,7 @@ impl<'db> Constraint<'db> { let effective_lower = lower.unwrap_or(Type::Never); let effective_upper = upper.unwrap_or(Type::object()); let when = effective_lower.when_constraint_set_assignable_to_owned(db, effective_upper); - let is_never_satisfied = when.query(|_builder, when| when.is_never_satisfied(db)); + let is_never_satisfied = when.query(|_storage, when| when.is_never_satisfied(db)); if is_never_satisfied { return (ALWAYS_FALSE, None); } @@ -2029,97 +2080,97 @@ impl<'db> Constraint<'db> { match (effective_lower, effective_upper) { // L ≤ T ≤ L == (T ≤ [L] ≤ T) (Type::TypeVar(lower), Type::TypeVar(upper)) if lower.is_same_typevar_as(db, upper) => { - let (bound, typevar) = if lower.can_be_bound_for(db, builder, typevar) { + let (bound, typevar) = if lower.can_be_bound_for(db, storage, typevar) { (lower, typevar) } else { (typevar, lower) }; let constraint = ConstraintId::new( db, - builder, + storage, typevar, Type::TypeVar(bound), Type::TypeVar(bound), ); - Node::new_constraint(builder, constraint) + Node::new_constraint(storage, constraint) } // L ≤ T ≤ U == ([L] ≤ T) && (T ≤ [U]) (Type::TypeVar(lower), Type::TypeVar(upper)) - if typevar.can_be_bound_for(db, builder, lower) - && typevar.can_be_bound_for(db, builder, upper) => + if typevar.can_be_bound_for(db, storage, lower) + && typevar.can_be_bound_for(db, storage, upper) => { let lower_constraint = ConstraintId::new_with_bounds( db, - builder, + storage, lower, None, Some(Type::TypeVar(typevar)), ); let (lower_node, lower_source_order) = - Node::new_constraint(builder, lower_constraint); + Node::new_constraint(storage, lower_constraint); let upper_constraint = ConstraintId::new_with_bounds( db, - builder, + storage, upper, Some(Type::TypeVar(typevar)), None, ); let (upper_node, upper_source_order) = - Node::new_constraint(builder, upper_constraint); - let node = lower_node.and(builder, upper_node); + Node::new_constraint(storage, upper_constraint); + let node = lower_node.and(storage, upper_node); let source_order = - builder.ordered_source_order(lower_source_order, upper_source_order); + storage.ordered_source_order(lower_source_order, upper_source_order); (node, source_order) } // L ≤ T ≤ U == ([L] ≤ T) && ([T] ≤ U) - (Type::TypeVar(lower), _) if typevar.can_be_bound_for(db, builder, lower) => { + (Type::TypeVar(lower), _) if typevar.can_be_bound_for(db, storage, lower) => { let lower_constraint = ConstraintId::new_with_bounds( db, - builder, + storage, lower, None, Some(Type::TypeVar(typevar)), ); let (lower_node, lower_source_order) = - Node::new_constraint(builder, lower_constraint); + Node::new_constraint(storage, lower_constraint); let (upper_node, upper_source_order) = if upper.is_none() { (ALWAYS_TRUE, None) } else { - Constraint::new_node_with_bounds(db, builder, typevar, None, upper) + Constraint::new_node_with_bounds(db, storage, typevar, None, upper) }; - let node = lower_node.and(builder, upper_node); + let node = lower_node.and(storage, upper_node); let source_order = - builder.ordered_source_order(lower_source_order, upper_source_order); + storage.ordered_source_order(lower_source_order, upper_source_order); (node, source_order) } // L ≤ T ≤ U == (L ≤ [T]) && (T ≤ [U]) - (_, Type::TypeVar(upper)) if typevar.can_be_bound_for(db, builder, upper) => { + (_, Type::TypeVar(upper)) if typevar.can_be_bound_for(db, storage, upper) => { let (lower_node, lower_source_order) = if lower.is_none() { (ALWAYS_TRUE, None) } else { - Constraint::new_node_with_bounds(db, builder, typevar, lower, None) + Constraint::new_node_with_bounds(db, storage, typevar, lower, None) }; let upper_constraint = ConstraintId::new_with_bounds( db, - builder, + storage, upper, Some(Type::TypeVar(typevar)), None, ); let (upper_node, upper_source_order) = - Node::new_constraint(builder, upper_constraint); - let node = lower_node.and(builder, upper_node); + Node::new_constraint(storage, upper_constraint); + let node = lower_node.and(storage, upper_node); let source_order = - builder.ordered_source_order(lower_source_order, upper_source_order); + storage.ordered_source_order(lower_source_order, upper_source_order); (node, source_order) } _ => { - let constraint = ConstraintId::new_with_bounds(db, builder, typevar, lower, upper); - Node::new_constraint(builder, constraint) + let constraint = ConstraintId::new_with_bounds(db, storage, typevar, lower, upper); + Node::new_constraint(storage, constraint) } } } @@ -2173,11 +2224,11 @@ impl ConstraintId { fn implies<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, other: Self, ) -> bool { - let self_constraint = builder.constraint_data(self); - let other_constraint = builder.constraint_data(other); + let self_constraint = storage.constraint_data(self); + let other_constraint = storage.constraint_data(other); if !self_constraint .typevar .is_same_typevar_as(db, other_constraint.typevar) @@ -2198,11 +2249,11 @@ impl ConstraintId { fn intersect<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, other: Self, ) -> IntersectionResult<'db> { - let self_constraint = builder.constraint_data(self); - let other_constraint = builder.constraint_data(other); + let self_constraint = storage.constraint_data(self); + let other_constraint = storage.constraint_data(other); // (s₁ ≤ α ≤ t₁) ∧ (s₂ ≤ α ≤ t₂) = (s₁ ∪ s₂) ≤ α ≤ (t₁ ∩ t₂)) let lower = match (self_constraint.bounds.lower, other_constraint.bounds.lower) { @@ -2225,8 +2276,8 @@ impl ConstraintId { // rather than a universal check ("is `lower ≤ upper` for *all* assignments?"), because the // bounds may mention typevars — e.g., `Sequence[int] ≤ A ≤ Sequence[T]` is satisfiable // when `int ≤ T`, even though it's not universally true for all `T`. - let when = merged_upper.when_satisfied_by(db, builder, effective_lower); - if when.is_never_satisfied(db) { + let (when, source_order) = merged_upper.when_satisfied_by(db, storage, effective_lower); + if when.is_never_satisfied(db, storage, source_order) { return IntersectionResult::Disjoint; } @@ -2250,12 +2301,8 @@ impl ConstraintId { }) } - pub(crate) fn display<'db>( - self, - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - ) -> impl Display { - self.when_true().display(db, builder) + fn display<'db>(self, db: &'db dyn Db, storage: &ConstraintSetStorage<'db>) -> impl Display { + self.when_true().display(db, storage) } } @@ -2305,17 +2352,17 @@ enum Node { impl NodeId { /// Creates a new BDD node, applying local TDD reductions. fn new( - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, constraint: ConstraintId, if_true: NodeId, if_false: NodeId, ) -> NodeId { - Self::with_uncertain(builder, constraint, if_true, ALWAYS_FALSE, if_false) + Self::with_uncertain(storage, constraint, if_true, ALWAYS_FALSE, if_false) } /// Creates a new TDD node with an explicit `if_uncertain` branch, applying local reductions. fn with_uncertain( - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, constraint: ConstraintId, if_true: NodeId, if_uncertain: NodeId, @@ -2323,21 +2370,21 @@ impl NodeId { ) -> NodeId { debug_assert!( if_true - .root_constraint(builder) + .root_constraint(storage) .is_none_or(|root_constraint| { root_constraint.ordering() > constraint.ordering() }) ); debug_assert!( if_uncertain - .root_constraint(builder) + .root_constraint(storage) .is_none_or(|root_constraint| { root_constraint.ordering() > constraint.ordering() }) ); debug_assert!( if_false - .root_constraint(builder) + .root_constraint(storage) .is_none_or(|root_constraint| { root_constraint.ordering() > constraint.ordering() }) @@ -2371,7 +2418,7 @@ impl NodeId { return if_uncertain; } - builder.intern_interior_node(InteriorNodeData { + storage.intern_interior_node(InteriorNodeData { constraint, if_true, if_uncertain, @@ -2384,12 +2431,12 @@ impl Node { /// Creates a new BDD node for an individual constraint. (The BDD will evaluate to `true` when /// the constraint holds, and to `false` when it does not.) fn new_constraint( - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, constraint: ConstraintId, ) -> (NodeId, Option) { ( - NodeId::with_uncertain(builder, constraint, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_FALSE), - Some(builder.constraint_source_order(constraint)), + NodeId::with_uncertain(storage, constraint, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_FALSE), + Some(storage.constraint_source_order(constraint)), ) } @@ -2399,26 +2446,26 @@ impl Node { /// negation of that BDD node. For an unconstrained constraint, the result holds regardless /// of the constraint's truth value.) fn new_satisfied_constraint( - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, constraint: ConstraintAssignment, ) -> (NodeId, Option) { let constraint_id = constraint.constraint(); let node = match constraint { ConstraintAssignment::Positive(constraint) => { - NodeId::with_uncertain(builder, constraint, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_FALSE) + NodeId::with_uncertain(storage, constraint, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_FALSE) } ConstraintAssignment::Negative(constraint) => { - NodeId::with_uncertain(builder, constraint, ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_TRUE) + NodeId::with_uncertain(storage, constraint, ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_TRUE) } // The result holds regardless of the constraint's truth value, so only // `if_uncertain` needs to be `ALWAYS_TRUE` — `n? 0: 1: 0`. It would also be // correct to use `n? 1: 1: 1` (i.e., `ALWAYS_TRUE` for all outgoing edges), but // that would throw away some of the efficiency gains this representation gives us. ConstraintAssignment::Unconstrained(constraint) => { - NodeId::with_uncertain(builder, constraint, ALWAYS_FALSE, ALWAYS_TRUE, ALWAYS_FALSE) + NodeId::with_uncertain(storage, constraint, ALWAYS_FALSE, ALWAYS_TRUE, ALWAYS_FALSE) } }; - (node, Some(builder.constraint_source_order(constraint_id))) + (node, Some(storage.constraint_source_order(constraint_id))) } } @@ -2445,17 +2492,17 @@ impl NodeId { /// Returns the BDD variable of the root node of this BDD, or `None` if this BDD is a terminal /// node. - fn root_constraint(self, builder: &ConstraintSetBuilder<'_>) -> Option { + fn root_constraint(self, storage: &ConstraintSetStorage<'_>) -> Option { if self.is_terminal() { return None; } - let interior = builder.interior_node_data(self); + let interior = storage.interior_node_data(self); Some(interior.constraint) } /// Checks whether this BDD represents a single conjunction (of an arbitrary number of /// positive or negative constraints). - fn is_single_conjunction(self, builder: &ConstraintSetBuilder<'_>) -> bool { + fn is_single_conjunction(self, storage: &mut ConstraintSetStorage<'_>) -> bool { // A BDD can be viewed as an encoding of the formula's DNF representation (OR of ANDs). // Each path from the root node to the `always` terminals represents one of the disjoints. // The constraints that we encounter on the path represent the conjoints. That means that a @@ -2473,7 +2520,7 @@ impl NodeId { Node::AlwaysTrue => return true, Node::AlwaysFalse => return false, Node::Interior(interior) => { - let data = builder.interior_node_data(interior.node()); + let data = storage.interior_node_data(interior.node()); // If both if_true and if_false point to non-never, there are multiple paths to // `always`, so this cannot be a simple conjunction. @@ -2502,15 +2549,15 @@ impl NodeId { fn is_always_satisfied<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, source_order: Option, ) -> bool { match self.node() { Node::AlwaysTrue => true, Node::AlwaysFalse => false, Node::Interior(interior) => { - let mut path = interior.path_assignments(builder, source_order); - path.visit_negated(db, builder, self, &mut IsNeverSatisfiedVisitor) + let mut path = interior.path_assignments(storage, source_order); + path.visit_negated(db, storage, self, &mut IsNeverSatisfiedVisitor) .is_continue() } } @@ -2520,7 +2567,7 @@ impl NodeId { fn is_never_satisfied<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, source_order: Option, ) -> bool { /// Checks whether this BDD is a single conjunction, where either (a) every constraint is @@ -2528,7 +2575,7 @@ impl NodeId { /// so, `object` or `Never` respectively is a valid solution regardless of the contents of /// the constraints. fn simple_conjunction_is_satisfiable( - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, mut node: NodeId, ) -> bool { let mut found_lower = false; @@ -2539,7 +2586,7 @@ impl NodeId { Node::AlwaysFalse => return false, Node::Interior(_) => { - let interior = builder.interior_node_data(node); + let interior = storage.interior_node_data(node); if interior.if_false != ALWAYS_FALSE || interior.if_uncertain != ALWAYS_FALSE @@ -2548,7 +2595,7 @@ impl NodeId { return false; } - let constraint = builder.constraint_data(interior.constraint); + let constraint = storage.constraint_data(interior.constraint); found_lower |= constraint.bounds.lower.is_some(); found_upper |= constraint.bounds.upper.is_some(); if found_lower && found_upper { @@ -2567,56 +2614,40 @@ impl NodeId { Node::AlwaysTrue => false, Node::AlwaysFalse => true, Node::Interior(interior) => { - if let Some(result) = builder.storage.borrow().never_satisfied_cache.get(&self) { + if let Some(result) = storage.never_satisfied_cache.get(&self) { return *result; } - let result = if simple_conjunction_is_satisfiable(builder, self) { + let result = if simple_conjunction_is_satisfiable(storage, self) { false } else { - let mut path = interior.path_assignments(builder, source_order); - path.visit(db, builder, self, &mut IsNeverSatisfiedVisitor) + let mut path = interior.path_assignments(storage, source_order); + path.visit(db, storage, self, &mut IsNeverSatisfiedVisitor) .is_continue() }; - builder - .storage - .borrow_mut() - .never_satisfied_cache - .insert(self, result); + storage.never_satisfied_cache.insert(self, result); result } } } - fn solutions_with<'db>( - self, - db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, - inferable: TypeVarSet<'db>, - source_order: Option, - choose: impl FnMut(TypeVarVariance, &PathBound<'db>) -> Result>, ()>, - ) -> Solutions<'db> { - let path_bounds = PathBounds::compute(db, builder, self, inferable, source_order); - path_bounds.solve_with(choose) - } - /// Returns the negation of this BDD. - fn negate(self, builder: &ConstraintSetBuilder<'_>) -> Self { + fn negate(self, storage: &mut ConstraintSetStorage<'_>) -> Self { match self.node() { Node::AlwaysTrue => ALWAYS_FALSE, Node::AlwaysFalse => ALWAYS_TRUE, - Node::Interior(interior) => interior.negate(builder), + Node::Interior(interior) => interior.negate(storage), } } /// Returns the `or` or union of two BDDs. - fn or(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { + fn or(self, storage: &mut ConstraintSetStorage<'_>, other: Self) -> Self { match (self.node(), other.node()) { (Node::AlwaysTrue, _) | (_, Node::AlwaysTrue) => ALWAYS_TRUE, (Node::AlwaysFalse, _) => other, (_, Node::AlwaysFalse) => self, (Node::Interior(self_interior), Node::Interior(other_interior)) => { - self_interior.or(builder, other_interior) + self_interior.or(storage, other_interior) } } } @@ -2644,7 +2675,7 @@ impl NodeId { nodes: impl Iterator)>, zero: Self, one: Self, - mut combine: impl FnMut(Self, &ConstraintSetBuilder<'_>, Self) -> Self, + mut combine: impl FnMut(Self, &mut ConstraintSetStorage<'_>, Self) -> Self, ) -> (Self, Option) { // To implement the "linear" shape described above, we could collect the iterator elements // into a vector, and then use the fold at the bottom of this method to combine the @@ -2684,8 +2715,9 @@ impl NodeId { { let (existing_node, existing_source_order, _) = accumulator.pop().expect("accumulator should not be empty"); - node = combine(existing_node, builder, node); - source_order = builder.ordered_source_order(existing_source_order, source_order); + let mut storage = builder.storage.borrow_mut(); + node = combine(existing_node, &mut storage, node); + source_order = storage.ordered_source_order(existing_source_order, source_order); if node == one { return (node, source_order); } @@ -2697,12 +2729,13 @@ impl NodeId { // At this point, we've consumed all of the iterator. The length of the accumulator will be // the same as the number of 1 bits in the length of the iterator. We do a final fold to // produce the overall result. + let mut storage = builder.storage.borrow_mut(); accumulator.into_iter().fold( (zero, None), |(result_node, result_source_order), (node, source_order, _)| { ( - combine(result_node, builder, node), - builder.ordered_source_order(result_source_order, source_order), + combine(result_node, &mut storage, node), + storage.ordered_source_order(result_source_order, source_order), ) }, ) @@ -2723,36 +2756,40 @@ impl NodeId { } /// Returns the `and` or intersection of two BDDs. - fn and(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { + fn and(self, storage: &mut ConstraintSetStorage<'_>, other: Self) -> Self { match (self.node(), other.node()) { (Node::AlwaysFalse, _) | (_, Node::AlwaysFalse) => ALWAYS_FALSE, (Node::AlwaysTrue, _) => other, (_, Node::AlwaysTrue) => self, (Node::Interior(self_interior), Node::Interior(other_interior)) => { - self_interior.and(builder, other_interior) + self_interior.and(storage, other_interior) } } } - fn implies(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { + fn implies(self, storage: &mut ConstraintSetStorage<'_>, other: Self) -> Self { // p → q == ¬p ∨ q - self.negate(builder).or(builder, other) + self.negate(storage).or(storage, other) } /// Returns a new BDD that evaluates to `true` when both input BDDs evaluate to the same /// result. - fn iff(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> Self { + fn iff(self, storage: &mut ConstraintSetStorage<'_>, other: Self) -> Self { // iff(a, b) = (a ∧ b) ∨ (¬a ∧ ¬b) - let a_and_b = self.and(builder, other); - let not_a_and_not_b = self.negate(builder).and(builder, other.negate(builder)); - a_and_b.or(builder, not_a_and_not_b) + let a_and_b = self.and(storage, other); + let not_a = self.negate(storage); + let not_b = other.negate(storage); + let not_a_and_not_b = not_a.and(storage, not_b); + a_and_b.or(storage, not_a_and_not_b) } /// Returns the `if-then-else` of three BDDs: when `self` evaluates to `true`, it returns what /// `then_node` evaluates to; otherwise it returns what `else_node` evaluates to. - fn ite(self, builder: &ConstraintSetBuilder<'_>, then_node: Self, else_node: Self) -> Self { - self.and(builder, then_node) - .or(builder, self.negate(builder).and(builder, else_node)) + fn ite(self, storage: &mut ConstraintSetStorage<'_>, then_node: Self, else_node: Self) -> Self { + let if_true = self.and(storage, then_node); + let negated = self.negate(storage); + let if_false = negated.and(storage, else_node); + if_true.or(storage, if_false) } /// Returns the TDD `if-then-else` of four BDDs: when `self` evaluates to `true`, it returns @@ -2760,7 +2797,7 @@ impl NodeId { /// `else_node` evaluates to; and `uncertain_node` is included regardless of `self`'s value. fn ite_uncertain( self, - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, then_node: Self, uncertain_node: Self, else_node: Self, @@ -2770,10 +2807,10 @@ impl NodeId { } match self.node() { - Node::AlwaysTrue => then_node.or(builder, uncertain_node), - Node::AlwaysFalse => else_node.or(builder, uncertain_node), + Node::AlwaysTrue => then_node.or(storage, uncertain_node), + Node::AlwaysFalse => else_node.or(storage, uncertain_node), Node::Interior(_) => { - let interior = builder.interior_node_data(self); + let interior = storage.interior_node_data(self); // Fast path for a bare positive constraint whose branches are still later in the // BDD variable ordering. This is the common case when loading an owned TDD into a // fresh builder, and lets us preserve an existing uncertain branch directly. @@ -2781,17 +2818,17 @@ impl NodeId { && interior.if_uncertain == ALWAYS_FALSE && interior.if_false == ALWAYS_FALSE && then_node - .root_constraint(builder) + .root_constraint(storage) .is_none_or(|root| root.ordering() > interior.constraint.ordering()) && uncertain_node - .root_constraint(builder) + .root_constraint(storage) .is_none_or(|root| root.ordering() > interior.constraint.ordering()) && else_node - .root_constraint(builder) + .root_constraint(storage) .is_none_or(|root| root.ordering() > interior.constraint.ordering()) { return NodeId::with_uncertain( - builder, + storage, interior.constraint, then_node, uncertain_node, @@ -2802,9 +2839,11 @@ impl NodeId { // For compound conditions, or when the new builder's variable ordering requires // one of the branches to move above `self`, fall back to the semantic expansion: // `(self ∧ then_node) ∨ uncertain_node ∨ (¬self ∧ else_node)`. - self.and(builder, then_node) - .or(builder, uncertain_node) - .or(builder, self.negate(builder).and(builder, else_node)) + let if_true = self.and(storage, then_node); + let if_true_or_uncertain = if_true.or(storage, uncertain_node); + let negated = self.negate(storage); + let if_false = negated.and(storage, else_node); + if_true_or_uncertain.or(storage, if_false) } } } @@ -2812,7 +2851,7 @@ impl NodeId { fn implies_subtype_of<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, lhs: Type<'db>, rhs: Type<'db>, ) -> (Self, Option) { @@ -2827,14 +2866,14 @@ impl NodeId { let (constraint, constraint_source_order) = match (lhs, rhs) { (Type::TypeVar(bound_typevar), _) => Constraint::new_node_with_bounds( db, - builder, + storage, bound_typevar, None, Some(rhs.bottom_materialization(db)), ), (_, Type::TypeVar(bound_typevar)) => Constraint::new_node_with_bounds( db, - builder, + storage, bound_typevar, Some(lhs.top_materialization(db)), None, @@ -2842,14 +2881,14 @@ impl NodeId { _ => panic!("at least one type should be a typevar"), }; - let node = self.implies(builder, constraint); + let node = self.implies(storage, constraint); (node, constraint_source_order) } - fn satisfied_by_all_typevars<'db, 'c>( + fn satisfied_by_all_typevars<'db>( self, db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, inferable: TypeVarSet<'db>, source_order: Option, ) -> bool { @@ -2860,35 +2899,41 @@ impl NodeId { } let mut typevars = FxHashSet::default(); - self.for_each_unique_constraint(builder, &mut |constraint| { - let constraint = builder.constraint_data(constraint); + self.for_each_unique_constraint_mut(storage, &mut |storage, constraint| { + let constraint = storage.constraint_data(constraint); typevars.insert(constraint.typevar); }); // Specializations can introduce constraints that do not appear in the original BDD. // Compose full constraint sets so those constraints retain their source orders when the // resulting BDD is traversed. - let original = ConstraintSet::from_node(builder, self, source_order); // Returns if some specialization satisfies this constraint set. - let some_specialization_satisfies = move |specializations: ConstraintSet<'db, 'c>| { - let when_satisfied = - specializations - .implies(db, builder, || original) - .and(db, builder, || specializations); - !when_satisfied.is_never_satisfied(db) - }; + let some_specialization_satisfies = + |storage: &mut ConstraintSetStorage<'db>, + specializations: (NodeId, Option)| { + let (specializations, specializations_source_order) = specializations; + let when_satisfied = specializations + .implies(storage, self) + .and(storage, specializations); + let source_order = + storage.ordered_source_order(source_order, specializations_source_order); + !when_satisfied.is_never_satisfied(db, storage, source_order) + }; // Returns if all specializations satisfy this constraint set. - let all_specializations_satisfy = move |specializations: ConstraintSet<'db, 'c>| { - let when_satisfied = - specializations - .implies(db, builder, || original) - .and(db, builder, || specializations); - when_satisfied - .iff(db, builder, specializations) - .is_always_satisfied(db) - }; + let all_specializations_satisfy = + |storage: &mut ConstraintSetStorage<'db>, + specializations: (NodeId, Option)| { + let (specializations, specializations_source_order) = specializations; + let when_satisfied = specializations + .implies(storage, self) + .and(storage, specializations) + .iff(storage, specializations); + let source_order = + storage.ordered_source_order(source_order, specializations_source_order); + when_satisfied.is_always_satisfied(db, storage, source_order) + }; #[expect( clippy::iter_over_hash_type, @@ -2898,8 +2943,8 @@ impl NodeId { if typevar.is_inferable(db, inferable) { // If the typevar is in inferable position, we need to verify that some valid // specialization satisfies the constraint set. - let valid_specializations = typevar.valid_specializations(db, builder); - if !some_specialization_satisfies(valid_specializations) { + let valid_specializations = typevar.valid_specializations(db, storage); + if !some_specialization_satisfies(storage, valid_specializations) { return false; } } else { @@ -2915,12 +2960,12 @@ impl NodeId { // constraint to refer to the synthetic typevar instead of the original gradual // constraint. let (static_specializations, gradual_constraints) = - typevar.required_specializations(db, builder); - if !all_specializations_satisfy(static_specializations) { + typevar.required_specializations(db, storage); + if !all_specializations_satisfy(storage, static_specializations) { return false; } for gradual_constraint in gradual_constraints { - if !some_specialization_satisfies(gradual_constraint) { + if !some_specialization_satisfies(storage, gradual_constraint) { return false; } } @@ -2936,7 +2981,7 @@ impl NodeId { fn exists<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, bound_typevars: TypeVarSet<'db>, source_order: Option, ) -> (Self, Option) { @@ -2949,15 +2994,12 @@ impl NodeId { }; let key = (self, bound_typevars, source_order); - let storage = builder.storage.borrow(); if let Some(result) = storage.exists_cache.get(&key) { return *result; } - drop(storage); - let result = interior.exists_inner(db, builder, bound_typevars, source_order); + let result = interior.exists_inner(db, storage, bound_typevars, source_order); - let mut storage = builder.storage.borrow_mut(); storage.exists_cache.insert(key, result); result } @@ -2965,7 +3007,7 @@ impl NodeId { fn remove_noninferable<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, inferable: TypeVarSet<'db>, source_order: Option, ) -> (Self, Option) { @@ -2973,7 +3015,7 @@ impl NodeId { Node::AlwaysTrue => (ALWAYS_TRUE, None), Node::AlwaysFalse => (ALWAYS_FALSE, None), Node::Interior(interior) => { - interior.remove_noninferable(db, builder, inferable, source_order) + interior.remove_noninferable(db, storage, inferable, source_order) } } } @@ -2986,13 +3028,13 @@ impl NodeId { fn restrict<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, assignment: impl IntoIterator, ) -> (Self, bool) { assignment .into_iter() .fold((self, true), |(restricted, found), assignment| { - let (restricted, found_this) = restricted.restrict_one(db, builder, assignment); + let (restricted, found_this) = restricted.restrict_one(db, storage, assignment); (restricted, found && found_this) }) } @@ -3005,12 +3047,12 @@ impl NodeId { fn restrict_one<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, assignment: ConstraintAssignment, ) -> (Self, bool) { match self.node() { Node::AlwaysTrue | Node::AlwaysFalse => (self, false), - Node::Interior(interior) => interior.restrict_one(db, builder, assignment), + Node::Interior(interior) => interior.restrict_one(db, storage, assignment), } } @@ -3018,7 +3060,7 @@ impl NodeId { fn substitute_intersection<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, left: ConstraintAssignment, right: ConstraintAssignment, replacement: NodeId, @@ -3028,7 +3070,7 @@ impl NodeId { // - left is false // - left is true and right is false // This covers the entire truth table of `left ∧ right`. - let (when_left_and_right, both_found) = self.restrict(db, builder, [left, right]); + let (when_left_and_right, both_found) = self.restrict(db, storage, [left, right]); if !both_found { // If left and right are not both present in the input BDD, we should not even attempt // the substitution, since the Shannon expansion might introduce the missing variables! @@ -3036,8 +3078,8 @@ impl NodeId { // with the input. return self; } - let (when_not_left, _) = self.restrict(db, builder, [left.negated()]); - let (when_left_but_not_right, _) = self.restrict(db, builder, [left, right.negated()]); + let (when_not_left, _) = self.restrict(db, storage, [left.negated()]); + let (when_left_but_not_right, _) = self.restrict(db, storage, [left, right.negated()]); // The result should test `replacement`, and when it's true, it should produce the same // output that input would when `left ∧ right` is true. When replacement is false, it @@ -3054,18 +3096,19 @@ impl NodeId { // false // // (Note that the `else` branch shouldn't be reachable, but we have to provide something!) - let (left_node, _) = Node::new_satisfied_constraint(builder, left); - let (right_node, _) = Node::new_satisfied_constraint(builder, right); - let right_result = right_node.ite(builder, ALWAYS_FALSE, when_left_but_not_right); - let left_result = left_node.ite(builder, right_result, when_not_left); - let result = replacement.ite(builder, when_left_and_right, left_result); + let (left_node, _) = Node::new_satisfied_constraint(storage, left); + let (right_node, _) = Node::new_satisfied_constraint(storage, right); + let right_result = right_node.ite(storage, ALWAYS_FALSE, when_left_but_not_right); + let left_result = left_node.ite(storage, right_result, when_not_left); + let result = replacement.ite(storage, when_left_and_right, left_result); // Lastly, verify that the result is consistent with the input. (It must produce the same // results when `left ∧ right`.) If it doesn't, the substitution isn't valid, and we should // return the original BDD unmodified. - let validity = replacement.iff(builder, left_node.and(builder, right_node)); - let constrained_original = self.and(builder, validity); - let constrained_replacement = result.and(builder, validity); + let intersection = left_node.and(storage, right_node); + let validity = replacement.iff(storage, intersection); + let constrained_original = self.and(storage, validity); + let constrained_replacement = result.and(storage, validity); if constrained_original == constrained_replacement { result } else { @@ -3077,7 +3120,7 @@ impl NodeId { fn substitute_union<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, left: ConstraintAssignment, right: ConstraintAssignment, replacement: NodeId, @@ -3088,7 +3131,7 @@ impl NodeId { // - left is false and right is true // - left and right are both false // This covers the entire truth table of `left ∨ right`. - let (when_l1_r1, both_found) = self.restrict(db, builder, [left, right]); + let (when_l1_r1, both_found) = self.restrict(db, storage, [left, right]); if !both_found { // If left and right are not both present in the input BDD, we should not even attempt // the substitution, since the Shannon expansion might introduce the missing variables! @@ -3096,9 +3139,9 @@ impl NodeId { // with the input. return self; } - let (when_l0_r0, _) = self.restrict(db, builder, [left.negated(), right.negated()]); - let (when_l1_r0, _) = self.restrict(db, builder, [left, right.negated()]); - let (when_l0_r1, _) = self.restrict(db, builder, [left.negated(), right]); + let (when_l0_r0, _) = self.restrict(db, storage, [left.negated(), right.negated()]); + let (when_l1_r0, _) = self.restrict(db, storage, [left, right.negated()]); + let (when_l0_r1, _) = self.restrict(db, storage, [left.negated(), right]); // The result should test `replacement`, and when it's true, it should produce the same // output that input would when `left ∨ right` is true. For OR, this is the union of what @@ -3110,20 +3153,19 @@ impl NodeId { // or(when_l1_r1, when_l1_r0, when_r0_l1) // else // when_l0_r0 - let result = replacement.ite( - builder, - when_l1_r0.or(builder, when_l0_r1.or(builder, when_l1_r1)), - when_l0_r0, - ); + let when_l0_r1_or_l1_r1 = when_l0_r1.or(storage, when_l1_r1); + let when_either = when_l1_r0.or(storage, when_l0_r1_or_l1_r1); + let result = replacement.ite(storage, when_either, when_l0_r0); // Lastly, verify that the result is consistent with the input. (It must produce the same // results when `left ∨ right`.) If it doesn't, the substitution isn't valid, and we should // return the original BDD unmodified. - let (left_node, _) = Node::new_satisfied_constraint(builder, left); - let (right_node, _) = Node::new_satisfied_constraint(builder, right); - let validity = replacement.iff(builder, left_node.or(builder, right_node)); - let constrained_original = self.and(builder, validity); - let constrained_replacement = result.and(builder, validity); + let (left_node, _) = Node::new_satisfied_constraint(storage, left); + let (right_node, _) = Node::new_satisfied_constraint(storage, right); + let union = left_node.or(storage, right_node); + let validity = replacement.iff(storage, union); + let constrained_original = self.and(storage, validity); + let constrained_replacement = result.and(storage, validity); if constrained_original == constrained_replacement { result } else { @@ -3138,26 +3180,50 @@ impl NodeId { /// root-to-leaf occurrence can be exponential in the presence of shared subgraphs. fn for_each_unique_constraint( self, - builder: &ConstraintSetBuilder<'_>, + storage: &ConstraintSetStorage<'_>, f: &mut dyn FnMut(ConstraintId), ) { fn walk( node: NodeId, - builder: &ConstraintSetBuilder<'_>, + storage: &ConstraintSetStorage<'_>, seen: &mut FxHashSet, f: &mut dyn FnMut(ConstraintId), ) { if node.is_terminal() || !seen.insert(node) { return; } - let interior = builder.interior_node_data(node); + let interior = storage.interior_node_data(node); f(interior.constraint); - walk(interior.if_true, builder, seen, f); - walk(interior.if_uncertain, builder, seen, f); - walk(interior.if_false, builder, seen, f); + walk(interior.if_true, storage, seen, f); + walk(interior.if_uncertain, storage, seen, f); + walk(interior.if_false, storage, seen, f); + } + + walk(self, storage, &mut FxHashSet::default(), f); + } + + fn for_each_unique_constraint_mut<'db>( + self, + storage: &mut ConstraintSetStorage<'db>, + f: &mut dyn FnMut(&mut ConstraintSetStorage<'db>, ConstraintId), + ) { + fn walk<'db>( + node: NodeId, + storage: &mut ConstraintSetStorage<'db>, + seen: &mut FxHashSet, + f: &mut dyn FnMut(&mut ConstraintSetStorage<'db>, ConstraintId), + ) { + if node.is_terminal() || !seen.insert(node) { + return; + } + let interior = storage.interior_node_data(node); + f(storage, interior.constraint); + walk(interior.if_true, storage, seen, f); + walk(interior.if_uncertain, storage, seen, f); + walk(interior.if_false, storage, seen, f); } - walk(self, builder, &mut FxHashSet::default(), f); + walk(self, storage, &mut FxHashSet::default(), f); } /// Simplifies a BDD, replacing constraints with simpler or smaller constraints where possible. @@ -3184,38 +3250,38 @@ impl NodeId { fn simplify_for_display<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, ) -> Self { match self.node() { Node::AlwaysTrue | Node::AlwaysFalse => self, - Node::Interior(interior) => interior.simplify(db, builder), + Node::Interior(interior) => interior.simplify(db, storage), } } /// Returns clauses describing all of the variable assignments that cause this BDD to evaluate /// to `true`. (This translates the boolean function that this BDD represents into DNF form.) - fn satisfied_clauses(self, builder: &ConstraintSetBuilder<'_>) -> SatisfiedClauses { + fn satisfied_clauses(self, storage: &ConstraintSetStorage<'_>) -> SatisfiedClauses { struct Searcher { clauses: SatisfiedClauses, current_clause: SatisfiedClause, } impl Searcher { - fn visit_node(&mut self, builder: &ConstraintSetBuilder<'_>, node: NodeId) { + fn visit_node(&mut self, storage: &ConstraintSetStorage<'_>, node: NodeId) { match node.node() { Node::AlwaysFalse => {} Node::AlwaysTrue => self.clauses.push(self.current_clause.clone()), Node::Interior(_) => { - let interior = builder.interior_node_data(node); + let interior = storage.interior_node_data(node); self.current_clause.push(interior.constraint.when_true()); - self.visit_node(builder, interior.if_true); + self.visit_node(storage, interior.if_true); self.current_clause.pop(); self.current_clause .push(interior.constraint.when_unconstrained()); - self.visit_node(builder, interior.if_uncertain); + self.visit_node(storage, interior.if_uncertain); self.current_clause.pop(); self.current_clause.push(interior.constraint.when_false()); - self.visit_node(builder, interior.if_false); + self.visit_node(storage, interior.if_false); self.current_clause.pop(); } } @@ -3226,11 +3292,15 @@ impl NodeId { clauses: SatisfiedClauses::default(), current_clause: SatisfiedClause::default(), }; - searcher.visit_node(builder, self); + searcher.visit_node(storage, self); searcher.clauses } - fn display<'db>(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> impl Display { + fn display<'db>( + self, + db: &'db dyn Db, + storage: &mut ConstraintSetStorage<'db>, + ) -> impl Display { // To render a BDD in DNF form, you perform a depth-first search of the BDD tree, looking // for any path that leads to the AlwaysTrue terminal. Each such path represents one of the // intersection clauses in the DNF form. The path traverses zero or more interior nodes, @@ -3239,7 +3309,7 @@ impl NodeId { struct DisplayNode<'db, 'c> { node: NodeId, db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, + storage: RefCell<&'c mut ConstraintSetStorage<'db>>, } impl Display for DisplayNode<'_, '_> { @@ -3248,9 +3318,10 @@ impl NodeId { Node::AlwaysTrue => f.write_str("always"), Node::AlwaysFalse => f.write_str("never"), Node::Interior(_) => { - let mut clauses = self.node.satisfied_clauses(self.builder); - clauses.simplify(self.db, self.builder); - Display::fmt(&clauses.display(self.db, self.builder), f) + let mut storage = self.storage.borrow_mut(); + let mut clauses = self.node.satisfied_clauses(&storage); + clauses.simplify(self.db, &mut storage); + Display::fmt(&clauses.display(self.db, &storage), f) } } } @@ -3259,7 +3330,7 @@ impl NodeId { DisplayNode { node: self, db, - builder, + storage: RefCell::new(storage), } } @@ -3284,12 +3355,12 @@ impl NodeId { fn display_graph<'db, 'a>( self, db: &'db dyn Db, - builder: &'a ConstraintSetBuilder<'db>, + storage: &'a ConstraintSetStorage<'db>, prefix: &'a dyn Display, ) -> impl Display + 'a { struct DisplayNode<'a, 'db> { db: &'db dyn Db, - builder: &'a ConstraintSetBuilder<'db>, + storage: &'a ConstraintSetStorage<'db>, node: NodeId, prefix: &'a dyn Display, seen: RefCell>, @@ -3297,7 +3368,7 @@ impl NodeId { fn format_node<'db>( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &ConstraintSetStorage<'db>, node: NodeId, prefix: &dyn Display, seen: &RefCell>, @@ -3311,14 +3382,14 @@ impl NodeId { if !is_new { return write!(f, "<{index}> SHARED"); } - let interior = builder.interior_node_data(node); - write!(f, "<{index}> {}", interior.constraint.display(db, builder))?; + let interior = storage.interior_node_data(node); + write!(f, "<{index}> {}", interior.constraint.display(db, storage))?; // Calling display_graph recursively here causes rustc to claim that the // expect(unused) up above is unfulfilled! write!(f, "\n{prefix}┡━₁ ")?; format_node( db, - builder, + storage, interior.if_true, &format_args!("{prefix}│ "), seen, @@ -3327,7 +3398,7 @@ impl NodeId { write!(f, "\n{prefix}├─? ")?; format_node( db, - builder, + storage, interior.if_uncertain, &format_args!("{prefix}│ "), seen, @@ -3336,7 +3407,7 @@ impl NodeId { write!(f, "\n{prefix}└─₀ ")?; format_node( db, - builder, + storage, interior.if_false, &format_args!("{prefix} "), seen, @@ -3349,13 +3420,13 @@ impl NodeId { impl Display for DisplayNode<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - format_node(self.db, self.builder, self.node, self.prefix, &self.seen, f) + format_node(self.db, self.storage, self.node, self.prefix, &self.seen, f) } } DisplayNode { db, - builder, + storage, node: self, prefix, seen: RefCell::default(), @@ -3536,7 +3607,8 @@ impl<'db> Type<'db> { ) -> PathBounds<'db> { let when = source.when_constraint_set_assignable_to_owned(db, target); when.query(|builder, when| { - PathBounds::compute(db, builder, when.node, inferable, when.source_order) + let mut storage = builder.storage.borrow_mut(); + PathBounds::compute(db, &mut storage, when.node, inferable, when.source_order) }) } @@ -3553,7 +3625,7 @@ fn is_possibly_constraint_set_assignable<'db>(db: &'db dyn Db, types: TypePair<' types .first(db) .when_constraint_set_assignable_to_owned(db, types.second(db)) - .query(|_builder, when| !when.is_never_satisfied(db)) + .query(|_storage, when| !when.is_never_satisfied(db)) } /// Per-path bounds for all typevars. Each element is the set of typevar bounds for one BDD path. @@ -3571,7 +3643,7 @@ impl<'db> PathBounds<'db> { /// typevar that appears in the path's constraints. fn compute( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, node: NodeId, inferable: TypeVarSet<'db>, source_order: Option, @@ -3588,7 +3660,7 @@ impl<'db> PathBounds<'db> { fn satisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow { let mut path: Vec<_> = path @@ -3609,7 +3681,7 @@ impl<'db> PathBounds<'db> { fn unsatisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Continue(()) @@ -3618,7 +3690,7 @@ impl<'db> PathBounds<'db> { fn impossible<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Continue(()) @@ -3627,7 +3699,7 @@ impl<'db> PathBounds<'db> { fn combine<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _if_true: Self::Result, _if_uncertain: Self::Result, _if_false: Self::Result, @@ -3636,16 +3708,16 @@ impl<'db> PathBounds<'db> { } } - let mut source_orders = builder.calculate_source_orders(source_order); + let mut source_orders = storage.calculate_source_orders(source_order); if let Some(path_bounds) = - Self::compute_simple_bound_conjunction(db, builder, &source_orders, node, inferable) + Self::compute_simple_bound_conjunction(db, storage, &source_orders, node, inferable) { return path_bounds; } let (node, derived_source_order) = - node.remove_noninferable(db, builder, inferable, source_order); - source_orders.extend(builder.calculate_source_orders(derived_source_order)); + node.remove_noninferable(db, storage, inferable, source_order); + source_orders.extend(storage.calculate_source_orders(derived_source_order)); let interior = match node.node() { Node::AlwaysTrue => return PathBounds::Unconstrained, Node::AlwaysFalse => return PathBounds::Unsatisfiable, @@ -3664,9 +3736,9 @@ impl<'db> PathBounds<'db> { // Sequent discovery must also happen in source order. Sorting the collected paths below // is too late: sequent pairs are not commutative, and TDD traversal order can otherwise // discard gradual evidence before solution extraction. - let path_source_order = builder.ordered_source_order(source_order, derived_source_order); - let mut path = interior.path_assignments(builder, path_source_order); - let _ = path.visit(db, builder, node, &mut collect_visitor); + let path_source_order = storage.ordered_source_order(source_order, derived_source_order); + let mut path = interior.path_assignments(storage, path_source_order); + let _ = path.visit(db, storage, node, &mut collect_visitor); collect_visitor.sorted_paths.sort_by(|path1, path2| { let source_orders1 = path1.iter().map(|(_, source_order)| *source_order); let source_orders2 = path2.iter().map(|(_, source_order)| *source_order); @@ -3680,7 +3752,7 @@ impl<'db> PathBounds<'db> { for path in collect_visitor.sorted_paths { mappings.clear(); for (constraint, _) in path { - let constraint = builder.constraint_data(constraint); + let constraint = storage.constraint_data(constraint); let typevar = constraint.typevar; if let Some(lower) = constraint.bounds.lower { let bounds = mappings.entry(typevar).or_default(); @@ -3721,7 +3793,7 @@ impl<'db> PathBounds<'db> { /// accumulated bound against the typevar's declared bound or constraints. fn compute_simple_bound_conjunction( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, source_orders: &FxIndexSet, node: NodeId, inferable: TypeVarSet<'db>, @@ -3739,12 +3811,12 @@ impl<'db> PathBounds<'db> { Node::AlwaysTrue => break, Node::AlwaysFalse => return None, Node::Interior(_) => { - let interior = builder.interior_node_data(current); + let interior = storage.interior_node_data(current); if interior.if_uncertain != ALWAYS_FALSE || interior.if_false != ALWAYS_FALSE { return None; } - let constraint = builder.constraint_data(interior.constraint); + let constraint = storage.constraint_data(interior.constraint); if !constraint.typevar.is_inferable(db, inferable) { return None; } @@ -3866,8 +3938,10 @@ impl<'db> PathBounds<'db> { // should only be used as a fallback when no concrete type was inferred. if let Some(lower) = path_bound.lower { if !path_bound.upper.is_satisfied_by(db, lower) { - let when_upper = path_bound.upper.when_satisfied_by(db, builder, lower); - if when_upper.is_never_satisfied(db) { + let mut storage = builder.storage.borrow_mut(); + let (when_upper, source_order) = + path_bound.upper.when_satisfied_by(db, &mut storage, lower); + if when_upper.is_never_satisfied(db, &mut storage, source_order) { // This path does not satisfy the accumulated upper bound, and is // therefore not a valid specialization. return Err(()); @@ -3968,14 +4042,16 @@ impl<'db> PathBounds<'db> { // for upper-bound evidence. let when_lower = lower.when_constraint_set_assignable_to_owned(db, constraint_upper); - let when_upper = + let mut storage = builder.storage.borrow_mut(); + let (when_upper, upper_source_order) = path_bound .upper - .when_satisfied_by(db, builder, constraint_lower); - let when = builder - .load(db, &when_lower) - .and(db, builder, || when_upper); - if when.is_never_satisfied(db) { + .when_satisfied_by(db, &mut storage, constraint_lower); + let (when_lower, lower_source_order) = storage.load(db, &when_lower); + let when = when_lower.and(&mut storage, when_upper); + let source_order = + storage.ordered_source_order(lower_source_order, upper_source_order); + if when.is_never_satisfied(db, &mut storage, source_order) { continue; } @@ -4038,94 +4114,94 @@ impl InteriorNode { self.0 } - fn negate(self, builder: &ConstraintSetBuilder<'_>) -> NodeId { + fn negate(self, storage: &mut ConstraintSetStorage<'_>) -> NodeId { let key = self.node(); - let storage = builder.storage.borrow(); if let Some(result) = storage.negate_cache.get(&key) { return *result; } - drop(storage); // negate(n ? C : U : D) = n ? negate(or(C, U)) : 0 : negate(or(D, U)) // // The uncertain branch U is absorbed into C and D via union before negation. The result's // uncertain branch is always zero. When U = 0 (the common case), this degenerates to the // standard binary BDD leaf-swap: n ? negate(C) : 0 : negate(D). - let interior = builder.interior_node_data(self.node()); - let not_true = interior.if_true.negate(builder); - let not_uncertain = interior.if_uncertain.negate(builder); - let not_false = interior.if_false.negate(builder); - let result = NodeId::new( - builder, - interior.constraint, - not_true.and(builder, not_uncertain), - not_false.and(builder, not_uncertain), - ); + let interior = storage.interior_node_data(self.node()); + let not_true = interior.if_true.negate(storage); + let not_uncertain = interior.if_uncertain.negate(storage); + let not_false = interior.if_false.negate(storage); + let if_true = not_true.and(storage, not_uncertain); + let if_false = not_false.and(storage, not_uncertain); + let result = NodeId::new(storage, interior.constraint, if_true, if_false); - let mut storage = builder.storage.borrow_mut(); storage.negate_cache.insert(key, result); result } - fn or(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> NodeId { + fn or(self, storage: &mut ConstraintSetStorage<'_>, other: Self) -> NodeId { let key = (self.node(), other.node()); - let storage = builder.storage.borrow(); if let Some(result) = storage.or_cache.get(&key) { return *result; } - drop(storage); - let self_interior = builder.interior_node_data(self.node()); + let self_interior = storage.interior_node_data(self.node()); let self_ordering = self_interior.constraint.ordering(); - let other_interior = builder.interior_node_data(other.node()); + let other_interior = storage.interior_node_data(other.node()); let other_ordering = other_interior.constraint.ordering(); let result = match self_ordering.cmp(&other_ordering) { - Ordering::Equal => NodeId::with_uncertain( - builder, - self_interior.constraint, - self_interior.if_true.or(builder, other_interior.if_true), - self_interior + Ordering::Equal => { + let if_true = self_interior.if_true.or(storage, other_interior.if_true); + let if_uncertain = self_interior .if_uncertain - .or(builder, other_interior.if_uncertain), - self_interior.if_false.or(builder, other_interior.if_false), - ), + .or(storage, other_interior.if_uncertain); + let if_false = self_interior.if_false.or(storage, other_interior.if_false); + NodeId::with_uncertain( + storage, + self_interior.constraint, + if_true, + if_uncertain, + if_false, + ) + } // This is from Frisch's original description of TDDs. If self < other, we check self // first. Instead of distributing other into the if_true and if_false branches, we // "park" it in the if_uncertain branch. That causes us to only evaluate other "lazily" // when needed. - Ordering::Less => NodeId::with_uncertain( - builder, - self_interior.constraint, - self_interior.if_true, - self_interior.if_uncertain.or(builder, other.node()), - self_interior.if_false, - ), + Ordering::Less => { + let if_uncertain = self_interior.if_uncertain.or(storage, other.node()); + NodeId::with_uncertain( + storage, + self_interior.constraint, + self_interior.if_true, + if_uncertain, + self_interior.if_false, + ) + } // Ditto above but for the other variable ordering - Ordering::Greater => NodeId::with_uncertain( - builder, - other_interior.constraint, - other_interior.if_true, - self.node().or(builder, other_interior.if_uncertain), - other_interior.if_false, - ), + Ordering::Greater => { + let if_uncertain = self.node().or(storage, other_interior.if_uncertain); + NodeId::with_uncertain( + storage, + other_interior.constraint, + other_interior.if_true, + if_uncertain, + other_interior.if_false, + ) + } }; - let mut storage = builder.storage.borrow_mut(); storage.or_cache.insert(key, result); result } - fn and(self, builder: &ConstraintSetBuilder<'_>, other: Self) -> NodeId { + fn and(self, storage: &mut ConstraintSetStorage<'_>, other: Self) -> NodeId { let key = (self.node(), other.node()); - let storage = builder.storage.borrow(); if let Some(result) = storage.and_cache.get(&key) { return *result; } - drop(storage); - let self_interior = builder.interior_node_data(self.node()); + let self_interior = storage.interior_node_data(self.node()); let self_ordering = self_interior.constraint.ordering(); - let other_interior = builder.interior_node_data(other.node()); + let other_interior = storage.interior_node_data(other.node()); let other_ordering = other_interior.constraint.ordering(); let result = match self_ordering.cmp(&other_ordering) { // This is one of Duboc's optimizations over Frisch's original TDD operators. Frisch @@ -4138,62 +4214,59 @@ impl InteriorNode { // // See [Duboc2026], §11.2 for more details. Ordering::Equal => { - let if_true = self_interior + let other_if_true = other_interior .if_true - .and( - builder, - other_interior - .if_true - .or(builder, other_interior.if_uncertain), - ) - .or( - builder, - self_interior - .if_uncertain - .and(builder, other_interior.if_true), - ); + .or(storage, other_interior.if_uncertain); + let true_from_true = self_interior.if_true.and(storage, other_if_true); + let true_from_uncertain = self_interior + .if_uncertain + .and(storage, other_interior.if_true); + let if_true = true_from_true.or(storage, true_from_uncertain); let if_uncertain = self_interior .if_uncertain - .and(builder, other_interior.if_uncertain); - let if_false = self_interior - .if_false - .and( - builder, - other_interior - .if_uncertain - .or(builder, other_interior.if_false), - ) - .or( - builder, - self_interior - .if_uncertain - .and(builder, other_interior.if_false), - ); + .and(storage, other_interior.if_uncertain); + let other_if_false = other_interior + .if_uncertain + .or(storage, other_interior.if_false); + let false_from_false = self_interior.if_false.and(storage, other_if_false); + let false_from_uncertain = self_interior + .if_uncertain + .and(storage, other_interior.if_false); + let if_false = false_from_false.or(storage, false_from_uncertain); NodeId::with_uncertain( - builder, + storage, self_interior.constraint, if_true, if_uncertain, if_false, ) } - Ordering::Less => NodeId::with_uncertain( - builder, - self_interior.constraint, - self_interior.if_true.and(builder, other.node()), - self_interior.if_uncertain.and(builder, other.node()), - self_interior.if_false.and(builder, other.node()), - ), - Ordering::Greater => NodeId::with_uncertain( - builder, - other_interior.constraint, - self.node().and(builder, other_interior.if_true), - self.node().and(builder, other_interior.if_uncertain), - self.node().and(builder, other_interior.if_false), - ), + Ordering::Less => { + let if_true = self_interior.if_true.and(storage, other.node()); + let if_uncertain = self_interior.if_uncertain.and(storage, other.node()); + let if_false = self_interior.if_false.and(storage, other.node()); + NodeId::with_uncertain( + storage, + self_interior.constraint, + if_true, + if_uncertain, + if_false, + ) + } + Ordering::Greater => { + let if_true = self.node().and(storage, other_interior.if_true); + let if_uncertain = self.node().and(storage, other_interior.if_uncertain); + let if_false = self.node().and(storage, other_interior.if_false); + NodeId::with_uncertain( + storage, + other_interior.constraint, + if_true, + if_uncertain, + if_false, + ) + } }; - let mut storage = builder.storage.borrow_mut(); storage.and_cache.insert(key, result); result } @@ -4201,24 +4274,24 @@ impl InteriorNode { fn exists_inner<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, bound_typevars: TypeVarSet<'db>, source_order: Option, ) -> (NodeId, Option) { - let mentions_typevar = |ty: Type<'db>| match ty { + let mentions_typevar = |ty: Type<'_>| match ty { Type::TypeVar(typevar) => typevar.is_inferable(db, bound_typevars), _ => false, }; self.abstract_inner( db, - builder, + storage, source_order, // Remove any node that constrains one of `bound_typevars`, or that has a lower/upper // bound that mentions one of them. Removed constraints are still added to `path`, so // the sequent map can propagate any derived constraints that do not mention the // quantified typevars. - &mut |constraint| { - let constraint = builder.constraint_data(constraint); + &mut |storage: &ConstraintSetStorage<'_>, constraint| { + let constraint = storage.constraint_data(constraint); constraint.typevar.is_inferable(db, bound_typevars) || constraint .bounds @@ -4235,17 +4308,17 @@ impl InteriorNode { fn remove_noninferable<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, inferable: TypeVarSet<'db>, source_order: Option, ) -> (NodeId, Option) { - let is_bare_inferable_typevar = |ty: Type<'db>| { + let is_bare_inferable_typevar = |ty: Type<'_>| { ty.as_typevar() .is_some_and(|bound_typevar| bound_typevar.is_inferable(db, inferable)) }; self.abstract_inner( db, - builder, + storage, source_order, // We only want to keep constraints on inferable typevars. If the constraint's typevar // is itself inferable, we keep it. We also need to keep some constraints in @@ -4256,8 +4329,8 @@ impl InteriorNode { // either as `Never ≤ I ≤ N` or `I ≤ N ≤ object`, depending on typevar ordering. If we // only checked the inferability of the constrained typevar, we would keep the first // encoding but remove the second. - &mut |constraint| { - let constraint = builder.constraint_data(constraint); + &mut |storage: &ConstraintSetStorage<'_>, constraint| { + let constraint = storage.constraint_data(constraint); !constraint.typevar.is_inferable(db, inferable) && !constraint .bounds @@ -4274,12 +4347,12 @@ impl InteriorNode { fn abstract_inner<'db, F>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, source_order: Option, should_remove: F, ) -> (NodeId, Option) where - F: FnMut(ConstraintId) -> bool, + F: FnMut(&ConstraintSetStorage<'_>, ConstraintId) -> bool, { #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Disposition { @@ -4293,7 +4366,7 @@ impl InteriorNode { impl PathVisitor for AbstractVisitor where - F: FnMut(ConstraintId) -> bool, + F: FnMut(&ConstraintSetStorage<'_>, ConstraintId) -> bool, { type Result = (NodeId, Option); type Interior = (Disposition, ConstraintId); @@ -4302,7 +4375,7 @@ impl InteriorNode { fn visit_satisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Continue((ALWAYS_TRUE, None)) @@ -4311,7 +4384,7 @@ impl InteriorNode { fn visit_unsatisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Continue((ALWAYS_FALSE, None)) @@ -4320,7 +4393,7 @@ impl InteriorNode { fn visit_impossible<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Continue((ALWAYS_FALSE, None)) @@ -4329,11 +4402,11 @@ impl InteriorNode { fn enter_interior<'db>( &mut self, _db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, interior: InteriorNode, ) -> ControlFlow { - let interior = builder.interior_node_data(interior.node()); - let disposition = if (self.should_remove)(interior.constraint) { + let interior = storage.interior_node_data(interior.node()); + let disposition = if (self.should_remove)(storage, interior.constraint) { Disposition::Remove } else { Disposition::Keep @@ -4344,7 +4417,7 @@ impl InteriorNode { fn visit_edge<'db>( &mut self, _db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, interior: &Self::Interior, subtree: Self::Result, path: &PathAssignments, @@ -4360,29 +4433,20 @@ impl InteriorNode { // that depend on the constraint we're about to remove. If so, we need to // "remember" them by AND-ing them in with the corresponding branch. Disposition::Remove => { - ControlFlow::Continue( - path.assignments[new_range] - .iter() - .filter(|(assignment, _)| { - // Don't add back any derived facts if they are ones that we would have - // removed! - !(self.should_remove)(assignment.constraint()) - }) - .fold( - subtree, - |(subtree, subtree_source_order), (assignment, _)| { - let (assignment, assignment_source_order) = - Node::new_satisfied_constraint(builder, *assignment); - ( - subtree.and(builder, assignment), - builder.ordered_source_order( - subtree_source_order, - assignment_source_order, - ), - ) - }, - ), - ) + let (mut result, mut result_source_order) = subtree; + for (assignment, _) in &path.assignments[new_range] { + // Don't add back any derived facts if they are ones that we would have + // removed! + if (self.should_remove)(storage, assignment.constraint()) { + continue; + } + let (assignment, assignment_source_order) = + Node::new_satisfied_constraint(storage, *assignment); + result = result.and(storage, assignment); + result_source_order = storage + .ordered_source_order(result_source_order, assignment_source_order); + } + ControlFlow::Continue((result, result_source_order)) } } } @@ -4390,7 +4454,7 @@ impl InteriorNode { fn leave_interior<'db>( &mut self, _db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, interior: &Self::Interior, if_true: Self::Result, if_uncertain: Self::Result, @@ -4408,22 +4472,20 @@ impl InteriorNode { // one in the BDD ordering. Disposition::Keep => { let (guard, guard_source_order) = - Node::new_constraint(builder, *constraint); + Node::new_constraint(storage, *constraint); let (if_true, if_true_source_order) = if_true; let (if_uncertain, if_uncertain_source_order) = if_uncertain; let (if_false, if_false_source_order) = if_false; - let node = guard.ite( - builder, - if_true.or(builder, if_uncertain), - if_false.or(builder, if_uncertain), - ); + let if_true = if_true.or(storage, if_uncertain); + let if_false = if_false.or(storage, if_uncertain); + let node = guard.ite(storage, if_true, if_false); let left_source_order = - builder.ordered_source_order(guard_source_order, if_true_source_order); - let right_source_order = builder + storage.ordered_source_order(guard_source_order, if_true_source_order); + let right_source_order = storage .ordered_source_order(if_uncertain_source_order, if_false_source_order); ControlFlow::Continue(( node, - builder.ordered_source_order(left_source_order, right_source_order), + storage.ordered_source_order(left_source_order, right_source_order), )) } @@ -4435,38 +4497,36 @@ impl InteriorNode { let (if_true, if_true_source_order) = if_true; let (if_uncertain, if_uncertain_source_order) = if_uncertain; let (if_false, if_false_source_order) = if_false; - let node = if_true.or(builder, if_uncertain).or(builder, if_false); - let source_order = builder + let node = if_true.or(storage, if_uncertain).or(storage, if_false); + let source_order = storage .ordered_source_order(if_true_source_order, if_uncertain_source_order); ControlFlow::Continue(( node, - builder.ordered_source_order(source_order, if_false_source_order), + storage.ordered_source_order(source_order, if_false_source_order), )) } } } } - let mut path = self.path_assignments(builder, source_order); + let mut path = self.path_assignments(storage, source_order); let mut visitor = AbstractVisitor { should_remove }; - let ControlFlow::Continue(result) = path.visit(db, builder, self.node(), &mut visitor); + let ControlFlow::Continue(result) = path.visit(db, storage, self.node(), &mut visitor); result } fn restrict_one<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, assignment: ConstraintAssignment, ) -> (NodeId, bool) { let key = (self.node(), assignment); - let storage = builder.storage.borrow(); if let Some(result) = storage.restrict_one_cache.get(&key) { return *result; } - drop(storage); - let self_interior = builder.interior_node_data(self.node()); + let self_interior = storage.interior_node_data(self.node()); let self_ordering = self_interior.constraint.ordering(); let result = if assignment.constraint().ordering() < self_ordering { // If this node's variable is larger than the assignment's variable, then we have reached a @@ -4482,7 +4542,7 @@ impl InteriorNode { ( self_interior .if_true - .or(builder, self_interior.if_uncertain), + .or(storage, self_interior.if_uncertain), true, ) } else if assignment == self_interior.constraint.when_false() { @@ -4490,7 +4550,7 @@ impl InteriorNode { ( self_interior .if_false - .or(builder, self_interior.if_uncertain), + .or(storage, self_interior.if_uncertain), true, ) } else if assignment == self_interior.constraint.when_unconstrained() { @@ -4498,21 +4558,21 @@ impl InteriorNode { ( self_interior .if_true - .or(builder, self_interior.if_uncertain) - .or(builder, self_interior.if_false), + .or(storage, self_interior.if_uncertain) + .or(storage, self_interior.if_false), true, ) } else { let (if_true, found_in_true) = - self_interior.if_true.restrict_one(db, builder, assignment); + self_interior.if_true.restrict_one(db, storage, assignment); let (if_uncertain, found_in_uncertain) = self_interior .if_uncertain - .restrict_one(db, builder, assignment); + .restrict_one(db, storage, assignment); let (if_false, found_in_false) = - self_interior.if_false.restrict_one(db, builder, assignment); + self_interior.if_false.restrict_one(db, storage, assignment); ( NodeId::with_uncertain( - builder, + storage, self_interior.constraint, if_true, if_uncertain, @@ -4523,22 +4583,21 @@ impl InteriorNode { } }; - let mut storage = builder.storage.borrow_mut(); storage.restrict_one_cache.insert(key, result); result } fn path_assignments( self, - builder: &ConstraintSetBuilder<'_>, + storage: &mut ConstraintSetStorage<'_>, source_order: Option, ) -> PathAssignments { let mut constraints: SmallVec<[_; 8]> = SmallVec::new(); self.node() - .for_each_unique_constraint(builder, &mut |constraint| { + .for_each_unique_constraint(storage, &mut |constraint| { constraints.push(constraint); }); - let source_orders = builder.calculate_source_orders(source_order); + let source_orders = storage.calculate_source_orders(source_order); // `PathAssignments` seeds its insertion-ordered discovered-constraint map from this list, // and uses that order when constructing non-commutative sequent pairs. Do not replace this // with TDD traversal order: doing so can change inference and lose gradual constraints. @@ -4557,13 +4616,11 @@ impl InteriorNode { /// This is calculated by looking at the relationships that exist between the constraints that /// are mentioned in the BDD. For instance, if one constraint implies another (`x → y`), then /// `x ∧ ¬y` is not a valid input, and we can rewrite any occurrences of `x ∨ y` into `y`. - fn simplify<'db>(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> NodeId { + fn simplify<'db>(self, db: &'db dyn Db, storage: &mut ConstraintSetStorage<'db>) -> NodeId { let key = self.node(); - let storage = builder.storage.borrow(); if let Some(result) = storage.simplify_cache.get(&key) { return *result; } - drop(storage); // To simplify a non-terminal BDD, we find all pairs of constraints that are mentioned in // the BDD. If any of those pairs can be simplified to some other BDD, we perform a @@ -4582,7 +4639,7 @@ impl InteriorNode { // need to compare a constraint against itself, and because ordering doesn't matter.) let mut seen_constraints = FxHashSet::default(); self.node() - .for_each_unique_constraint(builder, &mut |constraint| { + .for_each_unique_constraint(storage, &mut |constraint| { seen_constraints.insert(constraint); }); let mut to_visit: Vec<(_, _)> = (seen_constraints.iter().copied()) @@ -4596,9 +4653,9 @@ impl InteriorNode { while let Some((left_constraint, right_constraint)) = to_visit.pop() { // If the constraints refer to different typevars, the only simplifications we can make // are of the form `S ≤ T ∧ T ≤ int → S ≤ int`. - let left_constraint_data = builder.constraint_data(left_constraint); + let left_constraint_data = storage.constraint_data(left_constraint); let left_typevar = left_constraint_data.typevar; - let right_constraint_data = builder.constraint_data(right_constraint); + let right_constraint_data = storage.constraint_data(right_constraint); let right_typevar = right_constraint_data.typevar; if !left_typevar.is_same_typevar_as(db, right_typevar) { // We've structured our constraints so that a typevar's upper/lower bound can only @@ -4607,14 +4664,14 @@ impl InteriorNode { // have to figure out which of the two typevars is constrained, and which one is // the upper/lower bound. let (bound_constraint, constrained_constraint) = - if left_typevar.can_be_bound_for(db, builder, right_typevar) { + if left_typevar.can_be_bound_for(db, storage, right_typevar) { (left_constraint, right_constraint) } else { (right_constraint, left_constraint) }; - let bound_constraint_data = builder.constraint_data(bound_constraint); + let bound_constraint_data = storage.constraint_data(bound_constraint); let bound_typevar = bound_constraint_data.typevar; - let constrained_constraint_data = builder.constraint_data(constrained_constraint); + let constrained_constraint_data = storage.constraint_data(constrained_constraint); let constrained_typevar = constrained_constraint_data.typevar; // We then look for cases where the "constrained" typevar's upper and/or lower @@ -4657,7 +4714,7 @@ impl InteriorNode { let new_constraint = ConstraintId::new_with_bounds( db, - builder, + storage, constrained_typevar, new_lower, new_upper, @@ -4665,14 +4722,14 @@ impl InteriorNode { if seen_constraints.contains(&new_constraint) { continue; } - let (new_node, _) = Node::new_constraint(builder, new_constraint); + let (new_node, _) = Node::new_constraint(storage, new_constraint); let (positive_left_node, _) = - Node::new_satisfied_constraint(builder, left_constraint.when_true()); + Node::new_satisfied_constraint(storage, left_constraint.when_true()); let (positive_right_node, _) = - Node::new_satisfied_constraint(builder, right_constraint.when_true()); - let lhs = positive_left_node.and(builder, positive_right_node); - let intersection = new_node.ite(builder, lhs, ALWAYS_FALSE); - simplified = simplified.and(builder, intersection); + Node::new_satisfied_constraint(storage, right_constraint.when_true()); + let lhs = positive_left_node.and(storage, positive_right_node); + let intersection = new_node.ite(storage, lhs, ALWAYS_FALSE); + simplified = simplified.and(storage, intersection); continue; } @@ -4703,23 +4760,23 @@ impl InteriorNode { // Containment: The range of one constraint might completely contain the range of the // other. If so, there are several potential simplifications. - let larger_smaller = if left_constraint.implies(db, builder, right_constraint) { + let larger_smaller = if left_constraint.implies(db, storage, right_constraint) { Some((right_constraint, left_constraint)) - } else if right_constraint.implies(db, builder, left_constraint) { + } else if right_constraint.implies(db, storage, left_constraint) { Some((left_constraint, right_constraint)) } else { None }; if let Some((larger_constraint, smaller_constraint)) = larger_smaller { let (positive_larger_node, _) = - Node::new_satisfied_constraint(builder, larger_constraint.when_true()); + Node::new_satisfied_constraint(storage, larger_constraint.when_true()); let (negative_larger_node, _) = - Node::new_satisfied_constraint(builder, larger_constraint.when_false()); + Node::new_satisfied_constraint(storage, larger_constraint.when_false()); // larger ∨ smaller = larger simplified = simplified.substitute_union( db, - builder, + storage, larger_constraint.when_true(), smaller_constraint.when_true(), positive_larger_node, @@ -4728,7 +4785,7 @@ impl InteriorNode { // ¬larger ∧ ¬smaller = ¬larger simplified = simplified.substitute_intersection( db, - builder, + storage, larger_constraint.when_false(), smaller_constraint.when_false(), negative_larger_node, @@ -4738,7 +4795,7 @@ impl InteriorNode { // (¬larger removes everything that's present in smaller) simplified = simplified.substitute_intersection( db, - builder, + storage, larger_constraint.when_false(), smaller_constraint.when_true(), ALWAYS_FALSE, @@ -4748,7 +4805,7 @@ impl InteriorNode { // (larger fills in everything that's missing in ¬smaller) simplified = simplified.substitute_union( db, - builder, + storage, larger_constraint.when_true(), smaller_constraint.when_false(), ALWAYS_TRUE, @@ -4758,10 +4815,10 @@ impl InteriorNode { // There are some simplifications we can make when the intersection of the two // constraints is empty, and others that we can make when the intersection is // non-empty. - match left_constraint.intersect(db, builder, right_constraint) { + match left_constraint.intersect(db, storage, right_constraint) { IntersectionResult::Simplified(intersection_constraint_data) => { let intersection_constraint = - builder.intern_constraint(db, intersection_constraint_data); + storage.intern_constraint(db, intersection_constraint_data); // If the intersection is non-empty, we need to create a new constraint to // represent that intersection. We also need to add the new constraint to our @@ -4774,28 +4831,28 @@ impl InteriorNode { ); } let (positive_intersection_node, _) = Node::new_satisfied_constraint( - builder, + storage, intersection_constraint.when_true(), ); let (negative_intersection_node, _) = Node::new_satisfied_constraint( - builder, + storage, intersection_constraint.when_false(), ); let (positive_left_node, _) = - Node::new_satisfied_constraint(builder, left_constraint.when_true()); + Node::new_satisfied_constraint(storage, left_constraint.when_true()); let (negative_left_node, _) = - Node::new_satisfied_constraint(builder, left_constraint.when_false()); + Node::new_satisfied_constraint(storage, left_constraint.when_false()); let (positive_right_node, _) = - Node::new_satisfied_constraint(builder, right_constraint.when_true()); + Node::new_satisfied_constraint(storage, right_constraint.when_true()); let (negative_right_node, _) = - Node::new_satisfied_constraint(builder, right_constraint.when_false()); + Node::new_satisfied_constraint(storage, right_constraint.when_false()); // left ∧ right = intersection simplified = simplified.substitute_intersection( db, - builder, + storage, left_constraint.when_true(), right_constraint.when_true(), positive_intersection_node, @@ -4804,7 +4861,7 @@ impl InteriorNode { // ¬left ∨ ¬right = ¬intersection simplified = simplified.substitute_union( db, - builder, + storage, left_constraint.when_false(), right_constraint.when_false(), negative_intersection_node, @@ -4813,43 +4870,47 @@ impl InteriorNode { // left ∧ ¬right = left ∧ ¬intersection // (clip the negative constraint to the smallest range that actually removes // something from positive constraint) + let replacement = positive_left_node.and(storage, negative_intersection_node); simplified = simplified.substitute_intersection( db, - builder, + storage, left_constraint.when_true(), right_constraint.when_false(), - positive_left_node.and(builder, negative_intersection_node), + replacement, ); // ¬left ∧ right = ¬intersection ∧ right // (save as above but reversed) + let replacement = positive_right_node.and(storage, negative_intersection_node); simplified = simplified.substitute_intersection( db, - builder, + storage, left_constraint.when_false(), right_constraint.when_true(), - positive_right_node.and(builder, negative_intersection_node), + replacement, ); // left ∨ ¬right = intersection ∨ ¬right // (clip the positive constraint to the smallest range that actually adds // something to the negative constraint) + let replacement = negative_right_node.or(storage, positive_intersection_node); simplified = simplified.substitute_union( db, - builder, + storage, left_constraint.when_true(), right_constraint.when_false(), - negative_right_node.or(builder, positive_intersection_node), + replacement, ); // ¬left ∨ right = ¬left ∨ intersection // (save as above but reversed) + let replacement = negative_left_node.or(storage, positive_intersection_node); simplified = simplified.substitute_union( db, - builder, + storage, left_constraint.when_false(), right_constraint.when_true(), - negative_left_node.or(builder, positive_intersection_node), + replacement, ); } @@ -4862,14 +4923,14 @@ impl InteriorNode { // and right is empty. let (positive_left_node, _) = - Node::new_satisfied_constraint(builder, left_constraint.when_true()); + Node::new_satisfied_constraint(storage, left_constraint.when_true()); let (positive_right_node, _) = - Node::new_satisfied_constraint(builder, right_constraint.when_true()); + Node::new_satisfied_constraint(storage, right_constraint.when_true()); // left ∧ right = false simplified = simplified.substitute_intersection( db, - builder, + storage, left_constraint.when_true(), right_constraint.when_true(), ALWAYS_FALSE, @@ -4878,7 +4939,7 @@ impl InteriorNode { // ¬left ∨ ¬right = true simplified = simplified.substitute_union( db, - builder, + storage, left_constraint.when_false(), right_constraint.when_false(), ALWAYS_TRUE, @@ -4888,7 +4949,7 @@ impl InteriorNode { // (there is nothing in the hole of ¬right that overlaps with left) simplified = simplified.substitute_intersection( db, - builder, + storage, left_constraint.when_true(), right_constraint.when_false(), positive_left_node, @@ -4898,7 +4959,7 @@ impl InteriorNode { // (save as above but reversed) simplified = simplified.substitute_intersection( db, - builder, + storage, left_constraint.when_false(), right_constraint.when_true(), positive_right_node, @@ -4907,7 +4968,6 @@ impl InteriorNode { } } - let mut storage = builder.storage.borrow_mut(); storage.simplify_cache.insert(key, simplified); simplified } @@ -4974,7 +5034,7 @@ impl ConstraintAssignment { fn implies<'db>( self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, other: Self, ) -> bool { match (self, other) { @@ -4986,7 +5046,7 @@ impl ConstraintAssignment { ( ConstraintAssignment::Positive(self_constraint), ConstraintAssignment::Positive(other_constraint), - ) => self_constraint.implies(db, builder, other_constraint), + ) => self_constraint.implies(db, storage, other_constraint), // For two negative constraints, one range has to fully contain the other; the ranges // represent "holes", though, so the constraint with the larger range implies the one @@ -4997,7 +5057,7 @@ impl ConstraintAssignment { ( ConstraintAssignment::Negative(self_constraint), ConstraintAssignment::Negative(other_constraint), - ) => other_constraint.implies(db, builder, self_constraint), + ) => other_constraint.implies(db, storage, self_constraint), // For a positive and negative constraint, the ranges have to be disjoint, and the // positive range implies the negative range. @@ -5008,7 +5068,7 @@ impl ConstraintAssignment { ConstraintAssignment::Positive(self_constraint), ConstraintAssignment::Negative(other_constraint), ) => self_constraint - .intersect(db, builder, other_constraint) + .intersect(db, storage, other_constraint) .is_disjoint(), // It's theoretically possible for a negative constraint to imply a positive constraint @@ -5033,11 +5093,11 @@ impl ConstraintAssignment { } } - fn display<'db>(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> impl Display { + fn display<'db>(self, db: &'db dyn Db, storage: &ConstraintSetStorage<'db>) -> impl Display { struct DisplayConstraintAssignment<'db, 'c> { assignment: ConstraintAssignment, db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, + storage: &'c ConstraintSetStorage<'db>, } impl DisplayConstraintAssignment<'_, '_> { @@ -5060,7 +5120,7 @@ impl ConstraintAssignment { impl Display for DisplayConstraintAssignment<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let constraint_data = self.builder.constraint_data(self.assignment.constraint()); + let constraint_data = self.storage.constraint_data(self.assignment.constraint()); let lower = constraint_data.bounds.materialized_lower(); let upper = constraint_data.bounds.materialized_upper(); let typevar = constraint_data.typevar; @@ -5113,7 +5173,7 @@ impl ConstraintAssignment { DisplayConstraintAssignment { assignment: self, db, - builder, + storage, } } } @@ -5187,30 +5247,21 @@ impl SequentMap { /// constraint. fn for_constraint<'db, 'c>( db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, + storage: &'c mut ConstraintSetStorage<'db>, constraint: ConstraintId, - ) -> Ref<'c, Self> { + ) -> &'c Self { let key = constraint; - let storage = builder.storage.borrow(); - if let Ok(map) = Ref::filter_map(storage, |storage| storage.single_sequent_cache.get(&key)) - { - return map; + if !storage.single_sequent_cache.contains_key(&key) { + tracing::trace!( + target: "ty_python_semantic::types::constraints::SequentMap", + constraint = %constraint.display(db, storage), + "add sequents for constraint", + ); + let mut map = SequentMap::default(); + map.add_sequents_for_single(db, storage, constraint); + storage.single_sequent_cache.insert(key, map); } - - tracing::trace!( - target: "ty_python_semantic::types::constraints::SequentMap", - constraint = %constraint.display(db, builder), - "add sequents for constraint", - ); - let mut map = SequentMap::default(); - map.add_sequents_for_single(db, builder, constraint); - - let mut storage = builder.storage.borrow_mut(); - storage.single_sequent_cache.insert(key, map); - drop(storage); - - let storage = builder.storage.borrow(); - Ref::map(storage, |storage| &storage.single_sequent_cache[&key]) + &storage.single_sequent_cache[&key] } /// Returns a sequent map containing the sequents that we can infer from a pair of constraints. @@ -5221,31 +5272,23 @@ impl SequentMap { /// that retain that ordering.) fn for_constraint_pair<'db, 'c>( db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, + storage: &'c mut ConstraintSetStorage<'db>, left: ConstraintId, right: ConstraintId, - ) -> Ref<'c, Self> { + ) -> &'c Self { let key = (left, right); - let storage = builder.storage.borrow(); - if let Ok(map) = Ref::filter_map(storage, |storage| storage.pair_sequent_cache.get(&key)) { - return map; + if !storage.pair_sequent_cache.contains_key(&key) { + tracing::trace!( + target: "ty_python_semantic::types::constraints::SequentMap", + left = %left.display(db, storage), + right = %right.display(db, storage), + "add sequents for constraint pair", + ); + let mut map = SequentMap::default(); + map.add_sequents_for_pair(db, storage, left, right); + storage.pair_sequent_cache.insert(key, map); } - - tracing::trace!( - target: "ty_python_semantic::types::constraints::SequentMap", - left = %left.display(db, builder), - right = %right.display(db, builder), - "add sequents for constraint pair", - ); - let mut map = SequentMap::default(); - map.add_sequents_for_pair(db, builder, left, right); - - let mut storage = builder.storage.borrow_mut(); - storage.pair_sequent_cache.insert(key, map); - drop(storage); - - let storage = builder.storage.borrow(); - Ref::map(storage, |storage| &storage.pair_sequent_cache[&key]) + &storage.pair_sequent_cache[&key] } /// Quickly determines whether two constraints cannot possibly produce any sequents when passed @@ -5253,7 +5296,7 @@ impl SequentMap { /// to skip calling `for_constraint_pair` for this pair of constraints. fn pair_cannot_produce_sequents<'db>( db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, left: ConstraintId, right: ConstraintId, ) -> bool { @@ -5263,8 +5306,8 @@ impl SequentMap { // single constraint; we always break that apart into the two smaller constraints that we // started with. - let left = builder.constraint_data(left); - let right = builder.constraint_data(right); + let left = storage.constraint_data(left); + let right = storage.constraint_data(right); if !left.typevar.is_same_typevar_as(db, right.typevar) { return false; } @@ -5283,8 +5326,11 @@ impl SequentMap { return false; }; + // This call might need its own borrow of the builder's storage, so create a new builder + // that it can use. + let builder = ConstraintSetBuilder::new(); left_lower - .when_trivially_disjoint_from(db, right_lower, builder, TypeVarSet::None) + .when_trivially_disjoint_from(db, right_lower, &builder, TypeVarSet::None) .is_trivially_always_satisfied() } @@ -5300,27 +5346,27 @@ impl SequentMap { fn add_pair_implication<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, ante1: ConstraintId, ante2: ConstraintId, post: ConstraintId, ) { // If the post constraint is unsatisfiable, then the antecedents contradict each other. - let post_data = builder.constraint_data(post); - let when = builder.load( + let post_data = storage.constraint_data(post); + let (when, source_order) = storage.load( db, &post_data .bounds .materialized_lower() .when_constraint_set_assignable_to_owned(db, post_data.bounds.materialized_upper()), ); - if when.is_never_satisfied(db) { + if when.is_never_satisfied(db, storage, source_order) { self.add_pair_impossibility(ante1, ante2); return; } // If either antecedent implies the consequent on its own, this new sequent is redundant. - if ante1.implies(db, builder, post) || ante2.implies(db, builder, post) { + if ante1.implies(db, storage, post) || ante2.implies(db, storage, post) { return; } @@ -5340,12 +5386,12 @@ impl SequentMap { fn add_sequents_for_single<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, constraint: ConstraintId, ) { // If this constraint binds its typevar to `Never ≤ T ≤ object`, then the typevar can take // on any type, and the constraint is always satisfied. - let constraint_data = builder.constraint_data(constraint); + let constraint_data = storage.constraint_data(constraint); let lower = constraint_data.bounds.materialized_lower(); let upper = constraint_data.bounds.materialized_upper(); if lower.is_never() && upper.is_object() { @@ -5397,21 +5443,24 @@ impl SequentMap { return; } - let when = builder.load( + let (when, source_order) = storage.load( db, &lower.when_constraint_set_assignable_to_owned(db, upper), ); // If L is _never_ assignable to U, this constraint would violate transitivity, and should // never have been added. - debug_assert!(!when.is_never_satisfied(db)); + #[expect(clippy::debug_assert_with_mut_call)] + { + debug_assert!(!when.is_never_satisfied(db, storage, source_order)); + } // Fast path: If L is trivially always assignable to U, there are no derived constraints // that we can infer. This would be handled correctly by the logic below, but this is a // useful early return. Since we only use this check as an early return happy path, we can // accept false negatives. That lets us use the simpler and cheaper check against // ALWAYS_TRUE, rather than a more expensive is_always_satisfiable call. - if when.node == ALWAYS_TRUE { + if when == ALWAYS_TRUE { return; } @@ -5447,8 +5496,8 @@ impl SequentMap { // it once for _every_ root→always path in the BDD. (That would require resetting the // PathAssignments state for each of those paths, which is why the logic would have to // move.) - let mut node = when.node; - if !node.is_single_conjunction(builder) { + let mut node = when; + if !node.is_single_conjunction(storage) { return; } @@ -5456,7 +5505,7 @@ impl SequentMap { match node.node() { Node::AlwaysTrue | Node::AlwaysFalse => break, Node::Interior(interior) => { - let interior = builder.interior_node_data(interior.node()); + let interior = storage.interior_node_data(interior.node()); if interior.if_true != ALWAYS_FALSE { self.add_single_implication(constraint, interior.constraint); node = interior.if_true; @@ -5472,7 +5521,7 @@ impl SequentMap { fn add_sequents_for_pair<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, ) { @@ -5497,19 +5546,19 @@ impl SequentMap { // // If all of the lower and upper bounds are concrete (i.e., not typevars), then there // several _other_ sequents that we can add, as handled by `add_concrete_sequents`. - let left_constraint_data = builder.constraint_data(left_constraint); + let left_constraint_data = storage.constraint_data(left_constraint); let left_typevar = left_constraint_data.typevar; - let right_constraint_data = builder.constraint_data(right_constraint); + let right_constraint_data = storage.constraint_data(right_constraint); let right_typevar = right_constraint_data.typevar; if !left_typevar.is_same_typevar_as(db, right_typevar) { self.add_mutual_sequents_for_different_typevars( db, - builder, + storage, left_constraint, right_constraint, ); - self.add_nested_typevar_sequents(db, builder, left_constraint, right_constraint); + self.add_nested_typevar_sequents(db, storage, left_constraint, right_constraint); } else if left_constraint_data .bounds .lower @@ -5529,19 +5578,19 @@ impl SequentMap { { self.add_mutual_sequents_for_same_typevars( db, - builder, + storage, left_constraint, right_constraint, ); } else { - self.add_concrete_sequents(db, builder, left_constraint, right_constraint); + self.add_concrete_sequents(db, storage, left_constraint, right_constraint); } } fn add_mutual_sequents_for_different_typevars<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, ) { @@ -5550,12 +5599,12 @@ impl SequentMap { // we only have to check this pair of constraints in one direction — though we do // have to figure out which of the two typevars is constrained, and which one is // the upper/lower bound. - let left_constraint_data = builder.constraint_data(left_constraint); + let left_constraint_data = storage.constraint_data(left_constraint); let left_typevar = left_constraint_data.typevar; - let right_constraint_data = builder.constraint_data(right_constraint); + let right_constraint_data = storage.constraint_data(right_constraint); let right_typevar = right_constraint_data.typevar; let (bound_constraint, constrained_constraint) = - if left_typevar.can_be_bound_for(db, builder, right_typevar) { + if left_typevar.can_be_bound_for(db, storage, right_typevar) { (left_constraint, right_constraint) } else { (right_constraint, left_constraint) @@ -5565,9 +5614,9 @@ impl SequentMap { // matches the "bound" typevar. If so, we're going to add an implication sequent that // replaces the upper/lower bound that matched with the bound constraint's corresponding // bound. - let bound_constraint_data = builder.constraint_data(bound_constraint); + let bound_constraint_data = storage.constraint_data(bound_constraint); let bound_typevar = bound_constraint_data.typevar; - let constrained_constraint_data = builder.constraint_data(constrained_constraint); + let constrained_constraint_data = storage.constraint_data(constrained_constraint); let constrained_typevar = constrained_constraint_data.typevar; // Transitive pivots require subtyping; classes with dynamic bases can be assignable to @@ -5611,7 +5660,7 @@ impl SequentMap { (constrained_lower, Some(constrained_upper), Some(bound_lower), _) if !constrained_upper.is_never() && !constrained_upper.is_object() - && builder.cached_is_constraint_set_subtype_of( + && storage.cached_is_constraint_set_subtype_of( db, constrained_upper.top_materialization(db), bound_lower.bottom_materialization(db), @@ -5624,7 +5673,7 @@ impl SequentMap { (Some(constrained_lower), constrained_upper, _, Some(bound_upper)) if !constrained_lower.is_never() && !constrained_lower.is_object() - && builder.cached_is_constraint_set_subtype_of( + && storage.cached_is_constraint_set_subtype_of( db, bound_upper.top_materialization(db), constrained_lower.bottom_materialization(db), @@ -5660,11 +5709,11 @@ impl SequentMap { // `(Never ≤ [A] ≤ T)` and `(T ≤ [B] ≤ object)`. // This preserves the relationship while keeping all derived constraints canonical. if let Some(Type::TypeVar(lower_bound_typevar)) = new_lower - && !lower_bound_typevar.can_be_bound_for(db, builder, constrained_typevar) + && !lower_bound_typevar.can_be_bound_for(db, storage, constrained_typevar) { post_constraints.push(ConstraintId::new_with_bounds( db, - builder, + storage, lower_bound_typevar, None, Some(Type::TypeVar(constrained_typevar)), @@ -5673,11 +5722,11 @@ impl SequentMap { } if let Some(Type::TypeVar(upper_bound_typevar)) = new_upper - && !upper_bound_typevar.can_be_bound_for(db, builder, constrained_typevar) + && !upper_bound_typevar.can_be_bound_for(db, storage, constrained_typevar) { post_constraints.push(ConstraintId::new_with_bounds( db, - builder, + storage, upper_bound_typevar, Some(Type::TypeVar(constrained_typevar)), None, @@ -5690,7 +5739,7 @@ impl SequentMap { { post_constraints.push(ConstraintId::new_with_bounds( db, - builder, + storage, constrained_typevar, constrained_lower, constrained_upper, @@ -5700,7 +5749,7 @@ impl SequentMap { for post_constraint in post_constraints { self.add_pair_implication( db, - builder, + storage, left_constraint, right_constraint, post_constraint, @@ -5720,7 +5769,7 @@ impl SequentMap { fn add_nested_typevar_sequents<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, ) { @@ -5733,18 +5782,18 @@ impl SequentMap { .upper .is_some_and(|bound| any_over_type(db, bound, true, Type::is_type_var)) }; - if !has_typevar_bound(builder.constraint_data(left_constraint).bounds) - && !has_typevar_bound(builder.constraint_data(right_constraint).bounds) + if !has_typevar_bound(storage.constraint_data(left_constraint).bounds) + && !has_typevar_bound(storage.constraint_data(right_constraint).bounds) { return; } let mut try_tightening = |bound_constraint: ConstraintId, constrained_constraint: ConstraintId| { - let bound_data = builder.constraint_data(bound_constraint); + let bound_data = storage.constraint_data(bound_constraint); let bound_typevar = bound_data.typevar; let bound_identity = bound_typevar.identity(db); - let constrained_data = builder.constraint_data(constrained_constraint); + let constrained_data = storage.constraint_data(constrained_constraint); let constrained_typevar = constrained_data.typevar; let constrained_identity = constrained_typevar.identity(db); let constrained_lower = constrained_data.bounds.materialized_lower(); @@ -5821,14 +5870,14 @@ impl SequentMap { if new_upper != constrained_upper { let post = ConstraintId::new_with_bounds( db, - builder, + storage, constrained_typevar, constrained_data.bounds.lower, Some(new_upper), ); self.add_pair_implication( db, - builder, + storage, bound_constraint, constrained_constraint, post, @@ -5882,14 +5931,14 @@ impl SequentMap { if new_lower != constrained_lower { let post = ConstraintId::new_with_bounds( db, - builder, + storage, constrained_typevar, Some(new_lower), constrained_data.bounds.upper, ); self.add_pair_implication( db, - builder, + storage, bound_constraint, constrained_constraint, post, @@ -5924,10 +5973,10 @@ impl SequentMap { // bound constraint's typevar. let mut try_weakening = |bound_constraint: ConstraintId, constrained_constraint: ConstraintId| { - let bound_data = builder.constraint_data(bound_constraint); + let bound_data = storage.constraint_data(bound_constraint); let bound_typevar = bound_data.typevar; let bound_lower = bound_data.bounds.materialized_lower(); - let constrained_data = builder.constraint_data(constrained_constraint); + let constrained_data = storage.constraint_data(constrained_constraint); let constrained_typevar = constrained_data.typevar; let constrained_lower = constrained_data.bounds.materialized_lower(); let constrained_upper = constrained_data.bounds.materialized_upper(); @@ -5975,14 +6024,14 @@ impl SequentMap { if new_upper != constrained_upper { let post = ConstraintId::new_with_bounds( db, - builder, + storage, constrained_typevar, constrained_data.bounds.lower, Some(new_upper), ); self.add_pair_implication( db, - builder, + storage, bound_constraint, constrained_constraint, post, @@ -6013,14 +6062,14 @@ impl SequentMap { if new_lower != constrained_lower { let post = ConstraintId::new_with_bounds( db, - builder, + storage, constrained_typevar, Some(new_lower), constrained_data.bounds.upper, ); self.add_pair_implication( db, - builder, + storage, bound_constraint, constrained_constraint, post, @@ -6048,19 +6097,19 @@ impl SequentMap { fn add_mutual_sequents_for_same_typevars<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, ) { let mut try_one_direction = |left_constraint: ConstraintId, right_constraint: ConstraintId| { - let left_constraint_data = builder.constraint_data(left_constraint); + let left_constraint_data = storage.constraint_data(left_constraint); let left_lower = left_constraint_data.bounds.lower; let left_upper = left_constraint_data.bounds.upper; - let right_constraint_data = builder.constraint_data(right_constraint); + let right_constraint_data = storage.constraint_data(right_constraint); let right_lower = right_constraint_data.bounds.lower; let right_upper = right_constraint_data.bounds.upper; - let new_constraints = + let mut new_constraints = |bound_typevar: BoundTypeVarInstance<'db>, mut right_lower: Option>, mut right_upper: Option>| { @@ -6088,11 +6137,11 @@ impl SequentMap { let mut constrained_upper = right_upper.filter(|upper| !upper.is_object()); if let Some(Type::TypeVar(lower_bound_typevar)) = right_lower - && !lower_bound_typevar.can_be_bound_for(db, builder, bound_typevar) + && !lower_bound_typevar.can_be_bound_for(db, storage, bound_typevar) { post_constraints.push(ConstraintId::new_with_bounds( db, - builder, + storage, lower_bound_typevar, None, Some(Type::TypeVar(bound_typevar)), @@ -6101,11 +6150,11 @@ impl SequentMap { } if let Some(Type::TypeVar(upper_bound_typevar)) = right_upper - && !upper_bound_typevar.can_be_bound_for(db, builder, bound_typevar) + && !upper_bound_typevar.can_be_bound_for(db, storage, bound_typevar) { post_constraints.push(ConstraintId::new_with_bounds( db, - builder, + storage, upper_bound_typevar, Some(Type::TypeVar(bound_typevar)), None, @@ -6118,7 +6167,7 @@ impl SequentMap { { post_constraints.push(ConstraintId::new_with_bounds( db, - builder, + storage, bound_typevar, constrained_lower, constrained_upper, @@ -6145,7 +6194,7 @@ impl SequentMap { for post_constraint in post_constraints { self.add_pair_implication( db, - builder, + storage, left_constraint, right_constraint, post_constraint, @@ -6160,7 +6209,7 @@ impl SequentMap { fn add_concrete_sequents<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, ) { @@ -6169,39 +6218,39 @@ impl SequentMap { // identify constraints that are identical besides e.g. ordering of union/intersection // elements. (For instance, when processing `T ≤ τ₁ & τ₂` and `T ≤ τ₂ & τ₁`, these clauses // would add sequents for `(T ≤ τ₁ & τ₂) → (T ≤ τ₂ & τ₁)` and vice versa.) - if builder.cached_constraint_implies(db, left_constraint, right_constraint) { + if storage.cached_constraint_implies(db, left_constraint, right_constraint) { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db, builder), - right = %right_constraint.display(db, builder), + left = %left_constraint.display(db, storage), + right = %right_constraint.display(db, storage), "left implies right", ); self.add_single_implication(left_constraint, right_constraint); } - if builder.cached_constraint_implies(db, right_constraint, left_constraint) { + if storage.cached_constraint_implies(db, right_constraint, left_constraint) { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db, builder), - right = %right_constraint.display(db, builder), + left = %left_constraint.display(db, storage), + right = %right_constraint.display(db, storage), "right implies left", ); self.add_single_implication(right_constraint, left_constraint); } - match left_constraint.intersect(db, builder, right_constraint) { + match left_constraint.intersect(db, storage, right_constraint) { IntersectionResult::Simplified(intersection_constraint_data) => { let intersection_constraint = - builder.intern_constraint(db, intersection_constraint_data); + storage.intern_constraint(db, intersection_constraint_data); tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db, builder), - right = %right_constraint.display(db, builder), - intersection = %intersection_constraint.display(db, builder), + left = %left_constraint.display(db, storage), + right = %right_constraint.display(db, storage), + intersection = %intersection_constraint.display(db, storage), "left and right overlap", ); self.add_pair_implication( db, - builder, + storage, left_constraint, right_constraint, intersection_constraint, @@ -6218,8 +6267,8 @@ impl SequentMap { IntersectionResult::Disjoint => { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db, builder), - right = %right_constraint.display(db, builder), + left = %left_constraint.display(db, storage), + right = %right_constraint.display(db, storage), "left and right are disjoint", ); self.add_pair_impossibility(left_constraint, right_constraint); @@ -6231,14 +6280,14 @@ impl SequentMap { fn display<'db, 'a>( &'a self, db: &'db dyn Db, - builder: &'a ConstraintSetBuilder<'db>, + storage: &'a ConstraintSetStorage<'db>, prefix: &'a dyn Display, ) -> impl Display + 'a { struct DisplaySequentMap<'a, 'db> { map: &'a SequentMap, prefix: &'a dyn Display, db: &'db dyn Db, - builder: &'a ConstraintSetBuilder<'db>, + storage: &'a ConstraintSetStorage<'db>, } impl Display for DisplaySequentMap<'_, '_> { @@ -6262,8 +6311,8 @@ impl SequentMap { write!( f, "{} ∧ {} → false", - ante1.display(self.db, self.builder), - ante2.display(self.db, self.builder), + ante1.display(self.db, self.storage), + ante2.display(self.db, self.storage), )?; } @@ -6272,9 +6321,9 @@ impl SequentMap { write!( f, "{} ∧ {} → {}", - ante1.display(self.db, self.builder), - ante2.display(self.db, self.builder), - post.display(self.db, self.builder), + ante1.display(self.db, self.storage), + ante2.display(self.db, self.storage), + post.display(self.db, self.storage), )?; } @@ -6283,8 +6332,8 @@ impl SequentMap { write!( f, "{} → {}", - ante.display(self.db, self.builder), - post.display(self.db, self.builder) + ante.display(self.db, self.storage), + post.display(self.db, self.storage) )?; } } @@ -6301,7 +6350,7 @@ impl SequentMap { map: self, prefix, db, - builder, + storage, } } } @@ -6350,7 +6399,7 @@ trait PathVisitor { fn visit_satisfied<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow; @@ -6360,7 +6409,7 @@ trait PathVisitor { fn visit_unsatisfied<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow; @@ -6371,7 +6420,7 @@ trait PathVisitor { fn visit_impossible<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow; @@ -6382,7 +6431,7 @@ trait PathVisitor { fn enter_interior<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, interior_node: InteriorNode, ) -> ControlFlow; @@ -6392,7 +6441,7 @@ trait PathVisitor { fn visit_edge<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, interior_value: &Self::Interior, subtree: Self::Result, path: &PathAssignments, @@ -6404,7 +6453,7 @@ trait PathVisitor { fn leave_interior<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, interior_value: &Self::Interior, if_true: Self::Result, if_uncertain: Self::Result, @@ -6425,7 +6474,7 @@ trait PathFold { fn satisfied<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow; @@ -6433,7 +6482,7 @@ trait PathFold { fn unsatisfied<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow; @@ -6441,7 +6490,7 @@ trait PathFold { fn impossible<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow; @@ -6450,7 +6499,7 @@ trait PathFold { fn combine<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, if_true: Self::Result, if_uncertain: Self::Result, if_false: Self::Result, @@ -6468,34 +6517,34 @@ where fn visit_satisfied<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow { - PathFold::satisfied(self, db, builder, path) + PathFold::satisfied(self, db, storage, path) } fn visit_unsatisfied<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow { - PathFold::unsatisfied(self, db, builder, path) + PathFold::unsatisfied(self, db, storage, path) } fn visit_impossible<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow { - PathFold::impossible(self, db, builder, path) + PathFold::impossible(self, db, storage, path) } fn enter_interior<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _interior_node: InteriorNode, ) -> ControlFlow { ControlFlow::Continue(()) @@ -6504,7 +6553,7 @@ where fn visit_edge<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _interior_value: &Self::Interior, subtree: Self::Result, _path: &PathAssignments, @@ -6516,13 +6565,13 @@ where fn leave_interior<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, _interior_value: &Self::Interior, if_true: Self::Result, if_uncertain: Self::Result, if_false: Self::Result, ) -> ControlFlow { - PathFold::combine(self, db, builder, if_true, if_uncertain, if_false) + PathFold::combine(self, db, storage, if_true, if_uncertain, if_false) } } @@ -6538,7 +6587,7 @@ impl PathFold for IsNeverSatisfiedVisitor { fn satisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Break(()) @@ -6547,7 +6596,7 @@ impl PathFold for IsNeverSatisfiedVisitor { fn unsatisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Continue(()) @@ -6556,7 +6605,7 @@ impl PathFold for IsNeverSatisfiedVisitor { fn impossible<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { ControlFlow::Continue(()) @@ -6565,7 +6614,7 @@ impl PathFold for IsNeverSatisfiedVisitor { fn combine<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _if_true: Self::Result, _if_uncertain: Self::Result, _if_false: Self::Result, @@ -6705,34 +6754,34 @@ impl PathAssignments { fn visit<'db, V>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, node: NodeId, visitor: &mut V, ) -> ControlFlow where V: PathVisitor, { - self.visit_inner(db, builder, node, visitor, false) + self.visit_inner(db, storage, node, visitor, false) } /// Visits the paths of the negation of `node`, without constructing that negation eagerly. fn visit_negated<'db, V>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, node: NodeId, visitor: &mut V, ) -> ControlFlow where V: PathVisitor, { - self.visit_inner(db, builder, node, visitor, true) + self.visit_inner(db, storage, node, visitor, true) } fn visit_inner<'db, V>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, node: NodeId, visitor: &mut V, negated: bool, @@ -6741,35 +6790,35 @@ impl PathAssignments { V: PathVisitor, { match node.node() { - Node::AlwaysTrue if negated => visitor.visit_unsatisfied(db, builder, self), - Node::AlwaysTrue => visitor.visit_satisfied(db, builder, self), + Node::AlwaysTrue if negated => visitor.visit_unsatisfied(db, storage, self), + Node::AlwaysTrue => visitor.visit_satisfied(db, storage, self), - Node::AlwaysFalse if negated => visitor.visit_satisfied(db, builder, self), - Node::AlwaysFalse => visitor.visit_unsatisfied(db, builder, self), + Node::AlwaysFalse if negated => visitor.visit_satisfied(db, storage, self), + Node::AlwaysFalse => visitor.visit_unsatisfied(db, storage, self), Node::Interior(interior) => { - let interior_value = visitor.enter_interior(db, builder, interior)?; - let interior = builder.interior_node_data(node); + let interior_value = visitor.enter_interior(db, storage, interior)?; + let interior = storage.interior_node_data(node); let true_subtree = if negated { - interior.if_true.or(builder, interior.if_uncertain) + interior.if_true.or(storage, interior.if_uncertain) } else { interior.if_true }; let if_true = self.walk_edge( db, - builder, + storage, interior.constraint.when_true(), - |path, new_range, found_conflict| { + |storage, path, new_range, found_conflict| { let subtree = if found_conflict { - visitor.visit_impossible(db, builder, path) + visitor.visit_impossible(db, storage, path) } else { - path.visit_inner(db, builder, true_subtree, visitor, negated) + path.visit_inner(db, storage, true_subtree, visitor, negated) }; match subtree { ControlFlow::Continue(subtree) => visitor.visit_edge( db, - builder, + storage, &interior_value, subtree, path, @@ -6781,23 +6830,23 @@ impl PathAssignments { )?; let if_uncertain = if negated { - let subtree = visitor.visit_impossible(db, builder, self)?; - visitor.visit_edge(db, builder, &interior_value, subtree, self, 0..0)? + let subtree = visitor.visit_impossible(db, storage, self)?; + visitor.visit_edge(db, storage, &interior_value, subtree, self, 0..0)? } else { self.walk_edge( db, - builder, + storage, interior.constraint.when_unconstrained(), - |path, new_range, found_conflict| { + |storage, path, new_range, found_conflict| { let subtree = if found_conflict { - visitor.visit_impossible(db, builder, path) + visitor.visit_impossible(db, storage, path) } else { - path.visit_inner(db, builder, interior.if_uncertain, visitor, false) + path.visit_inner(db, storage, interior.if_uncertain, visitor, false) }; match subtree { ControlFlow::Continue(subtree) => visitor.visit_edge( db, - builder, + storage, &interior_value, subtree, path, @@ -6810,24 +6859,24 @@ impl PathAssignments { }; let false_subtree = if negated { - interior.if_false.or(builder, interior.if_uncertain) + interior.if_false.or(storage, interior.if_uncertain) } else { interior.if_false }; let if_false = self.walk_edge( db, - builder, + storage, interior.constraint.when_false(), - |path, new_range, found_conflict| { + |storage, path, new_range, found_conflict| { let subtree = if found_conflict { - visitor.visit_impossible(db, builder, path) + visitor.visit_impossible(db, storage, path) } else { - path.visit_inner(db, builder, false_subtree, visitor, negated) + path.visit_inner(db, storage, false_subtree, visitor, negated) }; match subtree { ControlFlow::Continue(subtree) => visitor.visit_edge( db, - builder, + storage, &interior_value, subtree, path, @@ -6840,7 +6889,7 @@ impl PathAssignments { visitor.leave_interior( db, - builder, + storage, &interior_value, if_true, if_uncertain, @@ -6875,9 +6924,9 @@ impl PathAssignments { fn walk_edge<'db, R>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, assignment: ConstraintAssignment, - f: impl FnOnce(&mut Self, Range, bool) -> R, + f: impl FnOnce(&mut ConstraintSetStorage<'db>, &mut Self, Range, bool) -> R, ) -> R { // Record a snapshot of the assignments that we already knew held — both so that we can // pass along the range of which assignments are new, and so that we can reset back to this @@ -6892,10 +6941,10 @@ impl PathAssignments { before = %format_args!( "[{}]", self.assignments[..start].iter().map(|(assignment, _)| { - assignment.display(db, builder) + assignment.display(db, storage) }).format(", "), ), - edge = %assignment.display(db, builder), + edge = %assignment.display(db, storage), "walk edge", ); debug_assert!(self.assignment_queue.is_empty()); @@ -6903,7 +6952,7 @@ impl PathAssignments { .push_back((assignment, AssignmentFuel::origin())); let source_constraint = assignment.constraint(); let found_conflict = self - .drain_assignment_queue(db, builder, source_constraint) + .drain_assignment_queue(db, storage, source_constraint) .is_err(); if !found_conflict { tracing::trace!( @@ -6911,7 +6960,7 @@ impl PathAssignments { new = %format_args!( "[{}]", self.assignments[start..].iter().map(|(assignment, _)| { - assignment.display(db, builder) + assignment.display(db, storage) }).format(", "), ), "new assignments", @@ -6923,7 +6972,7 @@ impl PathAssignments { // `add_assignment` call above — that is, the new assignment for this edge along with // the derived information we inferred from it. let end = self.assignments.len(); - let result = f(self, start..end, found_conflict); + let result = f(storage, self, start..end, found_conflict); // Reset back to where we were before following this edge, so that the caller can reuse a // single instance for the entire BDD traversal. @@ -6977,7 +7026,7 @@ impl PathAssignments { fn discover_constraint<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, constraint: ConstraintId, ) { // If we've already processed this constraint, we can skip it. @@ -6987,16 +7036,15 @@ impl PathAssignments { return; } - let single_map = SequentMap::for_constraint(db, builder, constraint); + let single_map = SequentMap::for_constraint(db, storage, constraint); self.sequents.extend_from_slice(&single_map.sequents); - drop(single_map); for (existing_index, (existing, _)) in self.discovered.iter().enumerate() { if *existing == constraint { continue; } - if SequentMap::pair_cannot_produce_sequents(db, builder, *existing, constraint) { + if SequentMap::pair_cannot_produce_sequents(db, storage, *existing, constraint) { continue; } @@ -7010,7 +7058,7 @@ impl PathAssignments { continue; } - let pair_map = SequentMap::for_constraint_pair(db, builder, a, b); + let pair_map = SequentMap::for_constraint_pair(db, storage, a, b); self.sequents.extend_from_slice(&pair_map.sequents); } } @@ -7018,11 +7066,11 @@ impl PathAssignments { fn drain_assignment_queue<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, source_constraint: ConstraintId, ) -> Result<(), PathAssignmentConflict> { while let Some((assignment, fuel)) = self.assignment_queue.pop_front() { - self.add_assignment(db, builder, assignment, source_constraint, fuel)?; + self.add_assignment(db, storage, assignment, source_constraint, fuel)?; } Ok(()) } @@ -7033,7 +7081,7 @@ impl PathAssignments { fn add_assignment<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, assignment: ConstraintAssignment, source_constraint: ConstraintId, fuel: AssignmentFuel, @@ -7059,11 +7107,11 @@ impl PathAssignments { if self.assignments.contains_key(&assignment.negated()) { tracing::trace!( target: "ty_python_semantic::types::constraints::PathAssignment", - assignment = %assignment.display(db, builder), + assignment = %assignment.display(db, storage), facts = %format_args!( "[{}]", self.assignments.iter().map(|(assignment, _)| { - assignment.display(db, builder) + assignment.display(db, storage) }).format(", "), ), "found contradiction", @@ -7130,11 +7178,11 @@ impl PathAssignments { // brute-force search. self.new_assignments.clear(); - self.discover_constraint(db, builder, assignment.constraint()); + self.discover_constraint(db, storage, assignment.constraint()); for i in 0..self.sequents.len() { let sequent = self.sequents[i]; - self.check_sequent(db, builder, sequent)?; + self.check_sequent(db, storage, sequent)?; } // If we were able to derive any new assignments from this one, add them to the processing @@ -7156,20 +7204,20 @@ impl PathAssignments { fn check_sequent<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, sequent: Sequent, ) -> Result<(), PathAssignmentConflict> { match sequent { - Sequent::SingleTautology { ante } => self.check_single_tautology(db, builder, ante), + Sequent::SingleTautology { ante } => self.check_single_tautology(db, storage, ante), Sequent::PairImpossibility { ante1, ante2 } => { - self.check_pair_impossibility(db, builder, ante1, ante2) + self.check_pair_impossibility(db, storage, ante1, ante2) } Sequent::PairImplication { ante1, ante2, post } => { - self.check_pair_implication(db, builder, ante1, ante2, post); + self.check_pair_implication(db, storage, ante1, ante2, post); Ok(()) } Sequent::SingleImplication { ante, post } => { - self.check_single_implication(db, builder, ante, post); + self.check_single_implication(db, storage, ante, post); Ok(()) } } @@ -7178,7 +7226,7 @@ impl PathAssignments { fn check_single_tautology<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, ante: ConstraintId, ) -> Result<(), PathAssignmentConflict> { if self.assignment_holds(ante.when_false()) { @@ -7186,11 +7234,11 @@ impl PathAssignments { // it's false. tracing::trace!( target: "ty_python_semantic::types::constraints::PathAssignment", - ante = %ante.display(db, builder), + ante = %ante.display(db, storage), facts = %format_args!( "[{}]", self.assignments.iter().map(|(assignment, _)| { - assignment.display(db, builder) + assignment.display(db, storage) }).format(", "), ), "found contradiction", @@ -7204,7 +7252,7 @@ impl PathAssignments { fn check_pair_impossibility<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, ante1: ConstraintId, ante2: ConstraintId, ) -> Result<(), PathAssignmentConflict> { @@ -7213,12 +7261,12 @@ impl PathAssignments { // current path asserts that both are true. tracing::trace!( target: "ty_python_semantic::types::constraints::PathAssignment", - ante1 = %ante1.display(db, builder), - ante2 = %ante2.display(db, builder), + ante1 = %ante1.display(db, storage), + ante2 = %ante2.display(db, storage), facts = %format_args!( "[{}]", self.assignments.iter().map(|(assignment, _)| { - assignment.display(db, builder) + assignment.display(db, storage) }).format(", "), ), "found contradiction", @@ -7232,7 +7280,7 @@ impl PathAssignments { fn check_pair_implication<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, ante1: ConstraintId, ante2: ConstraintId, post: ConstraintId, @@ -7244,10 +7292,10 @@ impl PathAssignments { return; }; let available_fuel = ante1_fuel.min(ante2_fuel); - let (ante1_constructor_depth, _) = builder.cached_constraint_bound_depth(db, ante1); - let (ante2_constructor_depth, _) = builder.cached_constraint_bound_depth(db, ante2); + let (ante1_constructor_depth, _) = storage.cached_constraint_bound_depth(db, ante1); + let (ante2_constructor_depth, _) = storage.cached_constraint_bound_depth(db, ante2); let antecedent_constructor_depth = ante1_constructor_depth.max(ante2_constructor_depth); - let fuel_cost = builder.sequent_fuel_cost(db, post, antecedent_constructor_depth); + let fuel_cost = storage.sequent_fuel_cost(db, post, antecedent_constructor_depth); if let Some(post_fuel) = available_fuel.checked_sub(fuel_cost) { self.enqueue_assignment( post.when_true(), @@ -7259,20 +7307,20 @@ impl PathAssignments { fn check_single_implication<'db>( &mut self, db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, ante: ConstraintId, post: ConstraintId, ) { let Some(available_fuel) = self.max_remaining_fuel_for(ante.when_true()) else { return; }; - let ante_data = builder.constraint_data(ante); - let (antecedent_constructor_depth, _) = builder.cached_constraint_bound_depth(db, ante); - let post_data = builder.constraint_data(post); + let ante_data = storage.constraint_data(ante); + let (antecedent_constructor_depth, _) = storage.cached_constraint_bound_depth(db, ante); + let post_data = storage.constraint_data(post); let fuel_cost = if post_data.is_bound_projection_of(db, ante_data) { 1 } else { - builder.sequent_fuel_cost(db, post, antecedent_constructor_depth) + storage.sequent_fuel_cost(db, post, antecedent_constructor_depth) }; if let Some(post_fuel) = available_fuel.checked_sub(fuel_cost) { self.enqueue_assignment( @@ -7330,7 +7378,7 @@ impl SatisfiedClause { /// want to remove the larger one and keep the smaller one.) /// /// Returns a boolean that indicates whether any simplifications were made. - fn simplify<'db>(&mut self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> bool { + fn simplify<'db>(&mut self, db: &'db dyn Db, storage: &mut ConstraintSetStorage<'db>) -> bool { let mut changes_made = false; let mut i = 0; // Loop through each constraint, comparing it with any constraints that appear later in the @@ -7338,7 +7386,7 @@ impl SatisfiedClause { 'outer: while i < self.constraints.len() { let mut j = i + 1; while j < self.constraints.len() { - if self.constraints[j].implies(db, builder, self.constraints[i]) { + if self.constraints[j].implies(db, storage, self.constraints[i]) { // If constraint `i` is removed, then we don't need to compare it with any // later constraints in the list. Note that we continue the outer loop, instead // of breaking from the inner loop, so that we don't bump index `i` below. @@ -7347,7 +7395,7 @@ impl SatisfiedClause { self.constraints.swap_remove(i); changes_made = true; continue 'outer; - } else if self.constraints[i].implies(db, builder, self.constraints[j]) { + } else if self.constraints[i].implies(db, storage, self.constraints[j]) { // If constraint `j` is removed, then we can continue the inner loop. We will // swap a new element into place at index `j`, and will continue comparing the // constraint at index `i` with later constraints. @@ -7362,7 +7410,7 @@ impl SatisfiedClause { changes_made } - fn display<'db>(&self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> String { + fn display<'db>(&self, db: &'db dyn Db, storage: &ConstraintSetStorage<'db>) -> String { if self.constraints.is_empty() { return String::from("always"); } @@ -7373,7 +7421,7 @@ impl SatisfiedClause { let mut constraints: Vec<_> = self .constraints .iter() - .map(|constraint| constraint.display(db, builder).to_string()) + .map(|constraint| constraint.display(db, storage).to_string()) .collect(); constraints.sort(); @@ -7409,11 +7457,11 @@ impl SatisfiedClauses { /// Simplifies the DNF representation, removing redundancies that do not change the underlying /// function. (This is used when displaying a BDD, to make sure that the representation that we /// show is as simple as possible while still producing the same results.) - fn simplify<'db>(&mut self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) { + fn simplify<'db>(&mut self, db: &'db dyn Db, storage: &mut ConstraintSetStorage<'db>) { // First simplify each clause individually, by removing constraints that are implied by // other constraints in the clause. for clause in &mut self.clauses { - clause.simplify(db, builder); + clause.simplify(db, storage); } while self.simplify_one_round() { @@ -7485,7 +7533,7 @@ impl SatisfiedClauses { false } - fn display<'db>(&self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> String { + fn display<'db>(&self, db: &'db dyn Db, storage: &ConstraintSetStorage<'db>) -> String { // This is a bit heavy-handed, but we need to output the clauses in a consistent order // even though Salsa IDs are assigned non-deterministically. This Display output is only // used in test cases, so we don't need to over-optimize it. @@ -7496,7 +7544,7 @@ impl SatisfiedClauses { let mut clauses: Vec<_> = self .clauses .iter() - .map(|clause| clause.display(db, builder)) + .map(|clause| clause.display(db, storage)) .collect(); clauses.sort(); clauses.join(" ∨ ") @@ -7507,14 +7555,14 @@ impl<'db> BoundTypeVarInstance<'db> { /// Returns the valid specializations of a typevar. This is used when checking a constraint set /// when this typevar is in inferable position, where we only need _some_ specialization to /// satisfy the constraint set. - fn valid_specializations<'c>( + fn valid_specializations( self, db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, - ) -> ConstraintSet<'db, 'c> { + storage: &mut ConstraintSetStorage<'db>, + ) -> (NodeId, Option) { if self.paramspec_attr(db).is_some() { // P.args and P.kwargs are variadic, and do not have an upper bound or constraints. - return ConstraintSet::always(builder); + return (ALWAYS_TRUE, None); } // For gradual upper bounds and constraints, we are free to choose any materialization that @@ -7528,26 +7576,29 @@ impl<'db> BoundTypeVarInstance<'db> { // that _some_ valid specialization satisfies the constraint set, it's correct for us to // return the range of valid materializations that we can choose from. match self.typevar(db).bound_or_constraints(db) { - None => ConstraintSet::always(builder), + None => (ALWAYS_TRUE, None), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let bound = bound.top_materialization(db); - ConstraintSet::constrain_typevar_upper_bound(db, builder, self, bound) + Constraint::new_node_with_bounds(db, storage, self, None, Some(bound)) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut specializations = ConstraintSet::never(builder); + let mut specializations = ALWAYS_FALSE; + let mut source_order = None; for constraint in constraints.elements(db) { let constraint_lower = constraint.bottom_materialization(db); let constraint_upper = constraint.top_materialization(db); - let constraint = ConstraintSet::constrain_typevar( + let (constraint, constraint_source_order) = Constraint::new_node_with_bounds( db, - builder, + storage, self, - constraint_lower, - constraint_upper, + Some(constraint_lower), + Some(constraint_upper), ); - specializations.union(db, builder, constraint); + specializations = specializations.or(storage, constraint); + source_order = + storage.ordered_source_order(source_order, constraint_source_order); } - specializations + (specializations, source_order) } } } @@ -7565,45 +7616,55 @@ impl<'db> BoundTypeVarInstance<'db> { /// specifies the required specializations, and the iterator will be empty. For a constrained /// typevar, the primary result will include the fully static constraints, and the iterator /// will include an entry for each non-fully-static constraint. - fn required_specializations<'c>( + #[expect(clippy::type_complexity)] + fn required_specializations( self, db: &'db dyn Db, - builder: &'c ConstraintSetBuilder<'db>, - ) -> (ConstraintSet<'db, 'c>, Vec>) { + storage: &mut ConstraintSetStorage<'db>, + ) -> ( + (NodeId, Option), + Vec<(NodeId, Option)>, + ) { // For upper bounds and constraints, we are free to choose any materialization that makes // the check succeed. In non-inferable positions, it is most helpful to choose a // materialization that is as restrictive as possible, since that minimizes the number of // valid specializations that must satisfy the check. We therefore take the bottom // materialization of the bound or constraints. match self.typevar(db).bound_or_constraints(db) { - None => (ConstraintSet::always(builder), Vec::new()), + None => ((ALWAYS_TRUE, None), Vec::new()), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let bound = bound.bottom_materialization(db); ( - ConstraintSet::constrain_typevar_upper_bound(db, builder, self, bound), + Constraint::new_node_with_bounds(db, storage, self, None, Some(bound)), Vec::new(), ) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut non_gradual_constraints = ConstraintSet::never(builder); + let mut non_gradual_constraints = ALWAYS_FALSE; + let mut non_gradual_source_order = None; let mut gradual_constraints = Vec::new(); for constraint in constraints.elements(db) { let constraint_lower = constraint.bottom_materialization(db); let constraint_upper = constraint.top_materialization(db); - let constraint = ConstraintSet::constrain_typevar( + let constraint = Constraint::new_node_with_bounds( db, - builder, + storage, self, - constraint_lower, - constraint_upper, + Some(constraint_lower), + Some(constraint_upper), ); if constraint_lower == constraint_upper { - non_gradual_constraints.union(db, builder, constraint); + non_gradual_constraints = non_gradual_constraints.or(storage, constraint.0); + non_gradual_source_order = + storage.ordered_source_order(non_gradual_source_order, constraint.1); } else { gradual_constraints.push(constraint); } } - (non_gradual_constraints, gradual_constraints) + ( + (non_gradual_constraints, non_gradual_source_order), + gradual_constraints, + ) } } } @@ -7867,11 +7928,12 @@ mod tests { .map_bound_or_constraints(&db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); let type_of_u = SubclassOfType::from(&db, u); let bool_class = KnownClass::Bool.to_class_literal(&db); - let left = ConstraintId::new_with_bounds(&db, &builder, t, Some(type_of_u), None); - let right = ConstraintId::new_with_bounds(&db, &builder, t, Some(bool_class), None); + let mut storage = builder.storage.borrow_mut(); + let left = ConstraintId::new_with_bounds(&db, &mut storage, t, Some(type_of_u), None); + let right = ConstraintId::new_with_bounds(&db, &mut storage, t, Some(bool_class), None); for (left, right) in [(left, right), (right, left)] { - let sequents = SequentMap::for_constraint_pair(&db, &builder, left, right); + let sequents = SequentMap::for_constraint_pair(&db, &mut storage, left, right); assert!( sequents @@ -7880,7 +7942,10 @@ mod tests { .any(|sequent| matches!(sequent, Sequent::SingleImplication { .. })) ); assert!(!SequentMap::pair_cannot_produce_sequents( - &db, &builder, left, right + &db, + &mut storage, + left, + right )); } } @@ -8018,14 +8083,16 @@ mod tests { let bytearray = known_instance(&db, KnownClass::Bytearray); let int_or_str = UnionType::from_two_elements(&db, int, str); let bytes_or_bytearray = UnionType::from_two_elements(&db, bytes, bytearray); - let left = ConstraintId::new_with_bounds(&db, &builder, t, Some(int), Some(int_or_str)); - let right = ConstraintId::new_with_bounds(&db, &builder, t, None, Some(bytes_or_bytearray)); + let mut storage = builder.storage.borrow_mut(); + let left = ConstraintId::new_with_bounds(&db, &mut storage, t, Some(int), Some(int_or_str)); + let right = + ConstraintId::new_with_bounds(&db, &mut storage, t, None, Some(bytes_or_bytearray)); // Check satisfiability against each upper clause before punting on the union-bearing // merged upper bound. The old size heuristic returned `CannotSimplify` here before // discovering that `int` cannot satisfy the second upper clause. assert!(matches!( - left.intersect(&db, &builder, right), + left.intersect(&db, &mut storage, right), IntersectionResult::Disjoint )); } @@ -8035,23 +8102,25 @@ mod tests { let db = setup_db(); let t = create_typevar(&db, "T"); let builder = ConstraintSetBuilder::new(); + let mut storage = builder.storage.borrow_mut(); let t_int = ConstraintId::new( &db, - &builder, + &mut storage, t, Type::Never, KnownClass::Int.to_instance(&db), ); let t_bool = ConstraintId::new( &db, - &builder, + &mut storage, t, Type::Never, KnownClass::Bool.to_instance(&db), ); - assert!(builder.cached_constraint_implies(&db, t_bool, t_int)); - assert!(builder.cached_constraint_implies(&db, t_bool, t_int)); + assert!(storage.cached_constraint_implies(&db, t_bool, t_int)); + assert!(storage.cached_constraint_implies(&db, t_bool, t_int)); + drop(storage); { let storage = builder.storage.borrow(); @@ -8062,8 +8131,10 @@ mod tests { assert_eq!(storage.constraint_implication_cache.len(), 1); } - assert!(!builder.cached_constraint_implies(&db, t_int, t_bool)); - assert!(!builder.cached_constraint_implies(&db, t_int, t_bool)); + let mut storage = builder.storage.borrow_mut(); + assert!(!storage.cached_constraint_implies(&db, t_int, t_bool)); + assert!(!storage.cached_constraint_implies(&db, t_int, t_bool)); + drop(storage); let storage = builder.storage.borrow(); assert_eq!( @@ -8223,14 +8294,8 @@ mod tests { owned.query(|builder, set| { assert!(!set.is_never_satisfied(&db)); assert!(!set.is_never_satisfied(&db)); - assert_eq!( - builder - .storage - .borrow() - .never_satisfied_cache - .get(&set.node), - Some(&false) - ); + let storage = builder.storage.borrow(); + assert_eq!(storage.never_satisfied_cache.get(&set.node), Some(&false)); }); } @@ -8250,7 +8315,8 @@ mod tests { assert_ne!(first.source_order, second.source_order); assert!(!first.is_never_satisfied(&db)); assert!(!second.is_never_satisfied(&db)); - assert_eq!(builder.storage.borrow().never_satisfied_cache.len(), 1); + let storage = builder.storage.borrow(); + assert_eq!(storage.never_satisfied_cache.len(), 1); } #[derive(Clone, Copy)] @@ -8261,9 +8327,9 @@ mod tests { ); impl<'db> PermutedConstraint<'db> { - fn node(self, db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>) -> NodeId { + fn node(self, db: &'db dyn Db, storage: &mut ConstraintSetStorage<'db>) -> NodeId { let PermutedConstraint(typevar, lower, upper) = self; - Constraint::new_node_with_bounds(db, builder, typevar, lower, upper).0 + Constraint::new_node_with_bounds(db, storage, typevar, lower, upper).0 } } @@ -8280,7 +8346,7 @@ mod tests { db: &'db dyn Db, typevars: &[BoundTypeVarInstance<'db>], atoms: &[PermutedConstraint<'db>], - build_bdd: impl Fn(&ConstraintSetBuilder<'db>) -> NodeId, + build_bdd: impl Fn(&mut ConstraintSetStorage<'db>) -> NodeId, expected: impl IntoIterator, ) { let inferable = TypeVarSet::from_typevars(db, typevars.iter().copied()); @@ -8288,12 +8354,13 @@ mod tests { for constraint_order in (0..atoms.len()).permutations(atoms.len()) { let builder = ConstraintSetBuilder::new(); + let mut storage = builder.storage.borrow_mut(); for typevar in typevars { - builder.intern_typevar(db, *typevar); + storage.intern_typevar(db, *typevar); } for index in constraint_order { let PermutedConstraint(typevar, lower, upper) = atoms[index]; - builder.intern_constraint( + storage.intern_constraint( db, Constraint { typevar, @@ -8302,21 +8369,21 @@ mod tests { ); } - let node = build_bdd(&builder); + let node = build_bdd(&mut storage); let source_order = atoms.iter().fold(None, |source_order, atom| { let PermutedConstraint(typevar, lower, upper) = *atom; - let constraint = builder.intern_constraint( + let constraint = storage.intern_constraint( db, Constraint { typevar, bounds: ConstraintBounds::new(lower, upper), }, ); - builder.ordered_source_order( - source_order, - Some(builder.constraint_source_order(constraint)), - ) + let constraint_source_order = storage.constraint_source_order(constraint); + storage.ordered_source_order(source_order, Some(constraint_source_order)) }); + drop(storage); + let set = ConstraintSet::from_node(&builder, node, source_order); let solutions = set.solutions(db, &builder, inferable); let mut merged = FxHashMap::default(); @@ -8385,9 +8452,9 @@ mod tests { &db, &[t], &atoms, - |builder| { - let [str_t, int_t] = atoms.map(|atom| atom.node(&db, builder)); - str_t.or(builder, int_t).and(builder, str_t) + |storage| { + let [str_t, int_t] = atoms.map(|atom| atom.node(&db, storage)); + str_t.or(storage, int_t).and(storage, str_t) }, ["never=false always=false merged=[T=str] paths=[T=str]"], ); @@ -8396,9 +8463,9 @@ mod tests { &db, &[t], &atoms, - |builder| { - let [str_t, int_t] = atoms.map(|atom| atom.node(&db, builder)); - str_t.or(builder, int_t) + |storage| { + let [str_t, int_t] = atoms.map(|atom| atom.node(&db, storage)); + str_t.or(storage, int_t) }, ["never=false always=false merged=[T=str | int] paths=[T=str; T=int]"], ); @@ -8422,10 +8489,10 @@ mod tests { &db, &[t, u], &atoms, - |builder| { - let [str_t, bytes_u, int_t] = atoms.map(|atom| atom.node(&db, builder)); - let compound = str_t.and(builder, bytes_u); - compound.or(builder, int_t).and(builder, compound) + |storage| { + let [str_t, bytes_u, int_t] = atoms.map(|atom| atom.node(&db, storage)); + let compound = str_t.and(storage, bytes_u); + compound.or(storage, int_t).and(storage, compound) }, ["never=false always=false merged=[T=str, U=bytes] paths=[T=str, U=bytes]"], ); @@ -8450,11 +8517,11 @@ mod tests { &db, &[t, u, x], &atoms, - |builder| { - let [str_t, bytes_u, int_x] = atoms.map(|atom| atom.node(&db, builder)); - let early = int_x.and(builder, str_t).and(builder, bytes_u); - let late = bytes_u.and(builder, str_t); - early.or(builder, late) + |storage| { + let [str_t, bytes_u, int_x] = atoms.map(|atom| atom.node(&db, storage)); + let early = int_x.and(storage, str_t).and(storage, bytes_u); + let late = bytes_u.and(storage, str_t); + early.or(storage, late) }, ["never=false always=false merged=[T=str, U=bytes] paths=[T=str, U=bytes]"], ); @@ -8475,11 +8542,11 @@ mod tests { &db, &[t], &atoms, - |builder| { - let [str_t, int_t] = atoms.map(|atom| atom.node(&db, builder)); - let true_path = int_t.and(builder, str_t); - let false_path = int_t.negate(builder).and(builder, str_t); - true_path.or(builder, false_path) + |storage| { + let [str_t, int_t] = atoms.map(|atom| atom.node(&db, storage)); + let true_path = int_t.and(storage, str_t); + let false_path = int_t.negate(storage).and(storage, str_t); + true_path.or(storage, false_path) }, ["never=false always=false merged=[T=str] paths=[T=str]"], ); @@ -8506,13 +8573,13 @@ mod tests { &db, &[t, u, v], &atoms, - |builder| { + |storage| { let [t_list_u, u_int, list_int_t, bytes_v] = - atoms.map(|atom| atom.node(&db, builder)); + atoms.map(|atom| atom.node(&db, storage)); t_list_u - .and(builder, u_int) - .and(builder, list_int_t) - .or(builder, bytes_v) + .and(storage, u_int) + .and(storage, list_int_t) + .or(storage, bytes_v) }, // TODO: All permutations should produce the first result. TDD traversal currently // leaks irrelevant positive constraints onto the `V = bytes` alternative. @@ -8543,12 +8610,12 @@ mod tests { &db, &[t, u], &atoms, - |builder| { - let [t_int, t_str, bytes_u] = atoms.map(|atom| atom.node(&db, builder)); + |storage| { + let [t_int, t_str, bytes_u] = atoms.map(|atom| atom.node(&db, storage)); t_int - .or(builder, t_str) - .negate(builder) - .or(builder, bytes_u) + .or(storage, t_str) + .negate(storage) + .or(storage, bytes_u) }, // TODO: All permutations should produce the first result. A satisfied alternative // should not infer `T` from unrelated positive decisions made earlier in a BDD path. @@ -8578,12 +8645,12 @@ mod tests { &db, &[t, u], &atoms, - |builder| { - let [t_int, t_str, int_t, u_int] = atoms.map(|atom| atom.node(&db, builder)); + |storage| { + let [t_int, t_str, int_t, u_int] = atoms.map(|atom| atom.node(&db, storage)); t_int - .or(builder, t_str) - .and(builder, int_t) - .and(builder, u_int) + .or(storage, t_str) + .and(storage, int_t) + .and(storage, u_int) }, // TODO: Constraint-ID permutations can still change which equivalent upper-bound // intersection is constructed first. @@ -8601,8 +8668,9 @@ mod tests { set: ConstraintSet<'db, 'c>, expected: &str, ) { + let storage = builder.storage.borrow(); let expected = expected.trim_end(); - let actual = set.node.display_graph(db, builder, &"").to_string(); + let actual = set.node.display_graph(db, &storage, &"").to_string(); assert_eq!(expected, actual); } @@ -8857,7 +8925,7 @@ mod tests { fn satisfied<'db>( &mut self, _db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, path: &PathAssignments, ) -> ControlFlow { let result = @@ -8866,10 +8934,10 @@ mod tests { .fold((ALWAYS_TRUE, None), |result, (assignment, _)| { let (node, source_order) = result; let (assignment, assignment_source_order) = - Node::new_satisfied_constraint(builder, *assignment); + Node::new_satisfied_constraint(storage, *assignment); ( - node.and(builder, assignment), - builder.ordered_source_order(source_order, assignment_source_order), + node.and(storage, assignment), + storage.ordered_source_order(source_order, assignment_source_order), ) }); self.result(PathFoldBreak::Satisfied, result) @@ -8878,7 +8946,7 @@ mod tests { fn unsatisfied<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { self.result(PathFoldBreak::Unsatisfied, (ALWAYS_FALSE, None)) @@ -8887,7 +8955,7 @@ mod tests { fn impossible<'db>( &mut self, _db: &'db dyn Db, - _builder: &ConstraintSetBuilder<'db>, + _storage: &mut ConstraintSetStorage<'db>, _path: &PathAssignments, ) -> ControlFlow { self.result(PathFoldBreak::Impossible, (ALWAYS_FALSE, None)) @@ -8896,7 +8964,7 @@ mod tests { fn combine<'db>( &mut self, _db: &'db dyn Db, - builder: &ConstraintSetBuilder<'db>, + storage: &mut ConstraintSetStorage<'db>, if_true: Self::Result, if_uncertain: Self::Result, if_false: Self::Result, @@ -8904,10 +8972,10 @@ mod tests { let (if_true, if_true_source_order) = if_true; let (if_uncertain, if_uncertain_source_order) = if_uncertain; let (if_false, if_false_source_order) = if_false; - let node = if_true.or(builder, if_uncertain).or(builder, if_false); + let node = if_true.or(storage, if_uncertain).or(storage, if_false); let source_order = - builder.ordered_source_order(if_true_source_order, if_uncertain_source_order); - let source_order = builder.ordered_source_order(source_order, if_false_source_order); + storage.ordered_source_order(if_true_source_order, if_uncertain_source_order); + let source_order = storage.ordered_source_order(source_order, if_false_source_order); self.result(PathFoldBreak::Combine, (node, source_order)) } } @@ -8919,7 +8987,10 @@ mod tests { ) -> PathAssignments { match node.node() { Node::AlwaysTrue | Node::AlwaysFalse => PathAssignments::new([]), - Node::Interior(interior) => interior.path_assignments(builder, source_order), + Node::Interior(interior) => { + let mut storage = builder.storage.borrow_mut(); + interior.path_assignments(&mut storage, source_order) + } } } @@ -8936,8 +9007,9 @@ mod tests { // initializer follows the sidecar rather than either TDD traversal or constraint IDs. let set = u_str.and(&db, &builder, || t_int); let path = path_assignments_for(&builder, set.node, set.source_order); + let storage = builder.storage.borrow(); let expected = - [u_str.node, t_int.node].map(|node| builder.interior_node_data(node).constraint); + [u_str.node, t_int.node].map(|node| storage.interior_node_data(node).constraint); let actual: Vec<_> = path.discovered.keys().copied().collect(); assert_eq!(actual, expected); @@ -8989,11 +9061,13 @@ mod tests { ] { let mut path = path_assignments_for(&builder, set.node, set.source_order); let mut fold = ReconstructPathFold { break_at: None }; + let mut storage = builder.storage.borrow_mut(); let ControlFlow::Continue((reconstructed, reconstructed_source_order)) = - path.visit(&db, &builder, set.node, &mut fold) + path.visit(&db, &mut storage, set.node, &mut fold) else { panic!("reconstruction unexpectedly aborted"); }; + drop(storage); let reconstructed = ConstraintSet::from_node(&builder, reconstructed, reconstructed_source_order); assert!( @@ -9026,17 +9100,19 @@ mod tests { let mut aborting_fold = ReconstructPathFold { break_at: Some(break_at), }; + let mut storage = builder.storage.borrow_mut(); assert_eq!( - path.visit(&db, &builder, set.node, &mut aborting_fold), + path.visit(&db, &mut storage, set.node, &mut aborting_fold), ControlFlow::Break(break_at) ); let mut completing_fold = ReconstructPathFold { break_at: None }; let ControlFlow::Continue((reconstructed, reconstructed_source_order)) = - path.visit(&db, &builder, set.node, &mut completing_fold) + path.visit(&db, &mut storage, set.node, &mut completing_fold) else { panic!("reconstruction unexpectedly aborted after {break_at:?}"); }; + drop(storage); let reconstructed = ConstraintSet::from_node(&builder, reconstructed, reconstructed_source_order); assert!( @@ -9093,7 +9169,9 @@ mod tests { let combined = t_int.and(&db, &builder, || u_str); for original in [t_int, combined] { - let original_source_order_count = builder.storage.borrow().source_orders.len(); + let storage = builder.storage.borrow(); + let original_source_order_count = storage.source_orders.len(); + drop(storage); let intersection = original.and(&db, &builder, || original); let union = original.or(&db, &builder, || original); @@ -9101,10 +9179,8 @@ mod tests { assert_eq!(intersection.source_order, original.source_order); assert_eq!(union.node, original.node); assert_eq!(union.source_order, original.source_order); - assert_eq!( - builder.storage.borrow().source_orders.len(), - original_source_order_count - ); + let storage = builder.storage.borrow(); + assert_eq!(storage.source_orders.len(), original_source_order_count); } } @@ -9220,22 +9296,26 @@ mod tests { ) }; - let existing_constraint = builder.interior_node_data(set.node).constraint; + let mut storage = builder.storage.borrow_mut(); + let existing_constraint = storage.interior_node_data(set.node).constraint; assert_eq!( - Some(builder.constraint_source_order(existing_constraint)), + Some(storage.constraint_source_order(existing_constraint)), set.source_order ); + drop(storage); let w = create_typevar(&db, "W"); let w_str = create_constraint(&db, builder, w, KnownClass::Str); + let mut storage = builder.storage.borrow_mut(); let new_constraint = w_str .node - .root_constraint(builder) + .root_constraint(&storage) .expect("new constraint should be nonterminal"); assert!(w_str.node.index() >= node_split); assert!(new_constraint.index() >= constraint_split); - assert!(builder.typevar_id(&db, w).index() >= typevar_split); + assert!(storage.typevar_id(&db, w).index() >= typevar_split); + drop(storage); assert!( w_str .source_order From 80790b348b5188e7fc253665540f442c6ec7dd05 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:54:42 -0400 Subject: [PATCH 157/390] Bump 0.16.1 (#27330) --- CHANGELOG.md | 61 ++++++++++++++++++ Cargo.lock | 74 +++++++++++----------- Cargo.toml | 72 +++++++++++----------- README.md | 6 +- crates/ruff/Cargo.toml | 2 +- crates/ruff/README.md | 2 +- crates/ruff_annotate_snippets/Cargo.toml | 2 +- crates/ruff_cache/Cargo.toml | 2 +- crates/ruff_cache/README.md | 4 +- crates/ruff_db/Cargo.toml | 2 +- crates/ruff_db/README.md | 4 +- crates/ruff_diagnostics/Cargo.toml | 2 +- crates/ruff_diagnostics/README.md | 4 +- crates/ruff_formatter/Cargo.toml | 2 +- crates/ruff_formatter/README.md | 4 +- crates/ruff_graph/Cargo.toml | 2 +- crates/ruff_graph/README.md | 4 +- crates/ruff_index/Cargo.toml | 2 +- crates/ruff_index/README.md | 4 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_linter/README.md | 4 +- crates/ruff_macros/Cargo.toml | 2 +- crates/ruff_macros/README.md | 4 +- crates/ruff_markdown/Cargo.toml | 2 +- crates/ruff_markdown/README.md | 4 +- crates/ruff_memory_usage/Cargo.toml | 2 +- crates/ruff_memory_usage/README.md | 4 +- crates/ruff_notebook/Cargo.toml | 2 +- crates/ruff_notebook/README.md | 4 +- crates/ruff_options_metadata/Cargo.toml | 2 +- crates/ruff_options_metadata/README.md | 4 +- crates/ruff_python_ast/Cargo.toml | 2 +- crates/ruff_python_ast/README.md | 4 +- crates/ruff_python_codegen/Cargo.toml | 2 +- crates/ruff_python_codegen/README.md | 4 +- crates/ruff_python_formatter/Cargo.toml | 2 +- crates/ruff_python_formatter/README.md | 4 +- crates/ruff_python_importer/Cargo.toml | 2 +- crates/ruff_python_importer/README.md | 4 +- crates/ruff_python_index/Cargo.toml | 2 +- crates/ruff_python_index/README.md | 4 +- crates/ruff_python_literal/Cargo.toml | 2 +- crates/ruff_python_literal/README.md | 4 +- crates/ruff_python_parser/Cargo.toml | 2 +- crates/ruff_python_parser/README.md | 4 +- crates/ruff_python_semantic/Cargo.toml | 2 +- crates/ruff_python_semantic/README.md | 4 +- crates/ruff_python_stdlib/Cargo.toml | 2 +- crates/ruff_python_stdlib/README.md | 4 +- crates/ruff_python_trivia/Cargo.toml | 2 +- crates/ruff_python_trivia/README.md | 4 +- crates/ruff_ranged_value/Cargo.toml | 2 +- crates/ruff_ranged_value/README.md | 4 +- crates/ruff_server/Cargo.toml | 2 +- crates/ruff_server/README.md | 4 +- crates/ruff_source_file/Cargo.toml | 2 +- crates/ruff_source_file/README.md | 4 +- crates/ruff_text_size/Cargo.toml | 2 +- crates/ruff_text_size/README.md | 4 +- crates/ruff_wasm/Cargo.toml | 2 +- crates/ruff_wasm/README.md | 4 +- crates/ruff_workspace/Cargo.toml | 2 +- crates/ruff_workspace/README.md | 4 +- crates/ty_combine/Cargo.toml | 2 +- crates/ty_combine/README.md | 4 +- crates/ty_module_resolver/Cargo.toml | 2 +- crates/ty_module_resolver/README.md | 4 +- crates/ty_python_core/Cargo.toml | 2 +- crates/ty_python_core/README.md | 4 +- crates/ty_python_semantic/Cargo.toml | 2 +- crates/ty_python_semantic/README.md | 4 +- crates/ty_site_packages/Cargo.toml | 2 +- crates/ty_site_packages/README.md | 4 +- crates/ty_static/Cargo.toml | 2 +- crates/ty_static/README.md | 4 +- crates/ty_vendored/Cargo.toml | 2 +- docs/formatter.md | 2 +- docs/integrations.md | 8 +-- docs/tutorial.md | 2 +- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- uv.lock | 78 ++++++++++++------------ 82 files changed, 290 insertions(+), 229 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf61cd779d..46fdebb3b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,66 @@ # Changelog +## 0.16.1 + +Released on 2026-07-30. + +### Preview features + +- Add an option to opt out of human-readable names ([#27160](https://github.com/astral-sh/ruff/pull/27160)) +- \[`flake8-pytest-style`\] Make fixes safe by default and unsafe only when comments are present (`PT018`) ([#27201](https://github.com/astral-sh/ruff/pull/27201)) +- \[`pyupgrade`\] Skip fix when a defaulted `TypeVar` precedes a non-defaulted one (`UP040`, `UP046`, `UP047`) ([#27133](https://github.com/astral-sh/ruff/pull/27133)) +- \[`ruff`\] Fix false positive with unpacked arguments (`RUF065`) ([#26959](https://github.com/astral-sh/ruff/pull/26959)) + +### Bug fixes + +- Bump `gen-lsp-types` to gracefully handle unknown enumeration values in LSP messages ([#27230](https://github.com/astral-sh/ruff/pull/27230)) +- \[`flake8-bugbear`\] Mark `range` as immutable (`B008`) ([#27247](https://github.com/astral-sh/ruff/pull/27247)) +- \[`flake8-comprehensions`\] NFKC-normalize keyword names in `C408` fix ([#26813](https://github.com/astral-sh/ruff/pull/26813)) +- \[`flake8-return`\] Fix false positive when variable is read in `finally` clause (`RET504`) ([#25441](https://github.com/astral-sh/ruff/pull/25441)) +- \[`pydocstyle`\] Skip section detection inside RST directive bodies (`D214`, `D405`, `D413`) ([#23635](https://github.com/astral-sh/ruff/pull/23635)) +- \[`refurb`\] Parenthesize `yield` arguments in the `FURB192` fix ([#27192](https://github.com/astral-sh/ruff/pull/27192)) + +### Rule changes + +- \[`flake8-pytest-style`\] Mark `PT022` fixes as unsafe ([#26440](https://github.com/astral-sh/ruff/pull/26440)) +- \[`refurb`\] Mark fixes that remove unknown separators as unsafe (`FURB105`) ([#27200](https://github.com/astral-sh/ruff/pull/27200)) + +### Server + +- Fix indexing of excluded nested Ruff workspaces ([#27303](https://github.com/astral-sh/ruff/pull/27303)) +- Lint TOML files in the LSP ([#26862](https://github.com/astral-sh/ruff/pull/26862)) + +### Documentation + +- Cover `pycon` Markdown formatting ([#27153](https://github.com/astral-sh/ruff/pull/27153)) +- \[`flake8-bandit`\] Document `TYPE_CHECKING` exception (`S101`) ([#27004](https://github.com/astral-sh/ruff/pull/27004)) +- \[`flake8-import-conventions`\] Document that `extend-aliases` can override default aliases ([#27191](https://github.com/astral-sh/ruff/pull/27191)) +- \[`pylint`\] Add missing fix safety gotchas for `non-augmented-assignment` (`PLR6104`) ([#27250](https://github.com/astral-sh/ruff/pull/27250)) + +### Other changes + +- Reduce syntax error noise by swallowing dedents like indents ([#27170](https://github.com/astral-sh/ruff/pull/27170)) +- Vendor latest annotate-snippets ([#27033](https://github.com/astral-sh/ruff/pull/27033)) + +### Contributors + +- [@bxff](https://github.com/bxff) +- [@anishgirianish](https://github.com/anishgirianish) +- [@Avasam](https://github.com/Avasam) +- [@epage](https://github.com/epage) +- [@LHMQ878](https://github.com/LHMQ878) +- [@MichaReiser](https://github.com/MichaReiser) +- [@ntBre](https://github.com/ntBre) +- [@HarshalPatel1972](https://github.com/HarshalPatel1972) +- [@mjpieters](https://github.com/mjpieters) +- [@joshuavetos](https://github.com/joshuavetos) +- [@jesco-absolute](https://github.com/jesco-absolut) +- [@vidigoat](https://github.com/vidigoat) +- [@baltasarblanco](https://github.com/baltasarblanco) +- [@ribru17](https://github.com/ribru17) +- [@oh-summy](https://github.com/oh-summy) +- [@Jayashanker-Padishala](https://github.com/Jayashanker-Padishala) + ## 0.16.0 Released on 2026-07-23. diff --git a/Cargo.lock b/Cargo.lock index 8259e1f6f9..107d64d4ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3065,7 +3065,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.16.0" +version = "0.16.1" dependencies = [ "anyhow", "argfile", @@ -3129,7 +3129,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anstream 1.0.0", "anstyle", @@ -3167,7 +3167,7 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.6" +version = "0.0.7" dependencies = [ "char_str", "filetime", @@ -3181,7 +3181,7 @@ dependencies = [ [[package]] name = "ruff_db" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anstyle", "arc-swap", @@ -3272,7 +3272,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.6" +version = "0.0.7" dependencies = [ "get-size2", "is-macro", @@ -3282,7 +3282,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.6" +version = "0.0.7" dependencies = [ "drop_bomb", "ruff_cache", @@ -3298,7 +3298,7 @@ dependencies = [ [[package]] name = "ruff_graph" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anyhow", "clap", @@ -3319,7 +3319,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.6" +version = "0.0.7" dependencies = [ "get-size2", "ruff_macros", @@ -3329,7 +3329,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.16.0" +version = "0.16.1" dependencies = [ "aho-corasick", "anyhow", @@ -3392,7 +3392,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.6" +version = "0.0.7" dependencies = [ "heck", "itertools 0.15.0", @@ -3405,7 +3405,7 @@ dependencies = [ [[package]] name = "ruff_markdown" -version = "0.0.6" +version = "0.0.7" dependencies = [ "insta", "regex", @@ -3436,14 +3436,14 @@ dependencies = [ [[package]] name = "ruff_memory_usage" -version = "0.0.6" +version = "0.0.7" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anyhow", "rand 0.10.2", @@ -3459,14 +3459,14 @@ dependencies = [ [[package]] name = "ruff_options_metadata" -version = "0.0.6" +version = "0.0.7" dependencies = [ "serde", ] [[package]] name = "ruff_python_ast" -version = "0.0.6" +version = "0.0.7" dependencies = [ "aho-corasick", "arrayvec", @@ -3503,7 +3503,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.6" +version = "0.0.7" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -3515,7 +3515,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anyhow", "clap", @@ -3548,7 +3548,7 @@ dependencies = [ [[package]] name = "ruff_python_importer" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anyhow", "insta", @@ -3563,7 +3563,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.6" +version = "0.0.7" dependencies = [ "ruff_python_ast", "ruff_python_parser", @@ -3574,7 +3574,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.6" +version = "0.0.7" dependencies = [ "bitflags 2.13.1", "icu_properties", @@ -3584,7 +3584,7 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -3613,7 +3613,7 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.6" +version = "0.0.7" dependencies = [ "bitflags 2.13.1", "insta", @@ -3634,7 +3634,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.6" +version = "0.0.7" dependencies = [ "bitflags 2.13.1", "unicode-ident", @@ -3642,7 +3642,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" -version = "0.0.6" +version = "0.0.7" dependencies = [ "itertools 0.15.0", "ruff_source_file", @@ -3663,7 +3663,7 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.6" +version = "0.0.7" dependencies = [ "get-size2", "ruff_db", @@ -3675,7 +3675,7 @@ dependencies = [ [[package]] name = "ruff_server" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anyhow", "crossbeam", @@ -3718,7 +3718,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.6" +version = "0.0.7" dependencies = [ "get-size2", "memchr", @@ -3728,7 +3728,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.6" +version = "0.0.7" dependencies = [ "get-size2", "schemars", @@ -3739,7 +3739,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.16.0" +version = "0.16.1" dependencies = [ "console_error_panic_hook", "console_log", @@ -3766,7 +3766,7 @@ dependencies = [ [[package]] name = "ruff_workspace" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anyhow", "colored", @@ -4633,7 +4633,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.6" +version = "0.0.7" dependencies = [ "ordermap", "ruff_db", @@ -4715,7 +4715,7 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anyhow", "camino", @@ -4788,7 +4788,7 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -4822,7 +4822,7 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.6" +version = "0.0.7" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -4916,7 +4916,7 @@ dependencies = [ [[package]] name = "ty_site_packages" -version = "0.0.6" +version = "0.0.7" dependencies = [ "camino", "colored", @@ -4937,7 +4937,7 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.6" +version = "0.0.7" dependencies = [ "ruff_macros", ] @@ -4969,7 +4969,7 @@ dependencies = [ [[package]] name = "ty_vendored" -version = "0.0.6" +version = "0.0.7" dependencies = [ "path-slash", "ruff_db", diff --git a/Cargo.toml b/Cargo.toml index 2dd480e63c..1f496bc91b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,51 +14,51 @@ license = "MIT" [workspace.dependencies] char_str = { version = "0.0.2" } -ruff = { version = "0.16.0", path = "crates/ruff" } -ruff_annotate_snippets = { version = "0.0.6", path = "crates/ruff_annotate_snippets" } -ruff_cache = { version = "0.0.6", path = "crates/ruff_cache" } -ruff_db = { version = "0.0.6", path = "crates/ruff_db", default-features = false } -ruff_diagnostics = { version = "0.0.6", path = "crates/ruff_diagnostics" } -ruff_formatter = { version = "0.0.6", path = "crates/ruff_formatter" } -ruff_graph = { version = "0.0.6", path = "crates/ruff_graph" } -ruff_index = { version = "0.0.6", path = "crates/ruff_index" } -ruff_linter = { version = "0.16.0", path = "crates/ruff_linter" } -ruff_macros = { version = "0.0.6", path = "crates/ruff_macros" } -ruff_markdown = { version = "0.0.6", path = "crates/ruff_markdown" } -ruff_memory_usage = { version = "0.0.6", path = "crates/ruff_memory_usage" } -ruff_notebook = { version = "0.0.6", path = "crates/ruff_notebook" } -ruff_options_metadata = { version = "0.0.6", path = "crates/ruff_options_metadata" } -ruff_python_ast = { version = "0.0.6", path = "crates/ruff_python_ast" } -ruff_python_codegen = { version = "0.0.6", path = "crates/ruff_python_codegen" } -ruff_python_formatter = { version = "0.0.6", path = "crates/ruff_python_formatter" } -ruff_python_importer = { version = "0.0.6", path = "crates/ruff_python_importer" } -ruff_python_index = { version = "0.0.6", path = "crates/ruff_python_index" } -ruff_python_literal = { version = "0.0.6", path = "crates/ruff_python_literal" } -ruff_python_parser = { version = "0.0.6", path = "crates/ruff_python_parser" } -ruff_python_semantic = { version = "0.0.6", path = "crates/ruff_python_semantic" } -ruff_python_stdlib = { version = "0.0.6", path = "crates/ruff_python_stdlib" } -ruff_python_trivia = { version = "0.0.6", path = "crates/ruff_python_trivia" } -ruff_server = { version = "0.0.6", path = "crates/ruff_server" } -ruff_source_file = { version = "0.0.6", path = "crates/ruff_source_file" } +ruff = { version = "0.16.1", path = "crates/ruff" } +ruff_annotate_snippets = { version = "0.0.7", path = "crates/ruff_annotate_snippets" } +ruff_cache = { version = "0.0.7", path = "crates/ruff_cache" } +ruff_db = { version = "0.0.7", path = "crates/ruff_db", default-features = false } +ruff_diagnostics = { version = "0.0.7", path = "crates/ruff_diagnostics" } +ruff_formatter = { version = "0.0.7", path = "crates/ruff_formatter" } +ruff_graph = { version = "0.0.7", path = "crates/ruff_graph" } +ruff_index = { version = "0.0.7", path = "crates/ruff_index" } +ruff_linter = { version = "0.16.1", path = "crates/ruff_linter" } +ruff_macros = { version = "0.0.7", path = "crates/ruff_macros" } +ruff_markdown = { version = "0.0.7", path = "crates/ruff_markdown" } +ruff_memory_usage = { version = "0.0.7", path = "crates/ruff_memory_usage" } +ruff_notebook = { version = "0.0.7", path = "crates/ruff_notebook" } +ruff_options_metadata = { version = "0.0.7", path = "crates/ruff_options_metadata" } +ruff_python_ast = { version = "0.0.7", path = "crates/ruff_python_ast" } +ruff_python_codegen = { version = "0.0.7", path = "crates/ruff_python_codegen" } +ruff_python_formatter = { version = "0.0.7", path = "crates/ruff_python_formatter" } +ruff_python_importer = { version = "0.0.7", path = "crates/ruff_python_importer" } +ruff_python_index = { version = "0.0.7", path = "crates/ruff_python_index" } +ruff_python_literal = { version = "0.0.7", path = "crates/ruff_python_literal" } +ruff_python_parser = { version = "0.0.7", path = "crates/ruff_python_parser" } +ruff_python_semantic = { version = "0.0.7", path = "crates/ruff_python_semantic" } +ruff_python_stdlib = { version = "0.0.7", path = "crates/ruff_python_stdlib" } +ruff_python_trivia = { version = "0.0.7", path = "crates/ruff_python_trivia" } +ruff_server = { version = "0.0.7", path = "crates/ruff_server" } +ruff_source_file = { version = "0.0.7", path = "crates/ruff_source_file" } ruff_mdtest = { path = "crates/ruff_mdtest" } -ruff_ranged_value = { version = "0.0.6", path = "crates/ruff_ranged_value" } -ruff_text_size = { version = "0.0.6", path = "crates/ruff_text_size" } -ruff_workspace = { version = "0.0.6", path = "crates/ruff_workspace" } +ruff_ranged_value = { version = "0.0.7", path = "crates/ruff_ranged_value" } +ruff_text_size = { version = "0.0.7", path = "crates/ruff_text_size" } +ruff_workspace = { version = "0.0.7", path = "crates/ruff_workspace" } ty = { path = "crates/ty" } -ty_combine = { version = "0.0.6", path = "crates/ty_combine" } +ty_combine = { version = "0.0.7", path = "crates/ty_combine" } ty_completion_bench = { path = "crates/ty_completion_bench" } ty_completion_eval = { path = "crates/ty_completion_eval" } ty_ide = { path = "crates/ty_ide" } -ty_module_resolver = { version = "0.0.6", path = "crates/ty_module_resolver" } +ty_module_resolver = { version = "0.0.7", path = "crates/ty_module_resolver" } ty_project = { path = "crates/ty_project", default-features = false } -ty_python_semantic = { version = "0.0.6", path = "crates/ty_python_semantic" } -ty_python_core = { version = "0.0.6", path = "crates/ty_python_core" } +ty_python_semantic = { version = "0.0.7", path = "crates/ty_python_semantic" } +ty_python_core = { version = "0.0.7", path = "crates/ty_python_core" } ty_server = { path = "crates/ty_server" } -ty_site_packages = { version = "0.0.6", path = "crates/ty_site_packages" } -ty_static = { version = "0.0.6", path = "crates/ty_static" } +ty_site_packages = { version = "0.0.7", path = "crates/ty_site_packages" } +ty_static = { version = "0.0.7", path = "crates/ty_static" } ty_test = { path = "crates/ty_test" } -ty_vendored = { version = "0.0.6", path = "crates/ty_vendored" } +ty_vendored = { version = "0.0.7", path = "crates/ty_vendored" } mdtest = { path = "crates/mdtest" } diff --git a/README.md b/README.md index 0d02aea570..21fc902fd5 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,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.16.0/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.16.0/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.16.1/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.16.1/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -186,7 +186,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.16.0 + rev: v0.16.1 hooks: # Run the linter. - id: ruff-check diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 825c93c37e..e98d2607ec 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.16.0" +version = "0.16.1" description = "An extremely fast Python linter and code formatter" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff/README.md b/crates/ruff/README.md index 569e763e15..c0029a5155 100644 --- a/crates/ruff/README.md +++ b/crates/ruff/README.md @@ -10,7 +10,7 @@ See the [documentation](https://docs.astral.sh/ruff/) or This crate is the entry point to the Ruff command-line interface. The Rust API exposed here is not considered public interface. -This is version 0.16.0. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff). +This is version 0.16.1. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff). The following Ruff workspace members are also available: diff --git a/crates/ruff_annotate_snippets/Cargo.toml b/crates/ruff_annotate_snippets/Cargo.toml index b84edcd631..0891b14e69 100644 --- a/crates/ruff_annotate_snippets/Cargo.toml +++ b/crates/ruff_annotate_snippets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_annotate_snippets" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cache/Cargo.toml b/crates/ruff_cache/Cargo.toml index a54bfa2828..410ee8004f 100644 --- a/crates/ruff_cache/Cargo.toml +++ b/crates/ruff_cache/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cache" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cache/README.md b/crates/ruff_cache/README.md index 4af95989f4..e43c934cc7 100644 --- a/crates/ruff_cache/README.md +++ b/crates/ruff_cache/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_cache). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_cache). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_db/Cargo.toml b/crates/ruff_db/Cargo.toml index 61874819dc..20202939b7 100644 --- a/crates/ruff_db/Cargo.toml +++ b/crates/ruff_db/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_db" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_db/README.md b/crates/ruff_db/README.md index 7b2ed316a7..32f39c655d 100644 --- a/crates/ruff_db/README.md +++ b/crates/ruff_db/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_db). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_db). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_diagnostics/Cargo.toml b/crates/ruff_diagnostics/Cargo.toml index 9421b7233f..93d4942f69 100644 --- a/crates/ruff_diagnostics/Cargo.toml +++ b/crates/ruff_diagnostics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_diagnostics" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_diagnostics/README.md b/crates/ruff_diagnostics/README.md index 2d7b1de2e2..89721632fb 100644 --- a/crates/ruff_diagnostics/README.md +++ b/crates/ruff_diagnostics/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_diagnostics). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_diagnostics). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_formatter/Cargo.toml b/crates/ruff_formatter/Cargo.toml index 1fa86bdf38..636ee9a116 100644 --- a/crates/ruff_formatter/Cargo.toml +++ b/crates/ruff_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_formatter" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_formatter/README.md b/crates/ruff_formatter/README.md index 58590c2812..71ec8676cf 100644 --- a/crates/ruff_formatter/README.md +++ b/crates/ruff_formatter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_formatter). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_graph/Cargo.toml b/crates/ruff_graph/Cargo.toml index 60454d4910..ba4e1ecd7c 100644 --- a/crates/ruff_graph/Cargo.toml +++ b/crates/ruff_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_graph" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" edition.workspace = true rust-version.workspace = true diff --git a/crates/ruff_graph/README.md b/crates/ruff_graph/README.md index d2672061e9..a3acdc1867 100644 --- a/crates/ruff_graph/README.md +++ b/crates/ruff_graph/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_graph). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_graph). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_index/Cargo.toml b/crates/ruff_index/Cargo.toml index 5bc2fe3818..8090d7ddf5 100644 --- a/crates/ruff_index/Cargo.toml +++ b/crates/ruff_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_index" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_index/README.md b/crates/ruff_index/README.md index c57ffb1aa0..2b08a67721 100644 --- a/crates/ruff_index/README.md +++ b/crates/ruff_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_index). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 70234e23f2..335592119d 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.16.0" +version = "0.16.1" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/README.md b/crates/ruff_linter/README.md index 06a64f5420..280497f7be 100644 --- a/crates/ruff_linter/README.md +++ b/crates/ruff_linter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.16.0) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_linter). +This version (0.16.1) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_linter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_macros/Cargo.toml b/crates/ruff_macros/Cargo.toml index 70456d34f7..7ea014674c 100644 --- a/crates/ruff_macros/Cargo.toml +++ b/crates/ruff_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_macros" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_macros/README.md b/crates/ruff_macros/README.md index 3686c4d081..d7f8afde7c 100644 --- a/crates/ruff_macros/README.md +++ b/crates/ruff_macros/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_macros). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_macros). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_markdown/Cargo.toml b/crates/ruff_markdown/Cargo.toml index e5c510c59c..543cca4eaa 100644 --- a/crates/ruff_markdown/Cargo.toml +++ b/crates/ruff_markdown/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_markdown" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ruff_markdown/README.md b/crates/ruff_markdown/README.md index 52234fd0f7..7b3358ede1 100644 --- a/crates/ruff_markdown/README.md +++ b/crates/ruff_markdown/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_markdown). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_markdown). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_memory_usage/Cargo.toml b/crates/ruff_memory_usage/Cargo.toml index 6e4befc136..e032ac417b 100644 --- a/crates/ruff_memory_usage/Cargo.toml +++ b/crates/ruff_memory_usage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_memory_usage" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_memory_usage/README.md b/crates/ruff_memory_usage/README.md index 1717f53d89..4b5a36084e 100644 --- a/crates/ruff_memory_usage/README.md +++ b/crates/ruff_memory_usage/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_memory_usage). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_memory_usage). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_notebook/Cargo.toml b/crates/ruff_notebook/Cargo.toml index 6c073f018f..3993be1563 100644 --- a/crates/ruff_notebook/Cargo.toml +++ b/crates/ruff_notebook/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_notebook" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_notebook/README.md b/crates/ruff_notebook/README.md index 5c0d10bc01..d17feb9eee 100644 --- a/crates/ruff_notebook/README.md +++ b/crates/ruff_notebook/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_notebook). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_notebook). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_options_metadata/Cargo.toml b/crates/ruff_options_metadata/Cargo.toml index db12da1970..c910fb71f1 100644 --- a/crates/ruff_options_metadata/Cargo.toml +++ b/crates/ruff_options_metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_options_metadata" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_options_metadata/README.md b/crates/ruff_options_metadata/README.md index e41074cf15..707687360b 100644 --- a/crates/ruff_options_metadata/README.md +++ b/crates/ruff_options_metadata/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_options_metadata). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_options_metadata). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_ast/Cargo.toml b/crates/ruff_python_ast/Cargo.toml index 4bf7791f16..9bbb0a9b14 100644 --- a/crates/ruff_python_ast/Cargo.toml +++ b/crates/ruff_python_ast/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_ast" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_ast/README.md b/crates/ruff_python_ast/README.md index 1fc24e18ef..4f14de15d6 100644 --- a/crates/ruff_python_ast/README.md +++ b/crates/ruff_python_ast/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_ast). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_ast). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_codegen/Cargo.toml b/crates/ruff_python_codegen/Cargo.toml index f59bb46d40..8c2317d2e5 100644 --- a/crates/ruff_python_codegen/Cargo.toml +++ b/crates/ruff_python_codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_codegen" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_codegen/README.md b/crates/ruff_python_codegen/README.md index 9a9f81bafd..9a75bff2a6 100644 --- a/crates/ruff_python_codegen/README.md +++ b/crates/ruff_python_codegen/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_codegen). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_codegen). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index c5eb9472f5..0d55fdf608 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_formatter" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index 0bb7b2eb28..b1fa7818d3 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -32,8 +32,8 @@ Head to [The Ruff Formatter](https://docs.astral.sh/ruff/formatter/) for usage i This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_formatter). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_importer/Cargo.toml b/crates/ruff_python_importer/Cargo.toml index 9da5d1f818..98a90c8741 100644 --- a/crates/ruff_python_importer/Cargo.toml +++ b/crates/ruff_python_importer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_importer" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_importer/README.md b/crates/ruff_python_importer/README.md index f8a888d0a4..8559e159df 100644 --- a/crates/ruff_python_importer/README.md +++ b/crates/ruff_python_importer/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_importer). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_importer). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_index/Cargo.toml b/crates/ruff_python_index/Cargo.toml index 1e5fd74c58..826104dc66 100644 --- a/crates/ruff_python_index/Cargo.toml +++ b/crates/ruff_python_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_index" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_index/README.md b/crates/ruff_python_index/README.md index 3d7881599b..ad1f6a79ce 100644 --- a/crates/ruff_python_index/README.md +++ b/crates/ruff_python_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_index). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_literal/Cargo.toml b/crates/ruff_python_literal/Cargo.toml index 63dcc0a110..094de8ef54 100644 --- a/crates/ruff_python_literal/Cargo.toml +++ b/crates/ruff_python_literal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_literal" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_literal/README.md b/crates/ruff_python_literal/README.md index 2e2d89c756..2a433bf5b0 100644 --- a/crates/ruff_python_literal/README.md +++ b/crates/ruff_python_literal/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_literal). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_literal). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_parser/Cargo.toml b/crates/ruff_python_parser/Cargo.toml index fae77341b7..a23e4f45e7 100644 --- a/crates/ruff_python_parser/Cargo.toml +++ b/crates/ruff_python_parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_parser" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_parser/README.md b/crates/ruff_python_parser/README.md index 33acbf61cc..c393a8ba57 100644 --- a/crates/ruff_python_parser/README.md +++ b/crates/ruff_python_parser/README.md @@ -19,8 +19,8 @@ Refer to the [contributing guidelines](./CONTRIBUTING.md) to get started and Git This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_parser). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_parser). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_semantic/Cargo.toml b/crates/ruff_python_semantic/Cargo.toml index 0b50dd1a01..ac12f2ec05 100644 --- a/crates/ruff_python_semantic/Cargo.toml +++ b/crates/ruff_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_semantic" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_semantic/README.md b/crates/ruff_python_semantic/README.md index 3c8a5c4540..b82ca3e975 100644 --- a/crates/ruff_python_semantic/README.md +++ b/crates/ruff_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_semantic). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_stdlib/Cargo.toml b/crates/ruff_python_stdlib/Cargo.toml index 889fb2e14a..a721b52c69 100644 --- a/crates/ruff_python_stdlib/Cargo.toml +++ b/crates/ruff_python_stdlib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_stdlib" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_stdlib/README.md b/crates/ruff_python_stdlib/README.md index 4609d18a54..19d792f380 100644 --- a/crates/ruff_python_stdlib/README.md +++ b/crates/ruff_python_stdlib/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_stdlib). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_stdlib). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_trivia/Cargo.toml b/crates/ruff_python_trivia/Cargo.toml index a765f27a86..f302c8118f 100644 --- a/crates/ruff_python_trivia/Cargo.toml +++ b/crates/ruff_python_trivia/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_trivia" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_trivia/README.md b/crates/ruff_python_trivia/README.md index e587085bc7..2a5d845526 100644 --- a/crates/ruff_python_trivia/README.md +++ b/crates/ruff_python_trivia/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_python_trivia). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_trivia). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_ranged_value/Cargo.toml b/crates/ruff_ranged_value/Cargo.toml index 69fb3ef962..2323710092 100644 --- a/crates/ruff_ranged_value/Cargo.toml +++ b/crates/ruff_ranged_value/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_ranged_value" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_ranged_value/README.md b/crates/ruff_ranged_value/README.md index cbcb301578..e4a3197d87 100644 --- a/crates/ruff_ranged_value/README.md +++ b/crates/ruff_ranged_value/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_ranged_value). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_ranged_value). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index fc48939e38..a5819c7340 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_server" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_server/README.md b/crates/ruff_server/README.md index 47f7c363d0..54f7b07aec 100644 --- a/crates/ruff_server/README.md +++ b/crates/ruff_server/README.md @@ -24,8 +24,8 @@ You can also join us on [**Discord**](https://discord.com/invite/astral-sh). This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_server). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_server). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_source_file/Cargo.toml b/crates/ruff_source_file/Cargo.toml index 602e059d62..b8921cac60 100644 --- a/crates/ruff_source_file/Cargo.toml +++ b/crates/ruff_source_file/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_source_file" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_source_file/README.md b/crates/ruff_source_file/README.md index 9f71dd12a9..e5f56b0bff 100644 --- a/crates/ruff_source_file/README.md +++ b/crates/ruff_source_file/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_source_file). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_source_file). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_text_size/Cargo.toml b/crates/ruff_text_size/Cargo.toml index 6a840ea237..8842992bd2 100644 --- a/crates/ruff_text_size/Cargo.toml +++ b/crates/ruff_text_size/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_text_size" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_text_size/README.md b/crates/ruff_text_size/README.md index 5d9cfabf2f..308ca891ee 100644 --- a/crates/ruff_text_size/README.md +++ b/crates/ruff_text_size/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_text_size). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_text_size). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index cf0d777f6d..845b99f7ae 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.16.0" +version = "0.16.1" description = "WebAssembly bindings for Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/README.md b/crates/ruff_wasm/README.md index 27ae8a6804..63a189fb46 100644 --- a/crates/ruff_wasm/README.md +++ b/crates/ruff_wasm/README.md @@ -55,8 +55,8 @@ const formatted = workspace.format(exampleDocument); This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.16.0) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_wasm). +This version (0.16.1) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_wasm). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_workspace/Cargo.toml b/crates/ruff_workspace/Cargo.toml index 6296e2c2e6..173a383e98 100644 --- a/crates/ruff_workspace/Cargo.toml +++ b/crates/ruff_workspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_workspace" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_workspace/README.md b/crates/ruff_workspace/README.md index db10bd6d95..7bf597aebc 100644 --- a/crates/ruff_workspace/README.md +++ b/crates/ruff_workspace/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ruff_workspace). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_workspace). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_combine/Cargo.toml b/crates/ty_combine/Cargo.toml index 9dd6d17dfe..bb7838fd7d 100644 --- a/crates/ty_combine/Cargo.toml +++ b/crates/ty_combine/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_combine" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" edition.workspace = true rust-version.workspace = true diff --git a/crates/ty_combine/README.md b/crates/ty_combine/README.md index b212ed1bd2..630d310d21 100644 --- a/crates/ty_combine/README.md +++ b/crates/ty_combine/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ty_combine). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ty_combine). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_module_resolver/Cargo.toml b/crates/ty_module_resolver/Cargo.toml index 268067e6b8..973b7f8f40 100644 --- a/crates/ty_module_resolver/Cargo.toml +++ b/crates/ty_module_resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_module_resolver" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_module_resolver/README.md b/crates/ty_module_resolver/README.md index 8bbf6015f5..d48136b649 100644 --- a/crates/ty_module_resolver/README.md +++ b/crates/ty_module_resolver/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ty_module_resolver). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ty_module_resolver). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_core/Cargo.toml b/crates/ty_python_core/Cargo.toml index 9703595f66..fad05d5d0e 100644 --- a/crates/ty_python_core/Cargo.toml +++ b/crates/ty_python_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_python_core" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_python_core/README.md b/crates/ty_python_core/README.md index 916d238fa5..1b439ca901 100644 --- a/crates/ty_python_core/README.md +++ b/crates/ty_python_core/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ty_python_core). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ty_python_core). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml index 8ddb877472..500d017ca5 100644 --- a/crates/ty_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_python_semantic" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_python_semantic/README.md b/crates/ty_python_semantic/README.md index ab169b6860..c81ed0bf32 100644 --- a/crates/ty_python_semantic/README.md +++ b/crates/ty_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ty_python_semantic). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ty_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_site_packages/Cargo.toml b/crates/ty_site_packages/Cargo.toml index 27b7a649fb..882efe6723 100644 --- a/crates/ty_site_packages/Cargo.toml +++ b/crates/ty_site_packages/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_site_packages" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_site_packages/README.md b/crates/ty_site_packages/README.md index c133994592..5353319c9e 100644 --- a/crates/ty_site_packages/README.md +++ b/crates/ty_site_packages/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ty_site_packages). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ty_site_packages). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_static/Cargo.toml b/crates/ty_static/Cargo.toml index 68f1627d0b..0fbac5fceb 100644 --- a/crates/ty_static/Cargo.toml +++ b/crates/ty_static/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_static" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ty_static/README.md b/crates/ty_static/README.md index 93fd1c573c..d374ebe91b 100644 --- a/crates/ty_static/README.md +++ b/crates/ty_static/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.6) is a component of [Ruff 0.16.0](https://crates.io/crates/ruff/0.16.0). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.0/crates/ty_static). +This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ty_static). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_vendored/Cargo.toml b/crates/ty_vendored/Cargo.toml index 405cb0c71c..f072319122 100644 --- a/crates/ty_vendored/Cargo.toml +++ b/crates/ty_vendored/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_vendored" -version = "0.0.6" +version = "0.0.7" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/docs/formatter.md b/docs/formatter.md index 2cff4f1040..5f3307f1b1 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -303,7 +303,7 @@ support needs to be explicitly included by adding it to `types_or`: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.16.0 + rev: v0.16.1 hooks: - id: ruff-format types_or: [python, pyi, jupyter, markdown] diff --git a/docs/integrations.md b/docs/integrations.md index 25f1f6e418..cddb2cadd2 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.16.0-alpine + name: ghcr.io/astral-sh/ruff:0.16.1-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.16.0 + rev: v0.16.1 hooks: # Run the linter. - id: ruff-check @@ -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.16.0 + rev: v0.16.1 hooks: # Run the linter. - id: ruff-check @@ -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.16.0 + rev: v0.16.1 hooks: # Run the linter. - id: ruff-check diff --git a/docs/tutorial.md b/docs/tutorial.md index 655d825a20..233e9a48b8 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -372,7 +372,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.16.0 + rev: v0.16.1 hooks: # Run the linter. - id: ruff-check diff --git a/pyproject.toml b/pyproject.toml index 323606cc39..03409de6a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.16.0" +version = "0.16.1" 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 530e00ead8..9adef2abf2 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.16.0" +version = "0.16.1" description = "" authors = ["Charles Marsh "] diff --git a/uv.lock b/uv.lock index 44b52b16b6..2fd9825d8f 100644 --- a/uv.lock +++ b/uv.lock @@ -30,8 +30,8 @@ name = "anyio" version = "4.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "idna", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version == '3.12.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } wheels = [ @@ -43,7 +43,7 @@ name = "anysqlite" version = "0.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, + { name = "anyio", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/4b/cd5d66b9f87e773bc71344a368b9472987e33514e6627e28342b9c3e7c43/anysqlite-0.0.5.tar.gz", hash = "sha256:9dfcf87baf6b93426ad1d9118088c41dbf24ef01b445eea4a5d486bac2755cce", size = 3432, upload-time = "2023-10-02T13:49:25.135Z" } wheels = [ @@ -64,7 +64,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "python_full_version >= '3.12' and implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -176,11 +176,11 @@ name = "hishel" version = "0.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "anysqlite" }, - { name = "httpx" }, - { name = "msgpack" }, - { name = "typing-extensions" }, + { name = "anyio", marker = "python_full_version >= '3.12'" }, + { name = "anysqlite", marker = "python_full_version >= '3.12'" }, + { name = "httpx", marker = "python_full_version >= '3.12'" }, + { name = "msgpack", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/64/a104ccac48f123f853254483617b16e0efc1649bd7e35bcdc5a5a5ef0ae2/hishel-0.1.5.tar.gz", hash = "sha256:9d40c682cd94fd6e1394fb05713ae20a75ed8aeba6f5272380444039ce6257f2", size = 75468, upload-time = "2025-10-18T13:32:41.854Z" } wheels = [ @@ -192,8 +192,8 @@ name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "h11" }, + { name = "certifi", marker = "python_full_version >= '3.12'" }, + { name = "h11", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ @@ -205,10 +205,10 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, + { name = "anyio", marker = "python_full_version >= '3.12'" }, + { name = "certifi", marker = "python_full_version >= '3.12'" }, + { name = "httpcore", marker = "python_full_version >= '3.12'" }, + { name = "idna", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ @@ -229,7 +229,7 @@ name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mdurl" }, + { name = "mdurl", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ @@ -374,10 +374,10 @@ name = "pydantic" version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "annotated-types", marker = "python_full_version >= '3.12'" }, + { name = "pydantic-core", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ @@ -389,7 +389,7 @@ name = "pydantic-core" version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ @@ -519,7 +519,7 @@ name = "pygit2" version = "1.19.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi" }, + { name = "cffi", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/44/415aa93422b4bfc21a6448acb7e16280d5f33a9a3fae38a384e37b046ae4/pygit2-1.19.3.tar.gz", hash = "sha256:a543e6d4ebb43825564935758dc234e770016fed673b84370d46ae9580558831", size = 810489, upload-time = "2026-06-13T08:06:04.982Z" } wheels = [ @@ -594,8 +594,8 @@ name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, + { name = "markdown-it-py", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ @@ -607,14 +607,14 @@ name = "rooster" version = "0.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "hishel" }, - { name = "httpx" }, - { name = "marko" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pygit2" }, - { name = "tqdm" }, - { name = "typer" }, + { name = "hishel", marker = "python_full_version >= '3.12'" }, + { name = "httpx", marker = "python_full_version >= '3.12'" }, + { name = "marko", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pydantic", marker = "python_full_version >= '3.12'" }, + { name = "pygit2", marker = "python_full_version >= '3.12'" }, + { name = "tqdm", marker = "python_full_version >= '3.12'" }, + { name = "typer", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/02/8ce565271dc52bd0d0d812043b12ec60111d947f81dc30301d19d7bfd453/rooster-0.1.1.tar.gz", hash = "sha256:c9823122f0c2b035985e70384323cdd353477af988e0f065bc302646a49da482", size = 18608, upload-time = "2025-10-29T15:18:49.478Z" } wheels = [ @@ -623,7 +623,7 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.0" +version = "0.16.1" source = { editable = "." } [package.dev-dependencies] @@ -654,7 +654,7 @@ name = "tqdm" version = "4.68.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } wheels = [ @@ -666,10 +666,10 @@ name = "typer" version = "0.26.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "rich" }, - { name = "shellingham" }, + { name = "annotated-doc", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "rich", marker = "python_full_version >= '3.12'" }, + { name = "shellingham", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } wheels = [ @@ -690,7 +690,7 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ From 4a437b093417a22abecc168cc186ef58ca48c0fc Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 30 Jul 2026 14:28:20 -0400 Subject: [PATCH 158/390] [ty] Precalculate supports in constraint sets (#27306) The _support_ of a constraint is the set of typevars that it mentions (i.e. the subject of the constraint, and the (free) typevars mentioned in its lower and upper bounds). The support of a BDD node is union of the supports of every constraint in that subtree. In #27173, we'll need the support to implement a custom path walker for finding solutions of a constraint set. So I've pulled out the calculation of the support into this separate PR, mostly to verify that it has no ecosystem or performance impact. It doesn't, largely because we can piggy-back on a type walk that we're already doing while interning things in the `ConstraintSetBuilder.` But also, it turns out that there's one existing method, `exists`, which is determining which constraints to quantify away by doing a deep type walk of each constraints. We can start using the new support for that instead! --- .../src/types/constraints.rs | 200 +++++++++++++++--- .../src/types/constraints/support.rs | 86 ++++++++ 2 files changed, 253 insertions(+), 33 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/constraints/support.rs diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 2a3c657e9b..f088f9582a 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -105,7 +105,8 @@ use ty_python_core::rank::RankBitBox; use ty_static::EnvVars; use crate::types::class::GenericAlias; -use crate::types::typevar::{BoundTypeVarIdentity, TypeVarSet, walk_bound_type_var_type}; +use crate::types::constraints::support::{Support, SupportId}; +use crate::types::typevar::{BoundTypeVarIdentity, TypeVarSet}; use crate::types::variance::VarianceInferable; use crate::types::visitor::{ TypeCollector, TypeKind, TypeVisitor, any_over_type, walk_non_atomic_type, @@ -117,6 +118,8 @@ use crate::types::{ }; use crate::{Db, FxIndexMap, FxIndexSet, FxOrderSet}; +mod support; + /// An extension trait for building constraint sets from [`Option`] values. pub(crate) trait OptionConstraintsExtension { /// Returns a constraint set that is always satisfiable if the option is `None`; otherwise @@ -251,10 +254,14 @@ pub struct OwnedConstraintSet<'db> { #[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] struct OwnedConstraintSetInner<'db> { constraints: Box<[Constraint<'db>]>, + constraint_supports: Box<[SupportId]>, constraint_indices: RankBitBox, typevars: IndexVec>, nodes: Box<[InteriorNodeData]>, + node_supports: Box<[SupportId]>, node_indices: RankBitBox, + supports: Box<[Support]>, + support_indices: RankBitBox, /// A dense, canonical source-order tree whose IDs are independent of sidecar construction /// history. source_orders: Box<[SourceOrder]>, @@ -340,6 +347,16 @@ impl OwnedConstraintSetInner<'_> { ); self.constraint_indices.rank(index) as usize } + + fn retained_support_index(&self, id: SupportId) -> usize { + let index = id.index(); + debug_assert_eq!( + self.support_indices.get_bit(index), + Some(true), + "should not access constraint set support that was marked unused", + ); + self.support_indices.rank(index) as usize + } } /// A set of constraints under which a type property holds. @@ -960,6 +977,10 @@ struct ConstraintSetStorage<'db> { /// The BDD nodes that appear in any of the constraint sets constructed in this builder. nodes: IndexVec, + supports: IndexVec, + constraint_supports: IndexVec, + node_supports: IndexVec, + /// Encodes an ordering on the constraints in a constraint set, which is based on the order /// that the constraints (or more accurately, the Python expressions they're derived from) /// appear in the source code. This ensures that any union and intersections types that appear @@ -1050,6 +1071,13 @@ impl ConstraintSetStorage<'_> { id } + fn adjusted_support_id(&self, id: SupportId) -> SupportId { + if let Some(compacted) = &self.compacted { + return id + compacted.support_indices.len(); + } + id + } + fn adjusted_source_order_id(&self, id: SourceOrderId) -> SourceOrderId { if let Some(compacted) = &self.compacted { return id + compacted.source_orders.len(); @@ -1103,15 +1131,22 @@ impl<'db> ConstraintSetBuilder<'db> { let mut used_nodes = RankBitBox::bits_with_capacity(storage.nodes.len()); let mut used_constraints = RankBitBox::bits_with_capacity(storage.constraints.len()); + let mut used_supports = RankBitBox::bits_with_capacity(storage.supports.len()); let mut stack = vec![node]; while let Some(node) = stack.pop() { if node.is_terminal() || used_nodes[node.index()] { continue; } - let interior = storage.nodes[node]; + let interior = storage.interior_node_data(node); + let node_support = storage + .node_support_id(node) + .expect("node should be non-terminal"); + let constraint_support = storage.constraint_support_id(interior.constraint); used_nodes.set(node.index(), true); used_constraints.set(interior.constraint.index(), true); + used_supports.set(node_support.index(), true); + used_supports.set(constraint_support.index(), true); stack.push(interior.if_true); stack.push(interior.if_uncertain); stack.push(interior.if_false); @@ -1134,6 +1169,7 @@ impl<'db> ConstraintSetBuilder<'db> { used_nodes.truncate(used_nodes.last_one().map_or(0, |last| last + 1)); used_constraints.truncate(used_constraints.last_one().map_or(0, |last| last + 1)); + used_supports.truncate(used_supports.last_one().map_or(0, |last| last + 1)); let nodes = storage .nodes @@ -1141,6 +1177,12 @@ impl<'db> ConstraintSetBuilder<'db> { .zip(&used_nodes) .filter_map(|(node, used)| used.then_some(node)) .collect(); + let node_supports = storage + .node_supports + .into_iter() + .zip(&used_nodes) + .filter_map(|(support, used)| used.then_some(support)) + .collect(); let node_indices = RankBitBox::from_bits(used_nodes); let constraints = storage @@ -1149,8 +1191,22 @@ impl<'db> ConstraintSetBuilder<'db> { .zip(&used_constraints) .filter_map(|(constraint, used)| used.then_some(constraint)) .collect(); + let constraint_supports = storage + .constraint_supports + .into_iter() + .zip(&used_constraints) + .filter_map(|(support, used)| used.then_some(support)) + .collect(); let constraint_indices = RankBitBox::from_bits(used_constraints); + let supports = storage + .supports + .into_iter() + .zip(&used_supports) + .filter_map(|(support, used)| used.then_some(support)) + .collect(); + let support_indices = RankBitBox::from_bits(used_supports); + storage.typevars.shrink_to_fit(); OwnedConstraintSet { @@ -1158,10 +1214,14 @@ impl<'db> ConstraintSetBuilder<'db> { source_order: Some(source_order), inner: Some(Arc::new(OwnedConstraintSetInner { constraints, + constraint_supports, constraint_indices, typevars: storage.typevars, nodes, + node_supports, node_indices, + supports, + support_indices, source_orders: source_orders.raw.into_boxed_slice(), })), } @@ -1203,9 +1263,15 @@ impl<'db> ConstraintSetStorage<'db> { } /// Interns all of the typevars mentioned in a type in a stable order. - fn intern_mentioned_typevars_in_type(&mut self, db: &'db dyn Db, ty: Type<'db>) { + fn intern_mentioned_typevars_in_type( + &mut self, + db: &'db dyn Db, + ty: Type<'db>, + support: &mut Support, + ) { struct InternMentionedTypevars<'a, 'db> { storage: RefCell<&'a mut ConstraintSetStorage<'db>>, + support: RefCell<&'a mut Support>, recursion_guard: TypeCollector<'db>, } @@ -1214,17 +1280,6 @@ impl<'db> ConstraintSetStorage<'db> { false } - fn visit_bound_type_var_type( - &self, - db: &'db dyn Db, - bound_typevar: BoundTypeVarInstance<'db>, - ) { - let mut storage = self.storage.borrow_mut(); - storage.intern_typevar(db, bound_typevar); - drop(storage); - walk_bound_type_var_type(db, bound_typevar, self); - } - fn visit_generic_alias_type(&self, db: &'db dyn Db, alias: GenericAlias<'db>) { for ty in alias.specialization(db).types(db) { self.visit_type(db, *ty); @@ -1232,12 +1287,19 @@ impl<'db> ConstraintSetStorage<'db> { } fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) { + if let Type::TypeVar(bound_typevar) = ty { + let mut storage = self.storage.borrow_mut(); + let typevar = storage.intern_typevar(db, bound_typevar); + let mut support = self.support.borrow_mut(); + support.insert(typevar); + } walk_type_with_recursion_guard(db, ty, self, &self.recursion_guard); } } InternMentionedTypevars { storage: RefCell::new(self), + support: RefCell::new(support), recursion_guard: TypeCollector::default(), } .visit_type(db, ty); @@ -1249,24 +1311,28 @@ impl<'db> ConstraintSetStorage<'db> { db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>, bounds: ConstraintBounds<'db>, - ) { - self.intern_typevar(db, typevar); + ) -> Support { + let mut support = Support::default(); + support.insert(self.intern_typevar(db, typevar)); if let Some(lower) = bounds.lower { - self.intern_mentioned_typevars_in_type(db, lower); + self.intern_mentioned_typevars_in_type(db, lower, &mut support); } if let Some(upper) = bounds.upper { - self.intern_mentioned_typevars_in_type(db, upper); + self.intern_mentioned_typevars_in_type(db, upper, &mut support); } + support } fn intern_constraint(&mut self, db: &'db dyn Db, data: Constraint<'db>) -> ConstraintId { - self.intern_constraint_typevars(db, data.typevar, data.bounds); + let support = self.intern_constraint_typevars(db, data.typevar, data.bounds); self.ensure_overlay_identity_caches(); if let Some(id) = self.constraint_cache.get(&data) { return *id; } + let support_id = self.intern_support(support); let id = self.constraints.push(data); + self.constraint_supports.push(support_id); let id = self.adjusted_constraint_id(id); self.constraint_cache.insert(data, id); id @@ -1277,7 +1343,16 @@ impl<'db> ConstraintSetStorage<'db> { if let Some(id) = self.node_cache.get(&data) { return *id; } + + let mut support = Support::default(); + support |= self.constraint_support(data.constraint); + support |= self.node_support(data.if_true); + support |= self.node_support(data.if_uncertain); + support |= self.node_support(data.if_false); + let support = self.intern_support(support); + let id = self.nodes.push(data); + self.node_supports.push(support); let id = self.adjusted_node_id(id); self.node_cache.insert(data, id); id @@ -1459,6 +1534,74 @@ impl<'db> ConstraintSetStorage<'db> { result } + fn intern_support(&mut self, data: Support) -> SupportId { + let id = self.supports.push(data); + self.adjusted_support_id(id) + } + + fn typevar_data(&self, typevar: TypeVarId) -> BoundTypeVarIdentity<'db> { + if let Some(compacted) = &self.compacted { + let index = typevar.index(); + let split = compacted.typevars.len(); + if index < split { + return compacted.typevars[typevar]; + } + return self.typevars[TypeVarId::from_usize(index - split)]; + } + self.typevars[typevar] + } + + fn support_data(&self, support: SupportId) -> &Support { + if let Some(compacted) = &self.compacted { + let index = support.index(); + let split = compacted.support_indices.len(); + if index < split { + let compacted_index = compacted.retained_support_index(support); + return &compacted.supports[compacted_index]; + } + return &self.supports[SupportId::from_usize(index - split)]; + } + &self.supports[support] + } + + fn constraint_support_id(&self, constraint: ConstraintId) -> SupportId { + if let Some(compacted) = &self.compacted { + let index = constraint.index(); + let split = compacted.constraint_indices.len(); + if index < split { + let compacted_index = compacted.retained_constraint_index(constraint); + return compacted.constraint_supports[compacted_index]; + } + return self.constraint_supports[ConstraintId::from_usize(index - split)]; + } + self.constraint_supports[constraint] + } + + fn constraint_support(&self, constraint: ConstraintId) -> &Support { + self.support_data(self.constraint_support_id(constraint)) + } + + fn node_support_id(&self, node: NodeId) -> Option { + if node.is_terminal() { + return None; + } + if let Some(compacted) = &self.compacted { + let index = node.index(); + let split = compacted.node_indices.len(); + if index < split { + let compacted_index = compacted.retained_node_index(node); + return Some(compacted.node_supports[compacted_index]); + } + return Some(self.node_supports[NodeId::from_usize(index - split)]); + } + Some(self.node_supports[node]) + } + + fn node_support(&self, node: NodeId) -> Option<&Support> { + self.node_support_id(node) + .map(|support| self.support_data(support)) + } + /// Loads an [`OwnedConstraintSet`] into this storage. fn load( &mut self, @@ -4278,10 +4421,6 @@ impl InteriorNode { bound_typevars: TypeVarSet<'db>, source_order: Option, ) -> (NodeId, Option) { - let mentions_typevar = |ty: Type<'_>| match ty { - Type::TypeVar(typevar) => typevar.is_inferable(db, bound_typevars), - _ => false, - }; self.abstract_inner( db, storage, @@ -4291,16 +4430,11 @@ impl InteriorNode { // the sequent map can propagate any derived constraints that do not mention the // quantified typevars. &mut |storage: &ConstraintSetStorage<'_>, constraint| { - let constraint = storage.constraint_data(constraint); - constraint.typevar.is_inferable(db, bound_typevars) - || constraint - .bounds - .lower - .is_some_and(|lower| any_over_type(db, lower, false, mentions_typevar)) - || constraint - .bounds - .upper - .is_some_and(|upper| any_over_type(db, upper, false, mentions_typevar)) + let support = storage.constraint_support(constraint); + support.iter().any(|typevar| { + let typevar = storage.typevar_data(typevar); + typevar.is_inferable(db, bound_typevars) + }) }, ) } diff --git a/crates/ty_python_semantic/src/types/constraints/support.rs b/crates/ty_python_semantic/src/types/constraints/support.rs new file mode 100644 index 0000000000..5b9a09ce67 --- /dev/null +++ b/crates/ty_python_semantic/src/types/constraints/support.rs @@ -0,0 +1,86 @@ +//! Tracks the support of each constraint and interior node in a BDD. +//! +//! The support of a constraint is the set of typevars mentioned anywhere in the constraint +//! (either the subject, or anywhere in the lower or upper bound). +//! +//! The support of a node is the union of the supports of every constraint reachable from that +//! node. + +use std::ops::BitOrAssign; + +use crate::types::constraints::TypeVarId; + +use ruff_index::newtype_index; +use smallvec::SmallVec; + +#[newtype_index] +#[derive(get_size2::GetSize)] +pub(super) struct SupportId; + +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] +pub(super) struct Support { + chunks: SmallVec<[usize; 2]>, +} + +const CHUNK_SIZE: usize = usize::BITS as usize; + +impl Support { + /// Adds a typevar to this support. + pub(super) fn insert(&mut self, typevar: TypeVarId) { + let index = typevar.index(); + let chunks_needed = (index + 1).div_ceil(CHUNK_SIZE); + if self.chunks.len() < chunks_needed { + self.chunks.resize(chunks_needed, 0); + } + + let chunk_index = index / CHUNK_SIZE; + let bit_index_within_chunk = index % CHUNK_SIZE; + let bit_mask_within_chunk = 1 << bit_index_within_chunk; + self.chunks[chunk_index] |= bit_mask_within_chunk; + } + + /// Returns an iterator of all of the typevars in this support. + pub(super) fn iter(&self) -> impl Iterator + '_ { + // Iterate through all of the chunks + let mut next_chunk_start = 0; + self.chunks.iter().copied().flat_map(move |mut chunk| { + // Figure out the starting index of this chunk + let chunk_start = next_chunk_start; + next_chunk_start += CHUNK_SIZE; + + // Iterate through the set bits in this chunk + std::iter::from_fn(move || { + // Find the lowest set bit, if there is one + let index = chunk.trailing_zeros() as usize; + if index == CHUNK_SIZE { + return None; + } + + // Clear out the bit we just found. + chunk ^= 1 << index; + + // And then return it, converted into a TypeVarId + Some(TypeVarId::from_usize(chunk_start + index)) + }) + }) + } +} + +impl BitOrAssign<&Self> for Support { + fn bitor_assign(&mut self, rhs: &Self) { + if self.chunks.len() < rhs.chunks.len() { + self.chunks.resize(rhs.chunks.len(), 0); + } + for (lhs, rhs) in std::iter::zip(&mut self.chunks, &rhs.chunks) { + *lhs |= *rhs; + } + } +} + +impl BitOrAssign> for Support { + fn bitor_assign(&mut self, rhs: Option<&Self>) { + if let Some(rhs) = rhs { + *self |= rhs; + } + } +} From f584b3d6083743d04ebc46f30c103433da6cb49b Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 09:50:52 +0200 Subject: [PATCH 159/390] [ty] Do not top-materialize (user) `TypeIs` return types (#26864) ## Summary We currently top-materialize `TypeIs` types to work around the fact that typeshed annotates some standard library functions like `isawaitable` with a gradual `TypeIs[Awaitable[Any]]` return type. This is problematic when taken verbatim, since that instructs us to intersect with a gradual `Awaitable[Any]` type instead of truly selecting *all* awaitables (`Awaitable[object]`). For example, narrowing `Item | Awaitable[Item]` with a final `Item` class using `isawaitable` would result in `Awaitable[Item] & Awaitable[Any] = Awaitable[Item & Any]`, instead of just `Awaitable[Item]`. However, the current behavior is problematic for user-defined `TypeIs` functions, since we don't respect their declared return type. Here, we fix this by patching typeshed while dropping the top-materialization. This leads to an unchanged behavior for standard-library functions like `isawaitable`, `iscallable`, etc, but respects users annotations for custom `TypeIs` functions: ```py def is_list(arg: object) -> TypeIs[list[Any]]: return isinstance(arg, list) def _(x: object): if is_list(x): reveal_type(x) # list[Any], previously: Top[list[Any]] ``` relates to https://github.com/astral-sh/ty/issues/3375 ## Ecosystem report Looks mostly good: Removed false positives, new unused ignore comment diagnostics Some expected problems, e.g. with this user-defined function in beartype, leading to unfortunate types like `((...) -> Unknown) & ~((...) -> Unknown)`, which are technically correct (and could maybe be simplified), but certainly unintended: ```py def is_callable_like(value: Any) -> TypeIs[Callable]: return callable(value) ``` ## Conformance results - One false positive removed, which is expected. - One new false positive, which looks like it's due to a generic solver gap. ## Test Plan Adapted tests --- crates/ty_ide/src/type_hierarchy.rs | 8 +-- .../resources/mdtest/async.md | 2 +- .../mdtest/dataclasses/dataclasses.md | 6 +- .../resources/mdtest/liskov.md | 4 +- .../resources/mdtest/narrow/callable.md | 2 +- .../resources/mdtest/narrow/type_guards.md | 53 ++++++++++++----- ...2\200\246_-_Basic_(f15db7dc447d0795).snap" | 4 +- ...lemen\342\200\246_(39b614d4707c0661).snap" | 4 +- ...lidat\342\200\246_(25381f371caa1401).snap" | 8 +-- ...loade\342\200\246_(4408ade1316b97c0).snap" | 4 +- crates/ty_python_semantic/src/types.rs | 10 +--- .../types/infer/builder/type_expression.rs | 7 +-- .../0005-inspect-isawaitable-object.patch | 26 +++++++++ .../0006-stdlib-typeis-static-types.patch | 58 +++++++++++++++++++ .../typeshed/stdlib/asyncio/base_futures.pyi | 3 +- .../typeshed/stdlib/asyncio/coroutines.pyi | 4 +- .../vendor/typeshed/stdlib/builtins.pyi | 3 +- .../vendor/typeshed/stdlib/inspect.pyi | 4 +- .../typeshed/stdlib/typing_extensions.pyi | 2 +- 19 files changed, 157 insertions(+), 55 deletions(-) create mode 100644 crates/ty_vendored/typeshed_patches/0005-inspect-isawaitable-object.patch create mode 100644 crates/ty_vendored/typeshed_patches/0006-stdlib-typeis-static-types.patch diff --git a/crates/ty_ide/src/type_hierarchy.rs b/crates/ty_ide/src/type_hierarchy.rs index 167015b463..e2a01b2cb3 100644 --- a/crates/ty_ide/src/type_hierarchy.rs +++ b/crates/ty_ide/src/type_hierarchy.rs @@ -221,7 +221,7 @@ mod tests { let supertypes = test.supertypes(); insta::assert_snapshot!( snapshot(&test.db, &supertypes), - @"vendored://stdlib/builtins.pyi:3620:3626 object :: builtins", + @"vendored://stdlib/builtins.pyi:3650:3656 object :: builtins", ); } @@ -435,12 +435,12 @@ mod tests { let item = test.prepare().unwrap(); insta::assert_snapshot!( snapshot(&test.db, &[item]), - @"vendored://stdlib/builtins.pyi:8520:8524 type :: builtins", + @"vendored://stdlib/builtins.pyi:8550:8554 type :: builtins", ); let supertypes = test.supertypes(); insta::assert_snapshot!( snapshot(&test.db, &supertypes), - @"vendored://stdlib/builtins.pyi:3620:3626 object :: builtins", + @"vendored://stdlib/builtins.pyi:3650:3656 object :: builtins", ); } @@ -492,7 +492,7 @@ mod tests { let supertypes = test.supertypes(); insta::assert_snapshot!( snapshot(&test.db, &supertypes), - @"vendored://stdlib/builtins.pyi:104692:104697 tuple :: builtins", + @"vendored://stdlib/builtins.pyi:104722:104727 tuple :: builtins", ); } diff --git a/crates/ty_python_semantic/resources/mdtest/async.md b/crates/ty_python_semantic/resources/mdtest/async.md index 09b858714b..d6d2c4d6b4 100644 --- a/crates/ty_python_semantic/resources/mdtest/async.md +++ b/crates/ty_python_semantic/resources/mdtest/async.md @@ -150,7 +150,7 @@ def get_any() -> Any: async def test(): x = get_any() if inspect.isawaitable(x): - reveal_type(x) # revealed: Any & Top[Awaitable[object]] + reveal_type(x) # revealed: Any & Awaitable[object] y = await x reveal_type(y) # revealed: Any ``` diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index ed90a0a7fe..8bf457b0fe 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -2223,7 +2223,8 @@ asdict(Foo) ## `dataclasses.is_dataclass` `is_dataclass` recognizes both dataclass instances and dataclass classes. A concrete dataclass -instance always satisfies the `DataclassInstance` protocol, so the negative branch is unreachable: +instance always satisfies the `DataclassInstance` protocol, but we do not currently recognize that +the negative branch is unreachable: ```py from dataclasses import dataclass, is_dataclass @@ -2234,7 +2235,8 @@ class Event: def check(event: Event) -> None: if not is_dataclass(event): - reveal_type(event) # revealed: Never + # TODO: This should be `Never`. + reveal_type(event) # revealed: Event & ~DataclassInstance & ~type[DataclassInstance] ``` ## `dataclasses.KW_ONLY` diff --git a/crates/ty_python_semantic/resources/mdtest/liskov.md b/crates/ty_python_semantic/resources/mdtest/liskov.md index 68be19b918..c15e975c26 100644 --- a/crates/ty_python_semantic/resources/mdtest/liskov.md +++ b/crates/ty_python_semantic/resources/mdtest/liskov.md @@ -1734,9 +1734,9 @@ error[invalid-method-override]: Invalid override of method `__eq__` 3 | def __eq__(self, other: "Bad") -> bool: # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `object.__eq__` | - ::: stdlib/builtins.pyi:136:9 + ::: stdlib/builtins.pyi:137:9 | -136 | def __eq__(self, value: object, /) -> bool: ... +137 | def __eq__(self, value: object, /) -> bool: ... | -------------------------------------- `object.__eq__` defined here info: parameter `value` has an incompatible type: `object` is not assignable to `Bad` info: This violates the Liskov Substitution Principle diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md index 25e35e90bf..613af0996b 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md @@ -2,7 +2,7 @@ ## Basic narrowing -The `callable()` builtin returns `TypeIs[Callable[..., object]]`, which narrows the type to the +The `callable()` builtin returns `TypeIs[Top[Callable[..., object]]]`, which narrows the type to the intersection with `Top[Callable[..., object]]`. The `Top[...]` wrapper indicates this is a fully static type representing the top materialization of a gradual callable. diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index f09caedac3..df101cb4b9 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -458,17 +458,10 @@ def _(x: Foo | Bar, is_bar: Callable[[object], TypeIs[Bar]]): reveal_type(x) # revealed: Foo & ~Bar ``` -For generics, we transform the argument passed into `TypeIs[]` from `X` to `Top[X]`. This helps -especially when using various functions from typeshed that are annotated as returning -`TypeIs[SomeCovariantGeneric[Any]]` to avoid false positives in other type checkers. For ty's -purposes, it would usually lead to more intuitive results if `object` was used as the specialization -for a covariant generic inside the `TypeIs` special form, but this is mitigated by our implicit -transformation from `TypeIs[SomeCovariantGeneric[Any]]` to `TypeIs[Top[SomeCovariantGeneric[Any]]]` -(which just simplifies to `TypeIs[SomeCovariantGeneric[object]]`). +A `TypeIs` function that returns a gradual specialization of a generic class narrows to that generic +type without replacing its gradual type argument: ```py -class Unrelated: ... - class Covariant[T]: def get(self) -> T: raise NotImplementedError @@ -476,6 +469,20 @@ class Covariant[T]: def is_instance_of_covariant(arg: object) -> TypeIs[Covariant[Any]]: return isinstance(arg, Covariant) +def _(x: object): + if is_instance_of_covariant(x): + reveal_type(x) # revealed: Covariant[Any] +``` + +However, intersecting with the declared gradual type does not necessarily exclude every other +specialization in the negative branch: + +```py +from typing import final + +@final +class Unrelated: ... + def needs_instance_of_unrelated(arg: Unrelated): pass @@ -483,11 +490,31 @@ def _(x: Unrelated | Covariant[int]): if is_instance_of_covariant(x): raise RuntimeError("oh no") - reveal_type(x) # revealed: Unrelated & ~Covariant[object] + reveal_type(x) # revealed: Unrelated | (Covariant[int] & ~Covariant[Any]) + + needs_instance_of_unrelated(x) # error: [invalid-argument-type] +``` + +If a user wants to select *all* instances of `Covariant`, they must use `Covariant[object]`, or more +generally, `Top[C[Any]]`, which also works for invariant generic types: + +```py +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ty_extensions import Top + +class Invariant[T]: + value: T # make it invariant in `T` - # We would emit a false-positive diagnostic here if we didn't implicitly transform - # `TypeIs[Covariant[Any]]` to `TypeIs[Covariant[object]]` - needs_instance_of_unrelated(x) +def is_instance_of_invariant(arg: object) -> "TypeIs[Top[Invariant[Any]]]": + return isinstance(arg, Invariant) + +def _(x: Unrelated | Invariant[int]): + if is_instance_of_invariant(x): + reveal_type(x) # revealed: Invariant[int] + else: + reveal_type(x) # revealed: Unrelated ``` ## `TypeGuard` special cases diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Basic_(f15db7dc447d0795).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Basic_(f15db7dc447d0795).snap" index 916e38516d..f30c616a0a 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Basic_(f15db7dc447d0795).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_await.md_-_Invalid_await_diagno\342\200\246_-_Basic_(f15db7dc447d0795).snap" @@ -26,9 +26,9 @@ error[invalid-await]: `Literal[1]` is not awaitable 2 | await 1 # error: [invalid-await] | ^ | - ::: stdlib/builtins.pyi:349:7 + ::: stdlib/builtins.pyi:350:7 | -349 | class int: +350 | class int: | --- type defined here info: `__await__` is missing diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" index f6b01187be..b33b171a3c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elemen\342\200\246_(39b614d4707c0661).snap" @@ -41,9 +41,9 @@ error[invalid-method-override]: Invalid override of method `__eq__` 6 | def __eq__(self, other) -> NotBoolable: | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `object.__eq__` | - ::: stdlib/builtins.pyi:136:9 + ::: stdlib/builtins.pyi:137:9 | -136 | def __eq__(self, value: object, /) -> bool: ... +137 | def __eq__(self, value: object, /) -> bool: ... | -------------------------------------- `object.__eq__` defined here info: incompatible return types: `NotBoolable` is not assignable to `bool` info: This violates the Liskov Substitution Principle diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Class_header_validat\342\200\246_(25381f371caa1401).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Class_header_validat\342\200\246_(25381f371caa1401).snap" index 0c16ade179..f3be40193e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Class_header_validat\342\200\246_(25381f371caa1401).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Class_header_validat\342\200\246_(25381f371caa1401).snap" @@ -46,9 +46,9 @@ error[invalid-typed-dict-header]: TypedDict class `Foo` can only inherit from Ty 3 | class Foo(TypedDict, int): ... # error: [invalid-typed-dict-header] | ^^^ `int` is not a `TypedDict` class | - ::: stdlib/builtins.pyi:349:7 + ::: stdlib/builtins.pyi:350:7 | -349 | class int: +350 | class int: | --- `int` defined here ``` @@ -60,9 +60,9 @@ error[invalid-typed-dict-header]: TypedDict class `Foo2` can only inherit from T 6 | class Foo2(TypedDict, object): ... # error: [invalid-typed-dict-header] | ^^^^^^ `object` is not a `TypedDict` class | - ::: stdlib/builtins.pyi:113:7 + ::: stdlib/builtins.pyi:114:7 | -113 | class object: +114 | class object: | ------ `object` defined here ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Union_with_overloade\342\200\246_(4408ade1316b97c0).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Union_with_overloade\342\200\246_(4408ade1316b97c0).snap" index cbaf244dbc..e2fa6a6cef 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Union_with_overloade\342\200\246_(4408ade1316b97c0).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f\342\200\246_-_Union_with_overloade\342\200\246_(4408ade1316b97c0).snap" @@ -41,9 +41,9 @@ info: ├── type `Literal[" "]` is not assignable to protocol `Buffer` info: │ └── protocol member `__buffer__` is not defined on type `Literal[" "]` info: └── ... omitted 1 union element without additional context info: Method defined here - --> stdlib/builtins.pyi:1843:9 + --> stdlib/builtins.pyi:1844:9 | -1843 | def split(self, sep: ReadableBuffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]: +1844 | def split(self, sep: ReadableBuffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]: | ^^^^^ --------------------------------- Parameter declared here info: Union variant `bound method bytes.split(sep: Buffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]` is incompatible with this call site info: Attempted to call union type `(bound method bytes.split(sep: Buffer | None = None, maxsplit: SupportsIndex = -1) -> list[bytes]) | (bound method str.split(sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str])` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3d4576eebe..3e280da02e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -8842,9 +8842,6 @@ impl<'db> TypeIsType<'db> { /// Construct an unbound `TypeIs` return type from the user-written type expression. /// - /// The user-written type is preserved for `TypeIs` invariance checks, while the return type - /// used for narrowing applies the top materialization on demand. - /// /// ```python /// from typing import TypeIs /// @@ -8856,12 +8853,7 @@ impl<'db> TypeIsType<'db> { } pub(crate) fn return_type(self, db: &'db dyn Db) -> Type<'db> { - // N.B. Using the top materialization here is a pragmatic decision that - // makes us produce more intuitive results given how `TypeIs` is used in - // the real world (in particular, in typeshed). However, there's some - // debate about whether this is really fully correct. See - // for more discussion. - self.type_argument(db).top_materialization(db) + self.type_argument(db) } #[must_use] diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 00edef8fa1..31da672420 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -2393,12 +2393,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::unknown() } - _ => TypeGuardType::unbound( - self.db(), - // Unlike `TypeIs`, don't use top materialization, because - // `TypeGuard` clobbering behavior makes it counterintuitive - self.infer_type_expression(arguments_slice), - ), + _ => TypeGuardType::unbound(self.db(), self.infer_type_expression(arguments_slice)), }, SpecialFormType::Concatenate => { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { diff --git a/crates/ty_vendored/typeshed_patches/0005-inspect-isawaitable-object.patch b/crates/ty_vendored/typeshed_patches/0005-inspect-isawaitable-object.patch new file mode 100644 index 0000000000..fe7242f7c0 --- /dev/null +++ b/crates/ty_vendored/typeshed_patches/0005-inspect-isawaitable-object.patch @@ -0,0 +1,26 @@ +--- a/stdlib/inspect.pyi ++++ b/stdlib/inspect.pyi +@@ -345,10 +345,10 @@ def isgenerator(object: object) -> TypeIs[GeneratorType[object, Never, object]]: + throw() used to raise an exception inside the generator + """ + +-def iscoroutine(object: object) -> TypeIs[CoroutineType[Any, Any, Any]]: ++def iscoroutine(object: object) -> TypeIs[CoroutineType[object, Never, object]]: + """Return true if the object is a coroutine.""" + +-def isawaitable(object: object) -> TypeIs[Awaitable[Any]]: ++def isawaitable(object: object) -> TypeIs[Awaitable[object]]: + """Return true if object can be passed to an ``await`` expression.""" + + @overload +--- a/stdlib/typing_extensions.pyi ++++ b/stdlib/typing_extensions.pyi +@@ -1337,7 +1337,7 @@ else: + + For example:: + +- def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]: ++ def is_awaitable(val: object) -> TypeIs[Awaitable[object]]: + return hasattr(val, '__await__') + + def f(val: Union[int, Awaitable[int]]) -> int: diff --git a/crates/ty_vendored/typeshed_patches/0006-stdlib-typeis-static-types.patch b/crates/ty_vendored/typeshed_patches/0006-stdlib-typeis-static-types.patch new file mode 100644 index 0000000000..871d83a51f --- /dev/null +++ b/crates/ty_vendored/typeshed_patches/0006-stdlib-typeis-static-types.patch @@ -0,0 +1,58 @@ +--- a/stdlib/builtins.pyi ++++ b/stdlib/builtins.pyi +@@ -78,6 +78,7 @@ from typing import ( # noqa: Y022,UP035 + + # we can't import `Literal` from typing or mypy crashes: see #11247 + from typing_extensions import Literal, LiteralString, Self, TypeIs, TypeVarTuple, deprecated, disjoint_base # noqa: Y023, UP035 ++from ty_extensions import Top + + if sys.version_info >= (3, 14): + from _typeshed import AnnotateFunc +@@ -3667,7 +3668,7 @@ def breakpoint(*args: Any, **kws: Any) -> None: + By default, this drops you into the pdb debugger. + """ + +-def callable(obj: object, /) -> TypeIs[Callable[..., object]]: ++def callable(obj: object, /) -> TypeIs[Top[Callable[..., object]]]: + """Return whether the object is callable (i.e., some kind of function). + + Note that classes are callable, as are instances of classes with a +--- a/stdlib/asyncio/base_futures.pyi ++++ b/stdlib/asyncio/base_futures.pyi +@@ -3,6 +3,7 @@ from collections.abc import Callable, Sequence + from contextvars import Context + from typing import Any, Final + from typing_extensions import TypeIs ++from ty_extensions import Top + + from . import futures + +@@ -12,7 +13,7 @@ _PENDING: Final = "PENDING" # undocumented + _CANCELLED: Final = "CANCELLED" # undocumented + _FINISHED: Final = "FINISHED" # undocumented + +-def isfuture(obj: object) -> TypeIs[Future[Any]]: ++def isfuture(obj: object) -> TypeIs[Top[Future[Any]]]: + """Check for a Future. + + This returns True when obj is a Future instance or is advertising +--- a/stdlib/asyncio/coroutines.pyi ++++ b/stdlib/asyncio/coroutines.pyi +@@ -1,6 +1,6 @@ + import sys + from collections.abc import Awaitable, Callable, Coroutine + from typing import Any, ParamSpec, TypeGuard, TypeVar, overload +-from typing_extensions import TypeIs, deprecated ++from typing_extensions import Never, TypeIs, deprecated + + # Keep asyncio.__all__ updated with any changes to __all__ here +@@ -22,7 +22,7 @@ if sys.version_info < (3, 11): + If the coroutine is not yielded from before it is destroyed, + an error message is logged. + """ + +-def iscoroutine(obj: object) -> TypeIs[Coroutine[Any, Any, Any]]: ++def iscoroutine(obj: object) -> TypeIs[Coroutine[object, Never, object]]: + """Return True if obj is a coroutine object.""" + + if sys.version_info >= (3, 11): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_futures.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_futures.pyi index 8b1ed8ae12..f19368430c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_futures.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/base_futures.pyi @@ -3,6 +3,7 @@ from collections.abc import Callable, Sequence from contextvars import Context from typing import Any, Final from typing_extensions import TypeIs +from ty_extensions import Top from . import futures @@ -12,7 +13,7 @@ _PENDING: Final = "PENDING" # undocumented _CANCELLED: Final = "CANCELLED" # undocumented _FINISHED: Final = "FINISHED" # undocumented -def isfuture(obj: object) -> TypeIs[Future[Any]]: +def isfuture(obj: object) -> TypeIs[Top[Future[Any]]]: """Check for a Future. This returns True when obj is a Future instance or is advertising diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi index 9479dc0d68..2b08e73098 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi @@ -1,7 +1,7 @@ import sys from collections.abc import Awaitable, Callable, Coroutine from typing import Any, ParamSpec, TypeGuard, TypeVar, overload -from typing_extensions import TypeIs, deprecated +from typing_extensions import Never, TypeIs, deprecated # Keep asyncio.__all__ updated with any changes to __all__ here if sys.version_info >= (3, 11): @@ -22,7 +22,7 @@ if sys.version_info < (3, 11): an error message is logged. """ -def iscoroutine(obj: object) -> TypeIs[Coroutine[Any, Any, Any]]: +def iscoroutine(obj: object) -> TypeIs[Coroutine[object, Never, object]]: """Return True if obj is a coroutine object.""" if sys.version_info >= (3, 11): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi index b48289887a..d1691cea5e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi @@ -78,6 +78,7 @@ from typing import ( # noqa: Y022,UP035 # we can't import `Literal` from typing or mypy crashes: see #11247 from typing_extensions import Literal, LiteralString, Self, TypeIs, TypeVarTuple, deprecated, disjoint_base # noqa: Y023, UP035 +from ty_extensions import Top if sys.version_info >= (3, 14): from _typeshed import AnnotateFunc @@ -3666,7 +3667,7 @@ def breakpoint(*args: Any, **kws: Any) -> None: By default, this drops you into the pdb debugger. """ -def callable(obj: object, /) -> TypeIs[Callable[..., object]]: +def callable(obj: object, /) -> TypeIs[Top[Callable[..., object]]]: """Return whether the object is callable (i.e., some kind of function). Note that classes are callable, as are instances of classes with a diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/inspect.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/inspect.pyi index 49c4fe17f0..2827a72b3d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/inspect.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/inspect.pyi @@ -345,10 +345,10 @@ def isgenerator(object: object) -> TypeIs[GeneratorType[object, Never, object]]: throw() used to raise an exception inside the generator """ -def iscoroutine(object: object) -> TypeIs[CoroutineType[Any, Any, Any]]: +def iscoroutine(object: object) -> TypeIs[CoroutineType[object, Never, object]]: """Return true if the object is a coroutine.""" -def isawaitable(object: object) -> TypeIs[Awaitable[Any]]: +def isawaitable(object: object) -> TypeIs[Awaitable[object]]: """Return true if object can be passed to an ``await`` expression.""" @overload diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi index 1b7157357e..6c6a63b2ab 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi @@ -1337,7 +1337,7 @@ else: For example:: - def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]: + def is_awaitable(val: object) -> TypeIs[Awaitable[object]]: return hasattr(val, '__await__') def f(val: Union[int, Awaitable[int]]) -> int: From 30d5dba981e73a3d8fcd13b9d789eabc611957bc Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:02:41 +0200 Subject: [PATCH 160/390] [ty] Migrate legacy TypeVar diagnostics to inline snapshots (#27343) ## Summary Migrate legacy TypeVar diagnostics to inline snapshots. --- .../mdtest/diagnostics/legacy_typevars.md | 148 +++++++++++------- .../mdtest/generics/legacy/variables.md | 61 +++++++- ...ers_m\342\200\246_(3edf97b20f58fa11).snap" | 58 ------- ...covar\342\200\246_(b7b0976739681470).snap" | 31 ---- ...h_bou\342\200\246_(4ca5f13621915554).snap" | 31 ---- ...y_one\342\200\246_(8b0258f5188209c6).snap" | 31 ---- ..._for_\342\200\246_(72827c64b5c73d05).snap" | 31 ---- ..._argu\342\200\246_(39164266ada3dc2f).snap" | 31 ---- ...y_ass\342\200\246_(c2e3e46852bb268f).snap" | 44 ------ ..._Must_have_a_name_(79a4ce09338e666b).snap" | 31 ---- ...efine\342\200\246_(b1be57970f924722).snap" | 37 ----- ...iven_\342\200\246_(8f6aed0dba79e995).snap" | 31 ---- ...ument\342\200\246_(9d57505425233fd8).snap" | 45 ------ ...eter_\342\200\246_(8424f2b8bc4351f9).snap" | 31 ---- 14 files changed, 142 insertions(+), 499 deletions(-) delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Boolean_parameters_m\342\200\246_(3edf97b20f58fa11).snap" delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_be_both_covar\342\200\246_(b7b0976739681470).snap" delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_both_bou\342\200\246_(4ca5f13621915554).snap" delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_only_one\342\200\246_(8b0258f5188209c6).snap" delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_feature_for_\342\200\246_(72827c64b5c73d05).snap" delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_keyword_argu\342\200\246_(39164266ada3dc2f).snap" delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_be_directly_ass\342\200\246_(c2e3e46852bb268f).snap" delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_have_a_name_(79a4ce09338e666b).snap" delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_not_be_redefine\342\200\246_(b1be57970f924722).snap" delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Name_can't_be_given_\342\200\246_(8f6aed0dba79e995).snap" delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_No_variadic_argument\342\200\246_(9d57505425233fd8).snap" delete mode 100644 "crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_`TypeVar`_parameter_\342\200\246_(8424f2b8bc4351f9).snap" diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md index 843073b5b7..afd1d818b7 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md @@ -2,26 +2,40 @@ The full tests for these features are in `generics/legacy/variables.md`. - - ## Must have a name ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar() ``` +```snapshot +error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` is required. + --> src/mdtest_snippet.py:4:5 + | +4 | T = TypeVar() + | ^^^^^^^^^ +``` + ## Name can't be given more than once ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", name="T") ``` +```snapshot +error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` can only be provided once. + --> src/mdtest_snippet.py:4:18 + | +4 | T = TypeVar("T", name="T") + | ^^^^^^^^ +``` + ## Must be directly assigned to a variable > A `TypeVar()` expression must always directly be assigned to a variable (it should not be used as @@ -31,13 +45,28 @@ T = TypeVar("T", name="T") from typing import TypeVar T = TypeVar("T") -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable U: TypeVar = TypeVar("U") -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable tuple_with_typevar = ("foo", TypeVar("W")) ``` +```snapshot +error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple variable assignment + --> src/mdtest_snippet.py:5:14 + | +5 | U: TypeVar = TypeVar("U") + | ^^^^^^^^^^^^ + + +error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple variable assignment + --> src/mdtest_snippet.py:8:30 + | +8 | tuple_with_typevar = ("foo", TypeVar("W")) + | ^^^^^^^^^^^^ +``` + ## `TypeVar` parameter must match variable name > The argument to `TypeVar()` must be a string equal to the variable name to which it is assigned. @@ -45,10 +74,18 @@ tuple_with_typevar = ("foo", TypeVar("W")) ```py from typing import TypeVar -# error: [mismatched-type-name] +# snapshot: mismatched-type-name T = TypeVar("Q") ``` +```snapshot +warning[mismatched-type-name]: The name passed to `TypeVar` must match the variable it is assigned to + --> src/mdtest_snippet.py:4:13 + | +4 | T = TypeVar("Q") + | ^^^ Expected "T", got "Q" +``` + ## Must not be redefined ```py @@ -56,10 +93,22 @@ from typing import TypeVar T = TypeVar("T") -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T") ``` +```snapshot +error[invalid-legacy-type-variable]: Cannot redefine `T` as a type variable + --> src/mdtest_snippet.py:6:1 + | +3 | T = TypeVar("T") + | - Previously defined here +4 | +5 | # snapshot: invalid-legacy-type-variable +6 | T = TypeVar("T") + | ^ +``` + ## No variadic arguments ```py @@ -67,63 +116,26 @@ from typing import TypeVar types = (int, str) -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", *types) -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable S = TypeVar("S", **{"bound": int}) ``` -## Cannot have only one constraint - -> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should -> be at least two constraints, if any; specifying a single constraint is disallowed. +```snapshot +error[invalid-legacy-type-variable]: Starred arguments are not supported in `TypeVar` creation + --> src/mdtest_snippet.py:6:18 + | +6 | T = TypeVar("T", *types) + | ^^^^^^ -```py -from typing import TypeVar -# error: [invalid-legacy-type-variable] -T = TypeVar("T", int) -``` - -## Cannot have both bound and constraint - -```py -from typing import TypeVar - -# error: [invalid-legacy-type-variable] -T = TypeVar("T", int, str, bound=bytes) -``` - -## Cannot be both covariant and contravariant - -> To facilitate the declaration of container types where covariant or contravariant type checking is -> acceptable, type variables accept keyword arguments `covariant=True` or `contravariant=True`. At -> most one of these may be passed. - -```py -from typing import TypeVar - -# error: [invalid-legacy-type-variable] -T = TypeVar("T", covariant=True, contravariant=True) -``` - -## Boolean parameters must be unambiguous - -```py -from typing_extensions import TypeVar - -def cond() -> bool: - return True - -# error: [invalid-legacy-type-variable] -T = TypeVar("T", covariant=cond()) - -# error: [invalid-legacy-type-variable] -U = TypeVar("U", contravariant=cond()) - -# error: [invalid-legacy-type-variable] -V = TypeVar("V", infer_variance=cond()) +error[invalid-legacy-type-variable]: Starred arguments are not supported in `TypeVar` creation + --> src/mdtest_snippet.py:9:18 + | +9 | S = TypeVar("S", **{"bound": int}) + | ^^^^^^^^^^^^^^^^ ``` ## Invalid keyword arguments @@ -131,10 +143,18 @@ V = TypeVar("V", infer_variance=cond()) ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", invalid_keyword=True) ``` +```snapshot +error[invalid-legacy-type-variable]: Unknown keyword argument `invalid_keyword` in `TypeVar` creation + --> src/mdtest_snippet.py:4:18 + | +4 | T = TypeVar("T", invalid_keyword=True) + | ^^^^^^^^^^^^^^^^^^^^ +``` + ## Invalid feature for this Python version ```toml @@ -145,6 +165,14 @@ python-version = "3.10" ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", default=int) ``` + +```snapshot +error[invalid-legacy-type-variable]: The `default` parameter of `typing.TypeVar` was added in Python 3.13 + --> src/mdtest_snippet.py:4:18 + | +4 | T = TypeVar("T", default=int) + | ^^^^^^^^^^^ +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md index 6be22a8e11..86e7fc9f4a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -6,7 +6,8 @@ for both type variable syntaxes. Unless otherwise specified, all quotations come from the [Generics] section of the typing spec. -Diagnostics for invalid type variables are snapshotted in `diagnostics/legacy_typevars.md`. +Additional diagnostics for invalid type variables are snapshotted in +`diagnostics/legacy_typevars.md`. ## Type variables @@ -586,19 +587,35 @@ reveal_type(S.__constraints__) # revealed: tuple[int | float, str] ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", int) ``` +```snapshot +error[invalid-legacy-type-variable]: A `TypeVar` cannot have exactly one constraint + --> src/mdtest_snippet.py:4:18 + | +4 | T = TypeVar("T", int) + | ^^^ +``` + ### Cannot have both bound and constraint ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", int, str, bound=bytes) ``` +```snapshot +error[invalid-legacy-type-variable]: A `TypeVar` cannot have both a bound and constraints + --> src/mdtest_snippet.py:4:5 + | +4 | T = TypeVar("T", int, str, bound=bytes) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``` + ### Cannot be both covariant and contravariant > To facilitate the declaration of container types where covariant or contravariant type checking is @@ -608,10 +625,18 @@ T = TypeVar("T", int, str, bound=bytes) ```py from typing import TypeVar -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", covariant=True, contravariant=True) ``` +```snapshot +error[invalid-legacy-type-variable]: A `TypeVar` cannot be both covariant and contravariant + --> src/mdtest_snippet.py:4:5 + | +4 | T = TypeVar("T", covariant=True, contravariant=True) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``` + ### Infer variance For a `TypeVar` with `infer_variance=True`, we infer covariance when the type variable only appears @@ -706,16 +731,38 @@ from typing_extensions import TypeVar def cond() -> bool: return True -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable T = TypeVar("T", covariant=cond()) -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable U = TypeVar("U", contravariant=cond()) -# error: [invalid-legacy-type-variable] +# snapshot: invalid-legacy-type-variable V = TypeVar("V", infer_variance=cond()) ``` +```snapshot +error[invalid-legacy-type-variable]: The `covariant` parameter of `TypeVar` cannot have an ambiguous truthiness + --> src/mdtest_snippet.py:7:28 + | +7 | T = TypeVar("T", covariant=cond()) + | ^^^^^^ + + +error[invalid-legacy-type-variable]: The `contravariant` parameter of `TypeVar` cannot have an ambiguous truthiness + --> src/mdtest_snippet.py:10:32 + | +10 | U = TypeVar("U", contravariant=cond()) + | ^^^^^^ + + +error[invalid-legacy-type-variable]: The `infer_variance` parameter of `TypeVar` cannot have an ambiguous truthiness + --> src/mdtest_snippet.py:13:33 + | +13 | V = TypeVar("V", infer_variance=cond()) + | ^^^^^^ +``` + ### Invalid keyword arguments ```py diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Boolean_parameters_m\342\200\246_(3edf97b20f58fa11).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Boolean_parameters_m\342\200\246_(3edf97b20f58fa11).snap" deleted file mode 100644 index 127f64be48..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Boolean_parameters_m\342\200\246_(3edf97b20f58fa11).snap" +++ /dev/null @@ -1,58 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Boolean parameters must be unambiguous -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` - 1 | from typing_extensions import TypeVar - 2 | - 3 | def cond() -> bool: - 4 | return True - 5 | - 6 | # error: [invalid-legacy-type-variable] - 7 | T = TypeVar("T", covariant=cond()) - 8 | - 9 | # error: [invalid-legacy-type-variable] -10 | U = TypeVar("U", contravariant=cond()) -11 | -12 | # error: [invalid-legacy-type-variable] -13 | V = TypeVar("V", infer_variance=cond()) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: The `covariant` parameter of `TypeVar` cannot have an ambiguous truthiness - --> src/mdtest_snippet.py:7:28 - | -7 | T = TypeVar("T", covariant=cond()) - | ^^^^^^ - -``` - -``` -error[invalid-legacy-type-variable]: The `contravariant` parameter of `TypeVar` cannot have an ambiguous truthiness - --> src/mdtest_snippet.py:10:32 - | -10 | U = TypeVar("U", contravariant=cond()) - | ^^^^^^ - -``` - -``` -error[invalid-legacy-type-variable]: The `infer_variance` parameter of `TypeVar` cannot have an ambiguous truthiness - --> src/mdtest_snippet.py:13:33 - | -13 | V = TypeVar("V", infer_variance=cond()) - | ^^^^^^ - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_be_both_covar\342\200\246_(b7b0976739681470).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_be_both_covar\342\200\246_(b7b0976739681470).snap" deleted file mode 100644 index 954a6e2b46..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_be_both_covar\342\200\246_(b7b0976739681470).snap" +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Cannot be both covariant and contravariant -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar("T", covariant=True, contravariant=True) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: A `TypeVar` cannot be both covariant and contravariant - --> src/mdtest_snippet.py:4:5 - | -4 | T = TypeVar("T", covariant=True, contravariant=True) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_both_bou\342\200\246_(4ca5f13621915554).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_both_bou\342\200\246_(4ca5f13621915554).snap" deleted file mode 100644 index faa09184ff..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_both_bou\342\200\246_(4ca5f13621915554).snap" +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Cannot have both bound and constraint -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar("T", int, str, bound=bytes) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: A `TypeVar` cannot have both a bound and constraints - --> src/mdtest_snippet.py:4:5 - | -4 | T = TypeVar("T", int, str, bound=bytes) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_only_one\342\200\246_(8b0258f5188209c6).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_only_one\342\200\246_(8b0258f5188209c6).snap" deleted file mode 100644 index 177dd3cc20..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Cannot_have_only_one\342\200\246_(8b0258f5188209c6).snap" +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Cannot have only one constraint -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar("T", int) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: A `TypeVar` cannot have exactly one constraint - --> src/mdtest_snippet.py:4:18 - | -4 | T = TypeVar("T", int) - | ^^^ - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_feature_for_\342\200\246_(72827c64b5c73d05).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_feature_for_\342\200\246_(72827c64b5c73d05).snap" deleted file mode 100644 index 38310e4d41..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_feature_for_\342\200\246_(72827c64b5c73d05).snap" +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Invalid feature for this Python version -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar("T", default=int) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: The `default` parameter of `typing.TypeVar` was added in Python 3.13 - --> src/mdtest_snippet.py:4:18 - | -4 | T = TypeVar("T", default=int) - | ^^^^^^^^^^^ - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_keyword_argu\342\200\246_(39164266ada3dc2f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_keyword_argu\342\200\246_(39164266ada3dc2f).snap" deleted file mode 100644 index 6cac2b9e9c..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Invalid_keyword_argu\342\200\246_(39164266ada3dc2f).snap" +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Invalid keyword arguments -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar("T", invalid_keyword=True) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: Unknown keyword argument `invalid_keyword` in `TypeVar` creation - --> src/mdtest_snippet.py:4:18 - | -4 | T = TypeVar("T", invalid_keyword=True) - | ^^^^^^^^^^^^^^^^^^^^ - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_be_directly_ass\342\200\246_(c2e3e46852bb268f).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_be_directly_ass\342\200\246_(c2e3e46852bb268f).snap" deleted file mode 100644 index 8ce6f006e0..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_be_directly_ass\342\200\246_(c2e3e46852bb268f).snap" +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Must be directly assigned to a variable -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | T = TypeVar("T") -4 | # error: [invalid-legacy-type-variable] -5 | U: TypeVar = TypeVar("U") -6 | -7 | # error: [invalid-legacy-type-variable] -8 | tuple_with_typevar = ("foo", TypeVar("W")) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple variable assignment - --> src/mdtest_snippet.py:5:14 - | -5 | U: TypeVar = TypeVar("U") - | ^^^^^^^^^^^^ - -``` - -``` -error[invalid-legacy-type-variable]: A `TypeVar` definition must be a simple variable assignment - --> src/mdtest_snippet.py:8:30 - | -8 | tuple_with_typevar = ("foo", TypeVar("W")) - | ^^^^^^^^^^^^ - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_have_a_name_(79a4ce09338e666b).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_have_a_name_(79a4ce09338e666b).snap" deleted file mode 100644 index 14a0c408e7..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_have_a_name_(79a4ce09338e666b).snap" +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Must have a name -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar() -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` is required. - --> src/mdtest_snippet.py:4:5 - | -4 | T = TypeVar() - | ^^^^^^^^^ - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_not_be_redefine\342\200\246_(b1be57970f924722).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_not_be_redefine\342\200\246_(b1be57970f924722).snap" deleted file mode 100644 index 61620f6628..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Must_not_be_redefine\342\200\246_(b1be57970f924722).snap" +++ /dev/null @@ -1,37 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Must not be redefined -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | T = TypeVar("T") -4 | -5 | # error: [invalid-legacy-type-variable] -6 | T = TypeVar("T") -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: Cannot redefine `T` as a type variable - --> src/mdtest_snippet.py:6:1 - | -3 | T = TypeVar("T") - | - Previously defined here -4 | -5 | # error: [invalid-legacy-type-variable] -6 | T = TypeVar("T") - | ^ - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Name_can't_be_given_\342\200\246_(8f6aed0dba79e995).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Name_can't_be_given_\342\200\246_(8f6aed0dba79e995).snap" deleted file mode 100644 index 83a517ddae..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_Name_can't_be_given_\342\200\246_(8f6aed0dba79e995).snap" +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - Name can't be given more than once -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [invalid-legacy-type-variable] -4 | T = TypeVar("T", name="T") -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: The `name` parameter of `TypeVar` can only be provided once. - --> src/mdtest_snippet.py:4:18 - | -4 | T = TypeVar("T", name="T") - | ^^^^^^^^ - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_No_variadic_argument\342\200\246_(9d57505425233fd8).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_No_variadic_argument\342\200\246_(9d57505425233fd8).snap" deleted file mode 100644 index a114575df7..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_No_variadic_argument\342\200\246_(9d57505425233fd8).snap" +++ /dev/null @@ -1,45 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - No variadic arguments -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | types = (int, str) -4 | -5 | # error: [invalid-legacy-type-variable] -6 | T = TypeVar("T", *types) -7 | -8 | # error: [invalid-legacy-type-variable] -9 | S = TypeVar("S", **{"bound": int}) -``` - -# Diagnostics - -``` -error[invalid-legacy-type-variable]: Starred arguments are not supported in `TypeVar` creation - --> src/mdtest_snippet.py:6:18 - | -6 | T = TypeVar("T", *types) - | ^^^^^^ - -``` - -``` -error[invalid-legacy-type-variable]: Starred arguments are not supported in `TypeVar` creation - --> src/mdtest_snippet.py:9:18 - | -9 | S = TypeVar("S", **{"bound": int}) - | ^^^^^^^^^^^^^^^^ - -``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_`TypeVar`_parameter_\342\200\246_(8424f2b8bc4351f9).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_`TypeVar`_parameter_\342\200\246_(8424f2b8bc4351f9).snap" deleted file mode 100644 index e6e4e1d4a9..0000000000 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/legacy_typevars.md_-_Legacy_typevar_creat\342\200\246_-_`TypeVar`_parameter_\342\200\246_(8424f2b8bc4351f9).snap" +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/mdtest/src/lib.rs -expression: snapshot ---- - ---- -mdtest name: legacy_typevars.md - Legacy typevar creation diagnostics - `TypeVar` parameter must match variable name -mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/legacy_typevars.md ---- - -# Python source files - -## mdtest_snippet.py - -``` -1 | from typing import TypeVar -2 | -3 | # error: [mismatched-type-name] -4 | T = TypeVar("Q") -``` - -# Diagnostics - -``` -warning[mismatched-type-name]: The name passed to `TypeVar` must match the variable it is assigned to - --> src/mdtest_snippet.py:4:13 - | -4 | T = TypeVar("Q") - | ^^^ Expected "T", got "Q" - -``` From eb822156cfbdf9769ca60146128f98e87b77192f Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:05:20 +0200 Subject: [PATCH 161/390] [ty] Reuse static-class accessor for tuple checks (#27364) ## Summary Reuse static-class accessor for tuple checks. --- crates/ty_python_semantic/src/types/class.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index be8ea4acdc..ea10c3f7af 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -689,13 +689,7 @@ impl<'db> ClassLiteral<'db> { /// Returns whether this class is `builtins.tuple` exactly pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool { - match self { - Self::Static(class) => class.is_tuple(db), - Self::Dynamic(_) - | Self::DynamicNamedTuple(_) - | Self::DynamicTypedDict(_) - | Self::DynamicEnum(_) => false, - } + self.as_static().is_some_and(|class| class.is_tuple(db)) } /// Return a type representing "the set of all instances of the metaclass of this class". From 10a40855ceba6499cfa58d01f020bb391a947232 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:09:54 +0200 Subject: [PATCH 162/390] [ty] Remove unused frozen-collection APIs (#27347) ## Summary Remove unused frozen-collection APIs. --- crates/ty_python_core/src/frozen.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/ty_python_core/src/frozen.rs b/crates/ty_python_core/src/frozen.rs index 6df174dd19..ab018689dd 100644 --- a/crates/ty_python_core/src/frozen.rs +++ b/crates/ty_python_core/src/frozen.rs @@ -86,15 +86,6 @@ impl std::ops::Index<&K> for FrozenMap { } } -impl IntoIterator for FrozenMap { - type Item = (K, V); - type IntoIter = std::vec::IntoIter<(K, V)>; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_vec().into_iter() - } -} - impl<'a, K, V> IntoIterator for &'a FrozenMap { type Item = &'a (K, V); type IntoIter = std::slice::Iter<'a, (K, V)>; From ec09a62c6f693836810d1b4ab160cfc2e4ec1941 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:10:23 +0200 Subject: [PATCH 163/390] [ty] Remove unused generic-context display settings (#27351) ## Summary Remove unused generic-context display settings. --- .../ty_python_semantic/src/types/display.rs | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index c2bc80164b..a472850a88 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -883,9 +883,7 @@ impl<'db> FmtDetailed<'db> for DisplayTypeAliasDeclaration<'db> { .display_with(self.db, settings.clone()) .fmt_detailed(f)?; if let Some(generic_context) = generic_context { - generic_context - .display_with(self.db, settings.clone()) - .fmt_detailed(f)?; + generic_context.display(self.db).fmt_detailed(f)?; } f.write_str(" = ")?; self.value_ty @@ -1132,7 +1130,6 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), db: self.db, - settings: self.settings.clone(), hide_unused_self, }; f.set_invalid_type_annotation(); @@ -1647,7 +1644,6 @@ impl<'db> FmtDetailed<'db> for DisplayOverloadLiteral<'db> { let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), db: self.db, - settings: self.settings.clone(), hide_unused_self, }; @@ -1716,7 +1712,6 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), db: self.db, - settings: settings.clone(), hide_unused_self, }; f.set_invalid_type_annotation(); @@ -1825,29 +1820,19 @@ impl Display for DisplayGenericAlias<'_> { impl<'db> GenericContext<'db> { fn display<'a>(&'a self, db: &'db dyn Db) -> DisplayGenericContext<'a, 'db> { - Self::display_with(self, db, DisplaySettings::default()) - } - - fn display_full<'a>(&'a self, db: &'db dyn Db) -> DisplayGenericContext<'a, 'db> { DisplayGenericContext { generic_context: self, db, - settings: DisplaySettings::default(), - full: true, + full: false, hide_unused_self: false, } } - fn display_with<'a>( - &'a self, - db: &'db dyn Db, - settings: DisplaySettings<'db>, - ) -> DisplayGenericContext<'a, 'db> { + fn display_full<'a>(&'a self, db: &'db dyn Db) -> DisplayGenericContext<'a, 'db> { DisplayGenericContext { generic_context: self, db, - settings, - full: false, + full: true, hide_unused_self: false, } } @@ -1856,7 +1841,6 @@ impl<'db> GenericContext<'db> { struct DisplayOptionalGenericContext<'a, 'db> { generic_context: Option<&'a GenericContext<'db>>, db: &'db dyn Db, - settings: DisplaySettings<'db>, /// If true, hide `Self` type variables from the generic context prefix /// when they are not displayed in the signature body. hide_unused_self: bool, @@ -1868,7 +1852,6 @@ impl<'db> FmtDetailed<'db> for DisplayOptionalGenericContext<'_, 'db> { DisplayGenericContext { generic_context, db: self.db, - settings: self.settings.clone(), full: false, hide_unused_self: self.hide_unused_self, } @@ -1888,8 +1871,6 @@ impl Display for DisplayOptionalGenericContext<'_, '_> { struct DisplayGenericContext<'a, 'db> { generic_context: &'a GenericContext<'db>, db: &'db dyn Db, - #[expect(dead_code)] - settings: DisplaySettings<'db>, full: bool, /// If true, hide `Self` type variables from the generic context prefix. hide_unused_self: bool, @@ -2297,7 +2278,6 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { DisplayOptionalGenericContext { generic_context: self.generic_context, db: self.db, - settings: settings.clone(), hide_unused_self, } .fmt_detailed(&mut f)?; From 6407d24ceea00dcb4e8fc50f5ca8cfafad8bf949 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:12:25 +0200 Subject: [PATCH 164/390] [ty] Share symmetric narrowing-constraint branches (#27358) ## Summary Share symmetric narrowing-constraint branches. --- .../src/narrowing_constraints.rs | 46 +++++++------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/crates/ty_python_core/src/narrowing_constraints.rs b/crates/ty_python_core/src/narrowing_constraints.rs index 5740ec075d..9042a93e03 100644 --- a/crates/ty_python_core/src/narrowing_constraints.rs +++ b/crates/ty_python_core/src/narrowing_constraints.rs @@ -303,19 +303,13 @@ impl NarrowingConstraintsBuilder { if_false, }) } - Ordering::Less => { - let node = self.interiors[a]; - let if_uncertain = self.add_or_constraint(node.if_uncertain, b); - self.add_interior(InteriorNode { - atom: node.atom, - if_true: node.if_true, - if_uncertain, - if_false: node.if_false, - }) - } - Ordering::Greater => { - let node = self.interiors[b]; - let if_uncertain = self.add_or_constraint(a, node.if_uncertain); + ordering @ (Ordering::Less | Ordering::Greater) => { + let (node, other) = if ordering == Ordering::Less { + (self.interiors[a], b) + } else { + (self.interiors[b], a) + }; + let if_uncertain = self.add_or_constraint(node.if_uncertain, other); self.add_interior(InteriorNode { atom: node.atom, if_true: node.if_true, @@ -380,23 +374,15 @@ impl NarrowingConstraintsBuilder { if_false, }) } - Ordering::Less => { - let node = self.interiors[a]; - let if_true = self.add_and_constraint(node.if_true, b); - let if_uncertain = self.add_and_constraint(node.if_uncertain, b); - let if_false = self.add_and_constraint(node.if_false, b); - self.add_interior(InteriorNode { - atom: node.atom, - if_true, - if_uncertain, - if_false, - }) - } - Ordering::Greater => { - let node = self.interiors[b]; - let if_true = self.add_and_constraint(a, node.if_true); - let if_uncertain = self.add_and_constraint(a, node.if_uncertain); - let if_false = self.add_and_constraint(a, node.if_false); + ordering @ (Ordering::Less | Ordering::Greater) => { + let (node, other) = if ordering == Ordering::Less { + (self.interiors[a], b) + } else { + (self.interiors[b], a) + }; + let if_true = self.add_and_constraint(node.if_true, other); + let if_uncertain = self.add_and_constraint(node.if_uncertain, other); + let if_false = self.add_and_constraint(node.if_false, other); self.add_interior(InteriorNode { atom: node.atom, if_true, From fce5f3754b30d2a919d452a2af7650c435703362 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:13:09 +0200 Subject: [PATCH 165/390] [ty] Remove unused string-literal display settings (#27352) ## Summary Remove unused string-literal display settings. --- crates/ty_python_semantic/src/types/display.rs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index a472850a88..cd9ead41e2 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1346,11 +1346,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { .write_str(if boolean { "True" } else { "False" }) } LiteralValueTypeKind::String(string) => { - write!( - f.with_type(self.ty), - "{}", - string.display_with(self.db, self.settings.clone()), - ) + write!(f.with_type(self.ty), "{}", string.display(self.db)) } // We used to return `str` as the type here because that feels generally more useful. // However, the inconsistency between the type shown in the inlay hint and its hover, and the @@ -3170,22 +3166,15 @@ impl Display for DisplayTypeArray<'_, '_> { } impl<'db> StringLiteralType<'db> { - fn display_with( - self, - db: &'db dyn Db, - settings: DisplaySettings<'db>, - ) -> DisplayStringLiteralType<'db> { + fn display(self, db: &'db dyn Db) -> DisplayStringLiteralType<'db> { DisplayStringLiteralType { string: self.value(db), - settings, } } } struct DisplayStringLiteralType<'db> { string: &'db str, - #[expect(dead_code)] - settings: DisplaySettings<'db>, } impl Display for DisplayStringLiteralType<'_> { From 2605c5508bccd7d267ee5b2fa864f654b3a051f5 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:13:33 +0200 Subject: [PATCH 166/390] [ty] Remove unused file-scoped place identifiers (#27354) ## Summary Remove unused file-scoped place identifiers. --- crates/ty_python_core/src/place.rs | 32 ------------------------------ 1 file changed, 32 deletions(-) diff --git a/crates/ty_python_core/src/place.rs b/crates/ty_python_core/src/place.rs index b74bc6666d..e04f967d96 100644 --- a/crates/ty_python_core/src/place.rs +++ b/crates/ty_python_core/src/place.rs @@ -4,7 +4,6 @@ use crate::member::{ ScopedMemberId, }; use crate::predicate::PatternPredicate; -use crate::scope::FileScopeId; use crate::symbol::{ScopedSymbolId, Symbol, SymbolTable, SymbolTableBuilder}; use crate::{Db, PossiblyNarrowedPlaces}; use ruff_db::parsed::ParsedModuleRef; @@ -449,14 +448,6 @@ impl ScopedPlaceId { } } } - - pub const fn as_member(self) -> Option { - if let ScopedPlaceId::Member(id) = self { - Some(id) - } else { - None - } - } } impl std::ops::Index for Vec { @@ -482,29 +473,6 @@ impl From for ScopedPlaceId { } } -/// ID that uniquely identifies a place in a file. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub struct FilePlaceId { - scope: FileScopeId, - scoped_place_id: ScopedPlaceId, -} - -impl FilePlaceId { - pub fn scope(self) -> FileScopeId { - self.scope - } - - pub(crate) fn scoped_place_id(self) -> ScopedPlaceId { - self.scoped_place_id - } -} - -impl From for ScopedPlaceId { - fn from(val: FilePlaceId) -> Self { - val.scoped_place_id() - } -} - pub struct ParentPlaceIter<'a> { state: Option>, } From e1f72fb545f6eb7cbf493539a5ca745324ff7d7b Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:13:48 +0200 Subject: [PATCH 167/390] [ty] Simplify static class specialization dispatch (#27359) ## Summary Simplify static class specialization dispatch. --- crates/ty_python_semantic/src/types/class.rs | 33 +++++++------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index ea10c3f7af..c92350a0bf 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -633,13 +633,10 @@ impl<'db> ClassLiteral<'db> { /// For static classes, this applies default type arguments. /// For dynamic classes, this returns a non-generic class type. pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - match self { - Self::Static(class) => class.default_specialization(db), - Self::Dynamic(_) - | Self::DynamicNamedTuple(_) - | Self::DynamicTypedDict(_) - | Self::DynamicEnum(_) => ClassType::NonGeneric(self), - } + self.as_static().map_or_else( + || ClassType::NonGeneric(self), + |class| class.default_specialization(db), + ) } /// Returns the unknown specialization of this class. @@ -648,24 +645,18 @@ impl<'db> ClassLiteral<'db> { /// 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::Static(class) => class.unknown_specialization(db), - Self::Dynamic(_) - | Self::DynamicNamedTuple(_) - | Self::DynamicTypedDict(_) - | Self::DynamicEnum(_) => ClassType::NonGeneric(self), - } + self.as_static().map_or_else( + || ClassType::NonGeneric(self), + |class| class.unknown_specialization(db), + ) } /// Returns the identity specialization for this class (same as default for non-generic). pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - match self { - Self::Static(class) => class.identity_specialization(db), - Self::Dynamic(_) - | Self::DynamicNamedTuple(_) - | Self::DynamicTypedDict(_) - | Self::DynamicEnum(_) => ClassType::NonGeneric(self), - } + self.as_static().map_or_else( + || ClassType::NonGeneric(self), + |class| class.identity_specialization(db), + ) } /// Returns the generic context if this is a generic class. From 0b3062089aa4116c4344f46efce0a83523cbdc54 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:14:18 +0200 Subject: [PATCH 168/390] [ty] Unify symmetric bounded TypeVar comparisons (#27357) ## Summary Unify symmetric bounded TypeVar comparisons. --- .../src/types/infer/comparisons.rs | 53 ++++++------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index 5b376bf1b2..77c4bdfc59 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -564,50 +564,29 @@ fn infer_binary_type_comparison_inner<'db>( None => None, // Fall through to default handling } } - // When the left operand is a bounded TypeVar and the right is not a TypeVar, - // delegate to the bound type. - (Type::TypeVar(left_tvar), right) if !right.is_type_var() => { - match left_tvar.typevar(db).bound_or_constraints(db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { - visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison_inner( - context, bound, op, right, range, visitor, - ) - }) - })) - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut builder = UnionBuilder::new(db); - for &constraint in constraints.elements(db) { - builder = builder.add(infer_binary_type_comparison_inner( - context, constraint, op, right, range, visitor, - )?); - } - Some(Ok(builder.build())) - } - None => None, - } - } - // When the right operand is a bounded TypeVar and the left is not a TypeVar, - // delegate to the bound type. - (left, Type::TypeVar(right_tvar)) if !left.is_type_var() => { - match right_tvar.typevar(db).bound_or_constraints(db) { + // A bounded or constrained TypeVar on either side delegates to its concrete alternatives. + (Type::TypeVar(typevar), other) | (other, Type::TypeVar(typevar)) + if !other.is_type_var() => + { + let compare_replacement = |replacement| { + let (left, right) = if left.is_type_var() { + (replacement, right) + } else { + (left, replacement) + }; + infer_binary_type_comparison_inner(context, left, op, right, range, visitor) + }; + + match typevar.typevar(db).bound_or_constraints(db) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { - visitor.visit(db, (left, op, right), || { - infer_binary_type_comparison_inner( - context, left, op, bound, range, visitor, - ) - }) + visitor.visit(db, (left, op, right), || compare_replacement(bound)) })) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { let mut builder = UnionBuilder::new(db); for &constraint in constraints.elements(db) { - builder = builder.add(infer_binary_type_comparison_inner( - context, left, op, constraint, range, visitor, - )?); + builder = builder.add(compare_replacement(constraint)?); } Some(Ok(builder.build())) } From 974916cfed9bc749ff18c3f324ed1cb3f9bdf38e Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:14:40 +0200 Subject: [PATCH 169/390] [ty] Consolidate functional enum mixin conversion (#27342) ## Summary Consolidate functional enum mixin conversion. --- .../src/types/infer/builder/enum_call.rs | 66 +++++++------------ 1 file changed, 23 insertions(+), 43 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs b/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs index 9ba0dbe76b..2c43cf2ce9 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs @@ -255,49 +255,29 @@ fn apply_generated_type_mixin_member_values<'db>( return None; }; - match class.known(db) { - Some(KnownClass::Str) => Some( - members - .into_iter() - .map(|(name, value)| { - let value = if let Some(literal) = value.as_int_literal() { - Type::string_literal(db, literal.to_compact_string()) - } else if value.is_assignable_to(db, KnownClass::Int.to_instance(db)) { - KnownClass::Str.to_instance(db) - } else { - return None; - }; - Some((name, value)) - }) - .collect::>>()?, - ), - Some(KnownClass::Bytes) => Some( - members - .into_iter() - .map(|(name, value)| { - let value = if value.is_assignable_to(db, KnownClass::Int.to_instance(db)) { - KnownClass::Bytes.to_instance(db) - } else { - return None; - }; - Some((name, value)) - }) - .collect::>>()?, - ), - Some(KnownClass::Float) => Some( - members - .into_iter() - .map(|(name, value)| { - if value.is_assignable_to(db, KnownClass::Int.to_instance(db)) { - Some((name, KnownClass::Float.to_instance(db))) - } else { - None - } - }) - .collect::>>()?, - ), - _ => None, - } + let mixin_class @ (KnownClass::Str | KnownClass::Bytes | KnownClass::Float) = + class.known(db)? + else { + return None; + }; + + members + .into_iter() + .map(|(name, value)| { + if !value.is_assignable_to(db, KnownClass::Int.to_instance(db)) { + return None; + } + + let value = if mixin_class == KnownClass::Str + && let Some(literal) = value.as_int_literal() + { + Type::string_literal(db, literal.to_compact_string()) + } else { + mixin_class.to_instance(db) + }; + Some((name, value)) + }) + .collect() } impl<'db> TypeInferenceBuilder<'db, '_> { From 05ae2d1fe7b48f115d5b42842dfe963df20a4ce5 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:15:26 +0200 Subject: [PATCH 170/390] [ty] Derive the corpus workspace root without spawning Cargo (#27345) ## Summary Derive the corpus workspace root without spawning Cargo. --- crates/ty_python_semantic/tests/corpus.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/tests/corpus.rs b/crates/ty_python_semantic/tests/corpus.rs index c8aa2108e2..87fa143bf7 100644 --- a/crates/ty_python_semantic/tests/corpus.rs +++ b/crates/ty_python_semantic/tests/corpus.rs @@ -19,16 +19,11 @@ use ruff_db::diagnostic::Diagnostic; use test_case::test_case; use ty_python_core::Db as _; -fn get_cargo_workspace_root() -> anyhow::Result { - Ok(SystemPathBuf::from(String::from_utf8( - std::process::Command::new("cargo") - .args(["locate-project", "--workspace", "--message-format", "plain"]) - .output()? - .stdout, - )?) - .parent() - .unwrap() - .to_owned()) +fn get_cargo_workspace_root() -> anyhow::Result<&'static SystemPath> { + SystemPath::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(SystemPath::parent) + .context("Failed to determine the Cargo workspace root") } /// Test that all snippets in testcorpus can be checked without panic (except for [`KNOWN_FAILURES`]) From e70e9b71bb15ab528b0a614c876eaab4e7ed6b18 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:16:47 +0200 Subject: [PATCH 171/390] [ty] Unify break and continue flow handling (#27360) ## Summary Unify break and continue flow handling. --- crates/ty_python_core/src/builder.rs | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index bd97790d2b..a19472602b 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -83,16 +83,6 @@ struct Loop { continue_states: Vec, } -impl Loop { - fn push_break(&mut self, state: FlowSnapshot) { - self.break_states.push(state); - } - - fn push_continue(&mut self, state: FlowSnapshot) { - self.continue_states.push(state); - } -} - /// A narrowing alias: a variable whose RHS is a narrowing expression /// (e.g., `is_none = x is None`). #[derive(Clone, Debug)] @@ -4203,20 +4193,14 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.mark_unreachable(); } - ast::Stmt::Continue(_) => { + ast::Stmt::Continue(_) | ast::Stmt::Break(_) => { let snapshot = self.flow_snapshot(); if let Some(current_loop) = self.current_loop_mut() { - current_loop.push_continue(snapshot); - } - self.record_terminal_finally_entry(); - // Everything in the current block after a terminal statement is unreachable. - self.mark_unreachable(); - } - - ast::Stmt::Break(_) => { - let snapshot = self.flow_snapshot(); - if let Some(current_loop) = self.current_loop_mut() { - current_loop.push_break(snapshot); + if stmt.is_continue_stmt() { + current_loop.continue_states.push(snapshot); + } else { + current_loop.break_states.push(snapshot); + } } self.record_terminal_finally_entry(); // Everything in the current block after a terminal statement is unreachable. From 7e45d7b077fc7a510cbac126a0fc357909992edb Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:17:14 +0200 Subject: [PATCH 172/390] [ty] Deduplicate module-resolver test-builder transitions (#27353) ## Summary Deduplicate module-resolver test-builder transitions. --- crates/ty_module_resolver/src/testing.rs | 41 +++++++----------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/crates/ty_module_resolver/src/testing.rs b/crates/ty_module_resolver/src/testing.rs index b3af169a31..199821f4c7 100644 --- a/crates/ty_module_resolver/src/testing.rs +++ b/crates/ty_module_resolver/src/testing.rs @@ -113,6 +113,16 @@ pub(crate) struct TestCaseBuilder { } impl TestCaseBuilder { + fn with_typeshed(self, typeshed_option: U) -> TestCaseBuilder { + TestCaseBuilder { + typeshed_option, + python_version: self.python_version, + first_party_files: self.first_party_files, + site_packages_files: self.site_packages_files, + roots: self.roots, + } + } + /// Specify files to be created in the `src` mock directory pub(crate) fn with_src_files(mut self, files: &[FileSpec]) -> Self { self.first_party_files.extend(files.iter().copied()); @@ -168,20 +178,7 @@ impl TestCaseBuilder { /// Use the vendored stdlib stubs included in the Ruff binary for this test case pub(crate) fn with_vendored_typeshed(self) -> TestCaseBuilder { - let TestCaseBuilder { - typeshed_option: _, - python_version, - first_party_files, - site_packages_files, - roots, - } = self; - TestCaseBuilder { - typeshed_option: VendoredTypeshed, - python_version, - first_party_files, - site_packages_files, - roots, - } + self.with_typeshed(VendoredTypeshed) } /// Use a mock typeshed directory for this test case @@ -189,21 +186,7 @@ impl TestCaseBuilder { self, typeshed: MockedTypeshed, ) -> TestCaseBuilder { - let TestCaseBuilder { - typeshed_option: _, - python_version, - first_party_files, - site_packages_files, - roots, - } = self; - - TestCaseBuilder { - typeshed_option: typeshed, - python_version, - first_party_files, - site_packages_files, - roots, - } + self.with_typeshed(typeshed) } pub(crate) fn build(self) -> TestCase<()> { From 93ac182e1db36f5598cf9cd7d44fe6d16e44a3b9 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:17:33 +0200 Subject: [PATCH 173/390] [ty] Remove unused nonlocal symbol lookup (#27363) ## Summary Remove unused nonlocal symbol lookup. --- crates/ty_python_core/src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/ty_python_core/src/lib.rs b/crates/ty_python_core/src/lib.rs index 2308bed39e..2d6b02eda2 100644 --- a/crates/ty_python_core/src/lib.rs +++ b/crates/ty_python_core/src/lib.rs @@ -433,10 +433,6 @@ impl<'db> SemanticIndex<'db> { self.place_table(scope).symbol(symbol).is_global() } - pub fn symbol_is_nonlocal_in_scope(&self, symbol: ScopedSymbolId, scope: FileScopeId) -> bool { - self.place_table(scope).symbol(symbol).is_nonlocal() - } - /// Returns `true` if the given symbol in the given scope resolves to the global scope, either /// because: /// From 28e5bf0216e9d37d8d0f514a6ffb048b382b965e Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:19:04 +0200 Subject: [PATCH 174/390] [ty] Share dynamic class header-range reconstruction (#27356) ## Summary Share dynamic class header-range reconstruction. --- crates/ty_python_semantic/src/types/class.rs | 45 ++++++++++++++++++- .../src/types/class/dynamic_literal.rs | 38 ++++------------ .../src/types/class/enum_literal.rs | 36 +++++---------- .../src/types/class/named_tuple.rs | 42 +++++------------ .../src/types/class/typed_dict.rs | 39 +++------------- 5 files changed, 83 insertions(+), 117 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index c92350a0bf..0bb6c040a3 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -53,10 +53,12 @@ use crate::{ }; use ruff_db::diagnostic::Span; use ruff_db::files::File; +use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; -use ruff_python_ast::{self as ast}; -use ruff_text_size::TextRange; +use ruff_python_ast::{self as ast, NodeIndex}; +use ruff_text_size::{Ranged, TextRange}; use ty_python_core::definition::Definition; +use ty_python_core::scope::ScopeId; use ty_python_core::{place_table, use_def_map}; mod dynamic_literal; @@ -66,6 +68,45 @@ mod named_tuple; mod static_literal; mod typed_dict; +#[derive(Clone, Copy)] +enum DynamicClassHeaderAnchor<'db> { + Definition(Definition<'db>), + ScopeOffset(u32), +} + +/// Returns the source range of a call that creates a dynamic class. +/// +/// ```python +/// Color = Enum("Color", "RED GREEN") +/// # ^^^^^^^^^^^^^^^^^^^^^^^^^^ +/// ``` +fn dynamic_class_header_range<'db>( + db: &'db dyn Db, + scope: ScopeId<'db>, + anchor: DynamicClassHeaderAnchor<'db>, +) -> TextRange { + let module = parsed_module(db, scope.file(db)).load(db); + match anchor { + DynamicClassHeaderAnchor::Definition(definition) => definition + .kind(db) + .value(&module) + .expect("dynamic class definitions should only be used for assignments") + .range(), + DynamicClassHeaderAnchor::ScopeOffset(offset) => { + let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); + let anchor_u32 = scope_anchor + .as_u32() + .expect("anchor should not be NodeIndex::NONE"); + let absolute_index = NodeIndex::from(anchor_u32 + offset); + let node: &ast::ExprCall = module + .get_by_index(absolute_index) + .try_into() + .expect("scope offset should point to ExprCall"); + node.range() + } + } +} + bitflags::bitflags! { /// Properties that affect the representation of instances of a class. /// diff --git a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs index 809ceec851..80bd1b33a6 100644 --- a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs +++ b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs @@ -1,6 +1,6 @@ use ruff_db::{diagnostic::Span, parsed::parsed_module}; -use ruff_python_ast::{self as ast, NodeIndex, name::Name}; -use ruff_text_size::{Ranged, TextRange}; +use ruff_python_ast::{self as ast, name::Name}; +use ruff_text_size::TextRange; use crate::{ Db, TypeQualifiers, @@ -9,7 +9,8 @@ use crate::{ ClassBase, ClassLiteral, ClassType, DataclassParams, KnownClass, MemberLookupPolicy, SubclassOfType, Type, class::{ - ClassMemberResult, CodeGeneratorKind, DisjointBase, InstanceMemberResult, MroLookup, + ClassMemberResult, CodeGeneratorKind, DisjointBase, DynamicClassHeaderAnchor, + InstanceMemberResult, MroLookup, dynamic_class_header_range, typed_dict::typed_dict_fallback_class_member, }, definition_expression_type, extract_fixed_length_iterable_element_types, @@ -237,36 +238,15 @@ impl<'db> DynamicClassLiteral<'db> { /// Returns the range of the `type()` call expression that created this class. pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); - - match self.anchor(db) { + let anchor = match self.anchor(db) { DynamicClassAnchor::Definition(definition) => { - // For definitions, get the range from the definition's value. - // The `type()` call is the value of the assignment. - definition - .kind(db) - .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments") - .range() + DynamicClassHeaderAnchor::Definition(*definition) } DynamicClassAnchor::ScopeOffset { offset, .. } => { - // For dangling `type()` calls, compute the absolute index from the offset. - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + *offset); - - // Get the node and return its range. - let node: &ast::ExprCall = module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall"); - node.range() + DynamicClassHeaderAnchor::ScopeOffset(*offset) } - } + }; + dynamic_class_header_range(db, self.scope(db), anchor) } /// Get the metaclass of this dynamic class. diff --git a/crates/ty_python_semantic/src/types/class/enum_literal.rs b/crates/ty_python_semantic/src/types/class/enum_literal.rs index 69b01345ea..2d9eec11c0 100644 --- a/crates/ty_python_semantic/src/types/class/enum_literal.rs +++ b/crates/ty_python_semantic/src/types/class/enum_literal.rs @@ -1,14 +1,15 @@ use ruff_db::diagnostic::Span; -use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; -use ruff_python_ast::{self as ast, NodeIndex}; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::TextRange; use crate::Db; use crate::place::{Place, PlaceAndQualifiers}; use crate::types::Type; use crate::types::class::known::KnownClass; -use crate::types::class::{ClassLiteral, ClassType, MemberLookupPolicy}; +use crate::types::class::{ + ClassLiteral, ClassType, DynamicClassHeaderAnchor, MemberLookupPolicy, + dynamic_class_header_range, +}; use crate::types::class_base::ClassBase; use crate::types::member::Member; use crate::types::mro::{DynamicMroError, Mro}; @@ -164,28 +165,15 @@ impl<'db> DynamicEnumLiteral<'db> { } pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); - match self.anchor(db) { - DynamicEnumAnchor::Definition { definition, .. } => definition - .kind(db) - .value(&module) - .expect("DynamicEnumAnchor::Definition should only be used for assignments") - .range(), + let anchor = match self.anchor(db) { + DynamicEnumAnchor::Definition { definition, .. } => { + DynamicClassHeaderAnchor::Definition(*definition) + } DynamicEnumAnchor::ScopeOffset { offset, .. } => { - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + offset); - let node: &ast::ExprCall = module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall"); - node.range() + DynamicClassHeaderAnchor::ScopeOffset(*offset) } - } + }; + dynamic_class_header_range(db, self.scope(db), anchor) } pub(super) fn header_span(self, db: &'db dyn Db) -> Span { diff --git a/crates/ty_python_semantic/src/types/class/named_tuple.rs b/crates/ty_python_semantic/src/types/class/named_tuple.rs index 44a840fce7..f0d18d11e9 100644 --- a/crates/ty_python_semantic/src/types/class/named_tuple.rs +++ b/crates/ty_python_semantic/src/types/class/named_tuple.rs @@ -1,7 +1,6 @@ use ruff_db::{diagnostic::Span, parsed::parsed_module}; -use ruff_python_ast as ast; -use ruff_python_ast::{NodeIndex, PythonVersion, name::Name}; -use ruff_text_size::{Ranged, TextRange}; +use ruff_python_ast::{PythonVersion, name::Name}; +use ruff_text_size::TextRange; use crate::{ Db, Program, @@ -10,7 +9,11 @@ use crate::{ BindingContext, BoundTypeVarInstance, ClassBase, ClassLiteral, ClassType, GenericContext, KnownClass, KnownInstanceType, MemberLookupPolicy, Parameter, Parameters, PropertyInstanceType, Signature, SubclassOfType, Type, TypeContext, TypeMapping, - definition_expression_type, member::Member, mro::Mro, tuple::TupleType, + class::{DynamicClassHeaderAnchor, dynamic_class_header_range}, + definition_expression_type, + member::Member, + mro::Mro, + tuple::TupleType, }, }; use ty_python_core::{definition::Definition, scope::ScopeId}; @@ -204,37 +207,16 @@ impl<'db> DynamicNamedTupleLiteral<'db> { /// Returns the range of the namedtuple call expression. pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); - - match self.anchor(db) { + let anchor = match self.anchor(db) { DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } | DynamicNamedTupleAnchor::TypingDefinition(definition) => { - // For definitions, get the range from the definition's value. - // The namedtuple call is the value of the assignment. - definition - .kind(db) - .value(&module) - .expect("DynamicClassAnchor::Definition should only be used for assignments") - .range() + DynamicClassHeaderAnchor::Definition(*definition) } DynamicNamedTupleAnchor::ScopeOffset { offset, .. } => { - // For dangling calls, compute the absolute index from the offset. - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + offset); - - // Get the node and return its range. - let node: &ast::ExprCall = module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall"); - node.range() + DynamicClassHeaderAnchor::ScopeOffset(*offset) } - } + }; + dynamic_class_header_range(db, self.scope(db), anchor) } /// Returns a [`Span`] pointing to the namedtuple call expression. diff --git a/crates/ty_python_semantic/src/types/class/typed_dict.rs b/crates/ty_python_semantic/src/types/class/typed_dict.rs index 5394fc50d8..810c2f185e 100644 --- a/crates/ty_python_semantic/src/types/class/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/class/typed_dict.rs @@ -2,17 +2,15 @@ use std::borrow::Cow; use itertools::Either; use ruff_db::diagnostic::Span; -use ruff_db::parsed::parsed_module; -use ruff_python_ast as ast; -use ruff_python_ast::NodeIndex; use ruff_python_ast::name::Name; use ruff_python_stdlib::identifiers::is_identifier; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::TextRange; use ty_module_resolver::KnownModule; use crate::place::PlaceAndQualifiers; use crate::place::known_module_symbol; use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; +use crate::types::class::{DynamicClassHeaderAnchor, dynamic_class_header_range}; use crate::types::generics::GenericContext; use crate::types::member::Member; use crate::types::mro::Mro; @@ -871,38 +869,15 @@ impl<'db> DynamicTypedDictLiteral<'db> { /// Returns the range of the `TypedDict` call expression. pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { - let scope = self.scope(db); - let file = scope.file(db); - let module = parsed_module(db, file).load(db); - - match self.anchor(db) { + let anchor = match self.anchor(db) { DynamicTypedDictAnchor::Definition(definition) => { - // For definitions, get the range from the definition's value. - // The TypedDict call is the value of the assignment. - definition - .kind(db) - .value(&module) - .expect( - "DynamicTypedDictAnchor::Definition should only be used for assignments", - ) - .range() + DynamicClassHeaderAnchor::Definition(*definition) } DynamicTypedDictAnchor::ScopeOffset { offset, .. } => { - // For dangling calls, compute the absolute index from the offset. - let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0)); - let anchor_u32 = scope_anchor - .as_u32() - .expect("anchor should not be NodeIndex::NONE"); - let absolute_index = NodeIndex::from(anchor_u32 + offset); - - // Get the node and return its range. - let node: &ast::ExprCall = module - .get_by_index(absolute_index) - .try_into() - .expect("scope offset should point to ExprCall"); - node.range() + DynamicClassHeaderAnchor::ScopeOffset(*offset) } - } + }; + dynamic_class_header_range(db, self.scope(db), anchor) } /// Returns a [`Span`] pointing to the `TypedDict` call expression. From 03a54724cad0615b1b1be914aa58706ae76eb76b Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:19:49 +0200 Subject: [PATCH 175/390] [ty] Remove bespoke inference VecMap iterator (#27362) ## Summary Remove bespoke inference VecMap iterator. --- .../src/types/infer/builder.rs | 37 ++----------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 04a4d4c82e..2f1f10accf 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1064,7 +1064,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut seen_overloaded_places = FxHashSet::default(); let mut seen_public_functions = FxHashSet::default(); - for (&definition, ty_and_quals) in &self.declarations { + for (&definition, ty_and_quals) in self.declarations.iter() { let ty = ty_and_quals.inner_type(); match definition.kind(self.db()) { DefinitionKind::Function(function) => { @@ -11830,10 +11830,8 @@ impl VecMap { self.0.is_empty() } - fn iter(&self) -> VecMapIterator<'_, K, V> { - VecMapIterator { - inner: self.0.iter(), - } + fn iter(&self) -> impl ExactSizeIterator { + self.0.iter().map(|(key, value)| (key, value)) } fn into_boxed_slice(self) -> Box<[(K, V)]> { @@ -11878,35 +11876,6 @@ impl Default for VecMap { } } -impl<'a, K, V> IntoIterator for &'a VecMap { - type Item = (&'a K, &'a V); - type IntoIter = VecMapIterator<'a, K, V>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -struct VecMapIterator<'a, K, V> { - inner: std::slice::Iter<'a, (K, V)>, -} - -impl<'a, K, V> Iterator for VecMapIterator<'a, K, V> { - type Item = (&'a K, &'a V); - - fn next(&mut self) -> Option { - self.inner.next().map(|(k, v)| (k, v)) - } -} - -impl std::iter::FusedIterator for VecMapIterator<'_, K, V> {} - -impl ExactSizeIterator for VecMapIterator<'_, K, V> { - fn len(&self) -> usize { - self.inner.len() - } -} - /// Set based on a `Vec`. It doesn't enforce /// uniqueness on insertion. Instead, it relies on the caller /// that elements are unique. For example, the way we visit definitions From cf3ddc324c5bc933fea006fb62056fcb60f15aeb Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:20:09 +0200 Subject: [PATCH 176/390] [ty] Merge duplicated VecSet implementation blocks (#27365) ## Summary Merge duplicated VecSet implementation blocks. --- crates/ty_python_semantic/src/types/infer/builder.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 2f1f10accf..8c6deb53de 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -11908,13 +11908,7 @@ where self.0.push(value); } -} -impl VecSet -where - V: Eq, - V: std::fmt::Debug, -{ #[inline] fn extend>(&mut self, iter: T) { if cfg!(debug_assertions) { From 81f97ec410abfe04a4535dc8bfe232c87ae9bb9f Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:21:01 +0200 Subject: [PATCH 177/390] [ty] Remove empty corpus known-failure machinery (#27344) ## Summary Remove empty corpus known-failure machinery. --- crates/ty_python_semantic/tests/corpus.rs | 40 +++-------------------- 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/crates/ty_python_semantic/tests/corpus.rs b/crates/ty_python_semantic/tests/corpus.rs index 87fa143bf7..6b6cffcf10 100644 --- a/crates/ty_python_semantic/tests/corpus.rs +++ b/crates/ty_python_semantic/tests/corpus.rs @@ -26,7 +26,7 @@ fn get_cargo_workspace_root() -> anyhow::Result<&'static SystemPath> { .context("Failed to determine the Cargo workspace root") } -/// Test that all snippets in testcorpus can be checked without panic (except for [`KNOWN_FAILURES`]) +/// Test that all snippets in testcorpus can be checked without panic. #[test] fn corpus_no_panic() -> anyhow::Result<()> { let crate_root = String::from(env!("CARGO_MANIFEST_DIR")); @@ -97,17 +97,6 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> { let relative_path = path.strip_prefix(&workspace_root)?; - let (py_expected_to_fail, pyi_expected_to_fail) = KNOWN_FAILURES - .iter() - .find_map(|(path, py_fail, pyi_fail)| { - if *path == relative_path.as_str().replace('\\', "/") { - Some((*py_fail, *pyi_fail)) - } else { - None - } - }) - .unwrap_or((false, false)); - let source = path.as_path(); let source_filename = source.file_name().unwrap(); @@ -122,25 +111,9 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> { // (and some non-expressions that clearly define a single type) let file = system_path_to_file(&db, path).unwrap(); - let result = std::panic::catch_unwind(|| pull_types(&db, file)); - - let expected_to_fail = if path.extension().map(|e| e == "pyi").unwrap_or(false) { - pyi_expected_to_fail - } else { - py_expected_to_fail - }; - if let Err(err) = result { - if !expected_to_fail { - println!( - "Check failed for {relative_path:?}. Consider fixing it or adding it to KNOWN_FAILURES" - ); - std::panic::resume_unwind(err); - } - } else { - assert!( - !expected_to_fail, - "Expected to panic, but did not. Consider removing this path from KNOWN_FAILURES" - ); + if let Err(err) = std::panic::catch_unwind(|| pull_types(&db, file)) { + println!("Check failed for {relative_path:?}."); + std::panic::resume_unwind(err); } db.memory_file_system().remove_file(path).unwrap(); @@ -165,11 +138,6 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> { Ok(()) } -/// Whether or not the .py/.pyi version of this file is expected to fail -#[rustfmt::skip] -const KNOWN_FAILURES: &[(&str, bool, bool)] = &[ -]; - #[salsa::db] #[derive(Clone)] pub struct CorpusDb { From 0e24940bd18b93e6bc8a2ab61099b2aeade03f0a Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:21:17 +0200 Subject: [PATCH 178/390] [ty] Reuse tuple length accessors for size hints (#27366) ## Summary Reuse tuple length accessors for size hints. --- crates/ty_python_semantic/src/types/tuple.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 6ad3c50045..b1a0bb6e6b 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -58,10 +58,7 @@ impl TupleLength { /// Returns the minimum and maximum length of this tuple. (The maximum length will be `None` /// for a tuple with a variable-length portion.) pub(crate) fn size_hint(self) -> (usize, Option) { - match self { - TupleLength::Fixed(len) => (len, Some(len)), - TupleLength::Variable(prefix, suffix) => (prefix + suffix, None), - } + (self.minimum(), self.maximum()) } /// Returns the minimum length of this tuple. From 9dfeff4c0fd3a848a20ab9359137ae1d2efde871 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:22:32 +0200 Subject: [PATCH 179/390] [ty] Remove single-use current-assignment conversions (#27361) ## Summary Remove single-use current-assignment conversions. --- crates/ty_python_core/src/builder.rs | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index a19472602b..a1a8b7ff06 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -3477,7 +3477,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { *node.target, ast::Expr::Attribute(_) | ast::Expr::Subscript(_) | ast::Expr::Name(_) ) { - self.push_assignment(node.into()); + self.push_assignment(CurrentAssignment::AnnAssign(node)); self.visit_expr(&node.target); self.pop_assignment(); @@ -3510,12 +3510,12 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { } } - self.push_assignment(aug_assign.into()); + self.push_assignment(CurrentAssignment::AugAssign(aug_assign)); self.visit_expr(target); self.pop_assignment(); } ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => { - self.push_assignment(aug_assign.into()); + self.push_assignment(CurrentAssignment::AugAssign(aug_assign)); self.visit_expr(target); self.pop_assignment(); } @@ -4671,7 +4671,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { // See https://peps.python.org/pep-0572/#differences-between-assignment-expressions-and-assignment-statements if node.target.is_name_expr() { - self.push_assignment(node.into()); + self.push_assignment(CurrentAssignment::Named(node)); self.visit_expr(&node.target); self.pop_assignment(); } else { @@ -5189,24 +5189,6 @@ impl CurrentAssignment<'_, '_> { } } -impl<'ast> From<&'ast ast::StmtAnnAssign> for CurrentAssignment<'ast, '_> { - fn from(value: &'ast ast::StmtAnnAssign) -> Self { - Self::AnnAssign(value) - } -} - -impl<'ast> From<&'ast ast::StmtAugAssign> for CurrentAssignment<'ast, '_> { - fn from(value: &'ast ast::StmtAugAssign) -> Self { - Self::AugAssign(value) - } -} - -impl<'ast> From<&'ast ast::ExprNamed> for CurrentAssignment<'ast, '_> { - fn from(value: &'ast ast::ExprNamed) -> Self { - Self::Named(value) - } -} - #[derive(Default)] struct CurrentStatement<'ast, 'db> { /// A list of lambda expressions contained in this statement. From 8ebf0eb8fa644c5a5de6b8d1f4cb66421fd44e8d Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 10:48:54 +0200 Subject: [PATCH 180/390] [ty] Restore frozen-collection APIs (#27367) ## Summary Revert #27347 and restore the consuming `IntoIterator` implementation for `FrozenMap`. See comment by Micha [here](https://github.com/astral-sh/ruff/pull/27347#discussion_r3689046712). --- crates/ty_python_core/src/frozen.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/ty_python_core/src/frozen.rs b/crates/ty_python_core/src/frozen.rs index ab018689dd..6df174dd19 100644 --- a/crates/ty_python_core/src/frozen.rs +++ b/crates/ty_python_core/src/frozen.rs @@ -86,6 +86,15 @@ impl std::ops::Index<&K> for FrozenMap { } } +impl IntoIterator for FrozenMap { + type Item = (K, V); + type IntoIter = std::vec::IntoIter<(K, V)>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_vec().into_iter() + } +} + impl<'a, K, V> IntoIterator for &'a FrozenMap { type Item = &'a (K, V); type IntoIter = std::slice::Iter<'a, (K, V)>; From 88afdefde0d1d57413ec8a9481f2e3db4510d323 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 04:52:08 -0400 Subject: [PATCH 181/390] [ty] Diagnose dataclass fields after inherited defaults (#27327) ## Summary We now enforce dataclass constructor field ordering across inherited fields, including `@dataclass_transform` classes. ```python from dataclasses import dataclass @dataclass class Base: x: int = 1 @dataclass class Child(Base): y: int # error: [dataclass-field-order] ``` Fields inherited from `@dataclass_transform(kw_only_default=True)` remain keyword-only, and overriding an inherited field with `ClassVar` removes it from the generated constructor without incorrectly excluding `InitVar` overrides. Closes https://github.com/astral-sh/ty/issues/4125. --- .../mdtest/dataclasses/dataclass_transform.md | 26 ++ .../mdtest/dataclasses/dataclasses.md | 254 +++++++++++++++++- .../src/types/class/static_literal.rs | 85 +++++- .../builder/post_inference/static_class.rs | 141 +++++++--- 4 files changed, 456 insertions(+), 50 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md index 6cda27020c..219dffc4d8 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md @@ -1418,6 +1418,10 @@ class InvalidModel: x: int = 1 y: str # error: [dataclass-field-order] +@create_model +class InvalidInheritedModel(ValidModel): + z: bytes # error: [dataclass-field-order] + @dataclass_transform(field_specifiers=(field,), kw_only_default=True) def create_kwonly_default_model[T](cls: type[T]) -> type[T]: ... @@ -1564,6 +1568,28 @@ reveal_type(t.key) # revealed: int reveal_type(t.name) # revealed: str ``` +Dataclass-transform defaults remain attached to inherited fields even when a subclass is explicitly +decorated with `@dataclass`. + +```py +@dataclass_transform(kw_only_default=True) +class KeywordOnlyModelMeta(type): + pass + +class RequiredModel(metaclass=KeywordOnlyModelMeta): + required: int + +class OptionalModel(metaclass=KeywordOnlyModelMeta): + optional: int = 1 + +@dataclass(kw_only=True) +class Child(RequiredModel, OptionalModel): + pass + +reveal_type(Child.__init__) # revealed: (self: Child, *, optional: int = 1, required: int) -> None +Child(required=1) +``` + ## `__dataclass_fields__` and `DataclassInstance` protocol Classes created via `dataclass_transform` should have `__dataclass_fields__` and diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 8bf457b0fe..7e2f477db6 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -169,10 +169,8 @@ class GoodWithClassInitFalse: GoodWithClassInitFalse("value") -# Re-enabling `init` makes the inherited default-before-required ordering invalid at runtime. -# TODO: error: [dataclass-field-order] @dataclass -class BadWithReenabledInit(GoodWithClassInitFalse): +class BadWithReenabledInit(GoodWithClassInitFalse): # error: [dataclass-field-order] pass ``` @@ -1870,6 +1868,256 @@ Derived(1, "a") Derived(True) ``` +### Required fields after inherited defaults + +A required positional field cannot follow a positional field with a default inherited from a +dataclass base. + +```toml +[environment] +python-version = "3.10" +``` + +```py +from dataclasses import dataclass, field + +@dataclass +class DefaultedBase: + x: int = 1 + +@dataclass +class InvalidChild(DefaultedBase): + # error: [dataclass-field-order] "Required field `y` cannot be defined after fields with default values" + y: int +``` + +A default factory also makes an inherited field optional. + +```py +@dataclass +class DefaultFactoryBase: + x: list[int] = field(default_factory=list) + +@dataclass +class InvalidDefaultFactoryChild(DefaultFactoryBase): + # error: [dataclass-field-order] + y: int +``` + +An ordering violation already present in an ancestor is not reported again on its descendants. + +```py +@dataclass +class InvalidAncestor: + optional: int = 1 + required: int # error: [dataclass-field-order] + +@dataclass +class ChildOfInvalidAncestor(InvalidAncestor): + pass + +@dataclass +class GrandchildOfInvalidAncestor(ChildOfInvalidAncestor): + pass +``` + +Suppressing the original diagnostic also suppresses that inherited violation throughout the +hierarchy. + +```py +@dataclass +class IgnoredInvalidAncestor: + optional: int = 1 + required: int # ty: ignore[dataclass-field-order] + +@dataclass +class ChildOfIgnoredAncestor(IgnoredInvalidAncestor): + pass + +@dataclass +class GrandchildOfIgnoredAncestor(ChildOfIgnoredAncestor): + pass +``` + +Redeclaring fields can introduce a new violation even when the same required field had an ignored +violation in an ancestor. + +```py +@dataclass +class IgnoredViolationsBase: + first: int = 1 + second: int # ty: ignore[dataclass-field-order] + third: int # ty: ignore[dataclass-field-order] + +@dataclass +class NewlyInvalidOverride(IgnoredViolationsBase): + first: int = field() + second: int = 1 + third: int = field() # error: [dataclass-field-order] +``` + +Combining independently valid bases can introduce a new ordering violation even when the child +declares no fields. + +```py +@dataclass +class DefaultOnlyBase: + optional: int = 1 + +@dataclass +class RequiredOnlyBase: + required: int + +@dataclass +class InvalidMergedBases(RequiredOnlyBase, DefaultOnlyBase): # error: [dataclass-field-order] + pass + +@dataclass +class ValidMergedBases(DefaultOnlyBase, RequiredOnlyBase): + pass +``` + +Inherited fields that are keyword-only or excluded from `__init__` do not affect positional field +ordering, and a required child field can itself be keyword-only. + +```py +@dataclass +class KeywordOnlyBase: + x: int = field(default=1, kw_only=True) + +@dataclass +class ValidKeywordOnlyBaseChild(KeywordOnlyBase): + y: int + +@dataclass +class NonInitBase: + x: int = field(default=1, init=False) + +@dataclass +class ValidNonInitBaseChild(NonInitBase): + y: int + +@dataclass +class ValidKeywordOnlyChild(DefaultedBase): + y: int = field(kw_only=True) +``` + +Overriding a field preserves its original position in the inherited field order. Removing its +default permits later required fields, while introducing a default before another inherited required +field is invalid. + +```py +@dataclass +class ValidRequiredOverride(DefaultedBase): + x: int = field() + y: int + +@dataclass +class RequiredBase: + first: int + second: int + +@dataclass +class InvalidDefaultOverride(RequiredBase): # error: [dataclass-field-order] + first: int = 1 +``` + +### Class variables overriding inherited fields + +Redeclaring an inherited instance field as a class variable removes it from the generated +constructor and positional ordering checks. The override itself remains invalid. + +```py +from dataclasses import InitVar, dataclass, field +from typing import ClassVar + +@dataclass +class DefaultedFieldBase: + x: int = 1 + +@dataclass +class ClassVariableOverride(DefaultedFieldBase): + x: ClassVar[int] = 1 # error: [invalid-attribute-override] + y: int + +reveal_type(ClassVariableOverride.__init__) # revealed: (self: ClassVariableOverride, y: int) -> None + +@dataclass +class InheritedClassVariableOverride(ClassVariableOverride): + z: int + +reveal_type(InheritedClassVariableOverride.__init__) # revealed: (self: InheritedClassVariableOverride, y: int, z: int) -> None +``` + +A class variable declared by an undecorated intermediate class does not remove the inherited +dataclass field. + +```py +class OrdinaryClassVariableOverride(DefaultedFieldBase): + x: ClassVar[int] = 1 # error: [invalid-attribute-override] + +@dataclass +class DataclassAfterOrdinaryOverride(OrdinaryClassVariableOverride): + y: int # error: [dataclass-field-order] +``` + +An annotation-only class variable also masks the inherited instance field. + +```py +@dataclass +class AnnotationOnlyClassVariableOverride(DefaultedFieldBase): + x: ClassVar[int] # error: [invalid-attribute-override] + y: int + +reveal_type(AnnotationOnlyClassVariableOverride.__init__) # revealed: (self: AnnotationOnlyClassVariableOverride, y: int) -> None +``` + +Restoring an instance field in a later subclass preserves the field's original inherited position. + +```py +@dataclass +class RestoredInstanceField(ClassVariableOverride): + x: int = field() # error: [invalid-attribute-override] + +reveal_type(RestoredInstanceField.__init__) # revealed: (self: RestoredInstanceField, x: int, y: int) -> None +``` + +An initialization-only field overrides an inherited class variable and remains a constructor +parameter. + +```py +@dataclass +class ClassVariableBase: + value: ClassVar[int] + +@dataclass +class InitializationVariableOverride(ClassVariableBase): + value: InitVar[int] + +reveal_type(InitializationVariableOverride.__init__) # revealed: (self: InitializationVariableOverride, value: int) -> None +InitializationVariableOverride(1) +``` + +### Fields named after generated dataclass attributes + +Fields named after generated dataclass attributes are still ordinary constructor parameters. + +```py +from dataclasses import dataclass + +@dataclass +class DataclassFieldsConstructorField: + __dataclass_fields__: int + +DataclassFieldsConstructorField(1) + +@dataclass +class DataclassParamsConstructorField: + __dataclass_params__: int + +DataclassParamsConstructorField(1) +``` + ### Overwriting attributes from base class The following example comes from the diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index cdd45ce7ee..df0ac4049d 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -188,6 +188,24 @@ struct InheritedFrozenDataclassFields<'db> { last_frozen_base: StaticClassLiteral<'db>, } +/// Annotated fields and class-variable declarations collected from one class body. +/// +/// Class variables are not constructor parameters, but they can mask inherited dataclass fields: +/// +/// ```python +/// @dataclass +/// class Child(Base): +/// value: ClassVar[int] +/// required: int +/// ``` +/// +/// Here, `required` is a constructor field and `value` masks an inherited `Base.value` field. +#[derive(Debug, Default, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +struct OwnClassFields<'db> { + fields: FxIndexMap>, + class_variables: Box<[Name]>, +} + #[salsa::tracked] impl<'db> StaticClassLiteral<'db> { /// Return `true` if this class represents `known_class` @@ -2178,6 +2196,7 @@ impl<'db> StaticClassLiteral<'db> { "Collecting `fields` for NamedTuples should short-circuit in `fields()`" ); + let mut class_variables = FxIndexSet::default(); let mut map: FxIndexMap<_, _> = self .iter_mro(db, specialization) .rev() @@ -2202,12 +2221,23 @@ impl<'db> StaticClassLiteral<'db> { None }) .flat_map(|source| match source { - FieldSource::Static(class, specialization) => Either::Left( - class - .own_fields(db, specialization, field_policy) - .iter() - .map(|(name, field)| (name.clone(), field.clone())), - ), + FieldSource::Static(class, specialization) => { + let own_fields = class.own_fields_inner(db, specialization, field_policy); + + if field_policy.is_dataclass_like() { + class_variables.extend(own_fields.class_variables.iter().cloned()); + for name in own_fields.fields.keys() { + class_variables.swap_remove(name); + } + } + + Either::Left( + own_fields + .fields + .iter() + .map(|(name, field)| (name.clone(), field.clone())), + ) + } FieldSource::DynamicTypedDict(typeddict) => { Either::Right(typeddict.items(db).iter().map(|(name, td_field)| { ( @@ -2230,6 +2260,12 @@ impl<'db> StaticClassLiteral<'db> { // We collect into a FxOrderMap here to deduplicate attributes .collect(); + if field_policy.is_dataclass_like() { + // `own_fields` excludes class variables, but their declarations can still mask + // inherited fields. Delay removal so restoring a field preserves its original slot. + map.retain(|name, _| !class_variables.contains(name)); + } + map.shrink_to_fit(); map } @@ -2313,17 +2349,31 @@ impl<'db> StaticClassLiteral<'db> { /// including properties inherited from class-level dataclass parameters (like `kw_only=True`) /// and dataclass-transform parameters (like `kw_only_default=True`). They do not represent /// only what is explicitly specified in each field definition. + pub(crate) fn own_fields( + self, + db: &'db dyn Db, + specialization: Option>, + field_policy: CodeGeneratorKind<'db>, + ) -> &'db FxIndexMap> { + &self + .own_fields_inner(db, specialization, field_policy) + .fields + } + + /// Collects ordered constructor fields and `ClassVar` masks in one pass over a class body. + /// + /// Keeping both together avoids reinterpreting declarations while merging inherited fields. #[salsa::tracked( returns(ref), - cycle_initial=|_, _, _, _, _| FxIndexMap::default(), + cycle_initial=|_, _, _, _, _| OwnClassFields::default(), heap_size=get_size2::GetSize::get_heap_size )] - pub(crate) fn own_fields( + fn own_fields_inner( self, db: &'db dyn Db, specialization: Option>, field_policy: CodeGeneratorKind<'db>, - ) -> FxIndexMap> { + ) -> OwnClassFields<'db> { let class_body_scope = self.body_scope(db); let table = place_table(db, class_body_scope); @@ -2340,9 +2390,11 @@ impl<'db> StaticClassLiteral<'db> { } else { false }; - let dataclass_kw_only_default = field_policy - .is_dataclass_like() - .then(|| self.has_dataclass_param(db, field_policy, DataclassFlags::KW_ONLY)); + let dataclass_kw_only_default = field_policy.is_dataclass_like().then(|| { + let own_field_policy = + CodeGeneratorKind::from_class(db, self.into()).unwrap_or(field_policy); + self.has_dataclass_param(db, own_field_policy, DataclassFlags::KW_ONLY) + }); let mut kw_only_sentinel_field_seen = false; let mut field_declarations = Vec::new(); @@ -2389,11 +2441,15 @@ impl<'db> StaticClassLiteral<'db> { .sort_unstable_by_key(|(first_declaration_order, _, _)| *first_declaration_order); let mut attributes = FxIndexMap::default(); + let mut class_variables = Vec::new(); for (_, symbol_id, result) in field_declarations { let symbol = table.symbol(symbol_id); let first_declaration = result.first_declaration; let attr = result.ignore_conflicting_declarations(); if attr.is_class_var() { + if field_policy.is_dataclass_like() { + class_variables.push(symbol.name().clone()); + } continue; } @@ -2506,7 +2562,10 @@ impl<'db> StaticClassLiteral<'db> { attributes.shrink_to_fit(); - attributes + OwnClassFields { + fields: attributes, + class_variables: class_variables.into_boxed_slice(), + } } /// Look up an instance attribute (available in `__dict__`) of the given name. diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs index 0d502eed31..3891f5e18e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs @@ -18,7 +18,7 @@ use crate::{ SpecialFormType, StaticClassLiteral, Type, TypeVarVariance, TypedDictModule, binding_type, call::Argument, class::{ - AbstractMethod, CodeGeneratorKind, FieldKind, MetaclassErrorKind, + AbstractMethod, CodeGeneratorKind, Field, FieldKind, MetaclassErrorKind, expanded_class_base_entries, }, context::InferContext, @@ -917,17 +917,16 @@ pub(crate) fn check_static_class_definitions<'db>( { let specialization = None; let class_init = class.has_dataclass_param(db, field_policy, DataclassFlags::INIT); + let own_fields = class.own_fields(db, specialization, field_policy); - let mut kw_only_sentinel_fields = vec![]; - let mut required_after_default_field_names = vec![]; - let mut has_seen_default_field = false; - - for (name, field) in class.own_fields(db, specialization, field_policy) { - if field.is_kw_only_sentinel(db) { - kw_only_sentinel_fields.push(name); - continue; - } + let kw_only_sentinel_fields: Vec<_> = own_fields + .iter() + .filter_map(|(name, field)| field.is_kw_only_sentinel(db).then_some(name)) + .collect(); + let mut field_order_violations = vec![]; + let mut previous_default_field = None; + for (name, field) in class.fields(db, specialization, field_policy) { // Extract dataclass field properties let FieldKind::Dataclass { default_ty, @@ -945,9 +944,9 @@ pub(crate) fn check_static_class_definitions<'db>( } if default_ty.is_some() { - has_seen_default_field = true; - } else if has_seen_default_field { - required_after_default_field_names.push(name); + previous_default_field = Some((name, field)); + } else if let Some((default_name, default_field)) = previous_default_field { + field_order_violations.push((default_name, default_field, name, field)); } } @@ -968,36 +967,52 @@ pub(crate) fn check_static_class_definitions<'db>( } } - if !required_after_default_field_names.is_empty() { - // Report field ordering violations + if !field_order_violations.is_empty() { let body_scope = class.body_scope(db).file_scope_id(db); let use_def_map = index.use_def_map(body_scope); let place_table = index.place_table(body_scope); - for name in required_after_default_field_names { - let Some(symbol_id) = place_table.symbol_id(name.as_str()) else { - continue; - }; - for decl_with_constraints in use_def_map.end_of_scope_symbol_declarations(symbol_id) + for (default_name, default_field, name, field) in field_order_violations { + if !own_fields.contains_key(default_name) + && !own_fields.contains_key(name) + && has_inherited_dataclass_field_order_violation( + db, + class, + default_name, + default_field, + name, + field, + ) { - let Some(definition) = decl_with_constraints.declaration.definition() else { - continue; - }; - let DefinitionKind::AnnotatedAssignment(ann_assign) = definition.kind(db) - else { - continue; - }; - let Some(builder) = context - .report_lint(&DATACLASS_FIELD_ORDER, ann_assign.target(context.module())) - else { - continue; + continue; + } + + let report = |range: TextRange| { + let Some(builder) = context.report_lint(&DATACLASS_FIELD_ORDER, range) else { + return false; }; builder.into_diagnostic(format_args!( - "Required field `{name}` cannot be defined \ - after fields with default values", + "Required field `{name}` cannot be defined after fields with default values", )); + true + }; - break; + if !own_fields.contains_key(name) { + report(class_node.name.range()); + continue; + } + + let Some(symbol_id) = place_table.symbol_id(name.as_str()) else { + continue; + }; + for decl_with_constraints in use_def_map.end_of_scope_symbol_declarations(symbol_id) + { + if let Some(definition) = decl_with_constraints.declaration.definition() + && let DefinitionKind::AnnotatedAssignment(ann_assign) = definition.kind(db) + && report(ann_assign.target(context.module()).range()) + { + break; + } } } } @@ -1027,6 +1042,64 @@ pub(crate) fn check_static_class_definitions<'db>( class.validate_members(context); } +/// Returns whether the same default-before-required field pair already violates an ancestor's +/// generated constructor ordering. +/// +/// ```python +/// from dataclasses import dataclass +/// +/// @dataclass +/// class Base: +/// optional: int = 1 +/// required: int +/// +/// @dataclass +/// class Child(Base): +/// pass +/// ``` +/// +/// `Child` inherits the existing error and should not report it again. Comparing declaration +/// provenance preserves diagnostics when a subclass redeclares either field. +fn has_inherited_dataclass_field_order_violation<'db>( + db: &'db dyn Db, + class: StaticClassLiteral<'db>, + default_name: &Name, + default_field: &Field<'db>, + required_name: &Name, + required_field: &Field<'db>, +) -> bool { + class + .iter_mro(db, None) + .skip(1) + .filter_map(ClassBase::into_class) + .filter_map(|ancestor| ancestor.static_class_literal(db)) + .any(|(ancestor, specialization)| { + let Some(field_policy @ CodeGeneratorKind::DataclassLike(_)) = + CodeGeneratorKind::from_class(db, ancestor.into()) + else { + return false; + }; + if !ancestor.has_dataclass_param(db, field_policy, DataclassFlags::INIT) { + return false; + } + + let fields = ancestor.fields(db, specialization, field_policy); + let Some((default_index, _, inherited_default_field)) = fields.get_full(default_name) + else { + return false; + }; + let Some((required_index, _, inherited_required_field)) = + fields.get_full(required_name) + else { + return false; + }; + + default_index < required_index + && inherited_default_field.first_declaration == default_field.first_declaration + && inherited_required_field.first_declaration == required_field.first_declaration + }) +} + /// Check compatibility between class namespace values and attributes populated by its metaclass. /// /// A binding in a class body is passed through the namespace used to construct the class object From 7a7b2abf60f6d7dc130f9435e7a7d3189233209f Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Jul 2026 12:48:28 +0200 Subject: [PATCH 182/390] [ty] Use legacy ParamSpec syntax in legacy wrapper test (#27348) ## Summary Use legacy ParamSpec syntax in `legacy/paramspec.md`. Previously, this was just a verbatim copy of [this PEP 695 test](https://github.com/astral-sh/ruff/blob/4a437b093417a22abecc168cc186ef58ca48c0fc/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md?plain=1#L436-L463). --- .../resources/mdtest/generics/legacy/paramspec.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index 158d0c83f7..f6a9befe22 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -593,16 +593,13 @@ def _(concrete: Command[[str]], gradual: Command[...]) -> None: This avoids rejecting wrappers around callbacks that are safe to use with a positional-only callback protocol. -```toml -[environment] -python-version = "3.12" -``` - ```py from collections.abc import Callable -from typing import Final +from typing import Final, Generic, ParamSpec + +P = ParamSpec("P", contravariant=True) -class Job[**P]: +class Job(Generic[P]): target: Final[Callable[P, None]] def __init__(self, target: Callable[P, None]) -> None: From 2b85a51d84113000461ff0da4315499c92f3e8db Mon Sep 17 00:00:00 2001 From: JS <44579963+Punisheroot@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:45:25 +0200 Subject: [PATCH 183/390] [ty] Index with-statement targets as symbols (#27256) Co-authored-by: Micha Reiser --- crates/ty_ide/src/document_symbols.rs | 222 ++++++++++++++++++++- crates/ty_ide/src/symbols.rs | 276 +++++++++++++++++++++++--- 2 files changed, 464 insertions(+), 34 deletions(-) diff --git a/crates/ty_ide/src/document_symbols.rs b/crates/ty_ide/src/document_symbols.rs index 23b7950345..ed831d2c82 100644 --- a/crates/ty_ide/src/document_symbols.rs +++ b/crates/ty_ide/src/document_symbols.rs @@ -10,7 +10,7 @@ pub fn document_symbols(db: &dyn Db, file: File) -> &FlatSymbols { #[cfg(test)] mod tests { use super::*; - use crate::symbols::{HierarchicalSymbols, SymbolId, SymbolInfo}; + use crate::symbols::{HierarchicalSymbols, SymbolId, SymbolInfo, SymbolKind}; use crate::tests::{CursorTest, IntoDiagnostic, cursor_test}; use insta::assert_snapshot; use ruff_db::diagnostic::{ @@ -263,6 +263,226 @@ class Aliases: "); } + #[test] + fn document_symbols_with_statement_targets() { + let test = cursor_test( + " +from contextlib import nullcontext + +with nullcontext() as module_target, nullcontext((1, 2)) as (left, right): + body_target = 1 + +class C: + with nullcontext() as class_target: + body_field = 1 + +def function(): + with nullcontext() as local_target: + pass +", + ); + + assert_snapshot!(test.document_symbols(), @" + info[document-symbols]: SymbolInfo + --> main.py:4:23 + | + 4 | with nullcontext() as module_target, nullcontext((1, 2)) as (left, right): + | ^^^^^^^^^^^^^ + info: Variable module_target + + info[document-symbols]: SymbolInfo + --> main.py:4:62 + | + 4 | with nullcontext() as module_target, nullcontext((1, 2)) as (left, right): + | ^^^^ + info: Variable left + + info[document-symbols]: SymbolInfo + --> main.py:4:68 + | + 4 | with nullcontext() as module_target, nullcontext((1, 2)) as (left, right): + | ^^^^^ + info: Variable right + + info[document-symbols]: SymbolInfo + --> main.py:5:5 + | + 5 | body_target = 1 + | ^^^^^^^^^^^ + info: Variable body_target + + info[document-symbols]: SymbolInfo + --> main.py:7:7 + | + 7 | class C: + | ^ + info: Class C + + info[document-symbols]: SymbolInfo + --> main.py:8:27 + | + 8 | with nullcontext() as class_target: + | ^^^^^^^^^^^^ + info: Field class_target + + info[document-symbols]: SymbolInfo + --> main.py:9:9 + | + 9 | body_field = 1 + | ^^^^^^^^^^ + info: Field body_field + + info[document-symbols]: SymbolInfo + --> main.py:11:5 + | + 11 | def function(): + | ^^^^^^^^ + info: Function function + "); + } + + #[test] + fn document_symbols_augmented_assignment_targets() { + let test = cursor_test( + " +items = [1] +items[(index := 0)] += 1 +(obj := factory()).value += 1 +items += (rhs := [1]) +", + ); + + assert_snapshot!(test.document_symbols(), @" + info[document-symbols]: SymbolInfo + --> main.py:2:1 + | + 2 | items = [1] + | ^^^^^ + info: Variable items + + info[document-symbols]: SymbolInfo + --> main.py:3:8 + | + 3 | items[(index := 0)] += 1 + | ^^^^^ + info: Variable index + + info[document-symbols]: SymbolInfo + --> main.py:4:2 + | + 4 | (obj := factory()).value += 1 + | ^^^ + info: Variable obj + + info[document-symbols]: SymbolInfo + --> main.py:5:11 + | + 5 | items += (rhs := [1]) + | ^^^ + info: Variable rhs + "); + } + + #[test] + fn document_symbols_store_context_targets() { + let test = cursor_test( + " +first, *rest, LAST = values + +for loop_left, [loop_right, *loop_rest] in rows: + loop_body = 1 + +with manager() as [with_left, *with_rest], manager() as WITH_CONSTANT: + with_body = 1 + +captured = (walrus := 1) + +def function(): + function_local = 1 + with manager() as function_target: + pass +", + ); + + let symbols = document_symbols(&test.db, test.cursor.file) + .iter() + .map(|(_, symbol)| (symbol.name.into_owned(), symbol.kind)) + .collect::>(); + + assert_eq!( + symbols, + [ + ("first", SymbolKind::Variable), + ("rest", SymbolKind::Variable), + ("LAST", SymbolKind::Constant), + ("loop_left", SymbolKind::Variable), + ("loop_right", SymbolKind::Variable), + ("loop_rest", SymbolKind::Variable), + ("loop_body", SymbolKind::Variable), + ("with_left", SymbolKind::Variable), + ("with_rest", SymbolKind::Variable), + ("WITH_CONSTANT", SymbolKind::Constant), + ("with_body", SymbolKind::Variable), + ("captured", SymbolKind::Variable), + ("walrus", SymbolKind::Variable), + ("function", SymbolKind::Function), + ] + .map(|(name, kind)| (name.to_owned(), kind)) + ); + } + + #[test] + fn document_symbols_comprehension_and_lambda_scopes() { + let test = cursor_test( + " +result = [item for item in values if (leaked := item)] +generator = (other for other in values) +lambda_value = lambda: (lambda_local := 1) +", + ); + + let names = document_symbols(&test.db, test.cursor.file) + .iter() + .map(|(_, symbol)| symbol.name.into_owned()) + .collect::>(); + + assert_eq!(names, ["result", "leaked", "generator", "lambda_value"]); + } + + #[test] + fn document_symbols_function_and_class_header_bindings() { + let test = cursor_test( + " +@(function_decorator := decorate) +def function(value=(default_value := 1)): + function_local = 1 + +@(class_decorator := decorate) +class Example((class_base := Base)): + class_field = 1 +", + ); + + let symbols = document_symbols(&test.db, test.cursor.file) + .iter() + .map(|(_, symbol)| (symbol.name.into_owned(), symbol.kind)) + .collect::>(); + + assert_eq!( + symbols, + [ + ("function_decorator", SymbolKind::Variable), + ("function", SymbolKind::Function), + ("default_value", SymbolKind::Variable), + ("class_decorator", SymbolKind::Variable), + ("Example", SymbolKind::Class), + ("class_base", SymbolKind::Variable), + ("class_field", SymbolKind::Field), + ] + .map(|(name, kind)| (name.to_owned(), kind)) + ); + } + impl CursorTest { fn document_symbols(&self) -> String { let symbols = document_symbols(&self.db, self.cursor.file).to_hierarchical(); diff --git a/crates/ty_ide/src/symbols.rs b/crates/ty_ide/src/symbols.rs index 5bf37f31f7..0b6463e923 100644 --- a/crates/ty_ide/src/symbols.rs +++ b/crates/ty_ide/src/symbols.rs @@ -698,6 +698,10 @@ struct SymbolVisitor<'db> { /// This is true even when we're inside a function definition /// that is inside a class. in_class: bool, + /// The statement whose expressions are currently being visited. + current_stmt: Option<&'db ast::Stmt>, + /// Whether store-context names should be excluded from the enclosing scope. + suppress_store_symbols: bool, /// When enabled, the visitor should only try to extract /// symbols from a module that we believed form the "exported" /// interface for that module. i.e., `__all__` is only respected @@ -727,6 +731,8 @@ impl<'db> SymbolVisitor<'db> { symbol_stack: vec![], in_function: false, in_class: false, + current_stmt: None, + suppress_store_symbols: false, exports_only: false, all_origin: None, all_names: FxHashSet::default(), @@ -820,6 +826,12 @@ impl<'db> SymbolVisitor<'db> { } } + fn visit_nonbinding_target(&mut self, target: &'db ast::Expr) { + let previous = std::mem::replace(&mut self.suppress_store_symbols, true); + self.visit_expr(target); + self.suppress_store_symbols = previous; + } + /// Add a new symbol and return its ID. fn add_symbol(&mut self, mut symbol: SymbolTree) -> SymbolId { if let Some(&parent_id) = self.symbol_stack.last() { @@ -1159,11 +1171,6 @@ impl<'db> SymbolVisitor<'db> { self.all_origin = Some(origin); } - fn push_symbol(&mut self, symbol: SymbolTree) { - let symbol_id = self.add_symbol(symbol); - self.symbol_stack.push(symbol_id); - } - fn pop_symbol(&mut self) { self.symbol_stack.pop().unwrap(); } @@ -1239,10 +1246,8 @@ impl<'db> SymbolVisitor<'db> { // ... otherwise, it's exported! true } -} -impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { - fn visit_stmt(&mut self, stmt: &'db ast::Stmt) { + fn visit_stmt_impl(&mut self, stmt: &'db ast::Stmt) { match stmt { ast::Stmt::FunctionDef(func_def) => { let kind = SymbolKind::function_kind( @@ -1262,19 +1267,32 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { imported_from: None, }; + for decorator in &func_def.decorator_list { + self.visit_decorator(decorator); + } + + let symbol_id = self.add_symbol(symbol); + + if let Some(type_params) = &func_def.type_params { + self.visit_type_params(type_params); + } + self.visit_parameters(&func_def.parameters); + if let Some(returns) = &func_def.returns { + self.visit_annotation(returns); + } + if self.exports_only { - self.add_symbol(symbol); // If global_only, don't walk function bodies return; } - self.push_symbol(symbol); + self.symbol_stack.push(symbol_id); // Mark that we're entering a function scope let was_in_function = self.in_function; self.in_function = true; - source_order::walk_stmt(self, stmt); + self.visit_body(&func_def.body); // Restore the previous function scope state self.in_function = was_in_function; @@ -1292,8 +1310,20 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { imported_from: None, }; + for decorator in &class_def.decorator_list { + self.visit_decorator(decorator); + } + + let symbol_id = self.add_symbol(symbol); + + if let Some(type_params) = &class_def.type_params { + self.visit_type_params(type_params); + } + if let Some(arguments) = &class_def.arguments { + self.visit_arguments(arguments); + } + if self.exports_only { - self.add_symbol(symbol); // If global_only, don't walk class bodies return; } @@ -1302,8 +1332,8 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { let was_in_class = self.in_class; self.in_class = true; - self.push_symbol(symbol); - source_order::walk_stmt(self, stmt); + self.symbol_stack.push(symbol_id); + self.visit_body(&class_def.body); self.pop_symbol(); // Restore the previous class scope state @@ -1321,28 +1351,23 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { } ast::Stmt::Assign(assign) => { self.add_all_assignment(&assign.targets, Some(&assign.value)); - - for target in &assign.targets { - let ast::Expr::Name(name) = target else { - continue; - }; - self.add_assignment(stmt, name); - } + source_order::walk_stmt(self, stmt); } ast::Stmt::AnnAssign(ann_assign) => { self.add_all_assignment( std::slice::from_ref(&ann_assign.target), ann_assign.value.as_deref(), ); - - let ast::Expr::Name(name) = &*ann_assign.target else { - return; - }; - self.add_assignment(stmt, name); + source_order::walk_stmt(self, stmt); } ast::Stmt::AugAssign(ast::StmtAugAssign { target, op, value, .. }) => { + if !target.is_name_expr() { + self.visit_expr(target); + } + self.visit_expr(value); + // We don't care about `__all__` unless we're // specifically looking for exported symbols. if !self.exports_only { @@ -1367,6 +1392,8 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { } } ast::Stmt::Expr(expr) => { + source_order::walk_stmt(self, stmt); + // We don't care about `__all__` unless we're // specifically looking for exported symbols. if !self.exports_only { @@ -1398,8 +1425,6 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { if !self.update_all_by_call_idiom(attr, arguments) { self.all_invalid = true; } - - source_order::walk_stmt(self, stmt); } ast::Stmt::Import(import) => { // We ignore any names introduced by imports @@ -1449,15 +1474,72 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { // statements. We just assume that all `if` statements are // always `True`. This applies to symbols in general but // also `__all__`. - _ => { - source_order::walk_stmt(self, stmt); + _ => source_order::walk_stmt(self, stmt), + } + } +} + +impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { + fn visit_stmt(&mut self, stmt: &'db ast::Stmt) { + let previous_stmt = self.current_stmt.replace(stmt); + self.visit_stmt_impl(stmt); + self.current_stmt = previous_stmt; + } + + fn visit_expr(&mut self, expr: &'db ast::Expr) { + if self.in_function { + return; + } + + match expr { + ast::Expr::Name(name) + if name.ctx.is_store() + && !self.suppress_store_symbols + && let Some(stmt) = self.current_stmt => + { + self.add_assignment(stmt, name); + + if name.id != "__all__" { + return; + } + + // We don't care about `__all__` unless we're + // specifically looking for exported symbols. + if !self.exports_only { + return; + } + + // We can't update `__all__` if it doesn't already exist. + if self.all_origin.is_none() { + return; + } + + if !is_recognized_all_assignment(stmt, name) { + self.all_invalid = true; + } } + ast::Expr::Lambda(lambda) => { + if let Some(parameters) = &lambda.parameters { + self.visit_parameters(parameters); + } + + let was_in_function = self.in_function; + self.in_function = true; + self.visit_expr(&lambda.body); + self.in_function = was_in_function; + } + _ => source_order::walk_expr(self, expr), } } - // TODO: We might consider handling walrus expressions - // here, since they can be used to introduce new names. - fn visit_expr(&mut self, _expr: &ast::Expr) {} + fn visit_comprehension(&mut self, comprehension: &'db ast::Comprehension) { + self.visit_nonbinding_target(&comprehension.target); + self.visit_expr(&comprehension.iter); + + for condition in &comprehension.ifs { + self.visit_expr(condition); + } + } } /// Represents where an `__all__` has been defined. @@ -1476,6 +1558,19 @@ fn is_dunder_all(expr: &ast::Expr) -> bool { matches!(expr, ast::Expr::Name(ast::ExprName { id, .. }) if id == "__all__") } +fn is_recognized_all_assignment(stmt: &ast::Stmt, name: &ast::ExprName) -> bool { + match stmt { + ast::Stmt::Assign(assign) => assign + .targets + .first() + .is_some_and(|target| is_dunder_all(target) && target.range() == name.range()), + ast::Stmt::AnnAssign(assign) => { + is_dunder_all(&assign.target) && assign.target.range() == name.range() + } + _ => false, + } +} + /// Create and return a string representing a name from the given /// expression, or `None` if it is an invalid expression for a /// `__all__` element. @@ -1552,6 +1647,121 @@ def quux(): ); } + #[test] + fn exports_with_statement_targets() { + insta::assert_snapshot!( + public_test("\ +from contextlib import nullcontext + +with nullcontext() as module_target, nullcontext((1, 2)) as (left, right): + body_target = 1 + +class C: + with nullcontext() as class_target: + body_field = 1 + +def function(): + with nullcontext() as local_target: + pass +").exports(), + @" + module_target :: Variable + left :: Variable + right :: Variable + body_target :: Variable + C :: Class + function :: Function + ", + ); + } + + #[test] + fn exports_store_context_targets() { + let test = public_test( + "\ +first, *rest, LAST = values +for loop_left, [loop_right, *loop_rest] in rows: + pass +with manager() as [with_left, *with_rest]: + pass +captured = (walrus := 1) +", + ); + + assert_eq!( + test.exports(), + "first :: Variable\n\ +rest :: Variable\n\ +LAST :: Constant\n\ +loop_left :: Variable\n\ +loop_right :: Variable\n\ +loop_rest :: Variable\n\ +with_left :: Variable\n\ +with_rest :: Variable\n\ +captured :: Variable\n\ +walrus :: Variable" + ); + } + + #[test] + fn exports_exclude_comprehension_targets() { + let test = public_test( + "\ +result = [item for item in values if (leaked := item)] +generator = (other for other in values) +lambda_value = lambda: (lambda_local := 1) +", + ); + + assert_eq!( + test.exports(), + "result :: Variable\n\ +leaked :: Variable\n\ +generator :: Variable\n\ +lambda_value :: Variable" + ); + } + + #[test] + fn exports_invalidate_all_rebound_by_with_target() { + let test = public_test( + "\ +hidden = 1 +visible = 2 +__all__ = ['visible'] +with manager() as __all__: + pass +", + ); + + assert_eq!( + test.exports(), + "hidden :: Variable\n\ +visible :: Variable\n\ +__all__ :: Variable" + ); + } + + #[test] + fn exports_invalidate_all_rebound_by_named_expression() { + let test = public_test( + "\ +hidden = 1 +visible = 2 +__all__ = ['visible'] +result = (__all__ := unknown) +", + ); + + assert_eq!( + test.exports(), + "hidden :: Variable\n\ +visible :: Variable\n\ +__all__ :: Variable\n\ +result :: Variable" + ); + } + /// The typing spec says that names beginning with an underscore /// ought to be considered unexported[1]. However, at present, we /// currently include them in completions but rank them lower than From 7111e134a4fee39b990c6fded8799b93d0497953 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:10:09 -0400 Subject: [PATCH 184/390] Register formatting capabilities dynamically to exclude TOML files (#27332) Summary -- Addresses https://github.com/astral-sh/ruff-vscode/pull/1127#discussion_r3684426847, which would otherwise expose Ruff as a TOML formatter in VS Code. Instead of registering formatting and range formatting capabilities statically, we now register them dynamically only for Python, Markdown, and notebook files, excluding TOML files. We still fall back on static registration if dynamic registration is unavailable. Test Plan -- New e2e tests --- crates/ruff_server/src/server.rs | 43 +++++++++- crates/ruff_server/src/server/main_loop.rs | 86 ++++++++++++++++++-- crates/ruff_server/tests/e2e/capabilities.rs | 77 ++++++++++++++++++ crates/ruff_server/tests/e2e/main.rs | 23 +++++- 4 files changed, 214 insertions(+), 15 deletions(-) create mode 100644 crates/ruff_server/tests/e2e/capabilities.rs diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index 58cd033ae5..c02b008a71 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -59,7 +59,8 @@ 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 server_capabilities = + Self::server_capabilities(position_encoding, &client_capabilities); let connection = connection.initialize_finish( id, @@ -150,7 +151,41 @@ impl Server { .unwrap_or_default() } - fn server_capabilities(position_encoding: PositionEncoding) -> types::ServerCapabilities { + fn supports_dynamic_formatting(client_capabilities: &ClientCapabilities) -> bool { + client_capabilities + .text_document + .as_ref() + .and_then(|text_document| text_document.formatting) + .and_then(|formatting| formatting.dynamic_registration) + .unwrap_or_default() + } + + fn supports_dynamic_range_formatting(client_capabilities: &ClientCapabilities) -> bool { + client_capabilities + .text_document + .as_ref() + .and_then(|text_document| text_document.range_formatting) + .and_then(|range_formatting| range_formatting.dynamic_registration) + .unwrap_or_default() + } + + fn server_capabilities( + position_encoding: PositionEncoding, + client_capabilities: &ClientCapabilities, + ) -> types::ServerCapabilities { + let document_formatting_provider = if Self::supports_dynamic_formatting(client_capabilities) + { + None + } else { + Some(true.into()) + }; + let document_range_formatting_provider = + if Self::supports_dynamic_range_formatting(client_capabilities) { + None + } else { + Some(true.into()) + }; + types::ServerCapabilities { position_encoding: Some(position_encoding.into()), code_action_provider: Some( @@ -176,8 +211,8 @@ impl Server { file_operations: None, text_document_content: None, }), - document_formatting_provider: Some(true.into()), - document_range_formatting_provider: Some(true.into()), + document_formatting_provider, + document_range_formatting_provider, diagnostic_provider: Some( DiagnosticOptions { identifier: Some(crate::DIAGNOSTIC_NAME.into()), diff --git a/crates/ruff_server/src/server/main_loop.rs b/crates/ruff_server/src/server/main_loop.rs index 370e84c7f1..34357d22f5 100644 --- a/crates/ruff_server/src/server/main_loop.rs +++ b/crates/ruff_server/src/server/main_loop.rs @@ -3,6 +3,7 @@ use crossbeam::select; use lsp_server::Message; use lsp_types::{ self as types, DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, Notification as _, + Request as _, }; use crate::{ @@ -135,20 +136,27 @@ impl Server { } fn initialize(&mut self, client: &Client) { - let dynamic_registration = self + let supports_watched_files = self .client_capabilities .workspace .as_ref() .and_then(|workspace| workspace.did_change_watched_files) .and_then(|watched_files| watched_files.dynamic_registration) .unwrap_or_default(); + let supports_formatting = Self::supports_dynamic_formatting(&self.client_capabilities); + let supports_range_formatting = + Self::supports_dynamic_range_formatting(&self.client_capabilities); + let dynamic_registration = + supports_watched_files || supports_formatting || supports_range_formatting; + if dynamic_registration { // Register all dynamic capabilities here + let mut registrations = vec![]; - // `workspace/didChangeWatchedFiles` - // (this registers the configuration file watcher) - let params = lsp_types::RegistrationParams { - registrations: vec![lsp_types::Registration { + if supports_watched_files { + // `workspace/didChangeWatchedFiles` + // (this registers the configuration file watcher) + registrations.push(lsp_types::Registration { id: "ruff-server-watch".into(), method: "workspace/didChangeWatchedFiles".into(), register_options: Some( @@ -176,11 +184,71 @@ impl Server { }) .unwrap(), ), - }], - }; + }); + } + + if supports_formatting || supports_range_formatting { + let document_selector = vec![ + types::TextDocumentFilter::Language(types::TextDocumentFilterLanguage { + language: "python".to_string(), + scheme: None, + pattern: None, + }) + .into(), + types::TextDocumentFilter::Language(types::TextDocumentFilterLanguage { + language: "markdown".to_string(), + scheme: None, + pattern: None, + }) + .into(), + types::NotebookCellTextDocumentFilter { + notebook: "*".into(), + language: Some("python".into()), + } + .into(), + ]; + + let text_document_registration_options = types::TextDocumentRegistrationOptions { + document_selector: Some(document_selector), + }; + + if supports_formatting { + registrations.push(types::Registration { + id: "ruff-server-format".into(), + method: types::DocumentFormattingRequest::METHOD.to_string(), + register_options: Some( + serde_json::to_value(types::DocumentFormattingRegistrationOptions { + text_document_registration_options: + text_document_registration_options.clone(), + document_formatting_options: + types::DocumentFormattingOptions::default(), + }) + .unwrap(), + ), + }); + } + + if supports_range_formatting { + registrations.push(types::Registration { + id: "ruff-server-format-range".into(), + method: types::DocumentRangeFormattingRequest::METHOD.to_string(), + register_options: Some( + serde_json::to_value( + types::DocumentRangeFormattingRegistrationOptions { + text_document_registration_options, + document_range_formatting_options: + types::DocumentRangeFormattingOptions::default(), + }, + ) + .unwrap(), + ), + }); + } + } + let params = types::RegistrationParams { registrations }; let response_handler = |_: &Client, ()| { - tracing::info!("Configuration file watcher successfully registered"); + tracing::info!("Dynamic capabilities successfully registered"); }; if let Err(err) = client.send_request::( @@ -189,7 +257,7 @@ impl Server { response_handler, ) { tracing::error!( - "An error occurred when trying to register the configuration file watcher: {err}" + "An error occurred when trying to register dynamic capabilities: {err}" ); } } else { diff --git a/crates/ruff_server/tests/e2e/capabilities.rs b/crates/ruff_server/tests/e2e/capabilities.rs new file mode 100644 index 0000000000..fe47aac6ec --- /dev/null +++ b/crates/ruff_server/tests/e2e/capabilities.rs @@ -0,0 +1,77 @@ +use anyhow::Result; +use insta::assert_json_snapshot; +use lsp_types::Request as _; +use lsp_types::{DocumentFormattingRequest, DocumentRangeFormattingRequest, RegistrationRequest}; + +use crate::TestServerBuilder; + +#[test] +fn statically_registers_formatting_when_dynamic_registration_is_unsupported() -> Result<()> { + let server = TestServerBuilder::new()?.build(); + let capabilities = &server + .initialization_result() + .expect("Server should return initialization capabilities") + .capabilities; + + assert_eq!(capabilities.document_formatting_provider, Some(true.into())); + assert_eq!( + capabilities.document_range_formatting_provider, + Some(true.into()) + ); + + Ok(()) +} + +#[test] +fn dynamically_registers_formatting_and_range_formatting_for_python_and_markdown() -> Result<()> { + let mut server = TestServerBuilder::new()? + .enable_formatting_dynamic_registration(true) + .enable_range_formatting_dynamic_registration(true) + .build(); + let capabilities = &server + .initialization_result() + .expect("Server should return initialization capabilities") + .capabilities; + + assert_eq!(capabilities.document_formatting_provider, None); + assert_eq!(capabilities.document_range_formatting_provider, None); + + let (_, params) = server.await_request::(); + let [formatting, range_formatting] = params.registrations.as_slice() else { + panic!("Expected both dynamic formatting registrations"); + }; + + assert_eq!( + formatting.method, + DocumentFormattingRequest::METHOD.as_str() + ); + assert_eq!( + range_formatting.method, + DocumentRangeFormattingRequest::METHOD.as_str() + ); + assert_json_snapshot!( + formatting.register_options, + @r#" + { + "documentSelector": [ + { + "language": "python" + }, + { + "language": "markdown" + }, + { + "language": "python", + "notebook": "*" + } + ] + } + "# + ); + assert_eq!( + range_formatting.register_options, + formatting.register_options + ); + + Ok(()) +} diff --git a/crates/ruff_server/tests/e2e/main.rs b/crates/ruff_server/tests/e2e/main.rs index 6349ed3c0c..d8e53ee40d 100644 --- a/crates/ruff_server/tests/e2e/main.rs +++ b/crates/ruff_server/tests/e2e/main.rs @@ -25,6 +25,7 @@ //! [`await_request`]: TestServer::await_request //! [`await_notification`]: TestServer::await_notification +mod capabilities; mod code_action; mod custom_extension; mod diagnostics; @@ -530,7 +531,6 @@ impl TestServer { /// /// If receiving the request fails. #[track_caller] - #[expect(dead_code)] pub(crate) fn await_request(&mut self) -> (RequestId, R::Params) { match self.try_await_request::(None) { Ok(result) => result, @@ -653,7 +653,6 @@ impl TestServer { } /// Get the initialization result - #[expect(dead_code)] pub(crate) fn initialization_result(&self) -> Option<&InitializeResult> { self.initialize_response.as_ref() } @@ -1062,6 +1061,26 @@ impl TestServerBuilder { self } + pub(crate) fn enable_formatting_dynamic_registration(mut self, enabled: bool) -> Self { + self.client_capabilities + .text_document + .get_or_insert_default() + .formatting + .get_or_insert_default() + .dynamic_registration = Some(enabled); + self + } + + pub(crate) fn enable_range_formatting_dynamic_registration(mut self, enabled: bool) -> Self { + self.client_capabilities + .text_document + .get_or_insert_default() + .range_formatting + .get_or_insert_default() + .dynamic_registration = Some(enabled); + self + } + /// Enable or disable workspace configuration capability #[expect(dead_code)] pub(crate) fn enable_workspace_configuration(mut self, enabled: bool) -> Self { From 31cb63ae5ec2dd8779e7931fee6e9400f02b53f4 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:19:43 -0400 Subject: [PATCH 185/390] [`flake8-pyi`] Avoid false positives on `singledispatch` functions (`PYI041`) (#27335) Summary -- Fixes #27333. The fixed annotation isn't actually equivalent in this case. ```pycon >>> import functools ... ... @functools.singledispatch ... def int_or_float(value: object) -> str: ... return False ... ... @int_or_float.register ... def _(value: int | float): ... return True ... ... assert int_or_float(3) ... >>> import functools ... ... @functools.singledispatch ... def int_or_float(value: object) -> str: ... return False ... ... @int_or_float.register ... def _(value: float): ... return True ... ... assert int_or_float(3) ... Traceback (most recent call last): File "", line 11, in assert int_or_float(3) ~~~~~~~~~~~~^^^ AssertionError ``` We had an existing helper for testing for `singledispatch` functions, which we use to skip the first parameter of such functions since this is the dispatching parameter. Test Plan -- New mdtests --- .../flake8-pyi/redundant-numeric-union.md | 117 ++++++++++++++++++ .../src/checkers/ast/analyze/statement.rs | 2 +- .../rules/redundant_numeric_union.rs | 15 ++- 3 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 crates/ruff_linter/resources/mdtest/flake8-pyi/redundant-numeric-union.md diff --git a/crates/ruff_linter/resources/mdtest/flake8-pyi/redundant-numeric-union.md b/crates/ruff_linter/resources/mdtest/flake8-pyi/redundant-numeric-union.md new file mode 100644 index 0000000000..e52d8921de --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/flake8-pyi/redundant-numeric-union.md @@ -0,0 +1,117 @@ +# `redundant-numeric-union` (`PYI041`) + +```toml +target-version = "py311" + +[lint] +select = ["PYI041"] +``` + +## Ordinary parameter annotations + +Numeric unions are redundant when they are used only for static typing. + +```py +def function(value: int | float) -> None: ... # error: [redundant-numeric-union] +``` + +## Single-dispatch registrations + +The first annotated parameter determines the concrete types registered at runtime, so its numeric +union is not redundant. + +```py +import functools + +@functools.singledispatch +def dispatch(value: object) -> None: ... + +@dispatch.register +def _(value: int | float) -> None: ... +``` + +## Generic single-dispatch functions + +The generic function's annotation does not register concrete types, so its numeric union remains +redundant even when a registered implementation needs the same union. + +```py +import functools + +@functools.singledispatch +def dispatch(value: int | float) -> None: ... # error: [redundant-numeric-union] + +@dispatch.register +def _(value: int | float) -> None: ... +``` + +## Other parameters of registered functions + +Numeric unions remain redundant for parameters that do not determine dispatch registration. + +```py +import functools + +@functools.singledispatch +def dispatch(value: object) -> None: ... + +@dispatch.register +def _(value: float | complex, other: int | float) -> None: ... # snapshot: redundant-numeric-union +``` + +```snapshot +error[PYI041]: Use `float` instead of `int | float` + --> src/mdtest_snippet.py:7:38 + | +7 | def _(value: float | complex, other: int | float) -> None: ... # snapshot: redundant-numeric-union + | ^^^^^^^^^^^ +help: Remove redundant type + | +6 | @dispatch.register + - def _(value: float | complex, other: int | float) -> None: ... # snapshot: redundant-numeric-union +7 + def _(value: float | complex, other: float) -> None: ... # snapshot: redundant-numeric-union + | +``` + +## Single-dispatch method registrations + +The dispatch parameter comes after the unannotated instance parameter. + +```py +import functools + +class Dispatch: + @functools.singledispatchmethod + def dispatch(self, value: object) -> None: ... + + @dispatch.register + def _(self, value: int | float) -> None: ... +``` + +## Explicit single-dispatch registrations + +An explicit registration does not inspect the implementation's parameter annotation. + +```py +import functools + +@functools.singledispatch +def dispatch(value: object) -> None: ... + +@dispatch.register(int | float) +def _(value: int | float) -> None: ... # error: [redundant-numeric-union] +``` + +## Stub annotations + +Registered dispatch implementations retain their numeric unions in stub files as well. + +```pyi +import functools + +@functools.singledispatch +def dispatch(value: object) -> None: ... + +@dispatch.register +def _(value: int | float) -> None: ... +``` diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index fceab4be1e..e09bc64071 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -152,7 +152,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { flake8_pyi::rules::bad_exit_annotation(checker, function_def); } if checker.is_rule_enabled(Rule::RedundantNumericUnion) { - flake8_pyi::rules::redundant_numeric_union(checker, parameters); + flake8_pyi::rules::redundant_numeric_union(checker, function_def); } if checker.is_rule_enabled(Rule::Pep484StylePositionalOnlyParameter) { flake8_pyi::rules::pep_484_positional_parameter(checker, function_def); 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 d3ee1a96b8..691d69f337 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,12 +1,13 @@ use bitflags::bitflags; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::{AnyParameterRef, Expr, ExprBinOp, Operator, Parameters, PythonVersion}; +use ruff_python_ast::{AnyParameterRef, Expr, ExprBinOp, Operator, PythonVersion, StmtFunctionDef}; use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::preview::is_resolve_string_annotation_pyi041_enabled; +use crate::rules::flake8_type_checking::helpers::is_singledispatch_implementation; use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; use super::generate_union_fix; @@ -80,8 +81,16 @@ impl Violation for RedundantNumericUnion { } /// PYI041 -pub(crate) fn redundant_numeric_union(checker: &Checker, parameters: &Parameters) { - for annotation in parameters.iter().filter_map(AnyParameterRef::annotation) { +pub(crate) fn redundant_numeric_union(checker: &Checker, function_def: &StmtFunctionDef) { + let skip_dispatch_annotation = + is_singledispatch_implementation(function_def, checker.semantic()); + + for annotation in function_def + .parameters + .iter() + .filter_map(AnyParameterRef::annotation) + .skip(usize::from(skip_dispatch_annotation)) + { check_annotation(checker, annotation); } } From 8430a098fb8b46b5a5c3b3c919804cc40954460c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 15:44:49 -0400 Subject: [PATCH 186/390] [ty] Reject specializing non-generic subclasses (#27377) ## Summary Reject specialization of a subclass after all generic parameters in its bases have been specialized, including by defaults. ```python T = TypeVar("T") DefaultT = TypeVar("DefaultT", default=str) class Base(Generic[T, DefaultT]): ... class Child(Base[int]): ... Child[bytes] # error: Child is not generic ``` The existing fallback treated any class with `Generic` anywhere in its MRO as generic. That fallback predates our support for legacy generic contexts and is now overly broad: `Generic` remains in the runtime MRO even when the class has no free type variables. Actual generic class literals are already accepted through their generic context, so we can remove the MRO fallback and report `not-subscriptable` for other classes. This brings ty into conformance with the `generics_defaults_specialization.py` case from [python/typing#2325](https://github.com/python/typing/pull/2325) and adds focused mdtest coverage. --- .../mdtest/generics/legacy/classes.md | 22 ++++++++++ .../mdtest/generics/pep695/classes.md | 2 +- .../ty_python_semantic/src/types/subscript.rs | 42 ++++++++++++------- 3 files changed, 49 insertions(+), 17 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 a079176aae..1505df5b69 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -462,6 +462,28 @@ Stop2T = TypeVar("Stop2T", default=int) class Bad(Generic[Start2T, Stop2T, StepT]): ... ``` +## A subclass of a fully specialized generic is not generic + +A subclass is generic only if its bases leave at least one type variable unspecialized. Omitting a +type variable that has a default fully specializes the base, so the subclass cannot be specialized +again. + +```py +from typing_extensions import Generic, TypeVar + +T = TypeVar("T") +DefaultT = TypeVar("DefaultT", default=str) + +class Base(Generic[T, DefaultT]): ... +class GenericSubclass(Base[int, DefaultT]): ... +class NonGenericSubclass(Base[int]): ... + +reveal_type(GenericSubclass[bytes]()) # revealed: GenericSubclass[bytes] + +# error: [not-subscriptable] "Cannot specialize non-generic class `NonGenericSubclass`" +NonGenericSubclass[bytes] +``` + ## Diagnostics for bad specializations We show the user where the type variable was defined if a specialization is given that doesn't 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 37288530fd..443dff274b 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -945,7 +945,7 @@ subclass generic. ```py class Child(NonGeneric): ... -class Generic[T]: ... +class Generic[T, U = str]: ... class SpecializedChild(Generic[int]): ... # error: [not-subscriptable] "Cannot subscript non-generic type ``" diff --git a/crates/ty_python_semantic/src/types/subscript.rs b/crates/ty_python_semantic/src/types/subscript.rs index 5d030131e8..983a4d1609 100644 --- a/crates/ty_python_semantic/src/types/subscript.rs +++ b/crates/ty_python_semantic/src/types/subscript.rs @@ -3,7 +3,6 @@ use std::fmt::{self, Display}; use compact_str::{CompactString, ToCompactString}; -use itertools::Itertools; use ruff_python_ast as ast; use crate::Db; @@ -23,8 +22,8 @@ use super::infer::TypeContext; use super::instance::SliceLiteral; use super::special_form::SpecialFormType; use super::{ - IntersectionBuilder, IntersectionType, KnownInstanceType, Type, TypeAliasType, TypedDictType, - UnionBuilder, UnionType, todo_type, + ClassLiteral, IntersectionBuilder, IntersectionType, KnownInstanceType, Type, TypeAliasType, + TypedDictType, UnionBuilder, UnionType, todo_type, }; /// The kind of subscriptable type that had an out-of-bounds index. @@ -108,6 +107,8 @@ pub(crate) enum SubscriptErrorKind<'db> { SliceStepSizeZero, /// A non-generic PEP 695 type alias was subscripted. NonGenericTypeAlias { alias: TypeAliasType<'db> }, + /// A non-generic subclass of a generic class was subscripted. + NonGenericClass { class: ClassLiteral<'db> }, /// `__getitem__` or `__class_getitem__` exists but is possibly unbound. DunderPossiblyUnbound { method: DunderMethod, @@ -246,6 +247,14 @@ impl<'db> SubscriptErrorKind<'db> { } } } + Self::NonGenericClass { class } => { + if let Some(builder) = context.report_lint(&NOT_SUBSCRIPTABLE, subscript) { + builder.into_diagnostic(format_args!( + "Cannot specialize non-generic class `{}`", + class.name(db) + )); + } + } Self::DunderPossiblyUnbound { method, value_ty } => { if let Some(builder) = context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, value_node) @@ -951,21 +960,22 @@ impl<'db> Type<'db> { // expression. return Ok(value_ty); } - } - // TODO: properly handle old-style generics; get rid of this temporary hack - if !value_ty - .as_class_literal() - .is_some_and(|class| class.iter_mro(db).contains(&ClassBase::Generic)) - { - return Err(SubscriptError::new( - Type::unknown(), - SubscriptErrorKind::NotSubscriptable { - value_ty, - method: DunderMethod::ClassGetItem, - }, - )); + if class.iter_mro(db).any(|base| base == ClassBase::Generic) { + return Err(SubscriptError::new( + Type::unknown(), + SubscriptErrorKind::NonGenericClass { class }, + )); + } } + + return Err(SubscriptError::new( + Type::unknown(), + SubscriptErrorKind::NotSubscriptable { + value_ty, + method: DunderMethod::ClassGetItem, + }, + )); } else if expr_context != ast::ExprContext::Store { return Err(SubscriptError::new( Type::unknown(), From 9e5424f0c45fb6db2463caad5cc4d85b64513b76 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 16:56:59 -0400 Subject: [PATCH 187/390] [ty] Preserve TypeVarTuple context during Generic recovery (#27381) ## Summary Prior to this change, we recovered an invalid `Generic[Shape]` subscription as `Unknown`. That lost the class's generic context and caused a cascading diagnostic for a correctly unpacked use: ```py Shape = TypeVarTuple("Shape") class ClassA(Generic[Shape]): # invalid-generic-class values: tuple[*Shape] # Previously: unbound-type-variable ``` We now preserve the bound `TypeVarTuple` in the recovery type while still reporting the invalid bare argument. This removes only the spurious `unbound-type-variable` diagnostic and makes `generics_typevartuple_basic.py` fully conformant. --- .../mdtest/generics/legacy/classes.md | 10 +++++++ .../src/types/infer/builder/subscript.rs | 26 +++++++++++++------ 2 files changed, 28 insertions(+), 8 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 1505df5b69..60cf17783c 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -126,6 +126,16 @@ error[shadowed-type-variable]: Generic class `InnerClass` uses ParamSpec `P` alr | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `P` used in class definition here ``` +A `TypeVarTuple` must be unpacked when used as an argument to `Generic`. Even though the base is +invalid, ty still treats the `TypeVarTuple` as a type parameter of the class during error recovery, +so correctly unpacked uses within the class do not produce cascading errors. + +```py +# error: [invalid-generic-class] "`TypeVarTuple` must be unpacked" +class BareTypeVarTuple(Generic[Ts]): + values: tuple[*Ts] +``` + 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. diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index 7baadc86f7..75308838c2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -2328,7 +2328,10 @@ enum LegacyGenericContextError<'db> { /// A duplicate typevar was provided. DuplicateTypevar(&'db str), /// A `TypeVarTuple` was provided but not unpacked. - TypeVarTupleMustBeUnpacked, + /// + /// The generic context is available when the argument is a bound `TypeVarTuple` and is used + /// to avoid cascading errors during recovery. + TypeVarTupleMustBeUnpacked(Option>), } impl<'db> LegacyGenericContextError<'db> { @@ -2337,7 +2340,7 @@ impl<'db> LegacyGenericContextError<'db> { LegacyGenericContextError::InvalidArgument(_) | LegacyGenericContextError::VariadicTupleArguments | LegacyGenericContextError::DuplicateTypevar(_) - | LegacyGenericContextError::TypeVarTupleMustBeUnpacked => Type::unknown(), + | LegacyGenericContextError::TypeVarTupleMustBeUnpacked(_) => Type::unknown(), LegacyGenericContextError::NotYetSupported => { todo_type!("ParamSpecs and TypeVarTuples") } @@ -2373,10 +2376,14 @@ fn infer_legacy_generic_subscript<'db>( typevar_name, }, )), - Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked) => Err(SubscriptError::new( - Type::unknown(), - SubscriptErrorKind::TypeVarTupleNotUnpacked { origin }, - )), + Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked(generic_context)) => { + Err(SubscriptError::new( + generic_context.map_or(Type::unknown(), |generic_context| { + Type::KnownInstance(wrap_ok(generic_context)) + }), + SubscriptErrorKind::TypeVarTupleNotUnpacked { origin }, + )) + } Err( error @ (LegacyGenericContextError::NotYetSupported | LegacyGenericContextError::VariadicTupleArguments), @@ -2423,7 +2430,10 @@ fn legacy_generic_class_context<'db>( let bound = bind_typevar(db, index, file_scope_id, typevar_binding_context, typevar) .ok_or(LegacyGenericContextError::InvalidArgument(argument_ty))?; if bound.is_typevartuple(db) { - return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked); + validated_typevars.insert(bound); + return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked(Some( + GenericContext::from_typevar_instances(db, validated_typevars), + ))); } if !validated_typevars.insert(bound) { return Err(LegacyGenericContextError::DuplicateTypevar( @@ -2442,7 +2452,7 @@ fn legacy_generic_class_context<'db>( Some(KnownClass::TypeVarTuple | KnownClass::ExtensionsTypeVarTuple) ) { - return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked); + return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked(None)); } else if any_over_type(db, argument_ty, true, |inner_ty| match inner_ty { Type::NominalInstance(nominal) => matches!( nominal.known_class(db), From 2272478c067945096c34350b20d0bfc41d356d3c Mon Sep 17 00:00:00 2001 From: BitWeaver Date: Sat, 1 Aug 2026 00:41:57 +0200 Subject: [PATCH 188/390] [ty] Reject `ClassVar` and `Final` qualifiers in `NamedTuple` fields (#27380) ## Summary `NamedTuple` builds each field's type by passing its annotation through `typing._type_check`, which rejects bare type qualifiers. A field annotated with `ClassVar` or `Final` therefore raises `TypeError` as soon as the class statement executes: ```pycon >>> from typing import ClassVar, NamedTuple >>> class Foo(NamedTuple): ... x: ClassVar[int] TypeError: typing.ClassVar[int] is not valid as type argument ``` ty accepted both silently. This extends the existing `invalid-named-tuple` rule to report them. The obvious place for the check iterates `own_fields`, but that cannot see either case: it drops `ClassVar` declarations outright, and does not retain the `Final` qualifier for the fields it does keep. Rather than duplicate the class-body walk, this extracts it into `own_annotated_declarations` and adds a second consumer, `own_annotated_qualifiers`, which reports what the class body literally says. Two behaviours worth calling out: - A field carrying both qualifiers is reported once per qualifier, since removing just one of them still leaves a class definition that raises `TypeError`. - A class that merely inherits from a `NamedTuple` is an ordinary class at runtime, so it is left alone. Separately, the two `invalid-named-tuple` reporters for malformed fields shared their anchoring and primary-annotation logic; that is now factored into a common `report_invalid_named_tuple_field` helper. Closes astral-sh/ty#4131 ## Test Plan New mdtests in `named_tuple.md` cover both qualifiers, the unsubscripted form (`x: ClassVar`), the inheritance non-case, and a snapshot of the rendered diagnostic. - `cargo test -p ty_python_semantic`: 297 + 14 unit tests and 479 mdtests pass - `cargo clippy --workspace --all-targets --all-features -- -D warnings`: clean - `uv run --only-group dev --locked prek run --files ...`: passes - `cargo dev generate-all` re-run for the rule documentation change --------- Co-authored-by: Charlie Marsh --- crates/ty/docs/rules.md | 238 +++++++++--------- .../lint_docs/invalid-named-tuple.md | 10 + .../resources/mdtest/named_tuple.md | 144 +++++++++++ .../src/types/class/static_literal.rs | 67 ++++- .../src/types/diagnostic.rs | 27 ++ .../builder/post_inference/static_class.rs | 29 ++- ty.schema.json | 2 +- 7 files changed, 398 insertions(+), 119 deletions(-) diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 5a3f80c9e6..e0b5252ed4 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.64 · Related issues · -View source +View source @@ -44,7 +44,7 @@ class Base(ABC): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class Derived(Base): # error Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -154,7 +154,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -237,7 +237,7 @@ value = unknown # ty: ignore[unresolved-reference] Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -292,7 +292,7 @@ Foo.method() # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -320,7 +320,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -355,7 +355,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -389,7 +389,7 @@ a = 1 # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -424,7 +424,7 @@ class C(A, B): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -460,7 +460,7 @@ class B(A): ... # error Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -496,7 +496,7 @@ type B = A # error Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -533,7 +533,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -572,7 +572,7 @@ old_func() # error: [deprecated] Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -605,7 +605,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -636,7 +636,7 @@ class B(A, A): ... # error Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -679,7 +679,7 @@ class A: # error Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -756,7 +756,7 @@ def foo() -> "intt\b": ... # error Default level: warn · Added in 0.0.50 · Related issues · -View source +View source @@ -796,7 +796,7 @@ def g(value: ~A) -> None: ... # error: [experimental-syntax] Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -831,7 +831,7 @@ def my_function() -> int: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -947,7 +947,7 @@ def test() -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -983,7 +983,7 @@ class C(A, B): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1013,7 +1013,7 @@ t[3] # error Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -1050,7 +1050,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -1151,7 +1151,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1183,7 +1183,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1214,7 +1214,7 @@ a: int = "" # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1272,7 +1272,7 @@ C.instance_only_var = 56 # error Default level: error · Added in 0.0.33 · Related issues · -View source +View source @@ -1318,7 +1318,7 @@ class Sub(Base): Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1360,7 +1360,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1387,7 +1387,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1417,7 +1417,7 @@ with 1: # error Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1470,7 +1470,7 @@ See: Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1506,7 +1506,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1538,7 +1538,7 @@ a: str # error Default level: warn · Added in 0.0.20 · Related issues · -View source +View source @@ -1595,7 +1595,7 @@ class Pet(Enum): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1659,7 +1659,7 @@ This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](h Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1712,7 +1712,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1763,7 +1763,7 @@ class NonFrozenChild(FrozenBase): # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1812,7 +1812,7 @@ class D(Generic[U, T]): ... # error Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1908,7 +1908,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1956,7 +1956,7 @@ carol = Person(name="Carol", aeg=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -2018,7 +2018,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2058,7 +2058,7 @@ def f(t: TypeVar("U")): ... # ty: ignore[invalid-type-form] Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -2108,7 +2108,7 @@ match object(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2143,7 +2143,7 @@ class B(metaclass=42): ... # error Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2261,7 +2261,7 @@ Correct use of `@override` is enforced by ty's [`invalid-explicit-override`](#in Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -2312,13 +2312,23 @@ without a type annotation will raise an `AttributeError` at runtime. AttributeError: Cannot overwrite NamedTuple attribute _asdict ``` +Finally, `NamedTuple` field annotations cannot use the `ClassVar` or `Final` type +qualifiers. These qualifiers also cause a runtime error when annotations are evaluated eagerly: + +```pycon +>>> from typing import ClassVar, NamedTuple +>>> class Foo(NamedTuple): +... x: ClassVar[int] +TypeError: typing.ClassVar[int] is not valid as type argument +``` + ## `invalid-named-tuple-override` Default level: warn · Added in 0.0.31 · Related issues · -View source +View source @@ -2366,7 +2376,7 @@ admin[0] # "Alice" Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -2404,7 +2414,7 @@ Baz = NewType("Baz", int | str) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2461,7 +2471,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2490,7 +2500,7 @@ def f(a: int = ""): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2526,7 +2536,7 @@ P2 = ParamSpec() # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2562,7 +2572,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2633,7 +2643,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2665,7 +2675,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2776,7 +2786,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2827,7 +2837,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2873,7 +2883,7 @@ InvalidAlias = TypeAliasType("InvalidAlias", list[T], type_params=(list[T],)) # Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2940,7 +2950,7 @@ Bar[int] # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2973,7 +2983,7 @@ TYPE_CHECKING = "" # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3009,7 +3019,7 @@ b: Annotated[int] # error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -3066,7 +3076,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -3110,7 +3120,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3167,7 +3177,7 @@ V = TypeVar("V", list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -3209,7 +3219,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.28 · Related issues · -View source +View source @@ -3245,7 +3255,7 @@ class Child(Base): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -3288,7 +3298,7 @@ def f(options: dict[str, object]): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -3323,7 +3333,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.25 · Related issues · -View source +View source @@ -3358,7 +3368,7 @@ def gen() -> Iterator[int]: Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -3425,7 +3435,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -3475,7 +3485,7 @@ def g(arg: object): Default level: warn · Added in 0.0.30 · Related issues · -View source +View source @@ -3518,7 +3528,7 @@ Movie = TypedDict("Film", {"title": str}) # error: [mismatched-type-name] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3549,7 +3559,7 @@ func() # error Default level: ignore · Added in 0.0.41 · Related issues · -View source +View source @@ -3608,7 +3618,7 @@ class ExplicitChild(Parent): Default level: ignore · Added in 0.0.45 · Related issues · -View source +View source @@ -3647,7 +3657,7 @@ def handle(m: re.Match[str]) -> str: Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -3686,7 +3696,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3724,7 +3734,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.30 · Related issues · -View source +View source @@ -3762,7 +3772,7 @@ class Sub(Super): ... # error: [non-callable-init-subclass] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3791,7 +3801,7 @@ for i in 34: # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3819,7 +3829,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -3856,7 +3866,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -3893,7 +3903,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3924,7 +3934,7 @@ f(1, x=2) # error Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3955,7 +3965,7 @@ f(x=1) # error Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3994,7 +4004,7 @@ A.c # error Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4033,7 +4043,7 @@ A()[0] # error Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4079,7 +4089,7 @@ from module import a # error Default level: warn · Added in 0.0.23 · Related issues · -View source +View source @@ -4111,7 +4121,7 @@ html.parser # error Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4148,7 +4158,7 @@ print(x) # error Default level: warn · Added in 0.0.60 · Related issues · -View source +View source @@ -4223,7 +4233,7 @@ def test() -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4258,7 +4268,7 @@ cast(int, f()) # error Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -4296,7 +4306,7 @@ class C: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -4340,7 +4350,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4375,7 +4385,7 @@ static_assert(int(2.0 * 3.0) == 6) # error Default level: warn · Added in 0.0.39 · Related issues · -View source +View source @@ -4426,7 +4436,7 @@ Consider using [`functools.total_ordering`][total_ordering] instead, which does Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4460,7 +4470,7 @@ class B(A): ... # error Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -4500,7 +4510,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4530,7 +4540,7 @@ f("foo") # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4569,7 +4579,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4627,7 +4637,7 @@ class A: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -4671,7 +4681,7 @@ class C(Generic[T]): Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4700,7 +4710,7 @@ reveal_type(1) # revealed: Literal[1] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4731,7 +4741,7 @@ f(x=1, y=2) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4764,7 +4774,7 @@ A().foo # error Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -4839,7 +4849,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4868,7 +4878,7 @@ import foo # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4896,7 +4906,7 @@ print(x) # error Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -4943,7 +4953,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4992,7 +5002,7 @@ b1 < b2 < b1 # error Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -5039,7 +5049,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5072,7 +5082,7 @@ A() + A() # error Default level: warn · Added in 0.0.21 · Related issues · -View source +View source @@ -5192,7 +5202,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -5271,7 +5281,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/lint_docs/invalid-named-tuple.md b/crates/ty_python_semantic/resources/lint_docs/invalid-named-tuple.md index 59507e46f0..690ed3c1fe 100644 --- a/crates/ty_python_semantic/resources/lint_docs/invalid-named-tuple.md +++ b/crates/ty_python_semantic/resources/lint_docs/invalid-named-tuple.md @@ -41,3 +41,13 @@ without a type annotation will raise an `AttributeError` at runtime. ... _asdict = 42 AttributeError: Cannot overwrite NamedTuple attribute _asdict ``` + +Finally, `NamedTuple` field annotations cannot use the `ClassVar` or `Final` type +qualifiers. These qualifiers also cause a runtime error when annotations are evaluated eagerly: + +```pycon +>>> from typing import ClassVar, NamedTuple +>>> class Foo(NamedTuple): +... x: ClassVar[int] +TypeError: typing.ClassVar[int] is not valid as type argument +``` diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index 52877163d8..4543bfce60 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -1635,6 +1635,150 @@ Invalid = NamedTuple("Invalid", [("not valid", int), ("ok", str)]) reveal_type(Invalid) # revealed: ``` +## NamedTuple fields cannot be qualified with `ClassVar` or `Final` + +Type checkers reject `ClassVar` and `Final` qualifiers on `NamedTuple` fields. When annotations are +evaluated eagerly, passing these qualifiers to `typing._type_check` also raises `TypeError` while +the class is defined. + +```py +from typing import ClassVar, Final, NamedTuple + +class Foo(NamedTuple): + # error: [invalid-named-tuple] "Type qualifier `ClassVar` is not allowed on NamedTuple field `a`" + a: ClassVar[int] + # error: [invalid-named-tuple] "Type qualifier `Final` is not allowed on NamedTuple field `b`" + b: Final[str] = "foo" + # error: [invalid-named-tuple] "Type qualifier `ClassVar` is not allowed on NamedTuple field `c`" + # error: [invalid-named-tuple] "Type qualifier `Final` is not allowed on NamedTuple field `c`" + # error: [redundant-final-classvar] "Combining `ClassVar` and `Final` is redundant" + c: ClassVar[Final[int]] +``` + +An unsubscripted qualifier is rejected for the same reason: + +```py +from typing import ClassVar, NamedTuple + +class Bare(NamedTuple): + # error: [invalid-named-tuple] "Type qualifier `ClassVar` is not allowed on NamedTuple field `x`" + x: ClassVar +``` + +A class that inherits from a `NamedTuple` class is an ordinary class at runtime, so it may use both +qualifiers freely: + +```py +from typing import ClassVar, Final, NamedTuple + +class Base(NamedTuple): + x: int + +class Sub(Base): + y: ClassVar[int] = 1 + z: Final[str] = "z" +``` + +The full diagnostic points at the offending field: + +```py +from typing import ClassVar, NamedTuple + +class Snapshot(NamedTuple): + # snapshot + a: ClassVar[int] +``` + +```snapshot +error[invalid-named-tuple]: Type qualifier `ClassVar` is not allowed in a NamedTuple field + --> src/mdtest_snippet.py:29:5 + | +29 | a: ClassVar[int] + | ^^^^^^^^^^^^^^^^ +``` + +## NamedTuple qualifiers and redeclared symbols + +A later method declaration does not change the field annotation processed by `NamedTuple`. + +```py +from typing import Final, NamedTuple + +class Redeclared(NamedTuple): + # error: [invalid-named-tuple] "Type qualifier `Final` is not allowed on NamedTuple field `x`" + x: Final[int] + + def x(self) -> int: + return 1 +``` + +## NamedTuple qualifiers in conditional declarations + +When only one branch qualifies a field, the diagnostic points to the declaration in that branch. + +```py +from typing import Final, NamedTuple + +def condition() -> bool: + return True + +class Conditional(NamedTuple): + if condition(): + y: int + else: + # error: [invalid-named-tuple] "Type qualifier `Final` is not allowed on NamedTuple field `y`" + y: Final[int] +``` + +Statically unreachable qualified declarations are ignored: + +```py +class Unreachable(NamedTuple): + if False: + hidden: Final[int] + visible: int +``` + +## NamedTuple qualifiers in deferred annotations + +The restriction still applies when annotation evaluation is postponed. The diagnostic does not claim +that defining the class will fail at runtime, because Python stores a forward reference in this +case. + +```py +from __future__ import annotations + +from typing import Final, NamedTuple + +class Deferred(NamedTuple): + # snapshot + x: Final[int] +``` + +```snapshot +error[invalid-named-tuple]: Type qualifier `Final` is not allowed in a NamedTuple field + --> src/mdtest_snippet.py:7:5 + | +7 | x: Final[int] + | ^^^^^^^^^^^^^ +``` + +## NamedTuple qualifiers in quoted and wrapped annotations + +Explicitly quoted and `Annotated` field annotations are rejected for the same static reason: + +```py +from typing import Annotated, Final, NamedTuple + +class Quoted(NamedTuple): + # error: [invalid-named-tuple] "Type qualifier `Final` is not allowed on NamedTuple field `x`" + x: "Final[int]" + +class Wrapped(NamedTuple): + # error: [invalid-named-tuple] "Type qualifier `Final` is not allowed on NamedTuple field `x`" + x: Annotated[Final[int], "metadata"] +``` + ## Prohibited NamedTuple attributes `NamedTuple` classes have certain synthesized attributes that cannot be overwritten. Attempting to diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index df0ac4049d..cdec37009e 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -15,7 +15,9 @@ use crate::{ DefinedPlace, Definedness, Place, PlaceAndQualifiers, Provenance, PublicTypePolicy, TypeOrigin, place_from_bindings, place_from_declarations, }, - reachability::{DeclarationsIteratorExtension, binding_reachability}, + reachability::{ + DeclarationsIteratorExtension, ReachabilityConstraintsExtension, binding_reachability, + }, types::{ ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, CallArguments, CallableType, ClassBase, ClassLiteral, ClassType, DATACLASS_FLAGS, DataclassFlags, @@ -2568,6 +2570,69 @@ impl<'db> StaticClassLiteral<'db> { } } + /// Return the type qualifiers attached to each reachable annotated assignment in source order. + /// + /// This uses the declaration history rather than [`StaticClassLiteral::own_fields`], because a + /// later method or nested class can replace the symbol's binding while leaving its entry in + /// `__annotations__`: + /// + /// ```python + /// class Example(NamedTuple): + /// value: Final[int] + /// def value(self) -> int: ... + /// ``` + /// + /// Each qualifier remains paired with its own definition so diagnostics can point to the + /// annotation that introduced it, including when declarations occur in different branches. + pub(crate) fn own_annotated_qualifiers( + self, + db: &'db dyn Db, + ) -> Vec<(Name, TypeQualifiers, Definition<'db>)> { + let body_scope = self.body_scope(db); + let table = place_table(db, body_scope); + let use_def = use_def_map(db, body_scope); + let mut annotated_qualifiers = Vec::new(); + + for (symbol_id, _) in use_def.all_end_of_scope_symbol_declarations() { + let declarations = use_def.reachable_symbol_declarations(symbol_id); + let predicates = declarations.predicates(); + let reachability_constraints = declarations.reachability_constraints(); + + for declaration in declarations { + if reachability_constraints + .evaluate(db, predicates, declaration.reachability_constraint) + .is_always_false() + { + continue; + } + + let DefinitionState::Defined(definition) = declaration.declaration else { + continue; + }; + if !matches!(definition.kind(db), DefinitionKind::AnnotatedAssignment(..)) { + continue; + } + + let Some(declared) = inferred_declaration(db, definition).declared() else { + continue; + }; + annotated_qualifiers.push(( + declaration.declaration_order, + table.symbol(symbol_id).name().clone(), + declared.qualifiers(), + definition, + )); + } + } + + annotated_qualifiers + .sort_unstable_by_key(|(declaration_order, _, _, _)| *declaration_order); + annotated_qualifiers + .into_iter() + .map(|(_, name, qualifiers, definition)| (name, qualifiers, definition)) + .collect() + } + /// Look up an instance attribute (available in `__dict__`) of the given name. /// /// See [`Type::instance_member`] for more details. diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index a0c1cbd5ce..e17eeaea36 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -18,6 +18,7 @@ use crate::types::function::{FunctionDecorators, FunctionType, KnownFunction, Ov use crate::types::infer::UnsupportedComparisonError; use crate::types::overrides::MethodKind; use crate::types::protocol_class::ProtocolMember; +use crate::types::special_form::TypeQualifier; use crate::types::string_annotation::{ ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION, RAW_STRING_TYPE_ANNOTATION, @@ -3336,6 +3337,32 @@ pub(super) fn report_named_tuple_field_with_leading_underscore<'db>( )); } +/// Report a `NamedTuple` field annotated with a type qualifier that `NamedTuple` does not accept. +/// +/// The diagnostic is anchored to the annotated assignment that introduced the qualifier. It does +/// not claim that class creation fails at runtime because deferred and wrapped annotations can +/// preserve the qualifier without passing it directly to `typing._type_check`. +pub(super) fn report_invalid_named_tuple_field_qualifier<'db>( + context: &InferContext<'db, '_>, + field_name: &str, + qualifier: TypeQualifier, + field_definition: Definition<'db>, +) { + let db = context.db(); + let module = context.module(); + let qualifier = qualifier.name(); + let diagnostic_range = field_definition.kind(db).full_range(module); + let Some(builder) = context.report_lint(&INVALID_NAMED_TUPLE, diagnostic_range) else { + return; + }; + let mut diagnostic = builder.into_diagnostic(format_args!( + "Type qualifier `{qualifier}` is not allowed in a NamedTuple field" + )); + diagnostic.set_concise_message(format_args!( + "Type qualifier `{qualifier}` is not allowed on NamedTuple field `{field_name}`" + )); +} + pub(crate) fn report_missing_typed_dict_key<'db>( context: &InferContext<'db, '_>, constructor_node: AnyNodeRef, diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs index 3891f5e18e..82c752c56a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs @@ -33,9 +33,10 @@ use crate::{ report_bad_frozen_dataclass_inheritance, report_conflicting_metaclass_from_bases, report_duplicate_bases, report_inconsistent_generic_bases, report_instance_layout_conflict, report_invalid_attribute_assignment, - report_invalid_or_unsupported_base, report_invalid_total_ordering, - report_invalid_type_param_order, report_invalid_typevar_default_reference, - report_missing_type_arguments, report_named_tuple_field_with_leading_underscore, + report_invalid_named_tuple_field_qualifier, report_invalid_or_unsupported_base, + report_invalid_total_ordering, report_invalid_type_param_order, + report_invalid_typevar_default_reference, report_missing_type_arguments, + report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_shadowed_type_variable, report_subclass_of_class_with_non_callable_init_subclass, report_unsupported_base, @@ -47,6 +48,7 @@ use crate::{ infer_definition_types, mro::StaticMroErrorKind, overrides, + special_form::TypeQualifier, tuple::Tuple, typevar::TypeVarInstance, variance::VarianceInferable, @@ -116,6 +118,27 @@ pub(crate) fn check_static_class_definitions<'db>( // If it's a `NamedTuple` class, check that no field without a default value // appears after a field with a default value. if class_kind == Some(CodeGeneratorKind::NamedTuple) { + // `ClassVar` and `Final` fields have to be checked against the class body's annotations + // rather than against `own_fields`, since `own_fields` drops `ClassVar` declarations and + // does not retain the `Final` qualifier for the fields that it does keep. + // + // A field carrying both qualifiers is reported once per qualifier, since each qualifier + // independently violates the restriction on `NamedTuple` fields. + for (field_name, qualifiers, declaration) in class.own_annotated_qualifiers(db) { + let invalid_qualifiers = [TypeQualifier::ClassVar, TypeQualifier::Final] + .into_iter() + .filter(|qualifier| qualifiers.contains(TypeQualifiers::from(*qualifier))); + + for qualifier in invalid_qualifiers { + report_invalid_named_tuple_field_qualifier( + context, + &field_name, + qualifier, + declaration, + ); + } + } + let mut field_with_default_encountered = None; for (field_name, field) in class.own_fields(db, None, CodeGeneratorKind::NamedTuple) { diff --git a/ty.schema.json b/ty.schema.json index be1fa4be1c..58818ff729 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -849,7 +849,7 @@ }, "invalid-named-tuple": { "title": "detects invalid `NamedTuple` class definitions", - "description": "## What it does\n\nChecks for invalidly defined `NamedTuple` classes.\n\n## Why is this bad?\n\nAn invalidly defined `NamedTuple` class may lead to the type checker\ndrawing incorrect conclusions. It may also lead to `TypeError`s or\n`AttributeError`s at runtime.\n\n## Examples\n\nA class definition cannot combine `NamedTuple` with other base classes\nin multiple inheritance; doing so raises a `TypeError` at runtime. The sole\nexception to this rule is `Generic[]`, which can be used alongside `NamedTuple`\nin a class's bases list.\n\n```pycon\n>>> from typing import NamedTuple\n>>> class Foo(NamedTuple, object): ...\nTypeError: can only inherit from a NamedTuple type and Generic\n```\n\nFurther, `NamedTuple` field names cannot start with an underscore:\n\n```pycon\n>>> from typing import NamedTuple\n>>> class Foo(NamedTuple):\n... _bar: int\nValueError: Field names cannot start with an underscore: '_bar'\n```\n\n`NamedTuple` classes also have certain synthesized attributes (like `_asdict`, `_make`,\n`_replace`, etc.) that cannot be overwritten. Attempting to assign to these attributes\nwithout a type annotation will raise an `AttributeError` at runtime.\n\n```pycon\n>>> from typing import NamedTuple\n>>> class Foo(NamedTuple):\n... x: int\n... _asdict = 42\nAttributeError: Cannot overwrite NamedTuple attribute _asdict\n```", + "description": "## What it does\n\nChecks for invalidly defined `NamedTuple` classes.\n\n## Why is this bad?\n\nAn invalidly defined `NamedTuple` class may lead to the type checker\ndrawing incorrect conclusions. It may also lead to `TypeError`s or\n`AttributeError`s at runtime.\n\n## Examples\n\nA class definition cannot combine `NamedTuple` with other base classes\nin multiple inheritance; doing so raises a `TypeError` at runtime. The sole\nexception to this rule is `Generic[]`, which can be used alongside `NamedTuple`\nin a class's bases list.\n\n```pycon\n>>> from typing import NamedTuple\n>>> class Foo(NamedTuple, object): ...\nTypeError: can only inherit from a NamedTuple type and Generic\n```\n\nFurther, `NamedTuple` field names cannot start with an underscore:\n\n```pycon\n>>> from typing import NamedTuple\n>>> class Foo(NamedTuple):\n... _bar: int\nValueError: Field names cannot start with an underscore: '_bar'\n```\n\n`NamedTuple` classes also have certain synthesized attributes (like `_asdict`, `_make`,\n`_replace`, etc.) that cannot be overwritten. Attempting to assign to these attributes\nwithout a type annotation will raise an `AttributeError` at runtime.\n\n```pycon\n>>> from typing import NamedTuple\n>>> class Foo(NamedTuple):\n... x: int\n... _asdict = 42\nAttributeError: Cannot overwrite NamedTuple attribute _asdict\n```\n\nFinally, `NamedTuple` field annotations cannot use the `ClassVar` or `Final` type\nqualifiers. These qualifiers also cause a runtime error when annotations are evaluated eagerly:\n\n```pycon\n>>> from typing import ClassVar, NamedTuple\n>>> class Foo(NamedTuple):\n... x: ClassVar[int]\nTypeError: typing.ClassVar[int] is not valid as type argument\n```", "default": "error", "oneOf": [ { From 6cfeb8aa07306cf06d7ab552d4190a5d24b6ff8d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 22:08:13 -0400 Subject: [PATCH 189/390] Narrow internal visibility with Hawk (#27339) ## Summary This PR uses Hawk 0.1.11 to narrow internal declarations to the visibility they actually need. Here, I've intentionally _only_ made visibility changes; unused API removals are split into follow-up PRs. In total, 1,201 declaration visibility modifiers are narrowed. Public generated formatter APIs and other declarations whose narrower visibility would activate dead-code or API-shape lints remain unchanged. --- crates/mdtest/src/lib.rs | 22 +- crates/mdtest/src/matcher.rs | 2 +- crates/mdtest/src/parser.rs | 4 +- crates/ruff/src/args.rs | 146 ++++---- crates/ruff/src/cache.rs | 8 +- crates/ruff/src/diagnostics.rs | 2 +- crates/ruff/src/main.rs | 2 +- crates/ruff_db/src/diagnostic/mod.rs | 18 +- crates/ruff_db/src/diagnostic/render.rs | 9 +- crates/ruff_db/src/file_revision.rs | 4 +- crates/ruff_db/src/files.rs | 8 +- crates/ruff_db/src/files/directory.rs | 2 +- crates/ruff_db/src/files/file_root.rs | 2 +- crates/ruff_db/src/parsed.rs | 4 +- crates/ruff_db/src/system/test.rs | 4 +- crates/ruff_diagnostics/src/edit.rs | 8 +- crates/ruff_formatter/src/buffer.rs | 10 +- crates/ruff_formatter/src/format_element.rs | 20 +- .../ruff_formatter/src/format_element/tag.rs | 32 +- crates/ruff_formatter/src/formatter.rs | 7 +- crates/ruff_formatter/src/lib.rs | 12 +- crates/ruff_formatter/src/printer/mod.rs | 6 +- crates/ruff_formatter/src/printer/stack.rs | 2 +- crates/ruff_graph/src/lib.rs | 2 +- crates/ruff_graph/src/resolver.rs | 2 +- crates/ruff_index/src/frozen.rs | 6 +- crates/ruff_linter/src/checkers/ast/mod.rs | 8 +- crates/ruff_linter/src/codes.rs | 5 +- crates/ruff_linter/src/directives.rs | 6 +- crates/ruff_linter/src/docstrings/sections.rs | 2 +- crates/ruff_linter/src/fs.rs | 2 +- crates/ruff_linter/src/line_width.rs | 12 +- crates/ruff_linter/src/linter.rs | 2 +- crates/ruff_linter/src/locator.rs | 20 +- crates/ruff_linter/src/logging.rs | 2 +- crates/ruff_linter/src/message/grouped.rs | 20 +- crates/ruff_linter/src/message/mod.rs | 12 +- crates/ruff_linter/src/message/sarif.rs | 4 +- crates/ruff_linter/src/noqa.rs | 5 +- crates/ruff_linter/src/registry/rule_set.rs | 2 +- .../ruff_linter/src/rules/airflow/helpers.rs | 2 +- .../src/rules/flake8_boolean_trap/helpers.rs | 11 +- .../src/rules/flake8_comprehensions/fixes.rs | 2 +- .../flake8_import_conventions/settings.rs | 2 +- .../rules/redundant_numeric_union.rs | 4 +- .../rules/unittest_assert.rs | 2 +- .../rules/flake8_pytest_style/rules/warns.rs | 2 +- .../src/rules/flake8_quotes/settings.rs | 4 +- .../src/rules/flake8_self/settings.rs | 2 +- .../flake8_simplify/rules/ast_bool_op.rs | 2 +- .../flake8_simplify/rules/collapsible_if.rs | 10 +- .../src/rules/flake8_tidy_imports/settings.rs | 4 +- .../src/rules/flake8_todos/rules/todos.rs | 2 +- .../src/rules/flake8_type_checking/helpers.rs | 2 +- .../src/rules/flake8_use_pathlib/helpers.rs | 2 +- crates/ruff_linter/src/rules/isort/mod.rs | 10 +- .../ruff_linter/src/rules/isort/settings.rs | 4 +- .../perflint/rules/unnecessary_list_cast.rs | 6 +- .../pycodestyle/rules/logical_lines/mod.rs | 21 +- .../property_docstring_starts_with_verb.rs | 2 +- .../src/rules/pydocstyle/settings.rs | 8 +- .../src/rules/pyflakes/rules/imports.rs | 4 +- .../rules/pylint/rules/bad_str_strip_call.rs | 4 +- .../rules/pylint/rules/nonlocal_and_global.rs | 2 +- .../rules/redefined_argument_from_local.rs | 2 +- .../ruff_linter/src/rules/pylint/settings.rs | 2 +- .../rules/super_call_with_parameters.rs | 4 +- .../pyupgrade/rules/use_pep604_isinstance.rs | 2 +- .../refurb/rules/reimplemented_starmap.rs | 6 +- .../ruff/rules/logging_eager_conversion.rs | 4 +- .../src/rules/ruff/rules/redirected_noqa.rs | 2 +- .../src/settings/fix_safety_table.rs | 2 +- crates/ruff_linter/src/settings/rule_table.rs | 2 +- crates/ruff_linter/src/settings/types.rs | 16 +- crates/ruff_linter/src/suppression.rs | 10 +- crates/ruff_python_ast/src/helpers.rs | 2 +- crates/ruff_python_ast/src/identifier.rs | 4 +- crates/ruff_python_ast/src/int.rs | 4 +- crates/ruff_python_ast/src/name.rs | 8 +- crates/ruff_python_ast/src/node_index.rs | 2 +- crates/ruff_python_ast/src/parenthesize.rs | 2 +- crates/ruff_python_ast/src/str.rs | 10 +- crates/ruff_python_ast/src/token/tokens.rs | 2 +- crates/ruff_python_codegen/src/generator.rs | 8 +- .../src/comments/format.rs | 4 +- .../ruff_python_formatter/src/comments/mod.rs | 2 +- crates/ruff_python_formatter/src/context.rs | 2 +- .../src/expression/expr_slice.rs | 2 +- .../src/expression/mod.rs | 12 +- crates/ruff_python_formatter/src/main.rs | 2 +- crates/ruff_python_formatter/src/options.rs | 10 +- .../src/other/interpolated_string_element.rs | 5 +- .../src/other/parameters.rs | 6 +- .../ruff_python_formatter/src/pattern/mod.rs | 5 +- .../src/statement/clause.rs | 2 +- .../src/statement/stmt_assign.rs | 2 +- .../src/statement/suite.rs | 2 +- .../src/string/normalize.rs | 2 +- crates/ruff_python_index/src/indexer.rs | 2 +- crates/ruff_python_literal/src/char.rs | 2 +- crates/ruff_python_literal/src/format.rs | 2 +- crates/ruff_python_literal/src/lib.rs | 2 +- crates/ruff_python_parser/src/lexer.rs | 2 +- .../src/lexer/indentation.rs | 6 +- crates/ruff_python_parser/src/lib.rs | 8 +- .../src/parser/expression.rs | 22 +- crates/ruff_python_parser/src/parser/mod.rs | 2 +- crates/ruff_python_parser/src/token_source.rs | 2 +- .../src/analyze/terminal.rs | 2 +- .../src/analyze/type_inference.rs | 2 +- .../src/analyze/typing.rs | 8 +- crates/ruff_python_semantic/src/binding.rs | 2 +- crates/ruff_python_semantic/src/cfg/graph.rs | 6 +- crates/ruff_python_semantic/src/definition.rs | 8 +- crates/ruff_python_semantic/src/model.rs | 22 +- crates/ruff_python_semantic/src/nodes.rs | 10 +- crates/ruff_python_semantic/src/scope.rs | 6 +- crates/ruff_python_stdlib/src/builtins.rs | 2 +- crates/ruff_python_trivia/src/cursor.rs | 8 +- crates/ruff_python_trivia/src/tokenizer.rs | 2 +- crates/ruff_ranged_value/src/lib.rs | 2 +- crates/ruff_server/src/edit/notebook.rs | 6 +- crates/ruff_server/src/edit/text_document.rs | 14 +- crates/ruff_server/src/format.rs | 6 +- crates/ruff_server/src/lib.rs | 12 +- crates/ruff_server/src/lint.rs | 8 +- crates/ruff_server/src/server/api.rs | 2 +- .../src/server/api/requests/format.rs | 2 +- .../src/server/api/requests/hover.rs | 2 +- .../src/server/schedule/thread/pool.rs | 2 +- crates/ruff_server/src/session/client.rs | 2 +- crates/ruff_server/src/session/index.rs | 10 +- crates/ruff_server/src/session/options.rs | 6 +- .../ruff_server/src/session/request_queue.rs | 2 +- crates/ruff_server/src/workspace.rs | 4 +- crates/ruff_source_file/src/line_index.rs | 6 +- crates/ruff_source_file/src/newlines.rs | 2 +- crates/ruff_workspace/src/configuration.rs | 10 +- crates/ruff_workspace/src/options.rs | 319 +++++++++--------- crates/ruff_workspace/src/pyproject.rs | 2 +- crates/ruff_workspace/src/resolver.rs | 10 +- crates/ty/docs/rules.md | 10 +- crates/ty/src/args.rs | 22 +- crates/ty/src/lib.rs | 4 +- crates/ty/src/logging.rs | 4 +- crates/ty/src/main.rs | 2 +- crates/ty_ide/src/all_symbols.rs | 2 +- crates/ty_ide/src/code_action.rs | 12 +- .../src/docstring/document/preformatted.rs | 6 +- crates/ty_ide/src/goto.rs | 4 +- crates/ty_ide/src/hints.rs | 2 +- crates/ty_ide/src/inlay_hints.rs | 10 +- crates/ty_ide/src/lib.rs | 6 +- crates/ty_ide/src/semantic_tokens.rs | 6 +- crates/ty_ide/src/stub_mapping.rs | 2 +- crates/ty_ide/src/symbols.rs | 27 +- crates/ty_module_resolver/src/lib.rs | 6 +- crates/ty_module_resolver/src/path.rs | 6 +- crates/ty_module_resolver/src/resolve.rs | 10 +- crates/ty_module_resolver/src/typeshed.rs | 2 +- crates/ty_project/src/db.rs | 2 +- crates/ty_project/src/lib.rs | 30 +- crates/ty_project/src/metadata.rs | 14 +- crates/ty_project/src/metadata/options.rs | 18 +- crates/ty_project/src/metadata/pyproject.rs | 10 +- .../ty_project/src/metadata/python_version.rs | 4 +- crates/ty_project/src/metadata/settings.rs | 22 +- crates/ty_project/src/metadata/value.rs | 8 +- crates/ty_project/src/walk.rs | 2 +- crates/ty_project/src/watch.rs | 4 +- crates/ty_project/src/watch/watcher.rs | 14 +- crates/ty_python_core/src/builder.rs | 4 +- .../src/builder/loop_bindings_visitor.rs | 2 +- crates/ty_python_core/src/db.rs | 2 +- crates/ty_python_core/src/definition.rs | 34 +- crates/ty_python_core/src/frozen.rs | 2 +- crates/ty_python_core/src/lib.rs | 10 +- crates/ty_python_core/src/member.rs | 8 +- crates/ty_python_core/src/place.rs | 20 +- .../src/reachability_constraints.rs | 4 +- crates/ty_python_core/src/scope.rs | 12 +- crates/ty_python_core/src/symbol.rs | 4 +- crates/ty_python_core/src/unpack.rs | 2 +- crates/ty_python_core/src/use_def.rs | 6 +- .../ty_python_core/src/use_def/place_state.rs | 10 +- crates/ty_python_semantic/src/db.rs | 2 +- .../ty_python_semantic/src/diagnostic/mod.rs | 2 +- crates/ty_python_semantic/src/lib.rs | 21 +- crates/ty_python_semantic/src/lint.rs | 18 +- crates/ty_python_semantic/src/place.rs | 17 +- crates/ty_python_semantic/src/reachability.rs | 2 +- .../ty_python_semantic/src/semantic_model.rs | 8 +- crates/ty_python_semantic/src/suppression.rs | 14 +- .../src/suppression/add_ignore.rs | 8 +- crates/ty_python_semantic/src/types.rs | 282 ++++++++-------- crates/ty_python_semantic/src/types/bool.rs | 4 +- .../src/types/call/arguments.rs | 8 +- .../ty_python_semantic/src/types/call/bind.rs | 32 +- .../ty_python_semantic/src/types/callable.rs | 11 +- crates/ty_python_semantic/src/types/class.rs | 28 +- .../src/types/class/known.rs | 2 +- .../src/types/class/named_tuple.rs | 2 +- .../src/types/class/static_literal.rs | 4 +- .../src/types/constraints.rs | 36 +- crates/ty_python_semantic/src/types/cyclic.rs | 4 +- .../src/types/dedicated/pydantic.rs | 2 +- .../src/types/diagnostic.rs | 2 +- .../ty_python_semantic/src/types/display.rs | 28 +- crates/ty_python_semantic/src/types/enums.rs | 6 +- .../ty_python_semantic/src/types/equality.rs | 2 +- .../ty_python_semantic/src/types/function.rs | 10 +- .../ty_python_semantic/src/types/generics.rs | 6 +- .../src/types/ide_support.rs | 2 +- crates/ty_python_semantic/src/types/infer.rs | 54 ++- .../src/types/infer/builder.rs | 4 +- .../types/infer/builder/final_attribute.rs | 2 +- .../src/types/infer/builder/subscript.rs | 4 +- .../types/infer/builder/type_expression.rs | 12 +- .../ty_python_semantic/src/types/instance.rs | 2 +- .../src/types/known_instance.rs | 2 +- .../src/types/list_members.rs | 12 +- .../src/types/match_pattern.rs | 2 +- crates/ty_python_semantic/src/types/mro.rs | 12 +- .../ty_python_semantic/src/types/newtype.rs | 4 +- .../types/property_tests/type_generation.rs | 2 +- .../src/types/protocol_class.rs | 4 +- .../ty_python_semantic/src/types/relation.rs | 24 +- .../src/types/set_theoretic.rs | 10 +- .../src/types/set_theoretic/builder.rs | 2 +- .../src/types/signatures.rs | 30 +- .../src/types/special_form.rs | 2 +- .../src/types/subclass_of.rs | 12 +- .../ty_python_semantic/src/types/subscript.rs | 4 +- crates/ty_python_semantic/src/types/tuple.rs | 16 +- .../src/types/type_alias.rs | 12 +- .../src/types/typed_dict.rs | 14 +- .../ty_python_semantic/src/types/typevar.rs | 8 +- .../ty_python_semantic/src/types/unpacker.rs | 5 +- .../ty_python_semantic/src/types/visitor.rs | 6 +- crates/ty_server/src/capabilities.rs | 2 +- crates/ty_server/src/document/notebook.rs | 4 +- crates/ty_server/src/document/range.rs | 2 +- .../ty_server/src/document/text_document.rs | 14 +- crates/ty_server/src/lib.rs | 2 +- crates/ty_server/src/server/api.rs | 2 +- .../src/server/schedule/thread/pool.rs | 2 +- crates/ty_server/src/session.rs | 27 +- crates/ty_server/src/session/client.rs | 2 +- crates/ty_server/src/session/index.rs | 8 +- crates/ty_server/src/session/options.rs | 24 +- crates/ty_server/src/system.rs | 2 +- crates/ty_site_packages/src/lib.rs | 30 +- crates/ty_test/src/config.rs | 12 +- 253 files changed, 1265 insertions(+), 1352 deletions(-) diff --git a/crates/mdtest/src/lib.rs b/crates/mdtest/src/lib.rs index 24a41dcb0f..9101de6704 100644 --- a/crates/mdtest/src/lib.rs +++ b/crates/mdtest/src/lib.rs @@ -190,7 +190,7 @@ impl OutputFormat { /// Actions can detect them as workflow commands. Workflow commands must /// appear at the beginning of a line in stdout to be parsed by GitHub. #[expect(clippy::print_stdout)] - pub fn write_error( + fn write_error( self, assertion_buf: &mut String, file: &str, @@ -220,7 +220,7 @@ impl OutputFormat { /// Write a module-resolution inconsistency in the appropriate format. /// - /// See [`write_error`](Self::write_error) for details on why GitHub-format + /// See `write_error` for details on why GitHub-format /// messages must be printed directly to stdout. #[expect(clippy::print_stdout)] pub fn write_inconsistency( @@ -314,7 +314,7 @@ impl TestFile<'_> { } } -pub(crate) fn diagnostic_display_config(tool_name: &'static str) -> DisplayDiagnosticConfig { +fn diagnostic_display_config(tool_name: &'static str) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig::new(tool_name) .color(false) .with_fix_applicability(Applicability::DisplayOnly) @@ -332,11 +332,7 @@ pub fn render_diagnostic(db: &dyn Db, tool_name: &'static str, diagnostic: &Diag .to_string() } -pub(crate) fn render_diagnostics( - db: &dyn Db, - tool_name: &'static str, - diagnostics: &[Diagnostic], -) -> String { +fn render_diagnostics(db: &dyn Db, tool_name: &'static str, diagnostics: &[Diagnostic]) -> String { let mut rendered = String::new(); for diag in diagnostics { writeln!(rendered, "{}", render_diagnostic(db, tool_name, diag)).unwrap(); @@ -345,14 +341,14 @@ pub(crate) fn render_diagnostics( rendered.trim_end_matches('\n').to_string() } -pub(crate) fn is_update_inline_snapshots_enabled() -> bool { +fn is_update_inline_snapshots_enabled() -> bool { let is_enabled: std::sync::LazyLock<_> = std::sync::LazyLock::new(|| { std::env::var_os(MDTEST_UPDATE_SNAPSHOTS).is_some_and(|v| v != "0") }); *is_enabled } -pub(crate) fn apply_snapshot_filters(rendered: &str) -> std::borrow::Cow<'_, str> { +fn apply_snapshot_filters(rendered: &str) -> std::borrow::Cow<'_, str> { static INLINE_SNAPSHOT_PATH_FILTER: std::sync::LazyLock = std::sync::LazyLock::new(|| regex::Regex::new(r#"\\(\w\w|\.|")"#).unwrap()); @@ -522,7 +518,7 @@ fn try_apply_markdown_edits( } } -pub fn create_diagnostic_snapshot<'d, C>( +fn create_diagnostic_snapshot<'d, C>( db: &dyn Db, tool_name: &'static str, relative_fixture_path: &Utf8Path, @@ -581,8 +577,8 @@ pub fn create_diagnostic_snapshot<'d, C>( #[derive(Debug, Clone)] pub struct MarkdownEdit { - pub(crate) range: TextRange, - pub(crate) replacement: String, + range: TextRange, + replacement: String, } /// Run a function over an embedded test file, catching any panics that occur in the process. diff --git a/crates/mdtest/src/matcher.rs b/crates/mdtest/src/matcher.rs index 88f70cf4b6..8b791b2414 100644 --- a/crates/mdtest/src/matcher.rs +++ b/crates/mdtest/src/matcher.rs @@ -29,7 +29,7 @@ pub struct FailuresByLine { } impl FailuresByLine { - pub fn iter(&self) -> impl Iterator { + pub(crate) fn iter(&self) -> impl Iterator { self.lines.iter().map(|line_failures| { ( line_failures.line_number, diff --git a/crates/mdtest/src/parser.rs b/crates/mdtest/src/parser.rs index 0fedd213b6..017e8ccca9 100644 --- a/crates/mdtest/src/parser.rs +++ b/crates/mdtest/src/parser.rs @@ -346,7 +346,7 @@ pub(crate) enum EmbeddedFilePath<'s> { } impl EmbeddedFilePath<'_> { - pub(crate) fn as_str(&self) -> &str { + fn as_str(&self) -> &str { match self { EmbeddedFilePath::Autogenerated(PySourceType::Python) => "mdtest_snippet.py", EmbeddedFilePath::Autogenerated(PySourceType::Stub) => "mdtest_snippet.pyi", @@ -428,7 +428,7 @@ impl EmbeddedFile<'_> { } } - pub(crate) fn is_checkable(&self) -> bool { + fn is_checkable(&self) -> bool { matches!(self.lang, "py" | "python" | "pyi" | "ipynb" | "toml") } } diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index de02e3dc8d..bbf6b5516c 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -57,7 +57,7 @@ pub struct GlobalConfigArgs { global = true, help_heading = "Global options", )] - pub config: Vec, + config: Vec, /// Ignore all configuration files. // // Note: We can't mark this as conflicting with `--config` here @@ -68,7 +68,7 @@ pub struct GlobalConfigArgs { // If a user specifies `ruff check --isolated --config=ruff.toml`, // we emit an error later on, after the initial parsing by clap. #[arg(long, help_heading = "Global options", global = true)] - pub isolated: bool, + isolated: bool, /// Control when colored output is used. #[arg( @@ -233,7 +233,7 @@ pub struct AnalyzeGraphCommand { pub struct CheckCommand { /// List of files or directories to check. #[clap(help = "List of files or directories to check, or `-` to read from stdin [default: .]")] - pub files: Vec, + files: Vec, /// Apply fixes to resolve lint violations. /// Use `--no-fix` to disable or `--unsafe-fixes` to include unsafe fixes. #[arg(long, overrides_with("no_fix"))] @@ -255,10 +255,10 @@ pub struct CheckCommand { /// Avoid writing any fixed files back; instead, output a diff for each changed file to stdout, and exit 0 if there are no diffs. /// Implies `--fix-only`. #[arg(long, conflicts_with = "show_fixes")] - pub diff: bool, + diff: bool, /// Run in watch mode by re-running whenever files change. #[arg(short, long)] - pub watch: bool, + watch: bool, /// Apply fixes to resolve lint violations, but don't report on, or exit non-zero for, leftover violations. Implies `--fix`. /// Use `--no-fix-only` to disable or `--unsafe-fixes` to include unsafe fixes. #[arg(long, overrides_with("no_fix_only"))] @@ -272,14 +272,14 @@ pub struct CheckCommand { /// Output serialization format for violations. /// The default serialization format is "full". #[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")] - pub output_format: Option, + output_format: Option, /// Specify file to write the linter output to (default: stdout). #[arg(short, long, env = "RUFF_OUTPUT_FILE")] - pub output_file: Option, + output_file: Option, /// The minimum Python version that should be supported. #[arg(long, value_enum)] - pub target_version: Option, + target_version: Option, /// Enable preview mode; checks will include unstable rules and fixes. /// Use `--no-preview` to disable. #[arg(long, overrides_with("no_preview"))] @@ -295,7 +295,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide_possible_values = true )] - pub select: Option>, + select: Option>, /// Comma-separated list of rule codes to disable. #[arg( long, @@ -305,7 +305,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide_possible_values = true )] - pub ignore: Option>, + ignore: Option>, /// Like --select, but adds additional rule codes on top of those already specified. #[arg( long, @@ -315,7 +315,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide_possible_values = true )] - pub extend_select: Option>, + extend_select: Option>, /// Like --ignore. (Deprecated: You can just use --ignore instead.) #[arg( long, @@ -325,13 +325,13 @@ pub struct CheckCommand { help_heading = "Rule selection", hide = true )] - pub extend_ignore: Option>, + extend_ignore: Option>, /// List of mappings from file pattern to code to exclude. #[arg(long, value_delimiter = ',', help_heading = "Rule selection")] - pub per_file_ignores: Option>, + per_file_ignores: Option>, /// Like `--per-file-ignores`, but adds additional ignores on top of those already specified. #[arg(long, value_delimiter = ',', help_heading = "Rule selection")] - pub extend_per_file_ignores: Option>, + extend_per_file_ignores: Option>, /// List of paths, used to omit files and/or directories from analysis. #[arg( long, @@ -339,7 +339,7 @@ pub struct CheckCommand { value_name = "FILE_PATTERN", help_heading = "File selection" )] - pub exclude: Option>, + exclude: Option>, /// Like --exclude, but adds additional files and directories on top of those already excluded. #[arg( long, @@ -347,7 +347,7 @@ pub struct CheckCommand { value_name = "FILE_PATTERN", help_heading = "File selection" )] - pub extend_exclude: Option>, + extend_exclude: Option>, /// List of rule codes to treat as eligible for fix. Only applicable when fix itself is enabled (e.g., via `--fix`). #[arg( long, @@ -357,7 +357,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide_possible_values = true )] - pub fixable: Option>, + fixable: Option>, /// List of rule codes to treat as ineligible for fix. Only applicable when fix itself is enabled (e.g., via `--fix`). #[arg( long, @@ -367,7 +367,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide_possible_values = true )] - pub unfixable: Option>, + unfixable: Option>, /// Like --fixable, but adds additional rule codes on top of those already specified. #[arg( long, @@ -377,7 +377,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide_possible_values = true )] - pub extend_fixable: Option>, + extend_fixable: Option>, /// Like --unfixable. (Deprecated: You can just use --unfixable instead.) #[arg( long, @@ -387,7 +387,7 @@ pub struct CheckCommand { help_heading = "Rule selection", hide = true )] - pub extend_unfixable: Option>, + extend_unfixable: Option>, /// Respect file exclusions via `.gitignore` and other standard ignore files. /// Use `--no-respect-gitignore` to disable. #[arg( @@ -410,23 +410,23 @@ pub struct CheckCommand { no_force_exclude: bool, /// Set the line-length for length-associated rules and automatic formatting. #[arg(long, help_heading = "Rule configuration", hide = true)] - pub line_length: Option, + line_length: Option, /// Regular expression matching the name of dummy variables. #[arg(long, help_heading = "Rule configuration", hide = true)] - pub dummy_variable_rgx: Option, + dummy_variable_rgx: Option, /// Disable cache reads. #[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")] - pub no_cache: bool, + no_cache: bool, /// Path to the cache directory. #[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")] - pub cache_dir: Option, + cache_dir: Option, /// The name of the file when passing it through stdin. #[arg(long, help_heading = "Miscellaneous")] - pub stdin_filename: Option, + stdin_filename: Option, /// List of mappings from file extension to language (one of `python`, `ipynb`, `pyi`). For /// example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`. #[arg(long, value_delimiter = ',')] - pub extension: Option>, + extension: Option>, /// Exit with status code "0", even upon detecting lint violations. #[arg( short, @@ -434,10 +434,10 @@ pub struct CheckCommand { help_heading = "Miscellaneous", conflicts_with = "exit_non_zero_on_fix" )] - pub exit_zero: bool, + exit_zero: bool, /// Exit with a non-zero status code if any files were modified via fix, even if no lint violations remain. #[arg(long, help_heading = "Miscellaneous", conflicts_with = "exit_zero")] - pub exit_non_zero_on_fix: bool, + exit_non_zero_on_fix: bool, /// Show counts for every rule with at least one violation. #[arg( long, @@ -445,7 +445,7 @@ pub struct CheckCommand { conflicts_with = "diff", conflicts_with = "watch", )] - pub statistics: bool, + statistics: bool, /// Enable automatic additions of `noqa` directives to failing lines. /// Optionally provide a reason to append after the codes. #[arg( @@ -466,7 +466,7 @@ pub struct CheckCommand { conflicts_with = "fix", conflicts_with = "diff", )] - pub add_noqa: Option, + add_noqa: Option, /// Enable automatic additions of `ruff: ignore` comments to failing lines. /// Optionally provide a reason to append after the codes. /// In preview, add suppression comments with rule names instead. @@ -488,7 +488,7 @@ pub struct CheckCommand { conflicts_with = "fix", conflicts_with = "diff", )] - pub add_ignore: Option, + add_ignore: Option, /// See the files Ruff will be run against with the current settings. #[arg( long, @@ -502,7 +502,7 @@ pub struct CheckCommand { conflicts_with = "stdin_filename", conflicts_with = "watch", )] - pub show_files: bool, + show_files: bool, /// See the settings Ruff will use to lint a given Python file. #[arg( long, @@ -516,7 +516,7 @@ pub struct CheckCommand { conflicts_with = "stdin_filename", conflicts_with = "watch", )] - pub show_settings: bool, + show_settings: bool, } #[derive(Clone, Debug, clap::Parser)] @@ -526,22 +526,22 @@ pub struct FormatCommand { #[clap( help = "List of files or directories to format, or `-` to read from stdin [default: .]" )] - pub files: Vec, + files: Vec, /// Avoid writing any formatted files back; instead, exit with a non-zero status code if any /// files would have been modified, and zero otherwise. #[arg(long)] - pub check: bool, + check: bool, /// Avoid writing any formatted files back; instead, exit with a non-zero status code and the /// difference between the current file and how the formatted file would look like. #[arg(long)] - pub diff: bool, + diff: bool, /// Disable cache reads. #[arg(short, long, env = "RUFF_NO_CACHE", help_heading = "Miscellaneous")] - pub no_cache: bool, + no_cache: bool, /// Path to the cache directory. #[arg(long, env = "RUFF_CACHE_DIR", help_heading = "Miscellaneous")] - pub cache_dir: Option, + cache_dir: Option, /// Respect file exclusions via `.gitignore` and other standard ignore files. /// Use `--no-respect-gitignore` to disable. @@ -560,7 +560,7 @@ pub struct FormatCommand { value_name = "FILE_PATTERN", help_heading = "File selection" )] - pub exclude: Option>, + exclude: Option>, /// Like --exclude, but adds additional files and directories on top of those already excluded. #[arg( long, @@ -582,17 +582,17 @@ pub struct FormatCommand { no_force_exclude: bool, /// Set the line-length. #[arg(long, help_heading = "Format configuration")] - pub line_length: Option, + line_length: Option, /// The name of the file when passing it through stdin. #[arg(long, help_heading = "Miscellaneous")] - pub stdin_filename: Option, + stdin_filename: Option, /// List of mappings from file extension to language (one of `python`, `ipynb`, `pyi`). For /// example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`. #[arg(long, value_delimiter = ',')] - pub extension: Option>, + extension: Option>, /// The minimum Python version that should be supported. #[arg(long, value_enum)] - pub target_version: Option, + target_version: Option, /// Enable preview mode; enables unstable formatting. /// Use `--no-preview` to disable. #[arg(long, overrides_with("no_preview"))] @@ -613,16 +613,16 @@ pub struct FormatCommand { /// /// The option can only be used when formatting a single file. Range formatting of notebooks is unsupported. #[clap(long, help_heading = "Editor options", verbatim_doc_comment)] - pub range: Option, + range: Option, /// Exit with a non-zero status code if any files were modified via format, even if all files were formatted successfully. #[arg(long, help_heading = "Miscellaneous", alias = "exit-non-zero-on-fix")] - pub exit_non_zero_on_format: bool, + exit_non_zero_on_format: bool, /// Output serialization format for violations, when used with `--check`. /// The default serialization format is "full". #[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")] - pub output_format: Option, + output_format: Option, } #[derive(Copy, Clone, Debug, clap::Parser)] @@ -660,7 +660,7 @@ pub struct LogLevelArgs { group = "verbosity", help_heading = "Log levels" )] - pub verbose: bool, + verbose: bool, /// Print diagnostics, but nothing else. #[arg( short, @@ -669,7 +669,7 @@ pub struct LogLevelArgs { group = "verbosity", help_heading = "Log levels" )] - pub quiet: bool, + quiet: bool, /// Disable all logging (but still exit with status code "1" upon detecting diagnostics). #[arg( short, @@ -678,7 +678,7 @@ pub struct LogLevelArgs { group = "verbosity", help_heading = "Log levels" )] - pub silent: bool, + silent: bool, } impl From<&LogLevelArgs> for LogLevel { @@ -717,7 +717,7 @@ pub struct ConfigArguments { } impl ConfigArguments { - pub fn config_file(&self) -> Option<&Path> { + pub(crate) fn config_file(&self) -> Option<&Path> { self.config_file.as_deref() } @@ -788,7 +788,7 @@ impl ConfigurationTransformer for ConfigArguments { impl CheckCommand { /// Partition the CLI into command-line arguments and configuration /// overrides. - pub fn partition( + pub(crate) fn partition( self, global_options: GlobalConfigArgs, ) -> anyhow::Result<(CheckArguments, ConfigArguments)> { @@ -884,7 +884,7 @@ impl FormatCommand { impl AnalyzeGraphCommand { /// Partition the CLI into command-line arguments and configuration /// overrides. - pub fn partition( + pub(crate) fn partition( self, global_options: GlobalConfigArgs, ) -> anyhow::Result<(AnalyzeGraphArgs, ConfigArguments)> { @@ -1131,21 +1131,21 @@ Possible choices: /// CLI settings that are distinct from configuration (commands, lists of files, /// etc.). #[expect(clippy::struct_excessive_bools)] -pub struct CheckArguments { - pub add_noqa: Option, - pub add_ignore: Option, - pub diff: bool, - pub exit_non_zero_on_fix: bool, - pub exit_zero: bool, - pub files: Vec, - pub ignore_noqa: bool, - pub no_cache: bool, - pub output_file: Option, - pub show_files: bool, - pub show_settings: bool, - pub statistics: bool, - pub stdin_filename: Option, - pub watch: bool, +pub(crate) struct CheckArguments { + pub(crate) add_noqa: Option, + pub(crate) add_ignore: Option, + pub(crate) diff: bool, + pub(crate) exit_non_zero_on_fix: bool, + pub(crate) exit_zero: bool, + pub(crate) files: Vec, + pub(crate) ignore_noqa: bool, + pub(crate) no_cache: bool, + pub(crate) output_file: Option, + pub(crate) show_files: bool, + pub(crate) show_settings: bool, + pub(crate) statistics: bool, + pub(crate) stdin_filename: Option, + pub(crate) watch: bool, } /// CLI settings that are distinct from configuration (commands, lists of files, @@ -1242,8 +1242,8 @@ impl std::error::Error for FormatRangeParseError {} #[derive(Copy, Clone, Debug)] pub struct LineColumn { - pub line: OneIndexed, - pub column: OneIndexed, + line: OneIndexed, + column: OneIndexed, } impl From for ruff_source_file::SourceLocation { @@ -1369,9 +1369,9 @@ impl LineColumnParseError { /// CLI settings that are distinct from configuration (commands, lists of files, etc.). #[derive(Clone, Debug)] pub struct AnalyzeGraphArgs { - pub files: Vec, - pub direction: Direction, - pub python: Option, + pub(crate) files: Vec, + pub(crate) direction: Direction, + pub(crate) python: Option, } /// Configuration overrides provided via dedicated CLI flags: @@ -1506,7 +1506,7 @@ impl ConfigurationTransformer for ExplicitConfigOverrides { } /// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`. -pub fn collect_per_file_ignores(pairs: Vec) -> Vec { +fn collect_per_file_ignores(pairs: Vec) -> Vec { let mut per_file_ignores: FxHashMap> = FxHashMap::default(); for pair in pairs { per_file_ignores diff --git a/crates/ruff/src/cache.rs b/crates/ruff/src/cache.rs index 33e3721df7..d19bbb5d4c 100644 --- a/crates/ruff/src/cache.rs +++ b/crates/ruff/src/cache.rs @@ -91,7 +91,7 @@ impl Cache { /// /// Finally `settings` is used to ensure we don't open a cache for different /// settings. It also defines the directory where to store the cache. - pub(crate) fn open(package_root: PathBuf, settings: &Settings) -> Self { + fn open(package_root: PathBuf, settings: &Settings) -> Self { debug_assert!(package_root.is_absolute(), "package root not canonicalized"); let key = format!("{}", cache_key(&package_root, settings)); @@ -154,7 +154,7 @@ impl Cache { } /// Applies the pending changes and persists the cache to disk, if it has been changed. - pub(crate) fn persist(mut self) -> Result<()> { + fn persist(mut self) -> Result<()> { if !self.save() { // No changes made, no need to write the same cache file back to // disk. @@ -199,7 +199,7 @@ impl Cache { /// Applies the pending changes without storing the cache to disk. #[expect(clippy::cast_possible_truncation)] - pub(crate) fn save(&mut self) -> bool { + 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_hours(720); // 30 days. @@ -371,7 +371,7 @@ fn cache_key(package_root: &Path, settings: &Settings) -> u64 { } /// Initialize the cache at the specified `Path`. -pub(crate) fn init(path: &Path) -> Result<()> { +fn init(path: &Path) -> Result<()> { // Create the cache directories. fs::create_dir_all(path.join(VERSION))?; diff --git a/crates/ruff/src/diagnostics.rs b/crates/ruff/src/diagnostics.rs index 634ad80b1c..f892eca98f 100644 --- a/crates/ruff/src/diagnostics.rs +++ b/crates/ruff/src/diagnostics.rs @@ -54,7 +54,7 @@ impl Diagnostics { } /// Generate [`Diagnostics`] based on a [`SourceError`]. - pub(crate) fn from_source_error( + fn from_source_error( err: &SourceError, path: Option<&Path>, settings: &LinterSettings, diff --git a/crates/ruff/src/main.rs b/crates/ruff/src/main.rs index 4342a360e6..af8cb8a9d6 100644 --- a/crates/ruff/src/main.rs +++ b/crates/ruff/src/main.rs @@ -27,7 +27,7 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -pub fn main() -> ExitCode { +fn main() -> ExitCode { // Enabled ANSI colors on Windows 10. #[cfg(windows)] assert!(colored::control::set_virtual_terminal(true).is_ok()); diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index 03d9319a2c..cbac14ec6d 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -312,7 +312,7 @@ impl Diagnostic { } /// Returns a reference to the primary span of this diagnostic. - pub fn primary_span_ref(&self) -> Option<&Span> { + fn primary_span_ref(&self) -> Option<&Span> { self.primary_annotation().map(|ann| &ann.span) } @@ -360,7 +360,7 @@ impl Diagnostic { } #[cfg(test)] - pub(crate) fn fix_mut(&mut self) -> Option<&mut Fix> { + fn fix_mut(&mut self) -> Option<&mut Fix> { Arc::make_mut(&mut self.inner).fix.as_mut() } @@ -419,7 +419,7 @@ impl Diagnostic { /// Returns the remapped offset for a suppression comment if it exists. /// /// Like [`Diagnostic::parent`], this is used for noqa code suppression comments in Ruff. - pub fn noqa_offset(&self) -> Option { + fn noqa_offset(&self) -> Option { self.inner.noqa_offset } @@ -514,7 +514,7 @@ impl Diagnostic { /// Returns the [`SourceFile`] which the message belongs to. /// /// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`. - pub fn expect_ruff_source_file(&self) -> &SourceFile { + fn expect_ruff_source_file(&self) -> &SourceFile { self.ruff_source_file() .expect("Expected a ruff source file") } @@ -1165,7 +1165,7 @@ impl DiagnosticId { } } - pub fn is_invalid_syntax(&self) -> bool { + fn is_invalid_syntax(&self) -> bool { matches!(self, Self::InvalidSyntax) } } @@ -1192,7 +1192,7 @@ pub enum UnifiedFile { } impl UnifiedFile { - pub fn path<'a>(&'a self, resolver: &'a dyn FileResolver) -> &'a str { + fn path<'a>(&'a self, resolver: &'a dyn FileResolver) -> &'a str { match self { UnifiedFile::Ty(file) => resolver.path(*file), UnifiedFile::Ruff(file) => file.name(), @@ -1200,7 +1200,7 @@ impl UnifiedFile { } /// Return the file's path relative to the current working directory. - pub fn relative_path<'a>(&'a self, resolver: &'a dyn FileResolver) -> &'a Path { + fn relative_path<'a>(&'a self, resolver: &'a dyn FileResolver) -> &'a Path { let cwd = resolver.current_directory(); let path = Path::new(self.path(resolver)); @@ -1293,7 +1293,7 @@ impl Span { /// Returns the [`SourceFile`] attached to this [`Span`]. /// /// Panics if the file is a [`UnifiedFile::Ty`] instead of a [`UnifiedFile::Ruff`]. - pub fn expect_ruff_file(&self) -> &SourceFile { + fn expect_ruff_file(&self) -> &SourceFile { self.as_ruff_file() .expect("Expected a ruff `SourceFile`, found a ty `File`") } @@ -1596,7 +1596,7 @@ impl DisplayDiagnosticConfig { self } - pub fn is_canceled(&self) -> bool { + fn is_canceled(&self) -> bool { self.cancellation_token .as_ref() .is_some_and(|token| token.is_cancelled()) diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index e40ca5594d..4cb86bf5b6 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -2639,12 +2639,7 @@ watermelon /// of the corresponding line minus one. (The "minus one" is because /// otherwise, the span will end where the next line begins, and this /// confuses `ruff_annotate_snippets` as of 2025-03-13.) - pub(super) fn span( - &self, - path: &str, - line_offset_start: &str, - line_offset_end: &str, - ) -> Span { + fn span(&self, path: &str, line_offset_start: &str, line_offset_end: &str) -> Span { let span = self.path(path); let file = span.expect_ty_file(); @@ -2805,7 +2800,7 @@ watermelon } /// Set the fix on the diagnostic. - pub(super) fn fix(mut self, fix: Fix) -> DiagnosticBuilder<'e> { + fn fix(mut self, fix: Fix) -> DiagnosticBuilder<'e> { self.diag.set_fix(fix); self } diff --git a/crates/ruff_db/src/file_revision.rs b/crates/ruff_db/src/file_revision.rs index 1f320ec7ae..826e3b1a6c 100644 --- a/crates/ruff_db/src/file_revision.rs +++ b/crates/ruff_db/src/file_revision.rs @@ -21,12 +21,12 @@ impl FileRevision { Self::from(file_time_now()) } - pub const fn zero() -> Self { + pub(crate) const fn zero() -> Self { Self(0) } #[must_use] - pub fn as_u128(self) -> u128 { + pub(crate) fn as_u128(self) -> u128 { self.0 } } diff --git a/crates/ruff_db/src/files.rs b/crates/ruff_db/src/files.rs index 92f46b8452..8f3b5cd39e 100644 --- a/crates/ruff_db/src/files.rs +++ b/crates/ruff_db/src/files.rs @@ -4,9 +4,9 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use dashmap::mapref::entry::Entry; -pub use directory::{ - DirectoryListing, DirectoryListingError, directory_listing, system_path_to_directory, -}; +#[expect(unused_imports)] +pub(crate) use directory::system_path_to_directory; +pub use directory::{DirectoryListing, DirectoryListingError, directory_listing}; pub use file_root::{FileRoot, FileRootKind}; pub use path::FilePath; use ruff_notebook::{Notebook, NotebookError}; @@ -428,7 +428,7 @@ impl File { /// /// Reading the same file multiple times isn't guaranteed to return the same content. It's possible /// that the file has been modified in between the reads. - pub fn read_to_notebook(&self, db: &dyn Db) -> Result { + pub(crate) fn read_to_notebook(&self, db: &dyn Db) -> Result { let path = self.path(db); match path { diff --git a/crates/ruff_db/src/files/directory.rs b/crates/ruff_db/src/files/directory.rs index cd41319c33..ba2f445a6d 100644 --- a/crates/ruff_db/src/files/directory.rs +++ b/crates/ruff_db/src/files/directory.rs @@ -13,7 +13,7 @@ pub struct DirectoryListing(Box<[(CompactString, FileType)]>); impl DirectoryListing { /// Returns the type of the entry named `name`, if present. - pub fn file_type(&self, name: &str) -> Option { + fn file_type(&self, name: &str) -> Option { self.0 .binary_search_by(|(candidate, _)| candidate.as_str().cmp(name)) .ok() diff --git a/crates/ruff_db/src/files/file_root.rs b/crates/ruff_db/src/files/file_root.rs index db54fde524..57520d0ebb 100644 --- a/crates/ruff_db/src/files/file_root.rs +++ b/crates/ruff_db/src/files/file_root.rs @@ -23,7 +23,7 @@ pub struct FileRoot { } impl FileRoot { - pub fn durability(self, db: &dyn Db) -> salsa::Durability { + pub(crate) fn durability(self, db: &dyn Db) -> salsa::Durability { self.kind_at_time_of_creation(db).durability() } } diff --git a/crates/ruff_db/src/parsed.rs b/crates/ruff_db/src/parsed.rs index 9e1b2b62d8..12899b6867 100644 --- a/crates/ruff_db/src/parsed.rs +++ b/crates/ruff_db/src/parsed.rs @@ -44,7 +44,7 @@ pub(super) fn disable_lru(db: &mut dyn Db) { parsed_module::set_lru_capacity(db, 0); } -pub fn parsed_module_impl(db: &dyn Db, file: File) -> Parsed { +fn parsed_module_impl(db: &dyn Db, file: File) -> Parsed { let source = source_text(db, file); let ty = file.source_type(db); @@ -115,7 +115,7 @@ pub struct ParsedModule { } impl ParsedModule { - pub fn new(file: File, parsed: Parsed) -> Self { + fn new(file: File, parsed: Parsed) -> Self { Self { file, inner: Arc::new(ArcSwapOption::new(Some(indexed::IndexedModule::new( diff --git a/crates/ruff_db/src/system/test.rs b/crates/ruff_db/src/system/test.rs index 09071d7d7f..bfea3a1276 100644 --- a/crates/ruff_db/src/system/test.rs +++ b/crates/ruff_db/src/system/test.rs @@ -69,7 +69,7 @@ impl TestSystem { } /// Returns the `InMemorySystem` or `None` if the underlying test system isn't the [`InMemorySystem`]. - pub fn as_in_memory(&self) -> Option<&InMemorySystem> { + fn as_in_memory(&self) -> Option<&InMemorySystem> { self.system().as_any().downcast_ref::() } @@ -88,7 +88,7 @@ impl TestSystem { self.inner = Arc::new(system); } - pub fn system(&self) -> &dyn WritableSystem { + fn system(&self) -> &dyn WritableSystem { &*self.inner } } diff --git a/crates/ruff_diagnostics/src/edit.rs b/crates/ruff_diagnostics/src/edit.rs index ae52088608..06dfc69454 100644 --- a/crates/ruff_diagnostics/src/edit.rs +++ b/crates/ruff_diagnostics/src/edit.rs @@ -77,20 +77,22 @@ impl Edit { } /// Returns `true` if this edit deletes content from the source document. + #[expect(dead_code)] #[inline] - pub fn is_deletion(&self) -> bool { + pub(crate) fn is_deletion(&self) -> bool { self.kind().is_deletion() } /// Returns `true` if this edit inserts new content into the source document. #[inline] - pub fn is_insertion(&self) -> bool { + pub(crate) fn is_insertion(&self) -> bool { self.kind().is_insertion() } /// Returns `true` if this edit replaces some existing content with new content. + #[expect(dead_code)] #[inline] - pub fn is_replacement(&self) -> bool { + pub(crate) fn is_replacement(&self) -> bool { self.kind().is_replacement() } } diff --git a/crates/ruff_formatter/src/buffer.rs b/crates/ruff_formatter/src/buffer.rs index 13bfa7ce13..bcc9034145 100644 --- a/crates/ruff_formatter/src/buffer.rs +++ b/crates/ruff_formatter/src/buffer.rs @@ -92,7 +92,7 @@ pub enum BufferSnapshot { impl BufferSnapshot { /// Creates a new buffer snapshot that points to the specified position. - pub const fn position(index: usize) -> Self { + const fn position(index: usize) -> Self { Self::Position(index) } @@ -101,7 +101,7 @@ impl BufferSnapshot { /// # Panics /// /// If self is not a [`BufferSnapshot::Position`] - pub fn unwrap_position(&self) -> usize { + fn unwrap_position(&self) -> usize { match self { BufferSnapshot::Position(index) => *index, BufferSnapshot::Any(_) => panic!("Tried to unwrap Any snapshot as a position."), @@ -113,7 +113,7 @@ impl BufferSnapshot { /// # Panics /// /// If `self` is not a [`BufferSnapshot::Any`]. - pub fn unwrap_any(self) -> T { + fn unwrap_any(self) -> T { match self { BufferSnapshot::Position(_) => { panic!("Tried to unwrap Position snapshot as Any snapshot.") @@ -179,12 +179,12 @@ impl<'a, Context> VecBuffer<'a, Context> { Self::new_with_vec(state, Vec::new()) } - pub fn new_with_vec(state: &'a mut FormatState, elements: Vec) -> Self { + fn new_with_vec(state: &'a mut FormatState, elements: Vec) -> Self { Self { state, elements } } /// Creates a buffer with the specified capacity - pub fn with_capacity(capacity: usize, state: &'a mut FormatState) -> Self { + pub(crate) fn with_capacity(capacity: usize, state: &'a mut FormatState) -> Self { Self { state, elements: Vec::with_capacity(capacity), diff --git a/crates/ruff_formatter/src/format_element.rs b/crates/ruff_formatter/src/format_element.rs index 37388838fb..719e62ca30 100644 --- a/crates/ruff_formatter/src/format_element.rs +++ b/crates/ruff_formatter/src/format_element.rs @@ -69,7 +69,7 @@ pub enum FormatElement { } impl FormatElement { - pub fn tag_kind(&self) -> Option { + pub(crate) fn tag_kind(&self) -> Option { if let FormatElement::Tag(tag) = self { Some(tag.kind()) } else { @@ -229,7 +229,7 @@ impl FormatElement { matches!(self, FormatElement::Tag(_)) } - /// Returns `true` if self is a [`FormatElement::Tag`] and [`Tag::is_start`] is `true`. + /// Returns `true` if self is a [`FormatElement::Tag`] and `Tag::is_start` is `true`. pub const fn is_start_tag(&self) -> bool { match self { FormatElement::Tag(tag) => tag.is_start(), @@ -238,14 +238,14 @@ impl FormatElement { } /// Returns `true` if self is a [`FormatElement::Tag`] and [`Tag::is_end`] is `true`. - pub const fn is_end_tag(&self) -> bool { + const fn is_end_tag(&self) -> bool { match self { FormatElement::Tag(tag) => tag.is_end(), _ => false, } } - pub const fn is_text(&self) -> bool { + const fn is_text(&self) -> bool { matches!( self, FormatElement::SourceCodeSlice { .. } @@ -254,7 +254,7 @@ impl FormatElement { ) } - pub const fn is_space(&self) -> bool { + const fn is_space(&self) -> bool { matches!(self, FormatElement::Space) } } @@ -339,7 +339,7 @@ impl BestFittingVariants { /// /// You're looking for a way to create a `BestFitting` object, use the `best_fitting![least_expanded, most_expanded]` macro. #[doc(hidden)] - pub fn from_vec_unchecked(variants: Vec) -> Self { + pub(crate) fn from_vec_unchecked(variants: Vec) -> Self { debug_assert!( variants .iter() @@ -368,7 +368,7 @@ impl BestFittingVariants { self.into_iter().last().unwrap() } - pub fn as_slice(&self) -> &[FormatElement] { + fn as_slice(&self) -> &[FormatElement] { &self.0 } @@ -377,7 +377,7 @@ impl BestFittingVariants { /// # Panics /// /// When the number of variants is less than two. - pub fn most_flat(&self) -> &[FormatElement] { + pub(crate) fn most_flat(&self) -> &[FormatElement] { assert!( self.as_slice() .iter() @@ -494,7 +494,7 @@ pub trait FormatElements { pub struct Width(NonZeroU32); impl Width { - pub(crate) const fn new(width: u32) -> Self { + const fn new(width: u32) -> Self { Width(NonZeroU32::MIN.saturating_add(width)) } @@ -553,7 +553,7 @@ impl TextWidth { } } - pub(crate) const fn is_multiline(self) -> bool { + const fn is_multiline(self) -> bool { matches!(self, TextWidth::Multiline) } } diff --git a/crates/ruff_formatter/src/format_element/tag.rs b/crates/ruff_formatter/src/format_element/tag.rs index 6069226482..897e74c191 100644 --- a/crates/ruff_formatter/src/format_element/tag.rs +++ b/crates/ruff_formatter/src/format_element/tag.rs @@ -100,7 +100,7 @@ pub enum Tag { impl Tag { /// Returns `true` if `self` is any start tag. - pub const fn is_start(&self) -> bool { + pub(crate) const fn is_start(&self) -> bool { matches!( self, Tag::StartIndent @@ -122,11 +122,11 @@ impl Tag { } /// Returns `true` if `self` is any end tag. - pub const fn is_end(&self) -> bool { + pub(crate) const fn is_end(&self) -> bool { !self.is_start() } - pub const fn kind(&self) -> TagKind { + pub(crate) const fn kind(&self) -> TagKind { #[allow(clippy::enum_glob_use)] use Tag::*; @@ -201,17 +201,17 @@ pub struct FitsExpanded { } impl FitsExpanded { - pub fn new() -> Self { + pub(crate) fn new() -> Self { Self::default() } #[must_use] - pub fn with_condition(mut self, condition: Option) -> Self { + pub(crate) fn with_condition(mut self, condition: Option) -> Self { self.condition = condition; self } - pub fn propagate_expand(&self) { + pub(crate) fn propagate_expand(&self) { self.propagate_expand.set(true); } } @@ -231,28 +231,28 @@ impl Group { } #[must_use] - pub fn with_id(mut self, id: Option) -> Self { + pub(crate) fn with_id(mut self, id: Option) -> Self { self.id = id; self } #[must_use] - pub fn with_mode(mut self, mode: GroupMode) -> Self { + pub(crate) fn with_mode(mut self, mode: GroupMode) -> Self { self.mode = Cell::new(mode); self } - pub fn mode(&self) -> GroupMode { + pub(crate) fn mode(&self) -> GroupMode { self.mode.get() } - pub fn propagate_expand(&self) { + pub(crate) fn propagate_expand(&self) { if self.mode.get() == GroupMode::Flat { self.mode.set(GroupMode::Propagated); } } - pub fn id(&self) -> Option { + pub(crate) fn id(&self) -> Option { self.id } } @@ -271,15 +271,15 @@ impl ConditionalGroup { } } - pub fn condition(&self) -> Condition { + pub(crate) fn condition(&self) -> Condition { self.condition } - pub fn propagate_expand(&self) { + pub(crate) fn propagate_expand(&self) { self.mode.set(GroupMode::Propagated); } - pub fn mode(&self) -> GroupMode { + pub(crate) fn mode(&self) -> GroupMode { self.mode.get() } } @@ -341,7 +341,7 @@ impl Condition { } #[must_use] - pub fn with_group_id(mut self, id: Option) -> Self { + pub(crate) fn with_group_id(mut self, id: Option) -> Self { self.group_id = id; self } @@ -351,7 +351,7 @@ impl Condition { pub struct Align(pub(crate) NonZeroU8); impl Align { - pub fn count(&self) -> NonZeroU8 { + pub(crate) fn count(&self) -> NonZeroU8 { self.0 } } diff --git a/crates/ruff_formatter/src/formatter.rs b/crates/ruff_formatter/src/formatter.rs index 8274485bc5..3656f9747c 100644 --- a/crates/ruff_formatter/src/formatter.rs +++ b/crates/ruff_formatter/src/formatter.rs @@ -12,7 +12,7 @@ pub struct Formatter<'buf, Context> { impl<'buf, Context> Formatter<'buf, Context> { /// Creates a new context that uses the given formatter context - pub fn new(buffer: &'buf mut (dyn Buffer + 'buf)) -> Self { + pub(crate) fn new(buffer: &'buf mut (dyn Buffer + 'buf)) -> Self { Self { buffer } } @@ -164,7 +164,10 @@ impl<'buf, Context> Formatter<'buf, Context> { } /// Formats `content` into an interned element without writing it to the formatter's buffer. - pub fn intern(&mut self, content: &dyn Format) -> FormatResult> { + pub(crate) fn intern( + &mut self, + content: &dyn Format, + ) -> FormatResult> { let mut buffer = VecBuffer::new(self.state_mut()); crate::write!(&mut buffer, [content])?; let elements = buffer.into_vec(); diff --git a/crates/ruff_formatter/src/lib.rs b/crates/ruff_formatter/src/lib.rs index d765af4704..319c994ce4 100644 --- a/crates/ruff_formatter/src/lib.rs +++ b/crates/ruff_formatter/src/lib.rs @@ -50,7 +50,9 @@ pub use builders::BestFitting; pub use source_code::{SourceCode, SourceCodeSlice}; pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, PrintError}; -pub use format_element::{FormatElement, LINE_TERMINATORS, normalize_newlines}; +#[expect(unused_imports)] +pub(crate) use format_element::LINE_TERMINATORS; +pub use format_element::{FormatElement, normalize_newlines}; pub use group_id::GroupId; use ruff_macros::CacheKey; use ruff_text_size::{TextLen, TextRange, TextSize}; @@ -361,7 +363,7 @@ pub struct Printed { } impl Printed { - pub fn new( + fn new( code: String, range: Option, sourcemap: Vec, @@ -951,12 +953,12 @@ impl FormatState { } } - pub fn into_context(self) -> Context { + fn into_context(self) -> Context { self.context } /// Returns the context specifying how to format the current CST - pub fn context(&self) -> &Context { + fn context(&self) -> &Context { &self.context } @@ -968,7 +970,7 @@ impl FormatState { /// Creates a new group id that is unique to this document. The passed debug name is used in the /// [`std::fmt::Debug`] of the document if this is a debug build. /// The name is unused for production builds and has no meaning on the equality of two group ids. - pub fn group_id(&self, debug_name: &'static str) -> GroupId { + fn group_id(&self, debug_name: &'static str) -> GroupId { self.group_id_builder.group_id(debug_name) } } diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index 19ba2b84fd..9f59c3b5be 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -39,7 +39,7 @@ pub struct Printer<'a> { } impl<'a> Printer<'a> { - pub fn new(source_code: SourceCode<'a>, options: PrinterOptions) -> Self { + pub(crate) fn new(source_code: SourceCode<'a>, options: PrinterOptions) -> Self { Self { source_code, options, @@ -48,14 +48,14 @@ impl<'a> Printer<'a> { } /// Prints the passed in element as well as all its content - pub fn print(self, document: &'a Document) -> PrintResult { + pub(crate) fn print(self, document: &'a Document) -> PrintResult { self.print_with_indent(document, 0) } /// Prints the passed in element as well as all its content, /// starting at the specified indentation level #[tracing::instrument(level = "debug", name = "Printer::print", skip_all)] - pub fn print_with_indent( + pub(crate) fn print_with_indent( mut self, document: &'a Document, indent: u16, diff --git a/crates/ruff_formatter/src/printer/stack.rs b/crates/ruff_formatter/src/printer/stack.rs index 69b6ab9b79..faa4a8fc7e 100644 --- a/crates/ruff_formatter/src/printer/stack.rs +++ b/crates/ruff_formatter/src/printer/stack.rs @@ -36,7 +36,7 @@ pub(super) struct StackedStack<'a, T> { impl<'a, T> StackedStack<'a, T> { #[cfg(test)] - pub(super) fn new(original: &'a [T]) -> Self { + fn new(original: &'a [T]) -> Self { Self::with_vec(original, Vec::new()) } diff --git a/crates/ruff_graph/src/lib.rs b/crates/ruff_graph/src/lib.rs index 0ada26454f..5f09b7f6f2 100644 --- a/crates/ruff_graph/src/lib.rs +++ b/crates/ruff_graph/src/lib.rs @@ -60,7 +60,7 @@ impl ModuleImports { } /// Insert a file path into the module imports. - pub fn insert(&mut self, path: SystemPathBuf) { + fn insert(&mut self, path: SystemPathBuf) { self.0.insert(path); } diff --git a/crates/ruff_graph/src/resolver.rs b/crates/ruff_graph/src/resolver.rs index a607f9e354..a7d357eb71 100644 --- a/crates/ruff_graph/src/resolver.rs +++ b/crates/ruff_graph/src/resolver.rs @@ -99,7 +99,7 @@ impl<'a> Resolver<'a> { } /// Resolves a module name to a module. - pub(crate) fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> { + fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> { let module = if let Some(file) = self.file { resolve_module(self.db, file, module_name)? } else { diff --git a/crates/ruff_index/src/frozen.rs b/crates/ruff_index/src/frozen.rs index 1a88b472a6..7fb455820d 100644 --- a/crates/ruff_index/src/frozen.rs +++ b/crates/ruff_index/src/frozen.rs @@ -12,7 +12,7 @@ pub struct FrozenIndexVec { impl FrozenIndexVec { #[inline] - pub fn from_raw(raw: Box<[T]>) -> Self { + fn from_raw(raw: Box<[T]>) -> Self { Self { raw, index: PhantomData, @@ -20,12 +20,12 @@ impl FrozenIndexVec { } #[inline] - pub fn as_slice(&self) -> &IndexSlice { + fn as_slice(&self) -> &IndexSlice { IndexSlice::from_raw(&self.raw) } #[inline] - pub fn as_mut_slice(&mut self) -> &mut IndexSlice { + fn as_mut_slice(&mut self) -> &mut IndexSlice { IndexSlice::from_raw_mut(&mut self.raw) } } diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 59a5a53530..029ac85d08 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -253,7 +253,7 @@ pub(crate) struct Checker<'a> { impl<'a> Checker<'a> { #[expect(clippy::too_many_arguments)] - pub(crate) fn new( + fn new( parsed: &'a Parsed, parsed_annotations_arena: &'a typed_arena::Arena>, settings: &'a LinterSettings, @@ -605,7 +605,7 @@ impl<'a> Checker<'a> { } /// Push `diagnostic` if the checker is not in a `@no_type_check` context. - pub(crate) fn report_type_diagnostic(&self, kind: T, range: TextRange) { + fn report_type_diagnostic(&self, kind: T, range: TextRange) { if !self.semantic.in_no_type_check() { self.report_diagnostic(kind, range); } @@ -3527,7 +3527,7 @@ impl<'a> LintContext<'a> { /// Prefer [`LintContext::report_diagnostic_if_enabled`] unless you need to attach /// sub-diagnostics before the fix title. See its documentation for more details. #[expect(unused)] - pub(crate) fn report_custom_diagnostic_if_enabled<'chk, T: Violation>( + fn report_custom_diagnostic_if_enabled<'chk, T: Violation>( &'chk self, kind: T, range: TextRange, @@ -3697,7 +3697,7 @@ impl DiagnosticGuard<'_, '_> { /// diagnostic.info("This will appear first"); /// diagnostic.before_drop(|diag| diag.info("This will appear last, after the fix title")); /// ``` - pub(crate) fn before_drop(&mut self, f: F) + fn before_drop(&mut self, f: F) where F: Fn(&mut Diagnostic) + 'static, { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 645b9266a2..106382d833 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -16,12 +16,13 @@ pub struct NoqaCode(&'static str, &'static str); impl NoqaCode { /// Return the prefix for the [`NoqaCode`], e.g., `SIM` for `SIM101`. - pub fn prefix(&self) -> &str { + #[expect(dead_code)] + pub(crate) fn prefix(&self) -> &str { self.0 } /// Return the suffix for the [`NoqaCode`], e.g., `101` for `SIM101`. - pub fn suffix(&self) -> &str { + pub(crate) fn suffix(&self) -> &str { self.1 } diff --git a/crates/ruff_linter/src/directives.rs b/crates/ruff_linter/src/directives.rs index 25a8ddcabf..ad9dbbed66 100644 --- a/crates/ruff_linter/src/directives.rs +++ b/crates/ruff_linter/src/directives.rs @@ -41,10 +41,10 @@ impl Flags { #[derive(Default, Debug)] pub struct IsortDirectives { /// Ranges for which sorting is disabled - pub exclusions: Vec, + pub(crate) exclusions: Vec, /// Text positions at which splits should be inserted - pub splits: Vec, - pub skip_file: bool, + pub(crate) splits: Vec, + pub(crate) skip_file: bool, } pub struct Directives { diff --git a/crates/ruff_linter/src/docstrings/sections.rs b/crates/ruff_linter/src/docstrings/sections.rs index 85c4474106..fb73383a78 100644 --- a/crates/ruff_linter/src/docstrings/sections.rs +++ b/crates/ruff_linter/src/docstrings/sections.rs @@ -52,7 +52,7 @@ pub(crate) enum SectionKind { } impl SectionKind { - pub(crate) fn from_str(s: &str) -> Option { + fn from_str(s: &str) -> Option { match s.to_ascii_lowercase().as_str() { "args" => Some(Self::Args), "arguments" => Some(Self::Arguments), diff --git a/crates/ruff_linter/src/fs.rs b/crates/ruff_linter/src/fs.rs index a16ad5dcfd..917f052299 100644 --- a/crates/ruff_linter/src/fs.rs +++ b/crates/ruff_linter/src/fs.rs @@ -8,7 +8,7 @@ use crate::settings::types::CompiledPerFileIgnoreList; /// Return the current working directory. /// /// On WASM this just returns `.`. Otherwise, defer to [`path_absolutize::path_dedot::CWD`]. -pub fn get_cwd() -> &'static Path { +pub(crate) fn get_cwd() -> &'static Path { cfg_select! { target_arch = "wasm32" => Path::new("."), _ => path_absolutize::path_dedot::CWD.as_path(), diff --git a/crates/ruff_linter/src/line_width.rs b/crates/ruff_linter/src/line_width.rs index 0c6eb95f70..911c43051e 100644 --- a/crates/ruff_linter/src/line_width.rs +++ b/crates/ruff_linter/src/line_width.rs @@ -21,7 +21,7 @@ pub struct LineLength(NonZeroU16); impl LineLength { /// Maximum allowed value for a valid [`LineLength`] - pub const MAX: u16 = u16::MAX; + const MAX: u16 = u16::MAX; /// Return the numeric value for this [`LineLength`] pub fn value(&self) -> u16 { @@ -184,12 +184,12 @@ impl Ord for LineWidthBuilder { } impl LineWidthBuilder { - pub fn get(&self) -> usize { + pub(crate) fn get(&self) -> usize { self.width } /// Creates a new `LineWidth` with the given tab size. - pub fn new(tab_size: IndentWidth) -> Self { + pub(crate) fn new(tab_size: IndentWidth) -> Self { LineWidthBuilder { width: 0, column: 0, @@ -221,13 +221,13 @@ impl LineWidthBuilder { /// Adds the given text to the line width. #[must_use] - pub fn add_str(self, text: &str) -> Self { + pub(crate) fn add_str(self, text: &str) -> Self { self.update(text.chars()) } /// Adds the given character to the line width. #[must_use] - pub fn add_char(self, c: char) -> Self { + pub(crate) fn add_char(self, c: char) -> Self { self.update(std::iter::once(c)) } @@ -237,7 +237,7 @@ impl LineWidthBuilder { /// The width and column should be the same for the corresponding text. /// Currently, this is only used to add spaces. #[must_use] - pub fn add_width(mut self, width: usize) -> Self { + pub(crate) fn add_width(mut self, width: usize) -> Self { self.width += width; self.column += width; self diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 3fd556aa02..d1491ad0b2 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -80,7 +80,7 @@ impl FixTable { .map(|(code, FixCount { rule_name, count })| (code, *rule_name, *count)) } - pub fn keys(&self) -> impl Iterator { + fn keys(&self) -> impl Iterator { self.0.keys() } diff --git a/crates/ruff_linter/src/locator.rs b/crates/ruff_linter/src/locator.rs index 87afaae8bf..fe9388e473 100644 --- a/crates/ruff_linter/src/locator.rs +++ b/crates/ruff_linter/src/locator.rs @@ -29,14 +29,14 @@ 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_line_index(&self, offset: TextSize) -> OneIndexed { + pub(crate) fn compute_line_index(&self, offset: TextSize) -> OneIndexed { self.to_index().line_index(offset) } #[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) -> LineColumn { + pub(crate) fn compute_source_location(&self, offset: TextSize) -> LineColumn { self.to_source_code().line_column(offset) } @@ -55,13 +55,13 @@ impl<'a> Locator<'a> { /// Take the source code up to the given [`TextSize`]. #[inline] - pub fn up_to(&self, offset: TextSize) -> &'a str { + pub(crate) fn up_to(&self, offset: TextSize) -> &'a str { &self.contents[TextRange::up_to(offset)] } /// Take the source code after the given [`TextSize`]. #[inline] - pub fn after(&self, offset: TextSize) -> &'a str { + pub(crate) fn after(&self, offset: TextSize) -> &'a str { &self.contents[usize::from(offset)..] } @@ -109,7 +109,7 @@ impl<'a> Locator<'a> { /// Take the source code between the given [`TextRange`]. #[inline] - pub fn slice(&self, ranged: T) -> &'a str { + pub(crate) fn slice(&self, ranged: T) -> &'a str { &self.contents[ranged.range()] } @@ -119,11 +119,11 @@ impl<'a> Locator<'a> { } /// Return the number of bytes in the source code. - pub const fn len(&self) -> usize { + pub(crate) const fn len(&self) -> usize { self.contents.len() } - pub fn text_len(&self) -> TextSize { + pub(crate) fn text_len(&self) -> TextSize { self.contents.text_len() } @@ -138,21 +138,21 @@ impl<'a> Locator<'a> { /// Returns the text of the `offset`'s line. /// /// See [`LineRanges::full_lines_str`]. - pub fn full_line_str(&self, offset: TextSize) -> &'a str { + pub(crate) fn full_line_str(&self, offset: TextSize) -> &'a str { self.contents.full_line_str(offset) } /// Returns the text of the `offset`'s line. /// /// See [`LineRanges::line_str`]. - pub fn line_str(&self, offset: TextSize) -> &'a str { + pub(crate) fn line_str(&self, offset: TextSize) -> &'a str { self.contents.line_str(offset) } /// Returns the text of all lines that include `range`. /// /// See [`LineRanges::lines_str`]. - pub fn lines_str(&self, range: TextRange) -> &'a str { + pub(crate) fn lines_str(&self, range: TextRange) -> &'a str { self.contents.lines_str(range) } diff --git a/crates/ruff_linter/src/logging.rs b/crates/ruff_linter/src/logging.rs index 9dba2228b0..0b8bc4a341 100644 --- a/crates/ruff_linter/src/logging.rs +++ b/crates/ruff_linter/src/logging.rs @@ -186,7 +186,7 @@ impl DisplayParseError { } /// Create a [`DisplayParseError`] from a [`ParseError`] and a [`SourceCode`]. - pub fn from_source_code( + fn from_source_code( error: ParseError, path: Option, source_code: &SourceCode, diff --git a/crates/ruff_linter/src/message/grouped.rs b/crates/ruff_linter/src/message/grouped.rs index b22e661012..7f06cadd5a 100644 --- a/crates/ruff_linter/src/message/grouped.rs +++ b/crates/ruff_linter/src/message/grouped.rs @@ -13,7 +13,7 @@ use ruff_source_file::{LineColumn, OneIndexed}; use crate::fs::relativize_path; use crate::message::{Emitter, EmitterContext}; -pub struct GroupedEmitter { +pub(crate) struct GroupedEmitter { show_fix_status: bool, applicability: Applicability, preview: bool, @@ -33,25 +33,25 @@ impl Default for GroupedEmitter { impl GroupedEmitter { #[must_use] - pub fn with_show_fix_status(mut self, show_fix_status: bool) -> Self { + pub(crate) fn with_show_fix_status(mut self, show_fix_status: bool) -> Self { self.show_fix_status = show_fix_status; self } #[must_use] - pub fn with_applicability(mut self, applicability: Applicability) -> Self { + pub(crate) fn with_applicability(mut self, applicability: Applicability) -> Self { self.applicability = applicability; self } #[must_use] - pub fn with_preview(mut self, preview: bool) -> Self { + pub(crate) fn with_preview(mut self, preview: bool) -> Self { self.preview = preview; self } #[must_use] - pub fn with_prefer_rule_codes(mut self, prefer_rule_codes: bool) -> Self { + pub(crate) fn with_prefer_rule_codes(mut self, prefer_rule_codes: bool) -> Self { self.prefer_rule_codes = prefer_rule_codes; self } @@ -201,11 +201,11 @@ impl Display for DisplayGroupedMessage<'_> { } pub(super) struct RuleCodeAndBody<'a> { - pub(crate) message: &'a Diagnostic, - pub(crate) show_fix_status: bool, - pub(crate) applicability: Applicability, - pub(crate) preview: bool, - pub(crate) prefer_rule_codes: bool, + message: &'a Diagnostic, + show_fix_status: bool, + applicability: Applicability, + preview: bool, + prefer_rule_codes: bool, } impl Display for RuleCodeAndBody<'_> { diff --git a/crates/ruff_linter/src/message/mod.rs b/crates/ruff_linter/src/message/mod.rs index a25ad43b22..38fab1523e 100644 --- a/crates/ruff_linter/src/message/mod.rs +++ b/crates/ruff_linter/src/message/mod.rs @@ -13,11 +13,11 @@ use ruff_db::diagnostic::{ }; use ruff_db::files::File; -pub use grouped::GroupedEmitter; +pub(crate) use grouped::GroupedEmitter; use ruff_notebook::NotebookIndex; use ruff_source_file::{SourceFile, SourceFileBuilder}; use ruff_text_size::{TextRange, TextSize}; -pub use sarif::SarifEmitter; +pub(crate) use sarif::SarifEmitter; use crate::Fix; use crate::registry::Rule; @@ -76,7 +76,7 @@ pub fn create_panic_diagnostic(error: &PanicError, path: Option<&Path>) -> Diagn } #[expect(clippy::too_many_arguments)] -pub fn create_lint_diagnostic( +pub(crate) fn create_lint_diagnostic( body: B, suggestion: Option, range: TextRange, @@ -165,7 +165,7 @@ impl FileResolver for EmitterContext<'_> { /// Display format for [`Diagnostic`]s. /// /// The emitter serializes a slice of [`Diagnostic`]s and writes them to a [`Write`]. -pub trait Emitter { +pub(crate) trait Emitter { /// Serializes the `diagnostics` and writes the output to `writer`. fn emit( &mut self, @@ -175,7 +175,7 @@ pub trait Emitter { ) -> anyhow::Result<()>; } -/// Context passed to [`Emitter`]. +/// Context passed to diagnostic emitters. pub struct EmitterContext<'a> { notebook_indexes: &'a FxHashMap, } @@ -190,7 +190,7 @@ impl<'a> EmitterContext<'a> { self.notebook_indexes.contains_key(name) } - pub fn notebook_index(&self, name: &str) -> Option<&NotebookIndex> { + fn notebook_index(&self, name: &str) -> Option<&NotebookIndex> { self.notebook_indexes.get(name) } } diff --git a/crates/ruff_linter/src/message/sarif.rs b/crates/ruff_linter/src/message/sarif.rs index 375e544f1c..0af6e30548 100644 --- a/crates/ruff_linter/src/message/sarif.rs +++ b/crates/ruff_linter/src/message/sarif.rs @@ -21,12 +21,12 @@ use crate::registry::{Linter, RuleNamespace}; /// Static Analysis Results Interchange Format (SARIF) is a standard format /// for static analysis results. For full specification, see: /// [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) -pub struct SarifEmitter<'a> { +pub(crate) struct SarifEmitter<'a> { config: &'a DisplayDiagnosticConfig, } impl<'a> SarifEmitter<'a> { - pub fn new(config: &'a DisplayDiagnosticConfig) -> Self { + pub(crate) fn new(config: &'a DisplayDiagnosticConfig) -> Self { Self { config } } } diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index 22113ce06a..55d6796856 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -1242,10 +1242,7 @@ impl<'a> NoqaDirectives<'a> { Self { inner: directives } } - pub(crate) fn find_line_with_directive( - &self, - offset: TextSize, - ) -> Option<&NoqaDirectiveLine<'_>> { + fn find_line_with_directive(&self, offset: TextSize) -> Option<&NoqaDirectiveLine<'_>> { self.find_line_index(offset).map(|index| &self.inner[index]) } diff --git a/crates/ruff_linter/src/registry/rule_set.rs b/crates/ruff_linter/src/registry/rule_set.rs index de625379c8..80cae3432e 100644 --- a/crates/ruff_linter/src/registry/rule_set.rs +++ b/crates/ruff_linter/src/registry/rule_set.rs @@ -257,7 +257,7 @@ impl RuleSet { /// Returns `true` if any of the rules in `rules` are in this set. #[inline] - pub const fn any(&self, rules: &[Rule]) -> bool { + pub(crate) const fn any(&self, rules: &[Rule]) -> bool { let mut any = false; let mut i = 0; diff --git a/crates/ruff_linter/src/rules/airflow/helpers.rs b/crates/ruff_linter/src/rules/airflow/helpers.rs index d16587c374..b2b0d554bb 100644 --- a/crates/ruff_linter/src/rules/airflow/helpers.rs +++ b/crates/ruff_linter/src/rules/airflow/helpers.rs @@ -204,7 +204,7 @@ pub(crate) fn is_airflow_builtin_or_provider( } /// Return the [`ast::ExprName`] at the head of the expression, if any. -pub(crate) fn match_head(value: &Expr) -> Option<&ExprName> { +fn match_head(value: &Expr) -> Option<&ExprName> { match value { Expr::Attribute(ExprAttribute { value, .. }) => value.as_name_expr(), Expr::Name(name) => Some(name), diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs index d851d0b0fe..55574b2bfe 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs @@ -8,7 +8,7 @@ use crate::checkers::ast::Checker; use crate::settings::LinterSettings; /// Returns `true` if a function call is allowed to use a boolean trap. -pub(super) fn is_allowed_func_call(name: &str) -> bool { +fn is_allowed_func_call(name: &str) -> bool { matches!( name, "__setattr__" @@ -51,10 +51,7 @@ pub(super) fn is_allowed_func_call(name: &str) -> bool { } /// Returns `true` if a call is semantically allowed to use a boolean trap. -pub(super) fn is_semantically_allowed_func_call( - call: &ast::ExprCall, - semantic: &SemanticModel, -) -> bool { +fn is_semantically_allowed_func_call(call: &ast::ExprCall, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(call.func.as_ref()) .is_some_and(|qualified_name| { @@ -66,7 +63,7 @@ pub(super) fn is_semantically_allowed_func_call( } /// Returns `true` if a call is allowed by the user to use a boolean trap. -pub(super) fn is_user_allowed_func_call( +fn is_user_allowed_func_call( call: &ast::ExprCall, semantic: &SemanticModel, settings: &LinterSettings, @@ -88,7 +85,7 @@ pub(super) fn is_user_allowed_func_call( /// This only includes operators, i.e., functions that are usually not called directly. /// /// See: -pub(super) fn is_operator_method(name: &str) -> bool { +fn is_operator_method(name: &str) -> bool { matches!( name, "__contains__" // in diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs index e04d218d90..eb0b0acdc9 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/fixes.rs @@ -319,7 +319,7 @@ pub(crate) fn fix_unnecessary_collection_call( /// However, this is a syntax error under the f-string grammar. As such, /// this method will pad the start and end of an expression as needed to /// avoid producing invalid syntax. -pub(crate) fn pad_expression( +fn pad_expression( content: String, range: TextRange, locator: &Locator, diff --git a/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs b/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs index 7574d1f2d5..be50ed5255 100644 --- a/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_import_conventions/settings.rs @@ -53,7 +53,7 @@ impl Display for BannedAliases { impl BannedAliases { /// Returns an iterator over the banned aliases. - pub fn iter(&self) -> impl Iterator { + pub(crate) fn iter(&self) -> impl Iterator { self.0.iter().map(String::as_str) } } 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 691d69f337..7f2e72ff2d 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 @@ -281,7 +281,7 @@ enum Redundancy { } impl Redundancy { - pub(super) fn from_numeric_flags(numeric_flags: NumericFlags) -> Option { + fn from_numeric_flags(numeric_flags: NumericFlags) -> Option { if numeric_flags == NumericFlags::INT | NumericFlags::FLOAT | NumericFlags::COMPLEX { Some(Self::IntFloatComplex) } else if numeric_flags == NumericFlags::FLOAT | NumericFlags::COMPLEX { @@ -309,7 +309,7 @@ bitflags! { } impl NumericFlags { - pub(super) fn seen_builtin_type(&mut self, name: &str) { + fn seen_builtin_type(&mut self, name: &str) { let flag: NumericFlags = match name { "int" => NumericFlags::INT, "float" => NumericFlags::FLOAT, diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/unittest_assert.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/unittest_assert.rs index 5c0c120275..c5bea94b96 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/unittest_assert.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/unittest_assert.rs @@ -228,7 +228,7 @@ impl UnittestAssert { } /// Create a map from argument name to value. - pub(crate) fn args_map<'a>( + fn args_map<'a>( &'a self, args: &'a [Expr], keywords: &'a [Keyword], diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/warns.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/warns.rs index 3ee1436118..455249e949 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/warns.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/warns.rs @@ -158,7 +158,7 @@ impl Violation for PytestWarnsWithoutWarning { } } -pub(crate) fn is_pytest_warns(func: &Expr, semantic: &SemanticModel) -> bool { +fn is_pytest_warns(func: &Expr, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(func) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "warns"])) diff --git a/crates/ruff_linter/src/rules/flake8_quotes/settings.rs b/crates/ruff_linter/src/rules/flake8_quotes/settings.rs index fe5129d6e3..7da94422e0 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/settings.rs @@ -64,7 +64,7 @@ impl Display for Settings { impl Quote { #[must_use] - pub const fn opposite(self) -> Self { + pub(crate) const fn opposite(self) -> Self { match self { Self::Double => Self::Single, Self::Single => Self::Double, @@ -72,7 +72,7 @@ impl Quote { } /// Get the character used to represent this quote. - pub const fn as_char(self) -> char { + pub(crate) const fn as_char(self) -> char { match self { Self::Double => '"', Self::Single => '\'', diff --git a/crates/ruff_linter/src/rules/flake8_self/settings.rs b/crates/ruff_linter/src/rules/flake8_self/settings.rs index a6d9f1dde3..b1d056c68b 100644 --- a/crates/ruff_linter/src/rules/flake8_self/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_self/settings.rs @@ -8,7 +8,7 @@ use std::fmt::{Display, Formatter}; // By default, ignore the `namedtuple` methods and attributes, as well as the // _sunder_ names in Enum, which are underscore-prefixed to prevent conflicts // with field names. -pub const IGNORE_NAMES: [&str; 7] = [ +const IGNORE_NAMES: [&str; 7] = [ "_make", "_asdict", "_replace", diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs index 29e61c15e0..d6c78abb01 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs @@ -291,7 +291,7 @@ impl AlwaysFixableViolation for ExprAndFalse { } /// Return `true` if two `Expr` instances are equivalent names. -pub(crate) fn is_same_expr<'a>(a: &'a Expr, b: &'a Expr) -> Option<&'a str> { +fn is_same_expr<'a>(a: &'a Expr, b: &'a Expr) -> Option<&'a str> { if let (Expr::Name(ast::ExprName { id: a, .. }), Expr::Name(ast::ExprName { id: b, .. })) = (&a, &b) { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs index d53f2908d0..c121d74468 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs @@ -172,14 +172,14 @@ pub(super) enum NestedIf<'a> { } impl<'a> NestedIf<'a> { - pub(super) fn body(self) -> &'a [Stmt] { + fn body(self) -> &'a [Stmt] { match self { NestedIf::If(stmt_if) => &stmt_if.body, NestedIf::Elif(clause) => &clause.body, } } - pub(super) fn is_elif(self) -> bool { + fn is_elif(self) -> bool { matches!(self, NestedIf::Elif(..)) } } @@ -316,11 +316,7 @@ fn parenthesize_and_operand(expr: libcst_native::Expression) -> libcst_native::E } /// Convert `if a: if b:` to `if a and b:`. -pub(super) fn collapse_nested_if( - locator: &Locator, - stylist: &Stylist, - nested_if: NestedIf, -) -> Result { +fn collapse_nested_if(locator: &Locator, stylist: &Stylist, nested_if: NestedIf) -> Result { // Infer the indentation of the outer block. let Some(outer_indent) = whitespace::indentation(locator.contents(), &nested_if) else { bail!("Unable to fix multiline statement"); diff --git a/crates/ruff_linter/src/rules/flake8_tidy_imports/settings.rs b/crates/ruff_linter/src/rules/flake8_tidy_imports/settings.rs index 07bc4c44d2..9bda29060b 100644 --- a/crates/ruff_linter/src/rules/flake8_tidy_imports/settings.rs +++ b/crates/ruff_linter/src/rules/flake8_tidy_imports/settings.rs @@ -11,7 +11,7 @@ use ruff_macros::CacheKey; #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ApiBan { /// The message to display when the API is used. - pub msg: String, + pub(crate) msg: String, } impl Display for ApiBan { @@ -201,7 +201,7 @@ pub struct Settings { } impl Settings { - pub fn banned_module_level_imports(&self) -> impl Iterator { + pub(crate) fn banned_module_level_imports(&self) -> impl Iterator { self.banned_module_level_imports.iter().map(AsRef::as_ref) } } diff --git a/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs b/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs index b46e4d44aa..8735e72a5f 100644 --- a/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs +++ b/crates/ruff_linter/src/rules/flake8_todos/rules/todos.rs @@ -351,7 +351,7 @@ fn directive_errors(context: &LintContext, directive: &TodoDirective) { } /// Checks for "static" errors in the comment: missing colon, missing author, etc. -pub(crate) fn static_errors( +fn static_errors( context: &LintContext, comment: &str, comment_range: TextRange, diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs index 83f74e3331..09e7523571 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs @@ -243,7 +243,7 @@ pub(crate) fn is_dataclass_meta_annotation(annotation: &Expr, semantic: &Semanti /// def fun(arg, verbose=False): /// ... /// ``` -pub(crate) fn is_singledispatch_interface( +fn is_singledispatch_interface( function_def: &ast::StmtFunctionDef, semantic: &SemanticModel, ) -> bool { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs index 66ba774400..46537dcd2d 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs @@ -103,7 +103,7 @@ pub(crate) fn check_os_pathlib_single_arg_calls( }); } -pub(crate) fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> { +fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> { match expr { Expr::Name(name) => Some(name), Expr::Call(ExprCall { func, .. }) => get_name_expr(func), diff --git a/crates/ruff_linter/src/rules/isort/mod.rs b/crates/ruff_linter/src/rules/isort/mod.rs index 168cc504ad..40241ba98b 100644 --- a/crates/ruff_linter/src/rules/isort/mod.rs +++ b/crates/ruff_linter/src/rules/isort/mod.rs @@ -38,11 +38,11 @@ mod types; #[derive(Debug)] pub(crate) struct AnnotatedAliasData<'a> { - pub(crate) name: &'a str, - pub(crate) asname: Option<&'a str>, - pub(crate) atop: Vec>, - pub(crate) inline: Vec>, - pub(crate) trailing: Vec>, + name: &'a str, + asname: Option<&'a str>, + atop: Vec>, + inline: Vec>, + trailing: Vec>, } #[derive(Debug)] diff --git a/crates/ruff_linter/src/rules/isort/settings.rs b/crates/ruff_linter/src/rules/isort/settings.rs index ced2dfdcb7..b3ce23653f 100644 --- a/crates/ruff_linter/src/rules/isort/settings.rs +++ b/crates/ruff_linter/src/rules/isort/settings.rs @@ -73,13 +73,13 @@ pub struct Settings { } impl Settings { - pub fn requires_module_import(&self, name: String, as_name: Option) -> bool { + pub(crate) fn requires_module_import(&self, name: String, as_name: Option) -> bool { self.required_imports .contains(&NameImport::Import(ModuleNameImport { name: Alias { name, as_name }, })) } - pub fn requires_member_import( + pub(crate) fn requires_member_import( &self, module: Option, name: String, diff --git a/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs b/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs index f616e0f541..04993d8ac2 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs @@ -159,12 +159,12 @@ fn remove_cast(checker: &Checker, list_range: TextRange, iterable_range: TextRan /// A [`StatementVisitor`] that (conservatively) identifies mutations to a variable. #[derive(Default)] pub(crate) struct MutationVisitor<'a> { - pub(crate) target: &'a str, - pub(crate) is_mutated: bool, + target: &'a str, + is_mutated: bool, } impl<'a> MutationVisitor<'a> { - pub(crate) fn new(target: &'a str) -> Self { + fn new(target: &'a str) -> Self { Self { target, is_mutated: false, 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 bb45d6379a..7d13e32047 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 @@ -118,7 +118,7 @@ pub(crate) struct LogicalLine<'a> { impl<'a> LogicalLine<'a> { /// Returns `true` if this line is positioned at the start of the file. - pub(crate) const fn is_start_of_file(&self) -> bool { + const fn is_start_of_file(&self) -> bool { self.line.tokens_start == 0 } @@ -128,7 +128,7 @@ impl<'a> LogicalLine<'a> { } /// Returns logical line's text including comments, indents, dedent and trailing new lines. - pub(crate) fn text(&self) -> &'a str { + fn text(&self) -> &'a str { let tokens = self.tokens(); match (tokens.first(), tokens.last()) { (Some(first), Some(last)) => self @@ -141,7 +141,7 @@ impl<'a> LogicalLine<'a> { /// Returns the text without any leading or trailing newline, comment, indent, or dedent of this line #[cfg(test)] - pub(crate) fn text_trimmed(&self) -> &'a str { + fn text_trimmed(&self) -> &'a str { let tokens = self.tokens_trimmed(); match (tokens.first(), tokens.last()) { @@ -153,7 +153,7 @@ impl<'a> LogicalLine<'a> { } } - pub(crate) fn tokens_trimmed(&self) -> &'a [LogicalLineToken] { + fn tokens_trimmed(&self) -> &'a [LogicalLineToken] { let tokens = self.tokens(); let start = tokens @@ -173,7 +173,7 @@ impl<'a> LogicalLine<'a> { /// Returns the text after `token` #[inline] - pub(crate) fn text_after(&self, token: &'a LogicalLineToken) -> &str { + fn text_after(&self, token: &'a LogicalLineToken) -> &str { // SAFETY: The line must have at least one token or `token` would not belong to this line. let last_token = self.tokens().last().unwrap(); self.lines @@ -183,7 +183,7 @@ impl<'a> LogicalLine<'a> { /// Returns the text before `token` #[inline] - pub(crate) fn text_before(&self, token: &'a LogicalLineToken) -> &str { + fn text_before(&self, token: &'a LogicalLineToken) -> &str { // SAFETY: The line must have at least one token or `token` would not belong to this line. let first_token = self.tokens().first().unwrap(); self.lines @@ -192,20 +192,17 @@ impl<'a> LogicalLine<'a> { } /// Returns the whitespace *after* the `token` with the byte length - pub(crate) fn trailing_whitespace( - &self, - token: &'a LogicalLineToken, - ) -> (Whitespace, TextSize) { + fn trailing_whitespace(&self, token: &'a LogicalLineToken) -> (Whitespace, TextSize) { Whitespace::leading(self.text_after(token)) } /// Returns the whitespace and whitespace byte-length *before* the `token` - pub(crate) fn leading_whitespace(&self, token: &'a LogicalLineToken) -> (Whitespace, TextSize) { + fn leading_whitespace(&self, token: &'a LogicalLineToken) -> (Whitespace, TextSize) { Whitespace::trailing(self.text_before(token)) } /// Returns all tokens of the line, including comments and trailing new lines. - pub(crate) fn tokens(&self) -> &'a [LogicalLineToken] { + fn tokens(&self) -> &'a [LogicalLineToken] { &self.lines.tokens[self.line.tokens_start as usize..self.line.tokens_end as usize] } diff --git a/crates/ruff_linter/src/rules/pydocstyle/rules/property_docstring_starts_with_verb.rs b/crates/ruff_linter/src/rules/pydocstyle/rules/property_docstring_starts_with_verb.rs index a0bb27401d..8debed2bd7 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/rules/property_docstring_starts_with_verb.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/rules/property_docstring_starts_with_verb.rs @@ -48,7 +48,7 @@ use crate::rules::pydocstyle::settings::Settings; #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "0.15.18")] pub(crate) struct PropertyDocstringStartsWithVerb { - pub(crate) first_word: String, + first_word: String, } impl Violation for PropertyDocstringStartsWithVerb { diff --git a/crates/ruff_linter/src/rules/pydocstyle/settings.rs b/crates/ruff_linter/src/rules/pydocstyle/settings.rs index f2a7389b07..d25dc9f5a3 100644 --- a/crates/ruff_linter/src/rules/pydocstyle/settings.rs +++ b/crates/ruff_linter/src/rules/pydocstyle/settings.rs @@ -95,19 +95,19 @@ pub struct Settings { } impl Settings { - pub fn convention(&self) -> Option { + pub(crate) fn convention(&self) -> Option { self.convention } - pub fn ignore_decorators(&self) -> DecoratorIterator<'_> { + pub(crate) fn ignore_decorators(&self) -> DecoratorIterator<'_> { DecoratorIterator::new(&self.ignore_decorators) } - pub fn property_decorators(&self) -> DecoratorIterator<'_> { + pub(crate) fn property_decorators(&self) -> DecoratorIterator<'_> { DecoratorIterator::new(&self.property_decorators) } - pub fn ignore_var_parameters(&self) -> bool { + pub(crate) fn ignore_var_parameters(&self) -> bool { self.ignore_var_parameters } } diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs b/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs index 7a97aa09ab..19f2efeb3c 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/imports.rs @@ -36,8 +36,8 @@ use crate::checkers::ast::Checker; #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.44")] pub(crate) struct ImportShadowedByLoopVar { - pub(crate) name: String, - pub(crate) row: SourceRow, + name: String, + row: SourceRow, } impl Violation for ImportShadowedByLoopVar { diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs index 41620db47b..71958f972d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_str_strip_call.rs @@ -104,7 +104,7 @@ pub(crate) enum StripKind { } impl StripKind { - pub(crate) fn from_str(s: &str) -> Option { + fn from_str(s: &str) -> Option { match s { "strip" => Some(Self::Strip), "lstrip" => Some(Self::LStrip), @@ -132,7 +132,7 @@ pub(crate) enum RemovalKind { } impl RemovalKind { - pub(crate) fn for_strip(s: StripKind) -> Option { + fn for_strip(s: StripKind) -> Option { match s { StripKind::Strip => None, StripKind::LStrip => Some(Self::RemovePrefix), diff --git a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_and_global.rs b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_and_global.rs index fd8d54441a..9a202324e8 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nonlocal_and_global.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nonlocal_and_global.rs @@ -43,7 +43,7 @@ use crate::checkers::ast::Checker; #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "0.5.0")] pub(crate) struct NonlocalAndGlobal { - pub(crate) name: String, + name: String, } impl Violation for NonlocalAndGlobal { diff --git a/crates/ruff_linter/src/rules/pylint/rules/redefined_argument_from_local.rs b/crates/ruff_linter/src/rules/pylint/rules/redefined_argument_from_local.rs index 67799df816..1fcf2e448d 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/redefined_argument_from_local.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/redefined_argument_from_local.rs @@ -35,7 +35,7 @@ use crate::checkers::ast::Checker; #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "0.5.0")] pub(crate) struct RedefinedArgumentFromLocal { - pub(crate) name: String, + name: String, } impl Violation for RedefinedArgumentFromLocal { diff --git a/crates/ruff_linter/src/rules/pylint/settings.rs b/crates/ruff_linter/src/rules/pylint/settings.rs index c163f40090..ca7d97f14b 100644 --- a/crates/ruff_linter/src/rules/pylint/settings.rs +++ b/crates/ruff_linter/src/rules/pylint/settings.rs @@ -20,7 +20,7 @@ pub enum ConstantType { } impl ConstantType { - pub fn try_from_literal_expr(literal_expr: LiteralExpressionRef<'_>) -> Option { + pub(crate) fn try_from_literal_expr(literal_expr: LiteralExpressionRef<'_>) -> Option { match literal_expr { LiteralExpressionRef::StringLiteral(_) => Some(Self::Str), LiteralExpressionRef::BytesLiteral(_) => Some(Self::Bytes), 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 c8cfbe8c7a..8acf96cc74 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 @@ -329,12 +329,12 @@ struct ClassCellReferenceFinder { } impl ClassCellReferenceFinder { - pub(crate) fn new() -> Self { + fn new() -> Self { ClassCellReferenceFinder { has_class_cell: false, } } - pub(crate) fn found(&self) -> bool { + fn found(&self) -> bool { self.has_class_cell } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_isinstance.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_isinstance.rs index df8c492c96..b817a55676 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_isinstance.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_isinstance.rs @@ -24,7 +24,7 @@ impl fmt::Display for CallKind { } impl CallKind { - pub(crate) fn from_name(name: &str) -> Option { + fn from_name(name: &str) -> Option { match name { "isinstance" => Some(CallKind::Isinstance), "issubclass" => Some(CallKind::Issubclass), diff --git a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs index 895e09048b..29580bb0dd 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs @@ -212,7 +212,7 @@ impl Ranged for StarmapCandidate<'_> { impl StarmapCandidate<'_> { /// Return the generated element for the candidate. - pub(crate) fn element(&self) -> &Expr { + fn element(&self) -> &Expr { match self { Self::Generator(generator) => generator.elt.as_ref(), Self::ListComp(list_comp) => list_comp.elt.as_ref(), @@ -221,7 +221,7 @@ impl StarmapCandidate<'_> { } /// Return the generator comprehensions for the candidate. - pub(crate) fn generators(&self) -> &[ast::Comprehension] { + fn generators(&self) -> &[ast::Comprehension] { match self { Self::Generator(generator) => generator.generators.as_slice(), Self::ListComp(list_comp) => list_comp.generators.as_slice(), @@ -230,7 +230,7 @@ impl StarmapCandidate<'_> { } /// Try to produce a fix suggestion transforming this node into a call to `starmap`. - pub(crate) fn try_make_suggestion( + fn try_make_suggestion( &self, name: Name, iter: &Expr, diff --git a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs index 861ee00c3d..1945816daf 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/logging_eager_conversion.rs @@ -64,8 +64,8 @@ use crate::rules::flake8_logging_format::rules::{LoggingCallType, find_logging_c #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "0.13.2")] pub(crate) struct LoggingEagerConversion { - pub(crate) format_conversion: FormatConversion, - pub(crate) function_name: Option<&'static str>, + format_conversion: FormatConversion, + function_name: Option<&'static str>, } impl Violation for LoggingEagerConversion { diff --git a/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs b/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs index 010679cf23..3b895126b1 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs @@ -67,7 +67,7 @@ pub(crate) fn redirected_file_noqa(context: &LintContext, noqa_directives: &File } /// Convert a sequence of [Codes] into [Diagnostic]s and append them to `diagnostics`. -pub(crate) fn build_diagnostics(context: &LintContext, codes: &Codes<'_>) { +fn build_diagnostics(context: &LintContext, codes: &Codes<'_>) { for code in codes.iter() { if let Some(redirected) = get_redirect_target(code.as_str()) { let mut diagnostic = context.report_diagnostic( diff --git a/crates/ruff_linter/src/settings/fix_safety_table.rs b/crates/ruff_linter/src/settings/fix_safety_table.rs index 2f7f68f901..8b92f55eed 100644 --- a/crates/ruff_linter/src/settings/fix_safety_table.rs +++ b/crates/ruff_linter/src/settings/fix_safety_table.rs @@ -21,7 +21,7 @@ pub struct FixSafetyTable { } impl FixSafetyTable { - pub const fn resolve_applicability( + pub(crate) const fn resolve_applicability( &self, rule: Rule, applicability: Applicability, diff --git a/crates/ruff_linter/src/settings/rule_table.rs b/crates/ruff_linter/src/settings/rule_table.rs index 9f7a6e2ec1..69d2fad7b8 100644 --- a/crates/ruff_linter/src/settings/rule_table.rs +++ b/crates/ruff_linter/src/settings/rule_table.rs @@ -36,7 +36,7 @@ impl RuleTable { /// Returns whether violations of the given rule should be fixed. #[inline] - pub const fn should_fix(&self, rule: Rule) -> bool { + pub(crate) const fn should_fix(&self, rule: Rule) -> bool { self.should_fix.contains(rule) } diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 255030ab51..9fcd1841a4 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -495,13 +495,13 @@ impl ExtensionMapping { } /// Return the [`Language`] for the given file. - pub fn get(&self, path: &Path) -> Option { + fn get(&self, path: &Path) -> Option { let ext = path.extension()?.to_str()?; self.0.get(ext).copied() } /// Return the [`Language`] for a given file extension. - pub fn get_extension(&self, ext: &str) -> Option { + fn get_extension(&self, ext: &str) -> Option { self.0.get(ext).copied() } @@ -699,14 +699,14 @@ impl IdentifierPattern { } } - pub fn matches(&self, candidate: &str) -> bool { + pub(crate) fn matches(&self, candidate: &str) -> bool { match self { Self::Literal(literal) => literal == candidate, Self::Glob(pattern) => pattern.matches(candidate), } } - pub fn as_str(&self) -> &str { + pub(crate) fn as_str(&self) -> &str { match self { Self::Literal(literal) => literal, Self::Glob(pattern) => pattern.as_str(), @@ -731,10 +731,10 @@ impl FromStr for IdentifierPattern { /// Like [`PerFile`] but with string globs compiled to [`GlobMatcher`]s for more efficient usage. #[derive(Debug, Clone)] pub struct CompiledPerFile { - pub absolute_matcher: GlobMatcher, - pub basename_matcher: GlobMatcher, - pub negated: bool, - pub data: T, + absolute_matcher: GlobMatcher, + basename_matcher: GlobMatcher, + negated: bool, + data: T, } impl CompiledPerFile { diff --git a/crates/ruff_linter/src/suppression.rs b/crates/ruff_linter/src/suppression.rs index 7d95fc5f5e..6c5853d5e2 100644 --- a/crates/ruff_linter/src/suppression.rs +++ b/crates/ruff_linter/src/suppression.rs @@ -184,13 +184,13 @@ pub(crate) enum SuppressionComments { } impl SuppressionComments { - pub(crate) fn first(&self) -> &SuppressionComment { + fn first(&self) -> &SuppressionComment { match self { SuppressionComments::Single(comment) => comment, SuppressionComments::DisableEnable(comment, _) => comment, } } - pub(crate) fn second(&self) -> Option<&SuppressionComment> { + fn second(&self) -> Option<&SuppressionComment> { match self { SuppressionComments::Single(_) => None, SuppressionComments::DisableEnable(_, comment) => Some(comment), @@ -703,7 +703,7 @@ impl Suppressions { } } -pub(crate) struct SuppressionsBuilder<'a> { +struct SuppressionsBuilder<'a> { source: &'a str, settings: &'a LinterSettings, @@ -714,7 +714,7 @@ pub(crate) struct SuppressionsBuilder<'a> { } impl<'a> SuppressionsBuilder<'a> { - pub(crate) fn new(source: &'a str, settings: &'a LinterSettings) -> Self { + fn new(source: &'a str, settings: &'a LinterSettings) -> Self { Self { source, settings, @@ -724,7 +724,7 @@ impl<'a> SuppressionsBuilder<'a> { } } - pub(crate) fn load_from_tokens(mut self, tokens: &Tokens, indexer: &Indexer) -> Suppressions { + fn load_from_tokens(mut self, tokens: &Tokens, indexer: &Indexer) -> Suppressions { let mut indents: Vec<&str> = vec![]; let mut errors = Vec::new(); diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index d0aa410fd0..4728592d62 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -806,7 +806,7 @@ pub fn is_assignment_to_a_dunder(stmt: &Stmt) -> bool { /// Return `true` if the [`Expr`] is a singleton (`None`, `True`, `False`, or /// `...`). -pub const fn is_singleton(expr: &Expr) -> bool { +const fn is_singleton(expr: &Expr) -> bool { matches!( expr, Expr::NoneLiteral(_) | Expr::BooleanLiteral(_) | Expr::EllipsisLiteral(_) diff --git a/crates/ruff_python_ast/src/identifier.rs b/crates/ruff_python_ast/src/identifier.rs index c8a54cbadf..b6737c182c 100644 --- a/crates/ruff_python_ast/src/identifier.rs +++ b/crates/ruff_python_ast/src/identifier.rs @@ -160,14 +160,14 @@ pub(crate) struct IdentifierTokenizer<'a> { } impl<'a> IdentifierTokenizer<'a> { - pub(crate) fn new(source: &'a str, range: TextRange) -> Self { + fn new(source: &'a str, range: TextRange) -> Self { Self { cursor: Cursor::new(&source[range]), offset: range.start(), } } - pub(crate) fn starts_at(offset: TextSize, source: &'a str) -> Self { + fn starts_at(offset: TextSize, source: &'a str) -> Self { let range = TextRange::new(offset, source.text_len()); Self::new(source, range) } diff --git a/crates/ruff_python_ast/src/int.rs b/crates/ruff_python_ast/src/int.rs index eacfd8b54a..5f8aedb308 100644 --- a/crates/ruff_python_ast/src/int.rs +++ b/crates/ruff_python_ast/src/int.rs @@ -82,7 +82,7 @@ impl Int { } /// Return the [`Int`] as an u32, if it can be represented as that data type. - pub fn as_u32(&self) -> Option { + fn as_u32(&self) -> Option { match &self.0 { Number::Small(small) => u32::try_from(*small).ok(), Number::Big(_) => None, @@ -114,7 +114,7 @@ impl Int { } /// Return the [`Int`] as an i16, if it can be represented as that data type. - pub fn as_i16(&self) -> Option { + fn as_i16(&self) -> Option { match &self.0 { Number::Small(small) => i16::try_from(*small).ok(), Number::Big(_) => None, diff --git a/crates/ruff_python_ast/src/name.rs b/crates/ruff_python_ast/src/name.rs index cc4b20f570..585a9c88ed 100644 --- a/crates/ruff_python_ast/src/name.rs +++ b/crates/ruff_python_ast/src/name.rs @@ -454,7 +454,7 @@ impl<'a> QualifiedNameBuilder<'a> { } #[inline] - pub fn is_empty(&self) -> bool { + pub(crate) fn is_empty(&self) -> bool { self.segments.is_empty() } @@ -464,7 +464,7 @@ impl<'a> QualifiedNameBuilder<'a> { } #[inline] - pub fn pop(&mut self) { + pub(crate) fn pop(&mut self) { self.segments.pop(); } @@ -474,7 +474,7 @@ impl<'a> QualifiedNameBuilder<'a> { } #[inline] - pub fn extend_from_slice(&mut self, segments: &[&'a str]) { + pub(crate) fn extend_from_slice(&mut self, segments: &[&'a str]) { self.segments.extend_from_slice(segments); } @@ -638,7 +638,7 @@ impl<'a> UnqualifiedName<'a> { } #[inline] - pub fn from_slice(segments: &[&'a str]) -> Self { + fn from_slice(segments: &[&'a str]) -> Self { Self(SegmentsVec::from_slice(segments)) } diff --git a/crates/ruff_python_ast/src/node_index.rs b/crates/ruff_python_ast/src/node_index.rs index f936e97bf2..9b82c285b0 100644 --- a/crates/ruff_python_ast/src/node_index.rs +++ b/crates/ruff_python_ast/src/node_index.rs @@ -62,7 +62,7 @@ pub struct NodeIndex(NonZeroU32); impl NodeIndex { /// A placeholder `NodeIndex`. - pub const NONE: NodeIndex = NodeIndex(NonZeroU32::new(NodeIndex::_NONE).unwrap()); + const NONE: NodeIndex = NodeIndex(NonZeroU32::new(NodeIndex::_NONE).unwrap()); // Note that the index `u32::MAX` is reserved for the `NonZeroU32` niche, and // this placeholder also reserves the second highest index. diff --git a/crates/ruff_python_ast/src/parenthesize.rs b/crates/ruff_python_ast/src/parenthesize.rs index 786ca0572c..8ad309fb1f 100644 --- a/crates/ruff_python_ast/src/parenthesize.rs +++ b/crates/ruff_python_ast/src/parenthesize.rs @@ -13,7 +13,7 @@ use crate::ExprRef; /// generally prefer [`parenthesized_range`]. /// /// Prefer [`crate::token::parentheses_iterator`] if you have access to [`crate::token::Tokens`]. -pub fn parentheses_iterator<'a>( +fn parentheses_iterator<'a>( expr: ExprRef<'a>, parent: Option, comment_ranges: &'a CommentRanges, diff --git a/crates/ruff_python_ast/src/str.rs b/crates/ruff_python_ast/src/str.rs index a058cf4c8e..a24c51a87f 100644 --- a/crates/ruff_python_ast/src/str.rs +++ b/crates/ruff_python_ast/src/str.rs @@ -163,7 +163,7 @@ const SINGLE_QUOTE_STR_PREFIXES: &[&str] = &[ /// /// See: #[rustfmt::skip] -pub const TRIPLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ +pub(crate) const TRIPLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ "BR\"\"\"", "Br\"\"\"", "bR\"\"\"", @@ -187,7 +187,7 @@ pub const TRIPLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ ]; #[rustfmt::skip] -pub const SINGLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ +pub(crate) const SINGLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ "BR\"", "Br\"", "bR\"", @@ -215,7 +215,7 @@ pub const SINGLE_QUOTE_BYTE_PREFIXES: &[&str] = &[ /// /// See: #[rustfmt::skip] -pub const TRIPLE_QUOTE_TEMPLATE_PREFIXES: &[&str] = &[ +pub(crate) const TRIPLE_QUOTE_TEMPLATE_PREFIXES: &[&str] = &[ "TR\"\"\"", "Tr\"\"\"", "tR\"\"\"", @@ -239,7 +239,7 @@ pub const TRIPLE_QUOTE_TEMPLATE_PREFIXES: &[&str] = &[ ]; #[rustfmt::skip] -pub const SINGLE_QUOTE_TEMPLATE_PREFIXES: &[&str] = &[ +pub(crate) const SINGLE_QUOTE_TEMPLATE_PREFIXES: &[&str] = &[ "TR\"", "Tr\"", "tR\"", @@ -271,7 +271,7 @@ pub fn raw_contents(contents: &str) -> Option<&str> { Some(&contents[range]) } -pub fn raw_contents_range(contents: &str) -> Option { +fn raw_contents_range(contents: &str) -> Option { let leading_quote_str = leading_quote(contents)?; let trailing_quote_str = trailing_quote(contents)?; diff --git a/crates/ruff_python_ast/src/token/tokens.rs b/crates/ruff_python_ast/src/token/tokens.rs index 1c1de82d25..7037ae1e34 100644 --- a/crates/ruff_python_ast/src/token/tokens.rs +++ b/crates/ruff_python_ast/src/token/tokens.rs @@ -27,7 +27,7 @@ impl Tokens { /// Unlike `binary_search_by_key`, this method ensures that if multiple tokens start at the same offset, /// it returns the index of the first one. Multiple tokens can start at the same offset in cases where /// zero-length tokens are involved (like `Dedent` or `Newline` at the end of the file). - pub fn binary_search_by_start(&self, offset: TextSize) -> Result { + fn binary_search_by_start(&self, offset: TextSize) -> Result { let partition_point = self.partition_point(|token| token.start() < offset); let after = &self[partition_point..]; diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index f76aed156e..5de64531f5 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -245,7 +245,7 @@ impl<'a> Generator<'a> { } } - pub(crate) fn unparse_stmt(&mut self, ast: &Stmt) { + fn unparse_stmt(&mut self, ast: &Stmt) { macro_rules! statement { ($body:block) => {{ self.newline(); @@ -881,7 +881,7 @@ impl<'a> Generator<'a> { self.p("]"); } - pub(crate) fn unparse_type_param(&mut self, ast: &TypeParam) { + fn unparse_type_param(&mut self, ast: &TypeParam) { match ast { TypeParam::TypeVar(TypeParamTypeVar { name, @@ -918,7 +918,7 @@ impl<'a> Generator<'a> { } } - pub(crate) fn unparse_expr(&mut self, ast: &Expr, level: u8) { + fn unparse_expr(&mut self, ast: &Expr, level: u8) { macro_rules! opprec { ($opty:ident, $x:expr, $enu:path, $($var:ident($op:literal, $prec:ident)),*$(,)?) => { match $x { @@ -1376,7 +1376,7 @@ impl<'a> Generator<'a> { } } - pub(crate) fn unparse_singleton(&mut self, singleton: Singleton) { + fn unparse_singleton(&mut self, singleton: Singleton) { match singleton { Singleton::None => self.p("None"), Singleton::True => self.p("True"), diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index dbcd341b8c..2fa1e5e2e1 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -361,7 +361,7 @@ impl Format> for FormatEmptyLines { /// * Black normalization of `SourceComment`. /// * Line suffix with reserved width for the final, normalized content. /// * Expands parent node. -pub(crate) const fn trailing_end_of_line_comment( +const fn trailing_end_of_line_comment( comment: &SourceComment, ) -> FormatTrailingEndOfLineComment<'_> { FormatTrailingEndOfLineComment { comment } @@ -428,7 +428,7 @@ impl Format> for FormatTrailingEndOfLineComment<'_> { /// unnecessary allocations. /// * If the content is modified then make as few allocations as possible and use /// a dynamic text element at the original slice's start position. -pub(crate) const fn format_normalized_comment( +const fn format_normalized_comment( comment: Cow<'_, str>, range: TextRange, ) -> FormatNormalizedComment<'_> { diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index 4b7ce3d05f..c5f6a67a44 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -162,7 +162,7 @@ impl SourceComment { } /// Returns a nice debug representation that prints the source code for every comment (and not just the range). - pub(crate) fn debug<'a>(&'a self, source_code: SourceCode<'a>) -> DebugComment<'a> { + fn debug<'a>(&'a self, source_code: SourceCode<'a>) -> DebugComment<'a> { DebugComment::new(self, source_code) } diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 57e12ee27c..99c7b973b5 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -115,7 +115,7 @@ impl<'a> PyFormatContext<'a> { self.interpolated_string_state } - pub(crate) fn set_interpolated_string_state( + fn set_interpolated_string_state( &mut self, interpolated_string_state: InterpolatedStringState, ) { diff --git a/crates/ruff_python_formatter/src/expression/expr_slice.rs b/crates/ruff_python_formatter/src/expression/expr_slice.rs index 4f4b0f001d..eb6678ac66 100644 --- a/crates/ruff_python_formatter/src/expression/expr_slice.rs +++ b/crates/ruff_python_formatter/src/expression/expr_slice.rs @@ -153,7 +153,7 @@ impl FormatNodeRule for FormatExprSlice { /// to find out whether there is a second one, too, e.g. `[1:2]` and `[1:10:2]`. /// /// Returns the first and optionally the second colon. -pub(crate) fn find_colons( +fn find_colons( contents: &str, range: TextRange, lower: Option<&Expr>, diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 5cb3299a90..3ef2f2340f 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -923,7 +923,7 @@ impl CallChainLayout { /// Returns new state decreasing count of remaining calls/subscripts /// to traverse, or the state `FirstCallOrSubscript`, as appropriate. #[must_use] - pub(crate) fn decrement_call_like_count(self) -> Self { + fn decrement_call_like_count(self) -> Self { match self { Self::Fluent(AttributeState::CallLikePreceding(x)) => { if x > 1 { @@ -945,7 +945,7 @@ impl CallChainLayout { /// `FirstCallOrSubscript` -> `BeforeFirstCallOrSubscript` /// and otherwise returns unchanged. #[must_use] - pub(crate) fn transition_after_attribute(self) -> Self { + fn transition_after_attribute(self) -> Self { match self { Self::Fluent(AttributeState::FirstCallLike) => { Self::Fluent(AttributeState::BeforeFirstCallLike) @@ -954,7 +954,7 @@ impl CallChainLayout { } } - pub(crate) fn is_first_call_like(self) -> bool { + fn is_first_call_like(self) -> bool { matches!(self, Self::Fluent(AttributeState::FirstCallLike)) } @@ -976,7 +976,7 @@ impl CallChainLayout { /// 3. If the root is parenthesized, add 1 to that value. /// 4. If the total is at least 2, return `Fluent`. Otherwise /// return `NonFluent` - pub(crate) fn from_expression(mut expr: ExprRef, context: &PyFormatContext) -> Self { + fn from_expression(mut expr: ExprRef, context: &PyFormatContext) -> Self { // TODO(dylan): Once the fluent layout preview style is // stabilized, see if it is possible to simplify some of // the logic around parenthesized roots. (While supporting @@ -1118,7 +1118,7 @@ impl CallChainLayout { /// Determine whether to actually apply fluent layout in attribute, call and subscript /// formatting - pub(crate) fn apply_in_node<'a>( + fn apply_in_node<'a>( self, item: impl Into>, f: &mut PyFormatter, @@ -1135,7 +1135,7 @@ impl CallChainLayout { } } - pub(crate) fn is_fluent(self) -> bool { + fn is_fluent(self) -> bool { matches!(self, CallChainLayout::Fluent(_)) } } diff --git a/crates/ruff_python_formatter/src/main.rs b/crates/ruff_python_formatter/src/main.rs index 09e5ea2ae6..72769b74ac 100644 --- a/crates/ruff_python_formatter/src/main.rs +++ b/crates/ruff_python_formatter/src/main.rs @@ -8,7 +8,7 @@ use clap::Parser as ClapParser; use ruff_python_formatter::cli::{Cli, Emit, format_and_debug_print}; /// Read a `String` from `stdin`. -pub(crate) fn read_from_stdin() -> Result { +fn read_from_stdin() -> Result { let mut buffer = String::new(); io::stdin().lock().read_to_string(&mut buffer)?; Ok(buffer) diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index d862f95468..f8ed0bdfb7 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -132,7 +132,7 @@ impl PyFormatOptions { self.source_type } - pub const fn source_map_generation(&self) -> SourceMapGeneration { + pub(crate) const fn source_map_generation(&self) -> SourceMapGeneration { self.source_map_generation } @@ -270,7 +270,7 @@ pub enum QuoteStyle { } impl QuoteStyle { - pub const fn is_preserve(self) -> bool { + pub(crate) const fn is_preserve(self) -> bool { matches!(self, QuoteStyle::Preserve) } @@ -317,7 +317,7 @@ pub enum MagicTrailingComma { } impl MagicTrailingComma { - pub const fn is_respect(self) -> bool { + pub(crate) const fn is_respect(self) -> bool { matches!(self, Self::Respect) } @@ -384,7 +384,7 @@ pub enum NestedStringQuoteStyle { } impl NestedStringQuoteStyle { - pub const fn is_preferred(self) -> bool { + pub(crate) const fn is_preferred(self) -> bool { matches!(self, NestedStringQuoteStyle::Preferred) } } @@ -410,7 +410,7 @@ pub enum DocstringCode { } impl DocstringCode { - pub const fn is_enabled(self) -> bool { + pub(crate) const fn is_enabled(self) -> bool { matches!(self, DocstringCode::Enabled) } } diff --git a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs index a93b942540..a252d0a4c7 100644 --- a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs +++ b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs @@ -57,10 +57,7 @@ pub(crate) struct FormatFStringLiteralElement<'a> { } impl<'a> FormatFStringLiteralElement<'a> { - pub(crate) fn new( - element: &'a InterpolatedStringLiteralElement, - fstring_flags: AnyStringFlags, - ) -> Self { + fn new(element: &'a InterpolatedStringLiteralElement, fstring_flags: AnyStringFlags) -> Self { Self { element, fstring_flags, diff --git a/crates/ruff_python_formatter/src/other/parameters.rs b/crates/ruff_python_formatter/src/other/parameters.rs index 1c6682bab1..e1dd1153a3 100644 --- a/crates/ruff_python_formatter/src/other/parameters.rs +++ b/crates/ruff_python_formatter/src/other/parameters.rs @@ -329,11 +329,11 @@ impl Format> for CommentsAroundText<'_> { #[derive(Debug)] pub(crate) struct ParameterSeparator { /// The end of the last node or separator before this separator - pub(crate) preceding_end: TextSize, + preceding_end: TextSize, /// The range of the separator itself - pub(crate) separator: TextRange, + separator: TextRange, /// The start of the first node or separator following this separator - pub(crate) following_start: TextSize, + following_start: TextSize, } /// Finds slash and star in `f(a, /, b, *, c)` or `lambda a, /, b, *, c: 1`. diff --git a/crates/ruff_python_formatter/src/pattern/mod.rs b/crates/ruff_python_formatter/src/pattern/mod.rs index 2b9dda12ff..0b3487abb5 100644 --- a/crates/ruff_python_formatter/src/pattern/mod.rs +++ b/crates/ruff_python_formatter/src/pattern/mod.rs @@ -222,10 +222,7 @@ impl Format> for MaybeParenthesizePattern<'_> { /// /// The layout is only applied when the parenthesized pattern is the first or last item in the pattern. /// For example, the layout isn't used for `a | [b, c] | d` because that would look weird. -pub(crate) fn can_pattern_omit_optional_parentheses( - pattern: &Pattern, - context: &PyFormatContext, -) -> bool { +fn can_pattern_omit_optional_parentheses(pattern: &Pattern, context: &PyFormatContext) -> bool { let mut visitor = CanOmitOptionalParenthesesVisitor::default(); visitor.visit_pattern(pattern, context); diff --git a/crates/ruff_python_formatter/src/statement/clause.rs b/crates/ruff_python_formatter/src/statement/clause.rs index 70b092567a..b4320bcab3 100644 --- a/crates/ruff_python_formatter/src/statement/clause.rs +++ b/crates/ruff_python_formatter/src/statement/clause.rs @@ -46,7 +46,7 @@ impl<'a> ClauseHeader<'a> { /// /// This is similar to [`ruff_python_ast::AnyNodeRef::last_child_in_body`] /// but restricted to the clause. - pub(crate) fn last_child_in_clause(self) -> Option> { + fn last_child_in_clause(self) -> Option> { match self { ClauseHeader::Class(StmtClassDef { body, .. }) | ClauseHeader::Function(StmtFunctionDef { body, .. }) diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index 8b42ea90a0..16bbb1dfe6 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -1332,7 +1332,7 @@ pub(super) fn has_target_own_parentheses(target: &Expr, context: &PyFormatContex matches!(target, Expr::Tuple(_)) || has_own_parentheses(target, context).is_some() } -pub(super) fn should_parenthesize_target(target: &Expr, context: &PyFormatContext) -> bool { +fn should_parenthesize_target(target: &Expr, context: &PyFormatContext) -> bool { !(has_target_own_parentheses(target, context) || is_attribute_with_parenthesized_value(target, context)) } diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 913548768e..661ec08e5b 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -740,7 +740,7 @@ fn stub_suite_can_omit_empty_line(preceding: &Stmt, following: &Stmt, f: &PyForm } /// Returns `true` if a function or class body contains only an ellipsis with no comments. -pub(crate) fn contains_only_an_ellipsis(body: &[Stmt], comments: &Comments) -> bool { +fn contains_only_an_ellipsis(body: &[Stmt], comments: &Comments) -> bool { as_only_an_ellipsis(body, comments).is_some() } diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs index f02187355d..7c4e6181a7 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -318,7 +318,7 @@ impl QuoteMetadata { } } - pub(crate) fn from_str(text: &str, flags: AnyStringFlags, preferred_quote: Quote) -> Self { + fn from_str(text: &str, flags: AnyStringFlags, preferred_quote: Quote) -> Self { let kind = if flags.is_raw_string() { QuoteMetadataKind::raw(text, preferred_quote, flags.triple_quotes()) } else if flags.is_triple_quoted() { diff --git a/crates/ruff_python_index/src/indexer.rs b/crates/ruff_python_index/src/indexer.rs index a193ced98d..fe45645826 100644 --- a/crates/ruff_python_index/src/indexer.rs +++ b/crates/ruff_python_index/src/indexer.rs @@ -123,7 +123,7 @@ impl Indexer { } /// Returns `true` if the given offset is part of a continuation line. - pub fn is_continuation(&self, offset: TextSize, source: &str) -> bool { + fn is_continuation(&self, offset: TextSize, source: &str) -> bool { let line_start = source.line_start(offset); self.continuation_lines.binary_search(&line_start).is_ok() } diff --git a/crates/ruff_python_literal/src/char.rs b/crates/ruff_python_literal/src/char.rs index 98117acfb4..46233c0cc8 100644 --- a/crates/ruff_python_literal/src/char.rs +++ b/crates/ruff_python_literal/src/char.rs @@ -9,7 +9,7 @@ use icu_properties::props::{EnumeratedProperty, GeneralCategory}; /// * Zl Separator, Line ('\u2028', LINE SEPARATOR) /// * Zp Separator, Paragraph ('\u2029', PARAGRAPH SEPARATOR) /// * Zs (Separator, Space) other than ASCII space('\x20'). -pub fn is_printable(c: char) -> bool { +pub(crate) fn is_printable(c: char) -> bool { let cat = GeneralCategory::for_char(c); !matches!( diff --git a/crates/ruff_python_literal/src/format.rs b/crates/ruff_python_literal/src/format.rs index 37653ab1de..41163af494 100644 --- a/crates/ruff_python_literal/src/format.rs +++ b/crates/ruff_python_literal/src/format.rs @@ -26,7 +26,7 @@ impl FormatConversion { } impl FormatConversion { - pub fn from_char(c: char) -> Option { + fn from_char(c: char) -> Option { match c { 's' => Some(FormatConversion::Str), 'r' => Some(FormatConversion::Repr), diff --git a/crates/ruff_python_literal/src/lib.rs b/crates/ruff_python_literal/src/lib.rs index 7c9025b4a7..89c446ef8b 100644 --- a/crates/ruff_python_literal/src/lib.rs +++ b/crates/ruff_python_literal/src/lib.rs @@ -1,5 +1,5 @@ pub mod cformat; -pub mod char; +mod char; pub mod escape; pub mod float; pub mod format; diff --git a/crates/ruff_python_parser/src/lexer.rs b/crates/ruff_python_parser/src/lexer.rs index 868805bb6c..5d0124a7fa 100644 --- a/crates/ruff_python_parser/src/lexer.rs +++ b/crates/ruff_python_parser/src/lexer.rs @@ -1495,7 +1495,7 @@ impl<'src> Lexer<'src> { self.errors.truncate(errors_position); } - pub fn finish(self) -> Vec { + pub(crate) fn finish(self) -> Vec { self.errors } } diff --git a/crates/ruff_python_parser/src/lexer/indentation.rs b/crates/ruff_python_parser/src/lexer/indentation.rs index c2193c9e7b..acdf53a2fb 100644 --- a/crates/ruff_python_parser/src/lexer/indentation.rs +++ b/crates/ruff_python_parser/src/lexer/indentation.rs @@ -12,7 +12,7 @@ use ruff_python_trivia::tab_offset_u32; pub(super) struct Column(u32); impl Column { - pub(super) const fn new(column: u32) -> Self { + const fn new(column: u32) -> Self { Self(column) } } @@ -22,7 +22,7 @@ impl Column { pub(super) struct Character(u32); impl Character { - pub(super) const fn new(characters: u32) -> Self { + const fn new(characters: u32) -> Self { Self(characters) } } @@ -45,7 +45,7 @@ impl Indentation { } #[cfg(test)] - pub(super) const fn new(column: Column, character: Character) -> Self { + const fn new(column: Column, character: Character) -> Self { Self { column, character } } diff --git a/crates/ruff_python_parser/src/lib.rs b/crates/ruff_python_parser/src/lib.rs index 208e6be77f..49beaf37b5 100644 --- a/crates/ruff_python_parser/src/lib.rs +++ b/crates/ruff_python_parser/src/lib.rs @@ -418,7 +418,7 @@ impl Parsed { } /// Consumes the [`Parsed`] output and returns a list of syntax errors found during parsing. - pub fn into_errors(self) -> Vec { + fn into_errors(self) -> Vec { self.errors } @@ -474,7 +474,7 @@ impl Parsed { /// /// Note that any [`unsupported_syntax_errors`](Parsed::unsupported_syntax_errors) will not /// cause [`Err`] to be returned. - pub(crate) fn into_result(self) -> Result, ParseError> { + fn into_result(self) -> Result, ParseError> { if self.has_valid_syntax() { Ok(self) } else { @@ -510,7 +510,7 @@ impl Parsed { /// Otherwise, it returns [`None`]. /// /// [`Some(Parsed)`]: Some - pub fn try_into_expression(self) -> Option> { + fn try_into_expression(self) -> Option> { match self.syntax { Mod::Module(_) => None, Mod::Expression(expression) => Some(Parsed { @@ -542,7 +542,7 @@ impl Parsed { } /// Returns a mutable reference to the expression contained in this parsed output. - pub fn expr_mut(&mut self) -> &mut Expr { + fn expr_mut(&mut self) -> &mut Expr { &mut self.syntax.body } diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 26ffaf2211..175d8cfd43 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -67,7 +67,7 @@ pub(super) const EXPR_SET: TokenSet = TokenSet::new([ .union(LITERAL_SET); /// Tokens that can appear after an expression. -pub(super) const END_EXPR_SET: TokenSet = TokenSet::new([ +const END_EXPR_SET: TokenSet = TokenSet::new([ // Ex) `expr` (without a newline) TokenKind::EndOfFile, // Ex) `expr` @@ -253,7 +253,7 @@ impl<'src> Parser<'src> { self.parse_binary_expression_or_higher_recursive(lhs, left_precedence, context, start) } - pub(super) fn parse_binary_expression_or_higher_recursive( + fn parse_binary_expression_or_higher_recursive( &mut self, mut left: ParsedExpr, left_precedence: OperatorPrecedence, @@ -746,7 +746,7 @@ impl<'src> Parser<'src> { /// expression, `[` for a subscript expression, or `.` for an attribute expression. /// /// This method does nothing if the current token is not a candidate for a postfix expression. - pub(super) fn parse_postfix_expression( + fn parse_postfix_expression( &mut self, mut lhs: Expr, start: TextSize, @@ -786,7 +786,7 @@ impl<'src> Parser<'src> { /// If the parser isn't position at a `(` token. /// /// See: - pub(super) fn parse_call_expression(&mut self, func: Expr, start: TextSize) -> ast::ExprCall { + fn parse_call_expression(&mut self, func: Expr, start: TextSize) -> ast::ExprCall { let arguments = self.parse_arguments(ArgumentsContext::Call); ast::ExprCall { @@ -2675,7 +2675,7 @@ impl<'src> Parser<'src> { /// parenthesized or the first token of the expression. /// /// See: - pub(super) fn parse_generator_expression( + fn parse_generator_expression( &mut self, element: Expr, start: TextSize, @@ -2919,11 +2919,7 @@ impl<'src> Parser<'src> { /// If the parser isn't positioned at a `:=` token. /// /// See: - pub(super) fn parse_named_expression( - &mut self, - mut target: Expr, - start: TextSize, - ) -> ast::ExprNamed { + fn parse_named_expression(&mut self, mut target: Expr, start: TextSize) -> ast::ExprNamed { self.bump(TokenKind::ColonEqual); if !target.is_name_expr() { @@ -3017,7 +3013,7 @@ impl<'src> Parser<'src> { /// If the parser isn't positioned at an `if` token. /// /// See: - pub(super) fn parse_if_expression(&mut self, body: Expr, start: TextSize) -> ast::ExprIf { + fn parse_if_expression(&mut self, body: Expr, start: TextSize) -> ast::ExprIf { self.bump(TokenKind::If); let test = self.parse_simple_expression(ExpressionContext::default()); @@ -3159,7 +3155,7 @@ impl ParsedExpr { } #[inline] - pub(super) const fn is_unparenthesized_named_expr(&self) -> bool { + const fn is_unparenthesized_named_expr(&self) -> bool { !self.is_parenthesized && self.expr.is_named_expr() } } @@ -3283,7 +3279,7 @@ impl ExpressionContext { ExpressionContext::starred_bitwise_or().with_yield_expression_allowed() } - pub(super) fn disallow_starred_expressions(self) -> Self { + fn disallow_starred_expressions(self) -> Self { let flags = self.0 & !ExpressionContextFlags::ALLOW_STARRED_EXPRESSION; ExpressionContext(flags) } diff --git a/crates/ruff_python_parser/src/parser/mod.rs b/crates/ruff_python_parser/src/parser/mod.rs index 6719c0ab86..641b0190f4 100644 --- a/crates/ruff_python_parser/src/parser/mod.rs +++ b/crates/ruff_python_parser/src/parser/mod.rs @@ -631,7 +631,7 @@ impl<'src> Parser<'src> { /// # Panics /// /// If the current token is not a soft keyword. - pub(crate) fn bump_soft_keyword_as_name(&mut self) { + fn bump_soft_keyword_as_name(&mut self) { assert!(self.at_soft_keyword()); self.do_bump(TokenKind::Name); diff --git a/crates/ruff_python_parser/src/token_source.rs b/crates/ruff_python_parser/src/token_source.rs index 39539194d4..ea1e482484 100644 --- a/crates/ruff_python_parser/src/token_source.rs +++ b/crates/ruff_python_parser/src/token_source.rs @@ -20,7 +20,7 @@ pub(crate) struct TokenSource<'src> { impl<'src> TokenSource<'src> { /// Create a new token source for the given lexer. - pub(crate) fn new(lexer: Lexer<'src>, source: &str, start_offset: TextSize) -> Self { + fn new(lexer: Lexer<'src>, source: &str, start_offset: TextSize) -> Self { TokenSource { lexer, tokens: allocate_tokens_vec(&source[start_offset.to_usize()..]), diff --git a/crates/ruff_python_semantic/src/analyze/terminal.rs b/crates/ruff_python_semantic/src/analyze/terminal.rs index 8bdb0fd849..adf4bf842f 100644 --- a/crates/ruff_python_semantic/src/analyze/terminal.rs +++ b/crates/ruff_python_semantic/src/analyze/terminal.rs @@ -28,7 +28,7 @@ impl Terminal { } /// Returns `true` if the [`Terminal`] behavior includes at least one `return` path. - pub fn has_any_return(self) -> bool { + fn has_any_return(self) -> bool { matches!( self, Self::Return | Self::RaiseOrReturn | Self::ConditionalReturn diff --git a/crates/ruff_python_semantic/src/analyze/type_inference.rs b/crates/ruff_python_semantic/src/analyze/type_inference.rs index c85dc7d411..b2573a31db 100644 --- a/crates/ruff_python_semantic/src/analyze/type_inference.rs +++ b/crates/ruff_python_semantic/src/analyze/type_inference.rs @@ -443,7 +443,7 @@ pub enum NumberLike { impl NumberLike { /// Coerces two number-like types to the "highest" number-like type. #[must_use] - pub fn coerce(self, other: NumberLike) -> NumberLike { + fn coerce(self, other: NumberLike) -> NumberLike { match (self, other) { (NumberLike::Complex, _) | (_, NumberLike::Complex) => NumberLike::Complex, (NumberLike::Float, _) | (_, NumberLike::Float) => NumberLike::Float, diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 5375235e87..404b33f2e3 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -861,7 +861,7 @@ impl BuiltinTypeChecker for FloatChecker { const EXPR_TYPE: PythonType = PythonType::Number(NumberLike::Float); } -pub struct IoBaseChecker; +struct IoBaseChecker; impl TypeChecker for IoBaseChecker { fn match_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { @@ -974,7 +974,7 @@ impl TypeChecker for PathlibPathChecker { } } -pub struct FastApiRouteChecker; +struct FastApiRouteChecker; impl FastApiRouteChecker { fn is_fastapi_route_constructor(semantic: &SemanticModel, expr: &Expr) -> bool { @@ -1003,7 +1003,7 @@ impl TypeChecker for FastApiRouteChecker { } } -pub struct TypeVarLikeChecker; +struct TypeVarLikeChecker; impl TypeVarLikeChecker { /// Returns `true` if an [`Expr`] is a `TypeVar`, `TypeVarTuple`, or `ParamSpec` call. @@ -1146,7 +1146,7 @@ pub fn is_fastapi_route(binding: &Binding, semantic: &SemanticModel) -> bool { } /// Test whether the given binding is for an old-style `TypeVar`, `TypeVarTuple` or a `ParamSpec`. -pub fn is_type_var_like(binding: &Binding, semantic: &SemanticModel) -> bool { +pub(crate) fn is_type_var_like(binding: &Binding, semantic: &SemanticModel) -> bool { check_type::(binding, semantic) } diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 099afc7ce9..83d42a20f2 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -463,7 +463,7 @@ impl<'a> Bindings<'a> { } /// Pushes a new [`Binding`] and returns its [`BindingId`]. - pub fn push(&mut self, binding: Binding<'a>) -> BindingId { + pub(crate) fn push(&mut self, binding: Binding<'a>) -> BindingId { self.0.push(binding) } } diff --git a/crates/ruff_python_semantic/src/cfg/graph.rs b/crates/ruff_python_semantic/src/cfg/graph.rs index 59acb348eb..c6e33fcde7 100644 --- a/crates/ruff_python_semantic/src/cfg/graph.rs +++ b/crates/ruff_python_semantic/src/cfg/graph.rs @@ -28,7 +28,7 @@ impl<'stmt> ControlFlowGraph<'stmt> { } /// Index of terminal block - pub fn terminal(&self) -> BlockId { + pub(crate) fn terminal(&self) -> BlockId { self.terminal } @@ -126,12 +126,12 @@ impl Edges { } /// Returns iterator over indices of blocks targeted by given edges - pub fn targets(&self) -> impl ExactSizeIterator + '_ { + pub(crate) fn targets(&self) -> impl ExactSizeIterator + '_ { self.targets.iter().copied() } /// Returns iterator over [`Condition`]s which must be satisfied to traverse corresponding edge - pub fn conditions(&self) -> impl ExactSizeIterator { + pub(crate) fn conditions(&self) -> impl ExactSizeIterator { self.conditions.iter() } diff --git a/crates/ruff_python_semantic/src/definition.rs b/crates/ruff_python_semantic/src/definition.rs index 7ff94cbed2..78dc611938 100644 --- a/crates/ruff_python_semantic/src/definition.rs +++ b/crates/ruff_python_semantic/src/definition.rs @@ -24,7 +24,7 @@ pub struct DefinitionId; impl DefinitionId { /// Returns the ID for the module definition. #[inline] - pub const fn module() -> Self { + pub(crate) const fn module() -> Self { DefinitionId::from_u32(0) } } @@ -69,7 +69,7 @@ impl<'a> Module<'a> { } /// Return the name of the module. - pub const fn name(&self) -> Option<&'a str> { + pub(crate) const fn name(&self) -> Option<&'a str> { self.name } } @@ -97,7 +97,7 @@ pub struct Member<'a> { impl<'a> Member<'a> { /// Return the name of the member. - pub fn name(&self) -> &'a str { + fn name(&self) -> &'a str { match self.kind { MemberKind::Class(class) => &class.name, MemberKind::NestedClass(class) => &class.name, @@ -201,7 +201,7 @@ impl<'a> Definition<'a> { pub struct Definitions<'a>(IndexVec>); impl<'a> Definitions<'a> { - pub fn for_module(definition: Module<'a>) -> Self { + pub(crate) fn for_module(definition: Module<'a>) -> Self { Self(IndexVec::from_raw(vec![Definition::Module(definition)])) } diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 21e10500b1..57844a8776 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -307,7 +307,7 @@ impl<'a> SemanticModel<'a> { } /// Create a new [`Binding`] for a builtin. - pub fn push_builtin(&mut self) -> BindingId { + fn push_builtin(&mut self) -> BindingId { self.bindings.push(Binding { range: TextRange::default(), kind: BindingKind::Builtin, @@ -1531,7 +1531,7 @@ impl<'a> SemanticModel<'a> { } /// Returns a mutable reference to the global [`Scope`]. - pub fn global_scope_mut(&mut self) -> &mut Scope<'a> { + fn global_scope_mut(&mut self) -> &mut Scope<'a> { self.scopes.global_mut() } @@ -1556,12 +1556,12 @@ impl<'a> SemanticModel<'a> { } /// Returns the parent of the given [`Scope`], if any. - pub fn parent_scope(&self, scope: &Scope) -> Option<&Scope<'a>> { + fn parent_scope(&self, scope: &Scope) -> Option<&Scope<'a>> { scope.parent.map(|scope_id| &self.scopes[scope_id]) } /// Returns the ID of the parent of the given [`ScopeId`], if any. - pub fn parent_scope_id(&self, scope_id: ScopeId) -> Option { + fn parent_scope_id(&self, scope_id: ScopeId) -> Option { self.scopes[scope_id].parent } @@ -1604,7 +1604,7 @@ impl<'a> SemanticModel<'a> { /// Given a [`NodeId`], return its parent, if any. #[inline] - pub fn parent_expression(&self, node_id: NodeId) -> Option<&'a Expr> { + pub(crate) fn parent_expression(&self, node_id: NodeId) -> Option<&'a Expr> { let parent_node_id = self.nodes.ancestor_ids(node_id).nth(1)?; self.nodes[parent_node_id].as_expression() } @@ -2041,7 +2041,7 @@ impl<'a> SemanticModel<'a> { } /// Return the union of all handled exceptions as an [`Exceptions`] bitflag. - pub fn exceptions(&self) -> Exceptions { + fn exceptions(&self) -> Exceptions { let mut exceptions = Exceptions::empty(); for exception in &self.handled_exceptions { exceptions.insert(*exception); @@ -2136,7 +2136,7 @@ impl<'a> SemanticModel<'a> { /// Return `true` if the model is visiting a "`__future__` type definition" /// that was previously deferred when initially traversing the AST - pub const fn in_future_type_definition(&self) -> bool { + const fn in_future_type_definition(&self) -> bool { self.flags .intersects(SemanticModelFlags::FUTURE_TYPE_DEFINITION) } @@ -2163,7 +2163,7 @@ impl<'a> SemanticModel<'a> { /// cast("Thread", x) # Forward reference /// cast(Thread, x) # Non-forward reference /// ``` - pub const fn in_forward_reference(&self) -> bool { + const fn in_forward_reference(&self) -> bool { self.in_string_type_definition() || (self.in_future_type_definition() && self.in_typing_only_annotation()) } @@ -2225,7 +2225,7 @@ impl<'a> SemanticModel<'a> { } /// Return `true` if the model is in a t-string. - pub const fn in_t_string(&self) -> bool { + const fn in_t_string(&self) -> bool { self.flags.intersects(SemanticModelFlags::T_STRING) } @@ -2432,7 +2432,7 @@ impl TypingOnlyBindingsStatus { matches!(self, TypingOnlyBindingsStatus::Allowed) } - pub const fn is_disallowed(self) -> bool { + const fn is_disallowed(self) -> bool { matches!(self, TypingOnlyBindingsStatus::Disallowed) } } @@ -2918,7 +2918,7 @@ bitflags! { } impl SemanticModelFlags { - pub fn new(path: &Path) -> Self { + fn new(path: &Path) -> Self { if PySourceType::from(path).is_stub() { Self::STUB_FILE } else { diff --git a/crates/ruff_python_semantic/src/nodes.rs b/crates/ruff_python_semantic/src/nodes.rs index d1e6358ad9..daef0a26a0 100644 --- a/crates/ruff_python_semantic/src/nodes.rs +++ b/crates/ruff_python_semantic/src/nodes.rs @@ -49,7 +49,7 @@ impl<'a> Nodes<'a> { /// Return the [`NodeId`] of the parent node. #[inline] - pub fn parent_id(&self, node_id: NodeId) -> Option { + pub(crate) fn parent_id(&self, node_id: NodeId) -> Option { self.nodes[node_id].parent } @@ -89,7 +89,7 @@ pub enum NodeRef<'a> { impl<'a> NodeRef<'a> { /// Returns the [`Stmt`] if this is a statement, or `None` if the reference is to another /// kind of AST node. - pub fn as_statement(&self) -> Option<&'a Stmt> { + pub(crate) fn as_statement(&self) -> Option<&'a Stmt> { match self { NodeRef::Stmt(stmt) => Some(stmt), NodeRef::Expr(_) => None, @@ -98,18 +98,18 @@ impl<'a> NodeRef<'a> { /// Returns the [`Expr`] if this is a expression, or `None` if the reference is to another /// kind of AST node. - pub fn as_expression(&self) -> Option<&'a Expr> { + pub(crate) fn as_expression(&self) -> Option<&'a Expr> { match self { NodeRef::Stmt(_) => None, NodeRef::Expr(expr) => Some(expr), } } - pub fn is_statement(&self) -> bool { + pub(crate) fn is_statement(&self) -> bool { self.as_statement().is_some() } - pub fn is_expression(&self) -> bool { + pub(crate) fn is_expression(&self) -> bool { self.as_expression().is_some() } } diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index fceaf14e75..eb80fda937 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -16,7 +16,7 @@ pub struct Scope<'a> { pub kind: ScopeKind<'a>, /// The parent scope, if any. - pub parent: Option, + pub(crate) parent: Option, /// A list of star imports in this scope. These represent _module_ imports (e.g., `sys` in /// `from sys import *`), rather than individual bindings (e.g., individual members in `sys`). @@ -45,7 +45,7 @@ pub struct Scope<'a> { } impl<'a> Scope<'a> { - pub fn global() -> Self { + fn global() -> Self { Scope { kind: ScopeKind::Module, parent: None, @@ -57,7 +57,7 @@ impl<'a> Scope<'a> { } } - pub fn local(kind: ScopeKind<'a>, parent: ScopeId) -> Self { + fn local(kind: ScopeKind<'a>, parent: ScopeId) -> Self { Scope { kind, parent: Some(parent), diff --git a/crates/ruff_python_stdlib/src/builtins.rs b/crates/ruff_python_stdlib/src/builtins.rs index c13705c187..08247aacda 100644 --- a/crates/ruff_python_stdlib/src/builtins.rs +++ b/crates/ruff_python_stdlib/src/builtins.rs @@ -15,7 +15,7 @@ const IPYTHON_BUILTINS: &[&str] = &["__IPYTHON__", "display", "get_ipython"]; /// Globally defined names which are not attributes of the builtins module, or /// are only present on some platforms. -pub const MAGIC_GLOBALS: &[&str] = &[ +const MAGIC_GLOBALS: &[&str] = &[ "WindowsError", "__annotations__", "__builtins__", diff --git a/crates/ruff_python_trivia/src/cursor.rs b/crates/ruff_python_trivia/src/cursor.rs index a2c7e17f2b..d1f54ccf8c 100644 --- a/crates/ruff_python_trivia/src/cursor.rs +++ b/crates/ruff_python_trivia/src/cursor.rs @@ -57,7 +57,7 @@ impl<'a> Cursor<'a> { /// Peeks the next character from the input stream without consuming it. /// Returns [`EOF_CHAR`] if the file is at the end of the file. - pub fn last(&self) -> char { + fn last(&self) -> char { self.chars.clone().next_back().unwrap_or(EOF_CHAR) } @@ -84,7 +84,7 @@ impl<'a> Cursor<'a> { } /// Consumes the next character from the back - pub fn bump_back(&mut self) -> Option { + pub(crate) fn bump_back(&mut self) -> Option { self.chars.next_back() } @@ -124,7 +124,7 @@ impl<'a> Cursor<'a> { } } - pub fn eat_char_back(&mut self, c: char) -> bool { + pub(crate) fn eat_char_back(&mut self, c: char) -> bool { if self.last() == c { self.bump_back(); true @@ -153,7 +153,7 @@ impl<'a> Cursor<'a> { } /// Eats symbols from the back while predicate returns true or until the beginning of file is reached. - pub fn eat_back_while(&mut self, mut predicate: impl FnMut(char) -> bool) { + pub(crate) fn eat_back_while(&mut self, mut predicate: impl FnMut(char) -> bool) { // It was tried making optimized version of this for eg. line comments, but // LLVM can inline all of this and compile it down to fast iteration over bytes. while predicate(self.last()) && !self.is_eof() { diff --git a/crates/ruff_python_trivia/src/tokenizer.rs b/crates/ruff_python_trivia/src/tokenizer.rs index d43b65462e..37547a8959 100644 --- a/crates/ruff_python_trivia/src/tokenizer.rs +++ b/crates/ruff_python_trivia/src/tokenizer.rs @@ -848,7 +848,7 @@ impl<'a> BackwardsTokenizer<'a> { self.filter(|t| !t.kind().is_trivia()) } - pub fn next_token(&mut self) -> SimpleToken { + fn next_token(&mut self) -> SimpleToken { self.cursor.start_token(); self.back_offset = self.cursor.text_len() + self.offset; diff --git a/crates/ruff_ranged_value/src/lib.rs b/crates/ruff_ranged_value/src/lib.rs index 7d9b5d64b6..a3eb869f70 100644 --- a/crates/ruff_ranged_value/src/lib.rs +++ b/crates/ruff_ranged_value/src/lib.rs @@ -155,7 +155,7 @@ impl RangedValue { Self::with_range(value, ValueSource::Editor, TextRange::default()) } - pub fn with_range(value: T, source: ValueSource, range: TextRange) -> Self { + fn with_range(value: T, source: ValueSource, range: TextRange) -> Self { Self { value, range: Some(range), diff --git a/crates/ruff_server/src/edit/notebook.rs b/crates/ruff_server/src/edit/notebook.rs index d3a6c3f32d..53d012edcf 100644 --- a/crates/ruff_server/src/edit/notebook.rs +++ b/crates/ruff_server/src/edit/notebook.rs @@ -239,11 +239,7 @@ impl NotebookDocument { } impl NotebookCell { - pub(crate) fn new( - cell: lsp_types::NotebookCell, - contents: String, - version: DocumentVersion, - ) -> Self { + fn new(cell: lsp_types::NotebookCell, contents: String, version: DocumentVersion) -> Self { Self { uri: cell.document, kind: cell.kind, diff --git a/crates/ruff_server/src/edit/text_document.rs b/crates/ruff_server/src/edit/text_document.rs index 016a33146f..f6002117e7 100644 --- a/crates/ruff_server/src/edit/text_document.rs +++ b/crates/ruff_server/src/edit/text_document.rs @@ -28,7 +28,7 @@ pub struct TextDocument { } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LanguageId { +pub(crate) enum LanguageId { Python, Markdown, Other, @@ -56,12 +56,12 @@ impl TextDocument { } #[must_use] - pub fn with_language_id(mut self, language_id: LanguageKind) -> Self { + pub(crate) fn with_language_id(mut self, language_id: LanguageKind) -> Self { self.language_id = Some(LanguageId::from(language_id)); self } - pub fn into_contents(self) -> String { + pub(crate) fn into_contents(self) -> String { self.contents } @@ -69,15 +69,15 @@ impl TextDocument { &self.contents } - pub fn index(&self) -> &LineIndex { + pub(crate) fn index(&self) -> &LineIndex { &self.index } - pub fn version(&self) -> DocumentVersion { + pub(crate) fn version(&self) -> DocumentVersion { self.version } - pub fn language_id(&self) -> Option { + pub(crate) fn language_id(&self) -> Option { self.language_id } @@ -131,7 +131,7 @@ impl TextDocument { }); } - pub fn update_version(&mut self, new_version: DocumentVersion) { + pub(crate) fn update_version(&mut self, new_version: DocumentVersion) { self.modify_with_manual_index(|_, version, _| { *version = new_version; }); diff --git a/crates/ruff_server/src/format.rs b/crates/ruff_server/src/format.rs index 770c2e6e85..0d42d9ffb0 100644 --- a/crates/ruff_server/src/format.rs +++ b/crates/ruff_server/src/format.rs @@ -301,7 +301,7 @@ impl UvFormatCommand { } /// Execute the format command on the given source. - pub(crate) fn format( + fn format( &self, source: &str, path: &Path, @@ -357,12 +357,12 @@ impl UvFormatCommand { } /// Format the entire document. - pub(crate) fn format_document(&self, source: &str, path: &Path) -> crate::Result { + fn format_document(&self, source: &str, path: &Path) -> crate::Result { self.format(source, path, None) } /// Format a specific range. - pub(crate) fn format_range( + fn format_range( &self, source: &str, range: TextRange, diff --git a/crates/ruff_server/src/lib.rs b/crates/ruff_server/src/lib.rs index 2177fe2092..2df7a8bdfd 100644 --- a/crates/ruff_server/src/lib.rs +++ b/crates/ruff_server/src/lib.rs @@ -21,22 +21,22 @@ mod server; mod session; mod workspace; -pub(crate) const SERVER_NAME: &str = "ruff"; +const SERVER_NAME: &str = "ruff"; pub(crate) const DIAGNOSTIC_NAME: &str = "Ruff"; -pub(crate) const SOURCE_FIX_ALL_RUFF: CodeActionKind = CodeActionKind::new("source.fixAll.ruff"); -pub(crate) const SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind = +const SOURCE_FIX_ALL_RUFF: CodeActionKind = CodeActionKind::new("source.fixAll.ruff"); +const SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind = CodeActionKind::new("source.organizeImports.ruff"); -pub(crate) const NOTEBOOK_SOURCE_FIX_ALL_RUFF: CodeActionKind = +const NOTEBOOK_SOURCE_FIX_ALL_RUFF: CodeActionKind = CodeActionKind::new("notebook.source.fixAll.ruff"); -pub(crate) const NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind = +const NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind = CodeActionKind::new("notebook.source.organizeImports.ruff"); /// A common result type used in most cases where a /// result type is needed. pub(crate) type Result = anyhow::Result; -pub(crate) fn version() -> &'static str { +fn version() -> &'static str { ruff_linter::VERSION } diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index 0946eb6b83..1b74b72222 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -39,14 +39,14 @@ use ruff_text_size::{Ranged, TextRange}; #[derive(Serialize, Deserialize, Debug, Clone)] pub(crate) struct AssociatedDiagnosticData { /// The message describing what the fix does, if it exists, or the diagnostic name otherwise. - pub(crate) title: String, + title: String, /// Edits to fix the diagnostic. If this is empty, a fix /// does not exist. - pub(crate) edits: Vec, + edits: Vec, /// The identifier displayed for the diagnostic. - pub(crate) code: String, + code: String, /// Possible edit to add a suppression comment which will disable this diagnostic. - pub(crate) noqa_edit: Option, + noqa_edit: Option, } /// Describes a fix for `fixed_diagnostic` that may have quick fix diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs index 0c695e3188..a949150275 100644 --- a/crates/ruff_server/src/server/api.rs +++ b/crates/ruff_server/src/server/api.rs @@ -386,7 +386,7 @@ impl> LSPResult for core::result::Result { } impl Error { - pub(crate) fn new(err: anyhow::Error, code: server::ErrorCode) -> Self { + fn new(err: anyhow::Error, code: server::ErrorCode) -> Self { Self { code, error: err } } } diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index 3482565ddc..36948e5266 100644 --- a/crates/ruff_server/src/server/api/requests/format.rs +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -77,7 +77,7 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result /// Formats either a full text document or an specific notebook cell. If the query within the snapshot is a notebook document /// with no selected cell, this will throw an error. -pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result { +fn format_document(snapshot: &DocumentSnapshot) -> Result { let text_document = snapshot .query() .as_single_document() diff --git a/crates/ruff_server/src/server/api/requests/hover.rs b/crates/ruff_server/src/server/api/requests/hover.rs index 2b30e67040..5085a02c13 100644 --- a/crates/ruff_server/src/server/api/requests/hover.rs +++ b/crates/ruff_server/src/server/api/requests/hover.rs @@ -43,7 +43,7 @@ impl super::BackgroundDocumentRequestHandler for Hover { } } -pub(crate) fn hover( +fn hover( snapshot: &DocumentSnapshot, position: &types::TextDocumentPositionParams, ) -> Option { diff --git a/crates/ruff_server/src/server/schedule/thread/pool.rs b/crates/ruff_server/src/server/schedule/thread/pool.rs index ac3e072ab8..dcf33500f9 100644 --- a/crates/ruff_server/src/server/schedule/thread/pool.rs +++ b/crates/ruff_server/src/server/schedule/thread/pool.rs @@ -127,7 +127,7 @@ impl Pool { } #[expect(dead_code)] - pub(super) fn len(&self) -> usize { + fn len(&self) -> usize { self.extant_tasks.load(Ordering::SeqCst) } } diff --git a/crates/ruff_server/src/session/client.rs b/crates/ruff_server/src/session/client.rs index e2896e2e57..99316b290f 100644 --- a/crates/ruff_server/src/session/client.rs +++ b/crates/ruff_server/src/session/client.rs @@ -114,7 +114,7 @@ impl Client { /// /// This is useful for notifications that don't require any data. #[expect(dead_code)] - pub(crate) fn send_notification_no_params(&self, method: &str) -> crate::Result<()> { + fn send_notification_no_params(&self, method: &str) -> crate::Result<()> { self.client_sender .send(lsp_server::Message::Notification(Notification::new( method.to_string(), diff --git a/crates/ruff_server/src/session/index.rs b/crates/ruff_server/src/session/index.rs index f920866c89..d979628775 100644 --- a/crates/ruff_server/src/session/index.rs +++ b/crates/ruff_server/src/session/index.rs @@ -512,28 +512,28 @@ impl DocumentController { } } - pub(crate) fn as_notebook_mut(&mut self) -> Option<&mut NotebookDocument> { + fn as_notebook_mut(&mut self) -> Option<&mut NotebookDocument> { Some(match self { Self::Notebook(notebook) => Arc::make_mut(notebook), Self::Text(_) => return None, }) } - pub(crate) fn as_notebook(&self) -> Option<&NotebookDocument> { + fn as_notebook(&self) -> Option<&NotebookDocument> { match self { Self::Notebook(notebook) => Some(notebook), Self::Text(_) => None, } } - pub(crate) fn as_text(&self) -> Option<&TextDocument> { + fn as_text(&self) -> Option<&TextDocument> { match self { Self::Text(document) => Some(document), Self::Notebook(_) => None, } } - pub(crate) fn as_text_mut(&mut self) -> Option<&mut TextDocument> { + fn as_text_mut(&mut self) -> Option<&mut TextDocument> { Some(match self { Self::Text(document) => Arc::make_mut(document), Self::Notebook(_) => return None, @@ -620,7 +620,7 @@ impl DocumentQuery { } /// Get the URI for the document selected by this query. - pub(crate) fn file_uri(&self) -> &Uri { + fn file_uri(&self) -> &Uri { match self { Self::Text { file_uri, .. } | Self::Notebook { file_uri, .. } => file_uri, } diff --git a/crates/ruff_server/src/session/options.rs b/crates/ruff_server/src/session/options.rs index 476379e060..30be0ff0d4 100644 --- a/crates/ruff_server/src/session/options.rs +++ b/crates/ruff_server/src/session/options.rs @@ -58,12 +58,12 @@ pub(crate) struct GlobalOptions { } impl GlobalOptions { - pub(crate) fn set_preview(&mut self, preview: bool) { + fn set_preview(&mut self, preview: bool) { self.client.set_preview(preview); } #[cfg(test)] - pub(crate) fn client(&self) -> &ClientOptions { + fn client(&self) -> &ClientOptions { &self.client } @@ -169,7 +169,7 @@ impl ClientOptions { } /// Update the preview flag for the linter and the formatter with the given value. - pub(crate) fn set_preview(&mut self, preview: bool) { + fn set_preview(&mut self, preview: bool) { match self.lint.as_mut() { None => self.lint = Some(LintOptions::default().with_preview(preview)), Some(lint) => lint.set_preview(preview), diff --git a/crates/ruff_server/src/session/request_queue.rs b/crates/ruff_server/src/session/request_queue.rs index 68696050bf..3abd5d3a87 100644 --- a/crates/ruff_server/src/session/request_queue.rs +++ b/crates/ruff_server/src/session/request_queue.rs @@ -77,7 +77,7 @@ impl Incoming { /// Returns `true` if the request with the given id is still pending. #[expect(dead_code)] - pub(crate) fn is_pending(&self, request_id: &RequestId) -> bool { + fn is_pending(&self, request_id: &RequestId) -> bool { self.pending.contains_key(request_id) } diff --git a/crates/ruff_server/src/workspace.rs b/crates/ruff_server/src/workspace.rs index 056a080992..75d311494b 100644 --- a/crates/ruff_server/src/workspace.rs +++ b/crates/ruff_server/src/workspace.rs @@ -91,7 +91,7 @@ impl Workspace { } /// Create a new default workspace with the given root URI. - pub(crate) fn default(uri: Uri) -> Self { + fn default(uri: Uri) -> Self { Self { uri, options: None, @@ -101,7 +101,7 @@ impl Workspace { /// Set the client options for this workspace. #[must_use] - pub(crate) fn with_options(mut self, options: ClientOptions) -> Self { + fn with_options(mut self, options: ClientOptions) -> Self { self.options = Some(options); self } diff --git a/crates/ruff_source_file/src/line_index.rs b/crates/ruff_source_file/src/line_index.rs index cf4d85c76e..6968839b71 100644 --- a/crates/ruff_source_file/src/line_index.rs +++ b/crates/ruff_source_file/src/line_index.rs @@ -224,7 +224,7 @@ impl LineIndex { } /// Returns `true` if the text only consists of ASCII characters - pub fn is_ascii(&self) -> bool { + fn is_ascii(&self) -> bool { self.kind().is_ascii() } @@ -286,7 +286,7 @@ impl LineIndex { /// Returns the [byte offset](TextSize) of the `line`'s end. /// The offset is the end of the line, excluding the newline character ending the line (if any). - pub fn line_end_exclusive(&self, line: OneIndexed, contents: &str) -> TextSize { + pub(crate) fn line_end_exclusive(&self, line: OneIndexed, contents: &str) -> TextSize { let row_index = line.to_zero_indexed(); let starts = self.line_starts(); @@ -580,7 +580,7 @@ impl OneIndexed { // SAFETY: These constants are being initialized with non-zero values /// The smallest value that can be represented by this integer type. pub const MIN: Self = Self::new(1).unwrap(); - pub const ONE: NonZeroUsize = NonZeroUsize::new(1).unwrap(); + const ONE: NonZeroUsize = NonZeroUsize::new(1).unwrap(); /// Creates a non-zero if the given value is not zero. pub const fn new(value: usize) -> Option { diff --git a/crates/ruff_source_file/src/newlines.rs b/crates/ruff_source_file/src/newlines.rs index 1078750b35..50b6111fe1 100644 --- a/crates/ruff_source_file/src/newlines.rs +++ b/crates/ruff_source_file/src/newlines.rs @@ -269,7 +269,7 @@ impl<'a> Line<'a> { } #[inline] - pub fn full_text_len(&self) -> TextSize { + fn full_text_len(&self) -> TextSize { self.text.text_len() } } diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 3790c9bb4c..5d3a52fdb3 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -695,7 +695,7 @@ impl Configuration { } #[must_use] - pub fn apply_fallbacks( + pub(crate) fn apply_fallbacks( mut self, origin: ConfigurationOrigin, initial_config_path: &Path, @@ -1232,7 +1232,7 @@ impl LintConfiguration { } #[must_use] - pub fn combine(self, config: Self) -> Self { + fn combine(self, config: Self) -> Self { let mut rule_selections = config.rule_selections; rule_selections.extend(self.rule_selections); @@ -1364,7 +1364,7 @@ impl FormatConfiguration { } #[must_use] - pub fn combine(self, config: Self) -> Self { + fn combine(self, config: Self) -> Self { Self { exclude: self.exclude.or(config.exclude), preview: self.preview.or(config.preview), @@ -1425,7 +1425,7 @@ impl AnalyzeConfiguration { } #[must_use] - pub fn combine(self, config: Self) -> Self { + fn combine(self, config: Self) -> Self { Self { exclude: self.exclude.or(config.exclude), preview: self.preview.or(config.preview), @@ -1458,7 +1458,7 @@ impl CombinePluginOptions for Option { /// Given a list of source paths, which could include glob patterns, resolve the /// matching paths. -pub fn resolve_src(src: &[String], project_root: &Path) -> Result> { +fn resolve_src(src: &[String], project_root: &Path) -> Result> { let expansions = src .iter() .map(shellexpand::full) diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index e1403f98e3..33666e2112 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -606,7 +606,7 @@ pub struct LintOptions { pub future_annotations: Option, } -pub fn validate_required_version(required_version: &RequiredVersion) -> anyhow::Result<()> { +pub(crate) fn validate_required_version(required_version: &RequiredVersion) -> anyhow::Result<()> { let ruff_pkg_version = pep440_rs::Version::from_str(RUFF_PKG_VERSION) .expect("RUFF_PKG_VERSION is not a valid PEP 440 version specifier"); if !required_version.contains(&ruff_pkg_version) { @@ -620,7 +620,7 @@ pub fn validate_required_version(required_version: &RequiredVersion) -> anyhow:: /// Newtype wrapper for [`LintCommonOptions`] that allows customizing the JSON schema and omitting the fields from the [`OptionsMetadata`]. #[derive(Clone, Debug, PartialEq, Eq, Default, Serialize)] #[serde(transparent)] -pub struct DeprecatedTopLevelLintOptions(pub LintCommonOptions); +pub struct DeprecatedTopLevelLintOptions(pub(crate) LintCommonOptions); impl<'de> Deserialize<'de> for DeprecatedTopLevelLintOptions { fn deserialize(deserializer: D) -> Result @@ -1118,7 +1118,7 @@ pub struct Flake8AnnotationsOptions { value_type = "bool", example = "mypy-init-return = true" )] - pub mypy_init_return: Option, + mypy_init_return: Option, /// Whether to suppress `ANN000`-level violations for arguments matching the /// "dummy" variable regex (like `_`). @@ -1127,7 +1127,7 @@ pub struct Flake8AnnotationsOptions { value_type = "bool", example = "suppress-dummy-args = true" )] - pub suppress_dummy_args: Option, + suppress_dummy_args: Option, /// Whether to suppress `ANN200`-level violations for functions that meet /// either of the following criteria: @@ -1140,7 +1140,7 @@ pub struct Flake8AnnotationsOptions { value_type = "bool", example = "suppress-none-returning = true" )] - pub suppress_none_returning: Option, + suppress_none_returning: Option, /// Whether to suppress `ANN401` for dynamically typed `*args` and /// `**kwargs` arguments. @@ -1149,7 +1149,7 @@ pub struct Flake8AnnotationsOptions { value_type = "bool", example = "allow-star-arg-any = true" )] - pub allow_star_arg_any: Option, + allow_star_arg_any: Option, /// Whether to suppress `ANN*` rules for any declaration /// that hasn't been typed at all. @@ -1159,11 +1159,13 @@ pub struct Flake8AnnotationsOptions { value_type = "bool", example = "ignore-fully-untyped = true" )] - pub ignore_fully_untyped: Option, + ignore_fully_untyped: Option, } impl Flake8AnnotationsOptions { - pub fn into_settings(self) -> ruff_linter::rules::flake8_annotations::settings::Settings { + pub(crate) fn into_settings( + self, + ) -> ruff_linter::rules::flake8_annotations::settings::Settings { ruff_linter::rules::flake8_annotations::settings::Settings { mypy_init_return: self.mypy_init_return.unwrap_or(false), suppress_dummy_args: self.suppress_dummy_args.unwrap_or(false), @@ -1187,7 +1189,7 @@ pub struct Flake8BanditOptions { value_type = "list[str]", example = "hardcoded-tmp-directory = [\"/foo/bar\"]" )] - pub hardcoded_tmp_directory: Option>, + hardcoded_tmp_directory: Option>, /// A list of directories to consider temporary, in addition to those /// specified by [`hardcoded-tmp-directory`](#lint_flake8-bandit_hardcoded-tmp-directory) (see `S108`). @@ -1196,7 +1198,7 @@ pub struct Flake8BanditOptions { value_type = "list[str]", example = "hardcoded-tmp-directory-extend = [\"/foo/bar\"]" )] - pub hardcoded_tmp_directory_extend: Option>, + hardcoded_tmp_directory_extend: Option>, /// Whether to disallow `try`-`except`-`pass` (`S110`) for specific /// exception types. By default, `try`-`except`-`pass` is only @@ -1206,7 +1208,7 @@ pub struct Flake8BanditOptions { value_type = "bool", example = "check-typed-exception = true" )] - pub check_typed_exception: Option, + check_typed_exception: Option, /// A list of additional callable names that behave like /// [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup). @@ -1218,7 +1220,7 @@ pub struct Flake8BanditOptions { value_type = "list[str]", example = "extend-markup-names = [\"webhelpers.html.literal\", \"my_package.Markup\"]" )] - pub extend_markup_names: Option>, + extend_markup_names: Option>, /// A list of callable names, whose result may be safely passed into /// [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup). @@ -1250,11 +1252,11 @@ pub struct Flake8BanditOptions { value_type = "list[str]", example = "allowed-markup-calls = [\"bleach.clean\", \"my_package.sanitize\"]" )] - pub allowed_markup_calls: Option>, + allowed_markup_calls: Option>, } impl Flake8BanditOptions { - pub fn into_settings( + pub(crate) fn into_settings( self, ruff_options: Option<&RuffOptions>, ) -> ruff_linter::rules::flake8_bandit::settings::Settings { @@ -1300,11 +1302,13 @@ pub struct Flake8BooleanTrapOptions { value_type = "list[str]", example = "extend-allowed-calls = [\"pydantic.Field\", \"django.db.models.Value\"]" )] - pub extend_allowed_calls: Option>, + extend_allowed_calls: Option>, } impl Flake8BooleanTrapOptions { - pub fn into_settings(self) -> ruff_linter::rules::flake8_boolean_trap::settings::Settings { + pub(crate) fn into_settings( + self, + ) -> ruff_linter::rules::flake8_boolean_trap::settings::Settings { ruff_linter::rules::flake8_boolean_trap::settings::Settings { extend_allowed_calls: self.extend_allowed_calls.unwrap_or_default(), } @@ -1332,11 +1336,11 @@ pub struct Flake8BugbearOptions { extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"] "# )] - pub extend_immutable_calls: Option>, + extend_immutable_calls: Option>, } impl Flake8BugbearOptions { - pub fn into_settings(self) -> ruff_linter::rules::flake8_bugbear::settings::Settings { + pub(crate) fn into_settings(self) -> ruff_linter::rules::flake8_bugbear::settings::Settings { ruff_linter::rules::flake8_bugbear::settings::Settings { extend_immutable_calls: self.extend_immutable_calls.unwrap_or_default(), } @@ -1364,7 +1368,7 @@ pub struct Flake8BuiltinsOptions { since = "0.10.0", note = "`builtins-ignorelist` has been renamed to `ignorelist`. Use that instead." )] - pub builtins_ignorelist: Option>, + pub(crate) builtins_ignorelist: Option>, /// Ignore list of builtins. #[option( @@ -1372,7 +1376,7 @@ pub struct Flake8BuiltinsOptions { value_type = "list[str]", example = "ignorelist = [\"id\"]" )] - pub ignorelist: Option>, + pub(crate) ignorelist: Option>, /// DEPRECATED: This option has been renamed to `allowed-modules`. Use `allowed-modules` instead. /// @@ -1388,7 +1392,7 @@ pub struct Flake8BuiltinsOptions { since = "0.10.0", note = "`builtins-allowed-modules` has been renamed to `allowed-modules`. Use that instead." )] - pub builtins_allowed_modules: Option>, + pub(crate) builtins_allowed_modules: Option>, /// List of builtin module names to allow. #[option( @@ -1396,7 +1400,7 @@ pub struct Flake8BuiltinsOptions { value_type = "list[str]", example = "allowed-modules = [\"secrets\"]" )] - pub allowed_modules: Option>, + pub(crate) allowed_modules: Option>, /// DEPRECATED: This option has been renamed to `strict-checking`. Use `strict-checking` instead. /// @@ -1412,7 +1416,7 @@ pub struct Flake8BuiltinsOptions { since = "0.10.0", note = "`builtins-strict-checking` has been renamed to `strict-checking`. Use that instead." )] - pub builtins_strict_checking: Option, + pub(crate) builtins_strict_checking: Option, /// Compare module names instead of full module paths. /// @@ -1422,11 +1426,11 @@ pub struct Flake8BuiltinsOptions { value_type = "bool", example = "strict-checking = true" )] - pub strict_checking: Option, + pub(crate) strict_checking: Option, } impl Flake8BuiltinsOptions { - pub fn into_settings(self) -> ruff_linter::rules::flake8_builtins::settings::Settings { + pub(crate) fn into_settings(self) -> ruff_linter::rules::flake8_builtins::settings::Settings { #[expect(deprecated)] ruff_linter::rules::flake8_builtins::settings::Settings { ignorelist: self @@ -1459,11 +1463,13 @@ pub struct Flake8ComprehensionsOptions { value_type = "bool", example = "allow-dict-calls-with-keyword-arguments = true" )] - pub allow_dict_calls_with_keyword_arguments: Option, + allow_dict_calls_with_keyword_arguments: Option, } impl Flake8ComprehensionsOptions { - pub fn into_settings(self) -> ruff_linter::rules::flake8_comprehensions::settings::Settings { + pub(crate) fn into_settings( + self, + ) -> ruff_linter::rules::flake8_comprehensions::settings::Settings { ruff_linter::rules::flake8_comprehensions::settings::Settings { allow_dict_calls_with_keyword_arguments: self .allow_dict_calls_with_keyword_arguments @@ -1494,12 +1500,12 @@ pub struct Flake8CopyrightOptions { value_type = "str", example = r#"notice-rgx = "(?i)Copyright \\(C\\) \\d{4}""# )] - pub notice_rgx: Option, + notice_rgx: Option, /// Author to enforce within the copyright notice. If provided, the /// author must be present immediately following the copyright notice. #[option(default = "null", value_type = "str", example = r#"author = "Ruff""#)] - pub author: Option, + author: Option, /// A minimum file size (in bytes) required for a copyright notice to /// be enforced. By default, all files are validated. @@ -1511,11 +1517,11 @@ pub struct Flake8CopyrightOptions { min-file-size = 1024 "# )] - pub min_file_size: Option, + min_file_size: Option, } impl Flake8CopyrightOptions { - pub fn try_into_settings(self) -> anyhow::Result { + pub(crate) fn try_into_settings(self) -> anyhow::Result { Ok(flake8_copyright::settings::Settings { notice_rgx: self .notice_rgx @@ -1537,11 +1543,11 @@ impl Flake8CopyrightOptions { pub struct Flake8ErrMsgOptions { /// Maximum string length for string literals in exception messages. #[option(default = "0", value_type = "int", example = "max-string-length = 20")] - pub max_string_length: Option, + max_string_length: Option, } impl Flake8ErrMsgOptions { - pub fn into_settings(self) -> flake8_errmsg::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_errmsg::settings::Settings { flake8_errmsg::settings::Settings { max_string_length: self.max_string_length.unwrap_or_default(), } @@ -1561,7 +1567,7 @@ pub struct Flake8GetTextOptions { value_type = "list[str]", example = r#"function-names = ["_", "gettext", "ngettext", "ugettetxt"]"# )] - pub function_names: Option>, + function_names: Option>, /// Additional function names to consider as internationalization calls, in addition to those /// included in [`function-names`](#lint_flake8-gettext_function-names). @@ -1570,11 +1576,11 @@ pub struct Flake8GetTextOptions { value_type = "list[str]", example = r#"extend-function-names = ["ugettetxt"]"# )] - pub extend_function_names: Option>, + extend_function_names: Option>, } impl Flake8GetTextOptions { - pub fn into_settings(self) -> flake8_gettext::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_gettext::settings::Settings { flake8_gettext::settings::Settings { function_names: self .function_names @@ -1610,11 +1616,11 @@ pub struct Flake8ImplicitStrConcatOptions { allow-multiline = false "# )] - pub allow_multiline: Option, + allow_multiline: Option, } impl Flake8ImplicitStrConcatOptions { - pub fn into_settings(self) -> flake8_implicit_str_concat::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_implicit_str_concat::settings::Settings { flake8_implicit_str_concat::settings::Settings { allow_multiline: self.allow_multiline.unwrap_or(true), } @@ -1644,7 +1650,7 @@ pub struct Flake8ImportConventionsOptions { scipy = "sp" "# )] - pub aliases: Option>, + aliases: Option>, /// A mapping from module to conventional import alias. These aliases will /// be added to the [`aliases`](#lint_flake8-import-conventions_aliases) mapping @@ -1658,7 +1664,7 @@ pub struct Flake8ImportConventionsOptions { "dask.dataframe" = "dd" "# )] - pub extend_aliases: Option>, + extend_aliases: Option>, /// A mapping from module to its banned import aliases. #[option( @@ -1670,7 +1676,7 @@ pub struct Flake8ImportConventionsOptions { "tensorflow.keras.backend" = ["K"] "# )] - pub banned_aliases: Option>, + banned_aliases: Option>, /// A list of modules that should not be imported from using the /// `from ... import ...` syntax. @@ -1685,7 +1691,7 @@ pub struct Flake8ImportConventionsOptions { banned-from = ["typing"] "# )] - pub banned_from: Option>, + banned_from: Option>, } #[derive(Clone, Debug, PartialEq, Eq, Hash, Default, Serialize)] @@ -1693,7 +1699,7 @@ pub struct Flake8ImportConventionsOptions { pub struct ModuleName(String); impl ModuleName { - pub fn into_string(self) -> String { + fn into_string(self) -> String { self.0 } } @@ -1720,7 +1726,7 @@ impl<'de> Deserialize<'de> for ModuleName { pub struct Alias(String); impl Alias { - pub fn into_string(self) -> String { + fn into_string(self) -> String { self.0 } } @@ -1752,7 +1758,7 @@ impl<'de> Deserialize<'de> for Alias { } impl Flake8ImportConventionsOptions { - pub fn try_into_settings( + pub(crate) fn try_into_settings( self, preview: PreviewMode, ) -> anyhow::Result { @@ -1815,7 +1821,7 @@ pub struct Flake8PytestStyleOptions { value_type = "bool", example = "fixture-parentheses = true" )] - pub fixture_parentheses: Option, + fixture_parentheses: Option, /// Expected type for multiple argument names in `@pytest.mark.parametrize`. /// The following values are supported: @@ -1830,7 +1836,7 @@ pub struct Flake8PytestStyleOptions { value_type = r#""csv" | "tuple" | "list""#, example = "parametrize-names-type = \"list\"" )] - pub parametrize_names_type: Option, + parametrize_names_type: Option, /// Expected type for the list of values rows in `@pytest.mark.parametrize`. /// The following values are supported: @@ -1842,7 +1848,7 @@ pub struct Flake8PytestStyleOptions { value_type = r#""tuple" | "list""#, example = "parametrize-values-type = \"tuple\"" )] - pub parametrize_values_type: Option, + parametrize_values_type: Option, /// Expected type for each row of values in `@pytest.mark.parametrize` in /// case of multiple parameters. The following values are supported: @@ -1856,7 +1862,7 @@ pub struct Flake8PytestStyleOptions { value_type = r#""tuple" | "list""#, example = "parametrize-values-row-type = \"list\"" )] - pub parametrize_values_row_type: Option, + parametrize_values_row_type: Option, /// List of exception names that require a match= parameter in a /// `pytest.raises()` call. @@ -1868,7 +1874,7 @@ pub struct Flake8PytestStyleOptions { value_type = "list[str]", example = "raises-require-match-for = [\"requests.RequestException\"]" )] - pub raises_require_match_for: Option>, + raises_require_match_for: Option>, /// List of additional exception names that require a match= parameter in a /// `pytest.raises()` call. This extends the default list of exceptions @@ -1886,7 +1892,7 @@ pub struct Flake8PytestStyleOptions { value_type = "list[str]", example = "raises-extend-require-match-for = [\"requests.RequestException\"]" )] - pub raises_extend_require_match_for: Option>, + raises_extend_require_match_for: Option>, /// Boolean flag specifying whether `@pytest.mark.foo()` without parameters /// should have parentheses. If the option is set to `false` (the @@ -1898,7 +1904,7 @@ pub struct Flake8PytestStyleOptions { value_type = "bool", example = "mark-parentheses = true" )] - pub mark_parentheses: Option, + mark_parentheses: Option, /// List of warning names that require a match= parameter in a /// `pytest.warns()` call. @@ -1910,7 +1916,7 @@ pub struct Flake8PytestStyleOptions { value_type = "list[str]", example = "warns-require-match-for = [\"requests.RequestsWarning\"]" )] - pub warns_require_match_for: Option>, + warns_require_match_for: Option>, /// List of additional warning names that require a match= parameter in a /// `pytest.warns()` call. This extends the default list of warnings that @@ -1930,11 +1936,13 @@ pub struct Flake8PytestStyleOptions { value_type = "list[str]", example = "warns-extend-require-match-for = [\"requests.RequestsWarning\"]" )] - pub warns_extend_require_match_for: Option>, + warns_extend_require_match_for: Option>, } impl Flake8PytestStyleOptions { - pub fn try_into_settings(self) -> anyhow::Result { + pub(crate) fn try_into_settings( + self, + ) -> anyhow::Result { Ok(flake8_pytest_style::settings::Settings { fixture_parentheses: self.fixture_parentheses.unwrap_or_default(), parametrize_names_type: self.parametrize_names_type.unwrap_or_default(), @@ -2008,7 +2016,7 @@ pub struct Flake8QuotesOptions { inline-quotes = "single" "# )] - pub inline_quotes: Option, + inline_quotes: Option, /// Quote style to prefer for multiline strings (either "single" or /// "double"). @@ -2022,7 +2030,7 @@ pub struct Flake8QuotesOptions { multiline-quotes = "single" "# )] - pub multiline_quotes: Option, + multiline_quotes: Option, /// Quote style to prefer for docstrings (either "single" or "double"). /// @@ -2035,7 +2043,7 @@ pub struct Flake8QuotesOptions { docstring-quotes = "single" "# )] - pub docstring_quotes: Option, + docstring_quotes: Option, /// Whether to avoid using single quotes if a string contains single quotes, /// or vice-versa with double quotes, as per [PEP 8](https://peps.python.org/pep-0008/#string-quotes). @@ -2048,11 +2056,11 @@ pub struct Flake8QuotesOptions { avoid-escape = false "# )] - pub avoid_escape: Option, + avoid_escape: Option, } impl Flake8QuotesOptions { - pub fn into_settings(self) -> flake8_quotes::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_quotes::settings::Settings { flake8_quotes::settings::Settings { inline_quotes: self.inline_quotes.unwrap_or_default(), multiline_quotes: self.multiline_quotes.unwrap_or_default(), @@ -2077,7 +2085,7 @@ pub struct Flake8SelfOptions { ignore-names = ["_new"] "# )] - pub ignore_names: Option>, + ignore_names: Option>, /// Additional names to ignore when considering `flake8-self` violations, /// in addition to those included in [`ignore-names`](#lint_flake8-self_ignore-names). @@ -2086,11 +2094,11 @@ pub struct Flake8SelfOptions { value_type = "list[str]", example = r#"extend-ignore-names = ["_base_manager", "_default_manager", "_meta"]"# )] - pub extend_ignore_names: Option>, + extend_ignore_names: Option>, } impl Flake8SelfOptions { - pub fn into_settings(self) -> flake8_self::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_self::settings::Settings { let defaults = flake8_self::settings::Settings::default(); flake8_self::settings::Settings { ignore_names: self @@ -2120,7 +2128,7 @@ pub struct Flake8TidyImportsOptions { ban-relative-imports = "all" "# )] - pub ban_relative_imports: Option, + ban_relative_imports: Option, /// Specific modules or module members that may not be imported or accessed. /// Note that this rule is only meant to flag accidental uses, @@ -2134,7 +2142,7 @@ pub struct Flake8TidyImportsOptions { "typing.TypedDict".msg = "Use typing_extensions.TypedDict instead." "# )] - pub banned_api: Option>, + banned_api: Option>, /// List of specific modules that may not be imported at module level, and should instead be /// imported lazily (e.g., within a function definition, or an `if TYPE_CHECKING:` @@ -2149,7 +2157,7 @@ pub struct Flake8TidyImportsOptions { banned-module-level-imports = ["torch", "tensorflow"] "# )] - pub banned_module_level_imports: Option>, + banned_module_level_imports: Option>, /// Specific modules that must be imported lazily in contexts where `lazy import` is legal, or /// `"all"` to require every lazily-convertible import to use the `lazy` keyword. Ruff ignores @@ -2167,7 +2175,7 @@ pub struct Flake8TidyImportsOptions { require-lazy = { include = "all", exclude = ["sitecustomize"] } "# )] - pub require_lazy: Option, + require_lazy: Option, /// Specific modules that may not be imported lazily, or `"all"` to forbid lazy imports except /// for any modules excluded from the selector. This rule is only enforced when targeting @@ -2183,11 +2191,11 @@ pub struct Flake8TidyImportsOptions { ban-lazy = { include = "all", exclude = ["typing"] } "# )] - pub ban_lazy: Option, + ban_lazy: Option, } impl Flake8TidyImportsOptions { - pub fn try_into_settings(self) -> Result { + pub(crate) fn try_into_settings(self) -> Result { let require_lazy = self.require_lazy.unwrap_or_default(); let ban_lazy = self.ban_lazy.unwrap_or_default(); @@ -2282,7 +2290,7 @@ pub struct Flake8TypeCheckingOptions { strict = true "# )] - pub strict: Option, + strict: Option, /// Exempt certain modules from needing to be moved into type-checking /// blocks. @@ -2293,7 +2301,7 @@ pub struct Flake8TypeCheckingOptions { exempt-modules = ["typing", "typing_extensions"] "# )] - pub exempt_modules: Option>, + exempt_modules: Option>, /// Exempt classes that list any of the enumerated classes as a base class /// from needing to be moved into type-checking blocks. @@ -2312,7 +2320,7 @@ pub struct Flake8TypeCheckingOptions { runtime-evaluated-base-classes = ["pydantic.BaseModel", "sqlalchemy.orm.DeclarativeBase"] "# )] - pub runtime_evaluated_base_classes: Option>, + runtime_evaluated_base_classes: Option>, /// Exempt classes and functions decorated with any of the enumerated /// decorators from being moved into type-checking blocks. @@ -2341,7 +2349,7 @@ pub struct Flake8TypeCheckingOptions { runtime-evaluated-decorators = ["pydantic.validate_call", "attrs.define"] "# )] - pub runtime_evaluated_decorators: Option>, + runtime_evaluated_decorators: Option>, /// Whether to add quotes around type annotations, if doing so would allow /// the corresponding import to be moved into a type-checking block. @@ -2393,11 +2401,11 @@ pub struct Flake8TypeCheckingOptions { quote-annotations = true "# )] - pub quote_annotations: Option, + quote_annotations: Option, } impl Flake8TypeCheckingOptions { - pub fn into_settings(self) -> flake8_type_checking::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_type_checking::settings::Settings { flake8_type_checking::settings::Settings { strict: self.strict.unwrap_or(false), exempt_modules: self @@ -2423,11 +2431,11 @@ pub struct Flake8UnusedArgumentsOptions { value_type = "bool", example = "ignore-variadic-names = true" )] - pub ignore_variadic_names: Option, + ignore_variadic_names: Option, } impl Flake8UnusedArgumentsOptions { - pub fn into_settings(self) -> flake8_unused_arguments::settings::Settings { + pub(crate) fn into_settings(self) -> flake8_unused_arguments::settings::Settings { flake8_unused_arguments::settings::Settings { ignore_variadic_names: self.ignore_variadic_names.unwrap_or_default(), } @@ -2468,7 +2476,7 @@ pub struct IsortOptions { combine-as-imports = true "# )] - pub force_wrap_aliases: Option, + force_wrap_aliases: Option, /// Forces all from imports to appear on their own line. #[option( @@ -2476,7 +2484,7 @@ pub struct IsortOptions { value_type = "bool", example = r#"force-single-line = true"# )] - pub force_single_line: Option, + force_single_line: Option, /// One or more modules to exclude from the single line rule. #[option( @@ -2486,7 +2494,7 @@ pub struct IsortOptions { single-line-exclusions = ["os", "json"] "# )] - pub single_line_exclusions: Option>, + single_line_exclusions: Option>, /// Combines as imports on the same line. See isort's [`combine-as-imports`](https://pycqa.github.io/isort/docs/configuration/options.html#combine-as-imports) /// option. @@ -2497,7 +2505,7 @@ pub struct IsortOptions { combine-as-imports = true "# )] - pub combine_as_imports: Option, + combine_as_imports: Option, /// If a comma is placed after the last member in a multi-line import, then /// the imports will never be folded into one line. @@ -2513,7 +2521,7 @@ pub struct IsortOptions { split-on-trailing-comma = false "# )] - pub split_on_trailing_comma: Option, + split_on_trailing_comma: Option, /// Order imports by type, which is determined by case, in addition to /// alphabetically. @@ -2527,7 +2535,7 @@ pub struct IsortOptions { order-by-type = true "# )] - pub order_by_type: Option, + order_by_type: Option, /// Don't sort straight-style imports (like `import sys`) before from-style /// imports (like `from itertools import groupby`). Instead, sort the @@ -2539,7 +2547,7 @@ pub struct IsortOptions { force-sort-within-sections = true "# )] - pub force_sort_within_sections: Option, + force_sort_within_sections: Option, /// Sort imports taking into account case sensitivity. /// @@ -2552,7 +2560,7 @@ pub struct IsortOptions { case-sensitive = true "# )] - pub case_sensitive: Option, + case_sensitive: Option, /// Force specific imports to the top of their appropriate section. #[option( @@ -2562,7 +2570,7 @@ pub struct IsortOptions { force-to-top = ["src"] "# )] - pub force_to_top: Option>, + force_to_top: Option>, /// A list of modules to consider first-party, regardless of whether they /// can be identified as such via introspection of the local filesystem. @@ -2576,7 +2584,7 @@ pub struct IsortOptions { known-first-party = ["src"] "# )] - pub known_first_party: Option>, + known_first_party: Option>, /// A list of modules to consider third-party, regardless of whether they /// can be identified as such via introspection of the local filesystem. @@ -2590,7 +2598,7 @@ pub struct IsortOptions { known-third-party = ["src"] "# )] - pub known_third_party: Option>, + known_third_party: Option>, /// A list of modules to consider being a local folder. /// Generally, this is reserved for relative imports (`from . import module`). @@ -2604,7 +2612,7 @@ pub struct IsortOptions { known-local-folder = ["src"] "# )] - pub known_local_folder: Option>, + known_local_folder: Option>, /// A list of modules to consider standard-library, in addition to those /// known to Ruff in advance. @@ -2618,7 +2626,7 @@ pub struct IsortOptions { extra-standard-library = ["path"] "# )] - pub extra_standard_library: Option>, + extra_standard_library: Option>, /// Whether to place "closer" imports (fewer `.` characters, most local) /// before "further" imports (more `.` characters, least local), or vice @@ -2635,7 +2643,7 @@ pub struct IsortOptions { relative-imports-order = "closest-to-furthest" "# )] - pub relative_imports_order: Option, + relative_imports_order: Option, /// Add the specified import line to all files. #[option( @@ -2645,7 +2653,7 @@ pub struct IsortOptions { required-imports = ["from __future__ import annotations"] "# )] - pub required_imports: Option>, + required_imports: Option>, /// An override list of tokens to always recognize as a Class for /// [`order-by-type`](#lint_isort_order-by-type) regardless of casing. @@ -2656,7 +2664,7 @@ pub struct IsortOptions { classes = ["SVC"] "# )] - pub classes: Option>, + classes: Option>, /// An override list of tokens to always recognize as a CONSTANT /// for [`order-by-type`](#lint_isort_order-by-type) regardless of casing. @@ -2667,7 +2675,7 @@ pub struct IsortOptions { constants = ["constant"] "# )] - pub constants: Option>, + constants: Option>, /// An override list of tokens to always recognize as a var /// for [`order-by-type`](#lint_isort_order-by-type) regardless of casing. @@ -2678,7 +2686,7 @@ pub struct IsortOptions { variables = ["VAR"] "# )] - pub variables: Option>, + variables: Option>, /// A list of sections that should _not_ be delineated from the previous /// section via empty lines. @@ -2689,7 +2697,7 @@ pub struct IsortOptions { no-lines-before = ["future", "standard-library"] "# )] - pub no_lines_before: Option>, + no_lines_before: Option>, /// A mapping from import section names to their heading comments. /// @@ -2710,7 +2718,7 @@ pub struct IsortOptions { local-folder = "Local folder imports" "# )] - pub import_heading: Option>, + import_heading: Option>, /// The number of blank lines to place after imports. /// Use `-1` for automatic determination. @@ -2728,7 +2736,7 @@ pub struct IsortOptions { lines-after-imports = 1 "# )] - pub lines_after_imports: Option, + lines_after_imports: Option, /// The number of lines to place between "direct" and `import from` imports. /// @@ -2742,7 +2750,7 @@ pub struct IsortOptions { lines-between-types = 1 "# )] - pub lines_between_types: Option, + lines_between_types: Option, /// A list of modules to separate into auxiliary block(s) of imports, /// in the order specified. @@ -2753,7 +2761,7 @@ pub struct IsortOptions { forced-separate = ["tests"] "# )] - pub forced_separate: Option>, + forced_separate: Option>, /// Override in which order the sections should be output. Can be used to move custom sections. #[option( @@ -2763,7 +2771,7 @@ pub struct IsortOptions { section-order = ["future", "standard-library", "first-party", "local-folder", "third-party"] "# )] - pub section_order: Option>, + section_order: Option>, /// Define a default section for any imports that don't fit into the specified [`section-order`](#lint_isort_section-order). #[option( @@ -2773,7 +2781,7 @@ pub struct IsortOptions { default-section = "first-party" "# )] - pub default_section: Option, + default_section: Option, /// Put all imports into the same section bucket. /// @@ -2800,7 +2808,7 @@ pub struct IsortOptions { no-sections = true "# )] - pub no_sections: Option, + no_sections: Option, /// Whether to automatically mark imports from within the same package as first-party. /// For example, when `detect-same-package = true`, then when analyzing files within the @@ -2816,7 +2824,7 @@ pub struct IsortOptions { detect-same-package = false "# )] - pub detect_same_package: Option, + detect_same_package: Option, /// Whether to place `import from` imports before straight imports when sorting. /// @@ -2842,7 +2850,7 @@ pub struct IsortOptions { from-first = true "# )] - pub from_first: Option, + from_first: Option, /// Sort imports by their string length, such that shorter imports appear /// before longer imports. For example, by default, imports will be sorted @@ -2865,7 +2873,7 @@ pub struct IsortOptions { length-sort = true "# )] - pub length_sort: Option, + length_sort: Option, /// Sort straight imports by their string length. Similar to [`length-sort`](#lint_isort_length-sort), /// but applies only to straight imports and doesn't affect `from` imports. @@ -2876,7 +2884,7 @@ pub struct IsortOptions { length-sort-straight = true "# )] - pub length_sort_straight: Option, + length_sort_straight: Option, // Tables are required to go last. /// A list of mappings from section names to modules. @@ -2920,11 +2928,11 @@ pub struct IsortOptions { "django" = ["django"] "# )] - pub sections: Option>>, + sections: Option>>, } impl IsortOptions { - pub fn try_into_settings( + pub(crate) fn try_into_settings( self, ) -> Result { // Verify that if `no_sections` is set, then `section_order` is empty. @@ -3144,11 +3152,11 @@ pub struct McCabeOptions { max-complexity = 5 "# )] - pub max_complexity: Option, + max_complexity: Option, } impl McCabeOptions { - pub fn into_settings(self) -> mccabe::settings::Settings { + pub(crate) fn into_settings(self) -> mccabe::settings::Settings { mccabe::settings::Settings { max_complexity: self .max_complexity @@ -3176,7 +3184,7 @@ pub struct Pep8NamingOptions { ignore-names = ["callMethod"] "# )] - pub ignore_names: Option>, + ignore_names: Option>, /// Additional names (or patterns) to ignore when considering `pep8-naming` violations, /// in addition to those included in [`ignore-names`](#lint_pep8-naming_ignore-names). @@ -3189,7 +3197,7 @@ pub struct Pep8NamingOptions { value_type = "list[str]", example = r#"extend-ignore-names = ["callMethod"]"# )] - pub extend_ignore_names: Option>, + extend_ignore_names: Option>, /// A list of decorators that, when applied to a method, indicate that the /// method should be treated as a class method (in addition to the builtin @@ -3215,7 +3223,7 @@ pub struct Pep8NamingOptions { ] "# )] - pub classmethod_decorators: Option>, + classmethod_decorators: Option>, /// A list of decorators that, when applied to a method, indicate that the /// method should be treated as a static method (in addition to the builtin @@ -3235,11 +3243,11 @@ pub struct Pep8NamingOptions { staticmethod-decorators = ["belay.Device.teardown"] "# )] - pub staticmethod_decorators: Option>, + staticmethod_decorators: Option>, } impl Pep8NamingOptions { - pub fn try_into_settings( + pub(crate) fn try_into_settings( self, ) -> Result { Ok(pep8_naming::settings::Settings { @@ -3316,7 +3324,10 @@ pub struct PycodestyleOptions { } impl PycodestyleOptions { - pub fn into_settings(self, global_line_length: LineLength) -> pycodestyle::settings::Settings { + pub(crate) fn into_settings( + self, + global_line_length: LineLength, + ) -> pycodestyle::settings::Settings { pycodestyle::settings::Settings { max_doc_length: self.max_doc_length, max_line_length: self.max_line_length.unwrap_or(global_line_length), @@ -3424,7 +3435,7 @@ pub struct PydocstyleOptions { convention = "google" "# )] - pub convention: Option, + pub(crate) convention: Option, /// Ignore docstrings for functions or methods decorated with the /// specified fully-qualified decorators. @@ -3435,7 +3446,7 @@ pub struct PydocstyleOptions { ignore-decorators = ["typing.overload"] "# )] - pub ignore_decorators: Option>, + pub(crate) ignore_decorators: Option>, /// A list of decorators that, when applied to a method, indicate that the /// method should be treated as a property (in addition to the builtin @@ -3450,7 +3461,7 @@ pub struct PydocstyleOptions { property-decorators = ["gi.repository.GObject.Property"] "# )] - pub property_decorators: Option>, + pub(crate) property_decorators: Option>, /// If set to `true`, ignore missing documentation for `*args` and `**kwargs` parameters. #[option( @@ -3460,11 +3471,11 @@ pub struct PydocstyleOptions { ignore-var-parameters = true "# )] - pub ignore_var_parameters: Option, + pub(crate) ignore_var_parameters: Option, } impl PydocstyleOptions { - pub fn into_settings(self) -> pydocstyle::settings::Settings { + pub(crate) fn into_settings(self) -> pydocstyle::settings::Settings { let PydocstyleOptions { convention, ignore_decorators, @@ -3499,11 +3510,11 @@ pub struct PydoclintOptions { ignore-one-line-docstrings = true "# )] - pub ignore_one_line_docstrings: Option, + ignore_one_line_docstrings: Option, } impl PydoclintOptions { - pub fn into_settings(self) -> pydoclint::settings::Settings { + pub(crate) fn into_settings(self) -> pydoclint::settings::Settings { pydoclint::settings::Settings { ignore_one_line_docstrings: self.ignore_one_line_docstrings.unwrap_or_default(), } @@ -3528,7 +3539,7 @@ pub struct PyflakesOptions { value_type = "list[str]", example = "extend-generics = [\"django.db.models.ForeignKey\"]" )] - pub extend_generics: Option>, + extend_generics: Option>, /// A list of modules to ignore when considering unused imports. /// @@ -3543,11 +3554,11 @@ pub struct PyflakesOptions { value_type = "list[str]", example = r#"allowed-unused-imports = ["hvplot.pandas"]"# )] - pub allowed_unused_imports: Option>, + allowed_unused_imports: Option>, } impl PyflakesOptions { - pub fn into_settings(self) -> pyflakes::settings::Settings { + pub(crate) fn into_settings(self) -> pyflakes::settings::Settings { pyflakes::settings::Settings { extend_generics: self.extend_generics.unwrap_or_default(), allowed_unused_imports: self.allowed_unused_imports.unwrap_or_default(), @@ -3570,7 +3581,7 @@ pub struct PylintOptions { allow-magic-value-types = ["int"] "# )] - pub allow_magic_value_types: Option>, + allow_magic_value_types: Option>, /// Dunder methods name to allow, in addition to the default set from the /// Python standard library (see `PLW3201`). @@ -3581,21 +3592,21 @@ pub struct PylintOptions { allow-dunder-method-names = ["__tablename__", "__table_args__"] "# )] - pub allow_dunder_method_names: Option>, + allow_dunder_method_names: Option>, /// Maximum number of branches allowed for a function or method body (see `PLR0912`). #[option(default = r"12", value_type = "int", example = r"max-branches = 15")] - pub max_branches: Option, + max_branches: Option, /// Maximum number of return statements allowed for a function or method /// body (see `PLR0911`) #[option(default = r"6", value_type = "int", example = r"max-returns = 10")] - pub max_returns: Option, + max_returns: Option, /// Maximum number of arguments allowed for a function or method definition /// (see `PLR0913`). #[option(default = r"5", value_type = "int", example = r"max-args = 10")] - pub max_args: Option, + max_args: Option, /// Maximum number of positional arguments allowed for a function or method definition /// (see `PLR0917`). @@ -3606,15 +3617,15 @@ pub struct PylintOptions { value_type = "int", example = r"max-positional-args = 3" )] - pub max_positional_args: Option, + max_positional_args: Option, /// Maximum number of local variables allowed for a function or method body (see `PLR0914`). #[option(default = r"15", value_type = "int", example = r"max-locals = 20")] - pub max_locals: Option, + max_locals: Option, /// Maximum number of statements allowed for a function or method body (see `PLR0915`). #[option(default = r"50", value_type = "int", example = r"max-statements = 75")] - pub max_statements: Option, + max_statements: Option, /// Maximum number of statements allowed for a try clause body (see `W0717`). #[option( @@ -3622,7 +3633,7 @@ pub struct PylintOptions { value_type = "int", example = r"max-statements-in-try = 10" )] - pub max_statements_in_try: Option, + max_statements_in_try: Option, /// Maximum number of public methods allowed for a class (see `PLR0904`). #[option( @@ -3630,12 +3641,12 @@ pub struct PylintOptions { value_type = "int", example = r"max-public-methods = 30" )] - pub max_public_methods: Option, + max_public_methods: Option, /// Maximum number of Boolean expressions allowed within a single `if` statement /// (see `PLR0916`). #[option(default = r"5", value_type = "int", example = r"max-bool-expr = 10")] - pub max_bool_expr: Option, + max_bool_expr: Option, /// Maximum number of nested blocks allowed within a function or method body /// (see `PLR1702`). @@ -3644,11 +3655,11 @@ pub struct PylintOptions { value_type = "int", example = r"max-nested-blocks = 10" )] - pub max_nested_blocks: Option, + max_nested_blocks: Option, } impl PylintOptions { - pub fn into_settings(self) -> pylint::settings::Settings { + pub(crate) fn into_settings(self) -> pylint::settings::Settings { let defaults = pylint::settings::Settings::default(); pylint::settings::Settings { allow_magic_value_types: self @@ -3721,11 +3732,11 @@ pub struct PyUpgradeOptions { keep-runtime-typing = true "# )] - pub keep_runtime_typing: Option, + keep_runtime_typing: Option, } impl PyUpgradeOptions { - pub fn into_settings(self) -> pyupgrade::settings::Settings { + pub(crate) fn into_settings(self) -> pyupgrade::settings::Settings { pyupgrade::settings::Settings { keep_runtime_typing: self.keep_runtime_typing.unwrap_or_default(), } @@ -3749,7 +3760,7 @@ pub struct RuffOptions { parenthesize-tuple-in-subscript = true "# )] - pub parenthesize_tuple_in_subscript: Option, + parenthesize_tuple_in_subscript: Option, /// A list of additional callable names that behave like /// [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup). @@ -3765,7 +3776,7 @@ pub struct RuffOptions { since = "0.10.0", note = "The `extend-markup-names` option has been moved to the `flake8-bandit` section of the configuration." )] - pub extend_markup_names: Option>, + extend_markup_names: Option>, /// A list of callable names, whose result may be safely passed into /// [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup). @@ -3801,7 +3812,7 @@ pub struct RuffOptions { since = "0.10.0", note = "The `allowed-markup-names` option has been moved to the `flake8-bandit` section of the configuration." )] - pub allowed_markup_calls: Option>, + allowed_markup_calls: Option>, /// Whether to require `__init__.py` files to contain no code at all, including imports and /// docstrings (see `RUF067`). #[option( @@ -3812,11 +3823,11 @@ pub struct RuffOptions { strictly-empty-init-modules = true "# )] - pub strictly_empty_init_modules: Option, + strictly_empty_init_modules: Option, } impl RuffOptions { - pub fn into_settings(self) -> ruff::settings::Settings { + pub(crate) fn into_settings(self) -> ruff::settings::Settings { ruff::settings::Settings { parenthesize_tuple_in_subscript: self .parenthesize_tuple_in_subscript @@ -4172,7 +4183,7 @@ pub struct AnalyzeOptions { exclude = ["generated"] "# )] - pub exclude: Option>, + pub(crate) exclude: Option>, /// Whether to enable preview mode. When preview mode is enabled, Ruff will expose unstable /// commands. #[option( @@ -4183,7 +4194,7 @@ pub struct AnalyzeOptions { preview = true "# )] - pub preview: Option, + pub(crate) preview: Option, /// Whether to generate a map from file to files that it depends on (dependencies) or files that /// depend on it (dependents). #[option( @@ -4193,7 +4204,7 @@ pub struct AnalyzeOptions { direction = "dependencies" "# )] - pub direction: Option, + pub(crate) direction: Option, /// Whether to detect imports from string literals. When enabled, Ruff will search for string /// literals that "look like" import paths, and include them in the import map, if they resolve /// to valid Python modules. @@ -4204,7 +4215,7 @@ pub struct AnalyzeOptions { detect-string-imports = true "# )] - pub detect_string_imports: Option, + pub(crate) detect_string_imports: Option, /// The minimum number of dots in a string to consider it a valid import. /// /// This setting is only relevant when [`detect-string-imports`](#detect-string-imports) is enabled. @@ -4217,7 +4228,7 @@ pub struct AnalyzeOptions { string-imports-min-dots = 2 "# )] - pub string_imports_min_dots: Option, + pub(crate) string_imports_min_dots: Option, /// A map from file path to the list of Python or non-Python file paths or globs that should be /// considered dependencies of that file, regardless of whether relevant imports are detected. #[option( @@ -4229,7 +4240,7 @@ pub struct AnalyzeOptions { "foo/baz/reader.py" = ["configs/bar.json"] "# )] - pub include_dependencies: Option>>, + pub(crate) include_dependencies: Option>>, /// Whether to include imports that are only used for type checking (i.e., imports within `if TYPE_CHECKING:` blocks). /// When enabled (default), type-checking-only imports are included in the import graph. /// When disabled, they are excluded. @@ -4241,7 +4252,7 @@ pub struct AnalyzeOptions { type-checking-imports = false "# )] - pub type_checking_imports: Option, + pub(crate) type_checking_imports: Option, } /// Like [`LintCommonOptions`], but with any `#[serde(flatten)]` fields inlined. This leads to far, diff --git a/crates/ruff_workspace/src/pyproject.rs b/crates/ruff_workspace/src/pyproject.rs index f841cfa83f..928fa6df3c 100644 --- a/crates/ruff_workspace/src/pyproject.rs +++ b/crates/ruff_workspace/src/pyproject.rs @@ -87,7 +87,7 @@ fn parse_pyproject_toml>(path: P) -> Result { } /// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section. -pub fn ruff_enabled>(path: P) -> Result { +fn ruff_enabled>(path: P) -> Result { let pyproject = parse_pyproject_toml(path)?; Ok(pyproject.tool.and_then(|tool| tool.ruff).is_some()) } diff --git a/crates/ruff_workspace/src/resolver.rs b/crates/ruff_workspace/src/resolver.rs index e7b728a788..92134b64f6 100644 --- a/crates/ruff_workspace/src/resolver.rs +++ b/crates/ruff_workspace/src/resolver.rs @@ -73,7 +73,7 @@ impl PyprojectDiscoveryStrategy { } #[inline] - pub const fn is_hierarchical(self) -> bool { + const fn is_hierarchical(self) -> bool { matches!(self, PyprojectDiscoveryStrategy::Hierarchical) } } @@ -89,7 +89,7 @@ pub enum Relativity { } impl Relativity { - pub fn resolve(self, path: &Path) -> &Path { + fn resolve(self, path: &Path) -> &Path { match self { Relativity::Parent => path .parent() @@ -126,7 +126,7 @@ impl<'a> Resolver<'a> { /// Return `true` if the [`Resolver`] is using a hierarchical discovery strategy. #[inline] - pub fn is_hierarchical(&self) -> bool { + fn is_hierarchical(&self) -> bool { self.pyproject_config.strategy.is_hierarchical() } @@ -138,7 +138,7 @@ impl<'a> Resolver<'a> { /// Return `true` if the [`Resolver`] should respect `.gitignore` files. #[inline] - pub fn respect_gitignore(&self) -> bool { + fn respect_gitignore(&self) -> bool { self.pyproject_config .settings .file_resolver @@ -836,7 +836,7 @@ pub fn match_exclusion, R: AsRef>( /// Return `true` if the given candidates should be ignored based on the exclusion /// criteria. -pub fn match_candidate_exclusion( +fn match_candidate_exclusion( file_path: &Candidate, file_basename: &Candidate, exclusion: &GlobSet, diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index e0b5252ed4..0d56dd7288 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -201,7 +201,7 @@ def _(x: int): Default level: ignore · Added in 0.0.57 · Related issues · -View source +View source @@ -868,7 +868,7 @@ INITIALIZED_CONSTANT: Final[int] = 1 Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1874,7 +1874,7 @@ x: G[int] Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5120,7 +5120,7 @@ async def main() -> None: Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5161,7 +5161,7 @@ to `false` to prevent this rule from reporting unused `type: ignore` comments. Default level: warn · Added in 0.0.14 · Related issues · -View source +View source diff --git a/crates/ty/src/args.rs b/crates/ty/src/args.rs index f954ed3390..33a2453e8c 100644 --- a/crates/ty/src/args.rs +++ b/crates/ty/src/args.rs @@ -101,11 +101,11 @@ pub(crate) struct CheckCommand { /// /// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix #[arg(long, value_name = "PATH", alias = "venv")] - pub(crate) python: Option, + python: Option, /// Custom directory to use for stdlib typeshed stubs. #[arg(long, value_name = "PATH", alias = "custom-typeshed-dir")] - pub(crate) typeshed: Option, + typeshed: Option, /// Additional path to use as a module-resolution source (can be passed multiple times). /// @@ -113,7 +113,7 @@ pub(crate) struct CheckCommand { /// modules that are not installed into your Python environment in a conventional way. /// Use `--python` to point ty to your Python environment if it is in an unusual location. #[arg(long, value_name = "PATH")] - pub(crate) extra_search_path: Option>, + extra_search_path: Option>, /// Python version to assume when resolving types. /// @@ -128,7 +128,7 @@ pub(crate) struct CheckCommand { /// and attempt to infer the Python version of that environment /// 3. Fall back to the latest stable Python version supported by ty (see `ty check --help` output) #[arg(long, value_name = "VERSION", alias = "target-version", value_enum)] - pub(crate) python_version: Option, + python_version: Option, /// Target platform to assume when resolving types. /// @@ -137,16 +137,16 @@ pub(crate) struct CheckCommand { /// assumptions are made about the target platform. If unspecified, the current system's /// platform will be used. #[arg(long, value_name = "PLATFORM", alias = "platform")] - pub(crate) python_platform: Option, + python_platform: Option, #[clap(flatten)] pub(crate) verbosity: Verbosity, #[clap(flatten)] - pub(crate) rules: RulesArg, + rules: RulesArg, #[clap(flatten)] - pub(crate) config: ConfigsArg, + config: ConfigsArg, /// The path to a `ty.toml` file to use for configuration. /// @@ -156,13 +156,13 @@ pub(crate) struct CheckCommand { /// The format to use for printing diagnostic messages. #[arg(long, env = EnvVars::TY_OUTPUT_FORMAT)] - pub(crate) output_format: Option, + output_format: Option, /// Use exit code 1 if there are any warning-level diagnostics. /// /// Cannot be used in combination with `--exit-zero` or `--exit-zero-on-warning`. #[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)] - pub(crate) error_on_warning: Option, + error_on_warning: Option, /// Always use exit code 0, even when there are error-level diagnostics. /// @@ -174,7 +174,7 @@ pub(crate) struct CheckCommand { /// /// Cannot be used in combination with `--error-on-warning`. #[arg(long, conflicts_with = "error_on_warning")] - pub(crate) exit_zero_on_warning: bool, + exit_zero_on_warning: bool, /// Watch files for changes and recheck files related to the changed files. #[arg(long, short = 'W')] @@ -515,7 +515,7 @@ over all configuration files.", } impl ConfigsArg { - pub(crate) fn into_options(self) -> Option { + fn into_options(self) -> Option { self.0 } } diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index 2b47acdb00..9e8f074da6 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -72,7 +72,7 @@ pub fn run() -> anyhow::Result { } } -pub(crate) fn version(output_format: HelpFormat) -> Result<()> { +fn version(output_format: HelpFormat) -> Result<()> { let mut stdout = Printer::default().stream_for_requested_summary().lock(); let version_info = crate::version::version(); @@ -265,7 +265,7 @@ pub enum ExitStatus { } impl ExitStatus { - pub const fn is_internal_error(self) -> bool { + const fn is_internal_error(self) -> bool { matches!(self, ExitStatus::InternalError) } } diff --git a/crates/ty/src/logging.rs b/crates/ty/src/logging.rs index 0e1c4a9efd..0f891e78d3 100644 --- a/crates/ty/src/logging.rs +++ b/crates/ty/src/logging.rs @@ -97,11 +97,11 @@ impl VerbosityLevel { } } - pub(crate) const fn is_trace(self) -> bool { + const fn is_trace(self) -> bool { matches!(self, VerbosityLevel::Trace) } - pub(crate) const fn is_extra_verbose(self) -> bool { + const fn is_extra_verbose(self) -> bool { matches!(self, VerbosityLevel::ExtraVerbose) } } diff --git a/crates/ty/src/main.rs b/crates/ty/src/main.rs index 169145d0a1..3a9c3deaf3 100644 --- a/crates/ty/src/main.rs +++ b/crates/ty/src/main.rs @@ -18,7 +18,7 @@ use ty::{ExitStatus, run}; #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -pub fn main() -> ExitStatus { +fn main() -> ExitStatus { run().unwrap_or_else(|error| { use io::Write; diff --git a/crates/ty_ide/src/all_symbols.rs b/crates/ty_ide/src/all_symbols.rs index ab25c746fa..dfce6c8c2b 100644 --- a/crates/ty_ide/src/all_symbols.rs +++ b/crates/ty_ide/src/all_symbols.rs @@ -193,7 +193,7 @@ impl<'db> AllSymbolInfo<'db> { /// /// This is only available for symbols that have been imported /// into `Self::module()` *and* are determined to be re-exports. - pub(crate) fn imported_from(&self) -> Option<&ImportedFrom> { + fn imported_from(&self) -> Option<&ImportedFrom> { self.symbol .as_ref() .and_then(|symbol| symbol.imported_from.as_ref()) diff --git a/crates/ty_ide/src/code_action.rs b/crates/ty_ide/src/code_action.rs index 0246b42469..44f3afcc8a 100644 --- a/crates/ty_ide/src/code_action.rs +++ b/crates/ty_ide/src/code_action.rs @@ -879,14 +879,14 @@ mod tests { "); } - pub(super) struct CodeActionTest { - pub(super) db: ty_project::TestDb, - pub(super) file: File, - pub(super) diagnostic_range: TextRange, + struct CodeActionTest { + db: ty_project::TestDb, + file: File, + diagnostic_range: TextRange, } impl CodeActionTest { - pub(super) fn with_source(source: &str) -> Self { + fn with_source(source: &str) -> Self { let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); @@ -922,7 +922,7 @@ mod tests { } } - pub(super) fn code_actions(&self, lint: &LintMetadata) -> String { + fn code_actions(&self, lint: &LintMetadata) -> String { use std::fmt::Write; let mut buf = String::new(); diff --git a/crates/ty_ide/src/docstring/document/preformatted.rs b/crates/ty_ide/src/docstring/document/preformatted.rs index 7dde49b6c2..df64de37ce 100644 --- a/crates/ty_ide/src/docstring/document/preformatted.rs +++ b/crates/ty_ide/src/docstring/document/preformatted.rs @@ -133,13 +133,13 @@ pub(super) struct RestLiteralBlockScanner { impl RestLiteralBlockScanner { /// Updates internal state for a possible reST literal block marker. - pub(super) fn observe_marker_in_line(&mut self, line: &str) { + fn observe_marker_in_line(&mut self, line: &str) { self.observe_marker(line, indentation(line)); } /// Updates internal state for a possible reST literal block marker whose text has already /// been split out from its source line. - pub(super) fn observe_marker(&mut self, line: &str, marker_indent: TextSize) { + fn observe_marker(&mut self, line: &str, marker_indent: TextSize) { let line = line.trim_start(); if matches!(self.state, RestLiteralBlockState::Inactive) && Self::line_starts_literal_block(line) @@ -152,7 +152,7 @@ impl RestLiteralBlockScanner { } /// Consumes a line if it is inside a reST literal block already observed by `observe_marker`. - pub(super) fn consume_line(&mut self, line: &str) -> bool { + fn consume_line(&mut self, line: &str) -> bool { let current_indent = indentation(line); let line_is_empty = line.trim_start().is_empty(); diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index ef0160f3fc..32a48f7eaa 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -343,7 +343,7 @@ impl<'db> Definitions<'db> { } /// Map definitions from stub files to corresponding source implementations. - pub(crate) fn map_stubs(self, db: &'db dyn ty_python_semantic::Db) -> Definitions<'db> { + fn map_stubs(self, db: &'db dyn ty_python_semantic::Db) -> Definitions<'db> { let resolved = StubMapper::new(db).map_definitions(self.0); Self::new(resolved) } @@ -1397,7 +1397,7 @@ pub(crate) fn find_goto_target<'a>( find_goto_target_impl(model, parsed.tokens(), parsed.syntax().into(), offset) } -pub(crate) fn find_goto_target_impl<'a>( +fn find_goto_target_impl<'a>( model: &'a SemanticModel, tokens: &'a Tokens, syntax: AnyNodeRef<'a>, diff --git a/crates/ty_ide/src/hints.rs b/crates/ty_ide/src/hints.rs index a358326f74..9b996e5cc4 100644 --- a/crates/ty_ide/src/hints.rs +++ b/crates/ty_ide/src/hints.rs @@ -26,7 +26,7 @@ pub enum HintKind { } impl HintKind { - pub fn message(&self) -> String { + fn message(&self) -> String { match self { Self::UnusedBinding(name) => format!("`{name}` is unused"), Self::UnreachableCode(UnreachableKind::Unconditional) => { diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 660bf3aa88..3952f7acd8 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -806,7 +806,7 @@ mod tests { use ruff_db::system::{DbWithWritableSystem, SystemPathBuf}; use ty_project::ProjectMetadata; - pub(super) fn inlay_hint_test(source: &str) -> InlayHintTest { + fn inlay_hint_test(source: &str) -> InlayHintTest { const START: &str = ""; const END: &str = ""; @@ -851,10 +851,10 @@ mod tests { } } - pub(super) struct InlayHintTest { - pub(super) db: ty_project::TestDb, - pub(super) file: File, - pub(super) range: TextRange, + struct InlayHintTest { + db: ty_project::TestDb, + file: File, + range: TextRange, _insta_settings_guard: SettingsBindDropGuard, } diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 5309a9190b..fd71d0d97d 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -135,7 +135,7 @@ pub struct NavigationTarget { impl NavigationTarget { /// Creates a new `NavigationTarget` where the focus and full range are identical. - pub fn new(file: File, range: TextRange) -> Self { + fn new(file: File, range: TextRange) -> Self { Self { file, focus_range: range, @@ -193,7 +193,7 @@ pub struct ReferenceTarget { impl ReferenceTarget { /// Creates a new `ReferenceTarget`. - pub fn new(file: File, range: TextRange, kind: ReferenceKind) -> Self { + fn new(file: File, range: TextRange, kind: ReferenceKind) -> Self { Self { file_range: FileRange::new(file, range), kind, @@ -346,7 +346,7 @@ impl HasNavigationTargets for TypeDefinition<'_> { } /// Get the cache-relative path where vendored paths should be written to. -pub fn relative_cached_vendored_root() -> SystemPathBuf { +fn relative_cached_vendored_root() -> SystemPathBuf { // The vendored files are uniquely identified by the source commit. SystemPathBuf::from(format!("vendored/typeshed/{}", ty_vendored::SOURCE_COMMIT)) } diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index c02d5826d0..f10e4109f0 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -168,7 +168,7 @@ pub struct SemanticTokens { impl SemanticTokens { /// Create a new `SemanticTokens` instance. - pub fn new(tokens: Vec) -> Self { + fn new(tokens: Vec) -> Self { Self { tokens } } } @@ -4685,8 +4685,8 @@ from pathlib import Missing as Alias assert_snapshot!(test.to_snapshot(&tokens), @r#""pathlib" @ 6..13: Namespace"#); } - pub(super) struct SemanticTokenTest { - pub(super) db: ty_project::TestDb, + struct SemanticTokenTest { + db: ty_project::TestDb, file: File, } diff --git a/crates/ty_ide/src/stub_mapping.rs b/crates/ty_ide/src/stub_mapping.rs index 7e67e5e2b5..861aa2028f 100644 --- a/crates/ty_ide/src/stub_mapping.rs +++ b/crates/ty_ide/src/stub_mapping.rs @@ -28,7 +28,7 @@ impl<'db> StubMapper<'db> { /// /// If the definition is in a stub file and a corresponding source file definition exists, /// returns the source file definition(s). Otherwise, returns the original definition. - pub(crate) fn map_definition( + fn map_definition( &self, def: ResolvedDefinition<'db>, ) -> impl Iterator> { diff --git a/crates/ty_ide/src/symbols.rs b/crates/ty_ide/src/symbols.rs index 0b6463e923..a30d8d23e2 100644 --- a/crates/ty_ide/src/symbols.rs +++ b/crates/ty_ide/src/symbols.rs @@ -31,7 +31,7 @@ pub struct QueryPattern { impl QueryPattern { /// Create a new query pattern from a literal search string given. - pub fn fuzzy(literal_query_string: &str) -> QueryPattern { + pub(crate) fn fuzzy(literal_query_string: &str) -> QueryPattern { let mut pattern = "(?i)".to_string(); for ch in literal_query_string.chars() { pattern.push_str(®ex::escape(ch.encode_utf8(&mut [0; 4]))); @@ -50,7 +50,7 @@ impl QueryPattern { } /// Create a new query - pub fn exactly(symbol: &str) -> QueryPattern { + pub(crate) fn exactly(symbol: &str) -> QueryPattern { QueryPattern { re: None, original: symbol.to_string(), @@ -59,7 +59,7 @@ impl QueryPattern { } /// Create a new query pattern that matches all symbols. - pub fn matches_all_symbols() -> QueryPattern { + pub(crate) fn matches_all_symbols() -> QueryPattern { QueryPattern { re: None, original: String::new(), @@ -71,7 +71,7 @@ impl QueryPattern { self.is_match_symbol_name(&symbol.name) } - pub fn is_match_symbol_name(&self, symbol_name: &str) -> bool { + pub(crate) fn is_match_symbol_name(&self, symbol_name: &str) -> bool { if let Some(ref re) = self.re { re.is_match(symbol_name) } else if self.original_is_exact { @@ -91,7 +91,7 @@ impl QueryPattern { /// This will never return `true` incorrectly, but it may return `false` /// incorrectly. That is, it's possible that this query will match all /// inputs but this still returns `false`. - pub fn will_match_everything(&self) -> bool { + pub(crate) fn will_match_everything(&self) -> bool { self.re.is_none() && self.original.is_empty() } } @@ -149,7 +149,10 @@ impl FlatSymbols { } /// Returns a sequence of symbols that matches the given query. - pub fn search(&self, query: &QueryPattern) -> impl Iterator)> { + pub(crate) fn search( + &self, + query: &QueryPattern, + ) -> impl Iterator)> { self.iter() .filter(|(_, symbol)| query.is_match_symbol(symbol)) } @@ -271,7 +274,7 @@ pub struct SymbolInfo<'a> { } impl SymbolInfo<'_> { - pub fn to_owned(&self) -> SymbolInfo<'static> { + pub(crate) fn to_owned(&self) -> SymbolInfo<'static> { SymbolInfo { name: Cow::Owned(self.name.to_string()), kind: self.kind, @@ -333,7 +336,7 @@ pub enum SymbolKind { } impl SymbolKind { - pub fn function_kind(name: &str, defined_in_class: bool) -> Self { + pub(crate) fn function_kind(name: &str, defined_in_class: bool) -> Self { if !defined_in_class { SymbolKind::Function } else if name == "__init__" { @@ -362,7 +365,7 @@ impl SymbolKind { } /// Maps this to a "completion" kind if a sensible mapping exists. - pub fn to_completion_kind(self) -> Option { + pub(crate) fn to_completion_kind(self) -> Option { Some(match self { SymbolKind::Module => CompletionKind::Module, SymbolKind::Class => CompletionKind::Class, @@ -3182,7 +3185,7 @@ class C: ... } impl PublicTestBuilder { - pub(super) fn build(&self) -> PublicTest { + fn build(&self) -> PublicTest { let metadata = ProjectMetadata::new("test", SystemPathBuf::from("/")); let mut db = TestDb::new(metadata); @@ -3216,7 +3219,7 @@ class C: ... } } - pub(super) fn source( + fn source( &mut self, path: impl Into, contents: impl AsRef, @@ -3227,7 +3230,7 @@ class C: ... self } - pub(super) fn python_version(&mut self, version: PythonVersion) -> &mut PublicTestBuilder { + fn python_version(&mut self, version: PythonVersion) -> &mut PublicTestBuilder { self.python_version = Some(version); self } diff --git a/crates/ty_module_resolver/src/lib.rs b/crates/ty_module_resolver/src/lib.rs index 7bcbea9f0d..8f7bbdad55 100644 --- a/crates/ty_module_resolver/src/lib.rs +++ b/crates/ty_module_resolver/src/lib.rs @@ -13,9 +13,9 @@ pub use resolve::{ }; pub use settings::{SearchPathSettings, SearchPathSettingsError}; pub use strategy::{FallibleStrategy, MisconfigurationStrategy, UseDefaultStrategy}; -pub use typeshed::{ - PyVersionRange, TypeshedVersions, TypeshedVersionsParseError, vendored_typeshed_versions, -}; +#[expect(unused_imports)] +pub(crate) use typeshed::vendored_typeshed_versions; +pub use typeshed::{PyVersionRange, TypeshedVersions, TypeshedVersionsParseError}; pub use list::{all_modules, list_modules}; pub use module_glob::{ModuleGlobError, ModuleGlobSet, ModuleGlobSetBuilder, ModuleNameMatch}; diff --git a/crates/ty_module_resolver/src/path.rs b/crates/ty_module_resolver/src/path.rs index efada7b377..351f3cecaa 100644 --- a/crates/ty_module_resolver/src/path.rs +++ b/crates/ty_module_resolver/src/path.rs @@ -31,7 +31,7 @@ pub(crate) struct ModulePath { impl ModulePath { #[must_use] - pub(crate) fn is_standard_library(&self) -> bool { + fn is_standard_library(&self) -> bool { matches!( &*self.search_path.0, SearchPathInner::StandardLibraryCustom(_) | SearchPathInner::StandardLibraryVendored(_) @@ -705,12 +705,12 @@ impl SearchPath { } #[must_use] - pub fn as_system_path(&self) -> Option<&SystemPath> { + pub(crate) fn as_system_path(&self) -> Option<&SystemPath> { self.as_path().as_system_path() } #[must_use] - pub(crate) fn as_vendored_path(&self) -> Option<&VendoredPath> { + fn as_vendored_path(&self) -> Option<&VendoredPath> { self.as_path().as_vendored_path() } diff --git a/crates/ty_module_resolver/src/resolve.rs b/crates/ty_module_resolver/src/resolve.rs index a4fcd37fdc..787f93460e 100644 --- a/crates/ty_module_resolver/src/resolve.rs +++ b/crates/ty_module_resolver/src/resolve.rs @@ -596,7 +596,7 @@ impl SearchPaths { /// This method also implements the typing spec's [module resolution order]. /// /// [module resolution order]: https://typing.python.org/en/latest/spec/distributing.html#import-resolution-ordering - pub fn from_settings( + pub(crate) fn from_settings( settings: &SearchPathSettings, system: &dyn System, vendored: &VendoredFileSystem, @@ -789,11 +789,7 @@ impl SearchPaths { } } - pub(super) fn iter<'a>( - &'a self, - db: &'a dyn Db, - mode: ModuleResolveMode, - ) -> SearchPathIterator<'a> { + fn iter<'a>(&'a self, db: &'a dyn Db, mode: ModuleResolveMode) -> SearchPathIterator<'a> { let stdlib_path = self.stdlib(mode); SearchPathIterator { db, @@ -804,7 +800,7 @@ impl SearchPaths { } } - pub(crate) fn stdlib(&self, mode: ModuleResolveMode) -> Option<&SearchPath> { + fn stdlib(&self, mode: ModuleResolveMode) -> Option<&SearchPath> { match mode { ModuleResolveMode::Typing => self.stdlib_path.as_ref(), ModuleResolveMode::Runtime | ModuleResolveMode::RuntimeSomeShadowingAllowed => { diff --git a/crates/ty_module_resolver/src/typeshed.rs b/crates/ty_module_resolver/src/typeshed.rs index 1fcac4050d..381bb58fb7 100644 --- a/crates/ty_module_resolver/src/typeshed.rs +++ b/crates/ty_module_resolver/src/typeshed.rs @@ -11,7 +11,7 @@ use rustc_hash::FxHashMap; use crate::db::Db; use crate::module_name::ModuleName; -pub fn vendored_typeshed_versions(vendored: &VendoredFileSystem) -> TypeshedVersions { +pub(crate) fn vendored_typeshed_versions(vendored: &VendoredFileSystem) -> TypeshedVersions { TypeshedVersions::from_str( &vendored .read_to_string("stdlib/VERSIONS") diff --git a/crates/ty_project/src/db.rs b/crates/ty_project/src/db.rs index a090447a0b..0919c85a67 100644 --- a/crates/ty_project/src/db.rs +++ b/crates/ty_project/src/db.rs @@ -96,7 +96,7 @@ impl ProjectDatabase { self.files.freeze(); } - /// See [`Project::freeze_open_files`]. + /// Permanently marks the project as never having open files. pub fn freeze_open_files(&mut self) { let project = self.project(); project.freeze_open_files(self); diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index aa61f4fe3f..51cab953df 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -176,7 +176,7 @@ impl Project { /// /// Program-settings diagnostics are accepted separately so callers do not need to know how to /// convert and merge them into the stored project settings diagnostics. - pub(crate) fn from_metadata( + fn from_metadata( db: &dyn Db, metadata: ProjectMetadata, settings: Settings, @@ -199,7 +199,7 @@ impl Project { /// Permanently freezes the most heavily read immutable project inputs. /// /// This is intentionally not exhaustive. - pub(crate) fn freeze(self, db: &mut dyn Db) { + fn freeze(self, db: &mut dyn Db) { let durability = Durability::NEVER_CHANGE; let metadata = Box::new(self.metadata(db).clone()); let settings = Box::new(self.settings(db).clone()); @@ -234,7 +234,7 @@ impl Project { self.metadata(db).root() } - pub fn name(self, db: &dyn Db) -> &str { + fn name(self, db: &dyn Db) -> &str { self.metadata(db).name() } @@ -259,7 +259,7 @@ impl Project { .is_file_included(path, GlobFilterCheckMode::Adhoc) } - pub fn is_directory_included(self, db: &dyn Db, path: &SystemPath) -> bool { + fn is_directory_included(self, db: &dyn Db, path: &SystemPath) -> bool { matches!( ProjectFilesFilter::from_project(db, self) .is_directory_included(path, GlobFilterCheckMode::Adhoc), @@ -325,7 +325,7 @@ impl Project { /// /// This is used when a change affects [`ty_python_core::program::ProgramSettings`] without /// reloading the full project. - pub(crate) fn update_settings_diagnostics( + fn update_settings_diagnostics( self, db: &mut dyn Db, settings_diagnostics: Vec, @@ -356,7 +356,7 @@ impl Project { } /// Checks the project and its dependencies according to the project's check mode. - pub(crate) fn check(self, db: &ProjectDatabase, reporter: &mut dyn ProgressReporter) { + fn check(self, db: &ProjectDatabase, reporter: &mut dyn ProgressReporter) { let project_span = tracing::debug_span!("Project::check"); let _span = project_span.enter(); @@ -455,7 +455,7 @@ impl Project { } } - pub fn verbose(self, db: &dyn Db) -> bool { + fn verbose(self, db: &dyn Db) -> bool { self.verbose_flag(db) } @@ -465,7 +465,7 @@ impl Project { } } - pub fn force_exclude(self, db: &dyn Db) -> bool { + fn force_exclude(self, db: &dyn Db) -> bool { self.force_exclude_flag(db) } @@ -487,7 +487,7 @@ impl Project { } /// Returns the open files in the project. - pub fn open_files(self, db: &dyn Db) -> &FxHashSet { + fn open_files(self, db: &dyn Db) -> &FxHashSet { self.open_fileset(db) } @@ -501,7 +501,7 @@ impl Project { /// Permanently marks the project as never having open files, so reads of the /// open-file state record no salsa dependency. Any later write panics. - pub fn freeze_open_files(self, db: &mut dyn Db) { + fn freeze_open_files(self, db: &mut dyn Db) { self.set_open_fileset(db) .with_durability(Durability::NEVER_CHANGE) .to(FxHashSet::default()); @@ -535,7 +535,7 @@ impl Project { /// /// This is a no-op if the project files are still lazily indexed. #[tracing::instrument(level = "debug", skip(self, db, paths))] - pub(crate) fn remove_files_under(self, db: &mut dyn Db, paths: I) + fn remove_files_under(self, db: &mut dyn Db, paths: I) where I: IntoIterator, P: AsRef, @@ -584,7 +584,7 @@ impl Project { } } - pub fn add_file(self, db: &mut dyn Db, file: File) { + fn add_file(self, db: &mut dyn Db, file: File) { tracing::debug!( "Adding file `{}` to project `{}`", file.path(db), @@ -601,7 +601,7 @@ impl Project { /// Replaces the diagnostics from indexing the project files with `diagnostics`. /// /// This is a no-op if the project files haven't been indexed yet. - pub fn replace_index_diagnostics(self, db: &mut dyn Db, diagnostics: Vec) { + fn replace_index_diagnostics(self, db: &mut dyn Db, diagnostics: Vec) { let Some(mut index) = IndexedFiles::indexed_mut(db, self) else { return; }; @@ -634,7 +634,7 @@ impl Project { } } - pub fn reload_files(self, db: &mut dyn Db) { + fn reload_files(self, db: &mut dyn Db) { tracing::debug!("Reloading files for project `{}`", self.name(db)); if !self.file_set(db).is_lazy() { @@ -652,7 +652,7 @@ impl Project { } } -pub(crate) fn check_file(db: &dyn Db, file: File) -> Vec { +fn check_file(db: &dyn Db, file: File) -> Vec { if !db.should_check_file(file) { return Vec::new(); } diff --git a/crates/ty_project/src/metadata.rs b/crates/ty_project/src/metadata.rs index e6439cf14d..c7e1a914ef 100644 --- a/crates/ty_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -44,7 +44,7 @@ pub struct ProjectMetadata { /// When [`Self::config_file_override`] is `None`, then these are the options from the /// project's `ty.toml` or `pyproject.toml`. The options come from /// the file specified by [`Self::config_file_override`] if it is `Some` (e.g. when using `--config-file `). - pub(super) options: Options, + options: Options, /// The Python version and interpreter path derived from uv workspace metadata. /// @@ -120,7 +120,7 @@ impl ProjectMetadata { } /// Loads a project from a `pyproject.toml` file. - pub(crate) fn from_pyproject( + fn from_pyproject( pyproject: PyProject, root: SystemPathBuf, ) -> Result { @@ -405,15 +405,15 @@ impl ProjectMetadata { Ok(metadata) } - pub fn root(&self) -> &SystemPath { + pub(crate) fn root(&self) -> &SystemPath { &self.root } - pub fn name(&self) -> &str { + pub(crate) fn name(&self) -> &str { self.name.as_str() } - pub fn options(&self) -> &Options { + fn options(&self) -> &Options { &self.options } @@ -423,7 +423,7 @@ impl ProjectMetadata { } /// Returns configuration paths outside normal project discovery that should be watched. - pub fn extra_configuration_paths(&self) -> impl Iterator { + pub(crate) fn extra_configuration_paths(&self) -> impl Iterator { self.config_file_override().into_iter().chain( self.user_configuration .as_deref() @@ -480,7 +480,7 @@ impl ProjectMetadata { /// merged.combine_with(layer.clone()); /// } /// ``` - pub(crate) fn options_in_precedence_order<'a>( + fn options_in_precedence_order<'a>( &'a self, options: &'a Options, ) -> impl Iterator { diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 14cf81fcfb..000dcfb460 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -1104,7 +1104,7 @@ impl FromIterator<(RangedValue, RangedValue)> for Rules { impl Rules { /// Convert the rules to a `RuleSelection` with diagnostics. - pub fn to_rule_selection( + pub(crate) fn to_rule_selection( &self, db: &dyn Db, diagnostics: &mut Vec, @@ -1169,7 +1169,7 @@ impl Rules { selection } - pub(super) fn is_empty(&self) -> bool { + fn is_empty(&self) -> bool { self.inner.is_empty() } } @@ -1834,7 +1834,7 @@ pub struct OverrideOptions { ] "# )] - pub include: Option>>, + include: Option>>, /// A list of file and directory patterns to exclude from this override. /// @@ -1856,7 +1856,7 @@ pub struct OverrideOptions { ] "# )] - pub exclude: Option>>, + exclude: Option>>, /// Rule overrides for files matching the include/exclude patterns. /// @@ -1875,11 +1875,11 @@ pub struct OverrideOptions { possibly-unresolved-reference = "ignore" "# )] - pub rules: Option, + rules: Option, #[serde(skip_serializing_if = "Option::is_none")] #[option_group] - pub analysis: Option, + analysis: Option, } trait ToOverride { @@ -2095,7 +2095,7 @@ pub struct ToSettingsError { } impl ToSettingsError { - pub fn pretty<'a>(&'a self, db: &'a dyn Db) -> impl fmt::Display + use<'a> { + pub(crate) fn pretty<'a>(&'a self, db: &'a dyn Db) -> impl fmt::Display + use<'a> { let db: &dyn ruff_db::Db = db; fmt::from_fn(move |f| { @@ -2113,7 +2113,7 @@ impl ToSettingsError { }) } - pub fn into_diagnostic(self) -> OptionDiagnostic { + pub(crate) fn into_diagnostic(self) -> OptionDiagnostic { *self.diagnostic } } @@ -2218,7 +2218,7 @@ pub struct OptionDiagnostic { } impl OptionDiagnostic { - pub fn new(id: DiagnosticId, message: String, severity: Severity) -> Self { + fn new(id: DiagnosticId, message: String, severity: Severity) -> Self { Self { id, message, diff --git a/crates/ty_project/src/metadata/pyproject.rs b/crates/ty_project/src/metadata/pyproject.rs index f77fa582a7..91e1a90272 100644 --- a/crates/ty_project/src/metadata/pyproject.rs +++ b/crates/ty_project/src/metadata/pyproject.rs @@ -72,11 +72,11 @@ pub struct Project { /// /// Note: Intentionally option to be more permissive during deserialization. /// `PackageMetadata::from_pyproject` reports missing names. - pub name: Option>, + pub(crate) name: Option>, /// The version of the project - pub version: Option>, + pub(crate) version: Option>, /// The Python versions this project is compatible with. - pub requires_python: Option>, + pub(crate) requires_python: Option>, } impl Project { @@ -177,7 +177,7 @@ pub struct PackageName(String); impl PackageName { /// Create a validated, normalized package name. - pub(crate) fn new(name: String) -> Result { + fn new(name: String) -> Result { if name.is_empty() { return Err(InvalidPackageNameError::Empty); } @@ -230,7 +230,7 @@ impl PackageName { } /// Returns the underlying package name. - pub(crate) fn as_str(&self) -> &str { + fn as_str(&self) -> &str { &self.0 } } diff --git a/crates/ty_project/src/metadata/python_version.rs b/crates/ty_project/src/metadata/python_version.rs index 8bac1a479f..c2ea830d3b 100644 --- a/crates/ty_project/src/metadata/python_version.rs +++ b/crates/ty_project/src/metadata/python_version.rs @@ -51,7 +51,7 @@ pub enum SupportedPythonVersion { } impl SupportedPythonVersion { - pub const fn as_str(self) -> &'static str { + const fn as_str(self) -> &'static str { match self { Self::Py37 => "3.7", Self::Py38 => "3.8", @@ -65,7 +65,7 @@ impl SupportedPythonVersion { } } - pub const fn to_python_version(self) -> PythonVersion { + pub(crate) const fn to_python_version(self) -> PythonVersion { match self { Self::Py37 => PythonVersion::PY37, Self::Py38 => PythonVersion::PY38, diff --git a/crates/ty_project/src/metadata/settings.rs b/crates/ty_project/src/metadata/settings.rs index e2e4b0c74b..e939243e88 100644 --- a/crates/ty_project/src/metadata/settings.rs +++ b/crates/ty_project/src/metadata/settings.rs @@ -38,15 +38,15 @@ pub struct Settings { } impl Settings { - pub fn rules(&self) -> &RuleSelection { + fn rules(&self) -> &RuleSelection { &self.rules } - pub fn src(&self) -> &SrcSettings { + pub(crate) fn src(&self) -> &SrcSettings { &self.src } - pub fn to_rules(&self) -> Arc { + pub(crate) fn to_rules(&self) -> Arc { self.rules.clone() } @@ -54,11 +54,11 @@ impl Settings { &self.terminal } - pub fn overrides(&self) -> &[Override] { + fn overrides(&self) -> &[Override] { &self.overrides } - pub fn analysis(&self) -> &AnalysisSettings { + pub(crate) fn analysis(&self) -> &AnalysisSettings { &self.analysis } } @@ -80,9 +80,9 @@ impl Default for TerminalSettings { #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] pub struct SrcSettings { - pub respect_ignore_files: bool, - pub exclude_scripts: bool, - pub files: IncludeExcludeFilter, + pub(crate) respect_ignore_files: bool, + pub(crate) exclude_scripts: bool, + pub(crate) files: IncludeExcludeFilter, } impl SrcSettings { pub(crate) fn default() -> Self { @@ -111,7 +111,7 @@ pub struct Override { impl Override { /// Returns whether this override applies to the given file path. - pub fn matches_file(&self, path: &ruff_db::system::SystemPath) -> bool { + fn matches_file(&self, path: &ruff_db::system::SystemPath) -> bool { use crate::glob::{GlobFilterCheckMode, IncludeResult}; matches!( @@ -258,14 +258,14 @@ pub enum FileSettings { } impl FileSettings { - pub fn rules<'a>(&'a self, db: &'a dyn Db) -> &'a RuleSelection { + pub(crate) fn rules<'a>(&'a self, db: &'a dyn Db) -> &'a RuleSelection { match self { FileSettings::Global => db.project().settings(db).rules(), FileSettings::File(override_settings) => &override_settings.rules, } } - pub fn analysis<'a>(&'a self, db: &'a dyn Db) -> &'a AnalysisSettings { + pub(crate) fn analysis<'a>(&'a self, db: &'a dyn Db) -> &'a AnalysisSettings { match self { FileSettings::Global => db.project().settings(db).analysis(), FileSettings::File(override_settings) => &override_settings.analysis, diff --git a/crates/ty_project/src/metadata/value.rs b/crates/ty_project/src/metadata/value.rs index d118dfde7e..a8b8a47bf7 100644 --- a/crates/ty_project/src/metadata/value.rs +++ b/crates/ty_project/src/metadata/value.rs @@ -37,7 +37,7 @@ use crate::glob::{ pub struct RelativePathBuf(RangedValue); impl RelativePathBuf { - pub fn new(path: impl AsRef, source: ValueSource) -> Self { + pub(crate) fn new(path: impl AsRef, source: ValueSource) -> Self { Self(RangedValue::new(path.as_ref().to_path_buf(), source)) } @@ -54,11 +54,11 @@ impl RelativePathBuf { &self.0 } - pub fn source(&self) -> &ValueSource { + pub(crate) fn source(&self) -> &ValueSource { self.0.source() } - pub fn range(&self) -> Option { + pub(crate) fn range(&self) -> Option { self.0.range() } @@ -131,7 +131,7 @@ impl fmt::Display for RelativePathBuf { pub struct RelativeGlobPattern(RangedValue); impl RelativeGlobPattern { - pub fn new(pattern: impl AsRef, source: ValueSource) -> Self { + fn new(pattern: impl AsRef, source: ValueSource) -> Self { Self(RangedValue::new(pattern.as_ref().to_string(), source)) } diff --git a/crates/ty_project/src/walk.rs b/crates/ty_project/src/walk.rs index 91e775544a..869742c220 100644 --- a/crates/ty_project/src/walk.rs +++ b/crates/ty_project/src/walk.rs @@ -36,7 +36,7 @@ impl<'a> ProjectFilesFilter<'a> { } } - pub(crate) fn force_exclude(&self) -> bool { + fn force_exclude(&self) -> bool { self.force_exclude } diff --git a/crates/ty_project/src/watch.rs b/crates/ty_project/src/watch.rs index de4fe6526a..b22596b99a 100644 --- a/crates/ty_project/src/watch.rs +++ b/crates/ty_project/src/watch.rs @@ -74,7 +74,7 @@ impl ChangeEvent { self.system_path().and_then(|path| path.file_name()) } - pub fn system_path(&self) -> Option<&SystemPath> { + pub(crate) fn system_path(&self) -> Option<&SystemPath> { match self { ChangeEvent::Opened(path) | ChangeEvent::Created { path, .. } @@ -151,7 +151,7 @@ impl ExistingPathKind { } } - pub fn from_io_metadata(metadata: &std::io::Result) -> Self { + fn from_io_metadata(metadata: &std::io::Result) -> Self { match metadata { Ok(metadata) if metadata.is_file() => Self::File, Ok(metadata) if metadata.is_dir() => Self::Directory, diff --git a/crates/ty_project/src/watch/watcher.rs b/crates/ty_project/src/watch/watcher.rs index 1802de36ae..1cfcef027d 100644 --- a/crates/ty_project/src/watch/watcher.rs +++ b/crates/ty_project/src/watch/watcher.rs @@ -129,7 +129,7 @@ impl Watcher { } /// Returns a transaction-like view for updating watched paths in one backend operation. - pub fn paths_mut(&mut self) -> WatcherPathsMut<'_> { + pub(crate) fn paths_mut(&mut self) -> WatcherPathsMut<'_> { WatcherPathsMut { inner: self.inner_mut().watcher.paths_mut(), } @@ -140,13 +140,13 @@ impl Watcher { /// Pending events will be discarded. /// /// The call blocks until the watcher has stopped. - pub fn stop(mut self) { + pub(crate) fn stop(mut self) { tracing::debug!("Stop file watcher"); self.set_stop(); } /// Flushes any pending events. - pub fn flush(&self) { + pub(crate) fn flush(&self) { self.inner() .debouncer_sender .send(DebouncerMessage::Flush) @@ -177,22 +177,22 @@ impl Watcher { } } -pub struct WatcherPathsMut<'a> { +pub(crate) struct WatcherPathsMut<'a> { inner: Box, } impl WatcherPathsMut<'_> { - pub fn add(&mut self, path: &SystemPath) -> notify::Result<()> { + pub(crate) fn add(&mut self, path: &SystemPath) -> notify::Result<()> { tracing::debug!("Watching path: `{path}`"); self.inner.add(path.as_std_path(), RecursiveMode::Recursive) } - pub fn remove(&mut self, path: &SystemPath) -> notify::Result<()> { + pub(crate) fn remove(&mut self, path: &SystemPath) -> notify::Result<()> { tracing::debug!("Unwatching path: `{path}`"); self.inner.remove(path.as_std_path()) } - pub fn commit(self) -> notify::Result<()> { + pub(crate) fn commit(self) -> notify::Result<()> { self.inner.commit() } } diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index a1a8b7ff06..7bd8684fc7 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -380,7 +380,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { } } - pub(crate) fn expect_single_definition( + fn expect_single_definition( &self, definition_key: impl Into + std::fmt::Debug + Copy, ) -> Definition<'db> { @@ -5437,6 +5437,6 @@ fn is_collection_initializer(expr: &ast::Expr) -> bool { is_collection_literal(expr) || is_empty_collection_constructor_call(expr) } -pub(crate) fn is_collection_literal(expr: &ast::Expr) -> bool { +fn is_collection_literal(expr: &ast::Expr) -> bool { expr.is_list_expr() || expr.is_set_expr() || expr.is_dict_expr() } diff --git a/crates/ty_python_core/src/builder/loop_bindings_visitor.rs b/crates/ty_python_core/src/builder/loop_bindings_visitor.rs index 8041e074b1..eb52d2b400 100644 --- a/crates/ty_python_core/src/builder/loop_bindings_visitor.rs +++ b/crates/ty_python_core/src/builder/loop_bindings_visitor.rs @@ -34,7 +34,7 @@ pub(crate) struct LoopBindingsVisitor { } impl LoopBindingsVisitor { - pub(crate) fn add_place_from_target(&mut self, target: &ast::Expr) { + fn add_place_from_target(&mut self, target: &ast::Expr) { match target { ast::Expr::Name(name) => { self.bound_places.push(PlaceExpr::from_expr_name(name)); diff --git a/crates/ty_python_core/src/db.rs b/crates/ty_python_core/src/db.rs index 2c447609f1..e486ae8a74 100644 --- a/crates/ty_python_core/src/db.rs +++ b/crates/ty_python_core/src/db.rs @@ -43,7 +43,7 @@ pub(crate) mod tests { } impl TestDb { - pub(crate) fn new() -> Self { + fn new() -> Self { let events = Events::default(); Self { storage: salsa::Storage::new(Some(Box::new({ diff --git a/crates/ty_python_core/src/definition.rs b/crates/ty_python_core/src/definition.rs index df1d8a6aee..f8e0c54668 100644 --- a/crates/ty_python_core/src/definition.rs +++ b/crates/ty_python_core/src/definition.rs @@ -277,7 +277,7 @@ impl<'db> Definitions<'db> { } } - pub fn push(&mut self, definition: Definition<'db>) { + pub(crate) fn push(&mut self, definition: Definition<'db>) { self.definitions.push(definition); } @@ -586,7 +586,7 @@ pub(crate) enum ParameterDefinitionNodeRef<'ast> { } impl ParameterDefinitionNodeRef<'_> { - pub(super) fn into_owned(self, parsed: &ParsedModuleRef) -> ParameterDefinitionNodeKind { + fn into_owned(self, parsed: &ParsedModuleRef) -> ParameterDefinitionNodeKind { match self { Self::VariadicPositionalParameter(parameter) => { ParameterDefinitionNodeKind::VariadicPositionalParameter(AstNodeRef::new( @@ -604,7 +604,7 @@ impl ParameterDefinitionNodeRef<'_> { } } - pub(super) fn key(self) -> DefinitionNodeKey { + fn key(self) -> DefinitionNodeKey { match self { Self::VariadicPositionalParameter(node) => node.into(), Self::VariadicKeywordParameter(node) => node.into(), @@ -943,7 +943,7 @@ pub enum DefinitionKind<'db> { } impl<'db> DefinitionKind<'db> { - pub fn is_reexported(&self) -> bool { + pub(crate) fn is_reexported(&self) -> bool { match self { DefinitionKind::Import(import) => import.is_reexported(), DefinitionKind::ImportFrom(import) => import.is_reexported(), @@ -980,7 +980,7 @@ impl<'db> DefinitionKind<'db> { matches!(self, DefinitionKind::Assignment(_)) } - pub fn as_unannotated_assignment(&self) -> Option> { + pub(crate) fn as_unannotated_assignment(&self) -> Option> { match self { DefinitionKind::Assignment(assignment) => Some(assignment.clone()), _ => None, @@ -1279,7 +1279,7 @@ pub enum ParameterDefinitionNodeKind { } impl ParameterDefinitionNodeKind { - pub(crate) fn target_range(&self, module: &ParsedModuleRef) -> TextRange { + fn target_range(&self, module: &ParsedModuleRef) -> TextRange { match self { Self::VariadicPositionalParameter(parameter) => parameter.node(module).name.range(), Self::VariadicKeywordParameter(parameter) => parameter.node(module).name.range(), @@ -1287,7 +1287,7 @@ impl ParameterDefinitionNodeKind { } } - pub(crate) fn full_range(&self, module: &ParsedModuleRef) -> TextRange { + fn full_range(&self, module: &ParsedModuleRef) -> TextRange { match self { Self::VariadicPositionalParameter(parameter) => parameter.node(module).range(), Self::VariadicKeywordParameter(parameter) => parameter.node(module).range(), @@ -1295,7 +1295,7 @@ impl ParameterDefinitionNodeKind { } } - pub(crate) fn category(&self, module: &ParsedModuleRef) -> DefinitionCategory { + fn category(&self, module: &ParsedModuleRef) -> DefinitionCategory { match self { // a parameter always binds a value, but is only a declaration if annotated Self::VariadicPositionalParameter(parameter) @@ -1346,7 +1346,7 @@ impl ImportDefinitionKind { &self.node.node(module).names[self.alias_index as usize] } - pub fn is_reexported(&self) -> bool { + fn is_reexported(&self) -> bool { self.is_reexported } } @@ -1367,7 +1367,7 @@ impl ImportFromDefinitionKind { &self.node.node(module).names[self.alias_index as usize] } - pub fn is_reexported(&self) -> bool { + fn is_reexported(&self) -> bool { self.is_reexported } } @@ -1382,14 +1382,14 @@ impl ImportFromSubmoduleDefinitionKind { self.node.node(module) } - pub fn module<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Identifier { + fn module<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::Identifier { self.import(module) .module .as_ref() .expect("import-from submodule definitions should always have a module identifier") } - pub fn target_range(&self, module: &ParsedModuleRef) -> TextRange { + fn target_range(&self, module: &ParsedModuleRef) -> TextRange { let module_ident = self.module(module); let module_str = module_ident.as_str(); @@ -1461,9 +1461,9 @@ impl AnnotatedAssignmentDefinitionKind { #[derive(Clone, Debug, get_size2::GetSize, salsa::SalsaValue)] pub struct DictKeyAssignmentKind<'db> { - pub(crate) key: AstNodeRef, - pub(crate) value: AstNodeRef, - pub(crate) assignment: Definition<'db>, + key: AstNodeRef, + value: AstNodeRef, + assignment: Definition<'db>, } impl<'db> DictKeyAssignmentKind<'db> { @@ -1584,7 +1584,7 @@ impl LoopHeaderDefinitionKind { self.place } - pub fn range(&self, module: &ParsedModuleRef) -> TextRange { + fn range(&self, module: &ParsedModuleRef) -> TextRange { match &self.loop_stmt { LoopStmtKind::While(stmt) => stmt.node(module).range(), LoopStmtKind::For(stmt) => stmt.node(module).range(), @@ -1606,7 +1606,7 @@ impl NestedBindingsDefinitionKind { /// Returns every nested binding source and whether it was declared `global`. /// /// Use [`Self::visible_binding_sources`] when resolving the binding in a particular scope. - pub fn binding_sources<'index, 'db>( + fn binding_sources<'index, 'db>( &'index self, index: &'index SemanticIndex<'db>, ) -> impl Iterator)> + 'index { diff --git a/crates/ty_python_core/src/frozen.rs b/crates/ty_python_core/src/frozen.rs index 6df174dd19..23f91a65f3 100644 --- a/crates/ty_python_core/src/frozen.rs +++ b/crates/ty_python_core/src/frozen.rs @@ -22,7 +22,7 @@ impl FrozenMap { self.into_iter() } - pub fn keys(&self) -> impl DoubleEndedIterator + ExactSizeIterator { + pub(crate) fn keys(&self) -> impl DoubleEndedIterator + ExactSizeIterator { self.0.iter().map(|(key, _)| key) } diff --git a/crates/ty_python_core/src/lib.rs b/crates/ty_python_core/src/lib.rs index 2d6b02eda2..4b5ad6c2b7 100644 --- a/crates/ty_python_core/src/lib.rs +++ b/crates/ty_python_core/src/lib.rs @@ -141,13 +141,13 @@ pub struct LoopHeader { } impl LoopHeader { - pub fn new() -> Self { + fn new() -> Self { Self { bindings: FxHashMap::default(), } } - pub fn add_binding(&mut self, place: ScopedPlaceId, binding: LiveBinding) { + fn add_binding(&mut self, place: ScopedPlaceId, binding: LiveBinding) { self.bindings.entry(place).or_default().push(binding); } @@ -390,7 +390,7 @@ impl<'db> SemanticIndex<'db> { } #[track_caller] - pub(crate) fn ast_ids(&self) -> &AstIds { + fn ast_ids(&self) -> &AstIds { &self.ast_ids } @@ -550,7 +550,7 @@ impl<'db> SemanticIndex<'db> { } /// Returns an iterator over the descendent scopes of `scope`. - pub(crate) fn descendent_scopes(&self, scope: FileScopeId) -> DescendantsIter<'_> { + fn descendent_scopes(&self, scope: FileScopeId) -> DescendantsIter<'_> { DescendantsIter::new(&self.scopes, scope) } @@ -874,7 +874,7 @@ pub struct ChildrenIter<'a> { } impl<'a> ChildrenIter<'a> { - pub fn new(scopes: &'a IndexSlice, parent: FileScopeId) -> Self { + fn new(scopes: &'a IndexSlice, parent: FileScopeId) -> Self { let descendants = DescendantsIter::new(scopes, parent); Self { diff --git a/crates/ty_python_core/src/member.rs b/crates/ty_python_core/src/member.rs index 3de8127005..cc1b20c507 100644 --- a/crates/ty_python_core/src/member.rs +++ b/crates/ty_python_core/src/member.rs @@ -80,7 +80,7 @@ impl Member { /// a method context, or whether the `` actually refers to the first /// parameter of the method (i.e. `self`). To answer those questions, /// use [`Self::as_instance_attribute`]. - pub(super) fn as_instance_attribute_candidate(&self) -> Option<&str> { + fn as_instance_attribute_candidate(&self) -> Option<&str> { let mut segments = self.expression().segments(); let first_segment = segments.next()?; @@ -105,7 +105,7 @@ impl Member { } /// Does the place expression have the form `self.{name}` (`self` is the first parameter of the method)? - pub(super) fn is_instance_attribute_named(&self, name: &str) -> bool { + fn is_instance_attribute_named(&self, name: &str) -> bool { self.as_instance_attribute() == Some(name) } @@ -164,7 +164,7 @@ pub(crate) struct MemberExpr { impl MemberExpr { #[cfg(test)] - pub(super) fn try_from_expr(expression: ast::ExprRef<'_>) -> Option { + fn try_from_expr(expression: ast::ExprRef<'_>) -> Option { MemberExprBuilder::visit_expr(expression).and_then(Self::try_from_builder) } @@ -190,7 +190,7 @@ impl MemberExpr { /// Returns the left most part of the member expression, e.g. `x` in `x.y.z`. /// /// This is the symbol on which the member access is performed. - pub(crate) fn symbol_name(&self) -> &str { + fn symbol_name(&self) -> &str { self.as_ref().symbol_name() } diff --git a/crates/ty_python_core/src/place.rs b/crates/ty_python_core/src/place.rs index e04f967d96..d5306bb7e6 100644 --- a/crates/ty_python_core/src/place.rs +++ b/crates/ty_python_core/src/place.rs @@ -282,7 +282,7 @@ pub struct PlaceTableBuilder { impl PlaceTableBuilder { /// Looks up a place ID by its expression. - pub fn place_id(&self, expression: PlaceExprRef) -> Option { + pub(crate) fn place_id(&self, expression: PlaceExprRef) -> Option { match expression { PlaceExprRef::Symbol(symbol) => self.symbols.symbol_id(symbol.name()).map(Into::into), PlaceExprRef::Member(member) => { @@ -310,12 +310,12 @@ impl PlaceTableBuilder { } #[track_caller] - pub(super) fn member_mut(&mut self, id: ScopedMemberId) -> &mut Member { + fn member_mut(&mut self, id: ScopedMemberId) -> &mut Member { self.member.member_mut(id) } #[track_caller] - pub fn place(&self, place_id: impl Into) -> PlaceExprRef<'_> { + pub(crate) fn place(&self, place_id: impl Into) -> PlaceExprRef<'_> { match place_id.into() { ScopedPlaceId::Symbol(id) => PlaceExprRef::Symbol(self.symbols.symbol(id)), ScopedPlaceId::Member(id) => PlaceExprRef::Member(self.member.member(id)), @@ -329,18 +329,18 @@ impl PlaceTableBuilder { } } - pub fn iter(&self) -> impl Iterator> { + pub(crate) fn iter(&self) -> impl Iterator> { self.symbols .iter() .map(Into::into) .chain(self.member.iter().map(PlaceExprRef::Member)) } - pub fn symbols(&self) -> impl Iterator { + pub(crate) fn symbols(&self) -> impl Iterator { self.symbols.iter() } - pub fn add_symbol(&mut self, symbol: Symbol) -> (ScopedSymbolId, bool) { + pub(crate) fn add_symbol(&mut self, symbol: Symbol) -> (ScopedSymbolId, bool) { let (id, is_new) = self.symbols.add(symbol); if is_new { @@ -351,7 +351,7 @@ impl PlaceTableBuilder { (id, is_new) } - pub fn add_member(&mut self, member: Member) -> (ScopedMemberId, bool) { + fn add_member(&mut self, member: Member) -> (ScopedMemberId, bool) { let (id, is_new) = self.member.add(member); if is_new { @@ -415,7 +415,7 @@ impl PlaceTableBuilder { } } - pub fn finish(self) -> PlaceTable { + pub(crate) fn finish(self) -> PlaceTable { PlaceTable { symbols: self.symbols.build(), members: self.member.build(), @@ -510,11 +510,11 @@ impl<'a> ParentPlaceIterState<'a> { } impl<'a> ParentPlaceIter<'a> { - pub(super) fn for_symbol() -> Self { + fn for_symbol() -> Self { ParentPlaceIter { state: None } } - pub(super) fn for_member( + fn for_member( expression: &'a MemberExpr, symbol_table: &'a SymbolTable, member_table: &'a MemberTable, diff --git a/crates/ty_python_core/src/reachability_constraints.rs b/crates/ty_python_core/src/reachability_constraints.rs index 04625ecbe2..ad188fec73 100644 --- a/crates/ty_python_core/src/reachability_constraints.rs +++ b/crates/ty_python_core/src/reachability_constraints.rs @@ -100,11 +100,11 @@ impl ScopedReachabilityConstraintId { pub const ALWAYS_FALSE: ScopedReachabilityConstraintId = ScopedReachabilityConstraintId(0xffff_fffd); - pub fn is_terminal(self) -> bool { + pub(crate) fn is_terminal(self) -> bool { self.0 >= SMALLEST_TERMINAL.0 } - pub fn as_u32(self) -> u32 { + fn as_u32(self) -> u32 { self.0 } } diff --git a/crates/ty_python_core/src/scope.rs b/crates/ty_python_core/src/scope.rs index 72abec9090..b62d495bd1 100644 --- a/crates/ty_python_core/src/scope.rs +++ b/crates/ty_python_core/src/scope.rs @@ -48,7 +48,7 @@ impl<'db> ScopeId<'db> { } /// Returns the class definition for the enclosing class if this scope is a method body. - pub fn class_definition_of_method(self, db: &'db dyn Db) -> Option> { + fn class_definition_of_method(self, db: &'db dyn Db) -> Option> { semantic_index(db, self.file(db)).class_definition_of_method(self.file_scope_id(db)) } @@ -152,7 +152,7 @@ impl Scope { self.kind().visibility() } - pub fn descendants(&self) -> Range { + pub(crate) fn descendants(&self) -> Range { self.descendants.clone() } @@ -227,7 +227,7 @@ impl ScopeKind { } } - pub(crate) const fn visibility(self) -> ScopeVisibility { + const fn visibility(self) -> ScopeVisibility { match self { ScopeKind::Module | ScopeKind::Class => ScopeVisibility::Public, ScopeKind::TypeParams @@ -259,7 +259,7 @@ impl ScopeKind { matches!(self, ScopeKind::Module) } - pub const fn is_annotation(self) -> bool { + pub(crate) const fn is_annotation(self) -> bool { matches!(self, ScopeKind::TypeParams | ScopeKind::TypeAlias) } @@ -328,7 +328,7 @@ impl NodeWithScopeRef<'_> { } } - pub fn node_key(self) -> NodeWithScopeKey { + pub(crate) fn node_key(self) -> NodeWithScopeKey { match self { NodeWithScopeRef::Module => NodeWithScopeKey::Module, NodeWithScopeRef::Class(class) => NodeWithScopeKey::Class(NodeKey::from_node(class)), @@ -423,7 +423,7 @@ impl NodeWithScopeKind { self.as_function().expect("expected function") } - pub fn as_type_alias(&self) -> Option<&AstNodeRef> { + fn as_type_alias(&self) -> Option<&AstNodeRef> { match self { Self::TypeAlias(type_alias) => Some(type_alias), _ => None, diff --git a/crates/ty_python_core/src/symbol.rs b/crates/ty_python_core/src/symbol.rs index b54f482ae9..8245f99a46 100644 --- a/crates/ty_python_core/src/symbol.rs +++ b/crates/ty_python_core/src/symbol.rs @@ -50,7 +50,7 @@ bitflags! { impl get_size2::GetSize for SymbolFlags {} impl Symbol { - pub const fn new(name: Name) -> Self { + pub(crate) const fn new(name: Name) -> Self { Self { name, flags: SymbolFlags::empty(), @@ -122,7 +122,7 @@ impl Symbol { self.flags.contains(SymbolFlags::IS_REASSIGNED) } - pub fn is_parameter(&self) -> bool { + pub(crate) fn is_parameter(&self) -> bool { self.flags.contains(SymbolFlags::IS_PARAMETER) } diff --git a/crates/ty_python_core/src/unpack.rs b/crates/ty_python_core/src/unpack.rs index 2037dede53..2f43e915c2 100644 --- a/crates/ty_python_core/src/unpack.rs +++ b/crates/ty_python_core/src/unpack.rs @@ -80,7 +80,7 @@ pub struct UnpackValue<'db> { } impl<'db> UnpackValue<'db> { - pub fn new(kind: UnpackKind, expression: Expression<'db>) -> Self { + pub(crate) fn new(kind: UnpackKind, expression: Expression<'db>) -> Self { Self { kind, expression } } diff --git a/crates/ty_python_core/src/use_def.rs b/crates/ty_python_core/src/use_def.rs index 963873dc6f..6e9ccb887c 100644 --- a/crates/ty_python_core/src/use_def.rs +++ b/crates/ty_python_core/src/use_def.rs @@ -997,7 +997,7 @@ impl<'db> UseDefMap<'db> { ) } - pub(crate) fn end_of_scope_member_bindings( + fn end_of_scope_member_bindings( &self, member: ScopedMemberId, ) -> BindingWithConstraintsIterator<'_, 'db> { @@ -1113,7 +1113,7 @@ impl<'db> UseDefMap<'db> { self.declarations_iterator(declarations, BoundnessAnalysis::BasedOnUnboundVisibility) } - pub(crate) fn end_of_scope_member_declarations<'map>( + fn end_of_scope_member_declarations<'map>( &'map self, member: ScopedMemberId, ) -> DeclarationsIterator<'map, 'db> { @@ -1772,7 +1772,7 @@ pub(super) struct UseDefMapBuilder<'db> { used_bindings: IndexVec, /// Builder of predicates. - pub(super) predicates: PredicatesBuilder<'db>, + predicates: PredicatesBuilder<'db>, /// Builder of reachability constraints. pub(super) reachability_constraints: ReachabilityConstraintsBuilder, diff --git a/crates/ty_python_core/src/use_def/place_state.rs b/crates/ty_python_core/src/use_def/place_state.rs index 4467d91958..01d0494bdc 100644 --- a/crates/ty_python_core/src/use_def/place_state.rs +++ b/crates/ty_python_core/src/use_def/place_state.rs @@ -105,7 +105,7 @@ pub(crate) enum FutureDefinitions { } impl PreviousDefinitions { - pub(super) fn are_shadowed(self) -> bool { + fn are_shadowed(self) -> bool { matches!(self, PreviousDefinitions::AreShadowed) } } @@ -159,7 +159,7 @@ impl Declarations { } /// Add given reachability constraint to all live declarations. - pub(super) fn record_reachability_constraint( + fn record_reachability_constraint( &mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder, constraint: ScopedReachabilityConstraintId, @@ -386,7 +386,7 @@ impl Bindings { } /// Add given constraint to all live bindings. - pub(super) fn record_narrowing_constraint( + fn record_narrowing_constraint( &mut self, narrowing_constraints: &mut NarrowingConstraintsBuilder, constraint: ScopedNarrowingConstraint, @@ -398,7 +398,7 @@ impl Bindings { } /// Add given reachability constraint to all live bindings. - pub(super) fn record_reachability_constraint( + fn record_reachability_constraint( &mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder, constraint: ScopedReachabilityConstraintId, @@ -621,7 +621,7 @@ mod tests { } #[track_caller] - pub(crate) fn assert_declarations(place: &PlaceState, expected: &[&str]) { + fn assert_declarations(place: &PlaceState, expected: &[&str]) { let actual = place .declarations() .iter() diff --git a/crates/ty_python_semantic/src/db.rs b/crates/ty_python_semantic/src/db.rs index 16e02dc948..7d34116493 100644 --- a/crates/ty_python_semantic/src/db.rs +++ b/crates/ty_python_semantic/src/db.rs @@ -64,7 +64,7 @@ pub(crate) mod tests { } impl TestDb { - pub(crate) fn new() -> Self { + fn new() -> Self { let events = Events::default(); Self { storage: salsa::Storage::new(Some(Box::new({ diff --git a/crates/ty_python_semantic/src/diagnostic/mod.rs b/crates/ty_python_semantic/src/diagnostic/mod.rs index 395a60b6fc..6ed4212c78 100644 --- a/crates/ty_python_semantic/src/diagnostic/mod.rs +++ b/crates/ty_python_semantic/src/diagnostic/mod.rs @@ -44,7 +44,7 @@ pub fn inferred_python_version_source_annotation( /// /// ty can infer the Python version from various sources, such as command-line arguments, /// configuration files, or defaults. -pub fn add_inferred_python_version_hint_to_diagnostic( +pub(crate) fn add_inferred_python_version_hint_to_diagnostic( db: &dyn Db, diagnostic: &mut Diagnostic, action: &str, diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 5b9b39d362..90816215fd 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -9,23 +9,24 @@ use crate::suppression::{ }; use crate::types::check_types; pub use db::Db; -pub use diagnostic::{ - add_inferred_python_version_hint_to_diagnostic, inferred_python_version_source_annotation, -}; +pub(crate) use diagnostic::add_inferred_python_version_hint_to_diagnostic; +pub use diagnostic::inferred_python_version_source_annotation; pub use fixes::{fix_all_diagnostics, suppress_all_diagnostics}; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_db::source::{SourceTextError, source_text}; use rustc_hash::FxHasher; +#[expect(unused_imports)] +pub(crate) use semantic_model::HasOptionalDefinition; pub use semantic_model::{ - Completion, ExpectedStringLiteralCompletion, HasDefinition, HasOptionalDefinition, HasType, - MemberDefinition, NameKind, SemanticModel, + Completion, ExpectedStringLiteralCompletion, HasDefinition, HasType, MemberDefinition, + NameKind, SemanticModel, }; use std::hash::BuildHasherDefault; -pub use suppression::{ +pub use suppression::suppress_single; +pub(crate) use suppression::{ SuppressFix, UNUSED_IGNORE_COMMENT, is_unused_ignore_comment_lint, suppress_all, - suppress_single, }; use ty_module_resolver::ModuleGlobSet; use ty_python_core::definition::docstring_from_body; @@ -81,7 +82,7 @@ pub fn default_lint_registry() -> &'static LintRegistry { } /// Register all known semantic lints. -pub fn register_lints(registry: &mut LintRegistryBuilder) { +fn register_lints(registry: &mut LintRegistryBuilder) { types::register_lints(registry); registry.register_lint(&UNUSED_IGNORE_COMMENT); registry.register_lint(&UNUSED_TYPE_IGNORE_COMMENT); @@ -222,7 +223,7 @@ pub struct IOErrorDiagnostic { } impl IOErrorDiagnostic { - pub fn to_diagnostic(&self) -> Diagnostic { + fn to_diagnostic(&self) -> Diagnostic { let mut diag = Diagnostic::new(DiagnosticId::Io, Severity::Error, &self.error); diag.annotate(Annotation::primary(Span::from(self.file))); diag @@ -234,4 +235,4 @@ impl IOErrorDiagnostic { /// values that will soon converge, but where unioning in the early value causes an /// unrecoverable loss of precision. This constant controls how many iterations /// are considered likely to produce "tainted" results that should be discarded. -pub(crate) const TAINTED_CYCLES: u32 = 3; +const TAINTED_CYCLES: u32 = 3; diff --git a/crates/ty_python_semantic/src/lint.rs b/crates/ty_python_semantic/src/lint.rs index 52b6909d70..4f2487eebf 100644 --- a/crates/ty_python_semantic/src/lint.rs +++ b/crates/ty_python_semantic/src/lint.rs @@ -123,7 +123,7 @@ impl LintMetadata { self.documentation_lines().join("\n") } - pub fn documentation_url(&self) -> String { + pub(crate) fn documentation_url(&self) -> String { lint_documentation_url(self.name()) } @@ -144,7 +144,7 @@ impl LintMetadata { } } -pub fn lint_documentation_url(lint_name: LintName) -> String { +pub(crate) fn lint_documentation_url(lint_name: LintName) -> String { format!("https://ty.dev/rules#{lint_name}") } @@ -205,11 +205,11 @@ impl LintStatus { LintStatus::Deprecated { since, reason } } - pub const fn removed(since: &'static str, reason: &'static str) -> Self { + pub(crate) const fn removed(since: &'static str, reason: &'static str) -> Self { LintStatus::Removed { since, reason } } - pub const fn is_removed(&self) -> bool { + const fn is_removed(&self) -> bool { matches!(self, LintStatus::Removed { .. }) } @@ -359,7 +359,7 @@ pub struct LintRegistryBuilder { impl LintRegistryBuilder { #[track_caller] - pub fn register_lint(&mut self, lint: &'static LintMetadata) { + pub(crate) fn register_lint(&mut self, lint: &'static LintMetadata) { assert_eq!( self.by_name.insert(&*lint.name, lint.into()), None, @@ -396,7 +396,7 @@ impl LintRegistryBuilder { ); } - pub fn build(self) -> LintRegistry { + pub(crate) fn build(self) -> LintRegistry { LintRegistry { lints: self.lints, by_name: self.by_name, @@ -593,16 +593,16 @@ impl RuleSelection { } /// Returns the configured severity for the lint with the given id or `None` if the lint is disabled. - pub fn severity(&self, lint: LintId) -> Option { + pub(crate) fn severity(&self, lint: LintId) -> Option { self.lints.get(&lint).map(|(severity, _)| *severity) } - pub fn get(&self, lint: LintId) -> Option<(Severity, LintSource)> { + pub(crate) fn get(&self, lint: LintId) -> Option<(Severity, LintSource)> { self.lints.get(&lint).copied() } /// Returns `true` if the `lint` is enabled. - pub fn is_enabled(&self, lint: LintId) -> bool { + pub(crate) fn is_enabled(&self, lint: LintId) -> bool { self.severity(lint).is_some() } diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 7e3e81a26d..70779da63f 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -88,7 +88,7 @@ pub(crate) enum PublicTypePolicy { impl PublicTypePolicy { /// Apply the public-type policy to the raw type. - pub(crate) fn apply_if_needed<'db>(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn apply_if_needed<'db>(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { match self { Self::Raw => ty, Self::Promote => ty.promote(db).promote_singletons(db), @@ -146,7 +146,7 @@ pub(crate) struct DefinedPlace<'db> { } impl<'db> DefinedPlace<'db> { - pub(crate) fn new(ty: Type<'db>) -> Self { + fn new(ty: Type<'db>) -> Self { Self { ty, origin: TypeOrigin::Inferred, @@ -156,7 +156,7 @@ impl<'db> DefinedPlace<'db> { } } - pub(crate) fn with_origin(mut self, origin: TypeOrigin) -> Self { + fn with_origin(mut self, origin: TypeOrigin) -> Self { self.origin = origin; self } @@ -166,17 +166,17 @@ impl<'db> DefinedPlace<'db> { self } - pub(crate) fn with_public_type_policy(mut self, public_type_policy: PublicTypePolicy) -> Self { + fn with_public_type_policy(mut self, public_type_policy: PublicTypePolicy) -> Self { self.public_type_policy = public_type_policy; self } - pub(crate) fn with_definition(mut self, definition: Definition<'db>) -> Self { + fn with_definition(mut self, definition: Definition<'db>) -> Self { self.provenance = Provenance::SingleDefinition(definition); self } - pub(crate) fn with_provenance(mut self, provenance: Provenance<'db>) -> Self { + fn with_provenance(mut self, provenance: Provenance<'db>) -> Self { self.provenance = provenance; self } @@ -309,10 +309,7 @@ impl<'db> Place<'db> { /// Set the public-type policy for this place. #[must_use] - pub(crate) fn with_public_type_policy( - self, - new_public_type_policy: PublicTypePolicy, - ) -> Place<'db> { + fn with_public_type_policy(self, new_public_type_policy: PublicTypePolicy) -> Place<'db> { match self { Place::Defined(defined) => { Place::Defined(defined.with_public_type_policy(new_public_type_policy)) diff --git a/crates/ty_python_semantic/src/reachability.rs b/crates/ty_python_semantic/src/reachability.rs index 34775a41d5..032f8d02ea 100644 --- a/crates/ty_python_semantic/src/reachability.rs +++ b/crates/ty_python_semantic/src/reachability.rs @@ -1628,7 +1628,7 @@ impl<'db> ReachabilityEvaluationCache<'db> { /// predicate determines whether the constraint belongs to the primary scope. A primary-scope /// constraint from the primary graph is cached by dense index; all other constraints are cached /// by graph identity and id. - pub(crate) fn evaluate( + fn evaluate( &self, db: &'db dyn Db, constraints: &ReachabilityConstraints, diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 104ae4cde4..9910fb77cf 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -389,7 +389,7 @@ impl<'db> SemanticModel<'db> { /// /// If we're analyzing a string annotation, it will return the string literal's node. /// Otherwise it will return the input. - pub fn node_in_ast<'a>(&'a self, node: ast::AnyNodeRef<'a>) -> ast::AnyNodeRef<'a> { + fn node_in_ast<'a>(&'a self, node: ast::AnyNodeRef<'a>) -> ast::AnyNodeRef<'a> { if let Some(string_annotation) = &self.in_string_annotation_expr { (&**string_annotation).into() } else { @@ -401,7 +401,7 @@ impl<'db> SemanticModel<'db> { /// /// If we're analyzing a string annotation, it will return the string literal's expression. /// Otherwise it will return the input. - pub fn expr_in_ast<'a>(&'a self, expr: &'a Expr) -> &'a Expr { + fn expr_in_ast<'a>(&'a self, expr: &'a Expr) -> &'a Expr { if let Some(string_annotation) = &self.in_string_annotation_expr { string_annotation } else { @@ -413,7 +413,7 @@ impl<'db> SemanticModel<'db> { /// /// If we're analyzing a string annotation, it will return the string literal's expression. /// Otherwise it will return the input. - pub fn expr_ref_in_ast<'a>(&'a self, expr: ExprRef<'a>) -> ExprRef<'a> { + fn expr_ref_in_ast<'a>(&'a self, expr: ExprRef<'a>) -> ExprRef<'a> { if let Some(string_annotation) = &self.in_string_annotation_expr { ExprRef::from(string_annotation) } else { @@ -681,7 +681,7 @@ pub trait HasDefinition { fn definition<'db>(&self, model: &SemanticModel<'db>) -> Definition<'db>; } -pub trait HasOptionalDefinition { +pub(crate) trait HasOptionalDefinition { /// Returns the definition of `self`, if it has one. /// /// ## Panics diff --git a/crates/ty_python_semantic/src/suppression.rs b/crates/ty_python_semantic/src/suppression.rs index e9ebac6d57..2682980c6c 100644 --- a/crates/ty_python_semantic/src/suppression.rs +++ b/crates/ty_python_semantic/src/suppression.rs @@ -17,7 +17,8 @@ use rustc_hash::FxHasher; use crate::diagnostic::DiagnosticGuard; use crate::lint::{GetLintError, Level, LintMetadata, LintRegistry, LintStatus}; -pub use crate::suppression::add_ignore::{SuppressFix, suppress_all, suppress_single}; +pub use crate::suppression::add_ignore::suppress_single; +pub(crate) use crate::suppression::add_ignore::{SuppressFix, suppress_all}; use crate::suppression::parser::{ ParseError, ParseErrorKind, SuppressionComment, SuppressionParser, }; @@ -27,7 +28,7 @@ use crate::{Db, declare_lint, lint::LintId}; declare_lint! { #[doc = include_str!("../resources/lint_docs/unused-ignore-comment.md")] - pub static UNUSED_IGNORE_COMMENT = { + pub(crate) static UNUSED_IGNORE_COMMENT = { summary: "detects unused `ty: ignore` comments", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Warn, @@ -70,7 +71,7 @@ declare_lint! { } } -pub fn is_unused_ignore_comment_lint(name: LintName) -> bool { +pub(crate) fn is_unused_ignore_comment_lint(name: LintName) -> bool { name == UNUSED_IGNORE_COMMENT.name() || name == UNUSED_TYPE_IGNORE_COMMENT.name() } @@ -264,7 +265,7 @@ impl<'a> CheckSuppressionsContext<'a> { /// /// This type exists to separate the phases of "check if a diagnostic should /// be reported" and "build the actual diagnostic." -pub(crate) struct SuppressionDiagnosticGuardBuilder<'ctx, 'db> { +struct SuppressionDiagnosticGuardBuilder<'ctx, 'db> { ctx: &'ctx CheckSuppressionsContext<'db>, id: DiagnosticId, range: TextRange, @@ -294,10 +295,7 @@ impl<'ctx, 'db> SuppressionDiagnosticGuardBuilder<'ctx, 'db> { /// /// The diagnostic can be further mutated on the guard via its `DerefMut` /// impl to `Diagnostic`. - pub(crate) fn into_diagnostic( - self, - message: impl IntoDiagnosticMessage, - ) -> DiagnosticGuard<'ctx> { + fn into_diagnostic(self, message: impl IntoDiagnosticMessage) -> DiagnosticGuard<'ctx> { let mut diag = Diagnostic::new(self.id, self.severity, message); let primary_span = Span::from(self.ctx.file).with_range(self.range); diff --git a/crates/ty_python_semantic/src/suppression/add_ignore.rs b/crates/ty_python_semantic/src/suppression/add_ignore.rs index 94781ed3f6..1a764d6f7e 100644 --- a/crates/ty_python_semantic/src/suppression/add_ignore.rs +++ b/crates/ty_python_semantic/src/suppression/add_ignore.rs @@ -31,7 +31,7 @@ use crate::suppression::{ /// an edit. It appends codes once to each applicable existing suppression and otherwise inserts at /// most one end-of-line suppression at each destination. Every returned [`SuppressFix`] records /// how many diagnostics its edit accounts for. -pub fn suppress_all( +pub(crate) fn suppress_all( db: &dyn Db, file: File, ids_with_range: &[(LintName, TextRange)], @@ -159,10 +159,10 @@ pub fn suppress_all( } /// Fix to suppress one or more diagnostics. -pub struct SuppressFix { - pub fix: Fix, +pub(crate) struct SuppressFix { + pub(crate) fix: Fix, /// The number of diagnostics that will be suppressed if this fix is applied. - pub suppressed_diagnostics: usize, + pub(crate) suppressed_diagnostics: usize, } /// Creates a fix to suppress a single lint. diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3e280da02e..e7bb25cdc6 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -25,8 +25,9 @@ use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module}; pub(crate) use self::callable::UpcastPolicy; pub use self::cyclic::CycleDetector; pub(crate) use self::cyclic::TypeTransformer; +pub(crate) use self::diagnostic::TypeCheckDiagnostics; pub(crate) use self::diagnostic::register_lints; -pub use self::diagnostic::{TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_REFERENCE}; +pub use self::diagnostic::{UNDEFINED_REVEAL, UNRESOLVED_REFERENCE}; pub(crate) use self::infer::{ InferredDeclaration, TypeContext, infer_complete_scope_types, infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types, @@ -46,14 +47,16 @@ use self::set_theoretic::KnownUnion; pub(crate) use self::set_theoretic::builder::{ IntersectionBuilder, UnionAccumulator, UnionBuilder, }; -pub use self::set_theoretic::{ - IntersectionType, NegativeIntersectionElements, NegativeIntersectionElementsIterator, UnionType, +pub use self::set_theoretic::{IntersectionType, UnionType}; +#[expect(unused_imports)] +pub(crate) use self::set_theoretic::{ + NegativeIntersectionElements, NegativeIntersectionElementsIterator, }; pub use self::signatures::ParameterKind; pub(crate) use self::signatures::Signature; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; pub(crate) use self::type_expansion::expand_type; -pub use crate::diagnostic::add_inferred_python_version_hint_to_diagnostic; +pub(crate) use crate::diagnostic::add_inferred_python_version_hint_to_diagnostic; use crate::place::{ DefinedPlace, Definedness, Place, PlaceAndQualifiers, Provenance, TypeOrigin, builtins_module_scope, imported_symbol, known_module_symbol, place_from_bindings, @@ -91,9 +94,10 @@ use crate::types::tuple::TupleSpec; pub use crate::types::type_alias::TypeAliasType; pub use crate::types::type_form::TypeFormType; pub(crate) use crate::types::typed_dict::TypedDictType; +pub(crate) use crate::types::typevar::TypeVarBoundOrConstraints; pub use crate::types::typevar::{ - BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, ParamSpecAttrKind, - TypeVarBoundOrConstraints, TypeVarKind, TypeVarNonce, + BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, ParamSpecAttrKind, TypeVarKind, + TypeVarNonce, }; use crate::types::typevar::{TypeVarInstance, TypeVarSet}; pub use crate::types::variance::TypeVarVariance; @@ -317,7 +321,7 @@ impl<'db> ApplyTypeMappingVisitor<'db> { .get_or_init(|| Rc::new(CycleDetector::new(true))) } - pub(crate) fn visit( + fn visit( &self, db: &'db dyn Db, ty: Type<'db>, @@ -344,7 +348,7 @@ impl<'db> ApplyTypeMappingVisitor<'db> { .visit_type(db, ty, func) } - pub(crate) fn is_equivalent_to_materialization( + fn is_equivalent_to_materialization( &self, db: &'db dyn Db, left: Type<'db>, @@ -356,7 +360,7 @@ impl<'db> ApplyTypeMappingVisitor<'db> { }) } - pub(crate) fn for_new_materialization_root(&self) -> Self { + fn for_new_materialization_root(&self) -> Self { let materialization_equivalence = OnceCell::new(); let was_empty = materialization_equivalence.set(Rc::clone(self.materialization_equivalence())); @@ -377,9 +381,8 @@ pub(crate) type FindLegacyTypeVarsVisitor<'db> = pub(crate) struct FindLegacyTypeVars; /// A [`CycleDetector`] that is used in `visit_specialization` methods. -pub(crate) type SpecializationVisitor<'db> = - CycleDetector<'db, VisitSpecialization, Type<'db>, (), 3>; -pub(crate) struct VisitSpecialization; +type SpecializationVisitor<'db> = CycleDetector<'db, VisitSpecialization, Type<'db>, (), 3>; +struct VisitSpecialization; /// How a generic type has been specialized. /// @@ -401,7 +404,7 @@ pub enum MaterializationKind { impl MaterializationKind { /// Flip the materialization type: `Top` becomes `Bottom` and vice versa. #[must_use] - pub const fn flip(self) -> Self { + const fn flip(self) -> Self { match self { Self::Top => Self::Bottom, Self::Bottom => Self::Top, @@ -488,32 +491,32 @@ impl MemberLookupPolicy { /// If false - Look up the attribute on the meta-type, but fall back to attributes on the instance /// if the meta-type attribute is not found or if the meta-type attribute is not a data /// descriptor. - pub(crate) const fn no_instance_fallback(self) -> bool { + const fn no_instance_fallback(self) -> bool { self.contains(Self::NO_INSTANCE_FALLBACK) } /// Exclude attributes defined on `object` when looking up attributes. - pub(crate) const fn mro_no_object_fallback(self) -> bool { + const fn mro_no_object_fallback(self) -> bool { self.contains(Self::MRO_NO_OBJECT_FALLBACK) } /// Exclude attributes defined on `type` when looking up meta-class-attributes. - pub(crate) const fn meta_class_no_type_fallback(self) -> bool { + const fn meta_class_no_type_fallback(self) -> bool { self.contains(Self::META_CLASS_NO_TYPE_FALLBACK) } /// Exclude attributes defined on `int` or `str` when looking up attributes. - pub(crate) const fn mro_no_int_or_str_fallback(self) -> bool { + const fn mro_no_int_or_str_fallback(self) -> bool { self.contains(Self::MRO_NO_INT_OR_STR_LOOKUP) } /// Do not call `__getattr__` during member lookup. - pub(crate) const fn no_getattr_lookup(self) -> bool { + const fn no_getattr_lookup(self) -> bool { self.contains(Self::NO_GETATTR_LOOKUP) } /// Ignore members that are only available through a dynamic type. - pub(crate) const fn require_concrete(self) -> bool { + const fn require_concrete(self) -> bool { self.contains(Self::REQUIRE_CONCRETE) } } @@ -538,7 +541,7 @@ struct MemberLookupKey<'db> { /// Meta data for `Type::Todo`, which represents a known limitation in ty. #[cfg(debug_assertions)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)] -pub struct TodoType(pub &'static str); +pub struct TodoType(&'static str); #[cfg(debug_assertions)] impl std::fmt::Display for TodoType { @@ -655,7 +658,7 @@ fn walk_property_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( impl get_size2::GetSize for PropertyInstanceType<'_> {} impl<'db> PropertyInstanceType<'db> { - pub fn new( + fn new( db: &'db dyn Db, getter: Option>, setter: Option>, @@ -891,7 +894,7 @@ impl<'db> DataclassParams<'db> { ) } - pub(super) fn recursive_type_normalized_impl( + fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, @@ -1044,17 +1047,17 @@ pub(crate) enum InstanceProjection { } impl InstanceProjection { - pub(crate) const fn is_exact(&self) -> bool { + const fn is_exact(&self) -> bool { matches!(self, Self::Exact(_)) } - pub(crate) fn into_inner(self) -> T { + fn into_inner(self) -> T { match self { Self::Exact(value) | Self::OverApproximation(value) => value, } } - pub(crate) fn map(self, transform: impl FnOnce(T) -> U) -> InstanceProjection { + fn map(self, transform: impl FnOnce(T) -> U) -> InstanceProjection { match self { Self::Exact(value) => InstanceProjection::Exact(transform(value)), Self::OverApproximation(value) => { @@ -1063,7 +1066,7 @@ impl InstanceProjection { } } - pub(crate) const fn new(value: T, is_exact: bool) -> Self { + const fn new(value: T, is_exact: bool) -> Self { if is_exact { Self::Exact(value) } else { @@ -1144,11 +1147,11 @@ impl<'db> Type<'db> { Self::Divergent(DivergentType::new(id)) } - pub(crate) const fn is_divergent(&self) -> bool { + const fn is_divergent(&self) -> bool { matches!(self, Type::Divergent(_)) } - pub(crate) const fn as_divergent(self) -> Option { + const fn as_divergent(self) -> Option { match self { Type::Divergent(divergent) => Some(divergent), _ => None, @@ -1193,7 +1196,7 @@ impl<'db> Type<'db> { }) } - pub(crate) const fn as_intersection(self) -> Option> { + const fn as_intersection(self) -> Option> { match self { Type::Intersection(intersection) => Some(intersection), _ => None, @@ -1223,7 +1226,7 @@ impl<'db> Type<'db> { } /// Returns `true` if this type contains a `Self` type variable. - pub(crate) fn contains_self(self, db: &'db dyn Db) -> bool { + fn contains_self(self, db: &'db dyn Db) -> bool { if let Type::NominalInstance(instance) = self && !instance.is_definition_generic(db) { @@ -1254,7 +1257,7 @@ impl<'db> Type<'db> { /// /// Types that defer `Self` binding to call time (functions, bound methods, function-like /// callables) are skipped; see `supports_self_binding`. - pub(crate) fn bind_self_typevars(self, db: &'db dyn Db, self_type: Type<'db>) -> Self { + fn bind_self_typevars(self, db: &'db dyn Db, self_type: Type<'db>) -> Self { if !self.supports_self_binding(db) { return self; } @@ -1267,7 +1270,7 @@ impl<'db> Type<'db> { } /// Returns `true` if `self` is [`Type::Callable`]. - pub(crate) const fn is_callable_type(&self) -> bool { + const fn is_callable_type(&self) -> bool { matches!(self, Type::Callable(..)) } @@ -1340,7 +1343,7 @@ impl<'db> Type<'db> { self.is_instance_of(db, KnownClass::NotImplementedType) } - pub(crate) fn is_todo(&self) -> bool { + fn is_todo(&self) -> bool { self.as_dynamic().is_some_and(|dynamic| match dynamic { DynamicType::Any | DynamicType::Unknown @@ -1360,7 +1363,7 @@ impl<'db> Type<'db> { /// /// For example, whereas `` is a generic type, `` /// is a specialization of that type. - pub(crate) fn is_specialized_generic(self, db: &'db dyn Db) -> bool { + fn is_specialized_generic(self, db: &'db dyn Db) -> bool { match self { Type::Union(union) => union .elements(db) @@ -1393,7 +1396,7 @@ impl<'db> Type<'db> { } } - pub(crate) const fn is_dynamic(&self) -> bool { + const fn is_dynamic(&self) -> bool { matches!( self, Type::Dynamic(_) @@ -1413,7 +1416,7 @@ impl<'db> Type<'db> { /// Currently checks for instances of `types.CoroutineType` (returned by `async def` calls). /// Unions are considered awaitable only if every element is awaitable. /// Intersections are considered awaitable if any positive element is awaitable. - pub(crate) fn is_awaitable(self, db: &'db dyn Db) -> bool { + fn is_awaitable(self, db: &'db dyn Db) -> bool { match self { Type::NominalInstance(instance) => { matches!(instance.known_class(db), Some(KnownClass::CoroutineType)) @@ -1453,7 +1456,7 @@ impl<'db> Type<'db> { } /// If the type is a specialized instance of the given `KnownClass`, returns the specialization. - pub(crate) fn known_specialization( + fn known_specialization( &self, db: &'db dyn Db, known_class: KnownClass, @@ -1463,7 +1466,7 @@ impl<'db> Type<'db> { } /// If the type is a specialized instance of the given class, returns the specialization. - pub(crate) fn specialization_of( + fn specialization_of( self, db: &'db dyn Db, expected_class: StaticClassLiteral<'_>, @@ -1475,7 +1478,7 @@ impl<'db> Type<'db> { } /// If this type is a class instance, returns the class and its specialization. - pub(crate) fn class_specialization( + fn class_specialization( self, db: &'db dyn Db, ) -> Option<(StaticClassLiteral<'db>, Specialization<'db>)> { @@ -1485,7 +1488,7 @@ impl<'db> Type<'db> { } /// If this type is a class instance, returns its class. - pub(crate) fn nominal_class(self, db: &'db dyn Db) -> Option> { + fn nominal_class(self, db: &'db dyn Db) -> Option> { match self { Type::NominalInstance(instance) => Some(instance.class(db)), Type::ProtocolInstance(instance) => instance.class_origin(db).map(|class| *class), @@ -1510,21 +1513,21 @@ impl<'db> Type<'db> { /// /// This is the case for any type which may contain types in non-covariant position within it, /// e.g., nominal instances of a generic class, or callables. - pub(crate) fn may_prefer_declared_type(self, db: &'db dyn Db) -> bool { + fn may_prefer_declared_type(self, db: &'db dyn Db) -> bool { self.class_specialization(db).is_some() || self.expand_eagerly(db).is_callable_type() } /// Returns the top materialization (or upper bound materialization) of this type, which is the /// most general form of the type that is fully static. #[must_use] - pub(crate) fn top_materialization(&self, db: &'db dyn Db) -> Type<'db> { + fn top_materialization(&self, db: &'db dyn Db) -> Type<'db> { (*self).cached_materialization(db, MaterializationKind::Top) } /// Returns the bottom materialization (or lower bound materialization) of this type, which is /// the most specific form of the type that is fully static. #[must_use] - pub(crate) fn bottom_materialization(&self, db: &'db dyn Db) -> Type<'db> { + fn bottom_materialization(&self, db: &'db dyn Db) -> Type<'db> { (*self).cached_materialization(db, MaterializationKind::Bottom) } @@ -1591,7 +1594,7 @@ impl<'db> Type<'db> { /// - `materialize()` calls `apply_type_mapping()` (or `apply_type_mapping_impl()`) /// - `materialize_impl()` gets called from `apply_type_mapping()` or from another /// `materialize_impl()` - pub(crate) fn materialize( + fn materialize( &self, db: &'db dyn Db, materialization_kind: MaterializationKind, @@ -1605,11 +1608,11 @@ impl<'db> Type<'db> { ) } - pub(crate) fn has_dynamic(self, db: &'db dyn Db) -> bool { + fn has_dynamic(self, db: &'db dyn Db) -> bool { any_over_type(db, self, false, |ty| ty.is_dynamic()) } - pub(crate) const fn as_special_form(self) -> Option { + const fn as_special_form(self) -> Option { match self { Type::SpecialForm(special_form) => Some(special_form), _ => None, @@ -1630,7 +1633,7 @@ impl<'db> Type<'db> { } } - pub(crate) const fn as_type_alias(self) -> Option> { + const fn as_type_alias(self) -> Option> { match self { Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => Some(type_alias), _ => None, @@ -1639,7 +1642,7 @@ impl<'db> Type<'db> { /// If this type is a `Type::TypeAlias`, recursively resolves it to its /// underlying value type. Otherwise, returns `self` unchanged. - pub(crate) fn resolve_type_alias(self, db: &'db dyn Db) -> Type<'db> { + fn resolve_type_alias(self, db: &'db dyn Db) -> Type<'db> { let mut ty = self; while let Type::TypeAlias(alias) = ty { ty = alias.value_type(db); @@ -1649,7 +1652,7 @@ impl<'db> Type<'db> { /// Returns `Some(UnionType)` if this type behaves like a union. Apart from explicit unions, /// this returns `Some` for `TypeAlias`es of unions and `NewType`s of `float` and `complex`. - pub(crate) fn as_union_like(self, db: &'db dyn Db) -> Option> { + fn as_union_like(self, db: &'db dyn Db) -> Option> { match self.resolve_type_alias(db) { Type::Union(union) => Some(union), Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).as_union_like(db), @@ -1657,25 +1660,25 @@ impl<'db> Type<'db> { } } - pub(crate) const fn as_dynamic(self) -> Option> { + const fn as_dynamic(self) -> Option> { match self { Type::Dynamic(dynamic_type) => Some(dynamic_type), _ => None, } } - pub(crate) const fn as_callable(self) -> Option> { + const fn as_callable(self) -> Option> { match self { Type::Callable(callable_type) => Some(callable_type), _ => None, } } - pub(crate) const fn expect_dynamic(self) -> DynamicType<'db> { + const fn expect_dynamic(self) -> DynamicType<'db> { self.as_dynamic().expect("Expected a Type::Dynamic variant") } - pub(crate) const fn as_protocol_instance(self) -> Option> { + const fn as_protocol_instance(self) -> Option> { match self { Type::ProtocolInstance(instance) => Some(instance), _ => None, @@ -1684,7 +1687,7 @@ impl<'db> Type<'db> { #[cfg(test)] #[track_caller] - pub(crate) const fn expect_class_literal(self) -> ClassLiteral<'db> { + const fn expect_class_literal(self) -> ClassLiteral<'db> { self.as_class_literal() .expect("Expected a Type::ClassLiteral variant") } @@ -1697,25 +1700,25 @@ impl<'db> Type<'db> { matches!(self, Type::ClassLiteral(..)) } - pub(crate) const fn as_literal_value(self) -> Option> { + const fn as_literal_value(self) -> Option> { match self { Type::LiteralValue(literal) => Some(literal), _ => None, } } - pub(crate) fn as_literal_value_kind(self) -> Option> { + fn as_literal_value_kind(self) -> Option> { match self { Type::LiteralValue(literal) => Some(literal.kind()), _ => None, } } - pub(crate) const fn is_typed_dict(&self) -> bool { + const fn is_typed_dict(&self) -> bool { matches!(self, Type::TypedDict(..)) } - pub(crate) const fn as_typed_dict(self) -> Option> { + const fn as_typed_dict(self) -> Option> { match self { Type::TypedDict(typed_dict) => Some(typed_dict), _ => None, @@ -1725,7 +1728,7 @@ impl<'db> Type<'db> { /// 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(crate) fn to_class_type(self, db: &'db dyn Db) -> Option> { + fn to_class_type(self, db: &'db dyn Db) -> Option> { match self { Type::ClassLiteral(class_literal) => Some(class_literal.default_specialization(db)), Type::GenericAlias(alias) => Some(ClassType::Generic(alias)), @@ -1733,7 +1736,7 @@ impl<'db> Type<'db> { } } - pub const fn is_property_instance(&self) -> bool { + const fn is_property_instance(&self) -> bool { matches!(self, Type::PropertyInstance(..)) } @@ -1749,7 +1752,7 @@ impl<'db> Type<'db> { )) } - pub(crate) const fn is_union(self) -> bool { + const fn is_union(self) -> bool { matches!(self, Type::Union(_)) } @@ -1762,18 +1765,18 @@ impl<'db> Type<'db> { #[cfg(test)] #[track_caller] - pub(crate) const fn expect_union(self) -> UnionType<'db> { + const fn expect_union(self) -> UnionType<'db> { self.as_union().expect("Expected a Type::Union variant") } - pub(crate) const fn is_intersection(self) -> bool { + const fn is_intersection(self) -> bool { matches!(self, Type::Intersection(_)) } /// Returns whether this is a "real" intersection type. (Negated types are represented by an /// intersection containing a single negative branch, which this method does _not_ consider a /// "real" intersection.) - pub(crate) fn is_nontrivial_intersection(self, db: &'db dyn Db) -> bool { + fn is_nontrivial_intersection(self, db: &'db dyn Db) -> bool { match self { Type::Intersection(intersection) => !intersection.is_simple_negation(db), _ => false, @@ -1789,7 +1792,7 @@ impl<'db> Type<'db> { #[cfg(test)] #[track_caller] - pub(crate) fn expect_function_literal(self) -> FunctionType<'db> { + fn expect_function_literal(self) -> FunctionType<'db> { self.as_function_literal() .expect("Expected a Type::FunctionLiteral variant") } @@ -1798,21 +1801,21 @@ impl<'db> Type<'db> { matches!(self, Type::FunctionLiteral(..)) } - pub(crate) fn as_string_literal(self) -> Option> { + fn as_string_literal(self) -> Option> { match self { Type::LiteralValue(literal) => literal.as_string(), _ => None, } } - pub(crate) fn as_int_literal(self) -> Option { + fn as_int_literal(self) -> Option { match self { Type::LiteralValue(literal) => literal.as_int(), _ => None, } } - pub(crate) fn as_int_like_literal(self) -> Option { + fn as_int_like_literal(self) -> Option { match self.as_literal_value_kind() { Some(LiteralValueTypeKind::Int(value)) => Some(value.as_i64()), Some(LiteralValueTypeKind::Bool(value)) => Some(i64::from(value)), @@ -1829,20 +1832,20 @@ impl<'db> Type<'db> { #[cfg(test)] #[track_caller] - pub(crate) fn expect_enum_literal(self) -> EnumLiteralType<'db> { + fn expect_enum_literal(self) -> EnumLiteralType<'db> { match self.as_literal_value_kind() { Some(LiteralValueTypeKind::Enum(e)) => e, _ => panic!("Expected a `LiteralValueTypeKind::Enum` variant"), } } - pub(crate) fn is_string_literal(&self) -> bool { + fn is_string_literal(&self) -> bool { self.as_literal_value() .is_some_and(literal::LiteralValueType::is_string) } /// Detects types which are valid to appear inside a `Literal[…]` type annotation. - pub(crate) fn is_literal_or_union_of_literals(&self, db: &'db dyn Db) -> bool { + fn is_literal_or_union_of_literals(&self, db: &'db dyn Db) -> bool { match self { Type::Union(union) => union .elements(db) @@ -1873,7 +1876,7 @@ impl<'db> Type<'db> { } /// Create a promotable enum literal. - pub(crate) fn enum_literal(value: EnumLiteralType<'db>) -> Self { + fn enum_literal(value: EnumLiteralType<'db>) -> Self { Self::LiteralValue(LiteralValueType::promotable(value)) } @@ -1883,7 +1886,7 @@ impl<'db> Type<'db> { } /// Create a promotable single-character string literal. - pub(crate) fn single_char_string_literal(db: &'db dyn Db, c: char) -> Self { + fn single_char_string_literal(db: &'db dyn Db, c: char) -> Self { Self::LiteralValue(LiteralValueType::promotable(StringLiteralType::new( db, c.to_compact_string(), @@ -1891,7 +1894,7 @@ impl<'db> Type<'db> { } /// Create a promotable bytes literal. - pub(crate) fn bytes_literal(db: &'db dyn Db, bytes: &[u8]) -> Self { + fn bytes_literal(db: &'db dyn Db, bytes: &[u8]) -> Self { Self::LiteralValue(LiteralValueType::promotable(BytesLiteralType::new( db, bytes, ))) @@ -1903,19 +1906,19 @@ impl<'db> Type<'db> { } /// Create a `LiteralString`. - pub(crate) fn literal_string() -> Self { + fn literal_string() -> Self { // Note that `LiteralString`s are never implicitly inferred, and so are always unpromotable. Self::LiteralValue(LiteralValueType::unpromotable( LiteralValueTypeKind::LiteralString, )) } - pub(crate) fn typed_dict(defining_class: impl Into>) -> Self { + fn typed_dict(defining_class: impl Into>) -> Self { Self::TypedDict(TypedDictType::new(defining_class.into())) } #[must_use] - pub(crate) fn negate(&self, db: &'db dyn Db) -> Type<'db> { + fn negate(&self, db: &'db dyn Db) -> Type<'db> { // Avoid invoking the `IntersectionBuilder` for negations that are trivial. // // We verify that this always produces the same result as @@ -1971,13 +1974,13 @@ impl<'db> Type<'db> { } #[must_use] - pub(crate) fn negate_if(&self, db: &'db dyn Db, yes: bool) -> Type<'db> { + fn negate_if(&self, db: &'db dyn Db, yes: bool) -> Type<'db> { if yes { self.negate(db) } else { *self } } /// Return `true` if it is possible to spell an equivalent type to this one /// in user annotations without nonstandard extensions to the type system - pub(crate) fn is_spellable(&self, db: &'db dyn Db) -> bool { + fn is_spellable(&self, db: &'db dyn Db) -> bool { match self { Type::LiteralValue(_) | Type::Never @@ -2093,11 +2096,7 @@ impl<'db> Type<'db> { /// based on the provided predicate. /// /// Otherwise, returns the type unchanged. - pub(crate) fn filter_union( - self, - db: &'db dyn Db, - f: impl FnMut(&Type<'db>) -> bool, - ) -> Type<'db> { + fn filter_union(self, db: &'db dyn Db, f: impl FnMut(&Type<'db>) -> bool) -> Type<'db> { if let Type::Union(union) = self.resolve_type_alias(db) { union.filter(db, f) } else { @@ -2108,7 +2107,7 @@ impl<'db> Type<'db> { /// If the type is a union, removes union elements that are disjoint from `target`. /// /// Otherwise, returns the type unchanged. - pub(crate) fn filter_disjoint_elements( + fn filter_disjoint_elements( self, db: &'db dyn Db, target: Type<'db>, @@ -2124,7 +2123,7 @@ impl<'db> Type<'db> { /// Returns the fallback instance type that a literal is an instance of, or `None` if the type /// is not a literal. - pub(crate) fn literal_fallback_instance(self, db: &'db dyn Db) -> Option> { + fn literal_fallback_instance(self, db: &'db dyn Db) -> Option> { // There are other literal types that could conceivable be included here: class literals // falling back to `type[X]`, for instance. For now, there is not much rigorous thought put // into what's included vs not; this is just an empirical choice that makes our ecosystem @@ -2160,7 +2159,7 @@ impl<'db> Type<'db> { /// This is intentionally separate from regular promotion. Applying it during collection /// inference would lose useful precision for local and module-level collections of class /// objects. - pub(crate) fn promote_class_literals(self, db: &'db dyn Db) -> Type<'db> { + fn promote_class_literals(self, db: &'db dyn Db) -> Type<'db> { self.apply_type_mapping( db, &TypeMapping::Promote(PromotionMode::On, PromotionKind::ClassLiteralsOnly), @@ -2172,7 +2171,7 @@ impl<'db> Type<'db> { /// `T | Unknown` within nominal type parameters, without recursing into unions. /// Used for collection literal inference so that `[None]` is inferred as /// `list[None | Unknown]` rather than `list[None]`. - pub(crate) fn promote_singletons_recursively(self, db: &'db dyn Db) -> Type<'db> { + fn promote_singletons_recursively(self, db: &'db dyn Db) -> Type<'db> { self.apply_type_mapping( db, &TypeMapping::Promote(PromotionMode::On, PromotionKind::SingletonsOnly), @@ -2327,7 +2326,7 @@ impl<'db> Type<'db> { /// /// The provided closure will be called on any nested types, along with their variance with /// respect to the outermost type. - pub(crate) fn visit_specialization(self, db: &'db dyn Db, mut f: F) + fn visit_specialization(self, db: &'db dyn Db, mut f: F) where F: FnMut(Type<'db>, TypeVarVariance), { @@ -2408,7 +2407,7 @@ impl<'db> Type<'db> { /// /// Note: This function aims to have no false positives, but might return `false` /// for more complicated types that are actually singletons. - pub(crate) fn is_singleton(self, db: &'db dyn Db) -> bool { + fn is_singleton(self, db: &'db dyn Db) -> bool { match self { Type::Dynamic(_) | Type::Divergent(_) | Type::Never => false, @@ -3496,7 +3495,7 @@ impl<'db> Type<'db> { /// Returns whether this type is a data descriptor, i.e. defines `__set__` or `__delete__`. /// If this type is a union, requires all elements of union to be data descriptors. /// A directly dynamic type is treated as a data descriptor because it could inhabit one. - pub(crate) fn is_data_descriptor(self, d: &'db dyn Db) -> bool { + fn is_data_descriptor(self, d: &'db dyn Db) -> bool { self.is_data_descriptor_impl(d, false) } @@ -3505,7 +3504,7 @@ impl<'db> Type<'db> { /// This is used to determine whether an attribute assignment is valid for narrowing. /// For practical convenience, dynamic union elements are not considered possible data /// descriptors here, because doing so would disable narrowing too frequently. - pub(crate) fn may_be_data_descriptor(self, d: &'db dyn Db) -> bool { + fn may_be_data_descriptor(self, d: &'db dyn Db) -> bool { self.is_data_descriptor_impl(d, true) } @@ -3513,7 +3512,7 @@ impl<'db> Type<'db> { /// /// Descriptor uncertainty only propagates through outer unions, intersections, and aliases; /// type arguments do not affect the runtime descriptor class. - pub(crate) fn is_definitely_non_data_descriptor(self, db: &'db dyn Db) -> bool { + fn is_definitely_non_data_descriptor(self, db: &'db dyn Db) -> bool { self.is_definitely_non_data_descriptor_impl(db, ()) } @@ -3729,7 +3728,7 @@ impl<'db> Type<'db> { /// TODO: We should return a `Result` here to handle errors that can appear during attribute /// lookup, like a failed `__get__` call on a descriptor. #[must_use] - pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + fn member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { self.member_lookup_with_policy(db, name, MemberLookupPolicy::default()) } @@ -5960,13 +5959,13 @@ impl<'db> Type<'db> { /// Use this only when an over-approximation is sound, such as constructor inference or a /// source-side relation. Target-side subtype checks must use [`Self::to_instance`]. #[must_use] - pub(crate) fn to_instance_approximation(self, db: &'db dyn Db) -> Option> { + fn to_instance_approximation(self, db: &'db dyn Db) -> Option> { self.to_instance(db).map(InstanceProjection::into_inner) } /// Project this class-object type into its instance type while preserving projection quality. #[must_use] - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option>> { + fn to_instance(self, db: &'db dyn Db) -> Option>> { match self { Type::Dynamic(_) | Type::Divergent(_) | Type::Never => { Some(InstanceProjection::Exact(self)) @@ -6038,7 +6037,7 @@ impl<'db> Type<'db> { /// /// The `scope_id` and `typevar_binding_context` arguments must always come from the file we are currently inferring, so /// as to avoid cross-module AST dependency. - pub(crate) fn in_type_expression( + fn in_type_expression( &self, db: &'db dyn Db, scope_id: ScopeId<'db>, @@ -6301,7 +6300,7 @@ impl<'db> Type<'db> { /// Note: the return type of `type(obj)` is subtly different from this. /// See `Self::dunder_class` for more details. #[must_use] - pub(crate) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { match self { Type::Never => Type::Never, Type::NominalInstance(instance) => instance.to_meta_type(db), @@ -6378,7 +6377,7 @@ impl<'db> Type<'db> { /// `type[dict[str, object]]`, because their inhabitants are instances of `dict` at runtime. /// Class-backed protocols return their structural `type[Protocol]` view. #[must_use] - pub(crate) fn dunder_class(self, db: &'db dyn Db) -> Type<'db> { + fn dunder_class(self, db: &'db dyn Db) -> Type<'db> { match self { Type::Union(union) => union.map(db, |element| element.dunder_class(db)), Type::Intersection(intersection) => intersection @@ -6395,7 +6394,7 @@ impl<'db> Type<'db> { } #[must_use] - pub(crate) fn apply_optional_specialization( + fn apply_optional_specialization( self, db: &'db dyn Db, specialization: Option>, @@ -6413,7 +6412,7 @@ impl<'db> Type<'db> { /// Note that this does not specialize generic classes, functions, or type aliases! That is a /// different operation that is performed explicitly (via a subscript operation), or implicitly /// via a call to the generic object. - pub(crate) fn apply_specialization( + fn apply_specialization( self, db: &'db dyn Db, specialization: Specialization<'db>, @@ -6841,7 +6840,7 @@ 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( + fn find_legacy_typevars( self, db: &'db dyn Db, binding_context: Option>, @@ -6855,7 +6854,7 @@ impl<'db> Type<'db> { ); } - pub(crate) fn find_legacy_typevars_impl( + fn find_legacy_typevars_impl( self, db: &'db dyn Db, binding_context: Option>, @@ -7109,7 +7108,7 @@ impl<'db> Type<'db> { /// Bind all unbound legacy type variables to the given context and then /// add all legacy typevars to the provided set. - pub(crate) fn bind_and_find_all_legacy_typevars( + fn bind_and_find_all_legacy_typevars( self, db: &'db dyn Db, binding_context: Option>, @@ -7128,7 +7127,7 @@ impl<'db> Type<'db> { } /// Replace default types in parameters of callables with `Unknown`. - pub(crate) fn replace_parameter_defaults(self, db: &'db dyn Db) -> Type<'db> { + fn replace_parameter_defaults(self, db: &'db dyn Db) -> Type<'db> { self.apply_type_mapping( db, &TypeMapping::ReplaceParameterDefaults, @@ -7161,7 +7160,7 @@ impl<'db> Type<'db> { /// When not available, this should fall back to the value of `[Type::repr]`. /// Note: this method is used in the builtins `format`, `print`, `str.format` and `f-strings`. #[must_use] - pub(crate) fn str(&self, db: &'db dyn Db) -> Type<'db> { + fn str(&self, db: &'db dyn Db) -> Type<'db> { match self { Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Int(_) | LiteralValueTypeKind::Bool(_) => self.repr(db), @@ -7199,7 +7198,7 @@ impl<'db> Type<'db> { /// Return the string representation of this type as it would be provided by the `__repr__` /// method at runtime. #[must_use] - pub(crate) fn repr(&self, db: &'db dyn Db) -> Type<'db> { + fn repr(&self, db: &'db dyn Db) -> Type<'db> { match self { Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Int(number) => { @@ -7411,7 +7410,7 @@ impl<'db> Type<'db> { } } - pub(crate) fn generic_origin(self, db: &'db dyn Db) -> Option> { + fn generic_origin(self, db: &'db dyn Db) -> Option> { match self { Type::GenericAlias(generic) => Some(generic.origin(db)), Type::NominalInstance(instance) @@ -7426,14 +7425,14 @@ impl<'db> Type<'db> { /// Default-specialize all legacy typevars in this type. /// /// This is used when an implicit type alias is referenced without explicitly specializing it. - pub(crate) fn default_specialize(self, db: &'db dyn Db) -> Type<'db> { + fn default_specialize(self, db: &'db dyn Db) -> Type<'db> { let mut variables = FxOrderSet::default(); self.find_legacy_typevars(db, None, &mut variables); let generic_context = GenericContext::from_typevar_instances(db, variables); self.apply_specialization(db, generic_context.default_specialization(db, None)) } - pub(crate) fn from_truthiness(db: &'db dyn Db, truthiness: Truthiness) -> Self { + fn from_truthiness(db: &'db dyn Db, truthiness: Truthiness) -> Self { match truthiness { Truthiness::AlwaysTrue => Type::bool_literal(true), Truthiness::AlwaysFalse => Type::bool_literal(false), @@ -7443,7 +7442,7 @@ impl<'db> Type<'db> { /// Return whether the negation of this type is a subtype of `target`, reusing `negated_cache` /// for type shapes whose negation must still be materialized. - pub(crate) fn negation_is_subtype_of_cached( + fn negation_is_subtype_of_cached( self, db: &'db dyn Db, target: Type<'db>, @@ -7465,7 +7464,7 @@ impl<'db> IntersectionType<'db> { /// Applying De Morgan's law to an intersection produces a union. Checking each branch /// directly avoids constructing and simplifying that temporary union, which can be costly /// for the large intersections produced by repeated narrowing. - pub(crate) fn negation_is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { + fn negation_is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { self.positive(db) .iter() .all(|positive| positive.negate(db).is_subtype_of(db, target)) @@ -7780,17 +7779,17 @@ pub struct SelfBinding<'db> { } impl<'db> SelfBinding<'db> { - pub(crate) fn self_type(&self) -> Type<'db> { + fn self_type(&self) -> Type<'db> { self.ty } - pub(crate) fn binding_context(&self) -> Option> { + fn binding_context(&self) -> Option> { self.binding_context } } impl<'db> SelfBinding<'db> { - pub(crate) fn new( + fn new( db: &'db dyn Db, self_type: Type<'db>, binding_context: Option>, @@ -7880,7 +7879,7 @@ pub enum TypeMapping<'a, 'db> { impl<'db> TypeMapping<'_, 'db> { /// Update the generic context of a [`Signature`] according to the current type mapping - pub(crate) fn update_signature_generic_context( + fn update_signature_generic_context( &self, db: &'db dyn Db, context: GenericContext<'db>, @@ -7946,7 +7945,7 @@ impl<'db> TypeMapping<'_, 'db> { } /// Returns a new `TypeMapping` that should be applied in contravariant positions. - pub(crate) fn flip(&self) -> Self { + fn flip(&self) -> Self { match self { TypeMapping::Materialize(materialization_kind) => { TypeMapping::Materialize(materialization_kind.flip()) @@ -8057,7 +8056,7 @@ impl DynamicType<'_> { self } - pub(crate) fn is_todo(&self) -> bool { + fn is_todo(&self) -> bool { matches!(self, Self::Todo(_)) } } @@ -8164,7 +8163,7 @@ impl<'db> TypeAndQualifiers<'db> { } } - pub(crate) fn declared(inner: Type<'db>) -> Self { + fn declared(inner: Type<'db>) -> Self { Self { inner, origin: TypeOrigin::Declared, @@ -8192,7 +8191,7 @@ impl<'db> TypeAndQualifiers<'db> { } /// Return `self` with an additional qualifier added to the set of qualifiers. - pub(crate) fn with_qualifier(mut self, qualifier: TypeQualifiers) -> Self { + fn with_qualifier(mut self, qualifier: TypeQualifiers) -> Self { self.qualifiers |= qualifier; self } @@ -8202,10 +8201,7 @@ impl<'db> TypeAndQualifiers<'db> { self.qualifiers } - pub(crate) fn map_type( - &self, - f: impl FnOnce(Type<'db>) -> Type<'db>, - ) -> TypeAndQualifiers<'db> { + fn map_type(&self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> TypeAndQualifiers<'db> { TypeAndQualifiers { inner: f(self.inner), origin: self.origin, @@ -8804,11 +8800,11 @@ pub(super) struct MetaclassCandidate<'db> { /// Information about a `@dataclass_transform`-decorated metaclass. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, get_size2::GetSize, salsa::SalsaValue)] pub(super) struct MetaclassTransformInfo<'db> { - pub(super) params: DataclassTransformerParams<'db>, + params: DataclassTransformerParams<'db>, /// Whether the metaclass providing these parameters was declared on the class itself /// (via an explicit `metaclass=` keyword) rather than inherited from a base class. - pub(super) from_explicit_metaclass: bool, + from_explicit_metaclass: bool, } #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] @@ -8833,7 +8829,7 @@ fn walk_typeis_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( impl get_size2::GetSize for TypeIsType<'_> {} impl<'db> TypeIsType<'db> { - pub(crate) fn place_name(self, db: &'db dyn Db) -> Option { + fn place_name(self, db: &'db dyn Db) -> Option { let (scope, place) = self.place_info(db)?; let table = place_table(db, scope); @@ -8848,30 +8844,25 @@ impl<'db> TypeIsType<'db> { /// def is_tuple(value: object) -> TypeIs[tuple[int, ...]]: /// return isinstance(value, tuple) /// ``` - pub(crate) fn from_type_expression(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn from_type_expression(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { Type::TypeIs(Self::new(db, ty, None)) } - pub(crate) fn return_type(self, db: &'db dyn Db) -> Type<'db> { + fn return_type(self, db: &'db dyn Db) -> Type<'db> { self.type_argument(db) } #[must_use] - pub(crate) fn bind( - self, - db: &'db dyn Db, - scope: ScopeId<'db>, - place: ScopedPlaceId, - ) -> Type<'db> { + fn bind(self, db: &'db dyn Db, scope: ScopeId<'db>, place: ScopedPlaceId) -> Type<'db> { Type::TypeIs(Self::new(db, self.type_argument(db), Some((scope, place)))) } #[must_use] - pub(crate) fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { Type::TypeIs(Self::new(db, ty, self.place_info(db))) } - pub(crate) fn is_bound(self, db: &'db dyn Db) -> bool { + fn is_bound(self, db: &'db dyn Db) -> bool { self.place_info(db).is_some() } } @@ -8908,18 +8899,18 @@ fn walk_typeguard_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( impl get_size2::GetSize for TypeGuardType<'_> {} impl<'db> TypeGuardType<'db> { - pub(crate) fn place_name(self, db: &'db dyn Db) -> Option { + fn place_name(self, db: &'db dyn Db) -> Option { let (scope, place) = self.place_info(db)?; let table = place_table(db, scope); Some(format!("{}", table.place(place))) } - pub(crate) fn unbound(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn unbound(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { Type::TypeGuard(Self::new(db, ty, None)) } - pub(crate) fn bound( + fn bound( db: &'db dyn Db, return_type: Type<'db>, scope: ScopeId<'db>, @@ -8929,21 +8920,16 @@ impl<'db> TypeGuardType<'db> { } #[must_use] - pub(crate) fn bind( - self, - db: &'db dyn Db, - scope: ScopeId<'db>, - place: ScopedPlaceId, - ) -> Type<'db> { + fn bind(self, db: &'db dyn Db, scope: ScopeId<'db>, place: ScopedPlaceId) -> Type<'db> { Self::bound(db, self.return_type(db), scope, place) } #[must_use] - pub(crate) fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn with_type(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { Type::TypeGuard(Self::new(db, ty, self.place_info(db))) } - pub(crate) fn is_bound(self, db: &'db dyn Db) -> bool { + fn is_bound(self, db: &'db dyn Db) -> bool { self.place_info(db).is_some() } } diff --git a/crates/ty_python_semantic/src/types/bool.rs b/crates/ty_python_semantic/src/types/bool.rs index d7505c8c12..aa0efd8ce0 100644 --- a/crates/ty_python_semantic/src/types/bool.rs +++ b/crates/ty_python_semantic/src/types/bool.rs @@ -336,9 +336,9 @@ impl<'db> Type<'db> { } /// A [`CycleDetector`] that is used in `try_bool` methods. -pub(crate) type TryBoolVisitor<'db> = +type TryBoolVisitor<'db> = CycleDetector<'db, TryBool, Type<'db>, Result>, 3>; -pub(crate) struct TryBool; +struct TryBool; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum BoolError<'db> { diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 6bfbccdec8..ab50eb6b46 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -56,7 +56,7 @@ pub(crate) struct CallArgumentTypes<'db> { } impl<'db> CallArgumentTypes<'db> { - pub(crate) fn new(fallback_ty: Option>) -> Self { + fn new(fallback_ty: Option>) -> Self { Self { fallback_type: fallback_ty, types: FxHashMap::default(), @@ -89,7 +89,7 @@ impl<'db> CallArgumentTypes<'db> { } /// Insert the type of this argument when inferred with the provided type context. - pub(crate) fn insert(&mut self, tcx: impl Into>, ty: Type<'db>) { + fn insert(&mut self, tcx: impl Into>, ty: Type<'db>) { match tcx.into().annotation { None => self.fallback_type = Some(ty), Some(tcx) => { @@ -98,7 +98,7 @@ impl<'db> CallArgumentTypes<'db> { } } - pub(crate) fn iter(&self) -> impl Iterator, Type<'db>)> { + fn iter(&self) -> impl Iterator, Type<'db>)> { self.types .iter() .map(|(tcx, ty)| (TypeContext::new(Some(*tcx)), *ty)) @@ -261,7 +261,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { } /// Create a new [`CallArguments`] starting from the specified index. - pub(crate) fn start_from(&self, index: usize) -> Self { + fn start_from(&self, index: usize) -> Self { Self { items: self.items[index..].to_vec(), } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index c34a7f018b..0ee60a7e0c 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -579,7 +579,7 @@ pub(crate) enum CheckTypesMode { } impl CheckTypesMode { - pub(crate) fn is_provisional(self) -> bool { + fn is_provisional(self) -> bool { matches!(self, Self::Provisional) } } @@ -1003,7 +1003,7 @@ impl<'db> Bindings<'db> { /// /// This handles the shared partial-specific preprocessing (callable validation and argument /// normalization) used by both inference and known-call evaluation. - pub(crate) fn functools_partial_matched_bindings<'a>( + fn functools_partial_matched_bindings<'a>( db: &'db dyn Db, wrapped_callable_ty: Type<'db>, call_arguments: &CallArguments<'a, 'db>, @@ -3066,7 +3066,7 @@ pub(crate) struct CallableBinding<'db> { /// If this is a callable object (i.e. called via a `__call__` method), the boundness of /// that call method. - pub(crate) dunder_call_is_possibly_unbound: bool, + dunder_call_is_possibly_unbound: bool, /// The type of the bound `self` or `cls` parameter if this signature is for a bound method. pub(crate) bound_type: Option>, @@ -3144,7 +3144,7 @@ impl<'db> CallableBinding<'db> { /// /// The preserved indexes are used for diagnostics so filtered or reordered bindings can still /// point back to the correct overload declaration. - pub(crate) fn from_indexed_overloads( + fn from_indexed_overloads( signature_type: Type<'db>, overloads: impl IntoIterator)>, ) -> Self { @@ -3389,7 +3389,7 @@ impl<'db> CallableBinding<'db> { self } - pub(super) fn argument_matches_keyword_variadic(&self, argument_index: usize) -> bool { + fn argument_matches_keyword_variadic(&self, argument_index: usize) -> bool { let argument_index = argument_index + usize::from(self.bound_type.is_some()); self.matching_overloads().any(|(_, overload)| { overload @@ -4056,7 +4056,7 @@ impl<'db> CallableBinding<'db> { Ok(()) } - pub(crate) fn is_callable(&self) -> bool { + fn is_callable(&self) -> bool { !self.overloads.is_empty() } @@ -4109,7 +4109,7 @@ impl<'db> CallableBinding<'db> { } /// Returns the index of the matching overload in the form of [`MatchingOverloadIndex`]. - pub(crate) fn matching_overload_index(&self) -> MatchingOverloadIndex { + fn matching_overload_index(&self) -> MatchingOverloadIndex { let mut matching_overloads = self.matching_overloads(); match matching_overloads.next() { None => MatchingOverloadIndex::None, @@ -4169,7 +4169,7 @@ impl<'db> CallableBinding<'db> { /// /// 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> { + fn return_type(&self) -> Type<'db> { if let Some(overload_call_return_type) = self.overload_call_return_type { return match overload_call_return_type { OverloadCallReturnType::ArgumentTypeExpansion(return_type) => return_type, @@ -6446,12 +6446,12 @@ pub(crate) struct Binding<'db> { source_parameter_index_offset: usize, /// The type that is (hopefully) callable. - pub(crate) callable_type: Type<'db>, + callable_type: Type<'db>, /// The type we'll use for error messages referring to details of the called signature. For /// calls to functions this will be the same as `callable_type`; for other callable instances /// it may be a `__call__` method. - pub(crate) signature_type: Type<'db>, + signature_type: Type<'db>, /// Return type of the call. pub(crate) return_ty: Type<'db>, @@ -7012,7 +7012,7 @@ impl<'db> Binding<'db> { self.return_ty = return_ty; } - pub(crate) fn return_type(&self) -> Type<'db> { + fn return_type(&self) -> Type<'db> { self.return_ty } @@ -7157,7 +7157,7 @@ impl<'db> Binding<'db> { /// that parameter. /// /// Returns an error if the parameter name is not found. - pub(crate) fn parameter_type_by_name( + fn parameter_type_by_name( &self, parameter_name: &str, fallback_to_default: bool, @@ -7399,8 +7399,8 @@ impl CallableBindingSnapshotter { /// Describes a callable for the purposes of diagnostics. #[derive(Debug)] pub(crate) struct CallableDescription<'a> { - pub(crate) name: Cow<'a, str>, - pub(crate) kind: Option<&'static str>, + name: Cow<'a, str>, + kind: Option<&'static str>, } impl<'db> CallableDescription<'db> { @@ -7753,7 +7753,7 @@ impl BindingError<'_> { ) } - pub(crate) fn maybe_apply_argument_index_offset(mut self, offset: Option) -> Self { + fn maybe_apply_argument_index_offset(mut self, offset: Option) -> Self { if let Some(offset) = offset { self.apply_argument_index_offset(offset); } @@ -7826,7 +7826,7 @@ impl BindingError<'_> { /// sub-call for a `ParamSpec`, where the argument indices are relative to the sub-call's /// argument list rather than the original call's argument list. The `offset` should be the /// number of arguments in the original call that were matched before the `ParamSpec` component. - pub(crate) fn apply_argument_index_offset(&mut self, offset: usize) { + fn apply_argument_index_offset(&mut self, offset: usize) { self.map_argument_indices(|argument_index| argument_index.map(|index| index + offset)); } } diff --git a/crates/ty_python_semantic/src/types/callable.rs b/crates/ty_python_semantic/src/types/callable.rs index 91f92dcf19..cba3d822e0 100644 --- a/crates/ty_python_semantic/src/types/callable.rs +++ b/crates/ty_python_semantic/src/types/callable.rs @@ -476,10 +476,7 @@ impl<'db> CallableType<'db> { ) } - pub(crate) fn paramspec_value( - db: &'db dyn Db, - parameters: Parameters<'db>, - ) -> CallableType<'db> { + fn paramspec_value(db: &'db dyn Db, parameters: Parameters<'db>) -> CallableType<'db> { CallableType::new( db, CallableSignature::single(Signature::new(parameters, Type::unknown())), @@ -497,7 +494,7 @@ impl<'db> CallableType<'db> { matches!(self.kind(db), CallableTypeKind::FunctionLike) } - pub(crate) fn is_dunder_paramspec(self, db: &'db dyn Db) -> bool { + fn is_dunder_paramspec(self, db: &'db dyn Db) -> bool { matches!(self.kind(db), CallableTypeKind::DunderParamSpec) } @@ -687,7 +684,7 @@ impl<'db> CallableType<'db> { pub(crate) struct CallableTypes<'db>(SmallVec<[CallableType<'db>; 1]>); impl<'db> CallableTypes<'db> { - pub(super) fn new(callables: SmallVec<[CallableType<'db>; 1]>) -> Self { + fn new(callables: SmallVec<[CallableType<'db>; 1]>) -> Self { assert!(!callables.is_empty(), "CallableTypes should not be empty"); CallableTypes(callables) } @@ -713,7 +710,7 @@ impl<'db> CallableTypes<'db> { &self.0 } - pub(super) fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> { + fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> { self.0 } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 0bb6c040a3..8a664bb8b9 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -278,7 +278,7 @@ impl<'db> CodeGeneratorKind<'db> { ) } - pub(super) fn dataclass_transformer_params(self) -> Option> { + fn dataclass_transformer_params(self) -> Option> { match self { Self::DataclassLike(params) => params, Self::Pydantic(_) | Self::NamedTuple | Self::TypedDict => None, @@ -324,7 +324,7 @@ impl<'db> CodeGeneratorKind<'db> { /// def f(c: C): /// c.value # okay, `value` will be set by `C`'s constructor /// ``` - pub(super) const fn treats_fields_as_instance_attributes(self) -> bool { + const fn treats_fields_as_instance_attributes(self) -> bool { matches!(self, Self::DataclassLike(_) | Self::Pydantic(_)) } @@ -339,7 +339,7 @@ impl<'db> CodeGeneratorKind<'db> { /// /// C(value=42) /// ``` - pub(super) fn synthesizes_constructor_signature_from_fields( + fn synthesizes_constructor_signature_from_fields( self, db: &'db dyn Db, class: StaticClassLiteral<'db>, @@ -571,7 +571,7 @@ impl<'db> ClassLiteral<'db> { } /// Returns whether this class has PEP 695 type parameters. - pub(crate) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { + fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool { self.as_static() .is_some_and(|class| class.has_pep_695_type_params(db)) } @@ -582,7 +582,7 @@ impl<'db> ClassLiteral<'db> { } /// Return the properties that affect how instances of this class are represented. - pub(super) fn instance_flags(self, db: &'db dyn Db) -> ClassInstanceFlags { + fn instance_flags(self, db: &'db dyn Db) -> ClassInstanceFlags { match self { Self::Static(literal) => literal.instance_flags(db), Self::DynamicTypedDict(_) => ClassInstanceFlags::TYPED_DICT, @@ -790,7 +790,7 @@ impl<'db> ClassLiteral<'db> { /// ```python /// X = type("X", (), {"__lt__": lambda self, other: True}) /// ``` - pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { + fn has_own_ordering_method(self, db: &'db dyn Db) -> bool { match self { Self::Static(class) => class.has_own_ordering_method(db), Self::Dynamic(class) => class.has_own_ordering_method(db), @@ -876,7 +876,7 @@ impl<'db> ClassLiteral<'db> { /// class Foo(int, X): ... /// TypeError: multiple bases have instance lay-out conflict /// ``` - pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { match self { Self::Static(class) => class.as_disjoint_base(db), Self::Dynamic(class) => class.as_disjoint_base(db), @@ -1179,7 +1179,7 @@ impl<'db> ClassType<'db> { } /// Return `Some` if this class is known to be a [`DisjointBase`], or `None` if it is not. - pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { + fn as_disjoint_base(self, db: &'db dyn Db) -> Option> { self.class_literal(db).as_disjoint_base(db) } @@ -1444,7 +1444,7 @@ impl<'db> ClassType<'db> { } /// Return `true` if this class could exist in the MRO of `other`. - pub(super) fn could_exist_in_mro_of( + fn could_exist_in_mro_of( self, db: &'db dyn Db, other: Self, @@ -2597,7 +2597,7 @@ pub(super) struct MroLookup<'db, I> { impl<'db, I: Iterator>> MroLookup<'db, I> { /// Create a new MRO lookup from a database and an MRO iterator. - pub(super) fn new(db: &'db dyn Db, mro_iter: I) -> Self { + fn new(db: &'db dyn Db, mro_iter: I) -> Self { Self { db, mro_iter } } @@ -2615,7 +2615,7 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { /// If we encounter a dynamic type in the MRO, we save it and after traversal: /// 1. Use it as the type if no other classes define the attribute, or /// 2. Intersect it with the type from non-dynamic MRO members. - pub(super) fn class_member( + fn class_member( self, name: &str, policy: MemberLookupPolicy, @@ -2696,7 +2696,7 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { /// /// Returns `InstanceMemberResult::TypedDict` if a `TypedDict` base is encountered, /// allowing the caller to handle this case specially. - pub(super) fn instance_member(self, name: &str) -> InstanceMemberResult<'db> { + fn instance_member(self, name: &str) -> InstanceMemberResult<'db> { let db = self.db; let mut union = UnionBuilder::new(db); let mut union_qualifiers = TypeQualifiers::empty(); @@ -2792,7 +2792,7 @@ pub(super) struct CompletedMemberLookup<'db> { impl<'db> CompletedMemberLookup<'db> { /// Finalize the lookup result by handling dynamic type intersection. - pub(super) fn finalize(self, db: &'db dyn Db) -> PlaceAndQualifiers<'db> { + fn finalize(self, db: &'db dyn Db) -> PlaceAndQualifiers<'db> { match ( PlaceAndQualifiers::from(self.lookup_result), self.dynamic_type, @@ -2840,7 +2840,7 @@ pub(super) struct QualifiedClassName<'db> { } impl<'db> QualifiedClassName<'db> { - pub(super) fn from_class_literal(db: &'db dyn Db, class: ClassLiteral<'db>) -> Self { + fn from_class_literal(db: &'db dyn Db, class: ClassLiteral<'db>) -> Self { Self { db, class } } diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs index 90ff4f51d3..d058af0c7a 100644 --- a/crates/ty_python_semantic/src/types/class/known.rs +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -1285,7 +1285,7 @@ impl KnownClass { } /// Return the module in which we should look up the definition for this class - pub(super) fn canonical_module(self, db: &dyn Db) -> KnownModule { + fn canonical_module(self, db: &dyn Db) -> KnownModule { match self { Self::Bool | Self::Object diff --git a/crates/ty_python_semantic/src/types/class/named_tuple.rs b/crates/ty_python_semantic/src/types/class/named_tuple.rs index f0d18d11e9..2a52ed5b1d 100644 --- a/crates/ty_python_semantic/src/types/class/named_tuple.rs +++ b/crates/ty_python_semantic/src/types/class/named_tuple.rs @@ -201,7 +201,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } /// Returns an instance type for this dynamic namedtuple. - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { + fn to_instance(self, db: &'db dyn Db) -> Type<'db> { Type::instance(db, ClassType::NonGeneric(self.into())) } diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index cdec37009e..3a1d32051d 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -224,7 +224,7 @@ impl<'db> StaticClassLiteral<'db> { /// /// When the base namedtuple's fields were determined dynamically (e.g., from a variable), /// we can't synthesize precise method signatures and should fall back to `NamedTupleFallback`. - pub(crate) fn namedtuple_base_has_unknown_fields(self, db: &'db dyn Db) -> bool { + fn namedtuple_base_has_unknown_fields(self, db: &'db dyn Db) -> bool { self.explicit_bases(db).iter().any(|base| match base { Type::ClassLiteral(ClassLiteral::DynamicNamedTuple(namedtuple)) => { !namedtuple.has_known_fields(db) @@ -312,7 +312,7 @@ impl<'db> StaticClassLiteral<'db> { /// /// Note: We use direct scope lookups here to avoid infinite recursion /// through `own_class_member` -> `own_synthesized_member`. - pub(super) fn total_ordering_root_method( + fn total_ordering_root_method( self, db: &'db dyn Db, specialization: Option>, diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index f088f9582a..40acd2679e 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -885,11 +885,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { } #[expect(dead_code)] // Keep this around for debugging purposes - pub(crate) fn display_graph<'a>( - self, - db: &'db dyn Db, - prefix: &'a dyn Display, - ) -> impl Display { + fn display_graph<'a>(self, db: &'db dyn Db, prefix: &'a dyn Display) -> impl Display { struct DisplayConstraintSet<'a, 'c, 'db> { node: NodeId, prefix: &'a dyn Display, @@ -1790,8 +1786,8 @@ enum SourceOrder { /// lower and upper bound. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)] pub(crate) struct Constraint<'db> { - pub(crate) typevar: BoundTypeVarInstance<'db>, - pub(crate) bounds: ConstraintBounds<'db>, + typevar: BoundTypeVarInstance<'db>, + bounds: ConstraintBounds<'db>, } /// The explicit lower and upper bounds inferred for a typevar on one constraint path. @@ -1822,11 +1818,11 @@ impl<'db> ConstraintBounds<'db> { self.upper.is_some() } - pub(crate) fn materialized_lower(self) -> Type<'db> { + fn materialized_lower(self) -> Type<'db> { self.lower.unwrap_or(Type::Never) } - pub(crate) fn materialized_upper(self) -> Type<'db> { + fn materialized_upper(self) -> Type<'db> { self.upper.unwrap_or(Type::object()) } } @@ -1850,7 +1846,7 @@ pub(crate) struct UpperBound<'db> { } impl<'db> UpperBound<'db> { - pub(crate) fn none() -> Self { + fn none() -> Self { Self::default() } @@ -1858,16 +1854,16 @@ impl<'db> UpperBound<'db> { /// /// This preserves an explicit `object` clause so callers can distinguish `T <= object` from a /// missing upper bound. Use [`UpperBound::add_clause`] when accumulating multiple clauses. - pub(crate) fn from_clause(clause: Type<'db>) -> Self { + fn from_clause(clause: Type<'db>) -> Self { let clauses = FxOrderSet::from_iter([clause]); Self { clauses } } - pub(crate) fn is_empty(&self) -> bool { + fn is_empty(&self) -> bool { self.clauses.is_empty() } - pub(crate) fn has_explicit_bound(&self) -> bool { + fn has_explicit_bound(&self) -> bool { !self.is_empty() } @@ -1898,7 +1894,7 @@ impl<'db> UpperBound<'db> { self.clauses.len() == 1 && self.clauses.contains(&Type::Never) } - pub(crate) fn add_clause(&mut self, clause: Type<'db>) { + fn add_clause(&mut self, clause: Type<'db>) { if self.is_never() { return; } @@ -1912,7 +1908,7 @@ impl<'db> UpperBound<'db> { self.clauses.insert(clause); } - pub(crate) fn shrink_to_fit(&mut self) { + fn shrink_to_fit(&mut self) { self.clauses.shrink_to_fit(); } @@ -1927,7 +1923,7 @@ impl<'db> UpperBound<'db> { self.clauses.iter().copied().any(Type::is_union) } - pub(crate) fn is_satisfied_by(&self, db: &'db dyn Db, ty: Type<'db>) -> bool { + fn is_satisfied_by(&self, db: &'db dyn Db, ty: Type<'db>) -> bool { self.clauses .iter() .all(|clause| ty.is_constraint_set_assignable_to(db, *clause)) @@ -3706,7 +3702,7 @@ impl<'db> PathBound<'db> { } } - pub(crate) fn variance(&self) -> TypeVarVariance { + fn variance(&self) -> TypeVarVariance { match (self.lower, self.has_upper()) { (None, true) => TypeVarVariance::Covariant, (Some(_), false) => TypeVarVariance::Contravariant, @@ -3715,7 +3711,7 @@ impl<'db> PathBound<'db> { } } - pub(crate) fn lower_or_never(&self) -> Type<'db> { + fn lower_or_never(&self) -> Type<'db> { self.lower.unwrap_or(Type::Never) } @@ -7117,9 +7113,7 @@ impl PathAssignments { result } - pub(crate) fn positive_constraints( - &self, - ) -> impl Iterator + '_ { + fn positive_constraints(&self) -> impl Iterator + '_ { self.assignments.iter().filter_map( |(assignment, (source_constraint, _))| match assignment { ConstraintAssignment::Positive(constraint) => { diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs index 3b99b53e8b..60b8d4dd0e 100644 --- a/crates/ty_python_semantic/src/types/cyclic.rs +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -78,7 +78,7 @@ impl<'db> Type<'db> { #[allow(clippy::inline_always)] #[inline(always)] - pub(crate) fn recursive_identity(self, db: &'db dyn Db) -> Option> { + fn recursive_identity(self, db: &'db dyn Db) -> Option> { match self { // We can create a self-referential function type: e.g. `def f(x: "TypeOf[f]"): reveal_type(x)` // To avoid the difficulty of equality checking for function types containing this, we simply use `literal` for equality checking. @@ -347,7 +347,7 @@ impl<'db, Tag, T, R, const INLINE_CAPACITY: usize> CycleDetector<'db, Tag, T, R, where T: HasIdentity<'db>, { - pub fn new(fallback: R) -> Self { + pub(crate) fn new(fallback: R) -> Self { CycleDetector { seen: RefCell::new(SmallVec::new()), cache: RefCell::new(CycleDetectorCache::new()), diff --git a/crates/ty_python_semantic/src/types/dedicated/pydantic.rs b/crates/ty_python_semantic/src/types/dedicated/pydantic.rs index 258bde82b2..7fa563a70b 100644 --- a/crates/ty_python_semantic/src/types/dedicated/pydantic.rs +++ b/crates/ty_python_semantic/src/types/dedicated/pydantic.rs @@ -1246,7 +1246,7 @@ pub(in crate::types) fn model_init_accepts_extra( } /// Return `true` if extra keywords passed to `class` are silently discarded by Pydantic. -pub(in crate::types) fn model_init_discards_extra( +fn model_init_discards_extra( db: &dyn Db, class: StaticClassLiteral<'_>, metadata: ModelMetadata<'_>, diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index e17eeaea36..fb09c8bdf6 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -1352,7 +1352,7 @@ impl TypeCheckDiagnostics { self.diagnostics.is_empty() && self.used_suppressions.is_empty() } - pub fn iter(&self) -> std::slice::Iter<'_, Diagnostic> { + fn iter(&self) -> std::slice::Iter<'_, Diagnostic> { self.diagnostics().iter() } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index cd9ead41e2..e95f2efb36 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -106,7 +106,7 @@ impl SignatureNameDisplay { #[derive(Debug, Clone, Default)] pub struct DisplaySettings<'db> { /// Whether rendering can be multiline - pub multiline: bool, + multiline: bool, /// Whether callable signatures should include their definition name. signature_name_display: SignatureNameDisplay, /// Class names that should be displayed fully qualified @@ -116,17 +116,17 @@ pub struct DisplaySettings<'db> { /// (e.g., `A.Alias` instead of just `Alias`) qualified_type_aliases: Rc>, /// Whether long unions and literals are displayed in full - pub preserve_full_unions: bool, + preserve_full_unions: bool, /// Scopes that are currently active in the display context (e.g. function scopes /// whose type parameters are currently being displayed). /// Used to suppress redundant `@{scope}` suffixes for type variables. - pub active_scopes: Rc>>, + active_scopes: Rc>>, /// Function types that are currently being displayed. /// Used to prevent infinite recursion when displaying self-referential function types. - pub visited_function_types: Rc>>, + visited_function_types: Rc>>, /// Whether to hide the return type of the outermost signature. /// Return types of nested callable types inside parameters are still shown. - pub hide_return_type: bool, + hide_return_type: bool, } impl<'db> DisplaySettings<'db> { @@ -147,7 +147,7 @@ impl<'db> DisplaySettings<'db> { } #[must_use] - pub fn preserve_long_unions(self) -> Self { + pub(crate) fn preserve_long_unions(self) -> Self { Self { preserve_full_unions: true, ..self @@ -155,7 +155,7 @@ impl<'db> DisplaySettings<'db> { } #[must_use] - pub fn disallow_signature_name(&self) -> Self { + pub(crate) fn disallow_signature_name(&self) -> Self { Self { signature_name_display: SignatureNameDisplay::Disallow, ..self.clone() @@ -171,7 +171,7 @@ impl<'db> DisplaySettings<'db> { } #[must_use] - pub fn hide_return_type(&self) -> Self { + pub(crate) fn hide_return_type(&self) -> Self { Self { hide_return_type: true, ..self.clone() @@ -206,7 +206,7 @@ impl<'db> DisplaySettings<'db> { } #[must_use] - pub fn from_possibly_ambiguous_types(db: &'db dyn Db, types: I) -> Self + pub(crate) fn from_possibly_ambiguous_types(db: &'db dyn Db, types: I) -> Self where I: IntoIterator, T: Into>, @@ -1610,7 +1610,7 @@ impl Display for DisplayTuple<'_, '_> { impl<'db> OverloadLiteral<'db> { // Not currently used, but useful for debugging. #[expect(dead_code)] - pub(crate) fn display(self, db: &'db dyn Db) -> DisplayOverloadLiteral<'db> { + fn display(self, db: &'db dyn Db) -> DisplayOverloadLiteral<'db> { Self::display_with(self, db, DisplaySettings::default()) } @@ -2106,7 +2106,7 @@ impl TupleSpecialization { } impl<'db> CallableType<'db> { - pub(crate) fn display<'a>(&'a self, db: &'db dyn Db) -> DisplayCallableType<'a, 'db> { + fn display<'a>(&'a self, db: &'db dyn Db) -> DisplayCallableType<'a, 'db> { Self::display_with(self, db, DisplaySettings::default()) } @@ -3193,9 +3193,9 @@ impl Display for DisplayStringLiteralType<'_> { } pub(crate) struct DisplayKnownInstanceRepr<'db> { - pub(crate) known_instance: KnownInstanceType<'db>, - pub(crate) db: &'db dyn Db, - pub(crate) settings: DisplaySettings<'db>, + known_instance: KnownInstanceType<'db>, + db: &'db dyn Db, + settings: DisplaySettings<'db>, } impl<'db> KnownInstanceType<'db> { diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index 66ea4a2d61..d747df9be1 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -224,7 +224,7 @@ impl<'db> EnumValueAnnotation<'db> { #[derive(Debug, PartialEq, Eq, salsa::SalsaValue)] pub(crate) struct EnumMetadata<'db> { pub(crate) members: FxIndexMap>, - pub(crate) aliases: FxHashMap, + aliases: FxHashMap, /// Whether alias detection was precise for every member declaration. pub(super) aliases_are_known: bool, @@ -516,7 +516,7 @@ impl<'db> EnumMetadata<'db> { /// data types normalize the value directly. A literal is preserved when its runtime class /// matches an inherited `_value_` annotation; otherwise, the annotation describes the /// normalized value. - pub(crate) fn value_type(&self, db: &'db dyn Db, member_name: &Name) -> Option> { + fn value_type(&self, db: &'db dyn Db, member_name: &Name) -> Option> { if !self.members.contains_key(member_name) { return None; } @@ -798,7 +798,7 @@ impl<'db> EnumComplementType<'db> { /// /// This handles `.name`, `.value`, `._name_`, and `._value_` by unioning the corresponding /// attribute type from each remaining canonical enum member. - pub(crate) fn member_type(self, db: &'db dyn Db, member_name: &str) -> Option> { + fn member_type(self, db: &'db dyn Db, member_name: &str) -> Option> { let enum_class_literal = self.enum_class_literal(db); let is_enum_subclass = Type::ClassLiteral(self.enum_class(db)) .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)); diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index 6bd92a1e18..0f3dc83707 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -375,7 +375,7 @@ pub(crate) struct ComparisonSoundnessPolicy { } impl ComparisonSoundnessPolicy { - pub(crate) const CONSERVATIVE: Self = Self { + const CONSERVATIVE: Self = Self { allow_unsafe_equality: false, }; diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index b6d9e6f32e..9983c100df 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -341,14 +341,14 @@ impl<'db> OverloadLiteral<'db> { /// Returns true if this overload is decorated with `@staticmethod`, or if it is implicitly a /// staticmethod. - pub(crate) fn is_staticmethod(self, db: &dyn Db) -> bool { + fn is_staticmethod(self, db: &dyn Db) -> bool { self.has_known_decorator(db, FunctionDecorators::STATICMETHOD) || is_implicit_staticmethod(self.name(db)) } /// Returns true if this overload is decorated with `@classmethod`, or if it is implicitly a /// classmethod. - pub(crate) fn is_classmethod(self, db: &dyn Db) -> bool { + fn is_classmethod(self, db: &dyn Db) -> bool { self.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) || is_implicit_classmethod(self.name(db)) } @@ -376,7 +376,7 @@ impl<'db> OverloadLiteral<'db> { /// Iterate through the decorators on this function, returning the span of the first one /// that matches the given predicate. - pub(super) fn find_decorator_span( + fn find_decorator_span( self, db: &'db dyn Db, predicate: impl Fn(Type<'db>) -> bool, @@ -966,7 +966,7 @@ impl<'db> FunctionLiteral<'db> { /// statements, or if it is a `Protocol` method that only has a docstring, /// or if it is a `Protocol` method whose body only consists of a single /// `raise NotImplementedError` statement. - pub(super) fn as_abstract_method( + fn as_abstract_method( self, db: &'db dyn Db, enclosing_class: ClassType<'db>, @@ -1022,7 +1022,7 @@ impl<'db> FunctionLiteral<'db> { /// /// Methods defined in stub files are never considered to have trivial bodies, /// since stubs use `...` as a placeholder regardless of the runtime implementation. - pub(crate) fn has_trivial_body(self, db: &'db dyn Db) -> bool { + fn has_trivial_body(self, db: &'db dyn Db) -> bool { !self.definition(db).file(db).is_stub(db) && matches!( self.body_kind(db), diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index e5776e7ff0..a7db82ec58 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1258,11 +1258,7 @@ impl<'db> Specialization<'db> { ) } - pub(crate) fn apply_type_mapping<'a>( - self, - db: &'db dyn Db, - type_mapping: &TypeMapping<'a, 'db>, - ) -> Self { + fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { self.apply_type_mapping_impl(db, type_mapping, &[], &ApplyTypeMappingVisitor::default()) } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 8368b2706b..5aec4f1a30 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -2046,7 +2046,7 @@ mod resolve_definition { } } - pub fn category(&self, db: &dyn Db) -> DefinitionCategory { + pub(crate) fn category(&self, db: &dyn Db) -> DefinitionCategory { match self { ResolvedDefinition::Definition(definition) => { let file = definition.file(db); diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 77290e4599..ff3caa9ea7 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -226,27 +226,25 @@ impl<'db> FunctionDecoratorInference<'db> { self.expression_types.get(&expression.into()).copied() } - pub(crate) fn expression_types( + fn expression_types( &self, ) -> impl ExactSizeIterator)> + '_ { self.expression_types.iter().copied() } - pub(crate) fn bindings( - &self, - ) -> impl ExactSizeIterator, Type<'db>)> + '_ { + fn bindings(&self) -> impl ExactSizeIterator, Type<'db>)> + '_ { self.bindings.iter().copied() } - pub(crate) fn called_functions(&self) -> &[FunctionType<'db>] { + fn called_functions(&self) -> &[FunctionType<'db>] { &self.called_functions } - pub(crate) fn known_decorators(&self) -> FunctionDecorators { + fn known_decorators(&self) -> FunctionDecorators { self.known_decorators } - pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics { + fn diagnostics(&self) -> &TypeCheckDiagnostics { &self.diagnostics } } @@ -525,7 +523,7 @@ pub(super) struct ExpressionWithContext<'db> { } impl<'db> InferExpression<'db> { - pub(super) fn new( + fn new( db: &'db dyn Db, expression: Expression<'db>, tcx: TypeContext<'db>, @@ -564,11 +562,7 @@ pub(super) struct ScopeWithContext<'db> { } impl<'db> InferScope<'db> { - pub(super) fn new( - db: &'db dyn Db, - scope: ScopeId<'db>, - tcx: TypeContext<'db>, - ) -> InferScope<'db> { + fn new(db: &'db dyn Db, scope: ScopeId<'db>, tcx: TypeContext<'db>) -> InferScope<'db> { if tcx.annotation.is_some() { InferScope::WithContext(ScopeWithContext::new(db, scope, tcx)) } else { @@ -614,19 +608,19 @@ impl<'db> TypeContext<'db> { .and_then(|ty| ty.known_specialization(db, known_class)) } - pub(crate) fn map(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Self { + fn map(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Self { Self { annotation: self.annotation.map(f), } } - pub(crate) fn is_typealias(&self) -> bool { + fn is_typealias(&self) -> bool { self.annotation .is_some_and(|ty| ty.is_typealias_special_form()) } /// If the type annotation is a union, returns the target elements that it can be narrowed to. - pub(crate) fn narrow_targets(&self, db: &'db dyn Db) -> Option]>> { + fn narrow_targets(&self, db: &'db dyn Db) -> Option]>> { let union = self.annotation?.as_union_like(db)?; let targets = if union.has_aliases(db) { @@ -1371,7 +1365,7 @@ impl<'db> DefinitionInference<'db> { .or_else(|| self.fallback_type()) } - pub(crate) fn collection_use_constraints( + fn collection_use_constraints( &self, collection_def: Definition<'db>, ) -> Option<&FxIndexSet>> { @@ -1463,14 +1457,14 @@ impl<'db> DefinitionInference<'db> { self.types.declaration_types() } - pub(crate) fn fallback_type(&self) -> Option> { + fn fallback_type(&self) -> Option> { match self.extra.as_deref() { Some(DefinitionInferenceExtra::Other(extra)) => extra.cycle_recovery, Some(_) | None => None, } } - pub(crate) fn discards_dict_key_assignments(&self) -> bool { + fn discards_dict_key_assignments(&self) -> bool { match self.extra.as_deref() { Some(DefinitionInferenceExtra::DiscardsDictKeyAssignments) => true, Some(DefinitionInferenceExtra::Other(extra)) => extra.discards_dict_key_assignments, @@ -1601,10 +1595,7 @@ impl<'db> ExpressionInference<'db> { self } - pub(crate) fn try_expression_type( - &self, - expression: impl Into, - ) -> Option> { + fn try_expression_type(&self, expression: impl Into) -> Option> { self.expressions .get(&expression.into()) .copied() @@ -1616,7 +1607,7 @@ impl<'db> ExpressionInference<'db> { .unwrap_or_else(Type::unknown) } - pub(crate) fn collection_use_constraints( + fn collection_use_constraints( &self, collection_def: Definition<'db>, ) -> Option<&FxIndexSet>> { @@ -1643,7 +1634,7 @@ pub(crate) enum StatementInference<'db> { } impl<'db> StatementInference<'db> { - pub(crate) fn expression_type(&self, expression: impl Into) -> Type<'db> { + fn expression_type(&self, expression: impl Into) -> Type<'db> { match self { StatementInference::Expression(inference) => inference.expression_type(expression), StatementInference::Definition(_, inference) => inference.expression_type(expression), @@ -1651,7 +1642,7 @@ impl<'db> StatementInference<'db> { } } - pub(crate) fn collection_use_constraints( + fn collection_use_constraints( &self, collection_def: Definition<'db>, ) -> Option<&FxIndexSet>> { @@ -1790,22 +1781,19 @@ impl<'db> StatementInferenceInner<'db> { self } - pub(crate) fn expression_type(&self, expression: impl Into) -> Type<'db> { + fn expression_type(&self, expression: impl Into) -> Type<'db> { self.try_expression_type(expression) .unwrap_or_else(Type::unknown) } - pub(crate) fn try_expression_type( - &self, - expression: impl Into, - ) -> Option> { + fn try_expression_type(&self, expression: impl Into) -> Option> { self.expressions .get(&expression.into()) .copied() .or_else(|| self.fallback_type()) } - pub(crate) fn collection_use_constraints( + fn collection_use_constraints( &self, collection_def: Definition<'db>, ) -> Option<&FxIndexSet>> { @@ -1825,7 +1813,7 @@ impl<'db> StatementInferenceInner<'db> { self.declarations.iter().copied() } - pub(crate) fn fallback_type(&self) -> Option> { + fn fallback_type(&self) -> Option> { self.extra.as_ref().and_then(|extra| extra.cycle_recovery) } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 8c6deb53de..799e081089 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -515,7 +515,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.cycle_recovery } - pub(super) fn recursive_type_expression_definition(&self) -> Option> { + fn recursive_type_expression_definition(&self) -> Option> { self.typevar_binding_context.or(match self.region { InferenceRegion::Definition(definition) | InferenceRegion::Deferred(definition) => { Some(definition) @@ -9893,7 +9893,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (place, constraint_keys) } - pub(super) fn report_unresolved_reference(&self, expr_name_node: &ast::ExprName) { + fn report_unresolved_reference(&self, expr_name_node: &ast::ExprName) { let Some(builder) = self .context .report_lint(&UNRESOLVED_REFERENCE, expr_name_node) diff --git a/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs b/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs index 7228d92b80..fed15f69e8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs @@ -98,7 +98,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// expression refers to the first parameter of the enclosing method and has not been shadowed /// in intermediate scopes. We additionally check that the nearest enclosing function has an /// implicit receiver, since static methods also have a first parameter. - pub(super) fn is_instance_attribute_assignment(&self, target: &ast::ExprAttribute) -> bool { + fn is_instance_attribute_assignment(&self, target: &ast::ExprAttribute) -> bool { let Some(place_expr) = PlaceExpr::try_from_expr(target) else { return false; }; diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index 75308838c2..d350c25301 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -166,7 +166,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_subscript_load_impl(value_ty, subscript) } - pub(super) fn infer_subscript_load_impl( + fn infer_subscript_load_impl( &mut self, value_ty: Type<'db>, subscript: &ast::ExprSubscript, @@ -563,7 +563,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { result } - pub(super) fn infer_explicit_callable_specialization_impl( + fn infer_explicit_callable_specialization_impl( &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 31da672420..a91b7754a5 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -31,7 +31,7 @@ use crate::{FxOrderSet, Program, add_inferred_python_version_hint_to_diagnostic} /// Type expressions impl<'db> TypeInferenceBuilder<'db, '_> { - pub(super) const fn type_expression_context(&self) -> &'static str { + const fn type_expression_context(&self) -> &'static str { self.inference_flags().type_expression_context() } @@ -1476,7 +1476,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } - pub(super) fn infer_subscript_type_expression( + fn infer_subscript_type_expression( &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, @@ -2759,7 +2759,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// /// It returns `None` if the argument is invalid i.e., not a list of types, parameter /// specification, `typing.Concatenate`, or `...`. - pub(super) fn infer_callable_parameter_types( + fn infer_callable_parameter_types( &mut self, parameters: &ast::Expr, ) -> Option> { @@ -3049,11 +3049,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// /// Returns `Unknown` as a fallback if the type variable is unbound, otherwise returns the /// original type unchanged. - pub(super) fn check_for_unbound_type_variable( - &self, - expression: &ast::Expr, - ty: Type<'db>, - ) -> Type<'db> { + fn check_for_unbound_type_variable(&self, expression: &ast::Expr, ty: Type<'db>) -> Type<'db> { if !self .inference_flags() .contains(InferenceFlags::CHECK_UNBOUND_TYPEVARS) diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 386c84c977..4ff9987cec 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -1229,7 +1229,7 @@ impl<'db> ProtocolInstanceType<'db> { } /// Returns an effective materialized member without applying the nominal class fallback. - pub(super) fn materialized_interface_member( + fn materialized_interface_member( self, db: &'db dyn Db, name: &str, diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs index 92f0deea33..e5894eedc7 100644 --- a/crates/ty_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -666,7 +666,7 @@ impl<'db> UnionTypeInstance<'db> { ))) } - pub(super) fn apply_type_mapping_impl( + fn apply_type_mapping_impl( self, db: &'db dyn Db, type_mapping: &TypeMapping<'_, 'db>, diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index 0f9e76a435..021f11052d 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -609,13 +609,13 @@ impl<'db> AllMembers<'db> { /// A member of a type or scope, with the first reachable definition of that member. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct MemberWithDefinition<'db> { - pub member: Member<'db>, - pub first_reachable_definition: Definition<'db>, + pub(crate) member: Member<'db>, + pub(crate) first_reachable_definition: Definition<'db>, } /// A member of a type or scope. /// -/// In the context of the [`all_members`] routine, this represents +/// In the context of the `all_members` routine, this represents /// a single item in (ideally) the list returned by `dir(object)`. /// /// The equality, comparison and hashing traits implemented for @@ -628,8 +628,8 @@ pub struct MemberWithDefinition<'db> { /// ordered comparisons. #[derive(Clone, Debug)] pub struct Member<'db> { - pub name: Name, - pub ty: Type<'db>, + pub(crate) name: Name, + pub(crate) ty: Type<'db>, } impl std::hash::Hash for Member<'_> { @@ -660,6 +660,6 @@ impl<'db> PartialOrd for Member<'db> { /// List all members of a given type: anything that would be valid when accessed /// as an attribute on an object of the given type. -pub fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet> { +pub(crate) fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet> { AllMembers::of(db, ty).members } diff --git a/crates/ty_python_semantic/src/types/match_pattern.rs b/crates/ty_python_semantic/src/types/match_pattern.rs index cb15ab8216..1e8f74161d 100644 --- a/crates/ty_python_semantic/src/types/match_pattern.rs +++ b/crates/ty_python_semantic/src/types/match_pattern.rs @@ -700,7 +700,7 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( /// case other: /// reveal_type(other) # Literal[2] /// ``` -pub(crate) fn pattern_fallthrough_type<'db>( +fn pattern_fallthrough_type<'db>( db: &'db dyn Db, kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 659dff017b..209b0ddd96 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -497,7 +497,7 @@ impl<'db> Mro<'db> { /// Compute a fallback MRO for a dynamic class when `of_dynamic_class` fails. /// /// Iterates over base MROs sequentially with deduplication. - pub(super) fn dynamic_fallback(db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>) -> Self { + fn dynamic_fallback(db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>) -> Self { let self_base = ClassBase::Class(ClassType::NonGeneric(dynamic.into())); let mut result = vec![self_base]; let mut seen = FxHashSet::default(); @@ -718,7 +718,7 @@ impl<'db> StaticMroError<'db> { /// Return the fallback MRO we should infer for this class during type inference /// (since accurate resolution of its "true" MRO was impossible) - pub(super) fn fallback_mro(&self) -> &Mro<'db> { + fn fallback_mro(&self) -> &Mro<'db> { &self.fallback_mro } } @@ -763,11 +763,7 @@ pub(super) enum StaticMroErrorKind<'db> { } impl<'db> StaticMroErrorKind<'db> { - pub(super) fn into_mro_error( - self, - db: &'db dyn Db, - class: ClassType<'db>, - ) -> StaticMroError<'db> { + fn into_mro_error(self, db: &'db dyn Db, class: ClassType<'db>) -> StaticMroError<'db> { StaticMroError { kind: self, fallback_mro: Mro::from_error(db, class), @@ -892,7 +888,7 @@ impl<'db> DynamicMroError<'db> { } /// Return the fallback MRO to use for type inference. - pub(crate) fn fallback_mro(&self) -> &Mro<'db> { + fn fallback_mro(&self) -> &Mro<'db> { &self.fallback_mro } } diff --git a/crates/ty_python_semantic/src/types/newtype.rs b/crates/ty_python_semantic/src/types/newtype.rs index d061bfc80c..2daf6ed408 100644 --- a/crates/ty_python_semantic/src/types/newtype.rs +++ b/crates/ty_python_semantic/src/types/newtype.rs @@ -111,7 +111,7 @@ impl<'db> NewType<'db> { Type::object() } - pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { + fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool { // Two instances of the "same" `NewType` won't compare == if one of them has an eagerly // evaluated base (or a normalized base, etc.) and the other doesn't, so we only check for // equality of the `definition`. @@ -121,7 +121,7 @@ impl<'db> NewType<'db> { /// Create a new `NewType` by mapping the underlying `ClassType`. This descends through any /// number of nested `NewType` layers and rebuilds the whole chain. In the rare case of cyclic /// `NewType`s with no underlying `ClassType`, this has no effect and does not call `f`. - pub(crate) fn try_map_base_class_type( + fn try_map_base_class_type( self, db: &'db dyn Db, f: impl FnOnce(ClassType<'db>) -> Option>, diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index caeec4f18a..976aa0d528 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -90,7 +90,7 @@ pub(crate) enum CallableParams { } impl CallableParams { - pub(crate) fn into_parameters(self, db: &TestDb) -> Parameters<'_> { + fn into_parameters(self, db: &TestDb) -> Parameters<'_> { match self { CallableParams::GradualForm => Parameters::gradual_form(), CallableParams::List(params) => Parameters::from_annotation( diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index e5d07f1469..3368b238b7 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -1669,11 +1669,11 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { self.name } - pub(super) fn qualifiers(&self) -> TypeQualifiers { + fn qualifiers(&self) -> TypeQualifiers { self.data.qualifiers } - pub(super) fn is_method(&self) -> bool { + fn is_method(&self) -> bool { matches!(self.data.kind, ProtocolMemberKind::Method(..)) } diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 0eb46402d2..ad0bb1e9a6 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -227,7 +227,7 @@ impl TypeRelation { matches!(self, TypeRelation::Subtyping) } - pub(crate) const fn can_safely_assume_reflexivity(self, ty: Type) -> bool { + const fn can_safely_assume_reflexivity(self, ty: Type) -> bool { match self { TypeRelation::Assignability | TypeRelation::Redundancy { .. } => true, TypeRelation::Subtyping | TypeRelation::SubtypingAssuming => { @@ -400,7 +400,11 @@ impl<'db> Type<'db> { } /// Return true if this type is assignable to type `target` using constraint-set typevar rules. - pub fn is_constraint_set_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { + pub(crate) fn is_constraint_set_assignable_to( + self, + db: &'db dyn Db, + target: Type<'db>, + ) -> bool { let constraints = ConstraintSetBuilder::new(); self.when_constraint_set_assignable_to(db, target, &constraints) .is_always_satisfied(db) @@ -518,7 +522,7 @@ impl<'db> Type<'db> { ) } - pub(super) fn when_constraint_set_subtype_of<'c>( + fn when_constraint_set_subtype_of<'c>( self, db: &'db dyn Db, target: Type<'db>, @@ -559,7 +563,7 @@ impl<'db> Type<'db> { is_redundant_with_impl(db, TypePair::new(db, self, other)) } - pub(super) fn has_relation_to<'c>( + fn has_relation_to<'c>( self, db: &'db dyn Db, target: Type<'db>, @@ -653,7 +657,7 @@ impl<'db> Type<'db> { ) } - pub(crate) fn when_equivalent_to_with_materialization_visitor<'c>( + fn when_equivalent_to_with_materialization_visitor<'c>( self, db: &'db dyn Db, other: Type<'db>, @@ -807,7 +811,7 @@ pub(super) struct TypeRelationChecker<'a, 'c, 'db> { pub(super) relation: TypeRelation, pub(super) typevar_evaluation: TypeVarEvaluation, context_tree: Option>, - pub(super) given: ConstraintSet<'db, 'c>, + given: ConstraintSet<'db, 'c>, perform_expensive_checks: bool, // N.B. these fields are private to reduce the risk of @@ -957,7 +961,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } /// Overwrite the error context tree with a new root context and child nodes. - pub(super) fn set_context( + fn set_context( &self, root: ErrorContext<'db>, children: impl IntoIterator>, @@ -2427,7 +2431,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { }) } - pub(super) fn as_equivalence_checker(&self) -> EquivalenceChecker<'_, 'c, 'db> { + fn as_equivalence_checker(&self) -> EquivalenceChecker<'_, 'c, 'db> { EquivalenceChecker { constraints: self.constraints, given: self.given, @@ -2549,7 +2553,7 @@ impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { pub(super) struct DisjointnessChecker<'a, 'c, 'db> { pub(super) constraints: &'c ConstraintSetBuilder<'db>, - pub(super) inferable: TypeVarSet<'db>, + inferable: TypeVarSet<'db>, given: ConstraintSet<'db, 'c>, perform_expensive_checks: bool, @@ -2605,7 +2609,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { } } - pub(super) fn as_equivalence_checker(&self) -> EquivalenceChecker<'_, 'c, 'db> { + fn as_equivalence_checker(&self) -> EquivalenceChecker<'_, 'c, 'db> { EquivalenceChecker { constraints: self.constraints, given: self.given, diff --git a/crates/ty_python_semantic/src/types/set_theoretic.rs b/crates/ty_python_semantic/src/types/set_theoretic.rs index 4704818407..4aba968713 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic.rs @@ -527,7 +527,7 @@ impl<'db> NegativeIntersectionElements<'db> { } } - pub(crate) fn len(&self) -> usize { + fn len(&self) -> usize { match self { Self::Empty => 0, Self::Single(_) => 1, @@ -572,7 +572,7 @@ impl<'db> NegativeIntersectionElements<'db> { } /// Shrink the capacity of the collection as much as possible. - pub(crate) fn shrink_to_fit(&mut self) { + fn shrink_to_fit(&mut self) { match self { Self::Empty | Self::Single(_) => {} Self::Multiple(set) => set.shrink_to_fit(), @@ -588,7 +588,7 @@ impl<'db> NegativeIntersectionElements<'db> { /// the last element in the collection is popped off the end of the collection /// and placed at the index where `ty` was previously, allowing this method to complete /// in O(1) time (average). - pub(crate) fn swap_remove(&mut self, ty: &Type<'db>) -> bool { + fn swap_remove(&mut self, ty: &Type<'db>) -> bool { match self { Self::Empty => false, Self::Single(existing) => { @@ -610,7 +610,7 @@ impl<'db> NegativeIntersectionElements<'db> { /// The element is removed by swapping it with the last element /// of the collection and popping it off, allowing this method to complete /// in O(1) time (average). - pub(crate) fn swap_remove_index(&mut self, index: usize) -> Option> { + fn swap_remove_index(&mut self, index: usize) -> Option> { match self { Self::Empty => None, Self::Single(existing) => { @@ -1085,7 +1085,7 @@ impl<'db> IntersectionType<'db> { self.positive(db).iter().copied() } - pub fn iter_negative(self, db: &'db dyn Db) -> impl Iterator> { + pub(crate) fn iter_negative(self, db: &'db dyn Db) -> impl Iterator> { self.negative(db).iter().copied() } diff --git a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs index d3836d858c..3b7dc5730b 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs @@ -671,7 +671,7 @@ impl<'db> UnionBuilder<'db> { self.add_in_place_impl(ty, &mut vec![]); } - pub(crate) fn add_in_place_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { + fn add_in_place_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { let cycle_recovery = self.cycle_recovery; let should_widen = |literals, recursively_defined: RecursivelyDefined| { if recursively_defined.is_yes() && cycle_recovery { diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 49a532c00d..5e06105700 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -822,7 +822,7 @@ impl<'db> Signature<'db> { .any(|p| p.should_annotation_be_displayed() && p.annotated_type().contains_self(db)) } - pub(crate) fn with_inherited_generic_context( + fn with_inherited_generic_context( mut self, db: &'db dyn Db, inherited_generic_context: GenericContext<'db>, @@ -867,7 +867,7 @@ impl<'db> Signature<'db> { } } - pub(super) fn recursive_type_normalized_impl( + fn recursive_type_normalized_impl( &self, db: &'db dyn Db, div: Type<'db>, @@ -898,7 +898,7 @@ impl<'db> Signature<'db> { }) } - pub(crate) fn apply_type_mapping_impl<'a>( + fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -937,7 +937,7 @@ impl<'db> Signature<'db> { ) } - pub(crate) fn max_typevar_freshness_matching_generic_context( + fn max_typevar_freshness_matching_generic_context( &self, db: &'db dyn Db, generic_context: GenericContext<'db>, @@ -1445,11 +1445,7 @@ impl<'db> Signature<'db> { } /// Returns this signature with the given specialization applied to parameters and return type. - pub(crate) fn apply_specialization( - &self, - db: &'db dyn Db, - specialization: Specialization<'db>, - ) -> Self { + fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { let type_mapping = TypeMapping::ApplySpecialization(ApplySpecialization::Specialization(specialization)); self.apply_type_mapping_impl( @@ -1461,7 +1457,7 @@ impl<'db> Signature<'db> { } /// Returns the callable signature produced by partially applying this signature. - pub(crate) fn partially_apply( + fn partially_apply( &self, db: &'db dyn Db, partial_application: &PartialApplication<'db>, @@ -1772,7 +1768,7 @@ impl<'db> Signature<'db> { } /// Create a new signature with the given parameters. - pub(crate) fn with_parameters(self, parameters: Parameters<'db>) -> Self { + fn with_parameters(self, parameters: Parameters<'db>) -> Self { Self { parameters, ..self } } @@ -3844,7 +3840,7 @@ impl<'db> Parameters<'db> { /// `TypedDict`. Use [`Self::standard`] for a known-standard list and /// [`Self::from_annotation`] when the kind should be inferred from annotations; preserve the /// existing kind when transforming a parameter list. - pub(crate) fn new(value: impl Into]>>, kind: ParametersKind<'db>) -> Self { + fn new(value: impl Into]>>, kind: ParametersKind<'db>) -> Self { Self { data: Arc::new(ParametersData { value: value.into(), @@ -4265,7 +4261,7 @@ impl<'db> Parameters<'db> { /// Return parameters that represents `(*args: object, **kwargs: object)`, the bottom signature /// (accepts any call, so subtype of all other signatures.) - pub(crate) fn bottom() -> Self { + fn bottom() -> Self { Self::new( [ Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::object()), @@ -4590,7 +4586,7 @@ impl<'db> Parameters<'db> { } /// Expands adjacent `P.args`/`P.kwargs` placeholders into their mapped parameters. - pub(crate) fn expand_paramspec_variadics(&self, db: &'db dyn Db) -> Self { + fn expand_paramspec_variadics(&self, db: &'db dyn Db) -> Self { let mut variadic_index = None; let mut paramspec_callable = None; @@ -4922,7 +4918,7 @@ impl<'db> Parameter<'db> { } } - pub(super) fn recursive_type_normalized_impl( + fn recursive_type_normalized_impl( &self, db: &'db dyn Db, div: Type<'db>, @@ -5083,7 +5079,7 @@ impl<'db> Parameter<'db> { } } - pub(crate) fn callable_by_name(&self, name: &str) -> bool { + fn callable_by_name(&self, name: &str) -> bool { match &self.kind { ParameterKind::PositionalOrKeyword { name: param_name, .. @@ -5165,7 +5161,7 @@ impl<'db> Parameter<'db> { } /// Rewrites a positional-or-keyword parameter as keyword-only while preserving its metadata. - pub(crate) fn positional_or_keyword_to_keyword_only(&self) -> Self { + fn positional_or_keyword_to_keyword_only(&self) -> Self { let mut result = self.clone(); if let ParameterKind::PositionalOrKeyword { name, default_type } = &self.kind { result.kind = ParameterKind::KeywordOnly { diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index c168bca8b8..04ac982359 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -529,7 +529,7 @@ impl SpecialFormType { /// /// Most variants can only exist in one module, which is the same as `self.class().canonical_module(db)`. /// Some variants could validly be defined in either `typing` or `typing_extensions`, however. - pub(super) const fn check_module(self, module: KnownModule) -> bool { + const fn check_module(self, module: KnownModule) -> bool { match self { Self::TypeQualifier(qualifier) => qualifier.check_module(module), Self::LegacyStdlibAlias(_) diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index 462cfd21f2..aebe16f125 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -116,7 +116,7 @@ impl<'db> SubclassOfType<'db> { } /// Return a [`Type`] instance representing the type `type[object]`. - pub(crate) fn subclass_of_object(db: &'db dyn Db) -> Type<'db> { + fn subclass_of_object(db: &'db dyn Db) -> Type<'db> { // See the documentation of `SubclassOfType::from` for details. KnownClass::Type.to_instance(db) } @@ -461,15 +461,15 @@ pub(crate) enum SubclassOfInner<'db> { } impl<'db> SubclassOfInner<'db> { - pub(crate) const fn unknown() -> Self { + const fn unknown() -> Self { Self::Dynamic(DynamicType::Unknown) } - pub(crate) const fn is_dynamic(self) -> bool { + const fn is_dynamic(self) -> bool { matches!(self, Self::Dynamic(_)) } - pub(crate) const fn is_type_var(self) -> bool { + const fn is_type_var(self) -> bool { matches!(self, Self::TypeVar(_)) } @@ -505,7 +505,7 @@ impl<'db> SubclassOfInner<'db> { } } - pub(crate) fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option { + fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option { Some(match ty { Type::NominalInstance(instance) => SubclassOfInner::Class(instance.class(db)), Type::TypedDict(typed_dict) => match typed_dict { @@ -562,7 +562,7 @@ impl<'db> SubclassOfInner<'db> { Self::TypeVar(bound_typevar) } - pub(super) fn recursive_type_normalized_impl( + fn recursive_type_normalized_impl( self, db: &'db dyn Db, div: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/subscript.rs b/crates/ty_python_semantic/src/types/subscript.rs index 983a4d1609..1a8efe6a58 100644 --- a/crates/ty_python_semantic/src/types/subscript.rs +++ b/crates/ty_python_semantic/src/types/subscript.rs @@ -35,7 +35,7 @@ pub(crate) enum SubscriptKind { } impl SubscriptKind { - pub(crate) const fn as_str(self) -> &'static str { + const fn as_str(self) -> &'static str { match self { Self::Tuple => "tuple", Self::String => "string", @@ -64,7 +64,7 @@ impl Display for DunderMethod { } impl DunderMethod { - pub(crate) const fn as_str(self) -> &'static str { + const fn as_str(self) -> &'static str { match self { Self::GetItem => "__getitem__", Self::ClassGetItem => "__class_getitem__", diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index b1a0bb6e6b..33fec18984 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -57,7 +57,7 @@ impl TupleLength { /// Returns the minimum and maximum length of this tuple. (The maximum length will be `None` /// for a tuple with a variable-length portion.) - pub(crate) fn size_hint(self) -> (usize, Option) { + fn size_hint(self) -> (usize, Option) { (self.minimum(), self.maximum()) } @@ -1066,7 +1066,7 @@ impl VariableLengthTuple { self.variable_segment } - pub(crate) fn variable_element_mut(&mut self) -> &mut V { + fn variable_element_mut(&mut self) -> &mut V { &mut self.variable_segment } @@ -1081,7 +1081,7 @@ impl VariableLengthTuple { self.prefix_elements().iter().copied() } - pub(crate) fn prefix_elements_mut(&mut self) -> &mut [T] { + fn prefix_elements_mut(&mut self) -> &mut [T] { &mut self.fixed_elements[..self.prefix_len] } @@ -1096,7 +1096,7 @@ impl VariableLengthTuple { self.suffix_elements().iter().copied() } - pub(crate) fn suffix_elements_mut(&mut self) -> &mut [T] { + fn suffix_elements_mut(&mut self) -> &mut [T] { &mut self.fixed_elements[self.prefix_len..] } @@ -2259,7 +2259,7 @@ impl Tuple { } } - pub(crate) fn into_all_elements_with_kind(self) -> impl Iterator> { + fn into_all_elements_with_kind(self) -> impl Iterator> { match self { Tuple::Fixed(tuple) => { Either::Left(tuple.owned_elements().into_iter().map(TupleElement::Fixed)) @@ -2372,7 +2372,7 @@ impl<'db> Tuple, VariableSegment<'db>> { } } - pub(super) fn recursive_type_normalized_impl( + fn recursive_type_normalized_impl( &self, db: &'db dyn Db, div: Type<'db>, @@ -2388,7 +2388,7 @@ impl<'db> Tuple, VariableSegment<'db>> { } } - pub(crate) fn apply_type_mapping_impl<'a>( + fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -2622,7 +2622,7 @@ impl<'db> PyIndex<'db> for &TupleSpec<'db> { } } -pub(crate) enum TupleElement { +enum TupleElement { Fixed(T), Prefix(T), Variable(V), diff --git a/crates/ty_python_semantic/src/types/type_alias.rs b/crates/ty_python_semantic/src/types/type_alias.rs index c21f76e99a..067f83019d 100644 --- a/crates/ty_python_semantic/src/types/type_alias.rs +++ b/crates/ty_python_semantic/src/types/type_alias.rs @@ -47,14 +47,14 @@ pub(super) fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized #[salsa::tracked] impl<'db> PEP695TypeAliasType<'db> { - pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { + fn definition(self, db: &'db dyn Db) -> Definition<'db> { let scope = self.rhs_scope(db); let type_alias_stmt_node = scope.node(db).expect_type_alias(); semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node) } /// The RHS type of a PEP-695 style type alias with specialization applied. - pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + fn value_type(self, db: &'db dyn Db) -> Type<'db> { apply_type_alias_specialization( db, self.raw_value_type(db), @@ -82,7 +82,7 @@ impl<'db> PEP695TypeAliasType<'db> { definition_expression_type(db, definition, &type_alias_stmt_node.node(&module).value) } - pub(crate) fn apply_specialization( + fn apply_specialization( self, db: &'db dyn Db, f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, @@ -157,7 +157,7 @@ impl<'db> ManualPEP695TypeAliasType<'db> { /// The value type of this manual type alias. /// /// Computed lazily from the definition with specialization applied. - pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + fn value_type(self, db: &'db dyn Db) -> Type<'db> { apply_type_alias_specialization( db, self.raw_value_type(db), @@ -196,7 +196,7 @@ impl<'db> ManualPEP695TypeAliasType<'db> { definition_expression_type(db, definition, value_arg) } - pub(crate) fn apply_specialization( + fn apply_specialization( self, db: &'db dyn Db, f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, @@ -444,7 +444,7 @@ pub(crate) struct QualifiedTypeAliasName<'db> { } impl<'db> QualifiedTypeAliasName<'db> { - pub(crate) fn from_type_alias(db: &'db dyn Db, type_alias: TypeAliasType<'db>) -> Self { + fn from_type_alias(db: &'db dyn Db, type_alias: TypeAliasType<'db>) -> Self { Self { db, type_alias } } diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index 0161984f1f..7eba50cbcf 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -551,7 +551,7 @@ impl<'db> TypedDictType<'db> { } /// Creates a synthesized schema while preserving its undeclared-item policy. - pub(crate) fn from_schema_items_with_openness( + fn from_schema_items_with_openness( db: &'db dyn Db, items: TypedDictSchema<'db>, openness: TypedDictOpenness<'db>, @@ -1575,7 +1575,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { /// Reports errors for any keys that are required but not provided. /// /// Returns true if the assignment is valid, or false otherwise. -pub(super) fn validate_typed_dict_required_keys<'db, 'ast>( +fn validate_typed_dict_required_keys<'db, 'ast>( context: &InferContext<'db, 'ast>, typed_dict: TypedDictType<'db>, provided_keys: &OrderSet, @@ -1961,7 +1961,7 @@ pub(super) fn infer_unpacked_keyword_types<'db>( .collect() } -pub(super) fn unpacked_keyword_is_gradual<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +fn unpacked_keyword_is_gradual<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { match ty.resolve_type_alias(db) { ty if ty.is_never() || ty.is_dynamic() => true, Type::Union(union) => union @@ -2070,7 +2070,7 @@ fn collect_guaranteed_keys_from_merged_unpacked_keyword<'db>( /// /// This is used for mixed positional-and-keyword constructor calls, where guaranteed keyword /// arguments override any same-named keys from the positional mapping. -pub(super) fn typed_dict_without_keys<'db>( +fn typed_dict_without_keys<'db>( db: &'db dyn Db, typed_dict: TypedDictType<'db>, excluded_keys: &OrderSet, @@ -2920,7 +2920,7 @@ impl<'db> SynthesizedTypedDictType<'db> { self.kind(db) == SynthesizedTypedDictKind::Patch } - pub(super) fn apply_type_mapping_impl<'a>( + fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -3041,7 +3041,7 @@ impl<'db> TypedDictField<'db> { .build() } - pub(crate) fn apply_type_mapping_impl<'a>( + fn apply_type_mapping_impl<'a>( self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, @@ -3088,7 +3088,7 @@ impl<'db> TypedDictFieldBuilder<'db> { self } - pub(crate) fn first_declaration(mut self, definition: Option>) -> Self { + fn first_declaration(mut self, definition: Option>) -> Self { self.first_declaration = definition; self } diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index 70217c4935..a1e29b1750 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -756,7 +756,7 @@ impl TypeVarNonce { ) } - pub(crate) fn add(self, delta: u32) -> Self { + fn add(self, delta: u32) -> Self { Self( self.0 .checked_add(delta) @@ -936,7 +936,7 @@ impl<'db> BoundTypeVarInstance<'db> { self.identity(db).paramspec_attr } - pub(super) fn freshness(self, db: &'db dyn Db) -> TypeVarNonce { + fn freshness(self, db: &'db dyn Db) -> TypeVarNonce { self.identity(db).freshness } @@ -1535,7 +1535,7 @@ pub struct BoundTypeVarIdentity<'db> { /// of a `ParamSpec` i.e., `P.args` or `P.kwargs`. pub(super) paramspec_attr: Option, /// The freshness nonce for this bound typevar occurrence; `0` is the source-level occurrence. - pub(super) freshness: TypeVarNonce, + freshness: TypeVarNonce, } impl<'db> BoundTypeVarIdentity<'db> { @@ -1653,7 +1653,7 @@ impl<'db> TypeVarSet<'db> { // Keep this around for debugging purposes #[cfg_attr(not(test), expect(dead_code))] - pub(crate) fn display(self, db: &'db dyn Db) -> String { + fn display(self, db: &'db dyn Db) -> String { format!( "[{}]", self.iter(db) diff --git a/crates/ty_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs index 0469e22d70..0676bcd8fa 100644 --- a/crates/ty_python_semantic/src/types/unpacker.rs +++ b/crates/ty_python_semantic/src/types/unpacker.rs @@ -305,10 +305,7 @@ impl<'db> UnpackResult<'db> { ) } - pub(crate) fn try_expression_type( - &self, - expr: impl Into, - ) -> Option> { + fn try_expression_type(&self, expr: impl Into) -> Option> { self.targets .get(&expr.into()) .copied() diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index c48404b969..3d72f97774 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -309,7 +309,7 @@ pub(crate) fn walk_type_with_recursion_guard<'db>( pub(crate) struct TypeCollector<'db>(RefCell>); impl<'db> TypeCollector<'db> { - pub(crate) fn type_was_already_seen(&self, ty: Type<'db>) -> bool { + fn type_was_already_seen(&self, ty: Type<'db>) -> bool { !self.0.borrow_mut().insert(ty) } } @@ -332,7 +332,7 @@ impl Default for SmallSet { impl SmallSet { #[inline] - pub(super) fn insert(&mut self, value: T) -> bool + fn insert(&mut self, value: T) -> bool where T: Hash + Eq, { @@ -367,7 +367,7 @@ impl SmallSet { } #[cfg(test)] - pub(super) const fn is_spilled(&self) -> bool { + const fn is_spilled(&self) -> bool { matches!(self, Self::Spilled(_)) } } diff --git a/crates/ty_server/src/capabilities.rs b/crates/ty_server/src/capabilities.rs index c70eaae144..b8bff33cc6 100644 --- a/crates/ty_server/src/capabilities.rs +++ b/crates/ty_server/src/capabilities.rs @@ -537,7 +537,7 @@ pub(crate) fn server_diagnostic_options(workspace_diagnostics: bool) -> Diagnost } } -pub(crate) fn server_rename_options() -> RenameOptions { +fn server_rename_options() -> RenameOptions { RenameOptions { prepare_provider: Some(true), work_done_progress_options: WorkDoneProgressOptions::default(), diff --git a/crates/ty_server/src/document/notebook.rs b/crates/ty_server/src/document/notebook.rs index 946bdbfe20..97a1d843c5 100644 --- a/crates/ty_server/src/document/notebook.rs +++ b/crates/ty_server/src/document/notebook.rs @@ -40,7 +40,7 @@ struct NotebookCell { } impl NotebookDocument { - pub fn new( + pub(crate) fn new( uri: lsp_types::Uri, notebook_version: DocumentVersion, cells: Vec, @@ -197,7 +197,7 @@ impl NotebookDocument { } impl NotebookCell { - pub(crate) fn new(cell: lsp_types::NotebookCell) -> Self { + fn new(cell: lsp_types::NotebookCell) -> Self { Self { uri: cell.document, kind: cell.kind, diff --git a/crates/ty_server/src/document/range.rs b/crates/ty_server/src/document/range.rs index f98486064f..fb34bbe6ec 100644 --- a/crates/ty_server/src/document/range.rs +++ b/crates/ty_server/src/document/range.rs @@ -75,7 +75,7 @@ impl LspPosition { /// Returns the uri of the text document this position belongs to. #[expect(unused)] - pub(crate) fn uri(&self) -> Option<&lsp_types::Uri> { + fn uri(&self) -> Option<&lsp_types::Uri> { self.uri.as_ref() } } diff --git a/crates/ty_server/src/document/text_document.rs b/crates/ty_server/src/document/text_document.rs index e908a471ef..3cfec04a7e 100644 --- a/crates/ty_server/src/document/text_document.rs +++ b/crates/ty_server/src/document/text_document.rs @@ -34,7 +34,7 @@ pub struct TextDocument { } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LanguageId { +pub(crate) enum LanguageId { Python, Other, } @@ -49,7 +49,7 @@ impl From for LanguageId { } impl TextDocument { - pub fn new( + pub(crate) fn new( uri: Uri, contents: String, version: DocumentVersion, @@ -78,15 +78,15 @@ impl TextDocument { &self.uri } - pub fn contents(&self) -> &str { + pub(crate) fn contents(&self) -> &str { &self.contents } - pub fn version(&self) -> DocumentVersion { + pub(crate) fn version(&self) -> DocumentVersion { self.version } - pub fn language_id(&self) -> LanguageId { + pub(crate) fn language_id(&self) -> LanguageId { self.language_id } @@ -94,7 +94,7 @@ impl TextDocument { self.notebook.as_ref() } - pub fn apply_changes( + pub(crate) fn apply_changes( &mut self, changes: Vec, new_version: DocumentVersion, @@ -144,7 +144,7 @@ impl TextDocument { }); } - pub fn update_version(&mut self, new_version: DocumentVersion) { + pub(crate) fn update_version(&mut self, new_version: DocumentVersion) { self.modify(|_, version| { *version = new_version; }); diff --git a/crates/ty_server/src/lib.rs b/crates/ty_server/src/lib.rs index 96e4ef5f7f..da4f138448 100644 --- a/crates/ty_server/src/lib.rs +++ b/crates/ty_server/src/lib.rs @@ -19,7 +19,7 @@ mod server; mod session; mod system; -pub(crate) const SERVER_NAME: &str = "ty"; +const SERVER_NAME: &str = "ty"; pub(crate) const DIAGNOSTIC_NAME: &str = "ty"; /// A common result type used in most cases where a diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index a3aa75e09e..0e0637321b 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -567,7 +567,7 @@ impl> LSPResult for core::result::Result { } impl Error { - pub(crate) fn new(err: anyhow::Error, code: server::ErrorCode) -> Self { + fn new(err: anyhow::Error, code: server::ErrorCode) -> Self { Self { code, error: err } } } diff --git a/crates/ty_server/src/server/schedule/thread/pool.rs b/crates/ty_server/src/server/schedule/thread/pool.rs index a66ea88af3..0d9cf303d0 100644 --- a/crates/ty_server/src/server/schedule/thread/pool.rs +++ b/crates/ty_server/src/server/schedule/thread/pool.rs @@ -130,7 +130,7 @@ impl Pool { } #[expect(dead_code)] - pub(super) fn len(&self) -> usize { + fn len(&self) -> usize { self.extant_tasks.load(Ordering::SeqCst) } } diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 8493677cb2..b57704fe11 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -187,7 +187,7 @@ impl Session { &mut self.request_queue } - pub(crate) fn initialization_options(&self) -> &InitializationOptions { + fn initialization_options(&self) -> &InitializationOptions { &self.initialization_options } @@ -325,7 +325,7 @@ impl Session { /// Refer to [`project_db`] for more details on how the project is selected. /// /// [`project_db`]: Session::project_db - pub(crate) fn project_db_mut(&mut self, path: &AnySystemPath) -> &mut ProjectDatabase { + fn project_db_mut(&mut self, path: &AnySystemPath) -> &mut ProjectDatabase { &mut self.project_state_mut(path).db } @@ -335,7 +335,7 @@ impl Session { /// given path, or the first project if no project is found for the path. /// /// If the path is a virtual path, it will return the first project database in the session. - pub(crate) fn project_state(&self, path: &AnySystemPath) -> &ProjectState { + fn project_state(&self, path: &AnySystemPath) -> &ProjectState { match path { AnySystemPath::System(system_path) => self .project_state_for_path(system_path) @@ -382,10 +382,7 @@ impl Session { /// Returns a reference to the project's [`ProjectState`] corresponding to the given path, if /// any. - pub(crate) fn project_state_for_path( - &self, - path: impl AsRef, - ) -> Option<&ProjectState> { + fn project_state_for_path(&self, path: impl AsRef) -> Option<&ProjectState> { let path = path.as_ref(); self.projects .range(..=path.to_path_buf()) @@ -424,7 +421,7 @@ impl Session { } /// Returns a mutable iterator over all projects. - pub(crate) fn project_states_mut(&mut self) -> impl Iterator + '_ { + fn project_states_mut(&mut self) -> impl Iterator + '_ { self.projects.values_mut() } @@ -531,7 +528,7 @@ impl Session { /// /// The client provided is used to show error messages and publish /// diagnostics related to configuration. - pub(crate) fn initialize_workspace_folder( + fn initialize_workspace_folder( &mut self, client: &Client, uri: &Uri, @@ -863,7 +860,7 @@ impl Session { /// This is done by notifying the client with an empty list of diagnostics for the document. /// For notebook cells, this clears diagnostics for the specific cell. /// For other document types, this clears diagnostics for the main document. - pub(crate) fn clear_diagnostics(&self, client: &Client, uri: &Uri) { + fn clear_diagnostics(&self, client: &Client, uri: &Uri) { if self.global_settings().diagnostic_mode().is_off() { return; } @@ -1622,15 +1619,15 @@ impl Workspace { &self.settings } - pub(crate) fn settings_arc(&self) -> Arc { + fn settings_arc(&self) -> Arc { self.settings.clone() } - pub(crate) fn is_initialized(&self) -> bool { + fn is_initialized(&self) -> bool { self.initialized } - pub(crate) fn initialize(&mut self, settings: WorkspaceSettings) { + fn initialize(&mut self, settings: WorkspaceSettings) { self.settings = Arc::new(settings); self.initialized = true; } @@ -1763,7 +1760,7 @@ impl DocumentHandle { } #[expect(unused)] - pub(crate) fn file_path(&self) -> Option<&AnySystemPath> { + fn file_path(&self) -> Option<&AnySystemPath> { match self { Self::Text { path, .. } | Self::Notebook { path, .. } => Some(path), Self::Cell { .. } => None, @@ -1771,7 +1768,7 @@ impl DocumentHandle { } #[expect(unused)] - pub(crate) fn notebook_path(&self) -> Option<&AnySystemPath> { + fn notebook_path(&self) -> Option<&AnySystemPath> { match self { DocumentHandle::Notebook { path, .. } => Some(path), DocumentHandle::Cell { notebook_path, .. } => Some(notebook_path), diff --git a/crates/ty_server/src/session/client.rs b/crates/ty_server/src/session/client.rs index 32baa04c40..2806110b2a 100644 --- a/crates/ty_server/src/session/client.rs +++ b/crates/ty_server/src/session/client.rs @@ -118,7 +118,7 @@ impl Client { /// /// This is useful for notifications that don't require any data. #[expect(dead_code)] - pub(crate) fn send_notification_no_params(&self, method: &str) { + fn send_notification_no_params(&self, method: &str) { if let Err(err) = self.client_sender .send(lsp_server::Message::Notification(Notification::new( diff --git a/crates/ty_server/src/session/index.rs b/crates/ty_server/src/session/index.rs index 3360f339e9..a46b23c012 100644 --- a/crates/ty_server/src/session/index.rs +++ b/crates/ty_server/src/session/index.rs @@ -56,7 +56,7 @@ impl Index { } #[expect(dead_code)] - pub(super) fn notebook_document_keys(&self) -> impl Iterator + '_ { + fn notebook_document_keys(&self) -> impl Iterator + '_ { self.documents .iter() .filter(|(_, doc)| doc.as_notebook().is_some()) @@ -230,11 +230,11 @@ pub(crate) enum Document { } impl Document { - pub(super) fn new_text(document: TextDocument) -> Self { + fn new_text(document: TextDocument) -> Self { Self::Text(Arc::new(document)) } - pub(super) fn new_notebook(document: NotebookDocument) -> Self { + fn new_notebook(document: NotebookDocument) -> Self { Self::Notebook(Arc::new(document)) } @@ -252,7 +252,7 @@ impl Document { } } - pub(crate) fn as_notebook_mut(&mut self) -> Option<&mut NotebookDocument> { + fn as_notebook_mut(&mut self) -> Option<&mut NotebookDocument> { Some(match self { Self::Notebook(notebook) => Arc::make_mut(notebook), Self::Text(_) => return None, diff --git a/crates/ty_server/src/session/options.rs b/crates/ty_server/src/session/options.rs index 18848bb40f..9934cd0e21 100644 --- a/crates/ty_server/src/session/options.rs +++ b/crates/ty_server/src/session/options.rs @@ -487,43 +487,43 @@ impl Combine for PythonExtension { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub(crate) struct ActiveEnvironment { - pub(crate) executable: PythonExecutable, + executable: PythonExecutable, #[deprecated] - pub(crate) environment: Option, - pub(crate) version: Option, + environment: Option, + version: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub(crate) struct EnvironmentVersion { - pub(crate) major: i64, - pub(crate) minor: i64, + major: i64, + minor: i64, #[deprecated( note = "Not provided by all clients (Zed, VS Code when using the Python Environment extension). Use `major` and `minor` instead." )] - pub(crate) patch: Option, + patch: Option, #[deprecated( note = "Not provided by all clients (Zed, VS Code when using the Python Environment extension)." )] - pub(crate) sys_version: Option, + sys_version: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub(crate) struct PythonEnvironment { #[deprecated] - pub(crate) folder_uri: Option, + folder_uri: Option, #[deprecated] #[serde(rename = "type")] - pub(crate) kind: Option, + kind: Option, #[deprecated] - pub(crate) name: Option, + name: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub(crate) struct PythonExecutable { #[deprecated] - pub(crate) uri: Option, - pub(crate) sys_prefix: SystemPathBuf, + uri: Option, + sys_prefix: SystemPathBuf, } diff --git a/crates/ty_server/src/system.rs b/crates/ty_server/src/system.rs index 998f0e1fa2..b5125c98c1 100644 --- a/crates/ty_server/src/system.rs +++ b/crates/ty_server/src/system.rs @@ -50,7 +50,7 @@ impl AnySystemPath { } #[expect(unused)] - pub(crate) const fn as_virtual(&self) -> Option<&SystemVirtualPath> { + const fn as_virtual(&self) -> Option<&SystemVirtualPath> { match self { AnySystemPath::SystemVirtual(path) => Some(path.as_path()), AnySystemPath::System(_) => None, diff --git a/crates/ty_site_packages/src/lib.rs b/crates/ty_site_packages/src/lib.rs index d52a37cac3..21add6d62d 100644 --- a/crates/ty_site_packages/src/lib.rs +++ b/crates/ty_site_packages/src/lib.rs @@ -709,10 +709,7 @@ pub struct VirtualEnvironment { } impl VirtualEnvironment { - pub(crate) fn new( - path: &SysPrefixPath, - system: &dyn System, - ) -> SitePackagesDiscoveryResult { + fn new(path: &SysPrefixPath, system: &dyn System) -> SitePackagesDiscoveryResult { let pyvenv_cfg_path = path.join("pyvenv.cfg"); tracing::debug!("Attempting to parse virtual environment metadata at '{pyvenv_cfg_path}'"); @@ -828,7 +825,7 @@ impl VirtualEnvironment { /// Return a list of `site-packages` directories that are available from this virtual environment /// /// See the documentation for [`site_packages_directories_from_sys_prefix`] for more details. - pub(crate) fn site_packages_directories( + fn site_packages_directories( &self, system: &dyn System, ) -> SitePackagesDiscoveryResult { @@ -898,10 +895,7 @@ System site-packages will not be used for module resolution.", /// Return the real stdlib path (containing actual .py files, and not some variation of typeshed). /// /// See the documentation for [`real_stdlib_directory_from_sys_prefix`] for more details. - pub(crate) fn real_stdlib_directory( - &self, - system: &dyn System, - ) -> StdlibDiscoveryResult { + fn real_stdlib_directory(&self, system: &dyn System) -> StdlibDiscoveryResult { let VirtualEnvironment { base_executable_home_path, implementation, @@ -1002,7 +996,7 @@ impl CondaEnvironmentKind { } /// Read `CONDA_PREFIX` and confirm that it has the expected kind -pub(crate) fn conda_environment_from_env( +fn conda_environment_from_env( system: &dyn System, kind: CondaEnvironmentKind, ) -> Option { @@ -1019,10 +1013,7 @@ pub(crate) fn conda_environment_from_env( Some(path) } -pub(crate) fn environment_from_binary( - system: &dyn System, - binary: &str, -) -> Option { +fn environment_from_binary(system: &dyn System, binary: &str) -> Option { let binary = system.which(binary).ok()?; let env = PythonEnvironment::new(binary, SysPrefixPathOrigin::PythonBinary, system).ok()?; @@ -1159,7 +1150,7 @@ impl SystemEnvironment { /// Return a list of `site-packages` directories that are available from this environment. /// /// See the documentation for [`site_packages_directories_from_sys_prefix`] for more details. - pub(crate) fn site_packages_directories( + fn site_packages_directories( &self, system: &dyn System, ) -> SitePackagesDiscoveryResult { @@ -1178,10 +1169,7 @@ impl SystemEnvironment { /// Return a list of `site-packages` directories that are available from this environment. /// /// See the documentation for [`site_packages_directories_from_sys_prefix`] for more details. - pub(crate) fn real_stdlib_directory( - &self, - system: &dyn System, - ) -> StdlibDiscoveryResult { + fn real_stdlib_directory(&self, system: &dyn System) -> StdlibDiscoveryResult { let stdlib_directory = real_stdlib_directory_from_sys_prefix( self.path.sys_prefix(), self.path.interpreter_layout().unwrap_or_default(), @@ -2144,7 +2132,7 @@ pub enum SysPrefixPathOrigin { impl SysPrefixPathOrigin { /// Whether the given `sys.prefix` path must be a virtual environment (rather than a system /// Python environment). - pub(crate) const fn must_be_virtual_env(&self) -> bool { + const fn must_be_virtual_env(&self) -> bool { match self { Self::LocalVenv | Self::VirtualEnvVar => true, Self::ConfigFileSetting(..) @@ -2162,7 +2150,7 @@ impl SysPrefixPathOrigin { /// /// Some variants can point either directly to `sys.prefix` or to a Python executable inside /// the `sys.prefix` directory, e.g. the `--python` CLI flag. - pub(crate) const fn must_point_directly_to_sys_prefix(&self) -> bool { + const fn must_point_directly_to_sys_prefix(&self) -> bool { match self { Self::PythonCliFlag | Self::ConfigFileSetting(..) diff --git a/crates/ty_test/src/config.rs b/crates/ty_test/src/config.rs index 935bc8b098..28fbed4297 100644 --- a/crates/ty_test/src/config.rs +++ b/crates/ty_test/src/config.rs @@ -119,16 +119,16 @@ pub(crate) struct Environment { /// stable version supported by ty is used (see `ty check --help` output). /// /// ty will not infer the Python version from the Python environment at this time. - pub(crate) python_version: Option, + python_version: Option, /// Target platform to assume when resolving types. - pub(crate) python_platform: Option, + python_platform: Option, /// Path to a custom typeshed directory. - pub(crate) typeshed: Option, + typeshed: Option, /// Additional search paths to consider when resolving modules. - pub(crate) extra_paths: Option>, + extra_paths: Option>, /// Path to the Python environment. /// @@ -142,7 +142,7 @@ pub(crate) struct Environment { /// ty will search in the resolved environment's `site-packages` directories for type /// information and third-party imports. #[serde(skip_serializing_if = "Option::is_none")] - pub python: Option, + python: Option, } #[derive(Deserialize, Default, Debug, Clone)] @@ -195,5 +195,5 @@ pub(crate) struct Project { /// The site-packages directory will then be copied into the test's filesystem. /// /// Example: `dependencies = ["pydantic==2.12.2"]` - pub(crate) dependencies: Option>, + dependencies: Option>, } From 4d03c621b363f14a16fa94065bb6b33301db0273 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 22:24:10 -0400 Subject: [PATCH 190/390] Remove unused APIs from ruff_ranged_value (#27391) ## Summary This PR removes unused APIs from ruff_ranged_value identified by Hawk 0.1.11. It is split from #27339 so the removals can be reviewed independently at the crate boundary. - 10 net lines removed - 10 deletions and 0 additions - 1 files changed --- crates/ruff_ranged_value/src/lib.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/ruff_ranged_value/src/lib.rs b/crates/ruff_ranged_value/src/lib.rs index a3eb869f70..f1305e61fd 100644 --- a/crates/ruff_ranged_value/src/lib.rs +++ b/crates/ruff_ranged_value/src/lib.rs @@ -43,10 +43,6 @@ impl ValueSource { ValueSource::UvWorkspace => None, } } - - pub const fn is_cli(&self) -> bool { - matches!(self, ValueSource::Cli) - } } thread_local! { @@ -171,12 +167,6 @@ impl RangedValue { &self.source } - #[must_use] - pub fn with_source(mut self, source: ValueSource) -> Self { - self.source = source; - self - } - #[must_use] pub fn map_value(self, f: impl FnOnce(T) -> R) -> RangedValue { RangedValue { From 589050b1f91e97deb8c5473b823dfb05e627f2a7 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 22:26:05 -0400 Subject: [PATCH 191/390] [ty] Remove unused APIs from ty_site_packages (#27399) ## Summary This PR removes unused APIs from ty_site_packages identified by Hawk 0.1.11. It is split from #27339 so the removals can be reviewed independently at the crate boundary. - 18 net lines removed - 18 deletions and 0 additions - 2 files changed --- crates/ty_site_packages/src/lib.rs | 8 -------- crates/ty_site_packages/src/version.rs | 10 ---------- 2 files changed, 18 deletions(-) diff --git a/crates/ty_site_packages/src/lib.rs b/crates/ty_site_packages/src/lib.rs index 21add6d62d..751d4f6236 100644 --- a/crates/ty_site_packages/src/lib.rs +++ b/crates/ty_site_packages/src/lib.rs @@ -399,14 +399,6 @@ impl PythonEnvironment { } } - /// Returns the `pyvenv.cfg` path for virtual environments. - pub fn pyvenv_cfg_path(&self) -> Option { - match self { - Self::Virtual(env) => Some(env.root_path.join("pyvenv.cfg")), - Self::System(_) => None, - } - } - /// Returns `true` if this is a virtual environment (has a `pyvenv.cfg` file). pub fn is_virtual(&self) -> bool { matches!(self, Self::Virtual(_)) diff --git a/crates/ty_site_packages/src/version.rs b/crates/ty_site_packages/src/version.rs index 0abe0db9d8..8a643a3e82 100644 --- a/crates/ty_site_packages/src/version.rs +++ b/crates/ty_site_packages/src/version.rs @@ -60,16 +60,6 @@ impl PythonVersionFileSource { Self { path, range } } - /// Returns the path to the configuration file. - pub fn path(&self) -> &SystemPathBuf { - &self.path - } - - /// Returns the range of the configuration setting. - pub fn range(&self) -> Option { - self.range - } - /// Attempt to resolve a [`Span`] that corresponds to the location of /// the configuration setting that specified the Python version. /// From e0a0b8d1ca33a57f7dff04481b94147547036eb5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 22:26:27 -0400 Subject: [PATCH 192/390] [ty] Remove unused APIs from ty_python_core (#27396) ## Summary This PR removes unused APIs from ty_python_core identified by Hawk 0.1.11. It is split from #27339 so the removals can be reviewed independently at the crate boundary. - 10 net lines removed - 10 deletions and 0 additions - 2 files changed --- crates/ty_python_core/src/definition.rs | 6 ------ crates/ty_python_core/src/use_def.rs | 4 ---- 2 files changed, 10 deletions(-) diff --git a/crates/ty_python_core/src/definition.rs b/crates/ty_python_core/src/definition.rs index f8e0c54668..f01dad0665 100644 --- a/crates/ty_python_core/src/definition.rs +++ b/crates/ty_python_core/src/definition.rs @@ -271,12 +271,6 @@ pub struct Definitions<'db> { } impl<'db> Definitions<'db> { - pub fn single(definition: Definition<'db>) -> Self { - Self { - definitions: smallvec::smallvec_inline![definition], - } - } - pub(crate) fn push(&mut self, definition: Definition<'db>) { self.definitions.push(definition); } diff --git a/crates/ty_python_core/src/use_def.rs b/crates/ty_python_core/src/use_def.rs index 6e9ccb887c..fc57fb9142 100644 --- a/crates/ty_python_core/src/use_def.rs +++ b/crates/ty_python_core/src/use_def.rs @@ -869,10 +869,6 @@ impl<'db> UseDefMap<'db> { &self.constraint_tables().reachability_constraints } - pub fn narrowing_constraints(&self) -> &NarrowingConstraints { - &self.constraint_tables().narrowing_constraints - } - pub fn predicates(&self) -> &Predicates<'db> { &self.constraint_tables().predicates } From d6c6b4505a27f4b1735c255e840846fb7a123f3a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 22:26:49 -0400 Subject: [PATCH 193/390] Remove unused APIs from ruff_workspace (#27393) ## Summary This PR removes unused APIs from ruff_workspace identified by Hawk 0.1.11. It is split from #27339 so the removals can be reviewed independently at the crate boundary. - 16 net lines removed - 16 deletions and 0 additions - 2 files changed --- crates/ruff_workspace/src/pyproject.rs | 11 ----------- crates/ruff_workspace/src/resolver.rs | 5 ----- 2 files changed, 16 deletions(-) diff --git a/crates/ruff_workspace/src/pyproject.rs b/crates/ruff_workspace/src/pyproject.rs index 928fa6df3c..e8b64d0f78 100644 --- a/crates/ruff_workspace/src/pyproject.rs +++ b/crates/ruff_workspace/src/pyproject.rs @@ -33,17 +33,6 @@ pub struct Pyproject { project: Option, } -impl Pyproject { - pub const fn new(options: Options) -> Self { - Self { - tool: Some(Tools { - ruff: Some(options), - }), - project: None, - } - } -} - fn parse_toml, T: DeserializeOwned>(path: P, table_path: &[&str]) -> Result { let path = path.as_ref(); diff --git a/crates/ruff_workspace/src/resolver.rs b/crates/ruff_workspace/src/resolver.rs index 92134b64f6..7be2f75e71 100644 --- a/crates/ruff_workspace/src/resolver.rs +++ b/crates/ruff_workspace/src/resolver.rs @@ -67,11 +67,6 @@ pub enum PyprojectDiscoveryStrategy { } impl PyprojectDiscoveryStrategy { - #[inline] - pub const fn is_fixed(self) -> bool { - matches!(self, PyprojectDiscoveryStrategy::Fixed) - } - #[inline] const fn is_hierarchical(self) -> bool { matches!(self, PyprojectDiscoveryStrategy::Hierarchical) From f21e418e5c6399d95e29ac03e4b34c330ac43022 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 22:28:44 -0400 Subject: [PATCH 194/390] Remove unused APIs from ruff_diagnostics (#27383) ## Summary This PR removes unused APIs from ruff_diagnostics identified by Hawk 0.1.11. It is split from #27339 so the removals can be reviewed independently at the crate boundary. - 11 net lines removed - 11 deletions and 0 additions - 1 files changed --- crates/ruff_diagnostics/src/fix.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/crates/ruff_diagnostics/src/fix.rs b/crates/ruff_diagnostics/src/fix.rs index 937d4cd22c..882d7b6eb1 100644 --- a/crates/ruff_diagnostics/src/fix.rs +++ b/crates/ruff_diagnostics/src/fix.rs @@ -104,17 +104,6 @@ impl Fix { } } - /// Create a new [`Fix`] that should only [display](Applicability::DisplayOnly) and not apply from multiple [`Edit`] elements. - pub fn display_only_edits(edit: Edit, rest: impl IntoIterator) -> Self { - let mut edits: Vec = std::iter::once(edit).chain(rest).collect(); - edits.sort_by_key(|edit| (edit.start(), edit.end())); - Self { - edits, - applicability: Applicability::DisplayOnly, - isolation_level: IsolationLevel::default(), - } - } - /// Create a new [`Fix`] with the specified [`Applicability`] to apply an [`Edit`] element. pub fn applicable_edit(edit: Edit, applicability: Applicability) -> Self { Self { From cc7a77ff9daa4cf8c62741b5df55888d7ced0f92 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 22:31:42 -0400 Subject: [PATCH 195/390] Remove unused APIs from ruff_linter (#27385) ## Summary This PR removes unused APIs from ruff_linter identified by Hawk 0.1.11. It is split from #27339 so the removals can be reviewed independently at the crate boundary. - 62 net lines removed - 64 deletions and 2 additions - 9 files changed --- crates/ruff_linter/src/line_width.rs | 5 ---- crates/ruff_linter/src/locator.rs | 23 ------------------- crates/ruff_linter/src/message/mod.rs | 7 +----- crates/ruff_linter/src/registry.rs | 4 ---- crates/ruff_linter/src/registry/rule_set.rs | 4 ---- crates/ruff_linter/src/rule_selector.rs | 3 ++- .../ruff_linter/src/rules/isort/categorize.rs | 14 ----------- .../src/settings/fix_safety_table.rs | 4 ---- crates/ruff_linter/src/settings/types.rs | 4 ---- 9 files changed, 3 insertions(+), 65 deletions(-) diff --git a/crates/ruff_linter/src/line_width.rs b/crates/ruff_linter/src/line_width.rs index 911c43051e..f01390036c 100644 --- a/crates/ruff_linter/src/line_width.rs +++ b/crates/ruff_linter/src/line_width.rs @@ -10,7 +10,6 @@ use unicode_width::UnicodeWidthChar; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_macros::CacheKey; use ruff_python_trivia::tab_offset; -use ruff_text_size::TextSize; /// The length of a line of text that is considered too long. /// @@ -27,10 +26,6 @@ impl LineLength { pub fn value(&self) -> u16 { self.0.get() } - - pub fn text_len(&self) -> TextSize { - TextSize::from(u32::from(self.value())) - } } impl Default for LineLength { diff --git a/crates/ruff_linter/src/locator.rs b/crates/ruff_linter/src/locator.rs index fe9388e473..1110594f78 100644 --- a/crates/ruff_linter/src/locator.rs +++ b/crates/ruff_linter/src/locator.rs @@ -19,13 +19,6 @@ impl<'a> Locator<'a> { } } - pub fn with_index(contents: &'a str, index: LineIndex) -> Self { - Self { - contents, - index: OnceCell::from(index), - } - } - #[deprecated( note = "This is expensive, avoid using outside of the diagnostic phase. Prefer the other `Locator` methods instead." )] @@ -45,10 +38,6 @@ impl<'a> Locator<'a> { .get_or_init(|| LineIndex::from_source_text(self.contents)) } - pub fn line_index(&self) -> Option<&LineIndex> { - self.index.get() - } - pub fn to_source_code(&self) -> SourceCode<'_, '_> { SourceCode::new(self.contents, self.to_index()) } @@ -126,11 +115,6 @@ impl<'a> Locator<'a> { pub(crate) fn text_len(&self) -> TextSize { self.contents.text_len() } - - /// Return `true` if the source code is empty. - pub const fn is_empty(&self) -> bool { - self.contents.is_empty() - } } // Override the `_str` methods from [`LineRanges`] to extend the lifetime to `'a`. @@ -155,13 +139,6 @@ impl<'a> Locator<'a> { pub(crate) fn lines_str(&self, range: TextRange) -> &'a str { self.contents.lines_str(range) } - - /// Returns the text of all lines that include `range`. - /// - /// See [`LineRanges::full_lines_str`]. - pub fn full_lines_str(&self, range: TextRange) -> &'a str { - self.contents.full_lines_str(range) - } } // Allow calling [`LineRanges`] methods on [`Locator`] directly. diff --git a/crates/ruff_linter/src/message/mod.rs b/crates/ruff_linter/src/message/mod.rs index 38fab1523e..9f33f21e04 100644 --- a/crates/ruff_linter/src/message/mod.rs +++ b/crates/ruff_linter/src/message/mod.rs @@ -175,7 +175,7 @@ pub(crate) trait Emitter { ) -> anyhow::Result<()>; } -/// Context passed to diagnostic emitters. +/// Context used while rendering diagnostics. pub struct EmitterContext<'a> { notebook_indexes: &'a FxHashMap, } @@ -185,11 +185,6 @@ impl<'a> EmitterContext<'a> { Self { notebook_indexes } } - /// Tests if the file with `name` is a jupyter notebook. - pub fn is_notebook(&self, name: &str) -> bool { - self.notebook_indexes.contains_key(name) - } - fn notebook_index(&self, name: &str) -> Option<&NotebookIndex> { self.notebook_indexes.get(name) } diff --git a/crates/ruff_linter/src/registry.rs b/crates/ruff_linter/src/registry.rs index 21892be74d..83b0b2c9d7 100644 --- a/crates/ruff_linter/src/registry.rs +++ b/crates/ruff_linter/src/registry.rs @@ -12,10 +12,6 @@ use crate::codes::{self}; mod rule_set; -pub trait AsRule { - fn rule(&self) -> Rule; -} - impl Rule { pub fn from_code(code: &str) -> Result { let (linter, code) = Linter::parse_code(code).ok_or(FromCodeError::Unknown)?; diff --git a/crates/ruff_linter/src/registry/rule_set.rs b/crates/ruff_linter/src/registry/rule_set.rs index 80cae3432e..bd785d463e 100644 --- a/crates/ruff_linter/src/registry/rule_set.rs +++ b/crates/ruff_linter/src/registry/rule_set.rs @@ -24,10 +24,6 @@ impl RuleSet { Self(Self::EMPTY) } - pub fn clear(&mut self) { - self.0 = Self::EMPTY; - } - #[inline] pub const fn from_rule(rule: Rule) -> Self { let rule = rule as u16; diff --git a/crates/ruff_linter/src/rule_selector.rs b/crates/ruff_linter/src/rule_selector.rs index d5c0295f91..1205af1a6b 100644 --- a/crates/ruff_linter/src/rule_selector.rs +++ b/crates/ruff_linter/src/rule_selector.rs @@ -438,7 +438,8 @@ impl RuleSelector { } /// Parse [`RuleSelector`] from a string; but do not follow redirects. - pub fn parse_no_redirect(s: &str) -> Result { + #[cfg(feature = "schemars")] + fn parse_no_redirect(s: &str) -> Result { // **Changes should be reflected in `from_str` as well** match s { "ALL" => Ok(Self::All), diff --git a/crates/ruff_linter/src/rules/isort/categorize.rs b/crates/ruff_linter/src/rules/isort/categorize.rs index 30010a1e65..9eae6f9b22 100644 --- a/crates/ruff_linter/src/rules/isort/categorize.rs +++ b/crates/ruff_linter/src/rules/isort/categorize.rs @@ -410,20 +410,6 @@ impl KnownModules { }; Some((section, reason)) } - - /// Return the list of user-defined modules, indexed by section. - pub fn user_defined(&self) -> FxHashMap<&str, Vec<&IdentifierPattern>> { - let mut user_defined: FxHashMap<&str, Vec<&IdentifierPattern>> = FxHashMap::default(); - for (module, section) in &self.known { - if let ImportSection::UserDefined(section_name) = section { - user_defined - .entry(section_name.as_str()) - .or_default() - .push(module); - } - } - user_defined - } } impl fmt::Display for KnownModules { diff --git a/crates/ruff_linter/src/settings/fix_safety_table.rs b/crates/ruff_linter/src/settings/fix_safety_table.rs index 8b92f55eed..6b3ad65b11 100644 --- a/crates/ruff_linter/src/settings/fix_safety_table.rs +++ b/crates/ruff_linter/src/settings/fix_safety_table.rs @@ -41,10 +41,6 @@ impl FixSafetyTable { } } - pub const fn is_empty(&self) -> bool { - self.forced_safe.is_empty() && self.forced_unsafe.is_empty() - } - pub fn from_rule_selectors( extend_safe_fixes: &[UnresolvedRuleSelector], extend_unsafe_fixes: &[UnresolvedRuleSelector], diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index 9fcd1841a4..757ca0b5b6 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -184,10 +184,6 @@ impl GlobPath { let absolute = fs::normalize_path_to(path, escaped); Self { path: absolute } } - - pub fn into_inner(self) -> PathBuf { - self.path - } } impl Deref for GlobPath { From 103168bd42584b777a8afaaa0d3ed0ca77e84a8d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 22:32:19 -0400 Subject: [PATCH 196/390] Remove unused APIs from ruff_python_stdlib (#27390) ## Summary This PR removes an unused API from ruff_python_stdlib identified by Hawk 0.1.11. It is split from #27339 so the removal can be reviewed independently at the crate boundary. - 6 net lines removed - 6 deletions - 1 file changed --- crates/ruff_python_stdlib/src/path.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/ruff_python_stdlib/src/path.rs b/crates/ruff_python_stdlib/src/path.rs index 6bc14c78bf..467287cc1f 100644 --- a/crates/ruff_python_stdlib/src/path.rs +++ b/crates/ruff_python_stdlib/src/path.rs @@ -1,12 +1,6 @@ use std::ffi::OsStr; use std::path::Path; -/// Return `true` if the [`Path`] is named `pyproject.toml`. -pub fn is_pyproject_toml(path: &Path) -> bool { - path.file_name() - .is_some_and(|name| name == "pyproject.toml") -} - /// Return `true` if a [`Path`] should use the name of its parent directory as its module name. pub fn is_module_file(path: &Path) -> bool { matches!( From 8089b331dae47afa7972f2a07e0de42bc6ee1c55 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 22:37:00 -0400 Subject: [PATCH 197/390] [ty] Remove unused APIs from ty_server (#27398) ## Summary This PR removes unused APIs from ty_server identified by Hawk 0.1.11. It is split from #27339 so the removals can be reviewed independently at the crate boundary. - 4 net lines removed - 4 deletions and 0 additions - 1 files changed --- crates/ty_server/src/document/text_document.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/ty_server/src/document/text_document.rs b/crates/ty_server/src/document/text_document.rs index 3cfec04a7e..b9005f1d57 100644 --- a/crates/ty_server/src/document/text_document.rs +++ b/crates/ty_server/src/document/text_document.rs @@ -70,10 +70,6 @@ impl TextDocument { self } - pub fn into_contents(self) -> String { - self.contents - } - pub(crate) fn uri(&self) -> &Uri { &self.uri } From 9783303186b1d05521154053fcd10da5d886bbf9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 22:37:36 -0400 Subject: [PATCH 198/390] Remove unused APIs from ruff_db (#27382) ## Summary This PR removes unused APIs from ruff_db identified by Hawk 0.1.11. It is split from #27339 so the removals can be reviewed independently at the crate boundary. - 56 net lines removed - 61 deletions and 5 additions - 8 files changed --- crates/ruff_db/src/cancellation.rs | 4 ---- crates/ruff_db/src/diagnostic/mod.rs | 26 +++++-------------------- crates/ruff_db/src/diagnostic/render.rs | 6 ------ crates/ruff_db/src/display.rs | 11 ----------- crates/ruff_db/src/file_revision.rs | 5 +---- crates/ruff_db/src/files.rs | 7 ------- crates/ruff_db/src/lib.rs | 2 -- crates/ruff_db/src/system/test.rs | 7 ------- 8 files changed, 6 insertions(+), 62 deletions(-) diff --git a/crates/ruff_db/src/cancellation.rs b/crates/ruff_db/src/cancellation.rs index 92086281fe..42c67031f4 100644 --- a/crates/ruff_db/src/cancellation.rs +++ b/crates/ruff_db/src/cancellation.rs @@ -21,10 +21,6 @@ impl CancellationTokenSource { } } - pub fn is_cancellation_requested(&self) -> bool { - self.cancelled.load(std::sync::atomic::Ordering::Relaxed) - } - /// Creates a new token that uses this source. pub fn token(&self) -> CancellationToken { CancellationToken { diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index cbac14ec6d..4794850337 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -385,13 +385,8 @@ impl Diagnostic { Arc::make_mut(&mut self.inner).fix = None; } - /// Returns `true` if the diagnostic contains a [`Fix`]. - pub fn fixable(&self) -> bool { - self.fix().is_some() - } - - /// Returns `true` if the diagnostic is [`fixable`](Diagnostic::fixable) and applies at the - /// configured applicability level. + /// Returns `true` if the diagnostic has a fix that applies at the configured applicability + /// level. pub fn has_applicable_fix(&self, fix_applicability: Applicability) -> bool { self.fix().is_some_and(|fix| fix.applies(fix_applicability)) } @@ -419,6 +414,7 @@ impl Diagnostic { /// Returns the remapped offset for a suppression comment if it exists. /// /// Like [`Diagnostic::parent`], this is used for noqa code suppression comments in Ruff. + #[cfg(feature = "serde")] fn noqa_offset(&self) -> Option { self.inner.noqa_offset } @@ -904,19 +900,6 @@ impl Annotation { self.span = 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); @@ -1519,7 +1502,8 @@ impl DisplayDiagnosticConfig { /// /// Nearby annotations or fix edits are rendered in a single source frame even when their /// configured context windows would not otherwise overlap. - pub fn merge_window(self, lines: usize) -> DisplayDiagnosticConfig { + #[cfg(test)] + fn merge_window(self, lines: usize) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig { merge_window: lines, ..self diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index 4cb86bf5b6..08d76e61ec 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -901,12 +901,6 @@ pub struct Input { pub(crate) line_index: LineIndex, } -impl Input { - pub fn line_index(&self) -> &LineIndex { - &self.line_index - } -} - /// Returns the line number accounting for the given `len` /// number of preceding context lines. /// diff --git a/crates/ruff_db/src/display.rs b/crates/ruff_db/src/display.rs index 27f93846e8..27e683e06e 100644 --- a/crates/ruff_db/src/display.rs +++ b/crates/ruff_db/src/display.rs @@ -35,17 +35,6 @@ impl Join<'_, '_> { self } - pub fn entries(&mut self, items: I) -> &mut Self - where - I: IntoIterator, - F: Display, - { - for item in items { - self.entry(&item); - } - self - } - pub fn finish(&mut self) -> fmt::Result { self.result } diff --git a/crates/ruff_db/src/file_revision.rs b/crates/ruff_db/src/file_revision.rs index 826e3b1a6c..1ce1c30abf 100644 --- a/crates/ruff_db/src/file_revision.rs +++ b/crates/ruff_db/src/file_revision.rs @@ -1,3 +1,4 @@ +#[cfg(test)] use crate::system::file_time_now; /// A number representing the revision of a file. @@ -17,10 +18,6 @@ impl FileRevision { Self(value) } - pub fn now() -> Self { - Self::from(file_time_now()) - } - pub(crate) const fn zero() -> Self { Self(0) } diff --git a/crates/ruff_db/src/files.rs b/crates/ruff_db/src/files.rs index 8f3b5cd39e..23f7175175 100644 --- a/crates/ruff_db/src/files.rs +++ b/crates/ruff_db/src/files.rs @@ -4,8 +4,6 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use dashmap::mapref::entry::Entry; -#[expect(unused_imports)] -pub(crate) use directory::system_path_to_directory; pub use directory::{DirectoryListing, DirectoryListingError, directory_listing}; pub use file_root::{FileRoot, FileRootKind}; pub use path::FilePath; @@ -568,11 +566,6 @@ impl File { self.source_type(db).is_stub() } - /// Returns `true` if the file is an `__init__.pyi` - pub fn is_package_stub(self, db: &dyn Db) -> bool { - self.path(db).as_str().ends_with("__init__.pyi") - } - /// Returns `true` if the file is an `__init__.pyi` pub fn is_package(self, db: &dyn Db) -> bool { let path = self.path(db).as_str(); diff --git a/crates/ruff_db/src/lib.rs b/crates/ruff_db/src/lib.rs index c8a19b0506..8fddfa7968 100644 --- a/crates/ruff_db/src/lib.rs +++ b/crates/ruff_db/src/lib.rs @@ -32,8 +32,6 @@ pub use std::time::{Instant, SystemTime, SystemTimeError}; pub use web_time::{Instant, SystemTime, SystemTimeError}; pub type FxDashMap = dashmap::DashMap>; -pub type FxDashSet = dashmap::DashSet>; - static VERSION: std::sync::OnceLock = std::sync::OnceLock::new(); /// Returns the version of the executing program if set. diff --git a/crates/ruff_db/src/system/test.rs b/crates/ruff_db/src/system/test.rs index bfea3a1276..5e01bffec9 100644 --- a/crates/ruff_db/src/system/test.rs +++ b/crates/ruff_db/src/system/test.rs @@ -333,13 +333,6 @@ pub struct InMemorySystem { } impl InMemorySystem { - pub fn new(cwd: SystemPathBuf) -> Self { - Self { - user_config_directory: Mutex::new(None).into(), - memory_fs: MemoryFileSystem::with_current_directory(cwd), - } - } - pub fn from_memory_fs(memory_fs: MemoryFileSystem) -> Self { Self { user_config_directory: Mutex::new(None).into(), From 67ecabc3c45604b7f3ee027b543244ac65e59935 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 22:37:55 -0400 Subject: [PATCH 199/390] [ty] Remove unused APIs from ty_module_resolver (#27394) ## Summary This PR removes unused APIs from ty_module_resolver identified by Hawk 0.1.11. It is split from #27339 so the removals can be reviewed independently at the crate boundary. - 1 net lines removed - 2 deletions and 1 additions - 2 files changed --- crates/ty_module_resolver/src/lib.rs | 2 -- crates/ty_module_resolver/src/resolve.rs | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/ty_module_resolver/src/lib.rs b/crates/ty_module_resolver/src/lib.rs index 8f7bbdad55..fb4039878c 100644 --- a/crates/ty_module_resolver/src/lib.rs +++ b/crates/ty_module_resolver/src/lib.rs @@ -13,8 +13,6 @@ pub use resolve::{ }; pub use settings::{SearchPathSettings, SearchPathSettingsError}; pub use strategy::{FallibleStrategy, MisconfigurationStrategy, UseDefaultStrategy}; -#[expect(unused_imports)] -pub(crate) use typeshed::vendored_typeshed_versions; pub use typeshed::{PyVersionRange, TypeshedVersions, TypeshedVersionsParseError}; pub use list::{all_modules, list_modules}; diff --git a/crates/ty_module_resolver/src/resolve.rs b/crates/ty_module_resolver/src/resolve.rs index 787f93460e..9403038e58 100644 --- a/crates/ty_module_resolver/src/resolve.rs +++ b/crates/ty_module_resolver/src/resolve.rs @@ -760,7 +760,8 @@ impl SearchPaths { /// Returns a new `SearchPaths` with no search paths configured. /// /// This is primarily useful for testing. - pub fn empty(vendored: &VendoredFileSystem) -> Self { + #[cfg(test)] + pub(crate) fn empty(vendored: &VendoredFileSystem) -> Self { Self { static_paths: vec![], stdlib_path: Some(SearchPath::vendored_stdlib()), From adb763deb889b3fbcf638218996c21f7bba52618 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 31 Jul 2026 22:47:23 -0400 Subject: [PATCH 200/390] [ty] Remove unused APIs from ty_project (#27395) ## Summary This PR removes unused APIs from ty_project identified by Hawk 0.1.11. It is split from #27339 so the removals can be reviewed independently at the crate boundary. - 27 net lines removed - 27 deletions - 2 files changed --- crates/ty_project/src/metadata/value.rs | 11 ----------- crates/ty_project/src/watch/watcher.rs | 16 ---------------- 2 files changed, 27 deletions(-) diff --git a/crates/ty_project/src/metadata/value.rs b/crates/ty_project/src/metadata/value.rs index a8b8a47bf7..616a118080 100644 --- a/crates/ty_project/src/metadata/value.rs +++ b/crates/ty_project/src/metadata/value.rs @@ -7,7 +7,6 @@ use ruff_macros::Combine; use ruff_ranged_value::{RangedValue, ValueSource}; use ruff_text_size::TextRange; -use crate::Db; use crate::glob::{ AbsolutePortableGlobPattern, PortableGlobError, PortableGlobKind, PortableGlobPattern, }; @@ -62,16 +61,6 @@ impl RelativePathBuf { self.0.range() } - /// Returns the owned relative path. - pub fn into_path_buf(self) -> SystemPathBuf { - self.0.into_inner() - } - - /// Resolves the absolute path for `self` based on its origin. - pub fn absolute_with_db(&self, db: &dyn Db) -> SystemPathBuf { - self.absolute(db.project().root(db), db.system()) - } - /// Resolves the absolute path for `self` based on its origin. pub fn absolute(&self, project_root: &SystemPath, system: &dyn System) -> SystemPathBuf { let relative_to = match self.0.source() { diff --git a/crates/ty_project/src/watch/watcher.rs b/crates/ty_project/src/watch/watcher.rs index 1cfcef027d..ba016f59ba 100644 --- a/crates/ty_project/src/watch/watcher.rs +++ b/crates/ty_project/src/watch/watcher.rs @@ -112,22 +112,6 @@ struct WatcherInner { } impl Watcher { - /// Sets up file watching for `path`. - pub fn watch(&mut self, path: &SystemPath) -> notify::Result<()> { - tracing::debug!("Watching path: `{path}`"); - - self.inner_mut() - .watcher - .watch(path.as_std_path(), RecursiveMode::Recursive) - } - - /// Stops file watching for `path`. - pub fn unwatch(&mut self, path: &SystemPath) -> notify::Result<()> { - tracing::debug!("Unwatching path: `{path}`"); - - self.inner_mut().watcher.unwatch(path.as_std_path()) - } - /// Returns a transaction-like view for updating watched paths in one backend operation. pub(crate) fn paths_mut(&mut self) -> WatcherPathsMut<'_> { WatcherPathsMut { From 62e3fb4d3c010e1ed1daa162f544cb95f8d2286e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:40:45 +0100 Subject: [PATCH 201/390] [ty] Sync vendored typeshed stubs (#27401) Close and reopen this PR to trigger CI --------- Co-authored-by: typeshedbot <> --- crates/ty_ide/src/type_hierarchy.rs | 8 +- .../vendor/typeshed/source_commit.txt | 2 +- .../vendor/typeshed/stdlib/__main__.pyi | 2 +- .../vendor/typeshed/stdlib/_asyncio.pyi | 2 +- .../vendor/typeshed/stdlib/_ctypes.pyi | 10 +- .../vendor/typeshed/stdlib/_curses.pyi | 9 +- .../vendor/typeshed/stdlib/_curses_panel.pyi | 8 +- .../vendor/typeshed/stdlib/_sitebuiltins.pyi | 5 +- .../vendor/typeshed/stdlib/_struct.pyi | 16 +- .../vendor/typeshed/stdlib/_thread.pyi | 8 +- .../typeshed/stdlib/_typeshed/__init__.pyi | 8 + .../vendor/typeshed/stdlib/_winapi.pyi | 9 +- .../vendor/typeshed/stdlib/argparse.pyi | 16 +- .../typeshed/stdlib/asyncio/coroutines.pyi | 8 +- .../vendor/typeshed/stdlib/asyncio/events.pyi | 12 +- .../vendor/typeshed/stdlib/asyncio/trsock.pyi | 14 +- .../stdlib/asyncio/windows_events.pyi | 11 +- .../vendor/typeshed/stdlib/builtins.pyi | 21 ++- .../vendor/typeshed/stdlib/codecs.pyi | 2 +- .../typeshed/stdlib/collections/__init__.pyi | 15 +- .../stdlib/concurrent/futures/process.pyi | 7 +- .../vendor/typeshed/stdlib/contextlib.pyi | 4 +- .../typeshed/stdlib/ctypes/__init__.pyi | 4 +- .../vendor/typeshed/stdlib/dataclasses.pyi | 12 +- .../vendor/typeshed/stdlib/datetime.pyi | 6 +- .../vendor/typeshed/stdlib/decimal.pyi | 2 +- .../vendor/typeshed/stdlib/glob.pyi | 8 +- .../vendor/typeshed/stdlib/http/cookiejar.pyi | 4 +- .../vendor/typeshed/stdlib/http/server.pyi | 2 +- .../vendor/typeshed/stdlib/imaplib.pyi | 57 +++++-- .../typeshed/stdlib/importlib/readers.pyi | 8 +- .../stdlib/importlib/resources/__init__.pyi | 2 +- .../stdlib/importlib/resources/abc.pyi | 2 +- .../stdlib/importlib/resources/simple.pyi | 6 +- .../typeshed/stdlib/lib2to3/pgen2/pgen.pyi | 7 +- .../vendor/typeshed/stdlib/lib2to3/pytree.pyi | 4 +- .../typeshed/stdlib/lib2to3/refactor.pyi | 5 +- .../vendor/typeshed/stdlib/locale.pyi | 2 +- .../typeshed/stdlib/logging/__init__.pyi | 2 +- .../vendor/typeshed/stdlib/math/__init__.pyi | 2 +- .../vendor/typeshed/stdlib/mmap.pyi | 6 +- .../vendor/typeshed/stdlib/nturl2path.pyi | 4 +- .../vendor/typeshed/stdlib/optparse.pyi | 8 +- .../vendor/typeshed/stdlib/os/__init__.pyi | 29 ++-- .../typeshed/stdlib/pathlib/__init__.pyi | 4 +- .../vendor/typeshed/stdlib/pkgutil.pyi | 4 +- .../vendor/typeshed/stdlib/platform.pyi | 6 +- .../vendor/typeshed/stdlib/poplib.pyi | 8 +- .../stdlib/profiling/sampling/collector.pyi | 6 +- .../vendor/typeshed/stdlib/pty.pyi | 4 +- .../vendor/typeshed/stdlib/pydoc.pyi | 8 +- .../vendor/typeshed/stdlib/random.pyi | 8 +- .../ty_vendored/vendor/typeshed/stdlib/re.pyi | 5 +- .../vendor/typeshed/stdlib/shutil.pyi | 6 +- .../vendor/typeshed/stdlib/ssl.pyi | 14 +- .../vendor/typeshed/stdlib/sunau.pyi | 6 +- .../vendor/typeshed/stdlib/symtable.pyi | 2 +- .../vendor/typeshed/stdlib/sys/__init__.pyi | 8 +- .../vendor/typeshed/stdlib/sysconfig.pyi | 4 +- .../vendor/typeshed/stdlib/tarfile.pyi | 4 +- .../typeshed/stdlib/tkinter/__init__.pyi | 146 +++++++++++------- .../typeshed/stdlib/tkinter/colorchooser.pyi | 2 + .../typeshed/stdlib/tkinter/commondialog.pyi | 2 + .../vendor/typeshed/stdlib/tkinter/dialog.pyi | 7 +- .../vendor/typeshed/stdlib/tkinter/font.pyi | 2 + .../typeshed/stdlib/tkinter/messagebox.pyi | 12 +- .../typeshed/stdlib/tkinter/simpledialog.pyi | 2 +- .../vendor/typeshed/stdlib/tkinter/ttk.pyi | 63 +++++--- .../vendor/typeshed/stdlib/types.pyi | 2 +- .../vendor/typeshed/stdlib/typing.pyi | 8 +- .../typeshed/stdlib/typing_extensions.pyi | 8 +- .../vendor/typeshed/stdlib/unittest/case.pyi | 5 +- .../typeshed/stdlib/unittest/loader.pyi | 6 +- .../vendor/typeshed/stdlib/unittest/main.pyi | 2 +- .../vendor/typeshed/stdlib/unittest/mock.pyi | 14 +- .../vendor/typeshed/stdlib/urllib/request.pyi | 6 +- .../vendor/typeshed/stdlib/uuid.pyi | 6 +- .../vendor/typeshed/stdlib/wave.pyi | 20 +-- .../vendor/typeshed/stdlib/webbrowser.pyi | 2 +- .../typeshed/stdlib/wsgiref/validate.pyi | 7 +- .../typeshed/stdlib/xml/dom/expatbuilder.pyi | 7 +- .../typeshed/stdlib/xml/dom/minidom.pyi | 28 ++-- .../typeshed/stdlib/xml/dom/pulldom.pyi | 8 +- .../typeshed/stdlib/xml/dom/xmlbuilder.pyi | 13 +- .../typeshed/stdlib/xml/sax/_exceptions.pyi | 4 +- .../typeshed/stdlib/xml/sax/handler.pyi | 11 +- .../typeshed/stdlib/xml/sax/saxutils.pyi | 7 +- 87 files changed, 518 insertions(+), 358 deletions(-) diff --git a/crates/ty_ide/src/type_hierarchy.rs b/crates/ty_ide/src/type_hierarchy.rs index e2a01b2cb3..e7fff23e78 100644 --- a/crates/ty_ide/src/type_hierarchy.rs +++ b/crates/ty_ide/src/type_hierarchy.rs @@ -221,7 +221,7 @@ mod tests { let supertypes = test.supertypes(); insta::assert_snapshot!( snapshot(&test.db, &supertypes), - @"vendored://stdlib/builtins.pyi:3650:3656 object :: builtins", + @"vendored://stdlib/builtins.pyi:3638:3644 object :: builtins", ); } @@ -435,12 +435,12 @@ mod tests { let item = test.prepare().unwrap(); insta::assert_snapshot!( snapshot(&test.db, &[item]), - @"vendored://stdlib/builtins.pyi:8550:8554 type :: builtins", + @"vendored://stdlib/builtins.pyi:8538:8542 type :: builtins", ); let supertypes = test.supertypes(); insta::assert_snapshot!( snapshot(&test.db, &supertypes), - @"vendored://stdlib/builtins.pyi:3650:3656 object :: builtins", + @"vendored://stdlib/builtins.pyi:3638:3644 object :: builtins", ); } @@ -492,7 +492,7 @@ mod tests { let supertypes = test.supertypes(); insta::assert_snapshot!( snapshot(&test.db, &supertypes), - @"vendored://stdlib/builtins.pyi:104722:104727 tuple :: builtins", + @"vendored://stdlib/builtins.pyi:104711:104716 tuple :: builtins", ); } diff --git a/crates/ty_vendored/vendor/typeshed/source_commit.txt b/crates/ty_vendored/vendor/typeshed/source_commit.txt index 0d7e1d20a2..5ba2971442 100644 --- a/crates/ty_vendored/vendor/typeshed/source_commit.txt +++ b/crates/ty_vendored/vendor/typeshed/source_commit.txt @@ -1 +1 @@ -b00c387c669cb50d5d388d77b74c2e832e147fe8 +1b116673774d062a4af7b0a0b3d05533a6be55d0 diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/__main__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/__main__.pyi index 5b0f74feb2..3536a6f021 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/__main__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/__main__.pyi @@ -1 +1 @@ -def __getattr__(name: str): ... # incomplete module +def __getattr__(name: str, /): ... # incomplete module diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_asyncio.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_asyncio.pyi index 87713067e5..9d05cede8f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_asyncio.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_asyncio.pyi @@ -133,7 +133,7 @@ else: # since the only reason why `asyncio.Future` is invariant is the `set_result()` method, # and `asyncio.Task.set_result()` always raises. @disjoint_base -class Task(Future[_T_co]): # type: ignore[type-var] # pyright: ignore[reportInvalidTypeArguments] +class Task(Future[_T_co]): # type: ignore[type-var] # pyright: ignore[reportInvalidTypeArguments] # ty:ignore[invalid-generic-class] """A coroutine wrapped in a Future.""" if sys.version_info >= (3, 12): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.pyi index 8f413c3035..108b7b7117 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_ctypes.pyi @@ -337,7 +337,7 @@ class _UnionType(_CTypeBaseType): # At runtime, various attributes are created on a Union subclass based # on its _fields_. This method doesn't exist, but represents those # dynamically created attributes. - def __getattr__(self, name: str) -> _CField[Any, Any, Any]: ... + def __getattr__(self, name: str, /) -> _CField[Any, Any, Any]: ... if sys.version_info < (3, 13): # Inherited from CType_Type starting on 3.13 def __mul__(cls: type[_CT], other: int) -> type[Array[_CT]]: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] @@ -353,8 +353,8 @@ class Union(_CData, metaclass=_UnionType): _align_: ClassVar[int] def __init__(self, *args: Any, **kw: Any) -> None: ... - def __getattr__(self, name: str) -> Any: ... - def __setattr__(self, name: str, value: Any) -> None: ... + def __getattr__(self, name: str, /) -> Any: ... + def __setattr__(self, name: str, value: Any, /) -> None: ... # This class is not exposed. It calls itself _ctypes.PyCStructType. @type_check_only @@ -367,7 +367,7 @@ class _PyCStructType(_CTypeBaseType): # At runtime, various attributes are created on a Structure subclass based # on its _fields_. This method doesn't exist, but represents those # dynamically created attributes. - def __getattr__(self, name: str) -> _CField[Any, Any, Any]: ... + def __getattr__(self, name: str, /) -> _CField[Any, Any, Any]: ... if sys.version_info < (3, 13): # Inherited from CType_Type starting on 3.13 def __mul__(cls: type[_CT], other: int) -> type[Array[_CT]]: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] @@ -387,7 +387,7 @@ class Structure(_CData, metaclass=_PyCStructType): _layout_: ClassVar[Literal["ms", "gcc-sysv"]] def __init__(self, *args: Any, **kw: Any) -> None: ... - def __getattr__(self, name: str) -> Any: ... + def __getattr__(self, name: str, /) -> Any: ... def __setattr__(self, name: str, value: Any) -> None: ... # This class is not exposed. It calls itself _ctypes.PyCArrayType. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_curses.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_curses.pyi index 2411012b11..77510b9b46 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_curses.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_curses.pyi @@ -891,10 +891,17 @@ def use_env(flag: bool, /) -> None: and COLUMNS are not set). """ -class error(Exception): ... +class error(Exception): + """Exception raised when a curses library function returns an error.""" @final class window: # undocumented + """A curses window. + + Window objects are returned by initscr() and newwin(), and by the + methods that create subwindows and pads. + """ + encoding: str """the typecode character used to create the array""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_curses_panel.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_curses_panel.pyi index b9b3f4b7b3..6d0c7a724f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_curses_panel.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_curses_panel.pyi @@ -4,10 +4,16 @@ from typing import Final, final __version__: Final[str] version: Final[str] -class error(Exception): ... +class error(Exception): + """Exception raised when a curses panel library function returns an error.""" @final class panel: + """A curses panel. + + Panel objects are returned by new_panel(). + """ + def above(self) -> panel: """Return the panel above the current panel.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_sitebuiltins.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_sitebuiltins.pyi index 98fa3d1ef8..811ab6bcc2 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_sitebuiltins.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_sitebuiltins.pyi @@ -4,13 +4,14 @@ The objects used by the site module to add custom builtins. import sys from collections.abc import Iterable -from typing import ClassVar, Literal, NoReturn +from typing import ClassVar, Literal +from typing_extensions import Never class Quitter: name: str eof: str def __init__(self, name: str, eof: str) -> None: ... - def __call__(self, code: sys._ExitCode = None) -> NoReturn: ... + def __call__(self, code: sys._ExitCode = None) -> Never: ... class _Printer: """interactive prompt objects for printing the license text, a list of diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_struct.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_struct.pyi index 8e275bbb90..c458b1bbb0 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_struct.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_struct.pyi @@ -44,8 +44,8 @@ def pack_into(fmt: str | bytes, buffer: WriteableBuffer, offset: int, /, *v: Any Pack the provided values according to the format string and write the packed bytes into the writable buffer starting at offset. Note that the - offset is a required argument. See help(struct) for more on format - strings. + offset is a required argument. A negative offset counts from the end of + the buffer. See help(struct) for more on format strings. """ def unpack(format: str | bytes, buffer: ReadableBuffer, /) -> tuple[Any, ...]: @@ -58,7 +58,8 @@ def unpack(format: str | bytes, buffer: ReadableBuffer, /) -> tuple[Any, ...]: def unpack_from(format: str | bytes, /, buffer: ReadableBuffer, offset: int = 0) -> tuple[Any, ...]: """Return a tuple containing values unpacked according to the format string. - The buffer's size, minus offset, must be at least calcsize(format). See + The buffer must contain at least calcsize(format) bytes starting at + offset. A negative offset counts from the end of the buffer. See help(struct) for more on format strings. """ @@ -103,8 +104,9 @@ class Struct: Pack the provided values according to the struct format string and write the packed bytes into the writable buffer starting at - offset. Note that the offset is a required argument. See - help(struct) for more on format strings. + offset. Note that the offset is a required argument. A negative + offset counts from the end of the buffer. See help(struct) for + more on format strings. """ def unpack(self, buffer: ReadableBuffer, /) -> tuple[Any, ...]: @@ -120,8 +122,8 @@ class Struct: Values are unpacked according to the struct format string. The buffer's size in bytes, starting at position offset, must be at - least the struct size. See help(struct) for more on format - strings. + least the struct size. A negative offset counts from the end of + the buffer. See help(struct) for more on format strings. """ def iter_unpack(self, buffer: ReadableBuffer, /) -> Iterator[tuple[Any, ...]]: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi index b053569b68..ba3a5b7841 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_thread.pyi @@ -8,8 +8,8 @@ from _typeshed import structseq from collections.abc import Callable from threading import Thread from types import TracebackType -from typing import Any, Final, NoReturn, final, overload -from typing_extensions import TypeVarTuple, Unpack, deprecated, disjoint_base +from typing import Any, Final, final, overload +from typing_extensions import Never, TypeVarTuple, Unpack, deprecated, disjoint_base _Ts = TypeVarTuple("_Ts") @@ -275,13 +275,13 @@ def interrupt_main(signum: signal.Signals = signal.SIGINT, /) -> None: Note: the default signal handler for SIGINT raises ``KeyboardInterrupt``. """ -def exit() -> NoReturn: +def exit() -> Never: """This is synonymous to ``raise SystemExit''. It will cause the current thread to exit silently unless the exception is caught. """ @deprecated("Obsolete synonym. Use `exit()` instead.") -def exit_thread() -> NoReturn: # undocumented +def exit_thread() -> Never: # undocumented """An obsolete synonym of exit().""" def allocate_lock() -> LockType: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi index 2f65d8aab9..5b2d7f7c54 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_typeshed/__init__.pyi @@ -58,6 +58,14 @@ Unused: TypeAlias = object # stable # for more information. MaybeNone: TypeAlias = Any # stable +# typeshed-internal type aliases to facilitate transition from +# `float` to either `float | int` or `float` (and similar for `complex`). +# When you encounter one of these type aliases, you are encouraged to +# replace them with the correct type. Please don't use them outside typeshed. +# See https://github.com/python/typeshed/issues/16059 for details. +FloatInt: TypeAlias = float | int +ComplexInt: TypeAlias = complex | float | int + # Used to mark arguments that default to a sentinel value. This prevents # stubtest from complaining about the default value not matching. # diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/_winapi.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/_winapi.pyi index 1a6ed3373c..dc5d8ad02d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/_winapi.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/_winapi.pyi @@ -1,7 +1,8 @@ import sys from _typeshed import ReadableBuffer from collections.abc import Sequence -from typing import Any, Final, Literal, NoReturn, final, overload +from typing import Any, Final, Literal, final, overload +from typing_extensions import Never if sys.platform == "win32": ABOVE_NORMAL_PRIORITY_CLASS: Final = 0x8000 @@ -267,7 +268,7 @@ if sys.platform == "win32": through both handles. """ - def ExitProcess(ExitCode: int, /) -> NoReturn: ... + def ExitProcess(ExitCode: int, /) -> Never: ... def GetACP() -> int: """Get the current Windows ANSI code page identifier.""" @@ -448,3 +449,7 @@ if sys.platform == "win32": """ def NeedCurrentDirectoryForExePath(exe_name: str, /) -> bool: ... + + if sys.version_info >= (3, 15): + def GetTickCount64() -> int: + """Number of milliseconds that have elapsed since the system was started.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/argparse.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/argparse.pyi index 98b1712b0b..3abc52ddbe 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/argparse.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/argparse.pyi @@ -65,8 +65,8 @@ import sys from _typeshed import SupportsWrite, sentinel from collections.abc import Callable, Generator, Iterable, Sequence from re import Pattern -from typing import IO, Any, ClassVar, Final, Generic, NoReturn, Protocol, TypeAlias, TypeVar, overload, type_check_only -from typing_extensions import Self, deprecated +from typing import IO, Any, ClassVar, Final, Generic, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Never, Self, deprecated __all__ = [ "ArgumentParser", @@ -184,7 +184,7 @@ class _ActionsContainer: conflict_handler: str = ..., ) -> _ArgumentGroup: ... @overload - @deprecated("The `prefix_chars` parameter deprecated since Python 3.14.") + @deprecated("The `prefix_chars` parameter is deprecated.") def add_argument_group( self, title: str | None = None, @@ -204,7 +204,7 @@ class _ActionsContainer: def _pop_action_class(self, kwargs: Any, default: type[Action] | None = None) -> type[Action]: ... def _get_handler(self) -> Callable[[Action, Iterable[tuple[str, Action]]], Any]: ... def _check_conflict(self, action: Action) -> None: ... - def _handle_conflict_error(self, action: Action, conflicting_actions: Iterable[tuple[str, Action]]) -> NoReturn: ... + def _handle_conflict_error(self, action: Action, conflicting_actions: Iterable[tuple[str, Action]]) -> Never: ... def _handle_conflict_resolve(self, action: Action, conflicting_actions: Iterable[tuple[str, Action]]) -> None: ... @type_check_only @@ -369,8 +369,8 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): def parse_known_args(self, *, namespace: _N) -> tuple[_N, list[str]]: ... def convert_arg_line_to_args(self, arg_line: str) -> list[str]: ... - def exit(self, status: int = 0, message: str | None = None) -> NoReturn: ... - def error(self, message: str) -> NoReturn: + def exit(self, status: int = 0, message: str | None = None) -> Never: ... + def error(self, message: str) -> Never: """error(message: string) Prints a usage message incorporating the message to stderr and @@ -729,7 +729,7 @@ class Namespace(_AttributeHolder): def __eq__(self, other: object) -> bool: ... __hash__: ClassVar[None] # type: ignore[assignment] -@deprecated("Deprecated since Python 3.14. Open files after parsing arguments instead.") +@deprecated("Deprecated; may leave files open. Open files after parsing arguments instead.") class FileType: """Deprecated factory for creating file object types @@ -771,7 +771,7 @@ class _ArgumentGroup(_ActionsContainer): conflict_handler: str = ..., ) -> None: ... @overload - @deprecated("Undocumented `prefix_chars` parameter is deprecated since Python 3.14.") + @deprecated("Undocumented `prefix_chars` parameter is deprecated.") def __init__( self, container: _ActionsContainer, diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi index 2b08e73098..8d4750b55f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/coroutines.pyi @@ -27,17 +27,17 @@ def iscoroutine(obj: object) -> TypeIs[Coroutine[object, Never, object]]: if sys.version_info >= (3, 11): @overload - @deprecated("Deprecated since Python 3.14. Use `inspect.iscoroutinefunction()` instead.") + @deprecated("Deprecated; will be removed in Python 3.16. Use `inspect.iscoroutinefunction()` instead.") def iscoroutinefunction(func: Callable[..., Coroutine[Any, Any, Any]]) -> bool: """Return True if func is a decorated coroutine function.""" @overload - @deprecated("Deprecated since Python 3.14. Use `inspect.iscoroutinefunction()` instead.") + @deprecated("Deprecated; will be removed in Python 3.16. Use `inspect.iscoroutinefunction()` instead.") def iscoroutinefunction(func: Callable[_P, Awaitable[_T]]) -> TypeGuard[Callable[_P, Coroutine[Any, Any, _T]]]: ... @overload - @deprecated("Deprecated since Python 3.14. Use `inspect.iscoroutinefunction()` instead.") + @deprecated("Deprecated; will be removed in Python 3.16. Use `inspect.iscoroutinefunction()` instead.") def iscoroutinefunction(func: Callable[_P, object]) -> TypeGuard[Callable[_P, Coroutine[Any, Any, Any]]]: ... @overload - @deprecated("Deprecated since Python 3.14. Use `inspect.iscoroutinefunction()` instead.") + @deprecated("Deprecated; will be removed in Python 3.16. Use `inspect.iscoroutinefunction()` instead.") def iscoroutinefunction(func: object) -> TypeGuard[Callable[..., Coroutine[Any, Any, Any]]]: ... else: # Sometimes needed in Python < 3.11 due to the fact that it supports @coroutine diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.pyi index 83b261fbf5..4d901d2741 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/events.pyi @@ -993,10 +993,10 @@ else: def new_event_loop(self) -> AbstractEventLoop: ... # Child processes handling (Unix only). @abstractmethod - @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") + @deprecated("Deprecated; removed in Python 3.14.") def get_child_watcher(self) -> AbstractChildWatcher: ... @abstractmethod - @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") + @deprecated("Deprecated; removed in Python 3.14.") def set_child_watcher(self, watcher: AbstractChildWatcher) -> None: ... AbstractEventLoopPolicy = _AbstractEventLoopPolicy @@ -1071,11 +1071,11 @@ if sys.version_info >= (3, 14): If policy is None, the default policy is restored. """ -@deprecated("Deprecated since Python 3.14; will be removed in Python 3.16.") +@deprecated("Deprecated; will be removed in Python 3.16.") def get_event_loop_policy() -> _AbstractEventLoopPolicy: """Get the current event loop policy.""" -@deprecated("Deprecated since Python 3.14; will be removed in Python 3.16.") +@deprecated("Deprecated; will be removed in Python 3.16.") def set_event_loop_policy(policy: _AbstractEventLoopPolicy | None) -> None: """Set the current event loop policy. @@ -1089,11 +1089,11 @@ def new_event_loop() -> AbstractEventLoop: """Equivalent to calling get_event_loop_policy().new_event_loop().""" if sys.version_info < (3, 14): - @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") + @deprecated("Deprecated; removed in Python 3.14.") def get_child_watcher() -> AbstractChildWatcher: """Equivalent to calling get_event_loop_policy().get_child_watcher().""" - @deprecated("Deprecated since Python 3.12; removed in Python 3.14.") + @deprecated("Deprecated; removed in Python 3.14.") def set_child_watcher(watcher: AbstractChildWatcher) -> None: """Equivalent to calling get_event_loop_policy().set_child_watcher(watcher). diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/trsock.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/trsock.pyi index df65fcc42d..86e87c21c8 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/trsock.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/trsock.pyi @@ -4,8 +4,8 @@ from _typeshed import ReadableBuffer from builtins import type as Type # alias to avoid name clashes with property named "type" from collections.abc import Iterable from types import TracebackType -from typing import Any, BinaryIO, NoReturn, TypeAlias, overload -from typing_extensions import deprecated +from typing import Any, BinaryIO, TypeAlias, overload +from typing_extensions import Never, deprecated # These are based in socket, maybe move them out into _typeshed.pyi or such _Address: TypeAlias = socket._Address @@ -29,7 +29,7 @@ class TransportSocket: def type(self) -> int: ... @property def proto(self) -> int: ... - def __getstate__(self) -> NoReturn: ... + def __getstate__(self) -> Never: ... def fileno(self) -> int: ... def dup(self) -> socket.socket: ... def get_inheritable(self) -> bool: ... @@ -47,7 +47,7 @@ class TransportSocket: def getpeername(self) -> _RetAddress: ... def getsockname(self) -> _RetAddress: ... - def getsockbyname(self) -> NoReturn: ... # This method doesn't exist on socket, yet is passed through? + def getsockbyname(self) -> Never: ... # This method doesn't exist on socket, yet is passed through? def settimeout(self, value: float | None) -> None: ... def gettimeout(self) -> float | None: ... def setblocking(self, flag: bool) -> None: ... @@ -67,7 +67,7 @@ class TransportSocket: def ioctl(self, control: int, option: int | tuple[int, int, int] | bool) -> None: ... else: @deprecated("Removed in Python 3.11") - def ioctl(self, control: int, option: int | tuple[int, int, int] | bool) -> NoReturn: ... + def ioctl(self, control: int, option: int | tuple[int, int, int] | bool) -> Never: ... @deprecated("Removed in Python 3.11") def listen(self, backlog: int = ..., /) -> None: ... @@ -89,7 +89,7 @@ class TransportSocket: @deprecated("Removed in Python 3.11.") def sendmsg_afalg( self, msg: Iterable[ReadableBuffer] = ..., *, op: int, iv: Any = ..., assoclen: int = ..., flags: int = 0 - ) -> NoReturn: ... + ) -> Never: ... @deprecated("Removed in Python 3.11.") def sendmsg( @@ -120,7 +120,7 @@ class TransportSocket: def share(self, process_id: int) -> bytes: ... else: @deprecated("Removed in Python 3.11.") - def share(self, process_id: int) -> NoReturn: ... + def share(self, process_id: int) -> Never: ... @deprecated("Removed in Python 3.11.") def recv_into(self, buffer: _WriteBuffer, nbytes: int = 0, flags: int = 0) -> int: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi index 99b9ec9565..8885192bcb 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/asyncio/windows_events.pyi @@ -4,7 +4,8 @@ import socket import sys from _typeshed import Incomplete, ReadableBuffer, WriteableBuffer from collections.abc import Callable -from typing import IO, Any, ClassVar, Final, NoReturn +from typing import IO, Any, ClassVar, Final +from typing_extensions import Never from . import events, futures, proactor_events, selector_events, streams, windows_utils @@ -123,18 +124,18 @@ if sys.platform == "win32": else: class WindowsSelectorEventLoopPolicy(events.BaseDefaultEventLoopPolicy): _loop_factory: ClassVar[type[SelectorEventLoop]] - def get_child_watcher(self) -> NoReturn: + def get_child_watcher(self) -> Never: """Get the watcher for child processes.""" - def set_child_watcher(self, watcher: Any) -> NoReturn: + def set_child_watcher(self, watcher: Any) -> Never: """Set the watcher for child processes.""" class WindowsProactorEventLoopPolicy(events.BaseDefaultEventLoopPolicy): _loop_factory: ClassVar[type[ProactorEventLoop]] - def get_child_watcher(self) -> NoReturn: + def get_child_watcher(self) -> Never: """Get the watcher for child processes.""" - def set_child_watcher(self, watcher: Any) -> NoReturn: + def set_child_watcher(self, watcher: Any) -> Never: """Set the watcher for child processes.""" if sys.version_info >= (3, 14): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi index d1691cea5e..867b68b551 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/builtins.pyi @@ -42,7 +42,7 @@ from _typeshed import ( SupportsRichComparisonT, SupportsWrite, ) -from collections.abc import Awaitable, Callable, Iterable, Iterator, MutableSet, Reversible, Set as AbstractSet, Sized +from collections.abc import Awaitable, Callable, Iterable, Iterator, MutableSet, Set as AbstractSet, Sized from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper from os import PathLike from types import CellType, CodeType, EllipsisType, GenericAlias, NotImplementedType, TracebackType, UnionType @@ -2830,6 +2830,7 @@ class slice(Generic[_StartT_co, _StopT_co, _StepT_co]): def __eq__(self, value: object, /) -> bool: ... if sys.version_info >= (3, 12): def __hash__(self) -> int: ... + else: __hash__: ClassVar[None] # type: ignore[assignment] @@ -2947,6 +2948,7 @@ class function: closure: tuple[CellType, ...] | None = None, kwdefaults: dict[str, object] | None = None, ) -> Self: ... + else: def __new__( cls, @@ -3206,6 +3208,7 @@ class dict(MutableMapping[_KT, _VT]): """Return value|self.""" @overload def __ror__(self, value: frozendict[_T1, _T2], /) -> frozendict[_KT | _T1, _VT | _T2]: ... + else: def __or__(self, value: dict[_T1, _T2], /) -> dict[_KT | _T1, _VT | _T2]: """Return self|value.""" @@ -4599,7 +4602,7 @@ _SupportsSomeKindOfPow = ( # noqa: Y026 # TODO: Use TypeAlias once mypy bugs a ) # TODO: `pow(int, int, Literal[0])` fails at runtime, -# but adding a `NoReturn` overload isn't a good solution for expressing that (see #8566). +# but adding a `Never` overload isn't a good solution for expressing that (see #8566). @overload def pow(base: int, exp: int, mod: int) -> int: """Equivalent to base**exp with 2 arguments or base**exp % mod with 3 arguments @@ -4645,19 +4648,23 @@ def pow(base: _SupportsSomeKindOfPow, exp: complex, mod: None = None) -> complex quit: _sitebuiltins.Quitter +@type_check_only +class _SupportsReversed(Protocol[_T_co]): + def __reversed__(self) -> _T_co: ... + @disjoint_base -class reversed(Generic[_T]): +class reversed(Generic[_T_co]): """Return a reverse iterator over the values of the given sequence.""" @overload - def __new__(cls, sequence: Reversible[_T], /) -> Iterator[_T]: ... # type: ignore[misc] + def __new__(cls, sequence: _SupportsReversed[_T], /) -> _T: ... # type: ignore[misc] @overload - def __new__(cls, sequence: SupportsLenAndGetItem[_T], /) -> Iterator[_T]: ... # type: ignore[misc] + def __new__(cls, sequence: SupportsLenAndGetItem[_T_co], /) -> Self: ... def __iter__(self) -> Self: """Implement iter(self).""" - def __next__(self) -> _T: + def __next__(self) -> _T_co: """Implement next(self).""" def __length_hint__(self) -> int: @@ -4895,7 +4902,7 @@ class BaseException: __suppress_context__: bool __traceback__: TracebackType | None def __init__(self, *args: object) -> None: ... - def __new__(cls, *args: Any, **kwds: Any) -> Self: ... + def __new__(cls, /, *args: Any, **kwds: Any) -> Self: ... def __setstate__(self, state: dict[str, Any] | None, /) -> None: ... def with_traceback(self, tb: TracebackType | None, /) -> Self: """Set self.__traceback__ to tb and return self.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/codecs.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/codecs.pyi index c84faeac6f..d21bb6f716 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/codecs.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/codecs.pyi @@ -248,7 +248,7 @@ def getwriter(encoding: str) -> _StreamWriter: """ -@deprecated("Deprecated since Python 3.14. Use `open()` instead.") +@deprecated("Deprecated. Use `open()` instead.") def open( filename: str, mode: str = "r", encoding: str | None = None, errors: str = "strict", buffering: int = -1 ) -> StreamReaderWriter: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.pyi index 69e52ce4d6..9b6589d6db 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/collections/__init__.pyi @@ -30,8 +30,8 @@ from collections.abc import ( ValuesView, ) from types import GenericAlias -from typing import Any, ClassVar, Generic, NoReturn, SupportsIndex, TypeVar, final, overload, type_check_only -from typing_extensions import Self, disjoint_base +from typing import Any, ClassVar, Generic, SupportsIndex, TypeVar, final, overload, type_check_only +from typing_extensions import Never, Self, disjoint_base if sys.version_info >= (3, 15): from builtins import frozendict @@ -367,6 +367,9 @@ class deque(MutableSequence[_T]): def __mul__(self, value: int, /) -> Self: """Return self*value.""" + def __rmul__(self, value: int, /) -> Self: + """Return value*self.""" + def __imul__(self, value: int, /) -> Self: """Implement self*=value.""" @@ -474,7 +477,7 @@ class Counter(dict[_T, int], Generic[_T]): """ @classmethod - def fromkeys(cls, iterable: Any, v: int | None = None) -> NoReturn: ... # type: ignore[override] + def fromkeys(cls, iterable: Any, v: int | None = None) -> Never: ... # type: ignore[override] @overload def subtract(self, iterable: None = None, /) -> None: @@ -677,17 +680,17 @@ class _OrderedDictValuesView(ValuesView[_VT_co]): # pyright doesn't have a specific error code for subclassing error! @final @type_check_only -class _odict_keys(dict_keys[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] +class _odict_keys(dict_keys[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[subclass-of-final-class] def __reversed__(self) -> Iterator[_KT_co]: ... @final @type_check_only -class _odict_items(dict_items[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] +class _odict_items(dict_items[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[subclass-of-final-class] def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ... @final @type_check_only -class _odict_values(dict_values[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] +class _odict_values(dict_values[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[subclass-of-final-class] def __reversed__(self) -> Iterator[_VT_co]: ... @disjoint_base diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi index 578715dce0..4625adfcf7 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/concurrent/futures/process.pyi @@ -237,7 +237,12 @@ class _ExecutorManagerThread(Thread): def wait_result_broken_or_wakeup(self) -> tuple[Any, bool, str]: ... def process_result_item(self, result_item: int | _ResultItem) -> None: ... def is_shutting_down(self) -> bool: ... - def terminate_broken(self, cause: str) -> None: ... + + if sys.version_info >= (3, 15): + def terminate_broken(self, cause: str, bpe_message: str | None = None) -> None: ... + else: + def terminate_broken(self, cause: str) -> None: ... + def flag_executor_shutting_down(self) -> None: ... def shutdown_workers(self) -> None: ... def join_executor_internals(self) -> None: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi index dcab0730e7..0272813d77 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/contextlib.pyi @@ -45,7 +45,7 @@ _CM_EF = TypeVar("_CM_EF", bound=AbstractContextManager[Any, Any] | _ExitFunc) # At runtime it inherits from ABC and is not a Protocol, but it is on the # allowlist for use as a Protocol. @runtime_checkable -class AbstractContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] +class AbstractContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-protocol] """An abstract base class for context managers.""" __slots__ = () @@ -62,7 +62,7 @@ class AbstractContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ignore[m # At runtime it inherits from ABC and is not a Protocol, but it is on the # allowlist for use as a Protocol. @runtime_checkable -class AbstractAsyncContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] +class AbstractAsyncContextManager(ABC, Protocol[_T_co, _ExitT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-protocol] """An abstract base class for asynchronous context managers.""" __slots__ = () diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi index 58ba93a524..975612e6b0 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ctypes/__init__.pyi @@ -236,10 +236,10 @@ def create_unicode_buffer(init: int | str, size: int | None = None) -> Array[c_w """ if sys.version_info < (3, 15): - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") + @deprecated("Deprecated; will be removed in Python 3.15.") def SetPointerType(pointer: type[_Pointer[Any]], cls: _CTypeBaseType) -> None: ... -@deprecated("Soft deprecated since Python 3.13. Use multiplication instead.") +@deprecated("Soft deprecated. Use multiplication instead.") def ARRAY(typ: _CT, len: int) -> Array[_CT]: ... if sys.platform == "win32": diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi index 18e9890a09..d46b694a7e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi @@ -402,7 +402,7 @@ def fields(class_or_instance: DataclassInstance | type[DataclassInstance]) -> tu # HACK: `obj: Never` typing matches if object argument is using `Any` type. @overload -def is_dataclass(obj: Never) -> TypeIs[DataclassInstance | type[DataclassInstance]]: # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues] +def is_dataclass(obj: Never) -> TypeIs[DataclassInstance | type[DataclassInstance]]: # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-type-guard-definition] """Returns True if obj is a dataclass or an instance of a dataclass. """ @@ -415,13 +415,17 @@ class FrozenInstanceError(AttributeError): ... class InitVar(Generic[_T]): __slots__ = ("type",) - type: Type[_T] + type: Type[_T] # ty:ignore[unbound-type-variable] def __init__(self, type: Type[_T]) -> None: ... @overload - def __class_getitem__(cls, type: Type[_T]) -> InitVar[_T]: ... # pyright: ignore[reportInvalidTypeForm] + def __class_getitem__( + cls, type: Type[_T] + ) -> InitVar[_T]: ... # pyright: ignore[reportInvalidTypeForm] # ty:ignore[invalid-type-form] @overload - def __class_getitem__(cls, type: Any) -> InitVar[Any]: ... # pyright: ignore[reportInvalidTypeForm] + def __class_getitem__( + cls, type: Any + ) -> InitVar[Any]: ... # pyright: ignore[reportInvalidTypeForm] # ty:ignore[invalid-type-form] if sys.version_info >= (3, 14): def make_dataclass( diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/datetime.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/datetime.pyi index 0820482cf8..dc9f7bff39 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/datetime.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/datetime.pyi @@ -7,8 +7,8 @@ time zone and DST data sources. import sys from abc import abstractmethod from time import struct_time -from typing import ClassVar, Final, NoReturn, SupportsIndex, TypeAlias, final, overload, type_check_only -from typing_extensions import CapsuleType, Self, deprecated, disjoint_base +from typing import ClassVar, Final, SupportsIndex, TypeAlias, final, overload, type_check_only +from typing_extensions import CapsuleType, Never, Self, deprecated, disjoint_base if sys.version_info >= (3, 11): __all__ = ("date", "datetime", "time", "timedelta", "timezone", "tzinfo", "MINYEAR", "MAXYEAR", "UTC") @@ -195,7 +195,7 @@ class date: """Return value+self.""" @overload - def __sub__(self, value: datetime, /) -> NoReturn: + def __sub__(self, value: datetime, /) -> Never: """Return self-value.""" @overload def __sub__(self, value: Self, /) -> timedelta: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/decimal.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/decimal.pyi index 9f8de2a7f5..d43b2ac9de 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/decimal.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/decimal.pyi @@ -734,7 +734,7 @@ class Context: # even settable attributes like `prec` and `rounding`, # but that's inexpressible in the stub. # Type checkers either ignore it or misinterpret it - # if you add a `def __delattr__(self, name: str, /) -> NoReturn` method to the stub + # if you add a `def __delattr__(self, name: str, /) -> Never` method to the stub prec: int rounding: str Emin: int diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/glob.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/glob.pyi index be4b9dedbc..1ee7ae3024 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/glob.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/glob.pyi @@ -49,8 +49,8 @@ if sys.version_info >= (3, 11): If `dir_fd` is not None, it should be a file descriptor referring to a directory, and paths will then be relative to that directory. - If `include_hidden` is true, the patterns '*', '?', '**' will match - hidden directories. + If `include_hidden` is true, wildcards can match path segments beginning + with a dot ('.'). If `recursive` is true, the pattern '**' will match any files and zero or more directories and subdirectories. @@ -83,8 +83,8 @@ if sys.version_info >= (3, 11): If `dir_fd` is not None, it should be a file descriptor referring to a directory, and paths will then be relative to that directory. - If `include_hidden` is true, the patterns '*', '?', '**' will match - hidden directories. + If `include_hidden` is true, wildcards can match path segments beginning + with a dot ('.'). If `recursive` is true, the pattern '**' will match any files and zero or more directories and subdirectories. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/http/cookiejar.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/http/cookiejar.pyi index 6aa11d0c37..c2241c182e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/http/cookiejar.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/http/cookiejar.pyi @@ -321,7 +321,7 @@ class Cookie: domain_initial_dot: bool def __init__( self, - version: int | None, + version: int | str | None, name: str, value: str | None, # undocumented port: str | None, @@ -332,7 +332,7 @@ class Cookie: path: str, path_specified: bool, secure: bool, - expires: int | None, + expires: float | str | None, discard: bool, comment: str | None, comment_url: str | None, diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/http/server.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/http/server.pyi index 083efad72e..0fcf0c053f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/http/server.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/http/server.pyi @@ -422,7 +422,7 @@ def executable(path: StrPath) -> bool: # undocumented """Test for executable file.""" if sys.version_info < (3, 15): - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") + @deprecated("Deprecated and unsafe; will be removed in Python 3.15.") class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): """Complete HTTP server with GET, HEAD and POST commands. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/imaplib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/imaplib.pyi index 05b584d279..0ae365b25e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/imaplib.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/imaplib.pyi @@ -1,6 +1,6 @@ """IMAP4 client. -Based on RFC 2060. +Based on RFC 3501. Public class: IMAP4 Public variable: Debug @@ -292,13 +292,23 @@ class IMAP4: Note: 'duration' requires a socket connection (not IMAP4_stream). """ - def list(self, directory: str = '""', pattern: str = "*") -> tuple[str, _AnyResponseData]: - """List mailbox names in directory matching pattern. + if sys.version_info >= (3, 15): + def list(self, directory: str = "", pattern: str = "*") -> tuple[str, _AnyResponseData]: + """List mailbox names in directory matching pattern. - (typ, [data]) = .list(directory='""', pattern='*') + (typ, [data]) = .list(directory='', pattern='*') - 'data' is list of LIST responses. - """ + 'data' is list of LIST responses. + """ + + else: + def list(self, directory: str = '""', pattern: str = "*") -> tuple[str, _AnyResponseData]: + """List mailbox names in directory matching pattern. + + (typ, [data]) = .list(directory='""', pattern='*') + + 'data' is list of LIST responses. + """ def login(self, user: str, password: str) -> tuple[Literal["OK"], _list[bytes]]: """Identify client using plaintext password. @@ -322,13 +332,23 @@ class IMAP4: Returns server 'BYE' response. """ - def lsub(self, directory: str = '""', pattern: str = "*") -> _CommandResults: - """List 'subscribed' mailbox names in directory matching pattern. + if sys.version_info >= (3, 15): + def lsub(self, directory: str = "", pattern: str = "*") -> _CommandResults: + """List 'subscribed' mailbox names in directory matching pattern. - (typ, [data, ...]) = .lsub(directory='""', pattern='*') + (typ, [data, ...]) = .lsub(directory='', pattern='*') - 'data' are tuples of message part envelope and data. - """ + 'data' are tuples of message part envelope and data. + """ + + else: + def lsub(self, directory: str = '""', pattern: str = "*") -> _CommandResults: + """List 'subscribed' mailbox names in directory matching pattern. + + (typ, [data, ...]) = .lsub(directory='""', pattern='*') + + 'data' are tuples of message part envelope and data. + """ def myrights(self, mailbox: str) -> _CommandResults: """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox). @@ -399,10 +419,17 @@ class IMAP4: (typ, [data]) = .setacl(mailbox, who, what) """ - def setannotation(self, *args: str) -> _CommandResults: - """(typ, [data]) = .setannotation(mailbox[, entry, attribute]+) - Set ANNOTATIONs. - """ + if sys.version_info >= (3, 15): + def setannotation(self, mailbox: str | bytes, *args: str) -> _CommandResults: + """(typ, [data]) = .setannotation(mailbox[, entry, attribute]+) + Set ANNOTATIONs. + """ + + else: + def setannotation(self, *args: str) -> _CommandResults: + """(typ, [data]) = .setannotation(mailbox[, entry, attribute]+) + Set ANNOTATIONs. + """ def setquota(self, root: str, limits: str) -> _CommandResults: """Set the quota root's resource limits. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/readers.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/readers.pyi index 629da34418..abd4309983 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/readers.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/readers.pyi @@ -16,7 +16,7 @@ from _typeshed import StrPath from collections.abc import Iterable, Iterator from importlib._bootstrap_external import FileLoader from io import BufferedReader -from typing import Literal, NoReturn, TypeVar +from typing import Literal, TypeVar from typing_extensions import Never from zipimport import zipimporter @@ -68,8 +68,8 @@ class MultiplexedPath(abc.Traversable): def __init__(self, *paths: abc.Traversable) -> None: ... def iterdir(self) -> Iterator[abc.Traversable]: ... - def read_bytes(self) -> NoReturn: ... - def read_text(self, *args: Never, **kwargs: Never) -> NoReturn: ... # type: ignore[override] + def read_bytes(self) -> Never: ... + def read_text(self, *args: Never, **kwargs: Never) -> Never: ... # type: ignore[override] def is_dir(self) -> Literal[True]: ... def is_file(self) -> Literal[False]: ... @@ -83,7 +83,7 @@ class MultiplexedPath(abc.Traversable): if sys.version_info < (3, 12): __truediv__ = joinpath - def open(self, *args: Never, **kwargs: Never) -> NoReturn: ... # type: ignore[override] + def open(self, *args: Never, **kwargs: Never) -> Never: ... # type: ignore[override] @property def name(self) -> str: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi index a10498d77c..65d360fb03 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/__init__.pyi @@ -97,7 +97,7 @@ else: Directories are *not* resources. """ - @deprecated("Deprecated since Python 3.11. Use `files(anchor).iterdir()`.") + @deprecated("Deprecated; limited resource support. Use `files(anchor).iterdir()`.") def contents(package: Package) -> Iterator[str]: """Return an iterable of entries in `package`. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi index 2b51af4b66..a372168628 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/abc.pyi @@ -7,7 +7,7 @@ from typing import IO, Any, Literal, Protocol, overload, runtime_checkable from typing_extensions import deprecated if sys.version_info >= (3, 11): - @deprecated("Deprecated since Python 3.12. Use `importlib.resources.abc.TraversableResources` instead.") + @deprecated("Deprecated. Use `importlib.resources.abc.TraversableResources` instead.") class ResourceReader(metaclass=ABCMeta): """Abstract base class for loaders to provide resource reading support.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/simple.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/simple.pyi index b2b1af1216..89e912fcf6 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/simple.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/importlib/resources/simple.pyi @@ -7,7 +7,7 @@ import sys from _typeshed import StrPath from collections.abc import Iterator from io import TextIOWrapper -from typing import IO, Any, BinaryIO, Literal, NoReturn, overload +from typing import IO, Any, BinaryIO, Literal, overload from typing_extensions import Never if sys.version_info >= (3, 11): @@ -73,7 +73,7 @@ if sys.version_info >= (3, 11): @overload def open(self, mode: str) -> IO[Any]: ... - def joinpath(self, name: Never) -> NoReturn: ... # type: ignore[override] + def joinpath(self, name: Never) -> Never: ... # type: ignore[override] class ResourceContainer(Traversable, metaclass=abc.ABCMeta): """ @@ -85,7 +85,7 @@ if sys.version_info >= (3, 11): def is_dir(self) -> Literal[True]: ... def is_file(self) -> Literal[False]: ... def iterdir(self) -> Iterator[ResourceHandle | ResourceContainer]: ... - def open(self, *args: Never, **kwargs: Never) -> NoReturn: ... # type: ignore[override] + def open(self, *args: Never, **kwargs: Never) -> Never: ... # type: ignore[override] if sys.version_info < (3, 12): def joinpath(self, *descendants: StrPath) -> Traversable: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi index 4b951e1489..e0e2b8593a 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pgen2/pgen.pyi @@ -1,6 +1,7 @@ from _typeshed import Incomplete, StrPath from collections.abc import Iterable, Iterator -from typing import IO, ClassVar, NoReturn, overload +from typing import IO, ClassVar, overload +from typing_extensions import Never from . import grammar from .tokenize import _TokenInfo @@ -31,9 +32,9 @@ class ParserGenerator: def gettoken(self) -> None: ... @overload - def raise_error(self, msg: object) -> NoReturn: ... + def raise_error(self, msg: object) -> Never: ... @overload - def raise_error(self, msg: str, *args: object) -> NoReturn: ... + def raise_error(self, msg: str, *args: object) -> Never: ... class NFAState: arcs: list[tuple[str | None, NFAState]] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pytree.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pytree.pyi index f31847b545..94917780b2 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pytree.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/pytree.pyi @@ -7,7 +7,7 @@ even the comments and whitespace between tokens. There's also a pattern matching implementation here. """ -from _typeshed import Incomplete, SupportsGetItem, SupportsLenAndGetItem, Unused +from _typeshed import SupportsGetItem, SupportsLenAndGetItem, Unused from abc import abstractmethod from collections.abc import Iterable, Iterator, MutableSequence from typing import ClassVar, Final, TypeAlias @@ -124,7 +124,7 @@ class Node(Base): fixers_applied: MutableSequence[BaseFix] | None # Is Unbound until set in refactor.RefactoringTool - future_features: frozenset[Incomplete] + future_features: frozenset[str] # Is Unbound until set in pgen2.parse.Parser.pop used_names: set[str] def __init__( diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/refactor.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/refactor.pyi index 75f07a3e5a..84665431af 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/refactor.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/lib2to3/refactor.pyi @@ -10,7 +10,8 @@ from collections.abc import Container, Generator, Iterable, Mapping from logging import Logger, _ExcInfoType from multiprocessing import JoinableQueue from multiprocessing.synchronize import Lock -from typing import Any, ClassVar, Final, NoReturn, overload +from typing import Any, ClassVar, Final, overload +from typing_extensions import Never from .btm_matcher import BottomMatcher from .fixer_base import BaseFix @@ -68,7 +69,7 @@ class RefactoringTool: post-order traversal. """ - def log_error(self, msg: str, *args: Iterable[str], **kwargs: _ExcInfoType) -> NoReturn: + def log_error(self, msg: str, *args: Iterable[str], **kwargs: _ExcInfoType) -> Never: """Called when an error occurs.""" @overload diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/locale.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/locale.pyi index 85f8664659..595c4f7ba8 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/locale.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/locale.pyi @@ -211,7 +211,7 @@ def normalize(localename: _str) -> _str: """ if sys.version_info < (3, 13): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13. Use `locale.setlocale(locale.LC_ALL, '')` instead.") + @deprecated("Deprecated; removed in Python 3.13. Use `locale.setlocale(locale.LC_ALL, '')` instead.") def resetlocale(category: int = ...) -> None: """Sets the locale for category to the default setting. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi index 65082bd82f..4e0853db45 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/logging/__init__.pyi @@ -496,7 +496,7 @@ class Handler(Filterer): level: int # undocumented formatter: Formatter | None # undocumented - lock: threading.Lock | None # undocumented + lock: threading.RLock | None # undocumented name: str | None # undocumented def __init__(self, level: _Level = 0) -> None: """ diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/math/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/math/__init__.pyi index d733d625fd..3012e3ed50 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/math/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/math/__init__.pyi @@ -250,7 +250,7 @@ def ldexp(x: _SupportsFloatOrIndex, i: int, /) -> float: def lgamma(x: _SupportsFloatOrIndex, /) -> float: """Natural logarithm of absolute value of Gamma function at x.""" -def log(x: _SupportsFloatOrIndex, base: _SupportsFloatOrIndex = ...) -> float: +def log(x: _SupportsFloatOrIndex, base: _SupportsFloatOrIndex = ..., /) -> float: """log(x, [base=math.e]) Return the logarithm of x to the given base. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/mmap.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/mmap.pyi index be6ad5f32c..ca6d2859a3 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/mmap.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/mmap.pyi @@ -2,8 +2,8 @@ import os import sys from _typeshed import ReadableBuffer, Unused from collections.abc import Iterator -from typing import Final, Literal, NoReturn, SupportsIndex, overload -from typing_extensions import Self, disjoint_base +from typing import Final, Literal, SupportsIndex, overload +from typing_extensions import Never, Self, disjoint_base ACCESS_DEFAULT: Final = 0 ACCESS_READ: Final = 1 @@ -139,7 +139,7 @@ class mmap: @overload def __getitem__(self, key: slice[SupportsIndex | None], /) -> bytes: ... - def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> NoReturn: + def __delitem__(self, key: SupportsIndex | slice[SupportsIndex | None], /) -> Never: """Delete self[key].""" @overload diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.pyi index d4b405903b..2f54e7353e 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/nturl2path.pyi @@ -6,13 +6,13 @@ for urllib.requests, thus do not use directly. from typing_extensions import deprecated -@deprecated("The `nturl2path` module is deprecated since Python 3.14.") +@deprecated("Deprecated; use `urllib.request` file-URL helpers instead.") def url2pathname(url: str) -> str: """OS-specific conversion from a relative URL of the 'file' scheme to a file system path; not recommended for general use. """ -@deprecated("The `nturl2path` module is deprecated since Python 3.14.") +@deprecated("Deprecated; use `urllib.request` file-URL helpers instead.") def pathname2url(p: str) -> str: """OS-specific conversion from a file system path to a relative URL of the 'file' scheme; not recommended for general use. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/optparse.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/optparse.pyi index 13e69c977d..7c53865911 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/optparse.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/optparse.pyi @@ -25,8 +25,8 @@ import builtins from _typeshed import MaybeNone, SupportsWrite from abc import abstractmethod from collections.abc import Callable, Iterable, Mapping, Sequence -from typing import Any, ClassVar, Final, Literal, NoReturn, overload -from typing_extensions import Self +from typing import Any, ClassVar, Final, Literal, overload +from typing_extensions import Never, Self __all__ = [ "Option", @@ -558,7 +558,7 @@ class OptionParser(OptionContainer): allow_interspersed_args. """ - def error(self, msg: str) -> NoReturn: + def error(self, msg: str) -> Never: """error(msg : string) Print a usage message incorporating 'msg' to stderr and exit. @@ -566,7 +566,7 @@ class OptionParser(OptionContainer): should either exit or raise an exception. """ - def exit(self, status: int = 0, msg: str | None = None) -> NoReturn: ... + def exit(self, status: int = 0, msg: str | None = None) -> Never: ... def expand_prog_name(self, s: str) -> str: ... def format_epilog(self, formatter: HelpFormatter) -> str: ... def format_help(self, formatter: HelpFormatter | None = None) -> str: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi index d20fba3a5e..e0bba440c9 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/os/__init__.pyi @@ -56,7 +56,6 @@ from typing import ( Final, Generic, Literal, - NoReturn, Protocol, TypeAlias, TypeVar, @@ -65,7 +64,7 @@ from typing import ( runtime_checkable, type_check_only, ) -from typing_extensions import LiteralString, Self, Unpack, deprecated +from typing_extensions import LiteralString, Never, Self, Unpack, deprecated from . import path as _path @@ -1026,7 +1025,7 @@ In the future, this property will contain the last metadata change time.""") # At runtime it inherits from ABC and is not a Protocol, but it will be # on the allowlist for use as a Protocol starting in 3.14. @runtime_checkable -class PathLike(ABC, Protocol[AnyStr_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] +class PathLike(ABC, Protocol[AnyStr_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-protocol] """Abstract base class for implementing the file system path protocol.""" __slots__ = () @@ -2496,7 +2495,7 @@ if sys.platform != "win32": of the file the link points to. """ -def abort() -> NoReturn: +def abort() -> Never: """Abort the interpreter immediately. This function 'dumps core' or otherwise fails in the hardest way @@ -2504,14 +2503,14 @@ def abort() -> NoReturn: """ # These are defined as execl(file, *args) but the first *arg is mandatory. -def execl(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]]]]) -> NoReturn: +def execl(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]]]]) -> Never: """execl(file, *args) Execute the executable file with argument list args, replacing the current process. """ -def execlp(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]]]]) -> NoReturn: +def execlp(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]]]]) -> Never: """execlp(file, *args) Execute the executable file (which is searched for along $PATH) @@ -2519,14 +2518,14 @@ def execlp(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tupl """ # These are: execle(file, *args, env) but env is pulled from the last element of the args. -def execle(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]], _ExecEnv]]) -> NoReturn: +def execle(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]], _ExecEnv]]) -> Never: """execle(file, *args, env) Execute the executable file with argument list args and environment env, replacing the current process. """ -def execlpe(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]], _ExecEnv]]) -> NoReturn: +def execlpe(file: StrOrBytesPath, *args: Unpack[tuple[StrOrBytesPath, Unpack[tuple[StrOrBytesPath, ...]], _ExecEnv]]) -> Never: """execlpe(file, *args, env) Execute the executable file (which is searched for along $PATH) @@ -2555,7 +2554,7 @@ _ExecVArgs: TypeAlias = ( # we limit to str | bytes. _ExecEnv: TypeAlias = Mapping[bytes, bytes | str] | Mapping[str, bytes | str] -def execv(path: StrOrBytesPath, argv: _ExecVArgs, /) -> NoReturn: +def execv(path: StrOrBytesPath, argv: _ExecVArgs, /) -> Never: """Execute an executable path with arguments, replacing current process. path @@ -2564,7 +2563,7 @@ def execv(path: StrOrBytesPath, argv: _ExecVArgs, /) -> NoReturn: Tuple or list of strings. """ -def execve(path: FileDescriptorOrPath, argv: _ExecVArgs, env: _ExecEnv) -> NoReturn: +def execve(path: FileDescriptorOrPath, argv: _ExecVArgs, env: _ExecEnv) -> Never: """Execute an executable path with arguments, replacing current process. path @@ -2575,7 +2574,7 @@ def execve(path: FileDescriptorOrPath, argv: _ExecVArgs, env: _ExecEnv) -> NoRet Dictionary of strings mapping to strings. """ -def execvp(file: StrOrBytesPath, args: _ExecVArgs) -> NoReturn: +def execvp(file: StrOrBytesPath, args: _ExecVArgs) -> Never: """execvp(file, args) Execute the executable file (which is searched for along $PATH) @@ -2583,7 +2582,7 @@ def execvp(file: StrOrBytesPath, args: _ExecVArgs) -> NoReturn: args may be a list or tuple of strings. """ -def execvpe(file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> NoReturn: +def execvpe(file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> Never: """execvpe(file, args, env) Execute the executable file (which is searched for along $PATH) @@ -2592,7 +2591,7 @@ def execvpe(file: StrOrBytesPath, args: _ExecVArgs, env: _ExecEnv) -> NoReturn: args may be a list or tuple of strings. """ -def _exit(status: int) -> NoReturn: +def _exit(status: int) -> Never: """Exit to the system with specified status, without normal exit processing.""" def kill(pid: int, signal: int, /) -> None: @@ -3394,8 +3393,8 @@ if sys.platform == "linux": def pidfd_open(pid: int, flags: int = 0) -> int: """Return a file descriptor referring to the process *pid*. - The descriptor can be used to perform process management without races and - signals. + The descriptor can be used to perform process management without races + and signals. """ if sys.version_info >= (3, 12) and sys.platform == "linux": diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/__init__.pyi index 10b8a16139..20507b40d4 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pathlib/__init__.pyi @@ -166,7 +166,7 @@ class PurePath(PathLike[str]): slashes. """ - @deprecated("Deprecated since Python 3.14; will be removed in Python 3.19. Use `Path.as_uri()` instead.") + @deprecated("Deprecated; will be removed in Python 3.19. Use `Path.as_uri()` instead.") def as_uri(self) -> str: """Return the path as a URI.""" @@ -201,7 +201,7 @@ class PurePath(PathLike[str]): def is_relative_to(self, other: StrPath, /) -> bool: """Return True if the path is relative to another path or False.""" @overload - @deprecated("Passing additional arguments is deprecated since Python 3.12; removed in Python 3.14.") + @deprecated("Passing additional arguments is deprecated; removed in Python 3.14.") def is_relative_to(self, other: StrPath, /, *_deprecated: StrPath) -> bool: ... if sys.version_info >= (3, 12): diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.pyi index 36d906f710..cc1c2f68a1 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pkgutil.pyi @@ -86,7 +86,7 @@ if sys.version_info < (3, 12): def __init__(self, fullname: str, file: IO[str], filename: StrOrBytesPath, etc: tuple[str, str, int]) -> None: ... if sys.version_info < (3, 14): - @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `importlib.util.find_spec()` instead.") + @deprecated("Deprecated; removed in Python 3.14. Use `importlib.util.find_spec()` instead.") def find_loader(fullname: str) -> LoaderProtocol | None: """Find a "loader" object for fullname @@ -95,7 +95,7 @@ if sys.version_info < (3, 14): and only returns the loader rather than the full spec """ - @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `importlib.util.find_spec()` instead.") + @deprecated("Deprecated; removed in Python 3.14. Use `importlib.util.find_spec()` instead.") def get_loader(module_or_name: str) -> LoaderProtocol | None: """Get a "loader" object for module_or_name diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/platform.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/platform.pyi index dcc64332be..f476f2b57a 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/platform.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/platform.pyi @@ -41,7 +41,7 @@ def mac_ver( """ if sys.version_info < (3, 15): - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") + @deprecated("Deprecated; will be removed in Python 3.15.") def java_ver( release: str = "", vendor: str = "", @@ -120,7 +120,7 @@ if sys.version_info >= (3, 12): """Create new instance of uname_result_base(system, node, release, version, machine)""" @property - def processor(self) -> str: ... + def processor(self) -> str: ... # ty:ignore[invalid-named-tuple-override] else: @disjoint_base @@ -137,7 +137,7 @@ else: """Create new instance of uname_result_base(system, node, release, version, machine)""" @property - def processor(self) -> str: ... + def processor(self) -> str: ... # ty:ignore[invalid-named-tuple-override] def uname() -> uname_result: """Fairly portable uname interface. Returns a tuple diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/poplib.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/poplib.pyi index 1b04fb1408..1646846344 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/poplib.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/poplib.pyi @@ -9,8 +9,8 @@ import sys from _typeshed import StrOrBytesPath from builtins import list as _list # conflicts with a method named "list" from re import Pattern -from typing import Any, BinaryIO, Final, NoReturn, TypeAlias, overload -from typing_extensions import deprecated +from typing import Any, BinaryIO, Final, TypeAlias, overload +from typing_extensions import Never, deprecated __all__ = ["POP3", "error_proto", "POP3_SSL"] @@ -208,7 +208,7 @@ class POP3_SSL(POP3): def __init__( self, host: str, port: int = 995, *, timeout: float = ..., context: ssl.SSLContext | None = None ) -> None: ... - def stls(self, context: Any = None) -> NoReturn: + def stls(self, context: Any = None) -> Never: """The method unconditionally raises an exception since the STLS command doesn't make any sense on an already established SSL/TLS session. @@ -244,7 +244,7 @@ class POP3_SSL(POP3): certfile: StrOrBytesPath | None # "context" is actually the last argument, # but that breaks LSP and it doesn't really matter because all the arguments are ignored - def stls(self, context: Any = None, keyfile: Any = None, certfile: Any = None) -> NoReturn: + def stls(self, context: Any = None, keyfile: Any = None, certfile: Any = None) -> Never: """The method unconditionally raises an exception since the STLS command doesn't make any sense on an already established SSL/TLS session. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/collector.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/collector.pyi index 446ba8e007..e79cfa87ea 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/collector.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/profiling/sampling/collector.pyi @@ -54,4 +54,8 @@ class Collector(ABC): @abstractmethod def export(self, filename: StrOrBytesPath) -> None: - """Export collected data to a file.""" + """Export collected data. + + Returns: + bool: True if output was generated, False if there was no data to export. + """ diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pty.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pty.pyi index 454798ef49..22bbbf356f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/pty.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pty.pyi @@ -20,14 +20,14 @@ if sys.platform != "win32": """ if sys.version_info < (3, 14): - @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `openpty()` instead.") + @deprecated("Deprecated; removed in Python 3.14. Use `openpty()` instead.") def master_open() -> tuple[int, str]: """master_open() -> (master_fd, slave_name) Open a pty master and return the fd, and the filename of the slave end. Deprecated, use openpty() instead. """ - @deprecated("Deprecated since Python 3.12; removed in Python 3.14. Use `openpty()` instead.") + @deprecated("Deprecated; removed in Python 3.14. Use `openpty()` instead.") def slave_open(tty_name: str) -> int: """slave_open(tty_name) -> slave_fd Open the pty slave and acquire the controlling terminal, returning diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/pydoc.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/pydoc.pyi index 2d5c4e8e1a..1f8dd6d0b7 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/pydoc.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/pydoc.pyi @@ -44,8 +44,8 @@ from builtins import list as _list # "list" conflicts with method name 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, TypeGuard, TypeVar, overload, type_check_only -from typing_extensions import deprecated +from typing import IO, Any, AnyStr, Final, Protocol, TypeGuard, TypeVar, overload, type_check_only +from typing_extensions import Never, deprecated __all__ = ["help"] @@ -91,7 +91,7 @@ def visiblename(name: str, all: Container[str] | None = None, obj: object = None def classify_class_attrs(object: object) -> list[tuple[str, str, type, str]]: """Wrap inspect.classify_class_attrs, with fixup for data descriptors and bound methods.""" -@deprecated("Deprecated since Python 3.13.") +@deprecated("Deprecated.") def ispackage(path: StrPath) -> bool: # undocumented """Guess whether a path refers to a package directory.""" @@ -138,7 +138,7 @@ class Doc: def document(self, object: object, name: str | None = None, *args: Any) -> str: """Generate documentation for an object.""" - def fail(self, object: object, name: str | None = None, *args: Any) -> NoReturn: + def fail(self, object: object, name: str | None = None, *args: Any) -> Never: """Raise an exception for unimplemented types.""" @abstractmethod diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/random.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/random.pyi index 69fd1aa2f6..a320c7afb8 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/random.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/random.pyi @@ -51,8 +51,8 @@ import sys from _typeshed import SupportsLenAndGetItem from collections.abc import Callable, Iterable, MutableSequence, Sequence, Set as AbstractSet from fractions import Fraction -from typing import Any, ClassVar, NoReturn, TypeVar, overload -from typing_extensions import deprecated +from typing import Any, ClassVar, TypeVar, overload +from typing_extensions import Never, deprecated __all__ = [ "Random", @@ -435,10 +435,10 @@ class SystemRandom(Random): def getrandbits(self, k: int) -> int: # k can be passed by keyword """getrandbits(k) -> x. Generates an int with k random bits.""" - def getstate(self, *args: Any, **kwds: Any) -> NoReturn: + def getstate(self, *args: Any, **kwds: Any) -> Never: """Method should not be called for a system random number generator.""" - def setstate(self, *args: Any, **kwds: Any) -> NoReturn: + def setstate(self, *args: Any, **kwds: Any) -> Never: """Method should not be called for a system random number generator.""" _inst: Random diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/re.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/re.pyi index 4ee5b34366..4a73f3f1be 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/re.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/re.pyi @@ -347,7 +347,7 @@ class Pattern(Generic[AnyStr]): @overload def split(self, string: AnyStr, maxsplit: int = 0) -> list[AnyStr | MaybeNone]: ... - # return type depends on the number of groups in the pattern + # return type is either list[str/bytes] or list[tuple[str/bytes, ...]] @overload def findall(self: Pattern[str], string: str, pos: int = 0, endpos: int = sys.maxsize) -> list[Any]: """Return a list of all non-overlapping matches of pattern in string.""" @@ -511,6 +511,7 @@ def split( pattern: bytes | Pattern[bytes], string: ReadableBuffer, maxsplit: int = 0, flags: _FlagsType = 0 ) -> list[bytes | MaybeNone]: ... +# return type is either list[str/bytes] or list[tuple[str/bytes, ...]] @overload def findall(pattern: str | Pattern[str], string: str, flags: _FlagsType = 0) -> list[Any]: """Return a list of all non-overlapping matches in the string. @@ -585,6 +586,6 @@ def purge() -> None: """Clear the regular expression caches""" if sys.version_info < (3, 13): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13. Use `re.compile()` instead.") + @deprecated("Deprecated; removed in Python 3.13. Use `re.compile()` instead.") def template(pattern: AnyStr | Pattern[AnyStr], flags: _FlagsType = 0) -> Pattern[AnyStr]: # undocumented """Compile a template pattern, returning a Pattern object, deprecated""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/shutil.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/shutil.pyi index 1276ea37da..a94bcb5565 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/shutil.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/shutil.pyi @@ -9,8 +9,8 @@ import sys from _typeshed import BytesPath, ExcInfo, FileDescriptorOrPath, MaybeNone, StrOrBytesPath, StrPath, SupportsRead, SupportsWrite from collections.abc import Callable, Iterable, Sequence from tarfile import _TarfileFilter -from typing import Any, AnyStr, NamedTuple, NoReturn, Protocol, TypeAlias, TypeVar, overload, type_check_only -from typing_extensions import deprecated +from typing import Any, AnyStr, NamedTuple, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Never, deprecated __all__ = [ "copyfileobj", @@ -374,7 +374,7 @@ else: if sys.platform == "win32" and sys.version_info < (3, 12): @overload @deprecated("On Windows before Python 3.12, using a PathLike as `cmd` would always fail or return `None`.") - def which(cmd: os.PathLike[str], mode: int = 1, path: StrPath | None = None) -> NoReturn: + def which(cmd: os.PathLike[str], mode: int = 1, path: StrPath | None = None) -> Never: """Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/ssl.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/ssl.pyi index 4aad7c3086..a07a059822 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/ssl.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/ssl.pyi @@ -392,8 +392,18 @@ class Purpose(_ASN1Object, enum.Enum): # because this is an enum, the inherited __new__ is replaced at runtime with # Enum.__new__. def __new__(cls, value: object) -> Self: ... - SERVER_AUTH = (129, "serverAuth", "TLS Web Server Authentication", "1.3.6.1.5.5.7.3.2") # pyright: ignore[reportCallIssue] - CLIENT_AUTH = (130, "clientAuth", "TLS Web Client Authentication", "1.3.6.1.5.5.7.3.1") # pyright: ignore[reportCallIssue] + SERVER_AUTH = ( # ty:ignore[invalid-assignment] + 129, + "serverAuth", + "TLS Web Server Authentication", + "1.3.6.1.5.5.7.3.2", + ) # pyright: ignore[reportCallIssue] + CLIENT_AUTH = ( # ty:ignore[invalid-assignment] + 130, + "clientAuth", + "TLS Web Client Authentication", + "1.3.6.1.5.5.7.3.1", + ) # pyright: ignore[reportCallIssue] class SSLSocket(socket.socket): """This class implements a subtype of socket.socket that wraps diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sunau.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sunau.pyi index 336d93e0af..d6ad755b42 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/sunau.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sunau.pyi @@ -104,8 +104,8 @@ is destroyed. """ from _typeshed import Unused -from typing import IO, Any, Final, Literal, NamedTuple, NoReturn, TypeAlias, overload -from typing_extensions import Self +from typing import IO, Any, Final, Literal, NamedTuple, TypeAlias, overload +from typing_extensions import Never, Self _File: TypeAlias = str | IO[bytes] @@ -153,7 +153,7 @@ class Au_read: def getcompname(self) -> str: ... def getparams(self) -> _sunau_params: ... def getmarkers(self) -> None: ... - def getmark(self, id: Any) -> NoReturn: ... + def getmark(self, id: Any) -> Never: ... def setpos(self, pos: int) -> None: ... def readframes(self, nframes: int) -> bytes | None: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/symtable.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/symtable.pyi index d2a0f678d0..8bea6b27db 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/symtable.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/symtable.pyi @@ -124,7 +124,7 @@ class Function(SymbolTable): """Return a tuple of nonlocals in the function.""" class Class(SymbolTable): - @deprecated("Deprecated since Python 3.14; will be removed in Python 3.16.") + @deprecated("Deprecated; will be removed in Python 3.16.") def get_methods(self) -> tuple[str, ...]: """Return a tuple of methods declared in the class.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi index 1e5ca3b05e..92bb308930 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sys/__init__.pyi @@ -78,8 +78,8 @@ from builtins import object as _object from collections.abc import AsyncGenerator, Callable, Sequence from io import TextIOWrapper from types import FrameType, ModuleType, SimpleNamespace, TracebackType -from typing import Any, Final, Literal, NoReturn, Protocol, TextIO, TypeAlias, TypeVar, final, overload, type_check_only -from typing_extensions import LiteralString, deprecated +from typing import Any, Final, Literal, Protocol, TextIO, TypeAlias, TypeVar, final, overload, type_check_only +from typing_extensions import LiteralString, Never, deprecated _T = TypeVar("_T") _LazyImportMode: TypeAlias = Literal["normal", "all", "none"] @@ -677,7 +677,7 @@ if sys.version_info >= (3, 11): if no such exception exists. """ -def exit(status: _ExitCode = None, /) -> NoReturn: +def exit(status: _ExitCode = None, /) -> Never: """Exit the interpreter by raising SystemExit(status). If the status is omitted or None, it defaults to zero (i.e., success). @@ -1038,7 +1038,7 @@ if sys.version_info >= (3, 12): """Activate stack profiler trampoline *backend*.""" else: - def activate_stack_trampoline(backend: str, /) -> NoReturn: + def activate_stack_trampoline(backend: str, /) -> Never: """Activate stack profiler trampoline *backend*.""" from . import _monitoring diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.pyi index dfbb25c721..9f3ec2784f 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/sysconfig.pyi @@ -105,13 +105,13 @@ elif sys.version_info >= (3, 11): @overload def is_python_build() -> bool: ... @overload - @deprecated("The `check_home` parameter is deprecated since Python 3.12; removed in Python 3.15.") + @deprecated("The `check_home` parameter is deprecated; removed in Python 3.15.") def is_python_build(check_home: object = None) -> bool: ... else: @overload def is_python_build() -> bool: ... @overload - @deprecated("The `check_home` parameter is deprecated since Python 3.12; removed in Python 3.15.") + @deprecated("The `check_home` parameter is deprecated; removed in Python 3.15.") def is_python_build(check_home: bool = False) -> bool: ... def parse_config_h(fp: IO[Any], vars: dict[str, Any] | None = None) -> dict[str, Any]: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tarfile.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tarfile.pyi index 13a6b16484..874019a7ed 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tarfile.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tarfile.pyi @@ -1176,10 +1176,10 @@ class TarInfo: """ @property - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.16.") + @deprecated("Deprecated; will be removed in Python 3.16.") def tarfile(self) -> TarFile | None: ... @tarfile.setter - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.16.") + @deprecated("Deprecated; will be removed in Python 3.16.") def tarfile(self, tarfile: TarFile | None) -> None: ... @classmethod diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi index c3316f9c21..50089041f1 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/__init__.pyi @@ -3,7 +3,7 @@ Tkinter provides classes which allow the display, positioning and control of widgets. Toplevel widgets are Tk and Toplevel. Other widgets are Frame, Label, Entry, Text, Canvas, Button, Radiobutton, -Checkbutton, Scale, Listbox, Scrollbar, OptionMenu, Spinbox +Checkbutton, Scale, Listbox, Scrollbar, OptionMenu, Spinbox, LabelFrame and PanedWindow. Properties of the widgets are specified with keyword arguments. @@ -460,7 +460,7 @@ class Variable: def trace_info(self) -> list[tuple[tuple[Literal["array", "read", "write", "unset"], ...], str]]: """Return all trace callback information.""" - @deprecated("Deprecated since Python 3.14. Use `trace_add()` instead.") + @deprecated("Deprecated; will be removed in Python 3.17. Use `trace_add()` instead.") def trace(self, mode, callback) -> str: """Define a trace callback for the variable. @@ -471,10 +471,11 @@ class Variable: Return the name of the callback. This deprecated method wraps a deprecated Tcl method removed - in Tcl 9.0. Use trace_add() instead. + in Tcl 9.0 and will be removed in Python 3.17. Use trace_add() + instead. """ - @deprecated("Deprecated since Python 3.14. Use `trace_add()` instead.") + @deprecated("Deprecated; will be removed in Python 3.17. Use `trace_add()` instead.") def trace_variable(self, mode, callback) -> str: """Define a trace callback for the variable. @@ -485,10 +486,11 @@ class Variable: Return the name of the callback. This deprecated method wraps a deprecated Tcl method removed - in Tcl 9.0. Use trace_add() instead. + in Tcl 9.0 and will be removed in Python 3.17. Use trace_add() + instead. """ - @deprecated("Deprecated since Python 3.14. Use `trace_remove()` instead.") + @deprecated("Deprecated; will be removed in Python 3.17. Use `trace_remove()` instead.") def trace_vdelete(self, mode, cbname) -> None: """Delete the trace callback for a variable. @@ -496,15 +498,17 @@ class Variable: CBNAME is the name of the callback returned from trace_variable or trace. This deprecated method wraps a deprecated Tcl method removed - in Tcl 9.0. Use trace_remove() instead. + in Tcl 9.0 and will be removed in Python 3.17. Use trace_remove() + instead. """ - @deprecated("Deprecated since Python 3.14. Use `trace_info()` instead.") + @deprecated("Deprecated; will be removed in Python 3.17. Use `trace_info()` instead.") def trace_vinfo(self) -> list[Incomplete]: """Return all trace callback information. This deprecated method wraps a deprecated Tcl method removed - in Tcl 9.0. Use trace_info() instead. + in Tcl 9.0 and will be removed in Python 3.17. Use trace_info() + instead. """ def __eq__(self, other: object) -> bool: ... @@ -2179,7 +2183,7 @@ class Wm: iconphoto = wm_iconphoto def wm_iconposition(self, x: int | None = None, y: int | None = None) -> tuple[int, int] | None: """Set the position of the icon of this widget to X and Y. Return - a tuple of the current values of X and X if None is given. + a tuple of the current values of X and Y if None is given. """ iconposition = wm_iconposition @@ -2748,10 +2752,11 @@ class Toplevel(BaseWidget, Wm): ) -> None: """Construct a toplevel widget with the parent MASTER. - Valid option names: background, bd, bg, borderwidth, class, - colormap, container, cursor, height, highlightbackground, - highlightcolor, highlightthickness, menu, relief, screen, takefocus, - use, visual, width. + Valid option names: background, backgroundimage (Tk 9.0+), bd, bg, + bgimg (Tk 9.0+), borderwidth, class, colormap, container, + cursor, height, highlightbackground, highlightcolor, + highlightthickness, menu, padx, pady, relief, screen, + takefocus, tile (Tk 9.0+), use, visual, width. """ @overload @@ -3863,12 +3868,13 @@ class Checkbutton(Widget): """Construct a checkbutton widget with the parent MASTER. Valid option names: activebackground, activeforeground, anchor, - background, bd, bg, bitmap, borderwidth, command, cursor, - disabledforeground, fg, font, foreground, height, - highlightbackground, highlightcolor, highlightthickness, image, - indicatoron, justify, offvalue, onvalue, padx, pady, relief, - selectcolor, selectimage, state, takefocus, text, textvariable, - underline, variable, width, wraplength. + background, bd, bg, bitmap, borderwidth, command, compound, + cursor, disabledforeground, fg, font, foreground, height, + highlightbackground, highlightcolor, highlightthickness, + image, indicatoron, justify, offrelief, offvalue, onvalue, + overrelief, padx, pady, relief, selectcolor, selectimage, + state, takefocus, text, textvariable, tristateimage, + tristatevalue, underline, variable, width, wraplength. """ @overload @@ -4000,13 +4006,15 @@ class Entry(Widget, XView): """Construct an entry widget with the parent MASTER. Valid option names: background, bd, bg, borderwidth, cursor, - exportselection, fg, font, foreground, highlightbackground, - highlightcolor, highlightthickness, insertbackground, - insertborderwidth, insertofftime, insertontime, insertwidth, - invalidcommand, invcmd, justify, relief, selectbackground, - selectborderwidth, selectforeground, show, state, takefocus, - textvariable, validate, validatecommand, vcmd, width, - xscrollcommand. + disabledbackground, disabledforeground, exportselection, fg, + font, foreground, highlightbackground, highlightcolor, + highlightthickness, insertbackground, insertborderwidth, + insertofftime, insertontime, insertwidth, invalidcommand, + invcmd, justify, locale (Tk 9.1+), placeholder (Tk 9.0+), + placeholderforeground (Tk 9.0+), readonlybackground, relief, + selectbackground, selectborderwidth, selectforeground, show, + state, takefocus, textvariable, validate, validatecommand, + vcmd, width, xscrollcommand. """ @overload @@ -4150,9 +4158,11 @@ class Frame(Widget): ) -> None: """Construct a frame widget with the parent MASTER. - Valid option names: background, bd, bg, borderwidth, class, - colormap, container, cursor, height, highlightbackground, - highlightcolor, highlightthickness, relief, takefocus, visual, width. + Valid option names: background, backgroundimage (Tk 9.0+), bd, bg, + bgimg (Tk 9.0+), borderwidth, class, colormap, container, + cursor, height, highlightbackground, highlightcolor, + highlightthickness, padx, pady, relief, takefocus, tile (Tk + 9.0+), visual, width. """ @overload @@ -4248,7 +4258,8 @@ class Label(Widget): WIDGET-SPECIFIC OPTIONS - height, state, width + compound, height, state, + textangle (Tk 9.1+), width """ @@ -4362,11 +4373,14 @@ class Listbox(Widget, XView, YView): ) -> None: """Construct a listbox widget with the parent MASTER. - Valid option names: background, bd, bg, borderwidth, cursor, - exportselection, fg, font, foreground, height, highlightbackground, - highlightcolor, highlightthickness, relief, selectbackground, - selectborderwidth, selectforeground, selectmode, setgrid, takefocus, - width, xscrollcommand, yscrollcommand, listvariable. + Valid option names: activestyle, background, bd, bg, borderwidth, + cursor, disabledforeground, exportselection, fg, font, + foreground, height, highlightbackground, highlightcolor, + highlightthickness, inactiveselectbackground (Tk 9.1+), + inactiveselectforeground (Tk 9.1+), justify, listvariable, + relief, selectbackground, selectborderwidth, selectforeground, + selectmode, setgrid, state, takefocus, width, xscrollcommand, + yscrollcommand. """ @overload @@ -4527,7 +4541,8 @@ class Menu(Widget): """Construct menu widget with the parent MASTER. Valid option names: activebackground, activeborderwidth, - activeforeground, background, bd, bg, borderwidth, cursor, + activeforeground, activerelief (Tk 9.0+), background, bd, bg, + borderwidth, cursor, disabledforeground, fg, font, foreground, postcommand, relief, selectcolor, takefocus, tearoff, tearoffcommand, title, type. """ @@ -4881,7 +4896,16 @@ class Menubutton(Widget): underline: int = -1, width: float | str = 0, wraplength: float | str = 0, - ) -> None: ... + ) -> None: + """Construct a menubutton widget with the parent MASTER. + + Valid option names: activebackground, activeforeground, anchor, + background, bd, bg, bitmap, borderwidth, compound, cursor, + direction, disabledforeground, fg, font, foreground, height, + highlightbackground, highlightcolor, highlightthickness, + image, indicatoron, justify, menu, padx, pady, relief, state, + takefocus, text, textvariable, underline, width, wraplength. + """ @overload def configure( @@ -4972,7 +4996,14 @@ class Message(Widget): textvariable: Variable = ..., # there's width but no height width: float | str = 0, - ) -> None: ... + ) -> None: + """Construct a message widget with the parent MASTER. + + Valid option names: anchor, aspect, background, bd, bg, borderwidth, + cursor, fg, font, foreground, highlightbackground, + highlightcolor, highlightthickness, justify, padx, pady, + relief, takefocus, text, textvariable, width. + """ @overload def configure( @@ -5073,12 +5104,13 @@ class Radiobutton(Widget): """Construct a radiobutton widget with the parent MASTER. Valid option names: activebackground, activeforeground, anchor, - background, bd, bg, bitmap, borderwidth, command, cursor, - disabledforeground, fg, font, foreground, height, - highlightbackground, highlightcolor, highlightthickness, image, - indicatoron, justify, padx, pady, relief, selectcolor, selectimage, - state, takefocus, text, textvariable, underline, value, variable, - width, wraplength. + background, bd, bg, bitmap, borderwidth, command, compound, + cursor, disabledforeground, fg, font, foreground, height, + highlightbackground, highlightcolor, highlightthickness, + image, indicatoron, justify, offrelief, overrelief, padx, + pady, relief, selectcolor, selectimage, state, takefocus, + text, textvariable, tristateimage, tristatevalue, underline, + value, variable, width, wraplength. """ @overload @@ -5486,9 +5518,11 @@ class Text(Widget, XView, YView): WIDGET-SPECIFIC OPTIONS - autoseparators, height, maxundo, - spacing1, spacing2, spacing3, - state, tabs, undo, width, wrap, + autoseparators, blockcursor, endline, + height, inactiveselectbackground, + insertunfocussed, locale (Tk 9.1+), maxundo, + spacing1, spacing2, spacing3, startline, + state, tabs, tabstyle, undo, width, wrap, """ @@ -6301,7 +6335,7 @@ class OptionMenu(Menubutton): variable: StringVar, value: str, *values: str, - command: Callable[[StringVar], object] | None = ..., + command: Callable[[str], object] | None = ..., name: str | None = None, ) -> None: """Construct an optionmenu widget with the parent MASTER, with @@ -6318,7 +6352,7 @@ class OptionMenu(Menubutton): variable: StringVar, value: str, *values: str, - command: Callable[[StringVar], object] | None = ..., + command: Callable[[str], object] | None = ..., ) -> None: """Construct an optionmenu widget with the parent MASTER, with the option textvariable set to VARIABLE, the initially selected @@ -6762,9 +6796,12 @@ class Spinbox(Widget, XView): buttondownrelief, buttonuprelief, command, disabledbackground, disabledforeground, format, from, - invalidcommand, increment, + invalidcommand, invcmd, increment, + locale (Tk 9.1+), + placeholder (Tk 9.0+), + placeholderforeground (Tk 9.0+), readonlybackground, state, to, - validate, validatecommand values, + validate, validatecommand, vcmd, values, width, wrap, """ @@ -7099,8 +7136,9 @@ class PanedWindow(Widget): WIDGET-SPECIFIC OPTIONS handlepad, handlesize, opaqueresize, - sashcursor, sashpad, sashrelief, - sashwidth, showhandle, + proxybackground, proxyborderwidth, + proxyrelief, sashcursor, sashpad, + sashrelief, sashwidth, showhandle, """ @overload diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.pyi index 22a917d210..e0fbc0100a 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/colorchooser.pyi @@ -1,3 +1,5 @@ +"""Interface to the native Tk color selection dialog.""" + from tkinter import Misc from tkinter.commondialog import Dialog from typing import ClassVar diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi index 6dba6bd609..38be812f6a 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/commondialog.pyi @@ -1,3 +1,5 @@ +"""Base class for the Tk common dialogs.""" + from collections.abc import Mapping from tkinter import Misc from typing import Any, ClassVar diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi index 971b64f091..011771c5b6 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/dialog.pyi @@ -1,3 +1,5 @@ +"""Classic Tk dialog box, wrapping the tk_dialog script.""" + from collections.abc import Mapping from tkinter import Widget from typing import Any, Final @@ -7,7 +9,10 @@ __all__ = ["Dialog"] DIALOG_ICON: Final = "questhead" class Dialog(Widget): + """A modal dialog box built from the classic (non-themed) Tk widgets.""" + widgetName: str num: int def __init__(self, master=None, cnf: Mapping[str, Any] = {}, **kw) -> None: ... - def destroy(self) -> None: ... + def destroy(self) -> None: + """Do nothing; the dialog window is already destroyed.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/font.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/font.pyi index c17189fc1c..2da1ac62ac 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/font.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/font.pyi @@ -1,3 +1,5 @@ +"""Utilities to help work with fonts in Tkinter.""" + import _tkinter import itertools import tkinter diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi index 424e8903d6..d378fbea8d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/messagebox.pyi @@ -1,3 +1,5 @@ +"""Interface to the standard Tk message boxes.""" + from tkinter import Misc from tkinter.commondialog import Dialog from typing import ClassVar, Final, Literal @@ -68,7 +70,7 @@ def askquestion( default: Literal["yes", "no"] = ..., parent: Misc = ..., ) -> str: - """Ask a question""" + """Ask a question; return the symbolic name of the selected button""" def askokcancel( title: str | None = None, @@ -79,7 +81,7 @@ def askokcancel( default: Literal["ok", "cancel"] = ..., parent: Misc = ..., ) -> bool: - """Ask if operation should proceed; return true if the answer is ok""" + """Ask if operation should proceed; return True if the answer is ok""" def askyesno( title: str | None = None, @@ -90,7 +92,7 @@ def askyesno( default: Literal["yes", "no"] = ..., parent: Misc = ..., ) -> bool: - """Ask a question; return true if the answer is yes""" + """Ask a question; return True if the answer is yes""" def askyesnocancel( title: str | None = None, @@ -101,7 +103,7 @@ def askyesnocancel( default: Literal["cancel", "yes", "no"] = ..., parent: Misc = ..., ) -> bool | None: - """Ask a question; return true if the answer is yes, None if cancelled.""" + """Ask a question; return True if the answer is yes, None if cancelled""" def askretrycancel( title: str | None = None, @@ -112,4 +114,4 @@ def askretrycancel( default: Literal["retry", "cancel"] = ..., parent: Misc = ..., ) -> bool: - """Ask if operation should be retried; return true if the answer is yes""" + """Ask if operation should be retried; return True if the answer is retry""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.pyi index bb59259c88..197b1294e2 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/simpledialog.pyi @@ -1,4 +1,4 @@ -"""This modules handles dialog boxes. +"""This module handles dialog boxes. It contains the following public symbols: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi index 98f42a65c2..9616bbf854 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/tkinter/ttk.pyi @@ -404,8 +404,8 @@ class Button(Widget): STANDARD OPTIONS - class, compound, cursor, image, state, style, takefocus, - text, textvariable, underline, width + class, compound, cursor, image, justify (Tk 9.0+), padding, + state, style, takefocus, text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS @@ -482,8 +482,8 @@ class Checkbutton(Widget): STANDARD OPTIONS - class, compound, cursor, image, state, style, takefocus, - text, textvariable, underline, width + class, compound, cursor, image, justify (Tk 9.0+), padding, + state, style, takefocus, text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS @@ -574,8 +574,10 @@ class Entry(Widget, tkinter.Entry): WIDGET-SPECIFIC OPTIONS - exportselection, invalidcommand, justify, show, state, - textvariable, validate, validatecommand, width + background, exportselection, font, foreground, invalidcommand, + justify, locale (Tk 9.1+), placeholder (Tk 9.0+), + placeholderforeground (Tk 9.0+), show, state, textvariable, + validate, validatecommand, width VALIDATION MODES @@ -708,12 +710,14 @@ class Combobox(Entry): STANDARD OPTIONS - class, cursor, style, takefocus + class, cursor, style, takefocus, xscrollcommand WIDGET-SPECIFIC OPTIONS - exportselection, justify, height, postcommand, state, - textvariable, values, width + background, exportselection, font, foreground, height, + invalidcommand, justify, locale (Tk 9.1+), placeholder (Tk 9.0+), + placeholderforeground (Tk 9.0+), postcommand, show, state, + textvariable, validate, validatecommand, values, width """ @overload # type: ignore[override] @@ -907,13 +911,13 @@ class Label(Widget): STANDARD OPTIONS - class, compound, cursor, image, style, takefocus, text, - textvariable, underline, width + class, compound, cursor, image, state, style, takefocus, + text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS - anchor, background, font, foreground, justify, padding, - relief, text, wraplength + anchor, background, borderwidth, font, foreground, justify, + padding, relief, text, textangle (Tk 9.1+), wraplength """ @overload @@ -992,8 +996,9 @@ class Labelframe(Widget): class, cursor, style, takefocus WIDGET-SPECIFIC OPTIONS - labelanchor, text, underline, padding, labelwidget, width, - height + + borderwidth, height, labelanchor, labelwidget, padding, + relief, text, underline, width """ @overload @@ -1063,8 +1068,8 @@ class Menubutton(Widget): STANDARD OPTIONS - class, compound, cursor, image, state, style, takefocus, - text, textvariable, underline, width + class, compound, cursor, image, justify (Tk 9.0+), padding, + state, style, takefocus, text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS @@ -1431,7 +1436,9 @@ class Progressbar(Widget): STANDARD OPTIONS - class, cursor, style, takefocus + anchor (Tk 9.0+), class, cursor, font (Tk 9.0+), + foreground (Tk 9.0+), justify (Tk 9.0+), style, takefocus, + text (Tk 9.0+), wraplength (Tk 9.0+) WIDGET-SPECIFIC OPTIONS @@ -1518,8 +1525,8 @@ class Radiobutton(Widget): STANDARD OPTIONS - class, compound, cursor, image, state, style, takefocus, - text, textvariable, underline, width + class, compound, cursor, image, justify (Tk 9.0+), padding, + state, style, takefocus, text, textvariable, underline, width WIDGET-SPECIFIC OPTIONS @@ -1598,7 +1605,7 @@ class Scale(Widget, tkinter.Scale): # type: ignore[misc] STANDARD OPTIONS - class, cursor, style, takefocus + class, cursor, state, style, takefocus WIDGET-SPECIFIC OPTIONS @@ -1899,7 +1906,10 @@ class Spinbox(Entry): WIDGET-SPECIFIC OPTIONS - to, from_, increment, values, wrap, format, command + background, command, exportselection, font, foreground, + format, from_, increment, justify, locale (Tk 9.1+), + placeholder (Tk 9.0+), placeholderforeground (Tk 9.0+), show, + state, textvariable, to, values, width, wrap """ @overload # type: ignore[override] @@ -2021,7 +2031,10 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): WIDGET-SPECIFIC OPTIONS - columns, displaycolumns, height, padding, selectmode, show + columns, displaycolumns, headingheight (Tk 9.1+), height, + padding, rowheight (Tk 9.1+), selectmode, selecttype (Tk 9.0+), + show, striped (Tk 9.0+), titlecolumns (Tk 9.0+), + titleitems (Tk 9.0+) ITEM OPTIONS @@ -2404,8 +2417,8 @@ class Treeview(Widget, tkinter.XView, tkinter.YView): @overload def tag_has(self, tagname: str, item: None = None) -> tuple[str, ...]: - """If item is specified, returns 1 or 0 depending on whether the - specified item has the given tagname. Otherwise, returns a list of + """If item is specified, returns True if the specified item has the + given tagname, False otherwise. Otherwise, returns a list of all items which have the specified tag. * Availability: Tk 8.6 diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/types.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/types.pyi index 1f54edb0b0..2c258e31bc 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/types.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/types.pyi @@ -301,7 +301,7 @@ class CodeType: """The same as replace().""" @final -class MappingProxyType(Mapping[_KT_co, _VT_co]): # type: ignore[type-var] # pyright: ignore[reportInvalidTypeArguments] +class MappingProxyType(Mapping[_KT_co, _VT_co]): # type: ignore[type-var] # pyright: ignore[reportInvalidTypeArguments] # ty:ignore[invalid-generic-class] """Read-only proxy of a mapping.""" __hash__: ClassVar[None] # type: ignore[assignment] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi index 0cde9fa309..6acb32d4b6 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi @@ -1172,7 +1172,7 @@ def no_type_check(arg: _F) -> _F: """ if sys.version_info < (3, 15): - @deprecated("Deprecated since Python 3.13; removed in Python 3.15.") + @deprecated("Deprecated; removed in Python 3.15.") def no_type_check_decorator(decorator: Callable[_P, _T]) -> Callable[_P, _T]: """Decorator to give another decorator the @no_type_check effect. @@ -2386,13 +2386,13 @@ class NamedTuple(tuple[Any, ...]): @final @classmethod - def _make(cls, iterable: Iterable[Any]) -> typing_extensions.Self: ... + def _make(cls, iterable: Iterable[Any]) -> typing_extensions.Self: ... # ty:ignore[invalid-type-form] @final def _asdict(self) -> dict[str, Any]: ... @final - def _replace(self, **kwargs: Any) -> typing_extensions.Self: ... + def _replace(self, **kwargs: Any) -> typing_extensions.Self: ... # ty:ignore[invalid-type-form] if sys.version_info >= (3, 13): - def __replace__(self, **kwargs: Any) -> typing_extensions.Self: ... + def __replace__(self, **kwargs: Any) -> typing_extensions.Self: ... # ty:ignore[invalid-type-form] # Internal mypy fallback type for all typed dicts (does not exist at runtime) # N.B. Keep this mostly in sync with typing_extensions._TypedDict/mypy_extensions._TypedDict diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi index 6c6a63b2ab..ff06ec0a88 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi @@ -900,9 +900,9 @@ else: def __init__(self, typename: str, fields: None = None, **kwargs: Any) -> None: ... @classmethod - def _make(cls, iterable: Iterable[Any]) -> Self: ... + def _make(cls, iterable: Iterable[Any]) -> Self: ... # ty:ignore[invalid-type-form] def _asdict(self) -> dict[str, Any]: ... - def _replace(self, **kwargs: Any) -> Self: ... + def _replace(self, **kwargs: Any) -> Self: ... # ty:ignore[invalid-type-form] class NewType: """NewType creates simple unique types with almost zero @@ -992,7 +992,7 @@ else: # At runtime it inherits from ABC and is not a Protocol, but it is on the # allowlist for use as a Protocol. @runtime_checkable - class Buffer(Protocol, abc.ABC): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] + class Buffer(Protocol, abc.ABC): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-protocol] """Base class for classes that implement the buffer protocol. The buffer protocol allows Python objects to expose a low-level @@ -1453,7 +1453,7 @@ else: def __module__(self) -> str | None: ... # type: ignore[override] # Returns typing._GenericAlias, which isn't stubbed. def __getitem__(self, parameters: Incomplete | tuple[Incomplete, ...]) -> AnnotationForm: ... - def __init_subclass__(cls, *args: Unused, **kwargs: Unused) -> NoReturn: ... + def __init_subclass__(cls, *args: Unused, **kwargs: Unused) -> Never: ... def __or__(self, right: Any, /) -> _SpecialForm: ... def __ror__(self, left: Any, /) -> _SpecialForm: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.pyi index 852ca88890..7ec169ebdc 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/case.pyi @@ -14,7 +14,6 @@ from typing import ( AnyStr, Final, Generic, - NoReturn, ParamSpec, Protocol, SupportsAbs, @@ -170,7 +169,7 @@ class TestCase: def run(self, result: unittest.result.TestResult | None = None) -> unittest.result.TestResult | None: ... def __call__(self, result: unittest.result.TestResult | None = ...) -> unittest.result.TestResult | None: ... - def skipTest(self, reason: Any) -> NoReturn: + def skipTest(self, reason: Any) -> Never: """Skip this test.""" def subTest(self, msg: Any = ..., **params: Any) -> AbstractContextManager[None]: @@ -612,7 +611,7 @@ class TestCase: # assertDictEqual accepts only true dict instances. We can't use that here, since that would make # assertDictEqual incompatible with TypedDict. def assertDictEqual(self, d1: Mapping[Any, object], d2: Mapping[Any, object], msg: Any = None) -> None: ... - def fail(self, msg: Any = None) -> NoReturn: + def fail(self, msg: Any = None) -> Never: """Fail immediately, with the given message.""" def countTestCases(self) -> int: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/loader.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/loader.pyi index acce46ff7e..32db574979 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/loader.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/loader.pyi @@ -88,21 +88,21 @@ class TestLoader: defaultTestLoader: TestLoader if sys.version_info < (3, 13): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + @deprecated("Deprecated; removed in Python 3.13.") def getTestCaseNames( testCaseClass: type[unittest.case.TestCase], prefix: str, sortUsing: _SortComparisonMethod = ..., testNamePatterns: list[str] | None = None, ) -> Sequence[str]: ... - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + @deprecated("Deprecated; removed in Python 3.13.") def makeSuite( testCaseClass: type[unittest.case.TestCase], prefix: str = "test", sortUsing: _SortComparisonMethod = ..., suiteClass: _SuiteClass = ..., ) -> unittest.suite.TestSuite: ... - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + @deprecated("Deprecated; removed in Python 3.13.") def findTestCases( module: ModuleType, prefix: str = "test", sortUsing: _SortComparisonMethod = ..., suiteClass: _SuiteClass = ... ) -> unittest.suite.TestSuite: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/main.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/main.pyi index 59f199311f..26df9a33e2 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/main.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/main.pyi @@ -70,7 +70,7 @@ class TestProgram: ) -> None: ... if sys.version_info < (3, 13): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + @deprecated("Deprecated; removed in Python 3.13.") def usageExit(self, msg: Any = None) -> None: ... def parseArgs(self, argv: list[str]) -> None: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.pyi index 61a084ca94..a45d47e2d9 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/unittest/mock.pyi @@ -233,26 +233,26 @@ class NonCallableMock(Base, Any): """Filter the output of `dir(mock)` to only useful members.""" def assert_called_with(self, *args: Any, **kwargs: Any) -> None: - """assert that the last call was made with the specified arguments. + """Assert that the last call was made with the specified arguments. Raises an AssertionError if the args and keyword args passed in are different to the last call to the mock. """ def assert_not_called(self) -> None: - """assert that the mock was never called.""" + """Assert that the mock was never called.""" def assert_called_once_with(self, *args: Any, **kwargs: Any) -> None: - """assert that the mock was called exactly once and that call was + """Assert that the mock was called exactly once and that call was with the specified arguments. """ def _format_mock_failure_message(self, args: Any, kwargs: Any, action: str = "call") -> str: ... def assert_called(self) -> None: - """assert that the mock was called at least once""" + """Assert that the mock was called at least once.""" def assert_called_once(self) -> None: - """assert that the mock was called only once.""" + """Assert that the mock was called only once.""" def reset_mock(self, visited: Any = None, *, return_value: bool = False, side_effect: bool = False) -> None: """Restore the mock object to its initial state.""" @@ -271,7 +271,7 @@ class NonCallableMock(Base, Any): """ def assert_any_call(self, *args: Any, **kwargs: Any) -> None: - """assert the mock has been called with the specified arguments. + """Assert the mock has been called with the specified arguments. The assert passes if the mock has *ever* been called, unlike `assert_called_with` and `assert_called_once_with` that only pass if @@ -279,7 +279,7 @@ class NonCallableMock(Base, Any): """ def assert_has_calls(self, calls: Sequence[_Call], any_order: bool = False) -> None: - """assert the mock has been called with the specified calls. + """Assert the mock has been called with the specified calls. The `mock_calls` list is checked for the calls. If `any_order` is False (the default) then the calls must be diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.pyi index 9b3c4b912b..48305aa02d 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/urllib/request.pyi @@ -75,8 +75,8 @@ from email.message import Message from http.client import HTTPConnection, HTTPMessage, HTTPResponse from http.cookiejar import CookieJar from re import Pattern -from typing import IO, Any, ClassVar, Literal, NoReturn, Protocol, TypeAlias, TypeVar, overload, type_check_only -from typing_extensions import deprecated +from typing import IO, Any, ClassVar, Literal, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Never, deprecated from urllib.error import HTTPError as HTTPError from urllib.response import addclosehook, addinfourl @@ -587,7 +587,7 @@ class CacheFTPHandler(FTPHandler): def clear_cache(self) -> None: ... # undocumented class UnknownHandler(BaseHandler): - def unknown_open(self, req: Request) -> NoReturn: ... + def unknown_open(self, req: Request) -> Never: ... class HTTPErrorProcessor(BaseHandler): """Process HTTP error responses.""" diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/uuid.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/uuid.pyi index 5def3196a8..e032e36d01 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/uuid.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/uuid.pyi @@ -60,8 +60,8 @@ import builtins import sys from _typeshed import Unused from enum import Enum -from typing import Final, NoReturn, TypeAlias -from typing_extensions import LiteralString +from typing import Final, TypeAlias +from typing_extensions import LiteralString, Never _FieldsType: TypeAlias = tuple[int, int, int, int, int, int] @@ -215,7 +215,7 @@ class UUID: def __gt__(self, other: UUID) -> bool: ... def __ge__(self, other: UUID) -> bool: ... def __hash__(self) -> builtins.int: ... - def __setattr__(self, name: Unused, value: Unused) -> NoReturn: ... + def __setattr__(self, name: Unused, value: Unused) -> Never: ... def getnode() -> int: """Get the hardware address as a 48-bit positive integer. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/wave.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wave.pyi index b40845a437..4bff5402b0 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/wave.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/wave.pyi @@ -74,8 +74,8 @@ is destroyed. import sys from _typeshed import ReadableBuffer, StrOrBytesPath, Unused -from typing import IO, Any, BinaryIO, Final, Literal, NamedTuple, NoReturn, TypeAlias, overload -from typing_extensions import Self, deprecated +from typing import IO, Any, BinaryIO, Final, Literal, NamedTuple, TypeAlias, overload +from typing_extensions import Never, Self, deprecated __all__ = ["open", "Error", "Wave_read", "Wave_write"] if sys.version_info >= (3, 15): @@ -157,10 +157,10 @@ class Wave_read: def getcompname(self) -> str: ... def getparams(self) -> _wave_params: ... if sys.version_info < (3, 15): - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") + @deprecated("Deprecated; will be removed in Python 3.15.") def getmarkers(self) -> None: ... - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") - def getmark(self, id: Any) -> NoReturn: ... + @deprecated("Deprecated; will be removed in Python 3.15.") + def getmark(self, id: Any) -> Never: ... def setpos(self, pos: int) -> None: ... def readframes(self, nframes: int) -> bytes: ... @@ -222,11 +222,11 @@ class Wave_write: def getparams(self) -> _wave_params: ... if sys.version_info < (3, 15): - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") - def setmark(self, id: Any, pos: Any, name: Any) -> NoReturn: ... - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") - def getmark(self, id: Any) -> NoReturn: ... - @deprecated("Deprecated since Python 3.13; will be removed in Python 3.15.") + @deprecated("Deprecated; will be removed in Python 3.15.") + def setmark(self, id: Any, pos: Any, name: Any) -> Never: ... + @deprecated("Deprecated; will be removed in Python 3.15.") + def getmark(self, id: Any) -> Never: ... + @deprecated("Deprecated; will be removed in Python 3.15.") def getmarkers(self) -> None: ... def tell(self) -> int: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/webbrowser.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/webbrowser.pyi index 060054063e..4aff0e567c 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/webbrowser.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/webbrowser.pyi @@ -116,7 +116,7 @@ if sys.platform == "win32": if sys.platform == "darwin": if sys.version_info < (3, 13): - @deprecated("Deprecated since Python 3.11; removed in Python 3.13.") + @deprecated("Deprecated; removed in Python 3.13.") class MacOSX(BaseBrowser): """Launcher class for Aqua browsers on Mac OS X diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/validate.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/validate.pyi index 9d6c0d79dd..b09a76f452 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/validate.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/wsgiref/validate.pyi @@ -107,7 +107,8 @@ Some of the things this checks: from _typeshed.wsgi import ErrorStream, InputStream, WSGIApplication from collections.abc import Callable, Iterable, Iterator -from typing import Any, NoReturn, TypeAlias +from typing import Any, TypeAlias +from typing_extensions import Never __all__ = ["validator"] @@ -134,7 +135,7 @@ class InputWrapper: def readline(self, size: int = ...) -> bytes: ... def readlines(self, hint: int = ...) -> bytes: ... def __iter__(self) -> Iterator[bytes]: ... - def close(self) -> NoReturn: ... + def close(self) -> Never: ... class ErrorWrapper: errors: ErrorStream @@ -142,7 +143,7 @@ class ErrorWrapper: def write(self, s: str) -> None: ... def flush(self) -> None: ... def writelines(self, seq: Iterable[str]) -> None: ... - def close(self) -> NoReturn: ... + def close(self) -> Never: ... _WriterCallback: TypeAlias = Callable[[bytes], Any] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi index a40866c848..256816ef98 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/expatbuilder.pyi @@ -5,7 +5,8 @@ This avoids all the overhead of SAX and pulldom to gain performance. """ from _typeshed import ReadableBuffer, SupportsRead -from typing import Any, Final, NoReturn, TypeAlias +from typing import Any, Final, TypeAlias +from typing_extensions import Never from xml.dom.minidom import Document, DocumentFragment, DOMImplementation, Element, Node, TypeInfo from xml.dom.xmlbuilder import DOMBuilderFilter, Options from xml.parsers.expat import XMLParserType @@ -176,8 +177,8 @@ class InternalSubsetExtractor(ExpatBuilder): def start_doctype_decl_handler( # type: ignore[override] self, name: str, publicId: str | None, systemId: str | None, has_internal_subset: bool ) -> None: ... - def end_doctype_decl_handler(self) -> NoReturn: ... - def start_element_handler(self, name: str, attrs: list[str]) -> NoReturn: ... + def end_doctype_decl_handler(self) -> Never: ... + def start_element_handler(self, name: str, attrs: list[str]) -> Never: ... def parse(file: str | SupportsRead[ReadableBuffer | str], namespaces: bool = True) -> Document: """Parse a document, returning the resulting Document node. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minidom.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minidom.pyi index 1bd6989d17..7c718491f5 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minidom.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/minidom.pyi @@ -20,8 +20,8 @@ from _collections_abc import dict_keys, dict_values from _typeshed import Incomplete, ReadableBuffer, SupportsRead, SupportsWrite from collections.abc import Iterable, Sequence from types import TracebackType -from typing import Any, ClassVar, Generic, Literal, NoReturn, Protocol, TypeAlias, TypeVar, overload, type_check_only -from typing_extensions import Self +from typing import Any, ClassVar, Generic, Literal, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing_extensions import Never, Self from xml.dom.minicompat import EmptyNodeList, NodeList from xml.dom.xmlbuilder import DocumentLS, DOMImplementationLS from xml.sax.xmlreader import XMLReader @@ -452,14 +452,14 @@ class Childless: def firstChild(self) -> None: ... @property def lastChild(self) -> None: ... - def appendChild(self, node: _NodesThatAreChildren | DocumentFragment) -> NoReturn: ... + def appendChild(self, node: _NodesThatAreChildren | DocumentFragment) -> Never: ... def hasChildNodes(self) -> Literal[False]: ... def insertBefore( self, newChild: _NodesThatAreChildren | DocumentFragment, refChild: _NodesThatAreChildren | None - ) -> NoReturn: ... - def removeChild(self, oldChild: _NodesThatAreChildren) -> NoReturn: ... + ) -> Never: ... + def removeChild(self, oldChild: _NodesThatAreChildren) -> Never: ... def normalize(self) -> None: ... - def replaceChild(self, newChild: _NodesThatAreChildren | DocumentFragment, oldChild: _NodesThatAreChildren) -> NoReturn: ... + def replaceChild(self, newChild: _NodesThatAreChildren | DocumentFragment, oldChild: _NodesThatAreChildren) -> Never: ... class ProcessingInstruction(Childless, Node): __slots__ = ("target", "data") @@ -596,10 +596,10 @@ class ReadOnlySequentialNamedNodeMap(Generic[_N]): def getNamedItemNS(self, namespaceURI: str | None, localName: str) -> _N | None: ... def __getitem__(self, name_or_tuple: str | _NSName) -> _N | None: ... def item(self, index: int) -> _N | None: ... - def removeNamedItem(self, name: str) -> NoReturn: ... - def removeNamedItemNS(self, namespaceURI: str | None, localName: str) -> NoReturn: ... - def setNamedItem(self, node: Node) -> NoReturn: ... - def setNamedItemNS(self, node: Node) -> NoReturn: ... + def removeNamedItem(self, name: str) -> Never: ... + def removeNamedItemNS(self, namespaceURI: str | None, localName: str) -> Never: ... + def setNamedItem(self, node: Node) -> Never: ... + def setNamedItemNS(self, node: Node) -> Never: ... @property def length(self) -> int: """Number of entries in the NamedNodeMap.""" @@ -671,10 +671,10 @@ class Entity(Identified, Node): notationName: str | None def __init__(self, name: str, publicId: str | None, systemId: str | None, notation: str | None) -> None: ... - def appendChild(self, newChild: _EntityChildren) -> NoReturn: ... # type: ignore[override] - def insertBefore(self, newChild: _EntityChildren, refChild: _EntityChildren | None) -> NoReturn: ... # type: ignore[override] - def removeChild(self, oldChild: _EntityChildren) -> NoReturn: ... # type: ignore[override] - def replaceChild(self, newChild: _EntityChildren, oldChild: _EntityChildren) -> NoReturn: ... # type: ignore[override] + def appendChild(self, newChild: _EntityChildren) -> Never: ... # type: ignore[override] + def insertBefore(self, newChild: _EntityChildren, refChild: _EntityChildren | None) -> Never: ... # type: ignore[override] + def removeChild(self, oldChild: _EntityChildren) -> Never: ... # type: ignore[override] + def replaceChild(self, newChild: _EntityChildren, oldChild: _EntityChildren) -> Never: ... # type: ignore[override] class Notation(Identified, Childless, Node): nodeType: ClassVar[Literal[12]] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.pyi index d75daed9e6..39b122ed57 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/pulldom.pyi @@ -1,8 +1,8 @@ import sys from _typeshed import Incomplete, Unused from collections.abc import MutableSequence, Sequence -from typing import Final, Literal, NoReturn, TypeAlias -from typing_extensions import Self +from typing import Final, Literal, TypeAlias +from typing_extensions import Never, Self from xml.dom.minidom import Comment, Document, DOMImplementation, Element, ProcessingInstruction, Text from xml.sax import _SupportsReadClose from xml.sax.handler import ContentHandler @@ -74,8 +74,8 @@ class PullDOM(ContentHandler): class ErrorHandler: def warning(self, exception: BaseException) -> None: ... - def error(self, exception: BaseException) -> NoReturn: ... - def fatalError(self, exception: BaseException) -> NoReturn: ... + def error(self, exception: BaseException) -> Never: ... + def fatalError(self, exception: BaseException) -> Never: ... class DOMEventStream: stream: _SupportsReadClose[bytes] | _SupportsReadClose[str] diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi index 837803c472..777555eff1 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/dom/xmlbuilder.pyi @@ -1,7 +1,8 @@ """Implementation of the DOM Level 3 'LS-Load' feature.""" from _typeshed import SupportsRead -from typing import Any, Final, Literal, NoReturn +from typing import Any, Final, Literal +from typing_extensions import Never from xml.dom.minidom import Document, Node, _DOMErrorHandler __all__ = ["DOMBuilder", "DOMEntityResolver", "DOMInputSource"] @@ -49,7 +50,7 @@ class DOMBuilder: def getFeature(self, name: str) -> Any: ... def parseURI(self, uri: str) -> Document: ... def parse(self, input: DOMInputSource) -> Document: ... - def parseWithContext(self, input: DOMInputSource, cnode: Node, action: Literal[1, 2, 3, 4]) -> NoReturn: ... + def parseWithContext(self, input: DOMInputSource, cnode: Node, action: Literal[1, 2, 3, 4]) -> Never: ... class DOMEntityResolver: __slots__ = ("_opener",) @@ -82,14 +83,14 @@ class DocumentLS: """Mixin to create documents that conform to the load/save spec.""" async_: bool - def abort(self) -> NoReturn: ... - def load(self, uri: str) -> NoReturn: ... - def loadXML(self, source: str) -> NoReturn: ... + def abort(self) -> Never: ... + def load(self, uri: str) -> Never: ... + def loadXML(self, source: str) -> Never: ... def saveXML(self, snode: Node | None) -> str: ... class DOMImplementationLS: MODE_SYNCHRONOUS: Final = 1 MODE_ASYNCHRONOUS: Final = 2 def createDOMBuilder(self, mode: Literal[1], schemaType: None) -> DOMBuilder: ... - def createDOMWriter(self) -> NoReturn: ... + def createDOMWriter(self) -> Never: ... def createDOMInputSource(self) -> DOMInputSource: ... diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi index 1803a2abc1..19de577279 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/_exceptions.pyi @@ -1,6 +1,6 @@ """Different kinds of SAX Exceptions""" -from typing import NoReturn +from typing_extensions import Never from xml.sax.xmlreader import Locator class SAXException(Exception): @@ -25,7 +25,7 @@ class SAXException(Exception): def getException(self) -> Exception | None: """Return the embedded exception, or None if there was none.""" - def __getitem__(self, ix: object) -> NoReturn: + def __getitem__(self, ix: object) -> Never: """Avoids weird error messages if someone does exception[ix] by mistake, since Exception has __getitem__ defined. """ diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/handler.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/handler.pyi index f2bfd2c519..7297b67f51 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/handler.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/handler.pyi @@ -9,15 +9,16 @@ of the interfaces. $Id$ """ -from typing import Final, NoReturn, Protocol, type_check_only +from typing import Final, Protocol, type_check_only +from typing_extensions import Never from xml.sax import xmlreader version: Final[str] @type_check_only class _ErrorHandlerProtocol(Protocol): # noqa: Y046 # Protocol is not used - def error(self, exception: BaseException) -> NoReturn: ... - def fatalError(self, exception: BaseException) -> NoReturn: ... + def error(self, exception: BaseException) -> Never: ... + def fatalError(self, exception: BaseException) -> Never: ... def warning(self, exception: BaseException) -> None: ... class ErrorHandler: @@ -31,10 +32,10 @@ class ErrorHandler: SAXParseException as the only parameter. """ - def error(self, exception: BaseException) -> NoReturn: + def error(self, exception: BaseException) -> Never: """Handle a recoverable error.""" - def fatalError(self, exception: BaseException) -> NoReturn: + def fatalError(self, exception: BaseException) -> Never: """Handle a non-recoverable error.""" def warning(self, exception: BaseException) -> None: diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.pyi index 9873218936..456a993c56 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/xml/sax/saxutils.pyi @@ -6,7 +6,8 @@ from _typeshed import SupportsWrite from codecs import StreamReaderWriter, StreamWriter from collections.abc import Mapping from io import RawIOBase, TextIOBase -from typing import Literal, NoReturn +from typing import Literal +from typing_extensions import Never from xml.sax import _Source, handler, xmlreader def escape(data: str, entities: Mapping[str, str] = {}) -> str: @@ -70,8 +71,8 @@ class XMLFilterBase(xmlreader.XMLReader): def __init__(self, parent: xmlreader.XMLReader | None = None) -> None: ... # ErrorHandler methods - def error(self, exception: BaseException) -> NoReturn: ... - def fatalError(self, exception: BaseException) -> NoReturn: ... + def error(self, exception: BaseException) -> Never: ... + def fatalError(self, exception: BaseException) -> Never: ... def warning(self, exception: BaseException) -> None: ... # ContentHandler methods def setDocumentLocator(self, locator: xmlreader.Locator) -> None: ... From 9ef3f9123ea3e639e773ecc58f10cccfc26b2486 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Sat, 1 Aug 2026 18:53:50 +0500 Subject: [PATCH 202/390] [`ruff_python_ast`] Fix double visit / missing elif visit (#27404) --- .../src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs | 5 +---- crates/ruff_python_ast/src/visitor.rs | 5 +---- crates/ruff_python_ast/src/visitor/transformer.rs | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs index e7133dffbc..96a64093c6 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs @@ -436,10 +436,7 @@ impl<'a> Visitor<'a> for LoopMutationsVisitor<'a> { // Handle the `elif` and `else` branches. for clause in elif_else_clauses { self.enter_new_branch(); - if let Some(test) = &clause.test { - self.visit_expr(test); - } - self.visit_body(&clause.body); + self.visit_elif_else_clause(clause); self.merge_branch_into(saved_branch); } } diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 9e90f6be8b..8fa8cdfa9d 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -265,10 +265,7 @@ pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) { visitor.visit_expr(test); visitor.visit_body(body); for clause in elif_else_clauses { - if let Some(test) = &clause.test { - visitor.visit_expr(test); - } - walk_elif_else_clause(visitor, clause); + visitor.visit_elif_else_clause(clause); } } Stmt::With(ast::StmtWith { items, body, .. }) => { diff --git a/crates/ruff_python_ast/src/visitor/transformer.rs b/crates/ruff_python_ast/src/visitor/transformer.rs index 7a0246403d..7b1d2098e3 100644 --- a/crates/ruff_python_ast/src/visitor/transformer.rs +++ b/crates/ruff_python_ast/src/visitor/transformer.rs @@ -252,7 +252,7 @@ pub fn walk_stmt(visitor: &V, stmt: &mut Stmt) { visitor.visit_expr(test); visitor.visit_body(body); for clause in elif_else_clauses { - walk_elif_else_clause(visitor, clause); + visitor.visit_elif_else_clause(clause); } } Stmt::With(ast::StmtWith { items, body, .. }) => { From ee278f44e0beeaed5798413c345b755322b4f80a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 1 Aug 2026 12:21:27 -0400 Subject: [PATCH 203/390] Restore symmetric public APIs (#27405) ## Summary This follows up on #27339 based on review feedback. - Remove internal re-exports instead of retaining them with `#[expect(unused_imports)]`. - Restore the symmetric `Edit` and `NoqaCode` accessors to public visibility. - Keep `EditOperationKind`'s helper methods private along with their containing type. The public accessors are retained as coherent downstream-facing APIs even when individual methods are not used within the workspace. Hawk will document those intentional exceptions in the separate CI/configuration PR. --- crates/ruff_diagnostics/src/edit.rs | 14 ++++++-------- crates/ruff_formatter/src/lib.rs | 2 -- crates/ruff_linter/src/codes.rs | 5 ++--- crates/ty_python_semantic/src/lib.rs | 2 -- crates/ty_python_semantic/src/types.rs | 5 +---- 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/crates/ruff_diagnostics/src/edit.rs b/crates/ruff_diagnostics/src/edit.rs index 06dfc69454..f008e64e9c 100644 --- a/crates/ruff_diagnostics/src/edit.rs +++ b/crates/ruff_diagnostics/src/edit.rs @@ -77,22 +77,20 @@ impl Edit { } /// Returns `true` if this edit deletes content from the source document. - #[expect(dead_code)] #[inline] - pub(crate) fn is_deletion(&self) -> bool { + pub fn is_deletion(&self) -> bool { self.kind().is_deletion() } /// Returns `true` if this edit inserts new content into the source document. #[inline] - pub(crate) fn is_insertion(&self) -> bool { + pub fn is_insertion(&self) -> bool { self.kind().is_insertion() } /// Returns `true` if this edit replaces some existing content with new content. - #[expect(dead_code)] #[inline] - pub(crate) fn is_replacement(&self) -> bool { + pub fn is_replacement(&self) -> bool { self.kind().is_replacement() } } @@ -131,15 +129,15 @@ enum EditOperationKind { } impl EditOperationKind { - pub(crate) const fn is_insertion(self) -> bool { + const fn is_insertion(self) -> bool { matches!(self, EditOperationKind::Insertion) } - pub(crate) const fn is_deletion(self) -> bool { + const fn is_deletion(self) -> bool { matches!(self, EditOperationKind::Deletion) } - pub(crate) const fn is_replacement(self) -> bool { + const fn is_replacement(self) -> bool { matches!(self, EditOperationKind::Replacement) } } diff --git a/crates/ruff_formatter/src/lib.rs b/crates/ruff_formatter/src/lib.rs index 319c994ce4..db299afce2 100644 --- a/crates/ruff_formatter/src/lib.rs +++ b/crates/ruff_formatter/src/lib.rs @@ -50,8 +50,6 @@ pub use builders::BestFitting; pub use source_code::{SourceCode, SourceCodeSlice}; pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, PrintError}; -#[expect(unused_imports)] -pub(crate) use format_element::LINE_TERMINATORS; pub use format_element::{FormatElement, normalize_newlines}; pub use group_id::GroupId; use ruff_macros::CacheKey; diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 106382d833..645b9266a2 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -16,13 +16,12 @@ pub struct NoqaCode(&'static str, &'static str); impl NoqaCode { /// Return the prefix for the [`NoqaCode`], e.g., `SIM` for `SIM101`. - #[expect(dead_code)] - pub(crate) fn prefix(&self) -> &str { + pub fn prefix(&self) -> &str { self.0 } /// Return the suffix for the [`NoqaCode`], e.g., `101` for `SIM101`. - pub(crate) fn suffix(&self) -> &str { + pub fn suffix(&self) -> &str { self.1 } diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 90816215fd..31ec9b0657 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -17,8 +17,6 @@ use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_db::source::{SourceTextError, source_text}; use rustc_hash::FxHasher; -#[expect(unused_imports)] -pub(crate) use semantic_model::HasOptionalDefinition; pub use semantic_model::{ Completion, ExpectedStringLiteralCompletion, HasDefinition, HasType, MemberDefinition, NameKind, SemanticModel, diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e7bb25cdc6..dc684dfc74 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -44,14 +44,11 @@ pub(crate) use self::match_pattern::{ }; pub(crate) use self::relation_error::{ErrorContext, ErrorContextTree, ParameterDescription}; use self::set_theoretic::KnownUnion; +use self::set_theoretic::NegativeIntersectionElements; pub(crate) use self::set_theoretic::builder::{ IntersectionBuilder, UnionAccumulator, UnionBuilder, }; pub use self::set_theoretic::{IntersectionType, UnionType}; -#[expect(unused_imports)] -pub(crate) use self::set_theoretic::{ - NegativeIntersectionElements, NegativeIntersectionElementsIterator, -}; pub use self::signatures::ParameterKind; pub(crate) use self::signatures::Signature; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; From 64737b2a5c437934d0cebb88d1ea39addb34708a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 1 Aug 2026 12:37:51 -0400 Subject: [PATCH 204/390] Remove unused APIs from ruff_notebook (#27386) ## Summary This PR removes unused APIs from ruff_notebook identified by Hawk 0.1.11. It is split from #27339 so the removals can be reviewed independently at the crate boundary. - 4 net lines removed - 4 deletions and 0 additions - 1 files changed --- crates/ruff_notebook/src/notebook.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/ruff_notebook/src/notebook.rs b/crates/ruff_notebook/src/notebook.rs index 76f8fa854f..37d32214a4 100644 --- a/crates/ruff_notebook/src/notebook.rs +++ b/crates/ruff_notebook/src/notebook.rs @@ -443,10 +443,6 @@ impl Notebook { &self.raw.cells } - pub fn metadata(&self) -> &RawNotebookMetadata { - &self.raw.metadata - } - /// Check if it's a Python notebook. /// /// This is determined by checking the `language_info` or `kernelspec` in the notebook From 1fcdb82be0169fc79f492f03728f52215af563c0 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 1 Aug 2026 12:45:56 -0400 Subject: [PATCH 205/390] Move formatter line terminators into test module (#27384) ## Summary This PR moves `LINE_TERMINATORS` and its component constants into the `format_element` test module, as suggested in the review of #27339. These constants are only used by the newline-normalization tests, so they do not need to be part of the public API of `ruff_formatter`. - 1 public constant removed - 4 additions and 5 deletions - 1 file changed --- crates/ruff_formatter/src/format_element.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/ruff_formatter/src/format_element.rs b/crates/ruff_formatter/src/format_element.rs index 719e62ca30..f3e5450d12 100644 --- a/crates/ruff_formatter/src/format_element.rs +++ b/crates/ruff_formatter/src/format_element.rs @@ -191,10 +191,6 @@ impl Deref for Interned { } } -const LINE_SEPARATOR: char = '\u{2028}'; -const PARAGRAPH_SEPARATOR: char = '\u{2029}'; -pub const LINE_TERMINATORS: [char; 3] = ['\r', LINE_SEPARATOR, PARAGRAPH_SEPARATOR]; - /// Replace the line terminators matching the provided list with "\n" /// since its the only line break type supported by the printer pub fn normalize_newlines(text: &str, terminators: [char; N]) -> Cow<'_, str> { @@ -560,9 +556,12 @@ impl TextWidth { #[cfg(test)] mod tests { + const LINE_SEPARATOR: char = '\u{2028}'; + const PARAGRAPH_SEPARATOR: char = '\u{2029}'; + const LINE_TERMINATORS: [char; 3] = ['\r', LINE_SEPARATOR, PARAGRAPH_SEPARATOR]; use crate::IndentWidth; - use crate::format_element::{LINE_TERMINATORS, TextWidth, normalize_newlines}; + use crate::format_element::{TextWidth, normalize_newlines}; #[test] fn text_width() { From 6002a07c8b9389f898971a284351e371b2b2d912 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 1 Aug 2026 12:54:35 -0400 Subject: [PATCH 206/390] Remove unused APIs from ruff_source_file (#27392) ## Summary This PR removes unused APIs from ruff_source_file identified by Hawk 0.1.11. It is split from #27339 so the removals can be reviewed independently at the crate boundary. - 31 net lines removed - 33 deletions and 2 additions - 1 files changed This preserves the general-purpose OneIndexed checked arithmetic APIs called out in review. --- crates/ruff_source_file/src/lib.rs | 35 ++---------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/crates/ruff_source_file/src/lib.rs b/crates/ruff_source_file/src/lib.rs index 64121dac56..23f07de888 100644 --- a/crates/ruff_source_file/src/lib.rs +++ b/crates/ruff_source_file/src/lib.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, OnceLock}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use ruff_text_size::{Ranged, TextRange, TextSize}; +use ruff_text_size::{Ranged, TextSize}; pub use crate::line_index::{LineIndex, OneIndexed, PositionEncoding}; pub use crate::line_ranges::LineRanges; @@ -55,19 +55,7 @@ impl<'src, 'index> SourceCode<'src, 'index> { self.index.line_index(offset) } - /// Take the source code up to the given [`TextSize`]. - #[inline] - pub fn up_to(&self, offset: TextSize) -> &'src str { - &self.text[TextRange::up_to(offset)] - } - - /// Take the source code after the given [`TextSize`]. - #[inline] - pub fn after(&self, offset: TextSize) -> &'src str { - &self.text[usize::from(offset)..] - } - - /// Take the source code between the given [`TextRange`]. + /// Take the source code between the given [`ruff_text_size::TextRange`]. pub fn slice(&self, ranged: T) -> &'src str { &self.text[ranged.range()] } @@ -84,10 +72,6 @@ impl<'src, 'index> SourceCode<'src, 'index> { self.index.line_end_exclusive(line, self.text) } - pub fn line_range(&self, line: OneIndexed) -> TextRange { - self.index.line_range(line, self.text) - } - /// Returns the source text of the line with the given index #[inline] pub fn line_text(&self, index: OneIndexed) -> &'src str { @@ -132,16 +116,6 @@ impl SourceFileBuilder { } } - #[must_use] - pub fn line_index(mut self, index: LineIndex) -> Self { - self.index = Some(index); - self - } - - pub fn set_line_index(&mut self, index: LineIndex) { - self.index = Some(index); - } - /// Consumes `self` and returns the [`SourceFile`]. pub fn finish(self) -> SourceFile { let index = if let Some(index) = self.index { @@ -185,11 +159,6 @@ impl SourceFile { &self.inner.name } - #[inline] - pub fn slice(&self, range: TextRange) -> &str { - &self.source_text()[range] - } - pub fn to_source_code(&self) -> SourceCode<'_, '_> { SourceCode { text: self.source_text(), From c4fb1f4f5790a398331197aeb2c4125e2efe184b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 1 Aug 2026 12:56:18 -0400 Subject: [PATCH 207/390] Remove unused ClassMemberBoundness predicates (#27389) ## Summary This PR removes the unused `ClassMemberBoundness::{is_bound, is_possibly_unbound}` predicates identified by Hawk 0.1.11. The other APIs and CFG visualization support considered in #27339 remain unchanged. - 2 unused public methods removed - 10 deletions - 1 file changed --- crates/ruff_python_semantic/src/analyze/class.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/ruff_python_semantic/src/analyze/class.rs b/crates/ruff_python_semantic/src/analyze/class.rs index 76855dc15d..dbefd24eba 100644 --- a/crates/ruff_python_semantic/src/analyze/class.rs +++ b/crates/ruff_python_semantic/src/analyze/class.rs @@ -157,16 +157,6 @@ pub enum ClassMemberBoundness { Bound, } -impl ClassMemberBoundness { - pub const fn is_bound(self) -> bool { - matches!(self, Self::Bound) - } - - pub const fn is_possibly_unbound(self) -> bool { - matches!(self, Self::PossiblyUnbound) - } -} - #[derive(Copy, Clone, Debug)] pub enum ClassMemberKind<'a> { Assign(&'a ast::StmtAssign), From 2742f956d87314cf1522c69c9a95a71d972bd76c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 1 Aug 2026 13:11:12 -0400 Subject: [PATCH 208/390] [ty] Remove unused TypeVarVariance bounds (#27408) ## Summary This PR removes the unused TypeVarVariance bottom and top constructors identified by Hawk 0.1.11. The variance lattice is implemented through the internal join operation, and no Ruff or ty code calls these public constructors. - 2 unused public methods removed - 8 lines removed - 1 file changed --- crates/ty_python_semantic/src/types/variance.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/ty_python_semantic/src/types/variance.rs b/crates/ty_python_semantic/src/types/variance.rs index 21a3253dbf..159ebe6561 100644 --- a/crates/ty_python_semantic/src/types/variance.rs +++ b/crates/ty_python_semantic/src/types/variance.rs @@ -9,14 +9,6 @@ pub enum TypeVarVariance { } impl TypeVarVariance { - pub const fn bottom() -> Self { - TypeVarVariance::Bivariant - } - - pub const fn top() -> Self { - TypeVarVariance::Invariant - } - // supremum #[must_use] pub(crate) const fn join(self, other: Self) -> Self { From e31dffc99317d5dde5773e3e8ed99a06e53b62e3 Mon Sep 17 00:00:00 2001 From: BitWeaver Date: Sun, 2 Aug 2026 21:01:55 +0200 Subject: [PATCH 209/390] [ty] Detect `__aenter__` and `__aexit__` that do not return awaitables (#27414) ## Summary `async with` awaits whatever `__aenter__` and `__aexit__` return, so a method that is callable but returns a non-awaitable still fails at runtime: ```python class C: def __aenter__(self) -> int: return 0 async def __aexit__(self, exc_type, exc, tb) -> None: ... async def f() -> None: async with C() as x: # TypeError: object int can't be used in 'await' expression ... ``` ty accepted both of these. `try_enter_with_mode` awaited the `__aenter__` return type with `try_await(db).unwrap_or(Type::unknown())`, discarding the failure and silently binding the target to `Unknown`, and it ignored the `__aexit__` return type entirely. Both return types are now awaited in async mode, and a new `ContextManagerError::NotAwaitable` variant reports the methods at fault along with the offending return type: ``` error[invalid-context-manager]: Object of type `C` cannot be used with `async with` because `__aenter__` does not return an awaitable --> example.py:14:16 | 14 | async with C() as x: | ^^^ info: `__aenter__` returns `int`, which is not awaitable info: Consider declaring the method with `async def` ``` `NonAwaitableMethods` is an enum rather than a collection so that "at least one method is at fault" cannot be represented as an empty set. The hint suggesting a switch between `with` and `async with` is suppressed for this variant. The object does implement the methods for the mode it was used in, so that suggestion would be misleading. Sync `with` is unaffected, since it does not await what `__enter__` and `__exit__` return. Closes astral-sh/ty#4124 ## Test Plan New cases in `with/async.md` cover a non-awaitable `__aenter__` (with a snapshot of the full diagnostic), a non-awaitable `__aexit__`, and both failing together. Since the risk here is false positives on valid context managers, the following are asserted to remain clean: - methods that return an awaitable without being `async def`, spelled as `Awaitable[T]`, as `Coroutine[...]`, and as a custom class implementing `__await__` - union return types where every member is awaitable, and `Never` - return types that are `Any` or unannotated A mixed union (`int | Awaitable[int]`) is asserted to still be an error, since awaiting the non-awaitable arm fails. Also verified manually, unchanged by this PR: `asyncio.timeout`, `asyncio.TaskGroup`, `@asynccontextmanager`, protocol-typed context managers, inherited `__aenter__`/`__aexit__`, and sync `with`. A possibly-unbound `__aenter__` continues to report the pre-existing diagnostic rather than double-reporting. Checks run: - `cargo test -p ty_python_semantic`: 297 + 14 unit tests and 479 mdtests pass - `cargo clippy --workspace --all-targets --all-features -- -D warnings`: clean - `uv run --only-group dev --locked prek run --files ...`: passes - `cargo dev generate-all`: no changes (this reuses the existing `invalid-context-manager` rule) --------- Co-authored-by: Charlie Marsh --- ...ion_w\342\200\246_(28ef812089a32e6a).snap" | 4 +- .../resources/mdtest/with/async.md | 285 +++++++++++++++++- .../src/types/context_manager.rs | 219 +++++++++++++- 3 files changed, 486 insertions(+), 22 deletions(-) diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/async.md_-_Async_with_statement\342\200\246_-_Context_expression_w\342\200\246_(28ef812089a32e6a).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/async.md_-_Async_with_statement\342\200\246_-_Context_expression_w\342\200\246_(28ef812089a32e6a).snap" index 856f10ff3f..3e94e3a0db 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/async.md_-_Async_with_statement\342\200\246_-_Context_expression_w\342\200\246_(28ef812089a32e6a).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/async.md_-_Async_with_statement\342\200\246_-_Context_expression_w\342\200\246_(28ef812089a32e6a).snap" @@ -14,10 +14,10 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/with/async.md ``` 1 | class Manager1: - 2 | def __aenter__(self) -> str: + 2 | async def __aenter__(self) -> str: 3 | return "foo" 4 | - 5 | def __aexit__(self, exc_type, exc_value, traceback): ... + 5 | async def __aexit__(self, exc_type, exc_value, traceback): ... 6 | 7 | class NotAContextManager: ... 8 | diff --git a/crates/ty_python_semantic/resources/mdtest/with/async.md b/crates/ty_python_semantic/resources/mdtest/with/async.md index 01ff963076..7a745c9db7 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/async.md +++ b/crates/ty_python_semantic/resources/mdtest/with/async.md @@ -2,9 +2,7 @@ ## Basic `async with` statement -The type of the target variable in a `with` statement should be the return type from the context -manager's `__aenter__` method. However, `async with` statements aren't supported yet. This test -asserts that it doesn't emit any context manager-related errors. +An `async with` statement awaits the return value of `__aenter__` and binds the result. ```py class Target: ... @@ -104,12 +102,15 @@ async def main(): +A union can contain a valid context manager and an object with no context-manager methods. The valid +manager still determines the type of the value bound by `async with`. + ```py class Manager1: - def __aenter__(self) -> str: + async def __aenter__(self) -> str: return "foo" - def __aexit__(self, exc_type, exc_value, traceback): ... + async def __aexit__(self, exc_type, exc_value, traceback): ... class NotAContextManager: ... @@ -119,7 +120,43 @@ async def _(context_expr: Manager1 | NotAContextManager): reveal_type(f) # revealed: str ``` -## Context expression with "sometimes" callable `__aenter__` method +## Missing and non-awaitable methods in a union + +If one member of a union does not define the context-manager methods, still check the return values +of the methods defined on the other member. + +```py +class Manager: + def __aenter__(self) -> int: + return 0 + + def __aexit__(self, exc_type, exc, tb) -> bool: + return False + +class NotAManager: ... + +async def main(manager: Manager | NotAManager): + # snapshot: invalid-context-manager + async with manager as value: + reveal_type(value) # revealed: Unknown +``` + +```snapshot +error[invalid-context-manager]: Object of type `Manager | NotAManager` cannot be used with `async with` because `__aenter__` and `__aexit__` may be missing or return non-awaitables + --> src/mdtest_snippet.py:12:16 + | +12 | async with manager as value: + | ^^^^^^^ +info: `NotAManager` does not implement `__aenter__` or `__aexit__` +info: `__aenter__` returns `int`, which is not awaitable +info: `__aexit__` returns `bool`, which is not awaitable +info: Consider declaring the methods with `async def` +``` + +## Conditionally defined `__aenter__` method + +A conditionally defined `__aenter__` method may be missing. When it exists, its awaited return type +still determines the type of the bound value. ```py async def _(flag: bool): @@ -132,7 +169,7 @@ async def _(flag: bool): # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because the method `__aenter__` may be missing" async with Manager() as f: - reveal_type(f) # revealed: CoroutineType[Any, Any, str] + reveal_type(f) # revealed: str ``` ## Invalid `__aenter__` signature @@ -152,10 +189,10 @@ async def main(): reveal_type(f) # revealed: CoroutineType[Any, Any, str] ``` -## Accidental use of async `async with` +## Synchronous context manager in `async with` -If a asynchronous `async with` statement is used on a type with `__enter__` and `__exit__`, we show -a diagnostic hint that the user might have intended to use `with` instead. +An object that only defines `__enter__` and `__exit__` cannot be used with `async with`. Suggest +using `with` instead. ```py class Manager: @@ -209,6 +246,234 @@ async def main(): pass ``` +## Non-awaitable `__aenter__` + +`async with` awaits the value returned by `__aenter__`. Returning an `int` therefore raises a +`TypeError`. + +```py +class Manager: + def __aenter__(self) -> int: + return 0 + + async def __aexit__(self, exc_type, exc, tb) -> None: ... + +async def main(): + # snapshot: invalid-context-manager + async with Manager(): + pass +``` + +```snapshot +error[invalid-context-manager]: Object of type `Manager` cannot be used with `async with` because `__aenter__` does not return an awaitable + --> src/mdtest_snippet.py:9:16 + | +9 | async with Manager(): + | ^^^^^^^^^ +info: `__aenter__` returns `int`, which is not awaitable +info: Consider declaring the method with `async def` +``` + +## Non-awaitable `__aexit__` + +`async with` also awaits the value returned by `__aexit__`. The value from `__aenter__` is still +bound before the invalid exit method runs. + +```py +class Manager: + async def __aenter__(self) -> int: + return 0 + + def __aexit__(self, exc_type, exc, tb) -> bool: + return False + +async def main(): + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because `__aexit__` does not return an awaitable" + async with Manager() as value: + reveal_type(value) # revealed: int +``` + +## Non-awaitable `__aenter__` with missing `__aexit__` + +A missing exit method does not excuse an entry method that returns a non-awaitable: + +```py +class Manager: + def __aenter__(self) -> int: + return 0 + +async def main(): + # snapshot: invalid-context-manager + async with Manager(): + pass +``` + +```snapshot +error[invalid-context-manager]: Object of type `Manager` cannot be used with `async with` because it does not implement `__aexit__`, and `__aenter__` does not return an awaitable + --> src/mdtest_snippet.py:7:16 + | +7 | async with Manager(): + | ^^^^^^^^^ +info: `__aenter__` returns `int`, which is not awaitable +info: Consider declaring the method with `async def` +``` + +## Missing `__aenter__` with non-awaitable `__aexit__` + +An exit method must return an awaitable even when the entry method is missing: + +```py +class Manager: + def __aexit__(self, exc_type, exc, tb) -> bool: + return False + +async def main(): + # snapshot: invalid-context-manager + async with Manager(): + pass +``` + +```snapshot +error[invalid-context-manager]: Object of type `Manager` cannot be used with `async with` because it does not implement `__aenter__`, and `__aexit__` does not return an awaitable + --> src/mdtest_snippet.py:7:16 + | +7 | async with Manager(): + | ^^^^^^^^^ +info: `__aexit__` returns `bool`, which is not awaitable +info: Consider declaring the method with `async def` +``` + +## Non-awaitable `__aenter__` and `__aexit__` + +When neither method returns an awaitable, both are named in a single diagnostic: + +```py +class Manager: + def __aenter__(self) -> int: + return 0 + + def __aexit__(self, exc_type, exc, tb) -> bool: + return False + +async def main(): + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because `__aenter__` and `__aexit__` do not return awaitables" + async with Manager(): + pass +``` + +## Awaitable return from a regular method + +A context-manager method does not need to be declared with `async def`. A regular method can return +an `Awaitable` instead. + +```py +from typing import Awaitable + +class Manager: + def __aenter__(self) -> Awaitable[int]: + raise NotImplementedError + + def __aexit__(self, exc_type, exc, tb) -> Awaitable[None]: + raise NotImplementedError + +async def main(): + async with Manager() as value: + reveal_type(value) # revealed: int +``` + +## Awaitable return from a custom `__await__` method + +An object is awaitable when its `__await__` method returns an iterator. + +```py +from typing import Generator + +class AwaitableValue: + def __await__(self) -> Generator[None, None, int]: + raise NotImplementedError + +class Manager: + def __aenter__(self) -> AwaitableValue: + raise NotImplementedError + + def __aexit__(self, exc_type, exc, tb) -> AwaitableValue: + raise NotImplementedError + +async def main(): + async with Manager() as value: + reveal_type(value) # revealed: int +``` + +## Union of awaitable return types + +When every possible return value is awaitable, the bound value includes the awaited result from each +union member. + +```py +from typing import Awaitable + +class Manager: + def __aenter__(self) -> Awaitable[int] | Awaitable[str]: + raise NotImplementedError + + def __aexit__(self, exc_type, exc, tb) -> Awaitable[None]: + raise NotImplementedError + +async def main(): + async with Manager() as value: + reveal_type(value) # revealed: int | str +``` + +## Union containing a non-awaitable return type + +Every possible return value must be awaitable. A union containing `int` does not satisfy that +requirement. + +```py +from typing import Awaitable + +class Manager: + def __aenter__(self) -> int | Awaitable[int]: + raise NotImplementedError + + async def __aexit__(self, exc_type, exc, tb) -> None: ... + +async def main(): + # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `async with` because `__aenter__` does not return an awaitable" + async with Manager(): + pass +``` + +## `Any` return type + +A return type of `Any` might be awaitable, so it must not produce an error. + +```py +from typing import Any + +class Manager: + def __aenter__(self) -> Any: ... + def __aexit__(self, exc_type, exc, tb) -> Any: ... + +async def main(): + async with Manager(): + pass +``` + +## Unknown return type + +An unannotated return type might also be awaitable, so it must not produce an error. + +```py +class Manager: + def __aenter__(self): ... + def __aexit__(self, exc_type, exc, tb): ... + +async def main(): + async with Manager(): + pass +``` + ## `@asynccontextmanager` ```py diff --git a/crates/ty_python_semantic/src/types/context_manager.rs b/crates/ty_python_semantic/src/types/context_manager.rs index cc4f07181a..abfaec2dec 100644 --- a/crates/ty_python_semantic/src/types/context_manager.rs +++ b/crates/ty_python_semantic/src/types/context_manager.rs @@ -1,7 +1,7 @@ use crate::{ Db, FxOrderSet, types::{ - CallArguments, CallDunderError, Type, TypeContext, call::CallErrorKind, + Bindings, CallArguments, CallDunderError, Type, TypeContext, call::CallErrorKind, context::InferContext, diagnostic::INVALID_CONTEXT_MANAGER, }, }; @@ -58,23 +58,57 @@ impl<'db> Type<'db> { TypeContext::default(), ); + let awaited_enter_type = if mode.is_async() { + let return_type = |call: &Result, CallDunderError<'db>>| match call { + Ok(bindings) => Some(bindings.return_type(db)), + Err(CallDunderError::PossiblyUnbound { bindings, .. }) => { + Some(bindings.return_type(db)) + } + Err(CallDunderError::MethodNotAvailable | CallDunderError::CallError(..)) => None, + }; + + let enter_return_type = return_type(&enter); + let exit_return_type = return_type(&exit); + let awaited_enter_type = + enter_return_type.and_then(|return_type| return_type.try_await(db).ok()); + let awaited_exit_type = + exit_return_type.and_then(|return_type| return_type.try_await(db).ok()); + let non_awaitable_enter = enter_return_type.filter(|_| awaited_enter_type.is_none()); + let non_awaitable_exit = exit_return_type.filter(|_| awaited_exit_type.is_none()); + + if let Some(non_awaitable) = + NonAwaitableMethods::from_parts(non_awaitable_enter, non_awaitable_exit) + { + return Err(ContextManagerError::NotAwaitable { + enter_return_type: awaited_enter_type.unwrap_or(Type::unknown()), + non_awaitable, + enter_error: enter.err().map(Box::new), + exit_error: exit.err().map(Box::new), + }); + } + + awaited_enter_type + } else { + None + }; + // TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`). match (enter, exit) { (Ok(enter), Ok(_)) => { - let ty = enter.return_type(db); + let return_type = enter.return_type(db); Ok(if mode.is_async() { - ty.try_await(db).unwrap_or(Type::unknown()) + awaited_enter_type.unwrap_or(Type::unknown()) } else { - ty + return_type }) } (Ok(enter), Err(exit_error)) => { - let ty = enter.return_type(db); + let return_type = enter.return_type(db); Err(ContextManagerError::Exit { enter_return_type: if mode.is_async() { - ty.try_await(db).unwrap_or(Type::unknown()) + awaited_enter_type.unwrap_or(Type::unknown()) } else { - ty + return_type }, exit_error, mode, @@ -105,6 +139,57 @@ pub(super) enum ContextManagerError<'db> { exit_error: CallDunderError<'db>, mode: EvaluationMode, }, + /// At least one async context-manager method returns a non-awaitable, possibly in addition to + /// a missing or invalid method. + NotAwaitable { + /// The type bound to the `as` target, already awaited when `__aenter__` allowed it. + enter_return_type: Type<'db>, + non_awaitable: NonAwaitableMethods<'db>, + enter_error: Option>>, + exit_error: Option>>, + }, +} + +/// Which of `__aenter__` and `__aexit__` returned a value that cannot be awaited, and what each +/// of them returned. +/// +/// At least one method must be at fault for the enclosing error to exist, which is why this is an +/// enum rather than a pair of `Option`s or a collection that could be empty. +#[derive(Debug)] +pub(super) enum NonAwaitableMethods<'db> { + Enter(Type<'db>), + Exit(Type<'db>), + Both { enter: Type<'db>, exit: Type<'db> }, +} + +impl<'db> NonAwaitableMethods<'db> { + /// Builds the error description from whichever methods are at fault, or `None` if both + /// returned awaitables and there is nothing to report. + fn from_parts(enter: Option>, exit: Option>) -> Option { + match (enter, exit) { + (Some(enter), Some(exit)) => Some(Self::Both { enter, exit }), + (Some(enter), None) => Some(Self::Enter(enter)), + (None, Some(exit)) => Some(Self::Exit(exit)), + (None, None) => None, + } + } + + /// The offending return types, paired with the name of the method that returned each one. + fn named_return_types( + &self, + enter_method: &'static str, + exit_method: &'static str, + ) -> Vec<(&'static str, Type<'db>)> { + match self { + Self::Enter(enter) => vec![(enter_method, *enter)], + Self::Exit(exit) => vec![(exit_method, *exit)], + Self::Both { enter, exit } => vec![(enter_method, *enter), (exit_method, *exit)], + } + } + + const fn is_both(&self) -> bool { + matches!(self, Self::Both { .. }) + } } impl<'db> ContextManagerError<'db> { @@ -120,14 +205,24 @@ impl<'db> ContextManagerError<'db> { enter_return_type, exit_error: _, mode: _, + } + | Self::NotAwaitable { + enter_return_type, .. } => Some(*enter_return_type), - Self::Enter(enter_error, _) + Self::Enter(enter_error, mode) | Self::EnterAndExit { enter_error, exit_error: _, - mode: _, + mode, } => match enter_error { - CallDunderError::PossiblyUnbound { bindings, .. } => Some(bindings.return_type(db)), + CallDunderError::PossiblyUnbound { bindings, .. } => { + let return_type = bindings.return_type(db); + Some(if mode.is_async() { + return_type.try_await(db).unwrap_or(Type::unknown()) + } else { + return_type + }) + } CallDunderError::CallError(CallErrorKind::NotCallable, _, _) => None, CallDunderError::CallError(_, bindings, _) => Some(bindings.return_type(db)), CallDunderError::MethodNotAvailable => None, @@ -160,6 +255,8 @@ impl<'db> ContextManagerError<'db> { Self::Exit { mode, .. } | Self::Enter(_, mode) | Self::EnterAndExit { mode, .. } => { *mode } + // `NotAwaitable` is only ever constructed for `async with`. + Self::NotAwaitable { .. } => EvaluationMode::Async, }; let (enter_method, exit_method) = match mode { @@ -218,6 +315,53 @@ impl<'db> ContextManagerError<'db> { exit_error, mode: _, } => format_call_dunder_errors(enter_error, enter_method, exit_error, exit_method), + Self::NotAwaitable { + non_awaitable, + enter_error, + exit_error, + .. + } => { + let methods = non_awaitable + .named_return_types(enter_method, exit_method) + .iter() + .map(|(name, _)| format!("`{name}`")) + .collect::>() + .join(" and "); + let await_error = if non_awaitable.is_both() { + format!("{methods} do not return awaitables") + } else { + format!("{methods} does not return an awaitable") + }; + + match (enter_error.as_deref(), exit_error.as_deref()) { + ( + Some(CallDunderError::PossiblyUnbound { .. }), + Some(CallDunderError::PossiblyUnbound { .. }), + ) if non_awaitable.is_both() => { + format!( + "`{enter_method}` and `{exit_method}` may be missing or return non-awaitables" + ) + } + (Some(enter_error), Some(exit_error)) => format!( + "{}, and {await_error}", + format_call_dunder_errors( + enter_error, + enter_method, + exit_error, + exit_method + ) + ), + (Some(enter_error), None) => format!( + "{}, and {await_error}", + format_call_dunder_error(enter_error, enter_method) + ), + (None, Some(exit_error)) => format!( + "{}, and {await_error}", + format_call_dunder_error(exit_error, exit_method) + ), + (None, None) => await_error, + } + } }; // Suggest using `async with` if only async methods are available in a sync context, @@ -284,6 +428,61 @@ impl<'db> ContextManagerError<'db> { } } } + Self::NotAwaitable { + non_awaitable, + enter_error, + exit_error, + .. + } => { + let enter_unbound_on = enter_error + .as_deref() + .map_or_else(FxOrderSet::default, unbound_on); + let exit_unbound_on = exit_error + .as_deref() + .map_or_else(FxOrderSet::default, unbound_on); + + for ty in &enter_unbound_on { + if exit_unbound_on.contains(ty) { + diag.info(format_args!( + "`{}` does not implement `{enter_method}` or `{exit_method}`", + ty.display(db) + )); + } else { + diag.info(format_args!( + "`{}` does not implement `{enter_method}`", + ty.display(db) + )); + } + } + + for ty in &exit_unbound_on { + if !enter_unbound_on.contains(ty) { + diag.info(format_args!( + "`{}` does not implement `{exit_method}`", + ty.display(db) + )); + } + } + + for (method, return_type) in + non_awaitable.named_return_types(enter_method, exit_method) + { + diag.info(format_args!( + "`{method}` returns `{}`, which is not awaitable", + return_type.display(db) + )); + } + if non_awaitable.is_both() { + diag.info("Consider declaring the methods with `async def`"); + } else { + diag.info("Consider declaring the method with `async def`"); + } + } + } + + // Do not suggest switching between `with` and `async with` for a non-awaitable return. + if matches!(self, Self::NotAwaitable { .. }) { + return; } let (alt_mode, alt_enter_method, alt_exit_method, alt_with_kw) = match mode { From 299a22766f0a4a4ba8870d021c1d80b9db67e3b7 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 2 Aug 2026 22:32:53 +0100 Subject: [PATCH 210/390] [ty] Improve error context for incompatible callable signatures (#27422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary We had a lot of branches in `signatures.rs` where we were failing to add error context, which often made our diagnostics less comprehensible than they could have been. This PR adds context to lots of these branches, and adds snapshots to cover the new code paths. There are still lots of `return self.never();` branches, even on this PR branch, that don't have any error context attached to them. I was too scared to touch the `ParamSpec`- and `TypeVarTuple`-specific branches in this PR 🙈 so left them out of scope for now. --- .../mdtest/diagnostics/error_context.md | 157 ++++++++++ .../resources/mdtest/liskov.md | 273 ++++++++++++++++++ .../resources/mdtest/loops/async_for.md | 2 + .../resources/mdtest/loops/for.md | 4 + .../src/types/relation_error.rs | 52 +++- .../src/types/signatures.rs | 175 ++++++++++- 6 files changed, 648 insertions(+), 15 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md index bc5c02d0fb..a805a76f91 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md @@ -286,6 +286,7 @@ error[invalid-assignment]: Object of type `(int, str, /) -> bool` is not assigna | | | Declared type info: unexpected extra parameter +help: The parameter must have a default value ``` Assigning a function with an extra required parameter to a `Callable`: @@ -306,6 +307,7 @@ error[invalid-assignment]: Object of type `def source(x: int, extra: str) -> boo | | | Declared type info: unexpected extra parameter `extra` +help: Parameter `extra` must have a default value ``` Assigning a class to a `Callable` @@ -350,6 +352,7 @@ error[invalid-argument-type]: Argument to function `accepts_callable` is incorre | ^^^ Expected `(Any, /) -> Any`, found `` info: type `` has inferred callable type `(x: Any, y: Any) -> Foo` info: └── unexpected extra parameter `y` +help: Parameter `y` must have a default value info: Function defined here --> src/mdtest_snippet.py:23:5 | @@ -423,6 +426,134 @@ error[invalid-assignment]: Object of type `partial[(y: str) -> bool]` is not ass info: the first parameter has an incompatible type: `bytes` is not assignable to `str` ``` +## Missing unnamed callable parameters + +Parameters in a `Callable` type do not have names, so a missing parameter is identified by its +position. + +```py +from typing import Callable + +def assign(source: Callable[[], None]) -> None: + target: Callable[[int], None] = source # snapshot: invalid-assignment +``` + +```snapshot +error[invalid-assignment]: Object of type `() -> None` is not assignable to `(int, /) -> None` + --> src/mdtest_snippet.py:4:37 + | +4 | target: Callable[[int], None] = source # snapshot: invalid-assignment + | --------------------- ^^^^^^ Incompatible value of type `() -> None` + | | + | Declared type +info: the first parameter is missing +``` + +## Missing parameters in nested generic calls involving `TypeVarTuple`s and `ParamSpec`s + +In the following example, the signature of the `callback` function does not satisfy the `fn` +parameter of `wrapper` in the `accept()` call, because the arguments provided to `accept()` +following `fn` indicate that it must accept the value `1` as a positional argument, and it does not. + +We don't currently add error context in this code path, but we could add it in the future: + +```py +from collections.abc import Callable + +def wrapper1[**P](fn: Callable[P, None]) -> Callable[P, None]: + return fn + +def accept1[**P](fn: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback1() -> None: ... + +accept1(wrapper1(callback1), 1) # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper1` is incorrect + --> src/mdtest_snippet.py:9:18 + | +9 | accept1(wrapper1(callback1), 1) # snapshot: invalid-argument-type + | ^^^^^^^^^ Expected `(**P@accept1) -> None`, found `def callback1() -> None` +info: Function defined here + --> src/mdtest_snippet.py:3:5 + | +3 | def wrapper1[**P](fn: Callable[P, None]) -> Callable[P, None]: + | ^^^^^^^^ --------------------- Parameter declared here +``` + +The following case is similar, but exercises a different code path. Here, we could also add error +context to improve the diagnostic in the future: + +```py +def wrapper2[**P](fn: Callable[P, None]) -> Callable[P, None]: + return fn + +def accept2[**P](fn: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def callback2(**kwargs: int) -> None: ... + +accept2(wrapper2(callback2), 1) # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper2` is incorrect + --> src/mdtest_snippet.py:16:18 + | +16 | accept2(wrapper2(callback2), 1) # snapshot: invalid-argument-type + | ^^^^^^^^^ Expected `(**P@accept2) -> None`, found `def callback2(**kwargs: int) -> None` +info: Function defined here + --> src/mdtest_snippet.py:10:5 + | +10 | def wrapper2[**P](fn: Callable[P, None]) -> Callable[P, None]: + | ^^^^^^^^ --------------------- Parameter declared here +``` + +And the same applies to the following two examples too, which both use a `TypeVarTuple` instead of a +`ParamSpec`: + +```py +def wrapper3[*Ts](fn: Callable[[*Ts], None]) -> Callable[[*Ts], None]: + return fn + +def accept3[*Ts](fn: Callable[[*Ts], None], *args: *Ts) -> None: ... +def callback3(value: int) -> None: ... + +accept3(wrapper3(callback3)) # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `wrapper3` is incorrect + --> src/mdtest_snippet.py:23:18 + | +23 | accept3(wrapper3(callback3)) # snapshot: invalid-argument-type + | ^^^^^^^^^ Expected `(*int) -> None`, found `def callback3(value: int) -> None` +info: Function defined here + --> src/mdtest_snippet.py:17:5 + | +17 | def wrapper3[*Ts](fn: Callable[[*Ts], None]) -> Callable[[*Ts], None]: + | ^^^^^^^^ ------------------------- Parameter declared here +``` + +```py +def accepts4[*Ts](fn: Callable[[*Ts, int], None]) -> None: ... +def callback4() -> None: ... + +accepts4(callback4) # snapshot: invalid-argument-type +``` + +```snapshot +error[invalid-argument-type]: Argument to function `accepts4` is incorrect + --> src/mdtest_snippet.py:27:10 + | +27 | accepts4(callback4) # snapshot: invalid-argument-type + | ^^^^^^^^^ Expected `(*args: Unknown, int, /) -> None`, found `def callback4() -> None` +info: Function defined here + --> src/mdtest_snippet.py:24:5 + | +24 | def accepts4[*Ts](fn: Callable[[*Ts, int], None]) -> None: ... + | ^^^^^^^^ ------------------------------ Parameter declared here +``` + ## Function assignability and overrides Liskov checks use function-to-function assignability. @@ -531,6 +662,31 @@ info: the parameter named `y` does not match `x` (and can be used as a keyword p info: This violates the Liskov Substitution Principle ``` +## Uncallable top signatures + +A top callable represents every possible callable signature, so no specific call is guaranteed to be +accepted. It therefore cannot be assigned to a callable that promises to accept an integer. + +```py +from typing import Callable +from ty_extensions import Top + +def assign(source: Top[Callable[..., int]]) -> None: + target: Callable[[int], int] = source # snapshot: invalid-assignment +``` + +```snapshot +error[invalid-assignment]: Object of type `Top[(...) -> int]` is not assignable to `(int, /) -> int` + --> src/mdtest_snippet.py:5:36 + | +5 | target: Callable[[int], int] = source # snapshot: invalid-assignment + | -------------------- ^^^^^^ Incompatible value of type `Top[(...) -> int]` + | | + | Declared type +info: Object of type `Top[(...) -> int]` is not safe to call; its signature is not known +help: This type includes all possible parameter sets, so it cannot safely be called because there is no valid set of arguments for it +``` + ## `TypedDict` Incompatible field types: @@ -1652,5 +1808,6 @@ info: └── incompatible return types: `WrongIterator` is not assignable info: └── type `WrongIterator` is not assignable to protocol `Iterator[Unknown]` info: └── protocol member `__next__` is incompatible info: └── unexpected extra parameter `wrong` +help: Parameter `wrong` must have a default value info: Expected signature for `__next__` is `def __next__(self): ...` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/liskov.md b/crates/ty_python_semantic/resources/mdtest/liskov.md index c15e975c26..533f251b84 100644 --- a/crates/ty_python_semantic/resources/mdtest/liskov.md +++ b/crates/ty_python_semantic/resources/mdtest/liskov.md @@ -169,6 +169,7 @@ error[invalid-method-override]: Invalid override of method `method` | 2 | def method(self, x: int, /): ... | ----------------------- `Super.method` defined here +info: parameter `x` is missing info: This violates the Liskov Substitution Principle ``` @@ -191,6 +192,7 @@ error[invalid-method-override]: Invalid override of method `method` 2 | def method(self, x: int, /): ... | ----------------------- `Super.method` defined here info: unexpected extra parameter `y` +help: Parameter `y` must have a default value info: This violates the Liskov Substitution Principle ``` @@ -314,6 +316,7 @@ error[invalid-method-override]: Invalid override of method `method3` 54 | class Sub19(Super3): 55 | def method3(self, x, /): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super3.method3` +info: parameter `x` is positional-only but must also accept keyword arguments info: This violates the Liskov Substitution Principle ``` @@ -346,6 +349,7 @@ error[invalid-method-override]: Invalid override of method `method` 61 | class Sub21(Super4): 62 | def method(self, *args): ... # snapshot: invalid-method-override | ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super4.method` +info: the signature must accept arbitrary keyword arguments info: This violates the Liskov Substitution Principle ``` @@ -367,6 +371,7 @@ error[invalid-method-override]: Invalid override of method `method` | 57 | def method(self, *args: int, **kwargs: str): ... | --------------------------------------- `Super4.method` defined here +info: the signature must accept arbitrary positional arguments info: This violates the Liskov Substitution Principle ``` @@ -379,6 +384,274 @@ class Sub23(Super4): def method(self, x, *args, y, **kwargs): ... ``` +## Variadic keyword parameters cannot replace positional parameters + +A method that accepts only keyword arguments cannot accept a positional argument required by the +superclass method. + +```pyi +class Parent: + def method(self, value: int, /) -> None: ... + +class Child(Parent): + def method(self, **kwargs: int) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:5:9 + | +2 | def method(self, value: int, /) -> None: ... + | ----------------------------------- `Parent.method` defined here +3 | +4 | class Child(Parent): +5 | def method(self, **kwargs: int) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +info: parameter `value` is missing +info: This violates the Liskov Substitution Principle +``` + +## Signatures with variadic positional arguments cannot add additional required arguments + +A method that accepts any number of positional arguments can be called with no arguments. An +override must not introduce a required positional argument before its variadic parameter. + +```pyi +class Parent: + def method(self, *args: int) -> None: ... + +class Child(Parent): + def method(self, first: int, *args: int) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:5:9 + | +2 | def method(self, *args: int) -> None: ... + | -------------------------------- `Parent.method` defined here +3 | +4 | class Child(Parent): +5 | def method(self, first: int, *args: int) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +info: unexpected extra parameter `first` +help: Parameter `first` must have a default value +info: This violates the Liskov Substitution Principle +``` + +Adding a positional parameter with a default is valid because callers can omit it. + +```pyi +class OptionalChild(Parent): + # TODO: this is a false-positive error that should be fixed. + def method(self, first: int = 0, *args: int) -> None: ... # error: [invalid-method-override] +``` + +## Variadic keyword parameters cannot be overridden with a limited set of keyword-only parameters + +A method that accepts arbitrary keyword arguments cannot be overridden by a method that accepts only +one named keyword argument, even when that argument is optional. + +```pyi +class Parent: + def method(self, **kwargs: int) -> None: ... + +class Child(Parent): + def method(self, *, value: int = 0) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:5:9 + | +2 | def method(self, **kwargs: int) -> None: ... + | ----------------------------------- `Parent.method` defined here +3 | +4 | class Child(Parent): +5 | def method(self, *, value: int = 0) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +info: the signature must accept arbitrary keyword arguments +info: This violates the Liskov Substitution Principle +``` + +## Optional parameters must remain optional on subclass overrides + +A positional-only parameter that callers may omit on the superclass cannot become required on the +subclass. + +```pyi +class ParentPositionalOnly: + def method(self, parent_value: int = 0, /) -> None: ... + +class ChildPositionalOnly(ParentPositionalOnly): + def method(self, child_value: int, /) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:5:9 + | +2 | def method(self, parent_value: int = 0, /) -> None: ... + | ---------------------------------------------- `ParentPositionalOnly.method` defined here +3 | +4 | class ChildPositionalOnly(ParentPositionalOnly): +5 | def method(self, child_value: int, /) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `ParentPositionalOnly.method` +info: parameter `child_value` must have a default value +info: This violates the Liskov Substitution Principle +``` + +The same rule applies when the optional parameter is positional-or-keyword: + +```pyi +class ParentPositionalOrKeyword: + def method(self, value: int = 0) -> None: ... + +class ChildPositionalOrKeyword(ParentPositionalOrKeyword): + def method(self, value: int) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:10:9 + | + 7 | def method(self, value: int = 0) -> None: ... + | ------------------------------------ `ParentPositionalOrKeyword.method` defined here + 8 | + 9 | class ChildPositionalOrKeyword(ParentPositionalOrKeyword): +10 | def method(self, value: int) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `ParentPositionalOrKeyword.method` +info: parameter `value` must have a default value +info: This violates the Liskov Substitution Principle +``` + +And if the parameter is keyword-only: + +```pyi +class ParentKeywordOnly: + def method(self, *, value: int = 0) -> None: ... + +class ChildKeywordOnly(ParentKeywordOnly): + def method(self, *, value: int) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:15:9 + | +12 | def method(self, *, value: int = 0) -> None: ... + | --------------------------------------- `ParentKeywordOnly.method` defined here +13 | +14 | class ChildKeywordOnly(ParentKeywordOnly): +15 | def method(self, *, value: int) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `ParentKeywordOnly.method` +info: parameter `value` must have a default value +info: This violates the Liskov Substitution Principle +``` + +## Subclass overrides may not add additional positional-only parameters without default values + +This is true if the new parameter is positional-only: + +```pyi +class PositionalOnlyParent: + def method(self, *, value: int) -> None: ... + +class PositionalOnlyChild(PositionalOnlyParent): + def method(self, extra: int, /, *, value: int) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:5:9 + | +2 | def method(self, *, value: int) -> None: ... + | ----------------------------------- `PositionalOnlyParent.method` defined here +3 | +4 | class PositionalOnlyChild(PositionalOnlyParent): +5 | def method(self, extra: int, /, *, value: int) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `PositionalOnlyParent.method` +info: unexpected extra parameter `extra` +help: Parameter `extra` must have a default value +info: This violates the Liskov Substitution Principle +``` + +And if the new parameter is keyword-only: + +```pyi +class KeywordOnlyParent: + def method(self, *, value: int) -> None: ... + +class KeywordOnlyChild(KeywordOnlyParent): + def method(self, *, value: int, extra: int) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:10:9 + | + 7 | def method(self, *, value: int) -> None: ... + | ----------------------------------- `KeywordOnlyParent.method` defined here + 8 | + 9 | class KeywordOnlyChild(KeywordOnlyParent): +10 | def method(self, *, value: int, extra: int) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `KeywordOnlyParent.method` +info: unexpected extra parameter `extra` +help: Parameter `extra` must have a default value +info: This violates the Liskov Substitution Principle +``` + +## Keyword-only parameters cannot be removed + +Removing a keyword-only parameter means that the overriding method no longer accepts the +corresponding keyword argument. + +```pyi +class Parent: + def method(self, *, value: int) -> None: ... + +class Child(Parent): + def method(self) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:5:9 + | +2 | def method(self, *, value: int) -> None: ... + | ----------------------------------- `Parent.method` defined here +3 | +4 | class Child(Parent): +5 | def method(self) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +info: parameter `value` is missing +info: This violates the Liskov Substitution Principle +``` + +Replacing the parameter with a differently named optional keyword also prevents callers from +providing the original argument. + +```pyi +class ChildWithDifferentKeyword(Parent): + def method(self, *, other: int = 0) -> None: ... # snapshot: invalid-method-override +``` + +```snapshot +error[invalid-method-override]: Invalid override of method `method` + --> src/mdtest_snippet.pyi:7:9 + | +2 | def method(self, *, value: int) -> None: ... + | ----------------------------------- `Parent.method` defined here +3 | +4 | class Child(Parent): +5 | def method(self) -> None: ... # snapshot: invalid-method-override +6 | class ChildWithDifferentKeyword(Parent): +7 | def method(self, *, other: int = 0) -> None: ... # snapshot: invalid-method-override + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method` +info: parameter `value` is missing +info: This violates the Liskov Substitution Principle +``` + ## `ClassVar` and instance variables A pure class variable cannot override an inherited instance variable, and an instance variable diff --git a/crates/ty_python_semantic/resources/mdtest/loops/async_for.md b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md index df30433d40..13a4ef25a4 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/async_for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md @@ -219,6 +219,7 @@ info: Its `__aiter__` method has an invalid signature info: type `AsyncIterable` is not assignable to protocol `AsyncIterable[Unknown]` info: └── protocol member `__aiter__` is incompatible info: └── unexpected extra parameter `arg` +help: Parameter `arg` must have a default value info: Expected signature `def __aiter__(self): ...` ``` @@ -252,5 +253,6 @@ info: └── incompatible return types: `AsyncIterator` is not assignable info: └── type `AsyncIterator` is not assignable to protocol `AsyncIterator[Unknown]` info: └── protocol member `__anext__` is incompatible info: └── unexpected extra parameter `arg` +help: Parameter `arg` must have a default value info: Expected signature for `__anext__` is `def __anext__(self): ...` ``` diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index 2a4e3f7fa7..142a5b0646 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -892,6 +892,7 @@ info: Its `__iter__` method has an invalid signature info: type `Iterable` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible info: └── unexpected extra parameter `extra_arg` +help: Parameter `extra_arg` must have a default value info: Expected signature `def __iter__(self): ...` ``` @@ -970,6 +971,7 @@ info: └── incompatible return types: `Iterator1` is not assignable to info: └── type `Iterator1` is not assignable to protocol `Iterator[Unknown]` info: └── protocol member `__next__` is incompatible info: └── unexpected extra parameter `extra_arg` +help: Parameter `extra_arg` must have a default value info: Expected signature for `__next__` is `def __next__(self): ...` ``` @@ -1233,6 +1235,7 @@ info: Its `__iter__` method may have an invalid signature info: type `Iterable1` is not assignable to protocol `Iterable[Unknown]` info: └── protocol member `__iter__` is incompatible info: └── unexpected extra parameter `invalid_extra_arg` +help: Parameter `invalid_extra_arg` must have a default value 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): ...` ``` @@ -1308,6 +1311,7 @@ info: └── incompatible return types: `Iterator1` is not assignable to info: └── type `Iterator1` is not assignable to protocol `Iterator[Unknown]` info: └── protocol member `__next__` is incompatible info: └── unexpected extra parameter `invalid_extra_arg` +help: Parameter `invalid_extra_arg` must have a default value info: Expected signature for `__next__` is `def __next__(self): ...` ``` diff --git a/crates/ty_python_semantic/src/types/relation_error.rs b/crates/ty_python_semantic/src/types/relation_error.rs index ce2602fdd6..84bed59520 100644 --- a/crates/ty_python_semantic/src/types/relation_error.rs +++ b/crates/ty_python_semantic/src/types/relation_error.rs @@ -107,6 +107,17 @@ pub(crate) enum ErrorContext<'db> { ExtraRequiredParameter { parameter: ParameterDescription, }, + MissingParameter { + parameter: ParameterDescription, + }, + RequiredParameterMustHaveDefault { + parameter: ParameterDescription, + }, + MissingVariadicPositionalParameter, + MissingVariadicKeywordParameter, + TopCallableAssignedToNonTop { + return_type: Type<'db>, + }, ParameterNameMismatch { source_name: Name, target_name: Name, @@ -287,9 +298,36 @@ impl<'db> ErrorContext<'db> { callable.display(db), ), Self::ExtraRequiredParameter { parameter } => match parameter { - ParameterDescription::Named(name) => format!("unexpected extra parameter `{name}`"), - ParameterDescription::Index(_) => "unexpected extra parameter".to_string(), + ParameterDescription::Named(name) => { + help_messages.insert(HelpMessages::ConsiderAddingADefaultValue { + parameter_name: Some(name.clone()), + }); + format!("unexpected extra parameter `{name}`") + } + ParameterDescription::Index(_) => { + help_messages.insert(HelpMessages::ConsiderAddingADefaultValue { + parameter_name: None, + }); + "unexpected extra parameter".to_string() + } }, + Self::MissingParameter { parameter } => format!("{parameter} is missing"), + Self::RequiredParameterMustHaveDefault { parameter } => { + format!("{parameter} must have a default value") + } + Self::MissingVariadicPositionalParameter => { + "the signature must accept arbitrary positional arguments".to_string() + } + Self::MissingVariadicKeywordParameter => { + "the signature must accept arbitrary keyword arguments".to_string() + } + Self::TopCallableAssignedToNonTop { return_type } => { + help_messages.insert(HelpMessages::TopCallableExplanation); + format!( + "Object of type `Top[(...) -> {}]` is not safe to call; its signature is not known", + return_type.display(db) + ) + } Self::ParameterNameMismatch { source_name, target_name, @@ -382,6 +420,8 @@ enum HelpMessages { RequiredFieldCouldBeRemoved, TypedDictNotAssignableToDict, ConsiderUsingMappingInsteadOfDict, + TopCallableExplanation, + ConsiderAddingADefaultValue { parameter_name: Option }, } impl std::fmt::Display for HelpMessages { @@ -396,6 +436,14 @@ impl std::fmt::Display for HelpMessages { HelpMessages::ConsiderUsingMappingInsteadOfDict => { f.write_str("Consider using `Mapping[..]` instead of `dict[..]`.") } + HelpMessages::TopCallableExplanation => f.write_str( + "This type includes all possible parameter sets, \ + so it cannot safely be called because there is no valid set of arguments for it", + ), + HelpMessages::ConsiderAddingADefaultValue { parameter_name } => match parameter_name { + Some(name) => write!(f, "Parameter `{name}` must have a default value"), + None => f.write_str("The parameter must have a default value"), + }, } } } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 5e06105700..83d22b9b30 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -2314,21 +2314,71 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { { let source_positional = source.parameters.positional().count(); let target_positional = target.parameters.positional().count(); + let target_variadic = target.parameters.variadic(); + + // A subdiagnostic telling the user that `source` is missing a `*args` parameter + // is only guaranteed to be correct when `target` has a plain, open-ended variadic tail. + // (Well: we might be able to do better here in the future, but we simplify the logic here + // for now.) + // + // An unpacked annotation may represent a fixed-length tuple, and a variadic parameter + // followed by positional parameters may represent an unpacked tuple with a required suffix + // instead of an open-ended tail. + let target_has_open_ended_variadic = || { + target_variadic.is_some_and(|(index, parameter)| { + !parameter.has_starred_annotation() + && !target + .parameters + .iter() + .skip(index) + .any(Parameter::is_positional) + }) + }; + let target_accepts_extra_positionals = - target_positional > source_positional || target.parameters.variadic().is_some(); + target_positional > source_positional || target_variadic.is_some(); if target_accepts_extra_positionals { if let Some(context) = self.report_context() - && target_positional > source_positional - && let Some(ParameterKind::KeywordOnly { name, .. }) = source - .parameters - .iter() - .nth(source_positional) - .map(Parameter::kind) + && (target_positional > source_positional || target_has_open_ended_variadic()) { - context.push(ErrorContext::ParameterMustAcceptPositionalArguments { - name: name.clone(), - }); + let error_context = if target_positional > source_positional { + let source_parameter_kind = source + .parameters + .get(source_positional) + .map(Parameter::kind); + + match source_parameter_kind { + Some(ParameterKind::KeywordOnly { name, .. }) => { + ErrorContext::ParameterMustAcceptPositionalArguments { + name: name.clone(), + } + } + Some(ParameterKind::KeywordVariadic { .. }) | None => { + let parameter = target + .parameters + .get_positional(source_positional) + .and_then(Parameter::name); + ErrorContext::MissingParameter { + parameter: ParameterDescription::new( + source_positional, + parameter, + ), + } + } + Some( + ParameterKind::PositionalOnly { .. } + | ParameterKind::PositionalOrKeyword { .. } + | ParameterKind::Variadic { .. }, + ) => unreachable!( + "the first parameter after the positional prefix \ + cannot be positional or variadic" + ), + } + } else { + ErrorContext::MissingVariadicPositionalParameter + }; + context.push(error_context); } return self.never(); @@ -2397,6 +2447,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .intersect(db, self.constraints, constraint_set) .is_never_satisfied(db) }; + let parameter_must_have_default = |parameter: &Parameter<'db>, index: usize| { + ErrorContext::RequiredParameterMustHaveDefault { + parameter: ParameterDescription::new(index, parameter.name()), + } + }; if self.typevar_evaluation == TypeVarEvaluation::Lazy { let source_paramspec = source.parameters.as_paramspec_with_prefix(); @@ -3000,6 +3055,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { if target.parameters.is_top() { return self.always(); } else if source.parameters.is_top() && !target.parameters.is_gradual() { + if let Some(context) = self.report_context() { + context.push(ErrorContext::TopCallableAssignedToNonTop { + return_type: source.return_ty, + }); + } return self.never(); } @@ -3344,9 +3404,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { EitherOrBoth::Right(target_parameter) => { if let Some(source_parameter_count) = as_target_typevartuple(target_parameter) { - if source_parameter_count > 0 { - return self.never(); - } + assert_eq!( + source_parameter_count, 0, + "an exhausted source signature cannot provide parameters \ + to a TypeVarTuple" + ); if !check_types( &mut result, target_parameter.annotated_type(), @@ -3362,6 +3424,34 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // If there are more parameters in `target` than in `source`, then `source` is // not a subtype of `target`. + if let Some(context) = self.report_context() + && target.parameters.as_paramspec_with_prefix().is_none() + { + let error_context = match target_parameter.kind() { + ParameterKind::PositionalOnly { .. } + | ParameterKind::PositionalOrKeyword { .. } => unreachable!( + "unmatched target positional parameters \ + are rejected by the positional fast path" + ), + ParameterKind::Variadic { .. } => { + unreachable!( + "an unmatched target `*args` is impossible: \ + a source without `*args` is rejected by the positional fast path, \ + while a source with `*args` consumes the target during matching" + ) + } + ParameterKind::KeywordOnly { .. } => ErrorContext::MissingParameter { + parameter: ParameterDescription::new( + target_index, + target_parameter.name(), + ), + }, + ParameterKind::KeywordVariadic { .. } => { + ErrorContext::MissingVariadicKeywordParameter + } + }; + context.push(error_context); + } return self.never(); } @@ -3382,6 +3472,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }, ) => { if source_default.is_none() && target_default.is_some() { + if let Some(context) = self.report_context() { + context.push(parameter_must_have_default( + source_param, + target_index, + )); + } return self.never(); } if !check_types( @@ -3416,6 +3512,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } // The following checks are the same as positional-only parameters. if source_default.is_none() && target_default.is_some() { + if let Some(context) = self.report_context() { + context.push(parameter_must_have_default( + source_param, + target_index, + )); + } return self.never(); } if !check_types( @@ -3560,6 +3662,16 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } if !source_param.is_variadic() { + if let Some(context) = self.report_context() + && target.parameters.as_paramspec_with_prefix().is_none() + { + let parameter = ParameterDescription::new( + target_index, + source_param.name(), + ); + context + .push(ErrorContext::ExtraRequiredParameter { parameter }); + } return self.never(); } if !check_types( @@ -3649,6 +3761,23 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // only contains keyword-only and keyword-variadic parameters. However, if the // parameter has a default, it's valid because callers don't need to provide it. if default_type.is_none() { + if let Some(context) = self.report_context() { + if let Some(source_name) = source_param.name() + && target + .parameters + .iter() + .any(|target_param| target_param.name() == Some(source_name)) + { + context.push(ErrorContext::ParameterMustAcceptKeywordArguments { + source_name: Some(source_name.clone()), + target_name: source_name.clone(), + }); + } else { + let parameter = + ParameterDescription::new(target_index, source_param.name()); + context.push(ErrorContext::ExtraRequiredParameter { parameter }); + } + } return self.never(); } } @@ -3677,6 +3806,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .. } => { if source_default.is_none() && target_default.is_some() { + if let Some(context) = self.report_context() { + context.push(parameter_must_have_default( + source_param, + target_index, + )); + } return self.never(); } if !check_types( @@ -3704,6 +3839,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return result; } } else { + if let Some(context) = self.report_context() { + let parameter = + ParameterDescription::new(target_index, target_param.name()); + context.push(ErrorContext::MissingParameter { parameter }); + } return self.never(); } } @@ -3711,6 +3851,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let Some(source_keyword_variadic) = source_keyword_variadic else { // For a `source <: target` relationship, if `target` has a keyword variadic // parameter, `source` must also have a keyword variadic parameter. + if let Some(context) = self.report_context() + && target.parameters.as_paramspec_with_prefix().is_none() + { + context.push(ErrorContext::MissingVariadicKeywordParameter); + } return self.never(); }; if !check_types( @@ -3738,6 +3883,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )] for (_, source_param) in source_keywords { if source_param.default_type().is_none() { + if let Some(context) = self.report_context() { + let parameter = ParameterDescription::new(target_index, source_param.name()); + context.push(ErrorContext::ExtraRequiredParameter { parameter }); + } return self.never(); } } From 2fa7c676200cee487ec7820e53c4672ee02936bd Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 2 Aug 2026 22:34:56 +0100 Subject: [PATCH 211/390] [ty] Prioritize readable mdtest organization in AGENTS.md (#27424) ## Summary Clarify that mdtest document structure and readability take precedence over avoiding duplicated setup. Add an existing-section suitability check: its heading and introductory prose must describe the new scenario; otherwise, create a separate section even when that requires repeating a small fixture. --- AGENTS.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b334d20b8c..ca99921b1c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,11 @@ consistency issues. Order findings by severity, cite files and lines, and distinguish blockers from non-blocking improvements. Number each review point for easy reference in subsequent review discussion. +During code review, check the proposed changes against all applicable code, test, +documentation, and architectural conventions in this `AGENTS.md`. Report +meaningful violations introduced by the changes; do not apply agent-only workflow +instructions to PR authors or flag unrelated pre-existing issues. + ## Running Tests Run all tests (using `nextest` for faster execution, setting `CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_DEBUG="line-tables-only"` to enable optimizations while retaining some debug info, and setting `INSTA_FORCE_PASS=1 INSTA_UPDATE=always MDTEST_UPDATE_SNAPSHOTS=1` to ensure all snapshots are updated): @@ -65,8 +70,9 @@ Never edit snapshot files or inline snapshot bodies manually. Regenerate them by ## Writing mdtests -- Write mdtests as readable, literate specifications, and minimize the context a reader must hold in mind. Prefer short, focused code blocks, and define types, fixtures, and helpers close to the assertions that use them. Give independent scenarios separate Markdown test headings; when scenarios need shared setup, interleave short prose-and-code blocks under the same heading. Code blocks for the same file within a section are concatenated, so do not repeat imports or definitions. -- Introduce each scenario with a short prose paragraph explaining the code immediately below. Use clear, precise terminology. Avoid long paragraphs covering multiple scenarios followed by a single long code block. +- Write mdtests as readable, literate specifications, and minimize the context a reader must hold in mind. Prefer short, focused code blocks, and define types, fixtures, and helpers close to the assertions that use them. Give independent scenarios separate sibling Markdown test headings at the same level; only introduce child headings if any existing code beneath their parent is first moved into child sections. When scenarios need shared setup, interleave short prose-and-code blocks under the same heading. Code blocks for the same file within a section are concatenated, so do not repeat imports or definitions. +- Prioritize document structure and readability over avoiding duplicated setup. Add a test to an existing section when its heading accurately describes the new scenario, adding or improving introductory prose as needed; otherwise, create a separate sibling section, even if that requires repeating a small fixture. +- Introduce each scenario with a short prose paragraph explaining the code immediately below. Use clear, precise terminology. Avoid using jargon where it's unnecessary, and avoid inventing new jargon if there's an existing term of art used in that file. Avoid long paragraphs covering multiple scenarios followed by a single long code block. - Minimize regression examples to the behavior under test. When adapting real-world code or an issue reproducer, remove incidental types, methods, type parameters, imports, and domain-specific details. Preserve complexity only when necessary to reproduce the regression or distinguish the intended behavior, and reuse nearby fixtures or simple built-in types when doing so keeps the test easy to understand. - Prefer a minimal, purpose-built custom type over a standard-library type when a regression depends on particular attributes, methods, bounds, or constraints. Define the relevant behavior in the test so readers do not need to look up the standard-library type to understand the scenario. For commonly used standard-library types, consider adding a separate regression using the real type to protect against changes in typeshed. - Place each mdtest in a file for the behavior it actually tests, and assert that behavior directly. Prefer an existing file when one already covers that behavior; create a new file when no existing file is a good fit. Do not choose a file solely because its directive or helper can express the assertion. From da17b774141925ad43363adc7de082776f5cd01f Mon Sep 17 00:00:00 2001 From: Niyuta <103232728+cheparity@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:06:16 +0800 Subject: [PATCH 212/390] [ty] Trigger signature help after completing callable with parentheses (#27084) Co-authored-by: Micha Reiser --- crates/ty_ide/src/completion.rs | 61 +++++++++++++------ crates/ty_ide/src/lib.rs | 4 +- crates/ty_server/src/capabilities.rs | 19 ++++++ .../src/server/api/requests/completion.rs | 35 ++++++++++- crates/ty_server/tests/e2e/completions.rs | 54 +++++++++++++++- crates/ty_server/tests/e2e/main.rs | 27 +++++++- 6 files changed, 175 insertions(+), 25 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 6aa69779c9..90f6d61760 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -301,6 +301,9 @@ pub struct Completion<'db> { pub insert: Option, /// The format of [`Self::insert`]. pub insert_text_format: CompletionInsertTextFormat, + /// An editor action the client should perform after applying this + /// completion, if any. See [`CompletionCommand`]. + pub command: Option, /// The type of this completion, if available. /// /// Generally speaking, this is always available @@ -487,33 +490,41 @@ impl<'db> CompletionBuilder<'db> { .kind .or_else(|| self.ty.and_then(|ty| completion_kind_from_type(db, ty))); let relevance = Relevance::new(ctx, query, &self); - let (label, insert, insert_text_format) = if ctx.should_complete_callable_parentheses(kind) - { - let label = self.insert.unwrap_or_else(|| self.name.clone()); - if ctx.capabilities.snippets { - let insert = compact_str::format_compact!("{label}($0)"); - ( - Some(label), - Some(insert), - CompletionInsertTextFormat::Snippet, - ) + let (label, insert, insert_text_format, command) = + if ctx.should_complete_callable_parentheses(kind) { + let label = self.insert.unwrap_or_else(|| self.name.clone()); + if ctx.capabilities.snippets { + let insert = compact_str::format_compact!("{label}($0)"); + ( + Some(label), + Some(insert), + CompletionInsertTextFormat::Snippet, + Some(CompletionCommand::TriggerSignatureHelp), + ) + } else { + let insert = compact_str::format_compact!("{label}()"); + ( + Some(label), + Some(insert), + CompletionInsertTextFormat::PlainText, + None, + ) + } } else { - let insert = compact_str::format_compact!("{label}()"); ( - Some(label), - Some(insert), + None, + self.insert, CompletionInsertTextFormat::PlainText, + None, ) - } - } else { - (None, self.insert, CompletionInsertTextFormat::PlainText) - }; + }; Completion { name: self.name, label, qualified: self.qualified, insert, insert_text_format, + command, ty: self.ty, kind, module_name: self.module_name, @@ -632,6 +643,22 @@ pub enum CompletionKind { TypeParameter, } +/// An editor action the client should perform after applying this completion. +/// +/// This is an editor-neutral *intent* produced by the analysis layer. The +/// language server maps it to a concrete command (for example +/// `ty.triggerParameterHints`) when building the LSP response, and only +/// attaches it when the client advertised support for that command, so this +/// enum never names a particular editor. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CompletionCommand { + /// The completion inserts an opening parenthesis with the cursor placed + /// inside it (e.g. `foo($0)`). Because that parenthesis is inserted + /// programmatically rather than typed, the client will not auto-trigger + /// signature help, so it should be asked to open it explicitly. + TriggerSignatureHelp, +} + /// The format of a completion's insertion text. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum CompletionInsertTextFormat { diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index fd71d0d97d..341ac0efa2 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -37,8 +37,8 @@ pub use call_hierarchy::outgoing_calls::{OutgoingCall, outgoing_calls}; pub use call_hierarchy::{CallHierarchyItem, prepare_call_hierarchy}; pub use code_action::{QuickFix, code_actions}; pub use completion::{ - Completion, CompletionCapabilities, CompletionInsertTextFormat, CompletionKind, - CompletionSettings, completion, + Completion, CompletionCapabilities, CompletionCommand, CompletionInsertTextFormat, + CompletionKind, CompletionSettings, completion, }; pub use doc_highlights::document_highlights; pub use document_symbols::document_symbols; diff --git a/crates/ty_server/src/capabilities.rs b/crates/ty_server/src/capabilities.rs index b8bff33cc6..601628d8c9 100644 --- a/crates/ty_server/src/capabilities.rs +++ b/crates/ty_server/src/capabilities.rs @@ -37,6 +37,7 @@ bitflags::bitflags! { const COMPLETION_ITEM_SNIPPET_SUPPORT = 1 << 19; const FULL_DIAGNOSTIC_OUTPUT = 1 << 20; const IMPLEMENTATION_LINK_SUPPORT = 1 << 21; + const TRIGGER_SIGNATURE_HELP_COMMAND = 1 << 22; } } @@ -201,6 +202,11 @@ impl ResolvedClientCapabilities { self.contains(Self::PREFER_MARKDOWN_IN_COMPLETION) } + /// Returns `true` if the client supports the `ty.triggerParameterHints` completion command. + pub(crate) const fn supports_trigger_parameter_hints_command(self) -> bool { + self.contains(Self::TRIGGER_SIGNATURE_HELP_COMMAND) + } + pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self { let mut flags = Self::empty(); @@ -272,6 +278,19 @@ impl ResolvedClientCapabilities { flags |= Self::FULL_DIAGNOSTIC_OUTPUT; } + if client_capabilities + .experimental + .as_ref() + .and_then(|experimental| experimental.get("commands")?.get("commands")?.as_array()) + .is_some_and(|commands| { + commands + .iter() + .any(|command| command.as_str() == Some("ty.triggerParameterHints")) + }) + { + flags |= Self::TRIGGER_SIGNATURE_HELP_COMMAND; + } + if text_document .and_then(|text_document| text_document.type_definition?.link_support) .unwrap_or_default() diff --git a/crates/ty_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs index cf1868f535..eb7ecbcc79 100644 --- a/crates/ty_server/src/server/api/requests/completion.rs +++ b/crates/ty_server/src/server/api/requests/completion.rs @@ -2,15 +2,19 @@ use std::borrow::Cow; use std::time::Instant; use lsp_types::{ - CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionList, + Command, CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionList, CompletionParams, CompletionRequest, CompletionResponse, Documentation, InsertTextFormat, TextEdit, Uri, }; use ruff_source_file::OneIndexed; use ruff_text_size::Ranged; -use ty_ide::{CompletionCapabilities, CompletionInsertTextFormat, CompletionKind, completion}; +use ty_ide::{ + CompletionCapabilities, CompletionCommand, CompletionInsertTextFormat, CompletionKind, + completion, +}; use ty_project::ProjectDatabase; +use crate::capabilities::ResolvedClientCapabilities; use crate::document::{PositionExt, ToRangeExt}; use crate::server::api::traits::{ BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, @@ -139,6 +143,9 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { insert_text_format, additional_text_edits: import_edit.map(|edit| vec![edit]), documentation, + command: comp + .command + .and_then(|command| to_lsp_command(command, client_capabilities)), ..Default::default() } }) @@ -162,6 +169,30 @@ impl RetriableRequestHandler for CompletionRequestHandler { const RETRY_ON_CANCELLATION: bool = true; } +/// Maps an editor-neutral completion intent to the concrete LSP command the +/// client should run after applying the completion. +/// +/// The intent itself is decided in `ty_ide`; this is the single place that knows +/// any editor-specific command identifiers. +/// +/// Returns `None` when the client has not advertised support for the command, +/// so that clients without a handler never receive one. +fn to_lsp_command( + command: CompletionCommand, + client_capabilities: ResolvedClientCapabilities, +) -> Option { + match command { + CompletionCommand::TriggerSignatureHelp => client_capabilities + .supports_trigger_parameter_hints_command() + .then(|| Command { + title: "Trigger parameter hints".into(), + tooltip: None, + command: "ty.triggerParameterHints".into(), + arguments: None, + }), + } +} + fn ty_kind_to_lsp_kind(kind: CompletionKind) -> CompletionItemKind { // Gimme my dang globs in tight scopes! #[allow(clippy::enum_glob_use)] diff --git a/crates/ty_server/tests/e2e/completions.rs b/crates/ty_server/tests/e2e/completions.rs index 2e6cd3561b..23e21bd506 100644 --- a/crates/ty_server/tests/e2e/completions.rs +++ b/crates/ty_server/tests/e2e/completions.rs @@ -121,6 +121,53 @@ fn complete_function_parentheses() -> Result<()> { let foo_content = "\ def complete_parentheses() -> None: ... +complete_parenth +"; + + let mut server = TestServerBuilder::new()? + .with_initialization_options( + ClientOptions::default().with_complete_function_parentheses(true), + ) + .enable_completion_snippets(true) + .with_trigger_parameter_hints_command() + .with_workspace(workspace_root, None)? + .with_file(foo, foo_content)? + .build() + .wait_until_workspaces_are_initialized(); + + server.open_text_document(foo, foo_content, 1); + + let completions = server.completion_request(&server.file_uri(foo), Position::new(2, 16)); + insta::assert_json_snapshot!(completions, @r#" + [ + { + "label": "complete_parentheses", + "kind": 3, + "detail": "def complete_parentheses() -> None", + "sortText": "0", + "insertText": "complete_parentheses($0)", + "insertTextFormat": 2, + "command": { + "title": "Trigger parameter hints", + "command": "ty.triggerParameterHints" + } + } + ] + "#); + + Ok(()) +} + +/// Tests that the signature-help command is omitted when the client has not +/// advertised support for it (for example, editors that would surface an +/// "unsupported command" error). +#[test] +fn complete_function_parentheses_without_command_support() -> Result<()> { + let workspace_root = SystemPath::new("src"); + let foo = SystemPath::new("src/foo.py"); + let foo_content = "\ +def complete_parentheses() -> None: ... + complete_parenth "; @@ -205,6 +252,7 @@ is_typedd ClientOptions::default().with_complete_function_parentheses(true), ) .enable_completion_snippets(true) + .with_trigger_parameter_hints_command() .with_workspace(workspace_root, None)? .with_file(foo, foo_content)? .build() @@ -220,7 +268,11 @@ is_typedd "kind": 3, "sortText": "0", "insertText": "typing.is_typeddict($0)", - "insertTextFormat": 2 + "insertTextFormat": 2, + "command": { + "title": "Trigger parameter hints", + "command": "ty.triggerParameterHints" + } } ] "#); diff --git a/crates/ty_server/tests/e2e/main.rs b/crates/ty_server/tests/e2e/main.rs index 84b3881185..996b3d62da 100644 --- a/crates/ty_server/tests/e2e/main.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -1378,9 +1378,30 @@ impl TestServerBuilder { /// Advertise support for ty's fully rendered diagnostic output. pub(crate) fn with_full_diagnostic_output(mut self) -> Self { - self.client_capabilities.experimental = Some(serde_json::json!({ - "fullDiagnosticOutput": true, - })); + let experimental = self + .client_capabilities + .experimental + .get_or_insert_with(|| serde_json::json!({})); + experimental + .as_object_mut() + .expect("experimental capabilities must be a JSON object") + .insert("fullDiagnosticOutput".to_string(), serde_json::json!(true)); + self + } + + /// Advertise support for the `ty.triggerParameterHints` completion command. + pub(crate) fn with_trigger_parameter_hints_command(mut self) -> Self { + let experimental = self + .client_capabilities + .experimental + .get_or_insert_with(|| serde_json::json!({})); + experimental + .as_object_mut() + .expect("experimental capabilities must be a JSON object") + .insert( + "commands".to_string(), + serde_json::json!({ "commands": ["ty.triggerParameterHints"] }), + ); self } From d3544b558fe87204c5f0fddab535842fea526e95 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 3 Aug 2026 09:16:40 +0200 Subject: [PATCH 213/390] [ty] Revert "Remove bespoke inference VecMap iterator" (#27428) Reverts #27362. --- .../src/types/infer/builder.rs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 799e081089..f853f7ff5b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1064,7 +1064,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut seen_overloaded_places = FxHashSet::default(); let mut seen_public_functions = FxHashSet::default(); - for (&definition, ty_and_quals) in self.declarations.iter() { + for (&definition, ty_and_quals) in &self.declarations { let ty = ty_and_quals.inner_type(); match definition.kind(self.db()) { DefinitionKind::Function(function) => { @@ -11830,8 +11830,10 @@ impl VecMap { self.0.is_empty() } - fn iter(&self) -> impl ExactSizeIterator { - self.0.iter().map(|(key, value)| (key, value)) + fn iter(&self) -> VecMapIterator<'_, K, V> { + VecMapIterator { + inner: self.0.iter(), + } } fn into_boxed_slice(self) -> Box<[(K, V)]> { @@ -11876,6 +11878,35 @@ impl Default for VecMap { } } +impl<'a, K, V> IntoIterator for &'a VecMap { + type Item = (&'a K, &'a V); + type IntoIter = VecMapIterator<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +struct VecMapIterator<'a, K, V> { + inner: std::slice::Iter<'a, (K, V)>, +} + +impl<'a, K, V> Iterator for VecMapIterator<'a, K, V> { + type Item = (&'a K, &'a V); + + fn next(&mut self) -> Option { + self.inner.next().map(|(k, v)| (k, v)) + } +} + +impl std::iter::FusedIterator for VecMapIterator<'_, K, V> {} + +impl ExactSizeIterator for VecMapIterator<'_, K, V> { + fn len(&self) -> usize { + self.inner.len() + } +} + /// Set based on a `Vec`. It doesn't enforce /// uniqueness on insertion. Instead, it relies on the caller /// that elements are unique. For example, the way we visit definitions From d3baae55bd49082f3a144fdf26eed306232a4e4e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 3 Aug 2026 12:17:12 +0500 Subject: [PATCH 214/390] Use `LinterSettings` builder methods in tests (#27418) --- crates/ruff/src/cache.rs | 7 +- crates/ruff_linter/src/rules/fastapi/mod.rs | 15 +--- .../ruff_linter/src/rules/flake8_async/mod.rs | 6 +- .../src/rules/flake8_bandit/mod.rs | 11 +-- .../src/rules/flake8_bugbear/mod.rs | 14 +--- .../src/rules/flake8_builtins/mod.rs | 15 +--- .../src/rules/flake8_comprehensions/mod.rs | 6 +- .../rules/flake8_future_annotations/mod.rs | 12 +-- .../src/rules/flake8_gettext/mod.rs | 6 +- .../ruff_linter/src/rules/flake8_pyi/mod.rs | 16 +--- .../src/rules/flake8_simplify/mod.rs | 6 +- .../src/rules/flake8_type_checking/mod.rs | 11 +-- .../src/rules/flake8_use_pathlib/mod.rs | 23 ++---- crates/ruff_linter/src/rules/perflint/mod.rs | 9 +-- .../ruff_linter/src/rules/pycodestyle/mod.rs | 11 +-- crates/ruff_linter/src/rules/pyflakes/mod.rs | 38 +++------ .../ruff_linter/src/rules/pygrep_hooks/mod.rs | 11 +-- crates/ruff_linter/src/rules/pylint/mod.rs | 10 +-- crates/ruff_linter/src/rules/pyupgrade/mod.rs | 77 ++++++------------- crates/ruff_linter/src/rules/ruff/mod.rs | 71 ++++++----------- 20 files changed, 103 insertions(+), 272 deletions(-) diff --git a/crates/ruff/src/cache.rs b/crates/ruff/src/cache.rs index d19bbb5d4c..94ca467719 100644 --- a/crates/ruff/src/cache.rs +++ b/crates/ruff/src/cache.rs @@ -518,7 +518,7 @@ mod tests { use ruff_linter::settings::LinterSettings; use ruff_linter::settings::flags; use ruff_linter::settings::types::UnsafeFixes; - use ruff_python_ast::{PySourceType, PythonVersion}; + use ruff_python_ast::PySourceType; use ruff_workspace::Settings; use crate::cache::{self, ChangeData, FileCache, FileCacheData, FileCacheKey}; @@ -536,10 +536,7 @@ mod tests { let settings = Settings { cache_dir, - linter: LinterSettings { - unresolved_target_version: PythonVersion::latest().into(), - ..LinterSettings::for_rule(Rule::UnusedVariable) - }, + linter: LinterSettings::for_rule(Rule::UnusedVariable), ..Settings::default() }; diff --git a/crates/ruff_linter/src/rules/fastapi/mod.rs b/crates/ruff_linter/src/rules/fastapi/mod.rs index 51ff84c5bc..b8314377e7 100644 --- a/crates/ruff_linter/src/rules/fastapi/mod.rs +++ b/crates/ruff_linter/src/rules/fastapi/mod.rs @@ -41,14 +41,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("fastapi").join(path).as_path(), - &LinterSettings { - unresolved_target_version: PythonVersion::PY313.into(), - ..LinterSettings::for_rule(rule_code) - }, - &LinterSettings { - unresolved_target_version: PythonVersion::PY314.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY313), + &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY314), ); Ok(()) } @@ -62,10 +56,7 @@ mod tests { let snapshot = format!("{}_{}_py38", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("fastapi").join(path).as_path(), - &LinterSettings { - unresolved_target_version: PythonVersion::PY38.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY38), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/flake8_async/mod.rs b/crates/ruff_linter/src/rules/flake8_async/mod.rs index 0707cba7ba..4d5c602928 100644 --- a/crates/ruff_linter/src/rules/flake8_async/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_async/mod.rs @@ -47,10 +47,8 @@ mod tests { fn async109_python_310_or_older(path: &Path) -> Result<()> { let diagnostics = test_path( Path::new("flake8_async").join(path), - &LinterSettings { - unresolved_target_version: PythonVersion::PY310.into(), - ..LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout) - }, + &LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout) + .with_target_version(PythonVersion::PY310), )?; assert_diagnostics!(path.file_name().unwrap().to_str().unwrap(), diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs index 2b8423fb02..f978ecc22d 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/mod.rs @@ -12,7 +12,6 @@ mod tests { use crate::registry::Rule; use crate::settings::LinterSettings; - use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_diagnostics, assert_diagnostics_diff}; @@ -116,14 +115,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("flake8_bandit").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Disabled, - ..LinterSettings::for_rule(rule_code) - }, - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - } + &LinterSettings::for_rule(rule_code), + &LinterSettings::for_rule(rule_code).with_preview_mode() ); Ok(()) } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs index b2d1d8fb59..53fb8d510a 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/mod.rs @@ -17,7 +17,6 @@ mod tests { use crate::settings::LinterSettings; use crate::test::{test_path, test_snippet}; - use crate::settings::types::PreviewMode; use ruff_python_ast::PythonVersion; #[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"))] @@ -105,11 +104,9 @@ mod tests { ); let diagnostics = test_path( Path::new("flake8_bugbear").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Enabled, - unresolved_target_version: PythonVersion::PY314.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code) + .with_preview_mode() + .with_target_version(PythonVersion::PY314), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -144,10 +141,7 @@ mod tests { ); let diagnostics = test_path( Path::new("flake8_bugbear").join(path).as_path(), - &LinterSettings { - unresolved_target_version: target_version.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_target_version(target_version), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/flake8_builtins/mod.rs b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs index 45645f2ab8..1b3818b2de 100644 --- a/crates/ruff_linter/src/rules/flake8_builtins/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_builtins/mod.rs @@ -73,14 +73,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("flake8_builtins").join(path).as_path(), - &LinterSettings { - unresolved_target_version: PythonVersion::PY313.into(), - ..LinterSettings::for_rule(rule_code) - }, - &LinterSettings { - unresolved_target_version: PythonVersion::PY314.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY313), + &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY314), ); Ok(()) } @@ -238,10 +232,7 @@ mod tests { let snapshot = format!("{}_{}_py38", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_builtins").join(path).as_path(), - &LinterSettings { - unresolved_target_version: PythonVersion::PY38.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY38), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs index 60e612f057..c7d99586b8 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs @@ -15,7 +15,6 @@ mod tests { use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings::LinterSettings; - use crate::settings::types::PreviewMode; use crate::test::test_path; #[test_case(Rule::UnnecessaryCallAroundSorted, Path::new("C413.py"))] @@ -78,10 +77,7 @@ mod tests { ); let diagnostics = test_path( Path::new("flake8_comprehensions").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs b/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs index 5e015e5428..dfd889c36a 100644 --- a/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs @@ -29,10 +29,8 @@ mod tests { let snapshot = path.to_string_lossy().into_owned(); let diagnostics = test_path( Path::new("flake8_future_annotations").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY37.into(), - ..settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation) - }, + &settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation) + .with_target_version(PythonVersion::PY37), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -49,10 +47,8 @@ mod tests { let snapshot = format!("fa102_{}", path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_future_annotations").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY37.into(), - ..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation) - }, + &settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation) + .with_target_version(PythonVersion::PY37), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/flake8_gettext/mod.rs b/crates/ruff_linter/src/rules/flake8_gettext/mod.rs index 12705a7624..21d78d8c76 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/mod.rs @@ -71,7 +71,6 @@ mod tests { use test_case::test_case; use crate::registry::Rule; - use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_diagnostics, settings}; @@ -95,10 +94,7 @@ mod tests { let snapshot = format!("preview__{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_gettext").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/mod.rs b/crates/ruff_linter/src/rules/flake8_pyi/mod.rs index 0f8b5f2cc7..d5e2c235c1 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/mod.rs @@ -11,7 +11,6 @@ mod tests { use crate::registry::Rule; use crate::rules::pep8_naming; - use crate::settings::types::PreviewMode; use crate::source_kind::SourceKind; use crate::test::{test_contents, test_path}; use crate::{assert_diagnostics, assert_diagnostics_diff, settings}; @@ -156,14 +155,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("flake8_pyi").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Disabled, - ..settings::LinterSettings::for_rule(rule_code) - }, - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code), + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), ); Ok(()) } @@ -195,10 +188,7 @@ mod tests { let snapshot = format!("py38_{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_pyi").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY38.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY38), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/flake8_simplify/mod.rs b/crates/ruff_linter/src/rules/flake8_simplify/mod.rs index 7851ed73f9..6109a05e50 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/mod.rs @@ -11,7 +11,6 @@ mod tests { use crate::registry::Rule; use crate::settings::LinterSettings; - use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_diagnostics, assert_diagnostics_diff, settings}; @@ -69,10 +68,7 @@ mod tests { ); let diagnostics = test_path( Path::new("flake8_simplify").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs index c04a64f236..926fd3f32e 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs @@ -164,10 +164,7 @@ mod tests { let snapshot = format!("pre_py310_{}_{}", rule_code.name(), path.to_string_lossy()); let diagnostics = test_path( Path::new("flake8_type_checking").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY39.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_target_version(PythonVersion::PY39), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -643,10 +640,8 @@ mod tests { fn contents_preview(contents: &str, snapshot: &str) { let diagnostics = test_snippet( contents, - &settings::LinterSettings { - preview: settings::types::PreviewMode::Enabled, - ..settings::LinterSettings::for_rules(Linter::Flake8TypeChecking.rules()) - }, + &settings::LinterSettings::for_rules(Linter::Flake8TypeChecking.rules()) + .with_preview_mode(), ); assert_diagnostics!(snapshot, diagnostics); } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs index 5d0938d5c6..23c0fd4498 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs @@ -93,14 +93,10 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("flake8_use_pathlib").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY313.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY314.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_target_version(PythonVersion::PY313), + &settings::LinterSettings::for_rule(rule_code) + .with_target_version(PythonVersion::PY314), ); Ok(()) } @@ -162,10 +158,7 @@ mod tests { ); let diagnostics = test_path( Path::new("flake8_use_pathlib").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -180,10 +173,8 @@ mod tests { ); let diagnostics = test_path( Path::new("flake8_use_pathlib").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY314.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_target_version(PythonVersion::PY314), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/perflint/mod.rs b/crates/ruff_linter/src/rules/perflint/mod.rs index 6f4ae7201f..13ddca3125 100644 --- a/crates/ruff_linter/src/rules/perflint/mod.rs +++ b/crates/ruff_linter/src/rules/perflint/mod.rs @@ -12,7 +12,6 @@ mod tests { use crate::assert_diagnostics; use crate::registry::Rule; use crate::settings::LinterSettings; - use crate::settings::types::PreviewMode; use crate::test::test_path; #[test_case(Rule::UnnecessaryListCast, Path::new("PERF101.py"))] @@ -43,11 +42,9 @@ mod tests { ); let diagnostics = test_path( Path::new("perflint").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Enabled, - unresolved_target_version: PythonVersion::PY310.into(), - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code) + .with_preview_mode() + .with_target_version(PythonVersion::PY310), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index 455646e0a1..fcbd0345fd 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -107,10 +107,7 @@ mod tests { ); let diagnostics = test_path( Path::new("pycodestyle").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -126,10 +123,8 @@ mod tests { let tested_notebook = assert_notebook_path( &actual, &expected, - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(Rule::TooManyNewlinesAtEndOfFile) - }, + &settings::LinterSettings::for_rule(Rule::TooManyNewlinesAtEndOfFile) + .with_preview_mode(), )?; assert_eq!(tested_notebook.diagnostics.len(), 3); diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 3cfdb088ff..e4f9641dde 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -223,10 +223,8 @@ mod tests { fn f821_with_builtin_added_on_new_py_version_but_old_target_version_specified() { let diagnostics = test_snippet( "PythonFinalizationError", - &LinterSettings { - unresolved_target_version: ruff_python_ast::PythonVersion::PY312.into(), - ..LinterSettings::for_rule(Rule::UndefinedName) - }, + &LinterSettings::for_rule(Rule::UndefinedName) + .with_target_version(ruff_python_ast::PythonVersion::PY312), ); assert_diagnostics!(diagnostics); } @@ -236,10 +234,8 @@ mod tests { // frozendict is available starting in Python 3.15. let diagnostics = test_snippet( "frozendict", - &LinterSettings { - unresolved_target_version: ruff_python_ast::PythonVersion::PY315.into(), - ..LinterSettings::for_rule(Rule::UndefinedName) - }, + &LinterSettings::for_rule(Rule::UndefinedName) + .with_target_version(ruff_python_ast::PythonVersion::PY315), ); assert!(diagnostics.is_empty()); } @@ -249,10 +245,8 @@ mod tests { // frozendict is not available before Python 3.15. let diagnostics = test_snippet( "frozendict", - &LinterSettings { - unresolved_target_version: ruff_python_ast::PythonVersion::PY314.into(), - ..LinterSettings::for_rule(Rule::UndefinedName) - }, + &LinterSettings::for_rule(Rule::UndefinedName) + .with_target_version(ruff_python_ast::PythonVersion::PY314), ); assert_diagnostics!(diagnostics); } @@ -274,10 +268,7 @@ mod tests { ); let diagnostics = test_path( Path::new("pyflakes").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - }, + &LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -471,10 +462,7 @@ mod tests { snapshot, Path::new("pyflakes").join(path).as_path(), &LinterSettings::for_rule(rule_code), - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - } + &LinterSettings::for_rule(rule_code).with_preview_mode() ); Ok(()) } @@ -612,10 +600,7 @@ mod tests { is_stub: false, }, Path::new("f401_preview_submodule.py"), - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(Rule::UnusedImport) - }, + &LinterSettings::for_rule(Rule::UnusedImport).with_preview_mode(), ) .0; assert_diagnostics!(snapshot, diagnostics); @@ -757,10 +742,7 @@ mod tests { fn f811_annotated_assignment_redefinition() -> Result<()> { let diagnostics = test_path( Path::new("pyflakes/F811_34.py"), - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(Rule::RedefinedWhileUnused) - }, + &LinterSettings::for_rule(Rule::RedefinedWhileUnused).with_preview_mode(), )?; assert_diagnostics!(diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/pygrep_hooks/mod.rs b/crates/ruff_linter/src/rules/pygrep_hooks/mod.rs index 17f2465a18..8b4e7e438c 100644 --- a/crates/ruff_linter/src/rules/pygrep_hooks/mod.rs +++ b/crates/ruff_linter/src/rules/pygrep_hooks/mod.rs @@ -11,7 +11,6 @@ mod tests { use crate::registry::Rule; use crate::settings::LinterSettings; - use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_diagnostics, assert_diagnostics_diff, settings}; @@ -43,14 +42,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("pygrep_hooks").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Disabled, - ..LinterSettings::for_rule(rule_code) - }, - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - } + &LinterSettings::for_rule(rule_code), + &LinterSettings::for_rule(rule_code).with_preview_mode() ); Ok(()) } diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index d8670cb72c..2b06835737 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -275,14 +275,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("pylint").join(path).as_path(), - &LinterSettings { - preview: PreviewMode::Disabled, - ..LinterSettings::for_rule(rule_code) - }, - &LinterSettings { - preview: PreviewMode::Enabled, - ..LinterSettings::for_rule(rule_code) - } + &LinterSettings::for_rule(rule_code), + &LinterSettings::for_rule(rule_code).with_preview_mode() ); Ok(()) } diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 0faa253c6e..566280d98a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -224,11 +224,9 @@ mod tests { let snapshot = path.to_string_lossy().to_string(); let diagnostics = test_path( Path::new("pyupgrade").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - unresolved_target_version: PythonVersion::PY312.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_preview_mode() + .with_target_version(PythonVersion::PY312), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -239,10 +237,7 @@ mod tests { let snapshot = format!("{}__preview", path.to_string_lossy()); let diagnostics = test_path( Path::new("pyupgrade").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -253,10 +248,8 @@ mod tests { let snapshot = format!("rules_py313__{}", path.to_string_lossy()); let diagnostics = test_path( Path::new("pyupgrade").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY313.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_target_version(PythonVersion::PY313), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -272,14 +265,8 @@ mod tests { assert_diagnostics_diff!( snapshot, Path::new("pyupgrade").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Disabled, - ..settings::LinterSettings::for_rule(rule_code) - }, - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code), + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), ); Ok(()) } @@ -304,10 +291,8 @@ mod tests { fn async_timeout_error_alias_not_applied_py310() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/UP041.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY310.into(), - ..settings::LinterSettings::for_rule(Rule::TimeoutErrorAlias) - }, + &settings::LinterSettings::for_rule(Rule::TimeoutErrorAlias) + .with_target_version(PythonVersion::PY310), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -317,10 +302,8 @@ mod tests { fn non_pep695_type_alias_not_applied_py311() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/UP040.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY311.into(), - ..settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias) - }, + &settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias) + .with_target_version(PythonVersion::PY311), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -362,10 +345,8 @@ mod tests { fn future_annotations_pep_585_p37() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/future_annotations.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY37.into(), - ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) - }, + &settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) + .with_target_version(PythonVersion::PY37), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -375,10 +356,8 @@ mod tests { fn future_annotations_pep_585_py310() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/future_annotations.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY310.into(), - ..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) - }, + &settings::LinterSettings::for_rule(Rule::NonPEP585Annotation) + .with_target_version(PythonVersion::PY310), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -420,10 +399,8 @@ mod tests { fn datetime_utc_alias_py311() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/UP017.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY311.into(), - ..settings::LinterSettings::for_rule(Rule::DatetimeTimezoneUTC) - }, + &settings::LinterSettings::for_rule(Rule::DatetimeTimezoneUTC) + .with_target_version(PythonVersion::PY311), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -433,10 +410,8 @@ mod tests { fn unpack_pep_646_py311() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/UP044.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY311.into(), - ..settings::LinterSettings::for_rule(Rule::NonPEP646Unpack) - }, + &settings::LinterSettings::for_rule(Rule::NonPEP646Unpack) + .with_target_version(PythonVersion::PY311), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -538,10 +513,8 @@ mod tests { let snapshot = "UP043.pyi"; let diagnostics = test_path( Path::new("pyupgrade/UP043.pyi"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY312.into(), - ..settings::LinterSettings::for_rule(Rule::UnnecessaryDefaultTypeArgs) - }, + &settings::LinterSettings::for_rule(Rule::UnnecessaryDefaultTypeArgs) + .with_target_version(PythonVersion::PY312), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -551,10 +524,8 @@ mod tests { fn up045_future_annotations_py39() -> Result<()> { let diagnostics = test_path( Path::new("pyupgrade/UP045_py39.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY39.into(), - ..settings::LinterSettings::for_rule(Rule::NonPEP604AnnotationOptional) - }, + &settings::LinterSettings::for_rule(Rule::NonPEP604AnnotationOptional) + .with_target_version(PythonVersion::PY39), )?; assert_diagnostics!(diagnostics); Ok(()) diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 4252591fd1..e0490ff258 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -243,14 +243,10 @@ mod tests { fn missing_fstring_syntax_backslash_py311() -> Result<()> { assert_diagnostics_diff!( Path::new("ruff/RUF027_0.py"), - &LinterSettings { - unresolved_target_version: PythonVersion::PY312.into(), - ..LinterSettings::for_rule(Rule::MissingFStringSyntax) - }, - &LinterSettings { - unresolved_target_version: PythonVersion::PY311.into(), - ..LinterSettings::for_rule(Rule::MissingFStringSyntax) - }, + &LinterSettings::for_rule(Rule::MissingFStringSyntax) + .with_target_version(PythonVersion::PY312), + &LinterSettings::for_rule(Rule::MissingFStringSyntax) + .with_target_version(PythonVersion::PY311), ); Ok(()) } @@ -353,10 +349,8 @@ mod tests { print(None | (int)and 2) ", - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY313.into(), - ..settings::LinterSettings::for_rule(Rule::NoneNotAtEndOfUnion) - }, + &settings::LinterSettings::for_rule(Rule::NoneNotAtEndOfUnion) + .with_target_version(PythonVersion::PY313), ); assert_diagnostics!("PY313_RUF036_runtime_evaluated", diagnostics); } @@ -365,10 +359,8 @@ mod tests { fn quadratic_list_summation_py315() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF017_0.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY315.into(), - ..settings::LinterSettings::for_rule(Rule::QuadraticListSummation) - }, + &settings::LinterSettings::for_rule(Rule::QuadraticListSummation) + .with_target_version(PythonVersion::PY315), )?; assert_diagnostics!("PY315_RUF017_RUF017_0.py", diagnostics); Ok(()) @@ -378,12 +370,8 @@ mod tests { fn unnecessary_iterable_allocation_for_first_element_py315() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF015_py315.py"), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY315.into(), - ..settings::LinterSettings::for_rule( - Rule::UnnecessaryIterableAllocationForFirstElement, - ) - }, + &settings::LinterSettings::for_rule(Rule::UnnecessaryIterableAllocationForFirstElement) + .with_target_version(PythonVersion::PY315), )?; assert_diagnostics!("PY315_RUF015_RUF015_py315.py", diagnostics); Ok(()) @@ -393,10 +381,8 @@ mod tests { fn access_annotations_from_class_dict_py310() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF063.py"), - &LinterSettings { - unresolved_target_version: PythonVersion::PY310.into(), - ..LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) - }, + &LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) + .with_target_version(PythonVersion::PY310), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -406,10 +392,8 @@ mod tests { fn access_annotations_from_class_dict_py314() -> Result<()> { let diagnostics = test_path( Path::new("ruff/RUF063.py"), - &LinterSettings { - unresolved_target_version: PythonVersion::PY314.into(), - ..LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) - }, + &LinterSettings::for_rule(Rule::AccessAnnotationsFromClassDict) + .with_target_version(PythonVersion::PY314), )?; assert_diagnostics!(diagnostics); Ok(()) @@ -820,10 +804,7 @@ mod tests { ); let diagnostics = test_path( Path::new("ruff").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code).with_preview_mode(), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -838,11 +819,9 @@ mod tests { ); let diagnostics = test_path( Path::new("ruff").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - unresolved_target_version: PythonVersion::PY37.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_preview_mode() + .with_target_version(PythonVersion::PY37), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -857,11 +836,9 @@ mod tests { ); let diagnostics = test_path( Path::new("ruff").join(path).as_path(), - &settings::LinterSettings { - preview: PreviewMode::Enabled, - unresolved_target_version: PythonVersion::PY38.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_preview_mode() + .with_target_version(PythonVersion::PY38), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) @@ -904,10 +881,8 @@ mod tests { ); let diagnostics = test_path( Path::new("ruff").join(path).as_path(), - &settings::LinterSettings { - unresolved_target_version: PythonVersion::PY314.into(), - ..settings::LinterSettings::for_rule(rule_code) - }, + &settings::LinterSettings::for_rule(rule_code) + .with_target_version(PythonVersion::PY314), )?; assert_diagnostics!(snapshot, diagnostics); Ok(()) From f78d51606f73df5583b19e2205052f7592131ffe Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 3 Aug 2026 09:46:10 +0200 Subject: [PATCH 215/390] [ty] Make parsed modules Python-version aware (#26856) ## Summary This is the first PR out of 3. It extends `ty_python_semantic` and lower crates to support checking files that use different Python versions while using a single Salsa db. The motivation for supporting different Python versions is that scrolls need to be checked independently. Initially, I planned to implement scroll support in a similar way to how we support multiple workspace folders in VS Code by using multiple dbs, but it turned out, that they're different enough. The main difference is that scrolls are checked as part of their enclosing project, and the diagnostics from scrolls are merged with the diagnostics from the project. If we used separate scroll dbs, then the scroll diagnostics would use `File`s pointing into the scroll db and all other diagnostics would use `File`s pointing into the main db. There's nothing that would allow us to tell to which db any such `File`s belonged, and `File`s from different dbs can even share the same value. There's also no lifetime that binds them to one particular db because they're inputs. We could have decided to rewrite the `Diagnostic`s for scrolls and convert the scroll db `File`s to project db `File`s, but that felt very brittle. The other disadvantage was that it became very difficult to tell which query is cached because you now also need to reason about whether the current query is cached for a specific argument and db (and not just the argument). This is why I decided to explore whether we could support checking files with different Python version, search paths, and platforms, and this PR is the first step towards this. It adds support for checking files with different Python versions. Specifically, this PR removes the `db.python_version` from `ruff_db::Db` and lifts it up to `ty_project::Db`, proving that no semantic crate depends on it. The first step is to make `parsed_module` Python version aware. That is, instead of calling `db.python_version`, parametrize the query by file and Python version. For this, I introduced a new `PythonFile` which is a `(File, PythonVersion)` tuple. We now attach this `PythonFile` to `Scope` (instead of `File`), because `::node` accessors need it to retrieve their node. That in turn required `semantic_index` to take a `PythonFile` instead of `File` as well. This was a mostly smooth ride. Supporting multiple Python versions in the module resolver required adding a `PythonVersion` argument to most queries. The next PR will replace the `PythonVersion` with a new `ResolverSettings` (or similar name) interned struct that wraps both Python version and search paths. This was also mostly straightforward. The hard part was to remove all `db.python_version` calls from type inference and type operations. Today, we call `db.python_version` where we need it, but we can't do this anymore. Instead, we have to pass the Python version down to each function that needs it. Now, this is not true for all functions. Functions that have access to a scope (via `Definition`, `Expression` or similar) could retrieve the Python version from there (because `Scope` stores a `PythonFile`). I decided against that and, instead established the following policy: * Explicitly pass the Python version to all semantic functions. * The only exception to this are Salsa queries that have exactly one argument other than `db`. For those, retrieving the Python version from their `Scope` is allowed for performance reasons. Ultimately what that means: The context should be initialized at the query boundary and then not change, unless we cross into another Salsa query (but assert that the python version does not change). There are two motivations for this: * I think it's easier to reason about what Python version a type operation uses if there's a consistent way of retrieving it (always as argument) * At query boundaries, we can assert that the explicitly passed Python version and the `Scope`'s Python version are identical, before calling into the query (debug assert). However, supporting different Python versions now requires that we parametrize all type operations with the Python version under which the operation should be performed (in the future, this will be Python version, search paths, and platforms). Now, passing `db` and `PythonVersion` as separate arguments not only results in a huge diff (add another 7k changes on top of the current diff), is cumbersome, it also is slower. That's why I this PR introduces a new `SemanticContext` that wraps a `db` and a `PythonVersion` (Happy to discuss names). The variable is consistently referred to as `ctx` to reduce the diff (using `context` makes it more likely that lines break). ## Next steps * Change `SemanticContext` to store a `db` and `SearchPathSettings` to support different search paths * After that, change `SemanticContext` to store a `db` and a `ProgramFile` (which is a `(File, Program)` tuple. * Use the new infrastructure for scroll support ## Performance There's currently a 2-3% walltime regression. Codex reports a lower local regression. There are some options that we can explore if we think this regression is too big. However, it would make the model a little more muddy. The most promising is probably to have two versions of very hot queries. E.g. `is_redundant_with` could have a `is_redundant_with_project` and `is_redundant_with_script` query. `is_redundant_with_project` would use `db.python_version` internally (yes, we'd have to add that back). I don't like that. Not only does it reintroduce `db.python_version` and make it very easy that we use it somewhere where we should not. ## Review I, honestly, tried to split this PR into smaller, reviewable commits. But I don't think I succeeded. But I also don't think it makes sense to review this commit line by line. I'm sure there are places where we still pass `db` where we should pass a `ctx`. I'm sure there are some call sites where we should retrieve the `python_version` in a different way. But I'm not sure it's worth finding all these places in a review. Instead, I'd focus on the overall change. What's your reaction on `SemanticContext`? That we now need to pass it everywhere? What about my policy that we use a single `SemanticContext` within a query? Should we relax this constraint, so that `SemanticIndex` must be passed in fewer places, if the `PythonVersion` can be retrieved at the relevant call site (because it has `Scope`)? The downside of that is that it becomes more difficult to assert that we don't mix programs in a single inference, but is this something we should really be concerned about? Do you think the performance regression is to big for this feature? ## Notes * Make associated tracked methods that don't take `python_version` normal methods with an inner query instead. The method should take a `python_version`, and we assert (before calling the query), that the target's (self's) `python_version` matches the passed python version --- Cargo.lock | 2 + crates/mdtest/src/assertion.rs | 5 +- crates/mdtest/src/lib.rs | 4 - crates/mdtest/src/matcher.rs | 8 +- crates/ruff/src/commands/analyze_graph.rs | 11 +- .../benches/module_resolution.rs | 7 +- crates/ruff_db/src/lib.rs | 23 +- crates/ruff_db/src/parsed.rs | 75 +- crates/ruff_graph/src/db.rs | 8 - crates/ruff_graph/src/lib.rs | 9 +- crates/ruff_graph/src/resolver.rs | 23 +- crates/ruff_mdtest/src/db.rs | 4 - crates/ruff_mdtest/src/lib.rs | 11 + crates/ruff_python_formatter/src/lib.rs | 3 +- crates/ty/src/lib.rs | 11 +- crates/ty/tests/file_watching.rs | 14 +- crates/ty_completion_bench/src/main.rs | 4 +- crates/ty_completion_eval/src/main.rs | 4 +- crates/ty_ide/src/all_symbols.rs | 30 +- crates/ty_ide/src/call_hierarchy.rs | 15 +- .../src/call_hierarchy/incoming_calls.rs | 61 +- .../src/call_hierarchy/outgoing_calls.rs | 16 +- crates/ty_ide/src/code_action.rs | 17 +- crates/ty_ide/src/completion.rs | 157 +- crates/ty_ide/src/doc_highlights.rs | 12 +- crates/ty_ide/src/document_symbols.rs | 14 +- crates/ty_ide/src/find_references.rs | 6 +- crates/ty_ide/src/folding_range.rs | 8 +- crates/ty_ide/src/goto.rs | 29 +- crates/ty_ide/src/goto_declaration.rs | 13 +- crates/ty_ide/src/goto_definition.rs | 13 +- crates/ty_ide/src/goto_implementation.rs | 36 +- crates/ty_ide/src/goto_type_definition.rs | 18 +- crates/ty_ide/src/hints.rs | 7 +- crates/ty_ide/src/hover.rs | 50 +- crates/ty_ide/src/importer.rs | 33 +- crates/ty_ide/src/inlay_hints.rs | 48 +- crates/ty_ide/src/lib.rs | 36 +- crates/ty_ide/src/references.rs | 39 +- crates/ty_ide/src/rename.rs | 37 +- crates/ty_ide/src/selection_range.rs | 10 +- crates/ty_ide/src/semantic_tokens.rs | 29 +- crates/ty_ide/src/signature_help.rs | 42 +- crates/ty_ide/src/symbols.rs | 44 +- crates/ty_ide/src/type_hierarchy.rs | 51 +- crates/ty_ide/src/workspace_symbols.rs | 3 +- crates/ty_module_resolver/src/db.rs | 8 +- crates/ty_module_resolver/src/list.rs | 81 +- crates/ty_module_resolver/src/module.rs | 45 +- crates/ty_module_resolver/src/module_name.rs | 24 +- crates/ty_module_resolver/src/resolve.rs | 204 +- crates/ty_project/src/db.rs | 38 +- crates/ty_project/src/lib.rs | 31 +- crates/ty_python_core/src/ast_ids.rs | 18 +- crates/ty_python_core/src/ast_node_ref.rs | 53 +- crates/ty_python_core/src/builder.rs | 30 +- crates/ty_python_core/src/db.rs | 4 - crates/ty_python_core/src/definition.rs | 17 +- crates/ty_python_core/src/expression.rs | 10 + crates/ty_python_core/src/lib.rs | 176 +- crates/ty_python_core/src/predicate.rs | 20 +- crates/ty_python_core/src/re_exports.rs | 14 +- crates/ty_python_core/src/scope.rs | 30 +- crates/ty_python_core/src/statement.rs | 14 +- crates/ty_python_core/src/unpack.rs | 15 +- crates/ty_python_semantic/src/db.rs | 18 +- .../ty_python_semantic/src/diagnostic/mod.rs | 4 +- crates/ty_python_semantic/src/dunder_all.rs | 43 +- crates/ty_python_semantic/src/fixes.rs | 55 +- crates/ty_python_semantic/src/lib.rs | 26 +- crates/ty_python_semantic/src/place.rs | 449 ++-- crates/ty_python_semantic/src/pull_types.rs | 6 +- crates/ty_python_semantic/src/reachability.rs | 196 +- .../ty_python_semantic/src/semantic_model.rs | 132 +- crates/ty_python_semantic/src/subscript.rs | 89 +- crates/ty_python_semantic/src/suppression.rs | 23 +- .../src/suppression/add_ignore.rs | 12 +- crates/ty_python_semantic/src/types.rs | 2074 ++++++++++------- .../src/types/attribute_write.rs | 115 +- crates/ty_python_semantic/src/types/bool.rs | 161 +- .../src/types/bound_super.rs | 209 +- crates/ty_python_semantic/src/types/call.rs | 99 +- .../src/types/call/arguments.rs | 97 +- .../ty_python_semantic/src/types/call/bind.rs | 1121 +++++---- .../src/types/call/bind/constructor.rs | 113 +- .../src/types/call/bind/enum_property.rs | 3 +- .../ty_python_semantic/src/types/callable.rs | 164 +- crates/ty_python_semantic/src/types/class.rs | 403 ++-- .../src/types/class/dynamic_literal.rs | 68 +- .../src/types/class/enum_literal.rs | 57 +- .../src/types/class/known.rs | 255 +- .../src/types/class/named_tuple.rs | 113 +- .../src/types/class/static_literal.rs | 448 ++-- .../src/types/class/typed_dict.rs | 238 +- .../src/types/class_base.rs | 137 +- .../src/types/constraints.rs | 1760 ++++++++------ .../ty_python_semantic/src/types/context.rs | 154 +- .../src/types/context_manager.rs | 80 +- crates/ty_python_semantic/src/types/cyclic.rs | 77 +- .../src/types/dedicated/pydantic.rs | 130 +- .../src/types/definition.rs | 4 +- .../src/types/diagnostic.rs | 416 ++-- .../ty_python_semantic/src/types/display.rs | 1020 ++++---- crates/ty_python_semantic/src/types/enums.rs | 289 ++- .../ty_python_semantic/src/types/equality.rs | 400 ++-- .../src/types/equality/enums.rs | 159 +- .../ty_python_semantic/src/types/function.rs | 383 +-- .../ty_python_semantic/src/types/generics.rs | 674 ++++-- .../src/types/ide_support.rs | 404 ++-- .../src/types/ide_support/unreachable_code.rs | 12 +- .../src/types/ide_support/unused_bindings.rs | 10 +- crates/ty_python_semantic/src/types/infer.rs | 268 ++- .../src/types/infer/builder.rs | 1321 ++++++----- .../infer/builder/annotation_expression.rs | 11 +- .../infer/builder/attribute_assignment.rs | 79 +- .../types/infer/builder/binary_expressions.rs | 91 +- .../src/types/infer/builder/class.rs | 101 +- .../src/types/infer/builder/dict.rs | 16 +- .../src/types/infer/builder/dynamic_class.rs | 42 +- .../src/types/infer/builder/enum_call.rs | 145 +- .../types/infer/builder/final_attribute.rs | 49 +- .../src/types/infer/builder/function.rs | 136 +- .../src/types/infer/builder/imports.rs | 38 +- .../src/types/infer/builder/named_tuple.rs | 87 +- .../src/types/infer/builder/new_class.rs | 24 +- .../builder/post_inference/dynamic_class.rs | 14 +- .../builder/post_inference/final_variable.rs | 5 +- .../infer/builder/post_inference/function.rs | 31 +- .../post_inference/overloaded_function.rs | 44 +- .../builder/post_inference/static_class.rs | 70 +- .../builder/post_inference/typed_dict.rs | 97 +- .../infer/builder/post_inference/typeguard.rs | 7 +- .../src/types/infer/builder/subscript.rs | 459 ++-- .../src/types/infer/builder/type_call.rs | 31 +- .../types/infer/builder/type_expression.rs | 205 +- .../src/types/infer/builder/type_form.rs | 33 +- .../src/types/infer/builder/typed_dict.rs | 62 +- .../src/types/infer/builder/typevar.rs | 163 +- .../src/types/infer/comparisons.rs | 133 +- .../src/types/infer/tests.rs | 223 +- .../ty_python_semantic/src/types/instance.rs | 295 ++- .../ty_python_semantic/src/types/iteration.rs | 218 +- .../src/types/known_instance.rs | 132 +- .../src/types/list_members.rs | 211 +- .../ty_python_semantic/src/types/literal.rs | 25 +- .../src/types/match_pattern.rs | 352 +-- crates/ty_python_semantic/src/types/member.rs | 5 +- crates/ty_python_semantic/src/types/method.rs | 148 +- crates/ty_python_semantic/src/types/mro.rs | 120 +- crates/ty_python_semantic/src/types/narrow.rs | 1131 +++++---- .../src/types/narrow/containment.rs | 64 +- .../ty_python_semantic/src/types/newtype.rs | 40 +- .../ty_python_semantic/src/types/overrides.rs | 167 +- .../src/types/property_tests.rs | 183 +- .../types/property_tests/type_generation.rs | 153 +- .../src/types/protocol_class.rs | 797 ++++--- .../ty_python_semantic/src/types/relation.rs | 510 ++-- .../src/types/relation_error.rs | 73 +- .../src/types/set_theoretic.rs | 248 +- .../src/types/set_theoretic/builder.rs | 559 +++-- .../src/types/signatures.rs | 553 +++-- .../src/types/special_form.rs | 80 +- .../src/types/subclass_of.rs | 179 +- .../ty_python_semantic/src/types/subscript.rs | 162 +- crates/ty_python_semantic/src/types/tests.rs | 517 ++-- crates/ty_python_semantic/src/types/tuple.rs | 402 +++- .../src/types/tuple/promotion.rs | 45 +- .../src/types/type_alias.rs | 65 +- .../src/types/type_expansion.rs | 107 +- .../ty_python_semantic/src/types/type_form.rs | 46 +- .../src/types/typed_dict.rs | 286 ++- .../ty_python_semantic/src/types/typevar.rs | 534 +++-- .../ty_python_semantic/src/types/unpacker.rs | 81 +- .../ty_python_semantic/src/types/variance.rs | 19 +- .../ty_python_semantic/src/types/visitor.rs | 127 +- crates/ty_python_semantic/tests/corpus.rs | 16 +- crates/ty_server/Cargo.toml | 1 + .../ty_server/src/server/api/diagnostics.rs | 3 +- .../requests/call_hierarchy_incoming_calls.rs | 6 +- .../requests/call_hierarchy_outgoing_calls.rs | 6 +- .../src/server/api/requests/code_action.rs | 5 +- .../src/server/api/requests/completion.rs | 9 +- .../src/server/api/requests/doc_highlights.rs | 6 +- .../server/api/requests/document_symbols.rs | 4 +- .../src/server/api/requests/folding_range.rs | 46 +- .../server/api/requests/goto_declaration.rs | 6 +- .../server/api/requests/goto_definition.rs | 6 +- .../api/requests/goto_implementation.rs | 6 +- .../api/requests/goto_type_definition.rs | 6 +- .../src/server/api/requests/hover.rs | 5 +- .../src/server/api/requests/inlay_hints.rs | 9 +- .../api/requests/prepare_call_hierarchy.rs | 8 +- .../src/server/api/requests/prepare_rename.rs | 5 +- .../api/requests/prepare_type_hierarchy.rs | 8 +- .../src/server/api/requests/references.rs | 9 +- .../src/server/api/requests/rename.rs | 9 +- .../server/api/requests/selection_range.rs | 5 +- .../src/server/api/requests/signature_help.rs | 6 +- .../api/requests/workspace_diagnostic.rs | 6 +- .../src/server/api/semantic_tokens.rs | 5 +- .../src/server/api/type_hierarchy.rs | 3 + crates/ty_test/src/db.rs | 12 +- crates/ty_test/src/lib.rs | 63 +- crates/ty_wasm/Cargo.toml | 1 + crates/ty_wasm/src/lib.rs | 97 +- fuzz/fuzz_targets/ty_check_invalid_syntax.rs | 14 +- 206 files changed, 17671 insertions(+), 10171 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 107d64d4ca..271125300f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4912,6 +4912,7 @@ dependencies = [ "ty_module_resolver", "ty_project", "ty_python_core", + "ty_python_semantic", ] [[package]] @@ -4998,6 +4999,7 @@ dependencies = [ "ty_ide", "ty_project", "ty_python_core", + "ty_python_semantic", "wasm-bindgen", "wasm-bindgen-test", ] diff --git a/crates/mdtest/src/assertion.rs b/crates/mdtest/src/assertion.rs index 2c4dd279d4..a49bafbdcc 100644 --- a/crates/mdtest/src/assertion.rs +++ b/crates/mdtest/src/assertion.rs @@ -535,10 +535,12 @@ pub(crate) enum ErrorAssertionParseError<'a> { mod tests { use super::*; use crate::tests::TestDb; + use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; use ruff_db::source::line_index; use ruff_db::system::DbWithWritableSystem as _; + use ruff_python_ast::PythonVersion; use ruff_python_trivia::textwrap::dedent; use ruff_source_file::OneIndexed; @@ -546,7 +548,8 @@ mod tests { let mut db = TestDb::setup(); db.write_file("/src/test.py", source).unwrap(); let file = system_path_to_file(&db, "/src/test.py").unwrap(); - let parsed = parsed_module(&db, file).load(&db); + let parsed = + parsed_module(&db, PythonFile::new(&db, file, PythonVersion::latest_ty())).load(&db); InlineFileAssertions::from_file( source, AssertionSource::Python(&parsed), diff --git a/crates/mdtest/src/lib.rs b/crates/mdtest/src/lib.rs index 9101de6704..e99d68b34f 100644 --- a/crates/mdtest/src/lib.rs +++ b/crates/mdtest/src/lib.rs @@ -771,10 +771,6 @@ pub(crate) mod tests { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> ruff_python_ast::PythonVersion { - ruff_python_ast::PythonVersion::latest_ty() - } } impl DbWithTestSystem for TestDb { diff --git a/crates/mdtest/src/matcher.rs b/crates/mdtest/src/matcher.rs index 8b791b2414..879e3c09e3 100644 --- a/crates/mdtest/src/matcher.rs +++ b/crates/mdtest/src/matcher.rs @@ -9,10 +9,12 @@ use std::sync::LazyLock; use colored::Colorize; use path_slash::PathExt; use ruff_db::Db; +use ruff_db::PythonFile; use ruff_db::diagnostic::{Diagnostic, DiagnosticId}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_db::source::{SourceText, line_index, source_text}; +use ruff_python_ast::PythonVersion; use ruff_source_file::{LineIndex, OneIndexed}; use smallvec::SmallVec; @@ -93,6 +95,7 @@ struct LineFailures { pub fn match_file( db: &dyn Db, file: File, + python_version: PythonVersion, diagnostics: &[Diagnostic], options: RunOptions, ) -> Result, FailuresByLine> { @@ -108,7 +111,7 @@ pub fn match_file( }); (assertions, diagnostics) } else { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, PythonFile::new(db, file, python_version)).load(db); let assertions = InlineFileAssertions::from_file( source.as_str(), AssertionSource::Python(&parsed), @@ -528,6 +531,7 @@ mod tests { use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span}; use ruff_db::files::{File, 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 ruff_text_size::TextRange; @@ -588,7 +592,7 @@ mod tests { .into_iter() .map(|diagnostic| diagnostic.into_diagnostic(file)) .collect(); - super::match_file(&db, file, &diagnostics, options) + super::match_file(&db, file, PythonVersion::latest_ty(), &diagnostics, options) } fn assert_fail(result: Result, FailuresByLine>, messages: &[(usize, &[&str])]) { diff --git a/crates/ruff/src/commands/analyze_graph.rs b/crates/ruff/src/commands/analyze_graph.rs index 48fdf14a7b..4d69c6e6b8 100644 --- a/crates/ruff/src/commands/analyze_graph.rs +++ b/crates/ruff/src/commands/analyze_graph.rs @@ -11,6 +11,7 @@ use ruff_linter::package::PackageRoot; use ruff_linter::source_kind::SourceKind; use ruff_linter::{warn_user, warn_user_once}; use ruff_python_ast::SourceType; +use ruff_python_parser::ParseOptions; use ruff_workspace::resolver::{ResolvedFile, match_exclusion, project_files_in_path}; use rustc_hash::{FxBuildHasher, FxHashMap}; use std::io::Write; @@ -99,12 +100,6 @@ pub(crate) fn analyze_graph( let db = ModuleDb::from_src_roots( system, src_roots.into_iter().collect(), - pyproject_config - .settings - .analyze - .target_version - .as_tuple() - .into(), args.python .and_then(|python| SystemPathBuf::from_path_buf(python).ok()), )?; @@ -135,6 +130,7 @@ pub(crate) fn analyze_graph( let string_imports = settings.analyze.string_imports; let include_dependencies = settings.analyze.include_dependencies.get(path).cloned(); let type_checking_imports = settings.analyze.type_checking_imports; + let python_version = settings.analyze.target_version; let source_type = settings.analyze.extension.get_source_type(path); // Skip excluded files. @@ -182,7 +178,8 @@ pub(crate) fn analyze_graph( let mut imports = ModuleImports::detect( &db, source_code, - source_type.expect_python(), + ParseOptions::from(source_type.expect_python()) + .with_target_version(python_version), &path, package.as_deref(), string_imports, diff --git a/crates/ruff_benchmark/benches/module_resolution.rs b/crates/ruff_benchmark/benches/module_resolution.rs index c33fb80371..2df5acb90c 100644 --- a/crates/ruff_benchmark/benches/module_resolution.rs +++ b/crates/ruff_benchmark/benches/module_resolution.rs @@ -5,6 +5,7 @@ use std::hint::black_box; use divan::{Bencher, bench}; +use ruff_db::PythonFile; use ruff_db::files::{File, system_path_to_file}; use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem}; use ruff_ranged_value::RangedValue; @@ -12,7 +13,7 @@ use ty_module_resolver::{ModuleName, resolve_module}; use ty_project::metadata::options::{EnvironmentOptions, Options}; use ty_project::metadata::python_version::SupportedPythonVersion; use ty_project::metadata::value::RelativePathBuf; -use ty_project::{ProjectDatabase, ProjectMetadata}; +use ty_project::{Db as _, ProjectDatabase, ProjectMetadata}; const SEEDED_TARGETS: &[&str] = &["target_0", "target_1", "target_2", "target_3", "target_4"]; // Exercise stub-overlay discovery followed by normal fallback. @@ -93,8 +94,10 @@ fn ty_module_resolver(bencher: Bencher) { bencher .with_inputs(|| setup_case(PATHS)) .bench_local_refs(|case| { + let importing_file = + PythonFile::new(&case.db, case.importing_file, case.db.python_version()); for name in &case.resolves { - black_box(resolve_module(&case.db, case.importing_file, name)); + black_box(resolve_module(&case.db, importing_file, name)); } }); } diff --git a/crates/ruff_db/src/lib.rs b/crates/ruff_db/src/lib.rs index 8fddfa7968..0f3eb8d109 100644 --- a/crates/ruff_db/src/lib.rs +++ b/crates/ruff_db/src/lib.rs @@ -3,7 +3,7 @@ reason = "Prefer System trait methods over std methods" )] -use crate::files::Files; +use crate::files::{File, Files}; use crate::system::System; use crate::vendored::VendoredFileSystem; use ruff_python_ast::PythonVersion; @@ -25,6 +25,22 @@ pub mod system; pub mod testing; pub mod vendored; +/// A file paired with the Python version used to parse its contents. +/// +/// This is the key for [`parsed::parsed_module`]. Including the Python version allows the same +/// file to be parsed for different versions within a single Salsa revision without sharing an +/// incompatible AST or syntax diagnostics. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +pub struct PythonFile<'db> { + #[returns(copy)] + pub file: File, + #[returns(copy)] + pub python_version: PythonVersion, +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for PythonFile<'_> {} + #[cfg(not(target_arch = "wasm32"))] pub use std::time::{Instant, SystemTime, SystemTimeError}; @@ -61,7 +77,6 @@ pub trait Db: salsa::Database { fn vendored(&self) -> &VendoredFileSystem; fn system(&self) -> &dyn System; fn files(&self) -> &Files; - fn python_version(&self) -> PythonVersion; } /// Returns the maximum number of tasks that ty is allowed @@ -180,10 +195,6 @@ mod tests { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> ruff_python_ast::PythonVersion { - ruff_python_ast::PythonVersion::latest_ty() - } } impl DbWithTestSystem for TestDb { diff --git a/crates/ruff_db/src/parsed.rs b/crates/ruff_db/src/parsed.rs index 12899b6867..b6ff446e72 100644 --- a/crates/ruff_db/src/parsed.rs +++ b/crates/ruff_db/src/parsed.rs @@ -5,16 +5,16 @@ use arc_swap::ArcSwapOption; use get_size2::GetSize; use ruff_python_ast::{ AnyRootNodeRef, HasNodeIndex, ModExpression, ModModule, NodeIndex, NodeIndexError, - StringLiteral, + PythonVersion, StringLiteral, }; use ruff_python_parser::{ ParseError, ParseErrorType, ParseOptions, Parsed, parse_cells_unchecked, parse_string_annotation, parse_unchecked, }; -use crate::Db; use crate::files::File; use crate::source::source_text; +use crate::{Db, PythonFile}; /// Returns the parsed AST of `file`, including its token stream. /// @@ -32,23 +32,24 @@ use crate::source::source_text; /// instead it's a wild guess that it should be unlikely that incremental changes involve /// more than 200 modules. Parsed ASTs within the same revision are never evicted by Salsa. #[salsa::tracked(returns(ref), no_eq, heap_size=ruff_memory_usage::heap_size, lru=200)] -pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule { - let _span = tracing::trace_span!("parsed_module", ?file).entered(); +pub fn parsed_module(db: &dyn Db, file: PythonFile<'_>) -> ParsedModule { + let source_file = file.file(db); + let python_version = file.python_version(db); + let _span = tracing::trace_span!("parsed_module", ?source_file, %python_version).entered(); - let parsed = parsed_module_impl(db, file); + let parsed = parsed_module_impl(db, source_file, python_version); - ParsedModule::new(file, parsed) + ParsedModule::new(source_file, python_version, parsed) } pub(super) fn disable_lru(db: &mut dyn Db) { parsed_module::set_lru_capacity(db, 0); } -fn parsed_module_impl(db: &dyn Db, file: File) -> Parsed { +fn parsed_module_impl(db: &dyn Db, file: File, target_version: PythonVersion) -> Parsed { let source = source_text(db, file); let ty = file.source_type(db); - let target_version = db.python_version(); let options = ParseOptions::from(ty).with_target_version(target_version); // Notebooks parse each cell as an independent module so a syntax error confined to one cell is @@ -110,14 +111,16 @@ pub fn parsed_string_annotation( #[derive(Clone, get_size2::GetSize)] pub struct ParsedModule { file: File, + python_version: PythonVersion, #[get_size(size_fn = arc_swap_size)] inner: Arc>, } impl ParsedModule { - fn new(file: File, parsed: Parsed) -> Self { + pub fn new(file: File, python_version: PythonVersion, parsed: Parsed) -> Self { Self { file, + python_version, inner: Arc::new(ArcSwapOption::new(Some(indexed::IndexedModule::new( parsed, )))), @@ -132,7 +135,11 @@ impl ParsedModule { Some(parsed) => parsed, None => { // Re-parse the file. - let parsed = indexed::IndexedModule::new(parsed_module_impl(db, self.file)); + let parsed = indexed::IndexedModule::new(parsed_module_impl( + db, + self.file, + self.python_version, + )); tracing::debug!( "File `{}` was reparsed after being collected in the current Salsa revision", self.file.path(db) @@ -158,6 +165,11 @@ impl ParsedModule { pub fn file(&self) -> File { self.file } + + /// Returns the Python version used to parse this module. + pub fn python_version(&self) -> PythonVersion { + self.python_version + } } impl std::fmt::Debug for ParsedModule { @@ -864,6 +876,7 @@ class C[T](Base, metaclass=Meta): #[cfg(test)] mod tests { use crate::Db; + use crate::PythonFile; use crate::files::{system_path_to_file, vendored_path_to_file}; use crate::parsed::parsed_module; use crate::system::{ @@ -871,6 +884,7 @@ mod tests { }; use crate::tests::TestDb; use crate::vendored::{VendoredFileSystemBuilder, VendoredPath}; + use ruff_python_ast::PythonVersion; use zip::CompressionMethod; #[test] @@ -882,6 +896,7 @@ mod tests { let file = system_path_to_file(&db, path).unwrap(); + let file = PythonFile::new(&db, file, PythonVersion::latest_ty()); let parsed = parsed_module(&db, file).load(&db); assert!(parsed.has_valid_syntax()); @@ -898,6 +913,7 @@ mod tests { let file = system_path_to_file(&db, path).unwrap(); + let file = PythonFile::new(&db, file, PythonVersion::latest_ty()); let parsed = parsed_module(&db, file).load(&db); assert!(parsed.has_valid_syntax()); @@ -914,7 +930,8 @@ mod tests { let virtual_file = db.files().virtual_file(&db, path); - let parsed = parsed_module(&db, virtual_file.file()).load(&db); + let file = PythonFile::new(&db, virtual_file.file(), PythonVersion::latest_ty()); + let parsed = parsed_module(&db, file).load(&db); assert!(parsed.has_valid_syntax()); @@ -930,7 +947,8 @@ mod tests { let virtual_file = db.files().virtual_file(&db, path); - let parsed = parsed_module(&db, virtual_file.file()).load(&db); + let file = PythonFile::new(&db, virtual_file.file(), PythonVersion::latest_ty()); + let parsed = parsed_module(&db, file).load(&db); assert!(parsed.has_valid_syntax()); @@ -961,8 +979,41 @@ else: let file = vendored_path_to_file(&db, VendoredPath::new("path.pyi")).unwrap(); + let file = PythonFile::new(&db, file, PythonVersion::latest_ty()); let parsed = parsed_module(&db, file).load(&db); assert!(parsed.has_valid_syntax()); } + + #[test] + fn same_file_at_different_python_versions() -> crate::system::Result<()> { + let mut db = TestDb::new(); + db.write_file("test.py", "type Alias = int")?; + let file = system_path_to_file(&db, "test.py").unwrap(); + + let py311 = PythonFile::new(&db, file, PythonVersion::PY311); + let py312 = PythonFile::new(&db, file, PythonVersion::PY312); + let parsed_py311 = parsed_module(&db, py311); + let parsed_py312 = parsed_module(&db, py312); + + for _ in 0..2 { + assert!( + !parsed_py311 + .load(&db) + .unsupported_syntax_errors() + .is_empty() + ); + assert!( + parsed_py312 + .load(&db) + .unsupported_syntax_errors() + .is_empty() + ); + + parsed_py311.clear(); + parsed_py312.clear(); + } + + Ok(()) + } } diff --git a/crates/ruff_graph/src/db.rs b/crates/ruff_graph/src/db.rs index f49f5bb631..61d6d1af45 100644 --- a/crates/ruff_graph/src/db.rs +++ b/crates/ruff_graph/src/db.rs @@ -7,7 +7,6 @@ use ruff_db::Db as SourceDb; use ruff_db::files::Files; use ruff_db::system::{System, SystemPathBuf}; use ruff_db::vendored::{VendoredFileSystem, VendoredFileSystemBuilder}; -use ruff_python_ast::PythonVersion; use ty_module_resolver::{FallibleStrategy, SearchPathSettings, SearchPaths}; use ty_site_packages::{PythonEnvironment, SysPrefixPathOrigin}; @@ -24,7 +23,6 @@ pub struct ModuleDb { files: Files, system: Arc, search_paths: Arc, - python_version: PythonVersion, } impl ModuleDb { @@ -32,7 +30,6 @@ impl ModuleDb { pub fn from_src_roots( system: S, src_roots: Vec, - python_version: PythonVersion, venv_path: Option, ) -> Result where @@ -57,7 +54,6 @@ impl ModuleDb { files: Files::default(), system: Arc::new(system), search_paths: Arc::new(search_paths), - python_version, }; // Register the static roots for salsa durability @@ -80,10 +76,6 @@ impl SourceDb for ModuleDb { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> PythonVersion { - self.python_version - } } #[salsa::db] diff --git a/crates/ruff_graph/src/lib.rs b/crates/ruff_graph/src/lib.rs index 5f09b7f6f2..d09e822046 100644 --- a/crates/ruff_graph/src/lib.rs +++ b/crates/ruff_graph/src/lib.rs @@ -3,7 +3,6 @@ use std::collections::{BTreeMap, BTreeSet}; use anyhow::Result; use ruff_db::system::{SystemPath, SystemPathBuf}; -use ruff_python_ast::PySourceType; use ruff_python_ast::helpers::to_module_path; use ruff_python_parser::{ParseOptions, parse}; @@ -26,14 +25,15 @@ impl ModuleImports { pub fn detect( db: &ModuleDb, source: &str, - source_type: PySourceType, + parse_options: ParseOptions, path: &SystemPath, package: Option<&SystemPath>, string_imports: StringImports, type_checking_imports: bool, ) -> Result { // Parse the source code. - let parsed = parse(source, ParseOptions::from(source_type))?; + let python_version = parse_options.target_version(); + let parsed = parse(source, parse_options)?; let module_path = package.and_then(|package| to_module_path(package.as_std_path(), path.as_std_path())); @@ -48,8 +48,9 @@ impl ModuleImports { // Resolve the imports. let mut resolved_imports = ModuleImports::default(); + let resolver = Resolver::new(db, path, python_version); for import in imports { - for resolved in Resolver::new(db, path).resolve(import) { + for resolved in resolver.resolve(import) { if let Some(path) = resolved.as_system_path() { resolved_imports.insert(path.to_path_buf()); } diff --git a/crates/ruff_graph/src/resolver.rs b/crates/ruff_graph/src/resolver.rs index a7d357eb71..41eadafcf3 100644 --- a/crates/ruff_graph/src/resolver.rs +++ b/crates/ruff_graph/src/resolver.rs @@ -1,5 +1,7 @@ -use ruff_db::files::{File, FilePath, system_path_to_file}; +use ruff_db::PythonFile; +use ruff_db::files::{FilePath, system_path_to_file}; use ruff_db::system::SystemPath; +use ruff_python_ast::PythonVersion; use ty_module_resolver::{ ModuleName, resolve_module, resolve_module_confident, resolve_real_module, resolve_real_module_confident, @@ -11,15 +13,22 @@ use crate::collector::CollectedImport; /// Collect all imports for a given Python file. pub(crate) struct Resolver<'a> { db: &'a ModuleDb, - file: Option, + file: Option>, + python_version: PythonVersion, } impl<'a> Resolver<'a> { /// Initialize a [`Resolver`] with a given [`ModuleDb`]. - pub(crate) fn new(db: &'a ModuleDb, path: &SystemPath) -> Self { + pub(crate) fn new(db: &'a ModuleDb, path: &SystemPath, python_version: PythonVersion) -> Self { // If we know the importing file we can potentially resolve more imports - let file = system_path_to_file(db, path).ok(); - Self { db, file } + let file = system_path_to_file(db, path) + .ok() + .map(|file| PythonFile::new(db, file, python_version)); + Self { + db, + file, + python_version, + } } /// Resolve the [`CollectedImport`] into a [`FilePath`]. @@ -103,7 +112,7 @@ impl<'a> Resolver<'a> { let module = if let Some(file) = self.file { resolve_module(self.db, file, module_name)? } else { - resolve_module_confident(self.db, module_name)? + resolve_module_confident(self.db, self.python_version, module_name)? }; Some(module.file(self.db)?.path(self.db)) } @@ -113,7 +122,7 @@ impl<'a> Resolver<'a> { let module = if let Some(file) = self.file { resolve_real_module(self.db, file, module_name)? } else { - resolve_real_module_confident(self.db, module_name)? + resolve_real_module_confident(self.db, self.python_version, module_name)? }; Some(module.file(self.db)?.path(self.db)) } diff --git a/crates/ruff_mdtest/src/db.rs b/crates/ruff_mdtest/src/db.rs index 4770ee591f..402d0c606d 100644 --- a/crates/ruff_mdtest/src/db.rs +++ b/crates/ruff_mdtest/src/db.rs @@ -40,10 +40,6 @@ impl SourceDb for Db { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> ruff_python_ast::PythonVersion { - ruff_python_ast::PythonVersion::latest() - } } #[salsa::db] diff --git a/crates/ruff_mdtest/src/lib.rs b/crates/ruff_mdtest/src/lib.rs index c8c9fa67a4..f9b570d7bc 100644 --- a/crates/ruff_mdtest/src/lib.rs +++ b/crates/ruff_mdtest/src/lib.rs @@ -148,9 +148,20 @@ fn run_test( }; normalize_diagnostics(test_file.file, &mut diagnostics); + let path = test_file + .file + .path(db) + .as_system_path() + .expect("mdtest files are on the system"); + let python_version = settings + .linter + .resolve_target_version(path.as_std_path()) + .parser_version(); + let failure = match matcher::match_file( db, test_file.file, + python_version, &diagnostics, mdtest::RunOptions::default(), ) diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 79e8a72552..02c3847c19 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -1,3 +1,4 @@ +use ruff_db::PythonFile; use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; @@ -180,7 +181,7 @@ where pub fn formatted_file(db: &dyn Db, file: File) -> Result, FormatModuleError> { let options = db.format_options(file); - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, PythonFile::new(db, file, options.target_version())).load(db); if let Some(first) = parsed.errors().first() { return Err(FormatModuleError::ParseError(first.clone())); diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index 9e8f074da6..facee85d1c 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -409,12 +409,17 @@ impl MainLoop { } } MainLoopMode::Fix(mode) => { + let python_version = db.python_version(); let result = match mode { - FixMode::AddIgnore => { - suppress_all_diagnostics(db, result, &self.cancellation_token) - } + FixMode::AddIgnore => suppress_all_diagnostics( + db, + python_version, + result, + &self.cancellation_token, + ), FixMode::ApplyFixes => fix_all_diagnostics( db, + python_version, result, Applicability::Safe, &self.cancellation_token, diff --git a/crates/ty/tests/file_watching.rs b/crates/ty/tests/file_watching.rs index 39fbdb10c9..1bf6a10089 100644 --- a/crates/ty/tests/file_watching.rs +++ b/crates/ty/tests/file_watching.rs @@ -11,7 +11,7 @@ use ruff_db::system::{ }; use ruff_python_ast::PythonVersion; use ruff_ranged_value::{RangedValue, ValueSource}; -use ty_module_resolver::{Module, ModuleName, resolve_module_confident}; +use ty_module_resolver::{Module, ModuleName}; use ty_project::metadata::options::{EnvironmentOptions, Options, SrcOptions}; use ty_project::metadata::pyproject::{PyProject, Tool}; use ty_project::metadata::python_version::SupportedPythonVersion; @@ -19,6 +19,7 @@ use ty_project::metadata::value::{RelativeGlobPattern, RelativePathBuf}; use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher}; use ty_project::{ChangeResult, Db, ProjectDatabase, ProjectMetadata}; use ty_python_core::platform::PythonPlatform; +use ty_python_core::program::Program; use ty_static::EnvVars; struct TestCase { @@ -32,6 +33,17 @@ struct TestCase { root_dir: SystemPathBuf, } +fn resolve_module_confident<'db>( + db: &'db ProjectDatabase, + module_name: &ModuleName, +) -> Option> { + ty_module_resolver::resolve_module_confident( + db, + Program::get(db).python_version(db), + module_name, + ) +} + impl TestCase { fn project_path(&self, relative: impl AsRef) -> SystemPathBuf { SystemPath::absolute(relative, self.db.project().root(&self.db)) diff --git a/crates/ty_completion_bench/src/main.rs b/crates/ty_completion_bench/src/main.rs index 914fe754a3..e99aa13494 100644 --- a/crates/ty_completion_bench/src/main.rs +++ b/crates/ty_completion_bench/src/main.rs @@ -11,9 +11,11 @@ use std::process::ExitCode; use anyhow::{Context, anyhow}; use clap::Parser; +use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; use ty_ide::{Completion, CompletionCapabilities}; +use ty_project::Db as _; use ty_project::metadata::Options; use ty_project::metadata::options::EnvironmentOptions; use ty_project::metadata::value::RelativePathBuf; @@ -142,7 +144,7 @@ fn get_completions<'db>( db, &settings, CompletionCapabilities::default(), - file, + PythonFile::new(db, file, db.python_version()), offset, )) } diff --git a/crates/ty_completion_eval/src/main.rs b/crates/ty_completion_eval/src/main.rs index 87496beace..57ab654e50 100644 --- a/crates/ty_completion_eval/src/main.rs +++ b/crates/ty_completion_eval/src/main.rs @@ -10,10 +10,12 @@ use anyhow::{Context, anyhow}; use clap::Parser; use regex::bytes::Regex; +use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; use ty_ide::{Completion, CompletionCapabilities}; use ty_module_resolver::ModuleName; +use ty_project::Db as _; use ty_project::metadata::Options; use ty_project::metadata::options::EnvironmentOptions; use ty_project::metadata::value::RelativePathBuf; @@ -330,7 +332,7 @@ impl Task { &self.db, &self.settings, CompletionCapabilities::default(), - file, + PythonFile::new(&self.db, file, self.db.python_version()), offset, ); Ok(completions) diff --git a/crates/ty_ide/src/all_symbols.rs b/crates/ty_ide/src/all_symbols.rs index dfce6c8c2b..ae54f569ac 100644 --- a/crates/ty_ide/src/all_symbols.rs +++ b/crates/ty_ide/src/all_symbols.rs @@ -1,6 +1,6 @@ use compact_str::CompactString; use rayon::prelude::*; -use ruff_db::files::File; +use ruff_db::{PythonFile, files::File}; use ty_module_resolver::{Module, ModuleName, all_modules, resolve_real_shadowable_module}; use ty_project::{Db, parallel::ParallelIteratorExt}; @@ -15,7 +15,7 @@ use crate::{ /// by the query. pub fn all_symbols<'db>( db: &'db dyn Db, - importing_from: File, + importing_from: PythonFile<'db>, query: &QueryPattern, ) -> Vec> { // If the query is empty, return immediately to avoid expensive file scanning @@ -27,15 +27,12 @@ pub fn all_symbols<'db>( let _span = all_symbols_span.enter(); let typing_extensions = ModuleName::new_static("typing_extensions").unwrap(); - let is_typing_extensions_available = importing_from.is_stub(db) + let is_typing_extensions_available = importing_from.file(db).is_stub(db) || resolve_real_shadowable_module(db, importing_from, &typing_extensions).is_some(); - let results = all_modules(db) + let results = all_modules(db, importing_from.python_version(db)) .into_par_iter() .map_with_db(db, |db, module| { - let Some(file) = module.file(db) else { - return Vec::new(); - }; let name = module.name(db); // Note that this will always consider namespace @@ -58,6 +55,11 @@ pub fn all_symbols<'db>( return Vec::new(); } + let Some(python_file) = module.python_file(db) else { + return Vec::new(); + }; + let file = python_file.file(db); + let symbols_for_file_span = tracing::debug_span!( parent: &all_symbols_span, "symbols_for_file_global_only", @@ -69,7 +71,7 @@ pub fn all_symbols<'db>( if query.is_match_symbol_name(module.name(db)) { symbols.push(AllSymbolInfo::from_module(db, module, file)); } - for (_, symbol) in symbols_for_file_global_only(db, file).search(query) { + for (_, symbol) in symbols_for_file_global_only(db, python_file).search(query) { // Test functions (starting with `test_`) in third-party // packages are almost never useful to import. if is_non_first_party && symbol.name.starts_with("test_") { @@ -706,7 +708,11 @@ def zqzqzq(): info: Function zqzqzq "); - let symbols = all_symbols(&test.db, test.cursor.file, &QueryPattern::fuzzy("zqzqzq")); + let symbols = all_symbols( + &test.db, + test.python_file(test.cursor.file), + &QueryPattern::fuzzy("zqzqzq"), + ); let symbol = symbols .iter() .find_map(|info| info.symbol.as_ref()) @@ -1104,7 +1110,11 @@ def test_helper_xyzxyzxyz(): impl CursorTest { fn all_symbols(&self, query: &str) -> String { - let symbols = all_symbols(&self.db, self.cursor.file, &QueryPattern::fuzzy(query)); + let symbols = all_symbols( + &self.db, + self.python_file(self.cursor.file), + &QueryPattern::fuzzy(query), + ); if symbols.is_empty() { return "No symbols found".to_string(); diff --git a/crates/ty_ide/src/call_hierarchy.rs b/crates/ty_ide/src/call_hierarchy.rs index 6b1f0ec596..4f8b455665 100644 --- a/crates/ty_ide/src/call_hierarchy.rs +++ b/crates/ty_ide/src/call_hierarchy.rs @@ -13,6 +13,7 @@ pub(crate) mod outgoing_calls; use crate::goto::{GotoTarget, find_goto_target}; use crate::{Db, SymbolKind}; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::find_node::CoveringNode; @@ -32,7 +33,7 @@ use ty_python_semantic::{ImportAliasResolution, ResolvedDefinition, SemanticMode /// cursor on a specific `@overload def` yields just that one. pub fn prepare_call_hierarchy( db: &dyn Db, - file: File, + file: PythonFile<'_>, offset: TextSize, ) -> Option> { let module = parsed_module(db, file).load(db); @@ -48,7 +49,7 @@ pub fn prepare_call_hierarchy( continue; }; - let module_ref = parsed_module(db, def.file(db)).load(db); + let module_ref = parsed_module(db, def.python_file(db)).load(db); if let Some(item) = CallHierarchyItem::from_definition(db, resolved, &module_ref) { items.push(item); @@ -108,7 +109,7 @@ impl CallHierarchyItem { Some(CallHierarchyItem { name: Name::new(name), kind, - detail: module_detail(db, def_file), + detail: module_detail(db, def.python_file(db)), file: def_file, full_range: def.full_range(db, module).range(), selection_range: def.focus_range(db, module).range(), @@ -116,7 +117,7 @@ impl CallHierarchyItem { } } -fn module_detail(db: &dyn Db, file: File) -> Option { +fn module_detail(db: &dyn Db, file: PythonFile<'_>) -> Option { ty_module_resolver::file_to_module(db, file).map(|module| module.name(db).to_string()) } @@ -196,7 +197,11 @@ mod tests { impl CursorTest { pub(super) fn prepare_calls(&self) -> Option> { - prepare_call_hierarchy(&self.db, self.cursor.file, self.cursor.offset) + prepare_call_hierarchy( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + ) } fn prepare_call_hierarchy(&self) -> String { diff --git a/crates/ty_ide/src/call_hierarchy/incoming_calls.rs b/crates/ty_ide/src/call_hierarchy/incoming_calls.rs index 68516e8d34..bc2bfa9f61 100644 --- a/crates/ty_ide/src/call_hierarchy/incoming_calls.rs +++ b/crates/ty_ide/src/call_hierarchy/incoming_calls.rs @@ -3,6 +3,7 @@ use crate::goto::{Definitions, GotoTarget, find_goto_target}; use crate::references::has_any_external_visible_definitions; use crate::{CallHierarchyItem, Db, SymbolKind}; use rayon::prelude::*; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_python_ast::helpers::is_dunder; @@ -28,8 +29,9 @@ const MAX_MIN_FILES_PER_PARALLEL_JOB: usize = 16; /// Find every place in the project that calls the symbol at `offset`, grouped /// by enclosing function/method/class/module. -pub fn incoming_calls(db: &dyn Db, file: File, offset: TextSize) -> Vec { +pub fn incoming_calls(db: &dyn Db, file: PythonFile<'_>, offset: TextSize) -> Vec { let module = parsed_module(db, file).load(db); + let source_file = file.file(db); let model = SemanticModel::new(db, file); let Some(goto_target) = find_goto_target(&model, &module, offset) else { return Vec::new(); @@ -74,10 +76,11 @@ pub fn incoming_calls(db: &dyn Db, file: File, offset: TextSize) -> Vec = files .iter() .copied() - .filter(|other| *other != file) + .filter(|other| *other != source_file) .collect(); let minimum_job_len = minimum_parallel_job_len(files.len(), MAX_MIN_FILES_PER_PARALLEL_JOB); // The byte-level text prefilter still pays off as a coarse gate: @@ -98,7 +101,13 @@ pub fn incoming_calls(db: &dyn Db, file: File, offset: TextSize) -> Vec>(); @@ -161,7 +170,7 @@ struct EnclosingKey { /// `target_definitions`. fn call_sites_for_file( db: &dyn Db, - file: File, + file: PythonFile<'_>, target_definitions: &Definitions<'_>, target_role: Option, needle: Option<&str>, @@ -303,6 +312,7 @@ impl<'a> CallSitesFinder<'a, '_> { /// accessor: a read calls the getter, a write calls the setter, and a /// `del` calls the deleter. fn check_property_access(&mut self, attribute: &'a ast::ExprAttribute) { + let db = self.db; let Some(Type::PropertyInstance(property)) = static_member_type_for_attribute(self.model, attribute) else { @@ -335,7 +345,7 @@ impl<'a> CallSitesFinder<'a, '_> { let intersects = current_definitions.iter().any(|resolved| { let role = resolved .definition() - .and_then(|def| property.accessor_role(self.db, def)); + .and_then(|def| property.accessor_role(db, def)); let matches_site_kind = match attribute.ctx { ast::ExprContext::Load => { matches!(role, Some(PropertyAccessorRole::Getter) | None) @@ -380,7 +390,8 @@ impl<'a> CallSitesFinder<'a, '_> { /// method's AST node. Comprehension and annotation scopes have no callable /// hierarchy item of their own, so walk outward until reaching one that does. fn enclosing_scope_item(&self, scope_node: AnyNodeRef<'_>) -> CallHierarchyItem { - let file = self.model.file(); + let python_file = self.model.python_file(); + let file = python_file.file(self.db); let mut ancestors = self.model.ancestor_scopes(scope_node); let Some((_, enclosing)) = ancestors.find(|(_, ancestor)| { matches!( @@ -388,11 +399,11 @@ impl<'a> CallSitesFinder<'a, '_> { ScopeKind::Module | ScopeKind::Function | ScopeKind::Class | ScopeKind::Lambda ) }) else { - return module_item(self.db, file); + return module_item(self.db, python_file); }; match enclosing.node() { - NodeWithScopeKind::Module => module_item(self.db, file), + NodeWithScopeKind::Module => module_item(self.db, python_file), NodeWithScopeKind::Function(func) => { let func = func.node(self.module); let is_method = ancestors @@ -411,7 +422,7 @@ impl<'a> CallSitesFinder<'a, '_> { } else { SymbolKind::Function }, - detail: module_detail(self.db, file), + detail: module_detail(self.db, python_file), file, full_range: func.range(), selection_range: func.name.range(), @@ -422,7 +433,7 @@ impl<'a> CallSitesFinder<'a, '_> { CallHierarchyItem { name: class.name.id.clone(), kind: SymbolKind::Class, - detail: module_detail(self.db, file), + detail: module_detail(self.db, python_file), file, full_range: class.range(), selection_range: class.name.range(), @@ -439,13 +450,13 @@ impl<'a> CallSitesFinder<'a, '_> { CallHierarchyItem { name: Name::new_static("(lambda)"), kind: SymbolKind::Function, - detail: module_detail(self.db, file), + detail: module_detail(self.db, python_file), file, full_range: lambda.range(), selection_range: TextRange::new(lambda.start(), end), } } - _ => module_item(self.db, file), + _ => module_item(self.db, python_file), } } } @@ -456,7 +467,7 @@ struct RawCallSite { } /// Build an item for the module-level enclosing scope (no enclosing function). -fn module_item(db: &dyn Db, file: File) -> CallHierarchyItem { +fn module_item(db: &dyn Db, file: PythonFile<'_>) -> CallHierarchyItem { let name = ty_module_resolver::file_to_module(db, file) .map(|module| Name::new(module.name(db).last_component())) .unwrap_or_else(|| Name::new_static("")); @@ -464,7 +475,7 @@ fn module_item(db: &dyn Db, file: File) -> CallHierarchyItem { name, kind: SymbolKind::Module, detail: None, - file, + file: file.file(db), full_range: TextRange::default(), selection_range: TextRange::default(), } @@ -514,7 +525,11 @@ mod tests { else { return "No incoming calls found".to_string(); }; - let calls = incoming_calls(&self.db, target.file, target.selection_range.start()); + let calls = incoming_calls( + &self.db, + self.python_file(target.file), + target.selection_range.start(), + ); if calls.is_empty() { return "No incoming calls found".to_string(); } @@ -1130,7 +1145,11 @@ def make() -> C: else { panic!("expected a call hierarchy target"); }; - let incoming = incoming_calls(&test.db, target.file, target.selection_range.start()); + let incoming = incoming_calls( + &test.db, + test.python_file(target.file), + target.selection_range.start(), + ); // The selection identifies the anonymous callable header. let sel = incoming[0].from.selection_range; let source = test.cursor.source.as_str(); @@ -1259,14 +1278,18 @@ def make() -> C: else { panic!("expected a call hierarchy target"); }; - let incoming = incoming_calls(&test.db, target.file, target.selection_range.start()); + let incoming = incoming_calls( + &test.db, + test.python_file(target.file), + target.selection_range.start(), + ); assert_eq!(incoming.len(), 1, "got {incoming:?}"); let lambda_item = &incoming[0].from; assert_eq!(lambda_item.name.as_str(), "(lambda)"); let follow_up_incoming = incoming_calls( &test.db, - lambda_item.file, + test.python_file(lambda_item.file), lambda_item.selection_range.start(), ); assert!( @@ -1276,7 +1299,7 @@ def make() -> C: let follow_up_outgoing = outgoing_calls( &test.db, - lambda_item.file, + test.python_file(lambda_item.file), lambda_item.selection_range.start(), ); assert!( diff --git a/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs b/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs index 47d8b8e4e7..535f68b7be 100644 --- a/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs +++ b/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs @@ -3,6 +3,7 @@ use std::collections::hash_map::Entry; use crate::call_hierarchy::CalleeLeaf; use crate::goto::find_goto_target; use crate::{CallHierarchyItem, Db}; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::token::Tokens; @@ -29,7 +30,7 @@ use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// are reported when the nested callable is expanded separately. Declaration /// expressions attached to a nested callable are still included while /// traversing the containing item's body. -pub fn outgoing_calls(db: &dyn Db, file: File, offset: TextSize) -> Vec { +pub fn outgoing_calls(db: &dyn Db, file: PythonFile<'_>, offset: TextSize) -> Vec { let module = parsed_module(db, file).load(db); let model = SemanticModel::new(db, file); let Some(goto_target) = find_goto_target(&model, &module, offset) else { @@ -51,10 +52,9 @@ pub fn outgoing_calls(db: &dyn Db, file: File, offset: TextSize) -> Vec OutgoingCallsFinder<'a, '_> { _ => continue, } let def_file = def.file(self.db); - let module_ref = parsed_module(self.db, def_file).load(self.db); + let module_ref = parsed_module(self.db, def.python_file(self.db)).load(self.db); let selection_range = def.focus_range(self.db, &module_ref).range(); let key = CalleeKey { @@ -304,7 +304,11 @@ mod tests { else { return "No outgoing calls found".to_string(); }; - let calls = outgoing_calls(&self.db, target.file, target.selection_range.start()); + let calls = outgoing_calls( + &self.db, + self.python_file(target.file), + target.selection_range.start(), + ); if calls.is_empty() { return "No outgoing calls found".to_string(); } diff --git a/crates/ty_ide/src/code_action.rs b/crates/ty_ide/src/code_action.rs index 44f3afcc8a..099e26ff26 100644 --- a/crates/ty_ide/src/code_action.rs +++ b/crates/ty_ide/src/code_action.rs @@ -1,6 +1,8 @@ use crate::completion; -use ruff_db::{files::File, parsed::parsed_module}; +use ruff_db::parsed::parsed_module; + +use ruff_db::PythonFile; use ruff_diagnostics::Edit; use ruff_python_ast::find_node::covering_node; use ruff_text_size::TextRange; @@ -19,7 +21,7 @@ pub struct QuickFix { pub fn code_actions( db: &dyn Db, - file: File, + file: PythonFile<'_>, diagnostic_range: TextRange, diagnostic_id: &str, ) -> Vec { @@ -51,13 +53,12 @@ pub fn code_actions( fn unresolved_fixes( db: &dyn Db, - file: File, + file: PythonFile<'_>, diagnostic_range: TextRange, ) -> Option> { let parsed = parsed_module(db, file).load(db); let node = covering_node(parsed.syntax().into(), diagnostic_range).node(); let symbol = &node.expr_name()?.id; - Some( completion::unresolved_fixes(db, file, &parsed, symbol, node) .into_iter() @@ -76,6 +77,7 @@ mod tests { use insta::assert_snapshot; use ruff_db::{ + PythonFile, diagnostic::{ Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName, Span, SubDiagnostic, @@ -932,7 +934,12 @@ mod tests { .context(0) .format(DiagnosticFormat::Full); - for mut action in code_actions(&self.db, self.file, self.diagnostic_range, &lint.name) { + for mut action in code_actions( + &self.db, + PythonFile::new(&self.db, self.file, self.db.python_version()), + self.diagnostic_range, + &lint.name, + ) { let mut diagnostic = Diagnostic::new( DiagnosticId::Lint(LintName::of("code-action")), ruff_db::diagnostic::Severity::Info, diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 90f6d61760..7b0c4cf0b6 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -1,8 +1,9 @@ use std::cmp::Ordering; use std::collections::{BinaryHeap, binary_heap}; +use ty_python_semantic::ProgramEnvironment; use compact_str::{CompactString, CompactStringExt}; -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_db::source::{SourceText, source_text}; use ruff_diagnostics::Edit; @@ -33,24 +34,30 @@ pub fn completion<'db>( db: &'db dyn Db, settings: &CompletionSettings, capabilities: CompletionCapabilities, - file: File, + file: PythonFile<'db>, offset: TextSize, ) -> Vec> { + let python_file = file; let parsed = parsed_module(db, file).load(db); + let file = file.file(db); let source = source_text(db, file); - let Some(context) = Context::new(db, file, &parsed, &source, offset) else { + let Some(context) = Context::new(db, python_file, &parsed, &source, offset) else { return vec![]; }; - let model = SemanticModel::new(db, file); + let model = SemanticModel::new(db, python_file); if !matches!(context.kind, ContextKind::Keywords(_)) && context.cursor.is_in_string() { let Some(string_expr) = context.cursor.enclosing_string_literal_expr() else { return vec![]; }; - let mut completions = - Completions::new(db, CollectionContext::none(), UserQuery::fuzzy(None)); + let mut completions = Completions::new( + db, + python_file, + CollectionContext::none(), + UserQuery::fuzzy(None), + ); add_string_literal_completions( &model, @@ -65,6 +72,7 @@ pub fn completion<'db>( let query = UserQuery::fuzzy(context.cursor.typed); let mut completions = Completions::new( db, + python_file, context.collection_context(db, &model, settings, capabilities), query, ); @@ -76,13 +84,14 @@ pub fn completion<'db>( } } ContextKind::Import(ref import) => { - import.add_completions(db, file, &mut completions); + import.add_completions(db, python_file, &mut completions); } ContextKind::NonImport(ref non_import) => match non_import.target { CompletionTargetAst::ObjectDot { expr } => { completions.extend(model.attribute_completions(expr)); } CompletionTargetAst::Scoped(scoped) => { + let env = model.program_environment(); for semantic_completion in model.scoped_completions(scoped.node) { let module_dependency_kind = if semantic_completion.builtin { ModuleDependencyKind::Builtin @@ -90,16 +99,22 @@ pub fn completion<'db>( ModuleDependencyKind::Current }; completions.add( - CompletionBuilder::from_semantic_completion(db, semantic_completion) + CompletionBuilder::from_semantic_completion(db, &env, semantic_completion) .module_dependency_kind(module_dependency_kind), ); } - add_keyword_completions(db, &mut completions); - add_argument_completions(db, &model, &context.cursor, &mut completions); + add_keyword_completions(db, &env, &mut completions); + add_argument_completions( + db, + python_file, + &model, + &context.cursor, + &mut completions, + ); if settings.auto_import { add_unimported_completions( db, - file, + python_file, &parsed, scoped, |module_name: &ModuleName, symbol: &str| { @@ -131,6 +146,7 @@ impl CompletionCapabilities { /// A collection of completions built up from various sources. struct Completions<'db> { db: &'db dyn Db, + python_file: PythonFile<'db>, context: CollectionContext<'db>, items: BinaryHeap>, /// The query used to match against candidate completions. @@ -154,9 +170,15 @@ impl<'db> Completions<'db> { /// the user has typed as part of the next symbol they are writing. /// This collection will treat it as a query when present, and only /// add completions that match it. - fn new(db: &'db dyn Db, context: CollectionContext<'db>, query: UserQuery) -> Completions<'db> { + fn new( + db: &'db dyn Db, + python_file: PythonFile<'db>, + context: CollectionContext<'db>, + query: UserQuery, + ) -> Completions<'db> { Completions { db, + python_file, context, items: BinaryHeap::new(), query, @@ -226,9 +248,14 @@ impl<'db> Completions<'db> { /// Attempts to add the given semantic completion to this collection. /// /// When added, `true` is returned. - fn add_semantic(&mut self, completion: SemanticCompletion<'db>) -> bool { + fn add_semantic( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + completion: SemanticCompletion<'db>, + ) -> bool { self.add(CompletionBuilder::from_semantic_completion( - self.db, completion, + db, env, completion, )) } @@ -246,7 +273,8 @@ impl<'db> Completions<'db> { if self.context.exclude(self.db, &builder) { return false; } - let completion = CompletionRanker(builder.build(self.db, &self.context, &self.query)); + let completion = + CompletionRanker(builder.build(self.db, self.python_file, &self.context, &self.query)); if self.items.len() >= Completions::LIMIT { // OK because `self.items` is guaranteed to be non-empty here. let worst = self.items.peek_mut().unwrap(); @@ -265,8 +293,10 @@ impl<'db> Extend> for Completions<'db> { where T: IntoIterator>, { + let db = self.db; + let env = ProgramEnvironment::from_file(self.python_file); for c in it { - self.add_semantic(c); + self.add_semantic(db, &env, c); } } } @@ -415,9 +445,10 @@ impl<'db> CompletionBuilder<'db> { fn from_semantic_completion( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, semantic: SemanticCompletion<'db>, ) -> CompletionBuilder<'db> { - let definition = semantic.ty.and_then(|ty| Definitions::from_ty(db, ty)); + let definition = semantic.ty.and_then(|ty| Definitions::from_ty(db, env, ty)); let documentation = definition.and_then(|def| def.docstring(db)); Completion::builder(semantic.name) .ty(semantic.ty) @@ -447,7 +478,7 @@ impl<'db> CompletionBuilder<'db> { /// Use this builder to construct a `Completion`. /// - /// `ctx` is any information about the position of the + /// `env` is any information about the position of the /// cursor in the source code that could impact the relevance /// ranking of the completion. /// @@ -456,7 +487,8 @@ impl<'db> CompletionBuilder<'db> { fn build( mut self, db: &'db dyn Db, - ctx: &CollectionContext<'db>, + python_file: PythonFile<'db>, + collection_context: &CollectionContext<'db>, query: &UserQuery, ) -> Completion<'db> { if let Some(ty) = self.ty { @@ -468,10 +500,11 @@ impl<'db> CompletionBuilder<'db> { // It's possible that some completions are usable in an exception // but aren't marked here. That is, false negatives are // possible but false positives are not. - if let Some(exception_ty) = ctx.exception_ty { - self.is_context_specific |= ty.is_assignable_to(db, exception_ty); + if let Some(exception_ty) = collection_context.exception_ty { + let env = ProgramEnvironment::from_file(python_file); + self.is_context_specific |= ty.is_assignable_to(db, &env, exception_ty); } - if ctx.is_in_class_def() { + if collection_context.is_in_class_def() { self.is_context_specific |= ty.is_class_literal() || matches!( ty, @@ -489,11 +522,11 @@ impl<'db> CompletionBuilder<'db> { let kind = self .kind .or_else(|| self.ty.and_then(|ty| completion_kind_from_type(db, ty))); - let relevance = Relevance::new(ctx, query, &self); + let relevance = Relevance::new(collection_context, query, &self); let (label, insert, insert_text_format, command) = - if ctx.should_complete_callable_parentheses(kind) { + if collection_context.should_complete_callable_parentheses(kind) { let label = self.insert.unwrap_or_else(|| self.name.clone()); - if ctx.capabilities.snippets { + if collection_context.capabilities.snippets { let insert = compact_str::format_compact!("{label}($0)"); ( Some(label), @@ -755,7 +788,7 @@ impl<'m> Context<'m> { /// Create a new context for finding completions. fn new( db: &'_ dyn Db, - file: File, + file: PythonFile<'_>, parsed: &'m ParsedModuleRef, source: &'m SourceText, offset: TextSize, @@ -791,7 +824,8 @@ impl<'m> Context<'m> { match self.kind { ContextKind::Keywords(_) | ContextKind::Import(_) => CollectionContext::none(), ContextKind::NonImport(_) => { - let exception_ty = self.cursor.exception_ty(db); + let env = model.program_environment(); + let exception_ty = self.cursor.exception_ty(db, &env); let complete_callable_parentheses = settings.complete_function_parentheses && !self.cursor.suppress_callable_parentheses(); let existing_class_bases = self.cursor.enclosing_class_def().map(|class_def| { @@ -1326,16 +1360,22 @@ impl<'m> ContextCursor<'m> { /// /// The return value is always `None` if the cursor is not /// inside a `raise` or `except` context. - fn exception_ty<'db>(&self, db: &'db dyn Db) -> Option> { - let base_exception_ty = KnownClass::BaseException.to_subclass_of(db); - let base_exception_instance = KnownClass::BaseException.to_instance(db); - let raise_ty = UnionType::from_elements(db, [base_exception_ty, base_exception_instance]); - let cause_ty = UnionType::from_elements(db, [raise_ty, Type::none(db)]); + fn exception_ty<'db>( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + let base_exception_ty = KnownClass::BaseException.to_subclass_of(db, env); + let base_exception_instance = KnownClass::BaseException.to_instance(db, env); + let raise_ty = + UnionType::from_elements(db, env, [base_exception_ty, base_exception_instance]); + let cause_ty = UnionType::from_elements(db, env, [raise_ty, Type::none(db, env)]); let except_ty = UnionType::from_elements( db, + env, [ base_exception_ty, - Type::homogeneous_tuple(db, base_exception_ty), + Type::homogeneous_tuple(db, env, base_exception_ty), ], ); @@ -1924,6 +1964,7 @@ enum Sort { /// Detect and add completions for unset arguments. fn add_argument_completions<'db>( db: &'db dyn Db, + file: PythonFile<'db>, model: &SemanticModel<'db>, cursor: &ContextCursor<'_>, completions: &mut Completions<'db>, @@ -1943,7 +1984,7 @@ fn add_argument_completions<'db>( } ast::AnyNodeRef::ExprCall(_) => { if in_arguments { - add_function_arg_completions(db, model.file(), cursor, completions); + add_function_arg_completions(db, file, cursor, completions); } return; } @@ -1975,29 +2016,31 @@ fn add_class_arg_completions<'db>( class_def: &ast::StmtClassDef, completions: &mut Completions<'db>, ) { + let db = model.db(); let is_set = |name| { class_def .arguments .as_ref() .is_some_and(|args| args.find_keyword(name).is_some()) }; + let env = model.program_environment(); if !is_set("metaclass") { - let ty = KnownClass::Type.to_subclass_of(model.db()); + let ty = KnownClass::Type.to_subclass_of(db, &env); completions.add(CompletionBuilder::argument("metaclass").ty(ty)); } let is_typed_dict = class_def .inferred_type(model) .and_then(Type::as_class_literal) - .is_some_and(|t| t.is_typed_dict(model.db())); + .is_some_and(|t| t.is_typed_dict(db)); // TODO: Handle PEP 728 that adds two extra keywords, // closed and extra_items. // // See https://peps.python.org/pep-0728/ if is_typed_dict && !is_set("total") { - let ty = KnownClass::Bool.to_instance(model.db()); + let ty = KnownClass::Bool.to_instance(db, &env); completions.add(CompletionBuilder::argument("total").ty(ty)); } } @@ -2009,7 +2052,7 @@ fn add_class_arg_completions<'db>( /// set and 2) been defined as positional-only. fn add_function_arg_completions<'db>( db: &'db dyn Db, - file: File, + file: PythonFile<'db>, cursor: &ContextCursor<'_>, completions: &mut Completions<'db>, ) { @@ -2088,9 +2131,9 @@ pub(crate) struct ImportEdit { } /// Get fixes that would resolve an unresolved reference -pub(crate) fn unresolved_fixes( - db: &dyn Db, - file: File, +pub(crate) fn unresolved_fixes<'db>( + db: &'db dyn Db, + file: PythonFile<'db>, parsed: &ParsedModuleRef, symbol: &str, node: AnyNodeRef, @@ -2101,7 +2144,7 @@ pub(crate) fn unresolved_fixes( let ctx = CollectionContext::none(); // Request imports we could add to put the symbol in scope - let mut completions = Completions::new(db, ctx.clone(), query.clone()); + let mut completions = Completions::new(db, file, ctx.clone(), query.clone()); add_unimported_completions( db, file, @@ -2115,7 +2158,7 @@ pub(crate) fn unresolved_fixes( results.extend(completions.into_imports()); // Request qualifications we could apply to the symbol to make it resolve - let mut completions = Completions::new(db, ctx, query); + let mut completions = Completions::new(db, file, ctx, query); add_unimported_completions( db, file, @@ -2136,9 +2179,13 @@ pub(crate) fn unresolved_fixes( /// This should generally only be used when offering "scoped" completions. /// This will include keywords corresponding to Python values (like `None`) /// and general language keywords (like `raise`). -fn add_keyword_completions<'db>(db: &'db dyn Db, completions: &mut Completions<'db>) { +fn add_keyword_completions<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + completions: &mut Completions<'db>, +) { let keyword_values = [ - ("None", Type::none(db)), + ("None", Type::none(db, env)), ("True", Type::bool_literal(true)), ("False", Type::bool_literal(false)), ]; @@ -2229,7 +2276,7 @@ fn add_string_literal_completions<'db>( /// when selected into `File`. fn add_unimported_completions<'db>( db: &'db dyn Db, - file: File, + file: PythonFile<'db>, parsed: &ParsedModuleRef, scoped: ScopedTarget<'_>, create_import_request: impl for<'a> Fn(&'a ModuleName, &'a str) -> ImportRequest<'a>, @@ -2243,13 +2290,14 @@ fn add_unimported_completions<'db>( return; } - let source = source_text(db, file); + let source_file = file.file(db); + let source = source_text(db, source_file); let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str()); let importer = Importer::new(db, &stylist, file, source.as_str(), parsed); let members = importer.members_in_scope_at(scoped.node, scoped.node.start()); for symbol in all_symbols(db, file, &completions.query.pattern) { - if symbol.file() == file || symbol.module().is_known(db, KnownModule::Builtins) { + if symbol.file() == source_file || symbol.module().is_known(db, KnownModule::Builtins) { continue; } @@ -2528,7 +2576,7 @@ impl<'a> ImportStatement<'a> { /// `tokens`. fn detect( db: &'_ dyn Db, - file: File, + file: PythonFile<'_>, cursor: &ContextCursor<'a>, ) -> Option> { use TokenKind as TK; @@ -2926,7 +2974,7 @@ impl<'a> ImportStatement<'a> { fn add_completions<'db>( &self, db: &'db dyn Db, - file: File, + file: PythonFile<'db>, completions: &mut Completions<'db>, ) { let model = SemanticModel::new(db, file); @@ -3023,9 +3071,10 @@ fn add_import_completions_impl<'db>( semantic_completions: impl IntoIterator>, module_dependency_kind: impl Fn(&SemanticCompletion<'db>) -> Option, ) { + let env = ProgramEnvironment::from_file(completions.python_file); for semantic in semantic_completions { let module_dependency_kind = module_dependency_kind(&semantic); - let mut builder = CompletionBuilder::from_semantic_completion(db, semantic); + let mut builder = CompletionBuilder::from_semantic_completion(db, &env, semantic); if let Some(module_dependency_kind) = module_dependency_kind { builder = builder.module_dependency_kind(module_dependency_kind); } @@ -10797,7 +10846,7 @@ raise &self.cursor_test.db, &self.settings, self.capabilities, - self.cursor_test.cursor.file, + self.cursor_test.python_file(self.cursor_test.cursor.file), self.cursor_test.cursor.offset, ); let filtered = original @@ -10953,6 +11002,7 @@ raise impl<'db> CompletionTest<'db> { fn snapshot(&self) -> String { + let db = self.db; if self.original.is_empty() { return "".to_string(); } else if self.filtered.is_empty() { @@ -10964,13 +11014,14 @@ raise // ---AG return "".to_string(); } + let env = self.db.program_environment(); self.filtered .iter() .map(|c| { let mut snapshot = c.insert.as_deref().unwrap_or(c.label()).to_string(); if self.type_signatures { let ty = - c.ty.map(|ty| ty.display(self.db).to_string()) + c.ty.map(|ty| ty.display(db, &env).to_string()) .unwrap_or_else(|| "Unavailable".to_string()); snapshot = format!("{snapshot} :: {ty}"); } diff --git a/crates/ty_ide/src/doc_highlights.rs b/crates/ty_ide/src/doc_highlights.rs index 31ca4400ce..e4fe854908 100644 --- a/crates/ty_ide/src/doc_highlights.rs +++ b/crates/ty_ide/src/doc_highlights.rs @@ -1,7 +1,7 @@ use crate::goto::find_goto_target; use crate::references::{ReferencesMode, references}; use crate::{Db, ReferenceTarget}; -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_text_size::TextSize; use ty_python_semantic::SemanticModel; @@ -9,7 +9,7 @@ use ty_python_semantic::SemanticModel; /// Document highlights are limited to the current file only. pub fn document_highlights( db: &dyn Db, - file: File, + file: PythonFile<'_>, offset: TextSize, ) -> Option> { let parsed = ruff_db::parsed::parsed_module(db, file); @@ -34,9 +34,11 @@ mod tests { impl CursorTest { fn document_highlights(&self) -> String { - let Some(highlight_results) = - document_highlights(&self.db, self.cursor.file, self.cursor.offset) - else { + let Some(highlight_results) = document_highlights( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + ) else { return "No highlights found".to_string(); }; diff --git a/crates/ty_ide/src/document_symbols.rs b/crates/ty_ide/src/document_symbols.rs index ed831d2c82..fb72ca94dc 100644 --- a/crates/ty_ide/src/document_symbols.rs +++ b/crates/ty_ide/src/document_symbols.rs @@ -1,9 +1,9 @@ use crate::symbols::{FlatSymbols, symbols_for_file}; -use ruff_db::files::File; +use ruff_db::PythonFile; use ty_project::Db; /// Get all document symbols for a file with the given options. -pub fn document_symbols(db: &dyn Db, file: File) -> &FlatSymbols { +pub fn document_symbols<'db>(db: &'db dyn Db, file: PythonFile<'db>) -> &'db FlatSymbols { symbols_for_file(db, file) } @@ -17,6 +17,7 @@ mod tests { Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic, SubDiagnosticSeverity, }; + use ruff_db::files::File; #[test] fn test_document_symbols_simple() { @@ -404,7 +405,7 @@ def function(): ", ); - let symbols = document_symbols(&test.db, test.cursor.file) + let symbols = document_symbols(&test.db, test.python_file(test.cursor.file)) .iter() .map(|(_, symbol)| (symbol.name.into_owned(), symbol.kind)) .collect::>(); @@ -441,7 +442,7 @@ lambda_value = lambda: (lambda_local := 1) ", ); - let names = document_symbols(&test.db, test.cursor.file) + let names = document_symbols(&test.db, test.python_file(test.cursor.file)) .iter() .map(|(_, symbol)| symbol.name.into_owned()) .collect::>(); @@ -463,7 +464,7 @@ class Example((class_base := Base)): ", ); - let symbols = document_symbols(&test.db, test.cursor.file) + let symbols = document_symbols(&test.db, test.python_file(test.cursor.file)) .iter() .map(|(_, symbol)| (symbol.name.into_owned(), symbol.kind)) .collect::>(); @@ -485,7 +486,8 @@ class Example((class_base := Base)): impl CursorTest { fn document_symbols(&self) -> String { - let symbols = document_symbols(&self.db, self.cursor.file).to_hierarchical(); + let symbols = + document_symbols(&self.db, self.python_file(self.cursor.file)).to_hierarchical(); if symbols.is_empty() { return "No symbols found".to_string(); diff --git a/crates/ty_ide/src/find_references.rs b/crates/ty_ide/src/find_references.rs index 0b331e17cb..d664bc0513 100644 --- a/crates/ty_ide/src/find_references.rs +++ b/crates/ty_ide/src/find_references.rs @@ -1,7 +1,7 @@ use crate::goto::find_goto_target; use crate::references::{ReferencesMode, references}; use crate::{Db, ReferenceTarget}; -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_text_size::TextSize; use ty_python_semantic::SemanticModel; @@ -9,7 +9,7 @@ use ty_python_semantic::SemanticModel; /// Search for references across all files in the project. pub fn find_references( db: &dyn Db, - file: File, + file: PythonFile<'_>, offset: TextSize, include_declaration: bool, ) -> Option> { @@ -48,7 +48,7 @@ mod tests { fn references_with_include_declaration(&self, include_declaration: bool) -> String { let Some(mut reference_results) = find_references( &self.db, - self.cursor.file, + self.python_file(self.cursor.file), self.cursor.offset, include_declaration, ) else { diff --git a/crates/ty_ide/src/folding_range.rs b/crates/ty_ide/src/folding_range.rs index 6a6cefa8b7..7d84c2a492 100644 --- a/crates/ty_ide/src/folding_range.rs +++ b/crates/ty_ide/src/folding_range.rs @@ -1,4 +1,4 @@ -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_python_ast::token::{TokenKind, Tokens, parenthesized_range}; @@ -56,10 +56,11 @@ impl From for FoldingRange { /// Returns a list of folding ranges for the given file. pub fn folding_ranges( db: &dyn Db, - file: File, + file: PythonFile<'_>, range_filter: Option, ) -> Vec { let parsed = parsed_module(db, file).load(db); + let file = file.file(db); let source = source_text(db, file); let mut visitor = FoldingRangeVisitor { @@ -762,6 +763,7 @@ mod tests { use crate::tests::CursorTest; use insta::assert_snapshot; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span}; + use ruff_db::files::File; #[test] fn test_folding_range_class() { @@ -2577,7 +2579,7 @@ with open("file.txt") as f: impl CursorTest { fn folding_ranges(&self) -> String { - let ranges = folding_ranges(&self.db, self.cursor.file, None); + let ranges = folding_ranges(&self.db, self.python_file(self.cursor.file), None); if ranges.is_empty() { return "No folding ranges found".to_string(); diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 32a48f7eaa..7b22e17d6d 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -2,6 +2,7 @@ use crate::docstring::Docstring; pub use crate::goto_declaration::goto_declaration; pub use crate::goto_definition::goto_definition; pub use crate::goto_type_definition::goto_type_definition; +use ty_python_semantic::Db; use std::borrow::Cow; @@ -14,16 +15,16 @@ use ruff_python_ast::{self as ast, AnyNodeRef, ExprRef}; use ruff_text_size::{Ranged, TextRange, TextSize}; use ty_python_core::definition::{Definition, DefinitionKind}; -use ty_python_semantic::ResolvedDefinition; use ty_python_semantic::types::Type; use ty_python_semantic::types::ide_support::{ call_signature_details, call_type_simplified_by_overloads, constructor_signature, definitions_and_overloads_for_function, definitions_for_keyword_argument, typed_dict_key_definition, }; +use ty_python_semantic::{Db as SemanticDb, ResolvedDefinition}; use ty_python_semantic::{ - HasDefinition, HasType, ImportAliasResolution, SemanticModel, TypeQualifiers, - definitions_for_imported_symbol, definitions_for_name, + HasDefinition, HasType, ImportAliasResolution, ProgramEnvironment, SemanticModel, + TypeQualifiers, definitions_for_imported_symbol, definitions_for_name, }; #[derive(Clone, Debug)] @@ -256,11 +257,15 @@ impl<'db> Definitions<'db> { Self(resolved) } - pub(crate) fn from_ty(db: &'db dyn crate::Db, ty: Type<'db>) -> Option { - let ty_def = ty.definition(db)?; + pub(crate) fn from_ty( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option { + let ty_def = ty.definition(db, env)?; let resolved = match ty_def { ty_python_semantic::types::TypeDefinition::Module(module) => { - ResolvedDefinition::Module(module.file(db)?) + ResolvedDefinition::Module(module.python_file(db)?) } ty_python_semantic::types::TypeDefinition::StaticClass(definition) | ty_python_semantic::types::TypeDefinition::DynamicClass(definition) @@ -387,8 +392,8 @@ impl<'db> Definitions<'db> { .into_iter() .map(|definition| match definition { ResolvedDefinition::Definition(definition) => { - let file = definition.file(db); - let module = ruff_db::parsed::parsed_module(db, file).load(db); + let module = + ruff_db::parsed::parsed_module(db, definition.python_file(db)).load(db); let focus_range = definition.focus_range(db, &module); let full_range = definition.full_range(db, &module); @@ -400,7 +405,7 @@ impl<'db> Definitions<'db> { } } ResolvedDefinition::Module(file) => { - NavigationTarget::new(file, TextRange::default()) + NavigationTarget::new(file.file(db), TextRange::default()) } ResolvedDefinition::FileWithRange(file_range) => NavigationTarget::from(file_range), }) @@ -412,7 +417,7 @@ impl<'db> Definitions<'db> { /// Typically documentation only appears on implementations and not stubs, /// so this will check both the goto-declarations and goto-definitions (in that order) /// and return the first one found. - pub(crate) fn docstring(self, db: &'db dyn crate::Db) -> Option { + pub(crate) fn docstring(self, db: &'db dyn SemanticDb) -> Option { for definition in &self { // If we got a docstring from the original definition, use it if let Some(docstring) = definition.docstring(db) { @@ -470,7 +475,7 @@ impl<'a, 'db> IntoIterator for &'a Definitions<'db> { /// Shared by hover and signature help so both surfaces render the same /// docstring for a given call site. pub(crate) fn docstring_for_call_definition<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, definition: Definition<'db>, ) -> Option { let resolved = ResolvedDefinition::Definition(definition); @@ -1446,7 +1451,7 @@ fn definitions_for_module<'db>( level: u32, ) -> Option>> { let module = model.resolve_module(module, level)?; - let file = module.file(model.db())?; + let file = module.python_file(model.db())?; Some(vec![ResolvedDefinition::Module(file)]) } diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index 21b591f97e..3961dde00d 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -1,6 +1,7 @@ use crate::goto::find_goto_target; use crate::{Db, NavigationTargets, RangedValue}; -use ruff_db::files::{File, FileRange}; +use ruff_db::PythonFile; +use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; use ty_python_semantic::{ImportAliasResolution, SemanticModel}; @@ -12,7 +13,7 @@ use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// is needed because Python doesn't require formal declarations of variables like most languages do. pub fn goto_declaration( db: &dyn Db, - file: File, + file: PythonFile<'_>, offset: TextSize, ) -> Option> { let module = parsed_module(db, file).load(db); @@ -25,7 +26,7 @@ pub fn goto_declaration( .into_navigation_targets(model.db()); Some(RangedValue { - range: FileRange::new(file, goto_target.range()), + range: FileRange::new(file.file(db), goto_target.range()), value: declaration_targets, }) } @@ -2765,7 +2766,11 @@ def ab(a: int, *, c: int): ... impl CursorTest { fn goto_declaration(&self) -> String { let Some(targets) = salsa::attach(&self.db, || { - goto_declaration(&self.db, self.cursor.file, self.cursor.offset) + goto_declaration( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + ) }) else { return "No goto target found".to_string(); }; diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index c4fdbf790a..a6e6d83de9 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -1,6 +1,7 @@ use crate::goto::find_goto_target; use crate::{Db, NavigationTargets, RangedValue}; -use ruff_db::files::{File, FileRange}; +use ruff_db::PythonFile; +use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; use ty_python_semantic::{ImportAliasResolution, SemanticModel}; @@ -13,7 +14,7 @@ use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// source file implementations using the `StubMapper`. pub fn goto_definition( db: &dyn Db, - file: File, + file: PythonFile<'_>, offset: TextSize, ) -> Option> { let module = parsed_module(db, file).load(db); @@ -25,7 +26,7 @@ pub fn goto_definition( .into_navigation_targets(model.db()); Some(RangedValue { - range: FileRange::new(file, goto_target.range()), + range: FileRange::new(file.file(db), goto_target.range()), value: definition_targets, }) } @@ -2648,7 +2649,11 @@ class GenericFoo[T](Base): impl CursorTest { fn goto_definition(&self) -> String { let Some(targets) = salsa::attach(&self.db, || { - goto_definition(&self.db, self.cursor.file, self.cursor.offset) + goto_definition( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + ) }) else { return "No goto target found".to_string(); }; diff --git a/crates/ty_ide/src/goto_implementation.rs b/crates/ty_ide/src/goto_implementation.rs index 175286ae00..54b055ce5c 100644 --- a/crates/ty_ide/src/goto_implementation.rs +++ b/crates/ty_ide/src/goto_implementation.rs @@ -58,6 +58,7 @@ use crate::goto::{Definitions, GotoTarget, find_goto_target}; use crate::{Db, NavigationTarget, NavigationTargets, RangedValue}; use rayon::prelude::*; +use ruff_db::PythonFile; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; @@ -72,26 +73,29 @@ use ty_python_semantic::{ /// identified. pub fn goto_implementation( db: &dyn Db, - file: File, + file: PythonFile<'_>, offset: TextSize, ) -> Option> { let module = parsed_module(db, file).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; let finder = prepare_implementations_finder_for_goto_target(&model, &goto_target)?; + let source_file = file.file(db); + let python_version = file.python_version(db); let mut candidate_files: Vec = db .project() .files(db) .iter() .copied() - .filter(|candidate| *candidate != file) + .filter(|candidate| *candidate != source_file) .collect(); - candidate_files.push(file); + candidate_files.push(source_file); let batches = candidate_files .into_par_iter() .map_with_db(db, |db, file| { + let file = PythonFile::new(db, file, python_version); let definitions = finder.implementations_for_file(db, file); definitions_to_implementation_targets(db, definitions) }) @@ -108,7 +112,7 @@ pub fn goto_implementation( let implementation_targets = implementation_targets.into_iter().collect(); Some(RangedValue { - range: FileRange::new(file, goto_target.range()), + range: FileRange::new(source_file, goto_target.range()), value: implementation_targets, }) } @@ -118,6 +122,8 @@ fn prepare_implementations_finder_for_goto_target<'db>( model: &SemanticModel<'db>, goto_target: &GotoTarget<'_>, ) -> Option> { + let db = model.db(); + let env = model.program_environment(); match goto_target { GotoTarget::Expression(expression) | GotoTarget::Call { @@ -132,7 +138,8 @@ fn prepare_implementations_finder_for_goto_target<'db>( .expression_definitions(model, ImportAliasResolution::ResolveAliases) .and_then(|definitions| { ImplementationsFinder::for_class_reference( - model.db(), + db, + &env, definitions.iter().as_slice(), ) }) @@ -146,10 +153,7 @@ fn prepare_implementations_finder_for_goto_target<'db>( GotoTarget::StringAnnotationSubexpr { .. } => goto_target .definitions(model, ImportAliasResolution::ResolveAliases) .and_then(|definitions| { - ImplementationsFinder::for_class_reference( - model.db(), - definitions.iter().as_slice(), - ) + ImplementationsFinder::for_class_reference(db, &env, definitions.iter().as_slice()) }), GotoTarget::FunctionDef(function) => ImplementationsFinder::for_method(model, function), GotoTarget::ClassDef(class) => ImplementationsFinder::for_class(model, class), @@ -832,8 +836,12 @@ mod tests { .build(); let targets = salsa::attach(&test.db, || { - goto_implementation(&test.db, test.cursor.file, test.cursor.offset) - .expect("implementation targets") + goto_implementation( + &test.db, + test.python_file(test.cursor.file), + test.cursor.offset, + ) + .expect("implementation targets") }); let paths = targets .into_iter() @@ -2134,7 +2142,11 @@ class MyClass: impl CursorTest { fn goto_implementation(&self) -> String { let Some(targets) = salsa::attach(&self.db, || { - goto_implementation(&self.db, self.cursor.file, self.cursor.offset) + goto_implementation( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + ) }) else { return "No goto target found".to_string(); }; diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index 080b43f01e..d888e49cd3 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -1,13 +1,14 @@ use crate::goto::find_goto_target; use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue}; -use ruff_db::files::{File, FileRange}; +use ruff_db::PythonFile; +use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; use ty_python_semantic::SemanticModel; pub fn goto_type_definition( db: &dyn Db, - file: File, + file: PythonFile<'_>, offset: TextSize, ) -> Option> { let module = parsed_module(db, file).load(db); @@ -15,13 +16,14 @@ pub fn goto_type_definition( let goto_target = find_goto_target(&model, &module, offset)?; let ty = goto_target.inferred_type(&model)?; + let env = model.program_environment(); - tracing::debug!("Inferred type of covering node is {}", ty.display(db)); + tracing::debug!("Inferred type of covering node is {}", ty.display(db, &env)); - let navigation_targets = ty.navigation_targets(db); + let navigation_targets = ty.navigation_targets(db, &env); Some(RangedValue { - range: FileRange::new(file, goto_target.range()), + range: FileRange::new(file.file(db), goto_target.range()), value: navigation_targets, }) } @@ -2051,7 +2053,11 @@ def function(): impl CursorTest { fn goto_type_definition(&self) -> String { let Some(targets) = salsa::attach(&self.db, || { - goto_type_definition(&self.db, self.cursor.file, self.cursor.offset) + goto_type_definition( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + ) }) else { return "No goto target found".to_string(); }; diff --git a/crates/ty_ide/src/hints.rs b/crates/ty_ide/src/hints.rs index 9b996e5cc4..21c65ec179 100644 --- a/crates/ty_ide/src/hints.rs +++ b/crates/ty_ide/src/hints.rs @@ -1,4 +1,4 @@ -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_python_ast::name::Name; use ruff_text_size::TextRange; use ty_python_semantic::types::ide_support::{ @@ -40,8 +40,9 @@ impl HintKind { } } -pub fn hints(db: &dyn Db, file: File) -> Vec { - if !db.should_check_file(file) { +pub fn hints(db: &dyn Db, file: PythonFile<'_>) -> Vec { + let source_file = file.file(db); + if !db.should_check_file(source_file) { return Vec::new(); } diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 689abf81a7..a7d9920e33 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -1,18 +1,24 @@ use crate::docstring::{Docstring, DocstringFragment}; use crate::goto::{Definitions, GotoTarget, docstring_for_call_definition, find_goto_target}; use crate::{Db, MarkupKind, RangedValue}; -use ruff_db::files::{File, FileRange}; +use ruff_db::PythonFile; +use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextSize}; use std::fmt; use std::fmt::Formatter; +use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::types::ide_support::{resolved_call_signature, typed_dict_key_hover}; use ty_python_semantic::types::{KnownInstanceType, Type, TypeAliasType, TypeVarVariance}; use ty_python_semantic::{DisplaySettings, SemanticModel, TypeQualifiers}; -pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option>> { +pub fn hover<'db>( + db: &'db dyn Db, + file: PythonFile<'db>, + offset: TextSize, +) -> Option>> { let parsed = parsed_module(db, file).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &parsed, offset)?; @@ -23,6 +29,7 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option Option { @@ -99,10 +106,10 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option { let value_ty = alias.value_type(db); - alias_docstring = Definitions::from_ty(db, ty) + alias_docstring = Definitions::from_ty(db, &env, ty) .and_then(|def| def.docstring(db)) .or_else(|| { - Definitions::from_ty(db, value_ty).and_then(|def| def.docstring(db)) + Definitions::from_ty(db, &env, value_ty).and_then(|def| def.docstring(db)) }); HoverContent::TypeAlias { alias, qualifiers } @@ -133,8 +140,11 @@ pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option Option { + python_file: PythonFile<'db>, contents: Vec>, } @@ -258,13 +269,15 @@ pub struct DisplayHover<'db, 'a> { impl fmt::Display for DisplayHover<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let db = self.db; let mut first = true; + let env = ProgramEnvironment::from_file(self.hover.python_file); for content in &self.hover.contents { if !first { self.kind.horizontal_line().fmt(f)?; } - content.display(self.db, self.kind).fmt(f)?; + content.display(db, &env, self.kind).fmt(f)?; first = false; } @@ -300,9 +313,15 @@ pub enum HoverContent<'db> { } impl<'db> HoverContent<'db> { - fn display(&self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHoverContent<'_, 'db> { + fn display<'a>( + &'a self, + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + kind: MarkupKind, + ) -> DisplayHoverContent<'a, 'db> { DisplayHoverContent { db, + env, content: self, kind, } @@ -311,16 +330,18 @@ impl<'db> HoverContent<'db> { pub(crate) struct DisplayHoverContent<'a, 'db> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, content: &'a HoverContent<'db>, kind: MarkupKind, } impl<'db> DisplayHoverContent<'_, 'db> { fn ty_string_and_syntax(&self, ty: &Type<'db>) -> (String, &'static str) { + let db = self.db; // Special types like `` // render poorly with python syntax-highlighting but well as xml let ty_string = ty - .display_with(self.db, DisplaySettings::default().multiline()) + .display_with(db, self.env, DisplaySettings::default().multiline()) .to_string(); let syntax = if ty_string.starts_with('<') { "xml" @@ -346,6 +367,7 @@ fn create_qualifier_suffix(qualifiers: TypeQualifiers) -> String { impl fmt::Display for DisplayHoverContent<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let db = self.db; match self.content { HoverContent::Signature(signature) => { self.kind.fenced_code_block(&signature, "python").fmt(f) @@ -375,7 +397,7 @@ impl fmt::Display for DisplayHoverContent<'_, '_> { } HoverContent::TypeAlias { alias, qualifiers } => { let qualifier_suffix = create_qualifier_suffix(*qualifiers); - let declaration = alias.display_declaration(self.db); + let declaration = alias.display_declaration(db, self.env); self.kind .fenced_code_block(format!("{declaration}{qualifier_suffix}"), "python") @@ -6876,7 +6898,11 @@ type U = MyType fn hover(&self) -> String { use std::fmt::Write; - let Some(hover) = hover(&self.db, self.cursor.file, self.cursor.offset) else { + let Some(hover) = hover( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + ) else { return "Hover provided no content".to_string(); }; diff --git a/crates/ty_ide/src/importer.rs b/crates/ty_ide/src/importer.rs index c2ed60d5ae..821b0ffa2a 100644 --- a/crates/ty_ide/src/importer.rs +++ b/crates/ty_ide/src/importer.rs @@ -18,8 +18,9 @@ The main differences here are: use rustc_hash::FxHashMap; -use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; + +use ruff_db::PythonFile; use ruff_db::source::source_text; use ruff_diagnostics::Edit; use ruff_python_ast as ast; @@ -40,7 +41,7 @@ pub(crate) struct Importer<'a> { db: &'a dyn Db, /// The file corresponding to the module that /// we want to insert an import statement into. - file: File, + file: PythonFile<'a>, /// The parsed module ref. parsed: &'a ParsedModuleRef, /// The tokens representing the Python AST. @@ -73,7 +74,7 @@ impl<'a> Importer<'a> { pub(crate) fn new( db: &'a dyn Db, stylist: &'a Stylist<'a>, - file: File, + file: PythonFile<'a>, source: &'a str, parsed: &'a ParsedModuleRef, ) -> Self { @@ -151,7 +152,7 @@ impl<'a> Importer<'a> { let insertion = if let Some(future) = self.find_last_future_import(members.at) { Insertion::end_of_statement(future.stmt, self.source, self.stylist) } else { - let range = source_text(self.db, self.file) + let range = source_text(self.db, self.file.file(self.db)) .as_notebook() .and_then(|notebook| notebook.cell_offsets().containing_range(members.at)); @@ -226,7 +227,7 @@ impl<'a> Importer<'a> { available_at: TextSize, ) -> Option> { let mut choice = None; - let source = source_text(self.db, self.file); + let source = source_text(self.db, self.file.file(self.db)); let notebook = source.as_notebook(); for import in &self.imports { @@ -283,7 +284,7 @@ impl<'a> Importer<'a> { /// Find the last `from __future__` import statement in the AST. fn find_last_future_import(&self, at: TextSize) -> Option<&'a AstImport> { - let source = source_text(self.db, self.file); + let source = source_text(self.db, self.file.file(self.db)); let notebook = source.as_notebook(); self.imports @@ -330,7 +331,7 @@ pub struct MembersInScope<'ast> { impl<'ast> MembersInScope<'ast> { fn new( db: &'ast dyn Db, - file: File, + file: PythonFile<'ast>, parsed: &'ast ParsedModuleRef, node: ast::AnyNodeRef<'_>, at: TextSize, @@ -372,7 +373,7 @@ impl<'ast> MembersInScope<'ast> { pub(crate) fn satisfies( &self, db: &dyn Db, - importing_file: File, + importing_file: PythonFile<'_>, request: &ImportRequest<'_>, ) -> bool { let symbol_text = request.member.unwrap_or(request.module); @@ -408,7 +409,7 @@ impl<'ast> MemberInScope<'ast> { fn satisfies_anywhere( &self, db: &dyn Db, - importing_file: File, + importing_file: PythonFile<'_>, request: &ImportRequest<'_>, ) -> bool { let MemberImportKind::Imported(ref ast_import) = self.kind else { @@ -480,7 +481,7 @@ impl<'ast> AstImport<'ast> { fn satisfies<'importer>( &'importer self, db: &'_ dyn Db, - importing_file: File, + importing_file: PythonFile<'_>, request: &ImportRequest<'_>, ) -> Option> { self.kind @@ -511,7 +512,7 @@ impl<'ast> AstImportKind<'ast> { fn satisfies<'importer>( &'importer self, db: &'_ dyn Db, - importing_file: File, + importing_file: PythonFile<'_>, request: &ImportRequest<'_>, ) -> Option> { match *self { @@ -635,7 +636,12 @@ impl<'a> ImportRequest<'a> { /// Attempts to change the import request style so that the chances /// of an import conflict are minimized (although not always reduced /// to zero). - fn avoid_conflicts(self, db: &dyn Db, importing_file: File, members: &MembersInScope) -> Self { + fn avoid_conflicts( + self, + db: &dyn Db, + importing_file: PythonFile<'_>, + members: &MembersInScope, + ) -> Self { let Some(member) = self.member else { return Self { style: ImportStyle::Import, @@ -918,6 +924,7 @@ mod tests { use ty_module_resolver::SearchPathSettings; use ty_project::ProjectMetadata; use ty_python_core::program::{Program, ProgramSettings}; + use ty_python_semantic::Db as _; use ty_python_semantic::{PythonVersionWithSource, SemanticModel}; use super::*; @@ -972,7 +979,7 @@ mod tests { Importer::new( &self.db, &self.cursor.stylist, - self.cursor.file, + PythonFile::new(&self.db, self.cursor.file, self.db.python_version()), self.cursor.source.as_str(), &self.cursor.parsed, ) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 3952f7acd8..35a507bf05 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -1,10 +1,11 @@ use std::{fmt, vec}; +use ty_python_semantic::ProgramEnvironment; use rustc_hash::FxHashMap; use crate::importer::{ImportAction, ImportRequest, Importer, MembersInScope}; use crate::{Db, HasNavigationTargets, NavigationTarget}; -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal}; @@ -25,11 +26,11 @@ pub struct InlayHint { } impl InlayHint { - fn variable_type( - context: InlayHintImportContext, + fn variable_type<'db>( + context: InlayHintImportContext<'_, 'db>, expr: &Expr, rhs: &Expr, - ty: Type, + ty: Type<'db>, mut allow_edits: bool, ) -> Option { let InlayHintImportContext { @@ -40,8 +41,9 @@ impl InlayHint { } = context; let position = expr.range().end(); + let env = ProgramEnvironment::from_file(file); // Render the type to a string, and get subspans for all the types that make it up - let details = ty.display(db).to_string_parts(); + let details = ty.display(db, &env).to_string_parts(); // Filter out repetitive hints like `x: T = T()` if call_matches_name(rhs, &details.label) { @@ -77,8 +79,8 @@ impl InlayHint { } // Possibly import the current type and return the qualified name - let mut qualified_name = |dynamic_importer: &mut DynamicImporter| { - let type_definition = ty.definition(db)?; + let mut qualified_name = |dynamic_importer: &mut DynamicImporter<'_, 'db>| { + let type_definition = ty.definition(db, &env)?; let definition = type_definition.definition()?; // Only module-level names can be imported with `from import `. @@ -90,7 +92,7 @@ impl InlayHint { // Don't try to import symbols in scope let definition_file = definition.file(db); - if definition_file == file { + if definition_file == file.file(db) { return None; } @@ -101,7 +103,7 @@ impl InlayHint { .as_deref() .unwrap_or(&details.label[start..end]); - let module = file_to_module(db, definition_file)?; + let module = file_to_module(db, definition.python_file(db))?; if should_skip_import(db, module, *ty) { return None; @@ -111,6 +113,7 @@ impl InlayHint { dynamic_importer.import_symbol( db, + &env, ty, module_name, definition_name, @@ -130,7 +133,7 @@ impl InlayHint { qualified_name.len().cast_signed() - (end - start).cast_signed(); } - let target = ty.navigation_targets(db).into_iter().next(); + let target = ty.navigation_targets(db, &env).into_iter().next(); // Always use original text for the label part label_parts.push( @@ -288,13 +291,14 @@ pub struct InlayHintTextEdit { pub fn inlay_hints( db: &dyn Db, - file: File, + file: PythonFile<'_>, range: TextRange, settings: &InlayHintSettings, ) -> Vec { let ast = parsed_module(db, file).load(db); + let source_file = file.file(db); - let source = source_text(db, file); + let source = source_text(db, source_file); let stylist = Stylist::from_tokens(ast.tokens(), source.as_str()); let importer = Importer::new(db, &stylist, file, source.as_str(), &ast); @@ -344,7 +348,7 @@ impl Default for InlayHintSettings { struct InlayHintImportContext<'a, 'db> { db: &'db dyn Db, - file: File, + file: PythonFile<'db>, importer: &'a Importer<'db>, dynamic_imports: &'a mut FxHashMap, } @@ -366,7 +370,7 @@ struct InlayHintVisitor<'a, 'db> { impl<'a, 'db> InlayHintVisitor<'a, 'db> { fn new( db: &'db dyn Db, - file: File, + file: PythonFile<'db>, importer: Importer<'db>, range: TextRange, settings: &'a InlayHintSettings, @@ -395,7 +399,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { let context = InlayHintImportContext { db: self.db, - file: self.model.file(), + file: self.model.python_file(), importer: &self.importer, dynamic_imports: &mut self.dynamic_imports, }; @@ -703,8 +707,9 @@ impl<'a, 'db> DynamicImporter<'a, 'db> { /// If the symbol in the text edit needs to be qualified, we return the qualified symbol text. fn import_symbol( &mut self, - db: &dyn Db, - ty: &Type, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: &Type<'db>, module_name: &str, symbol_name: &str, label_text: &str, @@ -721,7 +726,7 @@ impl<'a, 'db> DynamicImporter<'a, 'db> { let mut is_possibly_qualified_name = label_text.contains('.'); if let Some(member) = members.find_member(symbol_name) { - if member.ty.definition(db) == ty.definition(db) { + if member.ty.definition(db, env) == ty.definition(db, env) { return None; } @@ -875,7 +880,12 @@ mod tests { /// Returns the inlay hints for the given test case with custom settings. fn inlay_hints_with_settings(&mut self, settings: &InlayHintSettings) -> String { - let hints = inlay_hints(&self.db, self.file, self.range, settings); + let hints = inlay_hints( + &self.db, + PythonFile::new(&self.db, self.file, self.db.python_version()), + self.range, + settings, + ); let mut inlay_hint_buf = source_text(&self.db, self.file).as_str().to_string(); let mut text_edit_buf = inlay_hint_buf.clone(); diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 341ac0efa2..b0bdcf8403 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -74,6 +74,7 @@ use ruff_text_size::{Ranged, TextRange}; use rustc_hash::{FxBuildHasher, FxHashSet}; use std::ops::{Deref, DerefMut}; use ty_project::Db; +use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::types::{Type, TypeDefinition}; type FxIndexMap = indexmap::IndexMap; @@ -280,23 +281,23 @@ impl FromIterator for NavigationTargets { } pub trait HasNavigationTargets { - fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets; + fn navigation_targets(&self, db: &dyn Db, env: &ProgramEnvironment<'_>) -> NavigationTargets; } impl HasNavigationTargets for Type<'_> { - fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { + fn navigation_targets(&self, db: &dyn Db, env: &ProgramEnvironment<'_>) -> NavigationTargets { match self { Type::Union(union) => union .elements(db) .iter() - .flat_map(|target| target.navigation_targets(db)) + .flat_map(|target| target.navigation_targets(db, env)) .collect(), Type::Intersection(intersection) => { - if let Some(alternatives) = intersection.finite_alternatives(db) { + if let Some(alternatives) = intersection.finite_alternatives(db, env) { return alternatives .iter() - .flat_map(|alternative| alternative.navigation_targets(db)) + .flat_map(|alternative| alternative.navigation_targets(db, env)) .collect(); } @@ -313,26 +314,26 @@ impl HasNavigationTargets for Type<'_> { // because the type is the intersection of all those types. NavigationTargets::empty() } - None => first.navigation_targets(db), + None => first.navigation_targets(db, env), } } Type::EnumComplement(complement) => complement - .remaining_literal_types(db) + .remaining_literal_types(db, env) .iter() - .flat_map(|alternative| alternative.navigation_targets(db)) + .flat_map(|alternative| alternative.navigation_targets(db, env)) .collect(), ty => ty - .definition(db) - .map(|definition| definition.navigation_targets(db)) + .definition(db, env) + .map(|definition| definition.navigation_targets(db, env)) .unwrap_or_else(NavigationTargets::empty), } } } impl HasNavigationTargets for TypeDefinition<'_> { - fn navigation_targets(&self, db: &dyn Db) -> NavigationTargets { + fn navigation_targets(&self, db: &dyn Db, _: &ProgramEnvironment<'_>) -> NavigationTargets { let Some(full_range) = self.full_range(db) else { return NavigationTargets::empty(); }; @@ -402,6 +403,7 @@ mod tests { use insta::internals::SettingsBindDropGuard; use ruff_db::Db; + use ruff_db::PythonFile; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, UnifiedFile, }; @@ -438,6 +440,10 @@ mod tests { CursorTestBuilder::default() } + pub(super) fn python_file(&self, file: File) -> PythonFile<'_> { + PythonFile::new(&self.db, file, self.db.python_version()) + } + pub(super) fn write_file( &mut self, path: impl AsRef, @@ -562,7 +568,9 @@ mod tests { db.project().open_file(&mut db, file); let source = source_text(&db, file); - let parsed = parsed_module(&db, file).load(&db); + let parsed = + parsed_module(&db, PythonFile::new(&db, file, db.python_version())) + .load(&db); let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str()).into_owned(); cursor = Some(Cursor { @@ -714,7 +722,9 @@ mod tests { db.project().open_file(&mut db, file); let source = source_text(&db, file); - let parsed = parsed_module(&db, file).load(&db); + let parsed = + parsed_module(&db, PythonFile::new(&db, file, db.python_version())) + .load(&db); let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str()).into_owned(); cursor = Some(Cursor { diff --git a/crates/ty_ide/src/references.rs b/crates/ty_ide/src/references.rs index 4af8da9033..071d779ac6 100644 --- a/crates/ty_ide/src/references.rs +++ b/crates/ty_ide/src/references.rs @@ -13,7 +13,8 @@ use crate::goto::{Definitions, GotoTarget}; use crate::{Db, ReferenceKind, ReferenceTarget}; use rayon::prelude::*; -use ruff_db::files::File; +use ruff_db::PythonFile; +use ruff_db::parsed::parsed_module; use ruff_python_ast::find_node::{CoveringNode, covering_node}; use ruff_python_ast::token::Tokens; use ruff_python_ast::{ @@ -86,10 +87,11 @@ impl ReferencesMode { /// Search for references across all files in the project. pub(crate) fn references( db: &dyn Db, - file: File, + file: PythonFile<'_>, goto_target: &GotoTarget, mode: ReferencesMode, ) -> Option> { + let source_file = file.file(db); let model = SemanticModel::new(db, file); let target_definitions = goto_target.definitions(&model, mode.to_import_alias_resolution())?; let is_externally_visible_symbol = @@ -117,10 +119,11 @@ pub(crate) fn references( if search_across_files && (is_parameter || is_externally_visible_symbol) { let files = db.project().files(db); + let python_version = file.python_version(db); let files: Vec<_> = files .iter() .copied() - .filter(|other| *other != file) + .filter(|other| *other != source_file) .collect(); let minimum_job_len = minimum_parallel_job_len(files.len(), MAX_MIN_FILES_PER_PARALLEL_JOB); let other_references = files @@ -132,6 +135,8 @@ pub(crate) fn references( return Vec::new(); } + let other_file = PythonFile::new(db, other_file, python_version); + if is_externally_visible_symbol { references_for_file(db, other_file, &target_definitions, &target_text, mode) } else { @@ -159,7 +164,7 @@ pub(crate) fn references( fn references_for_keyword_arguments_in_file( db: &dyn Db, - file: File, + file: PythonFile<'_>, target_definitions: &Definitions<'_>, target_text: &str, mode: ReferencesMode, @@ -171,7 +176,7 @@ fn references_for_keyword_arguments_in_file( "keyword-label cross-file scan should not run in DocumentHighlights mode" ); - let parsed = ruff_db::parsed::parsed_module(db, file); + let parsed = parsed_module(db, file); let module = parsed.load(db); let model = SemanticModel::new(db, file); let mut references = Vec::new(); @@ -220,12 +225,12 @@ fn is_slots_assignment(node: AnyNodeRef<'_>, value: AnyNodeRef<'_>) -> bool { /// The behavior depends on the provided mode. fn references_for_file( db: &dyn Db, - file: File, + file: PythonFile<'_>, target_definitions: &Definitions<'_>, target_text: &str, mode: ReferencesMode, ) -> Vec { - let parsed = ruff_db::parsed::parsed_module(db, file); + let parsed = parsed_module(db, file); let module = parsed.load(db); let model = SemanticModel::new(db, file); let mut references = Vec::new(); @@ -256,7 +261,7 @@ pub(crate) fn has_any_external_visible_definitions( ScopeKind::Comprehension => { matches!(definition.kind(db), DefinitionKind::NamedExpression(_)) && definition.place(db).as_symbol().is_some_and(|symbol_id| { - ty_python_core::semantic_index(db, definition.file(db)) + ty_python_core::semantic_index(db, definition.python_file(db)) .symbol_resolves_to_global_scope(symbol_id, definition.file_scope(db)) }) } @@ -284,11 +289,13 @@ fn parameter_owner_is_externally_visible( fn parameter_owner_is_externally_visible_for_target( db: &dyn Db, - definition: &ResolvedDefinition, + resolved: &ResolvedDefinition, ) -> bool { - let target = definition.focus_range(db); - let file = target.file(); - let parsed = ruff_db::parsed::parsed_module(db, file); + let Some(definition) = resolved.definition() else { + return false; + }; + let parsed = parsed_module(db, definition.python_file(db)); + let target = definition.focus_range(db, &parsed.load(db)); let module = parsed.load(db); let covering = covering_node(module.syntax().into(), target.range()); @@ -699,8 +706,8 @@ impl<'a> LocalReferencesFinder<'a> { let db = self.model.db(); let file = self.model.file(); let class_range = class.range(); - let module = ruff_db::parsed::parsed_module(db, file).load(db); - let index = ty_python_core::semantic_index(db, file); + let module = ruff_db::parsed::parsed_module(db, self.model.python_file()).load(db); + let index = ty_python_core::semantic_index(db, self.model.python_file()); // The nearest class scope lexically enclosing `scope`, if any. `ancestor_scopes` skips // class scopes for name resolution, so we walk the lexical parents directly to stop at the @@ -757,7 +764,7 @@ impl<'a> LocalReferencesFinder<'a> { }; let file = local_definition.file(db); - let module = ruff_db::parsed::parsed_module(db, file).load(db); + let module = ruff_db::parsed::parsed_module(db, local_definition.python_file(db)).load(db); let kind = local_definition.kind(db); let category = kind.category(file.is_stub(db), &module); @@ -801,7 +808,7 @@ mod tests { use crate::tests::{CursorTest, cursor_test}; fn cursor_target_is_externally_visible(test: &CursorTest) -> bool { - let model = SemanticModel::new(&test.db, test.cursor.file); + let model = SemanticModel::new(&test.db, test.python_file(test.cursor.file)); let goto_target = find_goto_target(&model, &test.cursor.parsed, test.cursor.offset).unwrap(); let definitions = goto_target diff --git a/crates/ty_ide/src/rename.rs b/crates/ty_ide/src/rename.rs index 183125a5ea..bbe2adba45 100644 --- a/crates/ty_ide/src/rename.rs +++ b/crates/ty_ide/src/rename.rs @@ -1,14 +1,20 @@ use crate::goto::find_goto_target; use crate::references::{ReferencesMode, references}; use crate::{Db, ReferenceTarget}; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_text_size::{Ranged, TextSize}; use ty_python_semantic::SemanticModel; /// Returns the range of the symbol if it can be renamed, None if not. -pub fn can_rename(db: &dyn Db, file: File, offset: TextSize) -> Option { +pub fn can_rename( + db: &dyn Db, + file: PythonFile<'_>, + offset: TextSize, +) -> Option { let parsed = ruff_db::parsed::parsed_module(db, file); let module = parsed.load(db); + let source_file = file.file(db); let model = SemanticModel::new(db, file); // Get the definitions for the symbol at the offset @@ -22,7 +28,7 @@ pub fn can_rename(db: &dyn Db, file: File, offset: TextSize) -> Option Option Option, offset: TextSize, new_name: &str, ) -> Option> { @@ -68,7 +74,7 @@ pub fn rename( // Determine if we should do a multi-file rename or single-file rename // based on whether the current file is part of the project - let current_file_in_project = is_file_in_project(db, file); + let current_file_in_project = is_file_in_project(db, file.file(db)); // Choose the appropriate rename mode: // - If current file is in project, do multi-file rename @@ -100,7 +106,11 @@ mod tests { impl CursorTest { fn prepare_rename(&self) -> String { let Some(range) = salsa::attach(&self.db, || { - can_rename(&self.db, self.cursor.file, self.cursor.offset) + can_rename( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + ) }) else { return "Cannot rename".to_string(); }; @@ -110,9 +120,18 @@ mod tests { fn rename(&self, new_name: &str) -> String { let rename_results = salsa::attach(&self.db, || { - can_rename(&self.db, self.cursor.file, self.cursor.offset)?; - - rename(&self.db, self.cursor.file, self.cursor.offset, new_name) + can_rename( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + )?; + + rename( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + new_name, + ) }); let Some(rename_results) = rename_results else { diff --git a/crates/ty_ide/src/selection_range.rs b/crates/ty_ide/src/selection_range.rs index 6cac9348e3..2b6bc9b4dd 100644 --- a/crates/ty_ide/src/selection_range.rs +++ b/crates/ty_ide/src/selection_range.rs @@ -1,4 +1,4 @@ -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast::find_node::covering_node; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -7,7 +7,7 @@ use crate::Db; /// Returns a list of nested selection ranges, where each range contains the next one. /// The first range in the list is the largest range containing the cursor position. -pub fn selection_range(db: &dyn Db, file: File, offset: TextSize) -> Vec { +pub fn selection_range(db: &dyn Db, file: PythonFile<'_>, offset: TextSize) -> Vec { let parsed = parsed_module(db, file).load(db); let range = TextRange::empty(offset); @@ -435,7 +435,11 @@ b"123a𝐁c" impl CursorTest { fn selection_range(&self) -> String { - let ranges = selection_range(&self.db, self.cursor.file, self.cursor.offset); + let ranges = selection_range( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + ); if ranges.is_empty() { return "No selection range found".to_string(); diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index f10e4109f0..26155ae448 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -28,7 +28,7 @@ use crate::Db; use bitflags::bitflags; -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast::visitor::source_order::{ SourceOrderVisitor, TraversalSignal, walk_arguments, walk_expr, @@ -183,7 +183,11 @@ impl Deref for SemanticTokens { /// Generates semantic tokens for a Python file within the specified range. /// Pass None to get tokens for the entire file. -pub fn semantic_tokens(db: &dyn Db, file: File, range: Option) -> SemanticTokens { +pub fn semantic_tokens( + db: &dyn Db, + file: PythonFile<'_>, + range: Option, +) -> SemanticTokens { let parsed = parsed_module(db, file).load(db); let model = SemanticModel::new(db, file); @@ -299,8 +303,7 @@ impl<'db> SemanticTokenVisitor<'db> { ) -> Option<(SemanticTokenType, SemanticTokenModifier)> { let mut modifiers = SemanticTokenModifier::empty(); let db = self.model.db(); - let file = definition.file(db); - let model = SemanticModel::new(db, file); + let model = SemanticModel::new(db, definition.python_file(db)); if model.is_type_alias_definition(definition) { return Some((SemanticTokenType::Class, modifiers)); @@ -320,7 +323,7 @@ impl<'db> SemanticTokenVisitor<'db> { Some((SemanticTokenType::TypeParameter, modifiers)) } DefinitionKind::Parameter(ParameterDefinitionNodeKind::Parameter(parameter)) => { - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, definition.python_file(db)); let ty = parameter.node(&parsed.load(db)).inferred_type(&model); if let Some(ty) = ty { @@ -364,7 +367,7 @@ impl<'db> SemanticTokenVisitor<'db> { let value_ty = match kind { DefinitionKind::Assignment(assignment) => { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, definition.python_file(db)).load(db); assignment.value(&parsed).inferred_type(&model) } _ => None, @@ -1284,7 +1287,7 @@ mod tests { use insta::assert_snapshot; use ruff_db::{ - files::system_path_to_file, + files::{File, system_path_to_file}, system::{DbWithWritableSystem, SystemPath, SystemPathBuf}, }; use ty_project::ProjectMetadata; @@ -4708,12 +4711,20 @@ from pathlib import Missing as Alias /// Get semantic tokens for the entire file fn highlight_file(&self) -> SemanticTokens { - semantic_tokens(&self.db, self.file, None) + semantic_tokens( + &self.db, + PythonFile::new(&self.db, self.file, self.db.python_version()), + None, + ) } /// Get semantic tokens for a specific range in the file fn highlight_range(&self, range: TextRange) -> SemanticTokens { - semantic_tokens(&self.db, self.file, Some(range)) + semantic_tokens( + &self.db, + PythonFile::new(&self.db, self.file, self.db.python_version()), + Some(range), + ) } /// Helper function to convert semantic tokens to a snapshot-friendly text format diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs index e4455ea5ec..a94d33d4b3 100644 --- a/crates/ty_ide/src/signature_help.rs +++ b/crates/ty_ide/src/signature_help.rs @@ -10,7 +10,7 @@ use crate::Db; use crate::FxIndexMap; use crate::docstring::Docstring; use crate::goto::docstring_for_call_definition; -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast::find_node::covering_node; use ruff_python_ast::token::TokenKind; @@ -74,7 +74,11 @@ pub struct SignatureHelpInfo<'db> { } /// Signature help information for function calls at the given position -pub fn signature_help(db: &dyn Db, file: File, offset: TextSize) -> Option> { +pub fn signature_help<'db>( + db: &'db dyn Db, + file: PythonFile<'db>, + offset: TextSize, +) -> Option> { let parsed = parsed_module(db, file).load(db); // Get the call expression at the given position. @@ -159,7 +163,7 @@ fn get_call_expr( return None; }; - // Determine which argument corresponding to the current cursor location. + // Determine which argument corresponds to the current cursor location. let current_arg_index = get_argument_index(call_expr, offset); Some((call_expr, current_arg_index)) @@ -182,7 +186,7 @@ fn get_argument_index(call_expr: &ast::ExprCall, offset: TextSize) -> usize { /// Create signature details from `CallSignatureDetails`. fn create_signature_details_from_call_signature_details<'db>( - db: &dyn crate::Db, + db: &'db dyn Db, details: CallSignatureDetails<'db>, current_arg_index: usize, ) -> SignatureDetails<'db> { @@ -976,7 +980,12 @@ def ab(a: int, *, c: int): // the parameter type should be `str` (not `_KT`). let key_param = &signature.parameters[0]; assert_eq!(key_param.name, "key"); - let type_display = format!("{}", key_param.ty.display(&test.db)); + let type_display = format!( + "{}", + key_param + .ty + .display(&test.db, &test.db.program_environment()) + ); assert_eq!(type_display, "str"); } @@ -997,7 +1006,12 @@ def ab(a: int, *, c: int): // list.append's parameter is typed as `_T`, which should resolve // to `int` for a `list[int]`. let object_param = &signature.parameters[0]; - let type_display = format!("{}", object_param.ty.display(&test.db)); + let type_display = format!( + "{}", + object_param + .ty + .display(&test.db, &test.db.program_environment()) + ); assert_eq!(type_display, "int"); } @@ -1024,12 +1038,18 @@ def ab(a: int, *, c: int): // `T` should be resolved to `str` from the first argument. let a_param = &signature.parameters[0]; assert_eq!(a_param.name, "a"); - let a_type = format!("{}", a_param.ty.display(&test.db)); + let a_type = format!( + "{}", + a_param.ty.display(&test.db, &test.db.program_environment()) + ); assert_eq!(a_type, "str"); let b_param = &signature.parameters[1]; assert_eq!(b_param.name, "b"); - let b_type = format!("{}", b_param.ty.display(&test.db)); + let b_type = format!( + "{}", + b_param.ty.display(&test.db, &test.db.program_environment()) + ); assert_eq!(b_type, "str"); } @@ -1442,7 +1462,11 @@ def ab(a: int, *, c: int): impl CursorTest { fn signature_help(&self) -> Option> { - crate::signature_help::signature_help(&self.db, self.cursor.file, self.cursor.offset) + crate::signature_help::signature_help( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + ) } fn signature_help_render(&self) -> String { diff --git a/crates/ty_ide/src/symbols.rs b/crates/ty_ide/src/symbols.rs index a30d8d23e2..86fda0e4b8 100644 --- a/crates/ty_ide/src/symbols.rs +++ b/crates/ty_ide/src/symbols.rs @@ -6,8 +6,9 @@ use std::ops::Range; use regex::Regex; -use ruff_db::files::File; use ruff_db::parsed::parsed_module; + +use ruff_db::PythonFile; use ruff_index::{IndexVec, newtype_index}; use ruff_python_ast as ast; use ruff_python_ast::name::{Name, UnqualifiedName}; @@ -391,7 +392,7 @@ impl SymbolKind { /// The flattened list includes parent/child information and can be /// converted into a hierarchical collection of symbols. #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn symbols_for_file(db: &dyn Db, file: File) -> FlatSymbols { +pub(crate) fn symbols_for_file(db: &dyn Db, file: PythonFile<'_>) -> FlatSymbols { let parsed = parsed_module(db, file); let module = parsed.load(db); @@ -410,14 +411,15 @@ pub(crate) fn symbols_for_file(db: &dyn Db, file: File) -> FlatSymbols { cycle_initial=|_, _, _| FlatSymbols::default(), heap_size=ruff_memory_usage::heap_size, )] -pub(crate) fn symbols_for_file_global_only(db: &dyn Db, file: File) -> FlatSymbols { +pub(crate) fn symbols_for_file_global_only(db: &dyn Db, file: PythonFile<'_>) -> FlatSymbols { + let source_file = file.file(db); let parsed = parsed_module(db, file); let module = parsed.load(db); let mut visitor = SymbolVisitor::globals(db, file); visitor.visit_body(&module.syntax().body); - if file + if source_file .path(db) .as_system_path() .is_none_or(|path| !db.project().is_file_included(db, path).is_included()) @@ -453,7 +455,7 @@ impl ImportedFrom { fn import_from( db: &dyn Db, - importing_file: File, + importing_file: PythonFile<'_>, ast: &ast::StmtImportFrom, kind: ImportKind, ) -> Option { @@ -594,7 +596,7 @@ impl<'db> Imports<'db> { fn get_module_symbols( &self, db: &'db dyn Db, - importing_file: File, + importing_file: PythonFile<'db>, name: &ModuleName, ) -> Option<&'db FlatSymbols> { let module_name = match self.module_names.get(name.as_str())? { @@ -603,7 +605,7 @@ impl<'db> Imports<'db> { } }; let module = resolve_module(db, importing_file, &module_name)?; - Some(symbols_for_file_global_only(db, module.file(db)?)) + Some(symbols_for_file_global_only(db, module.python_file(db)?)) } } @@ -652,7 +654,11 @@ enum ImportModuleName<'db> { impl<'db> ImportModuleName<'db> { /// Converts the lazy representation of a module name into an /// actual `ModuleName` that can be used for module resolution. - fn to_module_name(self, db: &'db dyn Db, importing_file: File) -> Option { + fn to_module_name( + self, + db: &'db dyn Db, + importing_file: PythonFile<'db>, + ) -> Option { match self { ImportModuleName::Import(name) => ModuleName::new(name), ImportModuleName::ImportFrom { parent, child } => { @@ -688,7 +694,7 @@ impl Ranged for AstImport<'_> { #[expect(clippy::struct_excessive_bools)] struct SymbolVisitor<'db> { db: &'db dyn Db, - file: File, + file: PythonFile<'db>, symbols: IndexVec, symbol_stack: Vec, /// Track if we're currently inside a function at any point. @@ -726,7 +732,7 @@ struct SymbolVisitor<'db> { } impl<'db> SymbolVisitor<'db> { - fn tree(db: &'db dyn Db, file: File) -> Self { + fn tree(db: &'db dyn Db, file: PythonFile<'db>) -> Self { Self { db, file, @@ -744,7 +750,7 @@ impl<'db> SymbolVisitor<'db> { } } - fn globals(db: &'db dyn Db, file: File) -> Self { + fn globals(db: &'db dyn Db, file: PythonFile<'db>) -> Self { Self { exports_only: true, ..Self::tree(db, file) @@ -755,7 +761,10 @@ impl<'db> SymbolVisitor<'db> { // If `__all__` was found but wasn't recognized, // then we emit a diagnostic message indicating as such. if self.all_invalid { - tracing::debug!("Invalid `__all__` in `{}`", self.file.path(self.db)); + tracing::debug!( + "Invalid `__all__` in `{}`", + self.file.file(self.db).path(self.db) + ); } // We want to filter out some of the symbols we collected. // Specifically, to respect conventions around library @@ -1127,7 +1136,10 @@ impl<'db> SymbolVisitor<'db> { let module_name = ModuleName::from_import_statement(self.db, self.file, import_from).ok()?; let module = resolve_module(self.db, self.file, &module_name)?; - Some(symbols_for_file_global_only(self.db, module.file(self.db)?)) + Some(symbols_for_file_global_only( + self.db, + module.python_file(self.db)?, + )) } /// Add valid names from `__all__` to the set of existing `__all__` @@ -1587,6 +1599,7 @@ mod tests { use insta::internals::SettingsBindDropGuard; use ruff_db::Db; + use ruff_db::PythonFile; use ruff_db::files::{FileRootKind, system_path_to_file}; use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf}; use ruff_python_ast::PythonVersion; @@ -3150,7 +3163,10 @@ class C: ... /// The path given must have been written to this test's salsa DB. fn exported_symbols_for(&self, path: impl AsRef) -> &super::FlatSymbols { let file = system_path_to_file(&self.db, path.as_ref()).unwrap(); - symbols_for_file_global_only(&self.db, file) + symbols_for_file_global_only( + &self.db, + PythonFile::new(&self.db, file, self.db.python_version()), + ) } /// Returns the exports from the module at the given path. diff --git a/crates/ty_ide/src/type_hierarchy.rs b/crates/ty_ide/src/type_hierarchy.rs index e7fff23e78..d02da06ecb 100644 --- a/crates/ty_ide/src/type_hierarchy.rs +++ b/crates/ty_ide/src/type_hierarchy.rs @@ -1,14 +1,15 @@ use crate::Db; use crate::goto::find_goto_target; use rayon::prelude::*; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; use ruff_text_size::{TextRange, TextSize}; use ty_project::parallel::ParallelIteratorExt; -use ty_python_semantic::SemanticModel; use ty_python_semantic::TypeHierarchyClass; use ty_python_semantic::types::Type; +use ty_python_semantic::{ProgramEnvironment, SemanticModel}; /// Represents a type hierarchy item returned by the LSP type hierarchy requests. #[derive(Debug, Clone)] @@ -30,7 +31,7 @@ pub struct TypeHierarchyItem { /// Returns `None` if the position is not on a class definition or class reference. pub fn prepare_type_hierarchy( db: &dyn Db, - file: File, + file: PythonFile<'_>, offset: TextSize, ) -> Option { let module = parsed_module(db, file).load(db); @@ -38,20 +39,22 @@ pub fn prepare_type_hierarchy( let goto_target = find_goto_target(&model, &module, offset)?; let ty = goto_target.inferred_type(&model)?; - let hierarchy_class = ty_python_semantic::type_hierarchy_prepare(db, ty)?; + let env = model.program_environment(); + let hierarchy_class = ty_python_semantic::type_hierarchy_prepare(db, &env, ty)?; Some(type_hierarchy_class_to_item(db, hierarchy_class)) } /// Get the supertypes (base classes) of a type hierarchy item. pub fn type_hierarchy_supertypes( db: &dyn Db, - file: File, + file: PythonFile<'_>, offset: TextSize, ) -> Vec { let Some(ty) = resolve_type_at(db, file, offset) else { return vec![]; }; - ty_python_semantic::type_hierarchy_supertypes(db, ty) + let env = ProgramEnvironment::from_file(file); + ty_python_semantic::type_hierarchy_supertypes(db, &env, ty) .into_iter() .map(|c| type_hierarchy_class_to_item(db, c)) .collect() @@ -62,17 +65,18 @@ pub fn type_hierarchy_supertypes( /// This scans all available modules and can be expensive in large projects. pub fn type_hierarchy_subtypes( db: &dyn Db, - file: File, + file: PythonFile<'_>, offset: TextSize, ) -> Vec { let Some(ty) = resolve_type_at(db, file, offset) else { return vec![]; }; - ty_module_resolver::all_modules(db) + ty_module_resolver::all_modules(db, file.python_version(db)) .into_par_iter() .map_with_db(db, |db, module| { - ty_python_semantic::type_hierarchy_subtypes(db, ty, &[module]) + let env = ProgramEnvironment::from_file(file); + ty_python_semantic::type_hierarchy_subtypes(db, &env, ty, &[module]) .into_iter() .map(|class| type_hierarchy_class_to_item(db, class)) .collect::>() @@ -85,7 +89,11 @@ pub fn type_hierarchy_subtypes( /// /// If a symbol could not be found at the given offset or its type could /// not be inferred, `None` is returned. -fn resolve_type_at(db: &dyn Db, file: File, offset: TextSize) -> Option> { +fn resolve_type_at<'db>( + db: &'db dyn Db, + file: PythonFile<'db>, + offset: TextSize, +) -> Option> { let module = parsed_module(db, file).load(db); let model = SemanticModel::new(db, file); @@ -93,14 +101,17 @@ fn resolve_type_at(db: &dyn Db, file: File, offset: TextSize) -> Option goto_target.inferred_type(&model) } -fn type_hierarchy_class_to_item(db: &dyn Db, class: TypeHierarchyClass) -> TypeHierarchyItem { +fn type_hierarchy_class_to_item<'db>( + db: &'db dyn Db, + class: TypeHierarchyClass<'db>, +) -> TypeHierarchyItem { let detail = ty_module_resolver::file_to_module(db, class.file) .map(|module| module.name(db).to_string()); TypeHierarchyItem { name: class.name, detail, - file: class.file, + file: class.file.file(db), full_range: class.full_range, selection_range: class.selection_range, } @@ -720,21 +731,33 @@ Public = _Internal impl CursorTest { fn prepare(&self) -> Option { - prepare_type_hierarchy(&self.db, self.cursor.file, self.cursor.offset) + prepare_type_hierarchy( + &self.db, + self.python_file(self.cursor.file), + self.cursor.offset, + ) } fn supertypes(&self) -> Vec { let Some(item) = self.prepare() else { return vec![]; }; - type_hierarchy_supertypes(&self.db, item.file, item.selection_range.start()) + type_hierarchy_supertypes( + &self.db, + self.python_file(item.file), + item.selection_range.start(), + ) } fn subtypes(&self) -> Vec { let Some(item) = self.prepare() else { return vec![]; }; - type_hierarchy_subtypes(&self.db, item.file, item.selection_range.start()) + type_hierarchy_subtypes( + &self.db, + self.python_file(item.file), + item.selection_range.start(), + ) } } } diff --git a/crates/ty_ide/src/workspace_symbols.rs b/crates/ty_ide/src/workspace_symbols.rs index 6d29743335..7482b10c47 100644 --- a/crates/ty_ide/src/workspace_symbols.rs +++ b/crates/ty_ide/src/workspace_symbols.rs @@ -1,5 +1,6 @@ use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file}; use rayon::prelude::*; +use ruff_db::PythonFile; use ruff_db::files::File; use ty_project::{Db, parallel::ParallelIteratorExt}; @@ -30,7 +31,7 @@ pub fn workspace_symbols(db: &dyn Db, query: &str) -> Vec { ); let _entered = symbols_for_file_span.entered(); - symbols_for_file(db, file) + symbols_for_file(db, PythonFile::new(db, file, db.python_version())) .search(&query) .map(|(_, symbol)| WorkspaceSymbolInfo { symbol: symbol.to_owned(), diff --git a/crates/ty_module_resolver/src/db.rs b/crates/ty_module_resolver/src/db.rs index 5e9cc24c6f..9e6032b698 100644 --- a/crates/ty_module_resolver/src/db.rs +++ b/crates/ty_module_resolver/src/db.rs @@ -66,6 +66,10 @@ pub(crate) mod tests { self } + pub(crate) fn python_version(&self) -> PythonVersion { + self.python_version + } + pub(crate) fn set_search_paths(&mut self, search_paths: SearchPaths) { search_paths.try_register_static_roots(self); self.search_paths = Arc::new(search_paths); @@ -106,10 +110,6 @@ pub(crate) mod tests { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> PythonVersion { - self.python_version - } } #[salsa::db] diff --git a/crates/ty_module_resolver/src/list.rs b/crates/ty_module_resolver/src/list.rs index 9b56c84cb7..efdf581717 100644 --- a/crates/ty_module_resolver/src/list.rs +++ b/crates/ty_module_resolver/src/list.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::collections::btree_map::{BTreeMap, Entry}; +use ruff_db::PythonFile; use ruff_db::files::directory_listing; use ruff_python_ast::PythonVersion; @@ -11,8 +12,8 @@ use crate::path::{ModulePath, SearchPath, SystemOrVendoredPathRef}; use crate::resolve::{ModuleResolveMode, ResolverContext, resolve_file_module, search_paths}; /// List all available modules, including all sub-modules, sorted in lexicographic order. -pub fn all_modules(db: &dyn Db) -> Vec> { - let mut modules = list_modules(db).to_vec(); +pub fn all_modules(db: &dyn Db, python_version: PythonVersion) -> Vec> { + let mut modules = list_modules(db, python_version).to_vec(); let mut stack = modules.clone(); while let Some(module) = stack.pop() { for &submodule in module.all_submodules(db) { @@ -25,11 +26,28 @@ pub fn all_modules(db: &dyn Db) -> Vec> { } /// List all available top-level modules. +pub fn list_modules(db: &dyn Db, python_version: PythonVersion) -> &[Module<'_>] { + list_modules_impl(db, PythonVersionIngredient::new(db, python_version)) +} + +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +struct PythonVersionIngredient<'db> { + #[returns(copy)] + python_version: PythonVersion, +} + #[salsa::tracked(returns(deref))] -pub fn list_modules(db: &dyn Db) -> Box<[Module<'_>]> { +fn list_modules_impl<'db>( + db: &'db dyn Db, + version: PythonVersionIngredient<'db>, +) -> Box<[Module<'db>]> { + let python_version = version.python_version(db); let mut modules: BTreeMap<&ModuleName, ListedModule<'_>> = BTreeMap::new(); for search_path in search_paths(db, ModuleResolveMode::Typing) { - for &new in list_modules_in(db, SearchPathIngredient::new(db, search_path.clone())) { + for &new in list_modules_in( + db, + SearchPathIngredient::new(db, search_path.clone(), python_version), + ) { match modules.entry(new.module(db).name(db)) { Entry::Vacant(entry) => { entry.insert(new); @@ -67,6 +85,8 @@ pub fn list_modules(db: &dyn Db) -> Box<[Module<'_>]> { struct SearchPathIngredient<'db> { #[returns(ref)] path: SearchPath, + #[returns(copy)] + python_version: PythonVersion, } /// List all available top-level modules in the given `SearchPath`. @@ -77,7 +97,7 @@ fn list_modules_in<'db>( ) -> Vec> { let path = search_path.path(db); tracing::debug!("Listing modules in search path '{}'", path); - let mut lister = Lister::new(db, path); + let mut lister = Lister::new(db, path, search_path.python_version(db)); match path.as_path() { SystemOrVendoredPathRef::System(system_search_path) => { let Ok(listing) = directory_listing(db, system_search_path) else { @@ -118,16 +138,22 @@ impl get_size2::GetSize for ListedModule<'_> {} struct Lister<'db> { db: &'db dyn Db, search_path: &'db SearchPath, + python_version: PythonVersion, modules: BTreeMap<&'db ModuleName, ListedModule<'db>>, } impl<'db> Lister<'db> { /// Create new state that can accumulate modules from a list /// of file paths. - fn new(db: &'db dyn Db, search_path: &'db SearchPath) -> Lister<'db> { + fn new( + db: &'db dyn Db, + search_path: &'db SearchPath, + python_version: PythonVersion, + ) -> Lister<'db> { Lister { db, search_path, + python_version, modules: BTreeMap::new(), } } @@ -182,7 +208,7 @@ impl<'db> Lister<'db> { Cow::Owned(module_name), ModuleKind::Package, self.search_path.clone(), - file, + PythonFile::new(self.db, file, self.python_version), ), ); return; @@ -223,7 +249,11 @@ impl<'db> Lister<'db> { if !self.search_path.is_standard_library() { self.add_module( &module_path, - Module::namespace_package(self.db, Cow::Owned(module_name)), + Module::namespace_package( + self.db, + Cow::Owned(module_name), + self.python_version, + ), ); } return; @@ -254,7 +284,7 @@ impl<'db> Lister<'db> { Cow::Owned(module_name), ModuleKind::Module, self.search_path.clone(), - file, + PythonFile::new(self.db, file, self.python_version), ), ); } @@ -317,20 +347,14 @@ impl<'db> Lister<'db> { /// Returns true if the given module name cannot be shadowable. fn is_non_shadowable(&self, name: &ModuleName) -> bool { - ModuleResolveMode::Typing.is_non_shadowable(self.python_version().minor, name.as_str()) - } - - /// Returns the Python version we want to perform module resolution - /// with. - fn python_version(&self) -> PythonVersion { - self.db.python_version() + ModuleResolveMode::Typing.is_non_shadowable(self.python_version.minor, name.as_str()) } /// Constructs a resolver context for use with some APIs that require it. fn context(&self) -> ResolverContext<'db> { ResolverContext { db: self.db, - python_version: self.python_version(), + python_version: self.python_version, // We don't currently support listing modules // in a "no stubs allowed" mode. mode: ModuleResolveMode::Typing, @@ -407,7 +431,9 @@ mod tests { use crate::strategy::FallibleStrategy; use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder}; - use super::list_modules; + fn list_modules(db: &TestDb) -> &[Module<'_>] { + super::list_modules(db, db.python_version()) + } struct ModuleDebugSnapshot<'db> { db: &'db dyn Db, @@ -423,11 +449,14 @@ mod tests { Module::File(module) => { // For snapshots, just normalize all paths to using // Unix slashes for simplicity. - let path_components = match module.file(self.db).path(self.db) { - FilePath::System(path) => path.components(), - FilePath::Vendored(path) => path.components(), - FilePath::SystemVirtual(path) => Utf8Path::new(path.as_str()).components(), - }; + let path_components = + match module.python_file(self.db).file(self.db).path(self.db) { + FilePath::System(path) => path.components(), + FilePath::Vendored(path) => path.components(), + FilePath::SystemVirtual(path) => { + Utf8Path::new(path.as_str()).components() + } + }; let nice_path = path_components // Avoid including a root component, since that // results in a platform dependent separator. @@ -457,18 +486,18 @@ mod tests { } } - fn sorted_list(db: &dyn Db) -> Vec> { + fn sorted_list(db: &TestDb) -> Vec> { let mut modules = list_modules(db).to_vec(); modules.sort_by(|m1, m2| m1.name(db).cmp(m2.name(db))); modules } - fn list_snapshot(db: &dyn Db) -> Vec> { + fn list_snapshot(db: &TestDb) -> Vec> { list_snapshot_filter(db, |_| true) } fn list_snapshot_filter<'db>( - db: &'db dyn Db, + db: &'db TestDb, predicate: impl Fn(&Module<'db>) -> bool, ) -> Vec> { sorted_list(db) diff --git a/crates/ty_module_resolver/src/module.rs b/crates/ty_module_resolver/src/module.rs index 23b6ac6066..3a01008236 100644 --- a/crates/ty_module_resolver/src/module.rs +++ b/crates/ty_module_resolver/src/module.rs @@ -2,9 +2,11 @@ use std::borrow::Cow; use std::fmt::Formatter; use std::str::FromStr; +use ruff_db::PythonFile; use ruff_db::files::{File, directory_listing, system_path_to_file, vendored_path_to_file}; use ruff_db::system::SystemPath; use ruff_db::vendored::VendoredPath; +use ruff_python_ast::PythonVersion; use salsa::Database; use salsa::plumbing::AsId; @@ -29,15 +31,19 @@ impl<'db> Module<'db> { name: Cow<'_, ModuleName>, kind: ModuleKind, search_path: SearchPath, - file: File, + file: PythonFile<'db>, ) -> Self { let known = KnownModule::try_from_search_path_and_name(&search_path, &name); Self::File(FileModule::new(db, name, kind, search_path, file, known)) } - pub(crate) fn namespace_package(db: &'db dyn Db, name: Cow<'_, ModuleName>) -> Self { - Self::Namespace(NamespacePackage::new(db, name)) + pub(crate) fn namespace_package( + db: &'db dyn Db, + name: Cow<'_, ModuleName>, + python_version: PythonVersion, + ) -> Self { + Self::Namespace(NamespacePackage::new(db, name, python_version)) } /// The absolute name of the module (e.g. `foo.bar`) @@ -53,11 +59,29 @@ impl<'db> Module<'db> { /// This is `None` for namespace packages. pub fn file(self, db: &'db dyn Database) -> Option { match self { - Module::File(module) => Some(module.file(db)), + Module::File(module) => Some(module.python_file(db).file(db)), Module::Namespace(_) => None, } } + /// The versioned file used to parse this module. + /// + /// This is `None` for namespace packages. + pub fn python_file(self, db: &'db dyn Database) -> Option> { + match self { + Module::File(module) => Some(module.python_file(db)), + Module::Namespace(_) => None, + } + } + + /// The Python version used to resolve this module. + pub fn python_version(self, db: &'db dyn Database) -> PythonVersion { + match self { + Module::File(module) => module.python_file(db).python_version(db), + Module::Namespace(module) => module.python_version(db), + } + } + /// Is this a module that we special-case somehow? If so, which one? pub fn known(self, db: &'db dyn Database) -> Option { match self { @@ -163,7 +187,8 @@ fn all_submodule_names_for_package<'db>( return None; } - let path = SystemOrVendoredPathRef::try_from_file(db, module.file(db))?; + let python_file = module.python_file(db); + let path = SystemOrVendoredPathRef::try_from_file(db, python_file.file(db))?; debug_assert!( matches!(path.file_name(), Some("__init__.py" | "__init__.pyi")), "expected package file `{:?}` to be `__init__.py` or `__init__.pyi`", @@ -208,7 +233,7 @@ fn all_submodule_names_for_package<'db>( Cow::Owned(name), kind, module.search_path(db).clone(), - file, + PythonFile::new(db, file, python_file.python_version(db)), )) }) .collect() @@ -245,14 +270,14 @@ fn all_submodule_names_for_package<'db>( Cow::Owned(name), kind, module.search_path(db).clone(), - file, + PythonFile::new(db, file, python_file.python_version(db)), )) }) .collect(), }) } -/// A module that resolves to a file (`lib.py` or `package/__init__.py`) +/// A module that resolves to a file (`lib.py` or `package/__init__.py`). #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct FileModule<'db> { #[returns(ref)] @@ -262,7 +287,7 @@ pub struct FileModule<'db> { #[returns(ref)] pub(super) search_path: SearchPath, #[returns(copy)] - pub(super) file: File, + pub(super) python_file: PythonFile<'db>, #[returns(copy)] pub(super) known: Option, } @@ -275,6 +300,8 @@ pub struct FileModule<'db> { pub struct NamespacePackage<'db> { #[returns(ref)] pub(super) name: ModuleName, + #[returns(copy)] + pub(super) python_version: PythonVersion, } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] diff --git a/crates/ty_module_resolver/src/module_name.rs b/crates/ty_module_resolver/src/module_name.rs index 1db32e467c..95975e1b06 100644 --- a/crates/ty_module_resolver/src/module_name.rs +++ b/crates/ty_module_resolver/src/module_name.rs @@ -4,7 +4,7 @@ use std::ops::Deref; use compact_str::{CompactString, ToCompactString}; -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_python_ast as ast; use ruff_python_stdlib::identifiers::is_identifier; @@ -305,13 +305,13 @@ impl ModuleName { /// Extracts a module name from the AST of a `from import ...` /// statement. /// - /// `importing_file` must be the [`File`] that contains the import + /// `importing_file` must be the [`PythonFile`] that contains the import /// statement. /// /// This handles relative import statements. pub fn from_import_statement<'db>( db: &'db dyn Db, - importing_file: File, + importing_file: PythonFile<'db>, node: &'db ast::StmtImportFrom, ) -> Result { let ast::StmtImportFrom { @@ -326,9 +326,9 @@ impl ModuleName { } /// Computes the absolute module name from the LHS components of `from LHS import RHS` - pub fn from_identifier_parts( - db: &dyn Db, - importing_file: File, + pub fn from_identifier_parts<'db>( + db: &'db dyn Db, + importing_file: PythonFile<'db>, module: Option<&str>, level: u32, ) -> Result { @@ -344,9 +344,9 @@ impl ModuleName { /// Computes the absolute module name for the package this file belongs to. /// /// i.e. this resolves `.` - pub fn package_for_file( - db: &dyn Db, - importing_file: File, + pub fn package_for_file<'db>( + db: &'db dyn Db, + importing_file: PythonFile<'db>, ) -> Result { Self::from_identifier_parts(db, importing_file, None, 1) } @@ -478,9 +478,9 @@ impl std::fmt::Display for ModuleName { /// - `tail` is the relative module name stripped of all leading dots: /// - `from .foo import bar` => `tail == "foo"` /// - `from ..foo.bar import baz` => `tail == "foo.bar"` -fn relative_module_name( - db: &dyn Db, - importing_file: File, +fn relative_module_name<'db>( + db: &'db dyn Db, + importing_file: PythonFile<'db>, tail: Option<&str>, level: NonZeroU32, ) -> Result { diff --git a/crates/ty_module_resolver/src/resolve.rs b/crates/ty_module_resolver/src/resolve.rs index 9403038e58..5fd7b5f313 100644 --- a/crates/ty_module_resolver/src/resolve.rs +++ b/crates/ty_module_resolver/src/resolve.rs @@ -37,6 +37,7 @@ use std::iter::FusedIterator; use rustc_hash::{FxBuildHasher, FxHashSet}; +use ruff_db::PythonFile; use ruff_db::files::{File, FilePath, FileRootKind, directory_listing, system_path_to_file}; use ruff_db::source::source_text; use ruff_db::system::{System, SystemPath, SystemPathBuf}; @@ -57,13 +58,18 @@ use crate::{SearchPathSettings, SearchPathSettingsError}; /// Resolves a module name to a module. pub fn resolve_module<'db>( db: &'db dyn Db, - importing_file: File, + importing_file: PythonFile<'db>, module_name: &ModuleName, ) -> Option> { - let interned_name = ModuleNameIngredient::new(db, module_name, ModuleResolveMode::Typing); + let interned_name = ModuleNameIngredient::new( + db, + module_name, + ModuleResolveMode::Typing, + importing_file.python_version(db), + ); resolve_module_query(db, interned_name) - .or_else(|| desperately_resolve_module(db, importing_file, interned_name)) + .or_else(|| desperately_resolve_module(db, importing_file.file(db), interned_name)) } /// Resolves a module name to a module, without desperate resolution available. @@ -72,9 +78,11 @@ pub fn resolve_module<'db>( /// we don't have a well-defined importing file. pub fn resolve_module_confident<'db>( db: &'db dyn Db, + python_version: PythonVersion, module_name: &ModuleName, ) -> Option> { - let interned_name = ModuleNameIngredient::new(db, module_name, ModuleResolveMode::Typing); + let interned_name = + ModuleNameIngredient::new(db, module_name, ModuleResolveMode::Typing, python_version); resolve_module_query(db, interned_name) } @@ -82,13 +90,18 @@ pub fn resolve_module_confident<'db>( /// Resolves a module name to a module (stubs not allowed). pub fn resolve_real_module<'db>( db: &'db dyn Db, - importing_file: File, + importing_file: PythonFile<'db>, module_name: &ModuleName, ) -> Option> { - let interned_name = ModuleNameIngredient::new(db, module_name, ModuleResolveMode::Runtime); + let interned_name = ModuleNameIngredient::new( + db, + module_name, + ModuleResolveMode::Runtime, + importing_file.python_version(db), + ); resolve_module_query(db, interned_name) - .or_else(|| desperately_resolve_module(db, importing_file, interned_name)) + .or_else(|| desperately_resolve_module(db, importing_file.file(db), interned_name)) } /// Resolves a module name to a module, without desperate resolution available (stubs not allowed). @@ -97,9 +110,11 @@ pub fn resolve_real_module<'db>( /// we don't have a well-defined importing file. pub fn resolve_real_module_confident<'db>( db: &'db dyn Db, + python_version: PythonVersion, module_name: &ModuleName, ) -> Option> { - let interned_name = ModuleNameIngredient::new(db, module_name, ModuleResolveMode::Runtime); + let interned_name = + ModuleNameIngredient::new(db, module_name, ModuleResolveMode::Runtime, python_version); resolve_module_query(db, interned_name) } @@ -117,17 +132,18 @@ pub fn resolve_real_module_confident<'db>( /// are involved in an import cycle with `builtins`. pub fn resolve_real_shadowable_module<'db>( db: &'db dyn Db, - importing_file: File, + importing_file: PythonFile<'db>, module_name: &ModuleName, ) -> Option> { let interned_name = ModuleNameIngredient::new( db, module_name, ModuleResolveMode::RuntimeSomeShadowingAllowed, + importing_file.python_version(db), ); resolve_module_query(db, interned_name) - .or_else(|| desperately_resolve_module(db, importing_file, interned_name)) + .or_else(|| desperately_resolve_module(db, importing_file.file(db), interned_name)) } /// Selects typing or runtime module-resolution semantics. @@ -212,9 +228,10 @@ fn resolve_module_query<'db>( ) -> Option> { let name = module_name.name(db); let mode = module_name.mode(db); + let python_version = module_name.python_version(db); let _span = tracing::trace_span!("resolve_module", %name).entered(); - let Some(resolved) = resolve_name(db, name, mode) else { + let Some(resolved) = resolve_name(db, name, mode, python_version) else { tracing::debug!("Module `{name}` not found in search paths"); return None; }; @@ -222,7 +239,7 @@ fn resolve_module_query<'db>( resolved .into_iter() .next() - .map(|candidate| candidate.into_module(db, name)) + .map(|candidate| candidate.into_module(db, name, python_version)) } /// Like `resolve_module_query` but for cases where it failed to resolve the module @@ -245,9 +262,11 @@ fn desperately_resolve_module<'db>( ) -> Option> { let name = module_name.name(db); let mode = module_name.mode(db); + let python_version = module_name.python_version(db); let _span = tracing::trace_span!("desperately_resolve_module", %name).entered(); - let Some(resolved) = desperately_resolve_name(db, importing_file, name, mode) else { + let Some(resolved) = desperately_resolve_name(db, importing_file, name, mode, python_version) + else { let mode = match mode { ModuleResolveMode::Typing => "typing mode", ModuleResolveMode::Runtime => "runtime mode", @@ -262,14 +281,18 @@ fn desperately_resolve_module<'db>( resolved .into_iter() .next() - .map(|candidate| candidate.into_module(db, name)) + .map(|candidate| candidate.into_module(db, name, python_version)) } /// Resolves the module for the given path. /// /// Returns `None` if the path is not a module locatable via any of the known search paths. #[allow(unused)] -pub(crate) fn path_to_module<'db>(db: &'db dyn Db, path: &FilePath) -> Option> { +pub(crate) fn path_to_module<'db>( + db: &'db dyn Db, + path: &FilePath, + python_version: PythonVersion, +) -> Option> { // It's not entirely clear on first sight why this method calls `file_to_module` instead of // it being the other way round, considering that the first thing that `file_to_module` does // is to retrieve the file's path. @@ -279,7 +302,7 @@ pub(crate) fn path_to_module<'db>(db: &'db dyn Db, path: &FilePath) -> Option(db: &'db dyn Db, path: &FilePath) -> Option Option> { - let _span = tracing::trace_span!("file_to_module", ?file).entered(); +pub fn file_to_module<'db>(db: &'db dyn Db, file: PythonFile<'db>) -> Option> { + let source_file = file.file(db); + let _span = tracing::trace_span!("file_to_module", file=?source_file).entered(); - let path = SystemOrVendoredPathRef::try_from_file(db, file)?; + let path = SystemOrVendoredPathRef::try_from_file(db, source_file)?; file_to_module_impl(db, file, path, search_paths(db, ModuleResolveMode::Typing)).or_else(|| { file_to_module_impl( db, file, path, - relative_desperate_search_paths(db, file).iter(), + relative_desperate_search_paths(db, source_file).iter(), ) }) } fn file_to_module_impl<'db, 'a>( db: &'db dyn Db, - file: File, + file: PythonFile<'db>, path: SystemOrVendoredPathRef<'a>, mut search_paths: impl Iterator, ) -> Option> { @@ -327,10 +351,11 @@ fn file_to_module_impl<'db, 'a>( let module = resolve_module(db, file, &module_name)?; let module_file = module.file(db)?; - let file_path = file.path(db); + let source_file = file.file(db); + let file_path = source_file.path(db); if file_path == module_file.path(db) { return Some(module); - } else if file.source_type(db) == PySourceType::Python + } else if source_file.source_type(db) == PySourceType::Python && module_file.source_type(db) == PySourceType::Stub { // If a .py and .pyi are both defined, the .pyi will be the one returned by `resolve_module().file`, @@ -1045,7 +1070,8 @@ impl<'db> Iterator for SearchPathIterator<'db> { impl FusedIterator for SearchPathIterator<'_> {} -/// A thin wrapper around `ModuleName` to make it a Salsa ingredient. +/// A thin wrapper around a module name, resolution mode, and Python version to make them a Salsa +/// ingredient. /// /// This is needed because Salsa requires that all query arguments are salsa ingredients. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] @@ -1054,12 +1080,19 @@ struct ModuleNameIngredient<'db> { pub(super) name: ModuleName, #[returns(copy)] pub(super) mode: ModuleResolveMode, + #[returns(copy)] + pub(super) python_version: PythonVersion, } /// Given a module name and a list of search paths in which to lookup modules, /// attempt to resolve the module name -fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Option { - let resolver = NameResolver::new(db, name, mode); +fn resolve_name( + db: &dyn Db, + name: &ModuleName, + mode: ModuleResolveMode, + python_version: PythonVersion, +) -> Option { + let resolver = NameResolver::new(db, name, mode, python_version); match mode { ModuleResolveMode::Typing => resolver.resolve_typing(stub_package_index(db)), @@ -1078,9 +1111,10 @@ fn desperately_resolve_name( importing_file: File, name: &ModuleName, mode: ModuleResolveMode, + python_version: PythonVersion, ) -> Option { let search_paths = absolute_desperate_search_paths(db, importing_file).unwrap_or_default(); - let resolver = NameResolver::new(db, name, mode); + let resolver = NameResolver::new(db, name, mode, python_version); match mode { ModuleResolveMode::Typing => resolver.resolve_desperate_typing(search_paths), @@ -1164,11 +1198,16 @@ impl ModuleResolutionCandidate { } // This is the module we were actually interested in resolving, complete the resolution - fn into_module<'db>(self, db: &'db dyn Db, name: &ModuleName) -> Module<'db> { + fn into_module<'db>( + self, + db: &'db dyn Db, + name: &ModuleName, + python_version: PythonVersion, + ) -> Module<'db> { match self.module { ResolvedModule::NamespacePackage => { tracing::trace!("Resolve namespace package `{name}`"); - Module::namespace_package(db, Cow::Borrowed(name)) + Module::namespace_package(db, Cow::Borrowed(name), python_version) } ResolvedModule::LegacyNamespacePackage(file) => { // legacy namespace packages behave like regular packages @@ -1182,7 +1221,7 @@ impl ModuleResolutionCandidate { Cow::Borrowed(name), ModuleKind::Package, self.path.into_search_path(), - file, + PythonFile::new(db, file, python_version), ) } ResolvedModule::RegularPackage(file) => { @@ -1195,7 +1234,7 @@ impl ModuleResolutionCandidate { Cow::Borrowed(name), ModuleKind::Package, self.path.into_search_path(), - file, + PythonFile::new(db, file, python_version), ) } ResolvedModule::Module(file) => { @@ -1205,7 +1244,7 @@ impl ModuleResolutionCandidate { Cow::Borrowed(name), ModuleKind::Module, self.path.into_search_path(), - file, + PythonFile::new(db, file, python_version), ) } } @@ -1245,8 +1284,12 @@ struct NameResolver<'db, 'name> { } impl<'db, 'name> NameResolver<'db, 'name> { - fn new(db: &'db dyn Db, name: &'name ModuleName, mode: ModuleResolveMode) -> Self { - let python_version = db.python_version(); + fn new( + db: &'db dyn Db, + name: &'name ModuleName, + mode: ModuleResolveMode, + python_version: PythonVersion, + ) -> Self { Self { context: ResolverContext::new(db, python_version, mode), name, @@ -1693,7 +1736,10 @@ fn is_legacy_namespace_package( // // The downside is if you write slightly different syntax we will fail to detect the idiom, // but hey, this is better than nothing! - let parsed = ruff_db::parsed::parsed_module(context.db, init); + let parsed = ruff_db::parsed::parsed_module( + context.db, + ruff_db::PythonFile::new(context.db, init, context.python_version), + ); let mut visitor = LegacyNamespacePackageVisitor::default(); visitor.visit_body(parsed.load(context.db).suite()); @@ -1967,6 +2013,24 @@ mod tests { use super::*; + fn resolve_module_confident<'db>( + db: &'db TestDb, + module_name: &ModuleName, + ) -> Option> { + super::resolve_module_confident(db, db.python_version(), module_name) + } + + fn resolve_real_module_confident<'db>( + db: &'db TestDb, + module_name: &ModuleName, + ) -> Option> { + super::resolve_real_module_confident(db, db.python_version(), module_name) + } + + fn path_to_module<'db>(db: &'db TestDb, path: &FilePath) -> Option> { + super::path_to_module(db, path, db.python_version()) + } + #[test] fn first_party_module() { let TestCase { db, src, .. } = TestCaseBuilder::new() @@ -2038,6 +2102,7 @@ mod tests { ]) .build(); let importing_file = system_path_to_file(&db, src.join("nested/main.py")).unwrap(); + let importing_file = PythonFile::new(&db, importing_file, db.python_version()); let foo = resolve_module(&db, importing_file, &ModuleName::new_static("foo").unwrap()).unwrap(); @@ -2237,6 +2302,62 @@ mod tests { .collect() } + #[test] + fn resolve_module_uses_importing_file_python_version() { + const TYPESHED: MockedTypeshed = MockedTypeshed { + stdlib_files: &[("_sha256.pyi", ""), ("py312_only.pyi", "")], + versions: "_sha256: 3.11-\npy312_only: 3.12-", + }; + + let TestCase { + db, src, stdlib, .. + } = TestCaseBuilder::new() + .with_src_files(&[ + ("main.py", ""), + ("_sha256.py", ""), + ("namespace/module.py", ""), + ]) + .with_mocked_typeshed(TYPESHED) + .with_python_version(PythonVersion::PY311) + .build(); + let importing_file = system_path_to_file(&db, src.join("main.py")).unwrap(); + let py311 = PythonFile::new(&db, importing_file, PythonVersion::PY311); + let py312 = PythonFile::new(&db, importing_file, PythonVersion::PY312); + + let sha256 = ModuleName::new_static("_sha256").unwrap(); + let py311_module = resolve_module(&db, py311, &sha256).unwrap(); + let py312_module = resolve_module(&db, py312, &sha256).unwrap(); + assert_eq!( + py311_module.file(&db).unwrap().path(&db), + &stdlib.join("_sha256.pyi") + ); + assert_eq!( + py312_module.file(&db).unwrap().path(&db), + &src.join("_sha256.py") + ); + assert_eq!(py311_module.python_version(&db), PythonVersion::PY311); + assert_eq!(py312_module.python_version(&db), PythonVersion::PY312); + + let namespace = ModuleName::new_static("namespace").unwrap(); + let py311_namespace = resolve_module(&db, py311, &namespace).unwrap(); + let py312_namespace = resolve_module(&db, py312, &namespace).unwrap(); + assert!(matches!(py311_namespace, Module::Namespace(_))); + assert!(matches!(py312_namespace, Module::Namespace(_))); + assert_eq!(py311_namespace.python_version(&db), PythonVersion::PY311); + assert_eq!(py312_namespace.python_version(&db), PythonVersion::PY312); + assert_ne!(py311_namespace, py312_namespace); + + let py312_only = ModuleName::new_static("py312_only").unwrap(); + assert!(resolve_module(&db, py311, &py312_only).is_none()); + assert_eq!( + resolve_module(&db, py312, &py312_only) + .and_then(|module| module.file(&db)) + .unwrap() + .path(&db), + &stdlib.join("py312_only.pyi") + ); + } + #[test] fn stdlib_resolution_respects_versions_file_py38_existing_modules() { const VERSIONS: &str = "\ @@ -2769,7 +2890,12 @@ mod tests { assert_function_query_was_not_run( &db, resolve_module_query, - ModuleNameIngredient::new(&db, functools_module_name, ModuleResolveMode::Typing), + ModuleNameIngredient::new( + &db, + functools_module_name, + ModuleResolveMode::Typing, + db.python_version(), + ), &events, ); assert_eq!(&functools_search_path, &stdlib); @@ -3304,7 +3430,11 @@ not_a_directory db.set_search_paths(search_paths); let foo_module_file = File::new(&db, FilePath::from(installed_foo_module)); - let module = file_to_module(&db, foo_module_file).unwrap(); + let module = file_to_module( + &db, + PythonFile::new(&db, foo_module_file, db.python_version()), + ) + .unwrap(); assert_eq!(module.search_path(&db).unwrap(), &site_packages); } } diff --git a/crates/ty_project/src/db.rs b/crates/ty_project/src/db.rs index 0919c85a67..7f0496ce1c 100644 --- a/crates/ty_project/src/db.rs +++ b/crates/ty_project/src/db.rs @@ -13,6 +13,7 @@ use ruff_db::diagnostic::Diagnostic; use ruff_db::files::{File, Files}; use ruff_db::system::System; use ruff_db::vendored::VendoredFileSystem; +use ruff_python_ast::PythonVersion; use salsa::{Database, Event, Setter}; use ty_module_resolver::SearchPaths; use ty_python_core::program::{ @@ -25,6 +26,9 @@ mod changes; #[salsa::db] pub trait Db: SemanticDb { + /// Returns the Python version for files in the primary environment. + fn python_version(&self) -> PythonVersion; + fn project(&self) -> Project; fn dyn_clone(&self) -> Box; @@ -599,10 +603,6 @@ 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] @@ -610,6 +610,10 @@ impl salsa::Database for ProjectDatabase {} #[salsa::db] impl Db for ProjectDatabase { + fn python_version(&self) -> PythonVersion { + Program::get(self).python_version(self) + } + fn project(&self) -> Project { self.project.unwrap() } @@ -621,7 +625,7 @@ impl Db for ProjectDatabase { #[cfg(feature = "format")] mod format { - use crate::ProjectDatabase; + use crate::{Db as _, ProjectDatabase}; use ruff_db::files::File; use ruff_python_formatter::{Db as FormatDb, PyFormatOptions}; @@ -629,7 +633,7 @@ mod format { impl FormatDb for ProjectDatabase { fn format_options(&self, file: File) -> PyFormatOptions { let source_ty = file.source_type(self); - PyFormatOptions::from_source_type(source_ty) + PyFormatOptions::from_source_type(source_ty).with_target_version(self.python_version()) } } } @@ -649,7 +653,7 @@ pub(crate) mod testing { use ty_python_core::platform::PythonPlatform; use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; - use ty_python_semantic::{AnalysisSettings, PythonVersionWithSource}; + use ty_python_semantic::{AnalysisSettings, ProgramEnvironment, PythonVersionWithSource}; use crate::db::Db; use crate::{Project, ProjectMetadata}; @@ -728,6 +732,14 @@ pub(crate) mod testing { } impl TestDb { + pub fn python_version(&self) -> PythonVersion { + Program::get(self).python_version(self) + } + + pub fn program_environment(&self) -> ProgramEnvironment<'_> { + ProgramEnvironment::from_program(self.python_version()) + } + /// Takes the salsa events. pub fn take_salsa_events(&mut self) -> Vec { let mut events = self.events.lock().unwrap(); @@ -759,10 +771,6 @@ pub(crate) mod testing { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> ruff_python_ast::PythonVersion { - Program::get(self).python_version(self) - } } #[salsa::db] @@ -813,6 +821,10 @@ pub(crate) mod testing { #[salsa::db] impl Db for TestDb { + fn python_version(&self) -> PythonVersion { + Program::get(self).python_version(self) + } + fn project(&self) -> Project { self.project.unwrap() } @@ -833,7 +845,7 @@ mod tests { use ruff_db::system::{SystemPathBuf, TestSystem}; use ty_module_resolver::list_modules; - use crate::{ProjectDatabase, ProjectMetadata}; + use crate::{Db as _, ProjectDatabase, ProjectMetadata}; #[test] fn frozen_inputs_support_a_one_shot_check() -> anyhow::Result<()> { @@ -877,7 +889,7 @@ mod tests { let metadata = ProjectMetadata::discover(&project, &system)?; let db = ProjectDatabase::fallible(metadata, system)?; - let modules = list_modules(&db); + let modules = list_modules(&db, db.python_version()); assert!( modules .iter() diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index 51cab953df..7772fe0985 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -14,6 +14,7 @@ use files::{Index, Indexed, IndexedFiles}; use metadata::settings::Settings; pub use metadata::{ProjectMetadata, ProjectMetadataError}; use rayon::prelude::*; +use ruff_db::PythonFile; use ruff_db::diagnostic::{ Diagnostic, DiagnosticId, Severity, SubDiagnostic, SubDiagnosticSeverity, }; @@ -27,6 +28,7 @@ use std::collections::{BTreeSet, hash_set}; use std::iter::FusedIterator; use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::sync::Arc; +pub use ty_python_semantic::Db as SemanticDb; use ty_python_semantic::lint::RuleSelection; mod db; @@ -392,8 +394,9 @@ impl Project { let check_file_span = tracing::debug_span!(parent: &project_span, "check_file", ?file); let _entered = check_file_span.entered(); + let python_file = PythonFile::new(db, file, db.python_version()); - match check_file_impl(db, file) { + match check_file_impl(db, python_file) { Ok(diagnostics) => { reporter.report_checked_file(db, file, diagnostics); @@ -402,7 +405,7 @@ impl Project { if !open_files.contains(&file) { // The module has already been parsed by `check_file_impl`. // We only retrieve it here so that we can call `clear` on it. - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, python_file); // Drop the AST now that we are done checking this file. It is not currently open, // so it is unlikely to be accessed again soon. If any queries need to access the AST @@ -657,7 +660,7 @@ fn check_file(db: &dyn Db, file: File) -> Vec { return Vec::new(); } - check_file_impl(db, file) + check_file_impl(db, PythonFile::new(db, file, db.python_version())) .map(<[Diagnostic]>::to_vec) .unwrap_or_else(|diagnostic| vec![diagnostic.clone()]) } @@ -739,10 +742,16 @@ pub enum ProjectReloadResult { } #[salsa::tracked(returns(as_deref), heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Result, Diagnostic> { +pub(crate) fn check_file_impl( + db: &dyn Db, + file: PythonFile<'_>, +) -> Result, Diagnostic> { + let source_file = file.file(db); { let db = AssertUnwindSafe(db); - match catch(&**db, file, || ty_python_semantic::check_file(*db, file)) { + match catch(&**db, source_file, || { + ty_python_semantic::check_file(*db, file) + }) { Ok(result) => result, Err(diagnostic) => Ok(Box::new([diagnostic])), } @@ -885,6 +894,7 @@ mod tests { use crate::db::Db as _; use crate::db::testing::TestDb; use crate::{IncludeResult, ProjectMetadata}; + use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::source::source_text; use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf}; @@ -907,7 +917,7 @@ mod tests { assert_eq!(source_text(&db, file).as_str(), ""); assert_eq!( - check_file_impl(&db, file) + check_file_impl(&db, PythonFile::new(&db, file, db.python_version())) .as_ref() .unwrap_err() .headline_message() @@ -916,7 +926,12 @@ mod tests { ); let events = db.take_salsa_events(); - assert_function_query_was_not_run(&db, check_types, file, &events); + assert_function_query_was_not_run( + &db, + check_types, + PythonFile::new(&db, file, db.python_version()), + &events, + ); // The user now creates a new file with an empty text. The source text // content returned by `source_text` remains unchanged, but the diagnostics should get updated. @@ -924,7 +939,7 @@ mod tests { assert_eq!(source_text(&db, file).as_str(), ""); assert_eq!( - check_file_impl(&db, file) + check_file_impl(&db, PythonFile::new(&db, file, db.python_version())) .as_ref() .unwrap() .iter() diff --git a/crates/ty_python_core/src/ast_ids.rs b/crates/ty_python_core/src/ast_ids.rs index cfa3d25460..955994e6f2 100644 --- a/crates/ty_python_core/src/ast_ids.rs +++ b/crates/ty_python_core/src/ast_ids.rs @@ -1,6 +1,6 @@ use rustc_hash::FxHashMap; -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_index::{IndexVec, newtype_index}; use ruff_python_ast as ast; use ruff_python_ast::ExprRef; @@ -55,7 +55,7 @@ impl AstIds { } } -fn ast_ids(db: &dyn Db, file: File) -> &AstIds { +fn ast_ids<'db>(db: &'db dyn Db, file: PythonFile<'db>) -> &'db AstIds { semantic_index(db, file).ast_ids() } @@ -66,46 +66,46 @@ pub struct ScopedUseId; pub trait HasScopedUseId { /// Returns the ID that uniquely identifies the use in its scope. - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId; + fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId; } impl HasScopedUseId for ast::Identifier { - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId { let ast_ids = ast_ids(db, file); ast_ids.use_id(self) } } impl HasScopedUseId for ast::ExprName { - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId { let expression_ref = ExprRef::from(self); expression_ref.scoped_use_id(db, file) } } impl HasScopedUseId for ast::ExprAttribute { - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId { let expression_ref = ExprRef::from(self); expression_ref.scoped_use_id(db, file) } } impl HasScopedUseId for ast::ExprSubscript { - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId { let expression_ref = ExprRef::from(self); expression_ref.scoped_use_id(db, file) } } impl HasScopedUseId for ast::Keyword { - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId { let ast_ids = ast_ids(db, file); ast_ids.use_id(self) } } impl HasScopedUseId for ast::ExprRef<'_> { - fn scoped_use_id(&self, db: &dyn Db, file: File) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId { let ast_ids = ast_ids(db, file); ast_ids.use_id(*self) } diff --git a/crates/ty_python_core/src/ast_node_ref.rs b/crates/ty_python_core/src/ast_node_ref.rs index 81161a5179..d730d15b38 100644 --- a/crates/ty_python_core/src/ast_node_ref.rs +++ b/crates/ty_python_core/src/ast_node_ref.rs @@ -4,6 +4,8 @@ use std::marker::PhantomData; #[cfg(debug_assertions)] use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; +#[cfg(debug_assertions)] +use ruff_python_ast::PythonVersion; use ruff_python_ast::{AnyNodeRef, NodeIndex}; use ruff_python_ast::{AnyRootNodeRef, HasNodeIndex}; use ruff_text_size::Ranged; @@ -47,6 +49,8 @@ pub struct AstNodeRef { // AST. #[cfg(debug_assertions)] file: File, + #[cfg(debug_assertions)] + python_version: PythonVersion, _node: PhantomData, } @@ -66,7 +70,7 @@ where /// Creates a new `AstNodeRef` that references `node`. /// /// This method may panic or produce unspecified results if the provided module is from a - /// different file or Salsa revision than the module to which the node belongs. + /// different file, Python version, or Salsa revision than the module to which the node belongs. pub(super) fn new(module_ref: &ParsedModuleRef, node: &T) -> Self { let index = node.node_index().load(); debug_assert_eq!(module_ref.get_by_index(index).try_into().ok(), Some(node)); @@ -76,6 +80,8 @@ where #[cfg(debug_assertions)] file: module_ref.module().file(), #[cfg(debug_assertions)] + python_version: module_ref.module().python_version(), + #[cfg(debug_assertions)] kind: AnyNodeRef::from(node).kind(), #[cfg(debug_assertions)] range: node.range(), @@ -86,12 +92,19 @@ where /// Returns a reference to the wrapped node. /// /// This method may panic or produce unspecified results if the provided module is from a - /// different file or Salsa revision than the module to which the node belongs. + /// different file, Python version, or Salsa revision than the module to which the node belongs. #[track_caller] pub fn node<'ast>(&self, module_ref: &'ast ParsedModuleRef) -> &'ast T { #[cfg(debug_assertions)] - assert_eq!(module_ref.module().file(), self.file); - // The user guarantees that the module is from the same file and Salsa + assert_eq!( + ( + module_ref.module().file(), + module_ref.module().python_version() + ), + (self.file, self.python_version), + "an `AstNodeRef` cannot be used with a module parsed for a different file or Python version" + ); + // The user guarantees that the module is from the same file, Python version, and Salsa // revision, so the file contents cannot have changed. module_ref .get_by_index(self.index) @@ -124,3 +137,35 @@ where } } } + +#[cfg(all(test, debug_assertions))] +mod tests { + use ruff_db::PythonFile; + use ruff_db::files::system_path_to_file; + use ruff_db::parsed::parsed_module; + use ruff_python_ast::PythonVersion; + + use crate::ast_node_ref::AstNodeRef; + use crate::db::tests::TestDbBuilder; + + #[test] + #[should_panic( + expected = "an `AstNodeRef` cannot be used with a module parsed for a different file or Python version" + )] + fn rejects_module_parsed_for_different_python_version() { + let db = TestDbBuilder::new() + .with_file("test.py", "x = 1") + .build() + .unwrap(); + let file = system_path_to_file(&db, "test.py").unwrap(); + + let parsed_py311 = + parsed_module(&db, PythonFile::new(&db, file, PythonVersion::PY311)).load(&db); + let parsed_py312 = + parsed_module(&db, PythonFile::new(&db, file, PythonVersion::PY312)).load(&db); + let assignment = parsed_py311.syntax().body[0].as_assign_stmt().unwrap(); + + let node = AstNodeRef::new(&parsed_py311, assignment); + node.node(&parsed_py312); + } +} diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index 7bd8684fc7..014d12e47e 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -6,8 +6,9 @@ use itertools::Itertools; use ruff_python_ast::helpers::{Truthiness, any_over_expr, is_dotted_name}; use rustc_hash::{FxHashMap, FxHashSet}; -use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; + +use ruff_db::PythonFile; use ruff_db::source::{SourceText, source_text}; use ruff_index::IndexVec; use ruff_python_ast::name::Name; @@ -48,7 +49,6 @@ use crate::predicate::{ PatternPredicateKind, Predicate, PredicateNode, PredicateOrLiteral, ScopedPredicateId, SequencePatternPredicateKind, StarImportPlaceholderPredicate, SubjectElementPatternPredicate, }; -use crate::program::Program; use crate::re_exports::exported_names; use crate::reachability_constraints::{ ReachabilityConstraintsBuilder, ScopedReachabilityConstraintId, @@ -229,7 +229,7 @@ impl ConditionFlowSnapshot { pub(super) struct SemanticIndexBuilder<'db, 'ast> { // Builder state db: &'db dyn Db, - file: File, + file: PythonFile<'db>, source_type: PySourceType, module: &'ast ParsedModuleRef, scope_stack: Vec>, @@ -300,11 +300,15 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> { } impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { - pub(super) fn new(db: &'db dyn Db, file: File, module_ref: &'ast ParsedModuleRef) -> Self { + pub(super) fn new( + db: &'db dyn Db, + file: PythonFile<'db>, + module_ref: &'ast ParsedModuleRef, + ) -> Self { let mut builder = Self { db, file, - source_type: file.source_type(db), + source_type: file.file(db).source_type(db), module: module_ref, scope_stack: Vec::new(), current_assignments: Vec::new(), @@ -340,7 +344,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { enclosing_snapshots: FxHashMap::default(), - python_version: Program::get(db).python_version(db), + python_version: file.python_version(db), source_text: OnceCell::new(), semantic_checker: SemanticSyntaxChecker::default(), in_try: false, @@ -2937,7 +2941,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { fn source_text(&self) -> &SourceText { self.source_text - .get_or_init(|| source_text(self.db, self.file)) + .get_or_init(|| source_text(self.db, self.file.file(self.db))) } fn visit_stmt_impl(&mut self, stmt: &'ast ast::Stmt) { @@ -3161,7 +3165,8 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { // that `x` can be freely overwritten, and that we don't assume that an import // in one function is visible in another function. let mut is_self_import = false; - if self.file.is_package(self.db) + let source_file = self.file.file(self.db); + if source_file.is_package(self.db) && let Ok(module_name) = ModuleName::from_identifier_parts( self.db, self.file, @@ -3251,10 +3256,9 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { continue; }; - let Some(referenced_module) = module.file(self.db) else { + let Some(referenced_parse_file) = module.python_file(self.db) else { continue; }; - // In order to understand the reachability of definitions created by a `*` import, // we need to know the reachability of the global-scope definitions in the // `referenced_module` the symbols imported from. Much like predicates for `if` @@ -3269,14 +3273,14 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { // ``` // // For more details, see the doc-comment on `StarImportPlaceholderPredicate`. - for export in exported_names(self.db, referenced_module) { + for export in exported_names(self.db, referenced_parse_file) { let symbol_id = self.add_symbol(export.clone()); let node_ref = StarImportDefinitionNodeRef { node, symbol_id }; let star_import = StarImportPlaceholderPredicate::new( self.db, self.file, symbol_id, - referenced_module, + referenced_parse_file, ); let star_import_predicate = self.add_predicate(star_import.into()); @@ -5136,7 +5140,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { return; } - if self.db.should_check_file(self.file) { + if self.db.should_check_file(self.file.file(self.db)) { self.semantic_syntax_errors.borrow_mut().push(error); } } diff --git a/crates/ty_python_core/src/db.rs b/crates/ty_python_core/src/db.rs index e486ae8a74..0578773518 100644 --- a/crates/ty_python_core/src/db.rs +++ b/crates/ty_python_core/src/db.rs @@ -83,10 +83,6 @@ pub(crate) mod tests { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } } #[salsa::db] diff --git a/crates/ty_python_core/src/definition.rs b/crates/ty_python_core/src/definition.rs index f01dad0665..26a2b09243 100644 --- a/crates/ty_python_core/src/definition.rs +++ b/crates/ty_python_core/src/definition.rs @@ -1,5 +1,6 @@ use std::ops::Deref; +use ruff_db::PythonFile; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_python_ast::find_node::covering_node; @@ -19,7 +20,7 @@ use crate::scope::{FileScopeId, ScopeId}; use crate::symbol::ScopedSymbolId; use crate::unpack::{Unpack, UnpackPosition}; use crate::use_def::BindingWithConstraintsIterator; -use crate::{Db, SemanticIndex}; +use crate::{Db, Program, SemanticIndex}; /// A definition of a place. /// @@ -83,6 +84,14 @@ impl<'db> Definition<'db> { self.scope_id(db).file(db) } + pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.scope_id(db).python_file(db) + } + + pub fn program(self, db: &'db dyn Db) -> Program { + self.scope_id(db).program(db) + } + pub fn file_scope(self, db: &'db dyn Db) -> FileScopeId { self.scope_id(db).file_scope_id(db) } @@ -105,8 +114,7 @@ impl<'db> Definition<'db> { /// Returns the name of the item being defined, if applicable. pub fn name(self, db: &'db dyn Db) -> Option { - let file = self.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, self.python_file(db)).load(db); let kind = self.kind(db); match kind { DefinitionKind::Function(def) => { @@ -142,8 +150,7 @@ impl<'db> Definition<'db> { /// This method returns a docstring for function, class, and attribute definitions. /// The docstring is extracted from the first statement in the body if it's a string literal. pub fn docstring(self, db: &'db dyn Db) -> Option { - let file = self.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, self.python_file(db)).load(db); let kind = self.kind(db); match kind { diff --git a/crates/ty_python_core/src/expression.rs b/crates/ty_python_core/src/expression.rs index dec30584ac..4b92b4c147 100644 --- a/crates/ty_python_core/src/expression.rs +++ b/crates/ty_python_core/src/expression.rs @@ -1,6 +1,8 @@ +use crate::Program; use crate::ast_node_ref::AstNodeRef; use crate::db::Db; use crate::scope::ScopeId; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_python_ast as ast; use salsa; @@ -73,4 +75,12 @@ impl<'db> Expression<'db> { pub fn file(self, db: &'db dyn Db) -> File { self.scope_id(db).file(db) } + + pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.scope_id(db).python_file(db) + } + + pub fn program(self, db: &'db dyn Db) -> Program { + self.scope_id(db).program(db) + } } diff --git a/crates/ty_python_core/src/lib.rs b/crates/ty_python_core/src/lib.rs index 4b5ad6c2b7..39b32dec25 100644 --- a/crates/ty_python_core/src/lib.rs +++ b/crates/ty_python_core/src/lib.rs @@ -6,8 +6,9 @@ use ruff_python_ast as ast; use std::iter::{FusedIterator, once}; use std::sync::Arc; -use ruff_db::files::File; use ruff_db::parsed::parsed_module; + +use ruff_db::PythonFile; use ruff_index::{FrozenIndexVec, IndexSlice}; use ruff_python_ast::NodeIndex; use ruff_python_parser::semantic_errors::SemanticSyntaxError; @@ -17,6 +18,10 @@ use salsa::plumbing::AsId; use smallvec::SmallVec; use ty_module_resolver::ModuleName; +// FIXME: Replace this temporary alias once semantic query keys can use the environment-bearing +// `Program` Salsa ingredient directly. +pub type Program = ast::PythonVersion; + use crate::frozen::{FrozenMap, FrozenSet}; use crate::place::ScopedPlaceId; pub use crate::statement::{Statement, StatementNodeKey}; @@ -66,7 +71,7 @@ pub mod program; /// /// Prefer using [`symbol_table`] when working with symbols from a single scope. #[salsa::tracked(returns(ref), no_eq, heap_size=ruff_memory_usage::heap_size)] -pub fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { +pub fn semantic_index<'db>(db: &'db dyn Db, file: PythonFile<'db>) -> SemanticIndex<'db> { let _span = tracing::trace_span!("semantic_index", ?file).entered(); let module = parsed_module(db, file).load(db); @@ -81,9 +86,9 @@ pub fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { /// is unchanged. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] pub fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc { - let file = scope.file(db); - let _span = tracing::trace_span!("place_table", scope=?scope.as_id(), ?file).entered(); - let index = semantic_index(db, file); + let python_file = scope.python_file(db); + let _span = tracing::trace_span!("place_table", scope=?scope.as_id(), ?python_file).entered(); + let index = semantic_index(db, python_file); Arc::clone(&index.place_tables[scope.file_scope_id(db)]) } @@ -94,9 +99,9 @@ pub fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc /// is unchanged. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] pub fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc> { - let file = scope.file(db); - let _span = tracing::trace_span!("use_def_map", scope=?scope.as_id(), ?file).entered(); - let index = semantic_index(db, file); + let python_file = scope.python_file(db); + let _span = tracing::trace_span!("use_def_map", scope=?scope.as_id(), ?python_file).entered(); + let index = semantic_index(db, python_file); Arc::clone(&index.use_def_maps[scope.file_scope_id(db)]) } @@ -171,8 +176,7 @@ pub fn attribute_scopes<'db>( db: &'db dyn Db, class_body_scope: ScopeId<'db>, ) -> impl Iterator + 'db { - let file = class_body_scope.file(db); - let index = semantic_index(db, file); + let index = semantic_index(db, class_body_scope.python_file(db)); let class_scope_id = class_body_scope.file_scope_id(db); ChildrenIter::new(&index.scopes, class_scope_id) .filter_map(move |(child_scope_id, scope)| { @@ -221,7 +225,7 @@ pub fn attribute_scopes<'db>( /// Returns the module global scope of `file`. #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] -pub fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> { +pub fn global_scope<'db>(db: &'db dyn Db, file: PythonFile<'db>) -> ScopeId<'db> { let _span = tracing::trace_span!("global_scope", ?file).entered(); FileScopeId::global().to_scope_id(db, file) @@ -1065,7 +1069,10 @@ impl HasTrackedScope for ast::Identifier {} #[cfg(test)] mod tests { - use ruff_db::{files::system_path_to_file, parsed::ParsedModuleRef}; + use ruff_db::{ + files::{File, system_path_to_file}, + parsed::ParsedModuleRef, + }; use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextRange}; @@ -1077,6 +1084,7 @@ mod tests { definition::{ DefinitionKind, LambdaParameterDefinitionNodeKind, ParameterDefinitionNodeKind, }, + program::Program, }; impl UseDefMap<'_> { @@ -1116,6 +1124,10 @@ mod tests { TestCase { db, file } } + fn python_file(db: &TestDb, file: File) -> PythonFile<'_> { + PythonFile::new(db, file, Program::get(db).python_version(db)) + } + fn names(table: &PlaceTable) -> Vec { table .symbols() @@ -1126,7 +1138,7 @@ mod tests { #[test] fn empty() { let TestCase { db, file } = test_case(""); - let global_table = place_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); let global_names = names(global_table); @@ -1136,7 +1148,7 @@ mod tests { #[test] fn simple() { let TestCase { db, file } = test_case("x"); - let global_table = place_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); assert_eq!(names(global_table), vec!["x"]); } @@ -1144,7 +1156,7 @@ mod tests { #[test] fn annotation_only() { let TestCase { db, file } = test_case("x: int"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, python_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["int", "x"]); @@ -1162,7 +1174,7 @@ mod tests { #[test] fn import() { let TestCase { db, file } = test_case("import foo"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, python_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["foo"]); @@ -1176,7 +1188,7 @@ mod tests { #[test] fn import_sub() { let TestCase { db, file } = test_case("import foo.bar"); - let global_table = place_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); assert_eq!(names(global_table), vec!["foo"]); } @@ -1184,7 +1196,7 @@ mod tests { #[test] fn import_as() { let TestCase { db, file } = test_case("import foo.bar as baz"); - let global_table = place_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); assert_eq!(names(global_table), vec!["baz"]); } @@ -1192,7 +1204,7 @@ mod tests { #[test] fn import_from() { let TestCase { db, file } = test_case("from bar import foo"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, python_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["foo"]); @@ -1213,7 +1225,7 @@ mod tests { #[test] fn assign() { let TestCase { db, file } = test_case("x = foo"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, python_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["foo", "x"]); @@ -1233,7 +1245,7 @@ mod tests { #[test] fn augmented_assignment() { let TestCase { db, file } = test_case("x += 1"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, python_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["x"]); @@ -1258,12 +1270,12 @@ class C: y = 2 ", ); - let global_table = place_table(&db, global_scope(&db, file)); + let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); assert_eq!(names(global_table), vec!["C", "y"]); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, python_file(&db, file)).load(&db); + let index = semantic_index(&db, python_file(&db, file)); let [(class_scope_id, class_scope)] = index .child_scopes(FileScopeId::global()) @@ -1273,7 +1285,9 @@ y = 2 }; assert_eq!(class_scope.kind(), ScopeKind::Class); assert_eq!( - class_scope_id.to_scope_id(&db, file).name(&db, &module), + class_scope_id + .to_scope_id(&db, python_file(&db, file)) + .name(&db, &module), "C" ); @@ -1296,8 +1310,8 @@ def func(): y = 2 ", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, python_file(&db, file)).load(&db); + let index = semantic_index(&db, python_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["func", "y"]); @@ -1310,7 +1324,9 @@ y = 2 }; assert_eq!(function_scope.kind(), ScopeKind::Function); assert_eq!( - function_scope_id.to_scope_id(&db, file).name(&db, &module), + function_scope_id + .to_scope_id(&db, python_file(&db, file)) + .name(&db, &module), "func" ); @@ -1333,8 +1349,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ", ); - let index = semantic_index(&db, file); - let global_table = place_table(&db, global_scope(&db, file)); + let index = semantic_index(&db, python_file(&db, file)); + let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); assert_eq!(names(global_table), vec!["str", "int", "f"]); @@ -1378,8 +1394,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): fn lambda_parameter_symbols() { let TestCase { db, file } = test_case("lambda a, b, c=1, *args, d=2, **kwargs: None"); - let index = semantic_index(&db, file); - let global_table = place_table(&db, global_scope(&db, file)); + let index = semantic_index(&db, python_file(&db, file)); + let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); assert!(names(global_table).is_empty()); @@ -1444,8 +1460,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, python_file(&db, file)).load(&db); + let index = semantic_index(&db, python_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["iter1"]); @@ -1460,7 +1476,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension); assert_eq!( comprehension_scope_id - .to_scope_id(&db, file) + .to_scope_id(&db, python_file(&db, file)) .name(&db, &module), "" ); @@ -1495,7 +1511,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ", ); - let index = semantic_index(&db, file); + let index = semantic_index(&db, python_file(&db, file)); let [(comprehension_scope_id, _)] = index .child_scopes(FileScopeId::global()) .collect::>()[..] @@ -1505,7 +1521,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): let use_def = index.use_def_map(comprehension_scope_id); - let module = parsed_module(&db, file).load(&db); + let module = parsed_module(&db, python_file(&db, file)).load(&db); let syntax = module.syntax(); let element = syntax.body[0] .as_expr_stmt() @@ -1516,7 +1532,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): .elt .as_name_expr() .unwrap(); - let element_use_id = element.scoped_use_id(&db, file); + let element_use_id = element.scoped_use_id(&db, python_file(&db, file)); let binding = use_def.first_binding_at_use(element_use_id).unwrap(); let DefinitionKind::Comprehension(comprehension) = binding.kind(&db) else { @@ -1540,8 +1556,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, python_file(&db, file)).load(&db); + let index = semantic_index(&db, python_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["iter1"]); @@ -1556,7 +1572,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension); assert_eq!( comprehension_scope_id - .to_scope_id(&db, file) + .to_scope_id(&db, python_file(&db, file)) .name(&db, &module), "" ); @@ -1575,7 +1591,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): assert_eq!(inner_comprehension_scope.kind(), ScopeKind::Comprehension); assert_eq!( inner_comprehension_scope_id - .to_scope_id(&db, file) + .to_scope_id(&db, python_file(&db, file)) .name(&db, &module), "" ); @@ -1594,7 +1610,7 @@ with item1 as x, item2 as y: ", ); - let index = semantic_index(&db, file); + let index = semantic_index(&db, python_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["item1", "x", "item2", "y"]); @@ -1617,7 +1633,7 @@ with context() as (x, y): ", ); - let index = semantic_index(&db, file); + let index = semantic_index(&db, python_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["context", "x", "y"]); @@ -1641,8 +1657,8 @@ def func(): y = 2 ", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, python_file(&db, file)).load(&db); + let index = semantic_index(&db, python_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["func"]); @@ -1659,12 +1675,16 @@ def func(): assert_eq!(func_scope_1.kind(), ScopeKind::Function); assert_eq!( - func_scope1_id.to_scope_id(&db, file).name(&db, &module), + func_scope1_id + .to_scope_id(&db, python_file(&db, file)) + .name(&db, &module), "func" ); assert_eq!(func_scope_2.kind(), ScopeKind::Function); assert_eq!( - func_scope2_id.to_scope_id(&db, file).name(&db, &module), + func_scope2_id + .to_scope_id(&db, python_file(&db, file)) + .name(&db, &module), "func" ); @@ -1689,8 +1709,8 @@ def func[T](): ", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, python_file(&db, file)).load(&db); + let index = semantic_index(&db, python_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["func"]); @@ -1704,7 +1724,9 @@ def func[T](): assert_eq!(ann_scope.kind(), ScopeKind::TypeParams); assert_eq!( - ann_scope_id.to_scope_id(&db, file).name(&db, &module), + ann_scope_id + .to_scope_id(&db, python_file(&db, file)) + .name(&db, &module), "func" ); let ann_table = index.place_table(ann_scope_id); @@ -1717,7 +1739,9 @@ def func[T](): }; assert_eq!(func_scope.kind(), ScopeKind::Function); assert_eq!( - func_scope_id.to_scope_id(&db, file).name(&db, &module), + func_scope_id + .to_scope_id(&db, python_file(&db, file)) + .name(&db, &module), "func" ); let func_table = index.place_table(func_scope_id); @@ -1733,8 +1757,8 @@ class C[T]: ", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, python_file(&db, file)).load(&db); + let index = semantic_index(&db, python_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["C"]); @@ -1747,7 +1771,12 @@ class C[T]: }; assert_eq!(ann_scope.kind(), ScopeKind::TypeParams); - assert_eq!(ann_scope_id.to_scope_id(&db, file).name(&db, &module), "C"); + assert_eq!( + ann_scope_id + .to_scope_id(&db, python_file(&db, file)) + .name(&db, &module), + "C" + ); let ann_table = index.place_table(ann_scope_id); assert_eq!(names(ann_table), vec!["T"]); assert!( @@ -1765,7 +1794,9 @@ class C[T]: assert_eq!(class_scope.kind(), ScopeKind::Class); assert_eq!( - class_scope_id.to_scope_id(&db, file).name(&db, &module), + class_scope_id + .to_scope_id(&db, python_file(&db, file)) + .name(&db, &module), "C" ); assert_eq!(names(index.place_table(class_scope_id)), vec!["x"]); @@ -1774,8 +1805,8 @@ class C[T]: #[test] fn reachability_trivial() { let TestCase { db, file } = test_case("x = 1; x"); - let module = parsed_module(&db, file).load(&db); - let scope = global_scope(&db, file); + let module = parsed_module(&db, python_file(&db, file)).load(&db); + let scope = global_scope(&db, python_file(&db, file)); let ast = module.syntax(); let ast::Stmt::Expr(ast::StmtExpr { value: x_use_expr, .. @@ -1786,7 +1817,7 @@ class C[T]: let ast::Expr::Name(x_use_expr_name) = x_use_expr.as_ref() else { panic!("expected a Name"); }; - let x_use_id = x_use_expr_name.scoped_use_id(&db, file); + let x_use_id = x_use_expr_name.scoped_use_id(&db, python_file(&db, file)); let use_def = use_def_map(&db, scope); let binding = use_def.first_binding_at_use(x_use_id).unwrap(); let DefinitionKind::Assignment(assignment) = binding.kind(&db) else { @@ -1806,8 +1837,8 @@ class C[T]: fn expression_scope() { let TestCase { db, file } = test_case("x = 1;\ndef test():\n y = 4"); - let index = semantic_index(&db, file); - let module = parsed_module(&db, file).load(&db); + let index = semantic_index(&db, python_file(&db, file)); + let module = parsed_module(&db, python_file(&db, file)).load(&db); let ast = module.syntax(); let x_stmt = ast.body[0].as_assign_stmt().unwrap(); @@ -1833,7 +1864,14 @@ class C[T]: ) -> Vec<&'a str> { scopes .into_iter() - .map(|(scope_id, _)| scope_id.to_scope_id(db, file).name(db, module)) + .map(|(scope_id, _)| { + scope_id + .to_scope_id( + db, + PythonFile::new(db, file, Program::get(db).python_version(db)), + ) + .name(db, module) + }) .collect() } @@ -1850,8 +1888,8 @@ def x(): pass", ); - let module = parsed_module(&db, file).load(&db); - let index = semantic_index(&db, file); + let module = parsed_module(&db, python_file(&db, file)).load(&db); + let index = semantic_index(&db, python_file(&db, file)); let descendants = index.descendent_scopes(FileScopeId::global()); assert_eq!( @@ -1897,7 +1935,7 @@ match subject: ", ); - let global_scope_id = global_scope(&db, file); + let global_scope_id = global_scope(&db, python_file(&db, file)); let global_table = place_table(&db, global_scope_id); assert!(global_table.symbol_by_name("Foo").unwrap().is_used()); @@ -1929,7 +1967,7 @@ match 1: ", ); - let global_scope_id = global_scope(&db, file); + let global_scope_id = global_scope(&db, python_file(&db, file)); let global_table = place_table(&db, global_scope_id); assert_eq!(names(global_table), vec!["first", "second"]); @@ -1946,7 +1984,7 @@ match 1: #[test] fn for_loops_single_assignment() { let TestCase { db, file } = test_case("for x in a: pass"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, python_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(&names(global_table), &["a", "x"]); @@ -1962,7 +2000,7 @@ match 1: #[test] fn for_loops_simple_unpacking() { let TestCase { db, file } = test_case("for (x, y) in a: pass"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, python_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(&names(global_table), &["a", "x", "y"]); @@ -1982,7 +2020,7 @@ match 1: #[test] fn for_loops_complex_unpacking() { let TestCase { db, file } = test_case("for [((a,) b), (c, d)] in e: pass"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, python_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(&names(global_table), &["e", "a", "b", "c", "d"]); diff --git a/crates/ty_python_core/src/predicate.rs b/crates/ty_python_core/src/predicate.rs index 1f259d8584..ff2de40048 100644 --- a/crates/ty_python_core/src/predicate.rs +++ b/crates/ty_python_core/src/predicate.rs @@ -7,6 +7,8 @@ //! - [_Reachability constraints_][crate::reachability_constraints] determine the //! static reachability of a binding, and the reachability of a statement or expression. +use crate::Program; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_index::{FrozenIndexVec, Idx, IndexVec}; use ruff_python_ast::{Singleton, name::Name}; @@ -231,7 +233,7 @@ pub enum PatternPredicateKind<'db> { #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] pub struct PatternPredicate<'db> { #[returns(copy)] - pub file: File, + pub python_file: PythonFile<'db>, #[returns(copy)] pub file_scope: FileScopeId, @@ -254,8 +256,16 @@ pub struct PatternPredicate<'db> { impl get_size2::GetSize for PatternPredicate<'_> {} impl<'db> PatternPredicate<'db> { + pub fn file(self, db: &'db dyn Db) -> File { + self.python_file(db).file(db) + } + pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.file_scope(db).to_scope_id(db, self.file(db)) + self.file_scope(db).to_scope_id(db, self.python_file(db)) + } + + pub fn program(self, db: &'db dyn Db) -> Program { + self.scope(db).program(db) } } @@ -302,7 +312,7 @@ impl<'db> PatternPredicate<'db> { #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] pub struct StarImportPlaceholderPredicate<'db> { #[returns(copy)] - pub importing_file: File, + pub importing_parse_file: PythonFile<'db>, /// Each symbol imported by a `*` import has a separate predicate associated with it: /// this field identifies which symbol that is. @@ -317,7 +327,7 @@ pub struct StarImportPlaceholderPredicate<'db> { pub symbol_id: ScopedSymbolId, #[returns(copy)] - pub referenced_file: File, + pub referenced_parse_file: PythonFile<'db>, } // The Salsa heap is tracked separately. @@ -327,7 +337,7 @@ impl<'db> StarImportPlaceholderPredicate<'db> { pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { // See doc-comment above [`StarImportPlaceholderPredicate::symbol_id`]: // valid `*`-import definitions can only take place in the global scope. - global_scope(db, self.importing_file(db)) + global_scope(db, self.importing_parse_file(db)) } } diff --git a/crates/ty_python_core/src/re_exports.rs b/crates/ty_python_core/src/re_exports.rs index 49fa339a96..7194008de8 100644 --- a/crates/ty_python_core/src/re_exports.rs +++ b/crates/ty_python_core/src/re_exports.rs @@ -20,7 +20,9 @@ //! to handle cycles. We do this using fixpoint iteration; adding fixpoint iteration to the //! whole [`super::semantic_index()`] query would probably be prohibitively expensive. -use ruff_db::{files::File, parsed::parsed_module}; +use ruff_db::parsed::parsed_module; + +use ruff_db::PythonFile; use ruff_python_ast::{ self as ast, name::Name, @@ -36,7 +38,7 @@ use crate::Db; cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size) ] -pub(super) fn exported_names(db: &dyn Db, file: File) -> Box<[Name]> { +pub(super) fn exported_names(db: &dyn Db, file: PythonFile<'_>) -> Box<[Name]> { let module = parsed_module(db, file).load(db); let mut finder = ExportFinder::new(db, file); finder.visit_body(module.suite()); @@ -51,18 +53,18 @@ pub(super) fn exported_names(db: &dyn Db, file: File) -> Box<[Name]> { struct ExportFinder<'db> { db: &'db dyn Db, - file: File, + file: PythonFile<'db>, visiting_stub_file: bool, exports: FxHashMap<&'db Name, PossibleExportKind>, dunder_all: DunderAll, } impl<'db> ExportFinder<'db> { - fn new(db: &'db dyn Db, file: File) -> Self { + fn new(db: &'db dyn Db, file: PythonFile<'db>) -> Self { Self { db, file, - visiting_stub_file: file.is_stub(db), + visiting_stub_file: file.file(db).is_stub(db), exports: FxHashMap::default(), dunder_all: DunderAll::NotPresent, } @@ -257,7 +259,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { .iter() .flat_map(|module| { module - .file(self.db) + .python_file(self.db) .map(|file| exported_names(self.db, file)) .unwrap_or_default() }) diff --git a/crates/ty_python_core/src/scope.rs b/crates/ty_python_core/src/scope.rs index b62d495bd1..bfb8c85721 100644 --- a/crates/ty_python_core/src/scope.rs +++ b/crates/ty_python_core/src/scope.rs @@ -1,19 +1,19 @@ use std::ops::Range; -use ruff_db::{files::File, parsed::ParsedModuleRef}; +use ruff_db::{PythonFile, files::File, parsed::ParsedModuleRef}; use ruff_index::newtype_index; use ruff_python_ast::{self as ast, NodeIndex}; use crate::{ - Db, SemanticIndex, ast_node_ref::AstNodeRef, definition::Definition, node_key::NodeKey, - semantic_index, + Db, Program, SemanticIndex, ast_node_ref::AstNodeRef, definition::Definition, + node_key::NodeKey, semantic_index, }; /// A cross-module identifier of a scope that can be used as a salsa query parameter. #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] pub struct ScopeId<'db> { #[returns(copy)] - pub file: File, + pub python_file: PythonFile<'db>, #[returns(copy)] pub file_scope_id: FileScopeId, @@ -23,16 +23,24 @@ pub struct ScopeId<'db> { impl get_size2::GetSize for ScopeId<'_> {} impl<'db> ScopeId<'db> { + pub fn file(self, db: &dyn Db) -> File { + self.python_file(db).file(db) + } + + pub fn program(self, db: &dyn Db) -> Program { + self.python_file(db).python_version(db) + } + pub fn is_annotation(self, db: &'db dyn Db) -> bool { self.node(db).scope_kind().is_annotation() } - pub fn node(self, db: &dyn Db) -> &NodeWithScopeKind { + pub fn node(self, db: &'db dyn Db) -> &'db NodeWithScopeKind { self.scope(db).node() } /// Returns `true` if this scope may require type context from its parent scope. - pub fn accepts_type_context(self, db: &dyn Db) -> bool { + pub fn accepts_type_context(self, db: &'db dyn Db) -> bool { matches!( self.node(db), NodeWithScopeKind::Lambda(_) @@ -43,13 +51,13 @@ impl<'db> ScopeId<'db> { ) } - pub fn scope(self, db: &dyn Db) -> &Scope { - semantic_index(db, self.file(db)).scope(self.file_scope_id(db)) + pub fn scope(self, db: &'db dyn Db) -> &'db Scope { + semantic_index(db, self.python_file(db)).scope(self.file_scope_id(db)) } /// Returns the class definition for the enclosing class if this scope is a method body. - fn class_definition_of_method(self, db: &'db dyn Db) -> Option> { - semantic_index(db, self.file(db)).class_definition_of_method(self.file_scope_id(db)) + pub fn class_definition_of_method(self, db: &'db dyn Db) -> Option> { + semantic_index(db, self.python_file(db)).class_definition_of_method(self.file_scope_id(db)) } pub fn is_method_scope(self, db: &'db dyn Db) -> bool { @@ -97,7 +105,7 @@ impl FileScopeId { self == FileScopeId::global() } - pub fn to_scope_id(self, db: &dyn Db, file: File) -> ScopeId<'_> { + pub fn to_scope_id<'db>(self, db: &'db dyn Db, file: PythonFile<'db>) -> ScopeId<'db> { let index = semantic_index(db, file); index.scope_ids_by_scope[self] } diff --git a/crates/ty_python_core/src/statement.rs b/crates/ty_python_core/src/statement.rs index 9f431884f3..25d392ea26 100644 --- a/crates/ty_python_core/src/statement.rs +++ b/crates/ty_python_core/src/statement.rs @@ -1,9 +1,11 @@ +use crate::Program; use crate::ast_node_ref::AstNodeRef; use crate::db::Db; use crate::definition::Definition; use crate::expression::Expression; use crate::node_key::NodeKey; use crate::scope::{FileScopeId, ScopeId}; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_python_ast as ast; use salsa; @@ -38,7 +40,7 @@ pub enum Statement<'db> { pub struct StatementInner<'db> { /// The file in which the statement occurs. #[returns(copy)] - pub file: File, + pub python_file: PythonFile<'db>, /// The scope in which the statement occurs. #[returns(copy)] @@ -55,8 +57,16 @@ pub struct StatementInner<'db> { impl get_size2::GetSize for StatementInner<'_> {} impl<'db> StatementInner<'db> { + pub fn file(self, db: &'db dyn Db) -> File { + self.python_file(db).file(db) + } + pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.file_scope(db).to_scope_id(db, self.file(db)) + self.file_scope(db).to_scope_id(db, self.python_file(db)) + } + + pub fn program(self, db: &'db dyn Db) -> Program { + self.scope(db).program(db) } } diff --git a/crates/ty_python_core/src/unpack.rs b/crates/ty_python_core/src/unpack.rs index 2f43e915c2..ed422fe18a 100644 --- a/crates/ty_python_core/src/unpack.rs +++ b/crates/ty_python_core/src/unpack.rs @@ -1,3 +1,5 @@ +use crate::Program; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; use ruff_python_ast::{self as ast, AnyNodeRef}; @@ -30,7 +32,7 @@ use crate::scope::{FileScopeId, ScopeId}; #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] pub struct Unpack<'db> { #[returns(copy)] - pub file: File, + pub python_file: PythonFile<'db>, #[returns(copy)] pub(crate) value_file_scope: FileScopeId, @@ -55,13 +57,22 @@ pub struct Unpack<'db> { impl get_size2::GetSize for Unpack<'_> {} impl<'db> Unpack<'db> { + pub fn file(self, db: &'db dyn Db) -> File { + self.python_file(db).file(db) + } + pub fn target<'ast>(self, db: &'db dyn Db, parsed: &'ast ParsedModuleRef) -> &'ast ast::Expr { self._target(db).node(parsed) } /// Returns the scope where the unpack target expression belongs to. pub fn target_scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.target_file_scope(db).to_scope_id(db, self.file(db)) + self.target_file_scope(db) + .to_scope_id(db, self.python_file(db)) + } + + pub fn program(self, db: &'db dyn Db) -> Program { + self.target_scope(db).program(db) } /// Returns the range of the unpack target expression. diff --git a/crates/ty_python_semantic/src/db.rs b/crates/ty_python_semantic/src/db.rs index 7d34116493..2ee25ab997 100644 --- a/crates/ty_python_semantic/src/db.rs +++ b/crates/ty_python_semantic/src/db.rs @@ -36,13 +36,13 @@ pub(crate) mod tests { use anyhow::Context; use ty_python_core::platform::PythonPlatform; - use crate::{check_file_unwrap, default_lint_registry}; - use ruff_db::Db as SourceDb; + use crate::{ProgramEnvironment, check_file_unwrap, default_lint_registry}; use ruff_db::files::Files; use ruff_db::system::{ DbWithTestSystem, DbWithWritableSystem as _, System, SystemPath, SystemPathBuf, TestSystem, }; use ruff_db::vendored::VendoredFileSystem; + use ruff_db::{Db as SourceDb, PythonFile}; use ruff_python_ast::PythonVersion; use ty_module_resolver::{Db as ModuleResolverDb, SearchPathSettings, SearchPaths}; use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; @@ -85,6 +85,14 @@ pub(crate) mod tests { } } + pub(crate) fn python_version(&self) -> PythonVersion { + Program::get(self).python_version(self) + } + + pub(crate) fn program_environment(&self) -> ProgramEnvironment<'_> { + ProgramEnvironment::from_program(self.python_version()) + } + /// Marks `file` as open in the editor. /// /// This is untracked state: open a file before running any queries. @@ -131,10 +139,6 @@ pub(crate) mod tests { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } } #[salsa::db] @@ -151,7 +155,7 @@ pub(crate) mod tests { return Vec::new(); } - check_file_unwrap(self, file) + check_file_unwrap(self, PythonFile::new(self, file, self.python_version())) } fn rule_selection(&self, _file: File) -> &RuleSelection { diff --git a/crates/ty_python_semantic/src/diagnostic/mod.rs b/crates/ty_python_semantic/src/diagnostic/mod.rs index 6ed4212c78..0fbb0422c9 100644 --- a/crates/ty_python_semantic/src/diagnostic/mod.rs +++ b/crates/ty_python_semantic/src/diagnostic/mod.rs @@ -1,5 +1,5 @@ use crate::{ - Db, Program, PythonVersionSource, PythonVersionWithSource, lint::lint_documentation_url, + Db, PythonVersionSource, PythonVersionWithSource, lint::lint_documentation_url, types::TypeCheckDiagnostics, }; use levenshtein::{HideUnderscoredSuggestions, find_best_suggestion}; @@ -49,7 +49,7 @@ pub(crate) fn add_inferred_python_version_hint_to_diagnostic( diagnostic: &mut Diagnostic, action: &str, ) { - let program = Program::get(db); + let program = ty_python_core::program::Program::get(db); let PythonVersionWithSource { version, source } = program.python_version_with_source(db); match source { diff --git a/crates/ty_python_semantic/src/dunder_all.rs b/crates/ty_python_semantic/src/dunder_all.rs index 122d705d1d..150e781e74 100644 --- a/crates/ty_python_semantic/src/dunder_all.rs +++ b/crates/ty_python_semantic/src/dunder_all.rs @@ -1,4 +1,4 @@ -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; @@ -6,15 +6,16 @@ use ruff_python_ast::{self as ast}; use rustc_hash::FxHashSet; use ty_module_resolver::{ModuleName, resolve_module}; -use crate::Db; use crate::types::{Type, TypeContext, infer_expression_types}; +use crate::{Db, ProgramEnvironment}; use ty_python_core::{SemanticIndex, Truthiness, semantic_index}; /// Returns a set of names in the `__all__` variable for `file`, [`None`] if it is not defined or /// if it contains invalid elements. #[salsa::tracked(returns(as_ref), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn dunder_all_names(db: &dyn Db, file: File) -> Option> { - let _span = tracing::trace_span!("dunder_all_names", file=?file.path(db)).entered(); +pub(crate) fn dunder_all_names(db: &dyn Db, file: PythonFile<'_>) -> Option> { + let source_file = file.file(db); + let _span = tracing::trace_span!("dunder_all_names", file=?source_file.path(db)).entered(); let module = parsed_module(db, file).load(db); let index = semantic_index(db, file); @@ -26,7 +27,8 @@ pub(crate) fn dunder_all_names(db: &dyn Db, file: File) -> Option { db: &'db dyn Db, - file: File, + env: ProgramEnvironment<'db>, + file: PythonFile<'db>, /// The semantic index for the module. index: &'db SemanticIndex<'db>, @@ -43,9 +45,10 @@ struct DunderAllNamesCollector<'db> { } impl<'db> DunderAllNamesCollector<'db> { - fn new(db: &'db dyn Db, file: File, index: &'db SemanticIndex<'db>) -> Self { + fn new(db: &'db dyn Db, file: PythonFile<'db>, index: &'db SemanticIndex<'db>) -> Self { Self { db, + env: ProgramEnvironment::from_file(file), file, index, origin: None, @@ -70,6 +73,7 @@ impl<'db> DunderAllNamesCollector<'db> { /// /// Returns `true` if the expression is a valid list/tuple/set or module `__all__`, `false` otherwise. fn extend(&mut self, expr: &ast::Expr) -> bool { + let db = self.db; match expr { // `__all__ += [...]` // `__all__.extend([...])` @@ -83,14 +87,15 @@ impl<'db> DunderAllNamesCollector<'db> { if attr != "__all__" { return false; } + let Type::ModuleLiteral(module_literal) = self.standalone_expression_type(value) else { return false; }; let Some(module_dunder_all_names) = module_literal - .module(self.db) - .file(self.db) - .and_then(|file| dunder_all_names(self.db, file)) + .module(db) + .python_file(db) + .and_then(|file| dunder_all_names(db, file)) else { // The module either does not have a `__all__` variable or it is invalid. return false; @@ -156,10 +161,11 @@ impl<'db> DunderAllNamesCollector<'db> { &self, import_from: &ast::StmtImportFrom, ) -> Option<&'db FxHashSet> { - let module_name = - ModuleName::from_import_statement(self.db, self.file, import_from).ok()?; - let module = resolve_module(self.db, self.file, &module_name)?; - dunder_all_names(self.db, module.file(self.db)?) + let db = self.db; + + let module_name = ModuleName::from_import_statement(db, self.file, import_from).ok()?; + let module = resolve_module(db, self.file, &module_name)?; + dunder_all_names(db, module.python_file(db)?) } /// Infer the type of a standalone expression. @@ -168,7 +174,8 @@ impl<'db> DunderAllNamesCollector<'db> { /// /// This function panics if `expr` was not marked as a standalone expression during semantic indexing. fn standalone_expression_type(&self, expr: &ast::Expr) -> Type<'db> { - infer_expression_types(self.db, self.index.expression(expr), TypeContext::default()) + let db = self.db; + infer_expression_types(db, self.index.expression(expr), TypeContext::default()) .expression_type(expr) } @@ -176,7 +183,10 @@ impl<'db> DunderAllNamesCollector<'db> { /// /// Returns [`None`] if the expression type doesn't implement `__bool__` correctly. fn evaluate_test_expr(&self, expr: &ast::Expr) -> Option { - self.standalone_expression_type(expr).try_bool(self.db).ok() + let db = self.db; + self.standalone_expression_type(expr) + .try_bool(db, &self.env) + .ok() } /// Add valid names to the set. @@ -197,10 +207,11 @@ impl<'db> DunderAllNamesCollector<'db> { /// Returns [`None`] if `__all__` is not defined in the current module or if it contains /// invalid elements. fn into_names(mut self) -> Option> { + let db = self.db; if self.origin.is_none() { None } else if self.invalid { - tracing::debug!("Invalid `__all__` in `{}`", self.file.path(self.db)); + tracing::debug!("Invalid `__all__` in `{}`", self.file.file(db).path(db)); None } else { self.names.shrink_to_fit(); diff --git a/crates/ty_python_semantic/src/fixes.rs b/crates/ty_python_semantic/src/fixes.rs index 6616b9cde9..03a3a93086 100644 --- a/crates/ty_python_semantic/src/fixes.rs +++ b/crates/ty_python_semantic/src/fixes.rs @@ -1,4 +1,5 @@ use crate::{SuppressFix, is_unused_ignore_comment_lint, suppress_all}; +use ruff_db::PythonFile; use ruff_db::cancellation::{Canceled, CancellationToken}; use ruff_db::diagnostic::{DisplayDiagnosticConfig, DisplayDiagnostics}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; @@ -10,6 +11,7 @@ use ruff_db::{ source::source_text, }; use ruff_diagnostics::{Applicability, Edit, Fix, IsolationLevel, SourceMap}; +use ruff_python_ast::PythonVersion; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; use salsa::Setter as _; @@ -37,11 +39,13 @@ pub struct FixAllResults { /// If the `db`'s system isn't [writable](WritableSystem). pub fn suppress_all_diagnostics( db: &mut dyn Db, + python_version: PythonVersion, diagnostics: Vec, cancellation_token: &CancellationToken, ) -> Result { fix_all( db, + python_version, diagnostics, FixMode::Suppress, cancellation_token, @@ -57,12 +61,14 @@ pub fn suppress_all_diagnostics( /// If the `db`'s system isn't [writable](WritableSystem). pub fn fix_all_diagnostics( db: &mut dyn Db, + python_version: PythonVersion, diagnostics: Vec, applicability: Applicability, cancellation_token: &CancellationToken, ) -> Result { fix_all( db, + python_version, diagnostics, FixMode::ApplyFixes(applicability), cancellation_token, @@ -77,6 +83,7 @@ const MAX_ITERATIONS: usize = 10; /// `check_file` is a separate parameter so that tests can easily mock out a file's diagnostics. fn fix_all( db: &mut dyn Db, + python_version: PythonVersion, mut diagnostics: Vec, fix_mode: FixMode, cancellation_token: &CancellationToken, @@ -128,13 +135,14 @@ where continue; }; - let parsed = parsed_module(db, file); + let python_file = PythonFile::new(db, file, python_version); + let parsed = parsed_module(db, python_file); if parsed.load(db).has_syntax_errors() { tracing::warn!("Skipping file `{path}` with syntax errors"); continue; } - let fixes = fix_mode.fixes(db, file, diagnostics); + let fixes = fix_mode.fixes(db, python_file, diagnostics); if fixes.is_empty() { tracing::debug!("Skipping file `{path}` without applicable fixes."); @@ -176,6 +184,7 @@ where // This is done outside the above loop so that it can run in parallel. let check_results = recheck_files( &*db, + python_version, unstaged_fixes, fix_mode, cancellation_token, @@ -379,7 +388,12 @@ impl FixMode { } } - fn fixes(self, db: &dyn Db, file: File, file_diagnostics: &[Diagnostic]) -> Vec { + fn fixes( + self, + db: &dyn Db, + file: PythonFile<'_>, + file_diagnostics: &[Diagnostic], + ) -> Vec { match self { FixMode::Suppress => { let suppressable_diagnostics: Vec<_> = file_diagnostics @@ -743,6 +757,7 @@ enum CheckResult<'a> { fn recheck_files<'a, F>( db: &dyn Db, + python_version: PythonVersion, changes: Vec<(QueuedFile<'a>, usize)>, fix_mode: FixMode, cancellation_token: &CancellationToken, @@ -768,7 +783,8 @@ where let db = &*db; - let parsed = parsed_module(db, file.file); + let python_file = PythonFile::new(db, file.file, python_version); + let parsed = parsed_module(db, python_file); let parsed = parsed.load(db); let result = if parsed.has_syntax_errors() { @@ -778,7 +794,7 @@ where CheckResult::SyntaxError { diagnostic, file } } else { let diagnostics = check_file(db, file.file); - let fixes = fix_mode.fixes(db, file.file, &diagnostics); + let fixes = fix_mode.fixes(db, python_file, &diagnostics); file.applied_fixes += applied_fixes; file.diagnostics = Some(diagnostics); @@ -797,10 +813,8 @@ where #[cfg(test)] mod tests { - use std::collections::hash_map::Entry; - use std::hash::{DefaultHasher, Hash, Hasher}; - use insta::assert_snapshot; + use ruff_db::PythonFile; use ruff_db::cancellation::CancellationTokenSource; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, @@ -813,6 +827,8 @@ mod tests { use ruff_diagnostics::{Applicability, Edit, Fix}; use ruff_text_size::{TextLen as _, TextRange, TextSize}; use rustc_hash::FxHashMap; + use std::collections::hash_map::Entry; + use std::hash::{DefaultHasher, Hash, Hasher}; use super::suppress_all_diagnostics; use crate::Db; @@ -1709,10 +1725,12 @@ class B(A): }; let initial_diagnostics = check_file(&db, file); + let python_version = db.python_version(); let cancellation_token_source = CancellationTokenSource::new(); let fixes = fix_all( &mut db, + python_version, initial_diagnostics, FixMode::ApplyFixes(Applicability::Safe), &cancellation_token_source.token(), @@ -1786,10 +1804,12 @@ class B(A): }; let initial_diagnostics = check_file(&db, file); + let python_version = db.python_version(); let cancellation_token_source = CancellationTokenSource::new(); let fixes = fix_all( &mut db, + python_version, initial_diagnostics, FixMode::ApplyFixes(Applicability::Safe), &cancellation_token_source.token(), @@ -1861,8 +1881,10 @@ class B(A): create_diagnostics(file) }; + let python_version = db.python_version(); let result = fix_all( &mut db, + python_version, initial_diagnostics, FixMode::ApplyFixes(Applicability::Safe), &cancellation_token_source.token(), @@ -1961,10 +1983,12 @@ class B(A): }; let initial_diagnostics = check_file(&db, file); + let python_version = db.python_version(); let cancellation_token_source = CancellationTokenSource::new(); let fixes = fix_all( &mut db, + python_version, initial_diagnostics, FixMode::ApplyFixes(Applicability::Safe), &cancellation_token_source.token(), @@ -1996,7 +2020,8 @@ class B(A): let file = system_path_to_file(&db, "test.py").unwrap(); - let parsed_before = parsed_module(&db, file); + let python_version = db.python_version(); + let parsed_before = parsed_module(&db, PythonFile::new(&db, file, python_version)); let had_syntax_errors = parsed_before.load(&db).has_syntax_errors(); let diagnostics = db.check_file(file); @@ -2011,9 +2036,13 @@ class B(A): .cloned() .collect(); let cancellation_token_source = CancellationTokenSource::new(); - let fixes = - suppress_all_diagnostics(&mut db, diagnostics, &cancellation_token_source.token()) - .expect("operation never gets cancelled"); + let fixes = suppress_all_diagnostics( + &mut db, + python_version, + diagnostics, + &cancellation_token_source.token(), + ) + .expect("operation never gets cancelled"); if had_syntax_errors { assert_eq!(fixes.count, 0); @@ -2047,7 +2076,7 @@ class B(A): let fixed = source_text(&db, file); - let parsed = parsed_module(&db, file); + let parsed = parsed_module(&db, PythonFile::new(&db, file, python_version)); let parsed = parsed.load(&db); let diagnostics_after_applying_fixes = db.check_file(file); diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 31ec9b0657..3b2f48a91d 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -12,6 +12,7 @@ pub use db::Db; pub(crate) use diagnostic::add_inferred_python_version_hint_to_diagnostic; pub use diagnostic::inferred_python_version_source_annotation; pub use fixes::{fix_all_diagnostics, suppress_all_diagnostics}; +use ruff_db::PythonFile; use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; @@ -27,9 +28,9 @@ pub(crate) use suppression::{ SuppressFix, UNUSED_IGNORE_COMMENT, is_unused_ignore_comment_lint, suppress_all, }; use ty_module_resolver::ModuleGlobSet; +pub use ty_python_core::Program; use ty_python_core::definition::docstring_from_body; use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::Program; use ty_python_core::scope::ScopeId; use ty_python_core::{ BindingWithConstraintsIterator, DeclarationsIterator, FileScopeId, attribute_scopes, @@ -46,7 +47,7 @@ pub use types::ide_support::{ map_stub_definition, type_hierarchy_prepare, type_hierarchy_subtypes, type_hierarchy_supertypes, }; -pub use types::{DisplaySettings, TypeQualifiers}; +pub use types::{DisplaySettings, ProgramEnvironment, TypeQualifiers}; mod db; mod dunder_all; @@ -129,8 +130,7 @@ pub(crate) fn attribute_assignments<'db, 's>( class_body_scope: ScopeId<'db>, name: &'s str, ) -> impl Iterator, FileScopeId)> + use<'s, 'db> { - let file = class_body_scope.file(db); - let index = semantic_index(db, file); + let index = semantic_index(db, class_body_scope.python_file(db)); attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| { let place_table = index.place_table(function_scope_id); @@ -150,8 +150,7 @@ pub(crate) fn attribute_declarations<'db, 's>( class_body_scope: ScopeId<'db>, name: &'s str, ) -> impl Iterator, FileScopeId)> + use<'s, 'db> { - let file = class_body_scope.file(db); - let index = semantic_index(db, file); + let index = semantic_index(db, class_body_scope.python_file(db)); attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| { let place_table = index.place_table(function_scope_id); @@ -165,27 +164,28 @@ pub(crate) fn attribute_declarations<'db, 's>( } /// Get the module-level docstring for the given file. -pub(crate) fn module_docstring(db: &dyn Db, file: File) -> Option { +pub(crate) fn module_docstring(db: &dyn Db, file: PythonFile<'_>) -> Option { let module = parsed_module(db, file).load(db); docstring_from_body(module.suite()) .map(|docstring_expr| docstring_expr.value.to_str().to_owned()) } -pub fn check_file_unwrap(db: &dyn Db, file: File) -> Vec { +pub fn check_file_unwrap(db: &dyn Db, file: PythonFile<'_>) -> Vec { check_file(db, file) .map(<[ruff_db::diagnostic::Diagnostic]>::into_vec) .unwrap_or_else(|error| vec![error]) } -pub fn check_file(db: &dyn Db, file: File) -> Result, Diagnostic> { +pub fn check_file(db: &dyn Db, file: PythonFile<'_>) -> Result, Diagnostic> { + let source_file = file.file(db); let mut diagnostics: Vec = Vec::new(); // Abort checking if there are IO errors. - let source = source_text(db, file); + let source = source_text(db, source_file); if let Some(read_error) = source.read_error() { return Err(IOErrorDiagnostic { - file, + file: source_file, error: read_error.clone(), } .to_diagnostic()); @@ -198,11 +198,11 @@ pub fn check_file(db: &dyn Db, file: File) -> Result, Diagnost parsed_ref .errors() .iter() - .map(|error| Diagnostic::invalid_syntax(file, &error.error, error)), + .map(|error| Diagnostic::invalid_syntax(source_file, &error.error, error)), ); diagnostics.extend(parsed_ref.unsupported_syntax_errors().iter().map(|error| { - let mut error = Diagnostic::invalid_syntax(file, error, error); + let mut error = Diagnostic::invalid_syntax(source_file, error, error); add_inferred_python_version_hint_to_diagnostic(db, &mut error, "parsing syntax"); error })); diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 70779da63f..d07847b0c3 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -1,5 +1,6 @@ +use crate::ProgramEnvironment; use itertools::Either; -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_index::IndexSlice; use ruff_python_ast::PythonVersion; use ty_module_resolver::{ @@ -15,7 +16,7 @@ use crate::types::{ DynamicType, KnownClass, MemberLookupPolicy, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType, binding_type, inferred_declaration, is_discarded_dict_key_assignment, }; -use crate::{Db, FxIndexSet, FxOrderSet, Program}; +use crate::{Db, FxIndexSet, FxOrderSet}; use ty_python_core::definition::{Definition, DefinitionKind, DefinitionState}; use ty_python_core::narrowing_constraints::ScopedNarrowingConstraint; use ty_python_core::place::ScopedPlaceId; @@ -88,10 +89,15 @@ pub(crate) enum PublicTypePolicy { impl PublicTypePolicy { /// Apply the public-type policy to the raw type. - fn apply_if_needed<'db>(self, db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn apply_if_needed<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Type<'db> { match self { Self::Raw => ty, - Self::Promote => ty.promote(db).promote_singletons(db), + Self::Promote => ty.promote(db, env).promote_singletons(db, env), } } } @@ -329,15 +335,21 @@ impl<'db> Place<'db> { /// Try to call `__get__(None, owner)` on the type of this place (not on the meta type). /// If it succeeds, return the `__get__` return type. Otherwise, returns the original place. /// This is used to resolve (potential) descriptor attributes. - pub(crate) fn try_call_dunder_get(self, db: &'db dyn Db, owner: Type<'db>) -> Place<'db> { + pub(crate) fn try_call_dunder_get( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + owner: Type<'db>, + ) -> Place<'db> { match self { Place::Defined( place @ DefinedPlace { ty: Type::Union(union), .. }, - ) => union.map_with_boundness(db, |elem| { - Place::Defined(DefinedPlace { ty: *elem, ..place }).try_call_dunder_get(db, owner) + ) => union.map_with_boundness(db, env, |elem| { + Place::Defined(DefinedPlace { ty: *elem, ..place }) + .try_call_dunder_get(db, env, owner) }), Place::Defined( @@ -345,13 +357,14 @@ impl<'db> Place<'db> { ty: Type::Intersection(intersection), .. }, - ) => intersection.map_with_boundness(db, |elem| { - Place::Defined(DefinedPlace { ty: *elem, ..place }).try_call_dunder_get(db, owner) + ) => intersection.map_with_boundness(db, env, |elem| { + Place::Defined(DefinedPlace { ty: *elem, ..place }) + .try_call_dunder_get(db, env, owner) }), Place::Defined(defined) => { if let Some((dunder_get_return_ty, _)) = - defined.ty.try_call_dunder_get(db, None, owner) + defined.ty.try_call_dunder_get(db, env, None, owner) { Place::Defined(DefinedPlace { ty: dunder_get_return_ty, @@ -411,14 +424,15 @@ impl<'db> LookupError<'db> { pub(crate) fn or_fall_back_to( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, fallback: PlaceAndQualifiers<'db>, ) -> LookupResult<'db> { - let fallback = fallback.into_lookup_result(db); + let fallback = fallback.into_lookup_result(db, env); match (&self, &fallback) { (LookupError::Undefined(_), _) => fallback, (LookupError::PossiblyUndefined { .. }, Err(LookupError::Undefined(_))) => Err(self), (LookupError::PossiblyUndefined(ty), Ok(ty2)) => Ok(TypeAndQualifiers::new( - UnionType::from_two_elements(db, ty.inner_type(), ty2.inner_type()), + UnionType::from_two_elements(db, env, ty.inner_type(), ty2.inner_type()), ty.origin().merge(ty2.origin()), ty.qualifiers().union(ty2.qualifiers()), ) @@ -426,7 +440,7 @@ impl<'db> LookupError<'db> { (LookupError::PossiblyUndefined(ty), Err(LookupError::PossiblyUndefined(ty2))) => { Err(LookupError::PossiblyUndefined( TypeAndQualifiers::new( - UnionType::from_two_elements(db, ty.inner_type(), ty2.inner_type()), + UnionType::from_two_elements(db, env, ty.inner_type(), ty2.inner_type()), ty.origin().merge(ty2.origin()), ty.qualifiers().union(ty2.qualifiers()), ) @@ -472,7 +486,7 @@ pub(crate) fn symbol<'db>( /// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports). pub(crate) fn explicit_global_symbol<'db>( db: &'db dyn Db, - file: File, + file: PythonFile<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { symbol_impl( @@ -494,11 +508,13 @@ pub(crate) fn explicit_global_symbol<'db>( #[allow(unused)] pub(crate) fn global_symbol<'db>( db: &'db dyn Db, - file: File, + file: PythonFile<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { - explicit_global_symbol(db, file, name) - .or_fall_back_to(db, || module_type_implicit_global_symbol(db, file, name)) + let env = ProgramEnvironment::from_file(file); + explicit_global_symbol(db, file, name).or_fall_back_to(db, &env, || { + module_type_implicit_global_symbol(db, file, name) + }) } /// Infers the public type of an imported symbol. @@ -509,10 +525,15 @@ pub(crate) fn global_symbol<'db>( /// `None` should be passed for the `file` parameter if looking up a symbol on a namespace package. pub(crate) fn imported_symbol<'db>( db: &'db dyn Db, - file: Option, + env: &ProgramEnvironment<'db>, + file: Option>, name: &str, requires_explicit_reexport: Option, ) -> PlaceAndQualifiers<'db> { + if let Some(file) = file { + debug_assert_eq!(file.python_version(db), env.python_version(db)); + } + // If it's not found in the global scope, check if it's present as an instance on // `types.ModuleType` or `builtins.object`. // @@ -530,7 +551,7 @@ pub(crate) fn imported_symbol<'db>( // module we're dealing with. file.map(|file| { let requires_explicit_reexport = requires_explicit_reexport.unwrap_or_else(|| { - if file.is_stub(db) { + if file.file(db).is_stub(db) { RequiresExplicitReExport::Yes } else { RequiresExplicitReExport::No @@ -546,7 +567,7 @@ pub(crate) fn imported_symbol<'db>( ) }) .unwrap_or_default() - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { match name { "__file__" => { // We special-case `__file__` here because we know that for a successfully imported @@ -561,16 +582,16 @@ pub(crate) fn imported_symbol<'db>( // do not attempt to detect this; we just infer `str` still. This matches the // behaviour of other major type checkers. if file.is_some() { - Place::bound(KnownClass::Str.to_instance(db)).into() + Place::bound(KnownClass::Str.to_instance(db, env)).into() } else { - Place::bound(Type::none(db)).into() + Place::bound(Type::none(db, env)).into() } } "__getattr__" => Place::Undefined.into(), "__builtins__" => Place::bound(Type::any()).into(), _ => KnownClass::ModuleType - .to_instance(db) - .member_lookup_with_policy(db, name, MemberLookupPolicy::NO_GETATTR_LOOKUP), + .to_instance(db, env) + .member_lookup_with_policy(db, env, name, MemberLookupPolicy::NO_GETATTR_LOOKUP), } }) } @@ -582,31 +603,43 @@ pub(crate) fn imported_symbol<'db>( /// Note that this function is only intended for use in the context of the builtins *namespace* /// and should not be used when a symbol is being explicitly imported from the `builtins` module /// (e.g. `from builtins import int`). -pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { - let resolver = |module: Module<'_>| { - let file = module.file(db)?; +pub(crate) fn builtins_symbol<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + symbol: &str, +) -> PlaceAndQualifiers<'db> { + let python_version = env.python_version(db); + let resolver = |module: Module<'db>| { + let python_file = module.python_file(db)?; let found_symbol = symbol_impl( db, - global_scope(db, file), + global_scope(db, python_file), symbol, RequiresExplicitReExport::Yes, ConsideredDefinitions::EndOfScope, ) - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { // We're looking up in the builtins namespace and not the module, so we should // do the normal lookup in `types.ModuleType` and not the special one as in // `imported_symbol`. - module_type_implicit_global_symbol(db, file, symbol) + module_type_implicit_global_symbol(db, python_file, symbol) }); // If this symbol is not present in project-level builtins, search in the default ones. found_symbol .ignore_possibly_undefined() .map(|_| found_symbol) }; - resolve_module_confident(db, &ModuleName::new_static("__builtins__").unwrap()) - .and_then(&resolver) - .or_else(|| resolve_module_confident(db, &KnownModule::Builtins.name()).and_then(resolver)) - .unwrap_or_default() + resolve_module_confident( + db, + python_version, + &ModuleName::new_static("__builtins__").unwrap(), + ) + .and_then(&resolver) + .or_else(|| { + resolve_module_confident(db, python_version, &KnownModule::Builtins.name()) + .and_then(resolver) + }) + .unwrap_or_default() } /// Lookup the type of `symbol` in a given known module. @@ -614,13 +647,14 @@ pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQua /// Returns `Place::Undefined` if the given known module cannot be resolved for some reason. pub(crate) fn known_module_symbol<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, known_module: KnownModule, symbol: &str, ) -> PlaceAndQualifiers<'db> { - resolve_module_confident(db, &known_module.name()) + resolve_module_confident(db, env.python_version(db), &known_module.name()) .and_then(|module| { - let file = module.file(db)?; - Some(imported_symbol(db, Some(file), symbol, None)) + let file = module.python_file(db)?; + Some(imported_symbol(db, env, Some(file), symbol, None)) }) .unwrap_or_default() } @@ -630,8 +664,12 @@ pub(crate) fn known_module_symbol<'db>( /// Returns `Place::Undefined` if the `typing` module isn't available for some reason. #[inline] #[cfg(test)] -pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> { - known_module_symbol(db, KnownModule::Typing, symbol) +pub(crate) fn typing_symbol<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + symbol: &str, +) -> PlaceAndQualifiers<'db> { + known_module_symbol(db, env, KnownModule::Typing, symbol) } /// Lookup the type of `symbol` in the `typing_extensions` module namespace. @@ -640,24 +678,32 @@ pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQuali #[inline] pub(crate) fn typing_extensions_symbol<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, symbol: &str, ) -> PlaceAndQualifiers<'db> { - known_module_symbol(db, KnownModule::TypingExtensions, symbol) + known_module_symbol(db, env, KnownModule::TypingExtensions, symbol) } /// Get the `builtins` module scope. /// /// Can return `None` if a custom typeshed is used that is missing `builtins.pyi`. -pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option> { - core_module_scope(db, KnownModule::Builtins) +pub(crate) fn builtins_module_scope<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, +) -> Option> { + core_module_scope(db, env, KnownModule::Builtins) } /// Get the scope of a core stdlib module. /// /// Can return `None` if a custom typeshed is used that is missing the core module in question. -fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option> { - let module = resolve_module_confident(db, &core_module.name())?; - Some(global_scope(db, module.file(db)?)) +fn core_module_scope<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + core_module: KnownModule, +) -> Option> { + let module = resolve_module_confident(db, env.python_version(db), &core_module.name())?; + Some(global_scope(db, module.python_file(db)?)) } /// Infer the combined type from an iterator of bindings, and return it @@ -666,10 +712,12 @@ fn core_module_scope(db: &dyn Db, core_module: KnownModule) -> Option( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, ) -> PlaceWithDefinition<'db> { place_from_bindings_impl( db, + env, bindings_with_constraints, RequiresExplicitReExport::No, None, @@ -678,11 +726,13 @@ pub(super) fn place_from_bindings<'db>( pub(super) fn place_from_bindings_with_reachability_cache<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, reachability_cache: &ReachabilityEvaluationCache<'db>, ) -> PlaceWithDefinition<'db> { place_from_bindings_impl( db, + env, bindings_with_constraints, RequiresExplicitReExport::No, Some(reachability_cache), @@ -699,18 +749,21 @@ pub(super) fn place_from_bindings_with_reachability_cache<'db>( /// [`TypeQualifiers`] that have been specified on the declaration(s). pub(crate) fn place_from_declarations<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, declarations: DeclarationsIterator<'_, 'db>, ) -> PlaceFromDeclarationsResult<'db> { - place_from_declarations_impl(db, declarations, RequiresExplicitReExport::No, None) + place_from_declarations_impl(db, env, declarations, RequiresExplicitReExport::No, None) } pub(crate) fn place_from_declarations_with_reachability_cache<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, declarations: DeclarationsIterator<'_, 'db>, reachability_cache: &ReachabilityEvaluationCache<'db>, ) -> PlaceFromDeclarationsResult<'db> { place_from_declarations_impl( db, + env, declarations, RequiresExplicitReExport::No, Some(reachability_cache), @@ -849,13 +902,17 @@ impl<'db> PlaceAndQualifiers<'db> { /// /// For places whose public type differs from their raw stored type, this applies the /// public-type policy lazily during lookup. - pub(crate) fn into_lookup_result(self, db: &'db dyn Db) -> LookupResult<'db> { + pub(crate) fn into_lookup_result( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> LookupResult<'db> { match self { PlaceAndQualifiers { place: Place::Defined(place), qualifiers, } => { - let ty = place.public_type_policy.apply_if_needed(db, place.ty); + let ty = place.public_type_policy.apply_if_needed(db, env, place.ty); let type_and_qualifiers = TypeAndQualifiers::new(ty, place.origin, qualifiers) .with_provenance(place.provenance); match place.definedness { @@ -881,9 +938,11 @@ impl<'db> PlaceAndQualifiers<'db> { pub(crate) fn unwrap_with_diagnostic( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, diagnostic_fn: impl FnOnce(LookupError<'db>) -> TypeAndQualifiers<'db>, ) -> TypeAndQualifiers<'db> { - self.into_lookup_result(db).unwrap_or_else(diagnostic_fn) + self.into_lookup_result(db, env) + .unwrap_or_else(diagnostic_fn) } /// Fallback (partially or fully) to another place if `self` is partially or fully unbound. @@ -900,16 +959,18 @@ impl<'db> PlaceAndQualifiers<'db> { pub(crate) fn or_fall_back_to( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, fallback_fn: impl FnOnce() -> PlaceAndQualifiers<'db>, ) -> Self { - self.into_lookup_result(db) - .or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn())) + self.into_lookup_result(db, env) + .or_else(|lookup_error| lookup_error.or_fall_back_to(db, env, fallback_fn())) .into() } pub(crate) fn cycle_normalized( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous_place: Self, cycle: &salsa::Cycle, ) -> Self { @@ -924,7 +985,7 @@ impl<'db> PlaceAndQualifiers<'db> { // iteration into the current result; after the first couple iterations, the same // applies to boundness and qualifiers. (Place::Defined(prev), Place::Defined(current)) => Place::Defined(DefinedPlace { - ty: current.ty.cycle_normalized(db, prev.ty, cycle), + ty: current.ty.cycle_normalized(db, env, prev.ty, cycle), definedness: if cycle.iteration() <= 1 || matches!( (prev.definedness, current.definedness), @@ -945,7 +1006,7 @@ impl<'db> PlaceAndQualifiers<'db> { // However, the handling described above may reduce the exactness of reachability analysis, // so it may be better to remove it. In that case, this branch is necessary. (Place::Undefined, Place::Defined(current)) => Place::Defined(DefinedPlace { - ty: current.ty.recursive_type_normalized(db, cycle), + ty: current.ty.recursive_type_normalized(db, env, cycle), definedness: if cycle.iteration() <= 1 { current.definedness } else { @@ -960,7 +1021,7 @@ impl<'db> PlaceAndQualifiers<'db> { Place::Undefined } else { Place::Defined(DefinedPlace { - ty: prev.ty.recursive_type_normalized(db, cycle), + ty: prev.ty.recursive_type_normalized(db, env, cycle), definedness: Definedness::PossiblyUndefined, ..prev }) @@ -981,8 +1042,9 @@ impl<'db> From> for PlaceAndQualifiers<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _, _, _, _| Place::bound(Type::divergent(id)).into(), - cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, place: PlaceAndQualifiers<'db>, _, _, _, _| { - place.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, place: PlaceAndQualifiers<'db>, scope: ScopeId<'db>, _, _, _| { + let env = ProgramEnvironment::from_scope(scope); + place.cycle_normalized(db, &env, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -994,6 +1056,7 @@ pub(crate) fn place_by_id<'db>( considered_definitions: ConsideredDefinitions, ) -> PlaceAndQualifiers<'db> { let use_def = use_def_map(db, scope); + let env = ProgramEnvironment::from_scope(scope); // If the place is declared, the public type is based on declarations; otherwise, it's based // on inference from bindings. @@ -1003,8 +1066,9 @@ pub(crate) fn place_by_id<'db>( ConsideredDefinitions::AllReachable => use_def.reachable_declarations(place_id), }; - let declared = place_from_declarations_impl(db, declarations, requires_explicit_reexport, None) - .ignore_conflicting_declarations(); + let declared = + place_from_declarations_impl(db, &env, declarations, requires_explicit_reexport, None) + .ignore_conflicting_declarations(); let all_considered_bindings = || match considered_definitions { ConsideredDefinitions::EndOfScope => use_def.end_of_scope_bindings(place_id), @@ -1015,7 +1079,7 @@ pub(crate) fn place_by_id<'db>( // inferred type, without unioning with `Unknown`, because it cannot be modified. if let Some(qualifiers) = declared.is_bare_final() { let bindings = all_considered_bindings(); - return place_from_bindings_impl(db, bindings, requires_explicit_reexport, None) + return place_from_bindings_impl(db, &env, bindings, requires_explicit_reexport, None) .place .with_qualifiers(qualifiers); } @@ -1035,7 +1099,9 @@ pub(crate) fn place_by_id<'db>( qualifiers, } if qualifiers.contains(TypeQualifiers::CLASS_VAR) => { let bindings = all_considered_bindings(); - match place_from_bindings_impl(db, bindings, requires_explicit_reexport, None).place { + match place_from_bindings_impl(db, &env, bindings, requires_explicit_reexport, None) + .place + { Place::Defined(DefinedPlace { ty: inferred, origin, @@ -1043,7 +1109,7 @@ pub(crate) fn place_by_id<'db>( provenance: inferred_provenance, .. }) => Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements(db, Type::unknown(), inferred), + ty: UnionType::from_two_elements(db, &env, Type::unknown(), inferred), origin, definedness: boundness, public_type_policy: PublicTypePolicy::Raw, @@ -1083,7 +1149,8 @@ pub(crate) fn place_by_id<'db>( } => { let bindings = all_considered_bindings(); let boundness_analysis = bindings.boundness_analysis(); - let inferred = place_from_bindings_impl(db, bindings, requires_explicit_reexport, None); + let inferred = + place_from_bindings_impl(db, &env, bindings, requires_explicit_reexport, None); let place = match inferred.place { // Place is possibly undeclared and definitely unbound @@ -1107,7 +1174,7 @@ pub(crate) fn place_by_id<'db>( provenance: inferred_provenance, .. }) => Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements(db, inferred_ty, declared_ty), + ty: UnionType::from_two_elements(db, &env, inferred_ty, declared_ty), origin, definedness: if boundness_analysis == BoundnessAnalysis::AssumeBound { Definedness::AlwaysDefined @@ -1129,7 +1196,8 @@ pub(crate) fn place_by_id<'db>( let bindings = all_considered_bindings(); let boundness_analysis = bindings.boundness_analysis(); let mut inferred = - place_from_bindings_impl(db, bindings, requires_explicit_reexport, None).place; + place_from_bindings_impl(db, &env, bindings, requires_explicit_reexport, None) + .place; if boundness_analysis == BoundnessAnalysis::AssumeBound { if let Place::Defined(defined) = inferred { @@ -1280,7 +1348,8 @@ fn symbol_impl<'db>( let _span = tracing::trace_span!("symbol", ?name).entered(); let is_known_module = |known_module| { - file_to_module(db, scope.file(db)).is_some_and(|module| module.is_known(db, known_module)) + file_to_module(db, scope.python_file(db)) + .is_some_and(|module| module.is_known(db, known_module)) }; // Check the symbol name first to avoid a module-resolution query for every symbol lookup. @@ -1289,7 +1358,7 @@ fn symbol_impl<'db>( "version_info" => { return Place::bound(Type::sys_version_info()).into(); } - "platform" => match Program::get(db).python_platform(db) { + "platform" => match ty_python_core::program::Program::get(db).python_platform(db) { crate::PythonPlatform::Identifier(platform) => { return Place::bound(Type::string_literal(db, platform.as_str())).into(); } @@ -1302,7 +1371,7 @@ fn symbol_impl<'db>( } if name == "name" && is_known_module(KnownModule::Os) { - match Program::get(db).python_platform(db) { + match ty_python_core::program::Program::get(db).python_platform(db) { crate::PythonPlatform::Identifier(platform) => { // In CPython, `os.name` is `"nt"` on Windows and `"posix"` otherwise. let os_name = if platform == "win32" { "nt" } else { "posix" }; @@ -1331,7 +1400,9 @@ fn symbol_impl<'db>( /// Pre-computed reachability analysis for loop-back bindings in a loop header. #[salsa::tracked( returns(clone), - cycle_initial=|db, _, definition| loop_header_reachability_impl(db, definition, true), + cycle_initial=|db, _, definition: Definition<'db>| { + loop_header_reachability_impl(db, definition, true) + }, cycle_fn=loop_header_reachability_cycle_recover, heap_size = ruff_memory_usage::heap_size, )] @@ -1375,7 +1446,6 @@ fn loop_header_reachability_impl<'db>( let live_bindings: Vec<_> = loop_header.bindings_for_place(place).collect(); let use_exact_reachability = use_def.reachability_constraints().used_interiors().len() <= MAX_EXACT_LOOP_HEADER_REACHABILITY_NODES; - for live_binding in live_bindings { let reachability = if is_cycle_initial { Truthiness::Ambiguous @@ -1466,6 +1536,7 @@ pub(crate) struct ReachableLoopBinding<'db> { /// access any AST nodes from the file containing the declarations. fn place_from_bindings_impl<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>, requires_explicit_reexport: RequiresExplicitReExport, reachability_cache: Option<&ReachabilityEvaluationCache<'db>>, @@ -1634,7 +1705,7 @@ fn place_from_bindings_impl<'db>( provenance = provenance.or(Provenance::SingleDefinition(binding)); let binding_ty = binding_type(db, binding); Some(( - narrowing_constraint.narrow(db, binding_ty, binding.place(db)), + narrowing_constraint.narrow(db, env, binding_ty, binding.place(db)), static_reachability, )) }, @@ -1642,7 +1713,7 @@ fn place_from_bindings_impl<'db>( let place = if let Some((first, first_reachability)) = types.next() { let ty = if let Some((second, second_reachability)) = types.next() { - let mut builder = PublicTypeBuilder::new(db); + let mut builder = PublicTypeBuilder::new(db, env); builder.add(first, first_reachability); builder.add(second, second_reachability); @@ -1715,11 +1786,11 @@ struct PublicTypeBuilder<'db> { } impl<'db> PublicTypeBuilder<'db> { - fn new(db: &'db dyn Db) -> Self { + fn new(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { PublicTypeBuilder { db, queue: None, - builder: UnionBuilder::new(db), + builder: UnionBuilder::new(db, env), } } @@ -1734,10 +1805,11 @@ impl<'db> PublicTypeBuilder<'db> { } fn add(&mut self, element: Type<'db>, reachability: Truthiness) -> bool { + let db = self.db; match element { Type::FunctionLiteral(function) => { - let last_definition = function.literal(self.db).last_definition; - if last_definition.is_overload(self.db) { + let last_definition = function.literal(db).last_definition; + if last_definition.is_overload(db) { // Distinct overloaded function values can be assigned to the same public // symbol in separate branches. Preserve the queued value unless the next // overload belongs to the same place. @@ -1745,7 +1817,7 @@ impl<'db> PublicTypeBuilder<'db> { let Type::FunctionLiteral(queued_function) = queued else { return false; }; - function.has_same_place_as(self.db, queued_function) + function.has_same_place_as(db, queued_function) }) { self.drain_queue(); } @@ -1761,8 +1833,8 @@ impl<'db> PublicTypeBuilder<'db> { let Type::FunctionLiteral(queued_function) = queued else { return false; }; - let queued_definition = queued_function.last_definition(self.db); - function.contains_definition(self.db, queued_definition) + let queued_definition = queued_function.last_definition(db); + function.contains_definition(db, queued_definition) }) { self.queue = None; @@ -1797,21 +1869,29 @@ struct DeclaredTypeBuilder<'db> { } impl<'db> DeclaredTypeBuilder<'db> { - fn new(db: &'db dyn Db) -> Self { + fn new(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { DeclaredTypeBuilder { - inner: PublicTypeBuilder::new(db), + inner: PublicTypeBuilder::new(db, env), qualifiers: TypeQualifiers::empty(), first_type: None, conflicting_types: FxOrderSet::default(), } } - fn add(&mut self, element: TypeAndQualifiers<'db>, reachability: Truthiness) { + fn add( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + element: TypeAndQualifiers<'db>, + reachability: Truthiness, + ) { + debug_assert!(std::ptr::eq(db, self.inner.db)); + let element_ty = element.inner_type(); if self.inner.add(element_ty, reachability) { if let Some(first_ty) = self.first_type { - if !first_ty.is_equivalent_to(self.inner.db, element_ty) { + if !first_ty.is_equivalent_to(db, env, element_ty) { self.conflicting_types.insert(element_ty); } } else { @@ -1848,6 +1928,7 @@ impl<'db> DeclaredTypeBuilder<'db> { /// access any AST nodes from the file containing the declarations. fn place_from_declarations_impl<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, declarations_iterator: DeclarationsIterator<'_, 'db>, requires_explicit_reexport: RequiresExplicitReExport, reachability_cache: Option<&ReachabilityEvaluationCache<'db>>, @@ -1919,11 +2000,11 @@ fn place_from_declarations_impl<'db>( if let Some((first, first_reachability)) = types.next() { let (declared, conflicting) = if let Some((second, second_reachability)) = types.next() { - let mut builder = DeclaredTypeBuilder::new(db); - builder.add(first, first_reachability); - builder.add(second, second_reachability); + let mut builder = DeclaredTypeBuilder::new(db, env); + builder.add(db, env, first, first_reachability); + builder.add(db, env, second, second_reachability); for (element, reachability) in types { - builder.add(element, reachability); + builder.add(db, env, element, reachability); } builder.build() } else { @@ -1975,7 +2056,7 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { // At this point, the definition should either be an `import` or `from ... import` statement. // This is because the default value of `is_reexported` is `true` for any other kind of // definition. - let Some(all_names) = dunder_all_names(db, definition.file(db)) else { + let Some(all_names) = dunder_all_names(db, definition.python_file(db)) else { return false; }; let table = place_table(db, definition.scope(db)); @@ -1985,18 +2066,18 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { } pub(crate) mod implicit_globals { - use ruff_db::files::File; + use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_python_ast::name::Name; use ty_module_resolver::KnownModule; - use crate::Program; use crate::db::Db; use crate::module_docstring; use crate::place::{Definedness, PlaceAndQualifiers}; use crate::reachability::evaluate_reachability; use crate::types::{KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type}; + use crate::{Program, ProgramEnvironment}; use ruff_python_ast::PythonVersion; use ty_python_core::definition::{DefinitionKind, DefinitionState}; use ty_python_core::scope::{NodeWithScopeRef, ScopeId}; @@ -2014,14 +2095,15 @@ pub(crate) mod implicit_globals { module_scope: ScopeId<'db>, name: &str, ) -> Option> { - let file = module_scope.file(db); + let python_file = module_scope.python_file(db); + let file = python_file.file(db); if !file.path(db).is_vendored_path() { return None; } let symbol_id = place_table(db, module_scope).symbol_id(name)?; let use_def = use_def_map(db, module_scope); - let module = parsed_module(db, file).load(db); - let index = semantic_index(db, file); + let module = parsed_module(db, python_file).load(db); + let index = semantic_index(db, python_file); let mut body_scope = None; for binding in use_def.end_of_scope_symbol_bindings(symbol_id) { @@ -2041,7 +2123,7 @@ pub(crate) mod implicit_globals { }; let class_scope = index .node_scope(NodeWithScopeRef::Class(class.node(&module))) - .to_scope_id(db, file); + .to_scope_id(db, python_file); if body_scope.is_some_and(|body_scope| body_scope != class_scope) { return None; } @@ -2052,27 +2134,40 @@ pub(crate) mod implicit_globals { } /// Return the body scope of the canonical `types.ModuleType` class. + fn module_type_body_scope<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + module_type_body_scope_inner(db, env.program(db), ()) + } + #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] - fn module_type_body_scope(db: &dyn Db) -> Option> { - let module_scope = core_module_scope(db, KnownModule::Types)?; + fn module_type_body_scope_inner( + db: &dyn Db, + program: Program, + _: (), // FIXME: Remove once `Program` is a Salsa-interned struct. + ) -> Option> { + let env = ProgramEnvironment::from_program(program); + let module_scope = core_module_scope(db, &env, KnownModule::Types)?; try_vendored_class_scope(db, module_scope, "ModuleType").or_else(|| { KnownClass::ModuleType - .try_to_class_literal(db) + .try_to_class_literal(db, &env) .map(|class| class.body_scope(db)) }) } pub(crate) fn module_type_implicit_global_declaration<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { - if !module_type_symbols(db) + if !module_type_symbols(db, env) .iter() .any(|module_type_member| module_type_member == name) { return Place::Undefined.into(); } - let Some(module_type_scope) = module_type_body_scope(db) else { + let Some(module_type_scope) = module_type_body_scope(db, env) else { return Place::Undefined.into(); }; let place_table = place_table(db, module_type_scope); @@ -2081,6 +2176,7 @@ pub(crate) mod implicit_globals { }; place_from_declarations( db, + env, use_def_map(db, module_type_scope).end_of_scope_symbol_declarations(symbol_id), ) .ignore_conflicting_declarations() @@ -2102,14 +2198,15 @@ pub(crate) mod implicit_globals { /// global scope if they're being imported **from a different file**. pub(crate) fn module_type_implicit_global_symbol<'db>( db: &'db dyn Db, - file: File, + file: PythonFile<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { + let env = ProgramEnvironment::from_file(file); match name { // We special-case `__file__` here because we know that for an internal implicit global // lookup in a Python module, it is always a string, even though typeshed says `str | // None`. - "__file__" => Place::bound(KnownClass::Str.to_instance(db)).into(), + "__file__" => Place::bound(KnownClass::Str.to_instance(db, &env)).into(), // We special-case `__doc__` because a module with a literal docstring has `__doc__` // set to that string at runtime. We only narrow when a docstring is present: `__doc__` @@ -2118,37 +2215,37 @@ pub(crate) mod implicit_globals { // Docstrings are stripped in `-OO` optimized mode, but here we assume that the // existence of an actual docstring AND the usage of `__doc__` is reason enough to // believe that it will exist at runtime. - Place::bound(KnownClass::Str.to_instance(db)).into() + Place::bound(KnownClass::Str.to_instance(db, &env)).into() } "__builtins__" => Place::bound(Type::any()).into(), - "__debug__" => Place::bound(KnownClass::Bool.to_instance(db)).into(), + "__debug__" => Place::bound(KnownClass::Bool.to_instance(db, &env)).into(), // Created lazily by the warnings machinery; may be absent. // Model as possibly-unbound to avoid false negatives. - "__warningregistry__" => { - Place::Defined( - DefinedPlace::new(KnownClass::Dict.to_specialized_instance( - db, - &[Type::any(), KnownClass::Int.to_instance(db)], - )) - .with_definedness(Definedness::PossiblyUndefined), - ) - .into() - } + "__warningregistry__" => Place::Defined( + DefinedPlace::new(KnownClass::Dict.to_specialized_instance( + db, + &env, + &[Type::any(), KnownClass::Int.to_instance(db, &env)], + )) + .with_definedness(Definedness::PossiblyUndefined), + ) + .into(), // Marked as possibly-unbound as it is only present in the module namespace // if at least one global symbol is annotated in the module. - "__annotate__" if Program::get(db).python_version(db) >= PythonVersion::PY314 => { + "__annotate__" if env.python_version(db) >= PythonVersion::PY314 => { let signature = Signature::new( Parameters::standard([Parameter::positional_only(Some(Name::new_static( "format", ))) - .with_annotated_type(KnownClass::Int.to_instance(db))]), + .with_annotated_type(KnownClass::Int.to_instance(db, &env))]), KnownClass::Dict.to_specialized_instance( db, - &[KnownClass::Str.to_instance(db), Type::any()], + &env, + &[KnownClass::Str.to_instance(db, &env), Type::any()], ), ); Place::Defined( @@ -2163,16 +2260,22 @@ pub(crate) mod implicit_globals { // type, since it has the same end result. The reason to only call `.member()` on `ModuleType` // when absolutely necessary is that this function is used in a very hot path (name resolution // in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation. - _ if module_type_symbols(db) - .iter() - .any(|module_type_member| &**module_type_member == name) => - { + _ => { + if !module_type_symbols(db, &env) + .iter() + .any(|module_type_member| &**module_type_member == name) + { + return Place::Undefined.into(); + } KnownClass::ModuleType - .to_instance(db) - .member_lookup_with_policy(db, name, MemberLookupPolicy::NO_GETATTR_LOOKUP) + .to_instance(db, &env) + .member_lookup_with_policy( + db, + &env, + name, + MemberLookupPolicy::NO_GETATTR_LOOKUP, + ) } - - _ => Place::Undefined.into(), } } @@ -2213,13 +2316,25 @@ pub(crate) mod implicit_globals { .collect() } + fn module_type_symbols<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> &'db [ast::name::Name] { + module_type_symbols_inner(db, env.program(db), ()) + } + #[salsa::tracked( returns(deref), - cycle_initial=|_, _| smallvec::SmallVec::default(), + cycle_initial=|_, _, _, ()| smallvec::SmallVec::default(), heap_size=ruff_memory_usage::heap_size )] - fn module_type_symbols(db: &dyn Db) -> smallvec::SmallVec<[ast::name::Name; 8]> { - let Some(module_type_scope) = module_type_body_scope(db) else { + fn module_type_symbols_inner( + db: &dyn Db, + program: Program, + _: (), // FIXME: Remove once `Program` is a Salsa-interned struct. + ) -> smallvec::SmallVec<[ast::name::Name; 8]> { + let env = ProgramEnvironment::from_program(program); + let Some(module_type_scope) = module_type_body_scope(db, &env) else { // The most likely way we get here is if a user specified a `--custom-typeshed-dir` // without a resolvable `ModuleType` class in the `stdlib/types.pyi` stub. return smallvec::SmallVec::default(); @@ -2232,17 +2347,18 @@ pub(crate) mod implicit_globals { /// This is used for completions in the global scope of a module. It returns /// the correct types for special-cased symbols like `__file__` (which is `str` /// for the current module, not `str | None`). - pub(crate) fn all_implicit_module_globals( - db: &dyn Db, - file: File, - ) -> impl Iterator)> + '_ { + pub(crate) fn all_implicit_module_globals<'db>( + db: &'db dyn Db, + file: PythonFile<'db>, + ) -> impl Iterator)> + 'db { // Special-cased implicit globals that are not in `module_type_symbols` let special_cased = ["__builtins__", "__debug__", "__warningregistry__"] .into_iter() .map(Name::new_static); // All symbols from ModuleType (already includes `__file__`, `__name__`, etc.) - let module_type_syms = module_type_symbols(db).iter().cloned(); + let env = ProgramEnvironment::from_file(file); + let module_type_syms = module_type_symbols(db, &env).iter().cloned(); // Combine and map to (name, type) pairs special_cased @@ -2262,7 +2378,9 @@ pub(crate) mod implicit_globals { #[test] fn module_type_symbols_includes_declared_types_but_not_referenced_types() { let db = setup_db(); - let symbol_names = module_type_symbols(&db); + let db = &db; + let env = db.program_environment(); + let symbol_names = module_type_symbols(db, &env); let dunder_name_symbol_name = ast::name::Name::new_static("__name__"); assert!(symbol_names.contains(&dunder_name_symbol_name)); @@ -2283,21 +2401,23 @@ pub(crate) mod implicit_globals { /// See pub(crate) fn class_body_implicit_symbol<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { match name { - "__qualname__" => Place::bound(KnownClass::Str.to_instance(db)).into(), - "__module__" => Place::bound(KnownClass::Str.to_instance(db)).into(), + "__qualname__" => Place::bound(KnownClass::Str.to_instance(db, env)).into(), + "__module__" => Place::bound(KnownClass::Str.to_instance(db, env)).into(), // __doc__ is `str` if there's a docstring, `None` if there isn't "__doc__" => Place::bound(UnionType::from_two_elements( db, - KnownClass::Str.to_instance(db), - Type::none(db), + env, + KnownClass::Str.to_instance(db, env), + Type::none(db, env), )) .into(), // __firstlineno__ was added in Python 3.13 - "__firstlineno__" if Program::get(db).python_version(db) >= PythonVersion::PY313 => { - Place::bound(KnownClass::Int.to_instance(db)).into() + "__firstlineno__" if env.python_version(db) >= PythonVersion::PY313 => { + Place::bound(KnownClass::Int.to_instance(db, env)).into() } _ => Place::Undefined.into(), } @@ -2340,7 +2460,7 @@ pub(crate) enum ConsideredDefinitions { #[cfg(test)] mod tests { use super::*; - use crate::db::tests::setup_db; + use crate::db::tests::{TestDb, setup_db}; #[test] fn test_symbol_or_fall_back_to() { @@ -2348,6 +2468,8 @@ mod tests { use TypeOrigin::Inferred; let db = setup_db(); + let db = &db; + let env = db.program_environment(); let ty1 = Type::int_literal(1); let ty2 = Type::int_literal(2); @@ -2396,22 +2518,22 @@ mod tests { }; // Start from an unbound symbol - assert_eq!(unbound().or_fall_back_to(&db, unbound), unbound()); + assert_eq!(unbound().or_fall_back_to(db, &env, unbound), unbound()); assert_eq!( - unbound().or_fall_back_to(&db, possibly_unbound_ty1), + unbound().or_fall_back_to(db, &env, possibly_unbound_ty1), possibly_unbound_ty1() ); - assert_eq!(unbound().or_fall_back_to(&db, bound_ty1), bound_ty1()); + assert_eq!(unbound().or_fall_back_to(db, &env, bound_ty1), bound_ty1()); // Start from a possibly unbound symbol assert_eq!( - possibly_unbound_ty1().or_fall_back_to(&db, unbound), + possibly_unbound_ty1().or_fall_back_to(db, &env, unbound), possibly_unbound_ty1() ); assert_eq!( - possibly_unbound_ty1().or_fall_back_to(&db, possibly_unbound_ty2), + possibly_unbound_ty1().or_fall_back_to(db, &env, possibly_unbound_ty2), Place::Defined(DefinedPlace { - ty: UnionType::from_elements(&db, [ty1, ty2]), + ty: UnionType::from_elements(db, &env, [ty1, ty2]), origin: Inferred, definedness: PossiblyUndefined, public_type_policy: PublicTypePolicy::Raw, @@ -2420,9 +2542,9 @@ mod tests { .into() ); assert_eq!( - possibly_unbound_ty1().or_fall_back_to(&db, bound_ty2), + possibly_unbound_ty1().or_fall_back_to(db, &env, bound_ty2), Place::Defined(DefinedPlace { - ty: UnionType::from_elements(&db, [ty1, ty2]), + ty: UnionType::from_elements(db, &env, [ty1, ty2]), origin: Inferred, definedness: AlwaysDefined, public_type_policy: PublicTypePolicy::Raw, @@ -2432,16 +2554,19 @@ mod tests { ); // Start from a definitely bound symbol - assert_eq!(bound_ty1().or_fall_back_to(&db, unbound), bound_ty1()); + assert_eq!(bound_ty1().or_fall_back_to(db, &env, unbound), bound_ty1()); assert_eq!( - bound_ty1().or_fall_back_to(&db, possibly_unbound_ty2), + bound_ty1().or_fall_back_to(db, &env, possibly_unbound_ty2), + bound_ty1() + ); + assert_eq!( + bound_ty1().or_fall_back_to(db, &env, bound_ty2), bound_ty1() ); - assert_eq!(bound_ty1().or_fall_back_to(&db, bound_ty2), bound_ty1()); } #[track_caller] - fn assert_bound_string_symbol<'db>(db: &'db dyn Db, symbol: Place<'db>) { + fn assert_bound_string_symbol<'db>(db: &'db TestDb, symbol: Place<'db>) { assert!(matches!( symbol, Place::Defined(DefinedPlace { @@ -2450,25 +2575,37 @@ mod tests { .. }) )); - assert_eq!(symbol.expect_type(), KnownClass::Str.to_instance(db)); + assert_eq!( + symbol.expect_type(), + KnownClass::Str.to_instance(db, &db.program_environment()) + ); } #[test] fn implicit_builtin_globals() { let db = setup_db(); - assert_bound_string_symbol(&db, builtins_symbol(&db, "__name__").place); + assert_bound_string_symbol( + &db, + builtins_symbol(&db, &db.program_environment(), "__name__").place, + ); } #[test] fn implicit_typing_globals() { let db = setup_db(); - assert_bound_string_symbol(&db, typing_symbol(&db, "__name__").place); + assert_bound_string_symbol( + &db, + typing_symbol(&db, &db.program_environment(), "__name__").place, + ); } #[test] fn implicit_typing_extensions_globals() { let db = setup_db(); - assert_bound_string_symbol(&db, typing_extensions_symbol(&db, "__name__").place); + assert_bound_string_symbol( + &db, + typing_extensions_symbol(&db, &db.program_environment(), "__name__").place, + ); } #[test] @@ -2476,7 +2613,7 @@ mod tests { let db = setup_db(); assert_bound_string_symbol( &db, - known_module_symbol(&db, KnownModule::Sys, "__name__").place, + known_module_symbol(&db, &db.program_environment(), KnownModule::Sys, "__name__").place, ); } } diff --git a/crates/ty_python_semantic/src/pull_types.rs b/crates/ty_python_semantic/src/pull_types.rs index d2c1317b9f..7970ea9b8c 100644 --- a/crates/ty_python_semantic/src/pull_types.rs +++ b/crates/ty_python_semantic/src/pull_types.rs @@ -4,12 +4,12 @@ //! (Mdtest uses the `pull_types` function via the `ty_test` crate.) use crate::{Db, HasType, SemanticModel}; -use ruff_db::{files::File, parsed::parsed_module}; +use ruff_db::{PythonFile, parsed::parsed_module}; use ruff_python_ast::{ self as ast, visitor::source_order, visitor::source_order::SourceOrderVisitor, }; -pub fn pull_types(db: &dyn Db, file: File) { +pub fn pull_types(db: &dyn Db, file: PythonFile<'_>) { let mut visitor = PullTypesVisitor::new(db, file); let ast = parsed_module(db, file).load(db); @@ -22,7 +22,7 @@ struct PullTypesVisitor<'db> { } impl<'db> PullTypesVisitor<'db> { - fn new(db: &'db dyn Db, file: File) -> Self { + fn new(db: &'db dyn Db, file: PythonFile<'db>) -> Self { Self { model: SemanticModel::new(db, file), } diff --git a/crates/ty_python_semantic/src/reachability.rs b/crates/ty_python_semantic/src/reachability.rs index 032f8d02ea..6752797ad9 100644 --- a/crates/ty_python_semantic/src/reachability.rs +++ b/crates/ty_python_semantic/src/reachability.rs @@ -193,6 +193,7 @@ //! [Kleene]: //! [bdd]: https://en.wikipedia.org/wiki/Binary_decision_diagram +use crate::ProgramEnvironment; use std::cell::RefCell; use crate::{ @@ -238,8 +239,9 @@ use ty_python_core::{ #[salsa::tracked( returns(copy), cycle_initial = |_, id, _, _| Type::divergent(id), - cycle_fn = |db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { - result.cycle_normalized(db, *previous, cycle) + cycle_fn = |db: &'db dyn Db, cycle, previous: &Type<'db>, result: Type<'db>, predicate: PatternPredicate<'db>, _| { + let env = ProgramEnvironment::from_scope(predicate.subject(db).scope(db)); + result.cycle_normalized(db, &env, *previous, cycle) }, heap_size = ruff_memory_usage::heap_size )] @@ -268,8 +270,9 @@ pub(crate) fn type_narrowed_by_previous_patterns<'db>( #[salsa::tracked( returns(copy), cycle_initial = |_, id, _, _| Type::divergent(id), - cycle_fn = |db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { - result.cycle_normalized(db, *previous, cycle) + cycle_fn = |db: &'db dyn Db, cycle, previous: &Type<'db>, result: Type<'db>, predicate: PatternPredicate<'db>, _| { + let env = ProgramEnvironment::from_scope(predicate.subject(db).scope(db)); + result.cycle_normalized(db, &env, *previous, cycle) }, heap_size = ruff_memory_usage::heap_size )] @@ -278,7 +281,8 @@ fn type_narrowed_by_pattern<'db>( predicate: PatternPredicate<'db>, subject_ty: Type<'db>, ) -> Type<'db> { - pattern_binding_fallthrough_type(db, predicate.kind(db), subject_ty) + let env = ProgramEnvironment::from_file(predicate.python_file(db)); + pattern_binding_fallthrough_type(db, &env, predicate.kind(db), subject_ty) } /// Return the enum class and canonical member names represented by an enum-literal subject type. @@ -325,7 +329,9 @@ fn enum_literal_subject_names<'db>( add_enum_literal(db, &mut enum_class, &mut names, *element)?; } } - Type::TypeAlias(alias) => return enum_literal_subject_names(db, alias.value_type(db)), + Type::TypeAlias(alias) => { + return enum_literal_subject_names(db, alias.value_type(db)); + } _ => return None, } @@ -339,10 +345,11 @@ fn enum_literal_subject_names<'db>( /// canonical member names before returning. fn enum_member_pattern_name<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, enum_class: EnumClassLiteral<'db>, kind: &PatternPredicateKind<'db>, ) -> Option { - let value_ty = definite_match_pattern_type(db, kind); + let value_ty = definite_match_pattern_type(db, env, kind); let enum_literal = value_ty.as_enum_literal()?; if enum_literal.enum_class_literal(db) != enum_class { return None; @@ -368,6 +375,7 @@ struct EnumMemberPatternCoverage { /// produces only a lower bound: it definitely matches `Color.GREEN`, but can match other members. fn enum_member_pattern_coverage<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, enum_class: EnumClassLiteral<'db>, kind: &PatternPredicateKind<'db>, ) -> EnumMemberPatternCoverage { @@ -378,7 +386,7 @@ fn enum_member_pattern_coverage<'db>( match kind { PatternPredicateKind::Or(alts) => { for alt in alts { - let alt_coverage = enum_member_pattern_coverage(db, enum_class, alt); + let alt_coverage = enum_member_pattern_coverage(db, env, enum_class, alt); coverage .definitely_matched .extend(alt_coverage.definitely_matched); @@ -386,10 +394,10 @@ fn enum_member_pattern_coverage<'db>( } } PatternPredicateKind::As(Some(inner), _) => { - return enum_member_pattern_coverage(db, enum_class, inner); + return enum_member_pattern_coverage(db, env, enum_class, inner); } _ => { - if let Some(name) = enum_member_pattern_name(db, enum_class, kind) { + if let Some(name) = enum_member_pattern_name(db, env, enum_class, kind) { coverage.definitely_matched.insert(name); } else { coverage.is_exact = false; @@ -406,11 +414,12 @@ fn enum_member_pattern_coverage<'db>( /// ambiguous because the guard can reject an otherwise matching enum member. fn analyze_enum_literal_union_pattern_predicate<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, predicate: PatternPredicate<'db>, subject_ty: Type<'db>, ) -> Option { let (enum_class, mut remaining_names) = enum_literal_subject_names(db, subject_ty)?; - let current_coverage = enum_member_pattern_coverage(db, enum_class, predicate.kind(db)); + let current_coverage = enum_member_pattern_coverage(db, env, enum_class, predicate.kind(db)); let current_names = ¤t_coverage.definitely_matched; if current_names.is_empty() { return None; @@ -425,7 +434,7 @@ fn analyze_enum_literal_union_pattern_predicate<'db>( } let previous_coverage = - enum_member_pattern_coverage(db, enum_class, previous_predicate.kind(db)); + enum_member_pattern_coverage(db, env, enum_class, previous_predicate.kind(db)); #[expect( clippy::iter_over_hash_type, reason = "set removal is independent of iteration order" @@ -468,17 +477,18 @@ fn analyze_enum_literal_union_pattern_predicate<'db>( heap_size = get_size2::GetSize::get_heap_size )] fn analyze_pattern_predicate<'db>(db: &'db dyn Db, predicate: PatternPredicate<'db>) -> Truthiness { + let env = ProgramEnvironment::from_scope(predicate.subject(db).scope(db)); let subject_ty = infer_same_file_expression_type(db, predicate.subject(db), TypeContext::default()); if let Some(truthiness) = - analyze_enum_literal_union_pattern_predicate(db, predicate, subject_ty) + analyze_enum_literal_union_pattern_predicate(db, &env, predicate, subject_ty) { return truthiness; } - let coverage_subject_ty = expand_type(db, subject_ty) - .map(|types| UnionType::from_elements(db, types)) + let coverage_subject_ty = expand_type(db, &env, subject_ty) + .map(|types| UnionType::from_elements(db, &env, types)) .unwrap_or(subject_ty); let narrowed_subject_ty = type_narrowed_by_previous_patterns(db, predicate, coverage_subject_ty); @@ -497,8 +507,13 @@ fn analyze_pattern_predicate<'db>(db: &'db dyn Db, predicate: PatternPredicate<' return Truthiness::AlwaysTrue; } - let truthiness = - analyze_single_pattern_predicate_kind(db, predicate.kind(db), narrowed_subject_ty, None); + let truthiness = analyze_single_pattern_predicate_kind( + db, + &env, + predicate.kind(db), + narrowed_subject_ty, + None, + ); if truthiness == Truthiness::AlwaysTrue && predicate.guard(db).is_some() { // Fall back to ambiguous, the guard might change the result. @@ -580,7 +595,6 @@ fn analyze_non_terminal_call_prefix<'db>( // Leave the incomplete final block demand-driven. Its reverse dependency chain is bounded by // the block size, and every eagerly analyzed call remains behind a recoverable range query. let mut remaining = call_count / NON_TERMINAL_CALL_CHUNK_SIZE; - while remaining > 0 { let level = remaining.ilog2(); let length = 1 << level; @@ -612,11 +626,12 @@ fn non_terminal_call_predicates<'db>( fn analyze_non_terminal_calls<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, predicates: &IndexSlice>, call_predicates: &[ScopedPredicateId], ) { for id in call_predicates { - analyze_single(db, &predicates[*id]); + analyze_single(db, env, &predicates[*id]); } } @@ -642,11 +657,12 @@ fn analyze_non_terminal_call_range<'db>( index: usize, ) { if level == 0 { + let env = ProgramEnvironment::from_scope(scope); let use_def = use_def_map(db, scope); let call_predicates = non_terminal_call_predicates(db, scope); let start = index * NON_TERMINAL_CALL_CHUNK_SIZE; let end = start + NON_TERMINAL_CALL_CHUNK_SIZE; - analyze_non_terminal_calls(db, use_def.predicates(), &call_predicates[start..end]); + analyze_non_terminal_calls(db, &env, use_def.predicates(), &call_predicates[start..end]); return; } @@ -717,6 +733,8 @@ fn evaluate_reachability_path<'db>( mut id: ScopedReachabilityConstraintId, mut use_checkpoint: bool, ) -> Truthiness { + let env = ProgramEnvironment::from_scope(scope); + loop { if let Some(reachability) = terminal_reachability(id) { return reachability; @@ -731,7 +749,7 @@ fn evaluate_reachability_path<'db>( return evaluate_reachability_checkpoint(db, scope, id); } - id = match analyze_single(db, &predicates[node.atom()]) { + id = match analyze_single(db, &env, &predicates[node.atom()]) { Truthiness::AlwaysTrue => node.if_true(), Truthiness::Ambiguous => node.if_ambiguous(), Truthiness::AlwaysFalse => node.if_false(), @@ -805,6 +823,7 @@ impl<'db> ReachabilityConstraintsExtension<'db> for ReachabilityConstraints { pub(crate) fn narrow_type_by_constraint<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &NarrowingConstraints, predicates: &IndexSlice>, id: ScopedNarrowingConstraint, @@ -817,10 +836,11 @@ pub(crate) fn narrow_type_by_constraint<'db>( _ => {} } - let mut projector = NarrowingProjector::new(db, constraints, predicates, place); + let mut projector = NarrowingProjector::new(db, env, constraints, predicates, place); let projected_root = projector.project(id); let mut context = ProjectedNarrowingContext { db, + env, base_ty, graph: &projector.graph, joins: projector.graph.joins(projected_root), @@ -831,13 +851,14 @@ pub(crate) fn narrow_type_by_constraint<'db>( fn apply_accumulated_narrowing<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, base_ty: Type<'db>, accumulated: Option>, ) -> Type<'db> { match accumulated { Some(constraint) => NarrowingConstraint::intersection(base_ty) .merge_constraint_and(constraint) - .evaluate_constraint_type(db), + .evaluate_constraint_type(db, env), None => base_ty, } } @@ -1039,6 +1060,7 @@ impl ProjectedNarrowingGraph<'_> { /// Removes predicates that cannot narrow one place from a narrowing constraint. struct NarrowingProjector<'a, 'db> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, constraints: &'a NarrowingConstraints, predicates: &'a IndexSlice>, place: ScopedPlaceId, @@ -1050,12 +1072,14 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { /// Creates a projector for narrowing `place`. fn new( db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, constraints: &'a NarrowingConstraints, predicates: &'a IndexSlice>, place: ScopedPlaceId, ) -> Self { Self { db, + env, constraints, predicates, place, @@ -1072,12 +1096,13 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { Option>, Option>, ) { + let db = self.db; if let Some(cached) = self.graph.predicate_constraints_cache.get(&predicate_id) { return cached.clone(); } let constraints = - infer_narrowing_constraints(self.db, self.predicates[predicate_id], self.place); + infer_narrowing_constraints(db, self.predicates[predicate_id], self.place); self.graph .predicate_constraints_cache .insert(predicate_id, constraints.clone()); @@ -1093,6 +1118,7 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { FinishNonTerminal { id: Id, branch: Id }, FinishPredicate(Id), } + let db = self.db; let mut actions = SmallVec::<[Action; 8]>::new(); actions.push(Action::Visit(root)); @@ -1120,7 +1146,7 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { Action::AnalyzeNonTerminal(id) => { let node = self.constraints.get_interior_node(id); let predicate = self.predicates[node.atom]; - let branch = match analyze_single(self.db, &predicate) { + let branch = match analyze_single(db, self.env, &predicate) { Truthiness::AlwaysTrue => node.if_true, Truthiness::AlwaysFalse => node.if_false, Truthiness::Ambiguous => { @@ -1176,6 +1202,7 @@ impl<'a, 'db> NarrowingProjector<'a, 'db> { /// Evaluates narrowed types over a projected narrowing graph. struct ProjectedNarrowingContext<'a, 'db> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, base_ty: Type<'db>, graph: &'a ProjectedNarrowingGraph<'db>, /// Marks join boundaries in the projected DAG. @@ -1206,11 +1233,12 @@ impl<'db> ProjectedNarrowingContext<'_, 'db> { id: ProjectedNarrowingNodeId, accumulated: Option>, ) -> Type<'db> { + let db = self.db; if self.is_join(id) { // Preserve replacement narrowing order at a join: evaluate the shared suffix once, // then apply the incoming prefix constraint to its narrowed type. let suffix_ty = self.narrow_join(id); - return apply_accumulated_narrowing(self.db, suffix_ty, accumulated); + return apply_accumulated_narrowing(db, self.env, suffix_ty, accumulated); } self.narrow_uncached(id, accumulated) @@ -1222,12 +1250,13 @@ impl<'db> ProjectedNarrowingContext<'_, 'db> { id: ProjectedNarrowingNodeId, accumulated: Option>, ) -> Type<'db> { + let db = self.db; if id == ProjectedNarrowingNodeId::ALWAYS_FALSE { return Type::Never; } if id == ProjectedNarrowingNodeId::ALWAYS_TRUE { - apply_accumulated_narrowing(self.db, self.base_ty, accumulated) + apply_accumulated_narrowing(db, self.env, self.base_ty, accumulated) } else { let node = self.graph.node(id); let (pos_constraint, neg_constraint) = @@ -1253,8 +1282,8 @@ impl<'db> ProjectedNarrowingContext<'_, 'db> { let false_ty = self.narrow(node.if_false, false_accumulated); let true_or_uncertain = - UnionType::from_two_elements(self.db, true_ty, uncertain_ty); - UnionType::from_two_elements(self.db, true_or_uncertain, false_ty) + UnionType::from_two_elements(db, self.env, true_ty, uncertain_ty); + UnionType::from_two_elements(db, self.env, true_or_uncertain, false_ty) } } } @@ -1262,6 +1291,7 @@ impl<'db> ProjectedNarrowingContext<'_, 'db> { fn analyze_single_pattern_predicate_kind<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, predicate_kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, precomputed_definite_match_ty: Option>, @@ -1272,6 +1302,7 @@ fn analyze_single_pattern_predicate_kind<'db>( equality_truthiness( db, + env, subject_ty, value_ty, ComparisonSoundnessPolicy::from_analysis_settings( @@ -1280,11 +1311,11 @@ fn analyze_single_pattern_predicate_kind<'db>( ) } PatternPredicateKind::Singleton(singleton) => { - let singleton_ty = singleton_pattern_type(db, *singleton); + let singleton_ty = singleton_pattern_type(db, env, *singleton); - if subject_ty.is_equivalent_to(db, singleton_ty) { + if subject_ty.is_equivalent_to(db, env, singleton_ty) { Truthiness::AlwaysTrue - } else if subject_ty.is_disjoint_from(db, singleton_ty) { + } else if subject_ty.is_disjoint_from(db, env, singleton_ty) { Truthiness::AlwaysFalse } else { Truthiness::Ambiguous @@ -1300,21 +1331,23 @@ fn analyze_single_pattern_predicate_kind<'db>( let narrowed_subject_ty = remaining_subject_ty; let definitely_matched = - definite_match_pattern_type_for_subject(db, p, narrowed_subject_ty); + definite_match_pattern_type_for_subject(db, env, p, narrowed_subject_ty); - let truthiness = if narrowed_subject_ty.is_subtype_of(db, definitely_matched) { - Truthiness::AlwaysTrue - } else { - analyze_single_pattern_predicate_kind( - db, - p, - narrowed_subject_ty, - Some(definitely_matched), - ) - }; + let truthiness = + if narrowed_subject_ty.is_subtype_of(db, env, definitely_matched) { + Truthiness::AlwaysTrue + } else { + analyze_single_pattern_predicate_kind( + db, + env, + p, + narrowed_subject_ty, + Some(definitely_matched), + ) + }; remaining_subject_ty = - pattern_binding_fallthrough_type(db, p, narrowed_subject_ty); + pattern_binding_fallthrough_type(db, env, p, narrowed_subject_ty); truthiness }) // this is just a "max", but with a slight optimization: @@ -1335,49 +1368,51 @@ fn analyze_single_pattern_predicate_kind<'db>( PatternPredicateKind::Class(kind) => { let class_ty = match infer_same_file_expression_type(db, kind.class, TypeContext::default()) { - Type::ClassLiteral(class) => Type::instance(db, class.top_materialization(db)), + Type::ClassLiteral(class) => { + Type::instance(db, env, class.top_materialization(db)) + } Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) => { - callable_pattern_type(db) + callable_pattern_type(db, env) } _ => return Truthiness::Ambiguous, }; let definitely_matched = precomputed_definite_match_ty.unwrap_or_else(|| { - definite_match_pattern_type_for_subject(db, predicate_kind, subject_ty) + definite_match_pattern_type_for_subject(db, env, predicate_kind, subject_ty) }); - if subject_ty.is_equivalent_to(db, definitely_matched) - || subject_ty.is_subtype_of(db, definitely_matched) + if subject_ty.is_equivalent_to(db, env, definitely_matched) + || subject_ty.is_subtype_of(db, env, definitely_matched) { Truthiness::AlwaysTrue - } else if subject_ty.is_disjoint_from(db, class_ty) { + } else if subject_ty.is_disjoint_from(db, env, class_ty) { Truthiness::AlwaysFalse } else { Truthiness::Ambiguous } } PatternPredicateKind::Mapping(kind) => { - let mapping_ty = mapping_pattern_type(db); - if subject_ty.is_subtype_of(db, mapping_ty) { + let mapping_ty = mapping_pattern_type(db, env); + if subject_ty.is_subtype_of(db, env, mapping_ty) { if kind.is_irrefutable() { Truthiness::AlwaysTrue } else { Truthiness::Ambiguous } - } else if subject_ty.is_disjoint_from(db, mapping_ty) { + } else if subject_ty.is_disjoint_from(db, env, mapping_ty) { Truthiness::AlwaysFalse } else { Truthiness::Ambiguous } } PatternPredicateKind::Sequence(kind) => { - let sequence_ty = sequence_pattern_type_builder(db).build(); - if subject_ty.is_subtype_of(db, sequence_ty) { + let sequence_ty = sequence_pattern_type_builder(db, env).build(); + if subject_ty.is_subtype_of(db, env, sequence_ty) { if kind.is_irrefutable() { Truthiness::AlwaysTrue } else { Truthiness::Ambiguous } - } else if subject_ty.is_disjoint_from(db, sequence_ty) { + } else if subject_ty.is_disjoint_from(db, env, sequence_ty) { Truthiness::AlwaysFalse } else { Truthiness::Ambiguous @@ -1388,6 +1423,7 @@ fn analyze_single_pattern_predicate_kind<'db>( .map(|p| { analyze_single_pattern_predicate_kind( db, + env, p, subject_ty, precomputed_definite_match_ty, @@ -1416,6 +1452,7 @@ fn analyze_non_terminal_call<'db>( call_expr: Expression<'db>, is_await: bool, ) -> Truthiness { + let env = ProgramEnvironment::from_scope(callable.scope(db)); // We first infer just the type of the callable. In the most likely case that the function is // not marked with `NoReturn`, or that it always returns `NoReturn`, doing so allows us to avoid // the more expensive work of inferring the entire call expression (which could involve @@ -1434,7 +1471,7 @@ fn analyze_non_terminal_call<'db>( } let overloads_iterator = if let Some(callable) = ty - .try_upcast_to_callable(db) + .try_upcast_to_callable(db, &env) .and_then(CallableTypes::exactly_one) { callable.signatures(db).overloads.iter() @@ -1447,10 +1484,10 @@ fn analyze_non_terminal_call<'db>( let mut any_overload_is_generic = false; for overload in overloads_iterator { - let returns_never = overload.return_ty.is_equivalent_to(db, Type::Never); + let returns_never = overload.return_ty.is_equivalent_to(db, &env, Type::Never); no_overloads_return_never &= !returns_never; all_overloads_return_never &= returns_never; - any_overload_is_generic |= overload.return_ty.has_typevar(db); + any_overload_is_generic |= overload.return_ty.has_typevar(db, &env); } if no_overloads_return_never && !any_overload_is_generic && !is_await { @@ -1459,7 +1496,7 @@ fn analyze_non_terminal_call<'db>( Truthiness::AlwaysFalse } else { let call_expr_ty = infer_same_file_expression_type(db, call_expr, TypeContext::default()); - if call_expr_ty.is_equivalent_to(db, Type::Never) { + if call_expr_ty.is_equivalent_to(db, &env, Type::Never) { Truthiness::AlwaysFalse } else { Truthiness::AlwaysTrue @@ -1476,13 +1513,13 @@ fn analyze_non_empty_iterable(db: &dyn Db, iterable: Expression) -> Truthiness { } } -fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { +fn analyze_single(db: &dyn Db, env: &ProgramEnvironment<'_>, predicate: &Predicate) -> Truthiness { let _span = tracing::trace_span!("analyze_single", ?predicate).entered(); match predicate.node { PredicateNode::Expression(test_expr) => { infer_same_file_expression_type(db, test_expr, TypeContext::default()) - .bool(db) + .bool(db, env) .negate_if(!predicate.is_positive) } PredicateNode::IsNonTerminalCall(CallableAndCallExpr { @@ -1501,9 +1538,8 @@ fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { PredicateNode::StarImportPlaceholder(star_import) => { let place_table = place_table(db, star_import.scope(db)); let symbol = place_table.symbol(star_import.symbol_id(db)); - let referenced_file = star_import.referenced_file(db); - - let requires_explicit_reexport = match dunder_all_names(db, referenced_file) { + let python_file = star_import.referenced_parse_file(db); + let requires_explicit_reexport = match dunder_all_names(db, python_file) { Some(all_names) => { if all_names.contains(symbol.name()) { Some(RequiresExplicitReExport::No) @@ -1511,7 +1547,7 @@ fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { tracing::trace!( "Symbol `{}` (via star import) not found in `__all__` of `{}`", symbol.name(), - referenced_file.path(db) + python_file.file(db).path(db) ); return Truthiness::AlwaysFalse; } @@ -1521,7 +1557,8 @@ fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { match imported_symbol( db, - Some(referenced_file), + env, + Some(python_file), symbol.name(), requires_explicit_reexport, ) @@ -1544,7 +1581,7 @@ fn analyze_single(db: &dyn Db, predicate: &Predicate) -> Truthiness { /// Check whether a diagnostic emitted at `range` is in reachable code, considering both /// scope reachability and statement-level reachability within the scope. pub(crate) fn is_range_reachable<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, index: &SemanticIndex<'db>, scope_id: FileScopeId, range: TextRange, @@ -1753,6 +1790,7 @@ impl<'db> DeclarationsIteratorExtension<'db> for DeclarationsIterator<'_, 'db> { mod tests { use super::*; use crate::db::tests::setup_db; + use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::system::DbWithWritableSystem as _; use ty_python_core::narrowing_constraints::InteriorNode; @@ -1788,7 +1826,8 @@ class TargetB: db.write_files([("/src/a.py", a.as_str()), ("/src/b.py", b.as_str())])?; let file = system_path_to_file(&db, "/src/a.py").unwrap(); - let index = semantic_index(&db, file); + let python_file = PythonFile::new(&db, file, db.python_version()); + let index = semantic_index(&db, python_file); let class_scope = index .child_scopes(FileScopeId::global()) .find(|(_, scope)| scope.node().as_class().is_some()) @@ -1799,7 +1838,7 @@ class TargetB: .find(|(_, scope)| scope.node().as_function().is_some()) .unwrap() .0 - .to_scope_id(&db, file); + .to_scope_id(&db, python_file); // Enter the range directly so it becomes the cycle head when inferring `other.target` // reaches the other module and then re-enters this scope. @@ -1821,14 +1860,16 @@ class TargetB: let file = system_path_to_file(&db, "/src/test.py").unwrap(); let function_scope = { - let index = semantic_index(&db, file); + let python_file = PythonFile::new(&db, file, db.python_version()); + let index = semantic_index(&db, python_file); index.child_scopes(FileScopeId::global()).next().unwrap().0 }; { - let scope = function_scope.to_scope_id(&db, file); + let python_file = PythonFile::new(&db, file, db.python_version()); + let scope = function_scope.to_scope_id(&db, python_file); let use_def = use_def_map(&db, scope); assert!( - evaluate_reachability_constraint(&db, scope, use_def.end_of_scope_reachability()) + evaluate_reachability_constraint(&db, scope, use_def.end_of_scope_reachability(),) .may_be_true() ); } @@ -1838,10 +1879,11 @@ class TargetB: "from typing import NoReturn\ndef callback() -> NoReturn: ...", )?; - let scope = function_scope.to_scope_id(&db, file); + let python_file = PythonFile::new(&db, file, db.python_version()); + let scope = function_scope.to_scope_id(&db, python_file); let use_def = use_def_map(&db, scope); assert!( - evaluate_reachability_constraint(&db, scope, use_def.end_of_scope_reachability()) + evaluate_reachability_constraint(&db, scope, use_def.end_of_scope_reachability(),) .is_always_false() ); Ok(()) @@ -1866,7 +1908,7 @@ class TargetB: )?; let file = system_path_to_file(&db, "/src/test.py").unwrap(); - let index = semantic_index(&db, file); + let index = semantic_index(&db, PythonFile::new(&db, file, db.python_version())); let function_scope = index.child_scopes(FileScopeId::global()).next().unwrap().0; let use_def = index.use_def_map(function_scope); let predicate = use_def @@ -1892,8 +1934,10 @@ class TargetB: .collect(); let constraints = NarrowingConstraints::from_test_nodes(nodes); let x = index.place_table(function_scope).symbol_id("x").unwrap(); + let env = db.program_environment(); let mut projector = NarrowingProjector::new( &db, + &env, &constraints, &predicates, ScopedPlaceId::Symbol(x), diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 9910fb77cf..612c1f8b3a 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -1,4 +1,5 @@ use compact_str::CompactString; +use ruff_db::PythonFile; use ruff_db::files::{File, FilePath}; use ruff_db::parsed::{parsed_module, parsed_string_annotation}; use ruff_db::source::{line_index, source_text}; @@ -18,8 +19,8 @@ use crate::place::implicit_globals::all_implicit_module_globals; use crate::types::ide_support::{ImportAliasResolution, definition_for_name}; use crate::types::list_members::{Member, all_members, all_reachable_members}; use crate::types::{ - CycleDetector, SpecialFormType, Type, TypeQualifiers, binding_type, infer_complete_scope_types, - inferred_declaration, + CycleDetector, ProgramEnvironment, SpecialFormType, Type, TypeQualifiers, binding_type, + infer_complete_scope_types, inferred_declaration, }; use ty_python_core::definition::{Definition, DefinitionKind}; use ty_python_core::place_table; @@ -40,14 +41,14 @@ use ty_python_core::symbol::Symbol; /// methods will automatically handle using the string literal's AST node when necessary. pub struct SemanticModel<'db> { db: &'db dyn Db, - file: File, + file: PythonFile<'db>, /// If `Some` then this `SemanticModel` is for analyzing the sub-AST of a string annotation. /// This expression will be used as a witness to the scope/location we're analyzing. in_string_annotation_expr: Option>, } impl<'db> SemanticModel<'db> { - pub fn new(db: &'db dyn Db, file: File) -> Self { + pub fn new(db: &'db dyn Db, file: PythonFile<'db>) -> Self { Self { db, file, @@ -60,15 +61,23 @@ impl<'db> SemanticModel<'db> { } pub fn file(&self) -> File { + self.file.file(self.db) + } + + pub fn python_file(&self) -> PythonFile<'db> { self.file } + pub fn program_environment(&self) -> ProgramEnvironment<'db> { + ProgramEnvironment::from_file(self.python_file()) + } + pub fn file_path(&self) -> &FilePath { - self.file.path(self.db) + self.file().path(self.db) } pub fn line_index(&self) -> LineIndex { - line_index(self.db, self.file) + line_index(self.db, self.file()) } /// Returns a map from symbol name to that symbol's @@ -80,20 +89,20 @@ impl<'db> SemanticModel<'db> { &self, node: ast::AnyNodeRef<'_>, ) -> FxHashMap> { + let db = self.db; let mut members = FxHashMap::default(); - let index = semantic_index(self.db, self.file); + let python_file = self.python_file(); + let index = semantic_index(self.db, python_file); let Some(file_scope) = self.scope(node) else { return members; }; - for (file_scope, _) in index .visible_ancestor_scopes(file_scope) .collect::>() .into_iter() .rev() { - for memberdef in - all_reachable_members(self.db, file_scope.to_scope_id(self.db, self.file)) + for memberdef in all_reachable_members(db, file_scope.to_scope_id(self.db, python_file)) { members.insert( memberdef.member.name, @@ -110,22 +119,24 @@ impl<'db> SemanticModel<'db> { /// Resolve the given import made in this file to a Type pub fn resolve_module_type(&self, module: Option<&str>, level: u32) -> Option> { let module = self.resolve_module(module, level)?; - Some(Type::module_literal(self.db, self.file, module)) + Some(Type::module_literal(self.db, self.python_file(), module)) } /// Resolve the given import made in this file to a Module pub fn resolve_module(&self, module: Option<&str>, level: u32) -> Option> { let module_name = - ModuleName::from_identifier_parts(self.db, self.file, module, level).ok()?; - resolve_module(self.db, self.file, &module_name) + ModuleName::from_identifier_parts(self.db, self.python_file(), module, level).ok()?; + resolve_module(self.db, self.python_file(), &module_name) } /// Returns completions for symbols available in a `import ` context. pub fn import_completions(&self) -> Vec> { let typing_extensions = ModuleName::new_static("typing_extensions").unwrap(); - let is_typing_extensions_available = self.file.is_stub(self.db) - || resolve_real_shadowable_module(self.db, self.file, &typing_extensions).is_some(); - list_modules(self.db) + let file = self.file(); + let is_typing_extensions_available = file.is_stub(self.db) + || resolve_real_shadowable_module(self.db, self.python_file(), &typing_extensions) + .is_some(); + list_modules(self.db, self.python_file().python_version(self.db)) .iter() .copied() .filter(|module| { @@ -133,7 +144,7 @@ impl<'db> SemanticModel<'db> { }) .map(|module| { let builtin = module.is_known(self.db, KnownModule::Builtins); - let ty = Type::module_literal(self.db, self.file, module); + let ty = Type::module_literal(self.db, self.python_file(), module); Completion { name: CompactString::new(module.name(self.db).as_str()), ty: Some(ty), @@ -145,7 +156,11 @@ impl<'db> SemanticModel<'db> { /// Returns completions for symbols available in a `from module import ` context. pub fn from_import_completions(&self, import: &ast::StmtImportFrom) -> Vec> { - let module_name = match ModuleName::from_import_statement(self.db, self.file, import) { + let module_name = match ModuleName::from_import_statement( + self.db, + self.python_file(), + import, + ) { Ok(module_name) => module_name, Err(err) => { tracing::debug!( @@ -164,7 +179,7 @@ impl<'db> SemanticModel<'db> { &self, module_name: &ModuleName, ) -> Vec> { - let Some(module) = resolve_module(self.db, self.file, module_name) else { + let Some(module) = resolve_module(self.db, self.python_file(), module_name) else { tracing::debug!("Could not resolve module from `{module_name:?}`"); return vec![]; }; @@ -174,11 +189,12 @@ impl<'db> SemanticModel<'db> { /// Returns completions for symbols available in the given module as if /// it were imported by this model's `File`. fn module_completions(&self, module_name: &ModuleName) -> Vec> { - let Some(module) = resolve_module(self.db, self.file, module_name) else { + let db = self.db; + let Some(module) = resolve_module(self.db, self.python_file(), module_name) else { tracing::debug!("Could not resolve module from `{module_name:?}`"); return vec![]; }; - let ty = Type::module_literal(self.db, self.file, module); + let ty = Type::module_literal(self.db, self.python_file(), module); let builtin = module.is_known(self.db, KnownModule::Builtins); let mut completions = vec![]; @@ -186,7 +202,7 @@ impl<'db> SemanticModel<'db> { clippy::iter_over_hash_type, reason = "completion order is determined later by relevance ranking" )] - for Member { name, ty } in all_members(self.db, ty) { + for Member { name, ty } in all_members(db, &self.program_environment(), ty) { completions.push(Completion { name: CompactString::new(name), ty: Some(ty), @@ -203,7 +219,7 @@ impl<'db> SemanticModel<'db> { let mut completions = vec![]; for submodule in module.all_submodules(self.db) { - let ty = Type::module_literal(self.db, self.file, *submodule); + let ty = Type::module_literal(self.db, self.python_file(), *submodule); let base = submodule.name(self.db).last_component(); completions.push(Completion { name: CompactString::new(base), @@ -216,11 +232,12 @@ impl<'db> SemanticModel<'db> { /// Returns completions for symbols available in a `object.` context. pub fn attribute_completions(&self, node: &ast::ExprAttribute) -> Vec> { + let db = self.db; let Some(ty) = node.value.inferred_type(self) else { return Vec::new(); }; - all_members(self.db, ty) + all_members(db, &self.program_environment(), ty) .into_iter() .map(|member| Completion { name: CompactString::new(member.name), @@ -236,14 +253,16 @@ impl<'db> SemanticModel<'db> { /// If a scope could not be determined, then completions for the global /// scope of this model's `File` are returned. pub fn scoped_completions(&self, node: ast::AnyNodeRef<'_>) -> Vec> { - let index = semantic_index(self.db, self.file); + let db = self.db; + let python_file = self.python_file(); + let index = semantic_index(self.db, python_file); let Some(file_scope) = self.scope(node) else { return vec![]; }; let mut completions = vec![]; for (file_scope, _) in index.ancestor_scopes(file_scope) { completions.extend( - all_reachable_members(self.db, file_scope.to_scope_id(self.db, self.file)).map( + all_reachable_members(db, file_scope.to_scope_id(self.db, python_file)).map( |memberdef| Completion { name: CompactString::new(memberdef.member.name), ty: Some(memberdef.member.ty), @@ -279,7 +298,7 @@ impl<'db> SemanticModel<'db> { /// Returns `true` if the given class definition's name was previously /// bound in the same scope (i.e., the class definition is a re-assignment). pub fn is_class_name_reassigned(&self, class_def: &ast::StmtClassDef) -> bool { - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db, self.python_file()); let definition = index.expect_single_definition(class_def); let scope = definition.scope(self.db); let table = place_table(self.db, scope); @@ -289,7 +308,7 @@ impl<'db> SemanticModel<'db> { /// Returns the scope in which `node` is defined (handles string annotations). pub fn scope(&self, node: ast::AnyNodeRef<'_>) -> Option { - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db, self.python_file()); match self.node_in_ast(node) { ast::AnyNodeRef::Identifier(identifier) => index.try_expression_scope_id(identifier), @@ -343,7 +362,7 @@ impl<'db> SemanticModel<'db> { &self, node: ast::AnyNodeRef<'_>, ) -> impl Iterator + '_ { - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db, self.python_file()); self.scope(node) .into_iter() .flat_map(move |scope| index.ancestor_scopes(scope)) @@ -361,7 +380,7 @@ impl<'db> SemanticModel<'db> { &self, covering_node: &CoveringNode<'_>, ) -> Option> { - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db, self.python_file()); let parsed = parsed_module(self.db, self.file).load(self.db); let target_range = covering_node.node().range(); @@ -432,11 +451,11 @@ impl<'db> SemanticModel<'db> { ) -> Option<(Parsed, Self)> { // Ask the inference engine whether this is actually a string annotation let expr = ExprRef::StringLiteral(string_expr); - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db, self.python_file()); // When looking up scopes, use the expr in the top-level AST // (we might be trying to enter a sub-sub-AST, so this isn't silly) let file_scope = index.expression_scope_id(&self.expr_ref_in_ast(expr)); - let scope = file_scope.to_scope_id(self.db, self.file); + let scope = file_scope.to_scope_id(self.db, self.python_file()); // When querying whether the expr is a string annotation, we do however use the actual expr // (the inference engine should record this information even for sub-nodes) if !infer_complete_scope_types(self.db, scope).is_string_annotation(expr) { @@ -448,7 +467,7 @@ impl<'db> SemanticModel<'db> { // The string_annotation will be used as the expr/node for any query that needs // to look up a node in the AST to prevent panics, because these sub-AST nodes // are not in the File's AST! - let source = source_text(self.db, self.file); + let source = source_text(self.db, self.file()); let string_literal = string_expr.as_single_part_string()?; let ast = parsed_string_annotation(source.as_str(), string_literal).ok()?; let model = Self { @@ -476,8 +495,8 @@ impl<'db> SemanticModel<'db> { match definition.kind(self.db) { DefinitionKind::TypeAlias(_) => true, DefinitionKind::AnnotatedAssignment(assignment) => { - let parsed = parsed_module(self.db, definition.file(self.db)); - let model = Self::new(self.db, definition.file(self.db)); + let parsed = parsed_module(self.db, definition.python_file(self.db)); + let model = Self::new(self.db, definition.python_file(self.db)); model.is_type_alias_annotation(assignment.annotation(&parsed.load(self.db))) } _ => false, @@ -487,6 +506,7 @@ impl<'db> SemanticModel<'db> { /// Returns the type qualifiers (e.g. `Final`, `ClassVar`) for a given expression, /// if the expression refers to a name or attribute with declared qualifiers. pub fn type_qualifiers(&self, expr: ExprRef<'_>) -> TypeQualifiers { + let db = self.db; match expr { ExprRef::Name(name) => { let Some(definition) = @@ -495,7 +515,7 @@ impl<'db> SemanticModel<'db> { return TypeQualifiers::empty(); }; let definition_file = definition.file(self.db); - let module = parsed_module(self.db, definition_file).load(self.db); + let module = parsed_module(self.db, definition.python_file(self.db)).load(self.db); if !definition .kind(self.db) .category(definition_file.is_stub(self.db), &module) @@ -503,7 +523,7 @@ impl<'db> SemanticModel<'db> { { return TypeQualifiers::empty(); } - let Some(declared) = inferred_declaration(self.db, definition).declared() else { + let Some(declared) = inferred_declaration(self.db(), definition).declared() else { return TypeQualifiers::empty(); }; declared.qualifiers() @@ -514,7 +534,8 @@ impl<'db> SemanticModel<'db> { }; value_ty .member_lookup_with_policy( - self.db, + db, + &self.program_environment(), &attr.attr.id, crate::types::MemberLookupPolicy::default(), ) @@ -570,16 +591,13 @@ impl<'db> SemanticModel<'db> { _ => Vec::new(), } } + let db = self.db; let Some(expected_ty) = self.string_literal_completion_expected_type(string_expr) else { return Vec::new(); }; - let mut candidates = collect( - self.db, - expected_ty, - &StringLiteralCandidatesVisitor::default(), - ); + let mut candidates = collect(db, expected_ty, &StringLiteralCandidatesVisitor::default()); candidates.sort_unstable_by(|left, right| left.value.cmp(&right.value)); candidates.dedup_by(|left, right| left.value == right.value); candidates @@ -590,9 +608,9 @@ impl<'db> SemanticModel<'db> { string_expr: &ast::ExprStringLiteral, ) -> Option> { let expr = ast::ExprRef::from(string_expr); - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db, self.python_file()); let file_scope = index.try_expression_scope_id(&self.expr_ref_in_ast(expr))?; - let scope = file_scope.to_scope_id(self.db, self.file); + let scope = file_scope.to_scope_id(self.db, self.python_file()); infer_complete_scope_types(self.db, scope).try_expected_type(expr) } @@ -691,13 +709,14 @@ pub(crate) trait HasOptionalDefinition { impl HasType for ast::ExprRef<'_> { fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { - let index = semantic_index(model.db, model.file); + let file = model.python_file(); + let index = semantic_index(model.db, file); // TODO(#1637): semantic tokens is making this crash even with // `try_expr_ref_in_ast` guarding this, for now just use `try_expression_scope_id`. // The problematic input is `x: "float` (with a dangling quote). I imagine the issue // is we're too eagerly setting `is_string_annotation` in inference. let file_scope = index.try_expression_scope_id(&model.expr_ref_in_ast(*self))?; - let scope = file_scope.to_scope_id(model.db, model.file); + let scope = file_scope.to_scope_id(model.db, file); infer_complete_scope_types(model.db, scope).try_expression_type(*self) } @@ -794,7 +813,7 @@ macro_rules! impl_binding_has_ty_def { impl HasDefinition for $ty { #[inline] fn definition<'db>(&self, model: &SemanticModel<'db>) -> Definition<'db> { - let index = semantic_index(model.db, model.file); + let index = semantic_index(model.db, model.python_file()); index.expect_single_definition(self) } } @@ -803,7 +822,7 @@ macro_rules! impl_binding_has_ty_def { #[inline] fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { let binding = HasDefinition::definition(self, model); - Some(binding_type(model.db, binding)) + Some(binding_type(model.db(), binding)) } } }; @@ -823,8 +842,11 @@ impl HasType for ast::Alias { if &self.name == "*" { return Some(Type::Never); } - let index = semantic_index(model.db, model.file); - Some(binding_type(model.db, index.expect_single_definition(self))) + let index = semantic_index(model.db, model.python_file()); + Some(binding_type( + model.db(), + index.expect_single_definition(self), + )) } } @@ -832,7 +854,7 @@ impl HasOptionalDefinition for ast::ExceptHandlerExceptHandler { fn optional_definition<'db>(&self, model: &SemanticModel<'db>) -> Option> { self.name.as_ref()?; - let index = semantic_index(model.db, model.file); + let index = semantic_index(model.db, model.python_file()); Some(index.expect_single_definition(self)) } } @@ -840,7 +862,7 @@ impl HasOptionalDefinition for ast::ExceptHandlerExceptHandler { impl HasType for ast::ExceptHandlerExceptHandler { fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { let definition = self.optional_definition(model)?; - Some(binding_type(model.db, definition)) + Some(binding_type(model.db(), definition)) } } @@ -848,6 +870,7 @@ impl HasType for ast::ExceptHandlerExceptHandler { mod tests { use crate::db::tests::TestDbBuilder; use crate::{HasType, SemanticModel}; + use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; @@ -859,6 +882,7 @@ mod tests { let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); + let foo = PythonFile::new(&db, foo, db.python_version()); let ast = parsed_module(&db, foo).load(&db); let function = ast.suite()[0].as_function_def_stmt().unwrap(); @@ -878,6 +902,7 @@ mod tests { let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); + let foo = PythonFile::new(&db, foo, db.python_version()); let ast = parsed_module(&db, foo).load(&db); let class = ast.suite()[0].as_class_def_stmt().unwrap(); @@ -898,6 +923,7 @@ mod tests { let bar = system_path_to_file(&db, "/src/bar.py").unwrap(); + let bar = PythonFile::new(&db, bar, db.python_version()); let ast = parsed_module(&db, bar).load(&db); let import = ast.suite()[0].as_import_from_stmt().unwrap(); diff --git a/crates/ty_python_semantic/src/subscript.rs b/crates/ty_python_semantic/src/subscript.rs index c03d11dd3e..af4438fa3d 100644 --- a/crates/ty_python_semantic/src/subscript.rs +++ b/crates/ty_python_semantic/src/subscript.rs @@ -4,17 +4,21 @@ use std::num::NonZeroI32; +use crate::{Db, ProgramEnvironment}; use itertools::Either; -use crate::Db; - #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) struct OutOfBoundsError; pub(crate) trait PyIndex<'db> { type Item: 'db; - fn py_index(self, db: &'db dyn Db, index: i32) -> Result; + fn py_index( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + index: i32, + ) -> Result; } fn from_nonnegative_i32(index: i32) -> usize { @@ -82,7 +86,12 @@ impl Nth { impl<'db, T> PyIndex<'db> for &'db [T] { type Item = &'db T; - fn py_index(self, _db: &'db dyn Db, index: i32) -> Result<&'db T, OutOfBoundsError> { + fn py_index( + self, + _db: &'db dyn Db, + _ctx: &ProgramEnvironment<'db>, + index: i32, + ) -> Result<&'db T, OutOfBoundsError> { match Nth::from_index(index) { Nth::FromStart(nth) => self.get(nth).ok_or(OutOfBoundsError), Nth::FromEnd(nth_rev) => (self.len().checked_sub(nth_rev + 1)) @@ -98,7 +107,12 @@ where { type Item = I; - fn py_index(self, _db: &'db dyn Db, index: i32) -> Result { + fn py_index( + self, + _db: &'db dyn Db, + _ctx: &ProgramEnvironment<'db>, + index: i32, + ) -> Result { match Nth::from_index(index) { Nth::FromStart(nth) => self.nth(nth).ok_or(OutOfBoundsError), Nth::FromEnd(nth_rev) => self.nth_back(nth_rev).ok_or(OutOfBoundsError), @@ -232,55 +246,72 @@ mod tests { #[test] fn py_index_empty() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let iter = std::iter::empty::(); - assert_eq!(iter.clone().py_index(&db, 0), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(&db, 1), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(&db, -1), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(&db, i32::MIN), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(&db, i32::MAX), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(db, &env, 0), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(db, &env, 1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(db, &env, -1), Err(OutOfBoundsError)); + assert_eq!( + iter.clone().py_index(db, &env, i32::MIN), + Err(OutOfBoundsError) + ); + assert_eq!( + iter.clone().py_index(db, &env, i32::MAX), + Err(OutOfBoundsError) + ); } #[test] fn py_index_single_element() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let iter = ['a'].into_iter(); - assert_eq!(iter.clone().py_index(&db, 0), Ok('a')); - assert_eq!(iter.clone().py_index(&db, 1), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(&db, -1), Ok('a')); - assert_eq!(iter.clone().py_index(&db, -2), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(db, &env, 0), Ok('a')); + assert_eq!(iter.clone().py_index(db, &env, 1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(db, &env, -1), Ok('a')); + assert_eq!(iter.clone().py_index(db, &env, -2), Err(OutOfBoundsError)); } #[test] fn py_index_more_elements() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let iter = ['a', 'b', 'c', 'd', 'e'].into_iter(); - assert_eq!(iter.clone().py_index(&db, 0), Ok('a')); - assert_eq!(iter.clone().py_index(&db, 1), Ok('b')); - assert_eq!(iter.clone().py_index(&db, 4), Ok('e')); - assert_eq!(iter.clone().py_index(&db, 5), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(db, &env, 0), Ok('a')); + assert_eq!(iter.clone().py_index(db, &env, 1), Ok('b')); + assert_eq!(iter.clone().py_index(db, &env, 4), Ok('e')); + assert_eq!(iter.clone().py_index(db, &env, 5), Err(OutOfBoundsError)); - assert_eq!(iter.clone().py_index(&db, -1), Ok('e')); - assert_eq!(iter.clone().py_index(&db, -2), Ok('d')); - assert_eq!(iter.clone().py_index(&db, -5), Ok('a')); - assert_eq!(iter.clone().py_index(&db, -6), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(db, &env, -1), Ok('e')); + assert_eq!(iter.clone().py_index(db, &env, -2), Ok('d')); + assert_eq!(iter.clone().py_index(db, &env, -5), Ok('a')); + assert_eq!(iter.clone().py_index(db, &env, -6), Err(OutOfBoundsError)); } #[test] fn py_index_uses_full_index_range() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let iter = 0..=u32::MAX; // u32::MAX - |i32::MIN| + 1 = 2^32 - 1 - 2^31 + 1 = 2^31 - assert_eq!(iter.clone().py_index(&db, i32::MIN), Ok(2u32.pow(31))); - assert_eq!(iter.clone().py_index(&db, -2), Ok(u32::MAX - 2 + 1)); - assert_eq!(iter.clone().py_index(&db, -1), Ok(u32::MAX - 1 + 1)); - - assert_eq!(iter.clone().py_index(&db, 0), Ok(0)); - assert_eq!(iter.clone().py_index(&db, 1), Ok(1)); - assert_eq!(iter.clone().py_index(&db, i32::MAX), Ok(i32::MAX as u32)); + assert_eq!(iter.clone().py_index(db, &env, i32::MIN), Ok(2u32.pow(31))); + assert_eq!(iter.clone().py_index(db, &env, -2), Ok(u32::MAX - 2 + 1)); + assert_eq!(iter.clone().py_index(db, &env, -1), Ok(u32::MAX - 1 + 1)); + + assert_eq!(iter.clone().py_index(db, &env, 0), Ok(0)); + assert_eq!(iter.clone().py_index(db, &env, 1), Ok(1)); + assert_eq!( + iter.clone().py_index(db, &env, i32::MAX), + Ok(i32::MAX as u32) + ); } #[track_caller] diff --git a/crates/ty_python_semantic/src/suppression.rs b/crates/ty_python_semantic/src/suppression.rs index 2682980c6c..f81bbb6213 100644 --- a/crates/ty_python_semantic/src/suppression.rs +++ b/crates/ty_python_semantic/src/suppression.rs @@ -9,7 +9,7 @@ use std::hash::{Hash, Hasher}; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, LintName, Severity, Span, }; -use ruff_db::{files::File, parsed::parsed_module, source::source_text}; +use ruff_db::{PythonFile, files::File, parsed::parsed_module, source::source_text}; use ruff_python_ast::token::{TokenKind, Tokens}; use ruff_python_trivia::indentation_at_offset; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -76,11 +76,14 @@ pub(crate) fn is_unused_ignore_comment_lint(name: LintName) -> bool { } #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions { +pub(crate) fn suppressions(db: &dyn Db, file: PythonFile<'_>) -> Suppressions { + let source_file = file.file(db); let parsed = parsed_module(db, file).load(db); - let source = source_text(db, file); + let source = source_text(db, source_file); - let respect_type_ignore = db.analysis_settings(file).respect_type_ignore_comments; + let respect_type_ignore = db + .analysis_settings(source_file) + .respect_type_ignore_comments; let mut builder = SuppressionsBuilder::new(&source, db.lint_registry()); let mut line_start = TextSize::default(); @@ -137,7 +140,7 @@ pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions { pub(crate) fn check_suppressions( db: &dyn Db, - file: File, + file: PythonFile<'_>, diagnostics: TypeCheckDiagnostics, ) -> Vec { let mut context = CheckSuppressionsContext::new(db, file, diagnostics); @@ -216,11 +219,11 @@ struct CheckSuppressionsContext<'a> { } impl<'a> CheckSuppressionsContext<'a> { - fn new(db: &'a dyn Db, file: File, diagnostics: TypeCheckDiagnostics) -> Self { + fn new(db: &'a dyn Db, file: PythonFile<'a>, diagnostics: TypeCheckDiagnostics) -> Self { let suppressions = suppressions(db, file); Self { db, - file, + file: file.file(db), suppressions, diagnostics: diagnostics.into(), } @@ -912,7 +915,7 @@ impl IntervalIndex { #[cfg(test)] mod tests { - use ruff_db::files::system_path_to_file; + use ruff_db::{PythonFile, files::system_path_to_file}; use ruff_text_size::{TextLen as _, TextRange}; use super::suppressions; @@ -937,7 +940,7 @@ value = missing let missing_start = source.find("missing").unwrap().try_into().unwrap(); let missing_range = TextRange::at(missing_start, "missing".text_len()); - let suppressions = suppressions(&db, file); + let suppressions = suppressions(&db, PythonFile::new(&db, file, db.python_version())); assert_eq!(suppressions.inline.len(), 4); assert_eq!( suppressions @@ -964,7 +967,7 @@ value = missing let missing_start = source.find("missing").unwrap().try_into().unwrap(); let missing_range = TextRange::at(missing_start, "missing".text_len()); - let suppressions = suppressions(&db, file); + let suppressions = suppressions(&db, PythonFile::new(&db, file, db.python_version())); assert_eq!(suppressions.inline.len(), 4); assert_eq!( suppressions diff --git a/crates/ty_python_semantic/src/suppression/add_ignore.rs b/crates/ty_python_semantic/src/suppression/add_ignore.rs index 1a764d6f7e..2bd716e9a7 100644 --- a/crates/ty_python_semantic/src/suppression/add_ignore.rs +++ b/crates/ty_python_semantic/src/suppression/add_ignore.rs @@ -9,9 +9,9 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Formatter; +use ruff_db::PythonFile; use ruff_db::diagnostic::LintName; use ruff_db::display::FormatterJoinExtension; -use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_diagnostics::{Edit, Fix}; @@ -33,11 +33,11 @@ use crate::suppression::{ /// how many diagnostics its edit accounts for. pub(crate) fn suppress_all( db: &dyn Db, - file: File, + file: PythonFile<'_>, ids_with_range: &[(LintName, TextRange)], ) -> Vec { let suppressions = suppressions(db, file); - let source = source_text(db, file); + let source = source_text(db, file.file(db)); let parsed = parsed_module(db, file).load(db); let tokens = parsed.tokens(); @@ -166,11 +166,11 @@ pub(crate) struct SuppressFix { } /// Creates a fix to suppress a single lint. -pub fn suppress_single(db: &dyn Db, file: File, id: LintId, range: TextRange) -> Fix { +pub fn suppress_single(db: &dyn Db, file: PythonFile<'_>, id: LintId, range: TextRange) -> Fix { let suppression_range = suppression_range(db, file, range); let suppressions = suppressions(db, file); - let source = source_text(db, file); + let source = source_text(db, file.file(db)); let codes = &[id.name()]; if let Some(existing) = find_existing_suppression(suppressions, &source, range) { @@ -193,7 +193,7 @@ pub fn suppress_single(db: &dyn Db, file: File, id: LintId, range: TextRange) -> /// * If `range` is within a single-line interpolated expression, then the start and end are extended to the start and end of the enclosing interpolated string. /// * If there's a line continuation, then the suppression range is extended to include the following line too. /// * If there's a multiline string, then the suppression range is extended to cover the starting and ending line of the multiline string. -fn suppression_range(db: &dyn Db, file: File, range: TextRange) -> TextRange { +fn suppression_range(db: &dyn Db, file: PythonFile<'_>, range: TextRange) -> TextRange { // Always insert a new suppression at the end of the range to avoid having to deal with multiline strings // etc. Also make sure to not pass a sub-token range to `Tokens::after`. let parsed = parsed_module(db, file).load(db); diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index dc684dfc74..cc74584def 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -12,9 +12,10 @@ use std::time::Duration; use bitflags::bitflags; use call::{CallDunderError, CallError, CallErrorKind}; use context::InferContext; +pub use context::ProgramEnvironment; use ruff_db::Instant; +use ruff_db::PythonFile; use ruff_db::diagnostic::{Annotation, Diagnostic, Span}; -use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_python_ast::name::Name; @@ -172,9 +173,10 @@ mod definition; mod property_tests; mod subscript; -pub fn check_types(db: &dyn Db, file: File) -> Vec { - let _span = tracing::trace_span!("check_types", ?file).entered(); - tracing::debug!("Checking file '{path}'", path = file.path(db)); +pub fn check_types(db: &dyn Db, file: PythonFile<'_>) -> Vec { + let source_file = file.file(db); + let _span = tracing::trace_span!("check_types", ?source_file).entered(); + tracing::debug!("Checking file '{path}'", path = source_file.path(db)); let start = Instant::now(); @@ -199,7 +201,7 @@ pub fn check_types(db: &dyn Db, file: File) -> Vec { index .semantic_syntax_errors() .iter() - .map(|error| Diagnostic::invalid_syntax(file, error, error)), + .map(|error| Diagnostic::invalid_syntax(source_file, error, error)), ); let diagnostics = check_suppressions(db, file, diagnostics); @@ -208,7 +210,7 @@ pub fn check_types(db: &dyn Db, file: File) -> Vec { if elapsed >= Duration::from_millis(100) { tracing::info!( "Checking file `{path}` took more than 100ms ({elapsed:?})", - path = file.path(db) + path = source_file.path(db) ); } @@ -241,7 +243,7 @@ fn definition_expression_type<'db>( definition: Definition<'db>, expression: &ast::Expr, ) -> Type<'db> { - let file = definition.file(db); + let file = definition.python_file(db); let index = semantic_index(db, file); let file_scope = index.expression_scope_id(expression); let scope = file_scope.to_scope_id(db, file); @@ -268,7 +270,7 @@ fn definition_expression_annotation<'db>( definition: Definition<'db>, expression: &ast::Expr, ) -> TypeAndQualifiers<'db> { - let file = definition.file(db); + let file = definition.python_file(db); let index = semantic_index(db, file); let file_scope = index.expression_scope_id(expression); let scope = file_scope.to_scope_id(db, file); @@ -300,8 +302,8 @@ type MaterializationEquivalenceVisitor<'db> = /// Some recursive transformations visit the same type under more than one mapping mode within a /// single call chain. Keep separate cycle caches for those modes so one transformation cannot /// reuse the result of another. -#[derive(Default)] -pub(crate) struct ApplyTypeMappingVisitor<'db> { +pub(crate) struct ApplyTypeMappingVisitor<'env, 'db> { + env: &'env ProgramEnvironment<'db>, default: OnceCell>>, top_materialization: OnceCell>>, bottom_materialization: OnceCell>>, @@ -312,7 +314,21 @@ pub(crate) struct ApplyTypeMappingVisitor<'db> { materialization_equivalence: OnceCell>, } -impl<'db> ApplyTypeMappingVisitor<'db> { +impl<'env, 'db> ApplyTypeMappingVisitor<'env, 'db> { + fn new(env: &'env ProgramEnvironment<'db>) -> Self { + Self { + env, + default: OnceCell::default(), + top_materialization: OnceCell::default(), + bottom_materialization: OnceCell::default(), + top_specialization_materialization: OnceCell::default(), + bottom_specialization_materialization: OnceCell::default(), + promotion: OnceCell::default(), + skip_promotion: OnceCell::default(), + materialization_equivalence: OnceCell::default(), + } + } + fn materialization_equivalence(&self) -> &MaterializationEquivalenceVisitor<'db> { self.materialization_equivalence .get_or_init(|| Rc::new(CycleDetector::new(true))) @@ -365,7 +381,7 @@ impl<'db> ApplyTypeMappingVisitor<'db> { Self { materialization_equivalence, - ..Self::default() + ..Self::new(self.env) } } } @@ -527,6 +543,8 @@ impl Default for MemberLookupPolicy { /// The common key for class-member and instance-member lookup. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] struct MemberLookupKey<'db> { + #[returns(copy)] + program: Program, #[returns(copy)] ty: Type<'db>, #[returns(ref)] @@ -683,8 +701,8 @@ impl<'db> PropertyInstanceType<'db> { Self::new_internal(db, getter, setter, deleter, self.instance_class(db)) } - fn instance_fallback(self, db: &'db dyn Db) -> Type<'db> { - self.instance_class(db).to_instance(db) + fn instance_fallback(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.instance_class(db).to_instance(db, env) } /// Returns the [`PropertyAccessorRole`] that `def` plays in this property, or `None` when @@ -723,7 +741,7 @@ impl<'db> PropertyInstanceType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let getter = self .getter(db) @@ -740,29 +758,30 @@ impl<'db> PropertyInstanceType<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let getter = match self.getter(db) { - Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?), + Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, env, div, true)?), Some(ty) => Some( - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, }; let setter = match self.setter(db) { - Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?), + Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, env, div, true)?), Some(ty) => Some( - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, }; let deleter = match self.deleter(db) { - Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?), + Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, env, div, true)?), Some(ty) => Some( - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, @@ -773,18 +792,19 @@ impl<'db> PropertyInstanceType<'db> { fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { if let Some(ty) = self.getter(db) { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } if let Some(ty) = self.setter(db) { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } if let Some(ty) = self.deleter(db) { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } } @@ -870,12 +890,12 @@ pub struct DataclassParams<'db> { impl get_size2::GetSize for DataclassParams<'_> {} impl<'db> DataclassParams<'db> { - fn default_params(db: &'db dyn Db) -> Self { - Self::from_flags(db, DataclassFlags::default()) + fn default_params(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + Self::from_flags(db, env, DataclassFlags::default()) } - fn from_flags(db: &'db dyn Db, flags: DataclassFlags) -> Self { - let dataclasses_field = known_module_symbol(db, KnownModule::Dataclasses, "field") + fn from_flags(db: &'db dyn Db, env: &ProgramEnvironment<'db>, flags: DataclassFlags) -> Self { + let dataclasses_field = known_module_symbol(db, env, KnownModule::Dataclasses, "field") .place .ignore_possibly_undefined() .unwrap_or_else(Type::unknown); @@ -894,6 +914,7 @@ impl<'db> DataclassParams<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -901,7 +922,7 @@ impl<'db> DataclassParams<'db> { .field_specifiers(db) .iter() .map(|ty| { - let ty = ty.recursive_type_normalized_impl(db, div, true); + let ty = ty.recursive_type_normalized_impl(db, env, div, true); if nested { ty } else { Some(ty.unwrap_or(div)) } }) .collect::>>()?; @@ -1072,9 +1093,12 @@ impl InstanceProjection { } } -/// An ordered pair of types shared by type-relation and set-theoretic queries. +/// An ordered pair of types and their Python version shared by type-relation and set-theoretic +/// queries. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] struct TypePair<'db> { + #[returns(copy)] + program: Program, #[returns(copy)] first: Type<'db>, #[returns(copy)] @@ -1087,6 +1111,7 @@ impl get_size2::GetSize for TypePair<'_> {} /// Helper for `recursive_type_normalized_impl` for `TypeGuardLike` types. fn recursive_type_normalize_type_guard_like<'db, T: TypeGuardLike<'db>>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, guard: T, div: Type<'db>, nested: bool, @@ -1094,11 +1119,11 @@ fn recursive_type_normalize_type_guard_like<'db, T: TypeGuardLike<'db>>( let ty = if nested { guard .type_argument(db) - .recursive_type_normalized_impl(db, div, true)? + .recursive_type_normalized_impl(db, env, div, true)? } else { guard .type_argument(db) - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }; Some(guard.with_type(db, ty)) @@ -1114,8 +1139,13 @@ struct GeneratorTypes<'db> { impl<'db> GeneratorTypes<'db> { /// Apply a generator's materialization with the variance of each operation. - fn materialize(self, db: &'db dyn Db, kind: MaterializationKind) -> Self { - let visitor = ApplyTypeMappingVisitor::default(); + fn materialize( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + kind: MaterializationKind, + ) -> Self { + let visitor = ApplyTypeMappingVisitor::new(env); Self { yield_ty: self.yield_ty.map(|ty| ty.materialize(db, kind, &visitor)), send_ty: self @@ -1223,14 +1253,14 @@ impl<'db> Type<'db> { } /// Returns `true` if this type contains a `Self` type variable. - fn contains_self(self, db: &'db dyn Db) -> bool { + fn contains_self(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { if let Type::NominalInstance(instance) = self && !instance.is_definition_generic(db) { return false; } - any_over_type(db, self, false, |ty| { + any_over_type(db, env, self, false, |ty| { ty.as_typevar().is_some_and(|tv| tv.typevar(db).is_self(db)) }) } @@ -1239,11 +1269,11 @@ impl<'db> Type<'db> { /// /// `FunctionLiteral`, `BoundMethod`, and function-like `Callable` types return `false` /// because their `Self` binding is deferred to call time via the signature binding path. - fn supports_self_binding(&self, db: &'db dyn Db) -> bool { + fn supports_self_binding(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { match self { Type::FunctionLiteral(_) | Type::BoundMethod(_) | Type::KnownBoundMethod(_) => false, Type::Callable(callable) if callable.is_function_like(db) => false, - _ => self.contains_self(db), + _ => self.contains_self(db, env), } } @@ -1254,14 +1284,20 @@ impl<'db> Type<'db> { /// /// Types that defer `Self` binding to call time (functions, bound methods, function-like /// callables) are skipped; see `supports_self_binding`. - fn bind_self_typevars(self, db: &'db dyn Db, self_type: Type<'db>) -> Self { - if !self.supports_self_binding(db) { + fn bind_self_typevars( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Type<'db>, + ) -> Self { + if !self.supports_self_binding(db, env) { return self; } self.apply_type_mapping( db, - &TypeMapping::BindSelf(SelfBinding::new(db, self_type, None)), + env, + &TypeMapping::BindSelf(SelfBinding::new(db, env, self_type, None)), TypeContext::default(), ) } @@ -1274,6 +1310,17 @@ impl<'db> Type<'db> { pub(crate) fn cycle_normalized( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + cycle: &salsa::Cycle, + ) -> Self { + self.cycle_normalized_impl(db, env, previous, cycle) + } + + pub(super) fn cycle_normalized_impl( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: Self, cycle: &salsa::Cycle, ) -> Self { @@ -1292,15 +1339,15 @@ impl<'db> Type<'db> { // still ensures convergence in cases that are prone to oscillation. if cycle.iteration() <= crate::TAINTED_CYCLES { let self_degraded_by_overload = - any_over_type(db, self, false, |ty| { + any_over_type(db, env, self, false, |ty| { matches!(ty, Type::Dynamic(DynamicType::AmbiguousOverload)) - }) && !any_over_type(db, self, false, |ty| ty.is_divergent()) - && any_over_type(db, previous, false, |ty| ty.is_divergent()); + }) && !any_over_type(db, env, self, false, |ty| ty.is_divergent()) + && any_over_type(db, env, previous, false, |ty| ty.is_divergent()); // Generally, the precision of type inference improves with each iteration. // However, overload is an exception; as iterations progress, overload matching may become ambiguous, and a reversal of precision can occur. // This kind of precision degradation can be determined by whether the type contains `DynamicType::AmbiguousOverload`. if self_degraded_by_overload { - UnionType::from_elements_cycle_recovery(db, [previous, self]) + UnionType::from_elements_cycle_recovery(db, env, [previous, self]) } else { self } @@ -1314,9 +1361,9 @@ impl<'db> Type<'db> { // where the order of union types is different between the previous and current cycle. // We should use the previous union type as the base and only add new element types in // this cycle, if any. - UnionType::from_elements_cycle_recovery(db, [previous, self]) + UnionType::from_elements_cycle_recovery(db, env, [previous, self]) } - .recursive_type_normalized(db, cycle) + .recursive_type_normalized_impl_with_cycle(db, env, cycle) } pub fn is_none(&self, db: &'db dyn Db) -> bool { @@ -1327,9 +1374,9 @@ impl<'db> Type<'db> { self.is_instance_of(db, KnownClass::Bool) } - fn is_enum(&self, db: &'db dyn Db) -> bool { + fn is_enum(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { self.as_nominal_instance() - .is_some_and(|instance| enum_metadata(db, instance.class_literal(db)).is_some()) + .is_some_and(|instance| enum_metadata(db, instance.class_literal(db, env)).is_some()) } fn is_typealias_special_form(&self) -> bool { @@ -1456,19 +1503,21 @@ impl<'db> Type<'db> { fn known_specialization( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, known_class: KnownClass, ) -> Option> { - let class_literal = known_class.try_to_class_literal(db)?; - self.specialization_of(db, class_literal) + let class_literal = known_class.try_to_class_literal(db, env)?; + self.specialization_of(db, env, class_literal) } /// If the type is a specialized instance of the given class, returns the specialization. fn specialization_of( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, expected_class: StaticClassLiteral<'_>, ) -> Option> { - self.nominal_class(db)? + self.nominal_class(db, env)? .static_class_literal(db) .filter(|(class_literal, _)| *class_literal == expected_class) .and_then(|(_, specialization)| specialization) @@ -1478,29 +1527,38 @@ impl<'db> Type<'db> { fn class_specialization( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> Option<(StaticClassLiteral<'db>, Specialization<'db>)> { - self.nominal_class(db)? + self.nominal_class(db, env)? .static_class_literal(db) .and_then(|(class_literal, specialization)| Some((class_literal, specialization?))) } /// If this type is a class instance, returns its class. - fn nominal_class(self, db: &'db dyn Db) -> Option> { + fn nominal_class( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { - Type::NominalInstance(instance) => Some(instance.class(db)), + Type::NominalInstance(instance) => Some(instance.class(db, env)), Type::ProtocolInstance(instance) => instance.class_origin(db).map(|class| *class), - Type::TypeAlias(alias) => alias.value_type(db).nominal_class(db), - Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).nominal_class(db), + Type::TypeAlias(alias) => alias.value_type(db).nominal_class(db, env), + Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).nominal_class(db, env), Type::TypeVar(typevar) => { let TypeVarBoundOrConstraints::UpperBound(bound) = - typevar.typevar(db).bound_or_constraints(db)? + typevar.typevar(db).bound_or_constraints(db, env)? else { return None; }; - bound.nominal_class(db) + bound.nominal_class(db, env) + } + Type::LiteralValue(literal) => { + literal.fallback_instance(db, env).nominal_class(db, env) + } + Type::PropertyInstance(property) => { + property.instance_fallback(db, env).nominal_class(db, env) } - Type::LiteralValue(literal) => literal.fallback_instance(db).nominal_class(db), - Type::PropertyInstance(property) => property.instance_fallback(db).nominal_class(db), _ => None, } } @@ -1510,53 +1568,56 @@ impl<'db> Type<'db> { /// /// This is the case for any type which may contain types in non-covariant position within it, /// e.g., nominal instances of a generic class, or callables. - fn may_prefer_declared_type(self, db: &'db dyn Db) -> bool { - self.class_specialization(db).is_some() || self.expand_eagerly(db).is_callable_type() + fn may_prefer_declared_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + self.class_specialization(db, env).is_some() + || self.expand_eagerly(db, env).is_callable_type() } /// Returns the top materialization (or upper bound materialization) of this type, which is the /// most general form of the type that is fully static. #[must_use] - fn top_materialization(&self, db: &'db dyn Db) -> Type<'db> { - (*self).cached_materialization(db, MaterializationKind::Top) + fn top_materialization(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + (*self).cached_materialization(db, env.program(db), MaterializationKind::Top) } /// Returns the bottom materialization (or lower bound materialization) of this type, which is /// the most specific form of the type that is fully static. #[must_use] - fn bottom_materialization(&self, db: &'db dyn Db) -> Type<'db> { - (*self).cached_materialization(db, MaterializationKind::Bottom) + fn bottom_materialization(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + (*self).cached_materialization(db, env.program(db), MaterializationKind::Bottom) } #[salsa::tracked( returns(copy), - cycle_initial=|_, id, _, materialization_kind| { + cycle_initial=|_, id, _, _, materialization_kind| { Type::Divergent(DivergentType::new(id).materialized(materialization_kind)) }, - cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _, _| { - value.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _, program, _| { + value.cycle_normalized_impl(db, &ProgramEnvironment::from_program(program), *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] fn cached_materialization( self, db: &'db dyn Db, + program: Program, materialization_kind: MaterializationKind, ) -> Type<'db> { - self.materialize( - db, - materialization_kind, - &ApplyTypeMappingVisitor::default(), - ) + let env = &ProgramEnvironment::from_program(program); + self.materialize(db, materialization_kind, &ApplyTypeMappingVisitor::new(env)) } /// If this type is an instance type where the class has a tuple spec, returns the tuple spec. /// /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. /// For a subclass of `tuple[int, str]`, it will return the same tuple spec. - fn tuple_instance_spec(&self, db: &'db dyn Db) -> Option>> { + fn tuple_instance_spec( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { self.as_nominal_instance() - .and_then(|instance| instance.tuple_spec(db)) + .and_then(|instance| instance.tuple_spec(db, env)) } /// If this type is an *exact* tuple type (*not* a subclass of `tuple`), returns the @@ -1595,7 +1656,7 @@ impl<'db> Type<'db> { &self, db: &'db dyn Db, materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Type<'db> { self.apply_type_mapping_impl( db, @@ -1605,8 +1666,8 @@ impl<'db> Type<'db> { ) } - fn has_dynamic(self, db: &'db dyn Db) -> bool { - any_over_type(db, self, false, |ty| ty.is_dynamic()) + fn has_dynamic(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + any_over_type(db, env, self, false, |ty| ty.is_dynamic()) } const fn as_special_form(self) -> Option { @@ -1739,7 +1800,7 @@ impl<'db> Type<'db> { pub(crate) fn module_literal( db: &'db dyn Db, - importing_file: File, + importing_file: PythonFile<'db>, submodule: Module<'db>, ) -> Self { Self::ModuleLiteral(ModuleLiteralType::new( @@ -1842,12 +1903,16 @@ impl<'db> Type<'db> { } /// Detects types which are valid to appear inside a `Literal[…]` type annotation. - fn is_literal_or_union_of_literals(&self, db: &'db dyn Db) -> bool { + fn is_literal_or_union_of_literals( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { match self { Type::Union(union) => union .elements(db) .iter() - .all(|ty| ty.is_literal_or_union_of_literals(db)), + .all(|ty| ty.is_literal_or_union_of_literals(db, env)), Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::String(_) | LiteralValueTypeKind::Bytes(_) @@ -1856,7 +1921,9 @@ impl<'db> Type<'db> { | LiteralValueTypeKind::Enum(_) => true, LiteralValueTypeKind::LiteralString => false, }, - Type::NominalInstance(_) => self.is_none(db) || self.is_bool(db) || self.is_enum(db), + Type::NominalInstance(_) => { + self.is_none(db) || self.is_bool(db) || self.is_enum(db, env) + } _ => false, } } @@ -1915,11 +1982,11 @@ impl<'db> Type<'db> { } #[must_use] - fn negate(&self, db: &'db dyn Db) -> Type<'db> { + fn negate(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { // Avoid invoking the `IntersectionBuilder` for negations that are trivial. // // We verify that this always produces the same result as - // `IntersectionBuilder::new(db).add_negative(*self).build()` via the + // `IntersectionBuilder::new(db, env).add_negative(*self).build()` via the // property test `all_negated_types_identical_to_intersection_with_single_negated_element` match self { Type::Never => Type::object(), @@ -1965,14 +2032,16 @@ impl<'db> Type<'db> { )), Type::Union(_) | Type::Intersection(_) | Type::EnumComplement(_) => { - IntersectionBuilder::new(db).add_negative(*self).build() + IntersectionBuilder::new(db, env) + .add_negative(*self) + .build() } } } #[must_use] - fn negate_if(&self, db: &'db dyn Db, yes: bool) -> Type<'db> { - if yes { self.negate(db) } else { *self } + fn negate_if(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, yes: bool) -> Type<'db> { + if yes { self.negate(db, env) } else { *self } } /// Return `true` if it is possible to spell an equivalent type to this one @@ -2107,28 +2176,33 @@ impl<'db> Type<'db> { fn filter_disjoint_elements( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, inferable: TypeVarSet<'db>, ) -> Type<'db> { let constraints = ConstraintSetBuilder::new(); self.filter_union(db, |elem| { !elem - .when_disjoint_from(db, target, &constraints, inferable) - .is_always_satisfied(db) + .when_disjoint_from(db, env, target, &constraints, inferable) + .is_always_satisfied(db, env) }) } /// Returns the fallback instance type that a literal is an instance of, or `None` if the type /// is not a literal. - fn literal_fallback_instance(self, db: &'db dyn Db) -> Option> { + fn literal_fallback_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { // There are other literal types that could conceivable be included here: class literals // falling back to `type[X]`, for instance. For now, there is not much rigorous thought put // into what's included vs not; this is just an empirical choice that makes our ecosystem // report look better until we have proper bidirectional type inference. match self { - Type::ModuleLiteral(_) => Some(KnownClass::ModuleType.to_instance(db)), - Type::FunctionLiteral(_) => Some(KnownClass::FunctionType.to_instance(db)), - Type::LiteralValue(literal) => Some(literal.fallback_instance(db)), + Type::ModuleLiteral(_) => Some(KnownClass::ModuleType.to_instance(db, env)), + Type::FunctionLiteral(_) => Some(KnownClass::FunctionType.to_instance(db, env)), + Type::LiteralValue(literal) => Some(literal.fallback_instance(db, env)), _ => None, } } @@ -2138,17 +2212,22 @@ impl<'db> Type<'db> { /// Note that this function tries to promote literals to a more user-friendly form than their /// fallback instance type. For example, `def _() -> int` is promoted to `Callable[[], int]`, /// as opposed to `FunctionType`. - pub(crate) fn promote(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn promote(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { self.apply_type_mapping( db, + env, &TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular), TypeContext::default(), ) } /// Promote a top-level singleton type (like `None`, `EllipsisType`) to `T | Unknown`. - pub(crate) fn promote_singletons(self, db: &'db dyn Db) -> Type<'db> { - self.promote_singletons_impl(db) + pub(crate) fn promote_singletons( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.promote_singletons_impl(db, env) } /// Promote class literals to the class objects represented by `type[...]`. @@ -2156,9 +2235,10 @@ impl<'db> Type<'db> { /// This is intentionally separate from regular promotion. Applying it during collection /// inference would lose useful precision for local and module-level collections of class /// objects. - fn promote_class_literals(self, db: &'db dyn Db) -> Type<'db> { + fn promote_class_literals(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { self.apply_type_mapping( db, + env, &TypeMapping::Promote(PromotionMode::On, PromotionKind::ClassLiteralsOnly), TypeContext::default(), ) @@ -2168,28 +2248,35 @@ impl<'db> Type<'db> { /// `T | Unknown` within nominal type parameters, without recursing into unions. /// Used for collection literal inference so that `[None]` is inferred as /// `list[None | Unknown]` rather than `list[None]`. - fn promote_singletons_recursively(self, db: &'db dyn Db) -> Type<'db> { + fn promote_singletons_recursively( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { self.apply_type_mapping( db, + env, &TypeMapping::Promote(PromotionMode::On, PromotionKind::SingletonsOnly), TypeContext::default(), ) } /// Like [`Type::promote`], but does not recurse into nested types. - fn promote_impl(self, db: &'db dyn Db) -> Type<'db> { + fn promote_impl(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { - Type::LiteralValue(literal) if literal.is_promotable() => literal.fallback_instance(db), + Type::LiteralValue(literal) if literal.is_promotable() => { + literal.fallback_instance(db, env) + } Type::FunctionLiteral(literal) => Type::Callable(literal.into_callable_type(db)), _ => self, } } /// Like [`Type::promote_singletons_recursively`], but does not recurse into nested types. - fn promote_singletons_impl(self, db: &'db dyn Db) -> Type<'db> { + fn promote_singletons_impl(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { Type::NominalInstance(instance) if instance.is_singleton(db) => { - UnionType::from_two_elements(db, self, Type::unknown()) + UnionType::from_two_elements(db, env, self, Type::unknown()) } _ => self, } @@ -2210,9 +2297,23 @@ impl<'db> Type<'db> { /// If this continues, the query will not converge, so this method is called in the cycle recovery function. /// Then `tuple[tuple[Divergent, Literal[1]], Literal[1]]` is replaced with `tuple[Divergent, Literal[1]]` and the query converges. #[must_use] - pub(crate) fn recursive_type_normalized(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self { + pub(crate) fn recursive_type_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + cycle: &salsa::Cycle, + ) -> Self { + self.recursive_type_normalized_impl_with_cycle(db, env, cycle) + } + + fn recursive_type_normalized_impl_with_cycle( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + cycle: &salsa::Cycle, + ) -> Self { cycle.head_ids().fold(self, |ty, id| { - ty.recursive_type_normalized_impl(db, Type::divergent(id), false) + ty.recursive_type_normalized_impl(db, env, Type::divergent(id), false) .unwrap_or(Type::divergent(id)) }) } @@ -2236,6 +2337,7 @@ impl<'db> Type<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -2243,59 +2345,59 @@ impl<'db> Type<'db> { return None; } match self { - Type::Union(union) => union.recursive_type_normalized_impl(db, div, nested), + Type::Union(union) => union.recursive_type_normalized_impl(db, env, div, nested), Type::Intersection(intersection) => intersection - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::Intersection), Type::EnumComplement(complement) => complement - .to_intersection(db) - .recursive_type_normalized_impl(db, div, nested), + .to_intersection(db, env) + .recursive_type_normalized_impl(db, env, div, nested), Type::Callable(callable) => callable - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::Callable), Type::ProtocolInstance(protocol) => protocol - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::ProtocolInstance), Type::NominalInstance(instance) => instance - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::NominalInstance), Type::FunctionLiteral(function) => function - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::FunctionLiteral), Type::PropertyInstance(property) => property - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::PropertyInstance), Type::KnownBoundMethod(method_kind) => method_kind - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::KnownBoundMethod), Type::BoundMethod(method) => method - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::BoundMethod), Type::BoundSuper(bound_super) => bound_super - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::BoundSuper), Type::GenericAlias(generic) => generic - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::GenericAlias), Type::ClassLiteral(class) => class - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::ClassLiteral), Type::SubclassOf(subclass_of) => subclass_of - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::SubclassOf), Type::TypeVar(_) => Some(self), Type::KnownInstance(known_instance) => known_instance - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::KnownInstance), Type::TypeIs(type_is) => { - recursive_type_normalize_type_guard_like(db, type_is, div, nested) + recursive_type_normalize_type_guard_like(db, env, type_is, div, nested) } Type::TypeGuard(type_guard) => { - recursive_type_normalize_type_guard_like(db, type_guard, div, nested) + recursive_type_normalize_type_guard_like(db, env, type_guard, div, nested) } Type::TypeForm(typeform) => typeform .type_argument(db) - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(|ty| TypeFormType::from_type_expression(db, ty)), Type::Divergent(_) => Some(self), Type::Dynamic(dynamic) => Some(Type::Dynamic(dynamic.recursive_type_normalized())), @@ -2305,7 +2407,7 @@ impl<'db> Type<'db> { } Type::TypeAlias(_) => Some(self), Type::NewTypeInstance(newtype) => newtype - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Type::NewTypeInstance), Type::AlwaysFalsy | Type::AlwaysTruthy @@ -2323,12 +2425,13 @@ impl<'db> Type<'db> { /// /// The provided closure will be called on any nested types, along with their variance with /// respect to the outermost type. - fn visit_specialization(self, db: &'db dyn Db, mut f: F) + fn visit_specialization(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, mut f: F) where F: FnMut(Type<'db>, TypeVarVariance), { self.visit_specialization_impl( db, + env, TypeVarVariance::Covariant, &mut f, &SpecializationVisitor::default(), @@ -2338,26 +2441,27 @@ impl<'db> Type<'db> { fn visit_specialization_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, polarity: TypeVarVariance, f: &mut dyn FnMut(Type<'db>, TypeVarVariance), visitor: &SpecializationVisitor<'db>, ) { - let Some((_, specialization)) = self.class_specialization(db) else { + let Some((_, specialization)) = self.class_specialization(db, env) else { match self { Type::Union(union) => { for element in union.elements(db) { - element.visit_specialization_impl(db, polarity, f, visitor); + element.visit_specialization_impl(db, env, polarity, f, visitor); } } Type::Intersection(intersection) => { for element in intersection.positive(db) { - element.visit_specialization_impl(db, polarity, f, visitor); + element.visit_specialization_impl(db, env, polarity, f, visitor); } } Type::TypeAlias(alias) => visitor.visit(db, self, || { alias .value_type(db) - .visit_specialization_impl(db, polarity, f, visitor); + .visit_specialization_impl(db, env, polarity, f, visitor); }), Type::Callable(callable) => { for signature in callable.signatures(db) { @@ -2369,14 +2473,14 @@ impl<'db> Type<'db> { visitor.visit(db, parameter.annotated_type(), || { parameter .annotated_type() - .visit_specialization_impl(db, variance, f, visitor); + .visit_specialization_impl(db, env, variance, f, visitor); }); } visitor.visit(db, signature.return_ty, || { signature .return_ty - .visit_specialization_impl(db, polarity, f, visitor); + .visit_specialization_impl(db, env, polarity, f, visitor); }); } } @@ -2395,7 +2499,7 @@ impl<'db> Type<'db> { f(*ty, variance); visitor.visit(db, *ty, || { - ty.visit_specialization_impl(db, variance, f, visitor); + ty.visit_specialization_impl(db, env, variance, f, visitor); }); } } @@ -2404,7 +2508,7 @@ impl<'db> Type<'db> { /// /// Note: This function aims to have no false positives, but might return `false` /// for more complicated types that are actually singletons. - fn is_singleton(self, db: &'db dyn Db) -> bool { + fn is_singleton(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { match self { Type::Dynamic(_) | Type::Divergent(_) | Type::Never => false, @@ -2448,13 +2552,13 @@ impl<'db> Type<'db> { // A constrained typevar is a singleton if all of its constraints are singletons. (Note // that you cannot specialize a constrained typevar to a subtype of a constraint.) Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { None => false, Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints .elements(db) .iter() - .all(|constraint| constraint.is_singleton(db)), + .all(|constraint| constraint.is_singleton(db, env)), } } @@ -2501,7 +2605,7 @@ impl<'db> Type<'db> { false } Type::Intersection(intersection) => intersection - .enum_complement(db) + .enum_complement(db, env) .is_some_and(|complement| complement.is_singleton(db)), Type::EnumComplement(complement) => complement.is_singleton(db), Type::AlwaysTruthy | Type::AlwaysFalsy => false, @@ -2509,8 +2613,8 @@ impl<'db> Type<'db> { Type::TypeGuard(type_guard) => type_guard.is_bound(db), Type::TypeForm(_) => false, Type::TypedDict(_) => false, - Type::TypeAlias(alias) => alias.value_type(db).is_singleton(db), - Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).is_singleton(db), + Type::TypeAlias(alias) => alias.value_type(db).is_singleton(db, env), + Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).is_singleton(db, env), } } @@ -2521,32 +2625,40 @@ impl<'db> Type<'db> { /// /// [descriptor guide]: https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance /// [`_PyType_Lookup`]: https://github.com/python/cpython/blob/e285232c76606e3be7bf216efb1be1e742423e4b/Objects/typeobject.c#L5223 - fn find_name_in_mro(&self, db: &'db dyn Db, name: &str) -> Option> { - self.find_name_in_mro_with_policy(db, name, MemberLookupPolicy::default()) + fn find_name_in_mro( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Option> { + self.find_name_in_mro_with_policy(db, env, name, MemberLookupPolicy::default()) } fn find_name_in_mro_with_policy( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> Option> { if let Some(fallback) = (*self).materialized_divergent_fallback() { - return fallback.find_name_in_mro_with_policy(db, name, policy); + return fallback.find_name_in_mro_with_policy(db, env, name, policy); } match self { - Type::Union(union) => Some(union.map_with_boundness_and_qualifiers(db, |elem| { - elem.find_name_in_mro_with_policy(db, name, policy) - // If some elements are classes, and some are not, we simply fall back to `Unbound` for the non-class - // elements instead of short-circuiting the whole result to `None`. We would need a more detailed - // return type otherwise, and since `find_name_in_mro` is usually called via `class_member`, this is - // not a problem. - .unwrap_or_default() - })), + Type::Union(union) => { + Some(union.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.find_name_in_mro_with_policy(db, env, name, policy) + // If some elements are classes, and some are not, we simply fall back to `Unbound` for the non-class + // elements instead of short-circuiting the whole result to `None`. We would need a more detailed + // return type otherwise, and since `find_name_in_mro` is usually called via `class_member`, this is + // not a problem. + .unwrap_or_default() + })) + } Type::Intersection(inter) => { - Some(inter.map_with_boundness_and_qualifiers(db, |elem| { - elem.find_name_in_mro_with_policy(db, name, policy) + Some(inter.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.find_name_in_mro_with_policy(db, env, name, policy) // Fall back to Unbound, similar to the union case (see above). .unwrap_or_default() })) @@ -2557,7 +2669,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) | Type::Divergent(_) | Type::Never => Some(Place::bound(self).into()), Type::ClassLiteral(class) if class.is_typed_dict(db) => { - Some(class.typed_dict_member(db, None, name, policy)) + Some(class.typed_dict_member(db, env, None, name, policy)) } Type::ClassLiteral(class) => { @@ -2591,27 +2703,29 @@ impl<'db> Type<'db> { .into(), ), - _ => Some(class.class_member(db, name, policy)), + _ => Some(class.class_member(db, env, name, policy)), } } - Type::GenericAlias(alias) if alias.is_typed_dict(db) => { - Some(alias.origin(db).typed_dict_member(db, None, name, policy)) - } + Type::GenericAlias(alias) if alias.is_typed_dict(db) => Some( + alias + .origin(db) + .typed_dict_member(db, env, None, name, policy), + ), Type::GenericAlias(alias) => { - Some(ClassType::from(*alias).class_member(db, name, policy)) + Some(ClassType::from(*alias).class_member(db, env, name, policy)) } Type::SubclassOf(subclass_of_ty) => { - subclass_of_ty.find_name_in_mro_with_policy(db, name, policy) + subclass_of_ty.find_name_in_mro_with_policy(db, env, name, policy) } // Note: `super(pivot, owner).__class__` is `builtins.super`, not the owner's class. // `BoundSuper` should look up the name in the MRO of `builtins.super`. Type::BoundSuper(_) => KnownClass::Super - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy), + .to_class_literal(db, env) + .find_name_in_mro_with_policy(db, env, name, policy), // 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 @@ -2622,14 +2736,14 @@ impl<'db> Type<'db> { Some(Place::Undefined.into()) } else { KnownClass::Object - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy) + .to_class_literal(db, env) + .find_name_in_mro_with_policy(db, env, name, policy) } } Type::TypeAlias(alias) => alias .value_type(db) - .find_name_in_mro_with_policy(db, name, policy), + .find_name_in_mro_with_policy(db, env, name, policy), Type::FunctionLiteral(_) | Type::Callable(_) @@ -2657,21 +2771,26 @@ impl<'db> Type<'db> { } } - fn lookup_dunder_new(self, db: &'db dyn Db) -> Option> { - #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, ()| None, heap_size=ruff_memory_usage::heap_size)] + fn lookup_dunder_new( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] fn lookup_dunder_new_inner<'db>( db: &'db dyn Db, + program: Program, ty: Type<'db>, - _: (), ) -> Option> { + let env = &ProgramEnvironment::from_program(program); let mut flags = MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK; - if !ty.is_subtype_of(db, KnownClass::Type.to_instance(db)) { + if !ty.is_subtype_of(db, env, KnownClass::Type.to_instance(db, env)) { flags |= MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK; } - ty.find_name_in_mro_with_policy(db, "__new__", flags) + ty.find_name_in_mro_with_policy(db, env, "__new__", flags) } - lookup_dunder_new_inner(db, self, ()) + lookup_dunder_new_inner(db, env.program(db), self) } /// Look up an attribute in the MRO of the meta-type of `self`. This returns class-level attributes @@ -2679,24 +2798,33 @@ impl<'db> Type<'db> { /// /// Basically corresponds to `self.to_meta_type().find_name_in_mro(name)`, except for the handling /// of union and intersection types. - fn class_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - self.class_member_with_policy(db, name, MemberLookupPolicy::default()) + fn class_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + self.class_member_with_policy(db, env, name, MemberLookupPolicy::default()) } fn class_member_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - Self::class_member_with_policy_inner(db, MemberLookupKey::new(db, self, name, policy)) + Self::class_member_with_policy_inner( + db, + MemberLookupKey::new(db, env.program(db), self, name, policy), + ) } #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Place::bound(Type::divergent(id)).into(), - cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, member: PlaceAndQualifiers<'db>, _| { - member.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, member: PlaceAndQualifiers<'db>, key: MemberLookupKey<'db>| { + member.cycle_normalized(db, &ProgramEnvironment::from_program(key.program(db)), *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -2707,32 +2835,34 @@ impl<'db> Type<'db> { let ty = key.ty(db); let name = key.name(db); let policy = key.policy(db); + let program = key.program(db); + let env = &ProgramEnvironment::from_program(program); - tracing::trace!("class_member: {}.{}", ty.display(db), name); + tracing::trace!("class_member: {}.{}", ty.display(db, env), name); if let Some(fallback) = ty.materialized_divergent_fallback() { - return fallback.class_member_with_policy(db, name, policy); + return fallback.class_member_with_policy(db, env, name, policy); } if let Type::ProtocolInstance(protocol) = ty && let Some(origin) = protocol.materialized_origin(db) { let interface = protocol.interface(db); return if interface.includes_member(db, name) { - interface.instance_member(db, name) + interface.instance_member(db, env, name) } else { - Type::instance(db, *origin).class_member_with_policy(db, name, policy) + Type::instance(db, env, *origin).class_member_with_policy(db, env, name, policy) }; } match ty { - Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| { - elem.class_member_with_policy(db, name, policy) + Type::Union(union) => union.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.class_member_with_policy(db, env, name, policy) }), - Type::Intersection(inter) => inter.map_with_boundness_and_qualifiers(db, |elem| { - elem.class_member_with_policy(db, name, policy) + Type::Intersection(inter) => inter.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.class_member_with_policy(db, env, name, policy) }), // TODO: Remove this once synthesized protocols have a precise meta-type. Type::ProtocolInstance(protocol) if protocol.class_origin(db).is_none() => { - ty.instance_member(db, name) + ty.instance_member(db, env, name) } Type::LiteralValue(literal) @@ -2761,32 +2891,35 @@ impl<'db> Type<'db> { // their correct types instead of collapsing to `Any`/`Unknown`. Type::SubclassOf(subclass_of) if subclass_of.is_dynamic() => { let type_result = KnownClass::Type - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, policy) + .to_class_literal(db, env) + .find_name_in_mro_with_policy(db, env, name, policy) .expect("`find_name_in_mro` should return `Some` for a class literal"); if !type_result.place.is_undefined() { type_result } else { - ty.to_meta_type(db) - .find_name_in_mro_with_policy(db, name, policy) + ty.to_meta_type(db, env) + .find_name_in_mro_with_policy(db, env, name, policy) .expect( "`Type::find_name_in_mro()` should return `Some()` when called on a meta-type", ) } } - Type::NominalInstance(instance) => { - ty.to_meta_type(db) - .class_namespace_member(db, instance.class(db), name, policy) - } + Type::NominalInstance(instance) => ty.to_meta_type(db, env).class_namespace_member( + db, + env, + instance.class(db, env), + name, + policy, + ), - Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { - ty.to_meta_type(db).class_object_member(db, name, policy) - } + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => ty + .to_meta_type(db, env) + .class_object_member(db, env, name, policy), _ => ty - .to_meta_type(db) - .find_name_in_mro_with_policy(db, name, policy) + .to_meta_type(db, env) + .find_name_in_mro_with_policy(db, env, name, policy) .expect( "`Type::find_name_in_mro()` should return `Some()` when called on a meta-type", ), @@ -2800,21 +2933,23 @@ impl<'db> Type<'db> { /// Add those attributes using the same lookup as a concrete nominal instance. fn instance_lookup_class_member_with_policy( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, key: MemberLookupKey<'db>, ) -> PlaceAndQualifiers<'db> { let ty = key.ty(db); - if let Type::TypeVar(_) = ty - && let Some(class) = ty.nominal_class(db) - { - let name = key.name(db); - let policy = key.policy(db); + if let Type::TypeVar(_) = ty { + if let Some(class) = ty.nominal_class(db, env) { + let name = key.name(db); + let policy = key.policy(db); - ty.to_meta_type(db) - .class_namespace_member(db, class, name, policy) - } else { - Self::class_member_with_policy_inner(db, key) + return ty + .to_meta_type(db, env) + .class_namespace_member(db, env, class, name, policy); + } } + + Self::class_member_with_policy_inner(db, key) } /// Look up attributes stored in the namespace of a class object. @@ -2825,10 +2960,13 @@ impl<'db> Type<'db> { fn class_object_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - let class_attr = self.find_name_in_mro_with_policy(db, name, policy).expect( + let class_attr = self + .find_name_in_mro_with_policy(db, env, name, policy) + .expect( "Calling `class_object_member` on class literals and subclass-of types should always find an MRO", ); @@ -2837,11 +2975,12 @@ impl<'db> Type<'db> { SubclassOfInner::Protocol(protocol) => { protocol.class_origin(db).map(|origin| *origin) } - subclass_of => subclass_of.into_class(db), + subclass_of => subclass_of.into_class(db, env), }, _ => self.to_class_type(db), }; - let own_class_attr = own_class.map(|class| class.own_class_member(db, None, name).inner); + let own_class_attr = + own_class.map(|class| class.own_class_member(db, env, None, name).inner); // A definitely-declared attribute in this class's own namespace is the contract for // values populated by metaclass initialization, analogous to a declared instance @@ -2863,17 +3002,20 @@ impl<'db> Type<'db> { return class_attr; } - let Some(metaclass_instance) = self.to_meta_type(db).to_instance_approximation(db) else { + let Some(metaclass_instance) = self + .to_meta_type(db, env) + .to_instance_approximation(db, env) + else { return class_attr; }; - let metaclass_attr = metaclass_instance.instance_member(db, name); + let metaclass_attr = metaclass_instance.instance_member(db, env, name); if own_declaration_definedness.is_some() { // A conditionally-declared attribute is a contract only on paths where that // declaration is present; the metaclass value is the fallback on other paths. - class_attr.or_fall_back_to(db, || metaclass_attr) + class_attr.or_fall_back_to(db, env, || metaclass_attr) } else { - metaclass_attr.or_fall_back_to(db, || class_attr) + metaclass_attr.or_fall_back_to(db, env, || class_attr) } } @@ -2914,21 +3056,22 @@ impl<'db> Type<'db> { fn class_namespace_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassType<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { let class_attr = self - .find_name_in_mro_with_policy(db, name, policy) + .find_name_in_mro_with_policy(db, env, name, policy) .expect("The meta-type of an instance-like type should always have an MRO"); let Some(metaclass) = class .metaclass(db) - .to_instance_approximation(db) - .and_then(|metaclass| metaclass.nominal_class(db)) + .to_instance_approximation(db, env) + .and_then(|metaclass| metaclass.nominal_class(db, env)) else { return class_attr; }; - let metaclass_member = metaclass.instance_member(db, name); + let metaclass_member = metaclass.instance_member(db, env, name); if metaclass_member.is_undefined() { return class_attr; } @@ -2938,6 +3081,7 @@ impl<'db> Type<'db> { let own_class_member = class.class_literal(db).class_member_from_mro( db, + env, name, policy, class.iter_mro(db).take(1), @@ -2952,6 +3096,7 @@ impl<'db> Type<'db> { .is_some_and(|symbol| { place_from_bindings( db, + env, use_def_map(db, scope).end_of_scope_symbol_bindings(symbol), ) .place @@ -2964,6 +3109,7 @@ impl<'db> Type<'db> { }; let inherited_class_member = class.class_literal(db).class_member_from_mro( db, + env, name, policy, class.iter_mro(db).skip(1), @@ -2975,8 +3121,8 @@ impl<'db> Type<'db> { metaclass_member }; let class_member = own_class_member - .or_fall_back_to(db, || metaclass_member) - .or_fall_back_to(db, || inherited_class_member); + .or_fall_back_to(db, env, || metaclass_member) + .or_fall_back_to(db, env, || inherited_class_member); let class_member = if metaclass_member_is_implicit { // Preserve the existing convention that an inferred instance member is assumed to be // available even when no lower-precedence fallback exists. @@ -3003,7 +3149,7 @@ impl<'db> Type<'db> { let Some(class_member_ty) = class_member.ignore_possibly_undefined() else { return dynamic_instance_fallback; }; - if !class_member_ty.may_be_data_descriptor(db) { + if !class_member_ty.may_be_data_descriptor(db, env) { return dynamic_instance_fallback; } let PlaceAndQualifiers { @@ -3021,12 +3167,12 @@ impl<'db> Type<'db> { union .elements(db) .iter() - .all(|ty| ty.may_be_data_descriptor(db)) + .all(|ty| ty.may_be_data_descriptor(db, env)) }); Place::Defined(DefinedPlace { ty: declaration .ty - .filter_union(db, |ty| ty.may_be_data_descriptor(db)), + .filter_union(db, |ty| ty.may_be_data_descriptor(db, env)), definedness: if all_arms_are_possible_data_descriptors { declaration.definedness } else { @@ -3035,7 +3181,7 @@ impl<'db> Type<'db> { ..declaration }) .with_qualifiers(qualifiers) - .or_fall_back_to(db, || dynamic_instance_fallback) + .or_fall_back_to(db, env, || dynamic_instance_fallback) } /// This function roughly corresponds to looking up an attribute in the `__dict__` of an object. @@ -3054,93 +3200,106 @@ impl<'db> Type<'db> { /// def __init__(self): /// self.b: str = "a" /// ``` - fn instance_member(&self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + fn instance_member( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { match self { - Type::Union(union) => { - union.map_with_boundness_and_qualifiers(db, |elem| elem.instance_member(db, name)) - } + Type::Union(union) => union.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.instance_member(db, env, name) + }), Type::Intersection(intersection) => { - if let Some(complement) = intersection.enum_complement(db) { - enums::instance_member_for_enum_complement(db, complement, name) + if let Some(complement) = intersection.enum_complement(db, env) { + enums::instance_member_for_enum_complement(db, env, complement, name) } else { - intersection.map_with_boundness_and_qualifiers(db, |elem| { - elem.instance_member(db, name) + intersection.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.instance_member(db, env, name) }) } } Type::EnumComplement(complement) => { - enums::instance_member_for_enum_complement(db, *complement, name) + enums::instance_member_for_enum_complement(db, env, *complement, name) } Type::Dynamic(_) | Type::Divergent(_) | Type::Never => Place::bound(self).into(), - Type::NominalInstance(instance) => instance.class(db).instance_member(db, name), - Type::NewTypeInstance(newtype) => { - newtype.concrete_base_type(db).instance_member(db, name) + Type::NominalInstance(instance) => { + instance.class(db, env).instance_member(db, env, name) } + Type::NewTypeInstance(newtype) => newtype + .concrete_base_type(db) + .instance_member(db, env, name), - Type::ProtocolInstance(protocol) => protocol.instance_member(db, name), + Type::ProtocolInstance(protocol) => protocol.instance_member(db, env, name), Type::FunctionLiteral(_) => KnownClass::FunctionType - .to_instance(db) - .instance_member(db, name), + .to_instance(db, env) + .instance_member(db, env, name), Type::BoundMethod(_) => KnownClass::MethodType - .to_instance(db) - .instance_member(db, name), - Type::KnownBoundMethod(method) => { - method.class().to_instance(db).instance_member(db, name) - } + .to_instance(db, env) + .instance_member(db, env, name), + Type::KnownBoundMethod(method) => method + .class() + .to_instance(db, env) + .instance_member(db, env, name), Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType - .to_instance(db) - .instance_member(db, name), + .to_instance(db, env) + .instance_member(db, env, name), Type::DataclassDecorator(_) => KnownClass::FunctionType - .to_instance(db) - .instance_member(db, name), + .to_instance(db, env) + .instance_member(db, env, name), Type::Callable(_) | Type::DataclassTransformer(_) => { - Type::object().instance_member(db, name) + Type::object().instance_member(db, env, name) } Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { - None => Type::object().instance_member(db, name), + match bound_typevar.typevar(db).bound_or_constraints(db, env) { + None => Type::object().instance_member(db, env, name), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.instance_member(db, name) + bound.instance_member(db, env, name) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .map_with_boundness_and_qualifiers(db, |constraint| { - constraint.instance_member(db, name) + .map_with_boundness_and_qualifiers(db, env, |constraint| { + constraint.instance_member(db, env, name) }), } } - Type::TypeIs(_) | Type::TypeGuard(_) => { - KnownClass::Bool.to_instance(db).instance_member(db, name) - } + Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool + .to_instance(db, env) + .instance_member(db, env, name), - Type::LiteralValue(literal) => literal.fallback_instance(db).instance_member(db, name), + Type::LiteralValue(literal) => literal + .fallback_instance(db, env) + .instance_member(db, env, name), Type::AlwaysTruthy | Type::AlwaysFalsy | Type::TypeForm(_) => { - Type::object().instance_member(db, name) + Type::object().instance_member(db, env, name) } Type::ModuleLiteral(_) => KnownClass::ModuleType - .to_instance(db) - .instance_member(db, name), + .to_instance(db, env) + .instance_member(db, env, name), Type::SpecialForm(_) | Type::KnownInstance(_) => Place::Undefined.into(), - Type::PropertyInstance(property) => { - property.instance_fallback(db).instance_member(db, name) - } + Type::PropertyInstance(property) => property + .instance_class(db) + .to_instance(db, env) + .instance_member(db, env, name), // Note: `super(pivot, owner).__dict__` refers to the `__dict__` of the `builtins.super` instance, // not that of the owner. // This means we should only look up instance members defined on the `builtins.super()` instance itself. // If you want to look up a member in the MRO of the `super`'s owner, // refer to [`Type::member`] instead. - Type::BoundSuper(_) => KnownClass::Super.to_instance(db).instance_member(db, name), + Type::BoundSuper(_) => KnownClass::Super + .to_instance(db, env) + .instance_member(db, env, name), // TODO: we currently don't model the fact that class literals and subclass-of types have // a `__dict__` that is filled with class level attributes. Modeling this is currently not @@ -3152,7 +3311,7 @@ impl<'db> Type<'db> { Type::TypedDict(_) => Place::Undefined.into(), - Type::TypeAlias(alias) => alias.value_type(db).instance_member(db, name), + Type::TypeAlias(alias) => alias.value_type(db).instance_member(db, env, name), } } @@ -3160,17 +3319,23 @@ impl<'db> Type<'db> { /// method corresponds to `inspect.getattr_static(, name)`. /// /// See also: [`Type::member`] - fn static_member(&self, db: &'db dyn Db, name: &str) -> Place<'db> { + fn static_member( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Place<'db> { if let Type::ModuleLiteral(module) = self { - module.static_member(db, name).place - } else if let place @ Place::Defined(_) = self.class_member(db, name).place { + module.static_member(db, env, name).place + } else if let place @ Place::Defined(_) = self.class_member(db, env, name).place { place - } else if let Some(place @ Place::Defined(_)) = - self.find_name_in_mro(db, name).map(|inner| inner.place) + } else if let Some(place @ Place::Defined(_)) = self + .find_name_in_mro(db, env, name) + .map(|inner| inner.place) { place } else { - self.instance_member(db, name).place + self.instance_member(db, env, name).place } } @@ -3197,18 +3362,21 @@ impl<'db> Type<'db> { pub(crate) fn try_call_dunder_get( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, instance: Option>, owner: Type<'db>, ) -> Option<(Type<'db>, AttributeKind)> { - #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] fn try_call_dunder_get_inner<'db>( db: &'db dyn Db, + program: Program, ty: Type<'db>, instance: Option>, owner: Type<'db>, ) -> Option<(Type<'db>, AttributeKind)> { + let env = &ProgramEnvironment::from_program(program); if let Some(fallback) = ty.materialized_divergent_fallback() { - return fallback.try_call_dunder_get(db, instance, owner); + return fallback.try_call_dunder_get(db, env, instance, owner); } if let Some(dynamic) = ty.dynamic_descriptor_type() { @@ -3238,11 +3406,11 @@ impl<'db> Type<'db> { } else { let self_type = instance.unwrap_or_else(|| { // For classmethod-like callables, bind to the owner class. - owner.to_instance_approximation(db).unwrap_or(owner) + owner.to_instance_approximation(db, env).unwrap_or(owner) }); Some(( - Type::Callable(callable.bind_self(db, Some(self_type))), + Type::Callable(callable.bind_self(db, env, Some(self_type))), AttributeKind::NormalOrNonDataDescriptor, )) }; @@ -3254,7 +3422,7 @@ impl<'db> Type<'db> { ty: concrete_descr_get, .. }) = ty - .class_member_with_policy(db, "__get__", MemberLookupPolicy::REQUIRE_CONCRETE) + .class_member_with_policy(db, env, "__get__", MemberLookupPolicy::REQUIRE_CONCRETE) .place else { return None; @@ -3273,27 +3441,36 @@ impl<'db> Type<'db> { definedness: descr_get_boundness, .. }) = ty - .class_member_with_policy(db, "__get__", MemberLookupPolicy::NO_INSTANCE_FALLBACK) + .class_member_with_policy( + db, + env, + "__get__", + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) .place else { return None; }; - let instance_ty = instance.unwrap_or_else(|| Type::none(db)); + let instance_ty = instance.unwrap_or_else(|| Type::none(db, env)); let return_ty = descr_get - .try_call(db, &CallArguments::positional([ty, instance_ty, owner])) + .try_call( + db, + env, + &CallArguments::positional([ty, instance_ty, owner]), + ) .map(|bindings| { if descr_get_boundness == Definedness::AlwaysDefined { - bindings.return_type(db) + bindings.return_type(db, env) } else { - UnionType::from_two_elements(db, bindings.return_type(db), ty) + UnionType::from_two_elements(db, env, bindings.return_type(db, env), ty) } }) // TODO: an error when calling `__get__` will lead to a `TypeError` or similar at runtime; // we should emit a diagnostic here instead of silently ignoring the error. - .unwrap_or_else(|CallError(_, bindings)| bindings.return_type(db)); + .unwrap_or_else(|CallError(_, bindings)| bindings.return_type(db, env)); - let descriptor_kind = if ty.is_data_descriptor(db) { + let descriptor_kind = if ty.is_data_descriptor(db, env) { AttributeKind::DataDescriptor } else { AttributeKind::NormalOrNonDataDescriptor @@ -3304,9 +3481,11 @@ impl<'db> Type<'db> { tracing::trace!( "try_call_dunder_get: {}, {}, {}", - self.display(db), - instance.unwrap_or_else(|| Type::none(db)).display(db), - owner.display(db) + self.display(db, env), + instance + .unwrap_or_else(|| Type::none(db, env)) + .display(db, env), + owner.display(db, env) ); // Function descriptors have fixed binding behavior, so avoid retaining a tracked query @@ -3325,7 +3504,7 @@ impl<'db> Type<'db> { return Some((descriptor_result, AttributeKind::NormalOrNonDataDescriptor)); } - try_call_dunder_get_inner(db, self, instance, owner) + try_call_dunder_get_inner(db, env.program(db), self, instance, owner) } /// Look up `__get__` on the meta-type of `attribute`, and call it with `attribute`, `instance`, @@ -3333,6 +3512,7 @@ impl<'db> Type<'db> { /// and intersections explicitly. fn try_call_dunder_get_on_attribute( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, attribute: PlaceAndQualifiers<'db>, instance: Option>, owner: Type<'db>, @@ -3352,6 +3532,7 @@ impl<'db> Type<'db> { { return Self::try_call_dunder_get_on_attribute( db, + env, Place::Defined(DefinedPlace { ty: fallback, origin, @@ -3392,8 +3573,8 @@ impl<'db> Type<'db> { let mut all_data_descriptors = true; let place = union - .map_with_boundness(db, |elem| { - let ty = match elem.try_call_dunder_get(db, instance, owner) { + .map_with_boundness(db, env, |elem| { + let ty = match elem.try_call_dunder_get(db, env, instance, owner) { Some((ty, kind)) => { all_data_descriptors &= kind.is_data(); ty @@ -3438,10 +3619,10 @@ impl<'db> Type<'db> { attribute } else { intersection - .map_with_boundness(db, |elem| { + .map_with_boundness(db, env, |elem| { Place::Defined(DefinedPlace { ty: elem - .try_call_dunder_get(db, instance, owner) + .try_call_dunder_get(db, env, instance, owner) .map_or(*elem, |(ty, _)| ty), origin, definedness, @@ -3467,7 +3648,7 @@ impl<'db> Type<'db> { qualifiers: _, } => { if let Some((return_ty, attribute_kind)) = - attribute_ty.try_call_dunder_get(db, instance, owner) + attribute_ty.try_call_dunder_get(db, env, instance, owner) { ( Place::Defined(DefinedPlace { @@ -3492,8 +3673,8 @@ impl<'db> Type<'db> { /// Returns whether this type is a data descriptor, i.e. defines `__set__` or `__delete__`. /// If this type is a union, requires all elements of union to be data descriptors. /// A directly dynamic type is treated as a data descriptor because it could inhabit one. - fn is_data_descriptor(self, d: &'db dyn Db) -> bool { - self.is_data_descriptor_impl(d, false) + fn is_data_descriptor(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + self.is_data_descriptor_impl(db, env.program(db), false) } /// Returns whether this type should be considered a possible data descriptor. @@ -3501,38 +3682,43 @@ impl<'db> Type<'db> { /// This is used to determine whether an attribute assignment is valid for narrowing. /// For practical convenience, dynamic union elements are not considered possible data /// descriptors here, because doing so would disable narrowing too frequently. - fn may_be_data_descriptor(self, d: &'db dyn Db) -> bool { - self.is_data_descriptor_impl(d, true) + fn may_be_data_descriptor(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + self.is_data_descriptor_impl(db, env.program(db), true) } /// Returns whether this type is known not to be a data descriptor. /// /// Descriptor uncertainty only propagates through outer unions, intersections, and aliases; /// type arguments do not affect the runtime descriptor class. - fn is_definitely_non_data_descriptor(self, db: &'db dyn Db) -> bool { - self.is_definitely_non_data_descriptor_impl(db, ()) + fn is_definitely_non_data_descriptor( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + self.is_definitely_non_data_descriptor_impl(db, env.program(db)) } // Recursive aliases use `true`, the identity for the all-of classifications above. #[salsa::tracked( returns(copy), - cycle_initial=|_, _, _, ()| true, + cycle_initial=|_, _, _, _| true, heap_size=ruff_memory_usage::heap_size )] - fn is_definitely_non_data_descriptor_impl(self, db: &'db dyn Db, (): ()) -> bool { + fn is_definitely_non_data_descriptor_impl(self, db: &'db dyn Db, program: Program) -> bool { + let env = &ProgramEnvironment::from_program(program); match self { Type::Dynamic(_) | Type::Divergent(_) | Type::TypeVar(_) => false, Type::Union(union) => union .elements(db) .iter() - .all(|ty| ty.is_definitely_non_data_descriptor_impl(db, ())), + .all(|ty| ty.is_definitely_non_data_descriptor_impl(db, program)), Type::Intersection(intersection) => intersection .iter_positive(db) - .all(|ty| ty.is_definitely_non_data_descriptor_impl(db, ())), + .all(|ty| ty.is_definitely_non_data_descriptor_impl(db, program)), Type::TypeAlias(alias) => alias .value_type(db) - .is_definitely_non_data_descriptor_impl(db, ()), - _ => !self.may_be_data_descriptor(db), + .is_definitely_non_data_descriptor_impl(db, program), + _ => !self.may_be_data_descriptor(db, env), } } @@ -3540,10 +3726,16 @@ impl<'db> Type<'db> { // Seed recursive aliases with the corresponding identity value. #[salsa::tracked( returns(copy), - cycle_initial=|_, _, _, any_of_union: bool| !any_of_union, + cycle_initial=|_, _, _, _, any_of_union: bool| !any_of_union, heap_size=ruff_memory_usage::heap_size )] - fn is_data_descriptor_impl(self, db: &'db dyn Db, any_of_union: bool) -> bool { + fn is_data_descriptor_impl( + self, + db: &'db dyn Db, + program: Program, + any_of_union: bool, + ) -> bool { + let env = &ProgramEnvironment::from_program(program); match self { Type::Dynamic(_) => !any_of_union, Type::SubclassOf(_) if self.dynamic_descriptor_type().is_some() => true, @@ -3551,25 +3743,33 @@ impl<'db> Type<'db> { Type::Union(union) if any_of_union => union .elements(db) .iter() - .any(|ty| ty.is_data_descriptor_impl(db, any_of_union)), + .any(|ty| ty.is_data_descriptor_impl(db, program, any_of_union)), Type::Union(union) => union .elements(db) .iter() - .all(|ty| ty.is_data_descriptor_impl(db, any_of_union)), + .all(|ty| ty.is_data_descriptor_impl(db, program, any_of_union)), Type::Intersection(intersection) => intersection .iter_positive(db) - .any(|ty| ty.is_data_descriptor_impl(db, any_of_union)), - Type::TypeAlias(alias) => alias - .value_type(db) - .is_data_descriptor_impl(db, any_of_union), + .any(|ty| ty.is_data_descriptor_impl(db, program, any_of_union)), + Type::TypeAlias(alias) => { + alias + .value_type(db) + .is_data_descriptor_impl(db, program, any_of_union) + } _ => { !self - .class_member_with_policy(db, "__set__", MemberLookupPolicy::REQUIRE_CONCRETE) + .class_member_with_policy( + db, + env, + "__set__", + MemberLookupPolicy::REQUIRE_CONCRETE, + ) .place .is_undefined() || !self .class_member_with_policy( db, + env, "__delete__", MemberLookupPolicy::REQUIRE_CONCRETE, ) @@ -3595,6 +3795,7 @@ impl<'db> Type<'db> { /// back to lower-precedence stages of the descriptor protocol by building union types. fn invoke_descriptor_protocol( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, key: MemberLookupKey<'db>, receiver: Type<'db>, fallback: PlaceAndQualifiers<'db>, @@ -3609,9 +3810,10 @@ impl<'db> Type<'db> { meta_attr_kind, ) = Self::try_call_dunder_get_on_attribute( db, - Self::instance_lookup_class_member_with_policy(db, key), + env, + Self::instance_lookup_class_member_with_policy(db, env, key), Some(receiver), - ty.to_meta_type(db), + ty.to_meta_type(db, env), ); let PlaceAndQualifiers { @@ -3657,7 +3859,7 @@ impl<'db> Type<'db> { provenance: fallback_provenance, }), ) => Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements(db, meta_attr_ty, fallback_ty), + ty: UnionType::from_two_elements(db, env, meta_attr_ty, fallback_ty), origin: meta_origin.merge(fallback_origin), definedness: fallback_boundness, public_type_policy: fallback_public_type_policy, @@ -3704,7 +3906,7 @@ impl<'db> Type<'db> { provenance: fallback_provenance, }), ) => Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements(db, meta_attr_ty, fallback_ty), + ty: UnionType::from_two_elements(db, env, meta_attr_ty, fallback_ty), origin: meta_origin.merge(fallback_origin), definedness: meta_attr_boundness.max(fallback_boundness), public_type_policy: fallback_public_type_policy, @@ -3725,8 +3927,13 @@ impl<'db> Type<'db> { /// TODO: We should return a `Result` here to handle errors that can appear during attribute /// lookup, like a failed `__get__` call on a descriptor. #[must_use] - fn member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - self.member_lookup_with_policy(db, name, MemberLookupPolicy::default()) + fn member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + self.member_lookup_with_policy(db, env, name, MemberLookupPolicy::default()) } /// Similar to [`Type::member`], but allows the caller to specify what policy should be used @@ -3734,10 +3941,11 @@ impl<'db> Type<'db> { pub(crate) fn member_lookup_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - self.member_lookup_with_policy_and_receiver(db, name, policy, None) + self.member_lookup_with_policy_and_receiver(db, env, name, policy, None) } /// Perform member lookup while optionally binding descriptors and `Self` to a more precise @@ -3748,6 +3956,7 @@ impl<'db> Type<'db> { fn member_lookup_with_policy_and_receiver( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, receiver: Option>, @@ -3755,8 +3964,8 @@ impl<'db> Type<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Place::bound(Type::divergent(id)).into(), - cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, member: PlaceAndQualifiers<'db>, _| { - member.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, member: PlaceAndQualifiers<'db>, key: MemberLookupKey<'db>| { + member.cycle_normalized(db, &ProgramEnvironment::from_program(key.program(db)), *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -3770,8 +3979,8 @@ impl<'db> Type<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _, _| Place::bound(Type::divergent(id)).into(), - cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, member: PlaceAndQualifiers<'db>, _, _| { - member.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, member: PlaceAndQualifiers<'db>, key: MemberLookupKey<'db>, _| { + member.cycle_normalized(db, &ProgramEnvironment::from_program(key.program(db)), *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -3790,6 +3999,7 @@ impl<'db> Type<'db> { ) -> PlaceAndQualifiers<'db> { fn promote_inferred_attribute_class_literals<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, result: PlaceAndQualifiers<'db>, ) -> PlaceAndQualifiers<'db> { let should_promote = matches!( @@ -3801,7 +4011,7 @@ impl<'db> Type<'db> { ) && !result.qualifiers.contains(TypeQualifiers::FINAL); if should_promote { - result.map_type(|ty| ty.promote_class_literals(db)) + result.map_type(|ty| ty.promote_class_literals(db, env)) } else { result } @@ -3809,6 +4019,7 @@ impl<'db> Type<'db> { fn instance_like_member_lookup<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, key: MemberLookupKey<'db>, receiver: Type<'db>, ) -> PlaceAndQualifiers<'db> { @@ -3823,7 +4034,7 @@ impl<'db> Type<'db> { .as_enum() .map(|enum_literal| enum_literal.enum_class_literal(db)), _ => this - .nominal_class(db) + .nominal_class(db, env) .map(|class| class.class_literal(db)) .and_then(|class| class.into_enum_class(db)), } && let Some(resolved_name) = enum_class.resolve_member(db, name) @@ -3836,10 +4047,11 @@ impl<'db> Type<'db> { .into(); } - let fallback = this.instance_member(db, name_str); + let fallback = this.instance_member(db, env, name_str); let result = Type::invoke_descriptor_protocol( db, + env, key, receiver, fallback, @@ -3852,44 +4064,52 @@ impl<'db> Type<'db> { return Place::Undefined.into(); } - let result = this.fallback_to_getattr(db, name, result, key.policy(db)); + let result = this.fallback_to_getattr(db, env, name, result, key.policy(db)); // An inferred attribute accessed through an instance can resolve to an override // on a subclass, so an exact class object is not a safe public type here. - let result = result.map_type(|ty| ty.bind_self_typevars(db, receiver)); - promote_inferred_attribute_class_literals(db, result) + let result = result.map_type(|ty| ty.bind_self_typevars(db, env, receiver)); + promote_inferred_attribute_class_literals(db, env, result) } + let program = key.program(db); + let env = &ProgramEnvironment::from_program(program); let this = key.ty(db); let name = key.name(db); let name_str = name.as_str(); let policy = key.policy(db); - tracing::trace!("member_lookup_with_policy: {}.{}", this.display(db), name); + tracing::trace!( + "member_lookup_with_policy: {}.{}", + this.display(db, env), + name + ); if let Some(fallback) = this.materialized_divergent_fallback() { return fallback - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver); + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver); } match this { - Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| { - elem.member_lookup_with_policy_and_receiver(db, name_str, policy, receiver) + Type::Union(union) => union.map_with_boundness_and_qualifiers(db, env, |elem| { + elem.member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver) }), Type::Intersection(intersection) => { - if let Some(complement) = intersection.enum_complement(db) { - enums::member_lookup_for_enum_complement(db, complement, name_str, policy) + if let Some(complement) = intersection.enum_complement(db, env) { + enums::member_lookup_for_enum_complement( + db, env, complement, name_str, policy, + ) } else { let receiver = Some(receiver.unwrap_or(this)); - intersection.map_with_boundness_and_qualifiers(db, |elem| { + intersection.map_with_boundness_and_qualifiers(db, env, |elem| { elem.member_lookup_with_policy_and_receiver( - db, name_str, policy, receiver, + db, env, name_str, policy, receiver, ) }) } } Type::EnumComplement(complement) => { - enums::member_lookup_for_enum_complement(db, complement, name_str, policy) + enums::member_lookup_for_enum_complement(db, env, complement, name_str, policy) } Type::Dynamic(..) | Type::Divergent(_) | Type::Never => Place::bound(this).into(), @@ -4053,28 +4273,30 @@ impl<'db> Type<'db> { } _ => { KnownClass::MethodType - .to_instance(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver) - .or_fall_back_to(db, || { + .to_instance(db, env) + .member_lookup_with_policy_and_receiver( + db, env, name_str, policy, receiver, + ) + .or_fall_back_to(db, env, || { // If an attribute is not available on the bound method object, // it will be looked up on the underlying function object. This // changes the lookup object, so do not forward the bound-method // receiver. Type::FunctionLiteral(bound_method.function(db)) - .member_lookup_with_policy(db, name_str, policy) + .member_lookup_with_policy(db, env, name_str, policy) }) } }, Type::KnownBoundMethod(method) => method .class() - .to_instance(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver), + .to_instance(db, env) + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver), Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType - .to_instance(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver), + .to_instance(db, env) + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver), Type::DataclassDecorator(_) => KnownClass::FunctionType - .to_instance(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver), + .to_instance(db, env) + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver), Type::Callable(_) | Type::DataclassTransformer(_) if name_str == "__call__" => { Place::bound(this).into() @@ -4082,17 +4304,17 @@ impl<'db> Type<'db> { Type::Callable(callable) if callable.is_function_like(db) => { KnownClass::FunctionType - .to_instance(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver) + .to_instance(db, env) + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver) } Type::Callable(_) | Type::DataclassTransformer(_) => Type::object() - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver), + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver), Type::NominalInstance(instance) if matches!(name_str, "major" | "minor") && instance.is_sys_version_info() => { - let python_version = Program::get(db).python_version(db); + let python_version = env.python_version(db); let segment = if name == "major" { python_version.major } else { @@ -4102,13 +4324,13 @@ impl<'db> Type<'db> { } Type::PropertyInstance(property) if name == "fget" => { - Place::bound(property.getter(db).unwrap_or(Type::none(db))).into() + Place::bound(property.getter(db).unwrap_or(Type::none(db, env))).into() } Type::PropertyInstance(property) if name == "fset" => { - Place::bound(property.setter(db).unwrap_or(Type::none(db))).into() + Place::bound(property.setter(db).unwrap_or(Type::none(db, env))).into() } Type::PropertyInstance(property) if name == "fdel" => { - Place::bound(property.deleter(db).unwrap_or(Type::none(db))).into() + Place::bound(property.deleter(db).unwrap_or(Type::none(db, env))).into() } Type::LiteralValue(literal) @@ -4124,7 +4346,7 @@ impl<'db> Type<'db> { Place::bound(Type::int_literal(i64::from(bool_value))).into() } - Type::ModuleLiteral(module) => module.static_member(db, name_str), + Type::ModuleLiteral(module) => module.static_member(db, env, name_str), // If a protocol does not include a member and the policy disables falling back to // `object`, we return `Place::Undefined` here. This short-circuits attribute lookup @@ -4152,23 +4374,24 @@ impl<'db> Type<'db> { Type::NewTypeInstance(new_type_instance) if this.as_union_like(db).is_some() => { new_type_instance .concrete_base_type(db) - .member_lookup_with_policy(db, name_str, policy) + .member_lookup_with_policy(db, env, name_str, policy) } Type::TypeAlias(alias) => alias .value_type(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver), + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver), _ if policy.no_instance_fallback() => { let receiver = receiver.unwrap_or(this); Type::invoke_descriptor_protocol( db, + env, key, receiver, Place::Undefined.into(), InstanceFallbackShadowsNonDataDescriptor::No, ) - .map_type(|ty| ty.bind_self_typevars(db, receiver)) + .map_type(|ty| ty.bind_self_typevars(db, env, receiver)) } Type::LiteralValue(literal) @@ -4176,13 +4399,14 @@ impl<'db> Type<'db> { && let Some(enum_literal) = literal.as_enum() && !enums::class_defines_property( db, + env, enum_literal.enum_class(db), name_str, ) => { let enum_class = enum_literal.enum_class_literal(db); let is_enum_subclass = Type::ClassLiteral(enum_class.class_literal(db)) - .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)); + .is_subtype_of(db, env, KnownClass::Enum.to_subclass_of(db, env)); let ty = match name_str { "name" if is_enum_subclass => { @@ -4215,37 +4439,41 @@ impl<'db> Type<'db> { let receiver = receiver.unwrap_or(this); if let Some(bound) = typevar .typevar(db) - .bound_or_constraints(db) - .map(|bound| bound.as_type(db)) - && bound.to_instance(db).is_some() + .bound_or_constraints(db, env) + .map(|bound| bound.as_type(db, env)) + && bound.to_instance(db, env).is_some() { // A TypeVar can be bounded by a class-object type such as `type[A]`, which // requires the full lookup path rather than instance-member lookup. return bound.member_lookup_with_policy_and_receiver( db, + env, name_str, policy, Some(receiver), ); } - instance_like_member_lookup(db, key, receiver) + instance_like_member_lookup(db, env, key, receiver) } Type::NominalInstance(instance) if matches!(name_str, "name" | "_name_" | "value" | "_value_") - && let class_literal = instance.class_literal(db) + && let class_literal = instance.class_literal(db, env) && let Some(metadata) = enum_metadata(db, class_literal) - && !enums::class_defines_property(db, class_literal, name_str) => + && !enums::class_defines_property(db, env, class_literal, name_str) => { - let is_enum_subclass = Type::ClassLiteral(class_literal) - .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)); + let is_enum_subclass = Type::ClassLiteral(class_literal).is_subtype_of( + db, + env, + KnownClass::Enum.to_subclass_of(db, env), + ); let ty = match name_str { - "name" if is_enum_subclass => metadata.instance_name_type(db), - "_name_" => metadata.instance_name_type(db), - "value" if is_enum_subclass => metadata.instance_value_type(db), - "_value_" => metadata.instance_value_type(db), + "name" if is_enum_subclass => metadata.instance_name_type(db, env), + "_name_" => metadata.instance_name_type(db, env), + "value" if is_enum_subclass => metadata.instance_value_type(db, env), + "_value_" => metadata.instance_value_type(db, env), _ => None, }; @@ -4271,8 +4499,10 @@ impl<'db> Type<'db> { let wrapped = partial.wrapped(db).inner(db); let nominal_lookup = partial .partial(db) - .into_functools_partial_instance(db) - .member_lookup_with_policy_and_receiver(db, name_str, policy, receiver); + .into_functools_partial_instance(db, env) + .member_lookup_with_policy_and_receiver( + db, env, name_str, policy, receiver, + ); if name_str == "func" { match nominal_lookup.place { Place::Defined(DefinedPlace { @@ -4311,7 +4541,7 @@ impl<'db> Type<'db> { | Type::TypeForm(..) | Type::TypedDict(_) => { let receiver = receiver.unwrap_or(this); - instance_like_member_lookup(db, key, receiver) + instance_like_member_lookup(db, env, key, receiver) } Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { @@ -4322,7 +4552,7 @@ impl<'db> Type<'db> { Type::ClassLiteral(literal) => literal.into_enum_class(db), Type::SubclassOf(subclass_of) => subclass_of .subclass_of() - .into_class(db) + .into_class(db, env) .and_then(|class| class.class_literal(db).into_enum_class(db)), _ => None, }; @@ -4337,16 +4567,17 @@ impl<'db> Type<'db> { .into(); } - let class_attr_plain = this.class_object_member(db, name_str, policy); + let class_attr_plain = this.class_object_member(db, env, name_str, policy); - let self_instance = receiver.to_instance_approximation(db).expect( + let self_instance = receiver.to_instance_approximation(db, env).expect( "The receiver for a class-object lookup should always be instantiable", ); - let class_attr_plain = - class_attr_plain.map_type(|ty| ty.bind_self_typevars(db, self_instance)); + let class_attr_plain = class_attr_plain + .map_type(|ty| ty.bind_self_typevars(db, env, self_instance)); let class_attr_fallback = Type::try_call_dunder_get_on_attribute( db, + env, class_attr_plain, None, receiver, @@ -4355,6 +4586,7 @@ impl<'db> Type<'db> { let result = Type::invoke_descriptor_protocol( db, + env, key, receiver, class_attr_fallback, @@ -4367,13 +4599,13 @@ impl<'db> Type<'db> { // attribute access falls back to `__getattr__`/`__getattribute__` on the // class. `try_call_dunder` adds `NO_INSTANCE_FALLBACK`, which causes the // lookup to hit the catch-all that only checks the meta-type (the metaclass). - let result = this.fallback_to_getattr(db, name, result, policy); + let result = this.fallback_to_getattr(db, env, name, result, policy); // Unlike a specific class literal, `type[C]` can represent any subclass of // `C`, unless a `TypeVar` upper bound normalizes to a final class. let result = if let Type::SubclassOf(subclass_of) = this - && subclass_of.exact_typevar_upper_bound(db).is_none() + && subclass_of.exact_typevar_upper_bound(db, env).is_none() { - promote_inferred_attribute_class_literals(db, result) + promote_inferred_attribute_class_literals(db, env, result) } else { result }; @@ -4389,7 +4621,12 @@ impl<'db> Type<'db> { if ty.is_dynamic() { ty } else { - IntersectionType::from_two_elements(db, ty, Type::Dynamic(dynamic)) + IntersectionType::from_two_elements( + db, + env, + ty, + Type::Dynamic(dynamic), + ) } }) } else { @@ -4403,10 +4640,11 @@ impl<'db> Type<'db> { // 1. Search for the attribute in the MRO, starting just after the pivot class. // 2. If the attribute is a descriptor, invoke its `__get__` method. Type::BoundSuper(bound_super) => { - let owner_attr = bound_super.find_name_in_mro_after_pivot(db, name_str, policy); + let owner_attr = + bound_super.find_name_in_mro_after_pivot(db, env, name_str, policy); bound_super - .try_call_dunder_get_on_attribute(db, owner_attr) + .try_call_dunder_get_on_attribute(db, env, owner_attr) .unwrap_or(owner_attr) } } @@ -4414,7 +4652,7 @@ impl<'db> Type<'db> { if self.materialized_divergent_fallback().is_none() { if name == "__class__" { - return Place::bound(self.dunder_class(db)).into(); + return Place::bound(self.dunder_class(db, env)).into(); } if matches!(self, Type::Dynamic(_) | Type::Divergent(_) | Type::Never) { @@ -4422,7 +4660,7 @@ impl<'db> Type<'db> { } } - let key = MemberLookupKey::new(db, self, name, policy); + let key = MemberLookupKey::new(db, env.program(db), self, name, policy); match receiver { Some(receiver) => member_lookup_with_policy_and_receiver_inner(db, key, receiver), None => member_lookup_with_policy_inner(db, key), @@ -4434,8 +4672,12 @@ impl<'db> Type<'db> { /// /// In the second case, the return type of `len()` in `typeshed` (`int`) /// is used as a fallback. - fn len(&self, db: &'db dyn Db) -> Option> { - fn non_negative_int_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { + fn len(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { + fn non_negative_int_literal<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option> { match ty { // TODO: Emit diagnostic for non-integers and negative integers Type::LiteralValue(literal) => match literal.kind() { @@ -4443,28 +4685,29 @@ impl<'db> Type<'db> { LiteralValueTypeKind::Bool(value) => Some(Type::int_literal(i64::from(value))), _ => None, }, - Type::Union(union) => { - union.try_map(db, |element| non_negative_int_literal(db, *element)) - } + Type::Union(union) => union.try_map(db, env, |element| { + non_negative_int_literal(db, env, *element) + }), _ => None, } } let return_ty = match self.try_call_dunder( db, + env, "__len__", CallArguments::none(), TypeContext::default(), ) { - Ok(bindings) => bindings.return_type(db), - Err(CallDunderError::PossiblyUnbound { bindings, .. }) => bindings.return_type(db), + Ok(bindings) => bindings.return_type(db, env), + Err(CallDunderError::PossiblyUnbound { bindings, .. }) => bindings.return_type(db, env), // TODO: emit a diagnostic Err(CallDunderError::MethodNotAvailable) => return None, - Err(CallDunderError::CallError(_, bindings, _)) => bindings.return_type(db), + Err(CallDunderError::CallError(_, bindings, _)) => bindings.return_type(db, env), }; - non_negative_int_literal(db, return_ty) + non_negative_int_literal(db, env, return_ty) } /// If this type is a `ParamSpec` type variable, returns it. Otherwise, returns `None`. @@ -4478,13 +4721,23 @@ impl<'db> Type<'db> { // Returns the value type of a `__getitem__` dunder call on this object. // // Returns `None` if `__getitem__` is undefined or results in a call error. - fn getitem_dunder_call(self, db: &'db dyn Db, key: Option<&str>) -> Option> { + fn getitem_dunder_call( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + key: Option<&str>, + ) -> Option> { let key = key .map(|key| Type::string_literal(db, key)) .unwrap_or(Type::unknown()); match self - .member_lookup_with_policy(db, "__getitem__", MemberLookupPolicy::NO_INSTANCE_FALLBACK) + .member_lookup_with_policy( + db, + env, + "__getitem__", + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) .place { Place::Defined(DefinedPlace { @@ -4492,9 +4745,9 @@ impl<'db> Type<'db> { definedness: Definedness::AlwaysDefined, .. }) => getitem_method - .try_call(db, &CallArguments::positional([key])) + .try_call(db, env, &CallArguments::positional([key])) .ok() - .map(|bindings| bindings.return_type(db)), + .map(|bindings| bindings.return_type(db, env)), _ => None, } @@ -4502,9 +4755,13 @@ impl<'db> Type<'db> { /// Returns the key and value types of this object if it was unpacked using `**`, /// or `None` if the object does not support unpacking. - fn unpack_keys_and_items(self, db: &'db dyn Db) -> Option<(Type<'db>, Type<'db>)> { + fn unpack_keys_and_items( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option<(Type<'db>, Type<'db>)> { let key_ty = match self - .member_lookup_with_policy(db, "keys", MemberLookupPolicy::NO_INSTANCE_FALLBACK) + .member_lookup_with_policy(db, env, "keys", MemberLookupPolicy::NO_INSTANCE_FALLBACK) .place { Place::Defined(DefinedPlace { @@ -4512,15 +4769,15 @@ impl<'db> Type<'db> { definedness: Definedness::AlwaysDefined, .. }) => keys_method - .try_call(db, &CallArguments::none()) + .try_call(db, env, &CallArguments::none()) .ok() .and_then(|bindings| { Some( bindings - .return_type(db) - .try_iterate(db) + .return_type(db, env) + .try_iterate(db, env) .ok()? - .homogeneous_element_type(db), + .homogeneous_element_type(db, env), ) })?, @@ -4528,7 +4785,7 @@ impl<'db> Type<'db> { }; let value_ty = self - .getitem_dunder_call(db, None) + .getitem_dunder_call(db, env, None) .unwrap_or(Type::unknown()); Some((key_ty, value_ty)) @@ -4544,9 +4801,9 @@ impl<'db> Type<'db> { /// elements might be inconsistent, such that there's no argument list that's valid for all /// elements. It's usually best to only worry about "callability" relative to a particular /// argument list, via [`try_call`][Self::try_call] and [`CallErrorKind::NotCallable`]. - fn bindings(self, db: &'db dyn Db) -> Bindings<'db> { + fn bindings(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Bindings<'db> { if let Some(fallback) = self.materialized_divergent_fallback() { - return fallback.bindings(db); + return fallback.bindings(db, env); } match self { @@ -4556,13 +4813,16 @@ impl<'db> Type<'db> { } Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { None => CallableBinding::not_callable(self).into(), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.bindings(db), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.bindings(db, env), Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { Bindings::from_union( self, - constraints.elements(db).iter().map(|ty| ty.bindings(db)), + constraints + .elements(db) + .iter() + .map(|ty| ty.bindings(db, env)), ) } } @@ -4585,7 +4845,7 @@ impl<'db> Type<'db> { let mut binding = CallableBinding::from_overloads(self, signature.overloads.iter().cloned()) .with_bound_type(bound_method.typing_self_type(db)); - binding.bake_bound_type_into_overloads(db); + binding.bake_bound_type_into_overloads(db, env); binding.into() } else { CallableBinding::from_overloads(self, signature.overloads.iter().cloned()) @@ -4595,11 +4855,11 @@ impl<'db> Type<'db> { } Type::KnownBoundMethod(method) => { - CallableBinding::from_overloads(self, method.signatures(db)).into() + CallableBinding::from_overloads(self, method.signatures(db, env)).into() } Type::WrapperDescriptor(wrapper_descriptor) => { - CallableBinding::from_overloads(self, wrapper_descriptor.signatures(db)).into() + CallableBinding::from_overloads(self, wrapper_descriptor.signatures(db, env)).into() } // TODO: We should probably also check the original return type of the function @@ -4621,6 +4881,7 @@ impl<'db> Type<'db> { Some(KnownFunction::AssertType) => { let val_ty = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("T"), TypeVarVariance::Invariant, ); @@ -4628,7 +4889,7 @@ impl<'db> Type<'db> { Binding::single( self, Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [val_ty])), + Some(GenericContext::from_typevar_instances(db, env, [val_ty])), Parameters::standard([ Parameter::positional_only(Some(Name::new_static("value"))) .with_annotated_type(Type::TypeVar(val_ty)), @@ -4674,9 +4935,10 @@ impl<'db> Type<'db> { .into(), Some(KnownFunction::Dataclass) => { + let python_version = env.python_version(db); let bool_parameter = |name: &'static str, default: bool| { Parameter::keyword_only(Name::new_static(name)) - .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_annotated_type(KnownClass::Bool.to_instance(db, env)) .with_default_type(Type::bool_literal(default)) }; @@ -4689,7 +4951,7 @@ impl<'db> Type<'db> { bool_parameter("frozen", false), ]; - if Program::get(db).python_version(db) >= ast::PythonVersion::PY310 { + if python_version >= ast::PythonVersion::PY310 { decorator_factory_parameters.extend([ bool_parameter("match_args", true), bool_parameter("kw_only", false), @@ -4697,7 +4959,7 @@ impl<'db> Type<'db> { ]); } - if Program::get(db).python_version(db) >= ast::PythonVersion::PY311 { + if python_version >= ast::PythonVersion::PY311 { decorator_factory_parameters.push(bool_parameter("weakref_slot", false)); } @@ -4717,13 +4979,13 @@ impl<'db> Type<'db> { [ // def dataclass(cls: None, /, *, ...) -> Callable[[type[_T]], type[_T]]: ... Signature::new( - Parameters::standard(parameters_with_cls(Type::none(db))), + Parameters::standard(parameters_with_cls(Type::none(db, env))), Type::unknown(), ), // def dataclass(cls: type[_T], /, *, ...) -> type[_T]: ... Signature::new( Parameters::standard(parameters_with_cls( - KnownClass::Type.to_instance(db), + KnownClass::Type.to_instance(db, env), )), Type::unknown(), ), @@ -4758,20 +5020,24 @@ impl<'db> Type<'db> { Type::ClassLiteral(class) => self // TODO this should be called from `constructor_bindings` for better consistency - .known_class_literal_bindings(db, class) - .unwrap_or_else(|| self.constructor_bindings(db, ClassType::NonGeneric(class))), + .known_class_literal_bindings(db, env, class) + .unwrap_or_else(|| { + self.constructor_bindings(db, env, ClassType::NonGeneric(class)) + }), - Type::GenericAlias(alias) => self.constructor_bindings(db, ClassType::Generic(alias)), + Type::GenericAlias(alias) => { + self.constructor_bindings(db, env, ClassType::Generic(alias)) + } Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { SubclassOfInner::Dynamic(dynamic_type) => { Binding::single(self, Signature::dynamic(Type::Dynamic(dynamic_type))).into() } - SubclassOfInner::Class(class) => self.constructor_bindings(db, class), + SubclassOfInner::Class(class) => self.constructor_bindings(db, env, class), SubclassOfInner::Protocol(protocol) => protocol.class_origin(db).map_or_else( || Binding::single(self, Signature::dynamic(Type::unknown())).into(), |origin| { - let bindings = self.constructor_bindings(db, *origin); + let bindings = self.constructor_bindings(db, env, *origin); if protocol.materialization_kind(db).is_some() { bindings.with_constructed_instance_type( db, @@ -4784,10 +5050,10 @@ impl<'db> Type<'db> { ), SubclassOfInner::TypeVar(tvar) => { let constructor_instance_type = Type::TypeVar(tvar); - let bindings = match tvar.typevar(db).bound_or_constraints(db) { - None => KnownClass::Type.to_instance(db).bindings(db), + let bindings = match tvar.typevar(db).bound_or_constraints(db, env) { + None => KnownClass::Type.to_instance(db, env).bindings(db, env), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.to_meta_type(db).bindings(db) + bound.to_meta_type(db, env).bindings(db, env) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { Bindings::from_union( @@ -4795,7 +5061,7 @@ impl<'db> Type<'db> { constraints .elements(db) .iter() - .map(|ty| ty.to_meta_type(db).bindings(db)), + .map(|ty| ty.to_meta_type(db, env).bindings(db, env)), ) } }; @@ -4834,6 +5100,7 @@ impl<'db> Type<'db> { match self .member_lookup_with_policy( db, + env, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -4844,7 +5111,7 @@ impl<'db> Type<'db> { definedness: boundness, .. }) => { - let mut bindings = dunder_callable.bindings(db); + let mut bindings = dunder_callable.bindings(db, env); bindings.replace_callable_type(dunder_callable, self); if boundness == Definedness::PossiblyUndefined { bindings.set_dunder_call_is_possibly_unbound(); @@ -4867,31 +5134,35 @@ impl<'db> Type<'db> { union .elements(db) .iter() - .map(|element| element.bindings(db)), + .map(|element| element.bindings(db, env)), ), Type::Intersection(intersection) => Bindings::from_intersection( self, intersection .positive_elements_or_object(db) - .map(|element| element.bindings(db)), + .map(|element| element.bindings(db, env)), ), - Type::EnumComplement(complement) => complement.to_intersection(db).bindings(db), + Type::EnumComplement(complement) => { + complement.to_intersection(db, env).bindings(db, env) + } Type::DataclassDecorator(_) => { let typevar = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("T"), TypeVarVariance::Invariant, ); - let typevar_meta = SubclassOfType::from(db, typevar); - let context = GenericContext::from_typevar_instances(db, [typevar]); + let typevar_meta = SubclassOfType::from(db, env, typevar); + let context = GenericContext::from_typevar_instances(db, env, [typevar]); let parameters = [Parameter::positional_only(Some(Name::new_static("cls"))) .with_annotated_type(typevar_meta)]; // Intersect with `Any` for the return type to reflect the fact that the `dataclass()` // decorator adds methods to the class - let returns = IntersectionType::from_two_elements(db, typevar_meta, Type::any()); + let returns = + IntersectionType::from_two_elements(db, env, typevar_meta, Type::any()); let signature = Signature::new_generic( Some(context), Parameters::standard(parameters), @@ -4905,7 +5176,7 @@ impl<'db> Type<'db> { Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Enum(enum_literal) => { - enum_literal.enum_class_instance(db).bindings(db) + enum_literal.enum_class_instance(db, env).bindings(db, env) } _ => CallableBinding::not_callable(self).into(), }, @@ -4914,7 +5185,7 @@ impl<'db> Type<'db> { self, Signature::new( Parameters::standard([Parameter::positional_only(None) - .with_annotated_type(newtype.base(db).instance_type(db))]), + .with_annotated_type(newtype.base(db).instance_type(db, env))]), Type::NewTypeInstance(newtype), ), ) @@ -4923,13 +5194,13 @@ impl<'db> Type<'db> { Type::KnownInstance( KnownInstanceType::FunctoolsPartial(partial) | KnownInstanceType::FunctoolsPartialCall(partial), - ) => Type::Callable(partial.partial(db)).bindings(db), + ) => Type::Callable(partial.partial(db)).bindings(db, env), Type::KnownInstance(known_instance) => { - known_instance.instance_fallback(db).bindings(db) + known_instance.instance_fallback(db, env).bindings(db, env) } - Type::TypeAlias(alias) => alias.value_type(db).bindings(db), + Type::TypeAlias(alias) => alias.value_type(db).bindings(db, env), Type::PropertyInstance(_) | Type::AlwaysFalsy @@ -4946,6 +5217,7 @@ impl<'db> Type<'db> { fn known_class_literal_bindings( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassLiteral<'db>, ) -> Option> { // TODO: Some of these cases date back to when we didn't even support overloads yet; see if @@ -4965,7 +5237,7 @@ impl<'db> Type<'db> { )) .with_annotated_type(Type::any()) .with_default_type(Type::bool_literal(false))]), - KnownClass::Bool.to_instance(db), + KnownClass::Bool.to_instance(db, env), ), ) .into(), @@ -5005,16 +5277,19 @@ impl<'db> Type<'db> { Parameter::positional_only(Some(Name::new_static("obj"))) .with_annotated_type(Type::any()), ]), - KnownClass::Super.to_instance(db), + KnownClass::Super.to_instance(db, env), ), Signature::new( Parameters::standard([Parameter::positional_only(Some( Name::new_static("t"), )) .with_annotated_type(Type::any())]), - KnownClass::Super.to_instance(db), + KnownClass::Super.to_instance(db, env), + ), + Signature::new( + Parameters::empty(), + KnownClass::Super.to_instance(db, env), ), - Signature::new(Parameters::empty(), KnownClass::Super.to_instance(db)), ], ) .into(), @@ -5033,7 +5308,7 @@ impl<'db> Type<'db> { // stacklevel: int = 1 // ) -> Self: ... // ``` - let warning_class_type = KnownClass::Warning.to_subclass_of(db); + let warning_class_type = KnownClass::Warning.to_subclass_of(db, env); Some( Binding::single( @@ -5045,15 +5320,16 @@ impl<'db> Type<'db> { Parameter::keyword_only(Name::new_static("category")) .with_annotated_type(UnionType::from_two_elements( db, + env, warning_class_type, - Type::none(db), + Type::none(db, env), )) .with_default_type(warning_class_type), Parameter::keyword_only(Name::new_static("stacklevel")) - .with_annotated_type(KnownClass::Int.to_instance(db)) + .with_annotated_type(KnownClass::Int.to_instance(db, env)) .with_default_type(Type::int_literal(1)), ]), - KnownClass::Deprecated.to_instance(db), + KnownClass::Deprecated.to_instance(db, env), ), ) .into(), @@ -5076,22 +5352,24 @@ impl<'db> Type<'db> { Signature::new( Parameters::standard([ Parameter::positional_or_keyword(Name::new_static("name")) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), Parameter::positional_or_keyword(Name::new_static("value")) .with_annotated_type(object_type_form(db)), Parameter::keyword_only(Name::new_static("type_params")) .with_annotated_type(Type::homogeneous_tuple( db, + env, UnionType::from_elements( db, + env, [ - KnownClass::TypeVar.to_instance(db), - KnownClass::ParamSpec.to_instance(db), - KnownClass::TypeVarTuple.to_instance(db), + KnownClass::TypeVar.to_instance(db, env), + KnownClass::ParamSpec.to_instance(db, env), + KnownClass::TypeVarTuple.to_instance(db, env), ], ), )) - .with_default_type(Type::empty_tuple(db)), + .with_default_type(Type::empty_tuple(db, env)), ]), Type::unknown(), ), @@ -5112,7 +5390,7 @@ impl<'db> Type<'db> { Parameter::positional_only(None).with_annotated_type(Type::any()), Parameter::positional_only(None).with_annotated_type(Type::any()), ]), - Type::none(db), + Type::none(db, env), ); let deleter_signature = Signature::new( Parameters::standard([ @@ -5129,31 +5407,35 @@ impl<'db> Type<'db> { Parameter::positional_or_keyword(Name::new_static("fget")) .with_annotated_type(UnionType::from_two_elements( db, + env, Type::single_callable(db, getter_signature), - Type::none(db), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), Parameter::positional_or_keyword(Name::new_static("fset")) .with_annotated_type(UnionType::from_two_elements( db, + env, Type::single_callable(db, setter_signature), - Type::none(db), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), Parameter::positional_or_keyword(Name::new_static("fdel")) .with_annotated_type(UnionType::from_two_elements( db, + env, Type::single_callable(db, deleter_signature), - Type::none(db), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), Parameter::positional_or_keyword(Name::new_static("doc")) .with_annotated_type(UnionType::from_two_elements( db, - KnownClass::Str.to_instance(db), - Type::none(db), + env, + KnownClass::Str.to_instance(db, env), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), ]), Type::unknown(), ), @@ -5169,6 +5451,7 @@ impl<'db> Type<'db> { // ``` let return_ty = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("_T"), TypeVarVariance::Covariant, ); @@ -5177,7 +5460,7 @@ impl<'db> Type<'db> { Binding::single( self, Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [return_ty])), + Some(GenericContext::from_typevar_instances(db, env, [return_ty])), Parameters::concatenate( db, vec![ @@ -5192,8 +5475,11 @@ impl<'db> Type<'db> { ], ConcatenateTail::Gradual, ), - KnownClass::FunctoolsPartial - .to_specialized_instance(db, &[Type::TypeVar(return_ty)]), + KnownClass::FunctoolsPartial.to_specialized_instance( + db, + env, + &[Type::TypeVar(return_ty)], + ), ), ) .into(), @@ -5203,6 +5489,7 @@ impl<'db> Type<'db> { KnownClass::Tuple => { let element_ty = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("T"), TypeVarVariance::Covariant, ); @@ -5218,17 +5505,24 @@ impl<'db> Type<'db> { CallableBinding::from_overloads( self, [ - Signature::new(Parameters::empty(), Type::empty_tuple(db)), + Signature::new(Parameters::empty(), Type::empty_tuple(db, env)), Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [element_ty])), + Some(GenericContext::from_typevar_instances( + db, + env, + [element_ty], + )), Parameters::standard([Parameter::positional_only(Some( Name::new_static("iterable"), )) .with_annotated_type( - KnownClass::Iterable - .to_specialized_instance(db, &[Type::TypeVar(element_ty)]), + KnownClass::Iterable.to_specialized_instance( + db, + env, + &[Type::TypeVar(element_ty)], + ), )]), - Type::homogeneous_tuple(db, Type::TypeVar(element_ty)), + Type::homogeneous_tuple(db, env, Type::TypeVar(element_ty)), ), ], ) @@ -5242,9 +5536,15 @@ impl<'db> Type<'db> { // Build bindings for constructor calls by combining `__new__`/`__init__` signatures. // Returns fallback bindings for cases that intentionally keep bespoke call behavior. - fn constructor_bindings(self, db: &'db dyn Db, class: ClassType<'db>) -> Bindings<'db> { + fn constructor_bindings( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType<'db>, + ) -> Bindings<'db> { fn resolve_dunder_new_callable<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, owner: Type<'db>, place: Place<'db>, ) -> Option<(Type<'db>, Definedness)> { @@ -5261,7 +5561,7 @@ impl<'db> Type<'db> { ) { return None; } - match place.try_call_dunder_get(db, owner) { + match place.try_call_dunder_get(db, env, owner) { Place::Defined(DefinedPlace { ty: callable, definedness, @@ -5272,6 +5572,7 @@ impl<'db> Type<'db> { } fn bind_constructor_new<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bindings: Bindings<'db>, self_type: Type<'db>, ) -> Bindings<'db> { @@ -5281,7 +5582,7 @@ impl<'db> Type<'db> { // first, then bind `cls` for constructor-call semantics (the call site omits `cls`). // Note: This intentionally preserves `type.__call__` behavior for `@classmethod __new__`, // which receives an extra implicit `cls` and errors at call sites. - binding.bake_bound_type_into_overloads(db); + binding.bake_bound_type_into_overloads(db, env); binding.bound_type = Some(self_type); binding }) @@ -5293,7 +5594,7 @@ impl<'db> Type<'db> { // Keep bespoke constructor behavior for cases that don't map cleanly to `__new__`/`__init__`. let fallback_bindings = || { let return_type = self - .to_instance_approximation(db) + .to_instance_approximation(db, env) .unwrap_or(Type::unknown()); Binding::single( self, @@ -5340,9 +5641,9 @@ impl<'db> Type<'db> { // functional syntax for creating enum classes. TODO we should ideally check e.g. // `MyEnum(1)` to make sure `1` is a valid value for `MyEnum`. if KnownClass::Enum - .to_class_literal(db) + .to_class_literal(db, env) .to_class_type(db) - .is_some_and(|enum_class| class.is_subclass_of(db, enum_class)) + .is_some_and(|enum_class| class.is_subclass_of(db, env, enum_class)) { return fallback_bindings(); } @@ -5367,28 +5668,30 @@ impl<'db> Type<'db> { // until call-time overload resolution. let metaclass_dunder_call = self_type.member_lookup_with_policy( db, + env, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ); - let Some(constructor_instance_ty) = self_type.to_instance_approximation(db) else { + let Some(constructor_instance_ty) = self_type.to_instance_approximation(db, env) else { return fallback_bindings(); }; - let new_method = self_type.lookup_dunder_new(db); + let new_method = self_type.lookup_dunder_new(db, env); let init_method_no_object = constructor_instance_ty.member_lookup_with_policy( db, + env, "__init__", MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ); let (new_bindings, has_any_new) = match new_method.as_ref().map(|method| method.place) { - Some(place) => match resolve_dunder_new_callable(db, self_type, place) { + Some(place) => match resolve_dunder_new_callable(db, env, self_type, place) { Some((new_callable, definedness)) => { let mut bindings = - bind_constructor_new(db, new_callable.bindings(db), self_type) + bind_constructor_new(db, env, new_callable.bindings(db, env), self_type) .into_constructor_bindings( constructor_instance_ty, ConstructorCallableKind::New, @@ -5415,7 +5718,7 @@ impl<'db> Type<'db> { _, ) => { let mut bindings = init_method - .bindings(db) + .bindings(db, env) .into_constructor_bindings( constructor_instance_ty, ConstructorCallableKind::Init, @@ -5429,6 +5732,7 @@ impl<'db> Type<'db> { (Place::Undefined, false) => { let init_method_with_object = constructor_instance_ty.member_lookup_with_policy( db, + env, "__init__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ); @@ -5439,7 +5743,7 @@ impl<'db> Type<'db> { .. }) => { let mut bindings = init_method - .bindings(db) + .bindings(db, env) .into_constructor_bindings( constructor_instance_ty, ConstructorCallableKind::Init, @@ -5493,7 +5797,7 @@ impl<'db> Type<'db> { }) = metaclass_dunder_call.place { let mut metaclass_bindings = metaclass_call_method - .bindings(db) + .bindings(db, env) .into_constructor_bindings( constructor_instance_ty, ConstructorCallableKind::MetaclassCall, @@ -5523,13 +5827,15 @@ impl<'db> Type<'db> { fn try_call( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, argument_types: &CallArguments<'_, 'db>, ) -> Result, CallError<'db>> { let constraints = ConstraintSetBuilder::new(); - self.bindings(db) - .match_parameters(db, argument_types) + self.bindings(db, env) + .match_parameters(db, env, argument_types) .check_types( db, + env, &constraints, argument_types, TypeContext::default(), @@ -5544,12 +5850,14 @@ impl<'db> Type<'db> { fn try_call_dunder( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, mut argument_types: CallArguments<'_, 'db>, tcx: TypeContext<'db>, ) -> Result, CallDunderError<'db>> { self.try_call_dunder_with_policy( db, + env, name, &mut argument_types, tcx, @@ -5567,23 +5875,31 @@ impl<'db> Type<'db> { fn try_call_dunder_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, argument_types: &mut CallArguments<'_, 'db>, tcx: TypeContext<'db>, policy: MemberLookupPolicy, ) -> Result, CallDunderError<'db>> { if let Type::Intersection(intersection) = self { - return intersection.try_call_dunder_with_policy(db, name, argument_types, tcx, policy); + return intersection.try_call_dunder_with_policy( + db, + env, + name, + argument_types, + tcx, + policy, + ); } if let Type::Union(union) = self { - return union.try_call_dunder_with_policy(db, name, argument_types, tcx, policy); + return union.try_call_dunder_with_policy(db, env, name, argument_types, tcx, policy); } // Implicit calls to dunder methods never access instance members, so we pass // `NO_INSTANCE_FALLBACK` here in addition to other policies: let policy = policy | MemberLookupPolicy::NO_INSTANCE_FALLBACK; - match self.member_lookup_with_policy(db, name, policy).place { + match self.member_lookup_with_policy(db, env, name, policy).place { Place::Defined(DefinedPlace { ty: dunder_callable, definedness: boundness, @@ -5592,9 +5908,9 @@ impl<'db> Type<'db> { }) => { let constraints = ConstraintSetBuilder::new(); let bindings = dunder_callable - .bindings(db) - .match_parameters(db, argument_types) - .check_types(db, &constraints, argument_types, tcx, &[]); + .bindings(db, env) + .match_parameters(db, env, argument_types) + .check_types(db, env, &constraints, argument_types, tcx, &[]); let bindings = match bindings { Ok(bindings) => bindings, @@ -5625,11 +5941,12 @@ impl<'db> Type<'db> { fn try_call_dunder_on_class( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, argument_types: &CallArguments<'_, 'db>, tcx: TypeContext<'db>, ) -> Result, CallDunderError<'db>> { - match self.member(db, name).place { + match self.member(db, env, name).place { Place::Defined(DefinedPlace { ty: dunder_callable, definedness: boundness, @@ -5638,9 +5955,9 @@ impl<'db> Type<'db> { }) => { let constraints = ConstraintSetBuilder::new(); let bindings = dunder_callable - .bindings(db) - .match_parameters(db, argument_types) - .check_types(db, &constraints, argument_types, tcx, &[]); + .bindings(db, env) + .match_parameters(db, env, argument_types) + .check_types(db, env, &constraints, argument_types, tcx, &[]); let bindings = match bindings { Ok(bindings) => bindings, @@ -5668,6 +5985,7 @@ impl<'db> Type<'db> { fn fallback_to_getattr( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &Name, result: PlaceAndQualifiers<'db>, policy: MemberLookupPolicy, @@ -5679,11 +5997,12 @@ impl<'db> Type<'db> { self.try_call_dunder( db, + env, "__getattr__", CallArguments::positional([Type::string_literal(db, name)]), TypeContext::default(), ) - .map(|outcome| Place::bound(outcome.return_type(db))) + .map(|outcome| Place::bound(outcome.return_type(db, env))) // TODO: Handle call errors here. .unwrap_or_default() .into() @@ -5698,12 +6017,13 @@ impl<'db> Type<'db> { // already model via the normal attribute-lookup path. self.try_call_dunder_with_policy( db, + env, "__getattribute__", &mut CallArguments::positional([Type::string_literal(db, name)]), TypeContext::default(), MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ) - .map(|outcome| Place::bound(outcome.return_type(db))) + .map(|outcome| Place::bound(outcome.return_type(db, env))) // TODO: Handle call errors here. .unwrap_or_default() .into() @@ -5726,12 +6046,12 @@ impl<'db> Type<'db> { }), qualifiers: _, } => member - .or_fall_back_to(db, custom_getattribute_result) - .or_fall_back_to(db, custom_getattr_result), + .or_fall_back_to(db, env, custom_getattribute_result) + .or_fall_back_to(db, env, custom_getattr_result), PlaceAndQualifiers { place: Place::Undefined, qualifiers: _, - } => custom_getattribute_result().or_fall_back_to(db, custom_getattr_result), + } => custom_getattribute_result().or_fall_back_to(db, env, custom_getattr_result), } } @@ -5750,31 +6070,39 @@ impl<'db> Type<'db> { /// /// This only flattens typevars directly in unions and intersections; it does not descend /// into generic types or other nested structures. - fn flatten_typevars(self, db: &'db dyn Db) -> Type<'db> { + fn flatten_typevars(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { - Type::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.flatten_typevars(db), - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - constraints.as_type(db).flatten_typevars(db) + Type::TypeVar(tvar) => { + match tvar.typevar(db).bound_or_constraints(db, env) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.flatten_typevars(db, env) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + constraints.as_type(db, env).flatten_typevars(db, env) + } + // Unbounded typevar is effectively `object`. + None => Type::object(), } - // Unbounded typevar is effectively `object`. - None => Type::object(), - }, + } Type::Union(union) => { // Flatten each element and rebuild through the union builder. UnionType::from_elements( db, - union.elements(db).iter().map(|e| e.flatten_typevars(db)), + env, + union + .elements(db) + .iter() + .map(|e| e.flatten_typevars(db, env)), ) } Type::Intersection(intersection) => { // Flatten each positive element and rebuild through the intersection builder. - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); for pos in intersection.positive(db) { - builder.add_positive_in_place(pos.flatten_typevars(db)); + builder.add_positive_in_place(pos.flatten_typevars(db, env)); } for neg in intersection.negative(db) { - builder.add_negative_in_place(neg.flatten_typevars(db)); + builder.add_negative_in_place(neg.flatten_typevars(db, env)); } builder.build() } @@ -5784,17 +6112,22 @@ impl<'db> Type<'db> { } /// Resolve the type of an `await …` expression where `self` is the type of the awaitable. - fn try_await(self, db: &'db dyn Db) -> Result, AwaitError<'db>> { + fn try_await( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Result, AwaitError<'db>> { let await_result = self.try_call_dunder( db, + env, "__await__", CallArguments::none(), TypeContext::default(), ); match await_result { Ok(bindings) => { - let return_type = bindings.return_type(db); - Ok(return_type.generator_return_type(db).ok_or_else(|| { + let return_type = bindings.return_type(db, env); + Ok(return_type.generator_return_type(db, env).ok_or_else(|| { AwaitError::InvalidReturnType(return_type, Box::new(bindings)) })?) } @@ -5806,7 +6139,11 @@ impl<'db> Type<'db> { /// /// This corresponds to the `ReturnT` parameter of the generic `typing.Generator[YieldT, SendT, ReturnT]` /// protocol. - fn generator_types(self, db: &'db dyn Db) -> Option> { + fn generator_types( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { // TODO: Ideally, we would first try to upcast `self` to an instance of `Generator` and *then* // match on the protocol instance to get the `ReturnType` type parameter. For now, implement // an ad-hoc solution that works for protocols and instances of classes that explicitly inherit @@ -5839,10 +6176,11 @@ impl<'db> Type<'db> { || class.is_known(db, KnownClass::AsyncIterator)) && let [yield_ty] = specialization.types(db) { + let none = Type::none(db, env); Some(GeneratorTypes { yield_ty: Some(*yield_ty), - send_ty: Some(Type::none(db)), - return_ty: Some(Type::none(db)), + send_ty: Some(none), + return_ty: Some(none), }) } else { None @@ -5850,24 +6188,25 @@ impl<'db> Type<'db> { }; match self { - Type::NominalInstance(instance) => { - instance.class(db).iter_mro(db).find_map(from_class_base) - } + Type::NominalInstance(instance) => instance + .class(db, env) + .iter_mro(db) + .find_map(from_class_base), Type::ProtocolInstance(protocol) => protocol .class_origin(db) .and_then(|class| class.iter_mro(db).find_map(from_class_base)) .map(|types| { protocol .materialization_kind(db) - .map_or(types, |kind| types.materialize(db, kind)) + .map_or(types, |kind| types.materialize(db, env, kind)) }), Type::Union(union) => { - let mut yield_builder = Some(UnionBuilder::new(db)); - let mut send_builder = Some(UnionBuilder::new(db)); - let mut return_builder = Some(UnionBuilder::new(db)); + let mut yield_builder = Some(UnionBuilder::new(db, env)); + let mut send_builder = Some(UnionBuilder::new(db, env)); + let mut return_builder = Some(UnionBuilder::new(db, env)); for ty in union.elements(db) { - let gt = ty.generator_types(db)?; + let gt = ty.generator_types(db, env)?; match gt.yield_ty { Some(ty) => yield_builder = yield_builder.map(|b| b.add(ty)), None => yield_builder = None, @@ -5892,13 +6231,13 @@ impl<'db> Type<'db> { // Using `positive()` rather than `positive_elements_or_object()` is safe // here because `object` is not a generator, so falling back to it would // still return `None`. - let mut yield_builder = Some(IntersectionBuilder::new(db)); - let mut send_builder = Some(IntersectionBuilder::new(db)); - let mut return_builder = Some(IntersectionBuilder::new(db)); + let mut yield_builder = Some(IntersectionBuilder::new(db, env)); + let mut send_builder = Some(IntersectionBuilder::new(db, env)); + let mut return_builder = Some(IntersectionBuilder::new(db, env)); let mut any_success = false; for ty in intersection.positive(db) { - let Some(gt) = ty.generator_types(db) else { + let Some(gt) = ty.generator_types(db, env) else { continue; }; any_success = true; @@ -5941,13 +6280,21 @@ impl<'db> Type<'db> { } } - fn generator_return_type(self, db: &'db dyn Db) -> Option> { - self.generator_types(db) + fn generator_return_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + self.generator_types(db, env) .and_then(|generator_types| generator_types.return_ty) } - fn generator_send_type(self, db: &'db dyn Db) -> Option> { - self.generator_types(db) + fn generator_send_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + self.generator_types(db, env) .and_then(|generator_types| generator_types.send_ty) } @@ -5956,49 +6303,55 @@ impl<'db> Type<'db> { /// Use this only when an over-approximation is sound, such as constructor inference or a /// source-side relation. Target-side subtype checks must use [`Self::to_instance`]. #[must_use] - fn to_instance_approximation(self, db: &'db dyn Db) -> Option> { - self.to_instance(db).map(InstanceProjection::into_inner) + fn to_instance_approximation( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + self.to_instance(db, env) + .map(InstanceProjection::into_inner) } /// Project this class-object type into its instance type while preserving projection quality. #[must_use] - fn to_instance(self, db: &'db dyn Db) -> Option>> { + fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { match self { Type::Dynamic(_) | Type::Divergent(_) | Type::Never => { Some(InstanceProjection::Exact(self)) } Type::ClassLiteral(class) => Some(InstanceProjection::OverApproximation( - Type::instance(db, class.default_specialization(db)), + Type::instance(db, env, class.default_specialization(db)), )), Type::GenericAlias(alias) => Some(InstanceProjection::OverApproximation( - Type::instance(db, ClassType::from(alias)), + Type::instance(db, env, ClassType::from(alias)), + )), + Type::SubclassOf(subclass_of_ty) => Some(InstanceProjection::Exact( + subclass_of_ty.to_instance(db, env), )), - Type::SubclassOf(subclass_of_ty) => { - Some(InstanceProjection::Exact(subclass_of_ty.to_instance(db))) - } Type::KnownInstance(KnownInstanceType::NewType(newtype)) => Some( InstanceProjection::OverApproximation(Type::NewTypeInstance(newtype)), ), - Type::Union(union) => union.to_instance(db), + Type::Union(union) => union.to_instance(db, env), // If there is no bound or constraints on a typevar `T`, `T: object` implicitly, which // has no instance type. Otherwise, synthesize a typevar with bound or constraints // mapped through `to_instance`. Type::TypeVar(bound_typevar) => bound_typevar - .to_instance(db) + .to_instance(db, env) .map(|projection| projection.map(Type::TypeVar)), - Type::TypeAlias(alias) => alias.value_type(db).to_instance(db), - Type::Intersection(intersection) => intersection.to_instance(db), + Type::TypeAlias(alias) => alias.value_type(db).to_instance(db, env), + Type::Intersection(intersection) => intersection.to_instance(db, env), // An instance of class `C` may itself have instances if `C` is a subclass of `type`. - Type::NominalInstance(instance) - if KnownClass::Type - .to_class_literal(db) - .to_class_type(db) - .is_some_and(|type_class| { - instance.class(db).is_subclass_of(db, type_class) - }) => - { - Some(InstanceProjection::OverApproximation(Type::object())) - } + Type::NominalInstance(instance) => KnownClass::Type + .to_class_literal(db, env) + .to_class_type(db) + .is_some_and(|type_class| { + instance.class(db, env).is_subclass_of(db, env, type_class) + }) + .then_some(InstanceProjection::OverApproximation(Type::object())), Type::FunctionLiteral(_) | Type::Callable(..) | Type::KnownBoundMethod(_) @@ -6006,7 +6359,6 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) - | Type::NominalInstance(_) | Type::ProtocolInstance(_) | Type::SpecialForm(_) | Type::KnownInstance(_) @@ -6041,23 +6393,34 @@ impl<'db> Type<'db> { typevar_binding_context: Option>, inference_flags: InferenceFlags, ) -> Result, InvalidTypeExpressionError<'db>> { + self.in_type_expression_impl(db, scope_id, typevar_binding_context, inference_flags) + } + + fn in_type_expression_impl( + &self, + db: &'db dyn Db, + scope_id: ScopeId<'db>, + typevar_binding_context: Option>, + inference_flags: InferenceFlags, + ) -> Result, InvalidTypeExpressionError<'db>> { + let env = &ProgramEnvironment::from_scope(scope_id); match self { // Special cases for `float` and `complex` // https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex Type::ClassLiteral(class) => { let ty = match class.known(db) { - Some(KnownClass::Complex) => KnownUnion::Complex.to_type(db), + Some(KnownClass::Complex) => KnownUnion::Complex.to_type(db, env), Some(KnownClass::Float) if !inference_flags .contains(InferenceFlags::DISABLE_INT_FLOAT_SPECIAL_CASE) => { - KnownUnion::Float.to_type(db) + KnownUnion::Float.to_type(db, env) } - _ => Type::instance(db, class.default_specialization(db)), + _ => Type::instance(db, env, class.default_specialization(db)), }; Ok(ty) } - Type::GenericAlias(alias) => Ok(Type::instance(db, ClassType::from(*alias))), + Type::GenericAlias(alias) => Ok(Type::instance(db, env, ClassType::from(*alias))), Type::SubclassOf(_) | Type::EnumComplement(_) @@ -6111,7 +6474,7 @@ impl<'db> Type<'db> { fallback_type: Type::unknown(), }); } - let index = semantic_index(db, scope_id.file(db)); + let index = semantic_index(db, scope_id.python_file(db)); Ok(bind_typevar( db, index, @@ -6178,7 +6541,7 @@ impl<'db> Type<'db> { // (`int` -> instance of `int` -> subclass of `int`) can be lossy, but it is // okay for all valid arguments to `type[…]`. - Ok(instance.inner(db).to_meta_type(db)) + Ok(instance.inner(db).to_meta_type(db, env)) } KnownInstanceType::Callable(callable) => Ok(Type::Callable(*callable)), KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)), @@ -6217,10 +6580,10 @@ impl<'db> Type<'db> { }), Type::Union(union) => { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut invalid_expressions = smallvec::SmallVec::default(); for element in union.elements(db) { - match element.in_type_expression( + match element.in_type_expression_impl( db, scope_id, typevar_binding_context, @@ -6249,7 +6612,7 @@ impl<'db> Type<'db> { Type::Dynamic(_) | Type::Divergent(_) => Ok(*self), Type::NominalInstance(instance) => match instance.known_class(db) { - Some(KnownClass::NoneType) => Ok(Type::none(db)), + Some(KnownClass::NoneType) => Ok(Type::none(db, env)), // TODO: Emit an invalid-type-form diagnostic and recover to `Unknown` for // unrecognized `TypeVar` and `TypeVarTuple` instances. Some(KnownClass::TypeVar) => Ok(todo_type!( @@ -6270,7 +6633,7 @@ impl<'db> Type<'db> { Type::Intersection(_) => Ok(todo_type!("Type::Intersection.in_type_expression")), - Type::TypeAlias(alias) => alias.value_type(db).in_type_expression( + Type::TypeAlias(alias) => alias.value_type(db).in_type_expression_impl( db, scope_id, typevar_binding_context, @@ -6287,8 +6650,8 @@ impl<'db> Type<'db> { } /// The type `NoneType` / `None` - pub fn none(db: &'db dyn Db) -> Type<'db> { - KnownClass::NoneType.to_instance(db) + pub fn none(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + KnownClass::NoneType.to_instance(db, env) } /// Given a type that is assumed to represent an instance of a class, @@ -6297,74 +6660,83 @@ impl<'db> Type<'db> { /// Note: the return type of `type(obj)` is subtly different from this. /// See `Self::dunder_class` for more details. #[must_use] - fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + fn to_meta_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { Type::Never => Type::Never, - Type::NominalInstance(instance) => instance.to_meta_type(db), - Type::KnownInstance(known_instance) => known_instance.to_meta_type(db), - Type::SpecialForm(special_form) => special_form.to_meta_type(db), - Type::PropertyInstance(property) => property.instance_class(db).to_class_literal(db), - Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), - Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_class_literal(db), - Type::TypeForm(_) => Type::object().to_meta_type(db), + Type::NominalInstance(instance) => instance.to_meta_type(db, env), + Type::KnownInstance(known_instance) => known_instance.to_meta_type(db, env), + Type::SpecialForm(special_form) => special_form.to_meta_type(db, env), + Type::PropertyInstance(property) => { + property.instance_class(db).to_class_literal(db, env) + } + Type::Union(union) => union.map(db, env, |ty| ty.to_meta_type(db, env)), + Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_class_literal(db, env), + Type::TypeForm(_) => Type::object().to_meta_type(db, env), Type::LiteralValue(literal) => match literal.kind() { - LiteralValueTypeKind::Bool(_) => KnownClass::Bool.to_class_literal(db), - LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes.to_class_literal(db), - LiteralValueTypeKind::Int(_) => KnownClass::Int.to_class_literal(db), + LiteralValueTypeKind::Bool(_) => KnownClass::Bool.to_class_literal(db, env), + LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes.to_class_literal(db, env), + LiteralValueTypeKind::Int(_) => KnownClass::Int.to_class_literal(db, env), LiteralValueTypeKind::Enum(enum_literal) => { Type::ClassLiteral(enum_literal.enum_class(db)) } LiteralValueTypeKind::String(_) | LiteralValueTypeKind::LiteralString => { - KnownClass::Str.to_class_literal(db) + KnownClass::Str.to_class_literal(db, env) } }, - Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db), - Type::BoundMethod(_) => KnownClass::MethodType.to_class_literal(db), - Type::KnownBoundMethod(method) => method.class().to_class_literal(db), - Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType.to_class_literal(db), - Type::DataclassDecorator(_) => KnownClass::FunctionType.to_class_literal(db), + Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db, env), + Type::BoundMethod(_) => KnownClass::MethodType.to_class_literal(db, env), + Type::KnownBoundMethod(method) => method.class().to_class_literal(db, env), + Type::WrapperDescriptor(_) => { + KnownClass::WrapperDescriptorType.to_class_literal(db, env) + } + Type::DataclassDecorator(_) => KnownClass::FunctionType.to_class_literal(db, env), Type::Callable(callable) if callable.is_function_like(db) => { - KnownClass::FunctionType.to_class_literal(db) + KnownClass::FunctionType.to_class_literal(db, env) + } + Type::Callable(_) | Type::DataclassTransformer(_) => { + KnownClass::Type.to_instance(db, env) } - Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db), - Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), + Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db, env), Type::TypeVar(bound_typevar) => { - SubclassOfType::from(db, SubclassOfInner::TypeVar(bound_typevar)) + SubclassOfType::from(db, env, SubclassOfInner::TypeVar(bound_typevar)) } Type::ClassLiteral(class) => class.metaclass(db), Type::GenericAlias(alias) => ClassType::from(alias).metaclass(db), - Type::SubclassOf(subclass_of_ty) => subclass_of_ty.to_meta_type(db), - Type::Dynamic(dynamic) => SubclassOfType::from(db, SubclassOfInner::Dynamic(dynamic)), + Type::SubclassOf(subclass_of_ty) => subclass_of_ty.to_meta_type(db, env), + Type::Dynamic(dynamic) => { + SubclassOfType::from(db, env, SubclassOfInner::Dynamic(dynamic)) + } Type::Divergent(_) => self, // TODO intersections Type::Intersection(intersection) => { - if let Some(alternatives) = intersection.finite_alternative_union(db) { - alternatives.to_meta_type(db) + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { + alternatives.to_meta_type(db, env) } else { - SubclassOfType::try_from_type(db, todo_type!("Intersection meta-type")) + SubclassOfType::try_from_type(db, env, todo_type!("Intersection meta-type")) .expect("Type::Todo should be a valid `SubclassOfInner`") } } - Type::EnumComplement(complement) => { - complement.remaining_literal_union(db).to_meta_type(db) - } - Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db), - Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db), + Type::EnumComplement(complement) => complement + .remaining_literal_union(db, env) + .to_meta_type(db, env), + Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db, env), + Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db, env), // Class-member lookup on a protocol instance must use the protocol's nominal class. // The structural `type[Protocol]` view is exposed by `dunder_class` and explicit // `type[Protocol]` annotations instead. - Type::ProtocolInstance(protocol) => protocol.to_nominal_meta_type(db), + Type::ProtocolInstance(protocol) => protocol.to_nominal_meta_type(db, env), // `TypedDict` instances are instances of `dict` at runtime, but its important that we // understand a more specific meta type in order to correctly handle `__getitem__`. Type::TypedDict(typed_dict) => match typed_dict { - TypedDictType::Class(class) => SubclassOfType::from(db, class), + TypedDictType::Class(class) => SubclassOfType::from(db, env, class), TypedDictType::Synthesized(_) => SubclassOfType::from( db, + env, todo_type!("TypedDict synthesized meta-type").expect_dynamic(), ), }, - Type::TypeAlias(alias) => alias.value_type(db).to_meta_type(db), - Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).to_meta_type(db), + Type::TypeAlias(alias) => alias.value_type(db).to_meta_type(db, env), + Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).to_meta_type(db, env), } } @@ -6374,19 +6746,23 @@ impl<'db> Type<'db> { /// `type[dict[str, object]]`, because their inhabitants are instances of `dict` at runtime. /// Class-backed protocols return their structural `type[Protocol]` view. #[must_use] - fn dunder_class(self, db: &'db dyn Db) -> Type<'db> { + fn dunder_class(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { - Type::Union(union) => union.map(db, |element| element.dunder_class(db)), + Type::Union(union) => union.map(db, env, |element| element.dunder_class(db, env)), Type::Intersection(intersection) => intersection - .try_dunder_class(db) - .unwrap_or_else(|| self.to_meta_type(db)), - Type::ProtocolInstance(protocol) => protocol.to_meta_type(db), + .try_dunder_class(db, env) + .unwrap_or_else(|| self.to_meta_type(db, env)), + Type::ProtocolInstance(protocol) => protocol.to_meta_type(db, env), Type::TypedDict(_) => KnownClass::Dict - .to_specialized_class_type(db, &[KnownClass::Str.to_instance(db), Type::object()]) + .to_specialized_class_type( + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::object()], + ) .map(Type::from) // Guard against user-customized typesheds with a broken `dict` class .unwrap_or_else(Type::unknown), - _ => self.to_meta_type(db), + _ => self.to_meta_type(db, env), } } @@ -6468,8 +6844,11 @@ impl<'db> Type<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _, _| { - value.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _, specialization: Specialization<'db>| { + let env = ProgramEnvironment::from_program( + specialization.generic_context(db).program(db), + ); + value.cycle_normalized_impl(db, &env, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -6478,6 +6857,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, specialization: Specialization<'db>, ) -> Type<'db> { + let env = &ProgramEnvironment::from_program(specialization.generic_context(db).program(db)); let type_mapping = match specialization.materialization_kind(db) { None => TypeMapping::ApplySpecialization(ApplySpecialization::Specialization( specialization, @@ -6488,16 +6868,17 @@ impl<'db> Type<'db> { }, }; - self.apply_type_mapping(db, &type_mapping, TypeContext::default()) + self.apply_type_mapping(db, env, &type_mapping, TypeContext::default()) } fn apply_type_mapping<'a>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, ) -> Type<'db> { - self.apply_type_mapping_impl(db, type_mapping, tcx, &ApplyTypeMappingVisitor::default()) + self.apply_type_mapping_impl(db, type_mapping, tcx, &ApplyTypeMappingVisitor::new(env)) } fn apply_type_mapping_impl<'a>( @@ -6505,7 +6886,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Type<'db> { // If we are binding `typing.Self`, and this type is what we are binding `Self` to, return // early. This is not just an optimization, it also prevents us from infinitely expanding @@ -6531,7 +6912,7 @@ impl<'db> Type<'db> { TypeMapping::Promote(PromotionMode::On, PromotionKind::ClassLiteralsOnly) ) { - return SubclassOfType::from(db, class.default_specialization(db)); + return SubclassOfType::from(db, visitor.env, class.default_specialization(db)); } match self { @@ -6543,16 +6924,14 @@ impl<'db> Type<'db> { // Promote the types within the signature before promoting the signature to its // callable form. TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular) => { - Type::FunctionLiteral(function.apply_type_mapping_impl( - db, + Type::FunctionLiteral(function.apply_type_mapping_impl(db, type_mapping, tcx, visitor, )) - .promote_impl(db) + .promote_impl(db, visitor.env) } - _ => Type::FunctionLiteral(function.apply_type_mapping_impl( - db, + _ => Type::FunctionLiteral(function.apply_type_mapping_impl(db, type_mapping, tcx, visitor, @@ -6568,15 +6947,17 @@ impl<'db> Type<'db> { Type::NominalInstance(instance) if matches!(type_mapping, TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular)) => { match instance.known_class(db) { - Some(KnownClass::Complex) => KnownUnion::Complex.to_type(db), - Some(KnownClass::Float) => KnownUnion::Float.to_type(db), + Some(KnownClass::Complex) => { + KnownUnion::Complex.to_type(db, visitor.env) + } + Some(KnownClass::Float) => KnownUnion::Float.to_type(db, visitor.env), _ => instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor), } } Type::NominalInstance(instance) if matches!(type_mapping, TypeMapping::Promote(PromotionMode::On, PromotionKind::SingletonsOnly)) => { if instance.is_singleton(db) { - self.promote_singletons_impl(db) + self.promote_singletons_impl(db, visitor.env) } else { instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor) } @@ -6643,11 +7024,11 @@ impl<'db> Type<'db> { Type::PropertyInstance(property.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) } - Type::Union(union) => union.map_leave_aliases(db, |element| { + Type::Union(union) => union.map_leave_aliases(db, visitor.env, |element| { element.apply_type_mapping_impl(db, type_mapping, tcx, visitor) }), Type::Intersection(intersection) => { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, visitor.env); for positive in intersection.positive(db) { builder.add_positive_in_place(positive.apply_type_mapping_impl( db, @@ -6672,7 +7053,7 @@ impl<'db> Type<'db> { } Type::EnumComplement(complement) => complement - .to_intersection(db) + .to_intersection(db, visitor.env) .apply_type_mapping_impl(db, type_mapping, tcx, visitor), Type::TypeIs(type_is) => visitor.visit(db, self, type_mapping, || { @@ -6708,7 +7089,7 @@ impl<'db> Type<'db> { // detection rather than the visitor's cycle detection, because the visitor tracks // Type values and `RecursiveList` is different from `RecursiveList[T]`. TypeMapping::EagerExpansion => { - alias.raw_value_type(db).expand_eagerly(db) + alias.raw_value_type(db).expand_eagerly(db, visitor.env) }, // When specializing a generic type alias, instead of specializing the expanded type, the type alias itself is specialized. // Without this special handling, recursive type aliases would result in cycles, returning an unspecialized fallback type. @@ -6730,8 +7111,7 @@ impl<'db> Type<'db> { current_specialization = current_specialization .with_materialization_kind(db, Some(*materialization_kind)); } - Type::TypeAlias(alias.apply_specialization( - db, + Type::TypeAlias(alias.apply_specialization(db, |generic_context| { alias .specialization(db) @@ -6775,7 +7155,7 @@ impl<'db> Type<'db> { PromotionMode::On, PromotionKind::ClassLiteralsOnly | PromotionKind::SingletonsOnly, ) => self, - TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular) => self.promote_impl(db), + TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular) => self.promote_impl(db, visitor.env), } Type::Dynamic(_) => match type_mapping { @@ -6840,11 +7220,13 @@ impl<'db> Type<'db> { fn find_legacy_typevars( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, ) { self.find_legacy_typevars_impl( db, + env, binding_context, typevars, &FindLegacyTypeVarsVisitor::default(), @@ -6854,6 +7236,7 @@ impl<'db> Type<'db> { fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, @@ -6900,19 +7283,21 @@ impl<'db> Type<'db> { Type::FunctionLiteral(function) => { visitor.visit(db, self, || { - function.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + function.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); }); } Type::BoundMethod(method) => visitor.visit(db, self, || { method.self_instance(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, ); method.function(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -6923,7 +7308,7 @@ impl<'db> Type<'db> { KnownBoundMethodType::FunctionTypeDunderGet(function) | KnownBoundMethodType::FunctionTypeDunderCall(function), ) => visitor.visit(db, self, || { - function.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + function.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); }), Type::KnownBoundMethod( @@ -6931,46 +7316,46 @@ impl<'db> Type<'db> { | KnownBoundMethodType::PropertyDunderSet(property) | KnownBoundMethodType::PropertyDunderDelete(property), ) => visitor.visit(db, self, || { - property.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + property.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); }), Type::Callable(callable) => { - callable.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + callable.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Type::PropertyInstance(property) => visitor.visit(db, self, || { - property.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + property.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); }), Type::Union(union) => { for element in union.elements(db) { - element.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + element.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } Type::Intersection(intersection) => { for positive in intersection.positive(db) { - positive.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + positive.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } for negative in intersection.negative(db) { - negative.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + negative.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } Type::EnumComplement(complement) => { for rest in complement.rest(db) { - rest.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + rest.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } Type::GenericAlias(alias) => { - alias.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + alias.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Type::NominalInstance(instance) => { - instance.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + instance.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Type::ProtocolInstance(instance) => { - instance.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + instance.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Type::NewTypeInstance(_) => { @@ -6980,12 +7365,13 @@ impl<'db> Type<'db> { } Type::SubclassOf(subclass_of) => { - subclass_of.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + subclass_of.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Type::TypeIs(type_is) => { type_is.type_argument(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -6995,6 +7381,7 @@ impl<'db> Type<'db> { Type::TypeGuard(type_guard) => { type_guard.return_type(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -7004,6 +7391,7 @@ impl<'db> Type<'db> { Type::TypeForm(typeform) => { typeform.type_argument(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -7014,6 +7402,7 @@ impl<'db> Type<'db> { visitor.visit(db, self, || { alias.value_type(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -7026,6 +7415,7 @@ impl<'db> Type<'db> { if let Ok(union_type) = instance.union_type(db) { union_type.find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -7033,16 +7423,32 @@ impl<'db> Type<'db> { } } KnownInstanceType::Annotated(ty) => { - ty.inner(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.inner(db).find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } KnownInstanceType::Callable(callable_type) => { - callable_type.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + callable_type.find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } KnownInstanceType::TypeGenericAlias(ty) | KnownInstanceType::LiteralStringAlias(ty) => { - ty.inner(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.inner(db).find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } KnownInstanceType::SubscriptedProtocol(_) | KnownInstanceType::SubscriptedGeneric(_) @@ -7108,25 +7514,32 @@ impl<'db> Type<'db> { fn bind_and_find_all_legacy_typevars( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, variables: &mut FxOrderSet>, ) { self.apply_type_mapping( db, + env, &TypeMapping::BindLegacyTypevars( binding_context .map(BindingContext::Definition) - .unwrap_or(BindingContext::Synthetic), + .unwrap_or(BindingContext::Synthetic(env.program(db))), ), TypeContext::default(), ) - .find_legacy_typevars(db, None, variables); + .find_legacy_typevars(db, env, None, variables); } /// Replace default types in parameters of callables with `Unknown`. - fn replace_parameter_defaults(self, db: &'db dyn Db) -> Type<'db> { + fn replace_parameter_defaults( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { self.apply_type_mapping( db, + env, &TypeMapping::ReplaceParameterDefaults, TypeContext::default(), ) @@ -7134,21 +7547,26 @@ impl<'db> Type<'db> { /// Returns the eagerly expanded type. /// In the case of recursive type aliases, this will diverge, so that part will be replaced with `Divergent`. - fn expand_eagerly(self, db: &'db dyn Db) -> Type<'db> { - self.expand_eagerly_(db, ()) + fn expand_eagerly(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.expand_eagerly_(db, env.program(db)) } - #[allow(clippy::used_underscore_binding)] #[salsa::tracked( returns(copy), - cycle_initial=|_, id, _, ()| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _, ()| { - value.cycle_normalized(db, *previous, cycle) + cycle_initial=|_, id, _, _| Type::divergent(id), + cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _, program| { + value.cycle_normalized_impl(db, &ProgramEnvironment::from_program(program), *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] - fn expand_eagerly_(self, db: &'db dyn Db, _unit: ()) -> Type<'db> { - self.apply_type_mapping(db, &TypeMapping::EagerExpansion, TypeContext::default()) + fn expand_eagerly_(self, db: &'db dyn Db, program: Program) -> Type<'db> { + let env = &ProgramEnvironment::from_program(program); + self.apply_type_mapping( + db, + env, + &TypeMapping::EagerExpansion, + TypeContext::default(), + ) } /// Return the string representation of this type when converted to string as it would be @@ -7157,10 +7575,10 @@ impl<'db> Type<'db> { /// When not available, this should fall back to the value of `[Type::repr]`. /// Note: this method is used in the builtins `format`, `print`, `str.format` and `f-strings`. #[must_use] - fn str(&self, db: &'db dyn Db) -> Type<'db> { + fn str(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { Type::LiteralValue(literal) => match literal.kind() { - LiteralValueTypeKind::Int(_) | LiteralValueTypeKind::Bool(_) => self.repr(db), + LiteralValueTypeKind::Int(_) | LiteralValueTypeKind::Bool(_) => self.repr(db, env), LiteralValueTypeKind::String(_) | LiteralValueTypeKind::LiteralString => *self, LiteralValueTypeKind::Enum(enum_literal) => Type::string_literal( db, @@ -7170,32 +7588,34 @@ impl<'db> Type<'db> { name = enum_literal.name(db) ), ), - LiteralValueTypeKind::Bytes(_) => KnownClass::Str.to_instance(db), + LiteralValueTypeKind::Bytes(_) => KnownClass::Str.to_instance(db, env), }, Type::SpecialForm(special_form) => { Type::string_literal(db, special_form.to_compact_string()) } Type::KnownInstance(known_instance) => { - Type::string_literal(db, known_instance.repr(db).to_compact_string()) + Type::string_literal(db, known_instance.repr(db, env).to_compact_string()) } - ty if ty.is_subtype_of(db, Type::literal_string()) => Type::literal_string(), + ty if ty.is_subtype_of(db, env, Type::literal_string()) => Type::literal_string(), Type::Intersection(intersection) => { - if let Some(alternatives) = intersection.finite_alternative_union(db) { - alternatives.str(db) + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { + alternatives.str(db, env) } else { - KnownClass::Str.to_instance(db) + KnownClass::Str.to_instance(db, env) } } - Type::EnumComplement(complement) => complement.remaining_literal_union(db).str(db), + Type::EnumComplement(complement) => { + complement.remaining_literal_union(db, env).str(db, env) + } // TODO: handle more complex types - _ => KnownClass::Str.to_instance(db), + _ => KnownClass::Str.to_instance(db, env), } } /// Return the string representation of this type as it would be provided by the `__repr__` /// method at runtime. #[must_use] - fn repr(&self, db: &'db dyn Db) -> Type<'db> { + fn repr(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Int(number) => { @@ -7208,14 +7628,14 @@ impl<'db> Type<'db> { compact_str::format_compact!("'{}'", literal.value(db).escape_default()), ), LiteralValueTypeKind::LiteralString => Type::literal_string(), - _ => KnownClass::Str.to_instance(db), + _ => KnownClass::Str.to_instance(db, env), }, Type::SpecialForm(special_form) => Type::string_literal(db, &*special_form.to_string()), Type::KnownInstance(known_instance) => { - Type::string_literal(db, known_instance.repr(db).to_compact_string()) + Type::string_literal(db, known_instance.repr(db, env).to_compact_string()) } // TODO: handle more complex types - _ => KnownClass::Str.to_instance(db), + _ => KnownClass::Str.to_instance(db, env), } } @@ -7228,7 +7648,11 @@ impl<'db> Type<'db> { /// should be handled, especially when some variants don't have definitions, is /// specific to the call site. Exact singleton finite intersections delegate to /// their only alternative, since there is no ambiguity to preserve there. - pub fn definition(&self, db: &'db dyn Db) -> Option> { + pub fn definition( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { Self::BoundMethod(method) => { Some(TypeDefinition::Function(method.function(db).definition(db))) @@ -7239,7 +7663,7 @@ impl<'db> Type<'db> { Self::ModuleLiteral(module) => Some(TypeDefinition::Module(module.module(db))), Self::ClassLiteral(class_literal) => class_literal.type_definition(db), Self::GenericAlias(alias) => Some(TypeDefinition::StaticClass(alias.definition(db))), - Self::NominalInstance(instance) => instance.class(db).type_definition(db), + Self::NominalInstance(instance) => instance.class(db, env).type_definition(db), Self::KnownInstance(instance) => match instance { KnownInstanceType::TypeVar(var) => { Some(TypeDefinition::TypeVar(var.definition(db)?)) @@ -7264,30 +7688,34 @@ impl<'db> Type<'db> { )), }, - Self::TypeAlias(alias) => alias.value_type(db).definition(db), + Self::TypeAlias(alias) => alias.value_type(db).definition(db, env), Self::NewTypeInstance(newtype) => Some(TypeDefinition::NewType(newtype.definition(db))), Self::PropertyInstance(property) => property .getter(db) - .and_then(|getter| getter.definition(db)) - .or_else(|| property.setter(db).and_then(|setter| setter.definition(db))) + .and_then(|getter| getter.definition(db, env)) + .or_else(|| { + property + .setter(db) + .and_then(|setter| setter.definition(db, env)) + }) .or_else(|| { property .deleter(db) - .and_then(|deleter| deleter.definition(db)) + .and_then(|deleter| deleter.definition(db, env)) }), Self::LiteralValue(literal) => literal .as_enum() .and_then(|enum_lit| enum_lit.definition(db)) .map(TypeDefinition::EnumMember) - .or_else(|| self.to_meta_type(db).definition(db)), + .or_else(|| self.to_meta_type(db, env).definition(db, env)), Self::KnownBoundMethod(_) | Self::WrapperDescriptor(_) | Self::DataclassDecorator(_) | Self::DataclassTransformer(_) - | Self::BoundSuper(_) => self.to_meta_type(db).definition(db), + | Self::BoundSuper(_) => self.to_meta_type(db, env).definition(db, env), Self::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar( bound_typevar.typevar(db).definition(db)?, @@ -7301,36 +7729,40 @@ impl<'db> Type<'db> { Self::Union(_) => None, Self::Intersection(intersection) => { - let alternatives = intersection.finite_alternatives(db)?; + let alternatives = intersection.finite_alternatives(db, env)?; let [alternative] = alternatives.as_slice() else { return None; }; - alternative.definition(db) + alternative.definition(db, env) } Self::EnumComplement(complement) => { - let alternatives = complement.remaining_literal_types(db); + let alternatives = complement.remaining_literal_types(db, env); let [alternative] = alternatives.as_slice() else { return None; }; - alternative.definition(db) + alternative.definition(db, env) } - Self::SpecialForm(special_form) => special_form.definition(db), - Self::Never => Type::SpecialForm(SpecialFormType::Never).definition(db), + Self::SpecialForm(special_form) => special_form.definition(db, env), + Self::Never => Type::SpecialForm(SpecialFormType::Never).definition(db, env), Self::Dynamic(DynamicType::Any) => { - Type::SpecialForm(SpecialFormType::Any).definition(db) + Type::SpecialForm(SpecialFormType::Any).definition(db, env) } Self::Dynamic( DynamicType::Unknown | DynamicType::UnknownGeneric(_) | DynamicType::AmbiguousOverload, - ) => Type::SpecialForm(SpecialFormType::Unknown).definition(db), - Self::Divergent(_) => Type::SpecialForm(SpecialFormType::Divergent).definition(db), + ) => Type::SpecialForm(SpecialFormType::Unknown).definition(db, env), + Self::Divergent(_) => Type::SpecialForm(SpecialFormType::Divergent).definition(db, env), Self::Dynamic(DynamicType::Todo(_)) => { - Type::SpecialForm(SpecialFormType::Todo).definition(db) + Type::SpecialForm(SpecialFormType::Todo).definition(db, env) + } + Self::AlwaysTruthy => { + Type::SpecialForm(SpecialFormType::AlwaysTruthy).definition(db, env) + } + Self::AlwaysFalsy => { + Type::SpecialForm(SpecialFormType::AlwaysFalsy).definition(db, env) } - Self::AlwaysTruthy => Type::SpecialForm(SpecialFormType::AlwaysTruthy).definition(db), - Self::AlwaysFalsy => Type::SpecialForm(SpecialFormType::AlwaysFalsy).definition(db), // These types have no definition Self::Dynamic( @@ -7407,11 +7839,15 @@ impl<'db> Type<'db> { } } - fn generic_origin(self, db: &'db dyn Db) -> Option> { + fn generic_origin( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { Type::GenericAlias(generic) => Some(generic.origin(db)), Type::NominalInstance(instance) - if let ClassType::Generic(generic) = instance.class(db) => + if let ClassType::Generic(generic) = instance.class(db, env) => { Some(generic.origin(db)) } @@ -7422,18 +7858,22 @@ impl<'db> Type<'db> { /// Default-specialize all legacy typevars in this type. /// /// This is used when an implicit type alias is referenced without explicitly specializing it. - fn default_specialize(self, db: &'db dyn Db) -> Type<'db> { + fn default_specialize(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { let mut variables = FxOrderSet::default(); - self.find_legacy_typevars(db, None, &mut variables); - let generic_context = GenericContext::from_typevar_instances(db, variables); + self.find_legacy_typevars(db, env, None, &mut variables); + let generic_context = GenericContext::from_typevar_instances(db, env, variables); self.apply_specialization(db, generic_context.default_specialization(db, None)) } - fn from_truthiness(db: &'db dyn Db, truthiness: Truthiness) -> Self { + fn from_truthiness( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + truthiness: Truthiness, + ) -> Self { match truthiness { Truthiness::AlwaysTrue => Type::bool_literal(true), Truthiness::AlwaysFalse => Type::bool_literal(false), - Truthiness::Ambiguous => KnownClass::Bool.to_instance(db), + Truthiness::Ambiguous => KnownClass::Bool.to_instance(db, env), } } @@ -7442,14 +7882,17 @@ impl<'db> Type<'db> { fn negation_is_subtype_of_cached( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, negated_cache: &mut Option>, ) -> bool { match self { - Type::Intersection(intersection) => intersection.negation_is_subtype_of(db, target), + Type::Intersection(intersection) => { + intersection.negation_is_subtype_of(db, env, target) + } _ => { - let negated = negated_cache.get_or_insert_with(|| self.negate(db)); - negated.is_subtype_of(db, target) + let negated = negated_cache.get_or_insert_with(|| self.negate(db, env)); + negated.is_subtype_of(db, env, target) } } } @@ -7461,14 +7904,19 @@ impl<'db> IntersectionType<'db> { /// Applying De Morgan's law to an intersection produces a union. Checking each branch /// directly avoids constructing and simplifying that temporary union, which can be costly /// for the large intersections produced by repeated narrowing. - fn negation_is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { + fn negation_is_subtype_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + ) -> bool { self.positive(db) .iter() - .all(|positive| positive.negate(db).is_subtype_of(db, target)) + .all(|positive| positive.negate(db, env).is_subtype_of(db, env, target)) && self .negative(db) .iter() - .all(|negative| negative.is_subtype_of(db, target)) + .all(|negative| negative.is_subtype_of(db, env, target)) } // Calls the dunder on each element separately and combines the results. @@ -7480,13 +7928,21 @@ impl<'db> IntersectionType<'db> { fn try_call_dunder_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, argument_types: &mut CallArguments<'_, 'db>, tcx: TypeContext<'db>, policy: MemberLookupPolicy, ) -> Result, CallDunderError<'db>> { - if let Some(alternatives) = self.finite_alternative_union(db) { - return alternatives.try_call_dunder_with_policy(db, name, argument_types, tcx, policy); + if let Some(alternatives) = self.finite_alternative_union(db, env) { + return alternatives.try_call_dunder_with_policy( + db, + env, + name, + argument_types, + tcx, + policy, + ); } // Using `positive()` rather than `positive_elements_or_object()` is safe @@ -7499,7 +7955,7 @@ impl<'db> IntersectionType<'db> { let mut error_provenance = Provenance::Unknown; for element in positive { - match element.try_call_dunder_with_policy(db, name, argument_types, tcx, policy) { + match element.try_call_dunder_with_policy(db, env, name, argument_types, tcx, policy) { Ok(bindings) => successful_bindings.push(bindings), Err(err) => { error_provenance = error_provenance.or(err.provenance()); @@ -7533,13 +7989,14 @@ impl<'db> UnionType<'db> { fn try_call_dunder_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, argument_types: &mut CallArguments<'_, 'db>, tcx: TypeContext<'db>, policy: MemberLookupPolicy, ) -> Result, CallDunderError<'db>> { let elements = self.elements(db); - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut unbound_on: Vec> = Vec::new(); let mut any_defined = false; let mut possibly_undefined = false; @@ -7549,6 +8006,7 @@ impl<'db> UnionType<'db> { match element .member_lookup_with_policy( db, + env, name, policy | MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -7588,9 +8046,9 @@ impl<'db> UnionType<'db> { let dunder_callable = builder.build(); let constraints = ConstraintSetBuilder::new(); let bindings = match dunder_callable - .bindings(db) - .match_parameters(db, argument_types) - .check_types(db, &constraints, argument_types, tcx, &[]) + .bindings(db, env) + .match_parameters(db, env, argument_types) + .check_types(db, env, &constraints, argument_types, tcx, &[]) { Ok(bindings) => bindings, Err(CallError(kind, bindings)) => { @@ -7616,15 +8074,20 @@ impl<'db> From<&Type<'db>> for Type<'db> { } impl<'db> VarianceInferable<'db> for Type<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { tracing::trace!( "Checking variance of '{tvar}' in `{ty:?}`", tvar = typevar.identity.name(db), - ty = self.display(db), + ty = self.display(db, env), ); let v = match self { - Type::ClassLiteral(class_literal) => class_literal.variance_of(db, typevar), + Type::ClassLiteral(class_literal) => class_literal.variance_of(db, env, typevar), Type::FunctionLiteral(function_type) => { // TODO: do we need to replace self? @@ -7637,23 +8100,25 @@ impl<'db> VarianceInferable<'db> for Type<'db> { } Type::NominalInstance(nominal_instance_type) => { - nominal_instance_type.variance_of(db, typevar) + nominal_instance_type.variance_of(db, env, typevar) + } + Type::GenericAlias(generic_alias) => generic_alias.variance_of(db, env, typevar), + Type::Callable(callable_type) => { + callable_type.signatures(db).variance_of(db, env, typevar) } - Type::GenericAlias(generic_alias) => generic_alias.variance_of(db, typevar), - Type::Callable(callable_type) => callable_type.signatures(db).variance_of(db, typevar), // A type variable is always covariant in itself. Type::TypeVar(other_typevar) if other_typevar.identity(db) == typevar => { // type variables are covariant in themselves TypeVarVariance::Covariant } Type::ProtocolInstance(protocol_instance_type) => { - protocol_instance_type.variance_of(db, typevar) + protocol_instance_type.variance_of(db, env, typevar) } // unions are covariant in their disjuncts Type::Union(union_type) => union_type .elements(db) .iter() - .map(|ty| ty.variance_of(db, typevar)) + .map(|ty| ty.variance_of(db, env, typevar)) .collect(), // Products are covariant in their conjuncts. For negative @@ -7665,28 +8130,28 @@ impl<'db> VarianceInferable<'db> for Type<'db> { Type::Intersection(intersection_type) => intersection_type .positive(db) .iter() - .map(|ty| ty.variance_of(db, typevar)) + .map(|ty| ty.variance_of(db, env, typevar)) .chain(intersection_type.negative(db).iter().map(|ty| { ty.with_polarity(TypeVarVariance::Contravariant) - .variance_of(db, typevar) + .variance_of(db, env, typevar) })) .collect(), - Type::EnumComplement(complement) => { - complement.to_intersection(db).variance_of(db, typevar) - } + Type::EnumComplement(complement) => complement + .to_intersection(db, env) + .variance_of(db, env, typevar), Type::PropertyInstance(property_instance_type) => property_instance_type .getter(db) .iter() .chain(&property_instance_type.setter(db)) .chain(&property_instance_type.deleter(db)) - .map(|ty| ty.variance_of(db, typevar)) + .map(|ty| ty.variance_of(db, env, typevar)) .collect(), - Type::SubclassOf(subclass_of_type) => subclass_of_type.variance_of(db, typevar), - Type::TypeIs(type_is_type) => type_is_type.variance_of(db, typevar), - Type::TypeGuard(type_guard_type) => type_guard_type.variance_of(db, typevar), - Type::TypeForm(typeform_type) => typeform_type.variance_of(db, typevar), - Type::KnownInstance(known_instance) => known_instance.variance_of(db, typevar), - Type::TypeAlias(alias) => alias.variance_of(db, typevar), + Type::SubclassOf(subclass_of_type) => subclass_of_type.variance_of(db, env, typevar), + Type::TypeIs(type_is_type) => type_is_type.variance_of(db, env, typevar), + Type::TypeGuard(type_guard_type) => type_guard_type.variance_of(db, env, typevar), + Type::TypeForm(typeform_type) => typeform_type.variance_of(db, env, typevar), + Type::KnownInstance(known_instance) => known_instance.variance_of(db, env, typevar), + Type::TypeAlias(alias) => alias.variance_of(db, env, typevar), Type::Dynamic(_) | Type::Divergent(_) | Type::Never @@ -7708,7 +8173,7 @@ impl<'db> VarianceInferable<'db> for Type<'db> { tracing::trace!( "Result of variance of '{tvar}' in `{ty:?}` is `{v:?}`", tvar = typevar.identity.name(db), - ty = self.display(db), + ty = self.display(db, env), ); v } @@ -7743,12 +8208,13 @@ pub enum PromotionKind { /// Returns the [`ClassLiteral`] that "owns" a `Self` typevar (i.e., the class from its upper bound). fn self_typevar_owner_class_literal<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bound_typevar: BoundTypeVarInstance<'db>, ) -> Option> { bound_typevar .typevar(db) - .upper_bound(db) - .and_then(|ty| ty.nominal_class(db)) + .upper_bound(db, env) + .and_then(|ty| ty.nominal_class(db, env)) .map(|class| class.class_literal(db)) } @@ -7788,15 +8254,16 @@ impl<'db> SelfBinding<'db> { impl<'db> SelfBinding<'db> { fn new( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, self_type: Type<'db>, binding_context: Option>, ) -> Self { let class_literal = match self_type { Type::TypeVar(typevar) if typevar.typevar(db).is_self(db) => { - self_typevar_owner_class_literal(db, typevar) + self_typevar_owner_class_literal(db, env, typevar) } _ => self_type - .nominal_class(db) + .nominal_class(db, env) .map(|class| class.class_literal(db)), }; @@ -7808,7 +8275,12 @@ impl<'db> SelfBinding<'db> { } /// Returns whether `bound_typevar` should be replaced by this binding's concrete self type. - fn should_bind(&self, db: &'db dyn Db, bound_typevar: BoundTypeVarInstance<'db>) -> bool { + fn should_bind( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + bound_typevar: BoundTypeVarInstance<'db>, + ) -> bool { if !bound_typevar.typevar(db).is_self(db) { return false; } @@ -7823,7 +8295,7 @@ impl<'db> SelfBinding<'db> { // If we can't determine either class, conservatively don't bind. self.class_literal.is_some_and(|class_literal| { let class_mro = class_mro_literals(db, class_literal); - self_typevar_owner_class_literal(db, bound_typevar) + self_typevar_owner_class_literal(db, env, bound_typevar) .is_none_or(|owner_class| class_mro.contains(&owner_class)) }) } @@ -7879,14 +8351,16 @@ impl<'db> TypeMapping<'_, 'db> { fn update_signature_generic_context( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, context: GenericContext<'db>, ) -> GenericContext<'db> { match self { TypeMapping::FreshenBoundTypeVars { .. } => GenericContext::from_typevar_instances( db, + env, context.variables(db).map(|bound_typevar| { Type::TypeVar(bound_typevar) - .apply_type_mapping(db, self, TypeContext::default()) + .apply_type_mapping(db, env, self, TypeContext::default()) .as_typevar() .unwrap_or(bound_typevar) }), @@ -7897,6 +8371,7 @@ impl<'db> TypeMapping<'_, 'db> { // (i.e., mapped to a non-TypeVar type) GenericContext::from_typevar_instances( db, + env, context.variables(db).filter(|bound_typevar| { // Keep the type variable if it's not in the specialization // or if it's mapped to itself (still a TypeVar) @@ -7926,6 +8401,7 @@ impl<'db> TypeMapping<'_, 'db> { } TypeMapping::ReplaceSelf { new_upper_bound } => GenericContext::from_typevar_instances( db, + env, context.variables(db).map(|typevar| { if typevar.typevar(db).is_self(db) { BoundTypeVarInstance::synthetic_self( @@ -8224,16 +8700,18 @@ impl<'db> InvalidTypeExpressionError<'db> { node: &impl Ranged, flags: InferenceFlags, ) -> Type<'db> { + let db = context.db(); let InvalidTypeExpressionError { fallback_type, invalid_expressions, } = self; + let env = context.program_environment(); for error in invalid_expressions { let Some(builder) = context.report_lint(&INVALID_TYPE_FORM, node) else { continue; }; - let diagnostic = builder.into_diagnostic(error.reason(context.db(), flags)); - error.add_subdiagnostics(context.db(), diagnostic, node); + let diagnostic = builder.into_diagnostic(error.reason(db, env, flags)); + error.add_subdiagnostics(db, env, diagnostic, node); } fallback_type } @@ -8289,15 +8767,22 @@ enum InvalidTypeExpression<'db> { } impl<'db> InvalidTypeExpression<'db> { - const fn reason(self, db: &'db dyn Db, flags: InferenceFlags) -> impl std::fmt::Display + 'db { + fn reason( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + flags: InferenceFlags, + ) -> impl std::fmt::Display + 'db { struct Display<'db> { error: InvalidTypeExpression<'db>, db: &'db dyn Db, + env: ProgramEnvironment<'db>, flags: InferenceFlags, } impl std::fmt::Display for Display<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; let location = self.flags.type_expression_context(); match self.error { @@ -8393,28 +8878,28 @@ impl<'db> InvalidTypeExpression<'db> { write!( f, "Function `{function}` is not valid in a {location}", - function = function.name(self.db) + function = function.name(db) ) } InvalidTypeExpression::InvalidType(Type::ModuleLiteral(module), _) => write!( f, "Module `{module}` is not valid in a {location}", - module = module.module(self.db).name(self.db) + module = module.module(db).name(db) ), InvalidTypeExpression::InvalidType(ty, _) => write!( f, "Variable of type `{ty}` is not allowed in a {location}", - ty = ty.display(self.db) + ty = ty.display(db, &self.env) ), InvalidTypeExpression::InvalidBareParamSpec(paramspec) => write!( f, "Bare ParamSpec `{}` is not valid in this context in a {location}", - paramspec.name(self.db) + paramspec.name(db) ), InvalidTypeExpression::InvalidBareTypeVarTuple(typevartuple) => write!( f, "Bare TypeVarTuple `{}` is not valid in this context in a {location}", - typevartuple.name(self.db) + typevartuple.name(db) ), InvalidTypeExpression::Concatenate => write!( f, @@ -8427,6 +8912,7 @@ impl<'db> InvalidTypeExpression<'db> { Display { error: self, db, + env: env.clone(), flags, } } @@ -8434,6 +8920,7 @@ impl<'db> InvalidTypeExpression<'db> { fn add_subdiagnostics( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut diagnostic: LintDiagnosticGuard, node: &impl Ranged, ) { @@ -8448,7 +8935,7 @@ impl<'db> InvalidTypeExpression<'db> { let module = module.module(db); let module_name_final_part = module.name(db).last_component(); let Some(module_member_with_same_name) = ty - .member(db, module_name_final_part) + .member(db, env, module_name_final_part) .place .ignore_possibly_undefined() else { @@ -8483,8 +8970,8 @@ impl<'db> InvalidTypeExpression<'db> { && function_body_scope .scope(db) .parent() - .map(|parent| parent.to_scope_id(db, function_body_scope.file(db))) - == builtins_module_scope(db) + .map(|parent| parent.to_scope_id(db, function_body_scope.python_file(db))) + == builtins_module_scope(db, env) { diagnostic.set_primary_annotation_message("Did you mean `collections.abc.Callable`?"); } else if matches!(self, InvalidTypeExpression::InvalidBareParamSpec(_)) { @@ -8526,9 +9013,10 @@ impl<'db> AwaitError<'db> { }; let db = context.db(); + let env = context.program_environment(); let mut diag = builder.into_diagnostic( - format_args!("`{type}` is not awaitable", type = context_expression_type.display(db)), + format_args!("`{type}` is not awaitable", type = context_expression_type.display(db, env)), ); match self { Self::Call(CallDunderError::CallError(CallErrorKind::BindingError, bindings, _)) => { @@ -8552,7 +9040,7 @@ impl<'db> AwaitError<'db> { }; diag.info(format_args!("`__await__` is{possibly} not callable")); if let Some(definition) = attribute_provenance.definition() { - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); diag.annotate( Annotation::secondary(definition.focus_range(db, &module).into()) .message("attribute defined here"), @@ -8568,7 +9056,7 @@ impl<'db> AwaitError<'db> { for ty in unbound_on { diag.info(format_args!( "`{}` does not implement `__await__`", - ty.display(db) + ty.display(db, env) )); } } @@ -8581,7 +9069,7 @@ impl<'db> AwaitError<'db> { } Self::Call(CallDunderError::MethodNotAvailable) => { diag.info("`__await__` is missing"); - if let Some(type_definition) = context_expression_type.definition(db) + if let Some(type_definition) = context_expression_type.definition(db, env) && let Some(definition_range) = type_definition.focus_range(db) { diag.annotate( @@ -8592,7 +9080,7 @@ impl<'db> AwaitError<'db> { Self::InvalidReturnType(return_type, bindings) => { diag.info(format_args!( "`__await__` returns `{return_type}`, which is not a valid iterator", - return_type = return_type.display(db) + return_type = return_type.display(db, env) )); if let Some(definition_spans) = bindings.callable_type().function_spans(db) { diag.annotate( @@ -8622,14 +9110,14 @@ pub struct ModuleLiteralType<'db> { /// the same underlying single-file module are understood by ty as being equivalent types /// in all situations. #[returns(copy)] - _importing_file: Option, + _importing_file: Option>, } // The Salsa heap is tracked separately. impl get_size2::GetSize for ModuleLiteralType<'_> {} impl<'db> ModuleLiteralType<'db> { - fn importing_file(self, db: &'db dyn Db) -> Option { + fn importing_file(self, db: &'db dyn Db) -> Option> { debug_assert_eq!( self._importing_file(db).is_some(), self.module(db).kind(db).is_package() @@ -8705,21 +9193,28 @@ impl<'db> ModuleLiteralType<'db> { Some(Type::module_literal(db, importing_file, submodule)) } - fn try_module_getattr(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + fn try_module_getattr( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { // For module literals, we want to try calling the module's own `__getattr__` function // if it exists. First, we need to look up the `__getattr__` function in the module's scope. - if let Some(file) = self.module(db).file(db) { - let getattr_symbol = imported_symbol(db, Some(file), "__getattr__", None); + let module = self.module(db); + if let Some(file) = module.python_file(db) { + let getattr_symbol = imported_symbol(db, env, Some(file), "__getattr__", None); // If we found a __getattr__ function, try to call it with the name argument if let Place::Defined(place) = getattr_symbol.place && let Ok(outcome) = place.ty.try_call( db, + env, &CallArguments::positional([Type::string_literal(db, name)]), ) { return PlaceAndQualifiers { place: Place::Defined(DefinedPlace { - ty: outcome.return_type(db), + ty: outcome.return_type(db, env), provenance: Provenance::Unknown, ..place }), @@ -8731,14 +9226,20 @@ impl<'db> ModuleLiteralType<'db> { Place::Undefined.into() } - fn static_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + fn static_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + let module = self.module(db); // `__dict__` is a very special member that is never overridden by module globals; // we should always look it up directly as an attribute on `types.ModuleType`, // never in the global scope of the module. if name == "__dict__" { return KnownClass::ModuleType - .to_instance(db) - .member(db, "__dict__"); + .to_instance(db, env) + .member(db, env, "__dict__"); } // If the file that originally imported the module has also imported a submodule @@ -8756,11 +9257,11 @@ impl<'db> ModuleLiteralType<'db> { return Place::bound(submodule).into(); } - let place_and_qualifiers = imported_symbol(db, self.module(db).file(db), name, None); + let place_and_qualifiers = imported_symbol(db, env, module.python_file(db), name, None); // If the normal lookup failed, try to call the module's `__getattr__` function if place_and_qualifiers.place.is_undefined() { - return self.try_module_getattr(db, name); + return self.try_module_getattr(db, env, name); } // typeshed re-exports some special forms across modules (e.g. `collections.abc.Callable` @@ -8867,10 +9368,15 @@ impl<'db> TypeIsType<'db> { impl<'db> VarianceInferable<'db> for TypeIsType<'db> { // See the [typing spec] on why `TypeIs` is invariant in its type. // [typing spec]: https://typing.python.org/en/latest/spec/narrowing.html#typeis - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { self.type_argument(db) .with_polarity(TypeVarVariance::Invariant) - .variance_of(db, typevar) + .variance_of(db, env, typevar) } } @@ -8934,8 +9440,13 @@ impl<'db> TypeGuardType<'db> { impl<'db> VarianceInferable<'db> for TypeGuardType<'db> { // `TypeGuard` is covariant in its type parameter. See the `TypeGuard` // section of mdtest/generics/pep695/variance.md for details. - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { - self.return_type(db).variance_of(db, typevar) + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.return_type(db).variance_of(db, env, typevar) } } @@ -9003,6 +9514,7 @@ impl<'db> TypeGuardLike<'db> for TypeGuardType<'db> { /// being added to the given class. pub(super) fn determine_upper_bound<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class_literal: ClassLiteral<'db>, is_known_base: impl Fn(ClassBase<'db>) -> bool, ) -> Type<'db> { @@ -9012,7 +9524,7 @@ pub(super) fn determine_upper_bound<'db>( .filter_map(ClassBase::into_class) .last() .unwrap_or_else(|| class_literal.unknown_specialization(db)); - Type::instance(db, upper_bound) + Type::instance(db, env, upper_bound) } // Make sure that the `Type` enum does not grow unexpectedly. diff --git a/crates/ty_python_semantic/src/types/attribute_write.rs b/crates/ty_python_semantic/src/types/attribute_write.rs index 7adea613a2..a23f82d58f 100644 --- a/crates/ty_python_semantic/src/types/attribute_write.rs +++ b/crates/ty_python_semantic/src/types/attribute_write.rs @@ -7,6 +7,7 @@ //! diagnostics, while protocol checking can evaluate the same lookup result using its active type //! relation and constraint set. +use crate::Db; use ty_module_resolver::KnownModule; use super::call::CallArguments; @@ -14,7 +15,7 @@ use super::callable::CallableTypeKind; use super::{ IntersectionType, KnownClass, KnownInstanceType, MemberLookupPolicy, Type, TypeQualifiers, }; -use crate::Db; +use crate::ProgramEnvironment; use crate::place::{DefinedPlace, Definedness, Place, PlaceAndQualifiers, builtins_symbol}; /// The operation required to write an attribute. @@ -223,6 +224,7 @@ impl<'db> AssignmentAttributeMembers<'db> { /// paths. It does not compare the assigned value with the resulting types. pub(super) fn attribute_write_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> AttributeWriteRequirement<'db> { @@ -240,11 +242,16 @@ pub(super) fn attribute_write_requirement<'db>( } } - Type::EnumComplement(complement) => { - attribute_write_requirement(db, complement.remaining_literal_union(db), attribute) - } + Type::EnumComplement(complement) => attribute_write_requirement( + db, + env, + complement.remaining_literal_union(db, env), + attribute, + ), - Type::TypeAlias(alias) => attribute_write_requirement(db, alias.value_type(db), attribute), + Type::TypeAlias(alias) => { + attribute_write_requirement(db, env, alias.value_type(db), attribute) + } Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Super) => { AttributeWriteRequirement::CannotAssign @@ -257,9 +264,9 @@ pub(super) fn attribute_write_requirement<'db>( Type::ProtocolInstance(protocol) => protocol .interface(db) - .instance_write_requirement(db, object_ty, attribute) + .instance_write_requirement(db, env, object_ty, attribute) .map_or_else( - || instance_attribute_write_requirement(db, object_ty, attribute), + || instance_attribute_write_requirement(db, env, object_ty, attribute), |(write, qualifiers)| AttributeWriteRequirement::ProtocolMember { write, qualifiers, @@ -286,13 +293,13 @@ pub(super) fn attribute_write_requirement<'db>( | Type::TypeForm(_) | Type::TypedDict(_) | Type::NewTypeInstance(_) => { - instance_attribute_write_requirement(db, object_ty, attribute) + instance_attribute_write_requirement(db, env, object_ty, attribute) } Type::SubclassOf(subclass_of) => subclass_of - .meta_write_requirement(db, attribute) + .meta_write_requirement(db, env, attribute) .map_or_else( - || class_attribute_write_requirement(db, object_ty, attribute), + || class_attribute_write_requirement(db, env, object_ty, attribute), |(write_ty, qualifiers)| AttributeWriteRequirement::ProtocolMember { write: write_ty.map(ProtocolMemberWriteRequirement::AssignableTo), qualifiers, @@ -300,18 +307,18 @@ pub(super) fn attribute_write_requirement<'db>( ), Type::ClassLiteral(..) | Type::GenericAlias(..) => { - class_attribute_write_requirement(db, object_ty, attribute) + class_attribute_write_requirement(db, env, object_ty, attribute) } Type::ModuleLiteral(module) => { - let symbol = if module - .module(db) + let resolved_module = module.module(db); + let symbol = if resolved_module .known(db) .is_some_and(KnownModule::is_builtins) { - builtins_symbol(db, attribute) + builtins_symbol(db, env, attribute) } else { - module.static_member(db, attribute) + module.static_member(db, env, attribute) }; AttributeWriteRequirement::Module(match symbol.place { Place::Defined(DefinedPlace { ty, .. }) => Some(ty), @@ -323,12 +330,13 @@ pub(super) fn attribute_write_requirement<'db>( fn instance_attribute_write_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> AttributeWriteRequirement<'db> { AttributeWriteRequirement::Instance { object_ty, - member: instance_attribute_write_member_requirement(db, object_ty, attribute), + member: instance_attribute_write_member_requirement(db, env, object_ty, attribute), } } @@ -339,10 +347,11 @@ fn instance_attribute_write_requirement<'db>( /// `__setattr__`. fn instance_attribute_write_member_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> InstanceAttributeWriteMember<'db> { - let Some(members) = assignment_attribute_members(db, object_ty, attribute) else { + let Some(members) = assignment_attribute_members(db, env, object_ty, attribute) else { return InstanceAttributeWriteMember::SetAttr; }; let (type_member, receiver_fallback) = match members { @@ -352,7 +361,7 @@ fn instance_attribute_write_member_requirement<'db>( } => (member, receiver_fallback), AssignmentAttributeMembers::ReceiverMember(member) => { return InstanceAttributeWriteMember::Instance(instance_fallback_write_requirement( - db, object_ty, attribute, member, + db, env, object_ty, attribute, member, )); } }; @@ -365,13 +374,14 @@ fn instance_attribute_write_member_requirement<'db>( } => InstanceAttributeWriteMember::Explicit { member: explicit_attribute_write_requirement( db, + env, object_ty, attribute, - ty.bind_self_typevars(db, object_ty), + ty.bind_self_typevars(db, env, object_ty), qualifiers, ), fallback: receiver_fallback.map(|fallback| { - instance_fallback_write_requirement(db, object_ty, attribute, fallback) + instance_fallback_write_requirement(db, env, object_ty, attribute, fallback) }), }, PlaceAndQualifiers { @@ -384,7 +394,7 @@ fn instance_attribute_write_member_requirement<'db>( .. }, ) => InstanceAttributeWriteMember::Instance(instance_fallback_write_requirement( - db, object_ty, attribute, fallback, + db, env, object_ty, attribute, fallback, )), _ => InstanceAttributeWriteMember::SetAttr, }, @@ -397,13 +407,14 @@ fn instance_attribute_write_member_requirement<'db>( /// declarations can be bound consistently with normal class-object member lookup. fn class_attribute_write_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> AttributeWriteRequirement<'db> { - let Some(members) = assignment_attribute_members(db, object_ty, attribute) else { + let Some(members) = assignment_attribute_members(db, env, object_ty, attribute) else { return AttributeWriteRequirement::Unconstrained; }; - let Some(class_attr_self_ty) = object_ty.to_instance_approximation(db) else { + let Some(class_attr_self_ty) = object_ty.to_instance_approximation(db, env) else { return AttributeWriteRequirement::Unconstrained; }; let (type_member, receiver_fallback) = match members { @@ -415,7 +426,13 @@ fn class_attribute_write_requirement<'db>( return AttributeWriteRequirement::Class { object_ty, member: ClassAttributeWriteMember::ClassAttribute( - class_fallback_write_requirement(db, object_ty, class_attr_self_ty, member), + class_fallback_write_requirement( + db, + env, + object_ty, + class_attr_self_ty, + member, + ), ), }; } @@ -426,9 +443,11 @@ fn class_attribute_write_requirement<'db>( place: Place::Defined(DefinedPlace { ty, .. }), qualifiers, } => ClassAttributeWriteMember::Explicit { - member: explicit_attribute_write_requirement(db, object_ty, attribute, ty, qualifiers), + member: explicit_attribute_write_requirement( + db, env, object_ty, attribute, ty, qualifiers, + ), fallback: receiver_fallback.map(|fallback| { - class_fallback_write_requirement(db, object_ty, class_attr_self_ty, fallback) + class_fallback_write_requirement(db, env, object_ty, class_attr_self_ty, fallback) }), }, PlaceAndQualifiers { @@ -442,13 +461,14 @@ fn class_attribute_write_requirement<'db>( }, ) => ClassAttributeWriteMember::ClassAttribute(class_fallback_write_requirement( db, + env, object_ty, class_attr_self_ty, fallback, )), _ => ClassAttributeWriteMember::Unresolved { has_instance_attribute: !class_attr_self_ty - .instance_member(db, attribute) + .instance_member(db, env, attribute) .place .is_undefined(), }, @@ -465,13 +485,14 @@ fn class_attribute_write_requirement<'db>( /// ordinary attribute to be treated as a data descriptor. fn explicit_attribute_write_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, attr_ty: Type<'db>, qualifiers: TypeQualifiers, ) -> ExplicitAttributeWriteRequirement<'db> { if let Place::Defined(DefinedPlace { ty: setter_ty, .. }) = attr_ty - .class_member_with_policy(db, "__set__", MemberLookupPolicy::REQUIRE_CONCRETE) + .class_member_with_policy(db, env, "__set__", MemberLookupPolicy::REQUIRE_CONCRETE) .place { ExplicitAttributeWriteRequirement::Descriptor { @@ -481,7 +502,7 @@ fn explicit_attribute_write_requirement<'db>( } } else { ExplicitAttributeWriteRequirement::AssignableTo { - ty: effective_write_type(db, object_ty, attribute, attr_ty), + ty: effective_write_type(db, env, object_ty, attribute, attr_ty), qualifiers, } } @@ -493,6 +514,7 @@ fn explicit_attribute_write_requirement<'db>( /// assignment diagnostic layer. fn instance_fallback_write_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, fallback: PlaceAndQualifiers<'db>, @@ -506,9 +528,9 @@ fn instance_fallback_write_requirement<'db>( else { return FallbackAttributeWriteRequirement::PossiblyMissing; }; - let ty = ty.bind_self_typevars(db, object_ty); + let ty = ty.bind_self_typevars(db, env, object_ty); FallbackAttributeWriteRequirement::AssignableTo { - ty: effective_write_type(db, object_ty, attribute, ty), + ty: effective_write_type(db, env, object_ty, attribute, ty), qualifiers, possibly_missing: definedness == Definedness::PossiblyUndefined, } @@ -517,6 +539,7 @@ fn instance_fallback_write_requirement<'db>( /// Convert a class-attribute fallback into a write type, binding `Self` to the class instance. fn class_fallback_write_requirement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, class_attr_self_ty: Type<'db>, fallback: PlaceAndQualifiers<'db>, @@ -530,7 +553,7 @@ fn class_fallback_write_requirement<'db>( else { return FallbackAttributeWriteRequirement::PossiblyMissing; }; - let ty = ty.bind_self_typevars(db, class_attr_self_ty); + let ty = ty.bind_self_typevars(db, env, class_attr_self_ty); let ty = if matches!(object_ty, Type::ClassLiteral(_)) && let Type::FunctionLiteral(function) = ty && function.callable_type_kind(db) == CallableTypeKind::FunctionLike @@ -553,13 +576,14 @@ fn class_fallback_write_requirement<'db>( /// `(str) -> int` converter is read as `int` but accepts `str` assignments. fn effective_write_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, attr_ty: Type<'db>, ) -> Type<'db> { if let Type::NominalInstance(instance) = object_ty && let Some(converter_ty) = instance - .class(db) + .class(db, env) .converter_input_type_for_field(db, attribute) { converter_ty @@ -586,15 +610,16 @@ fn effective_write_type<'db>( /// ``` pub(super) fn property_setter_returns_never<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, property_ty: Type<'db>, object_ty: Type<'db>, value_ty: Type<'db>, ) -> bool { property_ty.as_property_instance().is_some_and(|property| { property.setter(db).is_some_and(|setter| { - match setter.try_call(db, &CallArguments::positional([object_ty, value_ty])) { - Ok(result) => result.return_type(db).is_never(), - Err(error) => error.return_type(db).is_never(), + match setter.try_call(db, env, &CallArguments::positional([object_ty, value_ty])) { + Ok(result) => result.return_type(db, env).is_never(), + Err(error) => error.return_type(db, env).is_never(), } }) }) @@ -603,6 +628,7 @@ pub(super) fn property_setter_returns_never<'db>( /// Return the class member that takes precedence over a definitely non-data metaclass member. fn class_member_preceding_non_data_metaclass_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, type_member: PlaceAndQualifiers<'db>, @@ -613,13 +639,13 @@ fn class_member_preceding_non_data_metaclass_member<'db>( ) || !type_member .place .ignore_possibly_undefined()? - .is_definitely_non_data_descriptor(db) + .is_definitely_non_data_descriptor(db, env) { return None; } object_ty - .find_name_in_mro_with_policy(db, attribute, MemberLookupPolicy::default()) + .find_name_in_mro_with_policy(db, env, attribute, MemberLookupPolicy::default()) .filter(|class_attr| !class_attr.place.is_undefined()) } @@ -635,6 +661,7 @@ fn class_member_preceding_non_data_metaclass_member<'db>( /// protocol compatibility, and `Final` validation share exactly the same lookup precedence. pub(super) fn assignment_attribute_members<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, ) -> Option> { @@ -645,16 +672,16 @@ pub(super) fn assignment_attribute_members<'db>( object_ty, Type::KnownInstance(KnownInstanceType::FunctoolsPartial(_)) ) { - object_ty.member(db, attribute) + object_ty.member(db, env, attribute) } else if let Type::ProtocolInstance(protocol) = object_ty && let Some(origin) = protocol.materialized_origin_property(db, attribute) { - Type::instance(db, *origin).class_member(db, attribute) + Type::instance(db, env, *origin).class_member(db, env, attribute) } else { - object_ty.class_member(db, attribute) + object_ty.class_member(db, env, attribute) }; if let Some(receiver_member) = - class_member_preceding_non_data_metaclass_member(db, object_ty, attribute, type_member) + class_member_preceding_non_data_metaclass_member(db, env, object_ty, attribute, type_member) { return Some(AssignmentAttributeMembers::ReceiverMember(receiver_member)); } @@ -688,9 +715,9 @@ pub(super) fn assignment_attribute_members<'db>( | Type::TypeGuard(_) | Type::TypeForm(_) | Type::TypedDict(_) - | Type::NewTypeInstance(_) => object_ty.instance_member(db, attribute), + | Type::NewTypeInstance(_) => object_ty.instance_member(db, env, attribute), Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { - object_ty.class_object_member(db, attribute, MemberLookupPolicy::default()) + object_ty.class_object_member(db, env, attribute, MemberLookupPolicy::default()) } Type::Union(..) | Type::Intersection(..) diff --git a/crates/ty_python_semantic/src/types/bool.rs b/crates/ty_python_semantic/src/types/bool.rs index aa0efd8ce0..557fb9f91c 100644 --- a/crates/ty_python_semantic/src/types/bool.rs +++ b/crates/ty_python_semantic/src/types/bool.rs @@ -1,14 +1,13 @@ +use crate::Db; +use crate::ProgramEnvironment; use ruff_db::diagnostic::{Annotation, SubDiagnostic, SubDiagnosticSeverity}; use ruff_text_size::{Ranged, TextRange}; -use crate::{ - Db, - types::{ - CallArguments, CallDunderError, ClassType, CycleDetector, KnownClass, KnownInstanceType, - LiteralValueTypeKind, SubclassOfInner, Type, TypeContext, TypeVarBoundOrConstraints, - UnionType, call::CallErrorKind, constraints::ConstraintSetBuilder, context::InferContext, - diagnostic::UNSUPPORTED_BOOL_CONVERSION, typed_dict::TypedDictField, - }, +use crate::types::{ + CallArguments, CallDunderError, ClassType, CycleDetector, KnownClass, KnownInstanceType, + LiteralValueTypeKind, SubclassOfInner, Type, TypeContext, TypeVarBoundOrConstraints, UnionType, + call::CallErrorKind, constraints::ConstraintSetBuilder, context::InferContext, + diagnostic::UNSUPPORTED_BOOL_CONVERSION, typed_dict::TypedDictField, }; use ty_python_core::Truthiness; @@ -18,9 +17,14 @@ impl<'db> Type<'db> { /// This method should only be used outside type checking or when evaluating if a type /// is truthy or falsy in a context where Python doesn't make an implicit `bool` call. /// Use [`try_bool`](Self::try_bool) for type checking or implicit `bool` calls. - pub(crate) fn bool(&self, db: &'db dyn Db) -> Truthiness { - self.try_bool_impl(db, true, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous))) - .unwrap_or_else(|err| err.fallback_truthiness()) + pub(crate) fn bool(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Truthiness { + self.try_bool_impl( + db, + env, + true, + &TryBoolVisitor::new(Ok(Truthiness::Ambiguous)), + ) + .unwrap_or_else(|err| err.fallback_truthiness()) } /// Resolves the boolean value of a type. @@ -29,8 +33,17 @@ impl<'db> Type<'db> { /// when `bool(x)` is called on an object `x`. /// /// Returns an error if the type doesn't implement `__bool__` correctly. - pub(crate) fn try_bool(&self, db: &'db dyn Db) -> Result> { - self.try_bool_impl(db, false, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous))) + pub(crate) fn try_bool( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Result> { + self.try_bool_impl( + db, + env, + false, + &TryBoolVisitor::new(Ok(Truthiness::Ambiguous)), + ) } /// Resolves the boolean value of a type. @@ -48,6 +61,7 @@ impl<'db> Type<'db> { fn try_bool_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, allow_short_circuit: bool, visitor: &TryBoolVisitor<'db>, ) -> Result> { @@ -63,13 +77,15 @@ impl<'db> Type<'db> { let try_dunders = || { match self.try_call_dunder( db, + env, "__bool__", CallArguments::none(), TypeContext::default(), ) { Ok(outcome) => { - let return_type = outcome.return_type(db); - if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { + let return_type = outcome.return_type(db, env); + if !return_type.is_assignable_to(db, env, KnownClass::Bool.to_instance(db, env)) + { // The type has a `__bool__` method, but it doesn't return a // boolean. return Err(BoolError::IncorrectReturnType { @@ -83,12 +99,13 @@ impl<'db> Type<'db> { Err(CallDunderError::PossiblyUnbound { bindings: outcome, .. }) => { - let return_type = outcome.return_type(db); - if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) { + let return_type = outcome.return_type(db, env); + if !return_type.is_assignable_to(db, env, KnownClass::Bool.to_instance(db, env)) + { // The type has a `__bool__` method, but it doesn't return a // boolean. return Err(BoolError::IncorrectReturnType { - return_type: outcome.return_type(db), + return_type: outcome.return_type(db, env), not_boolable_type: *self, }); } @@ -103,7 +120,7 @@ impl<'db> Type<'db> { // handling for tuples here isn't sound. Err(CallDunderError::MethodNotAvailable) if let Type::NominalInstance(instance) = self - && let Some(tuple_spec) = instance.tuple_spec(db) => + && let Some(tuple_spec) = instance.tuple_spec(db, env) => { Ok(tuple_spec.truthiness()) } @@ -113,19 +130,22 @@ impl<'db> Type<'db> { // and a subclass could add a `__bool__` method. Err(CallDunderError::MethodNotAvailable) if let Type::NominalInstance(instance) = self - && instance.class(db).is_final(db) => + && instance.class(db, env).is_final(db) => { match self.try_call_dunder( db, + env, "__len__", CallArguments::none(), TypeContext::default(), ) { Ok(outcome) => { - let return_type = outcome.return_type(db); - if return_type - .is_assignable_to(db, KnownClass::SupportsIndex.to_instance(db)) - { + let return_type = outcome.return_type(db, env); + if return_type.is_assignable_to( + db, + env, + KnownClass::SupportsIndex.to_instance(db, env), + ) { Ok(type_to_truthiness(return_type)) } else { // TODO: should report a diagnostic similar to if return type of `__bool__` @@ -145,7 +165,7 @@ impl<'db> Type<'db> { Err(CallDunderError::CallError(CallErrorKind::BindingError, bindings, _)) => { Err(BoolError::IncorrectArguments { - truthiness: type_to_truthiness(bindings.return_type(db)), + truthiness: type_to_truthiness(bindings.return_type(db, env)), not_boolable_type: *self, }) } @@ -171,7 +191,7 @@ impl<'db> Type<'db> { for element in union.elements(db) { let element_truthiness = - match element.try_bool_impl(db, allow_short_circuit, visitor) { + match element.try_bool_impl(db, env, allow_short_circuit, visitor) { Ok(truthiness) => truthiness, Err(err) => { has_errors = true; @@ -228,8 +248,8 @@ impl<'db> Type<'db> { Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked_set)) => { let constraints = ConstraintSetBuilder::new(); - let tracked_set = constraints.load(db, tracked_set.constraints(db)); - Truthiness::from(tracked_set.is_always_satisfied(db)) + let tracked_set = constraints.load(db, env, tracked_set.constraints(db)); + Truthiness::from(tracked_set.is_always_satisfied(db, env)) } Type::KnownInstance(KnownInstanceType::Range { is_non_empty }) => { @@ -251,36 +271,40 @@ impl<'db> Type<'db> { Type::AlwaysFalsy => Truthiness::AlwaysFalse, - Type::ClassLiteral(class) => { - class - .metaclass_instance_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)? - } + Type::ClassLiteral(class) => class.metaclass_instance_type(db, env).try_bool_impl( + db, + env, + allow_short_circuit, + visitor, + )?, Type::GenericAlias(alias) => ClassType::from(*alias) - .metaclass_instance_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)?, + .metaclass_instance_type(db, env) + .try_bool_impl(db, env, allow_short_circuit, visitor)?, Type::SubclassOf(subclass_of_ty) => { - match subclass_of_ty.subclass_of().with_transposed_type_var(db) { + match subclass_of_ty + .subclass_of() + .with_transposed_type_var(db, env) + { SubclassOfInner::Dynamic(_) => Truthiness::Ambiguous, SubclassOfInner::Class(class) => { - Type::from(class).try_bool_impl(db, allow_short_circuit, visitor)? + Type::from(class).try_bool_impl(db, env, allow_short_circuit, visitor)? } SubclassOfInner::Protocol(_) => Truthiness::Ambiguous, SubclassOfInner::TypeVar(bound_typevar) => Type::TypeVar(bound_typevar) - .try_bool_impl(db, allow_short_circuit, visitor)?, + .try_bool_impl(db, env, allow_short_circuit, visitor)?, } } Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { None => Truthiness::Ambiguous, Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.try_bool_impl(db, allow_short_circuit, visitor)? + bound.try_bool_impl(db, env, allow_short_circuit, visitor)? } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .as_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)?, + .as_type(db, env) + .try_bool_impl(db, env, allow_short_circuit, visitor)?, } } @@ -295,8 +319,8 @@ impl<'db> Type<'db> { Type::Union(union) => try_union(*union)?, Type::Intersection(intersection) => { - if let Some(alternatives) = intersection.finite_alternative_union(db) { - alternatives.try_bool_impl(db, allow_short_circuit, visitor)? + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { + alternatives.try_bool_impl(db, env, allow_short_circuit, visitor)? } else { // TODO Truthiness::Ambiguous @@ -304,14 +328,14 @@ impl<'db> Type<'db> { } Type::EnumComplement(complement) => complement - .remaining_literal_union(db) - .try_bool_impl(db, allow_short_circuit, visitor)?, + .remaining_literal_union(db, env) + .try_bool_impl(db, env, allow_short_circuit, visitor)?, Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::LiteralString => Truthiness::Ambiguous, LiteralValueTypeKind::Enum(enum_type) => enum_type - .enum_class_instance(db) - .try_bool_impl(db, allow_short_circuit, visitor)?, + .enum_class_instance(db, env) + .try_bool_impl(db, env, allow_short_circuit, visitor)?, LiteralValueTypeKind::Int(num) => Truthiness::from(num.as_i64() != 0), LiteralValueTypeKind::Bool(bool) => Truthiness::from(bool), @@ -322,13 +346,14 @@ impl<'db> Type<'db> { Type::TypeAlias(alias) => visitor.visit(db, *self, || { alias .value_type(db) - .try_bool_impl(db, allow_short_circuit, visitor) + .try_bool_impl(db, env, allow_short_circuit, visitor) })?, - Type::NewTypeInstance(newtype) => { - newtype - .concrete_base_type(db) - .try_bool_impl(db, allow_short_circuit, visitor)? - } + Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db).try_bool_impl( + db, + env, + allow_short_circuit, + visitor, + )?, }; Ok(truthiness) @@ -403,24 +428,26 @@ impl<'db> BoolError<'db> { } fn report_diagnostic_impl(&self, context: &InferContext, condition: TextRange) { + let db = context.db(); let Some(builder) = context.report_lint(&UNSUPPORTED_BOOL_CONVERSION, condition) else { return; }; + let env = context.program_environment(); match self { Self::IncorrectArguments { not_boolable_type, .. } => { let mut diag = builder.into_diagnostic(format_args!( "Boolean conversion is not supported for type `{}`", - not_boolable_type.display(context.db()) + not_boolable_type.display(db, env) )); let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::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(context.db()) + .member(db, env, "__bool__") + .into_lookup_result(db, env) .ok() .and_then(|quals| quals.inner_type().parameter_span(context.db(), None)) { @@ -437,18 +464,18 @@ impl<'db> BoolError<'db> { } => { let mut diag = builder.into_diagnostic(format_args!( "Boolean conversion is not supported for type `{not_boolable}`", - not_boolable = not_boolable_type.display(context.db()), + not_boolable = not_boolable_type.display(db, env), )); let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( "`{return_type}` is not assignable to `bool`", - return_type = return_type.display(context.db()), + return_type = return_type.display(db, env), ), ); if let Some((func_span, return_type_span)) = not_boolable_type - .member(context.db(), "__bool__") - .into_lookup_result(context.db()) + .member(db, env, "__bool__") + .into_lookup_result(db, env) .ok() .and_then(|quals| quals.inner_type().function_spans(context.db())) .and_then(|spans| Some((spans.name, spans.return_type?))) @@ -463,13 +490,13 @@ impl<'db> BoolError<'db> { Self::NotCallable { not_boolable_type } => { let mut diag = builder.into_diagnostic(format_args!( "Boolean conversion is not supported for type `{}`", - not_boolable_type.display(context.db()) + not_boolable_type.display(db, env) )); let sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( "`__bool__` on `{}` must be callable", - not_boolable_type.display(context.db()) + not_boolable_type.display(db, env) ), ); // TODO: It would be nice to create an annotation here for @@ -481,14 +508,14 @@ impl<'db> BoolError<'db> { let first_error = union .elements(context.db()) .iter() - .find_map(|element| element.try_bool(context.db()).err()) + .find_map(|element| element.try_bool(db, env).err()) .unwrap(); builder.into_diagnostic(format_args!( "Boolean conversion is not supported for union `{}` \ because `{}` doesn't implement `__bool__` correctly", - Type::Union(*union).display(context.db()), - first_error.not_boolable_type().display(context.db()), + Type::Union(*union).display(db, env), + first_error.not_boolable_type().display(db, env), )); } @@ -496,7 +523,7 @@ impl<'db> BoolError<'db> { builder.into_diagnostic(format_args!( "Boolean conversion is not supported for type `{}`; \ it incorrectly implements `__bool__`", - not_boolable_type.display(context.db()) + not_boolable_type.display(db, env) )); } } diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index a6490c9fab..584433ff51 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -1,5 +1,6 @@ //! Logic for inferring `super()`, `super(x)` and `super(x, y)` calls. +use crate::ProgramEnvironment; use itertools::{Either, Itertools}; use ruff_db::diagnostic::Diagnostic; use ruff_python_ast::{AnyNodeRef, name::Name}; @@ -35,25 +36,30 @@ impl<'db> TypeVarOwnerContext<'db> { } } - fn has_implicit_upper_bound(self, db: &'db dyn Db) -> bool { - self.typevar(db).bound_or_constraints(db).is_none() + fn has_implicit_upper_bound(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + self.typevar(db).bound_or_constraints(db, env).is_none() } /// The bound or constraints of this typevar, as a type (i.e. constraints are unioned), wrapped /// in `SubclassOf` if this is a `SubclassOf` context. `object` if no bound/constraints. /// Used for error messages. - fn bound_or_constraints_type(self, db: &'db dyn Db) -> Type<'db> { + fn bound_or_constraints_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { TypeVarOwnerContext::Bare(typevar) => typevar .typevar(db) - .require_bound_or_constraints(db) - .as_type(db), + .require_bound_or_constraints(db, env) + .as_type(db, env), TypeVarOwnerContext::SubclassOf(typevar) => SubclassOfType::try_from_instance( db, + env, typevar .typevar(db) - .require_bound_or_constraints(db) - .as_type(db), + .require_bound_or_constraints(db, env) + .as_type(db, env), ) .unwrap_or_else(SubclassOfType::subclass_of_unknown), } @@ -91,6 +97,7 @@ pub(crate) enum BoundSuperError<'db> { impl<'db> BoundSuperError<'db> { pub(super) fn report_diagnostic(&self, context: &'db InferContext<'db, '_>, node: AnyNodeRef) { + let db = context.db(); match self { BoundSuperError::AbstractOwnerType { owner_type, @@ -98,31 +105,33 @@ impl<'db> BoundSuperError<'db> { typevar_context, } => { if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { + let env = context.program_environment(); if let Some(typevar_context) = typevar_context { let mut diagnostic = builder.into_diagnostic(format_args!( "`{owner}` is a type variable with an abstract/structural type as \ its bounds or constraints, in `super({pivot_class}, {owner})` call", - pivot_class = pivot_class.display(context.db()), - owner = owner_type.display(context.db()), + pivot_class = pivot_class.display(db, env), + owner = owner_type.display(db, env), )); - Self::describe_typevar(context.db(), &mut diagnostic, *typevar_context); + Self::describe_typevar(db, env, &mut diagnostic, *typevar_context); } else { builder.into_diagnostic(format_args!( "`{owner}` is an abstract/structural type in \ `super({pivot_class}, {owner})` call", - pivot_class = pivot_class.display(context.db()), - owner = owner_type.display(context.db()), + pivot_class = pivot_class.display(db, env), + owner = owner_type.display(db, env), )); } } } BoundSuperError::InvalidPivotClassType { pivot_class } => { if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { + let env = context.program_environment(); match pivot_class { Type::GenericAlias(alias) => { builder.into_diagnostic(format_args!( "`types.GenericAlias` instance `{}` is not a valid class", - alias.display_with(context.db(), DisplaySettings::default()), + alias.display_with(db, env, DisplaySettings::default(),), )); } _ => { @@ -130,11 +139,11 @@ impl<'db> BoundSuperError<'db> { builder.into_diagnostic("Argument is not a valid class"); diagnostic.set_primary_annotation_message(format_args!( "Argument has type `{}`", - pivot_class.display(context.db()) + pivot_class.display(db, env) )); diagnostic.set_concise_message(format_args!( "`{}` is not a valid class", - pivot_class.display(context.db()), + pivot_class.display(db, env), )); } } @@ -146,22 +155,23 @@ impl<'db> BoundSuperError<'db> { typevar_context, } => { if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { + let env = context.program_environment(); let mut diagnostic = 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()), + pivot_class = pivot_class.display(db, env), + owner = owner.display(db, env), )); if let Some(typevar_context) = typevar_context { - Self::describe_typevar(context.db(), &mut diagnostic, *typevar_context); + Self::describe_typevar(db, env, &mut diagnostic, *typevar_context); diagnostic.info(format_args!( "`{bounds_or_constraints}` is not an instance or subclass of `{pivot_class}`", bounds_or_constraints = - typevar_context.bound_or_constraints_type(context.db()).display(context.db()), - pivot_class = pivot_class.display(context.db()), + typevar_context.bound_or_constraints_type(db, env).display(db, env), + pivot_class = pivot_class.display(db, env), )); let typevar = typevar_context.typevar(context.db()); - if typevar_context.has_implicit_upper_bound(context.db()) { + if typevar_context.has_implicit_upper_bound(db, env) { diagnostic.help(format_args!( "Consider adding an upper bound to type variable `{}`", typevar.name(context.db()) @@ -186,11 +196,12 @@ impl<'db> BoundSuperError<'db> { /// and return the type variable's upper bound or the union of its constraints. fn describe_typevar( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, diagnostic: &mut Diagnostic, type_var_context: TypeVarOwnerContext<'db>, ) -> Type<'db> { let type_var = type_var_context.typevar(db); - match type_var_context.typevar(db).bound_or_constraints(db) { + match type_var_context.typevar(db).bound_or_constraints(db, env) { None => { diagnostic.info(format_args!( "Type variable `{}` has `object` as its implicit upper bound", @@ -202,7 +213,7 @@ impl<'db> BoundSuperError<'db> { diagnostic.info(format_args!( "Type variable `{}` has upper bound `{}`", type_var.name(db), - bound.display(db) + bound.display(db, env) )); bound } @@ -213,10 +224,10 @@ impl<'db> BoundSuperError<'db> { constraints .elements(db) .iter() - .map(|c| c.display(db)) + .map(|c| c.display(db, env)) .join(", ") )); - constraints.as_type(db) + constraints.as_type(db, env) } } } @@ -262,25 +273,30 @@ impl<'db> ResolvedSuperOwner<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self { owner_type: self .owner_type - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, lookup_anchor: self .lookup_anchor - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, receiver: self.receiver, }) } - fn descriptor_binding(&self, db: &'db dyn Db) -> (Option>, Type<'db>) { + fn descriptor_binding( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> (Option>, Type<'db>) { match self.receiver { DescriptorReceiverKind::Class => (None, self.owner_type), DescriptorReceiverKind::Instance => { - (Some(self.owner_type), self.owner_type.to_meta_type(db)) + (Some(self.owner_type), self.owner_type.to_meta_type(db, env)) } } } @@ -297,6 +313,7 @@ impl<'db> SuperOwnerKind<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -306,18 +323,22 @@ impl<'db> SuperOwnerKind<'db> { } SuperOwnerKind::Divergent(_) => Some(*self), SuperOwnerKind::Resolved(resolved_owner) => Some(SuperOwnerKind::Resolved( - resolved_owner.recursive_type_normalized_impl(db, div, nested)?, + resolved_owner.recursive_type_normalized_impl(db, env, div, nested)?, )), } } - fn iter_mro(&self, db: &'db dyn Db) -> impl Iterator> + Clone { + fn iter_mro( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> impl Iterator> + Clone { match self { SuperOwnerKind::Dynamic(dynamic) => { - Either::Left(ClassBase::Dynamic(*dynamic).mro(db, None)) + Either::Left(ClassBase::Dynamic(*dynamic).mro(db, env, None)) } SuperOwnerKind::Divergent(divergent) => { - Either::Left(ClassBase::Divergent(*divergent).mro(db, None)) + Either::Left(ClassBase::Divergent(*divergent).mro(db, env, None)) } SuperOwnerKind::Resolved(resolved_owner) => { Either::Right(resolved_owner.lookup_anchor.iter_mro(db)) @@ -334,10 +355,16 @@ impl<'db> SuperOwnerKind<'db> { } } - fn descriptor_binding(self, db: &'db dyn Db) -> Option<(Option>, Type<'db>)> { + fn descriptor_binding( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option<(Option>, Type<'db>)> { match self { SuperOwnerKind::Dynamic(_) | SuperOwnerKind::Divergent(_) => None, - SuperOwnerKind::Resolved(resolved_owner) => Some(resolved_owner.descriptor_binding(db)), + SuperOwnerKind::Resolved(resolved_owner) => { + Some(resolved_owner.descriptor_binding(db, env)) + } } } } @@ -361,8 +388,12 @@ pub(super) fn walk_bound_super_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( ) { visitor.visit_type(db, Type::from(bound_super.pivot_class(db))); match bound_super.owner(db) { - SuperOwnerKind::Dynamic(dynamic) => visitor.visit_type(db, Type::Dynamic(dynamic)), - SuperOwnerKind::Divergent(divergent) => visitor.visit_type(db, Type::Divergent(divergent)), + SuperOwnerKind::Dynamic(dynamic) => { + visitor.visit_type(db, Type::Dynamic(dynamic)); + } + SuperOwnerKind::Divergent(divergent) => { + visitor.visit_type(db, Type::Divergent(divergent)); + } SuperOwnerKind::Resolved(resolved_owner) => { visitor.visit_type(db, resolved_owner.owner_type); visitor.visit_type(db, Type::from(resolved_owner.lookup_anchor)); @@ -486,11 +517,13 @@ impl<'db> BoundSuperType<'db> { /// However, the checking is skipped when any of the arguments is a dynamic type. pub(super) fn build( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, pivot_class_type: Type<'db>, owner_type: Type<'db>, ) -> Result, BoundSuperError<'db>> { - let delegate_to = - |type_to_delegate_to| BoundSuperType::build(db, pivot_class_type, type_to_delegate_to); + let delegate_to = |type_to_delegate_to| { + BoundSuperType::build(db, env, pivot_class_type, type_to_delegate_to) + }; // Delegate but rewrite errors to preserve TypeVar context. let delegate_with_error_mapped = @@ -532,7 +565,7 @@ impl<'db> BoundSuperType<'db> { Type::ClassLiteral(class) => ClassBase::Class(ClassType::NonGeneric(class)), Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { SubclassOfInner::Dynamic(dynamic) => ClassBase::Dynamic(dynamic), - _ => match subclass_of.subclass_of().into_class(db) { + _ => match subclass_of.subclass_of().into_class(db, env) { Some(class) => ClassBase::Class(class), None => { return Err(BoundSuperError::InvalidPivotClassType { @@ -558,10 +591,10 @@ impl<'db> BoundSuperType<'db> { let build_constrained_union = |constraints: TypeVarConstraints<'db>, typevar: TypeVarOwnerContext<'db>| -> Result, BoundSuperError<'db>> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for constraint in constraints.elements(db) { let class = match constraint { - Type::NominalInstance(instance) => Some(instance.class(db)), + Type::NominalInstance(instance) => Some(instance.class(db, env)), _ => constraint.to_class_type(db), }; match class { @@ -632,10 +665,10 @@ impl<'db> BoundSuperType<'db> { SubclassOfInner::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic), SubclassOfInner::TypeVar(bound_typevar) => { let typevar = bound_typevar.typevar(db); - match typevar.bound_or_constraints(db) { + match typevar.bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let class = match bound { - Type::NominalInstance(instance) => Some(instance.class(db)), + Type::NominalInstance(instance) => Some(instance.class(db, env)), Type::ProtocolInstance(protocol) => { protocol.class_origin(db).map(|class| *class) } @@ -652,7 +685,7 @@ impl<'db> BoundSuperType<'db> { Some(TypeVarOwnerContext::SubclassOf(bound_typevar)), )?) } else { - let subclass_of = SubclassOfType::try_from_instance(db, bound) + let subclass_of = SubclassOfType::try_from_instance(db, env, bound) .unwrap_or_else(SubclassOfType::subclass_of_unknown); return delegate_with_error_mapped( subclass_of, @@ -674,7 +707,7 @@ impl<'db> BoundSuperType<'db> { pivot_class_type, owner_type, owner_type, - ClassType::object(db), + ClassType::object(db, env), Some(TypeVarOwnerContext::SubclassOf(bound_typevar)), )?) } @@ -687,7 +720,7 @@ impl<'db> BoundSuperType<'db> { pivot_class, pivot_class_type, owner_type, - instance.class(db), + instance.class(db, env), None, )?) } @@ -715,13 +748,13 @@ impl<'db> BoundSuperType<'db> { return Ok(union .elements(db) .iter() - .try_fold(UnionBuilder::new(db), |builder, element| { + .try_fold(UnionBuilder::new(db, env), |builder, element| { delegate_to(*element).map(|ty| builder.add(ty)) })? .build()); } Type::Intersection(intersection) => { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); let mut one_good_element_found = false; for positive in intersection.positive(db) { if let Ok(good_element) = delegate_to(*positive) { @@ -744,17 +777,17 @@ impl<'db> BoundSuperType<'db> { return Ok(builder.build()); } Type::EnumComplement(complement) => { - return delegate_to(complement.to_intersection(db)); + return delegate_to(complement.to_intersection(db, env)); } Type::TypeAlias(alias) => { return delegate_to(alias.value_type(db)); } Type::TypeVar(bound_typevar) => { let typevar = bound_typevar.typevar(db); - match typevar.bound_or_constraints(db) { + match typevar.bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { let class = match bound { - Type::NominalInstance(instance) => Some(instance.class(db)), + Type::NominalInstance(instance) => Some(instance.class(db, env)), Type::ProtocolInstance(protocol) => { protocol.class_origin(db).map(|class| *class) } @@ -789,59 +822,68 @@ impl<'db> BoundSuperType<'db> { pivot_class, pivot_class_type, owner_type, - ClassType::object(db), + ClassType::object(db, env), Some(TypeVarOwnerContext::Bare(bound_typevar)), )?) } } } Type::TypeIs(_) | Type::TypeGuard(_) => { - return delegate_to(KnownClass::Bool.to_instance(db)); + return delegate_to(KnownClass::Bool.to_instance(db, env)); + } + Type::LiteralValue(literal) => { + return delegate_to(literal.fallback_instance(db, env)); } - Type::LiteralValue(literal) => return delegate_to(literal.fallback_instance(db)), Type::SpecialForm(special_form) => { - return delegate_to(special_form.instance_fallback(db)); + return delegate_to(special_form.instance_fallback(db, env)); } Type::KnownInstance(instance) => { - return delegate_to(instance.instance_fallback(db)); + return delegate_to(instance.instance_fallback(db, env)); } Type::FunctionLiteral(_) | Type::DataclassDecorator(_) => { - return delegate_to(KnownClass::FunctionType.to_instance(db)); + return delegate_to(KnownClass::FunctionType.to_instance(db, env)); } Type::WrapperDescriptor(_) => { - return delegate_to(KnownClass::WrapperDescriptorType.to_instance(db)); + return delegate_to(KnownClass::WrapperDescriptorType.to_instance(db, env)); } Type::KnownBoundMethod(method) => { - return delegate_to(method.class().to_instance(db)); + return delegate_to(method.class().to_instance(db, env)); + } + Type::BoundMethod(_) => { + return delegate_to(KnownClass::MethodType.to_instance(db, env)); } - Type::BoundMethod(_) => return delegate_to(KnownClass::MethodType.to_instance(db)), Type::ModuleLiteral(_) => { - return delegate_to(KnownClass::ModuleType.to_instance(db)); + return delegate_to(KnownClass::ModuleType.to_instance(db, env)); + } + Type::GenericAlias(_) => { + return delegate_to(KnownClass::GenericAlias.to_instance(db, env)); } - Type::GenericAlias(_) => return delegate_to(KnownClass::GenericAlias.to_instance(db)), Type::PropertyInstance(property) => { - return delegate_to(property.instance_fallback(db)); + return delegate_to(property.instance_fallback(db, env)); + } + Type::BoundSuper(_) => { + return delegate_to(KnownClass::Super.to_instance(db, env)); } - Type::BoundSuper(_) => return delegate_to(KnownClass::Super.to_instance(db)), Type::TypedDict(td) => { // In general it isn't sound to upcast a `TypedDict` to a `dict`, // but here it seems like it's probably sound? - let mut key_builder = UnionBuilder::new(db); - let mut value_builder = UnionBuilder::new(db); + let mut key_builder = UnionBuilder::new(db, env); + let mut value_builder = UnionBuilder::new(db, env); for (name, field) in td.items(db) { key_builder = key_builder.add(Type::string_literal(db, name)); value_builder = value_builder.add(field.declared_ty); } - return delegate_to( - KnownClass::Dict - .to_specialized_instance(db, &[key_builder.build(), value_builder.build()]), - ); + return delegate_to(KnownClass::Dict.to_specialized_instance( + db, + env, + &[key_builder.build(), value_builder.build()], + )); } Type::NewTypeInstance(newtype) => { return delegate_to(newtype.concrete_base_type(db)); } Type::Callable(callable) if callable.is_function_like(db) => { - return delegate_to(KnownClass::FunctionType.to_instance(db)); + return delegate_to(KnownClass::FunctionType.to_instance(db, env)); } Type::AlwaysFalsy | Type::AlwaysTruthy @@ -866,10 +908,11 @@ impl<'db> BoundSuperType<'db> { fn skip_until_after_pivot( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mro_iter: impl Iterator> + Clone, ) -> impl Iterator> + Clone { let Some(pivot_class) = self.pivot_class(db).into_class() else { - return Either::Left(ClassBase::Dynamic(DynamicType::Unknown).mro(db, None)); + return Either::Left(ClassBase::Dynamic(DynamicType::Unknown).mro(db, env, None)); }; let mut pivot_found = false; @@ -895,10 +938,11 @@ impl<'db> BoundSuperType<'db> { pub(super) fn try_call_dunder_get_on_attribute( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, attribute: PlaceAndQualifiers<'db>, ) -> Option> { - let (instance, owner) = self.owner(db).descriptor_binding(db)?; - Some(Type::try_call_dunder_get_on_attribute(db, attribute, instance, owner).0) + let (instance, owner) = self.owner(db).descriptor_binding(db, env)?; + Some(Type::try_call_dunder_get_on_attribute(db, env, attribute, instance, owner).0) } /// Similar to `Type::find_name_in_mro_with_policy`, but performs lookup starting *after* the @@ -906,6 +950,7 @@ impl<'db> BoundSuperType<'db> { pub(super) fn find_name_in_mro_after_pivot( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { @@ -913,20 +958,21 @@ impl<'db> BoundSuperType<'db> { let class = match &owner { SuperOwnerKind::Dynamic(dynamic) => { return Type::Dynamic(*dynamic) - .find_name_in_mro_with_policy(db, name, policy) + .find_name_in_mro_with_policy(db, env, name, policy) .expect("Calling `find_name_in_mro` on dynamic type should return `Some`"); } SuperOwnerKind::Divergent(_) => { return Type::unknown() - .find_name_in_mro_with_policy(db, name, policy) + .find_name_in_mro_with_policy(db, env, name, policy) .expect("Calling `find_name_in_mro` on Unknown should return `Some`"); } SuperOwnerKind::Resolved(resolved_owner) => resolved_owner.lookup_anchor, }; - let mut mro_after_pivot = self.skip_until_after_pivot(db, owner.iter_mro(db)); + let mut mro_after_pivot = self.skip_until_after_pivot(db, env, owner.iter_mro(db, env)); let class_literal = class.class_literal(db); - let result = class_literal.class_member_from_mro(db, name, policy, mro_after_pivot.clone()); + let result = + class_literal.class_member_from_mro(db, env, name, policy, mro_after_pivot.clone()); // TODO: Here we are hard-coding that __class_getitem__ is the only member defined in // typing._Generic in the typeshed, and we are hard-coding its signature. Ideally we would @@ -951,15 +997,16 @@ impl<'db> BoundSuperType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self::new( db, self.pivot_class(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, self.owner(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, )) } } diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index b8e8638de1..e1bddfbbc0 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -4,6 +4,7 @@ use crate::Db; use crate::place::Provenance; use crate::types::call::bind::BindingError; use crate::types::{MemberLookupPolicy, PropertyInstanceType}; +use crate::{Program, ProgramEnvironment}; use ruff_python_ast as ast; mod arguments; @@ -29,11 +30,15 @@ enum ReflectedMethodPriority { /// /// This is intentionally conservative: a false negative only widens a binary operation's result, /// while a false positive could discard a valid normal-method result. -fn has_exact_runtime_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +fn has_exact_runtime_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { match ty { Type::ClassLiteral(_) | Type::LiteralValue(_) => true, - Type::NominalInstance(instance) => instance.class(db).is_final(db), - Type::TypeAlias(alias) => has_exact_runtime_class(db, alias.value_type(db)), + Type::NominalInstance(instance) => instance.class(db, env).is_final(db), + Type::TypeAlias(alias) => has_exact_runtime_class(db, env, alias.value_type(db)), _ => false, } } @@ -42,10 +47,14 @@ fn has_exact_runtime_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { /// /// Instances dispatch through their nominal class, while class objects dispatch through their /// metaclass. -fn operator_dispatch_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { +fn operator_dispatch_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { match ty { Type::ClassLiteral(class) => class.metaclass(db).to_class_type(db), - _ => ty.nominal_class(db), + _ => ty.nominal_class(db, env), } } @@ -65,6 +74,7 @@ fn operator_dispatch_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left_ty: Type<'db>, right_ty: Type<'db>, ) -> ReflectedMethodPriority { @@ -73,17 +83,17 @@ fn reflected_method_priority<'db>( } if let (Some(left_class), Some(right_class)) = ( - operator_dispatch_class(db, left_ty), - operator_dispatch_class(db, right_ty), + operator_dispatch_class(db, env, left_ty), + operator_dispatch_class(db, env, right_ty), ) && left_class.class_literal(db) != right_class.class_literal(db) && right_class.is_subtype_of_class_literal(db, left_class.class_literal(db)) { - if has_exact_runtime_class(db, left_ty) { + if has_exact_runtime_class(db, env, left_ty) { ReflectedMethodPriority::Definitely } else { ReflectedMethodPriority::Possibly } - } else if right_ty.is_subtype_of(db, left_ty) { + } else if right_ty.is_subtype_of(db, env, left_ty) { ReflectedMethodPriority::Possibly } else { ReflectedMethodPriority::Never @@ -98,6 +108,7 @@ impl<'db> Type<'db> { /// equality when neither comparison method is available. pub(super) fn try_call_rich_comparison_dunder( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, dunder: &'static str, @@ -108,16 +119,17 @@ impl<'db> Type<'db> { receiver .try_call_dunder_with_policy( db, + env, name, &mut CallArguments::positional([argument]), TypeContext::default(), policy, ) - .map(|outcome| outcome.return_type(db)) + .map(|outcome| outcome.return_type(db, env)) .ok() }; - match reflected_method_priority(db, left, right) { + match reflected_method_priority(db, env, left, right) { ReflectedMethodPriority::Never => call_dunder(dunder, left, right) .or_else(|| call_dunder(reflected_dunder, right, left)), ReflectedMethodPriority::Possibly => { @@ -126,7 +138,7 @@ impl<'db> Type<'db> { call_dunder(reflected_dunder, right, left), ) { (Some(normal), Some(reflected)) => { - Some(UnionType::from_two_elements(db, normal, reflected)) + Some(UnionType::from_two_elements(db, env, normal, reflected)) } (Some(result), None) | (None, Some(result)) => Some(result), (None, None) => None, @@ -141,36 +153,48 @@ impl<'db> Type<'db> { /// expressions don't re-run overload selection at every call site. pub(crate) fn try_call_bin_op_return_type( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left_ty: Type<'db>, op: ast::Operator, right_ty: Type<'db>, ) -> Option> { - #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] fn try_call_bin_op_return_type_impl<'db>( db: &'db dyn Db, + program: Program, left_ty: Type<'db>, op: ast::Operator, right_ty: Type<'db>, ) -> Option> { - Type::try_call_bin_op(db, left_ty, op, right_ty) + let env = &ProgramEnvironment::from_program(program); + Type::try_call_bin_op(db, env, left_ty, op, right_ty) .ok() - .map(|bindings| bindings.return_type(db)) + .map(|bindings| bindings.return_type(db, env)) } - try_call_bin_op_return_type_impl(db, left_ty, op, right_ty) + try_call_bin_op_return_type_impl(db, env.program(db), left_ty, op, right_ty) } pub(crate) fn try_call_bin_op( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left_ty: Type<'db>, op: ast::Operator, right_ty: Type<'db>, ) -> Result, CallBinOpError> { - Self::try_call_bin_op_with_policy(db, left_ty, op, right_ty, MemberLookupPolicy::default()) + Self::try_call_bin_op_with_policy( + db, + env, + left_ty, + op, + right_ty, + MemberLookupPolicy::default(), + ) } pub(crate) fn try_call_bin_op_with_policy( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left_ty: Type<'db>, op: ast::Operator, right_ty: Type<'db>, @@ -189,21 +213,23 @@ impl<'db> Type<'db> { // Runtime classes determine reflected priority, but static operand types may only // establish that priority conditionally. - let reflected_priority = reflected_method_priority(db, left_ty, right_ty); + let reflected_priority = reflected_method_priority(db, env, left_ty, right_ty); - let left_class = left_ty.to_meta_type(db); - let right_class = right_ty.to_meta_type(db); + let left_class = left_ty.to_meta_type(db, env); + let right_class = right_ty.to_meta_type(db, env); if reflected_priority != ReflectedMethodPriority::Never { let reflected_dunder = op.reflected_dunder(); - let rhs_reflected = right_class.member(db, reflected_dunder).place; + let rhs_reflected = right_class.member(db, env, reflected_dunder).place; // TODO: if `rhs_reflected` is possibly unbound, we should union the two possible // Bindings together if !rhs_reflected.is_undefined() - && !rhs_reflected - .is_equal_ignoring_provenance(left_class.member(db, reflected_dunder).place) + && !rhs_reflected.is_equal_ignoring_provenance( + left_class.member(db, env, reflected_dunder).place, + ) { let call_on_right_instance = right_ty.try_call_dunder_with_policy( db, + env, reflected_dunder, &mut CallArguments::positional([left_ty]), TypeContext::default(), @@ -214,6 +240,7 @@ impl<'db> Type<'db> { return Ok(call_on_right_instance.or_else(|_| { left_ty.try_call_dunder_with_policy( db, + env, op.dunder(), &mut CallArguments::positional([right_ty]), TypeContext::default(), @@ -224,6 +251,7 @@ impl<'db> Type<'db> { let call_on_left_instance = left_ty.try_call_dunder_with_policy( db, + env, op.dunder(), &mut CallArguments::positional([right_ty]), TypeContext::default(), @@ -234,6 +262,7 @@ impl<'db> Type<'db> { (Ok(right_bindings), Ok(left_bindings)) => { let callable_type = UnionType::from_two_elements( db, + env, right_bindings.callable_type(), left_bindings.callable_type(), ); @@ -250,6 +279,7 @@ impl<'db> Type<'db> { let call_on_left_instance = left_ty.try_call_dunder_with_policy( db, + env, op.dunder(), &mut CallArguments::positional([right_ty]), TypeContext::default(), @@ -262,6 +292,7 @@ impl<'db> Type<'db> { } else { Ok(right_ty.try_call_dunder_with_policy( db, + env, op.reflected_dunder(), &mut CallArguments::positional([left_ty]), TypeContext::default(), @@ -280,8 +311,8 @@ impl<'db> Type<'db> { pub(crate) struct CallError<'db>(pub(crate) CallErrorKind, pub(crate) Box>); impl<'db> CallError<'db> { - pub(crate) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { - self.1.return_type(db) + pub(crate) fn return_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.1.return_type(db, env) } /// Returns `Some(property)` if the call error was caused by an attempt to set a property @@ -388,16 +419,24 @@ impl<'db> CallDunderError<'db> { } } - pub(super) fn return_type(&self, db: &'db dyn Db) -> Option> { + pub(super) fn return_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { Self::MethodNotAvailable | Self::CallError(CallErrorKind::NotCallable, _, _) => None, - Self::CallError(_, bindings, _) => Some(bindings.return_type(db)), - Self::PossiblyUnbound { bindings, .. } => Some(bindings.return_type(db)), + Self::CallError(_, bindings, _) => Some(bindings.return_type(db, env)), + Self::PossiblyUnbound { bindings, .. } => Some(bindings.return_type(db, env)), } } - pub(super) fn fallback_return_type(&self, db: &'db dyn Db) -> Type<'db> { - self.return_type(db).unwrap_or(Type::unknown()) + pub(super) fn fallback_return_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.return_type(db, env).unwrap_or(Type::unknown()) } } diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index ab50eb6b46..ae4bea09c4 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -1,3 +1,4 @@ +use crate::Db; use std::borrow::Cow; use std::fmt::Display; @@ -5,7 +6,7 @@ use itertools::{Either, Itertools}; use ruff_python_ast as ast; use rustc_hash::FxHashMap; -use crate::Db; +use crate::ProgramEnvironment; use crate::types::enums::enum_metadata; use crate::types::tuple::Tuple; use crate::types::typed_dict::extract_unpacked_typed_dict_keys_from_value_type; @@ -291,6 +292,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { pub(crate) fn functools_partial_bound_arguments( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> Option<(Self, bool)> { let bound_call_arguments = self.start_from(1); let mut can_synthesize_signature = true; @@ -300,7 +302,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { match argument { Argument::Variadic => { if !matches!( - argument_ty.tuple_instance_spec(db), + argument_ty.tuple_instance_spec(db, env), Some(spec) if spec.as_fixed_length().is_some() ) { return None; @@ -310,7 +312,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { // Known `TypedDict` items can still be checked against their target // parameters, even though possible hidden items prevent us from synthesizing // a precise partial signature. - extract_unpacked_typed_dict_keys_from_value_type(db, argument_ty)?; + extract_unpacked_typed_dict_keys_from_value_type(db, env, argument_ty)?; can_synthesize_signature = false; } Argument::Positional | Argument::Synthetic | Argument::Keyword(_) => {} @@ -326,7 +328,11 @@ impl<'a, 'db> CallArguments<'a, 'db> { /// contains the same arguments, but with one or more of the argument types expanded. /// /// [argument type expansion]: https://typing.python.org/en/latest/spec/overload.html#argument-type-expansion - pub(super) fn expand(&self, db: &'db dyn Db) -> impl Iterator> + '_ { + pub(super) fn expand( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> impl Iterator> + '_ { /// Represents the state of the expansion process. enum State<'a, 'db> { LimitReached(usize), @@ -361,6 +367,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { } } + let env = env.clone(); let mut index = 0; std::iter::successors( @@ -382,7 +389,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { // this only shows up in very convoluted instances of generic call inference across multiple // overloads, and is unlikely to happen in practice. if let Some(arg_type) = arg_type.get_default() - && let Some(expanded_types) = expand_type(db, arg_type) + && let Some(expanded_types) = expand_type(db, &env, arg_type) { break expanded_types; } @@ -427,31 +434,38 @@ impl<'a, 'db> CallArguments<'a, 'db> { }) } - pub(super) fn display(&self, db: &'db dyn Db) -> impl Display { - struct DisplayCallArgumentTypes<'a, 'db> { + pub(super) fn display<'env>( + &'env self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> impl Display + 'env { + struct DisplayCallArgumentTypes<'env, 'a, 'db> { types: &'a CallArgumentTypes<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, } - impl std::fmt::Display for DisplayCallArgumentTypes<'_, '_> { + impl std::fmt::Display for DisplayCallArgumentTypes<'_, '_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; f.debug_map() .entries(self.types.iter().map(|(tcx, ty)| { ( - tcx.annotation.as_ref().map(|ty| ty.display(self.db)), - ty.display(self.db), + tcx.annotation.as_ref().map(|ty| ty.display(db, self.env)), + ty.display(db, self.env), ) })) .finish() } } - struct DisplayCallArguments<'a, 'db> { + struct DisplayCallArguments<'env, 'a, 'db> { call_arguments: &'a CallArguments<'a, 'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, } - impl std::fmt::Display for DisplayCallArguments<'_, '_> { + impl std::fmt::Display for DisplayCallArguments<'_, '_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("(")?; for (index, (argument, types)) in self.call_arguments.iter().enumerate() { @@ -463,23 +477,55 @@ impl<'a, 'db> CallArguments<'a, 'db> { write!( f, "self: {}", - DisplayCallArgumentTypes { types, db: self.db } + DisplayCallArgumentTypes { + types, + db: self.db, + env: self.env, + } )?; } Argument::Positional => { - write!(f, "{}", DisplayCallArgumentTypes { types, db: self.db })?; + write!( + f, + "{}", + DisplayCallArgumentTypes { + types, + db: self.db, + env: self.env, + } + )?; } Argument::Variadic => { - write!(f, "*{}", DisplayCallArgumentTypes { types, db: self.db })?; + write!( + f, + "*{}", + DisplayCallArgumentTypes { + types, + db: self.db, + env: self.env, + } + )?; } Argument::Keyword(name) => write!( f, "{}={}", name, - DisplayCallArgumentTypes { types, db: self.db } + DisplayCallArgumentTypes { + types, + db: self.db, + env: self.env, + } )?, Argument::Keywords => { - write!(f, "**{}", DisplayCallArgumentTypes { types, db: self.db })?; + write!( + f, + "**{}", + DisplayCallArgumentTypes { + types, + db: self.db, + env: self.env, + } + )?; } } } @@ -490,6 +536,7 @@ impl<'a, 'db> CallArguments<'a, 'db> { DisplayCallArguments { call_arguments: self, db, + env, } } } @@ -533,27 +580,31 @@ impl<'a, 'db> FromIterator<(Argument<'a>, Option>)> for CallArguments< /// Returns `true` if the type can be expanded into its subtypes. /// /// In other words, it returns `true` if [`expand_type`] returns [`Some`] for the given type. -pub(crate) fn is_expandable_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { +pub(crate) fn is_expandable_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> bool { match ty { Type::EnumComplement(_) => true, - Type::Intersection(intersection) => intersection.finite_alternatives(db).is_some(), + Type::Intersection(intersection) => intersection.finite_alternatives(db, env).is_some(), Type::NominalInstance(instance) => { - let class = instance.class(db); + let class = instance.class(db, env); if class.is_known(db, KnownClass::Bool) { return true; } - if let Some(tuple_spec) = instance.tuple_spec(db) + if let Some(tuple_spec) = instance.tuple_spec(db, env) && let Tuple::Fixed(fixed_length_tuple) = &*tuple_spec && fixed_length_tuple .iter_all_elements() - .any(|element| is_expandable_type(db, element)) + .any(|element| is_expandable_type(db, env, element)) { return true; } enum_metadata(db, class.class_literal(db)).is_some() } Type::Union(_) => true, - Type::TypeAlias(alias) => is_expandable_type(db, alias.value_type(db)), + Type::TypeAlias(alias) => is_expandable_type(db, env, alias.value_type(db)), _ => false, } } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 0ee60a7e0c..dfb7747062 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -30,6 +30,7 @@ use crate::dunder_all::dunder_all_names; use crate::lint::LintMetadata; use crate::place::{DefinedPlace, Definedness, Place}; use crate::subscript::PyIndex; +use crate::types::ProgramEnvironment; use crate::types::call::arguments::{CallArgumentTypes, Expansion, is_expandable_type}; use crate::types::callable::CallableTypeKind; use crate::types::constraints::{ @@ -72,7 +73,7 @@ use crate::types::{ TypeMapping, TypeVarBoundOrConstraints, TypeVarVariance, UnionAccumulator, UnionBuilder, UnionType, WrapperDescriptorKind, enums, list_members, }; -use crate::{DisplaySettings, FxOrderSet, Program}; +use crate::{DisplaySettings, FxOrderSet}; use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}; use ruff_python_ast::{self as ast, AnyNodeRef, ArgOrKeyword, PythonVersion}; use ty_python_core::semantic_index; @@ -99,11 +100,11 @@ struct CallDiagnosticContext<'context, 'overrides, 'db, 'ast> { } impl<'db> CallDiagnosticContext<'_, '_, 'db, '_> { - fn report_lint<'ctx, T: Ranged>( - &'ctx self, + fn report_lint<'env, T: Ranged>( + &'env self, lint: &'static LintMetadata, ranged: T, - ) -> Option> { + ) -> Option> { let lint = self.overrides.map_or(lint, |overrides| overrides.lint); self.context.report_lint(lint, ranged).map(|builder| { if let Some(overrides) = self.overrides { @@ -135,14 +136,16 @@ impl<'db, 'ast> std::ops::Deref for CallDiagnosticContext<'_, '_, 'db, 'ast> { fn generic_contexts_mentioned_in_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> FxOrderSet> { - struct GenericContextCollector<'db> { + struct GenericContextCollector<'a, 'db> { + env: &'a ProgramEnvironment<'db>, generic_contexts: RefCell>>, recursion_guard: TypeCollector<'db>, } - impl<'db> GenericContextCollector<'db> { + impl<'db> GenericContextCollector<'_, 'db> { fn visit_signature(&self, db: &'db dyn Db, signature: &Signature<'db>) { if let Some(generic_context) = signature.generic_context { self.generic_contexts.borrow_mut().insert(generic_context); @@ -157,7 +160,11 @@ fn generic_contexts_mentioned_in_type<'db>( } } - impl<'db> TypeVisitor<'db> for GenericContextCollector<'db> { + impl<'db> TypeVisitor<'db> for GenericContextCollector<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -181,6 +188,7 @@ fn generic_contexts_mentioned_in_type<'db>( } let collector = GenericContextCollector { + env, generic_contexts: RefCell::default(), recursion_guard: TypeCollector::default(), }; @@ -190,6 +198,7 @@ fn generic_contexts_mentioned_in_type<'db>( fn freshen_generic_contexts_in_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, generic_contexts: FxOrderSet>, nonce_generator: &TypeVarNonceGenerator<'db>, @@ -199,6 +208,7 @@ fn freshen_generic_contexts_in_type<'db>( .fold(ty, |ty, generic_context| { ty.apply_type_mapping( db, + env, &TypeMapping::FreshenBoundTypeVars { generic_context, delta: nonce_generator.next().value(), @@ -210,10 +220,11 @@ fn freshen_generic_contexts_in_type<'db>( fn inferable_typevars_from_tuple<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, instance: &NominalInstanceType<'db>, ) -> Option> { let typevars: Option> = instance - .tuple_spec(db)? + .tuple_spec(db, env)? .fixed_elements() .map(|ty| ty.as_typevar()) .collect(); @@ -264,16 +275,17 @@ impl<'db> CallableItem<'db> { } } - fn return_type(&self, db: &'db dyn Db) -> Type<'db> { + fn return_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { CallableItem::Regular(binding) => binding.return_type(), - CallableItem::Constructor(binding) => binding.return_type(db), + CallableItem::Constructor(binding) => binding.return_type(db, env), } } fn check_types( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, argument_types: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, @@ -281,18 +293,34 @@ impl<'db> CallableItem<'db> { ) { match self { CallableItem::Regular(binding) => { - binding.check_types(db, constraints, argument_types, call_expression_tcx); + binding.check_types(db, env, constraints, argument_types, call_expression_tcx); } CallableItem::Constructor(binding) => { - binding.check_types(db, constraints, argument_types, call_expression_tcx, mode); + binding.check_types( + db, + env, + constraints, + argument_types, + call_expression_tcx, + mode, + ); } } } - fn match_parameters(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { + fn match_parameters( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + arguments: &CallArguments<'_, 'db>, + ) { match self { - CallableItem::Regular(binding) => binding.match_parameters(db, arguments), - CallableItem::Constructor(binding) => binding.match_parameters(db, arguments), + CallableItem::Regular(binding) => { + binding.match_parameters(db, env, arguments); + } + CallableItem::Constructor(binding) => { + binding.match_parameters(db, env, arguments); + } } } @@ -319,11 +347,12 @@ impl<'db> CallableItem<'db> { fn freshen_generic_contexts_in_place( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, nonce_generator: &TypeVarNonceGenerator<'db>, ) { match self { CallableItem::Regular(binding) => { - binding.freshen_generic_contexts_in_place(db, nonce_generator); + binding.freshen_generic_contexts_in_place(db, env, nonce_generator); } // TODO: Constructor freshening also has to keep constructor instance context in sync // with `__new__`/`__init__` signatures. @@ -364,12 +393,14 @@ impl<'db> CallableItem<'db> { fn functools_partial_callable<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, partial_overload: &mut Binding<'db>, bound_call_arguments: &CallArguments<'a, 'db>, ) -> Option> { match self { CallableItem::Regular(binding) => CallableType::partially_apply( db, + env, binding.partial_signature_applications( db, partial_overload, @@ -435,14 +466,15 @@ impl<'db> BindingsElement<'db> { self.items.len() > 1 } - fn return_type(&self, db: &'db dyn Db) -> Type<'db> { + fn return_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { if self.is_callable() { IntersectionType::from_elements( db, + env, self.items .iter() .filter(|item| item.is_callable()) - .map(|item| item.return_type(db)), + .map(|item| item.return_type(db, env)), ) } else { Type::unknown() @@ -453,13 +485,21 @@ impl<'db> BindingsElement<'db> { fn check_types( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, call_arguments: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, mode: CheckTypesMode, ) { for item in &mut self.items { - item.check_types(db, constraints, call_arguments, call_expression_tcx, mode); + item.check_types( + db, + env, + constraints, + call_arguments, + call_expression_tcx, + mode, + ); } } @@ -953,6 +993,7 @@ impl<'db> Bindings<'db> { pub(crate) fn map_types( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut map: impl FnMut(&CallableBinding<'db>) -> Option>, ) -> Type<'db> { let mut element_types = Vec::with_capacity(self.elements.len()); @@ -965,11 +1006,11 @@ impl<'db> Bindings<'db> { } if !binding_types.is_empty() { - element_types.push(IntersectionType::from_elements(db, binding_types)); + element_types.push(IntersectionType::from_elements(db, env, binding_types)); } } - UnionType::from_elements(db, element_types) + UnionType::from_elements(db, env, element_types) } /// Maps each `CallableItem` to a type and combines results while preserving @@ -980,6 +1021,7 @@ impl<'db> Bindings<'db> { fn map_item_types( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut map: impl FnMut(&CallableItem<'db>) -> Option>, ) -> Type<'db> { let mut element_types = Vec::with_capacity(self.elements.len()); @@ -992,11 +1034,11 @@ impl<'db> Bindings<'db> { } if !item_types.is_empty() { - element_types.push(IntersectionType::from_elements(db, item_types)); + element_types.push(IntersectionType::from_elements(db, env, item_types)); } } - UnionType::from_elements(db, element_types) + UnionType::from_elements(db, env, element_types) } /// Builds matched bindings for the callable wrapped by `functools.partial(...)`. @@ -1005,18 +1047,20 @@ impl<'db> Bindings<'db> { /// normalization) used by both inference and known-call evaluation. fn functools_partial_matched_bindings<'a>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, wrapped_callable_ty: Type<'db>, call_arguments: &CallArguments<'a, 'db>, ) -> Option<(CallArguments<'a, 'db>, Bindings<'db>, bool)> { // We can only infer bound-argument context from an actual callable. - wrapped_callable_ty.try_upcast_to_callable(db)?; + wrapped_callable_ty.try_upcast_to_callable(db, env)?; let (bound_call_arguments, can_synthesize_signature) = - call_arguments.functools_partial_bound_arguments(db)?; + call_arguments.functools_partial_bound_arguments(db, env)?; - let mut partial_bindings = wrapped_callable_ty - .bindings(db) - .match_parameters(db, &bound_call_arguments); + let mut partial_bindings = + wrapped_callable_ty + .bindings(db, env) + .match_parameters(db, env, &bound_call_arguments); for binding in partial_bindings.iter_flat_mut() { binding.clear_missing_argument_errors_for_partial_application(); } @@ -1040,14 +1084,15 @@ impl<'db> Bindings<'db> { fn functools_partial_type<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, wrapped_callable_ty: Type<'db>, partial_overload: &mut Binding<'db>, bound_call_arguments: &CallArguments<'a, 'db>, ) -> Type<'db> { if wrapped_callable_ty.is_union() || wrapped_callable_ty.is_intersection() { - return self.map_item_types(db, |partial_item| { + return self.map_item_types(db, env, |partial_item| { partial_item - .functools_partial_callable(db, partial_overload, bound_call_arguments) + .functools_partial_callable(db, env, partial_overload, bound_call_arguments) .map(|callable| { callable.into_precise_functools_partial_instance(db, wrapped_callable_ty) }) @@ -1057,7 +1102,12 @@ impl<'db> Bindings<'db> { let partial_callables: SmallVec<[CallableType<'db>; 1]> = self .iter_callable_items() .filter_map(|partial_item| { - partial_item.functools_partial_callable(db, partial_overload, bound_call_arguments) + partial_item.functools_partial_callable( + db, + env, + partial_overload, + bound_call_arguments, + ) }) .collect(); @@ -1095,6 +1145,7 @@ impl<'db> Bindings<'db> { fn freshen_generic_contexts_in_place( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, nonce_generator: &TypeVarNonceGenerator<'db>, ) { let enclosing_binding_contexts = self.enclosing_binding_contexts.take(); @@ -1103,7 +1154,7 @@ impl<'db> Bindings<'db> { .record_enclosing_binding_contexts(enclosing_binding_contexts.iter().copied()); } for item in self.iter_callable_items_mut() { - item.freshen_generic_contexts_in_place(db, nonce_generator); + item.freshen_generic_contexts_in_place(db, env, nonce_generator); } self.enclosing_binding_contexts = enclosing_binding_contexts; } @@ -1120,17 +1171,23 @@ impl<'db> Bindings<'db> { pub(crate) fn match_parameters( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, arguments: &CallArguments<'_, 'db>, ) -> Self { let nonce_generator = TypeVarNonceGenerator::default(); - self.freshen_generic_contexts_in_place(db, &nonce_generator); - self.match_parameters_in_place(db, arguments); + self.freshen_generic_contexts_in_place(db, env, &nonce_generator); + self.match_parameters_in_place(db, env, arguments); self } - fn match_parameters_in_place(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { + fn match_parameters_in_place( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + arguments: &CallArguments<'_, 'db>, + ) { for item in self.iter_callable_items_mut() { - item.match_parameters(db, arguments); + item.match_parameters(db, env, arguments); } } @@ -1149,6 +1206,7 @@ impl<'db> Bindings<'db> { pub(crate) fn check_types( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, call_arguments: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, @@ -1156,6 +1214,7 @@ impl<'db> Bindings<'db> { ) -> Result> { match self.check_types_impl( db, + env, constraints, call_arguments, call_expression_tcx, @@ -1167,9 +1226,11 @@ impl<'db> Bindings<'db> { } } + #[expect(clippy::too_many_arguments)] pub(crate) fn check_types_impl( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, call_arguments: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, @@ -1178,7 +1239,14 @@ impl<'db> Bindings<'db> { ) -> Result<(), CallErrorKind> { // Check types for each element (union variant) for element in &mut self.elements { - element.check_types(db, constraints, call_arguments, call_expression_tcx, mode); + element.check_types( + db, + env, + constraints, + call_arguments, + call_expression_tcx, + mode, + ); } // Generic call inference must maintain a stable set of overloads until the final round @@ -1187,13 +1255,14 @@ impl<'db> Bindings<'db> { return Ok(()); } - self.evaluate_known_cases(db, call_arguments, dataclass_field_specifiers); + self.evaluate_known_cases(db, env, call_arguments, dataclass_field_specifiers); // For constructor bindings with deferred downstream checks: validate downstream bindings // if the matched overload is instance-returning. for constructor in self.iter_constructor_items_mut() { constructor.check_downstream_constructor( db, + env, constraints, call_arguments, call_expression_tcx, @@ -1215,17 +1284,19 @@ impl<'db> Bindings<'db> { pub(crate) fn finalize_argument_inference( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, call_arguments: &CallArguments<'_, 'db>, dataclass_field_specifiers: &[Type<'db>], ) -> Result<(), CallErrorKind> { - self.evaluate_known_cases(db, call_arguments, dataclass_field_specifiers); + self.evaluate_known_cases(db, env, call_arguments, dataclass_field_specifiers); for constructor in self.iter_constructor_items_mut() { - if constructor.discard_downstream_constructor(db) + if constructor.discard_downstream_constructor(db, env) && let Some(downstream) = constructor.downstream_constructor_mut() { let _ = downstream.finalize_argument_inference( db, + env, call_arguments, dataclass_field_specifiers, ); @@ -1274,10 +1345,13 @@ impl<'db> Bindings<'db> { /// Returns the return type of the call. For successful calls, this is the actual return type. /// For calls with binding errors, this is a type that best approximates the return type. For /// types that are not callable, returns `Type::Unknown`. - pub(crate) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn return_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { UnionType::from_elements( db, - self.elements.iter().map(|element| element.return_type(db)), + env, + self.elements + .iter() + .map(|element| element.return_type(db, env)), ) } @@ -1344,13 +1418,15 @@ impl<'db> Bindings<'db> { context: &CallDiagnosticContext<'_, '_, 'db, '_>, node: ast::AnyNodeRef, ) { + let db = context.db(); + let env = context.program_environment(); // If all elements are not callable, report that the type as a whole is not callable. if self.elements.iter().all(|e| !e.is_callable()) { let range = all_arguments_range(node); if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, range) { builder.into_diagnostic(format_args!( "Object of type `{}` is not callable", - self.callable_type().display(context.db()) + self.callable_type().display(db, env) )); } return; @@ -1389,11 +1465,14 @@ impl<'db> Bindings<'db> { node: ast::AnyNodeRef, element: &BindingsElement<'db>, ) { + let db = context.db(); // If this element succeeded, no diagnostics to report if element.as_result(context.db()).is_ok() { return; } + let env = context.program_environment(); + let is_union = self.elements.len() > 1; // For intersection elements, use priority hierarchy @@ -1403,7 +1482,8 @@ impl<'db> Bindings<'db> { // Construct the intersection type from the bindings let intersection_type = IntersectionType::from_elements( - context.db(), + db, + env, element.items.iter().map(CallableItem::callable_type), ); @@ -1456,6 +1536,7 @@ impl<'db> Bindings<'db> { fn evaluate_known_cases( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, call_arguments: &CallArguments<'_, 'db>, dataclass_field_specifiers: &[Type<'db>], ) { @@ -1486,7 +1567,7 @@ impl<'db> Bindings<'db> { BoundMethodType::new( db, function, - instance.to_meta_type(db), + instance.to_meta_type(db, env), ), )); } @@ -1522,7 +1603,7 @@ impl<'db> Bindings<'db> { BoundMethodType::new( db, *function, - instance.to_meta_type(db), + instance.to_meta_type(db, env), ), )); } @@ -1576,46 +1657,41 @@ impl<'db> Bindings<'db> { Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))), .., - ] => { - match property - .getter(db) - .and_then(Type::as_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(Type::heterogeneous_tuple( - db, - typevar.constraints(db).into_iter().flatten(), - )); - } - Some("__default__") => { - overload.set_return_type( - typevar.default_type(db).unwrap_or_else(|| { - KnownClass::NoDefaultType.to_instance(db) - }), - ); - } - _ => {} + ] => match property.getter(db).and_then(Type::as_function_literal) { + Some(getter) if getter.name(db) == "__name__" => { + overload.set_return_type(Type::string_literal( + db, + typevar.name(db), + )); } - } + Some(getter) if getter.name(db) == "__bound__" => { + overload.set_return_type( + typevar + .upper_bound(db, env) + .unwrap_or_else(|| Type::none(db, env)), + ); + } + Some(getter) if getter.name(db) == "__constraints__" => { + overload.set_return_type(Type::heterogeneous_tuple( + db, + env, + typevar.constraints(db, env).into_iter().flatten(), + )); + } + Some(getter) if getter.name(db) == "__default__" => { + overload.set_return_type( + typevar.default_type(db, env).unwrap_or_else(|| { + KnownClass::NoDefaultType.to_instance(db, env) + }), + ); + } + _ => {} + }, [Some(Type::PropertyInstance(property)), Some(instance), ..] => { if let Some(getter) = property.getter(db) { if let Ok(return_ty) = getter - .try_call(db, &CallArguments::positional([*instance])) - .map(|binding| binding.return_type(db)) + .try_call(db, env, &CallArguments::positional([*instance])) + .map(|binding| binding.return_type(db, env)) { overload.set_return_type(return_ty); } else { @@ -1643,8 +1719,8 @@ impl<'db> Bindings<'db> { [Some(instance), ..] => { if let Some(getter) = property.getter(db) { if let Ok(return_ty) = getter - .try_call(db, &CallArguments::positional([*instance])) - .map(|binding| binding.return_type(db)) + .try_call(db, env, &CallArguments::positional([*instance])) + .map(|binding| binding.return_type(db, env)) { overload.set_return_type(return_ty); } else { @@ -1673,7 +1749,8 @@ impl<'db> Bindings<'db> { ] = overload.parameter_types() { if let Some(setter) = property.setter(db) { - overload.check_property_setter(db, setter, *instance, *value, 1); + overload + .check_property_setter(db, env, setter, *instance, *value, 1); } else { overload .errors @@ -1688,15 +1765,15 @@ impl<'db> Bindings<'db> { { if let Some(deleter) = property.deleter(db) { if let Ok(return_ty) = deleter - .try_call(db, &CallArguments::positional([*instance])) - .map(|binding| binding.return_type(db)) + .try_call(db, env, &CallArguments::positional([*instance])) + .map(|binding| binding.return_type(db, env)) { // `property.__delete__` returns `None` for ordinary deleters, // but preserving `Never` keeps non-returning deleters divergent. overload.set_return_type(if return_ty.is_never() { return_ty } else { - Type::none(db) + Type::none(db, env) }); } else { overload.errors.push(BindingError::InternalCallError( @@ -1715,7 +1792,8 @@ impl<'db> Bindings<'db> { Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderSet(property)) => { if let [Some(instance), Some(value), ..] = overload.parameter_types() { if let Some(setter) = property.setter(db) { - overload.check_property_setter(db, setter, *instance, *value, 0); + overload + .check_property_setter(db, env, setter, *instance, *value, 0); } else { overload .errors @@ -1730,15 +1808,15 @@ impl<'db> Bindings<'db> { if let [Some(instance), ..] = overload.parameter_types() { if let Some(deleter) = property.deleter(db) { if let Ok(return_ty) = deleter - .try_call(db, &CallArguments::positional([*instance])) - .map(|binding| binding.return_type(db)) + .try_call(db, env, &CallArguments::positional([*instance])) + .map(|binding| binding.return_type(db, env)) { // `property.__delete__` returns `None` for ordinary deleters, // but preserving `Never` keeps non-returning deleters divergent. overload.set_return_type(if return_ty.is_never() { return_ty } else { - Type::none(db) + Type::none(db, env) }); } else { overload.errors.push(BindingError::InternalCallError( @@ -1906,11 +1984,10 @@ impl<'db> Bindings<'db> { }; let init = init - .map(|init| !init.bool(db).is_always_false()) + .map(|init| !init.bool(db, env).is_always_false()) .unwrap_or(true); - let kw_only = if Program::get(db).python_version(db) >= PythonVersion::PY310 - { + let kw_only = if env.python_version(db) >= PythonVersion::PY310 { match kw_only.and_then(Type::as_literal_value_kind) { // We are more conservative here when turning the type for `kw_only` // into a bool, because a field specifier in a stub might use @@ -1938,10 +2015,10 @@ impl<'db> Bindings<'db> { // instances (`my_model.field = …`). The output type is used to validate // that the converter's return type is assignable to the field's declared type. let converter = converter.and_then(|converter_ty| { - let mut input_types = UnionBuilder::new(db); - let mut output_types = UnionBuilder::new(db); + let mut input_types = UnionBuilder::new(db, env); + let mut output_types = UnionBuilder::new(db, env); let mut found_any = false; - let bindings = converter_ty.bindings(db); + let bindings = converter_ty.bindings(db, env); // Note: `iter_callable_items` collapses the union/intersection // structure. In principle, if the converter is a union of callables, // we should only accept the intersection of all first parameter @@ -1962,7 +2039,7 @@ impl<'db> Bindings<'db> { let class_default_specialization = item .as_constructor() .map(ConstructorBinding::constructed_instance_type) - .and_then(|ty| ty.class_specialization(db)) + .and_then(|ty| ty.class_specialization(db, env)) .map(|(_, specialization)| { specialization .generic_context(db) @@ -1974,10 +2051,11 @@ impl<'db> Bindings<'db> { let default_specialization = class_default_specialization .or_else(|| { - overload - .signature - .generic_context - .map(|ctx| ctx.default_specialization(db, None)) + overload.signature.generic_context.map( + |generic_context| { + generic_context.default_specialization(db, None) + }, + ) }); if let Some(first_param) = params.get_positional(first_index) { @@ -2035,11 +2113,11 @@ impl<'db> Bindings<'db> { Type::FunctionLiteral(function_type) => match function_type.known(db) { Some(KnownFunction::IsEquivalentTo) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let ty_a = ty_a.project_type_form(db); - let ty_b = ty_b.project_type_form(db); + let ty_a = ty_a.project_type_form(db, env); + let ty_b = ty_b.project_type_form(db, env); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - ty_a.when_equivalent_to(db, ty_b, constraints) + ty_a.when_equivalent_to(db, env, ty_b, constraints) }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -2050,11 +2128,17 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsSubtypeOf) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let ty_a = ty_a.project_type_form(db); - let ty_b = ty_b.project_type_form(db); + let ty_a = ty_a.project_type_form(db, env); + let ty_b = ty_b.project_type_form(db, env); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - ty_a.when_subtype_of(db, ty_b, constraints, TypeVarSet::None) + ty_a.when_subtype_of( + db, + env, + ty_b, + constraints, + TypeVarSet::None, + ) }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -2065,11 +2149,17 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsAssignableTo) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let ty_a = ty_a.project_type_form(db); - let ty_b = ty_b.project_type_form(db); + let ty_a = ty_a.project_type_form(db, env); + let ty_b = ty_b.project_type_form(db, env); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - ty_a.when_assignable_to(db, ty_b, constraints, TypeVarSet::None) + ty_a.when_assignable_to( + db, + env, + ty_b, + constraints, + TypeVarSet::None, + ) }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -2080,11 +2170,16 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsConstraintSetAssignableTo) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let ty_a = ty_a.project_type_form(db); - let ty_b = ty_b.project_type_form(db); + let ty_a = ty_a.project_type_form(db, env); + let ty_b = ty_b.project_type_form(db, env); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - ty_a.when_constraint_set_assignable_to(db, ty_b, constraints) + ty_a.when_constraint_set_assignable_to( + db, + env, + ty_b, + constraints, + ) }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -2095,11 +2190,17 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsDisjointFrom) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { - let ty_a = ty_a.project_type_form(db); - let ty_b = ty_b.project_type_form(db); + let ty_a = ty_a.project_type_form(db, env); + let ty_b = ty_b.project_type_form(db, env); let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - ty_a.when_disjoint_from(db, ty_b, constraints, TypeVarSet::None) + ty_a.when_disjoint_from( + db, + env, + ty_b, + constraints, + TypeVarSet::None, + ) }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -2111,7 +2212,7 @@ impl<'db> Bindings<'db> { Some(KnownFunction::IsSingleton) => { if let [Some(ty)] = overload.parameter_types() { overload.set_return_type(Type::bool_literal( - ty.project_type_form(db).is_singleton(db), + ty.project_type_form(db, env).is_singleton(db, env), )); } } @@ -2128,6 +2229,7 @@ impl<'db> Bindings<'db> { |signature: &CallableSignature<'db>| { UnionType::try_from_elements( db, + env, signature.overloads.iter().map(|signature| { signature.generic_context.map(wrap_generic_context) }), @@ -2161,6 +2263,7 @@ impl<'db> Bindings<'db> { let generic_context = match ty { Type::Union(union_type) => UnionType::try_from_elements( db, + env, union_type .elements(db) .iter() @@ -2170,7 +2273,7 @@ impl<'db> Bindings<'db> { }; overload.set_return_type( - generic_context.unwrap_or_else(|| Type::none(db)), + generic_context.unwrap_or_else(|| Type::none(db, env)), ); } } @@ -2182,16 +2285,18 @@ impl<'db> Bindings<'db> { let [Some(ty)] = overload.parameter_types() else { continue; }; - let Some(callables) = ty.try_upcast_to_callable(db).map(|callables| { - if into_callable == KnownFunction::IntoRegularCallable { - callables.map(|callable| callable.into_regular(db)) - } else { - callables - } - }) else { + let Some(callables) = + ty.try_upcast_to_callable(db, env).map(|callables| { + if into_callable == KnownFunction::IntoRegularCallable { + callables.map(|callable| callable.into_regular(db)) + } else { + callables + } + }) + else { continue; }; - overload.set_return_type(callables.into_type(db)); + overload.set_return_type(callables.into_type(db, env)); } Some(KnownFunction::DunderAllNames) => { @@ -2200,7 +2305,7 @@ impl<'db> Bindings<'db> { Type::ModuleLiteral(module_literal) => { let all_names = module_literal .module(db) - .file(db) + .python_file(db) .map(|file| dunder_all_names(db, file)) .unwrap_or_default(); match all_names { @@ -2209,15 +2314,16 @@ impl<'db> Bindings<'db> { names.sort(); Type::heterogeneous_tuple( db, + env, names.iter().map(|name| { Type::string_literal(db, *name) }), ) } - None => Type::none(db), + None => Type::none(db, env), } } - _ => Type::none(db), + _ => Type::none(db, env), }); } } @@ -2231,6 +2337,7 @@ impl<'db> Bindings<'db> { { Type::heterogeneous_tuple( db, + env, metadata .members .keys() @@ -2251,7 +2358,8 @@ impl<'db> Bindings<'db> { if let [Some(ty)] = overload.parameter_types() { overload.set_return_type(Type::heterogeneous_tuple( db, - list_members::all_members(db, *ty) + env, + list_members::all_members(db, env, *ty) .into_iter() .sorted() .map(|member| Type::string_literal(db, &member.name)), @@ -2261,7 +2369,7 @@ impl<'db> Bindings<'db> { Some(KnownFunction::Len) => { if let [Some(first_arg)] = overload.parameter_types() - && let Some(len_ty) = first_arg.len(db) + && let Some(len_ty) = first_arg.len(db, env) { overload.set_return_type(len_ty); } @@ -2269,13 +2377,13 @@ impl<'db> Bindings<'db> { Some(KnownFunction::Repr) => { if let [Some(first_arg)] = overload.parameter_types() { - overload.set_return_type(first_arg.repr(db)); + overload.set_return_type(first_arg.repr(db, env)); } } Some(KnownFunction::Cast) => { if let [Some(casted_ty), Some(_)] = overload.parameter_types() { - overload.set_return_type(casted_ty.project_type_form(db)); + overload.set_return_type(casted_ty.project_type_form(db, env)); } } @@ -2303,10 +2411,14 @@ impl<'db> Bindings<'db> { .interface(db) .members(db) .map(|member| Type::string_literal(db, member.name())); - let specialization = UnionType::from_elements(db, member_names); + let specialization = + UnionType::from_elements(db, env, member_names); overload.set_return_type( - KnownClass::FrozenSet - .to_specialized_instance(db, &[specialization]), + KnownClass::FrozenSet.to_specialized_instance( + db, + env, + &[specialization], + ), ); } } @@ -2329,11 +2441,11 @@ impl<'db> Bindings<'db> { }; let union_with_default = - |ty| UnionType::from_two_elements(db, ty, default); + |ty| UnionType::from_two_elements(db, env, 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)) { + match instance_ty.static_member(db, env, attr_name.value(db)) { Place::Defined(DefinedPlace { ty, definedness: Definedness::AlwaysDefined, @@ -2442,7 +2554,7 @@ impl<'db> Bindings<'db> { _ => {} } - let params = DataclassParams::from_flags(db, flags); + let params = DataclassParams::from_flags(db, env, flags); if cls_argument.is_none_or(|cls_ty| cls_ty.is_none(db)) { overload.set_return_type(Type::DataclassDecorator(params)); @@ -2554,9 +2666,12 @@ impl<'db> Bindings<'db> { continue; }; - let return_type = parse_struct_format(db, format_literal.value(db)) - .map(|elements| Type::heterogeneous_tuple(db, elements)) - .unwrap_or_else(|| Type::homogeneous_tuple(db, Type::unknown())); + let return_type = + parse_struct_format(db, env, format_literal.value(db)) + .map(|elements| Type::heterogeneous_tuple(db, env, elements)) + .unwrap_or_else(|| { + Type::homogeneous_tuple(db, env, Type::unknown()) + }); overload.set_return_type(return_type); } @@ -2680,15 +2795,22 @@ impl<'db> Bindings<'db> { else { return; }; - let lower = lower.project_type_form(db); - let typevar = typevar.project_type_form(db); - let upper = upper.project_type_form(db); + let lower = lower.project_type_form(db, env); + let typevar = typevar.project_type_form(db, env); + let upper = upper.project_type_form(db, env); let Type::TypeVar(typevar) = typevar else { return; }; let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - ConstraintSet::constrain_typevar(db, constraints, typevar, lower, upper) + ConstraintSet::constrain_typevar( + db, + env, + constraints, + typevar, + lower, + upper, + ) }); let tracked = InternedConstraintSet::new(db, result); overload.set_return_type(Type::KnownInstance( @@ -2728,20 +2850,22 @@ impl<'db> Bindings<'db> { let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else { continue; }; - let ty_a = ty_a.project_type_form(db); - let ty_b = ty_b.project_type_form(db); + let ty_a = ty_a.project_type_form(db, env); + let ty_b = ty_b.project_type_form(db, env); let nonce_generator = TypeVarNonceGenerator::default(); let ty_a = freshen_generic_contexts_in_type( db, + env, ty_a, - generic_contexts_mentioned_in_type(db, ty_a), + generic_contexts_mentioned_in_type(db, env, ty_a), &nonce_generator, ); let ty_b = freshen_generic_contexts_in_type( db, + env, ty_b, - generic_contexts_mentioned_in_type(db, ty_b), + generic_contexts_mentioned_in_type(db, env, ty_b), &nonce_generator, ); @@ -2749,8 +2873,9 @@ impl<'db> Bindings<'db> { let result = constraints.into_owned(|constraints| { ty_a.when_subtype_of_assuming( db, + env, ty_b, - constraints.load(db, tracked.constraints(db)), + constraints.load(db, env, tracked.constraints(db)), constraints, TypeVarSet::None, ) @@ -2774,8 +2899,8 @@ impl<'db> Bindings<'db> { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let lhs = constraints.load(db, tracked.constraints(db)); - let rhs = constraints.load(db, other.constraints(db)); + let lhs = constraints.load(db, env, tracked.constraints(db)); + let rhs = constraints.load(db, env, other.constraints(db)); lhs.implies(db, constraints, || rhs) }); let tracked = InternedConstraintSet::new(db, result); @@ -2791,20 +2916,22 @@ impl<'db> Bindings<'db> { let [Some(typevars)] = overload.parameter_types() else { continue; }; - let Type::NominalInstance(instance) = typevars.project_type_form(db) else { + let Type::NominalInstance(instance) = typevars.project_type_form(db, env) + else { continue; }; - let Some(typevars) = inferable_typevars_from_tuple(db, &instance) else { + let Some(typevars) = inferable_typevars_from_tuple(db, env, &instance) + else { continue; }; let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let set = constraints.load(db, tracked.constraints(db)); + let set = constraints.load(db, env, tracked.constraints(db)); if matches!(method, KnownBoundMethodType::ConstraintSetExists(_)) { - set.reduce_inferable(db, constraints, typevars) + set.reduce_inferable(db, env, constraints, typevars) } else { - set.for_all(db, constraints, typevars) + set.for_all(db, env, constraints, typevars) } }); let tracked = InternedConstraintSet::new(db, result); @@ -2821,14 +2948,14 @@ impl<'db> Bindings<'db> { // Caller explicitly passed None, so no typevars are inferable. return Some(TypeVarSet::None); } - inferable_typevars_from_tuple(db, instance) + inferable_typevars_from_tuple(db, env, instance) }; let inferable = match overload.parameter_types() { // Caller did not provide argument, so no typevars are inferable. [None] => TypeVarSet::None, [Some(ty)] => { - let Type::NominalInstance(instance) = ty.project_type_form(db) + let Type::NominalInstance(instance) = ty.project_type_form(db, env) else { continue; }; @@ -2841,8 +2968,9 @@ impl<'db> Bindings<'db> { }; let constraints = ConstraintSetBuilder::new(); - let set = constraints.load(db, tracked.constraints(db)); - let result = set.satisfied_by_all_typevars(db, &constraints, inferable); + let set = constraints.load(db, env, tracked.constraints(db)); + let result = + set.satisfied_by_all_typevars(db, env, &constraints, inferable); overload.set_return_type(Type::bool_literal(result)); } @@ -2852,22 +2980,24 @@ impl<'db> Bindings<'db> { let [Some(typevar), Some(inferable)] = overload.parameter_types() else { continue; }; - let Type::TypeVar(typevar) = typevar.project_type_form(db) else { + let Type::TypeVar(typevar) = typevar.project_type_form(db, env) else { continue; }; - let Type::NominalInstance(inferable) = inferable.project_type_form(db) + let Type::NominalInstance(inferable) = inferable.project_type_form(db, env) else { continue; }; - let Some(inferable) = inferable_typevars_from_tuple(db, &inferable) else { + let Some(inferable) = inferable_typevars_from_tuple(db, env, &inferable) + else { continue; }; let constraints = ConstraintSetBuilder::new(); - let set = constraints.load(db, tracked.constraints(db)); - let result = match set.solutions(db, &constraints, inferable) { + let set = constraints.load(db, env, tracked.constraints(db)); + let result = match set.solutions(db, env, &constraints, inferable) { Solutions::Constrained(paths) => Type::heterogeneous_tuple( db, + env, paths.into_iter().map(|path| { let path: Box<[_]> = path .into_iter() @@ -2878,8 +3008,8 @@ impl<'db> Bindings<'db> { )) }), ), - Solutions::Unsatisfiable => Type::none(db), - Solutions::Unconstrained => Type::empty_tuple(db), + Solutions::Unsatisfiable => Type::none(db, env), + Solutions::Unconstrained => Type::empty_tuple(db, env), }; overload.set_return_type(result); } @@ -2890,19 +3020,21 @@ impl<'db> Bindings<'db> { let [Some(inferable)] = overload.parameter_types() else { continue; }; - let Type::NominalInstance(inferable) = inferable.project_type_form(db) + let Type::NominalInstance(inferable) = inferable.project_type_form(db, env) else { continue; }; - let Some(inferable) = inferable_typevars_from_tuple(db, &inferable) else { + let Some(inferable) = inferable_typevars_from_tuple(db, env, &inferable) + else { continue; }; let constraints = ConstraintSetBuilder::new(); - let set = constraints.load(db, tracked.constraints(db)); - let result = match set.solutions(db, &constraints, inferable) { + let set = constraints.load(db, env, tracked.constraints(db)); + let result = match set.solutions(db, env, &constraints, inferable) { Solutions::Constrained(paths) => Type::heterogeneous_tuple( db, + env, paths.into_iter().map(|path| { Type::KnownInstance(KnownInstanceType::ConstraintSetSolution( InternedConstraintSetSolution::new( @@ -2912,8 +3044,8 @@ impl<'db> Bindings<'db> { )) }), ), - Solutions::Unsatisfiable => Type::none(db), - Solutions::Unconstrained => Type::empty_tuple(db), + Solutions::Unsatisfiable => Type::none(db, env), + Solutions::Unconstrained => Type::empty_tuple(db, env), }; overload.set_return_type(result); } @@ -2930,7 +3062,11 @@ impl<'db> Bindings<'db> { Type::ClassLiteral(class) => match class.known(db) { Some(KnownClass::Bool) => match overload.parameter_types() { [Some(arg)] => { - overload.set_return_type(Type::from_truthiness(db, arg.bool(db))); + overload.set_return_type(Type::from_truthiness( + db, + env, + arg.bool(db, env), + )); } [None] => overload.set_return_type(Type::bool_literal(false)), _ => {} @@ -2938,7 +3074,9 @@ impl<'db> Bindings<'db> { Some(KnownClass::Str) if overload_index == 0 => { match overload.parameter_types() { - [Some(arg)] => overload.set_return_type(arg.str(db)), + [Some(arg)] => { + overload.set_return_type(arg.str(db, env)); + } [None] => { overload.set_return_type(Type::string_literal(db, "")); } @@ -2948,7 +3086,7 @@ impl<'db> Bindings<'db> { Some(KnownClass::Type) if overload_index == 0 => { if let [Some(arg)] = overload.parameter_types() { - overload.set_return_type(arg.dunder_class(db)); + overload.set_return_type(arg.dunder_class(db, env)); } } @@ -2965,7 +3103,7 @@ impl<'db> Bindings<'db> { Some(KnownClass::FunctoolsPartial) => { if let Some(new_return_type) = - overload.functools_partial_return_type(db, call_arguments) + overload.functools_partial_return_type(db, env, call_arguments) { overload.set_return_type(new_return_type); } @@ -2982,11 +3120,15 @@ impl<'db> Bindings<'db> { // `__iter__ = None`, for example). That would be badly written Python code, but we still // need to be able to handle it without crashing. let return_type = if let Type::Union(union) = argument { - union.map(db, |element| { - Type::tuple(TupleType::new(db, &element.iterate(db))) + union.map(db, env, |element| { + Type::tuple(TupleType::new( + db, + env, + &element.iterate(db, env), + )) }) } else { - Type::tuple(TupleType::new(db, &argument.iterate(db))) + Type::tuple(TupleType::new(db, env, &argument.iterate(db, env))) }; overload.set_return_type(return_type); } @@ -3180,7 +3322,11 @@ impl<'db> CallableBinding<'db> { /// Rewrites overload signatures as if an implicit bound receiver argument had already been /// consumed, preserving the corresponding source-parameter offset for diagnostics. - pub(crate) fn bake_bound_type_into_overloads(&mut self, db: &'db dyn Db) { + pub(crate) fn bake_bound_type_into_overloads( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) { let Some(bound_self) = self.bound_type.take() else { return; }; @@ -3194,10 +3340,12 @@ impl<'db> CallableBinding<'db> { .parameters() .get(0) .is_some_and(Parameter::is_positional); - overload.signature = - overload - .signature - .bind_self_with_receiver(db, Some(bound_self), Some(typing_self)); + overload.signature = overload.signature.bind_self_with_receiver( + db, + env, + Some(bound_self), + Some(typing_self), + ); overload.return_ty = overload.initial_return_type(db); overload.source_parameter_index_offset += usize::from(removed_receiver); } @@ -3206,6 +3354,7 @@ impl<'db> CallableBinding<'db> { fn freshen_generic_contexts_in_place( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, nonce_generator: &TypeVarNonceGenerator<'db>, ) { if self @@ -3242,7 +3391,7 @@ impl<'db> CallableBinding<'db> { continue; }; if nonce_generator.should_freshen(db, generic_context) { - overload.freshen_bound_typevars(db, nonce.value()); + overload.freshen_bound_typevars(db, env, nonce.value()); } } } @@ -3377,7 +3526,7 @@ impl<'db> CallableBinding<'db> { .into_iter() .filter_map(|index| { self.overloads().get(index).map(|overload| { - overload.partial_signature_application(signature_arguments.as_ref(), db) + overload.partial_signature_application(db, signature_arguments.as_ref()) }) }) .collect(); @@ -3434,19 +3583,25 @@ impl<'db> CallableBinding<'db> { } } - fn match_parameters(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { + fn match_parameters( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + arguments: &CallArguments<'_, 'db>, + ) { // If this callable is a bound method, prepend the self instance onto the arguments list // before checking. let bound_arguments = arguments.with_self(self.bound_type); for overload in &mut self.overloads { - overload.match_parameters(db, bound_arguments.as_ref()); + overload.match_parameters(db, env, bound_arguments.as_ref()); } } fn check_types( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, call_arguments: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, @@ -3457,8 +3612,8 @@ impl<'db> CallableBinding<'db> { let _span = tracing::trace_span!( "CallableBinding::check_types", - arguments = %call_arguments.display(db), - signature = %self.signature_type.display(db), + arguments = %call_arguments.display(db, env), + signature = %self.signature_type.display(db, env), ) .entered(); @@ -3475,7 +3630,7 @@ impl<'db> CallableBinding<'db> { // `*arg` where `arg` is a union of a 2-tuple and a 3-tuple, we shouldn't eliminate any // overload for arity reasons before trying argument expansion. let (should_retry_after_provisional_arity, overloads_for_expansion) = - if self.should_retry_after_provisional_arity(db, call_arguments.as_ref()) { + if self.should_retry_after_provisional_arity(db, env, call_arguments.as_ref()) { // We will retry all overloads after argument expansion. (true, (0..self.overloads.len()).collect()) } else { @@ -3487,6 +3642,7 @@ impl<'db> CallableBinding<'db> { if let [overload] = self.overloads.as_mut_slice() { overload.check_types( db, + env, constraints, call_arguments.as_ref(), call_expression_tcx, @@ -3500,6 +3656,7 @@ impl<'db> CallableBinding<'db> { self.matching_overload_before_type_checking = Some(index); self.overloads[index].check_types( db, + env, constraints, call_arguments.as_ref(), call_expression_tcx, @@ -3515,6 +3672,7 @@ impl<'db> CallableBinding<'db> { for (_, overload) in self.matching_overloads_mut() { overload.check_types( db, + env, constraints, call_arguments.as_ref(), call_expression_tcx, @@ -3563,6 +3721,7 @@ impl<'db> CallableBinding<'db> { // If two or more candidate overloads remain, proceed to step 5. self.filter_overloads_using_any_or_unknown( db, + env, constraints, call_arguments.as_ref(), &indexes, @@ -3584,7 +3743,7 @@ impl<'db> CallableBinding<'db> { // Step 3: Perform "argument type expansion". Reference: // https://typing.python.org/en/latest/spec/overload.html#argument-type-expansion - let mut expansions = call_arguments.expand(db).peekable(); + let mut expansions = call_arguments.expand(db, env).peekable(); // Return early if there are no argument types to expand. if expansions.peek().is_none() { @@ -3606,7 +3765,7 @@ impl<'db> CallableBinding<'db> { let Some(argument_type) = argument_types.get_default() else { continue; }; - if is_expandable_type(db, argument_type) { + if is_expandable_type(db, env, argument_type) { continue; } let mut is_argument_assignable_to_any_overload = false; @@ -3618,11 +3777,12 @@ impl<'db> CallableBinding<'db> { if argument_type .when_assignable_to( db, + env, parameter_type, constraints, overload.inferable_typevars, ) - .is_always_satisfied(db) + .is_always_satisfied(db, env) { is_argument_assignable_to_any_overload = true; break 'overload; @@ -3633,7 +3793,7 @@ impl<'db> CallableBinding<'db> { tracing::debug!( "Argument at {argument_index} (`{}`) is not assignable to any of the \ remaining overloads, skipping argument type expansion", - argument_type.display(db) + argument_type.display(db, env) ); return; } @@ -3677,7 +3837,7 @@ impl<'db> CallableBinding<'db> { for overload in &mut self.overloads { // Clear the state of all overloads before re-evaluating from step 1 overload.reset(db); - overload.match_parameters(db, expanded_arguments); + overload.match_parameters(db, env, expanded_arguments); } tracing::trace!( @@ -3687,7 +3847,13 @@ impl<'db> CallableBinding<'db> { ); for (_, overload) in self.matching_overloads_mut() { - overload.check_types(db, constraints, expanded_arguments, call_expression_tcx); + overload.check_types( + db, + env, + constraints, + expanded_arguments, + call_expression_tcx, + ); } tracing::trace!( @@ -3721,6 +3887,7 @@ impl<'db> CallableBinding<'db> { MatchingOverloadIndex::Multiple(indexes) => { self.filter_overloads_using_any_or_unknown( db, + env, constraints, expanded_arguments, &indexes, @@ -3775,7 +3942,7 @@ impl<'db> CallableBinding<'db> { // union to determine the final return type. self.overload_call_return_type = Some(OverloadCallReturnType::ArgumentTypeExpansion( - UnionType::from_elements(db, return_types), + UnionType::from_elements(db, env, return_types), )); return; @@ -3797,9 +3964,10 @@ impl<'db> CallableBinding<'db> { pub(crate) fn candidate_overload_indices( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, call_arguments: &CallArguments<'_, 'db>, ) -> SmallVec<[usize; 1]> { - if self.should_retry_after_provisional_arity(db, call_arguments) { + if self.should_retry_after_provisional_arity(db, env, call_arguments) { (0..self.overloads.len()).collect() } else { self.matching_overloads().map(|(index, _)| index).collect() @@ -3809,6 +3977,7 @@ impl<'db> CallableBinding<'db> { fn should_retry_after_provisional_arity( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, call_arguments: &CallArguments<'_, 'db>, ) -> bool { self.overloads.len() > 1 @@ -3817,7 +3986,7 @@ impl<'db> CallableBinding<'db> { matches!(argument, Argument::Variadic) && argument_types .get_default() - .is_some_and(|argument_type| is_expandable_type(db, argument_type)) + .is_some_and(|argument_type| is_expandable_type(db, env, argument_type)) }) } @@ -3862,6 +4031,7 @@ impl<'db> CallableBinding<'db> { fn filter_overloads_using_any_or_unknown( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, arguments: &CallArguments<'_, 'db>, matching_overload_indexes: &[usize], @@ -3918,8 +4088,8 @@ impl<'db> CallableBinding<'db> { match (first_parameter_type, current_parameter_type) { (Some(first_parameter_type), Some(current_parameter_type)) => { if !first_parameter_type - .when_equivalent_to(db, current_parameter_type, constraints) - .is_always_satisfied(db) + .when_equivalent_to(db, env, current_parameter_type, constraints) + .is_always_satisfied(db, env) { participating_slot_indices.insert(slot_index); } @@ -3945,9 +4115,10 @@ impl<'db> CallableBinding<'db> { continue; } - let mut union_argument_type_builders = std::iter::repeat_with(|| UnionBuilder::new(db)) - .take(max_slot_count) - .collect::>(); + let mut union_argument_type_builders = + std::iter::repeat_with(|| UnionBuilder::new(db, env)) + .take(max_slot_count) + .collect::>(); let (_, current_slots) = &matching_overload_slots[upto]; @@ -3960,13 +4131,14 @@ impl<'db> CallableBinding<'db> { .map_or(Type::unknown(), |slot| slot.argument) }); union_argument_type_builders[slot_index] - .add_in_place(argument_type.top_materialization(db)); + .add_in_place(argument_type.top_materialization(db, env)); } } } let top_materialized_argument_type = Type::heterogeneous_tuple( db, + env, union_argument_type_builders .into_iter() .filter_map(|builder| { @@ -3978,7 +4150,7 @@ impl<'db> CallableBinding<'db> { }), ); - let mut union_parameter_types = std::iter::repeat_with(|| UnionBuilder::new(db)) + let mut union_parameter_types = std::iter::repeat_with(|| UnionBuilder::new(db, env)) .take(max_slot_count) .collect::>(); for (_, slots) in &matching_overload_slots[..=upto] { @@ -3991,6 +4163,7 @@ impl<'db> CallableBinding<'db> { let parameter_types = Type::heterogeneous_tuple( db, + env, union_parameter_types.into_iter().filter_map(|builder| { if builder.is_empty() { None @@ -4003,11 +4176,12 @@ impl<'db> CallableBinding<'db> { if top_materialized_argument_type .when_assignable_to( db, + env, parameter_types, constraints, self.overloads[*current_index].inferable_typevars, ) - .is_always_satisfied(db) + .is_always_satisfied(db, env) { filter_remaining_overloads = true; } @@ -4025,8 +4199,8 @@ impl<'db> CallableBinding<'db> { matching_overloads.all(|(_, overload)| { overload .return_type() - .when_equivalent_to(db, first_overload_return_type, constraints) - .is_always_satisfied(db) + .when_equivalent_to(db, env, first_overload_return_type, constraints) + .is_always_satisfied(db, env) }) } else { // No matching overload @@ -4192,15 +4366,18 @@ impl<'db> CallableBinding<'db> { node: ast::AnyNodeRef, compound_diag: Option<&dyn CompoundDiagnostic>, ) { + let db = context.db(); + let env = context.program_environment(); + if !self.is_callable() { let range = all_arguments_range(node); if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, range) { let mut diag = builder.into_diagnostic(format_args!( "Object of type `{}` is not callable", - self.callable_type.display(context.db()), + self.callable_type.display(db, env), )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } return; @@ -4211,10 +4388,10 @@ impl<'db> CallableBinding<'db> { if let Some(builder) = context.report_lint(&CALL_NON_CALLABLE, range) { let mut diag = builder.into_diagnostic(format_args!( "Object of type `{}` is not callable (possibly missing `__call__` method)", - self.callable_type.display(context.db()), + self.callable_type.display(db, env), )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } return; @@ -4223,8 +4400,7 @@ impl<'db> CallableBinding<'db> { match self.overloads.as_slice() { [] => {} [overload] => { - let callable_description = - CallableDescription::new(context.db(), self.signature_type); + let callable_description = CallableDescription::new(db, self.signature_type); overload.report_diagnostics( context, node, @@ -4254,18 +4430,13 @@ impl<'db> CallableBinding<'db> { // If only one overload passed arity check, report its errors directly. if let Some(matching_overload_index) = self.matching_overload_before_type_checking { - let callable_description = - CallableDescription::new(context.db(), self.signature_type); + let callable_description = CallableDescription::new(db, self.signature_type); let matching_overload = function_type_and_kind.map(|(kind, function)| MatchingOverloadLiteral { index: self.overloads[matching_overload_index].source_overload_index(), kind, function, - candidate_indexes: self.diagnostic_overload_indexes( - context.db(), - kind, - function, - ), + candidate_indexes: self.diagnostic_overload_indexes(db, kind, function), }); self.overloads[matching_overload_index].report_diagnostics( context, @@ -4282,18 +4453,13 @@ impl<'db> CallableBinding<'db> { // (possibly with semantic errors), report its errors directly instead // of the generic "no matching overload" message. if let Ok((matching_overload_index, _)) = self.matching_overloads().exactly_one() { - let callable_description = - CallableDescription::new(context.db(), self.signature_type); + let callable_description = CallableDescription::new(db, self.signature_type); let matching_overload = function_type_and_kind.map(|(kind, function)| MatchingOverloadLiteral { index: self.overloads[matching_overload_index].source_overload_index(), kind, function, - candidate_indexes: self.diagnostic_overload_indexes( - context.db(), - kind, - function, - ), + candidate_indexes: self.diagnostic_overload_indexes(db, kind, function), }); self.overloads[matching_overload_index].report_diagnostics( context, @@ -4310,8 +4476,7 @@ impl<'db> CallableBinding<'db> { let Some(builder) = context.report_lint(&NO_MATCHING_OVERLOAD, range) else { return; }; - let callable_description = - CallableDescription::new(context.db(), self.callable_type); + let callable_description = CallableDescription::new(db, self.callable_type); let mut diag = builder.into_diagnostic(format_args!( "No overload{} matches arguments", callable_description @@ -4339,7 +4504,7 @@ impl<'db> CallableBinding<'db> { let (overloads, implementation) = function.overloads_and_implementation(context.db()); let diagnostic_overload_indexes = - self.diagnostic_overload_indexes(context.db(), kind, function); + self.diagnostic_overload_indexes(db, kind, function); let possible_overloads = diagnostic_overload_indexes .iter() .filter_map(|&index| overloads.get(index).copied()) @@ -4351,7 +4516,9 @@ impl<'db> CallableBinding<'db> { "First overload defined here", ); let file = function.file(context.db()); - let module = parsed_module(context.db(), file).load(context.db()); + let module = + parsed_module(context.db(), function.python_file(context.db())) + .load(context.db()); let node = overload.node(context.db(), function.file(context.db()), &module); let span = if node.body.len() == 1 { @@ -4373,7 +4540,7 @@ impl<'db> CallableBinding<'db> { for overload in possible_overloads.iter().take(MAXIMUM_OVERLOADS) { diag.info(format_args!( " {}", - overload.signature(context.db()).display(context.db()) + overload.signature(db).display(db, env) )); } if possible_overloads.len() > MAXIMUM_OVERLOADS { @@ -4396,7 +4563,7 @@ impl<'db> CallableBinding<'db> { } if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } } @@ -4639,6 +4806,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { fn match_variadic( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, argument_index: usize, argument: Argument<'a>, argument_type: Option>, @@ -4688,8 +4856,11 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { if self.parameters.variadic().is_none() && !self.has_later_positional_input(argument_index) => { - let tuple_specs: Vec<_> = - union.elements(db).iter().map(|ty| ty.iterate(db)).collect(); + let tuple_specs: Vec<_> = union + .elements(db) + .iter() + .map(|ty| ty.iterate(db, env)) + .collect(); let min_len = tuple_specs .iter() @@ -4712,7 +4883,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { if var_types.is_empty() { None } else { - Some(UnionType::from_elements_leave_aliases(db, var_types)) + Some(UnionType::from_elements_leave_aliases(db, env, var_types)) } }; @@ -4721,13 +4892,16 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { for index in 0..max_elements { let positional_types: Vec<_> = tuple_specs .iter() - .filter_map(|s| s.py_index(db, index).ok()) + .filter_map(|s| s.py_index(db, env, index).ok()) .collect(); if positional_types.is_empty() { break; } - argument_types_vec - .push(UnionType::from_elements_leave_aliases(db, positional_types)); + argument_types_vec.push(UnionType::from_elements_leave_aliases( + db, + env, + positional_types, + )); } let length = if any_variable || argument_types_vec.len() > min_len { @@ -4743,7 +4917,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { } } _ => { - let tuple = argument_type.iterate(db); + let tuple = argument_type.iterate(db, env); VariadicArgumentType::Other { argument_types: tuple.iter_element_types(db).collect(), length: tuple.len(), @@ -4870,11 +5044,12 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { fn match_keyword_variadic( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, argument_index: usize, argument_type: Option>, ) { if let Some(unpacked) = - argument_type.and_then(|ty| extract_unpacked_typed_dict_from_value_type(db, ty)) + argument_type.and_then(|ty| extract_unpacked_typed_dict_from_value_type(db, env, ty)) { let openness = unpacked.openness; @@ -4909,7 +5084,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { let value_type = match argument_type { Some(argument_type) => argument_type .as_paramspec_typevar(db) - .or_else(|| argument_type.getitem_dunder_call(db, parameter_name)) + .or_else(|| argument_type.getitem_dunder_call(db, env, parameter_name)) .unwrap_or(Type::unknown()), None => Type::unknown(), @@ -5035,6 +5210,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { struct ArgumentTypeChecker<'a, 'db> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, signature_type: Type<'db>, constructor_kind: Option, signature: &'a Signature<'db>, @@ -5071,6 +5247,7 @@ enum KeywordUnpackKeyTypeCheck<'db> { /// Validate the key type of a keyword-unpack argument without checking its value type. fn validate_keyword_unpack_key_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, argument_type: Type<'db>, inferable_typevars: TypeVarSet<'db>, @@ -5081,18 +5258,19 @@ fn validate_keyword_unpack_key_type<'db>( return KeywordUnpackKeyTypeCheck::NotApplicable; } - let Some((key_type, _)) = argument_type.unpack_keys_and_items(db) else { + let Some((key_type, _)) = argument_type.unpack_keys_and_items(db, env) else { return KeywordUnpackKeyTypeCheck::NotApplicable; }; if key_type .when_assignable_to( db, - KnownClass::Str.to_instance(db), + env, + KnownClass::Str.to_instance(db, env), constraints, inferable_typevars, ) - .is_always_satisfied(db) + .is_always_satisfied(db, env) { KeywordUnpackKeyTypeCheck::Valid } else { @@ -5104,6 +5282,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { #[expect(clippy::too_many_arguments)] fn new( db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, signature_type: Type<'db>, constructor_kind: Option, signature: &'a Signature<'db>, @@ -5116,6 +5295,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { ) -> Self { Self { db, + env, signature_type, constructor_kind, signature, @@ -5208,6 +5388,9 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { paramspec: BoundTypeVarInstance<'db>, overload_index: usize, ) -> Option> { + let db = self.db; + let env = self.env; + self.enumerate_argument_types() .find_map(|(argument_index, _, argument, argument_types)| { if matches!(argument, Argument::Synthetic) { @@ -5222,39 +5405,37 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let argument_type = argument_types.get_for_declared_type(declared_type); let paramspec_prefix_len = |candidate: Type<'db>| { candidate - .try_upcast_to_callable(self.db)? + .try_upcast_to_callable(db, env)? .iter() .find_map(|callable| { - callable.signatures(self.db).iter().find_map(|signature| { + callable.signatures(db).iter().find_map(|signature| { let (prefix, declared_paramspec) = signature.parameters().as_paramspec_with_prefix()?; (declared_paramspec == paramspec).then_some(prefix.len()) }) }) }; - let prefix_len = if let Type::Union(union) = - declared_type.resolve_type_alias(self.db) - { - union.elements(self.db).iter().find_map(|candidate| { - let specialized_candidate = candidate - .apply_optional_specialization(self.db, self.specialization()); - argument_type - .is_assignable_to(self.db, specialized_candidate) - .then_some(*candidate) - .and_then(paramspec_prefix_len) - }) - } else { - paramspec_prefix_len(declared_type) - }?; + let prefix_len = + if let Type::Union(union) = declared_type.resolve_type_alias(db) { + union.elements(db).iter().find_map(|candidate| { + let specialized_candidate = candidate + .apply_optional_specialization(db, self.specialization()); + argument_type + .is_assignable_to(db, env, specialized_candidate) + .then_some(*candidate) + .and_then(paramspec_prefix_len) + }) + } else { + paramspec_prefix_len(declared_type) + }?; let (source_type, partial_signature) = match argument_type { Type::KnownInstance( KnownInstanceType::FunctoolsPartial(partial) | KnownInstanceType::FunctoolsPartialCall(partial), ) => { - let signatures = - &partial.partial(self.db).signatures(self.db).overloads; + let signatures = &partial.partial(db).signatures(db).overloads; ( - partial.wrapped(self.db).inner(self.db), + partial.wrapped(db).inner(db), signatures .iter() .find(|signature| { @@ -5266,11 +5447,11 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } _ => (argument_type, None), }; - let argument_bindings = source_type.bindings(self.db); + let argument_bindings = source_type.bindings(db, env); let callable = argument_bindings.single_item()?.callable(); let (function, is_bound_method) = match callable.signature_type { Type::FunctionLiteral(function) => (function, false), - Type::BoundMethod(method) => (method.function(self.db), true), + Type::BoundMethod(method) => (method.function(db), true), _ => return None, }; let source_binding = callable @@ -5311,19 +5492,21 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } fn specialization(&self) -> Option> { - self.inference - .map(|inference| inference.specialization(self.db)) + let db = self.db; + self.inference.map(|inference| inference.specialization(db)) } fn infer_specialization(&mut self, constraints: &ConstraintSetBuilder<'db>) { + let db = self.db; let Some(generic_context) = self.signature.generic_context else { return; }; let return_with_tcx = Some(self.return_ty).zip(self.call_expression_tcx.annotation); - self.inferable_typevars = generic_context.inferable_typevars(self.db); - let mut builder = SpecializationBuilder::new(self.db, constraints, self.inferable_typevars); + self.inferable_typevars = generic_context.inferable_typevars(db); + let mut builder = + SpecializationBuilder::new(db, self.env, constraints, self.inferable_typevars); // Type variables for which we inferred a declared type based on a partially specialized // type from an outer generic context. For these type variables, we may infer types that @@ -5353,17 +5536,19 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let preferred_type_mappings = return_with_tcx .and_then(|(return_ty, tcx)| { if !tcx - .filter_union(self.db, |ty| ty.may_prefer_declared_type(self.db)) - .may_prefer_declared_type(self.db) + .filter_union(db, |ty| ty.may_prefer_declared_type(db, self.env)) + .may_prefer_declared_type(db, self.env) { return None; } let return_ty = - return_ty.filter_disjoint_elements(self.db, tcx, self.inferable_typevars); - let tcx = tcx.filter_disjoint_elements(self.db, return_ty, self.inferable_typevars); + return_ty.filter_disjoint_elements(db, self.env, tcx, self.inferable_typevars); + let tcx = + tcx.filter_disjoint_elements(db, self.env, return_ty, self.inferable_typevars); let path_bounds = return_ty.assignable_solutions_with_inferable( - self.db, + db, + self.env, tcx, self.inferable_typevars, ); @@ -5373,12 +5558,12 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let mut variance_map: FxHashMap, TypeVarVariance> = FxHashMap::default(); let solutions = path_bounds.solve_with(|variance, path_bound| { - let identity = path_bound.bound_typevar.identity(self.db); + let identity = path_bound.bound_typevar.identity(db); variance_map .entry(identity) .and_modify(|current| *current = current.join(variance)) .or_insert(variance); - PathBounds::default_solve(self.db, constraints, path_bound) + PathBounds::default_solve(db, self.env, constraints, path_bound) }); let Solutions::Constrained(solutions) = solutions else { @@ -5390,7 +5575,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for solution in &solutions { for binding in solution { - let identity = binding.bound_typevar.identity(self.db); + let identity = binding.bound_typevar.identity(db); // Avoid unnecessarily widening the return type based on a covariant // type parameter from the type context, as it can lead to argument @@ -5411,14 +5596,14 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { binding.bound_typevar, binding.solution, ) - .filter_union(self.db, |ty| { - if ty.has_unspecialized_type_var(self.db) { + .filter_union(db, |ty| { + if ty.has_unspecialized_type_var(db, self.env) { partially_specialized_declared_type.insert(identity); return false; } true }); - if inferred_ty.has_unspecialized_type_var(self.db) { + if inferred_ty.has_unspecialized_type_var(db, self.env) { continue; } @@ -5427,28 +5612,30 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // `T@h | list[T@h]` from an outer generic scope) don't provide // useful concrete information and would cause over-expansion. let concrete_content = - inferred_ty.filter_union(self.db, |ty| !ty.has_typevar(self.db)); - if concrete_content.is_never() && inferred_ty.has_typevar(self.db) { + inferred_ty.filter_union(db, |ty| !ty.has_typevar(db, self.env)); + if concrete_content.is_never() && inferred_ty.has_typevar(db, self.env) { continue; } preferred .entry(identity) - .and_modify(|existing| existing.add(self.db, inferred_ty)) + .and_modify(|existing| { + existing.add(db, self.env, inferred_ty); + }) .or_insert_with(|| UnionAccumulator::new(inferred_ty)); } } let preferred: FxHashMap, Type<'db>> = preferred .into_iter() - .map(|(identity, accumulator)| (identity, accumulator.into_type(self.db))) + .map(|(identity, accumulator)| (identity, accumulator.into_type(db, self.env))) .collect(); // Add preferred types to the builder so they serve as the base mapping // when argument inference adds more types. for solution in &solutions { for binding in solution { - let identity = binding.bound_typevar.identity(self.db); + let identity = binding.bound_typevar.identity(db); if let Some(&ty) = preferred.get(&identity) { builder.add_type_mapping( binding.bound_typevar, @@ -5477,7 +5664,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // Note that this will still lead to an invalid specialization, but may // produce more precise diagnostics. if !assignable_to_declared_type { - builder = SpecializationBuilder::new(self.db, constraints, self.inferable_typevars); + builder = + SpecializationBuilder::new(db, self.env, constraints, self.inferable_typevars); specialization_errors.clear(); self.constraint_set_errors.fill(false); @@ -5495,7 +5683,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // The hook receives (typevar, bounds) and returns Some(ty) to override the default // solution, or None to keep it. let maybe_promote = |typevar: BoundTypeVarInstance<'db>, bounds: &PathBound<'db>| { - let bound_or_constraints = typevar.typevar(self.db).bound_or_constraints(self.db); + let bound_or_constraints = typevar.typevar(db).bound_or_constraints(db, self.env); // For constrained TypeVars, the inferred type is already one of the // constraints. Promoting literals would produce a type that doesn't @@ -5511,7 +5699,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // Find all occurrences of the type variable in the return type. self.return_ty - .visit_specialization(self.db, |ty, variance| { + .visit_specialization(db, self.env, |ty, variance| { if ty != Type::TypeVar(typevar) { return; } @@ -5526,12 +5714,12 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } let lower = bounds.lower?; - let promoted = lower.promote(self.db); + let promoted = lower.promote(db, self.env); // If the TypeVar has an upper bound, only use the promoted type if it // still satisfies the bound. if let Some(TypeVarBoundOrConstraints::UpperBound(bound)) = bound_or_constraints { - if !promoted.is_assignable_to(self.db, bound) { + if !promoted.is_assignable_to(db, self.env, bound) { return None; } } @@ -5542,8 +5730,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let mut choose = |typevar: BoundTypeVarInstance<'db>, bounds: Option<&PathBound<'db>>| { let bounds = bounds?; if let Some(lower) = bounds.lower - && let Some(&preferred_ty) = preferred_type_mappings.get(&typevar.identity(self.db)) - && lower.is_assignable_to(self.db, preferred_ty) + && let Some(&preferred_ty) = preferred_type_mappings.get(&typevar.identity(db)) + && lower.is_assignable_to(db, self.env, preferred_ty) { return Some(preferred_ty); } @@ -5573,9 +5761,9 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { builder.build_diagnostic_inference_with(generic_context, argument_relations, choose) } }; - let specialization = inference.specialization(self.db); + let specialization = inference.specialization(db); - self.return_ty = self.return_ty.apply_specialization(self.db, specialization); + self.return_ty = self.return_ty.apply_specialization(db, specialization); self.inference = Some(inference); } @@ -5586,6 +5774,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { partially_specialized_declared_type: &FxHashSet>, specialization_errors: &mut Vec>, ) -> bool { + let db = self.db; let parameters = self.signature.parameters(); for (argument_index, adjusted_argument_index, _, argument_types) in self.enumerate_argument_types() @@ -5599,9 +5788,9 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { if parameter.has_starred_annotation() && (matches!( declared_type, - Type::TypeVar(typevar) if typevar.is_typevartuple(self.db) + Type::TypeVar(typevar) if typevar.is_typevartuple(db) ) || matches!( - declared_type.exact_tuple_instance_spec(self.db).as_deref(), + declared_type.exact_tuple_instance_spec(db).as_deref(), Some(TupleSpec::Variable(variable)) if matches!( variable.variable(), @@ -5648,6 +5837,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { mut argument_type: Type<'db>, matched_parameter: MatchedParameter<'db>, ) { + let db = self.db; let parameter_index = matched_parameter.index; let parameters = self.signature.parameters(); let parameter = ¶meters[parameter_index]; @@ -5691,16 +5881,16 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let constructor_receiver = matches!(argument, Argument::Synthetic) && self.constructor_kind == Some(ConstructorCallableKind::New) && matches!( - parameter.annotated_type().resolve_type_alias(self.db), + parameter.annotated_type().resolve_type_alias(db), Type::SubclassOf(subclass_of) if subclass_of.into_type_var().is_some() ); let mut expected_ty = parameter.annotated_type(); if let Some(specialization) = self.specialization() { if !constructor_receiver { - argument_type = argument_type.apply_specialization(self.db, specialization); + argument_type = argument_type.apply_specialization(db, specialization); } - expected_ty = expected_ty.apply_specialization(self.db, specialization); + expected_ty = expected_ty.apply_specialization(db, specialization); } // Some typing special forms are valid class-info arguments at runtime but are not @@ -5711,7 +5901,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { && matches!( self.signature_type .as_function_literal() - .and_then(|function| function.known(self.db)), + .and_then(|function| function.known(db)), Some(KnownFunction::IsInstance | KnownFunction::IsSubclass) ) && argument_type @@ -5738,8 +5928,14 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { && !parameter.has_starred_annotation() && !is_valid_isinstance_target() && argument_type - .when_assignable_to(self.db, expected_ty, constraints, self.inferable_typevars) - .is_never_satisfied(self.db) + .when_assignable_to( + db, + self.env, + expected_ty, + constraints, + self.inferable_typevars, + ) + .is_never_satisfied(db, self.env) && !self.should_defer_typevartuple_callable_check( parameter.annotated_type(), expected_ty, @@ -5776,7 +5972,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { { builder.add_in_place(argument_type); } else if let Some(existing) = self.parameter_tys[parameter_index] { - let mut builder = UnionBuilder::new(self.db); + let mut builder = UnionBuilder::new(db, self.env); builder.add_in_place(existing); builder.add_in_place(argument_type); if self.parameter_ty_builders.is_empty() { @@ -5807,16 +6003,17 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { expected_type: Type<'db>, argument_type: Type<'db>, ) -> bool { - let Some(declared_callables) = declared_type.try_upcast_to_callable(self.db) else { + let db = self.db; + let Some(declared_callables) = declared_type.try_upcast_to_callable(db, self.env) else { return false; }; let parameters_contain_typevartuple = declared_callables.iter().any(|callable| { - callable.signatures(self.db).iter().any(|signature| { + callable.signatures(db).iter().any(|signature| { signature.parameters().iter().any(|parameter| { - any_over_type(self.db, parameter.annotated_type(), false, |ty| { + any_over_type(db, self.env, parameter.annotated_type(), false, |ty| { matches!( ty, - Type::TypeVar(typevar) if typevar.is_typevartuple(self.db) + Type::TypeVar(typevar) if typevar.is_typevartuple(db) ) }) }) @@ -5826,28 +6023,28 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { return false; } - let Some(argument_callables) = argument_type.try_upcast_to_callable(self.db) else { + let Some(argument_callables) = argument_type.try_upcast_to_callable(db, self.env) else { return false; }; if argument_callables .iter() - .any(|callable| callable.signatures(self.db).overloads.len() > 1) + .any(|callable| callable.signatures(db).overloads.len() > 1) { return true; } let argument_is_generic = argument_callables.iter().any(|callable| { callable - .signatures(self.db) + .signatures(db) .iter() .any(|signature| signature.generic_context.is_some()) }); argument_is_generic && expected_type - .try_upcast_to_callable(self.db) + .try_upcast_to_callable(db, self.env) .is_some_and(|callables| { callables.iter().any(|callable| { - callable.signatures(self.db).iter().any(|signature| { + callable.signatures(db).iter().any(|signature| { signature .parameters() .variadic() @@ -5860,6 +6057,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } fn check_argument_types(&mut self, constraints: &ConstraintSetBuilder<'db>) { + let db = self.db; let paramspec = self.signature.parameters().as_paramspec_with_prefix(); let paramspec_component_start = paramspec.and_then(|(prefix, paramspec)| { let prefix_len = prefix.len(); @@ -5885,7 +6083,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { return false; }; - typevar.is_paramspec(self.db) + typevar.is_paramspec(db) }); if has_paramspec_component_argument @@ -5982,18 +6180,19 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { paramspec_arguments: Option<&[(usize, Option)]>, paramspec: BoundTypeVarInstance<'db>, ) -> bool { + let db = self.db; let Some(Type::Callable(callable)) = self .specialization() - .and_then(|specialization| specialization.get(self.db, paramspec)) + .and_then(|specialization| specialization.get(db, paramspec)) else { return false; }; - if callable.kind(self.db) != CallableTypeKind::ParamSpecValue { + if callable.kind(db) != CallableTypeKind::ParamSpecValue { return false; } - let signatures = &callable.signatures(self.db).overloads; + let signatures = &callable.signatures(db).overloads; if signatures.is_empty() { return false; } @@ -6015,9 +6214,10 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let callable_binding = CallableBinding::from_overloads(self.signature_type, signatures.iter().cloned()); let bindings = match Bindings::from(callable_binding) - .match_parameters(self.db, &sub_arguments) + .match_parameters(db, self.env, &sub_arguments) .check_types( - self.db, + db, + self.env, constraints, &sub_arguments, self.call_expression_tcx, @@ -6054,7 +6254,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { { if let Some(parameter_source) = parameter_source && parameter_source - .source_parameter_index(self.db, parameter) + .source_parameter_index(db, parameter) .is_some() { *error_parameter_source = Some(parameter_source); @@ -6142,7 +6342,8 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { argument_type: Type<'db>, paramspec_component_start: Option, ) { - if extract_unpacked_typed_dict_from_value_type(self.db, argument_type).is_some() { + let db = self.db; + if extract_unpacked_typed_dict_from_value_type(db, self.env, argument_type).is_some() { self.check_variadic_argument_type( constraints, argument_index, @@ -6153,28 +6354,28 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { return; } - let value_type_paramspec = - if let Some(paramspec) = argument_type.as_paramspec_typevar(self.db) { - Some(paramspec) - } else { - match validate_keyword_unpack_key_type( - self.db, - constraints, - argument_type, - self.inferable_typevars, - ) { - KeywordUnpackKeyTypeCheck::NotApplicable => return, - KeywordUnpackKeyTypeCheck::Valid => {} - KeywordUnpackKeyTypeCheck::Invalid(provided_ty) => { - self.errors.push(BindingError::InvalidKeyType { - argument_index: adjusted_argument_index, - provided_ty, - }); - } + let value_type_paramspec = if let Some(paramspec) = argument_type.as_paramspec_typevar(db) { + Some(paramspec) + } else { + match validate_keyword_unpack_key_type( + db, + self.env, + constraints, + argument_type, + self.inferable_typevars, + ) { + KeywordUnpackKeyTypeCheck::NotApplicable => return, + KeywordUnpackKeyTypeCheck::Valid => {} + KeywordUnpackKeyTypeCheck::Invalid(provided_ty) => { + self.errors.push(BindingError::InvalidKeyType { + argument_index: adjusted_argument_index, + provided_ty, + }); } + } - None - }; + None + }; for matched_parameter in self.argument_matches[argument_index].iter() { let parameter_index = matched_parameter.index; @@ -6190,7 +6391,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .map(Name::as_str); argument_type - .getitem_dunder_call(self.db, parameter_name) + .getitem_dunder_call(db, self.env, parameter_name) .unwrap_or(Type::unknown()) }; @@ -6364,9 +6565,10 @@ impl<'db> ArgumentTypeContext<'db> { #[derive(Debug, Clone, Copy)] pub(crate) struct UnknownParameterNameError; -#[derive(Clone, Copy)] +#[derive(Clone)] struct ParamSpecArgumentContext<'a, 'call, 'db> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, constraints: &'a ConstraintSetBuilder<'db>, binding: &'a CallableBinding<'db>, callable: CallableType<'db>, @@ -6378,16 +6580,22 @@ struct ParamSpecArgumentContext<'a, 'call, 'db> { /// Returns the number of occurrences of inferable type variables in the provided type. fn inferable_typevar_occurrences<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, inferable: TypeVarSet<'db>, ) -> usize { - struct InferableTypeVarVisitor<'db> { + struct InferableTypeVarVisitor<'a, 'db> { + env: &'a ProgramEnvironment<'db>, inferable: TypeVarSet<'db>, count: Cell, stack: RefCell; 8]>>, } - impl<'db> TypeVisitor<'db> for InferableTypeVarVisitor<'db> { + impl<'db> TypeVisitor<'db> for InferableTypeVarVisitor<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -6419,6 +6627,7 @@ fn inferable_typevar_occurrences<'db>( } let visitor = InferableTypeVarVisitor { + env, inferable, count: Cell::new(0), stack: RefCell::default(), @@ -6485,20 +6694,21 @@ impl<'db> Binding<'db> { fn check_property_setter( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, setter: Type<'db>, instance: Type<'db>, value: Type<'db>, argument_index_offset: usize, ) { - match setter.try_call(db, &CallArguments::positional([instance, value])) { + match setter.try_call(db, env, &CallArguments::positional([instance, value])) { Ok(bindings) => { - let return_ty = bindings.return_type(db); + let return_ty = bindings.return_type(db, env); // `property.__set__` returns `None` for ordinary setters, but preserving `Never` // keeps non-returning setters divergent. self.set_return_type(if return_ty.is_never() { return_ty } else { - Type::none(db) + Type::none(db, env) }); } Err(CallError(_, bindings)) => { @@ -6550,6 +6760,7 @@ impl<'db> Binding<'db> { pub(crate) fn typevar_occurrences_for_parameter( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding: &CallableBinding<'db>, argument_index: usize, ) -> usize { @@ -6568,6 +6779,7 @@ impl<'db> Binding<'db> { .map(|parameter| { inferable_typevar_occurrences( db, + env, self.signature.parameters()[parameter.index].annotated_type(), inferable_typevars, ) @@ -6622,17 +6834,19 @@ impl<'db> Binding<'db> { /// ``` fn paramspec_argument_context( &self, - context: ParamSpecArgumentContext<'_, '_, 'db>, + context: &ParamSpecArgumentContext<'_, '_, 'db>, ) -> Option> { let ParamSpecArgumentContext { db, + env, constraints, binding, callable, arguments_types, argument_index, call_expression_tcx, - } = context; + } = *context; + let (prefix, _) = self.signature.parameters().as_paramspec_with_prefix()?; let paramspec_argument_indices = self.paramspec_call_argument_indices(binding, prefix.len()); @@ -6651,9 +6865,10 @@ impl<'db> Binding<'db> { sub_arguments.clear_types(sub_argument_index); let mut specialized_bindings = - Bindings::from(specialized_binding).match_parameters(db, &sub_arguments); + Bindings::from(specialized_binding).match_parameters(db, env, &sub_arguments); let _ = specialized_bindings.check_types_impl( db, + env, constraints, &sub_arguments, call_expression_tcx, @@ -6684,8 +6899,9 @@ impl<'db> Binding<'db> { parameter_type.apply_specialization(db, specialization) }); - (!parameter_type.has_dynamic(db) && !parameter_type.has_typevar_or_typevar_instance(db)) - .then_some(parameter_type) + (!parameter_type.has_dynamic(db, env) + && !parameter_type.has_typevar_or_typevar_instance(db, env)) + .then_some(parameter_type) } /// Returns the type context to use for bidirectional inference of a source call argument, @@ -6703,6 +6919,7 @@ impl<'db> Binding<'db> { pub(crate) fn argument_type_context( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, binding: &CallableBinding<'db>, arguments_types: &CallArguments<'_, 'db>, @@ -6739,7 +6956,7 @@ impl<'db> Binding<'db> { if let Type::TypeVar(typevar) = parameter_type && !typevar.is_paramspec(db) && let Some(TypeVarBoundOrConstraints::UpperBound(bound)) = - typevar.typevar(db).bound_or_constraints(db) + typevar.typevar(db).bound_or_constraints(db, env) { return Some(ArgumentTypeContext::standard( original_parameter_type, @@ -6763,8 +6980,9 @@ impl<'db> Binding<'db> { if let Some(paramspec) = paramspec && let Some(callable) = paramspec_callable(paramspec) && let Some(specialized_parameter_type) = - self.paramspec_argument_context(ParamSpecArgumentContext { + self.paramspec_argument_context(&ParamSpecArgumentContext { db, + env, constraints, binding, callable, @@ -6797,6 +7015,7 @@ impl<'db> Binding<'db> { pub(crate) fn argument_type_context_specialization( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, call_expression_tcx: TypeContext<'db>, ) -> Option> { @@ -6810,19 +7029,24 @@ impl<'db> Binding<'db> { .unwrap_or(self.signature.return_ty); let path_bounds = normalized_return_ty.assignable_solutions_with_inferable( db, + env, declared_return_ty, generic_context.inferable_typevars(db), ); - if let Solutions::Constrained(solutions) = path_bounds.solve(db, constraints) { + if let Solutions::Constrained(solutions) = path_bounds.solve(db, env, constraints) { for solution in solutions { for binding in solution { let identity = binding.bound_typevar.identity(db); return_type_solutions .entry(identity) .and_modify(|existing| { - *existing = - UnionType::from_two_elements(db, *existing, binding.solution); + *existing = UnionType::from_two_elements( + db, + env, + *existing, + binding.solution, + ); }) .or_insert(binding.solution); } @@ -6846,8 +7070,8 @@ impl<'db> Binding<'db> { let argument_constraints = self .specialization(db) .and_then(|specialization| specialization.get(db, typevar)) - .filter(|ty| !ty.has_dynamic(db)) - .map(|ty| ty.promote(db)); + .filter(|ty| !ty.has_dynamic(db, env)) + .map(|ty| ty.promote(db, env)); // TODO: We should similarly combine both the call expression and argument constraints // here. We currently only rely on argument constraints when there is no explicit declared @@ -6881,16 +7105,25 @@ impl<'db> Binding<'db> { } } - fn freshen_bound_typevars(&mut self, db: &'db dyn Db, delta: u32) { + fn freshen_bound_typevars( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + delta: u32, + ) { if self.signature.generic_context.is_none() { return; } - - self.signature = self.signature.freshen_bound_typevars(db, delta); + self.signature = self.signature.freshen_bound_typevars(db, env, delta); self.return_ty = self.initial_return_type(db); } - fn match_parameters(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { + fn match_parameters( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + arguments: &CallArguments<'_, 'db>, + ) { let parameters = self.signature.parameters(); let mut matcher = ArgumentMatcher::new(arguments, parameters, &mut self.errors); let mut keywords_arguments = vec![]; @@ -6905,6 +7138,7 @@ impl<'db> Binding<'db> { Argument::Variadic => { let _ = matcher.match_variadic( db, + env, argument_index, argument, // Splatted arguments are inferred without type context. @@ -6919,6 +7153,7 @@ impl<'db> Binding<'db> { for (keywords_index, keywords_type) in keywords_arguments { matcher.match_keyword_variadic( db, + env, keywords_index, // Splatted arguments are inferred without type context. keywords_type.get_default(), @@ -6933,6 +7168,7 @@ impl<'db> Binding<'db> { fn check_types( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, arguments: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, @@ -6951,12 +7187,13 @@ impl<'db> Binding<'db> { .iter() .all(|parameter| parameter.is_variadic() || parameter.is_keyword_variadic()) { - self.check_keyword_unpack_key_types(db, constraints, arguments); + self.check_keyword_unpack_key_types(db, env, constraints, arguments); return; } let mut checker = ArgumentTypeChecker::new( db, + env, self.signature_type, self.constructor_context.map(ConstructorContext::kind), &self.signature, @@ -6979,6 +7216,7 @@ impl<'db> Binding<'db> { fn check_keyword_unpack_key_types( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, arguments: &CallArguments<'_, 'db>, ) { @@ -6998,7 +7236,13 @@ impl<'db> Binding<'db> { let argument_type = argument_types.get_default().unwrap_or(Type::unknown()); if let KeywordUnpackKeyTypeCheck::Invalid(provided_ty) = - validate_keyword_unpack_key_type(db, constraints, argument_type, TypeVarSet::None) + validate_keyword_unpack_key_type( + db, + env, + constraints, + argument_type, + TypeVarSet::None, + ) { self.errors.push(BindingError::InvalidKeyType { argument_index: adjusted_argument_index, @@ -7026,6 +7270,7 @@ impl<'db> Binding<'db> { fn functools_partial_return_type<'a>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, call_arguments: &CallArguments<'a, 'db>, ) -> Option> { // `partial(...)` receives the wrapped callable as its first explicit argument (after @@ -7036,15 +7281,16 @@ impl<'db> Binding<'db> { }; let imprecise_return_type = self.return_ty; let failed_synthesis_return_type = - KnownClass::FunctoolsPartial.to_specialized_instance(db, &[Type::unknown()]); + KnownClass::FunctoolsPartial.to_specialized_instance(db, env, &[Type::unknown()]); let (bound_call_arguments, partial_bindings, can_synthesize_signature) = - Bindings::functools_partial_matched_bindings(db, func_ty, call_arguments)?; + Bindings::functools_partial_matched_bindings(db, env, func_ty, call_arguments)?; // Reuse call-binding machinery to resolve which wrapped overloads are compatible with // bound arguments and to surface binding diagnostics. let partial_bindings = match partial_bindings.check_types( db, + env, &ConstraintSetBuilder::new(), &bound_call_arguments, TypeContext::default(), @@ -7054,7 +7300,7 @@ impl<'db> Binding<'db> { Err(CallError(_, bindings)) => *bindings, }; let new_return_type = - partial_bindings.functools_partial_type(db, func_ty, self, &bound_call_arguments); + partial_bindings.functools_partial_type(db, env, func_ty, self, &bound_call_arguments); Some(if !can_synthesize_signature { imprecise_return_type @@ -7142,8 +7388,8 @@ impl<'db> Binding<'db> { /// Packages the information needed to synthesize this overload's reduced partial signature. fn partial_signature_application( &self, - arguments: &CallArguments<'_, 'db>, db: &'db dyn Db, + arguments: &CallArguments<'_, 'db>, ) -> PartialSignatureApplication<'db> { PartialSignatureApplication::new( self.signature.clone(), @@ -7412,8 +7658,7 @@ impl<'db> CallableDescription<'db> { db: &'db dyn Db, function: FunctionType<'db>, ) -> Cow<'db, str> { - let file = function.file(db); - let semantic_index = semantic_index(db, file); + let semantic_index = semantic_index(db, function.python_file(db)); let enclosing_scope = semantic_index.scope(function.definition(db).file_scope(db)); if let Some(class_node) = enclosing_scope.node().as_class() && let Some(class) = @@ -7905,6 +8150,8 @@ impl<'db> BindingError<'db> { matching_overload: Option<&MatchingOverloadLiteral<'_>>, source_parameter_index_offset: usize, ) { + let db = context.db(); + let env = context.program_environment(); let callable_kind = match callable_ty { Type::FunctionLiteral(_) => "Function", Type::BoundMethod(_) => "Method", @@ -7931,12 +8178,13 @@ impl<'db> BindingError<'db> { }; let display_settings = DisplaySettings::from_possibly_ambiguous_types( - context.db(), + db, + env, [provided_ty, expected_ty], ); let provided_ty_display = - provided_ty.display_with(context.db(), display_settings.clone()); - let expected_ty_display = expected_ty.display_with(context.db(), display_settings); + provided_ty.display_with(db, env, display_settings.clone()); + let expected_ty_display = expected_ty.display_with(db, env, display_settings); let mut diag = builder.into_diagnostic(format_args!( "Argument{} is incorrect", @@ -7958,13 +8206,12 @@ impl<'db> BindingError<'db> { )); } - let error_context = - provided_ty.assignability_error_context(context.db(), *expected_ty); - error_context.attach_to(context.db(), &mut diag); + let error_context = provided_ty.assignability_error_context(db, env, *expected_ty); + error_context.attach_to(db, env, &mut diag); if let Some(parameter_source) = parameter_source { let (name_span, parameter_span) = - parameter_source.parameter_span(context.db(), parameter); + parameter_source.parameter_span(db, parameter); let callable_kind = if parameter_source.is_bound_method { "Method" } else { @@ -7982,7 +8229,7 @@ impl<'db> BindingError<'db> { } if let Some(matching_overload) = matching_overload { - if let Some(overload_literal) = matching_overload.get(context.db()) { + if let Some(overload_literal) = matching_overload.get(db) { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, "Matching overload defined here", @@ -7996,7 +8243,7 @@ impl<'db> BindingError<'db> { matches!(argument, ArgOrKeyword::Arg(_)) }); overload_literal - .signature(context.db()) + .signature(db) .parameters() .iter() .position(|candidate| { @@ -8026,7 +8273,7 @@ impl<'db> BindingError<'db> { matching_overload.function.name(context.db()) )); for (overload_index, overload) in matching_overload - .candidate_overloads(context.db()) + .candidate_overloads(db) .take(MAXIMUM_OVERLOADS) { if overload_index == matching_overload.index { @@ -8034,7 +8281,7 @@ impl<'db> BindingError<'db> { } diag.info(format_args!( " {}", - overload.signature(context.db()).display(context.db()) + overload.signature(db).display(db, env) )); } if matching_overload.candidate_count() > MAXIMUM_OVERLOADS { @@ -8062,25 +8309,26 @@ impl<'db> BindingError<'db> { } if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } // If the type comes from first-party code, the user may have some control over // the parameter annotation; provide additional context to help them fix it. if callable_ty - .definition(context.db()) + .definition(db, env) .and_then(|definition| definition.file(context.db())) .is_some_and(|file| context.db().should_check_file(file)) { note_numbers_module_not_supported( - context.db(), + db, + env, &mut diag, *expected_ty, *provided_ty, ); } - add_invariant_generic_hints(context.db(), &mut diag, *expected_ty, *provided_ty); + add_invariant_generic_hints(db, env, &mut diag, *expected_ty, *provided_ty); } Self::InvalidKeyType { @@ -8091,15 +8339,14 @@ impl<'db> BindingError<'db> { let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else { return; }; - - let provided_ty_display = provided_ty.display(context.db()); + let provided_ty_display = provided_ty.display(db, env); let mut diag = builder.into_diagnostic( "Argument expression after ** must be a mapping with `str` key type", ); diag.set_primary_annotation_message(format_args!("Found `{provided_ty_display}`")); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } @@ -8118,7 +8365,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } else if let Some(spans) = callable_ty.function_spans(context.db()) { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -8144,7 +8391,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } else { let span = callable_ty.parameter_span( context.db(), @@ -8188,7 +8435,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } else if let Some(spans) = callable_ty.function_spans(context.db()) { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -8210,7 +8457,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } else if let Some(spans) = callable_ty.function_spans(context.db()) { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -8237,7 +8484,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } else if let Some(spans) = callable_ty.function_spans(context.db()) { let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -8262,7 +8509,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } } @@ -8275,9 +8522,8 @@ impl<'db> BindingError<'db> { let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, range) else { return; }; - let argument_type = error.argument_type(); - let argument_ty_display = argument_type.display(context.db()); + let argument_ty_display = argument_type.display(db, env); let mut diag = builder.into_diagnostic(format_args!( "Argument{} is incorrect", @@ -8294,11 +8540,11 @@ impl<'db> BindingError<'db> { "Argument type `{argument_ty_display}` does not \ satisfy upper bound `{}` of type variable `{typevar_name}`", typevar - .upper_bound(context.db()) + .upper_bound(db, env) .expect( "type variable should have an upper bound if this error occurs" ) - .display(context.db()) + .display(db, env) )); } SpecializationError::MismatchedConstraint { bound_typevar, .. } => { @@ -8308,14 +8554,14 @@ impl<'db> BindingError<'db> { "Argument type `{argument_ty_display}` does not \ satisfy constraints ({}) of type variable `{typevar_name}`", typevar - .constraints(context.db()) + .constraints(db, env) .expect( "type variable should have constraints if this error occurs" ) .iter() .format_with(", ", |ty, f| f(&format_args!( "`{}`", - ty.display(context.db()) + ty.display(db, env) ))) )); } @@ -8326,8 +8572,9 @@ impl<'db> BindingError<'db> { .typevar(context.db()) .definition(context.db()) { - let module = parsed_module(context.db(), typevar_definition.file(context.db())) - .load(context.db()); + let module = + parsed_module(context.db(), typevar_definition.python_file(context.db())) + .load(context.db()); let typevar_range = typevar_definition.full_range(context.db(), &module); let mut sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, @@ -8338,7 +8585,7 @@ impl<'db> BindingError<'db> { } if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } @@ -8385,7 +8632,7 @@ impl<'db> BindingError<'db> { .unwrap_or_default() )); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } } @@ -8395,7 +8642,7 @@ impl<'db> BindingError<'db> { Self::CalledTopCallable(callable_ty) => { let range = context.get_range(node, None); if let Some(builder) = context.report_lint(&CALL_TOP_CALLABLE, range) { - let callable_ty_display = callable_ty.display(context.db()); + let callable_ty_display = callable_ty.display(db, env); let mut diag = builder.into_diagnostic(format_args!( "Object of type `{callable_ty_display}` is not safe to call; \ its signature is not known" @@ -8405,7 +8652,7 @@ impl<'db> BindingError<'db> { because there is no valid set of arguments for it", ); if let Some(compound_diag) = compound_diag { - compound_diag.add_context(context.db(), &mut diag); + compound_diag.add_context(db, env, &mut diag); } } } @@ -8503,7 +8750,7 @@ impl<'db> BindingError<'db> { /// Trait for adding context about compound types (unions/intersections) to diagnostics. trait CompoundDiagnostic { /// Adds context about any relevant compound type function types to the given diagnostic. - fn add_context(&self, db: &dyn Db, diag: &mut Diagnostic); + fn add_context(&self, db: &dyn Db, env: &ProgramEnvironment<'_>, diag: &mut Diagnostic); } /// Contains additional context for union specific diagnostics. @@ -8519,12 +8766,12 @@ struct UnionDiagnostic<'b, 'db> { } impl CompoundDiagnostic for UnionDiagnostic<'_, '_> { - fn add_context(&self, db: &dyn Db, diag: &mut Diagnostic) { + fn add_context(&self, db: &dyn Db, env: &ProgramEnvironment<'_>, diag: &mut Diagnostic) { let sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( "Union variant `{callable_ty}` is incompatible with this call site", - callable_ty = self.binding.callable_type.display(db), + callable_ty = self.binding.callable_type.display(db, env), ), ); diag.sub(sub); @@ -8533,7 +8780,7 @@ impl CompoundDiagnostic for UnionDiagnostic<'_, '_> { SubDiagnosticSeverity::Info, format_args!( "Attempted to call union type `{}`", - self.callable_type.display(db) + self.callable_type.display(db, env) ), ); diag.sub(sub); @@ -8553,12 +8800,12 @@ struct IntersectionDiagnostic<'b, 'db> { } impl CompoundDiagnostic for IntersectionDiagnostic<'_, '_> { - fn add_context(&self, db: &dyn Db, diag: &mut Diagnostic) { + fn add_context(&self, db: &dyn Db, env: &ProgramEnvironment<'_>, diag: &mut Diagnostic) { let sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( "Intersection element `{callable_ty}` is incompatible with this call site", - callable_ty = self.binding.callable_type.display(db), + callable_ty = self.binding.callable_type.display(db, env), ), ); diag.sub(sub); @@ -8567,7 +8814,7 @@ impl CompoundDiagnostic for IntersectionDiagnostic<'_, '_> { SubDiagnosticSeverity::Info, format_args!( "Attempted to call intersection type `{}`", - self.callable_type.display(db) + self.callable_type.display(db, env) ), ); diag.sub(sub); @@ -8588,13 +8835,13 @@ struct LayeredDiagnostic<'b, 'db> { } impl CompoundDiagnostic for LayeredDiagnostic<'_, '_> { - fn add_context(&self, db: &dyn Db, diag: &mut Diagnostic) { + fn add_context(&self, db: &dyn Db, env: &ProgramEnvironment<'_>, diag: &mut Diagnostic) { // Add intersection context first (more specific) let sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( "Intersection element `{callable_ty}` is incompatible with this call site", - callable_ty = self.binding.callable_type.display(db), + callable_ty = self.binding.callable_type.display(db, env), ), ); diag.sub(sub); @@ -8603,7 +8850,7 @@ impl CompoundDiagnostic for LayeredDiagnostic<'_, '_> { SubDiagnosticSeverity::Info, format_args!( "Attempted to call intersection type `{}`", - self.intersection_callable_type.display(db) + self.intersection_callable_type.display(db, env) ), ); diag.sub(sub); @@ -8613,7 +8860,7 @@ impl CompoundDiagnostic for LayeredDiagnostic<'_, '_> { SubDiagnosticSeverity::Info, format_args!( "Attempted to call union type `{}`", - self.union_callable_type.display(db) + self.union_callable_type.display(db, env) ), ); diag.sub(sub); @@ -8702,7 +8949,11 @@ const STRUCT_FORMAT_MAX_REPETITION: usize = 32; /// /// Returns `None` if the format contains unsupported specifiers or /// repetition counts exceed the limit, indicating a fallback to `tuple[Unknown, ...]`. -fn parse_struct_format<'db>(db: &'db dyn Db, format_string: &str) -> Option>> { +fn parse_struct_format<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + format_string: &str, +) -> Option>> { // Strip the byte order/size/alignment prefix let format = format_string.trim_start_matches(['@', '=', '<', '>', '!']); let mut chars = format.chars().peekable(); @@ -8730,15 +8981,15 @@ fn parse_struct_format<'db>(db: &'db dyn Db, format_string: &str) -> Option continue, // Pad byte: no value produced - 's' | 'p' => (KnownClass::Bytes.to_instance(db), 1), - 'c' => (KnownClass::Bytes.to_instance(db), count), + 's' | 'p' => (KnownClass::Bytes.to_instance(db, env), 1), + 'c' => (KnownClass::Bytes.to_instance(db, env), count), 'b' | 'B' | 'h' | 'H' | 'i' | 'I' | 'l' | 'L' | 'q' | 'Q' | 'n' | 'N' | 'P' => { - (KnownClass::Int.to_instance(db), count) + (KnownClass::Int.to_instance(db, env), count) } - '?' => (KnownClass::Bool.to_instance(db), count), - 'e' | 'f' | 'd' => (KnownClass::Float.to_instance(db), count), - 'F' | 'D' if Program::get(db).python_version(db) >= PythonVersion::PY314 => { - (KnownClass::Complex.to_instance(db), count) + '?' => (KnownClass::Bool.to_instance(db, env), count), + 'e' | 'f' | 'd' => (KnownClass::Float.to_instance(db, env), count), + 'F' | 'D' if env.python_version(db) >= PythonVersion::PY314 => { + (KnownClass::Complex.to_instance(db, env), count) } _ => return None, }; diff --git a/crates/ty_python_semantic/src/types/call/bind/constructor.rs b/crates/ty_python_semantic/src/types/call/bind/constructor.rs index b55b131e9f..20de4c1e17 100644 --- a/crates/ty_python_semantic/src/types/call/bind/constructor.rs +++ b/crates/ty_python_semantic/src/types/call/bind/constructor.rs @@ -1,5 +1,6 @@ use super::{Binding, Bindings, CallableBinding, CallableItem, CheckTypesMode}; -use crate::db::Db; +use crate::Db; +use crate::ProgramEnvironment; use crate::types::call::arguments::CallArguments; use crate::types::constraints::ConstraintSetBuilder; use crate::types::generics::Specialization; @@ -65,14 +66,19 @@ impl<'db> ConstructorBinding<'db> { } /// Match parameters for this constructor method and downstream constructors. - pub(super) fn match_parameters(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { - self.entry.match_parameters(db, arguments); + pub(super) fn match_parameters( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + arguments: &CallArguments<'_, 'db>, + ) { + self.entry.match_parameters(db, env, arguments); // We don't know at this point whether we'll need to check downstream constructors or not // (since we can't resolve return types yet), so we match parameters for all downstream // constructors; this may be needed for argument type contexts. if let Some(downstream) = self.downstream_constructor.as_mut() { - downstream.match_parameters_in_place(db, arguments); + downstream.match_parameters_in_place(db, env, arguments); } } @@ -83,13 +89,14 @@ impl<'db> ConstructorBinding<'db> { pub(super) fn check_types( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, argument_types: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, mode: CheckTypesMode, ) { self.entry - .check_types(db, constraints, argument_types, call_expression_tcx); + .check_types(db, env, constraints, argument_types, call_expression_tcx); // Now that we've fully checked our own callable, we can determine whether downstream // constructors should be checked or not. @@ -97,6 +104,7 @@ impl<'db> ConstructorBinding<'db> { if let Some(downstream) = self.downstream_constructor_mut() { let _ = downstream.check_types_impl( db, + env, constraints, argument_types, call_expression_tcx, @@ -104,7 +112,7 @@ impl<'db> ConstructorBinding<'db> { mode, ); } - } else if !self.should_check_downstream(db) { + } else if !self.should_check_downstream(db, env) { // If not, we can discard the downstream constructor bindings entirely. self.downstream_constructor = None; } @@ -117,7 +125,7 @@ impl<'db> ConstructorBinding<'db> { /// the overall callable, because in multiple-matching-overload cases where the overload /// resolution algorithm might just collapse to `Unknown`, we want to make a more informed /// decision based on whether all overloads return instance types, or not. - fn should_check_downstream(&self, db: &'db dyn Db) -> bool { + fn should_check_downstream(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { let constructor_kind = self.constructor_kind(); if constructor_kind.is_init() || self.downstream_constructor().is_none() { return false; @@ -129,21 +137,25 @@ impl<'db> ConstructorBinding<'db> { } let constructed_instance_type = self.constructed_instance_type(); - let constructor_class_literal = self.constructed_class_literal(db); + let constructor_class_literal = self.constructed_class_literal(db, env); // If any matching overload returns the constructed instance type itself, or an instance of // the constructed class, we need to check downstream constructors. callable.matching_overloads().any(|(_, overload)| { overload.return_ty == constructed_instance_type || constructor_class_literal.is_some_and(|class_literal| { - constructor_returns_instance(db, class_literal, overload.return_ty) + constructor_returns_instance(db, env, class_literal, overload.return_ty) }) }) } /// Discards an inactive downstream constructor. - pub(super) fn discard_downstream_constructor(&mut self, db: &'db dyn Db) -> bool { - if self.should_check_downstream(db) { + pub(super) fn discard_downstream_constructor( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + if self.should_check_downstream(db, env) { true } else { self.downstream_constructor = None; @@ -155,6 +167,7 @@ impl<'db> ConstructorBinding<'db> { pub(super) fn check_downstream_constructor( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraints: &ConstraintSetBuilder<'db>, argument_types: &CallArguments<'_, 'db>, call_expression_tcx: TypeContext<'db>, @@ -165,6 +178,7 @@ impl<'db> ConstructorBinding<'db> { // `as_result` that ultimately matter. let _ = downstream.check_types_impl( db, + env, constraints, argument_types, call_expression_tcx, @@ -207,7 +221,7 @@ impl<'db> ConstructorBinding<'db> { } /// Compute the overall effective return type of this `ConstructorBinding`. - pub(super) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { + pub(super) fn return_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { let constructed_instance_type = self.constructed_instance_type(); // If we are checking downstream constructors, and the downstream constructor resolves to a @@ -221,22 +235,23 @@ impl<'db> ConstructorBinding<'db> { // annotation. But no other type checker considers it an error, and it probably rarely if // ever comes up.) if let Some(downstream) = self.downstream_constructor() - && let Some(constructor_class_literal) = self.constructed_class_literal(db) + && let Some(constructor_class_literal) = self.constructed_class_literal(db, env) { - let downstream_return = downstream.return_type(db); - if !constructor_returns_instance(db, constructor_class_literal, downstream_return) { + let downstream_return = downstream.return_type(db, env); + if !constructor_returns_instance(db, env, constructor_class_literal, downstream_return) + { return downstream_return; } } // If `__new__` or metaclass `__call__` produced an explicit return type, use it // directly rather than building an instance of the constructed class. - if let Some(return_ty) = self.explicit_return_type(db) { + if let Some(return_ty) = self.explicit_return_type(db, env) { return return_ty; } constructed_instance_type - .apply_optional_specialization(db, self.instance_return_specialization(db)) + .apply_optional_specialization(db, self.instance_return_specialization(db, env)) } fn first_matching_overload(&self) -> Option<&Binding<'db>> { @@ -250,15 +265,19 @@ impl<'db> ConstructorBinding<'db> { /// resulting specialization can be applied either to the constructed instance type or to an /// explicit `__new__` / `__call__` return annotation that is an instance of the constructed /// type or a subclass. - fn instance_return_specialization(&self, db: &'db dyn Db) -> Option> { + fn instance_return_specialization( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let constructed_instance_type = self.constructed_instance_type(); // This will be `None` if we're constructing a non-generic class. If we're constructing a // non-specialized generic class (`C(...)`), it'll be the identity specialization. If we're // constructing an already-specialized generic alias (`C[str](...)`), it'll be the // specialization of that alias. - let (_, class_specialization) = constructed_instance_type.class_specialization(db)?; + let (_, class_specialization) = constructed_instance_type.class_specialization(db, env)?; let static_class_literal = self - .constructed_class_literal(db) + .constructed_class_literal(db, env) .and_then(ClassLiteral::as_static); let class_context = class_specialization.generic_context(db); @@ -269,7 +288,7 @@ impl<'db> ConstructorBinding<'db> { }; let return_specialization = static_class_literal // Use the already-resolved overload return type when possible. - .and_then(|lit| overload.return_ty.specialization_of(db, lit)); + .and_then(|lit| overload.return_ty.specialization_of(db, env, lit)); // TODO All this handling of return-specialization vs self-specialization is a hacky // work-around to a situation that can occur with a case like `def __init__(self: @@ -293,7 +312,7 @@ impl<'db> ConstructorBinding<'db> { .map_or(self_param_ty, |specialization| { self_param_ty.apply_specialization(db, specialization) }); - resolved_self_param_ty.specialization_of(db, lit) + resolved_self_param_ty.specialization_of(db, env, lit) }); let refined_self_parameter_specialization = self_parameter_specialization.map(|specialization| { @@ -309,7 +328,7 @@ impl<'db> ConstructorBinding<'db> { } else { without_unknown }; - mapped_ty.promote(db) + mapped_ty.promote(db, env) }) .collect(); Specialization::new( @@ -364,8 +383,12 @@ impl<'db> ConstructorBinding<'db> { /// /// This must be called only after downstream constructor bindings have been type-checked, /// because instance-returning constructor paths may incorporate downstream specializations. - fn explicit_return_type(&self, db: &'db dyn Db) -> Option> { - if self.constructor_kind().is_init() || self.constructed_class_literal(db).is_none() { + fn explicit_return_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + if self.constructor_kind().is_init() || self.constructed_class_literal(db, env).is_none() { return None; } @@ -378,9 +401,9 @@ impl<'db> ConstructorBinding<'db> { // consider all overloads' return types. (This increases the chances of an `Unknown` // return, but still preserves more precise returns in unambiguous cases.) if matching_overloads.clone().next().is_none() { - self.analyze_overload_returns(db, self.callable().overloads().iter()) + self.analyze_overload_returns(db, env, self.callable().overloads().iter()) } else { - self.analyze_overload_returns(db, matching_overloads) + self.analyze_overload_returns(db, env, matching_overloads) } } @@ -389,6 +412,7 @@ impl<'db> ConstructorBinding<'db> { fn analyze_overload_returns<'a>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overloads: impl IntoIterator>, ) -> Option> where @@ -405,7 +429,7 @@ impl<'db> ConstructorBinding<'db> { let mut saw_instance_return = false; let mut non_instance_return = None; for overload in overloads { - let (return_ty, is_instance_return) = self.single_overload_return(db, overload); + let (return_ty, is_instance_return) = self.single_overload_return(db, env, overload); if is_instance_return { if saw_instance_return { sole_instance_return = None; @@ -440,6 +464,7 @@ impl<'db> ConstructorBinding<'db> { fn single_overload_return( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overload: &Binding<'db>, ) -> (Type<'db>, bool) { let return_ty = overload @@ -447,16 +472,20 @@ impl<'db> ConstructorBinding<'db> { .apply_optional_specialization( db, overload.specialization(db).map(|specialization| { - self.unspecialize_class_type_variables(db, specialization) + self.unspecialize_class_type_variables(db, env, specialization) }), ); if self - .constructed_class_literal(db) - .is_some_and(|class_literal| constructor_returns_instance(db, class_literal, return_ty)) + .constructed_class_literal(db, env) + .is_some_and(|class_literal| { + constructor_returns_instance(db, env, class_literal, return_ty) + }) { return ( - return_ty - .apply_optional_specialization(db, self.instance_return_specialization(db)), + return_ty.apply_optional_specialization( + db, + self.instance_return_specialization(db, env), + ), true, ); } @@ -479,11 +508,12 @@ impl<'db> ConstructorBinding<'db> { fn unspecialize_class_type_variables( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Specialization<'db>, ) -> Specialization<'db> { let Some(class_context) = self .constructed_instance_type() - .class_specialization(db) + .class_specialization(db, env) .map(|(_, specialization)| specialization.generic_context(db)) else { return specialization; @@ -516,11 +546,15 @@ impl<'db> ConstructorBinding<'db> { ) } - fn constructed_class_literal(&self, db: &'db dyn Db) -> Option> { + fn constructed_class_literal( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { self.constructed_instance_type() .as_nominal_instance() // TODO may need to handle `Type::KnownInstance` here as well? - .map(|instance| instance.class(db).class_literal(db)) + .map(|instance| instance.class(db, env).class_literal(db)) } fn constructor_kind(&self) -> ConstructorCallableKind { @@ -580,6 +614,7 @@ impl ConstructorCallableKind { /// explicit `Any` is considered "not an instance", but an `Unknown` is considered "an instance". fn constructor_returns_instance<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class_literal: ClassLiteral<'db>, return_ty: Type<'db>, ) -> bool { @@ -587,10 +622,10 @@ fn constructor_returns_instance<'db>( Type::Union(union) => union .elements(db) .iter() - .all(|element| constructor_returns_instance(db, class_literal, *element)), + .all(|element| constructor_returns_instance(db, env, class_literal, *element)), Type::Intersection(intersection) => intersection .iter_positive(db) - .any(|element| constructor_returns_instance(db, class_literal, element)), + .any(|element| constructor_returns_instance(db, env, class_literal, element)), // Spec says an explicit `Any` return type should be considered non-instance. Type::Dynamic(DynamicType::Any) => false, // But a missing return annotation should be considered instance. @@ -600,7 +635,7 @@ fn constructor_returns_instance<'db>( // A `Never` constructor return is terminal and does not run downstream construction. Type::Never => false, Type::NominalInstance(instance) => instance - .class(db) + .class(db, env) .is_subtype_of_class_literal(db, class_literal), // We don't need to handle `ProtocolInstance` here, since the only way a protocol can be // instantiated is if a nominal class inherits it. If the nominal class inherits a diff --git a/crates/ty_python_semantic/src/types/call/bind/enum_property.rs b/crates/ty_python_semantic/src/types/call/bind/enum_property.rs index 1632373f06..b897cb7205 100644 --- a/crates/ty_python_semantic/src/types/call/bind/enum_property.rs +++ b/crates/ty_python_semantic/src/types/call/bind/enum_property.rs @@ -1,9 +1,8 @@ -use itertools::Itertools; - use super::Bindings; use crate::db::Db; use crate::types::call::CallArguments; use crate::types::{KnownClass, PropertyInstanceType, Type}; +use itertools::Itertools; impl<'db> Bindings<'db> { /// Replaces constructed `enum.property` instances with the property type derived from their diff --git a/crates/ty_python_semantic/src/types/callable.rs b/crates/ty_python_semantic/src/types/callable.rs index cba3d822e0..a7ccddfdc7 100644 --- a/crates/ty_python_semantic/src/types/callable.rs +++ b/crates/ty_python_semantic/src/types/callable.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use rustc_hash::FxHashSet; use smallvec::{SmallVec, smallvec_inline}; @@ -40,17 +41,23 @@ impl<'db> Type<'db> { Type::Callable(CallableType::paramspec_value(db, parameters)) } - pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option> { - self.try_upcast_to_callable_with_policy(db, UpcastPolicy::default()) + pub(crate) fn try_upcast_to_callable( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + self.try_upcast_to_callable_with_policy(db, env, UpcastPolicy::default()) } pub(crate) fn try_upcast_to_callable_with_recursive_fallback( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, recursive_definition: Option>, ) -> Option> { self.try_upcast_to_callable_with_policy_and_context( db, + env, UpcastPolicy::default(), CallableUpcastContext { recursive_definition, @@ -61,10 +68,12 @@ impl<'db> Type<'db> { pub(crate) fn try_upcast_to_callable_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, policy: UpcastPolicy, ) -> Option> { self.try_upcast_to_callable_with_policy_and_context( db, + env, policy, CallableUpcastContext::default(), ) @@ -73,11 +82,13 @@ impl<'db> Type<'db> { fn try_upcast_to_callable_with_policy_and_context( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, policy: UpcastPolicy, context: CallableUpcastContext<'db>, ) -> Option> { if let Some(fallback) = self.materialized_divergent_fallback() { - return fallback.try_upcast_to_callable_with_policy_and_context(db, policy, context); + return fallback + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context); } match self { @@ -113,6 +124,7 @@ impl<'db> Type<'db> { let call_symbol = self .member_lookup_with_policy( db, + env, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -123,7 +135,7 @@ impl<'db> Type<'db> { { place .ty - .try_upcast_to_callable_with_policy_and_context(db, policy, context) + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context) // The callable instance itself doesn't inherit the descriptor behavior of // its `__call__` method. .map(|callables| callables.map(|callable| callable.into_regular(db))) @@ -139,12 +151,12 @@ impl<'db> Type<'db> { Type::NewTypeInstance(newtype) => newtype .concrete_base_type(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context), + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context), Type::SubclassOf(subclass_of_ty) if policy == UpcastPolicy::Sound => { Some(CallableTypes::one(CallableType::function_like( db, - Signature::new(Parameters::top(), subclass_of_ty.to_instance(db)), + Signature::new(Parameters::top(), subclass_of_ty.to_instance(db, env)), ))) } @@ -161,52 +173,56 @@ impl<'db> Type<'db> { (*origin).into_callable(db) } }), - SubclassOfInner::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - let upcast_callables = bound - .to_meta_type(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context)?; - Some(upcast_callables.map(|callable| { - let signatures = callable - .signatures(db) - .into_iter() - .map(|sig| sig.clone().with_return_type(Type::TypeVar(tvar))); - CallableType::new( - db, - CallableSignature::from_overloads(signatures), - callable.kind(db), - callable.provenance(db), - ) - })) - } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut callables = SmallVec::new(); - for constraint in constraints.elements(db) { - let element_upcast = constraint - .to_meta_type(db) + SubclassOfInner::TypeVar(tvar) => { + match tvar.typevar(db).bound_or_constraints(db, env) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + let upcast_callables = bound + .to_meta_type(db, env) .try_upcast_to_callable_with_policy_and_context( - db, policy, context, + db, env, policy, context, )?; - for callable in element_upcast.into_inner() { + Some(upcast_callables.map(|callable| { let signatures = callable .signatures(db) .into_iter() .map(|sig| sig.clone().with_return_type(Type::TypeVar(tvar))); - callables.push(CallableType::new( + CallableType::new( db, CallableSignature::from_overloads(signatures), callable.kind(db), callable.provenance(db), - )); + ) + })) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + let mut callables = SmallVec::new(); + for constraint in constraints.elements(db) { + let element_upcast = constraint + .to_meta_type(db, env) + .try_upcast_to_callable_with_policy_and_context( + db, env, policy, context, + )?; + for callable in element_upcast.into_inner() { + let signatures = + callable.signatures(db).into_iter().map(|sig| { + sig.clone().with_return_type(Type::TypeVar(tvar)) + }); + callables.push(CallableType::new( + db, + CallableSignature::from_overloads(signatures), + callable.kind(db), + callable.provenance(db), + )); + } } + Some(CallableTypes::new(callables)) } - Some(CallableTypes::new(callables)) + None => Some(CallableTypes::one(CallableType::single( + db, + Signature::new(Parameters::gradual_form(), Type::TypeVar(tvar)), + ))), } - None => Some(CallableTypes::one(CallableType::single( - db, - Signature::new(Parameters::gradual_form(), Type::TypeVar(tvar)), - ))), - }, + } SubclassOfInner::Dynamic(_) => Some(CallableTypes::one(CallableType::single( db, Signature::new(Parameters::unknown(), Type::from(subclass_of_ty)), @@ -217,7 +233,7 @@ impl<'db> Type<'db> { let mut callables = SmallVec::new(); for element in union.elements(db) { let element_callable = element - .try_upcast_to_callable_with_policy_and_context(db, policy, context)?; + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context)?; callables.extend(element_callable.into_inner()); } Some(CallableTypes::new(callables)) @@ -225,14 +241,14 @@ impl<'db> Type<'db> { Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Enum(enum_literal) => enum_literal - .enum_class_instance(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context), + .enum_class_instance(db, env) + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context), _ => None, }, Type::TypeAlias(alias) => alias .value_type(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context), + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context), Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderCall(function)) if context.is_recursive_reference(db, function) => @@ -242,7 +258,7 @@ impl<'db> Type<'db> { Type::KnownBoundMethod(method) => Some(CallableTypes::one(CallableType::new( db, - CallableSignature::from_overloads(method.signatures(db)), + CallableSignature::from_overloads(method.signatures(db, env)), CallableTypeKind::Regular, CallableFunctionProvenance::None, ))), @@ -250,7 +266,7 @@ impl<'db> Type<'db> { Type::WrapperDescriptor(wrapper_descriptor) => { Some(CallableTypes::one(CallableType::new( db, - CallableSignature::from_overloads(wrapper_descriptor.signatures(db)), + CallableSignature::from_overloads(wrapper_descriptor.signatures(db, env)), CallableTypeKind::Regular, CallableFunctionProvenance::None, ))) @@ -261,7 +277,7 @@ impl<'db> Type<'db> { db, Signature::new( Parameters::standard([Parameter::positional_only(None) - .with_annotated_type(newtype.base(db).instance_type(db))]), + .with_annotated_type(newtype.base(db).instance_type(db, env))]), Type::NewTypeInstance(newtype), ), ))) @@ -281,17 +297,15 @@ impl<'db> Type<'db> { | KnownInstanceType::FunctoolsPartialCall(partial), ) => Some(CallableTypes::one(partial.partial(db))), - Type::Intersection(intersection) => { - intersection - .finite_alternative_union(db) - .and_then(|alternatives| { - alternatives.try_upcast_to_callable_with_policy(db, policy) - }) - } + Type::Intersection(intersection) => intersection + .finite_alternative_union(db, env) + .and_then(|alternatives| { + alternatives.try_upcast_to_callable_with_policy(db, env, policy) + }), Type::EnumComplement(complement) => complement - .remaining_literal_union(db) - .try_upcast_to_callable_with_policy_and_context(db, policy, context), + .remaining_literal_union(db, env) + .try_upcast_to_callable_with_policy_and_context(db, env, policy, context), // TODO Type::DataclassDecorator(_) @@ -532,20 +546,25 @@ impl<'db> CallableType<'db> { /// Returns the reduced callable produced by partially applying selected overloads. pub(crate) fn partially_apply( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overloads: impl IntoIterator>, ) -> Option { Some(Self::new( db, - CallableSignature::partially_apply(db, overloads)?, + CallableSignature::partially_apply(db, env, overloads)?, CallableTypeKind::Regular, CallableFunctionProvenance::None, )) } /// Reifies this callable as the nominal `functools.partial[T]` instance for its return type. - pub(crate) fn into_functools_partial_instance(self, db: &'db dyn Db) -> Type<'db> { - let return_ty = self.signatures(db).overload_return_type_or_unknown(db); - KnownClass::FunctoolsPartial.to_specialized_instance(db, &[return_ty]) + pub(crate) fn into_functools_partial_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + let return_ty = self.signatures(db).overload_return_type_or_unknown(db, env); + KnownClass::FunctoolsPartial.to_specialized_instance(db, env, &[return_ty]) } /// Wraps this reduced callable as a synthetic `functools.partial(...)` instance type. @@ -562,6 +581,7 @@ impl<'db> CallableType<'db> { pub(crate) fn bind_self( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, self_type: Option>, ) -> CallableType<'db> { if self.is_dunder_paramspec(db) { @@ -570,7 +590,7 @@ impl<'db> CallableType<'db> { CallableType::new( db, - self.signatures(db).bind_self(db, self_type), + self.signatures(db).bind_self(db, env, self_type), self.kind(db), self.provenance(db), ) @@ -594,20 +614,26 @@ impl<'db> CallableType<'db> { ) } - pub(crate) fn apply_self(self, db: &'db dyn Db, self_type: Type<'db>) -> CallableType<'db> { - self.apply_self_with_receiver(db, self_type, self_type) + pub(crate) fn apply_self( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Type<'db>, + ) -> CallableType<'db> { + self.apply_self_with_receiver(db, env, self_type, self_type) } pub(crate) fn apply_self_with_receiver( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Type<'db>, self_type: Type<'db>, ) -> CallableType<'db> { CallableType::new( db, self.signatures(db) - .apply_self_with_receiver(db, receiver_type, self_type), + .apply_self_with_receiver(db, env, receiver_type, self_type), self.kind(db), self.provenance(db), ) @@ -629,13 +655,14 @@ impl<'db> CallableType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(CallableType::new( db, self.signatures(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, self.kind(db), self.provenance(db), )) @@ -646,7 +673,7 @@ impl<'db> CallableType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { if let TypeMapping::RescopeReturnCallables(replacements) = type_mapping { return replacements.get(&self).copied().unwrap_or(self); @@ -664,12 +691,13 @@ impl<'db> CallableType<'db> { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { self.signatures(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + .find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } @@ -718,9 +746,9 @@ impl<'db> CallableTypes<'db> { self.0.iter() } - pub(crate) fn into_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn into_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { assert!(!self.0.is_empty(), "CallableTypes should not be empty"); - UnionType::from_elements(db, self.0.into_iter().map(Type::Callable)) + UnionType::from_elements(db, env, self.0.into_iter().map(Type::Callable)) } pub(crate) fn map(self, mut f: impl FnMut(CallableType<'db>) -> CallableType<'db>) -> Self { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 8a664bb8b9..6a3468f70f 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use std::fmt::Write; pub(crate) use self::dynamic_literal::{ @@ -51,6 +52,7 @@ use crate::{ }, types::{MetaclassCandidate, TypeDefinition, UnionType}, }; +use ruff_db::PythonFile; use ruff_db::diagnostic::Span; use ruff_db::files::File; use ruff_db::parsed::parsed_module; @@ -85,7 +87,7 @@ fn dynamic_class_header_range<'db>( scope: ScopeId<'db>, anchor: DynamicClassHeaderAnchor<'db>, ) -> TextRange { - let module = parsed_module(db, scope.file(db)).load(db); + let module = parsed_module(db, scope.python_file(db)).load(db); match anchor { DynamicClassHeaderAnchor::Definition(definition) => definition .kind(db) @@ -170,6 +172,7 @@ impl<'db> CodeGeneratorKind<'db> { db: &'db dyn Db, class: StaticClassLiteral<'db>, ) -> Option> { + let env = ProgramEnvironment::from_scope(class.body_scope(db)); // If a class is directly decorated as a dataclass, it's a dataclass. // If a class' metaclass is a dataclass transformer, it's a dataclass. // If a class inherits from a base class that is a dataclass @@ -186,7 +189,7 @@ impl<'db> CodeGeneratorKind<'db> { info.params, )) } else if KnownClass::Type - .try_to_class_literal(db) + .try_to_class_literal(db, &env) .is_none_or(|type_class| { !class.is_subclass_of( db, @@ -401,6 +404,7 @@ impl<'db> GenericAlias<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -408,7 +412,7 @@ impl<'db> GenericAlias<'db> { db, self.origin(db), self.specialization(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, )) } @@ -421,11 +425,11 @@ impl<'db> GenericAlias<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let tcx = tcx .annotation - .and_then(|ty| ty.specialization_of(db, self.origin(db))) + .and_then(|ty| ty.specialization_of(db, visitor.env, self.origin(db))) .map(|specialization| specialization.types(db)) .unwrap_or(&[]); @@ -442,12 +446,18 @@ impl<'db> GenericAlias<'db> { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { - self.specialization(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + self.specialization(db).find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool { @@ -461,15 +471,31 @@ impl<'db> From> for Type<'db> { } } -#[salsa::tracked] impl<'db> VarianceInferable<'db> for GenericAlias<'db> { + fn variance_of( + self, + db: &'db dyn Db, + _: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.variance_of_owner(db, typevar) + } +} + +#[salsa::tracked] +impl<'db> GenericAlias<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size )] - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of_owner( + self, + db: &'db dyn Db, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { let origin = self.origin(db); + let env = ProgramEnvironment::from_file(origin.python_file(db)); let specialization = self.specialization(db); @@ -481,7 +507,8 @@ impl<'db> VarianceInferable<'db> for GenericAlias<'db> { .zip(specialization.types(db)) .map(|(generic_typevar, ty)| { if let Some(explicit_variance) = generic_typevar.typevar(db).explicit_variance(db) { - ty.with_polarity(explicit_variance).variance_of(db, typevar) + ty.with_polarity(explicit_variance) + .variance_of(db, &env, typevar) } else { // `with_polarity` composes the passed variance with the // inferred one. The inference is done lazily, as we can @@ -494,10 +521,10 @@ impl<'db> VarianceInferable<'db> for GenericAlias<'db> { // If salsa let us look at the cache, we could check first // to see if the class literal query was already run. - let typevar_variance_in_substituted_type = ty.variance_of(db, typevar); + let typevar_variance_in_substituted_type = ty.variance_of(db, &env, typevar); origin .with_polarity(typevar_variance_in_substituted_type) - .variance_of(db, generic_typevar.identity(db)) + .variance_of(db, &env, generic_typevar.identity(db)) } }) .collect() @@ -524,9 +551,9 @@ pub enum ClassLiteral<'db> { #[salsa::tracked] impl<'db> ClassLiteral<'db> { /// Return a `ClassLiteral` representing the class `builtins.object` - pub(super) fn object(db: &'db dyn Db) -> Self { + pub(super) fn object(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { KnownClass::Object - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal() .expect("`object` should always be a non-generic class in typeshed") } @@ -534,21 +561,22 @@ impl<'db> ClassLiteral<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Self::Dynamic(dynamic) => Some(Self::Dynamic( - dynamic.recursive_type_normalized_impl(db, div, nested)?, + dynamic.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::DynamicNamedTuple(named_tuple) => Some(Self::DynamicNamedTuple( - named_tuple.recursive_type_normalized_impl(db, div, nested)?, + named_tuple.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::DynamicTypedDict(typed_dict) => Some(Self::DynamicTypedDict( - typed_dict.recursive_type_normalized_impl(db, div, nested)?, + typed_dict.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::DynamicEnum(enum_literal) => Some(Self::DynamicEnum( - enum_literal.recursive_type_normalized_impl(db, div, nested)?, + enum_literal.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::Static(_) => Some(self), } @@ -624,15 +652,16 @@ impl<'db> ClassLiteral<'db> { pub(crate) fn class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { match self { - Self::Static(class) => class.class_member(db, name, policy), - Self::Dynamic(class) => class.class_member(db, name, policy), - Self::DynamicNamedTuple(namedtuple) => namedtuple.class_member(db, name, policy), - Self::DynamicTypedDict(typeddict) => typeddict.class_member(db, name, policy), - Self::DynamicEnum(enum_lit) => enum_lit.class_member(db, name), + Self::Static(class) => class.class_member(db, env, name, policy), + Self::Dynamic(class) => class.class_member(db, env, name, policy), + Self::DynamicNamedTuple(namedtuple) => namedtuple.class_member(db, env, name, policy), + Self::DynamicTypedDict(typeddict) => typeddict.class_member(db, env, name, policy), + Self::DynamicEnum(enum_lit) => enum_lit.class_member(db, env, name), } } @@ -642,22 +671,24 @@ impl<'db> ClassLiteral<'db> { pub(super) fn class_member_from_mro( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, mro_iter: impl Iterator>, ) -> PlaceAndQualifiers<'db> { match self { - Self::Static(class) => class.class_member_from_mro(db, name, policy, mro_iter), + Self::Static(class) => class.class_member_from_mro(db, env, name, policy, mro_iter), Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicTypedDict(_) | Self::DynamicEnum(_) => { // Dynamic classes don't have inherited generic context and are never `object`. - let result = MroLookup::new(db, mro_iter).class_member(name, policy, None, false); + let result = + MroLookup::new(db, env, mro_iter).class_member(name, policy, None, false); match result { - ClassMemberResult::Done(result) => result.finalize(db), + ClassMemberResult::Done(result) => result.finalize(db, env), ClassMemberResult::TypedDict(module) => { - typed_dict::typed_dict_fallback_class_member(db, module, policy, name) + typed_dict::typed_dict_fallback_class_member(db, env, module, policy, name) } } } @@ -725,9 +756,13 @@ impl<'db> ClassLiteral<'db> { } /// Return a type representing "the set of all instances of the metaclass of this class". - pub(crate) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn metaclass_instance_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { self.metaclass(db) - .to_instance_approximation(db) + .to_instance_approximation(db, env) .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") } @@ -748,6 +783,16 @@ impl<'db> ClassLiteral<'db> { } } + pub(crate) fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + match self { + Self::Static(class) => class.python_file(db), + Self::Dynamic(class) => class.scope(db).python_file(db), + Self::DynamicNamedTuple(class) => class.scope(db).python_file(db), + Self::DynamicTypedDict(class) => class.scope(db).python_file(db), + Self::DynamicEnum(enum_lit) => enum_lit.scope(db).python_file(db), + } + } + /// Returns the range of the class's "header". /// /// For static classes, this is the class name and any arguments passed to the `class` statement. @@ -888,13 +933,17 @@ impl<'db> ClassLiteral<'db> { } /// Returns a non-generic instance of this class. - pub(crate) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn to_non_generic_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { Self::Static(class) => class.to_non_generic_instance(db), Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicTypedDict(_) - | Self::DynamicEnum(_) => Type::instance(db, ClassType::NonGeneric(self)), + | Self::DynamicEnum(_) => Type::instance(db, env, ClassType::NonGeneric(self)), } } @@ -926,15 +975,16 @@ impl<'db> ClassLiteral<'db> { pub(crate) fn instance_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, name: &str, ) -> PlaceAndQualifiers<'db> { match self { - Self::Static(class) => class.instance_member(db, specialization, name), - Self::Dynamic(class) => class.instance_member(db, name), - Self::DynamicNamedTuple(namedtuple) => namedtuple.instance_member(db, name), + Self::Static(class) => class.instance_member(db, env, specialization, name), + Self::Dynamic(class) => class.instance_member(db, env, name), + Self::DynamicNamedTuple(namedtuple) => namedtuple.instance_member(db, env, name), Self::DynamicTypedDict(_) => PlaceAndQualifiers::default(), - Self::DynamicEnum(enum_lit) => enum_lit.instance_member(db, name), + Self::DynamicEnum(enum_lit) => enum_lit.instance_member(db, env, name), } } @@ -953,13 +1003,14 @@ impl<'db> ClassLiteral<'db> { pub(crate) fn typed_dict_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { match self { - Self::Static(class) => class.typed_dict_member(db, specialization, name, policy), - Self::DynamicTypedDict(typeddict) => typeddict.class_member(db, name, policy), + Self::Static(class) => class.typed_dict_member(db, env, specialization, name, policy), + Self::DynamicTypedDict(typeddict) => typeddict.class_member(db, env, name, policy), Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicEnum(_) => { Place::Undefined.into() } @@ -990,7 +1041,8 @@ impl<'db> ClassLiteral<'db> { Self::Static(static_class) => static_class.explicit_bases(db).into(), Self::Dynamic(dynamic_class) => dynamic_class.explicit_bases(db).into(), Self::DynamicNamedTuple(namedtuple) => { - [Type::from(namedtuple.tuple_base_class(db))].into() + let env = ProgramEnvironment::from_scope(namedtuple.scope(db)); + [Type::from(namedtuple.tuple_base_class(db, &env))].into() } Self::DynamicTypedDict(_) => { // TypedDicts always inherit from `dict` @@ -1049,8 +1101,8 @@ pub enum ClassType<'db> { #[salsa::tracked] impl<'db> ClassType<'db> { /// Return a `ClassType` representing the class `builtins.object` - pub(super) fn object(db: &'db dyn Db) -> Self { - ClassType::NonGeneric(ClassLiteral::object(db)) + pub(super) fn object(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + ClassType::NonGeneric(ClassLiteral::object(db, env)) } pub(super) const fn is_generic(self) -> bool { @@ -1067,15 +1119,16 @@ impl<'db> ClassType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Self::NonGeneric(class) => Some(Self::NonGeneric( - class.recursive_type_normalized_impl(db, div, nested)?, + class.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::Generic(generic) => Some(Self::Generic( - generic.recursive_type_normalized_impl(db, div, nested)?, + generic.recursive_type_normalized_impl(db, env, div, nested)?, )), } } @@ -1145,14 +1198,17 @@ impl<'db> ClassType<'db> { | ClassLiteral::DynamicTypedDict(_) | ClassLiteral::DynamicEnum(_), ) => None, - Self::Generic(generic) => Some(( - generic.origin(db), - Some( - generic - .specialization(db) - .apply_optional_specialization(db, additional_specialization), - ), - )), + Self::Generic(generic) => { + let origin = generic.origin(db); + Some(( + origin, + Some( + generic + .specialization(db) + .apply_optional_specialization(db, additional_specialization), + ), + )) + } } } @@ -1214,7 +1270,7 @@ impl<'db> ClassType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { Self::NonGeneric(_) => self, @@ -1227,6 +1283,7 @@ impl<'db> ClassType<'db> { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, @@ -1234,7 +1291,7 @@ impl<'db> ClassType<'db> { match self { Self::NonGeneric(_) => {} Self::Generic(generic) => { - generic.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + generic.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } } @@ -1266,16 +1323,19 @@ impl<'db> ClassType<'db> { additional_specialization: Option>, ) -> MroIterator<'db> { match self { - Self::NonGeneric(class) => class.iter_mro(db), - Self::Generic(generic) => MroIterator::new( - db, - ClassLiteral::Static(generic.origin(db)), - Some( - generic - .specialization(db) - .apply_optional_specialization(db, additional_specialization), - ), - ), + Self::NonGeneric(class) => MroIterator::new(db, class, None), + Self::Generic(generic) => { + let origin = generic.origin(db); + MroIterator::new( + db, + ClassLiteral::Static(origin), + Some( + generic + .specialization(db) + .apply_optional_specialization(db, additional_specialization), + ), + ) + } } } @@ -1289,7 +1349,10 @@ impl<'db> ClassType<'db> { /// /// The value of the map is a struct containing information about the abstract method. #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn abstract_methods(self, db: &'db dyn Db) -> FxIndexMap> { + pub(in crate::types) fn abstract_methods( + self, + db: &'db dyn Db, + ) -> FxIndexMap> { fn type_as_abstract_method<'db>( db: &'db dyn Db, ty: Type<'db>, @@ -1321,6 +1384,7 @@ impl<'db> ClassType<'db> { } let mut abstract_methods: FxIndexMap = FxIndexMap::default(); + let env = &ProgramEnvironment::from_file(self.class_literal(db).python_file(db)); // Iterate through the MRO in reverse order, // skipping `object` (we know it doesn't define any abstract methods) @@ -1333,7 +1397,7 @@ impl<'db> ClassType<'db> { // but we do recognise them as being able to override abstract methods defined in static classes. let ClassLiteral::Static(class_literal) = class.class_literal(db) else { abstract_methods - .retain(|name, _| class.own_class_member(db, None, name).is_undefined()); + .retain(|name, _| class.own_class_member(db, env, None, name).is_undefined()); continue; }; @@ -1346,7 +1410,7 @@ impl<'db> ClassType<'db> { // or this class has a `ClassVar` declaration by that name abstract_methods.retain(|name, _| { if class_literal - .own_synthesized_member(db, None, None, name) + .own_synthesized_member(db, env, None, None, name) .is_some() { return false; @@ -1354,7 +1418,7 @@ impl<'db> ClassType<'db> { place_table.symbol_id(name).is_none_or(|symbol_id| { let declarations = use_def_map.end_of_scope_symbol_declarations(symbol_id); - !place_from_declarations(db, declarations) + !place_from_declarations(db, env, declarations) .ignore_conflicting_declarations() .qualifiers .contains(TypeQualifiers::CLASS_VAR) @@ -1363,7 +1427,7 @@ impl<'db> ClassType<'db> { for (symbol_id, bindings_iterator) in use_def_map.all_end_of_scope_symbol_bindings() { let name = place_table.symbol(symbol_id).name(); - let place_and_definition = place_from_bindings(db, bindings_iterator); + let place_and_definition = place_from_bindings(db, env, bindings_iterator); let Place::Defined(DefinedPlace { ty, .. }) = place_and_definition.place else { continue; }; @@ -1399,13 +1463,19 @@ impl<'db> ClassType<'db> { } /// Return `true` if `other` is present in this class's MRO. - pub(super) fn is_subclass_of(self, db: &'db dyn Db, target: ClassType<'db>) -> bool { + pub(super) fn is_subclass_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: ClassType<'db>, + ) -> bool { let constraints = ConstraintSetBuilder::new(); let relation_visitor = HasRelationToVisitor::default(&constraints); let disjointness_visitor = IsDisjointVisitor::default(&constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker::subtyping( + env, &constraints, TypeVarSet::None, &relation_visitor, @@ -1415,7 +1485,7 @@ impl<'db> ClassType<'db> { ); checker .check_class_pair(db, self, target) - .is_always_satisfied(db) + .is_always_satisfied(db, env) } /// Return the metaclass of this class, or `type[Unknown]` if the metaclass cannot be inferred. @@ -1447,12 +1517,13 @@ impl<'db> ClassType<'db> { fn could_exist_in_mro_of( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Self, constraints: &ConstraintSetBuilder<'db>, ) -> bool { self.could_exist_in_mro_of_impl(db, other, |this, other| { - this.is_disjoint_from(db, other, constraints, TypeVarSet::None) - .is_always_satisfied(db) + this.is_disjoint_from(db, env, other, constraints, TypeVarSet::None) + .is_always_satisfied(db, env) }) } @@ -1461,13 +1532,14 @@ impl<'db> ClassType<'db> { pub(super) fn could_exist_in_mro_of_with_disjointness_checker<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Self, checker: &DisjointnessChecker<'_, 'c, 'db>, ) -> bool { self.could_exist_in_mro_of_impl(db, other, |this, other| { checker .check_specialization_pair(db, this, other) - .is_always_satisfied(db) + .is_always_satisfied(db, env) }) } @@ -1530,20 +1602,22 @@ impl<'db> ClassType<'db> { pub(super) fn could_coexist_in_mro_with( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Self, constraints: &ConstraintSetBuilder<'db>, ) -> bool { self.could_coexist_in_mro_with_impl( db, + env, other, - |this, other| this.could_exist_in_mro_of(db, other, constraints), + |this, other| this.could_exist_in_mro_of(db, env, other, constraints), |this, other| { - this.is_disjoint_from(db, other, constraints, TypeVarSet::None) - .is_always_satisfied(db) + this.is_disjoint_from(db, env, other, constraints, TypeVarSet::None) + .is_always_satisfied(db, env) }, |this, other| { - this.when_disjoint_from(db, other, constraints, TypeVarSet::None) - .is_always_satisfied(db) + this.when_disjoint_from(db, env, other, constraints, TypeVarSet::None) + .is_always_satisfied(db, env) }, ) } @@ -1551,6 +1625,7 @@ impl<'db> ClassType<'db> { pub(super) fn could_coexist_in_mro_with_disjointness_checker<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Self, checker: &DisjointnessChecker<'_, 'c, 'db>, ) -> bool { @@ -1558,17 +1633,20 @@ impl<'db> ClassType<'db> { // metaclass checks so recursive class graphs keep the same cycle guard. self.could_coexist_in_mro_with_impl( db, + env, other, - |this, other| this.could_exist_in_mro_of_with_disjointness_checker(db, other, checker), + |this, other| { + this.could_exist_in_mro_of_with_disjointness_checker(db, env, other, checker) + }, |this, other| { checker .check_specialization_pair(db, this, other) - .is_always_satisfied(db) + .is_always_satisfied(db, env) }, |this, other| { checker .check_type_pair(db, this, other) - .is_always_satisfied(db) + .is_always_satisfied(db, env) }, ) } @@ -1576,6 +1654,7 @@ impl<'db> ClassType<'db> { fn could_coexist_in_mro_with_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Self, could_exist_in_mro_of: impl Fn(Self, Self) -> bool, specializations_are_disjoint: impl Fn(Specialization<'db>, Specialization<'db>) -> bool, @@ -1617,7 +1696,7 @@ impl<'db> ClassType<'db> { // 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` can coexist in an MRO // with any other arbitrary class, anyway). - let type_class = KnownClass::Type.to_class_literal(db); + let type_class = KnownClass::Type.to_class_literal(db, env); let self_metaclass = self.metaclass(db); if self_metaclass == type_class { return true; @@ -1626,10 +1705,12 @@ impl<'db> ClassType<'db> { if other_metaclass == type_class { return true; } - let Some(self_metaclass_instance) = self_metaclass.to_instance_approximation(db) else { + let Some(self_metaclass_instance) = self_metaclass.to_instance_approximation(db, env) + else { return true; }; - let Some(other_metaclass_instance) = other_metaclass.to_instance_approximation(db) else { + let Some(other_metaclass_instance) = other_metaclass.to_instance_approximation(db, env) + else { return true; }; if types_are_disjoint(self_metaclass_instance, other_metaclass_instance) { @@ -1640,10 +1721,14 @@ impl<'db> ClassType<'db> { } /// Return a type representing "the set of all instances of the metaclass of this class". - pub(super) fn metaclass_instance_type(self, db: &'db dyn Db) -> Type<'db> { + pub(super) fn metaclass_instance_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { self .metaclass(db) - .to_instance_approximation(db) + .to_instance_approximation(db, env) .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") } @@ -1655,13 +1740,15 @@ impl<'db> ClassType<'db> { pub(super) fn class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { match self { - Self::NonGeneric(class) => class.class_member(db, name, policy), + Self::NonGeneric(class) => class.class_member(db, env, name, policy), Self::Generic(generic) => generic.origin(db).class_member_inner( db, + env, Some(generic.specialization(db)), name, policy, @@ -1683,6 +1770,7 @@ impl<'db> ClassType<'db> { pub(super) fn own_class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, inherited_generic_context: Option>, name: &str, ) -> Member<'db> { @@ -1718,7 +1806,7 @@ impl<'db> ClassType<'db> { let specialization = specialization .map(|specialization| specialization.tuple_runtime_element_specialization(db)); class_literal - .own_class_member(db, inherited_generic_context, specialization, name) + .own_class_member(db, env, inherited_generic_context, specialization, name) .map_type(|ty| ty.apply_optional_specialization(db, specialization)) }; @@ -1729,12 +1817,12 @@ impl<'db> ClassType<'db> { .and_then(|tuple| tuple.len().into_fixed_length()) .and_then(|len| i64::try_from(len).ok()) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)); + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)); let parameters = Parameters::standard([Parameter::positional_only(Some( Name::new_static("self"), )) - .with_annotated_type(Type::instance(db, self))]); + .with_annotated_type(Type::instance(db, env, self))]); let synthesized_dunder_method = Type::function_like_callable(db, Signature::new(parameters, return_type)); @@ -1796,6 +1884,7 @@ impl<'db> ClassType<'db> { ) { let overload_return = UnionType::from_elements( db, + env, std::iter::once( variable_length_tuple.variable().element_type(db), ) @@ -1832,6 +1921,7 @@ impl<'db> ClassType<'db> { ) { let overload_return = UnionType::from_elements( db, + env, std::iter::once( variable_length_tuple.variable().element_type(db), ) @@ -1850,14 +1940,14 @@ impl<'db> ClassType<'db> { } } - let all_elements_unioned = tuple.homogeneous_element_type(db); + let all_elements_unioned = tuple.homogeneous_element_type(db, env); let mut overload_signatures = Vec::with_capacity(element_type_to_indices.len().saturating_add(2)); overload_signatures.extend(element_type_to_indices.into_iter().filter_map( |(return_type, mut indices)| { - if return_type.is_equivalent_to(db, all_elements_unioned) { + if return_type.is_equivalent_to(db, env, all_elements_unioned) { return None; } @@ -1866,6 +1956,7 @@ impl<'db> ClassType<'db> { let index_annotation = UnionType::from_elements( db, + env, indices.into_iter().map(Type::int_literal), ); @@ -1887,20 +1978,25 @@ impl<'db> ClassType<'db> { // __getitem__(self, index: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> tuple[str | float | bytes, ...] // overload_signatures.push(synthesize_getitem_overload_signature( - KnownClass::SupportsIndex.to_instance(db), + KnownClass::SupportsIndex.to_instance(db, env), all_elements_unioned, )); let slice_bound = UnionType::from_elements( db, - [KnownClass::SupportsIndex.to_instance(db), Type::none(db)], + env, + [ + KnownClass::SupportsIndex.to_instance(db, env), + Type::none(db, env), + ], ); overload_signatures.push(synthesize_getitem_overload_signature( KnownClass::Slice.to_specialized_instance( db, + env, &[slice_bound, slice_bound, slice_bound], ), - Type::homogeneous_tuple(db, all_elements_unioned), + Type::homogeneous_tuple(db, env, all_elements_unioned), )); let getitem_signature = @@ -1946,21 +2042,22 @@ impl<'db> ClassType<'db> { iterable_parameter = iterable_parameter.with_annotated_type( KnownClass::Iterable.to_specialized_instance( db, - &[tuple.homogeneous_element_type(db)], + env, + &[tuple.homogeneous_element_type(db, env)], ), ); } else { // But if the tuple is of a fixed length, or has a minimum length, we require a tuple rather // than an iterable, as a tuple is the only kind of iterable for which we can // specify a fixed length, or that the iterable must be at least a certain length. - iterable_parameter = - iterable_parameter.with_annotated_type(Type::instance(db, self)); + iterable_parameter = iterable_parameter + .with_annotated_type(Type::instance(db, env, self)); } } None => { // If the tuple isn't specialized at all, we allow any argument as long as it is iterable. iterable_parameter = iterable_parameter - .with_annotated_type(KnownClass::Iterable.to_instance(db)); + .with_annotated_type(KnownClass::Iterable.to_instance(db, env)); } } @@ -1970,12 +2067,12 @@ impl<'db> ClassType<'db> { // - a tuple with no minimum length if tuple.is_none_or(|tuple| tuple.len().minimum() == 0) { iterable_parameter = - iterable_parameter.with_default_type(Type::empty_tuple(db)); + iterable_parameter.with_default_type(Type::empty_tuple(db, env)); } let parameters = Parameters::standard([ Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(SubclassOfType::from(db, self)), + .with_annotated_type(SubclassOfType::from(db, env, self)), iterable_parameter, ]); @@ -1994,21 +2091,26 @@ impl<'db> ClassType<'db> { /// Look up an instance attribute (available in `__dict__`) of the given name. /// /// See [`Type::instance_member`] for more details. - pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub(super) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { match self { - Self::NonGeneric(ClassLiteral::Dynamic(class)) => class.instance_member(db, name), + Self::NonGeneric(ClassLiteral::Dynamic(class)) => class.instance_member(db, env, name), Self::NonGeneric(ClassLiteral::DynamicNamedTuple(namedtuple)) => { - namedtuple.instance_member(db, name) + namedtuple.instance_member(db, env, name) } Self::NonGeneric(ClassLiteral::DynamicTypedDict(_)) => PlaceAndQualifiers::default(), Self::NonGeneric(ClassLiteral::DynamicEnum(enum_lit)) => { - enum_lit.instance_member(db, name) + enum_lit.instance_member(db, env, name) } Self::NonGeneric(ClassLiteral::Static(class)) => { if class.is_typed_dict(db) { return Place::Undefined.into(); } - class.instance_member(db, None, name) + class.instance_member(db, env, None, name) } Self::Generic(generic) => { let class_literal = generic.origin(db); @@ -2019,7 +2121,7 @@ impl<'db> ClassType<'db> { } class_literal - .instance_member(db, specialization, name) + .instance_member(db, env, specialization, name) .map_type(|ty| ty.apply_optional_specialization(db, specialization)) } } @@ -2050,7 +2152,12 @@ impl<'db> ClassType<'db> { /// A helper function for `instance_member` that looks up the `name` attribute only on /// this class, not on its superclasses. - pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + pub(super) fn own_instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Member<'db> { match self { Self::NonGeneric(ClassLiteral::Dynamic(dynamic)) => { dynamic.own_instance_member(db, name) @@ -2063,13 +2170,13 @@ impl<'db> ClassType<'db> { enum_lit.own_instance_member(db, name) } Self::NonGeneric(ClassLiteral::Static(class_literal)) => { - class_literal.own_instance_member(db, name) + class_literal.own_instance_member(db, env, name) } Self::Generic(generic) => { let specialization = generic.specialization(db); generic .origin(db) - .own_instance_member(db, name) + .own_instance_member(db, env, name) .map_type(|ty| ty.apply_optional_specialization(db, Some(specialization))) } } @@ -2096,6 +2203,7 @@ impl<'db> ClassType<'db> { db: &'db dyn Db, receiver: Type<'db>, ) -> CallableTypes<'db> { + let env = &ProgramEnvironment::from_file(self.class_literal(db).python_file(db)); // TODO: This mimics a lot of the logic in Type::try_call_from_constructor. Can we // consolidate the two? Can we invoke a class by upcasting the class into a Callable, and // then relying on the call binding machinery to Just Work™? @@ -2107,12 +2215,13 @@ impl<'db> ClassType<'db> { let lookup_type = Type::from(self); let instance_type = receiver - .to_instance_approximation(db) + .to_instance_approximation(db, env) .unwrap_or_else(Type::unknown); let metaclass_dunder_call_function_symbol = lookup_type .member_lookup_with_policy( db, + env, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, @@ -2140,13 +2249,13 @@ impl<'db> ClassType<'db> { metaclass_dunder_call_function.into_callable_type(db) } else { metaclass_dunder_call_function - .into_callable_type_with_receiver(db, receiver, receiver) + .into_callable_type_with_receiver(db, env, receiver, receiver) }; return CallableTypes::one(callable); } } - let dunder_new_function_symbol = lookup_type.lookup_dunder_new(db); + let dunder_new_function_symbol = lookup_type.lookup_dunder_new(db, env); let dunder_new_signature = dunder_new_function_symbol .and_then(|place_and_quals| place_and_quals.ignore_possibly_undefined()) @@ -2159,6 +2268,7 @@ impl<'db> ClassType<'db> { let dunder_new_function = if let Some(dunder_new_signature) = dunder_new_signature { let bound_signature = dunder_new_signature.bind_self_with_receiver( db, + env, Some(receiver), Some(instance_type), ); @@ -2168,7 +2278,7 @@ impl<'db> ClassType<'db> { let returns_non_subclass = bound_signature .overloads .iter() - .any(|signature| !signature.return_ty.is_assignable_to(db, instance_type)); + .any(|signature| !signature.return_ty.is_assignable_to(db, env, instance_type)); let dunder_new_bound_method = CallableType::new( db, @@ -2188,6 +2298,7 @@ impl<'db> ClassType<'db> { let dunder_init_function_symbol = lookup_type .member_lookup_with_policy( db, + env, "__init__", MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, @@ -2234,6 +2345,7 @@ impl<'db> ClassType<'db> { .with_source_overload_index(signature.source_overload_index()) .bind_self_with_receiver( db, + env, Some(instance_type), Some(instance_type), ) @@ -2272,6 +2384,7 @@ impl<'db> ClassType<'db> { let new_function_symbol = lookup_type .member_lookup_with_policy( db, + env, "__new__", MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ) @@ -2354,16 +2467,21 @@ impl<'db> From> for Type<'db> { } impl<'db> VarianceInferable<'db> for ClassType<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { match self { - Self::NonGeneric(ClassLiteral::Static(class)) => class.variance_of(db, typevar), + Self::NonGeneric(ClassLiteral::Static(class)) => class.variance_of(db, env, typevar), Self::NonGeneric( ClassLiteral::Dynamic(_) | ClassLiteral::DynamicNamedTuple(_) | ClassLiteral::DynamicTypedDict(_) | ClassLiteral::DynamicEnum(_), ) => TypeVarVariance::Bivariant, - Self::Generic(generic) => generic.variance_of(db, typevar), + Self::Generic(generic) => generic.variance_of(db, env, typevar), } } } @@ -2574,9 +2692,14 @@ impl<'db> Field<'db> { } impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { match self { - Self::Static(class) => class.variance_of(db, typevar), + Self::Static(class) => class.variance_of(db, env, typevar), Self::Dynamic(_) | Self::DynamicNamedTuple(_) | Self::DynamicTypedDict(_) @@ -2592,13 +2715,18 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> { /// use this to avoid duplicating the MRO traversal logic. pub(super) struct MroLookup<'db, I> { db: &'db dyn Db, + env: ProgramEnvironment<'db>, mro_iter: I, } impl<'db, I: Iterator>> MroLookup<'db, I> { /// Create a new MRO lookup from a database and an MRO iterator. - fn new(db: &'db dyn Db, mro_iter: I) -> Self { - Self { db, mro_iter } + fn new(db: &'db dyn Db, env: &ProgramEnvironment<'db>, mro_iter: I) -> Self { + Self { + db, + env: env.clone(), + mro_iter, + } } /// Look up a class member by iterating through the MRO. @@ -2623,6 +2751,7 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { is_self_object: bool, ) -> ClassMemberResult<'db> { let db = self.db; + let mut dynamic_type: Option> = None; let mut lookup_result: LookupResult<'db> = Err(LookupError::Undefined(TypeQualifiers::empty())); @@ -2666,8 +2795,9 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { lookup_result = lookup_result.or_else(|lookup_error| { lookup_error.or_fall_back_to( db, + &self.env, class - .own_class_member(db, inherited_generic_context, name) + .own_class_member(db, &self.env, inherited_generic_context, name) .inner, ) }); @@ -2698,7 +2828,7 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { /// allowing the caller to handle this case specially. fn instance_member(self, name: &str) -> InstanceMemberResult<'db> { let db = self.db; - let mut union = UnionBuilder::new(db); + let mut union = UnionBuilder::new(db, &self.env); let mut union_qualifiers = TypeQualifiers::empty(); let mut is_definitely_bound = false; let mut provenance = Provenance::Unknown; @@ -2725,7 +2855,7 @@ impl<'db, I: Iterator>> MroLookup<'db, I> { .. }), qualifiers, - } = class.own_instance_member(db, name).inner + } = class.own_instance_member(db, &self.env, name).inner { if boundness == Definedness::AlwaysDefined { if origin.is_declared() { @@ -2792,7 +2922,7 @@ pub(super) struct CompletedMemberLookup<'db> { impl<'db> CompletedMemberLookup<'db> { /// Finalize the lookup result by handling dynamic type intersection. - fn finalize(self, db: &'db dyn Db) -> PlaceAndQualifiers<'db> { + fn finalize(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> PlaceAndQualifiers<'db> { match ( PlaceAndQualifiers::from(self.lookup_result), self.dynamic_type, @@ -2805,7 +2935,7 @@ impl<'db> CompletedMemberLookup<'db> { qualifiers, }, Some(dynamic), - ) => Place::bound(IntersectionType::from_two_elements(db, ty, dynamic)) + ) => Place::bound(IntersectionType::from_two_elements(db, env, ty, dynamic)) .with_provenance(provenance) .with_qualifiers(qualifiers), @@ -2856,7 +2986,7 @@ impl<'db> QualifiedClassName<'db> { let body_scope = class.body_scope(self.db); // Skip the class body scope itself. ( - body_scope.file(self.db), + body_scope.python_file(self.db), body_scope.file_scope_id(self.db), 1, ) @@ -2864,20 +2994,20 @@ impl<'db> QualifiedClassName<'db> { ClassLiteral::Dynamic(class) => { // Dynamic classes don't have a body scope; start from the enclosing scope. let scope = class.scope(self.db); - (scope.file(self.db), scope.file_scope_id(self.db), 0) + (scope.python_file(self.db), scope.file_scope_id(self.db), 0) } ClassLiteral::DynamicNamedTuple(namedtuple) => { // Dynamic namedtuples don't have a body scope; start from the enclosing scope. let scope = namedtuple.scope(self.db); - (scope.file(self.db), scope.file_scope_id(self.db), 0) + (scope.python_file(self.db), scope.file_scope_id(self.db), 0) } ClassLiteral::DynamicTypedDict(typeddict) => { let scope = typeddict.scope(self.db); - (scope.file(self.db), scope.file_scope_id(self.db), 0) + (scope.python_file(self.db), scope.file_scope_id(self.db), 0) } ClassLiteral::DynamicEnum(enum_lit) => { let scope = enum_lit.scope(self.db); - (scope.file(self.db), scope.file_scope_id(self.db), 0) + (scope.python_file(self.db), scope.file_scope_id(self.db), 0) } }; @@ -3018,12 +3148,19 @@ enum SlotsKind { impl SlotsKind { fn from(db: &dyn Db, base: StaticClassLiteral) -> Self { + let env = ProgramEnvironment::from_scope(base.body_scope(db)); let Place::Defined(DefinedPlace { ty: slots_ty, definedness: bound, .. }) = base - .own_class_member(db, base.inherited_generic_context(db), None, "__slots__") + .own_class_member( + db, + &env, + base.inherited_generic_context(db), + None, + "__slots__", + ) .inner .place else { @@ -3037,7 +3174,7 @@ impl SlotsKind { match slots_ty { // __slots__ = ("a", "b") Type::NominalInstance(nominal) => match nominal - .tuple_spec(db) + .tuple_spec(db, &env) .and_then(|spec| spec.len().into_fixed_length()) { Some(0) => Self::Empty, diff --git a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs index 80bd1b33a6..7f926a0a78 100644 --- a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs +++ b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use ruff_db::{diagnostic::Span, parsed::parsed_module}; use ruff_python_ast::{self as ast, name::Name}; use ruff_text_size::TextRange; @@ -113,6 +114,7 @@ impl<'db> DynamicClassAnchor<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -126,7 +128,7 @@ impl<'db> DynamicClassAnchor<'db> { let explicit_bases = explicit_bases .iter() .map(|base| { - let base = base.recursive_type_normalized_impl(db, div, true); + let base = base.recursive_type_normalized_impl(db, env, div, true); if nested { base } else { @@ -200,7 +202,9 @@ impl<'db> DynamicClassLiteral<'db> { db: &'db dyn Db, definition: Definition<'db>, ) -> Box<[Type<'db>]> { - let module = parsed_module(db, definition.file(db)).load(db); + let python_file = definition.python_file(db); + let env = ProgramEnvironment::from_file(python_file); + let module = parsed_module(db, python_file).load(db); let value = definition .kind(db) @@ -215,7 +219,7 @@ impl<'db> DynamicClassLiteral<'db> { }; // Use `definition_expression_type` for deferred inference support. - extract_fixed_length_iterable_element_types(db, bases_arg, |expr| { + extract_fixed_length_iterable_element_types(db, &env, bases_arg, |expr| { definition_expression_type(db, definition, expr) }) .unwrap_or_else(|| Box::from([Type::unknown()])) @@ -271,12 +275,13 @@ impl<'db> DynamicClassLiteral<'db> { db: &'db dyn Db, ) -> Result, DynamicMetaclassConflict<'db>> { let original_bases = self.explicit_bases(db); + let env = ProgramEnvironment::from_scope(self.scope(db)); // If no bases, metaclass is `type`. // To dynamically create a class with no bases that has a custom metaclass, // you have to invoke that metaclass rather than `type()`. if original_bases.is_empty() { - return Ok(KnownClass::Type.to_class_literal(db)); + return Ok(KnownClass::Type.to_class_literal(db, &env)); } // If there's an MRO error, return unknown to avoid cascading errors. @@ -289,23 +294,23 @@ impl<'db> DynamicClassLiteral<'db> { // returned `Err(InvalidBases)` if any failed, causing us to return early. let bases: Vec> = original_bases .iter() - .filter_map(|base_type| ClassBase::try_from_type(db, *base_type, None)) + .filter_map(|base_type| ClassBase::try_from_type(db, &env, *base_type, None)) .collect(); // If all bases failed to convert, return type as the metaclass. if bases.is_empty() { - return Ok(KnownClass::Type.to_class_literal(db)); + return Ok(KnownClass::Type.to_class_literal(db, &env)); } // Start with the first base's metaclass as the candidate. - let mut candidate = bases[0].metaclass(db); + let mut candidate = bases[0].metaclass(db, &env); // Track which base the candidate metaclass came from. let (mut candidate_base, rest) = bases.split_first().unwrap(); // Reconcile with other bases' metaclasses. for base in rest { - let base_metaclass = base.metaclass(db); + let base_metaclass = base.metaclass(db, &env); // Get the ClassType for comparison. let Some(candidate_class) = candidate.to_class_type(db) else { @@ -317,14 +322,14 @@ impl<'db> DynamicClassLiteral<'db> { }; // If base's metaclass is more derived, use it. - if base_metaclass_class.is_subclass_of(db, candidate_class) { + if base_metaclass_class.is_subclass_of(db, &env, candidate_class) { candidate = base_metaclass; candidate_base = base; continue; } // If candidate is already more derived, keep it. - if candidate_class.is_subclass_of(db, base_metaclass_class) { + if candidate_class.is_subclass_of(db, &env, base_metaclass_class) { continue; } @@ -353,14 +358,19 @@ impl<'db> DynamicClassLiteral<'db> { } /// Look up an instance member by iterating through the MRO. - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - match MroLookup::new(db, self.iter_mro(db)).instance_member(name) { + pub(crate) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + match MroLookup::new(db, env, self.iter_mro(db)).instance_member(name) { InstanceMemberResult::Done(result) => result, InstanceMemberResult::TypedDict => { // Simplified `TypedDict` handling without type mapping. KnownClass::TypedDictFallback - .to_instance(db) - .instance_member(db, name) + .to_instance(db, env) + .instance_member(db, env, name) } } } @@ -373,6 +383,7 @@ impl<'db> DynamicClassLiteral<'db> { pub(crate) fn class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { @@ -384,9 +395,10 @@ impl<'db> DynamicClassLiteral<'db> { // Make this class look like a subclass of the `DataClassInstance` protocol. return Place::declared(KnownClass::Dict.to_specialized_instance( db, + env, &[ - KnownClass::Str.to_instance(db), - KnownClass::Field.to_specialized_instance(db, &[Type::any()]), + KnownClass::Str.to_instance(db, env), + KnownClass::Field.to_specialized_instance(db, env, &[Type::any()]), ], )) .with_qualifiers(TypeQualifiers::CLASS_VAR); @@ -396,15 +408,15 @@ impl<'db> DynamicClassLiteral<'db> { } } - let result = MroLookup::new(db, self.iter_mro(db)).class_member( + let result = MroLookup::new(db, env, self.iter_mro(db)).class_member( name, policy, None, // No inherited generic context. false, // Dynamic classes are never `object`. ); match result { - ClassMemberResult::Done(result) => result.finalize(db), + ClassMemberResult::Done(result) => result.finalize(db, env), ClassMemberResult::TypedDict(module) => { - typed_dict_fallback_class_member(db, module, policy, name) + typed_dict_fallback_class_member(db, env, module, policy, name) } } } @@ -441,7 +453,7 @@ impl<'db> DynamicClassLiteral<'db> { cycle_initial=|db, _, self_: DynamicClassLiteral<'db>| { Ok(Mro::from([ ClassBase::Class(ClassType::NonGeneric(ClassLiteral::Dynamic(self_))), - ClassBase::object(db), + ClassBase::object(db, &ProgramEnvironment::from_scope(self_.scope(db))), ])) }, heap_size=ruff_memory_usage::heap_size @@ -464,9 +476,12 @@ impl<'db> DynamicClassLiteral<'db> { // Check if the slots are non-empty let is_non_empty = match ty { // __slots__ = ("a", "b") - Type::NominalInstance(nominal) => nominal.tuple_spec(db).is_some_and(|spec| { - spec.len().into_fixed_length().is_some_and(|len| len > 0) - }), + Type::NominalInstance(nominal) => { + let env = ProgramEnvironment::from_scope(self.scope(db)); + nominal.tuple_spec(db, &env).is_some_and(|spec| { + spec.len().into_fixed_length().is_some_and(|len| len > 0) + }) + } // __slots__ = "abc" # Same as ("abc",) Type::LiteralValue(literal) if literal.is_string() => true, // Other types are considered dynamic/unknown @@ -516,23 +531,24 @@ impl<'db> DynamicClassLiteral<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let anchor = self .anchor(db) - .recursive_type_normalized_impl(db, div, nested)?; + .recursive_type_normalized_impl(db, env, div, nested)?; let members = self .members(db) .iter() .map(|(name, ty)| { - let ty = ty.recursive_type_normalized_impl(db, div, true); + let ty = ty.recursive_type_normalized_impl(db, env, div, true); let ty = if nested { ty? } else { ty.unwrap_or(div) }; Some((name.clone(), ty)) }) .collect::>>()?; let dataclass_params = match self.dataclass_params(db) { - Some(params) => Some(params.recursive_type_normalized_impl(db, div, nested)?), + Some(params) => Some(params.recursive_type_normalized_impl(db, env, div, nested)?), None => None, }; diff --git a/crates/ty_python_semantic/src/types/class/enum_literal.rs b/crates/ty_python_semantic/src/types/class/enum_literal.rs index 2d9eec11c0..36535f410c 100644 --- a/crates/ty_python_semantic/src/types/class/enum_literal.rs +++ b/crates/ty_python_semantic/src/types/class/enum_literal.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use ruff_db::diagnostic::Span; use ruff_python_ast::name::Name; use ruff_text_size::TextRange; @@ -29,6 +30,7 @@ impl<'db> EnumSpec<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -36,7 +38,7 @@ impl<'db> EnumSpec<'db> { .members(db) .iter() .map(|(name, ty)| { - let ty = ty.recursive_type_normalized_impl(db, div, true); + let ty = ty.recursive_type_normalized_impl(db, env, div, true); let ty = if nested { ty? } else { ty.unwrap_or(div) }; Some((name.clone(), ty)) }) @@ -70,13 +72,14 @@ impl<'db> DynamicEnumAnchor<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Self::Definition { definition, spec } => Some(Self::Definition { definition: *definition, - spec: spec.recursive_type_normalized_impl(db, div, nested)?, + spec: spec.recursive_type_normalized_impl(db, env, div, nested)?, }), Self::ScopeOffset { scope, @@ -85,7 +88,7 @@ impl<'db> DynamicEnumAnchor<'db> { } => Some(Self::ScopeOffset { scope: *scope, offset: *offset, - spec: spec.recursive_type_normalized_impl(db, div, nested)?, + spec: spec.recursive_type_normalized_impl(db, env, div, nested)?, }), } } @@ -110,12 +113,13 @@ impl<'db> DynamicEnumLiteral<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let mixin_type = match self.mixin_type(db) { Some(mixin) => { - let mixin = mixin.recursive_type_normalized_impl(db, div, true); + let mixin = mixin.recursive_type_normalized_impl(db, env, div, true); Some(if nested { mixin? } else { mixin.unwrap_or(div) }) } None => None, @@ -125,7 +129,7 @@ impl<'db> DynamicEnumLiteral<'db> { db, self.name(db), self.anchor(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, self.base_class(db), mixin_type, )) @@ -160,7 +164,8 @@ impl<'db> DynamicEnumLiteral<'db> { if let Some(mixin) = self.mixin_type(db) { bases.push(mixin); } - bases.push(self.base_class(db).to_class_literal(db)); + let env = ProgramEnvironment::from_scope(self.scope(db)); + bases.push(self.base_class(db).to_class_literal(db, &env)); bases.into_boxed_slice() } @@ -180,9 +185,9 @@ impl<'db> DynamicEnumLiteral<'db> { Span::from(self.scope(db).file(db)).with_range(self.header_range(db)) } - #[expect(clippy::unused_self)] pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - KnownClass::EnumType.to_class_literal(db) + let env = ProgramEnvironment::from_scope(self.scope(db)); + KnownClass::EnumType.to_class_literal(db, &env) } #[salsa::tracked( @@ -191,7 +196,7 @@ impl<'db> DynamicEnumLiteral<'db> { cycle_initial=|db, _, self_: DynamicEnumLiteral<'db>| { Ok(Mro::from([ ClassBase::Class(ClassType::NonGeneric(ClassLiteral::DynamicEnum(self_))), - ClassBase::object(db), + ClassBase::object(db, &ProgramEnvironment::from_scope(self_.scope(db))), ])) } )] @@ -203,9 +208,9 @@ impl<'db> DynamicEnumLiteral<'db> { self.spec(db).has_known_members(db) } - fn mixin_class(self, db: &'db dyn Db) -> Option> { + fn mixin_class(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { let mixin = self.mixin_type(db)?; - let ClassBase::Class(class) = ClassBase::try_from_type(db, mixin, None)? else { + let ClassBase::Class(class) = ClassBase::try_from_type(db, env, mixin, None)? else { return None; }; Some(class) @@ -246,22 +251,27 @@ impl<'db> DynamicEnumLiteral<'db> { /// /// If members are unknown and nothing was found in the MRO, returns `Unknown` /// as a last resort to avoid false `unresolved-attribute` errors. - pub(crate) fn class_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub(crate) fn class_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { let own = self.own_class_member(db, name); if !own.is_undefined() { return own.inner; } - if let Some(mixin_class) = self.mixin_class(db) { - let result = mixin_class.class_member(db, name, MemberLookupPolicy::default()); + if let Some(mixin_class) = self.mixin_class(db, env) { + let result = mixin_class.class_member(db, env, name, MemberLookupPolicy::default()); if !result.place.is_undefined() { return result; } } let result = self .base_class(db) - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal() - .map(|cls| cls.class_member(db, name, MemberLookupPolicy::default())) + .map(|cls| cls.class_member(db, env, name, MemberLookupPolicy::default())) .unwrap_or_else(|| Place::Undefined.into()); // When members are unknown (e.g. `Enum("E", some_var)`), any name could @@ -275,17 +285,22 @@ impl<'db> DynamicEnumLiteral<'db> { /// /// If members are unknown and nothing was found, returns `Unknown` /// as a last resort. - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - if let Some(mixin_class) = self.mixin_class(db) { - let result = mixin_class.instance_member(db, name); + pub(crate) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + if let Some(mixin_class) = self.mixin_class(db, env) { + let result = mixin_class.instance_member(db, env, name); if !result.place.is_undefined() { return result; } } let result = self .base_class(db) - .to_instance(db) - .instance_member(db, name); + .to_instance(db, env) + .instance_member(db, env, name); self.with_unknown_member_fallback(db, result) } diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs index d058af0c7a..44f0a14586 100644 --- a/crates/ty_python_semantic/src/types/class/known.rs +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -1,5 +1,5 @@ use crate::{ - Db, Program, + Db, Program, ProgramEnvironment, place::{DefinedPlace, Definedness, Place, known_module_symbol}, types::{ Binding, ClassLiteral, ClassType, GenericContext, KnownInstanceType, StaticClassLiteral, @@ -13,7 +13,7 @@ use crate::{ known_instance::DeprecatedInstance, }, }; -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_python_ast as ast; use ruff_python_ast::PythonVersion; use rustc_hash::FxHashSet; @@ -891,7 +891,7 @@ impl KnownClass { } } - pub(crate) fn name(self, db: &dyn Db) -> &'static str { + pub(crate) fn name(self, python_version: PythonVersion) -> &'static str { match self { Self::Bool => "bool", Self::Object => "object", @@ -959,7 +959,7 @@ impl KnownClass { Self::Enum => "Enum", Self::EnumProperty => "property", Self::EnumType => { - if Program::get(db).python_version(db) >= PythonVersion::PY311 { + if python_version >= PythonVersion::PY311 { "EnumType" } else { "EnumMeta" @@ -1016,28 +1016,31 @@ impl KnownClass { } } - pub(crate) fn display(self, db: &dyn Db) -> impl std::fmt::Display + '_ { - struct KnownClassDisplay<'db> { - db: &'db dyn Db, + pub(crate) fn display(self, python_version: PythonVersion) -> impl std::fmt::Display { + struct KnownClassDisplay { class: KnownClass, + python_version: PythonVersion, } - impl std::fmt::Display for KnownClassDisplay<'_> { + impl std::fmt::Display for KnownClassDisplay { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let KnownClassDisplay { class: known_class, - db, + python_version, } = *self; write!( f, "{module}.{class}", - module = known_class.canonical_module(db), - class = known_class.name(db) + module = known_class.canonical_module(python_version), + class = known_class.name(python_version) ) } } - KnownClassDisplay { db, class: self } + KnownClassDisplay { + class: self, + python_version, + } } /// Look up a [`KnownClass`] in its canonical module and return a [`Type`] representing all @@ -1046,7 +1049,7 @@ impl KnownClass { /// /// If the class cannot be found, a debug-level log message will be emitted stating this. #[track_caller] - pub fn to_instance(self, db: &dyn Db) -> Type<'_> { + pub fn to_instance<'db>(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { debug_assert_ne!( self, KnownClass::Tuple, @@ -1056,30 +1059,35 @@ impl KnownClass { #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] fn known_class_to_instance<'db>( db: &'db dyn Db, - class: KnownClassArgument<'db>, + argument: KnownClassArgument<'db>, ) -> Type<'db> { - class + let env = &ProgramEnvironment::from_program(argument.program(db)); + argument .class(db) - .to_class_literal(db) + .to_class_literal(db, env) .to_class_type(db) - .map(|class| Type::instance(db, class)) + .map(|class| Type::instance(db, env, class)) .unwrap_or_else(Type::unknown) } - known_class_to_instance(db, KnownClassArgument::new(db, self)) + known_class_to_instance(db, KnownClassArgument::new(db, self, env.program(db))) } /// Similar to [`KnownClass::to_instance`], but returns the Unknown-specialization where each type /// parameter is specialized to `Unknown`. #[track_caller] - pub(crate) fn to_instance_unknown(self, db: &dyn Db) -> Type<'_> { + pub(crate) fn to_instance_unknown<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { debug_assert_ne!( self, KnownClass::Tuple, "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" ); - self.try_to_class_literal(db) - .map(|literal| Type::instance(db, literal.unknown_specialization(db))) + self.try_to_class_literal(db, env) + .map(|literal| Type::instance(db, env, literal.unknown_specialization(db))) .unwrap_or_else(Type::unknown) } @@ -1091,6 +1099,7 @@ impl KnownClass { pub(crate) fn to_specialized_class_type<'t, 'db, T>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: T, ) -> Option> where @@ -1099,6 +1108,7 @@ impl KnownClass { { fn to_specialized_class_type_impl<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: KnownClass, class_literal: StaticClassLiteral<'db>, specialization: Cow<[Type<'db>]>, @@ -1113,7 +1123,7 @@ impl KnownClass { tracing::info!( "Wrong number of types when specializing {}. \ Falling back to default specialization for the symbol instead.", - class.display(db) + class.display(env.python_version(db)) ); } return class_literal.default_specialization(db); @@ -1123,12 +1133,16 @@ impl KnownClass { .apply_specialization(db, |_| generic_context.specialize(db, specialization)) } - let class_literal = self.to_class_literal(db).as_class_literal()?.as_static()?; + let class_literal = self + .to_class_literal(db, env) + .as_class_literal()? + .as_static()?; let generic_context = class_literal.generic_context(db)?; let specialization = specialization.into(); Some(to_specialized_class_type_impl( db, + env, self, class_literal, specialization, @@ -1145,6 +1159,7 @@ impl KnownClass { pub(crate) fn to_specialized_instance<'t, 'db, T>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: T, ) -> Type<'db> where @@ -1156,27 +1171,31 @@ impl KnownClass { KnownClass::Tuple, "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" ); - self.to_specialized_class_type(db, specialization) - .and_then(|class_type| Type::from(class_type).to_instance_approximation(db)) + self.to_specialized_class_type(db, env, specialization) + .and_then(|class_type| Type::from(class_type).to_instance_approximation(db, env)) .unwrap_or_else(Type::unknown) } /// Look up a [`KnownClass`] in its canonical module. /// /// Lookup errors are logged when the cached query executes. - fn lookup_class_literal( + fn lookup_class_literal<'db>( self, - db: &dyn Db, - ) -> Result>, KnownClassLookupError<'_>> { + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Result>, KnownClassLookupError<'db>> { #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| Ok(None), heap_size=ruff_memory_usage::heap_size)] fn known_class_to_class_literal<'db>( db: &'db dyn Db, - class: KnownClassArgument<'db>, + argument: KnownClassArgument<'db>, ) -> Result>, KnownClassLookupError<'db>> { - let class = class.class(db); - let module = class.canonical_module(db); + let program = argument.program(db); + let env = &ProgramEnvironment::from_program(program); + let python_version = env.python_version(db); + let class = argument.class(db); + let module = class.canonical_module(python_version); let third_party = module.is_third_party(); - let symbol = known_module_symbol(db, module, class.name(db)).place; + let symbol = known_module_symbol(db, env, module, class.name(python_version)).place; let result = match symbol { Place::Defined(DefinedPlace { ty: Type::ClassLiteral(ClassLiteral::Static(class_literal)), @@ -1205,11 +1224,11 @@ impl KnownClass { lookup_error, KnownClassLookupError::ClassPossiblyUnbound { .. } ) { - tracing::info!("{}", lookup_error.display(db, class)); + tracing::info!("{}", lookup_error.display(db, env, class)); } else { tracing::info!( "{}. Falling back to `Unknown` for the symbol instead.", - lookup_error.display(db, class) + lookup_error.display(db, env, class) ); } } @@ -1217,15 +1236,19 @@ impl KnownClass { result } - known_class_to_class_literal(db, KnownClassArgument::new(db, self)) + known_class_to_class_literal(db, KnownClassArgument::new(db, self, env.program(db))) } /// Look up a [`KnownClass`] in its canonical module and return a [`Type`] representing that /// class literal. /// /// If the class cannot be found, a debug-level log message will be emitted stating this. - pub(crate) fn try_to_class_literal(self, db: &dyn Db) -> Option> { - match self.lookup_class_literal(db) { + pub(crate) fn try_to_class_literal<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + match self.lookup_class_literal(db, env) { Ok(class_literal) => class_literal, Err(KnownClassLookupError::ClassPossiblyUnbound { class_literal, .. }) => { Some(class_literal) @@ -1241,8 +1264,12 @@ impl KnownClass { /// class literal. /// /// If the class cannot be found, a debug-level log message will be emitted stating this. - pub(crate) fn to_class_literal(self, db: &dyn Db) -> Type<'_> { - self.try_to_class_literal(db) + pub(crate) fn to_class_literal<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.try_to_class_literal(db, env) .map(|class| Type::ClassLiteral(ClassLiteral::Static(class))) .unwrap_or_else(Type::unknown) } @@ -1251,41 +1278,48 @@ impl KnownClass { /// and all possible subclasses of the class. /// /// If the class cannot be found, a debug-level log message will be emitted stating this. - pub fn to_subclass_of(self, db: &dyn Db) -> Type<'_> { - self.to_class_literal(db) + pub fn to_subclass_of<'db>(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.to_class_literal(db, env) .to_class_type(db) - .map(|class| SubclassOfType::from(db, class)) + .map(|class| SubclassOfType::from(db, env, class)) .unwrap_or_else(SubclassOfType::subclass_of_unknown) } pub(crate) fn to_specialized_subclass_of<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: &[Type<'db>], ) -> Type<'db> { - self.to_specialized_class_type(db, specialization) - .map(|class_type| SubclassOfType::from(db, class_type)) + self.to_specialized_class_type(db, env, specialization) + .map(|class_type| SubclassOfType::from(db, env, class_type)) .unwrap_or_else(SubclassOfType::subclass_of_unknown) } /// Return `true` if this symbol can be resolved to a class definition `class` in its canonical /// module, *and* `class` is a subclass of `other`. - pub(crate) fn is_subclass_of<'db>(self, db: &'db dyn Db, other: ClassType<'db>) -> bool { - self.lookup_class_literal(db) + pub(crate) fn is_subclass_of<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: ClassType<'db>, + ) -> bool { + self.lookup_class_literal(db, env) .is_ok_and(|class| class.is_some_and(|class| class.is_subclass_of(db, None, other))) } pub(crate) fn when_subclass_of<'db, 'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: ClassType<'db>, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { - ConstraintSet::from_bool(constraints, self.is_subclass_of(db, other)) + ConstraintSet::from_bool(constraints, self.is_subclass_of(db, env, other)) } /// Return the module in which we should look up the definition for this class - fn canonical_module(self, db: &dyn Db) -> KnownModule { + fn canonical_module(self, python_version: PythonVersion) -> KnownModule { match self { Self::Bool | Self::Object @@ -1366,22 +1400,20 @@ impl KnownClass { | Self::ExtensionTypedDictFallback | Self::NewType => KnownModule::TypingExtensions, Self::TypeVarTuple => { - if Program::get(db).python_version(db) >= PythonVersion::PY311 { + if python_version >= PythonVersion::PY311 { KnownModule::Typing } else { KnownModule::TypingExtensions } } Self::Sentinel => { - if Program::get(db).python_version(db) >= PythonVersion::PY315 { + if python_version >= PythonVersion::PY315 { KnownModule::Builtins } else { KnownModule::TypingExtensions } } Self::NoDefaultType => { - let python_version = Program::get(db).python_version(db); - // typing_extensions has a 3.13+ re-export for the `typing.NoDefault` // singleton, but not for `typing._NoDefaultType`. So we need to switch // to `typing._NoDefaultType` for newer versions: @@ -1537,7 +1569,7 @@ impl KnownClass { pub(crate) fn try_from_file_and_name( db: &dyn Db, - file: File, + file: PythonFile<'_>, class_name: &str, ) -> Option { // We assert that this match is exhaustive over the right-hand side in the unit test @@ -1615,12 +1647,8 @@ impl KnownClass { "SupportsIndex" => &[Self::SupportsIndex], "Enum" => &[Self::Enum], "EnumMeta" => &[Self::EnumType], - "EnumType" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { - &[Self::EnumType] - } - "StrEnum" if Program::get(db).python_version(db) >= PythonVersion::PY311 => { - &[Self::StrEnum] - } + "EnumType" if file.python_version(db) >= PythonVersion::PY311 => &[Self::EnumType], + "StrEnum" if file.python_version(db) >= PythonVersion::PY311 => &[Self::StrEnum], "IntEnum" => &[Self::IntEnum], "Flag" => &[Self::Flag], "IntFlag" => &[Self::IntFlag], @@ -1655,15 +1683,16 @@ impl KnownClass { }; let module = file_to_module(db, file)?.known(db)?; + let python_version = file.python_version(db); candidates .iter() .copied() - .find(|&candidate| candidate.check_module(db, module)) + .find(|&candidate| candidate.check_module(python_version, module)) } /// Return `true` if the module of `self` matches `module` - fn check_module(self, db: &dyn Db, module: KnownModule) -> bool { + fn check_module(self, python_version: PythonVersion, module: KnownModule) -> bool { match self { Self::Bool | Self::Object @@ -1755,7 +1784,7 @@ impl KnownClass { | Self::PydanticBaseSettings | Self::PydanticConfigDict | Self::PydanticRootModel - | Self::PydanticStrict => module == self.canonical_module(db), + | Self::PydanticStrict => module == self.canonical_module(python_version), Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), Self::SpecialForm | Self::TypeAliasType @@ -1797,7 +1826,8 @@ impl KnownClass { // 2. The first parameter of the current function (typically `self` or `cls`) match overload.parameter_types() { [] => { - let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) + let Some(enclosing_class) = + nearest_enclosing_class(context.db(), index, scope) else { BoundSuperError::UnavailableImplicitArguments .report_diagnostic(context, call_expression.into()); @@ -1806,7 +1836,9 @@ impl KnownClass { }; // Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`. - if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class.into()) { + if CodeGeneratorKind::NamedTuple + .matches(context.db(), enclosing_class.into()) + { if let Some(builder) = context .report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression) { @@ -1843,10 +1875,11 @@ impl KnownClass { }; let definition = index.expect_single_definition(first_param); - let first_param = binding_type(db, definition); + let first_param = binding_type(context.db(), definition); let bound_super = BoundSuperType::build( db, + context.program_environment(), Type::ClassLiteral(ClassLiteral::Static(enclosing_class)), first_param, ) @@ -1859,8 +1892,12 @@ impl KnownClass { } [Some(pivot_class_type), Some(owner_type)] => { // Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`. - if let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) { - if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class.into()) { + if let Some(enclosing_class) = + nearest_enclosing_class(context.db(), index, scope) + { + if CodeGeneratorKind::NamedTuple + .matches(context.db(), enclosing_class.into()) + { if let Some(builder) = context .report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression) { @@ -1874,11 +1911,16 @@ impl KnownClass { } } - let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type) - .unwrap_or_else(|err| { - err.report_diagnostic(context, call_expression.into()); - Type::unknown() - }); + let bound_super = BoundSuperType::build( + db, + context.program_environment(), + *pivot_class_type, + *owner_type, + ) + .unwrap_or_else(|err| { + err.report_diagnostic(context, call_expression.into()); + Type::unknown() + }); overload.set_return_type(bound_super); } _ => {} @@ -1923,6 +1965,9 @@ impl KnownClass { struct KnownClassArgument { #[returns(copy)] class: KnownClass, + + #[returns(copy)] + program: Program, } /// Enumeration of ways in which looking up a [`KnownClass`] in its canonical module could fail. @@ -1952,19 +1997,31 @@ impl<'db> KnownClassLookupError<'db> { } } - fn display(&self, db: &'db dyn Db, class: KnownClass) -> impl std::fmt::Display + 'db { - struct ErrorDisplay<'db> { + fn display<'env>( + &self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + class: KnownClass, + ) -> impl std::fmt::Display + 'env { + struct ErrorDisplay<'env, 'db> { db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, class: KnownClass, error: KnownClassLookupError<'db>, } - impl std::fmt::Display for ErrorDisplay<'_> { + impl std::fmt::Display for ErrorDisplay<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let ErrorDisplay { db, class, error } = *self; - - let class = class.display(db); - let python_version = Program::get(db).python_version(db); + let db = self.db; + let ErrorDisplay { + db: _, + env, + class, + error, + } = self; + + let python_version = env.python_version(db); + let class = class.display(python_version); let location = if error.is_third_party() { "" } else { @@ -1980,7 +2037,7 @@ impl<'db> KnownClassLookupError<'db> { f, "Error looking up `{class}`{location}: expected to find a class definition \ on Python {python_version}, but found a symbol of type `{found_type}` instead", - found_type = found_type.display(db), + found_type = found_type.display(db, env), ), KnownClassLookupError::ClassPossiblyUnbound { .. } => write!( f, @@ -1993,6 +2050,7 @@ impl<'db> KnownClassLookupError<'db> { ErrorDisplay { db, + env, class, error: *self, } @@ -2011,24 +2069,29 @@ mod tests { #[test] fn known_class_roundtrip_from_str() { let mut db = setup_db(); - Program::get(&db) + ty_python_core::program::Program::get(&db) .set_python_version_with_source(&mut db) .to(PythonVersionWithSource { version: PythonVersion::latest_preview(), source: PythonVersionSource::default(), }); + let python_version = db.python_version(); for class in KnownClass::iter() { - if class.canonical_module(&db).is_third_party() { + if class.canonical_module(python_version).is_third_party() { continue; } - let class_name = class.name(&db); - let class_module = - resolve_module_confident(&db, &class.canonical_module(&db).name()).unwrap(); + let class_name = class.name(python_version); + let class_module = resolve_module_confident( + &db, + python_version, + &class.canonical_module(python_version).name(), + ) + .unwrap(); assert_eq!( KnownClass::try_from_file_and_name( &db, - class_module.file(&db).unwrap(), + class_module.python_file(&db).unwrap(), class_name ), Some(class), @@ -2041,26 +2104,28 @@ mod tests { fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_latest_version() { let mut db = setup_db(); - Program::get(&db) + ty_python_core::program::Program::get(&db) .set_python_version_with_source(&mut db) .to(PythonVersionWithSource { version: PythonVersion::latest_ty(), source: PythonVersionSource::default(), }); + let python_version = db.python_version(); + let env = db.program_environment(); for class in KnownClass::iter() { - if class.canonical_module(&db).is_third_party() { + if class.canonical_module(python_version).is_third_party() { continue; } // Check the class can be looked up successfully - class.try_to_class_literal(&db).unwrap(); + class.try_to_class_literal(&db, &env).unwrap(); // We can't call `KnownClass::Tuple.to_instance()`; // there are assertions to ensure that we always call `Type::homogeneous_tuple()` // or `Type::heterogeneous_tuple()` instead.` if class != KnownClass::Tuple { assert_ne!( - class.to_instance(&db), + class.to_instance(&db, &env), Type::unknown(), "Unexpectedly fell back to `Unknown` for `{class:?}`" ); @@ -2076,8 +2141,9 @@ mod tests { // and sort them according to the version they were added in. // This makes the test far faster as it minimizes the number of times // we need to change the Python version in the loop. + let python_version = db.python_version(); let mut classes: Vec<(KnownClass, PythonVersion)> = KnownClass::iter() - .filter(|class| !class.canonical_module(&db).is_third_party()) + .filter(|class| !class.canonical_module(python_version).is_third_party()) .map(|class| { let version_added = match class { KnownClass::Template => PythonVersion::PY314, @@ -2099,7 +2165,7 @@ mod tests { classes.sort_unstable_by_key(|(_, version)| *version); - let program = Program::get(&db); + let program = ty_python_core::program::Program::get(&db); let mut current_version = program.python_version(&db); for (class, version_added) in classes { @@ -2114,14 +2180,15 @@ mod tests { } // Check the class can be looked up successfully - class.try_to_class_literal(&db).unwrap(); + let env = db.program_environment(); + class.try_to_class_literal(&db, &env).unwrap(); // We can't call `KnownClass::Tuple.to_instance()`; // there are assertions to ensure that we always call `Type::homogeneous_tuple()` // or `Type::heterogeneous_tuple()` instead.` if class != KnownClass::Tuple { assert_ne!( - class.to_instance(&db), + class.to_instance(&db, &env), Type::unknown(), "Unexpectedly fell back to `Unknown` for `{class:?}` on Python {version_added}" ); diff --git a/crates/ty_python_semantic/src/types/class/named_tuple.rs b/crates/ty_python_semantic/src/types/class/named_tuple.rs index 2a52ed5b1d..0d150fb4fe 100644 --- a/crates/ty_python_semantic/src/types/class/named_tuple.rs +++ b/crates/ty_python_semantic/src/types/class/named_tuple.rs @@ -1,9 +1,10 @@ +use crate::ProgramEnvironment; use ruff_db::{diagnostic::Span, parsed::parsed_module}; use ruff_python_ast::{PythonVersion, name::Name}; use ruff_text_size::TextRange; use crate::{ - Db, Program, + Db, place::{Place, PlaceAndQualifiers}, types::{ BindingContext, BoundTypeVarInstance, ClassBase, ClassLiteral, ClassType, GenericContext, @@ -27,6 +28,7 @@ use ty_python_core::{definition::Definition, scope::ScopeId}; /// generic context in the synthesized `__new__` signature. pub(super) fn synthesize_namedtuple_class_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, instance_ty: Type<'db>, fields: impl Iterator>, @@ -35,8 +37,11 @@ pub(super) fn synthesize_namedtuple_class_member<'db>( match name { "__new__" => { // __new__(cls, field1, field2, ...) -> Self - let self_typevar = - BoundTypeVarInstance::synthetic_self(db, instance_ty, BindingContext::Synthetic); + let self_typevar = BoundTypeVarInstance::synthetic_self( + db, + instance_ty, + BindingContext::Synthetic(env.program(db)), + ); let self_ty = Type::TypeVar(self_typevar); let variables = inherited_generic_context @@ -44,12 +49,12 @@ pub(super) fn synthesize_namedtuple_class_member<'db>( .flat_map(|ctx| ctx.variables(db)) .chain(std::iter::once(self_typevar)); - let generic_context = GenericContext::from_typevar_instances(db, variables); + let generic_context = GenericContext::from_typevar_instances(db, env, variables); // CPython generates namedtuple `__new__` as `(_cls, field1, ...)` so field names like // `cls` remain usable as keyword arguments at call sites. let first_parameter = Parameter::positional_or_keyword(Name::new_static("_cls")) - .with_annotated_type(SubclassOfType::from(db, self_typevar)); + .with_annotated_type(SubclassOfType::from(db, env, self_typevar)); let parameters = std::iter::once(first_parameter).chain(fields.map(|field| { Parameter::positional_or_keyword(field.name) @@ -66,25 +71,25 @@ pub(super) fn synthesize_namedtuple_class_member<'db>( Some(Type::function_like_callable(db, signature)) } "__match_args__" => { - if Program::get(db).python_version(db) < PythonVersion::PY310 { + if env.python_version(db) < PythonVersion::PY310 { return None; } // __match_args__: tuple[Literal["field1"], Literal["field2"], ...] let field_types = fields.map(|field| Type::string_literal(db, &field.name)); - Some(Type::heterogeneous_tuple(db, field_types)) + Some(Type::heterogeneous_tuple(db, env, field_types)) } "_fields" => { // _fields: tuple[Literal["field1"], Literal["field2"], ...] let field_types = fields.map(|field| Type::string_literal(db, &field.name)); - Some(Type::heterogeneous_tuple(db, field_types)) + Some(Type::heterogeneous_tuple(db, env, field_types)) } "__slots__" => { // __slots__: tuple[()] - always empty for namedtuples - Some(Type::empty_tuple(db)) + Some(Type::empty_tuple(db, env)) } "_replace" | "__replace__" => { - if name == "__replace__" && Program::get(db).python_version(db) < PythonVersion::PY313 { + if name == "__replace__" && env.python_version(db) < PythonVersion::PY313 { return None; } @@ -92,7 +97,7 @@ pub(super) fn synthesize_namedtuple_class_member<'db>( let self_ty = Type::TypeVar(BoundTypeVarInstance::synthetic_self( db, instance_ty, - BindingContext::Synthetic, + BindingContext::Synthetic(env.program(db)), )); let first_parameter = Parameter::positional_or_keyword(Name::new_static("self")) @@ -115,10 +120,10 @@ pub(super) fn synthesize_namedtuple_class_member<'db>( _ => { // Fall back to NamedTupleFallback for other synthesized methods. KnownClass::NamedTupleFallback - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal()? .as_static()? - .own_class_member(db, inherited_generic_context, None, name) + .own_class_member(db, env, inherited_generic_context, None, name) .ignore_possibly_undefined() } } @@ -168,6 +173,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -175,7 +181,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { db, self.name(db), self.anchor(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, )) } } @@ -201,8 +207,8 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } /// Returns an instance type for this dynamic namedtuple. - fn to_instance(self, db: &'db dyn Db) -> Type<'db> { - Type::instance(db, ClassType::NonGeneric(self.into())) + fn to_instance(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + Type::instance(db, env, ClassType::NonGeneric(self.into())) } /// Returns the range of the namedtuple call expression. @@ -242,42 +248,49 @@ impl<'db> DynamicNamedTupleLiteral<'db> { #[salsa::tracked( returns(ref), heap_size=ruff_memory_usage::heap_size, - cycle_initial=|db, _, self_| Mro::from_error( - db, ClassType::NonGeneric(ClassLiteral::DynamicNamedTuple(self_)), + cycle_initial=|db, _, self_: DynamicNamedTupleLiteral<'db>| Mro::from_error( + db, + &ProgramEnvironment::from_scope(self_.scope(db)), + ClassType::NonGeneric(ClassLiteral::DynamicNamedTuple(self_)), ), )] pub(crate) fn mro(self, db: &'db dyn Db) -> Mro<'db> { + let env = ProgramEnvironment::from_scope(self.scope(db)); let self_base = ClassBase::Class(ClassType::NonGeneric(self.into())); - let tuple_class = self.tuple_base_class(db); + let tuple_class = self.tuple_base_class(db, &env); std::iter::once(self_base) .chain(tuple_class.iter_mro(db)) .collect() } - /// Get the metaclass of this dynamic namedtuple. + /// Returns the metaclass of this namedtuple. /// /// Namedtuples always have `type` as their metaclass. pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - let _ = self; - KnownClass::Type.to_class_literal(db) + let env = ProgramEnvironment::from_scope(self.scope(db)); + KnownClass::Type.to_class_literal(db, &env) } /// Compute the specialized tuple class that this namedtuple inherits from. /// /// For example, `namedtuple("Point", [("x", int), ("y", int)])` inherits from `tuple[int, int]`. - pub(crate) fn tuple_base_class(self, db: &'db dyn Db) -> ClassType<'db> { + pub(crate) fn tuple_base_class( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> ClassType<'db> { // If fields are unknown, return `tuple[Unknown, ...]` to avoid false positives // like index-out-of-bounds errors. if !self.has_known_fields(db) { - return TupleType::homogeneous(db, Type::unknown()).to_class_type(db); + return TupleType::homogeneous(db, env, Type::unknown()).to_class_type(db); } let field_types = self.fields(db).iter().map(|field| field.ty); - TupleType::heterogeneous(db, field_types) - .map(|t| t.to_class_type(db)) + TupleType::heterogeneous(db, env, field_types) + .map(|tuple| tuple.to_class_type(db)) .unwrap_or_else(|| { KnownClass::Tuple - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal() .expect("tuple should be a class literal") .default_specialization(db) @@ -297,7 +310,12 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } /// Look up an instance member by name (including superclasses). - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub(crate) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { // First check own instance members. let result = self.own_instance_member(db, name); if !result.is_undefined() { @@ -305,13 +323,14 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } // Fall back to the tuple base type for other attributes. - Type::instance(db, self.tuple_base_class(db)).instance_member(db, name) + Type::instance(db, env, self.tuple_base_class(db, env)).instance_member(db, env, name) } /// Look up a class-level member by name. pub(crate) fn class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { @@ -323,9 +342,9 @@ impl<'db> DynamicNamedTupleLiteral<'db> { // Fall back to tuple class members. let result = self - .tuple_base_class(db) + .tuple_base_class(db, env) .class_literal(db) - .class_member(db, name, policy); + .class_member(db, env, name, policy); // If fields are unknown (dynamic) and the attribute wasn't found, // return `Any` instead of failing. @@ -341,8 +360,9 @@ impl<'db> DynamicNamedTupleLiteral<'db> { /// This only checks synthesized members and field properties, without falling /// back to tuple or other base classes. pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + let env = ProgramEnvironment::from_scope(self.scope(db)); // Handle synthesized namedtuple attributes. - if let Some(ty) = self.synthesized_class_member(db, name) { + if let Some(ty) = self.synthesized_class_member(db, &env, name) { return Member::definitely_declared(ty); } @@ -357,8 +377,13 @@ impl<'db> DynamicNamedTupleLiteral<'db> { } /// Generate synthesized class members for namedtuples. - fn synthesized_class_member(self, db: &'db dyn Db, name: &str) -> Option> { - let instance_ty = self.to_instance(db); + fn synthesized_class_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Option> { + let instance_ty = self.to_instance(db, env); // When fields are unknown, handle constructor and field-specific methods specially. if !self.has_known_fields(db) { @@ -371,14 +396,15 @@ impl<'db> DynamicNamedTupleLiteral<'db> { // For other field-specific methods, fall through to NamedTupleFallback. "__match_args__" | "_fields" | "_replace" | "__replace__" => { return KnownClass::NamedTupleFallback - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal()? .as_static()? - .own_class_member(db, None, None, name) + .own_class_member(db, env, None, None, name) .ignore_possibly_undefined() .map(|ty| { ty.apply_type_mapping( db, + env, &TypeMapping::ReplaceSelf { new_upper_bound: instance_ty, }, @@ -392,6 +418,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { let result = synthesize_namedtuple_class_member( db, + env, name, instance_ty, self.fields(db).iter().cloned(), @@ -409,6 +436,7 @@ impl<'db> DynamicNamedTupleLiteral<'db> { result.map(|ty| { ty.apply_type_mapping( db, + env, &TypeMapping::ReplaceSelf { new_upper_bound: instance_ty, }, @@ -425,7 +453,8 @@ impl<'db> DynamicNamedTupleLiteral<'db> { heap_size=ruff_memory_usage::heap_size )] fn deferred_spec<'db>(db: &'db dyn Db, definition: Definition<'db>) -> NamedTupleSpec<'db> { - let module = parsed_module(db, definition.file(db)).load(db); + let python_file = definition.python_file(db); + let module = parsed_module(db, python_file).load(db); let node = definition .kind(db) .value(&module) @@ -520,13 +549,14 @@ impl<'db> DynamicNamedTupleAnchor<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Self::CollectionsDefinition { definition, spec } => Some(Self::CollectionsDefinition { definition: *definition, - spec: spec.recursive_type_normalized_impl(db, div, nested)?, + spec: spec.recursive_type_normalized_impl(db, env, div, nested)?, }), Self::TypingDefinition(definition) => Some(Self::TypingDefinition(*definition)), Self::ScopeOffset { @@ -536,7 +566,7 @@ impl<'db> DynamicNamedTupleAnchor<'db> { } => Some(Self::ScopeOffset { scope: *scope, offset: *offset, - spec: spec.recursive_type_normalized_impl(db, div, nested)?, + spec: spec.recursive_type_normalized_impl(db, env, div, nested)?, }), } } @@ -567,6 +597,7 @@ impl<'db> NamedTupleSpec<'db> { pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -574,11 +605,11 @@ impl<'db> NamedTupleSpec<'db> { .fields(db) .iter() .map(|f| { - let ty = f.ty.recursive_type_normalized_impl(db, div, true); + let ty = f.ty.recursive_type_normalized_impl(db, env, div, true); let ty = if nested { ty? } else { ty.unwrap_or(div) }; let default = match f.default { Some(default) => { - let default = default.recursive_type_normalized_impl(db, div, true); + let default = default.recursive_type_normalized_impl(db, env, div, true); Some(if nested { default? } else { diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index 3a1d32051d..d74015d87e 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -1,5 +1,7 @@ +use crate::ProgramEnvironment; use itertools::{Either, Itertools}; use ruff_db::{ + PythonFile, diagnostic::Span, files::File, parsed::{ParsedModuleRef, parsed_module}, @@ -10,7 +12,7 @@ use ruff_text_size::{Ranged, TextRange}; use std::cell::RefCell; use crate::{ - Db, FxIndexMap, FxIndexSet, Program, TypeQualifiers, + Db, FxIndexMap, FxIndexSet, TypeQualifiers, place::{ DefinedPlace, Definedness, Place, PlaceAndQualifiers, Provenance, PublicTypePolicy, TypeOrigin, place_from_bindings, place_from_declarations, @@ -142,11 +144,17 @@ impl<'db> FrozenDataclassDispatch<'db> { /// lookup must resume after the last frozen base. For example, assigning `Child().y` for /// `class Child(Frozen, Later)` uses `super(Frozen, child)` when `y` is not a field of `Frozen`; /// this preserves a later `__setattr__` or a descriptor for `y`. - pub(crate) fn receiver(self, db: &'db dyn Db, object_ty: Type<'db>) -> Type<'db> { + pub(crate) fn receiver( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + object_ty: Type<'db>, + ) -> Type<'db> { match self { Self::FrozenField => object_ty, Self::Delegate(frozen_base) => BoundSuperType::build( db, + env, Type::ClassLiteral(ClassLiteral::Static(frozen_base)), object_ty, ) @@ -396,11 +404,11 @@ impl<'db> StaticClassLiteral<'db> { )] fn pep695_generic_context_inner(self, db: &'db dyn Db) -> Option> { let scope = self.body_scope(db); - let file = scope.file(db); - let parsed = parsed_module(db, file).load(db); + let python_file = scope.python_file(db); + let parsed = parsed_module(db, python_file).load(db); let class_def_node = scope.node(db).expect_class().node(&parsed); class_def_node.type_params.as_ref().map(|type_params| { - let index = semantic_index(db, scope.file(db)); + let index = semantic_index(db, python_file); let definition = index.expect_single_definition(class_def_node); GenericContext::from_type_params(db, index, definition, type_params) }) @@ -453,13 +461,17 @@ impl<'db> StaticClassLiteral<'db> { self, db: &'db dyn Db, ) -> FxIndexSet> { - #[derive(Default)] - struct CollectTypeVars<'db> { + struct CollectTypeVars<'a, 'db> { + env: &'a ProgramEnvironment<'db>, typevars: RefCell>>, recursion_guard: TypeCollector<'db>, } - impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> { + impl<'db> TypeVisitor<'db> for CollectTypeVars<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -485,7 +497,12 @@ impl<'db> StaticClassLiteral<'db> { } } - let visitor = CollectTypeVars::default(); + let env = ProgramEnvironment::from_scope(self.body_scope(db)); + let visitor = CollectTypeVars { + env: &env, + typevars: RefCell::default(), + recursion_guard: TypeCollector::default(), + }; for base in self.explicit_bases(db) { visitor.visit_type(db, *base); } @@ -501,6 +518,10 @@ impl<'db> StaticClassLiteral<'db> { self.body_scope(db).file(db) } + pub(crate) fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.body_scope(db).python_file(db) + } + /// Return the original [`ast::StmtClassDef`] node associated with this class /// /// ## Note @@ -512,7 +533,7 @@ impl<'db> StaticClassLiteral<'db> { 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)); + let index = semantic_index(db, body_scope.python_file(db)); index.expect_single_definition(body_scope.node(db).expect_class()) } @@ -544,12 +565,13 @@ impl<'db> StaticClassLiteral<'db> { pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { self.apply_specialization(db, |generic_context| { + let env = ProgramEnvironment::from_program(generic_context.program(db)); generic_context .unknown_specialization(db, self.known(db)) .materialize_impl( db, MaterializationKind::Top, - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(&env), ) }) } @@ -603,12 +625,12 @@ impl<'db> StaticClassLiteral<'db> { class.name(db) ); - let module = parsed_module(db, class.file(db)).load(db); + let python_file = class.python_file(db); + let module = parsed_module(db, python_file).load(db); let class_stmt = class.node(db, &module); let class_definition = - semantic_index(db, class.file(db)).expect_single_definition(class_stmt); - + semantic_index(db, python_file).expect_single_definition(class_stmt); expanded_class_base_entries(db, class.known(db), class_stmt, class_definition) .into_iter() .map(ExpandedClassBaseEntry::ty) @@ -642,11 +664,12 @@ impl<'db> StaticClassLiteral<'db> { /// Iterate over this class's explicit bases, resolving them in the same way as MRO /// construction, filtering out any bases that are not fully static class objects. fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator> { + let env = ProgramEnvironment::from_scope(self.body_scope(db)); self.explicit_bases(db) .iter() .copied() .filter_map(move |ty| { - ClassBase::try_from_type(db, ty, Some(ClassLiteral::Static(self))) + ClassBase::try_from_type(db, &env, ty, Some(ClassLiteral::Static(self))) .and_then(ClassBase::into_class) }) } @@ -693,7 +716,8 @@ impl<'db> StaticClassLiteral<'db> { fn decorators_inner(self, db: &'db dyn Db) -> Box<[Type<'db>]> { tracing::trace!("StaticClassLiteral::decorators: {}", self.name(db)); - let module = parsed_module(db, self.file(db)).load(db); + let python_file = self.python_file(db); + let module = parsed_module(db, python_file).load(db); let class_stmt = self.node(db, &module); if class_stmt.decorator_list.is_empty() { @@ -701,7 +725,7 @@ impl<'db> StaticClassLiteral<'db> { } let class_definition = - semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + semantic_index(db, self.python_file(db)).expect_single_definition(class_stmt); class_stmt .decorator_list @@ -725,10 +749,10 @@ impl<'db> StaticClassLiteral<'db> { /// Iterate through the decorators on this class, returning the index of the first one /// that is either `@dataclass` or `@dataclass(...)`. pub(crate) fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option { - let module = parsed_module(db, self.file(db)).load(db); + let python_file = self.python_file(db); + let module = parsed_module(db, python_file).load(db); let class_stmt = self.node(db, &module); - let class_definition = - semantic_index(db, self.file(db)).expect_single_definition(class_stmt); + let class_definition = semantic_index(db, python_file).expect_single_definition(class_stmt); class_stmt.decorator_list.iter().position(|decorator| { let decorator_callable = decorator @@ -761,14 +785,15 @@ impl<'db> StaticClassLiteral<'db> { #[salsa::tracked( returns(as_ref), cycle_initial=|db, _, self_: StaticClassLiteral<'db>, specialization| { + let env = ProgramEnvironment::from_scope(self_.body_scope(db)); Err(StaticMroError::cycle( - db, + db, &env, self_.apply_optional_specialization(db, specialization), )) }, heap_size=ruff_memory_usage::heap_size )] - pub(crate) fn try_mro( + pub(in crate::types) fn try_mro( self, db: &'db dyn Db, specialization: Option>, @@ -880,7 +905,7 @@ impl<'db> StaticClassLiteral<'db> { return None; } - let module = parsed_module(db, self.file(db)).load(db); + let module = parsed_module(db, self.python_file(db)).load(db); let class_stmt = self.node(db, &module); Some(typed_dict_params_from_class_def(class_stmt)) } @@ -905,7 +930,7 @@ impl<'db> StaticClassLiteral<'db> { if let Some(transformer_params) = transformer_params.as_mut() && let Some(class_def) = self.definition(db).kind(db).as_class() { - let module = parsed_module(db, self.file(db)).load(db); + let module = parsed_module(db, self.python_file(db)).load(db); if let Some(arguments) = &class_def.node(&module).arguments { let mut flags = transformer_params.flags(db); @@ -1048,6 +1073,8 @@ impl<'db> StaticClassLiteral<'db> { db: &'db dyn Db, class: StaticClassLiteral<'db>, ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { + let python_file = class.python_file(db); + let env = ProgramEnvironment::from_file(python_file); tracing::trace!("StaticClassLiteral::try_metaclass: {}", class.name(db)); // Identify the class's own metaclass (or take the first base class's metaclass). @@ -1063,7 +1090,7 @@ impl<'db> StaticClassLiteral<'db> { return Ok((SubclassOfType::subclass_of_unknown(), None)); } - let module = parsed_module(db, class.file(db)).load(db); + let module = parsed_module(db, python_file).load(db); let explicit_metaclass = class.explicit_metaclass(db, &module); @@ -1075,7 +1102,7 @@ impl<'db> StaticClassLiteral<'db> { .specialization(db) .types(db) .iter() - .any(|ty| ty.has_typevar_or_typevar_instance(db)); + .any(|ty| ty.has_typevar_or_typevar_instance(db, &env)); if specialization_has_typevars { return Err(MetaclassError { kind: MetaclassErrorKind::GenericMetaclass, @@ -1095,7 +1122,7 @@ impl<'db> StaticClassLiteral<'db> { .unwrap_or(class); (base_class.metaclass(db), base_class_literal) } else { - (KnownClass::Type.to_class_literal(db), class) + (KnownClass::Type.to_class_literal(db, &env), class) }; let mut candidate = if let Some(metaclass_ty) = metaclass.to_class_type(db) { @@ -1105,15 +1132,18 @@ impl<'db> StaticClassLiteral<'db> { } } else { let name = Type::string_literal(db, class.name(db)); - let bases = Type::heterogeneous_tuple(db, class.explicit_bases(db)); - let namespace = KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]); + let bases = Type::heterogeneous_tuple(db, &env, class.explicit_bases(db)); + let namespace = KnownClass::Dict.to_specialized_instance( + db, + &env, + &[KnownClass::Str.to_instance(db, &env), Type::any()], + ); // TODO: Other keyword arguments? let arguments = CallArguments::positional([name, bases, namespace]); - let return_ty_result = match metaclass.try_call(db, &arguments) { - Ok(bindings) => Ok(bindings.return_type(db)), + let return_ty_result = match metaclass.try_call(db, &env, &arguments) { + Ok(bindings) => Ok(bindings.return_type(db, &env)), Err(CallError(CallErrorKind::NotCallable, bindings)) => Err(MetaclassError { kind: MetaclassErrorKind::NotCallable(bindings.callable_type()), @@ -1122,7 +1152,7 @@ impl<'db> StaticClassLiteral<'db> { // TODO we should also check for binding errors that would indicate the metaclass // does not accept the right arguments Err(CallError(CallErrorKind::BindingError, bindings)) => { - Ok(bindings.return_type(db)) + Ok(bindings.return_type(db, &env)) } Err(CallError(CallErrorKind::PossiblyNotCallable, _)) => Err(MetaclassError { @@ -1130,7 +1160,7 @@ impl<'db> StaticClassLiteral<'db> { }), }; - return return_ty_result.map(|ty| (ty.to_meta_type(db), None)); + return return_ty_result.map(|ty| (ty.to_meta_type(db, &env), None)); }; // Reconcile all base classes' metaclasses with the candidate metaclass. @@ -1149,14 +1179,14 @@ impl<'db> StaticClassLiteral<'db> { .static_class_literal(db) .map(|(lit, _)| lit) .unwrap_or(class); - if metaclass.is_subclass_of(db, candidate.metaclass) { + if metaclass.is_subclass_of(db, &env, candidate.metaclass) { candidate = MetaclassCandidate { metaclass, explicit_metaclass_of: base_class_literal, }; continue; } - if candidate.metaclass.is_subclass_of(db, metaclass) { + if candidate.metaclass.is_subclass_of(db, &env, metaclass) { continue; } return Err(MetaclassError { @@ -1185,7 +1215,8 @@ impl<'db> StaticClassLiteral<'db> { } if !self.has_explicit_bases(db) && !self.has_explicit_metaclass(db) { - return Ok((KnownClass::Type.to_class_literal(db), None)); + let env = ProgramEnvironment::from_scope(self.body_scope(db)); + return Ok((KnownClass::Type.to_class_literal(db, &env), None)); } try_metaclass_inner(db, self) } @@ -1198,30 +1229,37 @@ impl<'db> StaticClassLiteral<'db> { pub(super) fn class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - self.class_member_inner(db, None, name, policy) + self.class_member_inner(db, env, None, name, policy) } pub(super) fn class_member_inner( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization)) + self.class_member_from_mro(db, env, name, policy, self.iter_mro(db, specialization)) } pub(crate) fn class_member_from_mro( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, mro_iter: impl Iterator>, ) -> PlaceAndQualifiers<'db> { - fn into_function_like_callable<'d>(db: &'d dyn Db, ty: Type<'d>) -> Type<'d> { + fn into_function_like_callable<'d>( + db: &'d dyn Db, + env: &ProgramEnvironment<'d>, + ty: Type<'d>, + ) -> Type<'d> { match ty { Type::Callable(callable_ty) if callable_ty.is_regular(db) @@ -1229,16 +1267,17 @@ impl<'db> StaticClassLiteral<'db> { { Type::Callable(callable_ty.into_function_like(db)) } - Type::Union(union) => { - union.map(db, |element| into_function_like_callable(db, *element)) - } - Type::Intersection(intersection) => intersection - .map_positive(db, |element| into_function_like_callable(db, *element)), + Type::Union(union) => union.map(db, env, |element| { + into_function_like_callable(db, env, *element) + }), + Type::Intersection(intersection) => intersection.map_positive(db, env, |element| { + into_function_like_callable(db, env, *element) + }), _ => ty, } } - let result = MroLookup::new(db, mro_iter).class_member( + let result = MroLookup::new(db, env, mro_iter).class_member( name, policy, self.inherited_generic_context(db), @@ -1246,16 +1285,21 @@ impl<'db> StaticClassLiteral<'db> { ); let mut member = match result { - ClassMemberResult::Done(result) => result.finalize(db), - ClassMemberResult::TypedDict(module) => { - typed_dict_class_member(db, self.identity_specialization(db), module, policy, name) - } + ClassMemberResult::Done(result) => result.finalize(db, env), + ClassMemberResult::TypedDict(module) => typed_dict_class_member( + db, + env, + self.identity_specialization(db), + module, + policy, + name, + ), }; // We generally treat dunder attributes with `Callable` types as function-like callables. // See `callables_as_descriptors.md` for more details. if name.starts_with("__") && name.ends_with("__") { - member = member.map_type(|ty| into_function_like_callable(db, ty)); + member = member.map_type(|ty| into_function_like_callable(db, env, ty)); } member @@ -1270,11 +1314,16 @@ impl<'db> StaticClassLiteral<'db> { pub(super) fn own_class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, inherited_generic_context: Option>, specialization: Option>, name: &str, ) -> Member<'db> { - fn into_dunder_paramspec_callable<'d>(db: &'d dyn Db, ty: Type<'d>) -> Type<'d> { + fn into_dunder_paramspec_callable<'d>( + db: &'d dyn Db, + env: &ProgramEnvironment<'d>, + ty: Type<'d>, + ) -> Type<'d> { match ty { Type::Callable(callable_ty) if callable_ty.is_regular(db) @@ -1282,11 +1331,12 @@ impl<'db> StaticClassLiteral<'db> { { Type::Callable(callable_ty.into_dunder_paramspec(db)) } - Type::Union(union) => { - union.map(db, |element| into_dunder_paramspec_callable(db, *element)) - } - Type::Intersection(intersection) => intersection - .map_positive(db, |element| into_dunder_paramspec_callable(db, *element)), + Type::Union(union) => union.map(db, env, |element| { + into_dunder_paramspec_callable(db, env, *element) + }), + Type::Intersection(intersection) => intersection.map_positive(db, env, |element| { + into_dunder_paramspec_callable(db, env, *element) + }), _ => ty, } } @@ -1300,9 +1350,10 @@ impl<'db> StaticClassLiteral<'db> { return Member { inner: Place::declared(KnownClass::Dict.to_specialized_instance( db, + env, &[ - KnownClass::Str.to_instance(db), - KnownClass::Field.to_specialized_instance(db, &[Type::any()]), + KnownClass::Str.to_instance(db, env), + KnownClass::Field.to_specialized_instance(db, env, &[Type::any()]), ], )) .with_qualifiers(TypeQualifiers::CLASS_VAR), @@ -1335,7 +1386,7 @@ impl<'db> StaticClassLiteral<'db> { let body_scope = self.body_scope(db); let member = class_member(db, body_scope, name).map_type(|ty| { let ty = if name.starts_with("__") && name.ends_with("__") { - into_dunder_paramspec_callable(db, ty) + into_dunder_paramspec_callable(db, env, ty) } else { ty }; @@ -1365,9 +1416,13 @@ impl<'db> StaticClassLiteral<'db> { }); if member.is_undefined() { - if let Some(synthesized_member) = - self.own_synthesized_member(db, specialization, inherited_generic_context, name) - { + if let Some(synthesized_member) = self.own_synthesized_member( + db, + env, + specialization, + inherited_generic_context, + name, + ) { return Member::definitely_declared(synthesized_member); } // The symbol was not found in the class scope. It might still be implicitly defined in `@classmethod`s. @@ -1393,8 +1448,8 @@ impl<'db> StaticClassLiteral<'db> { // At runtime, the enum metaclass unwraps the value, so accessing the attribute // returns the inner value, not the `nonmember` wrapper. if let Some(ty) = member.inner.place.raw_type() - && let Some(value_ty) = try_unwrap_nonmember_value(db, ty) - && is_enum_class_by_inheritance(db, self) + && let Some(value_ty) = try_unwrap_nonmember_value(db, env, ty) + && is_enum_class_by_inheritance(db, env, self) { return Member::definitely_declared(value_ty); } @@ -1407,6 +1462,7 @@ impl<'db> StaticClassLiteral<'db> { pub(crate) fn own_synthesized_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, inherited_generic_context: Option>, name: &str, @@ -1434,9 +1490,9 @@ impl<'db> StaticClassLiteral<'db> { }) && self.has_ordering_method_in_mro(db, specialization) && let Some(root_method_ty) = self.total_ordering_root_method(db, specialization) - && let Some(callables) = root_method_ty.try_upcast_to_callable(db) + && let Some(callables) = root_method_ty.try_upcast_to_callable(db, env) { - let bool_ty = KnownClass::Bool.to_instance(db); + let bool_ty = KnownClass::Bool.to_instance(db, env); let synthesized_callables = callables.map(|callable| { let signatures = CallableSignature::from_overloads( callable.signatures(db).iter().map(|signature| { @@ -1445,7 +1501,7 @@ impl<'db> StaticClassLiteral<'db> { // def __gt__(self, other): return not (self == other or self < other) // If `__lt__` returns `int`, then `__gt__` could return `int | bool`. let return_ty = - UnionType::from_two_elements(db, signature.return_ty, bool_ty); + UnionType::from_two_elements(db, env, signature.return_ty, bool_ty); Signature::new_generic( signature.generic_context, signature.parameters().clone(), @@ -1461,7 +1517,7 @@ impl<'db> StaticClassLiteral<'db> { ) }); - return Some(synthesized_callables.into_type(db)); + return Some(synthesized_callables.into_type(db, env)); } // An ordinary subclass of a frozen dataclass is not itself dataclass-like, so the @@ -1471,7 +1527,7 @@ impl<'db> StaticClassLiteral<'db> { // frozen base fields. if let Some(method) = FrozenDataclassMethod::from_name(name) && let Some(synthesized_method) = - self.own_frozen_dataclass_subclass_method(db, specialization, method) + self.own_frozen_dataclass_subclass_method(db, env, specialization, method) { return Some(synthesized_method); } @@ -1483,8 +1539,11 @@ impl<'db> StaticClassLiteral<'db> { && field_policy.is_pydantic() && pydantic::constructor_fields_are_optional(db, self); - let instance_ty = - Type::instance(db, self.apply_optional_specialization(db, specialization)); + let instance_ty = Type::instance( + db, + env, + self.apply_optional_specialization(db, specialization), + ); let signature_from_fields = |mut parameters: Vec<_>, return_ty: Type<'db>| { if name == "__init__" && field_policy.is_pydantic() { @@ -1539,7 +1598,7 @@ impl<'db> StaticClassLiteral<'db> { continue; } - let dunder_set = field_ty.class_member(db, "__set__"); + let dunder_set = field_ty.class_member(db, env, "__set__"); if let Place::Defined(DefinedPlace { ty: dunder_set, definedness: Definedness::AlwaysDefined, @@ -1561,8 +1620,8 @@ impl<'db> StaticClassLiteral<'db> { // // We union parameter types across overloads of a single callable, intersect // callable bindings inside an intersection element, and union outer elements. - field_ty = dunder_set.bindings(db).map_types(db, |binding| { - let mut value_types = UnionBuilder::new(db); + field_ty = dunder_set.bindings(db, env).map_types(db, env, |binding| { + let mut value_types = UnionBuilder::new(db, env); let mut has_value_type = false; for overload in binding { if let Some(value_param) = @@ -1585,7 +1644,7 @@ impl<'db> StaticClassLiteral<'db> { if let Some(ref mut default_ty) = default_ty { *default_ty = default_ty - .try_call_dunder_get(db, None, Type::from(self)) + .try_call_dunder_get(db, env, None, Type::from(self)) .map(|(return_ty, _)| return_ty) .unwrap_or_else(Type::unknown); } @@ -1714,7 +1773,7 @@ impl<'db> StaticClassLiteral<'db> { let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) // TODO: could be `Self`. .with_annotated_type(instance_ty); - signature_from_fields(vec![self_parameter], Type::none(db)) + signature_from_fields(vec![self_parameter], Type::none(db, env)) } ( CodeGeneratorKind::NamedTuple, @@ -1723,14 +1782,15 @@ impl<'db> StaticClassLiteral<'db> { // When the namedtuple base has unknown fields, fall back to NamedTupleFallback // which has generic signatures that accept any arguments. KnownClass::NamedTupleFallback - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal()? .as_static()? - .own_class_member(db, inherited_generic_context, None, name) + .own_class_member(db, env, inherited_generic_context, None, name) .ignore_possibly_undefined() .map(|ty| { ty.apply_type_mapping( db, + env, &TypeMapping::ReplaceSelf { new_upper_bound: instance_ty, }, @@ -1757,6 +1817,7 @@ impl<'db> StaticClassLiteral<'db> { }); synthesize_namedtuple_class_member( db, + env, name, instance_ty, fields_iter, @@ -1780,7 +1841,7 @@ impl<'db> StaticClassLiteral<'db> { // TODO: could be `Self`. .with_annotated_type(instance_ty), ]), - KnownClass::Bool.to_instance(db), + KnownClass::Bool.to_instance(db, env), ); Some(Type::function_like_callable(db, signature)) @@ -1797,19 +1858,19 @@ impl<'db> StaticClassLiteral<'db> { "self", )) .with_annotated_type(instance_ty)]), - KnownClass::Int.to_instance(db), + KnownClass::Int.to_instance(db, env), ); Some(Type::function_like_callable(db, signature)) } else if eq && !frozen { - Some(Type::none(db)) + Some(Type::none(db, env)) } else { // No `__hash__` is generated, fall back to `object.__hash__` None } } (field_policy @ CodeGeneratorKind::DataclassLike(_), "__match_args__") - if Program::get(db).python_version(db) >= PythonVersion::PY310 => + if env.python_version(db) >= PythonVersion::PY310 => { if !self.has_dataclass_param(db, field_policy, DataclassFlags::MATCH_ARGS) { return None; @@ -1829,10 +1890,10 @@ impl<'db> StaticClassLiteral<'db> { } }) .map(|(name, _)| Type::string_literal(db, name)); - Some(Type::heterogeneous_tuple(db, match_args)) + Some(Type::heterogeneous_tuple(db, env, match_args)) } (field_policy @ CodeGeneratorKind::DataclassLike(_), "__weakref__") - if Program::get(db).python_version(db) >= PythonVersion::PY311 => + if env.python_version(db) >= PythonVersion::PY311 => { if !self.has_dataclass_param(db, field_policy, DataclassFlags::WEAKREF_SLOT) || !self.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) @@ -1844,23 +1905,26 @@ impl<'db> StaticClassLiteral<'db> { // model it precisely. Some(UnionType::from_two_elements( db, + env, Type::any(), - Type::none(db), + Type::none(db, env), )) } (CodeGeneratorKind::NamedTuple, name) if name != "__init__" => { KnownClass::NamedTupleFallback - .to_class_literal(db) + .to_class_literal(db, env) .as_class_literal()? .as_static()? - .own_class_member(db, self.inherited_generic_context(db), None, name) + .own_class_member(db, env, self.inherited_generic_context(db), None, name) .ignore_possibly_undefined() .map(|ty| { ty.apply_type_mapping( db, + env, &TypeMapping::ReplaceSelf { new_upper_bound: determine_upper_bound( db, + env, ClassLiteral::Static(self), |base| { base.into_class() @@ -1875,7 +1939,7 @@ impl<'db> StaticClassLiteral<'db> { ( CodeGeneratorKind::DataclassLike(_) | CodeGeneratorKind::Pydantic(_), "__replace__", - ) if Program::get(db).python_version(db) >= PythonVersion::PY313 => { + ) if env.python_version(db) >= PythonVersion::PY313 => { let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) .with_annotated_type(instance_ty); @@ -1918,17 +1982,18 @@ impl<'db> StaticClassLiteral<'db> { Some(Type::function_like_callable(db, signature)) } (field_policy @ CodeGeneratorKind::DataclassLike(_), "__slots__") - if Program::get(db).python_version(db) >= PythonVersion::PY310 => + if env.python_version(db) >= PythonVersion::PY310 => { self.has_dataclass_param(db, field_policy, DataclassFlags::SLOTS) .then(|| { let fields = self.fields(db, specialization, field_policy); let slots = fields.keys().map(|name| Type::string_literal(db, name)); - Type::heterogeneous_tuple(db, slots) + Type::heterogeneous_tuple(db, env, slots) }) } (CodeGeneratorKind::TypedDict, name) => synthesize_typed_dict_method( db, + env, instance_ty .as_typed_dict() .expect("TypedDict code generation should use a TypedDict instance"), @@ -1949,6 +2014,7 @@ impl<'db> StaticClassLiteral<'db> { fn own_frozen_dataclass_subclass_method( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, method: FrozenDataclassMethod, ) -> Option> { @@ -1959,8 +2025,11 @@ impl<'db> StaticClassLiteral<'db> { let frozen_base_fields = self.inherited_non_slotted_frozen_dataclass_fields(db, specialization, method.name())?; - let instance_ty = - Type::instance(db, self.apply_optional_specialization(db, specialization)); + let instance_ty = Type::instance( + db, + env, + self.apply_optional_specialization(db, specialization), + ); let method_signature = |name_ty, return_ty| { let self_parameter = Parameter::positional_or_keyword(Name::new_static("self")) .with_annotated_type(instance_ty); @@ -1984,8 +2053,8 @@ impl<'db> StaticClassLiteral<'db> { .iter() .map(|field| method_signature(Type::string_literal(db, field), Type::Never)) .chain([method_signature( - KnownClass::Str.to_instance(db), - Type::none(db), + KnownClass::Str.to_instance(db, env), + Type::none(db, env), )]); Some(Type::Callable(CallableType::new( @@ -2002,28 +2071,10 @@ impl<'db> StaticClassLiteral<'db> { /// instance of the exact frozen class. On an ordinary subclass instance, they reject only /// dataclass fields and delegate other names with `super(frozen_class, instance)`. /// - /// For example: - /// - /// ```python - /// @dataclass(frozen=True) - /// class Frozen: - /// x: int - /// - /// class Later: - /// y: int - /// - /// class Child(Frozen, Later): ... - /// ``` - /// - /// Assigning to `Child().x` is rejected because `x` is a field of `Frozen`. Assigning to - /// `Child().y` instead delegates to `super(Frozen, child).__setattr__`, where a later - /// `__setattr__` or the descriptor for `y` can still reject the assignment. - /// Deletion follows the same lookup through `__delattr__` and `__delete__`. - /// /// If multiple frozen dataclasses are reachable before an explicit implementation of - /// `method`, a non-field delegates past each generated method. [`FrozenDataclassDispatch::Delegate`] - /// stores the last frozen base so the caller can perform the equivalent lookup once, after all - /// of them. + /// `method`, a non-field delegates past each generated method. + /// [`FrozenDataclassDispatch::Delegate`] stores the last frozen base so the caller can perform + /// the equivalent lookup once, after all of them. pub(crate) fn inherited_frozen_dataclass_dispatch( self, db: &'db dyn Db, @@ -2138,11 +2189,12 @@ impl<'db> StaticClassLiteral<'db> { pub(crate) fn typed_dict_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - if let Some(member) = self.own_synthesized_member(db, specialization, None, name) { + if let Some(member) = self.own_synthesized_member(db, env, specialization, None, name) { Place::bound(member).into() } else { let class = match specialization { @@ -2154,7 +2206,7 @@ impl<'db> StaticClassLiteral<'db> { let Some(module) = self.typed_dict_module(db) else { return Place::Undefined.into(); }; - typed_dict_class_member(db, class, module, policy, name) + typed_dict_class_member(db, env, class, module, policy, name) } } @@ -2224,7 +2276,8 @@ impl<'db> StaticClassLiteral<'db> { }) .flat_map(|source| match source { FieldSource::Static(class, specialization) => { - let own_fields = class.own_fields_inner(db, specialization, field_policy); + let own_fields = + class.own_fields_with_class_variables(db, specialization, field_policy); if field_policy.is_dataclass_like() { class_variables.extend(own_fields.class_variables.iter().cloned()); @@ -2274,6 +2327,7 @@ impl<'db> StaticClassLiteral<'db> { pub(crate) fn validate_members(self, context: &InferContext<'db, '_>) { let db = context.db(); + let env = context.program_environment(); let Some(field_policy) = CodeGeneratorKind::from_static_class(db, self) else { return; }; @@ -2281,7 +2335,7 @@ impl<'db> StaticClassLiteral<'db> { let table = place_table(db, class_body_scope); let use_def = use_def_map(db, class_body_scope); for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { - let result = place_from_declarations(db, declarations.clone()); + let result = place_from_declarations(db, env, declarations.clone()); let attr = result.ignore_conflicting_declarations(); let symbol = table.symbol(symbol_id); let name = symbol.name(); @@ -2358,10 +2412,19 @@ impl<'db> StaticClassLiteral<'db> { field_policy: CodeGeneratorKind<'db>, ) -> &'db FxIndexMap> { &self - .own_fields_inner(db, specialization, field_policy) + .own_fields_with_class_variables(db, specialization, field_policy) .fields } + fn own_fields_with_class_variables( + self, + db: &'db dyn Db, + specialization: Option>, + field_policy: CodeGeneratorKind<'db>, + ) -> &'db OwnClassFields<'db> { + self.own_fields_inner(db, specialization, field_policy) + } + /// Collects ordered constructor fields and `ClassVar` masks in one pass over a class body. /// /// Keeping both together avoids reinterpreting declarations while merging inherited fields. @@ -2377,6 +2440,7 @@ impl<'db> StaticClassLiteral<'db> { field_policy: CodeGeneratorKind<'db>, ) -> OwnClassFields<'db> { let class_body_scope = self.body_scope(db); + let env = ProgramEnvironment::from_scope(class_body_scope); let table = place_table(db, class_body_scope); let use_def = use_def_map(db, class_body_scope); @@ -2435,7 +2499,7 @@ impl<'db> StaticClassLiteral<'db> { continue; }; - let result = place_from_declarations(db, declarations.clone()); + let result = place_from_declarations(db, &env, declarations.clone()); field_declarations.push((first_declaration_order, symbol_id, result)); } @@ -2460,7 +2524,7 @@ impl<'db> StaticClassLiteral<'db> { None } else { let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - place_from_bindings(db, bindings) + place_from_bindings(db, &env, bindings) .place .ignore_possibly_undefined() }; @@ -2639,6 +2703,7 @@ impl<'db> StaticClassLiteral<'db> { pub(super) fn instance_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, specialization: Option>, name: &str, ) -> PlaceAndQualifiers<'db> { @@ -2646,16 +2711,21 @@ impl<'db> StaticClassLiteral<'db> { return Place::Undefined.into(); } - match MroLookup::new(db, self.iter_mro(db, specialization)).instance_member(name) { + match MroLookup::new(db, env, self.iter_mro(db, specialization)).instance_member(name) { InstanceMemberResult::Done(result) => result, InstanceMemberResult::TypedDict => KnownClass::TypedDictFallback - .to_instance(db) - .instance_member(db, name) + .to_instance(db, env) + .instance_member(db, env, name) .map_type(|ty| { ty.apply_type_mapping( db, + env, &TypeMapping::ReplaceSelf { - new_upper_bound: Type::instance(db, self.unknown_specialization(db)), + new_upper_bound: Type::instance( + db, + env, + self.unknown_specialization(db), + ), }, TypeContext::default(), ) @@ -2707,19 +2777,20 @@ impl<'db> StaticClassLiteral<'db> { let class_body_scope = attribute.class_body_scope(db); let name = attribute.name(db).as_str(); let target_method_decorator = attribute.target_method_decorator(db); + let python_file = class_body_scope.python_file(db); + let env = &ProgramEnvironment::from_file(python_file); // If we do not see any declarations of an attribute, neither in the class body nor in // any method, we build a union of the raw types inferred from all bindings of that // attribute, then apply public-type promotion to the final union. - let mut union_of_inferred_types = UnionBuilder::new(db); + let mut union_of_inferred_types = UnionBuilder::new(db, env); let mut qualifiers = TypeQualifiers::IMPLICIT_INSTANCE_ATTRIBUTE; let mut is_attribute_bound = false; let mut provenance = Provenance::Unknown; - let file = class_body_scope.file(db); - let module = parsed_module(db, file).load(db); - let index = semantic_index(db, file); + let module = parsed_module(db, python_file).load(db); + let index = semantic_index(db, python_file); let class_map = use_def_map(db, class_body_scope); let class_table = place_table(db, class_body_scope); let is_valid_scope = |method_scope: &Scope| { @@ -2933,7 +3004,11 @@ impl<'db> StaticClassLiteral<'db> { TypeContext::default(), ); // TODO: Potential diagnostics resulting from the iterable are currently not reported. - Some(iterable_ty.iterate(db).homogeneous_element_type(db)) + Some( + iterable_ty + .iterate(db, env) + .homogeneous_element_type(db, env), + ) } }, DefinitionKind::WithItem(with_item) => match with_item.target_kind() { @@ -2956,9 +3031,9 @@ impl<'db> StaticClassLiteral<'db> { TypeContext::default(), ); Some(if with_item.is_async() { - context_ty.aenter(db) + context_ty.aenter(db, env) } else { - context_ty.enter(db) + context_ty.enter(db, env) }) } }, @@ -2983,7 +3058,11 @@ impl<'db> StaticClassLiteral<'db> { TypeContext::default(), ); // TODO: Potential diagnostics resulting from the iterable are currently not reported. - Some(iterable_ty.iterate(db).homogeneous_element_type(db)) + Some( + iterable_ty + .iterate(db, env) + .homogeneous_element_type(db, env), + ) } } } @@ -3010,8 +3089,8 @@ impl<'db> StaticClassLiteral<'db> { Place::bound( union_of_inferred_types .build() - .promote(db) - .promote_singletons(db), + .promote(db, env) + .promote_singletons(db, env), ) .with_provenance(provenance) .with_qualifiers(qualifiers) @@ -3023,7 +3102,12 @@ impl<'db> StaticClassLiteral<'db> { /// A helper function for `instance_member` that looks up the `name` attribute only on /// this class, not on its superclasses. - pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + pub(super) fn own_instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Member<'db> { // TODO: There are many things that are not yet implemented here: // - `typing.Final` // - Proper diagnostics @@ -3047,7 +3131,7 @@ impl<'db> StaticClassLiteral<'db> { let declarations = use_def.end_of_scope_symbol_declarations(symbol_id); let declared_and_qualifiers = - place_from_declarations(db, declarations).ignore_conflicting_declarations(); + place_from_declarations(db, env, declarations).ignore_conflicting_declarations(); match declared_and_qualifiers { PlaceAndQualifiers { @@ -3087,7 +3171,7 @@ impl<'db> StaticClassLiteral<'db> { // The attribute is declared in the class body. let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - let inferred = place_from_bindings(db, bindings).place; + let inferred = place_from_bindings(db, env, bindings).place; let has_binding = !inferred.is_undefined(); if has_binding { @@ -3113,6 +3197,7 @@ impl<'db> StaticClassLiteral<'db> { inner: Place::Defined(DefinedPlace { ty: UnionType::from_two_elements( db, + env, declared_ty, implicit_ty, ), @@ -3125,7 +3210,10 @@ impl<'db> StaticClassLiteral<'db> { } } } else if self.is_own_dataclass_instance_field(db, name) - && declared_ty.class_member(db, "__get__").place.is_undefined() + && declared_ty + .class_member(db, env, "__get__") + .place + .is_undefined() { // For dataclass-like classes, declared fields are assigned // by the synthesized `__init__`, so they are instance @@ -3177,6 +3265,7 @@ impl<'db> StaticClassLiteral<'db> { inner: Place::Defined(DefinedPlace { ty: UnionType::from_two_elements( db, + env, declared_ty, implicit_ty, ), @@ -3264,7 +3353,8 @@ impl<'db> StaticClassLiteral<'db> { } pub(super) fn to_non_generic_instance(self, db: &'db dyn Db) -> Type<'db> { - Type::instance(db, ClassType::NonGeneric(self.into())) + let env = ProgramEnvironment::from_scope(self.body_scope(db)); + Type::instance(db, &env, ClassType::NonGeneric(self.into())) } /// Return this class' involvement in an inheritance cycle, if any. @@ -3272,6 +3362,10 @@ impl<'db> StaticClassLiteral<'db> { /// A class definition like this will fail at runtime, /// but we must be resilient to it or we could panic. pub(crate) fn inheritance_cycle(self, db: &'db dyn Db) -> Option { + if !self.has_explicit_bases(db) { + return None; + } + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] fn inheritance_cycle_inner<'db>( db: &'db dyn Db, @@ -3316,7 +3410,6 @@ impl<'db> StaticClassLiteral<'db> { } tracing::trace!("Class::inheritance_cycle: {}", class.name(db)); - let visited_classes = &mut FxIndexSet::default(); if !is_cyclically_defined_recursive( db, @@ -3332,9 +3425,6 @@ impl<'db> StaticClassLiteral<'db> { } } - if !self.has_explicit_bases(db) { - return None; - } inheritance_cycle_inner(db, self) } @@ -3354,7 +3444,7 @@ impl<'db> StaticClassLiteral<'db> { /// ``` pub(crate) fn header_range(self, db: &'db dyn Db) -> TextRange { let class_scope = self.body_scope(db); - let module = parsed_module(db, class_scope.file(db)).load(db); + let module = parsed_module(db, class_scope.python_file(db)).load(db); let class_node = self.node(db, &module); let class_name = &class_node.name; TextRange::new( @@ -3370,7 +3460,7 @@ impl<'db> StaticClassLiteral<'db> { /// Returns the range of the class's name pub(crate) fn focus_range(self, db: &'db dyn Db) -> TextRange { let class_scope = self.body_scope(db); - let module = parsed_module(db, class_scope.file(db)).load(db); + let module = parsed_module(db, class_scope.python_file(db)).load(db); let class_node = self.node(db, &module); class_node.name.range() } @@ -3469,16 +3559,33 @@ fn expanded_fixed_length_starred_class_base_tuple<'db>( }; let starred_ty = definition_expression_type(db, class_definition, &starred.value); - let Tuple::Fixed(tuple) = starred_ty.tuple_instance_spec(db)?.into_owned() else { + let env = ProgramEnvironment::from_definition(class_definition); + let Tuple::Fixed(tuple) = starred_ty.tuple_instance_spec(db, &env)?.into_owned() else { return None; }; Some(tuple) } -#[salsa::tracked] impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { + fn variance_of( + self, + db: &'db dyn Db, + _: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.variance_of_owner(db, typevar) + } +} + +#[salsa::tracked] +impl<'db> StaticClassLiteral<'db> { #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size)] - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of_owner( + self, + db: &'db dyn Db, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + let env = ProgramEnvironment::from_scope(self.body_scope(db)); let typevar_in_generic_context = self .generic_context(db) .is_some_and(|generic_context| generic_context.contains(db, typevar)); @@ -3487,14 +3594,15 @@ impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { return TypeVarVariance::Bivariant; } let class_body_scope = self.body_scope(db); + let python_file = class_body_scope.python_file(db); + let python_version = env.python_version(db); - let file = class_body_scope.file(db); - let index = semantic_index(db, file); + let index = semantic_index(db, python_file); let explicit_bases_variances = self .explicit_bases(db) .iter() - .map(|class| class.variance_of(db, typevar)); + .map(|class| class.variance_of(db, &env, typevar)); let default_attribute_variance = { let is_namedtuple = CodeGeneratorKind::NamedTuple.matches(db, self.into()); @@ -3504,8 +3612,7 @@ impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { // not considered here, since they don't use field types in their signatures. TODO: // ideally we'd have a single source of truth for information about synthesized // methods, so we just look them up normally and don't hardcode this knowledge here. - let is_frozen_dataclass_prior_to_313 = Program::get(db).python_version(db) - <= PythonVersion::PY312 + let is_frozen_dataclass_prior_to_313 = python_version <= PythonVersion::PY312 && CodeGeneratorKind::from_static_class(db, self) .is_some_and(|kind| self.has_dataclass_param(db, kind, DataclassFlags::FROZEN)); @@ -3525,13 +3632,16 @@ impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { use_def_map .all_end_of_scope_symbol_declarations() .map(|(symbol_id, declarations)| { - let place_and_qual = - place_from_declarations(db, declarations).ignore_conflicting_declarations(); + let place_and_qual = place_from_declarations(db, &env, declarations) + .ignore_conflicting_declarations(); (symbol_id, place_and_qual) }) .chain(use_def_map.all_end_of_scope_symbol_bindings().map( |(symbol_id, bindings)| { - (symbol_id, place_from_bindings(db, bindings).place.into()) + ( + symbol_id, + place_from_bindings(db, &env, bindings).place.into(), + ) }, )) .filter_map(|(symbol_id, place_and_qual)| { @@ -3561,7 +3671,7 @@ impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { let attribute_variances = attribute_names .map(|name| { - let place_and_quals = self.own_instance_member(db, &name).inner; + let place_and_quals = self.own_instance_member(db, &env, &name).inner; (name, place_and_quals) }) .chain(attribute_places_and_qualifiers) @@ -3588,7 +3698,7 @@ impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { } else { default_attribute_variance }; - ty.with_polarity(variance).variance_of(db, typevar) + ty.with_polarity(variance).variance_of(db, &env, typevar) }) }); @@ -3603,7 +3713,7 @@ impl<'db> VarianceInferable<'db> for StaticClassLiteral<'db> { extra_items .declared_ty .with_polarity(polarity) - .variance_of(db, typevar) + .variance_of(db, &env, typevar) }); attribute_variances @@ -3634,7 +3744,7 @@ fn explicit_bases_cycle_initial<'db>( id: salsa::Id, literal: StaticClassLiteral<'db>, ) -> Box<[Type<'db>]> { - let module = parsed_module(db, literal.file(db)).load(db); + let module = parsed_module(db, literal.python_file(db)).load(db); let class_stmt = literal.node(db, &module); // Try to produce a list of `Divergent` types of the right length. However, if one or more of // the bases is a starred expression, we don't know how many entries that will eventually @@ -3647,15 +3757,16 @@ fn explicit_bases_cycle_fn<'db>( cycle: &salsa::Cycle, previous: &[Type<'db>], current: Box<[Type<'db>]>, - _literal: StaticClassLiteral<'db>, + literal: StaticClassLiteral<'db>, ) -> Box<[Type<'db>]> { if previous.len() == current.len() { + let env = ProgramEnvironment::from_scope(literal.body_scope(db)); // As long as the length of bases hasn't changed, use the same "monotonic widening" // strategy that we use with most types, to avoid oscillations. current .iter() .zip(previous.iter()) - .map(|(curr, prev)| curr.cycle_normalized(db, *prev, cycle)) + .map(|(curr, prev)| curr.cycle_normalized(db, &env, *prev, cycle)) .collect() } else { // The length of bases has changed, presumably because we expanded a starred expression. We @@ -3681,7 +3792,7 @@ impl get_size2::GetSize for ImplicitAttributeName<'_> {} #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] fn implicit_attribute_names<'db>(db: &'db dyn Db, class_body_scope: ScopeId<'db>) -> Box<[Name]> { - let index = semantic_index(db, class_body_scope.file(db)); + let index = semantic_index(db, class_body_scope.python_file(db)); let mut names = Vec::new(); for function_scope_id in attribute_scopes(db, class_body_scope) { @@ -3703,10 +3814,11 @@ fn implicit_attribute_cycle_recover<'db>( cycle: &salsa::Cycle, previous_member: &Member<'db>, member: Member<'db>, - _attribute: ImplicitAttributeName<'db>, + attribute: ImplicitAttributeName<'db>, ) -> Member<'db> { + let env = ProgramEnvironment::from_scope(attribute.class_body_scope(db)); let inner = member .inner - .cycle_normalized(db, previous_member.inner, cycle); + .cycle_normalized(db, &env, previous_member.inner, cycle); Member { inner } } diff --git a/crates/ty_python_semantic/src/types/class/typed_dict.rs b/crates/ty_python_semantic/src/types/class/typed_dict.rs index 810c2f185e..b4214af80b 100644 --- a/crates/ty_python_semantic/src/types/class/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/class/typed_dict.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use std::borrow::Cow; use itertools::Either; @@ -30,51 +31,67 @@ use ty_python_core::scope::ScopeId; pub(super) fn synthesize_typed_dict_method<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, method_name: &str, fields: impl Fn() -> TypedDictFields<'db>, ) -> Option> { let instance_ty = Type::TypedDict(typed_dict); match method_name { - "__init__" => Some(synthesize_typed_dict_init(db, typed_dict, fields())), - "__getitem__" => Some(synthesize_typed_dict_getitem(db, typed_dict, fields())), - "__setitem__" => Some(synthesize_typed_dict_setitem(db, typed_dict, fields())), - "__delitem__" => Some(synthesize_typed_dict_delitem(db, typed_dict, fields())), - "get" => Some(synthesize_typed_dict_get(db, typed_dict, fields())), - "update" => Some(synthesize_typed_dict_update(db, typed_dict, fields())), - "pop" => Some(synthesize_typed_dict_pop(db, typed_dict, fields())), - "setdefault" => Some(synthesize_typed_dict_setdefault(db, typed_dict, fields())), + "__init__" => Some(synthesize_typed_dict_init(db, env, typed_dict, fields())), + "__getitem__" => Some(synthesize_typed_dict_getitem(db, env, typed_dict, fields())), + "__setitem__" => Some(synthesize_typed_dict_setitem(db, env, typed_dict, fields())), + "__delitem__" => Some(synthesize_typed_dict_delitem(db, env, typed_dict, fields())), + "get" => Some(synthesize_typed_dict_get(db, env, typed_dict, fields())), + "update" => Some(synthesize_typed_dict_update(db, env, typed_dict, fields())), + "pop" => Some(synthesize_typed_dict_pop(db, env, typed_dict, fields())), + "setdefault" => Some(synthesize_typed_dict_setdefault( + db, + env, + typed_dict, + fields(), + )), "clear" if typed_dict.supports_arbitrary_key_deletion(db) => Some( - synthesize_typed_dict_no_argument_method(db, typed_dict, Type::none(db)), + synthesize_typed_dict_no_argument_method(db, typed_dict, Type::none(db, env)), ), "popitem" if typed_dict.supports_arbitrary_key_deletion(db) => { let return_ty = Type::heterogeneous_tuple( db, - [KnownClass::Str.to_instance(db), typed_dict.value_type(db)], + env, + [ + KnownClass::Str.to_instance(db, env), + typed_dict.value_type(db, env), + ], ); Some(synthesize_typed_dict_no_argument_method( db, typed_dict, return_ty, )) } "__iter__" if typed_dict.openness(db).is_closed() => { - let return_ty = - KnownClass::Iterator.to_specialized_instance(db, &[typed_dict.key_type(db)]); + let return_ty = KnownClass::Iterator.to_specialized_instance( + db, + env, + &[typed_dict.key_type(db, env)], + ); Some(synthesize_typed_dict_no_argument_method( db, typed_dict, return_ty, )) } "items" if !typed_dict.openness(db).is_implicitly_open() => Some( - synthesize_typed_dict_view_method(db, typed_dict, "dict_items"), + synthesize_typed_dict_view_method(db, env, typed_dict, "dict_items"), ), "keys" if !typed_dict.openness(db).is_implicitly_open() => Some( - synthesize_typed_dict_view_method(db, typed_dict, "dict_keys"), + synthesize_typed_dict_view_method(db, env, typed_dict, "dict_keys"), ), "values" if !typed_dict.openness(db).is_implicitly_open() => Some( - synthesize_typed_dict_view_method(db, typed_dict, "dict_values"), + synthesize_typed_dict_view_method(db, env, typed_dict, "dict_values"), ), - "__or__" | "__ror__" | "__ior__" => { - Some(synthesize_typed_dict_merge(db, instance_ty, method_name)) - } + "__or__" | "__ror__" | "__ior__" => Some(synthesize_typed_dict_merge( + db, + env, + instance_ty, + method_name, + )), _ => None, } } @@ -123,6 +140,7 @@ impl<'db> TypedDictFields<'db> { /// Keyword-only. Fields that are not valid Python identifiers are collapsed into `**kwargs`. fn synthesize_typed_dict_init<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -163,7 +181,7 @@ fn synthesize_typed_dict_init<'db>( .chain(params_with_default) .chain(keyword_rest_param.clone()), ), - Type::none(db), + Type::none(db, env), ); let keyword_field_params = keyword_fields.iter().map(|(name, field)| { @@ -183,7 +201,7 @@ fn synthesize_typed_dict_init<'db>( .chain(keyword_field_params) .chain(keyword_rest_param), ), - Type::none(db), + Type::none(db, env), ); Type::Callable(CallableType::new( @@ -197,6 +215,7 @@ fn synthesize_typed_dict_init<'db>( /// Synthesize the `__getitem__` method for a `TypedDict`. fn synthesize_typed_dict_getitem<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -218,10 +237,10 @@ fn synthesize_typed_dict_getitem<'db>( Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), ]), if typed_dict.explicit_extra_items(db).is_some() { - typed_dict.value_type(db) + typed_dict.value_type(db, env) } else { Type::object() }, @@ -238,6 +257,7 @@ fn synthesize_typed_dict_getitem<'db>( /// Synthesize the `__setitem__` method for a `TypedDict`. fn synthesize_typed_dict_setitem<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -246,7 +266,7 @@ fn synthesize_typed_dict_setitem<'db>( .iter() .filter(|(_, field)| !field.is_read_only()) .peekable(); - let arbitrary_key_mutation_type = typed_dict.arbitrary_key_mutation_type(db); + let arbitrary_key_mutation_type = typed_dict.arbitrary_key_mutation_type(db, env); if writable_fields.peek().is_none() && arbitrary_key_mutation_type.is_none() { let parameters = [ @@ -257,7 +277,7 @@ fn synthesize_typed_dict_setitem<'db>( Parameter::positional_only(Some(Name::new_static("value"))) .with_annotated_type(Type::any()), ]; - let signature = Signature::new(Parameters::standard(parameters), Type::none(db)); + let signature = Signature::new(Parameters::standard(parameters), Type::none(db, env)); return Type::function_like_callable(db, signature); } @@ -272,18 +292,18 @@ fn synthesize_typed_dict_setitem<'db>( Parameter::positional_only(Some(Name::new_static("value"))) .with_annotated_type(field.declared_ty), ]; - Signature::new(Parameters::standard(parameters), Type::none(db)) + Signature::new(Parameters::standard(parameters), Type::none(db, env)) }) .chain(arbitrary_key_mutation_type.map(|value_ty| { let parameters = [ Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), Parameter::positional_only(Some(Name::new_static("value"))) .with_annotated_type(value_ty), ]; - Signature::new(Parameters::standard(parameters), Type::none(db)) + Signature::new(Parameters::standard(parameters), Type::none(db, env)) })); Type::Callable(CallableType::new( @@ -297,6 +317,7 @@ fn synthesize_typed_dict_setitem<'db>( /// Synthesize the `__delitem__` method for a `TypedDict`. fn synthesize_typed_dict_delitem<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -314,7 +335,7 @@ fn synthesize_typed_dict_delitem<'db>( Parameter::positional_only(Some(Name::new_static("key"))) .with_annotated_type(Type::Never), ]; - let signature = Signature::new(Parameters::standard(parameters), Type::none(db)); + let signature = Signature::new(Parameters::standard(parameters), Type::none(db, env)); return Type::function_like_callable(db, signature); } @@ -327,16 +348,16 @@ fn synthesize_typed_dict_delitem<'db>( Parameter::positional_only(Some(Name::new_static("key"))) .with_annotated_type(key_type), ]; - Signature::new(Parameters::standard(parameters), Type::none(db)) + Signature::new(Parameters::standard(parameters), Type::none(db, env)) }) .chain(supports_arbitrary_key_deletion.then(|| { let parameters = [ Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), ]; - Signature::new(Parameters::standard(parameters), Type::none(db)) + Signature::new(Parameters::standard(parameters), Type::none(db, env)) })); Type::Callable(CallableType::new( @@ -350,6 +371,7 @@ fn synthesize_typed_dict_delitem<'db>( /// Synthesize the `get` method for a `TypedDict`. fn synthesize_typed_dict_get<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -357,7 +379,7 @@ fn synthesize_typed_dict_get<'db>( let fallback_value_ty = if typed_dict.openness(db).is_implicitly_open() { Type::unknown() } else { - typed_dict.value_type(db) + typed_dict.value_type(db, env) }; let overloads = fields .iter() @@ -375,12 +397,13 @@ fn synthesize_typed_dict_get<'db>( if field.is_required() { field.declared_ty } else { - UnionType::from_two_elements(db, field.declared_ty, Type::none(db)) + UnionType::from_two_elements(db, env, field.declared_ty, Type::none(db, env)) }, ); let t_default = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("T"), TypeVarVariance::Covariant, ); @@ -394,12 +417,17 @@ fn synthesize_typed_dict_get<'db>( .with_annotated_type(Type::TypeVar(t_default)), ]; let get_with_default_sig = Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [t_default])), + Some(GenericContext::from_typevar_instances(db, env, [t_default])), Parameters::standard(get_with_default_sig_params), if field.is_required() { field.declared_ty } else { - UnionType::from_two_elements(db, field.declared_ty, Type::TypeVar(t_default)) + UnionType::from_two_elements( + db, + env, + field.declared_ty, + Type::TypeVar(t_default), + ) }, ); @@ -432,13 +460,14 @@ fn synthesize_typed_dict_get<'db>( Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), ]), - UnionType::from_two_elements(db, fallback_value_ty, Type::none(db)), + UnionType::from_two_elements(db, env, fallback_value_ty, Type::none(db, env)), ))) .chain(std::iter::once({ let t_default = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("T"), TypeVarVariance::Covariant, ); @@ -447,15 +476,15 @@ fn synthesize_typed_dict_get<'db>( Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), Parameter::positional_only(Some(Name::new_static("default"))) .with_annotated_type(Type::TypeVar(t_default)), ]; Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [t_default])), + Some(GenericContext::from_typevar_instances(db, env, [t_default])), Parameters::standard(parameters), - UnionType::from_two_elements(db, fallback_value_ty, Type::TypeVar(t_default)), + UnionType::from_two_elements(db, env, fallback_value_ty, Type::TypeVar(t_default)), ) })); @@ -470,6 +499,7 @@ fn synthesize_typed_dict_get<'db>( /// Synthesize the `update` method for a `TypedDict`. fn synthesize_typed_dict_update<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -499,16 +529,26 @@ fn synthesize_typed_dict_update<'db>( let update_patch_ty = Type::TypedDict(typed_dict.to_update_patch(db)); - let mapping_ty = typed_dict.dict_value_type(db).map(|value_ty| { - KnownClass::Mapping - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), value_ty]) - }); - let iterable_ty = typed_dict.arbitrary_key_mutation_type(db).map(|value_ty| { - let item_ty = Type::heterogeneous_tuple(db, [KnownClass::Str.to_instance(db), value_ty]); - KnownClass::Iterable.to_specialized_instance(db, &[item_ty]) + let mapping_ty = typed_dict.dict_value_type(db, env).map(|value_ty| { + KnownClass::Mapping.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), value_ty], + ) }); + let iterable_ty = typed_dict + .arbitrary_key_mutation_type(db, env) + .map(|value_ty| { + let item_ty = Type::heterogeneous_tuple( + db, + env, + [KnownClass::Str.to_instance(db, env), value_ty], + ); + KnownClass::Iterable.to_specialized_instance(db, env, &[item_ty]) + }); let value_ty = UnionType::from_elements( db, + env, std::iter::once(update_patch_ty) .chain(mapping_ty) .chain(iterable_ty), @@ -518,18 +558,19 @@ fn synthesize_typed_dict_update<'db>( Parameter::positional_only(Some(Name::new_static("self"))).with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("value"))) .with_annotated_type(value_ty) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), ] .into_iter() .chain(keyword_parameters); - let update_signature = Signature::new(Parameters::standard(parameters), Type::none(db)); + let update_signature = Signature::new(Parameters::standard(parameters), Type::none(db, env)); Type::function_like_callable(db, update_signature) } /// Synthesize the `pop` method for a `TypedDict`. fn synthesize_typed_dict_pop<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -556,8 +597,12 @@ fn synthesize_typed_dict_pop<'db>( value_ty, ); - let t_default = - BoundTypeVarInstance::synthetic(db, Name::new_static("T"), TypeVarVariance::Covariant); + let t_default = BoundTypeVarInstance::synthetic( + db, + env, + Name::new_static("T"), + TypeVarVariance::Covariant, + ); let pop_with_default_parameters = [ Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), @@ -566,9 +611,9 @@ fn synthesize_typed_dict_pop<'db>( .with_annotated_type(Type::TypeVar(t_default)), ]; let pop_with_default_sig = Signature::new_generic( - Some(GenericContext::from_typevar_instances(db, [t_default])), + Some(GenericContext::from_typevar_instances(db, env, [t_default])), Parameters::standard(pop_with_default_parameters), - UnionType::from_two_elements(db, value_ty, Type::TypeVar(t_default)), + UnionType::from_two_elements(db, env, value_ty, Type::TypeVar(t_default)), ); [pop_sig, pop_with_typed_default_sig, pop_with_default_sig] @@ -583,7 +628,12 @@ fn synthesize_typed_dict_pop<'db>( .chain( typed_dict .supports_arbitrary_key_deletion(db) - .then(|| pop_overloads(KnownClass::Str.to_instance(db), typed_dict.value_type(db))) + .then(|| { + pop_overloads( + KnownClass::Str.to_instance(db, env), + typed_dict.value_type(db, env), + ) + }) .into_iter() .flatten(), ); @@ -599,6 +649,7 @@ fn synthesize_typed_dict_pop<'db>( /// Synthesize the `setdefault` method for a `TypedDict`. fn synthesize_typed_dict_setdefault<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, fields: TypedDictFields<'db>, ) -> Type<'db> { @@ -621,17 +672,20 @@ fn synthesize_typed_dict_setdefault<'db>( }) .chain( typed_dict - .arbitrary_key_mutation_type(db) + .arbitrary_key_mutation_type(db, env) .map(|default_ty| { let parameters = [ Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(instance_ty), Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), + .with_annotated_type(KnownClass::Str.to_instance(db, env)), Parameter::positional_only(Some(Name::new_static("default"))) .with_annotated_type(default_ty), ]; - Signature::new(Parameters::standard(parameters), typed_dict.value_type(db)) + Signature::new( + Parameters::standard(parameters), + typed_dict.value_type(db, env), + ) }), ); @@ -661,20 +715,23 @@ fn synthesize_typed_dict_no_argument_method<'db>( /// Synthesize `items`, `keys`, or `values` for a closed or extra-items `TypedDict`. fn synthesize_typed_dict_view_method<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, view_name: &str, ) -> Type<'db> { - let return_ty = known_module_symbol(db, KnownModule::CollectionsAbcInternal, view_name) + let return_ty = known_module_symbol(db, env, KnownModule::CollectionsAbcInternal, view_name) .place .ignore_possibly_undefined() .and_then(Type::as_class_literal) .map(|class| { class.apply_specialization(db, |generic_context| { - generic_context - .specialize(db, &[typed_dict.key_type(db), typed_dict.value_type(db)]) + generic_context.specialize( + db, + &[typed_dict.key_type(db, env), typed_dict.value_type(db, env)], + ) }) }) - .and_then(|class| Type::from(class).to_instance_approximation(db)) + .and_then(|class| Type::from(class).to_instance_approximation(db, env)) .unwrap_or_else(Type::unknown); synthesize_typed_dict_no_argument_method(db, typed_dict, return_ty) @@ -683,6 +740,7 @@ fn synthesize_typed_dict_view_method<'db>( /// Synthesize a merge operator (`__or__`, `__ror__`, or `__ior__`) for a `TypedDict`. fn synthesize_typed_dict_merge<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, instance_ty: Type<'db>, name: &str, ) -> Type<'db> { @@ -714,14 +772,18 @@ fn synthesize_typed_dict_merge<'db>( instance_ty }; - let dict_param_ty = KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]); + let dict_param_ty = KnownClass::Dict.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::any()], + ); let dict_return_ty = KnownClass::Dict.to_specialized_instance( db, + env, &[ - KnownClass::Str.to_instance(db), - KnownClass::Object.to_instance(db), + KnownClass::Str.to_instance(db, env), + KnownClass::Object.to_instance(db, env), ], ); @@ -790,6 +852,7 @@ impl<'db> DynamicTypedDictAnchor<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -803,8 +866,8 @@ impl<'db> DynamicTypedDictAnchor<'db> { } => Some(Self::ScopeOffset { scope: *scope, offset: *offset, - schema: schema.recursive_type_normalized_impl(db, div, nested)?, - openness: openness.recursive_type_normalized_impl(db, div, nested)?, + schema: schema.recursive_type_normalized_impl(db, env, div, nested)?, + openness: openness.recursive_type_normalized_impl(db, env, div, nested)?, }), } } @@ -836,6 +899,7 @@ impl<'db> DynamicTypedDictLiteral<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -843,7 +907,7 @@ impl<'db> DynamicTypedDictLiteral<'db> { db, self.name(db), self.anchor(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, self.typed_dict_module(db), )) } @@ -910,7 +974,8 @@ impl<'db> DynamicTypedDictLiteral<'db> { #[salsa::tracked(returns(ref), heap_size = ruff_memory_usage::heap_size)] pub(crate) fn mro(self, db: &'db dyn Db) -> Mro<'db> { let self_base = ClassBase::Class(ClassType::NonGeneric(self.into())); - let object_class = ClassType::object(db); + let env = ProgramEnvironment::from_scope(self.scope(db)); + let object_class = ClassType::object(db, &env); Mro::from([ self_base, ClassBase::TypedDict(self.typed_dict_module(db)), @@ -918,19 +983,20 @@ impl<'db> DynamicTypedDictLiteral<'db> { ]) } - /// Get the metaclass of this `TypedDict`. + /// Returns the metaclass of this `TypedDict`. /// /// `TypedDict`s use `type` as their metaclass. - #[expect(clippy::unused_self)] pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { - KnownClass::Type.to_class_literal(db) + let env = ProgramEnvironment::from_scope(self.scope(db)); + KnownClass::Type.to_class_literal(db, &env) } /// Look up a class-level member defined directly on this `TypedDict` (not inherited). pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> { + let env = ProgramEnvironment::from_scope(self.scope(db)); let typed_dict = TypedDictType::new(ClassType::NonGeneric(ClassLiteral::DynamicTypedDict(self))); - synthesize_typed_dict_method(db, typed_dict, name, || { + synthesize_typed_dict_method(db, &env, typed_dict, name, || { TypedDictFields::Dynamic(self.items(db)) }) .map(Member::definitely_declared) @@ -941,6 +1007,7 @@ impl<'db> DynamicTypedDictLiteral<'db> { pub(crate) fn class_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { @@ -954,6 +1021,7 @@ impl<'db> DynamicTypedDictLiteral<'db> { // This mirrors the behavior of StaticClassLiteral::typed_dict_member. typed_dict_class_member( db, + env, ClassType::NonGeneric(ClassLiteral::DynamicTypedDict(self)), self.typed_dict_module(db), policy, @@ -964,6 +1032,7 @@ impl<'db> DynamicTypedDictLiteral<'db> { pub(super) fn typed_dict_fallback_class_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, module: TypedDictModule, lookup_policy: MemberLookupPolicy, name: &str, @@ -974,34 +1043,39 @@ pub(super) fn typed_dict_fallback_class_member<'db>( }; fallback - .to_class_literal(db) - .find_name_in_mro_with_policy(db, name, lookup_policy) + .to_class_literal(db, env) + .find_name_in_mro_with_policy(db, env, name, lookup_policy) .expect("Will return Some() when called on class literal") } pub(super) fn typed_dict_class_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassType<'db>, module: TypedDictModule, lookup_policy: MemberLookupPolicy, name: &str, ) -> PlaceAndQualifiers<'db> { let self_class = class.class_literal(db); - let fallback_member = typed_dict_fallback_class_member(db, module, lookup_policy, name) + let fallback_member = typed_dict_fallback_class_member(db, env, module, lookup_policy, name) .map_type(|ty| { - let new_upper_bound = determine_upper_bound(db, self_class, ClassBase::is_typed_dict); + let new_upper_bound = + determine_upper_bound(db, env, self_class, ClassBase::is_typed_dict); let mapping = TypeMapping::ReplaceSelf { new_upper_bound }; - ty.apply_type_mapping(db, &mapping, TypeContext::default()) + ty.apply_type_mapping(db, env, &mapping, TypeContext::default()) }); if !fallback_member.is_undefined() { return fallback_member; } - if let Some(value_ty) = TypedDictType::new(class).dict_value_type(db) - && let Some(dict_class) = KnownClass::Dict - .to_specialized_class_type(db, &[KnownClass::Str.to_instance(db), value_ty]) + if let Some(value_ty) = TypedDictType::new(class).dict_value_type(db, env) + && let Some(dict_class) = KnownClass::Dict.to_specialized_class_type( + db, + env, + &[KnownClass::Str.to_instance(db, env), value_ty], + ) { - let member = dict_class.class_member(db, name, lookup_policy); + let member = dict_class.class_member(db, env, name, lookup_policy); if !member.is_undefined() { return member; } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index d93cedfb18..674f71f7e0 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use crate::types::class::CodeGeneratorKind; use crate::types::generics::{ApplySpecialization, Specialization}; use crate::types::mro::MroIterator; @@ -45,6 +46,7 @@ impl<'db> ClassBase<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -52,7 +54,7 @@ impl<'db> ClassBase<'db> { Self::Dynamic(dynamic) => Some(Self::Dynamic(dynamic.recursive_type_normalized())), Self::Divergent(_) => Some(self), Self::Class(class) => Some(Self::Class( - class.recursive_type_normalized_impl(db, div, nested)?, + class.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::Any | Self::Protocol | Self::Generic | Self::TypedDict(_) => Some(self), } @@ -79,8 +81,8 @@ impl<'db> ClassBase<'db> { } /// Return a `ClassBase` representing the class `builtins.object` - pub(super) fn object(db: &'db dyn Db) -> Self { - Self::Class(ClassType::object(db)) + pub(super) fn object(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + Self::Class(ClassType::object(db, env)) } pub(super) const fn is_typed_dict(self) -> bool { @@ -113,13 +115,14 @@ impl<'db> ClassBase<'db> { /// Convert an explicit base while preserving a direct use of the `Any` special form. pub(super) fn try_from_explicit_base( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, subclass: Option>, ) -> Option { if matches!(ty, Type::SpecialForm(SpecialFormType::Any)) { Some(Self::Any) } else { - Self::try_from_type(db, ty, subclass) + Self::try_from_type(db, env, ty, subclass) } } @@ -128,6 +131,7 @@ impl<'db> ClassBase<'db> { /// Return `None` if `ty` is not an acceptable type for a class base. pub(super) fn try_from_type( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, subclass: Option>, ) -> Option { @@ -139,7 +143,7 @@ impl<'db> ClassBase<'db> { Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::GenericAlias) => { - Self::try_from_type(db, todo_type!("GenericAlias instance"), subclass) + Self::try_from_type(db, env, todo_type!("GenericAlias instance"), subclass) } Type::SubclassOf(subclass_of) => subclass_of .subclass_of() @@ -149,9 +153,9 @@ impl<'db> ClassBase<'db> { let valid_element = inter .positive(db) .iter() - .find_map(|elem| ClassBase::try_from_type(db, *elem, subclass))?; + .find_map(|elem| ClassBase::try_from_type(db, env, *elem, subclass))?; - if ty.is_disjoint_from(db, KnownClass::Type.to_instance(db)) { + if ty.is_disjoint_from(db, env, KnownClass::Type.to_instance(db, env)) { None } else { Some(valid_element) @@ -180,7 +184,7 @@ impl<'db> ClassBase<'db> { if union .elements(db) .iter() - .all(|elem| ClassBase::try_from_type(db, *elem, subclass).is_some()) + .all(|elem| ClassBase::try_from_type(db, env, *elem, subclass).is_some()) { Some(ClassBase::Dynamic(*dynamic)) } else { @@ -193,10 +197,10 @@ impl<'db> ClassBase<'db> { // in which case we want to treat `Never` in a forgiving way and silence diagnostics Type::Never => Some(ClassBase::unknown()), - Type::TypeAlias(alias) => Self::try_from_type(db, alias.value_type(db), subclass), + Type::TypeAlias(alias) => Self::try_from_type(db, env, alias.value_type(db), subclass), Type::NewTypeInstance(newtype) => { - ClassBase::try_from_type(db, newtype.concrete_base_type(db), subclass) + ClassBase::try_from_type(db, env, newtype.concrete_base_type(db), subclass) } Type::PropertyInstance(_) @@ -245,13 +249,17 @@ impl<'db> ClassBase<'db> { | KnownInstanceType::FunctoolsPartial(_) | KnownInstanceType::FunctoolsPartialCall(_) => None, KnownInstanceType::TypeGenericAlias(_) => { - Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass) + Self::try_from_type( + db, env, + KnownClass::Type.to_class_literal(db, env), + subclass, + ) } KnownInstanceType::Annotated(ty) => { match ty.inner(db) { Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), Type::NominalInstance(instance) => { - Some(Self::Class(instance.class(db))) + Some(Self::Class(instance.class(db, env))) } _ => None, } @@ -298,8 +306,10 @@ impl<'db> ClassBase<'db> { let fields = class.own_fields(db, None, CodeGeneratorKind::NamedTuple); Self::try_from_type( db, + env, TupleType::heterogeneous( db, + env, fields.values().map(|field| field.declared_ty), )? .to_class_type(db) @@ -309,21 +319,31 @@ impl<'db> ClassBase<'db> { } // TODO: Classes inheriting from `typing.Type` also have `Generic` in their MRO - SpecialFormType::Type => { - Self::try_from_type(db, KnownClass::Type.to_class_literal(db), subclass) - } - - SpecialFormType::Tuple => { - Self::try_from_type(db, KnownClass::Tuple.to_class_literal(db), subclass) - } - - SpecialFormType::LegacyStdlibAlias(alias) => { - Self::try_from_type(db, alias.aliased_class().to_class_literal(db), subclass) - } + SpecialFormType::Type => Self::try_from_type( + db, + env, + KnownClass::Type.to_class_literal(db, env), + subclass, + ), + + SpecialFormType::Tuple => Self::try_from_type( + db, + env, + KnownClass::Tuple.to_class_literal(db, env), + subclass, + ), + + SpecialFormType::LegacyStdlibAlias(alias) => Self::try_from_type( + db, + env, + alias.aliased_class().to_class_literal(db, env), + subclass, + ), SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable => { Self::try_from_type( db, + env, todo_type!("Support for Callable as a base class"), subclass, ) @@ -345,14 +365,16 @@ impl<'db> ClassBase<'db> { } /// Return the metaclass of this class base. - pub(crate) fn metaclass(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn metaclass(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { Self::Class(class) => class.metaclass(db), Self::Any => Type::Dynamic(DynamicType::Any), Self::Dynamic(dynamic) => Type::Dynamic(dynamic), Self::Divergent(divergent) => Type::Divergent(divergent), // TODO: all `Protocol` classes actually have `_ProtocolMeta` as their metaclass. - Self::Protocol | Self::Generic | Self::TypedDict(_) => KnownClass::Type.to_instance(db), + Self::Protocol | Self::Generic | Self::TypedDict(_) => { + KnownClass::Type.to_instance(db, env) + } } } @@ -361,7 +383,7 @@ impl<'db> ClassBase<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { Self::Class(class) => { @@ -382,29 +404,36 @@ impl<'db> ClassBase<'db> { specialization: Option>, ) -> Self { if let Some(specialization) = specialization { + let env = + &ProgramEnvironment::from_program(specialization.generic_context(db).program(db)); let new_self = self.apply_type_mapping_impl( db, &TypeMapping::ApplySpecialization(ApplySpecialization::Specialization( specialization, )), TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ); match specialization.materialization_kind(db) { None => new_self, - Some(materialization_kind) => new_self.materialize(db, materialization_kind), + Some(materialization_kind) => new_self.materialize(db, env, materialization_kind), } } else { self } } - fn materialize(self, db: &'db dyn Db, kind: MaterializationKind) -> Self { + fn materialize( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + kind: MaterializationKind, + ) -> Self { self.apply_type_mapping_impl( db, &TypeMapping::Materialize(kind), TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ) } @@ -435,44 +464,54 @@ impl<'db> ClassBase<'db> { pub(super) fn mro( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, additional_specialization: Option>, ) -> impl Iterator> + Clone { match self { - ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic), + ClassBase::Protocol => { + ClassBaseMroIterator::length_3(db, env, self, ClassBase::Generic) + } ClassBase::Any | ClassBase::Dynamic(_) | ClassBase::Divergent(_) | ClassBase::Generic - | ClassBase::TypedDict(_) => ClassBaseMroIterator::length_2(db, self), + | ClassBase::TypedDict(_) => ClassBaseMroIterator::length_2(db, env, self), ClassBase::Class(class) => { ClassBaseMroIterator::from_class(db, class, additional_specialization) } } } - pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display { - self.display_with(db, DisplaySettings::default()) + pub(super) fn display( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> impl std::fmt::Display { + self.display_with(db, env, DisplaySettings::default()) } - pub(super) fn display_with( + pub(super) fn display_with<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, display_settings: DisplaySettings<'db>, - ) -> impl std::fmt::Display { - struct ClassBaseDisplay<'db> { + ) -> impl std::fmt::Display + 'env { + struct ClassBaseDisplay<'env, 'db> { db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, base: ClassBase<'db>, settings: DisplaySettings<'db>, } - impl std::fmt::Display for ClassBaseDisplay<'_> { + impl std::fmt::Display for ClassBaseDisplay<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; match self.base { ClassBase::Any => f.write_str("Any"), ClassBase::Dynamic(dynamic) => dynamic.fmt(f), ClassBase::Divergent(_) => f.write_str("Divergent"), ClassBase::Class(class) => Type::from(class) - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt(f), ClassBase::Protocol => f.write_str("typing.Protocol"), ClassBase::Generic => f.write_str("typing.Generic"), @@ -483,6 +522,7 @@ impl<'db> ClassBase<'db> { ClassBaseDisplay { db, + env, base: self, settings: display_settings, } @@ -525,13 +565,24 @@ enum ClassBaseMroIterator<'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()) + fn length_2( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + first_element: ClassBase<'db>, + ) -> Self { + ClassBaseMroIterator::Length2([first_element, ClassBase::object(db, env)].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()) + fn length_3( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + element_1: ClassBase<'db>, + element_2: ClassBase<'db>, + ) -> Self { + ClassBaseMroIterator::Length3( + [element_1, element_2, ClassBase::object(db, env)].into_iter(), + ) } /// Iterate over the MRO of an arbitrary class. The MRO may be of any length. diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 40acd2679e..f9f2f11107 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -101,6 +101,7 @@ use itertools::Itertools; use ruff_index::{Idx, IndexVec, newtype_index}; use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::SmallVec; +use ty_python_core::Program; use ty_python_core::rank::RankBitBox; use ty_static::EnvVars; @@ -116,7 +117,7 @@ use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, IntersectionType, Type, TypeContext, TypeMapping, TypePair, TypeVarBoundOrConstraints, TypeVarVariance, UnionType, }; -use crate::{Db, FxIndexMap, FxIndexSet, FxOrderSet}; +use crate::{Db, FxIndexMap, FxIndexSet, FxOrderSet, ProgramEnvironment}; mod support; @@ -418,17 +419,19 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// Returns a constraint set that constrains a typevar to an explicit range of types. pub(crate) fn constrain_typevar( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, typevar: BoundTypeVarInstance<'db>, lower: Type<'db>, upper: Type<'db>, ) -> Self { - Self::constrain_typevar_with_bounds(db, builder, typevar, Some(lower), Some(upper)) + Self::constrain_typevar_with_bounds(db, env, builder, typevar, Some(lower), Some(upper)) } /// Returns a constraint set that constrains a typevar with explicit lower and/or upper bounds. pub(crate) fn constrain_typevar_with_bounds( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, typevar: BoundTypeVarInstance<'db>, lower: Option>, @@ -436,28 +439,30 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { ) -> Self { let mut storage = builder.storage.borrow_mut(); let (node, source_order) = - Constraint::new_node_with_bounds(db, &mut storage, typevar, lower, upper); + Constraint::new_node_with_bounds(db, env, &mut storage, typevar, lower, upper); Self::from_node(builder, node, source_order) } /// Returns a constraint set that constrains a typevar to be a supertype of `lower`. pub(crate) fn constrain_typevar_lower_bound( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, typevar: BoundTypeVarInstance<'db>, lower: Type<'db>, ) -> Self { - Self::constrain_typevar_with_bounds(db, builder, typevar, Some(lower), None) + Self::constrain_typevar_with_bounds(db, env, builder, typevar, Some(lower), None) } /// Returns a constraint set that constrains a typevar to be a subtype of `upper`. pub(crate) fn constrain_typevar_upper_bound( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, typevar: BoundTypeVarInstance<'db>, upper: Type<'db>, ) -> Self { - Self::constrain_typevar_with_bounds(db, builder, typevar, None, Some(upper)) + Self::constrain_typevar_with_bounds(db, env, builder, typevar, None, Some(upper)) } /// Verifies that this constraint set was created by `builder` @@ -467,10 +472,10 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { } /// Returns whether this constraint set never holds. - pub(crate) fn is_never_satisfied(self, db: &'db dyn Db) -> bool { + pub(crate) fn is_never_satisfied(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { let mut storage = self.builder.storage.borrow_mut(); self.node - .is_never_satisfied(db, &mut storage, self.source_order) + .is_never_satisfied(db, env, &mut storage, self.source_order) } /// Returns whether this constraint set is the `never` terminal. @@ -483,10 +488,15 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { } /// Returns whether this constraint set always holds. - pub(crate) fn is_always_satisfied(self, db: &'db dyn Db) -> bool { + #[inline] + pub(crate) fn is_always_satisfied( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { let mut storage = self.builder.storage.borrow_mut(); self.node - .is_always_satisfied(db, &mut storage, self.source_order) + .is_always_satisfied(db, env, &mut storage, self.source_order) } /// Returns whether this constraint set is the `always` terminal. @@ -504,13 +514,16 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { pub(crate) fn implies_subtype_of( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, lhs: Type<'db>, rhs: Type<'db>, ) -> Self { self.verify_builder(builder); let mut storage = builder.storage.borrow_mut(); - let (node, extra_source_order) = self.node.implies_subtype_of(db, &mut storage, lhs, rhs); + let (node, extra_source_order) = + self.node + .implies_subtype_of(db, env, &mut storage, lhs, rhs); let source_order = storage.ordered_source_order(self.source_order, extra_source_order); Self::from_node(builder, node, source_order) } @@ -532,13 +545,14 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { pub(crate) fn satisfied_by_all_typevars( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, ) -> bool { self.verify_builder(builder); let mut storage = builder.storage.borrow_mut(); self.node - .satisfied_by_all_typevars(db, &mut storage, inferable, self.source_order) + .satisfied_by_all_typevars(db, env, &mut storage, inferable, self.source_order) } /// Updates this constraint set to hold the union of itself and another constraint set. @@ -656,6 +670,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { pub(crate) fn reduce_inferable( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, to_remove: TypeVarSet<'db>, ) -> Self { @@ -663,7 +678,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { let mut storage = builder.storage.borrow_mut(); let (node, derived_source_order) = self.node - .exists(db, &mut storage, to_remove, self.source_order); + .exists(db, env, &mut storage, to_remove, self.source_order); let source_order = storage.ordered_source_order(self.source_order, derived_source_order); Self::from_node(builder, node, source_order) } @@ -674,7 +689,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { db: &'db dyn Db, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { fn rebuild_node( storage: &mut ConstraintSetStorage<'_>, @@ -747,21 +762,24 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { .upper .map(|upper| upper.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + let env = visitor.env; let mut storage = self.builder.storage.borrow_mut(); let mapped = if let Type::TypeVar(typevar) = subject { - Constraint::new_node_with_bounds(db, &mut storage, typevar, lower, upper) + Constraint::new_node_with_bounds(db, env, &mut storage, typevar, lower, upper) } else { let (lower_holds, lower_holds_source_order) = match lower { Some(lower) => storage.load( db, - &lower.when_constraint_set_assignable_to_owned(db, subject), + env, + &lower.when_constraint_set_assignable_to_owned(db, env, subject), ), None => (ALWAYS_TRUE, None), }; let (upper_holds, upper_holds_source_order) = match upper { Some(upper) => storage.load( db, - &subject.when_constraint_set_assignable_to_owned(db, upper), + env, + &subject.when_constraint_set_assignable_to_owned(db, env, upper), ), None => (ALWAYS_TRUE, None), }; @@ -812,6 +830,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { pub(crate) fn for_all( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, to_remove: TypeVarSet<'db>, ) -> Self { @@ -823,7 +842,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { // Universal and existential quantification are duals. Reusing existential abstraction // also keeps this operation on its cached, single-pass implementation. self.negate(db, builder) - .reduce_inferable(db, builder, to_remove) + .reduce_inferable(db, env, builder, to_remove) .negate(db, builder) } @@ -839,65 +858,91 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { pub(crate) fn solutions( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, ) -> Solutions<'db> { - self.solutions_with(db, builder, inferable, |_variance, path_bound| { - PathBounds::default_solve(db, builder, path_bound) + self.solutions_with(db, env, builder, inferable, |_variance, path_bound| { + PathBounds::default_solve(db, env, builder, path_bound) }) } pub(crate) fn solutions_with( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, choose: impl FnMut(TypeVarVariance, &PathBound<'db>) -> Result>, ()>, ) -> Solutions<'db> { self.verify_builder(builder); let mut storage = builder.storage.borrow_mut(); - let path_bounds = - PathBounds::compute(db, &mut storage, self.node, inferable, self.source_order); + let path_bounds = PathBounds::compute( + db, + env, + &mut storage, + self.node, + inferable, + self.source_order, + ); drop(storage); path_bounds.solve_with(choose) } - pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { + pub(crate) fn display( + self, + db: &'db dyn Db, + env: &'c ProgramEnvironment<'db>, + ) -> impl Display + 'c { struct DisplayConstraintSet<'c, 'db> { node: NodeId, db: &'db dyn Db, + env: &'c ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, } impl Display for DisplayConstraintSet<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; let mut storage = self.builder.storage.borrow_mut(); - let node = self.node.simplify_for_display(self.db, &mut storage); - Display::fmt(&node.display(self.db, &mut storage), f) + let node = self.node.simplify_for_display(db, self.env, &mut storage); + Display::fmt(&node.display(db, self.env, &mut storage), f) } } DisplayConstraintSet { node: self.node, db, + env, builder: self.builder, } } #[expect(dead_code)] // Keep this around for debugging purposes - fn display_graph<'a>(self, db: &'db dyn Db, prefix: &'a dyn Display) -> impl Display { + fn display_graph<'a>( + self, + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + prefix: &'a dyn Display, + ) -> impl Display + 'a + where + 'db: 'a, + 'c: 'a, + { struct DisplayConstraintSet<'a, 'c, 'db> { node: NodeId, prefix: &'a dyn Display, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, builder: &'c ConstraintSetBuilder<'db>, } impl Display for DisplayConstraintSet<'_, '_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; let mut storage = self.builder.storage.borrow_mut(); - let node = self.node.simplify_for_display(self.db, &mut storage); - Display::fmt(&node.display_graph(self.db, &storage, self.prefix), f) + let node = self.node.simplify_for_display(db, self.env, &mut storage); + Display::fmt(&node.display_graph(db, self.env, &storage, self.prefix), f) } } @@ -905,6 +950,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { node: self.node, prefix, db, + env, builder: self.builder, } } @@ -1236,10 +1282,11 @@ impl<'db> ConstraintSetBuilder<'db> { pub(crate) fn load<'c>( &'c self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: &OwnedConstraintSet<'db>, ) -> ConstraintSet<'db, 'c> { let mut storage = self.storage.borrow_mut(); - let (node, source_order) = storage.load(db, other); + let (node, source_order) = storage.load(db, env, other); ConstraintSet::from_node(self, node, source_order) } } @@ -1262,16 +1309,22 @@ impl<'db> ConstraintSetStorage<'db> { fn intern_mentioned_typevars_in_type( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, support: &mut Support, ) { struct InternMentionedTypevars<'a, 'db> { + env: &'a ProgramEnvironment<'db>, storage: RefCell<&'a mut ConstraintSetStorage<'db>>, support: RefCell<&'a mut Support>, recursion_guard: TypeCollector<'db>, } impl<'db> TypeVisitor<'db> for InternMentionedTypevars<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -1294,6 +1347,7 @@ impl<'db> ConstraintSetStorage<'db> { } InternMentionedTypevars { + env, storage: RefCell::new(self), support: RefCell::new(support), recursion_guard: TypeCollector::default(), @@ -1305,22 +1359,28 @@ impl<'db> ConstraintSetStorage<'db> { fn intern_constraint_typevars( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar: BoundTypeVarInstance<'db>, bounds: ConstraintBounds<'db>, ) -> Support { let mut support = Support::default(); support.insert(self.intern_typevar(db, typevar)); if let Some(lower) = bounds.lower { - self.intern_mentioned_typevars_in_type(db, lower, &mut support); + self.intern_mentioned_typevars_in_type(db, env, lower, &mut support); } if let Some(upper) = bounds.upper { - self.intern_mentioned_typevars_in_type(db, upper, &mut support); + self.intern_mentioned_typevars_in_type(db, env, upper, &mut support); } support } - fn intern_constraint(&mut self, db: &'db dyn Db, data: Constraint<'db>) -> ConstraintId { - let support = self.intern_constraint_typevars(db, data.typevar, data.bounds); + fn intern_constraint( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + data: Constraint<'db>, + ) -> ConstraintId { + let support = self.intern_constraint_typevars(db, env, data.typevar, data.bounds); self.ensure_overlay_identity_caches(); if let Some(id) = self.constraint_cache.get(&data) { @@ -1379,13 +1439,14 @@ impl<'db> ConstraintSetStorage<'db> { fn cached_constraint_bound_depth( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraint: ConstraintId, ) -> (u16, u16) { if let Some(depth) = self.constraint_bound_depth_cache.get(&constraint) { return *depth; } - let depth = self.constraint_data(constraint).bound_depth(db); + let depth = self.constraint_data(constraint).bound_depth(db, env); self.constraint_bound_depth_cache.insert(constraint, depth); depth } @@ -1406,10 +1467,12 @@ impl<'db> ConstraintSetStorage<'db> { fn sequent_fuel_cost( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, constraint: ConstraintId, antecedent_constructor_depth: u16, ) -> u16 { - let (constructor_depth, typevar_depth) = self.cached_constraint_bound_depth(db, constraint); + let (constructor_depth, typevar_depth) = + self.cached_constraint_bound_depth(db, env, constraint); let constructor_growth = constructor_depth.saturating_sub(antecedent_constructor_depth); typevar_depth.max(constructor_growth).saturating_add(1) } @@ -1417,6 +1480,7 @@ impl<'db> ConstraintSetStorage<'db> { fn cached_constraint_implies( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ante: ConstraintId, post: ConstraintId, ) -> bool { @@ -1425,7 +1489,7 @@ impl<'db> ConstraintSetStorage<'db> { return *result; } - let result = ante.implies(db, self, post); + let result = ante.implies(db, env, self, post); self.constraint_implication_cache.insert(key, result); result } @@ -1433,6 +1497,7 @@ impl<'db> ConstraintSetStorage<'db> { fn cached_is_constraint_set_subtype_of( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, source: Type<'db>, target: Type<'db>, ) -> bool { @@ -1441,7 +1506,7 @@ impl<'db> ConstraintSetStorage<'db> { return *result; } - let result = source.is_constraint_set_subtype_of(db, target); + let result = source.is_constraint_set_subtype_of(db, env, target); self.constraint_set_subtype_cache.insert(key, result); result } @@ -1602,6 +1667,7 @@ impl<'db> ConstraintSetStorage<'db> { fn load( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: &OwnedConstraintSet<'db>, ) -> (NodeId, Option) { fn rebuild_node<'db>( @@ -1655,6 +1721,7 @@ impl<'db> ConstraintSetStorage<'db> { .map(|old_constraint| { Constraint::new_node_with_bounds( db, + env, self, old_constraint.typevar, old_constraint.bounds.lower, @@ -1873,11 +1940,15 @@ impl<'db> UpperBound<'db> { /// `S & (int | str)` into `(S & int) | (S & str)` would otherwise lose `S` as the single /// effective bound. Returns `None` instead of materializing intersections when no existing /// clause dominates the others. A missing bound remains distinct from an explicit `object`. - pub(crate) fn as_single_bound(&self, db: &'db dyn Db) -> Option> { + pub(crate) fn as_single_bound( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let mut clauses = self.clauses.iter().copied(); let first = clauses.next()?; let candidate = clauses.fold(first, |candidate, clause| { - if candidate.is_redundant_with(db, clause) { + if candidate.is_redundant_with(db, env, clause) { candidate } else { clause @@ -1886,7 +1957,7 @@ impl<'db> UpperBound<'db> { self.clauses .iter() - .all(|clause| candidate.is_redundant_with(db, *clause)) + .all(|clause| candidate.is_redundant_with(db, env, *clause)) .then_some(candidate) } @@ -1915,32 +1986,42 @@ impl<'db> UpperBound<'db> { /// Exact conversion to an ordinary [`Type`]. This may be expensive: if any stored clause is a /// union, [`IntersectionType::from_elements`] converts this factored CNF representation into /// ty's ordinary DNF representation by distributing intersections over unions. - pub(crate) fn materialize_exact(&self, db: &'db dyn Db) -> Type<'db> { - IntersectionType::from_elements(db, self.clauses.iter().copied()) + pub(crate) fn materialize_exact( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + IntersectionType::from_elements(db, env, self.clauses.iter().copied()) } fn has_visible_union_clause(&self) -> bool { self.clauses.iter().copied().any(Type::is_union) } - fn is_satisfied_by(&self, db: &'db dyn Db, ty: Type<'db>) -> bool { + fn is_satisfied_by( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { self.clauses .iter() - .all(|clause| ty.is_constraint_set_assignable_to(db, *clause)) + .all(|clause| ty.is_constraint_set_assignable_to(db, env, *clause)) } /// Returns the constraints under which `lower` is assignable to every stored upper clause. fn when_satisfied_by( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, lower: Type<'db>, ) -> (NodeId, Option) { let mut node = ALWAYS_TRUE; let mut source_order = None; for clause in &self.clauses { - let when_clause = lower.when_constraint_set_assignable_to_owned(db, *clause); - let (clause_node, clause_source_order) = storage.load(db, &when_clause); + let when_clause = lower.when_constraint_set_assignable_to_owned(db, env, *clause); + let (clause_node, clause_source_order) = storage.load(db, env, &when_clause); node = node.and(storage, clause_node); source_order = storage.ordered_source_order(source_order, clause_source_order); if node == ALWAYS_FALSE { @@ -1954,16 +2035,18 @@ impl<'db> UpperBound<'db> { impl ConstraintId { fn new<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, typevar: BoundTypeVarInstance<'db>, lower: Type<'db>, upper: Type<'db>, ) -> ConstraintId { - Self::new_with_bounds(db, storage, typevar, Some(lower), Some(upper)) + Self::new_with_bounds(db, env, storage, typevar, Some(lower), Some(upper)) } fn new_with_bounds<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, typevar: BoundTypeVarInstance<'db>, lower: Option>, @@ -1971,6 +2054,7 @@ impl ConstraintId { ) -> ConstraintId { storage.intern_constraint( db, + env, Constraint { typevar, bounds: ConstraintBounds::new(lower, upper), @@ -1984,20 +2068,30 @@ impl ConstraintId { /// /// Atomic types and bare typevars have constructor depth zero. The typevar depth is `0` if `ty` /// does not contain any typevars. -fn max_constructor_and_typevar_depth<'db>(db: &'db dyn Db, ty: Type<'db>) -> (u16, u16) { +fn max_constructor_and_typevar_depth<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> (u16, u16) { fn max_constructor_and_typevar_depth_impl<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, _dummy: (), ) -> (u16, u16) { - struct TypeDepthVisitor<'db> { + struct TypeDepthVisitor<'a, 'db> { + env: &'a ProgramEnvironment<'db>, active: RefCell>>, current_depth: Cell, max_constructor_depth: Cell, max_typevar_depth: Cell, } - impl<'db> TypeVisitor<'db> for TypeDepthVisitor<'db> { + impl<'db> TypeVisitor<'db> for TypeDepthVisitor<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -2028,6 +2122,7 @@ fn max_constructor_and_typevar_depth<'db>(db: &'db dyn Db, ty: Type<'db>) -> (u1 } let visitor = TypeDepthVisitor { + env, active: RefCell::default(), current_depth: Cell::default(), max_constructor_depth: Cell::default(), @@ -2040,15 +2135,15 @@ fn max_constructor_and_typevar_depth<'db>(db: &'db dyn Db, ty: Type<'db>) -> (u1 ) } - max_constructor_and_typevar_depth_impl(db, ty, ()) + max_constructor_and_typevar_depth_impl(db, env, ty, ()) } impl<'db> Constraint<'db> { - fn bound_depth(self, db: &'db dyn Db) -> (u16, u16) { + fn bound_depth(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> (u16, u16) { let both_bounds = iter::chain(self.bounds.lower, self.bounds.upper); both_bounds.fold((0, 0), |(constructor_depth, typevar_depth), bound| { let (bound_constructor_depth, bound_typevar_depth) = - max_constructor_and_typevar_depth(db, bound); + max_constructor_and_typevar_depth(db, env, bound); ( constructor_depth.max(bound_constructor_depth), typevar_depth.max(bound_typevar_depth), @@ -2079,6 +2174,7 @@ impl<'db> Constraint<'db> { /// Panics if present `lower` and `upper` bounds are not fully static. fn new_node_with_bounds( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, typevar: BoundTypeVarInstance<'db>, mut lower: Option>, @@ -2102,6 +2198,7 @@ impl<'db> Constraint<'db> { for lower_element in lower_union.elements(db) { let (element_node, element_source_order) = Constraint::new_node_with_bounds( db, + env, storage, typevar, Some(*lower_element), @@ -2123,6 +2220,7 @@ impl<'db> Constraint<'db> { for upper_element in upper_intersection.iter_positive(db) { let (element_node, element_source_order) = Constraint::new_node_with_bounds( db, + env, storage, typevar, lower, @@ -2134,10 +2232,11 @@ impl<'db> Constraint<'db> { for upper_element in upper_intersection.iter_negative(db) { let (element_node, element_source_order) = Constraint::new_node_with_bounds( db, + env, storage, typevar, lower, - Some(upper_element.negate(db)), + Some(upper_element.negate(db, env)), ); result = result.and(storage, element_node); source_order = storage.ordered_source_order(source_order, element_source_order); @@ -2170,7 +2269,7 @@ impl<'db> Constraint<'db> { }) => { let constraint = - ConstraintId::new(db, storage, typevar, Type::Never, Type::object()); + ConstraintId::new(db, env, storage, typevar, Type::Never, Type::object()); let (node, source_order) = Node::new_constraint(storage, constraint); let node = node.negate(storage); return (node, source_order); @@ -2195,7 +2294,7 @@ impl<'db> Constraint<'db> { _ => {} } - storage.intern_constraint_typevars(db, typevar, ConstraintBounds::new(lower, upper)); + storage.intern_constraint_typevars(db, env, typevar, ConstraintBounds::new(lower, upper)); // If `lower ≰ upper` for every possible assignment of typevars, then the constraint cannot // be satisfied, since there is no type that is both greater than `lower`, and less than @@ -2204,8 +2303,9 @@ impl<'db> Constraint<'db> { // typevars — e.g., `Sequence[int] ≤ A ≤ Sequence[T]` is satisfiable when `int ≤ T`. let effective_lower = lower.unwrap_or(Type::Never); let effective_upper = upper.unwrap_or(Type::object()); - let when = effective_lower.when_constraint_set_assignable_to_owned(db, effective_upper); - let is_never_satisfied = when.query(|_storage, when| when.is_never_satisfied(db)); + let when = + effective_lower.when_constraint_set_assignable_to_owned(db, env, effective_upper); + let is_never_satisfied = when.query(|_storage, when| when.is_never_satisfied(db, env)); if is_never_satisfied { return (ALWAYS_FALSE, None); } @@ -2226,6 +2326,7 @@ impl<'db> Constraint<'db> { }; let constraint = ConstraintId::new( db, + env, storage, typevar, Type::TypeVar(bound), @@ -2241,6 +2342,7 @@ impl<'db> Constraint<'db> { { let lower_constraint = ConstraintId::new_with_bounds( db, + env, storage, lower, None, @@ -2250,6 +2352,7 @@ impl<'db> Constraint<'db> { Node::new_constraint(storage, lower_constraint); let upper_constraint = ConstraintId::new_with_bounds( db, + env, storage, upper, Some(Type::TypeVar(typevar)), @@ -2267,6 +2370,7 @@ impl<'db> Constraint<'db> { (Type::TypeVar(lower), _) if typevar.can_be_bound_for(db, storage, lower) => { let lower_constraint = ConstraintId::new_with_bounds( db, + env, storage, lower, None, @@ -2277,7 +2381,7 @@ impl<'db> Constraint<'db> { let (upper_node, upper_source_order) = if upper.is_none() { (ALWAYS_TRUE, None) } else { - Constraint::new_node_with_bounds(db, storage, typevar, None, upper) + Constraint::new_node_with_bounds(db, env, storage, typevar, None, upper) }; let node = lower_node.and(storage, upper_node); let source_order = @@ -2290,10 +2394,11 @@ impl<'db> Constraint<'db> { let (lower_node, lower_source_order) = if lower.is_none() { (ALWAYS_TRUE, None) } else { - Constraint::new_node_with_bounds(db, storage, typevar, lower, None) + Constraint::new_node_with_bounds(db, env, storage, typevar, lower, None) }; let upper_constraint = ConstraintId::new_with_bounds( db, + env, storage, upper, Some(Type::TypeVar(typevar)), @@ -2308,7 +2413,8 @@ impl<'db> Constraint<'db> { } _ => { - let constraint = ConstraintId::new_with_bounds(db, storage, typevar, lower, upper); + let constraint = + ConstraintId::new_with_bounds(db, env, storage, typevar, lower, upper); Node::new_constraint(storage, constraint) } } @@ -2363,6 +2469,7 @@ impl ConstraintId { fn implies<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, other: Self, ) -> bool { @@ -2377,17 +2484,22 @@ impl ConstraintId { other_constraint .bounds .materialized_lower() - .is_constraint_set_assignable_to(db, self_constraint.bounds.materialized_lower()) + .is_constraint_set_assignable_to(db, env, self_constraint.bounds.materialized_lower()) && self_constraint .bounds .materialized_upper() - .is_constraint_set_assignable_to(db, other_constraint.bounds.materialized_upper()) + .is_constraint_set_assignable_to( + db, + env, + other_constraint.bounds.materialized_upper(), + ) } /// Returns the intersection of two range constraints, or `None` if the intersection is empty. fn intersect<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, other: Self, ) -> IntersectionResult<'db> { @@ -2396,7 +2508,7 @@ impl ConstraintId { // (s₁ ≤ α ≤ t₁) ∧ (s₂ ≤ α ≤ t₂) = (s₁ ∪ s₂) ≤ α ≤ (t₁ ∩ t₂)) let lower = match (self_constraint.bounds.lower, other_constraint.bounds.lower) { - (Some(left), Some(right)) => Some(UnionType::from_two_elements(db, left, right)), + (Some(left), Some(right)) => Some(UnionType::from_two_elements(db, env, left, right)), (Some(lower), None) | (None, Some(lower)) => Some(lower), (None, None) => None, }; @@ -2415,8 +2527,9 @@ impl ConstraintId { // rather than a universal check ("is `lower ≤ upper` for *all* assignments?"), because the // bounds may mention typevars — e.g., `Sequence[int] ≤ A ≤ Sequence[T]` is satisfiable // when `int ≤ T`, even though it's not universally true for all `T`. - let (when, source_order) = merged_upper.when_satisfied_by(db, storage, effective_lower); - if when.is_never_satisfied(db, storage, source_order) { + let (when, source_order) = + merged_upper.when_satisfied_by(db, env, storage, effective_lower); + if when.is_never_satisfied(db, env, storage, source_order) { return IntersectionResult::Disjoint; } @@ -2428,7 +2541,7 @@ impl ConstraintId { return IntersectionResult::CannotSimplify; } - let upper = (!merged_upper.is_empty()).then(|| merged_upper.materialize_exact(db)); + let upper = (!merged_upper.is_empty()).then(|| merged_upper.materialize_exact(db, env)); if upper.is_some_and(|upper| upper.is_nontrivial_intersection(db)) { return IntersectionResult::CannotSimplify; @@ -2440,8 +2553,13 @@ impl ConstraintId { }) } - fn display<'db>(self, db: &'db dyn Db, storage: &ConstraintSetStorage<'db>) -> impl Display { - self.when_true().display(db, storage) + fn display<'db, 'a>( + self, + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + storage: &'a ConstraintSetStorage<'db>, + ) -> impl Display + 'a { + self.when_true().display(db, env, storage) } } @@ -2688,6 +2806,7 @@ impl NodeId { fn is_always_satisfied<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, source_order: Option, ) -> bool { @@ -2696,7 +2815,7 @@ impl NodeId { Node::AlwaysFalse => false, Node::Interior(interior) => { let mut path = interior.path_assignments(storage, source_order); - path.visit_negated(db, storage, self, &mut IsNeverSatisfiedVisitor) + path.visit_negated(db, env, storage, self, &mut IsNeverSatisfiedVisitor) .is_continue() } } @@ -2706,6 +2825,7 @@ impl NodeId { fn is_never_satisfied<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, source_order: Option, ) -> bool { @@ -2761,7 +2881,7 @@ impl NodeId { false } else { let mut path = interior.path_assignments(storage, source_order); - path.visit(db, storage, self, &mut IsNeverSatisfiedVisitor) + path.visit(db, env, storage, self, &mut IsNeverSatisfiedVisitor) .is_continue() }; storage.never_satisfied_cache.insert(self, result); @@ -2990,6 +3110,7 @@ impl NodeId { fn implies_subtype_of<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, lhs: Type<'db>, rhs: Type<'db>, @@ -3005,16 +3126,18 @@ impl NodeId { let (constraint, constraint_source_order) = match (lhs, rhs) { (Type::TypeVar(bound_typevar), _) => Constraint::new_node_with_bounds( db, + env, storage, bound_typevar, None, - Some(rhs.bottom_materialization(db)), + Some(rhs.bottom_materialization(db, env)), ), (_, Type::TypeVar(bound_typevar)) => Constraint::new_node_with_bounds( db, + env, storage, bound_typevar, - Some(lhs.top_materialization(db)), + Some(lhs.top_materialization(db, env)), None, ), _ => panic!("at least one type should be a typevar"), @@ -3027,6 +3150,7 @@ impl NodeId { fn satisfied_by_all_typevars<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, inferable: TypeVarSet<'db>, source_order: Option, @@ -3057,7 +3181,7 @@ impl NodeId { .and(storage, specializations); let source_order = storage.ordered_source_order(source_order, specializations_source_order); - !when_satisfied.is_never_satisfied(db, storage, source_order) + !when_satisfied.is_never_satisfied(db, env, storage, source_order) }; // Returns if all specializations satisfy this constraint set. @@ -3071,7 +3195,7 @@ impl NodeId { .iff(storage, specializations); let source_order = storage.ordered_source_order(source_order, specializations_source_order); - when_satisfied.is_always_satisfied(db, storage, source_order) + when_satisfied.is_always_satisfied(db, env, storage, source_order) }; #[expect( @@ -3082,7 +3206,7 @@ impl NodeId { if typevar.is_inferable(db, inferable) { // If the typevar is in inferable position, we need to verify that some valid // specialization satisfies the constraint set. - let valid_specializations = typevar.valid_specializations(db, storage); + let valid_specializations = typevar.valid_specializations(db, env, storage); if !some_specialization_satisfies(storage, valid_specializations) { return false; } @@ -3099,7 +3223,7 @@ impl NodeId { // constraint to refer to the synthetic typevar instead of the original gradual // constraint. let (static_specializations, gradual_constraints) = - typevar.required_specializations(db, storage); + typevar.required_specializations(db, env, storage); if !all_specializations_satisfy(storage, static_specializations) { return false; } @@ -3120,6 +3244,7 @@ impl NodeId { fn exists<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, bound_typevars: TypeVarSet<'db>, source_order: Option, @@ -3137,7 +3262,7 @@ impl NodeId { return *result; } - let result = interior.exists_inner(db, storage, bound_typevars, source_order); + let result = interior.exists_inner(db, env, storage, bound_typevars, source_order); storage.exists_cache.insert(key, result); result @@ -3146,6 +3271,7 @@ impl NodeId { fn remove_noninferable<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, inferable: TypeVarSet<'db>, source_order: Option, @@ -3154,7 +3280,7 @@ impl NodeId { Node::AlwaysTrue => (ALWAYS_TRUE, None), Node::AlwaysFalse => (ALWAYS_FALSE, None), Node::Interior(interior) => { - interior.remove_noninferable(db, storage, inferable, source_order) + interior.remove_noninferable(db, env, storage, inferable, source_order) } } } @@ -3389,11 +3515,12 @@ impl NodeId { fn simplify_for_display<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, ) -> Self { match self.node() { Node::AlwaysTrue | Node::AlwaysFalse => self, - Node::Interior(interior) => interior.simplify(db, storage), + Node::Interior(interior) => interior.simplify(db, env, storage), } } @@ -3435,11 +3562,12 @@ impl NodeId { searcher.clauses } - fn display<'db>( + fn display<'db, 'a>( self, db: &'db dyn Db, - storage: &mut ConstraintSetStorage<'db>, - ) -> impl Display { + env: &'a ProgramEnvironment<'db>, + storage: &'a mut ConstraintSetStorage<'db>, + ) -> impl Display + 'a { // To render a BDD in DNF form, you perform a depth-first search of the BDD tree, looking // for any path that leads to the AlwaysTrue terminal. Each such path represents one of the // intersection clauses in the DNF form. The path traverses zero or more interior nodes, @@ -3448,19 +3576,21 @@ impl NodeId { struct DisplayNode<'db, 'c> { node: NodeId, db: &'db dyn Db, + env: &'c ProgramEnvironment<'db>, storage: RefCell<&'c mut ConstraintSetStorage<'db>>, } impl Display for DisplayNode<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; match self.node.node() { Node::AlwaysTrue => f.write_str("always"), Node::AlwaysFalse => f.write_str("never"), Node::Interior(_) => { let mut storage = self.storage.borrow_mut(); let mut clauses = self.node.satisfied_clauses(&storage); - clauses.simplify(self.db, &mut storage); - Display::fmt(&clauses.display(self.db, &storage), f) + clauses.simplify(db, self.env, &mut storage); + Display::fmt(&clauses.display(db, self.env, &storage), f) } } } @@ -3469,6 +3599,7 @@ impl NodeId { DisplayNode { node: self, db, + env, storage: RefCell::new(storage), } } @@ -3494,11 +3625,13 @@ impl NodeId { fn display_graph<'db, 'a>( self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, storage: &'a ConstraintSetStorage<'db>, prefix: &'a dyn Display, ) -> impl Display + 'a { struct DisplayNode<'a, 'db> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, storage: &'a ConstraintSetStorage<'db>, node: NodeId, prefix: &'a dyn Display, @@ -3507,6 +3640,7 @@ impl NodeId { fn format_node<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &ConstraintSetStorage<'db>, node: NodeId, prefix: &dyn Display, @@ -3522,12 +3656,17 @@ impl NodeId { return write!(f, "<{index}> SHARED"); } let interior = storage.interior_node_data(node); - write!(f, "<{index}> {}", interior.constraint.display(db, storage))?; + write!( + f, + "<{index}> {}", + interior.constraint.display(db, env, storage) + )?; // Calling display_graph recursively here causes rustc to claim that the // expect(unused) up above is unfulfilled! write!(f, "\n{prefix}┡━₁ ")?; format_node( db, + env, storage, interior.if_true, &format_args!("{prefix}│ "), @@ -3537,6 +3676,7 @@ impl NodeId { write!(f, "\n{prefix}├─? ")?; format_node( db, + env, storage, interior.if_uncertain, &format_args!("{prefix}│ "), @@ -3546,6 +3686,7 @@ impl NodeId { write!(f, "\n{prefix}└─₀ ")?; format_node( db, + env, storage, interior.if_false, &format_args!("{prefix} "), @@ -3559,12 +3700,22 @@ impl NodeId { impl Display for DisplayNode<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - format_node(self.db, self.storage, self.node, self.prefix, &self.seen, f) + let db = self.db; + format_node( + db, + self.env, + self.storage, + self.node, + self.prefix, + &self.seen, + f, + ) } } DisplayNode { db, + env, storage, node: self, prefix, @@ -3639,39 +3790,44 @@ struct ConstraintBoundsBuilder<'db> { } impl<'db> ConstraintBoundsBuilder<'db> { - fn classify_evidence(&mut self, db: &'db dyn Db, ty: Type<'db>) { - if ty.has_unspecialized_type_var(db) { + fn classify_evidence(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) { + if ty.has_unspecialized_type_var(db, env) { return; } - if ty.bottom_materialization(db) == ty.top_materialization(db) { + if ty.bottom_materialization(db, env) == ty.top_materialization(db, env) { self.has_static_evidence = true; } else { self.has_gradual_evidence = true; } } - fn add_lower(&mut self, db: &'db dyn Db, ty: Type<'db>) { + fn add_lower(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) { // Lower bounds are unioned. Our type representation is in DNF, so unioning a new // element is typically cheap (in that it does not involve a combinatorial // explosion from distributing the clause through an existing disjunction). So we // don't need to be as clever here as in `add_upper`. - self.classify_evidence(db, ty); + self.classify_evidence(db, env, ty); self.lower.insert(ty); } - fn add_upper(&mut self, db: &'db dyn Db, ty: Type<'db>) { - self.classify_evidence(db, ty); + fn add_upper(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) { + self.classify_evidence(db, env, ty); self.upper.add_clause(ty); } - fn finish(self, db: &'db dyn Db, bound_typevar: BoundTypeVarInstance<'db>) -> PathBound<'db> { + fn finish( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + bound_typevar: BoundTypeVarInstance<'db>, + ) -> PathBound<'db> { let Self { lower, mut upper, has_gradual_evidence, has_static_evidence, } = self; - let lower = (!lower.is_empty()).then(|| UnionType::from_elements(db, lower)); + let lower = (!lower.is_empty()).then(|| UnionType::from_elements(db, env, lower)); upper.shrink_to_fit(); PathBound { bound_typevar, @@ -3730,28 +3886,39 @@ impl<'db> Type<'db> { pub(crate) fn assignable_solutions_with_inferable( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, inferable: TypeVarSet<'db>, ) -> &'db PathBounds<'db> { #[salsa::tracked( returns(ref), - cycle_initial=|_, _, _, _, _| PathBounds::Unsatisfiable, + cycle_initial=|_, _, _, _, _, _| PathBounds::Unsatisfiable, heap_size=ruff_memory_usage::heap_size, )] fn assignable_solutions_impl<'db>( db: &'db dyn Db, + program: Program, source: Type<'db>, target: Type<'db>, inferable: TypeVarSet<'db>, ) -> PathBounds<'db> { - let when = source.when_constraint_set_assignable_to_owned(db, target); + let env = &ProgramEnvironment::from_program(program); + let when = source.when_constraint_set_assignable_to_owned(db, env, target); when.query(|builder, when| { let mut storage = builder.storage.borrow_mut(); - PathBounds::compute(db, &mut storage, when.node, inferable, when.source_order) + PathBounds::compute( + db, + env, + &mut storage, + when.node, + inferable, + when.source_order, + ) }) } - assignable_solutions_impl(db, self, target, inferable) + let program = env.program(db); + assignable_solutions_impl(db, program, self, target, inferable) } } @@ -3761,10 +3928,12 @@ impl<'db> Type<'db> { heap_size = get_size2::GetSize::get_heap_size )] fn is_possibly_constraint_set_assignable<'db>(db: &'db dyn Db, types: TypePair<'db>) -> bool { + let program = types.program(db); + let env = &ProgramEnvironment::from_program(program); types .first(db) - .when_constraint_set_assignable_to_owned(db, types.second(db)) - .query(|_storage, when| !when.is_never_satisfied(db)) + .when_constraint_set_assignable_to_owned(db, env, types.second(db)) + .query(|_storage, when| !when.is_never_satisfied(db, env)) } /// Per-path bounds for all typevars. Each element is the set of typevar bounds for one BDD path. @@ -3782,6 +3951,7 @@ impl<'db> PathBounds<'db> { /// typevar that appears in the path's constraints. fn compute( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, node: NodeId, inferable: TypeVarSet<'db>, @@ -3848,14 +4018,19 @@ impl<'db> PathBounds<'db> { } let mut source_orders = storage.calculate_source_orders(source_order); - if let Some(path_bounds) = - Self::compute_simple_bound_conjunction(db, storage, &source_orders, node, inferable) - { + if let Some(path_bounds) = Self::compute_simple_bound_conjunction( + db, + env, + storage, + &source_orders, + node, + inferable, + ) { return path_bounds; } let (node, derived_source_order) = - node.remove_noninferable(db, storage, inferable, source_order); + node.remove_noninferable(db, env, storage, inferable, source_order); source_orders.extend(storage.calculate_source_orders(derived_source_order)); let interior = match node.node() { Node::AlwaysTrue => return PathBounds::Unconstrained, @@ -3877,7 +4052,7 @@ impl<'db> PathBounds<'db> { // discard gradual evidence before solution extraction. let path_source_order = storage.ordered_source_order(source_order, derived_source_order); let mut path = interior.path_assignments(storage, path_source_order); - let _ = path.visit(db, storage, node, &mut collect_visitor); + let _ = path.visit(db, env, storage, node, &mut collect_visitor); collect_visitor.sorted_paths.sort_by(|path1, path2| { let source_orders1 = path1.iter().map(|(_, source_order)| *source_order); let source_orders2 = path2.iter().map(|(_, source_order)| *source_order); @@ -3895,28 +4070,28 @@ impl<'db> PathBounds<'db> { let typevar = constraint.typevar; if let Some(lower) = constraint.bounds.lower { let bounds = mappings.entry(typevar).or_default(); - bounds.add_lower(db, lower); + bounds.add_lower(db, env, lower); if let Type::TypeVar(lower_bound_typevar) = lower { let bounds = mappings.entry(lower_bound_typevar).or_default(); - bounds.add_upper(db, Type::TypeVar(typevar)); + bounds.add_upper(db, env, Type::TypeVar(typevar)); } } if let Some(upper) = constraint.bounds.upper { let bounds = mappings.entry(typevar).or_default(); - bounds.add_upper(db, upper); + bounds.add_upper(db, env, upper); if let Type::TypeVar(upper_bound_typevar) = upper { let bounds = mappings.entry(upper_bound_typevar).or_default(); - bounds.add_lower(db, Type::TypeVar(typevar)); + bounds.add_lower(db, env, Type::TypeVar(typevar)); } } } let path_bounds = mappings .drain(..) - .map(|(bound_typevar, bounds)| bounds.finish(db, bound_typevar)) + .map(|(bound_typevar, bounds)| bounds.finish(db, env, bound_typevar)) .collect(); result.push(path_bounds); } @@ -3932,6 +4107,7 @@ impl<'db> PathBounds<'db> { /// accumulated bound against the typevar's declared bound or constraints. fn compute_simple_bound_conjunction( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, source_orders: &FxIndexSet, node: NodeId, @@ -3960,9 +4136,9 @@ impl<'db> PathBounds<'db> { return None; } - if iter::chain(constraint.bounds.lower, constraint.bounds.upper) - .any(|bound| bound.has_typevar(db) || bound.has_unspecialized_type_var(db)) - { + if iter::chain(constraint.bounds.lower, constraint.bounds.upper).any(|bound| { + bound.has_typevar(db, env) || bound.has_unspecialized_type_var(db, env) + }) { return None; } @@ -3984,16 +4160,16 @@ impl<'db> PathBounds<'db> { for (typevar, constraint, _) in constraints { let bounds = mappings.entry(typevar).or_default(); if let Some(lower) = constraint.lower { - bounds.add_lower(db, lower); + bounds.add_lower(db, env, lower); } if let Some(upper) = constraint.upper { - bounds.add_upper(db, upper); + bounds.add_upper(db, env, upper); } } let path = mappings .drain(..) - .map(|(bound_typevar, bounds)| bounds.finish(db, bound_typevar)) + .map(|(bound_typevar, bounds)| bounds.finish(db, env, bound_typevar)) .collect(); Some(PathBounds::Constrained(Box::new([path]))) } @@ -4001,9 +4177,12 @@ impl<'db> PathBounds<'db> { pub(crate) fn solve( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &ConstraintSetBuilder<'db>, ) -> Solutions<'db> { - self.solve_with(|_variance, path_bound| PathBounds::default_solve(db, builder, path_bound)) + self.solve_with(|_variance, path_bound| { + PathBounds::default_solve(db, env, builder, path_bound) + }) } /// Solves each path by applying a per-typevar solver function, collecting valid solutions. @@ -4057,6 +4236,7 @@ impl<'db> PathBounds<'db> { /// - `Err(())` if the path is invalid (bounds violate the typevar's declared constraints) pub(crate) fn default_solve( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, builder: &ConstraintSetBuilder<'db>, path_bound: &PathBound<'db>, ) -> Result>, ()> { @@ -4068,19 +4248,24 @@ impl<'db> PathBounds<'db> { let bound_typevar = path_bound.bound_typevar; let lower = path_bound.lower_or_never(); - match bound_typevar.typevar(db).require_bound_or_constraints(db) { + match bound_typevar + .typevar(db) + .require_bound_or_constraints(db, env) + { TypeVarBoundOrConstraints::UpperBound(bound) => { - let declared_upper = bound.top_materialization(db); + let declared_upper = bound.top_materialization(db, env); // Prefer the lower bound (often the concrete actual type seen) over the // upper bound (which may include TypeVar bounds/constraints). The upper bound // should only be used as a fallback when no concrete type was inferred. if let Some(lower) = path_bound.lower { - if !path_bound.upper.is_satisfied_by(db, lower) { + if !path_bound.upper.is_satisfied_by(db, env, lower) { let mut storage = builder.storage.borrow_mut(); let (when_upper, source_order) = - path_bound.upper.when_satisfied_by(db, &mut storage, lower); - if when_upper.is_never_satisfied(db, &mut storage, source_order) { + path_bound + .upper + .when_satisfied_by(db, env, &mut storage, lower); + if when_upper.is_never_satisfied(db, env, &mut storage, source_order) { // This path does not satisfy the accumulated upper bound, and is // therefore not a valid specialization. return Err(()); @@ -4089,7 +4274,7 @@ impl<'db> PathBounds<'db> { if !is_possibly_constraint_set_assignable( db, - TypePair::new(db, lower, declared_upper), + TypePair::new(db, env.program(db), lower, declared_upper), ) { // This path does not satisfy the typevar's declared upper bound, and is // therefore not a valid specialization. @@ -4102,6 +4287,7 @@ impl<'db> PathBounds<'db> { if path_bound.has_upper() { return Ok(IntersectionType::bounded_from_elements( db, + env, path_bound .upper .clauses @@ -4152,8 +4338,10 @@ impl<'db> PathBounds<'db> { // assignable in both directions, prefer a fully static constraint over a // gradual one. Otherwise, keep the current best to preserve the TypeVar's // declared constraint order. - let candidate_assignable_to_best = candidate.is_assignable_to(db, current_best); - let best_assignable_to_candidate = current_best.is_assignable_to(db, candidate); + let candidate_assignable_to_best = + candidate.is_assignable_to(db, env, current_best); + let best_assignable_to_candidate = + current_best.is_assignable_to(db, env, candidate); if candidate_assignable_to_best != best_assignable_to_candidate { if path_bound.lower.is_some() { @@ -4162,10 +4350,10 @@ impl<'db> PathBounds<'db> { best_assignable_to_candidate } } else if candidate_assignable_to_best { - let candidate_is_static = candidate.bottom_materialization(db) - == candidate.top_materialization(db); - let best_is_static = current_best.bottom_materialization(db) - == current_best.top_materialization(db); + let candidate_is_static = candidate.bottom_materialization(db, env) + == candidate.top_materialization(db, env); + let best_is_static = current_best.bottom_materialization(db, env) + == current_best.top_materialization(db, env); candidate_is_static && !best_is_static } else { false @@ -4173,24 +4361,24 @@ impl<'db> PathBounds<'db> { }; for constraint in constraints.elements(db).iter().copied() { - let constraint_lower = constraint.bottom_materialization(db); - let constraint_upper = constraint.top_materialization(db); + let constraint_lower = constraint.bottom_materialization(db, env); + let constraint_upper = constraint.top_materialization(db, env); // A gradual constraint can choose any materialization that satisfies this // path. Its top materialization is the most permissive target for lower-bound // evidence, while its bottom materialization is the most permissive source // for upper-bound evidence. let when_lower = - lower.when_constraint_set_assignable_to_owned(db, constraint_upper); + lower.when_constraint_set_assignable_to_owned(db, env, constraint_upper); let mut storage = builder.storage.borrow_mut(); let (when_upper, upper_source_order) = path_bound .upper - .when_satisfied_by(db, &mut storage, constraint_lower); - let (when_lower, lower_source_order) = storage.load(db, &when_lower); + .when_satisfied_by(db, env, &mut storage, constraint_lower); + let (when_lower, lower_source_order) = storage.load(db, env, &when_lower); let when = when_lower.and(&mut storage, when_upper); let source_order = storage.ordered_source_order(lower_source_order, upper_source_order); - if when.is_never_satisfied(db, &mut storage, source_order) { + if when.is_never_satisfied(db, env, &mut storage, source_order) { continue; } @@ -4211,7 +4399,7 @@ impl<'db> PathBounds<'db> { }; if let (Some(ty @ Type::TypeVar(_)), _) | (_, Some(ty @ Type::TypeVar(_))) = - (path_bound.lower, path_bound.upper.as_single_bound(db)) + (path_bound.lower, path_bound.upper.as_single_bound(db, env)) { // This path relates two TypeVars, such as passing `S` to a parameter typed as // `T: (int, str)`. The compatibility check above has verified that at least @@ -4235,6 +4423,7 @@ impl<'db> PathBounds<'db> { } else if path_bound.has_upper() { Ok(IntersectionType::bounded_from_elements( db, + env, path_bound.upper.clauses.iter().copied(), )) } else { @@ -4413,12 +4602,14 @@ impl InteriorNode { fn exists_inner<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, bound_typevars: TypeVarSet<'db>, source_order: Option, ) -> (NodeId, Option) { self.abstract_inner( db, + env, storage, source_order, // Remove any node that constrains one of `bound_typevars`, or that has a lower/upper @@ -4438,6 +4629,7 @@ impl InteriorNode { fn remove_noninferable<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, inferable: TypeVarSet<'db>, source_order: Option, @@ -4448,6 +4640,7 @@ impl InteriorNode { }; self.abstract_inner( db, + env, storage, source_order, // We only want to keep constraints on inferable typevars. If the constraint's typevar @@ -4477,6 +4670,7 @@ impl InteriorNode { fn abstract_inner<'db, F>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, source_order: Option, should_remove: F, @@ -4641,7 +4835,7 @@ impl InteriorNode { let mut path = self.path_assignments(storage, source_order); let mut visitor = AbstractVisitor { should_remove }; - let ControlFlow::Continue(result) = path.visit(db, storage, self.node(), &mut visitor); + let ControlFlow::Continue(result) = path.visit(db, env, storage, self.node(), &mut visitor); result } @@ -4746,7 +4940,12 @@ impl InteriorNode { /// This is calculated by looking at the relationships that exist between the constraints that /// are mentioned in the BDD. For instance, if one constraint implies another (`x → y`), then /// `x ∧ ¬y` is not a valid input, and we can rewrite any occurrences of `x ∨ y` into `y`. - fn simplify<'db>(self, db: &'db dyn Db, storage: &mut ConstraintSetStorage<'db>) -> NodeId { + fn simplify<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + ) -> NodeId { let key = self.node(); if let Some(result) = storage.simplify_cache.get(&key) { return *result; @@ -4844,6 +5043,7 @@ impl InteriorNode { let new_constraint = ConstraintId::new_with_bounds( db, + env, storage, constrained_typevar, new_lower, @@ -4890,9 +5090,9 @@ impl InteriorNode { // Containment: The range of one constraint might completely contain the range of the // other. If so, there are several potential simplifications. - let larger_smaller = if left_constraint.implies(db, storage, right_constraint) { + let larger_smaller = if left_constraint.implies(db, env, storage, right_constraint) { Some((right_constraint, left_constraint)) - } else if right_constraint.implies(db, storage, left_constraint) { + } else if right_constraint.implies(db, env, storage, left_constraint) { Some((left_constraint, right_constraint)) } else { None @@ -4945,10 +5145,10 @@ impl InteriorNode { // There are some simplifications we can make when the intersection of the two // constraints is empty, and others that we can make when the intersection is // non-empty. - match left_constraint.intersect(db, storage, right_constraint) { + match left_constraint.intersect(db, env, storage, right_constraint) { IntersectionResult::Simplified(intersection_constraint_data) => { let intersection_constraint = - storage.intern_constraint(db, intersection_constraint_data); + storage.intern_constraint(db, env, intersection_constraint_data); // If the intersection is non-empty, we need to create a new constraint to // represent that intersection. We also need to add the new constraint to our @@ -5164,6 +5364,7 @@ impl ConstraintAssignment { fn implies<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, other: Self, ) -> bool { @@ -5176,7 +5377,7 @@ impl ConstraintAssignment { ( ConstraintAssignment::Positive(self_constraint), ConstraintAssignment::Positive(other_constraint), - ) => self_constraint.implies(db, storage, other_constraint), + ) => self_constraint.implies(db, env, storage, other_constraint), // For two negative constraints, one range has to fully contain the other; the ranges // represent "holes", though, so the constraint with the larger range implies the one @@ -5187,7 +5388,7 @@ impl ConstraintAssignment { ( ConstraintAssignment::Negative(self_constraint), ConstraintAssignment::Negative(other_constraint), - ) => other_constraint.implies(db, storage, self_constraint), + ) => other_constraint.implies(db, env, storage, self_constraint), // For a positive and negative constraint, the ranges have to be disjoint, and the // positive range implies the negative range. @@ -5198,7 +5399,7 @@ impl ConstraintAssignment { ConstraintAssignment::Positive(self_constraint), ConstraintAssignment::Negative(other_constraint), ) => self_constraint - .intersect(db, storage, other_constraint) + .intersect(db, env, storage, other_constraint) .is_disjoint(), // It's theoretically possible for a negative constraint to imply a positive constraint @@ -5223,10 +5424,16 @@ impl ConstraintAssignment { } } - fn display<'db>(self, db: &'db dyn Db, storage: &ConstraintSetStorage<'db>) -> impl Display { + fn display<'db, 'a>( + self, + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + storage: &'a ConstraintSetStorage<'db>, + ) -> impl Display + 'a { struct DisplayConstraintAssignment<'db, 'c> { assignment: ConstraintAssignment, db: &'db dyn Db, + env: &'c ProgramEnvironment<'db>, storage: &'c ConstraintSetStorage<'db>, } @@ -5250,17 +5457,19 @@ impl ConstraintAssignment { impl Display for DisplayConstraintAssignment<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; + let constraint_data = self.storage.constraint_data(self.assignment.constraint()); let lower = constraint_data.bounds.materialized_lower(); let upper = constraint_data.bounds.materialized_upper(); let typevar = constraint_data.typevar; - if lower.is_equivalent_to(self.db, upper) { + if lower.is_equivalent_to(db, self.env, upper) { // If this typevar is equivalent to another, output the constraint in a // consistent alphabetical order, regardless of the salsa ordering that we are // using the in BDD. if let Type::TypeVar(bound) = lower { - let bound = bound.identity(self.db).display(self.db).to_string(); - let typevar = typevar.identity(self.db).display(self.db).to_string(); + let bound = bound.identity(db).display(db).to_string(); + let typevar = typevar.identity(db).display(db).to_string(); let (smaller, larger) = if bound < typevar { (bound, typevar) } else { @@ -5272,9 +5481,9 @@ impl ConstraintAssignment { return write!( f, "({} {} {})", - typevar.identity(self.db).display(self.db), + typevar.identity(db).display(db), self.equality_sign(), - lower.display(self.db) + lower.display(db, self.env) ); } @@ -5282,7 +5491,7 @@ impl ConstraintAssignment { return write!( f, "({} {} *)", - typevar.identity(self.db).display(self.db), + typevar.identity(db).display(db), self.equality_sign() ); } @@ -5290,11 +5499,11 @@ impl ConstraintAssignment { f.write_str(self.range_prefix())?; f.write_str("(")?; if !lower.is_never() { - write!(f, "{} ≤ ", lower.display(self.db))?; + write!(f, "{} ≤ ", lower.display(db, self.env))?; } - typevar.identity(self.db).display(self.db).fmt(f)?; + typevar.identity(db).display(db).fmt(f)?; if !upper.is_object() { - write!(f, " ≤ {}", upper.display(self.db))?; + write!(f, " ≤ {}", upper.display(db, self.env))?; } f.write_str(")") } @@ -5303,6 +5512,7 @@ impl ConstraintAssignment { DisplayConstraintAssignment { assignment: self, db, + env, storage, } } @@ -5377,6 +5587,7 @@ impl SequentMap { /// constraint. fn for_constraint<'db, 'c>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &'c mut ConstraintSetStorage<'db>, constraint: ConstraintId, ) -> &'c Self { @@ -5384,11 +5595,11 @@ impl SequentMap { if !storage.single_sequent_cache.contains_key(&key) { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - constraint = %constraint.display(db, storage), + constraint = %constraint.display(db, env, storage), "add sequents for constraint", ); let mut map = SequentMap::default(); - map.add_sequents_for_single(db, storage, constraint); + map.add_sequents_for_single(db, env, storage, constraint); storage.single_sequent_cache.insert(key, map); } &storage.single_sequent_cache[&key] @@ -5402,6 +5613,7 @@ impl SequentMap { /// that retain that ordering.) fn for_constraint_pair<'db, 'c>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &'c mut ConstraintSetStorage<'db>, left: ConstraintId, right: ConstraintId, @@ -5410,12 +5622,12 @@ impl SequentMap { if !storage.pair_sequent_cache.contains_key(&key) { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left.display(db, storage), - right = %right.display(db, storage), + left = %left.display(db, env, storage), + right = %right.display(db, env, storage), "add sequents for constraint pair", ); let mut map = SequentMap::default(); - map.add_sequents_for_pair(db, storage, left, right); + map.add_sequents_for_pair(db, env, storage, left, right); storage.pair_sequent_cache.insert(key, map); } &storage.pair_sequent_cache[&key] @@ -5426,6 +5638,7 @@ impl SequentMap { /// to skip calling `for_constraint_pair` for this pair of constraints. fn pair_cannot_produce_sequents<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, left: ConstraintId, right: ConstraintId, @@ -5460,7 +5673,7 @@ impl SequentMap { // that it can use. let builder = ConstraintSetBuilder::new(); left_lower - .when_trivially_disjoint_from(db, right_lower, &builder, TypeVarSet::None) + .when_trivially_disjoint_from(db, env, right_lower, &builder, TypeVarSet::None) .is_trivially_always_satisfied() } @@ -5476,6 +5689,7 @@ impl SequentMap { fn add_pair_implication<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, ante1: ConstraintId, ante2: ConstraintId, @@ -5485,18 +5699,23 @@ impl SequentMap { let post_data = storage.constraint_data(post); let (when, source_order) = storage.load( db, + env, &post_data .bounds .materialized_lower() - .when_constraint_set_assignable_to_owned(db, post_data.bounds.materialized_upper()), + .when_constraint_set_assignable_to_owned( + db, + env, + post_data.bounds.materialized_upper(), + ), ); - if when.is_never_satisfied(db, storage, source_order) { + if when.is_never_satisfied(db, env, storage, source_order) { self.add_pair_impossibility(ante1, ante2); return; } // If either antecedent implies the consequent on its own, this new sequent is redundant. - if ante1.implies(db, storage, post) || ante2.implies(db, storage, post) { + if ante1.implies(db, env, storage, post) || ante2.implies(db, env, storage, post) { return; } @@ -5516,6 +5735,7 @@ impl SequentMap { fn add_sequents_for_single<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, constraint: ConstraintId, ) { @@ -5575,14 +5795,15 @@ impl SequentMap { let (when, source_order) = storage.load( db, - &lower.when_constraint_set_assignable_to_owned(db, upper), + env, + &lower.when_constraint_set_assignable_to_owned(db, env, upper), ); // If L is _never_ assignable to U, this constraint would violate transitivity, and should // never have been added. #[expect(clippy::debug_assert_with_mut_call)] { - debug_assert!(!when.is_never_satisfied(db, storage, source_order)); + debug_assert!(!when.is_never_satisfied(db, env, storage, source_order)); } // Fast path: If L is trivially always assignable to U, there are no derived constraints @@ -5651,6 +5872,7 @@ impl SequentMap { fn add_sequents_for_pair<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, @@ -5684,11 +5906,12 @@ impl SequentMap { if !left_typevar.is_same_typevar_as(db, right_typevar) { self.add_mutual_sequents_for_different_typevars( db, + env, storage, left_constraint, right_constraint, ); - self.add_nested_typevar_sequents(db, storage, left_constraint, right_constraint); + self.add_nested_typevar_sequents(db, env, storage, left_constraint, right_constraint); } else if left_constraint_data .bounds .lower @@ -5708,18 +5931,20 @@ impl SequentMap { { self.add_mutual_sequents_for_same_typevars( db, + env, storage, left_constraint, right_constraint, ); } else { - self.add_concrete_sequents(db, storage, left_constraint, right_constraint); + self.add_concrete_sequents(db, env, storage, left_constraint, right_constraint); } } fn add_mutual_sequents_for_different_typevars<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, @@ -5792,8 +6017,9 @@ impl SequentMap { && !constrained_upper.is_object() && storage.cached_is_constraint_set_subtype_of( db, - constrained_upper.top_materialization(db), - bound_lower.bottom_materialization(db), + env, + constrained_upper.top_materialization(db, env), + bound_lower.bottom_materialization(db, env), ) => { (constrained_lower, Some(Type::TypeVar(bound_typevar))) @@ -5805,8 +6031,9 @@ impl SequentMap { && !constrained_lower.is_object() && storage.cached_is_constraint_set_subtype_of( db, - bound_upper.top_materialization(db), - constrained_lower.bottom_materialization(db), + env, + bound_upper.top_materialization(db, env), + constrained_lower.bottom_materialization(db, env), ) => { (Some(Type::TypeVar(bound_typevar)), constrained_upper) @@ -5843,6 +6070,7 @@ impl SequentMap { { post_constraints.push(ConstraintId::new_with_bounds( db, + env, storage, lower_bound_typevar, None, @@ -5856,6 +6084,7 @@ impl SequentMap { { post_constraints.push(ConstraintId::new_with_bounds( db, + env, storage, upper_bound_typevar, Some(Type::TypeVar(constrained_typevar)), @@ -5869,6 +6098,7 @@ impl SequentMap { { post_constraints.push(ConstraintId::new_with_bounds( db, + env, storage, constrained_typevar, constrained_lower, @@ -5879,6 +6109,7 @@ impl SequentMap { for post_constraint in post_constraints { self.add_pair_implication( db, + env, storage, left_constraint, right_constraint, @@ -5899,6 +6130,7 @@ impl SequentMap { fn add_nested_typevar_sequents<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, @@ -5907,10 +6139,10 @@ impl SequentMap { let has_typevar_bound = |bounds: ConstraintBounds<'db>| { bounds .lower - .is_some_and(|bound| any_over_type(db, bound, true, Type::is_type_var)) + .is_some_and(|bound| any_over_type(db, env, bound, true, Type::is_type_var)) || bounds .upper - .is_some_and(|bound| any_over_type(db, bound, true, Type::is_type_var)) + .is_some_and(|bound| any_over_type(db, env, bound, true, Type::is_type_var)) }; if !has_typevar_bound(storage.constraint_data(left_constraint).bounds) && !has_typevar_bound(storage.constraint_data(right_constraint).bounds) @@ -5939,8 +6171,8 @@ impl SequentMap { // instead of calling `variance_of` on them. This avoids a large number of tiny // tracked `variance_of` queries in hot paths. let replacement_mentions_bound_or_constrained = |replacement: Type<'db>| { - replacement.variance_of(db, bound_identity) != TypeVarVariance::Bivariant - || replacement.variance_of(db, constrained_identity) + replacement.variance_of(db, env, bound_identity) != TypeVarVariance::Bivariant + || replacement.variance_of(db, env, constrained_identity) != TypeVarVariance::Bivariant }; @@ -5954,7 +6186,7 @@ impl SequentMap { // need an alternative representation for "typevar not present" // (e.g., `Option`). let upper_replacement = match ( - constrained_upper.variance_of(db, bound_identity), + constrained_upper.variance_of(db, env, bound_identity), bound_data.bounds.lower, bound_data.bounds.upper, ) { @@ -5995,11 +6227,16 @@ impl SequentMap { !replacement_mentions_bound_or_constrained(*replacement) }); if let Some(replacement) = upper_replacement { - let new_upper = - constrained_upper.substitute_one_typevar(db, bound_typevar, replacement); + let new_upper = constrained_upper.substitute_one_typevar( + db, + env, + bound_typevar, + replacement, + ); if new_upper != constrained_upper { let post = ConstraintId::new_with_bounds( db, + env, storage, constrained_typevar, constrained_data.bounds.lower, @@ -6007,6 +6244,7 @@ impl SequentMap { ); self.add_pair_implication( db, + env, storage, bound_constraint, constrained_constraint, @@ -6017,7 +6255,7 @@ impl SequentMap { // Check the lower bound of the constrained constraint for nested occurrences. let lower_replacement = match ( - constrained_lower.variance_of(db, bound_identity), + constrained_lower.variance_of(db, env, bound_identity), bound_data.bounds.lower, bound_data.bounds.upper, ) { @@ -6056,11 +6294,16 @@ impl SequentMap { !replacement_mentions_bound_or_constrained(*replacement) }); if let Some(replacement) = lower_replacement { - let new_lower = - constrained_lower.substitute_one_typevar(db, bound_typevar, replacement); + let new_lower = constrained_lower.substitute_one_typevar( + db, + env, + bound_typevar, + replacement, + ); if new_lower != constrained_lower { let post = ConstraintId::new_with_bounds( db, + env, storage, constrained_typevar, Some(new_lower), @@ -6068,6 +6311,7 @@ impl SequentMap { ); self.add_pair_implication( db, + env, storage, bound_constraint, constrained_constraint, @@ -6136,7 +6380,8 @@ impl SequentMap { && !constrained_upper.is_never() && !constrained_upper.is_object() && !constrained_upper.is_dynamic() - && match constrained_upper.variance_of(db, nested_typevar.identity(db)) { + && match constrained_upper.variance_of(db, env, nested_typevar.identity(db)) + { TypeVarVariance::Bivariant => false, TypeVarVariance::Covariant => !is_upper_bound, TypeVarVariance::Contravariant => is_upper_bound, @@ -6148,12 +6393,14 @@ impl SequentMap { if should_weaken_upper { let new_upper = constrained_upper.substitute_one_typevar( db, + env, nested_typevar, replacement, ); if new_upper != constrained_upper { let post = ConstraintId::new_with_bounds( db, + env, storage, constrained_typevar, constrained_data.bounds.lower, @@ -6161,6 +6408,7 @@ impl SequentMap { ); self.add_pair_implication( db, + env, storage, bound_constraint, constrained_constraint, @@ -6174,7 +6422,8 @@ impl SequentMap { && !constrained_lower.is_never() && !constrained_lower.is_object() && !constrained_lower.is_dynamic() - && match constrained_lower.variance_of(db, nested_typevar.identity(db)) { + && match constrained_lower.variance_of(db, env, nested_typevar.identity(db)) + { TypeVarVariance::Bivariant => false, TypeVarVariance::Covariant => is_upper_bound, TypeVarVariance::Contravariant => !is_upper_bound, @@ -6186,12 +6435,14 @@ impl SequentMap { if should_weaken_lower { let new_lower = constrained_lower.substitute_one_typevar( db, + env, nested_typevar, replacement, ); if new_lower != constrained_lower { let post = ConstraintId::new_with_bounds( db, + env, storage, constrained_typevar, Some(new_lower), @@ -6199,6 +6450,7 @@ impl SequentMap { ); self.add_pair_implication( db, + env, storage, bound_constraint, constrained_constraint, @@ -6227,6 +6479,7 @@ impl SequentMap { fn add_mutual_sequents_for_same_typevars<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, @@ -6271,6 +6524,7 @@ impl SequentMap { { post_constraints.push(ConstraintId::new_with_bounds( db, + env, storage, lower_bound_typevar, None, @@ -6284,6 +6538,7 @@ impl SequentMap { { post_constraints.push(ConstraintId::new_with_bounds( db, + env, storage, upper_bound_typevar, Some(Type::TypeVar(bound_typevar)), @@ -6297,6 +6552,7 @@ impl SequentMap { { post_constraints.push(ConstraintId::new_with_bounds( db, + env, storage, bound_typevar, constrained_lower, @@ -6324,6 +6580,7 @@ impl SequentMap { for post_constraint in post_constraints { self.add_pair_implication( db, + env, storage, left_constraint, right_constraint, @@ -6339,6 +6596,7 @@ impl SequentMap { fn add_concrete_sequents<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, left_constraint: ConstraintId, right_constraint: ConstraintId, @@ -6348,38 +6606,39 @@ impl SequentMap { // identify constraints that are identical besides e.g. ordering of union/intersection // elements. (For instance, when processing `T ≤ τ₁ & τ₂` and `T ≤ τ₂ & τ₁`, these clauses // would add sequents for `(T ≤ τ₁ & τ₂) → (T ≤ τ₂ & τ₁)` and vice versa.) - if storage.cached_constraint_implies(db, left_constraint, right_constraint) { + if storage.cached_constraint_implies(db, env, left_constraint, right_constraint) { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db, storage), - right = %right_constraint.display(db, storage), + left = %left_constraint.display(db, env, storage), + right = %right_constraint.display(db, env, storage), "left implies right", ); self.add_single_implication(left_constraint, right_constraint); } - if storage.cached_constraint_implies(db, right_constraint, left_constraint) { + if storage.cached_constraint_implies(db, env, right_constraint, left_constraint) { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db, storage), - right = %right_constraint.display(db, storage), + left = %left_constraint.display(db, env, storage), + right = %right_constraint.display(db, env, storage), "right implies left", ); self.add_single_implication(right_constraint, left_constraint); } - match left_constraint.intersect(db, storage, right_constraint) { + match left_constraint.intersect(db, env, storage, right_constraint) { IntersectionResult::Simplified(intersection_constraint_data) => { let intersection_constraint = - storage.intern_constraint(db, intersection_constraint_data); + storage.intern_constraint(db, env, intersection_constraint_data); tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db, storage), - right = %right_constraint.display(db, storage), - intersection = %intersection_constraint.display(db, storage), + left = %left_constraint.display(db, env, storage), + right = %right_constraint.display(db, env, storage), + intersection = %intersection_constraint.display(db, env, storage), "left and right overlap", ); self.add_pair_implication( db, + env, storage, left_constraint, right_constraint, @@ -6397,8 +6656,8 @@ impl SequentMap { IntersectionResult::Disjoint => { tracing::trace!( target: "ty_python_semantic::types::constraints::SequentMap", - left = %left_constraint.display(db, storage), - right = %right_constraint.display(db, storage), + left = %left_constraint.display(db, env, storage), + right = %right_constraint.display(db, env, storage), "left and right are disjoint", ); self.add_pair_impossibility(left_constraint, right_constraint); @@ -6410,6 +6669,7 @@ impl SequentMap { fn display<'db, 'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, storage: &'a ConstraintSetStorage<'db>, prefix: &'a dyn Display, ) -> impl Display + 'a { @@ -6417,11 +6677,13 @@ impl SequentMap { map: &'a SequentMap, prefix: &'a dyn Display, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, storage: &'a ConstraintSetStorage<'db>, } impl Display for DisplaySequentMap<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; let mut first = true; let mut maybe_write_prefix = |f: &mut std::fmt::Formatter<'_>| { if first { @@ -6441,8 +6703,8 @@ impl SequentMap { write!( f, "{} ∧ {} → false", - ante1.display(self.db, self.storage), - ante2.display(self.db, self.storage), + ante1.display(db, self.env, self.storage), + ante2.display(db, self.env, self.storage), )?; } @@ -6451,9 +6713,9 @@ impl SequentMap { write!( f, "{} ∧ {} → {}", - ante1.display(self.db, self.storage), - ante2.display(self.db, self.storage), - post.display(self.db, self.storage), + ante1.display(db, self.env, self.storage), + ante2.display(db, self.env, self.storage), + post.display(db, self.env, self.storage), )?; } @@ -6462,8 +6724,8 @@ impl SequentMap { write!( f, "{} → {}", - ante.display(self.db, self.storage), - post.display(self.db, self.storage) + ante.display(db, self.env, self.storage), + post.display(db, self.env, self.storage) )?; } } @@ -6480,6 +6742,7 @@ impl SequentMap { map: self, prefix, db, + env, storage, } } @@ -6884,6 +7147,7 @@ impl PathAssignments { fn visit<'db, V>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, node: NodeId, visitor: &mut V, @@ -6891,13 +7155,14 @@ impl PathAssignments { where V: PathVisitor, { - self.visit_inner(db, storage, node, visitor, false) + self.visit_inner(db, env, storage, node, visitor, false) } /// Visits the paths of the negation of `node`, without constructing that negation eagerly. fn visit_negated<'db, V>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, node: NodeId, visitor: &mut V, @@ -6905,12 +7170,13 @@ impl PathAssignments { where V: PathVisitor, { - self.visit_inner(db, storage, node, visitor, true) + self.visit_inner(db, env, storage, node, visitor, true) } fn visit_inner<'db, V>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, node: NodeId, visitor: &mut V, @@ -6937,13 +7203,14 @@ impl PathAssignments { }; let if_true = self.walk_edge( db, + env, storage, interior.constraint.when_true(), |storage, path, new_range, found_conflict| { let subtree = if found_conflict { visitor.visit_impossible(db, storage, path) } else { - path.visit_inner(db, storage, true_subtree, visitor, negated) + path.visit_inner(db, env, storage, true_subtree, visitor, negated) }; match subtree { ControlFlow::Continue(subtree) => visitor.visit_edge( @@ -6965,13 +7232,21 @@ impl PathAssignments { } else { self.walk_edge( db, + env, storage, interior.constraint.when_unconstrained(), |storage, path, new_range, found_conflict| { let subtree = if found_conflict { visitor.visit_impossible(db, storage, path) } else { - path.visit_inner(db, storage, interior.if_uncertain, visitor, false) + path.visit_inner( + db, + env, + storage, + interior.if_uncertain, + visitor, + false, + ) }; match subtree { ControlFlow::Continue(subtree) => visitor.visit_edge( @@ -6995,13 +7270,14 @@ impl PathAssignments { }; let if_false = self.walk_edge( db, + env, storage, interior.constraint.when_false(), |storage, path, new_range, found_conflict| { let subtree = if found_conflict { visitor.visit_impossible(db, storage, path) } else { - path.visit_inner(db, storage, false_subtree, visitor, negated) + path.visit_inner(db, env, storage, false_subtree, visitor, negated) }; match subtree { ControlFlow::Continue(subtree) => visitor.visit_edge( @@ -7054,6 +7330,7 @@ impl PathAssignments { fn walk_edge<'db, R>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, assignment: ConstraintAssignment, f: impl FnOnce(&mut ConstraintSetStorage<'db>, &mut Self, Range, bool) -> R, @@ -7071,10 +7348,10 @@ impl PathAssignments { before = %format_args!( "[{}]", self.assignments[..start].iter().map(|(assignment, _)| { - assignment.display(db, storage) + assignment.display(db, env, storage) }).format(", "), ), - edge = %assignment.display(db, storage), + edge = %assignment.display(db, env, storage), "walk edge", ); debug_assert!(self.assignment_queue.is_empty()); @@ -7082,7 +7359,7 @@ impl PathAssignments { .push_back((assignment, AssignmentFuel::origin())); let source_constraint = assignment.constraint(); let found_conflict = self - .drain_assignment_queue(db, storage, source_constraint) + .drain_assignment_queue(db, env, storage, source_constraint) .is_err(); if !found_conflict { tracing::trace!( @@ -7090,7 +7367,7 @@ impl PathAssignments { new = %format_args!( "[{}]", self.assignments[start..].iter().map(|(assignment, _)| { - assignment.display(db, storage) + assignment.display(db, env, storage) }).format(", "), ), "new assignments", @@ -7154,6 +7431,7 @@ impl PathAssignments { fn discover_constraint<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, constraint: ConstraintId, ) { @@ -7164,7 +7442,7 @@ impl PathAssignments { return; } - let single_map = SequentMap::for_constraint(db, storage, constraint); + let single_map = SequentMap::for_constraint(db, env, storage, constraint); self.sequents.extend_from_slice(&single_map.sequents); for (existing_index, (existing, _)) in self.discovered.iter().enumerate() { @@ -7172,7 +7450,7 @@ impl PathAssignments { continue; } - if SequentMap::pair_cannot_produce_sequents(db, storage, *existing, constraint) { + if SequentMap::pair_cannot_produce_sequents(db, env, storage, *existing, constraint) { continue; } @@ -7186,7 +7464,7 @@ impl PathAssignments { continue; } - let pair_map = SequentMap::for_constraint_pair(db, storage, a, b); + let pair_map = SequentMap::for_constraint_pair(db, env, storage, a, b); self.sequents.extend_from_slice(&pair_map.sequents); } } @@ -7194,11 +7472,12 @@ impl PathAssignments { fn drain_assignment_queue<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, source_constraint: ConstraintId, ) -> Result<(), PathAssignmentConflict> { while let Some((assignment, fuel)) = self.assignment_queue.pop_front() { - self.add_assignment(db, storage, assignment, source_constraint, fuel)?; + self.add_assignment(db, env, storage, assignment, source_constraint, fuel)?; } Ok(()) } @@ -7209,6 +7488,7 @@ impl PathAssignments { fn add_assignment<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, assignment: ConstraintAssignment, source_constraint: ConstraintId, @@ -7235,11 +7515,11 @@ impl PathAssignments { if self.assignments.contains_key(&assignment.negated()) { tracing::trace!( target: "ty_python_semantic::types::constraints::PathAssignment", - assignment = %assignment.display(db, storage), + assignment = %assignment.display(db, env, storage), facts = %format_args!( "[{}]", self.assignments.iter().map(|(assignment, _)| { - assignment.display(db, storage) + assignment.display(db, env, storage) }).format(", "), ), "found contradiction", @@ -7306,11 +7586,11 @@ impl PathAssignments { // brute-force search. self.new_assignments.clear(); - self.discover_constraint(db, storage, assignment.constraint()); + self.discover_constraint(db, env, storage, assignment.constraint()); for i in 0..self.sequents.len() { let sequent = self.sequents[i]; - self.check_sequent(db, storage, sequent)?; + self.check_sequent(db, env, storage, sequent)?; } // If we were able to derive any new assignments from this one, add them to the processing @@ -7332,20 +7612,23 @@ impl PathAssignments { fn check_sequent<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, sequent: Sequent, ) -> Result<(), PathAssignmentConflict> { match sequent { - Sequent::SingleTautology { ante } => self.check_single_tautology(db, storage, ante), + Sequent::SingleTautology { ante } => { + self.check_single_tautology(db, env, storage, ante) + } Sequent::PairImpossibility { ante1, ante2 } => { - self.check_pair_impossibility(db, storage, ante1, ante2) + self.check_pair_impossibility(db, env, storage, ante1, ante2) } Sequent::PairImplication { ante1, ante2, post } => { - self.check_pair_implication(db, storage, ante1, ante2, post); + self.check_pair_implication(db, env, storage, ante1, ante2, post); Ok(()) } Sequent::SingleImplication { ante, post } => { - self.check_single_implication(db, storage, ante, post); + self.check_single_implication(db, env, storage, ante, post); Ok(()) } } @@ -7354,6 +7637,7 @@ impl PathAssignments { fn check_single_tautology<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, ante: ConstraintId, ) -> Result<(), PathAssignmentConflict> { @@ -7362,11 +7646,11 @@ impl PathAssignments { // it's false. tracing::trace!( target: "ty_python_semantic::types::constraints::PathAssignment", - ante = %ante.display(db, storage), + ante = %ante.display(db, env, storage), facts = %format_args!( "[{}]", self.assignments.iter().map(|(assignment, _)| { - assignment.display(db, storage) + assignment.display(db, env, storage) }).format(", "), ), "found contradiction", @@ -7380,6 +7664,7 @@ impl PathAssignments { fn check_pair_impossibility<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, ante1: ConstraintId, ante2: ConstraintId, @@ -7389,12 +7674,12 @@ impl PathAssignments { // current path asserts that both are true. tracing::trace!( target: "ty_python_semantic::types::constraints::PathAssignment", - ante1 = %ante1.display(db, storage), - ante2 = %ante2.display(db, storage), + ante1 = %ante1.display(db, env, storage), + ante2 = %ante2.display(db, env, storage), facts = %format_args!( "[{}]", self.assignments.iter().map(|(assignment, _)| { - assignment.display(db, storage) + assignment.display(db, env, storage) }).format(", "), ), "found contradiction", @@ -7408,6 +7693,7 @@ impl PathAssignments { fn check_pair_implication<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, ante1: ConstraintId, ante2: ConstraintId, @@ -7420,10 +7706,10 @@ impl PathAssignments { return; }; let available_fuel = ante1_fuel.min(ante2_fuel); - let (ante1_constructor_depth, _) = storage.cached_constraint_bound_depth(db, ante1); - let (ante2_constructor_depth, _) = storage.cached_constraint_bound_depth(db, ante2); + let (ante1_constructor_depth, _) = storage.cached_constraint_bound_depth(db, env, ante1); + let (ante2_constructor_depth, _) = storage.cached_constraint_bound_depth(db, env, ante2); let antecedent_constructor_depth = ante1_constructor_depth.max(ante2_constructor_depth); - let fuel_cost = storage.sequent_fuel_cost(db, post, antecedent_constructor_depth); + let fuel_cost = storage.sequent_fuel_cost(db, env, post, antecedent_constructor_depth); if let Some(post_fuel) = available_fuel.checked_sub(fuel_cost) { self.enqueue_assignment( post.when_true(), @@ -7435,6 +7721,7 @@ impl PathAssignments { fn check_single_implication<'db>( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, ante: ConstraintId, post: ConstraintId, @@ -7443,12 +7730,13 @@ impl PathAssignments { return; }; let ante_data = storage.constraint_data(ante); - let (antecedent_constructor_depth, _) = storage.cached_constraint_bound_depth(db, ante); + let (antecedent_constructor_depth, _) = + storage.cached_constraint_bound_depth(db, env, ante); let post_data = storage.constraint_data(post); let fuel_cost = if post_data.is_bound_projection_of(db, ante_data) { 1 } else { - storage.sequent_fuel_cost(db, post, antecedent_constructor_depth) + storage.sequent_fuel_cost(db, env, post, antecedent_constructor_depth) }; if let Some(post_fuel) = available_fuel.checked_sub(fuel_cost) { self.enqueue_assignment( @@ -7506,7 +7794,12 @@ impl SatisfiedClause { /// want to remove the larger one and keep the smaller one.) /// /// Returns a boolean that indicates whether any simplifications were made. - fn simplify<'db>(&mut self, db: &'db dyn Db, storage: &mut ConstraintSetStorage<'db>) -> bool { + fn simplify<'db>( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + ) -> bool { let mut changes_made = false; let mut i = 0; // Loop through each constraint, comparing it with any constraints that appear later in the @@ -7514,7 +7807,7 @@ impl SatisfiedClause { 'outer: while i < self.constraints.len() { let mut j = i + 1; while j < self.constraints.len() { - if self.constraints[j].implies(db, storage, self.constraints[i]) { + if self.constraints[j].implies(db, env, storage, self.constraints[i]) { // If constraint `i` is removed, then we don't need to compare it with any // later constraints in the list. Note that we continue the outer loop, instead // of breaking from the inner loop, so that we don't bump index `i` below. @@ -7523,7 +7816,7 @@ impl SatisfiedClause { self.constraints.swap_remove(i); changes_made = true; continue 'outer; - } else if self.constraints[i].implies(db, storage, self.constraints[j]) { + } else if self.constraints[i].implies(db, env, storage, self.constraints[j]) { // If constraint `j` is removed, then we can continue the inner loop. We will // swap a new element into place at index `j`, and will continue comparing the // constraint at index `i` with later constraints. @@ -7538,7 +7831,12 @@ impl SatisfiedClause { changes_made } - fn display<'db>(&self, db: &'db dyn Db, storage: &ConstraintSetStorage<'db>) -> String { + fn display<'db>( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + storage: &ConstraintSetStorage<'db>, + ) -> String { if self.constraints.is_empty() { return String::from("always"); } @@ -7549,7 +7847,7 @@ impl SatisfiedClause { let mut constraints: Vec<_> = self .constraints .iter() - .map(|constraint| constraint.display(db, storage).to_string()) + .map(|constraint| constraint.display(db, env, storage).to_string()) .collect(); constraints.sort(); @@ -7585,11 +7883,16 @@ impl SatisfiedClauses { /// Simplifies the DNF representation, removing redundancies that do not change the underlying /// function. (This is used when displaying a BDD, to make sure that the representation that we /// show is as simple as possible while still producing the same results.) - fn simplify<'db>(&mut self, db: &'db dyn Db, storage: &mut ConstraintSetStorage<'db>) { + fn simplify<'db>( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + ) { // First simplify each clause individually, by removing constraints that are implied by // other constraints in the clause. for clause in &mut self.clauses { - clause.simplify(db, storage); + clause.simplify(db, env, storage); } while self.simplify_one_round() { @@ -7661,7 +7964,12 @@ impl SatisfiedClauses { false } - fn display<'db>(&self, db: &'db dyn Db, storage: &ConstraintSetStorage<'db>) -> String { + fn display<'db>( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + storage: &ConstraintSetStorage<'db>, + ) -> String { // This is a bit heavy-handed, but we need to output the clauses in a consistent order // even though Salsa IDs are assigned non-deterministically. This Display output is only // used in test cases, so we don't need to over-optimize it. @@ -7672,7 +7980,7 @@ impl SatisfiedClauses { let mut clauses: Vec<_> = self .clauses .iter() - .map(|clause| clause.display(db, storage)) + .map(|clause| clause.display(db, env, storage)) .collect(); clauses.sort(); clauses.join(" ∨ ") @@ -7686,6 +7994,7 @@ impl<'db> BoundTypeVarInstance<'db> { fn valid_specializations( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, ) -> (NodeId, Option) { if self.paramspec_attr(db).is_some() { @@ -7703,20 +8012,21 @@ impl<'db> BoundTypeVarInstance<'db> { // _equality_ comparisons, not _subtyping_ comparisons — since we are only going to check // that _some_ valid specialization satisfies the constraint set, it's correct for us to // return the range of valid materializations that we can choose from. - match self.typevar(db).bound_or_constraints(db) { + match self.typevar(db).bound_or_constraints(db, env) { None => (ALWAYS_TRUE, None), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - let bound = bound.top_materialization(db); - Constraint::new_node_with_bounds(db, storage, self, None, Some(bound)) + let bound = bound.top_materialization(db, env); + Constraint::new_node_with_bounds(db, env, storage, self, None, Some(bound)) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { let mut specializations = ALWAYS_FALSE; let mut source_order = None; for constraint in constraints.elements(db) { - let constraint_lower = constraint.bottom_materialization(db); - let constraint_upper = constraint.top_materialization(db); + let constraint_lower = constraint.bottom_materialization(db, env); + let constraint_upper = constraint.top_materialization(db, env); let (constraint, constraint_source_order) = Constraint::new_node_with_bounds( db, + env, storage, self, Some(constraint_lower), @@ -7748,6 +8058,7 @@ impl<'db> BoundTypeVarInstance<'db> { fn required_specializations( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, storage: &mut ConstraintSetStorage<'db>, ) -> ( (NodeId, Option), @@ -7758,12 +8069,12 @@ impl<'db> BoundTypeVarInstance<'db> { // materialization that is as restrictive as possible, since that minimizes the number of // valid specializations that must satisfy the check. We therefore take the bottom // materialization of the bound or constraints. - match self.typevar(db).bound_or_constraints(db) { + match self.typevar(db).bound_or_constraints(db, env) { None => ((ALWAYS_TRUE, None), Vec::new()), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - let bound = bound.bottom_materialization(db); + let bound = bound.bottom_materialization(db, env); ( - Constraint::new_node_with_bounds(db, storage, self, None, Some(bound)), + Constraint::new_node_with_bounds(db, env, storage, self, None, Some(bound)), Vec::new(), ) } @@ -7772,10 +8083,11 @@ impl<'db> BoundTypeVarInstance<'db> { let mut non_gradual_source_order = None; let mut gradual_constraints = Vec::new(); for constraint in constraints.elements(db) { - let constraint_lower = constraint.bottom_materialization(db); - let constraint_upper = constraint.top_materialization(db); + let constraint_lower = constraint.bottom_materialization(db, env); + let constraint_upper = constraint.top_materialization(db, env); let constraint = Constraint::new_node_with_bounds( db, + env, storage, self, Some(constraint_lower), @@ -7805,106 +8117,125 @@ mod tests { use indoc::indoc; use pretty_assertions::assert_eq; - use crate::db::tests::setup_db; + use crate::db::tests::{TestDb, setup_db}; use crate::types::generics::ApplySpecialization; use crate::types::{BoundTypeVarInstance, KnownClass, SubclassOfType, TypeVarVariance}; use ruff_python_ast::name::Name; - fn create_typevar<'db>(db: &'db dyn Db, name: &'static str) -> BoundTypeVarInstance<'db> { - BoundTypeVarInstance::synthetic(db, Name::new_static(name), TypeVarVariance::Invariant) + fn create_typevar<'db>(db: &'db TestDb, name: &'static str) -> BoundTypeVarInstance<'db> { + BoundTypeVarInstance::synthetic( + db, + &db.program_environment(), + Name::new_static(name), + TypeVarVariance::Invariant, + ) } fn create_constraint<'db, 'c>( - db: &'db dyn Db, + db: &'db TestDb, builder: &'c ConstraintSetBuilder<'db>, bound_typevar: BoundTypeVarInstance<'db>, bound: KnownClass, ) -> ConstraintSet<'db, 'c> { - let ty = bound.to_instance(db); - ConstraintSet::constrain_typevar(db, builder, bound_typevar, ty, ty) + let env = db.program_environment(); + let ty = bound.to_instance(db, &env); + ConstraintSet::constrain_typevar(db, &env, builder, bound_typevar, ty, ty) } - fn known_instance(db: &dyn Db, class: KnownClass) -> Type<'_> { - class.to_instance(db) + fn known_instance(db: &TestDb, class: KnownClass) -> Type<'_> { + class.to_instance(db, &db.program_environment()) } #[test] fn type_mapping_updates_constraint_bounds() { // (list[U] ≤ T ≤ list[U])[U ↦ int] = (list[int] ≤ T ≤ list[int]) let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let list_of_u = KnownClass::List.to_specialized_instance(&db, &[Type::TypeVar(u)]); - let set = ConstraintSet::constrain_typevar(&db, &builder, t, list_of_u, list_of_u); + let list_of_u = KnownClass::List.to_specialized_instance(db, &env, &[Type::TypeVar(u)]); + let set = ConstraintSet::constrain_typevar(db, &env, &builder, t, list_of_u, list_of_u); - let int = KnownClass::Int.to_instance(&db); + let int = KnownClass::Int.to_instance(db, &env); let mapped = set.apply_type_mapping_impl( - &db, + db, &TypeMapping::ApplySpecialization(ApplySpecialization::Single(u, int)), TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(&env), ); - let list_of_int = KnownClass::List.to_specialized_instance(&db, &[int]); - let expected = ConstraintSet::constrain_typevar(&db, &builder, t, list_of_int, list_of_int); + let list_of_int = KnownClass::List.to_specialized_instance(db, &env, &[int]); + let expected = + ConstraintSet::constrain_typevar(db, &env, &builder, t, list_of_int, list_of_int); - assert!(mapped.iff(&db, &builder, expected).is_always_satisfied(&db)); + assert!( + mapped + .iff(db, &builder, expected) + .is_always_satisfied(db, &env) + ); } #[test] fn type_mapping_evaluates_mapped_subjects() { // ((T = int) ∧ ¬(T = str))[T ↦ int] = true let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let set = create_constraint(&db, &builder, t, KnownClass::Int).and(&db, &builder, || { - create_constraint(&db, &builder, t, KnownClass::Str).negate(&db, &builder) + let set = create_constraint(db, &builder, t, KnownClass::Int).and(db, &builder, || { + create_constraint(db, &builder, t, KnownClass::Str).negate(db, &builder) }); let mapped = set.apply_type_mapping_impl( - &db, + db, &TypeMapping::ApplySpecialization(ApplySpecialization::Single( t, - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), )), TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(&env), ); - assert!(mapped.is_always_satisfied(&db)); + assert!(mapped.is_always_satisfied(db, &env)); } #[test] fn type_mapping_handles_absorbed_constraints_in_source_order() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let str = create_constraint(&db, &builder, t, KnownClass::Str); - let int = create_constraint(&db, &builder, t, KnownClass::Int); - let set = str.or(&db, &builder, || int).and(&db, &builder, || str); + let str = create_constraint(db, &builder, t, KnownClass::Str); + let int = create_constraint(db, &builder, t, KnownClass::Int); + let set = str.or(db, &builder, || int).and(db, &builder, || str); let mapped = set.apply_type_mapping_impl( - &db, + db, &TypeMapping::ApplySpecialization(ApplySpecialization::Single( t, - KnownClass::Str.to_instance(&db), + KnownClass::Str.to_instance(db, &env), )), TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(&env), ); - assert!(mapped.is_always_satisfied(&db)); + assert!(mapped.is_always_satisfied(db, &env)); } #[test] fn upper_bound_collapses_never() { let db = setup_db(); - let int = known_instance(&db, KnownClass::Int); + let db = &db; + let env = db.program_environment(); + let int = known_instance(db, KnownClass::Int); let mut upper = UpperBound::from_clause(int); upper.add_clause(Type::Never); assert_eq!(upper.clauses, FxOrderSet::from_iter([Type::Never])); - assert_eq!(upper.materialize_exact(&db), Type::Never); + assert_eq!(upper.materialize_exact(db, &env), Type::Never); upper.add_clause(int); assert_eq!(upper.clauses, FxOrderSet::from_iter([Type::Never])); @@ -7913,11 +8244,13 @@ mod tests { #[test] fn upper_bound_recovers_redundant_single_bounds() { let db = setup_db(); - let int = known_instance(&db, KnownClass::Int); - let bool = known_instance(&db, KnownClass::Bool); - let str = known_instance(&db, KnownClass::Str); - let int_or_str = UnionType::from_two_elements(&db, int, str); - let u = create_typevar(&db, "U").map_bound_or_constraints(&db, |_| { + let db = &db; + let env = db.program_environment(); + let int = known_instance(db, KnownClass::Int); + let bool = known_instance(db, KnownClass::Bool); + let str = known_instance(db, KnownClass::Str); + let int_or_str = UnionType::from_two_elements(db, &env, int, str); + let u = create_typevar(db, "U").map_bound_or_constraints(db, |_| { Some(TypeVarBoundOrConstraints::UpperBound(int_or_str)) }); let u = Type::TypeVar(u); @@ -7936,17 +8269,19 @@ mod tests { } assert_eq!(upper.clauses.len(), 2); - assert_eq!(upper.as_single_bound(&db), Some(expected)); + assert_eq!(upper.as_single_bound(db, &env), Some(expected)); } } #[test] fn upper_bound_distinguishes_missing_bound_from_explicit_object() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); - assert_eq!(UpperBound::none().as_single_bound(&db), None); + assert_eq!(UpperBound::none().as_single_bound(db, &env), None); assert_eq!( - UpperBound::from_clause(Type::object()).as_single_bound(&db), + UpperBound::from_clause(Type::object()).as_single_bound(db, &env), Some(Type::object()) ); } @@ -7954,11 +8289,13 @@ mod tests { #[test] fn upper_bound_does_not_materialize_overlapping_union_clauses() { let db = setup_db(); - let int = known_instance(&db, KnownClass::Int); - let str = known_instance(&db, KnownClass::Str); - let bytes = known_instance(&db, KnownClass::Bytes); - let int_or_str = UnionType::from_two_elements(&db, int, str); - let int_or_bytes = UnionType::from_two_elements(&db, int, bytes); + let db = &db; + let env = db.program_environment(); + let int = known_instance(db, KnownClass::Int); + let str = known_instance(db, KnownClass::Str); + let bytes = known_instance(db, KnownClass::Bytes); + let int_or_str = UnionType::from_two_elements(db, &env, int, str); + let int_or_bytes = UnionType::from_two_elements(db, &env, int, bytes); for clauses in [[int_or_str, int_or_bytes], [int_or_bytes, int_or_str]] { let mut upper = UpperBound::none(); @@ -7966,78 +8303,89 @@ mod tests { upper.add_clause(clause); } - assert_eq!(upper.materialize_exact(&db), int); - assert_eq!(upper.as_single_bound(&db), None); + assert_eq!(upper.materialize_exact(db, &env), int); + assert_eq!(upper.as_single_bound(db, &env), None); } } #[test] fn upper_bound_does_not_treat_nontrivial_intersection_as_single_bound() { let db = setup_db(); - let int = known_instance(&db, KnownClass::Int); - let u = Type::TypeVar(create_typevar(&db, "U")); + let db = &db; + let env = db.program_environment(); + let int = known_instance(db, KnownClass::Int); + let u = Type::TypeVar(create_typevar(db, "U")); let mut upper = UpperBound::from_clause(u); upper.add_clause(int); - assert!(upper.materialize_exact(&db).is_nontrivial_intersection(&db)); - assert_eq!(upper.as_single_bound(&db), None); + assert!( + upper + .materialize_exact(db, &env) + .is_nontrivial_intersection(db) + ); + assert_eq!(upper.as_single_bound(db, &env), None); } #[test] fn trivial_disjointness_does_not_claim_bounded_typevar_class_is_disjoint() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let builder = ConstraintSetBuilder::new(); - let bool = known_instance(&db, KnownClass::Bool); - let u = create_typevar(&db, "U") - .map_bound_or_constraints(&db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); - let type_of_u = SubclassOfType::from(&db, u); - let bool_class = KnownClass::Bool.to_class_literal(&db); + let bool = known_instance(db, KnownClass::Bool); + let u = create_typevar(db, "U") + .map_bound_or_constraints(db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); + let type_of_u = SubclassOfType::from(db, &env, u); + let bool_class = KnownClass::Bool.to_class_literal(db, &env); for (left, right) in [(type_of_u, bool_class), (bool_class, type_of_u)] { - let trivial = left.when_trivially_disjoint_from(&db, right, &builder, TypeVarSet::None); - let full = left.when_disjoint_from(&db, right, &builder, TypeVarSet::None); + let trivial = + left.when_trivially_disjoint_from(db, &env, right, &builder, TypeVarSet::None); + let full = left.when_disjoint_from(db, &env, right, &builder, TypeVarSet::None); assert!(trivial.is_trivially_never_satisfied()); - assert!(!full.is_always_satisfied(&db)); + assert!(!full.is_always_satisfied(db, &env)); } } #[test] fn trivial_disjointness_implies_full_disjointness() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let builder = ConstraintSetBuilder::new(); - let bool = known_instance(&db, KnownClass::Bool); - let u = create_typevar(&db, "U") - .map_bound_or_constraints(&db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); + let bool = known_instance(db, KnownClass::Bool); + let u = create_typevar(db, "U") + .map_bound_or_constraints(db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); let types = [ Type::Never, Type::object(), bool, - known_instance(&db, KnownClass::Int), - known_instance(&db, KnownClass::Str), + known_instance(db, KnownClass::Int), + known_instance(db, KnownClass::Str), Type::int_literal(0), Type::int_literal(1), Type::bool_literal(true), Type::bool_literal(false), - Type::string_literal(&db, "value"), - KnownClass::Bool.to_class_literal(&db), - KnownClass::Int.to_class_literal(&db), - SubclassOfType::from(&db, u), + Type::string_literal(db, "value"), + KnownClass::Bool.to_class_literal(db, &env), + KnownClass::Int.to_class_literal(db, &env), + SubclassOfType::from(db, &env, u), ]; let mut positive_results = 0; for left in types { for right in types { let trivial = - left.when_trivially_disjoint_from(&db, right, &builder, TypeVarSet::None); + left.when_trivially_disjoint_from(db, &env, right, &builder, TypeVarSet::None); if trivial.is_trivially_always_satisfied() { positive_results += 1; assert!( - left.when_disjoint_from(&db, right, &builder, TypeVarSet::None) - .is_always_satisfied(&db), + left.when_disjoint_from(db, &env, right, &builder, TypeVarSet::None) + .is_always_satisfied(db, &env), "cheap disjointness incorrectly accepts `{}` and `{}`", - left.display(&db), - right.display(&db) + left.display(db, &env), + right.display(db, &env) ); } } @@ -8049,19 +8397,22 @@ mod tests { #[test] fn overlapping_lower_bounds_do_not_skip_nonempty_sequent_map() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let builder = ConstraintSetBuilder::new(); - let t = create_typevar(&db, "T"); - let bool = known_instance(&db, KnownClass::Bool); - let u = create_typevar(&db, "U") - .map_bound_or_constraints(&db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); - let type_of_u = SubclassOfType::from(&db, u); - let bool_class = KnownClass::Bool.to_class_literal(&db); + let t = create_typevar(db, "T"); + let bool = known_instance(db, KnownClass::Bool); + let u = create_typevar(db, "U") + .map_bound_or_constraints(db, |_| Some(TypeVarBoundOrConstraints::UpperBound(bool))); + let type_of_u = SubclassOfType::from(db, &env, u); + let bool_class = KnownClass::Bool.to_class_literal(db, &env); let mut storage = builder.storage.borrow_mut(); - let left = ConstraintId::new_with_bounds(&db, &mut storage, t, Some(type_of_u), None); - let right = ConstraintId::new_with_bounds(&db, &mut storage, t, Some(bool_class), None); + let left = ConstraintId::new_with_bounds(db, &env, &mut storage, t, Some(type_of_u), None); + let right = + ConstraintId::new_with_bounds(db, &env, &mut storage, t, Some(bool_class), None); for (left, right) in [(left, right), (right, left)] { - let sequents = SequentMap::for_constraint_pair(&db, &mut storage, left, right); + let sequents = SequentMap::for_constraint_pair(db, &env, &mut storage, left, right); assert!( sequents @@ -8070,7 +8421,8 @@ mod tests { .any(|sequent| matches!(sequent, Sequent::SingleImplication { .. })) ); assert!(!SequentMap::pair_cannot_produce_sequents( - &db, + db, + &env, &mut storage, left, right @@ -8081,16 +8433,18 @@ mod tests { #[test] fn simple_lower_bound_conjunction_skips_sequent_analysis() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let int = KnownClass::Int.to_instance(&db); - let str = KnownClass::Str.to_instance(&db); - let set = ConstraintSet::constrain_typevar_lower_bound(&db, &builder, t, int).and( - &db, + let int = KnownClass::Int.to_instance(db, &env); + let str = KnownClass::Str.to_instance(db, &env); + let set = ConstraintSet::constrain_typevar_lower_bound(db, &env, &builder, t, int).and( + db, &builder, - || ConstraintSet::constrain_typevar_lower_bound(&db, &builder, t, str), + || ConstraintSet::constrain_typevar_lower_bound(db, &env, &builder, t, str), ); - let inferable = TypeVarSet::from_typevars(&db, [t]); + let inferable = TypeVarSet::from_typevars(db, [t]); let (single_sequents, pair_sequents) = { let storage = builder.storage.borrow(); ( @@ -8099,12 +8453,12 @@ mod tests { ) }; - let solutions = set.solutions(&db, &builder, inferable); + let solutions = set.solutions(db, &env, &builder, inferable); assert_eq!( solutions, Solutions::Constrained(vec![vec![TypeVarSolution { bound_typevar: t, - solution: UnionType::from_elements(&db, [int, str]), + solution: UnionType::from_elements(db, &env, [int, str]), }]]) ); @@ -8116,15 +8470,18 @@ mod tests { #[test] fn simple_exact_bound_conjunction_skips_sequent_analysis() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let int = KnownClass::Int.to_instance(&db); - let set = - ConstraintSet::constrain_typevar(&db, &builder, t, int, int).and(&db, &builder, || { - ConstraintSet::constrain_typevar(&db, &builder, u, int, int) - }); - let inferable = TypeVarSet::from_typevars(&db, [t, u]); + let int = KnownClass::Int.to_instance(db, &env); + let set = ConstraintSet::constrain_typevar(db, &env, &builder, t, int, int).and( + db, + &builder, + || ConstraintSet::constrain_typevar(db, &env, &builder, u, int, int), + ); + let inferable = TypeVarSet::from_typevars(db, [t, u]); let (single_sequents, pair_sequents) = { let storage = builder.storage.borrow(); ( @@ -8133,7 +8490,7 @@ mod tests { ) }; - let Solutions::Constrained(solutions) = set.solutions(&db, &builder, inferable) else { + let Solutions::Constrained(solutions) = set.solutions(db, &env, &builder, inferable) else { panic!("expected constrained solutions"); }; assert_eq!(solutions.len(), 1); @@ -8155,15 +8512,18 @@ mod tests { #[test] fn simple_unsatisfiable_exact_bound_conjunction_skips_sequent_analysis() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let int = KnownClass::Int.to_instance(&db); - let str = KnownClass::Str.to_instance(&db); - let set = - ConstraintSet::constrain_typevar(&db, &builder, t, int, int).and(&db, &builder, || { - ConstraintSet::constrain_typevar(&db, &builder, t, str, str) - }); - let inferable = TypeVarSet::from_typevars(&db, [t]); + let int = KnownClass::Int.to_instance(db, &env); + let str = KnownClass::Str.to_instance(db, &env); + let set = ConstraintSet::constrain_typevar(db, &env, &builder, t, int, int).and( + db, + &builder, + || ConstraintSet::constrain_typevar(db, &env, &builder, t, str, str), + ); + let inferable = TypeVarSet::from_typevars(db, [t]); let (single_sequents, pair_sequents) = { let storage = builder.storage.borrow(); ( @@ -8173,7 +8533,7 @@ mod tests { }; assert_eq!( - set.solutions(&db, &builder, inferable), + set.solutions(db, &env, &builder, inferable), Solutions::Unsatisfiable ); @@ -8185,7 +8545,9 @@ mod tests { #[test] fn default_solve_leaves_unbounded_typevar_unsolved_without_bounds() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); let path_bound = PathBound { bound_typevar: t, @@ -8195,7 +8557,7 @@ mod tests { }; assert_eq!( - PathBounds::default_solve(&db, &builder, &path_bound), + PathBounds::default_solve(db, &env, &builder, &path_bound), Ok(None) ); } @@ -8203,24 +8565,33 @@ mod tests { #[test] fn constraint_intersection_detects_disjoint_union_upper_bounds() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let int = known_instance(&db, KnownClass::Int); - let str = known_instance(&db, KnownClass::Str); - let bytes = known_instance(&db, KnownClass::Bytes); - let bytearray = known_instance(&db, KnownClass::Bytearray); - let int_or_str = UnionType::from_two_elements(&db, int, str); - let bytes_or_bytearray = UnionType::from_two_elements(&db, bytes, bytearray); + let int = known_instance(db, KnownClass::Int); + let str = known_instance(db, KnownClass::Str); + let bytes = known_instance(db, KnownClass::Bytes); + let bytearray = known_instance(db, KnownClass::Bytearray); + let int_or_str = UnionType::from_two_elements(db, &env, int, str); + let bytes_or_bytearray = UnionType::from_two_elements(db, &env, bytes, bytearray); let mut storage = builder.storage.borrow_mut(); - let left = ConstraintId::new_with_bounds(&db, &mut storage, t, Some(int), Some(int_or_str)); - let right = - ConstraintId::new_with_bounds(&db, &mut storage, t, None, Some(bytes_or_bytearray)); + let left = + ConstraintId::new_with_bounds(db, &env, &mut storage, t, Some(int), Some(int_or_str)); + let right = ConstraintId::new_with_bounds( + db, + &env, + &mut storage, + t, + None, + Some(bytes_or_bytearray), + ); // Check satisfiability against each upper clause before punting on the union-bearing // merged upper bound. The old size heuristic returned `CannotSimplify` here before // discovering that `int` cannot satisfy the second upper clause. assert!(matches!( - left.intersect(&db, &mut storage, right), + left.intersect(db, &env, &mut storage, right), IntersectionResult::Disjoint )); } @@ -8228,26 +8599,30 @@ mod tests { #[test] fn constraint_implications_are_cached() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); let mut storage = builder.storage.borrow_mut(); let t_int = ConstraintId::new( - &db, + db, + &env, &mut storage, t, Type::Never, - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), ); let t_bool = ConstraintId::new( - &db, + db, + &env, &mut storage, t, Type::Never, - KnownClass::Bool.to_instance(&db), + KnownClass::Bool.to_instance(db, &env), ); - assert!(storage.cached_constraint_implies(&db, t_bool, t_int)); - assert!(storage.cached_constraint_implies(&db, t_bool, t_int)); + assert!(storage.cached_constraint_implies(db, &env, t_bool, t_int)); + assert!(storage.cached_constraint_implies(db, &env, t_bool, t_int)); drop(storage); { @@ -8260,8 +8635,8 @@ mod tests { } let mut storage = builder.storage.borrow_mut(); - assert!(!storage.cached_constraint_implies(&db, t_int, t_bool)); - assert!(!storage.cached_constraint_implies(&db, t_int, t_bool)); + assert!(!storage.cached_constraint_implies(db, &env, t_int, t_bool)); + assert!(!storage.cached_constraint_implies(db, &env, t_int, t_bool)); drop(storage); let storage = builder.storage.borrow(); @@ -8275,11 +8650,13 @@ mod tests { #[test] fn trivial_satisfaction_only_recognizes_terminals() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let t_str = create_constraint(&db, &builder, t, KnownClass::Str); - let impossible = t_int.and(&db, &builder, || t_str); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let t_str = create_constraint(db, &builder, t, KnownClass::Str); + let impossible = t_int.and(db, &builder, || t_str); assert!(ConstraintSet::always(&builder).is_trivially_always_satisfied()); assert!(!ConstraintSet::always(&builder).is_trivially_never_satisfied()); @@ -8287,69 +8664,75 @@ mod tests { assert!(!ConstraintSet::never(&builder).is_trivially_always_satisfied()); assert!(!t_int.is_trivially_always_satisfied()); assert!(!t_int.is_trivially_never_satisfied()); - assert!(impossible.is_never_satisfied(&db)); + assert!(impossible.is_never_satisfied(db, &env)); assert!(!impossible.is_trivially_never_satisfied()); let t_bool_upper = ConstraintSet::constrain_typevar_upper_bound( - &db, + db, + &env, &builder, t, - KnownClass::Bool.to_instance(&db), + KnownClass::Bool.to_instance(db, &env), ); let t_int_upper = ConstraintSet::constrain_typevar_upper_bound( - &db, + db, + &env, &builder, t, - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), ); let tautology = t_bool_upper - .negate(&db, &builder) - .or(&db, &builder, || t_int_upper); + .negate(db, &builder) + .or(db, &builder, || t_int_upper); - assert!(tautology.is_always_satisfied(&db)); + assert!(tautology.is_always_satisfied(db, &env)); assert!(!tautology.is_trivially_always_satisfied()); } #[test] fn combinators_only_short_circuit_on_terminal_saturation() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let t_str = create_constraint(&db, &builder, t, KnownClass::Str); - let impossible = t_int.and(&db, &builder, || t_str); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let t_str = create_constraint(db, &builder, t, KnownClass::Str); + let impossible = t_int.and(db, &builder, || t_str); let t_bool_upper = ConstraintSet::constrain_typevar_upper_bound( - &db, + db, + &env, &builder, t, - KnownClass::Bool.to_instance(&db), + KnownClass::Bool.to_instance(db, &env), ); let t_int_upper = ConstraintSet::constrain_typevar_upper_bound( - &db, + db, + &env, &builder, t, - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), ); let tautology = t_bool_upper - .negate(&db, &builder) - .or(&db, &builder, || t_int_upper); + .negate(db, &builder) + .or(db, &builder, || t_int_upper); let forced = Cell::new(0); - ConstraintSet::never(&builder).and(&db, &builder, || { + ConstraintSet::never(&builder).and(db, &builder, || { forced.set(forced.get() + 1); t_int }); - ConstraintSet::always(&builder).or(&db, &builder, || { + ConstraintSet::always(&builder).or(db, &builder, || { forced.set(forced.get() + 1); t_int }); assert_eq!(forced.get(), 0); - impossible.and(&db, &builder, || { + impossible.and(db, &builder, || { forced.set(forced.get() + 1); t_int }); - tautology.or(&db, &builder, || { + tautology.or(db, &builder, || { forced.set(forced.get() + 1); t_int }); @@ -8358,7 +8741,7 @@ mod tests { let visited = Cell::new(0); [impossible, t_int] .into_iter() - .when_all(&db, &builder, |set| { + .when_all(db, &builder, |set| { visited.set(visited.get() + 1); set }); @@ -8367,7 +8750,7 @@ mod tests { visited.set(0); [tautology, t_int] .into_iter() - .when_any(&db, &builder, |set| { + .when_any(db, &builder, |set| { visited.set(visited.get() + 1); set }); @@ -8376,7 +8759,7 @@ mod tests { visited.set(0); [ConstraintSet::never(&builder), t_int] .into_iter() - .when_all(&db, &builder, |set| { + .when_all(db, &builder, |set| { visited.set(visited.get() + 1); set }); @@ -8385,7 +8768,7 @@ mod tests { visited.set(0); [ConstraintSet::always(&builder), t_int] .into_iter() - .when_any(&db, &builder, |set| { + .when_any(db, &builder, |set| { visited.set(visited.get() + 1); set }); @@ -8395,18 +8778,20 @@ mod tests { #[test] fn never_satisfied_results_are_cached() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let t_str = create_constraint(&db, &builder, t, KnownClass::Str); - let impossible = t_int.and(&db, &builder, || t_str); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let t_str = create_constraint(db, &builder, t, KnownClass::Str); + let impossible = t_int.and(db, &builder, || t_str); - assert!(!t_int.is_never_satisfied(&db)); - assert!(!t_int.is_never_satisfied(&db)); - assert!(impossible.is_never_satisfied(&db)); - assert!(impossible.is_never_satisfied(&db)); - assert!(ConstraintSet::never(&builder).is_never_satisfied(&db)); - assert!(!ConstraintSet::always(&builder).is_never_satisfied(&db)); + assert!(!t_int.is_never_satisfied(db, &env)); + assert!(!t_int.is_never_satisfied(db, &env)); + assert!(impossible.is_never_satisfied(db, &env)); + assert!(impossible.is_never_satisfied(db, &env)); + assert!(ConstraintSet::never(&builder).is_never_satisfied(db, &env)); + assert!(!ConstraintSet::always(&builder).is_never_satisfied(db, &env)); { let storage = builder.storage.borrow(); @@ -8418,10 +8803,10 @@ mod tests { assert_eq!(storage.never_satisfied_cache.len(), 2); } - let owned = create_compacted_owned_set(&db); + let owned = create_compacted_owned_set(db); owned.query(|builder, set| { - assert!(!set.is_never_satisfied(&db)); - assert!(!set.is_never_satisfied(&db)); + assert!(!set.is_never_satisfied(db, &env)); + assert!(!set.is_never_satisfied(db, &env)); let storage = builder.storage.borrow(); assert_eq!(storage.never_satisfied_cache.get(&set.node), Some(&false)); }); @@ -8430,19 +8815,21 @@ mod tests { #[test] fn never_satisfied_cache_is_shared_across_source_orders() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); - let first = t_int.and(&db, &builder, || u_str); - let second = u_str.and(&db, &builder, || t_int); + let first = t_int.and(db, &builder, || u_str); + let second = u_str.and(db, &builder, || t_int); assert_eq!(first.node, second.node); assert_ne!(first.source_order, second.source_order); - assert!(!first.is_never_satisfied(&db)); - assert!(!second.is_never_satisfied(&db)); + assert!(!first.is_never_satisfied(db, &env)); + assert!(!second.is_never_satisfied(db, &env)); let storage = builder.storage.borrow(); assert_eq!(storage.never_satisfied_cache.len(), 1); } @@ -8455,9 +8842,14 @@ mod tests { ); impl<'db> PermutedConstraint<'db> { - fn node(self, db: &'db dyn Db, storage: &mut ConstraintSetStorage<'db>) -> NodeId { + fn node( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + storage: &mut ConstraintSetStorage<'db>, + ) -> NodeId { let PermutedConstraint(typevar, lower, upper) = self; - Constraint::new_node_with_bounds(db, storage, typevar, lower, upper).0 + Constraint::new_node_with_bounds(db, env, storage, typevar, lower, upper).0 } } @@ -8471,12 +8863,13 @@ mod tests { /// that we get that specific result for each permutation. #[track_caller] fn check_solutions_for_constraint_orderings<'db>( - db: &'db dyn Db, + db: &'db TestDb, typevars: &[BoundTypeVarInstance<'db>], atoms: &[PermutedConstraint<'db>], build_bdd: impl Fn(&mut ConstraintSetStorage<'db>) -> NodeId, expected: impl IntoIterator, ) { + let env = db.program_environment(); let inferable = TypeVarSet::from_typevars(db, typevars.iter().copied()); let mut signatures = FxIndexSet::default(); @@ -8490,6 +8883,7 @@ mod tests { let PermutedConstraint(typevar, lower, upper) = atoms[index]; storage.intern_constraint( db, + &env, Constraint { typevar, bounds: ConstraintBounds::new(lower, upper), @@ -8502,6 +8896,7 @@ mod tests { let PermutedConstraint(typevar, lower, upper) = *atom; let constraint = storage.intern_constraint( db, + &env, Constraint { typevar, bounds: ConstraintBounds::new(lower, upper), @@ -8513,7 +8908,7 @@ mod tests { drop(storage); let set = ConstraintSet::from_node(&builder, node, source_order); - let solutions = set.solutions(db, &builder, inferable); + let solutions = set.solutions(db, &env, &builder, inferable); let mut merged = FxHashMap::default(); if let Solutions::Constrained(paths) = &solutions { for path in paths { @@ -8521,8 +8916,12 @@ mod tests { merged .entry(binding.bound_typevar) .and_modify(|existing| { - *existing = - UnionType::from_two_elements(db, *existing, binding.solution); + *existing = UnionType::from_two_elements( + db, + &env, + *existing, + binding.solution, + ); }) .or_insert(binding.solution); } @@ -8532,7 +8931,11 @@ mod tests { .iter() .filter_map(|typevar| { merged.get(typevar).map(|ty| { - format!("{}={}", typevar.identity(db).display(db), ty.display(db)) + format!( + "{}={}", + typevar.identity(db).display(db), + ty.display(db, &env) + ) }) }) .join(", "); @@ -8547,7 +8950,7 @@ mod tests { format!( "{}={}", binding.bound_typevar.identity(db).display(db), - binding.solution.display(db) + binding.solution.display(db, &env) ) }) .join(", ") @@ -8556,8 +8959,8 @@ mod tests { }; signatures.insert(format!( "never={} always={} merged=[{merged}] paths=[{paths}]", - set.is_never_satisfied(db), - set.is_always_satisfied(db), + set.is_never_satisfied(db, &env), + set.is_always_satisfied(db, &env), )); } @@ -8568,31 +8971,33 @@ mod tests { #[test] fn constraint_absorption_is_independent_of_constraint_order() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let str = KnownClass::Str.to_instance(&db); - let int = KnownClass::Int.to_instance(&db); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let str = KnownClass::Str.to_instance(db, &env); + let int = KnownClass::Int.to_instance(db, &env); let atoms = [ PermutedConstraint(t, Some(str), None), PermutedConstraint(t, Some(int), None), ]; check_solutions_for_constraint_orderings( - &db, + db, &[t], &atoms, |storage| { - let [str_t, int_t] = atoms.map(|atom| atom.node(&db, storage)); + let [str_t, int_t] = atoms.map(|atom| atom.node(db, &env, storage)); str_t.or(storage, int_t).and(storage, str_t) }, ["never=false always=false merged=[T=str] paths=[T=str]"], ); check_solutions_for_constraint_orderings( - &db, + db, &[t], &atoms, |storage| { - let [str_t, int_t] = atoms.map(|atom| atom.node(&db, storage)); + let [str_t, int_t] = atoms.map(|atom| atom.node(db, &env, storage)); str_t.or(storage, int_t) }, ["never=false always=false merged=[T=str | int] paths=[T=str; T=int]"], @@ -8602,11 +9007,13 @@ mod tests { #[test] fn compound_constraint_absorption_is_independent_of_constraint_order() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); - let str = KnownClass::Str.to_instance(&db); - let bytes = KnownClass::Bytes.to_instance(&db); - let int = KnownClass::Int.to_instance(&db); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let str = KnownClass::Str.to_instance(db, &env); + let bytes = KnownClass::Bytes.to_instance(db, &env); + let int = KnownClass::Int.to_instance(db, &env); let atoms = [ PermutedConstraint(t, Some(str), None), PermutedConstraint(u, Some(bytes), None), @@ -8614,11 +9021,11 @@ mod tests { ]; check_solutions_for_constraint_orderings( - &db, + db, &[t, u], &atoms, |storage| { - let [str_t, bytes_u, int_t] = atoms.map(|atom| atom.node(&db, storage)); + let [str_t, bytes_u, int_t] = atoms.map(|atom| atom.node(db, &env, storage)); let compound = str_t.and(storage, bytes_u); compound.or(storage, int_t).and(storage, compound) }, @@ -8629,12 +9036,14 @@ mod tests { #[test] fn compound_constraint_absorption_preserves_binding_source_order() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); - let x = create_typevar(&db, "X"); - let str = KnownClass::Str.to_instance(&db); - let bytes = KnownClass::Bytes.to_instance(&db); - let int = KnownClass::Int.to_instance(&db); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let x = create_typevar(db, "X"); + let str = KnownClass::Str.to_instance(db, &env); + let bytes = KnownClass::Bytes.to_instance(db, &env); + let int = KnownClass::Int.to_instance(db, &env); let atoms = [ PermutedConstraint(t, Some(str), None), PermutedConstraint(u, Some(bytes), None), @@ -8642,11 +9051,11 @@ mod tests { ]; check_solutions_for_constraint_orderings( - &db, + db, &[t, u, x], &atoms, |storage| { - let [str_t, bytes_u, int_x] = atoms.map(|atom| atom.node(&db, storage)); + let [str_t, bytes_u, int_x] = atoms.map(|atom| atom.node(db, &env, storage)); let early = int_x.and(storage, str_t).and(storage, bytes_u); let late = bytes_u.and(storage, str_t); early.or(storage, late) @@ -8658,20 +9067,22 @@ mod tests { #[test] fn constraint_partition_is_independent_of_constraint_order() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let str = KnownClass::Str.to_instance(&db); - let int = KnownClass::Int.to_instance(&db); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let str = KnownClass::Str.to_instance(db, &env); + let int = KnownClass::Int.to_instance(db, &env); let atoms = [ PermutedConstraint(t, Some(str), None), PermutedConstraint(t, Some(int), None), ]; check_solutions_for_constraint_orderings( - &db, + db, &[t], &atoms, |storage| { - let [str_t, int_t] = atoms.map(|atom| atom.node(&db, storage)); + let [str_t, int_t] = atoms.map(|atom| atom.node(db, &env, storage)); let true_path = int_t.and(storage, str_t); let false_path = int_t.negate(storage).and(storage, str_t); true_path.or(storage, false_path) @@ -8683,13 +9094,15 @@ mod tests { #[test] fn constraint_ordering_changes_nested_transitive_solutions() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); - let v = create_typevar(&db, "V"); - let int = KnownClass::Int.to_instance(&db); - let bytes = KnownClass::Bytes.to_instance(&db); - let list_u = KnownClass::List.to_specialized_instance(&db, &[Type::TypeVar(u)]); - let list_int = KnownClass::List.to_specialized_instance(&db, &[int]); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let v = create_typevar(db, "V"); + let int = KnownClass::Int.to_instance(db, &env); + let bytes = KnownClass::Bytes.to_instance(db, &env); + let list_u = KnownClass::List.to_specialized_instance(db, &env, &[Type::TypeVar(u)]); + let list_int = KnownClass::List.to_specialized_instance(db, &env, &[int]); let atoms = [ PermutedConstraint(t, None, Some(list_u)), PermutedConstraint(u, None, Some(int)), @@ -8698,12 +9111,12 @@ mod tests { ]; check_solutions_for_constraint_orderings( - &db, + db, &[t, u, v], &atoms, |storage| { let [t_list_u, u_int, list_int_t, bytes_v] = - atoms.map(|atom| atom.node(&db, storage)); + atoms.map(|atom| atom.node(db, &env, storage)); t_list_u .and(storage, u_int) .and(storage, list_int_t) @@ -8723,11 +9136,13 @@ mod tests { #[test] fn constraint_ordering_changes_negated_alternative_solutions() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); - let int = KnownClass::Int.to_instance(&db); - let str = KnownClass::Str.to_instance(&db); - let bytes = KnownClass::Bytes.to_instance(&db); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let int = KnownClass::Int.to_instance(db, &env); + let str = KnownClass::Str.to_instance(db, &env); + let bytes = KnownClass::Bytes.to_instance(db, &env); let atoms = [ PermutedConstraint(t, None, Some(int)), PermutedConstraint(t, None, Some(str)), @@ -8735,11 +9150,11 @@ mod tests { ]; check_solutions_for_constraint_orderings( - &db, + db, &[t, u], &atoms, |storage| { - let [t_int, t_str, bytes_u] = atoms.map(|atom| atom.node(&db, storage)); + let [t_int, t_str, bytes_u] = atoms.map(|atom| atom.node(db, &env, storage)); t_int .or(storage, t_str) .negate(storage) @@ -8758,10 +9173,12 @@ mod tests { #[test] fn constraint_ordering_changes_derived_upper_bound_display() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); - let int = KnownClass::Int.to_instance(&db); - let str = KnownClass::Str.to_instance(&db); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let int = KnownClass::Int.to_instance(db, &env); + let str = KnownClass::Str.to_instance(db, &env); let atoms = [ PermutedConstraint(t, None, Some(int)), PermutedConstraint(t, None, Some(str)), @@ -8770,11 +9187,11 @@ mod tests { ]; check_solutions_for_constraint_orderings( - &db, + db, &[t, u], &atoms, |storage| { - let [t_int, t_str, int_t, u_int] = atoms.map(|atom| atom.node(&db, storage)); + let [t_int, t_str, int_t, u_int] = atoms.map(|atom| atom.node(db, &env, storage)); t_int .or(storage, t_str) .and(storage, int_t) @@ -8791,33 +9208,35 @@ mod tests { #[track_caller] fn check_display_graph<'db, 'c>( - db: &'db dyn Db, + db: &'db TestDb, builder: &'c ConstraintSetBuilder<'db>, set: ConstraintSet<'db, 'c>, expected: &str, ) { + let env = db.program_environment(); let storage = builder.storage.borrow(); let expected = expected.trim_end(); - let actual = set.node.display_graph(db, &storage, &"").to_string(); + let actual = set.node.display_graph(db, &env, &storage, &"").to_string(); assert_eq!(expected, actual); } #[test] fn test_display_graph_output() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let constraints = ConstraintSetBuilder::new(); - let t_str = create_constraint(&db, &constraints, t, KnownClass::Str); - let t_bool = create_constraint(&db, &constraints, t, KnownClass::Bool); - let u_str = create_constraint(&db, &constraints, u, KnownClass::Str); - let u_bool = create_constraint(&db, &constraints, u, KnownClass::Bool); + let t_str = create_constraint(db, &constraints, t, KnownClass::Str); + let t_bool = create_constraint(db, &constraints, t, KnownClass::Bool); + let u_str = create_constraint(db, &constraints, u, KnownClass::Str); + let u_bool = create_constraint(db, &constraints, u, KnownClass::Bool); // Construct this in a different order than above to make the source_orders more // interesting. - let set = (u_str.or(&db, &constraints, || u_bool)) - .and(&db, &constraints, || t_str.or(&db, &constraints, || t_bool)); + let set = (u_str.or(db, &constraints, || u_bool)) + .and(db, &constraints, || t_str.or(db, &constraints, || t_bool)); check_display_graph( - &db, + db, &constraints, set, indoc! {r#" @@ -8865,18 +9284,19 @@ mod tests { #[test] fn tdd_union_creates_uncertain_branches() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); // Neither lhs nor rhs have uncertain branches (checked above). The operand with the // "lower" BDD variable (in this case, the lhs) is parked into a new uncertain branch in // the union result. - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let union = t_int.or(&db, &builder, || u_str); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let union = t_int.or(db, &builder, || u_str); check_display_graph( - &db, + db, &builder, union, indoc! {r#" @@ -8896,21 +9316,22 @@ mod tests { #[test] fn tdd_intersection_preserves_uncertain() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let t_bool = create_constraint(&db, &builder, t, KnownClass::Bool); - let u_int = create_constraint(&db, &builder, u, KnownClass::Int); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let t_bool = create_constraint(db, &builder, t, KnownClass::Bool); + let u_int = create_constraint(db, &builder, u, KnownClass::Int); // lhs and rhs both have uncertain branches (checked above). These uncertain branches are // carried through to the intersection result. - let lhs = t_int.or(&db, &builder, || u_str); - let rhs = t_bool.or(&db, &builder, || u_int); - let intersection = lhs.and(&db, &builder, || rhs); + let lhs = t_int.or(db, &builder, || u_str); + let rhs = t_bool.or(db, &builder, || u_int); + let intersection = lhs.and(db, &builder, || rhs); check_display_graph( - &db, + db, &builder, intersection, indoc! {r#" @@ -8935,15 +9356,16 @@ mod tests { #[test] fn tdd_negation_produces_flat_tdd() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let union = t_int.or(&db, &builder, || u_str); - let negated = union.negate(&db, &builder); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let union = t_int.or(db, &builder, || u_str); + let negated = union.negate(db, &builder); check_display_graph( - &db, + db, &builder, negated, indoc! {r#" @@ -8961,59 +9383,71 @@ mod tests { #[test] fn tdd_negation_correctness() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let tdd = t_int.or(&db, &builder, || u_str); - let negated = tdd.negate(&db, &builder); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let tdd = t_int.or(db, &builder, || u_str); + let negated = tdd.negate(db, &builder); // T ∧ ¬T == false - assert!(tdd.and(&db, &builder, || negated).is_never_satisfied(&db)); + assert!( + tdd.and(db, &builder, || negated) + .is_never_satisfied(db, &env) + ); // T ∨ ¬T == true - assert!(tdd.or(&db, &builder, || negated).is_always_satisfied(&db)); + assert!( + tdd.or(db, &builder, || negated) + .is_always_satisfied(db, &env) + ); } #[test] fn eager_and_lazy_negation_are_equivalent() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let t_bool = create_constraint(&db, &builder, t, KnownClass::Bool); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let u_int = create_constraint(&db, &builder, u, KnownClass::Int); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let t_bool = create_constraint(db, &builder, t, KnownClass::Bool); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let u_int = create_constraint(db, &builder, u, KnownClass::Int); - let lhs = t_int.or(&db, &builder, || u_str); - let rhs = t_bool.or(&db, &builder, || u_int); - let intersection = lhs.and(&db, &builder, || rhs); - let tautology = lhs.or(&db, &builder, || lhs.negate(&db, &builder)); + let lhs = t_int.or(db, &builder, || u_str); + let rhs = t_bool.or(db, &builder, || u_int); + let intersection = lhs.and(db, &builder, || rhs); + let tautology = lhs.or(db, &builder, || lhs.negate(db, &builder)); let t_bool_upper = ConstraintSet::constrain_typevar_upper_bound( - &db, + db, + &env, &builder, t, - KnownClass::Bool.to_instance(&db), + KnownClass::Bool.to_instance(db, &env), ); let t_int_upper = ConstraintSet::constrain_typevar_upper_bound( - &db, + db, + &env, &builder, t, - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), ); let implication = t_bool_upper - .negate(&db, &builder) - .or(&db, &builder, || t_int_upper); + .negate(db, &builder) + .or(db, &builder, || t_int_upper); for set in [lhs, rhs, intersection, tautology, implication] { assert_eq!( - set.is_always_satisfied(&db), - set.negate(&db, &builder).is_never_satisfied(&db) + set.is_always_satisfied(db, &env), + set.negate(db, &builder).is_never_satisfied(db, &env) ); } } @@ -9125,15 +9559,16 @@ mod tests { #[test] fn path_assignments_follow_constraint_source_order() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); // Construct the set in the opposite order from constraint creation. This ensures the // initializer follows the sidecar rather than either TDD traversal or constraint IDs. - let set = u_str.and(&db, &builder, || t_int); + let set = u_str.and(db, &builder, || t_int); let path = path_assignments_for(&builder, set.node, set.source_order); let storage = builder.storage.borrow(); let expected = @@ -9146,37 +9581,42 @@ mod tests { #[test] fn path_fold_reconstructs_constraint_sets() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); - let v = create_typevar(&db, "V"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + let v = create_typevar(db, "V"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let t_str = create_constraint(&db, &builder, t, KnownClass::Str); - let u_int = create_constraint(&db, &builder, u, KnownClass::Int); - let v_bytes = create_constraint(&db, &builder, v, KnownClass::Bytes); - let union = t_int.or(&db, &builder, || u_int); - let intersection = union.and(&db, &builder, || t_str.or(&db, &builder, || v_bytes)); - let contradiction = t_int.and(&db, &builder, || t_str); - let tautology = union.or(&db, &builder, || union.negate(&db, &builder)); - - let t_u = ConstraintSet::constrain_typevar_upper_bound(&db, &builder, t, Type::TypeVar(u)); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let t_str = create_constraint(db, &builder, t, KnownClass::Str); + let u_int = create_constraint(db, &builder, u, KnownClass::Int); + let v_bytes = create_constraint(db, &builder, v, KnownClass::Bytes); + let union = t_int.or(db, &builder, || u_int); + let intersection = union.and(db, &builder, || t_str.or(db, &builder, || v_bytes)); + let contradiction = t_int.and(db, &builder, || t_str); + let tautology = union.or(db, &builder, || union.negate(db, &builder)); + + let t_u = + ConstraintSet::constrain_typevar_upper_bound(db, &env, &builder, t, Type::TypeVar(u)); let u_int_upper = ConstraintSet::constrain_typevar_upper_bound( - &db, + db, + &env, &builder, u, - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), ); let int_t = ConstraintSet::constrain_typevar_lower_bound( - &db, + db, + &env, &builder, t, - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), ); let transitive = t_u - .and(&db, &builder, || u_int_upper) - .and(&db, &builder, || int_t) - .or(&db, &builder, || v_bytes); + .and(db, &builder, || u_int_upper) + .and(db, &builder, || int_t) + .or(db, &builder, || v_bytes); for set in [ ConstraintSet::always(&builder), @@ -9191,7 +9631,7 @@ mod tests { let mut fold = ReconstructPathFold { break_at: None }; let mut storage = builder.storage.borrow_mut(); let ControlFlow::Continue((reconstructed, reconstructed_source_order)) = - path.visit(&db, &mut storage, set.node, &mut fold) + path.visit(db, &env, &mut storage, set.node, &mut fold) else { panic!("reconstruction unexpectedly aborted"); }; @@ -9199,8 +9639,8 @@ mod tests { let reconstructed = ConstraintSet::from_node(&builder, reconstructed, reconstructed_source_order); assert!( - set.iff(&db, &builder, reconstructed) - .is_always_satisfied(&db) + set.iff(db, &builder, reconstructed) + .is_always_satisfied(db, &env) ); } } @@ -9208,15 +9648,15 @@ mod tests { #[test] fn path_fold_break_restores_path_assignments() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let t_str = create_constraint(&db, &builder, t, KnownClass::Str); - let u_int = create_constraint(&db, &builder, u, KnownClass::Int); - let set = t_int - .and(&db, &builder, || t_str) - .or(&db, &builder, || u_int); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let t_str = create_constraint(db, &builder, t, KnownClass::Str); + let u_int = create_constraint(db, &builder, u, KnownClass::Int); + let set = t_int.and(db, &builder, || t_str).or(db, &builder, || u_int); for break_at in [ PathFoldBreak::Satisfied, @@ -9230,13 +9670,13 @@ mod tests { }; let mut storage = builder.storage.borrow_mut(); assert_eq!( - path.visit(&db, &mut storage, set.node, &mut aborting_fold), + path.visit(db, &env, &mut storage, set.node, &mut aborting_fold), ControlFlow::Break(break_at) ); let mut completing_fold = ReconstructPathFold { break_at: None }; let ControlFlow::Continue((reconstructed, reconstructed_source_order)) = - path.visit(&db, &mut storage, set.node, &mut completing_fold) + path.visit(db, &env, &mut storage, set.node, &mut completing_fold) else { panic!("reconstruction unexpectedly aborted after {break_at:?}"); }; @@ -9244,8 +9684,8 @@ mod tests { let reconstructed = ConstraintSet::from_node(&builder, reconstructed, reconstructed_source_order); assert!( - set.iff(&db, &builder, reconstructed) - .is_always_satisfied(&db) + set.iff(db, &builder, reconstructed) + .is_always_satisfied(db, &env) ); } } @@ -9255,53 +9695,58 @@ mod tests { #[test] fn tdd_double_negation() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let tdd = t_int.or(&db, &builder, || u_str); - let negated = tdd.negate(&db, &builder); - let double_negated = negated.negate(&db, &builder); - let equivalent = tdd.iff(&db, &builder, double_negated); - assert!(equivalent.is_always_satisfied(&db)); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let tdd = t_int.or(db, &builder, || u_str); + let negated = tdd.negate(db, &builder); + let double_negated = negated.negate(db, &builder); + let equivalent = tdd.iff(db, &builder, double_negated); + assert!(equivalent.is_always_satisfied(db, &env)); } /// `iff(T, T)` is always satisfied for TDDs with uncertain branches. #[test] fn tdd_iff_self() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let tdd = t_int.or(&db, &builder, || u_str); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let tdd = t_int.or(db, &builder, || u_str); // iff(T, T) == true - assert!(tdd.iff(&db, &builder, tdd).is_always_satisfied(&db)); + assert!(tdd.iff(db, &builder, tdd).is_always_satisfied(db, &env)); // iff(T, ¬T) == false - let negated = tdd.negate(&db, &builder); - assert!(tdd.iff(&db, &builder, negated).is_never_satisfied(&db)); + let negated = tdd.negate(db, &builder); + assert!(tdd.iff(db, &builder, negated).is_never_satisfied(db, &env)); } #[test] fn constraint_set_source_order_combination_is_idempotent() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let builder = ConstraintSetBuilder::new(); - let t_int = create_constraint(&db, &builder, t, KnownClass::Int); - let u_str = create_constraint(&db, &builder, u, KnownClass::Str); - let combined = t_int.and(&db, &builder, || u_str); + let t_int = create_constraint(db, &builder, t, KnownClass::Int); + let u_str = create_constraint(db, &builder, u, KnownClass::Str); + let combined = t_int.and(db, &builder, || u_str); for original in [t_int, combined] { let storage = builder.storage.borrow(); let original_source_order_count = storage.source_orders.len(); drop(storage); - let intersection = original.and(&db, &builder, || original); - let union = original.or(&db, &builder, || original); + let intersection = original.and(db, &builder, || original); + let union = original.or(db, &builder, || original); assert_eq!(intersection.node, original.node); assert_eq!(intersection.source_order, original.source_order); @@ -9312,7 +9757,7 @@ mod tests { } } - fn create_compacted_owned_set(db: &dyn Db) -> OwnedConstraintSet<'_> { + fn create_compacted_owned_set(db: &TestDb) -> OwnedConstraintSet<'_> { let t = create_typevar(db, "T"); let u = create_typevar(db, "U"); let v = create_typevar(db, "V"); @@ -9352,20 +9797,21 @@ mod tests { #[test] fn owned_constraint_set_source_order_ignores_construction_history() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); let build = |include_redundant_combination| { ConstraintSetBuilder::new().into_owned(|builder| { - let t_int = create_constraint(&db, builder, t, KnownClass::Int); - let u_str = create_constraint(&db, builder, u, KnownClass::Str); - let combined = t_int.and(&db, builder, || u_str); + let t_int = create_constraint(db, builder, t, KnownClass::Int); + let u_str = create_constraint(db, builder, u, KnownClass::Str); + let combined = t_int.and(db, builder, || u_str); if include_redundant_combination { // Repeating one constraint leaves the BDD and first-occurrence source order // unchanged, but creates a distinct, reachable source-order tree. Both trees // must compact to the same owned set. - let redundant = combined.and(&db, builder, || t_int); + let redundant = combined.and(db, builder, || t_int); assert_eq!(redundant.node, combined.node); assert_ne!(redundant.source_order, combined.source_order); redundant @@ -9407,7 +9853,9 @@ mod tests { #[test] fn owned_constraint_set_mutating_query_allocates_after_overlay() { let db = setup_db(); - let owned = create_compacted_owned_set(&db); + let db = &db; + let env = db.program_environment(); + let owned = create_compacted_owned_set(db); owned.query(|builder, set| { let (node_split, constraint_split, typevar_split, source_order_split) = { @@ -9432,8 +9880,8 @@ mod tests { ); drop(storage); - let w = create_typevar(&db, "W"); - let w_str = create_constraint(&db, builder, w, KnownClass::Str); + let w = create_typevar(db, "W"); + let w_str = create_constraint(db, builder, w, KnownClass::Str); let mut storage = builder.storage.borrow_mut(); let new_constraint = w_str .node @@ -9442,7 +9890,7 @@ mod tests { assert!(w_str.node.index() >= node_split); assert!(new_constraint.index() >= constraint_split); - assert!(storage.typevar_id(&db, w).index() >= typevar_split); + assert!(storage.typevar_id(db, w).index() >= typevar_split); drop(storage); assert!( w_str @@ -9450,8 +9898,8 @@ mod tests { .is_some_and(|source_order| source_order.index() >= source_order_split) ); - let combined = set.and(&db, builder, || w_str); - assert!(!combined.is_never_satisfied(&db)); + let combined = set.and(db, builder, || w_str); + assert!(!combined.is_never_satisfied(db, &env)); let storage = builder.storage.borrow(); assert!(!storage.nodes.is_empty()); @@ -9463,12 +9911,14 @@ mod tests { #[test] fn owned_constraint_set_load_reads_compacted_storage() { let db = setup_db(); - let owned = create_compacted_owned_set(&db); + let db = &db; + let env = db.program_environment(); + let owned = create_compacted_owned_set(db); let builder = ConstraintSetBuilder::new(); - let loaded = builder.load(&db, &owned); + let loaded = builder.load(db, &env, &owned); check_display_graph( - &db, + db, &builder, loaded, indoc! {r#" @@ -9483,16 +9933,18 @@ mod tests { #[test] fn terminal_owned_constraint_set_discards_storage() { let db = setup_db(); - let t = create_typevar(&db, "T"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); let owned = ConstraintSetBuilder::new().into_owned(|builder| { - let _unused = create_constraint(&db, builder, t, KnownClass::Int); + let _unused = create_constraint(db, builder, t, KnownClass::Int); ConstraintSet::always(builder) }); assert!(owned.inner.is_none()); owned.query(|builder, set| { - assert!(set.is_always_satisfied(&db)); + assert!(set.is_always_satisfied(db, &env)); let storage = builder.storage.borrow(); assert!(storage.compacted.is_none()); assert!(storage.nodes.is_empty()); @@ -9501,8 +9953,8 @@ mod tests { }); let builder = ConstraintSetBuilder::new(); - let loaded = builder.load(&db, &owned); - assert!(loaded.is_always_satisfied(&db)); + let loaded = builder.load(db, &env, &owned); + assert!(loaded.is_always_satisfied(db, &env)); } /// Round-trip through `OwnedConstraintSet`: build a TDD with uncertain branches, convert to @@ -9510,17 +9962,19 @@ mod tests { #[test] fn tdd_owned_round_trip() { let db = setup_db(); - let t = create_typevar(&db, "T"); - let u = create_typevar(&db, "U"); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); // Build a TDD with uncertain branches and convert to owned let builder = ConstraintSetBuilder::new(); let owned = builder.into_owned(|builder| { - let t_int = create_constraint(&db, builder, t, KnownClass::Int); - let u_str = create_constraint(&db, builder, u, KnownClass::Str); - let result = t_int.or(&db, builder, || u_str); + let t_int = create_constraint(db, builder, t, KnownClass::Int); + let u_str = create_constraint(db, builder, u, KnownClass::Str); + let result = t_int.or(db, builder, || u_str); check_display_graph( - &db, + db, builder, result, indoc! {r#" @@ -9538,9 +9992,9 @@ mod tests { // Load into a new builder let builder = ConstraintSetBuilder::new(); - let loaded = builder.load(&db, &owned); + let loaded = builder.load(db, &env, &owned); check_display_graph( - &db, + db, &builder, loaded, indoc! {r#" diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 112b189758..4c9af02a68 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -1,13 +1,16 @@ -use std::fmt; +use std::{cell::Cell, fmt, hint::cold_path, marker::PhantomData}; use drop_bomb::DebugDropBomb; +use ruff_db::PythonFile; use ruff_db::diagnostic::DiagnosticTag; use ruff_db::parsed::ParsedModuleRef; use ruff_db::{ diagnostic::{Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, Severity, Span}, files::File, }; +use ruff_python_ast::PythonVersion; use ruff_text_size::{Ranged, TextRange}; +use salsa::plumbing::{AsId, FromId, Id}; use super::{Type, TypeCheckDiagnostics, infer_definition_types}; @@ -18,13 +21,101 @@ use crate::types::diagnostic::{INVALID_TYPE_FORM, UNBOUND_TYPE_VARIABLE}; use crate::types::function::FunctionDecorators; use crate::types::infer::InferenceFlags; use crate::{ - Db, + Db, Program, lint::{LintId, LintMetadata}, suppression::suppressions, }; +use ty_python_core::definition::Definition; use ty_python_core::scope::ScopeId; use ty_python_core::semantic_index; +/// The lazily resolved program used by a semantic operation. +#[derive(Clone)] +pub struct ProgramEnvironment<'db> { + environment: Cell, + lifetime: PhantomData<&'db ()>, +} + +impl<'db> ProgramEnvironment<'db> { + /// Creates an environment that lazily obtains its Python version from `file`. + pub fn from_file(file: PythonFile<'db>) -> Self { + Self { + environment: Cell::new(ProgramSource::File(file.as_id())), + lifetime: PhantomData, + } + } + + /// Creates an environment that lazily obtains its program from `definition`. + pub fn from_definition(definition: Definition<'db>) -> Self { + Self { + environment: Cell::new(ProgramSource::Definition(definition.as_id())), + lifetime: PhantomData, + } + } + + /// Creates an environment that lazily obtains its program from `scope`. + pub fn from_scope(scope: ScopeId<'db>) -> Self { + Self { + environment: Cell::new(ProgramSource::Scope(scope.as_id())), + lifetime: PhantomData, + } + } + + /// Creates an environment with an already-established program. + pub const fn from_program(program: Program) -> Self { + Self { + environment: Cell::new(ProgramSource::Program(program)), + lifetime: PhantomData, + } + } + + /// Returns the program used by this operation. + pub fn program(&self, db: &'db dyn Db) -> Program { + let program = match self.environment.get() { + ProgramSource::Program(program) => return program, + ProgramSource::File(file) => { + cold_path(); + // The source handle and database share `'db`; re-wrapping the stored ingredient + // ID immediately before the read restores the original database lifetime. + PythonFile::from_id(file).python_version(db) + } + ProgramSource::Definition(definition) => { + cold_path(); + // The source handle and database share `'db`; re-wrapping the stored ingredient + // ID immediately before the read restores the original database lifetime. + Definition::from_id(definition).program(db) + } + ProgramSource::Scope(scope) => { + cold_path(); + // The source handle and database share `'db`; re-wrapping the stored ingredient + // ID immediately before the read restores the original database lifetime. + ScopeId::from_id(scope).program(db) + } + }; + + self.environment.set(ProgramSource::Program(program)); + program + } + + /// Returns the Python version used by this operation. + #[inline] + pub fn python_version(&self, db: &'db dyn Db) -> PythonVersion { + self.program(db) + } +} + +#[derive(Clone, Copy)] +enum ProgramSource { + Program(Program), + // Salsa interned handles are thin `Id` wrappers, so converting between `PythonFile` and `Id` + // is an inlined representation change with no database lookup. Keeping the lifetime-bearing + // `PythonFile` out of the `Cell` preserves covariance in `'db`; replacing this variant after + // the first read avoids repeated Salsa ingredient reads in hot, recursive type operations. + File(Id), + Definition(Id), + Scope(Id), +} + /// Context for inferring the types of a single file. /// /// One context exists for at least for every inferred region but it's @@ -39,8 +130,10 @@ use ty_python_core::semantic_index; /// on the current inference result. pub(crate) struct InferContext<'db, 'ast> { db: &'db dyn Db, + program_environment: &'ast ProgramEnvironment<'db>, scope: ScopeId<'db>, file: File, + python_file: PythonFile<'db>, module: &'ast ParsedModuleRef, diagnostics: std::cell::RefCell, diagnostics_suppressed: bool, @@ -50,12 +143,24 @@ pub(crate) struct InferContext<'db, 'ast> { } impl<'db, 'ast> InferContext<'db, 'ast> { - pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>, module: &'ast ParsedModuleRef) -> Self { + pub(crate) fn new( + db: &'db dyn Db, + program_environment: &'ast ProgramEnvironment<'db>, + scope: ScopeId<'db>, + file: File, + python_file: PythonFile<'db>, + module: &'ast ParsedModuleRef, + ) -> Self { + debug_assert_eq!(scope.python_file(db), python_file); + debug_assert_eq!(python_file.file(db), file); + Self { db, + program_environment, scope, module, - file: scope.file(db), + file, + python_file, diagnostics: std::cell::RefCell::new(TypeCheckDiagnostics::default()), diagnostics_suppressed: false, inference_flags: InferenceFlags::empty(), @@ -70,6 +175,15 @@ impl<'db, 'ast> InferContext<'db, 'ast> { self.file } + pub(crate) fn python_file(&self) -> PythonFile<'db> { + self.python_file + } + + #[inline] + pub(crate) fn program_environment(&self) -> &'ast ProgramEnvironment<'db> { + self.program_environment + } + /// The module for which the types are inferred. pub(crate) fn module(&self) -> &'ast ParsedModuleRef { self.module @@ -97,6 +211,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { Annotation::secondary(self.span(ranged)) } + #[inline] pub(crate) fn db(&self) -> &'db dyn Db { self.db } @@ -187,9 +302,9 @@ impl<'db, 'ast> InferContext<'db, 'ast> { // Accessing the semantic index here is fine because // the index belongs to the same file as for which we emit the diagnostic. - let index = semantic_index(self.db, self.file); + let index = semantic_index(self.db(), self.python_file); - let scope_id = self.scope.file_scope_id(self.db); + let scope_id = self.scope.file_scope_id(self.db()); // Inspect all ancestor function scopes by walking bottom up and check // if any is decorated with `@no_type_check`. We use the undecorated type @@ -200,12 +315,12 @@ impl<'db, 'ast> InferContext<'db, 'ast> { .ancestor_scopes(scope_id) .filter_map(|(_, scope)| scope.node().as_function()) .filter_map(|node| { - infer_definition_types(self.db, index.expect_single_definition(node)) + infer_definition_types(self.db(), index.expect_single_definition(node)) .undecorated_type() .and_then(Type::as_function_literal) }) .any(|function_ty| { - function_ty.has_known_decorator(self.db, FunctionDecorators::NO_TYPE_CHECK) + function_ty.has_known_decorator(self.db(), FunctionDecorators::NO_TYPE_CHECK) }) } @@ -214,9 +329,10 @@ impl<'db, 'ast> InferContext<'db, 'ast> { /// This checks both whether the scope itself is reachable and whether the /// specific statement or expression containing this range is reachable. fn is_range_reachable(&self, range: TextRange) -> bool { - let index = semantic_index(self.db, self.file); - let scope_id = self.scope.file_scope_id(self.db); - is_range_reachable(self.db, index, scope_id, range) + let db = self.db; + let index = semantic_index(self.db(), self.python_file); + let scope_id = self.scope.file_scope_id(self.db()); + is_range_reachable(db, index, scope_id, range) } /// Are we currently inferring types in a stub file? @@ -246,10 +362,14 @@ impl<'db, 'ast> InferContext<'db, 'ast> { impl fmt::Debug for InferContext<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("TyContext") + f.debug_struct("InferContext") + .field("db", &"") + .field("scope", &self.scope) .field("file", &self.file) + .field("python_file", &self.python_file) .field("diagnostics", &self.diagnostics) - .field("defused", &self.bomb) + .field("diagnostics_suppressed", &self.diagnostics_suppressed) + .field("inference_flags", &self.inference_flags) .finish() } } @@ -461,12 +581,12 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { // returns a rule selector for a given file that respects the package's settings, // any global pragma comments in the file, and any per-file-ignores. - if !ctx.db.should_check_file(ctx.file) { + if !ctx.db().should_check_file(ctx.file) { return None; } // Skip over diagnostics if the rule // is disabled. - let (severity, source) = ctx.db.rule_selection(ctx.file).get(lint)?; + let (severity, source) = ctx.db().rule_selection(ctx.file).get(lint)?; // If we're not in type checking mode, // we can bail now. if ctx.is_in_no_type_check() { @@ -497,7 +617,7 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> { let (severity, source) = Self::severity_and_source(ctx, lint_id)?; - let suppressions = suppressions(ctx.db(), ctx.file()); + let suppressions = suppressions(ctx.db(), ctx.python_file()); if let Some(suppression) = suppressions.find_suppression(range, lint_id) { ctx.diagnostics.borrow_mut().mark_used(suppression.id()); return None; @@ -590,7 +710,7 @@ impl<'db, 'ctx> DiagnosticGuardBuilder<'db, 'ctx> { return None; } - if !ctx.db.should_check_file(ctx.file) { + if !ctx.db().should_check_file(ctx.file) { return None; } Some(DiagnosticGuardBuilder { ctx, id, severity }) diff --git a/crates/ty_python_semantic/src/types/context_manager.rs b/crates/ty_python_semantic/src/types/context_manager.rs index abfaec2dec..c4b9071837 100644 --- a/crates/ty_python_semantic/src/types/context_manager.rs +++ b/crates/ty_python_semantic/src/types/context_manager.rs @@ -1,5 +1,7 @@ +use crate::Db; +use crate::ProgramEnvironment; use crate::{ - Db, FxOrderSet, + FxOrderSet, types::{ Bindings, CallArguments, CallDunderError, Type, TypeContext, call::CallErrorKind, context::InferContext, diagnostic::INVALID_CONTEXT_MANAGER, @@ -13,18 +15,18 @@ impl<'db> Type<'db> { /// /// This method should only be used outside of type checking because it omits any errors. /// For type checking, use [`try_enter_with_mode`](Self::try_enter_with_mode) instead. - pub(super) fn enter(self, db: &'db dyn Db) -> Type<'db> { - self.try_enter_with_mode(db, EvaluationMode::Sync) - .unwrap_or_else(|err| err.fallback_enter_type(db)) + pub(super) fn enter(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.try_enter_with_mode(db, env, EvaluationMode::Sync) + .unwrap_or_else(|err| err.fallback_enter_type(db, env)) } /// Returns the type bound from a context manager with type `self`. /// /// This method should only be used outside of type checking because it omits any errors. /// For type checking, use [`try_enter_with_mode`](Self::try_enter_with_mode) instead. - pub(super) fn aenter(self, db: &'db dyn Db) -> Type<'db> { - self.try_enter_with_mode(db, EvaluationMode::Async) - .unwrap_or_else(|err| err.fallback_enter_type(db)) + pub(super) fn aenter(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.try_enter_with_mode(db, env, EvaluationMode::Async) + .unwrap_or_else(|err| err.fallback_enter_type(db, env)) } /// Given the type of an object that is used as a context manager (i.e. in a `with` statement), @@ -38,6 +40,7 @@ impl<'db> Type<'db> { pub(super) fn try_enter_with_mode( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mode: EvaluationMode, ) -> Result, ContextManagerError<'db>> { let (enter_method, exit_method) = match mode { @@ -47,22 +50,28 @@ impl<'db> Type<'db> { let enter = self.try_call_dunder( db, + env, enter_method, CallArguments::none(), TypeContext::default(), ); let exit = self.try_call_dunder( db, + env, exit_method, - CallArguments::positional([Type::none(db), Type::none(db), Type::none(db)]), + CallArguments::positional([ + Type::none(db, env), + Type::none(db, env), + Type::none(db, env), + ]), TypeContext::default(), ); let awaited_enter_type = if mode.is_async() { let return_type = |call: &Result, CallDunderError<'db>>| match call { - Ok(bindings) => Some(bindings.return_type(db)), + Ok(bindings) => Some(bindings.return_type(db, env)), Err(CallDunderError::PossiblyUnbound { bindings, .. }) => { - Some(bindings.return_type(db)) + Some(bindings.return_type(db, env)) } Err(CallDunderError::MethodNotAvailable | CallDunderError::CallError(..)) => None, }; @@ -70,9 +79,9 @@ impl<'db> Type<'db> { let enter_return_type = return_type(&enter); let exit_return_type = return_type(&exit); let awaited_enter_type = - enter_return_type.and_then(|return_type| return_type.try_await(db).ok()); + enter_return_type.and_then(|return_type| return_type.try_await(db, env).ok()); let awaited_exit_type = - exit_return_type.and_then(|return_type| return_type.try_await(db).ok()); + exit_return_type.and_then(|return_type| return_type.try_await(db, env).ok()); let non_awaitable_enter = enter_return_type.filter(|_| awaited_enter_type.is_none()); let non_awaitable_exit = exit_return_type.filter(|_| awaited_exit_type.is_none()); @@ -95,7 +104,7 @@ impl<'db> Type<'db> { // TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`). match (enter, exit) { (Ok(enter), Ok(_)) => { - let return_type = enter.return_type(db); + let return_type = enter.return_type(db, env); Ok(if mode.is_async() { awaited_enter_type.unwrap_or(Type::unknown()) } else { @@ -103,7 +112,7 @@ impl<'db> Type<'db> { }) } (Ok(enter), Err(exit_error)) => { - let return_type = enter.return_type(db); + let return_type = enter.return_type(db, env); Err(ContextManagerError::Exit { enter_return_type: if mode.is_async() { awaited_enter_type.unwrap_or(Type::unknown()) @@ -193,13 +202,17 @@ impl<'db> NonAwaitableMethods<'db> { } impl<'db> ContextManagerError<'db> { - pub(super) fn fallback_enter_type(&self, db: &'db dyn Db) -> Type<'db> { - self.enter_type(db).unwrap_or(Type::unknown()) + pub(super) fn fallback_enter_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.enter_type(db, env).unwrap_or(Type::unknown()) } /// Returns the `__enter__` or `__aenter__` return type if it is known, /// or `None` if the type never has a callable `__enter__` or `__aenter__` attribute - fn enter_type(&self, db: &'db dyn Db) -> Option> { + fn enter_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { match self { Self::Exit { enter_return_type, @@ -216,15 +229,15 @@ impl<'db> ContextManagerError<'db> { mode, } => match enter_error { CallDunderError::PossiblyUnbound { bindings, .. } => { - let return_type = bindings.return_type(db); + let return_type = bindings.return_type(db, env); Some(if mode.is_async() { - return_type.try_await(db).unwrap_or(Type::unknown()) + return_type.try_await(db, env).unwrap_or(Type::unknown()) } else { return_type }) } CallDunderError::CallError(CallErrorKind::NotCallable, _, _) => None, - CallDunderError::CallError(_, bindings, _) => Some(bindings.return_type(db)), + CallDunderError::CallError(_, bindings, _) => Some(bindings.return_type(db, env)), CallDunderError::MethodNotAvailable => None, }, } @@ -245,6 +258,7 @@ impl<'db> ContextManagerError<'db> { _ => FxOrderSet::default(), } } + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_CONTEXT_MANAGER, context_expression_node) else { @@ -301,7 +315,7 @@ impl<'db> ContextManagerError<'db> { } }; - let db = context.db(); + let env = context.program_environment(); let formatted_errors = match self { Self::Exit { @@ -373,7 +387,7 @@ impl<'db> ContextManagerError<'db> { let mut diag = builder.into_diagnostic(format_args!( "Object of type `{}` cannot be used with `{}` because {}", - context_expression_type.display(db), + context_expression_type.display(db, env), with_kw, formatted_errors, )); @@ -384,7 +398,7 @@ impl<'db> ContextManagerError<'db> { for ty in &exit_unbound_on { diag.info(format_args!( "`{}` does not implement `{exit_method}`", - ty.display(db) + ty.display(db, env) )); } } @@ -393,7 +407,7 @@ impl<'db> ContextManagerError<'db> { for ty in &enter_unbound_on { diag.info(format_args!( "`{}` does not implement `{enter_method}`", - ty.display(db) + ty.display(db, env) )); } } @@ -409,12 +423,12 @@ impl<'db> ContextManagerError<'db> { if exit_unbound_on.contains(ty) { diag.info(format_args!( "`{}` does not implement `{enter_method}` or `{exit_method}`", - ty.display(db) + ty.display(db, env) )); } else { diag.info(format_args!( "`{}` does not implement `{enter_method}`", - ty.display(db) + ty.display(db, env) )); } } @@ -423,7 +437,7 @@ impl<'db> ContextManagerError<'db> { if !enter_unbound_on.contains(ty) { diag.info(format_args!( "`{}` does not implement `{exit_method}`", - ty.display(db) + ty.display(db, env) )); } } @@ -445,12 +459,12 @@ impl<'db> ContextManagerError<'db> { if exit_unbound_on.contains(ty) { diag.info(format_args!( "`{}` does not implement `{enter_method}` or `{exit_method}`", - ty.display(db) + ty.display(db, env) )); } else { diag.info(format_args!( "`{}` does not implement `{enter_method}`", - ty.display(db) + ty.display(db, env) )); } } @@ -459,7 +473,7 @@ impl<'db> ContextManagerError<'db> { if !enter_unbound_on.contains(ty) { diag.info(format_args!( "`{}` does not implement `{exit_method}`", - ty.display(db) + ty.display(db, env) )); } } @@ -469,7 +483,7 @@ impl<'db> ContextManagerError<'db> { { diag.info(format_args!( "`{method}` returns `{}`, which is not awaitable", - return_type.display(db) + return_type.display(db, env) )); } if non_awaitable.is_both() { @@ -492,12 +506,14 @@ impl<'db> ContextManagerError<'db> { let alt_enter = context_expression_type.try_call_dunder( db, + env, alt_enter_method, CallArguments::none(), TypeContext::default(), ); let alt_exit = context_expression_type.try_call_dunder( db, + env, alt_exit_method, CallArguments::positional([Type::unknown(), Type::unknown(), Type::unknown()]), TypeContext::default(), @@ -508,7 +524,7 @@ impl<'db> ContextManagerError<'db> { { diag.info(format_args!( "Objects of type `{}` can be used as {} context managers", - context_expression_type.display(db), + context_expression_type.display(db, env), alt_mode )); diag.info(format!("Consider using `{alt_with_kw}` here")); diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs index 60b8d4dd0e..2cd9409587 100644 --- a/crates/ty_python_semantic/src/types/cyclic.rs +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -31,11 +31,11 @@ use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::SmallVec; use ty_python_core::definition::Definition; -use crate::Db; use crate::types::function::FunctionLiteral; use crate::types::generics::Specialization; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ClassType, ProtocolInstanceType, Type, TypeAliasType, TypedDictType}; +use crate::{Db, ProgramEnvironment}; /// The type identity used for recursive checks/transformations. #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] @@ -106,6 +106,7 @@ impl<'db> Type<'db> { } struct DefinitionReferenceVisitor<'db> { + env: ProgramEnvironment<'db>, target: Definition<'db>, active_definitions: ActiveRecursionDetector>, visited_types: TypeCollector<'db>, @@ -122,6 +123,7 @@ impl<'db> DefinitionReferenceVisitor<'db> { fn new(target: Definition<'db>) -> Self { Self { + env: ProgramEnvironment::from_definition(target), target, active_definitions: ActiveRecursionDetector::default(), visited_types: TypeCollector::default(), @@ -158,7 +160,9 @@ impl<'db> DefinitionReferenceVisitor<'db> { fn visit_definition_body(&self, db: &'db dyn Db, ty: Type<'db>) { match ty { Type::TypeAlias(alias) => self.visit_type_alias_type(db, alias), - Type::ProtocolInstance(protocol) => self.visit_protocol_instance_type(db, protocol), + Type::ProtocolInstance(protocol) => { + self.visit_protocol_instance_type(db, protocol); + } Type::TypedDict(typed_dict) => self.visit_typed_dict_type(db, typed_dict), _ => {} } @@ -166,6 +170,10 @@ impl<'db> DefinitionReferenceVisitor<'db> { } impl<'db> TypeVisitor<'db> for DefinitionReferenceVisitor<'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + &self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -241,9 +249,10 @@ impl<'db> ProtocolInstanceType<'db> { return false; }; let definition = origin.definition(db); + let env = ProgramEnvironment::from_definition(definition); // Inspect the definition without its current specialization. Otherwise, a finite // type such as `Protocol[Protocol[int]]` would appear recursive. - let unspecialized = Type::instance(db, ClassType::NonGeneric(origin.into())); + let unspecialized = Type::instance(db, &env, ClassType::NonGeneric(origin.into())); DefinitionReferenceVisitor::references(db, unspecialized, definition) } } @@ -677,9 +686,11 @@ impl Drop for ActiveRecursionGuard<'_, T> { #[cfg(test)] mod tests { use super::{CycleDetector, CycleDetectorVisit, Db, HasIdentity, TypeIdentity}; - use crate::db::tests::{TestDb, setup_db}; + use crate::ProgramEnvironment; + use crate::db::tests::setup_db; use crate::place::global_symbol; use crate::types::Type; + use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::system::DbWithWritableSystem; use std::cell::Cell; @@ -748,12 +759,17 @@ mod tests { fn to_identity(&self, _db: &'db dyn Db) -> Self::Id {} } - fn global_instance_type<'db>(db: &'db TestDb, name: &str) -> Type<'db> { + fn global_instance_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> Type<'db> { let file = system_path_to_file(db, "/src/a.py").unwrap(); + let file = PythonFile::new(db, file, env.python_version(db)); global_symbol(db, file, name) .place .expect_type() - .to_instance_approximation(db) + .to_instance_approximation(db, env) .unwrap() } @@ -785,16 +801,17 @@ class RecursivePropertySetter[T](Protocol): ) .unwrap(); + let env = db.program_environment(); assert_eq!( - global_instance_type(&db, "GenericProperty").recursive_identity(&db), + global_instance_type(&db, &env, "GenericProperty").recursive_identity(&db), None ); assert!(matches!( - global_instance_type(&db, "RecursiveProperty").recursive_identity(&db), + global_instance_type(&db, &env, "RecursiveProperty").recursive_identity(&db), Some(TypeIdentity::RecursiveProtocol(_)) )); assert!(matches!( - global_instance_type(&db, "RecursivePropertySetter").recursive_identity(&db), + global_instance_type(&db, &env, "RecursivePropertySetter").recursive_identity(&db), Some(TypeIdentity::RecursiveProtocol(_)) )); } @@ -802,26 +819,28 @@ class RecursivePropertySetter[T](Protocol): #[test] fn caches_results_and_spills_after_two_entries() { let db = setup_db(); + let db = &db; let detector = Detector::new(0); - assert_eq!(detector.visit(&db, 1, || 10), 10); - assert_eq!(detector.visit(&db, 1, || 40), 10); - assert_eq!(detector.visit(&db, 2, || 20), 20); + assert_eq!(detector.visit(db, 1, || 10), 10); + assert_eq!(detector.visit(db, 1, || 40), 10); + assert_eq!(detector.visit(db, 2, || 20), 20); assert!(!detector.cache.borrow().is_spilled()); - assert_eq!(detector.visit(&db, 3, || 30), 30); + assert_eq!(detector.visit(db, 3, || 30), 30); assert!(detector.cache.borrow().is_spilled()); - assert_eq!(detector.visit(&db, 2, || 40), 20); - assert_eq!(detector.visit(&db, 3, || 40), 30); + assert_eq!(detector.visit(db, 2, || 40), 20); + assert_eq!(detector.visit(db, 3, || 40), 30); } #[test] fn nested_visit_short_circuits_on_cycle() { let db = setup_db(); + let db = &db; let detector = Detector::new(0); assert_eq!( - detector.visit(&db, 1, || detector.visit(&db, 1, || 20) + 10), + detector.visit(db, 1, || detector.visit(db, 1, || 20) + 10), 10 ); } @@ -829,12 +848,13 @@ class RecursivePropertySetter[T](Protocol): #[test] fn computes_each_active_identity_once() { let db = setup_db(); + let db = &db; let identity_calls = Cell::new(0); let detector = CycleDetector::, u8, 1>::new(0); assert_eq!( - detector.visit(&db, CountingIdentityItem::new(1, &identity_calls), || { - detector.visit(&db, CountingIdentityItem::new(3, &identity_calls), || 1) + detector.visit(db, CountingIdentityItem::new(1, &identity_calls), || { + detector.visit(db, CountingIdentityItem::new(3, &identity_calls), || 1) }), 1 ); @@ -844,12 +864,13 @@ class RecursivePropertySetter[T](Protocol): #[test] fn skips_identity_for_distinct_candidates() { let db = setup_db(); + let db = &db; let identity_calls = Cell::new(0); let detector = CycleDetector::, u8, 1>::new(0); assert_eq!( - detector.visit(&db, CountingIdentityItem::new(1, &identity_calls), || { - detector.visit(&db, CountingIdentityItem::new(2, &identity_calls), || 1) + detector.visit(db, CountingIdentityItem::new(1, &identity_calls), || { + detector.visit(db, CountingIdentityItem::new(2, &identity_calls), || 1) }), 1 ); @@ -859,15 +880,16 @@ class RecursivePropertySetter[T](Protocol): #[test] fn skips_identity_without_a_distinct_active_item() { let db = setup_db(); + let db = &db; let identity_calls = Cell::new(0); let detector = CycleDetector::, u8, 1>::new(0); assert_eq!( - detector.visit(&db, CountingIdentityItem::new(1, &identity_calls), || 1), + detector.visit(db, CountingIdentityItem::new(1, &identity_calls), || 1), 1 ); assert_eq!( - detector.visit(&db, CountingIdentityItem::new(1, &identity_calls), || 2), + detector.visit(db, CountingIdentityItem::new(1, &identity_calls), || 2), 1 ); assert_eq!(identity_calls.get(), 0); @@ -876,32 +898,33 @@ class RecursivePropertySetter[T](Protocol): #[test] fn different_items_with_same_identity_form_cycle() { let db = setup_db(); + let db = &db; let detector = CycleDetector::::new(0); let CycleDetectorVisit::Pending(pending) = - detector.begin_visit(&db, ConstantIdentityItem(1)) + detector.begin_visit(db, ConstantIdentityItem(1)) else { panic!("the first identity should be pending"); }; - let CycleDetectorVisit::Cycle(item) = detector.begin_visit(&db, ConstantIdentityItem(2)) + let CycleDetectorVisit::Cycle(item) = detector.begin_visit(db, ConstantIdentityItem(2)) else { panic!("a different item with the same identity should form a cycle"); }; assert_eq!(item.0, 2); detector.finish_visit(pending, 1); - let CycleDetectorVisit::Ready(seen) = detector.begin_visit(&db, ConstantIdentityItem(1)) + let CycleDetectorVisit::Ready(seen) = detector.begin_visit(db, ConstantIdentityItem(1)) else { panic!("the first identity should be ready after the pending visit is finished"); }; assert_eq!(seen, 1); let CycleDetectorVisit::Pending(pending) = - detector.begin_visit(&db, ConstantIdentityItem(2)) + detector.begin_visit(db, ConstantIdentityItem(2)) else { panic!("the second identity should be pending after the first is finished"); }; detector.finish_visit(pending, 2); - let CycleDetectorVisit::Ready(seen) = detector.begin_visit(&db, ConstantIdentityItem(2)) + let CycleDetectorVisit::Ready(seen) = detector.begin_visit(db, ConstantIdentityItem(2)) else { panic!("the second identity should be ready after the pending visit is finished"); }; diff --git a/crates/ty_python_semantic/src/types/dedicated/pydantic.rs b/crates/ty_python_semantic/src/types/dedicated/pydantic.rs index 7fa563a70b..b8fe610283 100644 --- a/crates/ty_python_semantic/src/types/dedicated/pydantic.rs +++ b/crates/ty_python_semantic/src/types/dedicated/pydantic.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use char_str::CharStr; use ruff_db::parsed::parsed_module; use ruff_python_ast::{ArgOrKeyword, Arguments, Expr, ExprCall, ExprDict, Keyword, name::Name}; @@ -145,7 +146,7 @@ impl<'db> FieldMetadata<'db> { definition: Definition<'db>, specialization: Option>, ) { - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let DefinitionKind::AnnotatedAssignment(assignment) = definition.kind(db) else { return; }; @@ -170,7 +171,7 @@ impl<'db> FieldMetadata<'db> { // using `StrictInt = Annotated[int, Strict()]`. Since we don't retain the `Annotated` // metadata, we need to follow the alias back to its definition and parse the metadata // from there. - let model = SemanticModel::new(db, definition.file(db)); + let model = SemanticModel::new(db, definition.python_file(db)); let Some(alias_definition) = definitions_for_name( &model, name.id.as_str(), @@ -182,7 +183,7 @@ impl<'db> FieldMetadata<'db> { return; }; - let module = parsed_module(db, alias_definition.file(db)).load(db); + let module = parsed_module(db, alias_definition.python_file(db)).load(db); let kind = alias_definition.kind(db); let value = match &kind { DefinitionKind::Assignment(assignment) => assignment.value(&module), @@ -298,7 +299,8 @@ impl<'db> FieldMetadata<'db> { if let Some(init) = call.arguments.find_keyword("init") { let init = definition_expression_type(db, definition, &init.value); - self.init &= !init.bool(db).is_always_false(); + let env = ProgramEnvironment::from_definition(definition); + self.init &= !init.bool(db, &env).is_always_false(); } if let Some(alias) = call @@ -487,7 +489,7 @@ fn config_boolean( }) } -pub(in crate::types) fn is_model(db: &dyn Db, class: StaticClassLiteral<'_>) -> bool { +pub(in crate::types) fn is_model<'db>(db: &'db dyn Db, class: StaticClassLiteral<'db>) -> bool { class .iter_mro(db, None) .filter_map(ClassBase::into_class) @@ -495,8 +497,12 @@ pub(in crate::types) fn is_model(db: &dyn Db, class: StaticClassLiteral<'_>) -> } /// Return whether `ty` is an instance of a Pydantic model. -pub(in crate::types) fn is_model_instance(db: &dyn Db, ty: Type<'_>) -> bool { - ty.nominal_class(db) +pub(in crate::types) fn is_model_instance( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + ty: Type<'_>, +) -> bool { + ty.nominal_class(db, env) .and_then(|class| class.static_class_literal(db)) .is_some_and(|(class, _)| is_model(db, class)) } @@ -654,7 +660,7 @@ fn own_model_config(db: &dyn Db, class: StaticClassLiteral<'_>) -> Option assignment.value(&module), @@ -772,7 +778,7 @@ fn model_config_from_dict(db: &dyn Db, definition: Definition<'_>, dict: &ExprDi fn class_keyword_config(db: &dyn Db, class: StaticClassLiteral<'_>) -> ModelConfig { let definition = class.definition(db); - let module = parsed_module(db, class.file(db)).load(db); + let module = parsed_module(db, class.python_file(db)).load(db); let kind = definition.kind(db); let Some(class) = kind.as_class() else { return ModelConfig::default(); @@ -834,7 +840,8 @@ pub(in crate::types) fn constructor_parameter_type<'db>( return field_type; } - lax_input_type(db, field_type) + let env = ProgramEnvironment::from_scope(class.body_scope(db)); + lax_input_type(db, &env, field_type) } /// Return whether `field_name` has a Pydantic field validator that receives the raw input. @@ -894,7 +901,7 @@ fn function_has_before_or_plain_field_validator<'db>( let DefinitionKind::Function(function) = definition.kind(db) else { return false; }; - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let function_node = function.node(&module); if function_node.decorator_list.is_empty() { return false; @@ -937,12 +944,17 @@ fn function_has_before_or_plain_field_validator<'db>( } /// Return the documented Python input type accepted by Pydantic for `field_type` in lax mode. -fn lax_input_type<'db>(db: &'db dyn Db, field_type: Type<'db>) -> Type<'db> { - lax_input_type_impl(db, field_type, &mut FxHashSet::default()) +fn lax_input_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + field_type: Type<'db>, +) -> Type<'db> { + lax_input_type_impl(db, env, field_type, &mut FxHashSet::default()) } fn lax_input_type_impl<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, field_type: Type<'db>, expanding_types: &mut FxHashSet>, ) -> Type<'db> { @@ -954,35 +966,36 @@ fn lax_input_type_impl<'db>( if !expanding_types.insert(field_type) { return Type::any(); } - let result = lax_input_type_impl(db, alias.value_type(db), expanding_types); + let result = lax_input_type_impl(db, env, alias.value_type(db), expanding_types); expanding_types.remove(&field_type); return result; } if field_type.as_union().and_then(|union| union.known(db)) == Some(KnownUnion::Float) { - return lax_alias(db, "LaxFloat"); + return lax_alias(db, env, "LaxFloat"); } if let Type::Union(union) = field_type { return UnionType::from_elements_leave_aliases( db, + env, union .elements(db) .iter() - .map(|element| lax_input_type_impl(db, *element, expanding_types)), + .map(|element| lax_input_type_impl(db, env, *element, expanding_types)), ); } - if let Some(input_type) = root_model_input_type(db, field_type, expanding_types) { + if let Some(input_type) = root_model_input_type(db, env, field_type, expanding_types) { return input_type; } - if let Some(input_type) = model_input_type(db, field_type) { + if let Some(input_type) = model_input_type(db, env, field_type) { return input_type; } let known_class = field_type - .nominal_class(db) + .nominal_class(db, env) .and_then(|class| class.known(db)); if matches!( @@ -997,25 +1010,29 @@ fn lax_input_type_impl<'db>( | KnownClass::Tuple ) ) { - let Ok(elements) = field_type.try_iterate(db) else { + let Ok(elements) = field_type.try_iterate(db, env) else { return Type::any(); }; - let element_type = - lax_input_type_impl(db, elements.homogeneous_element_type(db), expanding_types); - return KnownClass::Iterable.to_specialized_instance(db, &[element_type]); + let element_type = lax_input_type_impl( + db, + env, + elements.homogeneous_element_type(db, env), + expanding_types, + ); + return KnownClass::Iterable.to_specialized_instance(db, env, &[element_type]); } if matches!(known_class, Some(KnownClass::Dict | KnownClass::Mapping)) { - let Some(specialization) = - known_class.and_then(|known_class| field_type.known_specialization(db, known_class)) + let Some(specialization) = known_class + .and_then(|known_class| field_type.known_specialization(db, env, known_class)) else { return Type::any(); }; let [key_type, value_type] = specialization.types(db) else { return Type::any(); }; - let value_type = lax_input_type_impl(db, *value_type, expanding_types); - return KnownClass::Mapping.to_specialized_instance(db, &[*key_type, value_type]); + let value_type = lax_input_type_impl(db, env, *value_type, expanding_types); + return KnownClass::Mapping.to_specialized_instance(db, env, &[*key_type, value_type]); } let builtin_alias = match known_class { @@ -1028,10 +1045,10 @@ fn lax_input_type_impl<'db>( _ => None, }; if let Some(alias) = builtin_alias { - return lax_alias(db, alias); + return lax_alias(db, env, alias); } - let Some((module, symbol, class)) = instance_symbol(db, field_type) else { + let Some((module, symbol, class)) = instance_symbol(db, env, field_type) else { return Type::any(); }; let symbol_alias = match (module, symbol) { @@ -1051,23 +1068,23 @@ fn lax_input_type_impl<'db>( _ => None, }; if let Some(alias) = symbol_alias { - return lax_alias(db, alias); + return lax_alias(db, env, alias); } let alias = if (module, symbol) == (KnownModule::Re, "Pattern") { - let Some(specialization) = field_type.specialization_of(db, class) else { + let Some(specialization) = field_type.specialization_of(db, env, class) else { return Type::any(); }; let [pattern_type] = specialization.types(db) else { return Type::any(); }; if pattern_type - .nominal_class(db) + .nominal_class(db, env) .is_some_and(|class| class.is_known(db, KnownClass::Str)) { "LaxStrPattern" } else if pattern_type - .nominal_class(db) + .nominal_class(db, env) .is_some_and(|class| class.is_known(db, KnownClass::Bytes)) { "LaxBytesPattern" @@ -1078,7 +1095,7 @@ fn lax_input_type_impl<'db>( return Type::any(); }; - lax_alias(db, alias) + lax_alias(db, env, alias) } /// Return the input type accepted for a Pydantic root model field. @@ -1088,10 +1105,13 @@ fn lax_input_type_impl<'db>( /// `IntList` instance and an `Iterable[LaxInt]`. fn root_model_input_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, field_type: Type<'db>, expanding_types: &mut FxHashSet>, ) -> Option> { - let (class, specialization) = field_type.nominal_class(db)?.static_class_literal(db)?; + let (class, specialization) = field_type + .nominal_class(db, env)? + .static_class_literal(db)?; if !is_root_model(db, class) { return None; } @@ -1108,15 +1128,16 @@ fn root_model_input_type<'db>( if !expanding_types.insert(field_type) { return Some(Type::any()); } - let root_input_type = lax_input_type_impl(db, root_field.declared_ty, expanding_types); + let root_input_type = lax_input_type_impl(db, env, root_field.declared_ty, expanding_types); expanding_types.remove(&field_type); // In lax mode, Pydantic accepts a Box[str] when a Box[int] is expected, so we widen // to a gradual specialization here. Widening to `Box[LaxStr]` would only work for // covariant generics. - let model_instance = Type::instance(db, class.unknown_specialization(db)); + let model_instance = Type::instance(db, env, class.unknown_specialization(db)); Some(UnionType::from_two_elements( db, + env, model_instance, root_input_type, )) @@ -1127,8 +1148,14 @@ fn root_model_input_type<'db>( /// By default, Pydantic accepts either an instance of the model or a mapping of string keys to /// input values. Other custom validators can accept additional input types, which are not modeled /// here. -fn model_input_type<'db>(db: &'db dyn Db, field_type: Type<'db>) -> Option> { - let (class, _) = field_type.nominal_class(db)?.static_class_literal(db)?; +fn model_input_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + field_type: Type<'db>, +) -> Option> { + let (class, _) = field_type + .nominal_class(db, env)? + .static_class_literal(db)?; if !is_model(db, class) || is_root_model(db, class) { return None; } @@ -1141,25 +1168,34 @@ fn model_input_type<'db>(db: &'db dyn Db, field_type: Type<'db>) -> Option( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option<(KnownModule, &'db str, StaticClassLiteral<'db>)> { - let class = ty.nominal_class(db)?.class_literal(db).as_static()?; - let module = file_to_module(db, class.file(db))?.known(db)?; + let class = ty.nominal_class(db, env)?.class_literal(db).as_static()?; + let module = file_to_module(db, class.python_file(db))?.known(db)?; Some((module, class.name(db).as_str(), class)) } /// Return a lax-input alias like `LaxInt` from `ty_extensions.pydantic`. -fn lax_alias<'db>(db: &'db dyn Db, name: &str) -> Type<'db> { - match known_module_symbol(db, KnownModule::TyExtensionsPydantic, name) +fn lax_alias<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, name: &str) -> Type<'db> { + match known_module_symbol(db, env, KnownModule::TyExtensionsPydantic, name) .place .ignore_possibly_undefined() { diff --git a/crates/ty_python_semantic/src/types/definition.rs b/crates/ty_python_semantic/src/types/definition.rs index 1720dad7df..000eb3d00a 100644 --- a/crates/ty_python_semantic/src/types/definition.rs +++ b/crates/ty_python_semantic/src/types/definition.rs @@ -33,7 +33,7 @@ impl TypeDefinition<'_> { | Self::SpecialForm(definition) | Self::NewType(definition) | Self::EnumMember(definition) => { - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); Some(definition.focus_range(db, &module)) } } @@ -54,7 +54,7 @@ impl TypeDefinition<'_> { | Self::SpecialForm(definition) | Self::NewType(definition) | Self::EnumMember(definition) => { - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); Some(definition.full_range(db, &module)) } } diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index fb09c8bdf6..5f66e82cd6 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -32,7 +32,7 @@ use crate::types::{ protocol_class::ProtocolClass, }; use crate::types::{KnownInstanceType, MemberLookupPolicy, TypeVarKind, TypedDictType, UnionType}; -use crate::{Db, DisplaySettings, FxIndexMap, Program, declare_lint}; +use crate::{Db, DisplaySettings, FxIndexMap, ProgramEnvironment, declare_lint}; use itertools::Itertools; use ruff_db::source::source_text; use ruff_db::{ @@ -1296,6 +1296,7 @@ pub(crate) fn report_mismatched_type_name<'db>( actual_name: Option<&str>, actual_name_ty: Type<'db>, ) { + let db = context.db(); if let Some(builder) = context.report_lint(&MISMATCHED_TYPE_NAME, node) { let mut diagnostic = builder.into_diagnostic(format_args!( "The name passed to `{constructor}` must match the variable it is assigned to" @@ -1305,9 +1306,10 @@ pub(crate) fn report_mismatched_type_name<'db>( "Expected \"{expected_name}\", got \"{actual_name}\"" )); } else { + let env = context.program_environment(); diagnostic.set_primary_annotation_message(format_args!( "Expected \"{expected_name}\", got variable of type `{}`", - actual_name_ty.display(context.db()) + actual_name_ty.display(db, env) )); } } @@ -1395,12 +1397,14 @@ pub(super) fn report_index_out_of_bounds( length: impl std::fmt::Display, index: i64, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INDEX_OUT_OF_BOUNDS, node) else { return; }; + let env = &context.program_environment(); builder.into_diagnostic(format_args!( "Index {index} is out of bounds for {kind} `{}` with length {length}", - tuple_ty.display(context.db()) + tuple_ty.display(db, env) )); } @@ -1411,18 +1415,20 @@ pub(super) fn report_not_subscriptable( not_subscriptable_ty: Type, method: &str, ) { + let db = context.db(); let Some(builder) = context.report_lint(&NOT_SUBSCRIPTABLE, node) else { return; }; + let env = &context.program_environment(); if method == "__delitem__" { builder.into_diagnostic(format_args!( "Cannot delete subscript on object of type `{}` with no `{method}` method", - not_subscriptable_ty.display(context.db()) + not_subscriptable_ty.display(db, env) )); } else { builder.into_diagnostic(format_args!( "Cannot subscript object of type `{}` with no `{method}` method", - not_subscriptable_ty.display(context.db()) + not_subscriptable_ty.display(db, env) )); } } @@ -1448,17 +1454,18 @@ pub(crate) fn is_invalid_typed_dict_literal( && matches!(source, AnyNodeRef::ExprDict(_)) } -fn report_invalid_assignment_with_message<'db, 'ctx: 'db, T: Ranged>( - context: &'ctx InferContext, +fn report_invalid_assignment_with_message<'db, 'env: 'db, T: Ranged>( + context: &'env InferContext, node: T, message: std::fmt::Arguments, -) -> Option> { +) -> Option> { let builder = context.report_lint(&INVALID_ASSIGNMENT, node)?; Some(builder.into_diagnostic(message)) } pub(super) fn note_numbers_module_not_supported<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, diag: &mut Diagnostic, target_ty: Type<'db>, value_ty: Type<'db>, @@ -1467,13 +1474,21 @@ pub(super) fn note_numbers_module_not_supported<'db>( [KnownClass::Int, KnownClass::Float, KnownClass::Complex]; if let Type::NominalInstance(target_instance) = target_ty { - let file = target_instance.class(db).class_literal(db).file(db); + let file = target_instance + .class(db, env) + .class_literal(db) + .python_file(db); if let Some(module) = file_to_module(db, file) && module.is_known(db, KnownModule::Numbers) { let is_numeric = value_ty.is_subtype_of( db, - UnionType::from_elements(db, BUILTIN_NUMBERS.iter().map(|cls| cls.to_instance(db))), + env, + UnionType::from_elements( + db, + env, + BUILTIN_NUMBERS.iter().map(|cls| cls.to_instance(db, env)), + ), ); if is_numeric { @@ -1517,15 +1532,16 @@ fn covariant_supertype_hint<'db>( /// that fails due to invariance. pub(super) fn add_invariant_generic_hints<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, diag: &mut Diagnostic, expected_ty: Type<'db>, provided_ty: Type<'db>, ) { - let Some((expected_class, expected_specialization)) = expected_ty.class_specialization(db) + let Some((expected_class, expected_specialization)) = expected_ty.class_specialization(db, env) else { return; }; - let Some((provided_class, provided_specialization)) = provided_ty.class_specialization(db) + let Some((provided_class, provided_specialization)) = provided_ty.class_specialization(db, env) else { return; }; @@ -1546,13 +1562,13 @@ pub(super) fn add_invariant_generic_hints<'db>( .enumerate() .filter_map(|(index, ((bound_typevar, expected_arg), provided_arg))| { (bound_typevar.variance(db) == TypeVarVariance::Invariant - && !expected_arg.is_equivalent_to(db, *provided_arg)) + && !expected_arg.is_equivalent_to(db, env, *provided_arg)) .then_some((index, expected_arg, provided_arg)) }); let mut mismatch_indices = Vec::new(); for (index, expected_arg, provided_arg) in mismatched_invariant_arguments { - if !provided_arg.is_assignable_to(db, *expected_arg) { + if !provided_arg.is_assignable_to(db, env, *expected_arg) { return; } mismatch_indices.push(index); @@ -1592,6 +1608,7 @@ pub(super) fn report_invalid_assignment<'db>( target_ty: Type, value_ty: Type<'db>, ) { + let db = context.db(); let definition_kind = definition.kind(context.db()); let value_node = match definition_kind { DefinitionKind::Assignment(def) => Some(def.value(context.module())), @@ -1601,13 +1618,13 @@ pub(super) fn report_invalid_assignment<'db>( }; if let Some(value_node) = value_node - && is_invalid_typed_dict_literal(context.db(), target_ty, value_node.into()) + && is_invalid_typed_dict_literal(db, target_ty, value_node.into()) { return; } - let settings = - DisplaySettings::from_possibly_ambiguous_types(context.db(), [target_ty, value_ty]); + let env = &context.program_environment(); + let settings = DisplaySettings::from_possibly_ambiguous_types(db, env, [target_ty, value_ty]); let diagnostic_range = if let Some(value_node) = value_node { // Expand the range to include parentheses around the value, if any. This allows @@ -1630,8 +1647,8 @@ pub(super) fn report_invalid_assignment<'db>( diagnostic_range, format_args!( "Object of type `{}` is not assignable to `{}`", - value_ty.display_with(context.db(), settings.clone()), - target_ty.display_with(context.db(), settings) + value_ty.display_with(db, env, settings.clone()), + target_ty.display_with(db, env, settings) ), ) else { return; @@ -1669,18 +1686,18 @@ pub(super) fn report_invalid_assignment<'db>( // Otherwise, annotate the target with its declared type. diag.annotate(context.secondary(target_node).message(format_args!( "Declared type `{}`", - target_ty.display(context.db()), + target_ty.display(db, env) ))); } } diag.set_primary_annotation_message(format_args!( "Incompatible value of type `{}`", - value_ty.display(context.db()), + value_ty.display(db, env), )); - let error_context = value_ty.assignability_error_context(context.db(), target_ty); - error_context.attach_to(context.db(), &mut diag); + let error_context = value_ty.assignability_error_context(db, env, target_ty); + error_context.attach_to(db, env, &mut diag); // Overwrite the concise message to avoid showing the value type twice let message = diag.headline_message().to_string(); @@ -1688,8 +1705,8 @@ pub(super) fn report_invalid_assignment<'db>( } // special case message - note_numbers_module_not_supported(context.db(), &mut diag, target_ty, value_ty); - add_invariant_generic_hints(context.db(), &mut diag, target_ty, value_ty); + note_numbers_module_not_supported(db, env, &mut diag, target_ty, value_ty); + add_invariant_generic_hints(db, env, &mut diag, target_ty, value_ty); } pub(super) fn report_invalid_attribute_assignment( @@ -1699,25 +1716,27 @@ pub(super) fn report_invalid_attribute_assignment( source_ty: Type, attribute_name: &'_ str, ) { + let db = context.db(); // TODO: Ideally we would not emit diagnostics for `TypedDict` literal arguments // here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have // silenced diagnostics during attribute resolution, and rely on the assignability // diagnostic being emitted here. + let env = &context.program_environment(); let Some(mut diag) = report_invalid_assignment_with_message( context, range, format_args!( "Object of type `{}` is not assignable to attribute `{attribute_name}` of type `{}`", - source_ty.display(context.db()), - target_ty.display(context.db()), + source_ty.display(db, env), + target_ty.display(db, env), ), ) else { return; }; - let error_context = source_ty.assignability_error_context(context.db(), target_ty); - error_context.attach_to(context.db(), &mut diag); + let error_context = source_ty.assignability_error_context(db, env, target_ty); + error_context.attach_to(db, env, &mut diag); } pub(super) fn report_bad_dunder_set_call<'db>( @@ -1730,18 +1749,19 @@ pub(super) fn report_bad_dunder_set_call<'db>( value: &ast::Expr, ) { let db = context.db(); + let env = &context.program_environment(); let attribute = target.attr.as_str(); if let Some(property) = dunder_set_failure.as_attempt_to_set_property_with_no_setter() { let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, target) else { return; }; - let object_type = object_type.display(db); + let object_type = object_type.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot assign to read-only property `{attribute}` on object of type `{object_type}`", )); if let Some(file_range) = property .getter(db) - .and_then(|getter| getter.definition(db)) + .and_then(|getter| getter.definition(db, env)) .and_then(|definition| definition.focus_range(db)) { diagnostic.annotate(Annotation::secondary(Span::from(file_range)).message( @@ -1764,11 +1784,11 @@ pub(super) fn report_bad_dunder_set_call<'db>( lint: &INVALID_ASSIGNMENT, message: format!( "Invalid assignment to data descriptor attribute `{attribute}` on type `{}`", - object_type.display(db) + object_type.display(db, env) ), info: &format!( "This assignment implicitly calls `__set__` on a descriptor of type `{}`", - descriptor_type.display(db) + descriptor_type.display(db, env) ), argument_ranges, }, @@ -1787,15 +1807,20 @@ pub(super) fn report_bad_dunder_delete_call<'db>( return; }; let db = context.db(); + let env = &context.program_environment(); if let Some(property) = dunder_delete_failure.as_attempt_to_delete_property_with_no_deleter() { - let object_type = object_type.display(db); + let object_type = object_type.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot delete read-only property `{attribute}` on object of type `{object_type}`", )); if let Some(file_range) = property .getter(db) - .and_then(|getter| getter.definition(db)) - .or_else(|| property.setter(db).and_then(|setter| setter.definition(db))) + .and_then(|getter| getter.definition(db, env)) + .or_else(|| { + property + .setter(db) + .and_then(|setter| setter.definition(db, env)) + }) .and_then(|definition| definition.focus_range(db)) { diagnostic.annotate(Annotation::secondary(Span::from(file_range)).message( @@ -1809,7 +1834,7 @@ pub(super) fn report_bad_dunder_delete_call<'db>( builder.into_diagnostic(format_args!( "Invalid deletion of data descriptor attribute \ `{attribute}` on type `{}` with custom `__delete__` method", - object_type.display(db) + object_type.display(db, env) )); } } @@ -1821,18 +1846,19 @@ pub(super) fn report_bad_dunder_delattr_call( target: &ast::ExprAttribute, binding_error: bool, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, target) else { return; }; - let db = context.db(); + let env = &context.program_environment(); let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot delete attribute `{attribute}` on type `{}` with custom `__delattr__` method", - object_type.display(db), + object_type.display(db, env), )); if binding_error { diagnostic.info(format_args!( "Type `{}` has a `__delattr__` method, but it cannot be called with the expected arguments", - object_type.display(db) + object_type.display(db, env) )); diagnostic.info( "Expected a signature at least as permissive as \ @@ -1848,29 +1874,31 @@ pub(super) fn report_invalid_return_type( expected_ty: Type, actual_ty: Type, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_RETURN_TYPE, object_range) else { return; }; + let env = &context.program_environment(); let settings = - DisplaySettings::from_possibly_ambiguous_types(context.db(), [expected_ty, actual_ty]); + DisplaySettings::from_possibly_ambiguous_types(db, env, [expected_ty, actual_ty]); let return_type_span = context.span(return_type_range); let mut diag = builder.into_diagnostic("Return type does not match returned value"); diag.set_primary_annotation_message(format_args!( "expected `{expected_ty}`, found `{actual_ty}`", - expected_ty = expected_ty.display_with(context.db(), settings.clone()), - actual_ty = actual_ty.display_with(context.db(), settings.clone()), + expected_ty = expected_ty.display_with(db, env, settings.clone()), + actual_ty = actual_ty.display_with(db, env, settings.clone()), )); diag.annotate( Annotation::secondary(return_type_span).message(format_args!( "Expected `{expected_ty}` because of return type", - expected_ty = expected_ty.display_with(context.db(), settings), + expected_ty = expected_ty.display_with(db, env, settings), )), ); - let error_context = actual_ty.assignability_error_context(context.db(), expected_ty); - error_context.attach_to(context.db(), &mut diag); + let error_context = actual_ty.assignability_error_context(db, env, expected_ty); + error_context.attach_to(db, env, &mut diag); } pub(super) fn report_invalid_generator_function_return_type( @@ -1879,15 +1907,17 @@ pub(super) fn report_invalid_generator_function_return_type( inferred_return: KnownClass, expected_ty: Type, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_RETURN_TYPE, return_type_range) else { return; }; + let env = &context.program_environment(); let mut diag = builder.into_diagnostic("Return type does not match returned value"); - let inferred_ty = inferred_return.display(context.db()); + let inferred_ty = inferred_return.display(env.python_version(db)); diag.set_primary_annotation_message(format_args!( "expected `{expected_ty}`, found `{inferred_ty}`", - expected_ty = expected_ty.display(context.db()), + expected_ty = expected_ty.display(db, env), )); let (description, link) = if inferred_return == KnownClass::AsyncGeneratorType { @@ -1922,14 +1952,16 @@ pub(super) fn report_invalid_generator_yield_type( actual_ty: Type, kind: GeneratorMismatchKind, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_YIELD, object_range) else { return; }; + let env = &context.program_environment(); let settings = - DisplaySettings::from_possibly_ambiguous_types(context.db(), [expected_ty, actual_ty]); - let expected_display = expected_ty.display_with(context.db(), settings.clone()); - let actual_display = actual_ty.display_with(context.db(), settings); + DisplaySettings::from_possibly_ambiguous_types(db, env, [expected_ty, actual_ty]); + let expected_display = expected_ty.display_with(db, env, settings.clone()); + let actual_display = actual_ty.display_with(db, env, settings); let (kind_name, title, concise) = match kind { GeneratorMismatchKind::YieldType => ( @@ -1966,8 +1998,8 @@ pub(super) fn report_invalid_generator_yield_type( ))); } - let error_context = actual_ty.assignability_error_context(context.db(), expected_ty); - error_context.attach_to(context.db(), &mut diag); + let error_context = actual_ty.assignability_error_context(db, env, expected_ty); + error_context.attach_to(db, env, &mut diag); } pub(super) fn report_implicit_return_type( @@ -1990,12 +2022,13 @@ pub(super) fn report_implicit_return_type( let Some(builder) = context.report_lint(lint_to_use, range) else { return; }; + let env = &context.program_environment(); // If no return statement is defined in the function, then the function always returns `None` let mut diagnostic = if no_return { let mut diag = builder.into_diagnostic(format_args!( "Function always implicitly returns `None`, which is not assignable to return type `{}`", - expected_ty.display(db), + expected_ty.display(db, env), )); diag.info( "Consider changing the return annotation to `-> None` or adding a `return` statement", @@ -2004,7 +2037,7 @@ pub(super) fn report_implicit_return_type( } else { builder.into_diagnostic(format_args!( "Function can implicitly return `None`, which is not assignable to return type `{}`", - expected_ty.display(db), + expected_ty.display(db, env), )) }; if !has_empty_body { @@ -2072,6 +2105,7 @@ pub(super) fn report_possibly_missing_attribute( return; }; let db = context.db(); + let env = &context.program_environment(); match object_ty { Type::ModuleLiteral(module) => builder.into_diagnostic(format_args!( "Member `{attribute}` may be missing on module `{}`", @@ -2083,11 +2117,11 @@ pub(super) fn report_possibly_missing_attribute( )), Type::GenericAlias(alias) => builder.into_diagnostic(format_args!( "Attribute `{attribute}` may be missing on class `{}`", - alias.display(db), + alias.display(db, env), )), _ => builder.into_diagnostic(format_args!( "Attribute `{attribute}` may be missing on object of type `{}`", - object_ty.display(db), + object_ty.display(db, env), )), }; } @@ -2098,21 +2132,23 @@ pub(super) fn report_invalid_exception_tuple_caught<'db, 'ast>( node_type: Type<'db>, invalid_tuple_nodes: impl IntoIterator)>, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_EXCEPTION_CAUGHT, node) else { return; }; + let env = &context.program_environment(); let mut diagnostic = builder.into_diagnostic("Invalid tuple caught in an exception handler"); diagnostic.set_concise_message(format_args!( "Cannot catch object of type `{}` in an exception handler", - node_type.display(context.db()) + node_type.display(db, env) )); for (sub_node, ty) in invalid_tuple_nodes { let span = context.span(sub_node); diagnostic.annotate(Annotation::secondary(span.clone()).message(format_args!( "Invalid element of type `{}`", - ty.display(context.db()) + ty.display(db, env) ))); if ty.is_notimplemented(context.db()) { diagnostic.annotate( @@ -2127,10 +2163,12 @@ pub(super) fn report_invalid_exception_tuple_caught<'db, 'ast>( } pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast::Expr, ty: Type) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_EXCEPTION_CAUGHT, node) else { return; }; + let env = &context.program_environment(); let mut diagnostic = if ty.is_notimplemented(context.db()) { let mut diag = builder.into_diagnostic("Cannot catch `NotImplemented` in an exception handler"); @@ -2139,7 +2177,7 @@ pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast } else { let mut diag = builder.into_diagnostic(format_args!( "Invalid {thing} caught in an exception handler", - thing = if ty.tuple_instance_spec(context.db()).is_some() { + thing = if ty.tuple_instance_spec(db, env).is_some() { "tuple" } else { "object" @@ -2147,7 +2185,7 @@ pub(super) fn report_invalid_exception_caught(context: &InferContext, node: &ast )); diag.set_primary_annotation_message(format_args!( "Object has type `{}`", - ty.display(context.db()) + ty.display(db, env) )); diag }; @@ -2162,9 +2200,11 @@ pub(crate) fn report_invalid_exception_raised( raised_node: &ast::Expr, raise_type: Type, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_RAISE, raised_node) else { return; }; + let env = &context.program_environment(); if raise_type.is_notimplemented(context.db()) { let mut diagnostic = builder.into_diagnostic(format_args!("Cannot raise `NotImplemented`")); diagnostic.set_primary_annotation_message("Did you mean `NotImplementedError`?"); @@ -2172,16 +2212,18 @@ pub(crate) fn report_invalid_exception_raised( } else { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot raise object of type `{}`", - raise_type.display(context.db()) + raise_type.display(db, env) )); diagnostic.set_primary_annotation_message("Not an instance or subclass of `BaseException`"); } } pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast::Expr, ty: Type) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_RAISE, node) else { return; }; + let env = &context.program_environment(); let mut diagnostic = if ty.is_notimplemented(context.db()) { let mut diag = builder.into_diagnostic(format_args!( "Cannot use `NotImplemented` as an exception cause", @@ -2191,7 +2233,7 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast: } else { builder.into_diagnostic(format_args!( "Cannot use object of type `{}` as an exception cause", - ty.display(context.db()) + ty.display(db, env) )) }; diagnostic.info( @@ -2486,7 +2528,7 @@ pub(crate) fn report_bad_argument_to_protocol_interface( "Only protocol classes can be passed to `reveal_protocol_interface`", ); - if let Some(class) = param_type.to_class_type(context.db()) { + if let Some(class) = param_type.to_class_type(db) { let mut class_def_diagnostic = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( @@ -2525,11 +2567,12 @@ pub(crate) fn report_invalid_class_match_pattern( pattern_cls: T, cls_ty: Type, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_MATCH_PATTERN, pattern_cls) else { return; }; - let db = context.db(); - let class_display = cls_ty.display(db); + let env = &context.program_environment(); + let class_display = cls_ty.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "`{class_display}` cannot be used in a class pattern because it is not a type" )); @@ -2557,20 +2600,21 @@ pub(crate) fn report_invalid_match_args_type( match_args_ty: Type, cls_ty: Type, ) { + let db = context.db(); let Some(builder) = context.report_lint(&INVALID_MATCH_PATTERN, pattern) else { return; }; - let db = context.db(); - let class_display = cls_ty.display(db); - let match_args_display = match_args_ty.display(db); + let env = &context.program_environment(); + let class_display = cls_ty.display(db, env); + let match_args_display = match_args_ty.display(db, env); builder.into_diagnostic(format_args!( "`__match_args__` for `{class_display}` must be an exact tuple, not `{match_args_display}`" )); } -pub(crate) fn add_type_expression_reference_link<'db, 'ctx>( - mut diag: LintDiagnosticGuard<'db, 'ctx>, -) -> LintDiagnosticGuard<'db, 'ctx> { +pub(crate) fn add_type_expression_reference_link<'db, 'env>( + mut diag: LintDiagnosticGuard<'db, 'env>, +) -> LintDiagnosticGuard<'db, 'env> { diag.info("See the following page for a reference on valid type expressions:"); diag.info( "https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions", @@ -2628,8 +2672,7 @@ pub(crate) fn report_issubclass_check_against_protocol_with_non_method_members<' if it has non-method members", ); if let Some(definition) = single_member.definition() { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let span = Span::from(definition.focus_range(db, &module)); sub.annotate(Annotation::primary(span).message(format_args!( "Non-method member `{}` declared here", @@ -2653,8 +2696,7 @@ pub(crate) fn report_issubclass_check_against_protocol_with_non_method_members<' .iter() .find_map(|member| Some((member.name(), member.definition()?))) { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let span = Span::from(definition.focus_range(db, &module)); sub.annotate( Annotation::primary(span) @@ -2802,7 +2844,7 @@ pub(super) fn abstract_method_span<'db>( }; let file = function.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, function.python_file(db)).load(db); let node = implementation.node(db, file, &module); let source_text = source_text(db, file); @@ -2836,7 +2878,11 @@ pub(crate) fn report_undeclared_protocol_member( /// We want to avoid suggesting an annotation for e.g. `x = None`, /// because the user almost certainly doesn't want to write `x: None = None`. /// We also want to avoid suggesting invalid syntax such as `x: = int`. - fn should_give_hint<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { + fn should_give_hint<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { let class = match ty { Type::ProtocolInstance(protocol) if protocol.class_origin(db).is_some() => return true, Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { @@ -2845,12 +2891,12 @@ pub(crate) fn report_undeclared_protocol_member( SubclassOfInner::Dynamic(DynamicType::Any) => return true, SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => return false, }, - Type::NominalInstance(instance) => instance.class(db), + Type::NominalInstance(instance) => instance.class(db, env), Type::Union(union) => { return union .elements(db) .iter() - .all(|elem| should_give_hint(db, *elem)); + .all(|elem| should_give_hint(db, env, *elem)); } _ => return false, }; @@ -2880,14 +2926,14 @@ pub(crate) fn report_undeclared_protocol_member( .into_diagnostic("Cannot assign to undeclared variable in the body of a protocol class"); if definition.kind(db).is_unannotated_assignment() { + let env = &context.program_environment(); let binding_type = binding_type(db, definition); + let suggestion = binding_type.promote(db, env); - let suggestion = binding_type.promote(db); - - if should_give_hint(db, suggestion) { + if should_give_hint(db, env, suggestion) { diagnostic.set_primary_annotation_message(format_args!( "Consider adding an annotation, e.g. `{symbol_name}: {} = ...`", - suggestion.display(db) + suggestion.display(db, env) )); } else { diagnostic.set_primary_annotation_message(format_args!( @@ -3007,9 +3053,10 @@ pub(crate) fn report_invalid_or_unsupported_base( class: StaticClassLiteral, ) { let db = context.db(); - let instance_of_type = KnownClass::Type.to_instance(db); + let env = &context.program_environment(); + let instance_of_type = KnownClass::Type.to_instance(db, env); - if base_type.is_assignable_to(db, instance_of_type) { + if base_type.is_assignable_to(db, env, instance_of_type) { report_unsupported_base(context, base_node, base_type, class); return; } @@ -3032,7 +3079,7 @@ pub(crate) fn report_invalid_or_unsupported_base( return; } - let tuple_of_types = Type::homogeneous_tuple(db, instance_of_type); + let tuple_of_types = Type::homogeneous_tuple(db, env, instance_of_type); let explain_mro_entries = |diagnostic: &mut LintDiagnosticGuard| { diagnostic.info( @@ -3041,14 +3088,19 @@ pub(crate) fn report_invalid_or_unsupported_base( ); }; + let env = &context.program_environment(); match base_type.try_call_dunder( db, + env, "__mro_entries__", CallArguments::positional([tuple_of_types]), TypeContext::default(), ) { Ok(ret) => { - if ret.return_type(db).is_assignable_to(db, tuple_of_types) { + if ret + .return_type(db, env) + .is_assignable_to(db, env, tuple_of_types) + { report_unsupported_base(context, base_node, base_type, class); } else { let Some(mut diagnostic) = @@ -3059,7 +3111,7 @@ pub(crate) fn report_invalid_or_unsupported_base( explain_mro_entries(&mut diagnostic); diagnostic.info(format_args!( "Type `{}` has an `__mro_entries__` method, but it does not return a tuple of types", - base_type.display(db) + base_type.display(db, env) )); } } @@ -3075,13 +3127,13 @@ pub(crate) fn report_invalid_or_unsupported_base( explain_mro_entries(&mut diagnostic); diagnostic.info(format_args!( "Type `{}` may have an `__mro_entries__` attribute, but it may be missing", - base_type.display(db) + base_type.display(db, env) )); if let Some(unbound_on) = unbound_on { for ty in unbound_on { diagnostic.info(format_args!( "`{}` does not implement `__mro_entries__`", - ty.display(db) + ty.display(db, env) )); } } @@ -3090,7 +3142,7 @@ pub(crate) fn report_invalid_or_unsupported_base( explain_mro_entries(&mut diagnostic); diagnostic.info(format_args!( "Type `{}` has an `__mro_entries__` attribute, but it is not callable", - base_type.display(db) + base_type.display(db, env) )); } CallDunderError::CallError(CallErrorKind::BindingError, _, _) => { @@ -3098,7 +3150,7 @@ pub(crate) fn report_invalid_or_unsupported_base( diagnostic.info(format_args!( "Type `{}` has an `__mro_entries__` method, \ but it cannot be called with the expected arguments", - base_type.display(db) + base_type.display(db, env) )); diagnostic.info( "Expected a signature at least as permissive as \ @@ -3110,7 +3162,7 @@ pub(crate) fn report_invalid_or_unsupported_base( diagnostic.info(format_args!( "Type `{}` has an `__mro_entries__` method, \ but it may not be callable", - base_type.display(db) + base_type.display(db, env) )); } } @@ -3128,11 +3180,13 @@ pub(crate) fn report_unsupported_base( return; }; let db = context.db(); + let env = &context.program_environment(); let mut diagnostic = builder.into_diagnostic("Unsupported class base"); - diagnostic.set_primary_annotation_message(format_args!("Has type `{}`", base_type.display(db))); + diagnostic + .set_primary_annotation_message(format_args!("Has type `{}`", base_type.display(db, env))); diagnostic.set_concise_message(format_args!( "Unsupported class base with type `{}`", - base_type.display(db) + base_type.display(db, env) )); diagnostic.info(format_args!( "ty cannot resolve a consistent method resolution order (MRO) for class `{}` due to this base", @@ -3141,16 +3195,18 @@ pub(crate) fn report_unsupported_base( diagnostic.info("Only class objects or `Any` are supported as class bases"); } -fn report_invalid_base<'ctx, 'db>( - context: &'ctx InferContext<'db, '_>, +fn report_invalid_base<'env, 'db>( + context: &'env InferContext<'db, '_>, base_node: &ast::Expr, base_type: Type<'db>, class: StaticClassLiteral<'db>, -) -> Option> { +) -> Option> { + let db = context.db(); let builder = context.report_lint(&INVALID_BASE, base_node)?; + let env = &context.program_environment(); let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid class base with type `{}`", - base_type.display(context.db()) + base_type.display(db, env) )); diagnostic.info(format_args!( "Definition of class `{}` will raise `TypeError` at runtime", @@ -3170,10 +3226,11 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( ) { let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_KEY, key_node) { + let env = &context.program_environment(); match key_ty.as_string_literal() { Some(key) => { let key = key.value(db); - let typed_dict_name = typed_dict_ty.display(db); + let typed_dict_name = typed_dict_ty.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Unknown key \"{key}\" for TypedDict `{typed_dict_name}`", @@ -3187,7 +3244,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( } else { "intersection" }, - full_object_ty = full_object_ty.display(db) + full_object_ty = full_object_ty.display(db, env) )) } else { context @@ -3226,7 +3283,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( if let Some(full_ty) = full_object_ty { diagnostic.set_concise_message(format_args!( "Unknown key \"{key}\" for TypedDict `{typed_dict_name}` (subscripted object has type `{full_ty}`)", - full_ty = full_ty.display(db), + full_ty = full_ty.display(db, env), )); } else { diagnostic.set_concise_message(format_args!( @@ -3239,14 +3296,14 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>( let mut diagnostic = builder.into_diagnostic(format_args!( "TypedDict `{}` can only be subscripted with a string literal key, \ got key of type `{}`", - typed_dict_ty.display(db), - key_ty.display(db), + typed_dict_ty.display(db, env), + key_ty.display(db, env), )); if let Some(full_object_ty) = full_object_ty { diagnostic.info(format_args!( "The full type of the subscripted object is `{}`", - full_object_ty.display(db) + full_object_ty.display(db, env) )); } } @@ -3371,7 +3428,8 @@ pub(crate) fn report_missing_typed_dict_key<'db>( ) { let db = context.db(); if let Some(builder) = context.report_lint(&MISSING_TYPED_DICT_KEY, constructor_node) { - let typed_dict_name = typed_dict_ty.display(db); + let env = &context.program_environment(); + let typed_dict_name = typed_dict_ty.display(db, env); builder.into_diagnostic(format_args!( "Missing required key '{missing_field}' in TypedDict `{typed_dict_name}` constructor", )); @@ -3386,7 +3444,8 @@ pub(crate) fn report_cannot_pop_required_field_on_typed_dict<'db>( ) { let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, key_node) { - let typed_dict_name = typed_dict_ty.display(db); + let env = &context.program_environment(); + let typed_dict_name = typed_dict_ty.display(db, env); builder.into_diagnostic(format_args!( "Cannot pop required field '{field_name}' from TypedDict `{typed_dict_name}`", )); @@ -3416,8 +3475,9 @@ pub(crate) fn report_cannot_delete_typed_dict_key<'db>( let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, key_node) else { return; }; + let env = &context.program_environment(); - let typed_dict_name = Type::TypedDict(typed_dict_ty).display(db); + let typed_dict_name = Type::TypedDict(typed_dict_ty).display(db, env); let mut diagnostic = match error_kind { TypedDictDeleteErrorKind::RequiredKey => builder.into_diagnostic(format_args!( @@ -3436,7 +3496,7 @@ pub(crate) fn report_cannot_delete_typed_dict_key<'db>( && let Some(declaration) = field.first_declaration() { let file = declaration.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, declaration.python_file(db)).load(db); let mut sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Field defined here"); for message in [ @@ -3481,7 +3541,7 @@ pub(crate) fn report_invalid_type_param_order<'db>( let db = context.db(); let base_index = class - .explicit_bases(db) + .explicit_bases(context.db()) .iter() .position(|base| { matches!( @@ -3545,10 +3605,9 @@ pub(crate) fn report_invalid_type_param_order<'db>( let Some(definition) = tvar.definition(db) else { continue; }; - let file = definition.file(db); diagnostic.annotate( Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), + definition.full_range(db, &parsed_module(db, definition.python_file(db)).load(db)), )) .message(format_args!("`{}` defined here", tvar.name(db))), ); @@ -3592,10 +3651,9 @@ pub(crate) fn report_invalid_typevar_default_reference<'db>( let Some(definition) = tvar.definition(db) else { continue; }; - let file = definition.file(db); diagnostic.annotate( Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), + definition.full_range(db, &parsed_module(db, definition.python_file(db)).load(db)), )) .message(format_args!("`{}` defined here", tvar.name(db))), ); @@ -3623,6 +3681,7 @@ pub(crate) fn report_inconsistent_generic_bases<'db>( base_nodes: Option<&[ast::Expr]>, ) -> bool { let db = context.db(); + let env = &context.program_environment(); // Maps each generic ancestor's class literal to the first // specialization seen and the index of the explicit base it // came from. @@ -3674,41 +3733,41 @@ pub(crate) fn report_inconsistent_generic_bases<'db>( ) { diagnostic.annotate(context.secondary(earlier_base).message(format_args!( "Earlier class base inherits from `{}`", - earlier_alias.display(db) + earlier_alias.display(db, env) ))); let later_annotation = context.secondary(later_base); diagnostic.annotate(if later_is_direct { later_annotation.message(format_args!( "Later class base is `{}`", - supercls_alias.display(db) + supercls_alias.display(db, env) )) } else { later_annotation.message(format_args!( "Later class base inherits from `{}`", - supercls_alias.display(db) + supercls_alias.display(db, env) )) }); } else { diagnostic.info(format_args!( "Earlier class base inherits from `{}`", - earlier_alias.display(db) + earlier_alias.display(db, env) )); if later_is_direct { diagnostic.info(format_args!( "Later class base is `{}`", - supercls_alias.display(db) + supercls_alias.display(db, env) )); } else { diagnostic.info(format_args!( "Later class base inherits from `{}`", - supercls_alias.display(db) + supercls_alias.display(db, env) )); } } diagnostic.set_concise_message(format_args!( "Inconsistent type arguments: class cannot inherit from both `{}` and `{}`", - supercls_alias.display(db), - earlier_alias.display(db) + supercls_alias.display(db, env), + earlier_alias.display(db, env) )); return true; } @@ -3759,7 +3818,7 @@ pub(crate) fn report_shadowed_type_variable<'db>( let Some(other_definition) = other_typevar.binding_context(db).definition() else { return; }; - let span = match binding_type(db, other_definition) { + let span = match binding_type(context.db(), other_definition) { Type::ClassLiteral(class) => class.header_span(db), Type::FunctionLiteral(function) => function.spans(db).signature, _ => return, @@ -3833,8 +3892,9 @@ pub(super) fn report_invalid_method_override<'db>( "Definition is incompatible with `{overridden_method}`" )); + let env = &context.program_environment(); let class_member = |cls: ClassType<'db>| { - cls.class_member(db, member, MemberLookupPolicy::default()) + cls.class_member(db, env, member, MemberLookupPolicy::default()) .place }; @@ -3860,7 +3920,7 @@ pub(super) fn report_invalid_method_override<'db>( )); } - error_context().attach_to(context.db(), &mut diagnostic); + error_context().attach_to(db, env, &mut diagnostic); diagnostic.info("This violates the Liskov Substitution Principle"); @@ -3884,10 +3944,10 @@ pub(super) fn report_invalid_method_override<'db>( .next() && let Some(definition) = binding.binding.definition() { - let definition_span = Span::from( - definition - .full_range(db, &parsed_module(db, superclass_scope.file(db)).load(db)), - ); + let definition_span = Span::from(definition.full_range( + db, + &parsed_module(db, superclass_scope.python_file(db)).load(db), + )); let superclass_function_span = match superclass_type { Type::FunctionLiteral(function) => Some(signature_span(function)), @@ -4014,14 +4074,14 @@ pub(super) fn report_incompatible_base_method<'db>( contract_decorator.description(), )); } - error_context().attach_to(db, &mut diagnostic); + error_context().attach_to(db, context.program_environment(), &mut diagnostic); diagnostic.info("This violates the Liskov Substitution Principle"); for (definition, owner_name) in [ (selected_definition, selected_name), (contract_definition, contract_name), ] { - let module = parsed_module(db, definition.file(db)).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); diagnostic.annotate( Annotation::secondary(Span::from(definition.focus_range(db, &module))) .message(format_args!("`{owner_name}.{member}` defined here")), @@ -4040,7 +4100,6 @@ pub(super) fn report_overridden_final_method<'db>( superclass_method_defs: &[FunctionType<'db>], ) { let db = context.db(); - // Some hijinks so that we emit a diagnostic on the property getter rather than the property setter let property_getter_definition = if subclass_definition.kind(db).is_function_def() && let Type::PropertyInstance(property) = subclass_type @@ -4105,13 +4164,13 @@ pub(super) fn report_overridden_final_method<'db>( sub.annotate( Annotation::secondary(Span::from(superclass_function_literal.focus_range( db, - &parsed_module(db, first_final_superclass_definition.file(db)).load(db), + &parsed_module(db, first_final_superclass_definition.python_file(db)).load(db), ))) .message(format_args!("`{superclass_name}.{member}` defined here")), ); if let Some(decorator_span) = - superclass_function_literal.find_known_decorator_span(db, KnownFunction::Final) + superclass_function_literal.find_known_decorator_span(context.db(), KnownFunction::Final) { sub.annotate(Annotation::secondary(decorator_span)); } @@ -4256,10 +4315,10 @@ pub(super) fn report_overridden_final_variable<'db>( ), ); sub.annotate( - Annotation::secondary(Span::from( - superclass_def - .focus_range(db, &parsed_module(db, superclass_def.file(db)).load(db)), - )) + Annotation::secondary(Span::from(superclass_def.focus_range( + db, + &parsed_module(db, superclass_def.python_file(db)).load(db), + ))) .message(format_args!("`{superclass_name}.{member}` defined here")), ); diagnostic.sub(sub); @@ -4280,43 +4339,44 @@ pub(super) fn report_unsupported_comparison<'db>( right_ty: Type<'db>, ) { let db = context.db(); - let Some(diagnostic_builder) = context.report_lint(&UNSUPPORTED_OPERATOR, range) else { return; }; + let env = &context.program_environment(); let display_settings = DisplaySettings::from_possibly_ambiguous_types( db, + env, [error.left_ty, error.right_ty, left_ty, right_ty], ); let mut diagnostic = diagnostic_builder.into_diagnostic(format_args!("Unsupported `{}` operation", error.op)); - if left_ty.is_equivalent_to(db, right_ty) { + if left_ty.is_equivalent_to(db, env, right_ty) { diagnostic.set_primary_annotation_message(format_args!( "Both operands have type `{}`", - left_ty.display_with(db, display_settings.clone()) + left_ty.display_with(db, env, display_settings.clone()) )); diagnostic.annotate(context.secondary(left)); diagnostic.annotate(context.secondary(right)); diagnostic.set_concise_message(format_args!( "Operator `{}` is not supported between two objects of type `{}`", error.op, - left_ty.display_with(db, display_settings.clone()) + left_ty.display_with(db, env, display_settings.clone()) )); } else { for (ty, expr) in [(left_ty, left), (right_ty, right)] { diagnostic.annotate(context.secondary(expr).message(format_args!( "Has type `{}`", - ty.display_with(db, display_settings.clone()) + ty.display_with(db, env, display_settings.clone()) ))); } diagnostic.set_concise_message(format_args!( "Operator `{}` is not supported between objects of type `{}` and `{}`", error.op, - left_ty.display_with(db, display_settings.clone()), - right_ty.display_with(db, display_settings.clone()) + left_ty.display_with(db, env, display_settings.clone()), + right_ty.display_with(db, env, display_settings.clone()) )); } @@ -4329,8 +4389,9 @@ pub(super) fn report_unsupported_comparison<'db>( // - `error.left_ty` is `Literal["foo"]` // - `error.right_ty` is `Literal[3]` if (error.left_ty, error.right_ty) != (left_ty, right_ty) { - if let Some(TupleSpec::Fixed(lhs_spec)) = left_ty.tuple_instance_spec(db).as_deref() - && let Some(TupleSpec::Fixed(rhs_spec)) = right_ty.tuple_instance_spec(db).as_deref() + if let Some(TupleSpec::Fixed(lhs_spec)) = left_ty.tuple_instance_spec(db, env).as_deref() + && let Some(TupleSpec::Fixed(rhs_spec)) = + right_ty.tuple_instance_spec(db, env).as_deref() && lhs_spec.len() == rhs_spec.len() && let Some(position) = lhs_spec .all_elements() @@ -4338,13 +4399,13 @@ pub(super) fn report_unsupported_comparison<'db>( .zip(rhs_spec.all_elements()) .position(|tup| tup == (&error.left_ty, &error.right_ty)) { - if error.left_ty.is_equivalent_to(db, error.right_ty) { + if error.left_ty.is_equivalent_to(db, env, error.right_ty) { diagnostic.info(format_args!( "Operation fails because operator `{}` is not supported between \ the tuple elements at index {} (both of type `{}`)", error.op, position + 1, - error.left_ty.display_with(db, display_settings), + error.left_ty.display_with(db, env, display_settings), )); } else { diagnostic.info(format_args!( @@ -4352,25 +4413,29 @@ pub(super) fn report_unsupported_comparison<'db>( the tuple elements at index {} (of type `{}` and `{}`)", error.op, position + 1, - error.left_ty.display_with(db, display_settings.clone()), - error.right_ty.display_with(db, display_settings), + error + .left_ty + .display_with(db, env, display_settings.clone()), + error.right_ty.display_with(db, env, display_settings), )); } } else { - if error.left_ty.is_equivalent_to(db, error.right_ty) { + if error.left_ty.is_equivalent_to(db, env, error.right_ty) { diagnostic.info(format_args!( "Operation fails because operator `{}` is not supported \ between two objects of type `{}`", error.op, - error.left_ty.display_with(db, display_settings), + error.left_ty.display_with(db, env, display_settings), )); } else { diagnostic.info(format_args!( "Operation fails because operator `{}` is not supported \ between objects of type `{}` and `{}`", error.op, - error.left_ty.display_with(db, display_settings.clone()), - error.right_ty.display_with(db, display_settings) + error + .left_ty + .display_with(db, env, display_settings.clone()), + error.right_ty.display_with(db, env, display_settings) )); } } @@ -4445,33 +4510,35 @@ fn report_unsupported_binary_operation_impl<'a>( ) -> Option> { let db = context.db(); let diagnostic_builder = context.report_lint(&UNSUPPORTED_OPERATOR, range)?; - let display_settings = DisplaySettings::from_possibly_ambiguous_types(db, [left_ty, right_ty]); + let env = &context.program_environment(); + let display_settings = + DisplaySettings::from_possibly_ambiguous_types(db, env, [left_ty, right_ty]); let mut diagnostic = diagnostic_builder.into_diagnostic(format_args!("Unsupported `{operator}` operation")); - if left_ty.is_equivalent_to(db, right_ty) { + if left_ty.is_equivalent_to(db, env, right_ty) { diagnostic.set_primary_annotation_message(format_args!( "Both operands have type `{}`", - left_ty.display_with(db, display_settings.clone()) + left_ty.display_with(db, env, display_settings.clone()) )); diagnostic.annotate(context.secondary(left)); diagnostic.annotate(context.secondary(right)); diagnostic.set_concise_message(format_args!( "Operator `{operator}` is not supported between two objects of type `{}`", - left_ty.display_with(db, display_settings.clone()) + left_ty.display_with(db, env, display_settings.clone()) )); } else { for (ty, expr) in [(left_ty, left), (right_ty, right)] { diagnostic.annotate(context.secondary(expr).message(format_args!( "Has type `{}`", - ty.display_with(db, display_settings.clone()) + ty.display_with(db, env, display_settings.clone()) ))); } diagnostic.set_concise_message(format_args!( "Operator `{operator}` is not supported between objects of type `{}` and `{}`", - left_ty.display_with(db, display_settings.clone()), - right_ty.display_with(db, display_settings.clone()) + left_ty.display_with(db, env, display_settings.clone()), + right_ty.display_with(db, env, display_settings.clone()) )); } @@ -4487,7 +4554,6 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( base_is_frozen: bool, ) { let db = context.db(); - let Some(builder) = context.report_lint(&INVALID_FROZEN_DATACLASS_SUBCLASS, class.header_range(db)) else { @@ -4546,7 +4612,7 @@ pub(super) fn report_bad_frozen_dataclass_inheritance<'db>( ); let base_class_file = base_class.file(db); - let module = parsed_module(db, base_class_file).load(db); + let module = parsed_module(db, base_class.python_file(db)).load(db); let decorator_range = base_class .body_scope(db) @@ -4635,14 +4701,14 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( return false; } - let program = Program::get(db); + let program = ty_python_core::program::Program::get(db); let typeshed_versions = program.search_paths(db).typeshed_versions(); let Some(version_range) = typeshed_versions.exact(full_submodule_name) else { return false; }; - let python_version = program.python_version(db); + let python_version = parent_module.python_version(db); if version_range.contains(python_version) { return false; } @@ -4682,7 +4748,7 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( return; }; let module = module_ty.module(db); - let Some(file) = module.file(db) else { + let Some(file) = module.python_file(db) else { return; }; let Some(search_path) = module.search_path(db) else { @@ -4718,14 +4784,16 @@ pub(super) fn report_invalid_concatenate_last_arg<'db>( last_arg: &ast::Expr, last_arg_type: Type<'db>, ) { + let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_TYPE_ARGUMENTS, last_arg) { + let env = &context.program_environment(); let mut diag = builder.into_diagnostic( "The last argument to `typing.Concatenate` must be either `...` or a `ParamSpec` \ type variable", ); diag.set_primary_annotation_message(format_args!( "Got `{}`", - last_arg_type.display(context.db()) + last_arg_type.display(db, env) )); } } @@ -4749,7 +4817,7 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( let class_name = class.name(db); let mut diagnostic = builder.into_diagnostic(format_args!("Invalid definition of class `{class_name}`")); - + let env = &context.program_environment(); let class_and_def = class .iter_mro(db, None) .filter_map(|base| base.into_class()?.class_literal(db).as_static()) @@ -4759,7 +4827,7 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( let symbol = place_table.symbol_id("__init_subclass__")?; let use_def = use_def_map(db, scope); let bindings = use_def.end_of_scope_bindings(ScopedPlaceId::Symbol(symbol)); - let place_with_def = place_from_bindings(db, bindings); + let place_with_def = place_from_bindings(db, env, bindings); if place_with_def.place.is_undefined() { return None; } @@ -4771,7 +4839,7 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( diagnostic.set_primary_annotation_message(format_args!( "Superclass `{superclass_name}` cannot be subclassed", )); - let definition_module = parsed_module(db, definition.file(db)); + let definition_module = parsed_module(db, definition.python_file(db)); let mut annotation = Annotation::secondary(Span::from( definition.focus_range(db, &definition_module.load(db)), )); @@ -4783,7 +4851,7 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( annotation = annotation.message(format_args!( "`{superclass_name}.__init_subclass__` has type `{}`, \ which is not callable", - bindings.callable_type().display(db) + bindings.callable_type().display(db, env) )); } else { diagnostic.set_concise_message(format_args!( @@ -4793,7 +4861,7 @@ pub(super) fn report_subclass_of_class_with_non_callable_init_subclass<'db>( annotation = annotation.message(format_args!( "`{superclass_name}.__init_subclass__` has type `{}`, \ which may not be callable", - bindings.callable_type().display(db) + bindings.callable_type().display(db, env) )); } diagnostic.annotate(annotation); diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index e95f2efb36..3f1c3d73c9 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1,12 +1,15 @@ //! Display implementations for types. +use crate::ProgramEnvironment; use std::borrow::Cow; use std::cell::RefCell; use std::collections::hash_map::Entry; use std::fmt::{self, Display, Formatter, Write}; use std::rc::Rc; +use ruff_db::PythonFile; use ruff_db::files::FilePath; +use ruff_db::parsed::parsed_module; use ruff_db::source::{line_index, source_text}; use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_literal::escape::AsciiEscape; @@ -14,7 +17,6 @@ use ruff_source_file::LineColumn; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; -use ruff_db::parsed::parsed_module; use ty_module_resolver::file_to_module; use crate::Db; @@ -206,13 +208,17 @@ impl<'db> DisplaySettings<'db> { } #[must_use] - pub(crate) fn from_possibly_ambiguous_types(db: &'db dyn Db, types: I) -> Self + pub(crate) fn from_possibly_ambiguous_types( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + types: I, + ) -> Self where I: IntoIterator, T: Into>, { fn build_display_settings<'db>( - collector: &AmbiguousNameCollector<'db>, + collector: &AmbiguousNameCollector<'_, 'db>, ) -> DisplaySettings<'db> { // Both classes and type aliases use the same qualification map since // a class and type alias with the same name need to be disambiguated. @@ -224,7 +230,11 @@ impl<'db> DisplaySettings<'db> { } } - let collector = AmbiguousNameCollector::default(); + let collector = AmbiguousNameCollector { + env, + visited_types: RefCell::default(), + names: RefCell::default(), + }; for ty in types { collector.visit_type(db, ty.into()); @@ -471,13 +481,13 @@ impl QualificationLevel { } } -#[derive(Debug, Default)] -struct AmbiguousNameCollector<'db> { +struct AmbiguousNameCollector<'a, 'db> { + env: &'a ProgramEnvironment<'db>, visited_types: RefCell>>, names: RefCell>>, } -impl<'db> AmbiguousNameCollector<'db> { +impl<'db> AmbiguousNameCollector<'_, 'db> { /// Records an item for ambiguity tracking. /// /// This updates the ambiguity state for items with the same name: @@ -561,7 +571,11 @@ enum AmbiguityState<'db> { RequiresFileAndLineNumber, } -impl<'db> TypeVisitor<'db> for AmbiguousNameCollector<'db> { +impl<'db> TypeVisitor<'db> for AmbiguousNameCollector<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -600,42 +614,56 @@ impl<'db> TypeVisitor<'db> for AmbiguousNameCollector<'db> { } impl<'db> Type<'db> { - pub fn display(self, db: &'db dyn Db) -> DisplayType<'db> { + pub fn display<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> DisplayType<'env, 'db> { DisplayType { ty: self, - settings: DisplaySettings::from_possibly_ambiguous_types(db, [self]), + settings: DisplaySettings::from_possibly_ambiguous_types(db, env, [self]), db, + env, } } - pub fn display_with(self, db: &'db dyn Db, settings: DisplaySettings<'db>) -> DisplayType<'db> { + pub fn display_with<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + settings: DisplaySettings<'db>, + ) -> DisplayType<'env, 'db> { DisplayType { ty: self, db, + env, settings, } } - fn representation( + fn representation<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayRepresentation<'db> { + ) -> DisplayRepresentation<'env, 'db> { DisplayRepresentation { db, + env, ty: self, settings, } } } -pub struct DisplayType<'db> { +pub struct DisplayType<'env, 'db> { ty: Type<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> DisplayType<'db> { +impl<'db> DisplayType<'_, 'db> { pub fn to_string_parts(&self) -> TypeDisplayDetails<'db> { let mut f = TypeWriter::Details(TypeDetailsWriter::new()); self.fmt_detailed(&mut f).unwrap(); @@ -647,9 +675,10 @@ impl<'db> DisplayType<'db> { } } -impl<'db> FmtDetailed<'db> for DisplayType<'db> { +impl<'db> FmtDetailed<'db> for DisplayType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - let representation = self.ty.representation(self.db, self.settings.clone()); + let db = self.db; + let representation = self.ty.representation(db, self.env, self.settings.clone()); match self.ty.as_literal_value_kind() { Some( LiteralValueTypeKind::Int(_) @@ -669,13 +698,13 @@ impl<'db> FmtDetailed<'db> for DisplayType<'db> { } } -impl Display for DisplayType<'_> { +impl Display for DisplayType<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } -impl fmt::Debug for DisplayType<'_> { +impl fmt::Debug for DisplayType<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(self, f) } @@ -713,7 +742,7 @@ fn fmt_file_location<'db>( /// A vector of path components in order (e.g., `["module", "OuterClass", "InnerClass"]`) pub(super) fn qualified_name_components_from_scope( db: &dyn Db, - file: ruff_db::files::File, + file: PythonFile<'_>, file_scope_id: FileScopeId, skip_count: usize, ) -> Vec { @@ -809,14 +838,20 @@ impl<'db> TypeAliasType<'db> { } /// Returns a source-style display of this type alias's declaration. - pub fn display_declaration(self, db: &'db dyn Db) -> impl Display + 'db { + pub fn display_declaration<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> impl Display + 'env { let value_ty = self.raw_value_type(db); DisplayTypeAliasDeclaration { db, + env, type_alias: self, value_ty, settings: DisplaySettings::from_possibly_ambiguous_types( db, + env, [Type::TypeAlias(self), value_ty], ), } @@ -848,7 +883,10 @@ impl<'db> FmtDetailed<'db> for TypeAliasDisplay<'db> { let definition = self.type_alias.definition(self.db); let file = definition.file(self.db); let offset = definition - .focus_range(self.db, &parsed_module(self.db, file).load(self.db)) + .focus_range( + self.db, + &parsed_module(self.db, definition.python_file(self.db)).load(self.db), + ) .range() .start(); fmt_file_location(self.db, file, offset, f)?; @@ -864,35 +902,37 @@ impl Display for TypeAliasDisplay<'_> { } /// A source-style display of a type alias declaration. -struct DisplayTypeAliasDeclaration<'db> { +struct DisplayTypeAliasDeclaration<'env, 'db> { db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, type_alias: TypeAliasType<'db>, value_ty: Type<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplayTypeAliasDeclaration<'db> { +impl<'db> FmtDetailed<'db> for DisplayTypeAliasDeclaration<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - let generic_context = self.type_alias.generic_context(self.db); + let db = self.db; + let generic_context = self.type_alias.generic_context(db); let settings = self .settings - .with_generic_context(self.db, generic_context.as_ref()); + .with_generic_context(db, generic_context.as_ref()); f.write_str("type ")?; self.type_alias - .display_with(self.db, settings.clone()) + .display_with(db, settings.clone()) .fmt_detailed(f)?; if let Some(generic_context) = generic_context { - generic_context.display(self.db).fmt_detailed(f)?; + generic_context.display(db).fmt_detailed(f)?; } f.write_str(" = ")?; self.value_ty - .display_with(self.db, settings) + .display_with(db, self.env, settings) .fmt_detailed(f) } } -impl Display for DisplayTypeAliasDeclaration<'_> { +impl Display for DisplayTypeAliasDeclaration<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } @@ -901,6 +941,7 @@ impl Display for DisplayTypeAliasDeclaration<'_> { /// Helper for displaying `TypeGuardLike` types `TypeIs` and `TypeGuard`. fn fmt_type_guard_like<'db, T: TypeGuardLike<'db>>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, guard: T, settings: &DisplaySettings<'db>, f: &mut TypeWriter<'_, '_, 'db>, @@ -910,7 +951,7 @@ fn fmt_type_guard_like<'db, T: TypeGuardLike<'db>>( f.write_char('[')?; guard .type_argument(db) - .display_with(db, settings.singleline()) + .display_with(db, env, settings.singleline()) .fmt_detailed(f)?; if let Some(name) = guard.place_name(db) { f.set_invalid_type_annotation(); @@ -923,9 +964,10 @@ fn fmt_type_guard_like<'db, T: TypeGuardLike<'db>>( /// Writes the string representation of a type, which is the value displayed either as /// `Literal[]` or `Literal[, ]` for literal types or as `` for /// non literals -struct DisplayRepresentation<'db> { +struct DisplayRepresentation<'env, 'db> { ty: Type<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } @@ -937,14 +979,15 @@ fn property_display_name(db: &dyn Db, property: PropertyInstanceType<'_>) -> &'s } } -impl Display for DisplayRepresentation<'_> { +impl Display for DisplayRepresentation<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } -impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { +impl<'db> FmtDetailed<'db> for DisplayRepresentation<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; match self.ty { Type::Dynamic(dynamic) => { if dynamic.is_todo() { @@ -955,34 +998,34 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { Type::Divergent(_) => f.with_type(self.ty).write_str("Divergent"), Type::Never => f.with_type(self.ty).write_str("Never"), Type::NominalInstance(instance) => { - let class = instance.class(self.db); + let class = instance.class(db, self.env); - match (class, class.known(self.db)) { + match (class, class.known(db)) { (_, Some(KnownClass::NoneType)) => f.with_type(self.ty).write_str("None"), (_, Some(KnownClass::NoDefaultType)) => f.with_type(self.ty).write_str("NoDefault"), (ClassType::Generic(alias), Some(KnownClass::Tuple)) => alias - .specialization(self.db) - .tuple(self.db) + .specialization(db) + .tuple(db) .expect("Specialization::tuple() should always return `Some()` for `KnownClass::Tuple`") - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), (ClassType::NonGeneric(class), _) => { - class.display_with(self.db, self.settings.clone()).fmt_detailed(f) + class.display_with(db, self.settings.clone()).fmt_detailed(f) }, - (ClassType::Generic(alias), _) => alias.display_with(self.db, self.settings.clone()).fmt_detailed(f), + (ClassType::Generic(alias), _) => alias.display_with(db, self.env, self.settings.clone()).fmt_detailed(f), } } Type::ProtocolInstance(protocol) => match protocol.inner { Protocol::FromClass(class) => match *class { ClassType::NonGeneric(class) => class - .display_with(self.db, self.settings.clone()) + .display_with(db, self.settings.clone()) .fmt_detailed(f), ClassType::Generic(alias) => alias - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), }, Protocol::Materialized(materialized) => { - let materialization_kind = protocol.display_materialization_kind(self.db); + let materialization_kind = protocol.display_materialization_kind(db, self.env); if let Some(kind) = materialization_kind { let (name, form) = match kind { MaterializationKind::Top => ("Top", SpecialFormType::Top), @@ -992,12 +1035,12 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f.write_char('[')?; } - match *materialized.origin(self.db) { + match *materialized.origin(db) { ClassType::NonGeneric(class) => class - .display_with(self.db, self.settings.clone()) + .display_with(db, self.settings.clone()) .fmt_detailed(f), ClassType::Generic(alias) => alias - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), }?; @@ -1013,7 +1056,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { .write_str("Protocol")?; f.write_str(" with members ")?; let interface = synthetic.interface(); - let member_list = interface.members(self.db); + let member_list = interface.members(db); let num_members = member_list.len(); for (i, member) in member_list.enumerate() { let is_last = i == num_members - 1; @@ -1027,15 +1070,14 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { }, Type::PropertyInstance(property) => f .with_type(self.ty) - .write_str(property_display_name(self.db, property)), + .write_str(property_display_name(db, property)), Type::ModuleLiteral(module) => { f.set_invalid_type_annotation(); f.write_char('<')?; - f.with_type(KnownClass::ModuleType.to_class_literal(self.db)) + f.with_type(KnownClass::ModuleType.to_class_literal(db, self.env)) .write_str("module")?; f.write_str(" '")?; - f.with_type(self.ty) - .write_str(module.module(self.db).name(self.db))?; + f.with_type(self.ty).write_str(module.module(db).name(db))?; f.write_str("'>") } Type::ClassLiteral(class) => { @@ -1043,7 +1085,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { let mut f = f.with_type(self.ty); f.write_str("") } @@ -1052,56 +1094,56 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { let mut f = f.with_type(self.ty); f.write_str("") } Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { SubclassOfInner::Class(ClassType::NonGeneric(class)) => { - f.with_type(KnownClass::Type.to_class_literal(self.db)) + f.with_type(KnownClass::Type.to_class_literal(db, self.env)) .write_str("type")?; f.write_char('[')?; class - .display_with(self.db, self.settings.clone()) + .display_with(db, self.settings.clone()) .fmt_detailed(f)?; f.write_char(']') } SubclassOfInner::Class(ClassType::Generic(alias)) => { - f.with_type(KnownClass::Type.to_class_literal(self.db)) + f.with_type(KnownClass::Type.to_class_literal(db, self.env)) .write_str("type")?; f.write_char('[')?; alias - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; f.write_char(']') } SubclassOfInner::Dynamic(dynamic) => { - f.with_type(KnownClass::Type.to_class_literal(self.db)) + f.with_type(KnownClass::Type.to_class_literal(db, self.env)) .write_str("type")?; f.write_char('[')?; write!(f.with_type(Type::Dynamic(dynamic)), "{dynamic}")?; f.write_char(']') } SubclassOfInner::Protocol(protocol) => { - f.with_type(KnownClass::Type.to_class_literal(self.db)) + f.with_type(KnownClass::Type.to_class_literal(db, self.env)) .write_str("type")?; f.write_char('[')?; Type::ProtocolInstance(protocol) - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; f.write_char(']') } SubclassOfInner::TypeVar(bound_typevar) => { f.set_invalid_type_annotation(); - f.with_type(KnownClass::Type.to_class_literal(self.db)) + f.with_type(KnownClass::Type.to_class_literal(db, self.env)) .write_str("type")?; f.write_char('[')?; write!( f.with_type(Type::TypeVar(bound_typevar)), "{}", bound_typevar - .identity(self.db) - .display_with(self.db, self.settings.clone()) + .identity(db) + .display_with(db, self.settings.clone()) )?; f.write_char(']') } @@ -1111,40 +1153,42 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { write!(f.with_type(self.ty), "") } Type::KnownInstance(known_instance) => known_instance - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), Type::FunctionLiteral(function) => function - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), Type::Callable(callable) => callable - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), Type::BoundMethod(bound_method) => { - let function = bound_method.function(self.db); - let self_ty = bound_method.self_instance(self.db); - let bound_signatures = bound_method.bound_signatures(self.db); + let function = bound_method.function(db); + let self_ty = bound_method.self_instance(db); + let bound_signatures = bound_method.bound_signatures(db); match bound_signatures.overloads.as_slice() { [signature] => { - let hide_unused_self = signature.should_hide_self_from_display(self.db); + let hide_unused_self = + signature.should_hide_self_from_display(db, self.env); let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), - db: self.db, + db, hide_unused_self, }; f.set_invalid_type_annotation(); f.write_str("bound method ")?; DisplayMaybeParenthesizedType { ty: self_ty, - db: self.db, + db, + env: self.env, settings: self.settings.singleline(), } .fmt_detailed(f)?; f.write_char('.')?; - f.with_type(self.ty).write_str(function.name(self.db))?; + f.with_type(self.ty).write_str(function.name(db))?; type_parameters.fmt_detailed(f)?; signature - .display_with(self.db, self.settings.disallow_signature_name()) + .display_with(db, self.env, self.settings.disallow_signature_name()) .fmt_detailed(f) } signatures => { @@ -1158,7 +1202,11 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { let separator = if self.settings.multiline { "\n" } else { ", " }; let mut join = f.join(separator); for signature in signatures { - join.entry(&signature.display_with(self.db, self.settings.clone())); + join.entry(&signature.display_with( + db, + self.env, + self.settings.clone(), + )); } join.finish()?; if !self.settings.multiline { @@ -1176,44 +1224,44 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { "__get__", "function", Type::FunctionLiteral(function), - Some(&**function.name(self.db)), + Some(&**function.name(db)), ), KnownBoundMethodType::FunctionTypeDunderCall(function) => ( KnownClass::FunctionType, "__call__", "function", Type::FunctionLiteral(function), - Some(&**function.name(self.db)), + Some(&**function.name(db)), ), KnownBoundMethodType::PropertyDunderGet(property) => ( - property.instance_class(self.db), + property.instance_class(db), "__get__", - property_display_name(self.db, property), + property_display_name(db, property), Type::PropertyInstance(property), property - .getter(self.db) + .getter(db) .and_then(Type::as_function_literal) - .map(|getter| &**getter.name(self.db)), + .map(|getter| &**getter.name(db)), ), KnownBoundMethodType::PropertyDunderSet(property) => ( - property.instance_class(self.db), + property.instance_class(db), "__set__", - property_display_name(self.db, property), + property_display_name(db, property), Type::PropertyInstance(property), property - .setter(self.db) + .setter(db) .and_then(Type::as_function_literal) - .map(|setter| &**setter.name(self.db)), + .map(|setter| &**setter.name(db)), ), KnownBoundMethodType::PropertyDunderDelete(property) => ( - property.instance_class(self.db), + property.instance_class(db), "__delete__", - property_display_name(self.db, property), + property_display_name(db, property), Type::PropertyInstance(property), property - .deleter(self.db) + .deleter(db) .and_then(Type::as_function_literal) - .map(|deleter| &**deleter.name(self.db)), + .map(|deleter| &**deleter.name(db)), ), KnownBoundMethodType::StrStartswith(literal) => ( KnownClass::Property, @@ -1222,7 +1270,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { Type::LiteralValue(LiteralValueType::promotable( LiteralValueTypeKind::String(literal), )), - Some(literal.value(self.db)), + Some(literal.value(db)), ), KnownBoundMethodType::ConstraintSetRange => { return f.write_str("bound method `ConstraintSet.range`"); @@ -1260,13 +1308,13 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } }; - let class_ty = cls.to_class_literal(self.db); + let class_ty = cls.to_class_literal(db, self.env); f.write_char('<')?; - f.with_type(KnownClass::MethodWrapperType.to_class_literal(self.db)) + f.with_type(KnownClass::MethodWrapperType.to_class_literal(db, self.env)) .write_str("method-wrapper")?; f.write_str(" '")?; if let Place::Defined(DefinedPlace { ty: member_ty, .. }) = - class_ty.member(self.db, member_name).place + class_ty.member(db, self.env, member_name).place { f.with_type(member_ty).write_str(member_name)?; } else { @@ -1299,12 +1347,12 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } }; f.write_char('<')?; - f.with_type(KnownClass::WrapperDescriptorType.to_class_literal(self.db)) + f.with_type(KnownClass::WrapperDescriptorType.to_class_literal(db, self.env)) .write_str("wrapper-descriptor")?; f.write_str(" '")?; f.write_str(method)?; f.write_str("' of '")?; - f.with_type(cls.to_class_literal(self.db)) + f.with_type(cls.to_class_literal(db, self.env)) .write_str(object)?; f.write_str("' objects>") } @@ -1317,25 +1365,26 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f.write_str("") } Type::Union(union) => union - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), Type::Intersection(intersection) => intersection - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), Type::EnumComplement(complement) => { if let Some(literals) = - complement.remaining_literal_types_for_display(self.db, LITERAL_POLICY.max) + complement.remaining_literal_types_for_display(db, self.env, LITERAL_POLICY.max) { DisplayLiteralGroup { literals, - db: self.db, + db, + env: self.env, settings: self.settings.clone(), } .fmt_detailed(f) } else { complement - .to_intersection(self.db) - .display_with(self.db, self.settings.clone()) + .to_intersection(db, self.env) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f) } } @@ -1346,7 +1395,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { .write_str(if boolean { "True" } else { "False" }) } LiteralValueTypeKind::String(string) => { - write!(f.with_type(self.ty), "{}", string.display(self.db)) + write!(f.with_type(self.ty), "{}", string.display(db)) } // We used to return `str` as the type here because that feels generally more useful. // However, the inconsistency between the type shown in the inlay hint and its hover, and the @@ -1356,8 +1405,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { .with_type(Type::SpecialForm(SpecialFormType::LiteralString)) .write_str("LiteralString"), LiteralValueTypeKind::Bytes(bytes) => { - let escape = - AsciiEscape::with_preferred_quote(bytes.value(self.db), Quote::Double); + let escape = AsciiEscape::with_preferred_quote(bytes.value(db), Quote::Double); write!( f.with_type(self.ty), @@ -1367,14 +1415,14 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } LiteralValueTypeKind::Enum(enum_literal) => { enum_literal - .enum_class(self.db) - .display_with(self.db, self.settings.clone()) + .enum_class(db) + .display_with(db, self.settings.clone()) .fmt_detailed(f)?; f.write_char('.')?; write!( f.with_type(Type::enum_literal(enum_literal)), "{}", - enum_literal.name(self.db) + enum_literal.name(db) ) } }, @@ -1384,8 +1432,8 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f, "{}", bound_typevar - .identity(self.db) - .display_with(self.db, self.settings.clone()) + .identity(db) + .display_with(db, self.settings.clone()) ) } Type::AlwaysTruthy => f.with_type(self.ty).write_str("AlwaysTruthy"), @@ -1393,37 +1441,37 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { Type::BoundSuper(bound_super) => { f.set_invalid_type_annotation(); f.write_str("") } - Type::TypeIs(type_is) => fmt_type_guard_like(self.db, type_is, &self.settings, f), + Type::TypeIs(type_is) => fmt_type_guard_like(db, self.env, type_is, &self.settings, f), Type::TypeGuard(type_guard) => { - fmt_type_guard_like(self.db, type_guard, &self.settings, f) + fmt_type_guard_like(db, self.env, type_guard, &self.settings, f) } Type::TypeForm(typeform) => { f.with_type(Type::SpecialForm(SpecialFormType::TypeForm)) .write_str("TypeForm")?; f.write_char('[')?; typeform - .type_argument(self.db) - .display_with(self.db, self.settings.clone()) + .type_argument(db) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; f.write_char(']') } Type::TypedDict(TypedDictType::Class(defining_class)) => match defining_class { ClassType::NonGeneric(class) => class - .display_with(self.db, self.settings.clone()) + .display_with(db, self.settings.clone()) .fmt_detailed(f), ClassType::Generic(alias) => alias - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), }, Type::TypedDict(TypedDictType::Synthesized(synthesized)) => { @@ -1434,7 +1482,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { ))) .write_str("TypedDict")?; f.write_str(" with items ")?; - let items = synthesized.items(self.db); + let items = synthesized.items(db); for (i, name) in items.keys().enumerate() { let is_last = i == items.len() - 1; write!(f, "'{name}'")?; @@ -1446,16 +1494,16 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } Type::TypeAlias(alias) => { alias - .display_with(self.db, self.settings.clone()) + .display_with(db, self.settings.clone()) .fmt_detailed(f)?; - match alias.specialization(self.db) { + match alias.specialization(db) { None => Ok(()), Some(specialization) => specialization - .display_short(self.db, TupleSpecialization::No, self.settings.clone()) + .display_short(db, self.env, TupleSpecialization::No, self.settings.clone()) .fmt_detailed(f), } } - Type::NewTypeInstance(newtype) => f.with_type(self.ty).write_str(newtype.name(self.db)), + Type::NewTypeInstance(newtype) => f.with_type(self.ty).write_str(newtype.name(db)), } } } @@ -1505,11 +1553,13 @@ impl<'db> TupleSpec<'db> { fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplayTuple<'a, 'db> { DisplayTuple { tuple: self, db, + env, settings, } } @@ -1518,12 +1568,14 @@ impl<'db> TupleSpec<'db> { struct DisplayTuple<'a, 'db> { tuple: &'a TupleSpec<'db>, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - f.with_type(KnownClass::Tuple.to_class_literal(self.db)) + let db = self.db; + f.with_type(KnownClass::Tuple.to_class_literal(db, self.env)) .write_str("tuple")?; f.write_char('[')?; match self.tuple { @@ -1533,7 +1585,7 @@ impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { f.write_str("()")?; } else { elements - .display_with(self.db, self.settings.singleline()) + .display_with(db, self.env, self.settings.singleline()) .fmt_detailed(f)?; } } @@ -1556,7 +1608,7 @@ impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { if !tuple.prefix_elements().is_empty() { tuple .prefix_elements() - .display_with(self.db, self.settings.singleline()) + .display_with(db, self.env, self.settings.singleline()) .fmt_detailed(f)?; f.write_str(", ")?; } @@ -1564,7 +1616,7 @@ impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { VariableSegment::TypeVarTuple(typevar) => { f.write_char('*')?; Type::TypeVar(typevar) - .display_with(self.db, self.settings.singleline()) + .display_with(db, self.env, self.settings.singleline()) .fmt_detailed(f)?; } VariableSegment::Homogeneous(variable) => { @@ -1573,12 +1625,12 @@ impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { { f.write_char('*')?; // Might as well link the type again here too - f.with_type(KnownClass::Tuple.to_class_literal(self.db)) + f.with_type(KnownClass::Tuple.to_class_literal(db, self.env)) .write_str("tuple")?; f.write_char('[')?; } variable - .display_with(self.db, self.settings.singleline()) + .display_with(db, self.env, self.settings.singleline()) .fmt_detailed(f)?; f.write_str(", ...")?; if !tuple.prefix_elements().is_empty() @@ -1592,7 +1644,7 @@ impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { f.write_str(", ")?; tuple .suffix_elements() - .display_with(self.db, self.settings.singleline()) + .display_with(db, self.env, self.settings.singleline()) .fmt_detailed(f)?; } } @@ -1610,87 +1662,99 @@ impl Display for DisplayTuple<'_, '_> { impl<'db> OverloadLiteral<'db> { // Not currently used, but useful for debugging. #[expect(dead_code)] - fn display(self, db: &'db dyn Db) -> DisplayOverloadLiteral<'db> { - Self::display_with(self, db, DisplaySettings::default()) + fn display<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> DisplayOverloadLiteral<'env, 'db> { + Self::display_with(self, db, env, DisplaySettings::default()) } - fn display_with( + fn display_with<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayOverloadLiteral<'db> { + ) -> DisplayOverloadLiteral<'env, 'db> { DisplayOverloadLiteral { literal: self, db, + env, settings, } } } -pub(crate) struct DisplayOverloadLiteral<'db> { +pub(crate) struct DisplayOverloadLiteral<'env, 'db> { literal: OverloadLiteral<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplayOverloadLiteral<'db> { +impl<'db> FmtDetailed<'db> for DisplayOverloadLiteral<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - let signature = self.literal.signature(self.db); - let hide_unused_self = signature.should_hide_self_from_display(self.db); + let db = self.db; + let signature = self.literal.signature(db); + let hide_unused_self = signature.should_hide_self_from_display(db, self.env); let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), - db: self.db, + db, hide_unused_self, }; f.set_invalid_type_annotation(); f.write_str("def ")?; - write!(f, "{}", self.literal.name(self.db))?; + write!(f, "{}", self.literal.name(db))?; type_parameters.fmt_detailed(f)?; signature - .display_with(self.db, self.settings.disallow_signature_name()) + .display_with(db, self.env, self.settings.disallow_signature_name()) .fmt_detailed(f) } } -impl Display for DisplayOverloadLiteral<'_> { +impl Display for DisplayOverloadLiteral<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } impl<'db> FunctionType<'db> { - fn display_with( + fn display_with<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayFunctionType<'db> { + ) -> DisplayFunctionType<'env, 'db> { DisplayFunctionType { ty: self, db, + env, settings, } } } -struct DisplayFunctionType<'db> { +struct DisplayFunctionType<'env, 'db> { ty: FunctionType<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { +impl<'db> FmtDetailed<'db> for DisplayFunctionType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { // Detect self-referential function types to prevent infinite recursion, // and limit display depth for chains of different function types // (e.g. multiple redefinitions with `TypeOf[foo]` return types). const MAX_FUNCTION_TYPE_DISPLAY_DEPTH: usize = 4; + let db = self.db; if self.settings.visited_function_types.contains(&self.ty) || self.settings.visited_function_types.len() >= MAX_FUNCTION_TYPE_DISPLAY_DEPTH { f.set_invalid_type_annotation(); f.write_str("def ")?; - write!(f, "{}", self.ty.name(self.db))?; + write!(f, "{}", self.ty.name(db))?; return f.write_str("(...)"); } @@ -1699,23 +1763,23 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { visited.insert(self.ty); settings.visited_function_types = Rc::new(visited); - let signature = self.ty.signature(self.db); + let signature = self.ty.signature(db); match signature.overloads.as_slice() { [signature] => { - let hide_unused_self = signature.should_hide_self_from_display(self.db); + let hide_unused_self = signature.should_hide_self_from_display(db, self.env); let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), - db: self.db, + db, hide_unused_self, }; f.set_invalid_type_annotation(); f.write_str("def ")?; - write!(f, "{}", self.ty.name(self.db))?; + write!(f, "{}", self.ty.name(db))?; type_parameters.fmt_detailed(f)?; signature - .display_with(self.db, settings.disallow_signature_name()) + .display_with(db, self.env, settings.disallow_signature_name()) .fmt_detailed(f) } signatures => { @@ -1729,7 +1793,7 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { let separator = if settings.multiline { "\n" } else { ", " }; let mut join = f.join(separator); for signature in signatures { - join.entry(&signature.display_with(self.db, settings.clone())); + join.entry(&signature.display_with(db, self.env, settings.clone())); } join.finish()?; if !settings.multiline { @@ -1741,51 +1805,59 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { } } -impl Display for DisplayFunctionType<'_> { +impl Display for DisplayFunctionType<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } impl<'db> GenericAlias<'db> { - pub(crate) fn display(self, db: &'db dyn Db) -> DisplayGenericAlias<'db> { - self.display_with(db, DisplaySettings::default()) + pub(crate) fn display<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> DisplayGenericAlias<'env, 'db> { + self.display_with(db, env, DisplaySettings::default()) } - pub(crate) fn display_with( + pub(crate) fn display_with<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayGenericAlias<'db> { + ) -> DisplayGenericAlias<'env, 'db> { DisplayGenericAlias { origin: ClassLiteral::Static(self.origin(db)), specialization: self.specialization(db), db, + env, settings, } } } -pub(crate) struct DisplayGenericAlias<'db> { +pub(crate) struct DisplayGenericAlias<'env, 'db> { origin: ClassLiteral<'db>, specialization: Specialization<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'db> { +impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - if let Some(tuple) = self.specialization.tuple(self.db) { + let db = self.db; + if let Some(tuple) = self.specialization.tuple(db) { tuple - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f) } else { - let prefix_details = match self.specialization.materialization_kind(self.db) { + let prefix_details = match self.specialization.materialization_kind(db) { None => None, Some(MaterializationKind::Top) => Some(("Top", SpecialFormType::Top)), Some(MaterializationKind::Bottom) => Some(("Bottom", SpecialFormType::Bottom)), }; - let suffix = match self.specialization.materialization_kind(self.db) { + let suffix = match self.specialization.materialization_kind(db) { None => "", Some(_) => "]", }; @@ -1794,12 +1866,13 @@ impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'db> { f.write_char('[')?; } self.origin - .display_with(self.db, self.settings.clone()) + .display_with(db, self.settings.clone()) .fmt_detailed(f)?; self.specialization .display_short( - self.db, - TupleSpecialization::from_class(self.db, self.origin), + db, + self.env, + TupleSpecialization::from_class(db, self.origin), self.settings.clone(), ) .fmt_detailed(f)?; @@ -1808,7 +1881,7 @@ impl<'db> FmtDetailed<'db> for DisplayGenericAlias<'db> { } } -impl Display for DisplayGenericAlias<'_> { +impl Display for DisplayGenericAlias<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } @@ -1945,10 +2018,15 @@ impl Display for DisplayGenericContext<'_, '_> { } impl<'db> Specialization<'db> { - fn display_full(self, db: &'db dyn Db) -> DisplaySpecialization<'db> { + fn display_full<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> DisplaySpecialization<'env, 'db> { DisplaySpecialization { specialization: self, db, + env, tuple_specialization: TupleSpecialization::No, settings: DisplaySettings::default(), full: true, @@ -1956,15 +2034,17 @@ impl<'db> Specialization<'db> { } /// Renders the specialization as it would appear in a subscript expression, e.g. `[int, str]`. - fn display_short( + fn display_short<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, tuple_specialization: TupleSpecialization, settings: DisplaySettings<'db>, - ) -> DisplaySpecialization<'db> { + ) -> DisplaySpecialization<'env, 'db> { DisplaySpecialization { specialization: self, db, + env, tuple_specialization, settings, full: false, @@ -1972,31 +2052,33 @@ impl<'db> Specialization<'db> { } } -struct DisplaySpecialization<'db> { +struct DisplaySpecialization<'env, 'db> { specialization: Specialization<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, tuple_specialization: TupleSpecialization, settings: DisplaySettings<'db>, full: bool, } -impl<'db> DisplaySpecialization<'db> { +impl<'db> DisplaySpecialization<'_, 'db> { fn fmt_normal(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; f.write_char('[')?; let variables = self .specialization - .generic_context(self.db) - .variables(self.db) + .generic_context(db) + .variables(db) .collect::>(); - let types = self.specialization.types(self.db); + let types = self.specialization.types(db); let mut wrote_any = false; for (typevar, ty) in variables.iter().zip(types) { - if typevar.is_typevartuple(self.db) { - let Some(tuple) = ty.exact_tuple_instance_spec(self.db) else { + if typevar.is_typevartuple(db) { + let Some(tuple) = ty.exact_tuple_instance_spec(db) else { if wrote_any { f.write_str(", ")?; } - ty.display_with(self.db, self.settings.clone()) + ty.display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; wrote_any = true; continue; @@ -2017,7 +2099,7 @@ impl<'db> DisplaySpecialization<'db> { f.write_str(", ")?; } element - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; wrote_any = true; } @@ -2027,7 +2109,7 @@ impl<'db> DisplaySpecialization<'db> { f.write_str(", ")?; } f.write_char('*')?; - ty.display_with(self.db, self.settings.clone()) + ty.display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; wrote_any = true; } @@ -2038,7 +2120,7 @@ impl<'db> DisplaySpecialization<'db> { if wrote_any { f.write_str(", ")?; } - ty.display_with(self.db, self.settings.clone()) + ty.display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; wrote_any = true; } @@ -2049,27 +2131,25 @@ impl<'db> DisplaySpecialization<'db> { } fn fmt_full(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; f.write_char('[')?; - let variables = self - .specialization - .generic_context(self.db) - .variables(self.db); - let types = self.specialization.types(self.db); + let variables = self.specialization.generic_context(db).variables(db); + let types = self.specialization.types(db); for (idx, (bound_typevar, ty)) in variables.zip(types).enumerate() { if idx > 0 { f.write_str(", ")?; } f.set_invalid_type_annotation(); - write!(f, "{}", bound_typevar.identity(self.db).display(self.db))?; + write!(f, "{}", bound_typevar.identity(db).display(db))?; f.write_str(" = ")?; - ty.display_with(self.db, self.settings.clone()) + ty.display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; } f.write_char(']') } } -impl<'db> FmtDetailed<'db> for DisplaySpecialization<'db> { +impl<'db> FmtDetailed<'db> for DisplaySpecialization<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { if self.full { self.fmt_full(f) @@ -2079,7 +2159,7 @@ impl<'db> FmtDetailed<'db> for DisplaySpecialization<'db> { } } -impl Display for DisplaySpecialization<'_> { +impl Display for DisplaySpecialization<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } @@ -2106,19 +2186,25 @@ impl TupleSpecialization { } impl<'db> CallableType<'db> { - fn display<'a>(&'a self, db: &'db dyn Db) -> DisplayCallableType<'a, 'db> { - Self::display_with(self, db, DisplaySettings::default()) + fn display<'a>( + &'a self, + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + ) -> DisplayCallableType<'a, 'db> { + Self::display_with(self, db, env, DisplaySettings::default()) } fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplayCallableType<'a, 'db> { DisplayCallableType { signatures: self.signatures(db), kind: self.kind(db), db, + env, settings, } } @@ -2128,11 +2214,13 @@ pub(crate) struct DisplayCallableType<'a, 'db> { signatures: &'a CallableSignature<'db>, kind: CallableTypeKind, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; match self.signatures.overloads.as_slice() { [signature] => { if matches!(self.kind, CallableTypeKind::ParamSpecValue) { @@ -2141,14 +2229,14 @@ impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> { } signature .parameters() - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; if signature.parameters().is_top() { f.write_str("]")?; } } else { signature - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; } } @@ -2163,7 +2251,7 @@ impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> { let separator = if self.settings.multiline { "\n" } else { ", " }; let mut join = f.join(separator); for signature in signatures { - join.entry(&signature.display_with(self.db, self.settings.clone())); + join.entry(&signature.display_with(db, self.env, self.settings.clone())); } join.finish()?; if !self.settings.multiline { @@ -2183,13 +2271,18 @@ impl Display for DisplayCallableType<'_, '_> { } impl<'db> Signature<'db> { - pub(crate) fn display<'a>(&'a self, db: &'db dyn Db) -> DisplaySignature<'a, 'db> { - Self::display_with(self, db, DisplaySettings::default()) + pub(crate) fn display<'a>( + &'a self, + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + ) -> DisplaySignature<'a, 'db> { + Self::display_with(self, db, env, DisplaySettings::default()) } pub(crate) fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplaySignature<'a, 'db> { DisplaySignature { @@ -2198,6 +2291,7 @@ impl<'db> Signature<'db> { parameters: self.parameters(), return_ty: self.return_ty, db, + env, settings, } } @@ -2209,10 +2303,11 @@ pub(crate) struct DisplaySignature<'a, 'db> { parameters: &'a Parameters<'db>, return_ty: Type<'db>, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> DisplaySignature<'_, 'db> { +impl DisplaySignature<'_, '_> { /// Get detailed display information including component ranges pub(crate) fn to_string_parts(&self) -> SignatureDisplayDetails { let mut f = TypeWriter::Details(TypeDetailsWriter::new()); @@ -2224,17 +2319,20 @@ impl<'db> DisplaySignature<'_, 'db> { } } - fn should_hide_self_from_display(&self, db: &'db dyn Db) -> bool { - !self.return_ty.contains_self(db) - && !self - .parameters - .iter() - .any(|p| p.should_annotation_be_displayed() && p.annotated_type().contains_self(db)) + fn should_hide_self_from_display(&self) -> bool { + let db = self.db; + let env = self.env; + + !self.return_ty.contains_self(db, env) + && !self.parameters.iter().any(|p| { + p.should_annotation_be_displayed() && p.annotated_type().contains_self(db, env) + }) } } impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; // Immediately write a marker signaling we're starting a signature let _ = f.with_detail(TypeDetail::SignatureStart); f.set_invalid_type_annotation(); @@ -2252,15 +2350,13 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { .signature_name_display .should_display(self.settings.multiline) && let Some(definition) = self.definition - && let Some(name) = definition.name(self.db) + && let Some(name) = definition.name(db) { f.write_str("def ")?; f.write_str(&name)?; } - let settings = self - .settings - .with_generic_context(self.db, self.generic_context); + let settings = self.settings.with_generic_context(db, self.generic_context); // Display type parameters if present, but only when the caller hasn't // already displayed them. @@ -2269,11 +2365,11 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { .signature_name_display .allows_type_parameters() { - let hide_unused_self = self.should_hide_self_from_display(self.db); + let hide_unused_self = self.should_hide_self_from_display(); DisplayOptionalGenericContext { generic_context: self.generic_context, - db: self.db, + db, hide_unused_self, } .fmt_detailed(&mut f)?; @@ -2285,7 +2381,7 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { ..settings.clone() }; self.parameters - .display_with(self.db, param_settings) + .display_with(db, self.env, param_settings) .fmt_detailed(&mut f)?; // Return type @@ -2293,12 +2389,12 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { f.write_str(" -> ")?; let should_parenthesize_return_type = - should_parenthesize_callable_type(self.return_ty, self.db); + should_parenthesize_callable_type(self.return_ty, db); if should_parenthesize_return_type { f.write_char('(')?; } self.return_ty - .display_with(self.db, settings.singleline()) + .display_with(db, self.env, settings.singleline()) .fmt_detailed(&mut f)?; if should_parenthesize_return_type { f.write_char(')')?; @@ -2334,11 +2430,13 @@ impl<'db> Parameters<'db> { fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplayParameters<'a, 'db> { DisplayParameters { parameters: self, db, + env, settings, } } @@ -2347,6 +2445,7 @@ impl<'db> Parameters<'db> { struct DisplayParameters<'a, 'db> { parameters: &'a Parameters<'db>, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } @@ -2358,6 +2457,7 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> { parameters: &[Parameter<'db>], arg_separator: &str, ) -> fmt::Result { + let db = display.db; let mut star_added = false; let mut needs_slash = false; let mut after_synthetic_unpack = false; @@ -2399,7 +2499,7 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> { .map(|name| name.to_string()) .unwrap_or_default(); parameter - .display_with(display.db, display.settings.singleline()) + .display_with(db, display.env, display.settings.singleline()) .fmt_detailed(&mut f.with_detail(TypeDetail::Parameter(param_name)))?; after_synthetic_unpack |= is_synthetic_unpack; @@ -2415,6 +2515,7 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> { Ok(()) } + let db = self.db; // For `ParamSpec` kind, the parameters still contain `*args` and `**kwargs`, but we // display them as `**P` instead, so avoid multiline in that case. @@ -2465,11 +2566,11 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> { display_parameters(self, f, self.parameters.as_slice(), arg_separator)?; } ParametersKind::ParamSpec(typevar) => { - let parameter_name = format!("**{}", typevar.name(self.db)); + let parameter_name = format!("**{}", typevar.name(db)); let mut parameter = f.with_detail(TypeDetail::Parameter(parameter_name.clone())); write!(parameter, "{parameter_name}")?; - let binding_context = typevar.binding_context(self.db); - if let Some(binding_context_name) = binding_context.name(self.db) + let binding_context = typevar.binding_context(db); + if let Some(binding_context_name) = binding_context.name(db) && let Some(definition) = binding_context.definition() && !self.settings.active_scopes.contains(&definition) { @@ -2497,11 +2598,13 @@ impl<'db> Parameter<'db> { fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplayParameter<'a, 'db> { DisplayParameter { param: self, db, + env, settings, } } @@ -2510,11 +2613,13 @@ impl<'db> Parameter<'db> { struct DisplayParameter<'a, 'db> { param: &'a Parameter<'db>, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } impl<'db> FmtDetailed<'db> for DisplayParameter<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; if self.param.definition().is_none() && self.param.is_variadic() && self.param.has_starred_annotation() @@ -2522,7 +2627,7 @@ impl<'db> FmtDetailed<'db> for DisplayParameter<'_, 'db> { f.write_str("*")?; self.param .annotated_type() - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; return Ok(()); } @@ -2533,7 +2638,7 @@ impl<'db> FmtDetailed<'db> for DisplayParameter<'_, 'db> { let annotated_type = self.param.annotated_type(); f.write_str(": ")?; annotated_type - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; } // Default value can only be specified if `name` is given. @@ -2556,14 +2661,14 @@ impl<'db> FmtDetailed<'db> for DisplayParameter<'_, 'db> { { // For Literal types display the value without `Literal[..]` wrapping let representation = - default_type.representation(self.db, self.settings.clone()); + default_type.representation(db, self.env, self.settings.clone()); representation.fmt_detailed(f)?; } Type::NominalInstance(instance) => { // Some key default types like `None` are worth showing - let class = instance.class(self.db); + let class = instance.class(db, self.env); - match (class, class.known(self.db)) { + match (class, class.known(db)) { (_, Some(KnownClass::NoneType)) => { f.with_type(default_type).write_str("None")?; } @@ -2582,7 +2687,7 @@ impl<'db> FmtDetailed<'db> for DisplayParameter<'_, 'db> { // have something visible in the parameter slot. self.param .annotated_type() - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; } Ok(()) @@ -2644,10 +2749,12 @@ impl<'db> UnionType<'db> { fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplayUnionType<'a, 'db> { DisplayUnionType { db, + env, ty: self, settings, } @@ -2657,9 +2764,54 @@ impl<'db> UnionType<'db> { struct DisplayUnionType<'a, 'db> { ty: &'a UnionType<'db>, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } +impl<'db> DisplayUnionType<'_, 'db> { + /// Return the literal types that can be folded into a displayed `Literal[...]` group. + /// + /// Plain literal types are returned as-is. Small enum complements are expanded to their + /// remaining enum literals so a type like `Color & ~Literal[Color.RED]` can be displayed + /// with the same condensation rules as explicit enum-literal unions. Large complements + /// stay compact to keep diagnostics readable. + /// + /// ```python + /// from enum import Enum + /// + /// class Color(Enum): + /// RED = 1 + /// BLUE = 2 + /// + /// # Color excluding RED displays through the literal-group path for BLUE. + /// ``` + fn condensable_literals(&self, ty: Type<'db>) -> Option>> { + match ty { + Type::LiteralValue(literal) + if matches!( + literal.kind(), + LiteralValueTypeKind::Int(_) + | LiteralValueTypeKind::String(_) + | LiteralValueTypeKind::Bytes(_) + | LiteralValueTypeKind::Bool(_) + | LiteralValueTypeKind::Enum(_) + ) => + { + Some(vec![ty]) + } + Type::EnumComplement(complement) => complement.remaining_literal_types_for_display( + self.db, + self.env, + LITERAL_POLICY.max, + ), + Type::Intersection(intersection) => { + intersection.finite_alternatives_for_display(self.db, self.env, LITERAL_POLICY.max) + } + _ => None, + } + } +} + const UNION_POLICY: TruncationPolicy = TruncationPolicy { max: 5, max_when_elided: 3, @@ -2667,52 +2819,15 @@ const UNION_POLICY: TruncationPolicy = TruncationPolicy { impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - /// Return the literal types that can be folded into a displayed `Literal[...]` group. - /// - /// Plain literal types are returned as-is. Small enum complements are expanded to their - /// remaining enum literals so a type like `Color & ~Literal[Color.RED]` can be displayed - /// with the same condensation rules as explicit enum-literal unions. Large complements - /// stay compact to keep diagnostics readable. - /// - /// ```python - /// from enum import Enum - /// - /// class Color(Enum): - /// RED = 1 - /// BLUE = 2 - /// - /// # Color excluding RED displays through the literal-group path for BLUE. - /// ``` - fn condensable_literals<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { - match ty { - Type::LiteralValue(literal) - if matches!( - literal.kind(), - LiteralValueTypeKind::Int(_) - | LiteralValueTypeKind::String(_) - | LiteralValueTypeKind::Bytes(_) - | LiteralValueTypeKind::Bool(_) - | LiteralValueTypeKind::Enum(_) - ) => - { - Some(vec![ty]) - } - Type::EnumComplement(complement) => { - complement.remaining_literal_types_for_display(db, LITERAL_POLICY.max) - } - Type::Intersection(intersection) => { - intersection.finite_alternatives_for_display(db, LITERAL_POLICY.max) - } - _ => None, - } - } - fn singleline_union_element_label<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, element: Type<'db>, settings: &DisplaySettings<'db>, ) -> String { - element.display_with(db, settings.singleline()).to_string() + element + .display_with(db, env, settings.singleline()) + .to_string() } fn duplicate_ambiguous_labels(element_labels: &[Option]) -> FxHashSet<&str> { @@ -2727,8 +2842,9 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { .filter_map(|(label, count)| (count > 1).then_some(label)) .collect() } + let db = self.db; - let elements = self.ty.elements(self.db); + let elements = self.ty.elements(db); let mut condensed_types = vec![]; let mut condensed_element_count = 0usize; let mut subclass_of_types = vec![]; @@ -2736,14 +2852,14 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { .iter() .copied() .map(|element| { - (condensable_literals(self.db, element).is_none() && !element.is_subclass_of()) - .then(|| singleline_union_element_label(self.db, element, &self.settings)) + (self.condensable_literals(element).is_none() && !element.is_subclass_of()) + .then(|| singleline_union_element_label(db, self.env, element, &self.settings)) }) .collect(); let duplicate_ambiguous_labels = duplicate_ambiguous_labels(&element_labels); for element in elements.iter().copied() { - if let Some(literals) = condensable_literals(self.db, element) { + if let Some(literals) = self.condensable_literals(element) { condensed_element_count += 1; for literal in literals { if !condensed_types.contains(&literal) { @@ -2776,12 +2892,13 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { break; } - if condensable_literals(self.db, *element).is_some() { + if self.condensable_literals(*element).is_some() { if let Some(condensed_types) = condensed_types.take() { displayed_entries += 1; join.entry(&DisplayLiteralGroup { literals: condensed_types, - db: self.db, + db, + env: self.env, settings: self.settings.singleline(), }); } @@ -2790,7 +2907,8 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { displayed_entries += 1; join.entry(&DisplaySubclassOfGroup { types: subclass_of_types, - db: self.db, + db, + env: self.env, settings: self.settings.singleline(), }); } @@ -2806,7 +2924,8 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { }; join.entry(&DisplayMaybeParenthesizedType { ty: *element, - db: self.db, + db, + env: self.env, settings, }); } @@ -2838,14 +2957,16 @@ impl fmt::Debug for DisplayUnionType<'_, '_> { } } -struct DisplaySubclassOfGroup<'db> { +struct DisplaySubclassOfGroup<'env, 'db> { types: Vec>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplaySubclassOfGroup<'db> { +impl<'db> FmtDetailed<'db> for DisplaySubclassOfGroup<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; f.write_str("type[")?; let total_entries = self.types.len(); let display_limit = @@ -2854,24 +2975,33 @@ impl<'db> FmtDetailed<'db> for DisplaySubclassOfGroup<'db> { for subclass_of in self.types.iter().take(display_limit) { match subclass_of.subclass_of() { SubclassOfInner::Class(ClassType::NonGeneric(class)) => { - join.entry(&class.display_with(self.db, self.settings.singleline())); + join.entry(&class.display_with(db, self.settings.singleline())); } SubclassOfInner::Class(ClassType::Generic(alias)) => { - join.entry(&alias.display_with(self.db, self.settings.singleline())); + join.entry(&alias.display_with(db, self.env, self.settings.singleline())); } SubclassOfInner::Dynamic(dynamic) => { - let rep = - Type::Dynamic(dynamic).representation(self.db, self.settings.singleline()); + let rep = Type::Dynamic(dynamic).representation( + db, + self.env, + self.settings.singleline(), + ); join.entry(&rep); } SubclassOfInner::Protocol(protocol) => { - let rep = Type::ProtocolInstance(protocol) - .representation(self.db, self.settings.singleline()); + let rep = Type::ProtocolInstance(protocol).representation( + db, + self.env, + self.settings.singleline(), + ); join.entry(&rep); } SubclassOfInner::TypeVar(bound_typevar) => { - let rep = Type::TypeVar(bound_typevar) - .representation(self.db, self.settings.singleline()); + let rep = Type::TypeVar(bound_typevar).representation( + db, + self.env, + self.settings.singleline(), + ); join.entry(&rep); } } @@ -2891,15 +3021,16 @@ impl<'db> FmtDetailed<'db> for DisplaySubclassOfGroup<'db> { } } -impl Display for DisplaySubclassOfGroup<'_> { +impl Display for DisplaySubclassOfGroup<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } -struct DisplayLiteralGroup<'db> { +struct DisplayLiteralGroup<'env, 'db> { literals: Vec>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } @@ -2908,8 +3039,9 @@ const LITERAL_POLICY: TruncationPolicy = TruncationPolicy { max_when_elided: 5, }; -impl<'db> FmtDetailed<'db> for DisplayLiteralGroup<'db> { +impl<'db> FmtDetailed<'db> for DisplayLiteralGroup<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; f.with_type(Type::SpecialForm(SpecialFormType::Literal)) .write_str("Literal")?; f.write_char('[')?; @@ -2922,7 +3054,7 @@ impl<'db> FmtDetailed<'db> for DisplayLiteralGroup<'db> { let mut join = f.join(", "); for lit in self.literals.iter().take(display_limit) { - let rep = lit.representation(self.db, self.settings.singleline()); + let rep = lit.representation(db, self.env, self.settings.singleline()); join.entry(&rep); } @@ -2942,7 +3074,7 @@ impl<'db> FmtDetailed<'db> for DisplayLiteralGroup<'db> { } } -impl Display for DisplayLiteralGroup<'_> { +impl Display for DisplayLiteralGroup<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } @@ -2952,10 +3084,12 @@ impl<'db> IntersectionType<'db> { fn display_with<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, ) -> DisplayIntersectionType<'a, 'db> { DisplayIntersectionType { db, + env, ty: self, settings, } @@ -2965,28 +3099,32 @@ impl<'db> IntersectionType<'db> { struct DisplayIntersectionType<'a, 'db> { ty: &'a IntersectionType<'db>, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } impl<'db> FmtDetailed<'db> for DisplayIntersectionType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; let tys = self .ty - .positive(self.db) + .positive(db) .iter() .map(|&ty| DisplayMaybeNegatedType { ty, - db: self.db, + db, + env: self.env, settings: self.settings.singleline(), negated: false, }) .chain( self.ty - .negative(self.db) + .negative(db) .iter() .map(|&ty| DisplayMaybeNegatedType { ty, - db: self.db, + db, + env: self.env, settings: self.settings.singleline(), negated: true, }), @@ -3009,28 +3147,31 @@ impl fmt::Debug for DisplayIntersectionType<'_, '_> { } } -struct DisplayMaybeNegatedType<'db> { +struct DisplayMaybeNegatedType<'env, 'db> { ty: Type<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, negated: bool, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplayMaybeNegatedType<'db> { +impl<'db> FmtDetailed<'db> for DisplayMaybeNegatedType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; if self.negated { f.write_str("~")?; } DisplayMaybeParenthesizedType { ty: self.ty, - db: self.db, + db, + env: self.env, settings: self.settings.clone(), } .fmt_detailed(f) } } -impl Display for DisplayMaybeNegatedType<'_> { +impl Display for DisplayMaybeNegatedType<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } @@ -3052,90 +3193,99 @@ fn should_parenthesize_callable_type(ty: Type<'_>, db: &dyn Db) -> bool { } } -struct DisplayMaybeParenthesizedType<'db> { +struct DisplayMaybeParenthesizedType<'env, 'db> { ty: Type<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } -impl<'db> FmtDetailed<'db> for DisplayMaybeParenthesizedType<'db> { +impl<'db> FmtDetailed<'db> for DisplayMaybeParenthesizedType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; let write_parentheses = |f: &mut TypeWriter<'_, '_, 'db>| { f.set_invalid_type_annotation(); f.write_char('(')?; self.ty - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; f.write_char(')') }; match self.ty { - ty if should_parenthesize_callable_type(ty, self.db) => write_parentheses(f), + ty if should_parenthesize_callable_type(ty, db) => write_parentheses(f), Type::KnownBoundMethod(_) | Type::FunctionLiteral(_) | Type::BoundMethod(_) | Type::Union(_) => write_parentheses(f), - Type::Intersection(intersection) if !intersection.has_one_element(self.db) => { + Type::Intersection(intersection) if !intersection.has_one_element(db) => { write_parentheses(f) } _ => self .ty - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), } } } -impl Display for DisplayMaybeParenthesizedType<'_> { +impl Display for DisplayMaybeParenthesizedType<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } trait TypeArrayDisplay<'db> { - fn display_with( - &self, + fn display_with<'a>( + &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayTypeArray<'_, 'db>; + ) -> DisplayTypeArray<'a, 'db>; } impl<'db> TypeArrayDisplay<'db> for Box<[Type<'db>]> { - fn display_with( - &self, + fn display_with<'a>( + &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayTypeArray<'_, 'db> { + ) -> DisplayTypeArray<'a, 'db> { DisplayTypeArray { types: self, db, + env, settings, } } } impl<'db> TypeArrayDisplay<'db> for Vec> { - fn display_with( - &self, + fn display_with<'a>( + &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayTypeArray<'_, 'db> { + ) -> DisplayTypeArray<'a, 'db> { DisplayTypeArray { types: self, db, + env, settings, } } } impl<'db> TypeArrayDisplay<'db> for [Type<'db>] { - fn display_with( - &self, + fn display_with<'a>( + &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayTypeArray<'_, 'db> { + ) -> DisplayTypeArray<'a, 'db> { DisplayTypeArray { types: self, db, + env, settings, } } @@ -3144,16 +3294,18 @@ impl<'db> TypeArrayDisplay<'db> for [Type<'db>] { struct DisplayTypeArray<'b, 'db> { types: &'b [Type<'db>], db: &'db dyn Db, + env: &'b ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } impl<'db> FmtDetailed<'db> for DisplayTypeArray<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; f.join(", ") .entries( self.types .iter() - .map(|ty| ty.display_with(self.db, self.settings.singleline())), + .map(|ty| ty.display_with(db, self.env, self.settings.singleline())), ) .finish() } @@ -3192,34 +3344,38 @@ impl Display for DisplayStringLiteralType<'_> { } } -pub(crate) struct DisplayKnownInstanceRepr<'db> { +pub(crate) struct DisplayKnownInstanceRepr<'env, 'db> { known_instance: KnownInstanceType<'db>, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, } impl<'db> KnownInstanceType<'db> { - pub(crate) fn display_with( + pub(crate) fn display_with<'env>( self, db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, settings: DisplaySettings<'db>, - ) -> DisplayKnownInstanceRepr<'db> { + ) -> DisplayKnownInstanceRepr<'env, 'db> { DisplayKnownInstanceRepr { known_instance: self, db, + env, settings, } } } -impl Display for DisplayKnownInstanceRepr<'_> { +impl Display for DisplayKnownInstanceRepr<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) } } -impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { +impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let db = self.db; let ty = Type::KnownInstance(self.known_instance); match self.known_instance { KnownInstanceType::SubscriptedProtocol(generic_context) => { @@ -3227,7 +3383,7 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { f.write_str("") } KnownInstanceType::SubscriptedGeneric(generic_context) => { @@ -3235,16 +3391,21 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { f.write_str("") } KnownInstanceType::TypeAliasType(alias) => { - if let Some(specialization) = alias.specialization(self.db) { + if let Some(specialization) = alias.specialization(db) { f.set_invalid_type_annotation(); f.write_str("") } else { @@ -3255,9 +3416,9 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { // 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(typevar_instance) => { - if typevar_instance.kind(self.db).is_paramspec() { + if typevar_instance.kind(db).is_paramspec() { f.with_type(ty).write_str("ParamSpec") - } else if typevar_instance.kind(self.db).is_typevartuple() { + } else if typevar_instance.kind(db).is_typevartuple() { f.with_type(ty).write_str("TypeVarTuple") } else { f.with_type(ty).write_str("TypeVar") @@ -3268,13 +3429,13 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { f.with_type(ty).write_str("dataclasses.Field")?; let field_type = field - .converter(self.db) + .converter(db) .map(|(_, converter_output)| converter_output) - .or(field.default_type(self.db)); + .or(field.default_type(db)); if let Some(field_ty) = field_type { f.write_char('[')?; - write!(f.with_type(field_ty), "{}", field_ty.display(self.db))?; + write!(f.with_type(field_ty), "{}", field_ty.display(db, self.env))?; f.write_char(']')?; } Ok(()) @@ -3282,12 +3443,12 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { KnownInstanceType::ConstraintSet(interned_set) => { f.with_type(ty).write_str("ConstraintSet")?; let constraints = ConstraintSetBuilder::new(); - let set = constraints.load(self.db, interned_set.constraints(self.db)); - if interned_set.detailed_display(self.db) { - write!(f, "[{}]", set.display(self.db)) - } else if set.is_always_satisfied(self.db) { + let set = constraints.load(db, self.env, interned_set.constraints(db)); + if interned_set.detailed_display(db) { + write!(f, "[{}]", set.display(db, self.env)) + } else if set.is_always_satisfied(db, self.env) { f.write_str("[Literal[True]]") - } else if set.is_never_satisfied(self.db) { + } else if set.is_never_satisfied(db, self.env) { f.write_str("[Literal[False]]") } else { f.write_str("[bool]") @@ -3296,14 +3457,14 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { KnownInstanceType::ConstraintSetSolution(solution) => { f.set_invalid_type_annotation(); f.with_type(ty).write_str("Solution[")?; - for (index, binding) in solution.bindings(self.db).iter().enumerate() { + for (index, binding) in solution.bindings(db).iter().enumerate() { if index > 0 { f.write_str(", ")?; } - write!(f, "{}=", binding.bound_typevar.name(self.db))?; + write!(f, "{}=", binding.bound_typevar.name(db))?; binding .solution - .display_with(self.db, self.settings.clone()) + .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f)?; } f.write_char(']') @@ -3311,23 +3472,23 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { KnownInstanceType::GenericContext(generic_context) => { f.with_type(ty) .write_str("ty_extensions._internal.GenericContext")?; - write!(f, "{}", generic_context.display_full(self.db)) + write!(f, "{}", generic_context.display_full(db)) } KnownInstanceType::Specialization(specialization) => { // Normalize for consistent output across CI platforms f.with_type(ty) .write_str("ty_extensions._internal.Specialization")?; - write!(f, "{}", specialization.display_full(self.db)) + write!(f, "{}", specialization.display_full(db, self.env)) } KnownInstanceType::UnionType(union) => { f.set_invalid_type_annotation(); f.write_char('<')?; - f.with_type(KnownClass::UnionType.to_class_literal(self.db)) + f.with_type(KnownClass::UnionType.to_class_literal(db, self.env)) .write_str("types.UnionType")?; f.write_str(" special-form")?; - if let Ok(ty) = union.union_type(self.db) { + if let Ok(ty) = union.union_type(db) { f.write_str(" '")?; - ty.display(self.db).fmt_detailed(f)?; + ty.display(db, self.env).fmt_detailed(f)?; f.write_char('\'')?; } f.write_char('>') @@ -3335,7 +3496,7 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { KnownInstanceType::Literal(inner) => { f.set_invalid_type_annotation(); f.write_str("") } KnownInstanceType::Annotated(inner) => { @@ -3344,7 +3505,7 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { f.with_type(Type::SpecialForm(SpecialFormType::Annotated)) .write_str("typing.Annotated")?; f.write_char('[')?; - inner.inner(self.db).display(self.db).fmt_detailed(f)?; + inner.inner(db).display(db, self.env).fmt_detailed(f)?; f.write_str(", ]'>") } KnownInstanceType::Callable(callable) => { @@ -3357,49 +3518,47 @@ impl<'db> FmtDetailed<'db> for DisplayKnownInstanceRepr<'db> { f.with_type(Type::SpecialForm(SpecialFormType::TypingCallable)) .write_str("Callable")?; f.write_str(" special-form '")?; - callable.display(self.db).fmt_detailed(f)?; + callable.display(db, self.env).fmt_detailed(f)?; f.write_str("'>") } KnownInstanceType::TypeGenericAlias(inner) => { f.set_invalid_type_annotation(); f.write_str("") } KnownInstanceType::LiteralStringAlias(_) => f - .with_type(KnownClass::Str.to_class_literal(self.db)) + .with_type(KnownClass::Str.to_class_literal(db, self.env)) .write_str("str"), KnownInstanceType::NewType(declaration) => { f.set_invalid_type_annotation(); f.write_char('<')?; - f.with_type(KnownClass::NewType.to_class_literal(self.db)) + f.with_type(KnownClass::NewType.to_class_literal(db, self.env)) .write_str("NewType")?; f.write_str(" pseudo-class '")?; - f.with_type(ty).write_str(declaration.name(self.db))?; + f.with_type(ty).write_str(declaration.name(db))?; f.write_str("'>") } KnownInstanceType::Sentinel(sentinel) => { - f.with_type(ty).write_str(sentinel.name(self.db).as_str()) + f.with_type(ty).write_str(sentinel.name(db).as_str()) } KnownInstanceType::NamedTupleSpec(_) => f.write_str("NamedTupleSpec"), KnownInstanceType::FunctoolsPartial(partial) => { f.write_str("partial[")?; - Type::Callable(partial.partial(self.db)) - .display_with(self.db, DisplaySettings::default().singleline()) + Type::Callable(partial.partial(db)) + .display_with(db, self.env, DisplaySettings::default().singleline()) .fmt_detailed(f)?; f.write_str("]") } KnownInstanceType::Range { .. } => f - .with_type(KnownClass::Range.to_class_literal(self.db)) + .with_type(KnownClass::Range.to_class_literal(db, self.env)) .write_str("range"), - KnownInstanceType::FunctoolsPartialCall(partial) => { - Type::Callable(partial.partial(self.db)) - .display_with(self.db, DisplaySettings::default().singleline()) - .fmt_detailed(f) - } + KnownInstanceType::FunctoolsPartialCall(partial) => Type::Callable(partial.partial(db)) + .display_with(db, self.env, DisplaySettings::default().singleline()) + .fmt_detailed(f), } } } @@ -3409,8 +3568,7 @@ mod tests { use insta::assert_snapshot; use ruff_python_ast::name::Name; - use crate::Db; - use crate::db::tests::setup_db; + use crate::db::tests::{TestDb, setup_db}; use crate::types::{KnownClass, Parameter, Parameters, Signature, Type}; #[test] @@ -3418,21 +3576,27 @@ mod tests { let db = setup_db(); assert_eq!( - Type::string_literal(&db, r"\n").display(&db).to_string(), + Type::string_literal(&db, r"\n") + .display(&db, &db.program_environment()) + .to_string(), r#"Literal["\\n"]"# ); assert_eq!( - Type::string_literal(&db, "'").display(&db).to_string(), + Type::string_literal(&db, "'") + .display(&db, &db.program_environment()) + .to_string(), r#"Literal["'"]"# ); assert_eq!( - Type::string_literal(&db, r#"""#).display(&db).to_string(), + Type::string_literal(&db, r#"""#) + .display(&db, &db.program_environment()) + .to_string(), r#"Literal["\""]"# ); } fn display_signature<'db>( - db: &'db dyn Db, + db: &'db TestDb, parameters: impl IntoIterator>, return_ty: Option>, ) -> String { @@ -3440,12 +3604,12 @@ mod tests { Parameters::from_annotation(db, parameters), return_ty.unwrap_or(Type::unknown()), ) - .display(db) + .display(db, &db.program_environment()) .to_string() } fn display_signature_multiline<'db>( - db: &'db dyn Db, + db: &'db TestDb, parameters: impl IntoIterator>, return_ty: Option>, ) -> String { @@ -3453,29 +3617,35 @@ mod tests { Parameters::from_annotation(db, parameters), return_ty.unwrap_or(Type::unknown()), ) - .display_with(db, super::DisplaySettings::default().multiline()) + .display_with( + db, + &db.program_environment(), + super::DisplaySettings::default().multiline(), + ) .to_string() } #[test] fn signature_display() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); // Empty parameters with no return type. - assert_snapshot!(display_signature(&db, [], None), @"() -> Unknown"); + assert_snapshot!(display_signature(db, [], None), @"() -> Unknown"); // Empty parameters with a return type. assert_snapshot!( - display_signature(&db, [], Some(Type::none(&db))), + display_signature(db, [], Some(Type::none(db, &env))), @"() -> None" ); // Single parameter type (no name) with a return type. assert_snapshot!( display_signature( - &db, - [Parameter::positional_only(None).with_annotated_type(Type::none(&db))], - Some(Type::none(&db)) + db, + [Parameter::positional_only(None).with_annotated_type(Type::none(db, &env))], + Some(Type::none(db, &env)) ), @"(None, /) -> None" ); @@ -3483,15 +3653,15 @@ mod tests { // Two parameters where one has annotation and the other doesn't. assert_snapshot!( display_signature( - &db, + db, [ Parameter::positional_or_keyword(Name::new_static("x")) - .with_default_type(KnownClass::Int.to_instance(&db)), + .with_default_type(KnownClass::Int.to_instance(db, &env)), Parameter::positional_or_keyword(Name::new_static("y")) - .with_annotated_type(KnownClass::Str.to_instance(&db)) - .with_default_type(KnownClass::Str.to_instance(&db)), + .with_annotated_type(KnownClass::Str.to_instance(db, &env)) + .with_default_type(KnownClass::Str.to_instance(db, &env)), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @"(x=..., y: str = ...) -> None" ); @@ -3499,12 +3669,12 @@ mod tests { // All positional only parameters. assert_snapshot!( display_signature( - &db, + db, [ Parameter::positional_only(Some(Name::new_static("x"))), Parameter::positional_only(Some(Name::new_static("y"))), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @"(x, y, /) -> None" ); @@ -3512,12 +3682,12 @@ mod tests { // Positional-only parameters mixed with non-positional-only parameters. assert_snapshot!( display_signature( - &db, + db, [ Parameter::positional_only(Some(Name::new_static("x"))), Parameter::positional_or_keyword(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @"(x, /, y) -> None" ); @@ -3525,12 +3695,12 @@ mod tests { // All keyword-only parameters. assert_snapshot!( display_signature( - &db, + db, [ Parameter::keyword_only(Name::new_static("x")), Parameter::keyword_only(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @"(*, x, y) -> None" ); @@ -3538,12 +3708,12 @@ mod tests { // Keyword-only parameters mixed with non-keyword-only parameters. assert_snapshot!( display_signature( - &db, + db, [ Parameter::positional_or_keyword(Name::new_static("x")), Parameter::keyword_only(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @"(x, *, y) -> None" ); @@ -3551,13 +3721,13 @@ mod tests { // '/' parameter must appear before '*' parameter assert_snapshot!( display_signature( - &db, + db, [ Parameter::positional_only(Some(Name::new_static("a"))), Parameter::keyword_only(Name::new_static("x")), Parameter::keyword_only(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @"(a, /, *, x, y) -> None" ); @@ -3565,32 +3735,32 @@ mod tests { // A mix of all parameter kinds. assert_snapshot!( display_signature( - &db, + db, [ Parameter::positional_only(Some(Name::new_static("a"))), Parameter::positional_only(Some(Name::new_static("b"))) - .with_annotated_type(KnownClass::Int.to_instance(&db)), + .with_annotated_type(KnownClass::Int.to_instance(db, &env)), Parameter::positional_only(Some(Name::new_static("c"))) .with_default_type(Type::int_literal(1)), Parameter::positional_only(Some(Name::new_static("d"))) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type(KnownClass::Int.to_instance(db, &env)) .with_default_type(Type::int_literal(2)), Parameter::positional_or_keyword(Name::new_static("e")) .with_default_type(Type::int_literal(3)), Parameter::positional_or_keyword(Name::new_static("f")) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type(KnownClass::Int.to_instance(db, &env)) .with_default_type(Type::int_literal(4)), Parameter::variadic(Name::new_static("args")) .with_annotated_type(Type::object()), Parameter::keyword_only(Name::new_static("g")) .with_default_type(Type::int_literal(5)), Parameter::keyword_only(Name::new_static("h")) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type(KnownClass::Int.to_instance(db, &env)) .with_default_type(Type::int_literal(6)), Parameter::keyword_variadic(Name::new_static("kwargs")) - .with_annotated_type(KnownClass::Str.to_instance(&db)), + .with_annotated_type(KnownClass::Str.to_instance(db, &env)), ], - Some(KnownClass::Bytes.to_instance(&db)) + Some(KnownClass::Bytes.to_instance(db, &env)) ), @"(a, b: int, c=1, d: int = 2, /, e=3, f: int = 4, *args: object, *, g=5, h: int = 6, **kwargs: str) -> bytes" ); @@ -3599,22 +3769,24 @@ mod tests { #[test] fn signature_display_multiline() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); // Empty parameters with no return type. - assert_snapshot!(display_signature_multiline(&db, [], None), @"() -> Unknown"); + assert_snapshot!(display_signature_multiline(db, [], None), @"() -> Unknown"); // Empty parameters with a return type. assert_snapshot!( - display_signature_multiline(&db, [], Some(Type::none(&db))), + display_signature_multiline(db, [], Some(Type::none(db, &env))), @"() -> None" ); // Single parameter type (no name) with a return type. assert_snapshot!( display_signature_multiline( - &db, - [Parameter::positional_only(None).with_annotated_type(Type::none(&db))], - Some(Type::none(&db)) + db, + [Parameter::positional_only(None).with_annotated_type(Type::none(db, &env))], + Some(Type::none(db, &env)) ), @"(None, /) -> None" ); @@ -3622,15 +3794,15 @@ mod tests { // Two parameters where one has annotation and the other doesn't. assert_snapshot!( display_signature_multiline( - &db, + db, [ Parameter::positional_or_keyword(Name::new_static("x")) - .with_default_type(KnownClass::Int.to_instance(&db)), + .with_default_type(KnownClass::Int.to_instance(db, &env)), Parameter::positional_or_keyword(Name::new_static("y")) - .with_annotated_type(KnownClass::Str.to_instance(&db)) - .with_default_type(KnownClass::Str.to_instance(&db)), + .with_annotated_type(KnownClass::Str.to_instance(db, &env)) + .with_default_type(KnownClass::Str.to_instance(db, &env)), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @" ( @@ -3643,12 +3815,12 @@ mod tests { // All positional only parameters. assert_snapshot!( display_signature_multiline( - &db, + db, [ Parameter::positional_only(Some(Name::new_static("x"))), Parameter::positional_only(Some(Name::new_static("y"))), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @" ( @@ -3662,12 +3834,12 @@ mod tests { // Positional-only parameters mixed with non-positional-only parameters. assert_snapshot!( display_signature_multiline( - &db, + db, [ Parameter::positional_only(Some(Name::new_static("x"))), Parameter::positional_or_keyword(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @" ( @@ -3681,12 +3853,12 @@ mod tests { // All keyword-only parameters. assert_snapshot!( display_signature_multiline( - &db, + db, [ Parameter::keyword_only(Name::new_static("x")), Parameter::keyword_only(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @" ( @@ -3700,12 +3872,12 @@ mod tests { // Keyword-only parameters mixed with non-keyword-only parameters. assert_snapshot!( display_signature_multiline( - &db, + db, [ Parameter::positional_or_keyword(Name::new_static("x")), Parameter::keyword_only(Name::new_static("y")), ], - Some(Type::none(&db)) + Some(Type::none(db, &env)) ), @" ( @@ -3719,32 +3891,32 @@ mod tests { // A mix of all parameter kinds. assert_snapshot!( display_signature_multiline( - &db, + db, [ Parameter::positional_only(Some(Name::new_static("a"))), Parameter::positional_only(Some(Name::new_static("b"))) - .with_annotated_type(KnownClass::Int.to_instance(&db)), + .with_annotated_type(KnownClass::Int.to_instance(db, &env)), Parameter::positional_only(Some(Name::new_static("c"))) .with_default_type(Type::int_literal(1)), Parameter::positional_only(Some(Name::new_static("d"))) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type(KnownClass::Int.to_instance(db, &env)) .with_default_type(Type::int_literal(2)), Parameter::positional_or_keyword(Name::new_static("e")) .with_default_type(Type::int_literal(3)), Parameter::positional_or_keyword(Name::new_static("f")) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type(KnownClass::Int.to_instance(db, &env)) .with_default_type(Type::int_literal(4)), Parameter::variadic(Name::new_static("args")) .with_annotated_type(Type::object()), Parameter::keyword_only(Name::new_static("g")) .with_default_type(Type::int_literal(5)), Parameter::keyword_only(Name::new_static("h")) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type(KnownClass::Int.to_instance(db, &env)) .with_default_type(Type::int_literal(6)), Parameter::keyword_variadic(Name::new_static("kwargs")) - .with_annotated_type(KnownClass::Str.to_instance(&db)), + .with_annotated_type(KnownClass::Str.to_instance(db, &env)), ], - Some(KnownClass::Bytes.to_instance(&db)) + Some(KnownClass::Bytes.to_instance(db, &env)) ), @" ( diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index d747df9be1..0b4bf2b309 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use compact_str::ToCompactString; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; @@ -61,9 +62,14 @@ impl KnownEnumDataTypeMixin { /// /// Literal conversions are preserved precisely, unions are normalized element-wise, and values /// whose conversion cannot be modeled precisely fall back to the mixin's instance type. - fn normalize_value<'db>(self, db: &'db dyn Db, value: Type<'db>) -> Type<'db> { + fn normalize_value<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + value: Type<'db>, + ) -> Type<'db> { if let Type::Union(union) = value { - return union.map(db, |element| self.normalize_value(db, *element)); + return union.map(db, env, |element| self.normalize_value(db, env, *element)); } match (self, value.as_literal_value_kind()) { @@ -78,8 +84,8 @@ impl KnownEnumDataTypeMixin { (Self::Str, Some(LiteralValueTypeKind::Bool(value))) => { Type::string_literal(db, if value { "True" } else { "False" }) } - (Self::Int, _) => KnownClass::Int.to_instance(db), - (Self::Str, _) => KnownClass::Str.to_instance(db), + (Self::Int, _) => KnownClass::Int.to_instance(db, env), + (Self::Str, _) => KnownClass::Str.to_instance(db, env), } } } @@ -160,13 +166,18 @@ impl<'db> EnumValueConstruction<'db> { /// Returns the payload after known built-in data-type construction, or `None` when the /// constructor may coerce it in a way that ty does not model. - fn normalize_value(self, db: &'db dyn Db, value: Type<'db>) -> Option> { + fn normalize_value( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + value: Type<'db>, + ) -> Option> { match self.data_type { InheritedEnumDataType::None => Some(value), InheritedEnumDataType::DeclaredValue(data_type) => { - value_has_exact_known_class(db, value, data_type).then_some(value) + value_has_exact_known_class(db, env, value, data_type).then_some(value) } - InheritedEnumDataType::Known(mixin) => Some(mixin.normalize_value(db, value)), + InheritedEnumDataType::Known(mixin) => Some(mixin.normalize_value(db, env, value)), InheritedEnumDataType::Opaque => None, } } @@ -182,6 +193,7 @@ impl<'db> EnumValueConstruction<'db> { fn alias_detection_value( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, value_ty: Type<'db>, is_auto: bool, ) -> Option> { @@ -197,11 +209,13 @@ impl<'db> EnumValueConstruction<'db> { } else if self.generate_next_value.is_opaque() { return None; } else if let Some(function) = self.generate_next_value.function() { - function.signature(db).overload_return_type_or_unknown(db) + function + .signature(db) + .overload_return_type_or_unknown(db, env) } else { value_ty }; - self.normalize_value(db, value) + self.normalize_value(db, env, value) } } @@ -243,6 +257,7 @@ impl get_size2::GetSize for EnumMetadata<'_> {} pub(super) fn class_defines_property<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassLiteral<'db>, name: &str, ) -> bool { @@ -266,7 +281,12 @@ pub(super) fn class_defines_property<'db>( ) { return false; } - if let Some(member) = base.own_class_member(db, None, name).inner.place.raw_type() { + if let Some(member) = base + .own_class_member(db, env, None, name) + .inner + .place + .raw_type() + { return member.is_property_instance(); } } @@ -312,11 +332,16 @@ fn enum_class_literal<'db>( db: &'db dyn Db, class: ClassLiteral<'db>, ) -> Option> { + let env = ProgramEnvironment::from_file(class.python_file(db)); let metadata = enum_metadata(db, class)?; let members = metadata .members .keys() - .map(|name| metadata.value_type(db, name).map(|ty| (name.clone(), ty))) + .map(|name| { + metadata + .value_type(db, &env, name) + .map(|ty| (name.clone(), ty)) + }) .collect::>>()?; let mut aliases: Vec<_> = metadata .aliases @@ -325,7 +350,11 @@ fn enum_class_literal<'db>( .collect(); aliases.sort_unstable(); let members_are_exhaustive = !metadata.value_construction.metaclass_may_transform_values - && !Type::ClassLiteral(class).is_subtype_of(db, KnownClass::Flag.to_subclass_of(db)) + && !Type::ClassLiteral(class).is_subtype_of( + db, + &env, + KnownClass::Flag.to_subclass_of(db, &env), + ) && !enum_has_custom_missing(db, class); Some(EnumClassLiteral::new( @@ -402,15 +431,16 @@ impl<'db> EnumClassLiteral<'db> { /// expand through the remaining literal union so descriptor lookup sees ordinary enum literals. pub(super) fn instance_member_for_enum_complement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, complement: EnumComplement<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { - if let Some(member) = special_member_for_enum_complement(db, complement, name) { + if let Some(member) = special_member_for_enum_complement(db, env, complement, name) { member } else { complement - .remaining_literal_union(db) - .instance_member(db, name) + .remaining_literal_union(db, env) + .instance_member(db, env, name) } } @@ -420,16 +450,17 @@ pub(super) fn instance_member_for_enum_complement<'db>( /// general member lookup so descriptor and class-variable policy is still applied. pub(super) fn member_lookup_for_enum_complement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, complement: EnumComplement<'db>, name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - if let Some(member) = special_member_for_enum_complement(db, complement, name) { + if let Some(member) = special_member_for_enum_complement(db, env, complement, name) { member } else { complement - .remaining_literal_union(db) - .member_lookup_with_policy(db, name, policy) + .remaining_literal_union(db, env) + .member_lookup_with_policy(db, env, name, policy) } } @@ -440,13 +471,14 @@ pub(super) fn member_lookup_for_enum_complement<'db>( /// directly from the remaining canonical members. fn special_member_for_enum_complement<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, complement: EnumComplement<'db>, name: &str, ) -> Option> { if matches!(name, "name" | "_name_" | "value" | "_value_") - && !class_defines_property(db, complement.enum_class(db), name) + && !class_defines_property(db, env, complement.enum_class(db), name) && complement.rest(db).iter().all(Type::is_dynamic) - && let Some(member_ty) = complement.member_type(db, name) + && let Some(member_ty) = complement.member_type(db, env, name) { Some(Place::bound(member_ty).into()) } else { @@ -461,6 +493,7 @@ fn special_member_for_enum_complement<'db>( /// are normalized to the annotated class by constructors such as `int.__new__`. fn known_constructor_preserves_value_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, value: Type<'db>, annotation: Type<'db>, ) -> bool { @@ -469,8 +502,8 @@ fn known_constructor_preserves_value_type<'db>( Type::Union(union) => union .elements(db) .iter() - .all(|element| known_constructor_preserves_value_type(db, *element, annotation)), - Type::LiteralValue(literal) => literal.fallback_instance(db) == annotation, + .all(|element| known_constructor_preserves_value_type(db, env, *element, annotation)), + Type::LiteralValue(literal) => literal.fallback_instance(db, env) == annotation, value => value == annotation, } } @@ -480,6 +513,7 @@ fn known_constructor_preserves_value_type<'db>( /// constructor may return an instance of the built-in base. fn value_has_exact_known_class<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, value: Type<'db>, data_type: KnownClass, ) -> bool { @@ -487,8 +521,8 @@ fn value_has_exact_known_class<'db>( Type::Union(union) => union .elements(db) .iter() - .all(|element| value_has_exact_known_class(db, *element, data_type)), - Type::LiteralValue(literal) => match literal.fallback_instance(db) { + .all(|element| value_has_exact_known_class(db, env, *element, data_type)), + Type::LiteralValue(literal) => match literal.fallback_instance(db, env) { Type::NominalInstance(instance) => instance.has_known_class(db, data_type), _ => false, }, @@ -516,7 +550,12 @@ impl<'db> EnumMetadata<'db> { /// data types normalize the value directly. A literal is preserved when its runtime class /// matches an inherited `_value_` annotation; otherwise, the annotation describes the /// normalized value. - fn value_type(&self, db: &'db dyn Db, member_name: &Name) -> Option> { + fn value_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + member_name: &Name, + ) -> Option> { if !self.members.contains_key(member_name) { return None; } @@ -524,12 +563,12 @@ impl<'db> EnumMetadata<'db> { if let Some(EnumValueAnnotation::UserDefined(annotation)) = self.value_annotation { return Some(annotation); } - let Some(value) = self.concrete_value_type(db, member_name) else { + let Some(value) = self.concrete_value_type(db, env, member_name) else { return Some(Type::Dynamic(DynamicType::Any)); }; if let Some(EnumValueAnnotation::StandardLibrary(annotation)) = self.value_annotation - && !known_constructor_preserves_value_type(db, value, annotation) + && !known_constructor_preserves_value_type(db, env, value, annotation) { Some(annotation) } else { @@ -544,6 +583,7 @@ impl<'db> EnumMetadata<'db> { pub(super) fn concrete_value_type( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, member_name: &Name, ) -> Option> { let declared_value = self.members.get(member_name).copied()?; @@ -557,11 +597,13 @@ impl<'db> EnumMetadata<'db> { .is_user_defined() && let Some(func_ty) = self.value_construction.generate_next_value.function() { - func_ty.signature(db).overload_return_type_or_unknown(db) + func_ty + .signature(db) + .overload_return_type_or_unknown(db, env) } else { declared_value }; - self.value_construction.normalize_value(db, value) + self.value_construction.normalize_value(db, env, value) } /// Return whether enum construction may replace the value declared for `member_name`. @@ -580,7 +622,11 @@ impl<'db> EnumMetadata<'db> { /// metaclass that may transform member values, returns `Any`. /// Otherwise, returns the union of each member's `value_type`, which /// applies `_generate_next_value_`'s return type to `auto()` members. - pub(crate) fn instance_value_type(&self, db: &'db dyn Db) -> Option> { + pub(crate) fn instance_value_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { if self.members.is_empty() { return None; } @@ -592,8 +638,8 @@ impl<'db> EnumMetadata<'db> { let union = self .members .keys() - .filter_map(|name| self.value_type(db, name)) - .fold(UnionBuilder::new(db), UnionBuilder::add) + .filter_map(|name| self.value_type(db, env, name)) + .fold(UnionBuilder::new(db, env), UnionBuilder::add) .build(); Some(union) } @@ -608,7 +654,11 @@ impl<'db> EnumMetadata<'db> { /// narrowed to a specific member (e.g. `x: MyEnum` where `MyEnum` has multiple members). /// /// Returns the union of all member name string literals. - pub(crate) fn instance_name_type(&self, db: &'db dyn Db) -> Option> { + pub(crate) fn instance_name_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { if self.members.is_empty() { return None; } @@ -616,7 +666,7 @@ impl<'db> EnumMetadata<'db> { .members .keys() .map(|name| Type::string_literal(db, name)) - .fold(UnionBuilder::new(db), UnionBuilder::add) + .fold(UnionBuilder::new(db, env), UnionBuilder::add) .build(); Some(union) } @@ -663,6 +713,7 @@ impl<'db> EnumComplementType<'db> { /// Recognize the compact enum-complement shape inside an intersection. pub(crate) fn from_intersection_parts( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, positive: &FxOrderSet>, negative: &NegativeIntersectionElements<'db>, ) -> Option { @@ -674,7 +725,8 @@ impl<'db> EnumComplementType<'db> { continue; }; - let Some(enum_class_literal) = instance.class_literal(db).into_enum_class(db) else { + let Some(enum_class_literal) = instance.class_literal(db, env).into_enum_class(db) + else { rest.push(*positive); continue; }; @@ -735,15 +787,23 @@ impl<'db> EnumComplementType<'db> { } /// Expand this complement to the enum literals that remain possible. - pub fn remaining_literal_types(self, db: &'db dyn Db) -> Vec> { + pub fn remaining_literal_types( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Vec> { self.remaining_member_names(db) - .map(|name| self.remaining_literal_type(db, name)) + .map(|name| self.remaining_literal_type(db, env, name)) .collect() } /// Expand this complement to the union of enum literals that remain possible. - pub(crate) fn remaining_literal_union(self, db: &'db dyn Db) -> Type<'db> { - let alternatives = self.remaining_literal_types(db); + pub(crate) fn remaining_literal_union( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + let alternatives = self.remaining_literal_types(db, env); match alternatives.as_slice() { [] => Type::Never, [single] => *single, @@ -759,14 +819,19 @@ impl<'db> EnumComplementType<'db> { } /// Build the type for one remaining canonical member, preserving any positive rest components. - fn remaining_literal_type(self, db: &'db dyn Db, name: &Name) -> Type<'db> { + fn remaining_literal_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &Name, + ) -> Type<'db> { let literal = Type::enum_literal(EnumLiteralType::new(db, self.enum_class_literal(db), name)); if self.rest(db).is_empty() { return literal; } - let mut builder = IntersectionBuilder::new(db).add_positive(literal); + let mut builder = IntersectionBuilder::new(db, env).add_positive(literal); for rest in self.rest(db) { builder.add_positive_in_place(*rest); } @@ -780,6 +845,7 @@ impl<'db> EnumComplementType<'db> { pub(crate) fn remaining_literal_types_for_display( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, max_literals: usize, ) -> Option>> { if !self.rest(db).is_empty() { @@ -791,18 +857,26 @@ impl<'db> EnumComplementType<'db> { return None; } - Some(self.remaining_literal_types(db)) + Some(self.remaining_literal_types(db, env)) } /// Return the type of a member attribute for all enum literals remaining in this complement. /// /// This handles `.name`, `.value`, `._name_`, and `._value_` by unioning the corresponding /// attribute type from each remaining canonical enum member. - fn member_type(self, db: &'db dyn Db, member_name: &str) -> Option> { + fn member_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + member_name: &str, + ) -> Option> { let enum_class_literal = self.enum_class_literal(db); - let is_enum_subclass = Type::ClassLiteral(self.enum_class(db)) - .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)); - let mut builder = UnionBuilder::new(db); + let is_enum_subclass = Type::ClassLiteral(self.enum_class(db)).is_subtype_of( + db, + env, + KnownClass::Enum.to_subclass_of(db, env), + ); + let mut builder = UnionBuilder::new(db, env); let mut found_member = false; for name in self.remaining_member_names(db) { @@ -831,9 +905,13 @@ impl<'db> EnumComplementType<'db> { } /// Reconstruct the equivalent set-theoretic intersection. - pub(crate) fn to_intersection(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn to_intersection( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { let enum_class = self.enum_class(db); - let mut positive = FxOrderSet::from_iter([enum_class.to_non_generic_instance(db)]); + let mut positive = FxOrderSet::from_iter([enum_class.to_non_generic_instance(db, env)]); positive.extend(self.rest(db).iter().copied()); let mut negative = NegativeIntersectionElements::default(); @@ -860,7 +938,8 @@ pub(crate) fn enum_ignored_names<'db>(db: &'db dyn Db, scope_id: ScopeId<'db>) - }; let ignore_bindings = use_def_map.reachable_symbol_bindings(ignore); - let ignore_place = place_from_bindings(db, ignore_bindings).place; + let env = ProgramEnvironment::from_scope(scope_id); + let ignore_place = place_from_bindings(db, &env, ignore_bindings).place; match ignore_place { Place::Defined(DefinedPlace { ty, .. }) => ty @@ -933,8 +1012,9 @@ pub(crate) fn enum_metadata<'db>( if !spec.has_known_members(db) { return None; } + let env = ProgramEnvironment::from_scope(enum_lit.scope(db)); let value_construction = EnumValueConstruction { - data_type: inherited_enum_data_type(db, ClassLiteral::DynamicEnum(enum_lit)), + data_type: inherited_enum_data_type(db, &env, ClassLiteral::DynamicEnum(enum_lit)), ..EnumValueConstruction::default() }; let mut members = FxIndexMap::default(); @@ -942,7 +1022,7 @@ pub(crate) fn enum_metadata<'db>( let mut enum_values: FxHashMap, Name> = FxHashMap::default(); for (name, ty) in spec.members(db) { if value_construction - .alias_detection_value(db, *ty, false) + .alias_detection_value(db, &env, *ty, false) .and_then(|alias_value_ty| { try_register_alias(alias_value_ty, name, &mut enum_values, &mut aliases) // Identical raw literals remain aliases even when normalization widens. @@ -968,7 +1048,6 @@ pub(crate) fn enum_metadata<'db>( }); } }; - // This is a fast path to avoid traversing the MRO of known classes if class .known(db) @@ -977,7 +1056,9 @@ pub(crate) fn enum_metadata<'db>( return None; } - if !is_enum_class_by_inheritance(db, class) { + let env = ProgramEnvironment::from_file(class.python_file(db)); + + if !is_enum_class_by_inheritance(db, &env, class) { return None; } @@ -995,26 +1076,27 @@ pub(crate) fn enum_metadata<'db>( // Look up custom construction methods, falling back to parent enum classes. An opaque binding // still shadows methods from classes later in the MRO. - let data_type = inherited_enum_data_type(db, ClassLiteral::Static(class)); + let data_type = inherited_enum_data_type(db, &env, ClassLiteral::Static(class)); let user_defined_init = custom_enum_method(db, scope_id, "__init__") - .or_else(|| inherited_user_defined_enum_method(db, class, "__init__")); + .or_else(|| inherited_user_defined_enum_method(db, &env, class, "__init__")); let init = resolve_enum_method(user_defined_init, || { - inherited_known_enum_method(db, class, "__init__") + inherited_known_enum_method(db, &env, class, "__init__") }); // CPython checks `__new_member__` and then `__new__` on each enum base before continuing // through the MRO or falling back to the data-type constructor. let user_defined_new = custom_enum_method(db, scope_id, "__new__") - .or_else(|| inherited_user_defined_enum_new(db, class)) + .or_else(|| inherited_user_defined_enum_new(db, &env, class)) .or_else(|| inherited_user_defined_mixin_new(db, class)); let new = resolve_enum_method(user_defined_new, || { - inherited_known_enum_method(db, class, "__new__") + inherited_known_enum_method(db, &env, class, "__new__") }); let metaclass_may_transform_values = enum_metaclass_may_transform_values(db, class); let user_defined_generate_next_value = - custom_enum_method(db, scope_id, "_generate_next_value_") - .or_else(|| inherited_user_defined_enum_method(db, class, "_generate_next_value_")); + custom_enum_method(db, scope_id, "_generate_next_value_").or_else(|| { + inherited_user_defined_enum_method(db, &env, class, "_generate_next_value_") + }); let generate_next_value = resolve_enum_method(user_defined_generate_next_value, || { - inherited_known_enum_method(db, class, "_generate_next_value_") + inherited_known_enum_method(db, &env, class, "_generate_next_value_") }); let value_construction = EnumValueConstruction { init, @@ -1046,7 +1128,7 @@ pub(crate) fn enum_metadata<'db>( return None; } - let inferred = place_from_bindings(db, bindings).place; + let inferred = place_from_bindings(db, &env, bindings).place; let mut explicit_member_wrapper = false; let value_ty = match inferred { @@ -1067,7 +1149,7 @@ pub(crate) fn enum_metadata<'db>( Some(KnownClass::Member) => { explicit_member_wrapper = true; Some( - ty.member(db, "value") + ty.member(db, &env, "value") .place .ignore_possibly_undefined() .unwrap_or(Type::unknown()), @@ -1082,7 +1164,11 @@ pub(crate) fn enum_metadata<'db>( // `StrEnum`s have different `auto()` behaviour to enums inheriting from `(str, Enum)` let auto_value_ty = if Type::ClassLiteral(ClassLiteral::Static(class)) - .is_subtype_of(db, KnownClass::StrEnum.to_subclass_of(db)) + .is_subtype_of( + db, + &env, + KnownClass::StrEnum.to_subclass_of(db, &env), + ) { Type::string_literal(db, &*name.to_lowercase()) } else { @@ -1094,7 +1180,8 @@ pub(crate) fn enum_metadata<'db>( .filter(|class| { !Type::from(*class).is_subtype_of( db, - KnownClass::Enum.to_subclass_of(db), + &env, + KnownClass::Enum.to_subclass_of(db, &env), ) }) .map(|class| class.known(db)) @@ -1113,7 +1200,7 @@ pub(crate) fn enum_metadata<'db>( [] | [Some(KnownClass::Int)] ) { if prev_value_was_non_literal_int { - KnownClass::Int.to_instance(db) + KnownClass::Int.to_instance(db, &env) } else if let Some(prev_bool_literal) = prev_bool_literal { @@ -1140,6 +1227,7 @@ pub(crate) fn enum_metadata<'db>( let dunder_get = ty .member_lookup_with_policy( db, + &env, "__get__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -1170,7 +1258,7 @@ pub(crate) fn enum_metadata<'db>( declaration.kind(db), DefinitionKind::AnnotatedAssignment(assignment) if assignment - .value(&parsed_module(db, declaration.file(db)).load(db)) + .value(&parsed_module(db, declaration.python_file(db)).load(db)) .is_some() ) }) @@ -1182,7 +1270,7 @@ pub(crate) fn enum_metadata<'db>( // Track whether this member's value is a non-literal `int`, so a // following `auto()` knows to widen its result to `int`. prev_value_was_non_literal_int = value_ty.as_int_like_literal().is_none() - && value_ty.is_assignable_to(db, KnownClass::Int.to_instance(db)); + && value_ty.is_assignable_to(db, &env, KnownClass::Int.to_instance(db, &env)); prev_bool_literal = value_ty .as_literal_value_kind() @@ -1192,7 +1280,7 @@ pub(crate) fn enum_metadata<'db>( }); match value_construction - .alias_detection_value(db, value_ty, auto_members.contains(name)) + .alias_detection_value(db, &env, value_ty, auto_members.contains(name)) .and_then(|alias_value_ty| { try_register_alias(alias_value_ty, name, &mut enum_values, &mut aliases) }) { @@ -1213,12 +1301,12 @@ pub(crate) fn enum_metadata<'db>( return None; } - let value_annotation = custom_value_annotation(db, scope_id) - .or_else(|| inherited_user_defined_value_annotation(db, class)) + let value_annotation = custom_value_annotation(db, &env, scope_id) + .or_else(|| inherited_user_defined_value_annotation(db, &env, class)) .map(EnumValueAnnotation::UserDefined) .or_else(|| { (!metaclass_may_transform_values) - .then(|| inherited_value_annotation(db, class)) + .then(|| inherited_value_annotation(db, &env, class)) .flatten() .map(EnumValueAnnotation::StandardLibrary) }); @@ -1267,8 +1355,11 @@ fn enum_metaclass_may_transform_values<'db>( /// which declare `_value_` annotations that normally should be inherited. fn iter_parent_enum_classes<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, ) -> impl Iterator> + 'db { + let env = env.clone(); + class .iter_mro(db, None) .skip(1) @@ -1281,15 +1372,19 @@ fn iter_parent_enum_classes<'db>( KnownClass::IntEnum | KnownClass::Flag | KnownClass::IntFlag ) }); - (is_traversable && is_enum_class_by_inheritance(db, base)).then_some(base) + (is_traversable && is_enum_class_by_inheritance(db, &env, base)).then_some(base) }) } /// Returns the `_value_` annotation type if one is declared in the given scope. -fn custom_value_annotation<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Option> { +fn custom_value_annotation<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + scope: ScopeId<'db>, +) -> Option> { let symbol_id = place_table(db, scope).symbol_id("_value_")?; let declarations = use_def_map(db, scope).end_of_scope_symbol_declarations(symbol_id); - place_from_declarations(db, declarations) + place_from_declarations(db, env, declarations) .ignore_conflicting_declarations() .ignore_possibly_undefined() } @@ -1297,20 +1392,22 @@ fn custom_value_annotation<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Option< /// Looks up an inherited `_value_` annotation from parent enum classes in the MRO. fn inherited_value_annotation<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, ) -> Option> { - iter_parent_enum_classes(db, class) - .find_map(|base| custom_value_annotation(db, base.body_scope(db))) + iter_parent_enum_classes(db, env, class) + .find_map(|base| custom_value_annotation(db, env, base.body_scope(db))) } /// Looks up an inherited `_value_` annotation from user-defined parent enum classes in the MRO. fn inherited_user_defined_value_annotation<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, ) -> Option> { - iter_parent_enum_classes(db, class) + iter_parent_enum_classes(db, env, class) .filter(|base| base.known(db).is_none()) - .find_map(|base| custom_value_annotation(db, base.body_scope(db))) + .find_map(|base| custom_value_annotation(db, env, base.body_scope(db))) } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -1328,6 +1425,7 @@ enum InheritedEnumDataType { /// precisely when no user-defined non-enum base can affect member construction or attribute access. fn inherited_enum_data_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassLiteral<'db>, ) -> InheritedEnumDataType { let mut selected = InheritedEnumDataType::None; @@ -1350,7 +1448,8 @@ fn inherited_enum_data_type<'db>( return InheritedEnumDataType::Opaque; }; - if base.known(db) == Some(KnownClass::Object) || is_enum_class_by_inheritance(db, base) + if base.known(db) == Some(KnownClass::Object) + || is_enum_class_by_inheritance(db, env, base) { continue; } @@ -1411,10 +1510,11 @@ fn custom_enum_method<'db>( /// Looks up the first user-defined enum method in the MRO. fn inherited_user_defined_enum_method<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, name: &str, ) -> Option> { - iter_parent_enum_classes(db, class) + iter_parent_enum_classes(db, env, class) .filter(|base| base.known(db).is_none()) .find_map(|base| custom_enum_method(db, base.body_scope(db), name)) } @@ -1422,9 +1522,10 @@ fn inherited_user_defined_enum_method<'db>( /// Looks up the first user-defined enum member constructor in the MRO. fn inherited_user_defined_enum_new<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, ) -> Option> { - iter_parent_enum_classes(db, class) + iter_parent_enum_classes(db, env, class) .filter(|base| base.known(db).is_none()) .find_map(|base| { let scope = base.body_scope(db); @@ -1454,10 +1555,11 @@ fn inherited_user_defined_mixin_new<'db>( /// Looks up a resolvable method inherited from a known enum class. fn inherited_known_enum_method<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, name: &str, ) -> Option> { - iter_parent_enum_classes(db, class) + iter_parent_enum_classes(db, env, class) .filter(|base| base.known(db).is_some()) .find_map( |base| match custom_enum_method(db, base.body_scope(db), name) { @@ -1523,13 +1625,16 @@ pub(crate) fn is_enum_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { /// verifies that the class has members. pub(crate) fn is_enum_class_by_inheritance<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: StaticClassLiteral<'db>, ) -> bool { - Type::ClassLiteral(ClassLiteral::Static(class)) - .is_subtype_of(db, KnownClass::Enum.to_subclass_of(db)) - || class - .metaclass(db) - .is_subtype_of(db, KnownClass::EnumType.to_subclass_of(db)) + Type::ClassLiteral(ClassLiteral::Static(class)).is_subtype_of( + db, + env, + KnownClass::Enum.to_subclass_of(db, env), + ) || class + .metaclass(db) + .is_subtype_of(db, env, KnownClass::EnumType.to_subclass_of(db, env)) } /// Extracts the inner value type from an `enum.nonmember()` wrapper. @@ -1538,11 +1643,15 @@ pub(crate) fn is_enum_class_by_inheritance<'db>( /// returns the inner value, not the `nonmember` wrapper. /// /// Returns `Some(value_type)` if the type is a `nonmember[T]`, otherwise `None`. -pub(crate) fn try_unwrap_nonmember_value<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { +pub(crate) fn try_unwrap_nonmember_value<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { match ty { Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Nonmember) => { Some( - ty.member(db, "value") + ty.member(db, env, "value") .place .ignore_possibly_undefined() .unwrap_or(Type::unknown()), diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index 0f3dc83707..317e864ea8 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -6,7 +6,7 @@ use rustc_hash::FxHashSet; -use crate::{AnalysisSettings, Db, place::PlaceAndQualifiers}; +use crate::{AnalysisSettings, Db, ProgramEnvironment, place::PlaceAndQualifiers}; use super::{ CallArguments, EnumLiteralType, IntersectionBuilder, KnownBoundMethodType, KnownClass, @@ -126,6 +126,7 @@ impl<'db> ComparisonResult<'db> { /// constrain `left`. pub(super) fn evaluate_type_equality<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, is_positive: bool, @@ -133,6 +134,7 @@ pub(super) fn evaluate_type_equality<'db>( ) -> Option> { evaluate_type_comparison( db, + env, left, right, is_positive, @@ -144,14 +146,15 @@ pub(super) fn evaluate_type_equality<'db>( /// Return a constraint excluding every value known to compare equal to `ty`. pub(super) fn equality_exclusion_constraint<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, soundness_policy: ComparisonSoundnessPolicy, ) -> Option> { let ty = ty.resolve_type_alias(db); - builtin_literal_constraint(db, ty, ty, ComparisonOperator::Equality, false).or_else(|| { - let mut evaluator = ComparisonEvaluator::new(db, soundness_policy); + builtin_literal_constraint(db, env, ty, ty, ComparisonOperator::Equality, false).or_else(|| { + let mut evaluator = ComparisonEvaluator::new(db, env, soundness_policy); all_values_compare_equal(&mut evaluator, ty, ComparisonOperator::Equality) - .then(|| ty.negate(db)) + .then(|| ty.negate(db, env)) }) } @@ -177,6 +180,7 @@ pub(super) fn equality_exclusion_constraint<'db>( /// ``` pub(super) fn evaluate_type_inequality<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, is_positive: bool, @@ -184,6 +188,7 @@ pub(super) fn evaluate_type_inequality<'db>( ) -> Option> { evaluate_type_comparison( db, + env, left, right, is_positive, @@ -195,6 +200,7 @@ pub(super) fn evaluate_type_inequality<'db>( /// Return a constraint for `left` in the selected branch of an equality or inequality comparison. fn evaluate_type_comparison<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, is_positive: bool, @@ -210,10 +216,11 @@ fn evaluate_type_comparison<'db>( if condition_expects_equality && let Type::TypeVar(typevar) = right && let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = - typevar.typevar(db).bound_or_constraints(db) + typevar.typevar(db).bound_or_constraints(db, env) && constraints.elements(db).iter().all(|constraint| { evaluate_type_comparison( db, + env, left, *constraint, is_positive, @@ -221,7 +228,7 @@ fn evaluate_type_comparison<'db>( soundness_policy, ) .is_some_and(|narrowed| { - equality_truthiness(db, narrowed, *constraint, soundness_policy) + equality_truthiness(db, env, narrowed, *constraint, soundness_policy) == Truthiness::AlwaysTrue }) }) @@ -229,12 +236,12 @@ fn evaluate_type_comparison<'db>( return Some(right); } - enum_literal_constraint(db, left, right, operator, condition_expects_equality) + enum_literal_constraint(db, env, left, right, operator, condition_expects_equality) .or_else(|| { - builtin_literal_constraint(db, left, right, operator, condition_expects_equality) + builtin_literal_constraint(db, env, left, right, operator, condition_expects_equality) }) .or_else(|| { - ComparisonEvaluator::new(db, soundness_policy) + ComparisonEvaluator::new(db, env, soundness_policy) .evaluate(left, right, branch, operator) .constraint(branch) }) @@ -245,12 +252,14 @@ fn evaluate_type_comparison<'db>( /// A result that only permits narrowing remains ambiguous because it can still evaluate either way. pub(crate) fn equality_truthiness<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, soundness_policy: ComparisonSoundnessPolicy, ) -> Truthiness { comparison_truthiness( db, + env, left, right, ComparisonOperator::Equality, @@ -263,12 +272,14 @@ pub(crate) fn equality_truthiness<'db>( /// A result that only permits narrowing remains ambiguous because it can still evaluate either way. pub(super) fn inequality_truthiness<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, soundness_policy: ComparisonSoundnessPolicy, ) -> Truthiness { comparison_truthiness( db, + env, left, right, ComparisonOperator::Inequality, @@ -284,9 +295,13 @@ pub(super) struct TupleEqualityEvaluator<'db> { } impl<'db> TupleEqualityEvaluator<'db> { - pub(super) fn new(db: &'db dyn Db, soundness_policy: ComparisonSoundnessPolicy) -> Self { + pub(super) fn new( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + soundness_policy: ComparisonSoundnessPolicy, + ) -> Self { Self { - evaluator: ComparisonEvaluator::for_truthiness(db, soundness_policy), + evaluator: ComparisonEvaluator::for_truthiness(db, env, soundness_policy), } } @@ -303,6 +318,7 @@ impl<'db> TupleEqualityEvaluator<'db> { let Some(result) = Type::try_call_rich_comparison_dunder( db, + &self.evaluator.env, left, right, "__eq__", @@ -313,7 +329,7 @@ impl<'db> TupleEqualityEvaluator<'db> { }; // Identity can turn a false equality result true, but cannot turn a true result false. - Ok(match result.try_bool(db)? { + Ok(match result.try_bool(db, &self.evaluator.env)? { Truthiness::AlwaysTrue => Truthiness::AlwaysTrue, Truthiness::AlwaysFalse | Truthiness::Ambiguous => Truthiness::Ambiguous, }) @@ -322,12 +338,13 @@ impl<'db> TupleEqualityEvaluator<'db> { fn comparison_truthiness<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, operator: ComparisonOperator, soundness_policy: ComparisonSoundnessPolicy, ) -> Truthiness { - match ComparisonEvaluator::for_truthiness(db, soundness_policy).evaluate( + match ComparisonEvaluator::for_truthiness(db, env, soundness_policy).evaluate( left, right, ComparisonBranch::Positive, @@ -400,24 +417,35 @@ struct ComparisonKey<'db> { /// Tracks comparisons that are already in progress so recursive evaluation terminates. struct ComparisonEvaluator<'db> { db: &'db dyn Db, + env: ProgramEnvironment<'db>, active: FxHashSet>, goal: ComparisonGoal, soundness_policy: ComparisonSoundnessPolicy, } impl<'db> ComparisonEvaluator<'db> { - fn new(db: &'db dyn Db, soundness_policy: ComparisonSoundnessPolicy) -> Self { + fn new( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + soundness_policy: ComparisonSoundnessPolicy, + ) -> Self { Self { db, + env: env.clone(), active: FxHashSet::default(), goal: ComparisonGoal::Constraint, soundness_policy, } } - fn for_truthiness(db: &'db dyn Db, soundness_policy: ComparisonSoundnessPolicy) -> Self { + fn for_truthiness( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + soundness_policy: ComparisonSoundnessPolicy, + ) -> Self { Self { db, + env: env.clone(), active: FxHashSet::default(), goal: ComparisonGoal::Truthiness, soundness_policy, @@ -429,7 +457,14 @@ impl<'db> ComparisonEvaluator<'db> { ty: Type<'db>, operator: ComparisonOperator, ) -> Option { - KnownComparisonSemantics::of_type_with_policy(self.db, ty, operator, self.soundness_policy) + let db = self.db; + KnownComparisonSemantics::of_type_with_policy( + db, + &self.env, + ty, + operator, + self.soundness_policy, + ) } /// Evaluate a comparison recursively, treating `left` as the operand being constrained. @@ -459,8 +494,9 @@ impl<'db> ComparisonEvaluator<'db> { branch: ComparisonBranch, operator: ComparisonOperator, ) -> ComparisonResult<'db> { - let left = left.resolve_type_alias(self.db); - let right = right.resolve_type_alias(self.db); + let db = self.db; + let left = left.resolve_type_alias(db); + let right = right.resolve_type_alias(db); let key = ComparisonKey { left, right, @@ -511,13 +547,14 @@ fn evaluate_dynamic_comparison<'db>( operator: ComparisonOperator, ) -> Option> { let db = evaluator.db; + let env = evaluator.env.clone(); match (left, right) { (Type::Dynamic(_), other) if !operator.condition_expects_equality(branch) && all_values_compare_equal(evaluator, other, operator) => { - let excluded = if other.is_enum(db) - && let Some(alternatives) = finite_alternatives(db, other, operator) + let excluded = if other.is_enum(db, &env) + && let Some(alternatives) = finite_alternatives(db, &env, other, operator) && let [alternative] = alternatives.as_slice() { *alternative @@ -525,7 +562,7 @@ fn evaluate_dynamic_comparison<'db>( other }; Some(ComparisonResult::CanNarrow( - IntersectionBuilder::new(db) + IntersectionBuilder::new(db, &env) .add_positive(left) .add_negative(excluded) .build(), @@ -547,10 +584,11 @@ fn evaluate_finite_comparison<'db>( operator: ComparisonOperator, ) -> Option> { let db = evaluator.db; - finite_alternatives(db, left, operator) + let env = evaluator.env.clone(); + finite_alternatives(db, &env, left, operator) .map(|alternatives| evaluate_union_left(evaluator, &alternatives, right, branch, operator)) .or_else(|| { - finite_alternatives(db, right, operator).map(|alternatives| { + finite_alternatives(db, &env, right, operator).map(|alternatives| { evaluate_union_right(evaluator, left, &alternatives, branch, operator) }) }) @@ -565,6 +603,9 @@ fn evaluate_structural_comparison<'db>( operator: ComparisonOperator, ) -> ComparisonResult<'db> { let db = evaluator.db; + let env = evaluator.env.clone(); + let env = &env; + match (left, right) { ( Type::Never @@ -594,7 +635,7 @@ fn evaluate_structural_comparison<'db>( && all_values_compare_equal(evaluator, other, operator) { ComparisonResult::CanNarrow( - IntersectionBuilder::new(db) + IntersectionBuilder::new(db, env) .add_positive(left) .add_negative(other) .build(), @@ -610,31 +651,31 @@ fn evaluate_structural_comparison<'db>( (Type::TypeVar(left_var), Type::TypeVar(right_var)) if left_var.is_same_typevar_as(db, right_var) && let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = - left_var.typevar(db).bound_or_constraints(db) + left_var.typevar(db).bound_or_constraints(db, env) && constraints.elements(db).iter().all(|constraint| { all_values_compare_equal(evaluator, *constraint, operator) }) => { operator.result_from_equality(true) } - (Type::TypeVar(var), other) => match var.typevar(db).bound_or_constraints(db) { + (Type::TypeVar(var), other) => match var.typevar(db).bound_or_constraints(db, env) { None => ComparisonResult::Ambiguous, Some(TypeVarBoundOrConstraints::UpperBound(_)) => { if !operator.condition_expects_equality(branch) && all_values_compare_equal(evaluator, other, operator) { - ComparisonResult::CanNarrow(other.negate(db)) + ComparisonResult::CanNarrow(other.negate(db, env)) } else { ComparisonResult::Ambiguous } } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - evaluator.evaluate(constraints.as_type(db), other, branch, operator) + evaluator.evaluate(constraints.as_type(db, env), other, branch, operator) } }, - (other, Type::TypeVar(var)) => match var.typevar(db).bound_or_constraints(db) { + (other, Type::TypeVar(var)) => match var.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - evaluator.evaluate(other, constraints.as_type(db), branch, operator) + evaluator.evaluate(other, constraints.as_type(db, env), branch, operator) } None | Some(TypeVarBoundOrConstraints::UpperBound(_)) => ComparisonResult::Ambiguous, }, @@ -662,10 +703,17 @@ fn evaluate_structural_comparison<'db>( ), (Type::LiteralValue(left_literal), Type::LiteralValue(right_literal)) => { - match known_literal_equality(db, left_literal.kind(), right_literal.kind(), operator) { + match known_literal_equality( + db, + env, + left_literal.kind(), + right_literal.kind(), + operator, + ) { Some(equal) => operator.result_from_equality(equal), None => narrow_literal_comparison( db, + env, left, right, left_literal.kind(), @@ -719,8 +767,8 @@ fn evaluate_structural_comparison<'db>( Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderCall(right_function)), ) if left_function == right_function => operator.result_from_equality(true), (left, right) - if has_known_identity_comparison_semantics(db, left, operator) - && has_known_identity_comparison_semantics(db, right, operator) => + if has_known_identity_comparison_semantics(db, env, left, operator) + && has_known_identity_comparison_semantics(db, env, right, operator) => { operator.result_from_equality(left == right) } @@ -730,9 +778,9 @@ fn evaluate_structural_comparison<'db>( } (left, right) - if left.is_singleton(db) - && left.is_equivalent_to(db, right) - && KnownComparisonSemantics::of_type(db, left, operator) + if left.is_singleton(db, env) + && left.is_equivalent_to(db, env, right) + && KnownComparisonSemantics::of_type(db, env, left, operator) == Some(KnownComparisonSemantics::Object) => { operator.result_from_equality(true) @@ -795,6 +843,7 @@ fn is_builtin_literal_type(db: &dyn Db, ty: Type) -> bool { /// both `Literal[0]` and `Literal[False]`, while `x != 1` excludes `Literal[1]` and `Literal[True]`. fn builtin_literal_constraint<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, operator: ComparisonOperator, @@ -804,17 +853,19 @@ fn builtin_literal_constraint<'db>( return None; }; - let equal_to_right = builtin_literals_equal_to(db, Type::LiteralValue(right), right.kind())?; + let equal_to_right = + builtin_literals_equal_to(db, env, Type::LiteralValue(right), right.kind())?; if !condition_expects_equality { let equal_to_right = add_equal_enum_literals( db, + env, left, right.kind(), operator, - UnionBuilder::new(db).add(equal_to_right), + UnionBuilder::new(db, env).add(equal_to_right), ); - return Some(equal_to_right.build().negate(db)); + return Some(equal_to_right.build().negate(db, env)); } match left.resolve_type_alias(db) { @@ -831,22 +882,23 @@ fn builtin_literal_constraint<'db>( /// Return the builtin literal values that compare equal to `literal_type`. fn builtin_literals_equal_to<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, literal_type: Type<'db>, literal: LiteralValueTypeKind<'db>, ) -> Option> { let builder = match literal { LiteralValueTypeKind::Int(value) => { - let mut builder = UnionBuilder::new(db).add(literal_type); + let mut builder = UnionBuilder::new(db, env).add(literal_type); if matches!(value.as_i64(), 0 | 1) { builder = builder.add(Type::bool_literal(value.as_i64() == 1)); } builder } - LiteralValueTypeKind::Bool(value) => UnionBuilder::new(db) + LiteralValueTypeKind::Bool(value) => UnionBuilder::new(db, env) .add(literal_type) .add(Type::int_literal(i64::from(value))), LiteralValueTypeKind::String(_) | LiteralValueTypeKind::Bytes(_) => { - UnionBuilder::new(db).add(literal_type) + UnionBuilder::new(db, env).add(literal_type) } LiteralValueTypeKind::LiteralString | LiteralValueTypeKind::Enum(_) => return None, }; @@ -856,6 +908,7 @@ fn builtin_literals_equal_to<'db>( /// Add finite enum members in `ty` that are known to compare equal to `right`. fn add_equal_enum_literals<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, right: LiteralValueTypeKind<'db>, operator: ComparisonOperator, @@ -864,19 +917,19 @@ fn add_equal_enum_literals<'db>( match ty.resolve_type_alias(db) { Type::Union(union) => { for element in union.elements(db) { - builder = add_equal_enum_literals(db, *element, right, operator, builder); + builder = add_equal_enum_literals(db, env, *element, right, operator, builder); } } Type::LiteralValue(literal) => { if matches!(literal.kind(), LiteralValueTypeKind::Enum(_)) - && known_literal_equality(db, literal.kind(), right, operator) == Some(true) + && known_literal_equality(db, env, literal.kind(), right, operator) == Some(true) { builder = builder.add(Type::LiteralValue(literal)); } } - ty if let Some(alternatives) = finite_alternatives(db, ty, operator) => { + ty if let Some(alternatives) = finite_alternatives(db, env, ty, operator) => { for alternative in alternatives { - builder = add_equal_enum_literals(db, alternative, right, operator, builder); + builder = add_equal_enum_literals(db, env, alternative, right, operator, builder); } } _ => {} @@ -906,6 +959,7 @@ fn add_equal_enum_literals<'db>( /// because those methods can change whether two members compare equal. fn enum_literal_constraint<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, operator: ComparisonOperator, @@ -917,9 +971,14 @@ fn enum_literal_constraint<'db>( let LiteralValueTypeKind::Enum(right) = right_literal.kind() else { return None; }; - if !is_same_enum_domain(db, left, right) - || KnownComparisonSemantics::of_instance(db, right.enum_class_instance(db), operator) - .is_none() + if !is_same_enum_domain(db, env, left, right) + || KnownComparisonSemantics::of_instance( + db, + env, + right.enum_class_instance(db, env), + operator, + ) + .is_none() { return None; } @@ -930,12 +989,13 @@ fn enum_literal_constraint<'db>( EnumLiteralType::new(db, enum_class_literal, name), right_literal.is_promotable(), )); - Some(equal_to_right.negate_if(db, !condition_expects_equality)) + Some(equal_to_right.negate_if(db, env, !condition_expects_equality)) } /// Return whether every possible value of `ty` belongs to the same enum as `right`. pub(super) fn is_same_enum_domain<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, right: EnumLiteralType<'db>, ) -> bool { @@ -948,11 +1008,11 @@ pub(super) fn is_same_enum_domain<'db>( Type::Union(union) => union .elements(db) .iter() - .all(|element| is_same_enum_domain(db, *element, right)), - Type::NominalInstance(instance) => instance.class_literal(db) == right.enum_class(db), + .all(|element| is_same_enum_domain(db, env, *element, right)), + Type::NominalInstance(instance) => instance.class_literal(db, env) == right.enum_class(db), Type::EnumComplement(complement) => complement.enum_class(db) == right.enum_class(db), Type::Intersection(intersection) => intersection - .enum_complement(db) + .enum_complement(db, env) .is_some_and(|complement| complement.enum_class(db) == right.enum_class(db)), _ => false, } @@ -966,6 +1026,7 @@ fn evaluate_union_left<'db>( branch: ComparisonBranch, operator: ComparisonOperator, ) -> ComparisonResult<'db> { + let db = evaluator.db; if evaluator.goal == ComparisonGoal::Truthiness { return combine_definite_truthiness( elements @@ -974,8 +1035,8 @@ fn evaluate_union_left<'db>( ); } - let db = evaluator.db; - evaluate_target_union(db, elements, branch, |element| { + let env = evaluator.env.clone(); + evaluate_target_union(db, &env, elements, branch, |element| { evaluator.evaluate(element, other, branch, operator) }) } @@ -986,6 +1047,7 @@ fn evaluate_union_left<'db>( /// negative constraints for removed arms so that the result still describes the branch predicate. fn evaluate_target_union<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, elements: &[Type<'db>], branch: ComparisonBranch, mut evaluate: impl FnMut(Type<'db>) -> ComparisonResult<'db>, @@ -997,7 +1059,7 @@ fn evaluate_target_union<'db>( let mut all_true = true; let mut all_false = true; let mut narrowed = Vec::with_capacity(elements.len()); - let mut removed = UnionBuilder::new(db); + let mut removed = UnionBuilder::new(db, env); let mut removed_any = false; for element in elements { @@ -1043,13 +1105,13 @@ fn evaluate_target_union<'db>( } let removed = removed_any.then(|| removed.build()); - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for narrowed in narrowed { let Some(mut narrowed) = narrowed else { continue; }; if let Some(removed) = removed { - narrowed = IntersectionBuilder::new(db) + narrowed = IntersectionBuilder::new(db, env) .add_positive(narrowed) .add_negative(removed) .build(); @@ -1067,6 +1129,7 @@ fn evaluate_union_right<'db>( branch: ComparisonBranch, operator: ComparisonOperator, ) -> ComparisonResult<'db> { + let db = evaluator.db; if evaluator.goal == ComparisonGoal::Truthiness { return combine_definite_truthiness( elements @@ -1075,9 +1138,10 @@ fn evaluate_union_right<'db>( ); } - let db = evaluator.db; + let env = evaluator.env.clone(); evaluate_against_results( db, + &env, left, branch, elements @@ -1120,13 +1184,14 @@ fn combine_definite_truthiness<'db>( /// truthiness is reported only when every alternative agrees. fn evaluate_against_results<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, branch: ComparisonBranch, results: impl IntoIterator>, ) -> ComparisonResult<'db> { let mut all_true = true; let mut all_false = true; - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut any = false; for result in results { @@ -1177,6 +1242,7 @@ fn evaluate_intersection_left<'db>( branch: ComparisonBranch, operator: ComparisonOperator, ) -> ComparisonResult<'db> { + let db = evaluator.db; if evaluator.goal == ComparisonGoal::Truthiness { return combine_definite_truthiness( positive @@ -1185,12 +1251,11 @@ fn evaluate_intersection_left<'db>( ); } - let db = evaluator.db; let mut any_true = false; let mut any_false = false; let mut any_ambiguous = false; let mut any_narrowing = false; - let mut builder = IntersectionBuilder::new(db).add_positive(original); + let mut builder = IntersectionBuilder::new(db, &evaluator.env).add_positive(original); for element in positive { match evaluator.evaluate(*element, other, branch, operator) { @@ -1222,26 +1287,29 @@ fn evaluate_intersection_left<'db>( /// may compare equal to values outside the enum domain. fn finite_alternatives<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, operator: ComparisonOperator, ) -> Option>> { match ty { - Type::EnumComplement(complement) => KnownComparisonSemantics::of_type(db, ty, operator) - .is_some() - .then(|| complement.remaining_literal_types(db)), + Type::EnumComplement(complement) => { + KnownComparisonSemantics::of_type(db, env, ty, operator) + .is_some() + .then(|| complement.remaining_literal_types(db, env)) + } Type::Intersection(intersection) => { - let complement = intersection.enum_complement(db)?; - KnownComparisonSemantics::of_type(db, ty, operator) + let complement = intersection.enum_complement(db, env)?; + KnownComparisonSemantics::of_type(db, env, ty, operator) .is_some() - .then(|| complement.remaining_literal_types(db)) + .then(|| complement.remaining_literal_types(db, env)) } Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Bool) => { Some(vec![Type::bool_literal(true), Type::bool_literal(false)]) } Type::NominalInstance(instance) - if KnownComparisonSemantics::of_type(db, ty, operator).is_some() => + if KnownComparisonSemantics::of_type(db, env, ty, operator).is_some() => { - enum_member_literals(db, instance.class_literal(db), None).map(Iterator::collect) + enum_member_literals(db, instance.class_literal(db, env), None).map(Iterator::collect) } _ => None, } @@ -1253,6 +1321,7 @@ fn finite_alternatives<'db>( /// or a string-valued enum member without having a single statically known runtime value. fn narrow_literal_comparison<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, left_literal: LiteralValueTypeKind<'db>, @@ -1261,16 +1330,16 @@ fn narrow_literal_comparison<'db>( ) -> ComparisonResult<'db> { match (left_literal, right_literal) { (LiteralValueTypeKind::LiteralString, LiteralValueTypeKind::String(_)) => { - ComparisonResult::CanNarrow(right.negate_if(db, !equality_is_positive)) + ComparisonResult::CanNarrow(right.negate_if(db, env, !equality_is_positive)) } (LiteralValueTypeKind::String(_), LiteralValueTypeKind::LiteralString) => { - ComparisonResult::CanNarrow(left.negate_if(db, !equality_is_positive)) + ComparisonResult::CanNarrow(left.negate_if(db, env, !equality_is_positive)) } (LiteralValueTypeKind::LiteralString, LiteralValueTypeKind::Enum(enum_literal)) => { - narrow_literal_string_against_enum(db, enum_literal, equality_is_positive) + narrow_literal_string_against_enum(db, env, enum_literal, equality_is_positive) } (LiteralValueTypeKind::Enum(enum_literal), LiteralValueTypeKind::LiteralString) => { - narrow_literal_string_against_enum(db, enum_literal, equality_is_positive) + narrow_literal_string_against_enum(db, env, enum_literal, equality_is_positive) } _ => ComparisonResult::Ambiguous, } @@ -1279,28 +1348,30 @@ fn narrow_literal_comparison<'db>( /// Narrow `LiteralString` against a string-valued enum member with inherited `str` semantics. fn narrow_literal_string_against_enum<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, enum_literal: EnumLiteralType<'db>, equality_is_positive: bool, ) -> ComparisonResult<'db> { if KnownComparisonSemantics::of_type( db, + env, Type::enum_literal(enum_literal), ComparisonOperator::Equality, ) != Some(KnownComparisonSemantics::Str) { return ComparisonResult::Ambiguous; } - let Some(value @ Type::LiteralValue(_)) = enum_literal_value(db, enum_literal) else { + let Some(value @ Type::LiteralValue(_)) = enum_literal_value(db, env, enum_literal) else { return ComparisonResult::Ambiguous; }; let Some(LiteralValueTypeKind::String(_)) = value.as_literal_value_kind() else { return ComparisonResult::Ambiguous; }; - let narrowed = UnionBuilder::new(db) + let narrowed = UnionBuilder::new(db, env) .add(value) .add(Type::enum_literal(enum_literal)) .build() - .negate_if(db, !equality_is_positive); + .negate_if(db, env, !equality_is_positive); ComparisonResult::CanNarrow(narrowed) } @@ -1335,6 +1406,8 @@ fn compare_literal_to_other<'db>( literal_operand: LiteralOperand, ) -> ComparisonResult<'db> { let db = evaluator.db; + let env = evaluator.env.clone(); + let env = &env; if matches!(literal, LiteralValueTypeKind::LiteralString) { return match evaluator.comparison_semantics(other, operator) { @@ -1344,7 +1417,7 @@ fn compare_literal_to_other<'db>( }; } - let Some(literal_semantics) = KnownComparisonSemantics::of_literal(db, literal, operator) + let Some(literal_semantics) = KnownComparisonSemantics::of_literal(db, env, literal, operator) else { return ComparisonResult::Ambiguous; }; @@ -1356,7 +1429,7 @@ fn compare_literal_to_other<'db>( if evaluator.soundness_policy.allow_unsafe_equality && condition_expects_equality && literal_operand == LiteralOperand::Other - && let Some(equal_to_literal) = builtin_literals_equal_to(db, literal_type, literal) + && let Some(equal_to_literal) = builtin_literals_equal_to(db, env, literal_type, literal) && let Some(other_semantics) = unsafe_narrowable_builtin_semantics(db, other) { return if literal_semantics == other_semantics { @@ -1375,7 +1448,7 @@ fn compare_literal_to_other<'db>( // disjoint here. Some(KnownComparisonSemantics::Object) if literal_semantics == KnownComparisonSemantics::Object - && other.is_disjoint_from(db, literal_type) => + && other.is_disjoint_from(db, env, literal_type) => { ComparisonResult::from_bool(operator == ComparisonOperator::Inequality) } @@ -1383,13 +1456,17 @@ fn compare_literal_to_other<'db>( // `int` subclass can compare equal to `1` despite being disjoint from `Literal[1]`. Some(_) if literal_operand == LiteralOperand::Other - && !other.is_disjoint_from(db, literal_type) => + && !other.is_disjoint_from(db, env, literal_type) => { - ComparisonResult::CanNarrow(literal_type.negate_if(db, !condition_expects_equality)) + ComparisonResult::CanNarrow(literal_type.negate_if( + db, + env, + !condition_expects_equality, + )) } Some(_) => ComparisonResult::Ambiguous, None if literal_operand == LiteralOperand::Other && !condition_expects_equality => { - ComparisonResult::CanNarrow(literal_type.negate(db)) + ComparisonResult::CanNarrow(literal_type.negate(db, env)) } None => ComparisonResult::Ambiguous, } @@ -1406,6 +1483,7 @@ fn compare_nominal_instances<'db>( operator: ComparisonOperator, ) -> ComparisonResult<'db> { let db = evaluator.db; + let env = &evaluator.env; let left = Type::NominalInstance(left_instance); let right = Type::NominalInstance(right_instance); let Some(left_semantics) = evaluator.comparison_semantics(left, operator) else { @@ -1416,16 +1494,17 @@ fn compare_nominal_instances<'db>( }; if left_semantics != right_semantics - || (left_semantics == KnownComparisonSemantics::Object && left.is_disjoint_from(db, right)) + || (left_semantics == KnownComparisonSemantics::Object + && left.is_disjoint_from(db, env, right)) { return ComparisonResult::from_bool(operator == ComparisonOperator::Inequality); } - if left == right && left.is_singleton(db) { + if left == right && left.is_singleton(db, env) { ComparisonResult::from_bool(operator == ComparisonOperator::Equality) } else if left_semantics == KnownComparisonSemantics::Tuple - && let Some(left_tuple) = left_instance.tuple_spec(db) - && let Some(right_tuple) = right_instance.tuple_spec(db) + && let Some(left_tuple) = left_instance.tuple_spec(db, env) + && let Some(right_tuple) = right_instance.tuple_spec(db, env) && let Some(left_tuple) = left_tuple.as_fixed_length() && let Some(right_tuple) = right_tuple.as_fixed_length() { @@ -1460,8 +1539,7 @@ fn evaluate_tuple_element_equality<'db>( right: Type<'db>, ) -> Truthiness { let db = evaluator.db; - - if left == right && left.is_singleton(db) { + if left == right && left.is_singleton(db, &evaluator.env) { return Truthiness::AlwaysTrue; } @@ -1535,35 +1613,48 @@ impl KnownComparisonSemantics { /// Determine the builtin comparison implementation inherited by `ty`. /// /// Returns `None` when dunder lookup finds custom or conflicting comparison behavior. - fn of_type<'db>(db: &'db dyn Db, ty: Type<'db>, operator: ComparisonOperator) -> Option { - Self::of_type_with_policy(db, ty, operator, ComparisonSoundnessPolicy::CONSERVATIVE) + fn of_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + operator: ComparisonOperator, + ) -> Option { + Self::of_type_with_policy( + db, + env, + ty, + operator, + ComparisonSoundnessPolicy::CONSERVATIVE, + ) } /// Determine comparison semantics, optionally assuming that subclasses do not override the /// inherited comparison method. fn of_type_with_policy<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, operator: ComparisonOperator, soundness_policy: ComparisonSoundnessPolicy, ) -> Option { match ty { - Type::LiteralValue(literal) => Self::of_literal(db, literal.kind(), operator), + Type::LiteralValue(literal) => Self::of_literal(db, env, literal.kind(), operator), Type::TypedDict(_) => Some(Self::Dict), Type::EnumComplement(complement) => Self::of_instance( db, - complement.enum_class(db).to_non_generic_instance(db), + env, + complement.enum_class(db).to_non_generic_instance(db, env), operator, ), Type::Intersection(intersection) - if let Some(complement) = intersection.enum_complement(db) => + if let Some(complement) = intersection.enum_complement(db, env) => { - let instance = complement.enum_class(db).to_non_generic_instance(db); - Self::of_instance(db, instance, operator) + let instance = complement.enum_class(db).to_non_generic_instance(db, env); + Self::of_instance(db, env, instance, operator) } Type::Intersection(intersection) => { let mut semantics = intersection.positive(db).iter().map(|element| { - Self::of_type_with_policy(db, *element, operator, soundness_policy) + Self::of_type_with_policy(db, env, *element, operator, soundness_policy) }); let first = semantics.next().flatten()?; semantics @@ -1571,23 +1662,27 @@ impl KnownComparisonSemantics { .then_some(first) } Type::NominalInstance(instance) - if instance.class(db).is_final(db) + if instance.class(db, env).is_final(db) || soundness_policy.allow_unsafe_equality // `object` can contain values whose classes define their own comparison // method, so treating it as exact would incorrectly eliminate those values. && !instance.has_known_class(db, KnownClass::Object) => { - Self::of_instance(db, ty, operator) + Self::of_instance(db, env, ty, operator) } Type::SpecialForm(special_form) => KnownComparisonSemantics::of_type_with_policy( db, - special_form.instance_fallback(db), + env, + special_form.instance_fallback(db, env), operator, soundness_policy, ), - Type::KnownInstance(instance) => { - KnownComparisonSemantics::of_instance(db, instance.instance_fallback(db), operator) - } + Type::KnownInstance(instance) => KnownComparisonSemantics::of_instance( + db, + env, + instance.instance_fallback(db, env), + operator, + ), _ => None, } } @@ -1595,6 +1690,7 @@ impl KnownComparisonSemantics { /// Return the builtin comparison implementation used by a literal value. fn of_literal<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, literal: LiteralValueTypeKind<'db>, operator: ComparisonOperator, ) -> Option { @@ -1605,7 +1701,7 @@ impl KnownComparisonSemantics { } LiteralValueTypeKind::Bytes(_) => Some(Self::Bytes), LiteralValueTypeKind::Enum(enum_literal) => { - Self::of_instance(db, enum_literal.enum_class_instance(db), operator) + Self::of_instance(db, env, enum_literal.enum_class_instance(db, env), operator) } } } @@ -1615,17 +1711,26 @@ impl KnownComparisonSemantics { /// Returns `None` when lookup finds custom comparison behavior. fn of_instance<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, instance: Type<'db>, operator: ComparisonOperator, ) -> Option { - let class = instance.to_meta_type(db); - let dunder = lookup_dunder(db, class, operator.dunder()); + instance.nominal_class(db, env)?; + let class = instance.to_meta_type(db, env); + let dunder = lookup_dunder(db, env, class, operator.dunder()); if dunder.place.is_undefined() { if operator == ComparisonOperator::Inequality { - let equality = lookup_dunder(db, class, "__eq__"); + let equality = lookup_dunder(db, env, class, "__eq__"); // `tuple.__ne__` delegates to its builtin equality implementation. - if equality == lookup_dunder(db, KnownClass::Tuple.to_class_literal(db), "__eq__") { + if equality + == lookup_dunder( + db, + env, + KnownClass::Tuple.to_class_literal(db, env), + "__eq__", + ) + { return Some(Self::Tuple); } if !equality.place.is_undefined() { @@ -1642,7 +1747,14 @@ impl KnownComparisonSemantics { (KnownClass::Tuple, Self::Tuple), (KnownClass::Dict, Self::Dict), ] { - if dunder == lookup_dunder(db, known_class.to_class_literal(db), operator.dunder()) { + if dunder + == lookup_dunder( + db, + env, + known_class.to_class_literal(db, env), + operator.dunder(), + ) + { return Some(semantics); } } @@ -1663,18 +1775,23 @@ fn has_reflexive_equality_semantics<'db>( /// Return whether `ty` is a singleton whose comparison uses object identity semantics. fn has_known_identity_comparison_semantics<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, operator: ComparisonOperator, ) -> bool { match ty { Type::FunctionLiteral(_) | Type::ModuleLiteral(_) => true, Type::ClassLiteral(class) => { - KnownComparisonSemantics::of_instance(db, class.metaclass_instance_type(db), operator) - == Some(KnownComparisonSemantics::Object) + KnownComparisonSemantics::of_instance( + db, + env, + class.metaclass_instance_type(db, env), + operator, + ) == Some(KnownComparisonSemantics::Object) } _ => { - ty.is_singleton(db) - && KnownComparisonSemantics::of_type(db, ty, operator) + ty.is_singleton(db, env) + && KnownComparisonSemantics::of_type(db, env, ty, operator) == Some(KnownComparisonSemantics::Object) } } @@ -1683,10 +1800,11 @@ fn has_known_identity_comparison_semantics<'db>( /// Look up a comparison method without falling back to `object`. fn lookup_dunder<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, name: &'static str, ) -> PlaceAndQualifiers<'db> { - ty.member_lookup_with_policy(db, name, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK) + ty.member_lookup_with_policy(db, env, name, MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK) } /// Return the comparison result for two literals when their runtime values determine it. @@ -1696,6 +1814,7 @@ fn lookup_dunder<'db>( /// insufficiently known to produce a definitive result. fn known_literal_equality<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: LiteralValueTypeKind<'db>, right: LiteralValueTypeKind<'db>, operator: ComparisonOperator, @@ -1703,10 +1822,16 @@ fn known_literal_equality<'db>( if let (LiteralValueTypeKind::Enum(left_enum), LiteralValueTypeKind::Enum(right_enum)) = (left, right) && same_enum_member(db, left_enum, right_enum) - && KnownComparisonSemantics::of_instance(db, left_enum.enum_class_instance(db), operator) - .is_none() + && KnownComparisonSemantics::of_instance( + db, + env, + left_enum.enum_class_instance(db, env), + operator, + ) + .is_none() && let Ok(bindings) = Type::enum_literal(left_enum).try_call_dunder_with_policy( db, + env, operator.dunder(), &mut CallArguments::positional([Type::unknown()]), TypeContext::default(), @@ -1714,7 +1839,7 @@ fn known_literal_equality<'db>( | MemberLookupPolicy::MRO_NO_INT_OR_STR_LOOKUP, ) && let Some(result) = bindings - .return_type(db) + .return_type(db, env) .as_literal_value() .and_then(LiteralValueType::as_bool) { @@ -1739,10 +1864,18 @@ fn known_literal_equality<'db>( Some(left.value(db) == right.value(db)) } (LiteralValueTypeKind::Enum(left), LiteralValueTypeKind::Enum(right)) => { - let left_semantics = - KnownComparisonSemantics::of_instance(db, left.enum_class_instance(db), operator)?; - let right_semantics = - KnownComparisonSemantics::of_instance(db, right.enum_class_instance(db), operator)?; + let left_semantics = KnownComparisonSemantics::of_instance( + db, + env, + left.enum_class_instance(db, env), + operator, + )?; + let right_semantics = KnownComparisonSemantics::of_instance( + db, + env, + right.enum_class_instance(db, env), + operator, + )?; if left_semantics != right_semantics { return Some(false); } @@ -1758,8 +1891,9 @@ fn known_literal_equality<'db>( } known_literal_equality( db, - enum_literal_value(db, left)?.as_literal_value_kind()?, - enum_literal_value(db, right)?.as_literal_value_kind()?, + env, + enum_literal_value(db, env, left)?.as_literal_value_kind()?, + enum_literal_value(db, env, right)?.as_literal_value_kind()?, ComparisonOperator::Equality, ) } @@ -1767,15 +1901,17 @@ fn known_literal_equality<'db>( | (other, LiteralValueTypeKind::Enum(enum_literal)) => { let enum_semantics = KnownComparisonSemantics::of_instance( db, - enum_literal.enum_class_instance(db), + env, + enum_literal.enum_class_instance(db, env), operator, )?; - if enum_semantics != KnownComparisonSemantics::of_literal(db, other, operator)? { + if enum_semantics != KnownComparisonSemantics::of_literal(db, env, other, operator)? { return Some(false); } known_literal_equality( db, - enum_literal_value(db, enum_literal)?.as_literal_value_kind()?, + env, + enum_literal_value(db, env, enum_literal)?.as_literal_value_kind()?, other, ComparisonOperator::Equality, ) @@ -1786,8 +1922,8 @@ fn known_literal_equality<'db>( ) | (LiteralValueTypeKind::String(_), LiteralValueTypeKind::LiteralString) => None, (left, right) => { - let left_semantics = KnownComparisonSemantics::of_literal(db, left, operator)?; - let right_semantics = KnownComparisonSemantics::of_literal(db, right, operator)?; + let left_semantics = KnownComparisonSemantics::of_literal(db, env, left, operator)?; + let right_semantics = KnownComparisonSemantics::of_literal(db, env, right, operator)?; (left_semantics != right_semantics).then_some(false) } } @@ -1796,11 +1932,15 @@ fn known_literal_equality<'db>( /// Return the statically known runtime value of an enum member. /// /// Custom enum construction can replace the declared value, so members of such enums return `None`. -fn enum_literal_value<'db>(db: &'db dyn Db, literal: EnumLiteralType<'db>) -> Option> { +fn enum_literal_value<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + literal: EnumLiteralType<'db>, +) -> Option> { let enum_class_literal = literal.enum_class_literal(db); let metadata = enum_metadata(db, enum_class_literal.class_literal(db))?; let name = enum_class_literal.resolve_member(db, literal.name(db))?; - metadata.concrete_value_type(db, name) + metadata.concrete_value_type(db, env, name) } /// Return whether two enum literals resolve to the same member, including aliases. diff --git a/crates/ty_python_semantic/src/types/equality/enums.rs b/crates/ty_python_semantic/src/types/equality/enums.rs index f32b3dc9cd..235ea35d17 100644 --- a/crates/ty_python_semantic/src/types/equality/enums.rs +++ b/crates/ty_python_semantic/src/types/equality/enums.rs @@ -9,7 +9,7 @@ use crate::types::{ EnumClassLiteral, EnumComplementType, EnumLiteralType, IntersectionBuilder, IntersectionType, LiteralValueType, LiteralValueTypeKind, Type, UnionBuilder, }; -use crate::{Db, FxOrderMap, FxOrderSet}; +use crate::{Db, FxOrderMap, FxOrderSet, ProgramEnvironment}; use super::{ ComparisonBranch, ComparisonEvaluator, ComparisonGoal, ComparisonOperator, ComparisonResult, @@ -29,11 +29,13 @@ pub(super) fn evaluate_enum_comparison<'db>( branch: ComparisonBranch, operator: ComparisonOperator, ) -> Option> { - evaluate_enum_domains(evaluator.db, target, other, branch, operator).or_else(|| { - PartitionedEnumComparison::new(evaluator.db, target, other, branch, operator).map( + let db = evaluator.db; + let env = evaluator.env.clone(); + evaluate_enum_domains(db, &env, target, other, branch, operator).or_else(|| { + PartitionedEnumComparison::new(db, &env, target, other, branch, operator).map( |comparison| match comparison.evaluate(evaluator, branch, operator) { ComparisonResult::CanNarrow(narrowed) - if narrowed == target.resolve_type_alias(evaluator.db) => + if narrowed == target.resolve_type_alias(db) => { ComparisonResult::Ambiguous } @@ -49,21 +51,22 @@ pub(super) fn evaluate_enum_comparison<'db>( /// the other. fn evaluate_enum_domains<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, other: Type<'db>, branch: ComparisonBranch, operator: ComparisonOperator, ) -> Option> { - let target = EnumDomainSet::from_type(db, target)?; - let other = EnumDomainSet::from_type(db, other)?; + let target = EnumDomainSet::from_type(db, env, target)?; + let other = EnumDomainSet::from_type(db, env, other)?; if let (Some(target), Some(other)) = (target.single(), other.single()) && target.enum_class == other.enum_class { return SameEnumComparison::new(db, target.clone(), other.clone(), operator) - .evaluate(db, branch, operator); + .evaluate(db, env, branch, operator); } - ProjectedEnumComparison::new(db, target, &other, operator)?.evaluate(db, branch, operator) + ProjectedEnumComparison::new(db, target, &other, operator)?.evaluate(db, env, branch, operator) } /// Compare unions that contain enums and other values. @@ -103,6 +106,7 @@ impl<'db> PartitionedEnumComparison<'db> { /// Return `None` if either enum has unsupported comparison behavior. fn new( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, other: Type<'db>, branch: ComparisonBranch, @@ -114,16 +118,16 @@ impl<'db> PartitionedEnumComparison<'db> { return None; } - let target = EnumDomainPartition::from_type(db, target)?; + let target = EnumDomainPartition::from_type(db, env, target)?; let other_type = other; - let other = EnumDomainPartition::from_type(db, other)?; + let other = EnumDomainPartition::from_type(db, env, other)?; if !target.has_other_values() && !other.has_other_values() { return None; } let enum_result = - evaluate_enum_domains(db, target.enum_type, other.enum_type, branch, operator)?; + evaluate_enum_domains(db, env, target.enum_type, other.enum_type, branch, operator)?; Some(Self { target, @@ -161,6 +165,7 @@ impl<'db> PartitionedEnumComparison<'db> { branch: ComparisonBranch, operator: ComparisonOperator, ) -> ComparisonResult<'db> { + let db = evaluator.db; if let [other] = self.other.alternatives.as_slice() { return self.evaluate_pair(evaluator, target, *other, branch, operator); } @@ -174,12 +179,14 @@ impl<'db> PartitionedEnumComparison<'db> { ); } - if matches!(target.resolve_type_alias(evaluator.db), Type::Dynamic(_)) { + let env = evaluator.env.clone(); + if matches!(target.resolve_type_alias(db), Type::Dynamic(_)) { return evaluator.evaluate(target, self.other_type, branch, operator); } evaluate_against_results( - evaluator.db, + db, + &env, target, branch, self.other @@ -200,6 +207,7 @@ impl<'db> PartitionedEnumComparison<'db> { branch: ComparisonBranch, operator: ComparisonOperator, ) -> ComparisonResult<'db> { + let db = evaluator.db; if let [target] = self.target.alternatives.as_slice() { return self.evaluate_against_other(evaluator, *target, branch, operator); } @@ -212,29 +220,29 @@ impl<'db> PartitionedEnumComparison<'db> { ); } + let env = evaluator.env.clone(); let mut narrowed_enum = None; - let result = - evaluate_target_union(evaluator.db, &self.target.alternatives, branch, |target| { - let result = self.evaluate_against_other(evaluator, target, branch, operator); - if target == self.target.enum_type - && let ComparisonResult::CanNarrow(narrowed) = result - && narrowed != target - { - narrowed_enum = Some(narrowed); - } - result - }); + let result = evaluate_target_union(db, &env, &self.target.alternatives, branch, |target| { + let result = self.evaluate_against_other(evaluator, target, branch, operator); + if target == self.target.enum_type + && let ComparisonResult::CanNarrow(narrowed) = result + && narrowed != target + { + narrowed_enum = Some(narrowed); + } + result + }); if let ComparisonResult::CanNarrow(narrowed) = result && let Some(narrowed_enum) = narrowed_enum - && let Some(domains) = EnumDomainSet::from_type(evaluator.db, self.target.enum_type) + && let Some(domains) = EnumDomainSet::from_type(db, &env, self.target.enum_type) { let excluded = domains .domains .iter() - .fold(UnionBuilder::new(evaluator.db), |builder, domain| { - let domain_type = domain.restriction_type(evaluator.db); - if domain_type.is_disjoint_from(evaluator.db, narrowed_enum) { + .fold(UnionBuilder::new(db, &env), |builder, domain| { + let domain_type = domain.restriction_type(db, &env); + if domain_type.is_disjoint_from(db, &env, narrowed_enum) { builder.add(domain_type) } else { builder @@ -243,7 +251,7 @@ impl<'db> PartitionedEnumComparison<'db> { .build(); if !excluded.is_never() { return ComparisonResult::CanNarrow( - IntersectionBuilder::new(evaluator.db) + IntersectionBuilder::new(db, &env) .add_positive(narrowed) .add_negative(excluded) .build(), @@ -308,6 +316,7 @@ impl<'db> SameEnumComparison<'db> { fn evaluate( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, branch: ComparisonBranch, operator: ComparisonOperator, ) -> Option> { @@ -317,15 +326,15 @@ impl<'db> SameEnumComparison<'db> { Truthiness::Ambiguous if !self.supports_domain_narrowing() => { Some(ComparisonResult::Ambiguous) } - Truthiness::Ambiguous if operator.condition_expects_equality(branch) => { - Some(ComparisonResult::CanNarrow(self.right.restriction_type(db))) - } + Truthiness::Ambiguous if operator.condition_expects_equality(branch) => Some( + ComparisonResult::CanNarrow(self.right.restriction_type(db, env)), + ), Truthiness::Ambiguous => Some(self.right.singleton_type(db).map_or( ComparisonResult::Ambiguous, |singleton| { ComparisonResult::CanNarrow( - IntersectionBuilder::new(db) - .add_positive(self.left.restriction_type(db)) + IntersectionBuilder::new(db, env) + .add_positive(self.left.restriction_type(db, env)) .add_negative(singleton) .build(), ) @@ -379,11 +388,13 @@ impl<'db> EnumValueSet<'db> { /// enum but remains disjoint from the enum's literal members. fn from_type( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, active_types: &mut FxHashSet>, ) -> Option { fn from_type_inner<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, active_types: &mut FxHashSet>, ) -> Option> { @@ -403,7 +414,7 @@ impl<'db> EnumValueSet<'db> { } } Type::NominalInstance(instance) => EnumValueSet { - enum_class: instance.class_literal(db).into_enum_class(db)?, + enum_class: instance.class_literal(db, env).into_enum_class(db)?, members: EnumValueSetMembers::All, }, Type::EnumComplement(complement) => EnumValueSet { @@ -411,10 +422,10 @@ impl<'db> EnumValueSet<'db> { members: EnumValueSetMembers::AllExcept(complement), }, Type::Union(union) => { - EnumValueSet::from_union(db, union.elements(db), active_types)? + EnumValueSet::from_union(db, env, union.elements(db), active_types)? } Type::Intersection(intersection) => { - EnumValueSet::from_intersection(db, intersection, active_types)? + EnumValueSet::from_intersection(db, env, intersection, active_types)? } _ => return None, }; @@ -425,7 +436,7 @@ impl<'db> EnumValueSet<'db> { if !active_types.insert(ty) { return None; } - let value_set = from_type_inner(db, ty, active_types); + let value_set = from_type_inner(db, env, ty, active_types); active_types.remove(&ty); value_set } @@ -435,13 +446,14 @@ impl<'db> EnumValueSet<'db> { /// Whole-domain and complement arms are rejected because they are not exact included sets. fn from_union( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, elements: &[Type<'db>], active_types: &mut FxHashSet>, ) -> Option { let mut enum_class = None; let mut included = FxOrderMap::default(); for element in elements { - let value_set = Self::from_type(db, *element, active_types)?; + let value_set = Self::from_type(db, env, *element, active_types)?; if let Some(enum_class) = enum_class && enum_class != value_set.enum_class { @@ -502,11 +514,12 @@ impl<'db> EnumValueSet<'db> { /// Extract the enum restriction while discarding unrelated positive intersection state. fn from_intersection( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, intersection: IntersectionType<'db>, active_types: &mut FxHashSet>, ) -> Option { - if let Some(complement) = intersection.enum_complement(db) { - return Self::from_type(db, Type::EnumComplement(complement), active_types); + if let Some(complement) = intersection.enum_complement(db, env) { + return Self::from_type(db, env, Type::EnumComplement(complement), active_types); } // Other intersection components can only reduce the represented enum values. Ignoring @@ -514,7 +527,7 @@ impl<'db> EnumValueSet<'db> { let mut value_sets = intersection .positive(db) .iter() - .filter_map(|positive| Self::from_type(db, *positive, active_types)); + .filter_map(|positive| Self::from_type(db, env, *positive, active_types)); let value_set = value_sets.next()?; value_sets .all(|other| other.enum_class == value_set.enum_class) @@ -603,18 +616,18 @@ impl<'db> EnumValueSet<'db> { } /// Reconstruct a constraint containing only this enum value restriction. - fn restriction_type(&self, db: &'db dyn Db) -> Type<'db> { + fn restriction_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match &self.members { EnumValueSetMembers::All => self .enum_class .class_literal(db) - .to_non_generic_instance(db), + .to_non_generic_instance(db, env), EnumValueSetMembers::One { name, promotable } => { self.member_type(db, name, *promotable) } EnumValueSetMembers::Included(members) => members .iter() - .fold(UnionBuilder::new(db), |builder, (name, promotable)| { + .fold(UnionBuilder::new(db, env), |builder, (name, promotable)| { builder.add(self.member_type(db, name, *promotable)) }) .build(), @@ -675,16 +688,17 @@ impl<'db> EnumDomainPartition<'db> { /// Combine enum values while keeping other values in their original order. /// /// Return `None` when there is no enum or a type alias refers to itself. - fn from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { + fn from_type(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> Option { fn collect<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, enum_types: &mut Vec>, alternatives: &mut Vec>, enum_position: &mut Option, active_types: &mut FxHashSet>, ) -> Option<()> { - if EnumValueSet::from_type(db, ty, active_types).is_some() { + if EnumValueSet::from_type(db, env, ty, active_types).is_some() { enum_position.get_or_insert(alternatives.len()); enum_types.push(ty); return Some(()); @@ -702,6 +716,7 @@ impl<'db> EnumDomainPartition<'db> { let result = union.elements(db).iter().try_for_each(|element| { collect( db, + env, *element, enum_types, alternatives, @@ -719,6 +734,7 @@ impl<'db> EnumDomainPartition<'db> { let mut active_types = FxHashSet::default(); collect( db, + env, ty, &mut enum_types, &mut alternatives, @@ -728,7 +744,7 @@ impl<'db> EnumDomainPartition<'db> { let enum_position = enum_position?; let enum_type = enum_types .into_iter() - .fold(UnionBuilder::new(db), UnionBuilder::add) + .fold(UnionBuilder::new(db, env), UnionBuilder::add) .build(); alternatives.insert(enum_position, enum_type); @@ -749,14 +765,15 @@ struct EnumDomainSet<'db> { } impl<'db> EnumDomainSet<'db> { - fn from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { + fn from_type(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> Option { fn collect<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, domains: &mut Vec>, active_types: &mut FxHashSet>, ) -> Option<()> { - if let Some(domain) = EnumValueSet::from_type(db, ty, active_types) { + if let Some(domain) = EnumValueSet::from_type(db, env, ty, active_types) { domains.push(domain); return Some(()); } @@ -764,13 +781,14 @@ impl<'db> EnumDomainSet<'db> { if !active_types.insert(ty) { return None; } - let result = collect_union(db, ty, domains, active_types); + let result = collect_union(db, env, ty, domains, active_types); active_types.remove(&ty); result } fn collect_union<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, domains: &mut Vec>, active_types: &mut FxHashSet>, @@ -779,14 +797,14 @@ impl<'db> EnumDomainSet<'db> { return None; }; for element in union.elements(db) { - collect(db, *element, domains, active_types)?; + collect(db, env, *element, domains, active_types)?; } Some(()) } let mut domains = Vec::new(); let mut active_types = FxHashSet::default(); - collect(db, ty, &mut domains, &mut active_types)?; + collect(db, env, ty, &mut domains, &mut active_types)?; (!domains.is_empty()).then_some(Self { domains }) } @@ -809,11 +827,11 @@ impl<'db> EnumDomainSet<'db> { Some(projection) } - fn restriction_type(&self, db: &'db dyn Db) -> Type<'db> { + fn restriction_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { self.domains .iter() - .fold(UnionBuilder::new(db), |builder, domain| { - builder.add(domain.restriction_type(db)) + .fold(UnionBuilder::new(db, env), |builder, domain| { + builder.add(domain.restriction_type(db, env)) }) .build() } @@ -821,17 +839,18 @@ impl<'db> EnumDomainSet<'db> { fn restrict_for_equality( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, operator: ComparisonOperator, other: &EnumKeyProjection<'db>, ) -> Option> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for domain in &self.domains { let mut projection = EnumKeyProjection::default(); domain.add_keys_to_projection(db, operator, &mut projection)?; if projection.unknowns_may_overlap(other) { - builder = builder.add(domain.restriction_type(db)); + builder = builder.add(domain.restriction_type(db, env)); } else if let Some(retained) = domain.retain_keys(db, operator, &other.keys).ok()? { - builder = builder.add(retained.restriction_type(db)); + builder = builder.add(retained.restriction_type(db, env)); } } Some(builder.build()) @@ -841,10 +860,11 @@ impl<'db> EnumDomainSet<'db> { fn known_equal_type( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, operator: ComparisonOperator, other: &EnumKeyProjection<'db>, ) -> Option> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for domain in &self.domains { let mut projection = EnumKeyProjection::default(); domain.add_keys_to_projection(db, operator, &mut projection)?; @@ -852,7 +872,7 @@ impl<'db> EnumDomainSet<'db> { continue; } if let Some(retained) = domain.retain_keys(db, operator, &other.keys).ok()? { - builder = builder.add(retained.restriction_type(db)); + builder = builder.add(retained.restriction_type(db, env)); } } Some(builder.build()) @@ -900,6 +920,7 @@ impl<'db> ProjectedEnumComparison<'db> { fn evaluate( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, branch: ComparisonBranch, operator: ComparisonOperator, ) -> Option> { @@ -909,16 +930,16 @@ impl<'db> ProjectedEnumComparison<'db> { Truthiness::Ambiguous if operator.condition_expects_equality(branch) => { Some(ComparisonResult::CanNarrow( self.left - .restrict_for_equality(db, operator, &self.right_projection)?, + .restrict_for_equality(db, env, operator, &self.right_projection)?, )) } Truthiness::Ambiguous if self.right_projection.single_key().is_some() => { let equal_left = self.left - .known_equal_type(db, operator, &self.right_projection)?; + .known_equal_type(db, env, operator, &self.right_projection)?; Some(ComparisonResult::CanNarrow( - IntersectionBuilder::new(db) - .add_positive(self.left.restriction_type(db)) + IntersectionBuilder::new(db, env) + .add_positive(self.left.restriction_type(db, env)) .add_negative(equal_left) .build(), )) @@ -1087,9 +1108,13 @@ fn enum_class_key_profile<'db>( enum_class: EnumClassLiteral<'db>, operator: ComparisonOperator, ) -> EnumClassKeyProfile<'db> { + let env = ProgramEnvironment::from_file(enum_class.class_literal(db).python_file(db)); let semantics = KnownComparisonSemantics::of_instance( db, - enum_class.class_literal(db).to_non_generic_instance(db), + &env, + enum_class + .class_literal(db) + .to_non_generic_instance(db, &env), operator, ); let members: Box<[(Name, Option>)]> = enum_class @@ -1099,7 +1124,7 @@ fn enum_class_key_profile<'db>( ( name.clone(), semantics.and_then(|semantics| { - enum_literal_value(db, EnumLiteralType::new(db, enum_class, name)) + enum_literal_value(db, &env, EnumLiteralType::new(db, enum_class, name)) .and_then(|value| enum_comparison_key(semantics, value)) }), ) diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 9983c100df..8c44e50273 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -53,6 +53,7 @@ use std::{borrow::Cow, str::FromStr}; use bitflags::bitflags; use itertools::Either; +use ruff_db::PythonFile; use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; @@ -96,7 +97,7 @@ use crate::types::{ SubclassOfType, Truthiness, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, UnionBuilder, UnionType, binding_type, definition_expression_type, walk_signature, }; -use crate::{Db, FxOrderSet}; +use crate::{Db, FxOrderSet, Program, ProgramEnvironment}; use ty_python_core::ast_ids::HasScopedUseId; use ty_python_core::definition::Definition; use ty_python_core::scope::ScopeId; @@ -331,6 +332,10 @@ impl<'db> OverloadLiteral<'db> { self.body_scope(db).file(db) } + pub(crate) fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.body_scope(db).python_file(db) + } + pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { self.decorators(db).contains(decorator) } @@ -383,17 +388,21 @@ impl<'db> OverloadLiteral<'db> { ) -> Option { let definition = self.definition(db); let file = definition.file(db); - self.node(db, file, &parsed_module(db, file).load(db)) - .decorator_list - .iter() - .find(|decorator| { - predicate(definition_expression_type( - db, - definition, - &decorator.expression, - )) - }) - .map(|decorator| Span::from(file).with_range(decorator.range)) + self.node( + db, + file, + &parsed_module(db, definition.python_file(db)).load(db), + ) + .decorator_list + .iter() + .find(|decorator| { + predicate(definition_expression_type( + db, + definition, + &decorator.expression, + )) + }) + .map(|decorator| Span::from(file).with_range(decorator.range)) } /// Iterate through the decorators on this function, returning the span of the first one @@ -432,7 +441,7 @@ impl<'db> OverloadLiteral<'db> { /// over-invalidation. 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)); + let index = semantic_index(db, body_scope.python_file(db)); index.expect_single_definition(body_scope.node(db).expect_function()) } @@ -442,22 +451,24 @@ impl<'db> OverloadLiteral<'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 module = parsed_module(db, self.file(db)).load(db); - let use_def = semantic_index(db, scope.file(db)).use_def_map(scope.file_scope_id(db)); + let module = parsed_module(db, self.python_file(db)).load(db); + let use_def = + semantic_index(db, scope.python_file(db)).use_def_map(scope.file_scope_id(db)); let use_id = self .body_scope(db) .node(db) .expect_function() .node(&module) .name - .scoped_use_id(db, self.file(db)); + .scoped_use_id(db, self.python_file(db)); + let env = ProgramEnvironment::from_scope(scope); let Place::Defined(DefinedPlace { ty: previous_type, definedness: Definedness::AlwaysDefined, provenance, .. - }) = place_from_bindings(db, use_def.bindings_at_use(use_id)).place + }) = place_from_bindings(db, &env, use_def.bindings_at_use(use_id)).place else { return None; }; @@ -501,17 +512,18 @@ impl<'db> OverloadLiteral<'db> { /// a cross-module dependency directly on the full AST which will lead to cache /// over-invalidation. pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> { - let mut signature = self.raw_signature(db, ReturnCallableTypeVarScope::Public); - let scope = self.body_scope(db); - let module = parsed_module(db, self.file(db)).load(db); + let python_file = self.python_file(db); + let mut signature = self.raw_signature(db, ReturnCallableTypeVarScope::Public); + let module = parsed_module(db, python_file).load(db); let function_node = scope.node(db).expect_function().node(&module); - let index = semantic_index(db, scope.file(db)); + let index = semantic_index(db, python_file); let file_scope_id = scope.file_scope_id(db); let is_generator = file_scope_id.is_generator_function(index); if function_node.is_async && !is_generator { - signature = signature.wrap_coroutine_return_type(db); + let env = ProgramEnvironment::from_file(python_file); + signature = signature.wrap_coroutine_return_type(db, &env); } signature @@ -601,11 +613,13 @@ impl<'db> OverloadLiteral<'db> { .is_some_and(|class| class.is_protocol(db)) } + let env = &ProgramEnvironment::from_scope(self.body_scope(db)); let scope = self.body_scope(db); - let module = parsed_module(db, self.file(db)).load(db); + let python_file = self.python_file(db); + let module = parsed_module(db, python_file).load(db); let function_stmt_node = scope.node(db).expect_function().node(&module); let definition = self.definition(db); - let index = semantic_index(db, scope.file(db)); + let index = semantic_index(db, python_file); let pep695_ctx = function_stmt_node.type_params.as_ref().map(|type_params| { GenericContext::from_type_params(db, index, definition, type_params) }); @@ -629,7 +643,7 @@ impl<'db> OverloadLiteral<'db> { ); let generic_context = raw_signature.generic_context; - raw_signature.add_implicit_self_annotation(db, || { + raw_signature.add_implicit_self_annotation(db, env, || { let is_staticmethod = self.is_staticmethod(db); let is_dunder_new = self.name(db) == "__new__"; if is_staticmethod && !is_dunder_new { @@ -671,7 +685,7 @@ impl<'db> OverloadLiteral<'db> { if method_has_explicit_self || class_is_generic || class_is_fallback { let scope_id = definition.scope(db); let typevar_binding_context = Some(definition); - let index = semantic_index(db, scope_id.file(db)); + let index = semantic_index(db, scope_id.python_file(db)); let class = nearest_enclosing_class(db, index, scope_id).unwrap(); let typing_self = typing_self(db, scope_id, typevar_binding_context, class.into()) @@ -683,6 +697,7 @@ impl<'db> OverloadLiteral<'db> { if self.is_classmethod(db) || is_dunder_new { Some(SubclassOfType::from( db, + env, SubclassOfInner::TypeVar(typing_self), )) } else { @@ -694,10 +709,11 @@ impl<'db> OverloadLiteral<'db> { if self.is_classmethod(db) || is_dunder_new { Some(SubclassOfType::from( db, + env, SubclassOfInner::Class(ClassType::NonGeneric(class_literal)), )) } else { - Some(class_literal.to_non_generic_instance(db)) + Some(class_literal.to_non_generic_instance(db, env)) } } }); @@ -712,7 +728,7 @@ impl<'db> OverloadLiteral<'db> { ) -> (Span, Span) { let file = self.file(db); let span = Span::from(file); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, self.python_file(db)).load(db); let func_def = self.node(db, file, &module); let range = parameter_index .and_then(|parameter_index| { @@ -731,7 +747,7 @@ impl<'db> OverloadLiteral<'db> { pub(crate) fn spans(self, db: &'db dyn Db) -> FunctionSpans { let file = self.file(db); let span = Span::from(file); - let module = parsed_module(db, self.file(db)).load(db); + let module = parsed_module(db, self.python_file(db)).load(db); let func_def = self.node(db, file, &module); let return_type_range = func_def.returns.as_ref().map(|returns| returns.range()); let mut signature = func_def.name.range.cover(func_def.parameters.range); @@ -807,7 +823,7 @@ impl<'db> FunctionLiteral<'db> { self.last_definition.known(db) } - fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { + fn has_known_decorator(self, db: &'db dyn Db, decorator: FunctionDecorators) -> bool { self.iter_overloads_and_implementation(db) .any(|overload| overload.decorators(db).contains(decorator)) } @@ -1000,10 +1016,12 @@ impl<'db> FunctionLiteral<'db> { implementation: OverloadLiteral<'db>, ) -> FunctionBodyKind { let definition = implementation.definition(db); - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let python_file = definition.python_file(db); + let env = ProgramEnvironment::from_file(python_file); + let file = python_file.file(db); + let module = parsed_module(db, python_file).load(db); let node = implementation.node(db, file, &module); - function_body_kind(db, node, |expr| { + function_body_kind(db, &env, node, |expr| { definition_expression_type(db, definition, expr) }) } @@ -1228,7 +1246,7 @@ impl<'db> FunctionType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { // Returned-callable rescoping and type-alias specialization should not rebuild signatures from the // function literal; doing so can re-enter recursive `TypeOf` evaluation. @@ -1321,6 +1339,14 @@ impl<'db> FunctionType<'db> { self.literal(db).last_definition.file(db) } + pub(crate) fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.literal(db).last_definition.python_file(db) + } + + pub(crate) fn program(self, db: &'db dyn Db) -> Program { + self.literal(db).last_definition.body_scope(db).program(db) + } + /// Returns the AST node for this function. pub(super) fn node<'ast>( self, @@ -1348,7 +1374,11 @@ impl<'db> FunctionType<'db> { /// Some decorators are expected to appear on every overload; others are expected to appear /// only the implementation or first overload. This method does not check either of those /// conditions. - pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { + pub(crate) fn has_known_decorator( + self, + db: &'db dyn Db, + decorator: FunctionDecorators, + ) -> bool { self.literal(db).has_known_decorator(db, decorator) } @@ -1517,8 +1547,18 @@ impl<'db> FunctionType<'db> { /// would depend on the function's AST and rerun for every change in that file. #[salsa::tracked( returns(ref), - cycle_initial=|db, id, _| CallableSignature::cycle_initial(db, id), - cycle_fn=|db, cycle, previous, value: CallableSignature<'db>, _| value.cycle_normalized(db, previous, cycle), + cycle_initial=|db, id, function: FunctionType<'db>| { + let env = ProgramEnvironment::from_scope( + function.literal(db).last_definition.body_scope(db), + ); + CallableSignature::cycle_initial(db, &env, id) + }, + cycle_fn=|db, cycle, previous, value: CallableSignature<'db>, function: FunctionType<'db>| { + let env = ProgramEnvironment::from_scope( + function.literal(db).last_definition.body_scope(db), + ); + value.cycle_normalized(db, &env, previous, cycle) + }, heap_size=ruff_memory_usage::heap_size, )] pub(crate) fn signature(self, db: &'db dyn Db) -> CallableSignature<'db> { @@ -1541,7 +1581,8 @@ impl<'db> FunctionType<'db> { db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>, ) -> TypeVarVariance { - self.signature(db).variance_of(db, typevar) + let env = ProgramEnvironment::from_scope(self.literal(db).last_definition.body_scope(db)); + self.signature(db).variance_of(db, &env, typevar) } /// Typed externally-visible signature of the last overload or implementation of this function. @@ -1579,7 +1620,7 @@ impl<'db> FunctionType<'db> { cycle_initial=|_, _, _, _|Signature::bottom(), heap_size=ruff_memory_usage::heap_size, )] - pub(crate) fn last_definition_raw_signature( + pub(super) fn last_definition_raw_signature( self, db: &'db dyn Db, return_callable_typevar_scope: ReturnCallableTypeVarScope, @@ -1623,19 +1664,21 @@ impl<'db> FunctionType<'db> { pub(crate) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { let signatures = self.signature(db); for signature in &signatures.overloads { - signature.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + signature.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -1647,7 +1690,7 @@ impl<'db> FunctionType<'db> { let literal = self.literal(db); let updated_signature = match self.updated_signature(db) { Some(signature) => { - Some(signature.recursive_type_normalized_impl(db, div, nested)?) + Some(signature.recursive_type_normalized_impl(db, env, div, nested)?) } None => None, }; @@ -1657,7 +1700,7 @@ impl<'db> FunctionType<'db> { callables .iter() .map(|callable| { - callable.recursive_type_normalized_impl(db, div, nested) + callable.recursive_type_normalized_impl(db, env, div, nested) }) .collect::>>()?, ), @@ -1760,7 +1803,9 @@ fn check_classinfo_in_isinstance<'db>( classinfo_expr, ); } - Type::NominalInstance(nominal) if let Some(tuple_spec) = nominal.tuple_spec(db) => { + Type::NominalInstance(nominal) + if let Some(tuple_spec) = nominal.tuple_spec(db, context.program_environment()) => + { let element_exprs = match classinfo_expr { Some(ast::Expr::Tuple(tuple_expr)) => Some(&tuple_expr.elts), _ => None, @@ -1794,6 +1839,7 @@ fn report_invalid_union_type_elements<'db>( ) { fn find_invalid_elements<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, function: KnownFunction, ty: Type<'db>, invalid_elements: &mut Vec>, @@ -1806,10 +1852,10 @@ fn report_invalid_union_type_elements<'db>( // `Any` can be used in `issubclass()` calls but not `isinstance()` calls Type::SpecialForm(SpecialFormType::Any) if function == KnownFunction::IsSubclass => {} Type::KnownInstance(KnownInstanceType::UnionType(instance)) => { - match instance.value_expression_types(db) { + match instance.value_expression_types(db, env) { Ok(value_expression_types) => { for element in value_expression_types { - find_invalid_elements(db, function, element, invalid_elements); + find_invalid_elements(db, env, function, element, invalid_elements); } } Err(_) => { @@ -1822,7 +1868,8 @@ fn report_invalid_union_type_elements<'db>( } let mut invalid_elements = vec![]; - find_invalid_elements(db, function, union_type, &mut invalid_elements); + let env = context.program_environment(); + find_invalid_elements(db, env, function, union_type, &mut invalid_elements); let Some((first_invalid_element, other_invalid_elements)) = invalid_elements.split_first() else { @@ -1850,10 +1897,11 @@ fn report_invalid_union_type_elements<'db>( // When we have a secondary annotation pointing at the UnionType expression, // "the union" is unambiguous. Otherwise, spell out the union type in the message. + let env = context.program_environment(); let union_suffix = match (&union_type_expr, union_type) { (None, Type::KnownInstance(KnownInstanceType::UnionType(instance))) => { match instance.union_type(db) { - Ok(ty) => format!(" `{}`", ty.display(db)), + Ok(ty) => format!(" `{}`", ty.display(db, env)), Err(_) => String::new(), } } @@ -1863,16 +1911,16 @@ fn report_invalid_union_type_elements<'db>( match other_invalid_elements { [] => diagnostic.info(format_args!( "Element `{}` in the union{union_suffix} is not a class object", - first_invalid_element.display(db) + first_invalid_element.display(db, env) )), [single] => diagnostic.info(format_args!( "Elements `{}` and `{}` in the union{union_suffix} are not class objects", - first_invalid_element.display(db), - single.display(db), + first_invalid_element.display(db, env), + single.display(db, env), )), _ => diagnostic.info(format_args!( "Element `{}` in the union{union_suffix}, and {} more elements, are not class objects", - first_invalid_element.display(db), + first_invalid_element.display(db, env), other_invalid_elements.len(), )), } @@ -1884,12 +1932,16 @@ fn report_invalid_union_type_elements<'db>( /// instead. fn is_instance_truthiness<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, class: ClassLiteral<'db>, ) -> Truthiness { let is_instance = |ty: &Type<'_>| { - ty.as_nominal_instance() - .is_some_and(|instance| instance.class(db).is_subtype_of_class_literal(db, class)) + ty.as_nominal_instance().is_some_and(|instance| { + instance + .class(db, env) + .is_subtype_of_class_literal(db, class) + }) }; let always_true_if = |test: bool| { @@ -1912,19 +1964,19 @@ fn is_instance_truthiness<'db>( // Along the way, short-circuit to `AlwaysTrue` if we find any positive element // that is always true. Type::Intersection(intersection) => { - let mut effective = IntersectionBuilder::new(db); + let mut effective = IntersectionBuilder::new(db, env); let mut found_tvars_or_newtypes = false; for &positive in intersection.positive(db) { - if is_instance_truthiness(db, positive, class).is_always_true() { + if is_instance_truthiness(db, env, positive, class).is_always_true() { return Truthiness::AlwaysTrue; } else if let Type::TypeVar(tvar) = positive { - match tvar.typevar(db).bound_or_constraints(db) { + match tvar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { effective.add_positive_in_place(bound); } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - effective.add_positive_in_place(constraints.as_type(db)); + effective.add_positive_in_place(constraints.as_type(db, env)); } // A typevar without bounds/constraints has `object` as its implicit upper bound, // and adding `object` to an intersection is a no-op @@ -1944,7 +1996,7 @@ fn is_instance_truthiness<'db>( } for &negative in intersection.negative(db) { - if is_instance_truthiness(db, negative, class).is_always_true() { + if is_instance_truthiness(db, env, negative, class).is_always_true() { return Truthiness::AlwaysFalse; } effective.add_negative_in_place(negative); @@ -1955,12 +2007,12 @@ fn is_instance_truthiness<'db>( if effective == ty { Truthiness::Ambiguous } else { - is_instance_truthiness(db, effective, class) + is_instance_truthiness(db, env, effective, class) } } Type::EnumComplement(complement) => { - is_instance_truthiness(db, complement.to_intersection(db), class) + is_instance_truthiness(db, env, complement.to_intersection(db, env), class) } Type::NominalInstance(..) => always_true_if(is_instance(&ty)), @@ -1971,28 +2023,32 @@ fn is_instance_truthiness<'db>( Type::LiteralValue(..) | Type::ModuleLiteral(..) | Type::FunctionLiteral(..) => { always_true_if( - ty.literal_fallback_instance(db) + ty.literal_fallback_instance(db, env) .as_ref() .is_some_and(is_instance), ) } - Type::ClassLiteral(..) => always_true_if(is_instance(&KnownClass::Type.to_instance(db))), + Type::ClassLiteral(..) => { + always_true_if(is_instance(&KnownClass::Type.to_instance(db, env))) + } - Type::TypeAlias(alias) => is_instance_truthiness(db, alias.value_type(db), class), + Type::TypeAlias(alias) => is_instance_truthiness(db, env, alias.value_type(db), class), - Type::TypeVar(bound_typevar) => match bound_typevar.typevar(db).bound_or_constraints(db) { - None => is_instance_truthiness(db, Type::object(), class), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - is_instance_truthiness(db, bound, class) + Type::TypeVar(bound_typevar) => { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { + None => is_instance_truthiness(db, env, Type::object(), class), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + is_instance_truthiness(db, env, bound, class) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => always_true_if( + constraints + .elements(db) + .iter() + .all(|c| is_instance_truthiness(db, env, *c, class).is_always_true()), + ), } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => always_true_if( - constraints - .elements(db) - .iter() - .all(|c| is_instance_truthiness(db, *c, class).is_always_true()), - ), - }, + } Type::BoundMethod(..) | Type::KnownBoundMethod(..) @@ -2032,19 +2088,25 @@ fn is_instance_truthiness<'db>( /// if isinstance(x, (A, B)): /// return True /// ``` -fn is_instance_tuple_exhaustive<'db>(db: &'db dyn Db, ty: Type<'db>, classinfo: Type<'db>) -> bool { - let Some(tuple) = classinfo.tuple_instance_spec(db) else { +fn is_instance_tuple_exhaustive<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + classinfo: Type<'db>, +) -> bool { + let Some(tuple) = classinfo.tuple_instance_spec(db, env) else { return false; }; if tuple.is_variadic() { return false; } - is_instance_tuple_covers(db, &tuple, ty, &ActiveRecursionDetector::default()) + is_instance_tuple_covers(db, env, &tuple, ty, &ActiveRecursionDetector::default()) } fn is_instance_tuple_covers<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, tuple: &TupleSpec<'db>, ty: Type<'db>, recursion_guard: &ActiveRecursionDetector>, @@ -2053,32 +2115,32 @@ fn is_instance_tuple_covers<'db>( Type::TypeAlias(alias) => recursion_guard.visit( &ty, || true, - || is_instance_tuple_covers(db, tuple, alias.value_type(db), recursion_guard), + || is_instance_tuple_covers(db, env, tuple, alias.value_type(db), recursion_guard), ), Type::Union(union) => union .elements(db) .iter() - .all(|element| is_instance_tuple_covers(db, tuple, *element, recursion_guard)), + .all(|element| is_instance_tuple_covers(db, env, tuple, *element, recursion_guard)), Type::Intersection(intersection) => intersection .positive(db) .iter() - .any(|element| is_instance_tuple_covers(db, tuple, *element, recursion_guard)), - Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db) { + .any(|element| is_instance_tuple_covers(db, env, tuple, *element, recursion_guard)), + Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - is_instance_tuple_covers(db, tuple, bound, recursion_guard) + is_instance_tuple_covers(db, env, tuple, bound, recursion_guard) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { constraints.elements(db).iter().all(|constraint| { - is_instance_tuple_covers(db, tuple, *constraint, recursion_guard) + is_instance_tuple_covers(db, env, tuple, *constraint, recursion_guard) }) } - None => is_instance_tuple_covers(db, tuple, Type::object(), recursion_guard), + None => is_instance_tuple_covers(db, env, tuple, Type::object(), recursion_guard), }, ty => tuple.fixed_elements().any(|element| { let Type::ClassLiteral(class) = element else { return false; }; - is_instance_truthiness(db, ty, *class).is_always_true() + is_instance_truthiness(db, env, ty, *class).is_always_true() }), } } @@ -2104,6 +2166,7 @@ pub(crate) fn function_has_stub_body(node: &ast::StmtFunctionDef) -> bool { /// the analysis is only done on the remaining statements if the first is a docstring. pub(super) fn function_body_kind<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, node: &ast::StmtFunctionDef, infer_type: impl Fn(&ast::Expr) -> Type<'db>, ) -> FunctionBodyKind { @@ -2121,16 +2184,19 @@ pub(super) fn function_body_kind<'db>( node_index: _, range: _, } = raise - && infer_type(exc).is_subtype_of( + { + if infer_type(exc).is_subtype_of( db, + env, UnionType::from_two_elements( db, - KnownClass::NotImplementedError.to_class_literal(db), - KnownClass::NotImplementedError.to_instance(db), + env, + KnownClass::NotImplementedError.to_class_literal(db, env), + KnownClass::NotImplementedError.to_instance(db, env), ), - ) - { - return FunctionBodyKind::AlwaysRaisesNotImplementedError; + ) { + return FunctionBodyKind::AlwaysRaisesNotImplementedError; + } } FunctionBodyKind::Regular @@ -2314,7 +2380,7 @@ impl KnownFunction { let candidate = Self::from_str(name).ok()?; candidate - .check_module(file_to_module(db, definition.file(db))?.known(db)?) + .check_module(file_to_module(db, definition.python_file(db))?.known(db)?) .then_some(candidate) } @@ -2391,16 +2457,18 @@ impl KnownFunction { overload: &mut Binding<'db>, call_arguments: &CallArguments<'_, 'db>, call_expression: &ast::ExprCall, - file: File, ) { let db = context.db(); let parameter_types = overload.parameter_types(); match self { KnownFunction::RevealType => { + let env = context.program_environment(); let revealed_type = overload .arguments_for_parameter(call_arguments, 0) - .fold(UnionBuilder::new(db), |builder, (_, ty)| builder.add(ty)) + .fold(UnionBuilder::new(db, env), |builder, (_, ty)| { + builder.add(ty) + }) .build(); report_revealed_type( context, @@ -2417,7 +2485,8 @@ impl KnownFunction { let Some(member) = literal.as_string() else { return; }; - let ty_members = all_members(db, *ty); + let env = context.program_environment(); + let ty_members = all_members(db, env, *ty); overload.set_return_type(Type::bool_literal( ty_members.iter().any(|m| m.name == member.value(db)), )); @@ -2427,20 +2496,22 @@ impl KnownFunction { let [Some(actual_ty), Some(asserted_ty)] = parameter_types else { return; }; - let asserted_ty = asserted_ty.project_type_form(db); - if actual_ty.is_equivalent_to(db, asserted_ty) { + let env = context.program_environment(); + let asserted_ty = asserted_ty.project_type_form(db, env); + if actual_ty.is_equivalent_to(db, env, asserted_ty) { return; } - let diagnostic = - if actual_ty.is_spellable(db) || !actual_ty.is_subtype_of(db, asserted_ty) { - &TYPE_ASSERTION_FAILURE - } else { - &ASSERT_TYPE_UNSPELLABLE_SUBTYPE - }; + let diagnostic = if actual_ty.is_spellable(db) + || !actual_ty.is_subtype_of(db, env, asserted_ty) + { + &TYPE_ASSERTION_FAILURE + } else { + &ASSERT_TYPE_UNSPELLABLE_SUBTYPE + }; if let Some(builder) = context.report_lint(diagnostic, call_expression) { let mut diagnostic = builder.into_diagnostic(format_args!( "Argument does not have asserted type `{}`", - asserted_ty.display(db), + asserted_ty.display(db, env), )); diagnostic.annotate( @@ -2450,27 +2521,30 @@ impl KnownFunction { .unwrap_or_else(|| ast::AnyNodeRef::from(call_expression)), ), ) - .message(format_args!("Inferred type is `{}`", actual_ty.display(db))), + .message(format_args!( + "Inferred type is `{}`", + actual_ty.display(db, env) + )), ); - if actual_ty.is_subtype_of(db, asserted_ty) { + if actual_ty.is_subtype_of(db, env, asserted_ty) { diagnostic.info(format_args!( "`{inferred_type}` is a subtype of `{asserted_type}`, but they are not equivalent", - asserted_type = asserted_ty.display(db), - inferred_type = actual_ty.display(db), + asserted_type = asserted_ty.display(db, env), + inferred_type = actual_ty.display(db, env), )); } else { diagnostic.info(format_args!( "`{asserted_type}` and `{inferred_type}` are not equivalent types", - asserted_type = asserted_ty.display(db), - inferred_type = actual_ty.display(db), + asserted_type = asserted_ty.display(db, env), + inferred_type = actual_ty.display(db, env), )); } diagnostic.set_concise_message(format_args!( "Type `{}` does not match asserted type `{}`", - actual_ty.display(db), - asserted_ty.display(db), + actual_ty.display(db, env), + asserted_ty.display(db, env), )); } } @@ -2479,7 +2553,8 @@ impl KnownFunction { let [Some(actual_ty)] = parameter_types else { return; }; - if actual_ty.is_equivalent_to(db, Type::Never) { + let env = context.program_environment(); + if actual_ty.is_equivalent_to(db, env, Type::Never) { return; } if let Some(builder) = context.report_lint(&TYPE_ASSERTION_FAILURE, call_expression) @@ -2495,17 +2570,17 @@ impl KnownFunction { ) .message(format_args!( "Inferred type of argument is `{}`", - actual_ty.display(db) + actual_ty.display(db, env) )), ); diagnostic.info(format_args!( "`Never` and `{inferred_type}` are not equivalent types", - inferred_type = actual_ty.display(db), + inferred_type = actual_ty.display(db, env), )); diagnostic.set_concise_message(format_args!( "Type `{}` is not equivalent to `Never`", - actual_ty.display(db), + actual_ty.display(db, env), )); } } @@ -2514,7 +2589,8 @@ impl KnownFunction { let [Some(parameter_ty), message] = parameter_types else { return; }; - let truthiness = match parameter_ty.try_bool(db) { + let env = context.program_environment(); + let truthiness = match parameter_ty.try_bool(db, env) { Ok(truthiness) => truthiness, Err(err) => { err.report_diagnostic( @@ -2544,20 +2620,20 @@ impl KnownFunction { builder.into_diagnostic(format_args!( "Static assertion error: argument of type `{parameter_ty}` \ is always falsy", - parameter_ty = parameter_ty.display(db) + parameter_ty = parameter_ty.display(db, env) )) } else { builder.into_diagnostic(format_args!( "Static assertion error: argument of type `{parameter_ty}` \ has an ambiguous static truthiness", - parameter_ty = parameter_ty.display(db) + parameter_ty = parameter_ty.display(db, env) )) }; if let Some(condition) = call_argument_node(call_expression, "condition", 0) { diagnostic.annotate( Annotation::secondary(context.span(condition)).message(format_args!( "Inferred type of argument is `{}`", - parameter_ty.display(db) + parameter_ty.display(db, env) )), ); } @@ -2568,14 +2644,15 @@ impl KnownFunction { let [Some(casted_type), Some(source_type)] = parameter_types else { return; }; - let casted_type = casted_type.project_type_form(db); - if source_type.is_equivalent_to(db, casted_type) - && non_any_dynamic_content(db, *source_type).is_absent() - && non_any_dynamic_content(db, casted_type).is_absent() + let env = context.program_environment(); + let casted_type = casted_type.project_type_form(db, env); + if source_type.is_equivalent_to(db, env, casted_type) + && non_any_dynamic_content(db, env, *source_type).is_absent() + && non_any_dynamic_content(db, env, casted_type).is_absent() { if let Some(builder) = context.report_lint(&REDUNDANT_CAST, call_expression) { - let source_display = source_type.display(db).to_string(); - let casted_display = casted_type.display(db).to_string(); + let source_display = source_type.display(db, env).to_string(); + let casted_display = casted_type.display(db, env).to_string(); let mut diagnostic = builder.into_diagnostic(format_args!( "Value is already of type `{casted_display}`", )); @@ -2597,7 +2674,7 @@ impl KnownFunction { let value_precedence = OperatorPrecedence::from_expr(value); OperatorPrecedence::from_expr_ref(parent) >= value_precedence }); - let value_text = &source_text(db, file)[value.range()]; + let value_text = &source_text(db, context.file())[value.range()]; let replacement = if needs_parens { format!("({value_text})") } else { @@ -2617,7 +2694,7 @@ impl KnownFunction { let [Some(Type::ClassLiteral(class))] = parameter_types else { return; }; - if class.is_protocol(db) { + if class.is_protocol(context.db()) { return; } report_bad_argument_to_get_protocol_members(context, call_expression, *class); @@ -2627,6 +2704,7 @@ impl KnownFunction { let [Some(param_type)] = parameter_types else { return; }; + let env = context.program_environment(); let Some(protocol_class) = param_type .to_class_type(db) .and_then(|class| class.into_protocol_class(db)) @@ -2648,7 +2726,7 @@ impl KnownFunction { ); diag.annotate(Annotation::primary(span).message(format_args!( "`{}`", - protocol_class.interface(db).display(db) + protocol_class.interface(db).display(db, env) ))); } } @@ -2701,6 +2779,7 @@ impl KnownFunction { if let Some(builder) = context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info) { + let env = context.program_environment(); let mut diag = builder.into_diagnostic("Revealed MRO"); let span = context.span( call_argument_node(call_expression, "cls", 0) @@ -2709,6 +2788,7 @@ impl KnownFunction { let mut message = String::new(); let display_settings = DisplaySettings::from_possibly_ambiguous_types( db, + env, classes .iter() .flat_map(|class| class.iter_mro(db)) @@ -2718,7 +2798,9 @@ impl KnownFunction { message.push('('); for class in class.iter_mro(db) { message.push_str( - &class.display_with(db, display_settings.clone()).to_string(), + &class + .display_with(db, env, display_settings.clone()) + .to_string(), ); // Omit the comma for the last element (which is always `object`) if class @@ -2759,26 +2841,35 @@ impl KnownFunction { ); if self == KnownFunction::IsInstance { + let env = context.program_environment(); let truthiness = match second_argument { - Type::ClassLiteral(class) => is_instance_truthiness(db, *first_arg, *class), + Type::ClassLiteral(class) => { + is_instance_truthiness(db, env, *first_arg, *class) + } Type::SpecialForm( SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable, ) => { - let callable_top = - Type::Callable(CallableType::unknown(db)).top_materialization(db); - if first_arg.is_subtype_of(db, callable_top) { + let callable_top = Type::Callable(CallableType::unknown(db)) + .top_materialization(db, env); + if first_arg.is_subtype_of(db, env, callable_top) { Truthiness::AlwaysTrue } else { Truthiness::Ambiguous } } - _ if is_instance_tuple_exhaustive(db, *first_arg, *second_argument) => { + _ if is_instance_tuple_exhaustive( + db, + env, + *first_arg, + *second_argument, + ) => + { Truthiness::AlwaysTrue } _ => Truthiness::Ambiguous, }; - overload.set_return_type(Type::from_truthiness(db, truthiness)); + overload.set_return_type(Type::from_truthiness(db, env, truthiness)); } } @@ -2807,11 +2898,11 @@ impl KnownFunction { let Some(module_name) = ModuleName::new(module_name) else { return; }; - let Some(module) = resolve_module(db, file, &module_name) else { + let Some(module) = resolve_module(db, context.python_file(), &module_name) else { return; }; - overload.set_return_type(Type::module_literal(db, file, module)); + overload.set_return_type(Type::module_literal(db, context.python_file(), module)); } KnownFunction::TotalOrdering => { @@ -2851,13 +2942,16 @@ pub(super) fn report_revealed_type<'db>( revealed_type: Type<'db>, argument_node: impl Ranged, ) { + let db = context.db(); if let Some(builder) = context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info) { + let env = context.program_environment(); let mut diag = builder.into_diagnostic("Revealed type"); diag.annotate( Annotation::primary(context.span(argument_node)).message(format_args!( "`{}`", revealed_type.display_with( - context.db(), + db, + env, DisplaySettings::default().preserve_long_unions() ) )), @@ -2942,11 +3036,12 @@ pub(crate) mod tests { continue; } - let function_definition = known_module_symbol(&db, module, function_name) - .place - .expect_type() - .expect_function_literal() - .definition(&db); + let function_definition = + known_module_symbol(&db, &db.program_environment(), module, function_name) + .place + .expect_type() + .expect_function_literal() + .definition(&db); assert_eq!( KnownFunction::try_from_definition_and_name( diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index a7db82ec58..e6acbb8a02 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1,3 +1,4 @@ +use crate::{Program, ProgramEnvironment}; use std::borrow::Cow; use std::cell::{Cell, RefCell}; use std::collections::hash_map::Entry; @@ -157,12 +158,12 @@ pub(crate) fn bind_typevar<'db>( /// Create a `typing.Self` type variable for a given class. pub(crate) fn typing_self<'db>( db: &'db dyn Db, - scope_id: ScopeId, + scope_id: ScopeId<'db>, typevar_binding_context: Option>, class: ClassLiteral<'db>, ) -> Option> { - let file = scope_id.file(db); - let index = semantic_index(db, file); + let env = ProgramEnvironment::from_scope(scope_id); + let index = semantic_index(db, scope_id.python_file(db)); let identity = TypeVarIdentity::new( db, @@ -179,6 +180,7 @@ pub(crate) fn typing_self<'db>( ); let bounds = TypeVarBoundOrConstraints::UpperBound(Type::instance( db, + &env, class.identity_specialization(db), )); let typevar = TypeVarInstance::new( @@ -248,6 +250,9 @@ pub(crate) fn typing_self<'db>( /// generic context can coexist without collapsing into each other. #[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] pub struct GenericContext<'db> { + #[returns(copy)] + pub(crate) program: Program, + #[returns(ref)] variables_inner: FxOrderMap, BoundTypeVarInstance<'db>>, } @@ -277,7 +282,7 @@ impl<'db> GenericContext<'db> { Self::variable_from_type_param(db, index, binding_context, type_param) }); - Self::from_typevar_instances(db, variables) + Self::from_typevar_instances_in_program(db, binding_context.program(db), variables) } pub(crate) fn of_node( @@ -311,10 +316,20 @@ impl<'db> GenericContext<'db> { /// Creates a generic context from a list of `BoundTypeVarInstance`s. pub(crate) fn from_typevar_instances( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + type_params: impl IntoIterator>, + ) -> Self { + Self::from_typevar_instances_in_program(db, env.program(db), type_params) + } + + fn from_typevar_instances_in_program( + db: &'db dyn Db, + program: Program, type_params: impl IntoIterator>, ) -> Self { Self::new_internal( db, + program, type_params .into_iter() .map(|variable| (variable.identity(db), variable)) @@ -325,8 +340,11 @@ impl<'db> GenericContext<'db> { /// Merge this generic context with another, returning a new generic context that /// contains type variables from both contexts. pub(crate) fn merge(self, db: &'db dyn Db, other: Self) -> Self { - Self::from_typevar_instances( + let program = self.program(db); + debug_assert_eq!(program, other.program(db)); + Self::from_typevar_instances_in_program( db, + program, self.variables_inner(db) .values() .chain(other.variables_inner(db).values()) @@ -357,8 +375,9 @@ impl<'db> GenericContext<'db> { generic_context: GenericContext<'db>, binding_context: Option>, ) -> GenericContext<'db> { - GenericContext::from_typevar_instances( + GenericContext::from_typevar_instances_in_program( db, + generic_context.program(db), generic_context.variables(db).filter(|bound_typevar| { !(bound_typevar.typevar(db).is_self(db) && binding_context.is_none_or(|binding_context| { @@ -384,13 +403,17 @@ impl<'db> GenericContext<'db> { /// also includes `A@C`. This is needed because at each call site, we need to infer the /// specialized class instance type whose method is being invoked. pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> TypeVarSet<'db> { - #[derive(Default)] - struct CollectTypeVars<'db> { + struct CollectTypeVars<'a, 'db> { + env: &'a ProgramEnvironment<'db>, typevars: RefCell, BoundTypeVarInstance<'db>>>, recursion_guard: TypeCollector<'db>, } - impl<'db> TypeVisitor<'db> for CollectTypeVars<'db> { + impl<'db> TypeVisitor<'db> for CollectTypeVars<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -405,7 +428,9 @@ impl<'db> GenericContext<'db> { .entry(bound_typevar.identity(db)) .or_insert(bound_typevar); let typevar = bound_typevar.typevar(db); - if let Some(bound_or_constraints) = typevar.bound_or_constraints(db) { + if let Some(bound_or_constraints) = + typevar.bound_or_constraints(db, self.program_environment()) + { walk_type_var_bounds(db, bound_or_constraints, self); } } @@ -424,7 +449,12 @@ impl<'db> GenericContext<'db> { db: &'db dyn Db, generic_context: GenericContext<'db>, ) -> TypeVarSet<'db> { - let visitor = CollectTypeVars::default(); + let env = ProgramEnvironment::from_program(generic_context.program(db)); + let visitor = CollectTypeVars { + env: &env, + typevars: RefCell::default(), + recursion_guard: TypeCollector::default(), + }; for bound_typevar in generic_context.variables(db) { visitor.visit_bound_type_var_type(db, bound_typevar); } @@ -516,22 +546,23 @@ impl<'db> GenericContext<'db> { parameters: &Parameters<'db>, return_type: Type<'db>, ) -> Option { + let env = ProgramEnvironment::from_definition(definition); // Find all of the legacy typevars mentioned in the function signature. let mut variables = FxOrderSet::default(); for param in parameters { param .annotated_type() - .find_legacy_typevars(db, Some(definition), &mut variables); + .find_legacy_typevars(db, &env, Some(definition), &mut variables); if let Some(ty) = param.default_type() { - ty.find_legacy_typevars(db, Some(definition), &mut variables); + ty.find_legacy_typevars(db, &env, Some(definition), &mut variables); } } - return_type.find_legacy_typevars(db, Some(definition), &mut variables); + return_type.find_legacy_typevars(db, &env, Some(definition), &mut variables); if variables.is_empty() { return None; } - Some(Self::from_typevar_instances(db, variables)) + Some(Self::from_typevar_instances(db, &env, variables)) } pub(crate) fn merge_pep695_and_legacy( @@ -540,17 +571,17 @@ impl<'db> GenericContext<'db> { legacy_generic_context: Option, ) -> Option { match (legacy_generic_context, pep695_generic_context) { - (Some(legacy_ctx), Some(ctx)) => { + (Some(legacy_ctx), Some(env)) => { if legacy_ctx .variables(db) .exactly_one() .is_ok_and(|bound_typevar| bound_typevar.typevar(db).is_self(db)) { - Some(legacy_ctx.merge(db, ctx)) + Some(legacy_ctx.merge(db, env)) } else { // Invalid mixes retained in the inferred signature are reported during // post-inference validation. - Some(ctx) + Some(env) } } (left, right) => left.or(right), @@ -564,14 +595,15 @@ impl<'db> GenericContext<'db> { definition: Definition<'db>, bases: impl Iterator>, ) -> Option { + let env = ProgramEnvironment::from_definition(definition); let mut variables = FxOrderSet::default(); for base in bases { - base.find_legacy_typevars(db, Some(definition), &mut variables); + base.find_legacy_typevars(db, &env, Some(definition), &mut variables); } if variables.is_empty() { return None; } - Some(Self::from_typevar_instances(db, variables)) + Some(Self::from_typevar_instances(db, &env, variables)) } pub(crate) fn remove_callable_only_typevars( @@ -607,6 +639,7 @@ impl<'db> GenericContext<'db> { FxHashSet>, FxHashMap, CallableType<'db>>, ) { + let env = ProgramEnvironment::from_definition(function_definition); let mut found_only_inside_callable_return = FxHashSet::default(); let replacements = self .found_inside_callable_return @@ -644,10 +677,11 @@ impl<'db> GenericContext<'db> { db, &TypeMapping::ApplySpecialization(apply), TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(&env), ); let generic_context = GenericContext::from_typevar_instances( db, + &env, typevar_replacements.values().copied(), ); let signatures = @@ -669,15 +703,19 @@ impl<'db> GenericContext<'db> { /// A visitor that walks through the parameter and return type annotations, recording /// whether each typevar appears inside and/or outside of a return type `Callable`. - #[derive(Default)] - struct FindTypeVarLocations<'db> { + struct FindTypeVarLocations<'a, 'db> { + env: &'a ProgramEnvironment<'db>, locations: RefCell>, recursion_guard: TypeCollector<'db>, in_return_type: bool, in_callable_type: Cell>>, } - impl<'db> TypeVisitor<'db> for FindTypeVarLocations<'db> { + impl<'db> TypeVisitor<'db> for FindTypeVarLocations<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -744,9 +782,16 @@ impl<'db> GenericContext<'db> { let Some(generic_context) = generic_context else { return (None, return_type); }; + let env = ProgramEnvironment::from_definition(function_definition); // Find whether each typevar appears inside and/or outside a return type Callable. - let mut find_typevar_locations = FindTypeVarLocations::default(); + let mut find_typevar_locations = FindTypeVarLocations { + env: &env, + locations: RefCell::default(), + recursion_guard: TypeCollector::default(), + in_return_type: false, + in_callable_type: Cell::default(), + }; for param in parameters { find_typevar_locations.visit_type(db, param.annotated_type()); } @@ -760,7 +805,8 @@ impl<'db> GenericContext<'db> { .into_inner() .finalize(db, function_definition); let type_mapping = TypeMapping::RescopeReturnCallables(&replacements); - let return_type = return_type.apply_type_mapping(db, &type_mapping, TypeContext::default()); + let return_type = + return_type.apply_type_mapping(db, &env, &type_mapping, TypeContext::default()); // And lastly remove those typevars from the function's generic context. let mut kept_typevars = generic_context @@ -770,7 +816,11 @@ impl<'db> GenericContext<'db> { let generic_context = if kept_typevars.peek().is_none() { None } else { - Some(GenericContext::from_typevar_instances(db, kept_typevars)) + Some(GenericContext::from_typevar_instances( + db, + &env, + kept_typevars, + )) }; (generic_context, return_type) @@ -787,12 +837,13 @@ impl<'db> GenericContext<'db> { ) -> Specialization<'db> { let partial = self.specialize_partial(db, std::iter::repeat_n(None, self.len(db))); if known_class == Some(KnownClass::Tuple) { + let env = ProgramEnvironment::from_program(self.program(db)); Specialization::new( db, self, partial.types(db), None, - Some(TupleType::homogeneous(db, Type::unknown())), + Some(TupleType::homogeneous(db, &env, Type::unknown())), ) } else { partial @@ -824,13 +875,14 @@ impl<'db> GenericContext<'db> { db: &'db dyn Db, known_class: Option, ) -> Specialization<'db> { + let env = ProgramEnvironment::from_program(self.program(db)); Specialization::new( db, self, self.variables(db) .map(|typevar| match typevar.kind(db) { TypeVarKind::LegacyTypeVarTuple | TypeVarKind::Pep695TypeVarTuple => { - Type::homogeneous_tuple(db, Type::unknown()) + Type::homogeneous_tuple(db, &env, Type::unknown()) } TypeVarKind::LegacyParamSpec | TypeVarKind::Pep695ParamSpec => { Type::paramspec_value_callable(db, Parameters::unknown()) @@ -840,7 +892,7 @@ impl<'db> GenericContext<'db> { .collect::>(), None, (known_class == Some(KnownClass::Tuple)) - .then(|| TupleType::homogeneous(db, Type::unknown())), + .then(|| TupleType::homogeneous(db, &env, Type::unknown())), ) } @@ -911,6 +963,7 @@ impl<'db> GenericContext<'db> { db: &'db dyn Db, mut types: Box<[Type<'db>]>, ) -> Specialization<'db> { + let env = ProgramEnvironment::from_program(self.program(db)); let len = types.len(); let variables = self.variables(db).collect_vec(); loop { @@ -935,6 +988,7 @@ impl<'db> GenericContext<'db> { }; let updated = types[i].apply_type_mapping( db, + &env, &TypeMapping::ApplySpecialization(specialization), TypeContext::default(), ); @@ -965,6 +1019,7 @@ impl<'db> GenericContext<'db> { I: IntoIterator>>, I::IntoIter: ExactSizeIterator, { + let env = ProgramEnvironment::from_program(self.program(db)); let types = types.into_iter(); let variables = self.variables(db); assert_eq!(self.len(db), types.len()); @@ -981,7 +1036,7 @@ impl<'db> GenericContext<'db> { for typevar in variables.clone() { expanded.push(match typevar.kind(db) { TypeVarKind::LegacyTypeVarTuple | TypeVarKind::Pep695TypeVarTuple => { - Type::homogeneous_tuple(db, Type::unknown()) + Type::homogeneous_tuple(db, &env, Type::unknown()) } TypeVarKind::LegacyParamSpec | TypeVarKind::Pep695ParamSpec => { Type::paramspec_value_callable(db, Parameters::unknown()) @@ -1010,6 +1065,7 @@ impl<'db> GenericContext<'db> { }; let default = default.apply_type_mapping( db, + &env, &TypeMapping::ApplySpecialization(specialization), TypeContext::default(), ); @@ -1193,10 +1249,11 @@ impl<'db> Specialization<'db> { return self; } + let env = ProgramEnvironment::from_program(self.generic_context(db).program(db)); Self::new( db, self.generic_context(db), - [tuple.tuple(db).homogeneous_element_type(db)].as_slice(), + [tuple.tuple(db).homogeneous_element_type(db, &env)].as_slice(), self.materialization_kind(db), None, ) @@ -1230,8 +1287,10 @@ impl<'db> Specialization<'db> { /// That lets us produce the generic alias `A[int]`, which is the corresponding entry in the /// MRO of `B[int]`. pub(crate) fn apply_specialization(self, db: &'db dyn Db, other: Specialization<'db>) -> Self { + let env = &ProgramEnvironment::from_program(other.generic_context(db).program(db)); let new_specialization = self.apply_type_mapping( db, + env, &TypeMapping::ApplySpecialization(ApplySpecialization::Specialization(other)), ); match other.materialization_kind(db) { @@ -1239,7 +1298,7 @@ impl<'db> Specialization<'db> { Some(materialization_kind) => new_specialization.materialize_impl( db, materialization_kind, - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ), } } @@ -1258,8 +1317,13 @@ impl<'db> Specialization<'db> { ) } - fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self { - self.apply_type_mapping_impl(db, type_mapping, &[], &ApplyTypeMappingVisitor::default()) + fn apply_type_mapping<'a>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + type_mapping: &TypeMapping<'a, 'db>, + ) -> Self { + self.apply_type_mapping_impl(db, type_mapping, &[], &ApplyTypeMappingVisitor::new(env)) } pub(crate) fn apply_type_mapping_impl<'a>( @@ -1267,7 +1331,7 @@ impl<'db> Specialization<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: &[Type<'db>], - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { if let TypeMapping::Materialize(materialization_kind) = type_mapping { return self.materialize_impl(db, *materialization_kind, visitor); @@ -1284,6 +1348,7 @@ impl<'db> Specialization<'db> { materialization_kind, }, ) => { + let env = visitor.env; // An invariant type argument cannot be materialized in isolation. Keep the // specialized argument and record the materialization on this specialization. // Comparing both mappings distinguishes substituted gradual types from @@ -1293,7 +1358,7 @@ impl<'db> Specialization<'db> { db, &TypeMapping::ApplySpecialization(*specialization), tcx, - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ); if new_materialization_kind.is_none() { @@ -1301,7 +1366,7 @@ impl<'db> Specialization<'db> { db, type_mapping, tcx, - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ); if specialized != materialized { new_materialization_kind = Some(*materialization_kind); @@ -1360,6 +1425,7 @@ impl<'db> Specialization<'db> { pub(crate) fn combine(self, db: &'db dyn Db, other: Self) -> Self { let generic_context = self.generic_context(db); assert_eq!(other.generic_context(db), generic_context); + let env = ProgramEnvironment::from_program(generic_context.program(db)); // 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. It's also wrong in case a typevar has a default, in which case it @@ -1371,7 +1437,7 @@ impl<'db> Specialization<'db> { .zip(other.types(db)) .map(|(self_type, other_type)| match (self_type, other_type) { (unknown, known) | (known, unknown) if unknown.is_unknown() => *known, - _ => UnionType::from_two_elements(db, *self_type, *other_type), + _ => UnionType::from_two_elements(db, &env, *self_type, *other_type), }) .collect(); // TODO: Combine the tuple specs too @@ -1382,25 +1448,26 @@ impl<'db> Specialization<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let types = if nested { self.types(db) .iter() - .map(|ty| ty.recursive_type_normalized_impl(db, div, true)) + .map(|ty| ty.recursive_type_normalized_impl(db, env, div, true)) .collect::>>()? } else { self.types(db) .iter() .map(|ty| { - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }) .collect::>() }; let tuple_inner = match self.tuple_inner(db) { - Some(tuple) => Some(tuple.recursive_type_normalized_impl(db, div, nested)?), + Some(tuple) => Some(tuple.recursive_type_normalized_impl(db, env, div, nested)?), None => None, }; let context = self.generic_context(db); @@ -1417,7 +1484,7 @@ impl<'db> Specialization<'db> { self, db: &'db dyn Db, materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { // The top and bottom materializations are fully static types already, so materializing them // further does nothing. @@ -1454,10 +1521,14 @@ impl<'db> Specialization<'db> { if has_dynamic_type && effective_materialization_kind == MaterializationKind::Top - && let Some(upper_bound) = - bound_typevar.typevar(db).top_materialized_upper_bound(db) + && let Some(upper_bound) = bound_typevar.top_materialized_upper_bound(db) { - IntersectionType::from_two_elements(db, materialized, upper_bound) + IntersectionType::from_two_elements( + db, + visitor.env, + materialized, + upper_bound, + ) } else { materialized } @@ -1500,6 +1571,7 @@ impl<'db> Specialization<'db> { pub(crate) fn is_disjoint_from<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Self, constraints: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, @@ -1507,8 +1579,9 @@ impl<'db> Specialization<'db> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = DisjointnessChecker::new( + env, constraints, inferable, &relation_visitor, @@ -1522,15 +1595,16 @@ impl<'db> Specialization<'db> { pub(crate) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { if let Some(tuple) = self.tuple_inner(db) { - tuple.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + tuple.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } else { for ty in self.types(db) { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } } @@ -1554,6 +1628,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return self.check_tuple_type_pair(db, source_tuple, target_tuple); } + let env = self.env; + // A gradual specialization is a subtype of a fully static specialization when all its // valid materializations are subtypes. Materializing the source applies declared bounds // and constraints before comparing arguments. This establishes `C[Any] <: Top[C[Any]]` @@ -1572,7 +1648,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // Performance only: `source_top != source` below already handles unchanged // arguments. Without expanding aliases, treat them as potentially gradual. && source.types(db).iter().any(|ty| { - any_over_type(db, *ty, false, |ty| { + any_over_type(db, env, *ty, false, |ty| { ty.is_dynamic() || matches!(ty, Type::TypeAlias(_)) }) }) @@ -1583,7 +1659,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { && target .types(db) .iter() - .all(|ty| !ty.has_typevar_or_typevar_instance(db)) + .all(|ty| !ty.has_typevar_or_typevar_instance(db, env)) // Only non-pure redundancy needs a target already equal to its top. // Materializing the source otherwise loses the bottom needed to // simplify `Covariant[Any] | Covariant[Any | str]`. Comparing both @@ -1703,6 +1779,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // A lazy upper bound may refer back to the enclosing specialization. Check whether this // type variable is constrained before evaluating its bounds or constraints. + let typevar = bound_typevar.typevar(db); if !typevar.is_constrained(db) { return ty; @@ -1716,7 +1793,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { { return ty; } - let Some(constraints) = typevar.constraints(db) else { + let env = self.env; + let Some(constraints) = typevar.constraints(db, env) else { return ty; }; let argument_bottom = ty.materialize( @@ -1732,8 +1810,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // A viable constraint must overlap the argument's upper materialization and contain // its lower materialization. The upper check matters for `Intersection[int, Any]`, // and the lower check matters for `Any | int`. - if argument_top.is_disjoint_from(db, constraint_top) - || !argument_bottom.is_subtype_of(db, constraint_top) + if argument_top.is_disjoint_from(db, env, constraint_top) + || !argument_bottom.is_subtype_of(db, env, constraint_top) { return None; } @@ -1751,13 +1829,15 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { match materialization { MaterializationKind::Top => IntersectionType::from_two_elements( db, + env, argument_top, - UnionType::from_elements(db, viable_constraints), + UnionType::from_elements(db, env, viable_constraints), ), MaterializationKind::Bottom => UnionType::from_two_elements( db, + env, argument_bottom, - IntersectionType::from_elements(db, viable_constraints), + IntersectionType::from_elements(db, env, viable_constraints), ), } } @@ -1816,12 +1896,23 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { && !ty.is_union() { let ty = ty.materialized_divergent_fallback().unwrap_or(ty); + let env = self.env; let (lower, upper) = if self.relation.is_subtyping() { - (ty.top_materialization(db), ty.bottom_materialization(db)) + ( + ty.top_materialization(db, env), + ty.bottom_materialization(db, env), + ) } else { (ty, ty) }; - ConstraintSet::constrain_typevar(db, self.constraints, typevar, lower, upper) + ConstraintSet::constrain_typevar( + db, + env, + self.constraints, + typevar, + lower, + upper, + ) } else { self.check_type_pair(db, target_type, source_type).and( db, @@ -2130,11 +2221,13 @@ impl<'db> Type<'db> { pub(crate) fn substitute_one_typevar( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bound_typevar: BoundTypeVarInstance<'db>, replacement: Type<'db>, ) -> Type<'db> { self.apply_type_mapping( db, + env, &TypeMapping::ApplySpecialization(ApplySpecialization::Single( bound_typevar, replacement, @@ -2148,6 +2241,7 @@ impl<'db> Type<'db> { /// specialization of a generic function. pub(crate) struct SpecializationBuilder<'db, 'c> { db: &'db dyn Db, + env: &'c ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, pending: ConstraintSet<'db, 'c>, @@ -2218,11 +2312,13 @@ enum ConstraintSetInferenceError<'db> { impl<'db, 'c> SpecializationBuilder<'db, 'c> { pub(crate) fn new( db: &'db dyn Db, + env: &'c ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, ) -> Self { Self { db, + env, constraints, inferable, pending: ConstraintSet::from_bool(constraints, true), @@ -2255,12 +2351,13 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { generic_context: GenericContext<'db>, mut choose: impl FnMut(BoundTypeVarInstance<'db>, Option<&PathBound<'db>>) -> Option>, ) -> Specialization<'db> { + let db = self.db; let types = self .solve_pending_with(generic_context, &mut choose) .unwrap_or_else(|()| self.solve_hash_map_with(generic_context, &mut choose)); let specialization = generic_context - .variables_inner(self.db) + .variables_inner(db) .iter() .map(|(identity, variable)| { types @@ -2269,7 +2366,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { .or_else(|| choose(*variable, None)) }); - generic_context.specialize_recursive(self.db, specialization) + generic_context.specialize_recursive(db, specialization) } /// Build raw type-variable inference, preserving which type variables were left unsolved. @@ -2295,8 +2392,10 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { argument_relations: impl IntoIterator, Type<'db>)>, mut choose: impl FnMut(BoundTypeVarInstance<'db>, Option<&PathBound<'db>>) -> Option>, ) -> TypeVarInference<'db> { + let db = self.db; for (formal, actual) in argument_relations { - let when = actual.when_constraint_set_assignable_to(self.db, formal, self.constraints); + let when = + actual.when_constraint_set_assignable_to(db, self.env, formal, self.constraints); let _ = self.add_type_mappings_from_constraint_set(when); } @@ -2309,13 +2408,14 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { generic_context: GenericContext<'db>, types: &FxHashMap, Type<'db>>, ) -> TypeVarInference<'db> { + let db = self.db; let inferred: Box<[_]> = generic_context - .variables_inner(self.db) + .variables_inner(db) .keys() .map(|identity| types.get(identity).copied()) .collect(); - TypeVarInference::new(self.db, generic_context, inferred) + TypeVarInference::new(db, generic_context, inferred) } fn solve_pending_with( @@ -2323,11 +2423,12 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { generic_context: GenericContext<'db>, choose: &mut impl FnMut(BoundTypeVarInstance<'db>, Option<&PathBound<'db>>) -> Option>, ) -> Result, Type<'db>>, ()> { + let db = self.db; // TODO: Move `ParamSpec` and `TypeVarTuple` handling to the new constraint solver. if generic_context - .variables_inner(self.db) + .variables_inner(db) .values() - .any(|typevar| typevar.is_paramspec(self.db) || typevar.is_typevartuple(self.db)) + .any(|typevar| typevar.is_paramspec(db) || typevar.is_typevartuple(db)) { return Ok(self.solve_hash_map_with(generic_context, choose)); } @@ -2345,7 +2446,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // skipped projection changed precision in LiteralString tests. See the // `ty_micro[pydantic_core_schema_dict]` benchmark for a minimized reproducer. let solutions = match self.pending.solutions_with( - self.db, + db, + self.env, self.constraints, self.inferable, |_variance, path_bound| { @@ -2354,7 +2456,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { return Ok(Some(ty)); } - PathBounds::default_solve(self.db, self.constraints, path_bound) + PathBounds::default_solve(db, self.env, self.constraints, path_bound) }, ) { Solutions::Unsatisfiable => return Err(()), @@ -2367,12 +2469,12 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { let mut types = FxHashMap::default(); for solution in solutions { for binding in solution { - let identity = binding.bound_typevar.identity(self.db); + let identity = binding.bound_typevar.identity(db); types .entry(identity) .and_modify(|existing| { *existing = - UnionType::from_two_elements(self.db, *existing, binding.solution); + UnionType::from_two_elements(db, self.env, *existing, binding.solution); }) .or_insert(binding.solution); } @@ -2386,7 +2488,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // TODO: This is a solution-level projection. A more principled version would live in the // constraint-set solution extraction layer, taking an explicit domain of typevars to solve // for and existentially quantifying away the other typevars in that domain. - for (identity, variable) in generic_context.variables_inner(self.db) { + for (identity, variable) in generic_context.variables_inner(db) { if let Some(ty) = types.get_mut(identity) { *ty = self.remove_inferable_typevar_artifacts_from_solution(*variable, *ty); } @@ -2413,6 +2515,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { identity: BoundTypeVarIdentity<'db>, ty: Type<'db>, ) -> bool { + let db = self.db; match ty { // A bare `T = U` edge only replaces one typevar with another; it does not wrap the // replacement in additional structure and therefore cannot grow during repeated @@ -2421,18 +2524,18 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // Unions and intersections are flattened and deduplicated as they are constructed. // A cyclic reference directly inside one can add elements but cannot create // unbounded nesting. Keep looking inside its elements for a genuinely embedded edge. - Type::Union(union) => union.elements(self.db).iter().any(|element| { + Type::Union(union) => union.elements(db).iter().any(|element| { self.has_expanding_cycle(generic_context, types, identity, *element) }), Type::Intersection(intersection) => intersection - .iter_positive(self.db) - .chain(intersection.iter_negative(self.db)) + .iter_positive(db) + .chain(intersection.iter_negative(db)) .any(|element| self.has_expanding_cycle(generic_context, types, identity, element)), - _ => any_over_type(self.db, ty, false, |nested| { + _ => any_over_type(db, self.env, ty, false, |nested| { nested.as_typevar().is_some_and(|dependency| { - let dependency = dependency.identity(self.db); + let dependency = dependency.identity(db); dependency != identity - && generic_context.contains(self.db, dependency) + && generic_context.contains(db, dependency) && self.reaches_pending_typevar( generic_context, types, @@ -2453,6 +2556,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { target: BoundTypeVarIdentity<'db>, visited: &RefCell>>, ) -> bool { + let db = self.db; if identity == target { return true; } @@ -2461,13 +2565,13 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } types.get(&identity).is_some_and(|ty| { - any_over_type(self.db, *ty, false, |nested| { + any_over_type(db, self.env, *ty, false, |nested| { nested.as_typevar().is_some_and(|dependency| { - let dependency = dependency.identity(self.db); + let dependency = dependency.identity(db); // Recursive specialization skips a typevar's own slot. Only references // through other mappings can recursively expand. dependency != identity - && generic_context.contains(self.db, dependency) + && generic_context.contains(db, dependency) && self.reaches_pending_typevar( generic_context, types, @@ -2485,14 +2589,15 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { target: BoundTypeVarInstance<'db>, ty: Type<'db>, ) -> bool { - let target_context = target.binding_context(self.db); + let db = self.db; + let target_context = target.binding_context(db); ty.as_typevar().is_some_and(|typevar| { // Relationships across binding contexts can intentionally remap one generic context // onto another, as with constructor `self` annotations. Synthetic contexts do not // identify a single source-level binding, so they are not safe to project either. - target_context != BindingContext::Synthetic - && typevar.is_inferable(self.db, self.inferable) - && typevar.binding_context(self.db) == target_context + !matches!(target_context, BindingContext::Synthetic(_)) + && typevar.is_inferable(db, self.inferable) + && typevar.binding_context(db) == target_context }) } @@ -2503,13 +2608,14 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { target: BoundTypeVarInstance<'db>, ty: Type<'db>, ) -> Type<'db> { + let db = self.db; match ty { Type::Intersection(intersection) if intersection - .iter_positive(self.db) + .iter_positive(db) .any(|element| !self.is_inferable_typevar_artifact(target, element)) => { - intersection.map_positive(self.db, |element| { + intersection.map_positive(db, self.env, |element| { if self.is_inferable_typevar_artifact(target, *element) { Type::object() } else { @@ -2519,11 +2625,11 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } Type::Union(union) if union - .elements(self.db) + .elements(db) .iter() .any(|element| !self.is_inferable_typevar_artifact(target, *element)) => { - union.map(self.db, |element| { + union.map(db, self.env, |element| { if self.is_inferable_typevar_artifact(target, *element) { Type::Never } else { @@ -2540,14 +2646,15 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { generic_context: GenericContext<'db>, choose: &mut impl FnMut(BoundTypeVarInstance<'db>, Option<&PathBound<'db>>) -> Option>, ) -> FxHashMap, Type<'db>> { + let db = self.db; generic_context - .variables_inner(self.db) + .variables_inner(db) .iter() .filter_map(|(identity, variable)| { let mapped_ty = self .types .get_mut(identity) - .map(|accumulator| accumulator.get_or_build(self.db)); + .map(|accumulator| accumulator.get_or_build(db, self.env)); let chosen = match mapped_ty { Some(mapped_ty) => { let path_bound = PathBound::exact(*variable, mapped_ty); @@ -2565,10 +2672,11 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { bound_typevar: BoundTypeVarInstance<'db>, ty: Type<'db>, ) { - let identity = bound_typevar.identity(self.db); + let db = self.db; + let identity = bound_typevar.identity(db); match self.types.entry(identity) { Entry::Occupied(mut entry) => { - match bound_typevar.kind(self.db) { + match bound_typevar.kind(db) { TypeVarKind::LegacyParamSpec | TypeVarKind::Pep695ParamSpec => { // TODO: The spec says that when a ParamSpec is used multiple times in a signature, // the type checker can solve it to a common behavioral supertype. We don't @@ -2583,28 +2691,28 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // candidates element-wise using unions. // https://typing.python.org/en/latest/spec/generics.html#type-variable-tuple-equality let accumulator = entry.get_mut(); - let existing = accumulator.get_or_build(self.db); + let existing = accumulator.get_or_build(db, self.env); if existing == ty { return; } - let Some(existing_tuple) = existing.exact_tuple_instance_spec(self.db) - else { + let Some(existing_tuple) = existing.exact_tuple_instance_spec(db) else { return; }; - let Some(new_tuple) = ty.exact_tuple_instance_spec(self.db) else { + let Some(new_tuple) = ty.exact_tuple_instance_spec(db) else { return; }; if existing_tuple.len() != new_tuple.len() { return; } let unioned = TupleSpecBuilder::from(existing_tuple.as_ref()) - .union(self.db, &new_tuple) + .union(db, self.env, &new_tuple) .build(); - *accumulator = - UnionAccumulator::new(Type::tuple(TupleType::new(self.db, &unioned))); + *accumulator = UnionAccumulator::new(Type::tuple(TupleType::new( + db, self.env, &unioned, + ))); } _ => { - entry.get_mut().add(self.db, ty); + entry.get_mut().add(db, self.env, ty); } } } @@ -2619,20 +2727,21 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { bound_typevar: BoundTypeVarInstance<'db>, bounds: ConstraintBounds<'db>, ) { - let identity = bound_typevar.identity(self.db); - if bound_typevar.is_paramspec(self.db) && !self.paramspec_seen.insert(identity) { + let db = self.db; + let identity = bound_typevar.identity(db); + if bound_typevar.is_paramspec(db) && !self.paramspec_seen.insert(identity) { return; } let constraint = ConstraintSet::constrain_typevar_with_bounds( - self.db, + db, + self.env, self.constraints, bound_typevar, bounds.lower, bounds.upper, ); - self.pending - .intersect(self.db, self.constraints, constraint); + self.pending.intersect(db, self.constraints, constraint); } pub(crate) fn inferred_type_is_assignable_to( @@ -2640,12 +2749,13 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { bound_typevar: BoundTypeVarIdentity<'db>, ty: Type<'db>, ) -> bool { + let db = self.db; self.types .get_mut(&bound_typevar) .is_some_and(|inferred_ty| { inferred_ty - .get_or_build(self.db) - .is_assignable_to(self.db, ty) + .get_or_build(db, self.env) + .is_assignable_to(db, self.env, ty) }) } @@ -2688,13 +2798,16 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { &mut self, set: ConstraintSet<'db, 'c>, ) -> Result<(), ConstraintSetInferenceError<'db>> { + let db = self.db; let mut first_error = None; let solutions = match set.solutions_with( - self.db, + db, + self.env, self.constraints, self.inferable, |_variance, path_bound| { - let solution = PathBounds::default_solve(self.db, self.constraints, path_bound); + let solution = + PathBounds::default_solve(db, self.env, self.constraints, path_bound); if solution.is_err() && first_error.is_none() { first_error = self.specialization_error_from_failed_bounds(path_bound); } @@ -2730,15 +2843,16 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { &self, path_bound: &PathBound<'db>, ) -> Option> { + let db = self.db; let bound_typevar = path_bound.bound_typevar; let argument = path_bound.lower?; match bound_typevar - .typevar(self.db) - .bound_or_constraints(self.db)? + .typevar(db) + .bound_or_constraints(db, self.env)? { TypeVarBoundOrConstraints::UpperBound(bound) => (!argument - .when_assignable_to(self.db, bound, self.constraints, self.inferable) - .is_always_satisfied(self.db)) + .when_assignable_to(db, self.env, bound, self.constraints, self.inferable) + .is_always_satisfied(db, self.env)) .then_some(SpecializationError::MismatchedBound { bound_typevar, argument, @@ -2760,8 +2874,9 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { &mut self, when: ConstraintSet<'db, 'c>, ) -> Result<(), SpecializationError<'db>> { + let db = self.db; let result = self.add_type_mappings_from_constraint_set(when); - self.pending.intersect(self.db, self.constraints, when); + self.pending.intersect(db, self.constraints, when); match result { Ok(()) | Err(ConstraintSetInferenceError::Unsatisfiable) => Ok(()), Err(ConstraintSetInferenceError::InvalidTypeVar(error)) => Err(error), @@ -2775,13 +2890,17 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { formal: Type<'db>, actual: UnionType<'db>, ) -> Option> { - fn is_string_keyed_mapping<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { + fn is_string_keyed_mapping<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { let Type::NominalInstance(instance) = ty.resolve_type_alias(db) else { return false; }; matches!( - instance.class(db).known(db), + instance.class(db, env).known(db), Some( KnownClass::Dict | KnownClass::Mapping @@ -2791,18 +2910,19 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { | KnownClass::OrderedDict ) ) && instance - .class(db) + .class(db, env) .into_generic_alias() .is_some_and(|alias| { matches!( alias.specialization(db).types(db), - [key, _] if key.resolve_type_alias(db) == KnownClass::Str.to_instance(db) + [key, _] if key.resolve_type_alias(db) == KnownClass::Str.to_instance(db, env) ) }) } fn collect_typed_dicts<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, resolving: &mut FxHashSet>, completed: &mut FxHashMap, bool>, @@ -2826,6 +2946,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { let result = union.elements(db).iter().all(|element| { collect_typed_dicts( db, + env, *element, resolving, completed, @@ -2846,8 +2967,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { element.is_typed_dict() || element == KnownClass::Dict - .to_instance_unknown(db) - .top_materialization(db) + .to_instance_unknown(db, env) + .top_materialization(db, env) }) => { // `isinstance(value, dict)` narrows a `TypedDict` to an intersection with @@ -2860,14 +2981,14 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { if intersection.negative(db).is_empty() && intersection .iter_positive(db) - .any(|element| is_string_keyed_mapping(db, element)) + .any(|element| is_string_keyed_mapping(db, env, element)) && intersection.iter_positive(db).all(|element| { let element = element.resolve_type_alias(db); - is_string_keyed_mapping(db, element) + is_string_keyed_mapping(db, env, element) || element == KnownClass::Dict - .to_instance_unknown(db) - .top_materialization(db) + .to_instance_unknown(db, env) + .top_materialization(db, env) }) => { // `isinstance(value, dict)` can also narrow a mapping to an intersection with @@ -2876,7 +2997,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { other_types.insert(ty); true } - Type::NominalInstance(_) if is_string_keyed_mapping(db, ty) => { + Type::NominalInstance(_) if is_string_keyed_mapping(db, env, ty) => { other_types.insert(ty); true } @@ -2885,14 +3006,18 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { completed.insert(ty, result); result } + let db = self.db; let mut resolving = FxHashSet::default(); let mut completed = FxHashMap::default(); let mut typed_dicts = FxHashSet::default(); let mut other_types = FxOrderSet::default(); - if !actual.elements(self.db).iter().all(|element| { + let env = self.env; + + if !actual.elements(db).iter().all(|element| { collect_typed_dicts( - self.db, + db, + env, *element, &mut resolving, &mut completed, @@ -2909,8 +3034,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // fallback erases; restrict mixed unions to the protocol used by dictionary constructors. if !other_types.is_empty() && !matches!(formal, Type::ProtocolInstance(protocol) - if protocol.class_origin(self.db).is_some_and(|class| { - class.is_known(self.db, KnownClass::SupportsKeysAndGetItem) + if protocol.class_origin(db).is_some_and(|class| { + class.is_known(db, KnownClass::SupportsKeysAndGetItem) })) { return None; @@ -2919,22 +3044,23 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // Use the read-only `Mapping[str, object]` as the fallback rather than `dict[str, object]`. // The current constraint solver can consider mutable protocol constraints equivalent even // when a `TypedDict` preserves more precise correlations between its keys and values. - let spec = &[KnownClass::Str.to_instance(self.db), Type::object()]; - let mapping = KnownClass::Mapping.to_specialized_instance(self.db, spec); - let mapping_when = mapping.when_constraint_set_assignable_to_owned(self.db, formal); - let mapping_when = self.constraints.load(self.db, &mapping_when); + let spec = &[KnownClass::Str.to_instance(db, env), Type::object()]; + let mapping = KnownClass::Mapping.to_specialized_instance(db, env, spec); + let mapping_when = mapping.when_constraint_set_assignable_to_owned(db, env, formal); + let mapping_when = self.constraints.load(db, env, &mapping_when); // Logically equivalent constraints can still infer different solutions, such as `Any` // instead of `object`; preserve the original constraints when gradual evidence differs. - let mapping_solutions = mapping_when.solutions(self.db, self.constraints, self.inferable); + let mapping_solutions = mapping_when.solutions(db, env, self.constraints, self.inferable); if !typed_dicts.into_iter().all(|element| { let element_when = self.constraints.load( - self.db, - &element.when_constraint_set_assignable_to_owned(self.db, formal), + db, + env, + &element.when_constraint_set_assignable_to_owned(db, env, formal), ); element_when - .iff(self.db, self.constraints, mapping_when) - .is_always_satisfied(self.db) - && element_when.solutions(self.db, self.constraints, self.inferable) + .iff(db, self.constraints, mapping_when) + .is_always_satisfied(db, env) + && element_when.solutions(db, env, self.constraints, self.inferable) == mapping_solutions }) { return None; @@ -2942,13 +3068,14 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // Reuse one constraint for all equivalent TypedDicts, but retain each mapping arm's // original constraints. - Some(mapping_when.and(self.db, self.constraints, || { + Some(mapping_when.and(db, self.constraints, || { other_types .into_iter() - .when_all(self.db, self.constraints, |element| { + .when_all(db, self.constraints, |element| { self.constraints.load( - self.db, - &element.when_constraint_set_assignable_to_owned(self.db, formal), + db, + env, + &element.when_constraint_set_assignable_to_owned(db, env, formal), ) }) })) @@ -2963,19 +3090,26 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { formal_signature: &CallableSignature<'db>, actual_callables: &CallableTypes<'db>, ) -> Result<(), SpecializationError<'db>> { + let db = self.db; let formal_is_single_paramspec = formal_signature.is_single_paramspec().is_some(); for actual_callable in actual_callables.as_slice() { if formal_is_single_paramspec { let when = actual_callable - .signatures(self.db) - .when_constraint_set_assignable_to(self.db, formal_signature, self.constraints); + .signatures(db) + .when_constraint_set_assignable_to( + db, + self.env, + formal_signature, + self.constraints, + ); self.infer_from_constraint_set(when)?; } else { // An overloaded actual callable is compatible with the formal signature if at // least one of its overloads is. We collect type mappings from all satisfiable // overloads, and only report an error if none of them are satisfiable. - let db = self.db; + + let env = self.env.clone(); let constraints = self.constraints; let mut first_error = None; let combined = actual_callable @@ -2985,6 +3119,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { .filter_map(|actual_signature| { let when = actual_signature.when_constraint_set_assignable_to_signatures( db, + &env, formal_signature, constraints, ); @@ -3004,7 +3139,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } return Ok(()); }; - self.pending.intersect(self.db, self.constraints, combined); + self.pending.intersect(db, self.constraints, combined); } } Ok(()) @@ -3031,6 +3166,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { polarity: TypeVarVariance, seen: &mut FxHashSet<(Type<'db>, Type<'db>)>, ) -> Result<(), SpecializationError<'db>> { + let db = self.db; // TODO: Eventually, the builder will maintain a constraint set, instead of a hash-map of // type mappings, to represent the specialization that we are building up. At that point, // this method will just need to compare `actual ≤ formal`, using constraint set @@ -3054,21 +3190,21 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // // For example, if `formal` is `list[T]` and `actual` is `list[int] | None`, we want to // specialize `T` to `int`, and so ignore the `None`. - let actual = actual.filter_disjoint_elements(self.db, formal, self.inferable); - let formal = formal.filter_disjoint_elements(self.db, actual, self.inferable); + let actual = actual.filter_disjoint_elements(db, self.env, formal, self.inferable); + let formal = formal.filter_disjoint_elements(db, self.env, actual, self.inferable); match (formal, actual) { // Expand PEP 695 type aliases in the formal type. // This is necessary for solving generics like `def head[T](my_list: MyList[T]) -> T`. (Type::TypeAlias(alias), _) => { - return self.infer_map_impl(alias.value_type(self.db), actual, polarity, seen); + return self.infer_map_impl(alias.value_type(db), actual, polarity, seen); } (Type::TypeForm(formal_typeform), Type::TypeForm(actual_typeform)) => { let variance = TypeVarVariance::Covariant.compose(polarity); return self.infer_map_impl( - formal_typeform.type_argument(self.db), - actual_typeform.type_argument(self.db), + formal_typeform.type_argument(db), + actual_typeform.type_argument(db), variance, seen, ); @@ -3079,9 +3215,9 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { actual @ (Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_)), ) => { let variance = TypeVarVariance::Covariant.compose(polarity); - if let Some(actual_instance) = actual.to_instance_approximation(self.db) { + if let Some(actual_instance) = actual.to_instance_approximation(db, self.env) { return self.infer_map_impl( - formal_typeform.type_argument(self.db), + formal_typeform.type_argument(db), actual_instance, variance, seen, @@ -3090,11 +3226,11 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } (Type::TypeForm(formal_typeform), Type::KnownInstance(actual_instance)) - if let Some(actual_argument) = actual_instance.type_form_argument(self.db) => + if let Some(actual_argument) = actual_instance.type_form_argument(db, self.env) => { let variance = TypeVarVariance::Covariant.compose(polarity); return self.infer_map_impl( - formal_typeform.type_argument(self.db), + formal_typeform.type_argument(db), actual_argument, variance, seen, @@ -3103,9 +3239,9 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { (Type::TypeForm(formal_typeform), Type::SpecialForm(actual_form)) => { let variance = TypeVarVariance::Covariant.compose(polarity); - if let Some(actual_argument) = actual_form.type_form_argument(self.db) { + if let Some(actual_argument) = actual_form.type_form_argument(db, self.env) { return self.infer_map_impl( - formal_typeform.type_argument(self.db), + formal_typeform.type_argument(db), actual_argument, variance, seen, @@ -3141,22 +3277,18 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // to prevent incorrect specialization: e.g. `T = int | list[int]` for `formal: T | list[T], actual: int | list[int]` // (the correct specialization is `T = int`). let types_have_typevars = formal_union - .elements(self.db) + .elements(db) .iter() - .filter(|ty| ty.has_typevar(self.db)); + .filter(|ty| ty.has_typevar(db, self.env)); let Ok(Type::TypeVar(formal_bound_typevar)) = types_have_typevars.exactly_one() else { return Ok(()); }; - if actual_union - .elements(self.db) - .iter() - .any(|ty| ty.is_type_var()) - { + if actual_union.elements(db).iter().any(|ty| ty.is_type_var()) { return Ok(()); } let remaining_actual = - actual_union.filter(self.db, |ty| !ty.is_subtype_of(self.db, formal)); + actual_union.filter(db, |ty| !ty.is_subtype_of(db, self.env, formal)); if remaining_actual.is_never() { return Ok(()); } @@ -3176,7 +3308,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // `ClassSelector[T]` with `ClassSelector[CT | None]`, descending into `None` // would map `T` to `None` before `CT` is solved from another argument. if let Type::TypeVar(actual_typevar) = actual - && actual_typevar.is_inferable(self.db, self.inferable) + && actual_typevar.is_inferable(db, self.inferable) && matches!(polarity, TypeVarVariance::Invariant) { self.add_type_mapping(actual_typevar, formal, polarity); @@ -3194,10 +3326,10 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // // without specializing `T` to `None`. if !actual.is_never() { - let assignable_elements = union_formal.elements(self.db).iter().filter(|ty| { + let assignable_elements = union_formal.elements(db).iter().filter(|ty| { actual - .when_subtype_of(self.db, **ty, self.constraints, self.inferable) - .is_always_satisfied(self.db) + .when_subtype_of(db, self.env, **ty, self.constraints, self.inferable) + .is_always_satisfied(db, self.env) }); if assignable_elements.exactly_one().is_ok() { return Ok(()); @@ -3205,7 +3337,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } let mut bound_typevars = union_formal - .elements(self.db) + .elements(db) .iter() .filter_map(|ty| ty.as_typevar()); @@ -3225,7 +3357,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // ``` let mut first_error = None; let mut found_matching_element = false; - for formal_element in union_formal.elements(self.db) { + for formal_element in union_formal.elements(db) { let result = self.infer_map_impl(*formal_element, actual, polarity, seen); if let Err(err) = result { first_error.get_or_insert(err); @@ -3234,12 +3366,13 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // not assignable to the formal element. if !actual .when_assignable_to( - self.db, + db, + self.env, *formal_element, self.constraints, self.inferable, ) - .is_never_satisfied(self.db) + .is_never_satisfied(db, self.env) { found_matching_element = true; } @@ -3252,9 +3385,9 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } (Type::TypeVar(bound_typevar), ty) | (ty, Type::TypeVar(bound_typevar)) - if bound_typevar.is_inferable(self.db, self.inferable) => + if bound_typevar.is_inferable(db, self.inferable) => { - match bound_typevar.typevar(self.db).bound_or_constraints(self.db) { + match bound_typevar.typevar(db).bound_or_constraints(db, self.env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { if polarity.is_contravariant() { // In a contravariant position, the formal type variable is a subtype of @@ -3265,14 +3398,20 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // check here. self.add_type_mapping( bound_typevar, - IntersectionType::from_two_elements(self.db, bound, ty), + IntersectionType::from_two_elements(db, self.env, bound, ty), polarity, ); return Ok(()); } if !ty - .when_assignable_to(self.db, bound, self.constraints, self.inferable) - .is_always_satisfied(self.db) + .when_assignable_to( + db, + self.env, + bound, + self.constraints, + self.inferable, + ) + .is_always_satisfied(db, self.env) { return Err(SpecializationError::MismatchedBound { bound_typevar, @@ -3283,7 +3422,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { // Prefer an exact match first. - for constraint in typevar_constraints.elements(self.db) { + for constraint in typevar_constraints.elements(db) { if ty == *constraint { self.add_type_mapping(bound_typevar, ty, polarity); return Ok(()); @@ -3304,14 +3443,17 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // constraint. if let Type::TypeVar(actual_typevar) = ty && let Some(actual_constraints) = - actual_typevar.typevar(self.db).constraints(self.db) + actual_typevar.typevar(db).constraints(db, self.env) { let all_satisfied = actual_constraints.iter().all(|actual_constraint| { - typevar_constraints.elements(self.db).iter().any( + typevar_constraints.elements(db).iter().any( |formal_constraint| { - actual_constraint - .is_equivalent_to(self.db, *formal_constraint) + actual_constraint.is_equivalent_to( + db, + self.env, + *formal_constraint, + ) }, ) }); @@ -3321,24 +3463,26 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } } - for constraint in typevar_constraints.elements(self.db) { + for constraint in typevar_constraints.elements(db) { let is_satisfied = if polarity.is_contravariant() { constraint .when_assignable_to( - self.db, + db, + self.env, ty, self.constraints, self.inferable, ) - .is_always_satisfied(self.db) + .is_always_satisfied(db, self.env) } else { ty.when_assignable_to( - self.db, + db, + self.env, *constraint, self.constraints, self.inferable, ) - .is_always_satisfied(self.db) + .is_always_satisfied(db, self.env) }; if is_satisfied { @@ -3368,7 +3512,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // formal intersection, so we must infer type mappings for each of them. (The // 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_intersection.iter_positive(self.db) { + for positive in formal_intersection.iter_positive(db) { self.infer_map_impl(positive, actual, polarity, seen)?; } } @@ -3390,7 +3534,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // They don't all have to. let mut first_error = None; let mut found_matching_element = false; - for positive in actual_intersection.iter_positive(self.db) { + for positive in actual_intersection.iter_positive(db) { let result = self.infer_map_impl(formal, positive, polarity, seen); if let Err(err) = result { // TODO: `infer_map_impl` can have side effects even in the error case, so @@ -3402,8 +3546,14 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // The recursive call to `infer_map_impl` may succeed even if the actual // type is not assignable to the formal element. if !positive - .when_assignable_to(self.db, formal, self.constraints, self.inferable) - .is_never_satisfied(self.db) + .when_assignable_to( + db, + self.env, + formal, + self.constraints, + self.inferable, + ) + .is_never_satisfied(db, self.env) { found_matching_element = true; } @@ -3423,10 +3573,10 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { ) if let SubclassOfInner::Protocol(protocol) = formal_subclass.subclass_of() => { let formal_protocol = Type::ProtocolInstance(protocol); if let Type::Union(union) = actual { - for element in union.elements(self.db) { + for element in union.elements(db) { self.infer_map_impl( formal_protocol, - element.bindings(self.db).return_type(self.db), + element.bindings(db, self.env).return_type(db, self.env), polarity, seen, )?; @@ -3435,7 +3585,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { } return self.infer_map_impl( formal_protocol, - actual.bindings(self.db).return_type(self.db), + actual.bindings(db, self.env).return_type(db, self.env), polarity, seen, ); @@ -3443,7 +3593,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { (Type::SubclassOf(subclass_of), ty) | (ty, Type::SubclassOf(subclass_of)) if let Some(type_var) = subclass_of.into_type_var() - && let Some(actual_instance) = ty.to_instance_approximation(self.db) => + && let Some(actual_instance) = ty.to_instance_approximation(db, self.env) => { return self.infer_map_impl( Type::TypeVar(type_var), @@ -3459,7 +3609,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { ) => { // Retry specialization with the literal's fallback instance so literals can // contribute to generic inference for nominal and protocol formals. - let actual_instance = literal.fallback_instance(self.db); + let actual_instance = literal.fallback_instance(db, self.env); return self.infer_map_impl(formal, actual_instance, polarity, seen); } @@ -3471,7 +3621,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // ordinary `range` instance when inferring through generic nominal/protocol types. return self.infer_map_impl( formal, - known_instance.instance_fallback(self.db), + known_instance.instance_fallback(db, self.env), polarity, seen, ); @@ -3479,27 +3629,20 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { (formal, Type::ProtocolInstance(actual_protocol)) => { if let Type::ProtocolInstance(formal_protocol) = formal - && let Some(actual_origin) = actual_protocol.materialized_origin(self.db) - && let Some(formal_origin) = formal_protocol.class_origin(self.db) + && let Some(actual_origin) = actual_protocol.materialized_origin(db) + && let Some(formal_origin) = formal_protocol.class_origin(db) { let nominally_inherited = actual_origin - .iter_mro(self.db) + .iter_mro(db) .filter_map(ClassBase::into_class) - .any(|base| { - base.class_literal(self.db) == formal_origin.class_literal(self.db) - }); + .any(|base| base.class_literal(db) == formal_origin.class_literal(db)); let when = if nominally_inherited - || formal_protocol - .interface(self.db) - .has_only_finite_members(self.db) + || formal_protocol.interface(db).has_only_finite_members(db) { - Some(actual.when_constraint_set_assignable_to_owned(self.db, formal)) + Some(actual.when_constraint_set_assignable_to_owned(db, self.env, formal)) } else { actual_protocol - .when_non_recursive_members_assignable_to_owned( - self.db, - formal_protocol, - ) + .when_non_recursive_members_assignable_to_owned(db, formal_protocol) .map(Cow::Borrowed) }; @@ -3508,7 +3651,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // interface when doing so is cycle-safe; otherwise use its nonrecursive // requirements and leave full recursive compatibility to argument checking. if let Some(when) = when { - let when = self.constraints.load(self.db, &when); + let when = self.constraints.load(db, self.env, &when); self.infer_from_constraint_set(when)?; return Ok(()); } @@ -3519,7 +3662,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // To handle classes that implicitly implement a generic protocol, we // will need to check the types of the protocol members to be able to // infer the specialization of the protocol that the class implements. - if let Some(actual_nominal) = actual_protocol.nominal_origin_instance(self.db) { + if let Some(actual_nominal) = actual_protocol.nominal_origin_instance(db) { return self.infer_map_impl( formal, Type::NominalInstance(actual_nominal), @@ -3531,8 +3674,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // Special case: `formal` and `actual` are both tuples. (Type::NominalInstance(formal), Type::NominalInstance(actual)) - if let Some(formal_tuple) = formal.tuple_spec(self.db) - && let Some(actual_tuple) = actual.tuple_spec(self.db) => + if let Some(formal_tuple) = formal.tuple_spec(db, self.env) + && let Some(actual_tuple) = actual.tuple_spec(db, self.env) => { if let TupleSpec::Variable(formal_variable) = &*formal_tuple && let VariableSegment::TypeVarTuple(typevartuple) = formal_variable.variable() @@ -3553,7 +3696,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { ( &elements[..formal_prefix_len], Type::heterogeneous_tuple( - self.db, + db, + self.env, elements[formal_prefix_len..middle_end].iter().copied(), ), &elements[middle_end..], @@ -3572,7 +3716,8 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { ( &actual_prefix_elements[..formal_prefix_len], Type::tuple(TupleType::mixed_with_segment( - self.db, + db, + self.env, actual_prefix_elements[formal_prefix_len..].iter().copied(), actual.variable(), actual_suffix_elements[..suffix_start].iter().copied(), @@ -3600,15 +3745,17 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { else { return Ok(()); }; - let Ok(formal_tuple) = formal_tuple.resize(self.db, most_precise_length) else { + let Ok(formal_tuple) = formal_tuple.resize(db, self.env, most_precise_length) + else { return Ok(()); }; - let Ok(actual_tuple) = actual_tuple.resize(self.db, most_precise_length) else { + let Ok(actual_tuple) = actual_tuple.resize(db, self.env, most_precise_length) + else { return Ok(()); }; for (formal_element, actual_element) in formal_tuple - .iter_element_types(self.db) - .zip(actual_tuple.iter_element_types(self.db)) + .iter_element_types(db) + .zip(actual_tuple.iter_element_types(db)) { let variance = TypeVarVariance::Covariant.compose(polarity); self.infer_map_impl(formal_element, actual_element, variance, seen)?; @@ -3623,7 +3770,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // Extract formal_alias if this is a generic class let formal_alias = match formal { Type::NominalInstance(formal_nominal) => { - formal_nominal.class(self.db).into_generic_alias() + formal_nominal.class(db, self.env).into_generic_alias() } Type::ProtocolInstance(_) => { @@ -3631,8 +3778,9 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // will handle implicitly implemented protocols and generic protocols. We // eventually want this logic to be used for _all_ nominal instances // (replacing the logic below). - let when = actual.when_constraint_set_assignable_to_owned(self.db, formal); - let when = self.constraints.load(self.db, &when); + let when = + actual.when_constraint_set_assignable_to_owned(db, self.env, formal); + let when = self.constraints.load(db, self.env, &when); self.infer_from_constraint_set(when)?; return Ok(()); } @@ -3641,27 +3789,26 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { }; if let Some(formal_alias) = formal_alias { - let formal_origin = formal_alias.origin(self.db); - for base in actual_nominal.class(self.db).iter_mro(self.db) { + let formal_origin = formal_alias.origin(db); + for base in actual_nominal.class(db, self.env).iter_mro(db) { let ClassBase::Class(ClassType::Generic(base_alias)) = base else { continue; }; - if formal_origin != base_alias.origin(self.db) { + if formal_origin != base_alias.origin(db) { continue; } let generic_context = formal_alias - .specialization(self.db) - .generic_context(self.db) - .variables(self.db); - let formal_specialization = - formal_alias.specialization(self.db).types(self.db); - let base_specialization = base_alias.specialization(self.db).types(self.db); + .specialization(db) + .generic_context(db) + .variables(db); + let formal_specialization = formal_alias.specialization(db).types(db); + let base_specialization = base_alias.specialization(db).types(db); for (typevar, formal_ty, base_ty) in itertools::izip!( generic_context, formal_specialization, base_specialization ) { - let variance = typevar.variance_with_polarity(self.db, polarity); + let variance = typevar.variance_with_polarity(db, polarity); self.infer_map_impl(*formal_ty, *base_ty, variance, seen)?; } return Ok(()); @@ -3676,15 +3823,20 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { let when = self .common_typed_dict_protocol_constraints(formal, actual_union) .unwrap_or_else(|| { - actual.when_constraint_set_assignable_to(self.db, formal, self.constraints) + actual.when_constraint_set_assignable_to( + db, + self.env, + formal, + self.constraints, + ) }); self.infer_from_constraint_set(when)?; return Ok(()); } (formal @ Type::ProtocolInstance(_), actual @ Type::TypedDict(_)) => { - let when = actual.when_constraint_set_assignable_to_owned(self.db, formal); - let when = self.constraints.load(self.db, &when); + let when = actual.when_constraint_set_assignable_to_owned(db, self.env, formal); + let when = self.constraints.load(db, self.env, &when); self.infer_from_constraint_set(when)?; return Ok(()); } @@ -3693,26 +3845,26 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // from matching the actual type's callable signature against the protocol's `__call__` // method signature. (Type::ProtocolInstance(formal_protocol), _) => { - let Some(call_method) = formal_protocol.interface(self.db).call_method(self.db) + let Some(call_method) = formal_protocol.interface(db).call_method(db, self.env) else { return Ok(()); }; - let Some(actual_callables) = actual.try_upcast_to_callable(self.db) else { + let Some(actual_callables) = actual.try_upcast_to_callable(db, self.env) else { return Ok(()); }; // The protocol interface exposes the callable signature already bound for // instance access. - let formal_signature = call_method.signatures(self.db); + let formal_signature = call_method.signatures(db); self.infer_from_callable_signature(formal_signature, &actual_callables)?; } (Type::Callable(formal_callable), _) => { - let Some(actual_callables) = actual.try_upcast_to_callable(self.db) else { + let Some(actual_callables) = actual.try_upcast_to_callable(db, self.env) else { return Ok(()); }; - let formal_signature = formal_callable.signatures(self.db); + let formal_signature = formal_callable.signatures(db); self.infer_from_callable_signature(formal_signature, &actual_callables)?; } @@ -3723,7 +3875,7 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { // when it can be matched directly against a type variable in the formal type, // e.g., `reveal_type(alias)` should reveal the type alias, not its value type. (formal, Type::TypeAlias(alias)) => { - return self.infer_map_impl(formal, alias.value_type(self.db), polarity, seen); + return self.infer_map_impl(formal, alias.value_type(db), polarity, seen); } // TODO: Add more forms that we can structurally induct into: type[C], callables @@ -3773,18 +3925,28 @@ mod tests { #[test] fn generic_context_inferable_typevars_retain_instances_from_bounds() { let db = setup_db(); - let u = - BoundTypeVarInstance::synthetic(&db, Name::new_static("U"), TypeVarVariance::Invariant); - let t = - BoundTypeVarInstance::synthetic(&db, Name::new_static("T"), TypeVarVariance::Invariant) - .map_bound_or_constraints(&db, |_| { - Some(TypeVarBoundOrConstraints::UpperBound(Type::TypeVar(u))) - }); - let context = GenericContext::from_typevar_instances(&db, [t]); + let db = &db; + let env = db.program_environment(); + let u = BoundTypeVarInstance::synthetic( + db, + &env, + Name::new_static("U"), + TypeVarVariance::Invariant, + ); + let t = BoundTypeVarInstance::synthetic( + db, + &env, + Name::new_static("T"), + TypeVarVariance::Invariant, + ) + .map_bound_or_constraints(db, |_| { + Some(TypeVarBoundOrConstraints::UpperBound(Type::TypeVar(u))) + }); + let context = GenericContext::from_typevar_instances(db, &env, [t]); - let inferable = context.inferable_typevars(&db); - assert_eq!(inferable.iter(&db).collect::>(), [t, u]); - assert!(t.is_inferable(&db, inferable)); - assert!(u.is_inferable(&db, inferable)); + let inferable = context.inferable_typevars(db); + assert_eq!(inferable.iter(db).collect::>(), [t, u]); + assert!(t.is_inferable(db, inferable)); + assert!(u.is_inferable(db, inferable)); } } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 5aec4f1a30..16d9e0d801 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -13,9 +13,10 @@ use crate::types::{ KnownUnion, PropertyAccessorRole, SubclassOfInner, Type, TypeContext, TypeVarBoundOrConstraints, binding_type, }; -use crate::{Db, DisplaySettings, HasDefinition, HasType, SemanticModel}; +use crate::{Db, DisplaySettings, HasDefinition, HasType, ProgramEnvironment, SemanticModel}; use itertools::Either; -use ruff_db::files::{File, FileRange}; +use ruff_db::PythonFile; +use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_python_ast::{self as ast, AnyNodeRef, name::Name}; @@ -63,7 +64,7 @@ pub fn definitions_for_name<'db>( alias_resolution: ImportAliasResolution, ) -> Vec> { let db = model.db(); - let file = model.file(); + let file = model.python_file(); let index = semantic_index(db, file); // Get the scope for this name expression @@ -169,8 +170,9 @@ pub fn definitions_for_name<'db>( } // If we didn't find any definitions in scopes, fallback to builtins + let env = model.program_environment(); if resolved_definitions.is_empty() - && let Some(builtins_scope) = builtins_module_scope(db) + && let Some(builtins_scope) = builtins_module_scope(db, &env) { // Special cases for `float` and `complex` in type annotation positions. // We don't know whether we're in a type annotation position, so we'll just ask `Name`'s type, @@ -195,7 +197,7 @@ pub fn definitions_for_name<'db>( .rev() .filter_map(|ty| ty.as_nominal_instance()) .filter_map(|instance| { - let definition = instance.class_literal(db).definition(db)?; + let definition = instance.class_literal(db, &env).definition(db)?; Some(ResolvedDefinition::Definition(definition)) }) .collect(); @@ -235,25 +237,27 @@ pub fn definitions_for_attribute<'db>( let db = model.db(); let name_str = attribute.attr.as_str(); + let mut resolved = Vec::new(); + + // Determine the type of the LHS + let Some(lhs_ty) = attribute.value.inferred_type(model) else { + return resolved; + }; + + let env = model.program_environment(); + // A structural protocol meta-type still uses its nominal protocol declaration as the source // location for go-to-definition, even though the origin is not a nominal upper bound. let subclass_origin = |subclass_of: SubclassOfInner<'db>| { let class = match subclass_of { SubclassOfInner::Protocol(protocol) => protocol.class_origin(db).map(|origin| *origin), - subclass_of => subclass_of.into_class(db), + subclass_of => subclass_of.into_class(db, &env), }?; class .static_class_literal(db) .map(|(literal, _)| ClassLiteral::Static(literal)) }; - let mut resolved = Vec::new(); - - // Determine the type of the LHS - let Some(lhs_ty) = attribute.value.inferred_type(model) else { - return resolved; - }; - let tys = match lhs_ty { Type::Union(union) => union.elements(model.db()), _ => std::slice::from_ref(&lhs_ty), @@ -271,7 +275,7 @@ pub fn definitions_for_attribute<'db>( for ty in expanded_tys { // Handle modules if let Type::ModuleLiteral(module_literal) = ty { - if let Some(module_file) = module_literal.module(db).file(db) { + if let Some(module_file) = module_literal.module(db).python_file(db) { let module_scope = global_scope(db, module_file); for def in find_symbol_in_scope(db, module_scope, name_str) { resolved.extend(resolve_definition( @@ -290,7 +294,7 @@ pub fn definitions_for_attribute<'db>( continue; } - let meta_type = ty.to_meta_type(db); + let meta_type = ty.to_meta_type(db, &env); // Look up the attribute first on the meta-type, unless it's already a class-like type. let lookup_type = match ty { @@ -433,7 +437,7 @@ impl<'db> ImplementationsFinder<'db> { pub fn implementations_for_file<'scan>( &'scan self, db: &'scan dyn Db, - file: File, + file: PythonFile<'scan>, ) -> Vec> where 'db: 'scan, @@ -477,9 +481,10 @@ impl<'db> ImplementationsFinder<'db> { ) -> Option { let db = model.db(); let lhs_ty = attribute.value.inferred_type(model)?; + let env = model.program_environment(); let mut roots = Vec::new(); let mut seen = FxHashSet::default(); - collect_implementation_root_classes(db, lhs_ty, &mut seen, &mut roots); + collect_implementation_root_classes(db, &env, lhs_ty, &mut seen, &mut roots); let accessor_role = match attribute.ctx { ast::ExprContext::Load => Some(PropertyAccessorRole::Getter), @@ -507,6 +512,7 @@ impl<'db> ImplementationsFinder<'db> { /// parent classes: on `Dog.speak`, the root is `Dog`, so `Animal.speak` is not included. pub fn for_method(model: &SemanticModel<'db>, function: &ast::StmtFunctionDef) -> Option { let db = model.db(); + let env = model.program_environment(); let function_definition = function.definition(model); if !is_reachable_implementation_definition(db, function_definition) { return None; @@ -518,10 +524,10 @@ impl<'db> ImplementationsFinder<'db> { .and_then(Type::as_property_instance) .and_then(|property| property.accessor_role(db, function_definition)); let class_node = containing_scope.node(db).as_class()?; - let class_definition = - semantic_index(db, containing_scope.file(db)).expect_single_definition(class_node); + let class_definition = semantic_index(db, containing_scope.python_file(db)) + .expect_single_definition(class_node); let class_ty = binding_type(db, class_definition); - let root = extract_class_literal(db, class_ty)?; + let root = extract_class_literal(db, &env, class_ty)?; ImplementationsFinder::for_member_roots( db, @@ -547,11 +553,12 @@ impl<'db> ImplementationsFinder<'db> { /// returns that class and its own subclasses, not its parents. pub fn for_class(model: &SemanticModel<'db>, class: &ast::StmtClassDef) -> Option { let db = model.db(); + let env = model.program_environment(); let class_definition = class.definition(model); if !is_reachable_implementation_definition(db, class_definition) { return None; } - let root = extract_class_literal(db, binding_type(db, class_definition))?; + let root = extract_class_literal(db, &env, binding_type(db, class_definition))?; Some(ImplementationsFinder::for_class_roots(db, vec![root])) } @@ -579,6 +586,7 @@ impl<'db> ImplementationsFinder<'db> { /// handling. pub fn for_class_reference( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, resolved_definitions: &[ResolvedDefinition<'db>], ) -> Option { let mut roots = Vec::new(); @@ -606,7 +614,7 @@ impl<'db> ImplementationsFinder<'db> { let root = match ty { Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::GenericAlias(_) => { - extract_class_literal(db, ty) + extract_class_literal(db, env, ty) } _ => None, }; @@ -629,10 +637,10 @@ impl<'db> ImplementationsFinder<'db> { /// Finds subclasses of `roots` defined in `file`. fn class_implementations_for_file<'db>( db: &'db dyn Db, - file: File, + file: PythonFile<'db>, roots: &FxHashSet>, ) -> Vec> { - if !contains_identifier(&source_text(db, file), "class") { + if !contains_identifier(&source_text(db, file.file(db)), "class") { return Vec::new(); } @@ -659,9 +667,10 @@ pub fn static_member_type_for_attribute<'db>( model: &SemanticModel<'db>, attribute: &ast::ExprAttribute, ) -> Option> { + let db = model.db(); let lhs_ty = attribute.value.inferred_type(model)?; lhs_ty - .static_member(model.db(), attribute.attr.as_str()) + .static_member(db, &model.program_environment(), attribute.attr.as_str()) .ignore_possibly_undefined() } @@ -702,8 +711,7 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( } // Look for instance attributes in method scopes (e.g., self.x = 1) - let file = class_scope.file(db); - let index = semantic_index(db, file); + let index = semantic_index(db, class_scope.python_file(db)); for function_scope_id in attribute_scopes(db, class_scope) { if let Some(place_id) = index @@ -737,7 +745,7 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( /// Finds member implementations contributed by subclasses of `roots` defined in `file`. fn member_implementations_for_file<'db>( db: &'db dyn Db, - file: File, + file: PythonFile<'db>, roots: &FxHashSet>, member_name: &str, accessor_role: Option, @@ -746,7 +754,7 @@ fn member_implementations_for_file<'db>( // A file can only contribute an override if it contains a class and spells the member name, // whether as a method name, a class-body target, or a `self.member` assignment. - let source = source_text(db, file); + let source = source_text(db, file.file(db)); if !contains_identifier(&source, "class") || !contains_identifier(&source, member_name) { return definitions; } @@ -869,7 +877,7 @@ fn own_member_definitions<'db>( } } - let file = class_scope.file(db); + let file = class_scope.python_file(db); let index = semantic_index(db, file); let mut instance_definitions = Vec::new(); for function_scope_id in attribute_scopes(db, class_scope) { @@ -905,9 +913,9 @@ fn own_member_definitions<'db>( } /// Returns whether `definition` is either not a property accessor or has the requested role. -fn property_accessor_role_matches( - db: &dyn Db, - definition: Definition<'_>, +fn property_accessor_role_matches<'db>( + db: &'db dyn Db, + definition: Definition<'db>, requested_role: Option, ) -> bool { if !matches!(definition.kind(db), DefinitionKind::Function(_)) { @@ -964,6 +972,7 @@ fn member_implementation_definition<'db>( /// Normalizes a receiver type into the class roots used for implementation lookup. fn collect_implementation_root_classes<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, seen: &mut FxHashSet>, roots: &mut Vec>, @@ -972,46 +981,58 @@ fn collect_implementation_root_classes<'db>( Type::Union(union) => { // `pet: Dog | Cat` can dispatch through either `Dog` or `Cat`. for element in union.elements(db) { - collect_implementation_root_classes(db, *element, seen, roots); + collect_implementation_root_classes(db, env, *element, seen, roots); } } Type::Intersection(intersection) => { // Finite intersections can stand for alternatives like `Dog` or `Cat`. - if let Some(alternatives) = intersection.finite_alternatives(db) { + if let Some(alternatives) = intersection.finite_alternatives(db, env) { for alternative in alternatives { - collect_implementation_root_classes(db, alternative, seen, roots); + collect_implementation_root_classes(db, env, alternative, seen, roots); } } } - Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db) { + Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db, env) { // `T: Animal` can dispatch through the `Animal` bound. Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - collect_implementation_root_classes(db, bound, seen, roots); + collect_implementation_root_classes(db, env, bound, seen, roots); } // `T: (Dog, Cat)` can dispatch through either constraint. Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - collect_implementation_root_classes(db, constraints.as_type(db), seen, roots); + collect_implementation_root_classes( + db, + env, + constraints.as_type(db, env), + seen, + roots, + ); } None => {} }, Type::SubclassOf(subclass_of) if subclass_of.is_type_var() => { // Both `type[T]` and the implicit `cls` parameter of a classmethod are represented as // `SubclassOf(TypeVar)`. Normalize them through the existing TypeVar handling above. - collect_implementation_root_classes(db, subclass_of.to_instance(db), seen, roots); + collect_implementation_root_classes( + db, + env, + subclass_of.to_instance(db, env), + seen, + roots, + ); } ty => { // `dog: Dog` maps directly to the `Dog` class root. let root = match ty { Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { - extract_class_literal(db, ty) + extract_class_literal(db, env, ty) } Type::NominalInstance(_) | Type::ProtocolInstance(_) | Type::KnownInstance(_) | Type::LiteralValue(_) | Type::TypedDict(_) - | Type::NewTypeInstance(_) => extract_class_literal(db, ty) - .or_else(|| extract_class_literal(db, ty.to_meta_type(db))), + | Type::NewTypeInstance(_) => extract_class_literal(db, env, ty) + .or_else(|| extract_class_literal(db, env, ty.to_meta_type(db, env))), _ => None, }; @@ -1052,7 +1073,7 @@ fn user_visible_definitions<'db>( match definition.kind(db) { DefinitionKind::NestedBindings(nested) => { - let index = semantic_index(db, definition.file(db)); + let index = semantic_index(db, definition.python_file(db)); let sources = nested .visible_binding_sources(index, definition.file_scope(db)) .flatten() @@ -1085,8 +1106,11 @@ fn reachable_implementation_definitions<'db>( .collect() } -fn is_reachable_implementation_definition(db: &dyn Db, definition: Definition<'_>) -> bool { - let file = definition.file(db); +fn is_reachable_implementation_definition<'db>( + db: &'db dyn Db, + definition: Definition<'db>, +) -> bool { + let file = definition.python_file(db); let parsed = parsed_module(db, file).load(db); is_range_reachable( db, @@ -1167,13 +1191,16 @@ pub fn typed_dict_key_hover<'db>( model: &SemanticModel<'db>, subscript: &ast::ExprSubscript, ) -> Option> { + let db = model.db(); let key = subscript .slice .as_string_literal_expr() .map(|literal| literal.value.to_str())?; let value_ty = subscript.value.inferred_type(model)?; let typed_dict = value_ty.as_typed_dict()?; - let owner = value_ty.display(model.db()).to_string(); + let owner = value_ty + .display(db, &model.program_environment()) + .to_string(); let field = typed_dict.items(model.db()).get(key)?; let docstring = field .first_declaration() @@ -1205,9 +1232,10 @@ pub fn definitions_for_keyword_argument<'db>( let keyword_name_str = keyword_name.as_str(); let mut resolved_definitions = Vec::new(); + let env = &model.program_environment(); if let Some(callable_type) = func_type - .try_upcast_to_callable(db) + .try_upcast_to_callable(db, env) .and_then(CallableTypes::exactly_one) { let signatures = callable_type.signatures(db); @@ -1241,7 +1269,7 @@ pub fn definitions_for_imported_symbol<'db>( let mut visited = FxHashSet::default(); resolve_definition::resolve_from_import_definitions( model.db(), - model.file(), + model.python_file(), import_node, symbol_name, &mut visited, @@ -1257,13 +1285,14 @@ pub fn definitions_and_overloads_for_function<'db>( model: &SemanticModel<'db>, function: &ast::StmtFunctionDef, ) -> Vec> { + let db = model.db(); if let Some(function_type) = function .inferred_type(model) .and_then(Type::as_function_literal) { function_type - .iter_overloads_and_implementation(model.db()) - .filter_map(|overload| overload.signature(model.db()).definition()) + .iter_overloads_and_implementation(db) + .filter_map(|overload| overload.signature(db).definition()) .map(ResolvedDefinition::Definition) .collect() } else { @@ -1319,11 +1348,15 @@ pub struct CallSignatureParameter<'db> { } impl<'db> CallSignatureDetails<'db> { - fn from_binding(db: &'db dyn Db, binding: &crate::types::call::Binding<'db>) -> Self { + fn from_binding( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + binding: &crate::types::call::Binding<'db>, + ) -> Self { let argument_to_parameter_mapping = binding.argument_matches().to_vec(); let specialization = binding.specialization(db); let signature = binding.signature.clone(); - let display_details = signature.display(db).to_string_parts(); + let display_details = signature.display(db, env).to_string_parts(); let (parameters, parameter_to_displayed_parameter_mapping) = displayed_parameters_for_signature(db, &signature, &display_details, specialization); let argument_to_displayed_parameter_mapping = argument_to_parameter_mapping @@ -1456,16 +1489,16 @@ pub fn call_signature_details<'db>( model: &SemanticModel<'db>, call_expr: &ast::ExprCall, ) -> Vec> { + let db = model.db(); let Some(func_type) = call_expr.func.inferred_type(model) else { return Vec::new(); }; - let db = model.db(); - // Use into_callable to handle all the complex type conversions + let env = &model.program_environment(); if let Some(callable_type) = func_type - .try_upcast_to_callable(db) - .map(|callables| callables.into_type(db)) + .try_upcast_to_callable(db, env) + .map(|callables| callables.into_type(db, env)) { // Use from_arguments_typed so that check_types can infer TypeVar // specializations from the actual argument types at this call site. @@ -1475,9 +1508,10 @@ pub fn call_signature_details<'db>( .inferred_type(model) .unwrap_or(Type::unknown()) }); - let mut bindings = callable_type - .bindings(db) - .match_parameters(db, &call_arguments); + let mut bindings = + callable_type + .bindings(db, env) + .match_parameters(db, env, &call_arguments); // Run type checking to resolve TypeVar bindings from argument types. // For example, calling `dict[str, int].get("a")` resolves the `_KT` @@ -1486,6 +1520,7 @@ pub fn call_signature_details<'db>( let constraints = ConstraintSetBuilder::new(); let _ = bindings.check_types_impl( db, + env, &constraints, &call_arguments, TypeContext::default(), @@ -1497,7 +1532,7 @@ pub fn call_signature_details<'db>( bindings .iter_flat() .flatten() - .map(|binding| CallSignatureDetails::from_binding(db, binding)) + .map(|binding| CallSignatureDetails::from_binding(db, env, binding)) .collect() } else { // Type is not callable, return empty signatures @@ -1513,7 +1548,8 @@ fn resolve_single_overload<'db>( call_expr: &ast::ExprCall, ) -> Option> { let db = model.db(); - let bindings = callable_type.bindings(db); + let env = &model.program_environment(); + let bindings = callable_type.bindings(db, env); let args = CallArguments::from_arguments_typed(&call_expr.arguments, |splatted_value| { splatted_value @@ -1523,8 +1559,8 @@ fn resolve_single_overload<'db>( let constraints = ConstraintSetBuilder::new(); let mut resolved: Vec<_> = bindings - .match_parameters(db, &args) - .check_types(db, &constraints, &args, TypeContext::default(), &[]) + .match_parameters(db, env, &args) + .check_types(db, env, &constraints, &args, TypeContext::default(), &[]) .iter() .flat_map(super::call::bind::Bindings::iter_flat) .flat_map(|binding| { @@ -1560,6 +1596,7 @@ fn full_type_bindings_for_call<'db>( call_expr: &ast::ExprCall, ) -> crate::types::call::Bindings<'db> { let db = model.db(); + let env = &model.program_environment(); let call_arguments = CallArguments::from_arguments_typed(&call_expr.arguments, |splatted_value| { splatted_value @@ -1569,10 +1606,11 @@ fn full_type_bindings_for_call<'db>( let constraints = ConstraintSetBuilder::new(); func_type - .bindings(db) - .match_parameters(db, &call_arguments) + .bindings(db, env) + .match_parameters(db, env, &call_arguments) .check_types( db, + env, &constraints, &call_arguments, TypeContext::default(), @@ -1638,7 +1676,7 @@ pub fn call_argument_forms( // Ordinary callables have only value-form arguments for IDE purposes, so skip full binding. if !func_type - .bindings(db) + .bindings(db, &model.program_environment()) .iter_flat() .any(|binding| known_type_form_parameter_index(db, binding.callable_type).is_some()) { @@ -1710,10 +1748,13 @@ pub fn call_type_simplified_by_overloads( let db = model.db(); let func_type = call_expr.func.inferred_type(model)?; - let callable_type = func_type.try_upcast_to_callable(db)?.into_type(db); + let env = &model.program_environment(); + let callable_type = func_type + .try_upcast_to_callable(db, env)? + .into_type(db, env); // If the callable is trivial this analysis is useless, bail out - if let Some(binding) = callable_type.bindings(db).single_element() + if let Some(binding) = callable_type.bindings(db, env).single_element() && binding.overloads().len() < 2 { return None; @@ -1722,7 +1763,7 @@ pub fn call_type_simplified_by_overloads( let signature = resolve_single_overload(model, callable_type, call_expr)?; Some( signature - .display_with(db, DisplaySettings::default().multiline()) + .display_with(db, env, DisplaySettings::default().multiline()) .to_string(), ) } @@ -1732,14 +1773,15 @@ pub fn definitions_for_bin_op<'db>( model: &SemanticModel<'db>, binary_op: &ast::ExprBinOp, ) -> Option<(Vec>, Type<'db>)> { + let db = model.db(); let left_ty = binary_op.left.inferred_type(model)?; let right_ty = binary_op.right.inferred_type(model)?; - - let Ok(bindings) = Type::try_call_bin_op(model.db(), left_ty, binary_op.op, right_ty) else { + let env = &model.program_environment(); + let Ok(bindings) = Type::try_call_bin_op(db, env, left_ty, binary_op.op, right_ty) else { return None; }; - let callable_type = promote_for_self(model.db(), bindings.callable_type()); + let callable_type = promote_for_self(db, env, bindings.callable_type()); let definitions: Vec<_> = bindings .iter_flat() @@ -1759,6 +1801,7 @@ pub fn definitions_for_unary_op<'db>( model: &SemanticModel<'db>, unary_op: &ast::ExprUnaryOp, ) -> Option<(Vec>, Type<'db>)> { + let db = model.db(); let operand_ty = unary_op.operand.inferred_type(model)?; let unary_dunder_method = match unary_op.op { @@ -1768,8 +1811,10 @@ pub fn definitions_for_unary_op<'db>( ast::UnaryOp::Not => "__bool__", }; + let env = &model.program_environment(); let bindings = match operand_ty.try_call_dunder( - model.db(), + db, + env, unary_dunder_method, CallArguments::none(), TypeContext::default(), @@ -1778,7 +1823,8 @@ pub fn definitions_for_unary_op<'db>( Err(CallDunderError::MethodNotAvailable) if unary_op.op == ast::UnaryOp::Not => { // The runtime falls back to `__len__` for `not` if `__bool__` is not defined. match operand_ty.try_call_dunder( - model.db(), + db, + env, "__len__", CallArguments::none(), TypeContext::default(), @@ -1798,7 +1844,7 @@ pub fn definitions_for_unary_op<'db>( ) => *bindings, }; - let callable_type = promote_for_self(model.db(), bindings.callable_type()); + let callable_type = promote_for_self(db, env, bindings.callable_type()); let definitions = bindings .iter_flat() @@ -1816,14 +1862,22 @@ pub fn definitions_for_unary_op<'db>( /// Promotes types in `self` positions. /// /// This is so that we show e.g. `int.__add__` instead of `Literal[4].__add__`. -fn promote_for_self<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { +fn promote_for_self<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Type<'db> { match ty { Type::BoundMethod(method) => Type::BoundMethod(method.map_self_type(db, |self_ty| { - self_ty.literal_fallback_instance(db).unwrap_or(self_ty) + self_ty + .literal_fallback_instance(db, env) + .unwrap_or(self_ty) })), - Type::Union(elements) => elements.map(db, |ty| match ty { + Type::Union(elements) => elements.map(db, env, |ty| match ty { Type::BoundMethod(method) => Type::BoundMethod(method.map_self_type(db, |self_ty| { - self_ty.literal_fallback_instance(db).unwrap_or(self_ty) + self_ty + .literal_fallback_instance(db, env) + .unwrap_or(self_ty) })), _ => *ty, }), @@ -1882,7 +1936,10 @@ pub fn resolved_call_signature<'db>( ) -> Option> { let db = model.db(); let func_type = call_expr.func.inferred_type(model)?; - let callable_type = func_type.try_upcast_to_callable(db)?.into_type(db); + let env = &model.program_environment(); + let callable_type = func_type + .try_upcast_to_callable(db, env)? + .into_type(db, env); let args = CallArguments::from_arguments_typed(&call_expr.arguments, |splatted_value| { splatted_value @@ -1893,16 +1950,16 @@ pub fn resolved_call_signature<'db>( // Extract the `Bindings` regardless of whether type checking succeeded or failed. let constraints = ConstraintSetBuilder::new(); let bindings = callable_type - .bindings(db) - .match_parameters(db, &args) - .check_types(db, &constraints, &args, TypeContext::default(), &[]) + .bindings(db, env) + .match_parameters(db, env, &args) + .check_types(db, env, &constraints, &args, TypeContext::default(), &[]) .unwrap_or_else(|CallError(_, bindings)| *bindings); // First, try to find the matching overload after full type checking. let type_checked_details: Vec<_> = bindings .iter_flat() .flat_map(|binding| binding.matching_overloads().map(|(_, overload)| overload)) - .map(|binding| CallSignatureDetails::from_binding(db, binding)) + .map(|binding| CallSignatureDetails::from_binding(db, env, binding)) .collect(); if !type_checked_details.is_empty() { @@ -1916,7 +1973,7 @@ pub fn resolved_call_signature<'db>( let all_details: Vec<_> = bindings .iter_flat() .flatten() - .map(|binding| CallSignatureDetails::from_binding(db, binding)) + .map(|binding| CallSignatureDetails::from_binding(db, env, binding)) .collect(); if all_details.is_empty() { @@ -1968,8 +2025,7 @@ pub fn inlay_hint_call_argument_details<'db>( }; let parameter_label_offset = param.definition().map(|definition| { - let param_file = definition.file(db); - let module = parsed_module(db, param_file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); definition.focus_range(db, &module) }); @@ -2000,7 +2056,8 @@ mod resolve_definition { } use indexmap::IndexSet; - use ruff_db::files::{File, FileRange, vendored_path_to_file}; + use ruff_db::PythonFile; + use ruff_db::files::{FileRange, vendored_path_to_file}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_db::system::SystemPath; use ruff_db::vendored::VendoredPathBuf; @@ -2028,7 +2085,7 @@ mod resolve_definition { /// The import resolved to a specific definition within a module Definition(Definition<'db>), /// The import resolved to an entire module - Module(File), + Module(PythonFile<'db>), /// The import resolved to a file with a specific range FileWithRange(FileRange), } @@ -2037,11 +2094,13 @@ mod resolve_definition { pub fn focus_range(&self, db: &dyn Db) -> FileRange { match self { ResolvedDefinition::Definition(definition) => { - let parsed = parsed_module(db, definition.file(db)).load(db); + let parsed = parsed_module(db, definition.python_file(db)).load(db); definition.focus_range(db, &parsed) } // For modules, navigate to the start of the file - ResolvedDefinition::Module(module) => FileRange::new(*module, TextRange::default()), + ResolvedDefinition::Module(module) => { + FileRange::new(module.file(db), TextRange::default()) + } ResolvedDefinition::FileWithRange(file_range) => *file_range, } } @@ -2050,7 +2109,7 @@ mod resolve_definition { match self { ResolvedDefinition::Definition(definition) => { let file = definition.file(db); - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, definition.python_file(db)).load(db); definition.kind(db).category(file.is_stub(db), &parsed) } ResolvedDefinition::Module(_) | ResolvedDefinition::FileWithRange(_) => { @@ -2067,11 +2126,11 @@ mod resolve_definition { } } - fn file(&self, db: &'db dyn Db) -> File { - match self { - ResolvedDefinition::Definition(definition) => definition.file(db), - ResolvedDefinition::Module(file) => *file, - ResolvedDefinition::FileWithRange(file_range) => file_range.file(), + fn python_file(&self, db: &'db dyn Db) -> Option> { + match *self { + ResolvedDefinition::Definition(definition) => Some(definition.python_file(db)), + ResolvedDefinition::Module(file) => Some(file), + ResolvedDefinition::FileWithRange(_) => None, } } @@ -2178,7 +2237,7 @@ mod resolve_definition { match kind { DefinitionKind::Import(import_def) => { - let file = definition.file(db); + let file = definition.python_file(db); let module = parsed_module(db, file).load(db); let alias = import_def.alias(&module); @@ -2198,7 +2257,7 @@ mod resolve_definition { return Vec::new(); // Module not found, return empty list }; - let Some(module_file) = resolved_module.file(db) else { + let Some(module_file) = resolved_module.python_file(db) else { return Vec::new(); // No file for module, return empty list }; @@ -2208,7 +2267,7 @@ mod resolve_definition { } DefinitionKind::ImportFrom(import_from_def) => { - let file = definition.file(db); + let file = definition.python_file(db); let module = parsed_module(db, file).load(db); let import_node = import_from_def.import(&module); let alias = import_from_def.alias(&module); @@ -2233,7 +2292,7 @@ mod resolve_definition { // For star imports, try to resolve to the specific symbol being accessed DefinitionKind::StarImport(star_import_def) => { - let file = definition.file(db); + let file = definition.python_file(db); let module = parsed_module(db, file).load(db); let import_node = star_import_def.import(&module); @@ -2261,7 +2320,7 @@ mod resolve_definition { /// Helper function to resolve import definitions for `ImportFrom` and `StarImport` cases. pub(crate) fn resolve_from_import_definitions<'db>( db: &'db dyn Db, - file: File, + file: PythonFile<'db>, import_node: &ast::StmtImportFrom, symbol_name: &str, visited: &mut FxHashSet>, @@ -2272,7 +2331,7 @@ mod resolve_definition { if let Some(asname) = &alias.asname { if asname.as_str() == symbol_name { return vec![ResolvedDefinition::FileWithRange(FileRange::new( - file, + file.file(db), asname.range, ))]; } @@ -2290,7 +2349,7 @@ mod resolve_definition { }; // Resolve the target module file - let module_file = resolved_module.file(db); + let module_file = resolved_module.python_file(db); let Some(module_file) = module_file else { // No file means this is a namespace package, try to import the submodule @@ -2335,7 +2394,7 @@ mod resolve_definition { // Helper to resolve `from x.y import z` assuming `x.y.z` is a module. fn resolve_from_import_submodule_definitions<'db>( db: &'db dyn Db, - file: File, + file: PythonFile<'db>, symbol_name: &str, module_name: ModuleName, ) -> Option> { @@ -2343,7 +2402,7 @@ mod resolve_definition { let mut full_submodule_name = module_name; full_submodule_name.extend(&submodule_name); let module = resolve_module(db, file, &full_submodule_name)?; - let file = module.file(db)?; + let file = module.python_file(db)?; Some(ResolvedDefinition::Module(file)) } @@ -2390,8 +2449,13 @@ mod resolve_definition { def: &ResolvedDefinition<'db>, cached_vendored_typeshed: Option<&SystemPath>, ) -> Option>> { + let Some(stub_parse_file) = def.python_file(db) else { + trace!("Found arbitrary FileWithRange while stub mapping, giving up"); + return None; + }; + // If the file isn't a stub, this is presumably the real definition - let stub_file = def.file(db); + let stub_file = stub_parse_file.file(db); trace!("Stub mapping definition in: {}", stub_file.path(db)); if !stub_file.is_stub(db) { trace!("File isn't a stub, no stub mapping to do"); @@ -2408,7 +2472,7 @@ mod resolve_definition { // we're in typeshed to successfully stub-map to the Real Stdlib. So here we attempt // to do just that. The resulting file must not be used for anything other than // this module lookup, as the `ResolvedDefinition` we're handling isn't for that file. - let mut stub_file_for_module_lookup = stub_file; + let mut stub_file_for_module_lookup = stub_parse_file; if let Some(vendored_typeshed) = cached_vendored_typeshed && let Some(stub_path) = stub_file.path(db).as_system_path() && let Ok(rel_path) = stub_path.strip_prefix(vendored_typeshed) @@ -2419,7 +2483,8 @@ mod resolve_definition { "Stub is cached vendored typeshed: {}", typeshed_file.path(db) ); - stub_file_for_module_lookup = typeshed_file; + stub_file_for_module_lookup = + PythonFile::new(db, typeshed_file, stub_parse_file.python_version(db)); } // It's definitely a stub, so now rerun module resolution but with stubs disabled. @@ -2434,13 +2499,14 @@ mod resolve_definition { // into the interpreter. In which case, all we have are stubs. // `resolve_real_module` will always return `None` for this case, but // it will emit false positive logs. And this saves us some work. - if is_builtin_module(db.python_version().minor, stub_module.name(db)) { + if is_builtin_module(stub_module.python_version(db).minor, stub_module.name(db)) { return None; } let real_module = resolve_real_module(db, stub_file_for_module_lookup, stub_module.name(db))?; trace!("Found real module: {}", real_module.name(db)); - let real_file = real_module.file(db)?; + let real_parse_file = real_module.python_file(db)?; + let real_file = real_parse_file.file(db); trace!("Found real file: {}", real_file.path(db)); // A definition has a "Definition Path" in a file made of nested definitions (~scopes): @@ -2461,7 +2527,7 @@ mod resolve_definition { let stub_ref; match *def { ResolvedDefinition::Definition(definition) => { - stub_parsed = parsed_module(db, stub_file); + stub_parsed = parsed_module(db, definition.python_file(db)); stub_ref = stub_parsed.load(db); // Get the leaf of the path (the definition itself) @@ -2473,7 +2539,7 @@ mod resolve_definition { path.push(leaf); // Get the ancestors of the path (all the definitions we're nested under) - let index = semantic_index(db, stub_file); + let index = semantic_index(db, definition.python_file(db)); for (_scope_id, scope) in index.ancestor_scopes(definition.file_scope(db)) { let node = scope.node(); let component = definition_path_component_for_node(&stub_ref, node) @@ -2493,7 +2559,7 @@ mod resolve_definition { stub_file.path(db), real_file.path(db) ); - return Some(vec![ResolvedDefinition::Module(real_file)]); + return Some(vec![ResolvedDefinition::Module(real_parse_file)]); } ResolvedDefinition::FileWithRange(_) => { // Not yet implemented -- in this case we want to recover something like a Definition @@ -2505,11 +2571,12 @@ mod resolve_definition { // Walk down the Definition Path in the real file let mut definitions = Vec::new(); - let index = semantic_index(db, real_file); - let real_parsed = parsed_module(db, real_file); + let index = semantic_index(db, real_parse_file); + let global_scope = global_scope(db, real_parse_file); + let real_parsed = parsed_module(db, global_scope.python_file(db)); let real_ref = real_parsed.load(db); // Start our search in the module (global) scope - let mut scopes = vec![global_scope(db, real_file)]; + let mut scopes = vec![global_scope]; while let Some(component) = path.pop() { trace!("Traversing definition path component: {}", component); // We're doing essentially a breadth-first traversal of the definitions. @@ -2540,7 +2607,7 @@ mod resolve_definition { definition_path_component_for_node(&real_ref, scope_node) { if real_component == component { - scopes.push(child_scope_id.to_scope_id(db, real_file)); + scopes.push(child_scope_id.to_scope_id(db, real_parse_file)); } } scope.node(db); @@ -2646,11 +2713,11 @@ mod resolve_definition { /// Information about a class in the type hierarchy. #[derive(Debug, Clone)] -pub struct TypeHierarchyClass { +pub struct TypeHierarchyClass<'db> { /// The name of the class. pub name: Name, /// The file containing the class definition. - pub file: ruff_db::files::File, + pub file: PythonFile<'db>, /// The range covering the full class definition header. pub full_range: TextRange, /// The range of the class name (for selection/focus). @@ -2665,8 +2732,12 @@ pub struct TypeHierarchyClass { /// This is meant to be used to "prepare" for a subtype or supertype request. /// That is, this effectively validates whether the given type can be used in /// subsequent requests for supertypes or subtypes. -pub fn type_hierarchy_prepare(db: &dyn Db, ty: Type<'_>) -> Option { - let class_literal = extract_class_literal(db, ty)?; +pub fn type_hierarchy_prepare<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + let class_literal = extract_class_literal(db, env, ty)?; Some(class_literal_to_hierarchy_info(db, class_literal)) } @@ -2676,18 +2747,22 @@ pub fn type_hierarchy_prepare(db: &dyn Db, ty: Type<'_>) -> Option) -> Vec { - let Some(class_literal) = extract_class_literal(db, ty) else { +pub fn type_hierarchy_supertypes<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Vec> { + let Some(class_literal) = extract_class_literal(db, env, ty) else { return vec![]; }; if class_literal.is_known(db, KnownClass::Object) { return vec![]; } - let mut supertypes: Vec = class_literal + let mut supertypes: Vec> = class_literal .explicit_bases(db) .into_iter() - .filter_map(|base| extract_class_literal(db, base)) + .filter_map(|base| extract_class_literal(db, env, base)) .map(|class_literal| class_literal_to_hierarchy_info(db, class_literal)) .collect(); // Every class implicitly inherits from `object` when no explicit @@ -2695,7 +2770,7 @@ pub fn type_hierarchy_supertypes(db: &dyn Db, ty: Type<'_>) -> Vec) -> Vec, - modules: &[Module<'_>], -) -> Vec { - let Some(target_class) = extract_class_literal(db, ty) else { +pub fn type_hierarchy_subtypes<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + modules: &[Module<'db>], +) -> Vec> { + let Some(target_class) = extract_class_literal(db, env, ty) else { return vec![]; }; direct_subtypes(db, target_class, modules) @@ -2738,9 +2814,10 @@ fn direct_subtypes<'db>( let mut subtypes = vec![]; for &module in modules { - let Some(file) = module.file(db) else { + let Some(python_file) = module.python_file(db) else { continue; }; + let file = python_file.file(db); // Note that this will always consider namespace // packages to be "not firsty party." This isn't @@ -2770,7 +2847,8 @@ fn direct_subtypes<'db>( continue; } - for class_ty in reachable_class_literals_in_file(db, file) { + let file_env = ProgramEnvironment::from_file(python_file); + for class_ty in reachable_class_literals_in_file(db, python_file) { let bases = class_ty.explicit_bases(db); let is_subtype = if target_is_object && bases.is_empty() @@ -2779,7 +2857,7 @@ fn direct_subtypes<'db>( true } else { bases.iter().any(|base| { - extract_class_literal(db, *base) + extract_class_literal(db, &file_env, *base) .is_some_and(|base_literal| base_literal == target_class) }) }; @@ -2792,7 +2870,11 @@ fn direct_subtypes<'db>( } /// Enumerates the reachable class definitions in `file`. -fn reachable_class_literals_in_file(db: &dyn Db, file: File) -> Vec> { +fn reachable_class_literals_in_file<'db>( + db: &'db dyn Db, + file: PythonFile<'db>, +) -> Vec> { + let env = ProgramEnvironment::from_file(file); let index = semantic_index(db, file); let parsed = parsed_module(db, file).load(db); let mut classes = Vec::new(); @@ -2816,7 +2898,7 @@ fn reachable_class_literals_in_file(db: &dyn Db, file: File) -> Vec Vec(db: &'db dyn Db, ty: Type<'db>) -> Option> { +fn extract_class_literal<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { match ty { Type::ClassLiteral(class_literal) => Some(class_literal), Type::SubclassOf(subclass_of) => { @@ -2840,11 +2926,11 @@ fn extract_class_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option Some(ClassLiteral::Static(generic_alias.origin(db))), - Type::NominalInstance(instance) => Some(instance.class(db).class_literal(db)), + Type::NominalInstance(instance) => Some(instance.class(db, env).class_literal(db)), Type::Union(union) => union .elements(db) .iter() - .find_map(|elem| extract_class_literal(db, *elem)), + .find_map(|elem| extract_class_literal(db, env, *elem)), _ => None, } @@ -2854,16 +2940,16 @@ fn extract_class_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option, -) -> TypeHierarchyClass { +fn class_literal_to_hierarchy_info<'db>( + db: &'db dyn Db, + class_literal: ClassLiteral<'db>, +) -> TypeHierarchyClass<'db> { let name = class_literal.name(db).clone(); - let file = class_literal.file(db); + let file = class_literal.python_file(db); let (full_range, selection_range) = match class_literal { ClassLiteral::Static(static_class) => { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, static_class.python_file(db)).load(db); let header_range = static_class.header_range(db); let body_scope = static_class.body_scope(db); @@ -2891,7 +2977,7 @@ fn class_literal_to_hierarchy_info( // (likely incorrectly) return the type hierarchy for `type` itself. ClassLiteral::Dynamic(dynamic_class) => { if let DynamicClassAnchor::Definition(definition) = dynamic_class.anchor(db) { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, definition.python_file(db)).load(db); let kind = definition.kind(db); (kind.full_range(&parsed), kind.target_range(&parsed)) } else { @@ -2903,7 +2989,7 @@ fn class_literal_to_hierarchy_info( if let DynamicNamedTupleAnchor::CollectionsDefinition { definition, .. } | DynamicNamedTupleAnchor::TypingDefinition(definition) = namedtuple.anchor(db) { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, definition.python_file(db)).load(db); let kind = definition.kind(db); (kind.full_range(&parsed), kind.target_range(&parsed)) } else { @@ -2917,7 +3003,7 @@ fn class_literal_to_hierarchy_info( } ClassLiteral::DynamicEnum(dynamic_enum) => { if let DynamicEnumAnchor::Definition { definition, .. } = dynamic_enum.anchor(db) { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, definition.python_file(db)).load(db); let kind = definition.kind(db); (kind.full_range(&parsed), kind.target_range(&parsed)) } else { @@ -2939,10 +3025,12 @@ pub fn constructor_signature(model: &SemanticModel, call_expr: &ast::ExprCall) - let function_ty = call_expr.func.inferred_type(model)?; let db = model.db(); let class_name = function_ty.as_class_literal()?.name(db); + let env = &model.program_environment(); let display_sig = |signature: &Signature| { let params = signature .display_with( db, + env, DisplaySettings::default() .multiline() .disallow_signature_name() @@ -2952,8 +3040,10 @@ pub fn constructor_signature(model: &SemanticModel, call_expr: &ast::ExprCall) - format!("class {class_name}{params}") }; - let callable_type = function_ty.try_upcast_to_callable(db)?.into_type(db); - let bindings = callable_type.bindings(db); + let callable_type = function_ty + .try_upcast_to_callable(db, env)? + .into_type(db, env); + let bindings = callable_type.bindings(db, env); if let Some(binding) = bindings.single_element() && binding.overloads().len() == 1 @@ -2986,6 +3076,7 @@ mod tests { use super::{CallArgumentForm, call_argument_forms, contains_identifier}; use crate::SemanticModel; use crate::db::tests::TestDbBuilder; + use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; @@ -3014,6 +3105,7 @@ cast(val="", typ=int) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); + let file = PythonFile::new(&db, file, db.python_version()); let parsed = parsed_module(&db, file).load(&db); let call = parsed .suite() @@ -3054,6 +3146,7 @@ f(y="", x=1) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); + let file = PythonFile::new(&db, file, db.python_version()); let parsed = parsed_module(&db, file).load(&db); let call = parsed .suite() @@ -3090,6 +3183,7 @@ f(val="", typ=int) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); + let file = PythonFile::new(&db, file, db.python_version()); let parsed = parsed_module(&db, file).load(&db); let call = parsed .suite() @@ -3127,6 +3221,7 @@ f("", int) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); + let file = PythonFile::new(&db, file, db.python_version()); let parsed = parsed_module(&db, file).load(&db); let call = parsed .suite() @@ -3167,6 +3262,7 @@ f(int, x) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); + let file = PythonFile::new(&db, file, db.python_version()); let parsed = parsed_module(&db, file).load(&db); let call = parsed .suite() @@ -3211,6 +3307,7 @@ TypeAliasType("Alias", int) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); + let file = PythonFile::new(&db, file, db.python_version()); let parsed = parsed_module(&db, file).load(&db); let calls: Vec<_> = parsed .suite() @@ -3256,6 +3353,7 @@ cast(*args) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); + let file = PythonFile::new(&db, file, db.python_version()); let parsed = parsed_module(&db, file).load(&db); let call = parsed .suite() diff --git a/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs b/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs index 9ca9ce3810..d3f54256cb 100644 --- a/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs +++ b/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs @@ -2,7 +2,7 @@ use crate::Db; use crate::reachability::is_reachable; use get_size2::GetSize; use itertools::Itertools; -use ruff_db::files::File; +use ruff_db::PythonFile; use ruff_text_size::TextRange; use ty_python_core::reachability_constraints::ScopedReachabilityConstraintId; use ty_python_core::semantic_index; @@ -45,10 +45,9 @@ pub enum UnreachableKind { /// `ALWAYS_FALSE` constraints are classified as unconditional; all others are /// unreachable only under the current analysis. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] -pub fn unreachable_ranges(db: &dyn Db, file: File) -> Box<[UnreachableRange]> { +pub fn unreachable_ranges(db: &dyn Db, file: PythonFile<'_>) -> Box<[UnreachableRange]> { let index = semantic_index(db, file); let mut unreachable = Vec::new(); - for scope_id in index.scope_ids() { let use_def = index.use_def_map(scope_id.file_scope_id(db)); unreachable.extend( @@ -93,8 +92,9 @@ fn merge_overlapping_ranges(mut ranges: Vec) -> Box<[Unreachab #[cfg(test)] mod tests { use super::{UnreachableKind, unreachable_ranges}; - use crate::db::tests::TestDbBuilder; + use crate::db::tests::{TestDb, TestDbBuilder}; use insta::assert_snapshot; + use ruff_db::PythonFile; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, Severity, }; @@ -145,9 +145,9 @@ mod tests { } } - fn render_unreachable_diagnostics(db: &crate::db::tests::TestDb, path: &str) -> String { + fn render_unreachable_diagnostics(db: &TestDb, path: &str) -> String { let file = system_path_to_file(db, path).unwrap(); - let diagnostics = unreachable_ranges(db, file) + let diagnostics = unreachable_ranges(db, PythonFile::new(db, file, db.python_version())) .iter() .map(|range| { let mut diagnostic = Diagnostic::new( diff --git a/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs b/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs index 6f318a9029..30c2f941e7 100644 --- a/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs +++ b/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs @@ -3,6 +3,7 @@ use crate::reachability::is_reachable; use crate::types::function::FunctionDecorators; use crate::types::infer::function_known_decorator_flags; use get_size2::GetSize; +use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; use ruff_text_size::TextRange; @@ -102,9 +103,10 @@ pub struct UnusedBinding { /// without broader reference analysis. Bare local annotations (`x: int`) are also /// reported, but only if the symbol is neither bound nor used elsewhere in the scope. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] -pub fn unused_bindings(db: &dyn Db, file: ruff_db::files::File) -> Box<[UnusedBinding]> { +pub fn unused_bindings(db: &dyn Db, file: PythonFile<'_>) -> Box<[UnusedBinding]> { + let source_file = file.file(db); let parsed = parsed_module(db, file).load(db); - let is_stub_file = file.is_stub(db); + let is_stub_file = source_file.is_stub(db); let index = semantic_index(db, file); let mut unused = Vec::new(); // A used synthetic definition counts as a use of the user-visible definitions it represents. @@ -232,6 +234,7 @@ pub fn unused_bindings(db: &dyn Db, file: ruff_db::files::File) -> Box<[UnusedBi mod tests { use super::{UnusedBinding, unused_bindings}; use crate::db::tests::TestDbBuilder; + use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_python_ast::name::Name; use ruff_python_trivia::textwrap::dedent; @@ -243,7 +246,8 @@ mod tests { ) -> anyhow::Result> { let db = TestDbBuilder::new().with_file(path, source).build()?; let file = system_path_to_file(&db, path).unwrap(); - let mut bindings = unused_bindings(&db, file).to_vec(); + let mut bindings = + unused_bindings(&db, PythonFile::new(&db, file, db.python_version())).to_vec(); bindings.sort_unstable_by_key(|binding| (binding.range.start(), binding.range.end())); Ok(bindings) } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index ff3caa9ea7..0df0e79aa3 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -43,6 +43,7 @@ //! of iterations, so if we fail to converge, Salsa will eventually panic. (This should of course //! be considered a bug.) +use crate::ProgramEnvironment; use itertools::Either; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; @@ -125,7 +126,7 @@ fn extend_collection_use_constraints<'db>( cycle_initial=|db, id, definition: Definition<'db>| { DefinitionInference::cycle_initial(db, definition, Type::divergent(id)) }, - cycle_fn=|db, cycle, previous: &DefinitionInference<'db>, inference: DefinitionInference<'db>, definition| { + cycle_fn=|db: &'db dyn Db, cycle, previous: &DefinitionInference<'db>, inference: DefinitionInference<'db>, definition: Definition<'db>| { inference.cycle_normalized(db, previous, cycle, definition) }, heap_size=ruff_memory_usage::heap_size @@ -134,19 +135,29 @@ pub(crate) fn infer_definition_types<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> DefinitionInference<'db> { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let python_file = definition.python_file(db); + let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_definition_types", range = ?definition.kind(db).target_range(&module), - ?file + ?python_file ) .entered(); - let index = semantic_index(db, file); + let index = semantic_index(db, python_file); - TypeInferenceBuilder::new(db, InferenceRegion::Definition(definition), index, &module) - .finish_definition(definition) + let env = ProgramEnvironment::from_file(python_file); + + TypeInferenceBuilder::new( + db, + &env, + InferenceRegion::Definition(definition), + python_file.file(db), + python_file, + index, + &module, + ) + .finish_definition(definition) } /// Returns `true` if the definition refers to a dictionary-key binding that should be discarded. @@ -166,7 +177,8 @@ pub(crate) fn is_discarded_dict_key_assignment<'db>( return false; }; - infer_definition_types(db, dict_key_assignment.assignment()).discards_dict_key_assignments() + let assignment = dict_key_assignment.assignment(); + infer_definition_types(db, assignment).discards_dict_key_assignments() } /// Infer decorator expression types for a function definition. @@ -184,13 +196,18 @@ pub(crate) fn function_known_decorators<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> FunctionDecoratorInference<'db> { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); - let index = semantic_index(db, file); + let python_file = definition.python_file(db); + let module = parsed_module(db, python_file).load(db); + let index = semantic_index(db, python_file); + + let env = ProgramEnvironment::from_file(python_file); TypeInferenceBuilder::new( db, + &env, InferenceRegion::FunctionDecorators(definition), + python_file.file(db), + python_file, index, &module, ) @@ -258,7 +275,7 @@ impl<'db> FunctionDecoratorInference<'db> { cycle_initial=|db, id, definition: Definition<'db>| { DefinitionInference::cycle_initial(db, definition, Type::divergent(id)) }, - cycle_fn=|db, cycle, previous: &DefinitionInference<'db>, inference: DefinitionInference<'db>, definition| { + cycle_fn=|db: &'db dyn Db, cycle, previous: &DefinitionInference<'db>, inference: DefinitionInference<'db>, definition: Definition<'db>| { inference.cycle_normalized(db, previous, cycle, definition) }, heap_size=ruff_memory_usage::heap_size @@ -267,20 +284,30 @@ pub(crate) fn infer_deferred_types<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> DefinitionInference<'db> { - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let python_file = definition.python_file(db); + let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_deferred_types", definition = ?definition.as_id(), range = ?definition.kind(db).target_range(&module), - ?file + ?python_file ) .entered(); - let index = semantic_index(db, file); + let index = semantic_index(db, python_file); + + let env = ProgramEnvironment::from_file(python_file); - TypeInferenceBuilder::new(db, InferenceRegion::Deferred(definition), index, &module) - .finish_definition(definition) + TypeInferenceBuilder::new( + db, + &env, + InferenceRegion::Deferred(definition), + python_file.file(db), + python_file, + index, + &module, + ) + .finish_definition(definition) } /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. @@ -296,13 +323,13 @@ pub(crate) fn infer_complete_scope_types<'db>( // Scopes that may require type context are inferred during the inference of // their outer scope. if scope.accepts_type_context(db) { - let file = scope.file(db); - let index = semantic_index(db, file); + let python_file = scope.python_file(db); + let index = semantic_index(db, python_file); if let Some(parent_scope) = index.parent_scope_id(scope.file_scope_id(db)) { // Note that nested lambdas or comprehensions may require recursing until we reach // an outer scope that is independent of any type context. - return infer_complete_scope_types(db, parent_scope.to_scope_id(db, file)); + return infer_complete_scope_types(db, parent_scope.to_scope_id(db, python_file)); } } @@ -328,8 +355,10 @@ pub(crate) fn infer_scope_types<'db>( #[salsa::tracked( returns(ref), cycle_initial=|_, id, _| ScopeInference::cycle_initial(Type::divergent(id)), - cycle_fn=|db, cycle, previous: &ScopeInference<'db>, inference: ScopeInference<'db>, _| { - inference.cycle_normalized(db, previous, cycle) + cycle_fn=|db, cycle, previous: &ScopeInference<'db>, inference: ScopeInference<'db>, input: InferScope<'db>| { + let (scope, _) = input.into_inner(db); + let env = ProgramEnvironment::from_scope(scope); + inference.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -338,16 +367,28 @@ pub(crate) fn infer_scope_types_impl<'db>( input: InferScope<'db>, ) -> ScopeInference<'db> { let (scope, tcx) = input.into_inner(db); - let file = scope.file(db); - let _span = tracing::trace_span!("infer_scope_types", scope=?scope.as_id(), ?file).entered(); + let python_file = scope.python_file(db); + let _span = + tracing::trace_span!("infer_scope_types", scope=?scope.as_id(), ?python_file).entered(); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, python_file).load(db); // Using the index here is fine because the code below depends on the AST anyway. // The isolation of the query is by the return inferred types. - let index = semantic_index(db, file); + let index = semantic_index(db, python_file); - TypeInferenceBuilder::new(db, InferenceRegion::Scope(scope, tcx), index, &module).finish_scope() + let env = ProgramEnvironment::from_file(python_file); + + TypeInferenceBuilder::new( + db, + &env, + InferenceRegion::Scope(scope, tcx), + python_file.file(db), + python_file, + index, + &module, + ) + .finish_scope() } /// Infer all types for an [`Expression`] (including sub-expressions). @@ -365,8 +406,10 @@ pub(crate) fn infer_expression_types<'db>( #[salsa::tracked( returns(ref), cycle_initial=expression_cycle_initial, - cycle_fn=|db, cycle, previous: &ExpressionInference<'db>, inference: ExpressionInference<'db>, _| { - inference.cycle_normalized(db, previous, cycle) + cycle_fn=|db, cycle, previous: &ExpressionInference<'db>, inference: ExpressionInference<'db>, input: InferExpression<'db>| { + let (expression, _) = input.into_inner(db); + let env = ProgramEnvironment::from_scope(expression.scope(db)); + inference.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -376,21 +419,26 @@ pub(super) fn infer_expression_types_impl<'db>( ) -> ExpressionInference<'db> { let (expression, tcx) = input.into_inner(db); - let file = expression.file(db); - let module = parsed_module(db, file).load(db); + let python_file = expression.python_file(db); + let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_expression_types", expression = ?expression.as_id(), range = ?expression.node_ref(db).node(&module).range(), - ?file + ?python_file ) .entered(); - let index = semantic_index(db, file); + let index = semantic_index(db, python_file); + + let env = ProgramEnvironment::from_file(python_file); TypeInferenceBuilder::new( db, + &env, InferenceRegion::Expression(expression, tcx), + python_file.file(db), + python_file, index, &module, ) @@ -439,8 +487,10 @@ pub(crate) fn infer_expression_type<'db>( #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _| { - result.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, input: InferExpression<'db>| { + let (expression, _) = input.into_inner(db); + let env = ProgramEnvironment::from_scope(expression.scope(db)); + result.cycle_normalized(db, &env, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -479,8 +529,9 @@ pub(super) fn infer_statement_types<'db>( cycle_initial=|db, id, statement: StatementInner<'db>| { StatementInferenceInner::cycle_initial(statement.scope(db), Type::divergent(id)) }, - cycle_fn=|db, cycle, previous: &StatementInferenceInner<'db>, inference: StatementInferenceInner<'db>, _| { - inference.cycle_normalized(db, previous, cycle) + cycle_fn=|db, cycle, previous: &StatementInferenceInner<'db>, inference: StatementInferenceInner<'db>, statement: StatementInner<'db>| { + let env = ProgramEnvironment::from_file(statement.python_file(db)); + inference.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -488,20 +539,30 @@ fn infer_statement_types_impl<'db>( db: &'db dyn Db, statement: StatementInner<'db>, ) -> StatementInferenceInner<'db> { - let file = statement.file(db); - let module = parsed_module(db, file).load(db); + let python_file = statement.python_file(db); + let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_statement_types", statement = ?statement.as_id(), range = ?statement.node_ref(db).node(&module).range(), - ?file + ?python_file ) .entered(); - let index = semantic_index(db, file); + let index = semantic_index(db, python_file); - TypeInferenceBuilder::new(db, InferenceRegion::Statement(statement), index, &module) - .finish_statement() + let env = ProgramEnvironment::from_file(python_file); + + TypeInferenceBuilder::new( + db, + &env, + InferenceRegion::Statement(statement), + python_file.file(db), + python_file, + index, + &module, + ) + .finish_statement() } /// An `Expression` with an optional `TypeContext`. @@ -602,10 +663,11 @@ impl<'db> TypeContext<'db> { fn known_specialization( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, known_class: KnownClass, ) -> Option> { self.annotation - .and_then(|ty| ty.known_specialization(db, known_class)) + .and_then(|ty| ty.known_specialization(db, env, known_class)) } fn map(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Self { @@ -620,11 +682,15 @@ impl<'db> TypeContext<'db> { } /// If the type annotation is a union, returns the target elements that it can be narrowed to. - fn narrow_targets(&self, db: &'db dyn Db) -> Option]>> { + fn narrow_targets( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option]>> { let union = self.annotation?.as_union_like(db)?; let targets = if union.has_aliases(db) { - let expanded = union.expand_aliases(db); + let expanded = union.expand_aliases(db, env); if let Some(union) = expanded.as_union_like(db) { Cow::Borrowed(union.elements(db)) } else { @@ -656,18 +722,24 @@ impl<'db> From> for TypeContext<'db> { #[salsa::tracked( returns(ref), cycle_initial=|_, id, _| UnpackResult::cycle_initial(Type::divergent(id)), - cycle_fn=|db, cycle, previous: &UnpackResult<'db>, result: UnpackResult<'db>, _| { - result.cycle_normalized(db, previous, cycle) + cycle_fn=|db, cycle, previous: &UnpackResult<'db>, result: UnpackResult<'db>, unpack: Unpack<'db>| { + let env = ProgramEnvironment::from_file(unpack.python_file(db)); + result.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] pub(super) fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> UnpackResult<'db> { - let file = unpack.file(db); - let module = parsed_module(db, file).load(db); - let _span = tracing::trace_span!("infer_unpack_types", range=?unpack.range(db, &module), ?file) - .entered(); + let python_file = unpack.python_file(db); + let module = parsed_module(db, python_file).load(db); + let _span = tracing::trace_span!( + "infer_unpack_types", + range=?unpack.range(db, &module), + ?python_file + ) + .entered(); - let mut unpacker = Unpacker::new(db, unpack.target_scope(db), &module); + let env = ProgramEnvironment::from_file(python_file); + let mut unpacker = Unpacker::new(db, &env, unpack.target_scope(db), python_file, &module); unpacker.unpack(unpack.target(db, &module), unpack.value(db)); unpacker.finish() } @@ -820,11 +892,12 @@ impl<'db> ScopeInference<'db> { fn cycle_normalized( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous_inference: &ScopeInference<'db>, cycle: &salsa::Cycle, ) -> ScopeInference<'db> { self.expressions.map_values(|expr, ty| { - ty.cycle_normalized(db, previous_inference.expression_type(expr), cycle) + ty.cycle_normalized(db, env, previous_inference.expression_type(expr), cycle) }); if cycle.iteration() > crate::TAINTED_CYCLES @@ -1008,6 +1081,7 @@ impl<'db> DefinitionTypes<'db> { fn normalize_binding( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: &DefinitionTypes<'db>, cycle: &salsa::Cycle, owner: Definition<'db>, @@ -1015,14 +1089,15 @@ impl<'db> DefinitionTypes<'db> { ty: Type<'db>, ) -> Type<'db> { if let Some(previous_ty) = previous.binding_type(owner, definition) { - ty.cycle_normalized(db, previous_ty, cycle) + ty.cycle_normalized(db, env, previous_ty, cycle) } else { - ty.recursive_type_normalized(db, cycle) + ty.recursive_type_normalized(db, env, cycle) } } fn normalize_declaration( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: &DefinitionTypes<'db>, cycle: &salsa::Cycle, owner: Definition<'db>, @@ -1030,15 +1105,16 @@ impl<'db> DefinitionTypes<'db> { ty: TypeAndQualifiers<'db>, ) -> TypeAndQualifiers<'db> { if let Some(previous_ty) = previous.declaration_type(owner, definition) { - ty.map_type(|inner| inner.cycle_normalized(db, previous_ty.inner_type(), cycle)) + ty.map_type(|inner| inner.cycle_normalized(db, env, previous_ty.inner_type(), cycle)) } else { - ty.map_type(|inner| inner.recursive_type_normalized(db, cycle)) + ty.map_type(|inner| inner.recursive_type_normalized(db, env, cycle)) } } fn cycle_normalized( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: &DefinitionTypes<'db>, cycle: &salsa::Cycle, owner: Definition<'db>, @@ -1046,22 +1122,30 @@ impl<'db> DefinitionTypes<'db> { match self { Self::Empty => Self::Empty, Self::Binding(ty) => Self::Binding(Self::normalize_binding( - db, previous, cycle, owner, owner, ty, + db, env, previous, cycle, owner, owner, ty, )), Self::Declaration(ty) => Self::Declaration(Self::normalize_declaration( - db, previous, cycle, owner, owner, ty, + db, env, previous, cycle, owner, owner, ty, )), Self::BindingAndDeclaration(declaration_ty) => { let binding_ty = Self::normalize_binding( db, + env, previous, cycle, owner, owner, declaration_ty.inner_type(), ); - let declaration_ty = - Self::normalize_declaration(db, previous, cycle, owner, owner, declaration_ty); + let declaration_ty = Self::normalize_declaration( + db, + env, + previous, + cycle, + owner, + owner, + declaration_ty, + ); if binding_ty == declaration_ty.inner_type() { Self::BindingAndDeclaration(declaration_ty) @@ -1074,10 +1158,19 @@ impl<'db> DefinitionTypes<'db> { } Self::Other(mut other) => { for (definition, ty) in &mut other.bindings { - *ty = Self::normalize_binding(db, previous, cycle, owner, *definition, *ty); + *ty = + Self::normalize_binding(db, env, previous, cycle, owner, *definition, *ty); } for (definition, ty) in &mut other.declarations { - *ty = Self::normalize_declaration(db, previous, cycle, owner, *definition, *ty); + *ty = Self::normalize_declaration( + db, + env, + previous, + cycle, + owner, + *definition, + *ty, + ); } match (&*other.bindings, &*other.declarations) { @@ -1270,12 +1363,14 @@ impl<'db> DefinitionInference<'db> { definition: Definition<'db>, cycle_recovery: Type<'db>, ) -> Self { + let env = ProgramEnvironment::from_definition(definition); let mut types = DefinitionTypes::Empty; // Eagerly store more precise types for collection literals to avoid an extra // cycle iteration, i.e., by inferring `list[Divergent]` instead of `Divergent`. if let DefinitionKind::Assignment(assignment) = definition.kind(db) { - let module = parsed_module(db, definition.file(db)).load(db); + let python_file = definition.python_file(db); + let module = parsed_module(db, python_file).load(db); let known_collection = match assignment.value(&module) { ast::Expr::Set(_) => Some(KnownClass::Set), ast::Expr::List(_) => Some(KnownClass::List), @@ -1283,15 +1378,16 @@ impl<'db> DefinitionInference<'db> { _ => None, }; - if let Some(collection_class) = known_collection - .and_then(|known_collection| known_collection.try_to_class_literal(db)) - { - let divergent_collection = collection_class - .apply_specialization(db, |generic_context| { - generic_context.repeat_specialization(db, cycle_recovery) - }); + if let Some(known_collection) = known_collection { + if let Some(collection_class) = known_collection.try_to_class_literal(db, &env) { + let divergent_collection = collection_class + .apply_specialization(db, |generic_context| { + generic_context.repeat_specialization(db, cycle_recovery) + }); - types = DefinitionTypes::Binding(Type::instance(db, divergent_collection)); + types = + DefinitionTypes::Binding(Type::instance(db, &env, divergent_collection)); + } } } @@ -1316,12 +1412,14 @@ impl<'db> DefinitionInference<'db> { cycle: &salsa::Cycle, definition: Definition<'db>, ) -> DefinitionInference<'db> { + let env = ProgramEnvironment::from_definition(definition); for (expr, ty) in &mut self.expressions { let previous_ty = previous_inference.expression_type(*expr); - *ty = ty.cycle_normalized(db, previous_ty, cycle); + *ty = ty.cycle_normalized(db, &env, previous_ty, cycle); } self.types = std::mem::take(&mut self.types).cycle_normalized( db, + &env, &previous_inference.types, cycle, definition, @@ -1558,6 +1656,7 @@ impl<'db> ExpressionInference<'db> { fn cycle_normalized( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: &ExpressionInference<'db>, cycle: &salsa::Cycle, ) -> ExpressionInference<'db> { @@ -1569,16 +1668,16 @@ impl<'db> ExpressionInference<'db> { .iter() .find(|(previous_binding, _)| previous_binding == binding) }) { - *binding_ty = binding_ty.cycle_normalized(db, *previous_binding, cycle); + *binding_ty = binding_ty.cycle_normalized(db, env, *previous_binding, cycle); } else { - *binding_ty = binding_ty.recursive_type_normalized(db, cycle); + *binding_ty = binding_ty.recursive_type_normalized(db, env, cycle); } } } for (expr, ty) in &mut self.expressions { let previous_ty = previous.expression_type(*expr); - *ty = ty.cycle_normalized(db, previous_ty, cycle); + *ty = ty.cycle_normalized(db, env, previous_ty, cycle); } if cycle.iteration() > crate::TAINTED_CYCLES @@ -1734,12 +1833,13 @@ impl<'db> StatementInferenceInner<'db> { fn cycle_normalized( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous_inference: &StatementInferenceInner<'db>, cycle: &salsa::Cycle, ) -> StatementInferenceInner<'db> { for (expr, ty) in &mut self.expressions { let previous_ty = previous_inference.expression_type(*expr); - *ty = ty.cycle_normalized(db, previous_ty, cycle); + *ty = ty.cycle_normalized(db, env, previous_ty, cycle); } for (binding, binding_ty) in &mut self.bindings { if let Some((_, previous_binding)) = previous_inference @@ -1747,9 +1847,9 @@ impl<'db> StatementInferenceInner<'db> { .iter() .find(|(previous_binding, _)| previous_binding == binding) { - *binding_ty = binding_ty.cycle_normalized(db, *previous_binding, cycle); + *binding_ty = binding_ty.cycle_normalized(db, env, *previous_binding, cycle); } else { - *binding_ty = binding_ty.recursive_type_normalized(db, cycle); + *binding_ty = binding_ty.recursive_type_normalized(db, env, cycle); } } for (declaration, declaration_ty) in &mut self.declarations { @@ -1759,11 +1859,11 @@ impl<'db> StatementInferenceInner<'db> { .find(|(previous_declaration, _)| previous_declaration == declaration) { *declaration_ty = declaration_ty.map_type(|decl_ty| { - decl_ty.cycle_normalized(db, previous_declaration.inner_type(), cycle) + decl_ty.cycle_normalized(db, env, previous_declaration.inner_type(), cycle) }); } else { - *declaration_ty = - declaration_ty.map_type(|decl_ty| decl_ty.recursive_type_normalized(db, cycle)); + *declaration_ty = declaration_ty + .map_type(|decl_ty| decl_ty.recursive_type_normalized(db, env, cycle)); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index f853f7ff5b..e300c38c0d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use compact_str::CompactString; use itertools::Itertools; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; use ruff_db::source::source_text; @@ -114,13 +115,13 @@ use crate::types::{ CallableTypes, ClassType, DynamicType, InferenceFlags, InternedConstraintSet, InternedType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, KnownUnion, LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, ParamSpecAttrKind, Parameter, - Parameters, SentinelInstance, Signature, SpecialFormType, SubclassOfType, Type, TypeAliasType, - TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarKind, - TypeVarVariance, TypedDictModule, TypedDictType, UnionAccumulator, UnionBuilder, UnionType, - any_over_type, binding_type, extract_fixed_length_iterable_element_types, + Parameters, ProgramEnvironment, SentinelInstance, Signature, SpecialFormType, SubclassOfType, + Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, + TypeVarKind, TypeVarVariance, TypedDictModule, TypedDictType, UnionAccumulator, UnionBuilder, + UnionType, any_over_type, binding_type, extract_fixed_length_iterable_element_types, infer_complete_scope_types, infer_scope_types, is_discarded_dict_key_assignment, todo_type, }; -use crate::{AnalysisSettings, Db, FxIndexSet, FxOrderSet, Program}; +use crate::{AnalysisSettings, Db, FxIndexSet, FxOrderSet}; use ty_python_core::ast_ids::ScopedUseId; use ty_python_core::definition::{ AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, ComprehensionDefinitionKind, @@ -370,6 +371,7 @@ pub(super) struct TypeInferenceBuilder<'db, 'ast> { fn transparent_callable_decorator_result<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, bindings: &Bindings<'db>, decorated_ty: Type<'db>, ) -> Option> { @@ -392,6 +394,7 @@ fn transparent_callable_decorator_result<'db>( fn callable_paramspec_and_return<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option<(BoundTypeVarInstance<'db>, TransparentCallableReturn<'db>)> { let callable = ty.resolve_type_alias(db).as_callable()?; @@ -405,9 +408,10 @@ fn transparent_callable_decorator_result<'db>( let return_typevar = if let Some(typevar) = signature.return_ty.as_typevar() { TransparentCallableReturn::TypeVar(typevar) } else { - let specialization = signature - .return_ty - .known_specialization(db, KnownClass::Awaitable)?; + let specialization = + signature + .return_ty + .known_specialization(db, env, KnownClass::Awaitable)?; let [inner] = specialization.types(db) else { return None; }; @@ -419,21 +423,22 @@ fn transparent_callable_decorator_result<'db>( if !matches!(decorated_ty, Type::FunctionLiteral(_) | Type::Callable(_)) { return None; } + let binding = bindings.single_element()?; let (_, overload) = binding.matching_overloads().exactly_one().ok()?; let decorator_signature = &overload.signature; let bound_signature = binding .bound_type - .map(|bound_type| decorator_signature.bind_self(db, Some(bound_type))); + .map(|bound_type| decorator_signature.bind_self(db, env, Some(bound_type))); let decorator_signature = bound_signature.as_ref().unwrap_or(decorator_signature); let [parameter] = decorator_signature.parameters().as_slice() else { return None; }; let (parameter_callable_paramspec, parameter_callable_return) = - callable_paramspec_and_return(db, parameter.annotated_type())?; + callable_paramspec_and_return(db, env, parameter.annotated_type())?; let (return_callable_paramspec, return_callable_return) = - callable_paramspec_and_return(db, decorator_signature.return_ty)?; + callable_paramspec_and_return(db, env, decorator_signature.return_ty)?; if !parameter_callable_paramspec.is_same_typevar_as(db, return_callable_paramspec) || !parameter_callable_return.matches(db, return_callable_return) { @@ -457,13 +462,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Creates a new builder for inferring types in a region. pub(super) fn new( db: &'db dyn Db, + env: &'ast ProgramEnvironment<'db>, region: InferenceRegion<'db>, + file: File, + python_file: PythonFile<'db>, index: &'db SemanticIndex<'db>, module: &'ast ParsedModuleRef, ) -> Self { let scope = region.scope(db); Self { - context: InferContext::new(db, scope, module), + context: InferContext::new(db, env, scope, file, python_file, module), index, region, scope, @@ -528,11 +536,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn extend_cycle_recovery(&mut self, other: Option>) { + let db = self.db(); if let Some(other) = other { match self.cycle_recovery { Some(existing) => { - self.cycle_recovery = - Some(UnionType::from_two_elements(self.db(), existing, other)); + self.cycle_recovery = Some(UnionType::from_two_elements( + db, + self.program_environment(), + existing, + other, + )); } None => { self.cycle_recovery = Some(other); @@ -763,6 +776,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.file() } + fn python_file(&self) -> PythonFile<'db> { + self.context.python_file() + } + + #[inline] + fn program_environment(&self) -> &'ast ProgramEnvironment<'db> { + self.context.program_environment() + } + fn module(&self) -> &'ast ParsedModuleRef { self.context.module() } @@ -782,7 +804,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn bindings_for_call(&self, callable_type: Type<'db>) -> Bindings<'db> { let db = self.db(); callable_type - .bindings(db) + .bindings(db, self.program_environment()) .with_enclosing_binding_contexts(enclosing_binding_contexts( self.index, self.scope().file_scope_id(db), @@ -850,9 +872,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// This is true for stub files, for files with `__future__.annotations`, and /// by default for all source files in Python 3.14 and later. fn defer_annotations(&self) -> bool { + let db = self.db(); self.index.has_future_annotations() || self.in_stub() - || Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY314 + || self.program_environment().python_version(db) >= PythonVersion::PY314 } /// Are we currently in a context where name resolution should be deferred @@ -962,7 +985,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// already in progress for that scope (further up the stack). fn file_expression_type(&self, expression: &ast::Expr) -> Type<'db> { let file_scope = self.index.expression_scope_id(expression); - let expr_scope = file_scope.to_scope_id(self.db(), self.file()); + let expr_scope = file_scope.to_scope_id(self.db(), self.python_file()); match self.region { InferenceRegion::Scope(scope, _) if scope == expr_scope => { self.expression_type(expression) @@ -974,7 +997,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Get metadata for a type expression from any scope in the same file. fn file_type_expression_flags(&self, expression: &ast::Expr) -> TypeExpressionFlags { let file_scope = self.index.expression_scope_id(expression); - let expr_scope = file_scope.to_scope_id(self.db(), self.file()); + let expr_scope = file_scope.to_scope_id(self.db(), self.python_file()); match self.region { InferenceRegion::Scope(scope, _) if scope == expr_scope => { self.type_expression_flags(expression) @@ -1403,8 +1426,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (use_def.declarations_at_binding(binding), true) }; + let env = self.program_environment(); let (mut place_and_quals, conflicting) = place_from_declarations_with_reachability_cache( - self.db(), + db, + env, declarations, self.reachability_cache(), ) @@ -1416,7 +1441,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(builder) = self.context.report_lint(&CONFLICTING_DECLARATIONS, node) { builder.into_diagnostic(format_args!( "Conflicting declared types for `{place}`: {}", - format_enumeration(conflicting.iter().map(|ty| ty.display(db))) + format_enumeration(conflicting.iter().map(|ty| ty.display(db, env))) )); } } @@ -1430,8 +1455,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if self.skip_non_global_scopes(file_scope_id, symbol_id) || self.scope.file_scope_id(self.db()).is_global() { - place_and_quals = place_and_quals.or_fall_back_to(self.db(), || { - module_type_implicit_global_declaration(self.db(), symbol.name()) + place_and_quals = place_and_quals.or_fall_back_to(db, env, || { + module_type_implicit_global_declaration(db, env, symbol.name()) }); } } @@ -1461,14 +1486,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// normal attribute or subscript lookup on its receiver. fn fallback_member_declared_type(&mut self, node: AnyNodeRef<'_>) -> Option> { let db = self.db(); - if let AnyNodeRef::ExprAttribute(ast::ExprAttribute { value, attr, .. }) = node { let value_type = self.infer_maybe_standalone_expression(value, TypeContext::default()); if let Place::Defined(DefinedPlace { ty, definedness: Definedness::AlwaysDefined, .. - }) = value_type.member(db, attr).place + }) = value_type + .member(db, self.program_environment(), attr) + .place { // TODO: also consider qualifiers on the attribute Some(ty) @@ -1585,6 +1611,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { declaration: Definition<'db>, ty: TypeAndQualifiers<'db>, ) { + let db = self.db(); debug_assert!( declaration .kind(self.db()) @@ -1593,15 +1620,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); let use_def = self.index.use_def_map(declaration.file_scope(self.db())); let prior_bindings = use_def.bindings_at_definition(declaration); + let env = self.program_environment(); // unbound_ty is Never because for this check we don't care about unbound let inferred_ty = place_from_bindings_with_reachability_cache( - self.db(), + db, + env, prior_bindings, self.reachability_cache(), ) .place .with_qualifiers(TypeQualifiers::empty()) - .or_fall_back_to(self.db(), || { + .or_fall_back_to(db, env, || { // Fallback to bindings declared on `types.ModuleType` if it's a global symbol let scope = self.scope().file_scope_id(self.db()); let place = self @@ -1612,7 +1641,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let PlaceExprRef::Symbol(symbol) = &place && scope.is_global() { - module_type_implicit_global_symbol(self.db(), self.file(), symbol.name()) + module_type_implicit_global_symbol(db, self.python_file(), symbol.name()) } else { Place::Undefined.into() } @@ -1620,14 +1649,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .place .ignore_possibly_undefined() .unwrap_or(Type::Never); - let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_type()) { + let ty = if inferred_ty.is_assignable_to(db, env, ty.inner_type()) { ty } else { 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()) + ty.inner_type().display(db, env), + inferred_ty.display(db, env) )); } TypeAndQualifiers::declared(Type::unknown()) @@ -1641,6 +1670,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definition: Definition<'db>, declared_and_inferred_ty: &DeclaredAndInferredType<'db>, ) { + let db = self.db(); debug_assert!( definition .kind(self.db()) @@ -1662,46 +1692,52 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { declared_ty, inferred_ty, } => { + let env = self.program_environment(); let file_scope_id = self.scope().file_scope_id(self.db()); if file_scope_id.is_global() { let place_table = self.index.place_table(file_scope_id); let place = place_table.place(definition.place(self.db())); - let file = self.file(); if let Some(module_type_implicit_declaration) = place .as_symbol() .map(|symbol| { - module_type_implicit_global_symbol(self.db(), file, symbol.name()) + module_type_implicit_global_symbol( + db, + self.python_file(), + symbol.name(), + ) }) .and_then(|place| place.place.ignore_possibly_undefined()) { let declared_type = declared_ty.inner_type(); - if !declared_type - .is_assignable_to(self.db(), module_type_implicit_declaration) - { + if !declared_type.is_assignable_to( + db, + env, + module_type_implicit_declaration, + ) { if let Some(builder) = self.context.report_lint(&INVALID_DECLARATION, node) { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot shadow implicit global attribute `{place}` with declaration of type `{}`", - declared_type.display(self.db()) + declared_type.display(db, env) )); diagnostic.info(format_args!("The global symbol `{}` must always have a type assignable to `{}`", place, - module_type_implicit_declaration.display(self.db()) + module_type_implicit_declaration.display(db, env) )); } } } } let declared_type = declared_ty.inner_type(); - if inferred_ty.is_assignable_to(self.db(), declared_type) { + if inferred_ty.is_assignable_to(db, env, declared_type) { if !should_preserve_inferred_binding_type(inferred_ty) // TODO We currently can't distinguish here between "no declared type" and // "declared types is `Unknown` (e.g. due to a bad annotation, missing // import, etc.)". Ideally we would still prefer `Unknown` declared type, // but use inferred type if there is no declared type. && !matches!(declared_type, Type::Dynamic(DynamicType::Unknown)) - && declared_type.is_assignable_to(self.db(), inferred_ty) + && declared_type.is_assignable_to(db, env, inferred_ty) { (declared_ty, declared_type) } else { @@ -1762,6 +1798,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_type_alias(&mut self, type_alias: &ast::StmtTypeAlias) { + let db = self.db(); let previous_check_unbound_typevars = self .context .inference_flags @@ -1790,7 +1827,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // type IntOrStr = int | StrOrInt # It's redundant, but OK // type StrOrInt = str | IntOrStr # It's redundant, but OK // ``` - let expanded = value_ty.expand_eagerly(self.db()); + let expanded = value_ty.expand_eagerly(db, self.program_environment()); if expanded.is_divergent() { if let Some(builder) = self .context @@ -1945,6 +1982,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_body(&mut self, suite: &[ast::Stmt]) { + let db = self.db(); for statement in suite { self.infer_maybe_standalone_statement(statement); @@ -1961,7 +1999,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { builder.into_diagnostic(format_args!( "Object of type `{}` is not awaited", - ty.display(self.db()), + ty.display(db, self.program_environment()), )); } } @@ -2041,7 +2079,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let rhs_scope = self .index .node_scope(NodeWithScopeRef::TypeAlias(type_alias)) - .to_scope_id(self.db(), self.file()); + .to_scope_id(self.db(), self.python_file()); let type_alias_ty = Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( @@ -2058,6 +2096,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_if_statement(&mut self, if_statement: &ast::StmtIf) { + let db = self.db(); + let env = self.program_environment(); let ast::StmtIf { range: _, node_index: _, @@ -2068,7 +2108,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let test_ty = self.infer_standalone_expression(test, TypeContext::default()); - if let Err(err) = test_ty.try_bool(self.db()) { + if let Err(err) = test_ty.try_bool(db, env) { err.report_diagnostic(&self.context, &**test); } @@ -2085,7 +2125,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(test) = &test { let test_ty = self.infer_standalone_expression(test, TypeContext::default()); - if let Err(err) = test_ty.try_bool(self.db()) { + if let Err(err) = test_ty.try_bool(db, env) { err.report_diagnostic(&self.context, test); } } @@ -2135,6 +2175,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_with_statement(&mut self, with_statement: &ast::StmtWith) { + let db = self.db(); let ast::StmtWith { range: _, node_index: _, @@ -2151,7 +2192,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // `with not_context_manager as a.x: ... builder .infer_standalone_expression(&item.context_expr, tcx) - .enter(builder.db()) + .enter(db, builder.program_environment()) }); } else { // Call into the context expression inference to validate that it evaluates @@ -2206,50 +2247,52 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { context_expression_type: Type<'db>, is_async: bool, ) -> Type<'db> { + let db = self.db(); let eval_mode = if is_async { EvaluationMode::Async } else { EvaluationMode::Sync }; + let env = self.program_environment(); context_expression_type - .try_enter_with_mode(self.db(), eval_mode) + .try_enter_with_mode(db, env, eval_mode) .unwrap_or_else(|err| { err.report_diagnostic( &self.context, context_expression_type, context_expression.into(), ); - err.fallback_enter_type(self.db()) + err.fallback_enter_type(db, env) }) } fn infer_exception(&mut self, node: Option<&ast::Expr>, is_star: bool) -> Type<'db> { + let db = self.db(); // If there is no handled exception, it's invalid syntax; // a diagnostic will have already been emitted let node_ty = node.map_or(Type::unknown(), |ty| { self.infer_expression(ty, TypeContext::default()) }); - let type_base_exception = KnownClass::BaseException.to_subclass_of(self.db()); + let env = self.program_environment(); + let type_base_exception = KnownClass::BaseException.to_subclass_of(db, env); // If it's an `except*` handler, this won't actually be the type of the bound symbol; // it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`. - let symbol_ty = if let Some(tuple_spec) = node_ty.tuple_instance_spec(self.db()) { - let mut builder = UnionBuilder::new(self.db()); + let symbol_ty = if let Some(tuple_spec) = node_ty.tuple_instance_spec(db, env) { + let mut builder = UnionBuilder::new(db, env); let mut invalid_elements = vec![]; for (index, element) in tuple_spec.iter_element_types(self.db()).enumerate() { - builder = builder.add( - if element.is_assignable_to(self.db(), type_base_exception) { - element.to_instance_approximation(self.db()).expect( - "`Type::to_instance()` should always return `Some()` \ + builder.add_in_place(if element.is_assignable_to(db, env, type_base_exception) { + element.to_instance_approximation(db, env).expect( + "`Type::to_instance()` should always return `Some()` \ if called on a type assignable to `type[BaseException]`", - ) - } else { - invalid_elements.push((index, element)); - Type::unknown() - }, - ); + ) + } else { + invalid_elements.push((index, element)); + Type::unknown() + }); } if !invalid_elements.is_empty() @@ -2280,16 +2323,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { symbol_ty } else if node_ty.is_assignable_to( - self.db(), + db, + env, UnionType::from_two_elements( - self.db(), + db, + env, type_base_exception, - Type::homogeneous_tuple(self.db(), type_base_exception), + Type::homogeneous_tuple(db, env, type_base_exception), ), ) { // TODO: Handle valid handler expressions that are opaque to the structural helper // above, for example a type variable bounded by the full class-or-tuple union. - KnownClass::BaseException.to_instance(self.db()) + KnownClass::BaseException.to_instance(db, env) } else { if let Some(node) = node { report_invalid_exception_caught(&self.context, node, node_ty); @@ -2298,14 +2343,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if is_star { - let class = if symbol_ty - .is_subtype_of(self.db(), KnownClass::Exception.to_instance(self.db())) - { - KnownClass::ExceptionGroup - } else { - KnownClass::BaseExceptionGroup - }; - class.to_specialized_instance(self.db(), &[symbol_ty]) + let class = + if symbol_ty.is_subtype_of(db, env, KnownClass::Exception.to_instance(db, env)) { + KnownClass::ExceptionGroup + } else { + KnownClass::BaseExceptionGroup + }; + class.to_specialized_instance(db, env, &[symbol_ty]) } else { symbol_ty } @@ -2316,13 +2360,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ty: Type<'db>, type_base_exception: Type<'db>, ) -> Option> { - if let Some(tuple_spec) = ty.tuple_instance_spec(self.db()) { + let db = self.db(); + let env = self.program_environment(); + + if let Some(tuple_spec) = ty.tuple_instance_spec(db, env) { // `except (ValueError, TypeError) as e:` UnionType::try_from_elements( - self.db(), + db, + env, tuple_spec.iter_element_types(self.db()).map(|element| { - if element.is_assignable_to(self.db(), type_base_exception) { - Some(element.to_instance_approximation(self.db()).expect( + if element.is_assignable_to(db, env, type_base_exception) { + Some(element.to_instance_approximation(db, env).expect( "`Type::to_instance()` should always return `Some()` \ if called on a type assignable to `type[BaseException]`", )) @@ -2331,40 +2379,42 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }), ) - } else if ty.is_assignable_to(self.db(), type_base_exception) { + } else if ty.is_assignable_to(db, env, type_base_exception) { // `except ValueError as e:` - Some(ty.to_instance_approximation(self.db()).expect( + Some(ty.to_instance_approximation(db, env).expect( "`Type::to_instance()` should always return `Some()` \ if called on a type assignable to `type[BaseException]`", )) } else if ty.is_assignable_to( - self.db(), - Type::homogeneous_tuple(self.db(), type_base_exception), + db, + env, + Type::homogeneous_tuple(db, env, type_base_exception), ) { // `except exception_types as e:`, where // `exception_types: tuple[type[ValueError], ...]` Some( - ty.tuple_instance_spec(self.db()) + ty.tuple_instance_spec(db, env) .and_then(|spec| { let specialization = spec - .homogeneous_element_type(self.db()) - .to_instance_approximation(self.db()); + .homogeneous_element_type(db, env) + .to_instance_approximation(db, env); debug_assert!(specialization.is_some_and(|specialization_type| { specialization_type.is_assignable_to( - self.db(), - KnownClass::BaseException.to_instance(self.db()), + db, + env, + KnownClass::BaseException.to_instance(db, env), ) })); specialization }) - .unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db())), + .unwrap_or_else(|| KnownClass::BaseException.to_instance(db, env)), ) } else if let Type::Union(union) = ty { // `except exception_types as e:`, where // `exception_types: type[ValueError] | tuple[type[ValueError], ...]` - union.try_map(self.db(), |element| { + union.try_map(db, env, |element| { self.exception_handler_symbol_ty_from_valid_ty(*element, type_base_exception) }) } else { @@ -2402,9 +2452,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // This cutoff was chosen by benchmarking real isort to keep loop analysis // overhead minimal while preserving diagnostics. const MAX_EXACT_LOOP_HEADER_REACHABILITY_NODES: usize = 4096; - let db = self.db(); - let loop_header = loop_header_reachability(db, definition); + + let loop_header = loop_header_reachability(self.db(), definition); let use_def = self .index .use_def_map(self.scope().file_scope_id(self.db())); @@ -2420,14 +2470,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let place = loop_header_kind.place(); - - let mut union = UnionBuilder::new(db).recursively_defined(RecursivelyDefined::Yes); + let env = self.program_environment(); + let mut union = UnionBuilder::new(db, env).recursively_defined(RecursivelyDefined::Yes); for reachable_binding in &loop_header.reachable_bindings { let binding_ty = binding_type(db, reachable_binding.definition); let narrowed_ty = use_def .narrowing_evaluator(reachable_binding.narrowing_constraint) - .narrow(db, binding_ty, place); + .narrow(db, env, binding_ty, place); union.add_in_place(narrowed_ty); } @@ -2466,7 +2516,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { NestedBindingExecution::Lazy => RecursivelyDefined::Yes, NestedBindingExecution::Eager => RecursivelyDefined::No, }; - let mut union = UnionBuilder::new(db).recursively_defined(recursively_defined); + let env = self.program_environment(); + let mut union = UnionBuilder::new(db, env).recursively_defined(recursively_defined); for bindings in binding_sources { if nested_bindings_kind.execution == NestedBindingExecution::Eager { // A comprehension can execute repeatedly, so a source that is unreachable in the @@ -2479,6 +2530,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let ty = binding_type(db, source); union.add_in_place(binding.narrowing_constraint.narrow( db, + env, ty, source.place(db), )); @@ -2488,6 +2540,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let Some(ty) = place_from_bindings_with_reachability_cache( db, + env, bindings, self.reachability_cache(), ) @@ -2500,12 +2553,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let ty = union.build(); let ty = match nested_bindings_kind.execution { NestedBindingExecution::Lazy => ty, - NestedBindingExecution::Eager => ty.promote(db), + NestedBindingExecution::Eager => ty.promote(db, env), }; self.bindings.insert(definition, ty); } fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) { + let db = self.db(); let ast::StmtMatch { range: _, node_index: _, @@ -2528,7 +2582,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(guard) = guard.as_deref() { let guard_ty = self.infer_standalone_expression(guard, TypeContext::default()); - if let Err(err) = guard_ty.try_bool(self.db()) { + if let Err(err) = guard_ty.try_bool(db, self.program_environment()) { err.report_diagnostic(&self.context, guard); } } @@ -2550,6 +2604,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn validate_class_pattern(&mut self, pattern: &ast::PatternMatchClass, cls_ty: Type<'db>) { + let db = self.db(); + let env = self.program_environment(); if let Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) = cls_ty { if let Some(first_excess_pattern) = pattern.arguments.patterns.first() { report_too_many_positional_patterns_for_class_pattern( @@ -2581,7 +2637,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let positional_patterns = &pattern.arguments.patterns; if let [first_positional_pattern, ..] = positional_patterns.as_slice() - && let Some(result) = class_pattern_positional_result(self.db(), class) + && let Some(result) = class_pattern_positional_result(db, env, class) { match result { ClassPatternPositionalResult::Limit(limit) => { @@ -2591,7 +2647,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { first_excess_pattern, limit, positional_patterns.len(), - cls_ty.display(self.db()), + cls_ty.display(db, env), ); } } @@ -2605,7 +2661,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } - } else if !cls_ty.is_assignable_to(self.db(), KnownClass::Type.to_instance(self.db())) { + } else if !cls_ty.is_assignable_to(db, env, KnownClass::Type.to_instance(db, env)) { report_invalid_class_match_pattern(&self.context, &*pattern.cls, cls_ty); } } @@ -2799,12 +2855,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Returns `true` if `property_ty` is a property whose deleter returns `Never`/`NoReturn` /// when called for deletion on `object_ty`. fn property_deleter_returns_never(&self, property_ty: Type<'db>, object_ty: Type<'db>) -> bool { + let env = self.program_environment(); let db = self.db(); property_ty.as_property_instance().is_some_and(|property| { property.deleter(db).is_some_and(|deleter| { - match deleter.try_call(db, &CallArguments::positional([object_ty])) { - Ok(result) => result.return_type(db).is_never(), - Err(err) => err.return_type(db).is_never(), + match deleter.try_call(db, env, &CallArguments::positional([object_ty])) { + Ok(result) => result.return_type(db, env).is_never(), + Err(err) => err.return_type(db, env).is_never(), } }) }) @@ -2817,6 +2874,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { attribute: &str, emit_diagnostics: bool, ) -> bool { + let env = self.program_environment(); let db = self.db(); match object_ty { @@ -2850,7 +2908,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::EnumComplement(complement) => self.validate_attribute_deletion( target, - complement.remaining_literal_union(db), + complement.remaining_literal_union(db, env), attribute, emit_diagnostics, ), @@ -2890,7 +2948,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::TypedDict(_) | Type::NewTypeInstance(_) => { let frozen_dataclass_dispatch = object_ty - .nominal_class(db) + .nominal_class(db, env) .and_then(|class| class.static_class_literal(db)) .and_then(|(class, specialization)| { class.inherited_frozen_dataclass_dispatch( @@ -2902,7 +2960,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }); let delattr_receiver = frozen_dataclass_dispatch - .map_or(object_ty, |dispatch| dispatch.receiver(db, object_ty)); + .map_or(object_ty, |dispatch| dispatch.receiver(db, env, object_ty)); let mut delattr_arguments = CallArguments::positional([Type::string_literal(db, attribute)]); @@ -2911,6 +2969,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match delattr_receiver .member_lookup_with_policy( db, + env, "__delattr__", MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ) @@ -2921,7 +2980,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definedness, provenance, .. - }) => match delattr.try_call(db, &delattr_arguments) { + }) => match delattr.try_call(db, env, &delattr_arguments) { Ok(bindings) if definedness == Definedness::PossiblyUndefined => { Err(CallDunderError::PossiblyUnbound { bindings: Box::new(bindings), @@ -2938,6 +2997,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { delattr_receiver.try_call_dunder_with_policy( db, + env, "__delattr__", &mut delattr_arguments, TypeContext::default(), @@ -2949,8 +3009,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { frozen_dataclass_dispatch, Some(FrozenDataclassDispatch::FrozenField) ) || match &delattr_dunder_call_result { - Ok(result) => result.return_type(db).is_never(), - Err(err) => err.return_type(db).is_some_and(|ty| ty.is_never()), + Ok(result) => result.return_type(db, env).is_never(), + Err(err) => err.return_type(db, env).is_some_and(|ty| ty.is_never()), }; if returns_never { if emit_diagnostics @@ -2959,7 +3019,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "Cannot delete attribute `{attribute}` on type `{}` \ whose `__delattr__` method returns `Never`/`NoReturn`", - object_ty.display(db), + object_ty.display(db, env), )); } return false; @@ -3015,12 +3075,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .. }), .. - }) = assignment_attribute_members(db, object_ty, attribute) + }) = assignment_attribute_members(db, env, object_ty, attribute) .and_then(AssignmentAttributeMembers::type_member) { - let attr_ty = attr_ty.bind_self_typevars(db, object_ty); + let attr_ty = attr_ty.bind_self_typevars(db, env, object_ty); let delete_dunder_call_result = attr_ty.try_call_dunder( db, + env, "__delete__", CallArguments::positional([object_ty]), TypeContext::default(), @@ -3030,8 +3091,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // value to mutate; it is not a concrete descriptor with a terminal deleter. let deleter_returns_never = !attr_ty.is_never() && match &delete_dunder_call_result { - Ok(bindings) => bindings.return_type(db).is_never(), - Err(error) => error.return_type(db).is_some_and(|ty| ty.is_never()), + Ok(bindings) => bindings.return_type(db, env).is_never(), + Err(error) => { + error.return_type(db, env).is_some_and(|ty| ty.is_never()) + } }; if deleter_returns_never || self.property_deleter_returns_never(attr_ty, object_ty) @@ -3043,7 +3106,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "Cannot delete attribute `{attribute}` on type `{}` \ whose `__delete__` method returns `Never`/`NoReturn`", - object_ty.display(db), + object_ty.display(db, env), )); } return false; @@ -3086,6 +3149,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value: &ast::Expr, infer_assigned_ty: Option<&dyn Fn(&mut Self, TypeContext<'db>) -> Type<'db>>, ) { + let db = self.db(); match target { ast::Expr::Name(name) => { if let Some(infer_assigned_ty) = infer_assigned_ty { @@ -3104,8 +3168,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { let assigned_ty = infer_assigned_ty.map(|f| f(self, TypeContext::default())); - if let Some(tuple_spec) = - assigned_ty.and_then(|ty| ty.tuple_instance_spec(self.db())) + if let Some(tuple_spec) = assigned_ty + .and_then(|ty| ty.tuple_instance_spec(db, self.program_environment())) { let assigned_tys = tuple_spec.iter_element_types(self.db()).collect::>(); @@ -3349,7 +3413,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if let Some(special_form) = target.as_name_expr().and_then(|name| { - SpecialFormType::try_from_file_and_name(self.db(), self.file(), &name.id) + SpecialFormType::try_from_file_and_name(self.db(), self.python_file(), &name.id) }) { target_ty = Type::SpecialForm(special_form); } @@ -3533,6 +3597,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_assignment_deferred(&mut self, target: &ast::Expr, value: &'ast ast::Expr) { + let db = self.db(); + let env = self.program_environment(); // Infer deferred bounds/constraints/defaults of a legacy TypeVar / ParamSpec / NewType, // and field types for functional TypedDict. let ast::Expr::Call(ast::ExprCall { @@ -3584,7 +3650,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let constraint = self.infer_type_expression(arg); constraint_tys.push(constraint); - if constraint.has_typevar_or_typevar_instance(self.db()) + if constraint.has_typevar_or_typevar_instance(db, env) && let Some(builder) = self .context .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, arg) @@ -3603,7 +3669,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let bound_type = self.infer_type_expression(&bound.value); bound_or_constraints = Some(TypeVarBoundOrConstraints::UpperBound(bound_type)); - if bound_type.has_typevar_or_typevar_instance(self.db()) + if bound_type.has_typevar_or_typevar_instance(db, env) && let Some(builder) = self .context .report_lint(&INVALID_TYPE_VARIABLE_BOUND, bound) @@ -3653,9 +3719,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Infer the deferred base type of a NewType. fn infer_newtype_assignment_deferred(&mut self, arguments: &ast::Arguments) { + let db = self.db(); + let env = self.program_environment(); let inferred = self.infer_type_expression(&arguments.args[1]); - if inferred.has_typevar_or_typevar_instance(self.db()) { + if inferred.has_typevar_or_typevar_instance(db, env) { if let Some(builder) = self .context .report_lint(&INVALID_NEWTYPE, &arguments.args[1]) @@ -3687,7 +3755,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_NEWTYPE, &arguments.args[1]) { let mut diag = builder.into_diagnostic("invalid base for `typing.NewType`"); - diag.set_primary_annotation_message(format!("type `{}`", inferred.display(self.db()))); + diag.set_primary_annotation_message(format!("type `{}`", inferred.display(db, env))); if matches!(inferred, Type::ProtocolInstance(_)) { diag.info("The base of a `NewType` is not allowed to be a protocol class."); } else if matches!(inferred, Type::TypedDict(_)) { @@ -3788,6 +3856,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definition: Definition<'db>, arguments: &ast::Arguments, ) { + let db = self.db(); // Match the binding context used by eager assignment inference so legacy type variables // in the alias value are bound to the alias definition. let previous_context = self.typevar_binding_context.replace(definition); @@ -3824,7 +3893,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for element in &tuple.elts { let bound_typevar = match self.expression_type(element) { Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => bind_typevar( - db, + self.db(), self.index, definition.file_scope(db), Some(definition), @@ -3870,7 +3939,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - if typevar.default_type(db).is_some() { + if typevar + .default_type(db, self.program_environment()) + .is_some() + { if let Some(typevar_tuple) = typevar_tuple { valid_type_params = false; if let Some(builder) = self @@ -3920,7 +3992,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if valid_type_params { let mut value_typevars = FxOrderSet::default(); - value_ty.find_legacy_typevars(self.db(), Some(definition), &mut value_typevars); + value_ty.find_legacy_typevars( + db, + self.program_environment(), + Some(definition), + &mut value_typevars, + ); for typevar in value_typevars { if !type_params.contains(&typevar.identity(self.db())) @@ -3946,6 +4023,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_annotated_assignment_statement(&mut self, assignment: &ast::StmtAnnAssign) { + let db = self.db(); + let env = self.program_environment(); if assignment.target.is_name_expr() { self.infer_definition(assignment); } else { @@ -4103,7 +4182,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // type, report an error and fall back to the annotated type. let target_ty = if let Some(value_ty) = value_ty { let declared_ty = annotated.inner_type(); - if value_ty.is_assignable_to(self.db(), declared_ty) { + if value_ty.is_assignable_to(db, env, declared_ty) { value_ty } else { if let Some(builder) = self @@ -4112,8 +4191,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut diag = builder.into_diagnostic(format_args!( "Object of type `{}` is not assignable to `{}`", - value_ty.display(self.db()), - declared_ty.display(self.db()), + value_ty.display(db, env), + declared_ty.display(db, env), )); diag.annotate( self.context @@ -4122,7 +4201,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); diag.set_primary_annotation_message(format_args!( "Incompatible value of type `{}`", - value_ty.display(self.db()), + value_ty.display(db, env), )); } declared_ty @@ -4140,6 +4219,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { assignment: &'db AnnotatedAssignmentDefinitionKind, definition: Definition<'db>, ) { + let db = self.db(); + let env = self.program_environment(); let target = assignment.target(self.module()); let value = assignment.value(self.module()); @@ -4276,8 +4357,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } - let nearest_enclosing_class = - nearest_enclosing_class(self.db(), self.index, self.scope()); + let nearest_enclosing_class = nearest_enclosing_class(db, self.index, self.scope()); let class_kind = nearest_enclosing_class.and_then(|class| { CodeGeneratorKind::from_class(self.db(), ClassLiteral::Static(class)) }); @@ -4348,10 +4428,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .as_name_expr() .is_some_and(|name| &name.id == "TYPE_CHECKING") { - if !KnownClass::Bool - .to_instance(self.db()) - .is_assignable_to(self.db(), declared.inner_type()) - { + if !KnownClass::Bool.to_instance(db, env).is_assignable_to( + db, + env, + declared.inner_type(), + ) { // annotation not assignable from `bool` is an error report_invalid_type_checking_constant(&self.context, target.into()); } else if self.in_stub() @@ -4374,8 +4455,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Handle various singletons. if let Some(name_expr) = target.as_name_expr() - && let Some(special_form) = - SpecialFormType::try_from_file_and_name(self.db(), self.file(), &name_expr.id) + && let Some(special_form) = SpecialFormType::try_from_file_and_name( + self.db(), + self.python_file(), + &name_expr.id, + ) { declared.inner = Type::SpecialForm(special_form); } @@ -4416,7 +4500,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::unknown() } Type::KnownInstance(KnownInstanceType::LiteralStringAlias(ty)) - if ty.inner(self.db()).contains_self(self.db()) => + if ty.inner(self.db()).contains_self(db, env) => { Type::KnownInstance(KnownInstanceType::LiteralStringAlias( InternedType::new(self.db(), Type::unknown()), @@ -4477,18 +4561,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && matches!(declared.inner_type(), Type::Dynamic(DynamicType::Unknown))) // Value type would be an enum member at runtime (exclude callables, // which are never members) - && !inferred_ty.is_subtype_of( - self.db(), - Type::Callable(CallableType::unknown(self.db())) - .top_materialization(self.db()), + && !inferred_ty.is_subtype_of(db, env, Type::Callable(CallableType::unknown(self.db())) + .top_materialization(db, env), ) { let current_scope_id = self.scope().file_scope_id(self.db()); let current_scope = self.index.scope(current_scope_id); if current_scope.kind() == ScopeKind::Class - && let Some(class) = - nearest_enclosing_class(self.db(), self.index, self.scope()) - && is_enum_class_by_inheritance(self.db(), class) + && let Some(class) = nearest_enclosing_class(db, self.index, self.scope()) + && is_enum_class_by_inheritance(db, env, class) && !enum_ignored_names(self.db(), self.scope()).contains(&name_expr.id) && let Some(builder) = self .context @@ -4560,10 +4641,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_expr: &ast::Expr, infer_value_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); // If the target defines, e.g., `__iadd__`, infer the augmented assignment as a call to that // dunder. let op = assignment.op; - let db = self.db(); // Fall back to non-augmented binary operator inference. let binary_return_ty = |builder: &mut Self, value_ty| { @@ -4588,7 +4670,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // equally applicable type contexts for each union member. infer_value_ty.infer_loud(self, TypeContext::default()); - union.map(db, |&elem_type| { + union.map(db, env, |&elem_type| { self.infer_augmented_op( assignment, elem_type, @@ -4614,7 +4696,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut call_arguments = CallArguments::positional([Type::unknown()]); let call = self.infer_and_try_call_dunder( - db, target_type, op.in_place_dunder(), MemberLookupPolicy::NO_INSTANCE_FALLBACK, @@ -4623,9 +4704,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut |builder, (_, _, tcx)| infer_value_ty(builder, tcx), TypeContext::default(), ); - match call { - Ok(outcome) => outcome.return_type(db), + Ok(outcome) => outcome.return_type(db, env), Err(CallDunderError::MethodNotAvailable) => { let value_ty = infer_value_ty(self, TypeContext::default()); binary_return_ty(self, value_ty) @@ -4636,7 +4716,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let value_ty = outcome.type_for_argument(&call_arguments, 0); UnionType::from_two_elements( db, - outcome.return_type(db), + env, + outcome.return_type(db, env), binary_return_ty(self, value_ty), ) } @@ -4648,7 +4729,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { target_type, value_ty, ); - bindings.return_type(db) + bindings.return_type(db, env) } } } @@ -4720,20 +4801,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { iterable: &ast::Expr, expression_type: impl FnMut(&ast::Expr) -> Type<'db>, ) -> Option> { + let db = self.db(); + let env = self.program_environment(); let element_types = - extract_fixed_length_iterable_element_types(self.db(), iterable, expression_type)?; + extract_fixed_length_iterable_element_types(db, env, iterable, expression_type)?; if element_types.is_empty() { None } else { Some(UnionType::from_elements( - self.db(), + db, + env, element_types.iter().copied(), )) } } fn infer_for_statement(&mut self, for_statement: &ast::StmtFor) { + let db = self.db(); let ast::StmtFor { range: _, node_index: _, @@ -4755,9 +4840,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { element_type } else { + let env = builder.program_environment(); iterable_type - .iterate(builder.db()) - .homogeneous_element_type(builder.db()) + .iterate(db, env) + .homogeneous_element_type(db, env) } }); @@ -4770,6 +4856,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for_stmt: &ForStmtDefinitionKind<'db>, definition: Definition<'db>, ) { + let db = self.db(); let iterable = for_stmt.iterable(self.module()); let target = for_stmt.target(self.module()); @@ -4794,15 +4881,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { element_type } else { + let env = self.program_environment(); iterable_type .try_iterate_with_mode( - self.db(), + db, + env, EvaluationMode::from_is_async(for_stmt.is_async()), ) - .map(|tuple| tuple.homogeneous_element_type(self.db())) + .map(|tuple| tuple.homogeneous_element_type(db, env)) .unwrap_or_else(|err| { err.report_diagnostic(&self.context, iterable_type, iterable.into()); - err.fallback_element_type(self.db()) + err.fallback_element_type(db, env) }) } } @@ -4814,6 +4903,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_while_statement(&mut self, while_statement: &ast::StmtWhile) { + let db = self.db(); let ast::StmtWhile { range: _, node_index: _, @@ -4824,7 +4914,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let test_ty = self.infer_standalone_expression(test, TypeContext::default()); - if let Err(err) = test_ty.try_bool(self.db()) { + if let Err(err) = test_ty.try_bool(db, self.program_environment()) { err.report_diagnostic(&self.context, &**test); } @@ -4833,6 +4923,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_assert_statement(&mut self, assert: &ast::StmtAssert) { + let db = self.db(); let ast::StmtAssert { range: _, node_index: _, @@ -4842,7 +4933,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let test_ty = self.infer_standalone_expression(test, TypeContext::default()); - if let Err(err) = test_ty.try_bool(self.db()) { + if let Err(err) = test_ty.try_bool(db, self.program_environment()) { err.report_diagnostic(&self.context, &**test); } @@ -4850,6 +4941,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_raise_statement(&mut self, raise: &ast::StmtRaise) { + let db = self.db(); let ast::StmtRaise { range: _, node_index: _, @@ -4857,18 +4949,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { cause, } = raise; - let base_exception_type = KnownClass::BaseException.to_subclass_of(self.db()); - let base_exception_instance = KnownClass::BaseException.to_instance(self.db()); + let env = self.program_environment(); + let base_exception_type = KnownClass::BaseException.to_subclass_of(db, env); + let base_exception_instance = KnownClass::BaseException.to_instance(db, env); let can_be_raised = - UnionType::from_two_elements(self.db(), base_exception_type, base_exception_instance); + UnionType::from_two_elements(db, env, base_exception_type, base_exception_instance); let can_be_exception_cause = - UnionType::from_two_elements(self.db(), can_be_raised, Type::none(self.db())); + UnionType::from_two_elements(db, env, can_be_raised, Type::none(db, env)); if let Some(raised) = exc { let raised_type = self.infer_expression(raised, TypeContext::default()); - if !raised_type.is_assignable_to(self.db(), can_be_raised) { + if !raised_type.is_assignable_to(db, env, can_be_raised) { report_invalid_exception_raised(&self.context, raised, raised_type); } } @@ -4876,22 +4969,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(cause) = cause { let cause_type = self.infer_expression(cause, TypeContext::default()); - if !cause_type.is_assignable_to(self.db(), can_be_exception_cause) { + if !cause_type.is_assignable_to(db, env, can_be_exception_cause) { report_invalid_exception_cause(&self.context, cause, cause_type); } } } fn infer_return_statement(&mut self, ret: &ast::StmtReturn) { + let db = self.db(); + let env = self.program_environment(); let tcx = if ret.value.is_some() { - nearest_enclosing_function(self.db(), self.index, self.scope()) + nearest_enclosing_function(db, self.index, self.scope()) .map(|func| { // When inferring expressions within a function body, // the expected type passed should be the "raw" type, // i.e. type variables in the return type are non-inferable, // and the return types of async functions are not wrapped in `CoroutineType[...]`. let return_ty = same_module_uncached_raw_signature( - self.db(), + db, func, ReturnCallableTypeVarScope::Lexical, ) @@ -4903,7 +4998,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let file_scope_id = self.scope().file_scope_id(self.db()); let context_ty = if file_scope_id.is_generator_function(self.index) { return_ty - .generator_return_type(self.db()) + .generator_return_type(db, env) .unwrap_or(return_ty) } else { return_ty @@ -4922,7 +5017,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map_or(ret.range(), |value| value.range()); self.record_return_type(ty, range); } else { - self.record_return_type(Type::none(self.db()), ret.range()); + self.record_return_type(Type::none(db, env), ret.range()); } } @@ -4968,7 +5063,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } } - if !module_type_implicit_global_symbol(self.db(), self.file(), name) + if !module_type_implicit_global_symbol(self.db(), self.python_file(), name) .place .is_undefined() { @@ -4993,8 +5088,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn module_type_from_name(&self, module_name: &ModuleName) -> Option> { - resolve_module(self.db(), self.file(), module_name) - .map(|module| Type::module_literal(self.db(), self.file(), module)) + resolve_module(self.db(), self.python_file(), module_name) + .map(|module| Type::module_literal(self.db(), self.python_file(), module)) } fn infer_decorator(&mut self, decorator: &ast::Decorator) -> Type<'db> { @@ -5015,6 +5110,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { call_expression: &ast::ExprCall, return_ty: Type<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let arguments = &call_expression.arguments; let [decorated_expression] = &arguments.args[..] else { return return_ty; @@ -5026,12 +5123,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let decorated_ty = self.get_or_infer_expression(decorated_expression, TypeContext::default()); let call_arguments = CallArguments::positional([decorated_ty]); - let Ok(bindings) = decorator_ty.try_call(self.db(), &call_arguments) else { + let Ok(bindings) = decorator_ty.try_call(db, env, &call_arguments) else { return return_ty; }; - transparent_callable_decorator_result(self.db(), &bindings, decorated_ty) - .unwrap_or(return_ty) + transparent_callable_decorator_result(db, env, &bindings, decorated_ty).unwrap_or(return_ty) } /// Apply a decorator to a function or class type and return the resulting type. @@ -5046,6 +5142,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) -> Type<'db> { fn propagate_callable_kind<'d>( db: &'d dyn Db, + env: &ProgramEnvironment<'d>, ty: Type<'d>, kind: CallableTypeKind, provenance: CallableFunctionProvenance, @@ -5057,11 +5154,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { kind, provenance, ))), - Type::Union(union) => union.try_map(db, |element| { - propagate_callable_kind(db, *element, kind, provenance) + Type::Union(union) => union.try_map(db, env, |element| { + propagate_callable_kind(db, env, *element, kind, provenance) }), Type::TypeAlias(alias) => { - propagate_callable_kind(db, alias.value_type(db), kind, provenance) + propagate_callable_kind(db, env, alias.value_type(db), kind, provenance) } // Intersections are currently not handled here because that would require // the decorator to be explicitly annotated as returning an intersection. @@ -5097,7 +5194,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | Type::NewTypeInstance(_) => None, } } + let db = self.db(); + let env = self.program_environment(); // For FunctionLiteral, get the kind directly without computing the full signature. // This avoids a query cycle when the function has default parameter values, since // computing the signature requires evaluating those defaults which may trigger @@ -5110,7 +5209,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ), )), _ => decorated_ty - .try_upcast_to_callable(self.db()) + .try_upcast_to_callable(db, env) .and_then(CallableTypes::exactly_one) .and_then(|callable| match callable.kind(self.db()) { kind @ (CallableTypeKind::FunctionLike @@ -5123,20 +5222,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let call_arguments = CallArguments::positional([decorated_ty]); - let (return_ty, decorator_bindings) = - match decorator_ty.try_call(self.db(), &call_arguments) { - Ok(bindings) => (bindings.return_type(self.db()), Some(bindings)), - Err(CallError(_, bindings)) => { - bindings.report_diagnostics(&self.context, decorator_node.into()); - (bindings.return_type(self.db()), None) - } - }; + let (return_ty, decorator_bindings) = match decorator_ty.try_call(db, env, &call_arguments) + { + Ok(bindings) => (bindings.return_type(db, env), Some(bindings)), + Err(CallError(_, bindings)) => { + bindings.report_diagnostics(&self.context, decorator_node.into()); + (bindings.return_type(db, env), None) + } + }; // TODO: Remove this special case once the new constraint solver can preserve // per-overload ParamSpec/return correlations for transparent callable decorators. if let Some(decorator_bindings) = decorator_bindings.as_ref() && let Some(result) = - transparent_callable_decorator_result(self.db(), decorator_bindings, decorated_ty) + transparent_callable_decorator_result(db, env, decorator_bindings, decorated_ty) { return result; } @@ -5148,7 +5247,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // extended explanation. propagatable_kind .and_then(|(kind, provenance)| { - propagate_callable_kind(self.db(), return_ty, kind, provenance) + propagate_callable_kind(db, env, return_ty, kind, provenance) }) .unwrap_or(return_ty) } @@ -5156,7 +5255,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { #[expect(clippy::too_many_arguments)] fn infer_and_try_call_dunder( &mut self, - db: &'db dyn Db, object: Type<'db>, name: &str, lookup_policy: MemberLookupPolicy, @@ -5165,8 +5263,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { infer_argument_ty: &mut dyn FnMut(&mut Self, ArgExpr<'db, '_>) -> Type<'db>, call_expression_tcx: TypeContext<'db>, ) -> Result, CallDunderError<'db>> { + let db = self.db(); + let env = self.program_environment(); match object - .member_lookup_with_policy(db, name, lookup_policy) + .member_lookup_with_policy(db, env, name, lookup_policy) .place { Place::Defined(DefinedPlace { @@ -5175,9 +5275,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { provenance, .. }) => { - let mut bindings = self - .bindings_for_call(dunder_callable) - .match_parameters(db, argument_types); + let mut bindings = self.bindings_for_call(dunder_callable).match_parameters( + db, + env, + argument_types, + ); if let Err(call_error) = self.infer_and_check_argument_types( ast_arguments, @@ -5216,6 +5318,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let constraints = ConstraintSetBuilder::new(); let initial_argument_types = argument_types.clone(); + let env = self.program_environment(); // Keep track of which arguments match generic parameters. let mut generic_arguments = SmallVec::<[bool; 8]>::with_capacity(argument_types.len()); @@ -5229,7 +5332,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // typevar occurrences across all overload candidates. Note that the set of overload candidates // stays stable across all iterations. bindings.visit_type_context_callables(&mut |binding| { - let candidate_overload_indices = binding.candidate_overload_indices(db, argument_types); + let candidate_overload_indices = + binding.candidate_overload_indices(db, env, argument_types); has_generic_context |= candidate_overload_indices.iter().any(|&overload_index| { binding.overloads()[overload_index] @@ -5250,8 +5354,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } - let typevar_occurrences = - overload.typevar_occurrences_for_parameter(db, binding, argument_index); + let typevar_occurrences = overload.typevar_occurrences_for_parameter( + db, + env, + binding, + argument_index, + ); *is_generic |= typevar_occurrences > 0; overload_typevar_occurrences += typevar_occurrences; } @@ -5279,7 +5387,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If the type context is a union, attempt to narrow to a specific element. let narrow_targets = call_expression_tcx - .narrow_targets(db) + .narrow_targets(db, env) // We only need to attempt narrowing on generic calls, otherwise the type // context has no effect. .filter(|_| has_generic_context) @@ -5296,8 +5404,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { !overload .return_ty - .when_assignable_to(db, narrowed_ty, &constraints, inferable) - .is_never_satisfied(db) + .when_assignable_to(db, env, narrowed_ty, &constraints, inferable) + .is_never_satisfied(db, env) }) { return None; } @@ -5348,8 +5456,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // cases where the constraint solver is not smart enough to solve complex unions. // We should see revisit this after the new constraint solver is implemented. if !speculative_bindings - .return_type(db) - .is_assignable_to(db, narrowed_ty) + .return_type(db, env) + .is_assignable_to(db, env, narrowed_ty) { return None; } @@ -5369,10 +5477,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for narrowed_ty in std::iter::chain( narrow_targets .iter() - .filter(|ty| ty.may_prefer_declared_type(db)), + .filter(|ty| ty.may_prefer_declared_type(db, env)), narrow_targets .iter() - .filter(|ty| !ty.may_prefer_declared_type(db)), + .filter(|ty| !ty.may_prefer_declared_type(db, env)), ) { if let Some(result) = try_narrow(*narrowed_ty) { if teardown_expression_cache { @@ -5433,6 +5541,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { call_expression_tcx: TypeContext<'db>, candidates: &OverloadSet, ) -> Result<(), CallErrorKind> { + let db = self.db(); + let env = self.program_environment(); let requires_overload_evaluation = requires_overload_evaluation(candidates); let arguments_tcx = self.collect_call_arguments_type_context( baseline_argument_types, @@ -5454,7 +5564,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); return bindings.check_types_impl( - self.db(), + db, + env, constraints, argument_types, call_expression_tcx, @@ -5477,7 +5588,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); let result = bindings.check_types_impl( - self.db(), + db, + env, constraints, argument_types, call_expression_tcx, @@ -5572,6 +5684,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { next_bindings = bindings.clone(); let _ = next_bindings.check_types_impl( db, + self.program_environment(), constraints, &next_argument_types, call_expression_tcx, @@ -5612,6 +5725,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Discard any non-matching constructors overloads now that the inferred types have converged. let result = next_bindings.finalize_argument_inference( db, + self.program_environment(), &converged_argument_types, &self.dataclass_field_specifiers, ); @@ -5664,6 +5778,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn add_overloads_from_binding<'a, 'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overloads_with_binding: &mut OverloadsWithBinding<'a, 'db>, binding: &'a CallableBinding<'db>, constraints: &ConstraintSetBuilder<'db>, @@ -5674,6 +5789,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { overloads_with_binding.extend(matching_overloads.map(|(_, overload)| { let specialization = overload.argument_type_context_specialization( db, + env, constraints, call_expression_tcx, ); @@ -5683,6 +5799,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else if let Some(overload) = binding.best_failing_overload() { let specialization = overload.argument_type_context_specialization( db, + env, constraints, call_expression_tcx, ); @@ -5692,15 +5809,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { overloads_with_binding.push((overload, binding, specialization)); } } - let db = self.db(); + let env = self.program_environment(); + // Collect the set of candidate overloads and bindings. let mut overloads_with_binding: OverloadsWithBinding = Vec::new(); if let Some(candidates) = candidates { bindings.visit_overload_set(candidates, &mut |overload, binding| { let specialization = overload.argument_type_context_specialization( db, + env, constraints, call_expression_tcx, ); @@ -5711,6 +5830,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { bindings.visit_type_context_callables(&mut |binding| { add_overloads_from_binding( db, + env, &mut overloads_with_binding, binding, constraints, @@ -5730,6 +5850,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { |overload: &Binding<'db>, binding: &CallableBinding<'db>, specialization| { overload.argument_type_context( db, + env, constraints, binding, argument_types, @@ -6093,11 +6214,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { expression: &ast::Expr, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); let ty = match expression { ast::Expr::NoneLiteral(ast::ExprNoneLiteral { range: _, node_index: _, - }) => Type::none(self.db()), + }) => Type::none(db, self.program_environment()), ast::Expr::NumberLiteral(literal) => self.infer_number_literal_expression(literal), ast::Expr::BooleanLiteral(literal) => self.infer_boolean_literal_expression(literal), ast::Expr::StringLiteral(literal) => self.infer_string_literal_expression(literal, tcx), @@ -6166,13 +6288,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { mut ty: Type<'db>, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); // Avoid promoting explicitly annotated literal values. if let Type::LiteralValue(literal) = ty && let Some(tcx) = tcx.annotation && let literal_tcx @ (Type::Union(_) | Type::LiteralValue(_)) = tcx - .resolve_type_alias(self.db()) - .filter_union(self.db(), |ty| ty.as_literal_value().is_some()) - && ty.is_assignable_to(self.db(), literal_tcx) + .resolve_type_alias(db) + .filter_union(db, |ty| ty.as_literal_value().is_some()) + && ty.is_assignable_to(db, env, literal_tcx) { ty = Type::LiteralValue(literal.to_unpromotable()); } @@ -6195,6 +6319,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// /// This lets `list` in a `Callable[[], list[str]]` context be treated as `list[str]`. fn specialize_generic_class_from_context(&self, ty: Type<'db>, target: Type<'db>) -> Type<'db> { + let env = self.program_environment(); // TODO: The constraint-set assignability rules should already be // able to determine that `list` (coerced into a callable) is assignable // to `Callable[[], list[str]]` when `_T@list = str`. However, when @@ -6248,7 +6373,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let Some(class_generic_context) = class.generic_context(db) else { return ty; }; - let Some(source_callable) = ty.try_upcast_to_callable(db) else { + let Some(source_callable) = ty.try_upcast_to_callable(db, env) else { return ty; }; // The callable relation existentially solves variables bound by each signature. Keep @@ -6267,7 +6392,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { variables .peek() .is_some() - .then(|| GenericContext::from_typevar_instances(db, variables)) + .then(|| GenericContext::from_typevar_instances(db, env, variables)) }); Signature::new_generic( signature_generic_context, @@ -6282,9 +6407,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let inferable = class_generic_context.inferable_typevars(db); let constraints = ConstraintSetBuilder::new(); let path_bounds = source_callable - .into_type(db) - .assignable_solutions_with_inferable(db, Type::Callable(target_callable), inferable); - let Solutions::Constrained(solutions) = path_bounds.solve(db, &constraints) else { + .into_type(db, env) + .assignable_solutions_with_inferable( + db, + env, + Type::Callable(target_callable), + inferable, + ); + let Solutions::Constrained(solutions) = path_bounds.solve(db, env, &constraints) else { return ty; }; @@ -6294,14 +6424,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for binding in solution { let inferred_ty = binding .solution - .filter_union(db, |ty| !ty.has_unspecialized_type_var(db)); - if inferred_ty.has_unspecialized_type_var(db) { + .filter_union(db, |ty| !ty.has_unspecialized_type_var(db, env)); + if inferred_ty.has_unspecialized_type_var(db, env) { continue; } type_context_mappings .entry(binding.bound_typevar.identity(db)) - .and_modify(|existing| existing.add(db, inferred_ty)) + .and_modify(|existing| existing.add(db, env, inferred_ty)) .or_insert_with(|| UnionAccumulator::new(inferred_ty)); } } @@ -6313,7 +6443,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let type_context_mappings: FxHashMap, Type<'db>> = type_context_mappings .into_iter() - .map(|(identity, accumulator)| (identity, accumulator.into_type(db))) + .map(|(identity, accumulator)| (identity, accumulator.into_type(db, env))) .collect(); let specialized = Type::from(class.apply_specialization(db, |generic_context| { generic_context.specialize_recursive( @@ -6323,7 +6453,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(|typevar| type_context_mappings.get(&typevar.identity(db)).copied()), ) })); - if specialized.is_assignable_to(db, Type::Callable(target_callable)) { + if specialized.is_assignable_to(db, env, Type::Callable(target_callable)) { specialized } else { ty @@ -6379,12 +6509,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn union_expected_types(&mut self, expected_types: &FxHashMap>) { + let db = self.db(); + let env = self.program_environment(); // Non-empty only if the producing inference collected, i.e. the file is open if expected_types.is_empty() { return; } - let db = self.db(); #[expect( clippy::iter_over_hash_type, reason = "expected types for distinct expressions are unioned independently" @@ -6392,26 +6523,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for (expression, ty) in expected_types { self.expected_types .entry(*expression) - .and_modify(|existing| *existing = UnionType::from_two_elements(db, *existing, *ty)) + .and_modify(|existing| { + *existing = UnionType::from_two_elements(db, env, *existing, *ty); + }) .or_insert(*ty); } } fn infer_number_literal_expression(&self, literal: &ast::ExprNumberLiteral) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprNumberLiteral { range: _, node_index: _, value, } = literal; - let db = self.db(); - match value { ast::Number::Int(n) => n .as_i64() .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), - ast::Number::Float(_) => KnownClass::Float.to_instance(db), - ast::Number::Complex { .. } => KnownClass::Complex.to_instance(db), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), + ast::Number::Float(_) => KnownClass::Float.to_instance(db, env), + ast::Number::Complex { .. } => KnownClass::Complex.to_instance(db, env), } } @@ -6456,6 +6589,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_fstring_expression(&mut self, fstring: &ast::ExprFString) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprFString { range: _, node_index: _, @@ -6503,11 +6638,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { collector.add_non_literal_string_expression(); } else { - let str_ty = ty.str(self.db()); + let str_ty = ty.str(db, env); if let Some(literal) = str_ty.as_string_literal() { collector.push_str(literal.value(self.db())); - } else if str_ty - .is_subtype_of(self.db(), Type::literal_string()) + } else if str_ty.is_subtype_of(db, env, Type::literal_string()) { collector.add_literal_string_expression(); } else { @@ -6523,10 +6657,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } - collector.string_type(self.db()) + collector.string_type(&self.context) } fn infer_tstring_expression(&mut self, tstring: &ast::ExprTString) -> Type<'db> { + let db = self.db(); let ast::ExprTString { value, .. } = tstring; for tstring in value { for element in &tstring.elements { @@ -6550,14 +6685,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } - KnownClass::Template.to_instance(self.db()) + KnownClass::Template.to_instance(db, self.program_environment()) } fn infer_ellipsis_literal_expression( &mut self, _literal: &ast::ExprEllipsisLiteral, ) -> Type<'db> { - KnownClass::EllipsisType.to_instance(self.db()) + let db = self.db(); + KnownClass::EllipsisType.to_instance(db, self.program_environment()) } fn infer_tuple_expression( @@ -6569,7 +6705,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// we promote `Literal` types when inferring the elements of the tuple. /// This provides a huge speedup on files that have very large unannotated tuple literals. const MAX_TUPLE_LENGTH_FOR_UNANNOTATED_LITERAL_INFERENCE: usize = 64; + let db = self.db(); + let env = self.program_environment(); let ast::ExprTuple { range: _, node_index: _, @@ -6581,13 +6719,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Remove any union elements of the annotation that are unrelated to the tuple type. let tcx = tcx.map(|annotation| { let inferable = KnownClass::Tuple - .try_to_class_literal(self.db()) - .and_then(|class| class.generic_context(self.db())) - .map(|generic_context| generic_context.inferable_typevars(self.db())) + .try_to_class_literal(db, env) + .and_then(|class| class.generic_context(db)) + .map(|generic_context| generic_context.inferable_typevars(db)) .unwrap_or(TypeVarSet::None); annotation.filter_disjoint_elements( - self.db(), - Type::homogeneous_tuple(self.db(), Type::unknown()), + db, + env, + Type::homogeneous_tuple(db, env, Type::unknown()), inferable, ) }); @@ -6595,7 +6734,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut is_homogeneous_tuple_annotation = false; let annotated_tuple = tcx - .known_specialization(self.db(), KnownClass::Tuple) + .known_specialization(db, env, KnownClass::Tuple) .and_then(|specialization| { let spec = specialization .tuple(self.db()) @@ -6609,7 +6748,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { is_homogeneous_tuple_annotation = true; } - spec.resize(self.db(), TupleLength::Fixed(elts.len())).ok() + spec.resize(db, env, TupleLength::Fixed(elts.len())).ok() }); // TODO: this is a simplification for now. @@ -6626,14 +6765,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .unwrap_or_default(); let mut annotated_elt_tys = annotated_elt_tys.into_iter(); - let db = self.db(); - let mut infer_element = |elt: &ast::Expr| { let annotated_elt_ty = annotated_elt_tys.by_ref().next(); - let ctx = if can_use_type_context { + let element_tcx = if can_use_type_context { let expected = if elt.is_starred_expr() { let expected_element = annotated_elt_ty.unwrap_or_else(Type::object); - Some(KnownClass::Iterable.to_specialized_instance(db, &[expected_element])) + Some(KnownClass::Iterable.to_specialized_instance(db, env, &[expected_element])) } else { annotated_elt_ty }; @@ -6644,9 +6781,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if tuple.len() > MAX_TUPLE_LENGTH_FOR_UNANNOTATED_LITERAL_INFERENCE { // Promote literals for very large unannotated tuples, // to avoid pathological performance issues - self.infer_expression(elt, ctx).promote(db) + self.infer_expression(elt, element_tcx).promote(db, env) } else { - self.infer_expression(elt, ctx) + self.infer_expression(elt, element_tcx) } }; @@ -6658,7 +6795,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Fine to use `iterate` rather than `try_iterate` here: // errors from iterating over something not iterable will have been // emitted in the `infer_element` call above. - let mut spec = element_type.iterate(db).into_owned(); + let mut spec = element_type.iterate(db, env).into_owned(); let known_length = match &*starred.value { ast::Expr::List(ast::ExprList { elts, .. }) @@ -6675,20 +6812,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(known_length) = known_length { spec = spec - .resize(db, TupleLength::Fixed(known_length)) + .resize(db, env, TupleLength::Fixed(known_length)) .unwrap_or(spec); } - builder = builder.concat(db, &spec); + builder = builder.concat(db, env, &spec); } else { builder.push(infer_element(element)); } } - Type::tuple(TupleType::new(db, &builder.build())) + Type::tuple(TupleType::new(db, env, &builder.build())) } fn infer_list_expression(&mut self, list: &ast::ExprList, tcx: TypeContext<'db>) -> Type<'db> { + let db = self.db(); let ast::ExprList { range: _, node_index: _, @@ -6707,10 +6845,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut infer_elt_ty, tcx, ) - .unwrap_or_else(|| KnownClass::List.to_specialized_instance(self.db(), &[Type::unknown()])) + .unwrap_or_else(|| { + KnownClass::List.to_specialized_instance( + db, + self.program_environment(), + &[Type::unknown()], + ) + }) } fn infer_set_expression(&mut self, set: &ast::ExprSet, tcx: TypeContext<'db>) -> Type<'db> { + let db = self.db(); let ast::ExprSet { range: _, node_index: _, @@ -6731,7 +6876,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut infer_elt_ty, tcx, ) - .unwrap_or_else(|| KnownClass::Set.to_specialized_instance(self.db(), &[Type::unknown()])) + .unwrap_or_else(|| { + KnownClass::Set.to_specialized_instance( + db, + self.program_environment(), + &[Type::unknown()], + ) + }) } /// Infers a set element, optionally with a fallback context for an incomplete `TypedDict` key. @@ -6745,6 +6896,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { elt_tcx: TypeContext<'db>, fallback_tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); let inference_tcx = if elt_tcx.annotation.is_some() { elt_tcx } else { @@ -6758,7 +6910,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let (Some(elt_ty), Some(fallback_ty)) = (elt_tcx.annotation, fallback_tcx.annotation) { self.store_expected_type( elt, - UnionType::from_two_elements(self.db(), elt_ty, fallback_ty), + UnionType::from_two_elements(db, self.program_environment(), elt_ty, fallback_ty), ); } @@ -6789,6 +6941,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_dict_expression(&mut self, dict: &ast::ExprDict, tcx: TypeContext<'db>) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprDict { range: _, node_index: _, @@ -6800,7 +6954,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Validate `TypedDict` dictionary literal assignments. if let Some(annotation) = tcx .annotation - .map(|annotation| annotation.resolve_type_alias(self.db())) + .map(|annotation| annotation.resolve_type_alias(db)) { if let Some(typed_dict) = annotation.as_typed_dict() { // If there is a single typed dict annotation, infer against it directly. @@ -6815,7 +6969,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut has_dict_compatible_fallback = false; for element in union_elements { - let element = element.resolve_type_alias(self.db()); + let element = element.resolve_type_alias(db); if let Some(typed_dict) = element.as_typed_dict() { typed_dicts.push(typed_dict); @@ -6825,7 +6979,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut speculative_builder = self.speculate_without_diagnostics(); has_dict_compatible_fallback = speculative_builder .infer_dict_expression(dict, TypeContext::new(Some(element))) - .is_assignable_to(self.db(), element); + .is_assignable_to(db, env, element); } } @@ -6874,7 +7028,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Successfully narrowed to a subset of typed dicts. if !narrowed_tys.is_empty() { - return UnionType::from_elements(self.db(), narrowed_tys); + return UnionType::from_elements(db, env, narrowed_tys); } } } @@ -6904,7 +7058,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { tcx, ) .unwrap_or_else(|| { - KnownClass::Dict.to_specialized_instance(self.db(), &[Type::unknown(), Type::unknown()]) + KnownClass::Dict.to_specialized_instance(db, env, &[Type::unknown(), Type::unknown()]) }) } @@ -6918,7 +7072,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { tcx: TypeContext<'db>, ) -> Option> { let db = self.db(); - + let env = self.program_environment(); let mut try_narrow = |narrowed_ty| { let mut speculative_builder = self.speculate(); @@ -6932,7 +7086,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )?; // Ensure the inferred return type is assignable to the narrowed declared type. - if !inferred_ty.is_assignable_to(db, narrowed_ty) { + if !inferred_ty.is_assignable_to(db, env, narrowed_ty) { return None; } @@ -6943,11 +7097,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If the type context is a union, attempt to narrow to a specific element. for narrowed_ty in tcx - .narrow_targets(db) + .narrow_targets(db, env) .as_deref() .into_iter() .flatten() - .filter(|ty| ty.class_specialization(db).is_some()) + .filter(|ty| ty.class_specialization(db, env).is_some()) { if let Some(result) = try_narrow(*narrowed_ty) { return Some(result); @@ -6972,11 +7126,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { infer_elt_expression: &mut dyn FnMut(&mut Self, ArgExpr<'db, 'expr>) -> Type<'db>, tcx: TypeContext<'db>, ) -> Option> { + let db = self.db(); + let env = self.program_environment(); + // Extract the type variable `T` from `list[T]` in typeshed. let elt_tys = |collection_class: KnownClass| { let collection_alias = collection_class - .try_to_class_literal(self.db())? - .identity_specialization(self.db()) + .try_to_class_literal(db, env)? + .identity_specialization(db) .into_generic_alias()?; let generic_context = collection_alias @@ -7001,17 +7158,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let constraints = ConstraintSetBuilder::new(); - let inferable = generic_context.inferable_typevars(self.db()); - let identity_instance = Type::instance(self.db(), ClassType::Generic(collection_alias)); - let mut builder = SpecializationBuilder::new(self.db(), &constraints, inferable); + let inferable = generic_context.inferable_typevars(db); + let identity_instance = Type::instance(db, env, ClassType::Generic(collection_alias)); + let mut builder = SpecializationBuilder::new(db, env, &constraints, inferable); // Remove any union elements of that are unrelated to the collection type. // // For example, we only want the `list[int]` from `annotation: list[int] | None` if // `collection_ty` is `list`. let tcx = tcx.map(|annotation| { - let collection_ty = collection_class.to_instance(self.db()); - annotation.filter_disjoint_elements(self.db(), collection_ty, inferable) + let collection_ty = collection_class.to_instance(db, env); + annotation.filter_disjoint_elements(db, env, collection_ty, inferable) }); // Collect type constraints from the declared element types. @@ -7027,15 +7184,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut elt_tcx_variance: FxHashMap, TypeVarVariance> = FxHashMap::default(); - if let Some(tcx) = tcx.annotation.map(|tcx| tcx.resolve_type_alias(self.db())) + if let Some(tcx) = tcx.annotation.map(|tcx| tcx.resolve_type_alias(db)) && matches!(tcx, Type::NominalInstance(_)) - && let Some(specialization) = tcx.known_specialization(self.db(), collection_class) + && let Some(specialization) = tcx.known_specialization(db, env, collection_class) && specialization.generic_context(self.db()) == generic_context && generic_context.variables(self.db()).all(|typevar| { !typevar.is_paramspec(self.db()) && typevar .typevar(self.db()) - .bound_or_constraints(self.db()) + .bound_or_constraints(db, env) .is_none() }) { @@ -7047,33 +7204,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .zip(specialization.types(self.db())) { let inferred_ty = inferred_ty - .filter_union(self.db(), |ty| { + .filter_union(db, |ty| { !ty.as_typevar() .is_some_and(|tv| tv.is_inferable(self.db(), inferable)) }) - .filter_union(self.db(), |ty| !ty.has_unspecialized_type_var(self.db())); - if inferred_ty.has_unspecialized_type_var(self.db()) { + .filter_union(db, |ty| !ty.has_unspecialized_type_var(db, env)); + if inferred_ty.has_unspecialized_type_var(db, env) { continue; } let identity = typevar.identity(self.db()); elt_tcx_constraints.insert(identity, UnionAccumulator::new(inferred_ty)); - elt_tcx_variance.insert(identity, typevar.variance(self.db())); + elt_tcx_variance.insert(identity, typevar.variance(db)); } } else if let Some(tcx) = tcx.annotation - && tcx.class_specialization(self.db()).is_some() + && tcx.class_specialization(db, env).is_some() { let db = self.db(); let path_bounds = - identity_instance.assignable_solutions_with_inferable(db, tcx, inferable); + identity_instance.assignable_solutions_with_inferable(db, env, tcx, inferable); let solutions = path_bounds.solve_with(|variance, path_bound| { let identity = path_bound.bound_typevar.identity(db); elt_tcx_variance .entry(identity) .and_modify(|current| *current = current.join(variance)) .or_insert(variance); - PathBounds::default_solve(db, &constraints, path_bound) + PathBounds::default_solve(db, env, &constraints, path_bound) }); match solutions { @@ -7102,15 +7259,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // type context from an outer generic call. If the type context is // a union, we try to keep any concrete elements. let inferred_ty = inferred_ty - .filter_union(db, |ty| !ty.has_unspecialized_type_var(db)); - if inferred_ty.has_unspecialized_type_var(db) { + .filter_union(db, |ty| !ty.has_unspecialized_type_var(db, env)); + if inferred_ty.has_unspecialized_type_var(db, env) { continue; } let identity = binding.bound_typevar.identity(db); elt_tcx_constraints .entry(identity) - .and_modify(|existing| existing.add(db, inferred_ty)) + .and_modify(|existing| { + existing.add(db, env, inferred_ty); + }) .or_insert_with(|| UnionAccumulator::new(inferred_ty)); } } @@ -7124,11 +7283,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - let db = self.db(); let elt_tcx_constraints: FxHashMap, Type<'db>> = elt_tcx_constraints .into_iter() - .map(|(identity, accumulator)| (identity, accumulator.into_type(db))) + .map(|(identity, accumulator)| (identity, accumulator.into_type(db, env))) .collect(); (elt_tcx_constraints, elt_tcx_variance) @@ -7179,7 +7337,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let Some(elt) = elt else { continue }; let elt_tcx = if elt.is_starred_expr() && collection_class != KnownClass::Dict { - Type::homogeneous_tuple(self.db(), elt_tcx) + Type::homogeneous_tuple(db, env, elt_tcx) } else { elt_tcx }; @@ -7187,7 +7345,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { infer_elt_expression(self, (i, elt, TypeContext::new(Some(elt_tcx)))); inferred_elt_tys[i] = Some(inferred_elt_ty); - if !inferred_elt_ty.is_assignable_to(self.db(), elt_tcx) { + if !inferred_elt_ty.is_assignable_to(db, env, elt_tcx) { compatible = false; } } @@ -7196,13 +7354,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if compatible { let class_type = collection_alias.origin(self.db()).apply_specialization( - self.db(), + db, |generic_context| { generic_context - .specialize_recursive(self.db(), specialization.into_iter().map(Some)) + .specialize_recursive(db, specialization.into_iter().map(Some)) }, ); - return Type::from(class_type).to_instance_approximation(self.db()); + return Type::from(class_type).to_instance_approximation(db, env); } pre_inferred_elt_tys = Some(inferred_elts); @@ -7271,7 +7429,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Infer `collection[Divergent]` for the initial cycle result. let divergent_instance = collection_alias .origin(self.db()) - .apply_specialization(self.db(), |generic_context| { + .apply_specialization(db, |generic_context| { generic_context .repeat_specialization(self.db(), Type::Divergent(divergent)) }); @@ -7279,14 +7437,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder .infer( identity_instance, - Type::instance(self.db(), divergent_instance), + Type::instance(db, env, divergent_instance), ) .ok()?; } else if let Some(constraints) = statement_use_types.collection_use_constraints(collection_def) { for constraint in constraints { - if constraint.has_unspecialized_type_var(self.db()) { + if constraint.has_unspecialized_type_var(db, env) { continue; } @@ -7302,7 +7460,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let unpack_ty = infer_elt_expression(self, (1, value_expr, tcx)); let Some((unpacked_key_ty, unpacked_value_ty)) = - unpack_ty.unpack_keys_and_items(self.db()) + unpack_ty.unpack_keys_and_items(db, env) else { if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_expr) @@ -7312,7 +7470,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { diag.set_primary_annotation_message(format_args!( "Found `{}`", - unpack_ty.display(self.db()) + unpack_ty.display(db, env) )); } @@ -7322,14 +7480,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut elt_tys = elt_tys.clone(); if let Some((key_ty, value_ty)) = elt_tys.next_tuple() { tuple_size_promotion_constraints.record_unpromotable_type( - self.db(), + db, + env, key_ty.identity(self.db()), - unpacked_key_ty.promote(self.db()), + unpacked_key_ty.promote(db, env), ); tuple_size_promotion_constraints.record_unpromotable_type( - self.db(), + db, + env, value_ty.identity(self.db()), - unpacked_value_ty.promote(self.db()), + unpacked_value_ty.promote(db, env), ); builder.infer(Type::TypeVar(key_ty), unpacked_key_ty).ok()?; @@ -7359,7 +7519,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .copied() .map(|tcx| { if elt.is_starred_expr() && collection_class != KnownClass::Dict { - Type::homogeneous_tuple(self.db(), tcx) + Type::homogeneous_tuple(db, env, tcx) } else { tcx } @@ -7375,7 +7535,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Simplify the inference based on a non-covariant declared type. if let Some(elt_tcx) = elt_tcx.filter(|_| !elt_tcx_variance[&elt_ty_identity].is_covariant()) - && inferred_elt_ty.is_assignable_to(self.db(), elt_tcx) + && inferred_elt_ty.is_assignable_to(db, env, elt_tcx) { continue; } @@ -7383,12 +7543,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // A covariant context is an upper bound, so promotion must not widen an otherwise // compatible element beyond that bound. In particular, promoting an exact float // introduces `int`, which is not assignable to an exact-float context. - let promoted_elt_ty = inferred_elt_ty.promote(self.db()); + let promoted_elt_ty = inferred_elt_ty.promote(db, env); let inferred_elt_ty = if let Some(elt_tcx) = elt_tcx && elt_tcx_variance[&elt_ty_identity].is_covariant() && promoted_elt_ty != inferred_elt_ty - && !promoted_elt_ty.is_assignable_to(self.db(), elt_tcx) - && inferred_elt_ty.is_assignable_to(self.db(), elt_tcx) + && !promoted_elt_ty.is_assignable_to(db, env, elt_tcx) + && inferred_elt_ty.is_assignable_to(db, env, elt_tcx) { inferred_elt_ty } else { @@ -7397,14 +7557,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let inferred_type_for_typevar = if elt.is_starred_expr() { inferred_elt_ty - .iterate(self.db()) - .homogeneous_element_type(self.db()) + .iterate(db, env) + .homogeneous_element_type(db, env) } else { inferred_elt_ty }; tuple_size_promotion_constraints.record_inferred_expression_type( - self.db(), + db, + env, elt_ty_identity, elt, inferred_type_for_typevar, @@ -7418,7 +7579,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let class_type = collection_alias .origin(self.db()) - .apply_specialization(self.db(), |_| { + .apply_specialization(db, |_| { builder.build_with(generic_context, |current_typevar, bounds| { let lower = bounds?.lower?; @@ -7426,7 +7587,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Constraints learned from later collection uses follow the same promotion // policy as literal elements: promote element literal types in invariant // position unless an explicit annotation made them unpromotable. - lower.promote(self.db()) + lower.promote(db, env) } else { lower }; @@ -7434,7 +7595,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let lower = if tuple_size_promotion_constraints .allow(current_typevar.identity(self.db())) { - lower.promote_tuple_size_in_union(self.db()) + lower.promote_tuple_size_in_union(db, env) } else { lower }; @@ -7443,7 +7604,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { lower // Promote singleton types to `T | Unknown` in inferred type parameters, // so that e.g. `[None]` is inferred as `list[None | Unknown]`. - .promote_singletons_recursively(self.db()) + .promote_singletons_recursively(db, env) } else { lower }; @@ -7452,7 +7613,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) }); - Type::from(class_type).to_instance_approximation(self.db()) + Type::from(class_type).to_instance_approximation(db, env) } /// Infer the type of the `iter` expression of the first comprehension. @@ -7477,32 +7638,35 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { tcx: TypeContext<'db>, evaluation_mode: EvaluationMode, ) -> TypeContext<'db> { + let db = self.db(); + let env = self.program_environment(); let Some(annotation) = tcx.annotation else { return TypeContext::default(); }; - let db = self.db(); let yield_typevar = BoundTypeVarInstance::synthetic( db, + env, Name::new_static("_GeneratorYieldT"), TypeVarVariance::Covariant, ); let yield_ty = Type::TypeVar(yield_typevar); - let none = Type::none(db); + let none = Type::none(db, env); let generator_ty = if evaluation_mode.is_async() { - KnownClass::AsyncGeneratorType.to_specialized_instance(db, &[yield_ty, none]) + KnownClass::AsyncGeneratorType.to_specialized_instance(db, env, &[yield_ty, none]) } else { - KnownClass::GeneratorType.to_specialized_instance(db, &[yield_ty, none, none]) + KnownClass::GeneratorType.to_specialized_instance(db, env, &[yield_ty, none, none]) }; - let generic_context = GenericContext::from_typevar_instances(db, [yield_typevar]); + let generic_context = GenericContext::from_typevar_instances(db, env, [yield_typevar]); let path_bounds = generator_ty.assignable_solutions_with_inferable( db, + env, annotation, generic_context.inferable_typevars(db), ); let constraints = ConstraintSetBuilder::new(); - let Solutions::Constrained(solutions) = path_bounds.solve(db, &constraints) else { + let Solutions::Constrained(solutions) = path_bounds.solve(db, env, &constraints) else { return TypeContext::default(); }; @@ -7513,13 +7677,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } match &mut yield_tcx { - Some(accumulator) => accumulator.add(db, binding.solution), + Some(accumulator) => { + accumulator.add(db, env, binding.solution); + } None => yield_tcx = Some(UnionAccumulator::new(binding.solution)), } } } - TypeContext::new(yield_tcx.map(|accumulator| accumulator.into_type(db))) + TypeContext::new(yield_tcx.map(|accumulator| accumulator.into_type(db, env))) } fn infer_generator_expression( @@ -7527,6 +7693,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { generator: &ast::ExprGenerator, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprGenerator { range: _, node_index: _, @@ -7546,18 +7714,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let evaluation_mode = EvaluationMode::from_is_async(scope_id.is_async_comprehension(self.index)); let yield_tcx = self.generator_yield_type_context(tcx, evaluation_mode); - let scope = scope_id.to_scope_id(self.db(), self.file()); + let scope = scope_id.to_scope_id(self.db(), self.python_file()); let inference = infer_scope_types(self.db(), scope, yield_tcx); self.extend_scope(inference); let yield_type = self.comprehension_element_type(elt, inference); if evaluation_mode.is_async() { - KnownClass::AsyncGeneratorType - .to_specialized_instance(self.db(), &[yield_type, Type::none(self.db())]) + KnownClass::AsyncGeneratorType.to_specialized_instance( + db, + env, + &[yield_type, Type::none(db, env)], + ) } else { KnownClass::GeneratorType.to_specialized_instance( - self.db(), - &[yield_type, Type::none(self.db()), Type::none(self.db())], + db, + env, + &[yield_type, Type::none(db, env), Type::none(db, env)], ) } } @@ -7567,11 +7739,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { element: &ast::Expr, inference: &ScopeInference<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let element_type = inference.expression_type(element); if element.is_starred_expr() { element_type - .iterate(self.db()) - .homogeneous_element_type(self.db()) + .iterate(db, env) + .homogeneous_element_type(db, env) } else { element_type } @@ -7604,6 +7778,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { listcomp: &ast::ExprListComp, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); let ast::ExprListComp { range: _, node_index: _, @@ -7619,7 +7794,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { else { return Type::unknown(); }; - let scope = scope_id.to_scope_id(self.db(), self.file()); + let scope = scope_id.to_scope_id(self.db(), self.python_file()); let inference = infer_scope_types(self.db(), scope, tcx); self.extend_scope(inference); @@ -7630,7 +7805,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { inference, tcx, ) - .unwrap_or_else(|| KnownClass::List.to_specialized_instance(self.db(), &[Type::unknown()])) + .unwrap_or_else(|| { + KnownClass::List.to_specialized_instance( + db, + self.program_environment(), + &[Type::unknown()], + ) + }) } fn infer_set_comprehension_expression( @@ -7638,6 +7819,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { setcomp: &ast::ExprSetComp, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); let ast::ExprSetComp { range: _, node_index: _, @@ -7653,7 +7835,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { else { return Type::unknown(); }; - let scope = scope_id.to_scope_id(self.db(), self.file()); + let scope = scope_id.to_scope_id(self.db(), self.python_file()); let inference = infer_scope_types(self.db(), scope, tcx); self.extend_scope(inference); @@ -7664,7 +7846,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { inference, tcx, ) - .unwrap_or_else(|| KnownClass::Set.to_specialized_instance(self.db(), &[Type::unknown()])) + .unwrap_or_else(|| { + KnownClass::Set.to_specialized_instance( + db, + self.program_environment(), + &[Type::unknown()], + ) + }) } fn infer_dict_comprehension_expression( @@ -7672,6 +7860,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { dictcomp: &ast::ExprDictComp, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); let ast::ExprDictComp { range: _, node_index: _, @@ -7688,7 +7877,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { else { return Type::unknown(); }; - let scope = scope_id.to_scope_id(self.db(), self.file()); + let scope = scope_id.to_scope_id(self.db(), self.python_file()); let inference = infer_scope_types(self.db(), scope, tcx); self.extend_scope(inference); @@ -7700,7 +7889,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { tcx, ) .unwrap_or_else(|| { - KnownClass::Dict.to_specialized_instance(self.db(), &[Type::unknown(), Type::unknown()]) + KnownClass::Dict.to_specialized_instance( + db, + self.program_environment(), + &[Type::unknown(), Type::unknown()], + ) }) } @@ -7709,6 +7902,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { generator: &ast::ExprGenerator, tcx: TypeContext<'db>, ) { + let db = self.db(); let ast::ExprGenerator { range: _, node_index: _, @@ -7718,7 +7912,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = generator; let elt_tcx = if elt.is_starred_expr() { - tcx.map(|yield_ty| KnownClass::Iterable.to_specialized_instance(self.db(), &[yield_ty])) + tcx.map(|yield_ty| { + KnownClass::Iterable.to_specialized_instance( + db, + self.program_environment(), + &[yield_ty], + ) + }) } else { tcx }; @@ -7830,6 +8030,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_comprehension(&mut self, comprehension: &ast::Comprehension, is_first: bool) { + let db = self.db(); + let env = self.program_environment(); let ast::Comprehension { range: _, node_index: _, @@ -7848,8 +8050,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { builder.infer_maybe_standalone_expression(iter, tcx) } - .iterate(builder.db()) - .homogeneous_element_type(builder.db()) + .iterate(db, env) + .homogeneous_element_type(db, env) }); for expr in ifs { @@ -7862,6 +8064,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { comprehension: &ComprehensionDefinitionKind<'db>, definition: Definition<'db>, ) { + let db = self.db(); let iterable = comprehension.iterable(self.module()); let target = comprehension.target(self.module()); @@ -7905,15 +8108,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(element_type) = element_type { element_type } else { + let env = self.program_environment(); iterable_type .try_iterate_with_mode( - self.db(), + db, + env, EvaluationMode::from_is_async(comprehension.is_async()), ) - .map(|tuple| tuple.homogeneous_element_type(self.db())) + .map(|tuple| tuple.homogeneous_element_type(db, env)) .unwrap_or_else(|err| { err.report_diagnostic(&self.context, iterable_type, iterable.into()); - err.fallback_element_type(self.db()) + err.fallback_element_type(db, env) }) } } @@ -7963,6 +8168,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if_expression: &ast::ExprIf, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprIf { range: _, node_index: _, @@ -7992,13 +8199,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (body_ty, orelse_ty) }; - match test_ty.try_bool(self.db()).unwrap_or_else(|err| { + match test_ty.try_bool(db, env).unwrap_or_else(|err| { err.report_diagnostic(&self.context, &**test); err.fallback_truthiness() }) { Truthiness::AlwaysTrue => body_ty, Truthiness::AlwaysFalse => orelse_ty, - Truthiness::Ambiguous => UnionType::from_two_elements(self.db(), body_ty, orelse_ty), + Truthiness::Ambiguous => UnionType::from_two_elements(db, env, body_ty, orelse_ty), } } @@ -8011,6 +8218,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { lambda_expression: &ast::ExprLambda, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprLambda { range: _, node_index: _, @@ -8026,7 +8235,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // TODO: We could perform multi-inference here if there are multiple `Callable` annotations // in the union/intersection. && let Some(callable) = tcx - .filter_union(self.db(), Type::is_callable_type) + .filter_union(db, Type::is_callable_type) .as_callable() { match callable.signatures(self.db()).overloads.as_slice() { @@ -8055,7 +8264,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let parameter = Parameter::positional_only(Some(param.name().id.clone())) .with_optional_default_type(param.default().map(|default_expr| { self.infer_expression(default_expr, TypeContext::default()) - .replace_parameter_defaults(self.db()) + .replace_parameter_defaults(db, env) })); if let Some(annotated_type) = parameter_types.next() { @@ -8072,7 +8281,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let parameter = Parameter::positional_or_keyword(param.name().id.clone()) .with_optional_default_type(param.default().map(|default_expr| { self.infer_expression(default_expr, TypeContext::default()) - .replace_parameter_defaults(self.db()) + .replace_parameter_defaults(db, env) })); if let Some(annotated_type) = parameter_types.next() { @@ -8093,7 +8302,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Parameter::keyword_only(param.name().id.clone()).with_optional_default_type( param.default().map(|default_expr| { self.infer_expression(default_expr, TypeContext::default()) - .replace_parameter_defaults(self.db()) + .replace_parameter_defaults(db, env) }), ) }) @@ -8110,7 +8319,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .chain(keyword_only) .chain(keyword_variadic); - Parameters::from_annotation(self.db(), parameters) + Parameters::from_annotation(db, parameters) } else { Parameters::empty() }; @@ -8124,7 +8333,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return Type::unknown(); }; - let scope = scope_id.to_scope_id(self.db(), self.file()); + let scope = scope_id.to_scope_id(self.db(), self.python_file()); // If we have a direct `Callable` type context, we can infer the body with the annotated // return type as type context. @@ -8162,6 +8371,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { argument_type: Type<'db>, argument: &'ast ast::ArgOrKeyword, ) -> Option> { + let env = self.program_environment(); let db = self.db(); let file_scope_id = self.scope().file_scope_id(db); let use_def = self.index.use_def_map(file_scope_id); @@ -8192,10 +8402,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Collect the types of each distinct key. let mut elements: Vec<(&str, Type<'db>)> = Vec::new(); - - for bindings in use_def.multi_bindings_at_use(keyword.scoped_use_id(db, self.file())) { + for bindings in use_def.multi_bindings_at_use(keyword.scoped_use_id(db, self.python_file())) + { let place = place_from_bindings_with_reachability_cache( db, + env, bindings.clone(), self.reachability_cache(), ); @@ -8231,6 +8442,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let getitem_protocol = Type::protocol_with_methods( db, + env, [( "__getitem__", CallableType::new( @@ -8246,6 +8458,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // as it may contain keys that were not explicitly assigned to. Some(IntersectionType::from_elements( db, + env, [argument_type, getitem_protocol], )) } @@ -8256,6 +8469,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, arguments: &'a ast::Arguments, ) -> CallArguments<'a, 'db> { + let db = self.db(); + let env = self.program_environment(); let call_arguments = CallArguments::from_arguments(arguments, |arg_or_keyword, splatted_value| { let ty = self.get_or_infer_expression(splatted_value, TypeContext::default()); @@ -8273,7 +8488,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for arg in &arguments.args { if let ast::Expr::Starred(ast::ExprStarred { value, .. }) = arg { let iterable_type = self.expression_type(value); - if let Err(err) = iterable_type.try_iterate(self.db()) { + if let Err(err) = iterable_type.try_iterate(db, env) { err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); } } @@ -8287,7 +8502,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mapping_type = self.expression_type(&keyword.value); if mapping_type.as_paramspec_typevar(self.db()).is_some() - || mapping_type.unpack_keys_and_items(self.db()).is_some() + || mapping_type.unpack_keys_and_items(db, env).is_some() { continue; } @@ -8303,7 +8518,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .into_diagnostic("Argument expression after ** must be a mapping type") .set_primary_annotation_message(format_args!( "Found `{}`", - mapping_type.display(self.db()) + mapping_type.display(db, env) )); } @@ -8319,7 +8534,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { receiver_generic_context: Option>, call_specialization: Specialization<'db>, ) -> Option> { - let constraint = identity_instance.apply_specialization(self.db(), call_specialization); + let db = self.db(); + let env = self.program_environment(); + let constraint = identity_instance.apply_specialization(db, call_specialization); let Some(receiver_generic_context) = receiver_generic_context else { return Some(constraint); }; @@ -8328,7 +8545,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // types learned for the collection. Until collection-use constraints are represented as // projected constraint sets, avoid leaking those method-local typevars into the inferred // collection literal type. - if any_over_type(self.db(), constraint, false, |ty| { + if any_over_type(db, env, constraint, false, |ty| { ty.as_typevar().is_some_and(|typevar| { !receiver_generic_context.contains(self.db(), typevar.identity(self.db())) }) @@ -8435,18 +8652,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) -> Type<'db> { fn report_missing_implicit_constructor_call<'db>( context: &InferContext<'db, '_>, - db: &'db dyn Db, callable_type: Type<'db>, call_expression: &ast::ExprCall, bindings: &Bindings<'db>, ) { + let db = context.db(); + let env = context.program_environment(); if bindings.has_implicit_dunder_new_is_possibly_unbound() { if let Some(builder) = context.report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, call_expression) { builder.into_diagnostic(format_args!( "Method `__new__` on type `{}` may be missing.", - callable_type.display(db), + callable_type.display(db, env), )); } } @@ -8457,12 +8675,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { builder.into_diagnostic(format_args!( "Method `__init__` on type `{}` may be missing.", - callable_type.display(db), + callable_type.display(db, env), )); } } } + let db = self.db(); + let env = self.program_environment(); let ast::ExprCall { range: _, node_index: _, @@ -8560,7 +8780,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let class = match callable_type { Type::ClassLiteral(class) => Some(ClassType::NonGeneric(class)), Type::GenericAlias(generic) => Some(ClassType::Generic(generic)), - Type::SubclassOf(subclass) => subclass.subclass_of().into_class(self.db()), + Type::SubclassOf(subclass) => subclass.subclass_of().into_class(db, env), _ => None, }; @@ -8634,10 +8854,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { TypeContext::new(Some(field.declared_ty)), ) } else { - Type::none(self.db()) + Type::none(db, env) }; return UnionType::from_two_elements( - self.db(), + db, + env, field.declared_ty, default_ty, ); @@ -8664,7 +8885,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { builder.into_diagnostic(format_args!( "Cannot {action} read-only extra item \"{key}\" {preposition} TypedDict `{}`", - Type::TypedDict(typed_dict_ty).display(self.db()), + Type::TypedDict(typed_dict_ty).display(db, env), )); } return Type::unknown(); @@ -8683,7 +8904,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { field.declared_ty, |default| { UnionType::from_two_elements( - self.db(), + db, + env, field.declared_ty, self.get_or_infer_expression( default, @@ -8767,10 +8989,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match callable_type { Type::BoundMethod(bound_method) => { let function = bound_method.function(self.db()); - if let Some(class) = bound_method - .self_instance(self.db()) - .to_class_type(self.db()) - { + if let Some(class) = bound_method.self_instance(self.db()).to_class_type(db) { if function.is_classmethod(self.db()) && function.as_abstract_method(self.db(), class).is_some() && function.has_trivial_body(self.db()) @@ -8787,7 +9006,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::FunctionLiteral(function) if function.is_staticmethod(self.db()) => { if let ast::Expr::Attribute(ast::ExprAttribute { value, .. }) = func.as_ref() { let value_type = self.expression_type(value); - if let Some(class) = value_type.to_class_type(self.db()) { + if let Some(class) = value_type.to_class_type(db) { if function.as_abstract_method(self.db(), class).is_some() && function.has_trivial_body(self.db()) { @@ -8877,14 +9096,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { _ => {} } } - - let mut bindings = self - .bindings_for_call(callable_type) - .match_parameters(self.db(), &call_arguments); + let mut bindings = + self.bindings_for_call(callable_type) + .match_parameters(db, env, &call_arguments); report_missing_implicit_constructor_call( &self.context, - self.db(), callable_type, call_expression, &bindings, @@ -8908,7 +9125,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Ok(()) => bindings, Err(_) => { bindings.report_diagnostics(&self.context, call_expression.into()); - return bindings.return_type(self.db()); + return bindings.return_type(db, env); } }; @@ -8927,7 +9144,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { overload, &call_arguments, call_expression, - self.file(), ); } } @@ -8969,18 +9185,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let value_type = self.expression_type(value); if let Some(collection_def) = self.index.unannotated_collection_initializer(value) - && let Some((collection_literal, _)) = value_type.class_specialization(self.db()) + && let Some((collection_literal, _)) = value_type.class_specialization(db, env) { - let identity_instance = Type::instance( - self.db(), - collection_literal.identity_specialization(self.db()), - ); - let collection_generic_context = collection_literal.generic_context(self.db()); - + let identity_instance = + Type::instance(db, env, collection_literal.identity_specialization(db)); + let collection_generic_context = collection_literal.generic_context(db); let mut identity_bindings = self .infer_attribute_load_impl(attribute, identity_instance) - .bindings(self.db()) - .match_parameters(self.db(), &call_arguments) + .bindings(db, env) + .match_parameters(db, env, &call_arguments) // Perform inference against the type variables on the receiver's generic context. .with_generic_context(self.db(), collection_generic_context); @@ -9031,15 +9244,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(instance_ty) = self.infer_builtin_range_instance_type(callable_type, arguments, &call_arguments) { - bindings = bindings.with_constructed_instance_type(self.db(), instance_ty); + bindings = bindings.with_constructed_instance_type(db, instance_ty); } let db = self.db(); - let return_ty = bindings.return_type(db); + let return_ty = bindings.return_type(db, env); let return_ty = match collection_initializer_class { Some(collection_class @ (KnownClass::List | KnownClass::Set)) if return_ty - .class_specialization(db) + .class_specialization(db, env) .is_some_and(|(class, _)| class.is_known(db, collection_class)) => { self.infer_empty_list_or_set_constructor( @@ -9060,6 +9273,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { starred: &ast::ExprStarred, tcx: TypeContext<'db>, ) -> Type<'db> { + let env = self.program_environment(); let ast::ExprStarred { range: _, node_index: _, @@ -9074,7 +9288,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if typevar.is_typevartuple(db) => { bind_typevar( - db, + self.db(), self.index, self.scope().file_scope_id(db), self.typevar_binding_context, @@ -9087,41 +9301,43 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(typevartuple) = typevartuple { return Type::tuple(TupleType::new( db, + env, &TupleSpecBuilder::with_capacity(0) - .concat_variadic_typevar(db, typevartuple) + .concat_variadic_typevar(db, env, typevartuple) .build(), )); } iterable_type - .try_iterate(db) - .map(|spec| Type::tuple(TupleType::new(db, &spec))) + .try_iterate(db, env) + .map(|spec| Type::tuple(TupleType::new(db, env, &spec))) .unwrap_or_else(|err| { err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); - Type::homogeneous_tuple(db, err.fallback_element_type(db)) + Type::homogeneous_tuple(db, env, err.fallback_element_type(db, env)) }) } fn infer_yield_expression(&mut self, yield_expression: &ast::ExprYield) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprYield { range: _, node_index: _, value, } = yield_expression; - let Some(enclosing_function) = - nearest_enclosing_function(self.db(), self.index, self.scope()) + let Some(enclosing_function) = nearest_enclosing_function(db, self.index, self.scope()) else { let _ = self.infer_optional_expression(value.as_deref(), TypeContext::default()); return Type::unknown(); }; let declared_return_ty = same_module_uncached_raw_signature( - self.db(), + db, enclosing_function, ReturnCallableTypeVarScope::Public, ) .return_ty; let return_type_span = enclosing_function.spans(self.db()).return_type; - let Some(generator_type_params) = declared_return_ty.generator_types(self.db()) else { + let Some(generator_type_params) = declared_return_ty.generator_types(db, env) else { let _ = self.infer_optional_expression(value.as_deref(), TypeContext::default()); return Type::unknown(); }; @@ -9130,13 +9346,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let tcx = TypeContext::new(expected_yield_ty); let yielded_ty = self .infer_optional_expression(value.as_deref(), tcx) - .unwrap_or_else(|| Type::none(self.db())); + .unwrap_or_else(|| Type::none(db, env)); let diagnostic_node: AnyNodeRef = value .as_deref() .map_or_else(|| yield_expression.into(), AnyNodeRef::from); if let Some(expected_yield_ty) = expected_yield_ty - && !yielded_ty.is_assignable_to(self.db(), expected_yield_ty) + && !yielded_ty.is_assignable_to(db, env, expected_yield_ty) { report_invalid_generator_yield_type( &self.context, @@ -9152,46 +9368,47 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_yield_from_expression(&mut self, yield_from: &ast::ExprYieldFrom) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprYieldFrom { range: _, node_index: _, value, } = yield_from; - let Some(enclosing_function) = - nearest_enclosing_function(self.db(), self.index, self.scope()) + let Some(enclosing_function) = nearest_enclosing_function(db, self.index, self.scope()) else { let _ = self.infer_expression(value, TypeContext::default()); return Type::unknown(); }; let annotated_return_ty = same_module_uncached_raw_signature( - self.db(), + db, enclosing_function, ReturnCallableTypeVarScope::Public, ) .return_ty; - let Some(outer_expected) = annotated_return_ty.generator_types(self.db()) else { + let Some(outer_expected) = annotated_return_ty.generator_types(db, env) else { let _ = self.infer_expression(value, TypeContext::default()); return Type::unknown(); }; let return_type_span = enclosing_function.spans(self.db()).return_type; let tcx = TypeContext::new(outer_expected.yield_ty.map(|yielded_ty| { - KnownClass::Iterable.to_specialized_instance(self.db(), &[yielded_ty]) + KnownClass::Iterable.to_specialized_instance(db, env, &[yielded_ty]) })); let iterable_type = self.infer_expression(value, tcx); let inner_yield_ty = iterable_type - .try_iterate(self.db()) - .map(|tuple| tuple.homogeneous_element_type(self.db())) + .try_iterate(db, env) + .map(|tuple| tuple.homogeneous_element_type(db, env)) .unwrap_or_else(|err| { err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); - err.fallback_element_type(self.db()) + err.fallback_element_type(db, env) }); if let Some(outer_yield_ty) = outer_expected.yield_ty - && !inner_yield_ty.is_assignable_to(self.db(), outer_yield_ty) + && !inner_yield_ty.is_assignable_to(db, env, outer_yield_ty) { report_invalid_generator_yield_type( &self.context, @@ -9205,9 +9422,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(outer_send_ty) = outer_expected.send_ty { let inner_send_ty = iterable_type - .generator_send_type(self.db()) - .unwrap_or_else(|| Type::none(self.db())); - if !outer_send_ty.is_assignable_to(self.db(), inner_send_ty) { + .generator_send_type(db, env) + .unwrap_or_else(|| Type::none(db, env)); + if !outer_send_ty.is_assignable_to(db, env, inner_send_ty) { report_invalid_generator_yield_type( &self.context, value.as_ref(), @@ -9220,7 +9437,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } iterable_type - .generator_return_type(self.db()) + .generator_return_type(db, env) .unwrap_or_else(Type::unknown) } @@ -9229,6 +9446,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { await_expression: &ast::ExprAwait, tcx: TypeContext<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprAwait { range: _, node_index: _, @@ -9237,10 +9456,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let expr_type = self.infer_expression( value, - tcx.map(|tcx| KnownClass::Awaitable.to_specialized_instance(self.db(), &[tcx])), + tcx.map(|tcx| KnownClass::Awaitable.to_specialized_instance(db, env, &[tcx])), ); - expr_type.try_await(self.db()).unwrap_or_else(|err| { + expr_type.try_await(db, env).unwrap_or_else(|err| { err.report_diagnostic(&self.context, expr_type, value.as_ref().into()); Type::unknown() }) @@ -9254,6 +9473,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { constraint_keys: &[(FileScopeId, ConstraintKey)], ) -> Type<'db> { let db = self.db(); + let env = self.program_environment(); for (enclosing_scope_file_id, constraint_key) in constraint_keys { let use_def = self.index.use_def_map(*enclosing_scope_file_id); let place_table = self.index.place_table(*enclosing_scope_file_id); @@ -9266,7 +9486,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.index, ) { ApplicableConstraints::UnboundBinding(constraint) => { - ty = constraint.narrow(db, ty, place); + ty = constraint.narrow(db, env, ty, place); } // Performs narrowing based on constrained bindings. // This handling must be performed even if narrowing is attempted and failed using `infer_place_load`. @@ -9286,7 +9506,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ApplicableConstraints::ConstrainedBindings(bindings) => { let reachability_constraints = bindings.reachability_constraints(); let predicates = bindings.predicates(); - let mut union = UnionBuilder::new(db); + let mut union = UnionBuilder::new(db, env); for binding in bindings { let static_reachability = evaluate_reachability_with_cache( db, @@ -9303,15 +9523,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if !is_discarded_dict_key_assignment(db, definition) => { let binding_ty = binding_type(db, definition); - union = union.add( - binding.narrowing_constraint.narrow(db, binding_ty, place), + union.add_in_place( + binding + .narrowing_constraint + .narrow(db, env, binding_ty, place), ); } DefinitionState::Defined(_) | DefinitionState::Undefined | DefinitionState::Deleted => { - union = - union.add(binding.narrowing_constraint.narrow(db, ty, place)); + union.add_in_place( + binding.narrowing_constraint.narrow(db, env, ty, place), + ); } } } @@ -9380,6 +9603,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn infer_name_load(&mut self, name_node: &ast::ExprName) -> Type<'db> { + let db = self.db(); let ast::ExprName { range: _, node_index: _, @@ -9387,35 +9611,37 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ctx: _, } = name_node; let expr = PlaceExpr::from_expr_name(name_node); - let db = self.db(); let (resolved, constraint_keys) = self.infer_place_load(PlaceExprRef::from(&expr), ast::ExprRef::Name(name_node)); + let env = self.program_environment(); let resolved_after_fallback = resolved // 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, self.file(), symbol_name).map_type(|ty| { - self.narrow_place_with_applicable_constraints( - PlaceExprRef::from(&expr), - ty, - &constraint_keys, - ) - }) + .or_fall_back_to(db, env, || { + module_type_implicit_global_symbol(db, self.python_file(), symbol_name).map_type( + |ty| { + self.narrow_place_with_applicable_constraints( + PlaceExprRef::from(&expr), + ty, + &constraint_keys, + ) + }, + ) }) // Not found in globals? Fallback to builtins // (without infinite recursion if we're already in builtins.) - .or_fall_back_to(db, || { - if Some(self.scope()) == builtins_module_scope(db) { + .or_fall_back_to(db, env, || { + if Some(self.scope()) == builtins_module_scope(db, env) { Place::Undefined.into() } else { - builtins_symbol(db, symbol_name) + builtins_symbol(db, env, symbol_name) } }) // Still not found? It might be `reveal_type`... - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { if symbol_name == "reveal_type" { if let Some(builder) = self.context.report_lint(&UNDEFINED_REVEAL, name_node) { let mut diag = @@ -9424,14 +9650,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "This is allowed for debugging convenience but will fail at runtime", ); } - typing_extensions_symbol(db, symbol_name) + typing_extensions_symbol(db, env, symbol_name) } else { Place::Undefined.into() } }); - let ty = - resolved_after_fallback.unwrap_with_diagnostic(db, |lookup_error| match lookup_error { + let ty = resolved_after_fallback.unwrap_with_diagnostic(db, env, |lookup_error| { + match lookup_error { LookupError::Undefined(qualifiers) => { self.report_unresolved_reference(name_node); TypeAndQualifiers::new(Type::unknown(), TypeOrigin::Inferred, qualifiers) @@ -9440,7 +9666,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { report_possibly_unresolved_reference(&self.context, name_node); type_when_bound } - }); + } + }); ty.inner_type() } @@ -9450,6 +9677,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { expr: PlaceExprRef, expr_ref: ast::ExprRef, ) -> (Place<'db>, Option) { + let env = self.program_environment(); let db = self.db(); let scope = self.scope(); let file_scope_id = scope.file_scope_id(db); @@ -9461,6 +9689,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let place = if let Some(place_id) = place_table.place_id(expr) { place_from_bindings_with_reachability_cache( db, + env, use_def.reachable_bindings(place_id), self.reachability_cache(), ) @@ -9487,16 +9716,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let ast::ExprRef::Named(named) = expr_ref { let place = if named.target.is_name_expr() { let definition = self.index.expect_single_definition(named); - Place::bound(binding_type(db, definition)).with_definition(definition) + Place::bound(binding_type(self.db(), definition)).with_definition(definition) } else { Place::Undefined }; return (place, None); } - let use_id = expr_ref.scoped_use_id(db, self.file()); + let use_id = expr_ref.scoped_use_id(db, self.python_file()); let place = place_from_bindings_with_reachability_cache( db, + env, use_def.bindings_at_use(use_id), self.reachability_cache(), ) @@ -9529,7 +9759,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { assume_bound: bool, ) -> PlaceAndQualifiers<'db> { let db = self.db(); - if current_scope_id.is_global() { return Place::Undefined.into(); } @@ -9551,6 +9780,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { EnclosingSnapshotResult::FoundBindings(bindings) => { let mut place_and_qualifiers = place_from_bindings_with_reachability_cache( db, + self.program_environment(), bindings, self.reachability_cache(), ); @@ -9583,7 +9813,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return Place::Undefined.into(); }; - explicit_global_symbol(db, self.file(), symbol_name).map_type(|ty| { + explicit_global_symbol(self.db(), self.python_file(), symbol_name).map_type(|ty| { self.narrow_place_with_applicable_constraints(place_expr, ty, constraint_keys) }) } @@ -9596,6 +9826,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { place_expr: PlaceExprRef, expr_ref: ast::ExprRef, ) -> (PlaceAndQualifiers<'db>, Vec<(FileScopeId, ConstraintKey)>) { + let env = self.program_environment(); let db = self.db(); let scope = self.scope(); let file_scope_id = scope.file_scope_id(db); @@ -9607,7 +9838,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { constraint_keys.push((file_scope_id, ConstraintKey::UseId(use_id))); } - let place = PlaceAndQualifiers::from(local_scope_place).or_fall_back_to(db, || { + let fallback = || { let mut symbol_resolves_locally = false; if let Some(symbol) = place_expr.as_symbol() && let Some(symbol_id) = place_table.symbol_id(symbol.name()) @@ -9767,6 +9998,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { EnclosingSnapshotResult::FoundBindings(bindings) => { let place = place_from_bindings_with_reachability_cache( db, + env, bindings, self.reachability_cache(), ) @@ -9834,10 +10066,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // We've reached the defining scope of the variable. Infer its public type. debug_assert!(enclosing_place.is_bound() || enclosing_place.is_declared()); - let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, self.file()); + let enclosing_scope_id = + enclosing_scope_file_id.to_scope_id(db, self.python_file()); return eagerly_resolved_place.unwrap_or_else(|| { place_by_id( - db, + self.db(), enclosing_scope_id, enclosing_place_id, RequiresExplicitReExport::No, @@ -9856,11 +10089,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { PlaceAndQualifiers::default() // If we're in a class body, check for implicit class body symbols first. // These take precedence over globals. - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { if scope.node(db).scope_kind().is_class() && let Some(symbol) = place_expr.as_symbol() { - let implicit = class_body_implicit_symbol(db, symbol.name()); + let implicit = class_body_implicit_symbol(db, env, symbol.name()); if implicit.place.is_definitely_bound() { return implicit.map_type(|ty| { self.narrow_place_with_applicable_constraints( @@ -9875,7 +10108,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) // No nonlocal binding? Check the module's explicit globals. // Avoid infinite recursion if `self.scope` already is the module's global scope. - .or_fall_back_to(db, || { + .or_fall_back_to(db, env, || { self.infer_explicit_global_symbol_load( place_expr, place_expr.as_symbol().map(|symbol| symbol.name().as_str()), @@ -9884,7 +10117,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { false, ) }) - }); + }; + let place = PlaceAndQualifiers::from(local_scope_place).or_fall_back_to(db, env, fallback); if let Some(ty) = place.place.ignore_possibly_undefined() { self.check_deprecated(expr_ref, ty); @@ -9894,6 +10128,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn report_unresolved_reference(&self, expr_name_node: &ast::ExprName) { + let db = self.db(); + let env = self.program_environment(); let Some(builder) = self .context .report_lint(&UNRESOLVED_REFERENCE, expr_name_node) @@ -9924,7 +10160,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // === // We don't need to check for typing_extensions.Type, // because it's already caught by typing.Type. - if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY39 { + if self.program_environment().python_version(db) >= PythonVersion::PY39 { if let Some(("", builtin_name)) = as_pep_585_generic("typing", id) { diagnostic .set_primary_annotation_message(format_args!("Did you mean `{builtin_name}`?")); @@ -9962,12 +10198,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let attribute_exists = match MethodDecorator::try_from_fn_type(self.db(), function_type) { - Some(MethodDecorator::ClassMethod) => !Type::instance(self.db(), class) - .class_member(self.db(), id) + Some(MethodDecorator::ClassMethod) => !Type::instance(db, env, class) + .class_member(db, env, id) .place .is_undefined(), - Some(MethodDecorator::None) => !Type::instance(self.db(), class) - .member(self.db(), id) + Some(MethodDecorator::None) => !Type::instance(db, env, class) + .member(db, env, id) .place .is_undefined(), Some(MethodDecorator::StaticMethod) | None => false, @@ -10026,19 +10262,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) -> Type<'db> { fn union_elements_missing_attribute<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, attr_name: &str, missing_types: &mut FxIndexSet>, ) { if let Some(union) = ty.as_union_like(db) { for element in union.elements(db) { - union_elements_missing_attribute(db, *element, attr_name, missing_types); + union_elements_missing_attribute(db, env, *element, attr_name, missing_types); } - } else if ty.member(db, attr_name).place.is_undefined() { + } else if ty.member(db, env, attr_name).place.is_undefined() { missing_types.insert(ty); } } + let env = self.program_environment(); let ast::ExprAttribute { value, attr, .. } = attribute; let db = self.db(); @@ -10073,13 +10311,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { assigned_type = Some(ty); } } - let fallback_place = value_type.member(db, &attr.id).map_type(|ty| { + let fallback_place = value_type.member(db, env, &attr.id).map_type(|ty| { self.narrow_expr_with_applicable_constraints(attribute, ty, &constraint_keys) }); let attr_name = &attr.id; let resolved_type = - fallback_place.unwrap_with_diagnostic(db, |lookup_err| match lookup_err { + fallback_place.unwrap_with_diagnostic(db, env, |lookup_err| match lookup_err { LookupError::Undefined(_) => { let fallback = || { TypeAndQualifiers::new( @@ -10091,12 +10329,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let bound_on_instance = match value_type { Type::ClassLiteral(class) => { - !class.instance_member(db, None, attr).is_undefined() + !class + .instance_member(db, env, None, attr) + .is_undefined() } Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => { match subclass_of.subclass_of() { SubclassOfInner::Class(class) => { - !class.instance_member(db, attr).is_undefined() + !class + .instance_member(db, env, attr) + .is_undefined() } SubclassOfInner::Dynamic(_) => unreachable!( "Attribute lookup on a dynamic `SubclassOf` type \ @@ -10117,7 +10359,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut maybe_submodule_name = module_name.clone(); maybe_submodule_name.extend(&relative_submodule); - if resolve_module(db, self.file(), &maybe_submodule_name).is_some() { + if resolve_module(db, self.python_file(), &maybe_submodule_name) + .is_some() + { if let Some(builder) = self .context .report_lint(&POSSIBLY_MISSING_SUBMODULE, attribute) @@ -10141,12 +10385,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diag = builder.into_diagnostic(format_args!( "Special form `{special_form}` has no attribute `{attr_name}`", )); - if let Ok(defined_type) = value_type.in_type_expression( - db, + if let Ok(defined_type) = value_type.in_type_expression(db, self.scope(), self.typevar_binding_context, self.inference_flags() - ) && !defined_type.member(db, attr_name).place.is_undefined() + ) && !defined_type + .member(db, env, attr_name) + .place + .is_undefined() { diag.help(format_args!( "Objects with type `{ty}` have a{maybe_n} `{attr_name}` \ @@ -10157,7 +10403,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { "" }, - ty = defined_type.display(self.db()) + ty = defined_type.display(db, env) )); if is_dotted_name(value) { let source = @@ -10182,7 +10428,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "Attribute `{attr_name}` can only be accessed on instances, \ not on the class object `{}` itself.", - value_type.display(db) + value_type.display(db, env) )); return fallback(); } @@ -10198,7 +10444,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )), Type::GenericAlias(alias) => builder.into_diagnostic(format_args!( "Class `{}` has no attribute `{attr_name}`", - alias.display(db), + alias.display(db, env), )), Type::FunctionLiteral(function) => builder.into_diagnostic(format_args!( "Function `{}` has no attribute `{attr_name}`", @@ -10206,14 +10452,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )), _ => builder.into_diagnostic(format_args!( "Object of type `{}` has no attribute `{attr_name}`", - value_type.display(db), + value_type.display(db, env), )), }; if value_type.is_callable_type() && KnownClass::FunctionType - .to_instance(db) - .member(db, attr_name) + .to_instance(db, env) + .member(db, env, attr_name) .place .is_definitely_bound() { @@ -10270,7 +10516,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Attribute lookup on a bounded type variable delegates to its upper bound, so // use that bound here too when determining whether the lookup was on a union. let union_like_type = if let Type::TypeVar(typevar) = value_type - && let Some(bound) = typevar.typevar(db).upper_bound(db) + && let Some(bound) = typevar.typevar(db).upper_bound(db, env) { bound } else { @@ -10281,7 +10527,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut elements_missing_the_attribute = FxIndexSet::default(); for element in union.elements(db) { union_elements_missing_attribute( - db, + db, env, *element, attr_name, &mut elements_missing_the_attribute, @@ -10294,14 +10540,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let missing_types = elements_missing_the_attribute .iter() - .map(|ty| format!("`{}`", ty.display(db))) + .map(|ty| { + format!("`{}`", ty.display(db, env)) + }) .collect::>() .join(", "); builder.into_diagnostic(format_args!( "Attribute `{attr_name}` is not defined on {} in union `{union_like_type}`", missing_types, - union_like_type = union_like_type.display(db), + union_like_type = + union_like_type.display(db, env), )); } return type_when_bound; @@ -10368,13 +10617,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { unary_dunder_method: &str, error: Option<&CallDunderError<'db>>, ) { + let db = self.db(); + let env = self.program_environment(); let Some(builder) = self.context.report_lint(&UNSUPPORTED_OPERATOR, unary) else { return; }; let mut diagnostic = builder.into_diagnostic(format_args!( "Unary operator `{op}` is not supported for object of type `{}`", - operand_type.display(self.db()), + operand_type.display(db, env), )); if let Some(CallDunderError::PossiblyUnbound { @@ -10385,7 +10636,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for ty in unbound_on.iter().copied() { diagnostic.info(format_args!( "`{}` does not implement `{unary_dunder_method}`", - ty.display(self.db()) + ty.display(db, env) )); } } @@ -10410,6 +10661,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { operand_type: Type<'db>, unary: &ast::ExprUnaryOp, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let fallback_unary_expression_type = || { let unary_dunder_method = match op { ast::UnaryOp::Invert => "__invert__", @@ -10421,12 +10674,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; match operand_type.try_call_dunder( - self.db(), + db, + env, unary_dunder_method, CallArguments::none(), TypeContext::default(), ) { - Ok(outcome) => outcome.return_type(self.db()), + Ok(outcome) => outcome.return_type(db, env), Err(e) => { self.report_unsupported_unary_operator( unary, @@ -10435,7 +10689,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { unary_dunder_method, Some(&e), ); - e.fallback_return_type(self.db()) + e.fallback_return_type(db, env) } } }; @@ -10446,7 +10700,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (_, Type::Never) => Type::Never, (_, Type::TypeAlias(alias)) => { - self.infer_unary_expression_type(op, alias.value_type(self.db()), unary) + self.infer_unary_expression_type(op, alias.value_type(db), unary) } (ast::UnaryOp::UAdd, Type::LiteralValue(literal)) => match literal.kind() { @@ -10460,7 +10714,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .as_i64() .checked_neg() .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), LiteralValueTypeKind::Bool(value) => Type::int_literal(-i64::from(value)), _ => fallback_unary_expression_type(), }, @@ -10474,7 +10728,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (ast::UnaryOp::Invert, Type::KnownInstance(KnownInstanceType::ConstraintSet(set))) => { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let set = constraints.load(self.db(), set.constraints(self.db())); + let set = constraints.load(db, env, set.constraints(self.db())); set.negate(self.db(), constraints) }); Type::KnownInstance(KnownInstanceType::ConstraintSet( @@ -10483,8 +10737,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } (ast::UnaryOp::Not, ty) => Type::from_truthiness( - self.db(), - ty.try_bool(self.db()) + db, + env, + ty.try_bool(db, env) .unwrap_or_else(|err| { err.report_diagnostic(&self.context, unary); err.fallback_truthiness() @@ -10506,22 +10761,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::UnaryOp::Not => unreachable!(), }; - match tvar.typevar(self.db()).bound_or_constraints(self.db()) { + match tvar.typevar(self.db()).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let db = self.db(); match Self::map_constrained_typevar_constraints( db, + env, operand_type, constraints, |constraint| { constraint .try_call_dunder( db, + env, unary_dunder_method, CallArguments::none(), TypeContext::default(), ) - .map(|outcome| outcome.return_type(db)) + .map(|outcome| outcome.return_type(db, env)) .ok() }, ) { @@ -10538,13 +10794,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { operand_type .try_call_dunder( db, + env, unary_dunder_method, CallArguments::none(), TypeContext::default(), ) .map_or_else( - |e| e.fallback_return_type(db), - |b| b.return_type(db), + |e| e.fallback_return_type(db, env), + |b| b.return_type(db, env), ) } } @@ -10555,24 +10812,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_unary_expression_type(op, bound, unary) } // For unconstrained TypeVars, fall through to default handling. - None => match operand_type.try_call_dunder( - self.db(), - unary_dunder_method, - CallArguments::none(), - TypeContext::default(), - ) { - Ok(outcome) => outcome.return_type(self.db()), - Err(e) => { - self.report_unsupported_unary_operator( - unary, - op, - operand_type, - unary_dunder_method, - Some(&e), - ); - e.fallback_return_type(self.db()) + None => { + match operand_type.try_call_dunder( + db, + env, + unary_dunder_method, + CallArguments::none(), + TypeContext::default(), + ) { + Ok(outcome) => outcome.return_type(db, env), + Err(e) => { + self.report_unsupported_unary_operator( + unary, + op, + operand_type, + unary_dunder_method, + Some(&e), + ); + e.fallback_return_type(db, env) + } } - }, + } } } @@ -10666,9 +10926,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { NeedsPeerType: Fn(&Item) -> bool, InferType: Fn(&mut Self, Item, Option>) -> (Type<'db>, TextRange), { + let db = self.db(); + let env = self.program_environment(); let mut done = false; let mut peer_types: Option> = None; - let db = self.db(); let elements = operations .into_iter() @@ -10679,7 +10940,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { peer_types .as_mut() - .map(|peer_types| peer_types.get_or_build(db)) + .map(|peer_types| peer_types.get_or_build(db, env)) }; let (ty, range) = infer_ty(self, item, peer_ty); @@ -10688,7 +10949,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if is_last { if done { Type::Never } else { ty } } else { - let truthiness = ty.try_bool(self.db()).unwrap_or_else(|err| { + let truthiness = ty.try_bool(db, env).unwrap_or_else(|err| { err.report_diagnostic(&self.context, range); err.fallback_truthiness() }); @@ -10710,11 +10971,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { (Truthiness::Ambiguous, _) => { if track_peer_types { match &mut peer_types { - Some(peer_types) => peer_types.add(db, ty), + Some(peer_types) => peer_types.add(db, env, ty), None => peer_types = Some(UnionAccumulator::new(ty)), } } - IntersectionBuilder::new(db) + IntersectionBuilder::new(db, env) .add_positive(ty) .add_negative(match op { ast::BoolOp::And => Type::AlwaysTruthy, @@ -10726,10 +10987,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }); - UnionType::from_elements(db, elements) + UnionType::from_elements(db, env, elements) } fn infer_compare_expression(&mut self, compare: &ast::ExprCompare) -> Type<'db> { + let db = self.db(); let ast::ExprCompare { range: _, node_index: _, @@ -10782,7 +11044,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match op { // `in, not in, is, is not` always return bool instances ast::CmpOp::In | ast::CmpOp::NotIn | ast::CmpOp::Is | ast::CmpOp::IsNot => { - KnownClass::Bool.to_instance(builder.db()) + KnownClass::Bool.to_instance(db, builder.program_environment()) } // Other operators can return arbitrary types _ => Type::unknown(), @@ -11245,6 +11507,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// The inference results can be merged into the current inference region using /// [`TypeInferenceBuilder::extend`]. fn speculate(&self) -> Self { + let db = self.db(); let Self { region, index, @@ -11274,7 +11537,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { type_expression_flags: _, } = *self; - let mut builder = TypeInferenceBuilder::new(self.db(), region, index, self.module()); + let mut builder = TypeInferenceBuilder::new( + db, + self.program_environment(), + region, + self.file(), + self.python_file(), + index, + self.module(), + ); // Speculated builders are often discarded immediately. builder.context.defuse(); @@ -11798,9 +12069,10 @@ impl StringPartsCollector { self.contains_non_literal_str = true; } - fn string_type(self, db: &dyn Db) -> Type<'_> { + fn string_type<'db>(self, context: &InferContext<'db, '_>) -> Type<'db> { + let db = context.db(); if self.contains_non_literal_str { - KnownClass::Str.to_instance(db) + KnownClass::Str.to_instance(db, context.program_environment()) } else if let Some(concatenated) = self.concatenated { Type::string_literal(db, &concatenated) } else { @@ -11986,6 +12258,7 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { builder: &mut TypeInferenceBuilder<'db, 'ast>, inferred_ty: Type<'db>, ) -> Type<'db> { + let env = builder.program_environment(); let declared_ty = self.declared_ty.unwrap_or(Type::unknown()); let db = builder.db(); @@ -12047,7 +12320,7 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { } } - if !bound_ty.is_assignable_to(db, declared_ty) { + if !bound_ty.is_assignable_to(db, env, declared_ty) { builder.discard_dict_key_assignments_for(self.binding); report_invalid_assignment( &builder.context, @@ -12066,10 +12339,10 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { builder.infer_maybe_standalone_expression(value, TypeContext::default()) }); // If the member is a data descriptor, the RHS value may differ from the value actually assigned. - if assignment_attribute_members(db, value_ty, &attr.id) + if assignment_attribute_members(db, env, value_ty, &attr.id) .and_then(AssignmentAttributeMembers::type_member) .and_then(|member| member.place.ignore_possibly_undefined()) - .is_some_and(|ty| ty.may_be_data_descriptor(db)) + .is_some_and(|ty| ty.may_be_data_descriptor(db, env)) { builder.discard_dict_key_assignments_for(self.binding); bound_ty = declared_ty; @@ -12079,7 +12352,7 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { .try_expression_type(value) .unwrap_or_else(|| builder.infer_expression(value, TypeContext::default())); - if !value_ty.is_typed_dict() && !Self::is_safe_mutable_class(db, value_ty) { + if !value_ty.is_typed_dict() && !Self::is_safe_mutable_class(db, env, value_ty) { builder.discard_dict_key_assignments_for(self.binding); bound_ty = declared_ty; } @@ -12099,7 +12372,11 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { /// pyright. TODO: Other standard library classes may also be considered safe. Also, /// subclasses of these safe classes that do not override `__getitem__/__setitem__` /// may be considered safe. - fn is_safe_mutable_class(db: &'db dyn Db, ty: Type<'db>) -> bool { + fn is_safe_mutable_class( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { const SAFE_MUTABLE_CLASSES: &[KnownClass] = &[ KnownClass::List, KnownClass::Dict, @@ -12113,12 +12390,12 @@ impl<'db, 'ast> AddBinding<'db, 'ast> { SAFE_MUTABLE_CLASSES .iter() - .map(|class| class.to_instance(db)) + .map(|class| class.to_instance(db, env)) .any(|safe_mutable_class| { - ty.is_equivalent_to(db, safe_mutable_class) + ty.is_equivalent_to(db, env, safe_mutable_class) || ty - .generic_origin(db) - .zip(safe_mutable_class.generic_origin(db)) + .generic_origin(db, env) + .zip(safe_mutable_class.generic_origin(db, env)) .is_some_and(|(l, r)| l == r) }) } diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index c2dbb2598e..547c8e0e40 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -175,7 +175,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { AnnotationExpressionInference::new(annotation_ty) } } + let db = self.db(); + let env = self.program_environment(); // https://typing.python.org/en/latest/spec/annotations.html#grammar-token-expression-grammar-annotation_expression let inferred = match annotation { // String annotations: https://typing.python.org/en/latest/spec/annotations.html#string-annotations @@ -239,12 +241,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); let in_type_expression = inferred .inner_type() - .in_type_expression( - self.db(), - self.scope(), - None, - self.inference_flags(), - ) + .in_type_expression(db, self.scope(), None, self.inference_flags()) .unwrap_or_else(|err| { err.into_fallback_type( &self.context, @@ -298,7 +295,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if qualifier == TypeQualifier::ClassVar && type_and_qualifiers .inner_type() - .has_non_self_typevar(self.db()) + .has_non_self_typevar(db, env) && let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { diff --git a/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs b/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs index f05ec5e804..8511760946 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/attribute_assignment.rs @@ -32,7 +32,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { infer_value_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, emit_diagnostics: bool, ) -> bool { - let requirement = attribute_write_requirement(self.db(), object_ty, attribute); + let db = self.db(); + let requirement = + attribute_write_requirement(db, self.program_environment(), object_ty, attribute); let mut evaluator = AssignmentAttributeWriteEvaluator { builder: self, target, @@ -135,7 +137,6 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::NO_INSTANCE_FALLBACK }; let setattr_result = self.builder.infer_and_try_call_dunder( - db, object_ty, "__setattr__", lookup_policy, @@ -159,6 +160,8 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { requirement: &AttributeWriteRequirement<'db>, emit_diagnostics: bool, ) -> bool { + let db = self.builder.db(); + let env = self.builder.program_environment(); match requirement { AttributeWriteRequirement::All { object_ty, @@ -168,7 +171,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { let mut valid = true; for element_ty in *element_tys { let requirement = - attribute_write_requirement(self.builder.db(), *element_ty, self.attribute); + attribute_write_requirement(db, env, *element_ty, self.attribute); if !self.evaluate(&requirement, false) { valid = false; break; @@ -196,7 +199,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { let mut valid = false; for element_ty in intersection.positive(self.builder.db()) { let requirement = - attribute_write_requirement(self.builder.db(), *element_ty, self.attribute); + attribute_write_requirement(db, env, *element_ty, self.attribute); if self.evaluate(&requirement, false) { valid = true; break; @@ -307,7 +310,8 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { emit_diagnostics: bool, ) -> bool { let db = self.builder.db(); - let assignable = value_ty.is_assignable_to(db, target_ty); + let assignable = + value_ty.is_assignable_to(db, self.builder.program_environment(), target_ty); if !assignable && emit_diagnostics { report_invalid_attribute_assignment( &self.builder.context, @@ -356,8 +360,10 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { emit_diagnostics: bool, ) -> bool { let db = self.builder.db(); + let env = self.builder.program_environment(); + let frozen_dataclass_dispatch = object_ty - .nominal_class(db) + .nominal_class(db, env) .and_then(|class| class.static_class_literal(db)) .and_then(|(class, specialization)| { class.inherited_frozen_dataclass_dispatch( @@ -368,7 +374,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { ) }); let setattr_receiver = frozen_dataclass_dispatch - .map_or(object_ty, |dispatch| dispatch.receiver(db, object_ty)); + .map_or(object_ty, |dispatch| dispatch.receiver(db, env, object_ty)); let (setattr_result, value_ty) = if matches!(member, InstanceAttributeWriteMember::SetAttr) || matches!( @@ -380,6 +386,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { let value_ty = self.infer_value(TypeContext::default(), emit_diagnostics); let setattr_result = setattr_receiver.try_call_dunder_with_policy( db, + env, "__setattr__", &mut CallArguments::positional([ Type::string_literal(db, self.attribute), @@ -396,8 +403,8 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { frozen_dataclass_dispatch, Some(FrozenDataclassDispatch::FrozenField) ) || match &setattr_result { - Ok(bindings) => bindings.return_type(db).is_never(), - Err(error) => error.return_type(db).is_some_and(|ty| ty.is_never()), + Ok(bindings) => bindings.return_type(db, env).is_never(), + Err(error) => error.return_type(db, env).is_some_and(|ty| ty.is_never()), }; // We could also model this more precisely by synthesizing a `__setattr__`overload set @@ -406,7 +413,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { let is_private_pydantic_attribute = matches!(member, InstanceAttributeWriteMember::Explicit { .. }) && pydantic::is_private_attribute(self.attribute) - && pydantic::is_model_instance(db, object_ty); + && pydantic::is_model_instance(db, env, object_ty); if setattr_returns_never && !is_private_pydantic_attribute { if emit_diagnostics { @@ -415,6 +422,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { Some(FrozenDataclassDispatch::Delegate(_)) ) && match object_ty.class_member_with_policy( db, + env, "__setattr__", MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ) { @@ -424,7 +432,10 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { } => ty.is_callable_type(), _ => false, }; - let member_exists = !object_ty.member(db, self.attribute).place.is_undefined(); + let member_exists = !object_ty + .member(db, env, self.attribute) + .place + .is_undefined(); self.report(AssignmentAttributeWriteDiagnostic::TerminalSetAttr { member_exists, is_setattr_synthesized, @@ -508,6 +519,8 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { member: &ClassAttributeWriteMember<'db>, emit_diagnostics: bool, ) -> bool { + let db = self.builder.db(); + let env = self.builder.program_environment(); match member { ClassAttributeWriteMember::Explicit { member, fallback } => { if !self.final_assignment_is_valid(object_ty, member.qualifiers(), emit_diagnostics) @@ -539,12 +552,11 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { ClassAttributeWriteMember::Unresolved { has_instance_attribute, } => { - let db = self.builder.db(); let (setattr_result, value_ty) = self.infer_and_try_call_setattr(object_ty, emit_diagnostics); let setattr_returns_never = match &setattr_result { - Ok(bindings) => bindings.return_type(db).is_never(), - Err(error) => error.return_type(db).is_some_and(|ty| ty.is_never()), + Ok(bindings) => bindings.return_type(db, env).is_never(), + Err(error) => error.return_type(db, env).is_some_and(|ty| ty.is_never()), }; if setattr_returns_never { if emit_diagnostics { @@ -615,6 +627,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { value_ty: Type<'db>, emit_diagnostics: bool, ) -> bool { + let env = self.builder.program_environment(); let db = self.builder.db(); let descriptor_ty = descriptor_ty.resolve_type_alias(db); if let Type::Union(union) = descriptor_ty { @@ -639,7 +652,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { return true; } - if property_setter_returns_never(db, descriptor_ty, receiver_ty, value_ty) { + if property_setter_returns_never(db, env, descriptor_ty, receiver_ty, value_ty) { if emit_diagnostics { self.report(AssignmentAttributeWriteDiagnostic::TerminalDescriptor); } @@ -648,6 +661,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { match descriptor_ty.try_call_dunder_with_policy( db, + env, "__set__", &mut CallArguments::positional([receiver_ty, value_ty]), TypeContext::default(), @@ -682,19 +696,21 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { emit_diagnostics: bool, ) -> bool { let db = self.builder.db(); + let env = self.builder.program_environment(); let setter_result = setter_ty.try_call( db, + env, &CallArguments::positional([descriptor_ty, object_ty, value_ty]), ); // `Never` supports arbitrary operations only because there can be no runtime value to // mutate; it is not a concrete descriptor with a terminal setter. let setter_returns_never = !descriptor_ty.is_never() && match &setter_result { - Ok(bindings) => bindings.return_type(db).is_never(), - Err(error) => error.return_type(db).is_never(), + Ok(bindings) => bindings.return_type(db, env).is_never(), + Err(error) => error.return_type(db, env).is_never(), }; if setter_returns_never - || property_setter_returns_never(db, descriptor_ty, object_ty, value_ty) + || property_setter_returns_never(db, env, descriptor_ty, object_ty, value_ty) { if emit_diagnostics { self.report(AssignmentAttributeWriteDiagnostic::TerminalDescriptor); @@ -781,6 +797,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { fn report(&mut self, diagnostic: AssignmentAttributeWriteDiagnostic<'db>) { let db = self.builder.db(); + let env = self.builder.program_environment(); match diagnostic { AssignmentAttributeWriteDiagnostic::InvalidCompositeAssignment { object_ty, @@ -793,9 +810,9 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { { builder.into_diagnostic(format_args!( "Object of type `{}` is not assignable to attribute `{}` on type `{}`", - value_ty.display(db), + value_ty.display(db, env), self.attribute, - object_ty.display(db), + object_ty.display(db, env), )); } } @@ -808,7 +825,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { builder.into_diagnostic(format_args!( "Cannot assign to attribute `{}` on type `{}`", self.attribute, - self.object_ty.display(db), + self.object_ty.display(db, env), )); } } @@ -821,7 +838,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { builder.into_diagnostic(format_args!( "Cannot assign to ClassVar `{}` from an instance of type `{}`", self.attribute, - self.object_ty.display(db), + self.object_ty.display(db, env), )); } } @@ -838,19 +855,19 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { format!( "Cannot assign to unresolved attribute `{}` on type `{}`", self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) ) } else if is_setattr_synthesized { format!( "Property `{}` defined in `{}` is read-only", self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) ) } else { format!( "Cannot assign to attribute `{}` on type `{}` whose `__setattr__` method returns `Never`/`NoReturn`", self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) ) }; builder.into_diagnostic(message); @@ -865,7 +882,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { builder.into_diagnostic(format_args!( "Cannot assign to attribute `{}` on type `{}` whose `__set__` method returns `Never`/`NoReturn`", self.attribute, - self.object_ty.display(db), + self.object_ty.display(db, env), )); } } @@ -900,9 +917,9 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { lint: &INVALID_ASSIGNMENT, message: format!( "Cannot assign object of type `{}` to attribute `{}` on type `{}`", - value_ty.display(db), + value_ty.display(db, env), self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) ), info: "This assignment implicitly calls a custom `__setattr__` method", argument_ranges: &[self.target.range(), self.value.range()], @@ -919,13 +936,13 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { builder.into_diagnostic(format_args!( "Unresolved attribute `{}` on type `{}`.", self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) )); } else { builder.into_diagnostic(format_args!( "Unresolved attribute `{}` on type `{}`", self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) )); } } @@ -939,7 +956,7 @@ impl<'db> AssignmentAttributeWriteEvaluator<'_, 'db, '_, '_> { builder.into_diagnostic(format_args!( "Cannot assign to instance attribute `{}` from the class object `{}`", self.attribute, - self.object_ty.display(db) + self.object_ty.display(db, env) )); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs index abb6c7df33..54745b96fa 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/binary_expressions.rs @@ -1,8 +1,9 @@ +use crate::Db; +use crate::ProgramEnvironment; use compact_str::CompactString; use ruff_python_ast::{self as ast, AnyNodeRef}; use super::TypeInferenceBuilder; -use crate::Db; use crate::types::call::CallArguments; use crate::types::constraints::ConstraintSetBuilder; use crate::types::cyclic::CycleDetector; @@ -62,6 +63,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { tcx: TypeContext<'db>, ) -> Type<'db> { let db = self.db(); + let env = self.program_environment(); let ast::ExprBinOp { left, op, @@ -84,7 +86,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // `TypeAlias`, which uses `X | Y` syntax, where the returned type is not actually a union. // And attempting to enforce this more tightly showed a lot of potential false positives in // the ecosystem. - if left_ty.is_equivalent_to(db, right_ty) { + if left_ty.is_equivalent_to(db, env, right_ty) { left_ty } else { UnionTypeInstance::from_value_expression_types( @@ -106,6 +108,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { right: &ast::Expr, tcx: TypeContext<'db>, ) -> BinaryExpressionOperandTypes<'db> { + let db = self.db(); // As a special case, pass `tcx` to binary operands that are collection literals/displays. // Note that it's not correct to pass it to all binary operands, for example: // ``` @@ -138,7 +141,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Type::TypedDict(typed_dict) = right_ty && let Some(ty) = self.try_typed_dict_pep_584_dunder( left, - typed_dict.to_partial(self.db()), + typed_dict.to_partial(db), typed_dict, "__ror__", ) @@ -160,7 +163,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { && matches!(right, ast::Expr::Dict(_)) && let Some(ty) = self.try_typed_dict_pep_584_dunder( right, - typed_dict.to_partial(self.db()), + typed_dict.to_partial(db), typed_dict, "__or__", ) @@ -182,21 +185,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { dunder_name: &str, ) -> Option> { let db = self.db(); - let update_ty = self.speculate_without_diagnostics().infer_expression( update, TypeContext::new(Some(Type::TypedDict(update_context_typed_dict))), ); + let env = self.program_environment(); Type::TypedDict(result_typed_dict) .try_call_dunder( db, + env, dunder_name, CallArguments::positional([update_ty]), TypeContext::default(), ) .ok() - .map(|bindings| bindings.return_type(db)) + .map(|bindings| bindings.return_type(db, env)) } /// Handle `TypedDict |= value` before the normal `__ior__` path runs. @@ -213,6 +217,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { value_expr: &ast::Expr, infer_value_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, ) -> Option> { + let db = self.db(); if assignment.op != ast::Operator::BitOr { return None; } @@ -234,7 +239,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } // Subset updates use the mutation-safe patch as context. - let update_patch = typed_dict.to_update_patch(self.db()); + let update_patch = typed_dict.to_update_patch(db); if self .try_typed_dict_pep_584_dunder(value_expr, update_patch, typed_dict, "__ior__") .is_some() @@ -256,16 +261,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// otherwise returns the union of all results. pub(super) fn map_constrained_typevar_constraints( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar: Type<'db>, constraints: TypeVarConstraints<'db>, mut op: impl FnMut(Type<'db>) -> Option>, ) -> Option> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut any_different = false; for constraint in constraints.elements(db) { let result = op(*constraint)?; - if !result.is_equivalent_to(db, *constraint) { + if !result.is_equivalent_to(db, env, *constraint) { any_different = true; } builder = builder.add(result); @@ -305,6 +311,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { op: ast::Operator, visitor: &BinaryExpressionVisitor<'db>, ) -> Option> { + let env = self.program_environment(); let db = self.db(); // Check for division by zero; this doesn't change the inferred type for the expression, but @@ -322,7 +329,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } match (left_ty, right_ty, op) { - (Type::Union(lhs_union), rhs, _) => lhs_union.try_map(db, |lhs_element| { + (Type::Union(lhs_union), rhs, _) => lhs_union.try_map(db, env, |lhs_element| { self.infer_binary_expression_type_impl( node, emitted_division_by_zero_diagnostic, @@ -332,7 +339,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { visitor, ) }), - (lhs, Type::Union(rhs_union), _) => rhs_union.try_map(db, |rhs_element| { + (lhs, Type::Union(rhs_union), _) => rhs_union.try_map(db, env, |rhs_element| { self.infer_binary_expression_type_impl( node, emitted_division_by_zero_diagnostic, @@ -366,13 +373,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }), (Type::TypedDict(left_typed_dict), rhs, ast::Operator::BitOr) - if rhs.is_assignable_to(db, Type::TypedDict(left_typed_dict)) => + if rhs.is_assignable_to(db, env, Type::TypedDict(left_typed_dict)) => { Some(Type::TypedDict(left_typed_dict)) } (lhs, Type::TypedDict(right_typed_dict), ast::Operator::BitOr) - if lhs.is_assignable_to(db, Type::TypedDict(right_typed_dict)) => + if lhs.is_assignable_to(db, env, Type::TypedDict(right_typed_dict)) => { Some(Type::TypedDict(right_typed_dict)) } @@ -416,10 +423,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { (Type::TypeVar(left_tvar), Type::TypeVar(right_tvar), _) if left_tvar.identity(db) == right_tvar.identity(db) => { - match left_tvar.typevar(db).bound_or_constraints(db) { + match left_tvar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { Self::map_constrained_typevar_constraints( db, + env, left_ty, constraints, |constraint| { @@ -434,7 +442,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } // For bounded TypeVars or unconstrained TypeVars, fall through to the default handling. - _ => Type::try_call_bin_op_return_type(db, left_ty, op, right_ty), + _ => Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty), } } @@ -446,10 +454,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // TODO: We expect to replace this with more general support once we migrate to the new // solver. (Type::TypeVar(left_tvar), rhs, _) if !rhs.is_type_var() => { - match left_tvar.typevar(db).bound_or_constraints(db) { + match left_tvar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { Self::map_constrained_typevar_constraints( db, + env, left_ty, constraints, |constraint| { @@ -465,17 +474,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } // For bounded TypeVars or unconstrained TypeVars, fall through to the default handling. - _ => Type::try_call_bin_op_return_type(db, left_ty, op, right_ty), + _ => Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty), } } // When the right operand is a constrained TypeVar and the left operand is not a TypeVar, // we check if each constraint supports the operation with the left operand. (lhs, Type::TypeVar(right_tvar), _) if !lhs.is_type_var() => { - match right_tvar.typevar(db).bound_or_constraints(db) { + match right_tvar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { Self::map_constrained_typevar_constraints( db, + env, right_ty, constraints, |constraint| { @@ -491,7 +501,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } // For bounded TypeVars or unconstrained TypeVars, fall through to the default handling. - _ => Type::try_call_bin_op_return_type(db, left_ty, op, right_ty), + _ => Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty), } } @@ -502,7 +512,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // positional arguments get. In those cases we need to explicitly delegate to the base // type, so that it hits the `Type::Union` branches above. (Type::NewTypeInstance(newtype), rhs, _) => { - Type::try_call_bin_op_return_type(db, left_ty, op, right_ty).or_else(|| { + Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty).or_else(|| { self.infer_binary_expression_type_impl( node, emitted_division_by_zero_diagnostic, @@ -514,7 +524,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }) } (lhs, Type::NewTypeInstance(newtype), _) => { - Type::try_call_bin_op_return_type(db, left_ty, op, right_ty).or_else(|| { + Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty).or_else(|| { self.infer_binary_expression_type_impl( node, emitted_division_by_zero_diagnostic, @@ -548,7 +558,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { n.as_i64() .checked_add(m.as_i64()) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), ), ( @@ -559,7 +569,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { n.as_i64() .checked_sub(m.as_i64()) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), ), ( @@ -570,14 +580,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { n.as_i64() .checked_mul(m.as_i64()) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), ), ( LiteralValueTypeKind::Int(_), LiteralValueTypeKind::Int(_), ast::Operator::Div, - ) => Some(KnownClass::Float.to_instance(db)), + ) => Some(KnownClass::Float.to_instance(db, env)), ( LiteralValueTypeKind::Int(n), @@ -594,7 +604,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { q = q.map(|q| q - 1); } q.map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)) + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)) }), ( @@ -612,7 +622,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { r = r.map(|x| x + m.as_i64()); } r.map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)) + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)) }), ( @@ -621,13 +631,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ast::Operator::Pow, ) => Some({ if m.as_i64() < 0 { - KnownClass::Float.to_instance(db) + KnownClass::Float.to_instance(db, env) } else { u32::try_from(m.as_i64()) .ok() .and_then(|m| n.as_i64().checked_pow(m)) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)) + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)) } }), @@ -799,7 +809,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .filter(|&m| m <= headroom) .and_then(|m| n.checked_shl(m)) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), ) } @@ -814,12 +824,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Err(_) if m.as_i64() > 0 => { Type::int_literal(if n >= 0 { 0 } else { -1 }) } - Err(_) => KnownClass::Int.to_instance(db), + Err(_) => KnownClass::Int.to_instance(db, env), }; Some(result) } - _ => Type::try_call_bin_op_return_type(db, left_ty, op, right_ty), + _ => Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty), }; result.map(|result| match result { @@ -837,8 +847,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) => { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let left = constraints.load(db, left.constraints(db)); - let right = constraints.load(db, right.constraints(db)); + let left = constraints.load(db, env, left.constraints(db)); + let right = constraints.load(db, env, right.constraints(db)); left.and(db, constraints, || right) }); Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( @@ -853,8 +863,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) => { let constraints = ConstraintSetBuilder::new(); let result = constraints.into_owned(|constraints| { - let left = constraints.load(db, left.constraints(db)); - let right = constraints.load(db, right.constraints(db)); + let left = constraints.load(db, env, left.constraints(db)); + let right = constraints.load(db, env, right.constraints(db)); left.or(db, constraints, || right) }); Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( @@ -894,7 +904,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ), ast::Operator::BitOr, ) => { - if left_ty.is_equivalent_to(db, right_ty) { + if left_ty.is_equivalent_to(db, env, right_ty) { Some(left_ty) } else { Some(UnionTypeInstance::from_value_expression_types( @@ -951,13 +961,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ast::Operator::BitOr, ) => Type::try_call_bin_op_with_policy( db, + env, left_ty, ast::Operator::BitOr, right_ty, MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, ) .ok() - .map(|binding| binding.return_type(db)), + .map(|binding| binding.return_type(db, env)), // We've handled all of the special cases that we support for literals, so we need to // fall back on looking for dunder methods on one of the operand types. @@ -1017,7 +1028,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { | Type::TypeForm(_) | Type::TypedDict(_), op, - ) => Type::try_call_bin_op_return_type(db, left_ty, op, right_ty), + ) => Type::try_call_bin_op_return_type(db, env, left_ty, op, right_ty), } } @@ -1055,7 +1066,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { 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(db) + left.display(db, self.program_environment()) )); } diff --git a/crates/ty_python_semantic/src/types/infer/builder/class.rs b/crates/ty_python_semantic/src/types/infer/builder/class.rs index fda696d8fc..1ac95a6db5 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/class.rs @@ -1,3 +1,5 @@ +use crate::Db; +use crate::ProgramEnvironment; use crate::place::Place; use crate::types::{ CallArguments, DataclassParams, KnownClass, KnownInstanceType, MemberLookupPolicy, @@ -83,6 +85,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { class_node: &ast::StmtClassDef, definition: Definition<'db>, ) { + let env = self.program_environment(); let ast::StmtClassDef { range: _, node_index: _, @@ -104,11 +107,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let body_scope = self .index .node_scope(NodeWithScopeRef::Class(class_node)) - .to_scope_id(db, self.file()); + .to_scope_id(db, self.python_file()); - let maybe_known_class = KnownClass::try_from_file_and_name(db, self.file(), name); + let maybe_known_class = KnownClass::try_from_file_and_name(db, self.python_file(), name); - let known_module = || file_to_module(db, self.file()).and_then(|module| module.known(db)); + let known_module = + || file_to_module(db, self.python_file()).and_then(|module| module.known(db)); let in_typing_module = || { matches!( known_module(), @@ -181,7 +185,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .as_function_literal() .is_some_and(|function| function.is_known(db, KnownFunction::Dataclass)) { - dataclass_params = Some(DataclassParams::default_params(db)); + dataclass_params = Some(DataclassParams::default_params(db, env)); continue; } @@ -273,21 +277,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { dataclass_transformer_params, total_ordering, ); - let decorator_result = apply_class_decorator(db, decorator_ty, original_class_ty); + let decorator_result = apply_class_decorator(db, env, decorator_ty, original_class_ty); let decorated_ty = match &decorator_result { Ok(return_ty) => *return_ty, - Err(error) => error.return_type(db), + Err(error) => error.return_type(db, env), }; if is_unknown_decorator_result(db, decorated_ty) { if !preserve_binding_for_unknown_result( db, + env, decorator_ty, decorator_call_ty(decorator), decorated_ty, ) { metadata_applies_to_original_class = false; } - } else if !type_retains_original_class(db, original_class_ty, decorated_ty) { + } else if !type_retains_original_class(db, env, original_class_ty, decorated_ty) { metadata_applies_to_original_class = false; } @@ -321,13 +326,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { decorator_result } - _ => apply_class_decorator(db, decorator_ty, inferred_ty), + _ => apply_class_decorator(db, env, decorator_ty, inferred_ty), }; let decorated_ty = match decorator_result { Ok(return_ty) => return_ty, Err(CallError(_, bindings)) => { bindings.report_diagnostics(&self.context, decorator_node.into()); - bindings.return_type(db) + bindings.return_type(db, env) } }; let decorated_ty = match decorated_ty { @@ -339,15 +344,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let should_preserve_binding = is_unknown_decorator_result(db, decorated_ty) && preserve_binding_for_unknown_result( db, + env, decorator_ty, decorator_call_ty(decorator_node), decorated_ty, ); inferred_ty = if should_preserve_binding { inferred_ty - } else if class_decorator_preserves_class_binding(db, original_class_ty, decorated_ty) { + } else if class_decorator_preserves_class_binding( + db, + env, + original_class_ty, + decorated_ty, + ) { merge_class_preserving_decorator_result( db, + env, original_class_ty, inferred_ty, decorated_ty, @@ -448,14 +460,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } fn apply_class_decorator<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, decorator_ty: Type<'db>, decorated_ty: Type<'db>, ) -> Result, CallError<'db>> { let call_arguments = CallArguments::positional([decorated_ty]); decorator_ty - .try_call(db, &call_arguments) - .map(|bindings| bindings.return_type(db)) + .try_call(db, env, &call_arguments) + .map(|bindings| bindings.return_type(db, env)) } /// Return true if a decorator result still binds the name to the original class. @@ -472,7 +485,8 @@ fn apply_class_decorator<'db>( /// This also accepts metaclass-shaped results such as `type[C]`, because those still describe the /// original class object even if the decorator call produced a `SubclassOf` type internally. fn class_decorator_preserves_class_binding<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, original_class: Type<'db>, decorated_class: Type<'db>, ) -> bool { @@ -489,26 +503,26 @@ fn class_decorator_preserves_class_binding<'db>( } Type::SubclassOf(subclass_of) => subclass_of .subclass_of() - .into_class(db) + .into_class(db, env) .is_some_and(|class| class == original_literal.default_specialization(db)), Type::Divergent(_) => true, - Type::Union(union) => union - .elements(db) - .iter() - .all(|element| class_decorator_preserves_class_binding(db, original_class, *element)), + Type::Union(union) => union.elements(db).iter().all(|element| { + class_decorator_preserves_class_binding(db, env, original_class, *element) + }), Type::TypeAlias(alias) => { - class_decorator_preserves_class_binding(db, original_class, alias.value_type(db)) + class_decorator_preserves_class_binding(db, env, original_class, alias.value_type(db)) } - _ => SubclassOfType::try_from_type(db, original_class).is_some_and(|original_meta_type| { - decorated_class.is_equivalent_to(db, original_meta_type) - }), + _ => SubclassOfType::try_from_type(db, env, original_class).is_some_and( + |original_meta_type| decorated_class.is_equivalent_to(db, env, original_meta_type), + ), } } /// Return true if a type still contains the original class object, even if it also carries extra /// intersection members. fn type_retains_original_class<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, original_class: Type<'db>, decorated_class: Type<'db>, ) -> bool { @@ -516,15 +530,15 @@ fn type_retains_original_class<'db>( Type::Intersection(intersection) => intersection .positive(db) .iter() - .any(|element| type_retains_original_class(db, original_class, *element)), + .any(|element| type_retains_original_class(db, env, original_class, *element)), Type::Union(union) => union .elements(db) .iter() - .all(|element| type_retains_original_class(db, original_class, *element)), + .all(|element| type_retains_original_class(db, env, original_class, *element)), Type::TypeAlias(alias) => { - type_retains_original_class(db, original_class, alias.value_type(db)) + type_retains_original_class(db, env, original_class, alias.value_type(db)) } - _ => class_decorator_preserves_class_binding(db, original_class, decorated_class), + _ => class_decorator_preserves_class_binding(db, env, original_class, decorated_class), } } @@ -547,21 +561,22 @@ fn type_retains_original_class<'db>( /// `decorator_factory` carries the static information that tells us whether an unknown result can /// be preserved. fn preserve_binding_for_unknown_result<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, decorator_ty: Type<'db>, decorator_call_ty: Option>, decorator_result_ty: Type<'db>, ) -> bool { - ClassDecoratorUnknownResultPolicy::from_decorator(db, decorator_ty, decorator_result_ty) + ClassDecoratorUnknownResultPolicy::from_decorator(db, env, decorator_ty, decorator_result_ty) == ClassDecoratorUnknownResultPolicy::PreserveBinding || decorator_call_ty.is_some_and(|ty| { - ClassDecoratorUnknownResultPolicy::from_decorator(db, ty, decorator_result_ty) + ClassDecoratorUnknownResultPolicy::from_decorator(db, env, ty, decorator_result_ty) == ClassDecoratorUnknownResultPolicy::PreserveBinding }) } /// Return true if applying a class decorator produced no useful replacement type. -fn is_unknown_decorator_result<'db>(db: &'db dyn crate::Db, ty: Type<'db>) -> bool { +fn is_unknown_decorator_result<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { ty.is_unknown() || is_unknown_class_object_decorator_result(db, ty) } @@ -578,7 +593,7 @@ fn is_unknown_decorator_result<'db>(db: &'db dyn crate::Db, ty: Type<'db>) -> bo /// @decorator /// class C: ... /// ``` -fn is_unknown_class_object_decorator_result<'db>(db: &'db dyn crate::Db, ty: Type<'db>) -> bool { +fn is_unknown_class_object_decorator_result<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { let Type::SubclassOf(subclass_of) = ty.resolve_type_alias(db) else { return false; }; @@ -610,7 +625,8 @@ impl ClassDecoratorUnknownResultPolicy { /// application result is unknown. Explicit return annotations are trusted as replacement /// intent. fn from_decorator<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, decorator_ty: Type<'db>, decorator_result_ty: Type<'db>, ) -> Self { @@ -618,7 +634,7 @@ impl ClassDecoratorUnknownResultPolicy { return Self::ReplaceBinding; } - Self::known_from_decorator(db, decorator_ty, decorator_result_ty) + Self::known_from_decorator(db, env, decorator_ty, decorator_result_ty) .unwrap_or(Self::ReplaceBinding) } @@ -640,7 +656,8 @@ impl ClassDecoratorUnknownResultPolicy { /// Callable instances and protocols delegate the decision to their `__call__` member, because /// the decorator value itself is not the function that receives the class. fn known_from_decorator<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, decorator_ty: Type<'db>, decorator_result_ty: Type<'db>, ) -> Option { @@ -663,6 +680,7 @@ impl ClassDecoratorUnknownResultPolicy { let call_symbol = decorator_ty .member_lookup_with_policy( db, + env, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -672,7 +690,7 @@ impl ClassDecoratorUnknownResultPolicy { && place.is_definitely_defined() { Some( - Self::known_from_decorator(db, place.ty, decorator_result_ty) + Self::known_from_decorator(db, env, place.ty, decorator_result_ty) .unwrap_or(Self::ReplaceBinding), ) } else { @@ -681,7 +699,7 @@ impl ClassDecoratorUnknownResultPolicy { } Type::Union(union) => Some( if union.elements(db).iter().all(|element| { - Self::known_from_decorator(db, *element, decorator_result_ty) + Self::known_from_decorator(db, env, *element, decorator_result_ty) == Some(Self::PreserveBinding) }) { Self::PreserveBinding @@ -690,7 +708,7 @@ impl ClassDecoratorUnknownResultPolicy { }, ), Type::TypeAlias(alias) => Some( - Self::known_from_decorator(db, alias.value_type(db), decorator_result_ty) + Self::known_from_decorator(db, env, alias.value_type(db), decorator_result_ty) .unwrap_or(Self::ReplaceBinding), ), Type::Callable(callable) => Some(match callable.provenance(db) { @@ -745,13 +763,14 @@ impl ClassDecoratorUnknownResultPolicy { /// members instead of collapsing back to the undecorated class when a later decorator simply /// returns the original class object again. fn merge_class_preserving_decorator_result<'db>( - db: &'db dyn crate::Db, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, original_class: Type<'db>, current_binding: Type<'db>, decorated_binding: Type<'db>, ) -> Type<'db> { if current_binding == original_class - || type_retains_original_class(db, original_class, current_binding) + || type_retains_original_class(db, env, original_class, current_binding) { current_binding } else { diff --git a/crates/ty_python_semantic/src/types/infer/builder/dict.rs b/crates/ty_python_semantic/src/types/infer/builder/dict.rs index cc20998b95..9a6c68ac1b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/dict.rs @@ -17,6 +17,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { collection_expr: Option>, call_expression_tcx: TypeContext<'db>, ) -> Option> { + let db = self.db(); if !arguments.args.is_empty() { return None; } @@ -25,9 +26,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // then validate and return the TypedDict type. This also covers `dict(**src)` when `src` // is `TypedDict`-shaped. if let Some(tcx) = call_expression_tcx.annotation - && let Some(typed_dict) = tcx - .filter_union(self.db(), Type::is_typed_dict) - .as_typed_dict() + && let Some(typed_dict) = tcx.filter_union(db, Type::is_typed_dict).as_typed_dict() { // Only speculate the `**kwargs` applicability check. Assignability handles inputs that // are already valid for the target, including gradual and bottom types. The additional @@ -41,19 +40,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // back. let supports_typed_dict_context = { let mut speculative_builder = self.speculate_without_diagnostics(); + let env = speculative_builder.program_environment(); infer_unpacked_keyword_types(arguments, |expr, tcx| { speculative_builder.infer_expression(expr, tcx) }) .into_iter() .flatten() .all(|keyword_ty| { - keyword_ty - .is_assignable_to(speculative_builder.db(), Type::TypedDict(typed_dict)) - || extract_unpacked_typed_dict_keys_from_value_type( - speculative_builder.db(), - keyword_ty, - ) - .is_some() + keyword_ty.is_assignable_to(db, env, Type::TypedDict(typed_dict)) + || extract_unpacked_typed_dict_keys_from_value_type(db, env, keyword_ty) + .is_some() }) }; diff --git a/crates/ty_python_semantic/src/types/infer/builder/dynamic_class.rs b/crates/ty_python_semantic/src/types/infer/builder/dynamic_class.rs index 18f8c325df..2eae58737a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/dynamic_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/dynamic_class.rs @@ -48,15 +48,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { kind: DynamicClassKind, ) -> Option]>> { let db = self.db(); + let env = self.program_environment(); let fn_name = kind.function_name(); let formal_parameter_type = match kind { - DynamicClassKind::TypeCall => Type::homogeneous_tuple(db, Type::object()), + DynamicClassKind::TypeCall => Type::homogeneous_tuple(db, env, Type::object()), DynamicClassKind::NewClass => { - KnownClass::Iterable.to_specialized_instance(db, &[Type::object()]) + KnownClass::Iterable.to_specialized_instance(db, env, &[Type::object()]) } }; - if !bases_type.is_assignable_to(db, formal_parameter_type) + if !bases_type.is_assignable_to(db, env, formal_parameter_type) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, bases_node) { let mut diagnostic = builder.into_diagnostic(format_args!( @@ -64,12 +65,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )); diagnostic.set_primary_annotation_message(format_args!( "Expected `{}`, found `{}`", - formal_parameter_type.display(db), - bases_type.display(db) + formal_parameter_type.display(db, env), + bases_type.display(db, env) )); } - extract_fixed_length_iterable_element_types(db, bases_node, |expr| { + extract_fixed_length_iterable_element_types(db, env, bases_node, |expr| { self.expression_type(expr) }) } @@ -95,13 +96,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .map(|tuple| tuple.elts.as_slice()); let mut disjoint_bases = IncompatibleBases::default(); let fn_name = kind.function_name(); + let env = self.context.program_environment(); for (idx, base) in bases.iter().enumerate() { let diagnostic_node = bases_tuple_elts .and_then(|elts| elts.get(idx)) .unwrap_or(bases_node); - let Some(class_base) = ClassBase::try_from_type(db, *base, None) else { + let Some(class_base) = ClassBase::try_from_type(db, env, *base, None) else { continue; }; @@ -114,7 +116,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )); diagnostic.set_primary_annotation_message(format_args!( "Has type `{}`", - base.display(db) + base.display(db, env) )); match class_base { ClassBase::Generic => { @@ -147,7 +149,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )); diagnostic.set_primary_annotation_message(format_args!( "Has type `{}`", - base.display(db) + base.display(db, env) )); diagnostic.info(format_args!( "Classes created via `{fn_name}` cannot be protocols", @@ -176,7 +178,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if kind == DynamicClassKind::TypeCall && let Some((static_class, _)) = class_type.static_class_literal(db) - && is_enum_class_by_inheritance(db, static_class) + && is_enum_class_by_inheritance(db, env, static_class) { if let Some(builder) = self.context.report_lint(&INVALID_BASE, diagnostic_node) @@ -185,7 +187,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .into_diagnostic("Invalid base for class created via `type()`"); diagnostic.set_primary_annotation_message(format_args!( "Has type `{}`", - base.display(db) + base.display(db, env) )); diagnostic.info("Creating an enum class via `type()` is not supported"); diagnostic.info(format_args!( @@ -220,14 +222,14 @@ pub(super) fn report_dynamic_mro_errors<'db>( bases: &ast::Expr, ) -> bool { let db = context.db(); + let env = context.program_environment(); let Err(error) = dynamic_class.try_mro(db) else { return true; }; - let bases_display = dynamic_class .explicit_bases(db) .iter() - .map(|base| base.display(db)) + .map(|base| base.display(db, env)) .join(", "); report_mro_error_kind( context, @@ -251,7 +253,7 @@ pub(super) fn report_inconsistent_dynamic_generic_bases<'db>( bases: &ast::Expr, ) { let db = context.db(); - let explicit_bases = dynamic_class.explicit_bases(db); + let explicit_bases = dynamic_class.explicit_bases(context.db()); let base_nodes = bases .as_tuple_expr() .map(|tuple| tuple.elts.as_slice()) @@ -292,22 +294,23 @@ pub(super) fn report_mro_error_kind<'db>( let Some(bases) = bases_expr else { return; }; + let env = context.program_environment(); let bases_tuple_elts = bases.as_tuple_expr().map(|tuple| tuple.elts.as_slice()); for (idx, base_type) in invalid_bases { - let instance_of_type = KnownClass::Type.to_instance(db); + let instance_of_type = KnownClass::Type.to_instance(db, env); let specific_base = bases_tuple_elts.and_then(|elts| elts.get(*idx)); let diagnostic_range = specific_base .map(ast::Expr::range) .unwrap_or_else(|| bases.range()); - if base_type.is_assignable_to(db, instance_of_type) { + if base_type.is_assignable_to(db, env, instance_of_type) { if let Some(builder) = context.report_lint(&UNSUPPORTED_DYNAMIC_BASE, diagnostic_range) { let mut diagnostic = builder.into_diagnostic("Unsupported class base"); diagnostic.set_primary_annotation_message(format_args!( "Has type `{}`", - base_type.display(db) + base_type.display(db, env) )); diagnostic.info(format_args!( "ty cannot determine a MRO for class `{class_name}` due to this base", @@ -317,7 +320,7 @@ pub(super) fn report_mro_error_kind<'db>( } else if let Some(builder) = context.report_lint(&INVALID_BASE, diagnostic_range) { let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid class base with type `{}`", - base_type.display(db) + base_type.display(db, env) )); if specific_base.is_none() { diagnostic @@ -333,12 +336,13 @@ pub(super) fn report_mro_error_kind<'db>( } DynamicMroErrorKind::DuplicateBases(duplicates) => { if let Some(builder) = context.report_lint(&DUPLICATE_BASE, call_expr) { + let env = context.program_environment(); builder.into_diagnostic(format_args!( "Duplicate base class{maybe_s} {dupes} in class `{class_name}`", maybe_s = if duplicates.len() == 1 { "" } else { "es" }, dupes = duplicates .iter() - .map(|base: &ClassBase<'_>| base.display(db)) + .map(|base: &ClassBase<'_>| base.display(db, env)) .join(", "), )); } diff --git a/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs b/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs index 2c43cf2ce9..39cf9db71e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/enum_call.rs @@ -6,11 +6,12 @@ use rustc_hash::FxHashSet; use ty_python_core::definition::Definition; use crate::{ - Db, Program, + Db, ProgramEnvironment, types::{ ClassLiteral, KnownClass, Type, TypeContext, UnionType, class::{DynamicEnumAnchor, DynamicEnumLiteral, EnumSpec}, constraints::ConstraintSetBuilder, + context::InferContext, diagnostic::{ INVALID_ARGUMENT_TYPE, INVALID_BASE, MISSING_ARGUMENT, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, report_mismatched_type_name, @@ -137,16 +138,20 @@ fn enum_functional_call_keyword_is_valid(name: &str, python_version: PythonVersi /// /// This includes the string form, iterables of strings, iterables of /// iterable-like `(name, value)` pairs, and mappings from `str` to values. -fn enum_names_type(db: &dyn Db) -> Type<'_> { - let str_type = KnownClass::Str.to_instance(db); - let iterable_str = KnownClass::Iterable.to_specialized_instance(db, &[str_type]); - let iterable_object = KnownClass::Iterable.to_specialized_instance(db, &[Type::object()]); +fn enum_names_type<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + let str_type = KnownClass::Str.to_instance(db, env); + let iterable_str = KnownClass::Iterable.to_specialized_instance(db, env, &[str_type]); + let iterable_object = KnownClass::Iterable.to_specialized_instance(db, env, &[Type::object()]); let iterable_iterable_object = - KnownClass::Iterable.to_specialized_instance(db, &[iterable_object]); - let mapping_str_object = KnownClass::Mapping - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::object()]); + KnownClass::Iterable.to_specialized_instance(db, env, &[iterable_object]); + let mapping_str_object = KnownClass::Mapping.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::object()], + ); UnionType::from_elements( db, + env, [ str_type, iterable_str, @@ -161,16 +166,18 @@ fn enum_names_type(db: &dyn Db) -> Type<'_> { /// `StrEnum` ignores `start` and uses the lowercased member name. Other enum kinds use the /// literal `start` value when available, and widen to `int` when `start` is a non-literal int. fn first_enum_auto_value<'db>( - db: &'db dyn Db, + context: &InferContext<'db, '_>, base_class: KnownClass, name: &str, start: EnumStart, ) -> Type<'db> { + let db = context.db(); + let env = context.program_environment(); match base_class { KnownClass::StrEnum => Type::string_literal(db, &*name.to_lowercase()), _ => match start { EnumStart::Literal(start) => Type::int_literal(start), - EnumStart::DynamicInt => KnownClass::Int.to_instance(db), + EnumStart::DynamicInt => KnownClass::Int.to_instance(db, env), }, } } @@ -183,16 +190,18 @@ fn first_enum_auto_value<'db>( /// - `Flag`/`IntFlag`: next highest power of two /// - Others: `last_value + 1` fn next_auto_value<'db>( - db: &'db dyn Db, + context: &InferContext<'db, '_>, base_class: KnownClass, name: &str, last_int_value: Option, ) -> Type<'db> { + let db = context.db(); + let env = context.program_environment(); match base_class { KnownClass::StrEnum => Type::string_literal(db, &*name.to_lowercase()), _ => { let Some(last) = last_int_value else { - return KnownClass::Int.to_instance(db); + return KnownClass::Int.to_instance(db, env); }; match base_class { KnownClass::Flag | KnownClass::IntFlag => { @@ -205,32 +214,32 @@ fn next_auto_value<'db>( .checked_shl(shift) .and_then(|value| i64::try_from(value).ok()) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)) + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)) } } _ => last .checked_add(1) .map(Type::int_literal) - .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + .unwrap_or_else(|| KnownClass::Int.to_instance(db, env)), } } } } -fn enum_members_from_names( - db: &dyn Db, +fn enum_members_from_names<'db>( + context: &InferContext<'db, '_>, names: Vec, start: EnumStart, base_class: KnownClass, -) -> Vec<(Name, Type<'_>)> { +) -> Vec<(Name, Type<'db>)> { let mut members = Vec::with_capacity(names.len()); let mut last_int_value = None; for (index, name) in names.into_iter().enumerate() { let value = if index == 0 { - first_enum_auto_value(db, base_class, name.as_str(), start) + first_enum_auto_value(context, base_class, name.as_str(), start) } else { - next_auto_value(db, base_class, name.as_str(), last_int_value) + next_auto_value(context, base_class, name.as_str(), last_int_value) }; last_int_value = value.as_int_literal(); members.push((name, value)); @@ -247,10 +256,11 @@ fn enum_members_from_names( /// Returns `None` when the mixin is not a supported builtin or when the generated values are not /// compatible with the corresponding builtin conversion. fn apply_generated_type_mixin_member_values<'db>( - db: &'db dyn Db, + context: &InferContext<'db, '_>, mixin_type: Type<'_>, members: Vec<(Name, Type<'db>)>, ) -> Option)>> { + let db = context.db(); let Type::ClassLiteral(ClassLiteral::Static(class)) = mixin_type else { return None; }; @@ -261,10 +271,11 @@ fn apply_generated_type_mixin_member_values<'db>( return None; }; + let env = context.program_environment(); members .into_iter() .map(|(name, value)| { - if !value.is_assignable_to(db, KnownClass::Int.to_instance(db)) { + if !value.is_assignable_to(db, env, KnownClass::Int.to_instance(db, env)) { return None; } @@ -273,7 +284,7 @@ fn apply_generated_type_mixin_member_values<'db>( { Type::string_literal(db, literal.to_compact_string()) } else { - mixin_class.to_instance(db) + mixin_class.to_instance(db, env) }; Some((name, value)) }) @@ -295,16 +306,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { node_index: _, } = &call_expr.arguments; - let base_name = base_class.name(db); - let python_version = Program::get(db).python_version(db); - for kw in keywords { - if let Some(name) = &kw.arg - && !enum_functional_call_keyword_is_valid(name.as_str(), python_version) + let Some(name) = &kw.arg else { + continue; + }; + let env = self.program_environment(); + let python_version = env.python_version(db); + if !enum_functional_call_keyword_is_valid(name.as_str(), python_version) && let Some(builder) = self.context.report_lint(&UNKNOWN_ARGUMENT, kw) { builder.into_diagnostic(format_args!( "Argument `{name}` does not match any known parameter of function `{base_name}`", + base_name = base_class.name(env.python_version(db)), )); } } @@ -323,7 +336,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .report_lint(&PARAMETER_ALREADY_ASSIGNED, keyword) { builder.into_diagnostic(format_args!( - "Multiple values provided for parameter `value` of `{base_name}()`" + "Multiple values provided for parameter `value` of `{base_name}()`", + base_name = base_class.name(self.program_environment().python_version(db)), )); } if args.len() >= 2 @@ -333,7 +347,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .report_lint(&PARAMETER_ALREADY_ASSIGNED, keyword) { builder.into_diagnostic(format_args!( - "Multiple values provided for parameter `names` of `{base_name}()`" + "Multiple values provided for parameter `names` of `{base_name}()`", + base_name = base_class.name(self.program_environment().python_version(db)), )); } @@ -344,6 +359,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let name_arg = name_arg?; + let env = self.program_environment(); let Some(names_arg) = names_arg else { for arg in args { @@ -361,13 +377,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_enum_mixin_argument(&keyword.value, base_class); } + let python_version = self.program_environment().python_version(db); if let Some(builder) = self.context.report_lint(&MISSING_ARGUMENT, call_expr) { builder.into_diagnostic(format_args!( - "Missing required argument `names` to `{base_name}()`" + "Missing required argument `names` to `{base_name}()`", + base_name = base_class.name(python_version), )); } - return Some(base_class.to_instance(db)); + return Some(base_class.to_instance(db, env)); }; for arg in args { @@ -395,6 +413,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.into_diagnostic(format_args!( "Too many positional arguments to function `{base_name}`: expected 2, got {}", args.len(), + base_name = base_class.name(self.program_environment().python_version(db)), )); } @@ -403,7 +422,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .as_string_literal() .map(|name_literal| name_literal.value(db)); - if (name.is_some() || name_ty.is_assignable_to(db, KnownClass::Str.to_instance(db))) + if (name.is_some() + || name_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env))) && let Some(definition) = definition && let Some(assigned_name) = definition.name(db) && Some(assigned_name.as_str()) != name @@ -411,7 +431,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { report_mismatched_type_name( &self.context, name_arg, - base_name, + base_class.name(self.program_environment().python_version(db)), &assigned_name, name, name_ty, @@ -429,7 +449,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // Non-literal names use the ordinary `type[EnumSubclass]` overload result // instead of synthesizing a `DynamicEnumLiteral`. let Some(name) = self.infer_enum_name_argument(name_arg, base_class) else { - return SubclassOfType::try_from_type(db, base_class.to_class_literal(db)); + return SubclassOfType::try_from_type(db, env, base_class.to_class_literal(db, env)); }; let anchor = self.create_dynamic_enum_anchor(call_expr, definition, spec); @@ -453,19 +473,20 @@ impl<'db> TypeInferenceBuilder<'db, '_> { base_class: KnownClass, ) -> Option<&'db str> { let db = self.db(); - let base_name = base_class.name(db); let name_type = self.expression_type(name_arg); let Some(name_literal) = name_type.as_string_literal() else { - if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + let env = self.program_environment(); + if !name_type.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid argument to parameter `value` of `{base_name}()`" + "Invalid argument to parameter `value` of `{base_name}()`", + base_name = base_class.name(env.python_version(db)) )); diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", - name_type.display(db) + name_type.display(db, env) )); } return None; @@ -481,14 +502,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return EnumStart::Literal(literal); } - if ty.is_assignable_to(db, KnownClass::Int.to_instance(db)) { + let env = self.program_environment(); + if ty.is_assignable_to(db, env, KnownClass::Int.to_instance(db, env)) { return EnumStart::DynamicInt; } if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, value) { builder.into_diagnostic(format_args!( "Expected `int` for `start` argument, got `{}`", - ty.display(db), + ty.display(db, env), )); } @@ -502,13 +524,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) -> (Option>, bool) { let db = self.db(); let ty = self.expression_type(value); + let env = self.program_environment(); if let Some(class_lit) = ty.as_class_literal() { if class_lit.is_typed_dict(db) && let Some(builder) = self.context.report_lint(&INVALID_BASE, value) { builder.into_diagnostic(format_args!( "TypedDict class `{}` cannot be used as an enum mixin", - ty.display(db), + ty.display(db, env), )); return (None, false); } @@ -516,17 +539,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let Some(mixin_class) = ty.to_class_type(db) else { return (Some(ty), true); }; - let Some(enum_base) = base_class.to_class_literal(db).to_class_type(db) else { + let Some(enum_base) = base_class.to_class_literal(db, env).to_class_type(db) else { return (Some(ty), true); }; let constraints = ConstraintSetBuilder::new(); - if !mixin_class.could_coexist_in_mro_with(db, enum_base, &constraints) + if !mixin_class.could_coexist_in_mro_with(db, env, enum_base, &constraints) && let Some(builder) = self.context.report_lint(&INVALID_BASE, value) { builder.into_diagnostic(format_args!( "Class `{}` cannot be used as an enum mixin with `{}`", mixin_class.name(db), - base_class.name(db), + base_class.name(self.program_environment().python_version(db)), )); return (None, false); } @@ -540,7 +563,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, value) { builder.into_diagnostic(format_args!( "Expected a class for `type` argument, got `{}`", - ty.display(db), + ty.display(db, env), )); } @@ -579,7 +602,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { TypeMixinMemberBehavior::Precise => (known_members.members, true), TypeMixinMemberBehavior::ConvertedValues => { match apply_generated_type_mixin_member_values( - db, + &self.context, mixin_type, known_members.members, ) { @@ -657,7 +680,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .filter(|s| !s.is_empty()) .map(Name::new) .collect(); - let members = enum_members_from_names(db, names, start, base_class); + let members = enum_members_from_names(&self.context, names, start, base_class); return EnumMembersArgParseResult::Known(KnownEnumMembers { members, value_form: EnumMemberValueForm::Generated, @@ -677,7 +700,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return self.parse_enum_members_from_dict(dict, base_class); } - if ty.is_dynamic() || ty.is_assignable_to(db, enum_names_type(db)) { + let env = self.program_environment(); + if ty.is_dynamic() || ty.is_assignable_to(db, env, enum_names_type(db, env)) { EnumMembersArgParseResult::Unknown } else { EnumMembersArgParseResult::Invalid @@ -739,7 +763,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if matches!(form, Some(SequenceEnumMemberForm::Names)) { return EnumMembersArgParseResult::Known(KnownEnumMembers { - members: enum_members_from_names(db, names, start, base_class), + members: enum_members_from_names(&self.context, names, start, base_class), value_form: EnumMemberValueForm::Generated, }); } @@ -756,7 +780,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut last_int_value = Some(0); for (name, value) in explicit_members { let value = if value.is_instance_of(db, KnownClass::Auto) { - next_auto_value(db, base_class, name.as_str(), last_int_value) + next_auto_value(&self.context, base_class, name.as_str(), last_int_value) } else { value }; @@ -783,6 +807,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut members = Vec::with_capacity(dict.items.len()); let mut last_int_value = Some(0); let mut has_opaque_keys = false; + let env = self.program_environment(); for item in &dict.items { let Some(key) = &item.key else { return EnumMembersArgParseResult::Invalid; @@ -790,7 +815,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let key_ty = self.expression_type(key); let Some(string_lit) = key_ty.as_string_literal() else { if key_ty.is_dynamic() - || key_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) + || key_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { has_opaque_keys = true; continue; @@ -800,7 +825,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let name = Name::new(string_lit.value(db)); let raw_value = self.expression_type(&item.value); let value = if raw_value.is_instance_of(db, KnownClass::Auto) { - next_auto_value(db, base_class, name.as_str(), last_int_value) + next_auto_value(&self.context, base_class, name.as_str(), last_int_value) } else { raw_value }; @@ -839,6 +864,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// This is used when the name position is not a known string literal, but /// is still compatible with `str`. fn is_potential_explicit_enum_member(&mut self, elt: &ast::Expr) -> bool { + let db = self.db(); let pair = match elt { ast::Expr::Tuple(tup) => &tup.elts, ast::Expr::List(list) => &list.elts, @@ -847,9 +873,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let [name_expr, _value_expr] = &**pair else { return false; }; - let db = self.db(); let name_ty = self.expression_type(name_expr); - name_ty.is_dynamic() || name_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) + let env = self.program_environment(); + name_ty.is_dynamic() + || name_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) } /// Classifies one element from a sequence-form `names` argument. @@ -865,7 +892,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some((name, value)) = self.parse_explicit_enum_member(elt) { return SequenceEnumMember::PairKnown(name, value); } - if ty.is_dynamic() || ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) { + let env = self.program_environment(); + if ty.is_dynamic() || ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { return SequenceEnumMember::NameOpaque; } if self.is_potential_explicit_enum_member(elt) { @@ -880,16 +908,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { base_class: KnownClass, ) { let db = self.db(); - let base_name = base_class.name(db); + let base_name = base_class.name(self.program_environment().python_version(db)); let names_ty = self.expression_type(names_arg); if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, names_arg) { + let env = self.program_environment(); let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid argument to parameter `names` of `{base_name}()`" )); diagnostic.set_primary_annotation_message(format_args!( "Expected `{}`, found `{}`", - enum_names_type(db).display(db), - names_ty.display(db), + enum_names_type(db, env).display(db, env), + names_ty.display(db, env), )); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs b/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs index fed15f69e8..b38d93780e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs @@ -26,7 +26,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) { let db = self.db(); let file = declaration.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, declaration.python_file(db)).load(db); let range = match declaration.kind(db) { DefinitionKind::AnnotatedAssignment(assignment) => { assignment.annotation(&module).range() @@ -50,7 +50,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { attribute: &str, ) -> Option> { let db = self.db(); - let class_ty = object_ty.nominal_class(db)?; + let env = self.program_environment(); + let class_ty = object_ty.nominal_class(db, env)?; for base in class_ty.iter_mro(db) { let Some(class) = base.into_class() else { @@ -62,16 +63,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let class_body_scope = class_literal.body_scope(db); let class_scope_id = class_body_scope.file_scope_id(db); - let class_index = semantic_index(db, class_body_scope.file(db)); + let class_index = semantic_index(db, class_body_scope.python_file(db)); let place_table = class_index.place_table(class_scope_id); let Some(symbol_id) = place_table.symbol_id(attribute) else { continue; }; let use_def = class_index.use_def_map(class_scope_id); - - let place_and_quals_result = - place_from_declarations(db, use_def.end_of_scope_symbol_declarations(symbol_id)); + let place_and_quals_result = place_from_declarations( + db, + env, + use_def.end_of_scope_symbol_declarations(symbol_id), + ); let Some(declaration) = place_and_quals_result.first_declaration else { continue; @@ -183,11 +186,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { attribute: &str, qualifiers: TypeQualifiers, ) -> bool { + let env = self.program_environment(); + let db = self.db(); if !qualifiers.contains(TypeQualifiers::FINAL) { return false; } - - let db = self.db(); let final_declaration = self.precise_final_attribute_declaration(object_ty, attribute); // TODO: Use the full assignment statement range for these diagnostics instead of @@ -199,10 +202,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let report_not_in_init = || { let is_dataclass_like = object_ty - .nominal_class(db) + .nominal_class(db, env) .or_else(|| object_ty.to_class_type(db)) .and_then(|cls| cls.static_class_literal(db)) - .is_some_and(|(class_literal, _)| class_literal.is_dataclass_like(db)); + .is_some_and(|(class_literal, _)| class_literal.is_dataclass_like(self.db())); let Some(builder) = self .context .report_lint(&INVALID_ASSIGNMENT, target.range()) @@ -211,7 +214,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot assign to final attribute `{attribute}` on type `{}`", - object_ty.display(db) + object_ty.display(db, env) )); diagnostic.set_primary_annotation_message(if is_dataclass_like { "`Final` attributes can only be assigned in the class body, `__init__`, or `__post_init__` on dataclass-like classes" @@ -241,9 +244,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // Final ownership is nominal: checking structural protocol requirements can // incorrectly reject the declaring class's own receiver. let is_current_class_instance = is_self_parameter - && object_ty.nominal_class(db).is_some_and(|object_class| { - object_class.is_subtype_of_class_literal(db, class_ty.class_literal(db)) - }); + && object_ty + .nominal_class(db, env) + .is_some_and(|object_class| { + object_class.is_subtype_of_class_literal(db, class_ty.class_literal(db)) + }); if !is_current_class_instance { report_not_in_init(); return true; @@ -252,7 +257,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some((class_literal, _)) = class_ty.static_class_literal(db) { let class_body_scope = class_literal.body_scope(db); let class_scope_id = class_body_scope.file_scope_id(db); - let class_index = semantic_index(db, class_body_scope.file(db)); + let class_index = semantic_index(db, class_body_scope.python_file(db)); let pt = class_index.place_table(class_scope_id); if let Some(symbol) = pt.symbol_by_name(attribute) @@ -287,12 +292,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { qualifiers: TypeQualifiers, emit_diagnostics: bool, ) -> bool { + let db = self.db(); if !qualifiers.contains(TypeQualifiers::FINAL) { return false; } if emit_diagnostics { - let db = self.db(); let final_declaration = self.precise_final_attribute_declaration(object_ty, attribute); if let Some(builder) = self @@ -301,7 +306,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot delete final attribute `{attribute}` on type `{}`", - object_ty.display(db) + object_ty.display(db, self.program_environment()) )); diagnostic.set_primary_annotation_message("`Final` attributes cannot be deleted"); if let Some(final_declaration) = final_declaration { @@ -319,7 +324,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { object_ty: Type<'db>, attribute: &str, ) { - let Some(members) = assignment_attribute_members(self.db(), object_ty, attribute) else { + let db = self.db(); + let Some(members) = + assignment_attribute_members(db, self.program_environment(), object_ty, attribute) + else { return; }; @@ -342,7 +350,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { attribute: &str, emit_diagnostics: bool, ) -> bool { - let Some(members) = assignment_attribute_members(self.db(), object_ty, attribute) else { + let db = self.db(); + let Some(members) = + assignment_attribute_members(db, self.program_environment(), object_ty, attribute) + else { return false; }; diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs index 7690042950..b4207b2650 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -1,5 +1,6 @@ +use crate::Db; +use crate::ProgramEnvironment; use crate::{ - Db, reachability::ReachabilityConstraintsExtension, types::{ KnownClass, KnownInstanceType, ParamSpecAttrKind, SubclassOfInner, SubclassOfType, Type, @@ -38,7 +39,7 @@ use ty_python_core::{ scope::NodeWithScopeRef, }; -use ruff_python_ast as ast; +use ruff_python_ast::{self as ast}; use ruff_text_size::Ranged; fn parameters_have_annotations(parameters: &ast::Parameters) -> bool { @@ -72,21 +73,28 @@ impl<'db> ExpectedReturnType<'db> { function_node: &ast::StmtFunctionDef, ) -> Self { /// Normalizes special return annotations to the type actually returned by expressions. - fn normalize<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + fn normalize<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Type<'db> { match ty { - Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_instance(db), + Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_instance(db, env), ty => ty, } } + let env = ProgramEnvironment::from_file(function.python_file(db)); let public = normalize( db, + &env, same_module_uncached_raw_signature(db, function, ReturnCallableTypeVarScope::Public) .return_ty, ); let lexical = function_node.type_params.is_some().then(|| { normalize( db, + &env, same_module_uncached_raw_signature( db, function, @@ -106,11 +114,11 @@ impl<'db> ExpectedReturnType<'db> { /// Returns `true` if `ty` is accepted by either the public return type or the lexical return /// type. - fn accepts(self, db: &'db dyn Db, ty: Type<'db>) -> bool { - ty.is_assignable_to(db, self.public) + fn accepts(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> bool { + ty.is_assignable_to(db, env, self.public) || self .lexical - .is_some_and(|lexical| ty.is_assignable_to(db, lexical)) + .is_some_and(|lexical| ty.is_assignable_to(db, env, lexical)) } } @@ -127,6 +135,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .is_always_false() } + let env = self.program_environment(); let db = self.db(); // Parameters are odd: they are Definitions in the function body scope, but have no @@ -145,7 +154,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(returns) = function.returns.as_deref() { let has_empty_body = self.return_types_and_ranges.is_empty() - && function_body_kind(db, function, |expr| self.expression_type(expr)) + && function_body_kind(db, env, function, |expr| self.expression_type(expr)) == FunctionBodyKind::Stub; let mut enclosing_class_context = None; @@ -195,10 +204,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { KnownClass::GeneratorType }; - if !inferred_return - .to_instance_unknown(db) - .is_assignable_to(db, expected_ty) + .to_instance_unknown(db, env) + .is_assignable_to(db, env, expected_ty) { report_invalid_generator_function_return_type( &self.context, @@ -208,13 +216,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - if let Some(expected_return_ty) = declared_ty.generator_return_type(db) { + if let Some(expected_return_ty) = declared_ty.generator_return_type(db, env) { for invalid in self.return_types_and_ranges .iter() .copied() .filter(|actual_return_ty| { - !actual_return_ty.ty.is_assignable_to(db, expected_return_ty) + !actual_return_ty + .ty + .is_assignable_to(db, env, expected_return_ty) }) { report_invalid_return_type( @@ -229,7 +239,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let use_def = self.index.use_def_map(scope_id); if can_implicitly_return_none(db, use_def) - && !Type::none(db).is_assignable_to(db, expected_return_ty) + && !Type::none(db, env).is_assignable_to(db, env, expected_return_ty) { let no_return = self.return_types_and_ranges.is_empty(); report_implicit_return_type( @@ -260,7 +270,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ty if ty.is_notimplemented(db) => None, _ => Some(ty_range), }) - .filter(|ty_range| !expected_return.accepts(db, ty_range.ty)) + .filter(|ty_range| !expected_return.accepts(db, env, ty_range.ty)) { report_invalid_return_type( &self.context, @@ -272,7 +282,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let use_def = self.index.use_def_map(scope_id); if can_implicitly_return_none(db, use_def) - && !Type::none(db).is_assignable_to(db, expected_ty) + && !Type::none(db, env).is_assignable_to(db, env, expected_ty) { let no_return = self.return_types_and_ranges.is_empty(); report_implicit_return_type( @@ -311,7 +321,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let decorator_inference = - (!decorator_list.is_empty()).then(|| function_known_decorators(db, definition)); + (!decorator_list.is_empty()).then(|| function_known_decorators(self.db(), definition)); if let Some(decorator_inference) = decorator_inference.as_ref() { self.context.extend(decorator_inference.diagnostics()); self.expressions @@ -421,7 +431,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let body_scope = self .index .node_scope(NodeWithScopeRef::Function(function)) - .to_scope_id(db, self.file()); + .to_scope_id(db, self.python_file()); let overload_literal = OverloadLiteral::new( db, @@ -457,7 +467,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let current_scope = self.scope().file_scope_id(db); for type_param in type_params.iter() { let param_name = type_param.name(); - for enclosing in enclosing_generic_contexts(db, self.index, current_scope) { + for enclosing in enclosing_generic_contexts(self.db(), self.index, current_scope) { if let Some(other_typevar) = enclosing.binds_named_typevar(db, ¶m_name.id) { let kind = match type_param { ast::TypeParam::TypeVar(_) => TypeVarKind::Pep695TypeVar, @@ -501,7 +511,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { function_type }; let implementation_callables = inferred_ty - .try_upcast_to_callable(db) + .try_upcast_to_callable(db, self.program_environment()) .map_or_else(Box::default, |callables| { callables.iter().copied().collect() }); @@ -605,9 +615,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let type_params_scope = self .index .node_scope(NodeWithScopeRef::FunctionTypeParameters(function)) - .to_scope_id(db, self.file()); + .to_scope_id(db, self.python_file()); let type_params_inference = - infer_scope_types(db, type_params_scope, TypeContext::default()); + infer_scope_types(self.db(), type_params_scope, TypeContext::default()); for param_with_default in function.parameters.iter_non_variadic_params() { let Some(default) = param_with_default.default.as_deref() else { @@ -709,6 +719,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn validate_unpacked_typed_dict_kwargs(&mut self, parameters: &ast::Parameters) { + let db = self.db(); + let env = self.program_environment(); let Some(kwargs) = parameters.kwarg.as_ref() else { return; }; @@ -722,7 +734,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let annotated_type = self.file_expression_type(annotation); let Some(unpacked_keys) = extract_unpacked_typed_dict_keys_from_kwargs_annotation( - self.db(), + db, annotated_type, annotation_flags, ) else { @@ -731,7 +743,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let diag = builder.into_diagnostic(format_args!( "Unpacked value for `**kwargs` must be a TypedDict, not `{}`", - annotated_type.display(self.db()) + annotated_type.display(db, env) )); add_type_expression_reference_link(diag); } @@ -844,6 +856,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parameter_with_default: &'ast ast::ParameterWithDefault, definition: Definition<'db>, ) { + let env = self.program_environment(); let ast::ParameterWithDefault { parameter, default, @@ -885,7 +898,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Avoid duplicate diagnostics: invalid TypedDict literals already emit specific errors. let suppress_invalid_default = is_invalid_typed_dict_literal(db, declared_ty, default_expr.into()); - if !default_ty.is_assignable_to(db, declared_ty) + if !default_ty.is_assignable_to(db, env, declared_ty) && !suppress_invalid_default && !((self.in_stub() || self.in_function_overload_or_abstractmethod() @@ -904,8 +917,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder.into_diagnostic(format_args!( "Default value of type `{}` is not assignable \ to annotated parameter type `{}`", - default_ty.display(db), - declared_ty.display(db) + default_ty.display(db, env), + declared_ty.display(db, env) )); } } @@ -919,7 +932,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { let ty = if let Some(default_expr) = default_expr { let default_ty = self.file_expression_type(default_expr); - UnionType::from_two_elements(db, Type::unknown(), default_ty) + UnionType::from_two_elements(db, env, Type::unknown(), default_ty) } else if let Some(ty) = self.special_first_method_parameter_type(parameter) { ty } else { @@ -956,8 +969,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { Type::tuple(TupleType::new( db, + self.program_environment(), &TupleSpecBuilder::with_capacity(0) - .concat_variadic_typevar(db, typevar) + .concat_variadic_typevar(db, self.program_environment(), typevar) .build(), )) } @@ -983,17 +997,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); add_type_expression_reference_link(diag); } - Type::homogeneous_tuple(db, Type::unknown()) + Type::homogeneous_tuple(db, self.program_environment(), Type::unknown()) } // `*args: P` None => { // The diagnostic for this case is handled in `in_type_expression`. - Type::homogeneous_tuple(db, Type::unknown()) + Type::homogeneous_tuple(db, self.program_environment(), Type::unknown()) } } } - _ => Type::homogeneous_tuple(db, annotated_type), + _ => Type::homogeneous_tuple(db, self.program_environment(), annotated_type), }; self.add_declaration_with_binding( @@ -1002,7 +1016,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &DeclaredAndInferredType::are_the_same_type(ty), ); } else { - let inferred_ty = Type::homogeneous_tuple(db, Type::unknown()); + let inferred_ty = + Type::homogeneous_tuple(db, self.program_environment(), Type::unknown()); self.add_binding(parameter.into(), definition) .insert(self, inferred_ty); } @@ -1013,8 +1028,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, parameter: &ast::Parameter, ) -> Option> { + let env = self.program_environment(); let db = self.db(); - let file = self.file(); + let file = self.python_file(); let function_scope_id = self.scope(); let function_scope = function_scope_id.scope(db); @@ -1050,7 +1066,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let function_name = &function_node.name; let mut is_classmethod = is_implicit_classmethod(function_name); - let inference = infer_definition_types(db, method_definition); + let inference = infer_definition_types(self.db(), method_definition); for decorator in &function_node.decorator_list { let decorator_ty = inference.expression_type(&decorator.expression); if let Some(known_class) = decorator_ty @@ -1067,11 +1083,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let class_definition = self.index.expect_single_definition(class); let class_literal = original_class_type(db, class_definition)?; - let typing_self = typing_self(db, self.scope(), Some(method_definition), class_literal); if is_classmethod || function_name == "__new__" { - typing_self - .map(|typing_self| SubclassOfType::from(db, SubclassOfInner::TypeVar(typing_self))) + typing_self.map(|typing_self| { + SubclassOfType::from(db, env, SubclassOfInner::TypeVar(typing_self)) + }) } else { typing_self.map(Type::TypeVar) } @@ -1089,6 +1105,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parameter: &'ast ast::Parameter, definition: Definition<'db>, ) { + let env = self.program_environment(); let db = self.db(); if let Some(annotation) = parameter.annotation() { @@ -1114,7 +1131,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } KnownClass::Dict.to_specialized_instance( db, - &[KnownClass::Str.to_instance(db), Type::unknown()], + env, + &[KnownClass::Str.to_instance(db, env), Type::unknown()], ) } @@ -1126,7 +1144,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // The diagnostic for this case is handled in `in_type_expression`. KnownClass::Dict.to_specialized_instance( db, - &[KnownClass::Str.to_instance(db), Type::unknown()], + env, + &[KnownClass::Str.to_instance(db, env), Type::unknown()], ) } } @@ -1139,8 +1158,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { annotated_type } else { - KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), annotated_type]) + KnownClass::Dict.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), annotated_type], + ) }; self.add_declaration_with_binding( parameter.into(), @@ -1148,8 +1170,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &DeclaredAndInferredType::are_the_same_type(ty), ); } else { - let inferred_ty = KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::unknown()]); + let inferred_ty = KnownClass::Dict.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::unknown()], + ); self.add_binding(parameter.into(), definition) .insert(self, inferred_ty); @@ -1165,6 +1190,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { lambda: &'ast ast::ExprLambda, definition: Definition<'db>, ) { + let db = self.db(); let ast::ParameterWithDefault { parameter, default, @@ -1177,7 +1203,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parameter_type } else if let Some(default_expr) = default_expr { let default_ty = self.file_expression_type(default_expr); - UnionType::from_two_elements(self.db(), Type::unknown(), default_ty) + UnionType::from_two_elements( + db, + self.program_environment(), + Type::unknown(), + default_ty, + ) } else { Type::unknown() }; @@ -1195,12 +1226,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { lambda: &'ast ast::ExprLambda, definition: Definition<'db>, ) { + let db = self.db(); // Note that this currently always returns `None` because we do not support `Unpack` // annotations for callable types. let ty = if let Some(parameter_type) = self.annotated_lambda_parameter_type(index, lambda) { parameter_type } else { - Type::homogeneous_tuple(self.db(), Type::unknown()) + Type::homogeneous_tuple(db, self.program_environment(), Type::unknown()) }; self.add_binding(parameter.into(), definition) .insert(self, ty); @@ -1213,9 +1245,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parameter: &'ast ast::Parameter, definition: Definition<'db>, ) { + let db = self.db(); + let env = self.program_environment(); let inferred_ty = KnownClass::Dict.to_specialized_instance( - self.db(), - &[KnownClass::Str.to_instance(self.db()), Type::unknown()], + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::unknown()], ); self.add_binding(parameter.into(), definition) @@ -1229,6 +1264,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { index: u32, lambda: &'ast ast::ExprLambda, ) -> Option> { + let db = self.db(); let enclosing_stmt = infer_statement_types( self.db(), self.index.enclosing_lambda_statement(lambda.into())?, @@ -1240,7 +1276,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let parameter_type = signature.parameters().as_slice()[index as usize].annotated_type(); - if parameter_type.is_unknown() || parameter_type.has_unspecialized_type_var(self.db()) { + if parameter_type.is_unknown() + || parameter_type.has_unspecialized_type_var(db, self.program_environment()) + { None } else { Some(parameter_type) diff --git a/crates/ty_python_semantic/src/types/infer/builder/imports.rs b/crates/ty_python_semantic/src/types/infer/builder/imports.rs index 6b38eacb1c..ac2385519c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/imports.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/imports.rs @@ -5,7 +5,7 @@ use ty_module_resolver::{ }; use crate::{ - Program, TypeQualifiers, add_inferred_python_version_hint_to_diagnostic, + TypeQualifiers, add_inferred_python_version_hint_to_diagnostic, place::{DefinedPlace, Definedness, Place, PlaceAndQualifiers, TypeOrigin}, types::{ ModuleLiteralType, Type, TypeAndQualifiers, @@ -69,14 +69,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if level == 0 { if let Some(module_name) = module_name { - let program = Program::get(db); + let program = ty_python_core::program::Program::get(db); let typeshed_versions = program.search_paths(db).typeshed_versions(); // Loop over ancestors in case we have info on the parent module but not submodule for module_name in module_name.ancestors() { if let Some(version_range) = typeshed_versions.exact(&module_name) { // We know it is a stdlib module on *some* Python versions... - let python_version = program.python_version(db); + let python_version = self.program_environment().python_version(db); if !version_range.contains(python_version) { // ...But not on *this* Python version. diagnostic.info(format_args!( @@ -97,12 +97,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } else { if let Some(better_level) = (0..level).rev().find(|reduced_level| { - let Ok(module_name) = - ModuleName::from_identifier_parts(db, self.file(), module, *reduced_level) - else { + let Ok(module_name) = ModuleName::from_identifier_parts( + db, + self.python_file(), + module, + *reduced_level, + ) else { return false; }; - resolve_module(db, self.file(), &module_name).is_some() + resolve_module(db, self.python_file(), &module_name).is_some() }) { diagnostic .help("The module can be resolved if the number of leading dots is reduced"); @@ -235,7 +238,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for alias in names { for definition in self.index.definitions(alias) { - let inferred = infer_definition_types(db, *definition); + let inferred = infer_definition_types(self.db(), *definition); // Check non-star imports for deprecations if definition.kind(db).as_star_import().is_none() { // In the initial cycle, `declaration_types()` is empty, so no deprecation check is performed. @@ -268,7 +271,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { format_import_from_module(*level, module), self.file().path(db), ); - let module_name = ModuleName::from_import_statement(db, self.file(), import_from); + let module_name = ModuleName::from_import_statement(db, self.python_file(), import_from); let module_name = match module_name { Ok(module_name) => module_name, @@ -297,7 +300,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }; - if resolve_module(db, self.file(), &module_name).is_none() { + if resolve_module(db, self.python_file(), &module_name).is_none() { self.report_unresolved_import(module_ref.range(), *level, module, Some(&module_name)); } } @@ -310,7 +313,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) { let db = self.db(); - let Ok(module_name) = ModuleName::from_import_statement(db, self.file(), import_from) + let Ok(module_name) = + ModuleName::from_import_statement(db, self.python_file(), import_from) else { self.add_unknown_declaration_with_binding(alias.into(), definition); return; @@ -330,7 +334,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return; } - let Some(module) = resolve_module(db, self.file(), &module_name) else { + let Some(module) = resolve_module(db, self.python_file(), &module_name) else { self.add_unknown_declaration_with_binding(alias.into(), definition); return; }; @@ -338,7 +342,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let module_literal = ModuleLiteralType::new( db, module, - module.kind(db).is_package().then_some(self.file()), + module.kind(db).is_package().then_some(self.python_file()), ); let module_ty = Type::ModuleLiteral(module_literal); @@ -376,7 +380,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .. }), qualifiers, - } = module_literal.static_member(db, name) + } = module_literal.static_member(db, self.program_environment(), name) { if &alias.name != "*" && boundness == Definedness::PossiblyUndefined { // TODO: Consider loading _both_ the attribute and any submodule and unioning them @@ -535,13 +539,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); // Get this package's absolute module name by resolving `.`, and make sure it exists - let Ok(thispackage_name) = ModuleName::package_for_file(db, self.file()) else { + let Ok(thispackage_name) = ModuleName::package_for_file(db, self.python_file()) else { self.add_binding(import_from.into(), definition) .insert(self, Type::unknown()); return; }; - let Some(module) = resolve_module(db, self.file(), &thispackage_name) else { + let Some(module) = resolve_module(db, self.python_file(), &thispackage_name) else { self.add_binding(import_from.into(), definition) .insert(self, Type::unknown()); return; @@ -553,7 +557,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // First we normalize to `whatever.thispackage.x.y` let Some(final_part) = ModuleName::from_identifier_parts( db, - self.file(), + self.python_file(), import_from.module.as_deref(), import_from.level, ) diff --git a/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs b/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs index aa69630a62..febb1b79f6 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/named_tuple.rs @@ -33,6 +33,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { definition: Option>, kind: NamedTupleKind, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); // The fallback type reflects the fact that if the call were successful, @@ -45,9 +46,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let fallback = || { IntersectionType::from_elements( db, + env, [ - Type::homogeneous_tuple(db, Type::unknown()).to_meta_type(db), - KnownClass::NamedTupleLike.to_subclass_of(db), + Type::homogeneous_tuple(db, env, Type::unknown()).to_meta_type(db, env), + KnownClass::NamedTupleLike.to_subclass_of(db, env), Type::unknown(), ], ) @@ -219,7 +221,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "defaults" if kind.is_collections() => { defaults_kw = Some(kw); if let Some(element_types) = - extract_fixed_length_iterable_element_types(db, &kw.value, |expr| { + extract_fixed_length_iterable_element_types(db, env, &kw.value, |expr| { self.expression_type(expr) }) { @@ -234,9 +236,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } // Emit diagnostic for invalid types (not Iterable[Any] | None). let iterable_any = - KnownClass::Iterable.to_specialized_instance(db, &[Type::any()]); - let valid_type = UnionType::from_two_elements(db, iterable_any, Type::none(db)); - if !kw_type.is_assignable_to(db, valid_type) + KnownClass::Iterable.to_specialized_instance(db, env, &[Type::any()]); + let valid_type = + UnionType::from_two_elements(db, env, iterable_any, Type::none(db, env)); + if !kw_type.is_assignable_to(db, env, valid_type) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) { @@ -245,7 +248,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )); diagnostic.set_primary_annotation_message(format_args!( "Expected `Iterable[Any] | None`, found `{}`", - kw_type.display(db) + kw_type.display(db, env) )); } } @@ -253,7 +256,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { rename_type = Some(kw_type); // Emit diagnostic for non-bool types. - if !kw_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) + if !kw_type.is_assignable_to(db, env, KnownClass::Bool.to_instance(db, env)) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) { @@ -262,7 +265,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )); diagnostic.set_primary_annotation_message(format_args!( "Expected `bool`, found `{}`", - kw_type.display(db) + kw_type.display(db, env) )); } } @@ -270,10 +273,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // Emit diagnostic for invalid types (not str | None). let valid_type = UnionType::from_two_elements( db, - KnownClass::Str.to_instance(db), - Type::none(db), + env, + KnownClass::Str.to_instance(db, env), + Type::none(db, env), ); - if !kw_type.is_assignable_to(db, valid_type) + if !kw_type.is_assignable_to(db, env, valid_type) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, &kw.value) { @@ -282,7 +286,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )); diagnostic.set_primary_annotation_message(format_args!( "Expected `str | None`, found `{}`", - kw_type.display(db) + kw_type.display(db, env) )); } } @@ -329,7 +333,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .map(|literal| literal.value(db)); if name.is_none() - && !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && !name_type.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) { let mut diagnostic = builder.into_diagnostic(format_args!( @@ -337,7 +341,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )); diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", - name_type.display(db) + name_type.display(db, env) )); } else if let Some(actual_name) = name && let Some(definition) = definition @@ -418,6 +422,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { default_types: &[Type<'db>], defaults_kw: Option<&ast::Keyword>, ) -> NamedTupleSpec<'db> { + let env = self.program_environment(); let db = self.db(); // `collections.namedtuple`: `field_names` is a list or tuple of strings, or a space or @@ -425,7 +430,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // Check for `rename=True`. Use `is_always_true()` to handle truthy values // (e.g., `rename=1`), though we'd still want a diagnostic for non-bool types. - let rename = rename_type.is_some_and(|ty| ty.bool(db).is_always_true()); + let rename = rename_type.is_some_and(|ty| ty.bool(db, env).is_always_true()); let fields_type = self.infer_expression(fields_arg, TypeContext::default()); @@ -442,7 +447,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .collect(), ) } else { - extract_fixed_length_iterable_element_types(db, fields_arg, |expr| { + extract_fixed_length_iterable_element_types(db, env, fields_arg, |expr| { self.expression_type(expr) }) .and_then(|field_types| { @@ -455,10 +460,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if maybe_field_names.is_none() { // Emit diagnostic if the type is outright invalid (not str | Iterable[str]). - let iterable_str = KnownClass::Iterable.to_specialized_instance(db, &[Type::any()]); - let valid_type = - UnionType::from_two_elements(db, KnownClass::Str.to_instance(db), iterable_str); - if !fields_type.is_assignable_to(db, valid_type) + let iterable_str = + KnownClass::Iterable.to_specialized_instance(db, env, &[Type::any()]); + let valid_type = UnionType::from_two_elements( + db, + env, + KnownClass::Str.to_instance(db, env), + iterable_str, + ); + if !fields_type.is_assignable_to(db, env, valid_type) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, fields_arg) { let mut diagnostic = builder.into_diagnostic(format_args!( @@ -466,7 +476,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )); diagnostic.set_primary_annotation_message(format_args!( "Expected `str` or an iterable of strings, found `{}`", - fields_type.display(db) + fields_type.display(db, env) )); } } @@ -552,6 +562,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Tuple, } + let env = self.program_environment(); let db = self.db(); // Get the elements from the list or tuple literal. @@ -587,12 +598,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { SequenceKind::List => { self.store_expression_type( fields_arg, - KnownClass::List.to_instance(db), + KnownClass::List.to_instance(db, env), ); } SequenceKind::Tuple => self.store_expression_type( fields_arg, - Type::homogeneous_tuple(db, Type::unknown()), + Type::homogeneous_tuple(db, env, Type::unknown()), ), } if let Some(builder) = @@ -616,11 +627,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } match field_arg_kind { SequenceKind::List => { - self.store_expression_type(fields_arg, KnownClass::List.to_instance(db)); + self.store_expression_type( + fields_arg, + KnownClass::List.to_instance(db, env), + ); } SequenceKind::Tuple => self.store_expression_type( fields_arg, - Type::homogeneous_tuple(db, Type::unknown()), + Type::homogeneous_tuple(db, env, Type::unknown()), ), } if let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, fields_arg) { @@ -638,10 +652,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let declared_type = self.infer_type_expression(declaration_expr); let element_type = match field_spec_kind { - SequenceKind::Tuple => Type::heterogeneous_tuple(db, [name_type, declared_type]), + SequenceKind::Tuple => { + Type::heterogeneous_tuple(db, env, [name_type, declared_type]) + } SequenceKind::List => KnownClass::List.to_specialized_instance( db, - &[UnionType::from_two_elements(db, name_type, declared_type)], + env, + &[UnionType::from_two_elements( + db, + env, + name_type, + declared_type, + )], ), }; @@ -653,11 +675,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } match field_arg_kind { SequenceKind::List => { - self.store_expression_type(fields_arg, KnownClass::List.to_instance(db)); + self.store_expression_type( + fields_arg, + KnownClass::List.to_instance(db, env), + ); } SequenceKind::Tuple => self.store_expression_type( fields_arg, - Type::homogeneous_tuple(db, Type::unknown()), + Type::homogeneous_tuple(db, env, Type::unknown()), ), } if let Some(builder) = self.context.report_lint(&INVALID_NAMED_TUPLE, name_expr) { @@ -665,7 +690,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.into_diagnostic("Invalid `NamedTuple` field name definition"); diagnostic.set_primary_annotation_message(format_args!( "Expected a string literal for the field name, found `{}`", - name_type.display(db) + name_type.display(db, env) )); } return NamedTupleSpec::unknown(db); diff --git a/crates/ty_python_semantic/src/types/infer/builder/new_class.rs b/crates/ty_python_semantic/src/types/infer/builder/new_class.rs index 1d82ff6841..ab3ae0b03e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/new_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/new_class.rs @@ -27,6 +27,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { call_expr: &ast::ExprCall, definition: Option>, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); let ast::Arguments { @@ -74,7 +75,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { literal.value(db) } else { if let Some(name_node) = name_node - && !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && !name_type.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_node) { let mut diagnostic = builder.into_diagnostic( @@ -82,7 +83,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", - name_type.display(db) + name_type.display(db, env) )); } "" @@ -189,9 +190,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { call_expr.into(), dynamic_class.name(db), metaclass1, - base1.display(db), + base1.display(db, env), metaclass2, - base2.display(db), + base2.display(db, env), ); } } @@ -215,7 +216,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; // Get the already-inferred class type from the initial pass. - let inferred_type = definition_expression_type(db, definition, call_expr); + let inferred_type = definition_expression_type(self.db(), definition, call_expr); let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = inferred_type else { return; }; @@ -256,20 +257,23 @@ impl<'db> TypeInferenceBuilder<'db, '_> { definition: Option>, ) { let db = self.db(); + let env = self.program_environment(); let callable_type = self.expression_type(call_expr.func.as_ref()); - let iterable_object = KnownClass::Iterable.to_specialized_instance(db, &[Type::object()]); + let iterable_object = + KnownClass::Iterable.to_specialized_instance(db, env, &[Type::object()]); let mut call_arguments = self.prepare_call_arguments(&call_expr.arguments); - let mut bindings = callable_type - .bindings(db) - .match_parameters(db, &call_arguments); + let mut bindings = + callable_type + .bindings(db, env) + .match_parameters(db, env, &call_arguments); let bindings_result = self.infer_and_check_argument_types( ArgumentsIter::from_ast(&call_expr.arguments), &mut call_arguments, &mut |builder, (_, expr, tcx)| { if name_node.is_some_and(|name| std::ptr::eq(expr, name)) { let _ = builder.infer_expression(expr, tcx); - KnownClass::Str.to_instance(builder.db()) + KnownClass::Str.to_instance(db, env) } else if bases_arg.is_some_and(|bases| std::ptr::eq(expr, bases)) { if definition.is_none() { let _ = builder.infer_expression(expr, tcx); diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/dynamic_class.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/dynamic_class.rs index 292d4e424f..7ebd5b17cd 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/dynamic_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/dynamic_class.rs @@ -24,7 +24,7 @@ pub(crate) fn check_dynamic_class_definition<'db>( return; }; - let ty = binding_type(db, definition); + let ty = binding_type(context.db(), definition); // Check if it's a dynamic class with a Definition anchor. let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = ty else { @@ -46,6 +46,8 @@ pub(crate) fn check_dynamic_class_definition<'db>( return; }; + let env = context.program_environment(); + // Check for MRO errors. if report_dynamic_mro_errors(context, dynamic_class, call_expr, bases) { report_inconsistent_dynamic_generic_bases(context, dynamic_class, bases); @@ -54,7 +56,11 @@ pub(crate) fn check_dynamic_class_definition<'db>( let mut disjoint_bases = IncompatibleBases::default(); let bases_tuple_elts = bases.as_tuple_expr().map(|tuple| tuple.elts.as_slice()); - for (idx, base_type) in dynamic_class.explicit_bases(db).iter().enumerate() { + for (idx, base_type) in dynamic_class + .explicit_bases(context.db()) + .iter() + .enumerate() + { // Convert to ClassType to access nearest_disjoint_base. if let Some(class_type) = base_type.to_class_type(db) && let Some(disjoint_base) = class_type.nearest_disjoint_base(db) @@ -87,9 +93,9 @@ pub(crate) fn check_dynamic_class_definition<'db>( call_expr.into(), dynamic_class.name(db), metaclass1, - base1.display(db), + base1.display(db, env), metaclass2, - base2.display(db), + base2.display(db, env), ); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/final_variable.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/final_variable.rs index f1ad602a35..a3ef01a8ea 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/final_variable.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/final_variable.rs @@ -27,9 +27,10 @@ pub(crate) fn check_final_without_value<'db>( let use_def = index.use_def_map(file_scope_id); let place_table = index.place_table(file_scope_id); + let env = context.program_environment(); for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { - let result = place_from_declarations(db, declarations); + let result = place_from_declarations(db, env, declarations); let first_declaration = result.first_declaration; let (place_and_quals, _) = result.into_place_and_conflicting_declarations(); @@ -46,7 +47,7 @@ pub(crate) fn check_final_without_value<'db>( // Check if the symbol has any bindings in the current scope. let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - let binding_place = place_from_bindings(db, bindings); + let binding_place = place_from_bindings(db, env, bindings); if !binding_place.place.is_undefined() { continue; diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/function.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/function.rs index 1288f6ef65..8a88629cfe 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/function.rs @@ -31,7 +31,8 @@ pub(crate) fn check_function_definition<'db>( ) { let db = context.db(); - let Some(function_type) = infer_definition_types(db, definition).function_type(definition) + let Some(function_type) = + infer_definition_types(context.db(), definition).function_type(definition) else { return; }; @@ -59,10 +60,10 @@ fn check_pep695_function_legacy_typevars<'db>( let Some(type_params) = node.type_params.as_deref() else { return; }; - + let env = context.program_environment(); let mut has_legacy_default = false; for default in type_params.iter().filter_map(ast::TypeParam::default) { - let Some(typevar) = find_over_type(db, file_expression_type(default), false, |ty| { + let Some(typevar) = find_over_type(db, env, file_expression_type(default), false, |ty| { if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = ty && matches!( typevar.kind(db), @@ -199,6 +200,8 @@ fn check_legacy_typevar_defaults<'db>( return; }; + let env = context.program_environment(); + let typevars = generic_context .variables(db) .map(|bound_tvar| bound_tvar.typevar(db)); @@ -216,11 +219,11 @@ fn check_legacy_typevar_defaults<'db>( continue; } - let Some(default_ty) = typevar.default_type(db) else { + let Some(default_ty) = typevar.default_type(db, env) else { continue; }; - let first_bad_tvar = find_over_type(db, default_ty, false, |t| { + let first_bad_tvar = find_over_type(db, env, default_ty, false, |t| { let tvar = match t { Type::TypeVar(tvar) => tvar.typevar(db), Type::KnownInstance(KnownInstanceType::TypeVar(tvar)) => tvar, @@ -275,11 +278,11 @@ fn check_legacy_typevar_defaults<'db>( } if let Some(typevar_definition) = typevar.definition(db) { - let file = typevar_definition.file(db); diagnostic.annotate( - Annotation::secondary(Span::from( - typevar_definition.full_range(db, &parsed_module(db, file).load(db)), - )) + Annotation::secondary(Span::from(typevar_definition.full_range( + db, + &parsed_module(db, typevar_definition.python_file(db)).load(db), + ))) .message(format_args!("`{typevar_name}` defined here")), ); } @@ -295,13 +298,14 @@ fn find_typevar_annotation_range<'db>( file_expression_type: impl Fn(&ast::Expr) -> Type<'db>, ) -> TextRange { let db = context.db(); + let env = context.program_environment(); let typevar_id = typevar.identity(db); node.parameters .iter() .filter_map(ast::AnyParameterRef::annotation) .chain(node.returns.as_deref()) - .find(|ann| file_expression_type(ann).references_typevar(db, typevar_id)) + .find(|ann| file_expression_type(ann).references_typevar(db, env, typevar_id)) .map(Ranged::range) .unwrap_or_else(|| node.name.range()) } @@ -328,6 +332,8 @@ fn check_legacy_typevar_ordering<'db>( return; }; + let env = context.program_environment(); + let mut state: Option> = None; for bound_typevar in generic_context.variables(db) { @@ -344,7 +350,7 @@ fn check_legacy_typevar_ordering<'db>( continue; } - let has_default = typevar.default_type(db).is_some(); + let has_default = typevar.default_type(db, env).is_some(); if let Some(state) = state.as_mut() { if !has_default { @@ -419,10 +425,9 @@ fn check_legacy_typevar_ordering<'db>( let Some(definition) = tvar.definition(db) else { continue; }; - let file = definition.file(db); diagnostic.annotate( Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), + definition.full_range(db, &parsed_module(db, definition.python_file(db)).load(db)), )) .message(format_args!("`{}` defined here", tvar.name(db))), ); diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs index 8a6ad9d270..439e5ebaae 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/overloaded_function.rs @@ -46,6 +46,7 @@ pub(crate) fn check_overloaded_function<'db>( }; let db = context.db(); + let env = context.program_environment(); if function.file(db) != context.file() { // If the function is not in this file, we don't need to check it. @@ -67,6 +68,7 @@ pub(crate) fn check_overloaded_function<'db>( .. }) = place_from_bindings( db, + env, use_def.end_of_scope_symbol_bindings(place.as_symbol().unwrap()), ) .place @@ -122,7 +124,7 @@ pub(crate) fn check_overloaded_function<'db>( )); diagnostic.set_primary_annotation_message("Only one overload defined here"); if let Some(decorator) = - single_overload.find_known_decorator_span(db, KnownFunction::Overload) + single_overload.find_known_decorator_span(context.db(), KnownFunction::Overload) { diagnostic.annotate(Annotation::secondary(decorator)); } @@ -149,11 +151,15 @@ pub(crate) fn check_overloaded_function<'db>( ) { if class.is_protocol(db) - || (Type::ClassLiteral(class) - .is_subtype_of(db, KnownClass::ABCMeta.to_instance(db)) - && overloads.iter().all(|overload| { - overload.has_known_decorator(db, FunctionDecorators::ABSTRACT_METHOD) - })) + || ({ + Type::ClassLiteral(class).is_subtype_of( + db, + env, + KnownClass::ABCMeta.to_instance(db, env), + ) + } && overloads.iter().all(|overload| { + overload.has_known_decorator(db, FunctionDecorators::ABSTRACT_METHOD) + })) { implementation_required = false; } @@ -199,7 +205,7 @@ pub(crate) fn check_overloaded_function<'db>( .message(format_args!("Missing here")), ); if let Some(decorator) = - function.find_known_decorator_span(db, KnownFunction::Overload) + function.find_known_decorator_span(context.db(), KnownFunction::Overload) { diagnostic.annotate(Annotation::secondary(decorator)); } @@ -227,7 +233,8 @@ pub(crate) fn check_overloaded_function<'db>( name = known_function.name() )); for known_function in [known_function, KnownFunction::Overload] { - if let Some(decorator) = overload.find_known_decorator_span(db, known_function) + if let Some(decorator) = + overload.find_known_decorator_span(context.db(), known_function) { diagnostic.annotate(Annotation::secondary(decorator)); } @@ -257,11 +264,13 @@ pub(crate) fn check_overloaded_function<'db>( first overload", name = known_function.name() )); - if let Some(decorator) = overload.find_known_decorator_span(db, known_function) { + if let Some(decorator) = + overload.find_known_decorator_span(context.db(), known_function) + { diagnostic.annotate(Annotation::secondary(decorator)); } let file = function.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, first_overload.python_file(db)).load(db); let node = first_overload.node(db, file, &module); let span = if node.body.len() == 1 { Span::from(file).with_range(node.range()) @@ -290,6 +299,7 @@ fn check_non_generic_overload_implementation_consistency<'db>( implementation_callables: &[CallableType<'db>], ) { let db = context.db(); + let env = context.program_environment(); if implementation_callables.is_empty() || implementation_callables .iter() @@ -336,11 +346,13 @@ fn check_non_generic_overload_implementation_consistency<'db>( let parameter_consistency = implementation_signature .non_generic_implementation_parameters_consistency_with( db, + env, &overload_signature, ); let return_type_consistency = implementation_signature .non_generic_implementation_return_type_consistency_with( db, + env, &overload_signature, ); if matches!( @@ -400,18 +412,18 @@ fn check_non_generic_overload_implementation_consistency<'db>( if let Some(error_context) = parameter_error_context { diagnostic.info(format_args!( "Implementation signature `{}` is not assignable to overload signature `{}`", - implementation_signature.display(db), - overload_signature.display(db), + implementation_signature.display(db, env), + overload_signature.display(db, env), )); - error_context.attach_to(db, &mut diagnostic); + error_context.attach_to(db, env, &mut diagnostic); } if let Some(error_context) = return_type_error_context { diagnostic.info(format_args!( "Overload returns `{}`, which is not assignable to implementation return type `{}`", - overload_signature.return_ty.display(db), - implementation_signature.return_ty.display(db), + overload_signature.return_ty.display(db, env), + implementation_signature.return_ty.display(db, env), )); - error_context.attach_to(db, &mut diagnostic); + error_context.attach_to(db, env, &mut diagnostic); } diagnostic.annotate( context diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs index 82c752c56a..e8fccc9062 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs @@ -1,3 +1,4 @@ +use crate::Db; use itertools::Itertools; use ruff_db::{ diagnostic::{Annotation, SubDiagnostic, SubDiagnosticSeverity}, @@ -9,7 +10,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use rustc_hash::FxHashSet; use crate::{ - Db, Program, TypeQualifiers, + TypeQualifiers, diagnostic::format_enumeration, place::{DefinedPlace, Place, TypeOrigin, place_from_bindings, place_from_declarations}, types::{ @@ -88,7 +89,7 @@ pub(crate) fn check_static_class_definitions<'db>( }; // Check that the class does not have a cyclic definition - if let Some(inheritance_cycle) = class.inheritance_cycle(db) { + if let Some(inheritance_cycle) = class.inheritance_cycle(context.db()) { if inheritance_cycle.is_participant() && let Some(builder) = context.report_lint(&CYCLIC_CLASS_DEFINITION, class_node) { @@ -103,8 +104,10 @@ pub(crate) fn check_static_class_definitions<'db>( return; } + let env = context.program_environment(); + // Check that the class is not an enum and generic - if is_enum_class_by_inheritance(db, class) && class.generic_context(db).is_some() { + if is_enum_class_by_inheritance(db, env, class) && class.generic_context(db).is_some() { if let Some(builder) = context.report_lint(&INVALID_GENERIC_ENUM, class_node) { builder.into_diagnostic(format_args!( "Enum class `{}` cannot be generic", @@ -216,7 +219,7 @@ pub(crate) fn check_static_class_definitions<'db>( "An exception will often be raised when instantiating the class at runtime", ); } - } else if is_enum_class_by_inheritance(db, class) { + } else if is_enum_class_by_inheritance(db, env, class) { if let Some(builder) = context.report_lint(&INVALID_DATACLASS, class.header_range(db)) { let mut diagnostic = builder.into_diagnostic(format_args!( "Enum class `{}` cannot be decorated with `@dataclass`", @@ -343,7 +346,7 @@ pub(crate) fn check_static_class_definitions<'db>( return None; } let required_variance = - base_alias.variance_of(db, typevar.identity(db)); + base_alias.variance_of(db, env, typevar.identity(db)); if declared_variance.join(required_variance) != declared_variance { Some((typevar, declared_variance, required_variance)) } else { @@ -459,7 +462,7 @@ pub(crate) fn check_static_class_definitions<'db>( for base in class_node.bases() { if let ast::Expr::Starred(starred) = base && let starred_ty = definition_expression_type(db, class_definition, &starred.value) - && let Some(tuple_spec) = starred_ty.tuple_instance_spec(db) + && let Some(tuple_spec) = starred_ty.tuple_instance_spec(db, env) && !matches!(tuple_spec.as_ref(), Tuple::Fixed(_)) { report_unsupported_base(context, base, starred_ty, class); @@ -492,7 +495,10 @@ pub(crate) fn check_static_class_definitions<'db>( "Cannot create a consistent method resolution order (MRO) \ for class `{}` with bases list `[{}]`", class.name(db), - bases_list.iter().map(|base| base.display(db)).join(", ") + bases_list + .iter() + .map(|base| base.display(db, env)) + .join(", ") )); let can_rewrite_bases = bases_list.len() == class_node.bases().len() && !class_node.bases().iter().any(ast::Expr::is_starred_expr); @@ -550,7 +556,7 @@ pub(crate) fn check_static_class_definitions<'db>( ); } - let explicit_bases = class.explicit_bases(db); + let explicit_bases = class.explicit_bases(context.db()); let base_nodes = (class_node.bases().len() == explicit_bases.len() && !class_node.bases().iter().any(ast::Expr::is_starred_expr)) .then_some(class_node.bases()); @@ -603,7 +609,7 @@ pub(crate) fn check_static_class_definitions<'db>( { builder.into_diagnostic(format_args!( "Metaclass type `{}` is not callable", - ty.display(db) + ty.display(db, env) )); } } @@ -613,7 +619,7 @@ pub(crate) fn check_static_class_definitions<'db>( { builder.into_diagnostic(format_args!( "Metaclass type `{}` is partly not callable", - ty.display(db) + ty.display(db, env) )); } } @@ -665,7 +671,7 @@ pub(crate) fn check_static_class_definitions<'db>( if class_kind == Some(CodeGeneratorKind::TypedDict) { let supports_pep_728 = context.in_stub() || class.typed_dict_module(db) == Some(TypedDictModule::TypingExtensions) - || Program::get(db).python_version(db) >= PythonVersion::PY315; + || env.python_version(db) >= PythonVersion::PY315; for keyword in &args.keywords { if !supports_pep_728 @@ -690,7 +696,7 @@ pub(crate) fn check_static_class_definitions<'db>( )); diagnostic.set_primary_annotation_message(format_args!( "Expected either `True` or `False`, got object of type `{}`", - passed_type.display(db) + passed_type.display(db, env) )); } } @@ -747,6 +753,7 @@ pub(crate) fn check_static_class_definitions<'db>( let init_subclass_type = class .class_member_from_mro( db, + env, "__init_subclass__", MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, // skip(1) to skip the current class and only consider base classes. @@ -756,7 +763,7 @@ pub(crate) fn check_static_class_definitions<'db>( if let Some(init_subclass) = init_subclass_type { let call_args = call_args.with_self(Some(Type::from(class))); - if let Err(call_error) = init_subclass.try_call(db, &call_args) { + if let Err(call_error) = init_subclass.try_call(db, env, &call_args) { report_subclass_of_class_with_non_callable_init_subclass( context, call_error, class, class_node, ); @@ -822,7 +829,7 @@ pub(crate) fn check_static_class_definitions<'db>( for bound_typevar in generic_context.variables(db) { let typevar = bound_typevar.typevar(db); - let has_default = typevar.default_type(db).is_some(); + let has_default = typevar.default_type(db, env).is_some(); if let Some(state) = state.as_mut() { if !has_default { @@ -860,11 +867,11 @@ pub(crate) fn check_static_class_definitions<'db>( // `variables` should be fairly cheap to clone; it's just several cheap wrappers around // a `std::slice::Iter` under the hood. for (i, typevar) in typevars.clone().enumerate() { - let Some(default_ty) = typevar.default_type(db) else { + let Some(default_ty) = typevar.default_type(db, env) else { continue; }; - let first_bad_tvar = find_over_type(db, default_ty, false, |t| { + let first_bad_tvar = find_over_type(db, env, default_ty, false, |t| { let tvar = match t { Type::TypeVar(tvar) => tvar.typevar(db), Type::KnownInstance(KnownInstanceType::TypeVar(tvar)) => tvar, @@ -1137,12 +1144,13 @@ fn check_class_namespace_against_metaclass_members<'db>( index: &SemanticIndex<'db>, ) { let db = context.db(); + let env = context.program_environment(); let metaclass = class.metaclass(db); - if metaclass == KnownClass::Type.to_class_literal(db) { + if metaclass == KnownClass::Type.to_class_literal(db, env) { return; } - let Some(metaclass_instance) = metaclass.to_instance_approximation(db) else { + let Some(metaclass_instance) = metaclass.to_instance_approximation(db, env) else { return; }; @@ -1164,7 +1172,7 @@ fn check_class_namespace_against_metaclass_members<'db>( .filter_map(|class| class.static_class_literal(db).map(|(literal, _)| literal)) { let body_scope = metaclass.body_scope(db); - let metaclass_index = semantic_index(db, body_scope.file(db)); + let metaclass_index = semantic_index(db, body_scope.python_file(db)); let body_scope_id = body_scope.file_scope_id(db); let metaclass_table = metaclass_index.place_table(body_scope_id); let metaclass_use_def = metaclass_index.use_def_map(body_scope_id); @@ -1207,7 +1215,9 @@ fn check_class_namespace_against_metaclass_members<'db>( ty: metaclass_member_ty, origin, .. - }) = metaclass_instance.instance_member(db, name.as_str()).place + }) = metaclass_instance + .instance_member(db, env, name.as_str()) + .place else { continue; }; @@ -1224,7 +1234,7 @@ fn check_class_namespace_against_metaclass_members<'db>( } let assigned_ty = binding_type(db, definition); - if !assigned_ty.is_assignable_to(db, metaclass_member_ty) { + if !assigned_ty.is_assignable_to(db, env, metaclass_member_ty) { reported_incompatible_binding = true; report_invalid_attribute_assignment( context, @@ -1248,7 +1258,7 @@ fn check_class_namespace_against_metaclass_members<'db>( } let result = - place_from_declarations(db, use_def.end_of_scope_symbol_declarations(symbol_id)); + place_from_declarations(db, env, use_def.end_of_scope_symbol_declarations(symbol_id)); let Some(definition) = result.first_declaration else { continue; }; @@ -1265,7 +1275,7 @@ fn check_class_namespace_against_metaclass_members<'db>( if !matches!(definition_kind, DefinitionKind::AnnotatedAssignment(_)) { continue; } - if !metaclass_member_ty.is_assignable_to(db, class_declared_ty) { + if !metaclass_member_ty.is_assignable_to(db, env, class_declared_ty) { report_invalid_attribute_assignment( context, definition_kind.target_range(context.module()), @@ -1309,7 +1319,6 @@ fn check_final_class_abstract_methods<'db>( class_node: &ast::StmtClassDef, ) { let db = context.db(); - // Only check if the class is final. if !class.is_final(db) { return; @@ -1323,6 +1332,8 @@ fn check_final_class_abstract_methods<'db>( return; } + let env = context.program_environment(); + let class_type = class.identity_specialization(db); let abstract_methods = class_type.abstract_methods(db); @@ -1445,14 +1456,14 @@ fn check_final_class_abstract_methods<'db>( if kind.is_implicit_due_to_stub_body() && db.should_check_file(definition.file(db)) { let function_type_as_callable = infer_definition_types(db, *definition) .binding_type(*definition) - .try_upcast_to_callable(db); + .try_upcast_to_callable(db, env); if let Some(callables) = function_type_as_callable && Type::function_like_callable( db, - Signature::new(Parameters::gradual_form(), Type::none(db)), + Signature::new(Parameters::gradual_form(), Type::none(db, env)), ) - .is_assignable_to(db, callables.into_type(db)) + .is_assignable_to(db, env, callables.into_type(db, env)) { diagnostic.help(format_args!( "Change the body of `{first_method_name}` to `return` \ @@ -1476,6 +1487,7 @@ fn check_class_final_without_value<'db>( } let db = context.db(); + let env = context.program_environment(); let body_scope = class.body_scope(db); let body_scope_id = body_scope.file_scope_id(db); let use_def = index.use_def_map(body_scope_id); @@ -1490,7 +1502,7 @@ fn check_class_final_without_value<'db>( } for (symbol_id, declarations) in use_def.all_end_of_scope_symbol_declarations() { - let result = place_from_declarations(db, declarations); + let result = place_from_declarations(db, env, declarations); let first_declaration = result.first_declaration; let (place_and_quals, _) = result.into_place_and_conflicting_declarations(); @@ -1500,7 +1512,7 @@ fn check_class_final_without_value<'db>( // Check if the symbol has any bindings at class level. let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - let binding_place = place_from_bindings(db, bindings); + let binding_place = place_from_bindings(db, env, bindings); if !binding_place.place.is_undefined() { continue; diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/typed_dict.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/typed_dict.rs index 95a42c06cf..441d3753d2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/typed_dict.rs @@ -1,8 +1,9 @@ +use crate::ProgramEnvironment; use ruff_db::{ diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}, parsed::parsed_module, }; -use ruff_python_ast as ast; +use ruff_python_ast::{self as ast}; use ruff_text_size::Ranged; use rustc_hash::FxHashSet; @@ -102,6 +103,7 @@ fn validate_typed_dict_field_overrides<'db>( direct_bases: &[ClassType<'db>], ) { let db = context.db(); + let env = context.program_environment(); let child_fields = TypedDictType::new(class.identity_specialization(db)).items(db); let own_fields = class.own_fields(db, None, CodeGeneratorKind::TypedDict); let mut reported_fields = FxHashSet::default(); @@ -113,7 +115,7 @@ fn validate_typed_dict_field_overrides<'db>( }; let Some(reason) = - TypedDictFieldOverrideReason::from_fields(db, child_field, base_field) + TypedDictFieldOverrideReason::from_fields(db, env, child_field, base_field) else { continue; }; @@ -134,7 +136,7 @@ fn validate_typed_dict_field_overrides<'db>( context, class, field_name.as_str(), - reason, + &reason, base.name(db), base_field.first_declaration(), own_field_definition, @@ -151,6 +153,7 @@ fn validate_typed_dict_openness<'db>( direct_bases: &[ClassType<'db>], ) { let db = context.db(); + let env = context.program_environment(); let child = TypedDictType::new(class.identity_specialization(db)); let child_openness = child.openness(db); let child_items = child.items(db); @@ -217,17 +220,18 @@ fn validate_typed_dict_openness<'db>( } TypedDictOpenness::Closed => {} TypedDictOpenness::Extra(child_extra_items) => { - if !child_extra_items - .declared_ty - .is_assignable_to(db, base_extra_items.declared_ty) - { + if !child_extra_items.declared_ty.is_assignable_to( + db, + env, + base_extra_items.declared_ty, + ) { report_invalid_typed_dict_openness( context, class, format_args!( "Extra items type `{}` is not assignable to `{}` from base `{}`", - child_extra_items.declared_ty.display(db), - base_extra_items.declared_ty.display(db), + child_extra_items.declared_ty.display(db, env), + base_extra_items.declared_ty.display(db, env), base.name(db), ), ); @@ -238,17 +242,19 @@ fn validate_typed_dict_openness<'db>( if let Some((field_name, field)) = child_items.iter().find(|(field_name, field)| { !base_items.contains_key(*field_name) - && !field - .declared_ty - .is_assignable_to(db, base_extra_items.declared_ty) + && !field.declared_ty.is_assignable_to( + db, + env, + base_extra_items.declared_ty, + ) }) { report_invalid_typed_dict_openness( context, class, format_args!( "Item `{field_name}` of type `{}` is not assignable to extra items type `{}` from base `{}`", - field.declared_ty.display(db), - base_extra_items.declared_ty.display(db), + field.declared_ty.display(db, env), + base_extra_items.declared_ty.display(db, env), base.name(db), ), ); @@ -269,12 +275,16 @@ fn validate_typed_dict_openness<'db>( }; if child_extra_items.is_read_only() - || !child_extra_items - .declared_ty - .is_assignable_to(db, base_extra_items.declared_ty) - || !base_extra_items - .declared_ty - .is_assignable_to(db, child_extra_items.declared_ty) + || !child_extra_items.declared_ty.is_assignable_to( + db, + env, + base_extra_items.declared_ty, + ) + || !base_extra_items.declared_ty.is_assignable_to( + db, + env, + child_extra_items.declared_ty, + ) { report_invalid_typed_dict_openness( context, @@ -282,7 +292,7 @@ fn validate_typed_dict_openness<'db>( format_args!( "TypedDict `{}` must preserve mutable extra items type `{}` from base `{}`", class.name(db), - base_extra_items.declared_ty.display(db), + base_extra_items.declared_ty.display(db, env), base.name(db), ), ); @@ -293,19 +303,23 @@ fn validate_typed_dict_openness<'db>( !base_items.contains_key(*field_name) && (field.is_required() || field.is_read_only() - || !field - .declared_ty - .is_assignable_to(db, base_extra_items.declared_ty) - || !base_extra_items - .declared_ty - .is_assignable_to(db, field.declared_ty)) + || !field.declared_ty.is_assignable_to( + db, + env, + base_extra_items.declared_ty, + ) + || !base_extra_items.declared_ty.is_assignable_to( + db, + env, + field.declared_ty, + )) }) { report_invalid_typed_dict_openness( context, class, format_args!( "Item `{field_name}` must be mutable, not required, and consistent with extra items type `{}` from base `{}`", - base_extra_items.declared_ty.display(db), + base_extra_items.declared_ty.display(db, env), base.name(db), ), ); @@ -327,7 +341,7 @@ fn report_invalid_typed_dict_openness( } } -#[derive(Clone, Copy)] +#[derive(Clone)] enum TypedDictFieldOverrideReason<'db> { /// A required inherited field was relaxed to `NotRequired`. RequiredFieldMadeNotRequired, @@ -338,12 +352,14 @@ enum TypedDictFieldOverrideReason<'db> { /// A read-only inherited field's new type is not assignable to the base type. ReadOnlyTypeNotAssignable { db: &'db dyn Db, + env: ProgramEnvironment<'db>, child_ty: Type<'db>, base_ty: Type<'db>, }, /// A mutable inherited field's new type is not mutually assignable with the base type. MutableTypeIncompatible { db: &'db dyn Db, + env: ProgramEnvironment<'db>, child_ty: Type<'db>, base_ty: Type<'db>, }, @@ -372,23 +388,25 @@ impl std::fmt::Display for TypedDictFieldOverrideReason<'_> { } Self::ReadOnlyTypeNotAssignable { db, + env, child_ty, base_ty, } => write!( f, "Inherited read-only field type `{}` is not assignable from `{}`", - base_ty.display(*db), - child_ty.display(*db), + base_ty.display(*db, env), + child_ty.display(*db, env), ), Self::MutableTypeIncompatible { db, + env, child_ty, base_ty, } => write!( f, "Inherited mutable field type `{}` is incompatible with `{}`", - base_ty.display(*db), - child_ty.display(*db), + base_ty.display(*db, env), + child_ty.display(*db, env), ), } } @@ -397,6 +415,7 @@ impl std::fmt::Display for TypedDictFieldOverrideReason<'_> { impl<'db> TypedDictFieldOverrideReason<'db> { fn from_fields( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, child_field: &TypedDictField<'db>, base_field: &TypedDictField<'db>, ) -> Option { @@ -417,14 +436,14 @@ impl<'db> TypedDictFieldOverrideReason<'db> { let types_are_compatible = if base_field.is_read_only() { child_field .declared_ty - .is_assignable_to(db, base_field.declared_ty) + .is_assignable_to(db, env, base_field.declared_ty) } else { child_field .declared_ty - .is_assignable_to(db, base_field.declared_ty) + .is_assignable_to(db, env, base_field.declared_ty) && base_field .declared_ty - .is_assignable_to(db, child_field.declared_ty) + .is_assignable_to(db, env, child_field.declared_ty) }; if types_are_compatible { @@ -434,12 +453,14 @@ impl<'db> TypedDictFieldOverrideReason<'db> { Some(if base_field.is_read_only() { Self::ReadOnlyTypeNotAssignable { db, + env: env.clone(), child_ty: child_field.declared_ty, base_ty: base_field.declared_ty, } } else { Self::MutableTypeIncompatible { db, + env: env.clone(), child_ty: child_field.declared_ty, base_ty: base_field.declared_ty, } @@ -452,7 +473,7 @@ fn report_typed_dict_field_override<'db>( context: &InferContext<'db, '_>, class: StaticClassLiteral<'db>, field_name: &str, - reason: TypedDictFieldOverrideReason<'db>, + reason: &TypedDictFieldOverrideReason<'db>, base_name: &str, base_definition: Option>, own_field_definition: Option>, @@ -511,7 +532,7 @@ fn add_definition_subdiagnostic<'db>( }; let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let mut sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Field declaration"); sub.annotate( Annotation::secondary( diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/typeguard.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/typeguard.rs index 374cecad8b..d0b7ca669c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/typeguard.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/typeguard.rs @@ -15,6 +15,7 @@ pub(crate) fn check_type_guard_definition<'db>( }; let db = context.db(); + let env = context.program_environment(); let overload = function.literal(db).last_definition; let signature = overload.signature(db); @@ -52,14 +53,14 @@ pub(crate) fn check_type_guard_definition<'db>( // For `TypeIs`, check that the narrowed type is assignable to the parameter type. if let Some(narrowed_ty) = narrowed_type { let param_ty = first_narrowed_param.annotated_type(); - if !narrowed_ty.is_assignable_to(db, param_ty) + if !narrowed_ty.is_assignable_to(db, env, param_ty) && let Some(builder) = context.report_lint(&INVALID_TYPE_GUARD_DEFINITION, returns_expr) { builder.into_diagnostic(format_args!( "Narrowed type `{narrowed}` is not assignable \ to the declared parameter type `{param}`", - narrowed = narrowed_ty.display(db), - param = param_ty.display(db) + narrowed = narrowed_ty.display(db, env), + param = param_ty.display(db, env) )); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index d350c25301..ec393c9650 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -32,7 +32,7 @@ use crate::types::{ TypeAliasType, TypeAndQualifiers, TypeContext, TypeVarBoundOrConstraints, UnionType, UnionTypeInstance, any_over_type, todo_type, }; -use crate::{Db, FxOrderSet}; +use crate::{Db, FxOrderSet, ProgramEnvironment}; use ty_python_core::definition::Definition; use ty_python_core::place::{PlaceExpr, PlaceExprRef}; use ty_python_core::scope::FileScopeId; @@ -69,45 +69,52 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn imp<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, visitor: &TypedDictKeyExpectedTypeVisitor<'db>, ) -> Option> { match ty { Type::TypedDict(typed_dict) => { if typed_dict.explicit_extra_items(db).is_some() { - return Some(KnownClass::Str.to_instance(db)); + return Some(KnownClass::Str.to_instance(db, env)); } let keys = typed_dict .items(db) .keys() .map(|key| Type::string_literal(db, key)) .collect_vec(); - (!keys.is_empty()).then(|| UnionType::from_elements(db, keys)) + (!keys.is_empty()).then(|| UnionType::from_elements(db, env, keys)) } Type::Union(union) => { let keys = union .elements(db) .iter() - .filter_map(|element| imp(db, *element, visitor)) + .filter_map(|element| imp(db, env, *element, visitor)) .collect_vec(); - (!keys.is_empty()).then(|| UnionType::from_elements(db, keys)) + (!keys.is_empty()).then(|| UnionType::from_elements(db, env, keys)) } Type::Intersection(intersection) => { let keys = intersection .positive(db) .iter() - .filter_map(|element| imp(db, *element, visitor)) + .filter_map(|element| imp(db, env, *element, visitor)) .collect_vec(); - (!keys.is_empty()).then(|| UnionType::from_elements(db, keys)) + (!keys.is_empty()).then(|| UnionType::from_elements(db, env, keys)) } Type::TypeAlias(alias) => { - visitor.visit(db, ty, || imp(db, alias.value_type(db), visitor)) + visitor.visit(db, ty, || imp(db, env, alias.value_type(db), visitor)) } _ => None, } } + let db = self.db(); - imp(self.db(), ty, &TypedDictKeyExpectedTypeVisitor::default()) + imp( + db, + self.program_environment(), + ty, + &TypedDictKeyExpectedTypeVisitor::default(), + ) } fn store_typed_dict_key_expected_type(&mut self, slice: &ast::Expr, value_ty: Type<'db>) { @@ -171,6 +178,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_ty: Type<'db>, subscript: &ast::ExprSubscript, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); let ast::ExprSubscript { @@ -178,7 +186,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { node_index: _, value: _, slice, - ctx, + ctx: expr_context, } = subscript; self.store_typed_dict_key_expected_type(slice, value_ty); @@ -202,14 +210,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Even if we can obtain the subscript type based on the assignments, we still perform default type inference // (to store the expression type and to report errors). let slice_ty = self.infer_expression(slice, TypeContext::default()); - self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); + self.infer_subscript_expression_types( + subscript, + value_ty, + slice_ty, + *expr_context, + ); return ty; } } } - let tuple_generic_alias = |db: &'db dyn Db, tuple: Option>| { - let tuple = tuple.unwrap_or_else(|| TupleType::homogeneous(db, Type::unknown())); + let tuple_generic_alias = |env: &ProgramEnvironment<'db>, tuple: Option>| { + let tuple = tuple.unwrap_or_else(|| TupleType::homogeneous(db, env, Type::unknown())); Type::from(tuple.to_class_type(db)) }; @@ -222,7 +235,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // updating all of the subscript logic below to use custom callables for all of the _other_ // special cases, too. if class.is_tuple(db) { - return tuple_generic_alias(db, self.infer_tuple_type_expression(subscript)); + return tuple_generic_alias(env, self.infer_tuple_type_expression(subscript)); } else if class.is_known(db, KnownClass::Type) { let argument_ty = self.infer_type_expression(slice); return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( @@ -253,7 +266,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } Type::SpecialForm(special_form) => match special_form { SpecialFormType::Tuple => { - return tuple_generic_alias(db, self.infer_tuple_type_expression(subscript)); + return tuple_generic_alias(env, self.infer_tuple_type_expression(subscript)); } SpecialFormType::Literal => match self.infer_literal_parameter_type(slice) { Ok(result) => { @@ -299,12 +312,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if ty.is_none(db) { return ty; } - return Type::KnownInstance(KnownInstanceType::UnionType( UnionTypeInstance::new( db, None, - Ok(UnionType::from_two_elements(db, ty, Type::none(db))), + Ok(UnionType::from_two_elements( + db, + env, + ty, + Type::none(db, env), + )), ), )); } @@ -316,7 +333,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { UnionTypeInstance::new( db, None, - Ok(UnionType::from_elements(db, elements)), + Ok(UnionType::from_elements(db, env, elements)), ), )); @@ -415,7 +432,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .collect(); return class - .to_specialized_class_type(db, arg_types) + .to_specialized_class_type(db, env, arg_types) .map(Type::from) .unwrap_or_else(Type::unknown); } @@ -435,17 +452,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut variables = FxOrderSet::default(); slice_ty.bind_and_find_all_legacy_typevars( db, + env, self.typevar_binding_context, &mut variables, ); - let generic_context = GenericContext::from_typevar_instances(db, variables); + let generic_context = GenericContext::from_typevar_instances(db, env, variables); return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); } _ => {} } let slice_ty = self.infer_expression(slice, TypeContext::default()); - let result_ty = self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); + let result_ty = + self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *expr_context); self.narrow_expr_with_applicable_constraints(subscript, result_ty, &constraint_keys) } @@ -456,6 +475,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { generic_class: StaticClassLiteral<'db>, generic_context: GenericContext<'db>, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); let specialize = &|types: &[Option>]| { Type::from(generic_class.apply_specialization(db, |_| { @@ -475,7 +495,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .is_some_and(|protocol| { protocol .interface(db) - .includes_generic_writable_instance_member(db, "__class__", generic_context) + .includes_generic_writable_instance_member( + db, + env, + "__class__", + generic_context, + ) }); let previously_disabled_int_float_special_case = disable_int_float_special_case.then(|| { @@ -591,7 +616,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return; }; let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let range = definition.focus_range(db, &module).range(); diagnostic.annotate( Annotation::secondary(Span::from(file).with_range(range)) @@ -610,6 +635,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { source_index: usize, } + let env = self.program_environment(); let db = self.db(); let constraints = ConstraintSetBuilder::new(); let slice_node = subscript.slice.as_ref(); @@ -751,7 +777,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && variable.suffix_elements().is_empty() && let Some(variable_type) = variable.variable().homogeneous_type() { - tuple_builder = tuple_builder.concat(db, &tuple); + tuple_builder = tuple_builder.concat(db, env, &tuple); packed.push(TypeArgument { ty: Some(variable_type), ..*type_argument @@ -804,12 +830,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .type_expression_flags(type_argument.node) .contains(TypeExpressionFlags::UNPACK); if is_unpack && let Some(tuple) = provided_type.exact_tuple_instance_spec(db) { - tuple_builder = tuple_builder.concat(db, &tuple); + tuple_builder = tuple_builder.concat(db, env, &tuple); } else if is_unpack && let Type::TypeVar(typevar) = provided_type && typevar.is_typevartuple(db) { - tuple_builder = tuple_builder.concat_variadic_typevar(db, typevar); + tuple_builder = tuple_builder.concat_variadic_typevar(db, env, typevar); } else { tuple_builder.push(provided_type); } @@ -831,7 +857,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && variable.suffix_elements().is_empty() && let Some(variable_type) = variable.variable().homogeneous_type() { - tuple_builder = tuple_builder.concat(db, &tuple); + tuple_builder = tuple_builder.concat(db, env, &tuple); packed_suffix.push(TypeArgument { ty: Some(variable_type), ..*type_argument @@ -869,7 +895,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { node: expanded_type_arguments .get(typevartuple_index) .map_or(slice_node, |argument| argument.node), - ty: Some(Type::tuple(TupleType::new(db, &tuple_builder.build()))), + ty: Some(Type::tuple(TupleType::new(db, env, &tuple_builder.build()))), source_index: expanded_type_arguments .get(typevartuple_index) .map_or(0, |argument| argument.source_index), @@ -950,7 +976,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; }; let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let range = definition.focus_range(db, &module).range(); diagnostic.annotate( Annotation::secondary(Span::from(file).with_range(range)) @@ -970,11 +996,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // against bounds/constraints, but recording the expression for deferred // checking at end of scope. This would avoid a lot of cycles caused by eagerly // doing assignment checks here. - match typevar.typevar(db).bound_or_constraints(db) { + match typevar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { if provided_type - .when_assignable_to(db, bound, &constraints, TypeVarSet::None) - .is_never_satisfied(db) + .when_assignable_to(db, env, bound, &constraints, TypeVarSet::None) + .is_never_satisfied(db, env) { if let Some(builder) = self .context @@ -983,14 +1009,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diagnostic = builder.into_diagnostic(format_args!( "Type `{}` is not assignable to upper bound `{}` \ of type variable `{}`", - provided_type.display(db), - bound.display(db), + provided_type.display(db, env), + bound.display(db, env), typevar.identity(db).display(db), )); add_typevar_definition(db, &mut diagnostic, typevar); provided_type - .assignability_error_context(db, bound) - .attach_to(db, &mut diagnostic); + .assignability_error_context(db, env, bound) + .attach_to(db, env, &mut diagnostic); } error = Some(ExplicitSpecializationError::UnsatisfiedBound); specialization_types.push(Some(Type::unknown())); @@ -1006,11 +1032,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if provided_type .when_assignable_to( db, - typevar_constraints.as_type(db), + env, + typevar_constraints.as_type(db, env), &constraints, TypeVarSet::None, ) - .is_never_satisfied(db) + .is_never_satisfied(db, env) { if let Some(builder) = self .context @@ -1019,11 +1046,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diagnostic = builder.into_diagnostic(format_args!( "Type `{}` does not satisfy constraints `{}` \ of type variable `{}`", - provided_type.display(db), + provided_type.display(db, env), typevar_constraints .elements(db) .iter() - .map(|c| c.display(db)) + .map(|c| c.display(db, env)) .format("`, `"), typevar.identity(db).display(db), )); @@ -1079,12 +1106,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(builder) = self.context.report_lint(&NOT_SUBSCRIPTABLE, subscript) { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot subscript non-generic type `{}`", - value_ty.display(db) + value_ty.display(db, env) )); let already_specialized = match value_ty { Type::GenericAlias(_) => true, Type::KnownInstance(KnownInstanceType::UnionType(union)) => union - .value_expression_types(db) + .value_expression_types(db, env) .is_ok_and(|mut tys| tys.any(|ty| ty.is_generic_alias())), _ => false, }; @@ -1127,6 +1154,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { slice_node, Type::heterogeneous_tuple( db, + env, inferred_type_arguments .into_iter() .map(|ty| ty.unwrap_or(Type::unknown())), @@ -1146,7 +1174,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(if typevar.is_paramspec(db) { Type::paramspec_value_callable(db, Parameters::unknown()) } else if typevar.is_typevartuple(db) { - Type::homogeneous_tuple(db, Type::unknown()) + Type::homogeneous_tuple(db, env, Type::unknown()) } else { Type::unknown() }) @@ -1375,6 +1403,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { slice_ty: Type<'db>, expr_context: ExprContext, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); if let Some(origin) = match value_ty { @@ -1454,6 +1483,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let subscript_result = match value_ty { Type::SpecialForm(SpecialFormType::Generic) => infer_legacy_generic_subscript( db, + env, self.index, self.scope().file_scope_id(db), self.typevar_binding_context, @@ -1463,6 +1493,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ), Type::SpecialForm(SpecialFormType::Protocol) => infer_legacy_generic_subscript( db, + env, self.index, self.scope().file_scope_id(db), self.typevar_binding_context, @@ -1475,13 +1506,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut variables = FxOrderSet::default(); slice_ty.bind_and_find_all_legacy_typevars( db, + env, self.typevar_binding_context, &mut variables, ); - let generic_context = GenericContext::from_typevar_instances(db, variables); + let generic_context = GenericContext::from_typevar_instances(db, env, variables); Ok(Type::Dynamic(DynamicType::UnknownGeneric(generic_context))) } - _ => value_ty.subscript(db, slice_ty, expr_context), + _ => value_ty.subscript(db, env, slice_ty, expr_context), }; subscript_result.unwrap_or_else(|e| { @@ -1492,7 +1524,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { pub(super) fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> { let db = self.db(); - + let env = self.program_environment(); let ast::ExprSlice { range: _, node_index: _, @@ -1507,10 +1539,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { KnownClass::Slice.to_specialized_instance( db, + env, &[ - ty_lower.unwrap_or_else(|| Type::none(db)), - ty_upper.unwrap_or_else(|| Type::none(db)), - ty_step.unwrap_or_else(|| Type::none(db)), + ty_lower.unwrap_or_else(|| Type::none(db, env)), + ty_upper.unwrap_or_else(|| Type::none(db, env)), + ty_step.unwrap_or_else(|| Type::none(db, env)), ], ) } @@ -1522,6 +1555,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { rhs_value: &ast::Expr, infer_rhs_value: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, ) -> bool { + let env = self.program_environment(); let ast::ExprSubscript { range: _, node_index: _, @@ -1550,9 +1584,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // unannotated collection initializer. if is_valid_assignment && let Some(collection_def) = self.index.unannotated_collection_initializer(object) - && let Some((class_literal, _)) = object_ty.class_specialization(db) + && let Some((class_literal, _)) = object_ty.class_specialization(db, env) { - let identity_instance = Type::instance(db, class_literal.identity_specialization(db)); + let identity_instance = + Type::instance(db, env, class_literal.identity_specialization(db)); let collection_generic_context = class_literal.generic_context(db); let ast_arguments = [ @@ -1569,14 +1604,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) = identity_instance .member_lookup_with_policy( db, + env, "__setitem__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) .place { let mut identity_bindings = dunder_callable - .bindings(db) - .match_parameters(db, &call_arguments) + .bindings(db, env) + .match_parameters(db, env, &call_arguments) // Perform inference against the type variables on the receiver's generic context. .with_generic_context(db, collection_generic_context); @@ -1637,13 +1673,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { infer_rhs_value: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, emit_diagnostic: bool, ) -> bool { + let env = self.program_environment(); let db = self.db(); let attach_original_type_info = |diagnostic: &mut LintDiagnosticGuard| { if let Some(full_object_ty) = full_object_ty { diagnostic.info(format_args!( "The full type of the subscripted object is `{}`", - full_object_ty.display(db) + full_object_ty.display(db, env) )); } }; @@ -1719,7 +1756,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::EnumComplement(complement) => self.validate_subscript_assignment_impl( target, full_object_ty, - complement.remaining_literal_union(db), + complement.remaining_literal_union(db, env), infer_slice_ty, rhs_value_node, infer_rhs_value, @@ -1743,12 +1780,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return true; } - if slice_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) - && let Some(expected_ty) = typed_dict.arbitrary_key_mutation_type(db) + if slice_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) + && let Some(expected_ty) = typed_dict.arbitrary_key_mutation_type(db, env) { let rhs_value_ty = infer_rhs_value(self, TypeContext::new(Some(expected_ty))); - if rhs_value_ty.is_assignable_to(db, expected_ty) { + if rhs_value_ty.is_assignable_to(db, env, expected_ty) { return true; } @@ -1759,13 +1796,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot assign value of type `{}` to key of type `{}` on TypedDict `{}`", - rhs_value_ty.display(db), - slice_ty.display(db), - object_ty.display(db), + rhs_value_ty.display(db, env), + slice_ty.display(db, env), + object_ty.display(db, env), )); diagnostic.set_primary_annotation_message(format_args!( "Expected value assignable to `{}`", - expected_ty.display(db) + expected_ty.display(db, env) )); attach_original_type_info(&mut diagnostic); } @@ -1773,11 +1810,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let rhs_value_ty = infer_rhs_value(self, TypeContext::default()); - let assigned_d = rhs_value_ty.display(db); - let value_d = object_ty.display(db); + let assigned_d = rhs_value_ty.display(db, env); + let value_d = object_ty.display(db, env); - if slice_ty.is_assignable_to(db, Type::literal_string()) - && !slice_ty.is_equivalent_to(db, Type::literal_string()) + if slice_ty.is_assignable_to(db, env, Type::literal_string()) + && !slice_ty.is_equivalent_to(db, env, Type::literal_string()) { if let Some(builder) = self .context @@ -1785,7 +1822,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot assign value of type `{assigned_d}` to key of type `{}` on TypedDict `{value_d}`", - slice_ty.display(db) + slice_ty.display(db, env) )); attach_original_type_info(&mut diagnostic); } @@ -1796,7 +1833,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut diagnostic = builder.into_diagnostic(format_args!( "TypedDict `{value_d}` can only be subscripted with a string literal key, got key of type `{}`.", - slice_ty.display(db) + slice_ty.display(db, env) )); attach_original_type_info(&mut diagnostic); } @@ -1864,7 +1901,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let Err(call_dunder_err) = self.infer_and_try_call_dunder( - db, object_ty, "__setitem__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, @@ -1885,7 +1921,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut diagnostic = builder.into_diagnostic(format_args!( "Method `__setitem__` of type `{}` may be missing", - object_ty.display(db), + object_ty.display(db, env), )); attach_original_type_info(&mut diagnostic); } @@ -1904,8 +1940,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diagnostic = builder.into_diagnostic(format_args!( "Method `__setitem__` of type `{}` is not callable \ on object of type `{}`", - bindings.callable_type().display(db), - object_ty.display(db), + bindings.callable_type().display(db, env), + object_ty.display(db, env), )); attach_original_type_info(&mut diagnostic); } @@ -1935,42 +1971,45 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { target.range.cover(rhs_value_node.range()), ) { - let assigned_d = rhs_value_ty.display(db); - let object_d = object_ty.display(db); + let assigned_d = rhs_value_ty.display(db, env); + let object_d = object_ty.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid subscript assignment with key of type `{}` and value of \ type `{assigned_d}` on object of type `{object_d}`", - slice_ty.display(db), + slice_ty.display(db, env), )); // Special diagnostic for dictionaries if let Some([expected_key_ty, expected_value_ty]) = object_ty - .known_specialization(db, KnownClass::Dict) + .known_specialization(db, env, KnownClass::Dict) .map(|s| s.types(db)) { - if !slice_ty.is_assignable_to(db, *expected_key_ty) { + if !slice_ty.is_assignable_to(db, env, *expected_key_ty) + { diagnostic.annotate( self.context .secondary(target.slice.as_ref()) .message(format_args!( "Expected key of type `{}`, got `{}`", - expected_key_ty.display(db), - slice_ty.display(db), + expected_key_ty.display(db, env), + slice_ty.display(db, env), )), ); } - if !rhs_value_ty - .is_assignable_to(db, *expected_value_ty) - { + if !rhs_value_ty.is_assignable_to( + db, + env, + *expected_value_ty, + ) { diagnostic.annotate( self.context.secondary(rhs_value_node).message( format_args!( "Expected value of type `{}`, got `{}`", - expected_value_ty.display(db), - rhs_value_ty.display(db), + expected_value_ty.display(db, env), + rhs_value_ty.display(db, env), ), ), ); @@ -1988,8 +2027,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut diagnostic = builder.into_diagnostic(format_args!( "Method `__setitem__` of type `{}` may not be callable on object of type `{}`", - bindings.callable_type().display(db), - object_ty.display(db), + bindings.callable_type().display(db, env), + object_ty.display(db, env), )); attach_original_type_info(&mut diagnostic); } @@ -2004,28 +2043,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot assign to a subscript on an object of type `{}`", - object_ty.display(db), + object_ty.display(db, env), )); attach_original_type_info(&mut diagnostic); // If it's a user-defined class, suggest adding a `__setitem__` method. if object_ty .as_nominal_instance() - .and_then(|instance| instance.class(db).static_class_literal(db)) + .and_then(|instance| { + instance.class(db, env).static_class_literal(db) + }) .and_then(|(class_literal, _)| { - file_to_module(db, class_literal.file(db)) + file_to_module(db, class_literal.python_file(db)) }) .and_then(|module| module.search_path(db)) .is_some_and(ty_module_resolver::SearchPath::is_first_party) { diagnostic.help(format_args!( "Consider adding a `__setitem__` method to `{}`.", - object_ty.display(db), + object_ty.display(db, env), )); } else { diagnostic.info(format_args!( "`{}` does not have a `__setitem__` method.", - object_ty.display(db), + object_ty.display(db, env), )); } } @@ -2053,13 +2094,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { object_ty: Type<'db>, slice_ty: Type<'db>, ) { + let env = self.program_environment(); let db = self.db(); let attach_original_type_info = |diagnostic: &mut LintDiagnosticGuard| { if let Some(full_object_ty) = full_object_ty { diagnostic.info(format_args!( "The full type of the subscripted object is `{}`", - full_object_ty.display(db) + full_object_ty.display(db, env) )); } }; @@ -2101,7 +2143,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::EnumComplement(complement) => self.validate_subscript_deletion_impl( target, full_object_ty, - complement.remaining_literal_union(db), + complement.remaining_literal_union(db, env), slice_ty, ), @@ -2117,9 +2159,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && string_literal_values(db, slice_ty).is_some_and(|mut literals| { literals.all(|literal| !typed_dict.items(db).contains_key(literal)) }); - let can_delete_arbitrary_key = slice_ty - .is_assignable_to(db, KnownClass::Str.to_instance(db)) - && typed_dict.supports_arbitrary_key_deletion(db); + let can_delete_arbitrary_key = + slice_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) + && typed_dict.supports_arbitrary_key_deletion(db); if can_delete_extra_literals || can_delete_arbitrary_key { return; } @@ -2127,138 +2169,144 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match object_ty.try_call_dunder( db, + env, "__delitem__", CallArguments::positional([slice_ty]), TypeContext::default(), ) { Ok(_) => {} - Err(err) => match err { - CallDunderError::PossiblyUnbound { .. } => { - if let Some(builder) = self - .context - .report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` may be missing", - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); + Err(err) => { + match err { + CallDunderError::PossiblyUnbound { .. } => { + if let Some(builder) = self + .context + .report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` may be missing", + object_ty.display(db, env), + )); + attach_original_type_info(&mut diagnostic); + } } - } - CallDunderError::CallError(call_error_kind, bindings, _) => { - match call_error_kind { - CallErrorKind::NotCallable => { - if let Some(builder) = - self.context.report_lint(&CALL_NON_CALLABLE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( + CallDunderError::CallError(call_error_kind, bindings, _) => { + match call_error_kind { + CallErrorKind::NotCallable => { + if let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( "Method `__delitem__` of type `{}` is not callable \ on object of type `{}`", - bindings.callable_type().display(db), - object_ty.display(db), + bindings.callable_type().display(db, env), + object_ty.display(db, env), )); - attach_original_type_info(&mut diagnostic); + attach_original_type_info(&mut diagnostic); + } } - } - CallErrorKind::BindingError => { - // For deletions of string literal keys on `TypedDict`, provide - // a more detailed diagnostic. - if let Some(typed_dict) = object_ty.as_typed_dict() { - if let Some(string_literal) = slice_ty.as_string_literal() { - let key = string_literal.value(db); - let items = typed_dict.items(db); - - if let Some(field) = items.get(key) { - // Key exists but is required (i.e., can't be deleted). - report_cannot_delete_typed_dict_key( - &self.context, - (&*target.slice).into(), - typed_dict, - key, - Some(field), - TypedDictDeleteErrorKind::RequiredKey, - ); - } else if typed_dict - .explicit_extra_items(db) - .is_some_and(|extra_items| { - extra_items.is_read_only() - }) + CallErrorKind::BindingError => { + // For deletions of string literal keys on `TypedDict`, provide + // a more detailed diagnostic. + if let Some(typed_dict) = object_ty.as_typed_dict() { + if let Some(string_literal) = + slice_ty.as_string_literal() { - report_cannot_delete_typed_dict_key( - &self.context, - (&*target.slice).into(), - typed_dict, - key, - None, - TypedDictDeleteErrorKind::ReadOnlyExtraItem, - ); + let key = string_literal.value(db); + let items = typed_dict.items(db); + + if let Some(field) = items.get(key) { + // Key exists but is required (i.e., can't be deleted). + report_cannot_delete_typed_dict_key( + &self.context, + (&*target.slice).into(), + typed_dict, + key, + Some(field), + TypedDictDeleteErrorKind::RequiredKey, + ); + } else if typed_dict + .explicit_extra_items(db) + .is_some_and(|extra_items| { + extra_items.is_read_only() + }) + { + report_cannot_delete_typed_dict_key( + &self.context, + (&*target.slice).into(), + typed_dict, + key, + None, + TypedDictDeleteErrorKind::ReadOnlyExtraItem, + ); + } else { + // Key doesn't exist. + report_cannot_delete_typed_dict_key( + &self.context, + (&*target.slice).into(), + typed_dict, + key, + None, + TypedDictDeleteErrorKind::UnknownKey, + ); + } } else { - // Key doesn't exist. - report_cannot_delete_typed_dict_key( - &self.context, - (&*target.slice).into(), - typed_dict, - key, - None, - TypedDictDeleteErrorKind::UnknownKey, - ); + // Non-string-literal key on `TypedDict`. + if let Some(builder) = self + .context + .report_lint(&INVALID_ARGUMENT_TYPE, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` cannot be called \ + with key of type `{}` on object of type `{}`", + bindings.callable_type().display(db, env), + slice_ty.display(db, env), + object_ty.display(db, env), + )); + attach_original_type_info(&mut diagnostic); + } } } else { - // Non-string-literal key on `TypedDict`. + // Non-`TypedDict` object if let Some(builder) = self .context .report_lint(&INVALID_ARGUMENT_TYPE, target) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` cannot be called \ - with key of type `{}` on object of type `{}`", - bindings.callable_type().display(db), - slice_ty.display(db), - object_ty.display(db), - )); + "Method `__delitem__` of type `{}` cannot be called \ + with key of type `{}` on object of type `{}`", + bindings.callable_type().display(db, env), + slice_ty.display(db, env), + object_ty.display(db, env), + )); attach_original_type_info(&mut diagnostic); } } - } else { - // Non-`TypedDict` object + } + CallErrorKind::PossiblyNotCallable => { if let Some(builder) = - self.context.report_lint(&INVALID_ARGUMENT_TYPE, target) + self.context.report_lint(&CALL_NON_CALLABLE, target) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` cannot be called \ - with key of type `{}` on object of type `{}`", - bindings.callable_type().display(db), - slice_ty.display(db), - object_ty.display(db), - )); - attach_original_type_info(&mut diagnostic); - } - } - } - CallErrorKind::PossiblyNotCallable => { - if let Some(builder) = - self.context.report_lint(&CALL_NON_CALLABLE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( "Method `__delitem__` of type `{}` may not be callable \ on object of type `{}`", - bindings.callable_type().display(db), - object_ty.display(db), + bindings.callable_type().display(db, env), + object_ty.display(db, env), )); - attach_original_type_info(&mut diagnostic); + attach_original_type_info(&mut diagnostic); + } } } } + CallDunderError::MethodNotAvailable => { + report_not_subscriptable( + &self.context, + target, + object_ty, + "__delitem__", + ); + } } - CallDunderError::MethodNotAvailable => { - report_not_subscriptable( - &self.context, - target, - object_ty, - "__delitem__", - ); - } - }, + } } } } @@ -2270,6 +2318,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { object_ty .try_call_dunder( db, + self.program_environment(), "__delitem__", CallArguments::positional([slice_ty]), TypeContext::default(), @@ -2350,8 +2399,10 @@ impl<'db> LegacyGenericContextError<'db> { /// Validate the type arguments to `Generic[...]` or `Protocol[...]`, returning /// either the resulting [`GenericContext`] or a [`SubscriptError`]. +#[expect(clippy::too_many_arguments)] fn infer_legacy_generic_subscript<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, index: &'db SemanticIndex<'db>, file_scope_id: FileScopeId, typevar_binding_context: Option>, @@ -2359,8 +2410,14 @@ fn infer_legacy_generic_subscript<'db>( origin: LegacyGenericOrigin, wrap_ok: impl FnOnce(GenericContext<'db>) -> KnownInstanceType<'db>, ) -> Result, SubscriptError<'db>> { - match legacy_generic_class_context(db, index, file_scope_id, typevar_binding_context, slice_ty) - { + match legacy_generic_class_context( + db, + env, + index, + file_scope_id, + typevar_binding_context, + slice_ty, + ) { Ok(context) => Ok(Type::KnownInstance(wrap_ok(context))), Err(LegacyGenericContextError::InvalidArgument(argument_ty)) => Err(SubscriptError::new( Type::unknown(), @@ -2395,6 +2452,7 @@ fn infer_legacy_generic_subscript<'db>( /// that each argument is a type variable. fn legacy_generic_class_context<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, index: &'db SemanticIndex<'db>, file_scope_id: FileScopeId, typevar_binding_context: Option>, @@ -2432,7 +2490,7 @@ fn legacy_generic_class_context<'db>( if bound.is_typevartuple(db) { validated_typevars.insert(bound); return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked(Some( - GenericContext::from_typevar_instances(db, validated_typevars), + GenericContext::from_typevar_instances(db, env, validated_typevars), ))); } if !validated_typevars.insert(bound) { @@ -2453,7 +2511,7 @@ fn legacy_generic_class_context<'db>( ) { return Err(LegacyGenericContextError::TypeVarTupleMustBeUnpacked(None)); - } else if any_over_type(db, argument_ty, true, |inner_ty| match inner_ty { + } else if any_over_type(db, env, argument_ty, true, |inner_ty| match inner_ty { Type::NominalInstance(nominal) => matches!( nominal.known_class(db), Some(KnownClass::TypeVarTuple | KnownClass::ExtensionsTypeVarTuple) @@ -2467,6 +2525,7 @@ fn legacy_generic_class_context<'db>( } Ok(GenericContext::from_typevar_instances( db, + env, validated_typevars, )) } diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_call.rs b/crates/ty_python_semantic/src/types/infer/builder/type_call.rs index fecd8bf33c..41c4fb9030 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_call.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_call.rs @@ -31,6 +31,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { call_expr: &ast::ExprCall, definition: Option>, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); let ast::Arguments { @@ -49,7 +50,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let arg_type = self.infer_expression(single, TypeContext::default()); return if keywords.is_empty() { - arg_type.dunder_class(db) + arg_type.dunder_class(db, env) } else { if keywords.iter().any(|keyword| keyword.arg.is_some()) && let Some(builder) = @@ -173,11 +174,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; if !matches!(namespace_type, Type::TypedDict(_)) - && !namespace_type.is_assignable_to( - db, - KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]), - ) + && { + !namespace_type.is_assignable_to( + db, + env, + KnownClass::Dict.to_specialized_instance( + db, + env, + &[KnownClass::Str.to_instance(db, env), Type::any()], + ), + ) + } && let Some(builder) = self .context .report_lint(&INVALID_ARGUMENT_TYPE, namespace_arg) @@ -186,7 +193,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .into_diagnostic("Invalid argument to parameter 3 (`namespace`) of `type()`"); diagnostic.set_primary_annotation_message(format_args!( "Expected `dict[str, Any]`, found `{}`", - namespace_type.display(db) + namespace_type.display(db, env) )); } @@ -194,14 +201,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let name = if let Some(literal) = name_type.as_string_literal() { literal.value(db) } else { - if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + if !name_type.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) { let mut diagnostic = builder.into_diagnostic("Invalid argument to parameter 1 (`name`) of `type()`"); diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", - name_type.display(db) + name_type.display(db, env) )); } "" @@ -295,9 +302,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { call_expr.into(), dynamic_class.name(db), metaclass1, - base1.display(db), + base1.display(db, env), metaclass2, - base2.display(db), + base2.display(db, env), ); } } @@ -321,7 +328,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; // Get the already-inferred class type from the initial pass. - let inferred_type = definition_expression_type(db, definition, call_expr); + let inferred_type = definition_expression_type(self.db(), definition, call_expr); let Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) = inferred_type else { return; }; diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index a91b7754a5..778b7cf259 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -27,7 +27,7 @@ use crate::types::{ TypeGuardType, TypeIsType, TypeMapping, TypeVarKind, UnionBuilder, UnionType, any_over_type, todo_type, }; -use crate::{FxOrderSet, Program, add_inferred_python_version_hint_to_diagnostic}; +use crate::{FxOrderSet, add_inferred_python_version_hint_to_diagnostic}; /// Type expressions impl<'db> TypeInferenceBuilder<'db, '_> { @@ -110,6 +110,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ty: Type<'db>, annotation: &ast::Expr, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); if annotation.is_attribute_expr() && let Type::TypeVar(tvar) = ty && tvar.paramspec_attr(self.db()).is_some() @@ -118,9 +120,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } report_missing_type_arguments(&self.context, ty, annotation); let result_ty = ty - .default_specialize(self.db()) + .default_specialize(db, env) .in_type_expression( - self.db(), + db, self.scope(), self.typevar_binding_context, self.inference_flags(), @@ -133,6 +135,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// Infer the type of a type expression without storing the result. pub(super) fn infer_type_expression_no_store(&mut self, expression: &ast::Expr) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ignore_runtime_errors = |builder: &Self| { builder.deferred_state.is_deferred() || builder.in_stub() @@ -183,7 +187,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - ast::Expr::NoneLiteral(_literal) => Type::none(self.db()), + ast::Expr::NoneLiteral(_literal) => Type::none(db, env), // https://typing.python.org/en/latest/spec/annotations.html#string-annotations ast::Expr::StringLiteral(string) => self.infer_string_type_expression(string), @@ -259,7 +263,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .infer_expression(&binary.right, TypeContext::default()); let dunder_fails = Type::try_call_bin_op( - self.db(), + db, + env, left_type_value, ast::Operator::BitOr, right_type_value, @@ -276,8 +281,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let literal = match (left_type_value, right_type_value) { (Type::ClassLiteral(class), Type::LiteralValue(literal)) | (Type::LiteralValue(literal), Type::ClassLiteral(class)) - if class.metaclass(self.db()) - == KnownClass::Type.to_class_literal(self.db()) => + if class.metaclass(db) + == KnownClass::Type.to_class_literal(db, env) => { Some(literal) } @@ -297,15 +302,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut diagnostic = builder.into_diagnostic("Unsupported `|` operation"); - if left_type_value.is_equivalent_to(self.db(), right_type_value) { + if left_type_value.is_equivalent_to(db, env, right_type_value) { diagnostic.set_primary_annotation_message(format_args!( "Both operands have type `{}`", - left_type_value.display(self.db()) + left_type_value.display(db, env) )); diagnostic.set_concise_message(format_args!( "Operator `|` is unsupported between \ two objects of type `{}`", - left_type_value.display(self.db()) + left_type_value.display(db, env) )); } else { for (operand, ty) in [ @@ -315,15 +320,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { diagnostic.annotate( self.context.secondary(operand).message(format_args!( "Has type `{}`", - ty.display(self.db()) + ty.display(db, env) )), ); } diagnostic.set_concise_message(format_args!( "Operator `|` is unsupported between \ objects of type `{}` and `{}`", - left_type_value.display(self.db()), - right_type_value.display(self.db()) + left_type_value.display(db, env), + right_type_value.display(db, env) )); } @@ -341,7 +346,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ), _ => { let python_version = - Program::get(self.db()).python_version(self.db()); + self.program_environment().python_version(db); if python_version < PythonVersion::PY314 { diagnostic.info(format_args!( @@ -368,7 +373,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - UnionType::from_elements_leave_aliases(self.db(), [left_ty, right_ty]) + UnionType::from_elements_leave_aliases(db, env, [left_ty, right_ty]) } ast::Operator::BitAnd => { if let Some(builder) = @@ -389,7 +394,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let right_value = speculative_builder .infer_expression(&binary.right, TypeContext::default()); if Type::try_call_bin_op( - self.db(), + db, + env, left_value, ast::Operator::BitAnd, right_value, @@ -406,7 +412,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - IntersectionType::from_two_elements(self.db(), left_ty, right_ty) + IntersectionType::from_two_elements(db, env, left_ty, right_ty) } // anything else is an invalid annotation: op => { @@ -518,8 +524,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ast::Expr::List(list) => { - let db = self.db(); - if !self.in_string_annotation() { self.infer_list_expression(list, TypeContext::default()); } @@ -537,11 +541,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if inner_type.is_hintable(self.db()) { let hinted_type = - KnownClass::List.to_specialized_instance(db, &[inner_type]); + KnownClass::List.to_specialized_instance(db, env, &[inner_type]); diagnostic.set_primary_annotation_message(format_args!( "Did you mean `{}`?", - hinted_type.display(self.db()), + hinted_type.display(db, env), )); } } @@ -571,10 +575,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .collect(); if inner_types.iter().all(|ty| ty.is_hintable(self.db())) { - let hinted_type = Type::heterogeneous_tuple(self.db(), inner_types); + let hinted_type = Type::heterogeneous_tuple(db, env, inner_types); diagnostic.set_primary_annotation_message(format_args!( "Did you mean `{}`?", - hinted_type.display(self.db()), + hinted_type.display(db, env), )); } } @@ -633,7 +637,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .speculate_without_diagnostics() .infer_expression(operand, TypeContext::default()); if let Err(error) = operand_value.try_call_dunder( - self.db(), + db, + env, "__invert__", CallArguments::none(), TypeContext::default(), @@ -648,7 +653,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - operand_ty.negate(self.db()) + operand_ty.negate(db, env) } ast::Expr::UnaryOp(unary) => { @@ -714,11 +719,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let key_type = speculative.infer_type_expression(key); let value_type = speculative.infer_type_expression(value); if key_type.is_hintable(self.db()) && value_type.is_hintable(self.db()) { - let hinted_type = KnownClass::Dict - .to_specialized_instance(self.db(), &[key_type, value_type]); + let hinted_type = KnownClass::Dict.to_specialized_instance( + db, + env, + &[key_type, value_type], + ); diagnostic.set_primary_annotation_message(format_args!( "Did you mean `{}`?", - hinted_type.display(self.db()), + hinted_type.display(db, env), )); } } @@ -742,11 +750,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if inner_type.is_hintable(self.db()) { let hinted_type = - KnownClass::Set.to_specialized_instance(self.db(), &[inner_type]); + KnownClass::Set.to_specialized_instance(db, env, &[inner_type]); diagnostic.set_primary_annotation_message(format_args!( "Did you mean `{}`?", - hinted_type.display(self.db()), + hinted_type.display(db, env), )); } } @@ -947,6 +955,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } fn infer_starred_type_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> { + let db = self.db(); let ast::ExprStarred { range: _, node_index: _, @@ -985,7 +994,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { builder.into_diagnostic("`*` can only unpack a tuple type or `TypeVarTuple`"), ); } - Type::homogeneous_tuple(self.db(), Type::unknown()) + Type::homogeneous_tuple(db, self.program_environment(), Type::unknown()) } } @@ -1062,6 +1071,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { &mut self, tuple: &ast::ExprSubscript, ) -> Option> { + let db = self.db(); + let env = self.program_environment(); match &*tuple.slice { ast::Expr::Tuple(elements) => { if let [element, ellipsis @ ast::Expr::EllipsisLiteral(_)] = &*elements.elts { @@ -1086,7 +1097,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "`...` cannot be used after an unpacked element", ); } - let result = TupleType::homogeneous(self.db(), element_ty); + let result = TupleType::homogeneous(db, env, element_ty); self.store_expression_type(&tuple.slice, Type::tuple(Some(result))); return Some(result); } @@ -1156,7 +1167,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; if let Some(inner_tuple) = element_ty.exact_tuple_instance_spec(self.db()) { - element_types = element_types.concat(self.db(), &inner_tuple); + element_types = element_types.concat(db, env, &inner_tuple); if inner_tuple.is_variadic() { report_too_many_unpacked_tuples(); @@ -1165,8 +1176,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { && typevar.is_typevartuple(self.db()) { report_too_many_unpacked_tuples(); - element_types = - element_types.concat_variadic_typevar(self.db(), typevar); + element_types = element_types.concat_variadic_typevar(db, env, typevar); } else { // TODO: emit a diagnostic } @@ -1175,7 +1185,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - let ty = TupleType::new(self.db(), &element_types.build()); + let ty = TupleType::new(db, env, &element_types.build()); // Here, we store the type for the inner `int, str` tuple-expression, // while the type for the outer `tuple[int, str]` slice-expression is @@ -1195,7 +1205,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); } self.store_expression_type(single_element, Type::unknown()); - return TupleType::heterogeneous(self.db(), std::iter::once(Type::unknown())); + return TupleType::heterogeneous(db, env, std::iter::once(Type::unknown())); } let previously_in_valid_unpack_context = self .context @@ -1217,25 +1227,28 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(inner_tuple) = single_element_ty.exact_tuple_instance_spec(self.db()) { - return TupleType::new(self.db(), &inner_tuple); + return TupleType::new(db, env, &inner_tuple); } else if let Type::TypeVar(typevar) = single_element_ty && typevar.is_typevartuple(self.db()) { return TupleType::new( - self.db(), + db, + env, &TupleSpecBuilder::with_capacity(0) - .concat_variadic_typevar(self.db(), typevar) + .concat_variadic_typevar(db, env, typevar) .build(), ); } } - TupleType::heterogeneous(self.db(), std::iter::once(single_element_ty)) + TupleType::heterogeneous(db, env, std::iter::once(single_element_ty)) } } } /// Given the slice of a `type[]` annotation, return the type that the annotation represents fn infer_subclass_of_type_expression(&mut self, slice: &ast::Expr) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let invalid_type_argument = |builder: &Self, slice: &ast::Expr| { builder.report_invalid_type_expression( slice, @@ -1245,18 +1258,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let subclass_of_type_argument = |builder: &Self, slice: &ast::Expr, slice_ty: Type<'db>| { - let slice_ty = slice_ty.resolve_type_alias(builder.db()); + let slice_ty = slice_ty.resolve_type_alias(db); let slice_ty = match slice_ty { Type::Union(union) if union.has_aliases(builder.db()) => { - union.expand_aliases(builder.db()) + union.expand_aliases(db, env) } _ => slice_ty, }; - SubclassOfType::try_from_instance(builder.db(), slice_ty).unwrap_or_else(|| { - match slice_ty { - Type::Callable(_) => invalid_type_argument(builder, slice), - _ => todo_type!("unsupported type[X] special form"), - } + SubclassOfType::try_from_instance(db, env, slice_ty).unwrap_or_else(|| match slice_ty { + Type::Callable(_) => invalid_type_argument(builder, slice), + _ => todo_type!("unsupported type[X] special form"), }) }; @@ -1283,7 +1294,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ast::Expr::NoneLiteral(_) => { self.infer_expression(slice, TypeContext::default()); - KnownClass::NoneType.to_subclass_of(self.db()) + KnownClass::NoneType.to_subclass_of(db, env) } ast::Expr::Subscript( subscript @ ast::ExprSubscript { @@ -1296,7 +1307,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::SpecialForm(SpecialFormType::Union) => match &**parameters { ast::Expr::Tuple(tuple) => { let ty = UnionType::from_elements_leave_aliases( - self.db(), + db, + env, tuple .iter() .map(|element| self.infer_subclass_of_type_expression(element)), @@ -1311,26 +1323,25 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let class_type = self .infer_tuple_type_expression(subscript) .map(|tuple_type| tuple_type.to_class_type(self.db())) - .unwrap_or_else(|| class_literal.default_specialization(self.db())); - SubclassOfType::from(self.db(), class_type) + .unwrap_or_else(|| class_literal.default_specialization(db)); + SubclassOfType::from(db, env, class_type) } else { - match class_literal.generic_context(self.db()) { + match class_literal.generic_context(db) { Some(generic_context) => { - let db = self.db(); let specialize = &|types: &[Option>]| { let class = class_literal.apply_specialization(db, |_| { generic_context .specialize_partial(db, types.iter().copied()) }); if class_literal.is_protocol(db) { - match Type::instance(db, class) { + match Type::instance(db, env, class) { Type::ProtocolInstance(protocol) => { SubclassOfType::from_protocol(protocol) } - _ => SubclassOfType::from(db, class), + _ => SubclassOfType::from(db, env, class), } } else { - SubclassOfType::from(db, class) + SubclassOfType::from(db, env, class) } }; self.infer_explicit_callable_specialization( @@ -1347,7 +1358,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { builder.into_diagnostic(format_args!( "Cannot subscript non-generic type `{}`", - value_ty.display(self.db()) + value_ty.display(db, self.program_environment()) )); } Type::unknown() @@ -1394,6 +1405,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { mut value_ty: Type<'db>, in_type_expression: bool, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_ty @@ -1401,14 +1413,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { value_ty = value_ty.apply_type_mapping( db, + env, &TypeMapping::BindLegacyTypevars(BindingContext::Definition(definition)), TypeContext::default(), ); } let mut variables = FxOrderSet::default(); - value_ty.find_legacy_typevars(db, None, &mut variables); - let generic_context = GenericContext::from_typevar_instances(db, variables); + value_ty.find_legacy_typevars(db, env, None, &mut variables); + let generic_context = GenericContext::from_typevar_instances(db, env, variables); let scope_id = self.scope(); let current_typevar_binding_context = self.typevar_binding_context; @@ -1422,7 +1435,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // instead of two. So until we properly support these, specialize all remaining type // variables with a `@Todo` type (since we don't know which of the type arguments // belongs to the remaining type variables). - if any_over_type(self.db(), value_ty, true, |ty| ty.is_divergent()) { + if any_over_type(db, env, value_ty, true, |ty| ty.is_divergent()) { let value_ty = value_ty.apply_specialization( db, generic_context.specialize( @@ -1481,6 +1494,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { subscript: &ast::ExprSubscript, value_ty: Type<'db>, ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprSubscript { range: _, node_index: _, @@ -1614,7 +1629,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { specialized_type_alias .in_type_expression( - self.db(), + db, self.scope(), self.typevar_binding_context, self.inference_flags(), @@ -1637,12 +1652,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if value_type.is_specialized_generic(self.db()) { diagnostic.annotate(secondary.message(format_args!( "Alias to `{}`, which is already specialized", - value_type.display(self.db()) + value_type.display(db, env) ))); } else { diagnostic.annotate(secondary.message(format_args!( "Alias to `{}`, which is not generic", - value_type.display(self.db()) + value_type.display(db, env) ))); } } @@ -1658,7 +1673,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "`{ty}` is not a generic class", - ty = ty.inner(self.db()).display(self.db()) + ty = ty.inner(self.db()).display(db, env) )); } Type::unknown() @@ -1775,7 +1790,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { specialized_class .in_type_expression( - self.db(), + db, self.scope(), self.typevar_binding_context, self.inference_flags(), @@ -1789,7 +1804,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { builder.into_diagnostic(format_args!( "Cannot subscript non-generic type `{}`", - value_ty.display(self.db()) + value_ty.display(db, self.program_environment()) )); } Type::unknown() @@ -1807,7 +1822,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::Union(union) => { let db = self.db(); let mut union_builder = - UnionBuilder::new(db).recursively_defined(union.recursively_defined(db)); + UnionBuilder::new(db, env).recursively_defined(union.recursively_defined(db)); for (index, element) in union.elements(db).iter().enumerate() { let mut speculative_builder = self.speculate(); @@ -1830,7 +1845,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { builder.into_diagnostic(format_args!( "Invalid subscript of object of type `{}` in a {}", - value_ty.display(self.db()), + value_ty.display(db, env), self.type_expression_context() )); } @@ -1844,6 +1859,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { subscript_node: &ast::ExprSubscript, alias: LegacyStdlibAlias, ) -> Type<'db> { + let db = self.db(); let arguments = &*subscript_node.slice; let args = if let ast::Expr::Tuple(t) = arguments { &*t.elts @@ -1871,7 +1887,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } let ty = class.to_specialized_instance( - self.db(), + db, + self.program_environment(), args.iter() .map(|node| self.infer_type_expression(node)) .collect::>(), @@ -1931,7 +1948,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(returns) = return_type { diagnostic.set_primary_annotation_message(format_args!( "Did you mean `Callable[..., {}]`?", - returns.display(db) + returns.display(db, builder.program_environment()) )); } } @@ -1997,6 +2014,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { subscript: &ast::ExprSubscript, special_form: SpecialFormType, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); let arguments_slice = &*subscript.slice; match special_form { @@ -2006,7 +2024,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { AnnotatedExprContext::TypeExpression, ) .inner_type() - .in_type_expression(self.db(), self.scope(), None, self.inference_flags()) + .in_type_expression(db, self.scope(), None, self.inference_flags()) .unwrap_or_else(|err| { err.into_fallback_type(&self.context, subscript, self.inference_flags()) }), @@ -2028,7 +2046,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }, SpecialFormType::Optional => { let param_type = self.infer_type_expression(arguments_slice); - UnionType::from_elements_leave_aliases(db, [param_type, Type::none(db)]) + UnionType::from_elements_leave_aliases(db, env, [param_type, Type::none(db, env)]) } SpecialFormType::Union => { // TODO: Support the union of a `TypeVarTuple`'s elements. Until then, reject @@ -2039,8 +2057,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { std::slice::from_ref(arguments_slice) }; let mut has_unpacked_typevartuple = false; - let union_ty = UnionType::from_elements_leave_aliases( - db, + let union_ty = UnionType::from_elements_leave_aliases(db, env, arguments.iter().map(|argument| { let ty = self.infer_type_expression(argument); if self @@ -2107,7 +2124,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let num_arguments = arguments.len(); let negated_type = if num_arguments == 1 { - self.infer_type_expression(&arguments[0]).negate(db) + self.infer_type_expression(&arguments[0]).negate(db, env) } else { if !self.in_string_annotation() { for argument in arguments { @@ -2135,7 +2152,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { }; let ty = elements - .fold(IntersectionBuilder::new(db), |builder, element| { + .fold(IntersectionBuilder::new(db, env), |builder, element| { builder.add_positive(self.infer_type_expression(element)) }) .build(); @@ -2169,7 +2186,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); Type::unknown() }; - arg.top_materialization(db) + arg.top_materialization(db, env) } SpecialFormType::Bottom => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { @@ -2195,7 +2212,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); Type::unknown() }; - arg.bottom_materialization(db) + arg.bottom_materialization(db, env) } SpecialFormType::TypeOf => { let arguments = if let ast::Expr::Tuple(tuple) = arguments_slice { @@ -2287,19 +2304,19 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } let argument_type = self.infer_expression(&arguments[0], TypeContext::default()); - let Some(callable_type) = argument_type .try_upcast_to_callable_with_recursive_fallback( db, + env, self.recursive_type_expression_definition(), ) .map(|callables| { if special_form == SpecialFormType::RegularCallableTypeOf { callables .map(|callable| callable.into_regular(db)) - .into_type(db) + .into_type(db, env) } else { - callables.into_type(db) + callables.into_type(db, env) } }) else { @@ -2311,7 +2328,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "Expected the first argument to `{special_form}` \ to be a callable object, \ but got an object of type `{actual_type}`", - actual_type = argument_type.display(db) + actual_type = argument_type.display(db, env) )); } if arguments_slice.is_tuple_expr() { @@ -2369,7 +2386,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } _ => { let narrowed = self.infer_type_expression(arguments_slice); - let expanded = narrowed.expand_eagerly(self.db()); + let expanded = narrowed.expand_eagerly(db, env); if expanded.is_divergent() { expanded @@ -2534,7 +2551,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { "`Unpack` can only unpack a tuple type or `TypeVarTuple`", )); } - Type::homogeneous_tuple(self.db(), Type::unknown()) + Type::homogeneous_tuple(db, env, Type::unknown()) } } SpecialFormType::NoReturn @@ -2638,6 +2655,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { &mut self, parameters: &'param ast::Expr, ) -> Result, Vec<&'param ast::Expr>> { + let db = self.db(); + let env = self.program_environment(); let ty = match parameters { ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { let value_ty = self.infer_expression(value, TypeContext::default()); @@ -2657,7 +2676,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ast::Expr::Tuple(tuple) if !tuple.parenthesized => { let mut errors = vec![]; - let mut builder = UnionBuilder::new(self.db()); + let mut builder = UnionBuilder::new(db, env); for elt in tuple { match self.infer_literal_parameter_type(elt) { Ok(ty) => { @@ -2712,8 +2731,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { match subscript_ty { // type aliases to literal types Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => { - let value_ty = type_alias.value_type(self.db()); - if value_ty.is_literal_or_union_of_literals(self.db()) { + let value_ty = type_alias.value_type(db); + if value_ty.is_literal_or_union_of_literals(db, env) { return Ok(value_ty); } } @@ -2727,7 +2746,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } // `Literal[SingletonEnum.Member]`, where `SingletonEnum.Member` simplifies to // just `SingletonEnum`. - Type::NominalInstance(_) if subscript_ty.is_enum(self.db()) => { + Type::NominalInstance(_) if subscript_ty.is_enum(db, env) => { return Ok(subscript_ty); } // suppress false positives for e.g. members of functional-syntax enums @@ -2763,6 +2782,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { &mut self, parameters: &ast::Expr, ) -> Option> { + let db = self.db(); match parameters { ast::Expr::EllipsisLiteral(ast::ExprEllipsisLiteral { .. }) => { return Some(Parameters::gradual_form()); @@ -2815,7 +2835,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { previously_in_valid_unpack_context, ); - return Some(Parameters::from_annotation(self.db(), parameters)); + return Some(Parameters::from_annotation(db, parameters)); } ast::Expr::Subscript(subscript) => { let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); @@ -2851,7 +2871,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Type::TypeVar(tvar) = parameters_type && tvar.is_paramspec(self.db()) { - return Some(Parameters::paramspec(self.db(), tvar)); + return Some(Parameters::paramspec(db, tvar)); } if parameters_type == Type::Dynamic(DynamicType::InvalidConcatenateUnknown) { // Avoid emitting a confusing error here saying that the first argument to @@ -2902,6 +2922,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { &mut self, subscript: &ast::ExprSubscript, ) -> Parameters<'db> { + let db = self.db(); let previous_concatenate_context = self .context .inference_flags @@ -2954,7 +2975,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let parameters = self .infer_concatenate_tail(last_arg) - .map(|tail| Parameters::concatenate(self.db(), prefix_params, tail)); + .map(|tail| Parameters::concatenate(db, prefix_params, tail)); if arguments_slice.is_tuple_expr() { // TODO: What type to store for the argument slice in `Concatenate` because diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_form.rs b/crates/ty_python_semantic/src/types/infer/builder/type_form.rs index 57157e9ef7..88dfa725aa 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_form.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_form.rs @@ -18,15 +18,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { expression: &ast::Expr, target: Type<'db>, ) -> Option> { - let non_type_form_fallback = match target.resolve_type_alias(self.db()) { + let db = self.db(); + let env = self.program_environment(); + let non_type_form_fallback = match target.resolve_type_alias(db) { Type::TypeForm(_) => None, Type::Union(union) - if union.elements(self.db()).iter().any(|element| { - matches!(element.resolve_type_alias(self.db()), Type::TypeForm(_)) - }) => + if union + .elements(self.db()) + .iter() + .any(|element| matches!(element.resolve_type_alias(db), Type::TypeForm(_))) => { - Some(target.filter_union(self.db(), |element| { - !matches!(element.resolve_type_alias(self.db()), Type::TypeForm(_)) + Some(target.filter_union(db, |element| { + !matches!(element.resolve_type_alias(db), Type::TypeForm(_)) })) } _ => return None, @@ -37,10 +40,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let value_ty = self .speculate_without_diagnostics() .infer_maybe_standalone_expression(expression, TypeContext::default()); - if matches!(value_ty.resolve_type_alias(self.db()), Type::Never) + if matches!(value_ty.resolve_type_alias(db), Type::Never) || self.contains_type_form_value(expression, value_ty) || non_type_form_fallback - .is_some_and(|alternative| value_ty.is_assignable_to(self.db(), alternative)) + .is_some_and(|alternative| value_ty.is_assignable_to(db, env, alternative)) { return None; } @@ -56,7 +59,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let contextual_ty = self .speculate_without_diagnostics() .infer_value_expression_impl(expression, TypeContext::new(Some(target))); - if contextual_ty.is_assignable_to(self.db(), target) { + if contextual_ty.is_assignable_to(db, env, target) { return None; } } @@ -78,6 +81,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ty: Type<'db>, visitor: &ContainsTypeFormValueVisitor<'db>, ) -> bool { + let db = builder.db(); + let env = builder.program_environment(); match ty { Type::TypeForm(_) | Type::SubclassOf(_) => true, // A bare class object is valid type-expression syntax and should still be @@ -99,18 +104,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::Intersection(intersection) => intersection .iter_positive(builder.db()) .any(|element| imp(builder, expression, element, visitor)), - Type::TypeAlias(alias) => visitor.visit(builder.db(), ty, || { - imp(builder, expression, alias.value_type(builder.db()), visitor) + Type::TypeAlias(alias) => visitor.visit(db, ty, || { + imp(builder, expression, alias.value_type(db), visitor) }), - Type::TypeVar(typevar) => visitor.visit(builder.db(), ty, || { + Type::TypeVar(typevar) => visitor.visit(db, ty, || { typevar .typevar(builder.db()) - .bound_or_constraints(builder.db()) + .bound_or_constraints(db, env) .is_some_and(|bound_or_constraints| { imp( builder, expression, - bound_or_constraints.as_type(builder.db()), + bound_or_constraints.as_type(db, env), visitor, ) }) diff --git a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs index 6cd7934cfd..1770a51061 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs @@ -5,6 +5,7 @@ use smallvec::SmallVec; use strum::IntoEnumIterator; use super::TypeInferenceBuilder; +use crate::TypeQualifiers; use crate::types::class::{ClassLiteral, DynamicTypedDictAnchor, DynamicTypedDictLiteral}; use crate::types::diagnostic::{ INVALID_ARGUMENT_TYPE, INVALID_TYPE_FORM, MISSING_ARGUMENT, TOO_MANY_POSITIONAL_ARGUMENTS, @@ -21,7 +22,6 @@ use crate::types::{ IntersectionType, KnownClass, Type, TypeAndQualifiers, TypeContext, TypedDictModule, TypedDictType, }; -use crate::{Program, TypeQualifiers}; use ty_python_core::definition::Definition; /// The shape of a `TypedDict` constructor call that affects how we prepare it for inference. @@ -75,8 +75,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { definition: Option>, typed_dict_module: TypedDictModule, ) -> Type<'db> { + let env = self.program_environment(); let db = self.db(); - let ast::Arguments { args, keywords, @@ -93,9 +93,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // it would return a class that is a subclass of `Mapping[str, object]` // with an unknown set of fields. let fallback = || { - let spec = &[KnownClass::Str.to_instance(db), Type::object()]; - let str_object_map = KnownClass::Mapping.to_specialized_subclass_of(db, spec); - IntersectionType::from_two_elements(db, str_object_map, Type::unknown()) + let spec = &[KnownClass::Str.to_instance(db, env), Type::object()]; + let str_object_map = KnownClass::Mapping.to_specialized_subclass_of(db, env, spec); + IntersectionType::from_two_elements(db, env, str_object_map, Type::unknown()) }; // Emit diagnostic for unsupported variadic arguments. @@ -148,7 +148,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let mut extra_items = None; let supports_pep_728 = self.in_stub() || typed_dict_module == TypedDictModule::TypingExtensions - || Program::get(db).python_version(db) >= PythonVersion::PY315; + || self.program_environment().python_version(db) >= PythonVersion::PY315; for kw in keywords { let Some(arg) = &kw.arg else { @@ -176,18 +176,18 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )); diagnostic.set_primary_annotation_message(format_args!( "Expected either `True` or `False`, got object of type `{}`", - kw_type.display(db) + kw_type.display(db, env) )); } if arg_name == "total" { - if kw_type.bool(db).is_always_false() { + if kw_type.bool(db, env).is_always_false() { total = false; - } else if !kw_type.bool(db).is_always_true() { + } else if !kw_type.bool(db, env).is_always_true() { total = true; } } else { - closed = kw_type.bool(db).is_always_true(); + closed = kw_type.bool(db, env).is_always_true(); } } "extra_items" => { @@ -269,7 +269,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .map(|literal| literal.value(db)); if name.is_none() - && !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && !name_type.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) { let mut diagnostic = builder.into_diagnostic(format_args!( @@ -277,7 +277,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )); diagnostic.set_primary_annotation_message(format_args!( "Expected `str`, found `{}`", - name_type.display(db) + name_type.display(db, env) )); } else if let Some(definition) = definition && let Some(assigned_name) = definition.name(db) @@ -338,6 +338,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { typed_dict: TypedDictType<'db>, item_types: &mut FxHashMap>, ) -> Option> { + let db = self.db(); + let env = self.program_environment(); let ast::ExprDict { range: _, node_index: _, @@ -358,12 +360,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { && let Some(field) = typed_dict.item(self.db(), key.value(self.db())) { self.infer_expression(&item.value, TypeContext::new(Some(field.declared_ty))) - } else if key_ty.is_some_and(|key_ty| { - key_ty.is_assignable_to(self.db(), KnownClass::Str.to_instance(self.db())) - }) && let Some(value_ty) = - typed_dict.arbitrary_key_initialization_type(self.db()) - { - self.infer_expression(&item.value, TypeContext::new(Some(value_ty))) + } else if let Some(key_ty) = key_ty { + if key_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) + && let Some(value_ty) = typed_dict.arbitrary_key_initialization_type(db, env) + { + self.infer_expression(&item.value, TypeContext::new(Some(value_ty))) + } else { + self.infer_expression(&item.value, TypeContext::default()) + } } else { self.infer_expression(&item.value, TypeContext::default()) }; @@ -404,6 +408,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { arguments: &'expr ast::Arguments, error_node: AnyNodeRef<'expr>, ) { + let db = self.db(); match form { TypedDictConstructorForm::LiteralOnly(argument) => { let target_ty = Type::TypedDict(typed_dict); @@ -420,14 +425,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.get_or_infer_expression(expr, tcx) }); let keyword_keys = collect_guaranteed_keyword_keys( - self.db(), + db, + self.program_environment(), typed_dict, arguments, &unpacked_keyword_types, &mut |expr, tcx| self.get_or_infer_expression(expr, tcx), ); - let positional_target = - typed_dict_with_relaxed_keys(self.db(), typed_dict, &keyword_keys); + let positional_target = typed_dict_with_relaxed_keys(db, typed_dict, &keyword_keys); let target_ty = Type::TypedDict(positional_target); self.get_or_infer_expression(&arguments.args[0], TypeContext::new(Some(target_ty))); } @@ -485,6 +490,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { typed_dict: TypedDictType<'db>, dict_expr: &ast::ExprDict, ) { + let db = self.db(); + let env = self.program_environment(); let key_tcx = TypeContext::new(self.typed_dict_key_expected_type(Type::TypedDict(typed_dict))); @@ -497,10 +504,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { && let Some(field) = typed_dict.item(self.db(), key.value(self.db())) { TypeContext::new(Some(field.declared_ty)) - } else if key_ty.is_some_and(|key_ty| { - key_ty.is_assignable_to(self.db(), KnownClass::Str.to_instance(self.db())) - }) { - TypeContext::new(typed_dict.arbitrary_key_initialization_type(self.db())) + } else if let Some(key_ty) = key_ty { + if key_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { + TypeContext::new(typed_dict.arbitrary_key_initialization_type(db, env)) + } else { + TypeContext::default() + } } else { TypeContext::default() }; @@ -638,7 +647,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// themselves. fn validate_fields_arg(&mut self, fields_arg: &ast::Expr) { let db = self.db(); - if let ast::Expr::Dict(dict_expr) = fields_arg { for ast::DictItem { key, value } in dict_expr { if let Some(key) = key { @@ -652,7 +660,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); diagnostic.set_primary_annotation_message(format_args!( "Found `{}`", - key_type.display(db) + key_type.display(db, self.program_environment()) )); } } else { diff --git a/crates/ty_python_semantic/src/types/infer/builder/typevar.rs b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs index 647120106d..246533f0ab 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typevar.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs @@ -1,5 +1,4 @@ use crate::{ - Program, reachability::is_reachable, types::{ BindingContext, KnownClass, KnownInstanceType, LintDiagnosticGuard, Truthiness, Type, @@ -85,6 +84,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } pub(super) fn infer_typevar_deferred(&mut self, node: &'ast ast::TypeParamTypeVar) { + let env = self.program_environment(); let ast::TypeParamTypeVar { range: _, node_index: _, @@ -106,7 +106,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .iter() .map(|expr| { let constraint = self.infer_type_expression(expr); - if constraint.has_typevar_or_typevar_instance(db) + if constraint.has_typevar_or_typevar_instance(db, env) && let Some(builder) = self .context .report_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS, expr) @@ -117,7 +117,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) .collect(); - let tuple_ty = Type::heterogeneous_tuple(db, constraint_tys.clone()); + let tuple_ty = Type::heterogeneous_tuple(db, env, constraint_tys.clone()); self.store_expression_type(expr, tuple_ty); // Mirror the `< 2` guard from `infer_typevar_definition` to avoid // a cascading `invalid-type-variable-default` diagnostic for tuples @@ -132,7 +132,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } Some(expr) => { let bound_ty = self.infer_type_expression(expr); - if bound_ty.has_typevar_or_typevar_instance(db) + if bound_ty.has_typevar_or_typevar_instance(db, env) && let Some(builder) = self.context.report_lint(&INVALID_TYPE_VARIABLE_BOUND, expr) { @@ -171,6 +171,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { default_node: &ast::Expr, bound_or_constraints_nodes: Option>, ) { + let env = self.program_environment(); let Some(bound_or_constraints) = bound_or_constraints else { return; }; @@ -240,11 +241,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Annotate the diagnostic with the definition span of the default TypeVar. let annotate_default_definition = |diagnostic: &mut LintDiagnosticGuard<'_, '_>| { if let Some(definition) = default_typevar.definition(db) { - let file = definition.file(db); diagnostic.annotate( - Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), - )) + Annotation::secondary(Span::from(definition.full_range( + db, + &parsed_module(db, definition.python_file(db)).load(db), + ))) .message(format_args!("`{default_name}` defined here")), ); } @@ -255,9 +256,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Default TypeVar's upper bound must be assignable to outer's bound. // If the default has constraints, all constraints must be assignable // to the outer bound. - if let Some(default_constraints) = default_typevar.constraints(db) { + if let Some(default_constraints) = default_typevar.constraints(db, env) { for constraint in default_constraints { - if !constraint.is_assignable_to(db, outer_bound) { + if !constraint.is_assignable_to(db, env, outer_bound) { if let Some(mut diagnostic) = not_assignable_to_upper_bound() { annotate_default_definition(&mut diagnostic); if let Some(name) = name { @@ -265,7 +266,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "Constraint `{constraint}` of default \ `{default_name}` is not assignable to upper \ bound of `{name}`", - constraint = constraint.display(db), + constraint = constraint.display(db, env), )); diagnostic.set_concise_message(format_args!( "Default `{default_name}` of TypeVar `{name}` \ @@ -273,23 +274,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { of `{name}` because constraint `{constraint}` \ of `{default_name}` is not assignable to \ `{bound}`", - bound = outer_bound.display(db), - constraint = constraint.display(db), + bound = outer_bound.display(db, env), + constraint = constraint.display(db, env), )); } else { diagnostic.set_primary_annotation_message(format_args!( "Constraint `{constraint}` of `{default_name}` is \ not assignable to upper bound `{bound}` of \ outer TypeVar", - constraint = constraint.display(db), - bound = outer_bound.display(db), + constraint = constraint.display(db, env), + bound = outer_bound.display(db, env), )); diagnostic.set_concise_message(format_args!( "Default of TypeVar is not assignable its upper \ bound `{bound}` because constraint `{constraint}` \ of `{default_name}` is not assignable to `{bound}`", - bound = outer_bound.display(db), - constraint = constraint.display(db), + bound = outer_bound.display(db, env), + constraint = constraint.display(db, env), )); } } @@ -297,9 +298,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } else { - let default_bound = - default_typevar.upper_bound(db).unwrap_or_else(Type::object); - if !default_bound.is_assignable_to(db, outer_bound) { + let default_bound = default_typevar + .upper_bound(db, env) + .unwrap_or_else(Type::object); + if !default_bound.is_assignable_to(db, env, outer_bound) { if let Some(mut diagnostic) = not_assignable_to_upper_bound() { annotate_default_definition(&mut diagnostic); if let Some(name) = name { @@ -307,7 +309,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "Upper bound `{default_bound}` of default \ `{default_name}` is not assignable to upper \ bound of `{name}`", - default_bound = default_bound.display(db), + default_bound = default_bound.display(db, env), )); diagnostic.set_concise_message(format_args!( "Default `{default_name}` of TypeVar `{name}` \ @@ -315,15 +317,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { of `{name}` because its upper bound \ `{default_bound}` is not assignable to \ `{bound}`", - bound = outer_bound.display(db), - default_bound = default_bound.display(db), + bound = outer_bound.display(db, env), + default_bound = default_bound.display(db, env), )); } else { diagnostic.set_primary_annotation_message(format_args!( "Upper bound `{default_bound}` of default \ `{default_name}` is not assignable to upper \ bound of outer TypeVar", - default_bound = default_bound.display(db), + default_bound = default_bound.display(db, env), )); diagnostic.set_concise_message(format_args!( "TypeVar default `{default_name}` is not \ @@ -331,8 +333,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { because upper bound of `{default_name}` (`{default_bound}`) is not assignable to `{bound}`", - bound = outer_bound.display(db), - default_bound = default_bound.display(db), + bound = outer_bound.display(db, env), + default_bound = default_bound.display(db, env), )); } } @@ -342,12 +344,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { TypeVarBoundOrConstraints::Constraints(outer_constraints) => { // TypeVar default with constrained outer. let outer = outer_constraints.elements(db); - if let Some(default_constraints) = default_typevar.constraints(db) { + if let Some(default_constraints) = default_typevar.constraints(db, env) { // Default has constraints: outer constraints must be a superset. for default_constraint in default_constraints { if !outer .iter() - .any(|o| default_constraint.is_equivalent_to(db, *o)) + .any(|o| default_constraint.is_equivalent_to(db, env, *o)) { if let Some(mut diagnostic) = inconsistent_with_constraints() { annotate_default_definition(&mut diagnostic); @@ -356,7 +358,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "Constraint `{constraint}` of default \ `{default_name}` is not one of the constraints \ of `{name}`", - constraint = default_constraint.display(db), + constraint = default_constraint.display(db, env), )); diagnostic.set_concise_message(format_args!( "Default `{default_name}` of TypeVar `{name}` \ @@ -364,14 +366,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { `{name}` because constraint `{constraint}` of \ `{default_name}` is not one of the constraints \ of `{name}`", - constraint = default_constraint.display(db), + constraint = default_constraint.display(db, env), )); } else { diagnostic.set_primary_annotation_message(format_args!( "Constraint `{constraint}` of outer TypeVar default \ `{default_name}` is not one of the constraints \ of the outer TypeVar", - constraint = default_constraint.display(db), + constraint = default_constraint.display(db, env), )); diagnostic.set_concise_message(format_args!( "Default `{default_name}` of outer TypeVar is \ @@ -379,7 +381,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { TypeVar because constraint `{constraint}` of \ default `{default_name}` is not one of the \ constraints of the outer TypeVar", - constraint = default_constraint.display(db), + constraint = default_constraint.display(db, env), )); } } @@ -391,14 +393,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // incompatible with a constrained outer TypeVar per the typing spec. if let Some(mut diagnostic) = inconsistent_with_constraints() { annotate_default_definition(&mut diagnostic); - if let Some(default_bound) = default_typevar.upper_bound(db) { + if let Some(default_bound) = default_typevar.upper_bound(db, env) { diagnostic.set_primary_annotation_message( "Bounded TypeVar cannot be used as the default \ for a constrained TypeVar", ); diagnostic.info(format_args!( "`{default_name}` has bound `{default_bound}` but is not constrained", - default_bound = default_bound.display(db), + default_bound = default_bound.display(db, env), )); } else { diagnostic.set_primary_annotation_message( @@ -419,7 +421,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Concrete default type checks. match bound_or_constraints { TypeVarBoundOrConstraints::UpperBound(bound) => { - if !default_ty.is_assignable_to(db, bound) { + if !default_ty.is_assignable_to(db, env, bound) { if let Some(mut diagnostic) = not_assignable_to_upper_bound() { if let Some(name) = name { diagnostic.set_primary_annotation_message(format_args!( @@ -437,18 +439,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { && !constraints .elements(db) .iter() - .any(|c| default_ty.is_equivalent_to(db, *c)) + .any(|c| default_ty.is_equivalent_to(db, env, *c)) { if let Some(mut diagnostic) = inconsistent_with_constraints() { if let Some(name) = name { diagnostic.set_primary_annotation_message(format_args!( "`{default}` is not one of the constraints of `{name}`", - default = default_ty.display(db), + default = default_ty.display(db, env), )); } else { diagnostic.set_primary_annotation_message(format_args!( "`{default}` is not one of the constraints", - default = default_ty.display(db), + default = default_ty.display(db, env), )); } } @@ -488,7 +490,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let expected_binding = BindingContext::Definition(expected_binding_def); - let outer_tv = find_over_type(db, default_ty, false, |ty| { + let outer_tv = find_over_type(db, self.program_environment(), default_ty, false, |ty| { if let Type::TypeVar(bound_tv) = ty && bound_tv.binding_context(db) != expected_binding { @@ -520,10 +522,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { outer-scope type parameter `{outer_name}` as its default" )); if let Some(definition) = outer_typevar.definition(db) { - let file = definition.file(db); diagnostic.annotate( Annotation::secondary(Span::from( - definition.full_range(db, &parsed_module(db, file).load(db)), + definition + .full_range(db, &parsed_module(db, definition.python_file(db)).load(db)), )) .message(format_args!("`{outer_name}` defined here")), ); @@ -626,7 +628,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); // N.B. We cannot represent a heterogeneous list of types in our type system, so we // use a heterogeneous tuple type to represent the list of types instead. - self.store_expression_type(default_expr, Type::heterogeneous_tuple(db, types)); + let ty = Type::heterogeneous_tuple(db, self.program_environment(), types); + self.store_expression_type(default_expr, ty); return; } ast::Expr::Name(_) => { @@ -761,19 +764,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { message: impl std::fmt::Display, node: impl Ranged, ) -> Type<'db> { + let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, node) { builder.into_diagnostic(message); } - KnownClass::TypeVarTuple.to_instance(context.db()) + KnownClass::TypeVarTuple.to_instance(db, context.program_environment()) } + let env = self.program_environment(); let db = self.db(); let arguments = &call_expr.arguments; let is_typing_extensions = known_class == KnownClass::ExtensionsTypeVarTuple; let assume_all_features = self.in_stub() || is_typing_extensions; - let python_version = Program::get(db).python_version(db); - let have_features_from = - |version: PythonVersion| assume_all_features || python_version >= version; let mut default = None; let mut covariant = false; @@ -820,7 +822,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(self.infer_expression(&kwarg.value, TypeContext::default())); } "default" => { - if !have_features_from(PythonVersion::PY313) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY313 + { error( &self.context, "The `default` parameter of `typing.TypeVarTuple` was added in Python 3.13", @@ -830,7 +834,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { default = Some(TypeVarDefaultEvaluation::Lazy); } "bound" => { - if !have_features_from(PythonVersion::PY315) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY315 + { return error( &self.context, "The `bound` parameter of `typing.TypeVarTuple` was added in Python 3.15", @@ -844,7 +850,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } "covariant" => { - if !have_features_from(PythonVersion::PY315) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY315 + { error( &self.context, "The `covariant` parameter of `typing.TypeVarTuple` was added in Python 3.15", @@ -853,7 +861,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => covariant = true, Truthiness::AlwaysFalse => {} @@ -868,7 +876,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } "contravariant" => { - if !have_features_from(PythonVersion::PY315) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY315 + { error( &self.context, "The `contravariant` parameter of `typing.TypeVarTuple` was added in Python 3.15", @@ -877,7 +887,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => contravariant = true, Truthiness::AlwaysFalse => {} @@ -892,7 +902,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } "infer_variance" => { - if !have_features_from(PythonVersion::PY315) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY315 + { error( &self.context, "The `infer_variance` parameter of `typing.TypeVarTuple` was added in Python 3.15", @@ -901,7 +913,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => infer_variance = true, Truthiness::AlwaysFalse => {} @@ -1021,21 +1033,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { message: impl std::fmt::Display, node: impl Ranged, ) -> Type<'db> { + let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_PARAMSPEC, node) { builder.into_diagnostic(message); } // If the call doesn't create a valid paramspec, we'll emit diagnostics and fall back to // just creating a regular instance of `typing.ParamSpec`. - KnownClass::ParamSpec.to_instance(context.db()) + KnownClass::ParamSpec.to_instance(db, context.program_environment()) } + let env = self.program_environment(); let db = self.db(); let arguments = &call_expr.arguments; let is_typing_extensions = known_class == KnownClass::ExtensionsParamSpec; let assume_all_features = self.in_stub() || is_typing_extensions; - let python_version = Program::get(db).python_version(db); - let have_features_from = - |version: PythonVersion| assume_all_features || python_version >= version; let mut default = None; let mut covariant = false; @@ -1091,7 +1102,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } "infer_variance" => { - if !have_features_from(PythonVersion::PY312) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY312 + { error( &self.context, "The `infer_variance` parameter of `typing.ParamSpec` was added in Python 3.12", @@ -1100,7 +1113,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => infer_variance = true, Truthiness::AlwaysFalse => {} @@ -1117,7 +1130,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "covariant" => { match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => covariant = true, Truthiness::AlwaysFalse => {} @@ -1134,7 +1147,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "contravariant" => { match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => contravariant = true, Truthiness::AlwaysFalse => {} @@ -1149,7 +1162,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } "default" => { - if !have_features_from(PythonVersion::PY313) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY313 + { // We don't return here; this error is informational since this will error // at runtime, but the user's intent is plain, we may as well respect it. error( @@ -1268,21 +1283,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { message: impl std::fmt::Display, node: impl Ranged, ) -> Type<'db> { + let db = context.db(); if let Some(builder) = context.report_lint(&INVALID_LEGACY_TYPE_VARIABLE, node) { builder.into_diagnostic(message); } // If the call doesn't create a valid typevar, we'll emit diagnostics and fall back to // just creating a regular instance of `typing.TypeVar`. - KnownClass::TypeVar.to_instance(context.db()) + KnownClass::TypeVar.to_instance(db, context.program_environment()) } + let env = self.program_environment(); let db = self.db(); let arguments = &call_expr.arguments; let is_typing_extensions = known_class == KnownClass::ExtensionsTypeVar; let assume_all_features = self.in_stub() || is_typing_extensions; - let python_version = Program::get(db).python_version(db); - let have_features_from = - |version: PythonVersion| assume_all_features || python_version >= version; let mut has_bound = false; let mut default = None; @@ -1327,7 +1341,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "covariant" => { match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => covariant = true, Truthiness::AlwaysFalse => {} @@ -1344,7 +1358,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "contravariant" => { match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => contravariant = true, Truthiness::AlwaysFalse => {} @@ -1359,7 +1373,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } "default" => { - if !have_features_from(PythonVersion::PY313) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY313 + { // We don't return here; this error is informational since this will error // at runtime, but the user's intent is plain, we may as well respect it. error( @@ -1372,7 +1388,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { default = Some(TypeVarDefaultEvaluation::Lazy); } "infer_variance" => { - if !have_features_from(PythonVersion::PY312) { + if !assume_all_features + && self.program_environment().python_version(db) < PythonVersion::PY312 + { // We don't return here; this error is informational since this will error // at runtime, but the user's intent is plain, we may as well respect it. error( @@ -1383,7 +1401,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } match self .infer_expression(&kwarg.value, TypeContext::default()) - .bool(db) + .bool(db, env) { Truthiness::AlwaysTrue => infer_variance = true, Truthiness::AlwaysFalse => {} @@ -1477,7 +1495,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { name_param_ty, ); } - let previous_definition_in = |scope, place, before| { let use_def = self.index.use_def_map(scope); use_def diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index 77c4bdfc59..0286852f20 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -1,8 +1,9 @@ +use crate::Db; use ruff_python_ast as ast; use ruff_text_size::TextRange; use smallvec::SmallVec; -use crate::Db; +use crate::ProgramEnvironment; use crate::types::call::{CallArguments, CallDunderError}; use crate::types::constraints::ConstraintSetBuilder; use crate::types::context::InferContext; @@ -30,32 +31,39 @@ impl<'db> Type<'db> { /// to the `True` singleton at runtime. However, excluding an entire nominal instance type is /// stable under `NewType` erasure, so constraints such as `~None` and `~SomeClass` are /// preserved. - pub(crate) fn identity_comparison_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn identity_comparison_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { struct IdentityComparisonUpcasting; fn upcast<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, visitor: &TypeTransformer<'db, IdentityComparisonUpcasting>, ) -> Type<'db> { match ty { Type::TypeAlias(alias) => { - visitor.visit_type(db, ty, || upcast(db, alias.value_type(db), visitor)) + visitor.visit_type(db, ty, || upcast(db, env, alias.value_type(db), visitor)) } Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db), Type::TypeVar(typevar) => visitor.visit_type(db, ty, || { - match typevar.typevar(db).bound_or_constraints(db) { + match typevar.typevar(db).bound_or_constraints(db, env) { Some(bound_or_constraints) => { - upcast(db, bound_or_constraints.as_type(db), visitor) + upcast(db, env, bound_or_constraints.as_type(db, env), visitor) } None => ty, } }), - Type::Union(union) => union.map(db, |element| upcast(db, *element, visitor)), + Type::Union(union) => { + union.map(db, env, |element| upcast(db, env, *element, visitor)) + } Type::Intersection(intersection) => { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); for element in intersection.positive(db) { - builder = builder.add_positive(upcast(db, *element, visitor)); + builder = builder.add_positive(upcast(db, env, *element, visitor)); } for element in intersection.negative(db) { if element.resolve_type_alias(db).is_nominal_instance() { @@ -70,6 +78,7 @@ impl<'db> Type<'db> { upcast( db, + env, self, &TypeTransformer::::default(), ) @@ -79,10 +88,11 @@ impl<'db> Type<'db> { pub(crate) fn identity_comparison_truthiness( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Type<'db>, ) -> Truthiness { let is_singleton_or_intersection_with_singleton = |ty: Type<'db>| { - ty.is_singleton(db) + ty.is_singleton(db, env) || ty .resolve_type_alias(db) .as_intersection() @@ -90,7 +100,7 @@ impl<'db> Type<'db> { intersection .positive(db) .iter() - .any(|ty| ty.is_singleton(db)) + .any(|ty| ty.is_singleton(db, env)) }) }; @@ -107,19 +117,19 @@ impl<'db> Type<'db> { // `NewType` instances are identity functions at runtime, so distinct static types can still // identify the same object. Compare the types of their possible runtime objects instead. - let left_identity = self.identity_comparison_type(db); - let right_identity = other.identity_comparison_type(db); + let left_identity = self.identity_comparison_type(db, env); + let right_identity = other.identity_comparison_type(db, env); // Non-disjoint singleton types do not necessarily identify the same object: disjointness can // be inconclusive, for example when aliases between enum members cannot be determined. // Require one singleton type to be a subtype of the other before concluding that they are // definitely identical. - if left_identity.is_disjoint_from(db, right_identity) { + if left_identity.is_disjoint_from(db, env, right_identity) { Truthiness::AlwaysFalse } else if is_singleton_or_intersection_with_singleton(left_identity) && is_singleton_or_intersection_with_singleton(right_identity) - && (left_identity.is_subtype_of(db, right_identity) - || right_identity.is_subtype_of(db, left_identity)) + && (left_identity.is_subtype_of(db, env, right_identity) + || right_identity.is_subtype_of(db, env, left_identity)) { Truthiness::AlwaysTrue } else { @@ -265,13 +275,14 @@ pub(super) fn infer_binary_type_comparison<'db>( range: TextRange, ) -> Result, UnsupportedComparisonError<'db>> { let db = context.db(); + let env = &context.program_environment(); let op = match op { ast::CmpOp::Is | ast::CmpOp::IsNot => { let truthiness = left - .identity_comparison_truthiness(db, right) + .identity_comparison_truthiness(db, env, right) .negate_if(op == ast::CmpOp::IsNot); - return Ok(Type::from_truthiness(db, truthiness)); + return Ok(Type::from_truthiness(db, env, truthiness)); } ast::CmpOp::Eq => NonIdentityOperator::Rich(RichCompareOperator::Eq), ast::CmpOp::NotEq => NonIdentityOperator::Rich(RichCompareOperator::Ne), @@ -302,9 +313,10 @@ fn infer_binary_type_comparison_inner<'db>( visitor: &BinaryComparisonVisitor<'db>, ) -> Result, UnsupportedComparisonError<'db>> { let db = context.db(); + let env = &context.program_environment(); let try_dunder = |policy: MemberLookupPolicy| { - let rich_comparison = |op| infer_rich_comparison(db, left, right, op, policy); + let rich_comparison = |op| infer_rich_comparison(context, left, right, op, policy); let membership_test_comparison = |op, range: TextRange| { infer_membership_test_comparison(context, left, right, op, range) }; @@ -321,8 +333,8 @@ fn infer_binary_type_comparison_inner<'db>( ComparisonSoundnessPolicy::from_analysis_settings(db.analysis_settings(context.file())); if let NonIdentityOperator::Rich(rich_op) = op - && let Some(left_tuple) = left.tuple_instance_spec(db) - && let Some(right_tuple) = right.tuple_instance_spec(db) + && let Some(left_tuple) = left.tuple_instance_spec(db, env) + && let Some(right_tuple) = right.tuple_instance_spec(db, env) { return visitor.visit(db, (left, op, right), || { infer_tuple_rich_comparison(context, &left_tuple, rich_op, &right_tuple, range, visitor) @@ -330,12 +342,12 @@ fn infer_binary_type_comparison_inner<'db>( } if let NonIdentityOperator::Membership(op) = op - && let Some(right_tuple) = right.tuple_instance_spec(db) + && let Some(right_tuple) = right.tuple_instance_spec(db, env) && let Tuple::Fixed(right_tuple) = &*right_tuple { let mut any_eq = false; let mut any_ambiguous = false; - let mut equality = TupleEqualityEvaluator::new(db, soundness_policy); + let mut equality = TupleEqualityEvaluator::new(db, env, soundness_policy); for &element_ty in right_tuple.elements_slice() { // It's okay to ignore errors here because Python doesn't call `__bool__` @@ -356,27 +368,27 @@ fn infer_binary_type_comparison_inner<'db>( } else if !any_ambiguous { Type::bool_literal(op.is_not_in()) } else { - KnownClass::Bool.to_instance(db) + KnownClass::Bool.to_instance(db, env) }); } let comparison_truthiness = match op { NonIdentityOperator::Rich(RichCompareOperator::Eq) => { - equality_truthiness(db, left, right, soundness_policy) + equality_truthiness(db, env, left, right, soundness_policy) } NonIdentityOperator::Rich(RichCompareOperator::Ne) => { - inequality_truthiness(db, left, right, soundness_policy) + inequality_truthiness(db, env, left, right, soundness_policy) } _ => Truthiness::Ambiguous, }; if comparison_truthiness != Truthiness::Ambiguous { - return Ok(Type::from_truthiness(db, comparison_truthiness)); + return Ok(Type::from_truthiness(db, env, comparison_truthiness)); } let comparison_result = match (left, right) { (Type::EnumComplement(complement), right) => Some(infer_binary_type_comparison_inner( context, - complement.remaining_literal_union(db), + complement.remaining_literal_union(db, env), op, right, range, @@ -386,13 +398,13 @@ fn infer_binary_type_comparison_inner<'db>( context, left, op, - complement.remaining_literal_union(db), + complement.remaining_literal_union(db, env), range, visitor, )), (Type::Union(union), other) => { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for element in union.elements(db) { builder = builder.add(infer_binary_type_comparison_inner( context, *element, op, other, range, visitor, @@ -401,7 +413,7 @@ fn infer_binary_type_comparison_inner<'db>( Some(Ok(builder.build())) } (other, Type::Union(union)) => { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for element in union.elements(db) { builder = builder.add(infer_binary_type_comparison_inner( context, other, op, *element, range, visitor, @@ -419,7 +431,7 @@ fn infer_binary_type_comparison_inner<'db>( { Some(infer_binary_type_comparison_inner( context, - intersection.with_expanded_typevars_and_newtypes(db), + intersection.with_expanded_typevars_and_newtypes(db, env), op, right, range, @@ -437,7 +449,7 @@ fn infer_binary_type_comparison_inner<'db>( context, left, op, - intersection.with_expanded_typevars_and_newtypes(db), + intersection.with_expanded_typevars_and_newtypes(db, env), range, visitor, )) @@ -541,7 +553,7 @@ fn infer_binary_type_comparison_inner<'db>( (Type::TypeVar(left_tvar), Type::TypeVar(right_tvar)) if left_tvar.identity(db) == right_tvar.identity(db) => { - match left_tvar.typevar(db).bound_or_constraints(db) { + match left_tvar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { visitor.visit(db, (left, op, right), || { @@ -553,7 +565,7 @@ fn infer_binary_type_comparison_inner<'db>( } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { // For constrained TypeVars, check each constraint paired with itself. - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for &constraint in constraints.elements(db) { builder = builder.add(infer_binary_type_comparison_inner( context, constraint, op, constraint, range, visitor, @@ -577,14 +589,14 @@ fn infer_binary_type_comparison_inner<'db>( infer_binary_type_comparison_inner(context, left, op, right, range, visitor) }; - match typevar.typevar(db).bound_or_constraints(db) { + match typevar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { Some(try_dunder(MemberLookupPolicy::default()).or_else(|_| { visitor.visit(db, (left, op, right), || compare_replacement(bound)) })) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for &constraint in constraints.elements(db) { builder = builder.add(compare_replacement(constraint)?); } @@ -778,9 +790,11 @@ fn infer_binary_type_comparison_inner<'db>( Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), ) => { let constraints = ConstraintSetBuilder::new(); - let left = constraints.load(db, left.constraints(db)); - let right = constraints.load(db, right.constraints(db)); - let equivalent = left.iff(db, &constraints, right).is_always_satisfied(db); + let left = constraints.load(db, env, left.constraints(db)); + let right = constraints.load(db, env, right.constraints(db)); + let equivalent = left + .iff(db, &constraints, right) + .is_always_satisfied(db, env); match op { NonIdentityOperator::Rich(RichCompareOperator::Eq) => { Some(Ok(Type::bool_literal(equivalent))) @@ -823,8 +837,9 @@ fn infer_binary_intersection_type_comparison<'db>( } let db = context.db(); + let env = &context.program_environment(); - if let Some(alternatives) = intersection.finite_alternative_union(db) { + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { return match intersection_on { IntersectionOn::Left => { infer_binary_type_comparison_inner(context, alternatives, op, other, range, visitor) @@ -895,9 +910,9 @@ fn infer_binary_intersection_type_comparison<'db>( // // we would get a result type `Literal[True]` which is too narrow. // - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); - builder = builder.add_positive(KnownClass::Bool.to_instance(db)); + builder.add_positive_in_place(KnownClass::Bool.to_instance(db, env)); let mut state = State::NoPositiveElements; @@ -914,7 +929,7 @@ fn infer_binary_intersection_type_comparison<'db>( match result { Ok(ty) => { state = State::Supported; - builder = builder.add_positive(ty); + builder.add_positive_in_place(ty); } Err(error) => { match state { @@ -969,14 +984,17 @@ fn infer_binary_intersection_type_comparison<'db>( /// This function performs rich comparison between two types and returns the resulting type. /// see `` fn infer_rich_comparison<'db>( - db: &'db dyn Db, + context: &InferContext<'db, '_>, left: Type<'db>, right: Type<'db>, op: RichCompareOperator, policy: MemberLookupPolicy, ) -> Result, UnsupportedComparisonError<'db>> { + let db = context.db(); + let env = &context.program_environment(); Type::try_call_rich_comparison_dunder( db, + env, left, right, op.dunder(), @@ -992,7 +1010,7 @@ fn infer_rich_comparison<'db>( // on `object`, so it does not apply if we skip looking up attributes on `object`. && !policy.mro_no_object_fallback() { - Some(KnownClass::Bool.to_instance(db)) + Some(KnownClass::Bool.to_instance(db, env)) } else { None } @@ -1016,19 +1034,21 @@ fn infer_membership_test_comparison<'db>( range: TextRange, ) -> Result, UnsupportedComparisonError<'db>> { let db = context.db(); + let env = &context.program_environment(); let compare_result_opt = match right.try_call_dunder( db, + env, "__contains__", CallArguments::positional([left]), TypeContext::default(), ) { // If `__contains__` is available, it is used directly for the membership test. - Ok(bindings) => Some(bindings.return_type(db)), + Ok(bindings) => Some(bindings.return_type(db, env)), // If `__contains__` is not available or possibly unbound, // fall back to iteration-based membership test. Err(CallDunderError::MethodNotAvailable | CallDunderError::PossiblyUnbound { .. }) => right - .try_iterate(db) - .map(|_| KnownClass::Bool.to_instance(db)) + .try_iterate(db, env) + .map(|_| KnownClass::Bool.to_instance(db, env)) .ok(), // `__contains__` exists but can't be called with the given arguments. Err(CallDunderError::CallError(..)) => None, @@ -1040,14 +1060,14 @@ fn infer_membership_test_comparison<'db>( return ty; } - let truthiness = ty.try_bool(db).unwrap_or_else(|err| { + let truthiness = ty.try_bool(db, env).unwrap_or_else(|err| { err.report_diagnostic(context, range); err.fallback_truthiness() }); match op { - MembershipOperator::In => Type::from_truthiness(db, truthiness), - MembershipOperator::NotIn => Type::from_truthiness(db, truthiness.negate()), + MembershipOperator::In => Type::from_truthiness(db, env, truthiness), + MembershipOperator::NotIn => Type::from_truthiness(db, env, truthiness.negate()), } }) .ok_or_else(|| UnsupportedComparisonError { @@ -1071,17 +1091,18 @@ fn infer_tuple_rich_comparison<'db>( visitor: &BinaryComparisonVisitor<'db>, ) -> Result, UnsupportedComparisonError<'db>> { let db = context.db(); + let env = &context.program_environment(); match (left, right) { // Both fixed-length: perform full lexicographic comparison. (TupleSpec::Fixed(left), TupleSpec::Fixed(right)) => { let left_iter = left.iter_all_elements(); let right_iter = right.iter_all_elements(); - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let soundness_policy = ComparisonSoundnessPolicy::from_analysis_settings( db.analysis_settings(context.file()), ); - let mut equality = TupleEqualityEvaluator::new(db, soundness_policy); + let mut equality = TupleEqualityEvaluator::new(db, env, soundness_policy); for (l_ty, r_ty) in left_iter.zip(right_iter) { let eq_truthiness = equality @@ -1160,7 +1181,7 @@ fn infer_tuple_rich_comparison<'db>( (TupleSpec::Variable(_), _) | (_, TupleSpec::Variable(_)) if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) => { - Ok(KnownClass::Bool.to_instance(db)) + Ok(KnownClass::Bool.to_instance(db, env)) } // At least one variable-length: check all elements that could potentially be compared. @@ -1179,12 +1200,12 @@ fn infer_tuple_rich_comparison<'db>( Ok::<_, UnsupportedComparisonError<'db>>(()) })?; - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for result in results { builder = builder.add(result); } // Length comparison (when all elements are equal) returns bool. - builder = builder.add(KnownClass::Bool.to_instance(db)); + builder = builder.add(KnownClass::Bool.to_instance(db, env)); Ok(builder.build()) } diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index 9a4ccf1dd8..ff04859e00 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -1,18 +1,28 @@ use super::builder::TypeInferenceBuilder; -use crate::db::tests::{TestDb, setup_db}; +use crate::db::tests::{TestDb, TestDbBuilder, setup_db}; use crate::place::symbol; -use crate::place::{ConsideredDefinitions, Place, global_symbol}; +use crate::place::{ConsideredDefinitions, Place, PlaceAndQualifiers}; use crate::types::{KnownClass, KnownInstanceType, check_types}; +use ruff_db::PythonFile; use ruff_db::diagnostic::{Diagnostic, DiagnosticId}; use ruff_db::files::{File, system_path_to_file}; use ruff_db::system::DbWithWritableSystem as _; use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run}; +use ruff_python_ast::PythonVersion; use ty_python_core::definition::Definition; use ty_python_core::scope::FileScopeId; use ty_python_core::{global_scope, place_table, semantic_index, use_def_map}; use super::*; +fn python_file(db: &TestDb, file: File) -> PythonFile<'_> { + PythonFile::new(db, file, db.python_version()) +} + +fn global_symbol<'db>(db: &'db TestDb, file: File, name: &str) -> PlaceAndQualifiers<'db> { + crate::place::global_symbol(db, python_file(db, file), name) +} + #[track_caller] fn get_symbol<'db>( db: &'db TestDb, @@ -21,6 +31,7 @@ fn get_symbol<'db>( symbol_name: &str, ) -> Place<'db> { let file = system_path_to_file(db, file_name).expect("file to exist"); + let file = python_file(db, file); let module = parsed_module(db, file).load(db); let index = semantic_index(db, file); let mut file_scope_id = FileScopeId::global(); @@ -50,7 +61,7 @@ fn assert_diagnostic_messages(diagnostics: &[Diagnostic], expected: &[&str]) { #[track_caller] fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) { let file = system_path_to_file(db, filename).unwrap(); - let diagnostics = check_types(db, file); + let diagnostics = check_types(db, python_file(db, file)); assert_diagnostic_messages(&diagnostics, expected); } @@ -58,7 +69,7 @@ fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) { #[track_caller] fn assert_revealed_type(db: &TestDb, filename: &str, expected: &str) { let file = system_path_to_file(db, filename).unwrap(); - let diagnostics = check_types(db, file); + let diagnostics = check_types(db, python_file(db, file)); assert_eq!(diagnostics.len(), 1, "{diagnostics:#?}"); let diagnostic = &diagnostics[0]; @@ -72,6 +83,74 @@ fn assert_revealed_type(db: &TestDb, filename: &str, expected: &str) { ); } +#[test] +fn same_file_at_different_python_versions() -> anyhow::Result<()> { + let mut db = TestDbBuilder::new() + .with_python_version(PythonVersion::PY311) + .build()?; + db.write_dedented( + "src/main.py", + r#" + import sys + + from typing import reveal_type + from zipfile._path import Path + + if sys.version_info >= (3, 12): + from py312_dependency import value + else: + from py311_dependency import value + + type Alias = int + + reveal_type(value) + "#, + )?; + db.write_dedented("src/py311_dependency.py", "value: str = 'py311'")?; + db.write_dedented("src/py312_dependency.py", "value: int = 312")?; + + let file = system_path_to_file(&db, "src/main.py").expect("file to exist"); + let py311 = PythonFile::new(&db, file, PythonVersion::PY311); + let py312 = PythonFile::new(&db, file, PythonVersion::PY312); + + let check = |file, expected_type, expect_invalid_syntax, expect_unresolved_import| { + let diagnostics = crate::check_file_unwrap(&db, file); + + assert_eq!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.id() == DiagnosticId::InvalidSyntax), + expect_invalid_syntax, + "{diagnostics:#?}" + ); + assert_eq!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.headline_message().contains("zipfile._path")), + expect_unresolved_import, + "{diagnostics:#?}" + ); + + let revealed = diagnostics + .iter() + .find(|diagnostic| diagnostic.id() == DiagnosticId::RevealedType) + .and_then(Diagnostic::primary_annotation) + .and_then(|annotation| annotation.get_message()); + assert_eq!(revealed, Some(expected_type), "{diagnostics:#?}"); + assert_eq!( + diagnostics.len(), + 1 + usize::from(expect_invalid_syntax) + usize::from(expect_unresolved_import), + "{diagnostics:#?}" + ); + }; + + check(py311, "`str`", true, true); + check(py312, "`int`", false, false); + check(py311, "`str`", true, true); + + Ok(()) +} + #[test] fn expected_types_are_collected_only_for_open_files() -> anyhow::Result<()> { let has_expected_type = |open_file: bool| -> anyhow::Result { @@ -90,7 +169,7 @@ fn expected_types_are_collected_only_for_open_files() -> anyhow::Result<()> { db.open_file(file); } - let module = parsed_module(&db, file).load(&db); + let module = parsed_module(&db, python_file(&db, file)).load(&db); let assignment = module.syntax().body[1] .as_ann_assign_stmt() .expect("annotated assignment"); @@ -100,7 +179,7 @@ fn expected_types_are_collected_only_for_open_files() -> anyhow::Result<()> { .expect("annotated assignment to have a value") .as_string_literal_expr() .expect("string literal value"); - let scope = global_scope(&db, file); + let scope = global_scope(&db, python_file(&db, file)); Ok(infer_complete_scope_types(&db, scope) .try_expected_type(ruff_python_ast::ExprRef::from(string_expr)) @@ -130,12 +209,12 @@ fn compact_definition_types_omit_owner() -> anyhow::Result<()> { )?; let file = system_path_to_file(&db, "/src/definitions.py").unwrap(); - let module = parsed_module(&db, file).load(&db); + let module = parsed_module(&db, python_file(&db, file)).load(&db); let first_assignment = module.syntax().body[0].as_assign_stmt().unwrap(); let second_assignment = module.syntax().body[1].as_assign_stmt().unwrap(); - let first = semantic_index(&db, file) + let first = semantic_index(&db, python_file(&db, file)) .expect_single_definition(first_assignment.targets[0].as_name_expr().unwrap()); - let second = semantic_index(&db, file) + let second = semantic_index(&db, python_file(&db, file)) .expect_single_definition(second_assignment.targets[0].as_name_expr().unwrap()); let owner_type = Type::unknown(); @@ -283,17 +362,18 @@ fn pep695_type_params() { ) .unwrap(); + let env = db.program_environment(); let check_typevar = |var: &'static str, display: &'static str, upper_bound: Option<&'static str>, 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(), display); + assert_eq!(var_ty.display(&db, &env).to_string(), display); let expected_name_ty = format!(r#"Literal["{var}"]"#); - let name_ty = var_ty.member(&db, "__name__").place.expect_type(); - assert_eq!(name_ty.display(&db).to_string(), expected_name_ty); + let name_ty = var_ty.member(&db, &env, "__name__").place.expect_type(); + assert_eq!(name_ty.display(&db, &env).to_string(), expected_name_ty); let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = var_ty else { panic!("expected TypeVar"); @@ -301,14 +381,14 @@ fn pep695_type_params() { assert_eq!( typevar - .upper_bound(&db) - .map(|ty| ty.display(&db).to_string()), + .upper_bound(&db, &env) + .map(|ty| ty.display(&db, &env).to_string()), upper_bound.map(std::borrow::ToOwned::to_owned) ); assert_eq!( - typevar.constraints(&db).map(|tys| tys + typevar.constraints(&db, &env).map(|tys| tys .iter() - .map(|ty| ty.display(&db).to_string()) + .map(|ty| ty.display(&db, &env).to_string()) .collect::>()), constraints.map(|strings| strings .iter() @@ -317,8 +397,8 @@ fn pep695_type_params() { ); assert_eq!( typevar - .default_type(&db) - .map(|ty| ty.display(&db).to_string()), + .default_type(&db, &env) + .map(|ty| ty.display(&db, &env).to_string()), default.map(std::borrow::ToOwned::to_owned) ); }; @@ -586,7 +666,7 @@ class Form(Ui): // Incremental inference tests #[track_caller] fn first_public_binding<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> { - let scope = global_scope(db, file); + let scope = global_scope(db, python_file(db, file)); use_def_map(db, scope) .end_of_scope_symbol_bindings(place_table(db, scope).symbol_id(name).unwrap()) .find_map(|b| b.binding.definition()) @@ -605,7 +685,10 @@ fn dependency_public_symbol_type_change() -> anyhow::Result<()> { let a = system_path_to_file(&db, "/src/a.py").unwrap(); let x_ty = global_symbol(&db, a, "x").place.expect_type(); - assert_eq!(x_ty.display(&db).to_string(), "int"); + assert_eq!( + x_ty.display(&db, &db.program_environment()).to_string(), + "int" + ); // Change `x` to a different value db.write_file("/src/foo.py", "x: bool = True\ndef foo(): ...")?; @@ -614,7 +697,10 @@ fn dependency_public_symbol_type_change() -> anyhow::Result<()> { let x_ty_2 = global_symbol(&db, a, "x").place.expect_type(); - assert_eq!(x_ty_2.display(&db).to_string(), "bool"); + assert_eq!( + x_ty_2.display(&db, &db.program_environment()).to_string(), + "bool" + ); Ok(()) } @@ -631,7 +717,10 @@ fn dependency_internal_symbol_change() -> anyhow::Result<()> { let a = system_path_to_file(&db, "/src/a.py").unwrap(); let x_ty = global_symbol(&db, a, "x").place.expect_type(); - assert_eq!(x_ty.display(&db).to_string(), "int"); + assert_eq!( + x_ty.display(&db, &db.program_environment()).to_string(), + "int" + ); db.write_file("/src/foo.py", "x: int = 10\ndef foo(): pass")?; @@ -641,7 +730,10 @@ fn dependency_internal_symbol_change() -> anyhow::Result<()> { let x_ty_2 = global_symbol(&db, a, "x").place.expect_type(); - assert_eq!(x_ty_2.display(&db).to_string(), "int"); + assert_eq!( + x_ty_2.display(&db, &db.program_environment()).to_string(), + "int" + ); let events = db.take_salsa_events(); @@ -667,7 +759,10 @@ fn dependency_unrelated_symbol() -> anyhow::Result<()> { let a = system_path_to_file(&db, "/src/a.py").unwrap(); let x_ty = global_symbol(&db, a, "x").place.expect_type(); - assert_eq!(x_ty.display(&db).to_string(), "int"); + assert_eq!( + x_ty.display(&db, &db.program_environment()).to_string(), + "int" + ); db.write_file("/src/foo.py", "x: int = 10\ny: bool = False")?; @@ -677,7 +772,10 @@ fn dependency_unrelated_symbol() -> anyhow::Result<()> { let x_ty_2 = global_symbol(&db, a, "x").place.expect_type(); - assert_eq!(x_ty_2.display(&db).to_string(), "int"); + assert_eq!( + x_ty_2.display(&db, &db.program_environment()).to_string(), + "int" + ); let events = db.take_salsa_events(); @@ -694,12 +792,12 @@ fn dependency_unrelated_symbol() -> anyhow::Result<()> { fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { fn x_rhs_expression(db: &TestDb) -> Expression<'_> { let file_main = system_path_to_file(db, "/src/main.py").unwrap(); - let ast = parsed_module(db, file_main).load(db); + let ast = parsed_module(db, python_file(db, file_main)).load(db); // Get the second statement in `main.py` (x = …) and extract the expression // node on the right-hand side: let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value; - let index = semantic_index(db, file_main); + let index = semantic_index(db, python_file(db, file_main)); index.expression(x_rhs_node.as_ref()) } @@ -724,7 +822,10 @@ fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { let file_main = system_path_to_file(&db, "/src/main.py").unwrap(); let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "int | None"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "int | None" + ); // Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred db.write_dedented( @@ -739,7 +840,10 @@ fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { let events = { db.clear_salsa_events(); let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "str | None"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "str | None" + ); db.take_salsa_events() }; assert_function_query_was_run( @@ -763,7 +867,10 @@ fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { let events = { db.clear_salsa_events(); let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "str | None"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "str | None" + ); db.take_salsa_events() }; @@ -783,12 +890,12 @@ fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { fn dependency_own_instance_member() -> anyhow::Result<()> { fn x_rhs_expression(db: &TestDb) -> Expression<'_> { let file_main = system_path_to_file(db, "/src/main.py").unwrap(); - let ast = parsed_module(db, file_main).load(db); + let ast = parsed_module(db, python_file(db, file_main)).load(db); // Get the second statement in `main.py` (x = …) and extract the expression // node on the right-hand side: let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value; - let index = semantic_index(db, file_main); + let index = semantic_index(db, python_file(db, file_main)); index.expression(x_rhs_node.as_ref()) } @@ -815,7 +922,10 @@ fn dependency_own_instance_member() -> anyhow::Result<()> { let file_main = system_path_to_file(&db, "/src/main.py").unwrap(); let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "int | None"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "int | None" + ); // Change the type of `attr` to `str | None`; this should trigger the type of `x` to be re-inferred db.write_dedented( @@ -832,7 +942,10 @@ fn dependency_own_instance_member() -> anyhow::Result<()> { let events = { db.clear_salsa_events(); let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "str | None"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "str | None" + ); db.take_salsa_events() }; assert_function_query_was_run( @@ -858,7 +971,10 @@ fn dependency_own_instance_member() -> anyhow::Result<()> { let events = { db.clear_salsa_events(); let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "str | None"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "str | None" + ); db.take_salsa_events() }; @@ -876,12 +992,12 @@ fn dependency_own_instance_member() -> anyhow::Result<()> { fn dependency_implicit_class_member() -> anyhow::Result<()> { fn x_rhs_expression(db: &TestDb) -> Expression<'_> { let file_main = system_path_to_file(db, "/src/main.py").unwrap(); - let ast = parsed_module(db, file_main).load(db); + let ast = parsed_module(db, python_file(db, file_main)).load(db); // Get the third statement in `main.py` (x = …) and extract the expression // node on the right-hand side: let x_rhs_node = &ast.syntax().body[2].as_assign_stmt().unwrap().value; - let index = semantic_index(db, file_main); + let index = semantic_index(db, python_file(db, file_main)); index.expression(x_rhs_node.as_ref()) } @@ -911,7 +1027,10 @@ fn dependency_implicit_class_member() -> anyhow::Result<()> { let file_main = system_path_to_file(&db, "/src/main.py").unwrap(); let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "int"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "int" + ); // Change the type of `class_attr` to `str`; this should trigger the type of `x` to be re-inferred db.write_dedented( @@ -930,7 +1049,10 @@ fn dependency_implicit_class_member() -> anyhow::Result<()> { let events = { db.clear_salsa_events(); let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "str"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "str" + ); db.take_salsa_events() }; assert_function_query_was_run( @@ -958,7 +1080,10 @@ fn dependency_implicit_class_member() -> anyhow::Result<()> { let events = { db.clear_salsa_events(); let attr_ty = global_symbol(&db, file_main, "x").place.expect_type(); - assert_eq!(attr_ty.display(&db).to_string(), "str"); + assert_eq!( + attr_ty.display(&db, &db.program_environment()).to_string(), + "str" + ); db.take_salsa_events() }; @@ -998,12 +1123,15 @@ fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> { let bar = system_path_to_file(&db, "src/bar.py")?; let a = global_symbol(&db, bar, "a").place; - assert_eq!(a.expect_type(), KnownClass::Int.to_instance(&db)); + assert_eq!( + a.expect_type(), + KnownClass::Int.to_instance(&db, &db.program_environment()) + ); let events = db.take_salsa_events(); - let module = parsed_module(&db, bar).load(&db); + let module = parsed_module(&db, python_file(&db, bar)).load(&db); let call = &*module.syntax().body[1].as_assign_stmt().unwrap().value; - let foo_call = semantic_index(&db, bar).expression(call); + let foo_call = semantic_index(&db, python_file(&db, bar)).expression(call); assert_function_query_was_run( &db, @@ -1026,12 +1154,15 @@ fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> { let a = global_symbol(&db, bar, "a").place; - assert_eq!(a.expect_type(), KnownClass::Int.to_instance(&db)); + assert_eq!( + a.expect_type(), + KnownClass::Int.to_instance(&db, &db.program_environment()) + ); let events = db.take_salsa_events(); - let module = parsed_module(&db, bar).load(&db); + let module = parsed_module(&db, python_file(&db, bar)).load(&db); let call = &*module.syntax().body[1].as_assign_stmt().unwrap().value; - let foo_call = semantic_index(&db, bar).expression(call); + let foo_call = semantic_index(&db, python_file(&db, bar)).expression(call); assert_function_query_was_not_run( &db, diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 4ff9987cec..f1bc7473f3 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -1,5 +1,6 @@ //! Instance types: both nominal and structural. +use crate::ProgramEnvironment; use std::borrow::Cow; use std::cell::Cell; use std::marker::PhantomData; @@ -54,7 +55,11 @@ impl<'db> Type<'db> { ) } - pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { + pub(crate) fn instance( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType<'db>, + ) -> Self { match class.class_literal(db) { // Dynamic classes created via `type()` don't have special instance types. ClassLiteral::Dynamic(_) @@ -69,6 +74,7 @@ impl<'db> Type<'db> { match class_literal.known(db) { Some(KnownClass::Tuple) => Type::tuple(TupleType::new( db, + env, specialization .and_then(|spec| Some(Cow::Borrowed(spec.tuple(db)?))) .unwrap_or_else(|| Cow::Owned(TupleSpec::homogeneous(Type::unknown()))) @@ -100,23 +106,32 @@ impl<'db> Type<'db> { Type::tuple_instance(tuple) } - pub fn homogeneous_tuple(db: &'db dyn Db, element: Type<'db>) -> Self { - Type::tuple_instance(TupleType::homogeneous(db, element)) + pub fn homogeneous_tuple( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + element: Type<'db>, + ) -> Self { + Type::tuple_instance(TupleType::homogeneous(db, env, element)) } - pub(crate) fn heterogeneous_tuple(db: &'db dyn Db, elements: I) -> Self + pub(crate) fn heterogeneous_tuple( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Self where I: IntoIterator, T: Into>, { Type::tuple(TupleType::heterogeneous( db, + env, elements.into_iter().map(Into::into), )) } - pub(crate) fn empty_tuple(db: &'db dyn Db) -> Self { - Type::tuple_instance(TupleType::empty(db)) + pub(crate) fn empty_tuple(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + Type::tuple_instance(TupleType::empty(db, env)) } /// **Private** helper function to create a `Type::NominalInstance` from a tuple. @@ -145,28 +160,38 @@ impl<'db> Type<'db> { /// Return `true` if `self` is a nominal instance of the given known class. pub(crate) fn is_instance_of(self, db: &'db dyn Db, known_class: KnownClass) -> bool { match self { - Type::NominalInstance(instance) => instance.class(db).is_known(db, known_class), + Type::NominalInstance(instance) => instance.has_known_class(db, known_class), _ => false, } } /// Synthesize a protocol instance type with a given set of read-only property members. - pub(super) fn protocol_with_readonly_members<'a, M>(db: &'db dyn Db, members: M) -> Self + pub(super) fn protocol_with_readonly_members<'a, M>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + members: M, + ) -> Self where M: IntoIterator)>, { Self::ProtocolInstance(ProtocolInstanceType::synthesized( - SynthesizedProtocolType::new(ProtocolInterface::with_property_members(db, members)), + SynthesizedProtocolType::new(ProtocolInterface::with_property_members( + db, env, members, + )), )) } /// Synthesize a protocol instance type with a given set of methods. - pub(super) fn protocol_with_methods<'a, M>(db: &'db dyn Db, methods: M) -> Self + pub(super) fn protocol_with_methods<'a, M>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + methods: M, + ) -> Self where M: IntoIterator)>, { Self::ProtocolInstance(ProtocolInstanceType::synthesized( - SynthesizedProtocolType::new(ProtocolInterface::with_methods(db, methods)), + SynthesizedProtocolType::new(ProtocolInterface::with_methods(db, env, methods)), )) } } @@ -189,7 +214,9 @@ pub(super) fn walk_nominal_instance_type<'db, V: super::visitor::TypeVisitor<'db walk_tuple_type(db, tuple, visitor); } NominalInstanceInner::Object => {} - NominalInstanceInner::NonTuple(class) => visitor.visit_type(db, class.class(db).into()), + NominalInstanceInner::NonTuple(class) => { + visitor.visit_type(db, class.class(db).into()); + } NominalInstanceInner::SysVersionInfo => {} } } @@ -216,8 +243,8 @@ impl<'db> NominalInstanceType<'db> { /// As of 2026-02-16, this method is not used in any crates in the Ruff /// repo, but is exposed as a public API for external users of /// `ty_python_semantic`. - pub fn class_name(&self, db: &'db dyn Db) -> &'db Name { - self.class(db).name(db) + pub fn class_name(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> &'db Name { + self.class(db, env).name(db) } /// Returns the fully qualified module name of the module in which the class @@ -230,25 +257,33 @@ impl<'db> NominalInstanceType<'db> { /// As of 2026-02-16, this method is not used in any crates in the Ruff /// repo, but is exposed as a public API for external users of /// `ty_python_semantic`. - pub fn class_module_name(&self, db: &'db dyn Db) -> Option<&'db ModuleName> { - let file = self.class(db).class_literal(db).file(db); - file_to_module(db, file).map(|module| module.name(db)) + pub fn class_module_name( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option<&'db ModuleName> { + let class = self.class(db, env).class_literal(db); + file_to_module(db, class.python_file(db)).map(|module| module.name(db)) } - pub(super) fn class(&self, db: &'db dyn Db) -> ClassType<'db> { + pub(super) fn class(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> ClassType<'db> { match self.0 { NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db), NominalInstanceInner::NonTuple(class) => class.class(db), NominalInstanceInner::SysVersionInfo => { - sys_version_info_class(db).unwrap_or_else(|| ClassType::object(db)) + sys_version_info_class(db, env).unwrap_or_else(|| ClassType::object(db, env)) } - NominalInstanceInner::Object => ClassType::object(db), + NominalInstanceInner::Object => ClassType::object(db, env), } } /// Returns the class literal for this instance. - pub(super) fn class_literal(&self, db: &'db dyn Db) -> ClassLiteral<'db> { - self.class(db).class_literal(db) + pub(super) fn class_literal( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> ClassLiteral<'db> { + self.class(db, env).class_literal(db) } /// Returns the [`KnownClass`] that this is a nominal instance of, or `None` if it is not an @@ -275,11 +310,15 @@ impl<'db> NominalInstanceType<'db> { /// /// I.e., for the type `tuple[int, str]`, this will return the tuple spec `[int, str]`. /// For a subclass of `tuple[int, str]`, it will return the same tuple spec. - pub(super) fn tuple_spec(&self, db: &'db dyn Db) -> Option>> { + pub(super) fn tuple_spec( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { match self.0 { NominalInstanceInner::ExactTuple(tuple) => Some(Cow::Borrowed(tuple.tuple(db))), NominalInstanceInner::SysVersionInfo => { - Some(Cow::Owned(TupleSpec::version_info_spec(db))) + Some(Cow::Owned(TupleSpec::version_info_spec(db, env))) } NominalInstanceInner::Object => None, NominalInstanceInner::NonTuple(class) => { @@ -387,13 +426,14 @@ impl<'db> NominalInstanceType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self.0 { NominalInstanceInner::ExactTuple(tuple) => { Some(Self(NominalInstanceInner::ExactTuple( - tuple.recursive_type_normalized_impl(db, div, nested)?, + tuple.recursive_type_normalized_impl(db, env, div, nested)?, ))) } NominalInstanceInner::SysVersionInfo => { @@ -403,7 +443,7 @@ impl<'db> NominalInstanceType<'db> { NominalInstanceInner::NonTuple(class) => { let transformed = class .class(db) - .recursive_type_normalized_impl(db, div, nested)?; + .recursive_type_normalized_impl(db, env, div, nested)?; Some(Self(NominalInstanceInner::NonTuple( class.with_class(db, transformed), ))) @@ -428,8 +468,8 @@ impl<'db> NominalInstanceType<'db> { } } - pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { - SubclassOfType::from(db, self.class(db)) + pub(super) fn to_meta_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + SubclassOfType::from(db, env, self.class(db, env)) } pub(super) fn apply_type_mapping_impl<'a>( @@ -437,7 +477,7 @@ impl<'db> NominalInstanceType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Type<'db> { match self.0 { NominalInstanceInner::ExactTuple(tuple) => { @@ -460,19 +500,24 @@ impl<'db> NominalInstanceType<'db> { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { match self.0 { NominalInstanceInner::ExactTuple(tuple) => { - tuple.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + tuple.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } NominalInstanceInner::SysVersionInfo | NominalInstanceInner::Object => {} NominalInstanceInner::NonTuple(class) => { - class - .class(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + class.class(db).find_legacy_typevars_impl( + db, + env, + binding_context, + typevars, + visitor, + ); } } } @@ -546,14 +591,15 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return nominally_satisfied; } + let env = self.env; // A nominal relation that cannot succeed cannot bypass any materialized requirement. // Check that inexpensive case first: comparing every requirement of an unrelated // recursive protocol can expand its interface before structural member ordering gets // a chance to reject an incompatible finite member. - let nominal_is_safe = nominally_satisfied.is_never_satisfied(db) - || (!protocol.materialization_changes_requirements(db, protocol) + let nominal_is_safe = nominally_satisfied.is_never_satisfied(db, env) + || (!protocol.materialization_changes_requirements(db, env, protocol) && !source_protocol.is_some_and(|source| { - source.materialization_changes_requirements(db, protocol) + source.materialization_changes_requirements(db, env, protocol) })); if nominal_is_safe { @@ -581,8 +627,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // potentially redundant union arm. if matches!(self.relation, TypeRelation::Redundancy { pure: false }) && source_protocol_as_nominal.is_some_and(|source_instance| { - source_instance.class(db).class_literal(db) - == nominal_instance.class(db).class_literal(db) + source_instance.class(db, env).class_literal(db) + == nominal_instance.class(db, env).class_literal(db) }) { return nominally_satisfied; @@ -593,8 +639,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // Fast path: skip expensive per-member type comparisons when members are plainly // missing. When collecting error context, we continue and let the structural check // below report per-member errors instead. + let env = self.env; if !self.is_context_collection_enabled() - && !has_all_protocol_members_defined(db, ty, protocol) + && !has_all_protocol_members_defined(db, env, ty, protocol) { return result; } @@ -615,7 +662,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }) }; if let Some(context) = self.report_context() - && structurally_satisfied.is_never_satisfied(db) + && structurally_satisfied.is_never_satisfied(db, env) { context.push(ErrorContext::TypeNotCompatibleWithProtocol { ty, @@ -647,9 +694,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return None; }; let source_instance = source_protocol_as_nominal?; - let (ClassType::Generic(source_alias), ClassType::Generic(target_alias)) = - (source_instance.class(db), nominal_instance.class(db)) - else { + let env = self.env; + let (ClassType::Generic(source_alias), ClassType::Generic(target_alias)) = ( + source_instance.class(db, env), + nominal_instance.class(db, env), + ) else { return None; }; if source_alias.origin(db) != target_alias.origin(db) { @@ -706,12 +755,13 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { meta_ty: Type<'db>, protocol: ProtocolInstanceType<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; debug_assert!(matches!( meta_ty, Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::GenericAlias(_) )); - let constructed_ty = meta_ty.bindings(db).return_type(db); + let constructed_ty = meta_ty.bindings(db, env).return_type(db, env); self.check_type_pair(db, constructed_ty, Type::ProtocolInstance(protocol)) .and(db, self.constraints, || { self.check_meta_protocol_members(db, constructed_ty, meta_ty, protocol) @@ -730,7 +780,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { NominalInstanceInner::ExactTuple(source_tuple), NominalInstanceInner::ExactTuple(target_tuple), ) => self.check_tuple_type_pair(db, source_tuple, target_tuple), - _ => self.check_class_pair(db, source.class(db), target.class(db)), + _ => { + let env = self.env; + self.check_class_pair(db, source.class(db, env), target.class(db, env)) + } } } } @@ -752,13 +805,18 @@ fn non_recursive_protocol_interface<'db>( protocol: ProtocolClass<'db>, receiver_ty: Type<'db>, ) -> ProtocolInterface<'db> { - struct ProtocolReferenceFinder<'db> { + struct ProtocolReferenceFinder<'a, 'db> { + env: &'a ProgramEnvironment<'db>, origin: ClassLiteral<'db>, found: Cell, recursion_guard: TypeCollector<'db>, } - impl<'db> TypeVisitor<'db> for ProtocolReferenceFinder<'db> { + impl<'db> TypeVisitor<'db> for ProtocolReferenceFinder<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -775,7 +833,9 @@ fn non_recursive_protocol_interface<'db>( if ty .as_protocol_instance() .and_then(|protocol| protocol.nominal_origin_instance(db)) - .is_some_and(|instance| instance.class_literal(db) == self.origin) + .is_some_and(|instance| { + instance.class_literal(db, self.program_environment()) == self.origin + }) { self.found.set(true); return; @@ -785,8 +845,10 @@ fn non_recursive_protocol_interface<'db>( } } + let env = ProgramEnvironment::from_file(protocol.class_literal(db).python_file(db)); interface.filter_members(db, |member| { let visitor = ProtocolReferenceFinder { + env: &env, origin: protocol.class_literal(db), found: Cell::new(false), recursion_guard: TypeCollector::default(), @@ -811,13 +873,15 @@ fn non_recursive_protocol_constraints<'db>( source: ProtocolInstanceType<'db>, target: ProtocolInterfaceView<'db>, ) -> OwnedConstraintSet<'db> { + let env = ProgramEnvironment::from_program(target.base().program(db)); let constraints = ConstraintSetBuilder::new(); constraints.into_owned(|constraints| { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(&env); let checker = TypeRelationChecker::constraint_set_assignability( + &env, constraints, &relation_visitor, &disjointness_visitor, @@ -857,8 +921,9 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { if left.is_object() || right.is_object() { return result; } - if let Some(left_spec) = left.tuple_spec(db) - && let Some(right_spec) = right.tuple_spec(db) + let env = self.env; + if let Some(left_spec) = left.tuple_spec(db, env) + && let Some(right_spec) = right.tuple_spec(db, env) { let compatible = self.check_tuple_spec_pair(db, &left_spec, &right_spec); if result @@ -873,8 +938,13 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { ConstraintSet::from_bool( self.constraints, !left - .class(db) - .could_coexist_in_mro_with_disjointness_checker(db, right.class(db), self), + .class(db, env) + .could_coexist_in_mro_with_disjointness_checker( + db, + env, + right.class(db, env), + self, + ), ) }) } @@ -956,9 +1026,12 @@ enum NominalInstanceInner<'db> { SysVersionInfo, } -fn sys_version_info_class(db: &dyn Db) -> Option> { +fn sys_version_info_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, +) -> Option> { KnownClass::VersionInfo - .try_to_class_literal(db) + .try_to_class_literal(db, env) .map(|class| class.default_specialization(db)) } @@ -969,8 +1042,13 @@ pub(crate) struct SliceLiteral { } impl<'db> VarianceInferable<'db> for NominalInstanceType<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { - self.class(db).variance_of(db, typevar) + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.class(db, env).variance_of(db, env, typevar) } } @@ -1114,12 +1192,13 @@ impl<'db> ProtocolInstanceType<'db> { fn materialization_changes_requirements( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: ProtocolInstanceType<'db>, ) -> bool { self.materialization_kind(db).is_some() && self .interface(db) - .differs_for_members_required_by(db, target.interface(db)) + .differs_for_members_required_by(db, env, target.interface(db)) } /// Returns the materialization wrapper needed for displaying this protocol. @@ -1130,6 +1209,7 @@ impl<'db> ProtocolInstanceType<'db> { pub(super) fn display_materialization_kind( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> Option { let Protocol::Materialized(materialized) = self.inner else { return None; @@ -1146,12 +1226,12 @@ impl<'db> ProtocolInstanceType<'db> { let interface = self.interface(db); interface - .differs_for_members_required_by(db, interface) + .differs_for_members_required_by(db, env, interface) .then_some(materialized.materialization_kind(db)) } /// Return the structural meta-type of this protocol-instance type. - pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { + pub(super) fn to_meta_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self.inner { Protocol::FromClass(_) | Protocol::Materialized(_) => { SubclassOfType::from_protocol(self) @@ -1170,15 +1250,19 @@ impl<'db> ProtocolInstanceType<'db> { // 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), + Protocol::Synthesized(_) => KnownClass::Type.to_instance(db, env), } } /// Return the nominal meta-type used for internal class-member lookup on a protocol instance. - pub(super) fn to_nominal_meta_type(self, db: &'db dyn Db) -> Type<'db> { + pub(super) fn to_nominal_meta_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { self.class_origin(db).map_or_else( - || self.to_meta_type(db), - |origin| SubclassOfType::from(db, *origin), + || self.to_meta_type(db, env), + |origin| SubclassOfType::from(db, env, *origin), ) } @@ -1195,12 +1279,14 @@ impl<'db> ProtocolInstanceType<'db> { protocol: ProtocolInstanceType<'db>, _: (), ) -> bool { + let env = ProgramEnvironment::from_program(protocol.interface(db).base().program(db)); let constraints = ConstraintSetBuilder::new(); let relation_visitor = HasRelationToVisitor::default(&constraints); let disjointness_visitor = IsDisjointVisitor::default(&constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(&env); let checker = TypeRelationChecker::subtyping( + &env, &constraints, TypeVarSet::None, &relation_visitor, @@ -1210,7 +1296,7 @@ impl<'db> ProtocolInstanceType<'db> { ); checker .check_type_satisfies_protocol(db, Type::object(), protocol) - .is_always_satisfied(db) + .is_always_satisfied(db, &env) } is_equivalent_to_object_inner(db, self, ()) @@ -1219,11 +1305,14 @@ impl<'db> ProtocolInstanceType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self { - inner: self.inner.recursive_type_normalized_impl(db, div, nested)?, + inner: self + .inner + .recursive_type_normalized_impl(db, env, div, nested)?, _phantom: PhantomData, }) } @@ -1232,22 +1321,30 @@ impl<'db> ProtocolInstanceType<'db> { fn materialized_interface_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, ) -> Option> { self.materialization_kind(db)?; let interface = self.interface(db); interface .includes_member(db, name) - .then(|| interface.instance_member(db, name)) + .then(|| interface.instance_member(db, env, name)) } - pub(crate) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub(crate) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { match self.inner { - Protocol::FromClass(class) => class.instance_member(db, name), - Protocol::Synthesized(synthesized) => synthesized.interface().instance_member(db, name), + Protocol::FromClass(class) => class.instance_member(db, env, name), + Protocol::Synthesized(synthesized) => { + synthesized.interface().instance_member(db, env, name) + } Protocol::Materialized(materialized) => self - .materialized_interface_member(db, name) - .unwrap_or_else(|| materialized.origin(db).instance_member(db, name)), + .materialized_interface_member(db, env, name) + .unwrap_or_else(|| materialized.origin(db).instance_member(db, env, name)), } } @@ -1256,7 +1353,7 @@ impl<'db> ProtocolInstanceType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self.inner { Protocol::FromClass(class) => { @@ -1292,20 +1389,22 @@ impl<'db> ProtocolInstanceType<'db> { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { match self.inner { Protocol::FromClass(class) => { - class.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + class.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Protocol::Synthesized(synthesized) => { - synthesized.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + synthesized.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Protocol::Materialized(materialized) => { materialized.origin(db).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -1345,8 +1444,13 @@ impl<'db> ProtocolInstanceType<'db> { } impl<'db> VarianceInferable<'db> for ProtocolInstanceType<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { - self.inner.variance_of(db, typevar) + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.inner.variance_of(db, env, typevar) } } @@ -1388,22 +1492,23 @@ impl<'db> Protocol<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Self::FromClass(class) => Some(Self::FromClass( - class.recursive_type_normalized_impl(db, div, nested)?, + class.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::Synthesized(synthesized) => Some(Self::Synthesized( - synthesized.recursive_type_normalized_impl(db, div, nested)?, + synthesized.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::Materialized(materialized) => { Some(Self::Materialized(MaterializedProtocolType::new( db, materialized .origin(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, materialized.materialization_kind(db), ))) } @@ -1412,27 +1517,33 @@ impl<'db> Protocol<'db> { } impl<'db> VarianceInferable<'db> for Protocol<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { match self { - Protocol::FromClass(class_type) => class_type.variance_of(db, typevar), + Protocol::FromClass(class_type) => class_type.variance_of(db, env, typevar), Protocol::Synthesized(synthesized_protocol_type) => { - synthesized_protocol_type.variance_of(db, typevar) + synthesized_protocol_type.variance_of(db, env, typevar) } Protocol::Materialized(materialized) => { - materialized.origin(db).variance_of(db, typevar) + materialized.origin(db).variance_of(db, env, typevar) } } } } mod synthesized_protocol { + use crate::types::protocol_class::ProtocolInterface; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, Type, TypeContext, TypeMapping, TypeVarVariance, VarianceInferable, }; - use crate::{Db, FxOrderSet}; + use crate::{Db, FxOrderSet, ProgramEnvironment}; use ty_python_core::definition::Definition; /// A "synthesized" protocol type that is dissociated from a class definition in source code. @@ -1449,7 +1560,7 @@ mod synthesized_protocol { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self( self.0 @@ -1460,12 +1571,13 @@ mod synthesized_protocol { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { self.0 - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + .find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } pub(in crate::types) fn interface(self) -> ProtocolInterface<'db> { @@ -1475,11 +1587,13 @@ mod synthesized_protocol { pub(in crate::types) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self( - self.0.recursive_type_normalized_impl(db, div, nested)?, + self.0 + .recursive_type_normalized_impl(db, env, div, nested)?, )) } } @@ -1488,9 +1602,10 @@ mod synthesized_protocol { fn variance_of( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar: BoundTypeVarIdentity<'db>, ) -> TypeVarVariance { - self.0.variance_of(db, typevar) + self.0.variance_of(db, env, typevar) } } } diff --git a/crates/ty_python_semantic/src/types/iteration.rs b/crates/ty_python_semantic/src/types/iteration.rs index 34602d75f9..a679e0541a 100644 --- a/crates/ty_python_semantic/src/types/iteration.rs +++ b/crates/ty_python_semantic/src/types/iteration.rs @@ -1,15 +1,14 @@ -use crate::{ - Db, - types::{ - AwaitError, Bindings, CallArguments, CallDunderError, KnownClass, LintDiagnosticGuard, - LintDiagnosticGuardBuilder, LiteralValueTypeKind, Type, TypeContext, - TypeVarBoundOrConstraints, UnionType, - call::CallErrorKind, - context::InferContext, - diagnostic::NOT_ITERABLE, - todo_type, - tuple::{TupleSpec, TupleSpecBuilder}, - }, +use crate::Db; +use crate::ProgramEnvironment; +use crate::types::{ + AwaitError, Bindings, CallArguments, CallDunderError, KnownClass, LintDiagnosticGuard, + LintDiagnosticGuardBuilder, LiteralValueTypeKind, Type, TypeContext, TypeVarBoundOrConstraints, + UnionType, + call::CallErrorKind, + context::InferContext, + diagnostic::NOT_ITERABLE, + todo_type, + tuple::{TupleSpec, TupleSpecBuilder}, }; use compact_str::ToCompactString; use ruff_python_ast as ast; @@ -22,11 +21,13 @@ use ty_python_core::EvaluationMode; /// recursively unpacking starred elements whose iterables are also fixed-length. pub(crate) fn extract_fixed_length_iterable_element_types<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, iterable: &ast::Expr, mut expression_type: impl FnMut(&ast::Expr) -> Type<'db>, ) -> Option]>> { fn extend_fixed_length_iterable<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, iterable: &ast::Expr, expression_type: &mut impl FnMut(&ast::Expr) -> Type<'db>, element_types: &mut Vec>, @@ -42,6 +43,7 @@ pub(crate) fn extract_fixed_length_iterable_element_types<'db>( if let ast::Expr::Starred(starred) = element { extend_fixed_length_iterable( db, + env, starred.value.as_ref(), expression_type, element_types, @@ -54,14 +56,14 @@ pub(crate) fn extract_fixed_length_iterable_element_types<'db>( } let iterable_type = expression_type(iterable); - let spec = iterable_type.try_iterate(db).ok()?; + let spec = iterable_type.try_iterate(db, env).ok()?; let tuple = spec.as_fixed_length()?; element_types.extend(tuple.all_elements().iter().copied()); Some(()) } let mut element_types = Vec::new(); - extend_fixed_length_iterable(db, iterable, &mut expression_type, &mut element_types)?; + extend_fixed_length_iterable(db, env, iterable, &mut expression_type, &mut element_types)?; Some(element_types.into_boxed_slice()) } @@ -70,9 +72,14 @@ impl<'db> Type<'db> { /// /// This method should only be used outside of type checking because it omits any errors. /// For type checking, use [`try_iterate`](Self::try_iterate) instead. - pub(super) fn iterate(self, db: &'db dyn Db) -> Cow<'db, TupleSpec<'db>> { - self.try_iterate(db) - .unwrap_or_else(|err| Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(db)))) + pub(super) fn iterate( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Cow<'db, TupleSpec<'db>> { + self.try_iterate(db, env).unwrap_or_else(|err| { + Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(db, env))) + }) } /// Given the type of an object that is iterated over in some way, @@ -86,17 +93,20 @@ impl<'db> Type<'db> { pub(super) fn try_iterate( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> Result>, IterationError<'db>> { - self.try_iterate_with_mode(db, EvaluationMode::Sync) + self.try_iterate_with_mode(db, env, EvaluationMode::Sync) } pub(super) fn try_iterate_with_mode( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mode: EvaluationMode, ) -> Result>, IterationError<'db>> { fn non_async_special_case<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option>> { // We will not infer precise heterogeneous tuple specs for literals with lengths above this threshold. @@ -106,8 +116,10 @@ impl<'db> Type<'db> { const MAX_TUPLE_LENGTH: usize = 128; match ty { - Type::NominalInstance(nominal) => nominal.tuple_spec(db), - Type::NewTypeInstance(newtype) => non_async_special_case(db, newtype.concrete_base_type(db)), + Type::NominalInstance(nominal) => nominal.tuple_spec(db, env), + Type::NewTypeInstance(newtype) => { + non_async_special_case(db, env, newtype.concrete_base_type(db)) + } Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => { Some(Cow::Owned(TupleSpec::homogeneous(todo_type!( "*tuple[] annotations" @@ -123,7 +135,9 @@ impl<'db> Type<'db> { .map(|b| Type::int_literal( i64::from(*b))), ) } else { - TupleSpec::homogeneous(KnownClass::Int.to_instance(db)) + TupleSpec::homogeneous( + KnownClass::Int.to_instance(db, env), + ) }; Some(Cow::Owned(spec)) }, @@ -155,22 +169,31 @@ impl<'db> Type<'db> { Some(Cow::Owned(TupleSpec::homogeneous(Type::unknown()))) } Type::TypeAlias(alias) => { - non_async_special_case(db, alias.value_type(db)) + non_async_special_case(db, env, alias.value_type(db)) } - Type::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db)? { + Type::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db, env)? { TypeVarBoundOrConstraints::UpperBound(bound) => { - non_async_special_case(db, bound) + non_async_special_case(db, env, bound) + } + TypeVarBoundOrConstraints::Constraints(constraints) => { + non_async_special_case(db, env, constraints.as_type(db, env)) } - TypeVarBoundOrConstraints::Constraints(constraints) => non_async_special_case(db, constraints.as_type(db)), }, Type::Union(union) => { let elements = union.elements(db); if elements.len() < MAX_TUPLE_LENGTH { let mut elements_iter = elements.iter(); - let first_element_spec = elements_iter.next()?.try_iterate_with_mode(db, EvaluationMode::Sync).ok()?; + let first_element_spec = elements_iter + .next()? + .try_iterate_with_mode(db, env, EvaluationMode::Sync) + .ok()?; let mut builder = TupleSpecBuilder::from(&*first_element_spec); for element in elements_iter { - builder = builder.union(db, &*element.try_iterate_with_mode(db, EvaluationMode::Sync).ok()?); + builder = builder.union(db, env, + &*element + .try_iterate_with_mode(db, env, EvaluationMode::Sync) + .ok()?, + ); } Some(Cow::Owned(builder.build())) } else { @@ -191,12 +214,16 @@ impl<'db> Type<'db> { // - A simpler type (if it fully simplified). // // We then iterate over the flattened type. - let flattened = ty.flatten_typevars(db); + let flattened = ty.flatten_typevars(db, env); // If flattening didn't change anything, iterate the intersection directly. if flattened == ty { let mut specs_iter = intersection.positive_elements_or_object(db).filter_map( - |element| element.try_iterate_with_mode(db, EvaluationMode::Sync).ok(), + |element| { + element + .try_iterate_with_mode(db, env, EvaluationMode::Sync) + .ok() + }, ); let first_spec = specs_iter.next()?; let mut builder = TupleSpecBuilder::from(&*first_spec); @@ -204,7 +231,7 @@ impl<'db> Type<'db> { // Two tuples cannot have incompatible specs unless the tuples themselves // are disjoint. `IntersectionBuilder` eagerly simplifies such // intersections to `Never`, so this should always return `Some`. - let Some(intersected) = builder.intersect(db, &spec) else { + let Some(intersected) = builder.intersect(db, env, &spec) else { return Some(Cow::Owned(TupleSpec::homogeneous(Type::unknown()))); }; builder = intersected; @@ -213,10 +240,10 @@ impl<'db> Type<'db> { } // Flattening changed the type; recursively iterate the flattened result. - flattened.try_iterate(db).ok() + flattened.try_iterate(db, env).ok() } Type::EnumComplement(complement) => { - non_async_special_case(db, complement.remaining_literal_union(db)) + non_async_special_case(db, env, complement.remaining_literal_union(db, env)) } // N.B. This special case isn't strictly necessary, it's just an obvious optimization Type::Dynamic(_) => Some(Cow::Owned(TupleSpec::homogeneous(ty))), @@ -253,9 +280,9 @@ impl<'db> Type<'db> { if mode.is_async() { if let Type::Intersection(_) = self { - let flattened = self.flatten_typevars(db); + let flattened = self.flatten_typevars(db, env); if flattened != self { - return flattened.try_iterate_with_mode(db, mode); + return flattened.try_iterate_with_mode(db, env, mode); } } @@ -266,21 +293,25 @@ impl<'db> Type<'db> { iterator .try_call_dunder( db, + env, "__anext__", CallArguments::none(), TypeContext::default(), ) - .map(|dunder_anext_outcome| dunder_anext_outcome.return_type(db).try_await(db)) + .map(|dunder_anext_outcome| { + dunder_anext_outcome.return_type(db, env).try_await(db, env) + }) }; return match self.try_call_dunder( db, + env, "__aiter__", CallArguments::none(), TypeContext::default(), ) { Ok(dunder_aiter_bindings) => { - let iterator = dunder_aiter_bindings.return_type(db); + let iterator = dunder_aiter_bindings.return_type(db, env); match try_call_dunder_anext_on_iterator(iterator) { Ok(Ok(result)) => Ok(Cow::Owned(TupleSpec::homogeneous(result))), Ok(Err(AwaitError::InvalidReturnType(..))) => { @@ -299,7 +330,7 @@ impl<'db> Type<'db> { bindings: dunder_aiter_bindings, .. }) => { - let iterator = dunder_aiter_bindings.return_type(db); + let iterator = dunder_aiter_bindings.return_type(db, env); match try_call_dunder_anext_on_iterator(iterator) { Ok(_) => Err(IterationError::IterCallError { kind: CallErrorKind::PossiblyNotCallable, @@ -326,39 +357,42 @@ impl<'db> Type<'db> { }; } - if let Some(special_case) = non_async_special_case(db, self) { + if let Some(special_case) = non_async_special_case(db, env, self) { return Ok(special_case); } let try_call_dunder_getitem = || { self.try_call_dunder( db, + env, "__getitem__", - CallArguments::positional([KnownClass::Int.to_instance(db)]), + CallArguments::positional([KnownClass::Int.to_instance(db, env)]), TypeContext::default(), ) - .map(|dunder_getitem_outcome| dunder_getitem_outcome.return_type(db)) + .map(|dunder_getitem_outcome| dunder_getitem_outcome.return_type(db, env)) }; let try_call_dunder_next_on_iterator = |iterator: Type<'db>| { iterator .try_call_dunder( db, + env, "__next__", CallArguments::none(), TypeContext::default(), ) - .map(|dunder_next_outcome| dunder_next_outcome.return_type(db)) + .map(|dunder_next_outcome| dunder_next_outcome.return_type(db, env)) }; let dunder_iter_result = self .try_call_dunder( db, + env, "__iter__", CallArguments::none(), TypeContext::default(), ) - .map(|dunder_iter_outcome| dunder_iter_outcome.return_type(db)); + .map(|dunder_iter_outcome| dunder_iter_outcome.return_type(db, env)); match dunder_iter_result { Ok(iterator) => { @@ -380,7 +414,7 @@ impl<'db> Type<'db> { bindings: dunder_iter_outcome, unbound_on: unbound_on_iter, }) => { - let iterator = dunder_iter_outcome.return_type(db); + let iterator = dunder_iter_outcome.return_type(db, env); match try_call_dunder_next_on_iterator(iterator) { Ok(dunder_next_return) => { @@ -395,6 +429,7 @@ impl<'db> Type<'db> { // No diagnostic is emitted; iteration will always succeed! Cow::Owned(TupleSpec::homogeneous(UnionType::from_two_elements( db, + env, dunder_next_return, dunder_getitem_return_type, ))) @@ -488,24 +523,28 @@ pub(super) enum IterationError<'db> { } impl<'db> IterationError<'db> { - pub(super) fn fallback_element_type(&self, db: &'db dyn Db) -> Type<'db> { - self.element_type(db).unwrap_or(Type::unknown()) + pub(super) fn fallback_element_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.element_type(db, env).unwrap_or(Type::unknown()) } /// Returns the element type if it is known, or `None` if the type is never iterable. - fn element_type(&self, db: &'db dyn Db) -> Option> { + fn element_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { let return_type = |result: Result, CallDunderError<'db>>| { result - .map(|outcome| Some(outcome.return_type(db))) - .unwrap_or_else(|call_error| call_error.return_type(db)) + .map(|outcome| Some(outcome.return_type(db, env))) + .unwrap_or_else(|call_error| call_error.return_type(db, env)) }; match self { Self::IterReturnsInvalidIterator { dunder_error, mode, .. - } => dunder_error.return_type(db).and_then(|ty| { + } => dunder_error.return_type(db, env).and_then(|ty| { if mode.is_async() { - ty.try_await(db).ok() + ty.try_await(db, env).ok() } else { Some(ty) } @@ -517,16 +556,18 @@ impl<'db> IterationError<'db> { mode, } => { if mode.is_async() { - return_type(dunder_iter_bindings.return_type(db).try_call_dunder( + return_type(dunder_iter_bindings.return_type(db, env).try_call_dunder( db, + env, "__anext__", CallArguments::none(), TypeContext::default(), )) - .and_then(|ty| ty.try_await(db).ok()) + .and_then(|ty| ty.try_await(db, env).ok()) } else { - return_type(dunder_iter_bindings.return_type(db).try_call_dunder( + return_type(dunder_iter_bindings.return_type(db, env).try_call_dunder( db, + env, "__next__", CallArguments::none(), TypeContext::default(), @@ -545,16 +586,18 @@ impl<'db> IterationError<'db> { .. } => Some(UnionType::from_two_elements( db, + env, *dunder_next_return, - dunder_getitem_outcome.return_type(db), + dunder_getitem_outcome.return_type(db, env), )), CallDunderError::CallError(CallErrorKind::NotCallable, _, _) => { Some(*dunder_next_return) } CallDunderError::CallError(_, dunder_getitem_bindings, _) => { - let dunder_getitem_return = dunder_getitem_bindings.return_type(db); + let dunder_getitem_return = dunder_getitem_bindings.return_type(db, env); Some(UnionType::from_two_elements( db, + env, *dunder_next_return, dunder_getitem_return, )) @@ -563,7 +606,7 @@ impl<'db> IterationError<'db> { Self::UnboundIterAndGetitemError { dunder_getitem_error, - } => dunder_getitem_error.return_type(db), + } => dunder_getitem_error.return_type(db, env), Self::UnboundAiterError => None, } @@ -595,14 +638,15 @@ impl<'db> IterationError<'db> { /// A little helper type for emitting a diagnostic /// based on the variant of iteration error. - struct Reporter<'a> { + struct Reporter<'env, 'a> { db: &'a dyn Db, + env: &'env ProgramEnvironment<'a>, builder: LintDiagnosticGuardBuilder<'a, 'a>, iterable_type: Type<'a>, mode: EvaluationMode, } - impl<'a> Reporter<'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. @@ -612,22 +656,23 @@ impl<'db> IterationError<'db> { because: impl std::fmt::Display, error_context: ErrorContext, ) -> LintDiagnosticGuard<'a, 'a> { + let db = self.db; let mut diag = self.builder.into_diagnostic(format_args!( "Object of type `{iterable_type}` is not {maybe_async}iterable", - iterable_type = self.iterable_type.display(self.db), + iterable_type = self.iterable_type.display(db, self.env), maybe_async = if self.mode.is_async() { "async-" } else { "" } )); diag.info(because); if let ErrorContext::Enabled = error_context { let target = if self.mode.is_async() { - KnownClass::TyExtensionsAsyncIterable.to_instance_unknown(self.db) + KnownClass::TyExtensionsAsyncIterable.to_instance_unknown(db, self.env) } else { - KnownClass::TyExtensionsIterable.to_instance_unknown(self.db) + KnownClass::TyExtensionsIterable.to_instance_unknown(db, self.env) }; self.iterable_type - .assignability_error_context(self.db, target) - .attach_to(self.db, &mut diag); + .assignability_error_context(db, self.env, target) + .attach_to(db, self.env, &mut diag); } diag @@ -641,35 +686,38 @@ impl<'db> IterationError<'db> { because: impl std::fmt::Display, error_context: ErrorContext, ) -> LintDiagnosticGuard<'a, 'a> { + let db = self.db; let mut diag = self.builder.into_diagnostic(format_args!( "Object of type `{iterable_type}` may not be {maybe_async}iterable", - iterable_type = self.iterable_type.display(self.db), + iterable_type = self.iterable_type.display(db, self.env), maybe_async = if self.mode.is_async() { "async-" } else { "" } )); diag.info(because); if let ErrorContext::Enabled = error_context { let target = if self.mode.is_async() { - KnownClass::TyExtensionsAsyncIterable.to_instance_unknown(self.db) + KnownClass::TyExtensionsAsyncIterable.to_instance_unknown(db, self.env) } else { - KnownClass::TyExtensionsIterable.to_instance_unknown(self.db) + KnownClass::TyExtensionsIterable.to_instance_unknown(db, self.env) }; self.iterable_type - .assignability_error_context(self.db, target) - .attach_to(self.db, &mut diag); + .assignability_error_context(db, self.env, target) + .attach_to(db, self.env, &mut diag); } diag } } + let db = context.db(); let Some(builder) = context.report_lint(&NOT_ITERABLE, iterable_node) else { return; }; - let db = context.db(); + let env = context.program_environment(); let mode = self.mode(); let reporter = Reporter { db, + env, builder, iterable_type, mode, @@ -694,7 +742,7 @@ impl<'db> IterationError<'db> { CallErrorKind::NotCallable => { reporter.is_not(format_args!( "Its `{method}` attribute has type `{dunder_iter_type}`, which is not callable", - dunder_iter_type = bindings.callable_type().display(db), + dunder_iter_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled); } CallErrorKind::PossiblyNotCallable => { @@ -702,7 +750,7 @@ impl<'db> IterationError<'db> { format_args!( "Its `{method}` attribute (with type `{dunder_iter_type}`) \ may not be callable", - dunder_iter_type = bindings.callable_type().display(db), + dunder_iter_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled, ); @@ -722,7 +770,7 @@ impl<'db> IterationError<'db> { ); diag.info(format_args!( "Type of `{method}` is `{dunder_iter_type}`", - dunder_iter_type = bindings.callable_type().display(db), + dunder_iter_type = bindings.callable_type().display(db, env), )); diag.info(format_args!( "Expected signature for `{method}` is `def {method}(self): ...`", @@ -752,28 +800,28 @@ impl<'db> IterationError<'db> { reporter.is_not(format_args!( "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ which has no `{dunder_next_name}` method", - iterator_type = iterator.display(db), + iterator_type = iterator.display(db, env), ), ErrorContext::Disabled); } CallDunderError::PossiblyUnbound { .. } => { reporter.may_not(format_args!( "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ which may not have a `{dunder_next_name}` method", - iterator_type = iterator.display(db), + iterator_type = iterator.display(db, env), ), ErrorContext::Enabled); } CallDunderError::CallError(CallErrorKind::NotCallable, _, _) => { reporter.is_not(format_args!( "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ which has a `{dunder_next_name}` attribute that is not callable", - iterator_type = iterator.display(db), + iterator_type = iterator.display(db, env), ), ErrorContext::Disabled); } CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _, _) => { reporter.may_not(format_args!( "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ which has a `{dunder_next_name}` attribute that may not be callable", - iterator_type = iterator.display(db), + iterator_type = iterator.display(db, env), ), ErrorContext::Enabled); } CallDunderError::CallError(CallErrorKind::BindingError, bindings, _) @@ -783,7 +831,7 @@ impl<'db> IterationError<'db> { .is_not(format_args!( "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ which has an invalid `{dunder_next_name}` method", - iterator_type = iterator.display(db), + iterator_type = iterator.display(db, env), ), ErrorContext::Enabled) .info(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`")); } @@ -792,7 +840,7 @@ impl<'db> IterationError<'db> { .may_not(format_args!( "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ which may have an invalid `{dunder_next_name}` method", - iterator_type = iterator.display(db), + iterator_type = iterator.display(db, env), ), ErrorContext::Enabled) .info(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`")); } @@ -820,7 +868,7 @@ impl<'db> IterationError<'db> { "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), + dunder_getitem_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled, ), @@ -839,7 +887,7 @@ impl<'db> IterationError<'db> { "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), + dunder_getitem_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled, ) @@ -866,7 +914,7 @@ impl<'db> IterationError<'db> { "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), + dunder_getitem_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled, ); @@ -882,7 +930,7 @@ impl<'db> IterationError<'db> { for ty in unbound_on.iter().copied() { diag.info(format_args!( "`{}` does not implement `__iter__`", - ty.display(db) + ty.display(db, env) )); } } @@ -909,7 +957,7 @@ impl<'db> IterationError<'db> { "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), + dunder_getitem_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled, ); @@ -929,7 +977,7 @@ impl<'db> IterationError<'db> { ErrorContext::Disabled, ).info(format_args!( "`__getitem__` has type `{dunder_getitem_type}`, which is not callable", - dunder_getitem_type = bindings.callable_type().display(db), + dunder_getitem_type = bindings.callable_type().display(db, env), )); } CallDunderError::CallError(CallErrorKind::BindingError, bindings, _) @@ -955,7 +1003,7 @@ impl<'db> IterationError<'db> { "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), + dunder_getitem_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled, ) diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs index e5894eedc7..2d52894179 100644 --- a/crates/ty_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use itertools::Either; use ruff_python_ast::name::Name; @@ -231,10 +232,15 @@ pub(super) fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Size } impl<'db> VarianceInferable<'db> for KnownInstanceType<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { match self { KnownInstanceType::TypeAliasType(type_alias) => { - type_alias.raw_value_type(db).variance_of(db, typevar) + type_alias.raw_value_type(db).variance_of(db, env, typevar) } _ => TypeVarVariance::Bivariant, } @@ -245,6 +251,7 @@ impl<'db> KnownInstanceType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -259,42 +266,42 @@ impl<'db> KnownInstanceType<'db> { Self::TypeVar(typevar) => Some(Self::TypeVar(typevar)), Self::TypeAliasType(type_alias) => Some(Self::TypeAliasType(type_alias)), Self::Field(field) => field - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Self::Field), Self::UnionType(union_type) => union_type - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Self::UnionType), Self::Literal(ty) => ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::Literal), Self::Annotated(ty) => ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::Annotated), Self::TypeGenericAlias(ty) => ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::TypeGenericAlias), Self::LiteralStringAlias(ty) => ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::LiteralStringAlias), Self::Callable(callable) => callable - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Self::Callable), Self::NewType(newtype) => newtype - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::NewType), Self::Sentinel(sentinel) => Some(Self::Sentinel(sentinel)), Self::GenericContext(generic) => Some(Self::GenericContext(generic)), Self::Specialization(specialization) => specialization - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::Specialization), Self::NamedTupleSpec(spec) => spec - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .map(Self::NamedTupleSpec), Self::FunctoolsPartial(partial) => partial - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Self::FunctoolsPartial), Self::FunctoolsPartialCall(partial) => partial - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(Self::FunctoolsPartialCall), } } @@ -334,31 +341,39 @@ impl<'db> KnownInstanceType<'db> { } } - pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { - self.class(db).to_class_literal(db) + pub(super) fn to_meta_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + self.class(db).to_class_literal(db, env) } /// Return the instance type which this type is a subtype of. /// /// For example, an alias created using the `type` statement is an instance of - /// `typing.TypeAliasType`, so `KnownInstanceType::TypeAliasType(_).instance_fallback(db)` + /// `typing.TypeAliasType`, so `KnownInstanceType::TypeAliasType(_).instance_fallback(db, python_version)` /// returns `Type::NominalInstance(NominalInstanceType { class: })`. - pub(super) fn instance_fallback(self, db: &'db dyn Db) -> Type<'db> { - self.class(db).to_instance(db) + pub(super) fn instance_fallback( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.class(db).to_instance(db, env) } /// Return the type denoted by this retained runtime type-expression object. /// /// This is the scope-independent subset of `Type::in_type_expression` used when a value /// reaches a `TypeForm` position after it has already been inferred in value context. - pub(crate) fn type_form_argument(self, db: &'db dyn Db) -> Option> { + pub(crate) fn type_form_argument( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { Self::TypeAliasType(alias) => Some(Type::TypeAlias(alias)), Self::UnionType(instance) => instance.union_type(db).as_ref().ok().copied(), Self::Literal(ty) | Self::Annotated(ty) | Self::LiteralStringAlias(ty) => { Some(ty.inner(db)) } - Self::TypeGenericAlias(instance) => Some(instance.inner(db).to_meta_type(db)), + Self::TypeGenericAlias(instance) => Some(instance.inner(db).to_meta_type(db, env)), Self::Callable(callable) => Some(Type::Callable(callable)), Self::NewType(newtype) => Some(Type::NewTypeInstance(newtype)), Self::Sentinel(sentinel) => { @@ -385,13 +400,22 @@ impl<'db> KnownInstanceType<'db> { } /// Return `true` if this symbol is an instance of `class`. - pub(super) fn is_instance_of(self, db: &dyn Db, class: ClassType) -> bool { - self.class(db).is_subclass_of(db, class) + pub(super) fn is_instance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType, + ) -> bool { + self.class(db).is_subclass_of(db, env, class) } /// Return the repr of the symbol at runtime - pub(super) fn repr(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { - self.display_with(db, DisplaySettings::default()) + pub(super) fn repr<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> impl std::fmt::Display + 'env { + self.display_with(db, env, DisplaySettings::default()) } pub(super) fn apply_type_mapping_impl( @@ -399,7 +423,7 @@ impl<'db> KnownInstanceType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Type<'db> { match self { KnownInstanceType::TypeVar(typevar) => match type_mapping { @@ -447,7 +471,7 @@ impl<'db> KnownInstanceType<'db> { } KnownInstanceType::Range { .. } => match type_mapping { TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular) => { - self.instance_fallback(db) + self.instance_fallback(db, visitor.env) } _ => Type::KnownInstance(self), }, @@ -559,29 +583,32 @@ impl<'db> FieldInstance<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let default_type = match self.default_type(db) { - Some(default) if nested => Some(default.recursive_type_normalized_impl(db, div, true)?), + Some(default) if nested => { + Some(default.recursive_type_normalized_impl(db, env, div, true)?) + } Some(default) => Some( default - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, }; let converter = match self.converter(db) { Some((input_ty, output_ty)) if nested => Some(( - input_ty.recursive_type_normalized_impl(db, div, true)?, - output_ty.recursive_type_normalized_impl(db, div, true)?, + input_ty.recursive_type_normalized_impl(db, env, div, true)?, + output_ty.recursive_type_normalized_impl(db, env, div, true)?, )), Some((input_ty, output_ty)) => Some(( input_ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), output_ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), )), None => None, @@ -633,9 +660,11 @@ impl<'db> UnionTypeInstance<'db> { typevar_binding_context: Option>, inference_flags: InferenceFlags, ) -> Type<'db> { - let mut builder = UnionBuilder::new(db); + let env = ProgramEnvironment::from_scope(scope_id); + let mut builder = UnionBuilder::new(db, &env); for ty in &value_expr_types { - match ty.in_type_expression(db, scope_id, typevar_binding_context, inference_flags) { + match ty.in_type_expression_impl(db, scope_id, typevar_binding_context, inference_flags) + { Ok(ty) => builder.add_in_place(ty), Err(error) => { return Type::KnownInstance(KnownInstanceType::UnionType( @@ -671,7 +700,7 @@ impl<'db> UnionTypeInstance<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { if let Ok(union_type) = self.union_type(db) { UnionTypeInstance::new( @@ -693,12 +722,14 @@ impl<'db> UnionTypeInstance<'db> { pub(crate) fn value_expression_types( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> Result> + 'db, InvalidTypeExpressionError<'db>> { - let to_class_literal = |ty: Type<'db>| { + let env = env.clone(); + let to_class_literal = move |ty: Type<'db>| { ty.as_nominal_instance() .and_then(|instance| { instance - .class(db) + .class(db, &env) .static_class_literal(db) .map(|(lit, _)| Type::ClassLiteral(lit.into())) }) @@ -722,6 +753,7 @@ impl<'db> UnionTypeInstance<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -729,23 +761,23 @@ impl<'db> UnionTypeInstance<'db> { // See `UnionType::recursive_type_normalized_impl` for details. let value_expr_types = match self._value_expr_types(db).as_ref() { Some([first, second]) if nested => Some([ - first.recursive_type_normalized_impl(db, div, nested)?, - second.recursive_type_normalized_impl(db, div, nested)?, + first.recursive_type_normalized_impl(db, env, div, nested)?, + second.recursive_type_normalized_impl(db, env, div, nested)?, ]), Some([first, second]) => Some([ first - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div), second - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div), ]), None => None, }; let union_type = match self.union_type(db).clone() { - Ok(ty) if nested => Ok(ty.recursive_type_normalized_impl(db, div, nested)?), + Ok(ty) if nested => Ok(ty.recursive_type_normalized_impl(db, env, div, nested)?), Ok(ty) => Ok(ty - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div)), Err(err) => Err(err), }; @@ -759,6 +791,7 @@ impl<'db> FunctoolsPartialInstance<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -768,10 +801,10 @@ impl<'db> FunctoolsPartialInstance<'db> { db, self.wrapped(db) .inner(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, ), self.partial(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, )) } @@ -781,7 +814,7 @@ impl<'db> FunctoolsPartialInstance<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self::new( db, @@ -810,15 +843,16 @@ impl<'db> InternedType<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let inner = if nested { self.inner(db) - .recursive_type_normalized_impl(db, div, nested)? + .recursive_type_normalized_impl(db, env, div, nested)? } else { self.inner(db) - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div) }; Some(InternedType::new(db, inner)) diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index 021f11052d..617e20101f 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -17,8 +17,9 @@ use crate::{ place_from_declarations, }, types::{ - ClassBase, ClassLiteral, KnownClass, KnownInstanceType, StaticClassLiteral, - SubclassOfInner, Type, TypeVarBoundOrConstraints, class::CodeGeneratorKind, + ClassBase, ClassLiteral, KnownClass, KnownInstanceType, ProgramEnvironment, + StaticClassLiteral, SubclassOfInner, Type, TypeVarBoundOrConstraints, + class::CodeGeneratorKind, }, }; use ty_python_core::{ @@ -32,13 +33,16 @@ pub(crate) fn all_end_of_scope_members<'db>( db: &'db dyn Db, scope_id: ScopeId<'db>, ) -> impl Iterator> + 'db { + let env = ProgramEnvironment::from_scope(scope_id); + let use_def_map = use_def_map(db, scope_id); let table = place_table(db, scope_id); + let bindings_ctx = env.clone(); use_def_map .all_end_of_scope_symbol_declarations() .filter_map(move |(symbol_id, declarations)| { - let place_result = place_from_declarations(db, declarations); + let place_result = place_from_declarations(db, &env, declarations); let first_reachable_definition = place_result.first_declaration?; let ty = place_result .ignore_conflicting_declarations() @@ -59,7 +63,7 @@ pub(crate) fn all_end_of_scope_members<'db>( let PlaceWithDefinition { place, first_definition, - } = place_from_bindings(db, bindings); + } = place_from_bindings(db, &bindings_ctx, bindings); let first_reachable_definition = first_definition?; let ty = place.ignore_possibly_undefined()?; @@ -83,6 +87,8 @@ pub(crate) fn all_reachable_members<'db>( db: &'db dyn Db, scope_id: ScopeId<'db>, ) -> impl Iterator> + 'db { + let env = ProgramEnvironment::from_scope(scope_id); + let use_def_map = use_def_map(db, scope_id); let table = place_table(db, scope_id); @@ -91,7 +97,7 @@ pub(crate) fn all_reachable_members<'db>( .flat_map(move |(symbol_id, declarations, bindings)| { let symbol = table.symbol(symbol_id); - let declaration_place_result = place_from_declarations(db, declarations); + let declaration_place_result = place_from_declarations(db, &env, declarations); let declaration = declaration_place_result .first_declaration @@ -110,7 +116,7 @@ pub(crate) fn all_reachable_members<'db>( }) }); - let place_with_definition = place_from_bindings(db, bindings); + let place_with_definition = place_from_bindings(db, &env, bindings); let binding = place_with_definition .first_definition @@ -153,15 +159,15 @@ struct AllMembers<'db> { } impl<'db> AllMembers<'db> { - fn of(db: &'db dyn Db, ty: Type<'db>) -> Self { + fn of(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> Self { let mut all_members = Self { members: FxHashSet::default(), }; - all_members.extend_with_type(db, ty); + all_members.extend_with_type(db, env, ty); all_members } - fn extend_with_type(&mut self, db: &'db dyn Db, ty: Type<'db>) { + fn extend_with_type(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) { match ty { Type::Union(union) => { fn is_dynamic(db: &dyn Db, ty: Type<'_>) -> bool { @@ -179,13 +185,13 @@ impl<'db> AllMembers<'db> { let union = match union.filter(db, |&ty| !is_dynamic(db, ty)) { Type::Union(union) => union, - ty => return self.extend_with_type(db, ty), + ty => return self.extend_with_type(db, env, ty), }; self.members.extend( union .elements(db) .iter() - .map(|ty| AllMembers::of(db, *ty).members) + .map(|ty| AllMembers::of(db, env, *ty).members) .reduce(|acc, members| acc.intersection(&members).cloned().collect()) .unwrap_or_default(), ); @@ -195,84 +201,117 @@ impl<'db> AllMembers<'db> { intersection .positive(db) .iter() - .map(|ty| AllMembers::of(db, *ty).members) + .map(|ty| AllMembers::of(db, env, *ty).members) .reduce(|acc, members| acc.union(&members).cloned().collect()) .unwrap_or_default(), ), Type::EnumComplement(complement) => { - self.extend_with_type(db, complement.to_intersection(db)); + self.extend_with_type(db, env, complement.to_intersection(db, env)); } Type::NominalInstance(instance) => { - let class = instance.class(db); + let class = instance.class(db, env); if let Some((class_literal, _)) = class.static_class_literal(db) { - self.extend_with_instance_members(db, ty, class_literal); - self.extend_with_synthetic_members(db, ty, ClassLiteral::Static(class_literal)); + self.extend_with_instance_members(db, env, ty, class_literal); + self.extend_with_synthetic_members( + db, + env, + ty, + ClassLiteral::Static(class_literal), + ); } else { // For dynamic classes, we can't enumerate instance members (requires body scope), // but we can still add synthetic members for dataclass-like classes. - self.extend_with_synthetic_members(db, ty, class.class_literal(db)); + self.extend_with_synthetic_members(db, env, ty, class.class_literal(db)); } } Type::NewTypeInstance(newtype) => { - self.extend_with_type(db, newtype.concrete_base_type(db)); + self.extend_with_type(db, env, newtype.concrete_base_type(db)); } Type::ClassLiteral(class_literal) if class_literal.is_typed_dict(db) => { - self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); + self.extend_with_type( + db, + env, + KnownClass::TypedDictFallback.to_class_literal(db, env), + ); } Type::GenericAlias(generic_alias) if generic_alias.is_typed_dict(db) => { - self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); + self.extend_with_type( + db, + env, + KnownClass::TypedDictFallback.to_class_literal(db, env), + ); } - Type::SubclassOf(subclass_of_type) if subclass_of_type.is_typed_dict(db) => { - self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db)); + Type::SubclassOf(subclass_of_type) if subclass_of_type.is_typed_dict(db, env) => { + self.extend_with_type( + db, + env, + KnownClass::TypedDictFallback.to_class_literal(db, env), + ); } Type::ClassLiteral(class_literal) => { - self.extend_with_class_members(db, ty, class_literal); - self.extend_with_synthetic_members(db, ty, class_literal); - self.extend_with_metaclass_members(db, ty, class_literal.metaclass(db)); + self.extend_with_class_members(db, env, ty, class_literal); + self.extend_with_synthetic_members(db, env, ty, class_literal); + self.extend_with_metaclass_members(db, env, ty, class_literal.metaclass(db)); } Type::GenericAlias(generic_alias) => { let class_literal = generic_alias.origin(db); - self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal)); - self.extend_with_synthetic_members(db, ty, ClassLiteral::Static(class_literal)); - self.extend_with_metaclass_members(db, ty, class_literal.metaclass(db)); + self.extend_with_class_members(db, env, ty, ClassLiteral::Static(class_literal)); + self.extend_with_synthetic_members( + db, + env, + ty, + ClassLiteral::Static(class_literal), + ); + self.extend_with_metaclass_members(db, env, ty, class_literal.metaclass(db)); } Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { SubclassOfInner::Dynamic(_) => { - self.extend_with_type(db, KnownClass::Type.to_instance(db)); + self.extend_with_type(db, env, KnownClass::Type.to_instance(db, env)); } SubclassOfInner::Protocol(protocol) => { if let Some((class_literal, _)) = protocol .class_origin(db) .and_then(|origin| origin.static_class_literal(db)) { - self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal)); + self.extend_with_class_members( + db, + env, + ty, + ClassLiteral::Static(class_literal), + ); self.extend_with_synthetic_members( db, + env, ty, ClassLiteral::Static(class_literal), ); } // A structural implementation can use any metaclass, so only members of // `type` itself are guaranteed in addition to the protocol interface. - self.extend_with_type(db, KnownClass::Type.to_instance(db)); + self.extend_with_type(db, env, KnownClass::Type.to_instance(db, env)); } _ => { - if let Some(class_type) = subclass_of_type.subclass_of().into_class(db) + if let Some(class_type) = subclass_of_type.subclass_of().into_class(db, env) && let Some((class_literal, _)) = class_type.static_class_literal(db) { let static_class = ClassLiteral::Static(class_literal); - self.extend_with_class_members(db, ty, static_class); - self.extend_with_synthetic_members(db, ty, static_class); - self.extend_with_metaclass_members(db, ty, class_literal.metaclass(db)); + self.extend_with_class_members(db, env, ty, static_class); + self.extend_with_synthetic_members(db, env, ty, static_class); + self.extend_with_metaclass_members( + db, + env, + ty, + class_literal.metaclass(db), + ); } } }, @@ -283,25 +322,27 @@ impl<'db> AllMembers<'db> { | Type::AlwaysTruthy | Type::AlwaysFalsy | Type::TypeForm(_) => { - self.extend_with_type(db, Type::object()); + self.extend_with_type(db, env, Type::object()); } - Type::TypeAlias(alias) => self.extend_with_type(db, alias.value_type(db)), + Type::TypeAlias(alias) => { + self.extend_with_type(db, env, alias.value_type(db)); + } Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { None => { - self.extend_with_type(db, Type::object()); + self.extend_with_type(db, env, Type::object()); } Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - self.extend_with_type(db, bound); + self.extend_with_type(db, env, bound); } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { self.members.extend( constraints .elements(db) .iter() - .map(|ty| AllMembers::of(db, *ty).members) + .map(|ty| AllMembers::of(db, env, *ty).members) .reduce(|acc, members| { acc.intersection(&members).cloned().collect() }) @@ -325,65 +366,76 @@ impl<'db> AllMembers<'db> { | Type::KnownInstance(_) | Type::BoundSuper(_) | Type::TypeIs(_) - | Type::TypeGuard(_) => match ty.to_meta_type(db) { + | Type::TypeGuard(_) => match ty.to_meta_type(db, env) { Type::ClassLiteral(class_literal) => { - self.extend_with_class_members(db, ty, class_literal); + self.extend_with_class_members(db, env, ty, class_literal); } Type::SubclassOf(subclass_of) => { - if let Some(class) = subclass_of.subclass_of().into_class(db) + if let Some(class) = subclass_of.subclass_of().into_class(db, env) && let Some((class_literal, _)) = class.static_class_literal(db) { - self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal)); + self.extend_with_class_members( + db, + env, + ty, + ClassLiteral::Static(class_literal), + ); } } Type::GenericAlias(generic_alias) => { let class_literal = generic_alias.origin(db); - self.extend_with_class_members(db, ty, ClassLiteral::Static(class_literal)); + self.extend_with_class_members( + db, + env, + ty, + ClassLiteral::Static(class_literal), + ); } _ => {} }, Type::TypedDict(_) => { - if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) { - self.extend_with_class_members(db, ty, class_literal); + if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db, env) { + self.extend_with_class_members(db, env, ty, class_literal); } if let Type::ClassLiteral(ClassLiteral::Static(class)) = - KnownClass::TypedDictFallback.to_class_literal(db) + KnownClass::TypedDictFallback.to_class_literal(db, env) { - self.extend_with_instance_members(db, ty, class); + self.extend_with_instance_members(db, env, ty, class); } } Type::ModuleLiteral(literal) => { + let module = literal.module(db); // Looking up `__file__` on `types.ModuleType` will not give as precise a type // as we infer in type inference, but it's confusing if autocomplete etc. // shows a different type in the tooltip to the one inferred by the type checker. - let dunder_file_type = if literal.module(db).file(db).is_some() { - KnownClass::Str.to_instance(db) + let dunder_file_type = if module.file(db).is_some() { + KnownClass::Str.to_instance(db, env) } else { - Type::none(db) + Type::none(db, env) }; self.members.insert(Member { name: Name::new_static("__file__"), ty: dunder_file_type, }); - self.extend_with_type(db, KnownClass::ModuleType.to_instance(db)); - let module = literal.module(db); + self.extend_with_type(db, env, KnownClass::ModuleType.to_instance(db, env)); - let Some(file) = module.file(db) else { + let Some(python_file) = module.python_file(db) else { return; }; + let file = python_file.file(db); - let module_scope = global_scope(db, file); + let module_scope = global_scope(db, python_file); let use_def_map = use_def_map(db, module_scope); let place_table = place_table(db, module_scope); for (symbol_id, _) in use_def_map.all_end_of_scope_symbol_declarations() { let symbol_name = place_table.symbol(symbol_id).name(); let Place::Defined(DefinedPlace { ty, .. }) = - imported_symbol(db, Some(file), symbol_name, None).place + imported_symbol(db, env, Some(python_file), symbol_name, None).place else { continue; }; @@ -464,6 +516,7 @@ impl<'db> AllMembers<'db> { fn extend_with_class_members( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, class_literal: ClassLiteral<'db>, ) { @@ -474,7 +527,7 @@ impl<'db> AllMembers<'db> { { let parent_scope = parent.body_scope(db); for memberdef in all_end_of_scope_members(db, parent_scope) { - let result = ty.member(db, memberdef.member.name.as_str()); + let result = ty.member(db, env, memberdef.member.name.as_str()); let Some(ty) = result.place.ignore_possibly_undefined() else { continue; }; @@ -493,6 +546,7 @@ impl<'db> AllMembers<'db> { fn extend_with_metaclass_members( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, metaclass: Type<'db>, ) { @@ -500,9 +554,9 @@ impl<'db> AllMembers<'db> { return; }; - self.extend_with_class_members(db, ty, metaclass.class_literal(db)); + self.extend_with_class_members(db, env, ty, metaclass.class_literal(db)); if let Some((metaclass, _)) = metaclass.static_class_literal(db) { - self.extend_with_instance_members(db, ty, metaclass); + self.extend_with_instance_members(db, env, ty, metaclass); } } @@ -510,18 +564,19 @@ impl<'db> AllMembers<'db> { fn extend_with_instance_members_for_class( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, class_literal: StaticClassLiteral<'db>, ) { let class_body_scope = class_literal.body_scope(db); - let file = class_body_scope.file(db); - let index = semantic_index(db, file); + let python_file = class_body_scope.python_file(db); + let index = semantic_index(db, python_file); for function_scope_id in attribute_scopes(db, class_body_scope) { for place_expr in index.place_table(function_scope_id).members() { let Some(name) = place_expr.as_instance_attribute() else { continue; }; - let result = ty.member(db, name); + let result = ty.member(db, env, name); let Some(ty) = result.place.ignore_possibly_undefined() else { continue; }; @@ -538,7 +593,7 @@ impl<'db> AllMembers<'db> { // member, e.g., `SomeClass.__delattr__` is not a bound // method, but `instance_of_SomeClass.__delattr__` is. for memberdef in all_end_of_scope_members(db, class_body_scope) { - let result = ty.member(db, memberdef.member.name.as_str()); + let result = ty.member(db, env, memberdef.member.name.as_str()); let Some(ty) = result.place.ignore_possibly_undefined() else { continue; }; @@ -553,6 +608,7 @@ impl<'db> AllMembers<'db> { fn extend_with_instance_members( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, class_literal: StaticClassLiteral<'db>, ) { @@ -561,7 +617,7 @@ impl<'db> AllMembers<'db> { .filter_map(ClassBase::into_class) { if let Some((class_literal, _)) = class.static_class_literal(db) { - self.extend_with_instance_members_for_class(db, ty, class_literal); + self.extend_with_instance_members_for_class(db, env, ty, class_literal); } } } @@ -569,15 +625,24 @@ impl<'db> AllMembers<'db> { fn extend_with_synthetic_members( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, class_literal: ClassLiteral<'db>, ) { match CodeGeneratorKind::from_class(db, class_literal) { Some(CodeGeneratorKind::NamedTuple) => { if ty.is_nominal_instance() { - self.extend_with_type(db, KnownClass::NamedTupleFallback.to_instance(db)); + self.extend_with_type( + db, + env, + KnownClass::NamedTupleFallback.to_instance(db, env), + ); } else { - self.extend_with_type(db, KnownClass::NamedTupleFallback.to_class_literal(db)); + self.extend_with_type( + db, + env, + KnownClass::NamedTupleFallback.to_class_literal(db, env), + ); } } Some(CodeGeneratorKind::TypedDict) => {} @@ -592,7 +657,7 @@ impl<'db> AllMembers<'db> { if let Place::Defined(DefinedPlace { ty: synthetic_member, .. - }) = ty.member(db, attr).place + }) = ty.member(db, env, attr).place { self.members.insert(Member { name: Name::from(*attr), @@ -660,6 +725,10 @@ impl<'db> PartialOrd for Member<'db> { /// List all members of a given type: anything that would be valid when accessed /// as an attribute on an object of the given type. -pub(crate) fn all_members<'db>(db: &'db dyn Db, ty: Type<'db>) -> FxHashSet> { - AllMembers::of(db, ty).members +pub(crate) fn all_members<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> FxHashSet> { + AllMembers::of(db, env, ty).members } diff --git a/crates/ty_python_semantic/src/types/literal.rs b/crates/ty_python_semantic/src/types/literal.rs index f6a68ac107..512dbdbfd9 100644 --- a/crates/ty_python_semantic/src/types/literal.rs +++ b/crates/ty_python_semantic/src/types/literal.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use bitflags::bitflags; use compact_str::CompactString; use ruff_python_ast::name::Name; @@ -247,15 +248,19 @@ impl<'db> LiteralValueType<'db> { matches!(self.kind(), LiteralValueTypeKind::Bytes(..)) } - pub(crate) fn fallback_instance(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn fallback_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self.kind() { LiteralValueTypeKind::String(_) | LiteralValueTypeKind::LiteralString => { - KnownClass::Str.to_instance(db) + KnownClass::Str.to_instance(db, env) } - LiteralValueTypeKind::Bool(_) => KnownClass::Bool.to_instance(db), - LiteralValueTypeKind::Int(_) => KnownClass::Int.to_instance(db), - LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes.to_instance(db), - LiteralValueTypeKind::Enum(literal) => literal.enum_class_instance(db), + LiteralValueTypeKind::Bool(_) => KnownClass::Bool.to_instance(db, env), + LiteralValueTypeKind::Int(_) => KnownClass::Int.to_instance(db, env), + LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes.to_instance(db, env), + LiteralValueTypeKind::Enum(literal) => literal.enum_class_instance(db, env), } } } @@ -402,8 +407,12 @@ impl<'db> EnumLiteralType<'db> { self.enum_class_literal(db).class_literal(db) } - pub(crate) fn enum_class_instance(self, db: &'db dyn Db) -> Type<'db> { - self.enum_class(db).to_non_generic_instance(db) + pub(crate) fn enum_class_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.enum_class(db).to_non_generic_instance(db, env) } pub(crate) fn definition(self, db: &'db dyn Db) -> Option> { diff --git a/crates/ty_python_semantic/src/types/match_pattern.rs b/crates/ty_python_semantic/src/types/match_pattern.rs index 1e8f74161d..0ee2c3b1fd 100644 --- a/crates/ty_python_semantic/src/types/match_pattern.rs +++ b/crates/ty_python_semantic/src/types/match_pattern.rs @@ -1,3 +1,5 @@ +use crate::Db; +use crate::ProgramEnvironment; use ruff_python_ast as ast; use ruff_python_ast::name::Name; use ty_python_core::Truthiness; @@ -6,7 +8,6 @@ use ty_python_core::predicate::{ SequencePatternPredicateKind, }; -use crate::Db; use crate::place::{DefinedPlace, Place}; use crate::types::callable::{CallableFunctionProvenance, CallableTypeKind}; use crate::types::equality::{ @@ -22,22 +23,34 @@ use crate::types::{ infer_same_file_expression_type, }; -pub(crate) fn singleton_pattern_type(db: &dyn Db, singleton: ast::Singleton) -> Type<'_> { +pub(crate) fn singleton_pattern_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + singleton: ast::Singleton, +) -> Type<'db> { let ty = match singleton { - ast::Singleton::None => Type::none(db), + ast::Singleton::None => Type::none(db, env), ast::Singleton::True => Type::bool_literal(true), ast::Singleton::False => Type::bool_literal(false), }; - debug_assert!(ty.is_singleton(db)); + debug_assert!(ty.is_singleton(db, env)); ty } -pub(crate) fn mapping_pattern_type(db: &dyn Db) -> Type<'_> { - KnownClass::Mapping.to_instance(db).top_materialization(db) +pub(crate) fn mapping_pattern_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, +) -> Type<'db> { + KnownClass::Mapping + .to_instance(db, env) + .top_materialization(db, env) } -pub(crate) fn callable_pattern_type(db: &dyn Db) -> Type<'_> { - Type::Callable(CallableType::unknown(db)).top_materialization(db) +pub(crate) fn callable_pattern_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, +) -> Type<'db> { + Type::Callable(CallableType::unknown(db)).top_materialization(db, env) } /// Return whether every runtime value represented by a `TypedDict` satisfies `class`. @@ -45,61 +58,81 @@ pub(crate) fn callable_pattern_type(db: &dyn Db) -> Type<'_> { /// `TypedDict` is not a nominal subtype of `dict` in the static type system, but every runtime /// value is a dictionary. A `TypedDict` therefore matches class patterns such as `dict()`, /// `Mapping()`, and `MutableMapping()`. -pub(crate) fn typed_dict_matches_class_pattern(db: &dyn Db, class: ClassLiteral<'_>) -> bool { - let Some(dict) = KnownClass::Dict.to_class_literal(db).as_class_literal() else { +pub(crate) fn typed_dict_matches_class_pattern<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassLiteral<'db>, +) -> bool { + let Some(dict) = KnownClass::Dict + .to_class_literal(db, env) + .as_class_literal() + else { return false; }; - Type::instance(db, dict.top_materialization(db)) - .is_subtype_of(db, Type::instance(db, class.top_materialization(db))) + Type::instance(db, env, dict.top_materialization(db)).is_subtype_of( + db, + env, + Type::instance(db, env, class.top_materialization(db)), + ) } /// Return whether every value in `ty` belongs to a `TypedDict` domain accepted by `predicate`. fn typed_dict_pattern_domain_satisfies<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, predicate: &impl Fn(TypedDictType<'db>) -> bool, ) -> bool { match ty.resolve_type_alias(db) { Type::TypedDict(typed_dict) => predicate(typed_dict), - Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db) { + Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - typed_dict_pattern_domain_satisfies(db, bound, predicate) + typed_dict_pattern_domain_satisfies(db, env, bound, predicate) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + constraints.elements(db).iter().all(|constraint| { + typed_dict_pattern_domain_satisfies(db, env, *constraint, predicate) + }) } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| typed_dict_pattern_domain_satisfies(db, *constraint, predicate)), None => false, }, Type::Union(union) => union .elements(db) .iter() - .all(|element| typed_dict_pattern_domain_satisfies(db, *element, predicate)), + .all(|element| typed_dict_pattern_domain_satisfies(db, env, *element, predicate)), Type::Intersection(intersection) => intersection .positive(db) .iter() - .any(|element| typed_dict_pattern_domain_satisfies(db, *element, predicate)), + .any(|element| typed_dict_pattern_domain_satisfies(db, env, *element, predicate)), _ => false, } } /// Return whether every value in `ty` is represented by a `TypedDict` schema at runtime. -fn is_typed_dict_pattern_domain(db: &dyn Db, ty: Type<'_>) -> bool { - typed_dict_pattern_domain_satisfies(db, ty, &|_| true) +fn is_typed_dict_pattern_domain(db: &dyn Db, env: &ProgramEnvironment<'_>, ty: Type<'_>) -> bool { + typed_dict_pattern_domain_satisfies(db, env, ty, &|_| true) } -pub(crate) fn sequence_pattern_type_builder(db: &dyn Db) -> IntersectionBuilder<'_> { - IntersectionBuilder::new(db) - .add_positive(KnownClass::Sequence.to_instance(db).top_materialization(db)) +pub(crate) fn sequence_pattern_type_builder<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, +) -> IntersectionBuilder<'db> { + IntersectionBuilder::new(db, env) + .add_positive( + KnownClass::Sequence + .to_instance(db, env) + .top_materialization(db, env), + ) // `str`, `bytes`, and `bytearray` are sequences, but Python sequence // patterns explicitly do not match them or their subclasses. - .add_negative(KnownClass::Str.to_instance(db)) - .add_negative(KnownClass::Bytes.to_instance(db)) - .add_negative(KnownClass::Bytearray.to_instance(db)) + .add_negative(KnownClass::Str.to_instance(db, env)) + .add_negative(KnownClass::Bytes.to_instance(db, env)) + .add_negative(KnownClass::Bytearray.to_instance(db, env)) } fn sequence_pattern_getitem_method<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, indexed_element_types: impl IntoIterator)>, fallback_return_type: Option>, ) -> CallableType<'db> { @@ -122,7 +155,7 @@ fn sequence_pattern_getitem_method<'db>( Parameters::standard([ self_parameter(), Parameter::positional_only(Some(Name::new_static("index"))) - .with_annotated_type(KnownClass::Int.to_instance(db)), + .with_annotated_type(KnownClass::Int.to_instance(db, env)), ]), fallback_return_type, ) @@ -151,16 +184,17 @@ fn sequence_pattern_getitem_method<'db>( /// and element types. pub(crate) fn exact_sequence_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, element_types: impl ExactSizeIterator>, ) -> Type<'db> { let Ok(length) = i64::try_from(element_types.len()) else { - return sequence_pattern_type_builder(db).build(); + return sequence_pattern_type_builder(db, env).build(); }; // `False == 0` and `True == 1`, so the protocol must accept both literals. let length_type = match length { - 0 => UnionType::from_two_elements(db, Type::int_literal(0), Type::bool_literal(false)), - 1 => UnionType::from_two_elements(db, Type::int_literal(1), Type::bool_literal(true)), + 0 => UnionType::from_two_elements(db, env, Type::int_literal(0), Type::bool_literal(false)), + 1 => UnionType::from_two_elements(db, env, Type::int_literal(1), Type::bool_literal(true)), _ => Type::int_literal(length), }; @@ -172,16 +206,17 @@ pub(crate) fn exact_sequence_pattern_type<'db>( let getitem_method = (element_types.len() > 0).then(|| { ( "__getitem__", - sequence_pattern_getitem_method(db, (0..length).zip(element_types), None), + sequence_pattern_getitem_method(db, env, (0..length).zip(element_types), None), ) }); let protocol = Type::protocol_with_methods( db, + env, std::iter::once(("__len__", len_method)).chain(getitem_method), ); - sequence_pattern_type_builder(db) + sequence_pattern_type_builder(db, env) .add_positive(protocol) .build() } @@ -192,25 +227,26 @@ pub(crate) fn exact_sequence_pattern_type<'db>( /// negative indices. Other integer indices retain the sequence's element type. pub(crate) fn starred_sequence_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, prefix_element_types: impl ExactSizeIterator>, suffix_element_types: impl ExactSizeIterator>, ) -> Type<'db> { if prefix_element_types.len() == 0 && suffix_element_types.len() == 0 { - return sequence_pattern_type_builder(db).build(); + return sequence_pattern_type_builder(db, env).build(); } let Ok(suffix_length) = i64::try_from(suffix_element_types.len()) else { - return sequence_pattern_type_builder(db).build(); + return sequence_pattern_type_builder(db, env).build(); }; let indexed_element_types = (0_i64..) .zip(prefix_element_types) .chain((-suffix_length..0).zip(suffix_element_types)); let getitem_method = - sequence_pattern_getitem_method(db, indexed_element_types, Some(Type::object())); - let protocol = Type::protocol_with_methods(db, [("__getitem__", getitem_method)]); + sequence_pattern_getitem_method(db, env, indexed_element_types, Some(Type::object())); + let protocol = Type::protocol_with_methods(db, env, [("__getitem__", getitem_method)]); - sequence_pattern_type_builder(db) + sequence_pattern_type_builder(db, env) .add_positive(protocol) .build() } @@ -231,14 +267,15 @@ pub(crate) fn starred_sequence_pattern_type<'db>( /// ``` fn class_pattern_is_exhaustive( db: &dyn Db, + env: &ProgramEnvironment<'_>, class: ClassLiteral<'_>, subject_ty: Type<'_>, kind: &ClassPatternPredicateKind<'_>, ) -> bool { - let class_instance_ty = Type::instance(db, class.top_materialization(db)); - let is_typed_dict_match = - is_typed_dict_pattern_domain(db, subject_ty) && typed_dict_matches_class_pattern(db, class); - if !is_typed_dict_match && !subject_ty.is_subtype_of(db, class_instance_ty) { + let class_instance_ty = Type::instance(db, env, class.top_materialization(db)); + let is_typed_dict_match = is_typed_dict_pattern_domain(db, env, subject_ty) + && typed_dict_matches_class_pattern(db, env, class); + if !is_typed_dict_match && !subject_ty.is_subtype_of(db, env, class_instance_ty) { return false; } @@ -247,21 +284,22 @@ fn class_pattern_is_exhaustive( } if !kind.keywords.iter().all(|keyword| { - member_pattern_is_exhaustive(db, subject_ty, keyword.attr.as_str(), &keyword.pattern) + member_pattern_is_exhaustive(db, env, subject_ty, keyword.attr.as_str(), &keyword.pattern) }) { return false; } - let positional_sources = class_pattern_positional_sources(db, class, kind.positional.len()); + let positional_sources = + class_pattern_positional_sources(db, env, class, kind.positional.len()); kind.positional .iter() .zip(positional_sources) .all(|(pattern, source)| match source { ClassPatternPositionalSource::MatchSelf => { - pattern_is_exhaustive_for_subject(db, pattern, subject_ty) + pattern_is_exhaustive_for_subject(db, env, pattern, subject_ty) } ClassPatternPositionalSource::Attribute(name) => { - member_pattern_is_exhaustive(db, subject_ty, name.as_str(), pattern) + member_pattern_is_exhaustive(db, env, subject_ty, name.as_str(), pattern) } ClassPatternPositionalSource::Unknown => false, }) @@ -302,8 +340,15 @@ pub(crate) enum ClassPatternPositionalSource { /// attributes, inferred assignments retain their literal binding type while an explicit annotation /// remains authoritative. `PossiblyUndefined` is distinct from `Undefined` because only a truly /// absent `__match_args__` enables match-self behavior. -fn class_match_args_type<'db>(db: &'db dyn Db, class: ClassLiteral<'db>) -> ClassMatchArgs<'db> { - match Type::ClassLiteral(class).member(db, "__match_args__").place { +fn class_match_args_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassLiteral<'db>, +) -> ClassMatchArgs<'db> { + match Type::ClassLiteral(class) + .member(db, env, "__match_args__") + .place + { Place::Defined( place @ DefinedPlace { ty, @@ -366,9 +411,10 @@ pub(crate) enum ClassPatternPositionalResult<'db> { /// Validate positional subpatterns against a statically known `__match_args__` type. pub(crate) fn class_pattern_positional_result<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class: ClassLiteral<'db>, ) -> Option> { - match class_match_args_type(db, class) { + match class_match_args_type(db, env, class) { ClassMatchArgs::Undefined if class_has_match_self_flag(db, class) => { Some(ClassPatternPositionalResult::Limit(1)) } @@ -390,7 +436,7 @@ pub(crate) fn class_pattern_positional_result<'db>( Some(ClassPatternPositionalResult::Limit(limit)) } else { match_args - .is_disjoint_from(db, Type::homogeneous_tuple(db, Type::unknown())) + .is_disjoint_from(db, env, Type::homogeneous_tuple(db, env, Type::unknown())) .then_some(ClassPatternPositionalResult::InvalidType(match_args)) } } @@ -420,10 +466,11 @@ pub(crate) fn class_pattern_positional_result<'db>( /// ``` pub(crate) fn class_pattern_positional_sources( db: &dyn Db, + env: &ProgramEnvironment<'_>, class: ClassLiteral<'_>, positional_count: usize, ) -> Vec { - let fixed = match class_match_args_type(db, class) { + let fixed = match class_match_args_type(db, env, class) { ClassMatchArgs::Undefined if class_has_match_self_flag(db, class) => { return (0..positional_count) .map(|index| { @@ -461,26 +508,29 @@ pub(crate) fn class_pattern_positional_sources( /// Return whether `name` is definitely bound and `pattern` consumes its entire static member type. fn member_pattern_is_exhaustive( db: &dyn Db, + env: &ProgramEnvironment<'_>, instance_ty: Type<'_>, name: &str, pattern: &PatternPredicateKind<'_>, ) -> bool { - let place = instance_ty.member(db, name).place; + let place = instance_ty.member(db, env, name).place; place.is_definitely_bound() && place .raw_type() - .is_some_and(|member_ty| pattern_is_exhaustive_for_subject(db, pattern, member_ty)) + .is_some_and(|member_ty| pattern_is_exhaustive_for_subject(db, env, pattern, member_ty)) } /// Return whether `pattern` is statically guaranteed to match every value in `subject_ty`. fn pattern_is_exhaustive_for_subject( db: &dyn Db, + env: &ProgramEnvironment<'_>, pattern: &PatternPredicateKind<'_>, subject_ty: Type<'_>, ) -> bool { subject_ty.is_subtype_of( db, - definite_match_pattern_type_for_subject(db, pattern, subject_ty), + env, + definite_match_pattern_type_for_subject(db, env, pattern, subject_ty), ) } @@ -491,10 +541,11 @@ fn pattern_is_exhaustive_for_subject( /// guarantee that a particular key is present. fn mapping_pattern_is_exhaustive( db: &dyn Db, + env: &ProgramEnvironment<'_>, kind: &MappingPatternPredicateKind<'_>, subject_ty: Type<'_>, ) -> bool { - typed_dict_pattern_domain_satisfies(db, subject_ty, &|typed_dict| { + typed_dict_pattern_domain_satisfies(db, env, subject_ty, &|typed_dict| { kind.entries.iter().all(|entry| { let key_ty = infer_same_file_expression_type(db, entry.key, TypeContext::default()); let Some(key) = key_ty.as_string_literal() else { @@ -502,7 +553,7 @@ fn mapping_pattern_is_exhaustive( }; typed_dict.item(db, key.value(db)).is_some_and(|field| { field.is_required() - && pattern_is_exhaustive_for_subject(db, &entry.pattern, field.declared_ty) + && pattern_is_exhaustive_for_subject(db, env, &entry.pattern, field.declared_ty) }) }) }) @@ -514,10 +565,11 @@ fn mapping_pattern_is_exhaustive( /// tuple element's actual static type. fn sequence_pattern_is_exhaustive_for_subject( db: &dyn Db, + env: &ProgramEnvironment<'_>, kind: &SequencePatternPredicateKind<'_>, subject_ty: Type<'_>, ) -> bool { - if !subject_ty.is_subtype_of(db, sequence_pattern_type_builder(db).build()) { + if !subject_ty.is_subtype_of(db, env, sequence_pattern_type_builder(db, env).build()) { return false; } @@ -539,7 +591,7 @@ fn sequence_pattern_is_exhaustive_for_subject( .iter() .zip(kind.patterns.iter()) .all(|(element, pattern)| { - pattern_is_exhaustive_for_subject(db, pattern, *element) + pattern_is_exhaustive_for_subject(db, env, pattern, *element) }); }; if elements.len() < prefix.len() + suffix.len() { @@ -550,7 +602,7 @@ fn sequence_pattern_is_exhaustive_for_subject( .iter() .zip(prefix) .chain(elements.iter().rev().zip(suffix.iter().rev())) - .all(|(element, pattern)| pattern_is_exhaustive_for_subject(db, pattern, *element)) + .all(|(element, pattern)| pattern_is_exhaustive_for_subject(db, env, pattern, *element)) } /// Return the values that are statically guaranteed to match `kind`, using `subject_ty` when the @@ -584,10 +636,12 @@ fn sequence_pattern_is_exhaustive_for_subject( /// ``` pub(crate) fn definite_match_pattern_type_for_subject<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { - if let Some(subject_independent_ty) = subject_independent_definite_match_pattern_type(db, kind) + if let Some(subject_independent_ty) = + subject_independent_definite_match_pattern_type(db, env, kind) { return subject_independent_ty; } @@ -596,10 +650,11 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( if let Type::Union(union) = resolved_subject_ty { return UnionType::from_elements( db, + env, union .elements(db) .iter() - .map(|element| definite_match_pattern_type_for_subject(db, kind, *element)), + .map(|element| definite_match_pattern_type_for_subject(db, env, kind, *element)), ); } @@ -608,6 +663,7 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( let value_ty = infer_same_file_expression_type(db, *value, TypeContext::default()); if equality_truthiness( db, + env, resolved_subject_ty, value_ty, ComparisonSoundnessPolicy::from_analysis_settings( @@ -622,9 +678,9 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( let class_ty = infer_same_file_expression_type(db, kind.class, TypeContext::default()); match class_ty { Type::ClassLiteral(class) => { - if class_pattern_is_exhaustive(db, class, resolved_subject_ty, kind) { - let top_subject_ty = resolved_subject_ty.top_materialization(db); - if !class_pattern_is_exhaustive(db, class, top_subject_ty, kind) { + if class_pattern_is_exhaustive(db, env, class, resolved_subject_ty, kind) { + let top_subject_ty = resolved_subject_ty.top_materialization(db, env); + if !class_pattern_is_exhaustive(db, env, class, top_subject_ty, kind) { return subject_ty; } return top_subject_ty; @@ -632,8 +688,8 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( } Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) if kind.is_empty() - && let callable_pattern_ty = callable_pattern_type(db) - && subject_ty.is_subtype_of(db, callable_pattern_ty) => + && let callable_pattern_ty = callable_pattern_type(db, env) + && subject_ty.is_subtype_of(db, env, callable_pattern_ty) => { return callable_pattern_ty; } @@ -641,25 +697,25 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( } } PatternPredicateKind::Sequence(kind) => { - if !sequence_pattern_is_exhaustive_for_subject(db, kind, resolved_subject_ty) { + if !sequence_pattern_is_exhaustive_for_subject(db, env, kind, resolved_subject_ty) { // A nested subject-dependent pattern rejected the context-free approximation. // Reusing that approximation for the surrounding sequence would reintroduce the // values that the recursive analysis deliberately excluded. return Type::Never; } - let top_subject_ty = resolved_subject_ty.top_materialization(db); - return if sequence_pattern_is_exhaustive_for_subject(db, kind, top_subject_ty) { + let top_subject_ty = resolved_subject_ty.top_materialization(db, env); + return if sequence_pattern_is_exhaustive_for_subject(db, env, kind, top_subject_ty) { top_subject_ty } else { subject_ty }; } PatternPredicateKind::Mapping(kind) => { - if !mapping_pattern_is_exhaustive(db, kind, resolved_subject_ty) { + if !mapping_pattern_is_exhaustive(db, env, kind, resolved_subject_ty) { return Type::Never; } - let top_subject_ty = resolved_subject_ty.top_materialization(db); - return if mapping_pattern_is_exhaustive(db, kind, top_subject_ty) { + let top_subject_ty = resolved_subject_ty.top_materialization(db, env); + return if mapping_pattern_is_exhaustive(db, env, kind, top_subject_ty) { top_subject_ty } else { subject_ty @@ -668,20 +724,21 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( PatternPredicateKind::Or(patterns) => { return UnionType::from_elements( db, + env, patterns.iter().map(|pattern| { - definite_match_pattern_type_for_subject(db, pattern, subject_ty) + definite_match_pattern_type_for_subject(db, env, pattern, subject_ty) }), ); } PatternPredicateKind::As(Some(pattern), _) => { - return definite_match_pattern_type_for_subject(db, pattern, subject_ty); + return definite_match_pattern_type_for_subject(db, env, pattern, subject_ty); } _ => return Type::Never, } - IntersectionBuilder::new(db) + IntersectionBuilder::new(db, env) .add_positive(subject_ty) - .add_positive(definite_match_pattern_type(db, kind)) + .add_positive(definite_match_pattern_type(db, env, kind)) .build() } @@ -702,6 +759,7 @@ pub(crate) fn definite_match_pattern_type_for_subject<'db>( /// ``` fn pattern_fallthrough_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { @@ -712,9 +770,10 @@ fn pattern_fallthrough_type<'db>( // matches. This includes narrowed intersections containing `Self` or another type variable // whose upper bound is that enum. if let Some(enum_literal) = value_ty.as_enum_literal() - && is_same_enum_pattern_domain(db, subject_ty, enum_literal) + && is_same_enum_pattern_domain(db, env, subject_ty, enum_literal) && equality_truthiness( db, + env, value_ty, value_ty, ComparisonSoundnessPolicy::from_analysis_settings( @@ -722,29 +781,30 @@ fn pattern_fallthrough_type<'db>( ), ) == Truthiness::AlwaysTrue { - return IntersectionBuilder::new(db) + return IntersectionBuilder::new(db, env) .add_positive(subject_ty) .add_negative(value_ty) .build(); } if let Some(constraint) = evaluate_type_equality( db, + env, subject_ty, value_ty, false, ComparisonSoundnessPolicy::from_analysis_settings(db.analysis_settings(value.file(db))), ) { - return IntersectionBuilder::new(db) + return IntersectionBuilder::new(db, env) .add_positive(subject_ty) .add_positive(constraint) .build(); } } - IntersectionBuilder::new(db) + IntersectionBuilder::new(db, env) .add_positive(subject_ty) .add_negative(definite_match_pattern_type_for_subject( - db, kind, subject_ty, + db, env, kind, subject_ty, )) .build() } @@ -770,12 +830,14 @@ fn pattern_fallthrough_type<'db>( /// ``` pub(crate) fn pattern_binding_fallthrough_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { let mut budget = ExactTuplePatternExpansionBudget::default(); - try_pattern_binding_fallthrough_type(db, kind, subject_ty, &mut budget) - .unwrap_or_else(|()| conservative_pattern_binding_fallthrough_type(db, kind, subject_ty)) + try_pattern_binding_fallthrough_type(db, env, kind, subject_ty, &mut budget).unwrap_or_else( + |()| conservative_pattern_binding_fallthrough_type(db, env, kind, subject_ty), + ) } /// Compute binding fallthrough while charging every nested exact-tuple expansion to `budget`. @@ -784,23 +846,24 @@ pub(crate) fn pattern_binding_fallthrough_type<'db>( /// complete pattern conservatively. fn try_pattern_binding_fallthrough_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, budget: &mut ExactTuplePatternExpansionBudget, ) -> Result, ()> { match kind { PatternPredicateKind::Sequence(sequence) => { - try_sequence_pattern_binding_fallthrough_type(db, sequence, subject_ty, budget) + try_sequence_pattern_binding_fallthrough_type(db, env, sequence, subject_ty, budget) } PatternPredicateKind::Or(patterns) => { patterns.iter().try_fold(subject_ty, |remaining, pattern| { - try_pattern_binding_fallthrough_type(db, pattern, remaining, budget) + try_pattern_binding_fallthrough_type(db, env, pattern, remaining, budget) }) } PatternPredicateKind::As(Some(pattern), _) => { - try_pattern_binding_fallthrough_type(db, pattern, subject_ty, budget) + try_pattern_binding_fallthrough_type(db, env, pattern, subject_ty, budget) } - _ => Ok(pattern_fallthrough_type(db, kind, subject_ty)), + _ => Ok(pattern_fallthrough_type(db, env, kind, subject_ty)), } } @@ -810,19 +873,20 @@ fn try_pattern_binding_fallthrough_type<'db>( /// used when the precise traversal exceeds its expansion budget. fn conservative_pattern_binding_fallthrough_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { match kind { PatternPredicateKind::Or(patterns) => { patterns.iter().fold(subject_ty, |remaining, pattern| { - conservative_pattern_binding_fallthrough_type(db, pattern, remaining) + conservative_pattern_binding_fallthrough_type(db, env, pattern, remaining) }) } PatternPredicateKind::As(Some(pattern), _) => { - conservative_pattern_binding_fallthrough_type(db, pattern, subject_ty) + conservative_pattern_binding_fallthrough_type(db, env, pattern, subject_ty) } - _ => pattern_fallthrough_type(db, kind, subject_ty), + _ => pattern_fallthrough_type(db, env, kind, subject_ty), } } @@ -832,6 +896,7 @@ fn conservative_pattern_binding_fallthrough_type<'db>( /// expansion cannot exceed the configured limits. fn try_sequence_pattern_binding_fallthrough_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &SequencePatternPredicateKind<'db>, subject_ty: Type<'db>, budget: &mut ExactTuplePatternExpansionBudget, @@ -839,14 +904,14 @@ fn try_sequence_pattern_binding_fallthrough_type<'db>( let resolved = subject_ty.resolve_type_alias(db); let narrowed = match resolved { Type::Union(union) => union - .try_map(db, |element| { - try_sequence_pattern_binding_fallthrough_type(db, kind, *element, budget).ok() + .try_map(db, env, |element| { + try_sequence_pattern_binding_fallthrough_type(db, env, kind, *element, budget).ok() }) .ok_or(())?, Type::Intersection(intersection) => { let mut failed = false; - let narrowed = intersection.map_positive(db, |element| { - try_sequence_pattern_binding_fallthrough_type(db, kind, *element, budget) + let narrowed = intersection.map_positive(db, env, |element| { + try_sequence_pattern_binding_fallthrough_type(db, env, kind, *element, budget) .unwrap_or_else(|()| { failed = true; *element @@ -858,18 +923,27 @@ fn try_sequence_pattern_binding_fallthrough_type<'db>( narrowed } Type::TypeVar(typevar) - if typevar.typevar(db).upper_bound(db).is_some_and(|bound| { - pattern_fallthrough_type(db, &PatternPredicateKind::Sequence(kind.clone()), bound) + if typevar + .typevar(db) + .upper_bound(db, env) + .is_some_and(|bound| { + pattern_fallthrough_type( + db, + env, + &PatternPredicateKind::Sequence(kind.clone()), + bound, + ) .is_never() - }) => + }) => { Type::Never } _ if resolved.exact_tuple_instance_spec(db).is_some() => { - exact_tuple_sequence_pattern_fallthrough_type(db, kind, resolved, budget)? + exact_tuple_sequence_pattern_fallthrough_type(db, env, kind, resolved, budget)? .unwrap_or_else(|| { pattern_fallthrough_type( db, + env, &PatternPredicateKind::Sequence(kind.clone()), resolved, ) @@ -877,9 +951,9 @@ fn try_sequence_pattern_binding_fallthrough_type<'db>( } // An irrefutable sequence pattern can only fail if the subject is not eligible for sequence // matching. Unlike length and indexed-element facts, eligibility is unaffected by mutation. - _ if kind.is_irrefutable() => IntersectionBuilder::new(db) + _ if kind.is_irrefutable() => IntersectionBuilder::new(db, env) .add_positive(resolved) - .add_negative(sequence_pattern_type_builder(db).build()) + .add_negative(sequence_pattern_type_builder(db, env).build()) .build(), _ => resolved, }; @@ -922,6 +996,7 @@ impl ExactTuplePatternExpansionBudget { /// representation used by the general fallthrough path. fn exact_tuple_sequence_pattern_fallthrough_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &SequencePatternPredicateKind<'db>, subject_ty: Type<'db>, budget: &mut ExactTuplePatternExpansionBudget, @@ -939,7 +1014,7 @@ fn exact_tuple_sequence_pattern_fallthrough_type<'db>( if tuple .all_elements() .iter() - .any(|element| any_over_type(db, *element, true, |ty| ty.is_dynamic())) + .any(|element| any_over_type(db, env, *element, true, |ty| ty.is_dynamic())) { return Ok(None); } @@ -952,7 +1027,7 @@ fn exact_tuple_sequence_pattern_fallthrough_type<'db>( .zip(kind.patterns.iter()) .enumerate() { - let remaining = try_pattern_binding_fallthrough_type(db, pattern, element, budget)?; + let remaining = try_pattern_binding_fallthrough_type(db, env, pattern, element, budget)?; if remaining == element { return Ok(Some(subject_ty)); } @@ -963,36 +1038,37 @@ fn exact_tuple_sequence_pattern_fallthrough_type<'db>( budget.add_alternative(tuple.len())?; let mut elements = tuple.all_elements().to_vec(); elements[index] = remaining; - alternatives.push(Type::heterogeneous_tuple(db, elements)); + alternatives.push(Type::heterogeneous_tuple(db, env, elements)); } - Ok(Some(UnionType::from_elements(db, alternatives))) + Ok(Some(UnionType::from_elements(db, env, alternatives))) } /// Return whether every possible value of `ty` belongs to the same enum as `right`, including /// bounded type variables nested inside unions or intersections. fn is_same_enum_pattern_domain<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, right: EnumLiteralType<'db>, ) -> bool { - if is_same_enum_domain(db, ty, right) { + if is_same_enum_domain(db, env, ty, right) { return true; } match ty.resolve_type_alias(db) { Type::TypeVar(typevar) => typevar .typevar(db) - .upper_bound(db) - .is_some_and(|bound| is_same_enum_domain(db, bound, right)), + .upper_bound(db, env) + .is_some_and(|bound| is_same_enum_domain(db, env, bound, right)), Type::Union(union) => union .elements(db) .iter() - .all(|element| is_same_enum_pattern_domain(db, *element, right)), + .all(|element| is_same_enum_pattern_domain(db, env, *element, right)), Type::Intersection(intersection) => intersection .positive(db) .iter() - .any(|element| is_same_enum_pattern_domain(db, *element, right)), + .any(|element| is_same_enum_pattern_domain(db, env, *element, right)), _ => false, } } @@ -1004,47 +1080,48 @@ fn is_same_enum_pattern_domain<'db>( /// the static subject type. fn subject_independent_definite_match_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, ) -> Option> { match kind { PatternPredicateKind::Class(kind) => { match infer_same_file_expression_type(db, kind.class, TypeContext::default()) { Type::ClassLiteral(class) if kind.is_empty() => { - let class_instance_ty = Type::instance(db, class.top_materialization(db)); + let class_instance_ty = Type::instance(db, env, class.top_materialization(db)); let typed_dict_adds_runtime_matches = - typed_dict_matches_class_pattern(db, class) - && !Type::object().is_subtype_of(db, class_instance_ty); + typed_dict_matches_class_pattern(db, env, class) + && !Type::object().is_subtype_of(db, env, class_instance_ty); (!typed_dict_adds_runtime_matches).then_some(class_instance_ty) } Type::ClassLiteral(_) => None, Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) if kind.is_empty() => { - Some(callable_pattern_type(db)) + Some(callable_pattern_type(db, env)) } _ => Some(Type::Never), } } PatternPredicateKind::Sequence(kind) => { - build_definite_sequence_pattern_type(db, kind, |pattern| { - subject_independent_definite_match_pattern_type(db, pattern) + build_definite_sequence_pattern_type(db, env, kind, |pattern| { + subject_independent_definite_match_pattern_type(db, env, pattern) }) } PatternPredicateKind::Mapping(kind) => { if kind.is_irrefutable() { - Some(mapping_pattern_type(db)) + Some(mapping_pattern_type(db, env)) } else { None } } PatternPredicateKind::Or(patterns) => patterns .iter() - .map(|pattern| subject_independent_definite_match_pattern_type(db, pattern)) + .map(|pattern| subject_independent_definite_match_pattern_type(db, env, pattern)) .collect::>>() - .map(|types| UnionType::from_elements(db, types)), + .map(|types| UnionType::from_elements(db, env, types)), PatternPredicateKind::As(Some(pattern), _) => { - subject_independent_definite_match_pattern_type(db, pattern) + subject_independent_definite_match_pattern_type(db, env, pattern) } PatternPredicateKind::Value(_) => None, - _ => Some(definite_match_pattern_type(db, kind)), + _ => Some(definite_match_pattern_type(db, env, kind)), } } @@ -1053,10 +1130,11 @@ fn subject_independent_definite_match_pattern_type<'db>( /// Reachability and negative narrowing can only subtract this under-approximation. pub(crate) fn definite_match_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &PatternPredicateKind<'db>, ) -> Type<'db> { match kind { - PatternPredicateKind::Singleton(singleton) => singleton_pattern_type(db, *singleton), + PatternPredicateKind::Singleton(singleton) => singleton_pattern_type(db, env, *singleton), PatternPredicateKind::Value(value) => { let ty = infer_same_file_expression_type(db, *value, TypeContext::default()); // Only return the type if it's guaranteed to match itself. @@ -1064,7 +1142,7 @@ pub(crate) fn definite_match_pattern_type<'db>( let policy = ComparisonSoundnessPolicy::from_analysis_settings( db.analysis_settings(value.file(db)), ); - if equality_truthiness(db, ty, ty, policy) == Truthiness::AlwaysTrue { + if equality_truthiness(db, env, ty, ty, policy) == Truthiness::AlwaysTrue { ty } else { Type::Never @@ -1073,31 +1151,32 @@ pub(crate) fn definite_match_pattern_type<'db>( PatternPredicateKind::Class(kind) => { match infer_same_file_expression_type(db, kind.class, TypeContext::default()) { Type::ClassLiteral(class) if kind.is_empty() => { - Type::instance(db, class.top_materialization(db)) + Type::instance(db, env, class.top_materialization(db)) } Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) if kind.is_empty() => { - callable_pattern_type(db) + callable_pattern_type(db, env) } _ => Type::Never, } } PatternPredicateKind::Mapping(kind) => { if kind.is_irrefutable() { - mapping_pattern_type(db) + mapping_pattern_type(db, env) } else { Type::Never } } - PatternPredicateKind::Sequence(kind) => definite_sequence_pattern_type(db, kind), + PatternPredicateKind::Sequence(kind) => definite_sequence_pattern_type(db, env, kind), PatternPredicateKind::Or(predicates) => UnionType::from_elements( db, + env, predicates .iter() - .map(|p| definite_match_pattern_type(db, p)), + .map(|p| definite_match_pattern_type(db, env, p)), ), PatternPredicateKind::As(pattern, _) => pattern .as_deref() - .map(|p| definite_match_pattern_type(db, p)) + .map(|p| definite_match_pattern_type(db, env, p)) .unwrap_or_else(Type::object), PatternPredicateKind::Star(_) => Type::object(), } @@ -1106,21 +1185,23 @@ pub(crate) fn definite_match_pattern_type<'db>( /// Return the values that are guaranteed to match a sequence pattern. fn definite_sequence_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &SequencePatternPredicateKind<'db>, ) -> Type<'db> { - build_definite_sequence_pattern_type(db, kind, |pattern| { - Some(definite_match_pattern_type(db, pattern)) + build_definite_sequence_pattern_type(db, env, kind, |pattern| { + Some(definite_match_pattern_type(db, env, pattern)) }) .unwrap_or(Type::Never) } fn build_definite_sequence_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &SequencePatternPredicateKind<'db>, mut element_type: impl FnMut(&PatternPredicateKind<'db>) -> Option>, ) -> Option> { if kind.is_irrefutable() { - return Some(sequence_pattern_type_builder(db).build()); + return Some(sequence_pattern_type_builder(db, env).build()); } if let Some((prefix, suffix)) = kind.split_around_star() { @@ -1134,6 +1215,7 @@ fn build_definite_sequence_pattern_type<'db>( .collect::>>()?; return Some(Type::tuple(TupleType::mixed( db, + env, prefix_types, Type::object(), suffix_types, @@ -1149,6 +1231,10 @@ fn build_definite_sequence_pattern_type<'db>( if element_types.iter().any(Type::is_never) { Some(Type::Never) } else { - Some(exact_sequence_pattern_type(db, element_types.into_iter())) + Some(exact_sequence_pattern_type( + db, + env, + element_types.into_iter(), + )) } } diff --git a/crates/ty_python_semantic/src/types/member.rs b/crates/ty_python_semantic/src/types/member.rs index 6f75d70b84..8f13081198 100644 --- a/crates/ty_python_semantic/src/types/member.rs +++ b/crates/ty_python_semantic/src/types/member.rs @@ -3,7 +3,7 @@ use crate::place::{ ConsideredDefinitions, DefinedPlace, Place, PlaceAndQualifiers, RequiresExplicitReExport, place_by_id, place_from_bindings, }; -use crate::types::Type; +use crate::types::{ProgramEnvironment, Type}; use ty_python_core::{place_table, scope::ScopeId, use_def_map}; /// The return type of certain member-lookup operations. Contains information @@ -85,7 +85,8 @@ pub(super) fn class_member<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str // Otherwise, we need to check if the symbol has bindings let use_def = use_def_map(db, scope); let bindings = use_def.end_of_scope_symbol_bindings(symbol_id); - let inferred = place_from_bindings(db, bindings).place; + let env = ProgramEnvironment::from_scope(scope); + let inferred = place_from_bindings(db, &env, bindings).place; // TODO: we should not need to calculate inferred type second time. This is a temporary // solution until the notion of Boundness and Declaredness is split. See #16036, #16264 diff --git a/crates/ty_python_semantic/src/types/method.rs b/crates/ty_python_semantic/src/types/method.rs index b6c5c28381..a622228b70 100644 --- a/crates/ty_python_semantic/src/types/method.rs +++ b/crates/ty_python_semantic/src/types/method.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use itertools::Either; use ruff_python_ast::name::Name; @@ -51,9 +52,12 @@ impl<'db> BoundMethodType<'db> { /// a `@classmethod`, then it should be an instance of that bound-instance type. pub(crate) fn typing_self_type(self, db: &'db dyn Db) -> Type<'db> { let mut self_instance = self.self_instance(db); - if self.function(db).is_classmethod(db) { + let function = self.function(db); + if function.is_classmethod(db) { + let env = + ProgramEnvironment::from_scope(function.literal(db).last_definition.body_scope(db)); self_instance = self_instance - .to_instance_approximation(db) + .to_instance_approximation(db, &env) .unwrap_or_else(Type::unknown); } self_instance @@ -74,7 +78,6 @@ impl<'db> BoundMethodType<'db> { )] pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { let function = self.function(db); - CallableType::new( db, self.bound_signatures(db), @@ -89,6 +92,7 @@ impl<'db> BoundMethodType<'db> { pub(crate) fn into_callable_type_with_receiver( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Type<'db>, typing_self_type: Type<'db>, ) -> CallableType<'db> { @@ -96,7 +100,7 @@ impl<'db> BoundMethodType<'db> { CallableType::new( db, - self.bound_signatures_with_receiver(db, receiver_type, typing_self_type), + self.bound_signatures_with_receiver(db, env, receiver_type, typing_self_type), CallableTypeKind::FunctionLike, CallableFunctionProvenance::from_function_return_annotation( function.has_explicit_return_annotation(db), @@ -106,15 +110,19 @@ impl<'db> BoundMethodType<'db> { #[salsa::tracked(returns(ref), cycle_initial=|_, _, _| CallableSignature::bottom(), heap_size=ruff_memory_usage::heap_size)] pub(crate) fn bound_signatures(self, db: &'db dyn Db) -> CallableSignature<'db> { + let function = self.function(db); + let env = + ProgramEnvironment::from_scope(function.literal(db).last_definition.body_scope(db)); let typing_self_type = self.typing_self_type(db); let receiver_type = self.self_instance(db); - self.bound_signatures_with_receiver(db, receiver_type, typing_self_type) + self.bound_signatures_with_receiver(db, &env, receiver_type, typing_self_type) } fn bound_signatures_with_receiver( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Type<'db>, typing_self_type: Type<'db>, ) -> CallableSignature<'db> { @@ -130,6 +138,7 @@ impl<'db> BoundMethodType<'db> { |signature| { signature.bind_self_with_receiver( db, + env, Some(receiver_type), Some(typing_self_type), ) @@ -139,13 +148,14 @@ impl<'db> BoundMethodType<'db> { return CallableSignature::from_overloads( function_signature.overloads.iter().filter_map(|signature| { - signature.bind_self_if_compatible(db, receiver_type, typing_self_type) + signature.bind_self_if_compatible(db, env, receiver_type, typing_self_type) }), ); }; CallableSignature::single(signature.bind_self_with_receiver( db, + env, Some(receiver_type), Some(typing_self_type), )) @@ -154,15 +164,16 @@ impl<'db> BoundMethodType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self::new( db, self.function(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, self.self_instance(db) - .recursive_type_normalized_impl(db, div, true)?, + .recursive_type_normalized_impl(db, env, div, true)?, )) } } @@ -267,33 +278,34 @@ impl<'db> KnownBoundMethodType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { KnownBoundMethodType::FunctionTypeDunderGet(function) => { Some(KnownBoundMethodType::FunctionTypeDunderGet( - function.recursive_type_normalized_impl(db, div, nested)?, + function.recursive_type_normalized_impl(db, env, div, nested)?, )) } KnownBoundMethodType::FunctionTypeDunderCall(function) => { Some(KnownBoundMethodType::FunctionTypeDunderCall( - function.recursive_type_normalized_impl(db, div, nested)?, + function.recursive_type_normalized_impl(db, env, div, nested)?, )) } KnownBoundMethodType::PropertyDunderGet(property) => { Some(KnownBoundMethodType::PropertyDunderGet( - property.recursive_type_normalized_impl(db, div, nested)?, + property.recursive_type_normalized_impl(db, env, div, nested)?, )) } KnownBoundMethodType::PropertyDunderSet(property) => { Some(KnownBoundMethodType::PropertyDunderSet( - property.recursive_type_normalized_impl(db, div, nested)?, + property.recursive_type_normalized_impl(db, env, div, nested)?, )) } KnownBoundMethodType::PropertyDunderDelete(property) => { Some(KnownBoundMethodType::PropertyDunderDelete( - property.recursive_type_normalized_impl(db, div, nested)?, + property.recursive_type_normalized_impl(db, env, div, nested)?, )) } KnownBoundMethodType::StrStartswith(_) @@ -339,7 +351,11 @@ impl<'db> KnownBoundMethodType<'db> { /// Return the signatures of this bound method type. /// /// If the bound method type is overloaded, it may have multiple signatures. - pub(super) fn signatures(self, db: &'db dyn Db) -> impl Iterator> { + pub(super) fn signatures( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> impl Iterator> { let object_type_form = || TypeFormType::from_type_expression(db, Type::object()); match self { @@ -368,9 +384,9 @@ impl<'db> KnownBoundMethodType<'db> { Signature::new( Parameters::standard([ Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::none(db)), + .with_annotated_type(Type::none(db, env)), Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(KnownClass::Type.to_instance(db)), + .with_annotated_type(KnownClass::Type.to_instance(db, env)), ]), Type::unknown(), ), @@ -381,10 +397,11 @@ impl<'db> KnownBoundMethodType<'db> { Parameter::positional_only(Some(Name::new_static("owner"))) .with_annotated_type(UnionType::from_two_elements( db, - KnownClass::Type.to_instance(db), - Type::none(db), + env, + KnownClass::Type.to_instance(db, env), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), ]), Type::unknown(), ), @@ -420,25 +437,32 @@ impl<'db> KnownBoundMethodType<'db> { Parameter::positional_only(Some(Name::new_static("prefix"))) .with_annotated_type(UnionType::from_two_elements( db, - KnownClass::Str.to_instance(db), - Type::homogeneous_tuple(db, KnownClass::Str.to_instance(db)), + env, + KnownClass::Str.to_instance(db, env), + Type::homogeneous_tuple( + db, + env, + KnownClass::Str.to_instance(db, env), + ), )), Parameter::positional_only(Some(Name::new_static("start"))) .with_annotated_type(UnionType::from_two_elements( db, - KnownClass::SupportsIndex.to_instance(db), - Type::none(db), + env, + KnownClass::SupportsIndex.to_instance(db, env), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), Parameter::positional_only(Some(Name::new_static("end"))) .with_annotated_type(UnionType::from_two_elements( db, - KnownClass::SupportsIndex.to_instance(db), - Type::none(db), + env, + KnownClass::SupportsIndex.to_instance(db, env), + Type::none(db, env), )) - .with_default_type(Type::none(db)), + .with_default_type(Type::none(db, env)), ]), - KnownClass::Bool.to_instance(db), + KnownClass::Bool.to_instance(db, env), ))) } @@ -452,7 +476,7 @@ impl<'db> KnownBoundMethodType<'db> { Parameter::positional_only(Some(Name::new_static("upper_bound"))) .with_annotated_type(object_type_form()), ]), - KnownClass::ConstraintSet.to_instance(db), + KnownClass::ConstraintSet.to_instance(db, env), ))) } @@ -460,7 +484,7 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::ConstraintSetNever => { Either::Right(std::iter::once(Signature::new( Parameters::empty(), - KnownClass::ConstraintSet.to_instance(db), + KnownClass::ConstraintSet.to_instance(db, env), ))) } @@ -472,7 +496,7 @@ impl<'db> KnownBoundMethodType<'db> { Parameter::positional_only(Some(Name::new_static("of"))) .with_annotated_type(object_type_form()), ]), - KnownClass::ConstraintSet.to_instance(db), + KnownClass::ConstraintSet.to_instance(db, env), ))) } @@ -481,8 +505,8 @@ impl<'db> KnownBoundMethodType<'db> { Parameters::standard([Parameter::positional_only(Some(Name::new_static( "other", ))) - .with_annotated_type(KnownClass::ConstraintSet.to_instance(db))]), - KnownClass::ConstraintSet.to_instance(db), + .with_annotated_type(KnownClass::ConstraintSet.to_instance(db, env))]), + KnownClass::ConstraintSet.to_instance(db, env), ))) } @@ -494,9 +518,9 @@ impl<'db> KnownBoundMethodType<'db> { ))) .with_annotated_type(TypeFormType::from_type_expression( db, - Type::homogeneous_tuple(db, Type::object()), + Type::homogeneous_tuple(db, env, Type::object()), ))]), - KnownClass::ConstraintSet.to_instance(db), + KnownClass::ConstraintSet.to_instance(db, env), ))) } @@ -505,14 +529,15 @@ impl<'db> KnownBoundMethodType<'db> { Parameters::standard([Parameter::keyword_only(Name::new_static("inferable")) .with_annotated_type(UnionType::from_two_elements( db, + env, TypeFormType::from_type_expression( db, - Type::homogeneous_tuple(db, Type::object()), + Type::homogeneous_tuple(db, env, Type::object()), ), - Type::none(db), + Type::none(db, env), )) - .with_default_type(Type::none(db))]), - KnownClass::Bool.to_instance(db), + .with_default_type(Type::none(db, env))]), + KnownClass::Bool.to_instance(db, env), ))) } @@ -524,17 +549,19 @@ impl<'db> KnownBoundMethodType<'db> { Parameter::keyword_only(Name::new_static("inferable")).with_annotated_type( TypeFormType::from_type_expression( db, - Type::homogeneous_tuple(db, Type::object()), + Type::homogeneous_tuple(db, env, Type::object()), ), ), ]), UnionType::from_two_elements( db, + env, Type::homogeneous_tuple( db, - KnownClass::ConstraintSetSolution.to_instance(db), + env, + KnownClass::ConstraintSetSolution.to_instance(db, env), ), - Type::none(db), + Type::none(db, env), ), ))) } @@ -544,15 +571,17 @@ impl<'db> KnownBoundMethodType<'db> { Parameters::standard([Parameter::keyword_only(Name::new_static("inferable")) .with_annotated_type(TypeFormType::from_type_expression( db, - Type::homogeneous_tuple(db, Type::object()), + Type::homogeneous_tuple(db, env, Type::object()), ))]), UnionType::from_two_elements( db, + env, Type::homogeneous_tuple( db, - KnownClass::ConstraintSetSolution.to_instance(db), + env, + KnownClass::ConstraintSetSolution.to_instance(db, env), ), - Type::none(db), + Type::none(db, env), ), ))) } @@ -560,7 +589,7 @@ impl<'db> KnownBoundMethodType<'db> { KnownBoundMethodType::ConstraintSetWithDetailedDisplay(_) => { Either::Right(std::iter::once(Signature::new( Parameters::empty(), - KnownClass::ConstraintSet.to_instance(db), + KnownClass::ConstraintSet.to_instance(db, env), ))) } } @@ -701,7 +730,11 @@ pub enum WrapperDescriptorKind { } impl WrapperDescriptorKind { - pub(super) fn signatures(self, db: &dyn Db) -> impl Iterator> { + pub(super) fn signatures<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> impl Iterator> { /// Similar to what we do in [`KnownBoundMethod::signatures`], /// here we also model `types.FunctionType.__get__` (or builtins.property.__get__), /// but now we consider a call to this as a function, i.e. we also expect the `self` @@ -710,10 +743,14 @@ impl WrapperDescriptorKind { /// TODO: Consider merging these synthesized signatures with the ones in /// [`KnownBoundMethod::signatures`], since that one is just this signature /// with the `self` parameters removed. - fn dunder_get_signatures(db: &dyn Db, class: KnownClass) -> [Signature<'_>; 2] { - let type_instance = KnownClass::Type.to_instance(db); - let none = Type::none(db); - let descriptor = class.to_instance(db); + fn dunder_get_signatures<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: KnownClass, + ) -> [Signature<'db>; 2] { + let type_instance = KnownClass::Type.to_instance(db, env); + let none = Type::none(db, env); + let descriptor = class.to_instance(db, env); [ Signature::new( Parameters::standard([ @@ -735,6 +772,7 @@ impl WrapperDescriptorKind { Parameter::positional_only(Some(Name::new_static("owner"))) .with_annotated_type(UnionType::from_two_elements( db, + env, type_instance, none, )) @@ -747,17 +785,17 @@ impl WrapperDescriptorKind { match self { WrapperDescriptorKind::FunctionTypeDunderGet => { - Either::Left(dunder_get_signatures(db, KnownClass::FunctionType).into_iter()) + Either::Left(dunder_get_signatures(db, env, KnownClass::FunctionType).into_iter()) } WrapperDescriptorKind::PropertyDunderGet => { - Either::Left(dunder_get_signatures(db, KnownClass::Property).into_iter()) + Either::Left(dunder_get_signatures(db, env, KnownClass::Property).into_iter()) } WrapperDescriptorKind::PropertyDunderSet => { let object = Type::object(); Either::Right(std::iter::once(Signature::new( Parameters::standard([ Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(KnownClass::Property.to_instance(db)), + .with_annotated_type(KnownClass::Property.to_instance(db, env)), Parameter::positional_only(Some(Name::new_static("instance"))) .with_annotated_type(object), Parameter::positional_only(Some(Name::new_static("value"))) @@ -770,7 +808,7 @@ impl WrapperDescriptorKind { Either::Right(std::iter::once(Signature::new( Parameters::standard([ Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(KnownClass::Property.to_instance(db)), + .with_annotated_type(KnownClass::Property.to_instance(db, env)), Parameter::positional_only(Some(Name::new_static("instance"))) .with_annotated_type(Type::object()), ]), diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 209b0ddd96..69d7fa00b9 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -1,10 +1,11 @@ +use crate::Db; +use crate::ProgramEnvironment; use std::collections::VecDeque; use std::ops::Deref; use indexmap::IndexMap; use rustc_hash::{FxBuildHasher, FxHashSet}; -use crate::Db; use crate::types::class::{DynamicClassLiteral, DynamicEnumLiteral}; use crate::types::class_base::ClassBase; use crate::types::generics::Specialization; @@ -87,6 +88,7 @@ impl<'db> Mro<'db> { resolved_bases.push(ClassBase::Generic); } + let env = &ProgramEnvironment::from_scope(class_literal.body_scope(db)); let class = class_literal.apply_optional_specialization(db, specialization); let original_bases = class_literal.explicit_bases(db); @@ -118,10 +120,13 @@ impl<'db> Mro<'db> { Ok(Self::from([ ClassBase::Class(class), ClassBase::Generic, - ClassBase::object(db), + ClassBase::object(db, env), ])) } else { - Ok(Self::from([ClassBase::Class(class), ClassBase::object(db)])) + Ok(Self::from([ + ClassBase::Class(class), + ClassBase::object(db, env), + ])) } } @@ -143,6 +148,7 @@ impl<'db> Mro<'db> { { ClassBase::try_from_explicit_base( db, + env, *single_base, Some(ClassLiteral::Static(class_literal)), ) @@ -158,12 +164,12 @@ impl<'db> Mro<'db> { Err(StaticMroErrorKind::InheritanceCycle) } else { Ok(std::iter::once(ClassBase::Class(class)) - .chain(single_base.mro(db, specialization)) + .chain(single_base.mro(db, env, specialization)) .collect()) } }, ) - .map_err(|err| err.into_mro_error(db, class)) + .map_err(|err| err.into_mro_error(db, env, class)) } // The class has multiple explicit bases. @@ -189,6 +195,7 @@ impl<'db> Mro<'db> { } else { match ClassBase::try_from_explicit_base( db, + env, *base, Some(ClassLiteral::Static(class_literal)), ) { @@ -201,7 +208,7 @@ impl<'db> Mro<'db> { if !invalid_bases.is_empty() { return Err( StaticMroErrorKind::InvalidBases(invalid_bases.into_boxed_slice()) - .into_mro_error(db, class), + .into_mro_error(db, env, class), ); } @@ -214,9 +221,11 @@ impl<'db> Mro<'db> { let mut seqs = vec![VecDeque::from([ClassBase::Class(class)])]; for base in &resolved_bases { if base.has_cyclic_mro(db) { - return Err(StaticMroErrorKind::InheritanceCycle.into_mro_error(db, class)); + return Err( + StaticMroErrorKind::InheritanceCycle.into_mro_error(db, env, class) + ); } - seqs.push(base.mro(db, specialization).collect()); + seqs.push(base.mro(db, env, specialization).collect()); } seqs.push( resolved_bases @@ -243,7 +252,7 @@ impl<'db> Mro<'db> { }) { return Err(StaticMroErrorKind::Pep695ClassWithGenericInheritance - .into_mro_error(db, class)); + .into_mro_error(db, env, class)); } let mut duplicate_dynamic_bases = false; @@ -264,6 +273,7 @@ impl<'db> Mro<'db> { for (index, base) in original_bases.iter().enumerate() { let Some(base) = ClassBase::try_from_explicit_base( db, + env, *base, Some(ClassLiteral::Static(class_literal)), ) else { @@ -306,33 +316,38 @@ impl<'db> Mro<'db> { if duplicate_bases.is_empty() { if duplicate_dynamic_bases { - Ok(Mro::from_error(db, class)) + Ok(Mro::from_error(db, env, class)) } else { Err(StaticMroErrorKind::UnresolvableMro { bases_list: original_bases.iter().copied().collect(), generic_index: check_generic_reorder_fixes_mro( db, + env, resolved_bases.as_slice(), original_bases, ), } - .into_mro_error(db, class)) + .into_mro_error(db, env, class)) } } else { Err( StaticMroErrorKind::DuplicateBases(duplicate_bases.into_boxed_slice()) - .into_mro_error(db, class), + .into_mro_error(db, env, class), ) } } } } - pub(super) fn from_error(db: &'db dyn Db, class: ClassType<'db>) -> Self { + pub(super) fn from_error( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType<'db>, + ) -> Self { Self::from([ ClassBase::Class(class), ClassBase::unknown(), - ClassBase::object(db), + ClassBase::object(db, env), ]) } @@ -343,6 +358,7 @@ impl<'db> Mro<'db> { db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>, ) -> Result> { + let env = &ProgramEnvironment::from_scope(dynamic.scope(db)); let original_bases = dynamic.explicit_bases(db); // Convert Types to ClassBases, tracking any that fail conversion. @@ -350,7 +366,7 @@ impl<'db> Mro<'db> { let mut invalid_bases = Vec::new(); for (i, base_type) in original_bases.iter().enumerate() { - match ClassBase::try_from_explicit_base(db, *base_type, None) { + match ClassBase::try_from_explicit_base(db, env, *base_type, None) { Some(class_base) => resolved_bases.push(class_base), None => invalid_bases.push((i, *base_type)), } @@ -360,7 +376,7 @@ impl<'db> Mro<'db> { if !invalid_bases.is_empty() { return Err( DynamicMroErrorKind::InvalidBases(invalid_bases.into_boxed_slice()) - .into_error(db, dynamic), + .into_error(db, env, dynamic), ); } @@ -373,16 +389,16 @@ impl<'db> Mro<'db> { // Handle empty bases case: MRO is just [self, object]. if resolved_bases.is_empty() { - return Ok(Self::from([self_base, ClassBase::object(db)])); + return Ok(Self::from([self_base, ClassBase::object(db, env)])); } // Build MRO sequences and check for inheritance cycles. let mut seqs = vec![VecDeque::from([self_base])]; for base in &resolved_bases { if base.has_cyclic_mro(db) { - return Err(DynamicMroErrorKind::InheritanceCycle.into_error(db, dynamic)); + return Err(DynamicMroErrorKind::InheritanceCycle.into_error(db, env, dynamic)); } - seqs.push(base.mro(db, None).collect()); + seqs.push(base.mro(db, env, None).collect()); } seqs.push(resolved_bases.iter().copied().collect()); @@ -412,15 +428,15 @@ impl<'db> Mro<'db> { if !duplicates.is_empty() { return Err( DynamicMroErrorKind::DuplicateBases(duplicates.into_boxed_slice()) - .into_error(db, dynamic), + .into_error(db, env, dynamic), ); } // No duplicate concrete bases. If there are dynamic bases, use fallback MRO. if has_dynamic_bases || has_duplicate_dynamic_bases { - Ok(Self::dynamic_fallback(db, dynamic)) + Ok(Self::dynamic_fallback(db, env, dynamic)) } else { - Err(DynamicMroErrorKind::UnresolvableMro.into_error(db, dynamic)) + Err(DynamicMroErrorKind::UnresolvableMro.into_error(db, env, dynamic)) } } @@ -434,6 +450,7 @@ impl<'db> Mro<'db> { db: &'db dyn Db, dynamic_enum: DynamicEnumLiteral<'db>, ) -> Result> { + let env = &ProgramEnvironment::from_scope(dynamic_enum.scope(db)); let self_base = ClassBase::Class(ClassType::NonGeneric(dynamic_enum.into())); // Convert the functional enum bases (`type=` mixin first, enum base second) @@ -442,7 +459,7 @@ impl<'db> Mro<'db> { let original_bases = dynamic_enum.explicit_bases(db); let mut resolved_bases: Vec> = Vec::with_capacity(original_bases.len()); for base_type in original_bases.iter().copied() { - if let Some(base) = ClassBase::try_from_explicit_base(db, base_type, None) { + if let Some(base) = ClassBase::try_from_explicit_base(db, env, base_type, None) { resolved_bases.push(base); } } @@ -464,7 +481,7 @@ impl<'db> Mro<'db> { let mut seen = FxHashSet::default(); seen.insert(self_base); for base in &resolved_bases { - for item in base.mro(db, None) { + for item in base.mro(db, env, None) { if seen.insert(item) { result.push(item); } @@ -484,7 +501,7 @@ impl<'db> Mro<'db> { fallback_mro: fallback_mro(), }); } - seqs.push(base.mro(db, None).collect()); + seqs.push(base.mro(db, env, None).collect()); } seqs.push(resolved_bases.iter().copied().collect()); @@ -497,7 +514,11 @@ impl<'db> Mro<'db> { /// Compute a fallback MRO for a dynamic class when `of_dynamic_class` fails. /// /// Iterates over base MROs sequentially with deduplication. - fn dynamic_fallback(db: &'db dyn Db, dynamic: DynamicClassLiteral<'db>) -> Self { + fn dynamic_fallback( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + dynamic: DynamicClassLiteral<'db>, + ) -> Self { let self_base = ClassBase::Class(ClassType::NonGeneric(dynamic.into())); let mut result = vec![self_base]; let mut seen = FxHashSet::default(); @@ -505,10 +526,10 @@ impl<'db> Mro<'db> { for base_type in dynamic.explicit_bases(db) { // Convert `Type` to `ClassBase`, falling back to `Unknown` if conversion fails. - let base = ClassBase::try_from_explicit_base(db, *base_type, None) + let base = ClassBase::try_from_explicit_base(db, env, *base_type, None) .unwrap_or_else(ClassBase::unknown); - for item in base.mro(db, None) { + for item in base.mro(db, env, None) { if seen.insert(item) { result.push(item); } @@ -600,10 +621,11 @@ impl<'db> MroIterator<'db> { } fn first_element(&self) -> ClassBase<'db> { + let db = self.db; match self.class { - ClassLiteral::Static(literal) => ClassBase::Class( - literal.apply_optional_specialization(self.db, self.specialization), - ), + ClassLiteral::Static(literal) => { + ClassBase::Class(literal.apply_optional_specialization(db, self.specialization)) + } ClassLiteral::Dynamic(literal) => { ClassBase::Class(ClassType::NonGeneric(literal.into())) } @@ -622,13 +644,14 @@ impl<'db> MroIterator<'db> { /// Materialize the full MRO of the class. /// Return an iterator over that MRO which skips the first element of the MRO. fn full_mro_except_first_element(&mut self) -> &mut std::slice::Iter<'db, ClassBase<'db>> { + let db = self.db; self.subsequent_elements .get_or_insert_with(|| match self.class { ClassLiteral::Static(literal) => { let specialization = self.specialization.map(|specialization| { - specialization.tuple_runtime_element_specialization(self.db) + specialization.tuple_runtime_element_specialization(db) }); - let mut full_mro_iter = match literal.try_mro(self.db, specialization) { + let mut full_mro_iter = match literal.try_mro(db, specialization) { Ok(mro) => mro.iter(), Err(error) => error.fallback_mro().iter(), }; @@ -636,7 +659,7 @@ impl<'db> MroIterator<'db> { full_mro_iter } ClassLiteral::Dynamic(literal) => { - let mut full_mro_iter = match literal.try_mro(self.db) { + let mut full_mro_iter = match literal.try_mro(db) { Ok(mro) => mro.iter(), Err(error) => error.fallback_mro().iter(), }; @@ -644,17 +667,17 @@ impl<'db> MroIterator<'db> { full_mro_iter } ClassLiteral::DynamicNamedTuple(literal) => { - let mut full_mro_iter = literal.mro(self.db).iter(); + let mut full_mro_iter = literal.mro(db).iter(); full_mro_iter.next(); full_mro_iter } ClassLiteral::DynamicTypedDict(literal) => { - let mut full_mro_iter = literal.mro(self.db).iter(); + let mut full_mro_iter = literal.mro(db).iter(); full_mro_iter.next(); full_mro_iter } ClassLiteral::DynamicEnum(literal) => { - let mut full_mro_iter = match literal.try_mro(self.db) { + let mut full_mro_iter = match literal.try_mro(db) { Ok(mro) => mro.iter(), Err(error) => error.fallback_mro().iter(), }; @@ -703,8 +726,12 @@ pub(super) struct StaticMroError<'db> { impl<'db> StaticMroError<'db> { /// Construct an MRO error of kind `InheritanceCycle`. - pub(super) fn cycle(db: &'db dyn Db, class: ClassType<'db>) -> Self { - StaticMroErrorKind::InheritanceCycle.into_mro_error(db, class) + pub(super) fn cycle( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType<'db>, + ) -> Self { + StaticMroErrorKind::InheritanceCycle.into_mro_error(db, env, class) } pub(super) fn is_cycle(&self) -> bool { @@ -763,10 +790,15 @@ pub(super) enum StaticMroErrorKind<'db> { } impl<'db> StaticMroErrorKind<'db> { - fn into_mro_error(self, db: &'db dyn Db, class: ClassType<'db>) -> StaticMroError<'db> { + fn into_mro_error( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + class: ClassType<'db>, + ) -> StaticMroError<'db> { StaticMroError { kind: self, - fallback_mro: Mro::from_error(db, class), + fallback_mro: Mro::from_error(db, env, class), } } } @@ -834,6 +866,7 @@ fn c3_merge(mut sequences: Vec>) -> Option { /// the `Generic[]` base. If not, this function will return `None`. fn check_generic_reorder_fixes_mro<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, resolved_bases: &[ClassBase<'db>], original_bases: &[Type<'db>], ) -> Option { @@ -865,7 +898,7 @@ fn check_generic_reorder_fixes_mro<'db>( if base.has_cyclic_mro(db) { return None; } - seqs.push(base.mro(db, None).collect()); + seqs.push(base.mro(db, env, None).collect()); } seqs.push(reordered); c3_merge(seqs)?; @@ -918,11 +951,12 @@ impl<'db> DynamicMroErrorKind<'db> { fn into_error( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class_literal: DynamicClassLiteral<'db>, ) -> DynamicMroError<'db> { DynamicMroError { kind: self, - fallback_mro: Mro::dynamic_fallback(db, class_literal), + fallback_mro: Mro::dynamic_fallback(db, env, class_literal), } } } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 3a217abf09..11c104cc2d 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; use std::collections::{BTreeMap, btree_map::Entry as BTreeEntry, hash_map::Entry}; -use crate::Db; use crate::reachability::{narrow_type_by_constraint, type_narrowed_by_previous_patterns}; use crate::subscript::PyIndex; use crate::types::function::KnownFunction; @@ -21,6 +20,7 @@ use crate::types::{ pattern_binding_fallthrough_type, sequence_pattern_type_builder, singleton_pattern_type, starred_sequence_pattern_type, typed_dict_matches_class_pattern, }; +use crate::{Db, ProgramEnvironment}; use ty_python_core::expression::Expression; use ty_python_core::frozen::FrozenMap; use ty_python_core::place::{PlaceExpr, PlaceTable, ScopedPlaceId}; @@ -119,8 +119,11 @@ fn all_narrowing_constraints_for_pattern<'db>( db: &'db dyn Db, pattern: PatternPredicate<'db>, ) -> Option> { - let module = parsed_module(db, pattern.file(db)).load(db); - NarrowingConstraintsBuilder::new(db, &module, PredicateNode::Pattern(pattern), true).finish() + let python_file = pattern.python_file(db); + let env = ProgramEnvironment::from_file(python_file); + let module = parsed_module(db, python_file).load(db); + NarrowingConstraintsBuilder::new(db, &env, &module, PredicateNode::Pattern(pattern), true) + .finish() } #[salsa::tracked( @@ -132,11 +135,13 @@ fn all_narrowing_constraints_for_expression<'db>( db: &'db dyn Db, expression: Expression<'db>, ) -> ExpressionNarrowingConstraints<'db> { - let module = parsed_module(db, expression.file(db)).load(db); + let python_file = expression.python_file(db); + let env = ProgramEnvironment::from_file(python_file); + let module = parsed_module(db, python_file).load(db); let predicate = PredicateNode::Expression(expression); ExpressionNarrowingConstraints { - positive: NarrowingConstraintsBuilder::new(db, &module, predicate, true).finish(), - negative: NarrowingConstraintsBuilder::new(db, &module, predicate, false).finish(), + positive: NarrowingConstraintsBuilder::new(db, &env, &module, predicate, true).finish(), + negative: NarrowingConstraintsBuilder::new(db, &env, &module, predicate, false).finish(), } } @@ -145,8 +150,11 @@ fn all_negative_narrowing_constraints_for_pattern<'db>( db: &'db dyn Db, pattern: PatternPredicate<'db>, ) -> Option> { - let module = parsed_module(db, pattern.file(db)).load(db); - NarrowingConstraintsBuilder::new(db, &module, PredicateNode::Pattern(pattern), false).finish() + let python_file = pattern.python_file(db); + let env = ProgramEnvironment::from_file(python_file); + let module = parsed_module(db, python_file).load(db); + NarrowingConstraintsBuilder::new(db, &env, &module, PredicateNode::Pattern(pattern), false) + .finish() } #[salsa::tracked(returns(as_ref), heap_size=ruff_memory_usage::heap_size)] @@ -155,9 +163,12 @@ fn all_narrowing_constraints_for_subject_element_pattern<'db>( pattern: PatternPredicate<'db>, target: ExpressionNodeKey, ) -> Option> { - let module = parsed_module(db, pattern.file(db)).load(db); + let python_file = pattern.python_file(db); + let env = ProgramEnvironment::from_file(python_file); + let module = parsed_module(db, python_file).load(db); NarrowingConstraintsBuilder::new( db, + &env, &module, PredicateNode::SubjectElementPattern(SubjectElementPatternPredicate { pattern, target }), true, @@ -198,13 +209,19 @@ impl<'db> PatternSuccessTypes<'db> { } } - fn cycle_normalized(mut self, db: &'db dyn Db, previous: &Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: &Self, + cycle: &salsa::Cycle, + ) -> Self { for (place, ty) in &mut self.bindings { - *ty = ty.cycle_normalized(db, previous.binding_type(*place), cycle); + *ty = ty.cycle_normalized(db, env, previous.binding_type(*place), cycle); } self.missing_binding_ty = self.missing_binding_ty - .cycle_normalized(db, previous.missing_binding_ty, cycle); + .cycle_normalized(db, env, previous.missing_binding_ty, cycle); self } } @@ -294,8 +311,8 @@ impl<'db> PatternBindingTypes<'db> { } /// Return the union of all contributions to this binding. - fn ty(&self, db: &'db dyn Db) -> Type<'db> { - UnionType::from_elements(db, self.contributions.iter().map(|binding| binding.ty)) + fn ty(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + UnionType::from_elements(db, env, self.contributions.iter().map(|binding| binding.ty)) } /// Mark every contribution as referring to a value extracted from the current subject. @@ -311,9 +328,10 @@ impl<'db> PatternBindingTypes<'db> { } /// Return the union of the contributions that alias the current subject. - fn subject_ty(&self, db: &'db dyn Db) -> Type<'db> { + fn subject_ty(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { UnionType::from_elements( db, + env, self.contributions .iter() .filter(|binding| binding.aliases_subject) @@ -364,6 +382,7 @@ enum PatternValueSource { /// preserve type variables. struct PatternSuccessAnalyzer<'db> { db: &'db dyn Db, + env: ProgramEnvironment<'db>, scope: ScopeId<'db>, } @@ -383,8 +402,9 @@ struct PatternSuccessAnalyzer<'db> { #[salsa::tracked( returns(ref), cycle_initial=|_, id, _| PatternSuccessTypes::cycle_initial(Type::divergent(id)), - cycle_fn=|db, cycle, previous: &PatternSuccessTypes<'db>, result: PatternSuccessTypes<'db>, _| { - result.cycle_normalized(db, previous, cycle) + cycle_fn=|db: &'db dyn Db, cycle, previous: &PatternSuccessTypes<'db>, result: PatternSuccessTypes<'db>, pattern: PatternPredicate<'db>| { + let env = ProgramEnvironment::from_scope(pattern.subject(db).scope(db)); + result.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -393,6 +413,7 @@ pub(crate) fn pattern_success_types<'db>( pattern: PatternPredicate<'db>, ) -> PatternSuccessTypes<'db> { let subject = pattern.subject(db); + let env = ProgramEnvironment::from_scope(subject.scope(db)); let incoming_subject_ty = infer_same_file_expression_type(db, subject, TypeContext::default()); let incoming_subject_ty = type_narrowed_by_previous_patterns(db, pattern, incoming_subject_ty); let analyzer = PatternSuccessAnalyzer::new(db, pattern.scope(db)); @@ -401,7 +422,7 @@ pub(crate) fn pattern_success_types<'db>( bindings: result .bindings .into_iter() - .map(|(place, binding)| (place, binding.ty(db))) + .map(|(place, binding)| (place, binding.ty(db, &env))) .collect(), missing_binding_ty: if result.matched_subject_ty.is_never() { Type::Never @@ -431,21 +452,22 @@ impl ClassInfoConstraintFunction { fn generate_constraint<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, classinfo: Type<'db>, is_positive: bool, ) -> Option> { let constraint_from_class_literal = |class: ClassLiteral<'db>| match self { ClassInfoConstraintFunction::IsInstance => { - Type::instance(db, class.top_materialization(db)) + Type::instance(db, env, class.top_materialization(db)) } ClassInfoConstraintFunction::IsSubclass => { - SubclassOfType::from(db, class.top_materialization(db)) + SubclassOfType::from(db, env, class.top_materialization(db)) } }; match classinfo { Type::TypeAlias(alias) => { - self.generate_constraint(db, alias.value_type(db), is_positive) + self.generate_constraint(db, env, alias.value_type(db), is_positive) } Type::ClassLiteral(class_literal) => Some(constraint_from_class_literal(class_literal)), Type::SubclassOf(subclass_of_ty) => { @@ -486,7 +508,7 @@ impl ClassInfoConstraintFunction { Type::Dynamic(_) | Type::Divergent(_) => Some(classinfo), Type::Intersection(intersection) => { if intersection.negative(db).is_empty() { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); let mut any_member = false; for element in intersection.positive(db) { // A member that yields no constraint (e.g. a parametrized @@ -494,8 +516,8 @@ impl ClassInfoConstraintFunction { // target) should be SKIPPED, not abort narrowing on the // whole intersection. Narrowing on the remaining members // is still sound. - if let Some(c) = self.generate_constraint(db, *element, is_positive) { - builder = builder.add_positive(c); + if let Some(c) = self.generate_constraint(db, env, *element, is_positive) { + builder.add_positive_in_place(c); any_member = true; } } @@ -509,16 +531,16 @@ impl ClassInfoConstraintFunction { None } } - Type::Union(union) => union.try_map(db, |element| { - self.generate_constraint(db, *element, is_positive) + Type::Union(union) => union.try_map(db, env, |element| { + self.generate_constraint(db, env, *element, is_positive) }), Type::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db)? { + match bound_typevar.typevar(db).bound_or_constraints(db, env)? { TypeVarBoundOrConstraints::UpperBound(bound) => { - self.generate_constraint(db, bound, is_positive) + self.generate_constraint(db, env, bound, is_positive) } TypeVarBoundOrConstraints::Constraints(constraints) => { - self.generate_constraint(db, constraints.as_type(db), is_positive) + self.generate_constraint(db, env, constraints.as_type(db, env), is_positive) } } } @@ -527,56 +549,69 @@ impl ClassInfoConstraintFunction { // e.g. `isinstance(x, list[int])` fails at runtime. Type::GenericAlias(_) => None, - Type::NominalInstance(nominal) if let Some(tuple) = nominal.tuple_spec(db) => { + Type::NominalInstance(nominal) if let Some(tuple) = nominal.tuple_spec(db, env) => { UnionType::try_from_elements( db, + env, tuple .iter_element_types(db) - .map(|element| self.generate_constraint(db, element, is_positive)), + .map(|element| self.generate_constraint(db, env, element, is_positive)), ) } Type::KnownInstance(KnownInstanceType::UnionType(instance)) => { UnionType::try_from_elements( db, - instance.value_expression_types(db).ok()?.map(|element| { - // A special case is made for `None` at runtime - // (it's implicitly converted to `NoneType` in `int | None`) - // which means that `isinstance(x, int | None)` works even though - // `None` is not a class literal. - if element.is_none(db) { - self.generate_constraint( - db, - KnownClass::NoneType.to_class_literal(db), - is_positive, - ) - } else { - self.generate_constraint(db, element, is_positive) - } - }), + env, + instance + .value_expression_types(db, env) + .ok()? + .map(|element| { + // A special case is made for `None` at runtime + // (it's implicitly converted to `NoneType` in `int | None`) + // which means that `isinstance(x, int | None)` works even though + // `None` is not a class literal. + if element.is_none(db) { + self.generate_constraint( + db, + env, + KnownClass::NoneType.to_class_literal(db, env), + is_positive, + ) + } else { + self.generate_constraint(db, env, element, is_positive) + } + }), ) } Type::SpecialForm(form) => match form { SpecialFormType::LegacyStdlibAlias(alias) => self.generate_constraint( db, - alias.aliased_class().to_class_literal(db), + env, + alias.aliased_class().to_class_literal(db, env), is_positive, ), SpecialFormType::Tuple => self.generate_constraint( db, - KnownClass::Tuple.to_class_literal(db), + env, + KnownClass::Tuple.to_class_literal(db, env), + is_positive, + ), + SpecialFormType::Type => self.generate_constraint( + db, + env, + KnownClass::Type.to_class_literal(db, env), is_positive, ), - SpecialFormType::Type => { - self.generate_constraint(db, KnownClass::Type.to_class_literal(db), is_positive) - } // We don't have a good meta-type for `Callable`s right now, // so only apply `isinstance()` narrowing, not `issubclass()` - SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable => (self - == ClassInfoConstraintFunction::IsInstance) - .then(|| Type::Callable(CallableType::unknown(db)).top_materialization(db)), + SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable => { + (self == ClassInfoConstraintFunction::IsInstance).then(|| { + Type::Callable(CallableType::unknown(db)).top_materialization(db, env) + }) + } // `InitVar` is a class at runtime, so can be used in `isinstance()`, // but we can't represent internally the type that we should narrow to after an `isinstance()` check, @@ -638,7 +673,7 @@ impl<'db> Conjunctions<'db> { self } - fn evaluate_constraint_type(self, db: &'db dyn Db) -> Type<'db> { + fn evaluate_constraint_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { if self.conjuncts.len() == 1 { return self.conjuncts[0]; } @@ -647,7 +682,7 @@ impl<'db> Conjunctions<'db> { self.conjuncts .into_iter() .fold(Type::object(), |accumulated, conjunct| { - IntersectionType::from_two_elements(db, accumulated, conjunct) + IntersectionType::from_two_elements(db, env, accumulated, conjunct) }) } } @@ -770,14 +805,18 @@ impl<'db> NarrowingConstraint<'db> { /// Evaluate the type this effectively constrains to /// /// Forgets whether each constraint originated from a `replacement` disjunct or not - pub(crate) fn evaluate_constraint_type(self, db: &'db dyn Db) -> Type<'db> { - let mut union = UnionBuilder::new(db); + pub(crate) fn evaluate_constraint_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + let mut union = UnionBuilder::new(db, env); for conjunctions in self .replacement_disjuncts .into_iter() .chain(self.intersection_disjuncts) { - union = union.add(conjunctions.evaluate_constraint_type(db)); + union.add_in_place(conjunctions.evaluate_constraint_type(db, env)); } union.build() } @@ -926,15 +965,22 @@ fn merge_constraints_or<'db>( /// value of `PatternClass` may be a subclass of `A`. fn positive_class_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class_expression_ty: Type<'db>, ) -> Option> { match class_expression_ty { Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) => { - Some(callable_pattern_type(db)) + Some(callable_pattern_type(db, env)) } - _ if class_expression_ty.is_assignable_to(db, KnownClass::Type.to_instance(db)) => { + _ if class_expression_ty.is_assignable_to( + db, + env, + KnownClass::Type.to_instance(db, env), + ) => + { ClassInfoConstraintFunction::IsInstance.generate_constraint( db, + env, class_expression_ty, true, ) @@ -963,6 +1009,7 @@ fn positive_class_pattern_type<'db>( /// ``` fn refine_exact_tuple_for_sequence_pattern<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, subject_ty: Type<'db>, pattern_element_types: &[Type<'db>], ) -> Option> { @@ -970,9 +1017,9 @@ fn refine_exact_tuple_for_sequence_pattern<'db>( let pattern_tuple = TupleSpec::heterogeneous(pattern_element_types.iter().copied()); Some( TupleSpecBuilder::from(tuple.as_ref()) - .intersect(db, &pattern_tuple) + .intersect(db, env, &pattern_tuple) .map_or(Type::Never, |refined| { - Type::tuple(TupleType::new(db, &refined.build())) + Type::tuple(TupleType::new(db, env, &refined.build())) }), ) } @@ -996,26 +1043,29 @@ fn refine_exact_tuple_for_sequence_pattern<'db>( /// every value that does. fn necessary_match_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, pattern: &PatternPredicateKind<'db>, ) -> Type<'db> { match pattern { - PatternPredicateKind::Singleton(singleton) => singleton_pattern_type(db, *singleton), + PatternPredicateKind::Singleton(singleton) => singleton_pattern_type(db, env, *singleton), PatternPredicateKind::Class(kind) => positive_class_pattern_type( db, + env, infer_same_file_expression_type(db, kind.class, TypeContext::default()), ) .unwrap_or_else(Type::object), - PatternPredicateKind::Mapping(_) => mapping_pattern_type(db), - PatternPredicateKind::Sequence(kind) => necessary_sequence_pattern_type(db, kind), + PatternPredicateKind::Mapping(_) => mapping_pattern_type(db, env), + PatternPredicateKind::Sequence(kind) => necessary_sequence_pattern_type(db, env, kind), PatternPredicateKind::Or(predicates) => UnionType::from_elements( db, + env, predicates .iter() - .map(|predicate| necessary_match_pattern_type(db, predicate)), + .map(|predicate| necessary_match_pattern_type(db, env, predicate)), ), PatternPredicateKind::As(pattern, _) => pattern .as_deref() - .map(|pattern| necessary_match_pattern_type(db, pattern)) + .map(|pattern| necessary_match_pattern_type(db, env, pattern)) .unwrap_or_else(Type::object), PatternPredicateKind::Value(_) | PatternPredicateKind::Star(_) => Type::object(), } @@ -1024,23 +1074,24 @@ fn necessary_match_pattern_type<'db>( /// Preserve the sequence element constraints that can be addressed at fixed indices. fn necessary_sequence_pattern_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, kind: &SequencePatternPredicateKind<'db>, ) -> Type<'db> { if let Some((prefix_patterns, suffix_patterns)) = kind.split_around_star() { let prefix_element_types = prefix_patterns .iter() - .map(|pattern| necessary_match_pattern_type(db, pattern)); + .map(|pattern| necessary_match_pattern_type(db, env, pattern)); let suffix_element_types = suffix_patterns .iter() - .map(|pattern| necessary_match_pattern_type(db, pattern)); + .map(|pattern| necessary_match_pattern_type(db, env, pattern)); - starred_sequence_pattern_type(db, prefix_element_types, suffix_element_types) + starred_sequence_pattern_type(db, env, prefix_element_types, suffix_element_types) } else { let element_types = kind .patterns .iter() - .map(|pattern| necessary_match_pattern_type(db, pattern)); - exact_sequence_pattern_type(db, element_types) + .map(|pattern| necessary_match_pattern_type(db, env, pattern)); + exact_sequence_pattern_type(db, env, element_types) } } @@ -1052,6 +1103,7 @@ enum NominalAttributeComparison { struct NarrowingConstraintsBuilder<'db, 'ast> { db: &'db dyn Db, + env: ProgramEnvironment<'db>, module: &'ast ParsedModuleRef, predicate: PredicateNode<'db>, is_positive: bool, @@ -1060,12 +1112,14 @@ struct NarrowingConstraintsBuilder<'db, 'ast> { impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { fn new( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, module: &'ast ParsedModuleRef, predicate: PredicateNode<'db>, is_positive: bool, ) -> Self { Self { db, + env: env.clone(), module, predicate, is_positive, @@ -1096,7 +1150,8 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { expression: Expression<'db>, is_positive: bool, ) -> Option> { - let expression_node = expression.node_ref(self.db).node(self.module); + let db = self.db; + let expression_node = expression.node_ref(db).node(self.module); self.evaluate_expression_node_predicate(expression_node, expression, is_positive) } @@ -1106,10 +1161,10 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { expression: Expression<'db>, is_positive: bool, ) -> Option> { + let db = self.db; match expression_node { ast::Expr::Name(_) => { - let file = expression.file(self.db); - let index = semantic_index(self.db, file); + let index = semantic_index(db, expression.python_file(db)); let constraints = self.evaluate_simple_expr(expression_node, is_positive); if let Some(alias_predicate) = index.narrowing_alias_predicate(expression_node) { let aliased_constraints = @@ -1125,7 +1180,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } ast::Expr::Attribute(attribute) => { let constraints = self.evaluate_simple_expr(expression_node, is_positive); - let inference = infer_expression_types(self.db, expression, TypeContext::default()); + let inference = infer_expression_types(db, expression, TypeContext::default()); let nominal_constraints = self .narrow_nominal_attribute_by_truthiness( inference.expression_type(&*attribute.value), @@ -1141,7 +1196,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } ast::Expr::Subscript(subscript) => { let constraints = self.evaluate_simple_expr(expression_node, is_positive); - let inference = infer_expression_types(self.db, expression, TypeContext::default()); + let inference = infer_expression_types(db, expression, TypeContext::default()); let typeddict_constraints = self .narrow_typeddict_subscript_by_truthiness( inference.expression_type(&*subscript.value), @@ -1207,9 +1262,10 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { expression: Expression<'db>, is_positive: bool, ) -> Option> { - let test_truthiness = infer_expression_types(self.db, expression, TypeContext::default()) + let db = self.db; + let test_truthiness = infer_expression_types(db, expression, TypeContext::default()) .expression_type(&expr_if.test) - .bool(self.db); + .bool(db, &self.env); match test_truthiness { Truthiness::AlwaysTrue => { @@ -1278,15 +1334,16 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { pattern: PatternPredicate<'db>, is_positive: bool, ) -> Option> { - let kind = pattern.kind(self.db); - let subject = pattern.subject(self.db); + let db = self.db; + let kind = pattern.kind(db); + let subject = pattern.subject(db); if !is_positive { return self .evaluate_negative_pattern_predicate_kind(kind, subject) .into_constraints(); } - let subject_node = subject.node_ref(self.db).node(self.module); + let subject_node = subject.node_ref(db).node(self.module); let expression_constraints = self .evaluate_positive_pattern_related_expressions(kind, subject, subject_node) .into_constraints(); @@ -1295,7 +1352,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { return expression_constraints; }; let place = self.expect_place(&subject_place); - let subject_ty = infer_same_file_expression_type(self.db, subject, TypeContext::default()); + let subject_ty = infer_same_file_expression_type(db, subject, TypeContext::default()); let mut constraints = expression_constraints.unwrap_or_default(); constraints.remove(&place); if let Some(subject_constraint) = self.positive_subject_constraint(kind, subject_ty) { @@ -1357,15 +1414,15 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { pattern: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Option> { + let db = self.db; match pattern { PatternPredicateKind::Value(value) => { - let value_ty = - infer_same_file_expression_type(self.db, *value, TypeContext::default()); + let value_ty = infer_same_file_expression_type(db, *value, TypeContext::default()); self.evaluate_expr_compare_op(subject_ty, value_ty, ast::CmpOp::Eq, true) .map(NarrowingConstraint::intersection) } PatternPredicateKind::Singleton(singleton) => Some(NarrowingConstraint::intersection( - singleton_pattern_type(self.db, *singleton), + singleton_pattern_type(db, &self.env, *singleton), )), PatternPredicateKind::As(Some(pattern), _) => { self.positive_subject_constraint(pattern, subject_ty) @@ -1383,9 +1440,9 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { Some(constraint) } _ => { - let matched_subject_ty = PatternSuccessAnalyzer::new(self.db, self.scope()) + let matched_subject_ty = PatternSuccessAnalyzer::new(db, self.scope()) .matched_subject_type(pattern, subject_ty); - (!matched_subject_ty.is_equivalent_to(self.db, subject_ty)) + (!matched_subject_ty.is_equivalent_to(db, &self.env, subject_ty)) .then(|| NarrowingConstraint::intersection(matched_subject_ty)) } } @@ -1394,13 +1451,16 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { impl<'db> PatternSuccessAnalyzer<'db> { fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self { - Self { db, scope } + Self { + db, + env: ProgramEnvironment::from_scope(scope), + scope, + } } fn comparison_soundness_policy(&self) -> ComparisonSoundnessPolicy { - ComparisonSoundnessPolicy::from_analysis_settings( - self.db.analysis_settings(self.scope.file(self.db)), - ) + let db = self.db; + ComparisonSoundnessPolicy::from_analysis_settings(db.analysis_settings(self.scope.file(db))) } fn merge_binding( @@ -1453,6 +1513,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { pattern: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> PatternSuccessResult<'db> { + let db = self.db; match pattern { PatternPredicateKind::Class(kind) => { self.analyze_successful_class_pattern(kind, subject_ty) @@ -1511,8 +1572,10 @@ impl<'db> PatternSuccessAnalyzer<'db> { } } PatternPredicateKind::Singleton(_) => { - let matched_subject_ty = self - .intersect_types(subject_ty, necessary_match_pattern_type(self.db, pattern)); + let matched_subject_ty = self.intersect_types( + subject_ty, + necessary_match_pattern_type(db, &self.env, pattern), + ); PatternSuccessResult { matched_subject_ty, binding_subject_ty: matched_subject_ty, @@ -1532,6 +1595,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { pattern: &PatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { + let db = self.db; match pattern { PatternPredicateKind::Class(kind) => { self.matched_class_pattern_subject_type(kind, subject_ty) @@ -1552,9 +1616,10 @@ impl<'db> PatternSuccessAnalyzer<'db> { PatternPredicateKind::Value(value) => { self.match_value_pattern_subject_type(*value, subject_ty) } - PatternPredicateKind::Singleton(_) => { - self.intersect_types(subject_ty, necessary_match_pattern_type(self.db, pattern)) - } + PatternPredicateKind::Singleton(_) => self.intersect_types( + subject_ty, + necessary_match_pattern_type(db, &self.env, pattern), + ), } } @@ -1563,12 +1628,14 @@ impl<'db> PatternSuccessAnalyzer<'db> { patterns: &[PatternPredicateKind<'db>], subject_ty: Type<'db>, ) -> Type<'db> { + let db = self.db; self.analyze_matched_subject_arms( subject_ty, OriginalSubjectPreservation::TypeVariablesOnly, |analyzer, _, subject_ty| { Some(UnionType::from_elements( - analyzer.db, + db, + &analyzer.env, patterns .iter() .map(|pattern| analyzer.matched_subject_type(pattern, subject_ty)), @@ -1595,9 +1662,11 @@ impl<'db> PatternSuccessAnalyzer<'db> { value: Expression<'db>, subject_ty: Type<'db>, ) -> Type<'db> { - let value_ty = infer_same_file_expression_type(self.db, value, TypeContext::default()); + let db = self.db; + let value_ty = infer_same_file_expression_type(db, value, TypeContext::default()); evaluate_type_equality( - self.db, + db, + &self.env, subject_ty, value_ty, true, @@ -1626,6 +1695,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { patterns: &[PatternPredicateKind<'db>], subject_ty: Type<'db>, ) -> PatternSuccessResult<'db> { + let db = self.db; let mut patterns = patterns.iter(); let Some(first_pattern) = patterns.next() else { return PatternSuccessResult { @@ -1635,9 +1705,9 @@ impl<'db> PatternSuccessAnalyzer<'db> { }; }; let first = self.analyze_successful_pattern(first_pattern, subject_ty); - let mut matched_subject_types = UnionBuilder::new(self.db); + let mut matched_subject_types = UnionBuilder::new(db, &self.env); matched_subject_types.add_in_place(first.matched_subject_ty); - let mut binding_subject_types = UnionBuilder::new(self.db); + let mut binding_subject_types = UnionBuilder::new(db, &self.env); binding_subject_types.add_in_place(first.binding_subject_ty); // All alternatives bind the same names. Merge by logical place so the case body sees the // union even though the semantic walk visits the definitions in order. @@ -1646,8 +1716,12 @@ impl<'db> PatternSuccessAnalyzer<'db> { let mut previous_pattern = first_pattern; for pattern in patterns { - remaining_subject_ty = - pattern_binding_fallthrough_type(self.db, previous_pattern, remaining_subject_ty); + remaining_subject_ty = pattern_binding_fallthrough_type( + db, + &self.env, + previous_pattern, + remaining_subject_ty, + ); let alternative = self.analyze_successful_pattern(pattern, remaining_subject_ty); binding_subject_types.add_in_place(alternative.binding_subject_ty); Self::merge_bindings(&mut bindings, alternative.bindings); @@ -1683,39 +1757,44 @@ impl<'db> PatternSuccessAnalyzer<'db> { class_ty: Type<'db>, subject_ty: Type<'db>, ) -> Type<'db> { + let db = self.db; match subject_ty { Type::TypeAlias(alias) => { - self.filter_class_pattern_subject_type(class, class_ty, alias.value_type(self.db)) + self.filter_class_pattern_subject_type(class, class_ty, alias.value_type(db)) } - Type::Union(union) => union.map(self.db, |element| { + Type::Union(union) => union.map(db, &self.env, |element| { self.filter_class_pattern_subject_type(class, class_ty, *element) }), - Type::Intersection(intersection) if intersection.positive(self.db).is_empty() => { + Type::Intersection(intersection) if intersection.positive(db).is_empty() => { self.intersect_types(subject_ty, class_ty) } - Type::Intersection(intersection) => intersection.map_positive(self.db, |positive| { - self.filter_class_pattern_subject_type(class, class_ty, *positive) - }), + Type::Intersection(intersection) => { + intersection.map_positive(db, &self.env, |positive| { + self.filter_class_pattern_subject_type(class, class_ty, *positive) + }) + } Type::NominalInstance(instance) => { let Some(class) = class else { return self.intersect_types(subject_ty, class_ty); }; - let subject_class = instance.class(self.db); - if subject_class.is_subtype_of_class_literal(self.db, class) { + let subject_class = instance.class(db, &self.env); + if subject_class.is_subtype_of_class_literal(db, class) { subject_ty - } else if subject_ty.is_disjoint_from(self.db, class_ty) { + } else if subject_ty.is_disjoint_from(db, &self.env, class_ty) { Type::Never } else { self.intersect_types(subject_ty, class_ty) } } Type::TypedDict(_) - if class.is_some_and(|class| typed_dict_matches_class_pattern(self.db, class)) => + if class.is_some_and(|class| { + typed_dict_matches_class_pattern(db, &self.env, class) + }) => { subject_ty } - _ if subject_ty.is_subtype_of(self.db, class_ty) => subject_ty, - _ if subject_ty.is_disjoint_from(self.db, class_ty) => Type::Never, + _ if subject_ty.is_subtype_of(db, &self.env, class_ty) => subject_ty, + _ if subject_ty.is_disjoint_from(db, &self.env, class_ty) => Type::Never, _ => self.intersect_types(subject_ty, class_ty), } } @@ -1728,67 +1807,69 @@ impl<'db> PatternSuccessAnalyzer<'db> { filtering_subject_ty: Type<'db>, subject_ty: Type<'db>, ) -> Option>> { + let db = self.db; let subject_is_final = subject_ty - .nominal_class(self.db) - .is_some_and(|class| class.is_final(self.db)); + .nominal_class(db, &self.env) + .is_some_and(|class| class.is_final(db)); let specialized_pattern_class = if context.positional_sources.is_empty() && kind.keywords.is_empty() { None } else { context .class - .zip(filtering_subject_ty.nominal_class(self.db)) + .zip(filtering_subject_ty.nominal_class(db, &self.env)) .and_then(|(pattern_class, subject_class)| { self.specialize_pattern_class_for_subject(pattern_class, subject_class) }) }; let member_type = |name: &Name| { let original_member_ty = original_subject_ty - .member(self.db, name.as_str()) + .member(db, &self.env, name.as_str()) .place .ignore_possibly_undefined(); - let place = subject_ty.member(self.db, name.as_str()).place; + let place = subject_ty.member(db, &self.env, name.as_str()).place; let mut member_ty = place.ignore_possibly_undefined(); if let Some(specialized_pattern_class) = specialized_pattern_class { - member_ty = Type::instance(self.db, specialized_pattern_class) - .member(self.db, name.as_str()) + member_ty = Type::instance(db, &self.env, specialized_pattern_class) + .member(db, &self.env, name.as_str()) .place .ignore_possibly_undefined(); } else if let Some(pattern_class) = context.class && pattern_class - .generic_context(self.db) + .generic_context(db) .and_then(|generic_context| { pattern_class .instance_member( - self.db, - Some(generic_context.identity_specialization(self.db)), + db, + &self.env, + Some(generic_context.identity_specialization(db)), name.as_str(), ) .place .ignore_possibly_undefined() }) - .is_some_and(|ty| ty.has_typevar(self.db)) + .is_some_and(|ty| ty.has_typevar(db, &self.env)) { - let unknown_pattern_class = pattern_class.unknown_specialization(self.db); - let unknown_pattern_member_ty = Type::instance(self.db, unknown_pattern_class) - .member(self.db, name.as_str()) - .place - .ignore_possibly_undefined(); + let unknown_pattern_class = pattern_class.unknown_specialization(db); + let unknown_pattern_member_ty = + Type::instance(db, &self.env, unknown_pattern_class) + .member(db, &self.env, name.as_str()) + .place + .ignore_possibly_undefined(); // For example, `Child[int]` and `Base[T]` share a generic hierarchy, so a `Base` // pattern can reuse `int` from the subject. This is also the conservative fallback // when the subject does not determine one exact specialization of the pattern // subclass. if original_subject_ty - .nominal_class(self.db) + .nominal_class(db, &self.env) .is_some_and(|original_class| { - unknown_pattern_class.is_subtype_of_class_literal( - self.db, - original_class.class_literal(self.db), - ) || original_class.is_subtype_of_class_literal( - self.db, - unknown_pattern_class.class_literal(self.db), - ) + unknown_pattern_class + .is_subtype_of_class_literal(db, original_class.class_literal(db)) + || original_class.is_subtype_of_class_literal( + db, + unknown_pattern_class.class_literal(db), + ) }) { // The pattern class's unknown specialization loses type arguments known @@ -1803,7 +1884,8 @@ impl<'db> PatternSuccessAnalyzer<'db> { // Unrelated classes can overlap through multiple inheritance, so retain the // generic pattern class's member as a possible runtime value. member_ty = Some(UnionType::from_elements( - self.db, + db, + &self.env, member_ty.into_iter().chain([pattern_member_ty]), )); } @@ -1862,30 +1944,32 @@ impl<'db> PatternSuccessAnalyzer<'db> { pattern_class: ClassLiteral<'db>, subject_class: ClassType<'db>, ) -> Option> { - let generic_context = pattern_class.generic_context(self.db)?; + let db = self.db; + let generic_context = pattern_class.generic_context(db)?; let pattern_base = pattern_class - .identity_specialization(self.db) - .iter_mro(self.db) + .identity_specialization(db) + .iter_mro(db) .filter_map(ClassBase::into_class) - .find(|base| base.class_literal(self.db) == subject_class.class_literal(self.db))?; + .find(|base| base.class_literal(db) == subject_class.class_literal(db))?; let constraints = ConstraintSetBuilder::new(); - let solutions = Type::instance(self.db, pattern_base) + let solutions = Type::instance(db, &self.env, pattern_base) .assignable_solutions_with_inferable( - self.db, - Type::instance(self.db, subject_class), - generic_context.inferable_typevars(self.db), + db, + &self.env, + Type::instance(db, &self.env, subject_class), + generic_context.inferable_typevars(db), ) .solve_with(|variance, path_bound| { let Some(lower) = path_bound.lower else { return Ok(None); }; if variance != TypeVarVariance::Invariant - || path_bound.upper.materialize_exact(self.db) != lower + || path_bound.upper.materialize_exact(db, &self.env) != lower { return Ok(None); } - PathBounds::default_solve(self.db, &constraints, path_bound) + PathBounds::default_solve(db, &self.env, &constraints, path_bound) }); let Solutions::Constrained(solutions) = solutions else { return None; @@ -1894,7 +1978,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { return None; }; - let typevars = generic_context.variables(self.db); + let typevars = generic_context.variables(db); let types = typevars .clone() .map(|typevar| { @@ -1906,43 +1990,42 @@ impl<'db> PatternSuccessAnalyzer<'db> { .collect::>>()?; if types.iter().any(|ty| { typevars.clone().any(|typevar| { - ty.references_typevar(self.db, typevar.typevar(self.db).identity(self.db)) + ty.references_typevar(db, &self.env, typevar.typevar(db).identity(db)) }) }) { return None; } - Some( - pattern_class - .apply_specialization(self.db, |_| generic_context.specialize(self.db, types)), - ) + Some(pattern_class.apply_specialization(db, |_| generic_context.specialize(db, types))) } fn class_pattern_contexts( &self, kind: &ClassPatternPredicateKind<'db>, ) -> SmallVec<[ClassPatternContext<'db>; 2]> { - let class_expr_ty = - infer_same_file_expression_type(self.db, kind.class, TypeContext::default()) - .resolve_type_alias(self.db); + let db = self.db; + let class_expr_ty = infer_same_file_expression_type(db, kind.class, TypeContext::default()) + .resolve_type_alias(db); let context = |class_expr_ty: Type<'db>| { let class = class_expr_ty.as_class_literal(); ClassPatternContext { class, - class_ty: positive_class_pattern_type(self.db, class_expr_ty) + class_ty: positive_class_pattern_type(db, &self.env, class_expr_ty) .unwrap_or_else(Type::object), positional_sources: class.map_or_else( || vec![ClassPatternPositionalSource::Unknown; kind.positional.len()], - |class| class_pattern_positional_sources(self.db, class, kind.positional.len()), + |class| { + class_pattern_positional_sources( + db, + &self.env, + class, + kind.positional.len(), + ) + }, ), } }; match class_expr_ty { - Type::Union(union) => union - .elements(self.db) - .iter() - .copied() - .map(context) - .collect(), + Type::Union(union) => union.elements(db).iter().copied().map(context).collect(), _ => smallvec![context(class_expr_ty)], } } @@ -1974,8 +2057,10 @@ impl<'db> PatternSuccessAnalyzer<'db> { kind: &ClassPatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { + let db = self.db; UnionType::from_elements( - self.db, + db, + &self.env, self.class_pattern_contexts(kind).iter().map(|context| { self.matched_class_pattern_subject_type_for_context(kind, context, subject_ty) }), @@ -2019,8 +2104,9 @@ impl<'db> PatternSuccessAnalyzer<'db> { kind: &ClassPatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> PatternSuccessResult<'db> { - let mut matched_subject_types = UnionBuilder::new(self.db); - let mut binding_subject_types = UnionBuilder::new(self.db); + let db = self.db; + let mut matched_subject_types = UnionBuilder::new(db, &self.env); + let mut binding_subject_types = UnionBuilder::new(db, &self.env); let mut bindings = BTreeMap::new(); for context in self.class_pattern_contexts(kind) { let result = @@ -2084,35 +2170,36 @@ impl<'db> PatternSuccessAnalyzer<'db> { subject_ty: Type<'db>, key_ty: Type<'db>, ) -> Option> { - if let Type::TypedDict(typed_dict) = subject_ty.resolve_type_alias(self.db) { - let key_ty = key_ty.resolve_type_alias(self.db); - let typed_dict_key_ty = typed_dict.key_type(self.db); + let db = self.db; + if let Type::TypedDict(typed_dict) = subject_ty.resolve_type_alias(db) { + let key_ty = key_ty.resolve_type_alias(db); + let typed_dict_key_ty = typed_dict.key_type(db, &self.env); let policy = self.comparison_soundness_policy(); if typed_dict_key_ty.is_never() - || equality_truthiness(self.db, typed_dict_key_ty, key_ty, policy) + || equality_truthiness(db, &self.env, typed_dict_key_ty, key_ty, policy) == Truthiness::AlwaysFalse { return None; } if let Some(key) = key_ty.as_string_literal() { return typed_dict - .item(self.db, key.value(self.db)) + .item(db, key.value(db)) .map(|field| field.declared_ty) .or_else(|| { typed_dict - .openness(self.db) + .openness(db) .is_implicitly_open() .then_some(Type::object()) }); } - return Some(typed_dict.value_type(self.db)); + return Some(typed_dict.value_type(db, &self.env)); } - let Some((_, mapping_value_ty)) = subject_ty.unpack_keys_and_items(self.db) else { + let Some((_, mapping_value_ty)) = subject_ty.unpack_keys_and_items(db, &self.env) else { return Some(Type::unknown()); }; let Some(get_method) = subject_ty - .member(self.db, "get") + .member(db, &self.env, "get") .place .ignore_possibly_undefined() else { @@ -2125,17 +2212,22 @@ impl<'db> PatternSuccessAnalyzer<'db> { }; Some( get_method - .try_call(self.db, &CallArguments::positional([key_ty, default_ty])) - .map(|bindings| bindings.return_type(self.db)) - .unwrap_or_else(|error| error.return_type(self.db)), + .try_call( + db, + &self.env, + &CallArguments::positional([key_ty, default_ty]), + ) + .map(|bindings| bindings.return_type(db, &self.env)) + .unwrap_or_else(|error| error.return_type(db, &self.env)), ) } fn mapping_pattern_uses_standard_get(&self, subject_ty: Type<'db>) -> bool { - let Some(class) = subject_ty.nominal_class(self.db) else { + let db = self.db; + let Some(class) = subject_ty.nominal_class(db, &self.env) else { return false; }; - for base in class.iter_mro(self.db) { + for base in class.iter_mro(db) { let class = match base { ClassBase::Class(class) => class, ClassBase::Generic | ClassBase::Protocol => continue, @@ -2146,14 +2238,20 @@ impl<'db> PatternSuccessAnalyzer<'db> { return false; } }; - if !class.own_instance_member(self.db, "get").is_undefined() { + if !class + .own_instance_member(db, &self.env, "get") + .is_undefined() + { return false; } - if class.own_class_member(self.db, None, "get").is_undefined() { + if class + .own_class_member(db, &self.env, None, "get") + .is_undefined() + { continue; } return matches!( - class.known(self.db), + class.known(db), Some(KnownClass::Dict | KnownClass::Mapping) ); } @@ -2161,11 +2259,10 @@ impl<'db> PatternSuccessAnalyzer<'db> { } fn mapping_pattern_key_types(&self, kind: &MappingPatternPredicateKind<'db>) -> Vec> { + let db = self.db; kind.entries .iter() - .map(|entry| { - infer_same_file_expression_type(self.db, entry.key, TypeContext::default()) - }) + .map(|entry| infer_same_file_expression_type(db, entry.key, TypeContext::default())) .collect() } @@ -2174,7 +2271,9 @@ impl<'db> PatternSuccessAnalyzer<'db> { subject_ty: Type<'db>, key_types: &[Type<'db>], ) -> Option<(Type<'db>, Vec>)> { - let narrowed_subject_ty = self.intersect_types(subject_ty, mapping_pattern_type(self.db)); + let db = self.db; + let narrowed_subject_ty = + self.intersect_types(subject_ty, mapping_pattern_type(db, &self.env)); if narrowed_subject_ty.is_never() { return None; } @@ -2256,13 +2355,14 @@ impl<'db> PatternSuccessAnalyzer<'db> { } fn mapping_pattern_rest_type_for_arm(&self, subject_ty: Type<'db>) -> Type<'db> { - let (key_ty, value_ty) = match subject_ty.resolve_type_alias(self.db) { - Type::TypedDict(_) => (KnownClass::Str.to_instance(self.db), Type::object()), + let db = self.db; + let (key_ty, value_ty) = match subject_ty.resolve_type_alias(db) { + Type::TypedDict(_) => (KnownClass::Str.to_instance(db, &self.env), Type::object()), _ => subject_ty - .unpack_keys_and_items(self.db) + .unpack_keys_and_items(db, &self.env) .unwrap_or_else(|| (Type::unknown(), Type::unknown())), }; - KnownClass::Dict.to_specialized_instance(self.db, &[key_ty, value_ty]) + KnownClass::Dict.to_specialized_instance(db, &self.env, &[key_ty, value_ty]) } fn matched_sequence_pattern_subject_type( @@ -2270,8 +2370,9 @@ impl<'db> PatternSuccessAnalyzer<'db> { kind: &SequencePatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> Type<'db> { + let db = self.db; let target_len = Self::sequence_pattern_target_len(kind); - let sequence_ty = sequence_pattern_type_builder(self.db).build(); + let sequence_ty = sequence_pattern_type_builder(db, &self.env).build(); self.analyze_matched_subject_arms( subject_ty, OriginalSubjectPreservation::TypeVariablesOnly, @@ -2316,8 +2417,9 @@ impl<'db> PatternSuccessAnalyzer<'db> { kind: &SequencePatternPredicateKind<'db>, subject_ty: Type<'db>, ) -> PatternSuccessResult<'db> { + let db = self.db; let target_len = Self::sequence_pattern_target_len(kind); - let sequence_ty = sequence_pattern_type_builder(self.db).build(); + let sequence_ty = sequence_pattern_type_builder(db, &self.env).build(); self.analyze_pattern_subject_arms( subject_ty, OriginalSubjectPreservation::TypeVariablesOnly, @@ -2369,9 +2471,14 @@ impl<'db> PatternSuccessAnalyzer<'db> { narrowed_subject_ty: Type<'db>, matched_element_types: &[Type<'db>], ) -> Type<'db> { + let db = self.db; if kind.split_around_star().is_none() - && let Some(refined) = - refine_exact_tuple_for_sequence_pattern(self.db, subject_ty, matched_element_types) + && let Some(refined) = refine_exact_tuple_for_sequence_pattern( + db, + &self.env, + subject_ty, + matched_element_types, + ) { return refined; } @@ -2394,20 +2501,28 @@ impl<'db> PatternSuccessAnalyzer<'db> { subject_ty: Type<'db>, binding_element_types: &[Type<'db>], ) -> Type<'db> { + let db = self.db; if kind.split_around_star().is_none() - && let Some(refined) = - refine_exact_tuple_for_sequence_pattern(self.db, subject_ty, binding_element_types) + && let Some(refined) = refine_exact_tuple_for_sequence_pattern( + db, + &self.env, + subject_ty, + binding_element_types, + ) { return refined; } - if subject_ty.exact_tuple_instance_spec(self.db).is_some() { + if subject_ty.exact_tuple_instance_spec(db).is_some() { self.intersect_types( subject_ty, self.successful_sequence_pattern_type(kind, binding_element_types), ) } else { - self.intersect_types(subject_ty, sequence_pattern_type_builder(self.db).build()) + self.intersect_types( + subject_ty, + sequence_pattern_type_builder(db, &self.env).build(), + ) } } @@ -2416,15 +2531,16 @@ impl<'db> PatternSuccessAnalyzer<'db> { kind: &SequencePatternPredicateKind<'db>, matched_element_types: &[Type<'db>], ) -> Type<'db> { + let db = self.db; if let Some((prefix, suffix)) = kind.split_around_star() { let prefix_types = matched_element_types.iter().copied().take(prefix.len()); let suffix_types = matched_element_types .iter() .copied() .skip(matched_element_types.len().saturating_sub(suffix.len())); - starred_sequence_pattern_type(self.db, prefix_types, suffix_types) + starred_sequence_pattern_type(db, &self.env, prefix_types, suffix_types) } else { - exact_sequence_pattern_type(self.db, matched_element_types.iter().copied()) + exact_sequence_pattern_type(db, &self.env, matched_element_types.iter().copied()) } } @@ -2439,22 +2555,25 @@ impl<'db> PatternSuccessAnalyzer<'db> { target_len: TupleLength, sequence_ty: Type<'db>, ) -> Option<(Type<'db>, Vec>)> { + let db = self.db; let narrowed_subject_ty = self.intersect_types(subject_ty, sequence_ty); if narrowed_subject_ty.is_never() { return None; } - let tuple = subject_ty.try_iterate(self.db).unwrap_or_else(|error| { - let fallback_element_ty = error.fallback_element_type(self.db); - Cow::Owned(TupleSpec::homogeneous( - if fallback_element_ty.is_unknown() { - Type::object() - } else { - fallback_element_ty - }, - )) - }); - let mut unpacker = TupleUnpacker::new(self.db, target_len); + let tuple = subject_ty + .try_iterate(db, &self.env) + .unwrap_or_else(|error| { + let fallback_element_ty = error.fallback_element_type(db, &self.env); + Cow::Owned(TupleSpec::homogeneous( + if fallback_element_ty.is_unknown() { + Type::object() + } else { + fallback_element_ty + }, + )) + }); + let mut unpacker = TupleUnpacker::new(db, &self.env, target_len); unpacker.unpack_tuple(tuple.as_ref()).ok()?; Some((narrowed_subject_ty, unpacker.into_types().collect())) } @@ -2465,15 +2584,17 @@ impl<'db> PatternSuccessAnalyzer<'db> { preservation: OriginalSubjectPreservation, analyze_arm: impl Fn(&Self, Type<'db>, Type<'db>) -> Option>, ) -> Type<'db> { + let db = self.db; let subject_arms = self.match_pattern_subject_arms(subject_ty); let grouped_arms = subject_arms .into_iter() .chunk_by(|(original_subject_ty, _)| *original_subject_ty); - let mut matched_subject_types = UnionBuilder::new(self.db); + let mut matched_subject_types = UnionBuilder::new(db, &self.env); for (original_subject_ty, arms) in &grouped_arms { let matched_types = UnionType::from_elements( - self.db, + db, + &self.env, arms.filter_map(|(_, filtering_subject_ty)| { analyze_arm(self, original_subject_ty, filtering_subject_ty) }), @@ -2494,17 +2615,18 @@ impl<'db> PatternSuccessAnalyzer<'db> { preservation: OriginalSubjectPreservation, analyze_arm: impl Fn(&Self, Type<'db>, Type<'db>) -> Option>, ) -> PatternSuccessResult<'db> { + let db = self.db; let subject_arms = self.match_pattern_subject_arms(subject_ty); let grouped_arms = subject_arms .into_iter() .chunk_by(|(original_subject_ty, _)| *original_subject_ty); - let mut matched_subject_types = UnionBuilder::new(self.db); - let mut binding_subject_types = UnionBuilder::new(self.db); + let mut matched_subject_types = UnionBuilder::new(db, &self.env); + let mut binding_subject_types = UnionBuilder::new(db, &self.env); let mut bindings = BTreeMap::new(); for (original_subject_ty, arms) in &grouped_arms { - let mut matched_types = UnionBuilder::new(self.db); - let mut binding_types = UnionBuilder::new(self.db); + let mut matched_types = UnionBuilder::new(db, &self.env); + let mut binding_types = UnionBuilder::new(db, &self.env); let mut arm_bindings = BTreeMap::new(); for (_, filtering_subject_ty) in arms { @@ -2516,7 +2638,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { } for binding in arm_bindings.values_mut() { - let subject_ty = binding.subject_ty(self.db); + let subject_ty = binding.subject_ty(db, &self.env); if !subject_ty.is_never() { binding.restore_subject(self.preserve_original_subject_type( original_subject_ty, @@ -2553,13 +2675,14 @@ impl<'db> PatternSuccessAnalyzer<'db> { filtered_ty: Type<'db>, preservation: OriginalSubjectPreservation, ) -> Type<'db> { + let db = self.db; let filtering_ty = self.pattern_filtering_type(original_subject_ty); - if filtered_ty.is_equivalent_to(self.db, filtering_ty) + if filtered_ty.is_equivalent_to(db, &self.env, filtering_ty) && (matches!(preservation, OriginalSubjectPreservation::EquivalentTypes) - || original_subject_ty.has_typevar(self.db)) + || original_subject_ty.has_typevar(db, &self.env)) { original_subject_ty - } else if original_subject_ty.has_typevar(self.db) { + } else if original_subject_ty.has_typevar(db, &self.env) { self.intersect_types(original_subject_ty, filtered_ty) } else { filtered_ty @@ -2575,14 +2698,15 @@ impl<'db> PatternSuccessAnalyzer<'db> { &self, subject_ty: Type<'db>, ) -> SmallVec<[(Type<'db>, Type<'db>); 2]> { - let subject_ty = subject_ty.resolve_type_alias(self.db); + let db = self.db; + let subject_ty = subject_ty.resolve_type_alias(db); let mut arms = SmallVec::new(); let mut add_arm = |original_subject_ty: Type<'db>| { let filtering_subject_ty = self.pattern_filtering_type(original_subject_ty); match filtering_subject_ty { Type::Union(union) => arms.extend( union - .elements(self.db) + .elements(db) .iter() .map(|element| (original_subject_ty, *element)), ), @@ -2591,11 +2715,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { }; match subject_ty { - Type::Union(union) => union - .elements(self.db) - .iter() - .copied() - .for_each(&mut add_arm), + Type::Union(union) => union.elements(db).iter().copied().for_each(&mut add_arm), _ => add_arm(subject_ty), } @@ -2603,11 +2723,12 @@ impl<'db> PatternSuccessAnalyzer<'db> { } fn pattern_filtering_type(&self, ty: Type<'db>) -> Type<'db> { - let ty = ty.resolve_type_alias(self.db); + let db = self.db; + let ty = ty.resolve_type_alias(db); if let Type::TypeVar(typevar) = ty - && let Some(bound) = typevar.typevar(self.db).upper_bound(self.db) + && let Some(bound) = typevar.typevar(db).upper_bound(db, &self.env) { - bound.resolve_type_alias(self.db) + bound.resolve_type_alias(db) } else { ty } @@ -2622,14 +2743,16 @@ impl<'db> PatternSuccessAnalyzer<'db> { } fn intersect_types(&self, left: Type<'db>, right: Type<'db>) -> Type<'db> { - IntersectionBuilder::new(self.db) + let db = self.db; + IntersectionBuilder::new(db, &self.env) .add_positive(left) .add_positive(right) .build() } fn places(&self) -> &'db PlaceTable { - place_table(self.db, self.scope) + let db = self.db; + place_table(db, self.scope) } } @@ -2638,40 +2761,44 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { &mut self, subject_element: SubjectElementPatternPredicate<'db>, ) -> Option> { + let db = self.db; let pattern = subject_element.pattern; - let subject_expression = pattern.subject(self.db); - let subject = subject_expression.node_ref(self.db).node(self.module); + let subject_expression = pattern.subject(db); + let subject = subject_expression.node_ref(db).node(self.module); self.evaluate_match_pattern_for_subject_element( subject_expression, subject, - pattern.kind(self.db), + pattern.kind(db), Some(subject_element.target), ) .into_constraints() } fn places(&self) -> &'db PlaceTable { - place_table(self.db, self.scope()) + let db = self.db; + place_table(db, self.scope()) } fn scope(&self) -> ScopeId<'db> { + let db = self.db; match self.predicate { - PredicateNode::Expression(expression) => expression.scope(self.db), - PredicateNode::Pattern(pattern) => pattern.scope(self.db), + PredicateNode::Expression(expression) => expression.scope(db), + PredicateNode::Pattern(pattern) => pattern.scope(db), PredicateNode::SubjectElementPattern(subject_element) => { - subject_element.pattern.scope(self.db) + subject_element.pattern.scope(db) } PredicateNode::IsNonTerminalCall(CallableAndCallExpr { callable, .. }) => { - callable.scope(self.db) + callable.scope(db) } - PredicateNode::IsNonEmptyIterable(expression) => expression.scope(self.db), - PredicateNode::StarImportPlaceholder(definition) => definition.scope(self.db), + PredicateNode::IsNonEmptyIterable(expression) => expression.scope(db), + PredicateNode::StarImportPlaceholder(definition) => definition.scope(db), } } fn comparison_soundness_policy(&self) -> ComparisonSoundnessPolicy { + let db = self.db; ComparisonSoundnessPolicy::from_analysis_settings( - self.db.analysis_settings(self.scope().file(self.db)), + db.analysis_settings(self.scope().file(db)), ) } @@ -2694,9 +2821,13 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { /// and much of our special-casing for tuples elsewhere depends on this assumption). /// - Arbitrary user types that return `Literal` types from both `__len__` and `__bool__`, /// where the returned `Literal` types are mutually consistent in their truthiness. - fn is_base_type_narrowable_by_len(db: &'db dyn Db, ty: Type<'db>) -> bool { + fn is_base_type_narrowable_by_len( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { match ty { - Type::NominalInstance(instance) if instance.tuple_spec(db).is_some() => true, + Type::NominalInstance(instance) if instance.tuple_spec(db, env).is_some() => true, Type::LiteralValue(literal) if matches!( literal.kind(), @@ -2707,9 +2838,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { { true } - _ => ty.len(db).is_some_and(|len_ty| { - let len_ty_bool = len_ty.bool(db); - len_ty_bool != Truthiness::Ambiguous && len_ty_bool == ty.bool(db) + _ => ty.len(db, env).is_some_and(|len_ty| { + let len_ty_bool = len_ty.bool(db, env); + len_ty_bool != Truthiness::Ambiguous && len_ty_bool == ty.bool(db, env) }), } } @@ -2720,7 +2851,12 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { /// `~AlwaysTruthy` (negative). For non-narrowable types, we return them unchanged. /// /// Returns `None` if no part of the type is narrowable. - fn narrow_type_by_len(db: &'db dyn Db, ty: Type<'db>, is_positive: bool) -> Option> { + fn narrow_type_by_len( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + is_positive: bool, + ) -> Option> { match ty { Type::Union(union) => { let mut has_narrowable = false; @@ -2728,7 +2864,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .elements(db) .iter() .map(|element| { - if let Some(narrowed) = Self::narrow_type_by_len(db, *element, is_positive) + if let Some(narrowed) = + Self::narrow_type_by_len(db, env, *element, is_positive) { has_narrowable = true; narrowed @@ -2740,7 +2877,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .collect(); if has_narrowable { - Some(UnionType::from_elements(db, narrowed_elements)) + Some(UnionType::from_elements(db, env, narrowed_elements)) } else { None } @@ -2750,27 +2887,27 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let positive = intersection.positive(db); let has_narrowable = positive .iter() - .any(|element| Self::is_base_type_narrowable_by_len(db, *element)); + .any(|element| Self::is_base_type_narrowable_by_len(db, env, *element)); if has_narrowable { // Apply the narrowing constraint to the whole intersection. - let mut builder = IntersectionBuilder::new(db).add_positive(ty); + let mut builder = IntersectionBuilder::new(db, env).add_positive(ty); if is_positive { - builder = builder.add_negative(Type::AlwaysFalsy); + builder.add_negative_in_place(Type::AlwaysFalsy); } else { - builder = builder.add_negative(Type::AlwaysTruthy); + builder.add_negative_in_place(Type::AlwaysTruthy); } Some(builder.build()) } else { None } } - _ if Self::is_base_type_narrowable_by_len(db, ty) => { - let mut builder = IntersectionBuilder::new(db).add_positive(ty); + _ if Self::is_base_type_narrowable_by_len(db, env, ty) => { + let mut builder = IntersectionBuilder::new(db, env).add_positive(ty); if is_positive { - builder = builder.add_negative(Type::AlwaysFalsy); + builder.add_negative_in_place(Type::AlwaysFalsy); } else { - builder = builder.add_negative(Type::AlwaysTruthy); + builder.add_negative_in_place(Type::AlwaysTruthy); } Some(builder.build()) } @@ -2785,6 +2922,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { /// an observed length would become stale after mutation. fn narrow_type_by_exact_len( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, length: usize, is_equality: bool, @@ -2792,28 +2930,35 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let resolved = ty.resolve_type_alias(db); let narrowed = match resolved { - Type::Union(union) => union.map(db, |element| { - Self::narrow_type_by_exact_len(db, *element, length, is_equality) + Type::Union(union) => union.map(db, env, |element| { + Self::narrow_type_by_exact_len(db, env, *element, length, is_equality) }), - Type::Intersection(intersection) => intersection.map_positive(db, |element| { - Self::narrow_type_by_exact_len(db, *element, length, is_equality) + Type::Intersection(intersection) => intersection.map_positive(db, env, |element| { + Self::narrow_type_by_exact_len(db, env, *element, length, is_equality) }), Type::TypeVar(typevar) => { - let Some(bound_or_constraints) = typevar.typevar(db).bound_or_constraints(db) + let Some(bound_or_constraints) = typevar.typevar(db).bound_or_constraints(db, env) else { return ty; }; - let upper_bound = bound_or_constraints.as_type(db); + let upper_bound = bound_or_constraints.as_type(db, env); let narrowed_upper_bound = match bound_or_constraints { TypeVarBoundOrConstraints::UpperBound(bound) => { - Self::narrow_type_by_exact_len(db, bound, length, is_equality) + Self::narrow_type_by_exact_len(db, env, bound, length, is_equality) } TypeVarBoundOrConstraints::Constraints(constraints) => { UnionType::from_elements( db, + env, constraints.elements(db).iter().map(|constraint| { - Self::narrow_type_by_exact_len(db, *constraint, length, is_equality) + Self::narrow_type_by_exact_len( + db, + env, + *constraint, + length, + is_equality, + ) }), ) } @@ -2822,17 +2967,17 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { if narrowed_upper_bound == upper_bound { resolved } else { - IntersectionType::from_two_elements(db, resolved, narrowed_upper_bound) + IntersectionType::from_two_elements(db, env, resolved, narrowed_upper_bound) } } _ => { if is_equality && let Some(tuple) = resolved.exact_tuple_instance_spec(db) { - match tuple.resize(db, TupleLength::Fixed(length)) { - Ok(tuple) => Type::tuple(TupleType::new(db, &tuple)), + match tuple.resize(db, env, TupleLength::Fixed(length)) { + Ok(tuple) => Type::tuple(TupleType::new(db, env, &tuple)), Err(_) => Type::Never, } } else { - let tuple_length = resolved.tuple_instance_spec(db).map(|spec| spec.len()); + let tuple_length = resolved.tuple_instance_spec(db, env).map(|spec| spec.len()); let satisfies_comparison = |length_type: Type<'db>| { length_type .as_int_literal() @@ -2840,7 +2985,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .is_some_and(|actual| (actual == length) == is_equality) }; let comparison_possible = resolved - .len(db) + .len(db, env) .map(|length_type| match length_type { Type::Union(union) => union .elements(db) @@ -2876,13 +3021,14 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { expr: &ast::Expr, is_positive: bool, ) -> Option> { + let db = self.db; let target = PlaceExpr::try_from_expr(expr)?; let place = self.expect_place(&target); let ty = if is_positive { - Type::AlwaysFalsy.negate(self.db) + Type::AlwaysFalsy.negate(db, &self.env) } else { - Type::AlwaysTruthy.negate(self.db) + Type::AlwaysTruthy.negate(db, &self.env) }; Some(NarrowingConstraints::from_iter([( @@ -2927,31 +3073,30 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } fn evaluate_expr_in(&self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { - let rhs_ty = rhs_ty.resolve_type_alias(self.db); + let db = self.db; + let rhs_ty = rhs_ty.resolve_type_alias(db); // The supported containers compare against their iterated elements, so union arms can be // combined. String membership also accepts multi-character substrings, so evaluate literal // haystacks separately, including when they occur in a union. if let Some(haystack) = rhs_ty.as_string_literal() { - return narrow_string_membership(self.db, lhs_ty, haystack.value(self.db), true); + return narrow_string_membership(db, &self.env, lhs_ty, haystack.value(db), true); } if let Type::Union(union) = rhs_ty - && union.elements(self.db).iter().any(|element| { - element - .resolve_type_alias(self.db) - .as_string_literal() - .is_some() - }) + && union + .elements(db) + .iter() + .any(|element| element.resolve_type_alias(db).as_string_literal().is_some()) { - let mut builder = UnionBuilder::new(self.db); - for element in union.elements(self.db) { + let mut builder = UnionBuilder::new(db, &self.env); + for element in union.elements(db) { builder = builder.add(self.evaluate_expr_in(lhs_ty, *element)?); } let narrowed = builder.build(); return (narrowed != lhs_ty).then_some(narrowed); } - let membership_type = elements_of(self.db, rhs_ty)?; - let iterable = membership_type.try_iterate(self.db).ok()?; + let membership_type = elements_of(db, &self.env, rhs_ty)?; + let iterable = membership_type.try_iterate(db, &self.env).ok()?; if iterable .as_fixed_length() @@ -2960,29 +3105,32 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { return Some(Type::Never); } evaluate_type_equality( - self.db, + db, + &self.env, lhs_ty, - iterable.homogeneous_element_type(self.db), + iterable.homogeneous_element_type(db, &self.env), true, self.comparison_soundness_policy(), ) } fn evaluate_expr_not_in(&self, lhs_ty: Type<'db>, rhs_ty: Type<'db>) -> Option> { - if let Some(haystack) = rhs_ty.resolve_type_alias(self.db).as_string_literal() { - return narrow_string_membership(self.db, lhs_ty, haystack.value(self.db), false); + let db = self.db; + if let Some(haystack) = rhs_ty.resolve_type_alias(db).as_string_literal() { + return narrow_string_membership(db, &self.env, lhs_ty, haystack.value(db), false); } - let membership_type = elements_of(self.db, rhs_ty)?; - let iterable = membership_type.try_iterate(self.db).ok()?; + let membership_type = elements_of(db, &self.env, rhs_ty)?; + let iterable = membership_type.try_iterate(db, &self.env).ok()?; let fixed_length = iterable.as_fixed_length()?; - let mut builder = IntersectionBuilder::new(self.db); + let mut builder = IntersectionBuilder::new(db, &self.env); let mut constrained = false; // `not in` negates equality with every element; it does not use `__ne__`. Only add an // exclusion when every value represented by a slot is known to compare equal. for element_ty in fixed_length.all_elements().iter().copied() { if let Some(constraint) = equality_exclusion_constraint( - self.db, + db, + &self.env, element_ty, self.comparison_soundness_policy(), ) { @@ -3003,6 +3151,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { rhs: &ast::Expr, inference: &ExpressionInference<'db>, ) -> Option> { + let db = self.db; let elements = match rhs.expression_value() { ast::Expr::List(list) => &list.elts, ast::Expr::Set(set) => &set.elts, @@ -3014,7 +3163,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } Some(Type::heterogeneous_tuple( - self.db, + db, + &self.env, elements .iter() .map(|element| inference.expression_type(element)), @@ -3028,9 +3178,11 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { op: ast::CmpOp, is_positive: bool, ) -> Option> { + let db = self.db; if op == ast::CmpOp::Eq { return evaluate_type_equality( - self.db, + db, + &self.env, lhs_ty, rhs_ty, is_positive, @@ -3039,7 +3191,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } if op == ast::CmpOp::NotEq { return evaluate_type_inequality( - self.db, + db, + &self.env, lhs_ty, rhs_ty, is_positive, @@ -3051,64 +3204,43 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { match op { ast::CmpOp::IsNot => { - let rhs_identity_ty = rhs_ty.identity_comparison_type(self.db); - // An `is not` check can narrow the LHS only when the RHS identifies a single - // runtime object. There are two ways this can happen: - // - // 1. The RHS's runtime identity type is itself a singleton. This includes ordinary - // singleton types, such as `None`, and distinct `NewType`s that all wrap the - // same singleton object. Narrow against the runtime identity type so that every - // static type representing that object is excluded. - // - // 2. The RHS is a constrained `TypeVar` whose constraints are all singletons. For - // example, `T = TypeVar("T", None, EllipsisType)` can be specialized to either - // `None` or `EllipsisType` across different calls. Within one specialization, - // however, every occurrence of `T` resolves to the same constraint, so all - // values of type `T` are either `None` or all are `...`. Keep `T` symbolic so - // that excluding it preserves its relationship with subsequent occurrences of - // the same specialization. - // - // In every other case, the RHS might identify multiple objects even within a - // single specialization, so excluding its entire type would be unsound. - let rhs_constraint = if rhs_identity_ty.is_singleton(self.db) { + let rhs_identity_ty = rhs_ty.identity_comparison_type(db, &self.env); + let rhs_constraint = if rhs_identity_ty.is_singleton(db, &self.env) { rhs_identity_ty - } else if matches!(rhs_ty.resolve_type_alias(self.db), Type::TypeVar(_)) - && rhs_ty.is_singleton(self.db) + } else if matches!(rhs_ty.resolve_type_alias(db), Type::TypeVar(_)) + && rhs_ty.is_singleton(db, &self.env) { rhs_ty } else { return None; }; - Some(rhs_constraint.negate(self.db)) + Some(rhs_constraint.negate(db, &self.env)) } ast::CmpOp::Is => { - // Preserve the nominal RHS constraint for ordinary overlaps. If a `NewType` - // creates additional runtime-only overlap, retain the corresponding part of the - // LHS as well so that applying the constraint does not erase that possibility. - let mut builder = UnionBuilder::new(self.db).add(rhs_ty); - let rhs_resolved = rhs_ty.resolve_type_alias(self.db); - let rhs_identity_ty = rhs_ty.identity_comparison_type(self.db); + let mut builder = UnionBuilder::new(db, &self.env).add(rhs_ty); + let rhs_resolved = rhs_ty.resolve_type_alias(db); + let rhs_identity_ty = rhs_ty.identity_comparison_type(db, &self.env); let add_runtime_overlap = |builder: UnionBuilder<'db>, element: Type<'db>| { let overlaps_only_at_runtime = |rhs_element| { - element.is_disjoint_from(self.db, rhs_element) + element.is_disjoint_from(db, &self.env, rhs_element) && element - .identity_comparison_truthiness(self.db, rhs_element) + .identity_comparison_truthiness(db, &self.env, rhs_element) .may_be_true() }; let has_runtime_only_overlap = match rhs_resolved { Type::Union(union) => union - .elements(self.db) + .elements(db) .iter() .copied() .any(overlaps_only_at_runtime), Type::TypeVar(typevar) => { - match typevar.typevar(self.db).bound_or_constraints(self.db) { + match typevar.typevar(db).bound_or_constraints(db, &self.env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { overlaps_only_at_runtime(bound) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { constraints - .elements(self.db) + .elements(db) .iter() .copied() .any(overlaps_only_at_runtime) @@ -3122,8 +3254,12 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { return builder; } - let runtime_overlap = - IntersectionType::from_two_elements(self.db, element, rhs_identity_ty); + let runtime_overlap = IntersectionType::from_two_elements( + db, + &self.env, + element, + rhs_identity_ty, + ); builder.add(if runtime_overlap.is_never() { element } else { @@ -3131,16 +3267,15 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { }) }; - if let Type::Union(union) = lhs_ty.resolve_type_alias(self.db) { + if let Type::Union(union) = lhs_ty.resolve_type_alias(db) { builder = union - .elements(self.db) + .elements(db) .iter() .copied() .fold(builder, add_runtime_overlap); } else { builder = add_runtime_overlap(builder, lhs_ty); } - Some(builder.build()) } ast::CmpOp::In => self.evaluate_expr_in(lhs_ty, rhs_ty), @@ -3183,22 +3318,28 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { /// at runtime). Similarly, we return `None` for `type[Y[int]]`, type variables /// bound to `type[Y[int]]`, and type aliases where the underlying value is a /// generic class. - fn find_underlying_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { + fn find_underlying_class<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option> { match ty { Type::ClassLiteral(class) => Some(class), Type::SubclassOf(subclass_of) => { - match subclass_of.subclass_of().with_transposed_type_var(db) { + match subclass_of.subclass_of().with_transposed_type_var(db, env) { SubclassOfInner::Class(ClassType::NonGeneric(class)) => Some(class), SubclassOfInner::Class(ClassType::Generic(_)) | SubclassOfInner::Dynamic(_) | SubclassOfInner::Protocol(_) => None, SubclassOfInner::TypeVar(tvar) => { - find_underlying_class(db, tvar.typevar(db).upper_bound(db)?) + find_underlying_class(db, env, tvar.typevar(db).upper_bound(db, env)?) } } } - Type::TypeVar(tvar) => find_underlying_class(db, tvar.typevar(db).upper_bound(db)?), - Type::TypeAlias(alias) => find_underlying_class(db, alias.value_type(db)), + Type::TypeVar(tvar) => { + find_underlying_class(db, env, tvar.typevar(db).upper_bound(db, env)?) + } + Type::TypeAlias(alias) => find_underlying_class(db, env, alias.value_type(db)), _ => None, } } @@ -3236,6 +3377,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { _ => None, } } + let db = self.db; let ast::ExprCompare { range: _, @@ -3262,7 +3404,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { return None; } - let inference = infer_expression_types(self.db, expression, TypeContext::default()); + let inference = infer_expression_types(db, expression, TypeContext::default()); let comparator_tuples = std::iter::once(&**left) .chain(comparators) @@ -3279,7 +3421,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { && let ast::Expr::Subscript(subscript) = left.expression_value() && let Type::Union(union) = inference .expression_type(&*subscript.value) - .resolve_type_alias(self.db) + .resolve_type_alias(db) && let Some(subscript_place_expr) = PlaceExpr::try_from_expr(&subscript.value) && let Some(index) = inference .expression_type(&*subscript.slice) @@ -3287,12 +3429,12 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { && let Ok(index) = i32::try_from(index) && let rhs_ty = inference.expression_type(&comparators[0]) { - let filtered = union.filter(self.db, |elem| { - elem.tuple_instance_spec(self.db) - .and_then(|spec| spec.py_index(self.db, index).ok()) + let filtered = union.filter(db, |elem| { + elem.tuple_instance_spec(db, &self.env) + .and_then(|spec| spec.py_index(db, &self.env, index).ok()) .is_none_or(|el_ty| { el_ty - .identity_comparison_truthiness(self.db, rhs_ty) + .identity_comparison_truthiness(db, &self.env, rhs_ty) .negate_if(!is_positive_check) .may_be_true() }) @@ -3325,7 +3467,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { else { return; }; - if function_type.known(self.db) != Some(KnownFunction::Len) + if function_type.known(db) != Some(KnownFunction::Len) || !call.arguments.keywords.is_empty() { return; @@ -3333,9 +3475,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let [arg] = &*call.arguments.args else { return; }; - let Some(length_literal) = length_type - .resolve_type_alias(self.db) - .as_int_like_literal() + let Some(length_literal) = length_type.resolve_type_alias(db).as_int_like_literal() else { return; }; @@ -3348,7 +3488,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let arg_type = inference.expression_type(arg); let narrowed = - Self::narrow_type_by_exact_len(self.db, arg_type, length, is_equality); + Self::narrow_type_by_exact_len(db, &self.env, arg_type, length, is_equality); if narrowed != arg_type { insert_narrowing_constraint( &mut constraints, @@ -3457,9 +3597,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { && let Some(key) = inference.expression_type(&**left).as_string_literal() && let rhs_expr = comparators[0].expression_value() && let rhs_type = inference.expression_type(&comparators[0]) - && is_or_contains_typeddict(self.db, rhs_type) + && is_or_contains_typeddict(db, rhs_type) { - let key = key.value(self.db); + let key = key.value(db); let apply_constraint = |constraints: &mut NarrowingConstraints<'db>, constraint: NarrowingConstraint<'db>| { @@ -3480,17 +3620,17 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { if is_positive == (ops[0] == ast::CmpOp::In) { let narrowed = self.narrow_with_present_key(rhs_type, key); - if narrowed != rhs_type.resolve_type_alias(self.db) { + if narrowed != rhs_type.resolve_type_alias(db) { apply_constraint(&mut constraints, NarrowingConstraint::replacement(narrowed)); } } else { let requires_key = |td: TypedDictType<'db>| -> bool { - td.items(self.db) + td.items(db) .get(key) .is_some_and(TypedDictField::is_required) }; - let resolved_rhs_type = rhs_type.resolve_type_alias(self.db); + let resolved_rhs_type = rhs_type.resolve_type_alias(db); let narrowed = match resolved_rhs_type { Type::TypedDict(td) => { @@ -3502,7 +3642,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } Type::Intersection(intersection) => { if intersection - .positive(self.db) + .positive(db) .iter() .copied() .filter_map(Type::as_typed_dict) @@ -3515,10 +3655,10 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } Type::Union(union) => { // remove all members of the union that would require the key - union.filter(self.db, |ty| match ty { + union.filter(db, |ty| match ty { Type::TypedDict(td) => !requires_key(*td), Type::Intersection(intersection) => !intersection - .positive(self.db) + .positive(db) .iter() .copied() .filter_map(Type::as_typed_dict) @@ -3561,8 +3701,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // - `if x.__class__ is y.__class__` // - `if x.__class__ is not y.__class__` let exact_class_checks = match ( - exact_class_narrowing_target(self.db, inference, left), - exact_class_narrowing_target(self.db, inference, right), + exact_class_narrowing_target(db, inference, left), + exact_class_narrowing_target(db, inference, right), ) { (Some(left_target), Some(right_target)) => { [Some((left_target, rhs_ty)), Some((right_target, lhs_ty))] @@ -3583,17 +3723,19 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { if let Some(is_positive) = is_positive && let Some(target) = PlaceExpr::try_from_expr(target_expr) - && let Some(other_class) = find_underlying_class(self.db, other) + && let Some(other_class) = + find_underlying_class(db, &self.env, other, + ) // `else`-branch narrowing for `if type(x) is Y` can only be done // if `Y` is a final class - && (is_positive || other_class.is_final(self.db)) + && (is_positive || other_class.is_final(db)) { let place = self.expect_place(&target); constraints.insert( place, NarrowingConstraint::intersection( - Type::instance(self.db, other_class.top_materialization(self.db)) - .negate_if(self.db, !is_positive), + Type::instance(db, &self.env, other_class.top_materialization(db)) + .negate_if(db, &self.env, !is_positive), ), ); } @@ -3643,7 +3785,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .or_insert(constraint); // Use the narrowed type for subsequent comparisons in a chain. - last_rhs_ty = Some(IntersectionType::from_two_elements(self.db, rhs_ty, ty)); + last_rhs_ty = Some(IntersectionType::from_two_elements( + db, &self.env, rhs_ty, ty, + )); } else { last_rhs_ty = Some(rhs_ty); } @@ -3657,7 +3801,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { expression: Expression<'db>, is_positive: bool, ) -> Option> { - let inference = infer_expression_types(self.db, expression, TypeContext::default()); + let db = self.db; + let inference = infer_expression_types(db, expression, TypeContext::default()); if let Some(type_guard_call_constraints) = self.evaluate_type_guard_call(inference, expr_call, is_positive) @@ -3675,13 +3820,15 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { Type::FunctionLiteral(function_type) if expr_call.arguments.args.len() == 1 && expr_call.arguments.keywords.is_empty() - && function_type.known(self.db) == Some(KnownFunction::Len) => + && function_type.known(db) == Some(KnownFunction::Len) => { let arg = &expr_call.arguments.args[0]; let arg_ty = inference.expression_type(arg); // Narrow only the parts of the type that are safe to narrow based on len(). - if let Some(narrowed_ty) = Self::narrow_type_by_len(self.db, arg_ty, is_positive) { + if let Some(narrowed_ty) = + Self::narrow_type_by_len(db, &self.env, arg_ty, is_positive) + { let target = PlaceExpr::try_from_expr(arg)?; let place = self.expect_place(&target); Some(NarrowingConstraints::from_iter([( @@ -3697,14 +3844,14 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { return None; }; let first_arg = PlaceExpr::try_from_expr(first_arg)?; - let function = function_type.known(self.db)?; + let function = function_type.known(db)?; let place = self.expect_place(&first_arg); if function == KnownFunction::HasAttr { let attr = inference .expression_type(second_arg) .as_string_literal()? - .value(self.db); + .value(db); if !is_identifier(attr) { return None; @@ -3712,14 +3859,19 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // Since `hasattr` only checks if an attribute is readable, // the type of the protocol member should be a read-only property that returns `object`. - let constraint = - Type::protocol_with_readonly_members(self.db, [(attr, Type::object())]); + let constraint = Type::protocol_with_readonly_members( + db, + &self.env, + [(attr, Type::object())], + ); return Some(NarrowingConstraints::from_iter([( place, - NarrowingConstraint::intersection( - constraint.negate_if(self.db, !is_positive), - ), + NarrowingConstraint::intersection(constraint.negate_if( + db, + &self.env, + !is_positive, + )), )])); } @@ -3728,13 +3880,15 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let class_info_ty = inference.expression_type(second_arg); function - .generate_constraint(self.db, class_info_ty, is_positive) + .generate_constraint(db, &self.env, class_info_ty, is_positive) .map(|constraint| { NarrowingConstraints::from_iter([( place, - NarrowingConstraint::intersection( - constraint.negate_if(self.db, !is_positive), - ), + NarrowingConstraint::intersection(constraint.negate_if( + db, + &self.env, + !is_positive, + )), )]) }) } @@ -3742,7 +3896,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { Type::ClassLiteral(class_type) if expr_call.arguments.args.len() == 1 && expr_call.arguments.keywords.is_empty() - && class_type.is_known(self.db, KnownClass::Bool) => + && class_type.is_known(db, KnownClass::Bool) => { self.evaluate_expression_node_predicate( &expr_call.arguments.args[0], @@ -3762,26 +3916,27 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { expr_call: &ast::ExprCall, is_positive: bool, ) -> Option> { + let db = self.db; let return_ty = inference.expression_type(expr_call); let place_and_constraint = match return_ty { Type::TypeIs(type_is) => { - let (_, place) = type_is.place_info(self.db)?; + let (_, place) = type_is.place_info(db)?; Some(( place, - NarrowingConstraint::intersection( - type_is - .return_type(self.db) - .negate_if(self.db, !is_positive), - ), + NarrowingConstraint::intersection(type_is.return_type(db).negate_if( + db, + &self.env, + !is_positive, + )), )) } // TypeGuard only narrows in the positive case Type::TypeGuard(type_guard) if is_positive => { - let (_, place) = type_guard.place_info(self.db)?; + let (_, place) = type_guard.place_info(db)?; Some(( place, - NarrowingConstraint::replacement(type_guard.return_type(self.db)), + NarrowingConstraint::replacement(type_guard.return_type(db)), )) } _ => None, @@ -3795,10 +3950,11 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { subject: Expression<'db>, singleton: ast::Singleton, ) -> Option> { - let subject = PlaceExpr::try_from_expr(subject.node_ref(self.db).node(self.module))?; + let db = self.db; + let subject = PlaceExpr::try_from_expr(subject.node_ref(db).node(self.module))?; let place = self.expect_place(&subject); - let ty = singleton_pattern_type(self.db, singleton).negate(self.db); + let ty = singleton_pattern_type(db, &self.env, singleton).negate(db, &self.env); Some(NarrowingConstraints::from_iter([( place, NarrowingConstraint::intersection(ty), @@ -3810,18 +3966,19 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { subject: Expression<'db>, pattern: &PatternPredicateKind<'db>, ) -> Option> { - let subject_place = PlaceExpr::try_from_expr(subject.node_ref(self.db).node(self.module))?; + let db = self.db; + let subject_place = PlaceExpr::try_from_expr(subject.node_ref(db).node(self.module))?; let place = self.expect_place(&subject_place); - let subject_ty = infer_same_file_expression_type(self.db, subject, TypeContext::default()); + let subject_ty = infer_same_file_expression_type(db, subject, TypeContext::default()); let definitely_matched = - definite_match_pattern_type_for_subject(self.db, pattern, subject_ty); + definite_match_pattern_type_for_subject(db, &self.env, pattern, subject_ty); if definitely_matched.is_never() { return None; } Some(NarrowingConstraints::from_iter([( place, - NarrowingConstraint::intersection(definitely_matched.negate(self.db)), + NarrowingConstraint::intersection(definitely_matched.negate(db, &self.env)), )])) } @@ -3831,7 +3988,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { kind: &SequencePatternPredicateKind<'db>, pattern: &PatternPredicateKind<'db>, ) -> PatternNarrowingResult<'db> { - let subject_node = subject.node_ref(self.db).node(self.module); + let db = self.db; + let subject_node = subject.node_ref(db).node(self.module); // A tuple or list expression has no place that can be narrowed as a whole. For example: // @@ -3849,8 +4007,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { return PatternNarrowingResult::Possible(None); }; - let subject_ty = infer_same_file_expression_type(self.db, subject, TypeContext::default()); - let narrowed_ty = pattern_binding_fallthrough_type(self.db, pattern, subject_ty); + let subject_ty = infer_same_file_expression_type(db, subject, TypeContext::default()); + let narrowed_ty = pattern_binding_fallthrough_type(db, &self.env, pattern, subject_ty); if narrowed_ty == subject_ty { return PatternNarrowingResult::Possible(None); } @@ -3932,6 +4090,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { pattern: &PatternPredicateKind<'db>, target: Option, ) -> PatternNarrowingResult<'db> { + let db = self.db; if let Some(elements) = Self::sequence_expression_elements(subject) { return match pattern { PatternPredicateKind::Sequence(kind) => self @@ -3968,15 +4127,14 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let Some(subject) = PlaceExpr::try_from_expr(subject_expr) else { return PatternNarrowingResult::Possible(None); }; - let subject_ty = - infer_expression_types(self.db, subject_expression, TypeContext::default()) - .expression_type(subject_expr); + let subject_ty = infer_expression_types(db, subject_expression, TypeContext::default()) + .expression_type(subject_expr); let Some(constraint) = self.positive_subject_constraint(pattern, subject_ty) else { return PatternNarrowingResult::Possible(None); }; if NarrowingConstraint::intersection(subject_ty) .merge_constraint_and(constraint.clone()) - .evaluate_constraint_type(self.db) + .evaluate_constraint_type(db, &self.env) .is_never() { return PatternNarrowingResult::Impossible; @@ -3998,13 +4156,14 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { value: Expression<'db>, is_positive: bool, ) -> Option> { - let subject_node = subject.node_ref(self.db).node(self.module); + let db = self.db; + let subject_node = subject.node_ref(db).node(self.module); let place = { let subject = PlaceExpr::try_from_expr(subject_node)?; self.expect_place(&subject) }; - let subject_ty = infer_same_file_expression_type(self.db, subject, TypeContext::default()); - let value_ty = infer_same_file_expression_type(self.db, value, TypeContext::default()); + let subject_ty = infer_same_file_expression_type(db, subject, TypeContext::default()); + let value_ty = infer_same_file_expression_type(db, value, TypeContext::default()); let mut constraints = self .evaluate_expr_compare_op(subject_ty, value_ty, ast::CmpOp::Eq, is_positive) @@ -4026,7 +4185,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // // Like in the `if` statement case, we're constraining `union` itself, not `union["tag"]`. if let ast::Expr::Subscript(subscript) = subject_node { - let inference = infer_expression_types(self.db, subject, TypeContext::default()); + let inference = infer_expression_types(db, subject, TypeContext::default()); if let Some((place, constraint)) = self.narrow_typeddict_subscript( inference.expression_type(&*subscript.value), &subscript.value, @@ -4047,7 +4206,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { constraints.insert(place, constraint); } } else if let ast::Expr::Attribute(attribute) = subject_node { - let inference = infer_expression_types(self.db, subject, TypeContext::default()); + let inference = infer_expression_types(db, subject, TypeContext::default()); if let Some((place, constraint)) = self.narrow_nominal_attribute( inference.expression_type(&*attribute.value), &attribute.value, @@ -4069,13 +4228,15 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { expression: Expression<'db>, is_positive: bool, ) -> Option> { - let inference = infer_expression_types(self.db, expression, TypeContext::default()); + let db = self.db; + let inference = infer_expression_types(db, expression, TypeContext::default()); + let env = self.env.clone(); let sub_constraints = expr_bool_op .values .iter() // filter our arms with statically known truthiness .filter(|expr| { - inference.expression_type(*expr).bool(self.db) + inference.expression_type(*expr).bool(db, &env) != match expr_bool_op.op { BoolOp::And => Truthiness::AlwaysTrue, BoolOp::Or => Truthiness::AlwaysFalse, @@ -4128,8 +4289,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { rhs_type: Type<'db>, is_equality: bool, ) -> Option<(ScopedPlaceId, NarrowingConstraint<'db>)> { + let db = self.db; // Check preconditions: we need a TypedDict, a string key, and a supported tag literal. - if !is_or_contains_typeddict(self.db, subscript_value_type) { + if !is_or_contains_typeddict(db, subscript_value_type) { return None; } let subscript_place_expr = PlaceExpr::try_from_expr(subscript_value_expr)?; @@ -4147,21 +4309,21 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { // literal type" without worrying about what other types might be present. if is_equality && !all_matching_typeddict_fields_have_literal_types( - self.db, + db, + &self.env, subscript_value_type, - key_literal.value(self.db), + key_literal.value(db), ) { return None; } - - let field_name = Name::from(key_literal.value(self.db)); + let field_name = Name::from(key_literal.value(db)); // To avoid excluding non-`TypedDict` types, our constraints are always expressed // as a negative intersection (i.e. "you're *not* this kind of `TypedDict`"). If // `is_equality` is true, the whole constraint is going to be a double // negative, i.e. "you're *not* a `TypedDict` *without* this literal field". As the // first step of building that, we negate the right hand side. - let field_type = rhs_type.negate_if(self.db, is_equality); + let field_type = rhs_type.negate_if(db, &self.env, is_equality); // Create the synthesized `TypedDict` with that (possibly negated) field. We don't // want to constrain the mutability or required-ness of the field, so the most // compatible form is not-required and read-only. @@ -4170,9 +4332,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .read_only(true) .build(); let schema = TypedDictSchema::from_iter([(field_name, field)]); - let synthesized_typeddict = TypedDictType::from_schema_items(self.db, schema); + let synthesized_typeddict = TypedDictType::from_schema_items(db, schema); // As mentioned above, the synthesized `TypedDict` is always negated. - let intersection = Type::TypedDict(synthesized_typeddict).negate(self.db); + let intersection = Type::TypedDict(synthesized_typeddict).negate(db, &self.env); let place = self.expect_place(&subscript_place_expr); Some((place, NarrowingConstraint::intersection(intersection))) } @@ -4185,7 +4347,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { subscript_key_type: Type<'db>, is_positive: bool, ) -> Option<(ScopedPlaceId, NarrowingConstraint<'db>)> { - if !is_or_contains_typeddict(self.db, subscript_value_type) { + let db = self.db; + if !is_or_contains_typeddict(db, subscript_value_type) { return None; } let subscript_place_expr = PlaceExpr::try_from_expr(subscript_value_expr)?; @@ -4200,9 +4363,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { .required(false) .read_only(true) .build(); - let schema = TypedDictSchema::from_iter([(Name::from(key_literal.value(self.db)), field)]); - let synthesized_typeddict = TypedDictType::from_schema_items(self.db, schema); - let intersection = Type::TypedDict(synthesized_typeddict).negate(self.db); + let schema = TypedDictSchema::from_iter([(Name::from(key_literal.value(db)), field)]); + let synthesized_typeddict = TypedDictType::from_schema_items(db, schema); + let intersection = Type::TypedDict(synthesized_typeddict).negate(db, &self.env); let place = self.expect_place(&subscript_place_expr); Some((place, NarrowingConstraint::intersection(intersection))) } @@ -4213,21 +4376,21 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { fn narrow_with_present_key(&self, ty: Type<'db>, key: &str) -> Type<'db> { let db = self.db; let constrain = |ty, key_presence_constraint| { - IntersectionType::from_two_elements(db, ty, key_presence_constraint) + IntersectionType::from_two_elements(db, &self.env, ty, key_presence_constraint) }; - match ty.resolve_type_alias(self.db) { - Type::Union(union) => union.map(self.db, |element| { + match ty.resolve_type_alias(db) { + Type::Union(union) => union.map(db, &self.env, |element| { self.narrow_with_present_key(*element, key) }), - resolved if typeddict_declares_key(self.db, resolved, key) => resolved, + resolved if typeddict_declares_key(db, resolved, key) => resolved, // TODO: Extend this to subtypes of `Mapping[str, object]` whose membership and // subscript operations obey the `Mapping` contract. - resolved if is_or_contains_typeddict(self.db, resolved) => constrain( + resolved if is_or_contains_typeddict(db, resolved) => constrain( ty, - Type::TypedDict(required_typeddict_key(self.db, key, Type::object())), + Type::TypedDict(required_typeddict_key(db, key, Type::object())), ), - _ => constrain(ty, key_membership_contains_protocol(self.db, key)), + _ => constrain(ty, key_membership_contains_protocol(db, &self.env, key)), } } @@ -4253,8 +4416,9 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { rhs_type: Type<'db>, is_equality: bool, ) -> Option<(ScopedPlaceId, NarrowingConstraint<'db>)> { + let db = self.db; // We need a union type for narrowing to be useful. - let Type::Union(union) = subscript_value_type.resolve_type_alias(self.db) else { + let Type::Union(union) = subscript_value_type.resolve_type_alias(db) else { return None; }; @@ -4268,30 +4432,31 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } let subscript_place_expr = PlaceExpr::try_from_expr(subscript_value_expr)?; - // Skip narrowing if any tuple in the union has an out-of-bounds index. // A diagnostic will be emitted elsewhere for the out-of-bounds access. - if any_tuple_has_out_of_bounds_index(self.db, union, index) { + if any_tuple_has_out_of_bounds_index(db, &self.env, union, index) { return None; } // For equality constraints, all matching elements must have literal types to safely narrow. // For inequality constraints, we can narrow even with non-literal element types. - if is_equality && !all_matching_tuple_elements_have_literal_types(self.db, union, index) { + if is_equality + && !all_matching_tuple_elements_have_literal_types(db, &self.env, union, index) + { return None; } // Filter the union based on whether each tuple element at the index could match the rhs. - let filtered = union.filter(self.db, |elem| { - elem.tuple_instance_spec(self.db) - .and_then(|spec| spec.py_index(self.db, index).ok()) + let filtered = union.filter(db, |elem| { + elem.tuple_instance_spec(db, &self.env) + .and_then(|spec| spec.py_index(db, &self.env, index).ok()) .is_none_or(|el_ty| { if is_equality { // Keep tuples where element could be equal to rhs. - !el_ty.is_disjoint_from(self.db, rhs_type) + !el_ty.is_disjoint_from(db, &self.env, rhs_type) } else { // Keep tuples where element is not always equal to rhs. - !el_ty.is_subtype_of(self.db, rhs_type) + !el_ty.is_subtype_of(db, &self.env, rhs_type) } }) }); @@ -4314,7 +4479,8 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { comparison: NominalAttributeComparison, is_positive: bool, ) -> Option<(ScopedPlaceId, NarrowingConstraint<'db>)> { - let Type::Union(union) = attribute_value_type.resolve_type_alias(self.db) else { + let db = self.db; + let Type::Union(union) = attribute_value_type.resolve_type_alias(db) else { return None; }; @@ -4323,22 +4489,22 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { return None; } - let narrowed = union.filter(self.db, |element| { + let narrowed = union.filter(db, |element| { element - .resolve_type_alias(self.db) - .member(self.db, attribute_name) + .resolve_type_alias(db) + .member(db, &self.env, attribute_name) .place .ignore_possibly_undefined() .is_none_or(|attribute_type| match (comparison, is_positive) { (NominalAttributeComparison::Equality, true) => { !is_supported_tag_literal(attribute_type) - || !attribute_type.is_disjoint_from(self.db, rhs_type) + || !attribute_type.is_disjoint_from(db, &self.env, rhs_type) } (NominalAttributeComparison::Equality, false) => { - !attribute_type.is_subtype_of(self.db, rhs_type) + !attribute_type.is_subtype_of(db, &self.env, rhs_type) } (NominalAttributeComparison::Identity, is_positive) => attribute_type - .identity_comparison_truthiness(self.db, rhs_type) + .identity_comparison_truthiness(db, &self.env, rhs_type) .negate_if(!is_positive) .may_be_true(), }) @@ -4360,18 +4526,19 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { attribute_name: &str, is_positive: bool, ) -> Option<(ScopedPlaceId, NarrowingConstraint<'db>)> { - let Type::Union(union) = attribute_value_type.resolve_type_alias(self.db) else { + let db = self.db; + let Type::Union(union) = attribute_value_type.resolve_type_alias(db) else { return None; }; - let narrowed = union.filter(self.db, |element| { + let narrowed = union.filter(db, |element| { element - .resolve_type_alias(self.db) - .member(self.db, attribute_name) + .resolve_type_alias(db) + .member(db, &self.env, attribute_name) .place .ignore_possibly_undefined() .is_none_or(|attribute_type| { - let truthiness = attribute_type.bool(self.db); + let truthiness = attribute_type.bool(db, &self.env); if is_positive { !truthiness.is_always_false() } else { @@ -4498,7 +4665,11 @@ fn required_typeddict_key<'db>( /// /// Non-`TypedDict` union arms therefore receive this `__contains__` protocol instead of the /// synthesized `TypedDict` used for `TypedDict` arms. -fn key_membership_contains_protocol<'db>(db: &'db dyn Db, key: &str) -> Type<'db> { +fn key_membership_contains_protocol<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + key: &str, +) -> Type<'db> { let signature = Signature::new( Parameters::standard([ Parameter::positional_only(Some(Name::new_static("self"))), @@ -4510,6 +4681,7 @@ fn key_membership_contains_protocol<'db>(db: &'db dyn Db, key: &str) -> Type<'db Type::protocol_with_methods( db, + env, [("__contains__", CallableType::function_like(db, signature))], ) } @@ -4532,6 +4704,7 @@ fn is_supported_tag_literal(ty: Type) -> bool { // supported tag literal type for that field, or a type alias to such a type. fn all_matching_typeddict_fields_have_literal_types<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, field_name: &str, ) -> bool { @@ -4549,13 +4722,17 @@ fn all_matching_typeddict_fields_have_literal_types<'db>( !is_or_contains_typeddict(db, *union_member_ty) || all_matching_typeddict_fields_have_literal_types( db, + env, *union_member_ty, field_name, ) }), - Type::TypeAlias(alias) => { - all_matching_typeddict_fields_have_literal_types(db, alias.value_type(db), field_name) - } + Type::TypeAlias(alias) => all_matching_typeddict_fields_have_literal_types( + db, + env, + alias.value_type(db), + field_name, + ), Type::Intersection(intersection) => { intersection .positive(db) @@ -4564,6 +4741,7 @@ fn all_matching_typeddict_fields_have_literal_types<'db>( !is_or_contains_typeddict(db, *intersection_member_ty) || all_matching_typeddict_fields_have_literal_types( db, + env, *intersection_member_ty, field_name, ) @@ -4603,7 +4781,7 @@ fn all_matching_typeddict_fields_have_literal_types<'db>( | Type::NewTypeInstance(_) => { unreachable!( "invalid type {} in all_matching_typeddict_fields_have_literal_types", - ty.display(db) + ty.display(db, env) ) } } @@ -4615,12 +4793,13 @@ fn all_matching_typeddict_fields_have_literal_types<'db>( /// since a diagnostic will be emitted elsewhere for the out-of-bounds access. fn any_tuple_has_out_of_bounds_index<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, union: UnionType<'db>, index: i32, ) -> bool { union.elements(db).iter().any(|elem| { - elem.tuple_instance_spec(db) - .is_some_and(|spec| spec.py_index(db, index).is_err()) + elem.tuple_instance_spec(db, env) + .is_some_and(|spec| spec.py_index(db, env, index).is_err()) }) } @@ -4632,24 +4811,38 @@ fn any_tuple_has_out_of_bounds_index<'db>( /// `__eq__` in unexpected ways. fn all_matching_tuple_elements_have_literal_types<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, union: UnionType<'db>, index: i32, ) -> bool { union.elements(db).iter().all(|elem| { - elem.tuple_instance_spec(db) - .and_then(|spec| spec.py_index(db, index).ok()) + elem.tuple_instance_spec(db, env) + .and_then(|spec| spec.py_index(db, env, index).ok()) .is_none_or(is_supported_tag_literal) }) } pub(crate) trait NarrowingEvaluatorExtension<'db> { - fn narrow(&self, db: &'db dyn Db, base_type: Type<'db>, place: ScopedPlaceId) -> Type<'db>; + fn narrow( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + base_type: Type<'db>, + place: ScopedPlaceId, + ) -> Type<'db>; } impl<'db> NarrowingEvaluatorExtension<'db> for NarrowingEvaluator<'_, 'db> { - fn narrow(&self, db: &'db dyn Db, base_type: Type<'db>, place: ScopedPlaceId) -> Type<'db> { + fn narrow( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + base_type: Type<'db>, + place: ScopedPlaceId, + ) -> Type<'db> { narrow_type_by_constraint( db, + env, self.narrowing_constraints(), self.predicates(), self.constraint(), diff --git a/crates/ty_python_semantic/src/types/narrow/containment.rs b/crates/ty_python_semantic/src/types/narrow/containment.rs index 057f40b588..8af46e2086 100644 --- a/crates/ty_python_semantic/src/types/narrow/containment.rs +++ b/crates/ty_python_semantic/src/types/narrow/containment.rs @@ -1,7 +1,6 @@ -use crate::{ - Db, - types::{ClassBase, IntersectionBuilder, KnownClass, Type, UnionBuilder}, -}; +use crate::Db; +use crate::ProgramEnvironment; +use crate::types::{ClassBase, IntersectionBuilder, KnownClass, Type, UnionBuilder}; enum ContainmentBehavior<'db> { /// Membership compares against the elements yielded by the wrapped type. Callers use @@ -14,7 +13,11 @@ enum ContainmentBehavior<'db> { } /// Return the containment behavior known for this type. -fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehavior<'db> { +fn containment_behavior<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> ContainmentBehavior<'db> { let ty = ty.resolve_type_alias(db); match ty { @@ -23,10 +26,10 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav // through wrappers such as type variables. Positive unions that contain string // literals are distributed in `evaluate_expr_in` instead because substring semantics // depend on the value of each literal haystack. - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut has_unknown_behavior = false; for element in union.elements(db) { - match containment_behavior(db, *element) { + match containment_behavior(db, env, *element) { ContainmentBehavior::ElementsOf(elements_of) => { builder = builder.add(elements_of); } @@ -40,13 +43,15 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav ContainmentBehavior::ElementsOf(builder.build()) } } - Type::TypeVar(type_var) => type_var - .typevar(db) - .bound_or_constraints(db) - .map_or(ContainmentBehavior::Unknown, |bound_or_constraints| { - containment_behavior(db, bound_or_constraints.as_type(db)) - }), - Type::NewTypeInstance(newtype) => containment_behavior(db, newtype.concrete_base_type(db)), + Type::TypeVar(type_var) => type_var.typevar(db).bound_or_constraints(db, env).map_or( + ContainmentBehavior::Unknown, + |bound_or_constraints| { + containment_behavior(db, env, bound_or_constraints.as_type(db, env)) + }, + ), + Type::NewTypeInstance(newtype) => { + containment_behavior(db, env, newtype.concrete_base_type(db)) + } Type::Intersection(intersection) => { // Preserve the narrowing already supported on main for unsimplified intersections // such as `Iterable[T] & tuple[object, ...]`. Replacing the component that establishes @@ -59,8 +64,8 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav // https://github.com/astral-sh/ruff/pull/26365 let mut has_elements_of = false; let mut has_custom_behavior = false; - let elements_of = - intersection.map_positive(db, |element| match containment_behavior(db, *element) { + let elements_of = intersection.map_positive(db, env, |element| { + match containment_behavior(db, env, *element) { ContainmentBehavior::ElementsOf(elements_of) => { has_elements_of = true; elements_of @@ -70,7 +75,8 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav *element } ContainmentBehavior::Unknown => *element, - }); + } + }); if has_custom_behavior { ContainmentBehavior::Custom } else if has_elements_of { @@ -83,7 +89,7 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav Type::NominalInstance(instance) => { // Walk the MRO until we find either a visible override or a supported built-in // implementation. - for base in instance.class(db).iter_mro(db) { + for base in instance.class(db, env).iter_mro(db) { let class = match base { ClassBase::Class(class) => class, ClassBase::Generic | ClassBase::Protocol => continue, @@ -113,16 +119,16 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav // takes precedence over `__iter__` for containment checks, but this is only // relevant to us for built-ins, since user types with `__contains__` have // containment behavior that we can't understand and don't try to model.) - return ContainmentBehavior::ElementsOf(Type::instance(db, class)); + return ContainmentBehavior::ElementsOf(Type::instance(db, env, class)); } if !class - .own_class_member(db, None, "__contains__") + .own_class_member(db, env, None, "__contains__") .is_undefined() { return ContainmentBehavior::Custom; } } - if instance.class(db).is_final(db) { + if instance.class(db, env).is_final(db) { ContainmentBehavior::ElementsOf(ty) } else { ContainmentBehavior::Unknown @@ -133,8 +139,12 @@ fn containment_behavior<'db>(db: &'db dyn Db, ty: Type<'db>) -> ContainmentBehav } /// Return the type whose iterated elements may satisfy membership for `ty`. -pub(super) fn elements_of<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option> { - match containment_behavior(db, ty) { +pub(super) fn elements_of<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option> { + match containment_behavior(db, env, ty) { ContainmentBehavior::ElementsOf(elements_of) => Some(elements_of), ContainmentBehavior::Custom | ContainmentBehavior::Unknown => None, } @@ -146,18 +156,20 @@ const MAX_STRING_MEMBERSHIP_EXCLUSIONS: usize = 128; /// Narrow membership in a known string literal using substring semantics. pub(super) fn narrow_string_membership<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, lhs_ty: Type<'db>, haystack: &str, is_contained: bool, ) -> Option> { let lhs_ty = lhs_ty.resolve_type_alias(db); - let flattened_lhs_ty = lhs_ty.flatten_typevars(db); + let flattened_lhs_ty = lhs_ty.flatten_typevars(db, env); let keep = |element: &Type<'db>| { let element = element.resolve_type_alias(db); if let Some(needle) = element.as_string_literal() { haystack.contains(needle.value(db)) == is_contained } else { - !(is_contained && element.is_disjoint_from(db, KnownClass::Str.to_instance(db))) + !(is_contained + && element.is_disjoint_from(db, env, KnownClass::Str.to_instance(db, env))) } }; @@ -173,7 +185,7 @@ pub(super) fn narrow_string_membership<'db>( .nth(MAX_STRING_MEMBERSHIP_EXCLUSIONS) .is_none() { - let mut builder = IntersectionBuilder::new(db).add_positive(narrowed); + let mut builder = IntersectionBuilder::new(db, env).add_positive(narrowed); for character in haystack.chars() { builder.add_negative_in_place(Type::single_char_string_literal(db, character)); } diff --git a/crates/ty_python_semantic/src/types/newtype.rs b/crates/ty_python_semantic/src/types/newtype.rs index 2daf6ed408..216f5b13bb 100644 --- a/crates/ty_python_semantic/src/types/newtype.rs +++ b/crates/ty_python_semantic/src/types/newtype.rs @@ -1,9 +1,10 @@ use crate::Db; +use crate::ProgramEnvironment; use crate::types::constraints::ConstraintSet; use crate::types::relation::{DisjointnessChecker, TypeRelation, TypeRelationChecker}; use crate::types::{ClassType, KnownUnion, Type, definition_expression_type, visitor}; use ruff_db::parsed::parsed_module; -use ruff_python_ast as ast; +use ruff_python_ast::{self as ast}; use rustc_hash::FxHashSet; use ty_python_core::definition::{Definition, DefinitionKind}; @@ -54,16 +55,21 @@ impl<'db> NewType<'db> { #[salsa::tracked( returns(copy), - cycle_initial=|db, _, _| NewTypeBase::ClassType(ClassType::object(db)), + cycle_initial=|db, _, self_: NewType<'db>| NewTypeBase::ClassType(ClassType::object( + db, + &ProgramEnvironment::from_definition(self_.definition(db)), + )), heap_size=ruff_memory_usage::heap_size )] fn lazy_base(self, db: &'db dyn Db) -> NewTypeBase<'db> { // `TypeInferenceBuilder` emits diagnostics for invalid `NewType` definitions that show up // in assignments, but invalid definitions still get here, and also `NewType` might show up // in places that aren't definitions at all. Fall back to `object` in all error cases. - let object_fallback = NewTypeBase::ClassType(ClassType::object(db)); let definition = self.definition(db); - let module = parsed_module(db, definition.file(db)).load(db); + let python_file = definition.python_file(db); + let env = ProgramEnvironment::from_file(python_file); + let object_fallback = NewTypeBase::ClassType(ClassType::object(db, &env)); + let module = parsed_module(db, python_file).load(db); let DefinitionKind::Assignment(assignment) = definition.kind(db) else { return object_fallback; }; @@ -75,7 +81,7 @@ impl<'db> NewType<'db> { }; match definition_expression_type(db, definition, second_arg) { Type::NominalInstance(nominal_instance_type) => { - NewTypeBase::ClassType(nominal_instance_type.class(db)) + NewTypeBase::ClassType(nominal_instance_type.class(db, &env)) } Type::NewTypeInstance(newtype) => NewTypeBase::NewType(newtype), // There are exactly two union types allowed as bases for NewType: `int | float` and @@ -105,7 +111,10 @@ impl<'db> NewType<'db> { for base in self.iter_bases(db) { match base { NewTypeBase::NewType(_) => continue, - concrete => return concrete.instance_type(db), + concrete => { + let env = ProgramEnvironment::from_definition(self.definition(db)); + return concrete.instance_type(db, &env); + } } } Type::object() @@ -182,11 +191,12 @@ impl<'db> NewType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let eager_base = match self.eager_base(db) { - Some(base) => Some(base.recursive_type_normalized_impl(db, div, nested)?), + Some(base) => Some(base.recursive_type_normalized_impl(db, env, div, nested)?), None => None, }; @@ -233,6 +243,7 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { // Two NewTypes are disjoint if they're not equal and neither inherits from the other. // NewTypes have single inheritance, and a regular class can't inherit from a NewType, so // it's not possible for some third type to multiply-inherit from both. + let relation_checker = self.as_relation_checker(TypeRelation::Subtyping); relation_checker .check_newtype_pair(db, left, right) @@ -254,7 +265,7 @@ pub(crate) fn walk_newtype_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Si newtype.eager_base(db) }; if let Some(base) = base { - visitor.visit_type(db, base.instance_type(db)); + visitor.visit_type(db, base.instance_type(db, visitor.program_environment())); } } @@ -272,27 +283,28 @@ pub enum NewTypeBase<'db> { } impl<'db> NewTypeBase<'db> { - pub fn instance_type(self, db: &'db dyn Db) -> Type<'db> { + pub fn instance_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { - NewTypeBase::ClassType(class_type) => Type::instance(db, class_type), + NewTypeBase::ClassType(class_type) => Type::instance(db, env, class_type), NewTypeBase::NewType(newtype) => Type::NewTypeInstance(newtype), - NewTypeBase::Float => KnownUnion::Float.to_type(db), - NewTypeBase::Complex => KnownUnion::Complex.to_type(db), + NewTypeBase::Float => KnownUnion::Float.to_type(db, env), + NewTypeBase::Complex => KnownUnion::Complex.to_type(db, env), } } fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { NewTypeBase::ClassType(class_type) => class_type - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(NewTypeBase::ClassType), NewTypeBase::NewType(newtype) => newtype - .recursive_type_normalized_impl(db, div, nested) + .recursive_type_normalized_impl(db, env, div, nested) .map(NewTypeBase::NewType), NewTypeBase::Float | NewTypeBase::Complex => Some(self), } diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index 209919b9d7..458b3a87e1 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -14,7 +14,7 @@ use ruff_python_stdlib::identifiers::is_mangled_private; use rustc_hash::FxHashSet; use crate::{ - Db, Program, + Db, ProgramEnvironment, lint::LintId, place::{DefinedPlace, Place, PlaceAndQualifiers, TypeOrigin}, reachability::ReachabilityConstraintsExtension, @@ -85,7 +85,6 @@ pub(super) fn check_class<'db>( if configuration.check_method_liskov_violations() && !inconsistent_generic_bases { check_inherited_method_conflicts(context, class, class_specialized, &own_class_members); } - let enum_info = enum_metadata(db, class.into()); #[expect( @@ -129,10 +128,11 @@ fn check_inherited_method_conflicts<'db>( own_class_members: &FxHashSet>, ) { let db = context.db(); + let env = &context.program_environment(); let mut direct_bases = Vec::new(); for base in class.explicit_bases(db) { - match ClassBase::try_from_explicit_base(db, *base, Some(class.into())) { + match ClassBase::try_from_explicit_base(db, env, *base, Some(class.into())) { Some(ClassBase::Class(base)) if base.static_class_literal(db).is_some() => { direct_bases.push(base); } @@ -154,7 +154,7 @@ fn check_inherited_method_conflicts<'db>( if direct_bases.iter().enumerate().any(|(index, left)| { direct_bases[index + 1..] .iter() - .any(|right| !left.could_coexist_in_mro_with(db, *right, &constraints)) + .any(|right| !left.could_coexist_in_mro_with(db, env, *right, &constraints)) }) { return; } @@ -172,7 +172,7 @@ fn check_inherited_method_conflicts<'db>( ClassBase::TypedDict(_) | ClassBase::Class(_) => return, } } - let receiver = Type::instance(db, class_specialized); + let receiver = Type::instance(db, env, class_specialized); let mut seen_names: FxHashSet<_> = own_class_members .iter() .map(|member| member.member.name.clone()) @@ -216,24 +216,24 @@ fn check_inherited_method_conflicts<'db>( continue; } let Some((selected_decorator, selected_ty)) = - source_method_contract(db, owner, receiver, name) + source_method_contract(db, env, owner, receiver, name) else { continue; }; for contract_owner in mro[index + 1..].iter().copied() { let Some((contract_decorator, contract_ty)) = - source_method_contract(db, contract_owner, receiver, name) + source_method_contract(db, env, contract_owner, receiver, name) else { continue; }; let Some((selected_ty, contract_ty)) = - method_override_types(db, selected_ty, contract_ty) + method_override_types(db, env, selected_ty, contract_ty) else { continue; }; if selected_decorator == contract_decorator - && selected_ty.is_assignable_to(db, contract_ty) + && selected_ty.is_assignable_to(db, env, contract_ty) { continue; } @@ -270,19 +270,23 @@ fn check_inherited_method_conflicts<'db>( .filter_map(ClassBase::into_class) .find(|ancestor| ancestor.class_literal(db) == contract_owner.class_literal(db)) { - let parent_receiver = Type::instance(db, owner); + let parent_receiver = Type::instance(db, env, owner); let Some((parent_decorator, parent_ty)) = - source_method_contract(db, owner, parent_receiver, name) + source_method_contract(db, env, owner, parent_receiver, name) else { continue; }; - let Some((ancestor_decorator, ancestor_ty)) = - source_method_contract(db, parent_contract_owner, parent_receiver, name) - else { + let Some((ancestor_decorator, ancestor_ty)) = source_method_contract( + db, + env, + parent_contract_owner, + parent_receiver, + name, + ) else { continue; }; if parent_decorator != ancestor_decorator - || !is_assignable_method_override(db, parent_ty, ancestor_ty) + || !is_assignable_method_override(db, env, parent_ty, ancestor_ty) { continue; } @@ -306,7 +310,7 @@ fn check_inherited_method_conflicts<'db>( name, (owner, member.first_reachable_definition, selected_decorator), (contract_owner, contract_definition, contract_decorator), - || selected_ty.assignability_error_context(db, contract_ty), + || selected_ty.assignability_error_context(db, env, contract_ty), ); continue 'members; } @@ -317,6 +321,7 @@ fn check_inherited_method_conflicts<'db>( /// Returns a source-defined method bound to the class whose MRO is being checked. fn source_method_contract<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, owner: ClassType<'db>, receiver: Type<'db>, name: &Name, @@ -335,7 +340,7 @@ fn source_method_contract<'db>( // class Conflict(ReturnsStr, ReturnsInt): ... // ``` let Type::FunctionLiteral(function) = owner - .own_class_member(db, None, name) + .own_class_member(db, env, None, name) .inner .place .raw_type()? @@ -343,7 +348,7 @@ fn source_method_contract<'db>( return None; }; let ty = Type::FunctionLiteral(function) - .try_call_dunder_get(db, Some(receiver), receiver.to_meta_type(db))? + .try_call_dunder_get(db, env, Some(receiver), receiver.to_meta_type(db, env))? .0; Some((MethodDecorator::try_from_fn_type(db, function)?, ty)) } @@ -360,7 +365,8 @@ fn enum_class_creation_manages_conflict<'db>( selected_owner: ClassType<'db>, contract_owner: ClassType<'db>, ) -> bool { - if !is_enum_class_by_inheritance(db, class) { + let env = ProgramEnvironment::from_scope(class.body_scope(db)); + if !is_enum_class_by_inheritance(db, &env, class) { return false; } @@ -372,8 +378,12 @@ fn enum_class_creation_manages_conflict<'db>( || contract_owner.is_known(db, KnownClass::Enum); } - Program::get(db).python_version(db) >= PythonVersion::PY311 - && Type::ClassLiteral(class.into()).is_subtype_of(db, KnownClass::Flag.to_subclass_of(db)) + env.python_version(db) >= PythonVersion::PY311 + && Type::ClassLiteral(class.into()).is_subtype_of( + db, + &env, + KnownClass::Flag.to_subclass_of(db, &env), + ) && matches!( name.as_str(), "__or__" | "__and__" | "__xor__" | "__ror__" | "__rand__" | "__rxor__" | "__invert__" @@ -430,15 +440,16 @@ fn check_class_declaration<'db>( member: &MemberWithDefinition<'db>, ) { let db = context.db(); + let env = &context.program_environment(); let MemberWithDefinition { member, first_reachable_definition, } = member; - let instance_of_class = Type::instance(db, class); + let instance_of_class = Type::instance(db, env, class); - let subclass_instance_member = instance_of_class.member(db, &member.name); + let subclass_instance_member = instance_of_class.member(db, env, &member.name); let Place::Defined(DefinedPlace { ty: type_on_subclass_instance, .. @@ -580,7 +591,7 @@ fn check_class_declaration<'db>( .can_validate_with_value_annotation() && let Some(expected_type) = enum_info.value_annotation_type() { - if !member_value_type.is_assignable_to(db, expected_type) { + if !member_value_type.is_assignable_to(db, env, expected_type) { if let Some(builder) = context.report_lint( &INVALID_ASSIGNMENT, first_reachable_definition.focus_range(db, context.module()), @@ -591,8 +602,8 @@ fn check_class_declaration<'db>( )); diagnostic.info(format_args!( "Expected `{}`, got `{}`", - expected_type.display(db), - member_value_type.display(db) + expected_type.display(db, env), + member_value_type.display(db, env) )); } } @@ -655,7 +666,7 @@ fn check_class_declaration<'db>( } } else { if superclass_literal - .own_synthesized_member(db, superclass_specialization, None, &member.name) + .own_synthesized_member(db, env, superclass_specialization, None, &member.name) .is_none() { continue; @@ -666,7 +677,7 @@ fn check_class_declaration<'db>( } let superclass_instance_member = - Type::instance(db, superclass).member(db, &member.name); + Type::instance(db, env, superclass).member(db, env, &member.name); let Place::Defined(DefinedPlace { ty: superclass_type, .. @@ -702,7 +713,7 @@ fn check_class_declaration<'db>( || (configuration.check_final_variable_overridden() && overridden_final_variable.is_none()) { - let own_class_member = superclass.own_class_member(db, None, &member.name); + let own_class_member = superclass.own_class_member(db, env, None, &member.name); if configuration.check_final_method_overridden() { overridden_final_method = overridden_final_method.or_else(|| { @@ -777,7 +788,8 @@ fn check_class_declaration<'db>( let subclass_kind = *subclass_variable_kind.get_or_insert_with(|| { variable_kind( db, - class.own_class_member(db, None, &member.name).inner, + env, + class.own_class_member(db, env, None, &member.name).inner, subclass_instance_member, ) }); @@ -803,7 +815,7 @@ fn check_class_declaration<'db>( if let Some((immediate_parent, immediate_parent_kind)) = immediate_parent_variable_kind && immediate_parent != superclass - && immediate_parent.is_subclass_of(db, superclass) + && immediate_parent.is_subclass_of(db, env, superclass) && immediate_parent_kind != superclass_variable_kind { continue; @@ -847,12 +859,12 @@ fn check_class_declaration<'db>( } let Some((subclass_override_type, superclass_override_type)) = - method_override_types(db, type_on_subclass_instance, superclass_type) + method_override_types(db, env, type_on_subclass_instance, superclass_type) else { continue; }; - if subclass_override_type.is_assignable_to(db, superclass_override_type) { + if subclass_override_type.is_assignable_to(db, env, superclass_override_type) { continue; } @@ -866,7 +878,12 @@ fn check_class_declaration<'db>( // The immediate parent already defines this method and is different from the // current ancestor we're checking. Check if the immediate parent's method // is also incompatible with this ancestor. - if !is_assignable_method_override(db, immediate_parent_type, superclass_type) { + if !is_assignable_method_override( + db, + env, + immediate_parent_type, + superclass_type, + ) { // The immediate parent already has an LSP violation with this ancestor. // Don't report the same violation for the child. continue; @@ -883,7 +900,13 @@ fn check_class_declaration<'db>( superclass, superclass_type, method_kind, - || subclass_override_type.assignability_error_context(db, superclass_override_type), + || { + subclass_override_type.assignability_error_context( + db, + env, + superclass_override_type, + ) + }, ); liskov_diagnostic_emitted = true; @@ -893,8 +916,8 @@ fn check_class_declaration<'db>( if !subclass_overrides_superclass_declaration && !has_dynamic_superclass { if has_typeddict_in_mro { if !KnownClass::TypedDictFallback - .to_instance(db) - .member(db, &member.name) + .to_instance(db, env) + .member(db, env, &member.name) .place .is_undefined() { @@ -902,8 +925,8 @@ fn check_class_declaration<'db>( } } else if class_kind == Some(CodeGeneratorKind::NamedTuple) { if !KnownClass::NamedTupleFallback - .to_instance(db) - .member(db, &member.name) + .to_instance(db, env) + .member(db, env, &member.name) .place .is_undefined() { @@ -970,16 +993,18 @@ fn check_class_declaration<'db>( /// ``` fn is_assignable_method_override<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, subclass_type: Type<'db>, superclass_type: Type<'db>, ) -> bool { - method_override_types(db, subclass_type, superclass_type).is_some_and( - |(subclass_type, superclass_type)| subclass_type.is_assignable_to(db, superclass_type), + method_override_types(db, env, subclass_type, superclass_type).is_some_and( + |(subclass_type, superclass_type)| subclass_type.is_assignable_to(db, env, superclass_type), ) } fn method_override_types<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, subclass_type: Type<'db>, superclass_type: Type<'db>, ) -> Option<(Type<'db>, Type<'db>)> { @@ -1000,19 +1025,22 @@ fn method_override_types<'db>( receiver.map_or((subclass_type, superclass_type), |receiver| { let typing_self_type = subclass_method.typing_self_type(db); - let receiver = receiver.bind_self_typevars(db, typing_self_type); + let receiver = receiver.bind_self_typevars(db, env, typing_self_type); let receiver = IntersectionType::from_elements( db, + env, [subclass_method.self_instance(db), receiver], ); ( Type::Callable(subclass_method.into_callable_type_with_receiver( db, + env, receiver, typing_self_type, )), Type::Callable(superclass_method.into_callable_type_with_receiver( db, + env, receiver, typing_self_type, )), @@ -1021,9 +1049,9 @@ fn method_override_types<'db>( } _ => (subclass_type, superclass_type), }; - let superclass_callable = superclass_type.try_upcast_to_callable(db)?; + let superclass_callable = superclass_type.try_upcast_to_callable(db, env)?; - Some((subclass_type, superclass_callable.into_type(db))) + Some((subclass_type, superclass_callable.into_type(db, env))) } /// Whether an attribute declaration is a class variable or an instance variable. @@ -1069,7 +1097,8 @@ fn superclass_variable_kind<'db>( return None; } - variable_kind(db, class_member, instance_member) + let env = ProgramEnvironment::from_scope(superclass_scope); + variable_kind(db, &env, class_member, instance_member) } /// Returns the variable kind for a superclass member, preserving inherited `ClassVar` declarations @@ -1097,6 +1126,7 @@ fn effective_superclass_variable_kind<'db>( superclass: ClassType<'db>, name: Name, ) -> Option { + let env = &ProgramEnvironment::from_file(superclass.class_literal(db).python_file(db)); let inherited_variable_kind = || { superclass .iter_mro(db) @@ -1115,7 +1145,7 @@ fn effective_superclass_variable_kind<'db>( superclass_symbol.is_bound() || superclass_symbol.is_declared() } else { superclass_literal - .own_synthesized_member(db, superclass_specialization, None, &name) + .own_synthesized_member(db, env, superclass_specialization, None, &name) .is_some() }; @@ -1124,8 +1154,8 @@ fn effective_superclass_variable_kind<'db>( db, superclass_scope, superclass_symbol_id, - superclass.own_class_member(db, None, &name).inner, - Type::instance(db, superclass).member(db, &name), + superclass.own_class_member(db, env, None, &name).inner, + Type::instance(db, env, superclass).member(db, env, &name), ); if superclass_variable_kind == Some(VariableKind::Instance) @@ -1188,6 +1218,7 @@ fn is_function_definition<'db>( /// Returns the variable kind for an attribute if it should participate in `ClassVar` override checks. fn variable_kind<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, class_member: PlaceAndQualifiers<'db>, instance_member: PlaceAndQualifiers<'db>, ) -> Option { @@ -1235,7 +1266,7 @@ fn variable_kind<'db>( .. }) = class_member.place && class_member_ty - .class_member(db, "__get__") + .class_member(db, env, "__get__") .place .ignore_possibly_undefined() .is_some() @@ -1553,11 +1584,12 @@ fn check_missing_overrides<'db>( "Method `{}` overrides `{superclass_member}` but is not decorated with `@override`", member.name )); - let override_module = if Program::get(db).python_version(db) >= PythonVersion::PY312 { - "typing" - } else { - "typing_extensions" - }; + let override_module = + if context.program_environment().python_version(db) >= PythonVersion::PY312 { + "typing" + } else { + "typing_extensions" + }; diagnostic.info(format_args!( "Decorate the method with `@{override_module}.override` to make the override explicit" )); @@ -1613,7 +1645,6 @@ fn extract_local_override_definitions<'db>( extract_member_functions_from_type(db, member.ty, &member.name, subclass_scope); let mut candidates = smallvec::smallvec![]; let mut seen_function_types = smallvec::SmallVec::<[FunctionType<'db>; 1]>::new(); - for definition in end_of_scope_function_definitions(db, subclass_scope, &member.name) { let function = member_functions .iter() @@ -1661,7 +1692,6 @@ fn end_of_scope_function_definitions<'db>( let use_def = use_def_map(db, subclass_scope); let predicates = use_def.predicates(); let reachability_constraints = use_def.reachability_constraints(); - use_def .end_of_scope_symbol_bindings(symbol_id) .filter_map(|binding| { @@ -1728,7 +1758,7 @@ fn is_local_member_function<'db>( member_name: &Name, member_scope: ScopeId<'db>, ) -> bool { - function.file(db) == member_scope.file(db) + function.python_file(db) == member_scope.python_file(db) && function.definition(db).scope(db) == member_scope && function.name(db) == member_name } @@ -1789,6 +1819,7 @@ fn check_post_init_signature<'db>( let Some((static_class, spec)) = class.static_class_literal(db) else { return; }; + let env = &context.program_environment(); let init_var_fields = static_class .fields(db, spec, policy) @@ -1804,7 +1835,7 @@ fn check_post_init_signature<'db>( }); let first_parameter = Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(Type::instance(db, class)); + .with_annotated_type(Type::instance(db, env, class)); let following_parameters = init_var_fields.map(|(name, field)| { Parameter::positional_only(Some(name.clone())).with_annotated_type(field.declared_ty) @@ -1817,7 +1848,7 @@ fn check_post_init_signature<'db>( if member .ty - .is_assignable_to(db, Type::Callable(expected_signature)) + .is_assignable_to(db, env, Type::Callable(expected_signature)) { return; } @@ -1870,12 +1901,13 @@ fn check_enum_member_against_constructor_method<'db>( method: EnumConstructorMethod, ) { let db = context.db(); + let env = &context.program_environment(); // The enum metaclass unpacks tuple values as positional args: // MEMBER = (a, b, c) → __new__(cls, a, b, c) / __init__(self, a, b, c) // MEMBER = x → __new__(cls, x) / __init__(self, x) let args: Vec> = if let Type::NominalInstance(instance) = member_value_type - && let Some(spec) = instance.tuple_spec(db) + && let Some(spec) = instance.tuple_spec(db, env) { if let Tuple::Fixed(fixed) = &*spec { fixed.all_elements().to_vec() @@ -1892,9 +1924,16 @@ fn check_enum_member_against_constructor_method<'db>( let constraints = ConstraintSetBuilder::new(); let result = Type::FunctionLiteral(function) - .bindings(db) - .match_parameters(db, &call_args) - .check_types(db, &constraints, &call_args, TypeContext::default(), &[]); + .bindings(db, env) + .match_parameters(db, env, &call_args) + .check_types( + db, + env, + &constraints, + &call_args, + TypeContext::default(), + &[], + ); if result.is_err() { if let Some(builder) = context.report_lint( @@ -1907,7 +1946,7 @@ fn check_enum_member_against_constructor_method<'db>( )); diagnostic.info(format_args!( "Expected compatible arguments for `{}`", - Type::FunctionLiteral(function).display(db), + Type::FunctionLiteral(function).display(db, env), )); } } diff --git a/crates/ty_python_semantic/src/types/property_tests.rs b/crates/ty_python_semantic/src/types/property_tests.rs index c24231235c..04e9a744f0 100644 --- a/crates/ty_python_semantic/src/types/property_tests.rs +++ b/crates/ty_python_semantic/src/types/property_tests.rs @@ -30,8 +30,8 @@ use type_generation::{intersection, union}; /// A macro to define a property test for types. /// -/// The `$test_name` identifier specifies the name of the test function. The `$db` identifier -/// is used to refer to the salsa database in the property to be tested. The actual property is +/// The `$test_name` identifier specifies the name of the test function. The `$env` identifier +/// is used to refer to the semantic context in the property to be tested. The actual property is /// specified using the syntax: /// /// forall types t1, t2, ..., tn . ` @@ -39,34 +39,36 @@ use type_generation::{intersection, union}; /// where `t1`, `t2`, ..., `tn` are identifiers that represent arbitrary types, and `` /// is an expression using these identifiers. macro_rules! type_property_test { - ($test_name:ident, $db:ident, forall types $($types:ident),+ . $property:expr) => { + ($test_name:ident, $db:ident, $env:ident, forall types $($types:ident),+ . $property:expr) => { #[quickcheck_macros::quickcheck] #[ignore] - fn $test_name($($types: crate::types::property_tests::type_generation::Ty),+) -> bool { - let $db = &crate::types::property_tests::setup::get_cached_db(); - $(let $types = $types.into_type($db);)+ + fn $test_name($($types: Ty),+) -> bool { + let $db = &get_cached_db(); + let $env = &$db.program_environment(); + $(let $types = $types.into_type($db, $env);)+ let result = $property; if !result { println!("\nFailing types were:"); - $(println!("{}", $types.display($db));)+ + $(println!("{}", $types.display($db, $env));)+ } result } }; - ($test_name:ident, $db:ident, forall fully_static_types $($types:ident),+ . $property:expr) => { + ($test_name:ident, $db:ident, $env:ident, forall fully_static_types $($types:ident),+ . $property:expr) => { #[quickcheck_macros::quickcheck] #[ignore] - fn $test_name($($types: crate::types::property_tests::type_generation::FullyStaticTy),+) -> bool { - let $db = &crate::types::property_tests::setup::get_cached_db(); - $(let $types = $types.into_type($db);)+ + fn $test_name($($types: FullyStaticTy),+) -> bool { + let $db = &get_cached_db(); + let $env = &$db.program_environment(); + $(let $types = $types.into_type($db, $env);)+ let result = $property; if !result { println!("\nFailing types were:"); - $(println!("{}", $types.display($db));)+ + $(println!("{}", $types.display($db, $env));)+ } result @@ -74,151 +76,155 @@ macro_rules! type_property_test { }; // A property test with a logical implication. - ($name:ident, $db:ident, forall $typekind:ident $($types:ident),+ . $premise:expr => $conclusion:expr) => { - type_property_test!($name, $db, forall $typekind $($types),+ . !($premise) || ($conclusion)); + ($name:ident, $db:ident, $env:ident, forall $typekind:ident $($types:ident),+ . $premise:expr => $conclusion:expr) => { + type_property_test!($name, $db, $env, forall $typekind $($types),+ . !($premise) || ($conclusion)); }; } mod stable { - use super::union; + use super::{ + setup::get_cached_db, + type_generation::{FullyStaticTy, Ty}, + union, + }; use crate::types::{CallableType, IntersectionBuilder, KnownClass, Type}; // Reflexivity: `T` is equivalent to itself. type_property_test!( - equivalent_to_is_reflexive, db, - forall types t. t.is_equivalent_to(db, t) + equivalent_to_is_reflexive, db, env, + forall types t. t.is_equivalent_to(db, env, t) ); // Symmetry: If `S` is equivalent to `T`, then `T` must be equivalent to `S`. type_property_test!( - equivalent_to_is_symmetric, db, - forall types s, t. s.is_equivalent_to(db, t) => t.is_equivalent_to(db, s) + equivalent_to_is_symmetric, db, env, + forall types s, t. s.is_equivalent_to(db, env, t) => t.is_equivalent_to(db, env, s) ); // Transitivity: If `S` is equivalent to `T` and `T` is equivalent to `U`, then `S` must be equivalent to `U`. type_property_test!( - equivalent_to_is_transitive, db, - forall types s, t, u. s.is_equivalent_to(db, t) && t.is_equivalent_to(db, u) => s.is_equivalent_to(db, u) + equivalent_to_is_transitive, db, env, + forall types s, t, u. s.is_equivalent_to(db, env, t) && t.is_equivalent_to(db, env, u) => s.is_equivalent_to(db, env, u) ); // `S <: T` and `T <: U` implies that `S <: U`. type_property_test!( - subtype_of_is_transitive, db, - forall types s, t, u. s.is_subtype_of(db, t) && t.is_subtype_of(db, u) => s.is_subtype_of(db, u) + subtype_of_is_transitive, db, env, + forall types s, t, u. s.is_subtype_of(db, env, t) && t.is_subtype_of(db, env, u) => s.is_subtype_of(db, env, u) ); // `S <: T` and `T <: S` implies that `S` is equivalent to `T`. type_property_test!( - subtype_of_is_antisymmetric, db, - forall types s, t. s.is_subtype_of(db, t) && t.is_subtype_of(db, s) => s.is_equivalent_to(db, t) + subtype_of_is_antisymmetric, db, env, + forall types s, t. s.is_subtype_of(db, env, t) && t.is_subtype_of(db, env, s) => s.is_equivalent_to(db, env, t) ); type_property_test!( - structural_negation_subtyping_matches_materialized_negation, db, + structural_negation_subtyping_matches_materialized_negation, db, env, forall types s, t. { let mut cache = None; - s.negation_is_subtype_of_cached(db, t, &mut cache) == s.negate(db).is_subtype_of(db, t) + s.negation_is_subtype_of_cached(db, env, t, &mut cache) == s.negate(db, env).is_subtype_of(db, env, t) } ); // `T` is not disjoint from itself, unless `T` is `Never`. type_property_test!( - disjoint_from_is_irreflexive, db, - forall types t. t.is_disjoint_from(db, t) => t.is_never() + disjoint_from_is_irreflexive, db, env, + forall types t. t.is_disjoint_from(db, env, t) => t.is_never() ); // `S` is disjoint from `T` implies that `T` is disjoint from `S`. type_property_test!( - disjoint_from_is_symmetric, db, - forall types s, t. s.is_disjoint_from(db, t) == t.is_disjoint_from(db, s) + disjoint_from_is_symmetric, db, env, + forall types s, t. s.is_disjoint_from(db, env, t) == t.is_disjoint_from(db, env, s) ); // `S <: T` implies that `S` is not disjoint from `T`, unless `S` is `Never`. type_property_test!( - subtype_of_implies_not_disjoint_from, db, - forall types s, t. s.is_subtype_of(db, t) => !s.is_disjoint_from(db, t) || s.is_never() + subtype_of_implies_not_disjoint_from, db, env, + forall types s, t. s.is_subtype_of(db, env, t) => !s.is_disjoint_from(db, env, t) || s.is_never() ); // `S <: T` implies that `S` can be assigned to `T`. type_property_test!( - subtype_of_implies_assignable_to, db, - forall types s, t. s.is_subtype_of(db, t) => s.is_assignable_to(db, t) + subtype_of_implies_assignable_to, db, env, + forall types s, t. s.is_subtype_of(db, env, t) => s.is_assignable_to(db, env, t) ); // All types should be assignable to `object` type_property_test!( - all_types_assignable_to_object, db, - forall types t. t.is_assignable_to(db, Type::object()) + all_types_assignable_to_object, db, env, + forall types t. t.is_assignable_to(db, env, Type::object()) ); // And all types should be subtypes of `object` type_property_test!( - all_types_subtype_of_object, db, - forall types t. t.is_subtype_of(db, Type::object()) + all_types_subtype_of_object, db, env, + forall types t. t.is_subtype_of(db, env, Type::object()) ); // Never should be assignable to every type type_property_test!( - never_assignable_to_every_type, db, - forall types t. Type::Never.is_assignable_to(db, t) + never_assignable_to_every_type, db, env, + forall types t. Type::Never.is_assignable_to(db, env, t) ); // And it should be a subtype of all types type_property_test!( - never_subtype_of_every_type, db, - forall types t. Type::Never.is_subtype_of(db, t) + never_subtype_of_every_type, db, env, + forall types t. Type::Never.is_subtype_of(db, env, t) ); // Similar to `Never`, a "bottom" callable type should be a subtype of all callable types type_property_test!( - bottom_callable_is_subtype_of_all_callable, db, + bottom_callable_is_subtype_of_all_callable, db, env, forall types t. t.is_callable_type() - => Type::Callable(CallableType::bottom(db)).is_subtype_of(db, t) + => Type::Callable(CallableType::bottom(db)).is_subtype_of(db, env, t) ); // `T` can be assigned to itself. type_property_test!( - assignable_to_is_reflexive, db, - forall types t. t.is_assignable_to(db, t) + assignable_to_is_reflexive, db, env, + forall types t. t.is_assignable_to(db, env, t) ); // For *any* pair of types, each of the pair should be assignable to the union of the two. type_property_test!( - all_type_pairs_are_assignable_to_their_union, db, - forall types s, t. s.is_assignable_to(db, union(db, [s, t])) && t.is_assignable_to(db, union(db, [s, t])) + all_type_pairs_are_assignable_to_their_union, db, env, + forall types s, t. s.is_assignable_to(db, env, union(db, env, [s, t])) && t.is_assignable_to(db, env, union(db, env, [s, t])) ); // Only `Never` is a subtype of `Any`. type_property_test!( - only_never_is_subtype_of_any, db, - forall types s. !s.is_equivalent_to(db, Type::Never) => !s.is_subtype_of(db, Type::any()) + only_never_is_subtype_of_any, db, env, + forall types s. !s.is_equivalent_to(db, env, Type::Never) => !s.is_subtype_of(db, env, Type::any()) ); // Only `object` is a supertype of `Any`. type_property_test!( - only_object_is_supertype_of_any, db, - forall types t. !t.is_equivalent_to(db, Type::object()) => !Type::any().is_subtype_of(db, t) + only_object_is_supertype_of_any, db, env, + forall types t. !t.is_equivalent_to(db, env, Type::object()) => !Type::any().is_subtype_of(db, env, t) ); // Equivalence is commutative. type_property_test!( - equivalent_to_is_commutative, db, - forall types s, t. s.is_equivalent_to(db, t) == t.is_equivalent_to(db, s) + equivalent_to_is_commutative, db, env, + forall types s, t. s.is_equivalent_to(db, env, t) == t.is_equivalent_to(db, env, s) ); // A fully static type `T` is a subtype of itself. (This is not true for non-fully-static // types; `Any` is not a subtype of `Any`, only `Never` is.) type_property_test!( - subtype_of_is_reflexive_for_fully_static_types, db, - forall fully_static_types t. t.is_subtype_of(db, t) + subtype_of_is_reflexive_for_fully_static_types, db, env, + forall fully_static_types t. t.is_subtype_of(db, env, t) ); // For any two fully static types, each type in the pair must be a subtype of their union. // (This is clearly not true for non-fully-static types, since their subtyping is not // reflexive.) type_property_test!( - all_fully_static_type_pairs_are_subtype_of_their_union, db, - forall fully_static_types s, t. s.is_subtype_of(db, union(db, [s, t])) && t.is_subtype_of(db, union(db, [s, t])) + all_fully_static_type_pairs_are_subtype_of_their_union, db, env, + forall fully_static_types s, t. s.is_subtype_of(db, env, union(db, env, [s, t])) && t.is_subtype_of(db, env, union(db, env, [s, t])) ); // Any type assignable to `Iterable[object]` should be considered iterable. @@ -236,15 +242,15 @@ mod stable { // the Liskov violation). All you need to do is to create a class that subclasses // `Iterable` but assigns `__iter__ = None` in the class body (or similar). type_property_test!( - all_types_assignable_to_iterable_are_iterable, db, - forall types t. t.is_assignable_to(db, KnownClass::Iterable.to_specialized_instance(db, &[Type::object()])) => t.try_iterate(db).is_ok() + all_types_assignable_to_iterable_are_iterable, db, env, + forall types t. t.is_assignable_to(db, env, KnownClass::Iterable.to_specialized_instance(db, env, &[Type::object()])) => t.try_iterate(db, env).is_ok() ); // Our optimized `Type::negate()` function should always produce the exact same type // as going "the long way" via the `IntersectionBuilder`. type_property_test!( - all_negated_types_identical_to_intersection_with_single_negated_element, db, - forall types t. t.negate(db) == IntersectionBuilder::new(db).add_negative(t).build() + all_negated_types_identical_to_intersection_with_single_negated_element, db, env, + forall types t. t.negate(db, env) == IntersectionBuilder::new(db, env).add_negative(t).build() ); } @@ -258,68 +264,73 @@ mod stable { mod flaky { use itertools::Itertools; - use super::{intersection, union}; + use super::{ + intersection, + setup::get_cached_db, + type_generation::{FullyStaticTy, Ty}, + union, + }; // Negating `T` twice is equivalent to `T`. type_property_test!( - double_negation_is_identity, db, - forall types t. t.negate(db).negate(db).is_equivalent_to(db, t) + double_negation_is_identity, db, env, + forall types t. t.negate(db, env).negate(db, env).is_equivalent_to(db, env, t) ); // For any fully static type `T`, `T` should be disjoint from `~T`. // https://github.com/astral-sh/ty/issues/216 type_property_test!( - negation_of_fully_static_types_is_disjoint, db, - forall fully_static_types t. t.negate(db).is_disjoint_from(db, t) + negation_of_fully_static_types_is_disjoint, db, env, + forall fully_static_types t. t.negate(db, env).is_disjoint_from(db, env, t) ); // For two types, their intersection must be a subtype of each type in the pair. type_property_test!( - all_type_pairs_are_supertypes_of_their_intersection, db, + all_type_pairs_are_supertypes_of_their_intersection, db, env, forall types s, t. - intersection(db, [s, t]).is_subtype_of(db, s) && intersection(db, [s, t]).is_subtype_of(db, t) + intersection(db, env, [s, t]).is_subtype_of(db, env, s) && intersection(db, env, [s, t]).is_subtype_of(db, env, t) ); // And the intersection of a pair of types // should be assignable to both types of the pair. // Currently fails due to https://github.com/astral-sh/ruff/issues/14899 type_property_test!( - all_type_pairs_can_be_assigned_from_their_intersection, db, - forall types s, t. intersection(db, [s, t]).is_assignable_to(db, s) && intersection(db, [s, t]).is_assignable_to(db, t) + all_type_pairs_can_be_assigned_from_their_intersection, db, env, + forall types s, t. intersection(db, env, [s, t]).is_assignable_to(db, env, s) && intersection(db, env, [s, t]).is_assignable_to(db, env, t) ); // Equal element sets of intersections implies equivalence // flaky at least in part because of https://github.com/astral-sh/ruff/issues/15513 type_property_test!( - intersection_equivalence_not_order_dependent, db, + intersection_equivalence_not_order_dependent, db, env, forall types s, t, u. [s, t, u] .into_iter() .permutations(3) - .map(|trio_of_types| intersection(db, trio_of_types)) + .map(|trio_of_types| intersection(db, env, trio_of_types)) .permutations(2) - .all(|vec_of_intersections| vec_of_intersections[0].is_equivalent_to(db, vec_of_intersections[1])) + .all(|vec_of_intersections| vec_of_intersections[0].is_equivalent_to(db, env, vec_of_intersections[1])) ); // Equal element sets of unions implies equivalence // flaky at least in part because of https://github.com/astral-sh/ruff/issues/15513 type_property_test!( - union_equivalence_not_order_dependent, db, + union_equivalence_not_order_dependent, db, env, forall types s, t, u. [s, t, u] .into_iter() .permutations(3) - .map(|trio_of_types| union(db, trio_of_types)) + .map(|trio_of_types| union(db, env, trio_of_types)) .permutations(2) - .all(|vec_of_unions| vec_of_unions[0].is_equivalent_to(db, vec_of_unions[1])) + .all(|vec_of_unions| vec_of_unions[0].is_equivalent_to(db, env, vec_of_unions[1])) ); // `S | T` is always a supertype of `S`. // Thus, `S` is never disjoint from `S | T`. type_property_test!( - constituent_members_of_union_is_not_disjoint_from_that_union, db, + constituent_members_of_union_is_not_disjoint_from_that_union, db, env, forall types s, t. - !s.is_disjoint_from(db, union(db, [s, t])) && !t.is_disjoint_from(db, union(db, [s, t])) + !s.is_disjoint_from(db, env, union(db, env, [s, t])) && !t.is_disjoint_from(db, env, union(db, env, [s, t])) ); // If `S <: T`, then `~T <: ~S`. @@ -333,8 +344,8 @@ mod flaky { // occur very rarely (even running the test with several million seeds does // not always reliably reproduce the flake). type_property_test!( - negation_reverses_subtype_order, db, - forall types s, t. s.is_subtype_of(db, t) => t.negate(db).is_subtype_of(db, s.negate(db)) + negation_reverses_subtype_order, db, env, + forall types s, t. s.is_subtype_of(db, env, t) => t.negate(db, env).is_subtype_of(db, env, s.negate(db, env)) ); // Both the top and bottom materialization tests are flaky in part due to various failures that @@ -343,13 +354,13 @@ mod flaky { // `T'`, the top materialization of `T`, should be assignable to `T`. type_property_test!( - top_materialization_of_type_is_assignable_to_type, db, - forall types t. t.top_materialization(db).is_assignable_to(db, t) + top_materialization_of_type_is_assignable_to_type, db, env, + forall types t. t.top_materialization(db, env).is_assignable_to(db, env, t) ); // Similarly, `T'`, the bottom materialization of `T`, should also be assignable to `T`. type_property_test!( - bottom_materialization_of_type_is_assignable_to_type, db, - forall types t. t.bottom_materialization(db).is_assignable_to(db, t) + bottom_materialization_of_type_is_assignable_to_type, db, env, + forall types t. t.bottom_materialization(db, env).is_assignable_to(db, env, t) ); } diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index 976aa0d528..0cea47808e 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -1,5 +1,5 @@ use crate::Db; -use crate::db::tests::TestDb; +use crate::ProgramEnvironment; use crate::place::{DefinedPlace, Place, builtins_symbol, global_symbol, known_module_symbol}; use crate::types::enums::is_single_member_enum; use crate::types::known_instance::KnownInstanceType; @@ -10,7 +10,9 @@ use crate::types::{ SpecialFormType, SubclassOfType, Type, UnionType, }; use quickcheck::{Arbitrary, Gen}; +use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; +use ruff_python_ast::PythonVersion; use ruff_python_ast::name::Name; use rustc_hash::FxHashSet; use ty_module_resolver::KnownModule; @@ -90,7 +92,11 @@ pub(crate) enum CallableParams { } impl CallableParams { - fn into_parameters(self, db: &TestDb) -> Parameters<'_> { + fn into_parameters<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Parameters<'db> { match self { CallableParams::GradualForm => Parameters::gradual_form(), CallableParams::List(params) => Parameters::from_annotation( @@ -108,8 +114,8 @@ impl CallableParams { } }; parameter - .with_annotated_type(param.annotated_ty.into_type(db)) - .with_optional_default_type(param.default_ty.map(|t| t.into_type(db))) + .with_annotated_type(param.annotated_ty.into_type(db, env)) + .with_optional_default_type(param.default_ty.map(|t| t.into_type(db, env))) }), ), } @@ -136,25 +142,31 @@ enum ParamKind { #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] fn create_bound_method<'db>( db: &'db dyn Db, + python_version: PythonVersion, function: Type<'db>, builtins_class: Type<'db>, ) -> Type<'db> { + let env = ProgramEnvironment::from_program(python_version); Type::BoundMethod(BoundMethodType::new( db, function.expect_function_literal(), - builtins_class.to_instance_approximation(db).unwrap(), + builtins_class.to_instance_approximation(db, &env).unwrap(), )) } impl Ty { - pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> { + pub(crate) fn into_type<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { Ty::Never => Type::Never, Ty::Unknown => Type::unknown(), - Ty::Divergent => divergent(db, 1, None), - Ty::TopDivergent => divergent(db, 2, Some(MaterializationKind::Top)), - Ty::BottomDivergent => divergent(db, 3, Some(MaterializationKind::Bottom)), - Ty::None => Type::none(db), + Ty::Divergent => divergent(db, env, 1, None), + Ty::TopDivergent => divergent(db, env, 2, Some(MaterializationKind::Top)), + Ty::BottomDivergent => divergent(db, env, 3, Some(MaterializationKind::Bottom)), + Ty::None => Type::none(db, env), Ty::Any => Type::any(), Ty::IntLiteral(n) => Type::int_literal(n), Ty::StringLiteral(s) => Type::string_literal(db, s), @@ -162,7 +174,7 @@ impl Ty { Ty::LiteralString => Type::literal_string(), Ty::BytesLiteral(s) => Type::bytes_literal(db, s.as_bytes()), Ty::EnumLiteral(name) => { - let enum_class = known_module_symbol(db, KnownModule::Uuid, "SafeUUID") + let enum_class = known_module_symbol(db, env, KnownModule::Uuid, "SafeUUID") .place .expect_type() .expect_class_literal() @@ -171,64 +183,67 @@ impl Ty { Type::enum_literal(EnumLiteralType::new(db, enum_class, Name::new(name))) } Ty::SingleMemberEnumLiteral => { - let ty = known_module_symbol(db, KnownModule::Dataclasses, "MISSING") + let ty = known_module_symbol(db, env, KnownModule::Dataclasses, "MISSING") .place .expect_type(); debug_assert!( - matches!(ty, Type::NominalInstance(instance) if is_single_member_enum(db, instance.class_literal(db))) + matches!(ty, Type::NominalInstance(instance) if is_single_member_enum(db, instance.class_literal(db, env))) ); ty } - Ty::BuiltinInstance(s) => builtins_symbol(db, s) + Ty::BuiltinInstance(s) => builtins_symbol(db, env, s) .place .expect_type() - .to_instance_approximation(db) + .to_instance_approximation(db, env) .unwrap(), - Ty::AbcInstance(s) => known_module_symbol(db, KnownModule::Abc, s) + Ty::AbcInstance(s) => known_module_symbol(db, env, KnownModule::Abc, s) .place .expect_type() - .to_instance_approximation(db) + .to_instance_approximation(db, env) .unwrap(), - Ty::AbcClassLiteral(s) => known_module_symbol(db, KnownModule::Abc, s) - .place - .expect_type(), - Ty::UnittestMockLiteral => known_module_symbol(db, KnownModule::UnittestMock, "Mock") + Ty::AbcClassLiteral(s) => known_module_symbol(db, env, KnownModule::Abc, s) .place .expect_type(), + Ty::UnittestMockLiteral => { + known_module_symbol(db, env, KnownModule::UnittestMock, "Mock") + .place + .expect_type() + } Ty::UnittestMockInstance => Ty::UnittestMockLiteral - .into_type(db) - .to_instance_approximation(db) + .into_type(db, env) + .to_instance_approximation(db, env) .unwrap(), Ty::TypingLiteral => Type::SpecialForm(SpecialFormType::Literal), - Ty::BuiltinClassLiteral(s) => builtins_symbol(db, s).place.expect_type(), - Ty::KnownClassInstance(known_class) => known_class.to_instance(db), + Ty::BuiltinClassLiteral(s) => builtins_symbol(db, env, s).place.expect_type(), + Ty::KnownClassInstance(known_class) => known_class.to_instance(db, env), Ty::Union(tys) => { - UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db))) + UnionType::from_elements(db, env, tys.into_iter().map(|ty| ty.into_type(db, env))) } Ty::Intersection { pos, neg } => { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); for p in pos { - builder.add_positive_in_place(p.into_type(db)); + builder.add_positive_in_place(p.into_type(db, env)); } for n in neg { - builder.add_negative_in_place(n.into_type(db)); + builder.add_negative_in_place(n.into_type(db, env)); } builder.build() } Ty::FixedLengthTuple(tys) => { - let elements = tys.into_iter().map(|ty| ty.into_type(db)); - Type::heterogeneous_tuple(db, elements) + let elements = tys.into_iter().map(|ty| ty.into_type(db, env)); + Type::heterogeneous_tuple(db, env, elements) } Ty::VariableLengthTuple(prefix, variable, suffix) => { - let prefix = prefix.into_iter().map(|ty| ty.into_type(db)); - let variable = variable.into_type(db); - let suffix = suffix.into_iter().map(|ty| ty.into_type(db)); - Type::tuple(TupleType::mixed(db, prefix, variable, suffix)) + let prefix = prefix.into_iter().map(|ty| ty.into_type(db, env)); + let variable = variable.into_type(db, env); + let suffix = suffix.into_iter().map(|ty| ty.into_type(db, env)); + Type::tuple(TupleType::mixed(db, env, prefix, variable, suffix)) } Ty::SubclassOfAny => SubclassOfType::subclass_of_any(), Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from( db, - builtins_symbol(db, s) + env, + builtins_symbol(db, env, s) .place .expect_type() .expect_class_literal() @@ -236,7 +251,8 @@ impl Ty { ), Ty::SubclassOfAbcClass(s) => SubclassOfType::from( db, - known_module_symbol(db, KnownModule::Abc, s) + env, + known_module_symbol(db, env, KnownModule::Abc, s) .place .expect_type() .expect_class_literal() @@ -244,44 +260,48 @@ impl Ty { ), Ty::AlwaysTruthy => Type::AlwaysTruthy, Ty::AlwaysFalsy => Type::AlwaysFalsy, - Ty::BuiltinsFunction(name) => builtins_symbol(db, name).place.expect_type(), + Ty::BuiltinsFunction(name) => builtins_symbol(db, env, name).place.expect_type(), Ty::BuiltinsBoundMethod { class, method } => { - let builtins_class = builtins_symbol(db, class).place.expect_type(); - let function = builtins_class.member(db, method).place.expect_type(); + let builtins_class = builtins_symbol(db, env, class).place.expect_type(); + let function = builtins_class.member(db, env, method).place.expect_type(); - create_bound_method(db, function, builtins_class) + create_bound_method(db, env.python_version(db), function, builtins_class) } Ty::Callable { params, returns } => Type::single_callable( db, - Signature::new(params.into_parameters(db), returns.into_type(db)), + Signature::new(params.into_parameters(db, env), returns.into_type(db, env)), ), - Ty::FloatNewtypeInstance => newtype_instance(db, "NewTypeOfFloat"), - Ty::IntNewtypeInstance => newtype_instance(db, "NewTypeOfInt"), - Ty::StrNewtypeInstance => newtype_instance(db, "NewTypeOfStr"), - Ty::ComplexNewtypeInstance => newtype_instance(db, "NewTypeOfComplex"), - Ty::SubNewTypeOfIntInstance => newtype_instance(db, "SubNewTypeOfInt"), - Ty::SubSubNewTypeOfIntInstance => newtype_instance(db, "SubSubNewTypeOfInt"), - Ty::SubNewTypeOfFloatInstance => newtype_instance(db, "SubNewTypeOfFloat"), + Ty::FloatNewtypeInstance => newtype_instance(db, env, "NewTypeOfFloat"), + Ty::IntNewtypeInstance => newtype_instance(db, env, "NewTypeOfInt"), + Ty::StrNewtypeInstance => newtype_instance(db, env, "NewTypeOfStr"), + Ty::ComplexNewtypeInstance => newtype_instance(db, env, "NewTypeOfComplex"), + Ty::SubNewTypeOfIntInstance => newtype_instance(db, env, "SubNewTypeOfInt"), + Ty::SubSubNewTypeOfIntInstance => newtype_instance(db, env, "SubSubNewTypeOfInt"), + Ty::SubNewTypeOfFloatInstance => newtype_instance(db, env, "SubNewTypeOfFloat"), } } } -fn divergent(db: &TestDb, id_bits: u64, materialization: Option) -> Type<'_> { +fn divergent<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + id_bits: u64, + materialization: Option, +) -> Type<'db> { let divergent = Type::divergent(salsa::plumbing::Id::from_bits(id_bits)); match materialization { - Some(materialization_kind) => divergent.materialize( - db, - materialization_kind, - &ApplyTypeMappingVisitor::default(), - ), + Some(materialization_kind) => { + divergent.materialize(db, materialization_kind, &ApplyTypeMappingVisitor::new(env)) + } None => divergent, } } -fn newtype_instance<'db>(db: &'db dyn Db, name: &str) -> Type<'db> { +fn newtype_instance<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, name: &str) -> Type<'db> { let file = system_path_to_file(db, super::setup::PROPERTY_TEST_MODULE_PATH) .expect("Property-test module must exist"); + let file = PythonFile::new(db, file, env.python_version(db)); let Place::Defined(DefinedPlace { ty, .. }) = global_symbol(db, file, name).place else { panic!( "Expected a global symbol for `{name}` in the property test module, but it was not found" @@ -297,8 +317,12 @@ fn newtype_instance<'db>(db: &'db dyn Db, name: &str) -> Type<'db> { pub(crate) struct FullyStaticTy(Ty); impl FullyStaticTy { - pub(crate) fn into_type(self, db: &TestDb) -> Type<'_> { - self.0.into_type(db) + pub(crate) fn into_type<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.0.into_type(db, env) } } @@ -618,12 +642,17 @@ impl Arbitrary for FullyStaticTy { } pub(crate) fn intersection<'db>( - db: &'db TestDb, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, tys: impl IntoIterator>, ) -> Type<'db> { - IntersectionType::from_elements(db, tys) + IntersectionType::from_elements(db, env, tys) } -pub(crate) fn union<'db>(db: &'db TestDb, tys: impl IntoIterator>) -> Type<'db> { - UnionType::from_elements(db, tys) +pub(crate) fn union<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + tys: impl IntoIterator>, +) -> Type<'db> { + UnionType::from_elements(db, env, tys) } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 3368b238b7..9dcc44c581 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -1,3 +1,4 @@ +use crate::{Program, ProgramEnvironment}; use std::fmt::Write; use std::{collections::BTreeMap, ops::Deref}; @@ -112,13 +113,17 @@ impl<'db> ProtocolClass<'db> { ) { let mut seen_members = FxHashSet::default(); - self.for_each_member_candidate(db, |name, candidate, specialization| { - if !seen_members.insert(name.clone()) { - return; - } - let candidate = candidate.apply_specialization(db, specialization); - candidate.walk_recursive_member_types(db, visitor); - }); + self.for_each_member_candidate( + db, + visitor.program_environment(), + |name, candidate, specialization| { + if !seen_members.insert(name.clone()) { + return; + } + let candidate = candidate.apply_specialization(db, specialization); + candidate.walk_recursive_member_types(db, visitor); + }, + ); } /// Visits protocol member candidates in MRO order after applying declaration precedence. @@ -127,6 +132,7 @@ impl<'db> ProtocolClass<'db> { fn for_each_member_candidate( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut visit: impl FnMut(&Name, ProtocolMemberCandidate<'db>, Option>), ) { for (parent_scope, specialization) in self @@ -149,7 +155,7 @@ impl<'db> ProtocolClass<'db> { // runtime-checkable protocols still consider them members for `isinstance()` and // `issubclass()`. for (symbol_id, bindings) in use_def_map.all_end_of_scope_symbol_bindings() { - let place_and_definition = place_from_bindings(db, bindings); + let place_and_definition = place_from_bindings(db, env, bindings); if let Some(ty) = place_and_definition.place.ignore_possibly_undefined() { direct_members.insert( symbol_id, @@ -164,7 +170,7 @@ impl<'db> ProtocolClass<'db> { } for (symbol_id, declarations) in use_def_map.all_end_of_scope_symbol_declarations() { - let place_result = place_from_declarations(db, declarations); + let place_result = place_from_declarations(db, env, declarations); let first_declaration = place_result.first_declaration; let place = place_result.ignore_conflicting_declarations(); if let Some(ty) = place.place.ignore_possibly_undefined() { @@ -217,6 +223,10 @@ impl<'db> ProtocolClass<'db> { /// __doc__: str /// ``` pub(super) fn has_member_declaration(self, db: &'db dyn Db, name: &str) -> bool { + let Some((class, _)) = self.static_class_literal(db) else { + return false; + }; + let env = ProgramEnvironment::from_scope(class.body_scope(db)); self.iter_mro(db) .filter_map(ClassBase::into_class) .any(|superclass| { @@ -230,6 +240,7 @@ impl<'db> ProtocolClass<'db> { }; !place_from_declarations( db, + &env, use_def_map(db, superclass_scope) .end_of_scope_declarations(ScopedPlaceId::Symbol(scoped_symbol_id)), ) @@ -279,7 +290,7 @@ impl<'db> ProtocolClass<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self( self.0 @@ -290,11 +301,13 @@ impl<'db> ProtocolClass<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self( - self.0.recursive_type_normalized_impl(db, div, nested)?, + self.0 + .recursive_type_normalized_impl(db, env, div, nested)?, )) } } @@ -316,6 +329,9 @@ impl<'db> From> for Type<'db> { /// The interface of a protocol: the members of that protocol, and the types of those members. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub(super) struct ProtocolInterface<'db> { + #[returns(copy)] + pub(super) program: Program, + #[returns(ref)] inner: BTreeMap>, } @@ -374,9 +390,10 @@ impl<'db> ProtocolInterfaceView<'db> { /// Returns whether structural comparison can avoid recursive member expansion. pub(super) fn has_only_finite_members(self, db: &'db dyn Db) -> bool { + let env = ProgramEnvironment::from_program(self.interface.program(db)); self.members(db).all(|member| { !matches!( - member.structural_member_priority(db), + member.structural_member_priority(db, &env), StructuralMemberPriority::Recursive ) }) @@ -401,7 +418,12 @@ impl<'db> ProtocolInterfaceView<'db> { /// /// An unrelated materialized member must not prevent a protocol from retaining its /// nominal relationship to one of its bases. - pub(super) fn differs_for_members_required_by(self, db: &'db dyn Db, required: Self) -> bool { + pub(super) fn differs_for_members_required_by( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + required: Self, + ) -> bool { required.members(db).any(|required_member| { let Some(materialized) = self.member_by_name(db, required_member.name()) else { return false; @@ -413,11 +435,11 @@ impl<'db> ProtocolInterfaceView<'db> { }; if materialized - .access(db, ProtocolMemberAccessMode::Instance) - .resolved(db) + .access(db, env, ProtocolMemberAccessMode::Instance) + .resolved(db, env) != original - .access(db, ProtocolMemberAccessMode::Instance) - .resolved(db) + .access(db, env, ProtocolMemberAccessMode::Instance) + .resolved(db, env) { return true; } @@ -430,11 +452,11 @@ impl<'db> ProtocolInterfaceView<'db> { } materialized - .access(db, ProtocolMemberAccessMode::Class) - .resolved(db) + .access(db, env, ProtocolMemberAccessMode::Class) + .resolved(db, env) != original - .access(db, ProtocolMemberAccessMode::Class) - .resolved(db) + .access(db, env, ProtocolMemberAccessMode::Class) + .resolved(db, env) }) } @@ -446,15 +468,16 @@ impl<'db> ProtocolInterfaceView<'db> { pub(super) fn instance_write_requirement( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_ty: Type<'db>, name: &str, ) -> Option<(Option>, TypeQualifiers)> { self.member_by_name(db, name).map(|member| { ( member - .access(db, ProtocolMemberAccessMode::Instance) + .access(db, env, ProtocolMemberAccessMode::Instance) .write - .and_then(|write| write.bind_requirement(db, receiver_ty)), + .and_then(|write| write.bind_requirement(db, env, receiver_ty)), member.qualifiers(), ) }) @@ -467,15 +490,16 @@ impl<'db> ProtocolInterfaceView<'db> { pub(super) fn meta_write_requirement( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_ty: Type<'db>, name: &str, ) -> Option<(Option>, TypeQualifiers)> { self.member_by_name(db, name).map(|member| { ( member - .access(db, ProtocolMemberAccessMode::Class) + .access(db, env, ProtocolMemberAccessMode::Class) .write - .and_then(|write| write.bind_compatibility_type(db, receiver_ty)), + .and_then(|write| write.bind_compatibility_type(db, env, receiver_ty)), member.qualifiers(), ) }) @@ -485,15 +509,19 @@ impl<'db> ProtocolInterfaceView<'db> { /// method. /// /// The callable is already in its instance-bound form, so callers must not bind it again. - pub(super) fn call_method(self, db: &'db dyn Db) -> Option> { + pub(super) fn call_method( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { self.member_by_name(db, "__call__").and_then(|member| { if !member.is_method() { return None; } match member - .access(db, ProtocolMemberAccessMode::Instance) + .access(db, env, ProtocolMemberAccessMode::Instance) .read - .and_then(|read| read.resolve(db)) + .and_then(|read| read.resolve(db, env)) .map(ProtocolMemberType::ty) { Some(Type::Callable(callable)) => Some(callable), @@ -502,19 +530,24 @@ impl<'db> ProtocolInterfaceView<'db> { }) } - pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { + pub(super) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { self.member_by_name(db, name) .map(|member| PlaceAndQualifiers { place: member - .access(db, ProtocolMemberAccessMode::Instance) + .access(db, env, ProtocolMemberAccessMode::Instance) .read - .and_then(|read| read.resolve(db)) + .and_then(|read| read.resolve(db, env)) .map(|read| Place::bound(read.ty())) .unwrap_or(Place::Undefined) .with_provenance(Provenance::from_definition(member.definition())), qualifiers: member.qualifiers(), }) - .unwrap_or_else(|| Type::object().member(db, name)) + .unwrap_or_else(|| Type::object().member(db, env, name)) } /// Looks up a member guaranteed to exist on every inhabitant of `type[Protocol]`. @@ -525,13 +558,14 @@ impl<'db> ProtocolInterfaceView<'db> { pub(super) fn meta_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, ) -> Option> { self.member_by_name(db, name).map(|member| { - let read = member.access(db, ProtocolMemberAccessMode::Class).read; + let read = member.access(db, env, ProtocolMemberAccessMode::Class).read; PlaceAndQualifiers { place: read - .and_then(|read| read.resolve(db)) + .and_then(|read| read.resolve(db, env)) .map(|read| Place::bound(read.ty())) .unwrap_or(Place::Undefined) .with_provenance(Provenance::from_definition(member.definition())), @@ -587,18 +621,19 @@ pub(super) fn walk_protocol_instance_member<'db, V: super::visitor::TypeVisitor< receiver_ty: Type<'db>, visitor: &V, ) { + let env = visitor.program_environment(); match member.data.kind { ProtocolMemberKind::Method(method, _) => { let method = member .materialization - .map_or(method, |kind| method.materialize(db, kind)); + .map_or(method, |kind| method.materialize(db, env, kind)); let Type::Callable(callable) = method.ty() else { visitor.visit_type(db, method.ty()); return; }; for signature in callable.signatures(db) { if signature.has_implicit_positional_receiver_annotation() { - let signature = signature.bind_self(db, Some(receiver_ty)); + let signature = signature.bind_self(db, env, Some(receiver_ty)); walk_signature(db, &signature, visitor); } else { walk_signature(db, signature, visitor); @@ -606,7 +641,7 @@ pub(super) fn walk_protocol_instance_member<'db, V: super::visitor::TypeVisitor< } } ProtocolMemberKind::Property { .. } => { - let access = member.access(db, ProtocolMemberAccessMode::Instance); + let access = member.access(db, env, ProtocolMemberAccessMode::Instance); for member_type in [ access.read, access.write.and_then(ProtocolMemberWrite::domain), @@ -615,7 +650,7 @@ pub(super) fn walk_protocol_instance_member<'db, V: super::visitor::TypeVisitor< .into_iter() .flatten() { - if let Some(ty) = member_type.bind_self(db, receiver_ty) { + if let Some(ty) = member_type.bind_self(db, env, receiver_ty) { visitor.visit_type(db, ty); } } @@ -623,8 +658,8 @@ pub(super) fn walk_protocol_instance_member<'db, V: super::visitor::TypeVisitor< ProtocolMemberKind::Attribute(attribute) => { let attribute = member .materialization - .map_or(attribute, |kind| attribute.materialize(db, kind)); - if let Some(ty) = attribute.bind_self(db, receiver_ty) { + .map_or(attribute, |kind| attribute.materialize(db, env, kind)); + if let Some(ty) = attribute.bind_self(db, env, receiver_ty) { visitor.visit_type(db, ty); } } @@ -636,7 +671,11 @@ impl<'db> ProtocolInterface<'db> { /// /// All created members will be covariant, read-only property members /// rather than method members or mutable attribute members. - pub(super) fn with_property_members<'a, M>(db: &'db dyn Db, members: M) -> Self + pub(super) fn with_property_members<'a, M>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + members: M, + ) -> Self where M: IntoIterator)>, { @@ -649,11 +688,15 @@ impl<'db> ProtocolInterface<'db> { ) }) .collect(); - Self::new(db, members) + Self::new(db, env.program(db), members) } /// Synthesize a new protocol interface with the given methods. - pub(super) fn with_methods<'a, M>(db: &'db dyn Db, members: M) -> Self + pub(super) fn with_methods<'a, M>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + members: M, + ) -> Self where M: IntoIterator)>, { @@ -662,18 +705,24 @@ impl<'db> ProtocolInterface<'db> { .map(|(name, callable)| { ( Name::new(name), - ProtocolMemberData::method(db, callable, None), + ProtocolMemberData::method(db, env, callable, None), ) }) .collect(); - Self::new(db, members) + Self::new(db, env.program(db), members) } - fn empty(db: &'db dyn Db) -> Self { - Self::new(db, BTreeMap::default()) + fn empty(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + Self::new(db, env.program(db), BTreeMap::default()) } - fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + cycle: &salsa::Cycle, + ) -> Self { let prev_inner = previous.inner(db); let curr_inner = self.inner(db); @@ -681,14 +730,14 @@ impl<'db> ProtocolInterface<'db> { .iter() .map(|(name, curr_data)| { let normalized = if let Some(prev_data) = prev_inner.get(name) { - curr_data.cycle_normalized(db, prev_data, cycle) + curr_data.cycle_normalized(db, env, prev_data, cycle) } else { curr_data.clone() }; (name.clone(), normalized) }) .collect(); - Self::new(db, members) + Self::new(db, env.program(db), members) } pub(super) fn members<'a>( @@ -712,6 +761,7 @@ impl<'db> ProtocolInterface<'db> { ) -> Self { Self::new( db, + self.program(db), self.inner(db) .iter() .filter(|&(name, data)| { @@ -745,14 +795,15 @@ impl<'db> ProtocolInterface<'db> { pub(super) fn includes_generic_writable_instance_member( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, generic_context: GenericContext<'db>, ) -> bool { self.inner(db) .get(name) - .and_then(|data| data.capabilities(db).instance.write) + .and_then(|data| data.capabilities(db, env).instance.write) .and_then(ProtocolMemberWrite::domain) - .and_then(|write| write.resolve(db)) + .and_then(|write| write.resolve(db, env)) .is_some_and(|write| { matches!( write.ty(), @@ -764,24 +815,31 @@ impl<'db> ProtocolInterface<'db> { }) } - pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { - ProtocolInterfaceView::new(self, None).instance_member(db, name) + pub(super) fn instance_member( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> PlaceAndQualifiers<'db> { + ProtocolInterfaceView::new(self, None).instance_member(db, env, name) } pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self::new( db, + env.program(db), self.inner(db) .iter() .map(|(name, data)| { Some(( name.clone(), - data.recursive_type_normalized_impl(db, div, nested)?, + data.recursive_type_normalized_impl(db, env, div, nested)?, )) }) .collect::>>()?, @@ -793,10 +851,11 @@ impl<'db> ProtocolInterface<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self::new( db, + visitor.env.program(db), self.inner(db) .iter() .map(|(name, data)| { @@ -812,27 +871,34 @@ impl<'db> ProtocolInterface<'db> { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { for data in self.inner(db).values() { - data.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + data.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } - pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display { - struct ProtocolInterfaceDisplay<'db> { + pub(super) fn display<'env>( + self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> impl std::fmt::Display + 'env { + struct ProtocolInterfaceDisplay<'env, 'db> { db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, interface: ProtocolInterface<'db>, } - impl std::fmt::Display for ProtocolInterfaceDisplay<'_> { + impl std::fmt::Display for ProtocolInterfaceDisplay<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; f.write_char('{')?; - for (i, (name, data)) in self.interface.inner(self.db).iter().enumerate() { - write!(f, "\"{name}\": {data}", data = data.display(self.db))?; - if i < self.interface.inner(self.db).len() - 1 { + for (i, (name, data)) in self.interface.inner(db).iter().enumerate() { + write!(f, "\"{name}\": {data}", data = data.display(db, self.env))?; + if i < self.interface.inner(db).len() - 1 { f.write_str(", ")?; } } @@ -842,6 +908,7 @@ impl<'db> ProtocolInterface<'db> { ProtocolInterfaceDisplay { db, + env, interface: self, } } @@ -891,13 +958,17 @@ impl<'db> ProtocolMemberWrite<'db> { } } - fn display_type(self, db: &'db dyn Db) -> Option> { + fn display_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { - Self::Type(member) => member.resolve(db), + Self::Type(member) => member.resolve(db, env), Self::Descriptor { domain: Some(domain), .. - } => domain.resolve(db), + } => domain.resolve(db, env), Self::Descriptor { domain: None, .. } => Some(ProtocolMemberType::new(Type::unknown())), } } @@ -905,28 +976,34 @@ impl<'db> ProtocolMemberWrite<'db> { fn bind_requirement( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, self_type: Type<'db>, ) -> Option> { match self { Self::Type(member) => Some(ProtocolMemberWriteRequirement::AssignableTo( - member.bind_self(db, self_type)?, + member.bind_self(db, env, self_type)?, )), Self::Descriptor { descriptor, domain } => { Some(ProtocolMemberWriteRequirement::Descriptor { - descriptor_ty: descriptor.bind_self(db, self_type)?, + descriptor_ty: descriptor.bind_self(db, env, self_type)?, receiver_ty: self_type, - domain: domain.and_then(|domain| domain.bind_self(db, self_type)), + domain: domain.and_then(|domain| domain.bind_self(db, env, self_type)), }) } } } - fn bind_compatibility_type(self, db: &'db dyn Db, self_type: Type<'db>) -> Option> { + fn bind_compatibility_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Type<'db>, + ) -> Option> { match self { - Self::Type(member) => member.bind_self(db, self_type), + Self::Type(member) => member.bind_self(db, env, self_type), Self::Descriptor { domain, .. } => Some( domain - .and_then(|domain| domain.bind_self(db, self_type)) + .and_then(|domain| domain.bind_self(db, env, self_type)) .unwrap_or_else(Type::unknown), ), } @@ -936,31 +1013,42 @@ impl<'db> ProtocolMemberWrite<'db> { /// /// The descriptor itself remains unchanged so normal descriptor dispatch and deletion /// continue to use the declaration on the original protocol class. - fn materialize(self, db: &'db dyn Db, kind: MaterializationKind) -> Self { + fn materialize( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + kind: MaterializationKind, + ) -> Self { match self { - Self::Type(member) => Self::Type(member.materialize(db, kind.flip())), + Self::Type(member) => Self::Type(member.materialize(db, env, kind.flip())), Self::Descriptor { descriptor, domain } => Self::Descriptor { descriptor, - domain: domain.map(|domain| domain.materialize(db, kind.flip())), + domain: domain.map(|domain| domain.materialize(db, env, kind.flip())), }, } } /// Resolve accessor representations without changing descriptor identity. - fn resolved(self, db: &'db dyn Db) -> Self { + fn resolved(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { match self { - Self::Type(member) => Self::Type(member.resolve(db).unwrap_or(member)), + Self::Type(member) => Self::Type(member.resolve(db, env).unwrap_or(member)), Self::Descriptor { descriptor, domain } => Self::Descriptor { - descriptor: descriptor.resolve(db).unwrap_or(descriptor), - domain: domain.map(|member| member.resolve(db).unwrap_or(member)), + descriptor: descriptor.resolve(db, env).unwrap_or(descriptor), + domain: domain.map(|member| member.resolve(db, env).unwrap_or(member)), }, } } - fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + cycle: &salsa::Cycle, + ) -> Self { match (self, previous) { (Self::Type(current), Self::Type(previous)) => { - Self::Type(current.cycle_normalized(db, previous, cycle)) + Self::Type(current.cycle_normalized(db, env, previous, cycle)) } ( Self::Descriptor { @@ -972,16 +1060,32 @@ impl<'db> ProtocolMemberWrite<'db> { domain: previous_domain, }, ) => Self::Descriptor { - descriptor: current_descriptor.cycle_normalized(db, previous_descriptor, cycle), - domain: cycle_normalized_optional_type(db, current_domain, previous_domain, cycle), + descriptor: current_descriptor.cycle_normalized( + db, + env, + previous_descriptor, + cycle, + ), + domain: cycle_normalized_optional_type( + db, + env, + current_domain, + previous_domain, + cycle, + ), }, (current, _) => current, } } - fn cycle_normalized_without_previous(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized_without_previous( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + cycle: &salsa::Cycle, + ) -> Self { let normalize = |member: ProtocolMemberType<'db>| { - member.with_ty(member.ty().recursive_type_normalized(db, cycle)) + member.with_ty(member.ty().recursive_type_normalized(db, env, cycle)) }; match self { Self::Type(member) => Self::Type(normalize(member)), @@ -995,17 +1099,20 @@ impl<'db> ProtocolMemberWrite<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(match self { Self::Type(member) => { - Self::Type(member.recursive_type_normalized_impl(db, div, nested)?) + Self::Type(member.recursive_type_normalized_impl(db, env, div, nested)?) } Self::Descriptor { descriptor, domain } => Self::Descriptor { - descriptor: descriptor.recursive_type_normalized_impl(db, div, nested)?, + descriptor: descriptor.recursive_type_normalized_impl(db, env, div, nested)?, domain: match domain { - Some(domain) => Some(domain.recursive_type_normalized_impl(db, div, nested)?), + Some(domain) => { + Some(domain.recursive_type_normalized_impl(db, env, div, nested)?) + } None => None, }, }, @@ -1017,7 +1124,7 @@ impl<'db> ProtocolMemberWrite<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { Self::Type(member) => { @@ -1033,15 +1140,20 @@ impl<'db> ProtocolMemberWrite<'db> { } impl<'db> VarianceInferable<'db> for ProtocolInterface<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { self.members(db) .flat_map(|member| { - let capabilities = member.capabilities(db); + let capabilities = member.capabilities(db, env); [capabilities.instance, capabilities.class] .into_iter() - .flat_map(|access| access.variances(db)) + .flat_map(|access| access.variances(db, env)) }) - .map(|(ty, variance)| ty.with_polarity(variance).variance_of(db, typevar)) + .map(|(ty, variance)| ty.with_polarity(variance).variance_of(db, env, typevar)) .collect() } } @@ -1111,11 +1223,11 @@ impl<'db> ProtocolMemberType<'db> { } /// Resolves a stored property accessor to the value type exposed by that access. - fn resolve(self, db: &'db dyn Db) -> Option { + fn resolve(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option { match self { Self::Value { .. } => Some(self), - Self::PropertyGetter(getter) => property_get_member_type(db, getter), - Self::PropertySetter(setter) => property_set_member_type(db, setter), + Self::PropertyGetter(getter) => property_get_member_type(db, env, getter), + Self::PropertySetter(setter) => property_set_member_type(db, env, setter), } } @@ -1123,53 +1235,72 @@ impl<'db> ProtocolMemberType<'db> { /// /// In particular, resolving a property setter before materialization prevents its callable /// parameter from introducing a second contravariant flip. - fn materialize(self, db: &'db dyn Db, kind: MaterializationKind) -> Self { - let Some(resolved) = self.resolve(db) else { + fn materialize( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + kind: MaterializationKind, + ) -> Self { + let Some(resolved) = self.resolve(db, env) else { return self; }; let ty = match kind { - MaterializationKind::Top => resolved.ty().top_materialization(db), - MaterializationKind::Bottom => resolved.ty().bottom_materialization(db), + MaterializationKind::Top => resolved.ty().top_materialization(db, env), + MaterializationKind::Bottom => resolved.ty().bottom_materialization(db, env), }; resolved.with_ty(ty) } /// Resolves this member type and binds member-local `Self` occurrences to `self_type`. - fn bind_self(self, db: &'db dyn Db, self_type: Type<'db>) -> Option> { + fn bind_self( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Type<'db>, + ) -> Option> { let Self::Value { ty, self_binding_context, - } = self.resolve(db)? + } = self.resolve(db, env)? else { return None; }; - if !ty.contains_self(db) { + if !ty.contains_self(db, env) { return Some(ty); } Some(ty.apply_type_mapping( db, - &TypeMapping::BindSelf(SelfBinding::new(db, self_type, self_binding_context)), + env, + &TypeMapping::BindSelf(SelfBinding::new(db, env, self_type, self_binding_context)), TypeContext::default(), )) } - fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { - let ty = self.ty().cycle_normalized(db, previous.ty(), cycle); + fn cycle_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + cycle: &salsa::Cycle, + ) -> Self { + let ty = self.ty().cycle_normalized(db, env, previous.ty(), cycle); self.with_ty(ty) } fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let ty = if nested { - self.ty().recursive_type_normalized_impl(db, div, true)? + self.ty() + .recursive_type_normalized_impl(db, env, div, true)? } else { self.ty() - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }; Some(self.with_ty(ty)) @@ -1180,7 +1311,7 @@ impl<'db> ProtocolMemberType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let ty = self .ty() @@ -1212,30 +1343,41 @@ impl<'db> ProtocolMemberAccess<'db> { Self { read, write } } - fn materialize(self, db: &'db dyn Db, kind: MaterializationKind) -> Self { + fn materialize( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + kind: MaterializationKind, + ) -> Self { Self { - read: self.read.map(|read| read.materialize(db, kind)), - write: self.write.map(|write| write.materialize(db, kind)), + read: self.read.map(|read| read.materialize(db, env, kind)), + write: self.write.map(|write| write.materialize(db, env, kind)), } } /// Resolve readable and writable accessor representations without losing descriptor identity. - fn resolved(self, db: &'db dyn Db) -> Self { + fn resolved(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { Self { - read: self.read.map(|member| member.resolve(db).unwrap_or(member)), - write: self.write.map(|write| write.resolved(db)), + read: self + .read + .map(|member| member.resolve(db, env).unwrap_or(member)), + write: self.write.map(|write| write.resolved(db, env)), } } - fn variances(self, db: &'db dyn Db) -> impl Iterator, TypeVarVariance)> { + fn variances( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> impl Iterator, TypeVarVariance)> { self.read - .and_then(|member| member.resolve(db)) + .and_then(|member| member.resolve(db, env)) .map(|member| (member.ty(), TypeVarVariance::Covariant)) .into_iter() .chain( self.write .and_then(ProtocolMemberWrite::domain) - .and_then(|member| member.resolve(db)) + .and_then(|member| member.resolve(db, env)) .map(|member| (member.ty(), TypeVarVariance::Contravariant)), ) } @@ -1253,10 +1395,15 @@ struct ProtocolMemberCapabilities<'db> { } impl<'db> ProtocolMemberCapabilities<'db> { - fn materialize(self, db: &'db dyn Db, kind: MaterializationKind) -> Self { + fn materialize( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + kind: MaterializationKind, + ) -> Self { Self { - instance: self.instance.materialize(db, kind), - class: self.class.materialize(db, kind), + instance: self.instance.materialize(db, env, kind), + class: self.class.materialize(db, env, kind), } } } @@ -1269,14 +1416,15 @@ enum ProtocolMemberAccessMode { fn cycle_normalized_optional_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, current: Option>, previous: Option>, cycle: &salsa::Cycle, ) -> Option> { match (current, previous) { - (Some(current), Some(previous)) => Some(current.cycle_normalized(db, previous, cycle)), + (Some(current), Some(previous)) => Some(current.cycle_normalized(db, env, previous, cycle)), (Some(current), None) => { - Some(current.with_ty(current.ty().recursive_type_normalized(db, cycle))) + Some(current.with_ty(current.ty().recursive_type_normalized(db, env, cycle))) } (None, _) => None, } @@ -1292,13 +1440,14 @@ pub(super) struct ProtocolMemberData<'db> { impl<'db> ProtocolMemberData<'db> { fn method( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, callable: CallableType<'db>, definition: Option>, ) -> Self { let (method_kind, callable) = if callable.is_classmethod_like(db) { ( ProtocolMethodKind::Class, - protocol_bind_self(db, callable, None), + protocol_bind_self(db, env.program(db), callable, None), ) } else if callable.is_staticmethod_like(db) { (ProtocolMethodKind::Static, callable.into_regular(db)) @@ -1346,13 +1495,17 @@ impl<'db> ProtocolMemberData<'db> { /// /// These are views of the canonical method, property, or attribute representation below; /// keeping them derived prevents the stored member kind and its capabilities from diverging. - fn capabilities(&self, db: &'db dyn Db) -> ProtocolMemberCapabilities<'db> { + fn capabilities( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> ProtocolMemberCapabilities<'db> { match self.kind { ProtocolMemberKind::Method(member, kind) => { let instance_method = match (member.ty(), kind) { - (Type::Callable(callable), ProtocolMethodKind::Instance) => { - member.with_ty(Type::Callable(protocol_bind_self(db, callable, None))) - } + (Type::Callable(callable), ProtocolMethodKind::Instance) => member.with_ty( + Type::Callable(protocol_bind_self(db, env.program(db), callable, None)), + ), _ => member, }; ProtocolMemberCapabilities { @@ -1386,9 +1539,15 @@ impl<'db> ProtocolMemberData<'db> { } } - fn cycle_normalized(&self, db: &'db dyn Db, previous: &Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: &Self, + cycle: &salsa::Cycle, + ) -> Self { Self { - kind: self.kind.cycle_normalized(db, previous.kind, cycle), + kind: self.kind.cycle_normalized(db, env, previous.kind, cycle), qualifiers: self.qualifiers, definition: self.definition, } @@ -1397,11 +1556,14 @@ impl<'db> ProtocolMemberData<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self { - kind: self.kind.recursive_type_normalized_impl(db, div, nested)?, + kind: self + .kind + .recursive_type_normalized_impl(db, env, div, nested)?, qualifiers: self.qualifiers, definition: self.definition, }) @@ -1412,7 +1574,7 @@ impl<'db> ProtocolMemberData<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self { kind: self @@ -1426,6 +1588,7 @@ impl<'db> ProtocolMemberData<'db> { fn find_legacy_typevars_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, _visitor: &FindLegacyTypeVarsVisitor<'db>, @@ -1433,36 +1596,49 @@ impl<'db> ProtocolMemberData<'db> { for member_type in self.kind.member_types() { member_type .ty() - .find_legacy_typevars(db, binding_context, typevars); + .find_legacy_typevars(db, env, binding_context, typevars); } } - fn display(&self, db: &'db dyn Db) -> impl std::fmt::Display { - struct ProtocolMemberDataDisplay<'db> { + fn display<'env>( + &self, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + ) -> impl std::fmt::Display + 'env { + struct ProtocolMemberDataDisplay<'env, 'db> { db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, kind: ProtocolMemberKind<'db>, qualifiers: TypeQualifiers, } - impl std::fmt::Display for ProtocolMemberDataDisplay<'_> { + impl std::fmt::Display for ProtocolMemberDataDisplay<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let db = self.db; match self.kind { ProtocolMemberKind::Method(member, _) => { - write!(f, "MethodMember(`{}`)", member.ty().display(self.db)) + write!(f, "MethodMember(`{}`)", member.ty().display(db, self.env)) } ProtocolMemberKind::Property { read, write } => { + let env = self.env; let mut d = f.debug_struct("PropertyMember"); - if let Some(read) = read.and_then(|read| read.resolve(self.db)) { - d.field("read", &format_args!("`{}`", read.ty().display(self.db))); + if let Some(read) = read.and_then(|read| read.resolve(db, env)) { + d.field( + "read", + &format_args!("`{}`", read.ty().display(db, self.env)), + ); } - if let Some(write) = write.and_then(|write| write.display_type(self.db)) { - d.field("write", &format_args!("`{}`", write.ty().display(self.db))); + if let Some(write) = write.and_then(|write| write.display_type(db, env)) { + d.field( + "write", + &format_args!("`{}`", write.ty().display(db, self.env)), + ); } d.finish() } ProtocolMemberKind::Attribute(attribute) => { f.write_str("AttributeMember(")?; - write!(f, "`{}`", attribute.ty().display(self.db))?; + write!(f, "`{}`", attribute.ty().display(db, self.env))?; if self.qualifiers.contains(TypeQualifiers::CLASS_VAR) { f.write_str("; ClassVar")?; } @@ -1474,6 +1650,7 @@ impl<'db> ProtocolMemberData<'db> { ProtocolMemberDataDisplay { db, + env, kind: self.kind, qualifiers: self.qualifiers, } @@ -1512,17 +1689,24 @@ impl<'db> ProtocolMemberKind<'db> { .flatten() } - fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + cycle: &salsa::Cycle, + ) -> Self { match (self, previous) { (Self::Method(current, kind), Self::Method(previous, _)) => { let (Type::Callable(current_callable), Type::Callable(previous_callable)) = (current.ty(), previous.ty()) else { - return Self::Method(current.cycle_normalized(db, previous, cycle), kind); + return Self::Method(current.cycle_normalized(db, env, previous, cycle), kind); }; debug_assert_eq!(current_callable.kind(db), previous_callable.kind(db)); let signatures = current_callable.signatures(db).cycle_normalized( db, + env, previous_callable.signatures(db), cycle, ); @@ -1546,19 +1730,19 @@ impl<'db> ProtocolMemberKind<'db> { write: previous_write, }, ) => Self::Property { - read: cycle_normalized_optional_type(db, current_read, previous_read, cycle), + read: cycle_normalized_optional_type(db, env, current_read, previous_read, cycle), write: match (current_write, previous_write) { (Some(current), Some(previous)) => { - Some(current.cycle_normalized(db, previous, cycle)) + Some(current.cycle_normalized(db, env, previous, cycle)) } (Some(current), None) => { - Some(current.cycle_normalized_without_previous(db, cycle)) + Some(current.cycle_normalized_without_previous(db, env, cycle)) } (None, _) => None, }, }, (Self::Attribute(current), Self::Attribute(previous)) => { - Self::Attribute(current.cycle_normalized(db, previous, cycle)) + Self::Attribute(current.cycle_normalized(db, env, previous, cycle)) } (current, _) => current, } @@ -1567,26 +1751,29 @@ impl<'db> ProtocolMemberKind<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(match self { Self::Method(member, kind) => Self::Method( - member.recursive_type_normalized_impl(db, div, nested)?, + member.recursive_type_normalized_impl(db, env, div, nested)?, kind, ), Self::Property { read, write } => Self::Property { read: match read { - Some(read) => Some(read.recursive_type_normalized_impl(db, div, nested)?), + Some(read) => Some(read.recursive_type_normalized_impl(db, env, div, nested)?), None => None, }, write: match write { - Some(write) => Some(write.recursive_type_normalized_impl(db, div, nested)?), + Some(write) => { + Some(write.recursive_type_normalized_impl(db, env, div, nested)?) + } None => None, }, }, Self::Attribute(attribute) => { - Self::Attribute(attribute.recursive_type_normalized_impl(db, div, nested)?) + Self::Attribute(attribute.recursive_type_normalized_impl(db, env, div, nested)?) } }) } @@ -1596,7 +1783,7 @@ impl<'db> ProtocolMemberKind<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { Self::Method(member, kind) => Self::Method( @@ -1643,7 +1830,7 @@ fn walk_protocol_member<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( visitor: &V, ) { if member.materialization.is_some() { - let capabilities = member.capabilities(db); + let capabilities = member.capabilities(db, visitor.program_environment()); for access in [capabilities.instance, capabilities.class] { for member_type in [ access.read, @@ -1681,9 +1868,13 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { /// /// Simple finite members are cheapest, followed by finite overloads. Recursive and /// alias-containing members are compared last because they can expand the same interface again. - fn structural_member_priority(&self, db: &'db dyn Db) -> StructuralMemberPriority { + fn structural_member_priority( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> StructuralMemberPriority { let is_recursive_type = |ty| { - any_over_type(db, ty, false, |nested| { + any_over_type(db, env, ty, false, |nested| { matches!(nested, Type::ProtocolInstance(_) | Type::TypeAlias(_)) }) }; @@ -1691,7 +1882,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { let ProtocolMemberKind::Method(member, _) = self.data.kind else { let is_finite = self.data.kind.member_types().all(|member| { member - .resolve(db) + .resolve(db, env) .is_some_and(|member| !is_recursive_type(member.ty())) }); return if is_finite { @@ -1869,24 +2060,33 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { self.data.definition } - fn capabilities(&self, db: &'db dyn Db) -> ProtocolMemberCapabilities<'db> { - let capabilities = self.data.capabilities(db); + fn capabilities( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> ProtocolMemberCapabilities<'db> { + let capabilities = self.data.capabilities(db, env); self.materialization - .map_or(capabilities, |kind| capabilities.materialize(db, kind)) + .map_or(capabilities, |kind| capabilities.materialize(db, env, kind)) } /// Materialize only the access that an operation actually observes. /// /// In particular, an instance-method relation must not materialize the class-side callable: /// its unbound receiver can recursively refer to the very protocol being compared. - fn access(&self, db: &'db dyn Db, mode: ProtocolMemberAccessMode) -> ProtocolMemberAccess<'db> { - let capabilities = self.data.capabilities(db); + fn access( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + mode: ProtocolMemberAccessMode, + ) -> ProtocolMemberAccess<'db> { + let capabilities = self.data.capabilities(db, env); let access = match mode { ProtocolMemberAccessMode::Instance => capabilities.instance, ProtocolMemberAccessMode::Class => capabilities.class, }; self.materialization - .map_or(access, |kind| access.materialize(db, kind)) + .map_or(access, |kind| access.materialize(db, env, kind)) } /// Returns the access that a candidate value must provide for this member. @@ -1898,6 +2098,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { fn implementation_access( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, mode: ProtocolMemberAccessMode, ) -> ProtocolMemberAccess<'db> { @@ -1915,43 +2116,45 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { { ProtocolMemberAccess::NONE } else { - self.access(db, mode) + self.access(db, env, mode) } } } fn property_get_member_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, getter: Type<'db>, ) -> Option> { let mut get_types = Vec::new(); let mut definition = None; - for callable in &getter.try_upcast_to_callable(db)? { + for callable in &getter.try_upcast_to_callable(db, env)? { for signature in callable.signatures(db) { get_types.push(signature.return_ty); definition = definition.or(signature.definition()); } } Some(ProtocolMemberType::with_definition( - UnionType::from_elements(db, get_types), + UnionType::from_elements(db, env, get_types), definition, )) } fn property_set_member_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, setter: Type<'db>, ) -> Option> { let mut set_types = Vec::new(); let mut definition = None; - for callable in &setter.try_upcast_to_callable(db)? { + for callable in &setter.try_upcast_to_callable(db, env)? { for signature in callable.signatures(db) { set_types.push(signature.parameters().get_positional(1)?.annotated_type()); definition = definition.or(signature.definition()); } } Some(ProtocolMemberType::with_definition( - UnionType::from_elements(db, set_types), + UnionType::from_elements(db, env, set_types), definition, )) } @@ -1959,6 +2162,7 @@ fn property_set_member_type<'db>( /// Derive the observable instance capabilities of a descriptor-decorated protocol member. fn descriptor_decorated_protocol_member<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, descriptor_ty: Type<'db>, protocol: ClassType<'db>, definition: Option>, @@ -1969,7 +2173,7 @@ fn descriptor_decorated_protocol_member<'db>( // variable can currently materialize that variable as `Unknown`. Reducing the descriptor to // its `__get__` result would then erase the remaining descriptor structure and weaken the // protocol member to a bare `Unknown`. - if super::visitor::any_over_type(db, descriptor_ty, false, |ty| ty.is_unknown()) { + if super::visitor::any_over_type(db, env, descriptor_ty, false, |ty| ty.is_unknown()) { return None; } @@ -1977,18 +2181,22 @@ fn descriptor_decorated_protocol_member<'db>( definedness: Definedness::AlwaysDefined, .. }) = descriptor_ty - .class_member_with_policy(db, "__get__", MemberLookupPolicy::REQUIRE_CONCRETE) + .class_member_with_policy(db, env, "__get__", MemberLookupPolicy::REQUIRE_CONCRETE) .place else { return None; }; - let receiver_ty = Type::instance(db, protocol); - let (read_ty, _) = - descriptor_ty.try_call_dunder_get(db, Some(receiver_ty), receiver_ty.to_meta_type(db))?; + let receiver_ty = Type::instance(db, env, protocol); + let (read_ty, _) = descriptor_ty.try_call_dunder_get( + db, + env, + Some(receiver_ty), + receiver_ty.to_meta_type(db, env), + )?; let read = Some(ProtocolMemberType::with_definition(read_ty, definition)); - let write = match descriptor_setter_domain(db, descriptor_ty, receiver_ty) { + let write = match descriptor_setter_domain(db, env, descriptor_ty, receiver_ty) { DescriptorSetterDomain::Missing => None, DescriptorSetterDomain::Known(domain) => Some(ProtocolMemberWrite::descriptor( descriptor_ty, @@ -2015,6 +2223,7 @@ enum DescriptorSetterDomain<'db> { /// Derive the values accepted by every possible descriptor setter when they fit in [`Type`]. fn descriptor_setter_domain<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, descriptor_ty: Type<'db>, receiver_ty: Type<'db>, ) -> DescriptorSetterDomain<'db> { @@ -2022,24 +2231,25 @@ fn descriptor_setter_domain<'db>( Type::Union(union) => { let mut write_types = Vec::with_capacity(union.elements(db).len()); for descriptor_ty in union.elements(db) { - match single_descriptor_setter_domain(db, *descriptor_ty, receiver_ty) { + match single_descriptor_setter_domain(db, env, *descriptor_ty, receiver_ty) { DescriptorSetterDomain::Missing => return DescriptorSetterDomain::Missing, DescriptorSetterDomain::Known(write_ty) => write_types.push(write_ty), DescriptorSetterDomain::Deferred => return DescriptorSetterDomain::Deferred, } } - IntersectionType::bounded_from_elements(db, write_types).map_or( + IntersectionType::bounded_from_elements(db, env, write_types).map_or( DescriptorSetterDomain::Deferred, DescriptorSetterDomain::Known, ) } - _ => single_descriptor_setter_domain(db, descriptor_ty, receiver_ty), + _ => single_descriptor_setter_domain(db, env, descriptor_ty, receiver_ty), } } /// Derive the values accepted by one possible runtime descriptor. fn single_descriptor_setter_domain<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, descriptor_ty: Type<'db>, receiver_ty: Type<'db>, ) -> DescriptorSetterDomain<'db> { @@ -2050,6 +2260,7 @@ fn single_descriptor_setter_domain<'db>( }) = descriptor_ty .member_lookup_with_policy( db, + env, "__set__", MemberLookupPolicy::REQUIRE_CONCRETE | MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -2058,14 +2269,15 @@ fn single_descriptor_setter_domain<'db>( return DescriptorSetterDomain::Missing; }; - let Some(callables) = setter_ty.try_upcast_to_callable(db) else { + let Some(callables) = setter_ty.try_upcast_to_callable(db, env) else { return DescriptorSetterDomain::Deferred; }; let mut callable_domains = Vec::with_capacity(callables.iter().len()); for callable in &callables { let mut write_types = Vec::new(); for signature in callable.signatures(db) { - match descriptor_setter_signature_domain(db, signature, descriptor_ty, receiver_ty) { + match descriptor_setter_signature_domain(db, env, signature, descriptor_ty, receiver_ty) + { DescriptorSetterSignatureDomain::Inapplicable => {} DescriptorSetterSignatureDomain::Known(write_ty) => write_types.push(write_ty), DescriptorSetterSignatureDomain::Deferred => { @@ -2073,9 +2285,9 @@ fn single_descriptor_setter_domain<'db>( } } } - callable_domains.push(UnionType::from_elements(db, write_types)); + callable_domains.push(UnionType::from_elements(db, env, write_types)); } - IntersectionType::bounded_from_elements(db, callable_domains).map_or( + IntersectionType::bounded_from_elements(db, env, callable_domains).map_or( DescriptorSetterDomain::Deferred, DescriptorSetterDomain::Known, ) @@ -2090,6 +2302,7 @@ enum DescriptorSetterSignatureDomain<'db> { /// Derive the values accepted by one `__set__` overload when they fit in [`Type`]. fn descriptor_setter_signature_domain<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, signature: &Signature<'db>, descriptor_ty: Type<'db>, receiver_ty: Type<'db>, @@ -2116,13 +2329,14 @@ fn descriptor_setter_signature_domain<'db>( let Some(receiver_parameter) = parameters.get_positional(0) else { return missing_required_parameter(); }; - let receiver_parameter = receiver_parameter - .annotated_type() - .bind_self_typevars(db, descriptor_ty); - if contains_signature_typevar(db, signature, receiver_parameter) { + let receiver_parameter = + receiver_parameter + .annotated_type() + .bind_self_typevars(db, env, descriptor_ty); + if contains_signature_typevar(db, env, signature, receiver_parameter) { return DescriptorSetterSignatureDomain::Deferred; } - if !receiver_ty.is_assignable_to(db, receiver_parameter) { + if !receiver_ty.is_assignable_to(db, env, receiver_parameter) { return DescriptorSetterSignatureDomain::Inapplicable; } @@ -2131,8 +2345,8 @@ fn descriptor_setter_signature_domain<'db>( }; let write_ty = write_parameter .annotated_type() - .bind_self_typevars(db, descriptor_ty); - if !contains_signature_typevar(db, signature, write_ty) { + .bind_self_typevars(db, env, descriptor_ty); + if !contains_signature_typevar(db, env, signature, write_ty) { return DescriptorSetterSignatureDomain::Known(write_ty); } @@ -2151,10 +2365,10 @@ fn descriptor_setter_signature_domain<'db>( return DescriptorSetterSignatureDomain::Deferred; } - match typevar.typevar(db).bound_or_constraints(db) { + match typevar.typevar(db).bound_or_constraints(db, env) { None => DescriptorSetterSignatureDomain::Known(Type::object()), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - DescriptorSetterSignatureDomain::Known(bound.bind_self_typevars(db, descriptor_ty)) + DescriptorSetterSignatureDomain::Known(bound.bind_self_typevars(db, env, descriptor_ty)) } Some(TypeVarBoundOrConstraints::Constraints(_)) => { DescriptorSetterSignatureDomain::Deferred @@ -2164,11 +2378,12 @@ fn descriptor_setter_signature_domain<'db>( fn contains_signature_typevar<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, signature: &Signature<'db>, ty: Type<'db>, ) -> bool { signature.generic_context.is_some_and(|generic_context| { - super::visitor::any_over_type(db, ty, true, |ty| { + super::visitor::any_over_type(db, env, ty, true, |ty| { matches!(ty, Type::TypeVar(typevar) if generic_context.contains(db, typevar.identity(db))) }) }) @@ -2176,10 +2391,11 @@ fn contains_signature_typevar<'db>( fn property_set_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, property: PropertyInstanceType<'db>, receiver_ty: Type<'db>, ) -> Option> { - property_set_member_type(db, property.setter(db)?)?.bind_self(db, receiver_ty) + property_set_member_type(db, env, property.setter(db)?)?.bind_self(db, env, receiver_ty) } fn is_class_object_type(ty: Type<'_>) -> bool { @@ -2191,6 +2407,7 @@ fn is_class_object_type(ty: Type<'_>) -> bool { fn protocol_member_read_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, receiver_ty: Type<'db>, member: &ProtocolMember<'_, 'db>, @@ -2214,8 +2431,10 @@ fn protocol_member_read_type<'db>( { Type::invoke_descriptor_protocol( db, + env, MemberLookupKey::new( db, + env.program(db), ty, member.name, // The undefined fallback excludes instance members. Keep the class @@ -2228,7 +2447,7 @@ fn protocol_member_read_type<'db>( ) .place } else { - receiver_ty.member(db, member.name).place + receiver_ty.member(db, env, member.name).place }; match place { @@ -2254,7 +2473,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { member_name: &str, value_ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { - let requirement = attribute_write_requirement(db, ty, member_name); + let env = self.env; + let requirement = attribute_write_requirement(db, env, ty, member_name); self.check_property_write_requirement(db, &requirement, member_name, value_ty) } @@ -2267,9 +2487,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ) -> ConstraintSet<'db, 'c> { match requirement { AttributeWriteRequirement::All { element_tys, .. } => { + let env = self.env; let mut result = self.always(); for element_ty in *element_tys { - let requirement = attribute_write_requirement(db, *element_ty, member_name); + let requirement = + attribute_write_requirement(db, env, *element_ty, member_name); let element_result = self.check_property_write_requirement( db, &requirement, @@ -2284,9 +2506,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { result } AttributeWriteRequirement::Any { intersection, .. } => { + let env = self.env; let mut result = self.never(); for element_ty in intersection.positive(db) { - let requirement = attribute_write_requirement(db, *element_ty, member_name); + let requirement = + attribute_write_requirement(db, env, *element_ty, member_name); let element_result = self.check_property_write_requirement( db, &requirement, @@ -2332,16 +2556,18 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { member_name: &str, value_ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; let setattr_result = object_ty.try_call_dunder_with_policy( db, + env, "__setattr__", &mut CallArguments::positional([Type::string_literal(db, member_name), value_ty]), TypeContext::default(), MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, ); if match &setattr_result { - Ok(bindings) => bindings.return_type(db).is_never(), - Err(error) => error.return_type(db).is_some_and(|ty| ty.is_never()), + Ok(bindings) => bindings.return_type(db, env).is_never(), + Err(error) => error.return_type(db, env).is_some_and(|ty| ty.is_never()), } { return self.never(); } @@ -2420,7 +2646,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .. } => { if let Some(property) = descriptor_ty.as_property_instance() - && let Some(set_type) = property_set_type(db, property, object_ty) + && let Some(set_type) = property_set_type(db, self.env, property, object_ty) { return self.check_type_pair(db, value_ty, set_type); } @@ -2446,9 +2672,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { object_ty: Type<'db>, value_ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; if setter_ty .try_call( db, + env, &CallArguments::positional([descriptor_ty, object_ty, Type::unknown()]), ) .is_err() @@ -2465,9 +2693,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { object_ty: Type<'db>, value_ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; let Place::Defined(DefinedPlace { ty: setattr_ty, .. }) = object_ty .member_lookup_with_policy( db, + env, "__setattr__", MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK | MemberLookupPolicy::NO_INSTANCE_FALLBACK, @@ -2503,8 +2733,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }); } + let env = self.env; callable_ty - .try_upcast_to_callable_with_policy(db, UpcastPolicy::from(self.relation)) + .try_upcast_to_callable_with_policy(db, env, UpcastPolicy::from(self.relation)) .when_some_and(db, self.constraints, |callables| { callables.iter().when_all(db, self.constraints, |callable| { callable.signatures(db).into_iter().when_any( @@ -2520,7 +2751,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }) }) .map(|parameter| { - parameter.annotated_type().bind_self_typevars(db, self_ty) + parameter + .annotated_type() + .bind_self_typevars(db, env, self_ty) }) .when_some_and(db, self.constraints, |write_ty| { self.check_type_pair(db, value_ty, write_ty) @@ -2558,7 +2791,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { required_ty: ProtocolMemberType<'db>, access: ProtocolMemberAccessMode, ) -> ConstraintSet<'db, 'c> { - let Some(attribute_type) = protocol_member_read_type(db, ty, receiver_ty, member, access) + let env = self.env; + let Some(attribute_type) = + protocol_member_read_type(db, env, ty, receiver_ty, member, access) else { return self.never(); }; @@ -2567,21 +2802,20 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // method on a class object names instances of that class: a `@classmethod` returning // `Self` returns `Factory`, not `type[Factory]`. Keep the bindings separate so a method // that returns an instance cannot satisfy a protocol that promises the class object. - let protocol_self_binding_ty = ty.literal_fallback_instance(db).unwrap_or(ty); + let protocol_self_binding_ty = ty.literal_fallback_instance(db, env).unwrap_or(ty); let implementation_self_binding_ty = ty - .to_instance_approximation(db) - .or_else(|| ty.literal_fallback_instance(db)) + .to_instance_approximation(db, env) + .or_else(|| ty.literal_fallback_instance(db, env)) .unwrap_or(ty); - let implementation_receiver_binding_ty = if member.is_class_method() { - implementation_self_binding_ty.to_meta_type(db) - } else { - implementation_self_binding_ty - }; - let protocol_receiver_binding_ty = if member.is_class_method() { - protocol_self_binding_ty.to_meta_type(db) - } else { - protocol_self_binding_ty - }; + let (implementation_receiver_binding_ty, protocol_receiver_binding_ty) = + if member.is_class_method() { + ( + implementation_self_binding_ty.to_meta_type(db, env), + protocol_self_binding_ty.to_meta_type(db, env), + ) + } else { + (implementation_self_binding_ty, protocol_self_binding_ty) + }; // Checking a class object against a protocol's instance capabilities can expose the // property descriptor itself rather than the value returned by its getter. Compatibility @@ -2593,20 +2827,21 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } if member.is_method() && access == ProtocolMemberAccessMode::Instance { - let Some(required_ty) = required_ty.resolve(db) else { + let Some(required_ty) = required_ty.resolve(db, env) else { return self.never(); }; let Type::Callable(required_callable) = required_ty.ty() else { return self.never(); }; attribute_type - .try_upcast_to_callable_with_policy(db, UpcastPolicy::from(self.relation)) + .try_upcast_to_callable_with_policy(db, env, UpcastPolicy::from(self.relation)) .when_some_and(db, self.constraints, |callables| { self.check_callables_vs_callable( db, &callables.map(|callable| { protocol_apply_self_with_receiver( db, + env.program(db), callable, implementation_receiver_binding_ty, implementation_self_binding_ty, @@ -2614,6 +2849,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }), protocol_apply_self_with_receiver( db, + env.program(db), required_callable, protocol_receiver_binding_ty, protocol_self_binding_ty, @@ -2621,22 +2857,23 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ) }) } else if member.is_instance_method() { - let Some(required_ty) = required_ty.resolve(db) else { + let Some(required_ty) = required_ty.resolve(db, env) else { return self.never(); }; let Type::Callable(required_callable) = required_ty.ty() else { return self.never(); }; attribute_type - .try_upcast_to_callable_with_policy(db, UpcastPolicy::from(self.relation)) + .try_upcast_to_callable_with_policy(db, env, UpcastPolicy::from(self.relation)) .when_some_and(db, self.constraints, |callables| { callables.iter().when_all(db, self.constraints, |callable| { if callable.is_function_like(db) { self.check_callable_pair( db, - callable.bind_self(db, Some(implementation_self_binding_ty)), + callable.bind_self(db, env, Some(implementation_self_binding_ty)), protocol_bind_self( db, + env.program(db), required_callable, Some(protocol_self_binding_ty), ), @@ -2647,7 +2884,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }) }) } else if member.is_method() { - let Some(required_ty) = required_ty.resolve(db) else { + let Some(required_ty) = required_ty.resolve(db, env) else { return self.never(); }; let Type::Callable(required_callable) = required_ty.ty() else { @@ -2658,6 +2895,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { attribute_type, Type::Callable(protocol_apply_self_with_receiver( db, + env.program(db), required_callable, protocol_receiver_binding_ty, protocol_self_binding_ty, @@ -2665,11 +2903,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ) } else { required_ty - .bind_self(db, protocol_self_binding_ty) + .bind_self(db, env, protocol_self_binding_ty) .when_some_and(db, self.constraints, |required_ty| { let result = self.check_type_pair(db, attribute_type, required_ty); if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::ProtocolMemberReadTypeIncompatible { source: attribute_type, @@ -2707,6 +2945,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { member.name == "__call__" || protocol_member_read_type( db, + self.env, ty, receiver_ty, member, @@ -2727,7 +2966,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { required.write.map_or_else( || self.always(), |write| { - let fallback_ty = ty.literal_fallback_instance(db).unwrap_or(ty); + let env = self.env; + let fallback_ty = ty.literal_fallback_instance(db, env).unwrap_or(ty); let receiver_ty = if access == ProtocolMemberAccessMode::Instance && matches!(ty, Type::LiteralValue(_)) { @@ -2736,12 +2976,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { receiver_ty }; write - .bind_compatibility_type(db, fallback_ty) + .bind_compatibility_type(db, env, fallback_ty) .when_some_and(db, self.constraints, |write_ty| { let result = self.check_property_write(db, receiver_ty, member.name, write_ty); if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::ProtocolMemberWriteTypeIncompatible { target: write_ty, @@ -2761,12 +3001,14 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ty: Type<'db>, member: &ProtocolMember<'_, 'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; let instance_access = - member.implementation_access(db, ty, ProtocolMemberAccessMode::Instance); + member.implementation_access(db, env, ty, ProtocolMemberAccessMode::Instance); if let Some(context) = self.report_context() { let instance_read_missing = instance_access.read.is_some() && protocol_member_read_type( db, + env, ty, ty, member, @@ -2774,13 +3016,14 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ) .is_none(); let class_access = - member.implementation_access(db, ty, ProtocolMemberAccessMode::Class); + member.implementation_access(db, env, ty, ProtocolMemberAccessMode::Class); let class_read_missing = class_access.read.is_some() && !(member.is_instance_method() && member.name == "__call__") && protocol_member_read_type( db, + env, ty, - ty.to_meta_type(db), + ty.to_meta_type(db, env), member, ProtocolMemberAccessMode::Class, ) @@ -2812,18 +3055,18 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ) .and(db, self.constraints, || { let class_access = - member.implementation_access(db, ty, ProtocolMemberAccessMode::Class); + member.implementation_access(db, env, ty, ProtocolMemberAccessMode::Class); self.type_satisfies_protocol_member_access( db, ty, - ty.to_meta_type(db), + ty.to_meta_type(db, env), member, class_access, ProtocolMemberAccessMode::Class, ) }); if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::ProtocolMemberIncompatible { member_name: member.name.into(), @@ -2845,11 +3088,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { meta_ty: Type<'db>, protocol: ProtocolInstanceType<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; protocol .interface(db) .members(db) .when_all(db, self.constraints, |member| { - let required = member.access(db, ProtocolMemberAccessMode::Class); + let required = member.access(db, env, ProtocolMemberAccessMode::Class); if required.read.is_none() && required.write.is_none() { return self.always(); } @@ -2880,7 +3124,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }; if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::ProtocolMemberIncompatible { member_name: member.name.into(), @@ -2902,7 +3146,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { target_member: &ProtocolMember<'_, 'db>, access: ProtocolMemberAccessMode, ) -> ConstraintSet<'db, 'c> { - let source = source_member.access(db, access); + let env = self.env; + let source = source_member.access(db, env, access); if access == ProtocolMemberAccessMode::Class && source_member.is_method() @@ -2912,7 +3157,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // access only establishes that the source member is also present on the class. return ConstraintSet::from_bool(self.constraints, source.read.is_some()); } - let target = target_member.access(db, access); + let target = target_member.access(db, env, access); let read_result = match (source.read, target.read) { (_, None) => self.always(), @@ -2920,18 +3165,19 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (Some(source), Some(target)) => { let bind_read = |member_type: ProtocolMemberType<'db>, member: &ProtocolMember<'_, 'db>| { - let member_type = member_type.resolve(db)?; + let member_type = member_type.resolve(db, env)?; if member.is_method() && let Type::Callable(callable) = member_type.ty() { Some(Type::Callable(protocol_apply_self_with_receiver( db, + env.program(db), callable, source_type, source_type, ))) } else { - member_type.bind_self(db, source_type) + member_type.bind_self(db, env, source_type) } }; let (Some(source), Some(target)) = ( @@ -2943,7 +3189,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let result = self.check_type_pair(db, source, target); if let Some(context) = self.report_context() && !target_member.is_method() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context .push(ErrorContext::ProtocolMemberReadTypeIncompatible { source, target }); @@ -2963,14 +3209,14 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } (Some(source), Some(target)) => { let (Some(target), Some(source)) = ( - target.bind_compatibility_type(db, source_type), - source.bind_compatibility_type(db, source_type), + target.bind_compatibility_type(db, env, source_type), + source.bind_compatibility_type(db, env, source_type), ) else { return self.never(); }; let result = self.check_type_pair(db, target, source); if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::ProtocolMemberWriteTypeIncompatible { target }); } @@ -2993,9 +3239,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return self.never(); } + let env = self.env; target .members(db) - .sorted_by_cached_key(|member| member.structural_member_priority(db)) + .sorted_by_cached_key(|member| member.structural_member_priority(db, env)) .when_all(db, self.constraints, |target_member| { let source_member = source.member_by_name(db, target_member.name); @@ -3028,7 +3275,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }) }); if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::ProtocolMemberIncompatible { member_name: target_member.name.into(), @@ -3050,8 +3297,9 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { member: &ProtocolMember<'_, 'db>, ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; if member - .access(db, ProtocolMemberAccessMode::Instance) + .access(db, env, ProtocolMemberAccessMode::Instance) .write .is_none() { @@ -3062,7 +3310,7 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { ty: Type::PropertyInstance(actual_property), definedness: Definedness::AlwaysDefined, .. - }) = ty.class_member(db, member.name()).place + }) = ty.class_member(db, env, member.name()).place else { return self.never(); }; @@ -3085,11 +3333,12 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { if member.is_property() && matches!(ty, Type::PropertyInstance(_)) { return self.never(); } - let access = member.access(db, ProtocolMemberAccessMode::Instance); + let env = self.env; + let access = member.access(db, env, ProtocolMemberAccessMode::Instance); if !member.is_method() { access.read.when_some_and(db, self.constraints, |read_ty| { read_ty - .resolve(db) + .resolve(db, env) .when_some_and(db, self.constraints, |read_ty| { self.check_type_pair(db, ty, read_ty.ty()) }) @@ -3097,7 +3346,7 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { } else { let Some(Type::Callable(method)) = access .read - .and_then(|read| read.resolve(db)) + .and_then(|read| read.resolve(db, env)) .map(ProtocolMemberType::ty) else { return self.never(); @@ -3106,7 +3355,8 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { return self.never(); } - let Some(callables) = ty.try_upcast_to_callable_with_policy(db, UpcastPolicy::Sound) + let Some(callables) = + ty.try_upcast_to_callable_with_policy(db, env, UpcastPolicy::Sound) else { return self.never(); }; @@ -3235,7 +3485,7 @@ impl<'db> ProtocolMemberCandidate<'db> { .into_iter() .flatten() { - if let Some(member) = member.resolve(db) { + if let Some(member) = member.resolve(db, visitor.program_environment()) { visitor.visit_type(db, member.ty()); } } @@ -3249,7 +3499,7 @@ impl<'db> ProtocolMemberCandidate<'db> { /// Inner Salsa query for [`ProtocolClass::interface`]. #[salsa::tracked( returns(copy), - cycle_initial=|db, _, _| ProtocolInterface::empty(db), + cycle_initial=protocol_interface_cycle_initial, cycle_fn=proto_interface_cycle_recover, heap_size=ruff_memory_usage::heap_size, )] @@ -3257,9 +3507,10 @@ fn cached_protocol_interface<'db>( db: &'db dyn Db, class: ClassType<'db>, ) -> ProtocolInterface<'db> { + let env = ProgramEnvironment::from_file(class.class_literal(db).python_file(db)); let mut members = BTreeMap::default(); - ProtocolClass(class).for_each_member_candidate(db, |name, candidate, specialization| { + ProtocolClass(class).for_each_member_candidate(db, &env, |name, candidate, specialization| { if members.contains_key(name) { return; } @@ -3282,20 +3533,20 @@ fn cached_protocol_interface<'db>( definition, ), Type::Callable(callable) if bound_on_class.is_yes() && callable.is_method_like(db) => { - ProtocolMemberData::method(db, callable, definition) + ProtocolMemberData::method(db, &env, callable, definition) } Type::FunctionLiteral(function) if bound_on_class.is_yes() || function.is_staticmethod(db) || function.is_classmethod(db) => { - ProtocolMemberData::method(db, function.into_callable_type(db), definition) + ProtocolMemberData::method(db, &env, function.into_callable_type(db), definition) } _ if bound_on_class.is_yes() && definition.is_some_and(|definition| definition.kind(db).is_function_def()) => { if let Some(descriptor) = - descriptor_decorated_protocol_member(db, ty, class, definition) + descriptor_decorated_protocol_member(db, &env, ty, class, definition) { descriptor } else { @@ -3308,7 +3559,18 @@ fn cached_protocol_interface<'db>( members.insert(name.clone(), member); }); - ProtocolInterface::new(db, members) + ProtocolInterface::new(db, env.program(db), members) +} + +fn protocol_interface_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + class: ClassType<'db>, +) -> ProtocolInterface<'db> { + ProtocolInterface::empty( + db, + &ProgramEnvironment::from_file(class.class_literal(db).python_file(db)), + ) } #[allow(clippy::trivially_copy_pass_by_ref)] @@ -3317,9 +3579,10 @@ fn proto_interface_cycle_recover<'db>( cycle: &salsa::Cycle, previous: &ProtocolInterface<'db>, value: ProtocolInterface<'db>, - _class: ClassType<'db>, + class: ClassType<'db>, ) -> ProtocolInterface<'db> { - value.cycle_normalized(db, *previous, cycle) + let env = ProgramEnvironment::from_file(class.class_literal(db).python_file(db)); + value.cycle_normalized(db, &env, *previous, cycle) } /// Bind `self` unless this is a `Callable[P, R]` dunder, and *also* discard the functionlike-ness @@ -3331,28 +3594,33 @@ fn proto_interface_cycle_recover<'db>( #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] fn protocol_bind_self<'db>( db: &'db dyn Db, + program: Program, callable: CallableType<'db>, self_type: Option>, ) -> CallableType<'db> { - callable.bind_self(db, self_type).into_regular(db) + let env = ProgramEnvironment::from_program(program); + callable.bind_self(db, &env, self_type).into_regular(db) } /// Cache receiver and `Self` binding only for protocol-member compatibility checks. #[salsa::tracked( returns(copy), - cycle_initial=|db, _, _, _, _| CallableType::bottom(db), + cycle_initial=|db, _, _, _, _, _| CallableType::bottom(db), heap_size=ruff_memory_usage::heap_size )] fn protocol_apply_self_with_receiver<'db>( db: &'db dyn Db, + program: Program, callable: CallableType<'db>, receiver_type: Type<'db>, self_type: Type<'db>, ) -> CallableType<'db> { + let env = ProgramEnvironment::from_program(program); + if receiver_type == self_type { - callable.apply_self(db, self_type) + callable.apply_self(db, &env, self_type) } else { - callable.apply_self_with_receiver(db, receiver_type, self_type) + callable.apply_self_with_receiver(db, &env, receiver_type, self_type) } } @@ -3377,6 +3645,7 @@ fn callable_has_only_non_never_returns<'db>(db: &'db dyn Db, callable: CallableT /// comparisons and generic protocol solving when the actual type is plainly missing a member. pub(super) fn has_all_protocol_members_defined<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, protocol: ProtocolInstanceType<'db>, ) -> bool { @@ -3393,7 +3662,7 @@ pub(super) fn has_all_protocol_members_defined<'db>( } _ => target_interface.members(db).all(|member| { matches!( - ty.member(db, member.name()).place, + ty.member(db, env, member.name()).place, Place::Defined(DefinedPlace { definedness: Definedness::AlwaysDefined, .. diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index ad0bb1e9a6..618ecd55d0 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use std::borrow::Cow; use itertools::Itertools; @@ -312,20 +313,33 @@ impl<'db> Type<'db> { /// Return true if this type is a subtype of type `target`. /// /// See [`TypeRelation::Subtyping`] for more details. - pub(crate) fn is_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { + pub(crate) fn is_subtype_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + ) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_subtype_of(db, target, &constraints, TypeVarSet::None) - .is_always_satisfied(db) + self.when_subtype_of(db, env, target, &constraints, TypeVarSet::None) + .is_always_satisfied(db, env) } pub(super) fn when_subtype_of<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, ) -> ConstraintSet<'db, 'c> { - self.has_relation_to(db, target, constraints, inferable, TypeRelation::Subtyping) + self.has_relation_to( + db, + env, + target, + constraints, + inferable, + TypeRelation::Subtyping, + ) } /// Return the constraints under which this type is a subtype of type `target`, assuming that @@ -335,6 +349,7 @@ impl<'db> Type<'db> { pub(super) fn when_subtype_of_assuming<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, assuming: ConstraintSet<'db, 'c>, constraints: &'c ConstraintSetBuilder<'db>, @@ -343,8 +358,9 @@ impl<'db> Type<'db> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker { + env, constraints, inferable, relation: TypeRelation::SubtypingAssuming, @@ -363,10 +379,15 @@ impl<'db> Type<'db> { /// Return true if this type is assignable to type `target`. /// /// See `TypeRelation::Assignability` for more details. - pub fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { + pub fn is_assignable_to( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + ) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_assignable_to(db, target, &constraints, TypeVarSet::None) - .is_always_satisfied(db) + self.when_assignable_to(db, env, target, &constraints, TypeVarSet::None) + .is_always_satisfied(db, env) } /// Re-run the assignability check with error context collection enabled. @@ -379,10 +400,12 @@ impl<'db> Type<'db> { pub(crate) fn assignability_error_context( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, ) -> ErrorContextTree<'db> { let builder = ConstraintSetBuilder::new(); let checker = TypeRelationChecker { + env, constraints: &builder, inferable: TypeVarSet::None, relation: TypeRelation::Assignability, @@ -393,7 +416,7 @@ impl<'db> Type<'db> { relation_visitor: &HasRelationToVisitor::default(&builder), disjointness_visitor: &IsDisjointVisitor::default(&builder), signature_relation_visitor: &SignatureRelationVisitor::default(), - materialization_visitor: &ApplyTypeMappingVisitor::default(), + materialization_visitor: &ApplyTypeMappingVisitor::new(env), }; checker.check_type_pair(db, self, target); checker.into_error_context() @@ -403,29 +426,37 @@ impl<'db> Type<'db> { pub(crate) fn is_constraint_set_assignable_to( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, ) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_constraint_set_assignable_to(db, target, &constraints) - .is_always_satisfied(db) + self.when_constraint_set_assignable_to(db, env, target, &constraints) + .is_always_satisfied(db, env) } /// Return true if this type is a subtype of `target` using constraint-set typevar rules. - pub(super) fn is_constraint_set_subtype_of(self, db: &'db dyn Db, target: Type<'db>) -> bool { + pub(super) fn is_constraint_set_subtype_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + ) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_constraint_set_subtype_of(db, target, &constraints) - .is_always_satisfied(db) + self.when_constraint_set_subtype_of(db, env, target, &constraints) + .is_always_satisfied(db, env) } pub(super) fn when_assignable_to<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, ) -> ConstraintSet<'db, 'c> { self.has_relation_to( db, + env, target, constraints, inferable, @@ -469,6 +500,7 @@ impl<'db> Type<'db> { pub(super) fn when_constraint_set_assignable_to_owned( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, ) -> Cow<'db, OwnedConstraintSet<'db>> { #[salsa::tracked( @@ -480,6 +512,8 @@ impl<'db> Type<'db> { db: &'db dyn Db, types: TypePair<'db>, ) -> OwnedConstraintSet<'db> { + let program = types.program(db); + let env = ProgramEnvironment::from_program(program); let constraints = ConstraintSetBuilder::new(); constraints.into_owned(|constraints| { let source = types.first(db); @@ -487,6 +521,7 @@ impl<'db> Type<'db> { source.has_relation_to_with_typevar_evaluation( db, + &env, target, constraints, TypeVarSet::None, @@ -500,20 +535,23 @@ impl<'db> Type<'db> { return Cow::Owned(OwnedConstraintSet::always()); } + let program = env.program(db); Cow::Borrowed(when_constraint_set_assignable_to_owned_impl( db, - TypePair::new(db, self, target), + TypePair::new(db, program, self, target), )) } pub(super) fn when_constraint_set_assignable_to<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { self.has_relation_to_with_typevar_evaluation( db, + env, target, constraints, TypeVarSet::None, @@ -525,11 +563,13 @@ impl<'db> Type<'db> { fn when_constraint_set_subtype_of<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { self.has_relation_to_with_typevar_evaluation( db, + env, target, constraints, TypeVarSet::None, @@ -541,31 +581,41 @@ impl<'db> Type<'db> { /// Return `true` if it would be redundant to add `self` to a union that already contains `other`. /// /// See [`TypeRelation::Redundancy`] for more details. - pub(super) fn is_redundant_with(self, db: &'db dyn Db, other: Type<'db>) -> bool { + pub(super) fn is_redundant_with( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: Type<'db>, + ) -> bool { #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| true, heap_size=ruff_memory_usage::heap_size)] fn is_redundant_with_impl<'db>(db: &'db dyn Db, types: TypePair<'db>) -> bool { + let program = types.program(db); + let env = ProgramEnvironment::from_program(program); types .first(db) .has_relation_to( db, + &env, types.second(db), &ConstraintSetBuilder::new(), TypeVarSet::None, TypeRelation::Redundancy { pure: false }, ) - .is_always_satisfied(db) + .is_always_satisfied(db, &env) } if self == other { return true; } - is_redundant_with_impl(db, TypePair::new(db, self, other)) + let program = env.program(db); + is_redundant_with_impl(db, TypePair::new(db, program, self, other)) } fn has_relation_to<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, @@ -573,6 +623,7 @@ impl<'db> Type<'db> { ) -> ConstraintSet<'db, 'c> { self.has_relation_to_with_typevar_evaluation( db, + env, target, constraints, inferable, @@ -581,9 +632,11 @@ impl<'db> Type<'db> { ) } + #[expect(clippy::too_many_arguments)] fn has_relation_to_with_typevar_evaluation<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, @@ -593,8 +646,9 @@ impl<'db> Type<'db> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker { + env, constraints, inferable, relation, @@ -622,16 +676,21 @@ impl<'db> Type<'db> { /// > — [Summary of type relations] /// /// [equivalent to]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent - pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool { - self.when_equivalent_to(db, other, &ConstraintSetBuilder::new()) - .is_always_satisfied(db) + pub(crate) fn is_equivalent_to( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: Type<'db>, + ) -> bool { + self.when_equivalent_to(db, env, other, &ConstraintSetBuilder::new()) + .is_always_satisfied(db, env) } pub(crate) fn is_equivalent_to_with_materialization_visitor( self, db: &'db dyn Db, other: Type<'db>, - materialization_visitor: &ApplyTypeMappingVisitor<'db>, + materialization_visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> bool { self.when_equivalent_to_with_materialization_visitor( db, @@ -639,16 +698,17 @@ impl<'db> Type<'db> { &ConstraintSetBuilder::new(), materialization_visitor, ) - .is_always_satisfied(db) + .is_always_satisfied(db, materialization_visitor.env) } pub(crate) fn when_equivalent_to<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); self.when_equivalent_to_with_materialization_visitor( db, other, @@ -662,12 +722,13 @@ impl<'db> Type<'db> { db: &'db dyn Db, other: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - materialization_visitor: &ApplyTypeMappingVisitor<'db>, + materialization_visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); let checker = EquivalenceChecker { + env: materialization_visitor.env, constraints, given: ConstraintSet::from_bool(constraints, false), perform_expensive_checks: true, @@ -694,15 +755,21 @@ impl<'db> Type<'db> { /// /// This function aims to have no false positives, but might return wrong /// `false` answers in some cases. - pub(crate) fn is_disjoint_from(self, db: &'db dyn Db, other: Type<'db>) -> bool { + pub(crate) fn is_disjoint_from( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: Type<'db>, + ) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_disjoint_from(db, other, &constraints, TypeVarSet::None) - .is_always_satisfied(db) + self.when_disjoint_from(db, env, other, &constraints, TypeVarSet::None) + .is_always_satisfied(db, env) } pub(crate) fn when_disjoint_from<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, @@ -710,8 +777,9 @@ impl<'db> Type<'db> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = DisjointnessChecker { + env, constraints, inferable, given: ConstraintSet::from_bool(constraints, false), @@ -730,6 +798,7 @@ impl<'db> Type<'db> { pub(crate) fn when_trivially_disjoint_from<'c>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, @@ -737,8 +806,9 @@ impl<'db> Type<'db> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = DisjointnessChecker { + env, constraints, inferable, given: ConstraintSet::from_bool(constraints, false), @@ -806,6 +876,7 @@ impl<'db, 'c> IsDisjointVisitor<'db, 'c> { #[derive(Clone)] pub(super) struct TypeRelationChecker<'a, 'c, 'db> { + pub(super) env: &'a ProgramEnvironment<'db>, pub(super) constraints: &'c ConstraintSetBuilder<'db>, pub(super) inferable: TypeVarSet<'db>, pub(super) relation: TypeRelation, @@ -823,19 +894,21 @@ pub(super) struct TypeRelationChecker<'a, 'c, 'db> { relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, pub(super) signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - pub(super) materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + pub(super) materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, } impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { pub(super) fn subtyping( + env: &'a ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, ) -> Self { Self { + env, constraints, inferable, relation: TypeRelation::Subtyping, @@ -851,13 +924,15 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } pub(super) fn constraint_set_assignability( + env: &'a ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, ) -> Self { Self { + env, constraints, inferable: TypeVarSet::None, relation: TypeRelation::Assignability, @@ -873,13 +948,15 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } pub(super) fn constraint_set_assignability_with_context( + env: &'a ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, ) -> Self { Self { + env, constraints, inferable: TypeVarSet::None, relation: TypeRelation::Assignability, @@ -895,13 +972,15 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } pub(super) fn assignability_with_context( + env: &'a ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, ) -> Self { Self { + env, constraints, inferable: TypeVarSet::None, relation: TypeRelation::Assignability, @@ -930,7 +1009,9 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { source: ClassType<'db>, target: ClassType<'db>, ) -> bool { + let env = self.env; Self::subtyping( + env, self.constraints, TypeVarSet::None, self.relation_visitor, @@ -939,7 +1020,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { self.materialization_visitor, ) .check_class_pair(db, source, target) - .is_always_satisfied(db) + .is_always_satisfied(db, env) } pub(super) const fn is_eager_assignability(&self) -> bool { @@ -1061,14 +1142,17 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { /// that are unrelated to each other in the regular-class domain (they do not inherit each /// other or any other common base), but they are all constrained to have a metaclass that /// inherits from `ABCMeta`. - fn is_metaclass_instance(db: &'db dyn Db, target: Type<'db>) -> bool { + fn is_metaclass_instance(&self, db: &'db dyn Db, target: Type<'db>) -> bool { target.as_nominal_instance().is_some_and(|instance| { + let env = self.env; KnownClass::Type - .try_to_class_literal(db) + .try_to_class_literal(db, env) .is_some_and(|type_class| { - instance - .class(db) - .is_subclass_of(db, ClassType::NonGeneric(ClassLiteral::Static(type_class))) + instance.class(db, env).is_subclass_of( + db, + env, + ClassType::NonGeneric(ClassLiteral::Static(type_class)), + ) }) }) } @@ -1110,17 +1194,19 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { target: Type<'db>, ) -> Option> { let source_i = source_subclass.into_type_var()?; - let is_exact_upper_bound = source_subclass.exact_typevar_upper_bound(db) == Some(target); + let env = self.env; + let is_exact_upper_bound = + source_subclass.exact_typevar_upper_bound(db, env) == Some(target); - if Self::is_metaclass_instance(db, target) { + if self.is_metaclass_instance(db, target) { return Some(self.check_type_pair( db, - source_subclass.to_metaclass_instance(db), + source_subclass.to_metaclass_instance(db, env), target, )); } - let projection = target.to_instance(db)?; + let projection = target.to_instance(db, env)?; if projection.is_exact() || is_exact_upper_bound { return Some(self.check_type_pair( db, @@ -1131,7 +1217,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { let source = source_subclass .subclass_of() - .with_transposed_type_var(db) + .with_transposed_type_var(db, env) .into_type_var()?; Some(self.check_type_pair(db, Type::TypeVar(source), target)) } @@ -1160,6 +1246,8 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { return self.always(); } + let env = self.env; + // Handle constraint implication first. If either `source` or `target` is a typevar, check // the constraint set to see if the corresponding constraint is satisfied. if self.relation == TypeRelation::SubtypingAssuming @@ -1167,7 +1255,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { { return self .given - .implies_subtype_of(db, self.constraints, source, target); + .implies_subtype_of(db, env, self.constraints, source, target); } // With lazy evaluation, comparisons with a type variable are translated directly into a @@ -1180,24 +1268,26 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // satisfies the upper bound/constraints). if let Type::TypeVar(bound_typevar) = source { let upper = if self.relation.is_subtyping() { - target.bottom_materialization(db) + target.bottom_materialization(db, env) } else { target }; return ConstraintSet::constrain_typevar_upper_bound( db, + env, self.constraints, bound_typevar, upper, ); } else if let Type::TypeVar(bound_typevar) = target { let lower = if self.relation.is_subtyping() { - source.top_materialization(db) + source.top_materialization(db, env) } else { source }; return ConstraintSet::constrain_typevar_lower_bound( db, + env, self.constraints, bound_typevar, lower, @@ -1265,7 +1355,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // that depend on multiple elements, such as all members of an enum, are visible. (_, Type::Union(union)) if union.has_aliases(db) => { self.with_recursion_guard(db, source, target, || { - self.check_type_pair(db, source, union.expand_aliases(db)) + self.check_type_pair(db, source, union.expand_aliases(db, env)) }) } @@ -1281,7 +1371,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::SubclassOf(source_subclass), Type::TypeForm(target_typeform)) => self .check_type_pair( db, - source_subclass.to_instance(db), + source_subclass.to_instance(db, env), target_typeform.type_argument(db), ), @@ -1294,25 +1384,25 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::ClassLiteral(source_class), Type::TypeForm(target_typeform)) => self .check_type_pair( db, - Type::instance(db, source_class.default_specialization(db)), + Type::instance(db, env, source_class.default_specialization(db)), target_typeform.type_argument(db), ), (Type::GenericAlias(source_alias), Type::TypeForm(target_typeform)) => self .check_type_pair( db, - Type::instance(db, ClassType::Generic(source_alias)), + Type::instance(db, env, ClassType::Generic(source_alias)), target_typeform.type_argument(db), ), (Type::KnownInstance(source_instance), Type::TypeForm(target_typeform)) - if let Some(source_argument) = source_instance.type_form_argument(db) => + if let Some(source_argument) = source_instance.type_form_argument(db, env) => { self.check_type_pair(db, source_argument, target_typeform.type_argument(db)) } (Type::SpecialForm(source_form), Type::TypeForm(target_typeform)) => source_form - .type_form_argument(db) + .type_form_argument(db, env) .when_some_and(db, self.constraints, |source_argument| { self.check_type_pair(db, source_argument, target_typeform.type_argument(db)) }), @@ -1324,15 +1414,15 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } (Type::EnumComplement(complement), Type::LiteralValue(_) | Type::Union(_)) => { - self.check_type_pair(db, complement.remaining_literal_union(db), target) + self.check_type_pair(db, complement.remaining_literal_union(db, env), target) } (Type::EnumComplement(complement), _) => { - self.check_type_pair(db, complement.to_intersection(db), target) + self.check_type_pair(db, complement.to_intersection(db, env), target) } (_, Type::EnumComplement(complement)) => { - self.check_type_pair(db, source, complement.to_intersection(db)) + self.check_type_pair(db, source, complement.to_intersection(db, env)) } // Field definitions in dataclasses and dataclass-transformers can involve calls to @@ -1399,10 +1489,10 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { Type::KnownInstance(KnownInstanceType::FunctoolsPartial(partial)), Type::NominalInstance(target_instance), ) if target_instance - .class(db) + .class(db, env) .is_known(db, KnownClass::FunctoolsPartial) => { - let specialized = partial.partial(db).into_functools_partial_instance(db); + let specialized = partial.partial(db).into_functools_partial_instance(db, env); self.check_type_pair(db, specialized, target) } @@ -1497,7 +1587,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // "collapse to 'object'" in this case is a sound over-approximation.) (_, Type::SubclassOf(subclass_of)) if let Some(type_var) = subclass_of.into_type_var() - && let Some(instance) = source.to_instance_approximation(db) => + && let Some(instance) = source.to_instance_approximation(db, env) => { self.check_type_pair(db, instance, Type::TypeVar(type_var)) } @@ -1512,7 +1602,11 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { { self.check_type_pair( db, - Type::tuple(Some(TupleType::unpacked_typevartuple(db, bound_typevar))), + Type::tuple(Some(TupleType::unpacked_typevartuple( + db, + env, + bound_typevar, + ))), target, ) } @@ -1524,7 +1618,11 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { self.check_type_pair( db, source, - Type::tuple(Some(TupleType::unpacked_typevartuple(db, bound_typevar))), + Type::tuple(Some(TupleType::unpacked_typevartuple( + db, + env, + bound_typevar, + ))), ) } @@ -1558,7 +1656,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::TypeVar(bound_typevar), _) if !bound_typevar.is_inferable(db, self.inferable) && let Some(bound_or_constraints) = - bound_typevar.typevar(db).bound_or_constraints(db) => + bound_typevar.typevar(db).bound_or_constraints(db, env) => { match bound_or_constraints { TypeVarBoundOrConstraints::UpperBound(bound) => { @@ -1581,13 +1679,13 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { if !bound_typevar.is_inferable(db, self.inferable) && let constraints = bound_typevar .typevar(db) - .constraints(db) + .constraints(db, env) .when_some_and(db, self.constraints, |constraints| { constraints.iter().when_all(db, self.constraints, |c| { self.check_type_pair(db, source, *c) }) }) - && !constraints.is_never_satisfied(db) => + && !constraints.is_never_satisfied(db, env) => { constraints } @@ -1625,7 +1723,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } (Type::Union(union), _) => { - if let Some(supertype) = union.common_literal_supertype(db) { + if let Some(supertype) = union.common_literal_supertype(db, env) { // Use the broader supertype only as a positive proof. If it has the requested // relation to the target, then every literal in the union does too. Otherwise, // check each literal individually. @@ -1642,7 +1740,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .when_all(db, self.constraints, |&elem_ty| { let constraint_set = self.check_type_pair(db, elem_ty, target); if let Some(context) = self.report_context() - && constraint_set.is_never_satisfied(db) + && constraint_set.is_never_satisfied(db, env) { context.push(ErrorContext::NotAllUnionElementsAssignable { element: elem_ty, @@ -1656,7 +1754,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (_, Type::Union(union)) => { if let Type::Intersection(intersection) = source - && let Some(alternatives) = intersection.finite_alternative_union(db) + && let Some(alternatives) = intersection.finite_alternative_union(db, env) { return self.check_type_pair(db, alternatives, target); } @@ -1673,7 +1771,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { { self.check_type_pair( db, - intersection.with_expanded_typevars_and_newtypes(db), + intersection.with_expanded_typevars_and_newtypes(db, env), target, ) } @@ -1698,9 +1796,9 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .when_any(db, self.constraints, |&elem_ty| { let result = self.check_type_pair(db, source, elem_ty); if let Some(context_tree) = context_tree { - let ctx = context_tree.take(); - if !ctx.is_empty() { - elements_context.push(ctx); + let env = context_tree.take(); + if !env.is_empty() { + elements_context.push(env); } } result @@ -1709,7 +1807,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { if context_tree.is_some() && !elements_context.is_empty() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { let elements_without_context = elements.len() - elements_context.len(); if elements_without_context > 0 && elements_without_context < elements.len() { @@ -1741,7 +1839,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .when_all(db, self.constraints, |&pos_ty| { let constraint_set = self.check_type_pair(db, source, pos_ty); if let Some(context) = self.report_context() - && constraint_set.is_never_satisfied(db) + && constraint_set.is_never_satisfied(db, env) { context.push(ErrorContext::NotAssignableToIntersectionElement { source, @@ -1769,7 +1867,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { TypeRelation::Subtyping | TypeRelation::Redundancy { .. } | TypeRelation::SubtypingAssuming => source, - TypeRelation::Assignability => source.bottom_materialization(db), + TypeRelation::Assignability => source.bottom_materialization(db, env), }; intersection .negative(db) @@ -1779,7 +1877,9 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { TypeRelation::Subtyping | TypeRelation::Redundancy { .. } | TypeRelation::SubtypingAssuming => neg_ty, - TypeRelation::Assignability => neg_ty.bottom_materialization(db), + TypeRelation::Assignability => { + neg_ty.bottom_materialization(db, env) + } }; self.as_disjointness_checker() .check_type_pair(db, source_ty, neg_ty) @@ -1788,7 +1888,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::Intersection(intersection), _) => { if matches!(target, Type::LiteralValue(_)) - && let Some(alternatives) = intersection.finite_alternative_union(db) + && let Some(alternatives) = intersection.finite_alternative_union(db, env) { return self.check_type_pair(db, alternatives, target); } @@ -1806,9 +1906,9 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .when_any(db, self.constraints, |elem_ty| { let result = self.check_type_pair(db, elem_ty, target); if let Some(context_tree) = context_tree { - let ctx = context_tree.take(); - if !ctx.is_empty() { - elements_context.push(ctx); + let env = context_tree.take(); + if !env.is_empty() { + elements_context.push(env); } } result @@ -1817,7 +1917,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { if should_expand_intersection(intersection) { self.check_type_pair( db, - intersection.with_expanded_typevars_and_newtypes(db), + intersection.with_expanded_typevars_and_newtypes(db, env), target, ) } else { @@ -1827,7 +1927,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { if context_tree.is_some() && !elements_context.is_empty() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { self.set_context( ErrorContext::NoIntersectionElementAssignableToTarget { @@ -1859,7 +1959,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (_, Type::TypeVar(typevar)) if typevar.is_inferable(db, self.inferable) => { if self.is_eager_assignability() { // TODO: record the unification constraints - typevar.typevar(db).upper_bound(db).when_none_or( + typevar.typevar(db).upper_bound(db, env).when_none_or( db, self.constraints, |bound| self.check_type_pair(db, source, bound), @@ -1885,10 +1985,10 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`. // If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively. (_, Type::AlwaysFalsy) => { - ConstraintSet::from_bool(self.constraints, source.bool(db).is_always_false()) + ConstraintSet::from_bool(self.constraints, source.bool(db, env).is_always_false()) } (_, Type::AlwaysTruthy) => { - ConstraintSet::from_bool(self.constraints, source.bool(db).is_always_true()) + ConstraintSet::from_bool(self.constraints, source.bool(db, env).is_always_true()) } // Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance). (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => { @@ -1980,9 +2080,11 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (_, Type::Callable(target_callable)) => { self.with_recursion_guard(db, source, target, || { - let Some(callables) = source - .try_upcast_to_callable_with_policy(db, UpcastPolicy::from(self.relation)) - else { + let Some(callables) = source.try_upcast_to_callable_with_policy( + db, + env, + UpcastPolicy::from(self.relation), + ) else { return self.never(); }; @@ -1990,11 +2092,11 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { if let Some(context) = self.report_context() && self.should_provide_callable_upcast_context(source) - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) { context.push(ErrorContext::InferredCallableType { source, - callable: callables.into_type(db), + callable: callables.into_type(db, env), }); } @@ -2012,7 +2114,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { if (source_subclass_ty.is_dynamic() || source_subclass_ty.is_type_var()) && !self.is_eager_assignability() => { - self.check_type_pair(db, KnownClass::Type.to_instance(db), target) + self.check_type_pair(db, KnownClass::Type.to_instance(db, env), target) } (_, Type::ProtocolInstance(target_proto)) => { @@ -2033,26 +2135,31 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::TypedDict(typed_dict), _) => { self.with_recursion_guard(db, source, target, || { let dict_value_type = if self.relation.is_assignability() { - typed_dict.assignable_dict_value_type(db) + typed_dict.assignable_dict_value_type(db, env) } else { - typed_dict.dict_value_type(db) + typed_dict.dict_value_type(db, env) }; let fallback = if let Some(value_ty) = dict_value_type { KnownClass::Dict.to_specialized_instance( db, - &[KnownClass::Str.to_instance(db), value_ty], + env, + &[KnownClass::Str.to_instance(db, env), value_ty], ) } else { KnownClass::Mapping.to_specialized_instance( db, - &[KnownClass::Str.to_instance(db), typed_dict.value_type(db)], + env, + &[ + KnownClass::Str.to_instance(db, env), + typed_dict.value_type(db, env), + ], ) }; let result = self.check_type_pair(db, fallback, target); if let Some(context) = self.report_context() - && result.is_never_satisfied(db) + && result.is_never_satisfied(db, env) && let Type::NominalInstance(instance) = target - && instance.class(db).is_known(db, KnownClass::Dict) + && instance.class(db, env).is_known(db, KnownClass::Dict) { context.push(ErrorContext::TypedDictNotAssignableToDict(typed_dict)); } @@ -2068,13 +2175,13 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::LiteralValue(literal), Type::NominalInstance(instance)) if let Some(value) = literal.as_string() => { - let target_class = instance.class(db); + let target_class = instance.class(db, env); if target_class.is_known(db, KnownClass::Str) { return self.always(); } - if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db) + if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db, env) && !sequence_class .iter_mro(db, None) .filter_map(ClassBase::into_class) @@ -2102,7 +2209,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { }; KnownClass::Sequence - .to_specialized_class_type(db, &[spec]) + .to_specialized_class_type(db, env, &[spec]) .when_some_and(db, self.constraints, |sequence| { self.check_class_pair(db, sequence, target_class) }) @@ -2115,13 +2222,13 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::LiteralValue(literal), Type::NominalInstance(instance)) if let Some(value) = literal.as_bytes() => { - let target_class = instance.class(db); + let target_class = instance.class(db, env); if target_class.is_known(db, KnownClass::Bytes) { return self.always(); } - if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db) + if let Some(sequence_class) = KnownClass::Sequence.try_to_class_literal(db, env) && !sequence_class .iter_mro(db, None) .filter_map(ClassBase::into_class) @@ -2148,7 +2255,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { }; KnownClass::Sequence - .to_specialized_class_type(db, &[spec]) + .to_specialized_class_type(db, env, &[spec]) .when_some_and(db, self.constraints, |sequence| { self.check_class_pair(db, sequence, target_class) }) @@ -2161,7 +2268,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { (Type::NominalInstance(_), Type::LiteralValue(literal)) if let Some(target_enum_literal) = literal.as_enum() => { - if target_enum_literal.enum_class_instance(db) != source { + if target_enum_literal.enum_class_instance(db, env) != source { self.never() } else { ConstraintSet::from_bool( @@ -2175,7 +2282,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // most `Literal` types delegate to their instance fallbacks // unless `source` is exactly equivalent to `target` (handled above) (Type::ModuleLiteral(_) | Type::LiteralValue(_) | Type::FunctionLiteral(_), _) => { - source.literal_fallback_instance(db).when_some_and( + source.literal_fallback_instance(db, env).when_some_and( db, self.constraints, |source_instance| self.check_type_pair(db, source_instance, target), @@ -2184,14 +2291,14 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // The same reasoning applies for these special callable types: (Type::BoundMethod(_), _) => { - self.check_type_pair(db, KnownClass::MethodType.to_instance(db), target) + self.check_type_pair(db, KnownClass::MethodType.to_instance(db, env), target) } (Type::KnownBoundMethod(method), _) => { - self.check_type_pair(db, method.class().to_instance(db), target) + self.check_type_pair(db, method.class().to_instance(db, env), target) } (Type::WrapperDescriptor(_), _) => self.check_type_pair( db, - KnownClass::WrapperDescriptorType.to_instance(db), + KnownClass::WrapperDescriptorType.to_instance(db, env), target, ), @@ -2217,12 +2324,12 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // `TypeIs[T]` and `TypeGuard[T]` are subtypes of `bool`. (Type::TypeIs(_) | Type::TypeGuard(_), _) => { - self.check_type_pair(db, KnownClass::Bool.to_instance(db), target) + self.check_type_pair(db, KnownClass::Bool.to_instance(db, env), target) } // Function-like callables are subtypes of `FunctionType` (Type::Callable(callable), _) if callable.is_function_like(db) => { - self.check_type_pair(db, KnownClass::FunctionType.to_instance(db), target) + self.check_type_pair(db, KnownClass::FunctionType.to_instance(db, env), target) } (Type::Callable(_), _) => self.never(), @@ -2232,7 +2339,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { .check_bound_super_pair(db, source, target), (Type::BoundSuper(_), _) => { - self.check_type_pair(db, KnownClass::Super.to_instance(db), target) + self.check_type_pair(db, KnownClass::Super.to_instance(db, env), target) } (Type::SubclassOf(subclass_of), _) | (_, Type::SubclassOf(subclass_of)) @@ -2252,7 +2359,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { target_protocol, ), target => target - .into_class(db) + .into_class(db, env) .map(|target_cls| { self.check_class_pair( db, @@ -2297,7 +2404,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { target_protocol, ), target => target - .into_class(db) + .into_class(db, env) .map(|target_cls| { self.check_class_pair(db, ClassType::Generic(source_alias), target_cls) }) @@ -2319,30 +2426,30 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object // is an instance of its metaclass `abc.ABCMeta`. (Type::ClassLiteral(source_class), _) => { - self.check_type_pair(db, source_class.metaclass_instance_type(db), target) + self.check_type_pair(db, source_class.metaclass_instance_type(db, env), target) } (Type::GenericAlias(source_alias), _) => self.check_type_pair( db, - ClassType::Generic(source_alias).metaclass_instance_type(db), + ClassType::Generic(source_alias).metaclass_instance_type(db, env), target, ), // `type[Any]` is a subtype of `type[object]`, and is assignable to any `type[...]` - (Type::SubclassOf(subclass_of_ty), _) if subclass_of_ty.is_dynamic() => { - self.check_type_pair(db, KnownClass::Type.to_instance(db), target) - .or(db, self.constraints, || { - ConstraintSet::from_bool(self.constraints, self.is_eager_assignability()) - .and(db, self.constraints, || { - self.check_type_pair(db, target, KnownClass::Type.to_instance(db)) - }) - }) - } + (Type::SubclassOf(subclass_of_ty), _) if subclass_of_ty.is_dynamic() => self + .check_type_pair(db, KnownClass::Type.to_instance(db, env), target) + .or(db, self.constraints, || { + ConstraintSet::from_bool(self.constraints, self.is_eager_assignability()).and( + db, + self.constraints, + || self.check_type_pair(db, target, KnownClass::Type.to_instance(db, env)), + ) + }), // Any `type[...]` type is assignable to `type[Any]` (_, Type::SubclassOf(subclass_of_ty)) if subclass_of_ty.is_dynamic() && self.is_eager_assignability() => { - self.check_type_pair(db, source, KnownClass::Type.to_instance(db)) + self.check_type_pair(db, source, KnownClass::Type.to_instance(db, env)) } // `type[str]` (== `SubclassOf("str")` in ty) describes all possible runtime subclasses @@ -2356,9 +2463,9 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { db, subclass_of_ty .subclass_of() - .into_class(db) - .map(|source_class| source_class.metaclass_instance_type(db)) - .unwrap_or_else(|| KnownClass::Type.to_instance(db)), + .into_class(db, env) + .map(|source_class| source_class.metaclass_instance_type(db, env)) + .unwrap_or_else(|| KnownClass::Type.to_instance(db, env)), target, ), @@ -2368,11 +2475,11 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { // because `Type::SpecialForm(SpecialFormType::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::SpecialForm(source_form), _) => { - self.check_type_pair(db, source_form.instance_fallback(db), target) + self.check_type_pair(db, source_form.instance_fallback(db, env), target) } (Type::KnownInstance(source), _) => { - self.check_type_pair(db, source.instance_fallback(db), target) + self.check_type_pair(db, source.instance_fallback(db, env), target) } // `bool` is a subtype of `int`, because `bool` subclasses `int`, @@ -2388,10 +2495,10 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { }), (Type::PropertyInstance(property), _) => { - self.check_type_pair(db, property.instance_fallback(db), target) + self.check_type_pair(db, property.instance_fallback(db, env), target) } (_, Type::PropertyInstance(property)) => { - self.check_type_pair(db, source, property.instance_fallback(db)) + self.check_type_pair(db, source, property.instance_fallback(db, env)) } // Other than the special cases enumerated above, nominal-instance types are never // subtypes of any other variants @@ -2405,6 +2512,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { source: PropertyInstanceType<'db>, target: PropertyInstanceType<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; let check_optional_methods = |source, target| match (source, target) { (None, None) => self.always(), (Some(source), Some(target)) => self.check_type_pair(db, source, target), @@ -2413,8 +2521,8 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { self.check_type_pair( db, - source.instance_fallback(db), - target.instance_fallback(db), + source.instance_fallback(db, env), + target.instance_fallback(db, env), ) .and(db, self.constraints, || { check_optional_methods(source.getter(db), target.getter(db)).and( @@ -2433,6 +2541,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { fn as_equivalence_checker(&self) -> EquivalenceChecker<'_, 'c, 'db> { EquivalenceChecker { + env: self.env, constraints: self.constraints, given: self.given, perform_expensive_checks: self.perform_expensive_checks, @@ -2445,6 +2554,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { pub(super) fn as_disjointness_checker(&self) -> DisjointnessChecker<'_, 'c, 'db> { DisjointnessChecker { + env: self.env, constraints: self.constraints, inferable: self.inferable, given: self.given, @@ -2485,6 +2595,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { } pub(super) struct EquivalenceChecker<'a, 'c, 'db> { + pub(super) env: &'a ProgramEnvironment<'db>, pub(super) constraints: &'c ConstraintSetBuilder<'db>, given: ConstraintSet<'db, 'c>, perform_expensive_checks: bool, @@ -2498,15 +2609,16 @@ pub(super) struct EquivalenceChecker<'a, 'c, 'db> { relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, } impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { fn as_relation_checker<'a>( &'a self, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, ) -> TypeRelationChecker<'a, 'c, 'db> { TypeRelationChecker { + env: self.env, relation: TypeRelation::Redundancy { pure: true }, typevar_evaluation: TypeVarEvaluation::Eager, constraints: self.constraints, @@ -2552,6 +2664,7 @@ impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { } pub(super) struct DisjointnessChecker<'a, 'c, 'db> { + pub(super) env: &'a ProgramEnvironment<'db>, pub(super) constraints: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, given: ConstraintSet<'db, 'c>, @@ -2566,19 +2679,21 @@ pub(super) struct DisjointnessChecker<'a, 'c, 'db> { disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, } impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { pub(super) fn new( + env: &'a ProgramEnvironment<'db>, constraints: &'c ConstraintSetBuilder<'db>, inferable: TypeVarSet<'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, signature_relation_visitor: &'a SignatureRelationVisitor<'db>, - materialization_visitor: &'a ApplyTypeMappingVisitor<'db>, + materialization_visitor: &'a ApplyTypeMappingVisitor<'a, 'db>, ) -> Self { Self { + env, constraints, inferable, given: ConstraintSet::from_bool(constraints, false), @@ -2595,6 +2710,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { relation: TypeRelation, ) -> TypeRelationChecker<'_, 'c, 'db> { TypeRelationChecker { + env: self.env, relation, typevar_evaluation: TypeVarEvaluation::Eager, constraints: self.constraints, @@ -2611,6 +2727,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { fn as_equivalence_checker(&self) -> EquivalenceChecker<'_, 'c, 'db> { EquivalenceChecker { + env: self.env, constraints: self.constraints, given: self.given, perform_expensive_checks: self.perform_expensive_checks, @@ -2637,12 +2754,13 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { protocol: ProtocolInstanceType<'db>, other: Type<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; protocol .interface(db) .members(db) .when_any(db, self.constraints, |member| { other - .member(db, member.name()) + .member(db, env, member.name()) .place .ignore_possibly_undefined() .when_none_or(db, self.constraints, |attribute_type| { @@ -2725,6 +2843,8 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { return self.check_type_pair(db, left, right); } + let env = self.env; + match (left, right) { (Type::Never, _) | (_, Type::Never) => self.always(), @@ -2746,11 +2866,11 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { }), (Type::EnumComplement(complement), other) => nontrivial_check(self, || { - self.check_type_pair(db, complement.remaining_literal_union(db), other) + self.check_type_pair(db, complement.remaining_literal_union(db, env), other) }), (other, Type::EnumComplement(complement)) => nontrivial_check(self, || { - self.check_type_pair(db, other, complement.remaining_literal_union(db)) + self.check_type_pair(db, other, complement.remaining_literal_union(db, env)) }), // `type[T]` and `TypeForm[S]` overlap whenever their represented instance types do. @@ -2759,7 +2879,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { nontrivial_check(self, || { self.check_type_pair( db, - subclass_of.to_instance(db), + subclass_of.to_instance(db, env), typeform.type_argument(db), ) }) @@ -2775,7 +2895,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { Type::SubclassOf(subclass_of), ) if let Some(type_var) = subclass_of .subclass_of() - .with_transposed_type_var(db) + .with_transposed_type_var(db, env) .into_type_var() => { nontrivial_check(self, || { @@ -2786,7 +2906,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // `type[T]` is disjoint from a class object `A` if every instance of `T` is disjoint from an instance of `A`. (Type::SubclassOf(subclass_of), other) | (other, Type::SubclassOf(subclass_of)) if let Some(type_var) = subclass_of.into_type_var() - && let Some(instance) = other.to_instance_approximation(db) => + && let Some(instance) = other.to_instance_approximation(db, env) => { nontrivial_check(self, || { self.check_type_pair(db, Type::TypeVar(type_var), instance) @@ -2819,17 +2939,19 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { (Type::TypeVar(tvar), other) | (other, Type::TypeVar(tvar)) if !tvar.is_inferable(db, self.inferable) => { - nontrivial_check(self, || match tvar.typevar(db).bound_or_constraints(db) { - None => self.never(), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - self.check_type_pair(db, bound, other) - } - Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { - typevar_constraints.elements(db).iter().when_all( - db, - self.constraints, - |constraint| self.check_type_pair(db, *constraint, other), - ) + nontrivial_check(self, || { + match tvar.typevar(db).bound_or_constraints(db, env) { + None => self.never(), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + self.check_type_pair(db, bound, other) + } + Some(TypeVarBoundOrConstraints::Constraints(typevar_constraints)) => { + typevar_constraints.elements(db).iter().when_all( + db, + self.constraints, + |constraint| self.check_type_pair(db, *constraint, other), + ) + } } }) } @@ -2853,10 +2975,11 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // This is similar to what would happen if we tried to build a new intersection that combines the two (Type::Intersection(left_intersection), Type::Intersection(right_intersection)) => { nontrivial_check(self, || { - if let Some(alternatives) = left_intersection.finite_alternative_union(db) { + if let Some(alternatives) = left_intersection.finite_alternative_union(db, env) + { self.check_type_pair(db, alternatives, right) } else if let Some(alternatives) = - right_intersection.finite_alternative_union(db) + right_intersection.finite_alternative_union(db, env) { self.check_type_pair(db, left, alternatives) } else { @@ -2880,7 +3003,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { } (Type::Intersection(intersection), other) => nontrivial_check(self, || { - if let Some(alternatives) = intersection.finite_alternative_union(db) { + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { self.check_type_pair(db, alternatives, other) } else { self.check_intersection_pair_via_elements(db, left, right, intersection, other) @@ -2888,7 +3011,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { }), (other, Type::Intersection(intersection)) => nontrivial_check(self, || { - if let Some(alternatives) = intersection.finite_alternative_union(db) { + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { self.check_type_pair(db, other, alternatives) } else { self.check_intersection_pair_via_elements(db, left, right, intersection, other) @@ -2981,13 +3104,13 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint. // Thus, they are only disjoint if `ty.bool() == AlwaysFalse`. nontrivial_check(self, || { - ConstraintSet::from_bool(self.constraints, ty.bool(db).is_always_false()) + ConstraintSet::from_bool(self.constraints, ty.bool(db, env).is_always_false()) }) } (Type::AlwaysFalsy, ty) | (ty, Type::AlwaysFalsy) => { // Similarly, they are only disjoint if `ty.bool() == AlwaysTrue`. nontrivial_check(self, || { - ConstraintSet::from_bool(self.constraints, ty.bool(db).is_always_true()) + ConstraintSet::from_bool(self.constraints, ty.bool(db, env).is_always_true()) }) } @@ -3006,7 +3129,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { self.any_protocol_members_absent_or_disjoint( db, protocol, - special_form.instance_fallback(db), + special_form.instance_fallback(db, env), ) }) }) @@ -3019,7 +3142,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { self.any_protocol_members_absent_or_disjoint( db, protocol, - known_instance.instance_fallback(db), + known_instance.instance_fallback(db, env), ) }) }) @@ -3078,7 +3201,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // () (Type::ProtocolInstance(protocol), Type::NominalInstance(nominal)) | (Type::NominalInstance(nominal), Type::ProtocolInstance(protocol)) - if self.perform_expensive_checks && nominal.class(db).is_final(db) => + if self.perform_expensive_checks && nominal.class(db, env).is_final(db) => { nontrivial_check(self, || { self.with_recursion_guard(db, left, right, || { @@ -3098,7 +3221,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { .interface(db) .members(db) .when_any(db, self.constraints, |member| { - match other.member(db, member.name()).place { + match other.member(db, env, member.name()).place { Place::Defined(DefinedPlace { ty: attribute_type, .. }) => self.protocol_member_has_disjoint_type_from_ty( @@ -3160,6 +3283,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { self.constraints, !class_a.could_exist_in_mro_of_with_disjointness_checker( db, + env, ClassType::NonGeneric(class_b), self, ), @@ -3179,6 +3303,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { self.constraints, !class_a.could_exist_in_mro_of_with_disjointness_checker( db, + env, ClassType::Generic(alias_b), self, ), @@ -3198,13 +3323,13 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { | (other, Type::SubclassOf(subclass_of_ty)) => { nontrivial_check(self, || match subclass_of_ty.subclass_of() { SubclassOfInner::Dynamic(_) => { - self.check_type_pair(db, KnownClass::Type.to_instance(db), other) + self.check_type_pair(db, KnownClass::Type.to_instance(db, env), other) } SubclassOfInner::Class(class) => { - self.check_type_pair(db, class.metaclass_instance_type(db), other) + self.check_type_pair(db, class.metaclass_instance_type(db, env), other) } SubclassOfInner::Protocol(_) => { - self.check_type_pair(db, KnownClass::Type.to_instance(db), other) + self.check_type_pair(db, KnownClass::Type.to_instance(db, env), other) } SubclassOfInner::TypeVar(_) => unreachable!(), }) @@ -3215,7 +3340,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { nontrivial_check(self, || { ConstraintSet::from_bool( self.constraints, - !special_form.is_instance_of(db, instance.class(db)), + !special_form.is_instance_of(db, env, instance.class(db, env)), ) }) } @@ -3225,7 +3350,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { nontrivial_check(self, || { ConstraintSet::from_bool( self.constraints, - !known_instance.is_instance_of(db, instance.class(db)), + !known_instance.is_instance_of(db, env, instance.class(db, env)), ) }) } @@ -3236,31 +3361,35 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { let positive_relation_holds = match literal.kind() { LiteralValueTypeKind::Int(_) => KnownClass::Int.when_subclass_of( db, - instance.class(db), + env, + instance.class(db, env), self.constraints, ), LiteralValueTypeKind::Bool(_) => KnownClass::Bool.when_subclass_of( db, - instance.class(db), + env, + instance.class(db, env), self.constraints, ), LiteralValueTypeKind::LiteralString | LiteralValueTypeKind::String(_) => { KnownClass::Str.when_subclass_of( db, - instance.class(db), + env, + instance.class(db, env), self.constraints, ) } LiteralValueTypeKind::Bytes(_) => KnownClass::Bytes.when_subclass_of( db, - instance.class(db), + env, + instance.class(db, env), self.constraints, ), LiteralValueTypeKind::Enum(enum_literal) => self .as_relation_checker(TypeRelation::Subtyping) .check_type_pair( db, - enum_literal.enum_class_instance(db), + enum_literal.enum_class_instance(db, env), Type::NominalInstance(instance), ), }; @@ -3274,7 +3403,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // (it cannot be an instance of a `bool` subclass) nontrivial_check(self, || { KnownClass::Bool - .when_subclass_of(db, instance.class(db), self.constraints) + .when_subclass_of(db, env, instance.class(db, env), self.constraints) .negate(db, self.constraints) }) } @@ -3291,9 +3420,10 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { | (Type::NominalInstance(instance), Type::ClassLiteral(class)) => { nontrivial_check(self, || { class - .metaclass_instance_type(db) + .metaclass_instance_type(db, env) .when_subtype_of( db, + env, Type::NominalInstance(instance), self.constraints, self.inferable, @@ -3308,7 +3438,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { self.as_relation_checker(TypeRelation::Subtyping) .check_type_pair( db, - ClassType::Generic(alias).metaclass_instance_type(db), + ClassType::Generic(alias).metaclass_instance_type(db, env), Type::NominalInstance(instance), ) .negate(db, self.constraints) @@ -3321,7 +3451,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { // (it cannot be an instance of a `types.FunctionType` subclass) nontrivial_check(self, || { KnownClass::FunctionType - .when_subclass_of(db, instance.class(db), self.constraints) + .when_subclass_of(db, env, instance.class(db, env), self.constraints) .negate(db, self.constraints) }) } @@ -3384,13 +3514,13 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { (Type::BoundMethod(_), other) | (other, Type::BoundMethod(_)) => { nontrivial_check(self, || { - self.check_type_pair(db, KnownClass::MethodType.to_instance(db), other) + self.check_type_pair(db, KnownClass::MethodType.to_instance(db, env), other) }) } (Type::KnownBoundMethod(method), other) | (other, Type::KnownBoundMethod(method)) => { nontrivial_check(self, || { - self.check_type_pair(db, method.class().to_instance(db), other) + self.check_type_pair(db, method.class().to_instance(db, env), other) }) } @@ -3398,7 +3528,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { nontrivial_check(self, || { self.check_type_pair( db, - KnownClass::WrapperDescriptorType.to_instance(db), + KnownClass::WrapperDescriptorType.to_instance(db, env), other, ) }) @@ -3428,11 +3558,12 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { | ( Type::NominalInstance(nominal), Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), - ) if self.perform_expensive_checks && nominal.class(db).is_final(db) => { + ) if self.perform_expensive_checks && nominal.class(db, env).is_final(db) => { nontrivial_check(self, || { Type::NominalInstance(nominal) .member_lookup_with_policy( db, + env, "__call__", MemberLookupPolicy::NO_INSTANCE_FALLBACK, ) @@ -3469,7 +3600,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { self.check_type_pair( db, Type::NominalInstance(instance), - KnownClass::ModuleType.to_instance(db), + KnownClass::ModuleType.to_instance(db, env), ) }) } @@ -3493,7 +3624,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { (Type::PropertyInstance(property), other) | (other, Type::PropertyInstance(property)) => nontrivial_check(self, || { - self.check_type_pair(db, property.instance_fallback(db), other) + self.check_type_pair(db, property.instance_fallback(db, env), other) }), (Type::BoundSuper(left), Type::BoundSuper(right)) => nontrivial_check(self, || { @@ -3504,7 +3635,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { (Type::BoundSuper(_), other) | (other, Type::BoundSuper(_)) => { nontrivial_check(self, || { - self.check_type_pair(db, KnownClass::Super.to_instance(db), other) + self.check_type_pair(db, KnownClass::Super.to_instance(db, env), other) }) } @@ -3526,7 +3657,8 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { nontrivial_check(self, || { let dict_str_any = KnownClass::Dict.to_specialized_instance( db, - &[KnownClass::Str.to_instance(db), Type::any()], + env, + &[KnownClass::Str.to_instance(db, env), Type::any()], ); self.as_relation_checker(TypeRelation::Assignability) diff --git a/crates/ty_python_semantic/src/types/relation_error.rs b/crates/ty_python_semantic/src/types/relation_error.rs index 84bed59520..91d00bf5c5 100644 --- a/crates/ty_python_semantic/src/types/relation_error.rs +++ b/crates/ty_python_semantic/src/types/relation_error.rs @@ -1,3 +1,4 @@ +use crate::Db; /// This module defines a tree structure for collecting contextual information about type relation errors /// ("why is this complex type not assignable to that other complex type?"). use std::cell::{Cell, RefCell}; @@ -8,7 +9,7 @@ use ruff_python_ast::name::Name; use crate::types::context::LintDiagnosticGuard; use crate::types::tuple::TupleLength; use crate::types::{Type, TypedDictType}; -use crate::{Db, FxOrderSet}; +use crate::{FxOrderSet, ProgramEnvironment}; /// Identifies a parameter, either by name or by position. #[derive(Clone, Debug, PartialEq, Eq)] @@ -165,11 +166,14 @@ impl<'db> ErrorContext<'db> { fn render( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, help_messages: &mut FxOrderSet, ) -> Option { let typed_dict_name = |typed_dict: &TypedDictType<'db>| match typed_dict { TypedDictType::Class(class) => format!("TypedDict `{}`", class.name(db)), - TypedDictType::Synthesized(_) => Type::TypedDict(*typed_dict).display(db).to_string(), + TypedDictType::Synthesized(_) => { + Type::TypedDict(*typed_dict).display(db, env).to_string() + } }; Some(match self { @@ -182,14 +186,14 @@ impl<'db> ErrorContext<'db> { target, } => format!( "element `{}` of union `{}` is not assignable to `{}`", - element.display(db), - union.display(db), - target.display(db), + element.display(db, env), + union.display(db, env), + target.display(db, env), ), Self::NotAssignableToAnyUnionElement { source, union } => format!( "type `{}` is not assignable to any element of the union `{}`", - source.display(db), - union.display(db), + source.display(db, env), + union.display(db, env), ), Self::NotAssignableToNOtherUnionElements { n } => format!( "... omitted {n} union element{} without additional context", @@ -201,17 +205,17 @@ impl<'db> ErrorContext<'db> { intersection, } => format!( "type `{}` is not assignable to element `{}` of intersection `{}`", - source.display(db), - element.display(db), - intersection.display(db), + source.display(db, env), + element.display(db, env), + intersection.display(db, env), ), Self::NoIntersectionElementAssignableToTarget { intersection, target, } => format!( "no element of intersection `{}` is assignable to `{}`", - intersection.display(db), - target.display(db), + intersection.display(db, env), + target.display(db, env), ), Self::TypedDictFieldMissing { field_name, source } => { format!( @@ -263,8 +267,8 @@ impl<'db> ErrorContext<'db> { "field \"{field_name}\" on {source} has type `{source_field}` which is not assignable to type `{target_field}` expected by {target}", source = typed_dict_name(source), target = typed_dict_name(target), - source_field = source_field.display(db), - target_field = target_field.display(db), + source_field = source_field.display(db, env), + target_field = target_field.display(db, env), ), Self::TypedDictNotAssignableToDict(typed_dict) => { help_messages.insert(HelpMessages::TypedDictNotAssignableToDict); @@ -277,8 +281,8 @@ impl<'db> ErrorContext<'db> { } Self::IncompatibleReturnTypes { source, target } => format!( "incompatible return types: `{source}` is not assignable to `{target}`", - source = source.display(db), - target = target.display(db), + source = source.display(db, env), + target = target.display(db, env), ), Self::IncompatibleParameterTypes { source, @@ -288,14 +292,14 @@ impl<'db> ErrorContext<'db> { // reversed order due to contravariance of parameter types format!( "{parameter} has an incompatible type: `{target}` is not assignable to `{source}`", - source = source.display(db), - target = target.display(db), + source = source.display(db, env), + target = target.display(db, env), ) } Self::InferredCallableType { source, callable } => format!( "type `{}` has inferred callable type `{}`", - source.display(db), - callable.display(db), + source.display(db, env), + callable.display(db, env), ), Self::ExtraRequiredParameter { parameter } => match parameter { ParameterDescription::Named(name) => { @@ -325,7 +329,7 @@ impl<'db> ErrorContext<'db> { help_messages.insert(HelpMessages::TopCallableExplanation); format!( "Object of type `Top[(...) -> {}]` is not safe to call; its signature is not known", - return_type.display(db) + return_type.display(db, env) ) } Self::ParameterNameMismatch { @@ -371,28 +375,28 @@ impl<'db> ErrorContext<'db> { }; format!( "{which} is not compatible: `{source}` is not assignable to `{target}`", - source = source.display(db), - target = target.display(db) + source = source.display(db, env), + target = target.display(db, env) ) } Self::TypeNotCompatibleWithProtocol { ty, protocol } => { if let Type::ProtocolInstance(_) = ty { format!( "protocol `{}` is not assignable to protocol `{}`", - ty.display(db), - protocol.display(db), + ty.display(db, env), + protocol.display(db, env), ) } else { format!( "type `{}` is not assignable to protocol `{}`", - ty.display(db), - protocol.display(db), + ty.display(db, env), + protocol.display(db, env), ) } } Self::ProtocolMemberNotDefined { member_name, ty } => format!( "protocol member `{member_name}` is not defined on type `{}`", - ty.display(db), + ty.display(db, env), ), Self::ProtocolSpecialMethodNotDefinedOnMetaType => { "special methods must be defined on the meta-type when matching a protocol" @@ -403,13 +407,13 @@ impl<'db> ErrorContext<'db> { } Self::ProtocolMemberReadTypeIncompatible { source, target } => format!( "read type `{source}` is not assignable to `{target}`", - source = source.display(db), - target = target.display(db), + source = source.display(db, env), + target = target.display(db, env), ), Self::ProtocolMemberNotWritable => "the member is not writable".to_string(), Self::ProtocolMemberWriteTypeIncompatible { target } => format!( "the member does not accept writes of type `{}`", - target.display(db), + target.display(db, env), ), }) } @@ -472,12 +476,13 @@ impl<'db> ErrorContextNode<'db> { fn render_tree( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, output_lines: &mut Vec, help_messages: &mut FxOrderSet, prefix: &str, continuation: &str, ) { - if let Some(line) = self.context.render(db, help_messages) { + if let Some(line) = self.context.render(db, env, help_messages) { output_lines.push(format!("{prefix}{line}")); } @@ -491,6 +496,7 @@ impl<'db> ErrorContextNode<'db> { }; child.render_tree( db, + env, output_lines, help_messages, &child_prefix, @@ -589,13 +595,14 @@ impl<'db> ErrorContextTree<'db> { pub(in crate::types) fn attach_to( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, diag: &mut LintDiagnosticGuard<'_, '_>, ) { let mut output_lines = Vec::new(); let mut help_messages = FxOrderSet::default(); self.root .borrow() - .render_tree(db, &mut output_lines, &mut help_messages, "", ""); + .render_tree(db, env, &mut output_lines, &mut help_messages, "", ""); for line in output_lines { diag.info(line); } diff --git a/crates/ty_python_semantic/src/types/set_theoretic.rs b/crates/ty_python_semantic/src/types/set_theoretic.rs index 4aba968713..952773384f 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use itertools::Either; use std::convert::Infallible; @@ -46,7 +47,11 @@ impl<'db> UnionType<'db> { /// /// For performance reasons, consider using [`UnionType::from_two_elements`] if /// the union is constructed from exactly two elements. - pub fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> + pub fn from_elements( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Type<'db> where I: IntoIterator, T: Into>, @@ -55,7 +60,7 @@ impl<'db> UnionType<'db> { if let Some(first) = iter_elements.next() { if let Some(second) = iter_elements.next() { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); builder.add_in_place(first.into()); builder.add_in_place(second.into()); for element in iter_elements { @@ -71,32 +76,42 @@ impl<'db> UnionType<'db> { } /// Create a union type `A | B` from two elements `A` and `B`. - pub fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { + pub fn from_two_elements( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + a: Type<'db>, + b: Type<'db>, + ) -> Type<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _| { - result.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, types: TypePair<'db>| { + result.cycle_normalized(db, &ProgramEnvironment::from_program(types.program(db)), *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] fn union_from_two_elements<'db>(db: &'db dyn Db, types: TypePair<'db>) -> Type<'db> { - UnionBuilder::new(db) + let env = ProgramEnvironment::from_program(types.program(db)); + UnionBuilder::new(db, &env) .add(types.first(db)) .add(types.second(db)) .build() } - union_from_two_elements(db, TypePair::new(db, a, b)) + union_from_two_elements(db, TypePair::new(db, env.program(db), a, b)) } /// Create a union from a list of elements without unpacking type aliases. - pub(crate) fn from_elements_leave_aliases(db: &'db dyn Db, elements: I) -> Type<'db> + pub(crate) fn from_elements_leave_aliases( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Type<'db> where I: IntoIterator, T: Into>, { - let mut builder = UnionBuilder::new(db).unpack_aliases(false); + let mut builder = UnionBuilder::new(db, env).unpack_aliases(false); for element in elements { builder.add_in_place(element.into()); } @@ -113,17 +128,25 @@ impl<'db> UnionType<'db> { /// Recursively expands aliases that expose top-level union elements. /// /// Aliases nested inside non-union elements remain part of those elements. - pub(crate) fn expand_aliases(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn expand_aliases( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { // Rebuild the union so that `UnionBuilder` simplifies any redundancies exposed. - Self::from_elements(db, self.elements(db).iter().copied()) + Self::from_elements(db, env, self.elements(db).iter().copied()) } - pub(crate) fn from_elements_cycle_recovery(db: &'db dyn Db, elements: I) -> Type<'db> + pub(crate) fn from_elements_cycle_recovery( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Type<'db> where I: IntoIterator, T: Into>, { - let mut builder = UnionBuilder::new(db).cycle_recovery(true); + let mut builder = UnionBuilder::new(db, env).cycle_recovery(true); for element in elements { builder.add_in_place(element.into()); } @@ -135,12 +158,16 @@ impl<'db> UnionType<'db> { /// If all items in `elements` are `Some()`, the result of unioning all elements is returned. /// As soon as a `None` element in the iterable is encountered, /// the function short-circuits and returns `None`. - pub(crate) fn try_from_elements(db: &'db dyn Db, elements: I) -> Option> + pub(crate) fn try_from_elements( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Option> where I: IntoIterator>, T: Into>, { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for element in elements { builder.add_in_place(element?.into()); } @@ -152,10 +179,12 @@ impl<'db> UnionType<'db> { pub(crate) fn map( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, ) -> Type<'db> { - let Ok(mapped) = - self.try_map_impl(db, |element| Ok::<_, Infallible>(transform_fn(element))); + let Ok(mapped) = self.try_map_impl(db, env, |element| { + Ok::<_, Infallible>(transform_fn(element)) + }); mapped } @@ -163,6 +192,7 @@ impl<'db> UnionType<'db> { pub(crate) fn map_leave_aliases( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, ) -> Type<'db> { let elements = self.elements(db); @@ -170,7 +200,7 @@ impl<'db> UnionType<'db> { while let Some((i, ty)) = iter.next() { let new_ty = transform_fn(ty); if &new_ty != ty { - let mut builder = UnionBuilder::new(db).unpack_aliases(false); + let mut builder = UnionBuilder::new(db, env).unpack_aliases(false); for prev in &elements[..i] { builder.add_in_place(*prev); } @@ -197,15 +227,17 @@ impl<'db> UnionType<'db> { pub(crate) fn try_map( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Option>, ) -> Option> { - self.try_map_impl(db, |element| transform_fn(element).ok_or(())) + self.try_map_impl(db, env, |element| transform_fn(element).ok_or(())) .ok() } fn try_map_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Result, E>, ) -> Result, E> { let elements = self.elements(db); @@ -213,7 +245,7 @@ impl<'db> UnionType<'db> { while let Some((i, ty)) = iter.next() { let new_ty = transform_fn(ty)?; if &new_ty != ty || matches!(new_ty, Type::TypeAlias(_)) { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for prev in &elements[..i] { builder.add_in_place(*prev); } @@ -230,10 +262,14 @@ impl<'db> UnionType<'db> { Ok(Type::Union(self)) } - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option>> { + pub(crate) fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { let mut is_exact = true; - let instance = self.try_map(db, |element| { - let projection = element.to_instance(db)?; + let instance = self.try_map(db, env, |element| { + let projection = element.to_instance(db, env)?; is_exact &= projection.is_exact(); Some(projection.into_inner()) })?; @@ -244,7 +280,11 @@ impl<'db> UnionType<'db> { /// /// The returned type is broader than the literal types themselves. For example, the /// supertype for `Literal["a"] | Literal["b"]` is `LiteralString`. - pub(crate) fn common_literal_supertype(self, db: &'db dyn Db) -> Option> { + pub(crate) fn common_literal_supertype( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { // Do not use `Type::literal_fallback_instance` here: it also falls back from function // literals to `FunctionType`. Since `FunctionType.__call__` is gradual, it can be // assignable to a callable that the function literal's precise signature is not. @@ -252,7 +292,7 @@ impl<'db> UnionType<'db> { // supertype proves the relation for every literal in the union. let supertype = |element: &Type<'db>| match element { Type::LiteralValue(literal) if literal.is_string() => Some(Type::literal_string()), - Type::LiteralValue(literal) => Some(literal.fallback_instance(db)), + Type::LiteralValue(literal) => Some(literal.fallback_instance(db, env)), _ => None, }; @@ -278,9 +318,10 @@ impl<'db> UnionType<'db> { pub(crate) fn map_with_boundness( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, ) -> Place<'db> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut all_unbound = true; let mut possibly_unbound = false; @@ -333,9 +374,10 @@ impl<'db> UnionType<'db> { pub(crate) fn map_with_boundness_and_qualifiers( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, ) -> PlaceAndQualifiers<'db> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut qualifiers = TypeQualifiers::empty(); let mut all_unbound = true; @@ -395,10 +437,11 @@ impl<'db> UnionType<'db> { pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option> { - let mut builder = UnionBuilder::new(db) + let mut builder = UnionBuilder::new(db, env) .unpack_aliases(false) .cycle_recovery(true) .recursively_defined(self.recursively_defined(db)); @@ -406,7 +449,7 @@ impl<'db> UnionType<'db> { for ty in self.elements(db) { if nested { // list[T | Divergent] => list[Divergent] - let ty = ty.recursive_type_normalized_impl(db, div, nested)?; + let ty = ty.recursive_type_normalized_impl(db, env, div, nested)?; if ty.same_divergent_marker(div) { return Some(ty); } @@ -420,7 +463,7 @@ impl<'db> UnionType<'db> { continue; } builder.add_in_place( - ty.recursive_type_normalized_impl(db, div, nested) + ty.recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div), ); empty = false; @@ -461,19 +504,21 @@ pub(crate) enum KnownUnion { } impl KnownUnion { - pub(crate) fn to_type(self, db: &dyn Db) -> Type<'_> { + pub(crate) fn to_type<'db>(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { KnownUnion::Float => UnionType::from_two_elements( db, - KnownClass::Int.to_instance(db), - KnownClass::Float.to_instance(db), + env, + KnownClass::Int.to_instance(db, env), + KnownClass::Float.to_instance(db, env), ), KnownUnion::Complex => UnionType::from_elements( db, + env, [ - KnownClass::Int.to_instance(db), - KnownClass::Float.to_instance(db), - KnownClass::Complex.to_instance(db), + KnownClass::Int.to_instance(db, env), + KnownClass::Float.to_instance(db, env), + KnownClass::Complex.to_instance(db, env), ], ), } @@ -729,36 +774,56 @@ pub(crate) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized> #[salsa::tracked] impl<'db> IntersectionType<'db> { /// Return the compact enum-complement view of this intersection, if it has one. - pub(crate) fn enum_complement(self, db: &'db dyn Db) -> Option> { - EnumComplement::from_intersection_parts(db, self.positive(db), self.negative(db)) + pub(crate) fn enum_complement( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + EnumComplement::from_intersection_parts(db, env, self.positive(db), self.negative(db)) } /// Return the exact finite alternatives represented by this intersection, if available. - pub fn finite_alternatives(self, db: &'db dyn Db) -> Option>> { - self.enum_complement(db) - .map(|complement| complement.remaining_literal_types(db)) + pub fn finite_alternatives( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { + self.enum_complement(db, env) + .map(|complement| complement.remaining_literal_types(db, env)) } /// Return the exact finite alternative union represented by this intersection, if available. - pub(crate) fn finite_alternative_union(self, db: &'db dyn Db) -> Option> { - Some(self.enum_complement(db)?.remaining_literal_union(db)) + pub(crate) fn finite_alternative_union( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + Some( + self.enum_complement(db, env)? + .remaining_literal_union(db, env), + ) } /// Return the finite alternatives only if they remain concise enough for display. pub(crate) fn finite_alternatives_for_display( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, max_literals: usize, ) -> Option>> { - self.enum_complement(db)? - .remaining_literal_types_for_display(db, max_literals) + self.enum_complement(db, env)? + .remaining_literal_types_for_display(db, env, max_literals) } /// Create an intersection type `E1 & E2 & ... & En` from a list of (positive) elements. /// /// For performance reasons, consider using [`IntersectionType::from_two_elements`] if /// the intersection is constructed from exactly two elements. - pub(crate) fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> + pub(crate) fn from_elements( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Type<'db> where I: IntoIterator, T: Into>, @@ -767,8 +832,8 @@ impl<'db> IntersectionType<'db> { if let Some(first) = elements_iter.next() { if let Some(second) = elements_iter.next() { - let mut builder = - IntersectionBuilder::new(db).positive_elements([first.into(), second.into()]); + let mut builder = IntersectionBuilder::new(db, env) + .positive_elements([first.into(), second.into()]); for element in elements_iter { builder.add_positive_in_place(element.into()); } @@ -790,7 +855,11 @@ impl<'db> IntersectionType<'db> { /// work, and if so, returns `None`. (Redundant terms do not count toward the budget.) /// /// Like [`from_elements`][Self::from_elements], a successful result is exact. - pub(crate) fn bounded_from_elements(db: &'db dyn Db, elements: I) -> Option> + pub(crate) fn bounded_from_elements( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + elements: I, + ) -> Option> where I: IntoIterator, I::IntoIter: Clone, @@ -806,21 +875,21 @@ impl<'db> IntersectionType<'db> { // there is a single union, the product of all union counts should be reasonable, even // if it exceeds the budget below. In both cases, just return the precise answer // without considering the budget. - return Some(Self::from_elements(db, elements)); + return Some(Self::from_elements(db, env, elements)); } let non_union_elements = elements.clone().filter(|element| !element.is_union()); - let initial = Self::from_elements(db, non_union_elements); + let initial = Self::from_elements(db, env, non_union_elements); let insert_candidate = |candidates: &mut Vec>, new_ty: Type<'db>| -> Option<()> { if new_ty.is_never() || candidates .iter() - .any(|old| new_ty.is_redundant_with(db, *old)) + .any(|old| new_ty.is_redundant_with(db, env, *old)) { return Some(()); } - candidates.retain(|old| !old.is_redundant_with(db, new_ty)); + candidates.retain(|old| !old.is_redundant_with(db, env, new_ty)); if candidates.len() >= MAX_INTERSECTION_DNF_TERMS { return None; } @@ -843,7 +912,7 @@ impl<'db> IntersectionType<'db> { next.clear(); for candidate in &frontier { for alternative in clause.elements(db) { - let refined = Self::from_two_elements(db, *candidate, *alternative); + let refined = Self::from_two_elements(db, env, *candidate, *alternative); insert_candidate(&mut next, refined).or(skip_budget_check)?; } } @@ -855,44 +924,51 @@ impl<'db> IntersectionType<'db> { std::mem::swap(&mut frontier, &mut next); } - Some(UnionType::from_elements(db, frontier)) + Some(UnionType::from_elements(db, env, frontier)) } /// Create an intersection type `A & B` from two elements `A` and `B`. - pub(crate) fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { + pub(crate) fn from_two_elements( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + a: Type<'db>, + b: Type<'db>, + ) -> Type<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _| { - result.cycle_normalized(db, *previous, cycle) + cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, types: TypePair<'db>| { + result.cycle_normalized(db, &ProgramEnvironment::from_program(types.program(db)), *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] fn intersection_from_two_elements<'db>(db: &'db dyn Db, types: TypePair<'db>) -> Type<'db> { - IntersectionBuilder::new(db) + let env = ProgramEnvironment::from_program(types.program(db)); + IntersectionBuilder::new(db, &env) .positive_elements([types.first(db), types.second(db)]) .build() } - intersection_from_two_elements(db, TypePair::new(db, a, b)) + intersection_from_two_elements(db, TypePair::new(db, env.program(db), a, b)) } pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let positive = if nested { self.positive(db) .iter() - .map(|ty| ty.recursive_type_normalized_impl(db, div, nested)) + .map(|ty| ty.recursive_type_normalized_impl(db, env, div, nested)) .collect::>>>()? } else { self.positive(db) .iter() .map(|ty| { - ty.recursive_type_normalized_impl(db, div, nested) + ty.recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div) }) .collect() @@ -900,10 +976,10 @@ impl<'db> IntersectionType<'db> { let negative = if nested { self.negative(db) - .try_map(|ty| ty.recursive_type_normalized_impl(db, div, nested))? + .try_map(|ty| ty.recursive_type_normalized_impl(db, env, div, nested))? } else { self.negative(db).map(|ty| { - ty.recursive_type_normalized_impl(db, div, nested) + ty.recursive_type_normalized_impl(db, env, div, nested) .unwrap_or(div) }) }; @@ -930,9 +1006,10 @@ impl<'db> IntersectionType<'db> { pub(crate) fn map_positive( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, ) -> Type<'db> { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); for ty in self.positive(db) { builder.add_positive_in_place(transform_fn(ty)); } @@ -947,7 +1024,11 @@ impl<'db> IntersectionType<'db> { /// /// Negative instance constraints are not transferred: an object not satisfying `P` does not /// imply that other instances of its class cannot satisfy `P`. - pub(crate) fn try_dunder_class(self, db: &'db dyn Db) -> Option> { + pub(crate) fn try_dunder_class( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { if !self.iter_positive(db).any(|positive| { matches!( positive, @@ -957,9 +1038,9 @@ impl<'db> IntersectionType<'db> { return None; } - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); for positive in self.iter_positive(db) { - builder.add_positive_in_place(positive.dunder_class(db)); + builder.add_positive_in_place(positive.dunder_class(db, env)); } Some(builder.build()) } @@ -967,9 +1048,10 @@ impl<'db> IntersectionType<'db> { pub(crate) fn map_with_boundness( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, ) -> Place<'db> { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); let mut all_unbound = true; let mut any_definitely_bound = false; @@ -1018,9 +1100,10 @@ impl<'db> IntersectionType<'db> { pub(crate) fn map_with_boundness_and_qualifiers( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, ) -> PlaceAndQualifiers<'db> { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); let mut qualifiers = TypeQualifiers::empty(); let mut all_unbound = true; @@ -1077,8 +1160,12 @@ impl<'db> IntersectionType<'db> { /// Return a version of this intersection type where any type variables in the positive elements /// have been replaced by their bounds or constraints, and where any newtypes in the positive elements /// have been replaced by their concrete base types. - pub(crate) fn with_expanded_typevars_and_newtypes(self, db: &'db dyn Db) -> Type<'db> { - expand_intersection_typevars_and_newtypes(db, self.positive(db), self.negative(db)) + pub(crate) fn with_expanded_typevars_and_newtypes( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + expand_intersection_typevars_and_newtypes(db, env, self.positive(db), self.negative(db)) } pub fn iter_positive(self, db: &'db dyn Db) -> impl Iterator> { @@ -1115,12 +1202,16 @@ impl<'db> IntersectionType<'db> { /// Projecting only the positive `type[Base]` is an over-approximation, since we have no /// representation of an exact instance type excluding subclasses, and projecting the negative /// `~TypeOf[Base]` to `~Base` would incorrectly exclude `Child` instances too. - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option>> { - let mut builder = IntersectionBuilder::new(db); + pub(crate) fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { + let mut builder = IntersectionBuilder::new(db, env); let mut has_projected_positive = false; let mut is_exact = self.negative(db).is_empty(); for positive in self.iter_positive(db) { - if let Some(projection) = positive.to_instance(db) { + if let Some(projection) = positive.to_instance(db, env) { has_projected_positive = true; is_exact &= projection.is_exact(); builder.add_positive_in_place(projection.into_inner()); @@ -1146,19 +1237,20 @@ impl<'db> IntersectionType<'db> { fn expand_intersection_typevars_and_newtypes<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, positive: &FxOrderSet>, negative: &NegativeIntersectionElements<'db>, ) -> Type<'db> { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); for &element in positive { match element { Type::TypeVar(tvar) => { - match tvar.typevar(db).bound_or_constraints(db) { + match tvar.typevar(db).bound_or_constraints(db, env) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { builder.add_positive_in_place(bound); } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - builder.add_positive_in_place(constraints.as_type(db)); + builder.add_positive_in_place(constraints.as_type(db, env)); } // Type variables without bounds or constraints implicitly have `object` // as their upper bound, and adding `object` to an intersection is always a no-op diff --git a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs index 3b7dc5730b..1bc3214608 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs @@ -36,7 +36,10 @@ //! shares exactly the same possible super-types, and none of them are subtypes of each other //! (unless exactly the same literal type), we can avoid many unnecessary redundancy checks. +use std::hint::cold_path; + use super::RecursivelyDefined; + use crate::types::enums::EnumComplement; use crate::types::set_theoretic::expand_intersection_typevars_and_newtypes; use crate::types::{ @@ -44,7 +47,7 @@ use crate::types::{ KnownInstanceType, LiteralValueType, LiteralValueTypeKind, NegativeIntersectionElements, StringLiteralType, SubclassOfType, Type, TypeVarBoundOrConstraints, TypeVarVariance, UnionType, }; -use crate::{Db, FxOrderMap, FxOrderSet}; +use crate::{Db, FxOrderMap, FxOrderSet, ProgramEnvironment}; use rustc_hash::FxHashSet; use smallvec::SmallVec; @@ -59,13 +62,14 @@ use smallvec::SmallVec; /// This only recognizes the "single truthiness guard" forms used by truthiness narrowing. fn split_truthiness_guarded_intersection<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option<(Type<'db>, Type<'db>)> { let Type::Intersection(intersection) = ty else { return None; }; - let falsy = Type::AlwaysTruthy.negate(db); - let truthy = Type::AlwaysFalsy.negate(db); + let falsy = Type::AlwaysTruthy.negate(db, env); + let truthy = Type::AlwaysFalsy.negate(db, env); let negative = intersection.negative(db); let has_not_truthy = negative.contains(&Type::AlwaysTruthy); @@ -76,9 +80,9 @@ fn split_truthiness_guarded_intersection<'db>( _ => return None, }; - let mut core = IntersectionBuilder::new(db); + let mut core = IntersectionBuilder::new(db, env); for positive in intersection.positive(db) { - core = core.add_positive(*positive); + core.add_positive_in_place(*positive); } for negative in negative { if (guard == falsy && *negative == Type::AlwaysTruthy) @@ -86,7 +90,7 @@ fn split_truthiness_guarded_intersection<'db>( { continue; } - core = core.add_negative(*negative); + core.add_negative_in_place(*negative); } Some((core.build(), guard)) } @@ -96,11 +100,12 @@ fn split_truthiness_guarded_intersection<'db>( /// `list[Any]` is an invariant-dynamic generalization of `list[int]`. fn is_invariant_dynamic_generalization_of<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, general: Type<'db>, specific: Type<'db>, ) -> bool { // Fast path to avoid performance regressions. - if !general.has_dynamic(db) { + if !general.has_dynamic(db, env) { return false; } @@ -112,8 +117,8 @@ fn is_invariant_dynamic_generalization_of<'db>( Some((general_class, general_specialization)), Some((specific_class, specific_specialization)), ) = ( - general.class_specialization(db), - specific.class_specialization(db), + general.class_specialization(db, env), + specific.class_specialization(db, env), ) else { return false; @@ -164,22 +169,23 @@ fn is_invariant_dynamic_generalization_of<'db>( /// Discussion: fn merge_truthiness_guarded_pair<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, left: Type<'db>, right: Type<'db>, ) -> Option> { - let (left_core, left_guard) = split_truthiness_guarded_intersection(db, left)?; - let (right_core, right_guard) = split_truthiness_guarded_intersection(db, right)?; + let (left_core, left_guard) = split_truthiness_guarded_intersection(db, env, left)?; + let (right_core, right_guard) = split_truthiness_guarded_intersection(db, env, right)?; if left_guard == right_guard { return None; } - if left_core.is_equivalent_to(db, right_core) { + if left_core.is_equivalent_to(db, env, right_core) { return Some(left_core); } - let candidate = UnionType::from_elements(db, [left_core, right_core]); - let left_reconstructed = IntersectionType::from_two_elements(db, candidate, left_guard); - let right_reconstructed = IntersectionType::from_two_elements(db, candidate, right_guard); + let candidate = UnionType::from_elements(db, env, [left_core, right_core]); + let left_reconstructed = IntersectionType::from_two_elements(db, env, candidate, left_guard); + let right_reconstructed = IntersectionType::from_two_elements(db, env, candidate, right_guard); if left_reconstructed == left && right_reconstructed == right { Some(candidate) } else { @@ -192,11 +198,16 @@ fn merge_truthiness_guarded_pair<'db>( /// /// Hashability does not obey normal inheritance rules: subclasses of hashable classes can be /// unhashable. Keeping the non-final type allows downstream checks to consider it independently. -fn should_preserve_hashable_union(db: &dyn Db, left: Type, right: Type) -> bool { +fn should_preserve_hashable_union( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + left: Type, + right: Type, +) -> bool { let is_hashable = |ty| matches!(ty, Type::ProtocolInstance(protocol) if protocol.is_hashable(db)); let is_non_final_nominal_instance = - |ty| matches!(ty, Type::NominalInstance(instance) if !instance.class(db).is_final(db)); + |ty| matches!(ty, Type::NominalInstance(instance) if !instance.class(db, env).is_final(db)); (is_hashable(left) && is_non_final_nominal_instance(right)) || (is_hashable(right) && is_non_final_nominal_instance(left)) @@ -217,7 +228,11 @@ fn should_preserve_hashable_union(db: &dyn Db, left: Type, right: Type) -> bool /// /// # (Color excluding RED) | Literal[Color.RED] simplifies to Color. /// ``` -fn normalize_enum_complement_unions<'db>(db: &'db dyn Db, types: &mut Vec>) -> bool { +fn normalize_enum_complement_unions<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + types: &mut Vec>, +) -> bool { for complement_index in 0..types.len() { let Type::EnumComplement(complement) = types[complement_index] else { continue; @@ -264,8 +279,8 @@ fn normalize_enum_complement_unions<'db>(db: &'db dyn Db, types: &mut Vec UnionElement<'db> { fn try_reduce( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other_type: Type<'db>, cycle_recovery: bool, ) -> ReduceResult<'db> { + if let UnionElement::Type(existing) = self { + return ReduceResult::Type(*existing); + } + if cycle_recovery { + cold_path(); + // A widened literal group must absorb matching literals from later iterations for // recovery to converge. Preserve that exact fallback reduction without relation queries. return match self { - UnionElement::Type(existing) => ReduceResult::Type(*existing), UnionElement::IntLiterals(_) => { ReduceResult::KeepIf(!other_type.is_instance_of(db, KnownClass::Int)) } @@ -398,8 +419,9 @@ impl<'db> UnionElement<'db> { UnionElement::EnumLiterals { enum_class, .. } => ReduceResult::KeepIf( other_type .as_nominal_instance() - .is_none_or(|instance| instance.class_literal(db) != *enum_class), + .is_none_or(|instance| instance.class_literal(db, env) != *enum_class), ), + UnionElement::Type(_) => unreachable!("ordinary types are handled before recovery"), }; } @@ -427,17 +449,22 @@ impl<'db> UnionElement<'db> { // both `ignore` and `collapse` are `false`. If either is `true`, // we skip the expensive redundancy check and return `true`. let mut should_retain_type = |ty| { - if ignore || other_type.is_redundant_with(db, ty) { + if ignore || other_type.is_redundant_with(db, env, ty) { ignore = true; return true; } if collapse - || other_type.negation_is_subtype_of_cached(db, ty, &mut other_type_negated_cache) + || other_type.negation_is_subtype_of_cached( + db, + env, + ty, + &mut other_type_negated_cache, + ) { collapse = true; return true; } - !ty.is_redundant_with(db, other_type) + !ty.is_redundant_with(db, env, other_type) }; let should_keep = match self { @@ -450,7 +477,7 @@ impl<'db> UnionElement<'db> { } else { let (literal, promotable) = literals.first().unwrap(); !Type::from(LiteralValueType::new(*literal, *promotable)) - .is_redundant_with(db, other_type) + .is_redundant_with(db, env, other_type) } } UnionElement::StringLiterals(literals) => { @@ -462,7 +489,7 @@ impl<'db> UnionElement<'db> { } else { let (literal, promotable) = literals.first().unwrap(); !Type::from(LiteralValueType::new(*literal, *promotable)) - .is_redundant_with(db, other_type) + .is_redundant_with(db, env, other_type) } } UnionElement::BytesLiterals(literals) => { @@ -474,7 +501,7 @@ impl<'db> UnionElement<'db> { } else { let (literal, promotable) = literals.first().unwrap(); !Type::from(LiteralValueType::new(*literal, *promotable)) - .is_redundant_with(db, other_type) + .is_redundant_with(db, env, other_type) } } UnionElement::EnumLiterals { @@ -492,10 +519,10 @@ impl<'db> UnionElement<'db> { } else { let (literal, promotable) = literals.first().unwrap(); !Type::from(LiteralValueType::new(*literal, *promotable)) - .is_redundant_with(db, other_type) + .is_redundant_with(db, env, other_type) } } - UnionElement::Type(existing) => return ReduceResult::Type(*existing), + UnionElement::Type(_) => unreachable!("ordinary types are handled before reduction"), }; if ignore { @@ -533,6 +560,7 @@ const MAX_NON_RECURSIVE_UNION_LITERALS: usize = 8192; pub(crate) struct UnionBuilder<'db> { elements: Vec>, db: &'db dyn Db, + env: ProgramEnvironment<'db>, unpack_aliases: bool, /// This is enabled when joining types in a `cycle_recovery` function. Because recovery cannot /// introduce a new cycle, relation-based union simplifications are skipped in this mode. @@ -555,13 +583,13 @@ impl<'db> UnionAccumulator<'db> { UnionAccumulator::One(ty) } - pub(crate) fn add(&mut self, db: &'db dyn Db, ty: Type<'db>) { + pub(crate) fn add(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) { match self { UnionAccumulator::One(existing) => { *self = UnionAccumulator::Two(*existing, ty); } UnionAccumulator::Two(first, second) => { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); builder.add_in_place(*first); builder.add_in_place(*second); builder.add_in_place(ty); @@ -571,35 +599,43 @@ impl<'db> UnionAccumulator<'db> { } } - pub(crate) fn get_or_build(&mut self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn get_or_build( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { UnionAccumulator::One(ty) => *ty, UnionAccumulator::Two(first, second) => { - let ty = UnionType::from_two_elements(db, *first, *second); + let ty = UnionType::from_two_elements(db, env, *first, *second); *self = UnionAccumulator::One(ty); ty } UnionAccumulator::Deferred(_) => { - let ty = std::mem::replace(self, UnionAccumulator::new(Type::Never)).into_type(db); + let ty = + std::mem::replace(self, UnionAccumulator::new(Type::Never)).into_type(db, env); *self = UnionAccumulator::new(ty); ty } } } - pub(crate) fn into_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn into_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { UnionAccumulator::One(ty) => ty, - UnionAccumulator::Two(first, second) => UnionType::from_two_elements(db, first, second), + UnionAccumulator::Two(first, second) => { + UnionType::from_two_elements(db, env, first, second) + } UnionAccumulator::Deferred(builder) => builder.build(), } } } impl<'db> UnionBuilder<'db> { - pub(crate) fn new(db: &'db dyn Db) -> Self { + pub(crate) fn new(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { Self { db, + env: env.clone(), elements: vec![], unpack_aliases: true, cycle_recovery: false, @@ -636,21 +672,22 @@ impl<'db> UnionBuilder<'db> { } fn widen_literal_types(&mut self, seen_aliases: &mut Vec>) { + let db = self.db; let mut replace_with = vec![]; for elem in &self.elements { match elem { UnionElement::IntLiterals(_) => { - replace_with.push(KnownClass::Int.to_instance(self.db)); + replace_with.push(KnownClass::Int.to_instance(db, &self.env)); } UnionElement::StringLiterals(_) => { - replace_with.push(KnownClass::Str.to_instance(self.db)); + replace_with.push(KnownClass::Str.to_instance(db, &self.env)); } UnionElement::BytesLiterals(_) => { - replace_with.push(KnownClass::Bytes.to_instance(self.db)); + replace_with.push(KnownClass::Bytes.to_instance(db, &self.env)); } UnionElement::EnumLiterals { literals, .. } => { let (enum_literal, _) = literals.first().unwrap(); - replace_with.push(enum_literal.enum_class_instance(self.db)); + replace_with.push(enum_literal.enum_class_instance(db, &self.env)); } UnionElement::Type(_) => {} } @@ -672,6 +709,7 @@ impl<'db> UnionBuilder<'db> { } fn add_in_place_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { + let db = self.db; let cycle_recovery = self.cycle_recovery; let should_widen = |literals, recursively_defined: RecursivelyDefined| { if recursively_defined.is_yes() && cycle_recovery { @@ -682,18 +720,17 @@ impl<'db> UnionBuilder<'db> { }; let mut ty_negated_cache = None; - let mut ty_negated = || *ty_negated_cache.get_or_insert_with(|| ty.negate(self.db)); + let mut ty_negated = || *ty_negated_cache.get_or_insert_with(|| ty.negate(db, &self.env)); match ty { Type::Union(union) => { - let new_elements = union.elements(self.db); + let new_elements = union.elements(db); self.elements.reserve(new_elements.len()); for element in new_elements { self.add_in_place_impl(*element, seen_aliases); } - self.recursively_defined = self - .recursively_defined - .or(union.recursively_defined(self.db)); + self.recursively_defined = + self.recursively_defined.or(union.recursively_defined(db)); if self.cycle_recovery && self.recursively_defined.is_yes() { let literals = self.elements.iter().fold(0, |acc, elem| match elem { UnionElement::IntLiterals(literals) => acc + literals.len(), @@ -715,7 +752,7 @@ impl<'db> UnionBuilder<'db> { // leave out the recursive alias. TODO surface this error. } else { seen_aliases.push(ty); - self.add_in_place_impl(alias.value_type(self.db), seen_aliases); + self.add_in_place_impl(alias.value_type(db), seen_aliases); } } Type::LiteralValue(literal) => { @@ -733,7 +770,8 @@ impl<'db> UnionBuilder<'db> { match element { UnionElement::StringLiterals(literals) => { if should_widen(literals.len(), self.recursively_defined) { - let replace_with = KnownClass::Str.to_instance(self.db); + let replace_with = + KnownClass::Str.to_instance(db, &self.env); self.add_in_place_impl(replace_with, seen_aliases); return; } @@ -742,21 +780,22 @@ impl<'db> UnionBuilder<'db> { } UnionElement::Type(existing) if cycle_recovery - && literal.fallback_instance(self.db) == *existing => + && literal.fallback_instance(db, &self.env) + == *existing => { return; } UnionElement::Type(existing) if !cycle_recovery => { // e.g. `existing` could be `Literal[""] & Any`, // and `ty` could be `Literal[""]` - if ty.is_redundant_with(self.db, *existing) { + if ty.is_redundant_with(db, &self.env, *existing) { return; } - if existing.is_redundant_with(self.db, ty) { + if existing.is_redundant_with(db, &self.env, ty) { to_remove = Some(index); continue; } - if ty_negated().is_subtype_of(self.db, *existing) { + if ty_negated().is_subtype_of(db, &self.env, *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(); @@ -786,7 +825,8 @@ impl<'db> UnionBuilder<'db> { match element { UnionElement::BytesLiterals(literals) => { if should_widen(literals.len(), self.recursively_defined) { - let replace_with = KnownClass::Bytes.to_instance(self.db); + let replace_with = + KnownClass::Bytes.to_instance(db, &self.env); self.add_in_place_impl(replace_with, seen_aliases); return; } @@ -795,21 +835,22 @@ impl<'db> UnionBuilder<'db> { } UnionElement::Type(existing) if cycle_recovery - && literal.fallback_instance(self.db) == *existing => + && literal.fallback_instance(db, &self.env) + == *existing => { return; } UnionElement::Type(existing) if !cycle_recovery => { - if ty.is_redundant_with(self.db, *existing) { + if ty.is_redundant_with(db, &self.env, *existing) { return; } // e.g. `existing` could be `Literal[b""] & Any`, // and `ty` could be `Literal[b""]` - if existing.is_redundant_with(self.db, ty) { + if existing.is_redundant_with(db, &self.env, ty) { to_remove = Some(index); continue; } - if ty_negated().is_subtype_of(self.db, *existing) { + if ty_negated().is_subtype_of(db, &self.env, *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(); @@ -841,7 +882,8 @@ impl<'db> UnionBuilder<'db> { match element { UnionElement::IntLiterals(literals) => { if should_widen(literals.len(), self.recursively_defined) { - let replace_with = KnownClass::Int.to_instance(self.db); + let replace_with = + KnownClass::Int.to_instance(db, &self.env); self.add_in_place_impl(replace_with, seen_aliases); return; } @@ -850,21 +892,22 @@ impl<'db> UnionBuilder<'db> { } UnionElement::Type(existing) if cycle_recovery - && literal.fallback_instance(self.db) == *existing => + && literal.fallback_instance(db, &self.env) + == *existing => { return; } UnionElement::Type(existing) if !cycle_recovery => { - if ty.is_redundant_with(self.db, *existing) { + if ty.is_redundant_with(db, &self.env, *existing) { return; } // e.g. `existing` could be `Literal[1] & Any`, // and `ty` could be `Literal[1]` - if existing.is_redundant_with(self.db, ty) { + if existing.is_redundant_with(db, &self.env, ty) { to_remove = Some(index); continue; } - if ty_negated().is_subtype_of(self.db, *existing) { + if ty_negated().is_subtype_of(db, &self.env, *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(); @@ -890,15 +933,14 @@ impl<'db> UnionBuilder<'db> { } } LiteralValueTypeKind::Enum(enum_member_to_add) => { - let enum_class_literal = enum_member_to_add.enum_class_literal(self.db); - let enum_class = enum_class_literal.class_literal(self.db); - let enum_member_count = enum_class_literal.member_count(self.db); - let members_are_exhaustive = - enum_class_literal.members_are_exhaustive(self.db); + let enum_class_literal = enum_member_to_add.enum_class_literal(db); + let enum_class = enum_class_literal.class_literal(db); + let enum_member_count = enum_class_literal.member_count(db); + let members_are_exhaustive = enum_class_literal.members_are_exhaustive(db); if members_are_exhaustive && enum_member_count == 1 { self.add_in_place_impl( - enum_member_to_add.enum_class_instance(self.db), + enum_member_to_add.enum_class_instance(db, &self.env), seen_aliases, ); return; @@ -917,7 +959,8 @@ impl<'db> UnionBuilder<'db> { } if should_widen(literals.len(), self.recursively_defined) { let (literal, _) = literals.first().unwrap(); - let replace_with = literal.enum_class_instance(self.db); + let replace_with = + literal.enum_class_instance(db, &self.env); self.add_in_place_impl(replace_with, seen_aliases); return; } @@ -926,21 +969,22 @@ impl<'db> UnionBuilder<'db> { } UnionElement::Type(existing) if cycle_recovery - && literal.fallback_instance(self.db) == *existing => + && literal.fallback_instance(db, &self.env) + == *existing => { return; } UnionElement::Type(existing) if !cycle_recovery => { - if ty.is_redundant_with(self.db, *existing) { + if ty.is_redundant_with(db, &self.env, *existing) { return; } // e.g. `existing` could be `Literal[Foo.X] & Any`, // and `ty` could be `Literal[Foo.X]` - if existing.is_redundant_with(self.db, ty) { + if existing.is_redundant_with(db, &self.env, ty) { to_remove = Some(index); continue; } - if ty_negated().is_subtype_of(self.db, *existing) { + if ty_negated().is_subtype_of(db, &self.env, *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(); @@ -957,7 +1001,7 @@ impl<'db> UnionBuilder<'db> { if members_are_exhaustive && found.len() == enum_member_count { self.add_in_place_impl( - enum_member_to_add.enum_class_instance(self.db), + enum_member_to_add.enum_class_instance(db, &self.env), seen_aliases, ); return; @@ -990,6 +1034,7 @@ impl<'db> UnionBuilder<'db> { } fn push_type(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { + let db = self.db; let mut ty = ty; let bool_pair = |ty: Type<'db>| { if let Some(LiteralValueTypeKind::Bool(b)) = ty.as_literal_value_kind() { @@ -1008,7 +1053,7 @@ impl<'db> UnionBuilder<'db> { let mut to_remove = SmallVec::<[usize; 2]>::new(); for (i, element) in self.elements.iter_mut().enumerate() { - let element_type = match element.try_reduce(self.db, ty, self.cycle_recovery) { + let element_type = match element.try_reduce(db, &self.env, ty, self.cycle_recovery) { ReduceResult::KeepIf(keep) => { if !keep { to_remove.push(i); @@ -1034,7 +1079,9 @@ impl<'db> UnionBuilder<'db> { return; } - if !self.cycle_recovery && should_preserve_hashable_union(self.db, ty, element_type) { + if !self.cycle_recovery + && should_preserve_hashable_union(db, &self.env, ty, element_type) + { continue; } @@ -1049,13 +1096,14 @@ impl<'db> UnionBuilder<'db> { && left != right { to_remove.push(i); - ty = KnownClass::Range.to_instance(self.db); + ty = KnownClass::Range.to_instance(db, &self.env); continue; } // Fold `(T & ~AlwaysTruthy) | (T & ~AlwaysFalsy)` to `T`. if !self.cycle_recovery - && let Some(merged_type) = merge_truthiness_guarded_pair(self.db, ty, element_type) + && let Some(merged_type) = + merge_truthiness_guarded_pair(db, &self.env, ty, element_type) { to_remove.push(i); ty = merged_type; @@ -1068,7 +1116,7 @@ impl<'db> UnionBuilder<'db> { .zip(bool_pair(ty)) .is_some_and(|(element, pair)| element == pair) { - self.add_in_place_impl(KnownClass::Bool.to_instance(self.db), seen_aliases); + self.add_in_place_impl(KnownClass::Bool.to_instance(db, &self.env), seen_aliases); return; } @@ -1081,16 +1129,16 @@ impl<'db> UnionBuilder<'db> { } if should_simplify_full && !matches!(element_type, Type::TypeAlias(_)) { - if ty.is_redundant_with(self.db, element_type) { + if ty.is_redundant_with(db, &self.env, element_type) { return; } - if element_type.is_redundant_with(self.db, ty) { + if element_type.is_redundant_with(db, &self.env, ty) { to_remove.push(i); continue; } - if ty.negation_is_subtype_of_cached(self.db, element_type, &mut ty_negated) { + if ty.negation_is_subtype_of_cached(db, &self.env, element_type, &mut ty_negated) { // 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 @@ -1124,6 +1172,7 @@ impl<'db> UnionBuilder<'db> { pub(crate) fn try_build(self) -> Option> { let db = self.db; + let unpack_aliases = self.unpack_aliases; let cycle_recovery = self.cycle_recovery; let recursively_defined = self.recursively_defined; @@ -1168,8 +1217,8 @@ impl<'db> UnionBuilder<'db> { } } - if normalize_enum_complement_unions(db, &mut types) { - let builder = UnionBuilder::new(db) + if normalize_enum_complement_unions(db, &self.env, &mut types) { + let builder = UnionBuilder::new(db, &self.env) .unpack_aliases(unpack_aliases) .cycle_recovery(cycle_recovery) .recursively_defined(recursively_defined); @@ -1200,19 +1249,22 @@ pub(crate) struct IntersectionBuilder<'db> { // create a union of intersections. intersections: Vec>, db: &'db dyn Db, + env: ProgramEnvironment<'db>, } impl<'db> IntersectionBuilder<'db> { - pub(crate) fn new(db: &'db dyn Db) -> Self { + pub(crate) fn new(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { Self { db, + env: env.clone(), intersections: vec![InnerIntersectionBuilder::default()], } } - fn empty(db: &'db dyn Db) -> Self { + fn empty(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { Self { db, + env: env.clone(), intersections: vec![], } } @@ -1238,6 +1290,7 @@ impl<'db> IntersectionBuilder<'db> { } fn add_positive_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { + let db = self.db; match ty { Type::TypeAlias(alias) => { if seen_aliases.contains(&ty) { @@ -1248,7 +1301,7 @@ impl<'db> IntersectionBuilder<'db> { return; } seen_aliases.push(ty); - let value_type = alias.value_type(self.db); + let value_type = alias.value_type(db); self.add_positive_impl(value_type, seen_aliases); } Type::Union(union) => { @@ -1260,8 +1313,8 @@ impl<'db> IntersectionBuilder<'db> { // (T2 & T4)`. If `self` is already a union-of-intersections `(T1 & T2) | (T3 & T4)` // and we add `T5 | T6` to it, that flattens all the way out to `(T1 & T2 & T5) | (T1 & // T2 & T6) | (T3 & T4 & T5) ...` -- you get the idea. - let mut distributed = IntersectionBuilder::empty(self.db); - for elem in union.elements(self.db) { + let mut distributed = IntersectionBuilder::empty(db, &self.env); + for elem in union.elements(db) { let mut branch = self.clone(); branch.add_positive_impl(*elem, seen_aliases); distributed.extend(branch); @@ -1270,7 +1323,6 @@ impl<'db> IntersectionBuilder<'db> { } // `(A & B & ~C) & (D & E & ~F)` -> `A & B & D & E & ~C & ~F` Type::Intersection(other) => { - let db = self.db; for pos in other.positive(db) { self.add_positive_impl(*pos, seen_aliases); } @@ -1279,14 +1331,14 @@ impl<'db> IntersectionBuilder<'db> { } } Type::EnumComplement(complement) => { - let db = self.db; - self.add_positive_impl(complement.to_intersection(db), seen_aliases); + let intersection = complement.to_intersection(db, &self.env); + self.add_positive_impl(intersection, seen_aliases); } _ => { // If we are already a union-of-intersections, distribute the new intersected element // across all of those intersections. for inner in &mut self.intersections { - inner.add_positive(self.db, ty); + inner.add_positive(db, &self.env, ty); } } } @@ -1302,6 +1354,7 @@ impl<'db> IntersectionBuilder<'db> { } fn add_negative_impl(&mut self, ty: Type<'db>, seen_aliases: &mut Vec>) { + let db = self.db; // See comments above in `add_positive`; this is just the negated version. match ty { Type::TypeAlias(alias) => { @@ -1313,11 +1366,11 @@ impl<'db> IntersectionBuilder<'db> { return; } seen_aliases.push(ty); - let value_type = alias.value_type(self.db); + let value_type = alias.value_type(db); self.add_negative_impl(value_type, seen_aliases); } Type::Union(union) => { - for elem in union.elements(self.db) { + for elem in union.elements(db) { self.add_negative_impl(*elem, seen_aliases); } } @@ -1329,15 +1382,15 @@ impl<'db> IntersectionBuilder<'db> { // and negative constraints D, then our new intersection // is (existing & ~C) | (existing & D) - let mut distributed = IntersectionBuilder::empty(self.db); + let mut distributed = IntersectionBuilder::empty(db, &self.env); // We negate all the positive constraints while distributing. - for elem in intersection.positive(self.db) { + for elem in intersection.positive(db) { let mut branch = self.clone(); branch.add_negative_impl(*elem, &mut seen_aliases.clone()); distributed.extend(branch); } // All negative constraints end up becoming positive constraints. - for elem in intersection.negative(self.db) { + for elem in intersection.negative(db) { let mut branch = self.clone(); branch.add_positive_impl(*elem, &mut seen_aliases.clone()); distributed.extend(branch); @@ -1345,12 +1398,12 @@ impl<'db> IntersectionBuilder<'db> { self.intersections = distributed.intersections; } Type::EnumComplement(complement) => { - let db = self.db; - self.add_negative_impl(complement.to_intersection(db), seen_aliases); + let intersection = complement.to_intersection(db, &self.env); + self.add_negative_impl(intersection, seen_aliases); } _ => { for inner in &mut self.intersections { - inner.add_negative(self.db, ty); + inner.add_negative(db, &self.env, ty); } } } @@ -1368,11 +1421,13 @@ impl<'db> IntersectionBuilder<'db> { } pub(crate) fn build(self) -> Type<'db> { + let db = self.db; UnionType::from_elements( - self.db, + db, + &self.env, self.intersections .into_iter() - .map(|inner| inner.build(self.db)), + .map(|inner| inner.build(db, &self.env)), ) } } @@ -1404,13 +1459,14 @@ impl<'db> InnerIntersectionBuilder<'db> { /// if color is not Color.RED and color is not Color.BLUE: /// reveal_type(color) # Never /// ``` - fn has_empty_enum_complement(&self, db: &'db dyn Db) -> bool { + fn has_empty_enum_complement(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { for positive in &self.positive { let Type::NominalInstance(instance) = positive else { continue; }; - let Some(enum_class_literal) = instance.class_literal(db).into_enum_class(db) else { + let Some(enum_class_literal) = instance.class_literal(db, env).into_enum_class(db) + else { continue; }; if !enum_class_literal.members_are_exhaustive(db) { @@ -1449,7 +1505,12 @@ impl<'db> InnerIntersectionBuilder<'db> { } /// Adds a positive type to this intersection. - fn add_positive(&mut self, db: &'db dyn Db, mut new_positive: Type<'db>) { + fn add_positive( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + mut new_positive: Type<'db>, + ) { // `Never & T` -> `Never` if self.positive.contains(&Type::Never) { return; @@ -1481,8 +1542,11 @@ impl<'db> InnerIntersectionBuilder<'db> { Type::TypeForm(typeform) => { if let Some(narrowed) = SubclassOfType::try_from_instance( db, + env, typeform.type_argument(db).resolve_type_alias(db), - ) && self.positive.swap_remove(&KnownClass::Type.to_instance(db)) + ) && self + .positive + .swap_remove(&KnownClass::Type.to_instance(db, env)) { new_positive = narrowed; } @@ -1495,6 +1559,7 @@ impl<'db> InnerIntersectionBuilder<'db> { .find_map(|(index, positive)| match positive { Type::TypeForm(typeform) => SubclassOfType::try_from_instance( db, + env, typeform.type_argument(db).resolve_type_alias(db), ) .map(|narrowed| (index, narrowed)), @@ -1511,39 +1576,39 @@ impl<'db> InnerIntersectionBuilder<'db> { match new_positive { // `LiteralString & AlwaysTruthy` -> `LiteralString & ~Literal[""]` Type::AlwaysTruthy if self.positive.contains(&Type::literal_string()) => { - self.add_negative(db, Type::string_literal(db, "")); + self.add_negative(db, env, Type::string_literal(db, "")); } // `LiteralString & AlwaysFalsy` -> `Literal[""]` Type::AlwaysFalsy if self.positive.swap_remove(&Type::literal_string()) => { - self.add_positive(db, Type::string_literal(db, "")); + self.add_positive(db, env, Type::string_literal(db, "")); } // `AlwaysTruthy & LiteralString` -> `LiteralString & ~Literal[""]` Type::LiteralValue(literal) if literal.is_literal_string() && self.positive.swap_remove(&Type::AlwaysTruthy) => { - self.add_positive(db, Type::literal_string()); - self.add_negative(db, Type::string_literal(db, "")); + self.add_positive(db, env, Type::literal_string()); + self.add_negative(db, env, Type::string_literal(db, "")); } // `AlwaysFalsy & LiteralString` -> `Literal[""]` Type::LiteralValue(literal) if literal.is_literal_string() && self.positive.swap_remove(&Type::AlwaysFalsy) => { - self.add_positive(db, Type::string_literal(db, "")); + self.add_positive(db, env, Type::string_literal(db, "")); } // `LiteralString & ~AlwaysTruthy` -> `LiteralString & AlwaysFalsy` -> `Literal[""]` Type::LiteralValue(literal) if literal.is_literal_string() && self.negative.swap_remove(&Type::AlwaysTruthy) => { - self.add_positive(db, Type::string_literal(db, "")); + self.add_positive(db, env, Type::string_literal(db, "")); } // `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` Type::LiteralValue(literal) if literal.is_literal_string() && self.negative.swap_remove(&Type::AlwaysFalsy) => { - self.add_positive(db, Type::literal_string()); - self.add_negative(db, Type::string_literal(db, "")); + self.add_positive(db, env, Type::literal_string()); + self.add_negative(db, env, Type::string_literal(db, "")); } _ => { @@ -1619,9 +1684,10 @@ impl<'db> InnerIntersectionBuilder<'db> { let mut to_remove = SmallVec::<[usize; 1]>::new(); for (index, existing_positive) in self.positive.iter().enumerate() { // S & T = S if S <: T or T is an invariant-dynamic generalization of S. - if existing_positive.is_redundant_with(db, new_positive) + if existing_positive.is_redundant_with(db, env, new_positive) || is_invariant_dynamic_generalization_of( db, + env, new_positive, *existing_positive, ) @@ -1629,9 +1695,10 @@ impl<'db> InnerIntersectionBuilder<'db> { return; } // same rule, reverse order - if new_positive.is_redundant_with(db, *existing_positive) + if new_positive.is_redundant_with(db, env, *existing_positive) || is_invariant_dynamic_generalization_of( db, + env, *existing_positive, new_positive, ) @@ -1639,7 +1706,7 @@ impl<'db> InnerIntersectionBuilder<'db> { to_remove.push(index); } // A & B = Never if A and B are disjoint - if new_positive.is_disjoint_from(db, *existing_positive) { + if new_positive.is_disjoint_from(db, env, *existing_positive) { *self = Self::default(); self.positive.insert(Type::Never); return; @@ -1652,13 +1719,13 @@ impl<'db> InnerIntersectionBuilder<'db> { let mut to_remove = SmallVec::<[usize; 1]>::new(); for (index, existing_negative) in self.negative.iter().enumerate() { // S & ~T = Never if S <: T - if new_positive.is_subtype_of(db, *existing_negative) { + if new_positive.is_subtype_of(db, env, *existing_negative) { *self = Self::default(); self.positive.insert(Type::Never); return; } // A & ~B = A if A and B are disjoint - if existing_negative.is_disjoint_from(db, new_positive) { + if existing_negative.is_disjoint_from(db, env, new_positive) { to_remove.push(index); } } @@ -1672,7 +1739,12 @@ impl<'db> InnerIntersectionBuilder<'db> { } /// Adds a negative type to this intersection. - fn add_negative(&mut self, db: &'db dyn Db, new_negative: Type<'db>) { + fn add_negative( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + new_negative: Type<'db>, + ) { // `Never & ~T` -> `Never`. if self.positive.contains(&Type::Never) { return; @@ -1701,10 +1773,10 @@ impl<'db> InnerIntersectionBuilder<'db> { match new_negative { Type::Intersection(inter) => { for pos in inter.positive(db) { - self.add_negative(db, *pos); + self.add_negative(db, env, *pos); } for neg in inter.negative(db) { - self.add_positive(db, *neg); + self.add_positive(db, env, *neg); } } Type::Never => { @@ -1719,31 +1791,31 @@ impl<'db> InnerIntersectionBuilder<'db> { // Adding any of these types to the negative side of an intersection // is equivalent to adding it to the positive side. We do this to // simplify the representation. - self.add_positive(db, ty); + self.add_positive(db, env, ty); } // `bool & ~AlwaysTruthy` -> `bool & Literal[False]` Type::AlwaysTruthy if contains_bool() => { - self.add_positive(db, Type::bool_literal(false)); + self.add_positive(db, env, Type::bool_literal(false)); } // `bool & ~Literal[True]` -> `bool & Literal[False]` Type::LiteralValue(literal) if literal.as_bool() == Some(true) && contains_bool() => { - self.add_positive(db, Type::bool_literal(false)); + self.add_positive(db, env, Type::bool_literal(false)); } // `LiteralString & ~AlwaysTruthy` -> `LiteralString & Literal[""]` Type::AlwaysTruthy if self.positive.contains(&Type::literal_string()) => { - self.add_positive(db, Type::string_literal(db, "")); + self.add_positive(db, env, Type::string_literal(db, "")); } // `bool & ~AlwaysFalsy` -> `bool & Literal[True]` Type::AlwaysFalsy if contains_bool() => { - self.add_positive(db, Type::bool_literal(true)); + self.add_positive(db, env, Type::bool_literal(true)); } // `bool & ~Literal[False]` -> `bool & Literal[True]` Type::LiteralValue(literal) if literal.as_bool() == Some(false) && contains_bool() => { - self.add_positive(db, Type::bool_literal(true)); + self.add_positive(db, env, Type::bool_literal(true)); } // `LiteralString & ~AlwaysFalsy` -> `LiteralString & ~Literal[""]` Type::AlwaysFalsy if self.positive.contains(&Type::literal_string()) => { - self.add_negative(db, Type::string_literal(db, "")); + self.add_negative(db, env, Type::string_literal(db, "")); } _ => { let new_negative_enum = new_negative.as_enum_literal(); @@ -1763,11 +1835,11 @@ impl<'db> InnerIntersectionBuilder<'db> { } // ~S & ~T = ~T if S <: T - if existing_negative.is_redundant_with(db, new_negative) { + if existing_negative.is_redundant_with(db, env, new_negative) { to_remove.push(index); } // same rule, reverse order - if new_negative.is_subtype_of(db, *existing_negative) { + if new_negative.is_subtype_of(db, env, *existing_negative) { return; } } @@ -1790,7 +1862,7 @@ impl<'db> InnerIntersectionBuilder<'db> { if existing_positive .as_nominal_instance() .is_some_and(|instance| { - instance.class_literal(db) == new_enum.enum_class(db) + instance.class_literal(db, env) == new_enum.enum_class(db) }) { continue; @@ -1798,13 +1870,13 @@ impl<'db> InnerIntersectionBuilder<'db> { } // S & ~T = Never if S <: T - if existing_positive.is_subtype_of(db, new_negative) { + if existing_positive.is_subtype_of(db, env, new_negative) { *self = Self::default(); self.positive.insert(Type::Never); return; } // A & ~B = A if A and B are disjoint - if existing_positive.is_disjoint_from(db, new_negative) { + if existing_positive.is_disjoint_from(db, env, new_negative) { return; } } @@ -1825,7 +1897,7 @@ impl<'db> InnerIntersectionBuilder<'db> { /// /// - If the intersection contains negative entries for all of the constraints, the overall /// intersection is `Never`. - fn simplify_constrained_typevars(&mut self, db: &'db dyn Db) { + fn simplify_constrained_typevars(&mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) { let mut to_add = SmallVec::<[Type<'db>; 1]>::new(); for ty in &self.positive { @@ -1833,7 +1905,7 @@ impl<'db> InnerIntersectionBuilder<'db> { continue; }; let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = - bound_typevar.typevar(db).bound_or_constraints(db) + bound_typevar.typevar(db).bound_or_constraints(db, env) else { continue; }; @@ -1847,7 +1919,7 @@ impl<'db> InnerIntersectionBuilder<'db> { let matching_constraints = constraints .iter() .enumerate() - .filter(|(_, c)| c.is_subtype_of(db, *negative)); + .filter(|(_, c)| c.is_subtype_of(db, env, *negative)); for (constraint_index, _) in matching_constraints { remaining_constraints[constraint_index] = None; } @@ -1875,16 +1947,16 @@ impl<'db> InnerIntersectionBuilder<'db> { } for remaining_constraint in to_add { - self.add_positive(db, remaining_constraint); + self.add_positive(db, env, remaining_constraint); } } - fn build(mut self, db: &'db dyn Db) -> Type<'db> { - if self.has_empty_enum_complement(db) { + fn build(mut self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + if self.has_empty_enum_complement(db, env) { return Type::Never; } - self.simplify_constrained_typevars(db); + self.simplify_constrained_typevars(db, env); // If any typevars are in `self.positive`, speculatively solve all bounded type variables // to their upper bound and all constrained type variables to the union of their constraints. @@ -1896,14 +1968,14 @@ impl<'db> InnerIntersectionBuilder<'db> { .any(|ty| matches!(ty, Type::TypeVar(_) | Type::NewTypeInstance(_))) { let speculative = - expand_intersection_typevars_and_newtypes(db, &self.positive, &self.negative); + expand_intersection_typevars_and_newtypes(db, env, &self.positive, &self.negative); if speculative.is_never() { return Type::Never; } } if let Some(complement) = - EnumComplement::from_intersection_parts(db, &self.positive, &self.negative) + EnumComplement::from_intersection_parts(db, env, &self.positive, &self.negative) { return Type::EnumComplement(complement); } @@ -1933,55 +2005,64 @@ mod tests { use crate::types::type_alias::TypeAliasType; use crate::types::{KnownClass, KnownInstanceType, Truthiness}; + use ruff_db::PythonFile; use ruff_db::system::DbWithWritableSystem as _; use ty_module_resolver::KnownModule; #[test] fn build_union_no_elements() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); - let empty_union = UnionBuilder::new(&db).build(); + let empty_union = UnionBuilder::new(db, &env).build(); assert_eq!(empty_union, Type::Never); } #[test] fn build_union_single_element() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let t0 = Type::int_literal(0); - let union = UnionType::from_elements(&db, [t0]); + let union = UnionType::from_elements(db, &env, [t0]); assert_eq!(union, t0); } #[test] fn build_union_two_elements() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let t0 = Type::int_literal(0); let t1 = Type::int_literal(1); - let union = UnionType::from_elements(&db, [t0, t1]).expect_union(); + let union = UnionType::from_elements(db, &env, [t0, t1]).expect_union(); - assert_eq!(union.elements(&db), &[t0, t1]); + assert_eq!(union.elements(db), &[t0, t1]); } #[test] fn cycle_recovery_widens_recursive_literal_union() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let literal_limit = i64::try_from(MAX_RECURSIVE_UNION_LITERALS).expect("literal limit fits in i64"); let union = (0..=literal_limit).map(Type::int_literal).fold( - UnionBuilder::new(&db) + UnionBuilder::new(db, &env) .cycle_recovery(true) .recursively_defined(RecursivelyDefined::Yes), UnionBuilder::add, ); - assert_eq!(union.build(), KnownClass::Int.to_instance(&db)); + assert_eq!(union.build(), KnownClass::Int.to_instance(db, &env)); let assert_widens = |literal, instance| { for (first, second) in [(literal, instance), (instance, literal)] { - let union = UnionBuilder::new(&db) + let union = UnionBuilder::new(db, &env) .cycle_recovery(true) .add(first) .add(second) @@ -1990,49 +2071,56 @@ mod tests { } }; - assert_widens(Type::int_literal(1), KnownClass::Int.to_instance(&db)); + assert_widens(Type::int_literal(1), KnownClass::Int.to_instance(db, &env)); assert_widens( - Type::string_literal(&db, "literal"), - KnownClass::Str.to_instance(&db), + Type::string_literal(db, "literal"), + KnownClass::Str.to_instance(db, &env), ); assert_widens( - Type::bytes_literal(&db, b"literal"), - KnownClass::Bytes.to_instance(&db), + Type::bytes_literal(db, b"literal"), + KnownClass::Bytes.to_instance(db, &env), ); - let safe_uuid_class = known_module_symbol(&db, KnownModule::Uuid, "SafeUUID") + let safe_uuid_class = known_module_symbol(db, &env, KnownModule::Uuid, "SafeUUID") .place .expect_type() .expect_class_literal(); - let enum_literal = enum_member_literals(&db, safe_uuid_class, None) + let enum_literal = enum_member_literals(db, safe_uuid_class, None) .expect("SafeUUID is an enum") .next() .expect("SafeUUID has members"); assert_widens( enum_literal, - enum_literal.expect_enum_literal().enum_class_instance(&db), + enum_literal + .expect_enum_literal() + .enum_class_instance(db, &env), ); } #[test] fn cycle_recovery_skips_other_redundancy_simplification() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); for (left, right) in [ - (Type::string_literal(&db, "literal"), Type::literal_string()), - (Type::bool_literal(true), KnownClass::Bool.to_instance(&db)), + (Type::string_literal(db, "literal"), Type::literal_string()), + ( + Type::bool_literal(true), + KnownClass::Bool.to_instance(db, &env), + ), (Type::int_literal(1), Type::object()), (Type::bool_literal(true), Type::bool_literal(false)), ] { for (first, second) in [(left, right), (right, left)] { - let union = UnionBuilder::new(&db) + let union = UnionBuilder::new(db, &env) .cycle_recovery(true) .add(first) .add(second) .build() .expect_union(); - assert!(union.elements(&db).contains(&left)); - assert!(union.elements(&db).contains(&right)); + assert!(union.elements(db).contains(&left)); + assert!(union.elements(db).contains(&right)); } } } @@ -2040,31 +2128,35 @@ mod tests { #[test] fn union_common_literal_supertype() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let str_union = UnionType::from_elements( - &db, - [ - Type::string_literal(&db, "a"), - Type::string_literal(&db, "b"), - ], + db, + &env, + [Type::string_literal(db, "a"), Type::string_literal(db, "b")], ) .expect_union(); assert_eq!( - str_union.common_literal_supertype(&db), + str_union.common_literal_supertype(db, &env), Some(Type::literal_string()) ); - let int_union = UnionType::from_elements(&db, [Type::int_literal(1), Type::int_literal(2)]) - .expect_union(); + let int_union = + UnionType::from_elements(db, &env, [Type::int_literal(1), Type::int_literal(2)]) + .expect_union(); assert_eq!( - int_union.common_literal_supertype(&db), - Some(KnownClass::Int.to_instance(&db)) + int_union.common_literal_supertype(db, &env), + Some(KnownClass::Int.to_instance(db, &env)) ); - let mixed_union = - UnionType::from_elements(&db, [Type::string_literal(&db, "a"), Type::int_literal(1)]) - .expect_union(); - assert_eq!(mixed_union.common_literal_supertype(&db), None); + let mixed_union = UnionType::from_elements( + db, + &env, + [Type::string_literal(db, "a"), Type::int_literal(1)], + ) + .expect_union(); + assert_eq!(mixed_union.common_literal_supertype(db, &env), None); } fn map_marker<'db>(ty: &Type<'db>, marker: Type<'db>, replacement: Type<'db>) -> Type<'db> { @@ -2074,26 +2166,32 @@ mod tests { #[test] fn map_rebuilds_prefix_for_literal_widening() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); - let marker = KnownClass::Str.to_instance(&db); + let marker = KnownClass::Str.to_instance(db, &env); let literal_limit = i64::try_from(MAX_NON_RECURSIVE_UNION_LITERALS).expect("literal limit fits in i64"); let widening_literal = Type::int_literal(literal_limit); - let expected = KnownClass::Int.to_instance(&db); + let expected = KnownClass::Int.to_instance(db, &env); let elements = (0..literal_limit).map(Type::int_literal).chain([marker]); - let union = UnionType::from_elements(&db, elements).expect_union(); + let union = UnionType::from_elements(db, &env, elements).expect_union(); assert_eq!( - union.map(&db, |ty| map_marker(ty, marker, widening_literal)), + union.map(db, &env, |ty| map_marker(ty, marker, widening_literal)), expected ); assert_eq!( - union.map_leave_aliases(&db, |ty| map_marker(ty, marker, widening_literal)), + union.map_leave_aliases(db, &env, |ty| map_marker(ty, marker, widening_literal)), expected ); assert_eq!( - union.try_map(&db, |ty| Some(map_marker(ty, marker, widening_literal))), + union.try_map(db, &env, |ty| Some(map_marker( + ty, + marker, + widening_literal + ))), Some(expected) ); } @@ -2102,8 +2200,10 @@ mod tests { fn map_preserves_alias_unpacking_behavior() { let mut db = setup_db(); db.write_dedented("/src/a.py", "type Alias = int").unwrap(); + let env = db.program_environment(); let module = ruff_db::files::system_path_to_file(&db, "/src/a.py").unwrap(); + let module = PythonFile::new(&db, module, db.python_version()); let alias_ty = global_symbol(&db, module, "Alias").place.expect_type(); let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(alias))) = alias_ty @@ -2112,35 +2212,42 @@ mod tests { }; let alias = Type::TypeAlias(TypeAliasType::PEP695(alias)); - let str_instance = KnownClass::Str.to_instance(&db); - let union_ty = UnionType::from_elements_leave_aliases(&db, [alias, str_instance]); + let str_instance = KnownClass::Str.to_instance(&db, &env); + let union_ty = UnionType::from_elements_leave_aliases(&db, &env, [alias, str_instance]); let union = union_ty.expect_union(); - let unpacked = - UnionType::from_elements(&db, [KnownClass::Int.to_instance(&db), str_instance]); + let unpacked = UnionType::from_elements( + &db, + &env, + [KnownClass::Int.to_instance(&db, &env), str_instance], + ); - assert_eq!(union.map(&db, |ty| *ty), unpacked); - assert_eq!(union.try_map(&db, |ty| Some(*ty)), Some(unpacked)); - assert_eq!(union.map_leave_aliases(&db, |ty| *ty), union_ty); + assert_eq!(union.map(&db, &env, |ty| *ty), unpacked); + assert_eq!(union.try_map(&db, &env, |ty| Some(*ty)), Some(unpacked)); + assert_eq!(union.map_leave_aliases(&db, &env, |ty| *ty), union_ty); } #[test] fn build_intersection_empty_intersection_equals_object() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); - let intersection = IntersectionBuilder::new(&db).build(); + let intersection = IntersectionBuilder::new(db, &env).build(); assert_eq!(intersection, Type::object()); } #[test] fn build_intersection_discards_never_dnf_branches() { let db = setup_db(); - let int = KnownClass::Int.to_instance(&db); - let str = KnownClass::Str.to_instance(&db); - let bytes = KnownClass::Bytes.to_instance(&db); - - let int_or_str = UnionType::from_elements(&db, [int, str]); - let int_or_bytes = UnionType::from_elements(&db, [int, bytes]); - let intersection = IntersectionBuilder::new(&db) + let db = &db; + let env = db.program_environment(); + let int = KnownClass::Int.to_instance(db, &env); + let str = KnownClass::Str.to_instance(db, &env); + let bytes = KnownClass::Bytes.to_instance(db, &env); + + let int_or_str = UnionType::from_elements(db, &env, [int, str]); + let int_or_bytes = UnionType::from_elements(db, &env, [int, bytes]); + let intersection = IntersectionBuilder::new(db, &env) .add_positive(int_or_str) .add_positive(int_or_bytes); @@ -2159,36 +2266,37 @@ mod tests { } fn build_intersection_simplify_split_bool_impl(db: &TestDb, t_splitter: Type) { - let bool_value = t_splitter.bool(db) == Truthiness::AlwaysTrue; + let env = db.program_environment(); + let bool_value = t_splitter.bool(db, &env) == Truthiness::AlwaysTrue; // We add t_object in various orders (in first or second position) in // the tests below to ensure that the boolean simplification eliminates // everything from the intersection, not just `bool`. let t_object = Type::object(); - let t_bool = KnownClass::Bool.to_instance(db); + let t_bool = KnownClass::Bool.to_instance(db, &env); - let ty = IntersectionBuilder::new(db) + let ty = IntersectionBuilder::new(db, &env) .add_positive(t_object) .add_positive(t_bool) .add_negative(t_splitter) .build(); assert_eq!(ty, Type::bool_literal(!bool_value)); - let ty = IntersectionBuilder::new(db) + let ty = IntersectionBuilder::new(db, &env) .add_positive(t_bool) .add_positive(t_object) .add_negative(t_splitter) .build(); assert_eq!(ty, Type::bool_literal(!bool_value)); - let ty = IntersectionBuilder::new(db) + let ty = IntersectionBuilder::new(db, &env) .add_positive(t_object) .add_negative(t_splitter) .add_positive(t_bool) .build(); assert_eq!(ty, Type::bool_literal(!bool_value)); - let ty = IntersectionBuilder::new(db) + let ty = IntersectionBuilder::new(db, &env) .add_negative(t_splitter) .add_positive(t_object) .add_positive(t_bool) @@ -2199,73 +2307,82 @@ mod tests { #[test] fn build_intersection_enums() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); - let safe_uuid_class = known_module_symbol(&db, KnownModule::Uuid, "SafeUUID") + let safe_uuid_class = known_module_symbol(db, &env, KnownModule::Uuid, "SafeUUID") .place .ignore_possibly_undefined() .unwrap(); - let literals = enum_member_literals(&db, safe_uuid_class.expect_class_literal(), None) + let literals = enum_member_literals(db, safe_uuid_class.expect_class_literal(), None) .unwrap() .collect::>(); assert_eq!(literals.len(), 3); // SafeUUID.safe let l_safe = literals[0]; - assert_eq!(l_safe.expect_enum_literal().name(&db), "safe"); + assert_eq!(l_safe.expect_enum_literal().name(db), "safe"); // SafeUUID.unsafe let l_unsafe = literals[1]; - assert_eq!(l_unsafe.expect_enum_literal().name(&db), "unsafe"); + assert_eq!(l_unsafe.expect_enum_literal().name(db), "unsafe"); // SafeUUID.unknown let l_unknown = literals[2]; - assert_eq!(l_unknown.expect_enum_literal().name(&db), "unknown"); + assert_eq!(l_unknown.expect_enum_literal().name(db), "unknown"); // The enum itself: SafeUUID - let safe_uuid = l_safe.expect_enum_literal().enum_class_instance(&db); + let safe_uuid = l_safe.expect_enum_literal().enum_class_instance(db, &env); { - let actual = IntersectionBuilder::new(&db) + let actual = IntersectionBuilder::new(db, &env) .add_positive(safe_uuid) .add_negative(l_safe) .build(); assert_eq!( - actual.display(&db).to_string(), + actual.display(db, &db.program_environment()).to_string(), "Literal[SafeUUID.unsafe, SafeUUID.unknown]" ); } { // Same as above, but with the order reversed - let actual = IntersectionBuilder::new(&db) + let actual = IntersectionBuilder::new(db, &env) .add_negative(l_safe) .add_positive(safe_uuid) .build(); assert_eq!( - actual.display(&db).to_string(), + actual.display(db, &db.program_environment()).to_string(), "Literal[SafeUUID.unsafe, SafeUUID.unknown]" ); } { // Also the same, but now with a nested intersection - let actual = IntersectionBuilder::new(&db) + let actual = IntersectionBuilder::new(db, &env) .add_positive(safe_uuid) - .add_positive(IntersectionBuilder::new(&db).add_negative(l_safe).build()) + .add_positive( + IntersectionBuilder::new(db, &env) + .add_negative(l_safe) + .build(), + ) .build(); assert_eq!( - actual.display(&db).to_string(), + actual.display(db, &db.program_environment()).to_string(), "Literal[SafeUUID.unsafe, SafeUUID.unknown]" ); } { - let actual = IntersectionBuilder::new(&db) + let actual = IntersectionBuilder::new(db, &env) .add_negative(l_safe) .add_positive(safe_uuid) .add_negative(l_unsafe) .build(); - assert_eq!(actual.display(&db).to_string(), "Literal[SafeUUID.unknown]"); + assert_eq!( + actual.display(db, &db.program_environment()).to_string(), + "Literal[SafeUUID.unknown]" + ); } } } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 83d22b9b30..b04ab05230 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -10,6 +10,7 @@ //! argument types and return types. For each callable type in the union, the call expression's //! arguments must match _at least one_ overload. +use crate::ProgramEnvironment; use std::fmt; use std::num::NonZeroU32; use std::slice::Iter; @@ -71,7 +72,7 @@ fn function_signature_expression_type<'db>( definition: Definition<'db>, expression: &ast::Expr, ) -> Type<'db> { - let file = definition.file(db); + let file = definition.python_file(db); let index = semantic_index(db, file); let file_scope = index.expression_scope_id(expression); let scope = file_scope.to_scope_id(db, file); @@ -89,7 +90,7 @@ fn function_signature_type_expression_flags<'db>( definition: Definition<'db>, expression: &ast::Expr, ) -> TypeExpressionFlags { - let file = definition.file(db); + let file = definition.python_file(db); let index = semantic_index(db, file); let file_scope = index.expression_scope_id(expression); let scope = file_scope.to_scope_id(db, file); @@ -113,6 +114,7 @@ pub struct CallableSignature<'db> { fn merge_receiver_constraints<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, first: Option<&OwnedConstraintSet<'db>>, second: Option<&OwnedConstraintSet<'db>>, ) -> Option> { @@ -129,8 +131,8 @@ fn merge_receiver_constraints<'db>( let constraints = ConstraintSetBuilder::new(); Some(constraints.into_owned(|builder| { builder - .load(db, first) - .and(db, builder, || builder.load(db, second)) + .load(db, env, first) + .and(db, builder, || builder.load(db, env, second)) })) } } @@ -174,10 +176,14 @@ impl<'db> CallableSignature<'db> { Self::single(Signature::bottom()) } - pub(crate) fn cycle_initial(db: &'db dyn Db, id: salsa::Id) -> Self { + pub(crate) fn cycle_initial( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + id: salsa::Id, + ) -> Self { Self::single(Signature::new( Parameters::bottom(), - Type::divergent(id).bottom_materialization(db), + Type::divergent(id).bottom_materialization(db, env), )) } @@ -208,11 +214,17 @@ impl<'db> CallableSignature<'db> { } /// Returns the union of all overload return types, or `Unknown` if there are no overloads. - pub(crate) fn overload_return_type_or_unknown(&self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn overload_return_type_or_unknown( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self.overloads.as_slice() { [] => Type::unknown(), [signature] => signature.return_ty, - overloads => UnionType::from_elements(db, overloads.iter().map(|sig| sig.return_ty)), + overloads => { + UnionType::from_elements(db, env, overloads.iter().map(|sig| sig.return_ty)) + } } } @@ -231,6 +243,7 @@ impl<'db> CallableSignature<'db> { /// Returns the reduced overloaded signature exposed by a `functools.partial(...)` object. pub(crate) fn partially_apply( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overloads: impl IntoIterator>, ) -> Option { let mut new_overloads = Vec::new(); @@ -239,6 +252,7 @@ impl<'db> CallableSignature<'db> { for overload in overloads { let signature = overload.signature.partially_apply( db, + env, &overload.partial_application, overload.inference, overload.unspecialized_return_ty, @@ -258,6 +272,7 @@ impl<'db> CallableSignature<'db> { pub(crate) fn cycle_normalized( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous: &Self, cycle: &salsa::Cycle, ) -> Self { @@ -267,7 +282,7 @@ impl<'db> CallableSignature<'db> { .overloads .iter() .zip(previous.overloads.iter()) - .map(|(curr, prev)| curr.cycle_normalized(db, prev, cycle)) + .map(|(curr, prev)| curr.cycle_normalized(db, env, prev, cycle)) .collect(), } } else { @@ -279,6 +294,7 @@ impl<'db> CallableSignature<'db> { pub(super) fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -286,7 +302,7 @@ impl<'db> CallableSignature<'db> { overloads: self .overloads .iter() - .map(|signature| signature.recursive_type_normalized_impl(db, div, nested)) + .map(|signature| signature.recursive_type_normalized_impl(db, env, div, nested)) .collect::>>()?, }) } @@ -296,7 +312,7 @@ impl<'db> CallableSignature<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { fn try_apply_type_mapping_for_paramspec<'db>( db: &'db dyn Db, @@ -305,7 +321,7 @@ impl<'db> CallableSignature<'db> { paramspec_value: Type<'db>, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Option> { match paramspec_value { Type::TypeVar(typevar) if typevar.is_paramspec(db) => { @@ -323,9 +339,10 @@ impl<'db> CallableSignature<'db> { ) }; + let env = visitor.env; Some(CallableSignature::single(Signature { generic_context: self_signature.generic_context.map(|context| { - type_mapping.update_signature_generic_context(db, context) + type_mapping.update_signature_generic_context(db, env, context) }), definition: self_signature.definition, source_overload_index: self_signature.source_overload_index, @@ -347,13 +364,14 @@ impl<'db> CallableSignature<'db> { Type::Callable(callable) if matches!(callable.kind(db), CallableTypeKind::ParamSpecValue) => { + let env = visitor.env; Some(CallableSignature::from_overloads( callable.signatures(db).iter().map(|signature| Signature { generic_context: GenericContext::merge_optional( db, signature.generic_context, self_signature.generic_context.map(|context| { - type_mapping.update_signature_generic_context(db, context) + type_mapping.update_signature_generic_context(db, env, context) }), ), definition: signature.definition, @@ -367,6 +385,7 @@ impl<'db> CallableSignature<'db> { ); merge_receiver_constraints( db, + env, signature.receiver_constraints.as_ref(), mapped.as_ref(), ) @@ -428,20 +447,26 @@ impl<'db> CallableSignature<'db> { pub(crate) fn find_legacy_typevars_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { for signature in &self.overloads { - signature.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + signature.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } /// Binds the first (presumably `self`) parameter of this signature. If a `self_type` is /// provided, we will replace any occurrences of `typing.Self` in the parameter and return /// annotations with that type. - pub(crate) fn bind_self(&self, db: &'db dyn Db, self_type: Option>) -> Self { - self.bind_self_with_receiver(db, self_type, self_type) + pub(crate) fn bind_self( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Option>, + ) -> Self { + self.bind_self_with_receiver(db, env, self_type, self_type) } /// Binds the receiver using its runtime type while using `typing_self_type` to replace @@ -452,6 +477,7 @@ impl<'db> CallableSignature<'db> { pub(crate) fn bind_self_with_receiver( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Option>, typing_self_type: Option>, ) -> Self { @@ -460,7 +486,7 @@ impl<'db> CallableSignature<'db> { .overloads .iter() .map(|signature| { - signature.bind_self_with_receiver(db, receiver_type, typing_self_type) + signature.bind_self_with_receiver(db, env, receiver_type, typing_self_type) }) .collect(), } @@ -477,6 +503,7 @@ impl<'db> CallableSignature<'db> { pub(crate) fn apply_self_with_receiver( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Type<'db>, self_type: Type<'db>, ) -> Self { @@ -484,7 +511,9 @@ impl<'db> CallableSignature<'db> { overloads: self .overloads .iter() - .map(|signature| signature.apply_self_with_receiver(db, receiver_type, self_type)) + .map(|signature| { + signature.apply_self_with_receiver(db, env, receiver_type, self_type) + }) .collect(), } } @@ -512,14 +541,16 @@ impl<'db> CallableSignature<'db> { pub(crate) fn when_constraint_set_assignable_to<'c>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: &Self, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker::constraint_set_assignability( + env, constraints, &relation_visitor, &disjointness_visitor, @@ -541,10 +572,15 @@ impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { impl<'db> VarianceInferable<'db> for &CallableSignature<'db> { // TODO: possibly need to replace self - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { self.overloads .iter() - .map(|signature| signature.variance_of(db, typevar)) + .map(|signature| signature.variance_of(db, env, typevar)) .collect() } } @@ -793,9 +829,16 @@ impl<'db> Signature<'db> { } } - pub(super) fn wrap_coroutine_return_type(self, db: &'db dyn Db) -> Self { - let return_ty = KnownClass::CoroutineType - .to_specialized_instance(db, &[Type::any(), Type::any(), self.return_ty]); + pub(super) fn wrap_coroutine_return_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Self { + let return_ty = KnownClass::CoroutineType.to_specialized_instance( + db, + env, + &[Type::any(), Type::any(), self.return_ty], + ); Self { return_ty, ..self } } @@ -814,12 +857,15 @@ impl<'db> Signature<'db> { /// `Self` is hidden if it does not appear in: /// 1. The return type /// 2. Any explicitly annotated parameter (not inferred) - pub(crate) fn should_hide_self_from_display(&self, db: &'db dyn Db) -> bool { - !self.return_ty.contains_self(db) - && !self - .parameters() - .iter() - .any(|p| p.should_annotation_be_displayed() && p.annotated_type().contains_self(db)) + pub(crate) fn should_hide_self_from_display( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + !self.return_ty.contains_self(db, env) + && !self.parameters().iter().any(|p| { + p.should_annotation_be_displayed() && p.annotated_type().contains_self(db, env) + }) } fn with_inherited_generic_context( @@ -838,17 +884,23 @@ impl<'db> Signature<'db> { self } - fn cycle_normalized(&self, db: &'db dyn Db, previous: &Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: &Self, + cycle: &salsa::Cycle, + ) -> Self { let return_ty = self .return_ty - .cycle_normalized(db, previous.return_ty, cycle); + .cycle_normalized(db, env, previous.return_ty, cycle); let parameters = if self.parameters.len() == previous.parameters.len() { Parameters::new( self.parameters .iter() .zip(previous.parameters.iter()) - .map(|(curr, prev)| curr.cycle_normalized(db, prev, cycle)) + .map(|(curr, prev)| curr.cycle_normalized(db, env, prev, cycle)) .collect::>(), self.parameters.kind(), ) @@ -870,21 +922,22 @@ impl<'db> Signature<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { let return_ty = if nested { self.return_ty - .recursive_type_normalized_impl(db, div, true)? + .recursive_type_normalized_impl(db, env, div, true)? } else { self.return_ty - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }; let parameters = { let mut parameters = Vec::with_capacity(self.parameters.len()); for param in &self.parameters { - parameters.push(param.recursive_type_normalized_impl(db, div, nested)?); + parameters.push(param.recursive_type_normalized_impl(db, env, div, nested)?); } Parameters::new(parameters, self.parameters.kind()) }; @@ -903,12 +956,13 @@ impl<'db> Signature<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { + let env = visitor.env; Self { generic_context: self .generic_context - .map(|context| type_mapping.update_signature_generic_context(db, context)), + .map(|context| type_mapping.update_signature_generic_context(db, env, context)), definition: self.definition, source_overload_index: self.source_overload_index, receiver_constraints: self.map_receiver_constraints(db, type_mapping, tcx, visitor), @@ -921,7 +975,12 @@ impl<'db> Signature<'db> { } } - pub(crate) fn freshen_bound_typevars(&self, db: &'db dyn Db, delta: u32) -> Self { + pub(crate) fn freshen_bound_typevars( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + delta: u32, + ) -> Self { let Some(generic_context) = self.generic_context else { return self.clone(); }; @@ -933,7 +992,7 @@ impl<'db> Signature<'db> { delta, }, TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ) } @@ -961,26 +1020,28 @@ impl<'db> Signature<'db> { pub(crate) fn find_legacy_typevars_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { for ty in self.receiver_constraint_types() { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } for param in &self.parameters { param.annotated_type().find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, ); if let Some(ty) = param.default_type() { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } self.return_ty - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + .find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } /// Return the parameters in this signature. @@ -996,6 +1057,7 @@ impl<'db> Signature<'db> { pub(crate) fn add_implicit_self_annotation( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, self_type: impl FnOnce() -> Option>, ) { if let Some(first_parameter) = self.parameters.data.value.first() @@ -1027,12 +1089,14 @@ impl<'db> Signature<'db> { Some(generic_context) => { *generic_context = GenericContext::from_typevar_instances( db, + env, std::iter::once(self_typevar).chain(generic_context.variables(db)), ); } None => { self.generic_context = Some(GenericContext::from_typevar_instances( db, + env, std::iter::once(self_typevar), )); } @@ -1046,8 +1110,13 @@ impl<'db> Signature<'db> { self.definition } - pub(crate) fn bind_self(&self, db: &'db dyn Db, self_type: Option>) -> Self { - self.bind_self_with_receiver(db, self_type, self_type) + pub(crate) fn bind_self( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Option>, + ) -> Self { + self.bind_self_with_receiver(db, env, self_type, self_type) } /// Binds the receiver while preserving the relation between its runtime type and annotation. @@ -1057,6 +1126,7 @@ impl<'db> Signature<'db> { pub(crate) fn bind_self_with_receiver( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Option>, typing_self_type: Option>, ) -> Self { @@ -1080,15 +1150,22 @@ impl<'db> Signature<'db> { Type::TypeVar(BoundTypeVarInstance::synthetic_self( db, Type::object(), - BindingContext::Synthetic, + BindingContext::Synthetic(env.program(db)), )) }); let annotation = if let Some(typing_self_type) = typing_self_type { - let mapping = - TypeMapping::BindSelf(SelfBinding::new(db, typing_self_type, binding_context)); - parameter - .annotated_type() - .apply_type_mapping(db, &mapping, TypeContext::default()) + let mapping = TypeMapping::BindSelf(SelfBinding::new( + db, + env, + typing_self_type, + binding_context, + )); + parameter.annotated_type().apply_type_mapping( + db, + env, + &mapping, + TypeContext::default(), + ) } else { parameter.annotated_type() }; @@ -1101,29 +1178,31 @@ impl<'db> Signature<'db> { _ => None, }; if receiver_typevar.is_some_and(|typevar| { - Self::receiver_violates_typevar_domain(db, receiver, typevar) + Self::receiver_violates_typevar_domain(db, env, receiver, typevar) }) { return std::borrow::Cow::Owned(OwnedConstraintSet::default()); } - receiver.when_constraint_set_assignable_to_owned(db, annotation) + receiver.when_constraint_set_assignable_to_owned(db, env, annotation) }); let receiver_constraints = merge_receiver_constraints( db, + env, self.receiver_constraints.as_ref(), receiver_constraint.as_deref(), ); if let Some(self_type) = typing_self_type - && self.needs_self_mapping(db, removed_receiver) + && self.needs_self_mapping(db, env, removed_receiver) { let self_mapping = - TypeMapping::BindSelf(SelfBinding::new(db, self_type, binding_context)); + TypeMapping::BindSelf(SelfBinding::new(db, env, self_type, binding_context)); parameters = parameters.apply_type_mapping_impl( db, &self_mapping, TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ); - return_ty = return_ty.apply_type_mapping(db, &self_mapping, TypeContext::default()); + return_ty = + return_ty.apply_type_mapping(db, env, &self_mapping, TypeContext::default()); } Self { generic_context: self @@ -1149,23 +1228,24 @@ impl<'db> Signature<'db> { /// ``` fn receiver_violates_typevar_domain( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver: Type<'db>, typevar: BoundTypeVarInstance<'db>, ) -> bool { - let Some(domain) = typevar.typevar(db).bound_or_constraints(db) else { + let Some(domain) = typevar.typevar(db).bound_or_constraints(db, env) else { return false; }; - if receiver.has_typevar(db) { + if receiver.has_typevar(db, env) { return false; } !match domain { TypeVarBoundOrConstraints::UpperBound(bound) => { - receiver.is_assignable_to(db, bound.top_materialization(db)) + receiver.is_assignable_to(db, env, bound.top_materialization(db, env)) } TypeVarBoundOrConstraints::Constraints(constraints) => { constraints.elements(db).iter().any(|constraint| { - receiver.is_assignable_to(db, constraint.top_materialization(db)) + receiver.is_assignable_to(db, env, constraint.top_materialization(db, env)) }) } } @@ -1180,24 +1260,25 @@ impl<'db> Signature<'db> { pub(crate) fn bind_self_if_compatible( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Type<'db>, typing_self_type: Type<'db>, ) -> Option { - if !self.can_bind_self_to(db, receiver_type) { + if !self.can_bind_self_to(db, env, receiver_type) { return None; } let bound_signature = - self.bind_self_with_receiver(db, Some(receiver_type), Some(typing_self_type)); + self.bind_self_with_receiver(db, env, Some(receiver_type), Some(typing_self_type)); let Some(receiver_constraints) = bound_signature.receiver_constraints.as_ref() else { return Some(bound_signature); }; let constraints = ConstraintSetBuilder::new(); - let when = constraints.load(db, receiver_constraints); + let when = constraints.load(db, env, receiver_constraints); let inferable = self.inferable_typevars(db); - match when.solutions(db, &constraints, inferable) { + match when.solutions(db, env, &constraints, inferable) { Solutions::Unsatisfiable => return None, Solutions::Unconstrained => return Some(bound_signature), // Each receiver path can leave a different type variable unconstrained. Preserve the @@ -1212,16 +1293,16 @@ impl<'db> Signature<'db> { return Some(bound_signature); }; - let mut builder = SpecializationBuilder::new(db, &constraints, inferable); + let mut builder = SpecializationBuilder::new(db, env, &constraints, inferable); builder.add_constraint_set(when).ok()?; let concrete_class_receiver = matches!(receiver_type, Type::ClassLiteral(_) | Type::GenericAlias(_)); let specialization = builder.build_with(generic_context, |typevar, bounds| { if let Some(bounds) = bounds && let Some(lower) = bounds.lower - && let Some(upper) = bounds.upper.as_single_bound(db) - && lower.is_equivalent_to(db, upper) - && let Ok(Some(solution)) = PathBounds::default_solve(db, &constraints, bounds) + && let Some(upper) = bounds.upper.as_single_bound(db, env) + && lower.is_equivalent_to(db, env, upper) + && let Ok(Some(solution)) = PathBounds::default_solve(db, env, &constraints, bounds) { return Some(solution); } @@ -1229,10 +1310,10 @@ impl<'db> Signature<'db> { if let Some(bounds) = bounds && concrete_class_receiver && bound_signature - .variance_of(db, typevar.identity(db)) + .variance_of(db, env, typevar.identity(db)) .is_covariant() && bounds.lower.is_some_and(|lower| !lower.is_never()) - && let Ok(Some(solution)) = PathBounds::default_solve(db, &constraints, bounds) + && let Ok(Some(solution)) = PathBounds::default_solve(db, env, &constraints, bounds) { return Some(solution); } @@ -1242,7 +1323,7 @@ impl<'db> Signature<'db> { Some( self.apply_specialization(db, specialization) - .bind_self_with_receiver(db, Some(receiver_type), Some(typing_self_type)), + .bind_self_with_receiver(db, env, Some(receiver_type), Some(typing_self_type)), ) } @@ -1250,7 +1331,12 @@ impl<'db> Signature<'db> { /// /// This is used to prune impossible overloads when a method is bound to a concrete receiver. /// If a signature has no positional first parameter, we conservatively keep it. - fn can_bind_self_to(&self, db: &'db dyn Db, self_type: Type<'db>) -> bool { + fn can_bind_self_to( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + self_type: Type<'db>, + ) -> bool { // A dynamic receiver might be compatible with any explicit receiver annotation. if self_type.is_dynamic() { return true; @@ -1284,7 +1370,7 @@ impl<'db> Signature<'db> { // TODO: Expand type aliases here so `type Alias = Self` in a class body // participates in receiver-specific overload pruning. - expected_self_ty = expected_self_ty.bind_self_typevars(db, self_type); + expected_self_ty = expected_self_ty.bind_self_typevars(db, env, self_type); // `Self` binding can make the receiver annotation trivially compatible. if accepts_any_or_exact_self(expected_self_ty) { @@ -1292,7 +1378,7 @@ impl<'db> Signature<'db> { } // A specialized receiver can make generic receiver annotations concrete enough to compare. - if let Some((_, self_specialization)) = self_type.class_specialization(db) { + if let Some((_, self_specialization)) = self_type.class_specialization(db, env) { expected_self_ty = expected_self_ty.apply_optional_specialization(db, Some(self_specialization)); @@ -1306,11 +1392,12 @@ impl<'db> Signature<'db> { self_type .when_assignable_to( db, + env, expected_self_ty, &constraints, self.inferable_typevars(db), ) - .is_always_satisfied(db) + .is_always_satisfied(db, env) } pub(crate) fn has_explicit_positional_receiver_annotation(&self) -> bool { @@ -1328,18 +1415,21 @@ impl<'db> Signature<'db> { fn apply_self_with_receiver( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, receiver_type: Type<'db>, self_type: Type<'db>, ) -> Self { let binding_context = self.definition.map(BindingContext::Definition); let receiver_mapping = TypeMapping::BindSelf(SelfBinding::new( db, + env, receiver_type, - Some(BindingContext::Synthetic), + Some(BindingContext::Synthetic(env.program(db))), )); - let self_mapping = TypeMapping::BindSelf(SelfBinding::new(db, self_type, binding_context)); - let receiver_visitor = ApplyTypeMappingVisitor::default(); - let self_visitor = ApplyTypeMappingVisitor::default(); + let self_mapping = + TypeMapping::BindSelf(SelfBinding::new(db, env, self_type, binding_context)); + let receiver_visitor = ApplyTypeMappingVisitor::new(env); + let self_visitor = ApplyTypeMappingVisitor::new(env); let receiver_constraints = self .map_receiver_constraints( db, @@ -1357,9 +1447,9 @@ impl<'db> Signature<'db> { ) }) .filter(|constraints| { - !constraints.query(|_builder, constraints| constraints.is_always_satisfied(db)) + !constraints.query(|_builder, constraints| constraints.is_always_satisfied(db, env)) }); - if !self.needs_self_mapping(db, false) { + if !self.needs_self_mapping(db, env, false) { return Self { receiver_constraints, ..self.clone() @@ -1390,13 +1480,13 @@ impl<'db> Signature<'db> { fn receiver_constraints_when_satisfied<'c>( &self, - checker: &TypeRelationChecker<'_, 'c, 'db>, db: &'db dyn Db, + checker: &TypeRelationChecker<'_, 'c, 'db>, ) -> ConstraintSet<'db, 'c> { let Some(constraints) = self.receiver_constraints.as_ref() else { return checker.always(); }; - checker.constraints.load(db, constraints) + checker.constraints.load(db, checker.env, constraints) } fn map_receiver_constraints( @@ -1404,7 +1494,7 @@ impl<'db> Signature<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Option> { let constraints = Self::map_constraints( db, @@ -1413,8 +1503,9 @@ impl<'db> Signature<'db> { tcx, visitor, ); - (!constraints.query(|_builder, constraints| constraints.is_always_satisfied(db))) - .then_some(constraints) + (!constraints + .query(|_builder, constraints| constraints.is_always_satisfied(db, visitor.env))) + .then_some(constraints) } fn map_constraints( @@ -1422,7 +1513,7 @@ impl<'db> Signature<'db> { constraints: &OwnedConstraintSet<'db>, type_mapping: &TypeMapping<'_, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> OwnedConstraintSet<'db> { if !constraints .types() @@ -1433,7 +1524,7 @@ impl<'db> Signature<'db> { let builder = ConstraintSetBuilder::new(); builder.into_owned(|builder| { - let constraints = builder.load(db, constraints); + let constraints = builder.load(db, visitor.env, constraints); constraints.apply_type_mapping_impl(db, type_mapping, tcx, visitor) }) } @@ -1446,13 +1537,14 @@ impl<'db> Signature<'db> { /// Returns this signature with the given specialization applied to parameters and return type. fn apply_specialization(&self, db: &'db dyn Db, specialization: Specialization<'db>) -> Self { + let env = &ProgramEnvironment::from_program(specialization.generic_context(db).program(db)); let type_mapping = TypeMapping::ApplySpecialization(ApplySpecialization::Specialization(specialization)); self.apply_type_mapping_impl( db, &type_mapping, TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(env), ) } @@ -1460,12 +1552,13 @@ impl<'db> Signature<'db> { fn partially_apply( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, partial_application: &PartialApplication<'db>, inference: Option>, unspecialized_return_ty: Type<'db>, ) -> Self { let signature_specialization = - self.partial_application_specialization(db, partial_application, inference); + self.partial_application_specialization(db, env, partial_application, inference); let signature = signature_specialization.map_or_else( || self.clone(), |specialization| self.apply_specialization(db, specialization), @@ -1545,6 +1638,7 @@ impl<'db> Signature<'db> { fn partial_application_specialization( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, partial_application: &PartialApplication<'db>, inference: Option>, ) -> Option> { @@ -1561,9 +1655,11 @@ impl<'db> Signature<'db> { .enumerate() .filter(|(index, _)| !partial_application.is_positionally_bound(*index)) .any(|(_, parameter)| { - parameter - .annotated_type() - .references_typevar(db, typevar.typevar(db).identity(db)) + parameter.annotated_type().references_typevar( + db, + env, + typevar.typevar(db).identity(db), + ) }) }) .map(|typevar| typevar.identity(db)) @@ -1576,20 +1672,25 @@ impl<'db> Signature<'db> { Some(inference.specialization_with(db, |typevar, inferred| { promoted_typevars .contains(&typevar.identity(db)) - .then(|| inferred.map_or(Type::TypeVar(typevar), |ty| ty.promote(db))) + .then(|| inferred.map_or(Type::TypeVar(typevar), |ty| ty.promote(db, env))) })) } - fn needs_self_mapping(&self, db: &'db dyn Db, receiver_is_removed: bool) -> bool { + fn needs_self_mapping( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + receiver_is_removed: bool, + ) -> bool { // TODO: Expand type aliases here so `type Alias = Self` in parameters or returns // triggers binding when a method is accessed on a concrete receiver. - self.return_ty.contains_self(db) + self.return_ty.contains_self(db, env) || self .parameters .iter() .enumerate() .skip(usize::from(receiver_is_removed)) - .any(|(_, parameter)| parameter.annotated_type().contains_self(db)) + .any(|(_, parameter)| parameter.annotated_type().contains_self(db, env)) } fn inferable_typevars(&self, db: &'db dyn Db) -> TypeVarSet<'db> { @@ -1612,6 +1713,7 @@ impl<'db> Signature<'db> { pub(crate) fn non_generic_implementation_parameters_consistency_with( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overload: &Self, ) -> ParameterConsistency<'db> { debug_assert!(self.is_non_generic()); @@ -1623,8 +1725,9 @@ impl<'db> Signature<'db> { let relation_visitor = HasRelationToVisitor::default(&constraints); let disjointness_visitor = IsDisjointVisitor::default(&constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker::constraint_set_assignability_with_context( + env, &constraints, &relation_visitor, &disjointness_visitor, @@ -1634,7 +1737,7 @@ impl<'db> Signature<'db> { let is_consistent = checker .check_signature_pair(db, &implementation, &overload) - .is_always_satisfied(db); + .is_always_satisfied(db, env); if is_consistent { ParameterConsistency::Consistent @@ -1648,6 +1751,7 @@ impl<'db> Signature<'db> { pub(crate) fn non_generic_implementation_return_type_consistency_with( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, overload: &Self, ) -> ReturnTypeConsistency<'db> { debug_assert!(self.is_non_generic()); @@ -1657,8 +1761,9 @@ impl<'db> Signature<'db> { let relation_visitor = HasRelationToVisitor::default(&constraints); let disjointness_visitor = IsDisjointVisitor::default(&constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker::assignability_with_context( + env, &constraints, &relation_visitor, &disjointness_visitor, @@ -1668,7 +1773,7 @@ impl<'db> Signature<'db> { let is_consistent = checker .check_type_pair(db, overload.return_ty, self.return_ty) - .is_always_satisfied(db); + .is_always_satisfied(db, env); if is_consistent { ReturnTypeConsistency::Consistent @@ -1680,6 +1785,7 @@ impl<'db> Signature<'db> { pub(crate) fn when_constraint_set_assignable_to_signatures<'c>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: &CallableSignature<'db>, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { @@ -1701,6 +1807,7 @@ impl<'db> Signature<'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar_upper_bound( db, + env, constraints, self_bound_typevar, upper, @@ -1712,6 +1819,7 @@ impl<'db> Signature<'db> { .when_any(db, constraints, |other_return_type| { self.return_ty.when_constraint_set_assignable_to( db, + env, other_return_type, constraints, ) @@ -1723,21 +1831,23 @@ impl<'db> Signature<'db> { .overloads .iter() .when_all(db, constraints, |other_signature| { - self.when_constraint_set_assignable_to(db, other_signature, constraints) + self.when_constraint_set_assignable_to(db, env, other_signature, constraints) }) } fn when_constraint_set_assignable_to<'c>( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, other: &Self, constraints: &'c ConstraintSetBuilder<'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let signature_relation_visitor = SignatureRelationVisitor::default(); - let materialization_visitor = ApplyTypeMappingVisitor::default(); + let materialization_visitor = ApplyTypeMappingVisitor::new(env); let checker = TypeRelationChecker::constraint_set_assignability( + env, constraints, &relation_visitor, &disjointness_visitor, @@ -1779,7 +1889,12 @@ impl<'db> Signature<'db> { } impl<'db> VarianceInferable<'db> for &Signature<'db> { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { tracing::trace!( "Checking variance of `{tvar}` in `{self:?}`", tvar = typevar.identity.name(db) @@ -1789,7 +1904,7 @@ impl<'db> VarianceInferable<'db> for &Signature<'db> { parameter .annotated_type() .with_polarity(TypeVarVariance::Contravariant) - .variance_of(db, typevar) + .variance_of(db, env, typevar) }; let parameter_variances = if let Some((prefix_parameters, paramspec)) = @@ -1802,7 +1917,7 @@ impl<'db> VarianceInferable<'db> for &Signature<'db> { .chain(std::iter::once( Type::TypeVar(paramspec) .with_polarity(TypeVarVariance::Contravariant) - .variance_of(db, typevar), + .variance_of(db, env, typevar), )), ) } else { @@ -1811,7 +1926,7 @@ impl<'db> VarianceInferable<'db> for &Signature<'db> { itertools::chain( parameter_variances, - Some(self.return_ty.variance_of(db, typevar)), + Some(self.return_ty.variance_of(db, env, typevar)), ) .collect() } @@ -1857,27 +1972,31 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } }; - let is_unary_overload_aggregate_candidate_type = |ty: Type<'db>| { - // Keep aggregate probing away from inference-sensitive shapes and defer them to the - // legacy path, which already handles dynamic/typevar interactions. - !ty.has_dynamic(db) && !ty.has_typevar_or_typevar_instance(db) - }; - let other_parameter_type = single_required_positional_parameter_type(target_signature)?; // Keep this aggregate path narrowly scoped to unary target callables whose parameter // domain is an explicit union. // // Broader overload-set assignability (non-union unary domains, higher arity, // typevars/dynamic interactions) needs dedicated relation logic. - if !matches!(other_parameter_type, Type::Union(_)) - || !is_unary_overload_aggregate_candidate_type(other_parameter_type) + if !matches!(other_parameter_type, Type::Union(_)) { + return None; + } + + let env = self.env; + let is_unary_overload_aggregate_candidate_type = |ty: Type<'db>| { + // Keep aggregate probing away from inference-sensitive shapes and defer them to the + // legacy path, which already handles dynamic/typevar interactions. + !ty.has_dynamic(db, env) && !ty.has_typevar_or_typevar_instance(db, env) + }; + + if !is_unary_overload_aggregate_candidate_type(other_parameter_type) || !is_unary_overload_aggregate_candidate_type(target_signature.return_ty) { return None; } - let mut parameter_type_union = UnionBuilder::new(db); - let mut return_type_union = UnionBuilder::new(db); + let mut parameter_type_union = UnionBuilder::new(db, env); + let mut return_type_union = UnionBuilder::new(db, env); let mut has_overlapping_domain = false; for self_signature in source_signatures { @@ -1890,7 +2009,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let signatures_are_disjoint = self .as_disjointness_checker() .check_type_pair(db, self_parameter_type, other_parameter_type) - .is_always_satisfied(db); + .is_always_satisfied(db, env); if signatures_are_disjoint { continue; @@ -1913,7 +2032,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let aggregate_relation = parameters_cover_target.and(db, self.constraints, returns_match_target); aggregate_relation - .is_always_satisfied(db) + .is_always_satisfied(db, env) .then_some(aggregate_relation) } @@ -1950,6 +2069,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // instead. match (source_is_single_paramspec, target_is_single_paramspec) { (Some((source_tvar, source_return)), None) if target_overloads.len() > 1 => { + let env = self.env; let upper = Type::Callable(CallableType::new( db, CallableSignature::from_overloads(target_overloads.iter().map( @@ -1967,6 +2087,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar_upper_bound( db, + env, self.constraints, source_tvar, upper, @@ -1987,6 +2108,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } (None, Some((target_tvar, target_return))) if source_overloads.len() > 1 => { + let env = self.env; // TODO: Ideally, the constraint solver should use the return type constraint // to remove unmatched overloads from the `ParamSpec` specialization instead // of filtering them here. @@ -2004,7 +2126,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { target_return, ) }) - .is_never_satisfied(db) + .is_never_satisfied(db, env) }) .map(|signature| { Signature::new_generic( @@ -2020,6 +2142,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar_lower_bound( db, + env, self.constraints, target_tvar, lower, @@ -2120,6 +2243,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { source: &Signature<'db>, target: &Signature<'db>, ) -> ConstraintSet<'db, 'c> { + let env = self.env; // If either signature is generic, freshen that signature's typevars before considering // them inferable for this relation. The relation only needs to find one specialization of // each generic callable that causes the check to succeed, but those callable-local @@ -2131,7 +2255,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .max_typevar_freshness_matching_generic_context(db, generic_context) .map(|freshness| freshness.increment().value()) { - freshened_source = source.freshen_bound_typevars(db, delta); + freshened_source = source.freshen_bound_typevars(db, env, delta); &freshened_source } else { source @@ -2143,7 +2267,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .max_typevar_freshness_matching_generic_context(db, generic_context) .map(|freshness| freshness.increment().value()) { - freshened_target = target.freshen_bound_typevars(db, delta); + freshened_target = target.freshen_bound_typevars(db, env, delta); &freshened_target } else { target @@ -2164,10 +2288,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } let when = checker.with_signature_recursion_guard(source, target, || { source - .receiver_constraints_when_satisfied(&checker, db) + .receiver_constraints_when_satisfied(db, &checker) .and(db, self.constraints, || { target - .receiver_constraints_when_satisfied(&checker, db) + .receiver_constraints_when_satisfied(db, &checker) .and(db, self.constraints, || { checker.check_signature_pair_inner(db, source, target) }) @@ -2178,7 +2302,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // we produce, we reduce it back down to the inferable set that the caller asked about. // If we introduced new inferable typevars, those will be existentially quantified away // before returning. - when.reduce_inferable(db, self.constraints, signature_inferable) + when.reduce_inferable(db, env, self.constraints, signature_inferable) } fn with_signature_recursion_guard( @@ -2385,6 +2509,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } } + let env = self.env; let mut result = self.always(); // Avoid returning early after checking the return types in case there is a `ParamSpec` type @@ -2393,7 +2518,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let return_type_constraints = self.check_type_pair(db, source.return_ty, target.return_ty); let return_type_checks = !result .intersect(db, self.constraints, return_type_constraints) - .is_never_satisfied(db); + .is_never_satisfied(db, env); if let Some(context) = self.report_context() && !return_type_checks { @@ -2432,7 +2557,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let constraint_set = self.check_type_pair(db, target_ty, source_ty); if let Some(context) = self.report_context() - && constraint_set.is_never_satisfied(db) + && constraint_set.is_never_satisfied(db, env) { let parameter = ParameterDescription::new(target_index, target_name); context.push(ErrorContext::IncompatibleParameterTypes { @@ -2445,7 +2570,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // replace the diagnostic context that explains the incompatible parameter. !result .intersect(db, self.constraints, constraint_set) - .is_never_satisfied(db) + .is_never_satisfied(db, env) }; let parameter_must_have_default = |parameter: &Parameter<'db>, index: usize| { ErrorContext::RequiredParameterMustHaveDefault { @@ -2467,6 +2592,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (Some(([], source_bound_typevar)), Some(([], target_bound_typevar))) => { let param_spec_matches = ConstraintSet::constrain_typevar( db, + env, self.constraints, source_bound_typevar, Type::TypeVar(target_bound_typevar), @@ -2498,6 +2624,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_lower_bound( db, + env, self.constraints, target_bound_typevar, lower, @@ -2528,6 +2655,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar_upper_bound( db, + env, self.constraints, source_bound_typevar, upper, @@ -2658,6 +2786,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let param_spec_prefix_matches = ConstraintSet::constrain_typevar_lower_bound( db, + env, self.constraints, target_bound_typevar, lower, @@ -2686,6 +2815,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let param_spec_prefix_matches = ConstraintSet::constrain_typevar_upper_bound( db, + env, self.constraints, source_bound_typevar, upper, @@ -2695,6 +2825,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // When the prefixes match exactly, we just relate the remaining tails. let param_spec_matches = ConstraintSet::constrain_typevar( db, + env, self.constraints, source_bound_typevar, Type::TypeVar(target_bound_typevar), @@ -2723,6 +2854,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar_lower_bound( db, + env, self.constraints, target_bound_typevar, lower, @@ -2872,6 +3004,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_lower_bound( db, + env, self.constraints, target_bound_typevar, lower, @@ -2899,6 +3032,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )); let param_spec_matches = ConstraintSet::constrain_typevar_upper_bound( db, + env, self.constraints, source_bound_typevar, upper, @@ -3016,6 +3150,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { )); let param_spec_prefix_matches = ConstraintSet::constrain_typevar_upper_bound( db, + env, self.constraints, source_bound_typevar, upper, @@ -3412,7 +3547,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { if !check_types( &mut result, target_parameter.annotated_type(), - Type::empty_tuple(db), + Type::empty_tuple(db, env), target_parameter.name(), target_index, ) { @@ -3618,6 +3753,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }; Type::tuple(TupleType::mixed_with_segment( db, + env, captured_source_parameters() .take(source_variadic_index) .map(Parameter::annotated_type), @@ -3629,6 +3765,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } else { Type::heterogeneous_tuple( db, + env, captured_source_parameters() .map(Parameter::annotated_type), ) @@ -4456,6 +4593,7 @@ impl<'db> Parameters<'db> { node_index: _, } = parameters; + let env = ProgramEnvironment::from_definition(definition); let default_type = |param: &ast::ParameterWithDefault| { param.default().map(|default| { // Use the same approach as function_signature_expression_type to avoid cycles. @@ -4463,7 +4601,7 @@ impl<'db> Parameters<'db> { // directly to infer_deferred_types without first checking infer_definition_types. infer_deferred_types(db, definition) .expression_type(default) - .replace_parameter_defaults(db) + .replace_parameter_defaults(db, &env) }) }; @@ -4563,7 +4701,7 @@ impl<'db> Parameters<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { if let TypeMapping::Materialize(materialization_kind) = type_mapping && matches!( @@ -5031,7 +5169,7 @@ impl<'db> Parameter<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self { annotated_type: self.annotated_type.apply_type_mapping_impl( @@ -5050,12 +5188,18 @@ impl<'db> Parameter<'db> { } } - fn cycle_normalized(&self, db: &'db dyn Db, previous: &Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: &Self, + cycle: &salsa::Cycle, + ) -> Self { let annotated_type = self.annotated_type - .cycle_normalized(db, previous.annotated_type, cycle); + .cycle_normalized(db, env, previous.annotated_type, cycle); - let kind = self.kind.cycle_normalized(db, &previous.kind, cycle); + let kind = self.kind.cycle_normalized(db, env, &previous.kind, cycle); Self { annotated_type, @@ -5070,6 +5214,7 @@ impl<'db> Parameter<'db> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -5083,10 +5228,10 @@ impl<'db> Parameter<'db> { } = self; let annotated_type = if nested { - annotated_type.recursive_type_normalized_impl(db, div, true)? + annotated_type.recursive_type_normalized_impl(db, env, div, true)? } else { annotated_type - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }; @@ -5094,9 +5239,11 @@ impl<'db> Parameter<'db> { ParameterKind::PositionalOnly { name, default_type } => ParameterKind::PositionalOnly { name: name.clone(), default_type: match default_type { - Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?), + Some(ty) if nested => { + Some(ty.recursive_type_normalized_impl(db, env, div, true)?) + } Some(ty) => Some( - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, @@ -5107,10 +5254,10 @@ impl<'db> Parameter<'db> { name: name.clone(), default_type: match default_type { Some(ty) if nested => { - Some(ty.recursive_type_normalized_impl(db, div, true)?) + Some(ty.recursive_type_normalized_impl(db, env, div, true)?) } Some(ty) => Some( - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, @@ -5120,9 +5267,11 @@ impl<'db> Parameter<'db> { ParameterKind::KeywordOnly { name, default_type } => ParameterKind::KeywordOnly { name: name.clone(), default_type: match default_type { - Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?), + Some(ty) if nested => { + Some(ty.recursive_type_normalized_impl(db, env, div, true)?) + } Some(ty) => Some( - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), None => None, @@ -5150,7 +5299,7 @@ impl<'db> Parameter<'db> { parameter: &ast::Parameter, kind: ParameterKind<'db>, ) -> Self { - let index = semantic_index(db, function_definition.file(db)); + let index = semantic_index(db, function_definition.python_file(db)); let definition = Some(index.expect_single_definition(parameter)); let (annotated_type, inferred_annotation, annotation_flags, has_starred_annotation) = @@ -5365,18 +5514,25 @@ impl<'db> ParameterKind<'db> { #[expect(clippy::ref_option)] fn cycle_normalized_default( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, current: &Option>, previous: &Option>, cycle: &salsa::Cycle, ) -> Option> { match (current, previous) { - (Some(curr), Some(prev)) => Some(curr.cycle_normalized(db, *prev, cycle)), - (Some(curr), None) => Some(curr.recursive_type_normalized(db, cycle)), + (Some(curr), Some(prev)) => Some(curr.cycle_normalized(db, env, *prev, cycle)), + (Some(curr), None) => Some(curr.recursive_type_normalized(db, env, cycle)), (None, _) => *current, } } - fn cycle_normalized(&self, db: &'db dyn Db, previous: &Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: &Self, + cycle: &salsa::Cycle, + ) -> Self { match (self, previous) { ( ParameterKind::PositionalOnly { name, default_type }, @@ -5386,7 +5542,13 @@ impl<'db> ParameterKind<'db> { }, ) => ParameterKind::PositionalOnly { name: name.clone(), - default_type: Self::cycle_normalized_default(db, default_type, prev_default, cycle), + default_type: Self::cycle_normalized_default( + db, + env, + default_type, + prev_default, + cycle, + ), }, ( ParameterKind::PositionalOrKeyword { name, default_type }, @@ -5396,7 +5558,13 @@ impl<'db> ParameterKind<'db> { }, ) => ParameterKind::PositionalOrKeyword { name: name.clone(), - default_type: Self::cycle_normalized_default(db, default_type, prev_default, cycle), + default_type: Self::cycle_normalized_default( + db, + env, + default_type, + prev_default, + cycle, + ), }, ( ParameterKind::KeywordOnly { name, default_type }, @@ -5406,7 +5574,13 @@ impl<'db> ParameterKind<'db> { }, ) => ParameterKind::KeywordOnly { name: name.clone(), - default_type: Self::cycle_normalized_default(db, default_type, prev_default, cycle), + default_type: Self::cycle_normalized_default( + db, + env, + default_type, + prev_default, + cycle, + ), }, // Variadic / KeywordVariadic have no types to normalize. // Also, if the current `ParameterKind` is different from `previous`, it means that `previous` is the cycle initial value, @@ -5420,7 +5594,7 @@ impl<'db> ParameterKind<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let apply_to_default_type = |default_type: &Option>| { if type_mapping == &TypeMapping::ReplaceParameterDefaults && default_type.is_some() { @@ -5456,11 +5630,13 @@ mod tests { use crate::db::tests::{TestDb, setup_db}; use crate::place::global_symbol; use crate::types::{FunctionType, KnownClass, LiteralValueType}; + use ruff_db::PythonFile; use ruff_db::system::DbWithWritableSystem as _; #[track_caller] fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> { let module = ruff_db::files::system_path_to_file(db, file).unwrap(); + let module = PythonFile::new(db, module, db.python_version()); global_symbol(db, module, "f") .place .expect_type() @@ -5523,11 +5699,15 @@ mod tests { #[test] fn always_satisfied_receiver_constraints_are_discarded() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); assert!( - merge_receiver_constraints(&db, Some(&OwnedConstraintSet::always()), None).is_none() + merge_receiver_constraints(db, &env, Some(&OwnedConstraintSet::always()), None,) + .is_none() ); assert!( - merge_receiver_constraints(&db, None, Some(&OwnedConstraintSet::always())).is_none() + merge_receiver_constraints(db, &env, None, Some(&OwnedConstraintSet::always()),) + .is_none() ); } @@ -5566,18 +5746,26 @@ mod tests { let sig = func.signature(&db); - assert_eq!(sig.return_ty.display(&db).to_string(), "bytes"); + assert_eq!( + sig.return_ty + .display(&db, &db.program_environment()) + .to_string(), + "bytes" + ); assert_params_have_definitions(&sig); assert_params( &sig, &[ Parameter::positional_only(Some(Name::new_static("a"))), - Parameter::positional_only(Some(Name::new_static("b"))) - .with_annotated_type(KnownClass::Int.to_instance(&db)), + Parameter::positional_only(Some(Name::new_static("b"))).with_annotated_type( + KnownClass::Int.to_instance(&db, &db.program_environment()), + ), Parameter::positional_only(Some(Name::new_static("c"))) .with_default_type(Type::int_literal(1)), Parameter::positional_only(Some(Name::new_static("d"))) - .with_annotated_type(KnownClass::Int.to_instance(&db)) + .with_annotated_type( + KnownClass::Int.to_instance(&db, &db.program_environment()), + ) .with_default_type(Type::int_literal(2)), Parameter::positional_or_keyword(Name::new_static("e")) .with_default_type(Type::int_literal(3)), @@ -5590,8 +5778,9 @@ mod tests { Parameter::keyword_only(Name::new_static("h")) .with_annotated_type(LiteralValueType::unpromotable(6).into()) .with_default_type(LiteralValueType::unpromotable(6).into()), - Parameter::keyword_variadic(Name::new_static("kwargs")) - .with_annotated_type(KnownClass::Str.to_instance(&db)), + Parameter::keyword_variadic(Name::new_static("kwargs")).with_annotated_type( + KnownClass::Str.to_instance(&db, &db.program_environment()), + ), ], ); } @@ -5631,7 +5820,12 @@ mod tests { }; assert_eq!(name, "a"); // Parameter resolution not deferred; we should see A not B - assert_eq!(annotated_type.display(&db).to_string(), "A"); + assert_eq!( + annotated_type + .display(&db, &db.program_environment()) + .to_string(), + "A" + ); } #[test] @@ -5669,7 +5863,12 @@ mod tests { }; assert_eq!(name, "a"); // Parameter resolution deferred: - assert_eq!(annotated_type.display(&db).to_string(), "A | B"); + assert_eq!( + annotated_type + .display(&db, &db.program_environment()) + .to_string(), + "A | B" + ); } #[test] @@ -5712,8 +5911,18 @@ mod tests { }; assert_eq!(a_name, "a"); assert_eq!(b_name, "b"); - assert_eq!(a_annotated_ty.display(&db).to_string(), "A"); - assert_eq!(b_annotated_ty.display(&db).to_string(), "T@f"); + assert_eq!( + a_annotated_ty + .display(&db, &db.program_environment()) + .to_string(), + "A" + ); + assert_eq!( + b_annotated_ty + .display(&db, &db.program_environment()) + .to_string(), + "T@f" + ); } #[test] @@ -5757,8 +5966,18 @@ mod tests { assert_eq!(a_name, "a"); assert_eq!(b_name, "b"); // Parameter resolution deferred: - assert_eq!(a_annotated_ty.display(&db).to_string(), "A | B"); - assert_eq!(b_annotated_ty.display(&db).to_string(), "T@f"); + assert_eq!( + a_annotated_ty + .display(&db, &db.program_environment()) + .to_string(), + "A | B" + ); + assert_eq!( + b_annotated_ty + .display(&db, &db.program_environment()) + .to_string(), + "T@f" + ); } #[test] diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 04ac982359..e427b2f96a 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -2,6 +2,7 @@ //! Each of these is considered to inhabit a unique type in our model of the type system. use super::{ClassType, Type, TypeFormType, class::KnownClass}; +use crate::ProgramEnvironment; use crate::db::Db; use crate::types::IntersectionType; use crate::types::infer::InferenceFlags; @@ -10,7 +11,7 @@ use crate::types::{ generics::typing_self, infer::{function_known_decorator_flags, nearest_enclosing_class}, }; -use ruff_db::files::File; +use ruff_db::PythonFile; use strum_macros::EnumString; use ty_module_resolver::{KnownModule, file_to_module, resolve_module_confident}; use ty_python_core::{ @@ -228,10 +229,14 @@ impl SpecialFormType { /// Return the instance type which this type is a subtype of. /// /// For example, the symbol `typing.Literal` is an instance of `typing._SpecialForm`, - /// so `SpecialFormType::Literal.instance_fallback(db)` + /// so `SpecialFormType::Literal.instance_fallback(db, python_version)` /// returns `Type::NominalInstance(NominalInstanceType { class: })`. - pub(super) fn instance_fallback(self, db: &dyn Db) -> Type<'_> { - self.class().to_instance(db) + pub(super) fn instance_fallback<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.class().to_instance(db, env) } /// Return `true` if this special form is guaranteed to be a singleton at runtime. @@ -248,7 +253,11 @@ impl SpecialFormType { /// Return the type denoted by this retained special-form value when it is valid without /// parameters or a surrounding inference scope. - pub(crate) fn type_form_argument(self, db: &dyn Db) -> Option> { + pub(crate) fn type_form_argument<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { Self::Never | Self::NoReturn => Some(Type::Never), Self::LiteralString => Some(Type::literal_string()), @@ -258,28 +267,34 @@ impl SpecialFormType { Self::AlwaysFalsy => Some(Type::AlwaysFalsy), Self::NamedTuple => Some(IntersectionType::from_two_elements( db, - Type::homogeneous_tuple(db, Type::object()), - KnownClass::NamedTupleLike.to_instance(db), + env, + Type::homogeneous_tuple(db, env, Type::object()), + KnownClass::NamedTupleLike.to_instance(db, env), )), - Self::Type => Some(KnownClass::Type.to_instance(db)), + Self::Type => Some(KnownClass::Type.to_instance(db, env)), Self::TypeForm => Some(TypeFormType::from_type_expression(db, Type::any())), - Self::Tuple => Some(Type::homogeneous_tuple(db, Type::unknown())), + Self::Tuple => Some(Type::homogeneous_tuple(db, env, Type::unknown())), Self::TypingCallable | Self::CollectionsAbcCallable => { Some(Type::Callable(CallableType::unknown(db))) } - Self::LegacyStdlibAlias(alias) => Some(alias.aliased_class().to_instance(db)), + Self::LegacyStdlibAlias(alias) => Some(alias.aliased_class().to_instance(db, env)), _ => None, } } /// Return `true` if this symbol is an instance of `class`. - pub(super) fn is_instance_of(self, db: &dyn Db, class: ClassType) -> bool { - self.class().is_subclass_of(db, class) + pub(super) fn is_instance_of( + self, + db: &dyn Db, + env: &ProgramEnvironment<'_>, + class: ClassType, + ) -> bool { + self.class().is_subclass_of(db, env, class) } pub(super) fn try_from_file_and_name( db: &dyn Db, - file: File, + file: PythonFile<'_>, symbol_name: &str, ) -> Option { Self::candidates_from_name(symbol_name) @@ -582,8 +597,12 @@ impl SpecialFormType { } } - pub(super) fn to_meta_type(self, db: &dyn Db) -> Type<'_> { - self.class().to_class_literal(db) + pub(super) fn to_meta_type<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + self.class().to_class_literal(db, env) } /// Return true if this special form is callable at runtime. @@ -802,11 +821,16 @@ impl SpecialFormType { } } - pub(super) fn definition(self, db: &dyn Db) -> Option> { + pub(super) fn definition<'db>( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { self.definition_modules() .iter() .find_map(|module| { - let file = resolve_module_confident(db, &module.name())?.file(db)?; + let file = resolve_module_confident(db, env.python_version(db), &module.name())? + .python_file(db)?; let scope = FileScopeId::global().to_scope_id(db, file); let symbol_id = place_table(db, scope).symbol_id(self.name())?; @@ -830,6 +854,8 @@ impl SpecialFormType { typevar_binding_context: Option>, inference_flags: InferenceFlags, ) -> Result, InvalidTypeExpression<'db>> { + let env = ProgramEnvironment::from_scope(scope_id); + let env = &env; match self { Self::Never | Self::NoReturn => Ok(Type::Never), Self::LiteralString => Ok(Type::literal_string()), @@ -850,8 +876,9 @@ impl SpecialFormType { // See conversation in https://github.com/astral-sh/ruff/pull/19915. Self::NamedTuple => Ok(IntersectionType::from_two_elements( db, - Type::homogeneous_tuple(db, Type::object()), - KnownClass::NamedTupleLike.to_instance(db), + env, + Type::homogeneous_tuple(db, env, Type::object()), + KnownClass::NamedTupleLike.to_instance(db, env), )), Self::TypingSelf => { @@ -859,7 +886,8 @@ impl SpecialFormType { return Err(InvalidTypeExpression::TypingSelfInTypeAlias); } - let index = semantic_index(db, scope_id.file(db)); + let python_file = scope_id.python_file(db); + let index = semantic_index(db, python_file); let Some(class) = nearest_enclosing_class(db, index, scope_id) else { return Err(InvalidTypeExpression::InvalidType( Type::SpecialForm(self), @@ -888,12 +916,12 @@ impl SpecialFormType { } let is_in_metaclass = KnownClass::Type - .to_class_literal(db) + .to_class_literal(db, env) .to_class_type(db) .is_some_and(|type_class| { class .default_specialization(db) - .is_subclass_of(db, type_class) + .is_subclass_of(db, env, type_class) }); if is_in_metaclass { return Err(InvalidTypeExpression::TypingSelfInMetaclass); @@ -939,13 +967,15 @@ impl SpecialFormType { | Self::RegularCallableTypeOf => Err(InvalidTypeExpression::RequiresOneArgument(self)), // We treat `typing.Type` exactly the same as `builtins.type`: - SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db)), + SpecialFormType::Type => Ok(KnownClass::Type.to_instance(db, env)), SpecialFormType::TypeForm => Ok(TypeFormType::from_type_expression(db, Type::any())), - SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, Type::unknown())), + SpecialFormType::Tuple => Ok(Type::homogeneous_tuple(db, env, Type::unknown())), SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable => { Ok(Type::Callable(CallableType::unknown(db))) } - SpecialFormType::LegacyStdlibAlias(alias) => Ok(alias.aliased_class().to_instance(db)), + SpecialFormType::LegacyStdlibAlias(alias) => { + Ok(alias.aliased_class().to_instance(db, env)) + } SpecialFormType::TypeQualifier(qualifier) => { Err(InvalidTypeExpression::TypeQualifier(qualifier)) } diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index aebe16f125..bb7eab33a8 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -1,3 +1,6 @@ +use crate::Db; +use crate::FxOrderSet; +use crate::ProgramEnvironment; use crate::place::PlaceAndQualifiers; use crate::types::class::DynamicClassLiteral; use crate::types::constraints::ConstraintSet; @@ -9,7 +12,6 @@ use crate::types::{ ProtocolInstanceType, SpecialFormType, Type, TypeContext, TypeMapping, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarVariance, TypedDictType, UnionType, todo_type, }; -use crate::{Db, FxOrderSet}; use ty_python_core::definition::Definition; /// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`. @@ -39,14 +41,18 @@ 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, + env: &ProgramEnvironment<'db>, + subclass_of: impl Into>, + ) -> Type<'db> { let subclass_of = subclass_of.into(); match subclass_of { SubclassOfInner::Class(class) => { if class.is_final(db) { Type::from(class) } else if class.is_object(db) { - Self::subclass_of_object(db) + Self::subclass_of_object(db, env) } else { Type::SubclassOf(Self { subclass_of }) } @@ -65,7 +71,11 @@ impl<'db> SubclassOfType<'db> { } /// Given the class object `T`, returns a [`Type`] instance representing `type[T]`. - pub(crate) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option> { + pub(crate) fn try_from_type( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option> { let subclass_of = match ty { Type::Dynamic(dynamic) => SubclassOfInner::Dynamic(dynamic), Type::ClassLiteral(literal) => { @@ -79,24 +89,29 @@ impl<'db> SubclassOfType<'db> { _ => return None, }; - Some(Self::from(db, subclass_of)) + Some(Self::from(db, env, subclass_of)) } /// Given an instance of the class or type variable `T`, returns a [`Type`] instance representing `type[T]`. - pub(crate) fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option> { + pub(crate) fn try_from_instance( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option> { // Handle unions by distributing `type[]` over each element: // `type[A | B]` -> `type[A] | type[B]` match ty { Type::Union(union) => UnionType::try_from_elements( db, + env, union .elements(db) .iter() - .map(|element| Self::try_from_instance(db, *element)), + .map(|element| Self::try_from_instance(db, env, *element)), ), - Type::ProtocolInstance(protocol) => Some(protocol.to_meta_type(db)), - _ => SubclassOfInner::try_from_instance(db, ty) - .map(|subclass_of| Self::from(db, subclass_of)), + Type::ProtocolInstance(protocol) => Some(protocol.to_meta_type(db, env)), + _ => SubclassOfInner::try_from_instance(db, env, ty) + .map(|subclass_of| Self::from(db, env, subclass_of)), } } @@ -116,9 +131,9 @@ impl<'db> SubclassOfType<'db> { } /// Return a [`Type`] instance representing the type `type[object]`. - fn subclass_of_object(db: &'db dyn Db) -> Type<'db> { + fn subclass_of_object(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { // See the documentation of `SubclassOfType::from` for details. - KnownClass::Type.to_instance(db) + KnownClass::Type.to_instance(db, env) } /// Return the inner [`SubclassOfInner`] value wrapped by this `SubclassOfType`. @@ -130,6 +145,7 @@ impl<'db> SubclassOfType<'db> { pub(super) fn meta_write_requirement( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, ) -> Option<(Option>, TypeQualifiers)> { let SubclassOfInner::Protocol(protocol) = self.subclass_of else { @@ -137,7 +153,7 @@ impl<'db> SubclassOfType<'db> { }; protocol .interface(db) - .meta_write_requirement(db, Type::ProtocolInstance(protocol), name) + .meta_write_requirement(db, env, Type::ProtocolInstance(protocol), name) .map(|(write_ty, mut qualifiers)| { // `ClassVar` prohibits instance writes, not writes through the class object. qualifiers.remove(TypeQualifiers::CLASS_VAR); @@ -163,11 +179,15 @@ impl<'db> SubclassOfType<'db> { /// Return the exact class-object type of this `type[T]` `TypeVar`'s upper bound, if it has one. /// /// This can only succeed when the upper bound normalizes to a final class. - pub(crate) fn exact_typevar_upper_bound(self, db: &'db dyn Db) -> Option> { + pub(crate) fn exact_typevar_upper_bound( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { self.into_type_var() - .and_then(|typevar| typevar.typevar(db).upper_bound(db)) + .and_then(|typevar| typevar.typevar(db).upper_bound(db, env)) .and_then(|bound| { - let bound = Self::try_from_instance(db, bound.resolve_type_alias(db))?; + let bound = Self::try_from_instance(db, env, bound.resolve_type_alias(db))?; matches!(bound, Type::ClassLiteral(_) | Type::GenericAlias(_)).then_some(bound) }) } @@ -177,7 +197,7 @@ impl<'db> SubclassOfType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Type<'db> { match self.subclass_of { SubclassOfInner::Class(class) => Type::SubclassOf(Self { @@ -190,17 +210,18 @@ impl<'db> SubclassOfType<'db> { }), SubclassOfInner::Protocol(protocol) => protocol .apply_type_mapping_impl(db, type_mapping, tcx, visitor) - .to_meta_type(db), + .to_meta_type(db, visitor.env), SubclassOfInner::Dynamic(_) => match type_mapping { TypeMapping::Materialize(materialization_kind) => match materialization_kind { - MaterializationKind::Top => KnownClass::Type.to_instance(db), + MaterializationKind::Top => KnownClass::Type.to_instance(db, visitor.env), MaterializationKind::Bottom => Type::Never, }, _ => Type::SubclassOf(self), }, SubclassOfInner::TypeVar(typevar) => { let mapped = typevar.apply_type_mapping_impl(db, type_mapping, visitor); - Self::try_from_instance(db, mapped).unwrap_or_else(|| mapped.to_meta_type(db)) + Self::try_from_instance(db, visitor.env, mapped) + .unwrap_or_else(|| mapped.to_meta_type(db, visitor.env)) } } } @@ -208,6 +229,7 @@ impl<'db> SubclassOfType<'db> { pub(super) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, @@ -215,14 +237,15 @@ impl<'db> SubclassOfType<'db> { match self.subclass_of { SubclassOfInner::Dynamic(_) => {} SubclassOfInner::Class(class) => { - class.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + class.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } SubclassOfInner::Protocol(protocol) => { - protocol.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + protocol.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } SubclassOfInner::TypeVar(typevar) => { Type::TypeVar(typevar).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -234,49 +257,51 @@ impl<'db> SubclassOfType<'db> { pub(crate) fn find_name_in_mro_with_policy( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &str, policy: MemberLookupPolicy, ) -> Option> { if let SubclassOfInner::Protocol(protocol) = self.subclass_of - && let Some(member) = protocol.interface(db).meta_member(db, name) + && let Some(member) = protocol.interface(db).meta_member(db, env, name) { return Some(member); } - let class_like = match self.subclass_of.with_transposed_type_var(db) { + let class_like = match self.subclass_of.with_transposed_type_var(db, env) { SubclassOfInner::Class(class) => Type::from(class), SubclassOfInner::Dynamic(dynamic) => Type::Dynamic(dynamic), SubclassOfInner::Protocol(protocol) => Type::from(*protocol.class_origin(db)?), SubclassOfInner::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { None => unreachable!(), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound, Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - constraints.as_type(db) + constraints.as_type(db, env) } } } }; - class_like.find_name_in_mro_with_policy(db, name, policy) + class_like.find_name_in_mro_with_policy(db, env, name, policy) } pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self { subclass_of: self .subclass_of - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, }) } - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn to_instance(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self.subclass_of { - SubclassOfInner::Class(class) => Type::instance(db, class), + SubclassOfInner::Class(class) => Type::instance(db, env, class), SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type), SubclassOfInner::Protocol(protocol) => Type::ProtocolInstance(protocol), SubclassOfInner::TypeVar(bound_typevar) => Type::TypeVar(bound_typevar), @@ -284,14 +309,18 @@ impl<'db> SubclassOfType<'db> { } /// Return a type representing "the set of all instances of the metaclass of this type". - pub(crate) fn to_metaclass_instance(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn to_metaclass_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { // This kind of looks like a no-op, but it's not. For `type[C]` where `C` has metaclass // `M`, `to_meta_type` transforms `type[C]` to `type[M]`, and then `to_instance` makes it // just `M`. And `to_meta_type` will transpose `type[T: C]` into `T: type[C]`, collapse to // the upper bound `type[C]`, and transform that to the meta-type `type[M]`, which // `to_instance` then resolves to `M`. - self.to_meta_type(db) - .to_instance_approximation(db) + self.to_meta_type(db, env) + .to_instance_approximation(db, env) .expect("the meta-type of a SubclassOf type should always be instantiable") } @@ -300,45 +329,54 @@ impl<'db> SubclassOfType<'db> { /// For `type[C]` where `C` is a concrete class, this returns `type[metaclass(C)]`. /// For `type[T]` where `T` is a `TypeVar`, this computes the metatype based on the /// `TypeVar`'s bounds or constraints. - pub(crate) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { - match self.subclass_of.with_transposed_type_var(db) { + pub(crate) fn to_meta_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + match self.subclass_of.with_transposed_type_var(db, env) { SubclassOfInner::Dynamic(dynamic) => { - SubclassOfType::from(db, SubclassOfInner::Dynamic(dynamic)) + SubclassOfType::from(db, env, SubclassOfInner::Dynamic(dynamic)) + } + SubclassOfInner::Class(class) => { + SubclassOfType::try_from_type(db, env, class.metaclass(db)) + .unwrap_or(SubclassOfType::subclass_of_unknown()) } - SubclassOfInner::Class(class) => SubclassOfType::try_from_type(db, class.metaclass(db)) - .unwrap_or(SubclassOfType::subclass_of_unknown()), // Structural implementations of a protocol can have arbitrary metaclasses. The only // guaranteed upper bound is therefore `type`, not the protocol origin's metaclass. - SubclassOfInner::Protocol(_) => KnownClass::Type.to_subclass_of(db), + SubclassOfInner::Protocol(_) => KnownClass::Type.to_subclass_of(db, env), // For `type[T]` where `T` is a TypeVar, `with_transposed_type_var` transforms // the bounds from instance types to `type[]` types. For example, `type[T]` where // `T: A | B` becomes a TypeVar with bound `type[A] | type[B]`. The metatype is // then the metatype of that bound. SubclassOfInner::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { + match bound_typevar.typevar(db).bound_or_constraints(db, env) { // `with_transposed_type_var` always adds a bound for unbounded TypeVars None => unreachable!(), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db), + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + bound.to_meta_type(db, env) + } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { - constraints.as_type(db).to_meta_type(db) + constraints.as_type(db, env).to_meta_type(db, env) } } } } } - pub(crate) fn is_typed_dict(self, db: &'db dyn Db) -> bool { + pub(crate) fn is_typed_dict(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { self.subclass_of - .into_class(db) + .into_class(db, env) .is_some_and(|class| class.class_literal(db).is_typed_dict(db)) } } impl<'db> VarianceInferable<'db> for SubclassOfType<'db> { - fn variance_of(self, db: &dyn Db, typevar: BoundTypeVarIdentity<'_>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'_>, + ) -> TypeVarVariance { match self.subclass_of { - SubclassOfInner::Class(class) => class.variance_of(db, typevar), - SubclassOfInner::Protocol(protocol) => protocol.variance_of(db, typevar), + SubclassOfInner::Class(class) => class.variance_of(db, env, typevar), + SubclassOfInner::Protocol(protocol) => protocol.variance_of(db, env, typevar), SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => TypeVarVariance::Bivariant, } } @@ -363,7 +401,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return self.check_type_pair( db, Type::ProtocolInstance(source_protocol), - target.to_instance(db), + target.to_instance(db, self.env), ); } @@ -423,7 +461,7 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { (SubclassOfInner::Class(left), SubclassOfInner::Class(right)) => { ConstraintSet::from_bool( self.constraints, - !left.could_coexist_in_mro_with_disjointness_checker(db, right, self), + !left.could_coexist_in_mro_with_disjointness_checker(db, self.env, right, self), ) } (SubclassOfInner::TypeVar(_), _) | (_, SubclassOfInner::TypeVar(_)) => { @@ -473,19 +511,25 @@ impl<'db> SubclassOfInner<'db> { matches!(self, Self::TypeVar(_)) } - pub(crate) fn into_class(self, db: &'db dyn Db) -> Option> { + pub(crate) fn into_class( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { match self { Self::Dynamic(_) | Self::Protocol(_) => None, Self::Class(class) => Some(class), Self::TypeVar(bound_typevar) => { - match bound_typevar.typevar(db).bound_or_constraints(db) { - None => Some(ClassType::object(db)), + match bound_typevar.typevar(db).bound_or_constraints(db, env) { + None => Some(ClassType::object(db, env)), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - Self::try_from_instance(db, bound) - .and_then(|subclass_of| subclass_of.into_class(db)) + Self::try_from_instance(db, env, bound) + .and_then(|subclass_of| subclass_of.into_class(db, env)) } // TODO this is quite imprecise - Some(TypeVarBoundOrConstraints::Constraints(_)) => Some(ClassType::object(db)), + Some(TypeVarBoundOrConstraints::Constraints(_)) => { + Some(ClassType::object(db, env)) + } } } } @@ -505,9 +549,13 @@ impl<'db> SubclassOfInner<'db> { } } - fn try_from_instance(db: &'db dyn Db, ty: Type<'db>) -> Option { + fn try_from_instance( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> Option { Some(match ty { - Type::NominalInstance(instance) => SubclassOfInner::Class(instance.class(db)), + Type::NominalInstance(instance) => SubclassOfInner::Class(instance.class(db, env)), Type::TypedDict(typed_dict) => match typed_dict { TypedDictType::Class(class) => SubclassOfInner::Class(class), TypedDictType::Synthesized(_) => SubclassOfInner::Dynamic( @@ -537,7 +585,11 @@ impl<'db> SubclassOfInner<'db> { /// - Otherwise, for an unbounded type variable, this returns `type[object]`. /// /// If this is type of a concrete type `C`, returns the type unchanged. - pub(crate) fn with_transposed_type_var(self, db: &'db dyn Db) -> Self { + pub(crate) fn with_transposed_type_var( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Self { let Some(bound_typevar) = self.into_type_var() else { return self; }; @@ -545,15 +597,15 @@ impl<'db> SubclassOfInner<'db> { let bound_typevar = bound_typevar.map_bound_or_constraints(db, |bound_or_constraints| { Some(match bound_or_constraints { None => TypeVarBoundOrConstraints::UpperBound( - SubclassOfType::try_from_instance(db, Type::object()) + SubclassOfType::try_from_instance(db, env, Type::object()) .unwrap_or(SubclassOfType::subclass_of_unknown()), ), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - TypeVarBoundOrConstraints::UpperBound(bound.to_meta_type(db)) + TypeVarBoundOrConstraints::UpperBound(bound.to_meta_type(db, env)) } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { TypeVarBoundOrConstraints::Constraints( - constraints.map(db, |constraint| constraint.to_meta_type(db)), + constraints.map(db, |constraint| constraint.to_meta_type(db, env)), ) } }) @@ -565,16 +617,17 @@ impl<'db> SubclassOfInner<'db> { fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Self::Class(class) => Some(Self::Class( - class.recursive_type_normalized_impl(db, div, nested)?, + class.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::Dynamic(dynamic) => Some(Self::Dynamic(dynamic.recursive_type_normalized())), Self::Protocol(protocol) => Some(Self::Protocol( - protocol.recursive_type_normalized_impl(db, div, nested)?, + protocol.recursive_type_normalized_impl(db, env, div, nested)?, )), Self::TypeVar(_) => Some(self), } diff --git a/crates/ty_python_semantic/src/types/subscript.rs b/crates/ty_python_semantic/src/types/subscript.rs index 1a8efe6a58..24bd6a55a9 100644 --- a/crates/ty_python_semantic/src/types/subscript.rs +++ b/crates/ty_python_semantic/src/types/subscript.rs @@ -1,11 +1,12 @@ //! Inference for subscript expressions (e.g., `x[0]`, `list[int]`). +use crate::Db; +use crate::ProgramEnvironment; use std::fmt::{self, Display}; use compact_str::{CompactString, ToCompactString}; use ruff_python_ast as ast; -use crate::Db; use crate::subscript::{PyIndex, PySlice}; use crate::types::special_form::TypeQualifier; @@ -211,6 +212,7 @@ impl<'db> SubscriptErrorKind<'db> { slice_node: &ast::Expr, ) { let db = context.db(); + let env = context.program_environment(); match self { Self::IndexOutOfBounds { kind, @@ -241,7 +243,7 @@ impl<'db> SubscriptErrorKind<'db> { diagnostic.annotate(context.secondary(&*subscript.value).message( format_args!( "Alias to `{}`, which is already specialized", - value_type.display(db) + value_type.display(db, env) ), )); } @@ -261,7 +263,7 @@ impl<'db> SubscriptErrorKind<'db> { { builder.into_diagnostic(format_args!( "Method `{method}` of type `{}` may be missing", - value_ty.display(db), + value_ty.display(db, env), )); } } @@ -277,8 +279,8 @@ impl<'db> SubscriptErrorKind<'db> { builder.into_diagnostic(format_args!( "Method `{method}` of type `{}` is not callable \ on object of type `{}`", - bindings.callable_type().display(db), - value_ty.display(db), + bindings.callable_type().display(db, env), + value_ty.display(db, env), )); } } @@ -299,9 +301,9 @@ impl<'db> SubscriptErrorKind<'db> { builder.into_diagnostic(format_args!( "Method `{method}` of type `{}` cannot be called \ with key of type `{}` on object of type `{}`", - bindings.callable_type().display(db), - slice_ty.display(db), - value_ty.display(db), + bindings.callable_type().display(db, env), + slice_ty.display(db, env), + value_ty.display(db, env), )); } } @@ -310,8 +312,8 @@ impl<'db> SubscriptErrorKind<'db> { builder.into_diagnostic(format_args!( "Method `{method}` of type `{}` may not be callable \ on object of type `{}`", - bindings.callable_type().display(db), - value_ty.display(db), + bindings.callable_type().display(db, env), + value_ty.display(db, env), )); } } @@ -342,7 +344,7 @@ impl<'db> SubscriptErrorKind<'db> { if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, value_node) { builder.into_diagnostic(format_args!( "`{}` is not a valid argument to `{origin}`", - argument_ty.display(db), + argument_ty.display(db, env), )); } } @@ -385,13 +387,14 @@ impl<'db> SubscriptErrorKind<'db> { fn map_union_subscript<'db, F>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, union: UnionType<'db>, mut map_fn: F, ) -> Result, SubscriptError<'db>> where F: FnMut(Type<'db>) -> Result, SubscriptError<'db>>, { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut errors = Vec::new(); for element in union.elements(db) { @@ -423,13 +426,14 @@ where fn map_intersection_subscript<'db, F>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, intersection: IntersectionType<'db>, mut map_fn: F, ) -> Result, SubscriptError<'db>> where F: FnMut(Type<'db>) -> Result, SubscriptError<'db>>, { - if let Some(alternatives) = intersection.finite_alternative_union(db) { + if let Some(alternatives) = intersection.finite_alternative_union(db, env) { return map_fn(alternatives); } @@ -448,7 +452,7 @@ where // If any element succeeded, return the intersection of successful results. if !results.is_empty() { - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); for result in results { builder.add_positive_in_place(result); } @@ -460,7 +464,7 @@ where // for elements that lack the method. let any_has_method = errors.iter().any(SubscriptError::any_method_available); - let mut builder = IntersectionBuilder::new(db); + let mut builder = IntersectionBuilder::new(db, env); let mut collected_errors = Vec::new(); let full_object_ty = Type::Intersection(intersection); @@ -492,11 +496,12 @@ where // `Unknown` otherwise. This is not naturally representable via synthesized `__getitem__` overloads. fn typed_dict_subscript<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, slice_ty: Type<'db>, ) -> Result, SubscriptError<'db>> { if let Some(fallback) = slice_ty.materialized_divergent_fallback() { - return typed_dict_subscript(db, typed_dict, fallback); + return typed_dict_subscript(db, env, typed_dict, fallback); } if slice_ty.is_dynamic() { @@ -508,14 +513,14 @@ fn typed_dict_subscript<'db>( .map(|literal| literal.value(db)) else { if typed_dict.explicit_extra_items(db).is_some() - && slice_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && slice_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { - return Ok(typed_dict.value_type(db)); + return Ok(typed_dict.value_type(db, env)); } let result_ty = if typed_dict.openness(db).is_closed() - && slice_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && slice_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { - typed_dict.value_type(db) + typed_dict.value_type(db, env) } else { Type::unknown() }; @@ -548,15 +553,16 @@ impl<'db> Type<'db> { pub(super) fn subscript( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, slice_ty: Type<'db>, expr_context: ast::ExprContext, ) -> Result, SubscriptError<'db>> { if let Some(fallback) = self.materialized_divergent_fallback() { - return fallback.subscript(db, slice_ty, expr_context); + return fallback.subscript(db, env, slice_ty, expr_context); } if let Some(fallback) = slice_ty.materialized_divergent_fallback() { - return self.subscript(db, fallback, expr_context); + return self.subscript(db, env, fallback, expr_context); } let value_ty = self; @@ -564,47 +570,58 @@ impl<'db> Type<'db> { let inferred = match (value_ty, slice_ty) { (Type::Dynamic(_) | Type::Divergent(_) | Type::Never, _) => Some(Ok(value_ty)), - (Type::TypeAlias(alias), _) => { - Some(alias.value_type(db).subscript(db, slice_ty, expr_context)) - } + (Type::TypeAlias(alias), _) => Some(alias.value_type(db).subscript( + db, + env, + slice_ty, + expr_context, + )), (_, Type::TypeAlias(alias)) => { - Some(value_ty.subscript(db, alias.value_type(db), expr_context)) + Some(value_ty.subscript(db, env, alias.value_type(db), expr_context)) } - (Type::Union(union), _) => Some(map_union_subscript(db, union, |element| { - element.subscript(db, slice_ty, expr_context) + (Type::Union(union), _) => Some(map_union_subscript(db, env, union, |element| { + element.subscript(db, env, slice_ty, expr_context) })), - (_, Type::Union(union)) => Some(map_union_subscript(db, union, |element| { - value_ty.subscript(db, element, expr_context) + (_, Type::Union(union)) => Some(map_union_subscript(db, env, union, |element| { + value_ty.subscript(db, env, element, expr_context) })), - (Type::EnumComplement(complement), _) => Some( - complement - .remaining_literal_union(db) - .subscript(db, slice_ty, expr_context), - ), - - (_, Type::EnumComplement(complement)) => { - Some(value_ty.subscript(db, complement.remaining_literal_union(db), expr_context)) + (Type::EnumComplement(complement), _) => { + Some(complement.remaining_literal_union(db, env).subscript( + db, + env, + slice_ty, + expr_context, + )) } - (Type::Intersection(intersection), _) => { - Some(map_intersection_subscript(db, intersection, |element| { - element.subscript(db, slice_ty, expr_context) - })) - } + (_, Type::EnumComplement(complement)) => Some(value_ty.subscript( + db, + env, + complement.remaining_literal_union(db, env), + expr_context, + )), - (_, Type::Intersection(intersection)) => { - Some(map_intersection_subscript(db, intersection, |element| { - value_ty.subscript(db, element, expr_context) - })) - } + (Type::Intersection(intersection), _) => Some(map_intersection_subscript( + db, + env, + intersection, + |element| element.subscript(db, env, slice_ty, expr_context), + )), + + (_, Type::Intersection(intersection)) => Some(map_intersection_subscript( + db, + env, + intersection, + |element| value_ty.subscript(db, env, element, expr_context), + )), // Ex) Given `person["name"]`, return `str` (Type::TypedDict(typed_dict), _) if expr_context != ast::ExprContext::Store => { - Some(typed_dict_subscript(db, typed_dict, slice_ty)) + Some(typed_dict_subscript(db, env, typed_dict, slice_ty)) } ( @@ -633,10 +650,10 @@ impl<'db> Type<'db> { // Ex) Given `("a", "b", "c", "d")[1]`, return `"b"` (Type::NominalInstance(nominal), Type::LiteralValue(literal)) if let Some(i64_int) = literal.as_int() - && let Some(tuple) = nominal.tuple_spec(db) + && let Some(tuple) = nominal.tuple_spec(db, env) && let Ok(i32_int) = i32::try_from(i64_int) => { - let result = tuple.py_index(db, i32_int).map_err(|_| { + let result = tuple.py_index(db, env, i32_int).map_err(|_| { SubscriptError::new( Type::unknown(), SubscriptErrorKind::IndexOutOfBounds { @@ -655,13 +672,20 @@ impl<'db> Type<'db> { ( Type::NominalInstance(maybe_tuple_nominal), Type::NominalInstance(maybe_slice_nominal), - ) if let Some(tuple) = maybe_tuple_nominal.tuple_spec(db) + ) if let Some(tuple) = maybe_tuple_nominal.tuple_spec(db, env) && let Some(SliceLiteral { start, stop, step }) = maybe_slice_nominal.slice_literal(db) => { - Some(tuple.py_slice_type(db, start, stop, step).map_err(|_| { - SubscriptError::new(Type::unknown(), SubscriptErrorKind::SliceStepSizeZero) - })) + Some( + tuple + .py_slice_type(db, env, start, stop, step) + .map_err(|_| { + SubscriptError::new( + Type::unknown(), + SubscriptErrorKind::SliceStepSizeZero, + ) + }), + ) } // Ex) Given `"value"[1]`, return `"a"` @@ -672,7 +696,7 @@ impl<'db> Type<'db> { { let literal_value = literal_ty.value(db); - let result = match (&mut literal_value.chars()).py_index(db, i32_int) { + let result = match (&mut literal_value.chars()).py_index(db, env, i32_int) { Ok(ch) => Ok(Type::string_literal(db, ch.to_compact_string())), Err(_) => Err(SubscriptError::new( Type::unknown(), @@ -731,7 +755,7 @@ impl<'db> Type<'db> { { let literal_value = literal_ty.value(db); - let result = match literal_value.py_index(db, i32_int) { + let result = match literal_value.py_index(db, env, i32_int) { Ok(byte) => Ok(Type::int_literal((*byte).into())), Err(_) => Err(SubscriptError::new( Type::unknown(), @@ -773,14 +797,14 @@ impl<'db> Type<'db> { if (lhs_literal.is_string() || lhs_literal.is_bytes()) && let Some(bool) = rhs_literal.as_bool() => { - Some(value_ty.subscript(db, Type::int_literal(i64::from(bool)), expr_context)) + Some(value_ty.subscript(db, env, Type::int_literal(i64::from(bool)), expr_context)) } (Type::NominalInstance(nominal), Type::LiteralValue(literal)) if let Some(bool) = literal.as_bool() - && nominal.tuple_spec(db).is_some() => + && nominal.tuple_spec(db, env).is_some() => { - Some(value_ty.subscript(db, Type::int_literal(i64::from(bool)), expr_context)) + Some(value_ty.subscript(db, env, Type::int_literal(i64::from(bool)), expr_context)) } (Type::KnownInstance(KnownInstanceType::SubscriptedProtocol(_)), _) => { @@ -866,16 +890,17 @@ impl<'db> Type<'db> { // See: https://docs.python.org/3/reference/datamodel.html#class-getitem-versus-getitem match value_ty.try_call_dunder( db, + env, "__getitem__", CallArguments::positional([slice_ty]), TypeContext::default(), ) { Ok(outcome) => { - return Ok(outcome.return_type(db)); + return Ok(outcome.return_type(db, env)); } Err(CallDunderError::PossiblyUnbound { bindings, .. }) => { return Err(SubscriptError::new( - bindings.return_type(db), + bindings.return_type(db, env), SubscriptErrorKind::DunderPossiblyUnbound { method: DunderMethod::GetItem, value_ty, @@ -884,7 +909,7 @@ impl<'db> Type<'db> { } Err(CallDunderError::CallError(call_error_kind, bindings, _)) => { return Err(SubscriptError::new( - bindings.return_type(db), + bindings.return_type(db, env), SubscriptErrorKind::DunderCallError { method: DunderMethod::GetItem, value_ty, @@ -908,20 +933,21 @@ impl<'db> Type<'db> { // even if the target version is Python 3.8 or lower, // despite the fact that there will be no corresponding `__class_getitem__` // method in these `sys.version_info` branches. - if value_ty.is_subtype_of(db, KnownClass::Type.to_instance(db)) { + if value_ty.is_subtype_of(db, env, KnownClass::Type.to_instance(db, env)) { let call_arguments = CallArguments::positional([slice_ty]); match value_ty.try_call_dunder_on_class( db, + env, "__class_getitem__", &call_arguments, TypeContext::default(), ) { Ok(bindings) => { - return Ok(bindings.return_type(db)); + return Ok(bindings.return_type(db, env)); } Err(CallDunderError::PossiblyUnbound { bindings, .. }) => { return Err(SubscriptError::new( - bindings.return_type(db), + bindings.return_type(db, env), SubscriptErrorKind::DunderPossiblyUnbound { method: DunderMethod::ClassGetItem, value_ty, @@ -930,7 +956,7 @@ impl<'db> Type<'db> { } Err(CallDunderError::CallError(call_error_kind, bindings, _)) => { return Err(SubscriptError::new( - bindings.return_type(db), + bindings.return_type(db, env), SubscriptErrorKind::DunderCallError { method: DunderMethod::ClassGetItem, value_ty, @@ -947,7 +973,7 @@ impl<'db> Type<'db> { if let Type::ClassLiteral(class) = value_ty { if class.is_known(db, KnownClass::Type) { - return Ok(KnownClass::GenericAlias.to_instance(db)); + return Ok(KnownClass::GenericAlias.to_instance(db, env)); } if class.generic_context(db).is_some() { diff --git a/crates/ty_python_semantic/src/types/tests.rs b/crates/ty_python_semantic/src/types/tests.rs index 7ca4c4726f..7ee494da31 100644 --- a/crates/ty_python_semantic/src/types/tests.rs +++ b/crates/ty_python_semantic/src/types/tests.rs @@ -1,7 +1,10 @@ use super::*; +use crate::Db; +use crate::ProgramEnvironment; use crate::db::tests::{TestDbBuilder, setup_db}; use crate::place::{typing_extensions_symbol, typing_symbol}; use crate::types::type_alias::PEP695TypeAliasType; +use ruff_db::PythonFile; use ruff_db::system::DbWithWritableSystem as _; use ruff_python_ast as ast; use ruff_python_ast::PythonVersion; @@ -18,9 +21,10 @@ fn no_default_type_is_singleton(python_version: PythonVersion) { .build() .unwrap(); - let no_default = KnownClass::NoDefaultType.to_instance(&db); + let env = db.program_environment(); + let no_default = KnownClass::NoDefaultType.to_instance(&db, &env); - assert!(no_default.is_singleton(&db)); + assert!(no_default.is_singleton(&db, &env)); } #[test] @@ -30,21 +34,35 @@ fn typing_vs_typeshed_no_default() { .build() .unwrap(); - let typing_no_default = typing_symbol(&db, "NoDefault").place.expect_type(); - let typing_extensions_no_default = typing_extensions_symbol(&db, "NoDefault") + let typing_no_default = typing_symbol(&db, &db.program_environment(), "NoDefault") .place .expect_type(); + let typing_extensions_no_default = + typing_extensions_symbol(&db, &db.program_environment(), "NoDefault") + .place + .expect_type(); - assert_eq!(typing_no_default.display(&db).to_string(), "NoDefault"); assert_eq!( - typing_extensions_no_default.display(&db).to_string(), + typing_no_default + .display(&db, &db.program_environment()) + .to_string(), + "NoDefault" + ); + assert_eq!( + typing_extensions_no_default + .display(&db, &db.program_environment()) + .to_string(), "NoDefault" ); } -fn list_alias<'db>(db: &'db dyn Db, argument: Type<'db>) -> GenericAlias<'db> { +fn list_alias<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + argument: Type<'db>, +) -> GenericAlias<'db> { KnownClass::List - .to_specialized_class_type(db, &[argument]) + .to_specialized_class_type(db, env, &[argument]) .expect("`list` should accept one type argument") .into_generic_alias() .expect("a specialized `list` should be a generic alias") @@ -56,7 +74,10 @@ fn oscillating_generic_alias_cycle_recover<'db>( previous: &Type<'db>, current: Type<'db>, ) -> Type<'db> { - current.cycle_normalized(db, *previous, cycle) + let env = ProgramEnvironment::from_program( + ty_python_core::program::Program::get(db).python_version(db), + ); + current.cycle_normalized(db, &env, *previous, cycle) } #[salsa::tracked( @@ -65,16 +86,19 @@ fn oscillating_generic_alias_cycle_recover<'db>( cycle_fn=oscillating_generic_alias_cycle_recover, )] fn oscillating_generic_alias(db: &dyn Db) -> Type<'_> { + let env = ProgramEnvironment::from_program( + ty_python_core::program::Program::get(db).python_version(db), + ); let previous = oscillating_generic_alias(db); let argument = if let Type::GenericAlias(alias) = previous && alias.specialization(db).types(db) == [Type::unknown()] { - KnownClass::Int.to_instance(db) + KnownClass::Int.to_instance(db, &env) } else { Type::unknown() }; - list_alias(db, argument).into() + list_alias(db, &env, argument).into() } #[test] @@ -90,14 +114,16 @@ fn generic_alias_cycle_recovery_normalizes_same_origin_unknown_oscillation() { #[test] fn generic_alias_cycle_recovery_rejects_unsafe_merges() { let db = setup_db(); - let int = list_alias(&db, KnownClass::Int.to_instance(&db)); - let str = list_alias(&db, KnownClass::Str.to_instance(&db)); - assert!(str.merge_cycle_recovery(&db, int).is_none()); + let db = &db; + let env = db.program_environment(); + let int = list_alias(db, &env, KnownClass::Int.to_instance(db, &env)); + let str = list_alias(db, &env, KnownClass::Str.to_instance(db, &env)); + assert!(str.merge_cycle_recovery(db, int).is_none()); - let generic_context = int.specialization(&db).generic_context(&db); + let generic_context = int.specialization(db).generic_context(db); let unknown_generic = Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); assert!( - int.merge_cycle_recovery(&db, list_alias(&db, unknown_generic)) + int.merge_cycle_recovery(db, list_alias(db, &env, unknown_generic)) .is_none() ); } @@ -108,15 +134,17 @@ fn generic_alias_cycle_recovery_rejects_unsafe_merges() { #[test] fn todo_types() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let todo1 = todo_type!("1"); let todo2 = todo_type!("2"); - let int = KnownClass::Int.to_instance(&db); + let int = KnownClass::Int.to_instance(db, &env); - assert!(int.is_assignable_to(&db, todo1)); + assert!(int.is_assignable_to(db, &env, todo1)); - assert!(todo1.is_assignable_to(&db, int)); + assert!(todo1.is_assignable_to(db, &env, int)); // We lose information when combining several `Todo` types. This is an // acknowledged limitation of the current implementation. We cannot @@ -128,12 +156,12 @@ fn todo_types() { // salsa, but that would mean we would have to pass in `db` everywhere. // A union of several `Todo` types collapses to a single `Todo` type: - assert!(UnionType::from_elements(&db, [todo1, todo2]).is_todo()); + assert!(UnionType::from_elements(db, &env, [todo1, todo2]).is_todo()); // And similar for intersection types: - assert!(IntersectionType::from_elements(&db, [todo1, todo2]).is_todo()); + assert!(IntersectionType::from_elements(db, &env, [todo1, todo2]).is_todo()); assert!( - IntersectionBuilder::new(&db) + IntersectionBuilder::new(db, &env) .add_positive(todo1) .add_negative(todo2) .build() @@ -144,105 +172,127 @@ fn todo_types() { #[test] fn divergent_type() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let div = Type::divergent(salsa::plumbing::Id::from_bits(1)); assert!(div.is_dynamic()); - assert!(div.has_dynamic(&db)); - let visitor = ApplyTypeMappingVisitor::default(); - let top_div = div.materialize(&db, MaterializationKind::Top, &visitor); - let bottom_div = div.materialize(&db, MaterializationKind::Bottom, &visitor); + assert!(div.has_dynamic(db, &env)); + let visitor = ApplyTypeMappingVisitor::new(&env); + let top_div = div.materialize(db, MaterializationKind::Top, &visitor); + let bottom_div = div.materialize(db, MaterializationKind::Bottom, &visitor); assert!(top_div.is_divergent()); assert!(bottom_div.is_divergent()); assert!(!top_div.is_dynamic()); assert!(!bottom_div.is_dynamic()); - assert!(!top_div.has_dynamic(&db)); - assert!(!bottom_div.has_dynamic(&db)); + assert!(!top_div.has_dynamic(db, &env)); + assert!(!bottom_div.has_dynamic(db, &env)); assert!(top_div.is_object()); assert!(!top_div.is_never()); assert!(!bottom_div.is_object()); assert!(bottom_div.is_never()); - assert_eq!(top_div.negate(&db), bottom_div); - assert_eq!(bottom_div.negate(&db), top_div); - assert_eq!(IntersectionBuilder::new(&db).add_negative(div).build(), div); + assert_eq!(top_div.negate(db, &env), bottom_div); + assert_eq!(bottom_div.negate(db, &env), top_div); assert_eq!( - IntersectionBuilder::new(&db).add_negative(top_div).build(), + IntersectionBuilder::new(db, &env).add_negative(div).build(), + div + ); + assert_eq!( + IntersectionBuilder::new(db, &env) + .add_negative(top_div) + .build(), bottom_div ); assert_eq!( - IntersectionBuilder::new(&db) + IntersectionBuilder::new(db, &env) .add_negative(bottom_div) .build(), top_div ); assert!( KnownClass::Int - .to_instance(&db) - .is_assignable_to(&db, top_div) + .to_instance(db, &env) + .is_assignable_to(db, &env, top_div) ); - assert!(!top_div.is_assignable_to(&db, KnownClass::Int.to_instance(&db))); - assert!(bottom_div.is_assignable_to(&db, KnownClass::Int.to_instance(&db))); + assert!(!top_div.is_assignable_to(db, &env, KnownClass::Int.to_instance(db, &env))); + assert!(bottom_div.is_assignable_to(db, &env, KnownClass::Int.to_instance(db, &env))); assert!( !KnownClass::Int - .to_instance(&db) - .is_assignable_to(&db, bottom_div) + .to_instance(db, &env) + .is_assignable_to(db, &env, bottom_div) ); assert_eq!( - top_div.member(&db, "__str__").place.expect_type(), - Type::object().member(&db, "__str__").place.expect_type() + top_div.member(db, &env, "__str__").place.expect_type(), + Type::object() + .member(db, &env, "__str__") + .place + .expect_type() ); assert_eq!( - top_div.member(&db, "__class__").place.expect_type(), - Type::object().dunder_class(&db) + top_div.member(db, &env, "__class__",).place.expect_type(), + Type::object().dunder_class(db, &env) ); - assert!(top_div.try_upcast_to_callable(&db).is_none()); + assert!(top_div.try_upcast_to_callable(db, &env).is_none()); assert!( top_div - .subscript(&db, Type::int_literal(0), ast::ExprContext::Load) + .subscript(db, &env, Type::int_literal(0), ast::ExprContext::Load,) .is_err() ); - assert_eq!(top_div.recursive_type_normalized_impl(&db, div, true), None); assert_eq!( - bottom_div.recursive_type_normalized_impl(&db, div, true), + top_div.recursive_type_normalized_impl(db, &env, div, true), + None + ); + assert_eq!( + bottom_div.recursive_type_normalized_impl(db, &env, div, true), None ); // The `Divergent` type must not be eliminated in union with other dynamic types, // as this would prevent detection of divergent type inference using `Divergent`. - let union = UnionType::from_elements(&db, [Type::unknown(), div]); - assert_eq!(union.display(&db).to_string(), "Unknown | Divergent"); + let union = UnionType::from_elements(db, &env, [Type::unknown(), div]); + assert_eq!( + union.display(db, &db.program_environment()).to_string(), + "Unknown | Divergent" + ); - let union = UnionType::from_elements(&db, [div, Type::unknown()]); - assert_eq!(union.display(&db).to_string(), "Divergent | Unknown"); + let union = UnionType::from_elements(db, &env, [div, Type::unknown()]); + assert_eq!( + union.display(db, &db.program_environment()).to_string(), + "Divergent | Unknown" + ); - let union = UnionType::from_elements(&db, [div, Type::unknown(), todo_type!("1")]); - assert_eq!(union.display(&db).to_string(), "Divergent | Unknown"); + let union = UnionType::from_elements(db, &env, [div, Type::unknown(), todo_type!("1")]); + assert_eq!( + union.display(db, &db.program_environment()).to_string(), + "Divergent | Unknown" + ); - assert!(div.is_equivalent_to(&db, div)); - assert!(!div.is_equivalent_to(&db, Type::unknown())); - assert!(!Type::unknown().is_equivalent_to(&db, div)); - assert!(!div.is_redundant_with(&db, Type::unknown())); - assert!(!Type::unknown().is_redundant_with(&db, div)); + assert!(div.is_equivalent_to(db, &env, div)); + assert!(!div.is_equivalent_to(db, &env, Type::unknown())); + assert!(!Type::unknown().is_equivalent_to(db, &env, div)); + assert!(!div.is_redundant_with(db, &env, Type::unknown())); + assert!(!Type::unknown().is_redundant_with(db, &env, div)); // `Divergent & T` and `Divergent & ~T` both simplify to `Divergent`, except for the // specific case of `Divergent & Never`, which simplifies to `Never`. - let divergent_intersection = IntersectionBuilder::new(&db) + let divergent_intersection = IntersectionBuilder::new(db, &env) .add_positive(div) .add_positive(todo_type!("2")) .add_negative(todo_type!("3")) .build(); assert_eq!(divergent_intersection, div); - let divergent_intersection = IntersectionBuilder::new(&db) + let divergent_intersection = IntersectionBuilder::new(db, &env) .add_positive(todo_type!("2")) .add_negative(todo_type!("3")) .add_positive(div) .build(); assert_eq!(divergent_intersection, div); - let divergent_never_intersection = IntersectionBuilder::new(&db) + let divergent_never_intersection = IntersectionBuilder::new(db, &env) .add_positive(div) .add_positive(Type::Never) .build(); assert_eq!(divergent_never_intersection, Type::Never); - let divergent_never_intersection = IntersectionBuilder::new(&db) + let divergent_never_intersection = IntersectionBuilder::new(db, &env) .add_positive(Type::Never) .add_positive(div) .build(); @@ -251,66 +301,99 @@ fn divergent_type() { // The `object` type has a good convergence property, that is, its union with all other types is `object`. // (e.g. `object | tuple[Divergent] == object`, `object | tuple[object] == object`) // So we can safely eliminate `Divergent`. - let union = UnionType::from_elements(&db, [div, KnownClass::Object.to_instance(&db)]); - assert_eq!(union.display(&db).to_string(), "object"); + let union = UnionType::from_elements(db, &env, [div, KnownClass::Object.to_instance(db, &env)]); + assert_eq!( + union.display(db, &db.program_environment()).to_string(), + "object" + ); - let union = UnionType::from_elements(&db, [KnownClass::Object.to_instance(&db), div]); - assert_eq!(union.display(&db).to_string(), "object"); + let union = UnionType::from_elements(db, &env, [KnownClass::Object.to_instance(db, &env), div]); + assert_eq!( + union.display(db, &db.program_environment()).to_string(), + "object" + ); let recursive = UnionType::from_elements( - &db, + db, + &env, [ - KnownClass::List.to_specialized_instance(&db, &[div]), - Type::none(&db), + KnownClass::List.to_specialized_instance(db, &env, &[div]), + Type::none(db, &env), ], ); - let nested_rec = KnownClass::List.to_specialized_instance(&db, &[recursive]); + let nested_rec = KnownClass::List.to_specialized_instance(db, &env, &[recursive]); assert_eq!( - nested_rec.display(&db).to_string(), + nested_rec + .display(db, &db.program_environment()) + .to_string(), "list[list[Divergent] | None]" ); let normalized = nested_rec - .recursive_type_normalized_impl(&db, div, false) + .recursive_type_normalized_impl(db, &env, div, false) .unwrap(); - assert_eq!(normalized.display(&db).to_string(), "list[Divergent]"); + assert_eq!( + normalized + .display(db, &db.program_environment()) + .to_string(), + "list[Divergent]" + ); let recursive_tuple = Type::heterogeneous_tuple( - &db, + db, + &env, [ UnionType::from_elements( - &db, + db, + &env, [ - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), Type::heterogeneous_tuple( - &db, + db, + &env, [ - UnionType::from_elements(&db, [KnownClass::Int.to_instance(&db), div]), - KnownClass::Str.to_instance(&db), + UnionType::from_elements( + db, + &env, + [KnownClass::Int.to_instance(db, &env), div], + ), + KnownClass::Str.to_instance(db, &env), ], ), ], ), - KnownClass::Str.to_instance(&db), + KnownClass::Str.to_instance(db, &env), ], ); let normalized = recursive_tuple - .recursive_type_normalized_impl(&db, div, false) + .recursive_type_normalized_impl(db, &env, div, false) .unwrap(); - assert_eq!(normalized.display(&db).to_string(), "tuple[Divergent, str]"); + assert_eq!( + normalized + .display(db, &db.program_environment()) + .to_string(), + "tuple[Divergent, str]" + ); let recursive_dict = KnownClass::Dict.to_specialized_instance( - &db, + db, + &env, &[ - KnownClass::Str.to_instance(&db), + KnownClass::Str.to_instance(db, &env), UnionType::from_elements( - &db, + db, + &env, [ - KnownClass::Int.to_instance(&db), + KnownClass::Int.to_instance(db, &env), KnownClass::Dict.to_specialized_instance( - &db, + db, + &env, &[ - KnownClass::Str.to_instance(&db), - UnionType::from_elements(&db, [KnownClass::Int.to_instance(&db), div]), + KnownClass::Str.to_instance(db, &env), + UnionType::from_elements( + db, + &env, + [KnownClass::Int.to_instance(db, &env), div], + ), ], ), ], @@ -318,27 +401,50 @@ fn divergent_type() { ], ); let normalized = recursive_dict - .recursive_type_normalized_impl(&db, div, false) + .recursive_type_normalized_impl(db, &env, div, false) .unwrap(); - assert_eq!(normalized.display(&db).to_string(), "dict[str, Divergent]"); + assert_eq!( + normalized + .display(db, &db.program_environment()) + .to_string(), + "dict[str, Divergent]" + ); - let union = UnionType::from_elements(&db, [div, KnownClass::Int.to_instance(&db)]); - assert_eq!(union.display(&db).to_string(), "Divergent | int"); + let union = UnionType::from_elements(db, &env, [div, KnownClass::Int.to_instance(db, &env)]); + assert_eq!( + union.display(db, &db.program_environment()).to_string(), + "Divergent | int" + ); for (source, target) in [(div, union), (div, Type::unknown()), (Type::unknown(), div)] { - let when = source.when_constraint_set_assignable_to_owned(&db, target); - assert!(when.query(|_builder, when| when.is_always_satisfied(&db))); + let when = source.when_constraint_set_assignable_to_owned(db, &env, target); + assert!(when.query(|_builder, when| when.is_always_satisfied(db, &env))); } let normalized = union - .recursive_type_normalized_impl(&db, div, false) + .recursive_type_normalized_impl(db, &env, div, false) .unwrap(); - assert_eq!(normalized.display(&db).to_string(), "int"); + assert_eq!( + normalized + .display(db, &db.program_environment()) + .to_string(), + "int" + ); // The same can be said about intersections for the `Never` type. - let intersection = IntersectionType::from_elements(&db, [Type::Never, div]); - assert_eq!(intersection.display(&db).to_string(), "Never"); + let intersection = IntersectionType::from_elements(db, &env, [Type::Never, div]); + assert_eq!( + intersection + .display(db, &db.program_environment()) + .to_string(), + "Never" + ); - let intersection = IntersectionType::from_elements(&db, [div, Type::Never]); - assert_eq!(intersection.display(&db).to_string(), "Never"); + let intersection = IntersectionType::from_elements(db, &env, [div, Type::Never]); + assert_eq!( + intersection + .display(db, &db.program_environment()) + .to_string(), + "Never" + ); } #[test] @@ -348,6 +454,7 @@ fn type_alias_variance() { fn get_type_alias<'db>(db: &'db TestDb, name: &str) -> PEP695TypeAliasType<'db> { let module = ruff_db::files::system_path_to_file(db, "/src/a.py").unwrap(); + let module = PythonFile::new(db, module, db.python_version()); let ty = global_symbol(db, module, name).place.expect_type(); let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( type_alias, @@ -422,124 +529,166 @@ type RecursiveAlias2[T] = None | list[T] | list[RecursiveAlias2[T]] "#, ) .unwrap(); - let covariant = get_type_alias(&db, "CovariantAlias"); + let db = &db; + let env = db.program_environment(); + let covariant = get_type_alias(db, "CovariantAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(covariant)) - .variance_of(&db, get_bound_typevar(&db, covariant)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(covariant)).variance_of( + db, + &env, + get_bound_typevar(db, covariant) + ), TypeVarVariance::Covariant ); - let contravariant = get_type_alias(&db, "ContravariantAlias"); + let contravariant = get_type_alias(db, "ContravariantAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(contravariant)) - .variance_of(&db, get_bound_typevar(&db, contravariant)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(contravariant)).variance_of( + db, + &env, + get_bound_typevar(db, contravariant) + ), TypeVarVariance::Contravariant ); - let invariant = get_type_alias(&db, "InvariantAlias"); + let invariant = get_type_alias(db, "InvariantAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(invariant)) - .variance_of(&db, get_bound_typevar(&db, invariant)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(invariant)).variance_of( + db, + &env, + get_bound_typevar(db, invariant) + ), TypeVarVariance::Invariant ); - let bivariant = get_type_alias(&db, "BivariantAlias"); + let bivariant = get_type_alias(db, "BivariantAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(bivariant)) - .variance_of(&db, get_bound_typevar(&db, bivariant)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(bivariant)).variance_of( + db, + &env, + get_bound_typevar(db, bivariant) + ), TypeVarVariance::Bivariant ); - let covariant_alias = get_type_alias(&db, "CovariantAliasAlias"); + let covariant_alias = get_type_alias(db, "CovariantAliasAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(covariant_alias)) - .variance_of(&db, get_bound_typevar(&db, covariant_alias)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(covariant_alias)).variance_of( + db, + &env, + get_bound_typevar(db, covariant_alias) + ), TypeVarVariance::Covariant ); - let contravariant_alias = get_type_alias(&db, "ContravariantAliasAlias"); + let contravariant_alias = get_type_alias(db, "ContravariantAliasAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(contravariant_alias)) - .variance_of(&db, get_bound_typevar(&db, contravariant_alias)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(contravariant_alias)).variance_of( + db, + &env, + get_bound_typevar(db, contravariant_alias) + ), TypeVarVariance::Contravariant ); - let invariant_alias = get_type_alias(&db, "InvariantAliasAlias"); + let invariant_alias = get_type_alias(db, "InvariantAliasAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(invariant_alias)) - .variance_of(&db, get_bound_typevar(&db, invariant_alias)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(invariant_alias)).variance_of( + db, + &env, + get_bound_typevar(db, invariant_alias) + ), TypeVarVariance::Invariant ); - let bivariant_alias = get_type_alias(&db, "BivariantAliasAlias"); + let bivariant_alias = get_type_alias(db, "BivariantAliasAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(bivariant_alias)) - .variance_of(&db, get_bound_typevar(&db, bivariant_alias)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(bivariant_alias)).variance_of( + db, + &env, + get_bound_typevar(db, bivariant_alias) + ), TypeVarVariance::Bivariant ); - let paramspec_contravariant = get_type_alias(&db, "ParamSpecContravariantAlias"); + let paramspec_contravariant = get_type_alias(db, "ParamSpecContravariantAlias"); assert_eq!( KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_contravariant)) - .variance_of(&db, get_bound_typevar(&db, paramspec_contravariant)), + .variance_of(db, &env, get_bound_typevar(db, paramspec_contravariant)), TypeVarVariance::Contravariant ); - let paramspec_default_contravariant = get_type_alias(&db, "ParamSpecDefaultContravariantAlias"); + let paramspec_default_contravariant = get_type_alias(db, "ParamSpecDefaultContravariantAlias"); assert_eq!( KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_default_contravariant)) - .variance_of(&db, get_bound_typevar(&db, paramspec_default_contravariant)), + .variance_of( + db, + &env, + get_bound_typevar(db, paramspec_default_contravariant) + ), TypeVarVariance::Contravariant ); - let paramspec_concatenate = get_type_alias(&db, "ParamSpecConcatenateAlias"); + let paramspec_concatenate = get_type_alias(db, "ParamSpecConcatenateAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_concatenate)) - .variance_of(&db, get_bound_typevar(&db, paramspec_concatenate)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_concatenate)).variance_of( + db, + &env, + get_bound_typevar(db, paramspec_concatenate) + ), TypeVarVariance::Contravariant ); - let paramspec_bivariant = get_type_alias(&db, "ParamSpecBivariantAlias"); + let paramspec_bivariant = get_type_alias(db, "ParamSpecBivariantAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_bivariant)) - .variance_of(&db, get_bound_typevar(&db, paramspec_bivariant)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(paramspec_bivariant)).variance_of( + db, + &env, + get_bound_typevar(db, paramspec_bivariant) + ), TypeVarVariance::Bivariant ); - let recursive = get_type_alias(&db, "RecursiveAlias"); + let recursive = get_type_alias(db, "RecursiveAlias"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive)) - .variance_of(&db, get_bound_typevar(&db, recursive)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive)).variance_of( + db, + &env, + get_bound_typevar(db, recursive) + ), TypeVarVariance::Bivariant ); - let recursive2 = get_type_alias(&db, "RecursiveAlias2"); + let recursive2 = get_type_alias(db, "RecursiveAlias2"); assert_eq!( - KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive2)) - .variance_of(&db, get_bound_typevar(&db, recursive2)), + KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(recursive2)).variance_of( + db, + &env, + get_bound_typevar(db, recursive2) + ), TypeVarVariance::Invariant ); - assert_effective_variance(&db, covariant, TypeVarVariance::Covariant); - assert_effective_variance(&db, contravariant, TypeVarVariance::Contravariant); - assert_effective_variance(&db, invariant, TypeVarVariance::Invariant); - assert_effective_variance(&db, bivariant, TypeVarVariance::Covariant); - assert_effective_variance(&db, covariant_alias, TypeVarVariance::Covariant); - assert_effective_variance(&db, contravariant_alias, TypeVarVariance::Contravariant); - assert_effective_variance(&db, invariant_alias, TypeVarVariance::Invariant); - assert_effective_variance(&db, bivariant_alias, TypeVarVariance::Covariant); - assert_effective_variance(&db, paramspec_contravariant, TypeVarVariance::Contravariant); + assert_effective_variance(db, covariant, TypeVarVariance::Covariant); + assert_effective_variance(db, contravariant, TypeVarVariance::Contravariant); + assert_effective_variance(db, invariant, TypeVarVariance::Invariant); + assert_effective_variance(db, bivariant, TypeVarVariance::Covariant); + assert_effective_variance(db, covariant_alias, TypeVarVariance::Covariant); + assert_effective_variance(db, contravariant_alias, TypeVarVariance::Contravariant); + assert_effective_variance(db, invariant_alias, TypeVarVariance::Invariant); + assert_effective_variance(db, bivariant_alias, TypeVarVariance::Covariant); + assert_effective_variance(db, paramspec_contravariant, TypeVarVariance::Contravariant); assert_effective_variance( - &db, + db, paramspec_default_contravariant, TypeVarVariance::Contravariant, ); - assert_effective_variance(&db, paramspec_concatenate, TypeVarVariance::Contravariant); - assert_effective_variance(&db, paramspec_bivariant, TypeVarVariance::Covariant); - assert_effective_variance(&db, recursive, TypeVarVariance::Covariant); - assert_effective_variance(&db, recursive2, TypeVarVariance::Invariant); + assert_effective_variance(db, paramspec_concatenate, TypeVarVariance::Contravariant); + assert_effective_variance(db, paramspec_bivariant, TypeVarVariance::Covariant); + assert_effective_variance(db, recursive, TypeVarVariance::Covariant); + assert_effective_variance(db, recursive2, TypeVarVariance::Invariant); - let bivariant_typevar = get_bound_typevar_instance(&db, bivariant); + let bivariant_typevar = get_bound_typevar_instance(db, bivariant); for polarity in [ TypeVarVariance::Covariant, TypeVarVariance::Contravariant, @@ -547,7 +696,7 @@ type RecursiveAlias2[T] = None | list[T] | list[RecursiveAlias2[T]] TypeVarVariance::Bivariant, ] { assert_eq!( - bivariant_typevar.variance_with_polarity(&db, polarity), + bivariant_typevar.variance_with_polarity(db, polarity), polarity ); } @@ -560,6 +709,7 @@ fn eager_expansion() { fn get_type_alias<'db>(db: &'db TestDb, name: &str) -> Type<'db> { let module = ruff_db::files::system_path_to_file(db, "/src/a.py").unwrap(); + let module = PythonFile::new(db, module, db.python_version()); let ty = global_symbol(db, module, name).place.expect_type(); let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( type_alias, @@ -590,43 +740,78 @@ type H[T] = G[T] let int_str = get_type_alias(&db, "IntStr"); assert_eq!( - int_str.expand_eagerly(&db).display(&db).to_string(), + int_str + .expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), "int | str", ); let list_int_str = get_type_alias(&db, "ListIntStr"); assert_eq!( - list_int_str.expand_eagerly(&db).display(&db).to_string(), + list_int_str + .expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), "list[int | str]", ); let rec_list = get_type_alias(&db, "RecursiveList"); assert_eq!( - rec_list.expand_eagerly(&db).display(&db).to_string(), + rec_list + .expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), "list[Divergent]", ); let rec_int_list = get_type_alias(&db, "RecursiveIntList"); assert_eq!( - rec_int_list.expand_eagerly(&db).display(&db).to_string(), + rec_int_list + .expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), "list[Divergent]", ); let itself = get_type_alias(&db, "Itself"); assert_eq!( - itself.expand_eagerly(&db).display(&db).to_string(), + itself + .expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), "Divergent", ); let a = get_type_alias(&db, "A"); - assert_eq!(a.expand_eagerly(&db).display(&db).to_string(), "Divergent",); + assert_eq!( + a.expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), + "Divergent", + ); let b = get_type_alias(&db, "B"); - assert_eq!(b.expand_eagerly(&db).display(&db).to_string(), "Divergent",); + assert_eq!( + b.expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), + "Divergent", + ); let g = get_type_alias(&db, "G"); - assert_eq!(g.expand_eagerly(&db).display(&db).to_string(), "Divergent",); + assert_eq!( + g.expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), + "Divergent", + ); let h = get_type_alias(&db, "H"); - assert_eq!(h.expand_eagerly(&db).display(&db).to_string(), "Divergent",); + assert_eq!( + h.expand_eagerly(&db, &db.program_environment()) + .display(&db, &db.program_environment()) + .to_string(), + "Divergent", + ); } diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 33fec18984..e3ec0ba9d8 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -16,6 +16,7 @@ //! that adds that "collapse `Never`" behavior, whereas [`TupleSpec`] allows you to add any element //! types, including `Never`.) +use crate::{Program, ProgramEnvironment}; use std::cmp::Ordering; use std::hash::Hash; use std::num::{NonZeroI32, NonZeroUsize}; @@ -34,7 +35,7 @@ use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, ErrorContext, FindLegacyTypeVarsVisitor, IntersectionType, Type, TypeContext, TypeMapping, UnionBuilder, UnionType, }; -use crate::{Db, FxOrderSet, Program}; +use crate::{Db, FxOrderSet}; use ty_python_core::Truthiness; use ty_python_core::definition::Definition; @@ -129,6 +130,9 @@ impl TupleLength { #[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] pub struct TupleType<'db> { + #[returns(copy)] + pub(crate) program: Program, + #[returns(ref)] pub(crate) tuple: TupleSpec<'db>, } @@ -149,7 +153,9 @@ pub(super) fn walk_tuple_type<'db, V: super::visitor::TypeVisitor<'db> + ?Sized> visitor.visit_type(db, element); } match tuple.variable() { - VariableSegment::Homogeneous(element) => visitor.visit_type(db, element), + VariableSegment::Homogeneous(element) => { + visitor.visit_type(db, element); + } VariableSegment::TypeVarTuple(typevartuple) => { visitor.visit_type(db, Type::TypeVar(typevartuple)); } @@ -166,7 +172,11 @@ impl get_size2::GetSize for TupleType<'_> {} #[salsa::tracked] impl<'db> TupleType<'db> { - pub(crate) fn new(db: &'db dyn Db, spec: &TupleSpec<'db>) -> Option { + pub(crate) fn new( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + spec: &TupleSpec<'db>, + ) -> Option { // If a fixed-length (i.e., mandatory) element of the tuple is `Never`, then it's not // possible to instantiate the tuple as a whole. if spec.fixed_elements().any(Type::is_never) { @@ -183,56 +193,79 @@ impl<'db> TupleType<'db> { .iter_prefix_elements() .chain(tuple.iter_suffix_elements()), )); - return Some(TupleType::new_internal::<_, TupleSpec<'db>>(db, tuple)); + return Some(TupleType::new_internal(db, env.program(db), tuple)); } - Some(TupleType::new_internal(db, spec)) + Some(TupleType::new_internal(db, env.program(db), spec)) } - pub(crate) fn empty(db: &'db dyn Db) -> Self { - TupleType::new_internal(db, TupleSpec::from(FixedLengthTuple::empty())) + pub(crate) fn empty(db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Self { + TupleType::new_internal( + db, + env.program(db), + TupleSpec::from(FixedLengthTuple::empty()), + ) } pub(crate) fn heterogeneous( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, types: impl IntoIterator>, ) -> Option { - TupleType::new(db, &TupleSpec::heterogeneous(types)) + TupleType::new(db, env, &TupleSpec::heterogeneous(types)) } pub(crate) fn mixed( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, prefix: impl IntoIterator>, variable: Type<'db>, suffix: impl IntoIterator>, ) -> Option { - Self::mixed_with_segment(db, prefix, VariableSegment::Homogeneous(variable), suffix) + Self::mixed_with_segment( + db, + env, + prefix, + VariableSegment::Homogeneous(variable), + suffix, + ) } pub(crate) fn mixed_with_segment( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, prefix: impl IntoIterator>, variable: VariableSegment<'db>, suffix: impl IntoIterator>, ) -> Option { - TupleType::new(db, &VariableLengthTuple::mixed(prefix, variable, suffix)) + TupleType::new( + db, + env, + &VariableLengthTuple::mixed(prefix, variable, suffix), + ) } - pub(crate) fn homogeneous(db: &'db dyn Db, element: Type<'db>) -> Self { + pub(crate) fn homogeneous( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + element: Type<'db>, + ) -> Self { match element { - Type::Never => TupleType::empty(db), - _ => TupleType::new_internal(db, TupleSpec::homogeneous(element)), + Type::Never => TupleType::empty(db, env), + _ => TupleType::new_internal(db, env.program(db), TupleSpec::homogeneous(element)), } } /// Packs a `TypeVarTuple` into the tuple value used for generic specialization relations. pub(crate) fn unpacked_typevartuple( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar: BoundTypeVarInstance<'db>, ) -> Self { debug_assert!(typevar.is_typevartuple(db)); TupleType::new_internal( db, + env.program(db), VariableLengthTuple::mixed([], VariableSegment::TypeVarTuple(typevar), []), ) } @@ -242,13 +275,14 @@ impl<'db> TupleType<'db> { // from `NominalInstanceType::class()`, which is a very hot method. #[salsa::tracked(returns(copy), cycle_initial=to_class_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn to_class_type(self, db: &'db dyn Db) -> ClassType<'db> { + let env = &ProgramEnvironment::from_program(self.program(db)); let tuple_class = KnownClass::Tuple - .try_to_class_literal(db) + .try_to_class_literal(db, env) .expect("Typeshed should always have a `tuple` class in `builtins.pyi`"); tuple_class.apply_specialization(db, |generic_context| { if generic_context.variables(db).len() == 1 { - let element_type = self.tuple(db).tuple_class_type(db); + let element_type = self.tuple(db).tuple_class_type(db, env); generic_context.specialize_tuple(db, element_type, self) } else { generic_context.default_specialization(db, Some(KnownClass::Tuple)) @@ -259,13 +293,15 @@ impl<'db> TupleType<'db> { pub(super) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { Some(Self::new_internal( db, + env.program(db), self.tuple(db) - .recursive_type_normalized_impl(db, div, nested)?, + .recursive_type_normalized_impl(db, env, div, nested)?, )) } @@ -274,10 +310,11 @@ impl<'db> TupleType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Option { TupleType::new( db, + visitor.env, &self .tuple(db) .apply_type_mapping_impl(db, type_mapping, tcx, visitor), @@ -287,12 +324,13 @@ impl<'db> TupleType<'db> { pub(crate) fn find_legacy_typevars_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { self.tuple(db) - .find_legacy_typevars_impl(db, binding_context, typevars, visitor); + .find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } @@ -349,7 +387,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { |(&source, &target)| { let constraint_set = self.check_type_pair(db, source, target); if let Some(context) = self.report_context() - && constraint_set.is_never_satisfied(db) + && constraint_set.is_never_satisfied(db, self.env) { context.push(ErrorContext::TupleElementNotCompatible { source, @@ -400,7 +438,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { match target.variable() { VariableSegment::TypeVarTuple(typevartuple) => { - let packed = Type::heterogeneous_tuple(db, source_iter.copied()); + let packed = Type::heterogeneous_tuple(db, self.env, source_iter.copied()); result.and(db, self.constraints, || { self.check_type_pair(db, packed, Type::TypeVar(typevartuple)) }) @@ -446,12 +484,14 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return self.never(); } + let env = self.env; + // In addition, the other tuple must have enough elements to match up with this // tuple's prefix and suffix, and each of those elements must pairwise satisfy the // relation. let mut result = self.always(); let mut target_iter = target.iter_all_elements(); - for source_ty in source.prenormalized_prefix_elements(db, None) { + for source_ty in source.prenormalized_prefix_elements(db, env, None) { let Some(target_ty) = target_iter.next() else { return self.never(); }; @@ -463,7 +503,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { return result; } } - let suffix: Vec<_> = source.prenormalized_suffix_elements(db, None).collect(); + let suffix: Vec<_> = source + .prenormalized_suffix_elements(db, env, None) + .collect(); for &source_ty in suffix.iter().rev() { let Some(target_ty) = target_iter.next_back() else { return self.never(); @@ -508,6 +550,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }); } + let env = self.env; + if self.typevar_evaluation == TypeVarEvaluation::Lazy && let VariableSegment::TypeVarTuple(typevartuple) = target.variable() { @@ -536,6 +580,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let packed = Type::tuple(TupleType::new( db, + env, &VariableLengthTuple::mixed( source_prefix[target_prefix.len()..].iter().copied(), source.variable(), @@ -569,10 +614,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // variable-length part. let mut result = self.always(); let pairwise = source - .prenormalized_prefix_elements(db, source_prenormalize_variable) - .zip_longest( - target.prenormalized_prefix_elements(db, target_prenormalize_variable), - ); + .prenormalized_prefix_elements(db, env, source_prenormalize_variable) + .zip_longest(target.prenormalized_prefix_elements( + db, + env, + target_prenormalize_variable, + )); for pair in pairwise { let pair_constraints = match pair { EitherOrBoth::Both(self_ty, other_ty) => { @@ -601,10 +648,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } let source_suffix: Vec<_> = source - .prenormalized_suffix_elements(db, source_prenormalize_variable) + .prenormalized_suffix_elements(db, env, source_prenormalize_variable) .collect(); let target_suffix: Vec<_> = target - .prenormalized_suffix_elements(db, target_prenormalize_variable) + .prenormalized_suffix_elements(db, env, target_prenormalize_variable) .collect(); let pairwise = source_suffix .iter() @@ -720,8 +767,9 @@ fn to_class_type_cycle_initial<'db>( id: salsa::Id, self_: TupleType<'db>, ) -> ClassType<'db> { + let env = &ProgramEnvironment::from_program(self_.program(db)); let tuple_class = KnownClass::Tuple - .try_to_class_literal(db) + .try_to_class_literal(db, env) .expect("Typeshed should always have a `tuple` class in `builtins.pyi`"); tuple_class.apply_specialization(db, |generic_context| { @@ -832,6 +880,7 @@ impl<'db> FixedLengthTuple> { fn resize( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, new_length: TupleLength, ) -> Result, ResizeTupleError> { match new_length { @@ -851,8 +900,11 @@ impl<'db> FixedLengthTuple> { // suffix. let mut elements = self.iter_all_elements(); let prefix: Vec<_> = elements.by_ref().take(prefix).collect(); - let variable = - UnionType::from_elements_leave_aliases(db, elements.by_ref().take(variable)); + let variable = UnionType::from_elements_leave_aliases( + db, + env, + elements.by_ref().take(variable), + ); let suffix = elements.by_ref().take(suffix); Ok(VariableLengthTuple::mixed( prefix, @@ -866,6 +918,7 @@ impl<'db> FixedLengthTuple> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -873,7 +926,7 @@ impl<'db> FixedLengthTuple> { Some(Self::from_elements( self.0 .iter() - .map(|ty| ty.recursive_type_normalized_impl(db, div, true)) + .map(|ty| ty.recursive_type_normalized_impl(db, env, div, true)) .collect::>>()?, )) } else { @@ -881,7 +934,7 @@ impl<'db> FixedLengthTuple> { self.0 .iter() .map(|ty| { - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }) .collect::>(), @@ -894,16 +947,18 @@ impl<'db> FixedLengthTuple> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let tcx_tuple = tcx .annotation - .and_then(|annotation| annotation.known_specialization(db, KnownClass::Tuple)) + .and_then(|annotation| { + annotation.known_specialization(db, visitor.env, KnownClass::Tuple) + }) .and_then(|specialization| { specialization .tuple(db) .expect("the specialization of `KnownClass::Tuple` must have a tuple spec") - .resize(db, TupleLength::Fixed(self.0.len())) + .resize(db, visitor.env, TupleLength::Fixed(self.0.len())) .ok() }); @@ -927,12 +982,13 @@ impl<'db> FixedLengthTuple> { fn find_legacy_typevars_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { for ty in &self.0 { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } } @@ -940,8 +996,13 @@ impl<'db> FixedLengthTuple> { impl<'db> PyIndex<'db> for &FixedLengthTuple> { type Item = Type<'db>; - fn py_index(self, db: &'db dyn Db, index: i32) -> Result { - self.0.py_index(db, index).copied() + fn py_index( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + index: i32, + ) -> Result { + self.0.py_index(db, env, index).copied() } } @@ -1300,10 +1361,12 @@ impl VariableSlice { fn ty<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, tuple: &VariableLengthTuple, VariableSegment<'db>>, ) -> Type<'db> { UnionType::from_elements_leave_aliases( db, + env, matches!( self.kind, VariableSliceKind::ElementType | VariableSliceKind::Preserved @@ -1324,15 +1387,16 @@ impl VariableTupleSlicePlan { fn into_type<'db>( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, tuple: &VariableLengthTuple, VariableSegment<'db>>, ) -> Type<'db> { match self { VariableTupleSlicePlan::Empty => { - Type::heterogeneous_tuple(db, std::iter::empty::>()) + Type::heterogeneous_tuple(db, env, std::iter::empty::>()) } VariableTupleSlicePlan::Fixed(fixed) => { - Type::heterogeneous_tuple(db, tuple.slice_fixed_position(db, fixed)) + Type::heterogeneous_tuple(db, env, tuple.slice_fixed_position(db, env, fixed)) } VariableTupleSlicePlan::Mixed { @@ -1343,11 +1407,12 @@ impl VariableTupleSlicePlan { let variable_segment = match variable.kind { VariableSliceKind::Preserved => tuple.variable(), VariableSliceKind::Excluded | VariableSliceKind::ElementType => { - VariableSegment::Homogeneous(variable.ty(db, tuple)) + VariableSegment::Homogeneous(variable.ty(db, env, tuple)) } }; Type::tuple(TupleType::new( db, + env, &VariableLengthTuple::mixed( VariableLengthTuple::optional_fixed_slice( tuple.prefix_elements(), @@ -1362,7 +1427,7 @@ impl VariableTupleSlicePlan { )) } - VariableTupleSlicePlan::Homogeneous => tuple.homogeneous_type(db), + VariableTupleSlicePlan::Homogeneous => tuple.homogeneous_type(db, env), } } } @@ -1441,6 +1506,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn slice_fixed_position<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, slice: FixedPositionSlice, ) -> impl Iterator> + 'a where @@ -1454,10 +1520,10 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { } = slice; match origin { FixedPositionOrigin::Front => { - Either::Left(self.slice_front_forward(db, start, exclusive_stop, step)) + Either::Left(self.slice_front_forward(db, env, start, exclusive_stop, step)) } FixedPositionOrigin::Back => { - Either::Right(self.slice_back(db, start, exclusive_stop, step)) + Either::Right(self.slice_back(db, env, start, exclusive_stop, step)) } } } @@ -1465,6 +1531,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn slice_front_forward<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, start: usize, exclusive_stop: usize, step: NonZeroUsize, @@ -1475,17 +1542,19 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { (start..exclusive_stop) .step_by(step.get()) .map(move |index| { - self.type_at_nonnegative_index(db, index).unwrap_or_else(|| { + self.type_at_nonnegative_index(db, env, index) + .unwrap_or_else(|| { unreachable!( "front-origin fixed slice positions are validated during plan construction" ) - }) + }) }) } fn slice_back<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, start: usize, exclusive_stop: usize, step: NonZeroUsize, @@ -1502,7 +1571,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { } let element = self - .type_at_negative_distance(db, distance) + .type_at_negative_distance(db, env, distance) .unwrap_or_else(|| { unreachable!( "back-origin fixed slice positions are validated during plan construction" @@ -1538,6 +1607,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn py_slice_type( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, start: Option, stop: Option, step: Option, @@ -1553,7 +1623,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { Ok(match direction { TupleSliceDirection::Forward => self .forward_slice_plan(start, stop, step) - .into_type(db, self), + .into_type(db, env, self), TupleSliceDirection::Backward => { let reversed = self.reversed(db); reversed @@ -1562,7 +1632,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { TupleSliceDirection::reverse_bound(stop), step, ) - .into_type(db, &reversed) + .into_type(db, env, &reversed) } }) } @@ -1870,20 +1940,36 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { start + ((stop - start - 1) / step) * step } - fn type_at_nonnegative_index(&self, db: &'db dyn Db, index: usize) -> Option> { - (index < self.len().minimum()).then(|| self.type_at_nonnegative_index_unbounded(db, index)) + fn type_at_nonnegative_index( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + index: usize, + ) -> Option> { + (index < self.len().minimum()) + .then(|| self.type_at_nonnegative_index_unbounded(db, env, index)) } - fn type_at_nonnegative_index_unbounded(&self, db: &'db dyn Db, index: usize) -> Type<'db> { + fn type_at_nonnegative_index_unbounded( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + index: usize, + ) -> Type<'db> { if let Some(element) = self.prefix_elements().get(index) { *element } else { let suffix_stop = index - self.prefix_len() + 1; - self.variable_and_suffix_type(db, Some(suffix_stop)) + self.variable_and_suffix_type(db, env, Some(suffix_stop)) } } - fn type_at_negative_distance(&self, db: &'db dyn Db, distance: usize) -> Option> { + fn type_at_negative_distance( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + distance: usize, + ) -> Option> { if distance == 0 || distance > self.len().minimum() { return None; } @@ -1898,6 +1984,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { let prefix_and_variable_len = distance - self.suffix_len(); Some(UnionType::from_elements_leave_aliases( db, + env, self.iter_prefix_elements() .skip(self.prefix_len() - prefix_and_variable_len) .chain(std::iter::once(self.variable().element_type(db))), @@ -1913,14 +2000,20 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { .chain(self.iter_suffix_elements()) } - fn homogeneous_type(&self, db: &'db dyn Db) -> Type<'db> { - let element = UnionType::from_elements_leave_aliases(db, self.iter_all_elements(db)); - Type::homogeneous_tuple(db, element) + fn homogeneous_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + let element = UnionType::from_elements_leave_aliases(db, env, self.iter_all_elements(db)); + Type::homogeneous_tuple(db, env, element) } - fn variable_and_suffix_type(&self, db: &'db dyn Db, suffix_stop: Option) -> Type<'db> { + fn variable_and_suffix_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + suffix_stop: Option, + ) -> Type<'db> { UnionType::from_elements_leave_aliases( db, + env, std::iter::once(self.variable().element_type(db)).chain( self.iter_suffix_elements() .take(suffix_stop.unwrap_or_else(|| self.suffix_len())), @@ -1950,12 +2043,13 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn prenormalized_prefix_elements<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, variable: Option>, ) -> impl Iterator> + 'a { let variable = variable.unwrap_or_else(|| self.variable().element_type(db)); self.iter_prefix_elements().chain( self.iter_suffix_elements() - .take_while(move |element| element.is_equivalent_to(db, variable)), + .take_while(move |element| element.is_equivalent_to(db, env, variable)), ) } @@ -1981,16 +2075,18 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn prenormalized_suffix_elements<'a>( &'a self, db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, variable: Option>, ) -> impl Iterator> + 'a { let variable = variable.unwrap_or_else(|| self.variable().element_type(db)); self.iter_suffix_elements() - .skip_while(move |element| element.is_equivalent_to(db, variable)) + .skip_while(move |element| element.is_equivalent_to(db, env, variable)) } fn resize( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, new_length: TupleLength, ) -> Result, ResizeTupleError> { match new_length { @@ -2026,6 +2122,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { // `I2` (variable empty, suffix shifts left), so it should be `I1 | I2`. let variable = UnionType::from_elements_leave_aliases( db, + env, self.iter_prefix_elements() .skip(prefix_length) .chain(std::iter::once(self.variable().element_type(db))) @@ -2047,6 +2144,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -2054,11 +2152,11 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { let prefix = self .prefix_elements() .iter() - .map(|ty| ty.recursive_type_normalized_impl(db, div, true)); + .map(|ty| ty.recursive_type_normalized_impl(db, env, div, true)); let variable_segment = match self.variable() { VariableSegment::Homogeneous(variable) => VariableSegment::Homogeneous( - variable.recursive_type_normalized_impl(db, div, true)?, + variable.recursive_type_normalized_impl(db, env, div, true)?, ), VariableSegment::TypeVarTuple(typevartuple) => { VariableSegment::TypeVarTuple(typevartuple) @@ -2068,19 +2166,19 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { let suffix = self .suffix_elements() .iter() - .map(|ty| ty.recursive_type_normalized_impl(db, div, true)); + .map(|ty| ty.recursive_type_normalized_impl(db, env, div, true)); Self::try_new(prefix, variable_segment, suffix) } else { let prefix = self.prefix_elements().iter().map(|ty| { - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }); let variable_segment = match self.variable() { VariableSegment::Homogeneous(variable) => VariableSegment::Homogeneous( variable - .recursive_type_normalized_impl(db, div, true) + .recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div), ), VariableSegment::TypeVarTuple(typevartuple) => { @@ -2089,7 +2187,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { }; let suffix = self.suffix_elements().iter().map(|ty| { - ty.recursive_type_normalized_impl(db, div, true) + ty.recursive_type_normalized_impl(db, env, div, true) .unwrap_or(div) }); @@ -2102,7 +2200,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> TupleSpec<'db> { let prefix = self .prefix_elements() @@ -2152,7 +2250,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { for element in prefix { builder.push(element); } - builder = builder.concat(db, &mapped_tuple); + builder = builder.concat(db, visitor.env, &mapped_tuple); for element in suffix { builder.push(element); } @@ -2167,20 +2265,22 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { fn find_legacy_typevars_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { for ty in self.prefix_elements() { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } match self.variable() { VariableSegment::Homogeneous(variable) => { - variable.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + variable.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } VariableSegment::TypeVarTuple(typevartuple) => { Type::TypeVar(typevartuple).find_legacy_typevars_impl( db, + env, binding_context, typevars, visitor, @@ -2188,7 +2288,7 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { } } for ty in self.suffix_elements() { - ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + ty.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } } @@ -2196,9 +2296,14 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { impl<'db> PyIndex<'db> for &VariableLengthTuple, VariableSegment<'db>> { type Item = Type<'db>; - fn py_index(self, db: &'db dyn Db, index: i32) -> Result { + fn py_index( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + index: i32, + ) -> Result { match Nth::from_index(index) { - Nth::FromStart(index) => Ok(self.type_at_nonnegative_index_unbounded(db, index)), + Nth::FromStart(index) => Ok(self.type_at_nonnegative_index_unbounded(db, env, index)), Nth::FromEnd(index_from_end) => { if index_from_end < self.suffix_elements().len() { @@ -2214,6 +2319,7 @@ impl<'db> PyIndex<'db> for &VariableLengthTuple, VariableSegment<'db>> let index_past_suffix = index_from_end - self.suffix_elements().len() + 1; Ok(UnionType::from_elements_leave_aliases( db, + env, (self.prefix_elements().iter().rev().copied()) .take(index_past_suffix) .rev() @@ -2295,24 +2401,29 @@ impl<'db> Tuple, VariableSegment<'db>> { )) } - pub(crate) fn homogeneous_element_type(&self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn homogeneous_element_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { match self { Tuple::Fixed(tuple) => { - UnionType::from_elements_leave_aliases(db, tuple.iter_all_elements()) + UnionType::from_elements_leave_aliases(db, env, tuple.iter_all_elements()) } Tuple::Variable(tuple) => { - UnionType::from_elements_leave_aliases(db, tuple.iter_all_elements(db)) + UnionType::from_elements_leave_aliases(db, env, tuple.iter_all_elements(db)) } } } - fn tuple_class_type(&self, db: &'db dyn Db) -> Type<'db> { + fn tuple_class_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { Tuple::Fixed(tuple) => { - UnionType::from_elements_leave_aliases(db, tuple.iter_all_elements()) + UnionType::from_elements_leave_aliases(db, env, tuple.iter_all_elements()) } Tuple::Variable(tuple) => UnionType::from_elements_leave_aliases( db, + env, tuple .iter_prefix_elements() .chain(std::iter::once(tuple.variable().tuple_class_type())) @@ -2345,6 +2456,7 @@ impl<'db> Tuple, VariableSegment<'db>> { pub(crate) fn py_slice_type( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, start: Option, stop: Option, step: Option, @@ -2352,9 +2464,10 @@ impl<'db> Tuple, VariableSegment<'db>> { match self { Tuple::Fixed(tuple) => Ok(Type::heterogeneous_tuple( db, + env, tuple.py_slice(db, start, stop, step)?, )), - Tuple::Variable(tuple) => tuple.py_slice_type(db, start, stop, step), + Tuple::Variable(tuple) => tuple.py_slice_type(db, env, start, stop, step), } } @@ -2364,26 +2477,28 @@ impl<'db> Tuple, VariableSegment<'db>> { pub(crate) fn resize( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, new_length: TupleLength, ) -> Result { match self { - Tuple::Fixed(tuple) => tuple.resize(db, new_length), - Tuple::Variable(tuple) => tuple.resize(db, new_length), + Tuple::Fixed(tuple) => tuple.resize(db, env, new_length), + Tuple::Variable(tuple) => tuple.resize(db, env, new_length), } } fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { match self { Tuple::Fixed(tuple) => Some(Tuple::Fixed( - tuple.recursive_type_normalized_impl(db, div, nested)?, + tuple.recursive_type_normalized_impl(db, env, div, nested)?, )), Tuple::Variable(tuple) => Some(Tuple::Variable( - tuple.recursive_type_normalized_impl(db, div, nested)?, + tuple.recursive_type_normalized_impl(db, env, div, nested)?, )), } } @@ -2393,7 +2508,7 @@ impl<'db> Tuple, VariableSegment<'db>> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { Tuple::Fixed(tuple) => { @@ -2406,16 +2521,17 @@ impl<'db> Tuple, VariableSegment<'db>> { fn find_legacy_typevars_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, binding_context: Option>, typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { match self { Tuple::Fixed(tuple) => { - tuple.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + tuple.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } Tuple::Variable(tuple) => { - tuple.find_legacy_typevars_impl(db, binding_context, typevars, visitor); + tuple.find_legacy_typevars_impl(db, env, binding_context, typevars, visitor); } } } @@ -2571,9 +2687,12 @@ impl<'db> Tuple, VariableSegment<'db>> { } /// Return the `TupleSpec` for the singleton `sys.version_info` - pub(crate) fn version_info_spec(db: &'db dyn Db) -> TupleSpec<'db> { - let python_version = Program::get(db).python_version(db); - let int_instance_ty = KnownClass::Int.to_instance(db); + pub(crate) fn version_info_spec( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> TupleSpec<'db> { + let python_version = env.python_version(db); + let int_instance_ty = KnownClass::Int.to_instance(db, env); // TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there) let release_level_ty = { @@ -2614,10 +2733,15 @@ impl From> for Tuple { impl<'db> PyIndex<'db> for &TupleSpec<'db> { type Item = Type<'db>; - fn py_index(self, db: &'db dyn Db, index: i32) -> Result { + fn py_index( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + index: i32, + ) -> Result { match self { - Tuple::Fixed(tuple) => tuple.py_index(db, index), - Tuple::Variable(tuple) => tuple.py_index(db, index), + Tuple::Fixed(tuple) => tuple.py_index(db, env, index), + Tuple::Variable(tuple) => tuple.py_index(db, env, index), } } } @@ -2638,23 +2762,29 @@ enum TupleElement { /// assigned to the starred target in `list`. pub(crate) struct TupleUnpacker<'db> { db: &'db dyn Db, + env: ProgramEnvironment<'db>, targets: Tuple>, } impl<'db> TupleUnpacker<'db> { - pub(crate) fn new(db: &'db dyn Db, len: TupleLength) -> Self { - let new_builders = |len: usize| std::iter::repeat_with(|| UnionBuilder::new(db)).take(len); + pub(crate) fn new(db: &'db dyn Db, env: &ProgramEnvironment<'db>, len: TupleLength) -> Self { + let new_builders = + |len: usize| std::iter::repeat_with(|| UnionBuilder::new(db, env)).take(len); let targets = match len { TupleLength::Fixed(len) => { Tuple::Fixed(FixedLengthTuple::from_elements(new_builders(len))) } TupleLength::Variable(prefix, suffix) => VariableLengthTuple::mixed( new_builders(prefix), - UnionBuilder::new(db), + UnionBuilder::new(db, env), new_builders(suffix), ), }; - Self { db, targets } + Self { + db, + env: env.clone(), + targets, + } } /// Unpacks a single rhs tuple into the target tuple that we are building. If you want to @@ -2665,13 +2795,14 @@ impl<'db> TupleUnpacker<'db> { /// side is variable-length, we will pull multiple values out of the rhs variable-length /// portion, and assign multiple values to the starred target, as needed. pub(crate) fn unpack_tuple(&mut self, values: &TupleSpec<'db>) -> Result<(), ResizeTupleError> { - let values = values.resize(self.db, self.targets.len())?; + let db = self.db; + let values = values.resize(db, &self.env, self.targets.len())?; match (&mut self.targets, &values) { (Tuple::Fixed(targets), Tuple::Fixed(values)) => { targets.unpack_tuple(values); } (Tuple::Variable(targets), Tuple::Variable(values)) => { - targets.unpack_tuple(self.db, values); + targets.unpack_tuple(db, &self.env, values); } _ => panic!("should have ensured that tuples are the same length"), } @@ -2683,11 +2814,12 @@ impl<'db> TupleUnpacker<'db> { /// union of the type unpacked into that target from each of the rhs tuples. If there is a /// starred target, we will each unpacked type in `list`. pub(crate) fn into_types(self) -> impl Iterator> { - self.targets + let Self { db, env, targets } = self; + targets .into_all_elements_with_kind() - .map(|builder| match builder { + .map(move |builder| match builder { TupleElement::Variable(builder) => builder.try_build().unwrap_or_else(|| { - KnownClass::List.to_specialized_instance(self.db, &[Type::unknown()]) + KnownClass::List.to_specialized_instance(db, &env, &[Type::unknown()]) }), TupleElement::Fixed(builder) | TupleElement::Prefix(builder) @@ -2711,6 +2843,7 @@ impl<'db> VariableLengthTuple> { fn unpack_tuple( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, values: &VariableLengthTuple, VariableSegment<'db>>, ) { // We have already verified above that the two tuples have the same length. @@ -2719,9 +2852,12 @@ impl<'db> VariableLengthTuple> { { target.add_in_place(value); } - self.variable_element_mut().add_in_place( - KnownClass::List.to_specialized_instance(db, &[values.variable().element_type(db)]), - ); + self.variable_element_mut() + .add_in_place(KnownClass::List.to_specialized_instance( + db, + env, + &[values.variable().element_type(db)], + )); for (target, value) in (self.suffix_elements_mut().iter_mut()).zip(values.iter_suffix_elements()) { @@ -2763,15 +2899,21 @@ impl<'db> TupleSpecBuilder<'db> { pub(crate) fn concat_variadic_typevar( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar: BoundTypeVarInstance<'db>, ) -> Self { debug_assert!(typevar.is_typevartuple(db)); let other = VariableLengthTuple::mixed([], VariableSegment::TypeVarTuple(typevar), []); - self.concat(db, &other) + self.concat(db, env, &other) } /// Concatenates another tuple to the end of this tuple, returning a new tuple. - pub(crate) fn concat(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self { + pub(crate) fn concat( + mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: &TupleSpec<'db>, + ) -> Self { match (&mut self, other) { (TupleSpecBuilder::Fixed(left_tuple), TupleSpec::Fixed(right_tuple)) => { left_tuple.extend_from_slice(&right_tuple.0); @@ -2809,6 +2951,7 @@ impl<'db> TupleSpecBuilder<'db> { ) => { let variable = UnionType::from_elements_leave_aliases( db, + env, left_suffix .iter() .copied() @@ -2854,13 +2997,18 @@ impl<'db> TupleSpecBuilder<'db> { /// `tuple[int, str, bytes]`, the result will be a tuple-spec builder for /// `tuple[int | str | bytes, ...]`. We could consider improving this in the future if real-world /// use cases arise. - pub(crate) fn union(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self { + pub(crate) fn union( + mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: &TupleSpec<'db>, + ) -> Self { match (&mut self, other) { (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Fixed(new_elements)) if our_elements.len() == new_elements.len() => { for (existing, new) in our_elements.iter_mut().zip(new_elements.all_elements()) { - *existing = UnionType::from_elements_leave_aliases(db, [*existing, *new]); + *existing = UnionType::from_elements_leave_aliases(db, env, [*existing, *new]); } self } @@ -2875,6 +3023,7 @@ impl<'db> TupleSpecBuilder<'db> { _ => { let unioned = UnionType::from_elements_leave_aliases( db, + env, self.iter_element_types(db) .chain(other.iter_element_types(db)), ); @@ -2894,14 +3043,19 @@ impl<'db> TupleSpecBuilder<'db> { /// For example, if `self` is a tuple-spec builder for `tuple[int, str]` and `other` is a /// tuple-spec for `tuple[object, object]`, the result will be a tuple-spec builder for /// `tuple[int, str]` (since `int & object` simplifies to `int`, and `str & object` to `str`). - pub(crate) fn intersect(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Option { + pub(crate) fn intersect( + mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: &TupleSpec<'db>, + ) -> Option { match (&mut self, other) { // Both fixed-length with the same length: element-wise intersection. (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Fixed(new_elements)) if our_elements.len() == new_elements.len() => { for (existing, new) in our_elements.iter_mut().zip(new_elements.all_elements()) { - *existing = IntersectionType::from_elements(db, [*existing, *new]); + *existing = IntersectionType::from_elements(db, env, [*existing, *new]); } Some(self) } @@ -2910,16 +3064,16 @@ impl<'db> TupleSpecBuilder<'db> { (TupleSpecBuilder::Fixed(_), TupleSpec::Fixed(_)) => None, (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Variable(var)) => var - .resize(db, TupleLength::Fixed(our_elements.len())) + .resize(db, env, TupleLength::Fixed(our_elements.len())) .ok() - .and_then(|tuple| self.intersect(db, &tuple)), + .and_then(|tuple| self.intersect(db, env, &tuple)), (TupleSpecBuilder::Variable { .. }, TupleSpec::Fixed(fixed)) => self .clone() .build() - .resize(db, TupleLength::Fixed(fixed.len())) + .resize(db, env, TupleLength::Fixed(fixed.len())) .ok() - .and_then(|tuple| TupleSpecBuilder::from(&tuple).intersect(db, other)), + .and_then(|tuple| TupleSpecBuilder::from(&tuple).intersect(db, env, other)), ( TupleSpecBuilder::Variable { @@ -2933,7 +3087,7 @@ impl<'db> TupleSpecBuilder<'db> { && suffix.len() == var.suffix_elements().len() { for (existing, new) in prefix.iter_mut().zip(var.prefix_elements()) { - *existing = IntersectionType::from_two_elements(db, *existing, *new); + *existing = IntersectionType::from_two_elements(db, env, *existing, *new); } *segment = match (*segment, var.variable()) { ( @@ -2943,26 +3097,30 @@ impl<'db> TupleSpecBuilder<'db> { (left, right) => { VariableSegment::Homogeneous(IntersectionType::from_two_elements( db, + env, left.element_type(db), right.element_type(db), )) } }; for (existing, new) in suffix.iter_mut().zip(var.suffix_elements()) { - *existing = IntersectionType::from_two_elements(db, *existing, *new); + *existing = IntersectionType::from_two_elements(db, env, *existing, *new); } return Some(self); } let self_built = self.clone().build(); let self_len = self_built.len(); - var.resize(db, self_len) + var.resize(db, env, self_len) .ok() - .and_then(|resized| self.intersect(db, &resized)) + .and_then(|resized| self.intersect(db, env, &resized)) .or_else(|| { - self_built.resize(db, var.len()).ok().and_then(|resized| { - TupleSpecBuilder::from(&resized).intersect(db, other) - }) + self_built + .resize(db, env, var.len()) + .ok() + .and_then(|resized| { + TupleSpecBuilder::from(&resized).intersect(db, env, other) + }) }) } } diff --git a/crates/ty_python_semantic/src/types/tuple/promotion.rs b/crates/ty_python_semantic/src/types/tuple/promotion.rs index b62cc3a336..018a6c4a71 100644 --- a/crates/ty_python_semantic/src/types/tuple/promotion.rs +++ b/crates/ty_python_semantic/src/types/tuple/promotion.rs @@ -1,8 +1,9 @@ +use crate::Db; +use crate::ProgramEnvironment; use rustc_hash::FxHashSet; -use ruff_python_ast as ast; +use ruff_python_ast::{self as ast}; -use crate::Db; use crate::types::tuple::TupleSpec; use crate::types::typevar::BoundTypeVarIdentity; use crate::types::visitor::any_over_type; @@ -24,12 +25,13 @@ impl<'db> TupleSizePromotionConstraints<'db> { pub(crate) fn record_inferred_expression_type( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar_identity: BoundTypeVarIdentity<'db>, expression: &ast::Expr, ty: Type<'db>, ) { - if !Self::is_promotable_tuple_literal(db, expression, ty) { - self.record_unpromotable_type(db, typevar_identity, ty); + if !Self::is_promotable_tuple_literal(db, env, expression, ty) { + self.record_unpromotable_type(db, env, typevar_identity, ty); } } @@ -38,10 +40,13 @@ impl<'db> TupleSizePromotionConstraints<'db> { pub(crate) fn record_unpromotable_type( &mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar_identity: BoundTypeVarIdentity<'db>, ty: Type<'db>, ) { - if any_over_type(db, ty, true, |ty| ty.tuple_instance_spec(db).is_some()) { + if any_over_type(db, env, ty, true, |ty| { + ty.tuple_instance_spec(db, env).is_some() + }) { self.blocked_typevars.insert(typevar_identity); } } @@ -54,9 +59,14 @@ impl<'db> TupleSizePromotionConstraints<'db> { /// Returns true if the given expression is either a non-starred homogeneous tuple literal or the /// empty tuple (and hence is eligible for tuple size promotion). - fn is_promotable_tuple_literal(db: &'db dyn Db, expression: &ast::Expr, ty: Type<'db>) -> bool { + fn is_promotable_tuple_literal( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + expression: &ast::Expr, + ty: Type<'db>, + ) -> bool { matches!(expression, ast::Expr::Tuple(tuple) if !tuple.iter().any(ast::Expr::is_starred_expr)) - && TupleSizePromotionCandidate::from_type(db, ty).is_some() + && TupleSizePromotionCandidate::from_type(db, env, ty).is_some() } } @@ -72,7 +82,7 @@ enum TupleSizePromotionCandidate<'db> { impl<'db> TupleSizePromotionCandidate<'db> { /// Returns an eligible candidate if the given type represents one (i.e., it is a /// fixed-length homogeneous tuple or the empty tuple). - fn from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { + fn from_type(db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> Option { let tuple_spec = ty.exact_tuple_instance_spec(db)?; let TupleSpec::Fixed(tuple) = tuple_spec.as_ref() else { return None; @@ -84,7 +94,7 @@ impl<'db> TupleSizePromotionCandidate<'db> { }; elements - .all(|element| element.is_equivalent_to(db, element_type)) + .all(|element| element.is_equivalent_to(db, env, element_type)) .then_some(Self::Homogeneous { element_type, length: tuple.len(), @@ -122,20 +132,21 @@ impl<'db> HomogeneousTupleUnionGroup<'db> { /// candidates for tuple size promotion, and another for groups of homogeneous tuple elements that are. fn partition_tuple_union_elements<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, elements: impl IntoIterator>, ) -> (Vec>, Vec>) { let mut other_union_elements = Vec::new(); let mut tuple_groups: Vec> = Vec::new(); for element in elements { - match TupleSizePromotionCandidate::from_type(db, element) { + match TupleSizePromotionCandidate::from_type(db, env, element) { Some(TupleSizePromotionCandidate::Homogeneous { element_type, length, }) => { if let Some(group) = tuple_groups .iter_mut() - .find(|group| group.element_type.is_equivalent_to(db, element_type)) + .find(|group| group.element_type.is_equivalent_to(db, env, element_type)) { group.add(element, length); } else { @@ -175,19 +186,23 @@ impl<'db> Type<'db> { /// reveal_type(languages) # revealed: dict[str, tuple[str, ...]] /// ``` /// - pub(crate) fn promote_tuple_size_in_union(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn promote_tuple_size_in_union( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { let Type::Union(union) = self else { return self; }; let (other_union_elements, tuple_groups) = - partition_tuple_union_elements(db, union.elements(db).iter().copied()); + partition_tuple_union_elements(db, env, union.elements(db).iter().copied()); if !tuple_groups.iter().any(|group| group.has_multiple_lengths) { return self; } - let mut builder = UnionBuilder::new(db) + let mut builder = UnionBuilder::new(db, env) .unpack_aliases(false) .recursively_defined(union.recursively_defined(db)); @@ -197,7 +212,7 @@ impl<'db> Type<'db> { for group in tuple_groups { if group.has_multiple_lengths { - builder = builder.add(Type::homogeneous_tuple(db, group.element_type)); + builder = builder.add(Type::homogeneous_tuple(db, env, group.element_type)); } else { for element in group.original_tuple_types { builder = builder.add(element); diff --git a/crates/ty_python_semantic/src/types/type_alias.rs b/crates/ty_python_semantic/src/types/type_alias.rs index 067f83019d..55650260a4 100644 --- a/crates/ty_python_semantic/src/types/type_alias.rs +++ b/crates/ty_python_semantic/src/types/type_alias.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use std::fmt::Write; use crate::{ @@ -19,8 +20,8 @@ use ty_python_core::{ }; use ruff_db::parsed::parsed_module; -use ruff_python_ast as ast; use ruff_python_ast::name::Name; +use ruff_python_ast::{self as ast}; #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct PEP695TypeAliasType<'db> { @@ -50,7 +51,7 @@ impl<'db> PEP695TypeAliasType<'db> { fn definition(self, db: &'db dyn Db) -> Definition<'db> { let scope = self.rhs_scope(db); let type_alias_stmt_node = scope.node(db).expect_type_alias(); - semantic_index(db, scope.file(db)).expect_single_definition(type_alias_stmt_node) + semantic_index(db, scope.python_file(db)).expect_single_definition(type_alias_stmt_node) } /// The RHS type of a PEP-695 style type alias with specialization applied. @@ -68,14 +69,16 @@ impl<'db> PEP695TypeAliasType<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _| { - value.cycle_normalized(db, *previous, cycle) + cycle_fn=|db: &'db dyn Db, cycle, previous: &Type<'db>, value: Type<'db>, alias: PEP695TypeAliasType<'db>| { + let env = ProgramEnvironment::from_scope(alias.rhs_scope(db)); + value.cycle_normalized(db, &env, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] pub(super) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { let scope = self.rhs_scope(db); - let module = parsed_module(db, scope.file(db)).load(db); + let python_file = scope.python_file(db); + let module = parsed_module(db, python_file).load(db); let type_alias_stmt_node = scope.node(db).expect_type_alias(); let definition = self.definition(db); @@ -110,8 +113,8 @@ impl<'db> PEP695TypeAliasType<'db> { #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { let scope = self.rhs_scope(db); - let file = scope.file(db); - let parsed = parsed_module(db, file).load(db); + let python_file = scope.python_file(db); + let parsed = parsed_module(db, python_file).load(db); let type_alias_stmt_node = scope.node(db).expect_type_alias(); type_alias_stmt_node @@ -119,7 +122,7 @@ impl<'db> PEP695TypeAliasType<'db> { .type_params .as_ref() .map(|type_params| { - let index = semantic_index(db, scope.file(db)); + let index = semantic_index(db, python_file); let definition = index.expect_single_definition(type_alias_stmt_node); GenericContext::from_type_params(db, index, definition, type_params) }) @@ -173,15 +176,15 @@ impl<'db> ManualPEP695TypeAliasType<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, id, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, value: Type<'db>, _| { - value.cycle_normalized(db, *previous, cycle) + cycle_fn=|db: &'db dyn Db, cycle, previous: &Type<'db>, value: Type<'db>, alias: ManualPEP695TypeAliasType<'db>| { + let env = ProgramEnvironment::from_definition(alias.definition(db)); + value.cycle_normalized(db, &env, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] pub(crate) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { let definition = self.definition(db); - let file = definition.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, definition.python_file(db)).load(db); let DefinitionKind::Assignment(assignment) = definition.kind(db) else { return Type::unknown(); }; @@ -216,7 +219,8 @@ impl<'db> ManualPEP695TypeAliasType<'db> { #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db); - let file = definition.file(db); + let file = definition.python_file(db); + let env = ProgramEnvironment::from_file(file); let module = parsed_module(db, file).load(db); let DefinitionKind::Assignment(assignment) = definition.kind(db) else { return None; @@ -248,7 +252,7 @@ impl<'db> ManualPEP695TypeAliasType<'db> { variables.insert(typevar); } - (!variables.is_empty()).then(|| GenericContext::from_typevar_instances(db, variables)) + (!variables.is_empty()).then(|| GenericContext::from_typevar_instances(db, &env, variables)) } } @@ -262,6 +266,7 @@ fn apply_type_alias_specialization<'db>( return ty; }; + let env = ProgramEnvironment::from_program(generic_context.program(db)); let specialization = specialization.unwrap_or_else(|| generic_context.default_specialization(db, None)); let type_mapping = match specialization.materialization_kind(db) { @@ -276,7 +281,7 @@ fn apply_type_alias_specialization<'db>( db, &type_mapping, TypeContext::default(), - &ApplyTypeMappingVisitor::default(), + &ApplyTypeMappingVisitor::new(&env), ) } @@ -393,16 +398,32 @@ impl<'db> TypeAliasType<'db> { } } -#[salsa::tracked] impl<'db> VarianceInferable<'db> for TypeAliasType<'db> { + fn variance_of( + self, + db: &'db dyn Db, + _: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.variance_of_owner(db, typevar) + } +} + +#[salsa::tracked] +impl<'db> TypeAliasType<'db> { #[salsa::tracked( returns(copy), cycle_initial=|_, _, _, _| TypeVarVariance::Bivariant, heap_size=ruff_memory_usage::heap_size )] - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of_owner( + self, + db: &'db dyn Db, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + let env = ProgramEnvironment::from_definition(self.definition(db)); let Some(generic_context) = self.generic_context(db) else { - return self.value_type(db).variance_of(db, typevar); + return self.value_type(db).variance_of(db, &env, typevar); }; // Infer an alias's own type-parameter variance from the raw RHS. Applying specialization @@ -411,7 +432,7 @@ impl<'db> VarianceInferable<'db> for TypeAliasType<'db> { .variables(db) .any(|alias_typevar| alias_typevar.identity(db) == typevar) { - return self.raw_value_type(db).variance_of(db, typevar); + return self.raw_value_type(db).variance_of(db, &env, typevar); } let raw_value_type = self.raw_value_type(db); @@ -426,8 +447,8 @@ impl<'db> VarianceInferable<'db> for TypeAliasType<'db> { .zip(specialization.types(db)) .map(|(alias_typevar, argument_ty)| { raw_value_type - .variance_of(db, alias_typevar.identity(db)) - .compose_thunk(|| argument_ty.variance_of(db, typevar)) + .variance_of(db, &env, alias_typevar.identity(db)) + .compose_thunk(|| argument_ty.variance_of(db, &env, typevar)) }) .collect() } @@ -454,7 +475,7 @@ impl<'db> QualifiedTypeAliasName<'db> { /// would return `["a", "b", "C"]`. pub(crate) fn components_excluding_self(&self) -> Vec { let definition = self.type_alias.definition(self.db); - let file = definition.file(self.db); + let file = definition.python_file(self.db); let file_scope_id = definition.file_scope(self.db); // Type aliases are defined directly in their enclosing scope (no body scope like classes), diff --git a/crates/ty_python_semantic/src/types/type_expansion.rs b/crates/ty_python_semantic/src/types/type_expansion.rs index 886f36bcf8..b3df97a456 100644 --- a/crates/ty_python_semantic/src/types/type_expansion.rs +++ b/crates/ty_python_semantic/src/types/type_expansion.rs @@ -1,6 +1,7 @@ +use crate::Db; use itertools::Itertools; -use crate::Db; +use crate::ProgramEnvironment; use crate::types::enums::enum_member_literals; use crate::types::tuple::Tuple; use crate::types::{KnownClass, Type}; @@ -16,19 +17,23 @@ const MAX_TUPLE_EXPANSION: usize = 64; /// Expands a type into its possible subtypes, if applicable. /// /// Returns [`None`] if the type cannot be expanded. -pub(crate) fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>> { +pub(crate) fn expand_type<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> Option>> { match ty { - Type::EnumComplement(complement) => Some(complement.remaining_literal_types(db)), - Type::Intersection(intersection) => intersection.finite_alternatives(db), + Type::EnumComplement(complement) => Some(complement.remaining_literal_types(db, env)), + Type::Intersection(intersection) => intersection.finite_alternatives(db, env), Type::NominalInstance(instance) => { - let class = instance.class(db); + let class = instance.class(db, env); if class.is_known(db, KnownClass::Bool) { return Some(vec![Type::bool_literal(true), Type::bool_literal(false)]); } // If the class is a fixed-length tuple subtype, we expand it to its elements. - if let Some(spec) = instance.tuple_spec(db) { + if let Some(spec) = instance.tuple_spec(db, env) { return match &*spec { Tuple::Fixed(fixed_length_tuple) => { // Pre-expand each element and compute the total Cartesian product size. @@ -38,7 +43,7 @@ pub(crate) fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option = fixed_length_tuple .iter_all_elements() .map(|element| { - expand_type(db, element).unwrap_or_else(|| vec![element]) + expand_type(db, env, element).unwrap_or_else(|| vec![element]) }) .collect(); @@ -53,7 +58,7 @@ pub(crate) fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option>(); Some(expanded) } @@ -73,16 +78,16 @@ pub(crate) fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option complement.remaining_literal_types(db), + Type::EnumComplement(complement) => complement.remaining_literal_types(db, env), Type::Intersection(intersection) => intersection - .finite_alternatives(db) + .finite_alternatives(db, env) .unwrap_or_else(|| vec![*element]), _ => vec![*element], }) .collect(), ), // For type aliases, expand the underlying value type. - Type::TypeAlias(alias) => expand_type(db, alias.value_type(db)), + Type::TypeAlias(alias) => expand_type(db, env, alias.value_type(db)), // We don't handle `type[A | B]` here because it's already stored in the expanded form // i.e., `type[A] | type[B]` which is handled by the `Type::Union` case. _ => None, @@ -100,13 +105,15 @@ mod tests { #[test] fn expand_union_type() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let types = [ - KnownClass::Int.to_instance(&db), - KnownClass::Str.to_instance(&db), - KnownClass::Bytes.to_instance(&db), + KnownClass::Int.to_instance(db, &env), + KnownClass::Str.to_instance(db, &env), + KnownClass::Bytes.to_instance(db, &env), ]; - let union_type = UnionType::from_elements(&db, types); - let expanded = expand_type(&db, union_type).unwrap(); + let union_type = UnionType::from_elements(db, &env, types); + let expanded = expand_type(db, &env, union_type).unwrap(); assert_eq!(expanded.len(), types.len()); assert_eq!(expanded, types); } @@ -114,8 +121,10 @@ mod tests { #[test] fn expand_bool_type() { let db = setup_db(); - let bool_instance = KnownClass::Bool.to_instance(&db); - let expanded = expand_type(&db, bool_instance).unwrap(); + let db = &db; + let env = db.program_environment(); + let bool_instance = KnownClass::Bool.to_instance(db, &env); + let expanded = expand_type(db, &env, bool_instance).unwrap(); let expected_types = [Type::bool_literal(true), Type::bool_literal(false)]; assert_eq!(expanded.len(), expected_types.len()); assert_eq!(expanded, expected_types); @@ -124,70 +133,78 @@ mod tests { #[test] fn expand_tuple_type() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); - let int_ty = KnownClass::Int.to_instance(&db); - let str_ty = KnownClass::Str.to_instance(&db); - let bytes_ty = KnownClass::Bytes.to_instance(&db); - let bool_ty = KnownClass::Bool.to_instance(&db); + let int_ty = KnownClass::Int.to_instance(db, &env); + let str_ty = KnownClass::Str.to_instance(db, &env); + let bytes_ty = KnownClass::Bytes.to_instance(db, &env); + let bool_ty = KnownClass::Bool.to_instance(db, &env); let true_ty = Type::bool_literal(true); let false_ty = Type::bool_literal(false); // Empty tuple - let empty_tuple = Type::empty_tuple(&db); - let expanded = expand_type(&db, empty_tuple); + let empty_tuple = Type::empty_tuple(db, &env); + let expanded = expand_type(db, &env, empty_tuple); assert!(expanded.is_none()); // None of the elements can be expanded. - let tuple_type1 = Type::heterogeneous_tuple(&db, [int_ty, str_ty]); - let expanded = expand_type(&db, tuple_type1); + let tuple_type1 = Type::heterogeneous_tuple(db, &env, [int_ty, str_ty]); + let expanded = expand_type(db, &env, tuple_type1); assert!(expanded.is_none()); // All elements can be expanded. let tuple_type2 = Type::heterogeneous_tuple( - &db, + db, + &env, [ bool_ty, - UnionType::from_elements(&db, [int_ty, str_ty, bytes_ty]), + UnionType::from_elements(db, &env, [int_ty, str_ty, bytes_ty]), ], ); let expected_types = [ - Type::heterogeneous_tuple(&db, [true_ty, int_ty]), - Type::heterogeneous_tuple(&db, [true_ty, str_ty]), - Type::heterogeneous_tuple(&db, [true_ty, bytes_ty]), - Type::heterogeneous_tuple(&db, [false_ty, int_ty]), - Type::heterogeneous_tuple(&db, [false_ty, str_ty]), - Type::heterogeneous_tuple(&db, [false_ty, bytes_ty]), + Type::heterogeneous_tuple(db, &env, [true_ty, int_ty]), + Type::heterogeneous_tuple(db, &env, [true_ty, str_ty]), + Type::heterogeneous_tuple(db, &env, [true_ty, bytes_ty]), + Type::heterogeneous_tuple(db, &env, [false_ty, int_ty]), + Type::heterogeneous_tuple(db, &env, [false_ty, str_ty]), + Type::heterogeneous_tuple(db, &env, [false_ty, bytes_ty]), ]; - let expanded = expand_type(&db, tuple_type2).unwrap(); + let expanded = expand_type(db, &env, tuple_type2).unwrap(); assert_eq!(expanded, expected_types); // Mixed set of elements where some can be expanded while others cannot be. let tuple_type3 = Type::heterogeneous_tuple( - &db, + db, + &env, [ bool_ty, int_ty, - UnionType::from_elements(&db, [str_ty, bytes_ty]), + UnionType::from_elements(db, &env, [str_ty, bytes_ty]), str_ty, ], ); let expected_types = [ - Type::heterogeneous_tuple(&db, [true_ty, int_ty, str_ty, str_ty]), - Type::heterogeneous_tuple(&db, [true_ty, int_ty, bytes_ty, str_ty]), - Type::heterogeneous_tuple(&db, [false_ty, int_ty, str_ty, str_ty]), - Type::heterogeneous_tuple(&db, [false_ty, int_ty, bytes_ty, str_ty]), + Type::heterogeneous_tuple(db, &env, [true_ty, int_ty, str_ty, str_ty]), + Type::heterogeneous_tuple(db, &env, [true_ty, int_ty, bytes_ty, str_ty]), + Type::heterogeneous_tuple(db, &env, [false_ty, int_ty, str_ty, str_ty]), + Type::heterogeneous_tuple(db, &env, [false_ty, int_ty, bytes_ty, str_ty]), ]; - let expanded = expand_type(&db, tuple_type3).unwrap(); + let expanded = expand_type(db, &env, tuple_type3).unwrap(); assert_eq!(expanded, expected_types); // Variable-length tuples are not expanded. let variable_length_tuple = Type::tuple(TupleType::mixed( - &db, + db, + &env, [bool_ty], int_ty, - [UnionType::from_elements(&db, [str_ty, bytes_ty]), str_ty], + [ + UnionType::from_elements(db, &env, [str_ty, bytes_ty]), + str_ty, + ], )); - let expanded = expand_type(&db, variable_length_tuple); + let expanded = expand_type(db, &env, variable_length_tuple); assert!(expanded.is_none()); } } diff --git a/crates/ty_python_semantic/src/types/type_form.rs b/crates/ty_python_semantic/src/types/type_form.rs index 6d817b329e..b85763b23c 100644 --- a/crates/ty_python_semantic/src/types/type_form.rs +++ b/crates/ty_python_semantic/src/types/type_form.rs @@ -4,6 +4,7 @@ use super::{ visitor, }; use crate::Db; +use crate::ProgramEnvironment; #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct TypeFormType<'db> { @@ -36,64 +37,73 @@ impl<'db> Type<'db> { /// bounds or constraints, using cycle detection for recursive types. Union and intersection /// elements that do not represent type forms are ignored, as are negative intersection /// elements. If no type-form component can be projected, this returns the original type. - pub(crate) fn project_type_form(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn project_type_form( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { struct TypeFormArgument; type TypeFormArgumentVisitor<'db> = CycleDetector<'db, TypeFormArgument, Type<'db>, Option>, 3>; fn project<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, visitor: &TypeFormArgumentVisitor<'db>, ) -> Option> { match ty { Type::TypeForm(type_form) => Some(type_form.type_argument(db)), Type::TypeAlias(alias) => { - visitor.visit(db, ty, || project(db, alias.value_type(db), visitor)) + visitor.visit(db, ty, || project(db, env, alias.value_type(db), visitor)) } Type::Union(union) => { let mut elements = union .elements(db) .iter() - .filter_map(|element| project(db, *element, visitor)) + .filter_map(|element| project(db, env, *element, visitor)) .peekable(); elements.peek()?; - Some(UnionType::from_elements(db, elements)) + Some(UnionType::from_elements(db, env, elements)) } Type::Intersection(intersection) => { let mut elements = intersection .iter_positive(db) - .filter_map(|element| project(db, element, visitor)) + .filter_map(|element| project(db, env, element, visitor)) .peekable(); elements.peek()?; - Some(IntersectionType::from_elements(db, elements)) + Some(IntersectionType::from_elements(db, env, elements)) } Type::TypeVar(typevar) => visitor.visit(db, ty, || { - typevar - .typevar(db) - .bound_or_constraints(db) - .and_then(|bound_or_constraints| { - project(db, bound_or_constraints.as_type(db), visitor) - }) + typevar.typevar(db).bound_or_constraints(db, env).and_then( + |bound_or_constraints| { + project(db, env, bound_or_constraints.as_type(db, env), visitor) + }, + ) }), - Type::SpecialForm(special_form) => special_form.type_form_argument(db), + Type::SpecialForm(special_form) => special_form.type_form_argument(db, env), Type::KnownInstance(instance) if instance.is_type_form_value() => { - instance.type_form_argument(db) + instance.type_form_argument(db, env) } Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => { - ty.to_instance_approximation(db) + ty.to_instance_approximation(db, env) } _ => None, } } - project(db, self, &TypeFormArgumentVisitor::default()).unwrap_or(self) + project(db, env, self, &TypeFormArgumentVisitor::default()).unwrap_or(self) } } impl<'db> VarianceInferable<'db> for TypeFormType<'db> { // `TypeForm` is covariant in its type argument. - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { - self.type_argument(db).variance_of(db, typevar) + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { + self.type_argument(db).variance_of(db, env, typevar) } } diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index 7eba50cbcf..078fdfb330 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -21,12 +21,12 @@ use super::{ ApplyTypeMappingVisitor, ErrorContext, IntersectionType, Type, TypeMapping, TypeQualifiers, UnionBuilder, definition_expression_annotation, definition_expression_type, visitor, }; -use crate::Db; use crate::types::TypeContext; use crate::types::TypeDefinition; use crate::types::class::FieldKind; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::relation::{DisjointnessChecker, TypeRelation, TypeRelationChecker}; +use crate::{Db, ProgramEnvironment}; use ty_python_core::definition::Definition; bitflags! { @@ -127,7 +127,7 @@ impl<'db> TypedDictOpenness<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { Self::ImplicitlyOpen | Self::Closed => self, @@ -144,6 +144,7 @@ impl<'db> TypedDictOpenness<'db> { pub(crate) fn recursive_type_normalized_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -152,7 +153,7 @@ impl<'db> TypedDictOpenness<'db> { Self::Extra(extra_items) => { let declared_ty = extra_items .declared_ty - .recursive_type_normalized_impl(db, div, true); + .recursive_type_normalized_impl(db, env, div, true); let declared_ty = if nested { declared_ty? } else { @@ -247,7 +248,9 @@ impl<'db> TypedDictType<'db> { let (class_literal, specialization) = class.class_literal_and_specialization(db); let static_class = match class_literal { ClassLiteral::Static(static_class) => static_class, - ClassLiteral::DynamicTypedDict(dynamic) => return dynamic.openness(db), + ClassLiteral::DynamicTypedDict(dynamic) => { + return dynamic.openness(db); + } ClassLiteral::Dynamic(_) | ClassLiteral::DynamicNamedTuple(_) | ClassLiteral::DynamicEnum(_) => { @@ -256,7 +259,9 @@ impl<'db> TypedDictType<'db> { } }; - let module = parsed_module(db, static_class.file(db)).load(db); + let python_file = static_class.python_file(db); + let env = ProgramEnvironment::from_file(python_file); + let module = parsed_module(db, python_file).load(db); let class_definition = static_class.definition(db); let class_stmt = class_definition .kind(db) @@ -278,7 +283,7 @@ impl<'db> TypedDictType<'db> { if let Some(closed) = arguments.find_keyword("closed") { let closed_ty = definition_expression_type(db, class_definition, &closed.value); - return if closed_ty.bool(db).is_always_true() { + return if closed_ty.bool(db, &env).is_always_true() { TypedDictOpenness::Closed } else { TypedDictOpenness::ImplicitlyOpen @@ -320,13 +325,13 @@ impl<'db> TypedDictType<'db> { /// /// An implicitly open `TypedDict` immediately returns `object` because hidden items may have /// any value type. This also avoids unnecessarily materializing its declared items. - pub(crate) fn value_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn value_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { let openness = self.openness(db); if openness.is_implicitly_open() { return Type::object(); } - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); for field in self.items(db).values() { builder = builder.add(field.declared_ty); } @@ -340,15 +345,15 @@ impl<'db> TypedDictType<'db> { /// /// A closed `TypedDict` has a finite set of literal keys. Open and extra-items `TypedDict`s may /// contain arbitrary string keys. - pub(crate) fn key_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn key_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { if !self.openness(db).is_closed() { - return KnownClass::Str.to_instance(db); + return KnownClass::Str.to_instance(db, env); } self.items(db) .iter() .filter(|(_, field)| field.may_be_present(db)) - .fold(UnionBuilder::new(db), |builder, (name, _)| { + .fold(UnionBuilder::new(db, env), |builder, (name, _)| { builder.add(Type::string_literal(db, name)) }) .build() @@ -375,8 +380,12 @@ impl<'db> TypedDictType<'db> { /// The runtime key may name either an extra item or any declared item, so the result is the /// intersection of all possible destination item types. Returns `None` unless extra items are /// explicit. - pub(crate) fn arbitrary_key_initialization_type(self, db: &'db dyn Db) -> Option> { - self.arbitrary_key_initialization_type_excluding(db, &OrderSet::new()) + pub(crate) fn arbitrary_key_initialization_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + self.arbitrary_key_initialization_type_excluding(db, env, &OrderSet::new()) } /// Returns the arbitrary-key initialization type after excluding keys that are known to be @@ -387,12 +396,14 @@ impl<'db> TypedDictType<'db> { fn arbitrary_key_initialization_type_excluding( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, excluded_keys: &OrderSet, ) -> Option> { let extra_items = self.explicit_extra_items(db)?; Some(IntersectionType::from_elements( db, + env, std::iter::once(extra_items.declared_ty).chain( self.items(db) .iter() @@ -406,7 +417,11 @@ impl<'db> TypedDictType<'db> { /// /// A mutation may target any declared or extra item, so no such mutation is allowed if any /// possible destination is read-only. - pub(crate) fn arbitrary_key_mutation_type(self, db: &'db dyn Db) -> Option> { + pub(crate) fn arbitrary_key_mutation_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { if self .explicit_extra_items(db) .is_some_and(TypedDictExtraItems::is_read_only) @@ -415,7 +430,7 @@ impl<'db> TypedDictType<'db> { return None; } - self.arbitrary_key_initialization_type(db) + self.arbitrary_key_initialization_type(db, env) } /// Returns whether operations that delete an arbitrary key are safe. @@ -440,7 +455,11 @@ impl<'db> TypedDictType<'db> { /// /// This requires mutable explicit extra items and optional, mutable declared items whose value /// types are equivalent to the extra-items type. - pub(crate) fn dict_value_type(self, db: &'db dyn Db) -> Option> { + pub(crate) fn dict_value_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let extra_items = self.explicit_extra_items(db)?; if extra_items.is_read_only() || self.items(db).values().any(|field| { @@ -448,7 +467,7 @@ impl<'db> TypedDictType<'db> { || field.is_read_only() || !field .declared_ty - .is_equivalent_to(db, extra_items.declared_ty) + .is_equivalent_to(db, env, extra_items.declared_ty) }) { return None; @@ -460,7 +479,11 @@ impl<'db> TypedDictType<'db> { /// /// This uses mutual assignability rather than equivalence so gradual value types can satisfy /// the mutable `dict` contract. - pub(crate) fn assignable_dict_value_type(self, db: &'db dyn Db) -> Option> { + pub(crate) fn assignable_dict_value_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let extra_items = self.explicit_extra_items(db)?; if extra_items.is_read_only() || self.items(db).values().any(|field| { @@ -468,10 +491,10 @@ impl<'db> TypedDictType<'db> { || field.is_read_only() || !field .declared_ty - .is_assignable_to(db, extra_items.declared_ty) + .is_assignable_to(db, env, extra_items.declared_ty) || !extra_items .declared_ty - .is_assignable_to(db, field.declared_ty) + .is_assignable_to(db, env, field.declared_ty) }) { return None; @@ -522,6 +545,7 @@ impl<'db> TypedDictType<'db> { if let ClassLiteral::DynamicTypedDict(class) = defining_class.class_literal(db) { return class.items(db); } + class_based_items(db, defining_class) } Self::Synthesized(synthesized) => synthesized.items(db), @@ -533,7 +557,7 @@ impl<'db> TypedDictType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { // TODO: Materialization of gradual TypedDicts needs more logic match self { @@ -644,16 +668,16 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let mut result = self.always(); for (source_item_name, source_item_field) in source_items { - let target_ty = if let Some(target_item_field) = target_items.get(source_item_name) - { - target_item_field.declared_ty - } else { - match target_openness { - TypedDictOpenness::ImplicitlyOpen => continue, - TypedDictOpenness::Closed => return self.never(), - TypedDictOpenness::Extra(extra_items) => extra_items.declared_ty, - } - }; + let target_ty = + if let Some(target_item_field) = target_items.get(source_item_name.as_str()) { + target_item_field.declared_ty + } else { + match target_openness { + TypedDictOpenness::ImplicitlyOpen => continue, + TypedDictOpenness::Closed => return self.never(), + TypedDictOpenness::Extra(extra_items) => extra_items.declared_ty, + } + }; result.intersect( db, @@ -730,7 +754,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { for (target_item_name, target_item_field) in target_items { let field_constraints = if target_item_field.is_required() { // required target fields - let Some(source_item_field) = source_items.get(target_item_name) else { + let Some(source_item_field) = source_items.get(target_item_name.as_str()) else { // Self is missing a required field. if let Some(context) = self.report_context() { context.push(ErrorContext::TypedDictFieldMissing { @@ -797,7 +821,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // A missing read-only field is checked against the source's effective extra // items. Missing mutable fields below require explicit mutable extra items and // a relation in both directions. - if let Some(source_item_field) = source_items.get(target_item_name) { + if let Some(source_item_field) = source_items.get(target_item_name.as_str()) { self.check_type_pair( db, source_item_field.declared_ty, @@ -815,7 +839,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } } } else { - if let Some(source_item_field) = source_items.get(target_item_name) { + if let Some(source_item_field) = source_items.get(target_item_name.as_str()) { if source_item_field.is_read_only() { // A read-only field can't be assigned to a mutable target. if let Some(context) = self.report_context() { @@ -881,10 +905,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }; result.intersect(db, self.constraints, field_constraints); if result.is_trivially_never_satisfied() - || (self.is_context_collection_enabled() && result.is_never_satisfied(db)) + || (self.is_context_collection_enabled() && result.is_never_satisfied(db, self.env)) { if let Some(context) = self.report_context() - && let Some(source_item_field) = source_items.get(target_item_name) + && let Some(source_item_field) = source_items.get(target_item_name.as_str()) { context.push(ErrorContext::TypedDictFieldIncompatible { field_name: target_item_name.clone(), @@ -1273,7 +1297,9 @@ pub(super) fn deferred_functional_typed_dict_schema<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> TypedDictSchema<'db> { - let module = parsed_module(db, definition.file(db)).load(db); + let python_file = definition.python_file(db); + let env = ProgramEnvironment::from_file(python_file); + let module = parsed_module(db, python_file).load(db); let node = definition .kind(db) .value(&module) @@ -1285,7 +1311,7 @@ pub(super) fn deferred_functional_typed_dict_schema<'db>( let total = node.arguments.find_keyword("total").is_none_or(|total_kw| { let total_ty = definition_expression_type(db, definition, &total_kw.value); - !total_ty.bool(db).is_always_false() + !total_ty.bool(db, &env).is_always_false() }); let mut schema = TypedDictSchema::default(); @@ -1335,7 +1361,9 @@ pub(super) fn deferred_functional_typed_dict_openness<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> TypedDictOpenness<'db> { - let module = parsed_module(db, definition.file(db)).load(db); + let python_file = definition.python_file(db); + let env = ProgramEnvironment::from_file(python_file); + let module = parsed_module(db, python_file).load(db); let node = definition .kind(db) .value(&module) @@ -1356,7 +1384,7 @@ pub(super) fn deferred_functional_typed_dict_openness<'db>( if let Some(closed) = node.arguments.find_keyword("closed") { let closed_ty = definition_expression_type(db, definition, &closed.value); - if closed_ty.bool(db).is_always_true() { + if closed_ty.bool(db, &env).is_always_true() { return TypedDictOpenness::Closed; } } @@ -1450,6 +1478,8 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { return false; }; + let env = &self.context.program_environment(); + if self.assignment_kind.is_subscript() && item.is_read_only() { if self.emit_diagnostic && let Some(builder) = self @@ -1457,7 +1487,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { .report_lint(self.assignment_kind.diagnostic_type(), self.key_node) { let typed_dict_ty = Type::TypedDict(self.typed_dict); - let typed_dict_d = typed_dict_ty.display(db); + let typed_dict_d = typed_dict_ty.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Cannot assign to key \"{}\" on TypedDict `{typed_dict_d}`", @@ -1465,7 +1495,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { )); diagnostic.set_primary_annotation_message(format_args!("key is marked read-only")); - self.add_object_type_annotation(db, &mut diagnostic); + self.add_object_type_annotation(db, env, &mut diagnostic); Self::add_item_definition_subdiagnostic( db, &item, @@ -1478,7 +1508,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { } // Key exists, check if value type is assignable to declared type - if self.value_ty.is_assignable_to(db, item.declared_ty) { + if self.value_ty.is_assignable_to(db, env, item.declared_ty) { return true; } @@ -1493,9 +1523,9 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { .report_lint(self.assignment_kind.diagnostic_type(), self.value_node) { let typed_dict_ty = Type::TypedDict(self.typed_dict); - let typed_dict_d = typed_dict_ty.display(db); - let value_d = self.value_ty.display(db); - let item_type_d = item.declared_ty.display(db); + let typed_dict_d = typed_dict_ty.display(db, env); + let value_d = self.value_ty.display(db, env); + let item_type_d = item.declared_ty.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Invalid {} to key \"{}\" with declared type `{item_type_d}` \ @@ -1518,19 +1548,24 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { &mut diagnostic, "Item declared here", ); - self.add_object_type_annotation(db, &mut diagnostic); + self.add_object_type_annotation(db, env, &mut diagnostic); } false } - fn add_object_type_annotation(&self, db: &'db dyn Db, diagnostic: &mut Diagnostic) { + fn add_object_type_annotation( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + diagnostic: &mut Diagnostic, + ) { if let Some(full_object_ty) = self.full_object_ty { diagnostic.annotate(self.context.secondary(self.typed_dict_node).message( format_args!( "TypedDict `{}` in {kind} type `{}`", - Type::TypedDict(self.typed_dict).display(db), - full_object_ty.display(db), + Type::TypedDict(self.typed_dict).display(db, env), + full_object_ty.display(db, env), kind = if full_object_ty.is_union() { "union" } else { @@ -1542,7 +1577,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { diagnostic.annotate(self.context.secondary(self.typed_dict_node).message( format_args!( "TypedDict `{}`", - Type::TypedDict(self.typed_dict).display(db) + Type::TypedDict(self.typed_dict).display(db, env) ), )); } @@ -1556,7 +1591,7 @@ impl<'db> TypedDictKeyAssignment<'_, 'db, '_> { ) { if let Some(declaration) = item.first_declaration() { let file = declaration.file(db); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, declaration.python_file(db)).load(db); let mut sub = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Item declaration"); sub.annotate( @@ -1635,6 +1670,7 @@ pub(crate) struct UnpackedTypedDict<'db> { /// writes through the synthesized policy. fn intersect_unpacked_typed_dict_openness<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, openness: impl IntoIterator>, ) -> TypedDictOpenness<'db> { let mut explicit_value_types = Vec::new(); @@ -1654,7 +1690,7 @@ fn intersect_unpacked_typed_dict_openness<'db>( } else { TypedDictOpenness::extra( db, - IntersectionType::from_elements(db, explicit_value_types), + IntersectionType::from_elements(db, env, explicit_value_types), true, ) } @@ -1673,9 +1709,10 @@ fn intersect_unpacked_typed_dict_openness<'db>( /// observes its values. fn union_unpacked_typed_dict_openness<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, openness: impl IntoIterator>, ) -> TypedDictOpenness<'db> { - let mut value_types = UnionBuilder::new(db); + let mut value_types = UnionBuilder::new(db, env); let mut has_implicitly_open = false; let mut has_explicit_extra_items = false; @@ -1712,14 +1749,16 @@ fn union_unpacked_typed_dict_openness<'db>( /// and a key is only considered required if every arm requires it. pub(crate) fn extract_unpacked_typed_dict_keys_from_value_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option>> { - extract_unpacked_typed_dict_from_value_type(db, ty).map(|unpacked| unpacked.keys) + extract_unpacked_typed_dict_from_value_type(db, env, ty).map(|unpacked| unpacked.keys) } /// Extracts the declared keys and openness from a `TypedDict`-shaped value. pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Option> { match ty { @@ -1748,7 +1787,9 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( let unpacked_elements: Vec<_> = intersection .positive(db) .iter() - .filter_map(|element| extract_unpacked_typed_dict_from_value_type(db, *element)) + .filter_map(|element| { + extract_unpacked_typed_dict_from_value_type(db, env, *element) + }) .collect(); if unpacked_elements.is_empty() { @@ -1765,6 +1806,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( .and_modify(|existing| { existing.value_ty = IntersectionType::from_two_elements( db, + env, existing.value_ty, unpacked_key.value_ty, ); @@ -1786,6 +1828,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( if let Some(extra_items) = unpacked.openness.effective_extra_items() { unpacked_key.value_ty = IntersectionType::from_two_elements( db, + env, unpacked_key.value_ty, extra_items.declared_ty, ); @@ -1796,6 +1839,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( let openness = intersect_unpacked_typed_dict_openness( db, + env, unpacked_elements.iter().map(|unpacked| unpacked.openness), ); @@ -1808,7 +1852,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( let unpacked_elements: Vec<_> = union .elements(db) .iter() - .map(|element| extract_unpacked_typed_dict_from_value_type(db, *element)) + .map(|element| extract_unpacked_typed_dict_from_value_type(db, env, *element)) .collect::>()?; let all_keys: OrderSet = unpacked_elements @@ -1818,13 +1862,13 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( let mut result = BTreeMap::new(); for key in all_keys { - let mut value_ty = UnionBuilder::new(db); + let mut value_ty = UnionBuilder::new(db, env); let mut is_required = true; let mut definition = None; let mut saw_key = false; for unpacked in &unpacked_elements { - if let Some(unpacked_key) = unpacked.keys.get(&key) { + if let Some(unpacked_key) = unpacked.keys.get(key.as_str()) { saw_key = true; value_ty.add_in_place(unpacked_key.value_ty); is_required &= unpacked_key.is_required; @@ -1858,6 +1902,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( let openness = union_unpacked_typed_dict_openness( db, + env, unpacked_elements.iter().map(|unpacked| unpacked.openness), ); @@ -1867,7 +1912,7 @@ pub(crate) fn extract_unpacked_typed_dict_from_value_type<'db>( }) } Type::TypeAlias(alias) => { - extract_unpacked_typed_dict_from_value_type(db, alias.value_type(db)) + extract_unpacked_typed_dict_from_value_type(db, env, alias.value_type(db)) } // All other types cannot contain a TypedDict Type::Dynamic(_) @@ -1979,6 +2024,7 @@ fn unpacked_keyword_is_gradual<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool { /// key diagnostics for the positional mapping. pub(super) fn collect_guaranteed_keyword_keys<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, arguments: &Arguments, unpacked_keyword_types: &[Option>], @@ -2011,6 +2057,7 @@ pub(super) fn collect_guaranteed_keyword_keys<'db>( collect_guaranteed_keys_from_merged_unpacked_keyword( db, + env, typed_dict, &keyword.value, unpacked_type, @@ -2025,6 +2072,7 @@ pub(super) fn collect_guaranteed_keyword_keys<'db>( /// Collects keys guaranteed by one unpacked constructor argument. fn collect_guaranteed_keys_from_merged_unpacked_keyword<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typed_dict: TypedDictType<'db>, expr: &ast::Expr, unpacked_type: Type<'db>, @@ -2042,6 +2090,7 @@ fn collect_guaranteed_keys_from_merged_unpacked_keyword<'db>( let nested_ty = expression_type_fn(&item.value, TypeContext::default()); collect_guaranteed_keys_from_merged_unpacked_keyword( db, + env, typed_dict, &item.value, nested_ty, @@ -2056,7 +2105,7 @@ fn collect_guaranteed_keys_from_merged_unpacked_keyword<'db>( if unpacked_keyword_is_gradual(db, unpacked_type) { provided_keys.extend(typed_dict.items(db).keys().cloned()); } else if let Some(unpacked_keys) = - extract_unpacked_typed_dict_keys_from_value_type(db, unpacked_type) + extract_unpacked_typed_dict_keys_from_value_type(db, env, unpacked_type) { for (key, unpacked_key) in unpacked_keys { if unpacked_key.is_required { @@ -2204,6 +2253,7 @@ fn validate_extracted_typed_dict_openness<'db, 'ast>( return true; }; let extra_items_ty = extra_items.declared_ty; + let env = context.program_environment(); let target_openness = typed_dict.openness(db); if target_openness.is_implicitly_open() && source_openness.is_implicitly_open() { @@ -2217,41 +2267,39 @@ fn validate_extracted_typed_dict_openness<'db, 'ast>( typed_dict.items(db).iter().find(|(name, field)| { !source_keys.contains_key(*name) && !ignored_keys.contains(*name) - && !extra_items_ty.is_assignable_to(db, field.declared_ty) + && !extra_items_ty.is_assignable_to(db, env, field.declared_ty) }) { if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, nodes.value) { let mut diagnostic = builder.into_diagnostic(format_args!( "Unpacked argument has extra items of type `{}` that are not assignable to item `{target_name}` with type `{}` on TypedDict `{}`", - extra_items_ty.display(db), - target_field.declared_ty.display(db), - typed_dict_ty.display(db), + extra_items_ty.display(db, env), + target_field.declared_ty.display(db, env), + typed_dict_ty.display(db, env), )); - diagnostic.annotate( - context - .secondary(nodes.typed_dict) - .message(format_args!("TypedDict `{}`", typed_dict_ty.display(db))), - ); + diagnostic.annotate(context.secondary(nodes.typed_dict).message(format_args!( + "TypedDict `{}`", + typed_dict_ty.display(db, env) + ))); } return false; } - if extra_items_ty.is_assignable_to(db, target_extra_items.declared_ty) { + if extra_items_ty.is_assignable_to(db, env, target_extra_items.declared_ty) { return true; } if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, nodes.value) { let mut diagnostic = builder.into_diagnostic(format_args!( "Unpacked argument has extra items of type `{}` that are not assignable to extra items type `{}` on TypedDict `{}`", - extra_items_ty.display(db), - target_extra_items.declared_ty.display(db), - typed_dict_ty.display(db), + extra_items_ty.display(db, env), + target_extra_items.declared_ty.display(db, env), + typed_dict_ty.display(db, env), )); - diagnostic.annotate( - context - .secondary(nodes.typed_dict) - .message(format_args!("TypedDict `{}`", typed_dict_ty.display(db))), - ); + diagnostic.annotate(context.secondary(nodes.typed_dict).message(format_args!( + "TypedDict `{}`", + typed_dict_ty.display(db, env) + ))); } return false; } @@ -2259,13 +2307,12 @@ fn validate_extracted_typed_dict_openness<'db, 'ast>( if let Some(builder) = context.report_lint(&INVALID_KEY, nodes.key) { let mut diagnostic = builder.into_diagnostic(format_args!( "Unpacked argument may contain unknown keys for TypedDict `{}`", - typed_dict_ty.display(db), + typed_dict_ty.display(db, env), )); - diagnostic.annotate( - context - .secondary(nodes.typed_dict) - .message(format_args!("TypedDict `{}`", typed_dict_ty.display(db))), - ); + diagnostic.annotate(context.secondary(nodes.typed_dict).message(format_args!( + "TypedDict `{}`", + typed_dict_ty.display(db, env) + ))); } false } @@ -2286,8 +2333,9 @@ fn validate_from_typed_dict_argument<'db, 'ast>( ignored_keys: &OrderSet, ) -> Option> { let db = context.db(); + let env = context.program_environment(); let typed_dict_items = typed_dict.items(db); - let unpacked = extract_unpacked_typed_dict_from_value_type(db, arg_ty)?; + let unpacked = extract_unpacked_typed_dict_from_value_type(db, env, arg_ty)?; let source_openness = unpacked.openness; let validate_extra_keys = !typed_dict.openness(db).is_implicitly_open(); let unpacked_keys = unpacked @@ -2329,11 +2377,13 @@ fn report_duplicate_typed_dict_constructor_key<'db, 'ast>( duplicate_node: AnyNodeRef<'ast>, original_node: AnyNodeRef<'ast>, ) { + let db = context.db(); let Some(builder) = context.report_lint(&PARAMETER_ALREADY_ASSIGNED, duplicate_node) else { return; }; - let typed_dict_display = Type::TypedDict(typed_dict).display(context.db()); + let env = context.program_environment(); + let typed_dict_display = Type::TypedDict(typed_dict).display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( "Multiple values provided for key \"{key}\" in TypedDict `{typed_dict_display}` constructor", )); @@ -2387,6 +2437,7 @@ pub(super) fn validate_typed_dict_constructor<'db, 'ast>( ) { let db = context.db(); let typed_dict_ty = Type::TypedDict(typed_dict); + let env = context.program_environment(); if arguments.args.len() > 1 { if let Some(builder) = @@ -2394,7 +2445,7 @@ pub(super) fn validate_typed_dict_constructor<'db, 'ast>( { builder.into_diagnostic(format_args!( "Too many positional arguments to TypedDict `{}` constructor: expected 1, got {}", - typed_dict_ty.display(db), + typed_dict_ty.display(db, env), arguments.args.len(), )); } @@ -2451,14 +2502,14 @@ pub(super) fn validate_typed_dict_constructor<'db, 'ast>( ) { provided_keys } else { - if !positional_target_is_unconstrained - && !arg_ty.is_assignable_to(db, positional_target_ty) - { - if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, arg) { + if !positional_target_is_unconstrained { + if !arg_ty.is_assignable_to(db, env, positional_target_ty) + && let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, arg) + { builder.into_diagnostic(format_args!( "Argument of type `{}` is not assignable to `{}`", - arg_ty.display(db), - positional_target_ty.display(db), + arg_ty.display(db, env), + positional_target_ty.display(db, env), )); } } @@ -2493,12 +2544,12 @@ pub(super) fn validate_typed_dict_constructor<'db, 'ast>( let arg = &arguments.args[0]; let arg_ty = expression_type_fn(arg, TypeContext::new(Some(typed_dict_ty))); - if !arg_ty.is_assignable_to(db, typed_dict_ty) { + if !arg_ty.is_assignable_to(db, env, typed_dict_ty) { if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, arg) { builder.into_diagnostic(format_args!( "Argument of type `{}` is not assignable to `{}`", - arg_ty.display(db), - typed_dict_ty.display(db), + arg_ty.display(db, env), + typed_dict_ty.display(db, env), )); } } @@ -2656,27 +2707,28 @@ fn validate_merged_dict_literal<'db, 'ast>( ) -> bool { let db = context.db(); let mut valid = true; + let env = &context.program_environment(); for item in dict_expr.items.iter().rev() { if let Some(key_expr) = &item.key { let key_ty = expression_type_fn(key_expr, TypeContext::default()); let Some(key_literal) = key_ty.as_string_literal() else { - if key_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) { - if let Some(expected_ty) = - typed_dict.arbitrary_key_initialization_type_excluding(db, shadowed_keys) + if key_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { + if let Some(expected_ty) = typed_dict + .arbitrary_key_initialization_type_excluding(db, env, shadowed_keys) { let value_ty = expression_type_fn(&item.value, TypeContext::new(Some(expected_ty))); - if !value_ty.is_assignable_to(db, expected_ty) { + if !value_ty.is_assignable_to(db, env, expected_ty) { valid = false; if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, &item.value) { builder.into_diagnostic(format_args!( "Value of type `{}` is not assignable to arbitrary key value type `{}` on TypedDict `{}`", - value_ty.display(db), - expected_ty.display(db), - Type::TypedDict(typed_dict).display(db), + value_ty.display(db, env), + expected_ty.display(db, env), + Type::TypedDict(typed_dict).display(db, env), )); } } @@ -2685,7 +2737,7 @@ fn validate_merged_dict_literal<'db, 'ast>( if let Some(builder) = context.report_lint(&INVALID_KEY, key_expr) { builder.into_diagnostic(format_args!( "Non-literal string key may be unknown for TypedDict `{}`", - Type::TypedDict(typed_dict).display(db), + Type::TypedDict(typed_dict).display(db, env), )); } } @@ -2694,8 +2746,8 @@ fn validate_merged_dict_literal<'db, 'ast>( if let Some(builder) = context.report_lint(&INVALID_KEY, key_expr) { builder.into_diagnostic(format_args!( "TypedDict `{}` requires string keys, got key of type `{}`", - Type::TypedDict(typed_dict).display(db), - key_ty.display(db), + Type::TypedDict(typed_dict).display(db, env), + key_ty.display(db, env), )); } } @@ -2765,6 +2817,7 @@ fn validate_merged_unpacked_keyword_argument<'db, 'ast>( expression_type_fn: &mut impl FnMut(&ast::Expr, TypeContext<'db>) -> Type<'db>, ) -> bool { let db = context.db(); + let env = context.program_environment(); let items = typed_dict.items(db); if let ast::Expr::Dict(dict_expr) = expr { @@ -2787,7 +2840,9 @@ fn validate_merged_unpacked_keyword_argument<'db, 'ast>( guaranteed_keys.entry(key_name.clone()).or_insert(None); } return true; - } else if let Some(unpacked) = extract_unpacked_typed_dict_from_value_type(db, unpacked_type) { + } + + if let Some(unpacked) = extract_unpacked_typed_dict_from_value_type(db, env, unpacked_type) { let ignored_keys = shadowed_keys.clone(); let (_, mut unpacked_valid) = validate_extracted_typed_dict_keys( context, @@ -2821,12 +2876,14 @@ fn validate_merged_unpacked_keyword_argument<'db, 'ast>( } return unpacked_valid; - } else if let Some((key_ty, value_ty)) = unpacked_type.unpack_keys_and_items(db) { - if !key_ty.is_assignable_to(db, KnownClass::Str.to_instance(db)) { + } + + if let Some((key_ty, value_ty)) = unpacked_type.unpack_keys_and_items(db, env) { + if !key_ty.is_assignable_to(db, env, KnownClass::Str.to_instance(db, env)) { if let Some(builder) = context.report_lint(&INVALID_ARGUMENT_TYPE, nodes.value) { builder.into_diagnostic(format_args!( "Unpacked argument has key type `{}` that is not assignable to `str`", - key_ty.display(db), + key_ty.display(db, env), )); } return false; @@ -2925,7 +2982,7 @@ impl<'db> SynthesizedTypedDictType<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let items = self .items(db) @@ -2957,6 +3014,7 @@ impl<'db> TypedDictSchema<'db> { pub(super) fn recursive_type_normalized_impl( &self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, div: Type<'db>, nested: bool, ) -> Option { @@ -2964,7 +3022,7 @@ impl<'db> TypedDictSchema<'db> { .map(|(name, field)| { let declared_ty = field .declared_ty - .recursive_type_normalized_impl(db, div, true); + .recursive_type_normalized_impl(db, env, div, true); let declared_ty = if nested { declared_ty? } else { @@ -3046,7 +3104,7 @@ impl<'db> TypedDictField<'db> { db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, tcx: TypeContext<'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self { declared_ty: self diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index a1e29b1750..0b9aeaed6a 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -1,3 +1,4 @@ +use crate::ProgramEnvironment; use std::cell::{Cell, RefCell}; use std::rc::Rc; @@ -24,6 +25,7 @@ use crate::{ }, }; use ty_python_core::{ + Program, definition::{Definition, DefinitionKind}, semantic_index, }; @@ -40,16 +42,17 @@ impl<'db> Type<'db> { } } - pub(crate) fn has_typevar(self, db: &'db dyn Db) -> bool { - any_over_type(db, self, false, |ty| matches!(ty, Type::TypeVar(_))) + pub(crate) fn has_typevar(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool { + any_over_type(db, env, self, false, |ty| matches!(ty, Type::TypeVar(_))) } pub(crate) fn references_typevar( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, typevar_id: TypeVarIdentity<'db>, ) -> bool { - any_over_type(db, self, false, |ty| match ty { + any_over_type(db, env, self, false, |ty| match ty { Type::TypeVar(bound_typevar) => typevar_id == bound_typevar.typevar(db).identity(db), Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => { typevar_id == typevar.identity(db) @@ -58,17 +61,26 @@ impl<'db> Type<'db> { }) } - pub(crate) fn has_non_self_typevar(self, db: &'db dyn Db) -> bool { + pub(crate) fn has_non_self_typevar( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { any_over_type( db, + env, self, false, |ty| matches!(ty, Type::TypeVar(tv) if !tv.typevar(db).is_self(db)), ) } - pub(crate) fn has_typevar_or_typevar_instance(self, db: &'db dyn Db) -> bool { - any_over_type(db, self, false, |ty| { + pub(crate) fn has_typevar_or_typevar_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + any_over_type(db, env, self, false, |ty| { matches!( ty, Type::KnownInstance(KnownInstanceType::TypeVar(_)) | Type::TypeVar(_) @@ -76,8 +88,12 @@ impl<'db> Type<'db> { }) } - pub(crate) fn has_unspecialized_type_var(self, db: &'db dyn Db) -> bool { - any_over_type(db, self, false, |ty| { + pub(crate) fn has_unspecialized_type_var( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> bool { + any_over_type(db, env, self, false, |ty| { matches!(ty, Type::Dynamic(DynamicType::UnspecializedTypeVar)) }) } @@ -146,10 +162,12 @@ pub(super) fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( visitor: &V, ) { if let Some(bound_or_constraints) = if visitor.should_visit_lazy_type_attributes() { - typevar.bound_or_constraints(db) + typevar.bound_or_constraints(db, visitor.program_environment()) } else { match typevar._bound_or_constraints(db) { - _ if visitor.should_visit_lazy_type_attributes() => typevar.bound_or_constraints(db), + _ if visitor.should_visit_lazy_type_attributes() => { + typevar.bound_or_constraints(db, visitor.program_environment()) + } Some(TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints)) => { Some(bound_or_constraints) } @@ -159,7 +177,7 @@ pub(super) fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( walk_type_var_bounds(db, bound_or_constraints, visitor); } if let Some(default_type) = if visitor.should_visit_lazy_type_attributes() { - typevar.default_type(db) + typevar.default_type(db, visitor.program_environment()) } else { match typevar._default(db) { Some(TypeVarDefaultEvaluation::Eager(default_type)) => Some(default_type), @@ -230,30 +248,19 @@ impl<'db> TypeVarInstance<'db> { self.kind(db).is_typevartuple() } - pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { - if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { + pub(crate) fn upper_bound( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db, env) + { Some(ty) } else { None } } - /// Returns the static upper bound used when materializing a gradual type argument. - /// - /// Constraints are unioned only when materializing an exposed member, where their union is a - /// valid conservative upper bound. A bound may recursively refer to its own generic class, - /// either directly or through other bounds. Such a bound has no finite static top - /// materialization, so recover from its cycle without applying an upper bound. - #[salsa::tracked( - returns(copy), - cycle_result=|_, _, _| None, - heap_size=ruff_memory_usage::heap_size - )] - pub(super) fn top_materialized_upper_bound(self, db: &'db dyn Db) -> Option> { - self.bound_or_constraints(db) - .map(|bound_or_constraints| bound_or_constraints.as_type(db).top_materialization(db)) - } - /// Returns whether this type variable has constraints without evaluating a lazy bound. pub(super) fn is_constrained(self, db: &'db dyn Db) -> bool { matches!( @@ -266,8 +273,14 @@ impl<'db> TypeVarInstance<'db> { ) } - pub(crate) fn constraints(self, db: &'db dyn Db) -> Option<&'db [Type<'db>]> { - if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = self.bound_or_constraints(db) { + pub(crate) fn constraints( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option<&'db [Type<'db>]> { + if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = + self.bound_or_constraints(db, env) + { Some(tuple.elements(db)) } else { None @@ -277,16 +290,17 @@ impl<'db> TypeVarInstance<'db> { pub(crate) fn bound_or_constraints( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> Option> { self._bound_or_constraints(db).and_then(|w| match w { TypeVarBoundOrConstraintsEvaluation::Eager(bound_or_constraints) => { Some(bound_or_constraints) } TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => self - .lazy_bound(db) + .lazy_bound(db, env) .map(TypeVarBoundOrConstraints::UpperBound), TypeVarBoundOrConstraintsEvaluation::LazyConstraints => self - .lazy_constraints(db) + .lazy_constraints(db, env) .map(TypeVarBoundOrConstraints::Constraints), }) } @@ -296,25 +310,31 @@ impl<'db> TypeVarInstance<'db> { pub(crate) fn require_bound_or_constraints( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ) -> TypeVarBoundOrConstraints<'db> { - self.bound_or_constraints(db) + self.bound_or_constraints(db, env) .unwrap_or_else(|| TypeVarBoundOrConstraints::UpperBound(Type::object())) } - pub(crate) fn default_type(self, db: &'db dyn Db) -> Option> { + pub(crate) fn default_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let visitor = TypeVarDefaultVisitor::new(None); - self.default_type_impl(db, &visitor) + self.default_type_impl(db, env, &visitor) } fn default_type_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, visitor: &TypeVarDefaultVisitor<'db>, ) -> Option> { visitor.visit(db, self, || { self._default(db).and_then(|default| match default { TypeVarDefaultEvaluation::Eager(ty) => Some(ty), - TypeVarDefaultEvaluation::Lazy => self.lazy_default_impl(db, visitor), + TypeVarDefaultEvaluation::Lazy => self.lazy_default_impl(db, env, visitor), }) }) } @@ -323,7 +343,7 @@ impl<'db> TypeVarInstance<'db> { self, db: &'db dyn Db, materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self::new( db, @@ -336,14 +356,14 @@ impl<'db> TypeVarInstance<'db> { .into(), ), TypeVarBoundOrConstraintsEvaluation::LazyUpperBound => { - self.lazy_bound(db).map(|bound| { + self.lazy_bound(db, visitor.env).map(|bound| { TypeVarBoundOrConstraints::UpperBound(bound) .materialize_impl(db, materialization_kind, visitor) .into() }) } TypeVarBoundOrConstraintsEvaluation::LazyConstraints => { - self.lazy_constraints(db).map(|constraints| { + self.lazy_constraints(db, visitor.env).map(|constraints| { TypeVarBoundOrConstraints::Constraints(constraints) .materialize_impl(db, materialization_kind, visitor) .into() @@ -356,19 +376,23 @@ impl<'db> TypeVarInstance<'db> { Some(ty.materialize(db, materialization_kind, visitor).into()) } TypeVarDefaultEvaluation::Lazy => self - .lazy_default(db) + .lazy_default(db, visitor.env) .map(|ty| ty.materialize(db, materialization_kind, visitor).into()), }), ) } - fn to_instance(self, db: &'db dyn Db) -> Option> { - let bound_or_constraints = match self.bound_or_constraints(db)? { + fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + let bound_or_constraints = match self.bound_or_constraints(db, env)? { TypeVarBoundOrConstraints::UpperBound(upper_bound) => upper_bound - .to_instance(db)? + .to_instance(db, env)? .map(TypeVarBoundOrConstraints::UpperBound), TypeVarBoundOrConstraints::Constraints(constraints) => constraints - .to_instance(db)? + .to_instance(db, env)? .map(TypeVarBoundOrConstraints::Constraints), }; let identity = TypeVarIdentity::new( @@ -391,6 +415,7 @@ impl<'db> TypeVarInstance<'db> { fn type_is_self_referential( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, visitor: &TypeVarDefaultVisitor<'db>, ) -> bool { @@ -399,6 +424,7 @@ impl<'db> TypeVarInstance<'db> { #[derive(Copy, Clone)] struct State<'db, 'a> { db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, visitor: &'a TypeVarDefaultVisitor<'db>, seen_typevars: &'a RefCell>>, seen_type_aliases: &'a RefCell>, @@ -409,7 +435,9 @@ impl<'db> TypeVarInstance<'db> { typevar: TypeVarInstance<'db>, self_identity: TypeVarIdentity<'db>, ) -> bool { - if typevar.identity(state.db) == self_identity { + let db = state.db; + + if typevar.identity(db) == self_identity { return true; } @@ -418,7 +446,7 @@ impl<'db> TypeVarInstance<'db> { } typevar - .default_type_impl(state.db, state.visitor) + .default_type_impl(db, state.env, state.visitor) .is_some_and(|default_ty| { type_is_self_referential_impl(state, default_ty, self_identity) }) @@ -429,9 +457,10 @@ impl<'db> TypeVarInstance<'db> { type_alias: TypeAliasType<'db>, self_identity: TypeVarIdentity<'db>, ) -> bool { + let db = state.db; { let mut seen_type_aliases = state.seen_type_aliases.borrow_mut(); - let definition = type_alias.definition(state.db); + let definition = type_alias.definition(db); // A recursive alias can produce a new specialization every time its body is // expanded, so use its definition as the stable recursion key. if seen_type_aliases.contains(&definition) { @@ -440,27 +469,23 @@ impl<'db> TypeVarInstance<'db> { seen_type_aliases.push(definition); } - let value_type = if let Some(specialization) = type_alias.specialization(state.db) { + let value_type = if let Some(specialization) = type_alias.specialization(db) { if specialization - .types(state.db) + .types(db) .iter() .any(|ty| type_is_self_referential_impl(state, *ty, self_identity)) { return true; } - type_alias.value_type(state.db) - } else if let Some(generic_context) = type_alias.generic_context(state.db) - && generic_context.variables(state.db).any(|typevar| { - typevar_default_is_self_referential( - state, - typevar.typevar(state.db), - self_identity, - ) + type_alias.value_type(db) + } else if let Some(generic_context) = type_alias.generic_context(db) + && generic_context.variables(db).any(|typevar| { + typevar_default_is_self_referential(state, typevar.typevar(db), self_identity) }) { return true; } else { - type_alias.raw_value_type(state.db) + type_alias.raw_value_type(db) }; type_is_self_referential_impl(state, value_type, self_identity) @@ -471,10 +496,11 @@ impl<'db> TypeVarInstance<'db> { ty: Type<'db>, self_identity: TypeVarIdentity<'db>, ) -> bool { - any_over_type(state.db, ty, false, |inner_ty| match inner_ty { + let db = state.db; + any_over_type(db, state.env, ty, false, |inner_ty| match inner_ty { Type::TypeVar(bound_typevar) => typevar_default_is_self_referential( state, - bound_typevar.typevar(state.db), + bound_typevar.typevar(db), self_identity, ), Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => { @@ -495,6 +521,7 @@ impl<'db> TypeVarInstance<'db> { let state = State { db, + env, visitor, seen_typevars: &seen_typevars, seen_type_aliases: &seen_type_aliases, @@ -513,7 +540,8 @@ impl<'db> TypeVarInstance<'db> { )] fn lazy_bound_unchecked(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db)?; - let module = parsed_module(db, definition.file(db)).load(db); + let python_file = definition.python_file(db); + let module = parsed_module(db, python_file).load(db); let ty = match definition.kind(db) { // PEP 695 typevar DefinitionKind::TypeVar(typevar) => { @@ -532,10 +560,10 @@ impl<'db> TypeVarInstance<'db> { Some(ty) } - fn lazy_bound(self, db: &'db dyn Db) -> Option> { + fn lazy_bound(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { let bound = self.lazy_bound_unchecked(db)?; - if bound.has_typevar_or_typevar_instance(db) { + if bound.has_typevar_or_typevar_instance(db, env) { return None; } @@ -552,14 +580,16 @@ impl<'db> TypeVarInstance<'db> { )] fn lazy_constraints_unchecked(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db)?; - let module = parsed_module(db, definition.file(db)).load(db); + let python_file = definition.python_file(db); + let env = ProgramEnvironment::from_file(python_file); + let module = parsed_module(db, python_file).load(db); let constraints = match definition.kind(db) { // PEP 695 typevar DefinitionKind::TypeVar(typevar) => { let typevar_node = typevar.node(&module); let bound = definition_expression_type(db, definition, typevar_node.bound.as_ref()?); - if let Some(tuple) = bound.tuple_instance_spec(db) + if let Some(tuple) = bound.tuple_instance_spec(db, &env) && let Tuple::Fixed(tuple) = tuple.into_owned() { TypeVarConstraints::new(db, tuple.owned_elements()) @@ -587,13 +617,17 @@ impl<'db> TypeVarInstance<'db> { Some(constraints) } - fn lazy_constraints(self, db: &'db dyn Db) -> Option> { + fn lazy_constraints( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let constraints = self.lazy_constraints_unchecked(db)?; if constraints .elements(db) .iter() - .any(|ty| ty.has_typevar_or_typevar_instance(db)) + .any(|ty| ty.has_typevar_or_typevar_instance(db, env)) { return None; } @@ -650,7 +684,8 @@ impl<'db> TypeVarInstance<'db> { } let definition = self.definition(db)?; - let module = parsed_module(db, definition.file(db)).load(db); + let python_file = definition.python_file(db); + let module = parsed_module(db, python_file).load(db); let ty = match definition.kind(db) { // PEP 695 typevar DefinitionKind::TypeVar(typevar) => { @@ -691,14 +726,15 @@ impl<'db> TypeVarInstance<'db> { Some(ty) } - fn lazy_default(self, db: &'db dyn Db) -> Option> { + fn lazy_default(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { let visitor = TypeVarDefaultVisitor::new(None); - self.lazy_default_impl(db, &visitor) + self.lazy_default_impl(db, env, &visitor) } fn lazy_default_impl( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, visitor: &TypeVarDefaultVisitor<'db>, ) -> Option> { let default = self.lazy_default_unchecked(db)?; @@ -707,7 +743,7 @@ impl<'db> TypeVarInstance<'db> { // (https://typing.python.org/en/latest/spec/generics.html#defaults-for-type-parameters). // Here we simply check for non-self-referential. // TODO: We should also check for non-forward references. - if self.type_is_self_referential(db, default, visitor) { + if self.type_is_self_referential(db, env, default, visitor) { return None; } @@ -722,7 +758,7 @@ impl<'db> TypeVarInstance<'db> { return None; } let typevar_definition = self.definition(db)?; - let index = semantic_index(db, typevar_definition.file(db)); + let index = semantic_index(db, typevar_definition.python_file(db)); let (_, child) = index .child_scopes(typevar_definition.file_scope(db)) .next()?; @@ -834,14 +870,19 @@ pub(crate) fn max_typevar_freshness_matching_generic_context<'db>( types: impl IntoIterator>, generic_context: GenericContext<'db>, ) -> Option { - struct MatchingFreshnessCollector<'db> { + struct MatchingFreshnessCollector<'a, 'db> { + env: &'a ProgramEnvironment<'db>, base_identities: FxHashSet>, recursion_guard: TypeCollector<'db>, max_freshness: Cell>, } - impl<'db> MatchingFreshnessCollector<'db> { - fn new(db: &'db dyn Db, generic_context: GenericContext<'db>) -> Self { + impl<'a, 'db> MatchingFreshnessCollector<'a, 'db> { + fn new( + db: &'db dyn Db, + env: &'a ProgramEnvironment<'db>, + generic_context: GenericContext<'db>, + ) -> Self { let base_identities = generic_context .variables(db) .map(|typevar| { @@ -851,6 +892,7 @@ pub(crate) fn max_typevar_freshness_matching_generic_context<'db>( }) .collect(); Self { + env, base_identities, recursion_guard: TypeCollector::default(), max_freshness: Cell::default(), @@ -858,7 +900,11 @@ pub(crate) fn max_typevar_freshness_matching_generic_context<'db>( } } - impl<'db> TypeVisitor<'db> for MatchingFreshnessCollector<'db> { + impl<'db> TypeVisitor<'db> for MatchingFreshnessCollector<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { false } @@ -884,7 +930,8 @@ pub(crate) fn max_typevar_freshness_matching_generic_context<'db>( } } - let collector = MatchingFreshnessCollector::new(db, generic_context); + let env = ProgramEnvironment::from_program(generic_context.program(db)); + let collector = MatchingFreshnessCollector::new(db, &env, generic_context); for ty in types { collector.visit_type(db, ty); } @@ -965,7 +1012,7 @@ impl<'db> BoundTypeVarInstance<'db> { } pub(crate) fn kind(self, db: &'db dyn Db) -> TypeVarKind { - self.typevar(db).kind(db) + self.identity(db).kind(db) } pub(crate) fn is_paramspec(self, db: &'db dyn Db) -> bool { @@ -991,11 +1038,16 @@ impl<'db> BoundTypeVarInstance<'db> { self.kind(db) ); + let env = ProgramEnvironment::from_program(self.binding_context(db).program(db)); let upper_bound = TypeVarBoundOrConstraints::UpperBound(match kind { - ParamSpecAttrKind::Args => Type::homogeneous_tuple(db, Type::object()), + ParamSpecAttrKind::Args => Type::homogeneous_tuple(db, &env, Type::object()), ParamSpecAttrKind::Kwargs => KnownClass::Dict - .to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]) - .top_materialization(db), + .to_specialized_instance( + db, + &env, + &[KnownClass::Str.to_instance(db, &env), Type::any()], + ) + .top_materialization(db, &env), }); let typevar = self.typevar(db); @@ -1054,7 +1106,12 @@ impl<'db> BoundTypeVarInstance<'db> { /// Create a new PEP 695 type variable that can be used in signatures /// of synthetic generic functions. - pub(crate) fn synthetic(db: &'db dyn Db, name: Name, variance: TypeVarVariance) -> Self { + pub(crate) fn synthetic( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: Name, + variance: TypeVarVariance, + ) -> Self { let identity = TypeVarIdentity::new( db, name, @@ -1071,7 +1128,7 @@ impl<'db> BoundTypeVarInstance<'db> { Self::new( db, typevar, - BindingContext::Synthetic, + BindingContext::Synthetic(env.program(db)), None, TypeVarNonce::NONE, ) @@ -1106,8 +1163,9 @@ impl<'db> BoundTypeVarInstance<'db> { db: &'db dyn Db, f: impl FnOnce(Option>) -> Option>, ) -> Self { + let env = ProgramEnvironment::from_program(self.binding_context(db).program(db)); let typevar = self.typevar(db); - let bound_or_constraints = f(typevar.bound_or_constraints(db)); + let bound_or_constraints = f(typevar.bound_or_constraints(db, &env)); let typevar = TypeVarInstance::new( db, typevar.identity(db), @@ -1136,13 +1194,14 @@ impl<'db> BoundTypeVarInstance<'db> { Some(explicit_variance) => explicit_variance.compose(polarity), None => match self.binding_context(db) { BindingContext::Definition(definition) => polarity.compose_thunk(|| { - match binding_type(db, definition).variance_of(db, self.identity(db)) { + let env = ProgramEnvironment::from_definition(definition); + match binding_type(db, definition).variance_of(db, &env, self.identity(db)) { // When both directions are valid, the typing spec selects covariance. TypeVarVariance::Bivariant => TypeVarVariance::Covariant, variance => variance, } }), - BindingContext::Synthetic => TypeVarVariance::Invariant, + BindingContext::Synthetic(_) => TypeVarVariance::Invariant, }, } } @@ -1155,7 +1214,7 @@ impl<'db> BoundTypeVarInstance<'db> { self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Type<'db> { let mapped_specialization_type = |specialization: &ApplySpecialization<'a, 'db>| -> Option> { @@ -1190,6 +1249,7 @@ impl<'db> BoundTypeVarInstance<'db> { if mapped == Type::TypeVar(self) { mapped } else { + let env = visitor.env; // Materialization uses a different mapping mode. Reuse of the outer // visitor can incorrectly hit a cache entry from specialization. let materialization_visitor = visitor.for_new_materialization_root(); @@ -1202,10 +1262,9 @@ impl<'db> BoundTypeVarInstance<'db> { mapped, materialized, ) - && let Some(upper_bound) = - self.typevar(db).top_materialized_upper_bound(db) + && let Some(upper_bound) = self.top_materialized_upper_bound(db) { - IntersectionType::from_two_elements(db, materialized, upper_bound) + IntersectionType::from_two_elements(db, env, materialized, upper_bound) } else { materialized } @@ -1213,7 +1272,7 @@ impl<'db> BoundTypeVarInstance<'db> { }) .unwrap_or(Type::TypeVar(self)), TypeMapping::BindSelf(binding) => { - if binding.should_bind(db, self) { + if binding.should_bind(db, visitor.env, self) { binding.self_type() } else { Type::TypeVar(self) @@ -1255,6 +1314,38 @@ impl<'db> BoundTypeVarInstance<'db> { } } } + + /// Returns the static upper bound used when materializing a gradual type argument. + /// + /// Constraints are unioned only when materializing an exposed member, where their union is a + /// valid conservative upper bound. A bound may recursively refer to its own generic class, + /// either directly or through other bounds. Such a bound has no finite static top + /// materialization, so recover from its cycle without applying an upper bound. + pub(super) fn top_materialized_upper_bound(self, db: &'db dyn Db) -> Option> { + #[salsa::tracked( + returns(copy), + cycle_result=|_, _, _| None, + heap_size=ruff_memory_usage::heap_size + )] + fn top_materialized_upper_bound_inner<'db>( + db: &'db dyn Db, + bound_typevar: BoundTypeVarInstance<'db>, + ) -> Option> { + let env = + ProgramEnvironment::from_program(bound_typevar.binding_context(db).program(db)); + + bound_typevar + .typevar(db) + .bound_or_constraints(db, &env) + .map(|bound_or_constraints| { + bound_or_constraints + .as_type(db, &env) + .top_materialization(db, &env) + }) + } + + top_materialized_upper_bound_inner(db, self) + } } pub(super) fn walk_bound_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -1296,7 +1387,7 @@ impl<'db> BoundTypeVarInstance<'db> { self, db: &'db dyn Db, materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { Self::new( db, @@ -1313,10 +1404,10 @@ impl<'db> BoundTypeVarInstance<'db> { db: &'db dyn Db, nonce: TypeVarNonce, type_mapping: &TypeMapping<'_, 'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let typevar = self.typevar(db); - let bound_or_constraints = typevar.bound_or_constraints(db); + let bound_or_constraints = typevar.bound_or_constraints(db, visitor.env); let default = self.default_type(db); if bound_or_constraints.is_none() && default.is_none() { @@ -1353,8 +1444,12 @@ impl<'db> BoundTypeVarInstance<'db> { ) } - pub(super) fn to_instance(self, db: &'db dyn Db) -> Option> { - Some(self.typevar(db).to_instance(db)?.map(|typevar| { + pub(super) fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { + Some(self.typevar(db).to_instance(db, env)?.map(|typevar| { Self::new( db, typevar, @@ -1433,14 +1528,19 @@ fn lazy_bound_cycle_recover<'db>( cycle: &salsa::Cycle, previous: &Option>, current: Option>, - _typevar: TypeVarInstance<'db>, + typevar: TypeVarInstance<'db>, ) -> Option> { // Normalize the bounds/constraints to ensure cycle convergence. - match (previous, current) { - (Some(prev), Some(current)) => Some(current.cycle_normalized(db, *prev, cycle)), - (None, Some(current)) => Some(current.recursive_type_normalized(db, cycle)), - (_, None) => None, - } + let current = current?; + let python_file = typevar + .definition(db) + .expect("a lazy TypeVar bound must have a source definition") + .python_file(db); + let env = ProgramEnvironment::from_file(python_file); + Some(match previous { + Some(prev) => current.cycle_normalized(db, &env, *prev, cycle), + None => current.recursive_type_normalized(db, &env, cycle), + }) } #[allow(clippy::trivially_copy_pass_by_ref)] @@ -1450,14 +1550,19 @@ fn lazy_constraints_cycle_recover<'db>( cycle: &salsa::Cycle, previous: &Option>, current: Option>, - _typevar: TypeVarInstance<'db>, + typevar: TypeVarInstance<'db>, ) -> Option> { // Normalize the bounds/constraints to ensure cycle convergence. - match (previous, current) { - (Some(prev), Some(constraints)) => Some(constraints.cycle_normalized(db, *prev, cycle)), - (None, Some(current)) => Some(current.recursive_type_normalized(db, cycle)), - (_, None) => None, - } + let current = current?; + let python_file = typevar + .definition(db) + .expect("lazy TypeVar constraints must have a source definition") + .python_file(db); + let env = ProgramEnvironment::from_file(python_file); + Some(match previous { + Some(prev) => current.cycle_normalized(db, &env, *prev, cycle), + None => current.recursive_type_normalized(db, &env, cycle), + }) } #[expect(clippy::ref_option)] @@ -1465,15 +1570,20 @@ fn lazy_default_cycle_recover<'db>( db: &'db dyn Db, cycle: &salsa::Cycle, previous_default: &Option>, - default: Option>, - _typevar: TypeVarInstance<'db>, + current: Option>, + typevar: TypeVarInstance<'db>, ) -> Option> { // Normalize the default to ensure cycle convergence. - match (previous_default, default) { - (Some(prev), Some(default)) => Some(default.cycle_normalized(db, *prev, cycle)), - (None, Some(default)) => Some(default.recursive_type_normalized(db, cycle)), - (_, None) => None, - } + let current = current?; + let python_file = typevar + .definition(db) + .expect("a lazy TypeVar default must have a source definition") + .python_file(db); + let env = ProgramEnvironment::from_file(python_file); + Some(match previous_default { + Some(prev) => current.cycle_normalized(db, &env, *prev, cycle), + None => current.recursive_type_normalized(db, &env, cycle), + }) } /// Where a type variable is bound and usable. @@ -1482,8 +1592,9 @@ pub enum BindingContext<'db> { /// The definition of the generic class, function, or type alias that binds this typevar. Definition(Definition<'db>), /// The typevar is synthesized internally, and is not associated with a particular definition - /// in the source, but is still bound and eligible for specialization inference. - Synthetic, + /// in the source, but is still bound and eligible for specialization inference. Its program + /// identifies the environment that cannot otherwise be recovered from a source definition. + Synthetic(Program), } impl<'db> From> for BindingContext<'db> { @@ -1496,7 +1607,14 @@ impl<'db> BindingContext<'db> { pub(crate) fn definition(self) -> Option> { match self { BindingContext::Definition(definition) => Some(definition), - BindingContext::Synthetic => None, + BindingContext::Synthetic(_) => None, + } + } + + pub(crate) fn program(self, db: &'db dyn Db) -> Program { + match self { + Self::Definition(definition) => definition.program(db), + Self::Synthetic(program) => program, } } @@ -1673,14 +1791,21 @@ fn bound_typevar_default_type<'db>( db: &'db dyn Db, bound_typevar: BoundTypeVarInstance<'db>, ) -> Option> { + let typevar = bound_typevar.typevar(db); + typevar._default(db)?; + let definition = typevar + .definition(db) + .expect("a bound TypeVar with a default must have a source definition"); + let env = ProgramEnvironment::from_definition(definition); + let default = typevar.default_type(db, &env)?; let binding_context = bound_typevar.binding_context(db); - bound_typevar.typevar(db).default_type(db).map(|ty| { - ty.apply_type_mapping( - db, - &TypeMapping::BindLegacyTypevars(binding_context), - TypeContext::default(), - ) - }) + + Some(default.apply_type_mapping( + db, + &env, + &TypeMapping::BindLegacyTypevars(binding_context), + TypeContext::default(), + )) } #[expect(clippy::ref_option)] @@ -1689,13 +1814,19 @@ fn bound_typevar_default_type_cycle_recover<'db>( cycle: &salsa::Cycle, previous_default: &Option>, default: Option>, - _bound_typevar: BoundTypeVarInstance<'db>, + bound_typevar: BoundTypeVarInstance<'db>, ) -> Option> { - match (previous_default, default) { - (Some(previous), Some(default)) => Some(default.cycle_normalized(db, *previous, cycle)), - (None, Some(default)) => Some(default.recursive_type_normalized(db, cycle)), - (_, None) => None, - } + let default = default?; + let python_file = bound_typevar + .typevar(db) + .definition(db) + .expect("a bound TypeVar with a default must have a source definition") + .python_file(db); + let env = ProgramEnvironment::from_file(python_file); + Some(match previous_default { + Some(previous) => default.cycle_normalized(db, &env, *previous, cycle), + None => default.recursive_type_normalized(db, &env, cycle), + }) } /// Whether a typevar default is eagerly specified or lazily evaluated. @@ -1751,15 +1882,19 @@ fn walk_type_var_constraints<'db, V: visitor::TypeVisitor<'db> + ?Sized>( } impl<'db> TypeVarConstraints<'db> { - pub(super) fn as_type(self, db: &'db dyn Db) -> Type<'db> { - UnionType::from_elements(db, self.elements(db)) + pub(super) fn as_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { + UnionType::from_elements(db, env, self.elements(db)) } - fn to_instance(self, db: &'db dyn Db) -> Option>> { + fn to_instance( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option>> { let mut instance_elements = Vec::new(); let mut is_exact = true; for ty in self.elements(db) { - let projection = ty.to_instance(db)?; + let projection = ty.to_instance(db, env)?; is_exact &= projection.is_exact(); instance_elements.push(projection.into_inner()); } @@ -1785,9 +1920,10 @@ impl<'db> TypeVarConstraints<'db> { pub(crate) fn map_with_boundness_and_qualifiers( self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, ) -> PlaceAndQualifiers<'db> { - let mut builder = UnionBuilder::new(db); + let mut builder = UnionBuilder::new(db, env); let mut qualifiers = TypeQualifiers::empty(); let mut all_unbound = true; @@ -1843,7 +1979,7 @@ impl<'db> TypeVarConstraints<'db> { self, db: &'db dyn Db, materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let materialized = self .elements(db) @@ -1857,7 +1993,7 @@ impl<'db> TypeVarConstraints<'db> { self, db: &'db dyn Db, type_mapping: &TypeMapping<'_, 'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { let mapped = self .elements(db) @@ -1871,7 +2007,13 @@ impl<'db> TypeVarConstraints<'db> { /// removing divergent types introduced by the cycle. /// /// See [`Type::cycle_normalized`] for more details on how this works. - fn cycle_normalized(self, db: &'db dyn Db, previous: Self, cycle: &salsa::Cycle) -> Self { + fn cycle_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + previous: Self, + cycle: &salsa::Cycle, + ) -> Self { let current_elements = self.elements(db); let prev_elements = previous.elements(db); TypeVarConstraints::new( @@ -1879,7 +2021,7 @@ impl<'db> TypeVarConstraints<'db> { current_elements .iter() .zip(prev_elements.iter()) - .map(|(ty, prev_ty)| ty.cycle_normalized(db, *prev_ty, cycle)) + .map(|(ty, prev_ty)| ty.cycle_normalized(db, env, *prev_ty, cycle)) .collect::>(), ) } @@ -1887,8 +2029,13 @@ impl<'db> TypeVarConstraints<'db> { /// Normalize recursive types for cycle recovery when there's no previous value. /// /// See [`Type::recursive_type_normalized`] for more details. - fn recursive_type_normalized(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self { - self.map(db, |ty| ty.recursive_type_normalized(db, cycle)) + fn recursive_type_normalized( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + cycle: &salsa::Cycle, + ) -> Self { + self.map(db, |ty| ty.recursive_type_normalized(db, env, cycle)) } } @@ -1904,7 +2051,9 @@ pub(super) fn walk_type_var_bounds<'db, V: visitor::TypeVisitor<'db> + ?Sized>( visitor: &V, ) { match bounds { - TypeVarBoundOrConstraints::UpperBound(bound) => visitor.visit_type(db, bound), + TypeVarBoundOrConstraints::UpperBound(bound) => { + visitor.visit_type(db, bound); + } TypeVarBoundOrConstraints::Constraints(constraints) => { walk_type_var_constraints(db, constraints, visitor); } @@ -1916,7 +2065,7 @@ impl<'db> TypeVarBoundOrConstraints<'db> { self, db: &'db dyn Db, materialization_kind: MaterializationKind, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { TypeVarBoundOrConstraints::UpperBound(bound) => TypeVarBoundOrConstraints::UpperBound( @@ -1936,7 +2085,7 @@ impl<'db> TypeVarBoundOrConstraints<'db> { self, db: &'db dyn Db, type_mapping: &TypeMapping<'_, 'db>, - visitor: &ApplyTypeMappingVisitor<'db>, + visitor: &ApplyTypeMappingVisitor<'_, 'db>, ) -> Self { match self { TypeVarBoundOrConstraints::UpperBound(bound) => TypeVarBoundOrConstraints::UpperBound( @@ -1958,10 +2107,10 @@ impl<'db> TypeVarBoundOrConstraints<'db> { /// constraints provides a conservative upper bound, but it loses precision. And for many use /// cases, it's more efficient to just map over the constraint types directly, rather than /// building a union out of them and mapping over that. - pub(crate) fn as_type(self, db: &'db dyn Db) -> Type<'db> { + pub(crate) fn as_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { TypeVarBoundOrConstraints::UpperBound(bound) => bound, - TypeVarBoundOrConstraints::Constraints(constraints) => constraints.as_type(db), + TypeVarBoundOrConstraints::Constraints(constraints) => constraints.as_type(db, env), } } } @@ -1989,6 +2138,7 @@ mod tests { fn bound_typevar<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, name: &'static str, kind: TypeVarKind, bound_or_constraints: Option>, @@ -2002,31 +2152,45 @@ mod tests { Some(TypeVarVariance::Invariant), None, ); - BoundTypeVarInstance::new(db, typevar, BindingContext::Synthetic, None, freshness) + BoundTypeVarInstance::new( + db, + typevar, + BindingContext::Synthetic(env.program(db)), + None, + freshness, + ) } #[test] fn typevar_set_empty_set_is_none() { let db = setup_db(); - let typevar = - BoundTypeVarInstance::synthetic(&db, Name::new_static("T"), TypeVarVariance::Invariant); - let inferable = TypeVarSet::from_typevars(&db, []); + let db = &db; + let env = db.program_environment(); + let typevar = BoundTypeVarInstance::synthetic( + db, + &env, + Name::new_static("T"), + TypeVarVariance::Invariant, + ); + let inferable = TypeVarSet::from_typevars(db, []); assert_eq!(inferable, TypeVarSet::None); - assert_eq!(inferable.iter(&db).count(), 0); - assert!(!typevar.is_inferable(&db, inferable)); - assert!(!typevar.identity(&db).is_inferable(&db, inferable)); + assert_eq!(inferable.iter(db).count(), 0); + assert!(!typevar.is_inferable(db, inferable)); + assert!(!typevar.identity(db).is_inferable(db, inferable)); } #[test] fn typevar_set_keeps_first_instance_for_each_identity() { let mut db = setup_db(); db.clear_salsa_events(); + let env = db.program_environment(); // The synthetic lazy bound has no definition, so it is equivalent to the implicit // `object` upper bound represented eagerly below. let lazy = bound_typevar( &db, + &env, "T", TypeVarKind::Pep695TypeVar, Some(TypeVarBoundOrConstraintsEvaluation::LazyUpperBound), @@ -2034,15 +2198,24 @@ mod tests { ); let eager = bound_typevar( &db, + &env, "T", TypeVarKind::Pep695TypeVar, Some(TypeVarBoundOrConstraints::UpperBound(Type::object()).into()), TypeVarNonce::NONE, ); - let u = - BoundTypeVarInstance::synthetic(&db, Name::new_static("U"), TypeVarVariance::Invariant); - let v = - BoundTypeVarInstance::synthetic(&db, Name::new_static("V"), TypeVarVariance::Invariant); + let u = BoundTypeVarInstance::synthetic( + &db, + &env, + Name::new_static("U"), + TypeVarVariance::Invariant, + ); + let v = BoundTypeVarInstance::synthetic( + &db, + &env, + Name::new_static("V"), + TypeVarVariance::Invariant, + ); assert_ne!(lazy, eager); assert_eq!(lazy.identity(&db), eager.identity(&db)); @@ -2066,52 +2239,57 @@ mod tests { #[test] fn typevar_set_distinguishes_fresh_and_paramspec_identities() { let db = setup_db(); + let db = &db; + let env = db.program_environment(); let typevar = bound_typevar( - &db, + db, + &env, "T", TypeVarKind::Pep695TypeVar, None, TypeVarNonce::NONE, ); let fresh = bound_typevar( - &db, + db, + &env, "T", TypeVarKind::Pep695TypeVar, None, TypeVarNonce::NONE.increment(), ); let paramspec = bound_typevar( - &db, + db, + &env, "P", TypeVarKind::Pep695ParamSpec, None, TypeVarNonce::NONE, ); - let args = paramspec.with_paramspec_attr(&db, ParamSpecAttrKind::Args); - let kwargs = paramspec.with_paramspec_attr(&db, ParamSpecAttrKind::Kwargs); + let args = paramspec.with_paramspec_attr(db, ParamSpecAttrKind::Args); + let kwargs = paramspec.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs); - let inferable = TypeVarSet::from_typevars(&db, [typevar, fresh, args, kwargs]); + let inferable = TypeVarSet::from_typevars(db, [typevar, fresh, args, kwargs]); assert_eq!( - inferable.iter(&db).collect::>(), + inferable.iter(db).collect::>(), [typevar, fresh, args, kwargs] ); - assert!(typevar.is_inferable(&db, inferable)); - assert!(fresh.is_inferable(&db, inferable)); - assert!(args.is_inferable(&db, inferable)); - assert!(kwargs.is_inferable(&db, inferable)); - assert!(!paramspec.is_inferable(&db, inferable)); + assert!(typevar.is_inferable(db, inferable)); + assert!(fresh.is_inferable(db, inferable)); + assert!(args.is_inferable(db, inferable)); + assert!(kwargs.is_inferable(db, inferable)); + assert!(!paramspec.is_inferable(db, inferable)); - let paramspec_only = TypeVarSet::from_typevars(&db, [paramspec]); + let paramspec_only = TypeVarSet::from_typevars(db, [paramspec]); assert!( - args.identity(&db) - .without_paramspec_attr(&db) - .is_inferable(&db, paramspec_only) + args.identity(db) + .without_paramspec_attr(db) + .is_inferable(db, paramspec_only) ); assert!( kwargs - .identity(&db) - .without_paramspec_attr(&db) - .is_inferable(&db, paramspec_only) + .identity(db) + .without_paramspec_attr(db) + .is_inferable(db, paramspec_only) ); } } diff --git a/crates/ty_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs index 0676bcd8fa..9341dac670 100644 --- a/crates/ty_python_semantic/src/types/unpacker.rs +++ b/crates/ty_python_semantic/src/types/unpacker.rs @@ -1,6 +1,9 @@ +use crate::ProgramEnvironment; use std::borrow::Cow; use ruff_db::parsed::ParsedModuleRef; + +use ruff_db::PythonFile; use rustc_hash::FxHashMap; use ruff_python_ast::visitor::{self, Visitor}; @@ -38,11 +41,20 @@ impl<'ast> Visitor<'ast> for UnknownTargetCollector<'_, '_> { impl<'db, 'ast> Unpacker<'db, 'ast> { pub(crate) fn new( db: &'db dyn Db, + env: &'ast ProgramEnvironment<'db>, target_scope: ScopeId<'db>, + python_file: PythonFile<'db>, module: &'ast ParsedModuleRef, ) -> Self { Self { - context: InferContext::new(db, target_scope, module), + context: InferContext::new( + db, + env, + target_scope, + python_file.file(db), + python_file, + module, + ), targets: FxHashMap::default(), } } @@ -57,13 +69,17 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { /// Unpack the value to the target expression. pub(crate) fn unpack(&mut self, target: &ast::Expr, value: UnpackValue<'db>) { + let db = self.db(); debug_assert!( matches!(target, ast::Expr::List(_) | ast::Expr::Tuple(_)), "Unpacking target must be a list or tuple expression" ); - let value_inference = - infer_expression_types(self.db(), value.expression(), TypeContext::default()); + let value_inference = infer_expression_types( + self.context.db(), + value.expression(), + TypeContext::default(), + ); let value_expr = value.expression().node_ref(self.db()).node(self.module()); if matches!(value.kind(), UnpackKind::Assign) @@ -82,27 +98,33 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { value_type } } - UnpackKind::Iterable { mode } => value_type - .try_iterate_with_mode(self.db(), mode) - .map(|tuple| tuple.homogeneous_element_type(self.db())) - .unwrap_or_else(|err| { - err.report_diagnostic( - &self.context, - value_type, - value.as_any_node_ref(self.db(), self.module()), - ); - err.fallback_element_type(self.db()) - }), - UnpackKind::ContextManager { mode } => value_type - .try_enter_with_mode(self.db(), mode) - .unwrap_or_else(|err| { - err.report_diagnostic( - &self.context, - value_type, - value.as_any_node_ref(self.db(), self.module()), - ); - err.fallback_enter_type(self.db()) - }), + UnpackKind::Iterable { mode } => { + let env = self.context.program_environment(); + value_type + .try_iterate_with_mode(db, env, mode) + .map(|tuple| tuple.homogeneous_element_type(db, env)) + .unwrap_or_else(|err| { + err.report_diagnostic( + &self.context, + value_type, + value.as_any_node_ref(self.db(), self.module()), + ); + err.fallback_element_type(db, env) + }) + } + UnpackKind::ContextManager { mode } => { + let env = self.context.program_environment(); + value_type + .try_enter_with_mode(db, env, mode) + .unwrap_or_else(|err| { + err.report_diagnostic( + &self.context, + value_type, + value.as_any_node_ref(self.db(), self.module()), + ); + err.fallback_enter_type(db, env) + }) + } }; self.unpack_inner(target, value_expr.into(), value_type); @@ -187,6 +209,7 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { value_expr: AnyNodeRef<'_>, value_ty: Type<'db>, ) { + let db = self.db(); match target { ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => { self.targets.insert(target.into(), value_ty); @@ -202,7 +225,8 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { } None => TupleLength::Fixed(elts.len()), }; - let mut unpacker = TupleUnpacker::new(self.db(), target_len); + let env = self.context.program_environment(); + let mut unpacker = TupleUnpacker::new(db, env, target_len); // N.B. `Type::try_iterate` internally handles unions, but in a lossy way. // For our purposes here, we get better error messages and more precise inference @@ -215,9 +239,9 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { }; for ty in unpack_types.iter().copied() { - let tuple = ty.try_iterate(self.db()).unwrap_or_else(|err| { + let tuple = ty.try_iterate(db, env).unwrap_or_else(|err| { err.report_diagnostic(&self.context, ty, value_expr); - Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(self.db()))) + Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(db, env))) }); if let Err(err) = unpacker.unpack_tuple(tuple.as_ref()) { @@ -328,12 +352,13 @@ impl<'db> UnpackResult<'db> { pub(crate) fn cycle_normalized( mut self, db: &'db dyn Db, + env: &ProgramEnvironment<'db>, previous_cycle_result: &UnpackResult<'db>, cycle: &salsa::Cycle, ) -> Self { for (expr, ty) in &mut self.targets { let previous_ty = previous_cycle_result.expression_type(*expr); - *ty = ty.cycle_normalized(db, previous_ty, cycle); + *ty = ty.cycle_normalized(db, env, previous_ty, cycle); } self diff --git a/crates/ty_python_semantic/src/types/variance.rs b/crates/ty_python_semantic/src/types/variance.rs index 159ebe6561..4e5ca4432b 100644 --- a/crates/ty_python_semantic/src/types/variance.rs +++ b/crates/ty_python_semantic/src/types/variance.rs @@ -1,4 +1,5 @@ -use crate::{Db, types::BoundTypeVarIdentity}; +use crate::Db; +use crate::{ProgramEnvironment, types::BoundTypeVarIdentity}; #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, get_size2::GetSize)] pub enum TypeVarVariance { @@ -133,7 +134,12 @@ pub(crate) trait VarianceInferable<'db>: Sized { /// /// Sometimes the recursive calls will be in positions where you need to /// specify a non-covariant polarity. See `with_polarity` for more details. - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance; + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance; /// Creates a `VarianceInferable` that applies `polarity` (see /// `TypeVarVariance::compose`) to the result of variance inference on the @@ -165,12 +171,17 @@ impl<'db, T> VarianceInferable<'db> for WithPolarity where T: VarianceInferable<'db>, { - fn variance_of(self, db: &'db dyn Db, typevar: BoundTypeVarIdentity<'db>) -> TypeVarVariance { + fn variance_of( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typevar: BoundTypeVarIdentity<'db>, + ) -> TypeVarVariance { let WithPolarity { variance_inferable, polarity, } = self; - polarity.compose_thunk(|| variance_inferable.variance_of(db, typevar)) + polarity.compose_thunk(|| variance_inferable.variance_of(db, env, typevar)) } } diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index 3d72f97774..b7c9b0bf97 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -1,35 +1,33 @@ +use crate::Db; +use crate::ProgramEnvironment; use std::cell::{Cell, RefCell}; use std::hash::Hash; use rustc_hash::{FxBuildHasher, FxHashSet}; use smallvec::SmallVec; -use crate::{ - Db, - types::{ - BoundMethodType, BoundSuperType, BoundTypeVarInstance, CallableType, EnumComplementType, - GenericAlias, IntersectionType, KnownBoundMethodType, KnownInstanceType, - NominalInstanceType, PropertyInstanceType, ProtocolInstanceType, StaticClassLiteral, - SubclassOfType, Type, TypeAliasType, TypeFormType, TypeGuardType, TypeIsType, - TypedDictType, UnionType, - bound_super::walk_bound_super_type, - callable::walk_callable_type, - class::walk_generic_alias, - cyclic::ActiveRecursionDetector, - function::{FunctionType, walk_function_type}, - instance::{walk_nominal_instance_type, walk_protocol_instance_type}, - known_instance::walk_known_instance_type, - method::{walk_bound_method_type, walk_method_wrapper_type}, - newtype::{NewType, walk_newtype_instance_type}, - protocol_class::walk_protocol_instance_interface, - set_theoretic::{walk_intersection_type, walk_union}, - subclass_of::walk_subclass_of_type, - type_alias::walk_type_alias_type, - type_form::walk_typeform_type, - typed_dict::walk_typed_dict_type, - typevar::{TypeVarInstance, walk_bound_type_var_type, walk_type_var_type}, - walk_property_instance_type, walk_typeguard_type, walk_typeis_type, - }, +use crate::types::{ + BoundMethodType, BoundSuperType, BoundTypeVarInstance, CallableType, EnumComplementType, + GenericAlias, IntersectionType, KnownBoundMethodType, KnownInstanceType, NominalInstanceType, + PropertyInstanceType, ProtocolInstanceType, StaticClassLiteral, SubclassOfType, Type, + TypeAliasType, TypeFormType, TypeGuardType, TypeIsType, TypedDictType, UnionType, + bound_super::walk_bound_super_type, + callable::walk_callable_type, + class::walk_generic_alias, + cyclic::ActiveRecursionDetector, + function::{FunctionType, walk_function_type}, + instance::{walk_nominal_instance_type, walk_protocol_instance_type}, + known_instance::walk_known_instance_type, + method::{walk_bound_method_type, walk_method_wrapper_type}, + newtype::{NewType, walk_newtype_instance_type}, + protocol_class::walk_protocol_instance_interface, + set_theoretic::{walk_intersection_type, walk_union}, + subclass_of::walk_subclass_of_type, + type_alias::walk_type_alias_type, + type_form::walk_typeform_type, + typed_dict::walk_typed_dict_type, + typevar::{TypeVarInstance, walk_bound_type_var_type, walk_type_var_type}, + walk_property_instance_type, walk_typeguard_type, walk_typeis_type, }; /// A visitor trait that recurses into nested types. @@ -38,6 +36,8 @@ use crate::{ /// but it makes it easy for implementors of the trait to do so. /// See [`any_over_type`] for an example of how to do this. pub(crate) trait TypeVisitor<'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db>; + /// Should the visitor trigger inference of and visit lazily-inferred type attributes? fn should_visit_lazy_type_attributes(&self) -> bool; @@ -245,7 +245,9 @@ pub(super) fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>( visitor: &V, ) { match non_atomic_type { - NonAtomicType::FunctionLiteral(function) => visitor.visit_function_type(db, function), + NonAtomicType::FunctionLiteral(function) => { + visitor.visit_function_type(db, function); + } NonAtomicType::Intersection(intersection) => { visitor.visit_intersection_type(db, intersection); } @@ -253,31 +255,49 @@ pub(super) fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>( visitor.visit_enum_complement_type(db, complement); } NonAtomicType::Union(union) => visitor.visit_union_type(db, union), - NonAtomicType::BoundMethod(method) => visitor.visit_bound_method_type(db, method), - NonAtomicType::BoundSuper(bound_super) => visitor.visit_bound_super_type(db, bound_super), + NonAtomicType::BoundMethod(method) => { + visitor.visit_bound_method_type(db, method); + } + NonAtomicType::BoundSuper(bound_super) => { + visitor.visit_bound_super_type(db, bound_super); + } NonAtomicType::MethodWrapper(method_wrapper) => { visitor.visit_method_wrapper_type(db, method_wrapper); } - NonAtomicType::Callable(callable) => visitor.visit_callable_type(db, callable), - NonAtomicType::GenericAlias(alias) => visitor.visit_generic_alias_type(db, alias), + NonAtomicType::Callable(callable) => { + visitor.visit_callable_type(db, callable); + } + NonAtomicType::GenericAlias(alias) => { + visitor.visit_generic_alias_type(db, alias); + } NonAtomicType::KnownInstance(known_instance) => { visitor.visit_known_instance_type(db, known_instance); } - NonAtomicType::SubclassOf(subclass_of) => visitor.visit_subclass_of_type(db, subclass_of), - NonAtomicType::NominalInstance(nominal) => visitor.visit_nominal_instance_type(db, nominal), + NonAtomicType::SubclassOf(subclass_of) => { + visitor.visit_subclass_of_type(db, subclass_of); + } + NonAtomicType::NominalInstance(nominal) => { + visitor.visit_nominal_instance_type(db, nominal); + } NonAtomicType::PropertyInstance(property) => { visitor.visit_property_instance_type(db, property); } NonAtomicType::TypeIs(type_is) => visitor.visit_typeis_type(db, type_is), - NonAtomicType::TypeGuard(type_guard) => visitor.visit_typeguard_type(db, type_guard), - NonAtomicType::TypeForm(typeform) => visitor.visit_typeform_type(db, typeform), + NonAtomicType::TypeGuard(type_guard) => { + visitor.visit_typeguard_type(db, type_guard); + } + NonAtomicType::TypeForm(typeform) => { + visitor.visit_typeform_type(db, typeform); + } NonAtomicType::TypeVar(bound_typevar) => { visitor.visit_bound_type_var_type(db, bound_typevar); } NonAtomicType::ProtocolInstance(protocol) => { visitor.visit_protocol_instance_type(db, protocol); } - NonAtomicType::TypedDict(typed_dict) => visitor.visit_typed_dict_type(db, typed_dict), + NonAtomicType::TypedDict(typed_dict) => { + visitor.visit_typed_dict_type(db, typed_dict); + } NonAtomicType::TypeAlias(alias) => { visitor.visit_type_alias_type(db, alias); } @@ -405,14 +425,19 @@ impl DynamicContent { /// /// Walking `Exact[int]` can skip its exact back-edge. Walking `Growing[int]` is indeterminate /// because each recursive edge creates a new specialization. -pub(super) fn non_any_dynamic_content<'db>(db: &'db dyn Db, ty: Type<'db>) -> DynamicContent { - struct DynamicContentVisitor<'db> { +pub(super) fn non_any_dynamic_content<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> DynamicContent { + struct DynamicContentVisitor<'a, 'db> { + env: &'a ProgramEnvironment<'db>, recursion_guard: TypeCollector<'db>, active_class_protocols: ActiveRecursionDetector>, content: Cell, } - impl DynamicContentVisitor<'_> { + impl DynamicContentVisitor<'_, '_> { fn record(&self, content: DynamicContent) { debug_assert!(self.content.get().is_absent()); debug_assert!(!content.is_absent()); @@ -420,7 +445,11 @@ pub(super) fn non_any_dynamic_content<'db>(db: &'db dyn Db, ty: Type<'db>) -> Dy } } - impl<'db> TypeVisitor<'db> for DynamicContentVisitor<'db> { + impl<'db> TypeVisitor<'db> for DynamicContentVisitor<'_, 'db> { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { true } @@ -467,12 +496,15 @@ pub(super) fn non_any_dynamic_content<'db>(db: &'db dyn Db, ty: Type<'db>) -> Dy self.active_class_protocols.visit( &origin, || self.record(DynamicContent::Indeterminate), - || walk_protocol_instance_interface(db, protocol.interface(db), protocol_ty, self), + || { + walk_protocol_instance_interface(db, protocol.interface(db), protocol_ty, self); + }, ); } } let visitor = DynamicContentVisitor { + env, recursion_guard: TypeCollector::default(), active_class_protocols: ActiveRecursionDetector::default(), content: Cell::new(DynamicContent::Absent), @@ -484,6 +516,7 @@ pub(super) fn non_any_dynamic_content<'db>(db: &'db dyn Db, ty: Type<'db>) -> Dy /// Implementation for `any_over_type` and `find_over_type`. fn any_over_type_impl<'db, F, T>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, should_visit_lazy_type_attributes: bool, query: F, @@ -493,6 +526,7 @@ where F: Fn(Type<'db>) -> T, { struct AnyOverTypeVisitor<'db, 'a, U> { + env: &'a ProgramEnvironment<'db>, query: &'a dyn Fn(Type<'db>) -> U, recursion_guard: TypeCollector<'db>, found_matching_type: Cell, @@ -503,6 +537,10 @@ where where U: Copy + Default + PartialEq, { + fn program_environment(&self) -> &ProgramEnvironment<'db> { + self.env + } + fn should_visit_lazy_type_attributes(&self) -> bool { self.should_visit_lazy_type_attributes } @@ -523,6 +561,7 @@ where } let visitor = AnyOverTypeVisitor { + env, query: &query, recursion_guard: TypeCollector::default(), found_matching_type: Cell::default(), @@ -542,11 +581,12 @@ where /// are visited or not. pub(super) fn any_over_type<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, should_visit_lazy_type_attributes: bool, query: impl Fn(Type<'db>) -> bool, ) -> bool { - any_over_type_impl(db, ty, should_visit_lazy_type_attributes, query) + any_over_type_impl(db, env, ty, should_visit_lazy_type_attributes, query) } /// Recurse into a type and calls the passed-in closure on every nested type @@ -564,6 +604,7 @@ pub(super) fn any_over_type<'db>( /// are visited or not. pub(super) fn find_over_type<'db, T>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, ty: Type<'db>, should_visit_lazy_type_attributes: bool, query: impl Fn(Type<'db>) -> Option, @@ -571,7 +612,7 @@ pub(super) fn find_over_type<'db, T>( where T: Copy + PartialEq, { - any_over_type_impl(db, ty, should_visit_lazy_type_attributes, query) + any_over_type_impl(db, env, ty, should_visit_lazy_type_attributes, query) } #[cfg(test)] diff --git a/crates/ty_python_semantic/tests/corpus.rs b/crates/ty_python_semantic/tests/corpus.rs index 6b6cffcf10..3fa7e4861e 100644 --- a/crates/ty_python_semantic/tests/corpus.rs +++ b/crates/ty_python_semantic/tests/corpus.rs @@ -1,10 +1,10 @@ use std::sync::Arc; use anyhow::{Context, anyhow}; -use ruff_db::Db; use ruff_db::files::{File, Files, system_path_to_file}; use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem}; use ruff_db::vendored::VendoredFileSystem; +use ruff_db::{Db, PythonFile}; use ruff_python_ast::PythonVersion; use ty_module_resolver::SearchPathSettings; @@ -111,7 +111,9 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> { // (and some non-expressions that clearly define a single type) let file = system_path_to_file(&db, path).unwrap(); - if let Err(err) = std::panic::catch_unwind(|| pull_types(&db, file)) { + if let Err(err) = std::panic::catch_unwind(|| { + pull_types(&db, PythonFile::new(&db, file, db.python_version())); + }) { println!("Check failed for {relative_path:?}."); std::panic::resume_unwind(err); } @@ -177,6 +179,10 @@ impl CorpusDb { db } + + fn python_version(&self) -> PythonVersion { + Program::get(self).python_version(self) + } } impl DbWithTestSystem for CorpusDb { @@ -202,10 +208,6 @@ impl ruff_db::Db for CorpusDb { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } } #[salsa::db] @@ -226,7 +228,7 @@ impl ty_python_core::Db for CorpusDb { impl ty_python_semantic::Db for CorpusDb { fn check_file(&self, file: File) -> Vec { if self.should_check_file(file) { - check_file_unwrap(self, file) + check_file_unwrap(self, PythonFile::new(self, file, self.python_version())) } else { Vec::new() } diff --git a/crates/ty_server/Cargo.toml b/crates/ty_server/Cargo.toml index d2887fbce5..7c423fb5bc 100644 --- a/crates/ty_server/Cargo.toml +++ b/crates/ty_server/Cargo.toml @@ -28,6 +28,7 @@ ty_combine = { workspace = true } ty_ide = { workspace = true } ty_module_resolver = { workspace = true } ty_project = { workspace = true } +ty_python_semantic = { workspace = true } anyhow = { workspace = true } bitflags = { workspace = true } diff --git a/crates/ty_server/src/server/api/diagnostics.rs b/crates/ty_server/src/server/api/diagnostics.rs index 3fe6808e75..4f3df55560 100644 --- a/crates/ty_server/src/server/api/diagnostics.rs +++ b/crates/ty_server/src/server/api/diagnostics.rs @@ -12,6 +12,7 @@ use ruff_text_size::Ranged; use rustc_hash::{FxHashMap, FxHashSet}; use ty_ide::{Hint, hints}; +use ruff_db::PythonFile; use ruff_db::diagnostic::{ Annotation, DisplayDiagnosticConfig, HyperlinkMode, Severity, SubDiagnostic, }; @@ -401,7 +402,7 @@ pub(super) fn compute_diagnostics( }; let diagnostics = db.check_file(file); - let unnecessary_hints = hints(db, file); + let unnecessary_hints = hints(db, PythonFile::new(db, file, db.python_version())); Some(Diagnostics { items: diagnostics, diff --git a/crates/ty_server/src/server/api/requests/call_hierarchy_incoming_calls.rs b/crates/ty_server/src/server/api/requests/call_hierarchy_incoming_calls.rs index b5ea24c099..875883fa6f 100644 --- a/crates/ty_server/src/server/api/requests/call_hierarchy_incoming_calls.rs +++ b/crates/ty_server/src/server/api/requests/call_hierarchy_incoming_calls.rs @@ -1,5 +1,7 @@ use lsp_types::CallHierarchyIncomingCallsRequest; use lsp_types::{CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams}; +use ruff_db::PythonFile; +use ty_project::Db as _; use crate::document::{ToRangeExt as _, resolve_file_uri_range}; use crate::server::api::requests::prepare_call_hierarchy::convert_to_lsp_item; @@ -40,7 +42,9 @@ impl BackgroundRequestHandler for CallHierarchyIncomingCallsRequestHandler { continue; }; - for call in ty_ide::incoming_calls(db, file, offset) { + for call in + ty_ide::incoming_calls(db, PythonFile::new(db, file, db.python_version()), offset) + { // `from_ranges` are byte offsets into `call.from.file` (the caller), // NOT into `file` (the prepared/queried symbol). Capture the caller // file before moving `call.from` into `convert_to_lsp_item`. diff --git a/crates/ty_server/src/server/api/requests/call_hierarchy_outgoing_calls.rs b/crates/ty_server/src/server/api/requests/call_hierarchy_outgoing_calls.rs index c8da7c21ed..d851d5afca 100644 --- a/crates/ty_server/src/server/api/requests/call_hierarchy_outgoing_calls.rs +++ b/crates/ty_server/src/server/api/requests/call_hierarchy_outgoing_calls.rs @@ -1,5 +1,7 @@ use lsp_types::CallHierarchyOutgoingCallsRequest; use lsp_types::{CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams}; +use ruff_db::PythonFile; +use ty_project::Db as _; use crate::document::{ToRangeExt as _, resolve_file_uri_range}; use crate::server::api::requests::prepare_call_hierarchy::convert_to_lsp_item; @@ -40,7 +42,9 @@ impl BackgroundRequestHandler for CallHierarchyOutgoingCallsRequestHandler { continue; }; - for call in ty_ide::outgoing_calls(db, file, offset) { + for call in + ty_ide::outgoing_calls(db, PythonFile::new(db, file, db.python_version()), offset) + { let Some(to) = convert_to_lsp_item(db, call.to, encoding) else { continue; }; diff --git a/crates/ty_server/src/server/api/requests/code_action.rs b/crates/ty_server/src/server/api/requests/code_action.rs index 2a5416610a..f70508bacf 100644 --- a/crates/ty_server/src/server/api/requests/code_action.rs +++ b/crates/ty_server/src/server/api/requests/code_action.rs @@ -2,10 +2,12 @@ use std::borrow::Cow; use std::collections::HashMap; use lsp_types::{self as types, Code, CodeActionRequest, CodeActionResponse, TextEdit, Uri}; +use ruff_db::PythonFile; use ruff_db::files::File; use ruff_diagnostics::Edit; use ruff_text_size::Ranged; use ty_ide::code_actions; +use ty_project::Db as _; use ty_project::ProjectDatabase; use types::CodeActionKind; @@ -41,6 +43,7 @@ impl BackgroundDocumentRequestHandler for CodeActionRequestHandler { let Some(file) = snapshot.to_notebook_or_file(db) else { return Ok(None); }; + let python_file = PythonFile::new(db, file, db.python_version()); let mut actions = Vec::new(); for mut diagnostic in diagnostics.into_iter().filter(|diagnostic| { @@ -99,7 +102,7 @@ impl BackgroundDocumentRequestHandler for CodeActionRequestHandler { if let Some(diagnostic_id) = diagnostic_id && let Some(range) = diagnostic.range.to_text_range(db, file, uri, encoding) { - for action in code_actions(db, file, range, &diagnostic_id) { + for action in code_actions(db, python_file, range, &diagnostic_id) { actions.push(CodeActionResponse::CodeAction(lsp_types::CodeAction { title: action.title, kind: Some(CodeActionKind::QuickFix), diff --git a/crates/ty_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs index eb7ecbcc79..33a318aca2 100644 --- a/crates/ty_server/src/server/api/requests/completion.rs +++ b/crates/ty_server/src/server/api/requests/completion.rs @@ -6,13 +6,16 @@ use lsp_types::{ CompletionParams, CompletionRequest, CompletionResponse, Documentation, InsertTextFormat, TextEdit, Uri, }; +use ruff_db::PythonFile; use ruff_source_file::OneIndexed; use ruff_text_size::Ranged; use ty_ide::{ CompletionCapabilities, CompletionCommand, CompletionInsertTextFormat, CompletionKind, completion, }; +use ty_project::Db as _; use ty_project::ProjectDatabase; +use ty_python_semantic::ProgramEnvironment; use crate::capabilities::ResolvedClientCapabilities; use crate::document::{PositionExt, ToRangeExt}; @@ -61,12 +64,14 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { return Ok(None); }; let client_capabilities = snapshot.resolved_client_capabilities(); + let python_file = PythonFile::new(db, file, db.python_version()); + let env = ProgramEnvironment::from_file(python_file); let completions = completion( db, snapshot.workspace_settings().completions(), CompletionCapabilities::default() .snippets(client_capabilities.supports_completion_item_snippets()), - file, + python_file, offset, ); if completions.is_empty() { @@ -80,7 +85,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { .enumerate() .map(|(i, comp)| { let kind = comp.kind.map(ty_kind_to_lsp_kind); - let type_display = comp.ty.map(|ty| ty.display(db).to_string()); + let type_display = comp.ty.map(|ty| ty.display(db, &env).to_string()); let import_edit = comp.import.as_ref().and_then(|edit| { let range = edit .range() diff --git a/crates/ty_server/src/server/api/requests/doc_highlights.rs b/crates/ty_server/src/server/api/requests/doc_highlights.rs index 3ced9d9588..b8ace78182 100644 --- a/crates/ty_server/src/server/api/requests/doc_highlights.rs +++ b/crates/ty_server/src/server/api/requests/doc_highlights.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use lsp_types::DocumentHighlightRequest; use lsp_types::{DocumentHighlight, DocumentHighlightKind, DocumentHighlightParams, Uri}; +use ruff_db::PythonFile; use ty_ide::{ReferenceKind, document_highlights}; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::document::{PositionExt, ToRangeExt}; @@ -49,7 +51,9 @@ impl BackgroundDocumentRequestHandler for DocumentHighlightRequestHandler { return Ok(None); }; - let Some(highlights_result) = document_highlights(db, file, offset) else { + let Some(highlights_result) = + document_highlights(db, PythonFile::new(db, file, db.python_version()), offset) + else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/document_symbols.rs b/crates/ty_server/src/server/api/requests/document_symbols.rs index 4b85aaf48e..ab7272b05f 100644 --- a/crates/ty_server/src/server/api/requests/document_symbols.rs +++ b/crates/ty_server/src/server/api/requests/document_symbols.rs @@ -2,8 +2,10 @@ use std::borrow::Cow; use lsp_types::DocumentSymbolRequest; use lsp_types::{DocumentSymbol, DocumentSymbolParams, Uri}; +use ruff_db::PythonFile; use ruff_db::files::File; use ty_ide::{HierarchicalSymbols, SymbolId, SymbolInfo, document_symbols}; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::Db; @@ -48,7 +50,7 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler { .resolved_client_capabilities() .supports_hierarchical_document_symbols(); - let symbols = document_symbols(db, file); + let symbols = document_symbols(db, PythonFile::new(db, file, db.python_version())); if symbols.is_empty() { return Ok(None); } diff --git a/crates/ty_server/src/server/api/requests/folding_range.rs b/crates/ty_server/src/server/api/requests/folding_range.rs index 21c06fb799..f8865fad97 100644 --- a/crates/ty_server/src/server/api/requests/folding_range.rs +++ b/crates/ty_server/src/server/api/requests/folding_range.rs @@ -2,9 +2,11 @@ use std::borrow::Cow; use lsp_types::FoldingRangeRequest; use lsp_types::{FoldingRange, FoldingRangeKind, FoldingRangeParams, Uri}; +use ruff_db::PythonFile; use ruff_db::source::source_text; use ruff_text_size::TextRange; use ty_ide::folding_ranges; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::db::Db; @@ -56,29 +58,33 @@ impl BackgroundDocumentRequestHandler for FoldingRangeRequestHandler { cell_range = cell_index.and_then(|index| notebook.cell_range(index)); } - let results: Vec<_> = folding_ranges(db, file, cell_range) - .into_iter() - .filter_map(|folding_range| { - let lsp_range = folding_range - .range - .to_lsp_range(db, file, snapshot.encoding())?; + let results: Vec<_> = folding_ranges( + db, + PythonFile::new(db, file, db.python_version()), + cell_range, + ) + .into_iter() + .filter_map(|folding_range| { + let lsp_range = folding_range + .range + .to_lsp_range(db, file, snapshot.encoding())?; - let kind = folding_range.kind.map(|k| match k { - ty_ide::FoldingRangeKind::Comment => FoldingRangeKind::Comment, - ty_ide::FoldingRangeKind::Imports => FoldingRangeKind::Imports, - ty_ide::FoldingRangeKind::Region => FoldingRangeKind::Region, - }); + let kind = folding_range.kind.map(|k| match k { + ty_ide::FoldingRangeKind::Comment => FoldingRangeKind::Comment, + ty_ide::FoldingRangeKind::Imports => FoldingRangeKind::Imports, + ty_ide::FoldingRangeKind::Region => FoldingRangeKind::Region, + }); - Some(FoldingRange { - start_line: lsp_range.local_range().start.line, - start_character: Some(lsp_range.local_range().start.character), - end_line: lsp_range.local_range().end.line, - end_character: Some(lsp_range.local_range().end.character), - kind, - collapsed_text: None, - }) + Some(FoldingRange { + start_line: lsp_range.local_range().start.line, + start_character: Some(lsp_range.local_range().start.character), + end_line: lsp_range.local_range().end.line, + end_character: Some(lsp_range.local_range().end.character), + kind, + collapsed_text: None, }) - .collect(); + }) + .collect(); if results.is_empty() { Ok(None) diff --git a/crates/ty_server/src/server/api/requests/goto_declaration.rs b/crates/ty_server/src/server/api/requests/goto_declaration.rs index 2dbf8c60aa..68ad4aee6e 100644 --- a/crates/ty_server/src/server/api/requests/goto_declaration.rs +++ b/crates/ty_server/src/server/api/requests/goto_declaration.rs @@ -1,7 +1,9 @@ use std::borrow::Cow; use lsp_types::{DeclarationParams, DeclarationRequest, DeclarationResponse, Uri}; +use ruff_db::PythonFile; use ty_ide::goto_declaration; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::document::{PositionExt, ToLink}; @@ -48,7 +50,9 @@ impl BackgroundDocumentRequestHandler for GotoDeclarationRequestHandler { return Ok(None); }; - let Some(ranged) = goto_declaration(db, file, offset) else { + let Some(ranged) = + goto_declaration(db, PythonFile::new(db, file, db.python_version()), offset) + else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/goto_definition.rs b/crates/ty_server/src/server/api/requests/goto_definition.rs index 88d989705e..5ca8eda1a8 100644 --- a/crates/ty_server/src/server/api/requests/goto_definition.rs +++ b/crates/ty_server/src/server/api/requests/goto_definition.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use lsp_types::DefinitionRequest; use lsp_types::{DefinitionParams, DefinitionResponse, Uri}; +use ruff_db::PythonFile; use ty_ide::goto_definition; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::document::{PositionExt, ToLink}; @@ -49,7 +51,9 @@ impl BackgroundDocumentRequestHandler for GotoDefinitionRequestHandler { return Ok(None); }; - let Some(ranged) = goto_definition(db, file, offset) else { + let Some(ranged) = + goto_definition(db, PythonFile::new(db, file, db.python_version()), offset) + else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/goto_implementation.rs b/crates/ty_server/src/server/api/requests/goto_implementation.rs index fad3fc7519..f51fbde05b 100644 --- a/crates/ty_server/src/server/api/requests/goto_implementation.rs +++ b/crates/ty_server/src/server/api/requests/goto_implementation.rs @@ -1,7 +1,9 @@ use std::borrow::Cow; use lsp_types::{ImplementationParams, ImplementationRequest, ImplementationResponse, Uri}; +use ruff_db::PythonFile; use ty_ide::goto_implementation; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::document::{PositionExt, ToLink}; @@ -48,7 +50,9 @@ impl BackgroundDocumentRequestHandler for GotoImplementationRequestHandler { return Ok(None); }; - let Some(ranged) = goto_implementation(db, file, offset) else { + let Some(ranged) = + goto_implementation(db, PythonFile::new(db, file, db.python_version()), offset) + else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/goto_type_definition.rs b/crates/ty_server/src/server/api/requests/goto_type_definition.rs index be0b040ce3..0dc7c12f46 100644 --- a/crates/ty_server/src/server/api/requests/goto_type_definition.rs +++ b/crates/ty_server/src/server/api/requests/goto_type_definition.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use lsp_types::{TypeDefinitionParams, TypeDefinitionRequest}; use lsp_types::{TypeDefinitionResponse, Uri}; +use ruff_db::PythonFile; use ty_ide::goto_type_definition; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::document::{PositionExt, ToLink}; @@ -49,7 +51,9 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler { return Ok(None); }; - let Some(ranged) = goto_type_definition(db, file, offset) else { + let Some(ranged) = + goto_type_definition(db, PythonFile::new(db, file, db.python_version()), offset) + else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/hover.rs b/crates/ty_server/src/server/api/requests/hover.rs index 81bbada3c4..c427a28c39 100644 --- a/crates/ty_server/src/server/api/requests/hover.rs +++ b/crates/ty_server/src/server/api/requests/hover.rs @@ -8,7 +8,9 @@ use crate::session::DocumentSnapshot; use crate::session::client::Client; use lsp_types::HoverRequest; use lsp_types::{HoverParams, MarkupContent, Uri}; +use ruff_db::PythonFile; use ty_ide::{MarkupKind, hover}; +use ty_project::Db as _; use ty_project::ProjectDatabase; pub(crate) struct HoverRequestHandler; @@ -48,7 +50,8 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler { return Ok(None); }; - let Some(range_info) = hover(db, file, offset) else { + let Some(range_info) = hover(db, PythonFile::new(db, file, db.python_version()), offset) + else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/inlay_hints.rs b/crates/ty_server/src/server/api/requests/inlay_hints.rs index 04aaac2ccd..905fa9327b 100644 --- a/crates/ty_server/src/server/api/requests/inlay_hints.rs +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs @@ -3,8 +3,10 @@ use std::time::Instant; use lsp_types::InlayHintRequest; use lsp_types::{InlayHintParams, Uri}; +use ruff_db::PythonFile; use ruff_db::files::File; use ty_ide::{InlayHintKind, InlayHintLabel, InlayHintTextEdit, inlay_hints}; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::PositionEncoding; @@ -51,7 +53,12 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler { return Ok(None); }; - let inlay_hints = inlay_hints(db, file, range, workspace_settings.inlay_hints()); + let inlay_hints = inlay_hints( + db, + PythonFile::new(db, file, db.python_version()), + range, + workspace_settings.inlay_hints(), + ); let inlay_hints: Vec = inlay_hints .into_iter() diff --git a/crates/ty_server/src/server/api/requests/prepare_call_hierarchy.rs b/crates/ty_server/src/server/api/requests/prepare_call_hierarchy.rs index 55b00ce963..03a04b288c 100644 --- a/crates/ty_server/src/server/api/requests/prepare_call_hierarchy.rs +++ b/crates/ty_server/src/server/api/requests/prepare_call_hierarchy.rs @@ -2,6 +2,8 @@ use std::borrow::Cow; use lsp_types::CallHierarchyPrepareRequest; use lsp_types::{CallHierarchyItem, CallHierarchyPrepareParams, Uri}; +use ruff_db::PythonFile; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::PositionEncoding; @@ -57,7 +59,11 @@ impl BackgroundDocumentRequestHandler for PrepareCallHierarchyRequestHandler { return Ok(None); }; - let Some(items) = ty_ide::prepare_call_hierarchy(db, file, offset) else { + let Some(items) = ty_ide::prepare_call_hierarchy( + db, + PythonFile::new(db, file, db.python_version()), + offset, + ) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/prepare_rename.rs b/crates/ty_server/src/server/api/requests/prepare_rename.rs index 2d2343da3c..1a4d1984fd 100644 --- a/crates/ty_server/src/server/api/requests/prepare_rename.rs +++ b/crates/ty_server/src/server/api/requests/prepare_rename.rs @@ -1,7 +1,9 @@ use std::borrow::Cow; use lsp_types::{PrepareRenameParams, PrepareRenameRequest, PrepareRenameResult, Uri}; +use ruff_db::PythonFile; use ty_ide::can_rename; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::document::{PositionExt, ToRangeExt}; @@ -48,7 +50,8 @@ impl BackgroundDocumentRequestHandler for PrepareRenameRequestHandler { return Ok(None); }; - let Some(range) = can_rename(db, file, offset) else { + let Some(range) = can_rename(db, PythonFile::new(db, file, db.python_version()), offset) + else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs b/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs index c5417e7f49..5a99a62179 100644 --- a/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs +++ b/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs @@ -2,6 +2,8 @@ use std::borrow::Cow; use lsp_types::TypeHierarchyPrepareRequest; use lsp_types::{TypeHierarchyItem, TypeHierarchyPrepareParams, Uri}; +use ruff_db::PythonFile; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::document::PositionExt; @@ -58,7 +60,11 @@ impl BackgroundDocumentRequestHandler for PrepareTypeHierarchyRequestHandler { return Ok(None); }; - let Some(item) = ty_ide::prepare_type_hierarchy(db, file, offset) else { + let Some(item) = ty_ide::prepare_type_hierarchy( + db, + PythonFile::new(db, file, db.python_version()), + offset, + ) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/references.rs b/crates/ty_server/src/server/api/requests/references.rs index daa7a03ee8..32d213acca 100644 --- a/crates/ty_server/src/server/api/requests/references.rs +++ b/crates/ty_server/src/server/api/requests/references.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use lsp_types::ReferencesRequest; use lsp_types::{Location, ReferenceParams, Uri}; +use ruff_db::PythonFile; use ty_ide::find_references; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::document::{PositionExt, ToLink}; @@ -51,7 +53,12 @@ impl BackgroundDocumentRequestHandler for ReferencesRequestHandler { let include_declaration = params.context.include_declaration; - let Some(references_result) = find_references(db, file, offset, include_declaration) else { + let Some(references_result) = find_references( + db, + PythonFile::new(db, file, db.python_version()), + offset, + include_declaration, + ) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/rename.rs b/crates/ty_server/src/server/api/requests/rename.rs index c975df8468..94efe4c1af 100644 --- a/crates/ty_server/src/server/api/requests/rename.rs +++ b/crates/ty_server/src/server/api/requests/rename.rs @@ -3,7 +3,9 @@ use std::collections::HashMap; use lsp_types::RenameRequest; use lsp_types::{RenameParams, TextEdit, Uri, WorkspaceEdit}; +use ruff_db::PythonFile; use ty_ide::rename; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::document::{PositionExt, ToLink}; @@ -50,7 +52,12 @@ impl BackgroundDocumentRequestHandler for RenameRequestHandler { return Ok(None); }; - let Some(rename_results) = rename(db, file, offset, ¶ms.new_name) else { + let Some(rename_results) = rename( + db, + PythonFile::new(db, file, db.python_version()), + offset, + ¶ms.new_name, + ) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/selection_range.rs b/crates/ty_server/src/server/api/requests/selection_range.rs index 904be7ecb5..0611f9386d 100644 --- a/crates/ty_server/src/server/api/requests/selection_range.rs +++ b/crates/ty_server/src/server/api/requests/selection_range.rs @@ -3,7 +3,9 @@ use std::borrow::Cow; use lsp_types::{ SelectionRange as LspSelectionRange, SelectionRangeParams, SelectionRangeRequest, Uri, }; +use ruff_db::PythonFile; use ty_ide::selection_range; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::document::{PositionExt, ToRangeExt}; @@ -40,6 +42,7 @@ impl BackgroundDocumentRequestHandler for SelectionRangeRequestHandler { let Some(file) = snapshot.to_notebook_or_file(db) else { return Ok(None); }; + let python_file = PythonFile::new(db, file, db.python_version()); let mut results = Vec::new(); @@ -49,7 +52,7 @@ impl BackgroundDocumentRequestHandler for SelectionRangeRequestHandler { continue; }; - let ranges = selection_range(db, file, offset); + let ranges = selection_range(db, python_file, offset); if !ranges.is_empty() { // Convert ranges to nested LSP SelectionRange structure let mut lsp_range = None; diff --git a/crates/ty_server/src/server/api/requests/signature_help.rs b/crates/ty_server/src/server/api/requests/signature_help.rs index 7572c3b69c..fdf1bf3309 100644 --- a/crates/ty_server/src/server/api/requests/signature_help.rs +++ b/crates/ty_server/src/server/api/requests/signature_help.rs @@ -11,7 +11,9 @@ use lsp_types::{ Documentation, ParameterInformation, ParameterInformationLabel, SignatureHelp, SignatureHelpParams, SignatureInformation, Uri, }; +use ruff_db::PythonFile; use ty_ide::signature_help; +use ty_project::Db as _; use ty_project::ProjectDatabase; pub(crate) struct SignatureHelpRequestHandler; @@ -54,7 +56,9 @@ impl BackgroundDocumentRequestHandler for SignatureHelpRequestHandler { // Extract signature help capabilities from the client let resolved_capabilities = snapshot.resolved_client_capabilities(); - let Some(signature_help_info) = signature_help(db, file, offset) else { + let Some(signature_help_info) = + signature_help(db, PythonFile::new(db, file, db.python_version()), offset) + else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs index a04267a6ca..ee72c99c24 100644 --- a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs +++ b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs @@ -10,6 +10,7 @@ use lsp_types::{ WorkspaceDiagnosticReportPartialResult, WorkspaceDocumentDiagnosticReport, WorkspaceFullDocumentDiagnosticReport, WorkspaceUnchangedDocumentDiagnosticReport, }; +use ruff_db::PythonFile; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::File; use ruff_db::source::source_text; @@ -17,6 +18,7 @@ use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use serde_json::json; use ty_ide::{Hint, hints}; +use ty_project::Db as _; use ty_project::{ProgressReporter, ProjectDatabase}; use crate::PositionEncoding; @@ -239,7 +241,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> { } fn report_checked_file(&self, db: &ProjectDatabase, file: File, diagnostics: &[Diagnostic]) { - let unnecessary_hints = hints(db, file); + let unnecessary_hints = hints(db, PythonFile::new(db, file, db.python_version())); // Another thread might have panicked at this point because of a salsa cancellation which // poisoned the result. If the response is poisoned, just don't report and wait for our thread @@ -287,7 +289,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> { let response = &mut self.state.get_mut().unwrap().response; for (file, diagnostics) in by_file { - let unnecessary_hints = hints(db, file); + let unnecessary_hints = hints(db, PythonFile::new(db, file, db.python_version())); response.write_diagnostics_for_file(db, file, &diagnostics, &unnecessary_hints); } response.maybe_flush(); diff --git a/crates/ty_server/src/server/api/semantic_tokens.rs b/crates/ty_server/src/server/api/semantic_tokens.rs index 07e4d5432b..0e33197fcd 100644 --- a/crates/ty_server/src/server/api/semantic_tokens.rs +++ b/crates/ty_server/src/server/api/semantic_tokens.rs @@ -1,8 +1,10 @@ use lsp_types::SemanticToken; +use ruff_db::PythonFile; use ruff_db::source::{line_index, source_text}; use ruff_source_file::OneIndexed; use ruff_text_size::{Ranged, TextRange}; use ty_ide::{SemanticTokenModifier, SemanticTokenType, semantic_tokens}; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::document::{PositionEncoding, ToRangeExt}; @@ -18,7 +20,8 @@ pub(crate) fn generate_semantic_tokens( ) -> Vec { let source = source_text(db, file); let line_index = line_index(db, file); - let semantic_token_data = semantic_tokens(db, file, range); + let semantic_token_data = + semantic_tokens(db, PythonFile::new(db, file, db.python_version()), range); let mut encoder = Encoder { tokens: Vec::with_capacity(semantic_token_data.len()), diff --git a/crates/ty_server/src/server/api/type_hierarchy.rs b/crates/ty_server/src/server/api/type_hierarchy.rs index b228c31f25..6fcf81f50e 100644 --- a/crates/ty_server/src/server/api/type_hierarchy.rs +++ b/crates/ty_server/src/server/api/type_hierarchy.rs @@ -1,4 +1,6 @@ use lsp_types::{SymbolKind, TypeHierarchyItem}; +use ruff_db::PythonFile; +use ty_project::Db as _; use ty_project::ProjectDatabase; use crate::PositionEncoding; @@ -33,6 +35,7 @@ pub(crate) fn hierarchy_handler( ) else { continue; }; + let file = PythonFile::new(db, file, db.python_version()); let hierarchy_types = match hierarchy_kind { TypeHierarchyKind::Subtypes => ty_ide::type_hierarchy_subtypes(db, file, offset), TypeHierarchyKind::Supertypes => ty_ide::type_hierarchy_supertypes(db, file, offset), diff --git a/crates/ty_test/src/db.rs b/crates/ty_test/src/db.rs index f4289e7899..f97a942cd4 100644 --- a/crates/ty_test/src/db.rs +++ b/crates/ty_test/src/db.rs @@ -1,6 +1,5 @@ use crate::config::{Analysis, Rules, ScriptOptions}; use camino::{Utf8Component, Utf8PathBuf}; -use ruff_db::Db as SourceDb; use ruff_db::diagnostic::{Diagnostic, Severity}; use ruff_db::files::{File, Files}; use ruff_db::source::source_text; @@ -9,6 +8,7 @@ use ruff_db::system::{ WritableSystem, }; use ruff_db::vendored::VendoredFileSystem; +use ruff_db::{Db as SourceDb, PythonFile}; use ruff_notebook::{Notebook, NotebookError}; use salsa::Setter as _; use std::borrow::Cow; @@ -50,6 +50,10 @@ impl Db { db } + pub(crate) fn python_version(&self) -> ruff_python_ast::PythonVersion { + Program::get(self).python_version(self) + } + fn settings(&self) -> Settings { self.settings.unwrap() } @@ -110,10 +114,6 @@ impl SourceDb for Db { fn files(&self) -> &Files { &self.files } - - fn python_version(&self) -> ruff_python_ast::PythonVersion { - Program::get(self).python_version(self) - } } #[salsa::db] @@ -137,7 +137,7 @@ impl SemanticDb for Db { return Vec::new(); } - check_file_unwrap(self, file) + check_file_unwrap(self, PythonFile::new(self, file, self.python_version())) } fn rule_selection(&self, file: File) -> &RuleSelection { diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index d8d06f6eef..9bca33b1b0 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -7,12 +7,12 @@ use mdtest::parser::{self}; use mdtest::{ Failures, FileFailures, MarkdownEdit, OutputFormat, TestFile, TestOutcome, attempt_test, }; -use ruff_db::Db; use ruff_db::cancellation::CancellationTokenSource; use ruff_db::diagnostic::DiagnosticId; use ruff_db::files::{FileRootKind, system_path_to_file}; use ruff_db::system::{DbWithWritableSystem as _, SystemPath, SystemPathBuf}; use ruff_db::testing::{setup_logging, setup_logging_with_filter}; +use ruff_db::{Db, PythonFile}; use ruff_diagnostics::Applicability; use ruff_source_file::OneIndexed; use std::fmt::Write; @@ -348,16 +348,22 @@ fn run_test( } }; - let failure = match matcher::match_file(db, test_file.file, &diagnostics, options) - .and_then(|inline_diagnostics| { - mdtest::validate_inline_snapshot( - db, - "ty", - test_file, - &inline_diagnostics, - &mut markdown_edits, - ) - }) { + let failure = match matcher::match_file( + db, + test_file.file, + python_version, + &diagnostics, + options, + ) + .and_then(|inline_diagnostics| { + mdtest::validate_inline_snapshot( + db, + "ty", + test_file, + &inline_diagnostics, + &mut markdown_edits, + ) + }) { Ok(()) => None, Err(line_failures) => Some(FileFailures { backtick_offsets: test_file.to_code_block_backtick_offsets(), @@ -367,7 +373,10 @@ fn run_test( all_diagnostics.extend(diagnostics); - let pull_types_result = attempt_test(|file| pull_types(db, file), test_file); + let pull_types_result = attempt_test( + |file| pull_types(db, PythonFile::new(db, file, python_version)), + test_file, + ); match pull_types_result { Ok(()) => {} Err(failures) => { @@ -428,6 +437,7 @@ fn run_test( let token_source = CancellationTokenSource::new(); let result = fix_all_diagnostics( db, + python_version, all_diagnostics, Applicability::Unsafe, &token_source.token(), @@ -498,22 +508,25 @@ struct ModuleInconsistency<'db> { /// `list_module`. fn run_module_resolution_consistency_test(db: &db::Db) -> Result<(), Vec>> { let mut errs = vec![]; - for from_list in list_modules(db).iter().copied() { + let python_version = db.python_version(); + for from_list in list_modules(db, python_version).iter().copied() { // TODO: For now list_modules does not partake in desperate module resolution so // only compare against confident module resolution. - errs.push(match resolve_module_confident(db, from_list.name(db)) { - None => ModuleInconsistency { - db, - from_list, - from_resolve: None, + errs.push( + match resolve_module_confident(db, python_version, from_list.name(db)) { + None => ModuleInconsistency { + db, + from_list, + from_resolve: None, + }, + Some(from_resolve) if from_list != from_resolve => ModuleInconsistency { + db, + from_list, + from_resolve: Some(from_resolve), + }, + _ => continue, }, - Some(from_resolve) if from_list != from_resolve => ModuleInconsistency { - db, - from_list, - from_resolve: Some(from_resolve), - }, - _ => continue, - }); + ); } if errs.is_empty() { Ok(()) } else { Err(errs) } } diff --git a/crates/ty_wasm/Cargo.toml b/crates/ty_wasm/Cargo.toml index 070981044a..bcb4b91fa3 100644 --- a/crates/ty_wasm/Cargo.toml +++ b/crates/ty_wasm/Cargo.toml @@ -29,6 +29,7 @@ ty_project = { workspace = true, default-features = false, features = [ "format", ] } ty_python_core = { workspace = true } +ty_python_semantic = { workspace = true } ruff_db = { workspace = true, default-features = false, features = [] } ruff_diagnostics = { workspace = true } diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index 6a69c4d60e..6a75c8fa80 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -2,6 +2,7 @@ use std::any::Any; use js_sys::{Error, JsString}; use ruff_db::Db as _; +use ruff_db::PythonFile; use ruff_db::diagnostic::{self, DisplayDiagnosticConfig}; use ruff_db::files::{File, FilePath, FileRange, system_path_to_file, vendored_path_to_file}; use ruff_db::source::{SourceText, line_index, source_text}; @@ -28,6 +29,7 @@ use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; use ty_project::{CheckMode, ProjectMetadata}; use ty_project::{Db, ProjectDatabase}; use ty_python_core::program::{FallibleStrategy, Program}; +use ty_python_semantic::ProgramEnvironment; use wasm_bindgen::prelude::*; #[wasm_bindgen] @@ -275,10 +277,13 @@ impl Workspace { #[wasm_bindgen(js_name = "hints")] pub fn hints(&self, file_id: &FileHandle) -> Result, Error> { - Ok(hints(&self.db, file_id.file) - .into_iter() - .map(|hint| Hint::from_ide_hint(&self.db, file_id.file, self.position_encoding, &hint)) - .collect()) + Ok(hints( + &self.db, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), + ) + .into_iter() + .map(|hint| Hint::from_ide_hint(&self.db, file_id.file, self.position_encoding, &hint)) + .collect()) } /// Checks all open files @@ -290,7 +295,11 @@ impl Workspace { /// Returns the parsed AST for `path` pub fn parsed(&self, file_id: &FileHandle) -> Result { - let parsed = ruff_db::parsed::parsed_module(&self.db, file_id.file).load(&self.db); + let parsed = ruff_db::parsed::parsed_module( + &self.db, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), + ) + .load(&self.db); Ok(format!("{:#?}", parsed.syntax())) } @@ -301,7 +310,11 @@ impl Workspace { /// Returns the token stream for `path` serialized as a string. pub fn tokens(&self, file_id: &FileHandle) -> Result { - let parsed = ruff_db::parsed::parsed_module(&self.db, file_id.file).load(&self.db); + let parsed = ruff_db::parsed::parsed_module( + &self.db, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), + ) + .load(&self.db); Ok(format!("{:#?}", parsed.tokens())) } @@ -324,7 +337,11 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = goto_type_definition(&self.db, file_id.file, offset) else { + let Some(targets) = goto_type_definition( + &self.db, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), + offset, + ) else { return Ok(Vec::new()); }; @@ -348,7 +365,11 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = goto_declaration(&self.db, file_id.file, offset) else { + let Some(targets) = goto_declaration( + &self.db, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), + offset, + ) else { return Ok(Vec::new()); }; @@ -372,7 +393,11 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = goto_definition(&self.db, file_id.file, offset) else { + let Some(targets) = goto_definition( + &self.db, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), + offset, + ) else { return Ok(Vec::new()); }; @@ -396,7 +421,12 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = find_references(&self.db, file_id.file, offset, true) else { + let Some(targets) = find_references( + &self.db, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), + offset, + true, + ) else { return Ok(Vec::new()); }; @@ -435,7 +465,11 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(range) = can_rename(&self.db, file_id.file, offset) else { + let Some(range) = can_rename( + &self.db, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), + offset, + ) else { return Ok(None); }; @@ -458,12 +492,13 @@ impl Workspace { let index = line_index(&self.db, file_id.file); let offset = position.to_text_size(&source, &index, self.position_encoding)?; + let python_file = PythonFile::new(&self.db, file_id.file, self.db.python_version()); - if can_rename(&self.db, file_id.file, offset).is_none() { + if can_rename(&self.db, python_file, offset).is_none() { return Ok(Vec::new()); } - let Some(rename_results) = rename(&self.db, file_id.file, offset, new_name) else { + let Some(rename_results) = rename(&self.db, python_file, offset, new_name) else { return Ok(Vec::new()); }; @@ -488,7 +523,11 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(range_info) = hover(&self.db, file_id.file, offset) else { + let Some(range_info) = hover( + &self.db, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), + offset, + ) else { return Ok(None); }; @@ -519,11 +558,13 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; let settings = ty_ide::CompletionSettings::default(); + let python_file = PythonFile::new(&self.db, file_id.file, self.db.python_version()); + let env = ProgramEnvironment::from_file(python_file); let completions = ty_ide::completion( &self.db, &settings, CompletionCapabilities::default(), - file_id.file, + python_file, offset, ); @@ -532,7 +573,7 @@ impl Workspace { .map(|comp| { let name = comp.label().to_string(); let kind = comp.kind.map(CompletionKind::from); - let type_display = comp.ty.map(|ty| ty.display(&self.db).to_string()); + let type_display = comp.ty.map(|ty| ty.display(&self.db, &env).to_string()); let import_edit = comp.import.as_ref().map(|edit| { let range = Range::from_text_range( edit.range(), @@ -567,7 +608,7 @@ impl Workspace { let result = inlay_hints( &self.db, - file_id.file, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), range.to_text_range(&index, &source, self.position_encoding)?, // TODO: Provide a way to configure this &InlayHintSettings { @@ -624,7 +665,11 @@ impl Workspace { let index = line_index(&self.db, file_id.file); let source = source_text(&self.db, file_id.file); - let semantic_token = ty_ide::semantic_tokens(&self.db, file_id.file, None); + let semantic_token = ty_ide::semantic_tokens( + &self.db, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), + None, + ); let result = semantic_token .iter() @@ -649,7 +694,7 @@ impl Workspace { let semantic_token = ty_ide::semantic_tokens( &self.db, - file_id.file, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), Some(range.to_text_range(&index, &source, self.position_encoding)?), ); @@ -686,7 +731,7 @@ impl Workspace { actions.extend( ty_ide::code_actions( &self.db, - file_id.file, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), range, diagnostic.inner.id().as_str(), ) @@ -721,7 +766,11 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(signature_help_info) = signature_help(&self.db, file_id.file, offset) else { + let Some(signature_help_info) = signature_help( + &self.db, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), + offset, + ) else { return Ok(None); }; @@ -768,7 +817,11 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = document_highlights(&self.db, file_id.file, offset) else { + let Some(targets) = document_highlights( + &self.db, + PythonFile::new(&self.db, file_id.file, self.db.python_version()), + offset, + ) else { return Ok(Vec::new()); }; diff --git a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs index 579180f8d1..181d7d54ff 100644 --- a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs +++ b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs @@ -8,6 +8,7 @@ use std::sync::{Arc, Mutex, OnceLock}; use libfuzzer_sys::{Corpus, fuzz_target}; use ruff_db::Db as SourceDb; +use ruff_db::PythonFile; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::{File, Files, system_path_to_file}; use ruff_db::system::{ @@ -56,6 +57,10 @@ impl TestDb { analysis_settings: AnalysisSettings::default().into(), } } + + fn python_version(&self) -> PythonVersion { + Program::get(self).python_version(self) + } } #[salsa::db] @@ -71,10 +76,6 @@ 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 { @@ -105,7 +106,8 @@ impl ty_python_core::Db for TestDb { impl SemanticDb for TestDb { fn check_file(&self, file: File) -> Vec { if self.should_check_file(file) { - ty_python_semantic::check_file_unwrap(self, file) + let python_file = PythonFile::new(self, file, self.python_version()); + ty_python_semantic::check_file_unwrap(self, python_file) } else { Vec::new() } @@ -181,7 +183,7 @@ fn do_fuzz(case: &[u8]) -> Corpus { for path in &["/src/a.py", "/src/a.pyi"] { db.write_file(path, code).unwrap(); let file = system_path_to_file(&*db, path).unwrap(); - check_types(&*db, file); + check_types(&*db, PythonFile::new(&*db, file, db.python_version())); db.memory_file_system().remove_file(path).unwrap(); file.sync(&mut *db); } From cd138a3289388802ddd09b1116be4016887ede59 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 3 Aug 2026 12:16:55 +0200 Subject: [PATCH 216/390] [ty] Document inference behavior of unannotated parameters with default values (#24943) ## Summary In this PR, I explored the proposal in https://github.com/astral-sh/ty/issues/3251. The idea was to treat a function like `def f(x="foo"): ...` as if the `x` parameter was declared as `str`. This is what pyright does, and would follow what we do for unannotated class attributes with default values. However, implementing this showed a huge ecosystem impact of 8700 new diagnostics. I tried various things to improve this. For example, I promoted `None` to `Unknown | None` because it seems unreasonable to assume that you only ever want callers to pass in `None`. Then there's a lot of code that takes `x=()` as a default value and wants callers to be able to pass in arbitrary tuples. I tried patching that by promoting `tuple[()] -> tuple[Unknown, ...]`. But the biggest problem is with functions that take a default int value (e.g. `x=0`), but want to allow their callers to pass in float values. I tried promoting the type of `x` from `int` to `float`/`complex`, but that leads to the opposite problem: now the function body of *every* function that takes `x=0` must also handle the possibility that `x` is `float`/`complex`. Ultimately, I think a scenario like this: ```py def some_computation(..., tolerance=0): ... some_computation(..., tolerance=1e-3) ``` is just far more common than the corresponding scenario with class attributes. I think it's unfortunate that our behavior is inconsistent with what we do for class attributes, but I don't think it's worth introducing ~8000 diagnostics (no variant that I tried went below that limit). The corresponding PR for attributes introduced an order of magnitude fewer new diagnostics (https://github.com/astral-sh/ruff/pull/24531). So in this PR, all I do is to document the current behavior. closes https://github.com/astral-sh/ty/issues/3251 --- .../resources/mdtest/function/parameters.md | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/function/parameters.md b/crates/ty_python_semantic/resources/mdtest/function/parameters.md index 06ee9b4e45..3ca4f5ddf5 100644 --- a/crates/ty_python_semantic/resources/mdtest/function/parameters.md +++ b/crates/ty_python_semantic/resources/mdtest/function/parameters.md @@ -1,12 +1,54 @@ # Function parameter types +## Basic + Within a function scope, the declared type of each parameter is its annotated type (or Unknown if -not annotated). The initial inferred type is the annotated type of the parameter, if any. If there -is no annotation, it is the union of `Unknown` with the type of the default value expression (if -any). +not annotated). The initial inferred type is the annotated type of the parameter, if any: + +```py +def f(declared: int, unannotated): + reveal_type(declared) # revealed: int + reveal_type(unannotated) # revealed: Unknown +``` The variadic parameter is a variadic tuple of its annotated type; the variadic-keywords parameter is -a dictionary from strings to its annotated type. +a dictionary from strings to its annotated type: + +```py +def g(*args: int, **kwargs: int): + reveal_type(args) # revealed: tuple[int, ...] + reveal_type(kwargs) # revealed: dict[str, int] +``` + +## Unannotated parameters with defaults + +If there is no annotation but there is a default value, the inferred paramter type is the union of +the inferred type of the default value and `Unknown`: + +```py +def f(a="foo", b=0, c=True, d=None): + reveal_type(a) # revealed: Unknown | Literal["foo"] + reveal_type(b) # revealed: Unknown | Literal[0] + reveal_type(c) # revealed: Unknown | Literal[True] + reveal_type(d) # revealed: Unknown | None +``` + +This means that the code in the function body needs to handle the case where the parameter is set to +the default value: + +```py +def g(x=0): + print(x + 1) + + # error: [unsupported-operator] "Operator `+` is not supported between objects of type `Unknown | Literal[0]` and `Literal["foo"]`" + x + "foo" +``` + +But it still allows callers to pass in arguments of a wider type: + +```py +g(1.5) # fine +``` ## Parameter kinds From a24a1dd3b36a5226cb90ac92330d29d2d812f636 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 3 Aug 2026 13:46:03 +0200 Subject: [PATCH 217/390] [ty] Recognize Pydantic on extra search paths (#27429) ## Summary Recognize `pydantic` as being third-party code, and therefore activate special Pydantic support, even if the package appears in `extra-paths`. Fixes https://github.com/astral-sh/ty/issues/4159. ## Test plan Added a regression test --- crates/ty_module_resolver/src/module.rs | 2 +- crates/ty_module_resolver/src/path.rs | 11 ++- .../external/pydantic_extra_search_paths.lock | 97 +++++++++++++++++++ .../external/pydantic_extra_search_paths.md | 23 +++++ crates/ty_test/README.md | 9 ++ crates/ty_test/src/lib.rs | 48 +++++---- 6 files changed, 165 insertions(+), 25 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.lock create mode 100644 crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.md diff --git a/crates/ty_module_resolver/src/module.rs b/crates/ty_module_resolver/src/module.rs index 3a01008236..b85db3330a 100644 --- a/crates/ty_module_resolver/src/module.rs +++ b/crates/ty_module_resolver/src/module.rs @@ -440,7 +440,7 @@ impl KnownModule { let known_module = Self::from_str(name.as_str()).ok()?; let is_expected_search_path = if known_module.is_third_party() { - search_path.is_third_party() + search_path.can_contain_third_party_code() } else { search_path.is_standard_library() }; diff --git a/crates/ty_module_resolver/src/path.rs b/crates/ty_module_resolver/src/path.rs index 351f3cecaa..b63bbdb025 100644 --- a/crates/ty_module_resolver/src/path.rs +++ b/crates/ty_module_resolver/src/path.rs @@ -611,12 +611,13 @@ impl SearchPath { matches!(&*self.0, SearchPathInner::SitePackages(_)) } - /// Is the module on a search path for installed third-party code? - pub(crate) fn is_third_party(&self) -> bool { + /// Is it plausible that this search path contains third-party code? + pub(crate) fn can_contain_third_party_code(&self) -> bool { match &*self.0 { - SearchPathInner::SitePackages(_) | SearchPathInner::Editable(_) => true, - SearchPathInner::Extra(_) - | SearchPathInner::FirstParty(_) + SearchPathInner::SitePackages(_) + | SearchPathInner::Editable(_) + | SearchPathInner::Extra(_) => true, + SearchPathInner::FirstParty(_) | SearchPathInner::StandardLibraryCustom(_) | SearchPathInner::StandardLibraryVendored(_) | SearchPathInner::StandardLibraryReal(_) => false, diff --git a/crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.lock b/crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.lock new file mode 100644 index 0000000000..8f9c35e475 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.lock @@ -0,0 +1,97 @@ +version = 1 +revision = 3 +requires-python = "==3.11.*" + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "mdtest-deps" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [{ name = "pydantic", specifier = "==2.13.4" }] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] diff --git a/crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.md b/crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.md new file mode 100644 index 0000000000..59a41d8a16 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/external/pydantic_extra_search_paths.md @@ -0,0 +1,23 @@ +# Pydantic on extra search paths + +Pydantic-specific behavior still applies when the installed package is resolved from an extra search +path, such as when its `site-packages` directory is included in `PYTHONPATH`. + +```toml +[environment] +python-version = "3.11" +python-platform = "linux" +extra-paths = ["/.venv/"] + +[project] +dependencies = ["pydantic==2.13.4"] +``` + +```py +from pydantic import BaseModel, ConfigDict + +class Model(BaseModel): + model_config = ConfigDict(extra="allow") + +Model(a=1) +``` diff --git a/crates/ty_test/README.md b/crates/ty_test/README.md index 555222bd70..4b4e981ead 100644 --- a/crates/ty_test/README.md +++ b/crates/ty_test/README.md @@ -430,6 +430,15 @@ X = 1 ``` ```` +The same placeholder can be used in `environment.extra-paths`: + +````markdown +```toml +[environment] +extra-paths = ["/.venv/"] +``` +```` + ## Documentation of tests Arbitrary Markdown syntax (including of course normal prose paragraphs) is permitted (and ignored by diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index 9bca33b1b0..46a967ef80 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -14,6 +14,7 @@ use ruff_db::system::{DbWithWritableSystem as _, SystemPath, SystemPathBuf}; use ruff_db::testing::{setup_logging, setup_logging_with_filter}; use ruff_db::{Db, PythonFile}; use ruff_diagnostics::Applicability; +use ruff_python_ast::PythonVersion; use ruff_source_file::OneIndexed; use std::fmt::Write; use ty_module_resolver::{ @@ -188,24 +189,10 @@ fn run_test( { typeshed_files.push(relative_path_to_custom_typeshed.to_path_buf()); } - } else if let Some(component_index) = full_path - .components() - .position(|c| c.as_str() == "") + } else if let Some(site_packages_path) = + expand_site_packages_placeholder(&full_path, python_version) { - // If the path contains ``, we need to replace it with the - // actual site-packages directory based on the Python platform and version. - let mut components = full_path.components(); - let mut new_path: SystemPathBuf = - components.by_ref().take(component_index).collect(); - if cfg!(target_os = "windows") { - new_path.extend(["Lib", "site-packages"]); - } else { - new_path.push("lib"); - new_path.push(format!("python{python_version}")); - new_path.push("site-packages"); - } - new_path.extend(components.skip(1)); - full_path = new_path; + full_path = site_packages_path; } let temp_string; @@ -289,11 +276,12 @@ fn run_test( .unwrap_or_default() .iter() .map(|path| { - if path.is_absolute() { + let path = if path.is_absolute() { path.clone() } else { src_path.join(path) - } + }; + expand_site_packages_placeholder(&path, python_version).unwrap_or(path) }) .collect(); @@ -579,6 +567,28 @@ impl std::fmt::Display for ModuleInconsistency<'_> { } } +fn expand_site_packages_placeholder( + path: &SystemPath, + python_version: PythonVersion, +) -> Option { + let component_index = path + .components() + .position(|component| component.as_str() == "")?; + + let mut components = path.components(); + let mut expanded: SystemPathBuf = components.by_ref().take(component_index).collect(); + if cfg!(target_os = "windows") { + expanded.extend(["Lib", "site-packages"]); + } else { + expanded.push("lib"); + expanded.push(format!("python{python_version}")); + expanded.push("site-packages"); + } + expanded.extend(components.skip(1)); + + Some(expanded) +} + fn parse<'s>( short_title: &'s str, source: &'s str, From 6ffd51204910a279f07922b7f0f790d3b5bc85b5 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 3 Aug 2026 13:03:38 +0100 Subject: [PATCH 218/390] [ty] Move Unknown into ty_extensions._internal (#27430) --- crates/ty_ide/src/goto_type_definition.rs | 12 ++--- crates/ty_ide/src/inlay_hints.rs | 14 +++--- .../resources/mdtest/call/builtins.md | 4 +- .../resources/mdtest/call/function.md | 6 +-- .../resources/mdtest/call/overloads.md | 3 +- .../resources/mdtest/call/subclass_of.md | 2 +- .../resources/mdtest/call/type.md | 2 +- .../resources/mdtest/comparison/identity.md | 2 +- .../mdtest/directives/assert_never.md | 4 +- .../mdtest/directives/assert_type.md | 4 +- .../resources/mdtest/directives/cast.md | 2 +- .../mdtest/generics/legacy/variance.md | 12 ++--- .../mdtest/generics/pep695/variance.md | 16 +++---- .../resources/mdtest/implicit_type_aliases.md | 2 +- .../resources/mdtest/intersection_types.md | 16 ++++--- .../resources/mdtest/loops/for.md | 4 +- .../resources/mdtest/mro.md | 4 +- .../resources/mdtest/narrow/complex_target.md | 2 +- .../mdtest/narrow/conditionals/eq.md | 2 +- .../resources/mdtest/narrow/match.md | 13 +++--- .../resources/mdtest/protocols.md | 8 ++-- .../resources/mdtest/ty_extensions.md | 6 +-- .../resources/mdtest/type_compendium/tuple.md | 4 +- .../resources/mdtest/type_of/generics.md | 3 +- .../type_properties/is_assignable_to.md | 46 +++++++++---------- .../type_properties/is_equivalent_to.md | 28 +++++------ .../mdtest/type_properties/is_subtype_of.md | 4 +- .../mdtest/type_properties/materialization.md | 34 +++++++------- .../resources/mdtest/union_types.md | 6 +-- .../src/types/special_form.rs | 14 +++--- crates/ty_vendored/ty_extensions/__init__.pyi | 11 +---- .../ty_vendored/ty_extensions/_internal.pyi | 16 +++++++ 32 files changed, 160 insertions(+), 146 deletions(-) diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index d888e49cd3..84244ea8ae 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -900,7 +900,7 @@ mod tests { LL | a: "MyClass |" = 1 | ^^^^^^^^^^^ Clicking here info: Found 1 type definition - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- @@ -950,7 +950,7 @@ mod tests { LL | a: "MyClass | No" = 1 | ^^ Clicking here info: Found 1 type definition - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- @@ -972,7 +972,7 @@ mod tests { LL | ab: "ab" | ^^ Clicking here info: Found 1 type definition - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- @@ -994,7 +994,7 @@ mod tests { LL | x: "foobar" | ^^^^^^ Clicking here info: Found 1 type definition - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- @@ -1144,7 +1144,7 @@ mod tests { LL | x: """'list["MyClass" | "str"]' | None""" | ^^^^^^^^^ Clicking here info: Found 1 type definition - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- @@ -1904,7 +1904,7 @@ def function(): LL | x = submod | ^^^^^^ Clicking here info: Found 1 type definition - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ------- diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 35a507bf05..7ecb1de55e 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -1985,7 +1985,7 @@ Source with applied edits: --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ^^^^^^^ @@ -2010,7 +2010,7 @@ Source with applied edits: info[inlay-hint-edit]: Inlay hint edits --> main.py:1:1 | - 1 + from ty_extensions import Unknown + 1 + from ty_extensions._internal import Unknown 2 | 3 | class A: 4 | def __init__(self, y): @@ -2551,7 +2551,7 @@ Source with applied edits: | ^^^^ info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ^^^^^^^ @@ -2741,7 +2741,7 @@ Source with applied edits: info[inlay-hint-edit]: Inlay hint edits --> main.py:1:1 | - 1 + from ty_extensions import Unknown + 1 + from ty_extensions._internal import Unknown 2 + from string.templatelib import Template 3 | - a = [1, 2] @@ -5077,7 +5077,7 @@ Source with applied edits: bar([a=]1, [b=]2) --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ^^^^^^^ @@ -5088,7 +5088,7 @@ Source with applied edits: | ^^^^^^^ info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ^^^^^^^ @@ -6113,7 +6113,7 @@ Source with applied edits: | ^^^ info[inlay-hint-location]: Inlay Hint Target - --> stdlib/ty_extensions/__init__.pyi:LL:1 + --> stdlib/ty_extensions/_internal.pyi:LL:1 | LL | Unknown: _SpecialForm | ^^^^^^^ diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index 923cbe87f8..259eaa4f61 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -406,7 +406,7 @@ result to `Sized` or `object`; ideally the element type would remain `Unknown`, return type would still be used where possible. ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(xs: Unknown): # TODO: should be `list[Unknown]` @@ -481,7 +481,7 @@ error[call-non-callable]: `NotImplemented` is not callable ## `map` with generic callbacks ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown import re def _(s: Unknown | str): diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index ebb48072dc..77db74c096 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -1573,7 +1573,7 @@ Or, it can be a type that is assignable to `str`. ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(kwargs1: dict[Any, int], kwargs2: dict[Unknown, int]) -> None: f(**kwargs1) @@ -1616,7 +1616,7 @@ def _(kwargs: dict[str, int]) -> None: ### `Unknown` type ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def f(**kwargs: int) -> None: ... def _(kwargs: Unknown): @@ -1760,7 +1760,7 @@ variadic expansion should not greedily consume optional positional parameters th as explicit keyword arguments. ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def f(a: int = 0, b: int = 0, c: int = 0, fmt: str | None = None) -> None: ... def _(args: "Unknown | tuple[int, int, int]"): diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md index 7713f85ecf..ba3f3d55ee 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -1483,8 +1483,7 @@ def _(int_str: tuple[int, str], int_any: tuple[int, Any], any_any: tuple[Any, An ```pyi from typing_extensions import Iterable, overload, LiteralString, Protocol -from ty_extensions import Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions._internal import Unknown, is_assignable_to class Foo: @overload diff --git a/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md b/crates/ty_python_semantic/resources/mdtest/call/subclass_of.md index 797d719a2f..da3ff4d4ff 100644 --- a/crates/ty_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 ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(subclass_of_any: type[Any], subclass_of_unknown: type[Unknown]): reveal_type(subclass_of_any()) # revealed: Any diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 397fd71037..1d97de797c 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -518,7 +518,7 @@ them: ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def f(a: type[Any], b: type[Unknown]): reveal_type(a.__mro__) # revealed: tuple[type, ...] & Any diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/identity.md b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md index 9ae34717f5..d295ce3acf 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/identity.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md @@ -132,7 +132,7 @@ Once `value is None` has succeeded, the value can only be the `None` singleton e original type is `Unknown`. ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def f(value: Unknown) -> None: if value is None: diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md index 61ed08ddb6..25398e0ce7 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_never.md @@ -8,7 +8,7 @@ ```py from typing_extensions import assert_never, Never, Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(never: Never): assert_never(never) # fine @@ -20,7 +20,7 @@ If it is not, a `type-assertion-failure` diagnostic is emitted. ```py from typing_extensions import assert_never, Never, Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(): assert_never(0) # snapshot: type-assertion-failure diff --git a/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md index ae51bba28f..88ab9a76b2 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/assert_type.md @@ -163,7 +163,7 @@ def _(f: F): from typing import Any from typing_extensions import Literal, assert_type -from ty_extensions import Unknown +from ty_extensions._internal import Unknown # Any and Unknown are considered equivalent def _(a: Unknown, b: Any): @@ -188,7 +188,7 @@ Tuple types with the same elements are the same. ```py from typing_extensions import Any, assert_type -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(a: tuple[int, str, bytes]): assert_type(a, tuple[int, str, bytes]) # fine diff --git a/crates/ty_python_semantic/resources/mdtest/directives/cast.md b/crates/ty_python_semantic/resources/mdtest/directives/cast.md index d4a148d009..0311c79766 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/cast.md @@ -63,7 +63,7 @@ the gradual guarantee and leads to cascading errors when an object is inferred a `Unknown` due to a missing import or similar. ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def f(x: Any, y: Unknown, z: Any | str | int): a = cast(dict[str, Any], x) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md index a047ae784c..268661e51d 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md @@ -22,8 +22,8 @@ Types that "produce" data on demand are covariant in their typevar. If you expec get from the sequence is a valid `int`. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Generic, TypeVar class A: ... @@ -104,8 +104,8 @@ 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 ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Generic, TypeVar class A: ... @@ -217,8 +217,8 @@ 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 ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Generic, TypeVar class A: ... diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md index 497f1b1741..898c872655 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md @@ -29,8 +29,8 @@ Types that "produce" data on demand are covariant in their typevar. If you expec get from the sequence is a valid `int`. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Never class A: ... @@ -108,8 +108,8 @@ 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 ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Never class A: ... @@ -216,8 +216,8 @@ 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 ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Never class A: ... @@ -292,8 +292,8 @@ If inference for a PEP 695 type parameter would otherwise conclude bivariance be parameter is unused, we fall back to covariance instead. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to, is_equivalent_to, is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, is_equivalent_to, is_subtype_of from typing import Any, Never class A: ... diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 5a714aaafc..d1cb3caba1 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -34,7 +34,7 @@ We also support unions in type aliases: ```py from typing_extensions import Any, Never, Literal, LiteralString, Tuple, Annotated, Optional, Union, Callable, TypeVar -from ty_extensions import Unknown +from ty_extensions._internal import Unknown T = TypeVar("T") diff --git a/crates/ty_python_semantic/resources/mdtest/intersection_types.md b/crates/ty_python_semantic/resources/mdtest/intersection_types.md index e0f35c42bf..68850a4335 100644 --- a/crates/ty_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/ty_python_semantic/resources/mdtest/intersection_types.md @@ -727,7 +727,8 @@ simplified, due to the fact that a `LiteralString` inhabitant is known to have ` exactly `str` (and not a subclass of `str`): ```py -from ty_extensions import AlwaysTruthy, AlwaysFalsy, Unknown +from ty_extensions import AlwaysTruthy, AlwaysFalsy +from ty_extensions._internal import Unknown from typing_extensions import LiteralString def f( @@ -826,7 +827,8 @@ This slightly strange-looking test is a regression test for a mistake that was n . ```py -from ty_extensions import AlwaysFalsy, Unknown +from ty_extensions import AlwaysFalsy +from ty_extensions._internal import Unknown from typing_extensions import Literal def _(x: str & Unknown & AlwaysFalsy & Literal[""]): @@ -842,7 +844,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 ty_extensions import Unknown +from ty_extensions._internal import Unknown from typing_extensions import Any, Never class P: ... @@ -872,7 +874,7 @@ The intersection of an unknown set of runtime values with (another) unknown set still an unknown set of runtime values: ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown from typing_extensions import Any class P: ... @@ -907,7 +909,7 @@ of another unknown set of values is not necessarily empty, so we keep the positi ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def any( i1: Any & ~Any, @@ -930,7 +932,7 @@ Gradually-equivalent types can be simplified out of intersections: ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def mixed( i1: Any & Unknown, @@ -1349,7 +1351,7 @@ For any gradual type `G`, `Invariant[G] & Invariant[Any] = Invariant[G]`. ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class P: ... class Q: ... diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index 142a5b0646..b85d7dadd4 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -1490,8 +1490,8 @@ A class literal can be iterated over if it has `Any` or `Unknown` in its MRO, si ```py from unresolved_module import SomethingUnknown # error: [unresolved-import] from typing import Any, Iterable -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import TypeOf, is_assignable_to, reveal_mro +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, TypeOf, is_assignable_to, reveal_mro class Foo(SomethingUnknown): ... diff --git a/crates/ty_python_semantic/resources/mdtest/mro.md b/crates/ty_python_semantic/resources/mdtest/mro.md index fe78f421f0..4f123c4abe 100644 --- a/crates/ty_python_semantic/resources/mdtest/mro.md +++ b/crates/ty_python_semantic/resources/mdtest/mro.md @@ -227,8 +227,8 @@ guarantee: ```py from typing import Any -from ty_extensions import Unknown, Intersection -from ty_extensions._internal import reveal_mro +from ty_extensions import Intersection +from ty_extensions._internal import Unknown, reveal_mro def f(x: type[Any], y: Intersection[Unknown, type[Any]]): class Foo(x): ... diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md b/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md index 39413d94ef..508f853c0e 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/complex_target.md @@ -7,7 +7,7 @@ We support type narrowing for attributes and subscripts. ### Basic ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class C: x: int | None = None diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index 49e7ebb59b..c404491f42 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -1754,7 +1754,7 @@ import sys from enum import Enum, IntEnum from typing import Any, Literal, TypeAlias, TypeVar -from ty_extensions import Unknown +from ty_extensions._internal import Unknown from typing_extensions import assert_never, assert_type T = TypeVar("T", bound=object) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index af988bb001..0d266c2a34 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -365,7 +365,7 @@ a fixed-length tuple, we can determine exactly which elements appear in that lis ```py from typing import Any, Literal, TypeVar -from ty_extensions import Unknown +from ty_extensions._internal import Unknown BoundTupleT = TypeVar("BoundTupleT", bound=tuple[int] | tuple[str]) @@ -1444,7 +1444,7 @@ declared by the pattern class. ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class GradualPatternBox: value: int @@ -1613,7 +1613,7 @@ keep the same uncertainty as the subject. ```py from typing import Any -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def test_match_gradual_mapping_captures(any_value: Any, unknown_value: Unknown) -> None: match any_value: @@ -1736,7 +1736,7 @@ also keeps the uncertainty of an `Any` or `Unknown` subject. ```py from typing import Any, Generic, Literal, TypeVar, final from typing_extensions import TypedDict -from ty_extensions import Unknown +from ty_extensions._internal import Unknown TagT = TypeVar("TagT") PayloadT = TypeVar("PayloadT") @@ -1948,7 +1948,8 @@ exercise three separate checks: an optional field, an unknown key, and a non-str ```py from typing import Any, Literal, Protocol, TypeVar, TypedDict -from ty_extensions import Intersection, Unknown +from ty_extensions import Intersection +from ty_extensions._internal import Unknown class RequiredPayload(TypedDict): tag: Literal["int"] @@ -3116,7 +3117,7 @@ python-version = "3.11" ```py from enum import Enum, IntEnum, StrEnum, auto from typing import Literal, assert_never -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class Color(StrEnum): RED = "r" diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 43c2b7f9f4..be83b96944 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -5685,7 +5685,7 @@ python-version = "3.12" ```py from typing import Protocol, cast -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class UnknownMethod[T](Protocol): def method(self) -> Unknown: ... @@ -5700,7 +5700,7 @@ checked. ```py from typing import Protocol, cast -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class IntProperty[T](Protocol): @property @@ -5723,7 +5723,7 @@ has been replaced by `int`, so the cast is redundant. ```py from typing import Protocol, TypeVar, cast -from ty_extensions import Unknown +from ty_extensions._internal import Unknown T = TypeVar("T", bound=Unknown) @@ -5776,7 +5776,7 @@ example, descriptor overload resolution exposes `Unknown` only through the neste ```py from typing import Protocol, cast, overload -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class Descriptor: @overload diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index 64436541b8..5b0e38225c 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -91,8 +91,8 @@ 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 ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_assignable_to, reveal_mro +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to, reveal_mro static_assert(is_assignable_to(Unknown, int)) static_assert(is_assignable_to(int, Unknown)) @@ -111,7 +111,7 @@ class C(Unknown): ... # revealed: (, Unknown, ) reveal_mro(C) -# error: "Special form `ty_extensions.Unknown` expected no type parameter" +# error: "Special form `ty_extensions._internal.Unknown` expected no type parameter" u: Unknown[str] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md index 05781cd147..69606757af 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md @@ -506,8 +506,8 @@ An unspecialized tuple is equivalent to `tuple[Any, ...]` and `tuple[Unknown, .. ```py from typing_extensions import Any, assert_type -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_equivalent_to static_assert(is_equivalent_to(tuple[Any, ...], tuple[Unknown, ...])) diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md index 37be37f7f7..0b824d6e83 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md @@ -97,7 +97,8 @@ reveal_type(union_bound(Multiply)) # revealed: Multiply ## Union ```py -from ty_extensions import Intersection, Unknown +from ty_extensions import Intersection +from ty_extensions._internal import Unknown def _[T: int](x: type | type[T]): reveal_type(x()) # revealed: Any diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 2410a01459..98dfb302e3 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -45,8 +45,8 @@ static_assert(not is_assignable_to(Child1, Child2)) The dynamic type is assignable to or from any type. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing import Any, Literal static_assert(is_assignable_to(Unknown, Literal[1])) @@ -208,8 +208,8 @@ 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 ty_extensions import static_assert, Unknown -from ty_extensions._internal import TypeOf, is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, TypeOf, is_assignable_to from typing import Any static_assert(is_assignable_to(type, type)) @@ -687,8 +687,8 @@ static_assert(not is_assignable_to(tuple[int, *tuple[int, ...], int], tuple[int, ## Union types ```py -from ty_extensions import AlwaysTruthy, AlwaysFalsy, static_assert, Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions import AlwaysTruthy, AlwaysFalsy, static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing_extensions import Literal, Any, LiteralString static_assert(is_assignable_to(int, int | str)) @@ -813,8 +813,8 @@ The root cause was that we failed to properly materialize a `Callable[..., Unkno `Unknown` return type originated from a missing annotation. ```pyi -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import RegularCallableTypeOf, is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, RegularCallableTypeOf, is_assignable_to from typing import Callable # `Callable[..., Unknown]` has explicit Unknown return type @@ -906,8 +906,8 @@ 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 ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing import Literal, Any static_assert(is_assignable_to(str, object)) @@ -927,8 +927,8 @@ static_assert(is_assignable_to(type[Any], object)) any type is assignable to them: ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing import Literal, Any static_assert(is_assignable_to(str, Any)) @@ -958,8 +958,8 @@ static_assert(is_assignable_to(type[Any], Unknown)) assignable to any arbitrary type. ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing_extensions import Never, Any, Literal static_assert(is_assignable_to(Never, str)) @@ -979,8 +979,8 @@ static_assert(is_assignable_to(Never, type[Any])) including `Never`. ```pyi -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing_extensions import Never, Any static_assert(is_assignable_to(Any, Never)) @@ -1001,8 +1001,8 @@ are covered in the [subtyping tests](./is_subtype_of.md#callable). ### Return type ```py -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import RegularCallableTypeOf, is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, RegularCallableTypeOf, is_assignable_to from typing import Any, Callable static_assert(is_assignable_to(Callable[[], Any], Callable[[], int])) @@ -1158,7 +1158,7 @@ python-version = "3.12" ```py from typing import Any, Callable -from ty_extensions import Unknown +from ty_extensions._internal import Unknown class C: def concrete[T](self: T) -> type[int]: @@ -1602,8 +1602,8 @@ static_assert(not is_assignable_to(TypeOf[GenericFinalClass[str]], type[GenericF `TypeGuard[...]` and `TypeIs[...]` are always assignable to `bool`. ```py -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_assignable_to from typing_extensions import Any, TypeGuard, TypeIs static_assert(is_assignable_to(TypeGuard[Unknown], bool)) @@ -1654,8 +1654,8 @@ takes_plugin_predicate(callable) ## `ParamSpec` ```py -from ty_extensions import static_assert, Unknown -from ty_extensions._internal import TypeOf, is_assignable_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, TypeOf, is_assignable_to from typing import ParamSpec, Mapping, Callable, Any P = ParamSpec("P") diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index cb20fd5a4b..65f8c67719 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -14,8 +14,8 @@ materializations of `B`, and all materializations of `B` are also materializatio ```py from typing_extensions import Literal, LiteralString, Protocol, Never -from ty_extensions import Unknown, static_assert, AlwaysTruthy, AlwaysFalsy -from ty_extensions._internal import TypeOf, is_equivalent_to +from ty_extensions import static_assert, AlwaysTruthy, AlwaysFalsy +from ty_extensions._internal import Unknown, TypeOf, is_equivalent_to from enum import Enum class Answer(Enum): @@ -72,8 +72,8 @@ static_assert(is_equivalent_to(type, type[object])) ```py from typing import Any from typing_extensions import Literal, LiteralString, Never -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_equivalent_to static_assert(is_equivalent_to(Any, Any)) static_assert(is_equivalent_to(Unknown, Unknown)) @@ -100,8 +100,8 @@ For a covariant bounded type parameter, this applies to aliases containing eithe ```py from typing import Any -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_equivalent_to type AnyTuple = tuple[Any, ...] type UnknownTuple = tuple[Unknown, ...] @@ -178,8 +178,8 @@ static_assert(not is_equivalent_to(BoundedInvariant[tuple[Any, ...]], BoundedInv ```pyi from typing import Any, Literal, TypeAlias -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_equivalent_to from enum import Enum static_assert(is_equivalent_to(str | int, str | int)) @@ -242,8 +242,8 @@ static_assert(is_equivalent_to(Any, ~None & Unknown | Unknown)) ## Tuples ```py -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_equivalent_to from typing import Any static_assert(is_equivalent_to(tuple[str, Any], tuple[str, Unknown])) @@ -461,8 +461,8 @@ Two unions containing different `Callable` types are equivalent even if the unio ordered: ```py -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import RegularCallableTypeOf, is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, RegularCallableTypeOf, is_equivalent_to def f(x): ... def g(x: Unknown): ... @@ -579,8 +579,8 @@ gradual types. The cases with fully static types and using different combination are covered above. ```py -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import CallableTypeOf, RegularCallableTypeOf, TypeOf, is_equivalent_to +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, CallableTypeOf, RegularCallableTypeOf, TypeOf, is_equivalent_to from typing import Any, Callable static_assert(is_equivalent_to(Callable[..., int], Callable[..., int])) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index e9756bb3f4..f9b6a41ccb 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -890,8 +890,8 @@ of the first type represent sets of values that are a subset of every possible s represented by a materialization of the second type. ```pyi -from ty_extensions import Unknown, static_assert -from ty_extensions._internal import is_subtype_of +from ty_extensions import static_assert +from ty_extensions._internal import Unknown, is_subtype_of from typing_extensions import Any static_assert(not is_subtype_of(Any, Any)) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md index 84692f51b0..0cbc637f23 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -29,7 +29,8 @@ The dynamic type at the top-level is replaced with `object`. ```py from typing import Any, Callable -from ty_extensions import Unknown, Top +from ty_extensions import Top +from ty_extensions._internal import Unknown def _(top_any: Top[Any], top_unknown: Top[Unknown]): reveal_type(top_any) # revealed: object @@ -56,7 +57,8 @@ The dynamic type at the top-level is replaced with `Never`. ```py from typing import Any, Callable -from ty_extensions import Unknown, Bottom +from ty_extensions import Bottom +from ty_extensions._internal import Unknown def _(bottom_any: Bottom[Any], bottom_unknown: Bottom[Unknown]): reveal_type(bottom_any) # revealed: Never @@ -150,8 +152,8 @@ python-version = "3.12" ```py from typing import Any, Callable -from ty_extensions import Unknown, Bottom, Top -from ty_extensions._internal import TypeOf +from ty_extensions import Bottom, Top +from ty_extensions._internal import Unknown, TypeOf type C1 = Callable[[Any, Unknown], Any] @@ -288,8 +290,8 @@ python-version = "3.12" ```py from typing import Any, Never -from ty_extensions import Unknown, Bottom, Top, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import Unknown, is_equivalent_to static_assert(is_equivalent_to(Top[tuple[Any, int]], tuple[object, int])) static_assert(is_equivalent_to(Bottom[tuple[Any, int]], Never)) @@ -351,8 +353,8 @@ python-version = "3.12" ```py from typing import Any -from ty_extensions import Unknown, Bottom, Top, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import Unknown, is_equivalent_to static_assert(is_equivalent_to(Top[Any | int], object)) static_assert(is_equivalent_to(Bottom[Any | int], int)) @@ -404,8 +406,8 @@ All positions in an intersection are covariant. ```pyi from typing import Any from typing_extensions import Never -from ty_extensions import Unknown, Bottom, Top, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import Unknown, is_equivalent_to static_assert(is_equivalent_to(Top[Any & int], int)) static_assert(is_equivalent_to(Bottom[Any & int], Never)) @@ -460,8 +462,8 @@ All positions in a negation are contravariant. ```pyi from typing import Any from typing_extensions import Never -from ty_extensions import Unknown, Bottom, Top, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import Unknown, is_equivalent_to # ~Any is still Any, so the top materialization is object static_assert(is_equivalent_to(Top[~Any], object)) @@ -483,8 +485,8 @@ python-version = "3.12" ```py from typing import Any from typing_extensions import Never -from ty_extensions import Unknown, Bottom, Top, static_assert -from ty_extensions._internal import is_equivalent_to +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import Unknown, is_equivalent_to static_assert(is_equivalent_to(Top[type[Any]], type)) static_assert(is_equivalent_to(Bottom[type[Any]], Never)) @@ -575,8 +577,8 @@ python-version = "3.12" ```py from typing import Any, Never, TypeVar -from ty_extensions import Unknown, Bottom, Top, static_assert -from ty_extensions._internal import is_subtype_of +from ty_extensions import Bottom, Top, static_assert +from ty_extensions._internal import Unknown, is_subtype_of def bounded_by_gradual[T: Any](t: T) -> None: # Top materialization of `T: Any` is `T: object` diff --git a/crates/ty_python_semantic/resources/mdtest/union_types.md b/crates/ty_python_semantic/resources/mdtest/union_types.md index 67539036d7..ebf3edf134 100644 --- a/crates/ty_python_semantic/resources/mdtest/union_types.md +++ b/crates/ty_python_semantic/resources/mdtest/union_types.md @@ -153,7 +153,7 @@ def _( ## Do not erase `Unknown` ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(u1: Unknown | str, u2: str | Unknown) -> None: reveal_type(u1) # revealed: Unknown | str @@ -166,7 +166,7 @@ Since `Unknown` is a gradual type, it is not a subtype of anything, but multiple union are still redundant: ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknown | Unknown) -> None: reveal_type(u1) # revealed: Unknown | str @@ -179,7 +179,7 @@ def _(u1: Unknown | Unknown | str, u2: Unknown | str | Unknown, u3: str | Unknow Simplifications still apply when `Unknown` is present. ```py -from ty_extensions import Unknown +from ty_extensions._internal import Unknown def _(u1: int | Unknown | bool) -> None: reveal_type(u1) # revealed: int | Unknown diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index e427b2f96a..2c10ab4be1 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -76,7 +76,7 @@ pub enum SpecialFormType { NoReturn, /// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`) Never, - /// The symbol `ty_extensions.Unknown` + /// The symbol `ty_extensions._internal.Unknown` Unknown, /// The symbol `ty_extensions._internal.Divergent` Divergent, @@ -574,15 +574,15 @@ impl SpecialFormType { matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) } - Self::Unknown - | Self::AlwaysTruthy + Self::AlwaysTruthy | Self::AlwaysFalsy | Self::Not | Self::Top | Self::Bottom | Self::Intersection => module.is_ty_extensions(), - Self::Divergent + Self::Unknown + | Self::Divergent | Self::Todo | Self::TypeOf | Self::CallableTypeOf @@ -805,15 +805,15 @@ impl SpecialFormType { SpecialFormType::CollectionsAbcCallable => &[KnownModule::CollectionsAbc], - SpecialFormType::Unknown - | SpecialFormType::AlwaysTruthy + SpecialFormType::AlwaysTruthy | SpecialFormType::AlwaysFalsy | SpecialFormType::Not | SpecialFormType::Intersection | SpecialFormType::Top | SpecialFormType::Bottom => &[KnownModule::TyExtensions], - SpecialFormType::Divergent + SpecialFormType::Unknown + | SpecialFormType::Divergent | SpecialFormType::Todo | SpecialFormType::TypeOf | SpecialFormType::CallableTypeOf diff --git a/crates/ty_vendored/ty_extensions/__init__.pyi b/crates/ty_vendored/ty_extensions/__init__.pyi index f6684b4bc9..5d46acdf50 100644 --- a/crates/ty_vendored/ty_extensions/__init__.pyi +++ b/crates/ty_vendored/ty_extensions/__init__.pyi @@ -1,4 +1,6 @@ # ruff: noqa: PYI021 +"""Experimental ty APIs intended to be exposed to end users.""" + import collections.abc import sys from typing import Any, ClassVar, Protocol, _SpecialForm @@ -106,15 +108,6 @@ eagerly. # Types # ----- -Unknown: _SpecialForm -""" -`Unknown` is a dynamic type inferred due to missing type information or an inference error. - -ty infers `Unknown` for unannotated values with insufficient type information. It also uses it as a -fallback after certain type errors. This contrasts with `Any`, which represents an *explicitly* -annotated dynamic type. Like `Any`, however, it is a dynamic type, so ty allows any operation on it. -""" - AlwaysTruthy: _SpecialForm """ `AlwaysTruthy` represents the set of all objects that always evaluate to `True` in a boolean diff --git a/crates/ty_vendored/ty_extensions/_internal.pyi b/crates/ty_vendored/ty_extensions/_internal.pyi index 9a612c4d81..d269f415ff 100644 --- a/crates/ty_vendored/ty_extensions/_internal.pyi +++ b/crates/ty_vendored/ty_extensions/_internal.pyi @@ -1,4 +1,11 @@ # ruff: noqa: PYI021 +""" +Internal-only symbols for special forms and type-system tests. + +Some symbols provide definitions and on-hover documentation for special forms. Others exist only as +helpers for ty's tests. None of these symbols are intended to be directly imported by end users. +""" + import types from enum import Enum from typing import Any, Protocol, _SpecialForm @@ -49,6 +56,15 @@ ordinary `Callable[...]` types in type-theoretic tests. # Types # ----- +Unknown: _SpecialForm +""" +`Unknown` is a dynamic type inferred due to missing type information or an inference error. + +ty infers `Unknown` for unannotated values with insufficient type information. It also uses it as a +fallback after certain type errors. This contrasts with `Any`, which represents an *explicitly* +annotated dynamic type. Like `Any`, however, it is a dynamic type, so ty allows any operation on it. +""" + Todo: _SpecialForm """ `@Todo` is a dynamic type inferred due to a known missing feature or incomplete implementation in From acc1b0f263ecb682c82c7c0a68c060807a074cb1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 3 Aug 2026 11:34:12 -0400 Subject: [PATCH 219/390] [ty] Reject out-of-scope ParamSpec components (#27378) ## Summary Make `P.args` and `P.kwargs` refer to an existing `ParamSpec` binding instead of creating one themselves. This is now rejected: ```python P = ParamSpec("P") def f(*args: P.args, **kwargs: P.kwargs) -> None: ... ``` `P` must be bound by another parameter or a visible enclosing scope. A `ParamSpec` attached to an enclosing factory's returned callable remains visible inside the factory, so this is still valid: ```python def factory() -> Callable[P, int]: def inner(*args: P.args, **kwargs: P.kwargs) -> int: return 1 return inner ``` Nested classes still hide bindings from outer classes. --- .../mdtest/generics/legacy/callables.md | 16 +++ .../mdtest/generics/legacy/paramspec.md | 124 +++++++++++++++++- .../resources/mdtest/overloads.md | 24 ++++ crates/ty_python_semantic/src/types.rs | 16 +-- .../ty_python_semantic/src/types/function.rs | 6 +- .../ty_python_semantic/src/types/generics.rs | 121 ++++++++++++++++- .../src/types/infer/builder/function.rs | 38 ++---- .../infer/builder/paramspec_validation.rs | 58 +++++++- .../src/types/signatures.rs | 40 +++++- .../ty_python_semantic/src/types/typevar.rs | 11 ++ 10 files changed, 403 insertions(+), 51 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md index 911f87d723..06eab72da0 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/callables.md @@ -93,6 +93,8 @@ def decorator_factory() -> IdentityCallable[T]: return fn # revealed: ty_extensions._internal.GenericContext[T@decorator] reveal_type(generic_context(decorator)) + # revealed: Literal[1] + reveal_type(decorator(1)) return decorator @@ -232,6 +234,20 @@ reveal_type(decorator_factory()(identity)) reveal_type(decorator_factory()(identity)(1)) ``` +A legacy factory's return statements are checked against the lexical form of its return type. This +also applies when the returned callable accepts and returns another callable: + +```py +from typing import NoReturn + +class WrappedCallable: + def __call__(self, *args: object, **kwargs: object) -> NoReturn: + raise NotImplementedError + +def nested_callable_factory() -> Callable[[Callable[P, T]], Callable[P, T]]: + return lambda callback: WrappedCallable() +``` + If the typevar also appears in a parameter, it is the function that is generic, and the returned `Callable` is not: diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index f6a9befe22..769951e064 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -369,6 +369,7 @@ annotated types of `*args` and `**kwargs` respectively. ```py from typing import Generic, Callable, ParamSpec +from ty_extensions._internal import generic_context P = ParamSpec("P") @@ -397,14 +398,83 @@ def foo1(c: Callable[P, int]) -> None: # error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`" **kwargs: int, ) -> None: ... +``` + +`P.args` and `P.kwargs` do not bind `P` themselves. They must refer to a `ParamSpec` bound by +another parameter annotation or a visible enclosing generic context. A return annotation on the same +function is not sufficient. A generic outer class does not make its `ParamSpec` visible across a +nested class boundary. -# TODO: error +```py +# snapshot: unbound-type-variable def bar1(*args: P.args, **kwargs: P.kwargs) -> None: pass +# error: [unbound-type-variable] "ParamSpec `P` is not in scope" +def return_only(*args: P.args, **kwargs: P.kwargs) -> Callable[P, int]: + raise NotImplementedError + class Foo1: - # TODO: error + # error: [unbound-type-variable] "ParamSpec `P` is not in scope" def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Outer(Generic[P]): + def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... + + class Inner: + # error: [unbound-type-variable] "ParamSpec `P` is not in scope" + def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... + + def method_with_nested_class(self, callback: Callable[P, int]) -> None: + class Inner: + # error: [unbound-type-variable] "ParamSpec `P` is not in scope" + def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... + + def method_with_components(self, *outer_args: P.args, **outer_kwargs: P.kwargs) -> None: + class Inner: + # error: [unbound-type-variable] "ParamSpec `P` is not in scope" + def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... +``` + +```snapshot +error[unbound-type-variable]: ParamSpec `P` is not in scope + --> src/mdtest_snippet.py:32:17 + | +32 | def bar1(*args: P.args, **kwargs: P.kwargs) -> None: + | ^^^^^^ -------- This component uses the same out-of-scope ParamSpec +``` + +A `ParamSpec` moved to an enclosing factory's returned callable remains lexically visible within the +factory's body. A nested function referring to it through its components owns its own binding, +consistently with an ordinary return-only `TypeVar`: + +```py +def callable_factory() -> Callable[P, int]: + def nested(*args: P.args, **kwargs: P.kwargs) -> int: + return 1 + + def nested_with_parameter(callback: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> int: + return callback(*args, **kwargs) + + # revealed: ty_extensions._internal.GenericContext[P@nested_with_parameter] + reveal_type(generic_context(nested_with_parameter)) + return nested + +def repeated_paramspec_factory() -> Callable[P, Callable[P, int]]: + def nested(*args: P.args, **kwargs: P.kwargs) -> Callable[P, int]: + callback: Callable[P, int] + raise NotImplementedError + + return nested + +def nested_components_factory() -> Callable[P, int]: + def outer(*args: P.args, **kwargs: P.kwargs) -> int: + def inner(*inner_args: P.args, **inner_kwargs: P.kwargs) -> int: + return 1 + + return inner(*args, **kwargs) + + return outer ``` And, they need to be used together. @@ -457,6 +527,56 @@ def bar(c: Callable[P, int]) -> None: def f4(*a: P.args, x: int, **kw: P.kwargs) -> None: ... ``` +## Return-only ParamSpecs own nested callables + +A legacy `ParamSpec` appearing only in a factory's returned callable belongs to the callable, just +as a return-only `TypeVar` does. The nested callable should therefore own its `ParamSpec`, making it +possible to infer concrete arguments when the callable is used inside the factory. + +```py +from typing import Callable, ParamSpec, TypeVar +from ty_extensions._internal import generic_context + +P = ParamSpec("P") +T = TypeVar("T") + +def takes_int(value: int) -> int: + return value + +def paramspec_factory() -> Callable[P, int]: + def nested(*args: P.args, **kwargs: P.kwargs) -> int: + reveal_type(args) # revealed: P@nested.args + return 1 + + def with_callback(callback: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> int: + reveal_type(callback) # revealed: (**P@with_callback) -> int + return callback(*args, **kwargs) + + reveal_type(generic_context(nested)) # revealed: ty_extensions._internal.GenericContext[P@nested] + reveal_type(generic_context(with_callback)) # revealed: ty_extensions._internal.GenericContext[P@with_callback] + reveal_type(with_callback(takes_int, 1)) # revealed: int + return nested + +def typevar_factory() -> Callable[[T], int]: + def nested(value: T) -> int: + reveal_type(value) # revealed: T@nested + return 1 + + reveal_type(generic_context(nested)) # revealed: ty_extensions._internal.GenericContext[T@nested] + return nested +``` + +A genuinely generic enclosing function still owns the `ParamSpec` captured by its nested callable. + +```py +def public_paramspec(callback: Callable[P, int]) -> None: + def nested(*args: P.args, **kwargs: P.kwargs) -> int: + reveal_type(args) # revealed: P@public_paramspec.args + return callback(*args, **kwargs) + + reveal_type(generic_context(nested)) # revealed: None +``` + ## Specializing generic classes explicitly ```py diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index ec4e76c179..8b3f9cdc04 100644 --- a/crates/ty_python_semantic/resources/mdtest/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -972,6 +972,30 @@ def generic_parameter_type(x: int) -> int | str: return x ``` +A method that refers to a type variable from its enclosing class is not itself generic. In +particular, overload consistency must still account for keyword names that may be included in an +enclosing `ParamSpec`: + +```py +from typing import Generic, ParamSpec, overload + +P = ParamSpec("P") + +class Task(Generic[P]): + @overload + # error: [invalid-overload] "Implementation does not accept all arguments of this overload" + def submit(self: "Task[P]", *args: P.args, **kwargs: P.kwargs) -> int: ... + @overload + def submit(self: "Task[P]", value: int) -> int: ... + def submit( + self: "Task[P]", + *args: object, + return_state: bool = False, + **kwargs: object, + ) -> int: + return 1 +``` + ### Decorated implementation consistency Decorators on an overload implementation apply only to the implementation signature. The decorated diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cc74584def..ab8e205c8a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4423,17 +4423,11 @@ impl<'db> Type<'db> { ty.map(Place::bound).unwrap_or_default().into() } - Type::TypeVar(typevar) if name_str == "args" && typevar.is_paramspec(db) => { - Place::declared(Type::TypeVar( - typevar.with_paramspec_attr(db, ParamSpecAttrKind::Args), - )) - .into() - } - Type::TypeVar(typevar) if name_str == "kwargs" && typevar.is_paramspec(db) => { - Place::declared(Type::TypeVar( - typevar.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs), - )) - .into() + Type::TypeVar(typevar) + if typevar.is_paramspec(db) + && let Some(attr) = ParamSpecAttrKind::from_name(name_str) => + { + Place::declared(Type::TypeVar(typevar.with_paramspec_attr(db, attr))).into() } Type::TypeVar(typevar) => { let receiver = receiver.unwrap_or(this); diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 8c44e50273..e609f535ec 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -97,7 +97,7 @@ use crate::types::{ SubclassOfType, Truthiness, Type, TypeContext, TypeMapping, TypeVarBoundOrConstraints, UnionBuilder, UnionType, binding_type, definition_expression_type, walk_signature, }; -use crate::{Db, FxOrderSet, Program, ProgramEnvironment}; +use crate::{Db, FxOrderSet, ProgramEnvironment}; use ty_python_core::ast_ids::HasScopedUseId; use ty_python_core::definition::Definition; use ty_python_core::scope::ScopeId; @@ -1343,10 +1343,6 @@ impl<'db> FunctionType<'db> { self.literal(db).last_definition.python_file(db) } - pub(crate) fn program(self, db: &'db dyn Db) -> Program { - self.literal(db).last_definition.body_scope(db).program(db) - } - /// Returns the AST node for this function. pub(super) fn node<'ast>( self, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index e6acbb8a02..7be886863c 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -19,7 +19,9 @@ use crate::types::relation::{ DisjointnessChecker, HasRelationToVisitor, IsDisjointVisitor, TypeRelation, TypeRelationChecker, TypeVarEvaluation, }; -use crate::types::signatures::{CallableSignature, Parameters, SignatureRelationVisitor}; +use crate::types::signatures::{ + CallableSignature, Parameters, ReturnCallableTypeVarScope, SignatureRelationVisitor, +}; use crate::types::tuple::{ TupleSpec, TupleSpecBuilder, TupleType, VariableSegment, walk_tuple_type, }; @@ -91,6 +93,71 @@ pub(crate) fn bind_typevar<'db>( typevar_binding_context: Option>, typevar: TypeVarInstance<'db>, ) -> Option> { + find_typevar_binding( + db, + index, + containing_scope, + typevar, + ReturnCallableTypeVarScope::Public, + ) + .or_else(|| { + typevar_binding_context.map(|typevar_binding_context| { + typevar.with_binding_context(db, typevar_binding_context) + }) + }) +} + +/// Resolves a reference to a type variable that must already be bound. +/// +/// Unlike [`bind_typevar`], this function never introduces a binding in the current context. It +/// also uses the lexical form of enclosing function signatures, in which type variables moved to +/// a returned callable's public generic context are still visible within the function body. This +/// lets `P.args` and `P.kwargs` validation establish that an enclosing `ParamSpec` is in scope +/// without changing the binding selected for the current function's signature. +pub(crate) fn resolve_typevar_reference<'db>( + db: &'db dyn Db, + index: &SemanticIndex<'db>, + containing_scope: FileScopeId, + typevar: TypeVarInstance<'db>, +) -> Option> { + find_typevar_binding( + db, + index, + containing_scope, + typevar, + ReturnCallableTypeVarScope::Lexical, + ) +} + +/// Finds the nearest visible binding under the requested treatment of return-only callable type +/// variables. +/// +/// Captured `ParamSpec` bindings are recovered from component annotations because those bindings +/// are deliberately excluded from a nested function's own generic context. A binding owned by a +/// class is hidden after the search crosses a nested class boundary. +fn find_typevar_binding<'db>( + db: &'db dyn Db, + index: &SemanticIndex<'db>, + containing_scope: FileScopeId, + typevar: TypeVarInstance<'db>, + return_callable_typevar_scope: ReturnCallableTypeVarScope, +) -> Option> { + /// Returns whether a binding remains visible after crossing an inner class boundary. + /// + /// Class-owned bindings are hidden by the inner class; function-owned and synthetic bindings + /// remain visible. + fn is_visible_across_class_boundary<'db>( + db: &'db dyn Db, + bound: BoundTypeVarInstance<'db>, + crossed_class_scope: bool, + ) -> bool { + !crossed_class_scope + || !bound + .binding_context(db) + .definition() + .is_some_and(|definition| matches!(definition.kind(db), DefinitionKind::Class(_))) + } + // typing.Self is treated like a legacy typevar, but doesn't follow the same scoping rules. It // is always bound to the outermost method in the nearest enclosing class. The walk looks for a // (function, class) pair in the scope hierarchy. The caller (`typing_self`) is responsible for @@ -138,12 +205,39 @@ pub(crate) fn bind_typevar<'db>( } continue; } - let generic_context = GenericContext::of_node(db, ancestor_scope.node(), index); + if typevar.is_paramspec(db) + && let NodeWithScopeKind::Function(function) = ancestor_scope.node() + { + let definition = index.expect_single_definition(function); + if let Some(function_ty) = + infer_definition_types(db, definition).function_type(definition) + { + let signature = function_ty + .last_definition_raw_signature(db, ReturnCallableTypeVarScope::Lexical); + if let Some(bound) = signature.paramspec_component_binding(db, typevar) + && bound.binding_context(db).definition() != Some(definition) + && is_visible_across_class_boundary(db, bound, crossed_class_scope) + { + return Some(bound); + } + } + } + let generic_context = match return_callable_typevar_scope { + ReturnCallableTypeVarScope::Lexical => { + GenericContext::lexical_of_node(db, ancestor_scope.node(), index) + } + ReturnCallableTypeVarScope::Public => { + GenericContext::of_node(db, ancestor_scope.node(), index) + } + }; // If we've already crossed a class boundary, skip class-scoped generic contexts. // This prevents inner classes from accessing type parameters of outer classes. + // An enclosing function's context can also retain a type variable originally bound by its + // enclosing class, so check the binding context as well as the ancestor node. if (!is_class_scope || !crossed_class_scope) && let Some(generic_context) = generic_context && let Some(bound) = generic_context.binds_typevar(db, typevar) + && is_visible_across_class_boundary(db, bound, crossed_class_scope) { return Some(bound); } @@ -151,8 +245,7 @@ pub(crate) fn bind_typevar<'db>( crossed_class_scope = true; } } - typevar_binding_context - .map(|typevar_binding_context| typevar.with_binding_context(db, typevar_binding_context)) + None } /// Create a `typing.Self` type variable for a given class. @@ -313,6 +406,26 @@ impl<'db> GenericContext<'db> { } } + /// Returns the generic context visible while checking the scope introduced by `node`. + /// + /// For functions, this retains type variables that are moved to a returned callable in the + /// externally visible signature. Other scope kinds have identical lexical and public contexts. + fn lexical_of_node( + db: &'db dyn Db, + node: &NodeWithScopeKind, + index: &SemanticIndex<'db>, + ) -> Option { + if let NodeWithScopeKind::Function(function) = node { + let definition = index.expect_single_definition(function); + infer_definition_types(db, definition) + .function_type(definition)? + .last_definition_raw_signature(db, ReturnCallableTypeVarScope::Lexical) + .generic_context + } else { + Self::of_node(db, node, index) + } + } + /// Creates a generic context from a list of `BoundTypeVarInstance`s. pub(crate) fn from_typevar_instances( db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs index b4207b2650..8d21840fe9 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -61,17 +61,13 @@ fn parameters_have_annotations(parameters: &ast::Parameters) -> bool { struct ExpectedReturnType<'db> { /// The externally-visible return type. public: Type<'db>, - /// The lexical return type, if it differs for a generic PEP 695 function. - lexical: Option>, + /// The return type as seen from inside the function body. + lexical: Type<'db>, } impl<'db> ExpectedReturnType<'db> { - /// Creates the expected return type policy for `function_node`. - fn from_function( - db: &'db dyn Db, - function: FunctionType<'db>, - function_node: &ast::StmtFunctionDef, - ) -> Self { + /// Creates the expected return type policy for `function`. + fn from_function(db: &'db dyn Db, function: FunctionType<'db>) -> Self { /// Normalizes special return annotations to the type actually returned by expressions. fn normalize<'db>( db: &'db dyn Db, @@ -91,18 +87,12 @@ impl<'db> ExpectedReturnType<'db> { same_module_uncached_raw_signature(db, function, ReturnCallableTypeVarScope::Public) .return_ty, ); - let lexical = function_node.type_params.is_some().then(|| { - normalize( - db, - &env, - same_module_uncached_raw_signature( - db, - function, - ReturnCallableTypeVarScope::Lexical, - ) + let lexical = normalize( + db, + &env, + same_module_uncached_raw_signature(db, function, ReturnCallableTypeVarScope::Lexical) .return_ty, - ) - }); + ); Self { public, lexical } } @@ -115,10 +105,7 @@ impl<'db> ExpectedReturnType<'db> { /// Returns `true` if `ty` is accepted by either the public return type or the lexical return /// type. fn accepts(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> bool { - ty.is_assignable_to(db, env, self.public) - || self - .lexical - .is_some_and(|lexical| ty.is_assignable_to(db, env, lexical)) + ty.is_assignable_to(db, env, self.public) || ty.is_assignable_to(db, env, self.lexical) } } @@ -145,7 +132,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_definition(parameter); } - validate_paramspec_components(&self.context, &function.parameters, |expr| { + validate_paramspec_components(&self.context, self.index, &function.parameters, |expr| { self.file_expression_type(expr) }); self.validate_unpacked_typed_dict_kwargs(&function.parameters); @@ -185,8 +172,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ReturnCallableTypeVarScope::Public, ) .return_ty; - let expected_return = - ExpectedReturnType::from_function(db, enclosing_function, function); + let expected_return = ExpectedReturnType::from_function(db, enclosing_function); let expected_ty = expected_return.public(); let scope_id = self.index.node_scope(NodeWithScopeRef::Function(function)); diff --git a/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs b/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs index dcd2ee3f54..7da6b708c3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/paramspec_validation.rs @@ -1,6 +1,15 @@ -use crate::types::{ParamSpecAttrKind, Type, context::InferContext, diagnostic::INVALID_PARAMSPEC}; +use crate::{ + FxOrderSet, + types::{ + ParamSpecAttrKind, Type, + context::InferContext, + diagnostic::{INVALID_PARAMSPEC, UNBOUND_TYPE_VARIABLE}, + generics::resolve_typevar_reference, + }, +}; use ruff_python_ast as ast; use ruff_text_size::Ranged; +use ty_python_core::SemanticIndex; /// Validate the usage of `ParamSpec` components (`P.args` and `P.kwargs`) across all /// parameters of a function. @@ -8,13 +17,16 @@ use ruff_text_size::Ranged; /// This enforces several rules from the typing spec: /// - `P.args` and `P.kwargs` must always be used together /// - When `*args: P.args` is present, `**kwargs: P.kwargs` must also be present (same P) +/// - `P` must already be in scope /// - No keyword-only parameters are allowed between `*args: P.args` and `**kwargs: P.kwargs` pub(super) fn validate_paramspec_components<'db>( context: &'db InferContext<'db, '_>, + index: &SemanticIndex<'db>, parameters: &ast::Parameters, infer_type: impl Fn(&ast::Expr) -> Type<'db>, ) { let db = context.db(); + let env = context.program_environment(); // Extract ParamSpec info from *args annotation let args_paramspec = parameters.vararg.as_deref().and_then(|vararg| { @@ -49,7 +61,7 @@ pub(super) fn validate_paramspec_components<'db>( match (args_paramspec, kwargs_paramspec) { // Both *args: P.args and **kwargs: P.kwargs present - (Some((args_tv, _args_annotation)), Some((kwargs_tv, kwargs_annotation))) => { + (Some((args_tv, args_annotation)), Some((kwargs_tv, kwargs_annotation))) => { // Check they refer to the same ParamSpec if !args_tv.is_same_typevar_as(db, kwargs_tv) { let args_name = args_tv.name(db); @@ -62,6 +74,48 @@ pub(super) fn validate_paramspec_components<'db>( )); } } else { + let paramspec_is_bound_by_parameter = parameters + .iter() + .filter_map(ast::AnyParameterRef::annotation) + .map(&infer_type) + .filter(|ty| { + !matches!( + ty, + Type::TypeVar(typevar) + if typevar.is_paramspec(db) + && typevar.paramspec_attr(db).is_some() + ) + }) + .any(|ty| { + let mut typevars = FxOrderSet::default(); + ty.find_legacy_typevars(db, env, None, &mut typevars); + typevars + .iter() + .any(|typevar| typevar.is_same_typevar_as(db, args_tv)) + }); + let paramspec_is_in_scope = paramspec_is_bound_by_parameter + || index + .scope(context.scope().file_scope_id(db)) + .parent() + .is_some_and(|parent_scope| { + resolve_typevar_reference(db, index, parent_scope, args_tv.typevar(db)) + .is_some() + }); + if !paramspec_is_in_scope + && let Some(builder) = + context.report_lint(&UNBOUND_TYPE_VARIABLE, args_annotation) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "ParamSpec `{}` is not in scope", + args_tv.name(db), + )); + diagnostic.annotate( + context + .secondary(kwargs_annotation) + .message("This component uses the same out-of-scope ParamSpec"), + ); + } + // Same ParamSpec - check no keyword-only params between them if !parameters.kwonlyargs.is_empty() { let name = args_tv.name(db); diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index b04ab05230..fb9356ebc5 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -37,7 +37,9 @@ use crate::types::relation::{ }; use crate::types::tuple::{Tuple, TupleType, VariableSegment}; use crate::types::typed_dict::extract_unpacked_typed_dict_keys_from_kwargs_annotation; -use crate::types::typevar::{TypeVarSet, max_typevar_freshness_matching_generic_context}; +use crate::types::typevar::{ + TypeVarInstance, TypeVarSet, max_typevar_freshness_matching_generic_context, +}; use crate::types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, CallableType, ErrorContext, ErrorContextTree, FindLegacyTypeVarsVisitor, KnownClass, @@ -829,6 +831,23 @@ impl<'db> Signature<'db> { } } + /// Returns the binding referenced by a direct `P.args` or `P.kwargs` variadic parameter. + /// + /// Returns `None` if this signature has no `ParamSpec` component parameters, or if none of + /// their `ParamSpec`s has the same identity as `typevar`. + /// + /// This also exposes captured bindings that are intentionally absent from the function's own + /// generic context. + pub(super) fn paramspec_component_binding( + &self, + db: &'db dyn Db, + typevar: TypeVarInstance<'db>, + ) -> Option> { + self.parameters + .paramspec_component_bindings(db) + .find(|bound| bound.typevar(db).identity(db) == typevar.identity(db)) + } + pub(super) fn wrap_coroutine_return_type( self, db: &'db dyn Db, @@ -4741,6 +4760,25 @@ impl<'db> Parameters<'db> { self.data.value.iter() } + /// Iterates over the `ParamSpec` bindings referenced by direct variadic component annotations. + /// + /// The returned bindings represent `P` itself, with the `args` or `kwargs` component removed. + fn paramspec_component_bindings( + &self, + db: &'db dyn Db, + ) -> impl Iterator> + '_ { + self.iter() + .filter(|parameter| parameter.is_variadic() || parameter.is_keyword_variadic()) + .filter_map(move |parameter| match parameter.annotated_type() { + Type::TypeVar(typevar) + if typevar.is_paramspec(db) && typevar.paramspec_attr(db).is_some() => + { + Some(typevar.without_paramspec_attr(db)) + } + _ => None, + }) + } + /// Iterate initial positional parameters, not including variadic parameter, if any. /// /// For a valid signature, this will be all positional parameters. In an invalid signature, diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index 0b9aeaed6a..6aea0560d0 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -1629,6 +1629,17 @@ pub enum ParamSpecAttrKind { Kwargs, } +impl ParamSpecAttrKind { + /// Returns the component represented by a `ParamSpec` attribute name. + pub(crate) fn from_name(name: &str) -> Option { + match name { + "args" => Some(Self::Args), + "kwargs" => Some(Self::Kwargs), + _ => None, + } + } +} + impl std::fmt::Display for ParamSpecAttrKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { From f70a1a3aceecf71f45715da7f2b5b746decc6ed7 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 3 Aug 2026 12:55:55 -0400 Subject: [PATCH 220/390] [ty] Hide stub-only helpers from implicit builtin lookup (#27423) ## Summary Typeshed's `builtins.pyi` defines private type variables, aliases, and protocols that do not exist in the runtime builtins namespace. Previously, implicit builtin fallback treated those stub-only helpers as ordinary builtins, so undefined names such as `_T_co` silently resolved (this should fail -- `_T_co` doesn't exist!): ```python class SupportsNext: def __next__(self) -> _T_co: # error: [unresolved-reference] raise NotImplementedError ``` This PR adds a shared `exists_at_runtime()` predicate for stub definitions and applies it consistently to implicit builtin resolution. Specifically, it excludes private type variables, aliases, and type-checking-only definitions, while preserving explicit imports, real private runtime builtins, and project-level `__builtins__.pyi` overrides. As a result, like mypy, Pyright, and Pyrefly, we reject implicit references to private typing-only builtin helpers. And like mypy and Pyrefly (but unlike Pyright), we still allow explicit imports, like `from builtins import _T_co`. Closes https://github.com/astral-sh/ty/issues/1483. --- crates/ty_ide/src/completion.rs | 35 ++- crates/ty_ide/src/rename.rs | 12 ++ crates/ty_ide/src/semantic_tokens.rs | 10 + .../resources/mdtest/import/builtins.md | 203 ++++++++++++++++++ crates/ty_python_semantic/src/place.rs | 78 ++++++- .../ty_python_semantic/src/semantic_model.rs | 13 +- crates/ty_python_semantic/src/types.rs | 98 ++++++++- .../src/types/ide_support.rs | 4 +- .../src/types/infer/builder.rs | 6 +- .../src/types/list_members.rs | 59 ++--- 10 files changed, 462 insertions(+), 56 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 7b0c4cf0b6..44b830ad8d 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -3541,7 +3541,8 @@ re. .source( "package/__init__.pyi", r#"\ -from typing import TypeAlias, Literal, TypeVar, ParamSpec, TypeVarTuple, Protocol +from types import UnionType +from typing import TYPE_CHECKING, Literal, ParamSpec, Protocol, TypeAlias, TypeVar, TypeVarTuple, type_check_only public_name = 1 _private_name = 1 @@ -3564,11 +3565,34 @@ _private_explicit_type_alias: TypeAlias = Literal[1] public_implicit_union_alias = int | str _private_implicit_union_alias = int | str +def make_union() -> UnionType: ... +def make_typevar() -> TypeVar: ... +def identity[T](value: T) -> T: ... + +_private_runtime_union = make_union() +_private_runtime_typevar = make_typevar() +_private_precise_runtime_union = identity(int | str) + class PublicProtocol(Protocol): def method(self) -> None: ... class _PrivateProtocol(Protocol): def method(self) -> None: ... + +@type_check_only +class PublicTypeOnlyProtocol(Protocol): + def method(self) -> None: ... + +@type_check_only +class _PrivateTypeOnlyProtocol(Protocol): + def method(self) -> None: ... + +if TYPE_CHECKING: + class PublicTypeCheckingProtocol(Protocol): + def method(self) -> None: ... + + class _PrivateTypeCheckingProtocol(Protocol): + def method(self) -> None: ... "#, ) .source("main.py", "import package; package.") @@ -3590,8 +3614,15 @@ class _PrivateProtocol(Protocol): test.not_contains("_private_explicit_type_alias"); test.contains("public_implicit_union_alias"); test.not_contains("_private_implicit_union_alias"); + test.contains("_private_runtime_union"); + test.contains("_private_runtime_typevar"); + test.contains("_private_precise_runtime_union"); test.contains("PublicProtocol"); - test.not_contains("_PrivateProtocol"); + test.contains("_PrivateProtocol"); + test.not_contains("PublicTypeOnlyProtocol"); + test.not_contains("_PrivateTypeOnlyProtocol"); + test.not_contains("PublicTypeCheckingProtocol"); + test.not_contains("_PrivateTypeCheckingProtocol"); } /// Unlike [`private_symbols_in_stub`], this test doesn't use a `.pyi` file so all of the names diff --git a/crates/ty_ide/src/rename.rs b/crates/ty_ide/src/rename.rs index bbe2adba45..2a5a789aa4 100644 --- a/crates/ty_ide/src/rename.rs +++ b/crates/ty_ide/src/rename.rs @@ -1215,6 +1215,18 @@ def convert_to_number(value): assert_snapshot!(test.prepare_rename(), @"Cannot rename"); } + #[test] + fn cannot_rename_private_builtin_helper() { + // Unresolved references must not resolve to a private typeshed helper that likely does not + // exist at runtime or rename matching unresolved references in other files. + let test = CursorTest::builder() + .source("other.py", "_T_co\n") + .source("main.py", "_T_co\n") + .build(); + + assert_snapshot!(test.prepare_rename(), @"Cannot rename"); + } + #[test] fn rename_keyword_argument() { // Test renaming a keyword argument and its corresponding parameter diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index 26155ae448..06b8e36548 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -4655,6 +4655,16 @@ def f(): assert_snapshot!(test.to_snapshot(&tokens), @r#""f" @ 5..6: Function [definition]"#); } + #[test] + fn private_builtin_helpers_do_not_receive_semantic_tokens() { + // Private helpers excluded from implicit builtin lookup must remain unresolved for IDE + // highlighting instead of receiving tokens from their typeshed definitions. + let test = SemanticTokenTest::new("_T_co\n_P\n"); + + let tokens = test.highlight_file(); + assert_snapshot!(test.to_snapshot(&tokens), @""); + } + #[test] fn unresolved_attributes_do_not_receive_semantic_tokens() { let test = SemanticTokenTest::new( diff --git a/crates/ty_python_semantic/resources/mdtest/import/builtins.md b/crates/ty_python_semantic/resources/mdtest/import/builtins.md index ab0eba432f..5669117976 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/import/builtins.md @@ -19,6 +19,209 @@ reveal_type(chr) # revealed: def chr(i: SupportsIndex, /) -> str reveal_type(str) # revealed: ``` +## Private type-checking-only builtin helpers are not implicit builtins + +Private type variables, type aliases, and type-checking-only definitions in a `builtins` stub are +implementation details. They must not be available without an explicit import. + +```toml +[environment] +typeshed = "/typeshed" +``` + +`/typeshed/stdlib/typing.pyi`: + +```pyi +class TypeVar: + def __new__(cls, name): ... + +class ParamSpec: + def __new__(cls, name): ... + +class Protocol: ... +class _SpecialForm: ... + +TypeAlias: _SpecialForm + +def type_check_only(obj): ... +``` + +`/typeshed/stdlib/builtins.pyi`: + +```pyi +from typing import ParamSpec, Protocol, TypeAlias, TypeVar, type_check_only + +class object: ... +class int: ... + +_T = TypeVar("_T") +_P = ParamSpec("_P") +_PrivateAlias: TypeAlias = int + +@type_check_only +class _PrivateProtocol(Protocol): ... + +@type_check_only +class PublicTypeOnlyClass: ... + +@type_check_only +def public_type_only_function(): ... +``` + +`module.py`: + +```py +_T # error: [unresolved-reference] +_P # error: [unresolved-reference] +_PrivateAlias # error: [unresolved-reference] +_PrivateProtocol # error: [unresolved-reference] +PublicTypeOnlyClass # error: [unresolved-reference] +public_type_only_function # error: [unresolved-reference] +``` + +## Explicitly importing private builtin helpers + +We still allow users to explicitly import implementation details from the `builtins` module. + +```toml +[environment] +typeshed = "/typeshed" +``` + +`/typeshed/stdlib/typing.pyi`: + +```pyi +class TypeVar: + def __new__(cls, name): ... + +class Protocol: ... +class _SpecialForm: ... + +TypeAlias: _SpecialForm + +def type_check_only(obj): ... +``` + +`/typeshed/stdlib/builtins.pyi`: + +```pyi +from typing import Protocol, TypeAlias, TypeVar, type_check_only + +class object: ... +class int: ... + +_T = TypeVar("_T") +_PrivateAlias: TypeAlias = int + +@type_check_only +class _PrivateProtocol(Protocol): ... + +@type_check_only +class PublicTypeOnlyClass: ... +``` + +`module.py`: + +```py +from builtins import PublicTypeOnlyClass, _PrivateAlias, _PrivateProtocol, _T + +_T +_PrivateAlias +_PrivateProtocol +PublicTypeOnlyClass +``` + +## Private project-level builtins + +A project-level `__builtins__.pyi` can deliberately provide private runtime names, including names +that overlap with private helpers in the standard `builtins` stub. + +```py +reveal_type(_private_value) # revealed: int +reveal_type(_T_co) # revealed: int + +_PrivateTypeVar # error: [unresolved-reference] +_PrivateAlias # error: [unresolved-reference] +_PrivateTypeOnlyProtocol # error: [unresolved-reference] +_PrivateTypeCheckingProtocol # error: [unresolved-reference] + +_RuntimeProtocol +_runtime_typevar +``` + +`__builtins__.pyi`: + +```pyi +from typing import TYPE_CHECKING, Protocol, TypeAlias, TypeVar, type_check_only + +_private_value: int +_T_co: int + +_PrivateTypeVar = TypeVar("_PrivateTypeVar") +_PrivateAlias: TypeAlias = int + +@type_check_only +class _PrivateTypeOnlyProtocol(Protocol): ... + +if TYPE_CHECKING: + class _PrivateTypeCheckingProtocol(Protocol): ... + +class _RuntimeProtocol(Protocol): ... + +def make_typevar() -> TypeVar: ... + +_runtime_typevar = make_typevar() +``` + +## Private type-checking-only builtins with stacked decorators + +An outer decorator can change the inferred type of a private function or class, but it does not make +an inner `@type_check_only` definition available at runtime. + +```py +_PrivateFunction # error: [unresolved-reference] +_PrivateClass # error: [unresolved-reference] +``` + +`__builtins__.pyi`: + +```pyi +from typing import Callable, type_check_only + +def decorate_function(callback: Callable[[int], int]) -> Callable[[int], int]: ... +def decorate_class(cls: type[object]) -> type[object]: ... +@decorate_function +@type_check_only +def _PrivateFunction(value: int) -> int: ... + +@decorate_class +@type_check_only +class _PrivateClass: ... +``` + +## Private runtime standard builtins + +A private class declared by the standard `builtins` stub remains available when it represents a real +runtime builtin, rather than a type-checking-only helper. + +```toml +[environment] +typeshed = "/typeshed" +``` + +`/typeshed/stdlib/builtins.pyi`: + +```pyi +class object: ... +class _IncompleteInputError: ... +``` + +`module.py`: + +```py +_IncompleteInputError +``` + ## Builtin symbol from custom typeshed If we specify a custom typeshed, we can use the builtin symbol from it, and no longer access the diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index d07847b0c3..1e6120f61a 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -14,7 +14,8 @@ use crate::reachability::{ use crate::types::narrow::NarrowingEvaluatorExtension; use crate::types::{ DynamicType, KnownClass, MemberLookupPolicy, Type, TypeAndQualifiers, TypeQualifiers, - UnionBuilder, UnionType, binding_type, inferred_declaration, is_discarded_dict_key_assignment, + UnionBuilder, UnionType, binding_type, exists_at_runtime, inferred_declaration, + is_discarded_dict_key_assignment, }; use crate::{Db, FxIndexSet, FxOrderSet}; use ty_python_core::definition::{Definition, DefinitionKind, DefinitionState}; @@ -608,12 +609,68 @@ pub(crate) fn builtins_symbol<'db>( env: &ProgramEnvironment<'db>, symbol: &str, ) -> PlaceAndQualifiers<'db> { + builtins_symbol_impl(db, env, symbol, BuiltinVisibility::All) + .map(|(_, symbol)| symbol) + .unwrap_or_default() +} + +/// Looks up `symbol` for implicit builtin fallback. +/// +/// Private type-checking-only definitions are implementation details, but private runtime +/// definitions from either the standard or project-level builtins remain available. +/// +/// ```python +/// # builtins.pyi +/// _T = TypeVar("_T") # Not available as an implicit builtin. +/// +/// # __builtins__.pyi +/// _custom: int # Available as an implicit builtin. +/// ``` +pub(crate) fn implicit_builtins_symbol<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + symbol: &str, +) -> PlaceAndQualifiers<'db> { + builtins_symbol_impl(db, env, symbol, BuiltinVisibility::RuntimeOnly) + .map(|(_, symbol)| symbol) + .unwrap_or_default() +} + +/// Returns the module scope that supplies `symbol` through implicit builtin fallback. +/// +/// Uses the same visibility rules as [`implicit_builtins_symbol`] so IDE definition lookup cannot +/// resolve a private typing-only helper that type inference considers undefined. +pub(crate) fn implicit_builtins_symbol_scope<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + symbol: &str, +) -> Option> { + builtins_symbol_impl(db, env, symbol, BuiltinVisibility::RuntimeOnly).map(|(scope, _)| scope) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum BuiltinVisibility { + All, + RuntimeOnly, +} + +/// Resolves project-level builtins before standard builtins and optionally hides typing-only names. +/// +/// Returns the supplying module's scope together with the symbol so inference and IDE lookups can +/// share the same resolution and visibility policy. +fn builtins_symbol_impl<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + symbol: &str, + visibility: BuiltinVisibility, +) -> Option<(ScopeId<'db>, PlaceAndQualifiers<'db>)> { let python_version = env.python_version(db); let resolver = |module: Module<'db>| { let python_file = module.python_file(db)?; + let scope = global_scope(db, python_file); let found_symbol = symbol_impl( db, - global_scope(db, python_file), + scope, symbol, RequiresExplicitReExport::Yes, ConsideredDefinitions::EndOfScope, @@ -624,11 +681,19 @@ pub(crate) fn builtins_symbol<'db>( // `imported_symbol`. module_type_implicit_global_symbol(db, python_file, symbol) }); - // If this symbol is not present in project-level builtins, search in the default ones. - found_symbol - .ignore_possibly_undefined() - .map(|_| found_symbol) + found_symbol.ignore_possibly_undefined()?; + + if matches!(visibility, BuiltinVisibility::RuntimeOnly) + && let Place::Defined(defined) = found_symbol.place + && let Some(definition) = defined.provenance.definition() + && !exists_at_runtime(db, definition) + { + return None; + } + + Some((scope, found_symbol)) }; + // If this symbol is not present in project-level builtins, search in the default ones. resolve_module_confident( db, python_version, @@ -639,7 +704,6 @@ pub(crate) fn builtins_symbol<'db>( resolve_module_confident(db, python_version, &KnownModule::Builtins.name()) .and_then(resolver) }) - .unwrap_or_default() } /// Lookup the type of `symbol` in a given known module. diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 612c1f8b3a..2bb727c036 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -284,8 +284,19 @@ impl<'db> SemanticModel<'db> { }), ); + // Project-level builtins take precedence over the standard builtins. + let project_builtins = ModuleName::new_static("__builtins__").unwrap(); + if resolve_module(self.db, self.file, &project_builtins).is_some() { + completions.extend(self.module_completions(&project_builtins).into_iter().map( + |mut completion| { + completion.builtin = true; + completion + }, + )); + } + // Builtins are available in all scopes. - let builtins = ModuleName::new_static("builtins").expect("valid module name"); + let builtins = KnownModule::Builtins.name(); completions.extend(self.module_completions(&builtins)); // The above can sometimes result in duplicates. Get rid of them. diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index ab8e205c8a..bc9287c64b 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -101,7 +101,7 @@ use crate::types::typevar::{TypeVarInstance, TypeVarSet}; pub use crate::types::variance::TypeVarVariance; use crate::types::variance::VarianceInferable; use crate::types::visitor::any_over_type; -use crate::{Db, FxOrderSet, Program}; +use crate::{Db, FxOrderSet, HasType, NameKind, Program, SemanticModel}; pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, StaticClassLiteral}; pub use class::{KnownClass, MethodDecorator}; use instance::Protocol; @@ -111,7 +111,7 @@ pub(crate) use literal::{ }; pub use special_form::SpecialFormType; pub(crate) use special_form::TypedDictModule; -use ty_python_core::definition::Definition; +use ty_python_core::definition::{Definition, DefinitionKind}; use ty_python_core::place::ScopedPlaceId; use ty_python_core::scope::ScopeId; use ty_python_core::{Truthiness, place_table, semantic_index, use_def_map}; @@ -223,6 +223,100 @@ pub(crate) fn binding_type<'db>(db: &'db dyn Db, definition: Definition<'db>) -> inference.binding_type(definition) } +/// Returns whether a definition represents a value that exists at runtime. +/// +/// Type-checking-only decorators and guards never represent runtime values. Private type-variable +/// declarations, explicit aliases, and unambiguous typing aliases in stub files are also +/// typing-only, while public aliases and genuine runtime values remain visible. +/// +/// ```python +/// _T = TypeVar("_T") # Typing-only helper. +/// _Alias: TypeAlias = list[int] # Typing-only alias. +/// _runtime_typevar = make_typevar() # Runtime value. +/// _runtime_callback = callbacks[0] # Runtime value. +/// ``` +#[salsa::tracked(returns(copy))] +pub(crate) fn exists_at_runtime<'db>(db: &'db dyn Db, definition: Definition<'db>) -> bool { + let file = definition.python_file(db); + let inference = infer_definition_types(db, definition); + let ty = inference.binding_type(definition); + + // A class or function decorated with `@type_check_only` never exists at runtime. + if ty.is_type_check_only(db) + || inference + .undecorated_type() + .is_some_and(|ty| ty.is_type_check_only(db)) + { + return false; + } + + let parsed = parsed_module(db, file); + let module = parsed.load(db); + + // Definitions inside an `if TYPE_CHECKING` block are never available at runtime. + if semantic_index(db, file).is_in_type_checking_block( + definition.file_scope(db), + definition.full_range(db, &module).range(), + ) { + return false; + } + + // The remaining heuristics only apply to stub definitions. + if !file.file(db).is_stub(db) { + return true; + } + + let is_private = definition.place(db).as_symbol().is_some_and(|symbol| { + matches!( + NameKind::classify(place_table(db, definition.scope(db)).symbol(symbol).name()), + NameKind::Sunder + ) + }); + + if !is_private { + return true; + } + + // Private type variables, parameter specifications, and type-variable tuples in stubs are + // implementation details rather than runtime values. + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = ty + && typevar.definition(db) == Some(definition) + { + return false; + } + + // Explicit PEP 613 and PEP 695 type aliases in stubs are also typing-only helpers. + let model = SemanticModel::new(db, file); + if model.is_type_alias_definition(definition) { + return false; + } + + let DefinitionKind::Assignment(assignment) = definition.kind(db) else { + return true; + }; + + // Treat only unambiguous union, `Literal`, and `Annotated` expressions as implicit aliases. + // Other expressions may also be aliases, but a false negative is preferable to incorrectly + // hiding a value that exists at runtime. + match (ty, assignment.value(&module)) { + ( + Type::KnownInstance(KnownInstanceType::UnionType(_)), + ast::Expr::BinOp(ast::ExprBinOp { + op: ast::Operator::BitOr, + .. + }), + ) => false, + ( + Type::KnownInstance(KnownInstanceType::Literal(_) | KnownInstanceType::Annotated(_)), + ast::Expr::Subscript(subscript), + ) => !matches!( + subscript.value.inferred_type(&model), + Some(Type::SpecialForm(_) | Type::ClassLiteral(_) | Type::GenericAlias(_)) + ), + _ => true, + } +} + /// Infer the type of a declaration, returning `Rejected` if it is not valid. pub(crate) fn inferred_declaration<'db>( db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 16d9e0d801..bb84a9acd2 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, VecDeque}; use crate::FxIndexSet; -use crate::place::builtins_module_scope; +use crate::place::implicit_builtins_symbol_scope; use crate::reachability::is_range_reachable; use crate::types::call::bind::CheckTypesMode; use crate::types::call::{CallArguments, CallError, MatchedArgument}; @@ -172,7 +172,7 @@ pub fn definitions_for_name<'db>( // If we didn't find any definitions in scopes, fallback to builtins let env = model.program_environment(); if resolved_definitions.is_empty() - && let Some(builtins_scope) = builtins_module_scope(db, &env) + && let Some(builtins_scope) = implicit_builtins_symbol_scope(db, &env, name_str) { // Special cases for `float` and `complex` in type annotation positions. // We don't know whether we're in a type annotation position, so we'll just ask `Name`'s type, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index e300c38c0d..7401736a69 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -35,8 +35,8 @@ use super::{ use crate::diagnostic::format_enumeration; use crate::place::{ ConsideredDefinitions, DefinedPlace, Definedness, LookupError, Place, PlaceAndQualifiers, - RequiresExplicitReExport, TypeOrigin, builtins_module_scope, builtins_symbol, - class_body_implicit_symbol, explicit_global_symbol, loop_header_reachability, + RequiresExplicitReExport, TypeOrigin, builtins_module_scope, class_body_implicit_symbol, + explicit_global_symbol, implicit_builtins_symbol, loop_header_reachability, module_type_implicit_global_declaration, module_type_implicit_global_symbol, place_by_id, place_from_bindings_with_reachability_cache, place_from_declarations_with_reachability_cache, typing_extensions_symbol, @@ -9637,7 +9637,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if Some(self.scope()) == builtins_module_scope(db, env) { Place::Undefined.into() } else { - builtins_symbol(db, env, symbol_name) + implicit_builtins_symbol(db, env, symbol_name) } }) // Still not found? It might be `reveal_type`... diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index 617e20101f..8bf6761f1a 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -11,15 +11,15 @@ use ruff_python_ast::name::Name; use rustc_hash::FxHashSet; use crate::{ - Db, NameKind, + Db, place::{ DefinedPlace, Place, PlaceWithDefinition, imported_symbol, place_from_bindings, place_from_declarations, }, types::{ - ClassBase, ClassLiteral, KnownClass, KnownInstanceType, ProgramEnvironment, - StaticClassLiteral, SubclassOfInner, Type, TypeVarBoundOrConstraints, - class::CodeGeneratorKind, + ClassBase, ClassLiteral, KnownClass, KnownFunction, ProgramEnvironment, StaticClassLiteral, + SubclassOfInner, Type, TypeVarBoundOrConstraints, class::CodeGeneratorKind, + exists_at_runtime, }, }; use ty_python_core::{ @@ -426,7 +426,6 @@ impl<'db> AllMembers<'db> { let Some(python_file) = module.python_file(db) else { return; }; - let file = python_file.file(db); let module_scope = global_scope(db, python_file); let use_def_map = use_def_map(db, module_scope); @@ -434,49 +433,31 @@ impl<'db> AllMembers<'db> { for (symbol_id, _) in use_def_map.all_end_of_scope_symbol_declarations() { let symbol_name = place_table.symbol(symbol_id).name(); - let Place::Defined(DefinedPlace { ty, .. }) = + let Place::Defined(defined) = imported_symbol(db, env, Some(python_file), symbol_name, None).place else { continue; }; - // Filter private symbols from stubs if they appear to be internal types - let is_stub_file = file.path(db).extension() == Some("pyi"); - let is_private_symbol = match NameKind::classify(symbol_name) { - NameKind::Dunder | NameKind::Normal => false, - NameKind::Sunder => true, - }; - if is_private_symbol && is_stub_file { - match ty { - Type::NominalInstance(instance) - if matches!( - instance.known_class(db), - Some( - KnownClass::TypeVar - | KnownClass::TypeVarTuple - | KnownClass::ExtensionsTypeVarTuple - | KnownClass::ParamSpec - | KnownClass::UnionType - ) - ) => - { - continue; - } - Type::ClassLiteral(class) if class.is_protocol(db) => continue, - Type::KnownInstance( - KnownInstanceType::TypeVar(_) - | KnownInstanceType::TypeAliasType(_) - | KnownInstanceType::UnionType(_) - | KnownInstanceType::Literal(_) - | KnownInstanceType::Annotated(_), - ) => continue, - _ => {} - } + if let Some(definition) = defined.provenance.definition() + && !exists_at_runtime(db, definition) + // Source-module completions retain `@type_check_only` symbols and rank them + // lower. + && (python_file.file(db).is_stub(db) || !defined.ty.is_type_check_only(db)) + // The decorator itself is typing-only, but users must still be able to + // import it when defining typing-only classes and functions. + && !matches!( + defined.ty, + Type::FunctionLiteral(function) + if function.known(db) == Some(KnownFunction::TypeCheckOnly) + ) + { + continue; } self.members.insert(Member { name: symbol_name.clone(), - ty, + ty: defined.ty, }); } From 64a4f10853b2ac1c72ff3ab5f4892d7054d0fe2a Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 3 Aug 2026 20:17:06 -0400 Subject: [PATCH 221/390] Add pywin32 to Ruff ecosystem checks (#27437) ## Summary As a pywin32 maintainer and Ruff user, I'd like to add pywin32 to the ecosystem checks. I think that pywin32 would be a good fit given its age, size, popularity, and importance to the Python on Windows ecosystem. As well as my efforts in the past 3 years modernizing its Python codebase. Most of the Ruff issues / requests I opened have come from using it in setuptools and pywin32. ## Test Plan N/A ? --- python/ruff-ecosystem/ruff_ecosystem/defaults.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/ruff-ecosystem/ruff_ecosystem/defaults.py b/python/ruff-ecosystem/ruff_ecosystem/defaults.py index 683c6864ea..947fae01e9 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/defaults.py +++ b/python/ruff-ecosystem/ruff_ecosystem/defaults.py @@ -46,6 +46,7 @@ Project(repo=Repository(owner="langchain-ai", name="langchain", ref="master")), Project(repo=Repository(owner="latchbio", name="latch", ref="main")), Project(repo=Repository(owner="lnbits", name="lnbits", ref="main")), + Project(repo=Repository(owner="mhammond", name="pywin32", ref="main")), Project(repo=Repository(owner="milvus-io", name="pymilvus", ref="master")), Project(repo=Repository(owner="mlflow", name="mlflow", ref="master")), Project(repo=Repository(owner="model-bakers", name="model_bakery", ref="main")), From 7fd30bb81c25a90a60427f46b6c5817ace0d2d2c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 3 Aug 2026 21:16:29 -0400 Subject: [PATCH 222/390] [ty] Check property compatibility when matching class objects to protocols (#27402) ## Summary When matching a class object against a protocol, we previously treated any `property` descriptor as an unconditional match for a protocol property. This reduced compatibility to a name-only check even though accessing an instance property on the class exposes the descriptor, not the value returned by its getter: ```py class HasValue(Protocol): @property def value(self) -> str: ... class Value: @property def value(self) -> str: ... target: HasValue = Value # error ``` This removes the stale shortcut so the class attribute and protocol member are compared using the normal type relation. That shortcut also interfered with generic inference for unions, like in NumPy's `_DTypeLike[T]`, where it allowed a scalar class such as `np.int64` to match `_SupportsDType[dtype[T]]` without constraining `T`, overriding the useful `type[T]` match and producing `Unknown`. With normal property compatibility, the class-object branch binds `T`, so explicit dtypes remain specialized through downstream calls: ```py values = np.array([0, 1, 2], dtype=np.int64) reveal_type(values) # ndarray[..., dtype[signedinteger[_64Bit]]] reveal_type(np.interp(values, values, values)) # ndarray[..., dtype[float64]] ``` Closes https://github.com/astral-sh/ty/issues/3199. --- .../resources/mdtest/external/numpy.md | 30 +++++++++++++++++-- .../resources/mdtest/libraries/numpy.md | 13 +++++++- .../resources/mdtest/protocols.md | 14 ++++++++- .../src/types/protocol_class.rs | 14 --------- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/external/numpy.md b/crates/ty_python_semantic/resources/mdtest/external/numpy.md index ddce7b3387..dff07f4f3d 100644 --- a/crates/ty_python_semantic/resources/mdtest/external/numpy.md +++ b/crates/ty_python_semantic/resources/mdtest/external/numpy.md @@ -19,6 +19,32 @@ xs = np.array([1, 2, 3]) reveal_type(xs) # revealed: ndarray[tuple[Any, ...], dtype[Unknown]] xs = np.array([1.0, 2.0, 3.0], dtype=np.float64) -# TODO: should be `ndarray[tuple[Any, ...], dtype[float64]]` -reveal_type(xs) # revealed: ndarray[tuple[Any, ...], dtype[Unknown]] +reveal_type(xs) # revealed: ndarray[tuple[Any, ...], dtype[float64]] +``` + +Explicit dtypes remain distinct when checking an array against a parameter annotation. This is a +regression test for : + +```py +def takes_float16(values: np.ndarray[tuple[int, ...], np.dtype[np.float16]]) -> None: ... + +float32_values = np.array([1, 2, 3], dtype=np.float32) +reveal_type(float32_values) # revealed: ndarray[tuple[Any, ...], dtype[floating[_32Bit]]] + +float16_values = np.array([1, 2, 3], dtype=np.float16) +reveal_type(float16_values) # revealed: ndarray[tuple[Any, ...], dtype[floating[_16Bit]]] + +takes_float16(float32_values) # error: [invalid-argument-type] +takes_float16(float16_values) +``` + +An explicit integer dtype is also preserved through `array`, allowing `interp` to select its array +overload. This is a regression test for : + +```py +values = np.array([0, 1, 2], dtype=np.int64) +reveal_type(values) # revealed: ndarray[tuple[Any, ...], dtype[signedinteger[_64Bit]]] + +interpolated = np.interp(values, values, values) +reveal_type(interpolated) # revealed: ndarray[tuple[Any, ...], dtype[float64]] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md b/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md index 2b657a34f7..c6516bf7c4 100644 --- a/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md +++ b/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md @@ -49,9 +49,13 @@ _DTypeLike: TypeAlias = type[_ScalarT] | dtype[_ScalarT] | _SupportsDType[dtype[ DTypeLike: TypeAlias = _DTypeLike[Any] | str | None ``` -Now we can make sure that a function which accepts `DTypeLike | None` works as expected: +Now we can make sure that a function which accepts `DTypeLike | None` works as expected. A generic +function accepting `_DTypeLike[_ScalarT]` should also infer the scalar type from a scalar class. The +protocol union element describes instances with a `dtype` property, not the class object whose class +access exposes that property descriptor: ```py +from typing import TypeVar import mini_numpy as np def accepts_dtype(dtype: np.DTypeLike | None) -> None: ... @@ -61,4 +65,11 @@ accepts_dtype(dtype=np.dtype[np.bool]) accepts_dtype(dtype=object) accepts_dtype(dtype=np.object_) accepts_dtype(dtype="U") + +_ScalarT = TypeVar("_ScalarT", bound=np.generic) + +def from_dtype_like(value: np._DTypeLike[_ScalarT]) -> np.dtype[_ScalarT]: + raise NotImplementedError + +reveal_type(from_dtype_like(np.bool)) # revealed: dtype[bool[bool]] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index be83b96944..7496efb5c8 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -2015,7 +2015,7 @@ read/write property, a `Final` attribute, or a `ClassVar` attribute: ```py from typing import ClassVar, Final, Protocol, final from ty_extensions import static_assert -from ty_extensions._internal import is_subtype_of, is_assignable_to, is_disjoint_from +from ty_extensions._internal import TypeOf, is_subtype_of, is_assignable_to, is_disjoint_from class HasXProperty(Protocol): @property @@ -2086,6 +2086,18 @@ static_assert(not is_assignable_to(HasStrXProperty, HasXProperty)) static_assert(not is_assignable_to(HasXProperty, HasStrXProperty)) ``` +Accessing an instance property on the class object exposes the property descriptor, not the value +returned by its getter. A class object with only an instance property is therefore disjoint from the +protocol: + +```py +static_assert(not is_subtype_of(TypeOf[XReadProperty], HasXProperty)) +static_assert(not is_assignable_to(TypeOf[XReadProperty], HasXProperty)) +static_assert(is_disjoint_from(TypeOf[XReadProperty], HasXProperty)) + +x_class: HasXProperty = XReadProperty # error: [invalid-assignment] +``` + A read-only property on a protocol, unlike a mutable attribute, is covariant: `XSub` in the below example satisfies the `HasXProperty` interface even though the type of the `x` attribute on `XSub` is a subtype of `int` rather than being exactly `int`. diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 9dcc44c581..ea44e6bf55 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -2817,15 +2817,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { (implementation_self_binding_ty, protocol_self_binding_ty) }; - // Checking a class object against a protocol's instance capabilities can expose the - // property descriptor itself rather than the value returned by its getter. Compatibility - // for properties on class objects is not yet modeled; retain the previous name-only - // behavior until generic upper-bound solving can handle the large recursive unions this - // otherwise creates. - if member.is_property() && matches!(attribute_type, Type::PropertyInstance(_)) { - return self.always(); - } - if member.is_method() && access == ProtocolMemberAccessMode::Instance { let Some(required_ty) = required_ty.resolve(db, env) else { return self.never(); @@ -3328,11 +3319,6 @@ impl<'c, 'db> DisjointnessChecker<'_, 'c, 'db> { member: &ProtocolMember<'_, 'db>, ty: Type<'db>, ) -> ConstraintSet<'db, 'c> { - // An unbound property descriptor does not establish that the value returned by its - // getter is disjoint from the required property type. - if member.is_property() && matches!(ty, Type::PropertyInstance(_)) { - return self.never(); - } let env = self.env; let access = member.access(db, env, ProtocolMemberAccessMode::Instance); if !member.is_method() { From 95734a549efc01405e1841be3896de959895c731 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 4 Aug 2026 08:51:17 +0200 Subject: [PATCH 223/390] [ty] Make module resolution environment-aware (#27128) ## Summary This PR continues the refactor to make type inference and module resolution support scripts that may have different settings than the main project. This PR introduces: * `ResolverEnvironment(SearchPaths, PythonVersion)`: Uniquely identifies the settings used to resolve a module * `ResolverFile(File, ResolverEnvironment)`: A file with its `ResolverEnvironment`. Used for desperate module resolution and `module_to_file` (and all other places where the operation doesn't depend on type-inference specific settings). This PR also `ProgramFile` and updates the `Program` alias to point to `ResolverEnvironment`. These changes are in preparation to reduce the diff of the third PR (we don't want to rewrite all `python_file` call sites to `resolver_file` to then rename them to `program_file`). ## Memory regression This PR changes `Program` from `PythonVersion` (u16) to `ResolverEnvironment` (u64). This increases the size of interned structs and interned query arguments that contain `Program`. Memory usage is still lower than before we made db `Program` aware (the base PR reduced memory usage by about 1%). ## Test Plan Testing: Ran workspace checks, resolver/core/semantic/IDE tests, and repository hooks, including cross-version inference coverage. --- Cargo.lock | 2 + crates/ruff/src/commands/analyze_graph.rs | 19 +- crates/ruff_benchmark/Cargo.toml | 4 +- .../benches/module_resolution.rs | 17 +- crates/ruff_graph/src/db.rs | 59 ++- crates/ruff_graph/src/lib.rs | 19 +- crates/ruff_graph/src/resolver.rs | 40 +- crates/ty/src/lib.rs | 11 +- crates/ty/tests/file_watching.rs | 2 +- crates/ty_completion_bench/src/main.rs | 6 +- crates/ty_completion_eval/src/main.rs | 6 +- crates/ty_ide/src/all_symbols.rs | 26 +- crates/ty_ide/src/call_hierarchy.rs | 13 +- .../src/call_hierarchy/incoming_calls.rs | 44 +- .../src/call_hierarchy/outgoing_calls.rs | 10 +- crates/ty_ide/src/code_action.rs | 18 +- crates/ty_ide/src/completion.rs | 65 +-- crates/ty_ide/src/doc_highlights.rs | 8 +- crates/ty_ide/src/document_symbols.rs | 12 +- crates/ty_ide/src/find_references.rs | 8 +- crates/ty_ide/src/folding_range.rs | 6 +- crates/ty_ide/src/goto.rs | 5 +- crates/ty_ide/src/goto_declaration.rs | 8 +- crates/ty_ide/src/goto_definition.rs | 8 +- crates/ty_ide/src/goto_implementation.rs | 14 +- crates/ty_ide/src/goto_type_definition.rs | 8 +- crates/ty_ide/src/hints.rs | 4 +- crates/ty_ide/src/hover.rs | 14 +- crates/ty_ide/src/importer.rs | 37 +- crates/ty_ide/src/inlay_hints.rs | 21 +- crates/ty_ide/src/lib.rs | 14 +- crates/ty_ide/src/references.rs | 22 +- crates/ty_ide/src/rename.rs | 16 +- crates/ty_ide/src/selection_range.rs | 2 +- crates/ty_ide/src/semantic_tokens.rs | 20 +- crates/ty_ide/src/signature_help.rs | 8 +- crates/ty_ide/src/symbols.rs | 70 ++- crates/ty_ide/src/type_hierarchy.rs | 22 +- crates/ty_ide/src/workspace_symbols.rs | 3 +- crates/ty_module_resolver/Cargo.toml | 1 + crates/ty_module_resolver/src/db.rs | 27 +- crates/ty_module_resolver/src/environment.rs | 100 +++++ crates/ty_module_resolver/src/lib.rs | 15 +- crates/ty_module_resolver/src/list.rs | 78 ++-- crates/ty_module_resolver/src/module.rs | 63 +-- crates/ty_module_resolver/src/module_name.rs | 79 +++- crates/ty_module_resolver/src/path.rs | 72 ++- crates/ty_module_resolver/src/resolve.rs | 419 +++++++++++------- crates/ty_module_resolver/src/typeshed.rs | 13 +- crates/ty_project/src/db.rs | 30 +- crates/ty_project/src/lib.rs | 24 +- .../ty_project/src/watch/project_watcher.rs | 4 +- crates/ty_python_core/src/ast_ids.rs | 18 +- crates/ty_python_core/src/builder.rs | 60 ++- crates/ty_python_core/src/db.rs | 10 +- crates/ty_python_core/src/definition.rs | 7 +- crates/ty_python_core/src/expression.rs | 8 +- crates/ty_python_core/src/lib.rs | 148 +++---- crates/ty_python_core/src/predicate.rs | 19 +- crates/ty_python_core/src/program.rs | 15 +- crates/ty_python_core/src/program_file.rs | 96 ++++ crates/ty_python_core/src/re_exports.rs | 56 ++- crates/ty_python_core/src/scope.rs | 20 +- crates/ty_python_core/src/statement.rs | 14 +- crates/ty_python_core/src/unpack.rs | 13 +- crates/ty_python_semantic/src/db.rs | 23 +- crates/ty_python_semantic/src/dunder_all.rs | 28 +- crates/ty_python_semantic/src/fixes.rs | 36 +- crates/ty_python_semantic/src/lib.rs | 11 +- crates/ty_python_semantic/src/place.rs | 83 ++-- crates/ty_python_semantic/src/pull_types.rs | 9 +- crates/ty_python_semantic/src/reachability.rs | 34 +- .../ty_python_semantic/src/semantic_model.rs | 127 ++++-- crates/ty_python_semantic/src/types.rs | 66 +-- crates/ty_python_semantic/src/types/call.rs | 2 +- .../ty_python_semantic/src/types/call/bind.rs | 7 +- crates/ty_python_semantic/src/types/class.rs | 31 +- .../src/types/class/dynamic_literal.rs | 5 +- .../src/types/class/known.rs | 15 +- .../src/types/class/static_literal.rs | 47 +- .../src/types/constraints.rs | 2 +- .../ty_python_semantic/src/types/context.rs | 56 ++- crates/ty_python_semantic/src/types/cyclic.rs | 4 +- .../src/types/dedicated/pydantic.rs | 4 +- .../src/types/diagnostic.rs | 12 +- .../ty_python_semantic/src/types/display.rs | 8 +- crates/ty_python_semantic/src/types/enums.rs | 4 +- .../src/types/equality/enums.rs | 2 +- .../ty_python_semantic/src/types/function.rs | 49 +- .../ty_python_semantic/src/types/generics.rs | 6 +- .../src/types/ide_support.rs | 201 +++++---- .../src/types/ide_support/unreachable_code.rs | 43 +- .../src/types/ide_support/unused_bindings.rs | 13 +- crates/ty_python_semantic/src/types/infer.rs | 74 ++-- .../src/types/infer/builder.rs | 83 ++-- .../src/types/infer/builder/class.rs | 13 +- .../types/infer/builder/final_attribute.rs | 4 +- .../src/types/infer/builder/function.rs | 8 +- .../src/types/infer/builder/imports.rs | 58 ++- .../builder/post_inference/static_class.rs | 2 +- .../src/types/infer/builder/subscript.rs | 3 +- .../src/types/infer/tests.rs | 64 +-- .../ty_python_semantic/src/types/instance.rs | 4 +- .../src/types/list_members.rs | 17 +- crates/ty_python_semantic/src/types/narrow.rs | 22 +- .../ty_python_semantic/src/types/newtype.rs | 5 +- .../ty_python_semantic/src/types/overrides.rs | 2 +- .../types/property_tests/type_generation.rs | 13 +- .../src/types/protocol_class.rs | 12 +- .../src/types/set_theoretic/builder.rs | 4 +- .../src/types/signatures.rs | 10 +- .../src/types/special_form.rs | 30 +- crates/ty_python_semantic/src/types/tests.rs | 10 +- crates/ty_python_semantic/src/types/tuple.rs | 2 +- .../src/types/type_alias.rs | 16 +- .../src/types/typed_dict.rs | 15 +- .../ty_python_semantic/src/types/typevar.rs | 41 +- .../ty_python_semantic/src/types/unpacker.rs | 8 +- crates/ty_python_semantic/tests/corpus.rs | 24 +- .../ty_server/src/server/api/diagnostics.rs | 5 +- .../requests/call_hierarchy_incoming_calls.rs | 7 +- .../requests/call_hierarchy_outgoing_calls.rs | 7 +- .../src/server/api/requests/code_action.rs | 8 +- .../src/server/api/requests/completion.rs | 10 +- .../src/server/api/requests/doc_highlights.rs | 8 +- .../server/api/requests/document_symbols.rs | 6 +- .../server/api/requests/execute_command.rs | 4 +- .../src/server/api/requests/folding_range.rs | 48 +- .../server/api/requests/goto_declaration.rs | 8 +- .../server/api/requests/goto_definition.rs | 8 +- .../api/requests/goto_implementation.rs | 8 +- .../api/requests/goto_type_definition.rs | 8 +- .../src/server/api/requests/hover.rs | 7 +- .../src/server/api/requests/inlay_hints.rs | 6 +- .../api/requests/prepare_call_hierarchy.rs | 10 +- .../src/server/api/requests/prepare_rename.rs | 7 +- .../api/requests/prepare_type_hierarchy.rs | 10 +- .../src/server/api/requests/references.rs | 13 +- .../src/server/api/requests/rename.rs | 12 +- .../server/api/requests/selection_range.rs | 6 +- .../src/server/api/requests/signature_help.rs | 8 +- .../api/requests/workspace_diagnostic.rs | 8 +- .../src/server/api/semantic_tokens.rs | 7 +- .../src/server/api/type_hierarchy.rs | 6 +- crates/ty_server/src/session.rs | 8 +- .../e2e__commands__debug_command.snap | 1 + crates/ty_test/src/db.rs | 22 +- crates/ty_test/src/lib.rs | 17 +- crates/ty_wasm/src/lib.rs | 97 ++-- fuzz/fuzz_targets/ty_check_invalid_syntax.rs | 23 +- 150 files changed, 2292 insertions(+), 1683 deletions(-) create mode 100644 crates/ty_module_resolver/src/environment.rs create mode 100644 crates/ty_python_core/src/program_file.rs diff --git a/Cargo.lock b/Cargo.lock index 271125300f..65928cd0b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3163,6 +3163,7 @@ dependencies = [ "tracing", "ty_module_resolver", "ty_project", + "ty_python_core", ] [[package]] @@ -4722,6 +4723,7 @@ dependencies = [ "compact_str", "get-size2", "insta", + "ordermap", "regex", "regex-syntax", "ruff_db", diff --git a/crates/ruff/src/commands/analyze_graph.rs b/crates/ruff/src/commands/analyze_graph.rs index 4d69c6e6b8..6b40086994 100644 --- a/crates/ruff/src/commands/analyze_graph.rs +++ b/crates/ruff/src/commands/analyze_graph.rs @@ -6,12 +6,13 @@ use indexmap::IndexSet; use log::{debug, warn}; use path_absolutize::CWD; use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; -use ruff_graph::{Direction, ImportMap, ModuleDb, ModuleImports}; +use ruff_graph::{ + Direction, ImportMap, ModuleDb, ModuleImports, ResolverEnvironment, resolve_search_paths, +}; use ruff_linter::package::PackageRoot; use ruff_linter::source_kind::SourceKind; use ruff_linter::{warn_user, warn_user_once}; use ruff_python_ast::SourceType; -use ruff_python_parser::ParseOptions; use ruff_workspace::resolver::{ResolvedFile, match_exclusion, project_files_in_path}; use rustc_hash::{FxBuildHasher, FxHashMap}; use std::io::Write; @@ -97,12 +98,14 @@ pub(crate) fn analyze_graph( ); let system = OsSystem::default(); - let db = ModuleDb::from_src_roots( - system, + let search_paths = resolve_search_paths( + &system, src_roots.into_iter().collect(), args.python .and_then(|python| SystemPathBuf::from_path_buf(python).ok()), )?; + let db = ModuleDb::new(system); + search_paths.try_register_static_roots(&db); let imports = { // Create a cache for resolved globs. @@ -112,6 +115,7 @@ pub(crate) fn analyze_graph( let result = Arc::new(Mutex::new(Vec::new())); let inner_result = Arc::clone(&result); let db = db.clone(); + let search_paths = &search_paths; rayon::scope(move |scope| { for resolved_file in paths { @@ -172,14 +176,13 @@ pub(crate) fn analyze_graph( } }; - let source_code = source_kind.source_code(); + let environment = ResolverEnvironment::new(&db, python_version, search_paths); // Identify any imports via static analysis. let mut imports = ModuleImports::detect( &db, - source_code, - ParseOptions::from(source_type.expect_python()) - .with_target_version(python_version), + environment, + &source_kind, &path, package.as_deref(), string_imports, diff --git a/crates/ruff_benchmark/Cargo.toml b/crates/ruff_benchmark/Cargo.toml index cf1a0fafd1..62cc4818a5 100644 --- a/crates/ruff_benchmark/Cargo.toml +++ b/crates/ruff_benchmark/Cargo.toml @@ -23,6 +23,7 @@ ruff_python_formatter = { workspace = true, optional = true } ruff_python_parser = { workspace = true, optional = true } ruff_python_trivia = { workspace = true, optional = true } ty_module_resolver = { workspace = true, optional = true } +ty_python_core = { workspace = true, optional = true } ty_project = { workspace = true, optional = true } anyhow = { workspace = true } @@ -60,7 +61,7 @@ ruff_instrumented = [ # Enables the ty instrumented benchmarks ty_instrumented = ["criterion", "ty_project", "ruff_python_trivia"] # Enables the module-resolution benchmark -module_resolution = ["divan", "ty_module_resolver", "ty_project"] +module_resolution = ["divan", "ty_module_resolver", "ty_project", "ty_python_core"] codspeed = ["codspeed-criterion-compat"] # Enables the ty_walltime benchmarks ty_walltime = ["ruff_db/os", "ty_project", "divan"] @@ -113,6 +114,7 @@ ignored = [ "ruff_python_parser", "ruff_python_trivia", "ty_module_resolver", + "ty_python_core", "mimalloc", "tikv-jemallocator" ] diff --git a/crates/ruff_benchmark/benches/module_resolution.rs b/crates/ruff_benchmark/benches/module_resolution.rs index 2df5acb90c..61bb8ba378 100644 --- a/crates/ruff_benchmark/benches/module_resolution.rs +++ b/crates/ruff_benchmark/benches/module_resolution.rs @@ -5,15 +5,15 @@ use std::hint::black_box; use divan::{Bencher, bench}; -use ruff_db::PythonFile; use ruff_db::files::{File, system_path_to_file}; use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem}; use ruff_ranged_value::RangedValue; -use ty_module_resolver::{ModuleName, resolve_module}; +use ty_module_resolver::{ImportingFile, ModuleName, resolve_module}; use ty_project::metadata::options::{EnvironmentOptions, Options}; use ty_project::metadata::python_version::SupportedPythonVersion; use ty_project::metadata::value::RelativePathBuf; -use ty_project::{Db as _, ProjectDatabase, ProjectMetadata}; +use ty_project::{ProjectDatabase, ProjectMetadata}; +use ty_python_core::program::Program; const SEEDED_TARGETS: &[&str] = &["target_0", "target_1", "target_2", "target_3", "target_4"]; // Exercise stub-overlay discovery followed by normal fallback. @@ -73,6 +73,8 @@ fn setup_case(n: usize) -> Case { }); let db = ProjectDatabase::fallible(metadata, system).unwrap(); + // Intern the resolver environment before timing so its initial allocation is not benchmarked. + let _ = Program::get(&db).resolver_environment(&db); let importing_file = system_path_to_file(&db, &importing_path).unwrap(); let resolves = SEEDED_TARGETS @@ -94,10 +96,13 @@ fn ty_module_resolver(bencher: Bencher) { bencher .with_inputs(|| setup_case(PATHS)) .bench_local_refs(|case| { - let importing_file = - PythonFile::new(&case.db, case.importing_file, case.db.python_version()); + let environment = Program::get(&case.db).resolver_environment(&case.db); for name in &case.resolves { - black_box(resolve_module(&case.db, importing_file, name)); + black_box(resolve_module( + &case.db, + ImportingFile::File(case.importing_file, environment), + name, + )); } }); } diff --git a/crates/ruff_graph/src/db.rs b/crates/ruff_graph/src/db.rs index 61d6d1af45..c2ae55aa56 100644 --- a/crates/ruff_graph/src/db.rs +++ b/crates/ruff_graph/src/db.rs @@ -22,45 +22,42 @@ pub struct ModuleDb { storage: salsa::Storage, files: Files, system: Arc, - search_paths: Arc, } impl ModuleDb { - /// Initialize a [`ModuleDb`] from the given source root. - pub fn from_src_roots( - system: S, - src_roots: Vec, - venv_path: Option, - ) -> Result + /// Initialize a [`ModuleDb`] for the given system. + pub fn new(system: S) -> Self where S: System + 'static + Send + Sync + RefUnwindSafe, { - let mut search_path_settings = SearchPathSettings::new(src_roots); - // TODO: Consider calling `PythonEnvironment::discover` if the `venv_path` is not provided. - if let Some(venv_path) = venv_path { - let environment = - PythonEnvironment::new(venv_path, SysPrefixPathOrigin::PythonCliFlag, &system)?; - search_path_settings.site_packages_paths = environment - .site_packages_paths(&system) - .context("Failed to discover the site-packages directory")? - .into_vec(); - } - let search_paths = search_path_settings - .to_search_paths(&system, &EMPTY_VENDORED, &FallibleStrategy) - .context("Invalid search path settings")?; - - let db = Self { + Self { storage: salsa::Storage::new(None), files: Files::default(), system: Arc::new(system), - search_paths: Arc::new(search_paths), - }; - - // Register the static roots for salsa durability - db.search_paths.try_register_static_roots(&db); + } + } +} - Ok(db) +/// Resolve module search paths for the given source roots and Python environment. +pub fn resolve_search_paths( + system: &dyn System, + src_roots: Vec, + venv_path: Option, +) -> Result { + let mut search_path_settings = SearchPathSettings::new(src_roots); + // TODO: Consider calling `PythonEnvironment::discover` if the `venv_path` is not provided. + if let Some(venv_path) = venv_path { + let environment = + PythonEnvironment::new(venv_path, SysPrefixPathOrigin::PythonCliFlag, system)?; + search_path_settings.site_packages_paths = environment + .site_packages_paths(system) + .context("Failed to discover the site-packages directory")? + .into_vec(); } + + search_path_settings + .to_search_paths(system, &EMPTY_VENDORED, &FallibleStrategy) + .context("Invalid search path settings") } #[salsa::db] @@ -79,11 +76,7 @@ impl SourceDb for ModuleDb { } #[salsa::db] -impl ty_module_resolver::Db for ModuleDb { - fn search_paths(&self) -> &SearchPaths { - &self.search_paths - } -} +impl ty_module_resolver::Db for ModuleDb {} #[salsa::db] impl salsa::Database for ModuleDb {} diff --git a/crates/ruff_graph/src/lib.rs b/crates/ruff_graph/src/lib.rs index d09e822046..4e2886ec69 100644 --- a/crates/ruff_graph/src/lib.rs +++ b/crates/ruff_graph/src/lib.rs @@ -3,11 +3,13 @@ use std::collections::{BTreeMap, BTreeSet}; use anyhow::Result; use ruff_db::system::{SystemPath, SystemPathBuf}; +use ruff_linter::source_kind::SourceKind; use ruff_python_ast::helpers::to_module_path; use ruff_python_parser::{ParseOptions, parse}; +pub use ty_module_resolver::ResolverEnvironment; use crate::collector::Collector; -pub use crate::db::ModuleDb; +pub use crate::db::{ModuleDb, resolve_search_paths}; use crate::resolver::Resolver; pub use crate::settings::{AnalyzeSettings, Direction, StringImports}; @@ -22,18 +24,19 @@ pub struct ModuleImports(BTreeSet); impl ModuleImports { /// Detect the [`ModuleImports`] for a given Python file. - pub fn detect( - db: &ModuleDb, - source: &str, - parse_options: ParseOptions, + pub fn detect<'db>( + db: &'db ModuleDb, + environment: ResolverEnvironment<'db>, + source: &SourceKind, path: &SystemPath, package: Option<&SystemPath>, string_imports: StringImports, type_checking_imports: bool, ) -> Result { // Parse the source code. - let python_version = parse_options.target_version(); - let parsed = parse(source, parse_options)?; + let parse_options = ParseOptions::from(source.py_source_type()) + .with_target_version(environment.python_version(db)); + let parsed = parse(source.source_code(), parse_options)?; let module_path = package.and_then(|package| to_module_path(package.as_std_path(), path.as_std_path())); @@ -48,7 +51,7 @@ impl ModuleImports { // Resolve the imports. let mut resolved_imports = ModuleImports::default(); - let resolver = Resolver::new(db, path, python_version); + let resolver = Resolver::new(db, path, environment); for import in imports { for resolved in resolver.resolve(import) { if let Some(path) = resolved.as_system_path() { diff --git a/crates/ruff_graph/src/resolver.rs b/crates/ruff_graph/src/resolver.rs index 41eadafcf3..3442317337 100644 --- a/crates/ruff_graph/src/resolver.rs +++ b/crates/ruff_graph/src/resolver.rs @@ -1,10 +1,8 @@ -use ruff_db::PythonFile; -use ruff_db::files::{FilePath, system_path_to_file}; +use ruff_db::files::{File, FilePath, system_path_to_file}; use ruff_db::system::SystemPath; -use ruff_python_ast::PythonVersion; use ty_module_resolver::{ - ModuleName, resolve_module, resolve_module_confident, resolve_real_module, - resolve_real_module_confident, + ImportingFile, ModuleName, ResolverEnvironment, resolve_module, resolve_module_confident, + resolve_real_module, resolve_real_module_confident, }; use crate::ModuleDb; @@ -13,21 +11,23 @@ use crate::collector::CollectedImport; /// Collect all imports for a given Python file. pub(crate) struct Resolver<'a> { db: &'a ModuleDb, - file: Option>, - python_version: PythonVersion, + file: Option, + environment: ResolverEnvironment<'a>, } impl<'a> Resolver<'a> { /// Initialize a [`Resolver`] with a given [`ModuleDb`]. - pub(crate) fn new(db: &'a ModuleDb, path: &SystemPath, python_version: PythonVersion) -> Self { + pub(crate) fn new( + db: &'a ModuleDb, + path: &SystemPath, + environment: ResolverEnvironment<'a>, + ) -> Self { // If we know the importing file we can potentially resolve more imports - let file = system_path_to_file(db, path) - .ok() - .map(|file| PythonFile::new(db, file, python_version)); + let file = system_path_to_file(db, path).ok(); Self { db, file, - python_version, + environment, } } @@ -110,9 +110,13 @@ impl<'a> Resolver<'a> { /// Resolves a module name to a module. fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> { let module = if let Some(file) = self.file { - resolve_module(self.db, file, module_name)? + resolve_module( + self.db, + ImportingFile::File(file, self.environment), + module_name, + )? } else { - resolve_module_confident(self.db, self.python_version, module_name)? + resolve_module_confident(self.db, self.environment, module_name)? }; Some(module.file(self.db)?.path(self.db)) } @@ -120,9 +124,13 @@ impl<'a> Resolver<'a> { /// Resolves a module name to a module (stubs not allowed). fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> { let module = if let Some(file) = self.file { - resolve_real_module(self.db, file, module_name)? + resolve_real_module( + self.db, + ImportingFile::File(file, self.environment), + module_name, + )? } else { - resolve_real_module_confident(self.db, self.python_version, module_name)? + resolve_real_module_confident(self.db, self.environment, module_name)? }; Some(module.file(self.db)?.path(self.db)) } diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index facee85d1c..9e8f074da6 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -409,17 +409,12 @@ impl MainLoop { } } MainLoopMode::Fix(mode) => { - let python_version = db.python_version(); let result = match mode { - FixMode::AddIgnore => suppress_all_diagnostics( - db, - python_version, - result, - &self.cancellation_token, - ), + FixMode::AddIgnore => { + suppress_all_diagnostics(db, result, &self.cancellation_token) + } FixMode::ApplyFixes => fix_all_diagnostics( db, - python_version, result, Applicability::Safe, &self.cancellation_token, diff --git a/crates/ty/tests/file_watching.rs b/crates/ty/tests/file_watching.rs index 1bf6a10089..7cc6b86024 100644 --- a/crates/ty/tests/file_watching.rs +++ b/crates/ty/tests/file_watching.rs @@ -39,7 +39,7 @@ fn resolve_module_confident<'db>( ) -> Option> { ty_module_resolver::resolve_module_confident( db, - Program::get(db).python_version(db), + Program::get(db).resolver_environment(db), module_name, ) } diff --git a/crates/ty_completion_bench/src/main.rs b/crates/ty_completion_bench/src/main.rs index e99aa13494..a46cf5e0b0 100644 --- a/crates/ty_completion_bench/src/main.rs +++ b/crates/ty_completion_bench/src/main.rs @@ -11,15 +11,13 @@ use std::process::ExitCode; use anyhow::{Context, anyhow}; use clap::Parser; -use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; use ty_ide::{Completion, CompletionCapabilities}; -use ty_project::Db as _; use ty_project::metadata::Options; use ty_project::metadata::options::EnvironmentOptions; use ty_project::metadata::value::RelativePathBuf; -use ty_project::{ProjectDatabase, ProjectMetadata}; +use ty_project::{ProjectDatabase, ProjectMetadata, SemanticDb as _}; #[derive(Debug, clap::Parser)] #[command( @@ -144,7 +142,7 @@ fn get_completions<'db>( db, &settings, CompletionCapabilities::default(), - PythonFile::new(db, file, db.python_version()), + db.program_file(file), offset, )) } diff --git a/crates/ty_completion_eval/src/main.rs b/crates/ty_completion_eval/src/main.rs index 57ab654e50..879b97dea9 100644 --- a/crates/ty_completion_eval/src/main.rs +++ b/crates/ty_completion_eval/src/main.rs @@ -10,16 +10,14 @@ use anyhow::{Context, anyhow}; use clap::Parser; use regex::bytes::Regex; -use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf}; use ty_ide::{Completion, CompletionCapabilities}; use ty_module_resolver::ModuleName; -use ty_project::Db as _; use ty_project::metadata::Options; use ty_project::metadata::options::EnvironmentOptions; use ty_project::metadata::value::RelativePathBuf; -use ty_project::{ProjectDatabase, ProjectMetadata}; +use ty_project::{ProjectDatabase, ProjectMetadata, SemanticDb as _}; #[derive(Debug, clap::Parser)] #[command( @@ -332,7 +330,7 @@ impl Task { &self.db, &self.settings, CompletionCapabilities::default(), - PythonFile::new(&self.db, file, self.db.python_version()), + self.db.program_file(file), offset, ); Ok(completions) diff --git a/crates/ty_ide/src/all_symbols.rs b/crates/ty_ide/src/all_symbols.rs index ae54f569ac..a9e64279f2 100644 --- a/crates/ty_ide/src/all_symbols.rs +++ b/crates/ty_ide/src/all_symbols.rs @@ -1,8 +1,11 @@ use compact_str::CompactString; use rayon::prelude::*; -use ruff_db::{PythonFile, files::File}; -use ty_module_resolver::{Module, ModuleName, all_modules, resolve_real_shadowable_module}; +use ruff_db::files::File; +use ty_module_resolver::{ + ImportingFile, Module, ModuleName, all_modules, resolve_real_shadowable_module, +}; use ty_project::{Db, parallel::ParallelIteratorExt}; +use ty_python_core::ProgramFile; use crate::{ SymbolKind, @@ -15,7 +18,7 @@ use crate::{ /// by the query. pub fn all_symbols<'db>( db: &'db dyn Db, - importing_from: PythonFile<'db>, + importing_from: ProgramFile<'db>, query: &QueryPattern, ) -> Vec> { // If the query is empty, return immediately to avoid expensive file scanning @@ -27,10 +30,13 @@ pub fn all_symbols<'db>( let _span = all_symbols_span.enter(); let typing_extensions = ModuleName::new_static("typing_extensions").unwrap(); + let program = importing_from.program(db); + let resolver_environment = importing_from.resolver_environment(db); + let importing_file = ImportingFile::File(importing_from.file(db), resolver_environment); let is_typing_extensions_available = importing_from.file(db).is_stub(db) - || resolve_real_shadowable_module(db, importing_from, &typing_extensions).is_some(); + || resolve_real_shadowable_module(db, importing_file, &typing_extensions).is_some(); - let results = all_modules(db, importing_from.python_version(db)) + let results = all_modules(db, resolver_environment) .into_par_iter() .map_with_db(db, |db, module| { let name = module.name(db); @@ -55,10 +61,10 @@ pub fn all_symbols<'db>( return Vec::new(); } - let Some(python_file) = module.python_file(db) else { + let Some(file) = module.file(db) else { return Vec::new(); }; - let file = python_file.file(db); + let program_file = ProgramFile::new(db, file, program); let symbols_for_file_span = tracing::debug_span!( parent: &all_symbols_span, @@ -71,7 +77,7 @@ pub fn all_symbols<'db>( if query.is_match_symbol_name(module.name(db)) { symbols.push(AllSymbolInfo::from_module(db, module, file)); } - for (_, symbol) in symbols_for_file_global_only(db, python_file).search(query) { + for (_, symbol) in symbols_for_file_global_only(db, program_file).search(query) { // Test functions (starting with `test_`) in third-party // packages are almost never useful to import. if is_non_first_party && symbol.name.starts_with("test_") { @@ -710,7 +716,7 @@ def zqzqzq(): let symbols = all_symbols( &test.db, - test.python_file(test.cursor.file), + test.program_file(test.cursor.file), &QueryPattern::fuzzy("zqzqzq"), ); let symbol = symbols @@ -1112,7 +1118,7 @@ def test_helper_xyzxyzxyz(): fn all_symbols(&self, query: &str) -> String { let symbols = all_symbols( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), &QueryPattern::fuzzy(query), ); diff --git a/crates/ty_ide/src/call_hierarchy.rs b/crates/ty_ide/src/call_hierarchy.rs index 4f8b455665..7dd33cad8a 100644 --- a/crates/ty_ide/src/call_hierarchy.rs +++ b/crates/ty_ide/src/call_hierarchy.rs @@ -13,7 +13,6 @@ pub(crate) mod outgoing_calls; use crate::goto::{GotoTarget, find_goto_target}; use crate::{Db, SymbolKind}; -use ruff_db::PythonFile; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::find_node::CoveringNode; @@ -21,6 +20,8 @@ use ruff_python_ast::name::Name; use ruff_python_ast::token::Tokens; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextRange, TextSize}; +use ty_module_resolver::ResolverFile; +use ty_python_core::ProgramFile; use ty_python_core::definition::DefinitionKind; use ty_python_semantic::{ImportAliasResolution, ResolvedDefinition, SemanticModel}; @@ -33,10 +34,10 @@ use ty_python_semantic::{ImportAliasResolution, ResolvedDefinition, SemanticMode /// cursor on a specific `@overload def` yields just that one. pub fn prepare_call_hierarchy( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, offset: TextSize, ) -> Option> { - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; let definitions = goto_target @@ -109,7 +110,7 @@ impl CallHierarchyItem { Some(CallHierarchyItem { name: Name::new(name), kind, - detail: module_detail(db, def.python_file(db)), + detail: module_detail(db, def.program_file(db).resolver_file(db)), file: def_file, full_range: def.full_range(db, module).range(), selection_range: def.focus_range(db, module).range(), @@ -117,7 +118,7 @@ impl CallHierarchyItem { } } -fn module_detail(db: &dyn Db, file: PythonFile<'_>) -> Option { +fn module_detail(db: &dyn Db, file: ResolverFile<'_>) -> Option { ty_module_resolver::file_to_module(db, file).map(|module| module.name(db).to_string()) } @@ -199,7 +200,7 @@ mod tests { pub(super) fn prepare_calls(&self) -> Option> { prepare_call_hierarchy( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, ) } diff --git a/crates/ty_ide/src/call_hierarchy/incoming_calls.rs b/crates/ty_ide/src/call_hierarchy/incoming_calls.rs index bc2bfa9f61..e674db8b42 100644 --- a/crates/ty_ide/src/call_hierarchy/incoming_calls.rs +++ b/crates/ty_ide/src/call_hierarchy/incoming_calls.rs @@ -3,7 +3,6 @@ use crate::goto::{Definitions, GotoTarget, find_goto_target}; use crate::references::has_any_external_visible_definitions; use crate::{CallHierarchyItem, Db, SymbolKind}; use rayon::prelude::*; -use ruff_db::PythonFile; use ruff_db::files::File; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_python_ast::helpers::is_dunder; @@ -13,7 +12,9 @@ use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use rustc_hash::FxHashMap; +use ty_module_resolver::ResolverFile; use ty_project::parallel::{ParallelIteratorExt, minimum_parallel_job_len}; +use ty_python_core::ProgramFile; use ty_python_core::scope::{NodeWithScopeKind, ScopeKind}; use ty_python_semantic::types::ide_support::static_member_type_for_attribute; use ty_python_semantic::types::{PropertyAccessorRole, Type}; @@ -29,8 +30,8 @@ const MAX_MIN_FILES_PER_PARALLEL_JOB: usize = 16; /// Find every place in the project that calls the symbol at `offset`, grouped /// by enclosing function/method/class/module. -pub fn incoming_calls(db: &dyn Db, file: PythonFile<'_>, offset: TextSize) -> Vec { - let module = parsed_module(db, file).load(db); +pub fn incoming_calls(db: &dyn Db, file: ProgramFile<'_>, offset: TextSize) -> Vec { + let module = parsed_module(db, file.python_file(db)).load(db); let source_file = file.file(db); let model = SemanticModel::new(db, file); let Some(goto_target) = find_goto_target(&model, &module, offset) else { @@ -75,8 +76,8 @@ pub fn incoming_calls(db: &dyn Db, file: PythonFile<'_>, offset: TextSize) -> Ve let mut raw = call_sites_for_file(db, file, &target_definitions, target_role, needle); if is_externally_visible { + let program = model.program(); let files = db.project().files(db); - let python_version = file.python_version(db); let files: Vec<_> = files .iter() .copied() @@ -103,7 +104,7 @@ pub fn incoming_calls(db: &dyn Db, file: PythonFile<'_>, offset: TextSize) -> Ve call_sites_for_file( db, - PythonFile::new(db, other_file, python_version), + ProgramFile::new(db, other_file, program), &target_definitions, target_role, needle, @@ -170,12 +171,12 @@ struct EnclosingKey { /// `target_definitions`. fn call_sites_for_file( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, target_definitions: &Definitions<'_>, target_role: Option, needle: Option<&str>, ) -> Vec { - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, file.python_file(db)); let module = parsed.load(db); let model = SemanticModel::new(db, file); let mut sites = Vec::new(); @@ -390,8 +391,9 @@ impl<'a> CallSitesFinder<'a, '_> { /// method's AST node. Comprehension and annotation scopes have no callable /// hierarchy item of their own, so walk outward until reaching one that does. fn enclosing_scope_item(&self, scope_node: AnyNodeRef<'_>) -> CallHierarchyItem { - let python_file = self.model.python_file(); - let file = python_file.file(self.db); + let program_file = self.model.program_file(); + let resolver_file = program_file.resolver_file(self.db); + let file = program_file.file(self.db); let mut ancestors = self.model.ancestor_scopes(scope_node); let Some((_, enclosing)) = ancestors.find(|(_, ancestor)| { matches!( @@ -399,11 +401,11 @@ impl<'a> CallSitesFinder<'a, '_> { ScopeKind::Module | ScopeKind::Function | ScopeKind::Class | ScopeKind::Lambda ) }) else { - return module_item(self.db, python_file); + return module_item(self.db, resolver_file); }; match enclosing.node() { - NodeWithScopeKind::Module => module_item(self.db, python_file), + NodeWithScopeKind::Module => module_item(self.db, resolver_file), NodeWithScopeKind::Function(func) => { let func = func.node(self.module); let is_method = ancestors @@ -422,7 +424,7 @@ impl<'a> CallSitesFinder<'a, '_> { } else { SymbolKind::Function }, - detail: module_detail(self.db, python_file), + detail: module_detail(self.db, resolver_file), file, full_range: func.range(), selection_range: func.name.range(), @@ -433,7 +435,7 @@ impl<'a> CallSitesFinder<'a, '_> { CallHierarchyItem { name: class.name.id.clone(), kind: SymbolKind::Class, - detail: module_detail(self.db, python_file), + detail: module_detail(self.db, resolver_file), file, full_range: class.range(), selection_range: class.name.range(), @@ -450,13 +452,13 @@ impl<'a> CallSitesFinder<'a, '_> { CallHierarchyItem { name: Name::new_static("(lambda)"), kind: SymbolKind::Function, - detail: module_detail(self.db, python_file), + detail: module_detail(self.db, resolver_file), file, full_range: lambda.range(), selection_range: TextRange::new(lambda.start(), end), } } - _ => module_item(self.db, python_file), + _ => module_item(self.db, resolver_file), } } } @@ -467,7 +469,7 @@ struct RawCallSite { } /// Build an item for the module-level enclosing scope (no enclosing function). -fn module_item(db: &dyn Db, file: PythonFile<'_>) -> CallHierarchyItem { +fn module_item(db: &dyn Db, file: ResolverFile<'_>) -> CallHierarchyItem { let name = ty_module_resolver::file_to_module(db, file) .map(|module| Name::new(module.name(db).last_component())) .unwrap_or_else(|| Name::new_static("")); @@ -527,7 +529,7 @@ mod tests { }; let calls = incoming_calls( &self.db, - self.python_file(target.file), + self.program_file(target.file), target.selection_range.start(), ); if calls.is_empty() { @@ -1147,7 +1149,7 @@ def make() -> C: }; let incoming = incoming_calls( &test.db, - test.python_file(target.file), + test.program_file(target.file), target.selection_range.start(), ); // The selection identifies the anonymous callable header. @@ -1280,7 +1282,7 @@ def make() -> C: }; let incoming = incoming_calls( &test.db, - test.python_file(target.file), + test.program_file(target.file), target.selection_range.start(), ); assert_eq!(incoming.len(), 1, "got {incoming:?}"); @@ -1289,7 +1291,7 @@ def make() -> C: let follow_up_incoming = incoming_calls( &test.db, - test.python_file(lambda_item.file), + test.program_file(lambda_item.file), lambda_item.selection_range.start(), ); assert!( @@ -1299,7 +1301,7 @@ def make() -> C: let follow_up_outgoing = outgoing_calls( &test.db, - test.python_file(lambda_item.file), + test.program_file(lambda_item.file), lambda_item.selection_range.start(), ); assert!( diff --git a/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs b/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs index 535f68b7be..a5a35885df 100644 --- a/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs +++ b/crates/ty_ide/src/call_hierarchy/outgoing_calls.rs @@ -3,7 +3,6 @@ use std::collections::hash_map::Entry; use crate::call_hierarchy::CalleeLeaf; use crate::goto::find_goto_target; use crate::{CallHierarchyItem, Db}; -use ruff_db::PythonFile; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::token::Tokens; @@ -16,6 +15,7 @@ use ruff_python_ast::{ }; use ruff_text_size::{Ranged, TextRange, TextSize}; use rustc_hash::FxHashMap; +use ty_python_core::ProgramFile; use ty_python_core::definition::DefinitionKind; use ty_python_semantic::{ImportAliasResolution, SemanticModel}; @@ -30,8 +30,8 @@ use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// are reported when the nested callable is expanded separately. Declaration /// expressions attached to a nested callable are still included while /// traversing the containing item's body. -pub fn outgoing_calls(db: &dyn Db, file: PythonFile<'_>, offset: TextSize) -> Vec { - let module = parsed_module(db, file).load(db); +pub fn outgoing_calls(db: &dyn Db, file: ProgramFile<'_>, offset: TextSize) -> Vec { + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let Some(goto_target) = find_goto_target(&model, &module, offset) else { return Vec::new(); @@ -54,7 +54,7 @@ pub fn outgoing_calls(db: &dyn Db, file: PythonFile<'_>, offset: TextSize) -> Ve }; let parsed = parsed_module(db, def.python_file(db)).load(db); - let model = SemanticModel::new(db, def.python_file(db)); + let model = SemanticModel::new(db, def.program_file(db)); let mut finder = OutgoingCallsFinder { db, model: &model, @@ -306,7 +306,7 @@ mod tests { }; let calls = outgoing_calls( &self.db, - self.python_file(target.file), + self.program_file(target.file), target.selection_range.start(), ); if calls.is_empty() { diff --git a/crates/ty_ide/src/code_action.rs b/crates/ty_ide/src/code_action.rs index 099e26ff26..fcd8648c66 100644 --- a/crates/ty_ide/src/code_action.rs +++ b/crates/ty_ide/src/code_action.rs @@ -2,11 +2,11 @@ use crate::completion; use ruff_db::parsed::parsed_module; -use ruff_db::PythonFile; use ruff_diagnostics::Edit; use ruff_python_ast::find_node::covering_node; use ruff_text_size::TextRange; use ty_project::Db; +use ty_python_core::ProgramFile; use ty_python_semantic::lint::LintId; use ty_python_semantic::suppress_single; use ty_python_semantic::types::{UNDEFINED_REVEAL, UNRESOLVED_REFERENCE}; @@ -21,7 +21,7 @@ pub struct QuickFix { pub fn code_actions( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, diagnostic_range: TextRange, diagnostic_id: &str, ) -> Vec { @@ -44,7 +44,7 @@ pub fn code_actions( // Suggest just suppressing the lint (always a valid option, but never ideal) actions.push(QuickFix { title: format!("Ignore '{}' for this line", lint_id.name()), - edits: suppress_single(db, file, lint_id, diagnostic_range).into_edits(), + edits: suppress_single(db, file.python_file(db), lint_id, diagnostic_range).into_edits(), preferred: false, }); @@ -53,10 +53,10 @@ pub fn code_actions( fn unresolved_fixes( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, diagnostic_range: TextRange, ) -> Option> { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, file.python_file(db)).load(db); let node = covering_node(parsed.syntax().into(), diagnostic_range).node(); let symbol = &node.expr_name()?.id; Some( @@ -77,7 +77,6 @@ mod tests { use insta::assert_snapshot; use ruff_db::{ - PythonFile, diagnostic::{ Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig, LintName, Span, SubDiagnostic, @@ -89,6 +88,7 @@ mod tests { use ruff_python_trivia::textwrap::dedent; use ruff_text_size::{TextRange, TextSize}; use ty_project::ProjectMetadata; + use ty_python_core::ProgramFile; use ty_python_semantic::{ default_lint_registry, lint::LintMetadata, @@ -936,7 +936,11 @@ mod tests { for mut action in code_actions( &self.db, - PythonFile::new(&self.db, self.file, self.db.python_version()), + ProgramFile::new( + &self.db, + self.file, + self.db.program_environment().program(&self.db), + ), self.diagnostic_range, &lint.name, ) { diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 44b830ad8d..2e493b186b 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -3,7 +3,6 @@ use std::collections::{BinaryHeap, binary_heap}; use ty_python_semantic::ProgramEnvironment; use compact_str::{CompactString, CompactStringExt}; -use ruff_db::PythonFile; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_db::source::{SourceText, source_text}; use ruff_diagnostics::Edit; @@ -16,7 +15,8 @@ use ruff_python_codegen::Stylist; use ruff_python_literal::escape::{Escape, UnicodeEscape}; use ruff_text_size::{Ranged, TextRange, TextSize}; use rustc_hash::FxHashSet; -use ty_module_resolver::{KnownModule, Module, ModuleName}; +use ty_module_resolver::{ImportingFile, KnownModule, Module, ModuleName}; +use ty_python_core::ProgramFile; use ty_python_semantic::HasType; use ty_python_semantic::types::{SpecialFormType, UnionType}; use ty_python_semantic::{ @@ -34,18 +34,18 @@ pub fn completion<'db>( db: &'db dyn Db, settings: &CompletionSettings, capabilities: CompletionCapabilities, - file: PythonFile<'db>, + file: ProgramFile<'db>, offset: TextSize, ) -> Vec> { - let python_file = file; - let parsed = parsed_module(db, file).load(db); + let program_file = file; + let parsed = parsed_module(db, file.python_file(db)).load(db); let file = file.file(db); let source = source_text(db, file); - let Some(context) = Context::new(db, python_file, &parsed, &source, offset) else { + let Some(context) = Context::new(db, program_file, &parsed, &source, offset) else { return vec![]; }; - let model = SemanticModel::new(db, python_file); + let model = SemanticModel::new(db, program_file); if !matches!(context.kind, ContextKind::Keywords(_)) && context.cursor.is_in_string() { let Some(string_expr) = context.cursor.enclosing_string_literal_expr() else { @@ -54,7 +54,7 @@ pub fn completion<'db>( let mut completions = Completions::new( db, - python_file, + program_file, CollectionContext::none(), UserQuery::fuzzy(None), ); @@ -72,7 +72,7 @@ pub fn completion<'db>( let query = UserQuery::fuzzy(context.cursor.typed); let mut completions = Completions::new( db, - python_file, + program_file, context.collection_context(db, &model, settings, capabilities), query, ); @@ -84,7 +84,7 @@ pub fn completion<'db>( } } ContextKind::Import(ref import) => { - import.add_completions(db, python_file, &mut completions); + import.add_completions(db, program_file, &mut completions); } ContextKind::NonImport(ref non_import) => match non_import.target { CompletionTargetAst::ObjectDot { expr } => { @@ -106,7 +106,7 @@ pub fn completion<'db>( add_keyword_completions(db, &env, &mut completions); add_argument_completions( db, - python_file, + program_file, &model, &context.cursor, &mut completions, @@ -114,7 +114,7 @@ pub fn completion<'db>( if settings.auto_import { add_unimported_completions( db, - python_file, + program_file, &parsed, scoped, |module_name: &ModuleName, symbol: &str| { @@ -146,7 +146,7 @@ impl CompletionCapabilities { /// A collection of completions built up from various sources. struct Completions<'db> { db: &'db dyn Db, - python_file: PythonFile<'db>, + program_file: ProgramFile<'db>, context: CollectionContext<'db>, items: BinaryHeap>, /// The query used to match against candidate completions. @@ -172,13 +172,13 @@ impl<'db> Completions<'db> { /// add completions that match it. fn new( db: &'db dyn Db, - python_file: PythonFile<'db>, + program_file: ProgramFile<'db>, context: CollectionContext<'db>, query: UserQuery, ) -> Completions<'db> { Completions { db, - python_file, + program_file, context, items: BinaryHeap::new(), query, @@ -274,7 +274,7 @@ impl<'db> Completions<'db> { return false; } let completion = - CompletionRanker(builder.build(self.db, self.python_file, &self.context, &self.query)); + CompletionRanker(builder.build(self.db, self.program_file, &self.context, &self.query)); if self.items.len() >= Completions::LIMIT { // OK because `self.items` is guaranteed to be non-empty here. let worst = self.items.peek_mut().unwrap(); @@ -294,7 +294,7 @@ impl<'db> Extend> for Completions<'db> { T: IntoIterator>, { let db = self.db; - let env = ProgramEnvironment::from_file(self.python_file); + let env = ProgramEnvironment::from_file(self.program_file); for c in it { self.add_semantic(db, &env, c); } @@ -487,7 +487,7 @@ impl<'db> CompletionBuilder<'db> { fn build( mut self, db: &'db dyn Db, - python_file: PythonFile<'db>, + program_file: ProgramFile<'db>, collection_context: &CollectionContext<'db>, query: &UserQuery, ) -> Completion<'db> { @@ -501,7 +501,7 @@ impl<'db> CompletionBuilder<'db> { // but aren't marked here. That is, false negatives are // possible but false positives are not. if let Some(exception_ty) = collection_context.exception_ty { - let env = ProgramEnvironment::from_file(python_file); + let env = ProgramEnvironment::from_file(program_file); self.is_context_specific |= ty.is_assignable_to(db, &env, exception_ty); } if collection_context.is_in_class_def() { @@ -788,7 +788,7 @@ impl<'m> Context<'m> { /// Create a new context for finding completions. fn new( db: &'_ dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, parsed: &'m ParsedModuleRef, source: &'m SourceText, offset: TextSize, @@ -802,7 +802,11 @@ impl<'m> Context<'m> { ContextKind::Keywords(keywords) } else if cursor.is_in_definition_place() { return None; - } else if let Some(import) = ImportStatement::detect(db, file, &cursor) { + } else if let Some(import) = ImportStatement::detect( + db, + ImportingFile::File(file.file(db), file.resolver_environment(db)), + &cursor, + ) { ContextKind::Import(import) } else { let target_token = CompletionTargetTokens::find(&cursor)?; @@ -1964,7 +1968,7 @@ enum Sort { /// Detect and add completions for unset arguments. fn add_argument_completions<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, model: &SemanticModel<'db>, cursor: &ContextCursor<'_>, completions: &mut Completions<'db>, @@ -2052,7 +2056,7 @@ fn add_class_arg_completions<'db>( /// set and 2) been defined as positional-only. fn add_function_arg_completions<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, cursor: &ContextCursor<'_>, completions: &mut Completions<'db>, ) { @@ -2133,7 +2137,7 @@ pub(crate) struct ImportEdit { /// Get fixes that would resolve an unresolved reference pub(crate) fn unresolved_fixes<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, parsed: &ParsedModuleRef, symbol: &str, node: AnyNodeRef, @@ -2276,7 +2280,7 @@ fn add_string_literal_completions<'db>( /// when selected into `File`. fn add_unimported_completions<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, parsed: &ParsedModuleRef, scoped: ScopedTarget<'_>, create_import_request: impl for<'a> Fn(&'a ModuleName, &'a str) -> ImportRequest<'a>, @@ -2295,6 +2299,7 @@ fn add_unimported_completions<'db>( let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str()); let importer = Importer::new(db, &stylist, file, source.as_str(), parsed); let members = importer.members_in_scope_at(scoped.node, scoped.node.start()); + let importing_file = ImportingFile::File(source_file, file.resolver_environment(db)); for symbol in all_symbols(db, file, &completions.query.pattern) { if symbol.file() == source_file || symbol.module().is_known(db, KnownModule::Builtins) { @@ -2311,7 +2316,7 @@ fn add_unimported_completions<'db>( }); // Don't suggest symbols that are already imported. - if members.satisfies(db, file, &request) { + if members.satisfies(db, importing_file, &request) { continue; } @@ -2576,7 +2581,7 @@ impl<'a> ImportStatement<'a> { /// `tokens`. fn detect( db: &'_ dyn Db, - file: PythonFile<'_>, + file: ImportingFile<'_>, cursor: &ContextCursor<'a>, ) -> Option> { use TokenKind as TK; @@ -2974,7 +2979,7 @@ impl<'a> ImportStatement<'a> { fn add_completions<'db>( &self, db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, completions: &mut Completions<'db>, ) { let model = SemanticModel::new(db, file); @@ -3071,7 +3076,7 @@ fn add_import_completions_impl<'db>( semantic_completions: impl IntoIterator>, module_dependency_kind: impl Fn(&SemanticCompletion<'db>) -> Option, ) { - let env = ProgramEnvironment::from_file(completions.python_file); + let env = ProgramEnvironment::from_file(completions.program_file); for semantic in semantic_completions { let module_dependency_kind = module_dependency_kind(&semantic); let mut builder = CompletionBuilder::from_semantic_completion(db, &env, semantic); @@ -10877,7 +10882,7 @@ raise &self.cursor_test.db, &self.settings, self.capabilities, - self.cursor_test.python_file(self.cursor_test.cursor.file), + self.cursor_test.program_file(self.cursor_test.cursor.file), self.cursor_test.cursor.offset, ); let filtered = original diff --git a/crates/ty_ide/src/doc_highlights.rs b/crates/ty_ide/src/doc_highlights.rs index e4fe854908..5f3116ecc4 100644 --- a/crates/ty_ide/src/doc_highlights.rs +++ b/crates/ty_ide/src/doc_highlights.rs @@ -1,18 +1,18 @@ use crate::goto::find_goto_target; use crate::references::{ReferencesMode, references}; use crate::{Db, ReferenceTarget}; -use ruff_db::PythonFile; use ruff_text_size::TextSize; +use ty_python_core::ProgramFile; use ty_python_semantic::SemanticModel; /// Find all document highlights for a symbol at the given position. /// Document highlights are limited to the current file only. pub fn document_highlights( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, offset: TextSize, ) -> Option> { - let parsed = ruff_db::parsed::parsed_module(db, file); + let parsed = ruff_db::parsed::parsed_module(db, file.python_file(db)); let module = parsed.load(db); let model = SemanticModel::new(db, file); @@ -36,7 +36,7 @@ mod tests { fn document_highlights(&self) -> String { let Some(highlight_results) = document_highlights( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, ) else { return "No highlights found".to_string(); diff --git a/crates/ty_ide/src/document_symbols.rs b/crates/ty_ide/src/document_symbols.rs index fb72ca94dc..facfda6b07 100644 --- a/crates/ty_ide/src/document_symbols.rs +++ b/crates/ty_ide/src/document_symbols.rs @@ -1,9 +1,9 @@ use crate::symbols::{FlatSymbols, symbols_for_file}; -use ruff_db::PythonFile; use ty_project::Db; +use ty_python_core::ProgramFile; /// Get all document symbols for a file with the given options. -pub fn document_symbols<'db>(db: &'db dyn Db, file: PythonFile<'db>) -> &'db FlatSymbols { +pub fn document_symbols<'db>(db: &'db dyn Db, file: ProgramFile<'db>) -> &'db FlatSymbols { symbols_for_file(db, file) } @@ -405,7 +405,7 @@ def function(): ", ); - let symbols = document_symbols(&test.db, test.python_file(test.cursor.file)) + let symbols = document_symbols(&test.db, test.program_file(test.cursor.file)) .iter() .map(|(_, symbol)| (symbol.name.into_owned(), symbol.kind)) .collect::>(); @@ -442,7 +442,7 @@ lambda_value = lambda: (lambda_local := 1) ", ); - let names = document_symbols(&test.db, test.python_file(test.cursor.file)) + let names = document_symbols(&test.db, test.program_file(test.cursor.file)) .iter() .map(|(_, symbol)| symbol.name.into_owned()) .collect::>(); @@ -464,7 +464,7 @@ class Example((class_base := Base)): ", ); - let symbols = document_symbols(&test.db, test.python_file(test.cursor.file)) + let symbols = document_symbols(&test.db, test.program_file(test.cursor.file)) .iter() .map(|(_, symbol)| (symbol.name.into_owned(), symbol.kind)) .collect::>(); @@ -487,7 +487,7 @@ class Example((class_base := Base)): impl CursorTest { fn document_symbols(&self) -> String { let symbols = - document_symbols(&self.db, self.python_file(self.cursor.file)).to_hierarchical(); + document_symbols(&self.db, self.program_file(self.cursor.file)).to_hierarchical(); if symbols.is_empty() { return "No symbols found".to_string(); diff --git a/crates/ty_ide/src/find_references.rs b/crates/ty_ide/src/find_references.rs index d664bc0513..f0b999175f 100644 --- a/crates/ty_ide/src/find_references.rs +++ b/crates/ty_ide/src/find_references.rs @@ -1,19 +1,19 @@ use crate::goto::find_goto_target; use crate::references::{ReferencesMode, references}; use crate::{Db, ReferenceTarget}; -use ruff_db::PythonFile; use ruff_text_size::TextSize; +use ty_python_core::ProgramFile; use ty_python_semantic::SemanticModel; /// Find all references to a symbol at the given position. /// Search for references across all files in the project. pub fn find_references( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, offset: TextSize, include_declaration: bool, ) -> Option> { - let parsed = ruff_db::parsed::parsed_module(db, file); + let parsed = ruff_db::parsed::parsed_module(db, file.python_file(db)); let module = parsed.load(db); let model = SemanticModel::new(db, file); @@ -48,7 +48,7 @@ mod tests { fn references_with_include_declaration(&self, include_declaration: bool) -> String { let Some(mut reference_results) = find_references( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, include_declaration, ) else { diff --git a/crates/ty_ide/src/folding_range.rs b/crates/ty_ide/src/folding_range.rs index 7d84c2a492..6507f3c19a 100644 --- a/crates/ty_ide/src/folding_range.rs +++ b/crates/ty_ide/src/folding_range.rs @@ -2579,7 +2579,11 @@ with open("file.txt") as f: impl CursorTest { fn folding_ranges(&self) -> String { - let ranges = folding_ranges(&self.db, self.python_file(self.cursor.file), None); + let ranges = folding_ranges( + &self.db, + self.program_file(self.cursor.file).python_file(&self.db), + None, + ); if ranges.is_empty() { return "No folding ranges found".to_string(); diff --git a/crates/ty_ide/src/goto.rs b/crates/ty_ide/src/goto.rs index 7b22e17d6d..aec1ee3913 100644 --- a/crates/ty_ide/src/goto.rs +++ b/crates/ty_ide/src/goto.rs @@ -14,6 +14,7 @@ use ruff_python_ast::token::{Token, TokenAt, TokenKind, Tokens}; use ruff_python_ast::{self as ast, AnyNodeRef, ExprRef}; use ruff_text_size::{Ranged, TextRange, TextSize}; +use ty_python_core::ProgramFile; use ty_python_core::definition::{Definition, DefinitionKind}; use ty_python_semantic::types::Type; use ty_python_semantic::types::ide_support::{ @@ -265,7 +266,7 @@ impl<'db> Definitions<'db> { let ty_def = ty.definition(db, env)?; let resolved = match ty_def { ty_python_semantic::types::TypeDefinition::Module(module) => { - ResolvedDefinition::Module(module.python_file(db)?) + ResolvedDefinition::Module(ProgramFile::new(db, module.file(db)?, env.program(db))) } ty_python_semantic::types::TypeDefinition::StaticClass(definition) | ty_python_semantic::types::TypeDefinition::DynamicClass(definition) @@ -1451,7 +1452,7 @@ fn definitions_for_module<'db>( level: u32, ) -> Option>> { let module = model.resolve_module(module, level)?; - let file = module.python_file(model.db())?; + let file = ProgramFile::new(model.db(), module.file(model.db())?, model.program()); Some(vec![ResolvedDefinition::Module(file)]) } diff --git a/crates/ty_ide/src/goto_declaration.rs b/crates/ty_ide/src/goto_declaration.rs index 3961dde00d..9982fcafa8 100644 --- a/crates/ty_ide/src/goto_declaration.rs +++ b/crates/ty_ide/src/goto_declaration.rs @@ -1,9 +1,9 @@ use crate::goto::find_goto_target; use crate::{Db, NavigationTargets, RangedValue}; -use ruff_db::PythonFile; use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; +use ty_python_core::ProgramFile; use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// Navigate to the declaration of a symbol. @@ -13,10 +13,10 @@ use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// is needed because Python doesn't require formal declarations of variables like most languages do. pub fn goto_declaration( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, offset: TextSize, ) -> Option> { - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; @@ -2768,7 +2768,7 @@ def ab(a: int, *, c: int): ... let Some(targets) = salsa::attach(&self.db, || { goto_declaration( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, ) }) else { diff --git a/crates/ty_ide/src/goto_definition.rs b/crates/ty_ide/src/goto_definition.rs index a6e6d83de9..2f6fc9c9ef 100644 --- a/crates/ty_ide/src/goto_definition.rs +++ b/crates/ty_ide/src/goto_definition.rs @@ -1,9 +1,9 @@ use crate::goto::find_goto_target; use crate::{Db, NavigationTargets, RangedValue}; -use ruff_db::PythonFile; use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; +use ty_python_core::ProgramFile; use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// Navigate to the definition of a symbol. @@ -14,10 +14,10 @@ use ty_python_semantic::{ImportAliasResolution, SemanticModel}; /// source file implementations using the `StubMapper`. pub fn goto_definition( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, offset: TextSize, ) -> Option> { - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; let definition_targets = goto_target @@ -2651,7 +2651,7 @@ class GenericFoo[T](Base): let Some(targets) = salsa::attach(&self.db, || { goto_definition( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, ) }) else { diff --git a/crates/ty_ide/src/goto_implementation.rs b/crates/ty_ide/src/goto_implementation.rs index 54b055ce5c..64f441f249 100644 --- a/crates/ty_ide/src/goto_implementation.rs +++ b/crates/ty_ide/src/goto_implementation.rs @@ -58,11 +58,11 @@ use crate::goto::{Definitions, GotoTarget, find_goto_target}; use crate::{Db, NavigationTarget, NavigationTargets, RangedValue}; use rayon::prelude::*; -use ruff_db::PythonFile; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; use ty_project::parallel::ParallelIteratorExt; +use ty_python_core::ProgramFile; use ty_python_semantic::{ ImplementationsFinder, ImportAliasResolution, ResolvedDefinition, SemanticModel, }; @@ -73,15 +73,15 @@ use ty_python_semantic::{ /// identified. pub fn goto_implementation( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, offset: TextSize, ) -> Option> { - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; let finder = prepare_implementations_finder_for_goto_target(&model, &goto_target)?; let source_file = file.file(db); - let python_version = file.python_version(db); + let program = file.program(db); let mut candidate_files: Vec = db .project() @@ -95,7 +95,7 @@ pub fn goto_implementation( let batches = candidate_files .into_par_iter() .map_with_db(db, |db, file| { - let file = PythonFile::new(db, file, python_version); + let file = ProgramFile::new(db, file, program); let definitions = finder.implementations_for_file(db, file); definitions_to_implementation_targets(db, definitions) }) @@ -838,7 +838,7 @@ mod tests { let targets = salsa::attach(&test.db, || { goto_implementation( &test.db, - test.python_file(test.cursor.file), + test.program_file(test.cursor.file), test.cursor.offset, ) .expect("implementation targets") @@ -2144,7 +2144,7 @@ class MyClass: let Some(targets) = salsa::attach(&self.db, || { goto_implementation( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, ) }) else { diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs index 84244ea8ae..893f98a9f3 100644 --- a/crates/ty_ide/src/goto_type_definition.rs +++ b/crates/ty_ide/src/goto_type_definition.rs @@ -1,17 +1,17 @@ use crate::goto::find_goto_target; use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue}; -use ruff_db::PythonFile; use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; +use ty_python_core::ProgramFile; use ty_python_semantic::SemanticModel; pub fn goto_type_definition( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, offset: TextSize, ) -> Option> { - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; @@ -2055,7 +2055,7 @@ def function(): let Some(targets) = salsa::attach(&self.db, || { goto_type_definition( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, ) }) else { diff --git a/crates/ty_ide/src/hints.rs b/crates/ty_ide/src/hints.rs index 21c65ec179..1581bc3382 100644 --- a/crates/ty_ide/src/hints.rs +++ b/crates/ty_ide/src/hints.rs @@ -1,6 +1,6 @@ -use ruff_db::PythonFile; use ruff_python_ast::name::Name; use ruff_text_size::TextRange; +use ty_python_core::ProgramFile; use ty_python_semantic::types::ide_support::{ UnreachableKind, unreachable_ranges, unused_bindings, }; @@ -40,7 +40,7 @@ impl HintKind { } } -pub fn hints(db: &dyn Db, file: PythonFile<'_>) -> Vec { +pub fn hints(db: &dyn Db, file: ProgramFile<'_>) -> Vec { let source_file = file.file(db); if !db.should_check_file(source_file) { return Vec::new(); diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index a7d9920e33..36b3024ff5 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -1,13 +1,13 @@ use crate::docstring::{Docstring, DocstringFragment}; use crate::goto::{Definitions, GotoTarget, docstring_for_call_definition, find_goto_target}; use crate::{Db, MarkupKind, RangedValue}; -use ruff_db::PythonFile; use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextSize}; use std::fmt; use std::fmt::Formatter; +use ty_python_core::ProgramFile; use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::types::ide_support::{resolved_call_signature, typed_dict_key_hover}; use ty_python_semantic::types::{KnownInstanceType, Type, TypeAliasType, TypeVarVariance}; @@ -16,10 +16,10 @@ use ty_python_semantic::{DisplaySettings, SemanticModel, TypeQualifiers}; pub fn hover<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, offset: TextSize, ) -> Option>> { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &parsed, offset)?; @@ -142,7 +142,7 @@ pub fn hover<'db>( Some(RangedValue { range: FileRange::new(file.file(db), goto_target.range()), value: Hover { - python_file: file, + program_file: file, contents, }, }) @@ -224,7 +224,7 @@ fn documentation_for_parameter(docstring: &Docstring, name: &str) -> Option { - python_file: PythonFile<'db>, + program_file: ProgramFile<'db>, contents: Vec>, } @@ -271,7 +271,7 @@ impl fmt::Display for DisplayHover<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let db = self.db; let mut first = true; - let env = ProgramEnvironment::from_file(self.hover.python_file); + let env = ProgramEnvironment::from_file(self.hover.program_file); for content in &self.hover.contents { if !first { self.kind.horizontal_line().fmt(f)?; @@ -6900,7 +6900,7 @@ type U = MyType let Some(hover) = hover( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, ) else { return "Hover provided no content".to_string(); diff --git a/crates/ty_ide/src/importer.rs b/crates/ty_ide/src/importer.rs index 821b0ffa2a..20b9fcc12b 100644 --- a/crates/ty_ide/src/importer.rs +++ b/crates/ty_ide/src/importer.rs @@ -20,7 +20,6 @@ use rustc_hash::FxHashMap; use ruff_db::parsed::ParsedModuleRef; -use ruff_db::PythonFile; use ruff_db::source::source_text; use ruff_diagnostics::Edit; use ruff_python_ast as ast; @@ -30,8 +29,9 @@ use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal use ruff_python_codegen::Stylist; use ruff_python_importer::Insertion; use ruff_text_size::{Ranged, TextRange, TextSize}; -use ty_module_resolver::ModuleName; +use ty_module_resolver::{ImportingFile, ModuleName}; use ty_project::Db; +use ty_python_core::ProgramFile; use ty_python_core::definition::DefinitionKind; use ty_python_semantic::types::Type; use ty_python_semantic::{MemberDefinition, SemanticModel}; @@ -41,7 +41,7 @@ pub(crate) struct Importer<'a> { db: &'a dyn Db, /// The file corresponding to the module that /// we want to insert an import statement into. - file: PythonFile<'a>, + file: ProgramFile<'a>, /// The parsed module ref. parsed: &'a ParsedModuleRef, /// The tokens representing the Python AST. @@ -74,7 +74,7 @@ impl<'a> Importer<'a> { pub(crate) fn new( db: &'a dyn Db, stylist: &'a Stylist<'a>, - file: PythonFile<'a>, + file: ProgramFile<'a>, source: &'a str, parsed: &'a ParsedModuleRef, ) -> Self { @@ -146,9 +146,13 @@ impl<'a> Importer<'a> { request: ImportRequest<'_>, members: &MembersInScope, ) -> ImportAction { - let request = request.avoid_conflicts(self.db, self.file, members); + let importing_file = ImportingFile::File( + self.file.file(self.db), + self.file.resolver_environment(self.db), + ); + let request = request.avoid_conflicts(self.db, importing_file, members); let mut symbol_text: Box = request.member.unwrap_or(request.module).into(); - let Some(response) = self.find(&request, members.at) else { + let Some(response) = self.find(importing_file, &request, members.at) else { let insertion = if let Some(future) = self.find_last_future_import(members.at) { Insertion::end_of_statement(future.stmt, self.source, self.stylist) } else { @@ -223,6 +227,7 @@ impl<'a> Importer<'a> { /// satisfies the request. fn find<'importer>( &'importer self, + importing_file: ImportingFile<'_>, request: &ImportRequest<'_>, available_at: TextSize, ) -> Option> { @@ -248,7 +253,7 @@ impl<'a> Importer<'a> { return choice; } - if let Some(response) = import.satisfies(self.db, self.file, request) { + if let Some(response) = import.satisfies(self.db, importing_file, request) { let partial = matches!(response.kind, ImportResponseKind::Partial { .. }); // The LSP doesn't support edits across cell boundaries. @@ -331,7 +336,7 @@ pub struct MembersInScope<'ast> { impl<'ast> MembersInScope<'ast> { fn new( db: &'ast dyn Db, - file: PythonFile<'ast>, + file: ProgramFile<'ast>, parsed: &'ast ParsedModuleRef, node: ast::AnyNodeRef<'_>, at: TextSize, @@ -373,7 +378,7 @@ impl<'ast> MembersInScope<'ast> { pub(crate) fn satisfies( &self, db: &dyn Db, - importing_file: PythonFile<'_>, + importing_file: ImportingFile<'_>, request: &ImportRequest<'_>, ) -> bool { let symbol_text = request.member.unwrap_or(request.module); @@ -409,7 +414,7 @@ impl<'ast> MemberInScope<'ast> { fn satisfies_anywhere( &self, db: &dyn Db, - importing_file: PythonFile<'_>, + importing_file: ImportingFile<'_>, request: &ImportRequest<'_>, ) -> bool { let MemberImportKind::Imported(ref ast_import) = self.kind else { @@ -481,7 +486,7 @@ impl<'ast> AstImport<'ast> { fn satisfies<'importer>( &'importer self, db: &'_ dyn Db, - importing_file: PythonFile<'_>, + importing_file: ImportingFile<'_>, request: &ImportRequest<'_>, ) -> Option> { self.kind @@ -512,7 +517,7 @@ impl<'ast> AstImportKind<'ast> { fn satisfies<'importer>( &'importer self, db: &'_ dyn Db, - importing_file: PythonFile<'_>, + importing_file: ImportingFile<'_>, request: &ImportRequest<'_>, ) -> Option> { match *self { @@ -639,7 +644,7 @@ impl<'a> ImportRequest<'a> { fn avoid_conflicts( self, db: &dyn Db, - importing_file: PythonFile<'_>, + importing_file: ImportingFile<'_>, members: &MembersInScope, ) -> Self { let Some(member) = self.member else { @@ -979,7 +984,11 @@ mod tests { Importer::new( &self.db, &self.cursor.stylist, - PythonFile::new(&self.db, self.cursor.file, self.db.python_version()), + ProgramFile::new( + &self.db, + self.cursor.file, + self.db.program_environment().program(&self.db), + ), self.cursor.source.as_str(), &self.cursor.parsed, ) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 7ecb1de55e..4669252242 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -5,7 +5,6 @@ use rustc_hash::FxHashMap; use crate::importer::{ImportAction, ImportRequest, Importer, MembersInScope}; use crate::{Db, HasNavigationTargets, NavigationTarget}; -use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal}; @@ -13,6 +12,7 @@ use ruff_python_ast::{AnyNodeRef, ArgOrKeyword, Expr, ExprUnaryOp, Stmt, UnaryOp use ruff_python_codegen::Stylist; use ruff_text_size::{Ranged, TextRange, TextSize}; use ty_module_resolver::file_to_module; +use ty_python_core::ProgramFile; use ty_python_semantic::types::ide_support::inlay_hint_call_argument_details; use ty_python_semantic::types::{Type, TypeDetail}; use ty_python_semantic::{HasType, SemanticModel}; @@ -103,7 +103,8 @@ impl InlayHint { .as_deref() .unwrap_or(&details.label[start..end]); - let module = file_to_module(db, definition.python_file(db))?; + let file = definition.program_file(db); + let module = file_to_module(db, file.resolver_file(db))?; if should_skip_import(db, module, *ty) { return None; @@ -291,11 +292,11 @@ pub struct InlayHintTextEdit { pub fn inlay_hints( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, range: TextRange, settings: &InlayHintSettings, ) -> Vec { - let ast = parsed_module(db, file).load(db); + let ast = parsed_module(db, file.python_file(db)).load(db); let source_file = file.file(db); let source = source_text(db, source_file); @@ -348,7 +349,7 @@ impl Default for InlayHintSettings { struct InlayHintImportContext<'a, 'db> { db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, importer: &'a Importer<'db>, dynamic_imports: &'a mut FxHashMap, } @@ -370,7 +371,7 @@ struct InlayHintVisitor<'a, 'db> { impl<'a, 'db> InlayHintVisitor<'a, 'db> { fn new( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, importer: Importer<'db>, range: TextRange, settings: &'a InlayHintSettings, @@ -399,7 +400,7 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> { let context = InlayHintImportContext { db: self.db, - file: self.model.python_file(), + file: self.model.program_file(), importer: &self.importer, dynamic_imports: &mut self.dynamic_imports, }; @@ -882,7 +883,11 @@ mod tests { fn inlay_hints_with_settings(&mut self, settings: &InlayHintSettings) -> String { let hints = inlay_hints( &self.db, - PythonFile::new(&self.db, self.file, self.db.python_version()), + ProgramFile::new( + &self.db, + self.file, + self.db.program_environment().program(&self.db), + ), self.range, settings, ); diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index b0bdcf8403..34ec0997a9 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -403,7 +403,6 @@ mod tests { use insta::internals::SettingsBindDropGuard; use ruff_db::Db; - use ruff_db::PythonFile; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, UnifiedFile, }; @@ -416,7 +415,8 @@ mod tests { use ruff_python_trivia::textwrap::dedent; use ruff_text_size::TextSize; use ty_module_resolver::SearchPathSettings; - use ty_project::{Db as _, ProjectMetadata}; + use ty_project::{Db as _, ProjectMetadata, SemanticDb as _}; + use ty_python_core::ProgramFile; use ty_python_core::platform::PythonPlatform; use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; use ty_python_semantic::PythonVersionWithSource; @@ -440,8 +440,8 @@ mod tests { CursorTestBuilder::default() } - pub(super) fn python_file(&self, file: File) -> PythonFile<'_> { - PythonFile::new(&self.db, file, self.db.python_version()) + pub(super) fn program_file(&self, file: File) -> ProgramFile<'_> { + self.db.program_file(file) } pub(super) fn write_file( @@ -569,8 +569,7 @@ mod tests { let source = source_text(&db, file); let parsed = - parsed_module(&db, PythonFile::new(&db, file, db.python_version())) - .load(&db); + parsed_module(&db, db.program_file(file).python_file(&db)).load(&db); let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str()).into_owned(); cursor = Some(Cursor { @@ -723,8 +722,7 @@ mod tests { let source = source_text(&db, file); let parsed = - parsed_module(&db, PythonFile::new(&db, file, db.python_version())) - .load(&db); + parsed_module(&db, db.program_file(file).python_file(&db)).load(&db); let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str()).into_owned(); cursor = Some(Cursor { diff --git a/crates/ty_ide/src/references.rs b/crates/ty_ide/src/references.rs index 071d779ac6..0bbadfd39a 100644 --- a/crates/ty_ide/src/references.rs +++ b/crates/ty_ide/src/references.rs @@ -13,7 +13,6 @@ use crate::goto::{Definitions, GotoTarget}; use crate::{Db, ReferenceKind, ReferenceTarget}; use rayon::prelude::*; -use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast::find_node::{CoveringNode, covering_node}; use ruff_python_ast::token::Tokens; @@ -23,6 +22,7 @@ use ruff_python_ast::{ }; use ruff_text_size::Ranged; use ty_project::parallel::{ParallelIteratorExt, minimum_parallel_job_len}; +use ty_python_core::ProgramFile; use ty_python_core::definition::{Definition, DefinitionKind, DefinitionState}; use ty_python_core::scope::{FileScopeId, NodeWithScopeKind, ScopeKind}; use ty_python_semantic::{ @@ -87,7 +87,7 @@ impl ReferencesMode { /// Search for references across all files in the project. pub(crate) fn references( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, goto_target: &GotoTarget, mode: ReferencesMode, ) -> Option> { @@ -118,8 +118,8 @@ pub(crate) fn references( let is_parameter = parameter_owner_is_externally_visible(db, &target_definitions); if search_across_files && (is_parameter || is_externally_visible_symbol) { + let program = model.program(); let files = db.project().files(db); - let python_version = file.python_version(db); let files: Vec<_> = files .iter() .copied() @@ -135,7 +135,7 @@ pub(crate) fn references( return Vec::new(); } - let other_file = PythonFile::new(db, other_file, python_version); + let other_file = ProgramFile::new(db, other_file, program); if is_externally_visible_symbol { references_for_file(db, other_file, &target_definitions, &target_text, mode) @@ -164,7 +164,7 @@ pub(crate) fn references( fn references_for_keyword_arguments_in_file( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, target_definitions: &Definitions<'_>, target_text: &str, mode: ReferencesMode, @@ -176,7 +176,7 @@ fn references_for_keyword_arguments_in_file( "keyword-label cross-file scan should not run in DocumentHighlights mode" ); - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, file.python_file(db)); let module = parsed.load(db); let model = SemanticModel::new(db, file); let mut references = Vec::new(); @@ -225,12 +225,12 @@ fn is_slots_assignment(node: AnyNodeRef<'_>, value: AnyNodeRef<'_>) -> bool { /// The behavior depends on the provided mode. fn references_for_file( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, target_definitions: &Definitions<'_>, target_text: &str, mode: ReferencesMode, ) -> Vec { - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, file.python_file(db)); let module = parsed.load(db); let model = SemanticModel::new(db, file); let mut references = Vec::new(); @@ -261,7 +261,7 @@ pub(crate) fn has_any_external_visible_definitions( ScopeKind::Comprehension => { matches!(definition.kind(db), DefinitionKind::NamedExpression(_)) && definition.place(db).as_symbol().is_some_and(|symbol_id| { - ty_python_core::semantic_index(db, definition.python_file(db)) + ty_python_core::semantic_index(db, definition.program_file(db)) .symbol_resolves_to_global_scope(symbol_id, definition.file_scope(db)) }) } @@ -707,7 +707,7 @@ impl<'a> LocalReferencesFinder<'a> { let file = self.model.file(); let class_range = class.range(); let module = ruff_db::parsed::parsed_module(db, self.model.python_file()).load(db); - let index = ty_python_core::semantic_index(db, self.model.python_file()); + let index = ty_python_core::semantic_index(db, self.model.program_file()); // The nearest class scope lexically enclosing `scope`, if any. `ancestor_scopes` skips // class scopes for name resolution, so we walk the lexical parents directly to stop at the @@ -808,7 +808,7 @@ mod tests { use crate::tests::{CursorTest, cursor_test}; fn cursor_target_is_externally_visible(test: &CursorTest) -> bool { - let model = SemanticModel::new(&test.db, test.python_file(test.cursor.file)); + let model = SemanticModel::new(&test.db, test.program_file(test.cursor.file)); let goto_target = find_goto_target(&model, &test.cursor.parsed, test.cursor.offset).unwrap(); let definitions = goto_target diff --git a/crates/ty_ide/src/rename.rs b/crates/ty_ide/src/rename.rs index 2a5a789aa4..273cbfe69d 100644 --- a/crates/ty_ide/src/rename.rs +++ b/crates/ty_ide/src/rename.rs @@ -1,18 +1,18 @@ use crate::goto::find_goto_target; use crate::references::{ReferencesMode, references}; use crate::{Db, ReferenceTarget}; -use ruff_db::PythonFile; use ruff_db::files::File; use ruff_text_size::{Ranged, TextSize}; +use ty_python_core::ProgramFile; use ty_python_semantic::SemanticModel; /// Returns the range of the symbol if it can be renamed, None if not. pub fn can_rename( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, offset: TextSize, ) -> Option { - let parsed = ruff_db::parsed::parsed_module(db, file); + let parsed = ruff_db::parsed::parsed_module(db, file.python_file(db)); let module = parsed.load(db); let source_file = file.file(db); let model = SemanticModel::new(db, file); @@ -56,11 +56,11 @@ pub fn can_rename( /// Returns all locations that need to be updated with the new name. pub fn rename( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, offset: TextSize, new_name: &str, ) -> Option> { - let parsed = ruff_db::parsed::parsed_module(db, file); + let parsed = ruff_db::parsed::parsed_module(db, file.python_file(db)); let module = parsed.load(db); let model = SemanticModel::new(db, file); @@ -108,7 +108,7 @@ mod tests { let Some(range) = salsa::attach(&self.db, || { can_rename( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, ) }) else { @@ -122,13 +122,13 @@ mod tests { let rename_results = salsa::attach(&self.db, || { can_rename( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, )?; rename( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, new_name, ) diff --git a/crates/ty_ide/src/selection_range.rs b/crates/ty_ide/src/selection_range.rs index 2b6bc9b4dd..686ec08ad1 100644 --- a/crates/ty_ide/src/selection_range.rs +++ b/crates/ty_ide/src/selection_range.rs @@ -437,7 +437,7 @@ b"123a𝐁c" fn selection_range(&self) -> String { let ranges = selection_range( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file).python_file(&self.db), self.cursor.offset, ); diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index 06b8e36548..0432ab3df7 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -28,7 +28,6 @@ use crate::Db; use bitflags::bitflags; -use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast::visitor::source_order::{ SourceOrderVisitor, TraversalSignal, walk_arguments, walk_expr, @@ -40,6 +39,7 @@ use ruff_python_ast::{ }; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use std::ops::Deref; +use ty_python_core::ProgramFile; use ty_python_core::definition::{Definition, DefinitionKind, ParameterDefinitionNodeKind}; use ty_python_semantic::{ HasType, ImportAliasResolution, ResolvedDefinition, SemanticModel, definitions_for_attribute, @@ -185,10 +185,10 @@ impl Deref for SemanticTokens { /// Pass None to get tokens for the entire file. pub fn semantic_tokens( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, range: Option, ) -> SemanticTokens { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let mut visitor = SemanticTokenVisitor::new(&model, range); @@ -303,7 +303,7 @@ impl<'db> SemanticTokenVisitor<'db> { ) -> Option<(SemanticTokenType, SemanticTokenModifier)> { let mut modifiers = SemanticTokenModifier::empty(); let db = self.model.db(); - let model = SemanticModel::new(db, definition.python_file(db)); + let model = SemanticModel::new(db, definition.program_file(db)); if model.is_type_alias_definition(definition) { return Some((SemanticTokenType::Class, modifiers)); @@ -4723,7 +4723,11 @@ from pathlib import Missing as Alias fn highlight_file(&self) -> SemanticTokens { semantic_tokens( &self.db, - PythonFile::new(&self.db, self.file, self.db.python_version()), + ProgramFile::new( + &self.db, + self.file, + self.db.program_environment().program(&self.db), + ), None, ) } @@ -4732,7 +4736,11 @@ from pathlib import Missing as Alias fn highlight_range(&self, range: TextRange) -> SemanticTokens { semantic_tokens( &self.db, - PythonFile::new(&self.db, self.file, self.db.python_version()), + ProgramFile::new( + &self.db, + self.file, + self.db.program_environment().program(&self.db), + ), Some(range), ) } diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs index a94d33d4b3..c7b00ab43d 100644 --- a/crates/ty_ide/src/signature_help.rs +++ b/crates/ty_ide/src/signature_help.rs @@ -10,12 +10,12 @@ use crate::Db; use crate::FxIndexMap; use crate::docstring::Docstring; use crate::goto::docstring_for_call_definition; -use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast::find_node::covering_node; use ruff_python_ast::token::TokenKind; use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_text_size::{Ranged, TextSize}; +use ty_python_core::ProgramFile; use ty_python_semantic::SemanticModel; use ty_python_semantic::types::Type; use ty_python_semantic::types::ide_support::{ @@ -76,10 +76,10 @@ pub struct SignatureHelpInfo<'db> { /// Signature help information for function calls at the given position pub fn signature_help<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, offset: TextSize, ) -> Option> { - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, file.python_file(db)).load(db); // Get the call expression at the given position. let (call_expr, current_arg_index) = get_call_expr(&parsed, offset)?; @@ -1464,7 +1464,7 @@ def ab(a: int, *, c: int): fn signature_help(&self) -> Option> { crate::signature_help::signature_help( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, ) } diff --git a/crates/ty_ide/src/symbols.rs b/crates/ty_ide/src/symbols.rs index 86fda0e4b8..2ccdaec2ec 100644 --- a/crates/ty_ide/src/symbols.rs +++ b/crates/ty_ide/src/symbols.rs @@ -8,15 +8,15 @@ use regex::Regex; use ruff_db::parsed::parsed_module; -use ruff_db::PythonFile; use ruff_index::{IndexVec, newtype_index}; use ruff_python_ast as ast; use ruff_python_ast::name::{Name, UnqualifiedName}; use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::{FxHashMap, FxHashSet}; -use ty_module_resolver::{ModuleName, resolve_module}; +use ty_module_resolver::{ImportingFile, ModuleName, resolve_module}; use ty_project::Db; +use ty_python_core::ProgramFile; use crate::completion::CompletionKind; @@ -392,8 +392,8 @@ impl SymbolKind { /// The flattened list includes parent/child information and can be /// converted into a hierarchical collection of symbols. #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn symbols_for_file(db: &dyn Db, file: PythonFile<'_>) -> FlatSymbols { - let parsed = parsed_module(db, file); +pub(crate) fn symbols_for_file(db: &dyn Db, file: ProgramFile<'_>) -> FlatSymbols { + let parsed = parsed_module(db, file.python_file(db)); let module = parsed.load(db); let mut visitor = SymbolVisitor::tree(db, file); @@ -411,9 +411,9 @@ pub(crate) fn symbols_for_file(db: &dyn Db, file: PythonFile<'_>) -> FlatSymbols cycle_initial=|_, _, _| FlatSymbols::default(), heap_size=ruff_memory_usage::heap_size, )] -pub(crate) fn symbols_for_file_global_only(db: &dyn Db, file: PythonFile<'_>) -> FlatSymbols { +pub(crate) fn symbols_for_file_global_only(db: &dyn Db, file: ProgramFile<'_>) -> FlatSymbols { let source_file = file.file(db); - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, file.python_file(db)); let module = parsed.load(db); let mut visitor = SymbolVisitor::globals(db, file); @@ -455,7 +455,7 @@ impl ImportedFrom { fn import_from( db: &dyn Db, - importing_file: PythonFile<'_>, + importing_file: ImportingFile<'_>, ast: &ast::StmtImportFrom, kind: ImportKind, ) -> Option { @@ -596,16 +596,22 @@ impl<'db> Imports<'db> { fn get_module_symbols( &self, db: &'db dyn Db, - importing_file: PythonFile<'db>, + program_file: ProgramFile<'db>, name: &ModuleName, ) -> Option<&'db FlatSymbols> { - let module_name = match self.module_names.get(name.as_str())? { + let module_kind = self.module_names.get(name.as_str())?; + let importing_file = + ImportingFile::File(program_file.file(db), program_file.resolver_environment(db)); + let module_name = match module_kind { ImportModuleKind::Definitive(name) | ImportModuleKind::Possible(name) => { name.to_module_name(db, importing_file)? } }; let module = resolve_module(db, importing_file, &module_name)?; - Some(symbols_for_file_global_only(db, module.python_file(db)?)) + Some(symbols_for_file_global_only( + db, + ProgramFile::new(db, module.file(db)?, program_file.program(db)), + )) } } @@ -657,7 +663,7 @@ impl<'db> ImportModuleName<'db> { fn to_module_name( self, db: &'db dyn Db, - importing_file: PythonFile<'db>, + importing_file: ImportingFile<'db>, ) -> Option { match self { ImportModuleName::Import(name) => ModuleName::new(name), @@ -694,7 +700,7 @@ impl Ranged for AstImport<'_> { #[expect(clippy::struct_excessive_bools)] struct SymbolVisitor<'db> { db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, symbols: IndexVec, symbol_stack: Vec, /// Track if we're currently inside a function at any point. @@ -732,7 +738,7 @@ struct SymbolVisitor<'db> { } impl<'db> SymbolVisitor<'db> { - fn tree(db: &'db dyn Db, file: PythonFile<'db>) -> Self { + fn tree(db: &'db dyn Db, file: ProgramFile<'db>) -> Self { Self { db, file, @@ -750,7 +756,7 @@ impl<'db> SymbolVisitor<'db> { } } - fn globals(db: &'db dyn Db, file: PythonFile<'db>) -> Self { + fn globals(db: &'db dyn Db, file: ProgramFile<'db>) -> Self { Self { exports_only: true, ..Self::tree(db, file) @@ -906,9 +912,15 @@ impl<'db> SymbolVisitor<'db> { let full_range = import.range(); let Some(imported_from) = (match import { AstImport::Import(_) => ImportedFrom::import(alias, import_kind), - AstImport::ImportFrom(ast) => { - ImportedFrom::import_from(self.db, self.file, ast, import_kind) - } + AstImport::ImportFrom(ast) => ImportedFrom::import_from( + self.db, + ImportingFile::File( + self.file.file(self.db), + self.file.resolver_environment(self.db), + ), + ast, + import_kind, + ), }) else { tracing::debug!( "Dropping imported symbol {name} since its module name could not be discovered", @@ -1073,6 +1085,10 @@ impl<'db> SymbolVisitor<'db> { .iter() .find(|alias| &alias.name == "*") .map(Ranged::range); + let importing_file = ImportingFile::File( + self.file.file(self.db), + self.file.resolver_environment(self.db), + ); self.symbols .extend(symbols.symbols.iter().filter_map(|symbol| { // If there's no `__all__`, then names with an underscore @@ -1088,7 +1104,7 @@ impl<'db> SymbolVisitor<'db> { } let Some(imported_from) = ImportedFrom::import_from( self.db, - self.file, + importing_file, import_from, ImportKind::Wildcard, ) else { @@ -1133,12 +1149,16 @@ impl<'db> SymbolVisitor<'db> { &self, import_from: &ast::StmtImportFrom, ) -> Option<&'db FlatSymbols> { + let importing_file = ImportingFile::File( + self.file.file(self.db), + self.file.resolver_environment(self.db), + ); let module_name = - ModuleName::from_import_statement(self.db, self.file, import_from).ok()?; - let module = resolve_module(self.db, self.file, &module_name)?; + ModuleName::from_import_statement(self.db, importing_file, import_from).ok()?; + let module = resolve_module(self.db, importing_file, &module_name)?; Some(symbols_for_file_global_only( self.db, - module.python_file(self.db)?, + ProgramFile::new(self.db, module.file(self.db)?, self.file.program(self.db)), )) } @@ -1599,12 +1619,12 @@ mod tests { use insta::internals::SettingsBindDropGuard; use ruff_db::Db; - use ruff_db::PythonFile; use ruff_db::files::{FileRootKind, system_path_to_file}; use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf}; use ruff_python_ast::PythonVersion; use ruff_python_trivia::textwrap::dedent; use ty_project::{ProjectMetadata, TestDb}; + use ty_python_core::ProgramFile; use super::symbols_for_file_global_only; @@ -3165,7 +3185,11 @@ class C: ... let file = system_path_to_file(&self.db, path.as_ref()).unwrap(); symbols_for_file_global_only( &self.db, - PythonFile::new(&self.db, file, self.db.python_version()), + ProgramFile::new( + &self.db, + file, + self.db.program_environment().program(&self.db), + ), ) } diff --git a/crates/ty_ide/src/type_hierarchy.rs b/crates/ty_ide/src/type_hierarchy.rs index d02da06ecb..ad66669f1e 100644 --- a/crates/ty_ide/src/type_hierarchy.rs +++ b/crates/ty_ide/src/type_hierarchy.rs @@ -1,12 +1,12 @@ use crate::Db; use crate::goto::find_goto_target; use rayon::prelude::*; -use ruff_db::PythonFile; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; use ruff_text_size::{TextRange, TextSize}; use ty_project::parallel::ParallelIteratorExt; +use ty_python_core::ProgramFile; use ty_python_semantic::TypeHierarchyClass; use ty_python_semantic::types::Type; use ty_python_semantic::{ProgramEnvironment, SemanticModel}; @@ -31,10 +31,10 @@ pub struct TypeHierarchyItem { /// Returns `None` if the position is not on a class definition or class reference. pub fn prepare_type_hierarchy( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, offset: TextSize, ) -> Option { - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; let ty = goto_target.inferred_type(&model)?; @@ -47,7 +47,7 @@ pub fn prepare_type_hierarchy( /// Get the supertypes (base classes) of a type hierarchy item. pub fn type_hierarchy_supertypes( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, offset: TextSize, ) -> Vec { let Some(ty) = resolve_type_at(db, file, offset) else { @@ -65,14 +65,14 @@ pub fn type_hierarchy_supertypes( /// This scans all available modules and can be expensive in large projects. pub fn type_hierarchy_subtypes( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, offset: TextSize, ) -> Vec { let Some(ty) = resolve_type_at(db, file, offset) else { return vec![]; }; - ty_module_resolver::all_modules(db, file.python_version(db)) + ty_module_resolver::all_modules(db, file.resolver_environment(db)) .into_par_iter() .map_with_db(db, |db, module| { let env = ProgramEnvironment::from_file(file); @@ -91,10 +91,10 @@ pub fn type_hierarchy_subtypes( /// not be inferred, `None` is returned. fn resolve_type_at<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, offset: TextSize, ) -> Option> { - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let model = SemanticModel::new(db, file); let goto_target = find_goto_target(&model, &module, offset)?; @@ -733,7 +733,7 @@ Public = _Internal fn prepare(&self) -> Option { prepare_type_hierarchy( &self.db, - self.python_file(self.cursor.file), + self.program_file(self.cursor.file), self.cursor.offset, ) } @@ -744,7 +744,7 @@ Public = _Internal }; type_hierarchy_supertypes( &self.db, - self.python_file(item.file), + self.program_file(item.file), item.selection_range.start(), ) } @@ -755,7 +755,7 @@ Public = _Internal }; type_hierarchy_subtypes( &self.db, - self.python_file(item.file), + self.program_file(item.file), item.selection_range.start(), ) } diff --git a/crates/ty_ide/src/workspace_symbols.rs b/crates/ty_ide/src/workspace_symbols.rs index 7482b10c47..f882fc6adf 100644 --- a/crates/ty_ide/src/workspace_symbols.rs +++ b/crates/ty_ide/src/workspace_symbols.rs @@ -1,6 +1,5 @@ use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file}; use rayon::prelude::*; -use ruff_db::PythonFile; use ruff_db::files::File; use ty_project::{Db, parallel::ParallelIteratorExt}; @@ -31,7 +30,7 @@ pub fn workspace_symbols(db: &dyn Db, query: &str) -> Vec { ); let _entered = symbols_for_file_span.entered(); - symbols_for_file(db, PythonFile::new(db, file, db.python_version())) + symbols_for_file(db, db.program_file(file)) .search(&query) .map(|(_, symbol)| WorkspaceSymbolInfo { symbol: symbol.to_owned(), diff --git a/crates/ty_module_resolver/Cargo.toml b/crates/ty_module_resolver/Cargo.toml index 973b7f8f40..e3eb41801b 100644 --- a/crates/ty_module_resolver/Cargo.toml +++ b/crates/ty_module_resolver/Cargo.toml @@ -20,6 +20,7 @@ anyhow = { workspace = true } camino = { workspace = true } compact_str = { workspace = true } get-size2 = { workspace = true } +ordermap = { workspace = true } regex = { workspace = true } regex-syntax = { workspace = true } rustc-hash = { workspace = true } diff --git a/crates/ty_module_resolver/src/db.rs b/crates/ty_module_resolver/src/db.rs index 9e6032b698..13950cf15c 100644 --- a/crates/ty_module_resolver/src/db.rs +++ b/crates/ty_module_resolver/src/db.rs @@ -1,12 +1,7 @@ use ruff_db::Db as SourceDb; -use crate::resolve::SearchPaths; - #[salsa::db] -pub trait Db: SourceDb { - /// Returns the search paths for module resolution. - fn search_paths(&self) -> &SearchPaths; -} +pub trait Db: SourceDb {} #[cfg(test)] pub(crate) mod tests { @@ -19,7 +14,7 @@ pub(crate) mod tests { use ruff_python_ast::PythonVersion; use super::Db; - use crate::resolve::SearchPaths; + use crate::{ResolverEnvironment, resolve::SearchPaths}; type Events = Arc>>; @@ -66,15 +61,19 @@ pub(crate) mod tests { self } - pub(crate) fn python_version(&self) -> PythonVersion { - self.python_version - } - pub(crate) fn set_search_paths(&mut self, search_paths: SearchPaths) { search_paths.try_register_static_roots(self); self.search_paths = Arc::new(search_paths); } + pub(crate) fn search_paths(&self) -> &SearchPaths { + &self.search_paths + } + + pub(crate) fn resolver_environment(&self) -> ResolverEnvironment<'_> { + ResolverEnvironment::new(self, self.python_version, self.search_paths.as_ref()) + } + /// Takes the salsa events. pub(crate) fn take_salsa_events(&mut self) -> Vec { let mut events = self.events.lock().unwrap(); @@ -113,11 +112,7 @@ pub(crate) mod tests { } #[salsa::db] - impl Db for TestDb { - fn search_paths(&self) -> &SearchPaths { - &self.search_paths - } - } + impl Db for TestDb {} #[salsa::db] impl salsa::Database for TestDb {} diff --git a/crates/ty_module_resolver/src/environment.rs b/crates/ty_module_resolver/src/environment.rs new file mode 100644 index 0000000000..478a854672 --- /dev/null +++ b/crates/ty_module_resolver/src/environment.rs @@ -0,0 +1,100 @@ +use std::fmt; + +use ruff_db::files::File; +use ruff_python_ast::PythonVersion; + +use crate::{Db, ModuleResolveMode, SearchPaths, search_paths}; + +/// The Python version and search paths used to resolve modules. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +pub struct ResolverEnvironment<'db> { + #[returns(copy)] + pub python_version: PythonVersion, + + #[returns(ref)] + pub search_paths: SearchPaths, +} + +impl get_size2::GetSize for ResolverEnvironment<'_> {} + +impl<'db> ResolverEnvironment<'db> { + pub fn display_search_paths( + self, + db: &'db dyn Db, + mode: ModuleResolveMode, + ) -> DisplaySearchPaths<'db> { + DisplaySearchPaths { + db, + resolver_environment: self, + mode, + } + } +} + +pub struct DisplaySearchPaths<'db> { + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, + mode: ModuleResolveMode, +} + +impl fmt::Display for DisplaySearchPaths<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut paths = search_paths(self.db, self.resolver_environment, self.mode).peekable(); + + if paths.peek().is_none() { + return f.write_str("[]"); + } + + writeln!(f, "[")?; + for path in paths { + writeln!(f, " {path},")?; + } + f.write_str("]") + } +} + +/// A file interpreted within a particular module-resolution environment. +/// +/// The same file can resolve imports differently depending on the Python version and search paths +/// used to interpret it. +/// +/// For example, consider a file containing: +/// +/// ```python +/// from zipfile._path import Path +/// ``` +/// +/// Typeshed makes `zipfile._path` available only on Python 3.12 and newer: +/// +/// ```text +/// resolve_module(ResolverFile(shared.py, Python 3.11), "zipfile._path") +/// -> unresolved +/// +/// resolve_module(ResolverFile(shared.py, Python 3.12), "zipfile._path") +/// -> zipfile/_path/__init__.pyi +/// ``` +/// +/// Search paths can also change which file an import resolves to, even when the Python version is +/// identical: +/// +/// ```text +/// resolve_module(ResolverFile(shared.py, project environment), "dependency") +/// -> .venv/lib/dependency.py +/// +/// resolve_module(ResolverFile(shared.py, script environment), "dependency") +/// -> .script-venv/lib/dependency.py +/// ``` +/// +/// Including the resolver environment in the file's identity keeps these resolution results +/// separate. Projects and scripts with equivalent resolver environments can still share resolution +/// results. +#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)] +pub struct ResolverFile<'db> { + #[returns(copy)] + pub file: File, + + #[returns(copy)] + pub environment: ResolverEnvironment<'db>, +} + +impl get_size2::GetSize for ResolverFile<'_> {} diff --git a/crates/ty_module_resolver/src/lib.rs b/crates/ty_module_resolver/src/lib.rs index fb4039878c..3bbf865101 100644 --- a/crates/ty_module_resolver/src/lib.rs +++ b/crates/ty_module_resolver/src/lib.rs @@ -1,11 +1,14 @@ +use std::hash::BuildHasherDefault; use std::iter::FusedIterator; use ruff_db::system::SystemPath; +use rustc_hash::FxHasher; pub use db::Db; +pub use environment::{ResolverEnvironment, ResolverFile}; pub use module::KnownModule; pub use module::Module; -pub use module_name::{ModuleName, ModuleNameResolutionError}; +pub use module_name::{ImportingFile, ModuleName, ModuleNameResolutionError}; pub use path::{SearchPath, SearchPathError}; pub use resolve::{ SearchPaths, file_to_module, resolve_module, resolve_module_confident, resolve_real_module, @@ -20,6 +23,7 @@ pub use module_glob::{ModuleGlobError, ModuleGlobSet, ModuleGlobSetBuilder, Modu pub use resolve::{ModuleResolveMode, SearchPathIterator, search_paths}; mod db; +mod environment; mod list; mod module; mod module_glob; @@ -30,15 +34,20 @@ mod settings; mod strategy; mod typeshed; +type FxOrderMap = ordermap::map::OrderMap>; + #[cfg(test)] mod testing; /// Returns an iterator over all search paths pointing to a system path -pub fn system_module_search_paths(db: &dyn Db) -> SystemModuleSearchPathsIter<'_> { +pub fn system_module_search_paths<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, +) -> SystemModuleSearchPathsIter<'db> { SystemModuleSearchPathsIter { // Always run in `Typing` mode because we want to include as much as possible // and we don't care about the "real" stdlib - inner: search_paths(db, ModuleResolveMode::Typing), + inner: search_paths(db, resolver_environment, ModuleResolveMode::Typing), } } diff --git a/crates/ty_module_resolver/src/list.rs b/crates/ty_module_resolver/src/list.rs index efdf581717..b8080af1e1 100644 --- a/crates/ty_module_resolver/src/list.rs +++ b/crates/ty_module_resolver/src/list.rs @@ -1,10 +1,9 @@ use std::borrow::Cow; use std::collections::btree_map::{BTreeMap, Entry}; -use ruff_db::PythonFile; use ruff_db::files::directory_listing; -use ruff_python_ast::PythonVersion; +use crate::ResolverEnvironment; use crate::db::Db; use crate::module::{Module, ModuleKind}; use crate::module_name::ModuleName; @@ -12,8 +11,11 @@ use crate::path::{ModulePath, SearchPath, SystemOrVendoredPathRef}; use crate::resolve::{ModuleResolveMode, ResolverContext, resolve_file_module, search_paths}; /// List all available modules, including all sub-modules, sorted in lexicographic order. -pub fn all_modules(db: &dyn Db, python_version: PythonVersion) -> Vec> { - let mut modules = list_modules(db, python_version).to_vec(); +pub fn all_modules<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, +) -> Vec> { + let mut modules = list_modules(db, resolver_environment).to_vec(); let mut stack = modules.clone(); while let Some(module) = stack.pop() { for &submodule in module.all_submodules(db) { @@ -26,27 +28,23 @@ pub fn all_modules(db: &dyn Db, python_version: PythonVersion) -> Vec } /// List all available top-level modules. -pub fn list_modules(db: &dyn Db, python_version: PythonVersion) -> &[Module<'_>] { - list_modules_impl(db, PythonVersionIngredient::new(db, python_version)) -} - -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -struct PythonVersionIngredient<'db> { - #[returns(copy)] - python_version: PythonVersion, +pub fn list_modules<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, +) -> &'db [Module<'db>] { + list_modules_impl(db, resolver_environment) } #[salsa::tracked(returns(deref))] fn list_modules_impl<'db>( db: &'db dyn Db, - version: PythonVersionIngredient<'db>, + resolver_environment: ResolverEnvironment<'db>, ) -> Box<[Module<'db>]> { - let python_version = version.python_version(db); let mut modules: BTreeMap<&ModuleName, ListedModule<'_>> = BTreeMap::new(); - for search_path in search_paths(db, ModuleResolveMode::Typing) { + for search_path in search_paths(db, resolver_environment, ModuleResolveMode::Typing) { for &new in list_modules_in( db, - SearchPathIngredient::new(db, search_path.clone(), python_version), + SearchPathIngredient::new(db, resolver_environment, search_path.clone()), ) { match modules.entry(new.module(db).name(db)) { Entry::Vacant(entry) => { @@ -83,10 +81,10 @@ fn list_modules_impl<'db>( #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] struct SearchPathIngredient<'db> { + #[returns(copy)] + resolver_environment: ResolverEnvironment<'db>, #[returns(ref)] path: SearchPath, - #[returns(copy)] - python_version: PythonVersion, } /// List all available top-level modules in the given `SearchPath`. @@ -97,7 +95,7 @@ fn list_modules_in<'db>( ) -> Vec> { let path = search_path.path(db); tracing::debug!("Listing modules in search path '{}'", path); - let mut lister = Lister::new(db, path, search_path.python_version(db)); + let mut lister = Lister::new(db, search_path.resolver_environment(db), path); match path.as_path() { SystemOrVendoredPathRef::System(system_search_path) => { let Ok(listing) = directory_listing(db, system_search_path) else { @@ -138,7 +136,7 @@ impl get_size2::GetSize for ListedModule<'_> {} struct Lister<'db> { db: &'db dyn Db, search_path: &'db SearchPath, - python_version: PythonVersion, + resolver_environment: ResolverEnvironment<'db>, modules: BTreeMap<&'db ModuleName, ListedModule<'db>>, } @@ -147,13 +145,13 @@ impl<'db> Lister<'db> { /// of file paths. fn new( db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, search_path: &'db SearchPath, - python_version: PythonVersion, ) -> Lister<'db> { Lister { db, search_path, - python_version, + resolver_environment, modules: BTreeMap::new(), } } @@ -205,10 +203,11 @@ impl<'db> Lister<'db> { &module_path, Module::file_module( self.db, + file, + self.resolver_environment, Cow::Owned(module_name), ModuleKind::Package, self.search_path.clone(), - PythonFile::new(self.db, file, self.python_version), ), ); return; @@ -251,8 +250,8 @@ impl<'db> Lister<'db> { &module_path, Module::namespace_package( self.db, + self.resolver_environment, Cow::Owned(module_name), - self.python_version, ), ); } @@ -281,10 +280,11 @@ impl<'db> Lister<'db> { &module_path, Module::file_module( self.db, + file, + self.resolver_environment, Cow::Owned(module_name), ModuleKind::Module, self.search_path.clone(), - PythonFile::new(self.db, file, self.python_version), ), ); } @@ -347,14 +347,17 @@ impl<'db> Lister<'db> { /// Returns true if the given module name cannot be shadowable. fn is_non_shadowable(&self, name: &ModuleName) -> bool { - ModuleResolveMode::Typing.is_non_shadowable(self.python_version.minor, name.as_str()) + ModuleResolveMode::Typing.is_non_shadowable( + self.resolver_environment.python_version(self.db).minor, + name.as_str(), + ) } /// Constructs a resolver context for use with some APIs that require it. fn context(&self) -> ResolverContext<'db> { ResolverContext { db: self.db, - python_version: self.python_version, + resolver_environment: self.resolver_environment, // We don't currently support listing modules // in a "no stubs allowed" mode. mode: ModuleResolveMode::Typing, @@ -432,7 +435,7 @@ mod tests { use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder}; fn list_modules(db: &TestDb) -> &[Module<'_>] { - super::list_modules(db, db.python_version()) + super::list_modules(db, db.resolver_environment()) } struct ModuleDebugSnapshot<'db> { @@ -449,14 +452,11 @@ mod tests { Module::File(module) => { // For snapshots, just normalize all paths to using // Unix slashes for simplicity. - let path_components = - match module.python_file(self.db).file(self.db).path(self.db) { - FilePath::System(path) => path.components(), - FilePath::Vendored(path) => path.components(), - FilePath::SystemVirtual(path) => { - Utf8Path::new(path.as_str()).components() - } - }; + let path_components = match module.file(self.db).path(self.db) { + FilePath::System(path) => path.components(), + FilePath::Vendored(path) => path.components(), + FilePath::SystemVirtual(path) => Utf8Path::new(path.as_str()).components(), + }; let nice_path = path_components // Avoid including a root component, since that // results in a platform dependent separator. @@ -1465,7 +1465,11 @@ not_a_directory assert_function_query_was_not_run( &db, dynamic_resolution_paths, - ModuleResolveModeIngredient::new(&db, ModuleResolveMode::Typing), + ModuleResolveModeIngredient::new( + &db, + db.resolver_environment(), + ModuleResolveMode::Typing, + ), &events, ); } diff --git a/crates/ty_module_resolver/src/module.rs b/crates/ty_module_resolver/src/module.rs index b85db3330a..0820bc6b07 100644 --- a/crates/ty_module_resolver/src/module.rs +++ b/crates/ty_module_resolver/src/module.rs @@ -2,7 +2,6 @@ use std::borrow::Cow; use std::fmt::Formatter; use std::str::FromStr; -use ruff_db::PythonFile; use ruff_db::files::{File, directory_listing, system_path_to_file, vendored_path_to_file}; use ruff_db::system::SystemPath; use ruff_db::vendored::VendoredPath; @@ -10,9 +9,9 @@ use ruff_python_ast::PythonVersion; use salsa::Database; use salsa::plumbing::AsId; -use crate::Db; use crate::module_name::ModuleName; use crate::path::{SearchPath, SystemOrVendoredPathRef}; +use crate::{Db, ResolverEnvironment}; /// Representation of a Python module. #[derive(Clone, Copy, Eq, Hash, PartialEq, salsa::Supertype, salsa::SalsaValue)] @@ -28,22 +27,39 @@ impl get_size2::GetSize for Module<'_> {} impl<'db> Module<'db> { pub(crate) fn file_module( db: &'db dyn Db, + file: File, + resolver_environment: ResolverEnvironment<'db>, name: Cow<'_, ModuleName>, kind: ModuleKind, search_path: SearchPath, - file: PythonFile<'db>, ) -> Self { let known = KnownModule::try_from_search_path_and_name(&search_path, &name); - Self::File(FileModule::new(db, name, kind, search_path, file, known)) + Self::File(FileModule::new( + db, + name, + kind, + search_path, + file, + resolver_environment, + known, + )) } pub(crate) fn namespace_package( db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, name: Cow<'_, ModuleName>, - python_version: PythonVersion, ) -> Self { - Self::Namespace(NamespacePackage::new(db, name, python_version)) + Self::Namespace(NamespacePackage::new(db, resolver_environment, name)) + } + + /// The resolver environment used to resolve this module. + pub fn resolver_environment(self, db: &'db dyn Database) -> ResolverEnvironment<'db> { + match self { + Module::File(module) => module.resolver_environment(db), + Module::Namespace(module) => module.resolver_environment(db), + } } /// The absolute name of the module (e.g. `foo.bar`) @@ -59,27 +75,14 @@ impl<'db> Module<'db> { /// This is `None` for namespace packages. pub fn file(self, db: &'db dyn Database) -> Option { match self { - Module::File(module) => Some(module.python_file(db).file(db)), - Module::Namespace(_) => None, - } - } - - /// The versioned file used to parse this module. - /// - /// This is `None` for namespace packages. - pub fn python_file(self, db: &'db dyn Database) -> Option> { - match self { - Module::File(module) => Some(module.python_file(db)), + Module::File(module) => Some(module.file(db)), Module::Namespace(_) => None, } } /// The Python version used to resolve this module. pub fn python_version(self, db: &'db dyn Database) -> PythonVersion { - match self { - Module::File(module) => module.python_file(db).python_version(db), - Module::Namespace(module) => module.python_version(db), - } + self.resolver_environment(db).python_version(db) } /// Is this a module that we special-case somehow? If so, which one? @@ -187,14 +190,14 @@ fn all_submodule_names_for_package<'db>( return None; } - let python_file = module.python_file(db); - let path = SystemOrVendoredPathRef::try_from_file(db, python_file.file(db))?; + let path = SystemOrVendoredPathRef::try_from_file(db, module.file(db))?; debug_assert!( matches!(path.file_name(), Some("__init__.py" | "__init__.pyi")), "expected package file `{:?}` to be `__init__.py` or `__init__.pyi`", path.file_name(), ); + let resolver_environment = module.resolver_environment(db); Some(match path.parent()? { SystemOrVendoredPathRef::System(parent_directory) => { directory_listing(db, parent_directory) @@ -230,10 +233,11 @@ fn all_submodule_names_for_package<'db>( }; Some(Module::file_module( db, + file, + resolver_environment, Cow::Owned(name), kind, module.search_path(db).clone(), - PythonFile::new(db, file, python_file.python_version(db)), )) }) .collect() @@ -267,10 +271,11 @@ fn all_submodule_names_for_package<'db>( }; Some(Module::file_module( db, + file, + resolver_environment, Cow::Owned(name), kind, module.search_path(db).clone(), - PythonFile::new(db, file, python_file.python_version(db)), )) }) .collect(), @@ -287,7 +292,9 @@ pub struct FileModule<'db> { #[returns(ref)] pub(super) search_path: SearchPath, #[returns(copy)] - pub(super) python_file: PythonFile<'db>, + pub(super) file: File, + #[returns(copy)] + pub(super) resolver_environment: ResolverEnvironment<'db>, #[returns(copy)] pub(super) known: Option, } @@ -298,10 +305,10 @@ pub struct FileModule<'db> { /// multiple possible paths and they have no corresponding code file. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct NamespacePackage<'db> { + #[returns(copy)] + pub(super) resolver_environment: ResolverEnvironment<'db>, #[returns(ref)] pub(super) name: ModuleName, - #[returns(copy)] - pub(super) python_version: PythonVersion, } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] diff --git a/crates/ty_module_resolver/src/module_name.rs b/crates/ty_module_resolver/src/module_name.rs index 95975e1b06..709b51e436 100644 --- a/crates/ty_module_resolver/src/module_name.rs +++ b/crates/ty_module_resolver/src/module_name.rs @@ -4,12 +4,13 @@ use std::ops::Deref; use compact_str::{CompactString, ToCompactString}; -use ruff_db::PythonFile; -use ruff_python_ast as ast; +use ruff_db::files::File; +use ruff_python_ast::{self as ast, PythonVersion}; use ruff_python_stdlib::identifiers::is_identifier; use crate::db::Db; use crate::resolve::file_to_module; +use crate::{ResolverEnvironment, ResolverFile}; /// A module name, e.g. `foo.bar`. /// @@ -305,13 +306,12 @@ impl ModuleName { /// Extracts a module name from the AST of a `from import ...` /// statement. /// - /// `importing_file` must be the [`PythonFile`] that contains the import - /// statement. + /// `importing_file` must be the file that contains the import statement. /// /// This handles relative import statements. pub fn from_import_statement<'db>( db: &'db dyn Db, - importing_file: PythonFile<'db>, + importing_file: ImportingFile<'db>, node: &'db ast::StmtImportFrom, ) -> Result { let ast::StmtImportFrom { @@ -328,12 +328,12 @@ impl ModuleName { /// Computes the absolute module name from the LHS components of `from LHS import RHS` pub fn from_identifier_parts<'db>( db: &'db dyn Db, - importing_file: PythonFile<'db>, + importing_file: ImportingFile<'db>, module: Option<&str>, level: u32, ) -> Result { if let Some(level) = NonZeroU32::new(level) { - relative_module_name(db, importing_file, module, level) + relative_module_name(db, importing_file.resolver_file(db), module, level) } else { module .and_then(Self::new) @@ -346,7 +346,7 @@ impl ModuleName { /// i.e. this resolves `.` pub fn package_for_file<'db>( db: &'db dyn Db, - importing_file: PythonFile<'db>, + importing_file: ImportingFile<'db>, ) -> Result { Self::from_identifier_parts(db, importing_file, None, 1) } @@ -468,6 +468,67 @@ impl std::fmt::Display for ModuleName { } } +/// The file from which an import is resolved. +/// +/// Most absolute imports only need the resolver environment. Creating a [`ResolverFile`] for each +/// such import would unnecessarily intern the file and environment together, even though that +/// combined identity is never used: +/// +/// ```text +/// resolve_module(ImportingFile::File(shared.py, environment), "dependency") +/// -> resolve using environment; no ResolverFile needed +/// ``` +/// +/// Relative imports, on the other hand, need the importing file's module identity and therefore +/// require a [`ResolverFile`]: +/// +/// ```text +/// from .dependency import value +/// -> importing_file.resolver_file(db) +/// -> ResolverFile(shared.py, environment) +/// ``` +/// +/// [`ImportingFile::File`] defers interning until such a code path actually calls +/// [`ImportingFile::resolver_file`]. Callers that already have an interned resolver file can pass +/// [`ImportingFile::ResolverFile`] to reuse it directly. +#[derive(Clone, Copy)] +pub enum ImportingFile<'db> { + /// An already-interned resolver key that can be reused without materialization. + ResolverFile(ResolverFile<'db>), + /// An importing file and resolver environment whose combined key is materialized lazily. + File(File, ResolverEnvironment<'db>), +} + +impl<'db> ImportingFile<'db> { + pub fn file(self, db: &dyn Db) -> File { + match self { + Self::ResolverFile(file) => file.file(db), + Self::File(file, _) => file, + } + } + + pub fn resolver_environment(self, db: &'db dyn Db) -> ResolverEnvironment<'db> { + match self { + Self::ResolverFile(file) => file.environment(db), + Self::File(_, resolver_environment) => resolver_environment, + } + } + + pub fn python_version(self, db: &'db dyn Db) -> PythonVersion { + self.resolver_environment(db).python_version(db) + } + + /// Returns the existing resolver key or materializes one when required. + pub fn resolver_file(self, db: &'db dyn Db) -> ResolverFile<'db> { + match self { + Self::ResolverFile(file) => file, + Self::File(file, resolver_environment) => { + ResolverFile::new(db, file, resolver_environment) + } + } + } +} + /// Given a `from .foo import bar` relative import, resolve the relative module /// we're importing `bar` from into an absolute [`ModuleName`] /// using the name of the module we're currently analyzing. @@ -480,7 +541,7 @@ impl std::fmt::Display for ModuleName { /// - `from ..foo.bar import baz` => `tail == "foo.bar"` fn relative_module_name<'db>( db: &'db dyn Db, - importing_file: PythonFile<'db>, + importing_file: ResolverFile<'db>, tail: Option<&str>, level: NonZeroU32, ) -> Result { diff --git a/crates/ty_module_resolver/src/path.rs b/crates/ty_module_resolver/src/path.rs index b63bbdb025..a6fa6f897b 100644 --- a/crates/ty_module_resolver/src/path.rs +++ b/crates/ty_module_resolver/src/path.rs @@ -13,7 +13,7 @@ use ruff_db::vendored::{VendoredPath, VendoredPathBuf}; use crate::Db; use crate::module_name::ModuleName; use crate::resolve::{PyTyped, ResolverContext}; -use crate::typeshed::{TypeshedVersionsQueryResult, typeshed_versions}; +use crate::typeshed::TypeshedVersionsQueryResult; /// A path that points to a Python module. /// @@ -428,13 +428,14 @@ fn query_stdlib_version( let Some(module_name) = stdlib_path_to_module_name(relative_path) else { return TypeshedVersionsQueryResult::DoesNotExist; }; - let ResolverContext { - db, - python_version, - mode: _, - } = context; - - typeshed_versions(*db).query_module(&module_name, *python_version) + context + .resolver_environment + .search_paths(context.db) + .typeshed_versions() + .query_module( + &module_name, + context.resolver_environment.python_version(context.db), + ) } #[derive(Debug, thiserror::Error)] @@ -894,6 +895,7 @@ mod tests { use ruff_db::Db; use ruff_python_ast::PythonVersion; + use crate::ResolverEnvironment; use crate::db::tests::TestDb; use crate::resolve::ModuleResolveMode; use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder}; @@ -1160,7 +1162,11 @@ mod tests { }; let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY38, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()), + ModuleResolveMode::Typing, + ); let asyncio_regular_package = stdlib_path.join("asyncio"); assert!(asyncio_regular_package.is_directory(&resolver)); @@ -1190,7 +1196,11 @@ mod tests { }; let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY38, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()), + ModuleResolveMode::Typing, + ); let xml_namespace_package = stdlib_path.join("xml"); assert!(xml_namespace_package.is_directory(&resolver)); @@ -1212,7 +1222,11 @@ mod tests { }; let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY38, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()), + ModuleResolveMode::Typing, + ); let functools_module = stdlib_path.join("functools.pyi"); assert!(functools_module.to_file(&resolver).is_some()); @@ -1228,7 +1242,11 @@ mod tests { }; let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY38, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()), + ModuleResolveMode::Typing, + ); let collections_regular_package = stdlib_path.join("collections"); assert_eq!(collections_regular_package.to_file(&resolver), None); @@ -1244,7 +1262,11 @@ mod tests { }; let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY38, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()), + ModuleResolveMode::Typing, + ); let importlib_namespace_package = stdlib_path.join("importlib"); assert_eq!(importlib_namespace_package.to_file(&resolver), None); @@ -1265,7 +1287,11 @@ mod tests { }; let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY38, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()), + ModuleResolveMode::Typing, + ); let non_existent = stdlib_path.join("doesnt_even_exist"); assert_eq!(non_existent.to_file(&resolver), None); @@ -1293,7 +1319,11 @@ mod tests { }; let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY39, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY39, db.search_paths()), + ModuleResolveMode::Typing, + ); // Since we've set the target version to Py39, // `collections` should now exist as a directory, according to VERSIONS... @@ -1324,7 +1354,11 @@ mod tests { }; let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY39, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY39, db.search_paths()), + ModuleResolveMode::Typing, + ); // The `importlib` directory now also exists let importlib_namespace_package = stdlib_path.join("importlib"); @@ -1348,7 +1382,11 @@ mod tests { }; let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED); - let resolver = ResolverContext::new(&db, PythonVersion::PY39, ModuleResolveMode::Typing); + let resolver = ResolverContext::new( + &db, + ResolverEnvironment::new(&db, PythonVersion::PY39, db.search_paths()), + ModuleResolveMode::Typing, + ); // The `xml` package no longer exists on py39: let xml_namespace_package = stdlib_path.join("xml"); diff --git a/crates/ty_module_resolver/src/resolve.rs b/crates/ty_module_resolver/src/resolve.rs index 5fd7b5f313..fef29c17f5 100644 --- a/crates/ty_module_resolver/src/resolve.rs +++ b/crates/ty_module_resolver/src/resolve.rs @@ -32,7 +32,6 @@ specifies ty's implementation of Python's import resolution algorithm. */ use std::borrow::Cow; -use std::fmt; use std::iter::FusedIterator; use rustc_hash::{FxBuildHasher, FxHashSet}; @@ -43,29 +42,30 @@ use ruff_db::source::source_text; use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::{ - self as ast, PySourceType, PythonVersion, + self as ast, PySourceType, visitor::{Visitor, walk_body}, }; use crate::db::Db; use crate::module::{Module, ModuleKind}; -use crate::module_name::ModuleName; +use crate::module_name::{ImportingFile, ModuleName}; use crate::path::{ModulePath, SearchPath, SystemOrVendoredPathRef}; use crate::strategy::MisconfigurationStrategy; use crate::typeshed::{TypeshedVersions, vendored_typeshed_versions}; -use crate::{SearchPathSettings, SearchPathSettingsError}; +use crate::{ResolverEnvironment, ResolverFile, SearchPathSettings, SearchPathSettingsError}; /// Resolves a module name to a module. pub fn resolve_module<'db>( db: &'db dyn Db, - importing_file: PythonFile<'db>, + importing_file: ImportingFile<'db>, module_name: &ModuleName, ) -> Option> { + let resolver_environment = importing_file.resolver_environment(db); let interned_name = ModuleNameIngredient::new( db, module_name, ModuleResolveMode::Typing, - importing_file.python_version(db), + resolver_environment, ); resolve_module_query(db, interned_name) @@ -78,11 +78,15 @@ pub fn resolve_module<'db>( /// we don't have a well-defined importing file. pub fn resolve_module_confident<'db>( db: &'db dyn Db, - python_version: PythonVersion, + resolver_environment: ResolverEnvironment<'db>, module_name: &ModuleName, ) -> Option> { - let interned_name = - ModuleNameIngredient::new(db, module_name, ModuleResolveMode::Typing, python_version); + let interned_name = ModuleNameIngredient::new( + db, + module_name, + ModuleResolveMode::Typing, + resolver_environment, + ); resolve_module_query(db, interned_name) } @@ -90,14 +94,15 @@ pub fn resolve_module_confident<'db>( /// Resolves a module name to a module (stubs not allowed). pub fn resolve_real_module<'db>( db: &'db dyn Db, - importing_file: PythonFile<'db>, + importing_file: ImportingFile<'db>, module_name: &ModuleName, ) -> Option> { + let resolver_environment = importing_file.resolver_environment(db); let interned_name = ModuleNameIngredient::new( db, module_name, ModuleResolveMode::Runtime, - importing_file.python_version(db), + resolver_environment, ); resolve_module_query(db, interned_name) @@ -110,11 +115,15 @@ pub fn resolve_real_module<'db>( /// we don't have a well-defined importing file. pub fn resolve_real_module_confident<'db>( db: &'db dyn Db, - python_version: PythonVersion, + resolver_environment: ResolverEnvironment<'db>, module_name: &ModuleName, ) -> Option> { - let interned_name = - ModuleNameIngredient::new(db, module_name, ModuleResolveMode::Runtime, python_version); + let interned_name = ModuleNameIngredient::new( + db, + module_name, + ModuleResolveMode::Runtime, + resolver_environment, + ); resolve_module_query(db, interned_name) } @@ -132,14 +141,15 @@ pub fn resolve_real_module_confident<'db>( /// are involved in an import cycle with `builtins`. pub fn resolve_real_shadowable_module<'db>( db: &'db dyn Db, - importing_file: PythonFile<'db>, + importing_file: ImportingFile<'db>, module_name: &ModuleName, ) -> Option> { + let resolver_environment = importing_file.resolver_environment(db); let interned_name = ModuleNameIngredient::new( db, module_name, ModuleResolveMode::RuntimeSomeShadowingAllowed, - importing_file.python_version(db), + resolver_environment, ); resolve_module_query(db, interned_name) @@ -174,6 +184,8 @@ pub enum ModuleResolveMode { #[salsa::interned(heap_size=ruff_memory_usage::heap_size)] #[derive(Debug)] pub(crate) struct ModuleResolveModeIngredient<'db> { + #[returns(copy)] + resolver_environment: ResolverEnvironment<'db>, #[returns(copy)] mode: ModuleResolveMode, } @@ -228,10 +240,10 @@ fn resolve_module_query<'db>( ) -> Option> { let name = module_name.name(db); let mode = module_name.mode(db); - let python_version = module_name.python_version(db); + let resolver_environment = module_name.resolver_environment(db); let _span = tracing::trace_span!("resolve_module", %name).entered(); - let Some(resolved) = resolve_name(db, name, mode, python_version) else { + let Some(resolved) = resolve_name(db, resolver_environment, name, mode) else { tracing::debug!("Module `{name}` not found in search paths"); return None; }; @@ -239,7 +251,7 @@ fn resolve_module_query<'db>( resolved .into_iter() .next() - .map(|candidate| candidate.into_module(db, name, python_version)) + .map(|candidate| candidate.into_module(db, resolver_environment, name)) } /// Like `resolve_module_query` but for cases where it failed to resolve the module @@ -262,10 +274,11 @@ fn desperately_resolve_module<'db>( ) -> Option> { let name = module_name.name(db); let mode = module_name.mode(db); - let python_version = module_name.python_version(db); + let resolver_environment = module_name.resolver_environment(db); let _span = tracing::trace_span!("desperately_resolve_module", %name).entered(); - let Some(resolved) = desperately_resolve_name(db, importing_file, name, mode, python_version) + let Some(resolved) = + desperately_resolve_name(db, importing_file, resolver_environment, name, mode) else { let mode = match mode { ModuleResolveMode::Typing => "typing mode", @@ -281,7 +294,7 @@ fn desperately_resolve_module<'db>( resolved .into_iter() .next() - .map(|candidate| candidate.into_module(db, name, python_version)) + .map(|candidate| candidate.into_module(db, resolver_environment, name)) } /// Resolves the module for the given path. @@ -290,8 +303,8 @@ fn desperately_resolve_module<'db>( #[allow(unused)] pub(crate) fn path_to_module<'db>( db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, path: &FilePath, - python_version: PythonVersion, ) -> Option> { // It's not entirely clear on first sight why this method calls `file_to_module` instead of // it being the other way round, considering that the first thing that `file_to_module` does @@ -302,7 +315,7 @@ pub(crate) fn path_to_module<'db>( // `VfsFile` is. So what we do here is to retrieve the `path`'s `VfsFile` so that we can make // use of Salsa's caching and invalidation. let file = path.to_file(db)?; - file_to_module(db, PythonFile::new(db, file, python_version)) + file_to_module(db, ResolverFile::new(db, file, resolver_environment)) } /// Resolves the module for the file with the given id. @@ -314,25 +327,35 @@ pub(crate) fn path_to_module<'db>( /// This intuition is particularly useful for understanding why it's correct that we pass /// the file itself as `importing_file` to various subroutines. #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] -pub fn file_to_module<'db>(db: &'db dyn Db, file: PythonFile<'db>) -> Option> { - let source_file = file.file(db); - let _span = tracing::trace_span!("file_to_module", file=?source_file).entered(); +pub fn file_to_module<'db>( + db: &'db dyn Db, + resolver_file: ResolverFile<'db>, +) -> Option> { + let resolver_environment = resolver_file.environment(db); + let file = resolver_file.file(db); + let _span = tracing::trace_span!("file_to_module", ?file).entered(); - let path = SystemOrVendoredPathRef::try_from_file(db, source_file)?; + let path = SystemOrVendoredPathRef::try_from_file(db, file)?; - file_to_module_impl(db, file, path, search_paths(db, ModuleResolveMode::Typing)).or_else(|| { + file_to_module_impl( + db, + resolver_file, + path, + search_paths(db, resolver_environment, ModuleResolveMode::Typing), + ) + .or_else(|| { file_to_module_impl( db, - file, + resolver_file, path, - relative_desperate_search_paths(db, source_file).iter(), + relative_desperate_search_paths(db, resolver_file).iter(), ) }) } fn file_to_module_impl<'db, 'a>( db: &'db dyn Db, - file: PythonFile<'db>, + resolver_file: ResolverFile<'db>, path: SystemOrVendoredPathRef<'a>, mut search_paths: impl Iterator, ) -> Option> { @@ -348,20 +371,21 @@ fn file_to_module_impl<'db, 'a>( // If it doesn't, then that means that multiple modules have the same name in different // root paths, but that the module corresponding to `path` is in a lower priority search path, // in which case we ignore it. - let module = resolve_module(db, file, &module_name)?; + let module = resolve_module(db, ImportingFile::ResolverFile(resolver_file), &module_name)?; let module_file = module.file(db)?; - let source_file = file.file(db); - let file_path = source_file.path(db); + let file: File = resolver_file.file(db); + let file_path = file.path(db); if file_path == module_file.path(db) { return Some(module); - } else if source_file.source_type(db) == PySourceType::Python + } else if file.source_type(db) == PySourceType::Python && module_file.source_type(db) == PySourceType::Stub { // If a .py and .pyi are both defined, the .pyi will be the one returned by `resolve_module().file`, // which would make us erroneously believe the `.py` is *not* also this module (breaking things // like relative imports). So here we try `resolve_real_module().file` to cover both cases. - let module = resolve_real_module(db, file, &module_name)?; + let module = + resolve_real_module(db, ImportingFile::ResolverFile(resolver_file), &module_name)?; let module_file = module.file(db)?; if file_path == module_file.path(db) { return Some(module); @@ -377,8 +401,20 @@ fn file_to_module_impl<'db, 'a>( None } -pub fn search_paths(db: &dyn Db, resolve_mode: ModuleResolveMode) -> SearchPathIterator<'_> { - db.search_paths().iter(db, resolve_mode) +pub fn search_paths<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, + resolve_mode: ModuleResolveMode, +) -> SearchPathIterator<'db> { + let search_paths = resolver_environment.search_paths(db); + + SearchPathIterator { + db, + static_paths: search_paths.static_paths.iter(), + stdlib_path: search_paths.stdlib(resolve_mode), + dynamic_paths: None, + mode: ModuleResolveModeIngredient::new(db, resolver_environment, resolve_mode), + } } #[derive(Debug, Clone, Copy, Default)] @@ -467,8 +503,14 @@ impl StubPackageIndex { /// Returns an index of search paths that may contain a top-level stub package, preserving their /// resolution order relative to stdlib. #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] -fn stub_package_index(db: &dyn Db) -> StubPackageIndex { - StubPackageIndex::from_search_paths(db, search_paths(db, ModuleResolveMode::Typing)) +fn stub_package_index( + db: &dyn Db, + resolver_environment: ResolverEnvironment<'_>, +) -> StubPackageIndex { + StubPackageIndex::from_search_paths( + db, + search_paths(db, resolver_environment, ModuleResolveMode::Typing), + ) } fn search_path_may_contain_stub_package(db: &dyn Db, search_path: &SearchPath) -> bool { @@ -490,21 +532,26 @@ fn search_path_may_contain_stub_package(db: &dyn Db, search_path: &SearchPath) - /// /// We exclude `__init__.py(i)` dirs to avoid truncating packages. #[salsa::tracked(returns(as_deref), heap_size=ruff_memory_usage::heap_size)] -fn absolute_desperate_search_paths(db: &dyn Db, importing_file: File) -> Option> { +fn absolute_desperate_search_paths( + db: &dyn Db, + importing_file: ResolverFile<'_>, +) -> Option> { + let resolver_environment = importing_file.environment(db); + let importing_file = importing_file.file(db); let system = db.system(); let importing_path = importing_file.path(db).as_system_path()?; // Only allow this if the importing_file is under the first-party search path - let (base_path, rel_path) = - search_paths(db, ModuleResolveMode::Typing).find_map(|search_path| { - if !search_path.is_first_party() { - return None; - } - Some(( - search_path.as_system_path()?, - search_path.relativize_system_path_only(importing_path)?, - )) - })?; + let (base_path, rel_path) = search_paths(db, resolver_environment, ModuleResolveMode::Typing) + .find_map(|search_path| { + if !search_path.is_first_party() { + return None; + } + Some(( + search_path.as_system_path()?, + search_path.relativize_system_path_only(importing_path)?, + )) + })?; // Only allow searching up to the first-party path's root let mut search_paths = Vec::new(); @@ -554,21 +601,26 @@ fn absolute_desperate_search_paths(db: &dyn Db, importing_file: File) -> Option< /// chaotic things. In particular, all files under a given pyproject.toml will currently /// agree on this being their desperate search-path, which is really nice. #[salsa::tracked(returns(clone), heap_size=ruff_memory_usage::heap_size)] -fn relative_desperate_search_paths(db: &dyn Db, importing_file: File) -> Option { +fn relative_desperate_search_paths( + db: &dyn Db, + importing_file: ResolverFile<'_>, +) -> Option { + let resolver_environment = importing_file.environment(db); + let importing_file = importing_file.file(db); let system = db.system(); let importing_path = importing_file.path(db).as_system_path()?; // Only allow this if the importing_file is under the first-party search path - let (base_path, rel_path) = - search_paths(db, ModuleResolveMode::Typing).find_map(|search_path| { - if !search_path.is_first_party() { - return None; - } - Some(( - search_path.as_system_path()?, - search_path.relativize_system_path_only(importing_path)?, - )) - })?; + let (base_path, rel_path) = search_paths(db, resolver_environment, ModuleResolveMode::Typing) + .find_map(|search_path| { + if !search_path.is_first_party() { + return None; + } + Some(( + search_path.as_system_path()?, + search_path.relativize_system_path_only(importing_path)?, + )) + })?; // Only allow searching up to the first-party path's root for rel_dir in rel_path.ancestors() { @@ -587,7 +639,7 @@ fn relative_desperate_search_paths(db: &dyn Db, importing_file: File) -> Option< None } -#[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)] pub struct SearchPaths { /// Search paths that have been statically determined purely from reading /// ty's configuration settings. These shouldn't ever change unless the @@ -815,17 +867,6 @@ impl SearchPaths { } } - fn iter<'a>(&'a self, db: &'a dyn Db, mode: ModuleResolveMode) -> SearchPathIterator<'a> { - let stdlib_path = self.stdlib(mode); - SearchPathIterator { - db, - static_paths: self.static_paths.iter(), - stdlib_path, - dynamic_paths: None, - mode: ModuleResolveModeIngredient::new(db, mode), - } - } - fn stdlib(&self, mode: ModuleResolveMode) -> Option<&SearchPath> { match mode { ModuleResolveMode::Typing => self.stdlib_path.as_ref(), @@ -835,18 +876,6 @@ impl SearchPaths { } } - pub fn display<'a>( - &'a self, - db: &'a dyn Db, - mode: ModuleResolveMode, - ) -> DisplaySearchPaths<'a> { - DisplaySearchPaths { - search_paths: self, - db, - mode, - } - } - pub fn custom_stdlib(&self) -> Option<&SystemPath> { self.stdlib_path .as_ref() @@ -858,28 +887,6 @@ impl SearchPaths { } } -pub struct DisplaySearchPaths<'a> { - search_paths: &'a SearchPaths, - db: &'a dyn Db, - mode: ModuleResolveMode, -} - -impl fmt::Display for DisplaySearchPaths<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut paths = self.search_paths.iter(self.db, self.mode).peekable(); - - if paths.peek().is_none() { - return f.write_str("[]"); - } - - writeln!(f, "[")?; - for path in paths { - writeln!(f, " {path},")?; - } - f.write_str("]") - } -} - /// Collect all dynamic search paths. For each `site-packages` path: /// - Collect that `site-packages` path /// - Collect any search paths listed in `.pth` files in that `site-packages` directory @@ -901,7 +908,7 @@ pub(crate) fn dynamic_resolution_paths<'db>( site_packages, typeshed_versions: _, real_stdlib_path, - } = db.search_paths(); + } = mode.resolver_environment(db).search_paths(db); let mut dynamic_paths = Vec::new(); @@ -1070,7 +1077,7 @@ impl<'db> Iterator for SearchPathIterator<'db> { impl FusedIterator for SearchPathIterator<'_> {} -/// A thin wrapper around a module name, resolution mode, and Python version to make them a Salsa +/// A thin wrapper around a module name, resolution mode, and resolver environment to make them a Salsa /// ingredient. /// /// This is needed because Salsa requires that all query arguments are salsa ingredients. @@ -1081,23 +1088,25 @@ struct ModuleNameIngredient<'db> { #[returns(copy)] pub(super) mode: ModuleResolveMode, #[returns(copy)] - pub(super) python_version: PythonVersion, + pub(super) resolver_environment: ResolverEnvironment<'db>, } /// Given a module name and a list of search paths in which to lookup modules, /// attempt to resolve the module name -fn resolve_name( - db: &dyn Db, +fn resolve_name<'db>( + db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, name: &ModuleName, mode: ModuleResolveMode, - python_version: PythonVersion, ) -> Option { - let resolver = NameResolver::new(db, name, mode, python_version); + let resolver = NameResolver::new(db, resolver_environment, name, mode); match mode { - ModuleResolveMode::Typing => resolver.resolve_typing(stub_package_index(db)), + ModuleResolveMode::Typing => { + resolver.resolve_typing(stub_package_index(db, resolver_environment)) + } ModuleResolveMode::Runtime | ModuleResolveMode::RuntimeSomeShadowingAllowed => { - resolver.resolve_runtime(search_paths(db, mode)) + resolver.resolve_runtime(search_paths(db, resolver_environment, mode)) } } } @@ -1106,15 +1115,16 @@ fn resolve_name( /// and we are now Getting Desperate and willing to try the ancestor directories of /// the `importing_file` as potential temporary search paths that are private /// to this import. -fn desperately_resolve_name( - db: &dyn Db, +fn desperately_resolve_name<'db>( + db: &'db dyn Db, importing_file: File, + resolver_environment: ResolverEnvironment<'db>, name: &ModuleName, mode: ModuleResolveMode, - python_version: PythonVersion, ) -> Option { + let importing_file = ResolverFile::new(db, importing_file, resolver_environment); let search_paths = absolute_desperate_search_paths(db, importing_file).unwrap_or_default(); - let resolver = NameResolver::new(db, name, mode, python_version); + let resolver = NameResolver::new(db, resolver_environment, name, mode); match mode { ModuleResolveMode::Typing => resolver.resolve_desperate_typing(search_paths), @@ -1201,13 +1211,13 @@ impl ModuleResolutionCandidate { fn into_module<'db>( self, db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, name: &ModuleName, - python_version: PythonVersion, ) -> Module<'db> { match self.module { ResolvedModule::NamespacePackage => { tracing::trace!("Resolve namespace package `{name}`"); - Module::namespace_package(db, Cow::Borrowed(name), python_version) + Module::namespace_package(db, resolver_environment, Cow::Borrowed(name)) } ResolvedModule::LegacyNamespacePackage(file) => { // legacy namespace packages behave like regular packages @@ -1218,10 +1228,11 @@ impl ModuleResolutionCandidate { ); Module::file_module( db, + file, + resolver_environment, Cow::Borrowed(name), ModuleKind::Package, self.path.into_search_path(), - PythonFile::new(db, file, python_version), ) } ResolvedModule::RegularPackage(file) => { @@ -1231,20 +1242,22 @@ impl ModuleResolutionCandidate { ); Module::file_module( db, + file, + resolver_environment, Cow::Borrowed(name), ModuleKind::Package, self.path.into_search_path(), - PythonFile::new(db, file, python_version), ) } ResolvedModule::Module(file) => { tracing::trace!("Resolved module `{name}` to `{path}`", path = file.path(db)); Module::file_module( db, + file, + resolver_environment, Cow::Borrowed(name), ModuleKind::Module, self.path.into_search_path(), - PythonFile::new(db, file, python_version), ) } } @@ -1286,12 +1299,13 @@ struct NameResolver<'db, 'name> { impl<'db, 'name> NameResolver<'db, 'name> { fn new( db: &'db dyn Db, + resolver_environment: ResolverEnvironment<'db>, name: &'name ModuleName, mode: ModuleResolveMode, - python_version: PythonVersion, ) -> Self { + let python_version = resolver_environment.python_version(db); Self { - context: ResolverContext::new(db, python_version, mode), + context: ResolverContext::new(db, resolver_environment, mode), name, is_non_shadowable: mode.is_non_shadowable(python_version.minor, name.as_str()), } @@ -1303,11 +1317,13 @@ impl<'db, 'name> NameResolver<'db, 'name> { /// a fallback when no stub provides the requested module. A stub overlay may use runtime /// packages as parents, but its final module must come from a stub file. fn resolve_typing(&self, stub_packages: &StubPackageIndex) -> Option { - let search_paths = self.context.db.search_paths(); - if self.name.components().nth(1).is_none() { let candidates = self.discover_roots( - search_paths.iter(self.context.db, ModuleResolveMode::Typing), + search_paths( + self.context.db, + self.context.resolver_environment, + ModuleResolveMode::Typing, + ), stub_packages.all(), ); return self.resolve_remaining(candidates, ComponentFileFilter::ByMode); @@ -1318,9 +1334,12 @@ impl<'db, 'name> NameResolver<'db, 'name> { // normal fallback so that each extra path is probed only once. let (overlay_stub_packages, remaining_stub_packages) = stub_packages.split_overlay(); let mut candidates = self.discover_roots( - search_paths - .iter(self.context.db, ModuleResolveMode::Typing) - .take_while(|search_path| search_path.is_extra()), + search_paths( + self.context.db, + self.context.resolver_environment, + ModuleResolveMode::Typing, + ) + .take_while(|search_path| search_path.is_extra()), overlay_stub_packages, ); if let Some(resolved) = @@ -1330,9 +1349,12 @@ impl<'db, 'name> NameResolver<'db, 'name> { } let remaining_candidates = self.discover_roots( - search_paths - .iter(self.context.db, ModuleResolveMode::Typing) - .skip_while(|search_path| search_path.is_extra()), + search_paths( + self.context.db, + self.context.resolver_environment, + ModuleResolveMode::Typing, + ) + .skip_while(|search_path| search_path.is_extra()), remaining_stub_packages, ); candidates.extend(remaining_candidates); @@ -1738,7 +1760,11 @@ fn is_legacy_namespace_package( // but hey, this is better than nothing! let parsed = ruff_db::parsed::parsed_module( context.db, - ruff_db::PythonFile::new(context.db, init, context.python_version), + PythonFile::new( + context.db, + init, + context.resolver_environment.python_version(context.db), + ), ); let mut visitor = LegacyNamespacePackageVisitor::default(); visitor.visit_body(parsed.load(context.db).suite()); @@ -1774,19 +1800,19 @@ impl PyTyped { pub(super) struct ResolverContext<'db> { pub(super) db: &'db dyn Db, - pub(super) python_version: PythonVersion, + pub(super) resolver_environment: ResolverEnvironment<'db>, pub(super) mode: ModuleResolveMode, } impl<'db> ResolverContext<'db> { pub(super) fn new( db: &'db dyn Db, - python_version: PythonVersion, + resolver_environment: ResolverEnvironment<'db>, mode: ModuleResolveMode, ) -> Self { Self { db, - python_version, + resolver_environment, mode, } } @@ -2017,18 +2043,18 @@ mod tests { db: &'db TestDb, module_name: &ModuleName, ) -> Option> { - super::resolve_module_confident(db, db.python_version(), module_name) + super::resolve_module_confident(db, db.resolver_environment(), module_name) } fn resolve_real_module_confident<'db>( db: &'db TestDb, module_name: &ModuleName, ) -> Option> { - super::resolve_real_module_confident(db, db.python_version(), module_name) + super::resolve_real_module_confident(db, db.resolver_environment(), module_name) } fn path_to_module<'db>(db: &'db TestDb, path: &FilePath) -> Option> { - super::path_to_module(db, path, db.python_version()) + super::path_to_module(db, db.resolver_environment(), path) } #[test] @@ -2102,10 +2128,13 @@ mod tests { ]) .build(); let importing_file = system_path_to_file(&db, src.join("nested/main.py")).unwrap(); - let importing_file = PythonFile::new(&db, importing_file, db.python_version()); - let foo = - resolve_module(&db, importing_file, &ModuleName::new_static("foo").unwrap()).unwrap(); + let foo = resolve_module( + &db, + ImportingFile::File(importing_file, db.resolver_environment()), + &ModuleName::new_static("foo").unwrap(), + ) + .unwrap(); assert_eq!( foo.file(&db).unwrap().path(&db), &src.join("nested/foo-stubs/__init__.pyi") @@ -2303,7 +2332,7 @@ mod tests { } #[test] - fn resolve_module_uses_importing_file_python_version() { + fn resolve_module_uses_resolver_environment_python_version() { const TYPESHED: MockedTypeshed = MockedTypeshed { stdlib_files: &[("_sha256.pyi", ""), ("py312_only.pyi", "")], versions: "_sha256: 3.11-\npy312_only: 3.12-", @@ -2321,12 +2350,13 @@ mod tests { .with_python_version(PythonVersion::PY311) .build(); let importing_file = system_path_to_file(&db, src.join("main.py")).unwrap(); - let py311 = PythonFile::new(&db, importing_file, PythonVersion::PY311); - let py312 = PythonFile::new(&db, importing_file, PythonVersion::PY312); - + let py311 = ResolverEnvironment::new(&db, PythonVersion::PY311, db.search_paths()); + let py312 = ResolverEnvironment::new(&db, PythonVersion::PY312, db.search_paths()); let sha256 = ModuleName::new_static("_sha256").unwrap(); - let py311_module = resolve_module(&db, py311, &sha256).unwrap(); - let py312_module = resolve_module(&db, py312, &sha256).unwrap(); + let py311_module = + resolve_module(&db, ImportingFile::File(importing_file, py311), &sha256).unwrap(); + let py312_module = + resolve_module(&db, ImportingFile::File(importing_file, py312), &sha256).unwrap(); assert_eq!( py311_module.file(&db).unwrap().path(&db), &stdlib.join("_sha256.pyi") @@ -2339,8 +2369,10 @@ mod tests { assert_eq!(py312_module.python_version(&db), PythonVersion::PY312); let namespace = ModuleName::new_static("namespace").unwrap(); - let py311_namespace = resolve_module(&db, py311, &namespace).unwrap(); - let py312_namespace = resolve_module(&db, py312, &namespace).unwrap(); + let py311_namespace = + resolve_module(&db, ImportingFile::File(importing_file, py311), &namespace).unwrap(); + let py312_namespace = + resolve_module(&db, ImportingFile::File(importing_file, py312), &namespace).unwrap(); assert!(matches!(py311_namespace, Module::Namespace(_))); assert!(matches!(py312_namespace, Module::Namespace(_))); assert_eq!(py311_namespace.python_version(&db), PythonVersion::PY311); @@ -2348,9 +2380,11 @@ mod tests { assert_ne!(py311_namespace, py312_namespace); let py312_only = ModuleName::new_static("py312_only").unwrap(); - assert!(resolve_module(&db, py311, &py312_only).is_none()); + assert!( + resolve_module(&db, ImportingFile::File(importing_file, py311), &py312_only).is_none() + ); assert_eq!( - resolve_module(&db, py312, &py312_only) + resolve_module(&db, ImportingFile::File(importing_file, py312), &py312_only) .and_then(|module| module.file(&db)) .unwrap() .path(&db), @@ -2358,6 +2392,44 @@ mod tests { ); } + #[test] + fn resolve_module_uses_resolver_environment_search_paths() { + let TestCase { mut db, src, .. } = TestCaseBuilder::new() + .with_src_files(&[("main.py", ""), ("shared.py", "from_src = True")]) + .with_vendored_typeshed() + .build(); + db.write_file("/alternate/shared.py", "from_alternate = True") + .unwrap(); + + let alternate_paths = SearchPathSettings { + src_roots: vec![SystemPathBuf::from("/alternate")], + ..SearchPathSettings::empty() + } + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .unwrap(); + alternate_paths.try_register_static_roots(&db); + + let primary = db.resolver_environment(); + let alternate = ResolverEnvironment::new(&db, PythonVersion::default(), &alternate_paths); + let importing_file = system_path_to_file(&db, src.join("main.py")).unwrap(); + let name = ModuleName::new_static("shared").unwrap(); + + let primary_module = + resolve_module(&db, ImportingFile::File(importing_file, primary), &name).unwrap(); + let alternate_module = + resolve_module(&db, ImportingFile::File(importing_file, alternate), &name).unwrap(); + + assert_eq!( + primary_module.file(&db).unwrap().path(&db), + &src.join("shared.py") + ); + assert_eq!( + alternate_module.file(&db).unwrap().path(&db), + &SystemPathBuf::from("/alternate/shared.py") + ); + assert_ne!(primary_module, alternate_module); + } + #[test] fn stdlib_resolution_respects_versions_file_py38_existing_modules() { const VERSIONS: &str = "\ @@ -2894,7 +2966,7 @@ mod tests { &db, functools_module_name, ModuleResolveMode::Typing, - db.python_version(), + db.resolver_environment(), ), &events, ); @@ -3153,7 +3225,11 @@ not_a_directory assert_function_query_was_not_run( &db, dynamic_resolution_paths, - ModuleResolveModeIngredient::new(&db, ModuleResolveMode::Typing), + ModuleResolveModeIngredient::new( + &db, + db.resolver_environment(), + ModuleResolveMode::Typing, + ), &events, ); } @@ -3172,7 +3248,11 @@ not_a_directory dynamic_resolution_paths( &db, - ModuleResolveModeIngredient::new(&db, ModuleResolveMode::Typing), + ModuleResolveModeIngredient::new( + &db, + db.resolver_environment(), + ModuleResolveMode::Typing, + ), ); db.clear_salsa_events(); @@ -3180,14 +3260,22 @@ not_a_directory .unwrap(); dynamic_resolution_paths( &db, - ModuleResolveModeIngredient::new(&db, ModuleResolveMode::Typing), + ModuleResolveModeIngredient::new( + &db, + db.resolver_environment(), + ModuleResolveMode::Typing, + ), ); let events = db.take_salsa_events(); assert_function_query_was_not_run( &db, dynamic_resolution_paths, - ModuleResolveModeIngredient::new(&db, ModuleResolveMode::Typing), + ModuleResolveModeIngredient::new( + &db, + db.resolver_environment(), + ModuleResolveMode::Typing, + ), &events, ); } @@ -3284,7 +3372,8 @@ not_a_directory .with_site_packages_files(&[("_foo.pth", "/src")]) .build(); - let search_paths: Vec<&SearchPath> = search_paths(&db, ModuleResolveMode::Typing).collect(); + let search_paths: Vec<&SearchPath> = + search_paths(&db, db.resolver_environment(), ModuleResolveMode::Typing).collect(); assert!(search_paths.contains( &&SearchPath::first_party(db.system(), SystemPathBuf::from("/src")).unwrap() @@ -3432,7 +3521,7 @@ not_a_directory let foo_module_file = File::new(&db, FilePath::from(installed_foo_module)); let module = file_to_module( &db, - PythonFile::new(&db, foo_module_file, db.python_version()), + ResolverFile::new(&db, foo_module_file, db.resolver_environment()), ) .unwrap(); assert_eq!(module.search_path(&db).unwrap(), &site_packages); diff --git a/crates/ty_module_resolver/src/typeshed.rs b/crates/ty_module_resolver/src/typeshed.rs index 381bb58fb7..94421882de 100644 --- a/crates/ty_module_resolver/src/typeshed.rs +++ b/crates/ty_module_resolver/src/typeshed.rs @@ -6,9 +6,8 @@ use std::str::FromStr; use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::{PythonVersion, PythonVersionDeserializationError}; -use rustc_hash::FxHashMap; -use crate::db::Db; +use crate::FxOrderMap; use crate::module_name::ModuleName; pub(crate) fn vendored_typeshed_versions(vendored: &VendoredFileSystem) -> TypeshedVersions { @@ -20,10 +19,6 @@ pub(crate) fn vendored_typeshed_versions(vendored: &VendoredFileSystem) -> Types .expect("The VERSIONS file in the vendored typeshed stubs should be well-formed") } -pub(crate) fn typeshed_versions(db: &dyn Db) -> &TypeshedVersions { - db.search_paths().typeshed_versions() -} - #[derive(Debug, PartialEq, Eq, Clone)] pub struct TypeshedVersionsParseError { line_number: Option, @@ -71,8 +66,8 @@ pub enum TypeshedVersionsParseErrorKind { VersionParseError(#[from] PythonVersionDeserializationError), } -#[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] -pub struct TypeshedVersions(FxHashMap); +#[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)] +pub struct TypeshedVersions(FxOrderMap); impl TypeshedVersions { #[must_use] @@ -164,7 +159,7 @@ impl FromStr for TypeshedVersions { type Err = TypeshedVersionsParseError; fn from_str(s: &str) -> Result { - let mut map = FxHashMap::default(); + let mut map = FxOrderMap::default(); for (line_index, line) in s.lines().enumerate() { // humans expect line numbers to be 1-indexed diff --git a/crates/ty_project/src/db.rs b/crates/ty_project/src/db.rs index 7f0496ce1c..a9cff91b5b 100644 --- a/crates/ty_project/src/db.rs +++ b/crates/ty_project/src/db.rs @@ -15,7 +15,7 @@ use ruff_db::system::System; use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::PythonVersion; use salsa::{Database, Event, Setter}; -use ty_module_resolver::SearchPaths; +use ty_python_core::ProgramFile; use ty_python_core::program::{ FallibleStrategy, MisconfigurationStrategy, Program, UseDefaultStrategy, }; @@ -538,11 +538,7 @@ impl SalsaMemoryDump { } #[salsa::db] -impl ty_module_resolver::Db for ProjectDatabase { - fn search_paths(&self) -> &SearchPaths { - Program::get(self).search_paths(self) - } -} +impl ty_module_resolver::Db for ProjectDatabase {} #[salsa::db] impl SemanticDb for ProjectDatabase { @@ -550,6 +546,10 @@ impl SemanticDb for ProjectDatabase { ProjectDatabase::check_file(self, file) } + fn program_file(&self, file: File) -> ProgramFile<'_> { + Program::get(self).program_file(self, file) + } + fn rule_selection(&self, file: File) -> &RuleSelection { let settings = file_settings(self, file); settings.rules(self) @@ -650,6 +650,7 @@ pub(crate) mod testing { use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::PythonVersion; use ty_module_resolver::SearchPathSettings; + use ty_python_core::ProgramFile; use ty_python_core::platform::PythonPlatform; use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; @@ -737,7 +738,7 @@ pub(crate) mod testing { } pub fn program_environment(&self) -> ProgramEnvironment<'_> { - ProgramEnvironment::from_program(self.python_version()) + ProgramEnvironment::from_program(Program::get(self).resolver_environment(self)) } /// Takes the salsa events. @@ -774,11 +775,7 @@ pub(crate) mod testing { } #[salsa::db] - impl ty_module_resolver::Db for TestDb { - fn search_paths(&self) -> &ty_module_resolver::SearchPaths { - Program::get(self).search_paths(self) - } - } + impl ty_module_resolver::Db for TestDb {} #[salsa::db] impl ty_python_core::Db for TestDb { @@ -794,6 +791,10 @@ pub(crate) mod testing { crate::check_file(self, file) } + fn program_file(&self, file: File) -> ProgramFile<'_> { + Program::get(self).program_file(self, file) + } + fn rule_selection(&self, _file: ruff_db::files::File) -> &RuleSelection { self.project().rules(self) } @@ -844,8 +845,9 @@ mod tests { use ruff_db::files::FileRootKind; use ruff_db::system::{SystemPathBuf, TestSystem}; use ty_module_resolver::list_modules; + use ty_python_core::program::Program; - use crate::{Db as _, ProjectDatabase, ProjectMetadata}; + use crate::{ProjectDatabase, ProjectMetadata}; #[test] fn frozen_inputs_support_a_one_shot_check() -> anyhow::Result<()> { @@ -889,7 +891,7 @@ mod tests { let metadata = ProjectMetadata::discover(&project, &system)?; let db = ProjectDatabase::fallible(metadata, system)?; - let modules = list_modules(&db, db.python_version()); + let modules = list_modules(&db, Program::get(&db).resolver_environment(&db)); assert!( modules .iter() diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index 7772fe0985..b461f64078 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -14,7 +14,6 @@ use files::{Index, Indexed, IndexedFiles}; use metadata::settings::Settings; pub use metadata::{ProjectMetadata, ProjectMetadataError}; use rayon::prelude::*; -use ruff_db::PythonFile; use ruff_db::diagnostic::{ Diagnostic, DiagnosticId, Severity, SubDiagnostic, SubDiagnosticSeverity, }; @@ -28,6 +27,7 @@ use std::collections::{BTreeSet, hash_set}; use std::iter::FusedIterator; use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::sync::Arc; +use ty_python_core::ProgramFile; pub use ty_python_semantic::Db as SemanticDb; use ty_python_semantic::lint::RuleSelection; @@ -394,15 +394,16 @@ impl Project { let check_file_span = tracing::debug_span!(parent: &project_span, "check_file", ?file); let _entered = check_file_span.entered(); - let python_file = PythonFile::new(db, file, db.python_version()); + let program_file = db.program_file(file); - match check_file_impl(db, python_file) { + match check_file_impl(db, program_file) { Ok(diagnostics) => { reporter.report_checked_file(db, file, diagnostics); // This is outside `check_file_impl` to avoid that opening or closing // a file invalidates the `check_file_impl` query of every file! if !open_files.contains(&file) { + let python_file = program_file.python_file(db); // The module has already been parsed by `check_file_impl`. // We only retrieve it here so that we can call `clear` on it. let parsed = parsed_module(db, python_file); @@ -660,7 +661,7 @@ fn check_file(db: &dyn Db, file: File) -> Vec { return Vec::new(); } - check_file_impl(db, PythonFile::new(db, file, db.python_version())) + check_file_impl(db, db.program_file(file)) .map(<[Diagnostic]>::to_vec) .unwrap_or_else(|diagnostic| vec![diagnostic.clone()]) } @@ -744,7 +745,7 @@ pub enum ProjectReloadResult { #[salsa::tracked(returns(as_deref), heap_size=ruff_memory_usage::heap_size)] pub(crate) fn check_file_impl( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, ) -> Result, Diagnostic> { let source_file = file.file(db); { @@ -894,11 +895,11 @@ mod tests { use crate::db::Db as _; use crate::db::testing::TestDb; use crate::{IncludeResult, ProjectMetadata}; - use ruff_db::PythonFile; 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 ty_python_semantic::Db as _; use ty_python_semantic::types::check_types; #[test] @@ -917,7 +918,7 @@ mod tests { assert_eq!(source_text(&db, file).as_str(), ""); assert_eq!( - check_file_impl(&db, PythonFile::new(&db, file, db.python_version())) + check_file_impl(&db, db.program_file(file)) .as_ref() .unwrap_err() .headline_message() @@ -926,12 +927,7 @@ mod tests { ); let events = db.take_salsa_events(); - assert_function_query_was_not_run( - &db, - check_types, - PythonFile::new(&db, file, db.python_version()), - &events, - ); + assert_function_query_was_not_run(&db, check_types, db.program_file(file), &events); // The user now creates a new file with an empty text. The source text // content returned by `source_text` remains unchanged, but the diagnostics should get updated. @@ -939,7 +935,7 @@ mod tests { assert_eq!(source_text(&db, file).as_str(), ""); assert_eq!( - check_file_impl(&db, PythonFile::new(&db, file, db.python_version())) + check_file_impl(&db, db.program_file(file)) .as_ref() .unwrap() .iter() diff --git a/crates/ty_project/src/watch/project_watcher.rs b/crates/ty_project/src/watch/project_watcher.rs index 92d7451d83..c4829c4727 100644 --- a/crates/ty_project/src/watch/project_watcher.rs +++ b/crates/ty_project/src/watch/project_watcher.rs @@ -6,6 +6,7 @@ use tracing::info; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_db::system::{SystemPath, SystemPathBuf}; use ty_module_resolver::system_module_search_paths; +use ty_python_core::program::Program; use crate::db::{Db, ProjectDatabase}; use crate::watch::Watcher; @@ -40,7 +41,8 @@ impl ProjectWatcher { } pub fn update(&mut self, db: &ProjectDatabase) { - let search_paths: Vec<_> = system_module_search_paths(db).collect(); + let environment = Program::get(db).resolver_environment(db); + let search_paths: Vec<_> = system_module_search_paths(db, environment).collect(); let project_path = db.project().root(db); let new_cache_key = Self::compute_cache_key(project_path, &search_paths); diff --git a/crates/ty_python_core/src/ast_ids.rs b/crates/ty_python_core/src/ast_ids.rs index 955994e6f2..8395deca18 100644 --- a/crates/ty_python_core/src/ast_ids.rs +++ b/crates/ty_python_core/src/ast_ids.rs @@ -1,11 +1,11 @@ use rustc_hash::FxHashMap; -use ruff_db::PythonFile; use ruff_index::{IndexVec, newtype_index}; use ruff_python_ast as ast; use ruff_python_ast::ExprRef; use crate::Db; +use crate::ProgramFile; use crate::frozen::FrozenMap; use crate::scope::FileScopeId; use crate::semantic_index; @@ -55,7 +55,7 @@ impl AstIds { } } -fn ast_ids<'db>(db: &'db dyn Db, file: PythonFile<'db>) -> &'db AstIds { +fn ast_ids<'db>(db: &'db dyn Db, file: ProgramFile<'db>) -> &'db AstIds { semantic_index(db, file).ast_ids() } @@ -66,46 +66,46 @@ pub struct ScopedUseId; pub trait HasScopedUseId { /// Returns the ID that uniquely identifies the use in its scope. - fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId; + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId; } impl HasScopedUseId for ast::Identifier { - fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId { let ast_ids = ast_ids(db, file); ast_ids.use_id(self) } } impl HasScopedUseId for ast::ExprName { - fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId { let expression_ref = ExprRef::from(self); expression_ref.scoped_use_id(db, file) } } impl HasScopedUseId for ast::ExprAttribute { - fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId { let expression_ref = ExprRef::from(self); expression_ref.scoped_use_id(db, file) } } impl HasScopedUseId for ast::ExprSubscript { - fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId { let expression_ref = ExprRef::from(self); expression_ref.scoped_use_id(db, file) } } impl HasScopedUseId for ast::Keyword { - fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId { let ast_ids = ast_ids(db, file); ast_ids.use_id(self) } } impl HasScopedUseId for ast::ExprRef<'_> { - fn scoped_use_id(&self, db: &dyn Db, file: PythonFile<'_>) -> ScopedUseId { + fn scoped_use_id(&self, db: &dyn Db, file: ProgramFile<'_>) -> ScopedUseId { let ast_ids = ast_ids(db, file); ast_ids.use_id(*self) } diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index 014d12e47e..42501ef7ed 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -8,7 +8,6 @@ use rustc_hash::{FxHashMap, FxHashSet}; use ruff_db::parsed::ParsedModuleRef; -use ruff_db::PythonFile; use ruff_db::source::{SourceText, source_text}; use ruff_index::IndexVec; use ruff_python_ast::name::Name; @@ -20,9 +19,10 @@ use ruff_python_parser::semantic_errors::{ }; use ruff_text_size::{Ranged, TextRange}; use smallvec::SmallVec; -use ty_module_resolver::{ModuleName, resolve_module}; +use ty_module_resolver::{ImportingFile, ModuleName, ResolverEnvironment, resolve_module}; use crate::HasTrackedScope; +use crate::ProgramFile; use crate::ast_ids::node_key::ExpressionNodeKey; use crate::ast_ids::{AstIdsBuilder, ScopedUseId}; use crate::ast_node_ref::AstNodeRef; @@ -229,7 +229,7 @@ impl ConditionFlowSnapshot { pub(super) struct SemanticIndexBuilder<'db, 'ast> { // Builder state db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, source_type: PySourceType, module: &'ast ParsedModuleRef, scope_stack: Vec>, @@ -253,7 +253,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> { in_type_checking_block: bool, // Used for checking semantic syntax errors - python_version: PythonVersion, + resolver_environment: ResolverEnvironment<'db>, source_text: OnceCell, semantic_checker: SemanticSyntaxChecker, in_try: bool, @@ -302,7 +302,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> { impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { pub(super) fn new( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, module_ref: &'ast ParsedModuleRef, ) -> Self { let mut builder = Self { @@ -344,7 +344,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { enclosing_snapshots: FxHashMap::default(), - python_version: file.python_version(db), + resolver_environment: file.resolver_environment(db), source_text: OnceCell::new(), semantic_checker: SemanticSyntaxChecker::default(), in_try: false, @@ -977,7 +977,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { symbol.name().to_string(), ), range: declaration.range, - python_version: self.python_version, + python_version: self.python_version(), }); } // This `nonlocal` is resolved. @@ -1040,7 +1040,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.report_semantic_error(SemanticSyntaxError { kind: SemanticSyntaxErrorKind::NonlocalWithoutBinding(name.to_string()), range: declaration.range, - python_version: self.python_version, + python_version: self.python_version(), }); } } @@ -3166,14 +3166,18 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { // in one function is visible in another function. let mut is_self_import = false; let source_file = self.file.file(self.db); + let resolver_environment = self.resolver_environment; if source_file.is_package(self.db) && let Ok(module_name) = ModuleName::from_identifier_parts( self.db, - self.file, + ImportingFile::File(source_file, resolver_environment), node.module.as_deref(), node.level, ) - && let Ok(thispackage) = ModuleName::package_for_file(self.db, self.file) + && let Ok(thispackage) = ModuleName::package_for_file( + self.db, + ImportingFile::File(source_file, resolver_environment), + ) { // Record whether this is equivalent to `from . import ...` is_self_import = module_name == thispackage; @@ -3246,19 +3250,27 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { continue; } - let Ok(module_name) = - ModuleName::from_import_statement(self.db, self.file, node) - else { + let Ok(module_name) = ModuleName::from_import_statement( + self.db, + ImportingFile::File(source_file, resolver_environment), + node, + ) else { continue; }; - let Some(module) = resolve_module(self.db, self.file, &module_name) else { + let Some(module) = resolve_module( + self.db, + ImportingFile::File(source_file, resolver_environment), + &module_name, + ) else { continue; }; - let Some(referenced_parse_file) = module.python_file(self.db) else { + let Some(referenced_file) = module.file(self.db) else { continue; }; + let referenced_program_file = + ProgramFile::new(self.db, referenced_file, self.file.program(self.db)); // In order to understand the reachability of definitions created by a `*` import, // we need to know the reachability of the global-scope definitions in the // `referenced_module` the symbols imported from. Much like predicates for `if` @@ -3273,14 +3285,14 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { // ``` // // For more details, see the doc-comment on `StarImportPlaceholderPredicate`. - for export in exported_names(self.db, referenced_parse_file) { + for export in exported_names(self.db, referenced_program_file) { let symbol_id = self.add_symbol(export.clone()); let node_ref = StarImportDefinitionNodeRef { node, symbol_id }; let star_import = StarImportPlaceholderPredicate::new( self.db, self.file, symbol_id, - referenced_parse_file, + referenced_program_file, ); let star_import_predicate = self.add_predicate(star_import.into()); @@ -3461,7 +3473,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.report_semantic_error(SemanticSyntaxError { kind: SemanticSyntaxErrorKind::AnnotatedGlobal(name.id.as_str().into()), range: name.range, - python_version: self.python_version, + python_version: self.python_version(), }); } // Check whether the variable has been declared nonlocal. @@ -3471,7 +3483,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { name.id.as_str().into(), ), range: name.range, - python_version: self.python_version, + python_version: self.python_version(), }); } } @@ -4230,7 +4242,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { start: name.range.start(), }, range: name.range, - python_version: self.python_version, + python_version: self.python_version(), }); } // Check whether the variable has also been declared nonlocal. @@ -4238,7 +4250,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.report_semantic_error(SemanticSyntaxError { kind: SemanticSyntaxErrorKind::NonlocalAndGlobal(name.to_string()), range: name.range, - python_version: self.python_version, + python_version: self.python_version(), }); // Never mark a symbol both global and nonlocal, even in this error case. continue; @@ -4283,7 +4295,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { start: name.range.start(), }, range: name.range, - python_version: self.python_version, + python_version: self.python_version(), }); } // Check whether the variable has also been declared global. @@ -4291,7 +4303,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.report_semantic_error(SemanticSyntaxError { kind: SemanticSyntaxErrorKind::NonlocalAndGlobal(name.to_string()), range: name.range, - python_version: self.python_version, + python_version: self.python_version(), }); // Never mark a symbol both global and nonlocal, even in this error case. continue; @@ -5003,7 +5015,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { } fn python_version(&self) -> PythonVersion { - self.python_version + self.resolver_environment.python_version(self.db) } fn source(&self) -> &str { diff --git a/crates/ty_python_core/src/db.rs b/crates/ty_python_core/src/db.rs index 0578773518..831360fc94 100644 --- a/crates/ty_python_core/src/db.rs +++ b/crates/ty_python_core/src/db.rs @@ -21,9 +21,7 @@ pub(crate) mod tests { }; use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::PythonVersion; - use ty_module_resolver::{ - Db as ModuleResolverDb, FallibleStrategy, SearchPathSettings, SearchPaths, - }; + use ty_module_resolver::{Db as ModuleResolverDb, FallibleStrategy, SearchPathSettings}; use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; use crate::platform::PythonPlatform; @@ -93,11 +91,7 @@ pub(crate) mod tests { } #[salsa::db] - impl ModuleResolverDb for TestDb { - fn search_paths(&self) -> &SearchPaths { - Program::get(self).search_paths(self) - } - } + impl ModuleResolverDb for TestDb {} #[salsa::db] impl salsa::Database for TestDb {} diff --git a/crates/ty_python_core/src/definition.rs b/crates/ty_python_core/src/definition.rs index 26a2b09243..42ffb515b1 100644 --- a/crates/ty_python_core/src/definition.rs +++ b/crates/ty_python_core/src/definition.rs @@ -11,6 +11,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize}; use smallvec::SmallVec; use crate::LoopHeaderId; +use crate::ProgramFile; use crate::ast_node_ref::AstNodeRef; use crate::member::ScopedMemberId; use crate::node_key::NodeKey; @@ -88,7 +89,11 @@ impl<'db> Definition<'db> { self.scope_id(db).python_file(db) } - pub fn program(self, db: &'db dyn Db) -> Program { + pub fn program_file(self, db: &'db dyn Db) -> ProgramFile<'db> { + self.scope_id(db).program_file(db) + } + + pub fn program(self, db: &'db dyn Db) -> Program<'db> { self.scope_id(db).program(db) } diff --git a/crates/ty_python_core/src/expression.rs b/crates/ty_python_core/src/expression.rs index 4b92b4c147..93edd77421 100644 --- a/crates/ty_python_core/src/expression.rs +++ b/crates/ty_python_core/src/expression.rs @@ -1,7 +1,7 @@ -use crate::Program; use crate::ast_node_ref::AstNodeRef; use crate::db::Db; use crate::scope::ScopeId; +use crate::{Program, ProgramFile}; use ruff_db::PythonFile; use ruff_db::files::File; use ruff_python_ast as ast; @@ -80,7 +80,11 @@ impl<'db> Expression<'db> { self.scope_id(db).python_file(db) } - pub fn program(self, db: &'db dyn Db) -> Program { + pub fn program_file(self, db: &'db dyn Db) -> ProgramFile<'db> { + self.scope_id(db).program_file(db) + } + + pub fn program(self, db: &'db dyn Db) -> Program<'db> { self.scope_id(db).program(db) } } diff --git a/crates/ty_python_core/src/lib.rs b/crates/ty_python_core/src/lib.rs index 39b32dec25..0d86cf8001 100644 --- a/crates/ty_python_core/src/lib.rs +++ b/crates/ty_python_core/src/lib.rs @@ -8,7 +8,6 @@ use std::sync::Arc; use ruff_db::parsed::parsed_module; -use ruff_db::PythonFile; use ruff_index::{FrozenIndexVec, IndexSlice}; use ruff_python_ast::NodeIndex; use ruff_python_parser::semantic_errors::SemanticSyntaxError; @@ -16,11 +15,11 @@ use ruff_text_size::TextRange; use rustc_hash::{FxHashMap, FxHashSet}; use salsa::plumbing::AsId; use smallvec::SmallVec; -use ty_module_resolver::ModuleName; +use ty_module_resolver::{ModuleName, ResolverEnvironment}; // FIXME: Replace this temporary alias once semantic query keys can use the environment-bearing // `Program` Salsa ingredient directly. -pub type Program = ast::PythonVersion; +pub type Program<'db> = ResolverEnvironment<'db>; use crate::frozen::{FrozenMap, FrozenSet}; use crate::place::ScopedPlaceId; @@ -66,15 +65,17 @@ pub mod unpack; mod use_def; pub use db::Db; pub mod program; +pub mod program_file; +pub use program_file::ProgramFile; /// Returns the semantic index for `file`. /// /// Prefer using [`symbol_table`] when working with symbols from a single scope. #[salsa::tracked(returns(ref), no_eq, heap_size=ruff_memory_usage::heap_size)] -pub fn semantic_index<'db>(db: &'db dyn Db, file: PythonFile<'db>) -> SemanticIndex<'db> { +pub fn semantic_index<'db>(db: &'db dyn Db, file: ProgramFile<'db>) -> SemanticIndex<'db> { let _span = tracing::trace_span!("semantic_index", ?file).entered(); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); SemanticIndexBuilder::new(db, file, &module).build() } @@ -86,9 +87,9 @@ pub fn semantic_index<'db>(db: &'db dyn Db, file: PythonFile<'db>) -> SemanticIn /// is unchanged. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] pub fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc { - let python_file = scope.python_file(db); - let _span = tracing::trace_span!("place_table", scope=?scope.as_id(), ?python_file).entered(); - let index = semantic_index(db, python_file); + let program_file = scope.program_file(db); + let _span = tracing::trace_span!("place_table", scope=?scope.as_id(), ?program_file).entered(); + let index = semantic_index(db, program_file); Arc::clone(&index.place_tables[scope.file_scope_id(db)]) } @@ -99,9 +100,9 @@ pub fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc /// is unchanged. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] pub fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc> { - let python_file = scope.python_file(db); - let _span = tracing::trace_span!("use_def_map", scope=?scope.as_id(), ?python_file).entered(); - let index = semantic_index(db, python_file); + let program_file = scope.program_file(db); + let _span = tracing::trace_span!("use_def_map", scope=?scope.as_id(), ?program_file).entered(); + let index = semantic_index(db, program_file); Arc::clone(&index.use_def_maps[scope.file_scope_id(db)]) } @@ -176,7 +177,7 @@ pub fn attribute_scopes<'db>( db: &'db dyn Db, class_body_scope: ScopeId<'db>, ) -> impl Iterator + 'db { - let index = semantic_index(db, class_body_scope.python_file(db)); + let index = semantic_index(db, class_body_scope.program_file(db)); let class_scope_id = class_body_scope.file_scope_id(db); ChildrenIter::new(&index.scopes, class_scope_id) .filter_map(move |(child_scope_id, scope)| { @@ -225,7 +226,7 @@ pub fn attribute_scopes<'db>( /// Returns the module global scope of `file`. #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] -pub fn global_scope<'db>(db: &'db dyn Db, file: PythonFile<'db>) -> ScopeId<'db> { +pub fn global_scope<'db>(db: &'db dyn Db, file: ProgramFile<'db>) -> ScopeId<'db> { let _span = tracing::trace_span!("global_scope", ?file).entered(); FileScopeId::global().to_scope_id(db, file) @@ -1124,8 +1125,8 @@ mod tests { TestCase { db, file } } - fn python_file(db: &TestDb, file: File) -> PythonFile<'_> { - PythonFile::new(db, file, Program::get(db).python_version(db)) + fn program_file(db: &TestDb, file: File) -> ProgramFile<'_> { + Program::get(db).program_file(db, file) } fn names(table: &PlaceTable) -> Vec { @@ -1138,7 +1139,7 @@ mod tests { #[test] fn empty() { let TestCase { db, file } = test_case(""); - let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); let global_names = names(global_table); @@ -1148,7 +1149,7 @@ mod tests { #[test] fn simple() { let TestCase { db, file } = test_case("x"); - let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); assert_eq!(names(global_table), vec!["x"]); } @@ -1156,7 +1157,7 @@ mod tests { #[test] fn annotation_only() { let TestCase { db, file } = test_case("x: int"); - let scope = global_scope(&db, python_file(&db, file)); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["int", "x"]); @@ -1174,7 +1175,7 @@ mod tests { #[test] fn import() { let TestCase { db, file } = test_case("import foo"); - let scope = global_scope(&db, python_file(&db, file)); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["foo"]); @@ -1188,7 +1189,7 @@ mod tests { #[test] fn import_sub() { let TestCase { db, file } = test_case("import foo.bar"); - let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); assert_eq!(names(global_table), vec!["foo"]); } @@ -1196,7 +1197,7 @@ mod tests { #[test] fn import_as() { let TestCase { db, file } = test_case("import foo.bar as baz"); - let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); assert_eq!(names(global_table), vec!["baz"]); } @@ -1204,7 +1205,7 @@ mod tests { #[test] fn import_from() { let TestCase { db, file } = test_case("from bar import foo"); - let scope = global_scope(&db, python_file(&db, file)); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["foo"]); @@ -1225,7 +1226,7 @@ mod tests { #[test] fn assign() { let TestCase { db, file } = test_case("x = foo"); - let scope = global_scope(&db, python_file(&db, file)); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["foo", "x"]); @@ -1245,7 +1246,7 @@ mod tests { #[test] fn augmented_assignment() { let TestCase { db, file } = test_case("x += 1"); - let scope = global_scope(&db, python_file(&db, file)); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(names(global_table), vec!["x"]); @@ -1270,12 +1271,12 @@ class C: y = 2 ", ); - let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); assert_eq!(names(global_table), vec!["C", "y"]); - let module = parsed_module(&db, python_file(&db, file)).load(&db); - let index = semantic_index(&db, python_file(&db, file)); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let [(class_scope_id, class_scope)] = index .child_scopes(FileScopeId::global()) @@ -1286,7 +1287,7 @@ y = 2 assert_eq!(class_scope.kind(), ScopeKind::Class); assert_eq!( class_scope_id - .to_scope_id(&db, python_file(&db, file)) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "C" ); @@ -1310,8 +1311,8 @@ def func(): y = 2 ", ); - let module = parsed_module(&db, python_file(&db, file)).load(&db); - let index = semantic_index(&db, python_file(&db, file)); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["func", "y"]); @@ -1325,7 +1326,7 @@ y = 2 assert_eq!(function_scope.kind(), ScopeKind::Function); assert_eq!( function_scope_id - .to_scope_id(&db, python_file(&db, file)) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "func" ); @@ -1349,8 +1350,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ", ); - let index = semantic_index(&db, python_file(&db, file)); - let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); + let index = semantic_index(&db, program_file(&db, file)); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); assert_eq!(names(global_table), vec!["str", "int", "f"]); @@ -1394,8 +1395,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): fn lambda_parameter_symbols() { let TestCase { db, file } = test_case("lambda a, b, c=1, *args, d=2, **kwargs: None"); - let index = semantic_index(&db, python_file(&db, file)); - let global_table = place_table(&db, global_scope(&db, python_file(&db, file))); + let index = semantic_index(&db, program_file(&db, file)); + let global_table = place_table(&db, global_scope(&db, program_file(&db, file))); assert!(names(global_table).is_empty()); @@ -1460,8 +1461,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ", ); - let module = parsed_module(&db, python_file(&db, file)).load(&db); - let index = semantic_index(&db, python_file(&db, file)); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["iter1"]); @@ -1476,7 +1477,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension); assert_eq!( comprehension_scope_id - .to_scope_id(&db, python_file(&db, file)) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "" ); @@ -1511,7 +1512,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ", ); - let index = semantic_index(&db, python_file(&db, file)); + let index = semantic_index(&db, program_file(&db, file)); let [(comprehension_scope_id, _)] = index .child_scopes(FileScopeId::global()) .collect::>()[..] @@ -1521,7 +1522,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): let use_def = index.use_def_map(comprehension_scope_id); - let module = parsed_module(&db, python_file(&db, file)).load(&db); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); let syntax = module.syntax(); let element = syntax.body[0] .as_expr_stmt() @@ -1532,7 +1533,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): .elt .as_name_expr() .unwrap(); - let element_use_id = element.scoped_use_id(&db, python_file(&db, file)); + let element_use_id = element.scoped_use_id(&db, program_file(&db, file)); let binding = use_def.first_binding_at_use(element_use_id).unwrap(); let DefinitionKind::Comprehension(comprehension) = binding.kind(&db) else { @@ -1556,8 +1557,8 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): ", ); - let module = parsed_module(&db, python_file(&db, file)).load(&db); - let index = semantic_index(&db, python_file(&db, file)); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["iter1"]); @@ -1572,7 +1573,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension); assert_eq!( comprehension_scope_id - .to_scope_id(&db, python_file(&db, file)) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "" ); @@ -1591,7 +1592,7 @@ def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs): assert_eq!(inner_comprehension_scope.kind(), ScopeKind::Comprehension); assert_eq!( inner_comprehension_scope_id - .to_scope_id(&db, python_file(&db, file)) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "" ); @@ -1610,7 +1611,7 @@ with item1 as x, item2 as y: ", ); - let index = semantic_index(&db, python_file(&db, file)); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["item1", "x", "item2", "y"]); @@ -1633,7 +1634,7 @@ with context() as (x, y): ", ); - let index = semantic_index(&db, python_file(&db, file)); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["context", "x", "y"]); @@ -1657,8 +1658,8 @@ def func(): y = 2 ", ); - let module = parsed_module(&db, python_file(&db, file)).load(&db); - let index = semantic_index(&db, python_file(&db, file)); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["func"]); @@ -1676,14 +1677,14 @@ def func(): assert_eq!( func_scope1_id - .to_scope_id(&db, python_file(&db, file)) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "func" ); assert_eq!(func_scope_2.kind(), ScopeKind::Function); assert_eq!( func_scope2_id - .to_scope_id(&db, python_file(&db, file)) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "func" ); @@ -1709,8 +1710,8 @@ def func[T](): ", ); - let module = parsed_module(&db, python_file(&db, file)).load(&db); - let index = semantic_index(&db, python_file(&db, file)); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["func"]); @@ -1725,7 +1726,7 @@ def func[T](): assert_eq!(ann_scope.kind(), ScopeKind::TypeParams); assert_eq!( ann_scope_id - .to_scope_id(&db, python_file(&db, file)) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "func" ); @@ -1740,7 +1741,7 @@ def func[T](): assert_eq!(func_scope.kind(), ScopeKind::Function); assert_eq!( func_scope_id - .to_scope_id(&db, python_file(&db, file)) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "func" ); @@ -1757,8 +1758,8 @@ class C[T]: ", ); - let module = parsed_module(&db, python_file(&db, file)).load(&db); - let index = semantic_index(&db, python_file(&db, file)); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let global_table = index.place_table(FileScopeId::global()); assert_eq!(names(global_table), vec!["C"]); @@ -1773,7 +1774,7 @@ class C[T]: assert_eq!(ann_scope.kind(), ScopeKind::TypeParams); assert_eq!( ann_scope_id - .to_scope_id(&db, python_file(&db, file)) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "C" ); @@ -1795,7 +1796,7 @@ class C[T]: assert_eq!(class_scope.kind(), ScopeKind::Class); assert_eq!( class_scope_id - .to_scope_id(&db, python_file(&db, file)) + .to_scope_id(&db, program_file(&db, file)) .name(&db, &module), "C" ); @@ -1805,8 +1806,8 @@ class C[T]: #[test] fn reachability_trivial() { let TestCase { db, file } = test_case("x = 1; x"); - let module = parsed_module(&db, python_file(&db, file)).load(&db); - let scope = global_scope(&db, python_file(&db, file)); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let scope = global_scope(&db, program_file(&db, file)); let ast = module.syntax(); let ast::Stmt::Expr(ast::StmtExpr { value: x_use_expr, .. @@ -1817,7 +1818,7 @@ class C[T]: let ast::Expr::Name(x_use_expr_name) = x_use_expr.as_ref() else { panic!("expected a Name"); }; - let x_use_id = x_use_expr_name.scoped_use_id(&db, python_file(&db, file)); + let x_use_id = x_use_expr_name.scoped_use_id(&db, program_file(&db, file)); let use_def = use_def_map(&db, scope); let binding = use_def.first_binding_at_use(x_use_id).unwrap(); let DefinitionKind::Assignment(assignment) = binding.kind(&db) else { @@ -1837,8 +1838,8 @@ class C[T]: fn expression_scope() { let TestCase { db, file } = test_case("x = 1;\ndef test():\n y = 4"); - let index = semantic_index(&db, python_file(&db, file)); - let module = parsed_module(&db, python_file(&db, file)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); let ast = module.syntax(); let x_stmt = ast.body[0].as_assign_stmt().unwrap(); @@ -1866,10 +1867,7 @@ class C[T]: .into_iter() .map(|(scope_id, _)| { scope_id - .to_scope_id( - db, - PythonFile::new(db, file, Program::get(db).python_version(db)), - ) + .to_scope_id(db, Program::get(db).program_file(db, file)) .name(db, module) }) .collect() @@ -1888,8 +1886,8 @@ def x(): pass", ); - let module = parsed_module(&db, python_file(&db, file)).load(&db); - let index = semantic_index(&db, python_file(&db, file)); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); + let index = semantic_index(&db, program_file(&db, file)); let descendants = index.descendent_scopes(FileScopeId::global()); assert_eq!( @@ -1935,7 +1933,7 @@ match subject: ", ); - let global_scope_id = global_scope(&db, python_file(&db, file)); + let global_scope_id = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, global_scope_id); assert!(global_table.symbol_by_name("Foo").unwrap().is_used()); @@ -1967,7 +1965,7 @@ match 1: ", ); - let global_scope_id = global_scope(&db, python_file(&db, file)); + let global_scope_id = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, global_scope_id); assert_eq!(names(global_table), vec!["first", "second"]); @@ -1984,7 +1982,7 @@ match 1: #[test] fn for_loops_single_assignment() { let TestCase { db, file } = test_case("for x in a: pass"); - let scope = global_scope(&db, python_file(&db, file)); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(&names(global_table), &["a", "x"]); @@ -2000,7 +1998,7 @@ match 1: #[test] fn for_loops_simple_unpacking() { let TestCase { db, file } = test_case("for (x, y) in a: pass"); - let scope = global_scope(&db, python_file(&db, file)); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(&names(global_table), &["a", "x", "y"]); @@ -2020,7 +2018,7 @@ match 1: #[test] fn for_loops_complex_unpacking() { let TestCase { db, file } = test_case("for [((a,) b), (c, d)] in e: pass"); - let scope = global_scope(&db, python_file(&db, file)); + let scope = global_scope(&db, program_file(&db, file)); let global_table = place_table(&db, scope); assert_eq!(&names(global_table), &["e", "a", "b", "c", "d"]); diff --git a/crates/ty_python_core/src/predicate.rs b/crates/ty_python_core/src/predicate.rs index ff2de40048..4241e92a95 100644 --- a/crates/ty_python_core/src/predicate.rs +++ b/crates/ty_python_core/src/predicate.rs @@ -13,6 +13,7 @@ use ruff_db::files::File; use ruff_index::{FrozenIndexVec, Idx, IndexVec}; use ruff_python_ast::{Singleton, name::Name}; +use crate::ProgramFile; use crate::ast_ids::ExpressionNodeKey; use crate::db::Db; use crate::expression::Expression; @@ -233,7 +234,7 @@ pub enum PatternPredicateKind<'db> { #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] pub struct PatternPredicate<'db> { #[returns(copy)] - pub python_file: PythonFile<'db>, + pub program_file: ProgramFile<'db>, #[returns(copy)] pub file_scope: FileScopeId, @@ -257,14 +258,18 @@ impl get_size2::GetSize for PatternPredicate<'_> {} impl<'db> PatternPredicate<'db> { pub fn file(self, db: &'db dyn Db) -> File { - self.python_file(db).file(db) + self.program_file(db).file(db) + } + + pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.program_file(db).python_file(db) } pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.file_scope(db).to_scope_id(db, self.python_file(db)) + self.file_scope(db).to_scope_id(db, self.program_file(db)) } - pub fn program(self, db: &'db dyn Db) -> Program { + pub fn program(self, db: &'db dyn Db) -> Program<'db> { self.scope(db).program(db) } } @@ -312,7 +317,7 @@ impl<'db> PatternPredicate<'db> { #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] pub struct StarImportPlaceholderPredicate<'db> { #[returns(copy)] - pub importing_parse_file: PythonFile<'db>, + pub importing_file: ProgramFile<'db>, /// Each symbol imported by a `*` import has a separate predicate associated with it: /// this field identifies which symbol that is. @@ -327,7 +332,7 @@ pub struct StarImportPlaceholderPredicate<'db> { pub symbol_id: ScopedSymbolId, #[returns(copy)] - pub referenced_parse_file: PythonFile<'db>, + pub referenced_file: ProgramFile<'db>, } // The Salsa heap is tracked separately. @@ -337,7 +342,7 @@ impl<'db> StarImportPlaceholderPredicate<'db> { pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { // See doc-comment above [`StarImportPlaceholderPredicate::symbol_id`]: // valid `*`-import definitions can only take place in the global scope. - global_scope(db, self.importing_parse_file(db)) + global_scope(db, self.importing_file(db)) } } diff --git a/crates/ty_python_core/src/program.rs b/crates/ty_python_core/src/program.rs index a4c9660685..6934f23896 100644 --- a/crates/ty_python_core/src/program.rs +++ b/crates/ty_python_core/src/program.rs @@ -1,12 +1,15 @@ use crate::{Db, platform::PythonPlatform}; +use ruff_db::files::File; use ruff_db::system::SystemPath; use ruff_python_ast::PythonVersion; use salsa::Durability; use salsa::Setter; -use ty_module_resolver::SearchPaths; +use ty_module_resolver::{ResolverEnvironment, SearchPaths}; use ty_site_packages::PythonVersionWithSource; +use crate::ProgramFile; + // Re-export the misconfiguration strategy types from ty_module_resolver. pub use ty_module_resolver::{FallibleStrategy, MisconfigurationStrategy, UseDefaultStrategy}; @@ -22,6 +25,7 @@ pub struct Program { pub search_paths: SearchPaths, } +#[salsa::tracked] impl Program { pub fn init_or_update(db: &mut dyn Db, settings: ProgramSettings) -> Self { match Self::try_get(db) { @@ -51,6 +55,15 @@ impl Program { self.python_version_with_source(db).version } + /// Returns the module-resolution environment for this program. + pub fn resolver_environment(self, db: &dyn Db) -> ResolverEnvironment<'_> { + ResolverEnvironment::new(db, self.python_version(db), self.search_paths(db)) + } + + pub fn program_file(self, db: &dyn Db, file: File) -> ProgramFile<'_> { + ProgramFile::new(db, file, self.resolver_environment(db)) + } + pub fn update_from_settings(self, db: &mut dyn Db, settings: ProgramSettings) { let ProgramSettings { python_version, diff --git a/crates/ty_python_core/src/program_file.rs b/crates/ty_python_core/src/program_file.rs new file mode 100644 index 0000000000..a232e82cf1 --- /dev/null +++ b/crates/ty_python_core/src/program_file.rs @@ -0,0 +1,96 @@ +use ruff_db::PythonFile; +use ruff_db::files::File; +use ruff_python_ast::PythonVersion; +use ty_module_resolver::{ResolverEnvironment, ResolverFile}; + +use crate::{Db, Program}; + +/// A file interpreted within a particular Python program. +/// +/// The same file can participate in multiple programs, each with different Python versions, search +/// paths, or other settings that affect type inference. +/// +/// For example: +/// +/// ```text +/// project/ +/// ├── app.py # Project program: Python 3.11 +/// ├── generate.py # Script program: Python 3.12 +/// └── shared.py # Imported by both +/// ``` +/// +/// In `shared.py`, version-dependent code can produce different types: +/// +/// ```python +/// import sys +/// +/// if sys.version_info >= (3, 12): +/// value = 1 +/// else: +/// value = "one" +/// ``` +/// +/// The two interpretations therefore need separate semantic identities: +/// +/// ```text +/// ProgramFile(shared.py, project program) -> value: str +/// ProgramFile(shared.py, script program) -> value: int +/// ``` +/// +/// Semantic queries, such as `semantic_index`, use `ProgramFile` to avoid sharing results between +/// incompatible programs. Lower-level operations use narrower identities where possible: +/// +/// ```text +/// program_file.python_file(db) -> File + Python version +/// program_file.resolver_file(db) -> File + resolver environment +/// ``` +/// +/// This allows programs with the same Python version to share parsed syntax, and programs with +/// equivalent resolver environments to share module resolution, while keeping type inference +/// isolated. +#[salsa::interned( + debug, + constructor = new_internal, + heap_size = ruff_memory_usage::heap_size +)] +pub struct ProgramFile<'db> { + /// The cached parser key for `file` and the environment's Python version. + #[returns(copy)] + pub python_file: PythonFile<'db>, + + #[returns(copy)] + pub resolver_environment: ResolverEnvironment<'db>, +} + +impl get_size2::GetSize for ProgramFile<'_> {} + +impl<'db> ProgramFile<'db> { + pub fn new( + db: &'db dyn Db, + file: File, + resolver_environment: ResolverEnvironment<'db>, + ) -> Self { + let python_file = PythonFile::new(db, file, resolver_environment.python_version(db)); + Self::new_internal(db, python_file, resolver_environment) + } + + /// Returns the physical file represented by this program file. + pub fn file(self, db: &'db dyn Db) -> File { + self.python_file(db).file(db) + } + + /// Returns the resolver key for this file. + pub fn resolver_file(self, db: &'db dyn Db) -> ResolverFile<'db> { + ResolverFile::new(db, self.file(db), self.resolver_environment(db)) + } + + /// Returns the program associated with this file. + pub fn program(self, db: &'db dyn Db) -> Program<'db> { + self.resolver_environment(db) + } + + /// Returns the Python version associated with this file's resolver environment. + pub fn python_version(self, db: &'db dyn Db) -> PythonVersion { + self.resolver_environment(db).python_version(db) + } +} diff --git a/crates/ty_python_core/src/re_exports.rs b/crates/ty_python_core/src/re_exports.rs index 7194008de8..2282ff0f4d 100644 --- a/crates/ty_python_core/src/re_exports.rs +++ b/crates/ty_python_core/src/re_exports.rs @@ -22,24 +22,23 @@ use ruff_db::parsed::parsed_module; -use ruff_db::PythonFile; use ruff_python_ast::{ self as ast, name::Name, visitor::{Visitor, walk_expr, walk_pattern, walk_stmt}, }; use rustc_hash::FxHashMap; -use ty_module_resolver::{ModuleName, resolve_module}; +use ty_module_resolver::{ImportingFile, ModuleName, resolve_module}; -use crate::Db; +use crate::{Db, ProgramFile}; #[salsa::tracked( returns(deref), cycle_initial=|_, _, _| Box::default(), heap_size=ruff_memory_usage::heap_size) ] -pub(super) fn exported_names(db: &dyn Db, file: PythonFile<'_>) -> Box<[Name]> { - let module = parsed_module(db, file).load(db); +pub(super) fn exported_names(db: &dyn Db, file: ProgramFile<'_>) -> Box<[Name]> { + let module = parsed_module(db, file.python_file(db)).load(db); let mut finder = ExportFinder::new(db, file); finder.visit_body(module.suite()); @@ -53,17 +52,17 @@ pub(super) fn exported_names(db: &dyn Db, file: PythonFile<'_>) -> Box<[Name]> { struct ExportFinder<'db> { db: &'db dyn Db, - file: PythonFile<'db>, + program_file: ProgramFile<'db>, visiting_stub_file: bool, exports: FxHashMap<&'db Name, PossibleExportKind>, dunder_all: DunderAll, } impl<'db> ExportFinder<'db> { - fn new(db: &'db dyn Db, file: PythonFile<'db>) -> Self { + fn new(db: &'db dyn Db, file: ProgramFile<'db>) -> Self { Self { db, - file, + program_file: file, visiting_stub_file: file.file(db).is_stub(db), exports: FxHashMap::default(), dunder_all: DunderAll::NotPresent, @@ -250,20 +249,35 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { if &name.name.id == "*" { if !found_star { found_star = true; - for export in - ModuleName::from_import_statement(self.db, self.file, node) - .ok() - .and_then(|module_name| { - resolve_module(self.db, self.file, &module_name) + let db = self.db; + let program_file = self.program_file; + let file = program_file.file(db); + let resolver_environment = program_file.resolver_environment(db); + for export in ModuleName::from_import_statement( + db, + ImportingFile::File(file, resolver_environment), + node, + ) + .ok() + .and_then(|module_name| { + resolve_module( + db, + ImportingFile::File(file, resolver_environment), + &module_name, + ) + }) + .iter() + .flat_map(|module| { + module + .file(db) + .map(|file| { + exported_names( + db, + ProgramFile::new(db, file, program_file.program(db)), + ) }) - .iter() - .flat_map(|module| { - module - .python_file(self.db) - .map(|file| exported_names(self.db, file)) - .unwrap_or_default() - }) - { + .unwrap_or_default() + }) { self.possibly_add_export(export, PossibleExportKind::Normal); } } diff --git a/crates/ty_python_core/src/scope.rs b/crates/ty_python_core/src/scope.rs index bfb8c85721..ca6b5181be 100644 --- a/crates/ty_python_core/src/scope.rs +++ b/crates/ty_python_core/src/scope.rs @@ -5,7 +5,7 @@ use ruff_index::newtype_index; use ruff_python_ast::{self as ast, NodeIndex}; use crate::{ - Db, Program, SemanticIndex, ast_node_ref::AstNodeRef, definition::Definition, + Db, Program, ProgramFile, SemanticIndex, ast_node_ref::AstNodeRef, definition::Definition, node_key::NodeKey, semantic_index, }; @@ -13,7 +13,7 @@ use crate::{ #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] pub struct ScopeId<'db> { #[returns(copy)] - pub python_file: PythonFile<'db>, + pub program_file: ProgramFile<'db>, #[returns(copy)] pub file_scope_id: FileScopeId, @@ -24,11 +24,15 @@ impl get_size2::GetSize for ScopeId<'_> {} impl<'db> ScopeId<'db> { pub fn file(self, db: &dyn Db) -> File { - self.python_file(db).file(db) + self.program_file(db).file(db) } - pub fn program(self, db: &dyn Db) -> Program { - self.python_file(db).python_version(db) + pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.program_file(db).python_file(db) + } + + pub fn program(self, db: &'db dyn Db) -> Program<'db> { + self.program_file(db).program(db) } pub fn is_annotation(self, db: &'db dyn Db) -> bool { @@ -52,12 +56,12 @@ impl<'db> ScopeId<'db> { } pub fn scope(self, db: &'db dyn Db) -> &'db Scope { - semantic_index(db, self.python_file(db)).scope(self.file_scope_id(db)) + semantic_index(db, self.program_file(db)).scope(self.file_scope_id(db)) } /// Returns the class definition for the enclosing class if this scope is a method body. pub fn class_definition_of_method(self, db: &'db dyn Db) -> Option> { - semantic_index(db, self.python_file(db)).class_definition_of_method(self.file_scope_id(db)) + semantic_index(db, self.program_file(db)).class_definition_of_method(self.file_scope_id(db)) } pub fn is_method_scope(self, db: &'db dyn Db) -> bool { @@ -105,7 +109,7 @@ impl FileScopeId { self == FileScopeId::global() } - pub fn to_scope_id<'db>(self, db: &'db dyn Db, file: PythonFile<'db>) -> ScopeId<'db> { + pub fn to_scope_id<'db>(self, db: &'db dyn Db, file: ProgramFile<'db>) -> ScopeId<'db> { let index = semantic_index(db, file); index.scope_ids_by_scope[self] } diff --git a/crates/ty_python_core/src/statement.rs b/crates/ty_python_core/src/statement.rs index 25d392ea26..eccdfc3c65 100644 --- a/crates/ty_python_core/src/statement.rs +++ b/crates/ty_python_core/src/statement.rs @@ -1,10 +1,10 @@ -use crate::Program; use crate::ast_node_ref::AstNodeRef; use crate::db::Db; use crate::definition::Definition; use crate::expression::Expression; use crate::node_key::NodeKey; use crate::scope::{FileScopeId, ScopeId}; +use crate::{Program, ProgramFile}; use ruff_db::PythonFile; use ruff_db::files::File; use ruff_python_ast as ast; @@ -40,7 +40,7 @@ pub enum Statement<'db> { pub struct StatementInner<'db> { /// The file in which the statement occurs. #[returns(copy)] - pub python_file: PythonFile<'db>, + pub program_file: ProgramFile<'db>, /// The scope in which the statement occurs. #[returns(copy)] @@ -58,14 +58,18 @@ impl get_size2::GetSize for StatementInner<'_> {} impl<'db> StatementInner<'db> { pub fn file(self, db: &'db dyn Db) -> File { - self.python_file(db).file(db) + self.program_file(db).file(db) + } + + pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.program_file(db).python_file(db) } pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> { - self.file_scope(db).to_scope_id(db, self.python_file(db)) + self.file_scope(db).to_scope_id(db, self.program_file(db)) } - pub fn program(self, db: &'db dyn Db) -> Program { + pub fn program(self, db: &'db dyn Db) -> Program<'db> { self.scope(db).program(db) } } diff --git a/crates/ty_python_core/src/unpack.rs b/crates/ty_python_core/src/unpack.rs index ed422fe18a..bb7ce5437f 100644 --- a/crates/ty_python_core/src/unpack.rs +++ b/crates/ty_python_core/src/unpack.rs @@ -7,6 +7,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::Db; use crate::EvaluationMode; +use crate::ProgramFile; use crate::ast_node_ref::AstNodeRef; use crate::expression::Expression; use crate::scope::{FileScopeId, ScopeId}; @@ -32,7 +33,7 @@ use crate::scope::{FileScopeId, ScopeId}; #[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)] pub struct Unpack<'db> { #[returns(copy)] - pub python_file: PythonFile<'db>, + pub program_file: ProgramFile<'db>, #[returns(copy)] pub(crate) value_file_scope: FileScopeId, @@ -58,7 +59,11 @@ impl get_size2::GetSize for Unpack<'_> {} impl<'db> Unpack<'db> { pub fn file(self, db: &'db dyn Db) -> File { - self.python_file(db).file(db) + self.program_file(db).file(db) + } + + pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + self.program_file(db).python_file(db) } pub fn target<'ast>(self, db: &'db dyn Db, parsed: &'ast ParsedModuleRef) -> &'ast ast::Expr { @@ -68,10 +73,10 @@ impl<'db> Unpack<'db> { /// Returns the scope where the unpack target expression belongs to. pub fn target_scope(self, db: &'db dyn Db) -> ScopeId<'db> { self.target_file_scope(db) - .to_scope_id(db, self.python_file(db)) + .to_scope_id(db, self.program_file(db)) } - pub fn program(self, db: &'db dyn Db) -> Program { + pub fn program(self, db: &'db dyn Db) -> Program<'db> { self.target_scope(db).program(db) } diff --git a/crates/ty_python_semantic/src/db.rs b/crates/ty_python_semantic/src/db.rs index 2ee25ab997..1d3044b0b4 100644 --- a/crates/ty_python_semantic/src/db.rs +++ b/crates/ty_python_semantic/src/db.rs @@ -2,13 +2,16 @@ use crate::AnalysisSettings; use crate::lint::{LintRegistry, RuleSelection}; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::File; -use ty_python_core::Db as PythonCoreDb; +use ty_python_core::{Db as PythonCoreDb, ProgramFile}; /// Database giving access to semantic information about a Python program. #[salsa::db] pub trait Db: PythonCoreDb { fn check_file(&self, file: File) -> Vec; + /// Returns the program file for `file`. + fn program_file(&self, file: File) -> ProgramFile<'_>; + /// Resolves the rule selection for a given file. fn rule_selection(&self, file: File) -> &RuleSelection; @@ -37,14 +40,14 @@ pub(crate) mod tests { use ty_python_core::platform::PythonPlatform; use crate::{ProgramEnvironment, check_file_unwrap, default_lint_registry}; + use ruff_db::Db as SourceDb; use ruff_db::files::Files; use ruff_db::system::{ DbWithTestSystem, DbWithWritableSystem as _, System, SystemPath, SystemPathBuf, TestSystem, }; use ruff_db::vendored::VendoredFileSystem; - use ruff_db::{Db as SourceDb, PythonFile}; use ruff_python_ast::PythonVersion; - use ty_module_resolver::{Db as ModuleResolverDb, SearchPathSettings, SearchPaths}; + use ty_module_resolver::{Db as ModuleResolverDb, SearchPathSettings}; use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; @@ -90,7 +93,7 @@ pub(crate) mod tests { } pub(crate) fn program_environment(&self) -> ProgramEnvironment<'_> { - ProgramEnvironment::from_program(self.python_version()) + ProgramEnvironment::from_program(Program::get(self).resolver_environment(self)) } /// Marks `file` as open in the editor. @@ -155,7 +158,11 @@ pub(crate) mod tests { return Vec::new(); } - check_file_unwrap(self, PythonFile::new(self, file, self.python_version())) + check_file_unwrap(self, self.program_file(file)) + } + + fn program_file(&self, file: File) -> ProgramFile<'_> { + Program::get(self).program_file(self, file) } fn rule_selection(&self, _file: File) -> &RuleSelection { @@ -184,11 +191,7 @@ pub(crate) mod tests { } #[salsa::db] - impl ModuleResolverDb for TestDb { - fn search_paths(&self) -> &SearchPaths { - Program::get(self).search_paths(self) - } - } + impl ModuleResolverDb for TestDb {} #[salsa::db] impl salsa::Database for TestDb {} diff --git a/crates/ty_python_semantic/src/dunder_all.rs b/crates/ty_python_semantic/src/dunder_all.rs index 150e781e74..f761bd3cf0 100644 --- a/crates/ty_python_semantic/src/dunder_all.rs +++ b/crates/ty_python_semantic/src/dunder_all.rs @@ -1,23 +1,22 @@ -use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_python_ast::{self as ast}; use rustc_hash::FxHashSet; -use ty_module_resolver::{ModuleName, resolve_module}; +use ty_module_resolver::{ImportingFile, ModuleName, resolve_module}; use crate::types::{Type, TypeContext, infer_expression_types}; use crate::{Db, ProgramEnvironment}; -use ty_python_core::{SemanticIndex, Truthiness, semantic_index}; +use ty_python_core::{ProgramFile, SemanticIndex, Truthiness, semantic_index}; /// Returns a set of names in the `__all__` variable for `file`, [`None`] if it is not defined or /// if it contains invalid elements. #[salsa::tracked(returns(as_ref), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] -pub(crate) fn dunder_all_names(db: &dyn Db, file: PythonFile<'_>) -> Option> { +pub(crate) fn dunder_all_names(db: &dyn Db, file: ProgramFile<'_>) -> Option> { let source_file = file.file(db); let _span = tracing::trace_span!("dunder_all_names", file=?source_file.path(db)).entered(); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let index = semantic_index(db, file); let mut collector = DunderAllNamesCollector::new(db, file, index); collector.visit_body(module.suite()); @@ -28,7 +27,7 @@ pub(crate) fn dunder_all_names(db: &dyn Db, file: PythonFile<'_>) -> Option { db: &'db dyn Db, env: ProgramEnvironment<'db>, - file: PythonFile<'db>, + file: ProgramFile<'db>, /// The semantic index for the module. index: &'db SemanticIndex<'db>, @@ -45,7 +44,7 @@ struct DunderAllNamesCollector<'db> { } impl<'db> DunderAllNamesCollector<'db> { - fn new(db: &'db dyn Db, file: PythonFile<'db>, index: &'db SemanticIndex<'db>) -> Self { + fn new(db: &'db dyn Db, file: ProgramFile<'db>, index: &'db SemanticIndex<'db>) -> Self { Self { db, env: ProgramEnvironment::from_file(file), @@ -94,7 +93,8 @@ impl<'db> DunderAllNamesCollector<'db> { }; let Some(module_dunder_all_names) = module_literal .module(db) - .python_file(db) + .file(db) + .map(|file| ProgramFile::new(db, file, self.env.program(db))) .and_then(|file| dunder_all_names(db, file)) else { // The module either does not have a `__all__` variable or it is invalid. @@ -163,9 +163,15 @@ impl<'db> DunderAllNamesCollector<'db> { ) -> Option<&'db FxHashSet> { let db = self.db; - let module_name = ModuleName::from_import_statement(db, self.file, import_from).ok()?; - let module = resolve_module(db, self.file, &module_name)?; - dunder_all_names(db, module.python_file(db)?) + let importing_file = + ImportingFile::File(self.file.file(db), self.env.resolver_environment(db)); + let module_name = + ModuleName::from_import_statement(db, importing_file, import_from).ok()?; + let module = resolve_module(db, importing_file, &module_name)?; + dunder_all_names( + db, + ProgramFile::new(db, module.file(db)?, self.env.program(db)), + ) } /// Infer the type of a standalone expression. diff --git a/crates/ty_python_semantic/src/fixes.rs b/crates/ty_python_semantic/src/fixes.rs index 03a3a93086..bdb82dbac8 100644 --- a/crates/ty_python_semantic/src/fixes.rs +++ b/crates/ty_python_semantic/src/fixes.rs @@ -11,7 +11,6 @@ use ruff_db::{ source::source_text, }; use ruff_diagnostics::{Applicability, Edit, Fix, IsolationLevel, SourceMap}; -use ruff_python_ast::PythonVersion; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use rustc_hash::{FxHashMap, FxHashSet}; use salsa::Setter as _; @@ -39,13 +38,11 @@ pub struct FixAllResults { /// If the `db`'s system isn't [writable](WritableSystem). pub fn suppress_all_diagnostics( db: &mut dyn Db, - python_version: PythonVersion, diagnostics: Vec, cancellation_token: &CancellationToken, ) -> Result { fix_all( db, - python_version, diagnostics, FixMode::Suppress, cancellation_token, @@ -61,14 +58,12 @@ pub fn suppress_all_diagnostics( /// If the `db`'s system isn't [writable](WritableSystem). pub fn fix_all_diagnostics( db: &mut dyn Db, - python_version: PythonVersion, diagnostics: Vec, applicability: Applicability, cancellation_token: &CancellationToken, ) -> Result { fix_all( db, - python_version, diagnostics, FixMode::ApplyFixes(applicability), cancellation_token, @@ -83,7 +78,6 @@ const MAX_ITERATIONS: usize = 10; /// `check_file` is a separate parameter so that tests can easily mock out a file's diagnostics. fn fix_all( db: &mut dyn Db, - python_version: PythonVersion, mut diagnostics: Vec, fix_mode: FixMode, cancellation_token: &CancellationToken, @@ -135,7 +129,7 @@ where continue; }; - let python_file = PythonFile::new(db, file, python_version); + let python_file = db.program_file(file).python_file(db); let parsed = parsed_module(db, python_file); if parsed.load(db).has_syntax_errors() { tracing::warn!("Skipping file `{path}` with syntax errors"); @@ -184,7 +178,6 @@ where // This is done outside the above loop so that it can run in parallel. let check_results = recheck_files( &*db, - python_version, unstaged_fixes, fix_mode, cancellation_token, @@ -757,7 +750,6 @@ enum CheckResult<'a> { fn recheck_files<'a, F>( db: &dyn Db, - python_version: PythonVersion, changes: Vec<(QueuedFile<'a>, usize)>, fix_mode: FixMode, cancellation_token: &CancellationToken, @@ -783,7 +775,7 @@ where let db = &*db; - let python_file = PythonFile::new(db, file.file, python_version); + let python_file = db.program_file(file.file).python_file(db); let parsed = parsed_module(db, python_file); let parsed = parsed.load(db); @@ -814,7 +806,6 @@ where #[cfg(test)] mod tests { use insta::assert_snapshot; - use ruff_db::PythonFile; use ruff_db::cancellation::CancellationTokenSource; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, @@ -1725,12 +1716,10 @@ class B(A): }; let initial_diagnostics = check_file(&db, file); - let python_version = db.python_version(); let cancellation_token_source = CancellationTokenSource::new(); let fixes = fix_all( &mut db, - python_version, initial_diagnostics, FixMode::ApplyFixes(Applicability::Safe), &cancellation_token_source.token(), @@ -1804,12 +1793,10 @@ class B(A): }; let initial_diagnostics = check_file(&db, file); - let python_version = db.python_version(); let cancellation_token_source = CancellationTokenSource::new(); let fixes = fix_all( &mut db, - python_version, initial_diagnostics, FixMode::ApplyFixes(Applicability::Safe), &cancellation_token_source.token(), @@ -1881,10 +1868,8 @@ class B(A): create_diagnostics(file) }; - let python_version = db.python_version(); let result = fix_all( &mut db, - python_version, initial_diagnostics, FixMode::ApplyFixes(Applicability::Safe), &cancellation_token_source.token(), @@ -1983,12 +1968,10 @@ class B(A): }; let initial_diagnostics = check_file(&db, file); - let python_version = db.python_version(); let cancellation_token_source = CancellationTokenSource::new(); let fixes = fix_all( &mut db, - python_version, initial_diagnostics, FixMode::ApplyFixes(Applicability::Safe), &cancellation_token_source.token(), @@ -2020,8 +2003,7 @@ class B(A): let file = system_path_to_file(&db, "test.py").unwrap(); - let python_version = db.python_version(); - let parsed_before = parsed_module(&db, PythonFile::new(&db, file, python_version)); + let parsed_before = parsed_module(&db, db.program_file(file).python_file(&db)); let had_syntax_errors = parsed_before.load(&db).has_syntax_errors(); let diagnostics = db.check_file(file); @@ -2036,13 +2018,9 @@ class B(A): .cloned() .collect(); let cancellation_token_source = CancellationTokenSource::new(); - let fixes = suppress_all_diagnostics( - &mut db, - python_version, - diagnostics, - &cancellation_token_source.token(), - ) - .expect("operation never gets cancelled"); + let fixes = + suppress_all_diagnostics(&mut db, diagnostics, &cancellation_token_source.token()) + .expect("operation never gets cancelled"); if had_syntax_errors { assert_eq!(fixes.count, 0); @@ -2076,7 +2054,7 @@ class B(A): let fixed = source_text(&db, file); - let parsed = parsed_module(&db, PythonFile::new(&db, file, python_version)); + let parsed = parsed_module(&db, db.program_file(file).python_file(&db)); let parsed = parsed.load(&db); let diagnostics_after_applying_fixes = db.check_file(file); diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 3b2f48a91d..417b52521e 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -29,6 +29,7 @@ pub(crate) use suppression::{ }; use ty_module_resolver::ModuleGlobSet; pub use ty_python_core::Program; +use ty_python_core::ProgramFile; use ty_python_core::definition::docstring_from_body; use ty_python_core::platform::PythonPlatform; use ty_python_core::scope::ScopeId; @@ -130,7 +131,7 @@ pub(crate) fn attribute_assignments<'db, 's>( class_body_scope: ScopeId<'db>, name: &'s str, ) -> impl Iterator, FileScopeId)> + use<'s, 'db> { - let index = semantic_index(db, class_body_scope.python_file(db)); + let index = semantic_index(db, class_body_scope.program_file(db)); attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| { let place_table = index.place_table(function_scope_id); @@ -150,7 +151,7 @@ pub(crate) fn attribute_declarations<'db, 's>( class_body_scope: ScopeId<'db>, name: &'s str, ) -> impl Iterator, FileScopeId)> + use<'s, 'db> { - let index = semantic_index(db, class_body_scope.python_file(db)); + let index = semantic_index(db, class_body_scope.program_file(db)); attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| { let place_table = index.place_table(function_scope_id); @@ -170,13 +171,13 @@ pub(crate) fn module_docstring(db: &dyn Db, file: PythonFile<'_>) -> Option) -> Vec { +pub fn check_file_unwrap(db: &dyn Db, file: ProgramFile<'_>) -> Vec { check_file(db, file) .map(<[ruff_db::diagnostic::Diagnostic]>::into_vec) .unwrap_or_else(|error| vec![error]) } -pub fn check_file(db: &dyn Db, file: PythonFile<'_>) -> Result, Diagnostic> { +pub fn check_file(db: &dyn Db, file: ProgramFile<'_>) -> Result, Diagnostic> { let source_file = file.file(db); let mut diagnostics: Vec = Vec::new(); @@ -191,7 +192,7 @@ pub fn check_file(db: &dyn Db, file: PythonFile<'_>) -> Result .to_diagnostic()); } - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, file.python_file(db)); let parsed_ref = parsed.load(db); diagnostics.extend( diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 1e6120f61a..a96115a544 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -1,6 +1,5 @@ use crate::ProgramEnvironment; use itertools::Either; -use ruff_db::PythonFile; use ruff_index::IndexSlice; use ruff_python_ast::PythonVersion; use ty_module_resolver::{ @@ -28,8 +27,8 @@ use ty_python_core::reachability_constraints::{ use ty_python_core::scope::ScopeId; use ty_python_core::{ BindingWithConstraints, BindingWithConstraintsIterator, BoundnessAnalysis, - DeclarationWithConstraint, DeclarationsIterator, Truthiness, global_scope, place_table, - use_def_map, + DeclarationWithConstraint, DeclarationsIterator, ProgramFile, Truthiness, global_scope, + place_table, use_def_map, }; pub(crate) use implicit_globals::{ @@ -487,7 +486,7 @@ pub(crate) fn symbol<'db>( /// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports). pub(crate) fn explicit_global_symbol<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { symbol_impl( @@ -509,7 +508,7 @@ pub(crate) fn explicit_global_symbol<'db>( #[allow(unused)] pub(crate) fn global_symbol<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { let env = ProgramEnvironment::from_file(file); @@ -527,12 +526,12 @@ pub(crate) fn global_symbol<'db>( pub(crate) fn imported_symbol<'db>( db: &'db dyn Db, env: &ProgramEnvironment<'db>, - file: Option>, + file: Option>, name: &str, requires_explicit_reexport: Option, ) -> PlaceAndQualifiers<'db> { if let Some(file) = file { - debug_assert_eq!(file.python_version(db), env.python_version(db)); + debug_assert_eq!(file.program(db), env.program(db)); } // If it's not found in the global scope, check if it's present as an instance on @@ -664,10 +663,10 @@ fn builtins_symbol_impl<'db>( symbol: &str, visibility: BuiltinVisibility, ) -> Option<(ScopeId<'db>, PlaceAndQualifiers<'db>)> { - let python_version = env.python_version(db); + let program = env.program(db); let resolver = |module: Module<'db>| { - let python_file = module.python_file(db)?; - let scope = global_scope(db, python_file); + let file = ProgramFile::new(db, module.file(db)?, program); + let scope = global_scope(db, file); let found_symbol = symbol_impl( db, scope, @@ -679,7 +678,7 @@ fn builtins_symbol_impl<'db>( // We're looking up in the builtins namespace and not the module, so we should // do the normal lookup in `types.ModuleType` and not the special one as in // `imported_symbol`. - module_type_implicit_global_symbol(db, python_file, symbol) + module_type_implicit_global_symbol(db, file, symbol) }); found_symbol.ignore_possibly_undefined()?; @@ -696,13 +695,12 @@ fn builtins_symbol_impl<'db>( // If this symbol is not present in project-level builtins, search in the default ones. resolve_module_confident( db, - python_version, + program, &ModuleName::new_static("__builtins__").unwrap(), ) .and_then(&resolver) .or_else(|| { - resolve_module_confident(db, python_version, &KnownModule::Builtins.name()) - .and_then(resolver) + resolve_module_confident(db, program, &KnownModule::Builtins.name()).and_then(resolver) }) } @@ -715,9 +713,9 @@ pub(crate) fn known_module_symbol<'db>( known_module: KnownModule, symbol: &str, ) -> PlaceAndQualifiers<'db> { - resolve_module_confident(db, env.python_version(db), &known_module.name()) + resolve_module_confident(db, env.resolver_environment(db), &known_module.name()) .and_then(|module| { - let file = module.python_file(db)?; + let file = ProgramFile::new(db, module.file(db)?, env.program(db)); Some(imported_symbol(db, env, Some(file), symbol, None)) }) .unwrap_or_default() @@ -766,8 +764,12 @@ fn core_module_scope<'db>( env: &ProgramEnvironment<'db>, core_module: KnownModule, ) -> Option> { - let module = resolve_module_confident(db, env.python_version(db), &core_module.name())?; - Some(global_scope(db, module.python_file(db)?)) + let program = env.program(db); + let module = resolve_module_confident(db, env.resolver_environment(db), &core_module.name())?; + Some(global_scope( + db, + ProgramFile::new(db, module.file(db)?, program), + )) } /// Infer the combined type from an iterator of bindings, and return it @@ -1412,7 +1414,7 @@ fn symbol_impl<'db>( let _span = tracing::trace_span!("symbol", ?name).entered(); let is_known_module = |known_module| { - file_to_module(db, scope.python_file(db)) + file_to_module(db, scope.program_file(db).resolver_file(db)) .is_some_and(|module| module.is_known(db, known_module)) }; @@ -2120,7 +2122,7 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { // At this point, the definition should either be an `import` or `from ... import` statement. // This is because the default value of `is_reexported` is `true` for any other kind of // definition. - let Some(all_names) = dunder_all_names(db, definition.python_file(db)) else { + let Some(all_names) = dunder_all_names(db, definition.program_file(db)) else { return false; }; let table = place_table(db, definition.scope(db)); @@ -2130,7 +2132,6 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool { } pub(crate) mod implicit_globals { - use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_python_ast::name::Name; @@ -2146,7 +2147,7 @@ pub(crate) mod implicit_globals { use ty_python_core::definition::{DefinitionKind, DefinitionState}; use ty_python_core::scope::{NodeWithScopeRef, ScopeId}; use ty_python_core::symbol::Symbol; - use ty_python_core::{place_table, semantic_index, use_def_map}; + use ty_python_core::{ProgramFile, place_table, semantic_index, use_def_map}; use super::{DefinedPlace, Place, core_module_scope, is_reexported, place_from_declarations}; @@ -2159,15 +2160,15 @@ pub(crate) mod implicit_globals { module_scope: ScopeId<'db>, name: &str, ) -> Option> { - let python_file = module_scope.python_file(db); - let file = python_file.file(db); + let program_file = module_scope.program_file(db); + let file = program_file.file(db); if !file.path(db).is_vendored_path() { return None; } let symbol_id = place_table(db, module_scope).symbol_id(name)?; let use_def = use_def_map(db, module_scope); - let module = parsed_module(db, python_file).load(db); - let index = semantic_index(db, python_file); + let module = parsed_module(db, program_file.python_file(db)).load(db); + let index = semantic_index(db, program_file); let mut body_scope = None; for binding in use_def.end_of_scope_symbol_bindings(symbol_id) { @@ -2187,7 +2188,7 @@ pub(crate) mod implicit_globals { }; let class_scope = index .node_scope(NodeWithScopeRef::Class(class.node(&module))) - .to_scope_id(db, python_file); + .to_scope_id(db, program_file); if body_scope.is_some_and(|body_scope| body_scope != class_scope) { return None; } @@ -2202,15 +2203,14 @@ pub(crate) mod implicit_globals { db: &'db dyn Db, env: &ProgramEnvironment<'db>, ) -> Option> { - module_type_body_scope_inner(db, env.program(db), ()) + module_type_body_scope_inner(db, env.program(db)) } #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] - fn module_type_body_scope_inner( - db: &dyn Db, - program: Program, - _: (), // FIXME: Remove once `Program` is a Salsa-interned struct. - ) -> Option> { + fn module_type_body_scope_inner<'db>( + db: &'db dyn Db, + program: Program<'db>, + ) -> Option> { let env = ProgramEnvironment::from_program(program); let module_scope = core_module_scope(db, &env, KnownModule::Types)?; try_vendored_class_scope(db, module_scope, "ModuleType").or_else(|| { @@ -2262,7 +2262,7 @@ pub(crate) mod implicit_globals { /// global scope if they're being imported **from a different file**. pub(crate) fn module_type_implicit_global_symbol<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { let env = ProgramEnvironment::from_file(file); @@ -2275,7 +2275,7 @@ pub(crate) mod implicit_globals { // We special-case `__doc__` because a module with a literal docstring has `__doc__` // set to that string at runtime. We only narrow when a docstring is present: `__doc__` // may be set dynamically, so we fall back to the typeshed's `str | None`. - "__doc__" if module_docstring(db, file).is_some() => { + "__doc__" if module_docstring(db, file.python_file(db)).is_some() => { // Docstrings are stripped in `-OO` optimized mode, but here we assume that the // existence of an actual docstring AND the usage of `__doc__` is reason enough to // believe that it will exist at runtime. @@ -2384,18 +2384,17 @@ pub(crate) mod implicit_globals { db: &'db dyn Db, env: &ProgramEnvironment<'db>, ) -> &'db [ast::name::Name] { - module_type_symbols_inner(db, env.program(db), ()) + module_type_symbols_inner(db, env.program(db)) } #[salsa::tracked( returns(deref), - cycle_initial=|_, _, _, ()| smallvec::SmallVec::default(), + cycle_initial=|_, _, _| smallvec::SmallVec::default(), heap_size=ruff_memory_usage::heap_size )] - fn module_type_symbols_inner( - db: &dyn Db, - program: Program, - _: (), // FIXME: Remove once `Program` is a Salsa-interned struct. + fn module_type_symbols_inner<'db>( + db: &'db dyn Db, + program: Program<'db>, ) -> smallvec::SmallVec<[ast::name::Name; 8]> { let env = ProgramEnvironment::from_program(program); let Some(module_type_scope) = module_type_body_scope(db, &env) else { @@ -2413,7 +2412,7 @@ pub(crate) mod implicit_globals { /// for the current module, not `str | None`). pub(crate) fn all_implicit_module_globals<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, ) -> impl Iterator)> + 'db { // Special-cased implicit globals that are not in `module_type_symbols` let special_cased = ["__builtins__", "__debug__", "__warningregistry__"] diff --git a/crates/ty_python_semantic/src/pull_types.rs b/crates/ty_python_semantic/src/pull_types.rs index 7970ea9b8c..ec2f4ad1a7 100644 --- a/crates/ty_python_semantic/src/pull_types.rs +++ b/crates/ty_python_semantic/src/pull_types.rs @@ -4,15 +4,16 @@ //! (Mdtest uses the `pull_types` function via the `ty_test` crate.) use crate::{Db, HasType, SemanticModel}; -use ruff_db::{PythonFile, parsed::parsed_module}; +use ruff_db::parsed::parsed_module; use ruff_python_ast::{ self as ast, visitor::source_order, visitor::source_order::SourceOrderVisitor, }; +use ty_python_core::ProgramFile; -pub fn pull_types(db: &dyn Db, file: PythonFile<'_>) { +pub fn pull_types(db: &dyn Db, file: ProgramFile<'_>) { let mut visitor = PullTypesVisitor::new(db, file); - let ast = parsed_module(db, file).load(db); + let ast = parsed_module(db, file.python_file(db)).load(db); visitor.visit_body(ast.suite()); } @@ -22,7 +23,7 @@ struct PullTypesVisitor<'db> { } impl<'db> PullTypesVisitor<'db> { - fn new(db: &'db dyn Db, file: PythonFile<'db>) -> Self { + fn new(db: &'db dyn Db, file: ProgramFile<'db>) -> Self { Self { model: SemanticModel::new(db, file), } diff --git a/crates/ty_python_semantic/src/reachability.rs b/crates/ty_python_semantic/src/reachability.rs index 6752797ad9..4635ae5b60 100644 --- a/crates/ty_python_semantic/src/reachability.rs +++ b/crates/ty_python_semantic/src/reachability.rs @@ -281,7 +281,7 @@ fn type_narrowed_by_pattern<'db>( predicate: PatternPredicate<'db>, subject_ty: Type<'db>, ) -> Type<'db> { - let env = ProgramEnvironment::from_file(predicate.python_file(db)); + let env = ProgramEnvironment::from_file(predicate.program_file(db)); pattern_binding_fallthrough_type(db, &env, predicate.kind(db), subject_ty) } @@ -1538,8 +1538,8 @@ fn analyze_single(db: &dyn Db, env: &ProgramEnvironment<'_>, predicate: &Predica PredicateNode::StarImportPlaceholder(star_import) => { let place_table = place_table(db, star_import.scope(db)); let symbol = place_table.symbol(star_import.symbol_id(db)); - let python_file = star_import.referenced_parse_file(db); - let requires_explicit_reexport = match dunder_all_names(db, python_file) { + let program_file = star_import.referenced_file(db); + let requires_explicit_reexport = match dunder_all_names(db, program_file) { Some(all_names) => { if all_names.contains(symbol.name()) { Some(RequiresExplicitReExport::No) @@ -1547,7 +1547,7 @@ fn analyze_single(db: &dyn Db, env: &ProgramEnvironment<'_>, predicate: &Predica tracing::trace!( "Symbol `{}` (via star import) not found in `__all__` of `{}`", symbol.name(), - python_file.file(db).path(db) + program_file.file(db).path(db) ); return Truthiness::AlwaysFalse; } @@ -1558,7 +1558,7 @@ fn analyze_single(db: &dyn Db, env: &ProgramEnvironment<'_>, predicate: &Predica match imported_symbol( db, env, - Some(python_file), + Some(program_file), symbol.name(), requires_explicit_reexport, ) @@ -1790,9 +1790,9 @@ impl<'db> DeclarationsIteratorExtension<'db> for DeclarationsIterator<'_, 'db> { mod tests { use super::*; use crate::db::tests::setup_db; - use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::system::DbWithWritableSystem as _; + use ty_python_core::ProgramFile; use ty_python_core::narrowing_constraints::InteriorNode; use ty_python_core::predicate::Predicates; use ty_python_core::semantic_index; @@ -1826,8 +1826,8 @@ class TargetB: db.write_files([("/src/a.py", a.as_str()), ("/src/b.py", b.as_str())])?; let file = system_path_to_file(&db, "/src/a.py").unwrap(); - let python_file = PythonFile::new(&db, file, db.python_version()); - let index = semantic_index(&db, python_file); + let program_file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let index = semantic_index(&db, program_file); let class_scope = index .child_scopes(FileScopeId::global()) .find(|(_, scope)| scope.node().as_class().is_some()) @@ -1838,7 +1838,7 @@ class TargetB: .find(|(_, scope)| scope.node().as_function().is_some()) .unwrap() .0 - .to_scope_id(&db, python_file); + .to_scope_id(&db, program_file); // Enter the range directly so it becomes the cycle head when inferring `other.target` // reaches the other module and then re-enters this scope. @@ -1860,13 +1860,13 @@ class TargetB: let file = system_path_to_file(&db, "/src/test.py").unwrap(); let function_scope = { - let python_file = PythonFile::new(&db, file, db.python_version()); - let index = semantic_index(&db, python_file); + let program_file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let index = semantic_index(&db, program_file); index.child_scopes(FileScopeId::global()).next().unwrap().0 }; { - let python_file = PythonFile::new(&db, file, db.python_version()); - let scope = function_scope.to_scope_id(&db, python_file); + let program_file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let scope = function_scope.to_scope_id(&db, program_file); let use_def = use_def_map(&db, scope); assert!( evaluate_reachability_constraint(&db, scope, use_def.end_of_scope_reachability(),) @@ -1879,8 +1879,8 @@ class TargetB: "from typing import NoReturn\ndef callback() -> NoReturn: ...", )?; - let python_file = PythonFile::new(&db, file, db.python_version()); - let scope = function_scope.to_scope_id(&db, python_file); + let program_file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let scope = function_scope.to_scope_id(&db, program_file); let use_def = use_def_map(&db, scope); assert!( evaluate_reachability_constraint(&db, scope, use_def.end_of_scope_reachability(),) @@ -1908,7 +1908,9 @@ class TargetB: )?; let file = system_path_to_file(&db, "/src/test.py").unwrap(); - let index = semantic_index(&db, PythonFile::new(&db, file, db.python_version())); + let program_file = + ProgramFile::new(&db, file, db.program_environment().program(&db)); + let index = semantic_index(&db, program_file); let function_scope = index.child_scopes(FileScopeId::global()).next().unwrap().0; let use_def = index.use_def_map(function_scope); let predicate = use_def diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 2bb727c036..d728d189bb 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -11,7 +11,8 @@ use ruff_source_file::LineIndex; use ruff_text_size::Ranged; use rustc_hash::FxHashMap; use ty_module_resolver::{ - KnownModule, Module, ModuleName, list_modules, resolve_module, resolve_real_shadowable_module, + ImportingFile, KnownModule, Module, ModuleName, list_modules, resolve_module, + resolve_real_shadowable_module, }; use crate::Db; @@ -27,6 +28,7 @@ use ty_python_core::place_table; use ty_python_core::scope::{FileScopeId, Scope}; use ty_python_core::semantic_index; use ty_python_core::symbol::Symbol; +use ty_python_core::{Program, ProgramFile}; /// The primary interface the LSP should use for querying semantic information about a [`File`]. /// @@ -41,14 +43,14 @@ use ty_python_core::symbol::Symbol; /// methods will automatically handle using the string literal's AST node when necessary. pub struct SemanticModel<'db> { db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, /// If `Some` then this `SemanticModel` is for analyzing the sub-AST of a string annotation. /// This expression will be used as a witness to the scope/location we're analyzing. in_string_annotation_expr: Option>, } impl<'db> SemanticModel<'db> { - pub fn new(db: &'db dyn Db, file: PythonFile<'db>) -> Self { + pub fn new(db: &'db dyn Db, file: ProgramFile<'db>) -> Self { Self { db, file, @@ -65,11 +67,19 @@ impl<'db> SemanticModel<'db> { } pub fn python_file(&self) -> PythonFile<'db> { + self.file.python_file(self.db) + } + + pub fn program_file(&self) -> ProgramFile<'db> { self.file } + pub fn program(&self) -> Program<'db> { + self.file.program(self.db) + } + pub fn program_environment(&self) -> ProgramEnvironment<'db> { - ProgramEnvironment::from_file(self.python_file()) + ProgramEnvironment::from_file(self.program_file()) } pub fn file_path(&self) -> &FilePath { @@ -91,8 +101,8 @@ impl<'db> SemanticModel<'db> { ) -> FxHashMap> { let db = self.db; let mut members = FxHashMap::default(); - let python_file = self.python_file(); - let index = semantic_index(self.db, python_file); + let program_file = self.program_file(); + let index = semantic_index(self.db, program_file); let Some(file_scope) = self.scope(node) else { return members; }; @@ -102,7 +112,8 @@ impl<'db> SemanticModel<'db> { .into_iter() .rev() { - for memberdef in all_reachable_members(db, file_scope.to_scope_id(self.db, python_file)) + for memberdef in + all_reachable_members(db, file_scope.to_scope_id(self.db, program_file)) { members.insert( memberdef.member.name, @@ -119,24 +130,33 @@ impl<'db> SemanticModel<'db> { /// Resolve the given import made in this file to a Type pub fn resolve_module_type(&self, module: Option<&str>, level: u32) -> Option> { let module = self.resolve_module(module, level)?; - Some(Type::module_literal(self.db, self.python_file(), module)) + Some(Type::module_literal(self.db, self.program_file(), module)) } /// Resolve the given import made in this file to a Module pub fn resolve_module(&self, module: Option<&str>, level: u32) -> Option> { + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(self.db), + ); let module_name = - ModuleName::from_identifier_parts(self.db, self.python_file(), module, level).ok()?; - resolve_module(self.db, self.python_file(), &module_name) + ModuleName::from_identifier_parts(self.db, importing_file, module, level).ok()?; + resolve_module(self.db, importing_file, &module_name) } /// Returns completions for symbols available in a `import ` context. pub fn import_completions(&self) -> Vec> { let typing_extensions = ModuleName::new_static("typing_extensions").unwrap(); let file = self.file(); + let resolver_environment = self.program_environment().resolver_environment(self.db); let is_typing_extensions_available = file.is_stub(self.db) - || resolve_real_shadowable_module(self.db, self.python_file(), &typing_extensions) - .is_some(); - list_modules(self.db, self.python_file().python_version(self.db)) + || resolve_real_shadowable_module( + self.db, + ImportingFile::File(file, resolver_environment), + &typing_extensions, + ) + .is_some(); + list_modules(self.db, resolver_environment) .iter() .copied() .filter(|module| { @@ -144,7 +164,7 @@ impl<'db> SemanticModel<'db> { }) .map(|module| { let builtin = module.is_known(self.db, KnownModule::Builtins); - let ty = Type::module_literal(self.db, self.python_file(), module); + let ty = Type::module_literal(self.db, self.program_file(), module); Completion { name: CompactString::new(module.name(self.db).as_str()), ty: Some(ty), @@ -158,7 +178,10 @@ impl<'db> SemanticModel<'db> { pub fn from_import_completions(&self, import: &ast::StmtImportFrom) -> Vec> { let module_name = match ModuleName::from_import_statement( self.db, - self.python_file(), + ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(self.db), + ), import, ) { Ok(module_name) => module_name, @@ -179,7 +202,14 @@ impl<'db> SemanticModel<'db> { &self, module_name: &ModuleName, ) -> Vec> { - let Some(module) = resolve_module(self.db, self.python_file(), module_name) else { + let Some(module) = resolve_module( + self.db, + ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(self.db), + ), + module_name, + ) else { tracing::debug!("Could not resolve module from `{module_name:?}`"); return vec![]; }; @@ -190,11 +220,18 @@ impl<'db> SemanticModel<'db> { /// it were imported by this model's `File`. fn module_completions(&self, module_name: &ModuleName) -> Vec> { let db = self.db; - let Some(module) = resolve_module(self.db, self.python_file(), module_name) else { + let Some(module) = resolve_module( + self.db, + ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(self.db), + ), + module_name, + ) else { tracing::debug!("Could not resolve module from `{module_name:?}`"); return vec![]; }; - let ty = Type::module_literal(self.db, self.python_file(), module); + let ty = Type::module_literal(self.db, self.program_file(), module); let builtin = module.is_known(self.db, KnownModule::Builtins); let mut completions = vec![]; @@ -219,7 +256,7 @@ impl<'db> SemanticModel<'db> { let mut completions = vec![]; for submodule in module.all_submodules(self.db) { - let ty = Type::module_literal(self.db, self.python_file(), *submodule); + let ty = Type::module_literal(self.db, self.program_file(), *submodule); let base = submodule.name(self.db).last_component(); completions.push(Completion { name: CompactString::new(base), @@ -254,15 +291,15 @@ impl<'db> SemanticModel<'db> { /// scope of this model's `File` are returned. pub fn scoped_completions(&self, node: ast::AnyNodeRef<'_>) -> Vec> { let db = self.db; - let python_file = self.python_file(); - let index = semantic_index(self.db, python_file); + let program_file = self.program_file(); + let index = semantic_index(self.db, program_file); let Some(file_scope) = self.scope(node) else { return vec![]; }; let mut completions = vec![]; for (file_scope, _) in index.ancestor_scopes(file_scope) { completions.extend( - all_reachable_members(db, file_scope.to_scope_id(self.db, python_file)).map( + all_reachable_members(db, file_scope.to_scope_id(self.db, program_file)).map( |memberdef| Completion { name: CompactString::new(memberdef.member.name), ty: Some(memberdef.member.ty), @@ -286,7 +323,9 @@ impl<'db> SemanticModel<'db> { // Project-level builtins take precedence over the standard builtins. let project_builtins = ModuleName::new_static("__builtins__").unwrap(); - if resolve_module(self.db, self.file, &project_builtins).is_some() { + let importing_file = + ImportingFile::File(self.file(), self.file.resolver_environment(self.db)); + if resolve_module(self.db, importing_file, &project_builtins).is_some() { completions.extend(self.module_completions(&project_builtins).into_iter().map( |mut completion| { completion.builtin = true; @@ -309,7 +348,7 @@ impl<'db> SemanticModel<'db> { /// Returns `true` if the given class definition's name was previously /// bound in the same scope (i.e., the class definition is a re-assignment). pub fn is_class_name_reassigned(&self, class_def: &ast::StmtClassDef) -> bool { - let index = semantic_index(self.db, self.python_file()); + let index = semantic_index(self.db, self.program_file()); let definition = index.expect_single_definition(class_def); let scope = definition.scope(self.db); let table = place_table(self.db, scope); @@ -319,7 +358,7 @@ impl<'db> SemanticModel<'db> { /// Returns the scope in which `node` is defined (handles string annotations). pub fn scope(&self, node: ast::AnyNodeRef<'_>) -> Option { - let index = semantic_index(self.db, self.python_file()); + let index = semantic_index(self.db, self.program_file()); match self.node_in_ast(node) { ast::AnyNodeRef::Identifier(identifier) => index.try_expression_scope_id(identifier), @@ -373,7 +412,7 @@ impl<'db> SemanticModel<'db> { &self, node: ast::AnyNodeRef<'_>, ) -> impl Iterator + '_ { - let index = semantic_index(self.db, self.python_file()); + let index = semantic_index(self.db, self.program_file()); self.scope(node) .into_iter() .flat_map(move |scope| index.ancestor_scopes(scope)) @@ -391,8 +430,8 @@ impl<'db> SemanticModel<'db> { &self, covering_node: &CoveringNode<'_>, ) -> Option> { - let index = semantic_index(self.db, self.python_file()); - let parsed = parsed_module(self.db, self.file).load(self.db); + let index = semantic_index(self.db, self.program_file()); + let parsed = parsed_module(self.db, self.python_file()).load(self.db); let target_range = covering_node.node().range(); for node in covering_node.ancestors() { @@ -462,11 +501,11 @@ impl<'db> SemanticModel<'db> { ) -> Option<(Parsed, Self)> { // Ask the inference engine whether this is actually a string annotation let expr = ExprRef::StringLiteral(string_expr); - let index = semantic_index(self.db, self.python_file()); + let index = semantic_index(self.db, self.program_file()); // When looking up scopes, use the expr in the top-level AST // (we might be trying to enter a sub-sub-AST, so this isn't silly) let file_scope = index.expression_scope_id(&self.expr_ref_in_ast(expr)); - let scope = file_scope.to_scope_id(self.db, self.python_file()); + let scope = file_scope.to_scope_id(self.db, self.program_file()); // When querying whether the expr is a string annotation, we do however use the actual expr // (the inference engine should record this information even for sub-nodes) if !infer_complete_scope_types(self.db, scope).is_string_annotation(expr) { @@ -507,7 +546,7 @@ impl<'db> SemanticModel<'db> { DefinitionKind::TypeAlias(_) => true, DefinitionKind::AnnotatedAssignment(assignment) => { let parsed = parsed_module(self.db, definition.python_file(self.db)); - let model = Self::new(self.db, definition.python_file(self.db)); + let model = Self::new(self.db, definition.program_file(self.db)); model.is_type_alias_annotation(assignment.annotation(&parsed.load(self.db))) } _ => false, @@ -619,9 +658,9 @@ impl<'db> SemanticModel<'db> { string_expr: &ast::ExprStringLiteral, ) -> Option> { let expr = ast::ExprRef::from(string_expr); - let index = semantic_index(self.db, self.python_file()); + let index = semantic_index(self.db, self.program_file()); let file_scope = index.try_expression_scope_id(&self.expr_ref_in_ast(expr))?; - let scope = file_scope.to_scope_id(self.db, self.python_file()); + let scope = file_scope.to_scope_id(self.db, self.program_file()); infer_complete_scope_types(self.db, scope).try_expected_type(expr) } @@ -720,7 +759,7 @@ pub(crate) trait HasOptionalDefinition { impl HasType for ast::ExprRef<'_> { fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option> { - let file = model.python_file(); + let file = model.program_file(); let index = semantic_index(model.db, file); // TODO(#1637): semantic tokens is making this crash even with // `try_expr_ref_in_ast` guarding this, for now just use `try_expression_scope_id`. @@ -824,7 +863,7 @@ macro_rules! impl_binding_has_ty_def { impl HasDefinition for $ty { #[inline] fn definition<'db>(&self, model: &SemanticModel<'db>) -> Definition<'db> { - let index = semantic_index(model.db, model.python_file()); + let index = semantic_index(model.db, model.program_file()); index.expect_single_definition(self) } } @@ -853,7 +892,7 @@ impl HasType for ast::Alias { if &self.name == "*" { return Some(Type::Never); } - let index = semantic_index(model.db, model.python_file()); + let index = semantic_index(model.db, model.program_file()); Some(binding_type( model.db(), index.expect_single_definition(self), @@ -865,7 +904,7 @@ impl HasOptionalDefinition for ast::ExceptHandlerExceptHandler { fn optional_definition<'db>(&self, model: &SemanticModel<'db>) -> Option> { self.name.as_ref()?; - let index = semantic_index(model.db, model.python_file()); + let index = semantic_index(model.db, model.program_file()); Some(index.expect_single_definition(self)) } } @@ -881,9 +920,9 @@ impl HasType for ast::ExceptHandlerExceptHandler { mod tests { use crate::db::tests::TestDbBuilder; use crate::{HasType, SemanticModel}; - use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; + use ty_python_core::ProgramFile; #[test] fn function_type() -> anyhow::Result<()> { @@ -893,8 +932,8 @@ mod tests { let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); - let foo = PythonFile::new(&db, foo, db.python_version()); - let ast = parsed_module(&db, foo).load(&db); + let foo = ProgramFile::new(&db, foo, db.program_environment().program(&db)); + let ast = parsed_module(&db, foo.python_file(&db)).load(&db); let function = ast.suite()[0].as_function_def_stmt().unwrap(); let model = SemanticModel::new(&db, foo); @@ -913,8 +952,8 @@ mod tests { let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); - let foo = PythonFile::new(&db, foo, db.python_version()); - let ast = parsed_module(&db, foo).load(&db); + let foo = ProgramFile::new(&db, foo, db.program_environment().program(&db)); + let ast = parsed_module(&db, foo.python_file(&db)).load(&db); let class = ast.suite()[0].as_class_def_stmt().unwrap(); let model = SemanticModel::new(&db, foo); @@ -934,8 +973,8 @@ mod tests { let bar = system_path_to_file(&db, "/src/bar.py").unwrap(); - let bar = PythonFile::new(&db, bar, db.python_version()); - let ast = parsed_module(&db, bar).load(&db); + let bar = ProgramFile::new(&db, bar, db.program_environment().program(&db)); + let ast = parsed_module(&db, bar.python_file(&db)).load(&db); let import = ast.suite()[0].as_import_from_stmt().unwrap(); let alias = &import.names[0]; diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index bc9287c64b..db165f8dd3 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -14,14 +14,13 @@ use call::{CallDunderError, CallError, CallErrorKind}; use context::InferContext; pub use context::ProgramEnvironment; use ruff_db::Instant; -use ruff_db::PythonFile; use ruff_db::diagnostic::{Annotation, Diagnostic, Span}; use ruff_db::parsed::parsed_module; use ruff_python_ast as ast; use ruff_python_ast::name::Name; use ruff_text_size::Ranged; use smallvec::smallvec_inline; -use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module}; +use ty_module_resolver::{ImportingFile, KnownModule, Module, ModuleName, resolve_module}; pub(crate) use self::callable::UpcastPolicy; pub use self::cyclic::CycleDetector; @@ -114,7 +113,7 @@ pub(crate) use special_form::TypedDictModule; use ty_python_core::definition::{Definition, DefinitionKind}; use ty_python_core::place::ScopedPlaceId; use ty_python_core::scope::ScopeId; -use ty_python_core::{Truthiness, place_table, semantic_index, use_def_map}; +use ty_python_core::{ProgramFile, Truthiness, place_table, semantic_index, use_def_map}; mod attribute_write; mod bool; @@ -173,7 +172,7 @@ mod definition; mod property_tests; mod subscript; -pub fn check_types(db: &dyn Db, file: PythonFile<'_>) -> Vec { +pub fn check_types(db: &dyn Db, file: ProgramFile<'_>) -> Vec { let source_file = file.file(db); let _span = tracing::trace_span!("check_types", ?source_file).entered(); tracing::debug!("Checking file '{path}'", path = source_file.path(db)); @@ -204,7 +203,7 @@ pub fn check_types(db: &dyn Db, file: PythonFile<'_>) -> Vec { .map(|error| Diagnostic::invalid_syntax(source_file, error, error)), ); - let diagnostics = check_suppressions(db, file, diagnostics); + let diagnostics = check_suppressions(db, file.python_file(db), diagnostics); let elapsed = start.elapsed(); if elapsed >= Duration::from_millis(100) { @@ -237,7 +236,7 @@ pub(crate) fn binding_type<'db>(db: &'db dyn Db, definition: Definition<'db>) -> /// ``` #[salsa::tracked(returns(copy))] pub(crate) fn exists_at_runtime<'db>(db: &'db dyn Db, definition: Definition<'db>) -> bool { - let file = definition.python_file(db); + let file = definition.program_file(db); let inference = infer_definition_types(db, definition); let ty = inference.binding_type(definition); @@ -250,7 +249,7 @@ pub(crate) fn exists_at_runtime<'db>(db: &'db dyn Db, definition: Definition<'db return false; } - let parsed = parsed_module(db, file); + let parsed = parsed_module(db, file.python_file(db)); let module = parsed.load(db); // Definitions inside an `if TYPE_CHECKING` block are never available at runtime. @@ -337,7 +336,7 @@ fn definition_expression_type<'db>( definition: Definition<'db>, expression: &ast::Expr, ) -> Type<'db> { - let file = definition.python_file(db); + let file = definition.program_file(db); let index = semantic_index(db, file); let file_scope = index.expression_scope_id(expression); let scope = file_scope.to_scope_id(db, file); @@ -364,7 +363,7 @@ fn definition_expression_annotation<'db>( definition: Definition<'db>, expression: &ast::Expr, ) -> TypeAndQualifiers<'db> { - let file = definition.python_file(db); + let file = definition.program_file(db); let index = semantic_index(db, file); let file_scope = index.expression_scope_id(expression); let scope = file_scope.to_scope_id(db, file); @@ -638,7 +637,7 @@ impl Default for MemberLookupPolicy { #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] struct MemberLookupKey<'db> { #[returns(copy)] - program: Program, + program: Program<'db>, #[returns(copy)] ty: Type<'db>, #[returns(ref)] @@ -1192,7 +1191,7 @@ impl InstanceProjection { #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] struct TypePair<'db> { #[returns(copy)] - program: Program, + program: Program<'db>, #[returns(copy)] first: Type<'db>, #[returns(copy)] @@ -1694,7 +1693,7 @@ impl<'db> Type<'db> { fn cached_materialization( self, db: &'db dyn Db, - program: Program, + program: Program<'db>, materialization_kind: MaterializationKind, ) -> Type<'db> { let env = &ProgramEnvironment::from_program(program); @@ -1894,7 +1893,7 @@ impl<'db> Type<'db> { pub(crate) fn module_literal( db: &'db dyn Db, - importing_file: PythonFile<'db>, + importing_file: ProgramFile<'db>, submodule: Module<'db>, ) -> Self { Self::ModuleLiteral(ModuleLiteralType::new( @@ -2873,7 +2872,7 @@ impl<'db> Type<'db> { #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] fn lookup_dunder_new_inner<'db>( db: &'db dyn Db, - program: Program, + program: Program<'db>, ty: Type<'db>, ) -> Option> { let env = &ProgramEnvironment::from_program(program); @@ -3463,7 +3462,7 @@ impl<'db> Type<'db> { #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] fn try_call_dunder_get_inner<'db>( db: &'db dyn Db, - program: Program, + program: Program<'db>, ty: Type<'db>, instance: Option>, owner: Type<'db>, @@ -3798,7 +3797,11 @@ impl<'db> Type<'db> { cycle_initial=|_, _, _, _| true, heap_size=ruff_memory_usage::heap_size )] - fn is_definitely_non_data_descriptor_impl(self, db: &'db dyn Db, program: Program) -> bool { + fn is_definitely_non_data_descriptor_impl( + self, + db: &'db dyn Db, + program: Program<'db>, + ) -> bool { let env = &ProgramEnvironment::from_program(program); match self { Type::Dynamic(_) | Type::Divergent(_) | Type::TypeVar(_) => false, @@ -3826,7 +3829,7 @@ impl<'db> Type<'db> { fn is_data_descriptor_impl( self, db: &'db dyn Db, - program: Program, + program: Program<'db>, any_of_union: bool, ) -> bool { let env = &ProgramEnvironment::from_program(program); @@ -6562,7 +6565,7 @@ impl<'db> Type<'db> { fallback_type: Type::unknown(), }); } - let index = semantic_index(db, scope_id.python_file(db)); + let index = semantic_index(db, scope_id.program_file(db)); Ok(bind_typevar( db, index, @@ -7647,7 +7650,7 @@ impl<'db> Type<'db> { }, heap_size=ruff_memory_usage::heap_size )] - fn expand_eagerly_(self, db: &'db dyn Db, program: Program) -> Type<'db> { + fn expand_eagerly_(self, db: &'db dyn Db, program: Program<'db>) -> Type<'db> { let env = &ProgramEnvironment::from_program(program); self.apply_type_mapping( db, @@ -9058,7 +9061,7 @@ impl<'db> InvalidTypeExpression<'db> { && function_body_scope .scope(db) .parent() - .map(|parent| parent.to_scope_id(db, function_body_scope.python_file(db))) + .map(|parent| parent.to_scope_id(db, function_body_scope.program_file(db))) == builtins_module_scope(db, env) { diagnostic.set_primary_annotation_message("Did you mean `collections.abc.Callable`?"); @@ -9198,14 +9201,14 @@ pub struct ModuleLiteralType<'db> { /// the same underlying single-file module are understood by ty as being equivalent types /// in all situations. #[returns(copy)] - _importing_file: Option>, + _importing_file: Option>, } // The Salsa heap is tracked separately. impl get_size2::GetSize for ModuleLiteralType<'_> {} impl<'db> ModuleLiteralType<'db> { - fn importing_file(self, db: &'db dyn Db) -> Option> { + fn importing_file(self, db: &'db dyn Db) -> Option> { debug_assert_eq!( self._importing_file(db).is_some(), self.module(db).kind(db).is_package() @@ -9277,7 +9280,14 @@ impl<'db> ModuleLiteralType<'db> { let relative_submodule_name = ModuleName::new(name)?; let mut absolute_submodule_name = self.module(db).name(db).clone(); absolute_submodule_name.extend(&relative_submodule_name); - let submodule = resolve_module(db, importing_file, &absolute_submodule_name)?; + let submodule = resolve_module( + db, + ImportingFile::File( + importing_file.file(db), + importing_file.resolver_environment(db), + ), + &absolute_submodule_name, + )?; Some(Type::module_literal(db, importing_file, submodule)) } @@ -9290,7 +9300,10 @@ impl<'db> ModuleLiteralType<'db> { // For module literals, we want to try calling the module's own `__getattr__` function // if it exists. First, we need to look up the `__getattr__` function in the module's scope. let module = self.module(db); - if let Some(file) = module.python_file(db) { + if let Some(file) = module + .file(db) + .map(|file| ProgramFile::new(db, file, env.program(db))) + { let getattr_symbol = imported_symbol(db, env, Some(file), "__getattr__", None); // If we found a __getattr__ function, try to call it with the name argument if let Place::Defined(place) = getattr_symbol.place @@ -9345,7 +9358,10 @@ impl<'db> ModuleLiteralType<'db> { return Place::bound(submodule).into(); } - let place_and_qualifiers = imported_symbol(db, env, module.python_file(db), name, None); + let file = module + .file(db) + .map(|file| ProgramFile::new(db, file, env.program(db))); + let place_and_qualifiers = imported_symbol(db, env, file, name, None); // If the normal lookup failed, try to call the module's `__getattr__` function if place_and_qualifiers.place.is_undefined() { diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index e1bddfbbc0..e40e5d3cbc 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -161,7 +161,7 @@ impl<'db> Type<'db> { #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] fn try_call_bin_op_return_type_impl<'db>( db: &'db dyn Db, - program: Program, + program: Program<'db>, left_ty: Type<'db>, op: ast::Operator, right_ty: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index dfb7747062..fc66282bfc 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -76,7 +76,7 @@ use crate::types::{ use crate::{DisplaySettings, FxOrderSet}; use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}; use ruff_python_ast::{self as ast, AnyNodeRef, ArgOrKeyword, PythonVersion}; -use ty_python_core::semantic_index; +use ty_python_core::{ProgramFile, semantic_index}; pub(crate) use self::constructor::ConstructorCallableKind; @@ -2305,7 +2305,8 @@ impl<'db> Bindings<'db> { Type::ModuleLiteral(module_literal) => { let all_names = module_literal .module(db) - .python_file(db) + .file(db) + .map(|file| ProgramFile::new(db, file, env.program(db))) .map(|file| dunder_all_names(db, file)) .unwrap_or_default(); match all_names { @@ -7658,7 +7659,7 @@ impl<'db> CallableDescription<'db> { db: &'db dyn Db, function: FunctionType<'db>, ) -> Cow<'db, str> { - let semantic_index = semantic_index(db, function.python_file(db)); + let semantic_index = semantic_index(db, function.program_file(db)); let enclosing_scope = semantic_index.scope(function.definition(db).file_scope(db)); if let Some(class_node) = enclosing_scope.node().as_class() && let Some(class) = diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 6a3468f70f..0136e37c41 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -52,7 +52,6 @@ use crate::{ }, types::{MetaclassCandidate, TypeDefinition, UnionType}, }; -use ruff_db::PythonFile; use ruff_db::diagnostic::Span; use ruff_db::files::File; use ruff_db::parsed::parsed_module; @@ -61,7 +60,7 @@ use ruff_python_ast::{self as ast, NodeIndex}; use ruff_text_size::{Ranged, TextRange}; use ty_python_core::definition::Definition; use ty_python_core::scope::ScopeId; -use ty_python_core::{place_table, use_def_map}; +use ty_python_core::{ProgramFile, place_table, use_def_map}; mod dynamic_literal; mod enum_literal; @@ -495,7 +494,7 @@ impl<'db> GenericAlias<'db> { typevar: BoundTypeVarIdentity<'db>, ) -> TypeVarVariance { let origin = self.origin(db); - let env = ProgramEnvironment::from_file(origin.python_file(db)); + let env = ProgramEnvironment::from_file(origin.program_file(db)); let specialization = self.specialization(db); @@ -783,13 +782,13 @@ impl<'db> ClassLiteral<'db> { } } - pub(crate) fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> { + pub(crate) fn program_file(self, db: &'db dyn Db) -> ProgramFile<'db> { match self { - Self::Static(class) => class.python_file(db), - Self::Dynamic(class) => class.scope(db).python_file(db), - Self::DynamicNamedTuple(class) => class.scope(db).python_file(db), - Self::DynamicTypedDict(class) => class.scope(db).python_file(db), - Self::DynamicEnum(enum_lit) => enum_lit.scope(db).python_file(db), + Self::Static(class) => class.program_file(db), + Self::Dynamic(class) => class.scope(db).program_file(db), + Self::DynamicNamedTuple(class) => class.scope(db).program_file(db), + Self::DynamicTypedDict(class) => class.scope(db).program_file(db), + Self::DynamicEnum(enum_lit) => enum_lit.scope(db).program_file(db), } } @@ -1384,7 +1383,7 @@ impl<'db> ClassType<'db> { } let mut abstract_methods: FxIndexMap = FxIndexMap::default(); - let env = &ProgramEnvironment::from_file(self.class_literal(db).python_file(db)); + let env = &ProgramEnvironment::from_file(self.class_literal(db).program_file(db)); // Iterate through the MRO in reverse order, // skipping `object` (we know it doesn't define any abstract methods) @@ -2203,7 +2202,7 @@ impl<'db> ClassType<'db> { db: &'db dyn Db, receiver: Type<'db>, ) -> CallableTypes<'db> { - let env = &ProgramEnvironment::from_file(self.class_literal(db).python_file(db)); + let env = &ProgramEnvironment::from_file(self.class_literal(db).program_file(db)); // TODO: This mimics a lot of the logic in Type::try_call_from_constructor. Can we // consolidate the two? Can we invoke a class by upcasting the class into a Callable, and // then relying on the call binding machinery to Just Work™? @@ -2986,7 +2985,7 @@ impl<'db> QualifiedClassName<'db> { let body_scope = class.body_scope(self.db); // Skip the class body scope itself. ( - body_scope.python_file(self.db), + body_scope.program_file(self.db), body_scope.file_scope_id(self.db), 1, ) @@ -2994,20 +2993,20 @@ impl<'db> QualifiedClassName<'db> { ClassLiteral::Dynamic(class) => { // Dynamic classes don't have a body scope; start from the enclosing scope. let scope = class.scope(self.db); - (scope.python_file(self.db), scope.file_scope_id(self.db), 0) + (scope.program_file(self.db), scope.file_scope_id(self.db), 0) } ClassLiteral::DynamicNamedTuple(namedtuple) => { // Dynamic namedtuples don't have a body scope; start from the enclosing scope. let scope = namedtuple.scope(self.db); - (scope.python_file(self.db), scope.file_scope_id(self.db), 0) + (scope.program_file(self.db), scope.file_scope_id(self.db), 0) } ClassLiteral::DynamicTypedDict(typeddict) => { let scope = typeddict.scope(self.db); - (scope.python_file(self.db), scope.file_scope_id(self.db), 0) + (scope.program_file(self.db), scope.file_scope_id(self.db), 0) } ClassLiteral::DynamicEnum(enum_lit) => { let scope = enum_lit.scope(self.db); - (scope.python_file(self.db), scope.file_scope_id(self.db), 0) + (scope.program_file(self.db), scope.file_scope_id(self.db), 0) } }; diff --git a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs index 7f926a0a78..5757f67c3d 100644 --- a/crates/ty_python_semantic/src/types/class/dynamic_literal.rs +++ b/crates/ty_python_semantic/src/types/class/dynamic_literal.rs @@ -202,8 +202,9 @@ impl<'db> DynamicClassLiteral<'db> { db: &'db dyn Db, definition: Definition<'db>, ) -> Box<[Type<'db>]> { - let python_file = definition.python_file(db); - let env = ProgramEnvironment::from_file(python_file); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); let module = parsed_module(db, python_file).load(db); let value = definition diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs index 44f0a14586..3c408bb588 100644 --- a/crates/ty_python_semantic/src/types/class/known.rs +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -13,7 +13,6 @@ use crate::{ known_instance::DeprecatedInstance, }, }; -use ruff_db::PythonFile; use ruff_python_ast as ast; use ruff_python_ast::PythonVersion; use rustc_hash::FxHashSet; @@ -21,7 +20,7 @@ use std::{ borrow::Cow, sync::{LazyLock, Mutex}, }; -use ty_module_resolver::{KnownModule, file_to_module}; +use ty_module_resolver::{ImportingFile, KnownModule, file_to_module}; use ty_python_core::{SemanticIndex, Truthiness, scope::NodeWithScopeKind}; /// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow @@ -1569,7 +1568,7 @@ impl KnownClass { pub(crate) fn try_from_file_and_name( db: &dyn Db, - file: PythonFile<'_>, + file: ImportingFile<'_>, class_name: &str, ) -> Option { // We assert that this match is exhaustive over the right-hand side in the unit test @@ -1682,9 +1681,8 @@ impl KnownClass { _ => return None, }; - let module = file_to_module(db, file)?.known(db)?; + let module = file_to_module(db, file.resolver_file(db))?.known(db)?; let python_version = file.python_version(db); - candidates .iter() .copied() @@ -1967,7 +1965,7 @@ struct KnownClassArgument { class: KnownClass, #[returns(copy)] - program: Program, + program: Program<'db>, } /// Enumeration of ways in which looking up a [`KnownClass`] in its canonical module could fail. @@ -2076,6 +2074,7 @@ mod tests { source: PythonVersionSource::default(), }); let python_version = db.python_version(); + let resolver_environment = db.program_environment().resolver_environment(&db); for class in KnownClass::iter() { if class.canonical_module(python_version).is_third_party() { continue; @@ -2083,7 +2082,7 @@ mod tests { let class_name = class.name(python_version); let class_module = resolve_module_confident( &db, - python_version, + resolver_environment, &class.canonical_module(python_version).name(), ) .unwrap(); @@ -2091,7 +2090,7 @@ mod tests { assert_eq!( KnownClass::try_from_file_and_name( &db, - class_module.python_file(&db).unwrap(), + ImportingFile::File(class_module.file(&db).unwrap(), resolver_environment), class_name ), Some(class), diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index d74015d87e..c547a42df1 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -62,7 +62,7 @@ use crate::{ }; use crate::{attribute_assignments, attribute_declarations}; use ty_python_core::{ - attribute_scopes, + ProgramFile, attribute_scopes, definition::{Definition, DefinitionKind, DefinitionState, TargetKind}, place_table, scope::{Scope, ScopeId}, @@ -404,11 +404,12 @@ impl<'db> StaticClassLiteral<'db> { )] fn pep695_generic_context_inner(self, db: &'db dyn Db) -> Option> { let scope = self.body_scope(db); - let python_file = scope.python_file(db); + let program_file = scope.program_file(db); + let python_file = program_file.python_file(db); let parsed = parsed_module(db, python_file).load(db); let class_def_node = scope.node(db).expect_class().node(&parsed); class_def_node.type_params.as_ref().map(|type_params| { - let index = semantic_index(db, python_file); + let index = semantic_index(db, program_file); let definition = index.expect_single_definition(class_def_node); GenericContext::from_type_params(db, index, definition, type_params) }) @@ -522,6 +523,10 @@ impl<'db> StaticClassLiteral<'db> { self.body_scope(db).python_file(db) } + pub(crate) fn program_file(self, db: &'db dyn Db) -> ProgramFile<'db> { + self.body_scope(db).program_file(db) + } + /// Return the original [`ast::StmtClassDef`] node associated with this class /// /// ## Note @@ -533,7 +538,7 @@ impl<'db> StaticClassLiteral<'db> { 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.python_file(db)); + let index = semantic_index(db, body_scope.program_file(db)); index.expect_single_definition(body_scope.node(db).expect_class()) } @@ -625,12 +630,13 @@ impl<'db> StaticClassLiteral<'db> { class.name(db) ); - let python_file = class.python_file(db); + let program_file = class.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let class_stmt = class.node(db, &module); let class_definition = - semantic_index(db, python_file).expect_single_definition(class_stmt); + semantic_index(db, program_file).expect_single_definition(class_stmt); expanded_class_base_entries(db, class.known(db), class_stmt, class_definition) .into_iter() .map(ExpandedClassBaseEntry::ty) @@ -716,7 +722,8 @@ impl<'db> StaticClassLiteral<'db> { fn decorators_inner(self, db: &'db dyn Db) -> Box<[Type<'db>]> { tracing::trace!("StaticClassLiteral::decorators: {}", self.name(db)); - let python_file = self.python_file(db); + let program_file = self.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let class_stmt = self.node(db, &module); @@ -725,7 +732,7 @@ impl<'db> StaticClassLiteral<'db> { } let class_definition = - semantic_index(db, self.python_file(db)).expect_single_definition(class_stmt); + semantic_index(db, self.program_file(db)).expect_single_definition(class_stmt); class_stmt .decorator_list @@ -749,10 +756,12 @@ impl<'db> StaticClassLiteral<'db> { /// Iterate through the decorators on this class, returning the index of the first one /// that is either `@dataclass` or `@dataclass(...)`. pub(crate) fn find_dataclass_decorator_position(self, db: &'db dyn Db) -> Option { - let python_file = self.python_file(db); + let program_file = self.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let class_stmt = self.node(db, &module); - let class_definition = semantic_index(db, python_file).expect_single_definition(class_stmt); + let class_definition = + semantic_index(db, program_file).expect_single_definition(class_stmt); class_stmt.decorator_list.iter().position(|decorator| { let decorator_callable = decorator @@ -1073,8 +1082,9 @@ impl<'db> StaticClassLiteral<'db> { db: &'db dyn Db, class: StaticClassLiteral<'db>, ) -> Result<(Type<'db>, Option>), MetaclassError<'db>> { - let python_file = class.python_file(db); - let env = ProgramEnvironment::from_file(python_file); + let program_file = class.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); tracing::trace!("StaticClassLiteral::try_metaclass: {}", class.name(db)); // Identify the class's own metaclass (or take the first base class's metaclass). @@ -2777,8 +2787,9 @@ impl<'db> StaticClassLiteral<'db> { let class_body_scope = attribute.class_body_scope(db); let name = attribute.name(db).as_str(); let target_method_decorator = attribute.target_method_decorator(db); - let python_file = class_body_scope.python_file(db); - let env = &ProgramEnvironment::from_file(python_file); + let program_file = class_body_scope.program_file(db); + let python_file = program_file.python_file(db); + let env = &ProgramEnvironment::from_file(program_file); // If we do not see any declarations of an attribute, neither in the class body nor in // any method, we build a union of the raw types inferred from all bindings of that @@ -2790,7 +2801,7 @@ impl<'db> StaticClassLiteral<'db> { let mut provenance = Provenance::Unknown; let module = parsed_module(db, python_file).load(db); - let index = semantic_index(db, python_file); + let index = semantic_index(db, program_file); let class_map = use_def_map(db, class_body_scope); let class_table = place_table(db, class_body_scope); let is_valid_scope = |method_scope: &Scope| { @@ -3594,10 +3605,10 @@ impl<'db> StaticClassLiteral<'db> { return TypeVarVariance::Bivariant; } let class_body_scope = self.body_scope(db); - let python_file = class_body_scope.python_file(db); + let program_file = class_body_scope.program_file(db); let python_version = env.python_version(db); - let index = semantic_index(db, python_file); + let index = semantic_index(db, program_file); let explicit_bases_variances = self .explicit_bases(db) @@ -3792,7 +3803,7 @@ impl get_size2::GetSize for ImplicitAttributeName<'_> {} #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] fn implicit_attribute_names<'db>(db: &'db dyn Db, class_body_scope: ScopeId<'db>) -> Box<[Name]> { - let index = semantic_index(db, class_body_scope.python_file(db)); + let index = semantic_index(db, class_body_scope.program_file(db)); let mut names = Vec::new(); for function_scope_id in attribute_scopes(db, class_body_scope) { diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index f9f2f11107..c12ef995a7 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -3897,7 +3897,7 @@ impl<'db> Type<'db> { )] fn assignable_solutions_impl<'db>( db: &'db dyn Db, - program: Program, + program: Program<'db>, source: Type<'db>, target: Type<'db>, inferable: TypeVarSet<'db>, diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 4c9af02a68..8170157912 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -25,9 +25,10 @@ use crate::{ lint::{LintId, LintMetadata}, suppression::suppressions, }; +use ty_module_resolver::ResolverEnvironment; use ty_python_core::definition::Definition; use ty_python_core::scope::ScopeId; -use ty_python_core::semantic_index; +use ty_python_core::{ProgramFile, semantic_index}; /// The lazily resolved program used by a semantic operation. #[derive(Clone)] @@ -37,8 +38,8 @@ pub struct ProgramEnvironment<'db> { } impl<'db> ProgramEnvironment<'db> { - /// Creates an environment that lazily obtains its Python version from `file`. - pub fn from_file(file: PythonFile<'db>) -> Self { + /// Creates an environment that lazily obtains its program from `file`. + pub fn from_file(file: ProgramFile<'db>) -> Self { Self { environment: Cell::new(ProgramSource::File(file.as_id())), lifetime: PhantomData, @@ -62,22 +63,23 @@ impl<'db> ProgramEnvironment<'db> { } /// Creates an environment with an already-established program. - pub const fn from_program(program: Program) -> Self { + pub fn from_program(program: Program<'db>) -> Self { Self { - environment: Cell::new(ProgramSource::Program(program)), + environment: Cell::new(ProgramSource::Program(program.as_id())), lifetime: PhantomData, } } /// Returns the program used by this operation. - pub fn program(&self, db: &'db dyn Db) -> Program { + #[inline] + pub fn program(&self, db: &'db dyn Db) -> Program<'db> { let program = match self.environment.get() { - ProgramSource::Program(program) => return program, + ProgramSource::Program(id) => return ResolverEnvironment::from_id(id), ProgramSource::File(file) => { cold_path(); // The source handle and database share `'db`; re-wrapping the stored ingredient // ID immediately before the read restores the original database lifetime. - PythonFile::from_id(file).python_version(db) + ProgramFile::from_id(file).program(db) } ProgramSource::Definition(definition) => { cold_path(); @@ -93,23 +95,30 @@ impl<'db> ProgramEnvironment<'db> { } }; - self.environment.set(ProgramSource::Program(program)); + self.environment + .set(ProgramSource::Program(program.as_id())); program } /// Returns the Python version used by this operation. #[inline] pub fn python_version(&self, db: &'db dyn Db) -> PythonVersion { + self.program(db).python_version(db) + } + + /// Returns the resolver environment used by this operation. + #[inline] + pub fn resolver_environment(&self, db: &'db dyn Db) -> ResolverEnvironment<'db> { self.program(db) } } #[derive(Clone, Copy)] enum ProgramSource { - Program(Program), - // Salsa interned handles are thin `Id` wrappers, so converting between `PythonFile` and `Id` + Program(Id), + // Salsa interned handles are thin `Id` wrappers, so converting between `ProgramFile` and `Id` // is an inlined representation change with no database lookup. Keeping the lifetime-bearing - // `PythonFile` out of the `Cell` preserves covariance in `'db`; replacing this variant after + // `ProgramFile` out of the `Cell` preserves covariance in `'db`; replacing this variant after // the first read avoids repeated Salsa ingredient reads in hot, recursive type operations. File(Id), Definition(Id), @@ -133,7 +142,7 @@ pub(crate) struct InferContext<'db, 'ast> { program_environment: &'ast ProgramEnvironment<'db>, scope: ScopeId<'db>, file: File, - python_file: PythonFile<'db>, + program_file: ProgramFile<'db>, module: &'ast ParsedModuleRef, diagnostics: std::cell::RefCell, diagnostics_suppressed: bool, @@ -148,11 +157,12 @@ impl<'db, 'ast> InferContext<'db, 'ast> { program_environment: &'ast ProgramEnvironment<'db>, scope: ScopeId<'db>, file: File, - python_file: PythonFile<'db>, + program_file: ProgramFile<'db>, module: &'ast ParsedModuleRef, ) -> Self { - debug_assert_eq!(scope.python_file(db), python_file); - debug_assert_eq!(python_file.file(db), file); + debug_assert_eq!(scope.program_file(db), program_file); + debug_assert_eq!(program_file.file(db), file); + debug_assert_eq!(program_environment.program(db), scope.program(db)); Self { db, @@ -160,7 +170,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { scope, module, file, - python_file, + program_file, diagnostics: std::cell::RefCell::new(TypeCheckDiagnostics::default()), diagnostics_suppressed: false, inference_flags: InferenceFlags::empty(), @@ -176,7 +186,11 @@ impl<'db, 'ast> InferContext<'db, 'ast> { } pub(crate) fn python_file(&self) -> PythonFile<'db> { - self.python_file + self.program_file.python_file(self.db()) + } + + pub(crate) fn program_file(&self) -> ProgramFile<'db> { + self.program_file } #[inline] @@ -302,7 +316,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { // Accessing the semantic index here is fine because // the index belongs to the same file as for which we emit the diagnostic. - let index = semantic_index(self.db(), self.python_file); + let index = semantic_index(self.db(), self.program_file); let scope_id = self.scope.file_scope_id(self.db()); @@ -330,7 +344,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> { /// specific statement or expression containing this range is reachable. fn is_range_reachable(&self, range: TextRange) -> bool { let db = self.db; - let index = semantic_index(self.db(), self.python_file); + let index = semantic_index(self.db(), self.program_file); let scope_id = self.scope.file_scope_id(self.db()); is_range_reachable(db, index, scope_id, range) } @@ -366,7 +380,7 @@ impl fmt::Debug for InferContext<'_, '_> { .field("db", &"") .field("scope", &self.scope) .field("file", &self.file) - .field("python_file", &self.python_file) + .field("program_file", &self.program_file) .field("diagnostics", &self.diagnostics) .field("diagnostics_suppressed", &self.diagnostics_suppressed) .field("inference_flags", &self.inference_flags) diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs index 2cd9409587..38a8a066d9 100644 --- a/crates/ty_python_semantic/src/types/cyclic.rs +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -690,11 +690,11 @@ mod tests { use crate::db::tests::setup_db; use crate::place::global_symbol; use crate::types::Type; - use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::system::DbWithWritableSystem; use std::cell::Cell; use std::hash::{Hash, Hasher}; + use ty_python_core::ProgramFile; struct TestVisit; @@ -765,7 +765,7 @@ mod tests { name: &str, ) -> Type<'db> { let file = system_path_to_file(db, "/src/a.py").unwrap(); - let file = PythonFile::new(db, file, env.python_version(db)); + let file = ProgramFile::new(db, file, env.program(db)); global_symbol(db, file, name) .place .expect_type() diff --git a/crates/ty_python_semantic/src/types/dedicated/pydantic.rs b/crates/ty_python_semantic/src/types/dedicated/pydantic.rs index b8fe610283..e30b36b5b0 100644 --- a/crates/ty_python_semantic/src/types/dedicated/pydantic.rs +++ b/crates/ty_python_semantic/src/types/dedicated/pydantic.rs @@ -171,7 +171,7 @@ impl<'db> FieldMetadata<'db> { // using `StrictInt = Annotated[int, Strict()]`. Since we don't retain the `Annotated` // metadata, we need to follow the alias back to its definition and parse the metadata // from there. - let model = SemanticModel::new(db, definition.python_file(db)); + let model = SemanticModel::new(db, definition.program_file(db)); let Some(alias_definition) = definitions_for_name( &model, name.id.as_str(), @@ -1189,7 +1189,7 @@ fn instance_symbol<'db>( ty: Type<'db>, ) -> Option<(KnownModule, &'db str, StaticClassLiteral<'db>)> { let class = ty.nominal_class(db, env)?.class_literal(db).as_static()?; - let module = file_to_module(db, class.python_file(db))?.known(db)?; + let module = file_to_module(db, class.program_file(db).resolver_file(db))?.known(db)?; Some((module, class.name(db).as_str(), class)) } diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 5f66e82cd6..ad80014d2a 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -50,7 +50,7 @@ use std::fmt::{self, Formatter}; use ty_module_resolver::{KnownModule, Module, ModuleName, file_to_module}; use ty_python_core::definition::{Definition, DefinitionKind}; use ty_python_core::place::{PlaceTable, ScopedPlaceId}; -use ty_python_core::{global_scope, place_table, use_def_map}; +use ty_python_core::{ProgramFile, global_scope, place_table, use_def_map}; const RUNTIME_CHECKABLE_DOCS_URL: &str = "https://docs.python.org/3/library/typing.html#typing.runtime_checkable"; @@ -1477,8 +1477,8 @@ pub(super) fn note_numbers_module_not_supported<'db>( let file = target_instance .class(db, env) .class_literal(db) - .python_file(db); - if let Some(module) = file_to_module(db, file) + .program_file(db); + if let Some(module) = file_to_module(db, file.resolver_file(db)) && module.is_known(db, KnownModule::Numbers) { let is_numeric = value_ty.is_subtype_of( @@ -4736,6 +4736,7 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( /// misconfigured their Python version. pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( db: &dyn Db, + env: &ProgramEnvironment<'_>, mut diagnostic: LintDiagnosticGuard, value_type: Type, attr: &str, @@ -4748,7 +4749,7 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( return; }; let module = module_ty.module(db); - let Some(file) = module.python_file(db) else { + let Some(file) = module.file(db) else { return; }; let Some(search_path) = module.search_path(db) else { @@ -4761,7 +4762,8 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( // We populate place_table entries for stdlib items across all known versions and platforms, // so if this lookup succeeds then we know that this lookup *could* succeed with possible // configuration changes. - let symbol_table = place_table(db, global_scope(db, file)); + let program_file = ProgramFile::new(db, file, env.program(db)); + let symbol_table = place_table(db, global_scope(db, program_file)); let Some(symbol) = symbol_table.symbol_by_name(attr) else { return; }; diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 3f1c3d73c9..8babe558a3 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -7,7 +7,6 @@ use std::collections::hash_map::Entry; use std::fmt::{self, Display, Formatter, Write}; use std::rc::Rc; -use ruff_db::PythonFile; use ruff_db::files::FilePath; use ruff_db::parsed::parsed_module; use ruff_db::source::{line_index, source_text}; @@ -38,6 +37,7 @@ use crate::types::{ SpecialFormType, StringLiteralType, SubclassOfInner, SubclassOfType, Type, TypeAliasType, TypeGuardLike, TypedDictModule, TypedDictType, UnionType, WrapperDescriptorKind, visitor, }; +use ty_python_core::ProgramFile; use ty_python_core::definition::Definition; use ty_python_core::scope::{FileScopeId, ScopeKind}; use ty_python_core::semantic_index; @@ -742,11 +742,11 @@ fn fmt_file_location<'db>( /// A vector of path components in order (e.g., `["module", "OuterClass", "InnerClass"]`) pub(super) fn qualified_name_components_from_scope( db: &dyn Db, - file: PythonFile<'_>, + file: ProgramFile<'_>, file_scope_id: FileScopeId, skip_count: usize, ) -> Vec { - let module_ast = parsed_module(db, file).load(db); + let module_ast = parsed_module(db, file.python_file(db)).load(db); let index = semantic_index(db, file); let mut name_parts = vec![]; @@ -772,7 +772,7 @@ pub(super) fn qualified_name_components_from_scope( } } - if let Some(module) = file_to_module(db, file) { + if let Some(module) = file_to_module(db, file.resolver_file(db)) { let module_name = module.name(db); name_parts.push(module_name.as_str().to_string()); } diff --git a/crates/ty_python_semantic/src/types/enums.rs b/crates/ty_python_semantic/src/types/enums.rs index 0b4bf2b309..ebd9c76311 100644 --- a/crates/ty_python_semantic/src/types/enums.rs +++ b/crates/ty_python_semantic/src/types/enums.rs @@ -332,7 +332,7 @@ fn enum_class_literal<'db>( db: &'db dyn Db, class: ClassLiteral<'db>, ) -> Option> { - let env = ProgramEnvironment::from_file(class.python_file(db)); + let env = ProgramEnvironment::from_file(class.program_file(db)); let metadata = enum_metadata(db, class)?; let members = metadata .members @@ -1056,7 +1056,7 @@ pub(crate) fn enum_metadata<'db>( return None; } - let env = ProgramEnvironment::from_file(class.python_file(db)); + let env = ProgramEnvironment::from_file(class.program_file(db)); if !is_enum_class_by_inheritance(db, &env, class) { return None; diff --git a/crates/ty_python_semantic/src/types/equality/enums.rs b/crates/ty_python_semantic/src/types/equality/enums.rs index 235ea35d17..afd8746ec5 100644 --- a/crates/ty_python_semantic/src/types/equality/enums.rs +++ b/crates/ty_python_semantic/src/types/equality/enums.rs @@ -1108,7 +1108,7 @@ fn enum_class_key_profile<'db>( enum_class: EnumClassLiteral<'db>, operator: ComparisonOperator, ) -> EnumClassKeyProfile<'db> { - let env = ProgramEnvironment::from_file(enum_class.class_literal(db).python_file(db)); + let env = ProgramEnvironment::from_file(enum_class.class_literal(db).program_file(db)); let semantics = KnownComparisonSemantics::of_instance( db, &env, diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index e609f535ec..9eb3ea838c 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -63,7 +63,7 @@ use ruff_python_ast::find_node::covering_node; use ruff_python_ast::{self as ast, OperatorPrecedence, ParameterWithDefault}; use ruff_text_size::Ranged; use salsa::plumbing::AsId; -use ty_module_resolver::{KnownModule, ModuleName, file_to_module, resolve_module}; +use ty_module_resolver::{ImportingFile, KnownModule, ModuleName, file_to_module, resolve_module}; use crate::place::{DefinedPlace, Definedness, Place, place_from_bindings}; use crate::types::call::{Binding, CallArguments}; @@ -101,7 +101,7 @@ use crate::{Db, FxOrderSet, ProgramEnvironment}; use ty_python_core::ast_ids::HasScopedUseId; use ty_python_core::definition::Definition; use ty_python_core::scope::ScopeId; -use ty_python_core::{FileScopeId, SemanticIndex, semantic_index}; +use ty_python_core::{FileScopeId, ProgramFile, SemanticIndex, semantic_index}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct RecursiveTypeNormalizationKey { @@ -336,6 +336,10 @@ impl<'db> OverloadLiteral<'db> { self.body_scope(db).python_file(db) } + pub(crate) fn program_file(self, db: &'db dyn Db) -> ProgramFile<'db> { + self.body_scope(db).program_file(db) + } + pub(crate) fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { self.decorators(db).contains(decorator) } @@ -441,7 +445,7 @@ impl<'db> OverloadLiteral<'db> { /// over-invalidation. fn definition(self, db: &'db dyn Db) -> Definition<'db> { let body_scope = self.body_scope(db); - let index = semantic_index(db, body_scope.python_file(db)); + let index = semantic_index(db, body_scope.program_file(db)); index.expect_single_definition(body_scope.node(db).expect_function()) } @@ -453,14 +457,14 @@ impl<'db> OverloadLiteral<'db> { let scope = self.definition(db).scope(db); let module = parsed_module(db, self.python_file(db)).load(db); let use_def = - semantic_index(db, scope.python_file(db)).use_def_map(scope.file_scope_id(db)); + semantic_index(db, scope.program_file(db)).use_def_map(scope.file_scope_id(db)); let use_id = self .body_scope(db) .node(db) .expect_function() .node(&module) .name - .scoped_use_id(db, self.python_file(db)); + .scoped_use_id(db, self.program_file(db)); let env = ProgramEnvironment::from_scope(scope); let Place::Defined(DefinedPlace { @@ -513,16 +517,17 @@ impl<'db> OverloadLiteral<'db> { /// over-invalidation. pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> { let scope = self.body_scope(db); - let python_file = self.python_file(db); + let program_file = self.program_file(db); + let python_file = program_file.python_file(db); let mut signature = self.raw_signature(db, ReturnCallableTypeVarScope::Public); let module = parsed_module(db, python_file).load(db); let function_node = scope.node(db).expect_function().node(&module); - let index = semantic_index(db, python_file); + let index = semantic_index(db, program_file); let file_scope_id = scope.file_scope_id(db); let is_generator = file_scope_id.is_generator_function(index); if function_node.is_async && !is_generator { - let env = ProgramEnvironment::from_file(python_file); + let env = ProgramEnvironment::from_file(program_file); signature = signature.wrap_coroutine_return_type(db, &env); } @@ -615,11 +620,12 @@ impl<'db> OverloadLiteral<'db> { let env = &ProgramEnvironment::from_scope(self.body_scope(db)); let scope = self.body_scope(db); - let python_file = self.python_file(db); + let program_file = self.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let function_stmt_node = scope.node(db).expect_function().node(&module); let definition = self.definition(db); - let index = semantic_index(db, python_file); + let index = semantic_index(db, program_file); let pep695_ctx = function_stmt_node.type_params.as_ref().map(|type_params| { GenericContext::from_type_params(db, index, definition, type_params) }); @@ -685,7 +691,7 @@ impl<'db> OverloadLiteral<'db> { if method_has_explicit_self || class_is_generic || class_is_fallback { let scope_id = definition.scope(db); let typevar_binding_context = Some(definition); - let index = semantic_index(db, scope_id.python_file(db)); + let index = semantic_index(db, scope_id.program_file(db)); let class = nearest_enclosing_class(db, index, scope_id).unwrap(); let typing_self = typing_self(db, scope_id, typevar_binding_context, class.into()) @@ -1016,8 +1022,9 @@ impl<'db> FunctionLiteral<'db> { implementation: OverloadLiteral<'db>, ) -> FunctionBodyKind { let definition = implementation.definition(db); - let python_file = definition.python_file(db); - let env = ProgramEnvironment::from_file(python_file); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); let file = python_file.file(db); let module = parsed_module(db, python_file).load(db); let node = implementation.node(db, file, &module); @@ -1343,6 +1350,10 @@ impl<'db> FunctionType<'db> { self.literal(db).last_definition.python_file(db) } + pub(crate) fn program_file(self, db: &'db dyn Db) -> ProgramFile<'db> { + self.literal(db).last_definition.program_file(db) + } + /// Returns the AST node for this function. pub(super) fn node<'ast>( self, @@ -2376,7 +2387,9 @@ impl KnownFunction { let candidate = Self::from_str(name).ok()?; candidate - .check_module(file_to_module(db, definition.python_file(db))?.known(db)?) + .check_module( + file_to_module(db, definition.program_file(db).resolver_file(db))?.known(db)?, + ) .then_some(candidate) } @@ -2894,11 +2907,15 @@ impl KnownFunction { let Some(module_name) = ModuleName::new(module_name) else { return; }; - let Some(module) = resolve_module(db, context.python_file(), &module_name) else { + let importing_file = ImportingFile::File( + context.file(), + context.program_environment().resolver_environment(db), + ); + let Some(module) = resolve_module(db, importing_file, &module_name) else { return; }; - overload.set_return_type(Type::module_literal(db, context.python_file(), module)); + overload.set_return_type(Type::module_literal(db, context.program_file(), module)); } KnownFunction::TotalOrdering => { diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 7be886863c..3bb46e0c5a 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -256,7 +256,7 @@ pub(crate) fn typing_self<'db>( class: ClassLiteral<'db>, ) -> Option> { let env = ProgramEnvironment::from_scope(scope_id); - let index = semantic_index(db, scope_id.python_file(db)); + let index = semantic_index(db, scope_id.program_file(db)); let identity = TypeVarIdentity::new( db, @@ -344,7 +344,7 @@ pub(crate) fn typing_self<'db>( #[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] pub struct GenericContext<'db> { #[returns(copy)] - pub(crate) program: Program, + pub(crate) program: Program<'db>, #[returns(ref)] variables_inner: FxOrderMap, BoundTypeVarInstance<'db>>, @@ -437,7 +437,7 @@ impl<'db> GenericContext<'db> { fn from_typevar_instances_in_program( db: &'db dyn Db, - program: Program, + program: Program<'db>, type_params: impl IntoIterator>, ) -> Self { Self::new_internal( diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index bb84a9acd2..0ff22b607e 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -15,16 +15,15 @@ use crate::types::{ }; use crate::{Db, DisplaySettings, HasDefinition, HasType, ProgramEnvironment, SemanticModel}; use itertools::Either; -use ruff_db::PythonFile; use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; use ruff_db::source::source_text; use ruff_python_ast::{self as ast, AnyNodeRef, name::Name}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; -use ty_module_resolver::Module; +use ty_module_resolver::{ImportingFile, Module, ResolverFile}; use ty_python_core::definition::{Definition, DefinitionKind, NestedBindingExecution}; -use ty_python_core::{attribute_scopes, global_scope, semantic_index, use_def_map}; +use ty_python_core::{ProgramFile, attribute_scopes, global_scope, semantic_index, use_def_map}; mod unreachable_code; #[path = "ide_support/unused_bindings.rs"] @@ -64,7 +63,8 @@ pub fn definitions_for_name<'db>( alias_resolution: ImportAliasResolution, ) -> Vec> { let db = model.db(); - let file = model.python_file(); + let env = model.program_environment(); + let file = model.program_file(); let index = semantic_index(db, file); // Get the scope for this name expression @@ -165,12 +165,11 @@ pub fn definitions_for_name<'db>( let mut resolved_definitions = Vec::new(); for definition in &all_definitions { - let resolved = resolve_definition(db, *definition, Some(name_str), alias_resolution); + let resolved = resolve_definition(db, &env, *definition, Some(name_str), alias_resolution); resolved_definitions.extend(resolved); } // If we didn't find any definitions in scopes, fallback to builtins - let env = model.program_environment(); if resolved_definitions.is_empty() && let Some(builtins_scope) = implicit_builtins_symbol_scope(db, &env, name_str) { @@ -209,6 +208,7 @@ pub fn definitions_for_name<'db>( .flat_map(|def| { resolve_definition( db, + &env, def, Some(name_str), ImportAliasResolution::ResolveAliases, @@ -275,11 +275,16 @@ pub fn definitions_for_attribute<'db>( for ty in expanded_tys { // Handle modules if let Type::ModuleLiteral(module_literal) = ty { - if let Some(module_file) = module_literal.module(db).python_file(db) { + if let Some(module_file) = module_literal + .module(db) + .file(db) + .map(|file| ProgramFile::new(db, file, model.program_environment().program(db))) + { let module_scope = global_scope(db, module_file); for def in find_symbol_in_scope(db, module_scope, name_str) { resolved.extend(resolve_definition( db, + &env, def, Some(name_str), ImportAliasResolution::ResolveAliases, @@ -437,7 +442,7 @@ impl<'db> ImplementationsFinder<'db> { pub fn implementations_for_file<'scan>( &'scan self, db: &'scan dyn Db, - file: PythonFile<'scan>, + file: ProgramFile<'scan>, ) -> Vec> where 'db: 'scan, @@ -524,7 +529,7 @@ impl<'db> ImplementationsFinder<'db> { .and_then(Type::as_property_instance) .and_then(|property| property.accessor_role(db, function_definition)); let class_node = containing_scope.node(db).as_class()?; - let class_definition = semantic_index(db, containing_scope.python_file(db)) + let class_definition = semantic_index(db, containing_scope.program_file(db)) .expect_single_definition(class_node); let class_ty = binding_type(db, class_definition); let root = extract_class_literal(db, &env, class_ty)?; @@ -637,7 +642,7 @@ impl<'db> ImplementationsFinder<'db> { /// Finds subclasses of `roots` defined in `file`. fn class_implementations_for_file<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, roots: &FxHashSet>, ) -> Vec> { if !contains_identifier(&source_text(db, file.file(db)), "class") { @@ -680,6 +685,7 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( attribute_name: &str, ) -> Vec> { let db = model.db(); + let env = model.program_environment(); let mut resolved = Vec::new(); 'scopes: for ancestor in class_literal .iter_mro(db) @@ -694,6 +700,7 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( let use_def = use_def_map(db, class_scope); let resolved_in_scope = resolve_reachable_definitions( db, + &env, attribute_name, use_def .reachable_symbol_declarations(place_id) @@ -711,7 +718,7 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( } // Look for instance attributes in method scopes (e.g., self.x = 1) - let index = semantic_index(db, class_scope.python_file(db)); + let index = semantic_index(db, class_scope.program_file(db)); for function_scope_id in attribute_scopes(db, class_scope) { if let Some(place_id) = index @@ -721,6 +728,7 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( let use_def = index.use_def_map(function_scope_id); let resolved_in_scope = resolve_reachable_definitions( db, + &env, attribute_name, use_def .reachable_member_declarations(place_id) @@ -745,7 +753,7 @@ fn definitions_for_attribute_in_class_hierarchy<'db>( /// Finds member implementations contributed by subclasses of `roots` defined in `file`. fn member_implementations_for_file<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, roots: &FxHashSet>, member_name: &str, accessor_role: Option, @@ -877,7 +885,7 @@ fn own_member_definitions<'db>( } } - let file = class_scope.python_file(db); + let file = class_scope.program_file(db); let index = semantic_index(db, file); let mut instance_definitions = Vec::new(); for function_scope_id in attribute_scopes(db, class_scope) { @@ -1073,7 +1081,7 @@ fn user_visible_definitions<'db>( match definition.kind(db) { DefinitionKind::NestedBindings(nested) => { - let index = semantic_index(db, definition.python_file(db)); + let index = semantic_index(db, definition.program_file(db)); let sources = nested .visible_binding_sources(index, definition.file_scope(db)) .flatten() @@ -1110,8 +1118,8 @@ fn is_reachable_implementation_definition<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> bool { - let file = definition.python_file(db); - let parsed = parsed_module(db, file).load(db); + let file = definition.program_file(db); + let parsed = parsed_module(db, file.python_file(db)).load(db); is_range_reachable( db, semantic_index(db, file), @@ -1152,6 +1160,7 @@ fn is_ascii_identifier_continue(byte: u8) -> bool { fn resolve_reachable_definitions<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, symbol_name: &str, definitions: impl IntoIterator>, ) -> Vec> { @@ -1160,6 +1169,7 @@ fn resolve_reachable_definitions<'db>( .flat_map(|definition| { resolve_definition( db, + env, definition, Some(symbol_name), ImportAliasResolution::ResolveAliases, @@ -1267,9 +1277,11 @@ pub fn definitions_for_imported_symbol<'db>( alias_resolution: ImportAliasResolution, ) -> Vec> { let mut visited = FxHashSet::default(); + let env = model.program_environment(); resolve_definition::resolve_from_import_definitions( model.db(), - model.python_file(), + &env, + ImportingFile::File(model.file(), env.resolver_environment(model.db())), import_node, symbol_name, &mut visited, @@ -2056,7 +2068,6 @@ mod resolve_definition { } use indexmap::IndexSet; - use ruff_db::PythonFile; use ruff_db::files::{FileRange, vendored_path_to_file}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_db::system::SystemPath; @@ -2066,14 +2077,17 @@ mod resolve_definition { use ruff_text_size::TextRange; use rustc_hash::FxHashSet; use tracing::trace; - use ty_module_resolver::{ModuleName, file_to_module, resolve_module, resolve_real_module}; + use ty_module_resolver::{ + ImportingFile, ModuleName, file_to_module, resolve_module, resolve_real_module, + }; use crate::Db; + use crate::ProgramEnvironment; use crate::module_docstring; use crate::types::binding_type; use ty_python_core::definition::{Definition, DefinitionCategory, DefinitionKind}; use ty_python_core::scope::{NodeWithScopeKind, ScopeId}; - use ty_python_core::{global_scope, place_table, semantic_index, use_def_map}; + use ty_python_core::{ProgramFile, global_scope, place_table, semantic_index, use_def_map}; /// Represents the result of resolving an import to either a specific definition or /// a specific range within a file. @@ -2085,7 +2099,7 @@ mod resolve_definition { /// The import resolved to a specific definition within a module Definition(Definition<'db>), /// The import resolved to an entire module - Module(PythonFile<'db>), + Module(ProgramFile<'db>), /// The import resolved to a file with a specific range FileWithRange(FileRange), } @@ -2126,9 +2140,9 @@ mod resolve_definition { } } - fn python_file(&self, db: &'db dyn Db) -> Option> { + fn program_file(&self, db: &'db dyn Db) -> Option> { match *self { - ResolvedDefinition::Definition(definition) => Some(definition.python_file(db)), + ResolvedDefinition::Definition(definition) => Some(definition.program_file(db)), ResolvedDefinition::Module(file) => Some(file), ResolvedDefinition::FileWithRange(_) => None, } @@ -2137,7 +2151,7 @@ mod resolve_definition { pub fn docstring(&self, db: &'db dyn Db) -> Option { match self { ResolvedDefinition::Definition(definition) => definition.docstring(db), - ResolvedDefinition::Module(file) => module_docstring(db, *file), + ResolvedDefinition::Module(file) => module_docstring(db, file.python_file(db)), ResolvedDefinition::FileWithRange(_) => None, } } @@ -2198,6 +2212,7 @@ mod resolve_definition { /// Always returns at least the original definition as a fallback if resolution fails. pub(crate) fn resolve_definition<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, definition: Definition<'db>, symbol_name: Option<&str>, alias_resolution: ImportAliasResolution, @@ -2205,6 +2220,7 @@ mod resolve_definition { let mut visited = FxHashSet::default(); let resolved = resolve_definition_recursive( db, + env, definition, &mut visited, symbol_name, @@ -2222,6 +2238,7 @@ mod resolve_definition { /// Helper function to resolve import definitions recursively. fn resolve_definition_recursive<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, definition: Definition<'db>, visited: &mut FxHashSet>, symbol_name: Option<&str>, @@ -2237,8 +2254,8 @@ mod resolve_definition { match kind { DefinitionKind::Import(import_def) => { - let file = definition.python_file(db); - let module = parsed_module(db, file).load(db); + let file = definition.program_file(db); + let module = parsed_module(db, file.python_file(db)).load(db); let alias = import_def.alias(&module); if alias.asname.is_some() @@ -2253,13 +2270,16 @@ mod resolve_definition { }; // Resolve the module to its file - let Some(resolved_module) = resolve_module(db, file, &module_name) else { + let importing_file = + ImportingFile::File(file.file(db), env.resolver_environment(db)); + let Some(resolved_module) = resolve_module(db, importing_file, &module_name) else { return Vec::new(); // Module not found, return empty list }; - let Some(module_file) = resolved_module.python_file(db) else { + let Some(module_file) = resolved_module.file(db) else { return Vec::new(); // No file for module, return empty list }; + let module_file = ProgramFile::new(db, module_file, env.program(db)); // For simple imports like "import os", we want to navigate to the module itself. // Return the module file directly instead of trying to find definitions within it. @@ -2267,8 +2287,8 @@ mod resolve_definition { } DefinitionKind::ImportFrom(import_from_def) => { - let file = definition.python_file(db); - let module = parsed_module(db, file).load(db); + let file = definition.program_file(db); + let module = parsed_module(db, file.python_file(db)).load(db); let import_node = import_from_def.import(&module); let alias = import_from_def.alias(&module); @@ -2282,7 +2302,8 @@ mod resolve_definition { // (alias.name), not the local alias (symbol_name) resolve_from_import_definitions( db, - file, + env, + ImportingFile::File(file.file(db), env.resolver_environment(db)), import_node, &alias.name, visited, @@ -2292,15 +2313,16 @@ mod resolve_definition { // For star imports, try to resolve to the specific symbol being accessed DefinitionKind::StarImport(star_import_def) => { - let file = definition.python_file(db); - let module = parsed_module(db, file).load(db); + let file = definition.program_file(db); + let module = parsed_module(db, file.python_file(db)).load(db); let import_node = star_import_def.import(&module); // If we have a symbol name, use the helper to resolve it in the target module if let Some(symbol_name) = symbol_name { resolve_from_import_definitions( db, - file, + env, + ImportingFile::File(file.file(db), env.resolver_environment(db)), import_node, symbol_name, visited, @@ -2320,7 +2342,8 @@ mod resolve_definition { /// Helper function to resolve import definitions for `ImportFrom` and `StarImport` cases. pub(crate) fn resolve_from_import_definitions<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + env: &ProgramEnvironment<'db>, + importing_file: ImportingFile<'db>, import_node: &ast::StmtImportFrom, symbol_name: &str, visited: &mut FxHashSet>, @@ -2331,7 +2354,7 @@ mod resolve_definition { if let Some(asname) = &alias.asname { if asname.as_str() == symbol_name { return vec![ResolvedDefinition::FileWithRange(FileRange::new( - file.file(db), + importing_file.file(db), asname.range, ))]; } @@ -2340,22 +2363,26 @@ mod resolve_definition { } // Resolve the module being imported from (handles both relative and absolute imports) - let Some(module_name) = ModuleName::from_import_statement(db, file, import_node).ok() + let Some(module_name) = + ModuleName::from_import_statement(db, importing_file, import_node).ok() else { return Vec::new(); }; - let Some(resolved_module) = resolve_module(db, file, &module_name) else { + let Some(resolved_module) = resolve_module(db, importing_file, &module_name) else { return Vec::new(); }; // Resolve the target module file - let module_file = resolved_module.python_file(db); + let module_file = resolved_module + .file(db) + .map(|file| ProgramFile::new(db, file, env.program(db))); let Some(module_file) = module_file else { // No file means this is a namespace package, try to import the submodule return Vec::from_iter(resolve_from_import_submodule_definitions( db, - file, + env, + importing_file, symbol_name, module_name, )); @@ -2368,8 +2395,14 @@ mod resolve_definition { // Recursively resolve any import definitions found in the target module let mut resolved_definitions = Vec::new(); for def in definitions_in_module { - let resolved = - resolve_definition_recursive(db, def, visited, Some(symbol_name), alias_resolution); + let resolved = resolve_definition_recursive( + db, + env, + def, + visited, + Some(symbol_name), + alias_resolution, + ); resolved_definitions.extend(resolved); } @@ -2382,7 +2415,8 @@ mod resolve_definition { // `child` has no binding in `pkg/__init__.py`. Vec::from_iter(resolve_from_import_submodule_definitions( db, - file, + env, + importing_file, symbol_name, module_name, )) @@ -2394,15 +2428,16 @@ mod resolve_definition { // Helper to resolve `from x.y import z` assuming `x.y.z` is a module. fn resolve_from_import_submodule_definitions<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + env: &ProgramEnvironment<'db>, + importing_file: ImportingFile<'db>, symbol_name: &str, module_name: ModuleName, ) -> Option> { let submodule_name = ModuleName::new(symbol_name)?; let mut full_submodule_name = module_name; full_submodule_name.extend(&submodule_name); - let module = resolve_module(db, file, &full_submodule_name)?; - let file = module.python_file(db)?; + let module = resolve_module(db, importing_file, &full_submodule_name)?; + let file = ProgramFile::new(db, module.file(db)?, env.program(db)); Some(ResolvedDefinition::Module(file)) } @@ -2449,13 +2484,14 @@ mod resolve_definition { def: &ResolvedDefinition<'db>, cached_vendored_typeshed: Option<&SystemPath>, ) -> Option>> { - let Some(stub_parse_file) = def.python_file(db) else { + let Some(stub_program_file) = def.program_file(db) else { trace!("Found arbitrary FileWithRange while stub mapping, giving up"); return None; }; + let env = ProgramEnvironment::from_file(stub_program_file); // If the file isn't a stub, this is presumably the real definition - let stub_file = stub_parse_file.file(db); + let stub_file = stub_program_file.file(db); trace!("Stub mapping definition in: {}", stub_file.path(db)); if !stub_file.is_stub(db) { trace!("File isn't a stub, no stub mapping to do"); @@ -2472,7 +2508,7 @@ mod resolve_definition { // we're in typeshed to successfully stub-map to the Real Stdlib. So here we attempt // to do just that. The resulting file must not be used for anything other than // this module lookup, as the `ResolvedDefinition` we're handling isn't for that file. - let mut stub_file_for_module_lookup = stub_parse_file; + let mut stub_file_for_module_lookup = stub_program_file; if let Some(vendored_typeshed) = cached_vendored_typeshed && let Some(stub_path) = stub_file.path(db).as_system_path() && let Ok(rel_path) = stub_path.strip_prefix(vendored_typeshed) @@ -2483,12 +2519,12 @@ mod resolve_definition { "Stub is cached vendored typeshed: {}", typeshed_file.path(db) ); - stub_file_for_module_lookup = - PythonFile::new(db, typeshed_file, stub_parse_file.python_version(db)); + stub_file_for_module_lookup = ProgramFile::new(db, typeshed_file, env.program(db)); } // It's definitely a stub, so now rerun module resolution but with stubs disabled. - let stub_module = file_to_module(db, stub_file_for_module_lookup)?; + let resolver_file = stub_file_for_module_lookup.resolver_file(db); + let stub_module = file_to_module(db, resolver_file)?; trace!("Found stub module: {}", stub_module.name(db)); // We need to pass an importing file to `resolve_real_module` which is a bit odd // here because there isn't really an importing file. However this `resolve_real_module` @@ -2502,10 +2538,13 @@ mod resolve_definition { if is_builtin_module(stub_module.python_version(db).minor, stub_module.name(db)) { return None; } - let real_module = - resolve_real_module(db, stub_file_for_module_lookup, stub_module.name(db))?; + let real_module = resolve_real_module( + db, + ImportingFile::ResolverFile(resolver_file), + stub_module.name(db), + )?; trace!("Found real module: {}", real_module.name(db)); - let real_parse_file = real_module.python_file(db)?; + let real_parse_file = ProgramFile::new(db, real_module.file(db)?, env.program(db)); let real_file = real_parse_file.file(db); trace!("Found real file: {}", real_file.path(db)); @@ -2539,7 +2578,7 @@ mod resolve_definition { path.push(leaf); // Get the ancestors of the path (all the definitions we're nested under) - let index = semantic_index(db, definition.python_file(db)); + let index = semantic_index(db, definition.program_file(db)); for (_scope_id, scope) in index.ancestor_scopes(definition.file_scope(db)) { let node = scope.node(); let component = definition_path_component_for_node(&stub_ref, node) @@ -2592,6 +2631,7 @@ mod resolve_definition { .flat_map(|definition| { resolve_definition( db, + &env, definition, Some(component), ImportAliasResolution::ResolveAliases, @@ -2717,7 +2757,7 @@ pub struct TypeHierarchyClass<'db> { /// The name of the class. pub name: Name, /// The file containing the class definition. - pub file: PythonFile<'db>, + pub file: ResolverFile<'db>, /// The range covering the full class definition header. pub full_range: TextRange, /// The range of the class name (for selection/focus). @@ -2789,7 +2829,7 @@ pub fn type_hierarchy_subtypes<'db>( let Some(target_class) = extract_class_literal(db, env, ty) else { return vec![]; }; - direct_subtypes(db, target_class, modules) + direct_subtypes(db, env, target_class, modules) .into_iter() .map(|class_literal| class_literal_to_hierarchy_info(db, class_literal)) .collect() @@ -2806,6 +2846,7 @@ pub fn type_hierarchy_subtypes<'db>( /// For `Animal`, this returns `Dog`, but not `LoudDog`. fn direct_subtypes<'db>( db: &'db dyn Db, + env: &ProgramEnvironment<'db>, target_class: ClassLiteral<'db>, modules: &[Module<'db>], ) -> Vec> { @@ -2814,10 +2855,9 @@ fn direct_subtypes<'db>( let mut subtypes = vec![]; for &module in modules { - let Some(python_file) = module.python_file(db) else { + let Some(file) = module.file(db) else { continue; }; - let file = python_file.file(db); // Note that this will always consider namespace // packages to be "not firsty party." This isn't @@ -2847,8 +2887,9 @@ fn direct_subtypes<'db>( continue; } - let file_env = ProgramEnvironment::from_file(python_file); - for class_ty in reachable_class_literals_in_file(db, python_file) { + let program_file = ProgramFile::new(db, file, env.program(db)); + let file_env = ProgramEnvironment::from_file(program_file); + for class_ty in reachable_class_literals_in_file(db, program_file) { let bases = class_ty.explicit_bases(db); let is_subtype = if target_is_object && bases.is_empty() @@ -2872,11 +2913,11 @@ fn direct_subtypes<'db>( /// Enumerates the reachable class definitions in `file`. fn reachable_class_literals_in_file<'db>( db: &'db dyn Db, - file: PythonFile<'db>, + file: ProgramFile<'db>, ) -> Vec> { let env = ProgramEnvironment::from_file(file); let index = semantic_index(db, file); - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, file.python_file(db)).load(db); let mut classes = Vec::new(); for scope_id in index.scope_ids() { @@ -2945,7 +2986,7 @@ fn class_literal_to_hierarchy_info<'db>( class_literal: ClassLiteral<'db>, ) -> TypeHierarchyClass<'db> { let name = class_literal.name(db).clone(); - let file = class_literal.python_file(db); + let file = class_literal.program_file(db).resolver_file(db); let (full_range, selection_range) = match class_literal { ClassLiteral::Static(static_class) => { @@ -3076,9 +3117,9 @@ mod tests { use super::{CallArgumentForm, call_argument_forms, contains_identifier}; use crate::SemanticModel; use crate::db::tests::TestDbBuilder; - use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; + use ty_python_core::ProgramFile; #[test] fn source_candidate_prefilters_use_identifier_boundaries() { @@ -3105,8 +3146,8 @@ cast(val="", typ=int) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let file = PythonFile::new(&db, file, db.python_version()); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let call = parsed .suite() .last() @@ -3146,8 +3187,8 @@ f(y="", x=1) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let file = PythonFile::new(&db, file, db.python_version()); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let call = parsed .suite() .last() @@ -3183,8 +3224,8 @@ f(val="", typ=int) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let file = PythonFile::new(&db, file, db.python_version()); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let call = parsed .suite() .last() @@ -3221,8 +3262,8 @@ f("", int) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let file = PythonFile::new(&db, file, db.python_version()); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let call = parsed .suite() .last() @@ -3262,8 +3303,8 @@ f(int, x) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let file = PythonFile::new(&db, file, db.python_version()); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let call = parsed .suite() .last() @@ -3307,8 +3348,8 @@ TypeAliasType("Alias", int) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let file = PythonFile::new(&db, file, db.python_version()); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let calls: Vec<_> = parsed .suite() .iter() @@ -3353,8 +3394,8 @@ cast(*args) .build()?; let file = system_path_to_file(&db, "/src/foo.py").unwrap(); - let file = PythonFile::new(&db, file, db.python_version()); - let parsed = parsed_module(&db, file).load(&db); + let file = ProgramFile::new(&db, file, db.program_environment().program(&db)); + let parsed = parsed_module(&db, file.python_file(&db)).load(&db); let call = parsed .suite() .last() diff --git a/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs b/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs index d3f54256cb..871bf4eb77 100644 --- a/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs +++ b/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs @@ -2,8 +2,8 @@ use crate::Db; use crate::reachability::is_reachable; use get_size2::GetSize; use itertools::Itertools; -use ruff_db::PythonFile; use ruff_text_size::TextRange; +use ty_python_core::ProgramFile; use ty_python_core::reachability_constraints::ScopedReachabilityConstraintId; use ty_python_core::semantic_index; @@ -45,7 +45,7 @@ pub enum UnreachableKind { /// `ALWAYS_FALSE` constraints are classified as unconditional; all others are /// unreachable only under the current analysis. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] -pub fn unreachable_ranges(db: &dyn Db, file: PythonFile<'_>) -> Box<[UnreachableRange]> { +pub fn unreachable_ranges(db: &dyn Db, file: ProgramFile<'_>) -> Box<[UnreachableRange]> { let index = semantic_index(db, file); let mut unreachable = Vec::new(); for scope_id in index.scope_ids() { @@ -94,13 +94,13 @@ mod tests { use super::{UnreachableKind, unreachable_ranges}; use crate::db::tests::{TestDb, TestDbBuilder}; use insta::assert_snapshot; - use ruff_db::PythonFile; use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, DisplayDiagnosticConfig, DisplayDiagnostics, Severity, }; use ruff_db::files::{FileRange, system_path_to_file}; use ruff_python_ast::PythonVersion; use ruff_python_trivia::textwrap::dedent; + use ty_python_core::ProgramFile; use ty_python_core::platform::PythonPlatform; const TEST_PATH: &str = "/src/main.py"; @@ -147,23 +147,26 @@ mod tests { fn render_unreachable_diagnostics(db: &TestDb, path: &str) -> String { let file = system_path_to_file(db, path).unwrap(); - let diagnostics = unreachable_ranges(db, PythonFile::new(db, file, db.python_version())) - .iter() - .map(|range| { - let mut diagnostic = Diagnostic::new( - DiagnosticId::lint("unreachable-code"), - Severity::Info, - match range.kind { - UnreachableKind::Unconditional => "Code is always unreachable", - UnreachableKind::CurrentAnalysis => "Code is unreachable", - }, - ); - diagnostic.annotate(Annotation::primary( - FileRange::new(file, range.range).into(), - )); - diagnostic - }) - .collect::>(); + let diagnostics = unreachable_ranges( + db, + ProgramFile::new(db, file, db.program_environment().program(db)), + ) + .iter() + .map(|range| { + let mut diagnostic = Diagnostic::new( + DiagnosticId::lint("unreachable-code"), + Severity::Info, + match range.kind { + UnreachableKind::Unconditional => "Code is always unreachable", + UnreachableKind::CurrentAnalysis => "Code is unreachable", + }, + ); + diagnostic.annotate(Annotation::primary( + FileRange::new(file, range.range).into(), + )); + diagnostic + }) + .collect::>(); DisplayDiagnostics::new( db, diff --git a/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs b/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs index 30c2f941e7..11e73a8af6 100644 --- a/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs +++ b/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs @@ -3,7 +3,6 @@ use crate::reachability::is_reachable; use crate::types::function::FunctionDecorators; use crate::types::infer::function_known_decorator_flags; use get_size2::GetSize; -use ruff_db::PythonFile; use ruff_db::parsed::parsed_module; use ruff_python_ast::name::Name; use ruff_text_size::TextRange; @@ -11,7 +10,7 @@ use rustc_hash::FxHashSet; use ty_python_core::definition::{DefinitionCategory, DefinitionKind, DefinitionState}; use ty_python_core::place::ScopedPlaceId; use ty_python_core::scope::{FileScopeId, ScopeKind}; -use ty_python_core::{SemanticIndex, semantic_index}; +use ty_python_core::{ProgramFile, SemanticIndex, semantic_index}; /// Returns `true` for definition kinds that create user-facing bindings we consider for /// unused-binding diagnostics. @@ -103,9 +102,9 @@ pub struct UnusedBinding { /// without broader reference analysis. Bare local annotations (`x: int`) are also /// reported, but only if the symbol is neither bound nor used elsewhere in the scope. #[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)] -pub fn unused_bindings(db: &dyn Db, file: PythonFile<'_>) -> Box<[UnusedBinding]> { +pub fn unused_bindings(db: &dyn Db, file: ProgramFile<'_>) -> Box<[UnusedBinding]> { let source_file = file.file(db); - let parsed = parsed_module(db, file).load(db); + let parsed = parsed_module(db, file.python_file(db)).load(db); let is_stub_file = source_file.is_stub(db); let index = semantic_index(db, file); let mut unused = Vec::new(); @@ -234,11 +233,11 @@ pub fn unused_bindings(db: &dyn Db, file: PythonFile<'_>) -> Box<[UnusedBinding] mod tests { use super::{UnusedBinding, unused_bindings}; use crate::db::tests::TestDbBuilder; - use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; use ruff_python_ast::name::Name; use ruff_python_trivia::textwrap::dedent; use ruff_text_size::{TextRange, TextSize}; + use ty_python_core::ProgramFile; fn collect_unused_bindings_in_file( path: &str, @@ -246,8 +245,8 @@ mod tests { ) -> anyhow::Result> { let db = TestDbBuilder::new().with_file(path, source).build()?; let file = system_path_to_file(&db, path).unwrap(); - let mut bindings = - unused_bindings(&db, PythonFile::new(&db, file, db.python_version())).to_vec(); + let program = db.program_environment().program(&db); + let mut bindings = unused_bindings(&db, ProgramFile::new(&db, file, program)).to_vec(); bindings.sort_unstable_by_key(|binding| (binding.range.start(), binding.range.end())); Ok(bindings) } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 0df0e79aa3..5e184f98a1 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -135,7 +135,8 @@ pub(crate) fn infer_definition_types<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> DefinitionInference<'db> { - let python_file = definition.python_file(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_definition_types", @@ -144,16 +145,16 @@ pub(crate) fn infer_definition_types<'db>( ) .entered(); - let index = semantic_index(db, python_file); + let index = semantic_index(db, program_file); - let env = ProgramEnvironment::from_file(python_file); + let env = ProgramEnvironment::from_file(program_file); TypeInferenceBuilder::new( db, &env, InferenceRegion::Definition(definition), python_file.file(db), - python_file, + program_file, index, &module, ) @@ -196,18 +197,19 @@ pub(crate) fn function_known_decorators<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> FunctionDecoratorInference<'db> { - let python_file = definition.python_file(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); - let index = semantic_index(db, python_file); + let index = semantic_index(db, program_file); - let env = ProgramEnvironment::from_file(python_file); + let env = ProgramEnvironment::from_file(program_file); TypeInferenceBuilder::new( db, &env, InferenceRegion::FunctionDecorators(definition), python_file.file(db), - python_file, + program_file, index, &module, ) @@ -284,7 +286,8 @@ pub(crate) fn infer_deferred_types<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> DefinitionInference<'db> { - let python_file = definition.python_file(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_deferred_types", @@ -294,16 +297,16 @@ pub(crate) fn infer_deferred_types<'db>( ) .entered(); - let index = semantic_index(db, python_file); + let index = semantic_index(db, program_file); - let env = ProgramEnvironment::from_file(python_file); + let env = ProgramEnvironment::from_file(program_file); TypeInferenceBuilder::new( db, &env, InferenceRegion::Deferred(definition), python_file.file(db), - python_file, + program_file, index, &module, ) @@ -323,13 +326,13 @@ pub(crate) fn infer_complete_scope_types<'db>( // Scopes that may require type context are inferred during the inference of // their outer scope. if scope.accepts_type_context(db) { - let python_file = scope.python_file(db); - let index = semantic_index(db, python_file); + let program_file = scope.program_file(db); + let index = semantic_index(db, program_file); if let Some(parent_scope) = index.parent_scope_id(scope.file_scope_id(db)) { // Note that nested lambdas or comprehensions may require recursing until we reach // an outer scope that is independent of any type context. - return infer_complete_scope_types(db, parent_scope.to_scope_id(db, python_file)); + return infer_complete_scope_types(db, parent_scope.to_scope_id(db, program_file)); } } @@ -367,7 +370,8 @@ pub(crate) fn infer_scope_types_impl<'db>( input: InferScope<'db>, ) -> ScopeInference<'db> { let (scope, tcx) = input.into_inner(db); - let python_file = scope.python_file(db); + let program_file = scope.program_file(db); + let python_file = program_file.python_file(db); let _span = tracing::trace_span!("infer_scope_types", scope=?scope.as_id(), ?python_file).entered(); @@ -375,16 +379,16 @@ pub(crate) fn infer_scope_types_impl<'db>( // Using the index here is fine because the code below depends on the AST anyway. // The isolation of the query is by the return inferred types. - let index = semantic_index(db, python_file); + let index = semantic_index(db, program_file); - let env = ProgramEnvironment::from_file(python_file); + let env = ProgramEnvironment::from_file(program_file); TypeInferenceBuilder::new( db, &env, InferenceRegion::Scope(scope, tcx), python_file.file(db), - python_file, + program_file, index, &module, ) @@ -419,7 +423,8 @@ pub(super) fn infer_expression_types_impl<'db>( ) -> ExpressionInference<'db> { let (expression, tcx) = input.into_inner(db); - let python_file = expression.python_file(db); + let program_file = expression.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_expression_types", @@ -429,16 +434,16 @@ pub(super) fn infer_expression_types_impl<'db>( ) .entered(); - let index = semantic_index(db, python_file); + let index = semantic_index(db, program_file); - let env = ProgramEnvironment::from_file(python_file); + let env = ProgramEnvironment::from_file(program_file); TypeInferenceBuilder::new( db, &env, InferenceRegion::Expression(expression, tcx), python_file.file(db), - python_file, + program_file, index, &module, ) @@ -530,7 +535,7 @@ pub(super) fn infer_statement_types<'db>( StatementInferenceInner::cycle_initial(statement.scope(db), Type::divergent(id)) }, cycle_fn=|db, cycle, previous: &StatementInferenceInner<'db>, inference: StatementInferenceInner<'db>, statement: StatementInner<'db>| { - let env = ProgramEnvironment::from_file(statement.python_file(db)); + let env = ProgramEnvironment::from_file(statement.program_file(db)); inference.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size @@ -539,7 +544,8 @@ fn infer_statement_types_impl<'db>( db: &'db dyn Db, statement: StatementInner<'db>, ) -> StatementInferenceInner<'db> { - let python_file = statement.python_file(db); + let program_file = statement.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_statement_types", @@ -549,16 +555,16 @@ fn infer_statement_types_impl<'db>( ) .entered(); - let index = semantic_index(db, python_file); + let index = semantic_index(db, program_file); - let env = ProgramEnvironment::from_file(python_file); + let env = ProgramEnvironment::from_file(program_file); TypeInferenceBuilder::new( db, &env, InferenceRegion::Statement(statement), python_file.file(db), - python_file, + program_file, index, &module, ) @@ -723,13 +729,14 @@ impl<'db> From> for TypeContext<'db> { returns(ref), cycle_initial=|_, id, _| UnpackResult::cycle_initial(Type::divergent(id)), cycle_fn=|db, cycle, previous: &UnpackResult<'db>, result: UnpackResult<'db>, unpack: Unpack<'db>| { - let env = ProgramEnvironment::from_file(unpack.python_file(db)); + let env = ProgramEnvironment::from_file(unpack.program_file(db)); result.cycle_normalized(db, &env, previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] pub(super) fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> UnpackResult<'db> { - let python_file = unpack.python_file(db); + let program_file = unpack.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let _span = tracing::trace_span!( "infer_unpack_types", @@ -738,8 +745,8 @@ pub(super) fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> U ) .entered(); - let env = ProgramEnvironment::from_file(python_file); - let mut unpacker = Unpacker::new(db, &env, unpack.target_scope(db), python_file, &module); + let env = ProgramEnvironment::from_file(program_file); + let mut unpacker = Unpacker::new(db, &env, unpack.target_scope(db), program_file, &module); unpacker.unpack(unpack.target(db, &module), unpack.value(db)); unpacker.finish() } @@ -1369,7 +1376,8 @@ impl<'db> DefinitionInference<'db> { // Eagerly store more precise types for collection literals to avoid an extra // cycle iteration, i.e., by inferring `list[Divergent]` instead of `Divergent`. if let DefinitionKind::Assignment(assignment) = definition.kind(db) { - let python_file = definition.python_file(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let known_collection = match assignment.value(&module) { ast::Expr::Set(_) => Some(KnownClass::Set), diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 7401736a69..b27e76a486 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -4,7 +4,6 @@ use std::rc::Rc; use compact_str::CompactString; use itertools::Itertools; -use ruff_db::PythonFile; use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; use ruff_db::source::source_text; @@ -20,7 +19,7 @@ use ruff_text_size::{Ranged, TextRange}; use rustc_hash::{FxHashMap, FxHashSet}; use smallvec::SmallVec; use strum::IntoEnumIterator; -use ty_module_resolver::{ModuleName, resolve_module}; +use ty_module_resolver::{ImportingFile, ModuleName, resolve_module}; use ty_python_core::ast_ids::HasScopedUseId; use ty_python_core::statement::StatementInner; @@ -138,8 +137,8 @@ use ty_python_core::predicate::PatternPredicate; use ty_python_core::scope::{FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId, ScopeKind}; use ty_python_core::symbol::{ScopedSymbolId, Symbol}; use ty_python_core::{ - ApplicableConstraints, EnclosingSnapshotResult, EvaluationMode, SemanticIndex, Truthiness, - unpack::UnpackPosition, + ApplicableConstraints, EnclosingSnapshotResult, EvaluationMode, ProgramFile, SemanticIndex, + Truthiness, unpack::UnpackPosition, }; use ty_python_core::{ExpressionNodeKey, Statement}; @@ -465,13 +464,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { env: &'ast ProgramEnvironment<'db>, region: InferenceRegion<'db>, file: File, - python_file: PythonFile<'db>, + program_file: ProgramFile<'db>, index: &'db SemanticIndex<'db>, module: &'ast ParsedModuleRef, ) -> Self { let scope = region.scope(db); Self { - context: InferContext::new(db, env, scope, file, python_file, module), + context: InferContext::new(db, env, scope, file, program_file, module), index, region, scope, @@ -776,8 +775,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.file() } - fn python_file(&self) -> PythonFile<'db> { - self.context.python_file() + fn program_file(&self) -> ProgramFile<'db> { + self.context.program_file() } #[inline] @@ -985,7 +984,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// already in progress for that scope (further up the stack). fn file_expression_type(&self, expression: &ast::Expr) -> Type<'db> { let file_scope = self.index.expression_scope_id(expression); - let expr_scope = file_scope.to_scope_id(self.db(), self.python_file()); + let expr_scope = file_scope.to_scope_id(self.db(), self.program_file()); match self.region { InferenceRegion::Scope(scope, _) if scope == expr_scope => { self.expression_type(expression) @@ -997,7 +996,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { /// Get metadata for a type expression from any scope in the same file. fn file_type_expression_flags(&self, expression: &ast::Expr) -> TypeExpressionFlags { let file_scope = self.index.expression_scope_id(expression); - let expr_scope = file_scope.to_scope_id(self.db(), self.python_file()); + let expr_scope = file_scope.to_scope_id(self.db(), self.program_file()); match self.region { InferenceRegion::Scope(scope, _) if scope == expr_scope => { self.type_expression_flags(expression) @@ -1641,7 +1640,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let PlaceExprRef::Symbol(symbol) = &place && scope.is_global() { - module_type_implicit_global_symbol(db, self.python_file(), symbol.name()) + module_type_implicit_global_symbol(db, self.program_file(), symbol.name()) } else { Place::Undefined.into() } @@ -1702,7 +1701,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(|symbol| { module_type_implicit_global_symbol( db, - self.python_file(), + self.program_file(), symbol.name(), ) }) @@ -2079,7 +2078,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let rhs_scope = self .index .node_scope(NodeWithScopeRef::TypeAlias(type_alias)) - .to_scope_id(self.db(), self.python_file()); + .to_scope_id(self.db(), self.program_file()); let type_alias_ty = Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( @@ -3413,7 +3412,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if let Some(special_form) = target.as_name_expr().and_then(|name| { - SpecialFormType::try_from_file_and_name(self.db(), self.python_file(), &name.id) + let db = self.db(); + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ); + SpecialFormType::try_from_file_and_name(db, importing_file, &name.id) }) { target_ty = Type::SpecialForm(special_form); } @@ -4457,7 +4461,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(name_expr) = target.as_name_expr() && let Some(special_form) = SpecialFormType::try_from_file_and_name( self.db(), - self.python_file(), + ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(self.db()), + ), &name_expr.id, ) { @@ -5063,7 +5070,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { continue; } } - if !module_type_implicit_global_symbol(self.db(), self.python_file(), name) + if !module_type_implicit_global_symbol(self.db(), self.program_file(), name) .place .is_undefined() { @@ -5088,8 +5095,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } fn module_type_from_name(&self, module_name: &ModuleName) -> Option> { - resolve_module(self.db(), self.python_file(), module_name) - .map(|module| Type::module_literal(self.db(), self.python_file(), module)) + let db = self.db(); + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ); + resolve_module(db, importing_file, module_name) + .map(|module| Type::module_literal(self.db(), self.program_file(), module)) } fn infer_decorator(&mut self, decorator: &ast::Decorator) -> Type<'db> { @@ -7714,7 +7726,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let evaluation_mode = EvaluationMode::from_is_async(scope_id.is_async_comprehension(self.index)); let yield_tcx = self.generator_yield_type_context(tcx, evaluation_mode); - let scope = scope_id.to_scope_id(self.db(), self.python_file()); + let scope = scope_id.to_scope_id(self.db(), self.program_file()); let inference = infer_scope_types(self.db(), scope, yield_tcx); self.extend_scope(inference); let yield_type = self.comprehension_element_type(elt, inference); @@ -7794,7 +7806,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { else { return Type::unknown(); }; - let scope = scope_id.to_scope_id(self.db(), self.python_file()); + let scope = scope_id.to_scope_id(self.db(), self.program_file()); let inference = infer_scope_types(self.db(), scope, tcx); self.extend_scope(inference); @@ -7835,7 +7847,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { else { return Type::unknown(); }; - let scope = scope_id.to_scope_id(self.db(), self.python_file()); + let scope = scope_id.to_scope_id(self.db(), self.program_file()); let inference = infer_scope_types(self.db(), scope, tcx); self.extend_scope(inference); @@ -7877,7 +7889,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { else { return Type::unknown(); }; - let scope = scope_id.to_scope_id(self.db(), self.python_file()); + let scope = scope_id.to_scope_id(self.db(), self.program_file()); let inference = infer_scope_types(self.db(), scope, tcx); self.extend_scope(inference); @@ -8333,7 +8345,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return Type::unknown(); }; - let scope = scope_id.to_scope_id(self.db(), self.python_file()); + let scope = scope_id.to_scope_id(self.db(), self.program_file()); // If we have a direct `Callable` type context, we can infer the body with the annotated // return type as type context. @@ -8402,7 +8414,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Collect the types of each distinct key. let mut elements: Vec<(&str, Type<'db>)> = Vec::new(); - for bindings in use_def.multi_bindings_at_use(keyword.scoped_use_id(db, self.python_file())) + for bindings in + use_def.multi_bindings_at_use(keyword.scoped_use_id(db, self.program_file())) { let place = place_from_bindings_with_reachability_cache( db, @@ -9621,7 +9634,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Check the "implicit globals" such as `__doc__`, `__file__`, `__name__`, etc. // These are looked up as attributes on `types.ModuleType`. .or_fall_back_to(db, env, || { - module_type_implicit_global_symbol(db, self.python_file(), symbol_name).map_type( + module_type_implicit_global_symbol(db, self.program_file(), symbol_name).map_type( |ty| { self.narrow_place_with_applicable_constraints( PlaceExprRef::from(&expr), @@ -9723,7 +9736,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return (place, None); } - let use_id = expr_ref.scoped_use_id(db, self.python_file()); + let use_id = expr_ref.scoped_use_id(db, self.program_file()); let place = place_from_bindings_with_reachability_cache( db, env, @@ -9813,7 +9826,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return Place::Undefined.into(); }; - explicit_global_symbol(self.db(), self.python_file(), symbol_name).map_type(|ty| { + explicit_global_symbol(self.db(), self.program_file(), symbol_name).map_type(|ty| { self.narrow_place_with_applicable_constraints(place_expr, ty, constraint_keys) }) } @@ -10067,7 +10080,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // We've reached the defining scope of the variable. Infer its public type. debug_assert!(enclosing_place.is_bound() || enclosing_place.is_declared()); let enclosing_scope_id = - enclosing_scope_file_id.to_scope_id(db, self.python_file()); + enclosing_scope_file_id.to_scope_id(db, self.program_file()); return eagerly_resolved_place.unwrap_or_else(|| { place_by_id( self.db(), @@ -10359,8 +10372,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let mut maybe_submodule_name = module_name.clone(); maybe_submodule_name.extend(&relative_submodule); - if resolve_module(db, self.python_file(), &maybe_submodule_name) - .is_some() + if resolve_module( + db, + ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ), + &maybe_submodule_name, + ) + .is_some() { if let Some(builder) = self .context @@ -10488,6 +10508,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { hint_if_stdlib_attribute_exists_on_other_versions( db, + env, diagnostic, value_type, attr_name, @@ -11542,7 +11563,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.program_environment(), region, self.file(), - self.python_file(), + self.program_file(), index, self.module(), ); diff --git a/crates/ty_python_semantic/src/types/infer/builder/class.rs b/crates/ty_python_semantic/src/types/infer/builder/class.rs index 1ac95a6db5..db731bfdb6 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/class.rs @@ -15,7 +15,7 @@ use crate::types::{ special_form::TypeQualifier, }; use ruff_python_ast::{self as ast, helpers::any_over_expr}; -use ty_module_resolver::{KnownModule, file_to_module}; +use ty_module_resolver::{ImportingFile, KnownModule, file_to_module}; use ty_python_core::{definition::Definition, scope::NodeWithScopeRef}; impl<'db> TypeInferenceBuilder<'db, '_> { @@ -107,12 +107,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let body_scope = self .index .node_scope(NodeWithScopeRef::Class(class_node)) - .to_scope_id(db, self.python_file()); + .to_scope_id(db, self.program_file()); - let maybe_known_class = KnownClass::try_from_file_and_name(db, self.python_file(), name); + let file = self.program_file(); + let importing_file = ImportingFile::File(file.file(db), env.resolver_environment(db)); + let maybe_known_class = KnownClass::try_from_file_and_name(db, importing_file, name); - let known_module = - || file_to_module(db, self.python_file()).and_then(|module| module.known(db)); + let known_module = || { + file_to_module(db, importing_file.resolver_file(db)).and_then(|module| module.known(db)) + }; let in_typing_module = || { matches!( known_module(), diff --git a/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs b/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs index b38d93780e..80e89a7fdf 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs @@ -63,7 +63,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { let class_body_scope = class_literal.body_scope(db); let class_scope_id = class_body_scope.file_scope_id(db); - let class_index = semantic_index(db, class_body_scope.python_file(db)); + let class_index = semantic_index(db, class_body_scope.program_file(db)); let place_table = class_index.place_table(class_scope_id); let Some(symbol_id) = place_table.symbol_id(attribute) else { continue; @@ -257,7 +257,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some((class_literal, _)) = class_ty.static_class_literal(db) { let class_body_scope = class_literal.body_scope(db); let class_scope_id = class_body_scope.file_scope_id(db); - let class_index = semantic_index(db, class_body_scope.python_file(db)); + let class_index = semantic_index(db, class_body_scope.program_file(db)); let pt = class_index.place_table(class_scope_id); if let Some(symbol) = pt.symbol_by_name(attribute) diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs index 8d21840fe9..f7dbc64059 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -80,7 +80,7 @@ impl<'db> ExpectedReturnType<'db> { } } - let env = ProgramEnvironment::from_file(function.python_file(db)); + let env = ProgramEnvironment::from_file(function.program_file(db)); let public = normalize( db, &env, @@ -417,7 +417,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let body_scope = self .index .node_scope(NodeWithScopeRef::Function(function)) - .to_scope_id(db, self.python_file()); + .to_scope_id(db, self.program_file()); let overload_literal = OverloadLiteral::new( db, @@ -601,7 +601,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let type_params_scope = self .index .node_scope(NodeWithScopeRef::FunctionTypeParameters(function)) - .to_scope_id(db, self.python_file()); + .to_scope_id(db, self.program_file()); let type_params_inference = infer_scope_types(self.db(), type_params_scope, TypeContext::default()); @@ -1016,7 +1016,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) -> Option> { let env = self.program_environment(); let db = self.db(); - let file = self.python_file(); + let file = self.program_file(); let function_scope_id = self.scope(); let function_scope = function_scope_id.scope(db); diff --git a/crates/ty_python_semantic/src/types/infer/builder/imports.rs b/crates/ty_python_semantic/src/types/infer/builder/imports.rs index ac2385519c..de63f9295f 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/imports.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/imports.rs @@ -1,7 +1,8 @@ use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextRange}; use ty_module_resolver::{ - ModuleName, ModuleNameResolutionError, ModuleResolveMode, resolve_module, search_paths, + ImportingFile, ModuleName, ModuleNameResolutionError, ModuleResolveMode, resolve_module, + search_paths, }; use crate::{ @@ -69,8 +70,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if level == 0 { if let Some(module_name) = module_name { - let program = ty_python_core::program::Program::get(db); - let typeshed_versions = program.search_paths(db).typeshed_versions(); + let resolver_environment = self.program_environment().program(db); + let typeshed_versions = resolver_environment.search_paths(db).typeshed_versions(); // Loop over ancestors in case we have info on the parent module but not submodule for module_name in module_name.ancestors() { @@ -96,16 +97,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } } else { + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ); if let Some(better_level) = (0..level).rev().find(|reduced_level| { - let Ok(module_name) = ModuleName::from_identifier_parts( - db, - self.python_file(), - module, - *reduced_level, - ) else { + let Ok(module_name) = + ModuleName::from_identifier_parts(db, importing_file, module, *reduced_level) + else { return false; }; - resolve_module(db, self.python_file(), &module_name).is_some() + resolve_module(db, importing_file, &module_name).is_some() }) { diagnostic .help("The module can be resolved if the number of leading dots is reduced"); @@ -124,7 +126,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Add search paths information to the diagnostic // Use the same search paths function that is used in actual module resolution let verbose = db.verbose(); - let search_paths = search_paths(db, ModuleResolveMode::Typing); + let search_paths = search_paths( + db, + self.program_environment().resolver_environment(db), + ModuleResolveMode::Typing, + ); diagnostic.info(format_args!( "Searched in the following paths during module resolution:" @@ -271,7 +277,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { format_import_from_module(*level, module), self.file().path(db), ); - let module_name = ModuleName::from_import_statement(db, self.python_file(), import_from); + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ); + let module_name = ModuleName::from_import_statement(db, importing_file, import_from); let module_name = match module_name { Ok(module_name) => module_name, @@ -300,7 +310,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }; - if resolve_module(db, self.python_file(), &module_name).is_none() { + if resolve_module(db, importing_file, &module_name).is_none() { self.report_unresolved_import(module_ref.range(), *level, module, Some(&module_name)); } } @@ -313,8 +323,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) { let db = self.db(); - let Ok(module_name) = - ModuleName::from_import_statement(db, self.python_file(), import_from) + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ); + let Ok(module_name) = ModuleName::from_import_statement(db, importing_file, import_from) else { self.add_unknown_declaration_with_binding(alias.into(), definition); return; @@ -334,7 +347,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return; } - let Some(module) = resolve_module(db, self.python_file(), &module_name) else { + let Some(module) = resolve_module(db, importing_file, &module_name) else { self.add_unknown_declaration_with_binding(alias.into(), definition); return; }; @@ -342,7 +355,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let module_literal = ModuleLiteralType::new( db, module, - module.kind(db).is_package().then_some(self.python_file()), + module.kind(db).is_package().then_some(self.program_file()), ); let module_ty = Type::ModuleLiteral(module_literal); @@ -510,6 +523,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if !submodule_hint_added { hint_if_stdlib_attribute_exists_on_other_versions( db, + self.program_environment(), diagnostic, module_ty, name, @@ -537,15 +551,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definition: Definition<'db>, ) { let db = self.db(); + let importing_file = ImportingFile::File( + self.file(), + self.program_environment().resolver_environment(db), + ); // Get this package's absolute module name by resolving `.`, and make sure it exists - let Ok(thispackage_name) = ModuleName::package_for_file(db, self.python_file()) else { + let Ok(thispackage_name) = ModuleName::package_for_file(db, importing_file) else { self.add_binding(import_from.into(), definition) .insert(self, Type::unknown()); return; }; - let Some(module) = resolve_module(db, self.python_file(), &thispackage_name) else { + let Some(module) = resolve_module(db, importing_file, &thispackage_name) else { self.add_binding(import_from.into(), definition) .insert(self, Type::unknown()); return; @@ -557,7 +575,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // First we normalize to `whatever.thispackage.x.y` let Some(final_part) = ModuleName::from_identifier_parts( db, - self.python_file(), + importing_file, import_from.module.as_deref(), import_from.level, ) diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs index e8fccc9062..24059e69e6 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs @@ -1172,7 +1172,7 @@ fn check_class_namespace_against_metaclass_members<'db>( .filter_map(|class| class.static_class_literal(db).map(|(literal, _)| literal)) { let body_scope = metaclass.body_scope(db); - let metaclass_index = semantic_index(db, body_scope.python_file(db)); + let metaclass_index = semantic_index(db, body_scope.program_file(db)); let body_scope_id = body_scope.file_scope_id(db); let metaclass_table = metaclass_index.place_table(body_scope_id); let metaclass_use_def = metaclass_index.use_def_map(body_scope_id); diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index ec393c9650..d5f9680793 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -2054,7 +2054,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { instance.class(db, env).static_class_literal(db) }) .and_then(|(class_literal, _)| { - file_to_module(db, class_literal.python_file(db)) + let file = class_literal.program_file(db); + file_to_module(db, file.resolver_file(db)) }) .and_then(|module| module.search_path(db)) .is_some_and(ty_module_resolver::SearchPath::is_first_party) diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index ff04859e00..cbea2b2190 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -3,24 +3,25 @@ use crate::db::tests::{TestDb, TestDbBuilder, setup_db}; use crate::place::symbol; use crate::place::{ConsideredDefinitions, Place, PlaceAndQualifiers}; use crate::types::{KnownClass, KnownInstanceType, check_types}; -use ruff_db::PythonFile; use ruff_db::diagnostic::{Diagnostic, DiagnosticId}; use ruff_db::files::{File, system_path_to_file}; use ruff_db::system::DbWithWritableSystem as _; use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run}; use ruff_python_ast::PythonVersion; +use ty_module_resolver::ResolverEnvironment; use ty_python_core::definition::Definition; +use ty_python_core::program::Program as ProjectProgram; use ty_python_core::scope::FileScopeId; -use ty_python_core::{global_scope, place_table, semantic_index, use_def_map}; +use ty_python_core::{ProgramFile, global_scope, place_table, semantic_index, use_def_map}; use super::*; -fn python_file(db: &TestDb, file: File) -> PythonFile<'_> { - PythonFile::new(db, file, db.python_version()) +fn program_file(db: &TestDb, file: File) -> ProgramFile<'_> { + ProgramFile::new(db, file, db.program_environment().program(db)) } fn global_symbol<'db>(db: &'db TestDb, file: File, name: &str) -> PlaceAndQualifiers<'db> { - crate::place::global_symbol(db, python_file(db, file), name) + crate::place::global_symbol(db, program_file(db, file), name) } #[track_caller] @@ -31,8 +32,8 @@ fn get_symbol<'db>( symbol_name: &str, ) -> Place<'db> { let file = system_path_to_file(db, file_name).expect("file to exist"); - let file = python_file(db, file); - let module = parsed_module(db, file).load(db); + let file = program_file(db, file); + let module = parsed_module(db, file.python_file(db)).load(db); let index = semantic_index(db, file); let mut file_scope_id = FileScopeId::global(); let mut scope = file_scope_id.to_scope_id(db, file); @@ -61,7 +62,7 @@ fn assert_diagnostic_messages(diagnostics: &[Diagnostic], expected: &[&str]) { #[track_caller] fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) { let file = system_path_to_file(db, filename).unwrap(); - let diagnostics = check_types(db, python_file(db, file)); + let diagnostics = check_types(db, program_file(db, file)); assert_diagnostic_messages(&diagnostics, expected); } @@ -69,7 +70,7 @@ fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) { #[track_caller] fn assert_revealed_type(db: &TestDb, filename: &str, expected: &str) { let file = system_path_to_file(db, filename).unwrap(); - let diagnostics = check_types(db, python_file(db, file)); + let diagnostics = check_types(db, program_file(db, file)); assert_eq!(diagnostics.len(), 1, "{diagnostics:#?}"); let diagnostic = &diagnostics[0]; @@ -110,8 +111,17 @@ fn same_file_at_different_python_versions() -> anyhow::Result<()> { db.write_dedented("src/py312_dependency.py", "value: int = 312")?; let file = system_path_to_file(&db, "src/main.py").expect("file to exist"); - let py311 = PythonFile::new(&db, file, PythonVersion::PY311); - let py312 = PythonFile::new(&db, file, PythonVersion::PY312); + let search_paths = ProjectProgram::get(&db).search_paths(&db); + let py311 = ProgramFile::new( + &db, + file, + ResolverEnvironment::new(&db, PythonVersion::PY311, search_paths), + ); + let py312 = ProgramFile::new( + &db, + file, + ResolverEnvironment::new(&db, PythonVersion::PY312, search_paths), + ); let check = |file, expected_type, expect_invalid_syntax, expect_unresolved_import| { let diagnostics = crate::check_file_unwrap(&db, file); @@ -169,7 +179,7 @@ fn expected_types_are_collected_only_for_open_files() -> anyhow::Result<()> { db.open_file(file); } - let module = parsed_module(&db, python_file(&db, file)).load(&db); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); let assignment = module.syntax().body[1] .as_ann_assign_stmt() .expect("annotated assignment"); @@ -179,7 +189,7 @@ fn expected_types_are_collected_only_for_open_files() -> anyhow::Result<()> { .expect("annotated assignment to have a value") .as_string_literal_expr() .expect("string literal value"); - let scope = global_scope(&db, python_file(&db, file)); + let scope = global_scope(&db, program_file(&db, file)); Ok(infer_complete_scope_types(&db, scope) .try_expected_type(ruff_python_ast::ExprRef::from(string_expr)) @@ -209,12 +219,12 @@ fn compact_definition_types_omit_owner() -> anyhow::Result<()> { )?; let file = system_path_to_file(&db, "/src/definitions.py").unwrap(); - let module = parsed_module(&db, python_file(&db, file)).load(&db); + let module = parsed_module(&db, program_file(&db, file).python_file(&db)).load(&db); let first_assignment = module.syntax().body[0].as_assign_stmt().unwrap(); let second_assignment = module.syntax().body[1].as_assign_stmt().unwrap(); - let first = semantic_index(&db, python_file(&db, file)) + let first = semantic_index(&db, program_file(&db, file)) .expect_single_definition(first_assignment.targets[0].as_name_expr().unwrap()); - let second = semantic_index(&db, python_file(&db, file)) + let second = semantic_index(&db, program_file(&db, file)) .expect_single_definition(second_assignment.targets[0].as_name_expr().unwrap()); let owner_type = Type::unknown(); @@ -666,7 +676,7 @@ class Form(Ui): // Incremental inference tests #[track_caller] fn first_public_binding<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> { - let scope = global_scope(db, python_file(db, file)); + let scope = global_scope(db, program_file(db, file)); use_def_map(db, scope) .end_of_scope_symbol_bindings(place_table(db, scope).symbol_id(name).unwrap()) .find_map(|b| b.binding.definition()) @@ -792,12 +802,12 @@ fn dependency_unrelated_symbol() -> anyhow::Result<()> { fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { fn x_rhs_expression(db: &TestDb) -> Expression<'_> { let file_main = system_path_to_file(db, "/src/main.py").unwrap(); - let ast = parsed_module(db, python_file(db, file_main)).load(db); + let ast = parsed_module(db, program_file(db, file_main).python_file(db)).load(db); // Get the second statement in `main.py` (x = …) and extract the expression // node on the right-hand side: let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value; - let index = semantic_index(db, python_file(db, file_main)); + let index = semantic_index(db, program_file(db, file_main)); index.expression(x_rhs_node.as_ref()) } @@ -890,12 +900,12 @@ fn dependency_implicit_instance_attribute() -> anyhow::Result<()> { fn dependency_own_instance_member() -> anyhow::Result<()> { fn x_rhs_expression(db: &TestDb) -> Expression<'_> { let file_main = system_path_to_file(db, "/src/main.py").unwrap(); - let ast = parsed_module(db, python_file(db, file_main)).load(db); + let ast = parsed_module(db, program_file(db, file_main).python_file(db)).load(db); // Get the second statement in `main.py` (x = …) and extract the expression // node on the right-hand side: let x_rhs_node = &ast.syntax().body[1].as_assign_stmt().unwrap().value; - let index = semantic_index(db, python_file(db, file_main)); + let index = semantic_index(db, program_file(db, file_main)); index.expression(x_rhs_node.as_ref()) } @@ -992,12 +1002,12 @@ fn dependency_own_instance_member() -> anyhow::Result<()> { fn dependency_implicit_class_member() -> anyhow::Result<()> { fn x_rhs_expression(db: &TestDb) -> Expression<'_> { let file_main = system_path_to_file(db, "/src/main.py").unwrap(); - let ast = parsed_module(db, python_file(db, file_main)).load(db); + let ast = parsed_module(db, program_file(db, file_main).python_file(db)).load(db); // Get the third statement in `main.py` (x = …) and extract the expression // node on the right-hand side: let x_rhs_node = &ast.syntax().body[2].as_assign_stmt().unwrap().value; - let index = semantic_index(db, python_file(db, file_main)); + let index = semantic_index(db, program_file(db, file_main)); index.expression(x_rhs_node.as_ref()) } @@ -1129,9 +1139,9 @@ fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> { ); let events = db.take_salsa_events(); - let module = parsed_module(&db, python_file(&db, bar)).load(&db); + let module = parsed_module(&db, program_file(&db, bar).python_file(&db)).load(&db); let call = &*module.syntax().body[1].as_assign_stmt().unwrap().value; - let foo_call = semantic_index(&db, python_file(&db, bar)).expression(call); + let foo_call = semantic_index(&db, program_file(&db, bar)).expression(call); assert_function_query_was_run( &db, @@ -1160,9 +1170,9 @@ fn call_type_doesnt_rerun_when_only_callee_changed() -> anyhow::Result<()> { ); let events = db.take_salsa_events(); - let module = parsed_module(&db, python_file(&db, bar)).load(&db); + let module = parsed_module(&db, program_file(&db, bar).python_file(&db)).load(&db); let call = &*module.syntax().body[1].as_assign_stmt().unwrap().value; - let foo_call = semantic_index(&db, python_file(&db, bar)).expression(call); + let foo_call = semantic_index(&db, program_file(&db, bar)).expression(call); assert_function_query_was_not_run( &db, diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index f1bc7473f3..1d59a26f6e 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -263,7 +263,7 @@ impl<'db> NominalInstanceType<'db> { env: &ProgramEnvironment<'db>, ) -> Option<&'db ModuleName> { let class = self.class(db, env).class_literal(db); - file_to_module(db, class.python_file(db)).map(|module| module.name(db)) + file_to_module(db, class.program_file(db).resolver_file(db)).map(|module| module.name(db)) } pub(super) fn class(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> ClassType<'db> { @@ -845,7 +845,7 @@ fn non_recursive_protocol_interface<'db>( } } - let env = ProgramEnvironment::from_file(protocol.class_literal(db).python_file(db)); + let env = ProgramEnvironment::from_file(protocol.class_literal(db).program_file(db)); interface.filter_members(db, |member| { let visitor = ProtocolReferenceFinder { env: &env, diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index 8bf6761f1a..c5f270fa3d 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -23,8 +23,8 @@ use crate::{ }, }; use ty_python_core::{ - attribute_scopes, definition::Definition, global_scope, place_table, scope::ScopeId, - semantic_index, use_def_map, + ProgramFile, attribute_scopes, definition::Definition, global_scope, place_table, + scope::ScopeId, semantic_index, use_def_map, }; /// Iterate over all declarations and bindings that exist at the end @@ -423,18 +423,19 @@ impl<'db> AllMembers<'db> { self.extend_with_type(db, env, KnownClass::ModuleType.to_instance(db, env)); - let Some(python_file) = module.python_file(db) else { + let Some(file) = module.file(db) else { return; }; + let program_file = ProgramFile::new(db, file, env.program(db)); - let module_scope = global_scope(db, python_file); + let module_scope = global_scope(db, program_file); let use_def_map = use_def_map(db, module_scope); let place_table = place_table(db, module_scope); for (symbol_id, _) in use_def_map.all_end_of_scope_symbol_declarations() { let symbol_name = place_table.symbol(symbol_id).name(); let Place::Defined(defined) = - imported_symbol(db, env, Some(python_file), symbol_name, None).place + imported_symbol(db, env, Some(program_file), symbol_name, None).place else { continue; }; @@ -443,7 +444,7 @@ impl<'db> AllMembers<'db> { && !exists_at_runtime(db, definition) // Source-module completions retain `@type_check_only` symbols and rank them // lower. - && (python_file.file(db).is_stub(db) || !defined.ty.is_type_check_only(db)) + && (file.is_stub(db) || !defined.ty.is_type_check_only(db)) // The decorator itself is typing-only, but users must still be able to // import it when defining typing-only classes and functions. && !matches!( @@ -550,8 +551,8 @@ impl<'db> AllMembers<'db> { class_literal: StaticClassLiteral<'db>, ) { let class_body_scope = class_literal.body_scope(db); - let python_file = class_body_scope.python_file(db); - let index = semantic_index(db, python_file); + let program_file = class_body_scope.program_file(db); + let index = semantic_index(db, program_file); for function_scope_id in attribute_scopes(db, class_body_scope) { for place_expr in index.place_table(function_scope_id).members() { let Some(name) = place_expr.as_instance_attribute() else { diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 11c104cc2d..bbecb07346 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -119,8 +119,9 @@ fn all_narrowing_constraints_for_pattern<'db>( db: &'db dyn Db, pattern: PatternPredicate<'db>, ) -> Option> { - let python_file = pattern.python_file(db); - let env = ProgramEnvironment::from_file(python_file); + let program_file = pattern.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); let module = parsed_module(db, python_file).load(db); NarrowingConstraintsBuilder::new(db, &env, &module, PredicateNode::Pattern(pattern), true) .finish() @@ -135,8 +136,9 @@ fn all_narrowing_constraints_for_expression<'db>( db: &'db dyn Db, expression: Expression<'db>, ) -> ExpressionNarrowingConstraints<'db> { - let python_file = expression.python_file(db); - let env = ProgramEnvironment::from_file(python_file); + let program_file = expression.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); let module = parsed_module(db, python_file).load(db); let predicate = PredicateNode::Expression(expression); ExpressionNarrowingConstraints { @@ -150,8 +152,9 @@ fn all_negative_narrowing_constraints_for_pattern<'db>( db: &'db dyn Db, pattern: PatternPredicate<'db>, ) -> Option> { - let python_file = pattern.python_file(db); - let env = ProgramEnvironment::from_file(python_file); + let program_file = pattern.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); let module = parsed_module(db, python_file).load(db); NarrowingConstraintsBuilder::new(db, &env, &module, PredicateNode::Pattern(pattern), false) .finish() @@ -163,8 +166,9 @@ fn all_narrowing_constraints_for_subject_element_pattern<'db>( pattern: PatternPredicate<'db>, target: ExpressionNodeKey, ) -> Option> { - let python_file = pattern.python_file(db); - let env = ProgramEnvironment::from_file(python_file); + let program_file = pattern.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); let module = parsed_module(db, python_file).load(db); NarrowingConstraintsBuilder::new( db, @@ -1164,7 +1168,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { let db = self.db; match expression_node { ast::Expr::Name(_) => { - let index = semantic_index(db, expression.python_file(db)); + let index = semantic_index(db, expression.program_file(db)); let constraints = self.evaluate_simple_expr(expression_node, is_positive); if let Some(alias_predicate) = index.narrowing_alias_predicate(expression_node) { let aliased_constraints = diff --git a/crates/ty_python_semantic/src/types/newtype.rs b/crates/ty_python_semantic/src/types/newtype.rs index 216f5b13bb..40e7703fc3 100644 --- a/crates/ty_python_semantic/src/types/newtype.rs +++ b/crates/ty_python_semantic/src/types/newtype.rs @@ -66,8 +66,9 @@ impl<'db> NewType<'db> { // in assignments, but invalid definitions still get here, and also `NewType` might show up // in places that aren't definitions at all. Fall back to `object` in all error cases. let definition = self.definition(db); - let python_file = definition.python_file(db); - let env = ProgramEnvironment::from_file(python_file); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); let object_fallback = NewTypeBase::ClassType(ClassType::object(db, &env)); let module = parsed_module(db, python_file).load(db); let DefinitionKind::Assignment(assignment) = definition.kind(db) else { diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index 458b3a87e1..7127364896 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -1126,7 +1126,7 @@ fn effective_superclass_variable_kind<'db>( superclass: ClassType<'db>, name: Name, ) -> Option { - let env = &ProgramEnvironment::from_file(superclass.class_literal(db).python_file(db)); + let env = &ProgramEnvironment::from_file(superclass.class_literal(db).program_file(db)); let inherited_variable_kind = || { superclass .iter_mro(db) diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index 0cea47808e..10a08692e8 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -1,5 +1,4 @@ use crate::Db; -use crate::ProgramEnvironment; use crate::place::{DefinedPlace, Place, builtins_symbol, global_symbol, known_module_symbol}; use crate::types::enums::is_single_member_enum; use crate::types::known_instance::KnownInstanceType; @@ -9,13 +8,13 @@ use crate::types::{ IntersectionType, KnownClass, MaterializationKind, Parameter, Parameters, Signature, SpecialFormType, SubclassOfType, Type, UnionType, }; +use crate::{Program, ProgramEnvironment}; use quickcheck::{Arbitrary, Gen}; -use ruff_db::PythonFile; use ruff_db::files::system_path_to_file; -use ruff_python_ast::PythonVersion; use ruff_python_ast::name::Name; use rustc_hash::FxHashSet; use ty_module_resolver::KnownModule; +use ty_python_core::ProgramFile; /// A test representation of a type that can be transformed unambiguously into a real Type, /// given a db. @@ -142,11 +141,11 @@ enum ParamKind { #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] fn create_bound_method<'db>( db: &'db dyn Db, - python_version: PythonVersion, + program: Program<'db>, function: Type<'db>, builtins_class: Type<'db>, ) -> Type<'db> { - let env = ProgramEnvironment::from_program(python_version); + let env = ProgramEnvironment::from_program(program); Type::BoundMethod(BoundMethodType::new( db, function.expect_function_literal(), @@ -265,7 +264,7 @@ impl Ty { let builtins_class = builtins_symbol(db, env, class).place.expect_type(); let function = builtins_class.member(db, env, method).place.expect_type(); - create_bound_method(db, env.python_version(db), function, builtins_class) + create_bound_method(db, env.program(db), function, builtins_class) } Ty::Callable { params, returns } => Type::single_callable( db, @@ -301,7 +300,7 @@ fn divergent<'db>( fn newtype_instance<'db>(db: &'db dyn Db, env: &ProgramEnvironment<'db>, name: &str) -> Type<'db> { let file = system_path_to_file(db, super::setup::PROPERTY_TEST_MODULE_PATH) .expect("Property-test module must exist"); - let file = PythonFile::new(db, file, env.python_version(db)); + let file = ProgramFile::new(db, file, env.program(db)); let Place::Defined(DefinedPlace { ty, .. }) = global_symbol(db, file, name).place else { panic!( "Expected a global symbol for `{name}` in the property test module, but it was not found" diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index ea44e6bf55..b27ac8c293 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -330,7 +330,7 @@ impl<'db> From> for Type<'db> { #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub(super) struct ProtocolInterface<'db> { #[returns(copy)] - pub(super) program: Program, + pub(super) program: Program<'db>, #[returns(ref)] inner: BTreeMap>, @@ -3493,7 +3493,7 @@ fn cached_protocol_interface<'db>( db: &'db dyn Db, class: ClassType<'db>, ) -> ProtocolInterface<'db> { - let env = ProgramEnvironment::from_file(class.class_literal(db).python_file(db)); + let env = ProgramEnvironment::from_file(class.class_literal(db).program_file(db)); let mut members = BTreeMap::default(); ProtocolClass(class).for_each_member_candidate(db, &env, |name, candidate, specialization| { @@ -3555,7 +3555,7 @@ fn protocol_interface_cycle_initial<'db>( ) -> ProtocolInterface<'db> { ProtocolInterface::empty( db, - &ProgramEnvironment::from_file(class.class_literal(db).python_file(db)), + &ProgramEnvironment::from_file(class.class_literal(db).program_file(db)), ) } @@ -3567,7 +3567,7 @@ fn proto_interface_cycle_recover<'db>( value: ProtocolInterface<'db>, class: ClassType<'db>, ) -> ProtocolInterface<'db> { - let env = ProgramEnvironment::from_file(class.class_literal(db).python_file(db)); + let env = ProgramEnvironment::from_file(class.class_literal(db).program_file(db)); value.cycle_normalized(db, &env, *previous, cycle) } @@ -3580,7 +3580,7 @@ fn proto_interface_cycle_recover<'db>( #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] fn protocol_bind_self<'db>( db: &'db dyn Db, - program: Program, + program: Program<'db>, callable: CallableType<'db>, self_type: Option>, ) -> CallableType<'db> { @@ -3596,7 +3596,7 @@ fn protocol_bind_self<'db>( )] fn protocol_apply_self_with_receiver<'db>( db: &'db dyn Db, - program: Program, + program: Program<'db>, callable: CallableType<'db>, receiver_type: Type<'db>, self_type: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs index 1bc3214608..c616a98d0c 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs @@ -2005,9 +2005,9 @@ mod tests { use crate::types::type_alias::TypeAliasType; use crate::types::{KnownClass, KnownInstanceType, Truthiness}; - use ruff_db::PythonFile; use ruff_db::system::DbWithWritableSystem as _; use ty_module_resolver::KnownModule; + use ty_python_core::ProgramFile; #[test] fn build_union_no_elements() { @@ -2203,7 +2203,7 @@ mod tests { let env = db.program_environment(); let module = ruff_db::files::system_path_to_file(&db, "/src/a.py").unwrap(); - let module = PythonFile::new(&db, module, db.python_version()); + let module = ProgramFile::new(&db, module, db.program_environment().program(&db)); let alias_ty = global_symbol(&db, module, "Alias").place.expect_type(); let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(alias))) = alias_ty diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index fb9356ebc5..e31d68d96c 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -74,7 +74,7 @@ fn function_signature_expression_type<'db>( definition: Definition<'db>, expression: &ast::Expr, ) -> Type<'db> { - let file = definition.python_file(db); + let file = definition.program_file(db); let index = semantic_index(db, file); let file_scope = index.expression_scope_id(expression); let scope = file_scope.to_scope_id(db, file); @@ -92,7 +92,7 @@ fn function_signature_type_expression_flags<'db>( definition: Definition<'db>, expression: &ast::Expr, ) -> TypeExpressionFlags { - let file = definition.python_file(db); + let file = definition.program_file(db); let index = semantic_index(db, file); let file_scope = index.expression_scope_id(expression); let scope = file_scope.to_scope_id(db, file); @@ -5337,7 +5337,7 @@ impl<'db> Parameter<'db> { parameter: &ast::Parameter, kind: ParameterKind<'db>, ) -> Self { - let index = semantic_index(db, function_definition.python_file(db)); + let index = semantic_index(db, function_definition.program_file(db)); let definition = Some(index.expect_single_definition(parameter)); let (annotated_type, inferred_annotation, annotation_flags, has_starred_annotation) = @@ -5668,13 +5668,13 @@ mod tests { use crate::db::tests::{TestDb, setup_db}; use crate::place::global_symbol; use crate::types::{FunctionType, KnownClass, LiteralValueType}; - use ruff_db::PythonFile; use ruff_db::system::DbWithWritableSystem as _; + use ty_python_core::ProgramFile; #[track_caller] fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> { let module = ruff_db::files::system_path_to_file(db, file).unwrap(); - let module = PythonFile::new(db, module, db.python_version()); + let module = ProgramFile::new(db, module, db.program_environment().program(db)); global_symbol(db, module, "f") .place .expect_type() diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 2c10ab4be1..a49e371a60 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -11,11 +11,10 @@ use crate::types::{ generics::typing_self, infer::{function_known_decorator_flags, nearest_enclosing_class}, }; -use ruff_db::PythonFile; use strum_macros::EnumString; -use ty_module_resolver::{KnownModule, file_to_module, resolve_module_confident}; +use ty_module_resolver::{ImportingFile, KnownModule, file_to_module, resolve_module_confident}; use ty_python_core::{ - FileScopeId, + FileScopeId, ProgramFile, definition::{Definition, DefinitionKind}, place::ScopedPlaceId, place_table, @@ -294,16 +293,18 @@ impl SpecialFormType { pub(super) fn try_from_file_and_name( db: &dyn Db, - file: PythonFile<'_>, + file: ImportingFile<'_>, symbol_name: &str, ) -> Option { - Self::candidates_from_name(symbol_name) + let candidates = Self::candidates_from_name(symbol_name); + if candidates.is_empty() { + return None; + } + + let known_module = file_to_module(db, file.resolver_file(db))?.known(db)?; + candidates .iter() - .find(|candidate| { - file_to_module(db, file) - .and_then(|module| module.known(db)) - .is_some_and(|known_module| candidate.check_module(known_module)) - }) + .find(|candidate| candidate.check_module(known_module)) .copied() } @@ -829,8 +830,9 @@ impl SpecialFormType { self.definition_modules() .iter() .find_map(|module| { - let file = resolve_module_confident(db, env.python_version(db), &module.name())? - .python_file(db)?; + let module = + resolve_module_confident(db, env.resolver_environment(db), &module.name())?; + let file = ProgramFile::new(db, module.file(db)?, env.program(db)); let scope = FileScopeId::global().to_scope_id(db, file); let symbol_id = place_table(db, scope).symbol_id(self.name())?; @@ -886,8 +888,8 @@ impl SpecialFormType { return Err(InvalidTypeExpression::TypingSelfInTypeAlias); } - let python_file = scope_id.python_file(db); - let index = semantic_index(db, python_file); + let program_file = scope_id.program_file(db); + let index = semantic_index(db, program_file); let Some(class) = nearest_enclosing_class(db, index, scope_id) else { return Err(InvalidTypeExpression::InvalidType( Type::SpecialForm(self), diff --git a/crates/ty_python_semantic/src/types/tests.rs b/crates/ty_python_semantic/src/types/tests.rs index 7ee494da31..8fafadcf43 100644 --- a/crates/ty_python_semantic/src/types/tests.rs +++ b/crates/ty_python_semantic/src/types/tests.rs @@ -4,11 +4,11 @@ use crate::ProgramEnvironment; use crate::db::tests::{TestDbBuilder, setup_db}; use crate::place::{typing_extensions_symbol, typing_symbol}; use crate::types::type_alias::PEP695TypeAliasType; -use ruff_db::PythonFile; use ruff_db::system::DbWithWritableSystem as _; use ruff_python_ast as ast; use ruff_python_ast::PythonVersion; use test_case::test_case; +use ty_python_core::ProgramFile; /// Explicitly test for Python version <3.13 and >=3.13, to ensure that /// the fallback to `typing_extensions` is working correctly. @@ -75,7 +75,7 @@ fn oscillating_generic_alias_cycle_recover<'db>( current: Type<'db>, ) -> Type<'db> { let env = ProgramEnvironment::from_program( - ty_python_core::program::Program::get(db).python_version(db), + ty_python_core::program::Program::get(db).resolver_environment(db), ); current.cycle_normalized(db, &env, *previous, cycle) } @@ -87,7 +87,7 @@ fn oscillating_generic_alias_cycle_recover<'db>( )] fn oscillating_generic_alias(db: &dyn Db) -> Type<'_> { let env = ProgramEnvironment::from_program( - ty_python_core::program::Program::get(db).python_version(db), + ty_python_core::program::Program::get(db).resolver_environment(db), ); let previous = oscillating_generic_alias(db); let argument = if let Type::GenericAlias(alias) = previous @@ -454,7 +454,7 @@ fn type_alias_variance() { fn get_type_alias<'db>(db: &'db TestDb, name: &str) -> PEP695TypeAliasType<'db> { let module = ruff_db::files::system_path_to_file(db, "/src/a.py").unwrap(); - let module = PythonFile::new(db, module, db.python_version()); + let module = ProgramFile::new(db, module, db.program_environment().program(db)); let ty = global_symbol(db, module, name).place.expect_type(); let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( type_alias, @@ -709,7 +709,7 @@ fn eager_expansion() { fn get_type_alias<'db>(db: &'db TestDb, name: &str) -> Type<'db> { let module = ruff_db::files::system_path_to_file(db, "/src/a.py").unwrap(); - let module = PythonFile::new(db, module, db.python_version()); + let module = ProgramFile::new(db, module, db.program_environment().program(db)); let ty = global_symbol(db, module, name).place.expect_type(); let Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( type_alias, diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index e3ec0ba9d8..c729d6cc19 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -131,7 +131,7 @@ impl TupleLength { #[salsa::interned(debug, constructor=new_internal, heap_size=ruff_memory_usage::heap_size)] pub struct TupleType<'db> { #[returns(copy)] - pub(crate) program: Program, + pub(crate) program: Program<'db>, #[returns(ref)] pub(crate) tuple: TupleSpec<'db>, diff --git a/crates/ty_python_semantic/src/types/type_alias.rs b/crates/ty_python_semantic/src/types/type_alias.rs index 55650260a4..38398a465b 100644 --- a/crates/ty_python_semantic/src/types/type_alias.rs +++ b/crates/ty_python_semantic/src/types/type_alias.rs @@ -51,7 +51,7 @@ impl<'db> PEP695TypeAliasType<'db> { fn definition(self, db: &'db dyn Db) -> Definition<'db> { let scope = self.rhs_scope(db); let type_alias_stmt_node = scope.node(db).expect_type_alias(); - semantic_index(db, scope.python_file(db)).expect_single_definition(type_alias_stmt_node) + semantic_index(db, scope.program_file(db)).expect_single_definition(type_alias_stmt_node) } /// The RHS type of a PEP-695 style type alias with specialization applied. @@ -77,7 +77,8 @@ impl<'db> PEP695TypeAliasType<'db> { )] pub(super) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { let scope = self.rhs_scope(db); - let python_file = scope.python_file(db); + let program_file = scope.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let type_alias_stmt_node = scope.node(db).expect_type_alias(); let definition = self.definition(db); @@ -113,7 +114,8 @@ impl<'db> PEP695TypeAliasType<'db> { #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { let scope = self.rhs_scope(db); - let python_file = scope.python_file(db); + let program_file = scope.program_file(db); + let python_file = program_file.python_file(db); let parsed = parsed_module(db, python_file).load(db); let type_alias_stmt_node = scope.node(db).expect_type_alias(); @@ -122,7 +124,7 @@ impl<'db> PEP695TypeAliasType<'db> { .type_params .as_ref() .map(|type_params| { - let index = semantic_index(db, python_file); + let index = semantic_index(db, program_file); let definition = index.expect_single_definition(type_alias_stmt_node); GenericContext::from_type_params(db, index, definition, type_params) }) @@ -219,9 +221,9 @@ impl<'db> ManualPEP695TypeAliasType<'db> { #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| None, heap_size=ruff_memory_usage::heap_size)] pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db); - let file = definition.python_file(db); + let file = definition.program_file(db); let env = ProgramEnvironment::from_file(file); - let module = parsed_module(db, file).load(db); + let module = parsed_module(db, file.python_file(db)).load(db); let DefinitionKind::Assignment(assignment) = definition.kind(db) else { return None; }; @@ -475,7 +477,7 @@ impl<'db> QualifiedTypeAliasName<'db> { /// would return `["a", "b", "C"]`. pub(crate) fn components_excluding_self(&self) -> Vec { let definition = self.type_alias.definition(self.db); - let file = definition.python_file(self.db); + let file = definition.program_file(self.db); let file_scope_id = definition.file_scope(self.db); // Type aliases are defined directly in their enclosing scope (no body scope like classes), diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index 078fdfb330..c29900f475 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -259,8 +259,9 @@ impl<'db> TypedDictType<'db> { } }; - let python_file = static_class.python_file(db); - let env = ProgramEnvironment::from_file(python_file); + let program_file = static_class.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); let module = parsed_module(db, python_file).load(db); let class_definition = static_class.definition(db); let class_stmt = class_definition @@ -1297,8 +1298,9 @@ pub(super) fn deferred_functional_typed_dict_schema<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> TypedDictSchema<'db> { - let python_file = definition.python_file(db); - let env = ProgramEnvironment::from_file(python_file); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); let module = parsed_module(db, python_file).load(db); let node = definition .kind(db) @@ -1361,8 +1363,9 @@ pub(super) fn deferred_functional_typed_dict_openness<'db>( db: &'db dyn Db, definition: Definition<'db>, ) -> TypedDictOpenness<'db> { - let python_file = definition.python_file(db); - let env = ProgramEnvironment::from_file(python_file); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); let module = parsed_module(db, python_file).load(db); let node = definition .kind(db) diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index 6aea0560d0..e2e7c115ba 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -540,7 +540,8 @@ impl<'db> TypeVarInstance<'db> { )] fn lazy_bound_unchecked(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db)?; - let python_file = definition.python_file(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let ty = match definition.kind(db) { // PEP 695 typevar @@ -580,8 +581,9 @@ impl<'db> TypeVarInstance<'db> { )] fn lazy_constraints_unchecked(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db)?; - let python_file = definition.python_file(db); - let env = ProgramEnvironment::from_file(python_file); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let env = ProgramEnvironment::from_file(program_file); let module = parsed_module(db, python_file).load(db); let constraints = match definition.kind(db) { // PEP 695 typevar @@ -684,7 +686,8 @@ impl<'db> TypeVarInstance<'db> { } let definition = self.definition(db)?; - let python_file = definition.python_file(db); + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); let module = parsed_module(db, python_file).load(db); let ty = match definition.kind(db) { // PEP 695 typevar @@ -758,7 +761,7 @@ impl<'db> TypeVarInstance<'db> { return None; } let typevar_definition = self.definition(db)?; - let index = semantic_index(db, typevar_definition.python_file(db)); + let index = semantic_index(db, typevar_definition.program_file(db)); let (_, child) = index .child_scopes(typevar_definition.file_scope(db)) .next()?; @@ -1532,11 +1535,11 @@ fn lazy_bound_cycle_recover<'db>( ) -> Option> { // Normalize the bounds/constraints to ensure cycle convergence. let current = current?; - let python_file = typevar + let program_file = typevar .definition(db) .expect("a lazy TypeVar bound must have a source definition") - .python_file(db); - let env = ProgramEnvironment::from_file(python_file); + .program_file(db); + let env = ProgramEnvironment::from_file(program_file); Some(match previous { Some(prev) => current.cycle_normalized(db, &env, *prev, cycle), None => current.recursive_type_normalized(db, &env, cycle), @@ -1554,11 +1557,11 @@ fn lazy_constraints_cycle_recover<'db>( ) -> Option> { // Normalize the bounds/constraints to ensure cycle convergence. let current = current?; - let python_file = typevar + let program_file = typevar .definition(db) .expect("lazy TypeVar constraints must have a source definition") - .python_file(db); - let env = ProgramEnvironment::from_file(python_file); + .program_file(db); + let env = ProgramEnvironment::from_file(program_file); Some(match previous { Some(prev) => current.cycle_normalized(db, &env, *prev, cycle), None => current.recursive_type_normalized(db, &env, cycle), @@ -1575,11 +1578,11 @@ fn lazy_default_cycle_recover<'db>( ) -> Option> { // Normalize the default to ensure cycle convergence. let current = current?; - let python_file = typevar + let program_file = typevar .definition(db) .expect("a lazy TypeVar default must have a source definition") - .python_file(db); - let env = ProgramEnvironment::from_file(python_file); + .program_file(db); + let env = ProgramEnvironment::from_file(program_file); Some(match previous_default { Some(prev) => current.cycle_normalized(db, &env, *prev, cycle), None => current.recursive_type_normalized(db, &env, cycle), @@ -1594,7 +1597,7 @@ pub enum BindingContext<'db> { /// The typevar is synthesized internally, and is not associated with a particular definition /// in the source, but is still bound and eligible for specialization inference. Its program /// identifies the environment that cannot otherwise be recovered from a source definition. - Synthetic(Program), + Synthetic(Program<'db>), } impl<'db> From> for BindingContext<'db> { @@ -1611,7 +1614,7 @@ impl<'db> BindingContext<'db> { } } - pub(crate) fn program(self, db: &'db dyn Db) -> Program { + pub(crate) fn program(self, db: &'db dyn Db) -> Program<'db> { match self { Self::Definition(definition) => definition.program(db), Self::Synthetic(program) => program, @@ -1828,12 +1831,12 @@ fn bound_typevar_default_type_cycle_recover<'db>( bound_typevar: BoundTypeVarInstance<'db>, ) -> Option> { let default = default?; - let python_file = bound_typevar + let program_file = bound_typevar .typevar(db) .definition(db) .expect("a bound TypeVar with a default must have a source definition") - .python_file(db); - let env = ProgramEnvironment::from_file(python_file); + .program_file(db); + let env = ProgramEnvironment::from_file(program_file); Some(match previous_default { Some(previous) => default.cycle_normalized(db, &env, *previous, cycle), None => default.recursive_type_normalized(db, &env, cycle), diff --git a/crates/ty_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs index 9341dac670..e0406747b1 100644 --- a/crates/ty_python_semantic/src/types/unpacker.rs +++ b/crates/ty_python_semantic/src/types/unpacker.rs @@ -3,7 +3,6 @@ use std::borrow::Cow; use ruff_db::parsed::ParsedModuleRef; -use ruff_db::PythonFile; use rustc_hash::FxHashMap; use ruff_python_ast::visitor::{self, Visitor}; @@ -14,6 +13,7 @@ use crate::types::infer::{ExpressionInference, FrozenMap}; use crate::types::tuple::{ResizeTupleError, TupleLength, TupleSpec, TupleUnpacker}; use crate::types::{Type, TypeCheckDiagnostics, TypeContext, infer_expression_types}; use ty_python_core::ExpressionNodeKey; +use ty_python_core::ProgramFile; use ty_python_core::scope::ScopeId; use ty_python_core::unpack::{UnpackKind, UnpackValue}; @@ -43,7 +43,7 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { db: &'db dyn Db, env: &'ast ProgramEnvironment<'db>, target_scope: ScopeId<'db>, - python_file: PythonFile<'db>, + program_file: ProgramFile<'db>, module: &'ast ParsedModuleRef, ) -> Self { Self { @@ -51,8 +51,8 @@ impl<'db, 'ast> Unpacker<'db, 'ast> { db, env, target_scope, - python_file.file(db), - python_file, + program_file.file(db), + program_file, module, ), targets: FxHashMap::default(), diff --git a/crates/ty_python_semantic/tests/corpus.rs b/crates/ty_python_semantic/tests/corpus.rs index 3fa7e4861e..3c3575c61a 100644 --- a/crates/ty_python_semantic/tests/corpus.rs +++ b/crates/ty_python_semantic/tests/corpus.rs @@ -1,10 +1,10 @@ use std::sync::Arc; use anyhow::{Context, anyhow}; +use ruff_db::Db; use ruff_db::files::{File, Files, system_path_to_file}; use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem}; use ruff_db::vendored::VendoredFileSystem; -use ruff_db::{Db, PythonFile}; use ruff_python_ast::PythonVersion; use ty_module_resolver::SearchPathSettings; @@ -12,12 +12,12 @@ use ty_python_core::platform::PythonPlatform; use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::pull_types::pull_types; -use ty_python_semantic::{AnalysisSettings, check_file_unwrap, default_lint_registry}; +use ty_python_semantic::{AnalysisSettings, Db as _, check_file_unwrap, default_lint_registry}; use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; use ruff_db::diagnostic::Diagnostic; use test_case::test_case; -use ty_python_core::Db as _; +use ty_python_core::{Db as _, ProgramFile}; fn get_cargo_workspace_root() -> anyhow::Result<&'static SystemPath> { SystemPath::new(env!("CARGO_MANIFEST_DIR")) @@ -112,7 +112,7 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> { let file = system_path_to_file(&db, path).unwrap(); if let Err(err) = std::panic::catch_unwind(|| { - pull_types(&db, PythonFile::new(&db, file, db.python_version())); + pull_types(&db, db.program_file(file)); }) { println!("Check failed for {relative_path:?}."); std::panic::resume_unwind(err); @@ -179,10 +179,6 @@ impl CorpusDb { db } - - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } } impl DbWithTestSystem for CorpusDb { @@ -211,11 +207,7 @@ impl ruff_db::Db for CorpusDb { } #[salsa::db] -impl ty_module_resolver::Db for CorpusDb { - fn search_paths(&self) -> &ty_module_resolver::SearchPaths { - Program::get(self).search_paths(self) - } -} +impl ty_module_resolver::Db for CorpusDb {} #[salsa::db] impl ty_python_core::Db for CorpusDb { @@ -228,12 +220,16 @@ impl ty_python_core::Db for CorpusDb { impl ty_python_semantic::Db for CorpusDb { fn check_file(&self, file: File) -> Vec { if self.should_check_file(file) { - check_file_unwrap(self, PythonFile::new(self, file, self.python_version())) + check_file_unwrap(self, self.program_file(file)) } else { Vec::new() } } + fn program_file(&self, file: File) -> ProgramFile<'_> { + Program::get(self).program_file(self, file) + } + fn rule_selection(&self, _file: File) -> &RuleSelection { &self.rule_selection } diff --git a/crates/ty_server/src/server/api/diagnostics.rs b/crates/ty_server/src/server/api/diagnostics.rs index 4f3df55560..3d91096d13 100644 --- a/crates/ty_server/src/server/api/diagnostics.rs +++ b/crates/ty_server/src/server/api/diagnostics.rs @@ -12,7 +12,6 @@ use ruff_text_size::Ranged; use rustc_hash::{FxHashMap, FxHashSet}; use ty_ide::{Hint, hints}; -use ruff_db::PythonFile; use ruff_db::diagnostic::{ Annotation, DisplayDiagnosticConfig, HyperlinkMode, Severity, SubDiagnostic, }; @@ -20,7 +19,7 @@ use ruff_db::files::{File, FileRange}; use ruff_db::source::source_text; use ruff_db::system::SystemPathBuf; use serde::{Deserialize, Serialize}; -use ty_project::{Db as _, ProjectDatabase}; +use ty_project::{Db as _, ProjectDatabase, SemanticDb as _}; use crate::capabilities::ResolvedClientCapabilities; use crate::document::{FileRangeExt, ToRangeExt}; @@ -402,7 +401,7 @@ pub(super) fn compute_diagnostics( }; let diagnostics = db.check_file(file); - let unnecessary_hints = hints(db, PythonFile::new(db, file, db.python_version())); + let unnecessary_hints = hints(db, db.program_file(file)); Some(Diagnostics { items: diagnostics, diff --git a/crates/ty_server/src/server/api/requests/call_hierarchy_incoming_calls.rs b/crates/ty_server/src/server/api/requests/call_hierarchy_incoming_calls.rs index 875883fa6f..efdef9bff3 100644 --- a/crates/ty_server/src/server/api/requests/call_hierarchy_incoming_calls.rs +++ b/crates/ty_server/src/server/api/requests/call_hierarchy_incoming_calls.rs @@ -1,7 +1,6 @@ use lsp_types::CallHierarchyIncomingCallsRequest; use lsp_types::{CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams}; -use ruff_db::PythonFile; -use ty_project::Db as _; +use ty_project::SemanticDb as _; use crate::document::{ToRangeExt as _, resolve_file_uri_range}; use crate::server::api::requests::prepare_call_hierarchy::convert_to_lsp_item; @@ -42,9 +41,7 @@ impl BackgroundRequestHandler for CallHierarchyIncomingCallsRequestHandler { continue; }; - for call in - ty_ide::incoming_calls(db, PythonFile::new(db, file, db.python_version()), offset) - { + for call in ty_ide::incoming_calls(db, db.program_file(file), offset) { // `from_ranges` are byte offsets into `call.from.file` (the caller), // NOT into `file` (the prepared/queried symbol). Capture the caller // file before moving `call.from` into `convert_to_lsp_item`. diff --git a/crates/ty_server/src/server/api/requests/call_hierarchy_outgoing_calls.rs b/crates/ty_server/src/server/api/requests/call_hierarchy_outgoing_calls.rs index d851d5afca..8562786ad0 100644 --- a/crates/ty_server/src/server/api/requests/call_hierarchy_outgoing_calls.rs +++ b/crates/ty_server/src/server/api/requests/call_hierarchy_outgoing_calls.rs @@ -1,7 +1,6 @@ use lsp_types::CallHierarchyOutgoingCallsRequest; use lsp_types::{CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams}; -use ruff_db::PythonFile; -use ty_project::Db as _; +use ty_project::SemanticDb as _; use crate::document::{ToRangeExt as _, resolve_file_uri_range}; use crate::server::api::requests::prepare_call_hierarchy::convert_to_lsp_item; @@ -42,9 +41,7 @@ impl BackgroundRequestHandler for CallHierarchyOutgoingCallsRequestHandler { continue; }; - for call in - ty_ide::outgoing_calls(db, PythonFile::new(db, file, db.python_version()), offset) - { + for call in ty_ide::outgoing_calls(db, db.program_file(file), offset) { let Some(to) = convert_to_lsp_item(db, call.to, encoding) else { continue; }; diff --git a/crates/ty_server/src/server/api/requests/code_action.rs b/crates/ty_server/src/server/api/requests/code_action.rs index f70508bacf..d9526d0ba8 100644 --- a/crates/ty_server/src/server/api/requests/code_action.rs +++ b/crates/ty_server/src/server/api/requests/code_action.rs @@ -2,13 +2,11 @@ use std::borrow::Cow; use std::collections::HashMap; use lsp_types::{self as types, Code, CodeActionRequest, CodeActionResponse, TextEdit, Uri}; -use ruff_db::PythonFile; use ruff_db::files::File; use ruff_diagnostics::Edit; use ruff_text_size::Ranged; use ty_ide::code_actions; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use types::CodeActionKind; use crate::db::Db; @@ -43,7 +41,7 @@ impl BackgroundDocumentRequestHandler for CodeActionRequestHandler { let Some(file) = snapshot.to_notebook_or_file(db) else { return Ok(None); }; - let python_file = PythonFile::new(db, file, db.python_version()); + let program_file = db.program_file(file); let mut actions = Vec::new(); for mut diagnostic in diagnostics.into_iter().filter(|diagnostic| { @@ -102,7 +100,7 @@ impl BackgroundDocumentRequestHandler for CodeActionRequestHandler { if let Some(diagnostic_id) = diagnostic_id && let Some(range) = diagnostic.range.to_text_range(db, file, uri, encoding) { - for action in code_actions(db, python_file, range, &diagnostic_id) { + for action in code_actions(db, program_file, range, &diagnostic_id) { actions.push(CodeActionResponse::CodeAction(lsp_types::CodeAction { title: action.title, kind: Some(CodeActionKind::QuickFix), diff --git a/crates/ty_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs index 33a318aca2..c6a63bba0a 100644 --- a/crates/ty_server/src/server/api/requests/completion.rs +++ b/crates/ty_server/src/server/api/requests/completion.rs @@ -6,15 +6,13 @@ use lsp_types::{ CompletionParams, CompletionRequest, CompletionResponse, Documentation, InsertTextFormat, TextEdit, Uri, }; -use ruff_db::PythonFile; use ruff_source_file::OneIndexed; use ruff_text_size::Ranged; use ty_ide::{ CompletionCapabilities, CompletionCommand, CompletionInsertTextFormat, CompletionKind, completion, }; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use ty_python_semantic::ProgramEnvironment; use crate::capabilities::ResolvedClientCapabilities; @@ -64,14 +62,14 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { return Ok(None); }; let client_capabilities = snapshot.resolved_client_capabilities(); - let python_file = PythonFile::new(db, file, db.python_version()); - let env = ProgramEnvironment::from_file(python_file); + let program_file = db.program_file(file); + let env = ProgramEnvironment::from_file(program_file); let completions = completion( db, snapshot.workspace_settings().completions(), CompletionCapabilities::default() .snippets(client_capabilities.supports_completion_item_snippets()), - python_file, + program_file, offset, ); if completions.is_empty() { diff --git a/crates/ty_server/src/server/api/requests/doc_highlights.rs b/crates/ty_server/src/server/api/requests/doc_highlights.rs index b8ace78182..f9f5e3a202 100644 --- a/crates/ty_server/src/server/api/requests/doc_highlights.rs +++ b/crates/ty_server/src/server/api/requests/doc_highlights.rs @@ -2,10 +2,8 @@ use std::borrow::Cow; use lsp_types::DocumentHighlightRequest; use lsp_types::{DocumentHighlight, DocumentHighlightKind, DocumentHighlightParams, Uri}; -use ruff_db::PythonFile; use ty_ide::{ReferenceKind, document_highlights}; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToRangeExt}; use crate::server::api::traits::{ @@ -51,9 +49,7 @@ impl BackgroundDocumentRequestHandler for DocumentHighlightRequestHandler { return Ok(None); }; - let Some(highlights_result) = - document_highlights(db, PythonFile::new(db, file, db.python_version()), offset) - else { + let Some(highlights_result) = document_highlights(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/document_symbols.rs b/crates/ty_server/src/server/api/requests/document_symbols.rs index ab7272b05f..7d6124ef65 100644 --- a/crates/ty_server/src/server/api/requests/document_symbols.rs +++ b/crates/ty_server/src/server/api/requests/document_symbols.rs @@ -2,11 +2,9 @@ use std::borrow::Cow; use lsp_types::DocumentSymbolRequest; use lsp_types::{DocumentSymbol, DocumentSymbolParams, Uri}; -use ruff_db::PythonFile; use ruff_db::files::File; use ty_ide::{HierarchicalSymbols, SymbolId, SymbolInfo, document_symbols}; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::Db; use crate::document::{PositionEncoding, ToRangeExt}; @@ -50,7 +48,7 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler { .resolved_client_capabilities() .supports_hierarchical_document_symbols(); - let symbols = document_symbols(db, PythonFile::new(db, file, db.python_version())); + let symbols = document_symbols(db, db.program_file(file)); if symbols.is_empty() { return Ok(None); } diff --git a/crates/ty_server/src/server/api/requests/execute_command.rs b/crates/ty_server/src/server/api/requests/execute_command.rs index c7ee7b2040..18f4f96f1f 100644 --- a/crates/ty_server/src/server/api/requests/execute_command.rs +++ b/crates/ty_server/src/server/api/requests/execute_command.rs @@ -84,8 +84,8 @@ fn debug_information(session: &Session) -> crate::Result { writer, " search-paths: {:#}", program - .search_paths(db) - .display(db, ModuleResolveMode::Typing) + .resolver_environment(db) + .display_search_paths(db, ModuleResolveMode::Typing) )?; writeln!(buffer, "Settings: {:#?}", db.project().settings(db))?; diff --git a/crates/ty_server/src/server/api/requests/folding_range.rs b/crates/ty_server/src/server/api/requests/folding_range.rs index f8865fad97..a7082487eb 100644 --- a/crates/ty_server/src/server/api/requests/folding_range.rs +++ b/crates/ty_server/src/server/api/requests/folding_range.rs @@ -2,12 +2,10 @@ use std::borrow::Cow; use lsp_types::FoldingRangeRequest; use lsp_types::{FoldingRange, FoldingRangeKind, FoldingRangeParams, Uri}; -use ruff_db::PythonFile; use ruff_db::source::source_text; use ruff_text_size::TextRange; use ty_ide::folding_ranges; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::db::Db; use crate::document::ToRangeExt; @@ -58,33 +56,29 @@ impl BackgroundDocumentRequestHandler for FoldingRangeRequestHandler { cell_range = cell_index.and_then(|index| notebook.cell_range(index)); } - let results: Vec<_> = folding_ranges( - db, - PythonFile::new(db, file, db.python_version()), - cell_range, - ) - .into_iter() - .filter_map(|folding_range| { - let lsp_range = folding_range - .range - .to_lsp_range(db, file, snapshot.encoding())?; + let results: Vec<_> = folding_ranges(db, db.program_file(file).python_file(db), cell_range) + .into_iter() + .filter_map(|folding_range| { + let lsp_range = folding_range + .range + .to_lsp_range(db, file, snapshot.encoding())?; - let kind = folding_range.kind.map(|k| match k { - ty_ide::FoldingRangeKind::Comment => FoldingRangeKind::Comment, - ty_ide::FoldingRangeKind::Imports => FoldingRangeKind::Imports, - ty_ide::FoldingRangeKind::Region => FoldingRangeKind::Region, - }); + let kind = folding_range.kind.map(|k| match k { + ty_ide::FoldingRangeKind::Comment => FoldingRangeKind::Comment, + ty_ide::FoldingRangeKind::Imports => FoldingRangeKind::Imports, + ty_ide::FoldingRangeKind::Region => FoldingRangeKind::Region, + }); - Some(FoldingRange { - start_line: lsp_range.local_range().start.line, - start_character: Some(lsp_range.local_range().start.character), - end_line: lsp_range.local_range().end.line, - end_character: Some(lsp_range.local_range().end.character), - kind, - collapsed_text: None, + Some(FoldingRange { + start_line: lsp_range.local_range().start.line, + start_character: Some(lsp_range.local_range().start.character), + end_line: lsp_range.local_range().end.line, + end_character: Some(lsp_range.local_range().end.character), + kind, + collapsed_text: None, + }) }) - }) - .collect(); + .collect(); if results.is_empty() { Ok(None) diff --git a/crates/ty_server/src/server/api/requests/goto_declaration.rs b/crates/ty_server/src/server/api/requests/goto_declaration.rs index 68ad4aee6e..95102bc07a 100644 --- a/crates/ty_server/src/server/api/requests/goto_declaration.rs +++ b/crates/ty_server/src/server/api/requests/goto_declaration.rs @@ -1,10 +1,8 @@ use std::borrow::Cow; use lsp_types::{DeclarationParams, DeclarationRequest, DeclarationResponse, Uri}; -use ruff_db::PythonFile; use ty_ide::goto_declaration; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToLink}; use crate::server::api::traits::{ @@ -50,9 +48,7 @@ impl BackgroundDocumentRequestHandler for GotoDeclarationRequestHandler { return Ok(None); }; - let Some(ranged) = - goto_declaration(db, PythonFile::new(db, file, db.python_version()), offset) - else { + let Some(ranged) = goto_declaration(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/goto_definition.rs b/crates/ty_server/src/server/api/requests/goto_definition.rs index 5ca8eda1a8..074b40ddc0 100644 --- a/crates/ty_server/src/server/api/requests/goto_definition.rs +++ b/crates/ty_server/src/server/api/requests/goto_definition.rs @@ -2,10 +2,8 @@ use std::borrow::Cow; use lsp_types::DefinitionRequest; use lsp_types::{DefinitionParams, DefinitionResponse, Uri}; -use ruff_db::PythonFile; use ty_ide::goto_definition; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToLink}; use crate::server::api::traits::{ @@ -51,9 +49,7 @@ impl BackgroundDocumentRequestHandler for GotoDefinitionRequestHandler { return Ok(None); }; - let Some(ranged) = - goto_definition(db, PythonFile::new(db, file, db.python_version()), offset) - else { + let Some(ranged) = goto_definition(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/goto_implementation.rs b/crates/ty_server/src/server/api/requests/goto_implementation.rs index f51fbde05b..c6f0c723af 100644 --- a/crates/ty_server/src/server/api/requests/goto_implementation.rs +++ b/crates/ty_server/src/server/api/requests/goto_implementation.rs @@ -1,10 +1,8 @@ use std::borrow::Cow; use lsp_types::{ImplementationParams, ImplementationRequest, ImplementationResponse, Uri}; -use ruff_db::PythonFile; use ty_ide::goto_implementation; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToLink}; use crate::server::api::traits::{ @@ -50,9 +48,7 @@ impl BackgroundDocumentRequestHandler for GotoImplementationRequestHandler { return Ok(None); }; - let Some(ranged) = - goto_implementation(db, PythonFile::new(db, file, db.python_version()), offset) - else { + let Some(ranged) = goto_implementation(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/goto_type_definition.rs b/crates/ty_server/src/server/api/requests/goto_type_definition.rs index 0dc7c12f46..e324327936 100644 --- a/crates/ty_server/src/server/api/requests/goto_type_definition.rs +++ b/crates/ty_server/src/server/api/requests/goto_type_definition.rs @@ -2,10 +2,8 @@ use std::borrow::Cow; use lsp_types::{TypeDefinitionParams, TypeDefinitionRequest}; use lsp_types::{TypeDefinitionResponse, Uri}; -use ruff_db::PythonFile; use ty_ide::goto_type_definition; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToLink}; use crate::server::api::traits::{ @@ -51,9 +49,7 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler { return Ok(None); }; - let Some(ranged) = - goto_type_definition(db, PythonFile::new(db, file, db.python_version()), offset) - else { + let Some(ranged) = goto_type_definition(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/hover.rs b/crates/ty_server/src/server/api/requests/hover.rs index c427a28c39..4e6fdfce7d 100644 --- a/crates/ty_server/src/server/api/requests/hover.rs +++ b/crates/ty_server/src/server/api/requests/hover.rs @@ -8,10 +8,8 @@ use crate::session::DocumentSnapshot; use crate::session::client::Client; use lsp_types::HoverRequest; use lsp_types::{HoverParams, MarkupContent, Uri}; -use ruff_db::PythonFile; use ty_ide::{MarkupKind, hover}; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; pub(crate) struct HoverRequestHandler; @@ -50,8 +48,7 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler { return Ok(None); }; - let Some(range_info) = hover(db, PythonFile::new(db, file, db.python_version()), offset) - else { + let Some(range_info) = hover(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/inlay_hints.rs b/crates/ty_server/src/server/api/requests/inlay_hints.rs index 905fa9327b..c33139b4ea 100644 --- a/crates/ty_server/src/server/api/requests/inlay_hints.rs +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs @@ -3,11 +3,9 @@ use std::time::Instant; use lsp_types::InlayHintRequest; use lsp_types::{InlayHintParams, Uri}; -use ruff_db::PythonFile; use ruff_db::files::File; use ty_ide::{InlayHintKind, InlayHintLabel, InlayHintTextEdit, inlay_hints}; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::PositionEncoding; use crate::document::{RangeExt, TextSizeExt, ToLink}; @@ -55,7 +53,7 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler { let inlay_hints = inlay_hints( db, - PythonFile::new(db, file, db.python_version()), + db.program_file(file), range, workspace_settings.inlay_hints(), ); diff --git a/crates/ty_server/src/server/api/requests/prepare_call_hierarchy.rs b/crates/ty_server/src/server/api/requests/prepare_call_hierarchy.rs index 03a04b288c..e2029034cb 100644 --- a/crates/ty_server/src/server/api/requests/prepare_call_hierarchy.rs +++ b/crates/ty_server/src/server/api/requests/prepare_call_hierarchy.rs @@ -2,9 +2,7 @@ use std::borrow::Cow; use lsp_types::CallHierarchyPrepareRequest; use lsp_types::{CallHierarchyItem, CallHierarchyPrepareParams, Uri}; -use ruff_db::PythonFile; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::PositionEncoding; use crate::document::{PositionExt, ToRangeExt as _}; @@ -59,11 +57,7 @@ impl BackgroundDocumentRequestHandler for PrepareCallHierarchyRequestHandler { return Ok(None); }; - let Some(items) = ty_ide::prepare_call_hierarchy( - db, - PythonFile::new(db, file, db.python_version()), - offset, - ) else { + let Some(items) = ty_ide::prepare_call_hierarchy(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/prepare_rename.rs b/crates/ty_server/src/server/api/requests/prepare_rename.rs index 1a4d1984fd..686f01d92e 100644 --- a/crates/ty_server/src/server/api/requests/prepare_rename.rs +++ b/crates/ty_server/src/server/api/requests/prepare_rename.rs @@ -1,10 +1,8 @@ use std::borrow::Cow; use lsp_types::{PrepareRenameParams, PrepareRenameRequest, PrepareRenameResult, Uri}; -use ruff_db::PythonFile; use ty_ide::can_rename; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToRangeExt}; use crate::server::api::traits::{ @@ -50,8 +48,7 @@ impl BackgroundDocumentRequestHandler for PrepareRenameRequestHandler { return Ok(None); }; - let Some(range) = can_rename(db, PythonFile::new(db, file, db.python_version()), offset) - else { + let Some(range) = can_rename(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs b/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs index 5a99a62179..c5e3289ba1 100644 --- a/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs +++ b/crates/ty_server/src/server/api/requests/prepare_type_hierarchy.rs @@ -2,9 +2,7 @@ use std::borrow::Cow; use lsp_types::TypeHierarchyPrepareRequest; use lsp_types::{TypeHierarchyItem, TypeHierarchyPrepareParams, Uri}; -use ruff_db::PythonFile; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::PositionExt; use crate::server::api::traits::{ @@ -60,11 +58,7 @@ impl BackgroundDocumentRequestHandler for PrepareTypeHierarchyRequestHandler { return Ok(None); }; - let Some(item) = ty_ide::prepare_type_hierarchy( - db, - PythonFile::new(db, file, db.python_version()), - offset, - ) else { + let Some(item) = ty_ide::prepare_type_hierarchy(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/references.rs b/crates/ty_server/src/server/api/requests/references.rs index 32d213acca..fc0f97fb2e 100644 --- a/crates/ty_server/src/server/api/requests/references.rs +++ b/crates/ty_server/src/server/api/requests/references.rs @@ -2,10 +2,8 @@ use std::borrow::Cow; use lsp_types::ReferencesRequest; use lsp_types::{Location, ReferenceParams, Uri}; -use ruff_db::PythonFile; use ty_ide::find_references; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToLink}; use crate::server::api::traits::{ @@ -53,12 +51,9 @@ impl BackgroundDocumentRequestHandler for ReferencesRequestHandler { let include_declaration = params.context.include_declaration; - let Some(references_result) = find_references( - db, - PythonFile::new(db, file, db.python_version()), - offset, - include_declaration, - ) else { + let Some(references_result) = + find_references(db, db.program_file(file), offset, include_declaration) + else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/rename.rs b/crates/ty_server/src/server/api/requests/rename.rs index 94efe4c1af..4829332b13 100644 --- a/crates/ty_server/src/server/api/requests/rename.rs +++ b/crates/ty_server/src/server/api/requests/rename.rs @@ -3,10 +3,8 @@ use std::collections::HashMap; use lsp_types::RenameRequest; use lsp_types::{RenameParams, TextEdit, Uri, WorkspaceEdit}; -use ruff_db::PythonFile; use ty_ide::rename; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToLink}; use crate::server::api::traits::{ @@ -52,12 +50,8 @@ impl BackgroundDocumentRequestHandler for RenameRequestHandler { return Ok(None); }; - let Some(rename_results) = rename( - db, - PythonFile::new(db, file, db.python_version()), - offset, - ¶ms.new_name, - ) else { + let Some(rename_results) = rename(db, db.program_file(file), offset, ¶ms.new_name) + else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/selection_range.rs b/crates/ty_server/src/server/api/requests/selection_range.rs index 0611f9386d..bfde25ff24 100644 --- a/crates/ty_server/src/server/api/requests/selection_range.rs +++ b/crates/ty_server/src/server/api/requests/selection_range.rs @@ -3,10 +3,8 @@ use std::borrow::Cow; use lsp_types::{ SelectionRange as LspSelectionRange, SelectionRangeParams, SelectionRangeRequest, Uri, }; -use ruff_db::PythonFile; use ty_ide::selection_range; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionExt, ToRangeExt}; use crate::server::api::traits::{ @@ -42,7 +40,7 @@ impl BackgroundDocumentRequestHandler for SelectionRangeRequestHandler { let Some(file) = snapshot.to_notebook_or_file(db) else { return Ok(None); }; - let python_file = PythonFile::new(db, file, db.python_version()); + let python_file = db.program_file(file).python_file(db); let mut results = Vec::new(); diff --git a/crates/ty_server/src/server/api/requests/signature_help.rs b/crates/ty_server/src/server/api/requests/signature_help.rs index fdf1bf3309..57e1f532c0 100644 --- a/crates/ty_server/src/server/api/requests/signature_help.rs +++ b/crates/ty_server/src/server/api/requests/signature_help.rs @@ -11,10 +11,8 @@ use lsp_types::{ Documentation, ParameterInformation, ParameterInformationLabel, SignatureHelp, SignatureHelpParams, SignatureInformation, Uri, }; -use ruff_db::PythonFile; use ty_ide::signature_help; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; pub(crate) struct SignatureHelpRequestHandler; @@ -56,9 +54,7 @@ impl BackgroundDocumentRequestHandler for SignatureHelpRequestHandler { // Extract signature help capabilities from the client let resolved_capabilities = snapshot.resolved_client_capabilities(); - let Some(signature_help_info) = - signature_help(db, PythonFile::new(db, file, db.python_version()), offset) - else { + let Some(signature_help_info) = signature_help(db, db.program_file(file), offset) else { return Ok(None); }; diff --git a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs index ee72c99c24..4bf6b9121f 100644 --- a/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs +++ b/crates/ty_server/src/server/api/requests/workspace_diagnostic.rs @@ -10,7 +10,6 @@ use lsp_types::{ WorkspaceDiagnosticReportPartialResult, WorkspaceDocumentDiagnosticReport, WorkspaceFullDocumentDiagnosticReport, WorkspaceUnchangedDocumentDiagnosticReport, }; -use ruff_db::PythonFile; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::File; use ruff_db::source::source_text; @@ -18,8 +17,7 @@ use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use serde_json::json; use ty_ide::{Hint, hints}; -use ty_project::Db as _; -use ty_project::{ProgressReporter, ProjectDatabase}; +use ty_project::{ProgressReporter, ProjectDatabase, SemanticDb as _}; use crate::PositionEncoding; use crate::capabilities::ResolvedClientCapabilities; @@ -241,7 +239,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> { } fn report_checked_file(&self, db: &ProjectDatabase, file: File, diagnostics: &[Diagnostic]) { - let unnecessary_hints = hints(db, PythonFile::new(db, file, db.python_version())); + let unnecessary_hints = hints(db, db.program_file(file)); // Another thread might have panicked at this point because of a salsa cancellation which // poisoned the result. If the response is poisoned, just don't report and wait for our thread @@ -289,7 +287,7 @@ impl ProgressReporter for WorkspaceDiagnosticsProgressReporter<'_> { let response = &mut self.state.get_mut().unwrap().response; for (file, diagnostics) in by_file { - let unnecessary_hints = hints(db, PythonFile::new(db, file, db.python_version())); + let unnecessary_hints = hints(db, db.program_file(file)); response.write_diagnostics_for_file(db, file, &diagnostics, &unnecessary_hints); } response.maybe_flush(); diff --git a/crates/ty_server/src/server/api/semantic_tokens.rs b/crates/ty_server/src/server/api/semantic_tokens.rs index 0e33197fcd..a9c906807a 100644 --- a/crates/ty_server/src/server/api/semantic_tokens.rs +++ b/crates/ty_server/src/server/api/semantic_tokens.rs @@ -1,11 +1,9 @@ use lsp_types::SemanticToken; -use ruff_db::PythonFile; use ruff_db::source::{line_index, source_text}; use ruff_source_file::OneIndexed; use ruff_text_size::{Ranged, TextRange}; use ty_ide::{SemanticTokenModifier, SemanticTokenType, semantic_tokens}; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::document::{PositionEncoding, ToRangeExt}; @@ -20,8 +18,7 @@ pub(crate) fn generate_semantic_tokens( ) -> Vec { let source = source_text(db, file); let line_index = line_index(db, file); - let semantic_token_data = - semantic_tokens(db, PythonFile::new(db, file, db.python_version()), range); + let semantic_token_data = semantic_tokens(db, db.program_file(file), range); let mut encoder = Encoder { tokens: Vec::with_capacity(semantic_token_data.len()), diff --git a/crates/ty_server/src/server/api/type_hierarchy.rs b/crates/ty_server/src/server/api/type_hierarchy.rs index 6fcf81f50e..2b0095a67e 100644 --- a/crates/ty_server/src/server/api/type_hierarchy.rs +++ b/crates/ty_server/src/server/api/type_hierarchy.rs @@ -1,7 +1,5 @@ use lsp_types::{SymbolKind, TypeHierarchyItem}; -use ruff_db::PythonFile; -use ty_project::Db as _; -use ty_project::ProjectDatabase; +use ty_project::{ProjectDatabase, SemanticDb as _}; use crate::PositionEncoding; use crate::document::{ToRangeExt, resolve_file_uri_range}; @@ -35,7 +33,7 @@ pub(crate) fn hierarchy_handler( ) else { continue; }; - let file = PythonFile::new(db, file, db.python_version()); + let file = db.program_file(file); let hierarchy_types = match hierarchy_kind { TypeHierarchyKind::Subtypes => ty_ide::type_hierarchy_subtypes(db, file, offset), TypeHierarchyKind::Supertypes => ty_ide::type_hierarchy_supertypes(db, file, offset), diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index b57704fe11..017b0a3edf 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -27,7 +27,7 @@ use ty_project::watch::{ChangeEvent, CreatedKind}; use ty_project::{ChangeResult, Db as _, ProjectDatabase, ProjectMetadata}; use index::DocumentError; -use ty_python_core::program::UseDefaultStrategy; +use ty_python_core::program::{Program, UseDefaultStrategy}; pub(crate) use self::options::InitializationOptions; pub use self::options::{ClientOptions, DiagnosticMode, GlobalOptions, WorkspaceOptions}; @@ -1087,7 +1087,11 @@ impl Session { let paths = self .project_dbs() .flat_map(|db| { - ty_module_resolver::system_module_search_paths(db).map(move |path| (db, path)) + ty_module_resolver::system_module_search_paths( + db, + Program::get(db).resolver_environment(db), + ) + .map(move |path| (db, path)) }) .filter(|(db, path)| !path.starts_with(db.project().root(*db))) .map(|(_, path)| path) diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index 464020fa4f..b93f2b699c 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -181,6 +181,7 @@ Settings: Settings { Memory report: =======SALSA STRUCTS======= `Program` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`ResolverEnvironment` metadata=[X.XXMB] fields=[X.XXMB] count=1 `Project` metadata=[X.XXMB] fields=[X.XXMB] count=1 `FileRoot` metadata=[X.XXMB] fields=[X.XXMB] count=1 `ModuleResolveModeIngredient` metadata=[X.XXMB] fields=[X.XXMB] count=1 diff --git a/crates/ty_test/src/db.rs b/crates/ty_test/src/db.rs index f97a942cd4..af2cf8255b 100644 --- a/crates/ty_test/src/db.rs +++ b/crates/ty_test/src/db.rs @@ -1,5 +1,6 @@ use crate::config::{Analysis, Rules, ScriptOptions}; use camino::{Utf8Component, Utf8PathBuf}; +use ruff_db::Db as SourceDb; use ruff_db::diagnostic::{Diagnostic, Severity}; use ruff_db::files::{File, Files}; use ruff_db::source::source_text; @@ -8,15 +9,14 @@ use ruff_db::system::{ WritableSystem, }; use ruff_db::vendored::VendoredFileSystem; -use ruff_db::{Db as SourceDb, PythonFile}; use ruff_notebook::{Notebook, NotebookError}; use salsa::Setter as _; use std::borrow::Cow; use std::sync::Arc; use tempfile::TempDir; -use ty_module_resolver::{ModuleGlobSetBuilder, SearchPaths}; -use ty_python_core::Db as _; +use ty_module_resolver::ModuleGlobSetBuilder; use ty_python_core::program::Program; +use ty_python_core::{Db as _, ProgramFile}; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::{ AnalysisSettings, Db as SemanticDb, check_file_unwrap, default_lint_registry, @@ -50,10 +50,6 @@ impl Db { db } - pub(crate) fn python_version(&self) -> ruff_python_ast::PythonVersion { - Program::get(self).python_version(self) - } - fn settings(&self) -> Settings { self.settings.unwrap() } @@ -117,11 +113,7 @@ impl SourceDb for Db { } #[salsa::db] -impl ty_module_resolver::Db for Db { - fn search_paths(&self) -> &SearchPaths { - Program::get(self).search_paths(self) - } -} +impl ty_module_resolver::Db for Db {} #[salsa::db] impl ty_python_core::Db for Db { @@ -137,7 +129,11 @@ impl SemanticDb for Db { return Vec::new(); } - check_file_unwrap(self, PythonFile::new(self, file, self.python_version())) + check_file_unwrap(self, self.program_file(file)) + } + + fn program_file(&self, file: File) -> ProgramFile<'_> { + Program::get(self).program_file(self, file) } fn rule_selection(&self, file: File) -> &RuleSelection { diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index 46a967ef80..65a6875dc4 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -7,12 +7,12 @@ use mdtest::parser::{self}; use mdtest::{ Failures, FileFailures, MarkdownEdit, OutputFormat, TestFile, TestOutcome, attempt_test, }; +use ruff_db::Db; use ruff_db::cancellation::CancellationTokenSource; use ruff_db::diagnostic::DiagnosticId; use ruff_db::files::{FileRootKind, system_path_to_file}; use ruff_db::system::{DbWithWritableSystem as _, SystemPath, SystemPathBuf}; use ruff_db::testing::{setup_logging, setup_logging_with_filter}; -use ruff_db::{Db, PythonFile}; use ruff_diagnostics::Applicability; use ruff_python_ast::PythonVersion; use ruff_source_file::OneIndexed; @@ -25,7 +25,7 @@ use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; use ty_python_semantic::pull_types::pull_types; use ty_python_semantic::types::UNDEFINED_REVEAL; use ty_python_semantic::{ - PythonEnvironment, PythonVersionSource, PythonVersionWithSource, SysPrefixPathOrigin, + Db as _, PythonEnvironment, PythonVersionSource, PythonVersionWithSource, SysPrefixPathOrigin, fix_all_diagnostics, }; @@ -361,10 +361,8 @@ fn run_test( all_diagnostics.extend(diagnostics); - let pull_types_result = attempt_test( - |file| pull_types(db, PythonFile::new(db, file, python_version)), - test_file, - ); + let pull_types_result = + attempt_test(|file| pull_types(db, db.program_file(file)), test_file); match pull_types_result { Ok(()) => {} Err(failures) => { @@ -425,7 +423,6 @@ fn run_test( let token_source = CancellationTokenSource::new(); let result = fix_all_diagnostics( db, - python_version, all_diagnostics, Applicability::Unsafe, &token_source.token(), @@ -496,12 +493,12 @@ struct ModuleInconsistency<'db> { /// `list_module`. fn run_module_resolution_consistency_test(db: &db::Db) -> Result<(), Vec>> { let mut errs = vec![]; - let python_version = db.python_version(); - for from_list in list_modules(db, python_version).iter().copied() { + let environment = Program::get(db).resolver_environment(db); + for from_list in list_modules(db, environment).iter().copied() { // TODO: For now list_modules does not partake in desperate module resolution so // only compare against confident module resolution. errs.push( - match resolve_module_confident(db, python_version, from_list.name(db)) { + match resolve_module_confident(db, environment, from_list.name(db)) { None => ModuleInconsistency { db, from_list, diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index 6a75c8fa80..baeb5ea85f 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -27,7 +27,7 @@ use ty_ide::{NavigationTarget, NavigationTargets, hints, signature_help}; use ty_project::metadata::options::Options; use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; use ty_project::{CheckMode, ProjectMetadata}; -use ty_project::{Db, ProjectDatabase}; +use ty_project::{Db, ProjectDatabase, SemanticDb as _}; use ty_python_core::program::{FallibleStrategy, Program}; use ty_python_semantic::ProgramEnvironment; use wasm_bindgen::prelude::*; @@ -277,13 +277,10 @@ impl Workspace { #[wasm_bindgen(js_name = "hints")] pub fn hints(&self, file_id: &FileHandle) -> Result, Error> { - Ok(hints( - &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), - ) - .into_iter() - .map(|hint| Hint::from_ide_hint(&self.db, file_id.file, self.position_encoding, &hint)) - .collect()) + Ok(hints(&self.db, self.db.program_file(file_id.file)) + .into_iter() + .map(|hint| Hint::from_ide_hint(&self.db, file_id.file, self.position_encoding, &hint)) + .collect()) } /// Checks all open files @@ -337,11 +334,9 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = goto_type_definition( - &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), - offset, - ) else { + let Some(targets) = + goto_type_definition(&self.db, self.db.program_file(file_id.file), offset) + else { return Ok(Vec::new()); }; @@ -365,11 +360,8 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = goto_declaration( - &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), - offset, - ) else { + let Some(targets) = goto_declaration(&self.db, self.db.program_file(file_id.file), offset) + else { return Ok(Vec::new()); }; @@ -393,11 +385,8 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = goto_definition( - &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), - offset, - ) else { + let Some(targets) = goto_definition(&self.db, self.db.program_file(file_id.file), offset) + else { return Ok(Vec::new()); }; @@ -421,12 +410,9 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = find_references( - &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), - offset, - true, - ) else { + let Some(targets) = + find_references(&self.db, self.db.program_file(file_id.file), offset, true) + else { return Ok(Vec::new()); }; @@ -465,11 +451,7 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(range) = can_rename( - &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), - offset, - ) else { + let Some(range) = can_rename(&self.db, self.db.program_file(file_id.file), offset) else { return Ok(None); }; @@ -492,13 +474,13 @@ impl Workspace { let index = line_index(&self.db, file_id.file); let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let python_file = PythonFile::new(&self.db, file_id.file, self.db.python_version()); + let program_file = self.db.program_file(file_id.file); - if can_rename(&self.db, python_file, offset).is_none() { + if can_rename(&self.db, program_file, offset).is_none() { return Ok(Vec::new()); } - let Some(rename_results) = rename(&self.db, python_file, offset, new_name) else { + let Some(rename_results) = rename(&self.db, program_file, offset, new_name) else { return Ok(Vec::new()); }; @@ -523,11 +505,7 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(range_info) = hover( - &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), - offset, - ) else { + let Some(range_info) = hover(&self.db, self.db.program_file(file_id.file), offset) else { return Ok(None); }; @@ -558,13 +536,13 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; let settings = ty_ide::CompletionSettings::default(); - let python_file = PythonFile::new(&self.db, file_id.file, self.db.python_version()); - let env = ProgramEnvironment::from_file(python_file); + let program_file = self.db.program_file(file_id.file); + let env = ProgramEnvironment::from_file(program_file); let completions = ty_ide::completion( &self.db, &settings, CompletionCapabilities::default(), - python_file, + program_file, offset, ); @@ -608,7 +586,7 @@ impl Workspace { let result = inlay_hints( &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), + self.db.program_file(file_id.file), range.to_text_range(&index, &source, self.position_encoding)?, // TODO: Provide a way to configure this &InlayHintSettings { @@ -665,11 +643,8 @@ impl Workspace { let index = line_index(&self.db, file_id.file); let source = source_text(&self.db, file_id.file); - let semantic_token = ty_ide::semantic_tokens( - &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), - None, - ); + let semantic_token = + ty_ide::semantic_tokens(&self.db, self.db.program_file(file_id.file), None); let result = semantic_token .iter() @@ -694,7 +669,7 @@ impl Workspace { let semantic_token = ty_ide::semantic_tokens( &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), + self.db.program_file(file_id.file), Some(range.to_text_range(&index, &source, self.position_encoding)?), ); @@ -731,7 +706,7 @@ impl Workspace { actions.extend( ty_ide::code_actions( &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), + self.db.program_file(file_id.file), range, diagnostic.inner.id().as_str(), ) @@ -766,11 +741,9 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(signature_help_info) = signature_help( - &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), - offset, - ) else { + let Some(signature_help_info) = + signature_help(&self.db, self.db.program_file(file_id.file), offset) + else { return Ok(None); }; @@ -817,11 +790,9 @@ impl Workspace { let offset = position.to_text_size(&source, &index, self.position_encoding)?; - let Some(targets) = document_highlights( - &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), - offset, - ) else { + let Some(targets) = + document_highlights(&self.db, self.db.program_file(file_id.file), offset) + else { return Ok(Vec::new()); }; diff --git a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs index 181d7d54ff..cbe62a7a19 100644 --- a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs +++ b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs @@ -8,19 +8,17 @@ use std::sync::{Arc, Mutex, OnceLock}; use libfuzzer_sys::{Corpus, fuzz_target}; use ruff_db::Db as SourceDb; -use ruff_db::PythonFile; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::{File, Files, system_path_to_file}; use ruff_db::system::{ DbWithTestSystem, DbWithWritableSystem as _, System, SystemPathBuf, TestSystem, }; use ruff_db::vendored::VendoredFileSystem; -use ruff_python_ast::PythonVersion; use ruff_python_parser::{Mode, ParseOptions, parse_unchecked}; use ty_module_resolver::{Db as ModuleResolverDb, SearchPathSettings}; -use ty_python_core::Db as _; use ty_python_core::platform::PythonPlatform; use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; +use ty_python_core::{Db as _, ProgramFile}; use ty_python_semantic::lint::LintRegistry; use ty_python_semantic::types::check_types; use ty_python_semantic::{ @@ -57,10 +55,6 @@ impl TestDb { analysis_settings: AnalysisSettings::default().into(), } } - - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } } #[salsa::db] @@ -89,11 +83,7 @@ impl DbWithTestSystem for TestDb { } #[salsa::db] -impl ModuleResolverDb for TestDb { - fn search_paths(&self) -> &ty_module_resolver::SearchPaths { - Program::get(self).search_paths(self) - } -} +impl ModuleResolverDb for TestDb {} #[salsa::db] impl ty_python_core::Db for TestDb { @@ -106,13 +96,16 @@ impl ty_python_core::Db for TestDb { impl SemanticDb for TestDb { fn check_file(&self, file: File) -> Vec { if self.should_check_file(file) { - let python_file = PythonFile::new(self, file, self.python_version()); - ty_python_semantic::check_file_unwrap(self, python_file) + ty_python_semantic::check_file_unwrap(self, self.program_file(file)) } else { Vec::new() } } + fn program_file(&self, file: File) -> ProgramFile<'_> { + Program::get(self).program_file(self, file) + } + fn rule_selection(&self, _file: File) -> &RuleSelection { &self.rule_selection } @@ -183,7 +176,7 @@ fn do_fuzz(case: &[u8]) -> Corpus { for path in &["/src/a.py", "/src/a.pyi"] { db.write_file(path, code).unwrap(); let file = system_path_to_file(&*db, path).unwrap(); - check_types(&*db, PythonFile::new(&*db, file, db.python_version())); + check_types(&*db, db.program_file(file)); db.memory_file_system().remove_file(path).unwrap(); file.sync(&mut *db); } From aa5b04ad924393205b99db05ed59ab5241d5e8d2 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 4 Aug 2026 08:51:18 +0200 Subject: [PATCH 224/390] [ty] Support different type inference settings in a single Salsa DB (#27152) ## Summary This is the last PR that adds support for type checking multiple programs with different Python Version, search paths, and platform using a single Salsa DB. This PR changes `Program` from a singleton input to an interned struct. `ProgramFile` changes from `(File, ResolverEnvironment)` to `(File, Program)`. Changing `Program` from an input to an interned means that it now has a `'db` lifetime. This requires changing how we construct and store `Program`. That's why some of the db initialization code changes. I doubt that it will be noticeable. But this should speed up mdtests that contain tests with different Python versions (e.g. Py312, Py311, Py312, Py311), because we now reuse parsed modules, and even type inference results because the inference is now keyed by Program (instead of updating an existing Program) ## Memory regression I had codex look into the CodSpeed reported memory regression. The reason is that `Program` now being an interned and being initialized later changes the assigned `File` ids, which in turn changes in which order we iterate over the project files. And this change the max peak memory usage. It can also change the usage reported on the memory usage report (we enter fixpoint cycles from different heads, etc). The changes in the memory report are mainly from the new `ProgramFile::python_version` (needed to mitigate a larger regression, but we can reconsider), and that `Project` now stores `ProgramSettings`. --- Cargo.lock | 1 - crates/ruff_benchmark/Cargo.toml | 4 +- .../benches/module_resolution.rs | 15 +- crates/ty/tests/file_watching.rs | 3 +- crates/ty_ide/src/code_action.rs | 2 - crates/ty_ide/src/inlay_hints.rs | 2 - crates/ty_ide/src/lib.rs | 22 +-- crates/ty_ide/src/semantic_tokens.rs | 2 - crates/ty_ide/src/symbols.rs | 3 +- crates/ty_ide/src/workspace_symbols.rs | 1 - crates/ty_module_resolver/src/resolve.rs | 5 +- crates/ty_project/src/db.rs | 137 ++++++++---------- crates/ty_project/src/db/changes.rs | 35 +++-- crates/ty_project/src/lib.rs | 84 +++++------ crates/ty_project/src/metadata/options.rs | 2 +- .../ty_project/src/watch/project_watcher.rs | 3 +- crates/ty_python_core/Cargo.toml | 1 + crates/ty_python_core/src/builder.rs | 4 +- crates/ty_python_core/src/db.rs | 62 ++++++-- crates/ty_python_core/src/lib.rs | 25 ++-- crates/ty_python_core/src/platform.rs | 2 +- crates/ty_python_core/src/program.rs | 109 ++++---------- crates/ty_python_core/src/program_file.rs | 31 ++-- crates/ty_python_semantic/Cargo.toml | 2 +- crates/ty_python_semantic/src/db.rs | 45 +++--- .../ty_python_semantic/src/diagnostic/mod.rs | 9 +- crates/ty_python_semantic/src/lib.rs | 7 +- crates/ty_python_semantic/src/place.rs | 10 +- .../src/types/class/known.rs | 47 +++--- .../ty_python_semantic/src/types/context.rs | 4 +- .../src/types/diagnostic.rs | 7 +- .../src/types/infer/builder.rs | 3 +- .../src/types/infer/builder/imports.rs | 5 +- .../types/infer/builder/type_expression.rs | 3 +- .../src/types/infer/tests.rs | 83 ++++++++++- crates/ty_python_semantic/src/types/tests.rs | 23 ++- crates/ty_python_semantic/tests/corpus.rs | 44 +++--- .../server/api/requests/execute_command.rs | 3 +- crates/ty_server/src/session.rs | 4 +- crates/ty_server/tests/e2e/commands.rs | 17 ++- .../e2e__commands__debug_command.snap | 8 +- crates/ty_site_packages/src/version.rs | 6 +- crates/ty_test/src/db.rs | 29 +++- crates/ty_test/src/lib.rs | 7 +- crates/ty_wasm/src/lib.rs | 28 ++-- fuzz/Cargo.lock | 133 +++++++++-------- fuzz/Cargo.toml | 2 +- fuzz/fuzz_targets/ty_check_invalid_syntax.rs | 64 ++++---- 48 files changed, 617 insertions(+), 531 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65928cd0b5..fdc535485a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3163,7 +3163,6 @@ dependencies = [ "tracing", "ty_module_resolver", "ty_project", - "ty_python_core", ] [[package]] diff --git a/crates/ruff_benchmark/Cargo.toml b/crates/ruff_benchmark/Cargo.toml index 62cc4818a5..cf1a0fafd1 100644 --- a/crates/ruff_benchmark/Cargo.toml +++ b/crates/ruff_benchmark/Cargo.toml @@ -23,7 +23,6 @@ ruff_python_formatter = { workspace = true, optional = true } ruff_python_parser = { workspace = true, optional = true } ruff_python_trivia = { workspace = true, optional = true } ty_module_resolver = { workspace = true, optional = true } -ty_python_core = { workspace = true, optional = true } ty_project = { workspace = true, optional = true } anyhow = { workspace = true } @@ -61,7 +60,7 @@ ruff_instrumented = [ # Enables the ty instrumented benchmarks ty_instrumented = ["criterion", "ty_project", "ruff_python_trivia"] # Enables the module-resolution benchmark -module_resolution = ["divan", "ty_module_resolver", "ty_project", "ty_python_core"] +module_resolution = ["divan", "ty_module_resolver", "ty_project"] codspeed = ["codspeed-criterion-compat"] # Enables the ty_walltime benchmarks ty_walltime = ["ruff_db/os", "ty_project", "divan"] @@ -114,7 +113,6 @@ ignored = [ "ruff_python_parser", "ruff_python_trivia", "ty_module_resolver", - "ty_python_core", "mimalloc", "tikv-jemallocator" ] diff --git a/crates/ruff_benchmark/benches/module_resolution.rs b/crates/ruff_benchmark/benches/module_resolution.rs index 61bb8ba378..7f7c23f3c9 100644 --- a/crates/ruff_benchmark/benches/module_resolution.rs +++ b/crates/ruff_benchmark/benches/module_resolution.rs @@ -12,8 +12,7 @@ use ty_module_resolver::{ImportingFile, ModuleName, resolve_module}; use ty_project::metadata::options::{EnvironmentOptions, Options}; use ty_project::metadata::python_version::SupportedPythonVersion; use ty_project::metadata::value::RelativePathBuf; -use ty_project::{ProjectDatabase, ProjectMetadata}; -use ty_python_core::program::Program; +use ty_project::{Db as _, ProjectDatabase, ProjectMetadata}; const SEEDED_TARGETS: &[&str] = &["target_0", "target_1", "target_2", "target_3", "target_4"]; // Exercise stub-overlay discovery followed by normal fallback. @@ -73,8 +72,10 @@ fn setup_case(n: usize) -> Case { }); let db = ProjectDatabase::fallible(metadata, system).unwrap(); - // Intern the resolver environment before timing so its initial allocation is not benchmarked. - let _ = Program::get(&db).resolver_environment(&db); + + // Keep lazy program and resolver initialization out of the measured module-resolution queries. + let _ = db.project().program(&db).resolver_environment(&db); + let importing_file = system_path_to_file(&db, &importing_path).unwrap(); let resolves = SEEDED_TARGETS @@ -96,7 +97,11 @@ fn ty_module_resolver(bencher: Bencher) { bencher .with_inputs(|| setup_case(PATHS)) .bench_local_refs(|case| { - let environment = Program::get(&case.db).resolver_environment(&case.db); + let environment = case + .db + .project() + .program(&case.db) + .resolver_environment(&case.db); for name in &case.resolves { black_box(resolve_module( &case.db, diff --git a/crates/ty/tests/file_watching.rs b/crates/ty/tests/file_watching.rs index 7cc6b86024..943cab4bb5 100644 --- a/crates/ty/tests/file_watching.rs +++ b/crates/ty/tests/file_watching.rs @@ -19,7 +19,6 @@ use ty_project::metadata::value::{RelativeGlobPattern, RelativePathBuf}; use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher}; use ty_project::{ChangeResult, Db, ProjectDatabase, ProjectMetadata}; use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::Program; use ty_static::EnvVars; struct TestCase { @@ -39,7 +38,7 @@ fn resolve_module_confident<'db>( ) -> Option> { ty_module_resolver::resolve_module_confident( db, - Program::get(db).resolver_environment(db), + db.project().program(db).resolver_environment(db), module_name, ) } diff --git a/crates/ty_ide/src/code_action.rs b/crates/ty_ide/src/code_action.rs index fcd8648c66..67fd68257c 100644 --- a/crates/ty_ide/src/code_action.rs +++ b/crates/ty_ide/src/code_action.rs @@ -892,8 +892,6 @@ mod tests { let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); - db.init_program().unwrap(); - let mut cleansed = dedent(source).to_string(); let start = cleansed diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 4669252242..265f9a6531 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -819,8 +819,6 @@ mod tests { let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); - db.init_program().unwrap(); - let source = dedent(source); let start = source.find(START); diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 34ec0997a9..f5a9276d3c 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -409,7 +409,7 @@ mod tests { use ruff_db::files::{File, FileRootKind, system_path_to_file}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_db::source::{SourceText, source_text}; - use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem, SystemPath, SystemPathBuf}; + use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf}; use ruff_python_ast::PythonVersion; use ruff_python_codegen::Stylist; use ruff_python_trivia::textwrap::dedent; @@ -418,7 +418,7 @@ mod tests { use ty_project::{Db as _, ProjectMetadata, SemanticDb as _}; use ty_python_core::ProgramFile; use ty_python_core::platform::PythonPlatform; - use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; + use ty_python_core::program::{FallibleStrategy, ProgramSettings}; use ty_python_semantic::PythonVersionWithSource; /// A way to create a simple single-file (named `main.py`) cursor test. @@ -525,10 +525,9 @@ mod tests { let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); - db.init_program_with_python_version( - self.python_version.unwrap_or_else(PythonVersion::latest_ty), - ) - .unwrap(); + if let Some(python_version) = self.python_version { + db.set_python_version(python_version); + } let mut cursor: Option = None; for &Source { @@ -656,7 +655,7 @@ mod tests { let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", project_root.clone())); - // Write site-packages files first (before init) + // Write site-packages files first. for Source { path, contents, @@ -668,11 +667,6 @@ mod tests { .expect("write to memory file system to be successful"); } - // Create /src directory for first-party code - db.memory_file_system() - .create_directory_all(&project_root) - .expect("create /src directory"); - // Configure search paths with site-packages let search_paths = SearchPathSettings { src_roots: vec![project_root.clone()], @@ -682,8 +676,8 @@ mod tests { .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) .expect("valid search paths"); - Program::from_settings( - &db, + db.project().update_program( + &mut db, ProgramSettings { python_version: PythonVersionWithSource::default(), python_platform: PythonPlatform::default(), diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index 0432ab3df7..86a78648ce 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -4708,8 +4708,6 @@ from pathlib import Missing as Alias let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); - db.init_program().unwrap(); - let path = SystemPath::new("src/main.py"); db.write_file(path, ruff_python_trivia::textwrap::dedent(source)) .expect("Write to memory file system to always succeed"); diff --git a/crates/ty_ide/src/symbols.rs b/crates/ty_ide/src/symbols.rs index 2ccdaec2ec..2020ccb099 100644 --- a/crates/ty_ide/src/symbols.rs +++ b/crates/ty_ide/src/symbols.rs @@ -3229,8 +3229,7 @@ class C: ... let metadata = ProjectMetadata::new("test", SystemPathBuf::from("/")); let mut db = TestDb::new(metadata); - db.init_program_with_python_version(self.python_version.unwrap_or_default()) - .unwrap(); + db.set_python_version(self.python_version.unwrap_or_default()); for Source { path, contents } in &self.sources { db.write_file(path, contents) diff --git a/crates/ty_ide/src/workspace_symbols.rs b/crates/ty_ide/src/workspace_symbols.rs index f882fc6adf..da1b0a3d17 100644 --- a/crates/ty_ide/src/workspace_symbols.rs +++ b/crates/ty_ide/src/workspace_symbols.rs @@ -15,7 +15,6 @@ pub fn workspace_symbols(db: &dyn Db, query: &str) -> Vec { let _span = workspace_symbols_span.enter(); let project = db.project(); - let query = QueryPattern::fuzzy(query); let files = project.files(db); let files: Vec<_> = files.iter().copied().collect(); diff --git a/crates/ty_module_resolver/src/resolve.rs b/crates/ty_module_resolver/src/resolve.rs index fef29c17f5..0a100649e9 100644 --- a/crates/ty_module_resolver/src/resolve.rs +++ b/crates/ty_module_resolver/src/resolve.rs @@ -836,9 +836,8 @@ impl SearchPaths { /// Returns a new `SearchPaths` with no search paths configured. /// - /// This is primarily useful for testing. - #[cfg(test)] - pub(crate) fn empty(vendored: &VendoredFileSystem) -> Self { + /// The vendored standard library remains available. + pub fn empty(vendored: &VendoredFileSystem) -> Self { Self { static_paths: vec![], stdlib_path: Some(SearchPath::vendored_stdlib()), diff --git a/crates/ty_project/src/db.rs b/crates/ty_project/src/db.rs index a9cff91b5b..a276873110 100644 --- a/crates/ty_project/src/db.rs +++ b/crates/ty_project/src/db.rs @@ -13,12 +13,9 @@ use ruff_db::diagnostic::Diagnostic; use ruff_db::files::{File, Files}; use ruff_db::system::System; use ruff_db::vendored::VendoredFileSystem; -use ruff_python_ast::PythonVersion; use salsa::{Database, Event, Setter}; use ty_python_core::ProgramFile; -use ty_python_core::program::{ - FallibleStrategy, MisconfigurationStrategy, Program, UseDefaultStrategy, -}; +use ty_python_core::program::{FallibleStrategy, MisconfigurationStrategy, UseDefaultStrategy}; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::{AnalysisSettings, Db as SemanticDb}; @@ -26,9 +23,6 @@ mod changes; #[salsa::db] pub trait Db: SemanticDb { - /// Returns the Python version for files in the primary environment. - fn python_version(&self) -> PythonVersion; - fn project(&self) -> Project; fn dyn_clone(&self) -> Box; @@ -87,16 +81,12 @@ impl ProjectDatabase { /// Permanently freezes the most heavily read inputs that are immutable during a one-shot check. /// - /// This is intentionally not exhaustive. It includes every [`Program`] input, the most heavily + /// This is intentionally not exhaustive. It includes the program, the most heavily /// read immutable [`Project`] inputs, and every field on files created after this call. Existing /// files retain their durability. This must not be used by incremental consumers or checks that /// apply fixes. pub fn freeze(&mut self) { - let program = Program::try_get(self).expect("the program should be initialized"); - let project = self.project(); - - program.freeze(self); - project.freeze(self); + self.project().freeze(self); self.files.freeze(); } @@ -142,28 +132,30 @@ impl ProjectDatabase { let merged_options = project_metadata.to_merged_options(); - // Initialize the `Program` singleton let (program_settings, program_settings_diagnostics) = strategy .to_anyhow(merged_options.to_program_settings(db.system(), db.vendored(), strategy))?; - // This must be called before `from_settings`, or the `SearchPath` root + // This must be called before `from_metadata`, or the `SearchPath` root // will take precedence over the `Project` root, resulting in // all project files having HIGH durability. project_metadata.try_add_project_root(&db); - Program::from_settings(&db, program_settings); - - let (settings, settings_diagnostics) = strategy + let (settings, mut settings_diagnostics) = strategy .map_err(merged_options.to_settings(&db, strategy), |error| { anyhow::anyhow!("{}", error.pretty(&db)) })?; + settings_diagnostics.extend( + program_settings_diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_diagnostic(&db)), + ); db.project = Some(Project::from_metadata( &db, project_metadata, settings, + program_settings, settings_diagnostics, - program_settings_diagnostics, )); Ok(db) @@ -547,7 +539,7 @@ impl SemanticDb for ProjectDatabase { } fn program_file(&self, file: File) -> ProgramFile<'_> { - Program::get(self).program_file(self, file) + self.project().program(self).program_file(self, file) } fn rule_selection(&self, file: File) -> &RuleSelection { @@ -610,10 +602,6 @@ impl salsa::Database for ProjectDatabase {} #[salsa::db] impl Db for ProjectDatabase { - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } - fn project(&self) -> Project { self.project.unwrap() } @@ -625,15 +613,17 @@ impl Db for ProjectDatabase { #[cfg(feature = "format")] mod format { - use crate::{Db as _, ProjectDatabase}; + use crate::ProjectDatabase; use ruff_db::files::File; use ruff_python_formatter::{Db as FormatDb, PyFormatOptions}; + use ty_python_semantic::Db as _; #[salsa::db] impl FormatDb for ProjectDatabase { fn format_options(&self, file: File) -> PyFormatOptions { let source_ty = file.source_type(self); - PyFormatOptions::from_source_type(source_ty).with_target_version(self.python_version()) + PyFormatOptions::from_source_type(source_ty) + .with_target_version(self.program_file(file).python_version(self)) } } } @@ -648,13 +638,16 @@ pub(crate) mod testing { use ruff_db::files::{File, FileRootKind, Files}; use ruff_db::system::{DbWithTestSystem, System, TestSystem}; use ruff_db::vendored::VendoredFileSystem; + #[cfg(feature = "testing")] use ruff_python_ast::PythonVersion; use ty_module_resolver::SearchPathSettings; use ty_python_core::ProgramFile; use ty_python_core::platform::PythonPlatform; - use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; + use ty_python_core::program::{FallibleStrategy, ProgramSettings}; + #[cfg(feature = "testing")] + use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; - use ty_python_semantic::{AnalysisSettings, ProgramEnvironment, PythonVersionWithSource}; + use ty_python_semantic::{AnalysisSettings, PythonVersionWithSource}; use crate::db::Db; use crate::{Project, ProjectMetadata}; @@ -694,51 +687,52 @@ pub(crate) mod testing { .to_merged_options() .to_settings(&db, &FallibleStrategy) .unwrap(); - let project = - Project::from_metadata(&db, project, settings, settings_diagnostics, Vec::new()); + let root = project.root().to_path_buf(); + db.system + .memory_file_system() + .create_directory_all(&root) + .expect("create project root"); + let search_paths = SearchPathSettings::new(vec![root.clone()]) + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .expect("Valid search path settings"); + + db.files().try_add_root(&db, &root, FileRootKind::Project); + + let program_settings = ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths, + }; + let project = Project::from_metadata( + &db, + project, + settings, + program_settings, + settings_diagnostics, + ); db.project = Some(project); db } - pub fn init_program(&mut self) -> anyhow::Result<()> { - self.init_program_with_python_version(PythonVersion::latest_ty()) - } - - pub fn init_program_with_python_version( - &mut self, - python_version: PythonVersion, - ) -> anyhow::Result<()> { - let root = self.project().root(self); - - let search_paths = SearchPathSettings::new(vec![root.to_path_buf()]) - .to_search_paths(self.system(), self.vendored(), &FallibleStrategy) - .expect("Valid search path settings"); - - self.files().try_add_root(self, root, FileRootKind::Project); - - Program::from_settings( - self, - ProgramSettings { - python_version: PythonVersionWithSource { - source: ty_python_semantic::PythonVersionSource::Default, - version: python_version, - }, - python_platform: PythonPlatform::default(), - search_paths, + #[cfg(feature = "testing")] + pub fn set_python_version(&mut self, python_version: PythonVersion) { + let program = self.project().program(self); + let settings = ProgramSettings { + python_version: PythonVersionWithSource { + source: ty_python_semantic::PythonVersionSource::Default, + version: python_version, }, - ); - - Ok(()) + python_platform: program.python_platform(self).clone(), + search_paths: program.search_paths(self).clone(), + }; + self.project().update_program(self, settings); } } impl TestDb { - pub fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } - + #[cfg(feature = "testing")] pub fn program_environment(&self) -> ProgramEnvironment<'_> { - ProgramEnvironment::from_program(Program::get(self).resolver_environment(self)) + ProgramEnvironment::from_program(self.project().program(self)) } /// Takes the salsa events. @@ -786,15 +780,15 @@ pub(crate) mod testing { #[salsa::db] impl ty_python_semantic::Db for TestDb { + fn program_file(&self, file: File) -> ProgramFile<'_> { + self.project().program(self).program_file(self, file) + } + #[inline] fn check_file(&self, file: File) -> Vec { crate::check_file(self, file) } - fn program_file(&self, file: File) -> ProgramFile<'_> { - Program::get(self).program_file(self, file) - } - fn rule_selection(&self, _file: ruff_db::files::File) -> &RuleSelection { self.project().rules(self) } @@ -822,10 +816,6 @@ pub(crate) mod testing { #[salsa::db] impl Db for TestDb { - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } - fn project(&self) -> Project { self.project.unwrap() } @@ -845,9 +835,8 @@ mod tests { use ruff_db::files::FileRootKind; use ruff_db::system::{SystemPathBuf, TestSystem}; use ty_module_resolver::list_modules; - use ty_python_core::program::Program; - use crate::{ProjectDatabase, ProjectMetadata}; + use crate::{Db as _, ProjectDatabase, ProjectMetadata}; #[test] fn frozen_inputs_support_a_one_shot_check() -> anyhow::Result<()> { @@ -891,7 +880,7 @@ mod tests { let metadata = ProjectMetadata::discover(&project, &system)?; let db = ProjectDatabase::fallible(metadata, system)?; - let modules = list_modules(&db, Program::get(&db).resolver_environment(&db)); + let modules = list_modules(&db, db.project().program(&db).resolver_environment(&db)); assert!( modules .iter() diff --git a/crates/ty_project/src/db/changes.rs b/crates/ty_project/src/db/changes.rs index c27045e445..4530a9d189 100644 --- a/crates/ty_project/src/db/changes.rs +++ b/crates/ty_project/src/db/changes.rs @@ -8,7 +8,7 @@ use ruff_db::Db as _; use ruff_db::files::{File, Files, system_path_to_file}; use ruff_db::system::{SystemPath, SystemPathBuf}; use rustc_hash::FxHashSet; -use ty_python_core::program::{FallibleStrategy, Program}; +use ty_python_core::program::FallibleStrategy; /// Represents the result of applying changes to the project database. pub struct ChangeResult { @@ -34,7 +34,7 @@ impl ProjectDatabase { let project = self.project(); let project_root = project.root(self).to_path_buf(); let configuration_paths = ConfigurationPaths::from_metadata(project.metadata(self)); - let program = Program::get(self); + let program = self.project().program(self); let custom_stdlib_versions_path = program .custom_stdlib_search_path(self) .map(|path| path.join("VERSIONS")); @@ -253,8 +253,7 @@ impl ProjectDatabase { &FallibleStrategy, ) { Ok((program_settings, diagnostics)) => { - let program = Program::get(self); - program.update_from_settings(self, program_settings); + project.update_program(self, program_settings); diagnostics } Err(error) => { @@ -265,7 +264,7 @@ impl ProjectDatabase { } }; - let (settings, settings_diagnostics) = match merged_options + let (settings, mut settings_diagnostics) = match merged_options .to_settings(self, &FallibleStrategy) { Ok((settings, diagnostics)) => (Some(settings), diagnostics), @@ -276,15 +275,14 @@ impl ProjectDatabase { (None, vec![error.into_diagnostic()]) } }; + settings_diagnostics.extend( + program_settings_diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_diagnostic(self)), + ); tracing::debug!("Reloading project after structural change"); - match project.reload( - self, - metadata, - settings, - settings_diagnostics, - program_settings_diagnostics, - ) { + match project.reload(self, metadata, settings, settings_diagnostics) { ProjectReloadResult::Unchanged => {} ProjectReloadResult::Changed { files_changed } => { result.project_changed = true; @@ -325,17 +323,18 @@ impl ProjectDatabase { &FallibleStrategy, ) { Ok((program_settings, program_settings_diagnostics)) => { - let settings_diagnostics = + let mut settings_diagnostics = match merged_options.to_settings(self, &FallibleStrategy) { Ok((_, diagnostics)) => diagnostics, Err(error) => vec![error.into_diagnostic()], }; - program.update_from_settings(self, program_settings); - project.update_settings_diagnostics( - self, - settings_diagnostics, - program_settings_diagnostics, + project.update_program(self, program_settings); + settings_diagnostics.extend( + program_settings_diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_diagnostic(self)), ); + project.update_settings_diagnostics(self, settings_diagnostics); } Err(error) => { tracing::error!("Failed to resolve program settings: {error}"); diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index b461f64078..45c84dd18a 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -3,7 +3,7 @@ reason = "Prefer System trait methods over std methods in ty crates" )] use crate::glob::{GlobFilterCheckMode, IncludeResult}; -use crate::metadata::options::{OptionDiagnostic, ProgramSettingsDiagnostic}; +use crate::metadata::options::OptionDiagnostic; use crate::parallel::ParallelIteratorExt; use crate::walk::{ProjectFilesFilter, ProjectFilesWalker}; #[cfg(feature = "testing")] @@ -28,6 +28,7 @@ use std::iter::FusedIterator; use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::sync::Arc; use ty_python_core::ProgramFile; +use ty_python_core::program::{Program, ProgramSettings}; pub use ty_python_semantic::Db as SemanticDb; use ty_python_semantic::lint::RuleSelection; @@ -44,7 +45,7 @@ pub mod watch; /// ## How is a project different from a program? /// There are two (related) motivations: /// -/// 1. Program is defined in `ruff_db` and it can't reference the settings types for the linter and formatter +/// 1. Program is defined in `ty_python_core` and it can't reference the settings types for the linter and formatter /// without introducing a cyclic dependency. The project is defined in a higher level crate /// where it can reference these setting types. /// 2. Running `ruff check` with different target versions results in different programs (settings) but @@ -79,6 +80,10 @@ pub struct Project { #[returns(deref)] pub settings: Box, + /// The settings used to construct the Python program for this project. + #[returns(ref)] + pub program_settings: ProgramSettings, + /// The paths that should be included when checking this project. /// /// The default (when this list is empty) is to include all files in the project root @@ -175,34 +180,33 @@ impl ProgressReporter for CollectReporter { #[salsa::tracked] impl Project { /// Create a project from resolved metadata and settings. - /// - /// Program-settings diagnostics are accepted separately so callers do not need to know how to - /// convert and merge them into the stored project settings diagnostics. fn from_metadata( db: &dyn Db, metadata: ProjectMetadata, settings: Settings, + program_settings: ProgramSettings, settings_diagnostics: Vec, - program_settings_diagnostics: Vec, ) -> Self { - let diagnostics = Self::settings_diagnostics_with_program_diagnostics( - db, - settings_diagnostics, - program_settings_diagnostics, - ); + program_settings.search_paths.try_register_static_roots(db); - Project::builder(Box::new(metadata), Box::new(settings), diagnostics) - .durability(Durability::MEDIUM) - .open_fileset_durability(Durability::LOW) - .file_set_durability(Durability::LOW) - .new(db) + Project::builder( + Box::new(metadata), + Box::new(settings), + program_settings, + settings_diagnostics, + ) + .durability(Durability::MEDIUM) + .open_fileset_durability(Durability::LOW) + .file_set_durability(Durability::LOW) + .new(db) } - /// Permanently freezes the most heavily read immutable project inputs. + /// Permanently freezes the most heavily read immutable project and program inputs. /// /// This is intentionally not exhaustive. fn freeze(self, db: &mut dyn Db) { let durability = Durability::NEVER_CHANGE; + let program_settings = self.program_settings(db).clone(); let metadata = Box::new(self.metadata(db).clone()); let settings = Box::new(self.settings(db).clone()); let included_paths = self.included_paths_list(db).to_vec(); @@ -216,6 +220,9 @@ impl Project { self.set_settings(db) .with_durability(durability) .to(settings); + self.set_program_settings(db) + .with_durability(durability) + .to(program_settings); self.set_included_paths_list(db) .with_durability(durability) .to(included_paths); @@ -232,6 +239,18 @@ impl Project { IndexedFiles::freeze(db, self); } + #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] + pub fn program(self, db: &dyn Db) -> Program<'_> { + Program::from_settings(db, self.program_settings(db).clone()) + } + + pub fn update_program(self, db: &mut dyn Db, settings: ProgramSettings) { + if self.program_settings(db) != &settings { + settings.search_paths.try_register_static_roots(db); + self.set_program_settings(db).to(settings); + } + } + pub fn root(self, db: &dyn Db) -> &SystemPath { self.metadata(db).root() } @@ -270,25 +289,15 @@ impl Project { } /// Reload the project after its metadata or settings have changed. - /// - /// Program-settings diagnostics are converted and merged here to keep reload behavior - /// consistent with initial project creation. pub fn reload( self, db: &mut dyn Db, metadata: ProjectMetadata, settings: Option, settings_diagnostics: Vec, - program_settings_diagnostics: Vec, ) -> ProjectReloadResult { tracing::debug!("Reloading project"); let metadata_changed = &metadata != self.metadata(db); - let settings_diagnostics = Self::settings_diagnostics_with_program_diagnostics( - db, - settings_diagnostics, - program_settings_diagnostics, - ); - let root_changed = metadata.root() != self.root(db); let (settings_changed, files_changed) = if let Some(settings) = settings && self.settings(db) != &settings @@ -331,32 +340,12 @@ impl Project { self, db: &mut dyn Db, settings_diagnostics: Vec, - program_settings_diagnostics: Vec, ) { - let settings_diagnostics = Self::settings_diagnostics_with_program_diagnostics( - db, - settings_diagnostics, - program_settings_diagnostics, - ); - if self.settings_diagnostics(db) != settings_diagnostics { self.set_settings_diagnostics(db).to(settings_diagnostics); } } - fn settings_diagnostics_with_program_diagnostics( - db: &dyn Db, - mut settings_diagnostics: Vec, - program_settings_diagnostics: Vec, - ) -> Vec { - settings_diagnostics.extend( - program_settings_diagnostics - .into_iter() - .map(|diagnostic| diagnostic.into_diagnostic(db)), - ); - settings_diagnostics - } - /// Checks the project and its dependencies according to the project's check mode. fn check(self, db: &ProjectDatabase, reporter: &mut dyn ProgressReporter) { let project_span = tracing::debug_span!("Project::check"); @@ -906,7 +895,6 @@ mod tests { fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> { let project = ProjectMetadata::new("test", SystemPathBuf::from("/")); let mut db = TestDb::new(project); - db.init_program().unwrap(); let path = SystemPath::new("test.py"); db.write_file(path, "x = 10")?; diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 000dcfb460..a07a8ce03a 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -613,7 +613,7 @@ pub enum ProgramSettingsDiagnostic { impl ProgramSettingsDiagnostic { /// Convert this program-settings diagnostic into a diagnostic that can be stored on a project. - pub(crate) fn into_diagnostic(self, db: &dyn Db) -> OptionDiagnostic { + pub fn into_diagnostic(self, db: &dyn Db) -> OptionDiagnostic { match self { Self::UnsupportedInferredPythonVersion(python_version) => { unsupported_inferred_python_version_diagnostic(db, &python_version) diff --git a/crates/ty_project/src/watch/project_watcher.rs b/crates/ty_project/src/watch/project_watcher.rs index c4829c4727..4b5f8afef2 100644 --- a/crates/ty_project/src/watch/project_watcher.rs +++ b/crates/ty_project/src/watch/project_watcher.rs @@ -6,7 +6,6 @@ use tracing::info; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_db::system::{SystemPath, SystemPathBuf}; use ty_module_resolver::system_module_search_paths; -use ty_python_core::program::Program; use crate::db::{Db, ProjectDatabase}; use crate::watch::Watcher; @@ -41,7 +40,7 @@ impl ProjectWatcher { } pub fn update(&mut self, db: &ProjectDatabase) { - let environment = Program::get(db).resolver_environment(db); + let environment = db.project().program(db).resolver_environment(db); let search_paths: Vec<_> = system_module_search_paths(db, environment).collect(); let project_path = db.project().root(db); diff --git a/crates/ty_python_core/Cargo.toml b/crates/ty_python_core/Cargo.toml index fad05d5d0e..35238e3ea3 100644 --- a/crates/ty_python_core/Cargo.toml +++ b/crates/ty_python_core/Cargo.toml @@ -48,6 +48,7 @@ anyhow = { workspace = true } [features] serde = ["dep:serde", "dep:ruff_macros"] schemars = ["dep:schemars", "dep:serde_json"] +testing = [] [lints] workspace = true diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index 42501ef7ed..d0b9227631 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -254,6 +254,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> { // Used for checking semantic syntax errors resolver_environment: ResolverEnvironment<'db>, + python_version: PythonVersion, source_text: OnceCell, semantic_checker: SemanticSyntaxChecker, in_try: bool, @@ -345,6 +346,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { enclosing_snapshots: FxHashMap::default(), resolver_environment: file.resolver_environment(db), + python_version: file.python_version(db), source_text: OnceCell::new(), semantic_checker: SemanticSyntaxChecker::default(), in_try: false, @@ -5015,7 +5017,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { } fn python_version(&self) -> PythonVersion { - self.resolver_environment.python_version(self.db) + self.python_version } fn source(&self) -> &str { diff --git a/crates/ty_python_core/src/db.rs b/crates/ty_python_core/src/db.rs index 831360fc94..98bd1ae087 100644 --- a/crates/ty_python_core/src/db.rs +++ b/crates/ty_python_core/src/db.rs @@ -1,6 +1,9 @@ use ruff_db::files::File; use ty_module_resolver::Db as ModuleResolverDb; +#[cfg(any(test, feature = "testing"))] +use crate::program::{Program, ProgramSettings}; + /// Database giving access to semantic information about a Python program. #[salsa::db] pub trait Db: ModuleResolverDb { @@ -8,6 +11,25 @@ pub trait Db: ModuleResolverDb { fn should_check_file(&self, file: File) -> bool; } +#[cfg(any(test, feature = "testing"))] +#[salsa::db] +pub trait TestProgramDb: Db { + fn program_settings(&self) -> &ProgramSettings; + + // Salsa-cached because interning a program requires hashing all search paths. + fn program(&self) -> Program<'_> + where + Self: Sized, + { + #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] + fn program_inner(db: &dyn TestProgramDb) -> Program<'_> { + Program::from_settings(db, db.program_settings().clone()) + } + + program_inner(self) + } +} + #[cfg(test)] pub(crate) mod tests { use std::sync::{Arc, Mutex}; @@ -25,9 +47,9 @@ pub(crate) mod tests { use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; use crate::platform::PythonPlatform; - use crate::program::{Program, ProgramSettings}; + use crate::program::ProgramSettings; - use super::Db; + use super::{Db, TestProgramDb}; type Events = Arc>>; @@ -38,11 +60,14 @@ pub(crate) mod tests { files: Files, system: TestSystem, vendored: VendoredFileSystem, + program_settings: ProgramSettings, } impl TestDb { fn new() -> Self { let events = Events::default(); + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); Self { storage: salsa::Storage::new(Some(Box::new({ move |event| { @@ -52,8 +77,9 @@ pub(crate) mod tests { } }))), system: TestSystem::default(), - vendored: ty_vendored::file_system().clone(), + vendored, files: Files::default(), + program_settings, } } } @@ -93,6 +119,13 @@ pub(crate) mod tests { #[salsa::db] impl ModuleResolverDb for TestDb {} + #[salsa::db] + impl TestProgramDb for TestDb { + fn program_settings(&self) -> &ProgramSettings { + &self.program_settings + } + } + #[salsa::db] impl salsa::Database for TestDb {} @@ -132,19 +165,18 @@ pub(crate) mod tests { db.write_files(self.files) .context("Failed to write test files")?; - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersionWithSource { - version: self.python_version, - source: PythonVersionSource::default(), - }, - python_platform: self.python_platform, - search_paths: SearchPathSettings::new(vec![src_root]) - .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) - .context("Invalid search path settings")?, + let program_settings = ProgramSettings { + python_version: PythonVersionWithSource { + version: self.python_version, + source: PythonVersionSource::default(), }, - ); + python_platform: self.python_platform, + search_paths: SearchPathSettings::new(vec![src_root]) + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .context("Invalid search path settings")?, + }; + program_settings.search_paths.try_register_static_roots(&db); + db.program_settings = program_settings; Ok(db) } diff --git a/crates/ty_python_core/src/lib.rs b/crates/ty_python_core/src/lib.rs index 0d86cf8001..84ed3749a6 100644 --- a/crates/ty_python_core/src/lib.rs +++ b/crates/ty_python_core/src/lib.rs @@ -15,11 +15,7 @@ use ruff_text_size::TextRange; use rustc_hash::{FxHashMap, FxHashSet}; use salsa::plumbing::AsId; use smallvec::SmallVec; -use ty_module_resolver::{ModuleName, ResolverEnvironment}; - -// FIXME: Replace this temporary alias once semantic query keys can use the environment-bearing -// `Program` Salsa ingredient directly. -pub type Program<'db> = ResolverEnvironment<'db>; +use ty_module_resolver::ModuleName; use crate::frozen::{FrozenMap, FrozenSet}; use crate::place::ScopedPlaceId; @@ -64,8 +60,11 @@ pub mod symbol; pub mod unpack; mod use_def; pub use db::Db; +#[cfg(any(test, feature = "testing"))] +pub use db::TestProgramDb; pub mod program; pub mod program_file; +pub use program::Program; pub use program_file::ProgramFile; /// Returns the semantic index for `file`. @@ -1126,7 +1125,7 @@ mod tests { } fn program_file(db: &TestDb, file: File) -> ProgramFile<'_> { - Program::get(db).program_file(db, file) + db.program().program_file(db, file) } fn names(table: &PlaceTable) -> Vec { @@ -1861,13 +1860,14 @@ class C[T]: scopes: impl Iterator, db: &'db dyn Db, file: File, + program: Program<'db>, module: &'a ParsedModuleRef, ) -> Vec<&'a str> { scopes .into_iter() .map(|(scope_id, _)| { scope_id - .to_scope_id(db, Program::get(db).program_file(db, file)) + .to_scope_id(db, program.program_file(db, file)) .name(db, module) }) .collect() @@ -1891,17 +1891,20 @@ def x(): let descendants = index.descendent_scopes(FileScopeId::global()); assert_eq!( - scope_names(descendants, &db, file, &module), + scope_names(descendants, &db, file, db.program(), &module), vec!["Test", "foo", "bar", "baz", "x"] ); let children = index.child_scopes(FileScopeId::global()); - assert_eq!(scope_names(children, &db, file, &module), vec!["Test", "x"]); + assert_eq!( + scope_names(children, &db, file, db.program(), &module), + vec!["Test", "x"] + ); let test_class = index.child_scopes(FileScopeId::global()).next().unwrap().0; let test_child_scopes = index.child_scopes(test_class); assert_eq!( - scope_names(test_child_scopes, &db, file, &module), + scope_names(test_child_scopes, &db, file, db.program(), &module), vec!["foo", "baz"] ); @@ -1913,7 +1916,7 @@ def x(): let ancestors = index.ancestor_scopes(bar_scope); assert_eq!( - scope_names(ancestors, &db, file, &module), + scope_names(ancestors, &db, file, db.program(), &module), vec!["bar", "foo", "Test", ""] ); } diff --git a/crates/ty_python_core/src/platform.rs b/crates/ty_python_core/src/platform.rs index a575baba47..8da3b45817 100644 --- a/crates/ty_python_core/src/platform.rs +++ b/crates/ty_python_core/src/platform.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter}; use ty_combine::Combine; /// The target platform to assume when resolving types. -#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize, ruff_macros::RustDoc), diff --git a/crates/ty_python_core/src/program.rs b/crates/ty_python_core/src/program.rs index 6934f23896..59f5971b0d 100644 --- a/crates/ty_python_core/src/program.rs +++ b/crates/ty_python_core/src/program.rs @@ -2,9 +2,8 @@ use crate::{Db, platform::PythonPlatform}; use ruff_db::files::File; use ruff_db::system::SystemPath; +use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::PythonVersion; -use salsa::Durability; -use salsa::Setter; use ty_module_resolver::{ResolverEnvironment, SearchPaths}; use ty_site_packages::PythonVersionWithSource; @@ -13,110 +12,66 @@ use crate::ProgramFile; // Re-export the misconfiguration strategy types from ty_module_resolver. pub use ty_module_resolver::{FallibleStrategy, MisconfigurationStrategy, UseDefaultStrategy}; -#[salsa::input(singleton, heap_size=ruff_memory_usage::heap_size)] -pub struct Program { +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct Program<'db> { + // FIXME: Move the source out of `Program`. Different source locations prevent otherwise + // equivalent programs from being reused across scripts. #[returns(ref)] pub python_version_with_source: PythonVersionWithSource, #[returns(ref)] pub python_platform: PythonPlatform, - #[returns(ref)] - pub search_paths: SearchPaths, + #[returns(copy)] + pub resolver_environment: ResolverEnvironment<'db>, } -#[salsa::tracked] -impl Program { - pub fn init_or_update(db: &mut dyn Db, settings: ProgramSettings) -> Self { - match Self::try_get(db) { - Some(program) => { - program.update_from_settings(db, settings); - program - } - None => Self::from_settings(db, settings), - } - } +impl get_size2::GetSize for Program<'_> {} - pub fn from_settings(db: &dyn Db, settings: ProgramSettings) -> Self { +impl<'db> Program<'db> { + /// Creates a program from settings whose search roots have already been registered. + pub fn from_settings(db: &'db dyn Db, settings: ProgramSettings) -> Self { let ProgramSettings { python_version, python_platform, search_paths, } = settings; - search_paths.try_register_static_roots(db); - - Program::builder(python_version, python_platform, search_paths) - .durability(Durability::HIGH) - .new(db) + let resolver_environment = + ResolverEnvironment::new(db, python_version.version, &search_paths); + Program::new(db, python_version, python_platform, resolver_environment) } - pub fn python_version(self, db: &dyn Db) -> PythonVersion { - self.python_version_with_source(db).version + pub fn python_version(self, db: &'db dyn Db) -> PythonVersion { + self.resolver_environment(db).python_version(db) } - /// Returns the module-resolution environment for this program. - pub fn resolver_environment(self, db: &dyn Db) -> ResolverEnvironment<'_> { - ResolverEnvironment::new(db, self.python_version(db), self.search_paths(db)) + pub fn search_paths(self, db: &'db dyn Db) -> &'db SearchPaths { + self.resolver_environment(db).search_paths(db) } - pub fn program_file(self, db: &dyn Db, file: File) -> ProgramFile<'_> { - ProgramFile::new(db, file, self.resolver_environment(db)) + pub fn program_file(self, db: &'db dyn Db, file: File) -> ProgramFile<'db> { + ProgramFile::new(db, file, self) } - pub fn update_from_settings(self, db: &mut dyn Db, settings: ProgramSettings) { - let ProgramSettings { - python_version, - python_platform, - search_paths, - } = settings; - - if self.search_paths(db) != &search_paths { - tracing::debug!("Updating search paths"); - search_paths.try_register_static_roots(db); - self.set_search_paths(db).to(search_paths); - } - - if &python_platform != self.python_platform(db) { - tracing::debug!("Updating python platform: `{python_platform:?}`"); - self.set_python_platform(db).to(python_platform); - } - - if &python_version != self.python_version_with_source(db) { - tracing::debug!( - "Updating python version: Python {version}", - version = python_version.version - ); - self.set_python_version_with_source(db).to(python_version); - } - } - - /// Permanently freezes all program inputs. - pub fn freeze(self, db: &mut dyn Db) { - let durability = Durability::NEVER_CHANGE; - let python_version = self.python_version_with_source(db).clone(); - let python_platform = self.python_platform(db).clone(); - let search_paths = self.search_paths(db).clone(); - - self.set_python_version_with_source(db) - .with_durability(durability) - .to(python_version); - self.set_python_platform(db) - .with_durability(durability) - .to(python_platform); - self.set_search_paths(db) - .with_durability(durability) - .to(search_paths); - } - - pub fn custom_stdlib_search_path(self, db: &dyn Db) -> Option<&SystemPath> { + pub fn custom_stdlib_search_path(self, db: &'db dyn Db) -> Option<&'db SystemPath> { self.search_paths(db).custom_stdlib() } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)] pub struct ProgramSettings { pub python_version: PythonVersionWithSource, pub python_platform: PythonPlatform, pub search_paths: SearchPaths, } + +impl ProgramSettings { + pub fn empty(vendored: &VendoredFileSystem) -> Self { + Self { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPaths::empty(vendored), + } + } +} diff --git a/crates/ty_python_core/src/program_file.rs b/crates/ty_python_core/src/program_file.rs index a232e82cf1..bc6548a6d8 100644 --- a/crates/ty_python_core/src/program_file.rs +++ b/crates/ty_python_core/src/program_file.rs @@ -3,7 +3,7 @@ use ruff_db::files::File; use ruff_python_ast::PythonVersion; use ty_module_resolver::{ResolverEnvironment, ResolverFile}; -use crate::{Db, Program}; +use crate::{Db, program::Program}; /// A file interpreted within a particular Python program. /// @@ -54,24 +54,21 @@ use crate::{Db, Program}; heap_size = ruff_memory_usage::heap_size )] pub struct ProgramFile<'db> { - /// The cached parser key for `file` and the environment's Python version. + /// Cache the parser key even though its Python version is redundant with `program`: + /// program files are created infrequently, but their parser keys are looked up extensively. #[returns(copy)] pub python_file: PythonFile<'db>, #[returns(copy)] - pub resolver_environment: ResolverEnvironment<'db>, + pub program: Program<'db>, } impl get_size2::GetSize for ProgramFile<'_> {} impl<'db> ProgramFile<'db> { - pub fn new( - db: &'db dyn Db, - file: File, - resolver_environment: ResolverEnvironment<'db>, - ) -> Self { - let python_file = PythonFile::new(db, file, resolver_environment.python_version(db)); - Self::new_internal(db, python_file, resolver_environment) + pub fn new(db: &'db dyn Db, file: File, program: Program<'db>) -> Self { + let python_file = PythonFile::new(db, file, program.python_version(db)); + Self::new_internal(db, python_file, program) } /// Returns the physical file represented by this program file. @@ -79,18 +76,18 @@ impl<'db> ProgramFile<'db> { self.python_file(db).file(db) } + /// Returns the module-resolution environment for this program file. + pub fn resolver_environment(self, db: &'db dyn Db) -> ResolverEnvironment<'db> { + self.program(db).resolver_environment(db) + } + /// Returns the resolver key for this file. pub fn resolver_file(self, db: &'db dyn Db) -> ResolverFile<'db> { ResolverFile::new(db, self.file(db), self.resolver_environment(db)) } - /// Returns the program associated with this file. - pub fn program(self, db: &'db dyn Db) -> Program<'db> { - self.resolver_environment(db) - } - - /// Returns the Python version associated with this file's resolver environment. + /// Returns the Python version associated with this file's program. pub fn python_version(self, db: &'db dyn Db) -> PythonVersion { - self.resolver_environment(db).python_version(db) + self.program(db).python_version(db) } } diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml index 500d017ca5..ebdbfb5af8 100644 --- a/crates/ty_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -73,7 +73,7 @@ serde = [ "ruff_python_ast/serde", "ty_python_core/serde", ] -testing = [] +testing = ["ty_python_core/testing"] [[test]] name = "mdtest" diff --git a/crates/ty_python_semantic/src/db.rs b/crates/ty_python_semantic/src/db.rs index 1d3044b0b4..305e7323d1 100644 --- a/crates/ty_python_semantic/src/db.rs +++ b/crates/ty_python_semantic/src/db.rs @@ -48,7 +48,8 @@ pub(crate) mod tests { use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::PythonVersion; use ty_module_resolver::{Db as ModuleResolverDb, SearchPathSettings}; - use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; + use ty_python_core::TestProgramDb; + use ty_python_core::program::{FallibleStrategy, ProgramSettings}; use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; type Events = Arc>>; @@ -64,11 +65,14 @@ pub(crate) mod tests { rule_selection: Arc, analysis_settings: Arc, open_files: rustc_hash::FxHashSet, + program_settings: ProgramSettings, } impl TestDb { fn new() -> Self { let events = Events::default(); + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); Self { storage: salsa::Storage::new(Some(Box::new({ let events = events.clone(); @@ -79,21 +83,22 @@ pub(crate) mod tests { } }))), system: TestSystem::default(), - vendored: ty_vendored::file_system().clone(), + vendored, events, files: Files::default(), rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())), analysis_settings: AnalysisSettings::default().into(), open_files: rustc_hash::FxHashSet::default(), + program_settings, } } pub(crate) fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) + self.program().python_version(self) } pub(crate) fn program_environment(&self) -> ProgramEnvironment<'_> { - ProgramEnvironment::from_program(Program::get(self).resolver_environment(self)) + ProgramEnvironment::from_program(self.program()) } /// Marks `file` as open in the editor. @@ -151,6 +156,13 @@ pub(crate) mod tests { } } + #[salsa::db] + impl TestProgramDb for TestDb { + fn program_settings(&self) -> &ProgramSettings { + &self.program_settings + } + } + #[salsa::db] impl Db for TestDb { fn check_file(&self, file: File) -> Vec { @@ -162,7 +174,7 @@ pub(crate) mod tests { } fn program_file(&self, file: File) -> ProgramFile<'_> { - Program::get(self).program_file(self, file) + self.program().program_file(self, file) } fn rule_selection(&self, _file: File) -> &RuleSelection { @@ -242,19 +254,18 @@ pub(crate) mod tests { db.write_files(self.files) .context("Failed to write test files")?; - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersionWithSource { - version: self.python_version, - source: PythonVersionSource::default(), - }, - python_platform: self.python_platform, - search_paths: SearchPathSettings::new(vec![src_root]) - .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) - .context("Invalid search path settings")?, + let program_settings = ProgramSettings { + python_version: PythonVersionWithSource { + version: self.python_version, + source: PythonVersionSource::default(), }, - ); + python_platform: self.python_platform, + search_paths: SearchPathSettings::new(vec![src_root]) + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .context("Invalid search path settings")?, + }; + program_settings.search_paths.try_register_static_roots(&db); + db.program_settings = program_settings; Ok(db) } diff --git a/crates/ty_python_semantic/src/diagnostic/mod.rs b/crates/ty_python_semantic/src/diagnostic/mod.rs index 0fbb0422c9..8ead4596ee 100644 --- a/crates/ty_python_semantic/src/diagnostic/mod.rs +++ b/crates/ty_python_semantic/src/diagnostic/mod.rs @@ -1,6 +1,6 @@ use crate::{ - Db, PythonVersionSource, PythonVersionWithSource, lint::lint_documentation_url, - types::TypeCheckDiagnostics, + Db, ProgramEnvironment, PythonVersionSource, PythonVersionWithSource, + lint::lint_documentation_url, types::TypeCheckDiagnostics, }; use levenshtein::{HideUnderscoredSuggestions, find_best_suggestion}; use ruff_db::{ @@ -46,11 +46,12 @@ pub fn inferred_python_version_source_annotation( /// configuration files, or defaults. pub(crate) fn add_inferred_python_version_hint_to_diagnostic( db: &dyn Db, + env: &ProgramEnvironment, diagnostic: &mut Diagnostic, action: &str, ) { - let program = ty_python_core::program::Program::get(db); - let PythonVersionWithSource { version, source } = program.python_version_with_source(db); + let PythonVersionWithSource { version, source } = + env.program(db).python_version_with_source(db); match source { crate::PythonVersionSource::Cli => { diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 417b52521e..773990d89f 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -204,7 +204,12 @@ pub fn check_file(db: &dyn Db, file: ProgramFile<'_>) -> Result( visibility: BuiltinVisibility, ) -> Option<(ScopeId<'db>, PlaceAndQualifiers<'db>)> { let program = env.program(db); + let resolver_environment = program.resolver_environment(db); let resolver = |module: Module<'db>| { let file = ProgramFile::new(db, module.file(db)?, program); let scope = global_scope(db, file); @@ -695,12 +696,13 @@ fn builtins_symbol_impl<'db>( // If this symbol is not present in project-level builtins, search in the default ones. resolve_module_confident( db, - program, + resolver_environment, &ModuleName::new_static("__builtins__").unwrap(), ) .and_then(&resolver) .or_else(|| { - resolve_module_confident(db, program, &KnownModule::Builtins.name()).and_then(resolver) + resolve_module_confident(db, resolver_environment, &KnownModule::Builtins.name()) + .and_then(resolver) }) } @@ -1424,7 +1426,7 @@ fn symbol_impl<'db>( "version_info" => { return Place::bound(Type::sys_version_info()).into(); } - "platform" => match ty_python_core::program::Program::get(db).python_platform(db) { + "platform" => match scope.program(db).python_platform(db) { crate::PythonPlatform::Identifier(platform) => { return Place::bound(Type::string_literal(db, platform.as_str())).into(); } @@ -1437,7 +1439,7 @@ fn symbol_impl<'db>( } if name == "name" && is_known_module(KnownModule::Os) { - match ty_python_core::program::Program::get(db).python_platform(db) { + match scope.program(db).python_platform(db) { crate::PythonPlatform::Identifier(platform) => { // In CPython, `os.name` is `"nt"` on Windows and `"posix"` otherwise. let os_name = if platform == "win32" { "nt" } else { "posix" }; diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs index 3c408bb588..b9ec5226a6 100644 --- a/crates/ty_python_semantic/src/types/class/known.rs +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -2058,21 +2058,19 @@ impl<'db> KnownClassLookupError<'db> { #[cfg(test)] mod tests { use super::*; - use crate::db::tests::setup_db; + use crate::db::tests::{TestDbBuilder, setup_db}; use crate::{PythonVersionSource, PythonVersionWithSource}; - use salsa::Setter; use strum::IntoEnumIterator; use ty_module_resolver::resolve_module_confident; + use ty_python_core::TestProgramDb as _; + use ty_python_core::program::{Program, ProgramSettings}; #[test] fn known_class_roundtrip_from_str() { - let mut db = setup_db(); - ty_python_core::program::Program::get(&db) - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { - version: PythonVersion::latest_preview(), - source: PythonVersionSource::default(), - }); + let db = TestDbBuilder::new() + .with_python_version(PythonVersion::latest_preview()) + .build() + .expect("valid TestDb setup"); let python_version = db.python_version(); let resolver_environment = db.program_environment().resolver_environment(&db); for class in KnownClass::iter() { @@ -2101,14 +2099,10 @@ mod tests { #[test] fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_latest_version() { - let mut db = setup_db(); - - ty_python_core::program::Program::get(&db) - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { - version: PythonVersion::latest_ty(), - source: PythonVersionSource::default(), - }); + let db = TestDbBuilder::new() + .with_python_version(PythonVersion::latest_ty()) + .build() + .expect("valid TestDb setup"); let python_version = db.python_version(); let env = db.program_environment(); @@ -2134,7 +2128,7 @@ mod tests { #[test] fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_low_python_version() { - let mut db = setup_db(); + let db = setup_db(); // First, collect the `KnownClass` variants // and sort them according to the version they were added in. @@ -2164,22 +2158,27 @@ mod tests { classes.sort_unstable_by_key(|(_, version)| *version); - let program = ty_python_core::program::Program::get(&db); + let mut program = db.program(); let mut current_version = program.python_version(&db); + let python_platform = program.python_platform(&db).clone(); + let search_paths = program.search_paths(&db).clone(); for (class, version_added) in classes { if version_added != current_version { - program - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { + let settings = ProgramSettings { + python_version: PythonVersionWithSource { version: version_added, source: PythonVersionSource::default(), - }); + }, + python_platform: python_platform.clone(), + search_paths: search_paths.clone(), + }; + program = Program::from_settings(&db, settings); current_version = version_added; } // Check the class can be looked up successfully - let env = db.program_environment(); + let env = ProgramEnvironment::from_program(program); class.try_to_class_literal(&db, &env).unwrap(); // We can't call `KnownClass::Tuple.to_instance()`; diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 8170157912..c47d4297f6 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -74,7 +74,7 @@ impl<'db> ProgramEnvironment<'db> { #[inline] pub fn program(&self, db: &'db dyn Db) -> Program<'db> { let program = match self.environment.get() { - ProgramSource::Program(id) => return ResolverEnvironment::from_id(id), + ProgramSource::Program(id) => return Program::from_id(id), ProgramSource::File(file) => { cold_path(); // The source handle and database share `'db`; re-wrapping the stored ingredient @@ -109,7 +109,7 @@ impl<'db> ProgramEnvironment<'db> { /// Returns the resolver environment used by this operation. #[inline] pub fn resolver_environment(&self, db: &'db dyn Db) -> ResolverEnvironment<'db> { - self.program(db) + self.program(db).resolver_environment(db) } } diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index ad80014d2a..a9929e669e 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -4689,6 +4689,7 @@ pub(super) fn report_invalid_total_ordering_call( /// The function returns `true` if a hint was added, `false` otherwise. pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( db: &dyn Db, + env: &ProgramEnvironment<'_>, diagnostic: &mut Diagnostic, full_submodule_name: &ModuleName, parent_module: Module, @@ -4701,7 +4702,7 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( return false; } - let program = ty_python_core::program::Program::get(db); + let program = env.program(db); let typeshed_versions = program.search_paths(db).typeshed_versions(); let Some(version_range) = typeshed_versions.exact(full_submodule_name) else { @@ -4721,7 +4722,7 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( version_range = version_range.diagnostic_display(), )); - add_inferred_python_version_hint_to_diagnostic(db, diagnostic, "resolving modules"); + add_inferred_python_version_hint_to_diagnostic(db, env, diagnostic, "resolving modules"); true } @@ -4778,7 +4779,7 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( // TODO: determine what version they need to be on // TODO: also mention the platform we're assuming // TODO: determine what platform they need to be on - add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, action); + add_inferred_python_version_hint_to_diagnostic(db, env, &mut diagnostic, action); } pub(super) fn report_invalid_concatenate_last_arg<'db>( diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b27e76a486..acad198bc3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -10162,7 +10162,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "`{id}` was added as a builtin in Python 3.{version_added_to_builtins}" )); add_inferred_python_version_hint_to_diagnostic( - self.db(), + db, + self.program_environment(), &mut diagnostic, "resolving types", ); diff --git a/crates/ty_python_semantic/src/types/infer/builder/imports.rs b/crates/ty_python_semantic/src/types/infer/builder/imports.rs index de63f9295f..78dab4f12e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/imports.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/imports.rs @@ -86,6 +86,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); add_inferred_python_version_hint_to_diagnostic( db, + self.program_environment(), &mut diagnostic, "resolving modules", ); @@ -514,6 +515,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(full_submodule_name) = full_submodule_name { submodule_hint_added = hint_if_stdlib_submodule_exists_on_other_versions( db, + self.program_environment(), &mut diagnostic, &full_submodule_name, module, @@ -635,7 +637,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); hint_if_stdlib_submodule_exists_on_other_versions( - db, + self.db(), + self.program_environment(), &mut diagnostic, &full_submodule_name, module, diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 778b7cf259..0ed17b16e5 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -355,7 +355,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.type_expression_context() )); add_inferred_python_version_hint_to_diagnostic( - self.db(), + db, + self.program_environment(), &mut diagnostic, "inferring types", ); diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index cbea2b2190..0317728238 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -8,11 +8,14 @@ use ruff_db::files::{File, system_path_to_file}; use ruff_db::system::DbWithWritableSystem as _; use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run}; use ruff_python_ast::PythonVersion; -use ty_module_resolver::ResolverEnvironment; +use salsa::plumbing::AsId; use ty_python_core::definition::Definition; -use ty_python_core::program::Program as ProjectProgram; +use ty_python_core::program::{Program, ProgramSettings}; use ty_python_core::scope::FileScopeId; -use ty_python_core::{ProgramFile, global_scope, place_table, semantic_index, use_def_map}; +use ty_python_core::{ + ProgramFile, TestProgramDb as _, global_scope, place_table, semantic_index, use_def_map, +}; +use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; use super::*; @@ -111,16 +114,38 @@ fn same_file_at_different_python_versions() -> anyhow::Result<()> { db.write_dedented("src/py312_dependency.py", "value: int = 312")?; let file = system_path_to_file(&db, "src/main.py").expect("file to exist"); - let search_paths = ProjectProgram::get(&db).search_paths(&db); + let default_program = db.program(); + let search_paths = default_program.search_paths(&db).clone(); + let python_platform = default_program.python_platform(&db).clone(); let py311 = ProgramFile::new( &db, file, - ResolverEnvironment::new(&db, PythonVersion::PY311, search_paths), + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource { + version: PythonVersion::PY311, + source: PythonVersionSource::Default, + }, + python_platform: python_platform.clone(), + search_paths: search_paths.clone(), + }, + ), ); let py312 = ProgramFile::new( &db, file, - ResolverEnvironment::new(&db, PythonVersion::PY312, search_paths), + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource { + version: PythonVersion::PY312, + source: PythonVersionSource::Default, + }, + python_platform, + search_paths, + }, + ), ); let check = |file, expected_type, expect_invalid_syntax, expect_unresolved_import| { @@ -161,6 +186,52 @@ fn same_file_at_different_python_versions() -> anyhow::Result<()> { Ok(()) } +#[test] +fn program_file_changes_with_python_version() -> anyhow::Result<()> { + let db = TestDbBuilder::new() + .with_python_version(PythonVersion::PY311) + .with_file("src/main.py", "type Alias = int") + .build()?; + let file = system_path_to_file(&db, "src/main.py").expect("file to exist"); + let program = db.program(); + let (program_file_id, py311) = { + let program_file = program.program_file(&db, file); + (program_file.as_id(), program_file.python_file(&db).as_id()) + }; + + let equivalent_program = Program::from_settings( + &db, + ProgramSettings { + python_version: program.python_version_with_source(&db).clone(), + python_platform: program.python_platform(&db).clone(), + search_paths: program.search_paths(&db).clone(), + }, + ); + assert_eq!(program, equivalent_program); + assert_eq!( + program_file_id, + equivalent_program.program_file(&db, file).as_id() + ); + + let py312_program = Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource { + version: PythonVersion::PY312, + source: PythonVersionSource::Default, + }, + python_platform: program.python_platform(&db).clone(), + search_paths: program.search_paths(&db).clone(), + }, + ); + + let program_file = py312_program.program_file(&db, file); + assert_ne!(program_file_id, program_file.as_id()); + assert_eq!(program_file.python_version(&db), PythonVersion::PY312); + assert_ne!(py311, program_file.python_file(&db).as_id()); + Ok(()) +} + #[test] fn expected_types_are_collected_only_for_open_files() -> anyhow::Result<()> { let has_expected_type = |open_file: bool| -> anyhow::Result { diff --git a/crates/ty_python_semantic/src/types/tests.rs b/crates/ty_python_semantic/src/types/tests.rs index 8fafadcf43..df9c2a5e84 100644 --- a/crates/ty_python_semantic/src/types/tests.rs +++ b/crates/ty_python_semantic/src/types/tests.rs @@ -1,14 +1,14 @@ use super::*; -use crate::Db; -use crate::ProgramEnvironment; use crate::db::tests::{TestDbBuilder, setup_db}; use crate::place::{typing_extensions_symbol, typing_symbol}; use crate::types::type_alias::PEP695TypeAliasType; +use crate::{Db, ProgramEnvironment}; use ruff_db::system::DbWithWritableSystem as _; use ruff_python_ast as ast; use ruff_python_ast::PythonVersion; use test_case::test_case; -use ty_python_core::ProgramFile; +use ty_python_core::program::Program; +use ty_python_core::{ProgramFile, TestProgramDb as _}; /// Explicitly test for Python version <3.13 and >=3.13, to ensure that /// the fallback to `typing_extensions` is working correctly. @@ -73,23 +73,20 @@ fn oscillating_generic_alias_cycle_recover<'db>( cycle: &salsa::Cycle, previous: &Type<'db>, current: Type<'db>, + program: Program<'db>, ) -> Type<'db> { - let env = ProgramEnvironment::from_program( - ty_python_core::program::Program::get(db).resolver_environment(db), - ); + let env = ProgramEnvironment::from_program(program); current.cycle_normalized(db, &env, *previous, cycle) } #[salsa::tracked( returns(copy), - cycle_initial=|_, id| Type::divergent(id), + cycle_initial=|_, id, _| Type::divergent(id), cycle_fn=oscillating_generic_alias_cycle_recover, )] -fn oscillating_generic_alias(db: &dyn Db) -> Type<'_> { - let env = ProgramEnvironment::from_program( - ty_python_core::program::Program::get(db).resolver_environment(db), - ); - let previous = oscillating_generic_alias(db); +fn oscillating_generic_alias<'db>(db: &'db dyn Db, program: Program<'db>) -> Type<'db> { + let env = ProgramEnvironment::from_program(program); + let previous = oscillating_generic_alias(db, program); let argument = if let Type::GenericAlias(alias) = previous && alias.specialization(db).types(db) == [Type::unknown()] { @@ -104,7 +101,7 @@ fn oscillating_generic_alias(db: &dyn Db) -> Type<'_> { #[test] fn generic_alias_cycle_recovery_normalizes_same_origin_unknown_oscillation() { let db = setup_db(); - let Type::GenericAlias(alias) = oscillating_generic_alias(&db) else { + let Type::GenericAlias(alias) = oscillating_generic_alias(&db, db.program()) else { panic!("cycle recovery should preserve the generic alias"); }; diff --git a/crates/ty_python_semantic/tests/corpus.rs b/crates/ty_python_semantic/tests/corpus.rs index 3c3575c61a..1c7db3c6b6 100644 --- a/crates/ty_python_semantic/tests/corpus.rs +++ b/crates/ty_python_semantic/tests/corpus.rs @@ -1,23 +1,18 @@ use std::sync::Arc; use anyhow::{Context, anyhow}; -use ruff_db::Db; use ruff_db::files::{File, Files, system_path_to_file}; use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem}; use ruff_db::vendored::VendoredFileSystem; -use ruff_python_ast::PythonVersion; -use ty_module_resolver::SearchPathSettings; -use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; +use ty_python_core::program::ProgramSettings; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::pull_types::pull_types; use ty_python_semantic::{AnalysisSettings, Db as _, check_file_unwrap, default_lint_registry}; -use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; use ruff_db::diagnostic::Diagnostic; use test_case::test_case; -use ty_python_core::{Db as _, ProgramFile}; +use ty_python_core::{Db as _, ProgramFile, TestProgramDb}; fn get_cargo_workspace_root() -> anyhow::Result<&'static SystemPath> { SystemPath::new(env!("CARGO_MANIFEST_DIR")) @@ -149,35 +144,23 @@ pub struct CorpusDb { system: TestSystem, vendored: VendoredFileSystem, analysis_settings: Arc, + program_settings: ProgramSettings, } impl CorpusDb { #[expect(clippy::new_without_default)] pub fn new() -> Self { - let db = Self { + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); + Self { storage: salsa::Storage::new(None), system: TestSystem::default(), - vendored: ty_vendored::file_system().clone(), + vendored, rule_selection: RuleSelection::from_registry(default_lint_registry()), files: Files::default(), analysis_settings: Arc::new(AnalysisSettings::default()), - }; - - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersionWithSource { - version: PythonVersion::latest_ty(), - source: PythonVersionSource::default(), - }, - python_platform: PythonPlatform::default(), - search_paths: SearchPathSettings::new(vec![]) - .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) - .unwrap(), - }, - ); - - db + program_settings, + } } } @@ -216,6 +199,13 @@ impl ty_python_core::Db for CorpusDb { } } +#[salsa::db] +impl TestProgramDb for CorpusDb { + fn program_settings(&self) -> &ProgramSettings { + &self.program_settings + } +} + #[salsa::db] impl ty_python_semantic::Db for CorpusDb { fn check_file(&self, file: File) -> Vec { @@ -227,7 +217,7 @@ impl ty_python_semantic::Db for CorpusDb { } fn program_file(&self, file: File) -> ProgramFile<'_> { - Program::get(self).program_file(self, file) + self.program().program_file(self, file) } fn rule_selection(&self, _file: File) -> &RuleSelection { diff --git a/crates/ty_server/src/server/api/requests/execute_command.rs b/crates/ty_server/src/server/api/requests/execute_command.rs index 18f4f96f1f..ed0c2234c6 100644 --- a/crates/ty_server/src/server/api/requests/execute_command.rs +++ b/crates/ty_server/src/server/api/requests/execute_command.rs @@ -12,7 +12,6 @@ use std::fmt::{self, Write}; use std::str::FromStr; use ty_module_resolver::ModuleResolveMode; use ty_project::Db as _; -use ty_python_core::program::Program; pub(crate) struct ExecuteCommand; @@ -67,7 +66,7 @@ fn debug_information(session: &Session) -> crate::Result { for db in session.project_dbs() { writeln!(buffer, "Project at {}", db.project().root(db))?; - let program = Program::get(db); + let program = db.project().program(db); writeln!(buffer, "Program:")?; writeln!( buffer, diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 017b0a3edf..808e9ecc0d 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -27,7 +27,7 @@ use ty_project::watch::{ChangeEvent, CreatedKind}; use ty_project::{ChangeResult, Db as _, ProjectDatabase, ProjectMetadata}; use index::DocumentError; -use ty_python_core::program::{Program, UseDefaultStrategy}; +use ty_python_core::program::UseDefaultStrategy; pub(crate) use self::options::InitializationOptions; pub use self::options::{ClientOptions, DiagnosticMode, GlobalOptions, WorkspaceOptions}; @@ -1089,7 +1089,7 @@ impl Session { .flat_map(|db| { ty_module_resolver::system_module_search_paths( db, - Program::get(db).resolver_environment(db), + db.project().program(db).resolver_environment(db), ) .map(move |path| (db, path)) }) diff --git a/crates/ty_server/tests/e2e/commands.rs b/crates/ty_server/tests/e2e/commands.rs index 5725f6be6b..4f641562f3 100644 --- a/crates/ty_server/tests/e2e/commands.rs +++ b/crates/ty_server/tests/e2e/commands.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use lsp_types::{ExecuteCommandParams, ExecuteCommandRequest, WorkDoneProgressParams}; use ruff_db::system::SystemPath; @@ -49,6 +49,21 @@ python-platform = \"linux\" .as_str() .expect("debug command to return a string response"); + let (before_structs, salsa_structs) = response + .split_once("=======SALSA STRUCTS=======\n") + .context("debug response missing Salsa structs section")?; + let (salsa_structs, after_structs) = salsa_structs + .split_once("=======SALSA QUERIES=======\n") + .context("debug response missing Salsa queries section")?; + + // The production report orders structs by memory usage, which varies between platforms. + let mut salsa_structs = salsa_structs.lines().collect::>(); + salsa_structs.sort_unstable(); + let response = format!( + "{before_structs}=======SALSA STRUCTS=======\n{}\n=======SALSA QUERIES=======\n{after_structs}", + salsa_structs.join("\n") + ); + let mut settings = insta::Settings::clone_current(); settings.add_filter(r"\b[0-9]+.[0-9]+MB\b", "[X.XXMB]"); settings.add_filter(r"Workspace .+\)", "Workspace XXX"); diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index b93f2b699c..45c48b2f97 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -180,14 +180,16 @@ Settings: Settings { Memory report: =======SALSA STRUCTS======= -`Program` metadata=[X.XXMB] fields=[X.XXMB] count=1 -`ResolverEnvironment` metadata=[X.XXMB] fields=[X.XXMB] count=1 -`Project` metadata=[X.XXMB] fields=[X.XXMB] count=1 `FileRoot` metadata=[X.XXMB] fields=[X.XXMB] count=1 `ModuleResolveModeIngredient` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`Program` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`Project` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`ResolverEnvironment` metadata=[X.XXMB] fields=[X.XXMB] count=1 =======SALSA QUERIES======= `dynamic_resolution_paths -> alloc::vec::Vec` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`Project::program_ -> ty_python_core::program::Program<'_>` + metadata=[X.XXMB] fields=[X.XXMB] count=1 =======SALSA SUMMARY======= TOTAL MEMORY USAGE: [X.XXMB] struct metadata = [X.XXMB] diff --git a/crates/ty_site_packages/src/version.rs b/crates/ty_site_packages/src/version.rs index 8a643a3e82..6832650350 100644 --- a/crates/ty_site_packages/src/version.rs +++ b/crates/ty_site_packages/src/version.rs @@ -10,7 +10,7 @@ use ruff_python_ast::PythonVersion; use ruff_text_size::TextRange; /// The source of the Python version. -#[derive(Clone, Debug, Eq, PartialEq, Default, get_size2::GetSize)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Default, get_size2::GetSize)] pub enum PythonVersionSource { /// Value loaded from a project's configuration file. ConfigFile(PythonVersionFileSource), @@ -49,7 +49,7 @@ pub enum PythonVersionSource { /// Information regarding the file and [`TextRange`] of the configuration /// from which we inferred the Python version. -#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, get_size2::GetSize)] pub struct PythonVersionFileSource { path: Arc, range: Option, @@ -72,7 +72,7 @@ impl PythonVersionFileSource { } /// A Python version with its source. -#[derive(Eq, PartialEq, Debug, Clone, get_size2::GetSize)] +#[derive(Eq, PartialEq, Hash, Debug, Clone, get_size2::GetSize)] pub struct PythonVersionWithSource { pub version: PythonVersion, pub source: PythonVersionSource, diff --git a/crates/ty_test/src/db.rs b/crates/ty_test/src/db.rs index af2cf8255b..51995c81f2 100644 --- a/crates/ty_test/src/db.rs +++ b/crates/ty_test/src/db.rs @@ -15,8 +15,8 @@ use std::borrow::Cow; use std::sync::Arc; use tempfile::TempDir; use ty_module_resolver::ModuleGlobSetBuilder; -use ty_python_core::program::Program; -use ty_python_core::{Db as _, ProgramFile}; +use ty_python_core::program::ProgramSettings; +use ty_python_core::{Db as _, ProgramFile, TestProgramDb}; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::{ AnalysisSettings, Db as SemanticDb, check_file_unwrap, default_lint_registry, @@ -34,6 +34,8 @@ pub(crate) struct Db { impl Db { pub(crate) fn setup() -> Self { + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); let mut db = Self { system: MdtestSystem::in_memory(), storage: salsa::Storage::new(Some(Box::new({ @@ -41,12 +43,12 @@ impl Db { tracing::trace!("event: {:?}", event); } }))), - vendored: ty_vendored::file_system().clone(), + vendored, files: Files::default(), settings: None, }; - db.settings = Some(Settings::new(&db)); + db.settings = Some(Settings::new(&db, program_settings)); db } @@ -54,6 +56,14 @@ impl Db { self.settings.unwrap() } + pub(crate) fn update_program(&mut self, settings: ProgramSettings) { + let db_settings = self.settings(); + if db_settings.program(self) != &settings { + settings.search_paths.try_register_static_roots(self); + db_settings.set_program(self).to(settings); + } + } + pub(crate) fn set_verbosity(&mut self, verbose: bool) { self.settings().set_verbose(self).to(verbose); } @@ -133,7 +143,7 @@ impl SemanticDb for Db { } fn program_file(&self, file: File) -> ProgramFile<'_> { - Program::get(self).program_file(self, file) + self.program().program_file(self, file) } fn rule_selection(&self, file: File) -> &RuleSelection { @@ -161,6 +171,13 @@ impl SemanticDb for Db { } } +#[salsa::db] +impl TestProgramDb for Db { + fn program_settings(&self) -> &ProgramSettings { + self.settings().program(self) + } +} + #[salsa::db] impl salsa::Database for Db {} @@ -214,6 +231,8 @@ impl FileSettings { #[salsa::input(debug)] struct Settings { + #[returns(ref)] + program: ProgramSettings, #[default] #[returns(ref)] analysis: AnalysisSettings, diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index 65a6875dc4..7e9c793191 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -20,8 +20,9 @@ use std::fmt::Write; use ty_module_resolver::{ Module, SearchPath, SearchPathSettings, list_modules, resolve_module_confident, }; +use ty_python_core::TestProgramDb as _; use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; +use ty_python_core::program::{FallibleStrategy, ProgramSettings}; use ty_python_semantic::pull_types::pull_types; use ty_python_semantic::types::UNDEFINED_REVEAL; use ty_python_semantic::{ @@ -304,7 +305,7 @@ fn run_test( .expect("Failed to resolve search path settings"), }; - Program::init_or_update(db, settings); + db.update_program(settings); db.update_analysis_options(configuration.analysis.as_ref()); db.update_mdtest_rule_selection(configuration.rules.as_ref(), options.default_error_rule); db.set_verbosity(test.configuration().verbose()); @@ -493,7 +494,7 @@ struct ModuleInconsistency<'db> { /// `list_module`. fn run_module_resolution_consistency_test(db: &db::Db) -> Result<(), Vec>> { let mut errs = vec![]; - let environment = Program::get(db).resolver_environment(db); + let environment = db.program().resolver_environment(db); for from_list in list_modules(db, environment).iter().copied() { // TODO: For now list_modules does not partake in desperate module resolution so // only compare against confident module resolution. diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index baeb5ea85f..19d61f6f7c 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -2,7 +2,6 @@ use std::any::Any; use js_sys::{Error, JsString}; use ruff_db::Db as _; -use ruff_db::PythonFile; use ruff_db::diagnostic::{self, DisplayDiagnosticConfig}; use ruff_db::files::{File, FilePath, FileRange, system_path_to_file, vendored_path_to_file}; use ruff_db::source::{SourceText, line_index, source_text}; @@ -28,7 +27,7 @@ use ty_project::metadata::options::Options; use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; use ty_project::{CheckMode, ProjectMetadata}; use ty_project::{Db, ProjectDatabase, SemanticDb as _}; -use ty_python_core::program::{FallibleStrategy, Program}; +use ty_python_core::program::FallibleStrategy; use ty_python_semantic::ProgramEnvironment; use wasm_bindgen::prelude::*; @@ -171,20 +170,23 @@ impl Workspace { let (program_settings, program_settings_diagnostics) = merged_options .to_program_settings(&self.system, self.db.vendored(), &FallibleStrategy) .map_err(into_error)?; - Program::get(&self.db).update_from_settings(&mut self.db, program_settings); + self.db + .project() + .update_program(&mut self.db, program_settings); - let (settings, settings_diagnostics) = merged_options + let (settings, mut settings_diagnostics) = merged_options .to_settings(&self.db, &FallibleStrategy) .map_err(into_error)?; - - self.db.project().reload( - &mut self.db, - project, - Some(settings), - settings_diagnostics, - program_settings_diagnostics, + settings_diagnostics.extend( + program_settings_diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_diagnostic(&self.db)), ); + self.db + .project() + .reload(&mut self.db, project, Some(settings), settings_diagnostics); + Ok(()) } @@ -294,7 +296,7 @@ impl Workspace { pub fn parsed(&self, file_id: &FileHandle) -> Result { let parsed = ruff_db::parsed::parsed_module( &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), + self.db.program_file(file_id.file).python_file(&self.db), ) .load(&self.db); @@ -309,7 +311,7 @@ impl Workspace { pub fn tokens(&self, file_id: &FileHandle) -> Result { let parsed = ruff_db::parsed::parsed_module( &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), + self.db.program_file(file_id.file).python_file(&self.db), ) .load(&self.db); diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index a7e92f5efc..9767b80f2d 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -115,7 +115,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -131,7 +131,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn", + "syn 2.0.119", ] [[package]] @@ -287,7 +287,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -422,7 +422,7 @@ dependencies = [ "defmt-parser", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -442,7 +442,7 @@ checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -453,7 +453,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -558,7 +558,7 @@ checksum = "c736d226c32e496b8377813b52269e11ad3a48d8373b68862d0364f04fd1229d" dependencies = [ "attribute-derive", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -816,7 +816,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -873,7 +873,7 @@ checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -940,7 +940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0903173ea316c34a44d0497161e04d9210af44f5f5e89bf2f55d9a254c9a0e8d" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -982,7 +982,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1093,21 +1093,18 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "path-absolutize" -version = "3.1.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" +checksum = "f808742975794703469f67a28dd14b1d1009a1743c18b0353b4b951dbb0068ad" dependencies = [ "path-dedot", ] [[package]] name = "path-dedot" -version = "3.1.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" -dependencies = [ - "once_cell", -] +checksum = "03351d0f1c066c114015408dc6a3e101f080fc03ef9d8d799aa58ec760cac5a6" [[package]] name = "path-slash" @@ -1371,7 +1368,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1513,7 +1510,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.5" +version = "0.0.7" dependencies = [ "anstyle", "memchr", @@ -1522,7 +1519,7 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.5" +version = "0.0.7" dependencies = [ "char_str", "filetime", @@ -1535,7 +1532,7 @@ dependencies = [ [[package]] name = "ruff_db" -version = "0.0.5" +version = "0.0.7" dependencies = [ "anstyle", "arc-swap", @@ -1573,7 +1570,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.5" +version = "0.0.7" dependencies = [ "get-size2", "is-macro", @@ -1583,7 +1580,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.5" +version = "0.0.7" dependencies = [ "drop_bomb", "ruff_cache", @@ -1598,7 +1595,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.5" +version = "0.0.7" dependencies = [ "get-size2", "ruff_macros", @@ -1607,7 +1604,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.15.22" +version = "0.16.1" dependencies = [ "aho-corasick", "anyhow", @@ -1665,7 +1662,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.5" +version = "0.0.7" dependencies = [ "heck", "itertools 0.15.0", @@ -1673,19 +1670,19 @@ dependencies = [ "quote", "regex", "ruff_python_trivia", - "syn", + "syn 3.0.3", ] [[package]] name = "ruff_memory_usage" -version = "0.0.5" +version = "0.0.7" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.5" +version = "0.0.7" dependencies = [ "anyhow", "rand 0.10.2", @@ -1700,7 +1697,7 @@ dependencies = [ [[package]] name = "ruff_python_ast" -version = "0.0.5" +version = "0.0.7" dependencies = [ "aho-corasick", "arrayvec", @@ -1724,7 +1721,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.5" +version = "0.0.7" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -1735,7 +1732,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.5" +version = "0.0.7" dependencies = [ "anyhow", "clap", @@ -1763,7 +1760,7 @@ dependencies = [ [[package]] name = "ruff_python_importer" -version = "0.0.5" +version = "0.0.7" dependencies = [ "anyhow", "ruff_diagnostics", @@ -1776,7 +1773,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.5" +version = "0.0.7" dependencies = [ "ruff_python_ast", "ruff_python_trivia", @@ -1786,7 +1783,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.5" +version = "0.0.7" dependencies = [ "bitflags 2.13.0", "icu_properties", @@ -1796,7 +1793,7 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.5" +version = "0.0.7" dependencies = [ "bitflags 2.13.0", "bstr", @@ -1816,7 +1813,7 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.5" +version = "0.0.7" dependencies = [ "bitflags 2.13.0", "is-macro", @@ -1833,7 +1830,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.5" +version = "0.0.7" dependencies = [ "bitflags 2.13.0", "unicode-ident", @@ -1841,7 +1838,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" -version = "0.0.5" +version = "0.0.7" dependencies = [ "itertools 0.15.0", "ruff_source_file", @@ -1852,7 +1849,7 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.5" +version = "0.0.7" dependencies = [ "ruff_db", "ruff_text_size", @@ -1862,7 +1859,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.5" +version = "0.0.7" dependencies = [ "get-size2", "memchr", @@ -1872,7 +1869,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.5" +version = "0.0.7" dependencies = [ "get-size2", "serde", @@ -1947,7 +1944,7 @@ checksum = "ec5c48c5a4a53a6e2be9762f56566f1325d4394c011c00b3ea86ba2d13411e71" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1998,7 +1995,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2101,7 +2098,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2121,6 +2118,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2129,7 +2137,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2173,7 +2181,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2184,7 +2192,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2294,7 +2302,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2308,7 +2316,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.5" +version = "0.0.7" dependencies = [ "ordermap", "ruff_db", @@ -2318,12 +2326,13 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.5" +version = "0.0.7" dependencies = [ "anyhow", "camino", "compact_str", "get-size2", + "ordermap", "regex", "regex-syntax", "ruff_db", @@ -2340,7 +2349,7 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.5" +version = "0.0.7" dependencies = [ "bitflags 2.13.0", "bitvec", @@ -2368,7 +2377,7 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.5" +version = "0.0.7" dependencies = [ "bitflags 2.13.0", "char_str", @@ -2408,7 +2417,7 @@ dependencies = [ [[package]] name = "ty_site_packages" -version = "0.0.5" +version = "0.0.7" dependencies = [ "camino", "colored", @@ -2428,14 +2437,14 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.5" +version = "0.0.7" dependencies = [ "ruff_macros", ] [[package]] name = "ty_vendored" -version = "0.0.5" +version = "0.0.7" dependencies = [ "path-slash", "ruff_db", @@ -2618,7 +2627,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -2711,7 +2720,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -2732,7 +2741,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2752,7 +2761,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -2786,7 +2795,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 14cef7c28c..3a41d55495 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -26,7 +26,7 @@ ruff_python_formatter = { path = "../crates/ruff_python_formatter" } ruff_text_size = { path = "../crates/ruff_text_size" } ty_module_resolver = { path = "../crates/ty_module_resolver" } -ty_python_semantic = { path = "../crates/ty_python_semantic" } +ty_python_semantic = { path = "../crates/ty_python_semantic", features = ["testing"] } ty_vendored = { path = "../crates/ty_vendored" } ty_python_core = { path = "../crates/ty_python_core" } diff --git a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs index cbe62a7a19..6141d7c74a 100644 --- a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs +++ b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs @@ -17,8 +17,8 @@ use ruff_db::vendored::VendoredFileSystem; use ruff_python_parser::{Mode, ParseOptions, parse_unchecked}; use ty_module_resolver::{Db as ModuleResolverDb, SearchPathSettings}; use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; -use ty_python_core::{Db as _, ProgramFile}; +use ty_python_core::program::{FallibleStrategy, ProgramSettings}; +use ty_python_core::{Db as _, ProgramFile, TestProgramDb}; use ty_python_semantic::lint::LintRegistry; use ty_python_semantic::types::check_types; use ty_python_semantic::{ @@ -38,22 +38,43 @@ struct TestDb { vendored: VendoredFileSystem, rule_selection: Arc, analysis_settings: Arc, + program_settings: ProgramSettings, } impl TestDb { fn new() -> Self { - Self { + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); + let mut db = Self { storage: salsa::Storage::new(Some(Box::new({ move |event| { tracing::trace!("event: {:?}", event); } }))), system: TestSystem::default(), - vendored: ty_vendored::file_system().clone(), + vendored, files: Files::default(), rule_selection: RuleSelection::from_registry(default_lint_registry()).into(), analysis_settings: AnalysisSettings::default().into(), - } + program_settings, + }; + + let src_root = SystemPathBuf::from("/src"); + db.memory_file_system() + .create_directory_all(&src_root) + .unwrap(); + + let program_settings = ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPathSettings::new(vec![src_root]) + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .expect("Valid search path settings"), + }; + program_settings.search_paths.try_register_static_roots(&db); + db.program_settings = program_settings; + + db } } @@ -103,7 +124,7 @@ impl SemanticDb for TestDb { } fn program_file(&self, file: File) -> ProgramFile<'_> { - Program::get(self).program_file(self, file) + self.program().program_file(self, file) } fn rule_selection(&self, _file: File) -> &RuleSelection { @@ -132,30 +153,15 @@ impl SemanticDb for TestDb { } #[salsa::db] -impl salsa::Database for TestDb {} - -fn setup_db() -> TestDb { - let db = TestDb::new(); - - let src_root = SystemPathBuf::from("/src"); - db.memory_file_system() - .create_directory_all(&src_root) - .unwrap(); - - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersionWithSource::default(), - python_platform: PythonPlatform::default(), - search_paths: SearchPathSettings::new(vec![src_root]) - .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) - .expect("Valid search path settings"), - }, - ); - - db +impl TestProgramDb for TestDb { + fn program_settings(&self) -> &ProgramSettings { + &self.program_settings + } } +#[salsa::db] +impl salsa::Database for TestDb {} + static TEST_DB: OnceLock> = OnceLock::new(); fn do_fuzz(case: &[u8]) -> Corpus { @@ -169,7 +175,7 @@ fn do_fuzz(case: &[u8]) -> Corpus { } let mut db = TEST_DB - .get_or_init(|| Mutex::new(setup_db())) + .get_or_init(|| Mutex::new(TestDb::new())) .lock() .unwrap(); From e87ddc00ead5b14550bf22c3bfc8b7a26d3e538a Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 4 Aug 2026 08:51:18 +0200 Subject: [PATCH 225/390] [ty] Move Python version source out of `Program` (#27434) ## Summary `PythonVersionWithSource` contains from where ty derived the `PythonVersion`. If the Python version comes from the project's `pyproject.toml`, then the source includes the configuration file name and the text range of the `requires-python` or `python-version` key-value pair. Any change to that `pyproject.toml` would result in a new and different `PythonVersionWithSource`, which, in turn, results in a distinct interned `Program`. This results in invalidating every single type inference query (more accurately, we now get a new instance of each type inference query, but we loose all cached results). Storing `PythonVersionWithSource` also has the downside that it prevents scripts that otherwise have identical `Program` settings to share the same `Program`, only because their `python-version` comes from different script metadata blocks. This PR moves `PythonVersionWithSource` out of `Program`. `Program` should only contain information that changes type inference. `PythonVersionWithSource` does not. `Db` now has a new `python_version_with_source` method that, given a file, returns the source for it (A script can return a different source than a project file). ## Test Plan Testing: Semantic, project, core, server, Markdown, and corpus tests pass; repository hooks pass. --- crates/ty_project/src/db.rs | 10 +++++++++- crates/ty_python_core/src/program.rs | 7 +------ crates/ty_python_semantic/src/db.rs | 9 ++++++++- .../ty_python_semantic/src/diagnostic/mod.rs | 9 ++++----- crates/ty_python_semantic/src/lib.rs | 2 +- .../src/types/diagnostic.rs | 20 ++++++++++++------- .../src/types/infer/builder.rs | 4 ++-- .../src/types/infer/builder/imports.rs | 6 ++++-- .../types/infer/builder/type_expression.rs | 2 +- .../src/types/infer/tests.rs | 2 +- crates/ty_python_semantic/tests/corpus.rs | 8 +++++++- .../server/api/requests/execute_command.rs | 6 +----- crates/ty_test/src/db.rs | 7 ++++++- fuzz/fuzz_targets/ty_check_invalid_syntax.rs | 4 ++++ 14 files changed, 62 insertions(+), 34 deletions(-) diff --git a/crates/ty_project/src/db.rs b/crates/ty_project/src/db.rs index a276873110..ae2491a36d 100644 --- a/crates/ty_project/src/db.rs +++ b/crates/ty_project/src/db.rs @@ -17,7 +17,7 @@ use salsa::{Database, Event, Setter}; use ty_python_core::ProgramFile; use ty_python_core::program::{FallibleStrategy, MisconfigurationStrategy, UseDefaultStrategy}; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; -use ty_python_semantic::{AnalysisSettings, Db as SemanticDb}; +use ty_python_semantic::{AnalysisSettings, Db as SemanticDb, PythonVersionWithSource}; mod changes; @@ -542,6 +542,10 @@ impl SemanticDb for ProjectDatabase { self.project().program(self).program_file(self, file) } + fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource { + &self.project().program_settings(self).python_version + } + fn rule_selection(&self, file: File) -> &RuleSelection { let settings = file_settings(self, file); settings.rules(self) @@ -784,6 +788,10 @@ pub(crate) mod testing { self.project().program(self).program_file(self, file) } + fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource { + &self.project().program_settings(self).python_version + } + #[inline] fn check_file(&self, file: File) -> Vec { crate::check_file(self, file) diff --git a/crates/ty_python_core/src/program.rs b/crates/ty_python_core/src/program.rs index 59f5971b0d..613bd8d634 100644 --- a/crates/ty_python_core/src/program.rs +++ b/crates/ty_python_core/src/program.rs @@ -14,11 +14,6 @@ pub use ty_module_resolver::{FallibleStrategy, MisconfigurationStrategy, UseDefa #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct Program<'db> { - // FIXME: Move the source out of `Program`. Different source locations prevent otherwise - // equivalent programs from being reused across scripts. - #[returns(ref)] - pub python_version_with_source: PythonVersionWithSource, - #[returns(ref)] pub python_platform: PythonPlatform, @@ -39,7 +34,7 @@ impl<'db> Program<'db> { let resolver_environment = ResolverEnvironment::new(db, python_version.version, &search_paths); - Program::new(db, python_version, python_platform, resolver_environment) + Program::new(db, python_platform, resolver_environment) } pub fn python_version(self, db: &'db dyn Db) -> PythonVersion { diff --git a/crates/ty_python_semantic/src/db.rs b/crates/ty_python_semantic/src/db.rs index 305e7323d1..6bded70ce5 100644 --- a/crates/ty_python_semantic/src/db.rs +++ b/crates/ty_python_semantic/src/db.rs @@ -1,5 +1,5 @@ -use crate::AnalysisSettings; use crate::lint::{LintRegistry, RuleSelection}; +use crate::{AnalysisSettings, PythonVersionWithSource}; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::File; use ty_python_core::{Db as PythonCoreDb, ProgramFile}; @@ -12,6 +12,9 @@ pub trait Db: PythonCoreDb { /// Returns the program file for `file`. fn program_file(&self, file: File) -> ProgramFile<'_>; + /// Returns the Python version and its configuration source for `file`. + fn python_version_with_source(&self, file: File) -> &PythonVersionWithSource; + /// Resolves the rule selection for a given file. fn rule_selection(&self, file: File) -> &RuleSelection; @@ -177,6 +180,10 @@ pub(crate) mod tests { self.program().program_file(self, file) } + fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource { + &self.program_settings.python_version + } + fn rule_selection(&self, _file: File) -> &RuleSelection { &self.rule_selection } diff --git a/crates/ty_python_semantic/src/diagnostic/mod.rs b/crates/ty_python_semantic/src/diagnostic/mod.rs index 8ead4596ee..f8346f25a0 100644 --- a/crates/ty_python_semantic/src/diagnostic/mod.rs +++ b/crates/ty_python_semantic/src/diagnostic/mod.rs @@ -1,6 +1,6 @@ use crate::{ - Db, ProgramEnvironment, PythonVersionSource, PythonVersionWithSource, - lint::lint_documentation_url, types::TypeCheckDiagnostics, + Db, PythonVersionSource, PythonVersionWithSource, lint::lint_documentation_url, + types::TypeCheckDiagnostics, }; use levenshtein::{HideUnderscoredSuggestions, find_best_suggestion}; use ruff_db::{ @@ -46,12 +46,11 @@ pub fn inferred_python_version_source_annotation( /// configuration files, or defaults. pub(crate) fn add_inferred_python_version_hint_to_diagnostic( db: &dyn Db, - env: &ProgramEnvironment, + file: File, diagnostic: &mut Diagnostic, action: &str, ) { - let PythonVersionWithSource { version, source } = - env.program(db).python_version_with_source(db); + let PythonVersionWithSource { version, source } = db.python_version_with_source(file); match source { crate::PythonVersionSource::Cli => { diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 773990d89f..103be21c14 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -206,7 +206,7 @@ pub fn check_file(db: &dyn Db, file: ProgramFile<'_>) -> Result, diagnostic: &mut Diagnostic, full_submodule_name: &ModuleName, @@ -4722,7 +4723,7 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( version_range = version_range.diagnostic_display(), )); - add_inferred_python_version_hint_to_diagnostic(db, env, diagnostic, "resolving modules"); + add_inferred_python_version_hint_to_diagnostic(db, file, diagnostic, "resolving modules"); true } @@ -4737,7 +4738,7 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( /// misconfigured their Python version. pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( db: &dyn Db, - env: &ProgramEnvironment<'_>, + source_file: ProgramFile<'_>, mut diagnostic: LintDiagnosticGuard, value_type: Type, attr: &str, @@ -4750,7 +4751,7 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( return; }; let module = module_ty.module(db); - let Some(file) = module.file(db) else { + let Some(module_file) = module.file(db) else { return; }; let Some(search_path) = module.search_path(db) else { @@ -4763,7 +4764,7 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( // We populate place_table entries for stdlib items across all known versions and platforms, // so if this lookup succeeds then we know that this lookup *could* succeed with possible // configuration changes. - let program_file = ProgramFile::new(db, file, env.program(db)); + let program_file = ProgramFile::new(db, module_file, source_file.program(db)); let symbol_table = place_table(db, global_scope(db, program_file)); let Some(symbol) = symbol_table.symbol_by_name(attr) else { return; @@ -4779,7 +4780,12 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( // TODO: determine what version they need to be on // TODO: also mention the platform we're assuming // TODO: determine what platform they need to be on - add_inferred_python_version_hint_to_diagnostic(db, env, &mut diagnostic, action); + add_inferred_python_version_hint_to_diagnostic( + db, + source_file.file(db), + &mut diagnostic, + action, + ); } pub(super) fn report_invalid_concatenate_last_arg<'db>( diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index acad198bc3..b29e660d66 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -10163,7 +10163,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); add_inferred_python_version_hint_to_diagnostic( db, - self.program_environment(), + self.file(), &mut diagnostic, "resolving types", ); @@ -10509,7 +10509,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { hint_if_stdlib_attribute_exists_on_other_versions( db, - env, + self.program_file(), diagnostic, value_type, attr_name, diff --git a/crates/ty_python_semantic/src/types/infer/builder/imports.rs b/crates/ty_python_semantic/src/types/infer/builder/imports.rs index 78dab4f12e..2814ffd535 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/imports.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/imports.rs @@ -86,7 +86,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); add_inferred_python_version_hint_to_diagnostic( db, - self.program_environment(), + self.file(), &mut diagnostic, "resolving modules", ); @@ -515,6 +515,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(full_submodule_name) = full_submodule_name { submodule_hint_added = hint_if_stdlib_submodule_exists_on_other_versions( db, + self.file(), self.program_environment(), &mut diagnostic, &full_submodule_name, @@ -525,7 +526,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if !submodule_hint_added { hint_if_stdlib_attribute_exists_on_other_versions( db, - self.program_environment(), + self.program_file(), diagnostic, module_ty, name, @@ -638,6 +639,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { hint_if_stdlib_submodule_exists_on_other_versions( self.db(), + self.file(), self.program_environment(), &mut diagnostic, &full_submodule_name, diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 0ed17b16e5..6c280d1b84 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -356,7 +356,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { )); add_inferred_python_version_hint_to_diagnostic( db, - self.program_environment(), + self.file(), &mut diagnostic, "inferring types", ); diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index 0317728238..72960282d9 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -202,7 +202,7 @@ fn program_file_changes_with_python_version() -> anyhow::Result<()> { let equivalent_program = Program::from_settings( &db, ProgramSettings { - python_version: program.python_version_with_source(&db).clone(), + python_version: db.program_settings().python_version.clone(), python_platform: program.python_platform(&db).clone(), search_paths: program.search_paths(&db).clone(), }, diff --git a/crates/ty_python_semantic/tests/corpus.rs b/crates/ty_python_semantic/tests/corpus.rs index 1c7db3c6b6..ae99496dcc 100644 --- a/crates/ty_python_semantic/tests/corpus.rs +++ b/crates/ty_python_semantic/tests/corpus.rs @@ -8,7 +8,9 @@ use ruff_db::vendored::VendoredFileSystem; use ty_python_core::program::ProgramSettings; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::pull_types::pull_types; -use ty_python_semantic::{AnalysisSettings, Db as _, check_file_unwrap, default_lint_registry}; +use ty_python_semantic::{ + AnalysisSettings, Db as _, PythonVersionWithSource, check_file_unwrap, default_lint_registry, +}; use ruff_db::diagnostic::Diagnostic; use test_case::test_case; @@ -220,6 +222,10 @@ impl ty_python_semantic::Db for CorpusDb { self.program().program_file(self, file) } + fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource { + &self.program_settings.python_version + } + fn rule_selection(&self, _file: File) -> &RuleSelection { &self.rule_selection } diff --git a/crates/ty_server/src/server/api/requests/execute_command.rs b/crates/ty_server/src/server/api/requests/execute_command.rs index ed0c2234c6..1a3a8335bb 100644 --- a/crates/ty_server/src/server/api/requests/execute_command.rs +++ b/crates/ty_server/src/server/api/requests/execute_command.rs @@ -68,11 +68,7 @@ fn debug_information(session: &Session) -> crate::Result { writeln!(buffer, "Project at {}", db.project().root(db))?; let program = db.project().program(db); writeln!(buffer, "Program:")?; - writeln!( - buffer, - " python-version: {}", - program.python_version_with_source(db).version - )?; + writeln!(buffer, " python-version: {}", program.python_version(db))?; writeln!(buffer, " python-platform: {}", program.python_platform(db))?; let mut writer = IndentingWriter { inner: &mut buffer, diff --git a/crates/ty_test/src/db.rs b/crates/ty_test/src/db.rs index 51995c81f2..f2700d6fe0 100644 --- a/crates/ty_test/src/db.rs +++ b/crates/ty_test/src/db.rs @@ -19,7 +19,8 @@ use ty_python_core::program::ProgramSettings; use ty_python_core::{Db as _, ProgramFile, TestProgramDb}; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::{ - AnalysisSettings, Db as SemanticDb, check_file_unwrap, default_lint_registry, + AnalysisSettings, Db as SemanticDb, PythonVersionWithSource, check_file_unwrap, + default_lint_registry, }; #[salsa::db] @@ -146,6 +147,10 @@ impl SemanticDb for Db { self.program().program_file(self, file) } + fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource { + &self.settings().program(self).python_version + } + fn rule_selection(&self, file: File) -> &RuleSelection { file_settings(self, file).rules(self) } diff --git a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs index 6141d7c74a..d86e988072 100644 --- a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs +++ b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs @@ -127,6 +127,10 @@ impl SemanticDb for TestDb { self.program().program_file(self, file) } + fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource { + &self.program_settings.python_version + } + fn rule_selection(&self, _file: File) -> &RuleSelection { &self.rule_selection } From b9fa93c51134867e4784d5ad56f72cb145cce700 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 4 Aug 2026 12:21:38 +0530 Subject: [PATCH 226/390] [ty] Preserve return constraints for object-variadic callables (#27431) ## Summary Preserve callable return-type constraints when matching `Callable[..., T]` against callbacks with `*args: object` and `**kwargs: object`. fixes: astral-sh/ty#4151 fixes: astral-sh/ty#4169 ## Test plan Add a focused regression for return-type inference through object-variadic callbacks. Verified the original issue reproduction now infers `ChildClass` for the decorated method result. --- .../mdtest/generics/pep695/functions.md | 40 +++++++++++++++++++ .../src/types/signatures.rs | 4 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index a611e9d970..5798c87be5 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -1040,6 +1040,46 @@ def f[T](x: T, y: Not[T]) -> T: ## `Callable` parameters +### Return type inference from object-variadic callbacks + +Object-variadic callbacks must preserve `Callable[..., T]` return constraints. + +```py +from collections.abc import Callable + +def call[T](callback: Callable[..., T]) -> T: + return callback() + +def bounded[T: int](callback: Callable[..., T]) -> T: + return callback() + +def callback(*args: object, **kwargs: object) -> int: + return 1 + +reveal_type(call(callback)) # revealed: int +reveal_type(bounded(callback)) # revealed: int +``` + +### Gradual callable parameters with a required prefix + +```py +from collections.abc import Callable +from typing import Concatenate + +def invoke[T](callback: Callable[Concatenate[int, ...], T]) -> T: + return callback(1) + +def accepts_int(value: int, *args: object, **kwargs: object) -> int: + return value + +def needs_str(value: str, *args: object, **kwargs: object) -> int: + return len(value) + +reveal_type(invoke(accepts_int)) # revealed: int +# error: [invalid-argument-type] +reveal_type(invoke(needs_str)) # revealed: int +``` + ### Class constructors We can recurse into the parameters and return values of `Callable` parameters to infer diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index e31d68d96c..8a4150327f 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -3191,6 +3191,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // A gradual parameter list is a supertype of the "bottom" parameter list (*args: object, // **kwargs: object). if target.parameters.is_gradual() + && (matches!(target.parameters.kind(), ParametersKind::Gradual) + || self.typevar_evaluation == TypeVarEvaluation::Lazy) && !source.parameters.is_top() && source .parameters @@ -3201,7 +3203,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .keyword_variadic() .is_some_and(|(_, param)| param.annotated_type().is_object()) { - return self.always(); + return result; } // The top signature is supertype of (and assignable from) all other signatures. It is a From e55b98e26f55871221bd7871c1fe4b67772d2312 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 4 Aug 2026 14:21:58 +0530 Subject: [PATCH 227/390] [ty] Preserve return constraints for top callables (#27446) ## Summary Preserve return-type inference for `Top[Callable[..., T]]`. Follow-up to https://github.com/astral-sh/ruff/pull/27431#discussion_r3708772027. ## Test plan Cover return-type inference for an ordinary callback accepted as a top callable. --- .../mdtest/generics/pep695/functions.md | 17 +++++++++++++++++ .../ty_python_semantic/src/types/signatures.rs | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 5798c87be5..57d31e99d6 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -1060,6 +1060,23 @@ reveal_type(call(callback)) # revealed: int reveal_type(bounded(callback)) # revealed: int ``` +### Return type inference from top callables + +Top callable parameters must preserve return-type constraints. + +```py +from collections.abc import Callable +from ty_extensions import Top + +def accept_top[T](callback: Top[Callable[..., T]]) -> T: + raise NotImplementedError + +def ordinary() -> int: + return 1 + +reveal_type(accept_top(ordinary)) # revealed: int +``` + ### Gradual callable parameters with a required prefix ```py diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 8a4150327f..9e7d1fcdad 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -3209,7 +3209,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // The top signature is supertype of (and assignable from) all other signatures. It is a // subtype of no signature except itself, and assignable only to the gradual signature. if target.parameters.is_top() { - return self.always(); + return result; } else if source.parameters.is_top() && !target.parameters.is_gradual() { if let Some(context) = self.report_context() { context.push(ErrorContext::TopCallableAssignedToNonTop { From f00d9b8a7f26d9c70b64246712c25f15682fe6e3 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 4 Aug 2026 14:22:05 +0200 Subject: [PATCH 228/390] [ty] Bump ecosystem-analyzer to fix attrs and Bokeh (#27457) ## Summary Bump both ty ecosystem workflows to astral-sh/ecosystem-analyzer#143, which includes hauntsaninja/mypy_primer#257, which fixes `attrs` and `Bokeh`. --- .github/workflows/ty-ecosystem-analyzer.yaml | 2 +- .github/workflows/ty-ecosystem-report.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index 2174f274fd..cc019609c9 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -42,7 +42,7 @@ env: RUST_BACKTRACE: 1 # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_PROFILING_DEBUG: line-tables-only - ECOSYSTEM_ANALYZER_COMMIT: 263b5500881186e8c918193577c23b341e5b7237 + ECOSYSTEM_ANALYZER_COMMIT: f6f1b7b8586c8a6c60dc3d37c3f6ae917a9ad9c4 jobs: build-ty: diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 97998b6682..72fd7b40d1 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -20,7 +20,7 @@ env: RUST_BACKTRACE: 1 # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_PROFILING_DEBUG: line-tables-only - ECOSYSTEM_ANALYZER_COMMIT: e2c5b76149b147fae104a7d8fa0997a9eb7f7754 + ECOSYSTEM_ANALYZER_COMMIT: f6f1b7b8586c8a6c60dc3d37c3f6ae917a9ad9c4 jobs: ty-ecosystem-report: From f96df0444698b0b129dbe71388672ec141b48cff Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 4 Aug 2026 14:47:40 +0200 Subject: [PATCH 229/390] [ty] Top-materialize `is_dataclass` type guard (#27455) ## Summary Top-materialize th gradual `DataclassInstance` protocol in the `TypeIs` return type of `is_dataclass`, so that we can recognize dataclass instances as being a subtype of that return type. Fixes https://github.com/astral-sh/ty/issues/4149. ## Ecosystem impact Looks good! ## Test plan Updated and added mdtests. --- .../mdtest/dataclasses/dataclasses.md | 14 ++++++--- .../0007-dataclasses-is-dataclass-top.patch | 30 +++++++++++++++++++ .../vendor/typeshed/stdlib/dataclasses.pyi | 7 +++-- 3 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 crates/ty_vendored/typeshed_patches/0007-dataclasses-is-dataclass-top.patch diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 7e2f477db6..d30fff360e 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -2471,8 +2471,7 @@ asdict(Foo) ## `dataclasses.is_dataclass` `is_dataclass` recognizes both dataclass instances and dataclass classes. A concrete dataclass -instance always satisfies the `DataclassInstance` protocol, but we do not currently recognize that -the negative branch is unreachable: +instance always satisfies the `DataclassInstance` protocol: ```py from dataclasses import dataclass, is_dataclass @@ -2483,8 +2482,15 @@ class Event: def check(event: Event) -> None: if not is_dataclass(event): - # TODO: This should be `Never`. - reveal_type(event) # revealed: Event & ~DataclassInstance & ~type[DataclassInstance] + reveal_type(event) # revealed: Never +``` + +This also works for class objects: + +```py +def check_class(event_type: type[Event]) -> None: + if not is_dataclass(event_type): + reveal_type(event_type) # revealed: Never ``` ## `dataclasses.KW_ONLY` diff --git a/crates/ty_vendored/typeshed_patches/0007-dataclasses-is-dataclass-top.patch b/crates/ty_vendored/typeshed_patches/0007-dataclasses-is-dataclass-top.patch new file mode 100644 index 0000000000..c0f47888b2 --- /dev/null +++ b/crates/ty_vendored/typeshed_patches/0007-dataclasses-is-dataclass-top.patch @@ -0,0 +1,30 @@ +diff --git a/stdlib/dataclasses.pyi b/stdlib/dataclasses.pyi +index d46b694a7e..1db97b1893 100644 +--- a/stdlib/dataclasses.pyi ++++ b/stdlib/dataclasses.pyi +@@ -7,6 +7,7 @@ from collections.abc import Callable, Iterable, Mapping + from types import GenericAlias + from typing import Any, Final, Generic, Literal, Protocol, TypeVar, overload, type_check_only + from typing_extensions import Never, TypeIs ++from ty_extensions import Top + + _T = TypeVar("_T") + _T_co = TypeVar("_T_co", covariant=True) +@@ -402,14 +403,14 @@ def fields(class_or_instance: DataclassInstance | type[DataclassInstance]) -> tu + + # HACK: `obj: Never` typing matches if object argument is using `Any` type. + @overload +-def is_dataclass(obj: Never) -> TypeIs[DataclassInstance | type[DataclassInstance]]: # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-type-guard-definition] ++def is_dataclass(obj: Never) -> TypeIs[Top[DataclassInstance | type[DataclassInstance]]]: # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-type-guard-definition] + """Returns True if obj is a dataclass or an instance of a + dataclass. + """ + @overload +-def is_dataclass(obj: type) -> TypeIs[type[DataclassInstance]]: ... ++def is_dataclass(obj: type) -> TypeIs[Top[type[DataclassInstance]]]: ... + @overload +-def is_dataclass(obj: object) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ... ++def is_dataclass(obj: object) -> TypeIs[Top[DataclassInstance | type[DataclassInstance]]]: ... + + class FrozenInstanceError(AttributeError): ... + diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi index d46b694a7e..1db97b1893 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/dataclasses.pyi @@ -7,6 +7,7 @@ from collections.abc import Callable, Iterable, Mapping from types import GenericAlias from typing import Any, Final, Generic, Literal, Protocol, TypeVar, overload, type_check_only from typing_extensions import Never, TypeIs +from ty_extensions import Top _T = TypeVar("_T") _T_co = TypeVar("_T_co", covariant=True) @@ -402,14 +403,14 @@ def fields(class_or_instance: DataclassInstance | type[DataclassInstance]) -> tu # HACK: `obj: Never` typing matches if object argument is using `Any` type. @overload -def is_dataclass(obj: Never) -> TypeIs[DataclassInstance | type[DataclassInstance]]: # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-type-guard-definition] +def is_dataclass(obj: Never) -> TypeIs[Top[DataclassInstance | type[DataclassInstance]]]: # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues] # ty:ignore[invalid-type-guard-definition] """Returns True if obj is a dataclass or an instance of a dataclass. """ @overload -def is_dataclass(obj: type) -> TypeIs[type[DataclassInstance]]: ... +def is_dataclass(obj: type) -> TypeIs[Top[type[DataclassInstance]]]: ... @overload -def is_dataclass(obj: object) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ... +def is_dataclass(obj: object) -> TypeIs[Top[DataclassInstance | type[DataclassInstance]]]: ... class FrozenInstanceError(AttributeError): ... From 94d1befea50e60149c71f154ecd40b2785d74925 Mon Sep 17 00:00:00 2001 From: David Peter Date: Tue, 4 Aug 2026 14:55:14 +0200 Subject: [PATCH 230/390] [ty] Reject unsupported `dataclass_transform` parameters (#27458) ## Summary Reject unrecognized `dataclass_transform` parameters based on this paragraph in the [spec](https://typing.python.org/en/latest/spec/dataclasses.html#dataclass-transform-parameters) (emphasis mine): > kwargs allows arbitrary additional keyword args to be passed to dataclass_transform. This gives type checkers the freedom to support experimental parameters without needing to wait for changes in typing.py. **Type checkers should report errors for any unrecognized parameters.** Since we currently don't do any experiments with unofficial parameters to `dataclass_transform`, we can just remove `**kwargs`. Fixes https://github.com/astral-sh/ty/issues/4170. ## Test plan New Markdown tests. --- .../mdtest/dataclasses/dataclass_transform.md | 24 +++++++++++++++++++ ...\200\246_-_Syntax_(142fa2948c3c6cf1).snap" | 4 ++-- ...ataclass-transform-unknown-arguments.patch | 22 +++++++++++++++++ .../vendor/typeshed/stdlib/typing.pyi | 1 - .../typeshed/stdlib/typing_extensions.pyi | 1 - 5 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 crates/ty_vendored/typeshed_patches/0007-dataclass-transform-unknown-arguments.patch diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md index 219dffc4d8..0434c3bbe5 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md @@ -847,6 +847,30 @@ class NotOrderedWithOverrides: return False ``` +### Unrecognized parameters + +`dataclass_transform` rejects unrecognized parameters: + +```py +from typing import dataclass_transform + +# error: [unknown-argument] "Argument `unsupported` does not match any known parameter" +@dataclass_transform(unsupported=True) +def my_model[T](cls: type[T]) -> type[T]: + return cls +``` + +This also works for the variant from `typing_extensions`: + +```py +from typing_extensions import dataclass_transform + +# error: [unknown-argument] "Argument `unsupported` does not match any known parameter" +@dataclass_transform(unsupported=True) +def my_model[T](cls: type[T]) -> type[T]: + return cls +``` + ## Other `dataclass` parameters Other parameters from normal dataclasses can also be set on models created using diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Syntax_(142fa2948c3c6cf1).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Syntax_(142fa2948c3c6cf1).snap" index 624cc4b50a..73a092e41e 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Syntax_(142fa2948c3c6cf1).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr\342\200\246_-_Syntax_(142fa2948c3c6cf1).snap" @@ -83,9 +83,9 @@ error[missing-argument]: No argument provided for required parameter `arg` of bo 6 | invalid_deco() # error: [missing-argument] | ^^^^^^^^^^^^^^ info: Parameter declared here - --> stdlib/typing_extensions.pyi:1220:28 + --> stdlib/typing_extensions.pyi:1219:28 | -1220 | def __call__(self, arg: _T, /) -> _T: ... +1219 | def __call__(self, arg: _T, /) -> _T: ... | ^^^^^^^ ``` diff --git a/crates/ty_vendored/typeshed_patches/0007-dataclass-transform-unknown-arguments.patch b/crates/ty_vendored/typeshed_patches/0007-dataclass-transform-unknown-arguments.patch new file mode 100644 index 0000000000..3e6b7a8db8 --- /dev/null +++ b/crates/ty_vendored/typeshed_patches/0007-dataclass-transform-unknown-arguments.patch @@ -0,0 +1,22 @@ +--- a/stdlib/typing.pyi ++++ b/stdlib/typing.pyi +@@ -2280,7 +2280,6 @@ if sys.version_info >= (3, 11): + order_default: bool = False, + kw_only_default: bool = False, + frozen_default: bool = False, # on 3.11, runtime accepts it as part of kwargs + field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (), +- **kwargs: Any, + ) -> IdentityFunction: + """Decorator to mark an object as providing dataclass-like behavior. + +--- a/stdlib/typing_extensions.pyi ++++ b/stdlib/typing_extensions.pyi +@@ -804,8 +804,7 @@ else: + order_default: bool = False, + kw_only_default: bool = False, + frozen_default: bool = False, + field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (), +- **kwargs: object, + ) -> IdentityFunction: + """Decorator that marks a function, class, or metaclass as providing + dataclass-like behavior. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi index 6acb32d4b6..6c4746be17 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/typing.pyi @@ -2281,7 +2281,6 @@ if sys.version_info >= (3, 11): kw_only_default: bool = False, frozen_default: bool = False, # on 3.11, runtime accepts it as part of kwargs field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (), - **kwargs: Any, ) -> IdentityFunction: """Decorator to mark an object as providing dataclass-like behavior. diff --git a/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi b/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi index ff06ec0a88..579c487db4 100644 --- a/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi +++ b/crates/ty_vendored/vendor/typeshed/stdlib/typing_extensions.pyi @@ -805,7 +805,6 @@ else: kw_only_default: bool = False, frozen_default: bool = False, field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (), - **kwargs: object, ) -> IdentityFunction: """Decorator that marks a function, class, or metaclass as providing dataclass-like behavior. From 0fcc5aea4934e44347ca8c00f5f680a74fc73c0b Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 4 Aug 2026 06:29:57 -0700 Subject: [PATCH 231/390] [ty] Fix unused hints for OR-pattern captures (#27438) ## Summary OR-pattern alternatives can bind the same names but "execute" mutually exclusively. The semantic index previously visited alternatives as consecutive assignments, so later captures shadowed earlier captures and caused the language server to incorrectly report those earlier bindings as unused. Visit each capture-bearing alternative from the same incoming flow state and merge the resulting bindings. Preserve the existing fast path for OR patterns that do not bind names. Closes astral-sh/ty#4163. ## Test plan - Used captures across multiple OR-pattern alternatives and captured names. - Nested OR-pattern captures referenced only by a match guard. - Genuinely unused alternative captures, earlier shadowed assignments, and captures in separate match cases. --- crates/ty_python_core/src/builder.rs | 27 ++++++++++ .../src/types/ide_support/unused_bindings.rs | 51 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index d0b9227631..c343160776 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -4949,6 +4949,33 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { } fn visit_pattern(&mut self, pattern: &'ast ast::Pattern) { + if let ast::Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) = pattern + && let Some((first, alternatives)) = patterns.split_first() + { + let incoming = self.flow_snapshot(); + let first_definition = self.current_use_def_map().next_definition_id(); + self.visit_pattern(first); + + // Valid alternatives bind the same names, so capture-free patterns need no flow merge. + if self.current_use_def_map().next_definition_id() == first_definition { + for alternative in alternatives { + self.visit_pattern(alternative); + } + return; + } + + // Each alternative starts with the same bindings. Otherwise a repeated capture in a + // later alternative shadows the earlier capture even though only one pattern matches. + let mut merged_alternatives = self.flow_snapshot(); + for alternative in alternatives { + self.flow_restore(incoming.clone()); + self.visit_pattern(alternative); + self.flow_merge(merged_alternatives); + merged_alternatives = self.flow_snapshot(); + } + return; + } + if let ast::Pattern::MatchStar(ast::PatternMatchStar { name: Some(name), range: _, diff --git a/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs b/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs index 11e73a8af6..1597d2a040 100644 --- a/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs +++ b/crates/ty_python_semantic/src/types/ide_support/unused_bindings.rs @@ -320,6 +320,57 @@ mod tests { Ok(()) } + #[test] + fn or_pattern_captures_used_in_body_are_not_reported() -> anyhow::Result<()> { + let source = dedent( + " + def f(subject): + match subject: + case [first, second] | {\"first\": first, \"second\": second} | (first, second): + print(first, second) + ", + ); + + assert!(collect_unused_names(&source)?.is_empty()); + Ok(()) + } + + #[test] + fn nested_or_pattern_capture_used_in_guard_is_not_reported() -> anyhow::Result<()> { + let source = dedent( + " + def f(subject): + match subject: + case [[value] | {\"item\": value}] if value: + pass + ", + ); + + assert!(collect_unused_names(&source)?.is_empty()); + Ok(()) + } + + #[test] + fn or_pattern_captures_do_not_hide_other_unused_bindings() -> anyhow::Result<()> { + let source = dedent( + " + def f(subject): + value = 0 + match subject: + case [value] | {\"used\": value}: + print(value) + case {\"unused\": value} | {\"also_unused\": value}: + pass + ", + ); + + assert_eq!( + collect_unused_names(&source)?, + vec!["value", "value", "value"] + ); + Ok(()) + } + #[test] fn skips_module_and_class_scope_bindings() -> anyhow::Result<()> { let source = dedent( From b56bb12ea69ace7b72740bd924137ae1b36ad923 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Aug 2026 09:41:57 -0400 Subject: [PATCH 232/390] [ty] Preserve generic class type variables in constructor inference (#27340) ## Summary Constructing a generic class from one of its own type variables currently loses that type variable and produces `C[Unknown]`. We now freshen the constructor-specific generic context while preserving the source-level return template, so `C(value)` within `C[T]` retains `C[T]`. We also keep bound constructor receivers and downstream `__init__` bindings on the same fresh occurrence, and distinguish that occurrence from outer type variables during inference. Closes https://github.com/astral-sh/ty/issues/4132. Closes https://github.com/astral-sh/ty/issues/3963. --- .../mdtest/generics/legacy/classes.md | 47 ++++++++++ .../mdtest/generics/pep695/classes.md | 60 ++++++++++++ .../ty_python_semantic/src/types/call/bind.rs | 6 +- .../src/types/call/bind/constructor.rs | 93 ++++++++++++++++++- .../ty_python_semantic/src/types/generics.rs | 8 +- .../src/types/signatures.rs | 2 +- .../ty_python_semantic/src/types/typevar.rs | 2 +- 7 files changed, 208 insertions(+), 10 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 60cf17783c..0469cec067 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -615,6 +615,53 @@ reveal_type(C(1)) # revealed: C[int] wrong_innards: C[int] = C("five") ``` +### Constructing the class from its own type variable + +A constructor call inside a generic class can use a value whose type is one of the class's type +variables. The constructed instance keeps that type variable instead of falling back to `Unknown`, +so an incompatible type context is rejected. + +```py +from typing_extensions import Generic, TypeVar + +T = TypeVar("T") + +class C(Generic[T]): + def __init__(self, value: T) -> None: + reveal_type(C(value)) # revealed: C[T@C] + + # error: [invalid-assignment] "Object of type `C[T@C]` is not assignable to `C[int]`" + invalid: C[int] = C(value) +``` + +### Constructing through a classmethod receiver + +A constructor call through a classmethod receiver keeps an enclosing `TypeVarTuple` when checking +the constructor arguments. In particular, freshening the constructor must not replace the +`TypeVarTuple` in the receiver with `Unknown`. + +```toml +[environment] +python-version = "3.11" +``` + +```py +from __future__ import annotations + +from typing import Generic, TypeVarTuple + +Ts = TypeVarTuple("Ts") + +class Thunk(Generic[*Ts]): + def __init__(self, state: Unresolved[*Ts] | None) -> None: ... + @classmethod + def make(cls, *values: *Ts) -> Thunk[*Ts]: + return cls(Unresolved(values)) + +class Unresolved(Generic[*Ts]): + def __init__(self, values: tuple[*Ts]) -> None: ... +``` + ### Many invariant parameters with dynamic bounds Treating unrelated classes with `Any` in their MRO as transitive pivots caused inference time to 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 443dff274b..d31fdcdb12 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -307,6 +307,35 @@ If a typevar does not provide a default, we use `Unknown`: reveal_type(C()) # revealed: C[Unknown] ``` +## Calls within the generic class + +A call to a generic class from one of its own methods creates an independent generic occurrence. The +enclosing class's type variable does not constrain the new instance. + +```py +class C[T]: + def __init__(self) -> None: ... + def method(self) -> None: + reveal_type(C()) # revealed: C[Unknown] + contextual: C[int] = C() +``` + +The same applies when an explicit `__new__` is followed by a downstream `__init__`. Both bound +receivers refer to the new generic occurrence. + +```py +from typing import Self + +class D[T]: + def __new__(cls) -> Self: + return super().__new__(cls) + + def __init__(self) -> None: ... + def method(self) -> None: + reveal_type(D()) # revealed: D[Unknown] + contextual: D[int] = D() +``` + ## 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 @@ -352,6 +381,37 @@ reveal_type(C(1)) # revealed: C[Literal[1]] wrong_innards: C[int] = C("five") ``` +### Constructing the class from its own type variable + +A constructor call inside a generic class can use a value whose type is one of the class's type +variables. The constructed instance keeps that type variable instead of falling back to `Unknown`, +so an incompatible type context is rejected. + +```py +class C[T]: + def __init__(self, value: T) -> None: + reveal_type(C(value)) # revealed: C[T@C] + + # error: [invalid-assignment] "Object of type `C[T@C]` is not assignable to `C[int]`" + invalid: C[int] = C(value) + + def from_union(self, value: T | list[T]) -> None: + reveal_type(C(value)) # revealed: C[T@C | list[T@C]] + + # error: [invalid-assignment] "Object of type `C[T@C | list[T@C]]` is not assignable to `C[list[T@C]]`" + invalid_union: C[list[T]] = C(value) +``` + +A method's own type variable is independent of the class type variable and is preserved in the same +way. + +```py +class D[T]: + def __init__(self, value: T) -> None: ... + def method[S](self, value: S) -> None: + reveal_type(D(value)) # revealed: D[S@method] +``` + ### Identical `__new__` and `__init__` signatures ```py diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index fc66282bfc..47e50f1069 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -354,9 +354,9 @@ impl<'db> CallableItem<'db> { CallableItem::Regular(binding) => { binding.freshen_generic_contexts_in_place(db, env, nonce_generator); } - // TODO: Constructor freshening also has to keep constructor instance context in sync - // with `__new__`/`__init__` signatures. - CallableItem::Constructor(_) => {} + CallableItem::Constructor(binding) => { + binding.freshen_generic_contexts_in_place(db, env, nonce_generator); + } } } diff --git a/crates/ty_python_semantic/src/types/call/bind/constructor.rs b/crates/ty_python_semantic/src/types/call/bind/constructor.rs index 20de4c1e17..cff5c0d96b 100644 --- a/crates/ty_python_semantic/src/types/call/bind/constructor.rs +++ b/crates/ty_python_semantic/src/types/call/bind/constructor.rs @@ -1,11 +1,17 @@ -use super::{Binding, Bindings, CallableBinding, CallableItem, CheckTypesMode}; +use super::{ + Binding, Bindings, CallableBinding, CallableItem, CheckTypesMode, generic_context_has_paramspec, +}; use crate::Db; use crate::ProgramEnvironment; use crate::types::call::arguments::CallArguments; use crate::types::constraints::ConstraintSetBuilder; -use crate::types::generics::Specialization; +use crate::types::generics::{GenericContext, Specialization}; use crate::types::signatures::Parameter; -use crate::types::{BoundTypeVarInstance, ClassLiteral, DynamicType, Type, TypeContext}; +use crate::types::typevar::TypeVarNonceGenerator; +use crate::types::{ + ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, DynamicType, Type, TypeContext, + TypeMapping, +}; /// Bindings for a constructor call. /// @@ -65,6 +71,87 @@ impl<'db> ConstructorBinding<'db> { self.downstream_constructor = Some(Box::new(bindings)); } + pub(super) fn freshen_generic_contexts_in_place( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + nonce_generator: &TypeVarNonceGenerator<'db>, + ) { + let instance_type = self.constructed_instance_type(); + let Some((_, specialization)) = instance_type.class_specialization(db, env) else { + return; + }; + let generic_context = specialization.generic_context(db); + if generic_context_has_paramspec(db, generic_context) + || !nonce_generator.should_freshen(db, generic_context) + { + return; + } + + let delta = nonce_generator.next().value(); + let type_mapping = TypeMapping::FreshenBoundTypeVars { + generic_context, + delta, + }; + let fresh_instance_type = + instance_type.apply_type_mapping(db, env, &type_mapping, TypeContext::default()); + // Only freshen a generic context that belongs to the constructed instance itself. + // `class_specialization` can also find a context through a class-object type variable's + // bound, but freshening that context would detach the constructor parameters from the + // receiver. + if fresh_instance_type == instance_type { + return; + } + self.freshen_class_typevars(db, env, generic_context, delta, fresh_instance_type); + } + + fn freshen_class_typevars( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + generic_context: GenericContext<'db>, + delta: u32, + fresh_instance_type: Type<'db>, + ) { + let type_mapping = TypeMapping::FreshenBoundTypeVars { + generic_context, + delta, + }; + + // Keep the source-level instance on `ConstructorBinding`; the final return type applies + // the inferred specialization to that instance. Only the per-overload context is + // call-local, so its instance must use the same fresh type variables as the signature. + let constructor_context = self.context().with_instance_type(fresh_instance_type); + let visitor = ApplyTypeMappingVisitor::new(env); + self.entry.bound_type = self.entry.bound_type.map(|bound_type| { + bound_type.apply_type_mapping_impl(db, &type_mapping, TypeContext::default(), &visitor) + }); + for overload in &mut self.entry.overloads { + overload.signature = overload.signature.apply_type_mapping_impl( + db, + &type_mapping, + TypeContext::default(), + &visitor, + ); + overload.set_constructor_context(db, constructor_context); + } + + if let Some(downstream) = self.downstream_constructor_mut() { + for downstream_binding in downstream + .iter_callable_items_mut() + .filter_map(CallableItem::as_constructor_mut) + { + downstream_binding.freshen_class_typevars( + db, + env, + generic_context, + delta, + fresh_instance_type, + ); + } + } + } + /// Match parameters for this constructor method and downstream constructors. pub(super) fn match_parameters( &mut self, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 3bb46e0c5a..d99af4f849 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -2704,13 +2704,17 @@ impl<'db, 'c> SpecializationBuilder<'db, 'c> { ) -> bool { let db = self.db; let target_context = target.binding_context(db); + let target_freshness = target.freshness(db); ty.as_typevar().is_some_and(|typevar| { // Relationships across binding contexts can intentionally remap one generic context - // onto another, as with constructor `self` annotations. Synthetic contexts do not - // identify a single source-level binding, so they are not safe to project either. + // onto another, as with constructor `self` annotations. Relationships across fresh + // occurrences preserve an outer generic value through a recursive call. Synthetic + // contexts do not identify a single source-level binding, so they are not safe to + // project either. !matches!(target_context, BindingContext::Synthetic(_)) && typevar.is_inferable(db, self.inferable) && typevar.binding_context(db) == target_context + && typevar.freshness(db) == target_freshness }) } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 9e7d1fcdad..a2c1755ebc 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -970,7 +970,7 @@ impl<'db> Signature<'db> { }) } - fn apply_type_mapping_impl<'a>( + pub(super) fn apply_type_mapping_impl<'a>( &self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>, diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index e2e7c115ba..6ba7476926 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -986,7 +986,7 @@ impl<'db> BoundTypeVarInstance<'db> { self.identity(db).paramspec_attr } - fn freshness(self, db: &'db dyn Db) -> TypeVarNonce { + pub(super) fn freshness(self, db: &'db dyn Db) -> TypeVarNonce { self.identity(db).freshness } From d82ee58c3456523513e5950b19c1b9ef0caccc41 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Aug 2026 09:43:27 -0400 Subject: [PATCH 233/390] [ty] Retain typing-only symbols in explicit completions (#27435) ## Summary We now exclude private type variables, parameter specifications, type-variable tuples, aliases, and `@type_check_only` definitions from completions, even though those names can be explicitly imported in typing-only contexts: ```python from typing import TYPE_CHECKING if TYPE_CHECKING: from package import _Alias, _T ``` This PR makes those definitions consistent with existing `@type_check_only` completion behavior: retain them as explicit import and attribute completions, mark them as typing-only, and rank them below runtime values. --- crates/ty_ide/src/completion.rs | 98 ++++++++++++++++--- .../mdtest/ide_support/all_members.md | 26 +++++ .../ty_python_semantic/src/semantic_model.rs | 38 ++++--- .../src/types/list_members.rs | 39 ++++---- 4 files changed, 161 insertions(+), 40 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 2e493b186b..f4b196f348 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -453,6 +453,7 @@ impl<'db> CompletionBuilder<'db> { Completion::builder(semantic.name) .ty(semantic.ty) .builtin(semantic.builtin) + .type_check_only(semantic.is_type_check_only) .docstring(documentation) } @@ -492,7 +493,7 @@ impl<'db> CompletionBuilder<'db> { query: &UserQuery, ) -> Completion<'db> { if let Some(ty) = self.ty { - self.is_type_check_only = ty.is_type_check_only(db); + self.is_type_check_only |= ty.is_type_check_only(db); // Tags completions with context-specific if they are // known to be usable in an exception context and we have // determined an `exception_ty`. @@ -611,6 +612,11 @@ impl<'db> CompletionBuilder<'db> { self } + fn type_check_only(mut self, yes: bool) -> CompletionBuilder<'db> { + self.is_type_check_only = yes; + self + } + fn context_specific(mut self, yes: bool) -> CompletionBuilder<'db> { self.is_context_specific = yes; self @@ -3609,25 +3615,95 @@ if TYPE_CHECKING: test.contains("__mangled_name"); test.contains("__dunder_name__"); test.contains("public_type_var"); - test.not_contains("_private_type_var"); - test.not_contains("__mangled_type_var"); test.contains("public_param_spec"); - test.not_contains("_private_param_spec"); test.contains("public_type_var_tuple"); - test.not_contains("_private_type_var_tuple"); test.contains("public_explicit_type_alias"); - test.not_contains("_private_explicit_type_alias"); test.contains("public_implicit_union_alias"); - test.not_contains("_private_implicit_union_alias"); test.contains("_private_runtime_union"); test.contains("_private_runtime_typevar"); test.contains("_private_precise_runtime_union"); test.contains("PublicProtocol"); test.contains("_PrivateProtocol"); - test.not_contains("PublicTypeOnlyProtocol"); - test.not_contains("_PrivateTypeOnlyProtocol"); - test.not_contains("PublicTypeCheckingProtocol"); - test.not_contains("_PrivateTypeCheckingProtocol"); + + for name in [ + "_private_type_var", + "__mangled_type_var", + "_private_param_spec", + "_private_type_var_tuple", + "_private_explicit_type_alias", + "_private_implicit_union_alias", + "PublicTypeOnlyProtocol", + "_PrivateTypeOnlyProtocol", + "PublicTypeCheckingProtocol", + "_PrivateTypeCheckingProtocol", + ] { + assert!( + test.completions() + .iter() + .any(|completion| completion.name == name && completion.is_type_check_only), + "Expected `{name}` to be marked as typing-only", + ); + } + } + + #[test] + fn private_stub_symbols_rank_below_runtime_values() { + let builder = CursorTest::builder() + .source( + "package/__init__.pyi", + "from typing import TypeVar\n_Alpha = TypeVar(\"_Alpha\")\n_Zeta = 1", + ) + .source("main.py", "import package; package._") + .completion_test_builder() + .filter(|completion| matches!(completion.name.as_str(), "_Alpha" | "_Zeta")); + + let test = builder.build(); + let completions = test + .completions() + .iter() + .map(|completion| (completion.name.as_str(), completion.is_type_check_only)) + .collect::>(); + + assert_eq!(completions, [("_Zeta", false), ("_Alpha", true)]); + } + + #[test] + fn type_checking_import_includes_private_stub_symbols() { + let builder = CursorTest::builder() + .source( + "package/__init__.pyi", + "from typing import TypeAlias, TypeVar\n_Alias: TypeAlias = int\n_T = TypeVar(\"_T\")", + ) + .source( + "main.py", + "from typing import TYPE_CHECKING\nif TYPE_CHECKING:\n from package import _", + ) + .completion_test_builder(); + + let test = builder.build(); + for name in ["_Alias", "_T"] { + assert!( + test.completions() + .iter() + .any(|completion| completion.name == name && completion.is_type_check_only), + "Expected `{name}` to be available as a typing-only completion", + ); + } + } + + #[test] + fn typing_only_project_builtins_not_suggested_implicitly() { + let builder = CursorTest::builder() + .source( + "__builtins__.pyi", + "from typing import TypeVar\n_typing_only = TypeVar(\"_typing_only\")\n_runtime: int", + ) + .source("main.py", "_") + .completion_test_builder() + .skip_auto_import(); + + let test = builder.build(); + test.contains("_runtime").not_contains("_typing_only"); } /// Unlike [`private_symbols_in_stub`], this test doesn't use a `.pyi` file so all of the names diff --git a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md index 43c5eccf57..038af00f21 100644 --- a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md +++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md @@ -675,6 +675,32 @@ static_assert(has_member(module, "evaluate")) static_assert(not has_member(module, "Optional")) ``` +### Private typing-only stub members + +Typing-only helpers in stubs remain available as module members for autocomplete. + +`module.pyi`: + +```pyi +from typing import TypeAlias, TypeVar + +_Alias: TypeAlias = int +_T = TypeVar("_T") +_runtime: int +``` + +`main.py`: + +```py +import module +from ty_extensions import static_assert +from ty_extensions._internal import has_member + +static_assert(has_member(module, "_runtime")) +static_assert(has_member(module, "_Alias")) +static_assert(has_member(module, "_T")) +``` + ## Conditionally available members Some members are only conditionally available. For example, `bytearray.take_bytes` was only diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index d728d189bb..a203fc8e66 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -18,7 +18,7 @@ use ty_module_resolver::{ use crate::Db; use crate::place::implicit_globals::all_implicit_module_globals; use crate::types::ide_support::{ImportAliasResolution, definition_for_name}; -use crate::types::list_members::{Member, all_members, all_reachable_members}; +use crate::types::list_members::{all_members, all_reachable_members}; use crate::types::{ CycleDetector, ProgramEnvironment, SpecialFormType, Type, TypeQualifiers, binding_type, infer_complete_scope_types, inferred_declaration, @@ -169,6 +169,7 @@ impl<'db> SemanticModel<'db> { name: CompactString::new(module.name(self.db).as_str()), ty: Some(ty), builtin, + is_type_check_only: false, } }) .collect() @@ -239,11 +240,12 @@ impl<'db> SemanticModel<'db> { clippy::iter_over_hash_type, reason = "completion order is determined later by relevance ranking" )] - for Member { name, ty } in all_members(db, &self.program_environment(), ty) { + for member in all_members(db, &self.program_environment(), ty) { completions.push(Completion { - name: CompactString::new(name), - ty: Some(ty), + name: CompactString::new(member.name), + ty: Some(member.ty), builtin, + is_type_check_only: member.is_type_check_only, }); } completions.extend(self.submodule_completions(&module)); @@ -262,6 +264,7 @@ impl<'db> SemanticModel<'db> { name: CompactString::new(base), ty: Some(ty), builtin, + is_type_check_only: false, }); } completions @@ -280,6 +283,7 @@ impl<'db> SemanticModel<'db> { name: CompactString::new(member.name), ty: Some(member.ty), builtin: false, + is_type_check_only: member.is_type_check_only, }) .collect() } @@ -304,6 +308,7 @@ impl<'db> SemanticModel<'db> { name: CompactString::new(memberdef.member.name), ty: Some(memberdef.member.ty), builtin: false, + is_type_check_only: memberdef.member.is_type_check_only, }, ), ); @@ -318,6 +323,7 @@ impl<'db> SemanticModel<'db> { name: CompactString::new(name), ty: Some(ty), builtin: true, + is_type_check_only: false, }), ); @@ -326,17 +332,24 @@ impl<'db> SemanticModel<'db> { let importing_file = ImportingFile::File(self.file(), self.file.resolver_environment(self.db)); if resolve_module(self.db, importing_file, &project_builtins).is_some() { - completions.extend(self.module_completions(&project_builtins).into_iter().map( - |mut completion| { - completion.builtin = true; - completion - }, - )); + completions.extend( + self.module_completions(&project_builtins) + .into_iter() + .filter(|completion| !completion.is_type_check_only) + .map(|mut completion| { + completion.builtin = true; + completion + }), + ); } // Builtins are available in all scopes. let builtins = KnownModule::Builtins.name(); - completions.extend(self.module_completions(&builtins)); + completions.extend( + self.module_completions(&builtins) + .into_iter() + .filter(|completion| !completion.is_type_check_only), + ); // The above can sometimes result in duplicates. Get rid of them. completions.sort_by(|c1, c2| c1.name.cmp(&c2.name)); @@ -725,6 +738,9 @@ pub struct Completion<'db> { /// use it mainly in tests so that we can write less /// noisy tests. pub builtin: bool, + /// Whether this symbol is known to exist only for type checking and should + /// be ranked below runtime values. + pub is_type_check_only: bool, } #[derive(Clone, Debug)] diff --git a/crates/ty_python_semantic/src/types/list_members.rs b/crates/ty_python_semantic/src/types/list_members.rs index c5f270fa3d..0ee6f36c46 100644 --- a/crates/ty_python_semantic/src/types/list_members.rs +++ b/crates/ty_python_semantic/src/types/list_members.rs @@ -17,7 +17,7 @@ use crate::{ place_from_declarations, }, types::{ - ClassBase, ClassLiteral, KnownClass, KnownFunction, ProgramEnvironment, StaticClassLiteral, + ClassBase, ClassLiteral, KnownClass, ProgramEnvironment, StaticClassLiteral, SubclassOfInner, Type, TypeVarBoundOrConstraints, class::CodeGeneratorKind, exists_at_runtime, }, @@ -52,6 +52,7 @@ pub(crate) fn all_end_of_scope_members<'db>( let member = Member { name: symbol.name().clone(), ty, + is_type_check_only: false, }; Some(MemberWithDefinition { member, @@ -72,6 +73,7 @@ pub(crate) fn all_end_of_scope_members<'db>( let member = Member { name: symbol.name().clone(), ty, + is_type_check_only: false, }; Some(MemberWithDefinition { member, @@ -109,6 +111,7 @@ pub(crate) fn all_reachable_members<'db>( let member = Member { name: symbol.name().clone(), ty, + is_type_check_only: false, }; Some(MemberWithDefinition { member, @@ -125,6 +128,7 @@ pub(crate) fn all_reachable_members<'db>( let member = Member { name: symbol.name().clone(), ty, + is_type_check_only: false, }; Some(MemberWithDefinition { member, @@ -419,6 +423,7 @@ impl<'db> AllMembers<'db> { self.members.insert(Member { name: Name::new_static("__file__"), ty: dunder_file_type, + is_type_check_only: false, }); self.extend_with_type(db, env, KnownClass::ModuleType.to_instance(db, env)); @@ -440,25 +445,13 @@ impl<'db> AllMembers<'db> { continue; }; - if let Some(definition) = defined.provenance.definition() - && !exists_at_runtime(db, definition) - // Source-module completions retain `@type_check_only` symbols and rank them - // lower. - && (file.is_stub(db) || !defined.ty.is_type_check_only(db)) - // The decorator itself is typing-only, but users must still be able to - // import it when defining typing-only classes and functions. - && !matches!( - defined.ty, - Type::FunctionLiteral(function) - if function.known(db) == Some(KnownFunction::TypeCheckOnly) - ) - { - continue; - } - self.members.insert(Member { name: symbol_name.clone(), ty: defined.ty, + is_type_check_only: defined + .provenance + .definition() + .is_some_and(|definition| !exists_at_runtime(db, definition)), }); } @@ -467,7 +460,11 @@ impl<'db> AllMembers<'db> { |submodule_name| { let ty = literal.resolve_submodule(db, &submodule_name)?; let name = submodule_name.clone(); - Some(Member { name, ty }) + Some(Member { + name, + ty, + is_type_check_only: false, + }) }, )); } @@ -516,6 +513,7 @@ impl<'db> AllMembers<'db> { self.members.insert(Member { name: memberdef.member.name, ty, + is_type_check_only: memberdef.member.is_type_check_only, }); } } @@ -565,6 +563,7 @@ impl<'db> AllMembers<'db> { self.members.insert(Member { name: Name::new(name), ty, + is_type_check_only: false, }); } } @@ -582,6 +581,7 @@ impl<'db> AllMembers<'db> { self.members.insert(Member { name: memberdef.member.name, ty, + is_type_check_only: memberdef.member.is_type_check_only, }); } } @@ -644,6 +644,7 @@ impl<'db> AllMembers<'db> { self.members.insert(Member { name: Name::from(*attr), ty: synthetic_member, + is_type_check_only: false, }); } } @@ -677,6 +678,8 @@ pub struct MemberWithDefinition<'db> { pub struct Member<'db> { pub(crate) name: Name, pub(crate) ty: Type<'db>, + /// Whether this member is known to exist only during type checking. + pub(crate) is_type_check_only: bool, } impl std::hash::Hash for Member<'_> { From 721f3104c72693b7800b2a6b70020bfae552f2ce Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 4 Aug 2026 16:17:29 +0200 Subject: [PATCH 234/390] [ty] Remove deprecated `src.root` setting in favor of `environment.root` (#27456) --- crates/ty/docs/configuration.md | 37 -------- crates/ty/tests/cli/python_environment.rs | 107 ---------------------- crates/ty_project/src/metadata.rs | 42 +++++---- crates/ty_project/src/metadata/options.rs | 58 +----------- ty.schema.json | 12 --- 5 files changed, 25 insertions(+), 231 deletions(-) diff --git a/crates/ty/docs/configuration.md b/crates/ty/docs/configuration.md index fcd90c42b1..94143a37b7 100644 --- a/crates/ty/docs/configuration.md +++ b/crates/ty/docs/configuration.md @@ -1054,43 +1054,6 @@ Enabled by default. --- -### `root` - -!!! warning "Deprecated" - This option has been deprecated. Use `environment.root` instead. - -The root of the project, used for finding first-party modules. - -If left unspecified, ty will try to detect common project layouts and initialize `src.root` accordingly. -The project root (`.`) is always included. Additionally, the following directories are included -if they exist and are not packages (i.e. they do not contain `__init__.py` or `__init__.pyi` files): - -* `./src` -* `./` (if a `.//` directory exists) -* `./python` - -**Default value**: `null` - -**Type**: `str` - -**Example usage**: - -=== "pyproject.toml" - - ```toml - [tool.ty.src] - root = "./app" - ``` - -=== "ty.toml" - - ```toml - [src] - root = "./app" - ``` - ---- - ## `terminal` ### `error-on-warning` diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs index 2f20a098af..e7966e5d83 100644 --- a/crates/ty/tests/cli/python_environment.rs +++ b/crates/ty/tests/cli/python_environment.rs @@ -2205,113 +2205,6 @@ fn ty_system_environment_and_local_venv() -> anyhow::Result<()> { Ok(()) } -#[test] -fn src_root_deprecation_warning() -> anyhow::Result<()> { - let case = CliTest::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.src] - root = "./src" - "#, - ), - ("src/test.py", ""), - ])?; - - assert_cmd_snapshot!(case.command(), @r#" - success: false - exit_code: 1 - ----- stdout ----- - warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead. - --> pyproject.toml:3:8 - | - 3 | root = "./src" - | ^^^^^^^ - - Found 1 diagnostic - - ----- stderr ----- - "#); - - Ok(()) -} - -#[test] -fn src_root_deprecation_warning_with_environment_root() -> anyhow::Result<()> { - let case = CliTest::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.src] - root = "./src" - - [tool.ty.environment] - root = ["./app"] - "#, - ), - ("app/test.py", ""), - ])?; - - assert_cmd_snapshot!(case.command(), @r#" - success: false - exit_code: 1 - ----- stdout ----- - warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead. - --> pyproject.toml:3:8 - | - 3 | root = "./src" - | ^^^^^^^ - info: The `src.root` setting was ignored in favor of the `environment.root` setting - - Found 1 diagnostic - - ----- stderr ----- - "#); - - Ok(()) -} - -#[test] -fn environment_root_takes_precedence_over_src_root() -> anyhow::Result<()> { - let case = CliTest::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.src] - root = "./src" - - [tool.ty.environment] - root = ["./app"] - "#, - ), - ("src/test.py", "import my_module"), - ( - "app/my_module.py", - "# This module exists in app/ but not src/", - ), - ])?; - - // The test should pass because environment.root points to ./app where my_module.py exists - // If src.root took precedence, it would fail because my_module.py doesn't exist in ./src - assert_cmd_snapshot!(case.command(), @r#" - success: false - exit_code: 1 - ----- stdout ----- - warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead. - --> pyproject.toml:3:8 - | - 3 | root = "./src" - | ^^^^^^^ - info: The `src.root` setting was ignored in favor of the `environment.root` setting - - Found 1 diagnostic - - ----- stderr ----- - "#); - - Ok(()) -} - #[test] fn default_root_src_layout() -> anyhow::Result<()> { let case = CliTest::with_files([ diff --git a/crates/ty_project/src/metadata.rs b/crates/ty_project/src/metadata.rs index c7e1a914ef..e97c390a4d 100644 --- a/crates/ty_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -769,8 +769,8 @@ unclosed table, expected `]` [project] name = "project-root" - [tool.ty.src] - root = "src" + [tool.ty.environment] + root = ["src"] "#, ), ( @@ -779,8 +779,8 @@ unclosed table, expected `]` [project] name = "nested-project" - [tool.ty.src] - root = "src" + [tool.ty.environment] + root = ["src"] "#, ), ]) @@ -794,8 +794,10 @@ unclosed table, expected `]` name: ProjectName("nested-project"), root: "/app/packages/a", options: Options( - src: Some(SrcOptions( - root: Some("src"), + environment: Some(EnvironmentOptions( + root: Some([ + "src", + ]), )), ), ) @@ -819,8 +821,8 @@ unclosed table, expected `]` [project] name = "project-root" - [tool.ty.src] - root = "src" + [tool.ty.environment] + root = ["src"] "#, ), ( @@ -829,8 +831,8 @@ unclosed table, expected `]` [project] name = "nested-project" - [tool.ty.src] - root = "src" + [tool.ty.environment] + root = ["src"] "#, ), ]) @@ -844,8 +846,10 @@ unclosed table, expected `]` name: ProjectName("project-root"), root: "/app", options: Options( - src: Some(SrcOptions( - root: Some("src"), + environment: Some(EnvironmentOptions( + root: Some([ + "src", + ]), )), ), ) @@ -1221,15 +1225,15 @@ unclosed table, expected `]` name = "super-app" requires-python = ">=3.12" - [tool.ty.src] - root = "this_option_is_ignored" + [tool.ty.environment] + root = ["this_option_is_ignored"] "#, ), ( root.join("ty.toml"), r#" - [src] - root = "src" + [environment] + root = ["src"] "#, ), ]) @@ -1244,11 +1248,11 @@ unclosed table, expected `]` root: "/app", options: Options( environment: Some(EnvironmentOptions( + root: Some([ + "src", + ]), r#python-version: Some(r#3.12), )), - src: Some(SrcOptions( - root: Some("src"), - )), ), ) "#); diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index a07a8ce03a..446bef4a56 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -307,14 +307,8 @@ impl Options { strategy: &Strategy, ) -> Result> { let environment = self.environment.or_default(); - let src = self.src.or_default(); - #[allow(deprecated)] - let src_roots = if let Some(roots) = environment - .root - .as_deref() - .or_else(|| Some(std::slice::from_ref(src.root.as_ref()?))) - { + let environment_roots = if let Some(roots) = environment.root.as_deref() { roots .iter() .map(|root| root.absolute(project_root, system)) @@ -410,7 +404,7 @@ impl Options { let settings = SearchPathSettings { extra_paths, - src_roots, + src_roots: environment_roots, custom_typeshed: environment .typeshed .as_ref() @@ -443,34 +437,6 @@ impl Options { let src_options = self.src.or_default(); - #[allow(deprecated)] - if let Some(src_root) = src_options.root.as_ref() { - let mut diagnostic = OptionDiagnostic::new( - DiagnosticId::DeprecatedSetting, - "The `src.root` setting is deprecated. Use `environment.root` instead.".to_string(), - Severity::Warning, - ); - - if let Some(file) = src_root - .source() - .file() - .and_then(|path| system_path_to_file(db, path).ok()) - { - diagnostic = diagnostic.with_annotation(Some(Annotation::primary( - Span::from(file).with_optional_range(src_root.range()), - ))); - } - - if self.environment.or_default().root.is_some() { - diagnostic = diagnostic.sub(SubDiagnostic::new( - SubDiagnosticSeverity::Info, - "The `src.root` setting was ignored in favor of the `environment.root` setting", - )); - } - - diagnostics.push(diagnostic); - } - let src = src_options .to_settings(db, project_root, &mut diagnostics) .map_err(|err| ToSettingsError { @@ -910,26 +876,6 @@ pub struct EnvironmentOptions { #[serde(rename_all = "kebab-case", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct SrcOptions { - /// The root of the project, used for finding first-party modules. - /// - /// If left unspecified, ty will try to detect common project layouts and initialize `src.root` accordingly. - /// The project root (`.`) is always included. Additionally, the following directories are included - /// if they exist and are not packages (i.e. they do not contain `__init__.py` or `__init__.pyi` files): - /// - /// * `./src` - /// * `./` (if a `.//` directory exists) - /// * `./python` - #[serde(skip_serializing_if = "Option::is_none")] - #[option( - default = r#"null"#, - value_type = "str", - example = r#" - root = "./app" - "# - )] - #[deprecated(note = "Use `environment.root` instead.")] - pub root: Option, - /// Whether to automatically exclude files that are ignored by `.ignore`, /// `.gitignore`, `.git/info/exclude`, and global `gitignore` files. /// Enabled by default. diff --git a/ty.schema.json b/ty.schema.json index 58818ff729..aafd177dd8 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -1610,18 +1610,6 @@ "boolean", "null" ] - }, - "root": { - "description": "The root of the project, used for finding first-party modules.\n\nIf left unspecified, ty will try to detect common project layouts and initialize `src.root` accordingly.\nThe project root (`.`) is always included. Additionally, the following directories are included\nif they exist and are not packages (i.e. they do not contain `__init__.py` or `__init__.pyi` files):\n\n* `./src`\n* `./` (if a `.//` directory exists)\n* `./python`", - "anyOf": [ - { - "$ref": "#/definitions/RelativePathBuf" - }, - { - "type": "null" - } - ], - "deprecated": true } }, "additionalProperties": false From 9f45b2e85f48b7c236132310f26aa1ef90c6c56e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Aug 2026 10:28:05 -0400 Subject: [PATCH 235/390] [ty] Fix specialization cycle with deferred TypeVar defaults (#27453) ## Summary On Python 3.14, deferred annotations allow a function to reference a `TypeVar` declared after calls to that function: ```python class C: ... def f(a: T): ... if f(): pass if f(): sum() else: sum() from typing import TypeVar T = TypeVar("T", default=C) ``` Previously, resolving `T`'s default during generic specialization re-entered the same Salsa query through reachability analysis, causing a dependency-cycle panic. We now initialize specialization cycle recovery with unknown type arguments, allowing Salsa to complete the surrounding inference without evaluating the default recursively. Once inference stabilizes, the actual default remains `C` and we report the expected argument and overload diagnostics. Closes https://github.com/astral-sh/ty/issues/4174. --- .../mdtest/generics/legacy/variables.md | 65 +++++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 16 ++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md index 86e7fc9f4a..c58be073c8 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -1021,6 +1021,71 @@ reveal_type(D().x) # revealed: Unknown ## Regression +### Specialization cycle recovery preserves concrete defaults + +When a generic call uses a type variable's default, cycle recovery must allow the initial `Unknown` +specialization to resolve to the concrete default. + +```toml +[environment] +python-version = "3.14" +``` + +```py +class C: + pass + +def f(a: T | None = None) -> T: + raise NotImplementedError + +if f(): + pass + +if f(): + sum() # error: [no-matching-overload] +else: + sum() # error: [no-matching-overload] + +from typing import TypeVar + +T = TypeVar("T", default=C) + +reveal_type(f()) # revealed: C +``` + +### Specialization cycle recovery prevents oscillating defaults + +A type variable's default can depend on an overloaded call that itself uses the same type variable. +Specialization must converge even when overload selection changes between cycle iterations. + +```toml +[environment] +python-version = "3.14" +``` + +```py +from typing import TypeVar, overload + +@overload +def choose(value: int) -> type[int]: ... +@overload +def choose(value: object) -> type[str]: ... +def choose(value: object) -> type[int] | type[str]: + return str + +def f() -> T: + raise NotImplementedError + +if f(): + Default = str +else: + Default = choose(f()) + +T = TypeVar("T", default=Default) + +reveal_type(f()) # revealed: Unknown +``` + ### Use of typevar with default inside a function body that binds it ```toml diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index d99af4f849..4eff6afd92 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -2380,7 +2380,21 @@ impl get_size2::GetSize for TypeVarInference<'_> {} impl<'db> TypeVarInference<'db> { /// Project this inference result into a closed specialization. pub(crate) fn specialization(self, db: &'db dyn Db) -> Specialization<'db> { - #[salsa::tracked(returns(copy))] + #[salsa::tracked( + returns(copy), + cycle_initial=|db, _, inference: TypeVarInference<'db>| { + inference.generic_context(db).unknown_specialization(db, None) + }, + cycle_fn=|db, cycle: &salsa::Cycle, previous: &Specialization<'db>, current: Specialization<'db>, inference: TypeVarInference<'db>| { + if cycle.iteration() <= crate::TAINTED_CYCLES { + current + } else { + current + .merge_cycle_recovery(db, *previous) + .unwrap_or_else(|| inference.generic_context(db).unknown_specialization(db, None)) + } + } + )] fn specialization_inner<'db>( db: &'db dyn Db, inference: TypeVarInference<'db>, From 3f5093504284c4d00456249fbc76f1ec1a7f45ce Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 4 Aug 2026 10:30:49 -0400 Subject: [PATCH 236/390] [ty] Extract constraint-set-related benchmarks into separate file (#27459) This PR moves the benchmarks that exercise our constraint set implementation into their own file. This is a refactoring pulled out of #27337. The source file is included in Codspeed's benchmark ID, so moving them like this disconnects these benchmarks from their history. Doing the move in a separate PR means that #27337 will at least show an accurate delta for the existing benchmarks relative to `main`. --- .github/workflows/ci.yaml | 10 +- crates/ruff_benchmark/Cargo.toml | 5 + crates/ruff_benchmark/benches/ty.rs | 693 +----------------- .../benches/ty_constraint_set.rs | 578 +++++++++++++++ .../ruff_benchmark/benches/ty_shared/mod.rs | 79 ++ 5 files changed, 703 insertions(+), 662 deletions(-) create mode 100644 crates/ruff_benchmark/benches/ty_constraint_set.rs create mode 100644 crates/ruff_benchmark/benches/ty_shared/mod.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d9b7e3130a..5ac4bdaeff 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1127,7 +1127,7 @@ jobs: tool: cargo-codspeed - name: "Build benchmarks" - run: cargo codspeed build -m simulation -m memory --features "codspeed,module_resolution,ty_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench module_resolution --bench ty + run: cargo codspeed build -m simulation -m memory --features "codspeed,module_resolution,ty_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench module_resolution --bench ty --bench ty_constraint_set - name: "Upload benchmark binary" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -1158,6 +1158,14 @@ jobs: target: ty filter: micro mode: memory + - name: constraint_set + target: ty_constraint_set + filter: micro + mode: simulation + - name: constraint_set + target: ty_constraint_set + filter: micro + mode: memory - name: projects target: ty filter: "check_file|anyio|attrs|hydra|datetype" diff --git a/crates/ruff_benchmark/Cargo.toml b/crates/ruff_benchmark/Cargo.toml index cf1a0fafd1..cbb817b668 100644 --- a/crates/ruff_benchmark/Cargo.toml +++ b/crates/ruff_benchmark/Cargo.toml @@ -90,6 +90,11 @@ name = "ty" harness = false required-features = ["ty_instrumented"] +[[bench]] +name = "ty_constraint_set" +harness = false +required-features = ["ty_instrumented"] + [[bench]] name = "module_resolution" harness = false diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index e4e2322ee6..8900b21aa8 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -1,16 +1,11 @@ #![allow(clippy::disallowed_names)] use ruff_benchmark::criterion; -use ruff_benchmark::real_world_projects::{ - InstalledProject, RealWorldProject, TY_ECOSYSTEM_PIN, copy_directory_recursive, - get_project_cache_dir, install_dependencies_to_cache, -}; +use ruff_benchmark::real_world_projects::{InstalledProject, RealWorldProject, TY_ECOSYSTEM_PIN}; use std::fmt::Write; use std::ops::Range; -use std::path::{Path, PathBuf}; use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; -use rayon::ThreadPoolBuilder; use rustc_hash::FxHashSet; use ruff_benchmark::TestFile; @@ -25,7 +20,11 @@ use ty_project::metadata::value::RelativePathBuf; use ty_project::watch::{ChangeEvent, ChangedKind}; use ty_project::{CheckMode, Db, ProjectDatabase, ProjectMetadata}; -struct Case { +mod ty_shared; + +use ty_shared::{Case, setup_micro_case, setup_rayon}; + +struct FileCase { db: ProjectDatabase, fs: MemoryFileSystem, file: File, @@ -72,7 +71,7 @@ fn tomllib_path(file: &TestFile) -> SystemPathBuf { SystemPathBuf::from("src").join(file.name()) } -fn setup_tomllib_case() -> Case { +fn setup_tomllib_case() -> FileCase { let system = TestSystem::default(); let fs = system.memory_file_system().clone(); @@ -115,7 +114,7 @@ fn setup_tomllib_case() -> Case { db.project().set_open_files(&mut db, tomllib_files); let re_path = re.path(&db).as_system_path().unwrap().to_owned(); - Case { + FileCase { db, fs, file: re, @@ -123,24 +122,8 @@ fn setup_tomllib_case() -> Case { } } -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 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() - .num_threads(1) - .use_current_thread() - .build_global() - .unwrap(); - }); -} - fn benchmark_incremental(criterion: &mut Criterion) { - fn setup() -> Case { + fn setup() -> FileCase { let case = setup_tomllib_case(); let result: Vec<_> = case.db.check(); @@ -160,8 +143,8 @@ fn benchmark_incremental(criterion: &mut Criterion) { case } - fn incremental(case: &mut Case) { - let Case { db, .. } = case; + fn incremental(case: &mut FileCase) { + let FileCase { db, .. } = case; db.apply_changes(&[ChangeEvent::Changed { path: case.file_path.clone(), @@ -187,7 +170,7 @@ fn benchmark_cold(criterion: &mut Criterion) { b.iter_batched_ref( setup_tomllib_case, |case| { - let Case { db, .. } = case; + let FileCase { db, .. } = case; let result: Vec<_> = db.check(); assert_diagnostics(db, &result, EXPECTED_TOMLLIB_DIAGNOSTICS); @@ -220,76 +203,6 @@ fn assert_diagnostics(db: &dyn Db, diagnostics: &[Diagnostic], expected: &[KeyDi assert_eq!(&normalized, expected); } -fn setup_micro_case(code: &str) -> Case { - setup_micro_case_inner(code, None) -} - -fn setup_micro_case_venv(name: &str, dependencies: &[&str]) -> PathBuf { - let cache_dir = get_project_cache_dir(name).expect("Failed to get cache directory"); - std::fs::create_dir_all(&cache_dir).expect("Failed to create cache directory"); - - let venv_path = cache_dir.join(".venv"); - install_dependencies_to_cache( - name, - dependencies, - &venv_path, - SupportedPythonVersion::Py312, - TY_ECOSYSTEM_PIN, - ) - .expect("Failed to install dependencies"); - - venv_path -} - -fn setup_micro_case_inner(code: &str, venv_path: Option<&Path>) -> Case { - let system = TestSystem::default(); - let fs = system.memory_file_system().clone(); - - let python = venv_path.map(|venv_path| { - // Copy the on-disk venv into the in-memory filesystem. - // ProjectMetadata::discover walks up from /src and uses / as the project root, - // so the venv must be at /.venv for the `python = ".venv"` option to resolve correctly. - copy_directory_recursive(&fs, venv_path, SystemPath::new("/.venv")) - .expect("Failed to copy venv to memory filesystem"); - - RelativePathBuf::cli(SystemPath::new(".venv")) - }); - - let file_path = "src/test.py"; - fs.write_file_all( - SystemPathBuf::from(file_path), - &*ruff_python_trivia::textwrap::dedent(code), - ) - .unwrap(); - - let src_root = SystemPath::new("/src"); - let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap(); - metadata.apply_override_options(Options { - environment: Some(EnvironmentOptions { - python_version: Some(RangedValue::cli(SupportedPythonVersion::Py312)), - python, - ..EnvironmentOptions::default() - }), - ..Options::default() - }); - - let mut db = ProjectDatabase::fallible(metadata, system).unwrap(); - let file = system_path_to_file(&db, SystemPathBuf::from(file_path)).unwrap(); - - db.set_check_mode(CheckMode::OpenFiles); - db.project() - .set_open_files(&mut db, FxHashSet::from_iter([file])); - - let file_path = file.path(&db).as_system_path().unwrap().to_owned(); - - Case { - db, - fs, - file, - file_path, - } -} - fn benchmark_many_string_assignments(criterion: &mut Criterion) { setup_rayon(); @@ -334,7 +247,7 @@ fn benchmark_many_string_assignments(criterion: &mut Criterion) { ) }, |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -380,7 +293,7 @@ fn benchmark_many_tuple_assignments(criterion: &mut Criterion) { ) }, |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -415,7 +328,7 @@ fn benchmark_tuple_implicit_instance_attributes(criterion: &mut Criterion) { ) }, |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -467,7 +380,7 @@ fn benchmark_complex_constrained_attributes_1(criterion: &mut Criterion) { ) }, |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert!(!result.is_empty()); }, @@ -512,7 +425,7 @@ fn benchmark_complex_constrained_attributes_2(criterion: &mut Criterion) { ) }, |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -556,7 +469,7 @@ fn benchmark_complex_constrained_attributes_3(criterion: &mut Criterion) { ) }, |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -588,7 +501,7 @@ fn benchmark_many_enum_members(criterion: &mut Criterion) { b.iter_batched_ref( || setup_micro_case(&code), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -625,7 +538,7 @@ fn benchmark_large_enum_membership(criterion: &mut Criterion) { b.iter_batched_ref( || setup_micro_case(&code), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -641,7 +554,7 @@ fn benchmark_enum_comparison(criterion: &mut Criterion, name: &str, code: &str) b.iter_batched_ref( || setup_micro_case(code), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -830,7 +743,7 @@ for msg in translations_tuple: b.iter_batched_ref( || setup_micro_case(&code), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -882,7 +795,7 @@ class E(Enum): b.iter_batched_ref( || setup_micro_case(&code), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -925,7 +838,7 @@ fn benchmark_many_protocol_members_mismatch(criterion: &mut Criterion) { b.iter_batched_ref( || setup_micro_case(&code), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), NUM_FUNCTIONS); }, @@ -963,7 +876,7 @@ accepts_anything( b.iter_batched_ref( || setup_micro_case(&code), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -999,79 +912,7 @@ accepts_objects( b.iter_batched_ref( || setup_micro_case(&code), |case| { - let Case { db, .. } = case; - let result = db.check(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }); -} - -/// Regression benchmark for many precise arguments constraining the same type variable. -/// -/// The parameters are distinct to avoid exercising the `*args` parameter-type accumulator. The -/// important part is that specialization inference should not repeatedly rebuild a growing union -/// for `T` as each argument adds another solution. -fn benchmark_typevar_mapping_large_accumulation(criterion: &mut Criterion) { - const NUM_ARGUMENTS: usize = 256; - - setup_rayon(); - - let mut code = "def combine[T](\n".to_string(); - for i in 0..NUM_ARGUMENTS { - writeln!(&mut code, " p{i}: T,").ok(); - } - code.push_str(") -> T:\n return p0\n\ncombine(\n"); - - for i in 0..NUM_ARGUMENTS { - writeln!(&mut code, r#" ("field_{i}", {i}),"#).ok(); - } - - code.push_str(")\n"); - - criterion.bench_function("ty_micro[typevar_mapping_accumulation]", |b| { - b.iter_batched_ref( - || setup_micro_case(&code), - |case| { - let Case { db, .. } = case; - let result = db.check(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }); -} - -/// Benchmark for many small type-variable accumulations. -/// -/// This guards the common case where each type variable only receives a few constraints. Optimizing -/// the large-accumulation case should not make these small generic calls slower. -fn benchmark_typevar_mapping_small_accumulations(criterion: &mut Criterion) { - const NUM_CALLS: usize = 256; - - setup_rayon(); - - let mut code = "\ -def combine[T](first: T, second: T, third: T) -> T: - return first - -" - .to_string(); - - for i in 0..NUM_CALLS { - writeln!( - &mut code, - r#"combine(("field_{i}", {i}), ("other_{i}", "{i}"), ("flag_{i}", True))"# - ) - .ok(); - } - - criterion.bench_function("ty_micro[typevar_mapping_small_accumulations]", |b| { - b.iter_batched_ref( - || setup_micro_case(&code), - |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -1135,7 +976,7 @@ fn benchmark_large_union_narrowing(criterion: &mut Criterion) { b.iter_batched_ref( || setup_micro_case(&code), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -1189,7 +1030,7 @@ fn benchmark_large_isinstance_narrowing(criterion: &mut Criterion) { b.iter_batched_ref( || setup_micro_case(&code), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -1404,7 +1245,7 @@ fn benchmark_literal_or_pattern_reachability(criterion: &mut Criterion) { b.iter_batched_ref( || setup_micro_case(&code), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -1420,7 +1261,7 @@ fn benchmark_literal_fallthrough(criterion: &mut Criterion, name: &str, code: &s b.iter_batched_ref( || setup_micro_case(code), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -1442,7 +1283,7 @@ fn benchmark_typeis_narrowing(criterion: &mut Criterion) { b.iter_batched_ref( || setup_micro_case(include_str!("../resources/typeis_narrowing.py")), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -1486,7 +1327,7 @@ fn benchmark_repeated_statement_calls(criterion: &mut Criterion) { b.iter_batched_ref( || setup_micro_case(&code), |case| { - let Case { db, .. } = case; + let Case { db } = case; let result = db.check(); assert_eq!(result.len(), 0); }, @@ -1496,464 +1337,6 @@ fn benchmark_repeated_statement_calls(criterion: &mut Criterion) { } } -/// Benchmarks solving many union-bearing upper bounds while inferring a generic call. -/// -/// Each callable argument places a distinct union upper bound on `T` through callable-parameter -/// contravariance. Fully materializing the conjunction of these bounds would require constructing -/// the cross product of all union alternatives. Factored path bounds and bounded intersection -/// keep the work bounded instead. -fn benchmark_factored_upper_bounds(criterion: &mut Criterion) { - const NUM_CLAUSES: usize = 12; - const ALTERNATIVES_PER_CLAUSE: usize = 8; - - setup_rayon(); - - let mut code = "from collections.abc import Callable\n\n".to_string(); - for clause in 0..NUM_CLAUSES { - for alternative in 0..ALTERNATIVES_PER_CLAUSE { - writeln!(&mut code, "class C{clause}_{alternative}: ...").ok(); - } - } - - code.push_str("\ndef infer[T](\n"); - for clause in 0..NUM_CLAUSES { - writeln!(&mut code, " consumer{clause}: Callable[[T], None],").ok(); - } - code.push_str(") -> T:\n raise NotImplementedError\n\n"); - - for clause in 0..NUM_CLAUSES { - write!(&mut code, "def consume{clause}(value: ").ok(); - for alternative in 0..ALTERNATIVES_PER_CLAUSE { - if alternative > 0 { - code.push_str(" | "); - } - write!(&mut code, "C{clause}_{alternative}").ok(); - } - code.push_str(") -> None: ...\n"); - } - - code.push_str("\nresult = infer(\n"); - for clause in 0..NUM_CLAUSES { - writeln!(&mut code, " consume{clause},").ok(); - } - code.push_str(")\n"); - - criterion.bench_function("ty_micro[factored_upper_bounds]", |b| { - b.iter_batched_ref( - || setup_micro_case(&code), - |case| { - let Case { db, .. } = case; - let result = db.check(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }); -} - -/// Guards against quadratic pruning when contravariant callbacks contribute many upper-only bounds. -fn benchmark_many_upper_bound_callbacks(criterion: &mut Criterion) { - const NUM_CALLBACKS: usize = 1_200; - - setup_rayon(); - - let mut code = String::from( - "from collections.abc import Callable\nfrom typing import Literal\n\ndef accepts[T](\n", - ); - for i in 0..NUM_CALLBACKS { - writeln!(&mut code, " cb{i}: Callable[[T], None],").ok(); - } - code.push_str(") -> None: ...\n\ndef call_many(\n"); - for i in 0..NUM_CALLBACKS { - writeln!(&mut code, " cb{i}: Callable[[Literal[{i}]], None],").ok(); - } - code.push_str(") -> None:\n accepts(\n"); - for i in 0..NUM_CALLBACKS { - writeln!(&mut code, " cb{i},").ok(); - } - code.push_str(" )\n"); - - criterion.bench_function("ty_micro[many_upper_bound_callbacks]", |b| { - b.iter_batched_ref( - || setup_micro_case(&code), - |case| { - let Case { db, .. } = case; - let result = db.check(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }); -} - -fn benchmark_pandas_tdd(criterion: &mut Criterion) { - setup_rayon(); - let venv_path = setup_micro_case_venv("pandas_tdd", &["pandas-stubs"]); - let code = r#" - import pandas as pd - - df = pd.DataFrame({ - "a": [1, 2, 3], - "b": [4, 5, 6], - "c": [7, 8, 9], - }) - df["d"] = df["a"] + df["b"] + df["c"] + 1 + ( - df["a"] ** 2 + df["b"] ** 2 + df["c"] ** 2) - "#; - - // This example was reported in https://github.com/astral-sh/ty/issues/3039. - criterion.bench_function("ty_micro[pandas_tdd]", |b| { - b.iter_batched_ref( - || setup_micro_case_inner(code, Some(&venv_path)), - |case| { - let Case { db, .. } = case; - let result = db.check(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }); -} - -fn benchmark_mixed_typed_dict_union_copy(criterion: &mut Criterion) { - const NUM_VARIANTS: usize = 12; - - setup_rayon(); - - let mut code = concat!( - "from collections import ChainMap, OrderedDict, defaultdict\n", - "from collections.abc import Mapping, MutableMapping\n", - "from typing import Any, Literal, TypedDict\n\n", - ) - .to_string(); - - for i in 0..NUM_VARIANTS { - writeln!( - &mut code, - "class Item{i}(TypedDict):\n type: Literal[{i}]" - ) - .ok(); - if i == 0 { - code.push_str(" other: Any\n"); - } - code.push('\n'); - } - - code.push_str("type Item = "); - for i in 0..NUM_VARIANTS { - if i > 0 { - code.push_str(" | "); - } - write!(&mut code, "Item{i}").ok(); - } - - code.push_str( - r#" - -def copy_dict(value: Item | dict[str, Any]) -> dict[str, object]: - return dict(value) - -def copy_mapping(value: Item | Mapping[str, Any]) -> dict[str, object]: - return dict(value) - -def copy_mutable_mapping(value: Item | MutableMapping[str, Any]) -> dict[str, object]: - return dict(value) - -def copy_ordered_dict(value: Item | OrderedDict[str, Any]) -> dict[str, object]: - return dict(value) - -def copy_default_dict(value: Item | defaultdict[str, Any]) -> dict[str, object]: - return dict(value) - -def copy_chain_map(value: Item | ChainMap[str, Any]) -> dict[str, object]: - return dict(value) - -def copy_narrowed_mapping(value: Item | Mapping[str, Any]) -> dict[str, object] | None: - if isinstance(value, dict): - return dict(value) - return None -"#, - ); - - criterion.bench_function("ty_micro[mixed_typed_dict_union_copy]", |b| { - b.iter_batched_ref( - || setup_micro_case(&code), - |case| { - let Case { db, .. } = case; - let result = db.check(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }); -} - -fn benchmark_recursive_typed_dict_union_contextual_inference(criterion: &mut Criterion) { - const NUM_BRANCHES: usize = 11; - - setup_rayon(); - - // Regression benchmark for https://github.com/astral-sh/ty/issues/3663. - let mut code = "from typing import Literal, TypedDict\n\n".to_string(); - for i in 0..NUM_BRANCHES { - writeln!( - &mut code, - "class Node{i}(TypedDict):\n type: Literal['node-{i}']\n children: list['Node']\n" - ) - .ok(); - } - code.push_str("class Leaf(TypedDict):\n type: Literal['leaf']\n text: str\n\n"); - code.push_str("type Node = "); - for i in 0..NUM_BRANCHES { - if i > 0 { - code.push_str(" | "); - } - write!(&mut code, "Node{i}").ok(); - } - code.push_str( - r#" | Leaf - -value: list[Node] = [ - {"type": "node-0", "children": [ - {"type": "node-1", "children": [ - {"type": "node-2", "children": [{"type": "leaf", "text": "x"}]}, - {"type": "node-3", "children": [{"type": "leaf", "text": "y"}]}, - ]}, - {"type": "node-4", "children": [ - {"type": "node-5", "children": [{"type": "leaf", "text": "z"}]}, - {"type": "node-6", "children": [{"type": "leaf", "text": "w"}]}, - ]}, - ]}, -] -"#, - ); - - criterion.bench_function( - "ty_micro[recursive_typed_dict_union_contextual_inference]", - |b| { - b.iter_batched_ref( - || setup_micro_case(&code), - |case| { - let Case { db, .. } = case; - let result = db.check(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }, - ); -} - -fn benchmark_invariant_generic_return_union(criterion: &mut Criterion) { - const NUM_VARIANTS: usize = 21; - - setup_rayon(); - - // Regression benchmark for https://github.com/astral-sh/ty/issues/3896. - let mut code = String::new(); - for i in 0..NUM_VARIANTS { - writeln!(&mut code, "class M{i}: pass").ok(); - } - code.push_str("\nAllResults = (\n"); - for i in 0..NUM_VARIANTS { - if i > 0 { - code.push_str(" |\n"); - } - write!(&mut code, " dict[int, M{i}]").ok(); - } - code.push_str("\n)\n\nRows = (\n"); - for i in 0..NUM_VARIANTS { - if i > 0 { - code.push_str(" |\n"); - } - write!(&mut code, " list[tuple[int, M{i}]]").ok(); - } - code.push_str( - r#" -) - -def map_rows[T](rows: list[tuple[int, T]]) -> dict[int, T]: - return {} - -def perform(rows: Rows) -> AllResults: - return map_rows(rows) -"#, - ); - - criterion.bench_function("ty_micro[invariant_generic_return_union]", |b| { - b.iter_batched_ref( - || setup_micro_case(&code), - |case| { - let Case { db, .. } = case; - let result = db.check(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }); -} - -fn benchmark_sequence_literal_union_access(criterion: &mut Criterion) { - const NUM_LITERALS: usize = 1_200; - - setup_rayon(); - - // Regression benchmark for https://github.com/astral-sh/ty/issues/4089. - let mut code = String::from( - "from collections.abc import Sequence\nfrom typing import Literal\n\nItem = Literal[\n", - ); - for i in 0..NUM_LITERALS { - writeln!(&mut code, " 'value-{i}',").ok(); - } - code.push_str( - r#"] - -def iterate(items: Sequence[Item]) -> None: - for item in items: - pass - -def access(items: Sequence[Item]) -> None: - items[0] -"#, - ); - - criterion.bench_function("ty_micro[sequence_literal_union_access]", |b| { - b.iter_batched_ref( - || setup_micro_case(&code), - |case| { - let Case { db, .. } = case; - let result = db.check(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }); -} - -fn benchmark_invariant_generic_union_bound(criterion: &mut Criterion) { - const NUM_ALIASES: usize = 64; - - setup_rayon(); - - let mut code = - String::from("from collections.abc import Iterable\nfrom typing import Literal\n\n"); - for i in 0..NUM_ALIASES { - writeln!( - &mut code, - "type A{i} = Literal[{i}] | int | str | bytes | float" - ) - .ok(); - } - code.push_str("\nALIASES = {\n"); - for i in 0..NUM_ALIASES { - writeln!(&mut code, " A{i}: {{{i}: A{i}}},").ok(); - } - code.push_str( - r#"} - -def consume(items: Iterable[object]) -> None: ... - -consume(ALIASES.items()) -"#, - ); - - criterion.bench_function("ty_micro[invariant_generic_union_bound]", |b| { - b.iter_batched_ref( - || setup_micro_case(&code), - |case| { - let Case { db, .. } = case; - let result = db.check(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }); -} - -fn benchmark_many_invariant_typevars(criterion: &mut Criterion) { - setup_rayon(); - - // Regression benchmark for https://github.com/astral-sh/ty/issues/3989. - let code = r#" -class Invariant[T]: - x: T - -def f[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]( - box1: Invariant[T1], - box2: Invariant[T2], - box3: Invariant[T3], - box4: Invariant[T4], - box5: Invariant[T5], - box6: Invariant[T6], - box7: Invariant[T7], - box8: Invariant[T8], - box9: Invariant[T9], - box10: Invariant[T10], -) -> None: ... - -x = Invariant[int]() -f(x, x, x, x, x, x, x, x, x, x) -"#; - - criterion.bench_function("ty_micro[many_invariant_typevars]", |b| { - b.iter_batched_ref( - || setup_micro_case(code), - |case| { - let Case { db, .. } = case; - let result = db.check(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }); -} -fn benchmark_pydantic_core_schema_dict(criterion: &mut Criterion) { - const NUM_CORE_SCHEMA_VARIANTS: usize = 24; - - setup_rayon(); - - // Minimized from the pydantic and hydra-zen ecosystem regressions seen during the - // SpecializationBuilder pending-constraint-set migration. Pydantic has several empty dict - // literals with a type context equivalent to `dict[Hashable, core_schema.CoreSchema]` - // (including `schema.setdefault("metadata", {})` and tagged-union choice tables). - // `CoreSchema` is a large union of TypedDict schema types; this local `CoreSchema` alias is - // derived from pydantic-core's real `CoreSchema`, but reduced to enough variants to show the - // regression quickly. Solving the empty-dict specialization creates one lower-bound constraint - // per union element for `_VT@dict`. Combined with `_KT@dict = Hashable`, - // PathAssignments/SequentMap traversal derives cross-typevar facts like - // `TypedDictSchema <= _VT@dict <= Hashable`. This benchmark tracks the cost until constraint - // projection / path-bounds solving can avoid that work. - let mut code = "from collections.abc import Hashable\nfrom typing import Literal, NotRequired, TypedDict\n\n" - .to_string(); - for i in 0..NUM_CORE_SCHEMA_VARIANTS { - writeln!( - &mut code, - "class Schema{i}(TypedDict):\n type: Literal['schema-{i}']\n ref: NotRequired[str]\n value_{i}: NotRequired[int]\n" - ) - .ok(); - } - code.push_str("type CoreSchema = "); - for i in 0..NUM_CORE_SCHEMA_VARIANTS { - if i > 0 { - code.push_str(" | "); - } - write!(&mut code, "Schema{i}").ok(); - } - code.push_str("\n\nchoices: dict[Hashable, CoreSchema] = {}\n"); - - criterion.bench_function("ty_micro[pydantic_core_schema_dict]", |b| { - b.iter_batched_ref( - || setup_micro_case(&code), - |case| { - let Case { db, .. } = case; - let result = db.check(); - assert_eq!(result.len(), 0); - }, - BatchSize::SmallInput, - ); - }); -} - struct ProjectBenchmark<'a> { project: InstalledProject<'a>, fs: MemoryFileSystem, @@ -2149,8 +1532,6 @@ criterion_group!( benchmark_many_enum_members_2, benchmark_many_protocol_members_mismatch, benchmark_vararg_parameter_type_accumulation, - benchmark_typevar_mapping_large_accumulation, - benchmark_typevar_mapping_small_accumulations, benchmark_very_large_tuple, benchmark_large_union_narrowing, benchmark_large_isinstance_narrowing, @@ -2160,16 +1541,6 @@ criterion_group!( benchmark_literal_or_pattern_reachability, benchmark_typeis_narrowing, benchmark_repeated_statement_calls, - benchmark_factored_upper_bounds, - benchmark_many_upper_bound_callbacks, - benchmark_pandas_tdd, - benchmark_mixed_typed_dict_union_copy, - benchmark_recursive_typed_dict_union_contextual_inference, - benchmark_invariant_generic_return_union, - benchmark_sequence_literal_union_access, - benchmark_invariant_generic_union_bound, - benchmark_many_invariant_typevars, - benchmark_pydantic_core_schema_dict, ); criterion_group!(project, anyio, attrs, hydra, datetype); criterion_main!(check_file, micro, project); diff --git a/crates/ruff_benchmark/benches/ty_constraint_set.rs b/crates/ruff_benchmark/benches/ty_constraint_set.rs new file mode 100644 index 0000000000..d726221e60 --- /dev/null +++ b/crates/ruff_benchmark/benches/ty_constraint_set.rs @@ -0,0 +1,578 @@ +use std::fmt::Write; +use std::path::PathBuf; + +use ruff_benchmark::criterion; +use ruff_benchmark::real_world_projects::{ + TY_ECOSYSTEM_PIN, get_project_cache_dir, install_dependencies_to_cache, +}; + +use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; +use ty_project::metadata::python_version::SupportedPythonVersion; + +mod ty_shared; + +use ty_shared::{Case, setup_micro_case, setup_micro_case_inner, setup_rayon}; + +fn setup_micro_case_venv(name: &str, dependencies: &[&str]) -> PathBuf { + let cache_dir = get_project_cache_dir(name).expect("Failed to get cache directory"); + std::fs::create_dir_all(&cache_dir).expect("Failed to create cache directory"); + + let venv_path = cache_dir.join(".venv"); + install_dependencies_to_cache( + name, + dependencies, + &venv_path, + SupportedPythonVersion::Py312, + TY_ECOSYSTEM_PIN, + ) + .expect("Failed to install dependencies"); + + venv_path +} + +/// Regression benchmark for many precise arguments constraining the same type variable. +/// +/// The parameters are distinct to avoid exercising the `*args` parameter-type accumulator. The +/// important part is that specialization inference should not repeatedly rebuild a growing union +/// for `T` as each argument adds another solution. +fn benchmark_typevar_mapping_large_accumulation(criterion: &mut Criterion) { + const NUM_ARGUMENTS: usize = 256; + + setup_rayon(); + + let mut code = "def combine[T](\n".to_string(); + for i in 0..NUM_ARGUMENTS { + writeln!(&mut code, " p{i}: T,").ok(); + } + code.push_str(") -> T:\n return p0\n\ncombine(\n"); + + for i in 0..NUM_ARGUMENTS { + writeln!(&mut code, r#" ("field_{i}", {i}),"#).ok(); + } + + code.push_str(")\n"); + + criterion.bench_function("ty_micro[typevar_mapping_accumulation]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +/// Benchmark for many small type-variable accumulations. +/// +/// This guards the common case where each type variable only receives a few constraints. Optimizing +/// the large-accumulation case should not make these small generic calls slower. +fn benchmark_typevar_mapping_small_accumulations(criterion: &mut Criterion) { + const NUM_CALLS: usize = 256; + + setup_rayon(); + + let mut code = "\ +def combine[T](first: T, second: T, third: T) -> T: + return first + +" + .to_string(); + + for i in 0..NUM_CALLS { + writeln!( + &mut code, + r#"combine(("field_{i}", {i}), ("other_{i}", "{i}"), ("flag_{i}", True))"# + ) + .ok(); + } + + criterion.bench_function("ty_micro[typevar_mapping_small_accumulations]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +/// Benchmarks solving many union-bearing upper bounds while inferring a generic call. +/// +/// Each callable argument places a distinct union upper bound on `T` through callable-parameter +/// contravariance. Fully materializing the conjunction of these bounds would require constructing +/// the cross product of all union alternatives. Factored path bounds and bounded intersection +/// keep the work bounded instead. +fn benchmark_factored_upper_bounds(criterion: &mut Criterion) { + const NUM_CLAUSES: usize = 12; + const ALTERNATIVES_PER_CLAUSE: usize = 8; + + setup_rayon(); + + let mut code = "from collections.abc import Callable\n\n".to_string(); + for clause in 0..NUM_CLAUSES { + for alternative in 0..ALTERNATIVES_PER_CLAUSE { + writeln!(&mut code, "class C{clause}_{alternative}: ...").ok(); + } + } + + code.push_str("\ndef infer[T](\n"); + for clause in 0..NUM_CLAUSES { + writeln!(&mut code, " consumer{clause}: Callable[[T], None],").ok(); + } + code.push_str(") -> T:\n raise NotImplementedError\n\n"); + + for clause in 0..NUM_CLAUSES { + write!(&mut code, "def consume{clause}(value: ").ok(); + for alternative in 0..ALTERNATIVES_PER_CLAUSE { + if alternative > 0 { + code.push_str(" | "); + } + write!(&mut code, "C{clause}_{alternative}").ok(); + } + code.push_str(") -> None: ...\n"); + } + + code.push_str("\nresult = infer(\n"); + for clause in 0..NUM_CLAUSES { + writeln!(&mut code, " consume{clause},").ok(); + } + code.push_str(")\n"); + + criterion.bench_function("ty_micro[factored_upper_bounds]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +/// Guards against quadratic pruning when contravariant callbacks contribute many upper-only bounds. +fn benchmark_many_upper_bound_callbacks(criterion: &mut Criterion) { + const NUM_CALLBACKS: usize = 1_200; + + setup_rayon(); + + let mut code = String::from( + "from collections.abc import Callable\nfrom typing import Literal\n\ndef accepts[T](\n", + ); + for i in 0..NUM_CALLBACKS { + writeln!(&mut code, " cb{i}: Callable[[T], None],").ok(); + } + code.push_str(") -> None: ...\n\ndef call_many(\n"); + for i in 0..NUM_CALLBACKS { + writeln!(&mut code, " cb{i}: Callable[[Literal[{i}]], None],").ok(); + } + code.push_str(") -> None:\n accepts(\n"); + for i in 0..NUM_CALLBACKS { + writeln!(&mut code, " cb{i},").ok(); + } + code.push_str(" )\n"); + + criterion.bench_function("ty_micro[many_upper_bound_callbacks]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +fn benchmark_pandas_tdd(criterion: &mut Criterion) { + setup_rayon(); + let venv_path = setup_micro_case_venv("pandas_tdd", &["pandas-stubs"]); + let code = r#" + import pandas as pd + + df = pd.DataFrame({ + "a": [1, 2, 3], + "b": [4, 5, 6], + "c": [7, 8, 9], + }) + df["d"] = df["a"] + df["b"] + df["c"] + 1 + ( + df["a"] ** 2 + df["b"] ** 2 + df["c"] ** 2) + "#; + + // This example was reported in https://github.com/astral-sh/ty/issues/3039. + criterion.bench_function("ty_micro[pandas_tdd]", |b| { + b.iter_batched_ref( + || setup_micro_case_inner(code, Some(&venv_path)), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +fn benchmark_mixed_typed_dict_union_copy(criterion: &mut Criterion) { + const NUM_VARIANTS: usize = 12; + + setup_rayon(); + + let mut code = concat!( + "from collections import ChainMap, OrderedDict, defaultdict\n", + "from collections.abc import Mapping, MutableMapping\n", + "from typing import Any, Literal, TypedDict\n\n", + ) + .to_string(); + + for i in 0..NUM_VARIANTS { + writeln!( + &mut code, + "class Item{i}(TypedDict):\n type: Literal[{i}]" + ) + .ok(); + if i == 0 { + code.push_str(" other: Any\n"); + } + code.push('\n'); + } + + code.push_str("type Item = "); + for i in 0..NUM_VARIANTS { + if i > 0 { + code.push_str(" | "); + } + write!(&mut code, "Item{i}").ok(); + } + + code.push_str( + r#" + +def copy_dict(value: Item | dict[str, Any]) -> dict[str, object]: + return dict(value) + +def copy_mapping(value: Item | Mapping[str, Any]) -> dict[str, object]: + return dict(value) + +def copy_mutable_mapping(value: Item | MutableMapping[str, Any]) -> dict[str, object]: + return dict(value) + +def copy_ordered_dict(value: Item | OrderedDict[str, Any]) -> dict[str, object]: + return dict(value) + +def copy_default_dict(value: Item | defaultdict[str, Any]) -> dict[str, object]: + return dict(value) + +def copy_chain_map(value: Item | ChainMap[str, Any]) -> dict[str, object]: + return dict(value) + +def copy_narrowed_mapping(value: Item | Mapping[str, Any]) -> dict[str, object] | None: + if isinstance(value, dict): + return dict(value) + return None +"#, + ); + + criterion.bench_function("ty_micro[mixed_typed_dict_union_copy]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +fn benchmark_recursive_typed_dict_union_contextual_inference(criterion: &mut Criterion) { + const NUM_BRANCHES: usize = 11; + + setup_rayon(); + + // Regression benchmark for https://github.com/astral-sh/ty/issues/3663. + let mut code = "from typing import Literal, TypedDict\n\n".to_string(); + for i in 0..NUM_BRANCHES { + writeln!( + &mut code, + "class Node{i}(TypedDict):\n type: Literal['node-{i}']\n children: list['Node']\n" + ) + .ok(); + } + code.push_str("class Leaf(TypedDict):\n type: Literal['leaf']\n text: str\n\n"); + code.push_str("type Node = "); + for i in 0..NUM_BRANCHES { + if i > 0 { + code.push_str(" | "); + } + write!(&mut code, "Node{i}").ok(); + } + code.push_str( + r#" | Leaf + +value: list[Node] = [ + {"type": "node-0", "children": [ + {"type": "node-1", "children": [ + {"type": "node-2", "children": [{"type": "leaf", "text": "x"}]}, + {"type": "node-3", "children": [{"type": "leaf", "text": "y"}]}, + ]}, + {"type": "node-4", "children": [ + {"type": "node-5", "children": [{"type": "leaf", "text": "z"}]}, + {"type": "node-6", "children": [{"type": "leaf", "text": "w"}]}, + ]}, + ]}, +] +"#, + ); + + criterion.bench_function( + "ty_micro[recursive_typed_dict_union_contextual_inference]", + |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }, + ); +} + +fn benchmark_invariant_generic_return_union(criterion: &mut Criterion) { + const NUM_VARIANTS: usize = 21; + + setup_rayon(); + + // Regression benchmark for https://github.com/astral-sh/ty/issues/3896. + let mut code = String::new(); + for i in 0..NUM_VARIANTS { + writeln!(&mut code, "class M{i}: pass").ok(); + } + code.push_str("\nAllResults = (\n"); + for i in 0..NUM_VARIANTS { + if i > 0 { + code.push_str(" |\n"); + } + write!(&mut code, " dict[int, M{i}]").ok(); + } + code.push_str("\n)\n\nRows = (\n"); + for i in 0..NUM_VARIANTS { + if i > 0 { + code.push_str(" |\n"); + } + write!(&mut code, " list[tuple[int, M{i}]]").ok(); + } + code.push_str( + r#" +) + +def map_rows[T](rows: list[tuple[int, T]]) -> dict[int, T]: + return {} + +def perform(rows: Rows) -> AllResults: + return map_rows(rows) +"#, + ); + + criterion.bench_function("ty_micro[invariant_generic_return_union]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +fn benchmark_sequence_literal_union_access(criterion: &mut Criterion) { + const NUM_LITERALS: usize = 1_200; + + setup_rayon(); + + // Regression benchmark for https://github.com/astral-sh/ty/issues/4089. + let mut code = String::from( + "from collections.abc import Sequence\nfrom typing import Literal\n\nItem = Literal[\n", + ); + for i in 0..NUM_LITERALS { + writeln!(&mut code, " 'value-{i}',").ok(); + } + code.push_str( + r#"] + +def iterate(items: Sequence[Item]) -> None: + for item in items: + pass + +def access(items: Sequence[Item]) -> None: + items[0] +"#, + ); + + criterion.bench_function("ty_micro[sequence_literal_union_access]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +fn benchmark_invariant_generic_union_bound(criterion: &mut Criterion) { + const NUM_ALIASES: usize = 64; + + setup_rayon(); + + let mut code = + String::from("from collections.abc import Iterable\nfrom typing import Literal\n\n"); + for i in 0..NUM_ALIASES { + writeln!( + &mut code, + "type A{i} = Literal[{i}] | int | str | bytes | float" + ) + .ok(); + } + code.push_str("\nALIASES = {\n"); + for i in 0..NUM_ALIASES { + writeln!(&mut code, " A{i}: {{{i}: A{i}}},").ok(); + } + code.push_str( + r#"} + +def consume(items: Iterable[object]) -> None: ... + +consume(ALIASES.items()) +"#, + ); + + criterion.bench_function("ty_micro[invariant_generic_union_bound]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +fn benchmark_many_invariant_typevars(criterion: &mut Criterion) { + setup_rayon(); + + // Regression benchmark for https://github.com/astral-sh/ty/issues/3989. + let code = r#" +class Invariant[T]: + x: T + +def f[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]( + box1: Invariant[T1], + box2: Invariant[T2], + box3: Invariant[T3], + box4: Invariant[T4], + box5: Invariant[T5], + box6: Invariant[T6], + box7: Invariant[T7], + box8: Invariant[T8], + box9: Invariant[T9], + box10: Invariant[T10], +) -> None: ... + +x = Invariant[int]() +f(x, x, x, x, x, x, x, x, x, x) +"#; + + criterion.bench_function("ty_micro[many_invariant_typevars]", |b| { + b.iter_batched_ref( + || setup_micro_case(code), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} +fn benchmark_pydantic_core_schema_dict(criterion: &mut Criterion) { + const NUM_CORE_SCHEMA_VARIANTS: usize = 24; + + setup_rayon(); + + // Minimized from the pydantic and hydra-zen ecosystem regressions seen during the + // SpecializationBuilder pending-constraint-set migration. Pydantic has several empty dict + // literals with a type context equivalent to `dict[Hashable, core_schema.CoreSchema]` + // (including `schema.setdefault("metadata", {})` and tagged-union choice tables). + // `CoreSchema` is a large union of TypedDict schema types; this local `CoreSchema` alias is + // derived from pydantic-core's real `CoreSchema`, but reduced to enough variants to show the + // regression quickly. Solving the empty-dict specialization creates one lower-bound constraint + // per union element for `_VT@dict`. Combined with `_KT@dict = Hashable`, + // PathAssignments/SequentMap traversal derives cross-typevar facts like + // `TypedDictSchema <= _VT@dict <= Hashable`. This benchmark tracks the cost until constraint + // projection / path-bounds solving can avoid that work. + let mut code = "from collections.abc import Hashable\nfrom typing import Literal, NotRequired, TypedDict\n\n" + .to_string(); + for i in 0..NUM_CORE_SCHEMA_VARIANTS { + writeln!( + &mut code, + "class Schema{i}(TypedDict):\n type: Literal['schema-{i}']\n ref: NotRequired[str]\n value_{i}: NotRequired[int]\n" + ) + .ok(); + } + code.push_str("type CoreSchema = "); + for i in 0..NUM_CORE_SCHEMA_VARIANTS { + if i > 0 { + code.push_str(" | "); + } + write!(&mut code, "Schema{i}").ok(); + } + code.push_str("\n\nchoices: dict[Hashable, CoreSchema] = {}\n"); + + criterion.bench_function("ty_micro[pydantic_core_schema_dict]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + +criterion_group!( + constraint_set, + benchmark_typevar_mapping_large_accumulation, + benchmark_typevar_mapping_small_accumulations, + benchmark_factored_upper_bounds, + benchmark_many_upper_bound_callbacks, + benchmark_pandas_tdd, + benchmark_mixed_typed_dict_union_copy, + benchmark_recursive_typed_dict_union_contextual_inference, + benchmark_invariant_generic_return_union, + benchmark_sequence_literal_union_access, + benchmark_invariant_generic_union_bound, + benchmark_many_invariant_typevars, + benchmark_pydantic_core_schema_dict, +); +criterion_main!(constraint_set); diff --git a/crates/ruff_benchmark/benches/ty_shared/mod.rs b/crates/ruff_benchmark/benches/ty_shared/mod.rs new file mode 100644 index 0000000000..37e6fc099a --- /dev/null +++ b/crates/ruff_benchmark/benches/ty_shared/mod.rs @@ -0,0 +1,79 @@ +use std::path::Path; + +use rayon::ThreadPoolBuilder; +use rustc_hash::FxHashSet; + +use ruff_benchmark::real_world_projects::copy_directory_recursive; +use ruff_db::files::system_path_to_file; +use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem}; +use ruff_ranged_value::RangedValue; +use ty_project::metadata::options::{EnvironmentOptions, Options}; +use ty_project::metadata::python_version::SupportedPythonVersion; +use ty_project::metadata::value::RelativePathBuf; +use ty_project::{CheckMode, Db, ProjectDatabase, ProjectMetadata}; + +pub(super) struct Case { + pub(super) db: ProjectDatabase, +} + +static RAYON_INITIALIZED: std::sync::Once = std::sync::Once::new(); + +pub(super) 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 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() + .num_threads(1) + .use_current_thread() + .build_global() + .unwrap(); + }); +} + +pub(super) fn setup_micro_case(code: &str) -> Case { + setup_micro_case_inner(code, None) +} + +pub(super) fn setup_micro_case_inner(code: &str, venv_path: Option<&Path>) -> Case { + let system = TestSystem::default(); + let fs = system.memory_file_system().clone(); + + let python = venv_path.map(|venv_path| { + // Copy the on-disk venv into the in-memory filesystem. + // ProjectMetadata::discover walks up from /src and uses / as the project root, + // so the venv must be at /.venv for the `python = ".venv"` option to resolve correctly. + copy_directory_recursive(&fs, venv_path, SystemPath::new("/.venv")) + .expect("Failed to copy venv to memory filesystem"); + + RelativePathBuf::cli(SystemPath::new(".venv")) + }); + + let file_path = "src/test.py"; + fs.write_file_all( + SystemPathBuf::from(file_path), + &*ruff_python_trivia::textwrap::dedent(code), + ) + .unwrap(); + + let src_root = SystemPath::new("/src"); + let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap(); + metadata.apply_override_options(Options { + environment: Some(EnvironmentOptions { + python_version: Some(RangedValue::cli(SupportedPythonVersion::Py312)), + python, + ..EnvironmentOptions::default() + }), + ..Options::default() + }); + + let mut db = ProjectDatabase::fallible(metadata, system).unwrap(); + let file = system_path_to_file(&db, SystemPathBuf::from(file_path)).unwrap(); + + db.set_check_mode(CheckMode::OpenFiles); + db.project() + .set_open_files(&mut db, FxHashSet::from_iter([file])); + + Case { db } +} From 1a92e30085b2cd76b4b9fe645d3b623cbe46370e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Aug 2026 10:59:25 -0400 Subject: [PATCH 237/390] [ty] Infer generic TypedDicts through synthesized constructor signatures (#27436) ## Summary Prior to this change, constructing a bare generic `TypedDict` discarded the constraints provided by its fields: ```py from typing import TypedDict class Box[T](TypedDict): value: T reveal_type(Box(value=1)) # Box[Unknown] ``` This PR makes the synthesized `TypedDict.__init__` generic, so direct keyword calls use the same overload matching, contextual inference, and constraint solver as other generic constructors. For now, we only support direct keyword calls, as in: ```py class Pair[T](TypedDict): first: T second: T reveal_type(Box(value=1)) # Box[int] reveal_type(Pair(first=1, second="x")) # Pair[int | str] ``` Positional mappings and dictionary unpacking are deferred... their field constraints can't yet be propagated through ordinary constructor binding without losing key-specific information or mishandling overwrites: ```py reveal_type(Box({"value": 1})) # Box[Unknown] reveal_type(Box(**{"value": 1})) # Box[Unknown] ``` Nested generic `TypedDict` fields are also deferred until their constraints can be propagated soundly. Recursive construction remains valid, but retains its existing gradual specialization: ```py from typing import NotRequired class Node[T](TypedDict): value: NotRequired[T] child: NotRequired["Node[T]"] reveal_type(Node(child=Node(value=1))) # Node[Unknown] ``` Closes https://github.com/astral-sh/ty/issues/4134. --- .../resources/mdtest/typed_dict.md | 532 ++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 48 +- .../src/types/class/static_literal.rs | 11 +- .../src/types/class/typed_dict.rs | 18 +- .../src/types/infer/builder.rs | 38 +- .../src/types/infer/builder/typed_dict.rs | 203 ++++++- 6 files changed, 794 insertions(+), 56 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index ba010f468a..a715eafdd2 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -3473,6 +3473,538 @@ static_assert(is_assignable_to(Items[Any], Items[int])) static_assert(not is_subtype_of(Items[Any], Items[int])) ``` +### Specialized constructor signatures + +An explicitly specialized constructor substitutes its type parameter in both the receiver and the +fields. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypedDict + +class Box[T](TypedDict): + value: T + +# revealed: Overload[(self: Box[int], map: Box[int], /, *, value: int = ...) -> None, (self: Box[int], /, *, value: int) -> None] +reveal_type(Box[int].__init__) +``` + +### Constructor inference from keyword arguments + +Both PEP 695 and legacy generic constructors infer their type arguments from keyword values. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Generic, TypeVar, TypedDict + +class Box[T](TypedDict): + value: T + +reveal_type(Box(value=1)) # revealed: Box[int] + +T = TypeVar("T") + +class LegacyBox(TypedDict, Generic[T]): + value: T + +reveal_type(LegacyBox(value=1)) # revealed: LegacyBox[int] +``` + +### Generic constructor diagnostics + +Generic constructors preserve the usual diagnostics for missing and unexpected fields. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypedDict + +class Box[T](TypedDict): + value: T + +Box() # error: [missing-typed-dict-key] +Box(value=1, extra=2) # error: [invalid-key] +``` + +An invalid field value points to the field declaration and retains the usual `TypedDict` +annotations. + +```py +class LabeledBox[T](TypedDict): + value: T + label: str + +# snapshot: invalid-argument-type +LabeledBox(value=1, label=2) +``` + +```snapshot +error[invalid-argument-type]: Invalid argument to key "label" with declared type `str` on TypedDict `LabeledBox` + --> src/mdtest_snippet.py:13:27 + | +13 | LabeledBox(value=1, label=2) + | ---------- ------^ + | | | | + | | | value of type `Literal[2]` + | | key has declared type `str` + | TypedDict `LabeledBox` +info: Item declaration + --> src/mdtest_snippet.py:10:5 + | +10 | label: str + | ---------- Item declared here +``` + +### Constructor inference from multiple fields + +Different fields can contribute different types to the same type parameter. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypedDict + +class Pair[T](TypedDict): + first: T + second: T + +reveal_type(Pair(first=1, second="x")) # revealed: Pair[int | str] +``` + +### Constructor inference from inherited fields + +An inherited field constrains the child class's type parameter. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypedDict + +class Base[T](TypedDict): + value: T + +class Child[T](Base[T]): + pass + +reveal_type(Child(value=1)) # revealed: Child[int] +``` + +### Constructor inference and mapping arguments + +A named keyword can infer the element type of a mutable container. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypedDict + +class ListBox[T](TypedDict): + value: list[T] + +reveal_type(ListBox(value=[1])) # revealed: ListBox[int] +``` + +Positional and unpacked dictionary literals are validated but do not yet infer type arguments. + +```py +# TODO: Infer `ListBox[int]`. +reveal_type(ListBox({"value": [1]})) # revealed: ListBox[Unknown] +# TODO: Infer `ListBox[int]`. +reveal_type(ListBox(**{"value": [1]})) # revealed: ListBox[Unknown] +``` + +A dictionary containing different field types, or multiple unpacked dictionaries, must not cause +spurious argument errors. + +```py +class Pair[T](TypedDict): + first: T + second: str + +reveal_type(Pair(**{"first": 1, "second": "x"})) # revealed: Pair[Unknown] +reveal_type(Pair(**{"first": 1}, **{"second": "x"})) # revealed: Pair[Unknown] +``` + +### Constructor inference from recursive fields + +Recursive construction remains valid even though the outer constructor cannot yet infer its type +argument from the nested value. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import NotRequired, TypedDict + +class Node[T](TypedDict): + value: NotRequired[T] + child: NotRequired["Node[T]"] + +# TODO: Infer `Node[int]`. +reveal_type(Node(child=Node(value=1))) # revealed: Node[Unknown] +``` + +A recursive field wrapped in a union also remains diagnostic-free. + +```py +class UnionNode[T](TypedDict): + value: NotRequired[T] + child: NotRequired["UnionNode[T] | None"] + +# TODO: Infer `UnionNode[int]`. +reveal_type(UnionNode(child=UnionNode(value=1))) # revealed: UnionNode[Unknown] +``` + +The same applies when a type alias wraps the recursive union. + +```py +class AliasNode[T](TypedDict): + value: NotRequired[T] + child: NotRequired["AliasNodeChild[T]"] + +type AliasNodeChild[T] = AliasNode[T] | None + +# TODO: Infer `AliasNode[int]`. +reveal_type(AliasNode(child=AliasNode(value=1))) # revealed: AliasNode[Unknown] +``` + +### Constructor inference from nested values + +Nested `TypedDict` fields do not yet contribute constraints to the outer constructor. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import TypedDict + +class Inner[T](TypedDict): + value: T + +class Outer[T](TypedDict): + inner: Inner[T] + marker: T + +# TODO: Infer `Outer[int | str]`. +reveal_type(Outer(inner=Inner(value=1), marker="x")) # revealed: Outer[Unknown] +``` + +A nested dictionary literal also falls back without exposing an internal type parameter. + +```py +# TODO: Infer `Outer[int | str]`. +reveal_type(Outer(inner={"value": 1}, marker="x")) # revealed: Outer[Unknown] +``` + +A generic `TypedDict` nested in a container or type alias must not acquire an incompatible concrete +type from another field. + +```py +type MaybeInner[T] = Inner[T] | None + +class AliasOuter[T](TypedDict): + values: list[MaybeInner[T]] + marker: T + +# TODO: Infer `AliasOuter[int | str]`. +outer = AliasOuter(values=[Inner(value=1)], marker="x") +reveal_type(outer) # revealed: AliasOuter[Unknown] +item = outer["values"][0] +if item is not None: + reveal_type(item["value"]) # revealed: Unknown +``` + +A non-generic nested `TypedDict` does not prevent another field from inferring the type argument. + +```py +class FixedInner(TypedDict): + value: int + +class FixedOuter[T](TypedDict): + inner: FixedInner + marker: T + +reveal_type(FixedOuter(inner={"value": 1}, marker="x")) # revealed: FixedOuter[str] +``` + +A type alias without a nested `TypedDict` still contributes its ordinary field constraints. + +```py +type Values[T] = list[T] + +class AliasBox[T](TypedDict): + value: Values[T] + +reveal_type(AliasBox(value=[1])) # revealed: AliasBox[int] +``` + +### Constructor inference with upper bounds + +A literal upper bound preserves its literal, while an ordinary `int` upper bound permits the usual +promotion. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal, TypedDict + +class LiteralBound[T: Literal[1]](TypedDict): + value: T + +reveal_type(LiteralBound(value=1)) # revealed: LiteralBound[Literal[1]] + +class IntBound[T: int](TypedDict): + value: T + +reveal_type(IntBound(value=1)) # revealed: IntBound[int] +``` + +### Constructor inference with callable parameters + +Like other generic constructors, a callback must accept the promoted type inferred from another +field. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, Literal, TypedDict + +class Box[T](TypedDict): + value: T + callback: Callable[[T], None] + +def accepts_one(value: Literal[1]) -> None: ... + +Box(value=1, callback=accepts_one) # error: [invalid-argument-type] +``` + +### Constructor inference with an expected type + +The expected type can preserve a literal that inference from the value alone would promote. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Literal, TypedDict + +class Box[T](TypedDict): + value: T + +literal: Box[Literal[1]] = Box(value=1) +``` + +A wider expected type is also respected because a mutable `TypedDict` is invariant. + +```py +class Animal: ... +class Dog(Animal): ... + +animal: Box[Animal] = Box(value=Dog()) +``` + +### Constructor inference with read-only fields + +A type parameter that appears only in a read-only field is covariant. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Generic, Literal, TypeVar, TypedDict +from typing_extensions import ReadOnly + +class Animal: ... +class Dog(Animal): ... + +class Box[T](TypedDict): + value: ReadOnly[T] + +dog = Box(value=Dog()) +animal: Box[Animal] = dog +``` + +A read-only field also preserves a literal when the inferred value is used with a narrower type. + +```py +literal_box = Box(value=1) +literal: Box[Literal[1]] = literal_box +``` + +A legacy type variable is invariant by default, so assigning `LegacyBox[Dog]` to `LegacyBox[Animal]` +should eventually produce an error. + +```py +T = TypeVar("T") + +class LegacyBox(TypedDict, Generic[T]): + value: ReadOnly[T] + +legacy_dog = LegacyBox(value=Dog()) +# TODO: Reject this assignment: https://github.com/astral-sh/ty/issues/1017 +legacy_animal: LegacyBox[Animal] = legacy_dog +``` + +### Constructor inference with contravariant fields + +A read-only field is covariant in its value, while a callable is contravariant in its parameter. +Combining them makes the `TypedDict`'s type parameter contravariant. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, TypedDict +from typing_extensions import ReadOnly + +class Animal: ... +class Dog(Animal): ... + +class Consumer[T](TypedDict): + callback: ReadOnly[Callable[[T], None]] + +def accepts_animal(value: Animal) -> None: ... +def accepts_dog(value: Dog) -> None: ... + +dog_consumer: Consumer[Dog] = Consumer(callback=accepts_animal) +``` + +An incompatible callback reports its argument error without producing an additional assignment +error. + +```py +animal_consumer: Consumer[Animal] = Consumer( + callback=accepts_dog, # error: [invalid-argument-type] +) +``` + +### Constructor inference from extra items + +An extra keyword constrains the type parameter used by mutable extra items. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing_extensions import TypedDict + +class Box[T](TypedDict, extra_items=T): ... + +box = Box(value=1) +reveal_type(box) # revealed: Box[int] +box["value"] = "invalid" # error: [invalid-assignment] +``` + +A nested generic extra item should constrain its enclosing `TypedDict` without rejecting the inner +constructor. + +```py +class Inner[T](TypedDict): + value: T + +class NestedExtra[T](TypedDict, extra_items=Inner[T]): ... + +# TODO: Infer `NestedExtra[int]`. +reveal_type(NestedExtra(item=Inner(value=1))) # revealed: NestedExtra[Unknown] +``` + +### Constructor inference and context-sensitive arguments + +After inferring the type parameter, the constructor checks a lambda with its inferred parameter +type. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, TypedDict + +class Box[T](TypedDict): + value: T + callback: Callable[[T], int] + +Box(value=1, callback=lambda x: x.upper()) # error: [unresolved-attribute] +``` + +### Constructor inference with a contextual callable + +An expected specialization supplies the parameter type of a lambda stored in a field. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, TypedDict + +class Box[T](TypedDict): + value: T + +direct: Box[Callable[[int], int]] = Box(value=lambda x: x.upper()) # error: [unresolved-attribute] +optional: Box[Callable[[int], int]] | None = Box( + value=lambda x: x.upper(), # error: [unresolved-attribute] +) +``` + +### Constructor inference with a default type parameter + +A constructor argument takes precedence over the type parameter's default. + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import TypedDict + +class Defaulted[T = str](TypedDict): + value: T + +reveal_type(Defaulted(value=1)) # revealed: Defaulted[int] +``` + ### Validation of generic `TypedDict`s ```toml diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index db165f8dd3..83feb59eb1 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1610,19 +1610,23 @@ impl<'db> Type<'db> { env: &ProgramEnvironment<'db>, expected_class: StaticClassLiteral<'_>, ) -> Option> { - self.nominal_class(db, env)? - .static_class_literal(db) + self.class_specialization(db, env) .filter(|(class_literal, _)| *class_literal == expected_class) - .and_then(|(_, specialization)| specialization) + .map(|(_, specialization)| specialization) } - /// If this type is a class instance, returns the class and its specialization. + /// If this type is a class instance or class-backed `TypedDict`, returns its specialization. fn class_specialization( self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ) -> Option<(StaticClassLiteral<'db>, Specialization<'db>)> { - self.nominal_class(db, env)? + let class = match self { + Type::TypedDict(typed_dict) => typed_dict.defining_class()?, + _ => self.nominal_class(db, env)?, + }; + + class .static_class_literal(db) .and_then(|(class_literal, specialization)| Some((class_literal, specialization?))) } @@ -2800,11 +2804,15 @@ impl<'db> Type<'db> { } } - Type::GenericAlias(alias) if alias.is_typed_dict(db) => Some( - alias - .origin(db) - .typed_dict_member(db, env, None, name, policy), - ), + Type::GenericAlias(alias) if alias.is_typed_dict(db) => { + Some(alias.origin(db).typed_dict_member( + db, + env, + (name == "__init__").then_some(alias.specialization(db)), + name, + policy, + )) + } Type::GenericAlias(alias) => { Some(ClassType::from(*alias).class_member(db, env, name, policy)) @@ -5698,11 +5706,12 @@ impl<'db> Type<'db> { .into() }; - // Checking TypedDict construction happens in `infer_call_expression_impl`. - // We don't want to use the synthesized binding for type inference, so here we just - // return a permissive fallback binding. - if class_literal.is_typed_dict(db) - || class::CodeGeneratorKind::TypedDict.matches(db, class_literal) + // Specialized and non-generic TypedDict constructors use their dedicated validation. + // An unspecialized generic constructor also needs its real `__init__` signature so + // ordinary call inference can solve the class type variables. + if (class_literal.is_typed_dict(db) + || class::CodeGeneratorKind::TypedDict.matches(db, class_literal)) + && (!matches!(self, Type::ClassLiteral(_)) || class_generic_context.is_none()) { return fallback_bindings(); } @@ -5769,7 +5778,14 @@ impl<'db> Type<'db> { return fallback_bindings(); }; - let new_method = self_type.lookup_dunder_new(db, env); + // TypedDict classes inherit `dict.__new__`, whose gradual `**kwargs` signature cannot + // constrain their type variables. Their synthesized `__init__` contains the actual field + // types, including generic extra items, so constructor inference should start there. + let new_method = if class_literal.is_typed_dict(db) { + None + } else { + self_type.lookup_dunder_new(db, env) + }; let init_method_no_object = constructor_instance_ty.member_lookup_with_policy( db, diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index c547a42df1..474f7ad68a 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -3691,9 +3691,12 @@ impl<'db> StaticClassLiteral<'db> { place_and_qual.ignore_possibly_undefined().map(|ty| { let variance = if place_and_qual .qualifiers - // `CLASS_VAR || FINAL` is really `all()`, but - // we want to be robust against new qualifiers - .intersects(TypeQualifiers::CLASS_VAR | TypeQualifiers::FINAL) + // None of these fields can be mutated through an instance. + .intersects( + TypeQualifiers::CLASS_VAR + | TypeQualifiers::FINAL + | TypeQualifiers::READ_ONLY, + ) // We don't allow mutation of methods or properties || ty.is_function_literal() || ty.is_property_instance() @@ -3704,7 +3707,7 @@ impl<'db> StaticClassLiteral<'db> { // type variable, but they could if it's a // callable type. They can't be mutated on instances. // - // FINAL: final attributes are immutable, and thus covariant + // FINAL and READ_ONLY: immutable fields are covariant. TypeVarVariance::Covariant } else { default_attribute_variance diff --git a/crates/ty_python_semantic/src/types/class/typed_dict.rs b/crates/ty_python_semantic/src/types/class/typed_dict.rs index b4214af80b..5884deb616 100644 --- a/crates/ty_python_semantic/src/types/class/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/class/typed_dict.rs @@ -133,8 +133,8 @@ impl<'db> TypedDictFields<'db> { /// 1. `__init__(self, __map: TD, /, *, field1: T1 = ..., field2: T2 = ...) -> None` /// Allows passing another instance of the `TypedDict` when creating a new instance. /// Technically, `__map` could accept a subset of the `TypedDict` if the remaining -/// fields are provided as keyword arguments, but we don't model that in the -/// synthesized `__init__`, since this signature is primarily used for IDE support. +/// fields are provided as keyword arguments. Such mixed calls use dedicated constructor +/// validation instead because this overload cannot describe the overwritten mapping entries. /// Fields that are not valid Python identifiers are collapsed into `**kwargs`. /// 2. `__init__(self, *, field1: T1, field2: T2 = ...) -> None` /// Keyword-only. Fields that are not valid Python identifiers are collapsed into `**kwargs`. @@ -145,6 +145,14 @@ fn synthesize_typed_dict_init<'db>( fields: TypedDictFields<'db>, ) -> Type<'db> { let instance_ty = Type::TypedDict(typed_dict); + // Only a bare generic class exposes a generic method. Explicit aliases already substitute + // their arguments into both the receiver and the fields. + let generic_context = typed_dict.defining_class().and_then(|class| { + let alias = class.into_generic_alias()?; + let specialization = alias.specialization(db); + let generic_context = specialization.generic_context(db); + (specialization == generic_context.identity_specialization(db)).then_some(generic_context) + }); let keyword_fields: Vec<_> = fields .iter() .filter(|(name, _)| is_identifier(name)) @@ -174,7 +182,8 @@ fn synthesize_typed_dict_init<'db>( .with_definition(field.first_declaration()) }); - let map_overload = Signature::new( + let map_overload = Signature::new_generic( + generic_context, Parameters::standard( [self_param.clone(), map_param] .into_iter() @@ -195,7 +204,8 @@ fn synthesize_typed_dict_init<'db>( } }); - let keyword_overload = Signature::new( + let keyword_overload = Signature::new_generic( + generic_context, Parameters::standard( std::iter::once(self_param) .chain(keyword_field_params) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b29e660d66..5f7b6b9a65 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -87,7 +87,6 @@ use crate::types::generics::{ }; use crate::types::infer::builder::named_tuple::NamedTupleKind; use crate::types::infer::builder::paramspec_validation::validate_paramspec_components; -use crate::types::infer::builder::typed_dict::TypedDictConstructorForm; use crate::types::infer::{ StatementInference, StatementInferenceInner, StatementInferenceInnerExtra, TypeAndRange, TypeExpressionFlags, infer_statement_types, nearest_enclosing_class, @@ -116,8 +115,8 @@ use crate::types::{ LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, ParamSpecAttrKind, Parameter, Parameters, ProgramEnvironment, SentinelInstance, Signature, SpecialFormType, SubclassOfType, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, TypeVarBoundOrConstraints, - TypeVarKind, TypeVarVariance, TypedDictModule, TypedDictType, UnionAccumulator, UnionBuilder, - UnionType, any_over_type, binding_type, extract_fixed_length_iterable_element_types, + TypeVarKind, TypeVarVariance, TypedDictModule, UnionAccumulator, UnionBuilder, UnionType, + any_over_type, binding_type, extract_fixed_length_iterable_element_types, infer_complete_scope_types, infer_scope_types, is_discarded_dict_key_assignment, todo_type, }; use crate::{AnalysisSettings, Db, FxIndexSet, FxOrderSet}; @@ -8797,21 +8796,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { _ => None, }; - // Prepare `TypedDict` constructor calls before variadic argument setup so field-directed - // value inference becomes canonical before `**kwargs` expressions are inferred. - let has_prepared_typed_dict_constructor = class - .filter(|class| class.is_typed_dict(self.db())) - .map(|class| { - let typed_dict = TypedDictType::new(class); - let form = TypedDictConstructorForm::from_arguments(arguments); - self.prepare_typed_dict_constructor( - typed_dict, - form, - arguments, - func.as_ref().into(), - ); - }) - .is_some(); + if let Some(class) = class + && class.is_typed_dict(db) + { + return self.infer_typed_dict_constructor( + callable_type, + class, + call_expression, + call_expression_tcx, + ); + } // 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 @@ -9123,13 +9117,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let bindings_result = self.infer_and_check_argument_types( ArgumentsIter::from_ast(arguments), &mut call_arguments, - &mut |builder, (_, expr, tcx)| { - if has_prepared_typed_dict_constructor { - builder.get_or_infer_expression(expr, tcx) - } else { - builder.infer_expression(expr, tcx) - } - }, + &mut |builder, (_, expr, tcx)| builder.infer_expression(expr, tcx), &mut bindings, call_expression_tcx, ); diff --git a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs index 1770a51061..e15809bd8e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs @@ -4,9 +4,9 @@ use rustc_hash::FxHashMap; use smallvec::SmallVec; use strum::IntoEnumIterator; -use super::TypeInferenceBuilder; -use crate::TypeQualifiers; +use super::{ArgumentsIter, TypeInferenceBuilder}; use crate::types::class::{ClassLiteral, DynamicTypedDictAnchor, DynamicTypedDictLiteral}; +use crate::types::cyclic::ActiveRecursionDetector; use crate::types::diagnostic::{ INVALID_ARGUMENT_TYPE, INVALID_TYPE_FORM, MISSING_ARGUMENT, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT, report_mismatched_type_name, @@ -19,14 +19,40 @@ use crate::types::typed_dict::{ validate_typed_dict_constructor, validate_typed_dict_dict_literal, }; use crate::types::{ - IntersectionType, KnownClass, Type, TypeAndQualifiers, TypeContext, TypedDictModule, - TypedDictType, + ClassType, IntersectionType, KnownClass, Type, TypeAndQualifiers, TypeContext, TypedDictModule, + TypedDictType, any_over_type, }; +use crate::{Db, ProgramEnvironment, TypeQualifiers}; use ty_python_core::definition::Definition; +/// Returns whether a field type contains a `TypedDict` with unresolved type variables. +/// +/// Structural wrappers and type aliases are traversed. Revisiting an alias definition counts as a +/// match so aliases that grow with every specialization cannot recurse indefinitely: +/// +/// ```python +/// type Growing[T] = T | Growing[list[T]] +/// ``` +fn contains_generic_typed_dict<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + active_aliases: &ActiveRecursionDetector>, +) -> bool { + any_over_type(db, env, ty, false, |nested| match nested { + Type::TypedDict(_) => nested.has_typevar(db, env), + Type::TypeAlias(alias) => active_aliases.visit( + &alias.definition(db), + || true, + || contains_generic_typed_dict(db, env, alias.value_type(db), active_aliases), + ), + _ => false, + }) +} + /// The shape of a `TypedDict` constructor call that affects how we prepare it for inference. #[derive(Debug, Clone, Copy)] -pub(super) enum TypedDictConstructorForm<'expr> { +enum TypedDictConstructorForm<'expr> { /// // Ex) `TD(x=1)` KeywordOnly, /// // Ex) `TD({"x": 1})` @@ -45,7 +71,7 @@ pub(super) enum TypedDictConstructorForm<'expr> { impl<'expr> TypedDictConstructorForm<'expr> { /// Return the constructor form for `arguments`. - pub(super) fn from_arguments(arguments: &'expr ast::Arguments) -> Self { + fn from_arguments(arguments: &'expr ast::Arguments) -> Self { let [argument] = &arguments.args[..] else { return if arguments.args.is_empty() { Self::KeywordOnly @@ -394,6 +420,169 @@ impl<'db> TypeInferenceBuilder<'db, '_> { .map(|_| Type::TypedDict(typed_dict)) } + /// Infers and validates a `TypedDict` constructor through one call-binding pipeline. + /// + /// Bare generic constructors infer from direct keyword arguments. Other forms use the existing + /// field-directed validation and bind against the class's default specialization: + /// + /// ```python + /// Box(value=1) # Box[int] + /// Box({"value": 1}) # Box[Unknown] + /// ``` + pub(super) fn infer_typed_dict_constructor<'expr>( + &mut self, + callable_type: Type<'db>, + class: ClassType<'db>, + call_expression: &'expr ast::ExprCall, + call_expression_tcx: TypeContext<'db>, + ) -> Type<'db> { + let db = self.db(); + let env = self.program_environment(); + let typed_dict = TypedDictType::new(class); + let arguments = &call_expression.arguments; + let form = TypedDictConstructorForm::from_arguments(arguments); + let error_node: AnyNodeRef = call_expression.func.as_ref().into(); + let fallback_ty = callable_type + .to_instance_approximation(db, env) + .unwrap_or_else(Type::unknown); + let is_generic = matches!( + callable_type, + Type::ClassLiteral(class_literal) if class_literal.generic_context(db).is_some() + ); + let can_infer = is_generic + && self.can_infer_generic_typed_dict_constructor(class, arguments, call_expression_tcx); + + if !can_infer { + self.prepare_typed_dict_constructor(typed_dict, form, arguments, error_node); + } + + let mut call_arguments = self.prepare_call_arguments(arguments); + let binding_callable = if is_generic && !can_infer { + class.class_literal(db).default_specialization(db).into() + } else { + callable_type + }; + let mut bindings = + self.bindings_for_call(binding_callable) + .match_parameters(db, env, &call_arguments); + + if can_infer && !bindings.satisfies(|_| true) { + self.prepare_typed_dict_constructor(typed_dict, form, arguments, error_node); + return fallback_ty; + } + + let result = self.infer_and_check_argument_types( + ArgumentsIter::from_ast(arguments), + &mut call_arguments, + &mut |builder, (_, expr, tcx)| { + if can_infer { + builder.infer_expression(expr, tcx) + } else { + builder.get_or_infer_expression(expr, tcx) + } + }, + &mut bindings, + call_expression_tcx, + ); + + if result.is_err() { + if can_infer + && arguments.keywords.iter().any(|keyword| { + keyword + .arg + .as_ref() + .and_then(|name| typed_dict.item(db, name.id.as_str())) + .is_some_and(|field| { + !self.expression_type(&keyword.value).is_assignable_to( + db, + env, + field.declared_ty, + ) + }) + }) + { + validate_typed_dict_constructor( + &self.context, + typed_dict, + arguments, + error_node, + |expr, _| self.expression_type(expr), + ); + return fallback_ty; + } + + bindings.report_diagnostics(&self.context, call_expression.into()); + if can_infer { + // TODO: Remove this fallback once failed generic binding no longer exposes + // unresolved type variables. For example, if `Consumer[T]` has a callback + // field and `accepts_dog` only accepts `Dog`, then + // `value: Consumer[Animal] = Consumer(callback=accepts_dog)` fails while + // inferring `T`. Returning `Consumer[T]` would leak the unresolved type + // variable and produce an additional assignment error. + return fallback_ty; + } + } + + bindings.return_type(db, env) + } + + /// Returns whether constructor arguments can safely constrain a generic `TypedDict`. + /// + /// Mapping arguments, unresolved nested `TypedDict` fields, and gradual expected types remain + /// on the field-directed path so sibling arguments cannot force an unsound specialization: + /// + /// ```python + /// Outer(inner=Inner(value=1), marker="x") # Outer[Unknown] + /// ``` + /// + /// TODO: Remove this gate once ordinary generic call inference can safely handle mapping + /// arguments, nested `TypedDict` fields, and unresolved contextual type arguments. + fn can_infer_generic_typed_dict_constructor( + &self, + class: ClassType<'db>, + arguments: &ast::Arguments, + call_expression_tcx: TypeContext<'db>, + ) -> bool { + let db = self.db(); + let env = self.program_environment(); + let class_literal = class.class_literal(db); + + // An inner `Node(value=1)` must retain `Node[Unknown]` when its enclosing + // `Node(child=...)` cannot infer through the recursive field. + let has_gradual_class_context = class_literal + .as_static() + .zip(call_expression_tcx.annotation) + .is_some_and(|(class_literal, annotation)| { + any_over_type(db, env, annotation.resolve_type_alias(db), false, |ty| { + ty.resolve_type_alias(db) + .specialization_of(db, env, class_literal) + .is_some_and(|specialization| { + specialization + .types(db) + .iter() + .any(|ty| ty.is_unknown() || ty.has_typevar(db, env)) + }) + }) + }); + let typed_dict = TypedDictType::new(class_literal.identity_specialization(db)); + + arguments.args.is_empty() + && !has_gradual_class_context + && arguments.keywords.iter().all(|keyword| { + let Some(name) = keyword.arg.as_ref() else { + return false; + }; + typed_dict.item(db, name.id.as_str()).is_none_or(|field| { + !contains_generic_typed_dict( + db, + env, + field.declared_ty, + &ActiveRecursionDetector::default(), + ) + }) + }) + } + /// Prepare a `TypedDict` constructor call before general argument inference. /// /// This gives constructor values the declared field type as context, then validates the full @@ -401,7 +590,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { /// expression directly, while mixed dict-literal and keyword calls infer the nested key and /// value expressions without re-inferring the outer dict literal later during argument /// binding. - pub(super) fn prepare_typed_dict_constructor<'expr>( + fn prepare_typed_dict_constructor<'expr>( &mut self, typed_dict: TypedDictType<'db>, form: TypedDictConstructorForm<'expr>, From b404fe149fb5ae78ef59964c885cbdf5070308c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9r=C3=A8?= Date: Tue, 4 Aug 2026 09:56:21 -0700 Subject: [PATCH 238/390] [ty] Rename the Markdown code-span predicate used in docstring parsing (#27463) ## Summary This renames an internal helper function to be slightly more descriptive of the predicate it encapsulates. ## Test Plan This is a simple rename that relies on existing test coverage. --- crates/ty_ide/src/docstring/document/numpy.rs | 8 ++++---- crates/ty_ide/src/docstring/document/syntax.rs | 13 +++++++------ crates/ty_ide/src/docstring/markdown/structured.rs | 6 ++++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/ty_ide/src/docstring/document/numpy.rs b/crates/ty_ide/src/docstring/document/numpy.rs index 4f454bc570..eea746546d 100644 --- a/crates/ty_ide/src/docstring/document/numpy.rs +++ b/crates/ty_ide/src/docstring/document/numpy.rs @@ -28,8 +28,8 @@ use ruff_text_size::{TextRange, TextSize}; use super::preformatted::{PreformattedBlockScanner, starts_preformatted_block}; use super::syntax::{ - ParsedLine, container_block_end, is_dotted_identifier, is_markdown_code_span, parsed_lines, - split_once_at_top_level_colon, starts_container_block, + ParsedLine, container_block_end, is_dotted_identifier, is_wrapped_in_markdown_code_span, + parsed_lines, split_once_at_top_level_colon, starts_container_block, }; use super::{DescriptionBuilder, HeaderKind, SectionKind}; use crate::FxIndexMap; @@ -652,7 +652,7 @@ impl<'a> ItemLine<'a> { } // A complete code span is an anonymous type even when its contents contain a colon. - if is_markdown_code_span(text) { + if is_wrapped_in_markdown_code_span(text) { return Some(Self::new(ItemBuilder::new(None, Some(text), ""), false)); } @@ -689,7 +689,7 @@ impl<'a> ItemLine<'a> { .map_or((text, ""), |(name, description)| { (name.trim(), description.trim()) }); - if !is_item_name(name) && !is_markdown_code_span(name) { + if !is_item_name(name) && !is_wrapped_in_markdown_code_span(name) { return None; } diff --git a/crates/ty_ide/src/docstring/document/syntax.rs b/crates/ty_ide/src/docstring/document/syntax.rs index 9ad96adb9f..e23fbc9e39 100644 --- a/crates/ty_ide/src/docstring/document/syntax.rs +++ b/crates/ty_ide/src/docstring/document/syntax.rs @@ -58,11 +58,11 @@ pub(in crate::docstring) fn starts_with_markdown_list_item(line: &str) -> bool { && matches!(bytes.get(digits + 1), Some(b' ' | b'\t')) } -/// Returns whether `text` consists of a complete Markdown code span. +/// Returns whether `text` is wrapped in a Markdown code span. /// /// For example, this returns `true` for ``"`value`"`` and `false` for /// ``"`value` trailing"``. -pub(crate) fn is_markdown_code_span(text: &str) -> bool { +pub(crate) fn is_wrapped_in_markdown_code_span(text: &str) -> bool { let mut tokens = InlineMarkupScanner::new(text); let Some(InlineMarkupToken::Code(_)) = tokens.next() else { return false; @@ -530,8 +530,9 @@ pub(super) fn indentation(line: &str) -> TextSize { #[cfg(test)] mod tests { use super::{ - BacktickScanner, InlineMarkupScanner, InlineMarkupToken, TextSize, is_markdown_code_span, - split_once_at_top_level_colon, split_trailing_parenthetical, + BacktickScanner, InlineMarkupScanner, InlineMarkupToken, TextSize, + is_wrapped_in_markdown_code_span, split_once_at_top_level_colon, + split_trailing_parenthetical, }; #[test] @@ -581,7 +582,7 @@ mod tests { } #[test] - fn recognizes_complete_markdown_code_spans() { + fn recognizes_wrapped_markdown_code_spans() { for (text, expected) in [ ("`value`", true), ("``value`with:ticks``", true), @@ -592,7 +593,7 @@ mod tests { ("``", false), ("value", false), ] { - assert_eq!(is_markdown_code_span(text), expected, "{text:?}"); + assert_eq!(is_wrapped_in_markdown_code_span(text), expected, "{text:?}"); } } diff --git a/crates/ty_ide/src/docstring/markdown/structured.rs b/crates/ty_ide/src/docstring/markdown/structured.rs index f7df4c74cd..918768a839 100644 --- a/crates/ty_ide/src/docstring/markdown/structured.rs +++ b/crates/ty_ide/src/docstring/markdown/structured.rs @@ -6,7 +6,9 @@ use strum::IntoEnumIterator; use super::general; use crate::docstring::document::SectionKind; use crate::docstring::document::preformatted::MarkdownFence; -use crate::docstring::document::syntax::{is_markdown_code_span, starts_with_markdown_list_item}; +use crate::docstring::document::syntax::{ + is_wrapped_in_markdown_code_span, starts_with_markdown_list_item, +}; mod google; mod numpy; @@ -372,7 +374,7 @@ fn description_block_start(description: &str) -> Option { fn render_type_code_span_into(output: &mut String, ty: &str) { let normalized = normalize_type_for_code_span(ty); - if is_markdown_code_span(&normalized) { + if is_wrapped_in_markdown_code_span(&normalized) { output.push_str(&normalized); return; } From e63c27e4995dcc7c5e57fbd399c1d66598d9f0ff Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 4 Aug 2026 11:02:49 -0700 Subject: [PATCH 239/390] [ty] Preserve class type parameters through generic decorators (#27442) ## Summary Generic method decorators and descriptor constructors could erase enclosing class type parameters when callable-signature comparison expanded implicit `Self` bounds and existentially quantified the entire inferable set. This caused decorated methods to return `Unknown` and produced false-positive diagnostics for generic `cached_property` methods. Preserve enclosing class variables during lazy comparisons while retaining expanded inference for eager unbound-method comparisons, including inside generic higher-order calls. Generic cached-property protocol members now expose their correctly specialized readable and writable types. Fixes astral-sh/ty#4153. Fixes astral-sh/ty#3256. ## Test plan - Added mdtests for PEP 695 and legacy generic method decorators, including nested `list[T]` return types. - Added mdtests for generic `cached_property` methods with direct and union return types, and updated generic protocol descriptor specialization coverage. - Added mdtests for generic higher-order functions receiving unbound generic methods, including `functools.reduce(set.union, ...)`. - Added a constraint-level regression while preserving existing unbound-method assignability coverage. --- .../mdtest/call/callables_as_descriptors.md | 39 +++++++++++++++++++ .../resources/mdtest/decorators.md | 24 ++++++++++++ .../mdtest/generics/pep695/functions.md | 29 ++++++++++++++ .../resources/mdtest/protocols.md | 9 ++--- .../type_properties/is_assignable_to.md | 22 ++++++++++- .../src/types/signatures.rs | 28 ++++++++++++- 6 files changed, 142 insertions(+), 9 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md b/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md index 7e50e13b19..e702bad953 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md +++ b/crates/ty_python_semantic/resources/mdtest/call/callables_as_descriptors.md @@ -143,6 +143,45 @@ class C2: C2().method_decorated(1) ``` +A generic decorator must preserve a type variable bound by the method's enclosing class, even when +the decorator uses an ellipsis instead of a `ParamSpec`: + +```py +def preserve_return[R](function: Callable[..., R]) -> Callable[..., R]: + return function + +class DecoratedBox[T]: + @preserve_return + def value(self) -> T: + raise NotImplementedError + + @preserve_return + def values(self) -> list[T]: + raise NotImplementedError + +reveal_type(DecoratedBox[int]().value()) # revealed: int +reveal_type(DecoratedBox[int]().values()) # revealed: list[int] +``` + +The same behavior applies to decorators and classes using legacy type variables: + +```py +from typing import Generic, TypeVar + +LegacyT = TypeVar("LegacyT") +LegacyR = TypeVar("LegacyR") + +def legacy_preserve_return(function: Callable[..., LegacyR]) -> Callable[..., LegacyR]: + return function + +class LegacyDecoratedBox(Generic[LegacyT]): + @legacy_preserve_return + def value(self) -> LegacyT: + raise NotImplementedError + +reveal_type(LegacyDecoratedBox[int]().value()) # revealed: int +``` + And if the callable-typed decorator leaves some generic parameters unconstrained, we should keep those parameters unspecialized rather than collapsing them to `Never`: diff --git a/crates/ty_python_semantic/resources/mdtest/decorators.md b/crates/ty_python_semantic/resources/mdtest/decorators.md index 6d252dfa98..dcfed4f5a2 100644 --- a/crates/ty_python_semantic/resources/mdtest/decorators.md +++ b/crates/ty_python_semantic/resources/mdtest/decorators.md @@ -179,6 +179,30 @@ class Foo: reveal_type(Foo().foo) # revealed: str ``` +### `functools.cached_property` on a generic class + +A cached property must preserve the type variable bound by its enclosing generic class, including +when the return type is a union: + +```py +from functools import cached_property +from typing import Generic, TypeVar + +T = TypeVar("T") + +class Box(Generic[T]): + @cached_property + def value(self) -> T: + raise NotImplementedError + + @cached_property + def values(self) -> list[T] | None: + raise NotImplementedError + +reveal_type(Box[int]().value) # revealed: int +reveal_type(Box[int]().values) # revealed: list[int] | None +``` + ## Lambdas as decorators ```py diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 57d31e99d6..9f0738a4cc 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -740,6 +740,35 @@ reveal_type(invoke(head_invariant, Invariant[int]())) reveal_type(invoke(lift_invariant, 1)) ``` +## Passing unbound generic methods to generic functions + +An unbound method of a generic class can be passed to a generic higher-order function. The class +type parameter must still be inferred from the concrete receiver expected by that function. + +```py +from __future__ import annotations + +from collections.abc import Callable + +class Box[T]: + def merge(self, other: Box[T]) -> Box[T]: + return self + +def fold[T](function: Callable[[T, T], T], values: list[T]) -> T: + return values[0] + +def merge_boxes(values: list[Box[str]]) -> Box[str]: + return fold(Box.merge, values) +``` + +The same applies to the standard-library `set.union` method passed to `functools.reduce`. + +```py +from functools import reduce + +reveal_type(reduce(set.union, [set[str]()])) # revealed: set[str] +``` + ## Protocols as TypeVar bounds Protocol types can be used as TypeVar bounds, just like nominal types. diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 7496efb5c8..a238f22ae5 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -2768,9 +2768,8 @@ has_name: HasCachedName = WithCachedName() ### Generic descriptor result types -Applying a generic descriptor decorator to a generic protocol method currently loses the protocol's -type variable and produces `cached_property[Unknown]`. The protocol must preserve that descriptor -type instead of reducing it to a bare `Unknown`, which would allow an incompatible implementation. +Applying a generic descriptor decorator to a generic protocol method must preserve the protocol's +type variable and expose the specialized descriptor's readable and writable member types. ```py from functools import cached_property @@ -2791,9 +2790,7 @@ class StrValue: static_assert(not is_assignable_to(StrValue, HasValue[int])) -# TODO: This should be a property with an `int` read type once decorator calls preserve enclosing -# type variables. -# revealed: {"value": AttributeMember(`cached_property[Unknown]`)} +# revealed: {"value": PropertyMember { read: `int`, write: `int` }} reveal_protocol_interface(HasValue[int]) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 98dfb302e3..5bccad8617 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -1400,7 +1400,13 @@ the generic callable.) ```py from typing import Callable, Self from ty_extensions import static_assert -from ty_extensions._internal import RegularCallableTypeOf, TypeOf, is_assignable_to +from ty_extensions._internal import ( + ConstraintSet, + RegularCallableTypeOf, + TypeOf, + is_assignable_to, + is_constraint_set_assignable_to, +) def identity[T](t: T) -> T: return t @@ -1492,6 +1498,20 @@ static_assert( ) ``` +A constraint-producing comparison must keep an enclosing class variable symbolic while solving the +surrounding callable's return variable: + +```py +class OuterCarrier[A_outer]: + def method(self) -> A_outer: + raise NotImplementedError + + def check[R](self) -> None: + actual = is_constraint_set_assignable_to(RegularCallableTypeOf[OuterCarrier[A_outer].method], Callable[..., R]) + expected = ConstraintSet.range(A_outer, R, object) + static_assert(actual == expected) +``` + The reverse is not true — if someone expects a generic function that can be called with any specialization, we cannot hand them a function that only works with one specialization. diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index a2c1755ebc..a6c53ab411 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -2292,8 +2292,32 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { target }; - let source_inferable = source.inferable_typevars(db); - let target_inferable = target.inferable_typevars(db); + // `inferable` has different roles in the two type-variable evaluation modes: + // + // * Eager comparisons decide whether the relation holds immediately. An unbound generic + // method's `Self` can have an upper bound such as `C[T]`, so `T` must also be + // inferable; otherwise, a concrete receiver such as `C[int]` is compared against a + // fixed, symbolic `T` and valid higher-order calls are rejected. + // * Lazy comparisons record constraints for every type variable, regardless of whether + // it is inferable. Here, `signature_inferable` also determines which type variables + // `reduce_inferable` existentially removes below, so it must contain only variables + // actually bound by these signatures. Including an enclosing class's `T` would turn a + // decorator's return constraint `T <= R` into `exists T. T <= R`, losing the + // relationship needed to infer `R = T`. + let include_bound_dependencies = self.typevar_evaluation == TypeVarEvaluation::Eager; + let signature_typevars = |signature: &Signature<'db>| { + signature + .generic_context + .map_or(TypeVarSet::None, |context| { + if include_bound_dependencies { + context.inferable_typevars(db) + } else { + TypeVarSet::from_typevars(db, context.variables(db)) + } + }) + }; + let source_inferable = signature_typevars(source); + let target_inferable = signature_typevars(target); let signature_inferable = source_inferable.merge(db, target_inferable); let inferable = self.inferable.merge(db, signature_inferable); From e1938d3de967f067ad4e7a3991c63a9881493df8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Aug 2026 15:08:19 -0400 Subject: [PATCH 240/390] [ty] Recover generic constructor types from failing overloads (#27460) ## Summary Previously, we only recovered a generic constructor's specialization from an overload that passed argument validation. If validation failed, we discarded any type arguments the binding had already inferred, allowing the class's internal type variables to escape into the constructed value. See here, where we emit two diagnostics on `consumer: Consumer[Animal] = Consumer(accepts_dog)`: ```py from collections.abc import Callable class Animal: ... class Dog(Animal): ... class Consumer[T]: def __init__(self, callback: Callable[[T], None]) -> None: self.callback = callback def accepts_dog(value: Dog) -> None: ... # error: [invalid-assignment] Object of type `Consumer[T@Consumer]` is not assignable to `Consumer[Animal]` # error: [invalid-argument-type] Argument to `Consumer.__init__` is incorrect: Expected `(Animal, /) -> None`, found `def accepts_dog(value: Dog) -> None` consumer: Consumer[Animal] = Consumer(accepts_dog) ``` The callback error is correct: `accepts_dog` cannot handle every `Animal`. But the constructor binding had already established `T = Animal` from its surrounding context. Discarding that specialization returned `Consumer[T@Consumer]` and produced a second, misleading assignment error. We now preserve the specialization already established by constructor binding, even when argument validation fails, so only the callback error remains. The net effect is that we show fewer redundant errors and fewer cascading errors. --- .../resources/mdtest/call/builtins.md | 58 +++++++++++++++++++ .../mdtest/generics/pep695/classes.md | 20 +++++++ .../ty_python_semantic/src/types/call/bind.rs | 14 +++++ .../src/types/call/bind/constructor.rs | 5 +- .../src/types/infer/builder/typed_dict.rs | 9 --- 5 files changed, 96 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index 259eaa4f61..70172094d6 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -495,3 +495,61 @@ def _(xs: Unknown | list[str]): tokens: list[Unknown | str] = [] tokens.extend(escaped) ``` + +## Failed `map` calls retain their result type + +When the argument count identifies a single `map` overload, an incompatible callback still produces +its usual error. The mapped values retain the callback's return type and do not produce an +additional error when called. + +```py +class Function: + def __init__(self, value: str) -> None: ... + def __call__(self) -> None: ... + +# error: [invalid-argument-type] +for function in map(Function, [object()]): + function() +``` + +## Failed `dict` calls do not expose internal type variables + +Several `dict` overloads accept one positional argument. When none matches, an arbitrarily selected +overload must not make an otherwise compatible return type fail. + +```py +from collections.abc import Mapping + +def copy(value: object) -> dict[str, str]: + if isinstance(value, Mapping): + return dict(value) # error: [no-matching-overload] + return {} +``` + +## Failed `dict` calls preserve narrowed mapping types + +An invalid `dict` call must not invalidate an assignment inside a branch where the original value +has already been narrowed to a mapping. + +```py +from collections.abc import Mapping + +def clean(value: dict[str, int] | str | None) -> None: + if isinstance(value, Mapping): + value = dict(value) # error: [no-matching-overload] + for key, item in value.items(): + value[key] = item +``` + +## Failed inner `OrderedDict` calls do not invalidate outer constructors + +Constructing an `OrderedDict` from a list containing both strings and floats is already rejected. +That failure must not cause a second error when the resulting value is passed to another +`OrderedDict` constructor. + +```py +from collections import OrderedDict + +items = [OrderedDict([["key", 1.0]])] # error: [no-matching-overload] +OrderedDict(zip(["name"], items)) +``` 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 d31fdcdb12..f4c119a04b 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -381,6 +381,26 @@ reveal_type(C(1)) # revealed: C[Literal[1]] wrong_innards: C[int] = C("five") ``` +### Failed constructor inference + +A failed constructor call reports its argument error without exposing an unsolved class type +parameter or producing an additional assignment error. + +```py +from collections.abc import Callable + +class Animal: ... +class Dog(Animal): ... + +class Consumer[T]: + def __init__(self, callback: Callable[[T], None]) -> None: + self.callback = callback + +def accepts_dog(value: Dog) -> None: ... + +consumer: Consumer[Animal] = Consumer(accepts_dog) # error: [invalid-argument-type] +``` + ### Constructing the class from its own type variable A constructor call inside a generic class can use a value whose type is one of the class's type diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 47e50f1069..95fabb132f 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -4324,6 +4324,20 @@ impl<'db> CallableBinding<'db> { .and_then(|index| self.overloads.get(index)) } + /// Returns a failing overload only when the call's argument shape selected it uniquely. + /// + /// The overload chosen for diagnostics can be arbitrary when multiple signatures accept the + /// same argument shape. Its specialization must not determine a constructor's return type: + /// for example, `dict(value)` may match the shapes of both mapping and iterable overloads. + fn unambiguous_failing_overload(&self) -> Option<&Binding<'db>> { + match self.overloads.as_slice() { + [overload] => Some(overload), + _ => self + .matching_overload_before_type_checking + .and_then(|index| self.overloads.get(index)), + } + } + /// Returns an iterator over all the mutable overloads that matched for this call binding. pub(crate) fn matching_overloads_mut( &mut self, diff --git a/crates/ty_python_semantic/src/types/call/bind/constructor.rs b/crates/ty_python_semantic/src/types/call/bind/constructor.rs index cff5c0d96b..5f8f1dc8c9 100644 --- a/crates/ty_python_semantic/src/types/call/bind/constructor.rs +++ b/crates/ty_python_semantic/src/types/call/bind/constructor.rs @@ -370,7 +370,10 @@ impl<'db> ConstructorBinding<'db> { let mut combined: Option> = None; let mut combine_binding_specialization = |binding: &ConstructorBinding<'db>| { - let Some(overload) = binding.first_matching_overload() else { + let Some(overload) = binding + .first_matching_overload() + .or_else(|| binding.callable().unambiguous_failing_overload()) + else { return; }; let return_specialization = static_class_literal diff --git a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs index e15809bd8e..109b600664 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs @@ -512,15 +512,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } bindings.report_diagnostics(&self.context, call_expression.into()); - if can_infer { - // TODO: Remove this fallback once failed generic binding no longer exposes - // unresolved type variables. For example, if `Consumer[T]` has a callback - // field and `accepts_dog` only accepts `Dog`, then - // `value: Consumer[Animal] = Consumer(callback=accepts_dog)` fails while - // inferring `T`. Returning `Consumer[T]` would leak the unresolved type - // variable and produce an additional assignment error. - return fallback_ty; - } } bindings.return_type(db, env) From e49143a56844560165df2b869a77da80858bca9f Mon Sep 17 00:00:00 2001 From: JS <44579963+Punisheroot@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:27:12 +0200 Subject: [PATCH 241/390] [ty] Index match-pattern bindings as symbols (#27260) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Teach the `SymbolVisitor` to recognize names introduced by structural pattern matching. This handles: - capture and `as` patterns; - sequence and starred patterns; - mapping patterns, including `**rest`; - positional and keyword class patterns; - nested patterns and `|` alternatives; - module-level bindings as variables or constants; - class-level bindings as fields. Function-local bindings remain excluded, consistently with regular assignments. Pattern bindings, guards, and case bodies retain source-order traversal. Addresses the `case bar:` item in astral-sh/ty#1771. > [!NOTE] > This draft has been realigned on top of #27256, which now provides the generic Store-context handling. This PR now contains only the match-pattern binding support. ## Test Plan - `cargo test -p ty_ide` - `cargo clippy -p ty_ide --all-targets --all-features -- -D warnings` - `uv run --only-group dev --locked prek run --files crates/ty_ide/src/symbols.rs crates/ty_ide/src/document_symbols.rs` --------- Co-authored-by: Lérè --- crates/ty_ide/src/document_symbols.rs | 113 ++++++++++++++++++++ crates/ty_ide/src/symbols.rs | 142 ++++++++++++++++++++++++-- 2 files changed, 247 insertions(+), 8 deletions(-) diff --git a/crates/ty_ide/src/document_symbols.rs b/crates/ty_ide/src/document_symbols.rs index facfda6b07..b2400ccffa 100644 --- a/crates/ty_ide/src/document_symbols.rs +++ b/crates/ty_ide/src/document_symbols.rs @@ -432,6 +432,119 @@ def function(): ); } + #[test] + fn document_symbols_match_pattern_bindings() { + let test = cursor_test( + " +match subject: + case [first, *middle, last] as sequence: + body_target = 1 + case {\"key\": mapping_value, **remaining}: + fallback_target = 2 + case Point(positional, named=keyword): + pass + case (0 as alternative) | (1 as alternative): + pass + case _: + wildcard_body = 3 + +match other: + case CONSTANT_CAPTURE: + pass +", + ); + + let symbols = document_symbols(&test.db, test.program_file(test.cursor.file)) + .iter() + .map(|(_, symbol)| (symbol.name.into_owned(), symbol.kind)) + .collect::>(); + + assert_eq!( + symbols, + [ + ("first", SymbolKind::Variable), + ("middle", SymbolKind::Variable), + ("last", SymbolKind::Variable), + ("sequence", SymbolKind::Variable), + ("body_target", SymbolKind::Variable), + ("mapping_value", SymbolKind::Variable), + ("remaining", SymbolKind::Variable), + ("fallback_target", SymbolKind::Variable), + ("positional", SymbolKind::Variable), + ("keyword", SymbolKind::Variable), + ("alternative", SymbolKind::Variable), + ("alternative", SymbolKind::Variable), + ("wildcard_body", SymbolKind::Variable), + ("CONSTANT_CAPTURE", SymbolKind::Constant), + ] + .map(|(name, kind)| (name.to_owned(), kind)) + ); + } + + #[test] + fn document_symbols_ignore_invalid_pattern_bindings() { + let test = cursor_test( + " +match subject: + case [*]: + pass +", + ); + + assert!(document_symbols(&test.db, test.program_file(test.cursor.file)).is_empty()); + } + + #[test] + fn document_symbols_reports_mapping_pattern_bindings_in_source_order() { + let test = cursor_test( + " +match subject: + case {\"a\": before, **between, \"b\": after}: + pass +", + ); + + let names = document_symbols(&test.db, test.program_file(test.cursor.file)) + .iter() + .map(|(_, symbol)| symbol.name.into_owned()) + .collect::>(); + + assert_eq!(names, ["before", "between", "after"]); + } + + #[test] + fn document_symbols_match_pattern_scopes() { + let test = cursor_test( + " +class C: + match subject: + case class_capture: + body_field = 1 + +def function(): + match subject: + case local_capture: + pass +", + ); + + let symbols = document_symbols(&test.db, test.program_file(test.cursor.file)) + .iter() + .map(|(_, symbol)| (symbol.name.into_owned(), symbol.kind)) + .collect::>(); + + assert_eq!( + symbols, + [ + ("C", SymbolKind::Class), + ("class_capture", SymbolKind::Field), + ("body_field", SymbolKind::Field), + ("function", SymbolKind::Function), + ] + .map(|(name, kind)| (name.to_owned(), kind)) + ); + } + #[test] fn document_symbols_comprehension_and_lambda_scopes() { let test = cursor_test( diff --git a/crates/ty_ide/src/symbols.rs b/crates/ty_ide/src/symbols.rs index 2020ccb099..ecd37d7506 100644 --- a/crates/ty_ide/src/symbols.rs +++ b/crates/ty_ide/src/symbols.rs @@ -715,6 +715,8 @@ struct SymbolVisitor<'db> { in_class: bool, /// The statement whose expressions are currently being visited. current_stmt: Option<&'db ast::Stmt>, + /// The binding declared directly by the pattern currently being visited. + pattern_binding: Option<&'db ast::Identifier>, /// Whether store-context names should be excluded from the enclosing scope. suppress_store_symbols: bool, /// When enabled, the visitor should only try to extract @@ -747,6 +749,7 @@ impl<'db> SymbolVisitor<'db> { in_function: false, in_class: false, current_stmt: None, + pattern_binding: None, suppress_store_symbols: false, exports_only: false, all_origin: None, @@ -865,27 +868,45 @@ impl<'db> SymbolVisitor<'db> { } /// Adds a symbol for a name definition. - fn add_name_symbol(&mut self, stmt: &ast::Stmt, name: &ast::ExprName, kind: SymbolKind) { + fn add_name_symbol( + &mut self, + stmt: &ast::Stmt, + name: &Name, + name_range: TextRange, + kind: SymbolKind, + ) { let symbol = SymbolTree { parent: None, - name: name.id.to_string(), + name: name.to_string(), kind, deprecated: false, - name_range: name.range(), + name_range, full_range: stmt.range(), imported_from: None, }; self.add_symbol(symbol); } + fn add_pattern_binding(&mut self, stmt: &ast::Stmt, name: &ast::Identifier) { + if self.in_function || !name.is_valid() || name.id == "_" { + return; + } + + self.add_assignment(stmt, &name.id, name.range()); + + if self.exports_only && self.all_origin.is_some() && name.id == "__all__" { + self.all_invalid = true; + } + } + /// Adds a symbol introduced via an assignment. - fn add_assignment(&mut self, stmt: &ast::Stmt, name: &ast::ExprName) { + fn add_assignment(&mut self, stmt: &ast::Stmt, name: &Name, name_range: TextRange) { // Include assignments only when we're in global or class scope. if self.in_function { return; } - let kind = if Self::is_constant_name(name.id.as_str()) { + let kind = if Self::is_constant_name(name.as_str()) { SymbolKind::Constant } else if self .iter_symbol_stack() @@ -895,7 +916,7 @@ impl<'db> SymbolVisitor<'db> { } else { SymbolKind::Variable }; - self.add_name_symbol(stmt, name, kind); + self.add_name_symbol(stmt, name, name_range, kind); } /// Adds a symbol introduced via an import `stmt`. @@ -1382,7 +1403,7 @@ impl<'db> SymbolVisitor<'db> { let ast::Expr::Name(name) = &*type_alias.name else { return; }; - self.add_name_symbol(stmt, name, SymbolKind::Variable); + self.add_name_symbol(stmt, &name.id, name.range(), SymbolKind::Variable); } ast::Stmt::Assign(assign) => { self.add_all_assignment(&assign.targets, Some(&assign.value)); @@ -1532,7 +1553,7 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { && !self.suppress_store_symbols && let Some(stmt) = self.current_stmt => { - self.add_assignment(stmt, name); + self.add_assignment(stmt, &name.id, name.range()); if name.id != "__all__" { return; @@ -1575,6 +1596,32 @@ impl<'db> SourceOrderVisitor<'db> for SymbolVisitor<'db> { self.visit_expr(condition); } } + + fn visit_pattern(&mut self, pattern: &'db ast::Pattern) { + let binding = match pattern { + ast::Pattern::MatchStar(pattern) => pattern.name.as_ref(), + ast::Pattern::MatchAs(pattern) => pattern.name.as_ref(), + ast::Pattern::MatchMapping(pattern) => pattern.rest.as_ref(), + _ => None, + }; + + let previous_binding = self.pattern_binding; + self.pattern_binding = binding; + source_order::walk_pattern(self, pattern); + self.pattern_binding = previous_binding; + } + + fn visit_identifier(&mut self, identifier: &'db ast::Identifier) { + source_order::walk_identifier(self, identifier); + + if let Some(stmt) = self.current_stmt + && self + .pattern_binding + .is_some_and(|binding| std::ptr::eq(binding, identifier)) + { + self.add_pattern_binding(stmt, identifier); + } + } } /// Represents where an `__all__` has been defined. @@ -1739,6 +1786,85 @@ walrus :: Variable" ); } + #[test] + fn exports_match_pattern_bindings() { + let test = public_test( + "\ +match subject: + case [first, *middle, last] as sequence: + body_target = 1 + case {\"key\": mapping_value, **remaining}: + fallback_target = 2 + case Point(positional, named=keyword): + pass + case (0 as alternative) | (1 as alternative): + pass + case _: + wildcard_body = 3 + +match other: + case CONSTANT_CAPTURE: + pass +", + ); + + assert_eq!( + test.exports(), + "first :: Variable\n\ +middle :: Variable\n\ +last :: Variable\n\ +sequence :: Variable\n\ +body_target :: Variable\n\ +mapping_value :: Variable\n\ +remaining :: Variable\n\ +fallback_target :: Variable\n\ +positional :: Variable\n\ +keyword :: Variable\n\ +alternative :: Variable\n\ +wildcard_body :: Variable\n\ +CONSTANT_CAPTURE :: Constant" + ); + } + + #[test] + fn exports_reports_mapping_pattern_bindings_in_source_order() { + let test = public_test( + "\ +match subject: + case {\"a\": before, **between, \"b\": after}: + pass +", + ); + + assert_eq!( + test.exports(), + "before :: Variable\n\ +between :: Variable\n\ +after :: Variable" + ); + } + + #[test] + fn exports_invalidate_all_rebound_by_match_pattern() { + let test = public_test( + "\ +hidden = 1 +visible = 2 +__all__ = ['visible'] +match subject: + case __all__: + pass +", + ); + + assert_eq!( + test.exports(), + "hidden :: Variable\n\ +visible :: Variable\n\ +__all__ :: Variable" + ); + } + #[test] fn exports_exclude_comprehension_targets() { let test = public_test( From 5810946e38b12a68d63fdf6eb739d85bd1e41305 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 4 Aug 2026 18:10:51 -0700 Subject: [PATCH 242/390] [ty] Preserve called types in intersection diagnostics (#27475) ## Summary Previously when we reported the intersection type that a constructor call tried to call, we synthesized an intersection of the constructor methods (e.g. `__init__`, `__new__`, or metaclass `__call__`). Since these are often bound methods, which are disjoint from other bound methods, this synthesized intersection might resolve to `Never`, leading to a confusing sub-diagnostic claiming that we had tried to call the intersection `Never`. Instead, preserve each union element's called type while constructing and transforming bindings, so intersection diagnostics report the original class types instead of collapsing constructor method signature intersections into `Never`. ## Test plan - Add snapshot-backed mdtests for standalone constructor intersections and constructor intersections nested inside unions. - Cover excluded types in layered intersection diagnostics and single-callable union variants. --- .../resources/mdtest/call/union.md | 118 ++++++++++++++++++ .../resources/mdtest/intersection_types.md | 39 ++++++ .../ty_python_semantic/src/types/call/bind.rs | 56 ++++----- 3 files changed, 180 insertions(+), 33 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md index ff862e998f..8f1272f748 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/union.md +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -1005,6 +1005,124 @@ info: Attempted to call intersection type `IntCaller & StrCaller` info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller` ``` +## Union of intersected constructors retains the called class types + +When one union variant is an intersection of class objects, its constructor diagnostics should +describe that original intersection instead of intersecting the underlying constructor methods. + +```py +from typing_extensions import Self + +class UsesInit: + def __init__(self, value: int) -> None: ... + +class UsesNew: + def __new__(cls, value: str) -> Self: + return object.__new__(cls) + +class UsesBytes: + def __init__(self, value: bytes) -> None: ... + +def _(cls: type[UsesInit], other: type[UsesBytes], condition: bool) -> None: + if issubclass(cls, UsesNew): + constructor = cls if condition else other + reveal_type(constructor) # revealed: (type[UsesInit] & type[UsesNew]) | type[UsesBytes] + # error: [invalid-argument-type] "UsesBytes.__init__" + # error: [invalid-argument-type] "UsesNew.__new__" + # snapshot: invalid-argument-type + constructor(None) +``` + +```snapshot +error[invalid-argument-type]: Argument to `UsesInit.__init__` is incorrect + --> src/mdtest_snippet.py:20:21 + | +20 | constructor(None) + | ^^^^ Expected `int`, found `None` +info: Method defined here + --> src/mdtest_snippet.py:4:9 + | +4 | def __init__(self, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +info: Intersection element `bound method UsesInit.__init__(value: int) -> None` is incompatible with this call site +info: Attempted to call intersection type `type[UsesInit] & type[UsesNew]` +info: Attempted to call union type `(type[UsesInit] & type[UsesNew]) | type[UsesBytes]` +``` + +## Union intersection diagnostics retain excluded types + +A failing intersection inside a union should keep its excluded type in the intersection-specific +diagnostic, even though the exclusion does not contribute a callable binding. + +```py +from ty_extensions import Intersection, Not + +class IntCaller: + def __call__(self, value: int) -> None: ... + +class Required: ... +class Excluded: ... + +class AcceptsNone: + def __call__(self, value: None) -> None: ... + +def _(value: Intersection[IntCaller, Required, Not[Excluded]] | AcceptsNone) -> None: + # snapshot: invalid-argument-type + value(None) +``` + +```snapshot +error[invalid-argument-type]: Argument to bound method `IntCaller.__call__` is incorrect + --> src/mdtest_snippet.py:14:11 + | +14 | value(None) + | ^^^^ Expected `int`, found `None` +info: Method defined here + --> src/mdtest_snippet.py:4:9 + | +4 | def __call__(self, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +info: Intersection element `IntCaller` is incompatible with this call site +info: Attempted to call intersection type `IntCaller & Required & ~Excluded` +info: Attempted to call union type `(IntCaller & Required & ~Excluded) | AcceptsNone` +``` + +## Union variants retain excluded types with one callable + +An intersection with only one positive callable is still a distinct union variant, so its excluded +type should remain visible in the variant-specific diagnostic. + +```py +from ty_extensions import Intersection, Not + +class IntCaller: + def __call__(self, value: int) -> None: ... + +class Excluded: ... + +class AcceptsNone: + def __call__(self, value: None) -> None: ... + +def _(value: Intersection[IntCaller, Not[Excluded]] | AcceptsNone) -> None: + # snapshot: invalid-argument-type + value(None) +``` + +```snapshot +error[invalid-argument-type]: Argument to bound method `IntCaller.__call__` is incorrect + --> src/mdtest_snippet.py:13:11 + | +13 | value(None) + | ^^^^ Expected `int`, found `None` +info: Method defined here + --> src/mdtest_snippet.py:4:9 + | +4 | def __call__(self, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +info: Union variant `IntCaller & ~Excluded` is incompatible with this call site +info: Attempted to call union type `(IntCaller & ~Excluded) | AcceptsNone` +``` + ## Union semantics with constrained callable typevars ```toml diff --git a/crates/ty_python_semantic/resources/mdtest/intersection_types.md b/crates/ty_python_semantic/resources/mdtest/intersection_types.md index 68850a4335..7d43f6767b 100644 --- a/crates/ty_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/ty_python_semantic/resources/mdtest/intersection_types.md @@ -1007,6 +1007,45 @@ def _( x(1.0) ``` +### Constructor intersection diagnostics retain the called class types + +When an intersection of class objects rejects a constructor call, the diagnostic should describe the +original class types instead of reconstructing an intersection from their `__init__` and `__new__` +methods. + +```py +from typing import Self + +class UsesInit: + def __init__(self, value: int) -> None: ... + +class UsesNew: + def __new__(cls, value: str) -> Self: + return object.__new__(cls) + +def _(cls: type[UsesInit]) -> None: + if issubclass(cls, UsesNew): + reveal_type(cls) # revealed: type[UsesInit] & type[UsesNew] + # error: [invalid-argument-type] "UsesNew.__new__" + # snapshot: invalid-argument-type + cls(None) +``` + +```snapshot +error[invalid-argument-type]: Argument to `UsesInit.__init__` is incorrect + --> src/mdtest_snippet.py:15:13 + | +15 | cls(None) + | ^^^^ Expected `int`, found `None` +info: Method defined here + --> src/mdtest_snippet.py:4:9 + | +4 | def __init__(self, value: int) -> None: ... + | ^^^^^^^^ ---------- Parameter declared here +info: Intersection element `bound method UsesInit.__init__(value: int) -> None` is incompatible with this call site +info: Attempted to call intersection type `type[UsesInit] & type[UsesNew]` +``` + ### Error priority: binding error over top-callable When intersection elements fail with different error types, we use a priority hierarchy to determine diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 95fabb132f..ce2bb1101c 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -385,10 +385,6 @@ impl<'db> CallableItem<'db> { self.callable().is_callable() } - fn callable_type(&self) -> Type<'db> { - self.callable().callable_type - } - /// Returns the reduced callable synthesized from this callable item. fn functools_partial_callable<'a>( &self, @@ -441,6 +437,10 @@ impl<'db> CallableItem<'db> { /// If there are multiple items, they form an intersection. #[derive(Debug, Clone)] struct BindingsElement<'db> { + /// The callable type associated with this union element. For an intersection, retain the + /// complete source type because its bindings can omit negative contributions or represent + /// constructor methods instead of the called class objects. + callable_type: Type<'db>, items: SmallVec<[CallableItem<'db>; 1]>, } @@ -778,6 +778,7 @@ impl<'db> Bindings<'db> { } assert!(!inner_items_acc.is_empty()); let elements = smallvec![BindingsElement { + callable_type, items: inner_items_acc, }]; Self { @@ -793,8 +794,13 @@ impl<'db> Bindings<'db> { if self.callable_type == before { self.callable_type = after; } - for binding in self.iter_flat_mut() { - binding.replace_callable_type(before, after); + for element in &mut self.elements { + if element.callable_type == before { + element.callable_type = after; + } + for binding in element.callables_mut() { + binding.replace_callable_type(before, after); + } } } @@ -1132,6 +1138,7 @@ impl<'db> Bindings<'db> { .elements .into_iter() .map(|elem| BindingsElement { + callable_type: elem.callable_type, items: elem.items.into_iter().map(|item| item.map(f)).collect(), }) .collect(), @@ -1465,14 +1472,11 @@ impl<'db> Bindings<'db> { node: ast::AnyNodeRef, element: &BindingsElement<'db>, ) { - let db = context.db(); // If this element succeeded, no diagnostics to report if element.as_result(context.db()).is_ok() { return; } - let env = context.program_environment(); - let is_union = self.elements.len() > 1; // For intersection elements, use priority hierarchy @@ -1480,13 +1484,6 @@ impl<'db> Bindings<'db> { // Find the highest priority error among bindings in this element let max_priority = element.error_priority(context.db()); - // Construct the intersection type from the bindings - let intersection_type = IntersectionType::from_elements( - db, - env, - element.items.iter().map(CallableItem::callable_type), - ); - // Only report errors from bindings with the highest priority for item in &element.items { let binding = item.callable(); @@ -1498,14 +1495,14 @@ impl<'db> Bindings<'db> { // Use layered diagnostic for intersection inside a union let layered_diag = LayeredDiagnostic { union_callable_type: self.callable_type(), - intersection_callable_type: intersection_type, + intersection_callable_type: element.callable_type, binding, }; binding.report_diagnostics(context, node, Some(&layered_diag)); } else { // Just intersection, no union context needed let intersection_diag = IntersectionDiagnostic { - callable_type: intersection_type, + callable_type: element.callable_type, binding, }; binding.report_diagnostics(context, node, Some(&intersection_diag)); @@ -1524,7 +1521,7 @@ impl<'db> Bindings<'db> { } let union_diag = UnionDiagnostic { callable_type: self.callable_type(), - binding, + variant_type: element.callable_type, }; binding.report_diagnostics(context, node, Some(&union_diag)); } @@ -3153,6 +3150,7 @@ impl<'db> From> for Bindings<'db> { Bindings { callable_type: from.callable_type, elements: smallvec_inline![BindingsElement { + callable_type: from.callable_type, items: smallvec_inline![CallableItem::Regular(from)], }], implicit_dunder_new_is_possibly_unbound: false, @@ -3175,15 +3173,7 @@ impl<'db> From> for Bindings<'db> { matching_overload_before_type_checking: None, overloads: smallvec_inline![from], }; - Bindings { - callable_type, - elements: smallvec_inline![BindingsElement { - items: smallvec_inline![CallableItem::Regular(callable_binding)], - }], - implicit_dunder_new_is_possibly_unbound: false, - implicit_dunder_init_is_possibly_unbound: false, - enclosing_binding_contexts: None, - } + callable_binding.into() } } @@ -8773,20 +8763,20 @@ trait CompoundDiagnostic { /// This is used when a function call is inconsistent with one or more variants /// of a union. This can be used to attach sub-diagnostics that clarify that /// the error is part of a union. -struct UnionDiagnostic<'b, 'db> { +struct UnionDiagnostic<'db> { /// The type of the union. callable_type: Type<'db>, - /// The specific binding that failed. - binding: &'b CallableBinding<'db>, + /// The type associated with the specific union variant that failed. + variant_type: Type<'db>, } -impl CompoundDiagnostic for UnionDiagnostic<'_, '_> { +impl CompoundDiagnostic for UnionDiagnostic<'_> { fn add_context(&self, db: &dyn Db, env: &ProgramEnvironment<'_>, diag: &mut Diagnostic) { let sub = SubDiagnostic::new( SubDiagnosticSeverity::Info, format_args!( "Union variant `{callable_ty}` is incompatible with this call site", - callable_ty = self.binding.callable_type.display(db, env), + callable_ty = self.variant_type.display(db, env), ), ); diag.sub(sub); From ea933ff891a5b53599f6d51a163d67013bc2a524 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Aug 2026 21:34:08 -0400 Subject: [PATCH 243/390] [ty] Validate constructor calls on unbounded type variables (#27449) ## Summary This PR validates `type[T]` against `T`'s upper-bound, but leaves bare `type` permissive: ```py from collections.abc import Callable def permissive(cls: type) -> None: cls(1) # still accepted def unbounded[T](cls: type[T]) -> T: zero_argument: Callable[[], T] = cls one_argument: Callable[[int], T] = cls # error: [invalid-assignment] return cls(1) # error: [too-many-positional-arguments] def object_bound[T: object](cls: type[T]) -> T: return cls(1) # error: [too-many-positional-arguments] ``` I believe this is both consistent with the [constructor typing specification](https://typing.python.org/en/latest/spec/constructors.html#constructor-calls-for-type-t) and gets us passing the relevant conformance tests. The background is that in #24357, we brought constructor handling much closer to the typing specification by respecting `__new__` and metaclass `__call__` return types, but `constructors_call_type.py` still had one false negative (calling an unbounded `type[T]` with arguments). We then put up #23514 which addressed that case by overriding the permissive typeshed signature for `type.__call__`, making bare `type`, `type[object]`, and unbounded `type[T]` all use `object`'s zero-argument constructor. That created substantial ecosystem fallout and added conformance false-positives. @carljm suggested that, if we eventually want stricter `type[object]`, we should instead distinguish it from bare `type` instead of globally replacing `type.__call__`. --- .../resources/mdtest/type_of/generics.md | 117 ++++++++++++++++-- crates/ty_python_semantic/src/types.rs | 48 ++++--- .../ty_python_semantic/src/types/call/bind.rs | 13 ++ .../ty_python_semantic/src/types/callable.rs | 12 +- 4 files changed, 157 insertions(+), 33 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md index 0b824d6e83..582bdf40e3 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md @@ -16,7 +16,8 @@ def _[T](x: T): reveal_type(type(x)) # revealed: type[T@_] ``` -`type[T]` with an unbounded type variable represents any subclass of `object`. +`type[T]` with an unbounded type variable represents any subclass of `object`. Constructor calls are +checked against `object.__init__`. ```py def unbounded[T](x: type[T]) -> T: @@ -25,10 +26,54 @@ def unbounded[T](x: type[T]) -> T: reveal_type(x.__init__) # revealed: def __init__(self) -> None reveal_type(x.__qualname__) # revealed: str reveal_type(x()) # revealed: T@unbounded + # error: [too-many-positional-arguments] "Too many positional arguments to `type[T]`: expected 0, got 1" + x(1) + # error: [unknown-argument] "Argument `value` does not match any known parameter of `type[T]`" + x(value=1) return x() ``` +An explicit `object` upper bound has the same constructor signature as an implicit `object` bound, +including for legacy type variables: + +```py +from typing import TypeVar + +LegacyObjectT = TypeVar("LegacyObjectT", bound=object) + +def explicit_object_bound[T: object](x: type[T]) -> T: + reveal_type(x()) # revealed: T@explicit_object_bound + x(1) # error: [too-many-positional-arguments] + return x() + +def legacy_object_bound(x: type[LegacyObjectT]) -> LegacyObjectT: + reveal_type(x()) # revealed: LegacyObjectT@legacy_object_bound + x(1) # error: [too-many-positional-arguments] + return x() +``` + +Aliases of `object` have the same constructor and callable signature as a direct `object` bound, +even when the bound contains more than one alias: + +```py +from collections.abc import Callable + +type ObjectAlias = object +type ChainedObjectAlias = ObjectAlias + +def aliased_object_bound[T: ChainedObjectAlias](cls: type[T]) -> T: + # error: [too-many-positional-arguments] "Too many positional arguments to `type[T]`: expected 0, got 1" + cls(1) + # error: [unknown-argument] "Argument `value` does not match any known parameter of `type[T]`" + cls(value=1) + + zero_argument: Callable[[], T] = cls + # error: [invalid-assignment] + one_argument: Callable[[int], T] = cls + return cls() +``` + `type[T]` with an upper bound of `T: A` represents any subclass of `A`. ```py @@ -162,6 +207,51 @@ class Holder(Generic[T]): reveal_type(self.value) # revealed: type[T@Holder] | ((() -> type[T@Holder]) & type) ``` +## Narrowing constructor calls + +Narrowing a class object with `issubclass` uses the narrowed class's constructor without losing its +original type variable: + +```py +class IntConstructor: + def __init__(self, value: int) -> None: ... + +def narrowed_subclass[T](cls: type[T]) -> T: + if issubclass(cls, IntConstructor): + reveal_type(cls(1)) # revealed: T@narrowed_subclass & IntConstructor + return cls(1) + return cls() +``` + +An invalid call after narrowing should report only the narrowed constructor's argument error. +Existing intersection-call issues currently add a redundant error from the original upper bound: + +```py +def narrowed_invalid[T](cls: type[T]) -> None: + if issubclass(cls, IntConstructor): + # TODO: Only report `invalid-argument-type`; the upper-bound constructor also reports + # `too-many-positional-arguments` and describes the attempted intersection as `Never`. + # error: [too-many-positional-arguments] + # error: [invalid-argument-type] + cls("wrong") + + # TODO: Only report `invalid-argument-type`; the upper-bound constructor also reports + # `unknown-argument` for the same call. + # error: [unknown-argument] + # error: [invalid-argument-type] + cls(value="wrong") +``` + +Checking a class object's identity preserves the original type variable in the same way: + +```py +def narrowed_identity[T](cls: type[T]) -> T: + if cls is IntConstructor: + reveal_type(cls(1)) # revealed: T@narrowed_identity & IntConstructor + return cls(1) + return cls() +``` + ## `__class__` ```py @@ -638,11 +728,12 @@ expects_type_c_default_of_int_str(C[str, int]) ## Upcasting a `type[]` type to a `Callable` type -`type[T]` accepts the same parameters as `object.__init__` if `T` does not have an upper bound. If -`T` is bound to a nominal-instance type, `type[T]` accepts the same parameters as the constructor of -the class that the instance-type refers to. +`type[T]` accepts the same parameters as `object.__init__` if `T` has an implicit or explicit +`object` upper bound. If `T` has a more specific upper bound, `type[T]` accepts the same parameters +as that bound's constructor. Bare `type` retains its permissive constructor signature. ```py +from collections.abc import Callable from ty_extensions._internal import RegularCallableTypeOf class TakesStrInConstructor: @@ -678,13 +769,21 @@ def f[ reveal_type(type_object("")) # revealed: Any reveal_type(type_t_unbound()) # revealed: T@f - # TODO: we could consider emitting an error here as well + # error: [too-many-positional-arguments] reveal_type(type_t_unbound("")) # revealed: T@f + zero_argument_unbound: Callable[[], T] = type_t_unbound + # error: [invalid-assignment] + one_argument_unbound: Callable[[int], T] = type_t_unbound + reveal_type(type_t_object_bound()) # revealed: T1@f - # TODO: we could consider emitting an error here as well + # error: [too-many-positional-arguments] reveal_type(type_t_object_bound("")) # revealed: T1@f + zero_argument_object_bound: Callable[[], T1] = type_t_object_bound + # error: [invalid-assignment] + one_argument_object_bound: Callable[[int], T1] = type_t_object_bound + reveal_type(type_int()) # revealed: int reveal_type(type_int("1")) # revealed: int # error: [invalid-argument-type] @@ -726,10 +825,8 @@ def f[ reveal_type(bare_type_upcast) # revealed: (...) -> Any reveal_type(type_object_upcast) # revealed: (...) -> Any - # TODO: if we did decide to override typeshed's `type.__call__` annotations (see above), - # we should also turn these two into `() -> T@f` / `() -> T1@f` - reveal_type(type_t_unbound_upcast) # revealed: (...) -> T@f - reveal_type(type_t_object_bound_upcast) # revealed: (...) -> T1@f + reveal_type(type_t_unbound_upcast) # revealed: () -> T@f + reveal_type(type_t_object_bound_upcast) # revealed: () -> T1@f # revealed: Overload[(x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> int, (x: str | bytes | bytearray, /, base: SupportsIndex) -> int] reveal_type(type_int_upcast) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 83feb59eb1..efd325245d 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1805,6 +1805,23 @@ impl<'db> Type<'db> { ty } + /// Selects the constructor used for a type variable's upper bound. + /// + /// The meta-type of `object` simplifies to permissive bare `type`, so retain the exact class + /// object instead. Resolve aliases first so an alias of `object` cannot bypass that behavior. + fn constructor_for_typevar_bound( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Type<'db> { + let bound = self.resolve_type_alias(db); + if bound.is_object() { + KnownClass::Object.to_class_literal(db, env) + } else { + bound.to_meta_type(db, env) + } + } + /// Returns `Some(UnionType)` if this type behaves like a union. Apart from explicit unions, /// this returns `Some` for `TypeAlias`es of unions and `NewType`s of `float` and `complex`. fn as_union_like(self, db: &'db dyn Db) -> Option> { @@ -5149,12 +5166,19 @@ impl<'db> Type<'db> { ), SubclassOfInner::TypeVar(tvar) => { let constructor_instance_type = Type::TypeVar(tvar); - let bindings = match tvar.typevar(db).bound_or_constraints(db, env) { - None => KnownClass::Type.to_instance(db, env).bindings(db, env), - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.to_meta_type(db, env).bindings(db, env) + let bindings = match tvar.typevar(db).require_bound_or_constraints(db, env) { + TypeVarBoundOrConstraints::UpperBound(bound) => { + let constructor = bound.constructor_for_typevar_bound(db, env); + if let Type::ClassLiteral(class) = constructor + && let Some(bindings) = + self.known_class_literal_bindings(db, env, class) + { + bindings + } else { + constructor.bindings(db, env) + } } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + TypeVarBoundOrConstraints::Constraints(constraints) => { Bindings::from_union( self, constraints @@ -5164,17 +5188,11 @@ impl<'db> Type<'db> { ) } }; - // TODO We would ideally be able to just do `into_constructor_bindings` in the - // no-bounds/constraints case above (where we get back the bindings for - // `Type.__call__`), and just do `with_constructed_instance_type` in the - // bound/constrained cases, where we should get back constructor bindings (or - // if we don't, we probably shouldn't return `T` from the call?). But currently - // we can't because we special-case some built-in types to return regular - // (not constructor) bindings from `constructor_bindings()`. + // Some built-in constructors, including `object`, are special-cased as regular + // callable bindings. Wrap them so that every bound or constrained call has + // constructor context and constructs `T`; existing constructor bindings keep + // their original kind. bindings - // `into_constructor_bindings` is a no-op for already-constructor bindings, - // so we are just setting the `MetaclassCall` type for `Type.__call__`, or - // the special-cased builtin classes that return regular bindings. .into_constructor_bindings( constructor_instance_type, ConstructorCallableKind::MetaclassCall, diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index ce2bb1101c..3f0794e1f6 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -531,11 +531,18 @@ impl<'db> BindingsElement<'db> { /// `f: KnownCallable & Top[Callable[..., Awaitable[object]]]`, even though the top-callable /// call itself is unsafe. (We know that somewhere in the infinite-union of the top callable, /// there is a callable with the right parameters to match the call.) + /// + /// Likewise, a narrowed class can provide a more specific constructor signature than `type[T]`. + /// Even when the `type[T]` constructor rejects the arguments, its return type still constrains + /// the successful constructor call. fn retain_successful(&mut self, db: &'db dyn Db) { if self.is_intersection() && self.as_result(db).is_ok() { self.items.retain(|item| { item.as_result(db).is_ok() || item.error_priority(db) == CallErrorPriority::TopCallable + || item.as_constructor().is_some_and(|constructor| { + matches!(constructor.constructed_instance_type(), Type::TypeVar(_)) + }) }); } } @@ -7688,6 +7695,12 @@ impl<'db> CallableDescription<'db> { kind: Some("class"), name: Cow::Borrowed(class_type.name(db)), }), + Type::SubclassOf(subclass) if let Some(typevar) = subclass.into_type_var() => { + Some(CallableDescription { + kind: None, + name: Cow::Owned(format!("type[{}]", typevar.name(db))), + }) + } Type::BoundMethod(bound_method) => Some({ let function = bound_method.function(db); let kind = if function.name(db) == "__init__" { diff --git a/crates/ty_python_semantic/src/types/callable.rs b/crates/ty_python_semantic/src/types/callable.rs index a7ccddfdc7..d77fe4ff51 100644 --- a/crates/ty_python_semantic/src/types/callable.rs +++ b/crates/ty_python_semantic/src/types/callable.rs @@ -174,10 +174,10 @@ impl<'db> Type<'db> { } }), SubclassOfInner::TypeVar(tvar) => { - match tvar.typevar(db).bound_or_constraints(db, env) { - Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + match tvar.typevar(db).require_bound_or_constraints(db, env) { + TypeVarBoundOrConstraints::UpperBound(bound) => { let upcast_callables = bound - .to_meta_type(db, env) + .constructor_for_typevar_bound(db, env) .try_upcast_to_callable_with_policy_and_context( db, env, policy, context, )?; @@ -194,7 +194,7 @@ impl<'db> Type<'db> { ) })) } - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + TypeVarBoundOrConstraints::Constraints(constraints) => { let mut callables = SmallVec::new(); for constraint in constraints.elements(db) { let element_upcast = constraint @@ -217,10 +217,6 @@ impl<'db> Type<'db> { } Some(CallableTypes::new(callables)) } - None => Some(CallableTypes::one(CallableType::single( - db, - Signature::new(Parameters::gradual_form(), Type::TypeVar(tvar)), - ))), } } SubclassOfInner::Dynamic(_) => Some(CallableTypes::one(CallableType::single( From 2ddadb9937e0fe579bcd0904cec50d067f176edd Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 4 Aug 2026 19:12:59 -0700 Subject: [PATCH 244/390] [ty] Fix constructor calls on narrowed type-variable intersections (#27493) ## Summary In #27449, we start treating calls to `type[object]` strictly (using `object.__init__` rather than the overly forgiving `type.__call__`). This also means that `type[T] & type[SomeType]` intersections (which can easily arise with a parameter `cls: type[T]` which is then narrowed via `issubclass(cls, SomeType)`) start validating the `type[T]` portion against `object.__init__` (assuming `T` has no upper bound besides `object`). That means if the call to `type[SomeType]` fails, we now get confusing double diagnostics also complaining about the failed call to `object.__init__`. This PR resolves that problem separately in a general way: if we have an intersection `type[T] & type[SomeClass]`, where `SomeClass` is a subclass of the upper bound of `T`, we don't try calling both constructors; we just try the constructor of `SomeClass`. (But we preserve the full intersection as receiver, so that the result of the call will still be `T & SomeClass`, not just `SomeClass`.) Since all classes are subclasses of `object`, this means we don't try calling `object.__init__` or emit errors about it when they are redundant and confusing in an intersection with some more specific `type[...]`. (This is only relevant to intersections with `type[T]` where `T` is a typevar, since a normal `type[Base] & type[Child]` intersection would immediately simplify to `type[Child]`. That simplification doesn't occur with a typevar, since we need to preserve the typevar identity; it may actually represent a narrower type, not its upper bound.) - Resolve constructor calls on narrowed `type[T] & type[Child]` intersections using the applicable subclass constructor instead of treating the type-variable bound as an independent alternative. - Preserve the precise `T & Child` result, report only subclass-constructor argument errors, and retain specialized generic constructors, built-in behavior, independent metaclass callables, and explicit `__new__` / metaclass `__call__` return types. ## Test plan - Added constructor mdtests for `issubclass`-narrowed bounded, constrained, and unbounded type variables; valid return types; rejected arguments; and deduplicated diagnostics. - Covered explicitly specialized generic constructors, literal-preserving built-in constructors, `Self` returns, non-instance `__new__` and metaclass `__call__` returns, and intersections with independent metaclass callables. Ecosystem changes are both correct/improvements. --- .../resources/mdtest/call/constructor.md | 187 ++++++++++++++++++ .../resources/mdtest/type_of/generics.md | 14 +- crates/ty_python_semantic/src/types.rs | 26 +++ .../ty_python_semantic/src/types/call/bind.rs | 15 ++ .../src/types/call/bind/constructor.rs | 7 +- 5 files changed, 238 insertions(+), 11 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/constructor.md b/crates/ty_python_semantic/resources/mdtest/call/constructor.md index b79d67143d..5bbbf508f3 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/constructor.md +++ b/crates/ty_python_semantic/resources/mdtest/call/constructor.md @@ -1512,6 +1512,193 @@ def f(cls: type[T]): reveal_type(cls(1, "foo")) # revealed: T@f ``` +## Intersection constructors + +```toml +[environment] +python-version = "3.12" +``` + +### Narrowed bound type variables + +Narrowing a bounded class type with `issubclass` must use the subclass constructor while preserving +both the original type variable and the narrowed subclass in the return type. + +```py +class Base: + def __init__(self, value: str) -> None: ... + +class IntConstructor(Base): + def __init__(self, value: int) -> None: ... + +def valid[T: Base](cls: type[T]) -> T: + if issubclass(cls, IntConstructor): + reveal_type(cls) # revealed: type[T@valid] & type[IntConstructor] + reveal_type(cls(1)) # revealed: T@valid & IntConstructor + return cls(1) + return cls("ok") +``` + +An argument accepted by the bound's constructor must still be rejected by the narrowed subclass. +Arguments rejected by both constructors produce only the subclass constructor's diagnostic. + +```py +def invalid_arguments[T: Base](cls: type[T]) -> None: + if issubclass(cls, IntConstructor): + # error: [invalid-argument-type] "Argument to `IntConstructor.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" + cls("wrong") + # error: [invalid-argument-type] "Argument to `IntConstructor.__init__` is incorrect: Expected `int`, found `None`" + cls(None) +``` + +### Narrowed constrained type variables + +Narrowing a constrained type variable selects the matching constructor without losing the original +type variable in the return type. + +```py +class StringConstructor: + def __init__(self, value: str) -> None: ... + +class IntConstructor: + def __init__(self, value: int) -> None: ... + +def construct[T: (StringConstructor, IntConstructor)](cls: type[T]) -> None: + if issubclass(cls, IntConstructor): + reveal_type(cls) # revealed: type[T@construct] & type[IntConstructor] + reveal_type(cls(1)) # revealed: T@construct & IntConstructor + # error: [invalid-argument-type] "Argument to `IntConstructor.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" + cls("wrong") +``` + +### Narrowed unbounded type variables + +The narrowed subclass constructor also determines which arguments are valid when the original type +variable has no upper bound. + +```py +class IntConstructor: + def __init__(self, value: int) -> None: ... + +def construct[T](cls: type[T]) -> None: + if issubclass(cls, IntConstructor): + reveal_type(cls) # revealed: type[T@construct] & type[IntConstructor] + reveal_type(cls(1)) # revealed: T@construct & IntConstructor + # error: [invalid-argument-type] "Argument to `IntConstructor.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" + cls("wrong") +``` + +### Specialized generic constructors + +A generic constructor provider must retain its explicit specialization when validating arguments and +preserve the original type variable in its return type. + +```py +from ty_extensions import Intersection + +class Box[S]: + def __init__(self, value: S) -> None: ... + +def construct[T](cls: Intersection[type[T], type[Box[int]]]) -> None: + reveal_type(cls(1)) # revealed: T@construct & Box[int] + # error: [invalid-argument-type] "Argument to `Box.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" + cls("wrong") +``` + +### Built-in constructor behavior + +Narrowing to a final built-in class must retain its specialized constructor behavior and the +original type variable. + +```py +def construct[T](cls: type[T]) -> None: + if issubclass(cls, bool): + reveal_type(cls) # revealed: type[T@construct] & + reveal_type(cls(1)) # revealed: T@construct & Literal[True] +``` + +### `Self` constructor returns + +An explicit `Self` return still represents the constructed instance, so narrowing must preserve the +original type variable and validate the subclass initializer. + +```py +from typing import Self + +class Base: + def __init__(self, value: str) -> None: ... + +class NewChild(Base): + def __new__(cls, value: object) -> Self: + return object.__new__(cls) + + def __init__(self, value: int) -> None: ... + +def construct[T: Base](cls: type[T]) -> None: + if issubclass(cls, NewChild): + reveal_type(cls(1)) # revealed: T@construct & NewChild + # error: [invalid-argument-type] "Argument to `NewChild.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" + cls("wrong") +``` + +### Non-instance `__new__` returns + +A constructor explicitly returning a non-instance type must retain that return type instead of +intersecting it with the original type variable. + +```py +class Base: + def __init__(self, value: str) -> None: ... + +class ReturnsString(Base): + def __new__(cls, value: int) -> str: + return str(value) + +def construct[T: Base](cls: type[T]) -> None: + if issubclass(cls, ReturnsString): + reveal_type(cls(1)) # revealed: str +``` + +### Non-instance metaclass `__call__` returns + +A custom metaclass's explicit non-instance return similarly takes precedence over the original type +variable. + +```py +class StringFactory(type): + def __call__(cls, value: int) -> str: + return str(value) + +class Base: + def __init__(self, value: str) -> None: ... + +class Factory(Base, metaclass=StringFactory): ... + +def construct[T: Base](cls: type[T]) -> None: + if issubclass(cls, Factory): + reveal_type(cls(1)) # revealed: str +``` + +### Independent metaclass callables + +An intersection of a class-object type and a metaclass instance retains both independent callables. +Arguments accepted by only one callable use that callable's return type. + +```py +from ty_extensions import Intersection + +class StringBase: + def __init__(self, value: str) -> None: ... + +class IntMeta(type): + def __call__(cls, value: int) -> str: + return str(value) + +def construct(cls: Intersection[type[StringBase], IntMeta]) -> None: + reveal_type(cls(1)) # revealed: str + reveal_type(cls("ok")) # revealed: StringBase +``` + ## Union of constructors ```py diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md index 582bdf40e3..f4f8429588 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/generics.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/generics.md @@ -223,22 +223,16 @@ def narrowed_subclass[T](cls: type[T]) -> T: return cls() ``` -An invalid call after narrowing should report only the narrowed constructor's argument error. -Existing intersection-call issues currently add a redundant error from the original upper bound: +Invalid positional and keyword arguments each produce only the narrowed subclass constructor's +diagnostic: ```py def narrowed_invalid[T](cls: type[T]) -> None: if issubclass(cls, IntConstructor): - # TODO: Only report `invalid-argument-type`; the upper-bound constructor also reports - # `too-many-positional-arguments` and describes the attempted intersection as `Never`. - # error: [too-many-positional-arguments] - # error: [invalid-argument-type] + # error: [invalid-argument-type] "Argument to `IntConstructor.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" cls("wrong") - # TODO: Only report `invalid-argument-type`; the upper-bound constructor also reports - # `unknown-argument` for the same call. - # error: [unknown-argument] - # error: [invalid-argument-type] + # error: [invalid-argument-type] "Argument to `IntConstructor.__init__` is incorrect: Expected `int`, found `Literal["wrong"]`" cls(value="wrong") ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index efd325245d..eb2abe6fdd 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -5254,6 +5254,32 @@ impl<'db> Type<'db> { .map(|element| element.bindings(db, env)), ), + // A narrowed `type[T: Base] & type[Child]` still needs to construct `T & Child`, + // but its constructor must come from `Child`, not from `Base` as an independent, + // competing alternative. Flattening the projected instance lets intersection + // simplification select that constructor without discarding unrelated providers. + Type::Intersection(intersection) + if intersection.positive(db).iter().all(|element| { + // A metaclass instance also has an instance-space projection, but it can + // provide an independent `__call__`. Only simplify actual class-object + // variants so `type[Base] & Meta` retains both callable candidates. + matches!( + element.resolve_type_alias(db), + Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) + ) + }) && let Some(instance_type) = self.to_instance_approximation(db, env) + && let Type::NominalInstance(lookup_instance) = + instance_type.flatten_typevars(db, env) + && let Some(bindings) = { + let bindings = lookup_instance.to_meta_type(db, env).bindings(db, env); + bindings.has_only_constructor_items().then_some(bindings) + } => + { + bindings + .with_constructed_instance_type(db, instance_type) + .with_callable_type(self) + } + Type::Intersection(intersection) => Bindings::from_intersection( self, intersection diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 3f0794e1f6..3ca642ad74 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -811,6 +811,15 @@ impl<'db> Bindings<'db> { } } + /// Set the overall receiver without replacing individual constructor callables. + pub(crate) fn with_callable_type(mut self, callable_type: Type<'db>) -> Self { + self.callable_type = callable_type; + for element in &mut self.elements { + element.callable_type = callable_type; + } + self + } + pub(crate) fn with_constructed_instance_type( mut self, db: &'db dyn Db, @@ -916,6 +925,12 @@ impl<'db> Bindings<'db> { .filter_map(CallableItem::as_constructor) } + /// Return whether every callable uses ordinary constructor binding semantics. + pub(crate) fn has_only_constructor_items(&self) -> bool { + self.iter_callable_items() + .all(|item| item.as_constructor().is_some()) + } + fn iter_constructor_items_mut(&mut self) -> impl Iterator> { self.iter_callable_items_mut() .filter_map(CallableItem::as_constructor_mut) diff --git a/crates/ty_python_semantic/src/types/call/bind/constructor.rs b/crates/ty_python_semantic/src/types/call/bind/constructor.rs index 5f8f1dc8c9..28a61f6851 100644 --- a/crates/ty_python_semantic/src/types/call/bind/constructor.rs +++ b/crates/ty_python_semantic/src/types/call/bind/constructor.rs @@ -641,7 +641,12 @@ impl<'db> ConstructorBinding<'db> { db: &'db dyn Db, env: &ProgramEnvironment<'db>, ) -> Option> { - self.constructed_instance_type() + let instance_type = self.constructed_instance_type(); + let lookup_instance = match instance_type { + Type::Intersection(_) => instance_type.flatten_typevars(db, env), + _ => instance_type, + }; + lookup_instance .as_nominal_instance() // TODO may need to handle `Type::KnownInstance` here as well? .map(|instance| instance.class(db, env).class_literal(db)) From 7982c94a370c90e4903284fd8692d1398e205a38 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 07:59:50 +0200 Subject: [PATCH 245/390] Update Rust crate camino to v1.2.5 (#27481) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdc535485a..0d58d678d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,9 +377,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.4" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" dependencies = [ "serde_core", ] From b2200030f47847bb8e1cc10f7491b48f9c80c00e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:01:11 +0200 Subject: [PATCH 246/390] Update Rust crate thin-vec to v0.2.19 (#27484) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d58d678d7..1d7e762fdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4315,9 +4315,9 @@ dependencies = [ [[package]] name = "thin-vec" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" +checksum = "79def32ffcd477db1ff26f76dab9e3a91f0bd42a85ca96577089b24623056f9d" dependencies = [ "serde", ] From 5ba9d8f2b3808f2a51805b027067261b7b13be0e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:01:41 +0200 Subject: [PATCH 247/390] Update taiki-e/install-action action to v2.85.3 (#27490) --- .github/workflows/ci.yaml | 16 ++++++++-------- .github/workflows/sync_typeshed.yaml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5ac4bdaeff..ed44ce68f4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -339,7 +339,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest and insta" - uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 + uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: | cargo-nextest @@ -405,7 +405,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 + uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: cargo-nextest - name: "Install uv" @@ -444,7 +444,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 + uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: cargo-nextest - name: "Install uv" @@ -1083,7 +1083,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 + uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: cargo-codspeed @@ -1122,7 +1122,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 + uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: cargo-codspeed @@ -1188,7 +1188,7 @@ jobs: version: "0.12.0" - name: "Install codspeed" - uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 + uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: cargo-codspeed @@ -1242,7 +1242,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 + uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: cargo-codspeed @@ -1292,7 +1292,7 @@ jobs: version: "0.12.0" - name: "Install codspeed" - uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 + uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: cargo-codspeed diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index d992d92beb..3dde403996 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -268,7 +268,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest and insta" - uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0 + uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: | cargo-nextest From c82cbfe23039d8def72f47bf608c760352988825 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:02:50 +0200 Subject: [PATCH 248/390] Update CodSpeedHQ/action action to v4.19.1 (#27487) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ed44ce68f4..ea2a6704b2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1091,7 +1091,7 @@ jobs: run: cargo codspeed build -m simulation -m memory --features "codspeed,ruff_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser - name: "Run benchmarks" - uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5 + uses: CodSpeedHQ/action@f22792bfac16f3e14eb9fbea76f4a48e9cc22b93 # v4.19.1 with: mode: "simulation,memory" run: cargo codspeed run @@ -1203,7 +1203,7 @@ jobs: run: find target/codspeed -type f -exec chmod +x {} + - name: "Run benchmarks" - uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5 + uses: CodSpeedHQ/action@f22792bfac16f3e14eb9fbea76f4a48e9cc22b93 # v4.19.1 with: mode: ${{ matrix.mode }} run: cargo codspeed run --bench "${{ matrix.target }}" "${{ matrix.filter }}" @@ -1307,7 +1307,7 @@ jobs: run: find target/codspeed -type f -exec chmod +x {} + - name: "Run benchmarks" - uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5 + uses: CodSpeedHQ/action@f22792bfac16f3e14eb9fbea76f4a48e9cc22b93 # v4.19.1 env: # enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't # appear to provide much useful insight for our walltime benchmarks right now From e38aea029bfd8d77ee80c41bd55c4e587efa68cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:04:12 +0200 Subject: [PATCH 249/390] Update Rust crate toml to v1.1.4 (#27485) --- Cargo.lock | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d7e762fdf..0e24dd3bb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2113,7 +2113,7 @@ dependencies = [ "similar 3.1.1", "smallvec", "thiserror 2.0.19", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "toml_parser", "tracing", ] @@ -3121,7 +3121,7 @@ dependencies = [ "test-case", "thiserror 2.0.19", "tikv-jemallocator", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "walkdir", "wild", @@ -3259,7 +3259,7 @@ dependencies = [ "similar 3.1.1", "strum", "tempfile", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -3382,7 +3382,7 @@ dependencies = [ "tempfile", "test-case", "thiserror 2.0.19", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "typed-arena", "unicode-normalization", "unicode-width", @@ -3670,7 +3670,7 @@ dependencies = [ "ruff_text_size", "schemars", "serde", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", ] [[package]] @@ -3710,7 +3710,7 @@ dependencies = [ "smallvec", "tempfile", "thiserror 2.0.19", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "tracing-log", "tracing-subscriber", @@ -3803,7 +3803,7 @@ dependencies = [ "shellexpand", "strum", "tempfile", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "unicode-normalization", ] @@ -4443,9 +4443,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -4488,9 +4488,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow 1.0.0", ] @@ -4617,7 +4617,7 @@ dependencies = [ "serde_json", "tempfile", "tikv-jemallocator", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "tracing-flame", "tracing-subscriber", @@ -4667,7 +4667,7 @@ dependencies = [ "ruff_text_size", "serde", "tempfile", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "ty_ide", "ty_module_resolver", "ty_project", @@ -4777,7 +4777,7 @@ dependencies = [ "strum", "strum_macros", "thiserror 2.0.19", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "ty_combine", "ty_module_resolver", @@ -4961,7 +4961,7 @@ dependencies = [ "salsa", "serde", "tempfile", - "toml 1.1.3+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "ty_module_resolver", "ty_python_core", From 8961e0e41ea478b01cfd9c11e9b48c99c1532457 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:05:36 +0200 Subject: [PATCH 250/390] Update Rust crate toml_parser to v1.1.3 (#27486) From bfa551cad5071760ffd6144d5e7bc132d79fd172 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:06:04 +0200 Subject: [PATCH 251/390] Update Rust crate jiff to v0.2.35 (#27482) --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e24dd3bb0..6f40d45a0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1813,9 +1813,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ "defmt", "jiff-core", @@ -1839,9 +1839,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" dependencies = [ "jiff-core", "proc-macro2", From 78fd19cf21c8c9e440bee22122ffdbd1293bc746 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:06:14 +0200 Subject: [PATCH 252/390] Update dependency astral-sh/uv to v0.12.1 (#27477) --- .github/workflows/ci.yaml | 28 ++++++++++---------- .github/workflows/daily_fuzz.yaml | 2 +- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/sync_typeshed.yaml | 6 ++--- .github/workflows/ty-ecosystem-analyzer.yaml | 4 +-- .github/workflows/ty-ecosystem-report.yaml | 2 +- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ea2a6704b2..30f58d51f7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -347,7 +347,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" enable-cache: "true" - name: ty mdtests (GitHub annotations) if: ${{ needs.determine_changes.outputs.ty == 'true' }} @@ -411,7 +411,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" enable-cache: "true" - name: "Run tests" run: cargo nextest run --cargo-profile profiling --all-features @@ -450,7 +450,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" enable-cache: "true" - name: "Run tests" run: | @@ -560,7 +560,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: ruff-linux-debug @@ -602,7 +602,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - name: "Install Rust toolchain" run: rustup component add rustfmt # Run all code generation scripts, and verify that the current output is @@ -649,7 +649,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} activate-environment: true - version: "0.12.0" + version: "0.12.1" - name: "Install Rust toolchain" run: rustup show @@ -763,7 +763,7 @@ jobs: run: git fetch --no-tags --filter=blob:none --unshallow origin - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -829,7 +829,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -882,7 +882,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 @@ -920,7 +920,7 @@ jobs: with: python-version: 3.13 activate-environment: true - version: "0.12.0" + version: "0.12.1" - name: "Install dependencies" run: uv pip install -r docs/requirements.txt - name: "Update README File" @@ -1077,7 +1077,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - name: "Install Rust toolchain" run: rustup show @@ -1185,7 +1185,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - name: "Install codspeed" uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 @@ -1236,7 +1236,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - name: "Install Rust toolchain" run: rustup show @@ -1289,7 +1289,7 @@ jobs: - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - name: "Install codspeed" uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index c430ccfb75..f21772ef7c 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -38,7 +38,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - name: "Install Rust toolchain" run: rustup show - name: "Install mold" diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index cec7e0b981..981d5bace7 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -24,7 +24,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: wheels-* diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index 3dde403996..04d0955cf3 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -86,7 +86,7 @@ jobs: git config --global user.email '<>' - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - name: Sync typeshed stubs run: | rm -rf "ruff/${VENDORED_TYPESHED}" @@ -142,7 +142,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - name: Setup git run: | git config --global user.name typeshedbot @@ -184,7 +184,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" - name: Setup git run: | git config --global user.name typeshedbot diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index cc019609c9..48fb53cf5b 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -127,7 +127,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" ignore-empty-workdir: true # The default of `enable-cache: auto` leads to warnings from setup-uv in this job, # since its cache-invalidation keys (pyproject.toml, uv.lock, etc.) aren't available @@ -187,7 +187,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.0" + version: "0.12.1" ignore-empty-workdir: true # The default of `enable-cache: auto` leads to warnings from setup-uv in this job, # since its cache-invalidation keys (pyproject.toml, uv.lock, etc.) aren't available diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 72fd7b40d1..49f5557781 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -36,7 +36,7 @@ jobs: uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: enable-cache: true - version: "0.12.0" + version: "0.12.1" - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: From f76455dda8125c8a88d54e25909d3620b6e7d010 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:06:38 +0200 Subject: [PATCH 253/390] Update dependency unidiff to v1 (#27491) --- python/ruff-ecosystem/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ruff-ecosystem/pyproject.toml b/python/ruff-ecosystem/pyproject.toml index 434b03dd0a..8b8d2deea0 100644 --- a/python/ruff-ecosystem/pyproject.toml +++ b/python/ruff-ecosystem/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "ruff-ecosystem" version = "0.0.0" requires-python = ">=3.11" -dependencies = ["unidiff==0.7.5", "tomli_w==1.2.0", "tomli==2.4.1"] +dependencies = ["unidiff==1.0.0", "tomli_w==1.2.0", "tomli==2.4.1"] [project.scripts] ruff-ecosystem = "ruff_ecosystem.cli:entrypoint" From 04eb43cdec90855165c1d5b367f767f35bc8dd72 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:07:52 +0200 Subject: [PATCH 254/390] Update Rust crate schemars to v1.2.2 (#27483) --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f40d45a0a..e1f8c33033 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3909,9 +3909,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", "ref-cast", @@ -3922,14 +3922,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -3993,13 +3993,13 @@ dependencies = [ [[package]] name = "serde_derive_internals" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] From 44460d49e1719f0105cdc02ff1f9226f1283943e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:08:03 +0200 Subject: [PATCH 255/390] Update cargo-bins/cargo-binstall action to v1.21.1 (#27476) --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 30f58d51f7..7705906eef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -540,7 +540,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo-binstall" - uses: cargo-bins/cargo-binstall@ead08b90bd7b2e6d81963fb9cf0b7239f66d5db4 # v1.21.0 + uses: cargo-bins/cargo-binstall@e00d2c94cc0067b77737821097a62d91c0301baa # v1.21.1 - name: "Install cargo-fuzz" # Download the latest version from quick install and not the github releases because github releases only has MUSL targets. run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm @@ -811,7 +811,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: cargo-bins/cargo-binstall@ead08b90bd7b2e6d81963fb9cf0b7239f66d5db4 # v1.21.0 + - uses: cargo-bins/cargo-binstall@e00d2c94cc0067b77737821097a62d91c0301baa # v1.21.1 - run: cargo binstall --no-confirm cargo-shear@1.12.4 - run: cargo shear --deny-warnings From 26435b552dfce5c3e63e090f3d91286492e7e2b7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:08:16 +0200 Subject: [PATCH 256/390] Update prek dependencies (#27480) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ea178f587f..94890905af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -113,13 +113,13 @@ repos: priority: 0 - repo: https://github.com/astral-sh/uv-pre-commit - rev: 5900cba2cfe6d20f562458d4d308ac55569f92eb # frozen: 0.12.0 + rev: 8ff2449591c8de025b17661ba76d60237a1ae62b # frozen: 0.12.1 hooks: - id: uv-lock priority: 0 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: cb8c523fd4835aba42af70f4cad5568db4df0b6c # frozen: v0.16.0 + rev: 39d9ac5938dadb73df0564a45f163e25ff9fa6e2 # frozen: v0.16.1 hooks: - id: ruff-format exclude: crates/ty_python_semantic/resources/corpus/ @@ -127,7 +127,7 @@ repos: # Priority 1: Second-pass fixers (e.g., markdownlint-fix runs after mdformat). - repo: https://github.com/astral-sh/ruff-pre-commit - rev: cb8c523fd4835aba42af70f4cad5568db4df0b6c # frozen: v0.16.0 + rev: 39d9ac5938dadb73df0564a45f163e25ff9fa6e2 # frozen: v0.16.1 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix] @@ -150,7 +150,7 @@ repos: # Priority 2: ruffen-docs runs after markdownlint-fix (both modify markdown). - repo: https://github.com/astral-sh/ruff-pre-commit - rev: cb8c523fd4835aba42af70f4cad5568db4df0b6c # frozen: v0.16.0 + rev: 39d9ac5938dadb73df0564a45f163e25ff9fa6e2 # frozen: v0.16.1 hooks: - id: ruff-format name: mdtest format From 8348f2f0644a6d99f90a29f3feaed84d1bf13420 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:08:26 +0200 Subject: [PATCH 257/390] Update dependency ruff to v0.16.1 (#27479) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 8dba234ac0..9187a9af0d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ PyYAML==6.0.3 -ruff==0.16.0 +ruff==0.16.1 mkdocs==1.6.1 mkdocs-material==9.7.7 mkdocs-redirects==1.2.3 From 101d25d79b5e733849432bf5bbee3d25cea7b951 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:08:44 +0200 Subject: [PATCH 258/390] Update dependency prek to v0.4.11 (#27478) --- pyproject.toml | 2 +- uv.lock | 114 ++++++++++++++++++++++++------------------------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 03409de6a0..3a209bf5ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ exclude = [ [dependency-groups] dev = [ - "prek==0.4.10", + "prek==0.4.11", ] release = [ "rooster==0.1.1", diff --git a/uv.lock b/uv.lock index 2fd9825d8f..cbd2bdc168 100644 --- a/uv.lock +++ b/uv.lock @@ -30,8 +30,8 @@ name = "anyio" version = "4.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", marker = "python_full_version == '3.12.*'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } wheels = [ @@ -43,7 +43,7 @@ name = "anysqlite" version = "0.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.12'" }, + { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/4b/cd5d66b9f87e773bc71344a368b9472987e33514e6627e28342b9c3e7c43/anysqlite-0.0.5.tar.gz", hash = "sha256:9dfcf87baf6b93426ad1d9118088c41dbf24ef01b445eea4a5d486bac2755cce", size = 3432, upload-time = "2023-10-02T13:49:25.135Z" } wheels = [ @@ -64,7 +64,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "python_full_version >= '3.12' and implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -176,11 +176,11 @@ name = "hishel" version = "0.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.12'" }, - { name = "anysqlite", marker = "python_full_version >= '3.12'" }, - { name = "httpx", marker = "python_full_version >= '3.12'" }, - { name = "msgpack", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "anyio" }, + { name = "anysqlite" }, + { name = "httpx" }, + { name = "msgpack" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/64/a104ccac48f123f853254483617b16e0efc1649bd7e35bcdc5a5a5ef0ae2/hishel-0.1.5.tar.gz", hash = "sha256:9d40c682cd94fd6e1394fb05713ae20a75ed8aeba6f5272380444039ce6257f2", size = 75468, upload-time = "2025-10-18T13:32:41.854Z" } wheels = [ @@ -192,8 +192,8 @@ name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi", marker = "python_full_version >= '3.12'" }, - { name = "h11", marker = "python_full_version >= '3.12'" }, + { name = "certifi" }, + { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ @@ -205,10 +205,10 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.12'" }, - { name = "certifi", marker = "python_full_version >= '3.12'" }, - { name = "httpcore", marker = "python_full_version >= '3.12'" }, - { name = "idna", marker = "python_full_version >= '3.12'" }, + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ @@ -229,7 +229,7 @@ name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mdurl", marker = "python_full_version >= '3.12'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ @@ -338,26 +338,26 @@ wheels = [ [[package]] name = "prek" -version = "0.4.10" +version = "0.4.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/54/edc21e275f9fa3540d4d98cf349c2de11621d6729cc401bb7aedf563609e/prek-0.4.10.tar.gz", hash = "sha256:db3122f4e780eb4587635e6a83df881caf2dbb1eb7799d1cca51158216d6f33b", size = 502565, upload-time = "2026-07-16T10:13:00.788Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/1a/73b6dae5ce7e997cb35a69bfe1d25a798e85fa3d2eabf95324563f461b30/prek-0.4.11.tar.gz", hash = "sha256:4a14cb9bbae850605ae3904fbdbb12f0e00c12455efaa2266da8fb8e5c0350d7", size = 516254, upload-time = "2026-07-24T17:05:35.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/e7/5a63528ba7b95b64f38db3e253aed49ee8e5e8ba16589889d2b7f809edb7/prek-0.4.10-py3-none-linux_armv6l.whl", hash = "sha256:023f302741d79301346c3088ba43a9592aff0ecdbe5ddc3019fa9b1183319c5e", size = 5694609, upload-time = "2026-07-16T10:12:26.352Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ef/ee9e6bf9a5ce242e9e4e66ac4e2e9042a0f6fd9f367cee18ad404456e93d/prek-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:72adc707e16f97564bbae08d22b222ac3bb2491f8fbfb5a0754f80d472c28a71", size = 6044037, upload-time = "2026-07-16T10:12:28.539Z" }, - { url = "https://files.pythonhosted.org/packages/68/7e/da08cc39e5348ccb9234e63a21ee56861f72e8497d6a78f0db1ccae6515d/prek-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:04c9321957e1b32e1fc7cf60bb4f90bba3761f8659d5551ed04f96e25596de49", size = 5535983, upload-time = "2026-07-16T10:12:30.691Z" }, - { url = "https://files.pythonhosted.org/packages/30/c6/0486a35bb687a9beac7a5810bd1104c6da56d469b30b1eeaeefd03c99da2/prek-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e66ccf6c5e4ebadd05cd98cb338d7f553e4d27aa243cf91279c5a569b3cdccc7", size = 5862085, upload-time = "2026-07-16T10:12:33.042Z" }, - { url = "https://files.pythonhosted.org/packages/52/39/277fe17ae1f121e532e3942456f5a6d01ddacfbc550e481dcb359be7a1b0/prek-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63f9061d75a50ef0ca92c4b596ad352937a845df80758244950e513b27e9e18f", size = 5605697, upload-time = "2026-07-16T10:12:35.498Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a1/08354af3e000f2656fad086690d834eab6c04631ff41313a219ea6232199/prek-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c2ff7110e4bfaafbbab13c2893a337081aca61ed797f14b6b224d2ea9741eef", size = 6034111, upload-time = "2026-07-16T10:12:37.545Z" }, - { url = "https://files.pythonhosted.org/packages/e4/74/4702396c8d486132e5ce009ab56a0b37f50cb6866830d371f2617b7bdfdc/prek-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b696a05542e79aa27bcce68d1792e77f4fe6f9c6b012b34d74d62f964f3c72d", size = 6787203, upload-time = "2026-07-16T10:12:40.031Z" }, - { url = "https://files.pythonhosted.org/packages/90/29/b5d5d6fb87ebd64b37471e3e79761de9983f85e14d69c522efe7af6620ce/prek-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:431b44d6054e72815b4b05e1173596dfd02a7f7461211d40a2e3117e414642ad", size = 6261333, upload-time = "2026-07-16T10:12:42.216Z" }, - { url = "https://files.pythonhosted.org/packages/94/d6/54ba696d19f7efdc184093353cce713a850aef9c3556e23faeecafa22e94/prek-0.4.10-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ccbd2b4fd1df790087ba18b4506f680471922a5f13714f19801568434a040dee", size = 5867761, upload-time = "2026-07-16T10:12:44.329Z" }, - { url = "https://files.pythonhosted.org/packages/be/7d/3975098aa2baaabfc10f99f9fcf78045c4f10851beed8e9812b6a2688eab/prek-0.4.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:479e7480b447191aa5c6ed67e80f081d0f5ee4e878b140f4d2cee44165395f1c", size = 5714412, upload-time = "2026-07-16T10:12:46.297Z" }, - { url = "https://files.pythonhosted.org/packages/97/c0/3e0aac190fe95fdef98526343559b61d4d9fd54444c8c9137ba02412afe1/prek-0.4.10-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0bb7451025cbd2b68e480a13cf665d7a5c87c8b87bf18549a78985c17df817ed", size = 5578145, upload-time = "2026-07-16T10:12:48.261Z" }, - { url = "https://files.pythonhosted.org/packages/d7/44/7b26035534204b8b8a9d5e625479201e616413d287262f557cb32e1f8d77/prek-0.4.10-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4fb047e5776676805794574b2d7b178cb3ab536793aadf172419fcda56b34a57", size = 5889245, upload-time = "2026-07-16T10:12:50.818Z" }, - { url = "https://files.pythonhosted.org/packages/7e/6c/178a9d768876b4211a1bf63907fe308ae02d173639bcf41cea3c5eed35c1/prek-0.4.10-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:08318818d19caf79643babb89f872c92fda134a622b4731df1d6ed61e29d2d26", size = 6372849, upload-time = "2026-07-16T10:12:52.952Z" }, - { url = "https://files.pythonhosted.org/packages/4d/84/d5f5ac8193602883f9dd1d675d9d4084e34fbe3ed2ef50a0c336d8a53d8f/prek-0.4.10-py3-none-win32.whl", hash = "sha256:092872714dcde480a662bbdd98b980b248c2d3e10543d4d53a3a58cc9e5b35b0", size = 5413005, upload-time = "2026-07-16T10:12:55.113Z" }, - { url = "https://files.pythonhosted.org/packages/41/63/9e648fda10bc02c9b6ba305f93b6a6e4fd37d23d13a269a9d2d6bb44eaa1/prek-0.4.10-py3-none-win_amd64.whl", hash = "sha256:3d323a18d0f8c50e474a8fa29fb93bd2db680116d8afb19b76e72ad4667f58e6", size = 5799075, upload-time = "2026-07-16T10:12:56.963Z" }, - { url = "https://files.pythonhosted.org/packages/22/74/b34d8c80cec8dccc7b922c75b9dca62b18b603b5ed2eea93c9d7c2928d2d/prek-0.4.10-py3-none-win_arm64.whl", hash = "sha256:5e93865ef96756c4a26f37ece04ad514abbc19ae6a23ed1a507b6314e6a0d2fb", size = 5563955, upload-time = "2026-07-16T10:12:59.07Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d7/a00b2de492a80e99b1698e72c1d196ac3ed544dc7ee0ada261ac066e78e0/prek-0.4.11-py3-none-linux_armv6l.whl", hash = "sha256:3830cb7cc47e837888b8b464ecb21355a69235cfacef0fd89101e17f09345d63", size = 5770511, upload-time = "2026-07-24T17:05:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1e/f97c74defcd5d5645888cb99fcdd9b8b48cc7247cf31e64414d106d56d66/prek-0.4.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45facadf9c2332b28e6ab2744312ae0275a93ab5a437da8bcc53a8c7260cb4b0", size = 6118049, upload-time = "2026-07-24T17:05:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/d9/92/8367d26421ee6fe6019a63928fb0ed31179cd0d6199879c524f89ef4c95c/prek-0.4.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2f8a194b1d00d24dff8baff691c99e2668339da2da3482b4ee99c1a0f2409378", size = 5601478, upload-time = "2026-07-24T17:05:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6f/7617c9b87afaadede4167720aae7ef12d7e51167db903bc8fbac0cadef7b/prek-0.4.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:21d93e6d76bf3d7a9bb70c6ac86ef372adaee50068c45749e6b9e1c2ac4ec939", size = 5932071, upload-time = "2026-07-24T17:05:17.217Z" }, + { url = "https://files.pythonhosted.org/packages/ef/41/6796a4011b04212333259064aa885a71d03d9581ba9bff52db3ce58d1f06/prek-0.4.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7fb07cde2d2156efa6980122b3f13dc88f20c84b933c6205675d0f1e7be2cde8", size = 5677617, upload-time = "2026-07-24T17:05:18.658Z" }, + { url = "https://files.pythonhosted.org/packages/03/5f/3e339901f8460b6073313619b4fd9bf4135e48430695c53640b83ace88ea/prek-0.4.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b7f5e446d2aca739bd18b380578e9c78bb4c086299abc3e167d51c47840c56f", size = 6106370, upload-time = "2026-07-24T17:05:20.219Z" }, + { url = "https://files.pythonhosted.org/packages/8e/4e/94e24b5c1910ec15692ecaf33d0d8ef0d02a02a8b5e40d3c976396880cba/prek-0.4.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7059d640e595d098600e2d97af961f46292b4112670edbe632de84f6828389e", size = 6884342, upload-time = "2026-07-24T17:05:21.587Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7c/fc0daa033dcafe74990c00af2da1e16922790b9c1b8da7e8eebf1123838b/prek-0.4.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a4df33998fcac878bce3b2c624e1caeb9609f3d562c640702c1234ed815daf", size = 6331365, upload-time = "2026-07-24T17:05:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/d7/36/49f152b8f539930e9685cff0509695ce4054abcecc22f4c16c4e1e5c23d0/prek-0.4.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:866991f387527c5f880ce2cc3ddea9b33cc5b986881f8c7f92524cf0969c1350", size = 5939075, upload-time = "2026-07-24T17:05:24.249Z" }, + { url = "https://files.pythonhosted.org/packages/4f/fe/7b097af9161edae7ddedcc9f5cda4a5f31346a492ce3f92583d96c46f628/prek-0.4.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:247e5d8740e137ebdf24fa96f01a95b2d2f1892e956a1ab00c7b1474a59ebcc0", size = 5799029, upload-time = "2026-07-24T17:05:25.652Z" }, + { url = "https://files.pythonhosted.org/packages/49/63/dc955ff99e1002d3cd375b21467c87046bc41deddfce86d898240757b151/prek-0.4.11-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:603ba9f2fd9d666dddb3ae190a25a5c55091b843cde90ed52e0a1116f50d4062", size = 5651211, upload-time = "2026-07-24T17:05:27.027Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/bf2139ec25eefb5afef43afac7620c5181f2c2661f220598beacb1771207/prek-0.4.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f2682ece3c5fc7201106c4fdd84b0587ccea4b8a2ecefa3e94d3074d2841f1df", size = 5954784, upload-time = "2026-07-24T17:05:28.391Z" }, + { url = "https://files.pythonhosted.org/packages/fc/29/3fe5990aee1bd7c4d50a03358ad867c5e912d9510aafb83a359c7347e5fa/prek-0.4.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:22721d30394192931fecc80d6fc6dd47e8ddf8db8b9693805aba8ec0f50087ec", size = 6448916, upload-time = "2026-07-24T17:05:29.837Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fb/abddacf43738302242ecd237236c7c80cd7c1d27545e16e803770aee76e9/prek-0.4.11-py3-none-win32.whl", hash = "sha256:8b093e7624522146049e994d5cf283d01d71632b638620b79ae0f96afeaa2624", size = 5483539, upload-time = "2026-07-24T17:05:31.228Z" }, + { url = "https://files.pythonhosted.org/packages/00/1e/c293f7a15cb93963c4be36a02e144b93f43906bc02644a3f04e5708e7453/prek-0.4.11-py3-none-win_amd64.whl", hash = "sha256:5a3d7c80b970b456e5f1bcec8382008ee1ae6a3f324a6b9bb4ff7e666ab0f3c4", size = 5861119, upload-time = "2026-07-24T17:05:32.479Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0c/05fe6eb9d6a54d0e02dfa8cc5ad6f86869bf953419ec15909a32466a28ee/prek-0.4.11-py3-none-win_arm64.whl", hash = "sha256:e7b0df37ce05e45a14a9da39ab104691474d72f139bf4f6c860f754763a322cb", size = 5626386, upload-time = "2026-07-24T17:05:33.813Z" }, ] [[package]] @@ -374,10 +374,10 @@ name = "pydantic" version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types", marker = "python_full_version >= '3.12'" }, - { name = "pydantic-core", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.12'" }, + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ @@ -389,7 +389,7 @@ name = "pydantic-core" version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ @@ -519,7 +519,7 @@ name = "pygit2" version = "1.19.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "python_full_version >= '3.12'" }, + { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/44/415aa93422b4bfc21a6448acb7e16280d5f33a9a3fae38a384e37b046ae4/pygit2-1.19.3.tar.gz", hash = "sha256:a543e6d4ebb43825564935758dc234e770016fed673b84370d46ae9580558831", size = 810489, upload-time = "2026-06-13T08:06:04.982Z" } wheels = [ @@ -594,8 +594,8 @@ name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "markdown-it-py" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ @@ -607,14 +607,14 @@ name = "rooster" version = "0.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "hishel", marker = "python_full_version >= '3.12'" }, - { name = "httpx", marker = "python_full_version >= '3.12'" }, - { name = "marko", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pydantic", marker = "python_full_version >= '3.12'" }, - { name = "pygit2", marker = "python_full_version >= '3.12'" }, - { name = "tqdm", marker = "python_full_version >= '3.12'" }, - { name = "typer", marker = "python_full_version >= '3.12'" }, + { name = "hishel" }, + { name = "httpx" }, + { name = "marko" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pygit2" }, + { name = "tqdm" }, + { name = "typer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/02/8ce565271dc52bd0d0d812043b12ec60111d947f81dc30301d19d7bfd453/rooster-0.1.1.tar.gz", hash = "sha256:c9823122f0c2b035985e70384323cdd353477af988e0f065bc302646a49da482", size = 18608, upload-time = "2025-10-29T15:18:49.478Z" } wheels = [ @@ -637,7 +637,7 @@ release = [ [package.metadata] [package.metadata.requires-dev] -dev = [{ name = "prek", marker = "python_full_version >= '3.12'", specifier = "==0.4.10" }] +dev = [{ name = "prek", marker = "python_full_version >= '3.12'", specifier = "==0.4.11" }] release = [{ name = "rooster", marker = "python_full_version >= '3.12'", specifier = "==0.1.1" }] [[package]] @@ -654,7 +654,7 @@ name = "tqdm" version = "4.68.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } wheels = [ @@ -666,10 +666,10 @@ name = "typer" version = "0.26.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "rich", marker = "python_full_version >= '3.12'" }, - { name = "shellingham", marker = "python_full_version >= '3.12'" }, + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } wheels = [ @@ -690,7 +690,7 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ From a16cb1b923477f23b92655ee3a5019c9a1376f8b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:12:40 +0200 Subject: [PATCH 259/390] Update NPM Development dependencies (#27489) --- playground/api/package-lock.json | 435 ++++++++++++++++++------------- 1 file changed, 260 insertions(+), 175 deletions(-) diff --git a/playground/api/package-lock.json b/playground/api/package-lock.json index 2aebfef42e..cf8d13533e 100644 --- a/playground/api/package-lock.json +++ b/playground/api/package-lock.json @@ -46,9 +46,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260721.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260721.1.tgz", - "integrity": "sha512-VivNMhiEdZIB4JBWxf1RMJGROErv53qmQ+dvhjA1evrCouvqRYW718VqDideU3PSV7Ythl5Df48NqZYWoaEHpQ==", + "version": "1.20260722.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260722.1.tgz", + "integrity": "sha512-vZOP8vIS3NwnuaO+gz0FZ7kIGeiO3bZmxV35Ph9zOXKSREhDFlH7wQ7mkCdhW3O4jnXsew+XT7b+DNEI2CcJGQ==", "cpu": [ "x64" ], @@ -63,9 +63,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260721.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260721.1.tgz", - "integrity": "sha512-k7oye1ZiuwnnBBA2eTMduconr/ud5ZxFtRNTsYwMdmJeeeislw2+M72otrHxxvybCP7JWPPlJ38uhfajpcyhOA==", + "version": "1.20260722.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260722.1.tgz", + "integrity": "sha512-EmIQymihDq6WNdER4+LF8Qn80yqayBUpJ+tkOO7wmY8pmgfyXjIUFNXotl21AHovTeu2seR7HdVUgeN/BilCWw==", "cpu": [ "arm64" ], @@ -80,9 +80,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260721.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260721.1.tgz", - "integrity": "sha512-hon0lW4ZQ4boAVgaw+0ZFTNS8v5MWPWvK0HZnt4tDpKYnDUviLZawtUW3KqvFmCQTipVHl1S34j3J8Eqb93hGQ==", + "version": "1.20260722.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260722.1.tgz", + "integrity": "sha512-jvZ3k9fxcnEn04s80CgIYxQfpOyAiz/8qC42DP8EBa9tR27qWyg9wmm31zIobVlrgBZn/+8NfdP73avRGcQOjQ==", "cpu": [ "x64" ], @@ -97,9 +97,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260721.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260721.1.tgz", - "integrity": "sha512-nAl+HRQqpX5b7xVwWcvLPZmCk8NQ2yjI0yvJTWcHiRswbMEg1ZZckVmjJUAn0PHzZARbCSyIV7v3UjM+SPRmIQ==", + "version": "1.20260722.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260722.1.tgz", + "integrity": "sha512-BOSB55SMNdy+DA5uj2WirgiNanpHGis5PVvXH1wSfvjRKr4JGgWK+EZzxz0RFUo6QjjQQC/NimEzNZ7va7jmKg==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260721.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260721.1.tgz", - "integrity": "sha512-9paFG5cMTKz/CRixnEEnZbe5uvFPBFSDthxJHANfCWhUtBj49GSL1FPIokIg+Q+H8DGJEExU0lL92LtxD0lTxQ==", + "version": "1.20260722.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260722.1.tgz", + "integrity": "sha512-sYM8YgUpKnRz2xjvdJLX1Ojzoi4MlA4gk8WTTExhGydjYB2UTs5NIbv0ZmpKgMoK9io3ixgmiW56ZnTbcWOdiA==", "cpu": [ "x64" ], @@ -131,9 +131,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "5.20260722.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260722.1.tgz", - "integrity": "sha512-8+kivCgFGzwrAfNOWgSpzy/VDvmT/i5KWBgQhnygv3d1kajNn6mCYTbLKpouG0aY8mXjhv+IQm1a8r2K/H4pqQ==", + "version": "5.20260729.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260729.1.tgz", + "integrity": "sha512-X5r/4y0gKMq/B72qkz/tEwNK4c3v2regT3to6Ia8qqC66E0+YIN/fU7x0JG6ej2rLxtU3TV3aVhfK9y8+jMMAw==", "dev": true, "license": "MIT OR Apache-2.0" }, @@ -150,9 +150,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "dev": true, "license": "MIT", "optional": true, @@ -603,9 +603,9 @@ } }, "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "dev": true, "license": "MIT", "engines": { @@ -613,9 +613,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz", + "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==", "cpu": [ "arm64" ], @@ -626,19 +626,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.1" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz", + "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==", "cpu": [ "x64" ], @@ -649,19 +649,39 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz", + "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.2" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz", + "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==", "cpu": [ "arm64" ], @@ -676,9 +696,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz", + "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==", "cpu": [ "x64" ], @@ -693,13 +713,16 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz", + "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -710,13 +733,16 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz", + "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -727,13 +753,16 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz", + "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -744,13 +773,16 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz", + "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -761,13 +793,16 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz", + "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -778,13 +813,16 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz", + "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -795,13 +833,16 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz", + "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -812,13 +853,16 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz", + "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -829,213 +873,254 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz", + "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.1" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz", + "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.1" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz", + "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.1" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz", + "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.1" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz", + "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.1" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz", + "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.1" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz", + "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz", + "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.1" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz", + "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz", + "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==", "cpu": [ "wasm32" ], "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.2" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz", + "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==", "cpu": [ "arm64" ], @@ -1046,16 +1131,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz", + "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==", "cpu": [ "ia32" ], @@ -1066,16 +1151,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz", + "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==", "cpu": [ "x64" ], @@ -1086,7 +1171,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1754,16 +1839,16 @@ } }, "node_modules/miniflare": { - "version": "4.20260721.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260721.0.tgz", - "integrity": "sha512-fBLaCxZ2i/nPH8iyLzvza0C8/sSF4sjD1ma1Skf+pkZVK0TlaW5ujHJlUHwcwR66v2JZt+Q28d4DCX/oaLG0cA==", + "version": "4.20260722.1", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260722.1.tgz", + "integrity": "sha512-FJIg4omaCb2wwSyOeRosEdmVRi3JzGAOOH3pa3twmmtvWECl6BVZMIDwJbjByLKRyU+mrfk0M/n3oSiojFZvSA==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", - "sharp": "0.34.5", + "sharp": "0.35.2", "undici": "7.28.0", - "workerd": "1.20260721.1", + "workerd": "1.20260722.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, @@ -1863,9 +1948,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1875,48 +1960,48 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", + "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", "dev": true, - "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.4" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.2", + "@img/sharp-darwin-x64": "0.35.2", + "@img/sharp-freebsd-wasm32": "0.35.2", + "@img/sharp-libvips-darwin-arm64": "1.3.1", + "@img/sharp-libvips-darwin-x64": "1.3.1", + "@img/sharp-libvips-linux-arm": "1.3.1", + "@img/sharp-libvips-linux-arm64": "1.3.1", + "@img/sharp-libvips-linux-ppc64": "1.3.1", + "@img/sharp-libvips-linux-riscv64": "1.3.1", + "@img/sharp-libvips-linux-s390x": "1.3.1", + "@img/sharp-libvips-linux-x64": "1.3.1", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", + "@img/sharp-libvips-linuxmusl-x64": "1.3.1", + "@img/sharp-linux-arm": "0.35.2", + "@img/sharp-linux-arm64": "0.35.2", + "@img/sharp-linux-ppc64": "0.35.2", + "@img/sharp-linux-riscv64": "0.35.2", + "@img/sharp-linux-s390x": "0.35.2", + "@img/sharp-linux-x64": "0.35.2", + "@img/sharp-linuxmusl-arm64": "0.35.2", + "@img/sharp-linuxmusl-x64": "0.35.2", + "@img/sharp-webcontainers-wasm32": "0.35.2", + "@img/sharp-win32-arm64": "0.35.2", + "@img/sharp-win32-ia32": "0.35.2", + "@img/sharp-win32-x64": "0.35.2" } }, "node_modules/shebang-command": { @@ -2069,9 +2154,9 @@ } }, "node_modules/workerd": { - "version": "1.20260721.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260721.1.tgz", - "integrity": "sha512-b/DWhpV0jTudzQpLhDovcOgBz233386q+3Hbari7CLCNT9UXxjQziSTZ9yCoKdT2K3TSx5jrwlOisq8hlLWXYg==", + "version": "1.20260722.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260722.1.tgz", + "integrity": "sha512-NycKuc1x2onvsRfGGpM093vRlLFU2zHDAM0+APpccfg4+gZxDGCH27RmdDvkeBuoZyYqgLo3oAfF6re4mvC3vQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -2082,17 +2167,17 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260721.1", - "@cloudflare/workerd-darwin-arm64": "1.20260721.1", - "@cloudflare/workerd-linux-64": "1.20260721.1", - "@cloudflare/workerd-linux-arm64": "1.20260721.1", - "@cloudflare/workerd-windows-64": "1.20260721.1" + "@cloudflare/workerd-darwin-64": "1.20260722.1", + "@cloudflare/workerd-darwin-arm64": "1.20260722.1", + "@cloudflare/workerd-linux-64": "1.20260722.1", + "@cloudflare/workerd-linux-arm64": "1.20260722.1", + "@cloudflare/workerd-windows-64": "1.20260722.1" } }, "node_modules/wrangler": { - "version": "4.113.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.113.0.tgz", - "integrity": "sha512-ROGzSloJv0y21It6Oc9LaruNcu1tdiQ/XzL3Jc3YkFjzXEMXzTqVhA8vQaGMTdZHTjFP0PVcwAHNgaw3gXu4wA==", + "version": "4.115.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.115.0.tgz", + "integrity": "sha512-+upG2VW66M1sjb43yzgUZ6Ss8iYpJ6+7F3U4GF8TY5EYd+08sYdS54d24AEwCnhuef3J9KlSPusqiqR9WYy1UA==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { @@ -2100,10 +2185,10 @@ "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "4.20260721.0", + "miniflare": "4.20260722.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260721.1" + "workerd": "1.20260722.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -2117,7 +2202,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^5.20260721.1" + "@cloudflare/workers-types": "^5.20260722.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { From 21b866ea00f257397b4e4a34cde70b8246a917d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:12:59 +0200 Subject: [PATCH 260/390] Update docker/login-action action to v4.5.2 (#27488) --- .github/workflows/build-docker.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 06f39d7d98..c97856d8f6 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -46,7 +46,7 @@ jobs: - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + - uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2 if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} with: registry: ghcr.io @@ -142,7 +142,7 @@ jobs: type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }} type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }} - - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + - uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -204,7 +204,7 @@ jobs: steps: - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + - uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -322,7 +322,7 @@ jobs: type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }} type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }} - - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 + - uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2 with: registry: ghcr.io username: ${{ github.repository_owner }} From 7b025a54e6cbad5e00f2be755d65980ba4992da0 Mon Sep 17 00:00:00 2001 From: GiGaGon Date: Tue, 4 Aug 2026 23:37:21 -0700 Subject: [PATCH 261/390] [ty] [playground] Make run panel scrollable and wrapping (#27474) --- playground/ty/src/Editor/Chrome.tsx | 2 +- playground/ty/src/Editor/SecondaryPanel.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/playground/ty/src/Editor/Chrome.tsx b/playground/ty/src/Editor/Chrome.tsx index b806cd18ff..8c3642bad5 100644 --- a/playground/ty/src/Editor/Chrome.tsx +++ b/playground/ty/src/Editor/Chrome.tsx @@ -171,7 +171,7 @@ export default function Chrome({ From 3e628b8ef074b270d44a182a272e091117db4405 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 5 Aug 2026 08:39:56 +0200 Subject: [PATCH 262/390] [ty] Update Salsa to 0.28.2 (#27496) --- Cargo.lock | 14 +++++++------- Cargo.toml | 2 +- fuzz/Cargo.lock | 14 +++++++------- fuzz/Cargo.toml | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1f8c33033..a5956f32d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3856,9 +3856,9 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a14fdadbf856222e731756d7fdbdf193a7abf8fdab009bb45f48671a42719a84" +checksum = "cf0e374215cd2db2b5c75d7b3a99cb0cc052c0595335dfdefc03d4eb08f4aa81" dependencies = [ "boxcar", "compact_str", @@ -3883,19 +3883,19 @@ dependencies = [ [[package]] name = "salsa-macro-rules" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d7dc08ba69b9aedfa61dfc4d65548ae42c0d8b90bbd62cd121776920841bcf" +checksum = "85f4b7d4405540bbd6d4ffa52d4322d983f3781954d3073067ac1bdb028459b3" [[package]] name = "salsa-macros" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5c48c5a4a53a6e2be9762f56566f1325d4394c011c00b3ea86ba2d13411e71" +checksum = "445be2bfbb2f67cb663225ecd7bc5a25370c0250fca30f9d8cbad9a913650370" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1f496bc91b..05a918187f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -158,7 +158,7 @@ regex-syntax = { version = "0.8.8" } rustc-hash = { version = "2.0.0" } rustc-stable-hash = { version = "0.1.2" } # When updating salsa, make sure to also update the version in `fuzz/Cargo.toml` -salsa = { version = "0.28.1", default-features = false, features = [ +salsa = { version = "0.28.2", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 9767b80f2d..10def6231b 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1905,9 +1905,9 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a14fdadbf856222e731756d7fdbdf193a7abf8fdab009bb45f48671a42719a84" +checksum = "cf0e374215cd2db2b5c75d7b3a99cb0cc052c0595335dfdefc03d4eb08f4aa81" dependencies = [ "boxcar", "compact_str", @@ -1932,19 +1932,19 @@ dependencies = [ [[package]] name = "salsa-macro-rules" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d7dc08ba69b9aedfa61dfc4d65548ae42c0d8b90bbd62cd121776920841bcf" +checksum = "85f4b7d4405540bbd6d4ffa52d4322d983f3781954d3073067ac1bdb028459b3" [[package]] name = "salsa-macros" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5c48c5a4a53a6e2be9762f56566f1325d4394c011c00b3ea86ba2d13411e71" +checksum = "445be2bfbb2f67cb663225ecd7bc5a25370c0250fca30f9d8cbad9a913650370" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 3a41d55495..92c7b21e30 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -31,7 +31,7 @@ ty_vendored = { path = "../crates/ty_vendored" } ty_python_core = { path = "../crates/ty_python_core" } libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false } -salsa = { version = "0.28.1", default-features = false, features = [ +salsa = { version = "0.28.2", default-features = false, features = [ "compact_str", "macros", "salsa_unstable", From bc0422e25ea2c5f567f9011679cd9c6207dc9d75 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 5 Aug 2026 09:27:26 +0200 Subject: [PATCH 263/390] [ty] Select benchmark projects in memory reports (#27499) --- scripts/memory_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/memory_report.py b/scripts/memory_report.py index cbbb93497e..35226a1929 100644 --- a/scripts/memory_report.py +++ b/scripts/memory_report.py @@ -280,7 +280,7 @@ def run_ty_memory_check( print(f"Running {ty_path} on {project_path.name}...", file=sys.stderr) result = subprocess.run( - [ty_path, "check", str(project_path), "--exit-zero"], + [ty_path, "check", "--project", str(project_path), "--exit-zero"], capture_output=True, text=True, env=env, From 082dc5b8d5ee6c07a6d328b4ef86e43d505151d3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Aug 2026 06:50:48 -0400 Subject: [PATCH 264/390] [ty] Infer generic TypedDicts from unpacked TypedDicts (#27439) ## Summary We now allow unpacked `TypedDict` values to contribute their field types when constructing a generic `TypedDict`: ```py from typing import TypedDict class Source(TypedDict): value: int class Box[T](TypedDict): value: T def f(source: Source) -> None: reveal_type(Box(**source)) # Box[int] ``` This reuses the generic constructor introduced in #27436 and preserves the individual field types of each unpacked `TypedDict`, including calls with multiple unpacked sources. --- .../resources/mdtest/typed_dict.md | 87 +++++++++++++++++++ .../src/types/infer/builder/typed_dict.rs | 38 +++++--- 2 files changed, 115 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index a715eafdd2..1f4a532cbb 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -3645,6 +3645,93 @@ reveal_type(Pair(**{"first": 1, "second": "x"})) # revealed: Pair[Unknown] reveal_type(Pair(**{"first": 1}, **{"second": "x"})) # revealed: Pair[Unknown] ``` +### Constructor inference from unpacked TypedDicts + +Unpacking a `TypedDict` with required keys contributes each field's type to constructor inference. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import NotRequired, TypedDict + +class Source(TypedDict): + value: int + +class Box[T](TypedDict): + value: T + +def unpack(source: Source): + reveal_type(Box(**source)) # revealed: Box[int] +``` + +A type alias to a single `TypedDict` contributes the same field information. + +```py +type SourceAlias = Source + +def unpack_alias(source: SourceAlias): + reveal_type(Box(**source)) # revealed: Box[int] +``` + +Different unpacked `TypedDict` arguments retain their separate field types. + +```py +class First(TypedDict): + first: int + +class Second(TypedDict): + second: str + +class Pair[T](TypedDict): + first: T + second: str + +def unpack_multiple(first: First, second: Second): + reveal_type(Pair(**first, **second)) # revealed: Pair[int] +``` + +An optional source key does not satisfy a required constructor field. + +```py +class MaybeSource(TypedDict): + value: NotRequired[int] + +def unpack_optional(source: MaybeSource): + Box(**source) # error: [missing-typed-dict-key] +``` + +### Constructor inference from unpacked TypedDict unions + +A union can associate different value types with different callbacks. Inference remains gradual +because combining those fields independently would reject a valid constructor call. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, TypedDict + +class IntSource(TypedDict): + value: int + callback: Callable[[int], None] + +class StrSource(TypedDict): + value: str + callback: Callable[[str], None] + +class Box[T](TypedDict): + value: T + callback: Callable[[T], None] + +def unpack(source: IntSource | StrSource): + reveal_type(Box(**source)) # revealed: Box[Unknown] +``` + ### Constructor inference from recursive fields Recursive construction remains valid even though the outer constructor cannot yet infer its type diff --git a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs index 109b600664..9abc4a8a92 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs @@ -449,6 +449,13 @@ impl<'db> TypeInferenceBuilder<'db, '_> { callable_type, Type::ClassLiteral(class_literal) if class_literal.generic_context(db).is_some() ); + if is_generic && arguments.args.is_empty() { + for keyword in &arguments.keywords { + if keyword.arg.is_none() && !keyword.value.is_dict_expr() { + self.get_or_infer_expression(&keyword.value, TypeContext::default()); + } + } + } let can_infer = is_generic && self.can_infer_generic_typed_dict_constructor(class, arguments, call_expression_tcx); @@ -560,17 +567,28 @@ impl<'db> TypeInferenceBuilder<'db, '_> { arguments.args.is_empty() && !has_gradual_class_context && arguments.keywords.iter().all(|keyword| { - let Some(name) = keyword.arg.as_ref() else { - return false; + let permits_field_inference = |name: &str| { + typed_dict.item(db, name).is_none_or(|field| { + !contains_generic_typed_dict( + db, + env, + field.declared_ty, + &ActiveRecursionDetector::default(), + ) + }) }; - typed_dict.item(db, name.id.as_str()).is_none_or(|field| { - !contains_generic_typed_dict( - db, - env, - field.declared_ty, - &ActiveRecursionDetector::default(), - ) - }) + + if let Some(name) = keyword.arg.as_ref() { + return permits_field_inference(name.id.as_str()); + } + + self.try_expression_type(&keyword.value) + .and_then(|ty| ty.resolve_type_alias(db).as_typed_dict()) + .is_some_and(|unpacked| { + unpacked.items(db).iter().all(|(name, field)| { + field.is_required() && permits_field_inference(name.as_str()) + }) + }) }) } From c0efa1a3d98040afcaf977f291ea92103a223320 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 5 Aug 2026 04:23:38 -0700 Subject: [PATCH 265/390] [ty] Avoid exponential slowdown copying narrowed TypedDict unions (#27492) ## Summary We didn't synthesize various methods/attributes of synthesized TypedDicts like we did for class-backed TypedDicts, instead falling back to a `@Todo` type. This meant that we built up large non-simplifiable unions of intersections (since `@Todo` types are dynamic and do not simplify) in cases where all class-backed TypedDicts would have returned the same type (e.g. a `dict_keys[str, object]`) and immediately collapsed the complex union/intersection into a simple type. Synthesizing these members and avoiding the `@Todo` type thus actually fixes a combinatorial-explosion performance issue. - Resolve synthesized `TypedDict` members directly from their schemas and reuse the existing class-backed fallback logic, including `Self` handling and explicit extra-item behavior. - Prevent undeclared-key membership narrowing from injecting dynamic placeholder constraints into discriminated `TypedDict` unions passed to `dict()`. - Reduces the 12-variant reproduction in astral-sh/ty#4176 from 12.97 seconds to 0.03 seconds; 48 variants complete in 0.04 seconds. Synthesized `TypedDict` schemas still lack a general-purpose meta-type representation; removing the remaining meta-type TODOs belongs in a separate follow-up. Closes astral-sh/ty#4176. ## Test plan - Add an mdtest covering a ten-variant discriminated `TypedDict` union after undeclared-key membership narrowing, including precise `keys()`, `items()`, `values()`, hidden-key indexing, `dict()`, and the narrowed `copy()` result. - Add a dedicated 12-variant Criterion regression benchmark for copying a narrowed `TypedDict` union. --- .../benches/ty_constraint_set.rs | 47 +++++++++++++++ .../resources/mdtest/typed_dict.md | 44 ++++++++++++++ crates/ty_python_semantic/src/types.rs | 3 + crates/ty_python_semantic/src/types/class.rs | 4 +- .../src/types/class/typed_dict.rs | 60 +++++++++++++++++-- 5 files changed, 151 insertions(+), 7 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty_constraint_set.rs b/crates/ruff_benchmark/benches/ty_constraint_set.rs index d726221e60..409cd451d6 100644 --- a/crates/ruff_benchmark/benches/ty_constraint_set.rs +++ b/crates/ruff_benchmark/benches/ty_constraint_set.rs @@ -294,6 +294,52 @@ def copy_narrowed_mapping(value: Item | Mapping[str, Any]) -> dict[str, object] }); } +fn benchmark_missing_key_typed_dict_union_copy(criterion: &mut Criterion) { + const NUM_VARIANTS: usize = 12; + + setup_rayon(); + + // Regression benchmark for https://github.com/astral-sh/ty/issues/4176. + let mut code = "from typing import Literal, NotRequired, TypedDict\n\n".to_string(); + for i in 0..NUM_VARIANTS { + writeln!( + &mut code, + "class Item{i}(TypedDict):\n kind: Literal[{i}]\n field_{i}: NotRequired[int]\n" + ) + .ok(); + } + + code.push_str("type Item = "); + for i in 0..NUM_VARIANTS { + if i > 0 { + code.push_str(" | "); + } + write!(&mut code, "Item{i}").ok(); + } + + code.push_str( + r#" + +def copy(value: Item) -> dict[str, object] | None: + if "missing" in value: + return dict(value) + return None +"#, + ); + + criterion.bench_function("ty_micro[missing_key_typed_dict_union_copy]", |b| { + b.iter_batched_ref( + || setup_micro_case(&code), + |case| { + let Case { db } = case; + let result = db.check(); + assert_eq!(result.len(), 0); + }, + BatchSize::SmallInput, + ); + }); +} + fn benchmark_recursive_typed_dict_union_contextual_inference(criterion: &mut Criterion) { const NUM_BRANCHES: usize = 11; @@ -568,6 +614,7 @@ criterion_group!( benchmark_many_upper_bound_callbacks, benchmark_pandas_tdd, benchmark_mixed_typed_dict_union_copy, + benchmark_missing_key_typed_dict_union_copy, benchmark_recursive_typed_dict_union_contextual_inference, benchmark_invariant_generic_return_union, benchmark_sequence_literal_union_access, diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 1f4a532cbb..5c1eac5632 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -2652,6 +2652,50 @@ def _(item: Item | str) -> None: reveal_type(dict(item)) # revealed: dict[str, object] ``` +A successful membership test for an undeclared key narrows each union member to an intersection with +a synthesized `TypedDict`. Its mapping methods should retain their precise types, and copying the +narrowed union should remain efficient even when each member has a distinct optional field: + +```py +from typing import NotRequired + +MembershipA = TypedDict("MembershipA", {"kind": Literal["a"], "field_a": NotRequired[int]}) +MembershipB = TypedDict("MembershipB", {"kind": Literal["b"], "field_b": NotRequired[int]}) +MembershipC = TypedDict("MembershipC", {"kind": Literal["c"], "field_c": NotRequired[int]}) +MembershipD = TypedDict("MembershipD", {"kind": Literal["d"], "field_d": NotRequired[int]}) +MembershipE = TypedDict("MembershipE", {"kind": Literal["e"], "field_e": NotRequired[int]}) +MembershipF = TypedDict("MembershipF", {"kind": Literal["f"], "field_f": NotRequired[int]}) +MembershipG = TypedDict("MembershipG", {"kind": Literal["g"], "field_g": NotRequired[int]}) +MembershipH = TypedDict("MembershipH", {"kind": Literal["h"], "field_h": NotRequired[int]}) +MembershipI = TypedDict("MembershipI", {"kind": Literal["i"], "field_i": NotRequired[int]}) +MembershipJ = TypedDict("MembershipJ", {"kind": Literal["j"], "field_j": NotRequired[int]}) + +type MembershipItem = ( + MembershipA + | MembershipB + | MembershipC + | MembershipD + | MembershipE + | MembershipF + | MembershipG + | MembershipH + | MembershipI + | MembershipJ +) + +def _(item: MembershipItem) -> None: + if "missing" in item: + reveal_type(item.keys()) # revealed: dict_keys[str, object] + reveal_type(item.items()) # revealed: dict_items[str, object] + reveal_type(item.values()) # revealed: dict_values[str, object] + reveal_type(item["missing"]) # revealed: object + reveal_type(dict(item)) # revealed: dict[str, object] + +def _(item: MembershipA) -> None: + if "missing" in item: + reveal_type(item.copy()) # revealed: MembershipA & +``` + Adding a regular dictionary to the union should not make copying it slow: ```py diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index eb2abe6fdd..3de7f3ca99 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2978,6 +2978,9 @@ impl<'db> Type<'db> { Type::Intersection(inter) => inter.map_with_boundness_and_qualifiers(db, env, |elem| { elem.class_member_with_policy(db, env, name, policy) }), + Type::TypedDict(TypedDictType::Synthesized(synthesized)) => { + class::synthesized_typed_dict_class_member(db, env, synthesized, policy, name) + } // TODO: Remove this once synthesized protocols have a precise meta-type. Type::ProtocolInstance(protocol) if protocol.class_origin(db).is_none() => { ty.instance_member(db, env, name) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 0136e37c41..222241649e 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -14,7 +14,9 @@ pub(crate) use self::static_literal::{ ExpandedClassBaseEntry, FrozenDataclassDispatch, StaticClassLiteral, expanded_class_base_entries, }; -pub(super) use self::typed_dict::{DynamicTypedDictAnchor, DynamicTypedDictLiteral}; +pub(super) use self::typed_dict::{ + DynamicTypedDictAnchor, DynamicTypedDictLiteral, synthesized_typed_dict_class_member, +}; use super::dedicated::pydantic; use super::{ BoundTypeVarIdentity, BoundTypeVarInstance, MemberLookupPolicy, MroIterator, SpecialFormType, diff --git a/crates/ty_python_semantic/src/types/class/typed_dict.rs b/crates/ty_python_semantic/src/types/class/typed_dict.rs index 5884deb616..fa2a1586fc 100644 --- a/crates/ty_python_semantic/src/types/class/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/class/typed_dict.rs @@ -17,8 +17,8 @@ use crate::types::member::Member; use crate::types::mro::Mro; use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; use crate::types::typed_dict::{ - TypedDictField, TypedDictOpenness, TypedDictSchema, deferred_functional_typed_dict_openness, - deferred_functional_typed_dict_schema, + SynthesizedTypedDictType, TypedDictField, TypedDictOpenness, TypedDictSchema, + deferred_functional_typed_dict_openness, deferred_functional_typed_dict_schema, }; use crate::types::{ BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, ClassType, KnownClass, @@ -1040,6 +1040,33 @@ impl<'db> DynamicTypedDictLiteral<'db> { } } +/// Resolves members of a schema that has no defining `TypedDict` class. +pub(in crate::types) fn synthesized_typed_dict_class_member<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + synthesized: SynthesizedTypedDictType<'db>, + lookup_policy: MemberLookupPolicy, + name: &str, +) -> PlaceAndQualifiers<'db> { + let typed_dict = TypedDictType::Synthesized(synthesized); + + if let Some(member) = synthesize_typed_dict_method(db, env, typed_dict, name, || { + TypedDictFields::Dynamic(synthesized.items(db)) + }) { + return Member::definitely_declared(member).inner; + } + + typed_dict_inherited_class_member( + db, + env, + typed_dict, + TypedDictModule::Typing, + lookup_policy, + name, + || Type::TypedDict(typed_dict), + ) +} + pub(super) fn typed_dict_fallback_class_member<'db>( db: &'db dyn Db, env: &ProgramEnvironment<'db>, @@ -1067,18 +1094,39 @@ pub(super) fn typed_dict_class_member<'db>( name: &str, ) -> PlaceAndQualifiers<'db> { let self_class = class.class_literal(db); + + typed_dict_inherited_class_member( + db, + env, + TypedDictType::new(class), + module, + lookup_policy, + name, + || determine_upper_bound(db, env, self_class, ClassBase::is_typed_dict), + ) +} + +fn typed_dict_inherited_class_member<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + typed_dict: TypedDictType<'db>, + module: TypedDictModule, + lookup_policy: MemberLookupPolicy, + name: &str, + new_upper_bound: impl FnOnce() -> Type<'db>, +) -> PlaceAndQualifiers<'db> { let fallback_member = typed_dict_fallback_class_member(db, env, module, lookup_policy, name) .map_type(|ty| { - let new_upper_bound = - determine_upper_bound(db, env, self_class, ClassBase::is_typed_dict); - let mapping = TypeMapping::ReplaceSelf { new_upper_bound }; + let mapping = TypeMapping::ReplaceSelf { + new_upper_bound: new_upper_bound(), + }; ty.apply_type_mapping(db, env, &mapping, TypeContext::default()) }); if !fallback_member.is_undefined() { return fallback_member; } - if let Some(value_ty) = TypedDictType::new(class).dict_value_type(db, env) + if let Some(value_ty) = typed_dict.dict_value_type(db, env) && let Some(dict_class) = KnownClass::Dict.to_specialized_class_type( db, env, From 19330b46ec4293b02463e683487fb53e149edb49 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Aug 2026 07:26:49 -0400 Subject: [PATCH 266/390] [ty] Reject Self with incompatible explicit receiver annotations (#27454) ## Summary Reject `Self` in method signatures when an explicitly annotated receiver is anything other than `Self` for an instance method or `type[Self]` for a class method, matching the typing specification, mypy, and Pyright: ```python from typing import Self, TypeVar T = TypeVar("T") class Example: def invalid(self: T) -> Self: ... # error: [invalid-type-form] @classmethod def invalid_classmethod(cls: type[T]) -> Self: ... # error: [invalid-type-form] def valid(self: Self) -> Self: ... ``` We infer explicitly annotated method receivers before the rest of the signature and report incompatible occurrences through the existing `Self` special-form checks. Receiver classification is shared with implicit `self`/`cls` inference, and invalid annotations retain their `Self` fallback type. Each `Self` occurrence is diagnosed independently, including quoted annotations, PEP 695 generic methods, normalized unions such as `Self | object`, and lazy aliases such as `Identity[Self]`. Diagnostics highlight the offending token and respect per-occurrence suppressions. --- .../resources/mdtest/annotations/self.md | 260 +++++++++++++++++- .../resources/mdtest/liskov.md | 10 +- crates/ty_python_semantic/src/types.rs | 22 +- crates/ty_python_semantic/src/types/infer.rs | 3 + .../src/types/infer/builder/function.rs | 181 +++++++++--- .../src/types/special_form.rs | 11 + 6 files changed, 432 insertions(+), 55 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index f7fdebaf0a..57fa7b068c 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -799,12 +799,7 @@ def x(s: Self): ... # error: [invalid-type-form] b: Self -# TODO: "Self" cannot be used in a function with a `self` or `cls` parameter that has a type annotation other than "Self" class Foo: - # TODO: This `self: T` annotation should be rejected because `T` is not `Self` - def has_existing_self_annotation(self: T) -> Self: - return self # error: [invalid-return-type] - def return_concrete_type(self) -> Self: # TODO: We could emit a hint that suggests annotating with `Foo` instead of `Self` # error: [invalid-return-type] @@ -821,6 +816,261 @@ class Bar(Generic[T]): ... class Baz(Bar[Self]): ... ``` +## Explicit instance-method receivers with `Self` + +An instance method can use `Self` when its first parameter is unannotated or annotated as `Self`: + +```py +from __future__ import annotations + +from typing import Self, TypeVar + +T = TypeVar("T") + +class Valid: + def implicit(self) -> Self: + return self + + def explicit(self: Self) -> Self: + return self +``` + +A different receiver annotation is valid when the method's signature does not use `Self`: + +```py +class WithoutSelf: + def method(self: T) -> T: + return self +``` + +Any other annotation for the first parameter is incompatible with `Self`, even an annotation that +names the class itself: + +```py +class Invalid: + def type_variable(self: T) -> Self: # error: [invalid-type-form] + raise NotImplementedError + + def concrete(self: Invalid) -> Self: # error: [invalid-type-form] + raise NotImplementedError + + def union(self: T | None) -> Self: # error: [invalid-type-form] + raise NotImplementedError + + def class_object(self: type[Self]) -> Self: # error: [invalid-type-form] + raise NotImplementedError +``` + +The invalid receiver does not change the inferred return type of the bound method: + +```py +reveal_type(Invalid().concrete) # revealed: bound method Invalid.concrete() -> Invalid +``` + +## Explicit classmethod receivers with `Self` + +A class method receives the class as its first argument. When the method uses `Self`, that argument +can be unannotated or annotated as `type[Self]`: + +```py +from __future__ import annotations + +from typing import Self, TypeVar + +T = TypeVar("T") + +class Valid: + @classmethod + def implicit(cls) -> Self: + return cls() + + @classmethod + def explicit(cls: type[Self]) -> Self: + return cls() +``` + +A class method can also use a different receiver annotation when its signature does not use `Self`: + +```py +class WithoutSelf: + @classmethod + def method(cls: type[T]) -> T: + return cls() +``` + +Other annotations are incompatible with `Self`, including `Self` without the enclosing `type`: + +```py +class Invalid: + @classmethod + def type_variable(cls: type[T]) -> Self: # error: [invalid-type-form] + raise NotImplementedError + + @classmethod + def concrete(cls: type[Invalid]) -> Self: # error: [invalid-type-form] + raise NotImplementedError + + @classmethod + def instance(cls: Self) -> Self: # error: [invalid-type-form] + raise NotImplementedError +``` + +## `Self` in unions with explicit receivers + +An incompatible receiver makes `Self` invalid even when the surrounding union simplifies to +`object`. This applies to both return and parameter annotations: + +```py +from typing import Self + +class Example: + def return_type(self: object) -> Self | object: ... # error: [invalid-type-form] + def parameter(self: object, value: Self | object) -> None: ... # error: [invalid-type-form] +``` + +## `Self` in type aliases with explicit receivers + +An incompatible receiver also makes `Self` invalid when it appears as an argument to a generic type +alias, whether the alias is used in a return or parameter annotation: + +```py +from typing import Self + +type Identity[T] = T + +class Example: + def return_type(self: object) -> Identity[Self]: # error: [invalid-type-form] + raise NotImplementedError + + def parameter(self: object, value: Identity[Self]) -> None: ... # error: [invalid-type-form] +``` + +## Multiple `Self` annotations with explicit receivers + +An incompatible receiver produces a separate error for each `Self` annotation: + +```py +from typing import Self, Union + +class Multiple: + def method( + self: object, + other: Self, # error: [invalid-type-form] + ) -> Self: # error: [invalid-type-form] + raise NotImplementedError +``` + +Two occurrences in the same annotation also produce separate errors, each pointing at its own +`Self`: + +```py +class Repeated: + # snapshot: invalid-type-form + # snapshot: invalid-type-form + def method(self: object, other: Union[Self, Self]) -> None: ... +``` + +```snapshot +error[invalid-type-form]: `Self` requires `self: Self` or `cls: type[Self]` for annotated receivers + --> src/mdtest_snippet.py:12:43 + | +12 | def method(self: object, other: Union[Self, Self]) -> None: ... + | ^^^^ + + +error[invalid-type-form]: `Self` requires `self: Self` or `cls: type[Self]` for annotated receivers + --> src/mdtest_snippet.py:12:49 + | +12 | def method(self: object, other: Union[Self, Self]) -> None: ... + | ^^^^ +``` + +Suppressing the error on the return annotation does not suppress the error on a parameter +annotation: + +```py +class SuppressedReturn: + def method( + self: object, + other: Self, # error: [invalid-type-form] + ) -> Self: # ty: ignore[invalid-type-form] + raise NotImplementedError +``` + +## Generic methods with explicit receiver annotations + +Methods with their own type parameters follow the same rules for `Self` in both instance methods and +class methods: + +```py +from typing import Self + +class Valid: + def instance[T](self: Self, value: T) -> Self: + return self + + @classmethod + def class_method[T](cls: type[Self], value: T) -> Self: + return cls() +``` + +A method's own type parameter cannot replace `Self` in its receiver annotation: + +```py +class Invalid: + def instance[T](self: T) -> Self: # error: [invalid-type-form] + raise NotImplementedError + + @classmethod + def class_method[T](cls: type[T]) -> Self: # error: [invalid-type-form] + raise NotImplementedError +``` + +## Quoted `Self` with explicit receiver annotations + +A receiver annotation and a `Self` return annotation can both be quoted: + +```py +from typing import Self + +class Valid: + def instance(self: "Self") -> "Self": + return self + + @classmethod + def class_method(cls: "type[Self]") -> "Self": + return cls() +``` + +An incompatible receiver makes a quoted `Self` invalid, including when the quoted union simplifies +to `object`: + +```py +class InvalidReturn: + def simple(self: object) -> "Self": # error: [invalid-type-form] + raise NotImplementedError + + # snapshot: invalid-type-form + def union(self: object) -> "Self | object": ... +``` + +```snapshot +error[invalid-type-form]: `Self` requires `self: Self` or `cls: type[Self]` for annotated receivers + --> src/mdtest_snippet.py:15:33 + | +15 | def union(self: object) -> "Self | object": ... + | ^^^^ +``` + +A quoted parameter annotation is also invalid when it passes `Self` to a type alias: + +```py +type Identity[T] = T + +class InvalidParameter: + def method(self: object, value: "Identity[Self]") -> None: ... # error: [invalid-type-form] +``` + ## Self usage in static methods `Self` cannot be used anywhere in a static method, including parameters, return types, nested diff --git a/crates/ty_python_semantic/resources/mdtest/liskov.md b/crates/ty_python_semantic/resources/mdtest/liskov.md index 533f251b84..8b10f3a7c7 100644 --- a/crates/ty_python_semantic/resources/mdtest/liskov.md +++ b/crates/ty_python_semantic/resources/mdtest/liskov.md @@ -1642,22 +1642,26 @@ class C3(A3): class D3(A3): def method(self: Self) -> Self: ... # fine +# These overrides would otherwise be valid, but a method returning `Self` must leave `self` +# unannotated or annotate it as `Self`. class E3(A3): - def method(self: E3) -> Self: ... # fine + def method(self: E3) -> Self: ... # error: [invalid-type-form] class F3(A3): - def method(self: A3) -> Self: ... # fine + def method(self: A3) -> Self: ... # error: [invalid-type-form] class G3(A3): - def method(self: object) -> Self: ... # fine + def method(self: object) -> Self: ... # error: [invalid-type-form] class H3(A3): # `A3.method()` can be called on any subtype of `A3`, but `H3.method()` can only be called on # objects that are subtypes of `str`. + # error: [invalid-type-form] def method(self: str) -> Self: ... # error: [invalid-method-override] class I3(A3): # `I3.method()` cannot be called with any inhabited type. + # error: [invalid-type-form] def method(self: Never) -> Self: ... # error: [invalid-method-override] class A4: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3de7f3ca99..f90a194083 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6715,16 +6715,15 @@ impl<'db> Type<'db> { Type::SpecialForm(special_form) => special_form .in_type_expression(db, scope_id, typevar_binding_context, inference_flags) .map_err(|err| { - let fallback_type = if matches!( - err, + let fallback_type = match err { InvalidTypeExpression::Concatenate - | InvalidTypeExpression::RequiresTwoArguments( - SpecialFormType::Concatenate - ) - ) { - Type::Dynamic(DynamicType::InvalidConcatenateUnknown) - } else { - Type::unknown() + | InvalidTypeExpression::RequiresTwoArguments( + SpecialFormType::Concatenate, + ) => Type::Dynamic(DynamicType::InvalidConcatenateUnknown), + InvalidTypeExpression::TypingSelfWithIncompatibleReceiver(typing_self) => { + Type::TypeVar(typing_self) + } + _ => Type::unknown(), }; InvalidTypeExpressionError { @@ -8914,6 +8913,8 @@ enum InvalidTypeExpression<'db> { TypingSelfInTypeAlias, /// `typing.Self` cannot be used in metaclass definitions. TypingSelfInMetaclass, + /// `typing.Self` cannot be used with an incompatible explicit method receiver. + TypingSelfWithIncompatibleReceiver(BoundTypeVarInstance<'db>), /// Some types are always invalid in type expressions InvalidType(Type<'db>, ScopeId<'db>), InvalidBareParamSpec(TypeVarInstance<'db>), @@ -9028,6 +9029,9 @@ impl<'db> InvalidTypeExpression<'db> { InvalidTypeExpression::TypingSelfInMetaclass => { f.write_str("`Self` cannot be used in a metaclass") } + InvalidTypeExpression::TypingSelfWithIncompatibleReceiver(_) => f.write_str( + "`Self` requires `self: Self` or `cls: type[Self]` for annotated receivers", + ), InvalidTypeExpression::InvalidType(Type::FunctionLiteral(function), _) => { write!( f, diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 5e184f98a1..45c6f48532 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -1983,6 +1983,9 @@ bitflags::bitflags! { /// Whether the visitor is currently visiting the argument to `Unpack[...]` or `*`. const IN_UNPACK_TYPE_ARGUMENT = 1 << 14; + + /// Whether the current method's explicit receiver annotation is incompatible with `Self`. + const HAS_INCOMPATIBLE_SELF_RECEIVER = 1 << 15; } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs index f7dbc64059..896ec53ce8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -24,10 +24,10 @@ use crate::{ DeclaredAndInferredType, DeferredExpressionState, TypeAndRange, validate_paramspec_components, }, - function_known_decorators, infer_statement_types, nearest_enclosing_function, - original_class_type, + function_known_decorator_flags, function_known_decorators, infer_statement_types, + nearest_enclosing_function, original_class_type, }, - infer_definition_types, infer_scope_types, + infer_scope_types, signatures::ReturnCallableTypeVarScope, tuple::{TupleSpecBuilder, TupleType}, typed_dict::extract_unpacked_typed_dict_keys_from_kwargs_annotation, @@ -39,7 +39,7 @@ use ty_python_core::{ scope::NodeWithScopeRef, }; -use ruff_python_ast::{self as ast}; +use ruff_python_ast as ast; use ruff_text_size::Ranged; fn parameters_have_annotations(parameters: &ast::Parameters) -> bool { @@ -56,6 +56,65 @@ fn parameters_have_annotations(parameters: &ast::Parameters) -> bool { .is_some_and(|param| param.annotation.is_some()) } +/// Whether a non-static method receives an instance or the class itself. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MethodReceiverKind { + Instance, + Class, +} + +impl MethodReceiverKind { + /// Classifies methods by their decorators and implicit class-receiver rules. + /// + /// Free functions and ordinary static methods have no receiver; `__new__` receives the class. + /// + /// ```python + /// class Example: + /// def instance(self): ... + /// @classmethod + /// def class_method(cls): ... + /// @staticmethod + /// def static_method(): ... + /// ``` + fn from_function<'db>( + db: &'db dyn Db, + definition: Definition<'db>, + function: &ast::StmtFunctionDef, + ) -> Option { + if !definition.scope(db).scope(db).kind().is_class() { + return None; + } + + let decorators = function_known_decorator_flags(db, definition); + if decorators.contains(FunctionDecorators::STATICMETHOD) && function.name.id != "__new__" { + return None; + } + + if decorators.contains(FunctionDecorators::CLASSMETHOD) + || is_implicit_classmethod(&function.name) + || function.name.id == "__new__" + { + Some(Self::Class) + } else { + Some(Self::Instance) + } + } + + /// Accepts only `Self` for an instance receiver and `type[Self]` for a class receiver. + fn accepts_annotation(self, db: &dyn Db, annotation: Type<'_>) -> bool { + match (self, annotation) { + (Self::Instance, Type::TypeVar(typevar)) => typevar.typevar(db).is_self(db), + (Self::Class, Type::SubclassOf(subclass)) => { + matches!( + subclass.subclass_of(), + SubclassOfInner::TypeVar(typevar) if typevar.typevar(db).is_self(db) + ) + } + _ => false, + } + } +} + /// Return type policy for checking explicit `return` statements in a function body. #[derive(Debug, Copy, Clone)] struct ExpectedReturnType<'db> { @@ -585,8 +644,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let previous_typevar_binding_context = self.typevar_binding_context.replace(definition); if !has_type_params { - self.infer_return_type_annotation(function.returns.as_deref()); - self.infer_parameters(function.parameters.as_ref()); + self.infer_function_signature_annotations(function, definition); } if has_defaults { @@ -656,21 +714,81 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } pub(super) fn infer_function_type_params(&mut self, function: &ast::StmtFunctionDef) { - let type_params = function - .type_params - .as_deref() - .expect("function type params scope without type params"); - let binding_context = self.index.expect_single_definition(function); let previous_typevar_binding_context = self.typevar_binding_context.replace(binding_context); - self.infer_return_type_annotation(function.returns.as_deref()); - self.infer_type_parameters(type_params); - self.infer_parameters(&function.parameters); + self.infer_function_signature_annotations(function, binding_context); self.typevar_binding_context = previous_typevar_binding_context; } - fn infer_parameters(&mut self, parameters: &ast::Parameters) { + /// Infer an annotated method receiver before the rest of its signature so `Self` can be + /// validated where it occurs, including inside parsed string annotations. + /// + /// ```python + /// class Example: + /// def method(self: object) -> "Self | object": ... + /// ``` + fn infer_function_signature_annotations( + &mut self, + function: &ast::StmtFunctionDef, + definition: Definition<'db>, + ) { + let receiver_is_incompatible = self.infer_method_receiver_annotation(function, definition); + let previous_incompatible_receiver = self.context.inference_flags.replace( + InferenceFlags::HAS_INCOMPATIBLE_SELF_RECEIVER, + receiver_is_incompatible == Some(true), + ); + + self.infer_return_type_annotation(function.returns.as_deref()); + if let Some(type_params) = function.type_params.as_deref() { + self.infer_type_parameters(type_params); + } + self.infer_parameters(&function.parameters, receiver_is_incompatible.is_some()); + + self.context.inference_flags.set( + InferenceFlags::HAS_INCOMPATIBLE_SELF_RECEIVER, + previous_incompatible_receiver, + ); + } + + /// Infers an explicitly annotated method receiver before the rest of its signature. + /// + /// Returns whether the annotation is incompatible with `Self`, or `None` for functions without + /// an annotated instance or class receiver. + fn infer_method_receiver_annotation( + &mut self, + function: &ast::StmtFunctionDef, + definition: Definition<'db>, + ) -> Option { + let receiver = function + .parameters + .posonlyargs + .first() + .or_else(|| function.parameters.args.first())?; + let annotation = receiver.parameter.annotation.as_deref()?; + let receiver_kind = MethodReceiverKind::from_function(self.db(), definition, function)?; + + let previously_in_parameter_annotation = self + .context + .inference_flags + .replace(InferenceFlags::IN_PARAMETER_ANNOTATION, true); + let annotation_type = self.infer_type_expression_with_state( + annotation, + DeferredExpressionState::from(self.defer_annotations()), + ); + self.context.inference_flags.set( + InferenceFlags::IN_PARAMETER_ANNOTATION, + previously_in_parameter_annotation, + ); + + Some(!receiver_kind.accepts_annotation(self.db(), annotation_type)) + } + + fn infer_parameters( + &mut self, + parameters: &ast::Parameters, + first_annotation_already_inferred: bool, + ) { let ast::Parameters { range: _, node_index: _, @@ -682,7 +800,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = parameters; self.context.inference_flags |= InferenceFlags::IN_PARAMETER_ANNOTATION; - for param_with_default in parameters.iter_non_variadic_params() { + for param_with_default in parameters + .iter_non_variadic_params() + .skip(usize::from(first_annotation_already_inferred)) + { self.infer_parameter_with_default(param_with_default); } if let Some(vararg) = vararg { @@ -1049,33 +1170,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let function_node = function_definition.node(self.module()); - let function_name = &function_node.name; - - let mut is_classmethod = is_implicit_classmethod(function_name); - let inference = infer_definition_types(self.db(), method_definition); - for decorator in &function_node.decorator_list { - let decorator_ty = inference.expression_type(&decorator.expression); - if let Some(known_class) = decorator_ty - .as_class_literal() - .and_then(|class| class.known(db)) - { - if known_class == KnownClass::Staticmethod && function_name != "__new__" { - return None; - } - - is_classmethod |= known_class == KnownClass::Classmethod; - } - } + let receiver_kind = + MethodReceiverKind::from_function(db, method_definition, function_node)?; let class_definition = self.index.expect_single_definition(class); let class_literal = original_class_type(db, class_definition)?; let typing_self = typing_self(db, self.scope(), Some(method_definition), class_literal); - if is_classmethod || function_name == "__new__" { - typing_self.map(|typing_self| { + match receiver_kind { + MethodReceiverKind::Class => typing_self.map(|typing_self| { SubclassOfType::from(db, env, SubclassOfInner::TypeVar(typing_self)) - }) - } else { - typing_self.map(Type::TypeVar) + }), + MethodReceiverKind::Instance => typing_self.map(Type::TypeVar), } } diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index a49e371a60..5932d4bd54 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -929,6 +929,17 @@ impl SpecialFormType { return Err(InvalidTypeExpression::TypingSelfInMetaclass); } + if inference_flags.contains(InferenceFlags::HAS_INCOMPATIBLE_SELF_RECEIVER) + && inference_flags.intersects( + InferenceFlags::IN_RETURN_TYPE | InferenceFlags::IN_PARAMETER_ANNOTATION, + ) + && let Some(typing_self) = typing_self + { + return Err(InvalidTypeExpression::TypingSelfWithIncompatibleReceiver( + typing_self, + )); + } + Ok(typing_self .map(Type::TypeVar) .unwrap_or(Type::SpecialForm(self))) From 4f758843ae9d50968b1c4b0144cf710a01ea2b0b Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 5 Aug 2026 14:06:54 +0200 Subject: [PATCH 267/390] [ty] Install primer dependencies for memory reports (#27500) --- .github/workflows/memory_report.yaml | 5 +++ .github/workflows/ty-ecosystem-analyzer.yaml | 2 + scripts/memory_report.py | 40 +++++++++++--------- scripts/setup_primer_project.py | 4 +- scripts/setup_primer_project.py.lock | 4 +- 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/.github/workflows/memory_report.yaml b/.github/workflows/memory_report.yaml index ebb76a1ae9..3dd492d887 100644 --- a/.github/workflows/memory_report.yaml +++ b/.github/workflows/memory_report.yaml @@ -61,6 +61,11 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} + - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.12.1" + enable-cache: true + - name: Install Rust toolchain run: rustup show diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index 48fb53cf5b..4d625e41d2 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -42,6 +42,8 @@ env: RUST_BACKTRACE: 1 # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_PROFILING_DEBUG: line-tables-only + # TODO: Update the mypy-primer revision in scripts/setup_primer_project.py + # and regenerate its lockfile when updating ecosystem-analyzer. ECOSYSTEM_ANALYZER_COMMIT: f6f1b7b8586c8a6c60dc3d37c3f6ae917a9ad9c4 jobs: diff --git a/scripts/memory_report.py b/scripts/memory_report.py index 35226a1929..339f07750c 100644 --- a/scripts/memory_report.py +++ b/scripts/memory_report.py @@ -4,13 +4,13 @@ This script can be used in two modes: 1. Report comparison mode: Reads pre-generated JSON memory reports and compares them. -2. Full run mode: Clones projects, builds ty, runs memory tests, and generates comparison. +2. Full run mode: Sets up projects, runs memory tests, and generates comparison. Examples: # Compare pre-generated memory reports %(prog)s compare --old-dir old_reports/ --new-dir new_reports/ - # Full run: clone projects, build ty, run memory tests + # Full run: set up projects and their dependencies, then run memory tests %(prog)s run --old-ty ./ty-old --new-ty ./ty-new # Write output to a file @@ -25,18 +25,11 @@ import subprocess import sys import tempfile -from collections.abc import Mapping from dataclasses import dataclass, field from pathlib import Path from typing import Any, Final, Self -# Known projects with their Git URLs for memory testing. -KNOWN_PROJECTS: Final[Mapping[str, str]] = { - "flake8": "https://github.com/PyCQA/flake8", - "sphinx": "https://github.com/sphinx-doc/sphinx", - "prefect": "https://github.com/PrefectHQ/prefect", - "trio": "https://github.com/python-trio/trio", -} +KNOWN_PROJECTS: Final = ("flake8", "sphinx", "prefect", "trio") @dataclass(slots=True, kw_only=True) @@ -251,18 +244,29 @@ def render_summary(projects: list[ProjectComparison]) -> str: return "\n".join(lines) -def clone_project(*, name: str, url: str, dest: Path) -> Path: - """Clone a project from Git. Returns the path to the cloned project.""" +def setup_project(*, name: str, dest: Path) -> Path: + """Clone a project and install its mypy-primer dependencies.""" project_path = dest / name if project_path.exists(): print(f"Project {name} already exists at {project_path}", file=sys.stderr) return project_path - print(f"Cloning {name} from {url}...", file=sys.stderr) + setup_script = Path(__file__).with_name("setup_primer_project.py") + print(f"Setting up {name} and its dependencies...", file=sys.stderr) subprocess.run( - ["git", "clone", "--depth=1", url, str(project_path)], + [ + "uv", + "run", + "--locked", + "--python", + sys.executable, + "--script", + str(setup_script), + name, + str(project_path), + ], check=True, - capture_output=True, + stdout=sys.stderr, ) return project_path @@ -301,8 +305,8 @@ def run_memory_tests( old_reports_dir.mkdir(parents=True, exist_ok=True) new_reports_dir.mkdir(parents=True, exist_ok=True) - for project_name, url in KNOWN_PROJECTS.items(): - project_path = clone_project(name=project_name, url=url, dest=projects_dir) + for project_name in KNOWN_PROJECTS: + project_path = setup_project(name=project_name, dest=projects_dir) # Run old ty old_report_path = old_reports_dir / f"{project_name}.json" @@ -455,7 +459,7 @@ def parse_args() -> argparse.Namespace: # Run subcommand run_parser = subparsers.add_parser( "run", - help="Clone projects, run ty, and compare memory usage", + help="Set up projects, run ty, and compare memory usage", ) run_parser.add_argument( "--old-ty", diff --git a/scripts/setup_primer_project.py b/scripts/setup_primer_project.py index 9f694f8927..79ed417765 100644 --- a/scripts/setup_primer_project.py +++ b/scripts/setup_primer_project.py @@ -12,7 +12,9 @@ # exclude-newer = "7 days" # # [tool.uv.sources] -# mypy-primer = { git = "https://github.com/hauntsaninja/mypy_primer" } +# # Keep this revision and the script's lockfile in sync with ecosystem-analyzer's +# # mypy-primer pin so memory reports and ecosystem jobs use the same project definitions. +# mypy-primer = { git = "https://github.com/hauntsaninja/mypy_primer", rev = "6d6eebd8d37c9b8931381e79aa99808d9378c988" } # /// """Clone a mypy-primer project and set up a virtualenv with its dependencies installed. diff --git a/scripts/setup_primer_project.py.lock b/scripts/setup_primer_project.py.lock index 2034e8f7f5..feea69058f 100644 --- a/scripts/setup_primer_project.py.lock +++ b/scripts/setup_primer_project.py.lock @@ -7,9 +7,9 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for exclude-newer-span = "P7D" [manifest] -requirements = [{ name = "mypy-primer", git = "https://github.com/hauntsaninja/mypy_primer" }] +requirements = [{ name = "mypy-primer", git = "https://github.com/hauntsaninja/mypy_primer?rev=6d6eebd8d37c9b8931381e79aa99808d9378c988" }] [[package]] name = "mypy-primer" version = "0.1.0" -source = { git = "https://github.com/hauntsaninja/mypy_primer#05f73ec3d85bb4f55676f3c57f2c3e5136228977" } +source = { git = "https://github.com/hauntsaninja/mypy_primer?rev=6d6eebd8d37c9b8931381e79aa99808d9378c988#6d6eebd8d37c9b8931381e79aa99808d9378c988" } From 79e9224cf8eee3d019de62e3a74ee881e7d56254 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 5 Aug 2026 15:40:13 +0100 Subject: [PATCH 268/390] [ty] Suppress `unimported-reveal` diagnostics in stub files and `if TYPE_CHECKING` blocks (#27508) --- .../mdtest/directives/reveal_type.md | 37 +++++++++++++++++++ .../src/types/infer/builder.rs | 6 ++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/directives/reveal_type.md b/crates/ty_python_semantic/resources/mdtest/directives/reveal_type.md index 44648443fe..83bc6b0181 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/reveal_type.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/reveal_type.md @@ -38,6 +38,43 @@ fail at runtime: reveal_type(1) # revealed: Literal[1] ``` +## In type-checking blocks + +An unimported `reveal_type` cannot fail at runtime inside a `TYPE_CHECKING` block because that code +is never executed at runtime. + +Note that this test uses `# error: [revealed-type]` assertions instead of the more common +`# revealed` assertions that we use elsewhere for `reveal_type` calls. `# revealed` assertions +swallow `undefined-reveal` errors as well as asserting the revealed type, but +`# error: [revealed-type]` assertions do not also match `undefined-reveal`. This means that an +unexpected so an unexpected `undefined-reveal` warning would cause these tests to fail. + +```py +from typing import TYPE_CHECKING +import typing + +if TYPE_CHECKING: + reveal_type(1) # error: [revealed-type] "Literal[1]" + + def nested() -> None: + reveal_type("nested") # error: [revealed-type] "nested" + +if typing.TYPE_CHECKING: + reveal_type(True) # error: [revealed-type] "Literal[True]" +``` + +## In stub files + +An unimported `reveal_type` also cannot fail at runtime in a stub file because stub files are never +executed. + +As in the previous section, this test uses `# error: [revealed-type]` rather than `revealed:` +assertions to ensure that an unexpected `undefined-reveal` warning is not silently matched. + +```pyi +reveal_type(1) # error: [revealed-type] "Literal[1]" +``` + ## In unreachable code Make sure that `reveal_type` works even in unreachable code. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 5f7b6b9a65..6d72d6f46e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -9644,7 +9644,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Still not found? It might be `reveal_type`... .or_fall_back_to(db, env, || { if symbol_name == "reveal_type" { - if let Some(builder) = self.context.report_lint(&UNDEFINED_REVEAL, name_node) { + if !self.in_stub() + && !self.is_in_type_checking_block(self.scope(), name_node) + && 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( From f33f59f071d258487ac207a360ff1754d0ddb5c3 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 5 Aug 2026 15:53:12 +0100 Subject: [PATCH 269/390] [ty] Clarify ecosystem summary diagnostic placement (#27511) --- .../assets/report-template.md | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.agents/skills/summarise-ecosystem-results/assets/report-template.md b/.agents/skills/summarise-ecosystem-results/assets/report-template.md index ff1435d568..6a4d478d2c 100644 --- a/.agents/skills/summarise-ecosystem-results/assets/report-template.md +++ b/.agents/skills/summarise-ecosystem-results/assets/report-template.md @@ -14,9 +14,26 @@ + + ```python -# Merge base: -# PR: ``` From 8c30182896080ffe45bdb99dc20c83fbe9229a8d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Aug 2026 11:04:38 -0400 Subject: [PATCH 270/390] [ty] Normalize unpacked callable signatures for assignability (#27450) ## Summary Previously, we compared unpacked callable parameters in inconsistent forms: the positional fast path inspected the original signatures, while later matching sometimes expanded only the source. This could panic on valid code or reject compatible assignments such as: ```python from typing import Callable, Unpack def callback(*args: str | None) -> None: ... target: Callable[[Unpack[tuple[str, ...]], None], None] = callback ``` We now normalize both callable signatures before comparing them. Empty and fixed-length unpacked tuples become positional parameters, homogeneous tuples become variadic parameters with the correct element type, and required target suffixes can be matched against source variadics. The positional fast path, `ParamSpec` handling, gradual-callable checks, and `TypeVarTuple` inference all use the same normalized representation. This also fixes previously unsupported `TypeVarTuple` inference for nested unpacked parameters, callable protocols, and both legacy and PEP 695 callable aliases. Closes https://github.com/astral-sh/ty/issues/4172. --- .../resources/mdtest/bidirectional.md | 7 +- .../mdtest/generics/legacy/typevartuple.md | 12 +- .../mdtest/generics/pep695/typevartuple.md | 65 +++-- .../type_properties/is_assignable_to.md | 195 ++++++++++++++ .../mdtest/type_properties/is_subtype_of.md | 130 ++++++++++ .../src/types/signatures.rs | 239 ++++++++++++------ 6 files changed, 518 insertions(+), 130 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index a8d86627b4..3b1f729c3b 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -1671,11 +1671,10 @@ reveal_type(f7) # revealed: (*args) -> None f8: Callable[[int], None] = lambda *, x=1: None reveal_type(f8) # revealed: (int, /) -> None -# `Callable` annotations only describe positional parameters, so the keyword-only `x` is not -# compatible with the positional suffix in the annotation. -# error: [invalid-assignment] +# An optional keyword-only parameter does not prevent `*args` from accepting the positional +# suffix in a `Callable` annotation. f9: Callable[[*tuple[int, ...], int], None] = lambda *args, x=1: None -reveal_type(f9) # revealed: (*tuple[int, ...], int) -> None +reveal_type(f9) # revealed: (*args, *, x=1) -> None f10: Callable[[str, int, str], tuple[str, int, str]] = lambda x, y, z: reveal_type((x, y, z)) # revealed: tuple[str, int, str] reveal_type(f10) # revealed: (x: str, y: int, z: str) -> tuple[str, int, str] diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md index 82e2d7864f..832cb3dda7 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/typevartuple.md @@ -614,13 +614,7 @@ def fn0(a: int) -> None: ... def fn1(a: int, b: str) -> None: ... def fn2(a: int, b: str, c: bytes) -> None: ... -# TODO: Should reveal `tuple[()]` without an error. -# error: [invalid-argument-type] "Argument to function `test` is incorrect: Expected `(int, /, *args: tuple[Unknown, ...]) -> None`, found `def fn0(a: int) -> None`" -reveal_type(test(fn0)) # revealed: tuple[Unknown, ...] -# TODO: Should reveal `tuple[str]` without an error. -# error: [invalid-argument-type] "Argument to function `test` is incorrect: Expected `(int, /, *args: tuple[Unknown, ...]) -> None`, found `def fn1(a: int, b: str) -> None`" -reveal_type(test(fn1)) # revealed: tuple[Unknown, ...] -# TODO: Should reveal `tuple[str, bytes]` without an error. -# error: [invalid-argument-type] "Argument to function `test` is incorrect: Expected `(int, /, *args: tuple[Unknown, ...]) -> None`, found `def fn2(a: int, b: str, c: bytes) -> None`" -reveal_type(test(fn2)) # revealed: tuple[Unknown, ...] +reveal_type(test(fn0)) # revealed: tuple[()] +reveal_type(test(fn1)) # revealed: tuple[str] +reveal_type(test(fn2)) # revealed: tuple[str, bytes] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md index a3919da77f..0c1ee74061 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md @@ -546,12 +546,27 @@ def expect_nested( def pass_flattened( callback: Callable[[int, *tuple[str, ...], bytes, str], None], ) -> None: - # TODO: This should be assignable because the nested unpacking is equivalent to the flattened - # form. - # error: [invalid-argument-type] expect_nested(callback) ``` +### Nested unpacked `TypeVarTuple` callable parameters + +A `TypeVarTuple` nested inside an unpacked tuple remains inferable after the surrounding tuple is +expanded into its fixed prefix and suffix. + +```py +from typing import Callable + +def infer_nested[*Ts](callback: Callable[[int, *tuple[*Ts, bytes]], None]) -> tuple[*Ts]: + raise NotImplementedError + +def fixed_middle(prefix: int, middle: str, suffix: bytes, /) -> None: ... +def empty_middle(prefix: int, suffix: bytes, /) -> None: ... + +reveal_type(infer_nested(fixed_middle)) # revealed: tuple[str] +reveal_type(infer_nested(empty_middle)) # revealed: tuple[()] +``` + ### Callable inference with additional keyword parameters Additional keyword-only or variadic keyword parameters do not contribute to a `TypeVarTuple` @@ -595,18 +610,12 @@ def positional_only_with_keyword(x: int, y: str, /, *, flag: bool) -> None: ... def positional_or_keyword(x: int, y: str, flag: bool) -> None: ... def keyword_catch_all(x: int, y: str, **kwargs: object) -> None: ... -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_keyword_only` is incorrect: Expected `KeywordOnlyCallback[*tuple[Unknown, ...]]`, found `def explicit_keyword_only(x: int, y: str, *, flag: bool) -> None`" -reveal_type(infer_keyword_only(explicit_keyword_only)) # revealed: tuple[Unknown, ...] -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_keyword_only` is incorrect: Expected `KeywordOnlyCallback[*tuple[Unknown, ...]]`, found `def positional_only_with_keyword(x: int, y: str, /, *, flag: bool) -> None`" -reveal_type(infer_keyword_only(positional_only_with_keyword)) # revealed: tuple[Unknown, ...] +reveal_type(infer_keyword_only(explicit_keyword_only)) # revealed: tuple[int, str] +reveal_type(infer_keyword_only(positional_only_with_keyword)) # revealed: tuple[int, str] # TODO: Should reveal `tuple[int, str]`. # error: [invalid-argument-type] "Argument to function `infer_keyword_only` is incorrect: Expected `KeywordOnlyCallback[*tuple[Unknown, ...]]`, found `def positional_or_keyword(x: int, y: str, flag: bool) -> None`" reveal_type(infer_keyword_only(positional_or_keyword)) # revealed: tuple[Unknown, ...] -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_keyword_only` is incorrect: Expected `KeywordOnlyCallback[*tuple[Unknown, ...]]`, found `def keyword_catch_all(x: int, y: str, **kwargs: object) -> None`" -reveal_type(infer_keyword_only(keyword_catch_all)) # revealed: tuple[Unknown, ...] +reveal_type(infer_keyword_only(keyword_catch_all)) # revealed: tuple[int, str] class OptionalKeywordCallback[*Ts](Protocol): def __call__(self, *args: *Ts, flag: bool = False) -> None: ... @@ -616,9 +625,7 @@ def infer_optional_keyword[*Ts](callback: OptionalKeywordCallback[*Ts]) -> tuple def optional_keyword_callback(x: int, y: str, *, flag: bool = False) -> None: ... -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_optional_keyword` is incorrect: Expected `OptionalKeywordCallback[*tuple[Unknown, ...]]`, found `def optional_keyword_callback(x: int, y: str, *, flag: bool = False) -> None`" -reveal_type(infer_optional_keyword(optional_keyword_callback)) # revealed: tuple[Unknown, ...] +reveal_type(infer_optional_keyword(optional_keyword_callback)) # revealed: tuple[int, str] class PrefixedKeywordCallback[*Ts](Protocol): def __call__(self, prefix: bytes, *args: *Ts, flag: bool) -> None: ... @@ -629,9 +636,7 @@ def infer_prefixed[*Ts](callback: PrefixedKeywordCallback[*Ts]) -> tuple[*Ts]: def prefixed(prefix: bytes, x: int, y: str, *, flag: bool) -> None: ... def prefixed_variadic(prefix: bytes, *args: str, flag: bool) -> None: ... -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_prefixed` is incorrect: Expected `PrefixedKeywordCallback[*tuple[Unknown, ...]]`, found `def prefixed(prefix: bytes, x: int, y: str, *, flag: bool) -> None`" -reveal_type(infer_prefixed(prefixed)) # revealed: tuple[Unknown, ...] +reveal_type(infer_prefixed(prefixed)) # revealed: tuple[int, str] # An open-ended positional parameter can be inferred in an otherwise mixed signature. reveal_type(infer_prefixed(prefixed_variadic)) # revealed: tuple[str, ...] @@ -653,9 +658,7 @@ def infer_keyword_variadic[*Ts](callback: KeywordVariadicCallback[*Ts]) -> tuple def keyword_variadic(x: int, y: str, **kwargs: int) -> None: ... -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_keyword_variadic` is incorrect: Expected `KeywordVariadicCallback[*tuple[Unknown, ...]]`, found `def keyword_variadic(x: int, y: str, **kwargs: int) -> None`" -reveal_type(infer_keyword_variadic(keyword_variadic)) # revealed: tuple[Unknown, ...] +reveal_type(infer_keyword_variadic(keyword_variadic)) # revealed: tuple[int, str] class KeywordOnlyAndVariadicCallback[*Ts](Protocol): def __call__(self, *args: *Ts, flag: bool, **kwargs: int) -> None: ... @@ -667,9 +670,7 @@ def infer_keyword_only_and_variadic[*Ts]( def keyword_only_and_variadic(x: int, y: str, *, flag: bool, **kwargs: int) -> None: ... -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_keyword_only_and_variadic` is incorrect: Expected `KeywordOnlyAndVariadicCallback[*tuple[Unknown, ...]]`, found `def keyword_only_and_variadic(x: int, y: str, *, flag: bool, **kwargs: int) -> None`" -reveal_type(infer_keyword_only_and_variadic(keyword_only_and_variadic)) # revealed: tuple[Unknown, ...] +reveal_type(infer_keyword_only_and_variadic(keyword_only_and_variadic)) # revealed: tuple[int, str] class MultipleKeywordCallback[*Ts](Protocol): def __call__(self, *args: *Ts, first: int, second: str) -> None: ... @@ -679,9 +680,7 @@ def infer_multiple_keywords[*Ts](callback: MultipleKeywordCallback[*Ts]) -> tupl def multiple_keyword_catch_all(x: int, y: str, **kwargs: object) -> None: ... -# TODO: Should reveal `tuple[int, str]`. -# error: [invalid-argument-type] "Argument to function `infer_multiple_keywords` is incorrect: Expected `MultipleKeywordCallback[*tuple[Unknown, ...]]`, found `def multiple_keyword_catch_all(x: int, y: str, **kwargs: object) -> None`" -reveal_type(infer_multiple_keywords(multiple_keyword_catch_all)) # revealed: tuple[Unknown, ...] +reveal_type(infer_multiple_keywords(multiple_keyword_catch_all)) # revealed: tuple[int, str] ``` ### Length-sensitive inference @@ -937,15 +936,9 @@ def fn0(a: int) -> None: ... def fn1(a: int, b: str) -> None: ... def fn2(a: int, b: str, c: bytes) -> None: ... -# TODO: Should reveal `tuple[()]` without an error. -# error: [invalid-argument-type] "Argument to function `test` is incorrect: Expected `Alias[*tuple[int, *tuple[Unknown, ...]]]`, found `def fn0(a: int) -> None`" -reveal_type(test(fn0)) # revealed: tuple[Unknown, ...] -# TODO: Should reveal `tuple[str]` without an error. -# error: [invalid-argument-type] "Argument to function `test` is incorrect: Expected `Alias[*tuple[int, *tuple[Unknown, ...]]]`, found `def fn1(a: int, b: str) -> None`" -reveal_type(test(fn1)) # revealed: tuple[Unknown, ...] -# TODO: Should reveal `tuple[str, bytes]` without an error. -# error: [invalid-argument-type] "Argument to function `test` is incorrect: Expected `Alias[*tuple[int, *tuple[Unknown, ...]]]`, found `def fn2(a: int, b: str, c: bytes) -> None`" -reveal_type(test(fn2)) # revealed: tuple[Unknown, ...] +reveal_type(test(fn0)) # revealed: tuple[()] +reveal_type(test(fn1)) # revealed: tuple[str] +reveal_type(test(fn2)) # revealed: tuple[str, bytes] ``` ### Indexing and iteration diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 5bccad8617..985d118c88 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -1077,6 +1077,201 @@ static_assert(is_assignable_to(RegularCallableTypeOf[keyword_variadic], Callable static_assert(is_assignable_to(RegularCallableTypeOf[mixed], Callable[..., None])) ``` +### Unpacked positional parameters with a required suffix + +A variadic positional parameter can accept both the unpacked tuple and a required positional +parameter following that tuple. + +```py +from typing import Any, Callable, Never, Unpack, cast +from ty_extensions import static_assert +from ty_extensions._internal import RegularCallableTypeOf, is_assignable_to + +def expects_suffix(callback: Callable[[Unpack[tuple[str, ...]], None], None]) -> None: ... +def accepts_unknown(*args): ... + +expects_suffix(accepts_unknown) +``` + +The variadic parameter's annotation must be compatible with the unpacked elements and the required +suffix. + +```py +def accepts_objects(*args: object) -> None: ... +def accepts_strings_or_none(*args: str | None) -> None: ... +def accepts_strings(*args: str) -> None: ... + +expects_suffix(accepts_objects) +expects_suffix(accepts_strings_or_none) +expects_suffix(accepts_strings) # error: [invalid-argument-type] + +static_assert( + is_assignable_to( + RegularCallableTypeOf[accepts_objects], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + is_assignable_to( + RegularCallableTypeOf[accepts_strings_or_none], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + not is_assignable_to( + RegularCallableTypeOf[accepts_strings], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +``` + +A required keyword-only parameter cannot be supplied by the positional callback signature. + +```py +def requires_keyword(*args: object, value: int) -> None: ... + +expects_suffix(requires_keyword) # error: [invalid-argument-type] +``` + +A required positional prefix does not prevent the source variadic parameter from also accepting the +target's required suffix. + +```py +def expects_prefix_and_suffix( + callback: Callable[[int, Unpack[tuple[str, ...]], None], None], +) -> None: ... +def accepts_prefixed_objects(first: int, *args: object) -> None: ... + +expects_prefix_and_suffix(accepts_prefixed_objects) +``` + +A required suffix can align with a longer suffix or an equivalent positional prefix when all the +unpacked elements have the same type. + +```py +def requires_one_integer(*args: *tuple[*tuple[int, ...], int]) -> None: ... + +longer_suffix: Callable[[*tuple[int, ...], int, int], None] = requires_one_integer +equivalent_prefix: Callable[[int, *tuple[int, ...]], None] = requires_one_integer + +type OneOrMoreIntegers = RegularCallableTypeOf[requires_one_integer] + +static_assert(is_assignable_to(OneOrMoreIntegers, Callable[[*tuple[int, ...], int, int], None])) +static_assert(is_assignable_to(OneOrMoreIntegers, Callable[[int, *tuple[int, ...]], None])) +``` + +A type alias for the variadic element does not prevent the required suffix from matching. + +```py +type Integer = int + +def requires_one_aliased_integer(*args: *tuple[*tuple[Integer, ...], int]) -> None: ... + +type AliasedIntegers = RegularCallableTypeOf[requires_one_aliased_integer] + +static_assert(is_assignable_to(AliasedIntegers, Callable[[int, *tuple[int, ...]], None])) +``` + +A longer suffix is aligned from the end when its other elements fit the source variadic parameter. + +```py +def requires_string_suffix(*args: *tuple[*tuple[object, ...], str]) -> None: ... +def requires_string_after_integers(*args: *tuple[*tuple[int, ...], str]) -> None: ... + +type StringSuffix = RegularCallableTypeOf[requires_string_suffix] +type IntegerStringSuffix = RegularCallableTypeOf[requires_string_after_integers] + +static_assert(is_assignable_to(StringSuffix, Callable[[*tuple[object, ...], int, str], None])) +static_assert(is_assignable_to(IntegerStringSuffix, Callable[[*tuple[int, ...], int, str], None])) +``` + +Gradual variadic elements remain assignable in both directions. + +```py +type GradualSuffix = Callable[[*tuple[Any, ...], int], None] + +static_assert(is_assignable_to(OneOrMoreIntegers, GradualSuffix)) +static_assert(is_assignable_to(GradualSuffix, OneOrMoreIntegers)) +``` + +A positional parameter cannot also be filled by a target keyword argument. + +```py +def occupies_keyword(a: int, *args: int, **kwargs: int) -> None: ... +def accepts_keyword(*args: *tuple[*tuple[int, ...], int], **kwargs: int) -> None: ... + +type OccupiesKeyword = RegularCallableTypeOf[occupies_keyword] +type AcceptsKeyword = RegularCallableTypeOf[accepts_keyword] + +static_assert(not is_assignable_to(OccupiesKeyword, AcceptsKeyword)) +``` + +An uninhabited keyword parameter cannot collide with an occupied positional parameter. + +```py +type Bottom = Never + +def rejects_keywords(*args: *tuple[*tuple[int, ...], int], **kwargs: Bottom) -> None: ... +def rejects_named_keyword(*args: *tuple[*tuple[int, ...], int], a: Never = cast(Never, 0)) -> None: ... + +static_assert(is_assignable_to(OccupiesKeyword, RegularCallableTypeOf[rejects_keywords])) +static_assert(is_assignable_to(OccupiesKeyword, RegularCallableTypeOf[rejects_named_keyword])) +``` + +A suffix cannot be extended with elements that the source variadic parameter rejects. + +```py +# error: [invalid-assignment] +incompatible_suffix: Callable[[*tuple[int, ...], str, str], None] = requires_string_after_integers +``` + +### Fixed-length unpacked positional parameters + +An unpacked fixed-length tuple accepts exactly its declared positional arguments, including when the +tuple is empty. Equivalent unpacked source and target tuples are compatible. + +```py +from typing import Callable, Unpack +from ty_extensions import static_assert +from ty_extensions._internal import RegularCallableTypeOf, is_assignable_to + +def accepts_no_arguments(*args: Unpack[tuple[()]]) -> None: ... +def accepts_one_integer(*args: Unpack[tuple[int]]) -> None: ... +def accepts_strings(*args: str) -> None: ... + +empty_callback: Callable[[Unpack[tuple[()]]], None] = accepts_no_arguments +fixed_callback: Callable[[Unpack[tuple[int]]], None] = accepts_one_integer +fixed_strings: Callable[[Unpack[tuple[str, str]]], None] = accepts_strings +empty_strings: Callable[[Unpack[tuple[()]]], None] = accepts_strings + +static_assert(is_assignable_to(RegularCallableTypeOf[accepts_no_arguments], Callable[[Unpack[tuple[()]]], None])) +static_assert(is_assignable_to(RegularCallableTypeOf[accepts_one_integer], Callable[[Unpack[tuple[int]]], None])) +``` + +Empty and exhausted fixed-length source tuples cannot satisfy a target with additional positional +arguments or an open-ended variadic parameter. + +```py +# error: [invalid-assignment] +empty_with_prefix: Callable[[int, Unpack[tuple[str, ...]], None], None] = accepts_no_arguments + +# error: [invalid-assignment] +empty_with_suffix: Callable[[Unpack[tuple[str, ...]], None], None] = accepts_no_arguments + +# error: [invalid-assignment] +exhausted_with_suffix: Callable[[int, Unpack[tuple[str, ...]], None], None] = accepts_one_integer + +# error: [invalid-assignment] +callback: Callable[[Unpack[tuple[tuple[int], ...]], tuple[int]], None] = accepts_one_integer + +static_assert( + not is_assignable_to( + RegularCallableTypeOf[accepts_one_integer], + Callable[[Unpack[tuple[tuple[int], ...]], tuple[int]], None], + ) +) +``` + ### Function types ```py diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index f9b6a41ccb..1c3541efa7 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -1345,6 +1345,136 @@ static_assert(is_subtype_of(RegularCallableTypeOf[variadic], RegularCallableType static_assert(is_subtype_of(RegularCallableTypeOf[variadic], RegularCallableTypeOf[positional_variadic])) ``` +#### Variadic with an unpacked positional suffix + +A variadic positional parameter must accept both the unpacked elements and any fixed positional +suffix in the supertype. + +```py +from typing import Callable, Never, Unpack, cast +from ty_extensions import static_assert +from ty_extensions._internal import RegularCallableTypeOf, is_subtype_of + +def accepts_objects(*args: object) -> None: ... +def accepts_strings_or_none(*args: str | None) -> None: ... +def accepts_strings(*args: str) -> None: ... + +static_assert( + is_subtype_of( + RegularCallableTypeOf[accepts_objects], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + is_subtype_of( + RegularCallableTypeOf[accepts_strings_or_none], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + not is_subtype_of( + RegularCallableTypeOf[accepts_strings], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +``` + +A required suffix can align with a longer suffix or an equivalent positional prefix when all the +unpacked elements have the same type. + +```py +def requires_one_integer(*args: *tuple[*tuple[int, ...], int]) -> None: ... + +type OneOrMoreIntegers = RegularCallableTypeOf[requires_one_integer] + +static_assert(is_subtype_of(OneOrMoreIntegers, Callable[[*tuple[int, ...], int, int], None])) +static_assert(is_subtype_of(OneOrMoreIntegers, Callable[[int, *tuple[int, ...]], None])) +``` + +A type alias for the variadic element does not prevent the required suffix from matching. + +```py +type Integer = int + +def requires_one_aliased_integer(*args: *tuple[*tuple[Integer, ...], int]) -> None: ... + +type AliasedIntegers = RegularCallableTypeOf[requires_one_aliased_integer] + +static_assert(is_subtype_of(AliasedIntegers, Callable[[int, *tuple[int, ...]], None])) +``` + +A longer suffix is aligned from the end when its other elements fit the source variadic parameter. + +```py +def requires_string_suffix(*args: *tuple[*tuple[object, ...], str]) -> None: ... +def requires_string_after_integers(*args: *tuple[*tuple[int, ...], str]) -> None: ... + +type StringSuffix = RegularCallableTypeOf[requires_string_suffix] +type IntegerStringSuffix = RegularCallableTypeOf[requires_string_after_integers] + +static_assert(is_subtype_of(StringSuffix, Callable[[*tuple[object, ...], int, str], None])) +static_assert(is_subtype_of(IntegerStringSuffix, Callable[[*tuple[int, ...], int, str], None])) +``` + +A positional parameter cannot also be filled by a target keyword argument. + +```py +def occupies_keyword(a: int, *args: int, **kwargs: int) -> None: ... +def accepts_keyword(*args: *tuple[*tuple[int, ...], int], **kwargs: int) -> None: ... + +type OccupiesKeyword = RegularCallableTypeOf[occupies_keyword] +type AcceptsKeyword = RegularCallableTypeOf[accepts_keyword] + +static_assert(not is_subtype_of(OccupiesKeyword, AcceptsKeyword)) +``` + +An uninhabited keyword parameter cannot collide with an occupied positional parameter. + +```py +type Bottom = Never + +def rejects_keywords(*args: *tuple[*tuple[int, ...], int], **kwargs: Bottom) -> None: ... +def rejects_named_keyword(*args: *tuple[*tuple[int, ...], int], a: Never = cast(Never, 0)) -> None: ... + +static_assert(is_subtype_of(OccupiesKeyword, RegularCallableTypeOf[rejects_keywords])) +static_assert(is_subtype_of(OccupiesKeyword, RegularCallableTypeOf[rejects_named_keyword])) +``` + +Equivalent empty or fixed-length unpacked parameters are compatible, but cannot be reused for +additional positional arguments. + +```py +def accepts_no_arguments(*args: Unpack[tuple[()]]) -> None: ... +def accepts_one_integer(*args: Unpack[tuple[int]]) -> None: ... + +static_assert(is_subtype_of(RegularCallableTypeOf[accepts_no_arguments], Callable[[Unpack[tuple[()]]], None])) +static_assert(is_subtype_of(RegularCallableTypeOf[accepts_one_integer], Callable[[Unpack[tuple[int]]], None])) +static_assert( + not is_subtype_of( + RegularCallableTypeOf[accepts_no_arguments], + Callable[[int, Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + not is_subtype_of( + RegularCallableTypeOf[accepts_no_arguments], + Callable[[Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + not is_subtype_of( + RegularCallableTypeOf[accepts_one_integer], + Callable[[int, Unpack[tuple[str, ...]], None], None], + ) +) +static_assert( + not is_subtype_of( + RegularCallableTypeOf[accepts_one_integer], + Callable[[Unpack[tuple[tuple[int], ...]], tuple[int]], None], + ) +) +``` + #### Variadic with other kinds Variadic parameter in a subtype can only be used to match against an unmatched positional-only diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index a6c53ab411..ef40cac318 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -2454,8 +2454,47 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } } + let mut source_parameters = source.parameters.expand_starred_variadic_annotations(db); + let mut target_parameters = target.parameters.expand_starred_variadic_annotations(db); + + // Gradual variadics and TypeVarTuples need their original suffix boundaries for + // materialization and inference. Named source prefixes must also remain visible when a + // target keyword could fill the same parameter. + if let (Some((_, source_variadic)), Some((_, target_variadic))) = + (source_parameters.variadic(), target_parameters.variadic()) + && !source_variadic.has_starred_annotation() + && !target_variadic.has_starred_annotation() + && source_variadic.annotated_type().resolve_type_alias(db) + == target_variadic.annotated_type().resolve_type_alias(db) + && !source_variadic + .annotated_type() + .resolve_type_alias(db) + .is_dynamic() + && source_parameters.positional().all(|source_parameter| { + source_parameter.is_positional_only() + || source_parameter.name().is_none_or(|name| { + match target_parameters.keyword_by_name(name) { + Some((_, parameter)) => { + !parameter.is_keyword_only() + || parameter.annotated_type().resolve_type_alias(db).is_never() + } + None => { + target_parameters + .keyword_variadic() + .is_none_or(|(_, parameter)| { + parameter.annotated_type().resolve_type_alias(db).is_never() + }) + } + } + }) + }) + { + source_parameters = source_parameters.with_homogeneous_variadic_suffix_in_prefix(db); + target_parameters = target_parameters.with_homogeneous_variadic_suffix_in_prefix(db); + } + let target_typevartuple = if self.typevar_evaluation == TypeVarEvaluation::Lazy { - target.parameters.variadic().and_then(|(index, parameter)| { + target_parameters.variadic().and_then(|(index, parameter)| { if parameter.has_starred_annotation() && let Type::TypeVar(typevartuple) = parameter.annotated_type() && typevartuple.is_typevartuple(db) @@ -2474,14 +2513,14 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // comparison below reaches the same result, but only after doing work that is expensive for // large overload sets. An unpacked target TypeVarTuple bypasses this fast path so it can be // constrained from the source parameters. - if source.parameters.is_standard() - && target.parameters.is_standard() - && source.parameters.variadic().is_none() + if source_parameters.is_standard() + && target_parameters.is_standard() + && source_parameters.variadic().is_none() && target_typevartuple.is_none() { - let source_positional = source.parameters.positional().count(); - let target_positional = target.parameters.positional().count(); - let target_variadic = target.parameters.variadic(); + let source_positional = source_parameters.positional().count(); + let target_positional = target_parameters.positional().count(); + let target_variadic = target_parameters.variadic(); // A subdiagnostic telling the user that `source` is missing a `*args` parameter // is only guaranteed to be correct when `target` has a plain, open-ended variadic tail. @@ -2494,8 +2533,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let target_has_open_ended_variadic = || { target_variadic.is_some_and(|(index, parameter)| { !parameter.has_starred_annotation() - && !target - .parameters + && !target_parameters .iter() .skip(index) .any(Parameter::is_positional) @@ -2510,8 +2548,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { && (target_positional > source_positional || target_has_open_ended_variadic()) { let error_context = if target_positional > source_positional { - let source_parameter_kind = source - .parameters + let source_parameter_kind = source_parameters .get(source_positional) .map(Parameter::kind); @@ -2522,8 +2559,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } } Some(ParameterKind::KeywordVariadic { .. }) | None => { - let parameter = target - .parameters + let parameter = target_parameters .get_positional(source_positional) .and_then(Parameter::name); ErrorContext::MissingParameter { @@ -2622,8 +2658,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { }; if self.typevar_evaluation == TypeVarEvaluation::Lazy { - let source_paramspec = source.parameters.as_paramspec_with_prefix(); - let target_paramspec = target.parameters.as_paramspec_with_prefix(); + let source_paramspec = source_parameters.as_paramspec_with_prefix(); + let target_paramspec = target_parameters.as_paramspec_with_prefix(); // If either signature is a ParamSpec, the constraint set should bind the ParamSpec to // the other signature before the return-type and gradual/top fast paths can return @@ -2887,7 +2923,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { CallableSignature::single( Signature::new_generic( source.generic_context, - source.parameters.clone(), + source_parameters.clone(), Type::unknown(), ) .with_source_overload_index(source.source_overload_index()), @@ -2909,8 +2945,6 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // self: callable without ParamSpec // other: `Concatenate[, P]` (None, Some((target_prefix_params, target_bound_typevar))) => { - let source_parameters = - source.parameters.expand_starred_variadic_annotations(db); // Loop over self parameters and target_prefix_params in a similar manner to the // above loop let mut parameters = ParametersZip { @@ -3029,9 +3063,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } let (source_params, _) = parameters.into_remaining(); - let source_params = source - .parameters - .with_transformed_parameters(source_params.cloned()); + let source_params = + source_parameters.with_transformed_parameters(source_params.cloned()); let lower = Type::Callable(CallableType::new( db, CallableSignature::single( @@ -3065,7 +3098,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { CallableSignature::single( Signature::new_generic( target.generic_context, - target.parameters.clone(), + target_parameters.clone(), Type::unknown(), ) .with_source_overload_index(target.source_overload_index()), @@ -3091,10 +3124,10 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { current_source: None, current_target: None, source_iter: source_prefix_params.iter(), - target_iter: target.parameters.iter(), + target_iter: target_parameters.iter(), }; - if target.parameters.kind() != ParametersKind::Gradual { + if target_parameters.kind() != ParametersKind::Gradual { let mut target_index = 0usize; while let Some(next_parameter) = parameters.next() { match next_parameter { @@ -3175,9 +3208,8 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } let (_, target_params) = parameters.into_remaining(); - let target_params = target - .parameters - .with_transformed_parameters(target_params.cloned()); + let target_params = + target_parameters.with_transformed_parameters(target_params.cloned()); let upper = Type::Callable(CallableType::new( db, CallableSignature::single( @@ -3214,16 +3246,14 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // A gradual parameter list is a supertype of the "bottom" parameter list (*args: object, // **kwargs: object). - if target.parameters.is_gradual() - && (matches!(target.parameters.kind(), ParametersKind::Gradual) + if target_parameters.is_gradual() + && (matches!(target_parameters.kind(), ParametersKind::Gradual) || self.typevar_evaluation == TypeVarEvaluation::Lazy) - && !source.parameters.is_top() - && source - .parameters + && !source_parameters.is_top() + && source_parameters .variadic() .is_some_and(|(_, param)| param.annotated_type().is_object()) - && source - .parameters + && source_parameters .keyword_variadic() .is_some_and(|(_, param)| param.annotated_type().is_object()) { @@ -3232,9 +3262,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // The top signature is supertype of (and assignable from) all other signatures. It is a // subtype of no signature except itself, and assignable only to the gradual signature. - if target.parameters.is_top() { + if target_parameters.is_top() { return result; - } else if source.parameters.is_top() && !target.parameters.is_gradual() { + } else if source_parameters.is_top() && !target_parameters.is_gradual() { if let Some(context) = self.report_context() { context.push(ErrorContext::TopCallableAssignedToNonTop { return_type: source.return_ty, @@ -3248,9 +3278,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // unpacked target TypeVarTuple instead continues to the ordinary parameter comparison so it // can be constrained from the source parameters. if target_typevartuple.is_none() - && (source.parameters.is_gradual() || target.parameters.is_gradual()) + && (source_parameters.is_gradual() || target_parameters.is_gradual()) { - match (source.parameters.kind(), target.parameters.kind()) { + match (source_parameters.kind(), target_parameters.kind()) { // Both parameter lists are `Concatenate` with gradual forms. All prefix parameters // are going to be positional-only. ( @@ -3258,9 +3288,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ParametersKind::Concatenate(ConcatenateTail::Gradual), ) => { let source_prefix_params = - &source.parameters.as_slice()[..source.parameters.len().saturating_sub(2)]; + &source_parameters.as_slice()[..source_parameters.len().saturating_sub(2)]; let target_prefix_params = - &target.parameters.as_slice()[..target.parameters.len().saturating_sub(2)]; + &target_parameters.as_slice()[..target_parameters.len().saturating_sub(2)]; for (target_index, (source_param, target_param)) in source_prefix_params .iter() @@ -3286,11 +3316,11 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ParametersKind::Standard, ) => { let source_prefix_params = - &source.parameters.as_slice()[..source.parameters.len().saturating_sub(2)]; + &source_parameters.as_slice()[..source_parameters.len().saturating_sub(2)]; for (target_index, param) in source_prefix_params .iter() - .zip_longest(target.parameters.iter()) + .zip_longest(target_parameters.iter()) .enumerate() { match param { @@ -3343,12 +3373,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ParametersKind::Concatenate(ConcatenateTail::Gradual), ) => { let target_prefix_params = - &target.parameters.as_slice()[..target.parameters.len().saturating_sub(2)]; + &target_parameters.as_slice()[..target_parameters.len().saturating_sub(2)]; let mut parameters = ParametersZip { current_source: None, current_target: None, - source_iter: source.parameters.iter(), + source_iter: source_parameters.iter(), target_iter: target_prefix_params.iter(), }; @@ -3438,21 +3468,13 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { self.constraints, ConstraintSet::from_bool( self.constraints, - source.parameters.is_gradual() && target.parameters.is_gradual(), + source_parameters.is_gradual() && target_parameters.is_gradual(), ), ), TypeRelation::Assignability => result, }; } - // TODO: Normalize starred variadic annotations for all signature comparisons. Restricting - // expansion to target TypeVarTuple inference means equivalent nested unpackings such as - // `*tuple[*tuple[str, ...], bytes]` and `*tuple[str, ...], bytes` are not related correctly. - let source_parameters = if target_typevartuple.is_some() { - source.parameters.expand_starred_variadic_annotations(db) - } else { - source.parameters.clone() - }; // Align the fixed target prefix and suffix before entering the parameter loop so that the // target TypeVarTuple captures only the source parameter entries between them. let typevartuple_source_parameter_count = if let Some((typevartuple_index, _)) = @@ -3462,7 +3484,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .iter() .take_while(|parameter| parameter.is_positional() || parameter.is_variadic()) .count(); - let target_suffix_len = target.parameters.as_slice()[typevartuple_index + 1..] + let target_suffix_len = target_parameters.as_slice()[typevartuple_index + 1..] .iter() .take_while(|parameter| parameter.is_positional()) .count(); @@ -3511,7 +3533,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { current_source: None, current_target: None, source_iter: source_parameters.iter(), - target_iter: target.parameters.iter(), + target_iter: target_parameters.iter(), }; // Collect all the standard parameters that have only been matched against a variadic @@ -3605,27 +3627,20 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // If there are more parameters in `target` than in `source`, then `source` is // not a subtype of `target`. if let Some(context) = self.report_context() - && target.parameters.as_paramspec_with_prefix().is_none() + && target_parameters.as_paramspec_with_prefix().is_none() { let error_context = match target_parameter.kind() { ParameterKind::PositionalOnly { .. } - | ParameterKind::PositionalOrKeyword { .. } => unreachable!( - "unmatched target positional parameters \ - are rejected by the positional fast path" - ), - ParameterKind::Variadic { .. } => { - unreachable!( - "an unmatched target `*args` is impossible: \ - a source without `*args` is rejected by the positional fast path, \ - while a source with `*args` consumes the target during matching" - ) - } - ParameterKind::KeywordOnly { .. } => ErrorContext::MissingParameter { + | ParameterKind::PositionalOrKeyword { .. } + | ParameterKind::KeywordOnly { .. } => ErrorContext::MissingParameter { parameter: ParameterDescription::new( target_index, target_parameter.name(), ), }, + ParameterKind::Variadic { .. } => { + ErrorContext::MissingVariadicPositionalParameter + } ParameterKind::KeywordVariadic { .. } => { ErrorContext::MissingVariadicKeywordParameter } @@ -3832,7 +3847,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } target_index += 1; - if source.parameters.is_gradual() { + if source_parameters.is_gradual() { return match self.relation { TypeRelation::Assignability => result, TypeRelation::Subtyping @@ -3845,7 +3860,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { if !source_param.is_variadic() { if let Some(context) = self.report_context() - && target.parameters.as_paramspec_with_prefix().is_none() + && target_parameters.as_paramspec_with_prefix().is_none() { let parameter = ParameterDescription::new( target_index, @@ -3865,6 +3880,37 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ) { return result; } + + // Align fixed suffixes from the end, reusing the source variadic for + // any additional target suffix elements. + let source_suffix_len = parameters + .source_iter + .as_slice() + .iter() + .take_while(|parameter| parameter.is_positional()) + .count(); + let target_suffix_len = parameters + .target_iter + .as_slice() + .iter() + .take_while(|parameter| parameter.is_positional()) + .count(); + for _ in source_suffix_len..target_suffix_len { + let Some(target_parameter) = parameters.peek_target() else { + break; + }; + target_index += 1; + if !check_types( + &mut result, + target_parameter.annotated_type(), + source_param.annotated_type(), + target_parameter.name(), + target_index, + ) { + return result; + } + parameters.next_target(); + } } ( @@ -3945,8 +3991,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { if default_type.is_none() { if let Some(context) = self.report_context() { if let Some(source_name) = source_param.name() - && target - .parameters + && target_parameters .iter() .any(|target_param| target_param.name() == Some(source_name)) { @@ -4034,7 +4079,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // For a `source <: target` relationship, if `target` has a keyword variadic // parameter, `source` must also have a keyword variadic parameter. if let Some(context) = self.report_context() - && target.parameters.as_paramspec_with_prefix().is_none() + && target_parameters.as_paramspec_with_prefix().is_none() { context.push(ErrorContext::MissingVariadicKeywordParameter); } @@ -4864,6 +4909,35 @@ impl<'db> Parameters<'db> { .rfind(|(_, parameter)| parameter.is_keyword_variadic()) } + /// Moves required suffix elements that match a homogeneous variadic into its prefix. + fn with_homogeneous_variadic_suffix_in_prefix(self, db: &'db dyn Db) -> Self { + let Some((variadic_index, variadic)) = self.variadic() else { + return self; + }; + + let matching_suffix_len = self.as_slice()[variadic_index + 1..] + .iter() + .take_while(|parameter| { + parameter.is_positional_only() + && parameter.annotated_type().resolve_type_alias(db) + == variadic.annotated_type().resolve_type_alias(db) + }) + .count(); + + if matching_suffix_len == 0 + || self + .as_slice() + .get(variadic_index + matching_suffix_len + 1) + .is_some_and(Parameter::is_positional) + { + return self; + } + + let mut parameters = self.as_slice().to_vec(); + parameters[variadic_index..=variadic_index + matching_suffix_len].rotate_left(1); + self.with_transformed_parameters(parameters) + } + /// Expands an unpacked `*args` annotation into its logical callable parameters. /// /// Preserve the original `*args` definition and source position on every expanded parameter @@ -4909,14 +4983,17 @@ impl<'db> Parameters<'db> { .name() .cloned() .unwrap_or_else(|| Name::new_static("args")); + let variadic = Parameter::variadic(name); + let variadic = match variable.variable() { + VariableSegment::Homogeneous(element) => { + variadic.with_annotated_type(element) + } + VariableSegment::TypeVarTuple(typevartuple) => variadic + .with_annotated_type(Type::TypeVar(typevartuple)) + .with_starred_annotation(), + }; parameters.push( - Parameter::variadic(name) - .with_annotated_type(match variable.variable() { - VariableSegment::Homogeneous(element) => element, - VariableSegment::TypeVarTuple(typevartuple) => { - Type::TypeVar(typevartuple) - } - }) + variadic .with_definition(parameter.definition()) .with_source_parameter_index(parameter.source_parameter_index()), ); @@ -4930,7 +5007,7 @@ impl<'db> Parameters<'db> { } if expanded { - Self::from_annotation(db, parameters) + self.with_transformed_parameters(parameters) } else { self.clone() } From c8399f2d7016cac879b2c4638d416c025636febb Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 31 Jul 2026 13:00:26 -0500 Subject: [PATCH 271/390] test(annotate): Verify cell_index behavior --- crates/ruff_annotate_snippets/tests/ruff.rs | 447 ++++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 crates/ruff_annotate_snippets/tests/ruff.rs diff --git a/crates/ruff_annotate_snippets/tests/ruff.rs b/crates/ruff_annotate_snippets/tests/ruff.rs new file mode 100644 index 0000000000..93bae4de21 --- /dev/null +++ b/crates/ruff_annotate_snippets/tests/ruff.rs @@ -0,0 +1,447 @@ +use annotate_snippets::{AnnotationKind, Level, Patch, Renderer, Snippet}; + +use annotate_snippets::renderer::DecorStyle; +use snapbox::{assert_data_eq, str}; + +#[test] +fn snippet_with_cell() { + let source = "First line\r\nSecond oops line"; + let input = &[Level::ERROR.primary_title("oops").element( + Snippet::source(source) + .cell_index(Some(1)) + .annotation(AnnotationKind::Primary.span(19..23).label("oops")), + )]; + let expected_ascii = str![[r#" +error: oops + | +2 | Second oops line + | ^^^^ oops +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected_ascii); + + let expected_unicode = str![[r#" +error: oops + ╭▸ +2 │ Second oops line + ╰╴ ━━━━ oops +"#]]; + let renderer = renderer.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer.render(input), expected_unicode); +} + +#[test] +fn snippet_with_path_cell() { + let source = "First line\r\nSecond oops line"; + let input = &[Level::ERROR.primary_title("oops").element( + Snippet::source(source) + .path("foo.ipynb") + .cell_index(Some(1)) + .annotation(AnnotationKind::Primary.span(19..23).label("oops")), + )]; + let expected_ascii = str![[r#" +error: oops + --> foo.ipynb:cell 1:2:8 + | +2 | Second oops line + | ^^^^ oops +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected_ascii); + + let expected_unicode = str![[r#" +error: oops + ╭▸ foo.ipynb:cell 1:2:8 + │ +2 │ Second oops line + ╰╴ ━━━━ oops +"#]]; + let renderer = renderer.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer.render(input), expected_unicode); +} + +#[test] +fn patch_with_primary_cell() { + let source = "First line\r\nSecond oops line"; + let input = &[ + Level::ERROR.primary_title("oops").element( + Snippet::source(source) + .cell_index(Some(1)) + .annotation(AnnotationKind::Primary.span(19..23).label("oops")), + ), + Level::HELP + .secondary_title("remove the entry") + .element( + Snippet::source(source) + .cell_index(Some(1)) + .patch(Patch::new(19..24, "")), + ) + .element( + Snippet::source(source) + .cell_index(Some(1)) + .patch(Patch::new(23..28, "")), + ), + ]; + let expected_ascii = str![[r#" +error: oops + | +2 | Second oops line + | ^^^^ oops + | +help: remove the entry + | +2 - Second oops line +2 + Second line + | +2 - Second oops line +2 + Second oops + | +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected_ascii); + + let expected_unicode = str![[r#" +error: oops + ╭▸ +2 │ Second oops line + │ ━━━━ oops + ╰╴ +help: remove the entry + ╭╴ +2 - Second oops line +2 + Second line + ├╴ +2 - Second oops line +2 + Second oops + ╰╴ +"#]]; + let renderer = renderer.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer.render(input), expected_unicode); +} + +#[test] +fn patch_with_primary_path_cell() { + let source = "First line\r\nSecond oops line"; + let input = &[ + Level::ERROR.primary_title("oops").element( + Snippet::source(source) + .path("foo.ipynb") + .cell_index(Some(1)) + .annotation(AnnotationKind::Primary.span(19..23).label("oops")), + ), + Level::HELP + .secondary_title("remove the entry") + .element( + Snippet::source(source) + .path("foo.ipynb") + .cell_index(Some(1)) + .patch(Patch::new(19..24, "")), + ) + .element( + Snippet::source(source) + .path("foo.ipynb") + .cell_index(Some(1)) + .patch(Patch::new(23..28, "")), + ), + ]; + let expected_ascii = str![[r#" +error: oops + --> foo.ipynb:cell 1:2:8 + | +2 | Second oops line + | ^^^^ oops + | +help: remove the entry + | +2 - Second oops line +2 + Second line + | +2 - Second oops line +2 + Second oops + | +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected_ascii); + + let expected_unicode = str![[r#" +error: oops + ╭▸ foo.ipynb:cell 1:2:8 + │ +2 │ Second oops line + │ ━━━━ oops + ╰╴ +help: remove the entry + ╭╴ +2 - Second oops line +2 + Second line + ├╴ +2 - Second oops line +2 + Second oops + ╰╴ +"#]]; + let renderer = renderer.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer.render(input), expected_unicode); +} + +#[test] +fn patch_with_primary_path_incrementing_cell() { + let source = "First line\r\nSecond oops line"; + let input = &[ + Level::ERROR.primary_title("oops").element( + Snippet::source(source) + .path("foo.ipynb") + .cell_index(Some(1)) + .annotation(AnnotationKind::Primary.span(19..23).label("oops")), + ), + Level::HELP + .secondary_title("remove the entry") + .element( + Snippet::source(source) + .path("foo.ipynb") + .cell_index(Some(1)) + .patch(Patch::new(19..24, "")), + ) + .element( + Snippet::source(source) + .path("foo.ipynb") + .cell_index(Some(2)) + .patch(Patch::new(23..28, "")), + ), + ]; + let expected_ascii = str![[r#" +error: oops + --> foo.ipynb:cell 1:2:8 + | +2 | Second oops line + | ^^^^ oops + | +help: remove the entry + | +2 - Second oops line +2 + Second line + | +2 - Second oops line +2 + Second oops + | +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected_ascii); + + let expected_unicode = str![[r#" +error: oops + ╭▸ foo.ipynb:cell 1:2:8 + │ +2 │ Second oops line + │ ━━━━ oops + ╰╴ +help: remove the entry + ╭╴ +2 - Second oops line +2 + Second line + ├╴ +2 - Second oops line +2 + Second oops + ╰╴ +"#]]; + let renderer = renderer.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer.render(input), expected_unicode); +} + +#[test] +fn patch_with_other_cell() { + let source = "First line\r\nSecond oops line"; + let input = &[ + Level::ERROR.primary_title("oops").element( + Snippet::source(source) + .cell_index(Some(1)) + .annotation(AnnotationKind::Primary.span(19..23).label("oops")), + ), + Level::HELP + .secondary_title("remove the entry") + .element( + Snippet::source(source) + .cell_index(Some(2)) + .patch(Patch::new(19..24, "")), + ) + .element( + Snippet::source(source) + .cell_index(Some(2)) + .patch(Patch::new(23..28, "")), + ), + ]; + let expected_ascii = str![[r#" +error: oops + | +2 | Second oops line + | ^^^^ oops + | +help: remove the entry + | +2 - Second oops line +2 + Second line + | +2 - Second oops line +2 + Second oops + | +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected_ascii); + + let expected_unicode = str![[r#" +error: oops + ╭▸ +2 │ Second oops line + │ ━━━━ oops + ╰╴ +help: remove the entry + ╭╴ +2 - Second oops line +2 + Second line + ├╴ +2 - Second oops line +2 + Second oops + ╰╴ +"#]]; + let renderer = renderer.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer.render(input), expected_unicode); +} + +#[test] +fn patch_with_other_path_cell() { + let source = "First line\r\nSecond oops line"; + let input = &[ + Level::ERROR.primary_title("oops").element( + Snippet::source(source) + .path("foo.ipynb") + .cell_index(Some(1)) + .annotation(AnnotationKind::Primary.span(19..23).label("oops")), + ), + Level::HELP + .secondary_title("remove the entry") + .element( + Snippet::source(source) + .path("bar.ipynb") + .cell_index(Some(2)) + .patch(Patch::new(19..24, "")), + ) + .element( + Snippet::source(source) + .path("bar.ipynb") + .cell_index(Some(2)) + .patch(Patch::new(23..28, "")), + ), + ]; + let expected_ascii = str![[r#" +error: oops + --> foo.ipynb:cell 1:2:8 + | +2 | Second oops line + | ^^^^ oops + | +help: remove the entry + --> bar.ipynb:LL:8 + | +2 - Second oops line +2 + Second line + | +2 - Second oops line +2 + Second oops + | +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected_ascii); + + let expected_unicode = str![[r#" +error: oops + ╭▸ foo.ipynb:cell 1:2:8 + │ +2 │ Second oops line + │ ━━━━ oops + ╰╴ +help: remove the entry + ╭▸ bar.ipynb:LL:8 + │ +2 - Second oops line +2 + Second line + ├╴ +2 - Second oops line +2 + Second oops + ╰╴ +"#]]; + let renderer = renderer.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer.render(input), expected_unicode); +} + +#[test] +fn patch_with_other_path_incrementing_cell() { + let source = "First line\r\nSecond oops line"; + let input = &[ + Level::ERROR.primary_title("oops").element( + Snippet::source(source) + .path("foo.ipynb") + .cell_index(Some(1)) + .annotation(AnnotationKind::Primary.span(19..23).label("oops")), + ), + Level::HELP + .secondary_title("remove the entry") + .element( + Snippet::source(source) + .path("bar.ipynb") + .cell_index(Some(2)) + .patch(Patch::new(19..24, "")), + ) + .element( + Snippet::source(source) + .path("bar.ipynb") + .cell_index(Some(3)) + .patch(Patch::new(23..28, "")), + ), + ]; + let expected_ascii = str![[r#" +error: oops + --> foo.ipynb:cell 1:2:8 + | +2 | Second oops line + | ^^^^ oops + | +help: remove the entry + --> bar.ipynb:LL:8 + | +2 - Second oops line +2 + Second line + | +2 - Second oops line +2 + Second oops + | +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected_ascii); + + let expected_unicode = str![[r#" +error: oops + ╭▸ foo.ipynb:cell 1:2:8 + │ +2 │ Second oops line + │ ━━━━ oops + ╰╴ +help: remove the entry + ╭▸ bar.ipynb:LL:8 + │ +2 - Second oops line +2 + Second line + ├╴ +2 - Second oops line +2 + Second oops + ╰╴ +"#]]; + let renderer = renderer.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer.render(input), expected_unicode); +} From 22b2bc8ef4c434c142c3107e80d0f409c16a03a4 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 31 Jul 2026 17:07:35 -0500 Subject: [PATCH 272/390] fix(annotate): Fix the suggestion anonymous logic --- crates/ruff_annotate_snippets/src/renderer/render.rs | 4 ++-- .../color/highlight_first_line_tab_371.ascii.term.svg | 2 +- crates/ruff_annotate_snippets/tests/formatter.rs | 4 ++-- crates/ruff_annotate_snippets/tests/ruff.rs | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/ruff_annotate_snippets/src/renderer/render.rs b/crates/ruff_annotate_snippets/src/renderer/render.rs index e17ef80039..94184096d6 100644 --- a/crates/ruff_annotate_snippets/src/renderer/render.rs +++ b/crates/ruff_annotate_snippets/src/renderer/render.rs @@ -1535,9 +1535,9 @@ fn emit_suggestion_default( let arrow = renderer.decor_style.file_start(is_first, false); buffer.append(row_num - 1, arrow, ElementStyle::LineNumber); let message = if renderer.anonymized_line_numbers { - format!("{}:{}:{}", path, loc.line, loc.char + 1) - } else { format!("{}:{}:{}", path, ANONYMIZED_LINE_NUM, loc.char + 1) + } else { + format!("{}:{}:{}", path, loc.line, loc.char + 1) }; buffer.append(row_num - 1, &message, ElementStyle::LineAndColumn); diff --git a/crates/ruff_annotate_snippets/tests/color/highlight_first_line_tab_371.ascii.term.svg b/crates/ruff_annotate_snippets/tests/color/highlight_first_line_tab_371.ascii.term.svg index 5f9dadc2ea..67f544a9a8 100644 --- a/crates/ruff_annotate_snippets/tests/color/highlight_first_line_tab_371.ascii.term.svg +++ b/crates/ruff_annotate_snippets/tests/color/highlight_first_line_tab_371.ascii.term.svg @@ -22,7 +22,7 @@ error: <sample error message> - --> <sample path>:LL:18 + --> <sample path>:6:18 | diff --git a/crates/ruff_annotate_snippets/tests/formatter.rs b/crates/ruff_annotate_snippets/tests/formatter.rs index 2b627f441c..d3afafd6b3 100644 --- a/crates/ruff_annotate_snippets/tests/formatter.rs +++ b/crates/ruff_annotate_snippets/tests/formatter.rs @@ -5281,7 +5281,7 @@ error[E0624]: method `five_years` is private | ---------- private method defined here | help: consider making `bar` public - --> other.rs:LL:1 + --> other.rs:1:1 | 1 | pub fn bar(&self) { | +++ @@ -5302,7 +5302,7 @@ error[E0624]: method `five_years` is private │ ────────── private method defined here ╰╴ help: consider making `bar` public - ╭▸ other.rs:LL:1 + ╭▸ other.rs:1:1 │ 1 │ pub fn bar(&self) { ╰╴+++ diff --git a/crates/ruff_annotate_snippets/tests/ruff.rs b/crates/ruff_annotate_snippets/tests/ruff.rs index 93bae4de21..bb5c1cd6c7 100644 --- a/crates/ruff_annotate_snippets/tests/ruff.rs +++ b/crates/ruff_annotate_snippets/tests/ruff.rs @@ -345,7 +345,7 @@ error: oops | ^^^^ oops | help: remove the entry - --> bar.ipynb:LL:8 + --> bar.ipynb:2:8 | 2 - Second oops line 2 + Second line @@ -366,7 +366,7 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭▸ bar.ipynb:LL:8 + ╭▸ bar.ipynb:2:8 │ 2 - Second oops line 2 + Second line @@ -412,7 +412,7 @@ error: oops | ^^^^ oops | help: remove the entry - --> bar.ipynb:LL:8 + --> bar.ipynb:2:8 | 2 - Second oops line 2 + Second line @@ -433,7 +433,7 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭▸ bar.ipynb:LL:8 + ╭▸ bar.ipynb:2:8 │ 2 - Second oops line 2 + Second line From 36eb998b81a9af27c6dc31ea77ef0d3c8d65b880 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 31 Jul 2026 13:50:20 -0500 Subject: [PATCH 273/390] refactor(annotate): Pull out origin formatting --- .../src/renderer/render.rs | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/crates/ruff_annotate_snippets/src/renderer/render.rs b/crates/ruff_annotate_snippets/src/renderer/render.rs index 94184096d6..b1bfd0b9db 100644 --- a/crates/ruff_annotate_snippets/src/renderer/render.rs +++ b/crates/ruff_annotate_snippets/src/renderer/render.rs @@ -528,26 +528,28 @@ fn render_origin( ); } - let str = { - use core::fmt::Write as _; + let str = format_origin(origin, renderer.anonymized_line_numbers); + buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn); +} + +fn format_origin(origin: &Origin<'_>, anonymized_line_numbers: bool) -> String { + use core::fmt::Write as _; - let mut buffer = origin.path.as_ref().to_owned(); - if let Some(cell_index) = origin.cell_index { - write!(&mut buffer, ":cell {cell_index}").unwrap(); + let mut buffer = origin.path.as_ref().to_owned(); + if let Some(cell_index) = origin.cell_index { + write!(&mut buffer, ":cell {cell_index}").unwrap(); + } + if let Some(line) = origin.line { + if anonymized_line_numbers { + write!(&mut buffer, ":{ANONYMIZED_LINE_NUM}").unwrap(); + } else { + write!(&mut buffer, ":{line}").unwrap(); } - if let Some(line) = origin.line { - if renderer.anonymized_line_numbers { - write!(&mut buffer, ":{ANONYMIZED_LINE_NUM}").unwrap(); - } else { - write!(&mut buffer, ":{line}").unwrap(); - } - if let Some(col) = origin.char_column { - write!(&mut buffer, ":{col}").unwrap(); - } + if let Some(col) = origin.char_column { + write!(&mut buffer, ":{col}").unwrap(); } - buffer - }; - buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn); + } + buffer } #[allow(clippy::too_many_arguments)] From c6c4d38bbeeae91fe59d9c277a78169db8306561 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 31 Jul 2026 13:59:31 -0500 Subject: [PATCH 274/390] fix(annotate): Render cells for patches --- crates/ruff_annotate_snippets/src/renderer/render.rs | 9 ++++----- crates/ruff_annotate_snippets/tests/ruff.rs | 8 ++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/ruff_annotate_snippets/src/renderer/render.rs b/crates/ruff_annotate_snippets/src/renderer/render.rs index b1bfd0b9db..62c7301918 100644 --- a/crates/ruff_annotate_snippets/src/renderer/render.rs +++ b/crates/ruff_annotate_snippets/src/renderer/render.rs @@ -1536,11 +1536,10 @@ fn emit_suggestion_default( } let arrow = renderer.decor_style.file_start(is_first, false); buffer.append(row_num - 1, arrow, ElementStyle::LineNumber); - let message = if renderer.anonymized_line_numbers { - format!("{}:{}:{}", path, ANONYMIZED_LINE_NUM, loc.char + 1) - } else { - format!("{}:{}:{}", path, loc.line, loc.char + 1) - }; + let mut origin = Origin::path(path.as_ref()).cell_index(suggestion.cell_index); + origin.line = Some(loc.line); + origin.char_column = Some(loc.char + 1); + let message = format_origin(&origin, renderer.anonymized_line_numbers); buffer.append(row_num - 1, &message, ElementStyle::LineAndColumn); draw_col_separator_no_space(renderer, buffer, row_num, max_line_num_len + 1); diff --git a/crates/ruff_annotate_snippets/tests/ruff.rs b/crates/ruff_annotate_snippets/tests/ruff.rs index bb5c1cd6c7..16caa4b1d7 100644 --- a/crates/ruff_annotate_snippets/tests/ruff.rs +++ b/crates/ruff_annotate_snippets/tests/ruff.rs @@ -345,7 +345,7 @@ error: oops | ^^^^ oops | help: remove the entry - --> bar.ipynb:2:8 + --> bar.ipynb:cell 2:2:8 | 2 - Second oops line 2 + Second line @@ -366,7 +366,7 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭▸ bar.ipynb:2:8 + ╭▸ bar.ipynb:cell 2:2:8 │ 2 - Second oops line 2 + Second line @@ -412,7 +412,7 @@ error: oops | ^^^^ oops | help: remove the entry - --> bar.ipynb:2:8 + --> bar.ipynb:cell 2:2:8 | 2 - Second oops line 2 + Second line @@ -433,7 +433,7 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭▸ bar.ipynb:2:8 + ╭▸ bar.ipynb:cell 2:2:8 │ 2 - Second oops line 2 + Second line From 97065696c0e4dbe6ff43a89c73f923bf60ee231a Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 31 Jul 2026 14:14:58 -0500 Subject: [PATCH 275/390] refactor(annotate): Allow optional paths in Origin --- crates/ruff_annotate_snippets/src/renderer/render.rs | 9 ++++++--- crates/ruff_annotate_snippets/src/snippet.rs | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/ruff_annotate_snippets/src/renderer/render.rs b/crates/ruff_annotate_snippets/src/renderer/render.rs index 62c7301918..70640d8689 100644 --- a/crates/ruff_annotate_snippets/src/renderer/render.rs +++ b/crates/ruff_annotate_snippets/src/renderer/render.rs @@ -181,7 +181,7 @@ pub(crate) fn render(renderer: &Renderer, groups: Report<'_>) -> String { PreProcessedElement::Origin(origin) => { let buffer_msg_line_offset = buffer.num_lines(); - let is_primary = primary_path == Some(&origin.path) && !seen_primary; + let is_primary = primary_path == origin.path.as_ref() && !seen_primary; seen_primary |= is_primary; render_origin( renderer, @@ -535,7 +535,10 @@ fn render_origin( fn format_origin(origin: &Origin<'_>, anonymized_line_numbers: bool) -> String { use core::fmt::Write as _; - let mut buffer = origin.path.as_ref().to_owned(); + let mut buffer = String::new(); + if let Some(path) = &origin.path { + write!(&mut buffer, "{path}").unwrap(); + } if let Some(cell_index) = origin.cell_index { write!(&mut buffer, ":cell {cell_index}").unwrap(); } @@ -2806,7 +2809,7 @@ fn pre_process<'a>( } Element::Origin(origin) => { if primary_path.is_none() { - primary_path = Some(Some(&origin.path)); + primary_path = Some(origin.path.as_ref()); } elements.push(PreProcessedElement::Origin(origin)); } diff --git a/crates/ruff_annotate_snippets/src/snippet.rs b/crates/ruff_annotate_snippets/src/snippet.rs index b70a9364f4..d10553453c 100644 --- a/crates/ruff_annotate_snippets/src/snippet.rs +++ b/crates/ruff_annotate_snippets/src/snippet.rs @@ -523,7 +523,7 @@ impl<'a> Patch<'a> { /// ``` #[derive(Clone, Debug)] pub struct Origin<'a> { - pub(crate) path: Cow<'a, str>, + pub(crate) path: Option>, /// The optional cell index in a Jupyter notebook, used for reporting source locations along /// with the ranges on `annotations`. pub(crate) cell_index: Option, @@ -541,7 +541,7 @@ impl<'a> Origin<'a> { /// pub fn path(path: impl Into>) -> Self { Self { - path: path.into(), + path: Some(path.into()), cell_index: None, line: None, char_column: None, From 54adc11774bc32f10b3c78b76ba7ae1b1ec6edc9 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 31 Jul 2026 16:19:30 -0500 Subject: [PATCH 276/390] fix(annotate): Show origin when cell changed from last suggestion --- crates/ruff_annotate_snippets/src/renderer/render.rs | 7 ++++--- crates/ruff_annotate_snippets/tests/ruff.rs | 10 ++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/ruff_annotate_snippets/src/renderer/render.rs b/crates/ruff_annotate_snippets/src/renderer/render.rs index 70640d8689..c98b848083 100644 --- a/crates/ruff_annotate_snippets/src/renderer/render.rs +++ b/crates/ruff_annotate_snippets/src/renderer/render.rs @@ -155,8 +155,8 @@ pub(crate) fn render(renderer: &Renderer, groups: Report<'_>) -> String { spliced_lines, display_suggestion, )) => { - let matches_previous_suggestion = - last_suggestion_path == Some(suggestion.path.as_ref()); + let matches_previous_suggestion = last_suggestion_path + == Some((Some(suggestion.path.as_ref()), suggestion.cell_index)); emit_suggestion_default( renderer, &mut buffer, @@ -173,7 +173,8 @@ pub(crate) fn render(renderer: &Renderer, groups: Report<'_>) -> String { ); if matches!(peek, Some(PreProcessedElement::Suggestion(_))) { - last_suggestion_path = Some(suggestion.path.as_ref()); + last_suggestion_path = + Some((Some(suggestion.path.as_ref()), suggestion.cell_index)); } else { last_suggestion_path = None; } diff --git a/crates/ruff_annotate_snippets/tests/ruff.rs b/crates/ruff_annotate_snippets/tests/ruff.rs index 16caa4b1d7..22f56214ac 100644 --- a/crates/ruff_annotate_snippets/tests/ruff.rs +++ b/crates/ruff_annotate_snippets/tests/ruff.rs @@ -224,6 +224,7 @@ help: remove the entry 2 - Second oops line 2 + Second line | + | 2 - Second oops line 2 + Second oops | @@ -243,7 +244,8 @@ help: remove the entry ╭╴ 2 - Second oops line 2 + Second line - ├╴ + │ + ╭╴ 2 - Second oops line 2 + Second oops ╰╴ @@ -417,6 +419,8 @@ help: remove the entry 2 - Second oops line 2 + Second line | + --> bar.ipynb:cell 3:2:12 + | 2 - Second oops line 2 + Second oops | @@ -437,7 +441,9 @@ help: remove the entry │ 2 - Second oops line 2 + Second line - ├╴ + │ + ├▸ bar.ipynb:cell 3:2:12 + │ 2 - Second oops line 2 + Second oops ╰╴ From 03989bfe060d0ed48dd9ea7d0c99a35c12b9c323 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 31 Jul 2026 15:47:39 -0500 Subject: [PATCH 277/390] fix(annotate): Always show cell index --- .../src/renderer/render.rs | 18 +++++++++++------ crates/ruff_annotate_snippets/tests/ruff.rs | 20 ++++++++++++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/crates/ruff_annotate_snippets/src/renderer/render.rs b/crates/ruff_annotate_snippets/src/renderer/render.rs index c98b848083..4010620641 100644 --- a/crates/ruff_annotate_snippets/src/renderer/render.rs +++ b/crates/ruff_annotate_snippets/src/renderer/render.rs @@ -541,7 +541,10 @@ fn format_origin(origin: &Origin<'_>, anonymized_line_numbers: bool) -> String { write!(&mut buffer, "{path}").unwrap(); } if let Some(cell_index) = origin.cell_index { - write!(&mut buffer, ":cell {cell_index}").unwrap(); + if !buffer.is_empty() { + write!(&mut buffer, ":").unwrap(); + } + write!(&mut buffer, "cell {cell_index}").unwrap(); } if let Some(line) = origin.line { if anonymized_line_numbers { @@ -1528,8 +1531,8 @@ fn emit_suggestion_default( let (complete, parts, highlights, replaced_highlights) = spliced_lines; let is_multiline = complete.lines().count() > 1; - if suggestion.path.as_ref() != primary_path - && let Some(path) = suggestion.path.as_ref() + let secondary_path = suggestion.path.as_ref() != primary_path; + if ((secondary_path && suggestion.path.as_ref().is_some()) || suggestion.cell_index.is_some()) && !matches_previous_suggestion { let (loc, _) = sm.span_to_locations(parts[0].span.clone()); @@ -1540,9 +1543,12 @@ fn emit_suggestion_default( } let arrow = renderer.decor_style.file_start(is_first, false); buffer.append(row_num - 1, arrow, ElementStyle::LineNumber); - let mut origin = Origin::path(path.as_ref()).cell_index(suggestion.cell_index); - origin.line = Some(loc.line); - origin.char_column = Some(loc.char + 1); + let origin = Origin { + path: suggestion.path.as_ref().map(|p| Cow::Borrowed(p.as_ref())), + cell_index: suggestion.cell_index, + line: Some(loc.line), + char_column: Some(loc.char + 1), + }; let message = format_origin(&origin, renderer.anonymized_line_numbers); buffer.append(row_num - 1, &message, ElementStyle::LineAndColumn); diff --git a/crates/ruff_annotate_snippets/tests/ruff.rs b/crates/ruff_annotate_snippets/tests/ruff.rs index 22f56214ac..167ee3c3f3 100644 --- a/crates/ruff_annotate_snippets/tests/ruff.rs +++ b/crates/ruff_annotate_snippets/tests/ruff.rs @@ -91,6 +91,7 @@ error: oops | ^^^^ oops | help: remove the entry + --> cell 1:2:8 | 2 - Second oops line 2 + Second line @@ -110,7 +111,8 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭╴ + ╭▸ cell 1:2:8 + │ 2 - Second oops line 2 + Second line ├╴ @@ -155,6 +157,7 @@ error: oops | ^^^^ oops | help: remove the entry + --> foo.ipynb:cell 1:2:8 | 2 - Second oops line 2 + Second line @@ -175,7 +178,8 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭╴ + ╭▸ foo.ipynb:cell 1:2:8 + │ 2 - Second oops line 2 + Second line ├╴ @@ -220,10 +224,12 @@ error: oops | ^^^^ oops | help: remove the entry + --> foo.ipynb:cell 1:2:8 | 2 - Second oops line 2 + Second line | + --> foo.ipynb:cell 2:2:12 | 2 - Second oops line 2 + Second oops @@ -241,11 +247,13 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭╴ + ╭▸ foo.ipynb:cell 1:2:8 + │ 2 - Second oops line 2 + Second line │ - ╭╴ + ├▸ foo.ipynb:cell 2:2:12 + │ 2 - Second oops line 2 + Second oops ╰╴ @@ -283,6 +291,7 @@ error: oops | ^^^^ oops | help: remove the entry + --> cell 2:2:8 | 2 - Second oops line 2 + Second line @@ -302,7 +311,8 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭╴ + ╭▸ cell 2:2:8 + │ 2 - Second oops line 2 + Second line ├╴ From 68d556d0630ecb8367cf041c3e857aa917799c43 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 31 Jul 2026 16:28:31 -0500 Subject: [PATCH 278/390] fix(annotate): Hide the path when just the cell changes --- crates/ruff_annotate_snippets/src/renderer/render.rs | 6 +++++- crates/ruff_annotate_snippets/tests/ruff.rs | 12 ++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/ruff_annotate_snippets/src/renderer/render.rs b/crates/ruff_annotate_snippets/src/renderer/render.rs index 4010620641..dd0a199bed 100644 --- a/crates/ruff_annotate_snippets/src/renderer/render.rs +++ b/crates/ruff_annotate_snippets/src/renderer/render.rs @@ -1544,7 +1544,11 @@ fn emit_suggestion_default( let arrow = renderer.decor_style.file_start(is_first, false); buffer.append(row_num - 1, arrow, ElementStyle::LineNumber); let origin = Origin { - path: suggestion.path.as_ref().map(|p| Cow::Borrowed(p.as_ref())), + path: suggestion + .path + .as_ref() + .filter(|_| secondary_path) + .map(|p| Cow::Borrowed(p.as_ref())), cell_index: suggestion.cell_index, line: Some(loc.line), char_column: Some(loc.char + 1), diff --git a/crates/ruff_annotate_snippets/tests/ruff.rs b/crates/ruff_annotate_snippets/tests/ruff.rs index 167ee3c3f3..0ff112ac2f 100644 --- a/crates/ruff_annotate_snippets/tests/ruff.rs +++ b/crates/ruff_annotate_snippets/tests/ruff.rs @@ -157,7 +157,7 @@ error: oops | ^^^^ oops | help: remove the entry - --> foo.ipynb:cell 1:2:8 + --> cell 1:2:8 | 2 - Second oops line 2 + Second line @@ -178,7 +178,7 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭▸ foo.ipynb:cell 1:2:8 + ╭▸ cell 1:2:8 │ 2 - Second oops line 2 + Second line @@ -224,12 +224,12 @@ error: oops | ^^^^ oops | help: remove the entry - --> foo.ipynb:cell 1:2:8 + --> cell 1:2:8 | 2 - Second oops line 2 + Second line | - --> foo.ipynb:cell 2:2:12 + --> cell 2:2:12 | 2 - Second oops line 2 + Second oops @@ -247,12 +247,12 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭▸ foo.ipynb:cell 1:2:8 + ╭▸ cell 1:2:8 │ 2 - Second oops line 2 + Second line │ - ├▸ foo.ipynb:cell 2:2:12 + ├▸ cell 2:2:12 │ 2 - Second oops line 2 + Second oops From 2e64753372956af0578c56cfb0e5b655bd21e54a Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 31 Jul 2026 21:18:55 -0500 Subject: [PATCH 279/390] fix(annotate): Use ruff's patch origin marker This is a hack to reduce differences --- .../src/renderer/render.rs | 4 +-- ...ighlight_first_line_tab_371.ascii.term.svg | 2 +- .../ruff_annotate_snippets/tests/formatter.rs | 4 +-- crates/ruff_annotate_snippets/tests/ruff.rs | 32 +++++++++---------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/ruff_annotate_snippets/src/renderer/render.rs b/crates/ruff_annotate_snippets/src/renderer/render.rs index dd0a199bed..877e94185d 100644 --- a/crates/ruff_annotate_snippets/src/renderer/render.rs +++ b/crates/ruff_annotate_snippets/src/renderer/render.rs @@ -1523,7 +1523,7 @@ fn emit_suggestion_default( sm: &SourceMap<'_>, primary_path: Option<&Cow<'_, str>>, matches_previous_suggestion: bool, - is_first: bool, + _is_first: bool, is_cont: bool, ) { let buffer_offset = buffer.num_lines(); @@ -1541,7 +1541,7 @@ fn emit_suggestion_default( for _ in 0..max_line_num_len { buffer.append(row_num - 1, " ", ElementStyle::NoStyle); } - let arrow = renderer.decor_style.file_start(is_first, false); + let arrow = renderer.decor_style.secondary_file_start(); buffer.append(row_num - 1, arrow, ElementStyle::LineNumber); let origin = Origin { path: suggestion diff --git a/crates/ruff_annotate_snippets/tests/color/highlight_first_line_tab_371.ascii.term.svg b/crates/ruff_annotate_snippets/tests/color/highlight_first_line_tab_371.ascii.term.svg index 67f544a9a8..a06799a9c9 100644 --- a/crates/ruff_annotate_snippets/tests/color/highlight_first_line_tab_371.ascii.term.svg +++ b/crates/ruff_annotate_snippets/tests/color/highlight_first_line_tab_371.ascii.term.svg @@ -22,7 +22,7 @@ error: <sample error message> - --> <sample path>:6:18 + ::: <sample path>:6:18 | diff --git a/crates/ruff_annotate_snippets/tests/formatter.rs b/crates/ruff_annotate_snippets/tests/formatter.rs index d3afafd6b3..c605d35b23 100644 --- a/crates/ruff_annotate_snippets/tests/formatter.rs +++ b/crates/ruff_annotate_snippets/tests/formatter.rs @@ -5281,7 +5281,7 @@ error[E0624]: method `five_years` is private | ---------- private method defined here | help: consider making `bar` public - --> other.rs:1:1 + ::: other.rs:1:1 | 1 | pub fn bar(&self) { | +++ @@ -5302,7 +5302,7 @@ error[E0624]: method `five_years` is private │ ────────── private method defined here ╰╴ help: consider making `bar` public - ╭▸ other.rs:1:1 + ⸬ other.rs:1:1 │ 1 │ pub fn bar(&self) { ╰╴+++ diff --git a/crates/ruff_annotate_snippets/tests/ruff.rs b/crates/ruff_annotate_snippets/tests/ruff.rs index 0ff112ac2f..96e6f08673 100644 --- a/crates/ruff_annotate_snippets/tests/ruff.rs +++ b/crates/ruff_annotate_snippets/tests/ruff.rs @@ -91,7 +91,7 @@ error: oops | ^^^^ oops | help: remove the entry - --> cell 1:2:8 + ::: cell 1:2:8 | 2 - Second oops line 2 + Second line @@ -111,7 +111,7 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭▸ cell 1:2:8 + ⸬ cell 1:2:8 │ 2 - Second oops line 2 + Second line @@ -157,7 +157,7 @@ error: oops | ^^^^ oops | help: remove the entry - --> cell 1:2:8 + ::: cell 1:2:8 | 2 - Second oops line 2 + Second line @@ -178,7 +178,7 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭▸ cell 1:2:8 + ⸬ cell 1:2:8 │ 2 - Second oops line 2 + Second line @@ -224,12 +224,12 @@ error: oops | ^^^^ oops | help: remove the entry - --> cell 1:2:8 + ::: cell 1:2:8 | 2 - Second oops line 2 + Second line | - --> cell 2:2:12 + ::: cell 2:2:12 | 2 - Second oops line 2 + Second oops @@ -247,12 +247,12 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭▸ cell 1:2:8 + ⸬ cell 1:2:8 │ 2 - Second oops line 2 + Second line │ - ├▸ cell 2:2:12 + ⸬ cell 2:2:12 │ 2 - Second oops line 2 + Second oops @@ -291,7 +291,7 @@ error: oops | ^^^^ oops | help: remove the entry - --> cell 2:2:8 + ::: cell 2:2:8 | 2 - Second oops line 2 + Second line @@ -311,7 +311,7 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭▸ cell 2:2:8 + ⸬ cell 2:2:8 │ 2 - Second oops line 2 + Second line @@ -357,7 +357,7 @@ error: oops | ^^^^ oops | help: remove the entry - --> bar.ipynb:cell 2:2:8 + ::: bar.ipynb:cell 2:2:8 | 2 - Second oops line 2 + Second line @@ -378,7 +378,7 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭▸ bar.ipynb:cell 2:2:8 + ⸬ bar.ipynb:cell 2:2:8 │ 2 - Second oops line 2 + Second line @@ -424,12 +424,12 @@ error: oops | ^^^^ oops | help: remove the entry - --> bar.ipynb:cell 2:2:8 + ::: bar.ipynb:cell 2:2:8 | 2 - Second oops line 2 + Second line | - --> bar.ipynb:cell 3:2:12 + ::: bar.ipynb:cell 3:2:12 | 2 - Second oops line 2 + Second oops @@ -447,12 +447,12 @@ error: oops │ ━━━━ oops ╰╴ help: remove the entry - ╭▸ bar.ipynb:cell 2:2:8 + ⸬ bar.ipynb:cell 2:2:8 │ 2 - Second oops line 2 + Second line │ - ├▸ bar.ipynb:cell 3:2:12 + ⸬ bar.ipynb:cell 3:2:12 │ 2 - Second oops line 2 + Second oops From f5f7e142734bd759eab34146c82e178da2fb34a7 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 3 Aug 2026 16:27:29 -0500 Subject: [PATCH 280/390] test(annotate): Capture confusing suggestion --- crates/ruff_annotate_snippets/tests/ruff.rs | 80 +++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/crates/ruff_annotate_snippets/tests/ruff.rs b/crates/ruff_annotate_snippets/tests/ruff.rs index 96e6f08673..965635bf1a 100644 --- a/crates/ruff_annotate_snippets/tests/ruff.rs +++ b/crates/ruff_annotate_snippets/tests/ruff.rs @@ -461,3 +461,83 @@ help: remove the entry let renderer = renderer.decor_style(DecorStyle::Unicode); assert_data_eq!(renderer.render(input), expected_unicode); } + +#[test] +fn insertion_with_trailing_whitespace() { + let source = "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\n"; + let input = &[ + Level::ERROR + .primary_title("main diagnostic message") + .id("test-diagnostic") + .element( + Snippet::source(source) + .path("example.py") + .annotation(AnnotationKind::Primary.span(7..13)), + ), + Level::HELP + .secondary_title("Replace three lines") + .element(Snippet::source(source).patch(Patch::new(7..7, "fixed "))), + ]; + let expected_ascii = str![[r#" +error[test-diagnostic]: main diagnostic message + --> example.py:2:1 + | +2 | line 2 + | ^^^^^^ + | +help: Replace three lines + | +2 | fixed line 2 + | +++++ +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected_ascii); + + let expected_unicode = str![[r#" +error[test-diagnostic]: main diagnostic message + ╭▸ example.py:2:1 + │ +2 │ line 2 + │ ━━━━━━ + ╰╴ +help: Replace three lines + ╭╴ +2 │ fixed line 2 + ╰╴+++++ +"#]]; + let renderer = renderer.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer.render(input), expected_unicode); +} + +#[test] +#[should_panic = "index out of bounds: the len is 11 but the index is 11"] +fn multiple_insertions() { + let source = "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\n"; + let input = &[ + Level::ERROR + .primary_title("main diagnostic message") + .id("test-diagnostic") + .element( + Snippet::source(source) + .path("example.py") + .annotation(AnnotationKind::Primary.span(7..13)), + ), + Level::HELP.secondary_title("Replace three lines").element( + Snippet::source(source) + .patch(Patch::new(7..7, "fixed ")) + .patch(Patch::new(42..42, "fixed ")) + .patch(Patch::new(87..87, "fixed ")), + ), + ]; + let expected_ascii = str![[r#" +"#]]; + + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input), expected_ascii); + + let expected_unicode = str![[r#" +"#]]; + let renderer = renderer.decor_style(DecorStyle::Unicode); + assert_data_eq!(renderer.render(input), expected_unicode); +} From bb868838d75876c3b016d8cf1687d33eae80daa6 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 4 Aug 2026 13:16:03 -0500 Subject: [PATCH 281/390] perf(annotate): Remove redundant location lookup --- crates/ruff_annotate_snippets/src/renderer/source_map.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ruff_annotate_snippets/src/renderer/source_map.rs b/crates/ruff_annotate_snippets/src/renderer/source_map.rs index e0d9f21609..e84273afba 100644 --- a/crates/ruff_annotate_snippets/src/renderer/source_map.rs +++ b/crates/ruff_annotate_snippets/src/renderer/source_map.rs @@ -457,6 +457,7 @@ impl<'a> SourceMap<'a> { }; let lines = self.span_to_lines(lo..hi); + let (bounding_lo, bounding_hi) = self.span_to_locations(lo..hi); let mut highlights = vec![]; // To build up the result, we do this for each span: @@ -468,7 +469,7 @@ impl<'a> SourceMap<'a> { // - splice in the span substitution // // Finally push the trailing line segment of the last span - let (mut prev_hi, _) = self.span_to_locations(lo..hi); + let mut prev_hi = bounding_lo; prev_hi.char = 0; let mut prev_line = lines.first().map(|line| line.line); let mut buf = String::new(); @@ -565,7 +566,6 @@ impl<'a> SourceMap<'a> { buf.pop(); } - let (bounding_lo, bounding_hi) = self.span_to_locations(lo..hi); let line_count = bounding_hi.line.saturating_sub(bounding_lo.line) + 1; let mut replaced_highlights: Vec> = vec![Vec::new(); line_count]; for part in &trimmed_patches { From 9c53b5de3aae4b1832eb7ee137647dda2466f888 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 4 Aug 2026 13:18:41 -0500 Subject: [PATCH 282/390] fix(annotate): Remove panic in suggestions This was introduced in 958efb1ff40d8a433bde48ce84b9e35e99332350 when dealing with newline handling: a span's end after a newline would be moved to befopre the newline. --- .../src/renderer/source_map.rs | 19 +++++++--- crates/ruff_annotate_snippets/tests/ruff.rs | 37 ++++++++++++++++++- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/crates/ruff_annotate_snippets/src/renderer/source_map.rs b/crates/ruff_annotate_snippets/src/renderer/source_map.rs index e84273afba..2c40ee1a92 100644 --- a/crates/ruff_annotate_snippets/src/renderer/source_map.rs +++ b/crates/ruff_annotate_snippets/src/renderer/source_map.rs @@ -449,15 +449,24 @@ impl<'a> SourceMap<'a> { // Find the bounding span. let (lo, hi) = if fold { - let lo = patches.iter().map(|p| p.span.start).min()?; - let hi = patches.iter().map(|p| p.span.end).max()?; + let lo = patches + .iter() + .map(|p| p.span.clone()) + .min_by_key(|s| s.start)?; + let hi = patches + .iter() + .map(|p| p.span.clone()) + .max_by_key(|s| s.end)?; (lo, hi) } else { - (0, source_len) + let lo = 0..source_len; + let hi = 0..source_len; + (lo, hi) }; - let lines = self.span_to_lines(lo..hi); - let (bounding_lo, bounding_hi) = self.span_to_locations(lo..hi); + let lines = self.span_to_lines(lo.start..hi.end); + let (bounding_lo, _) = self.span_to_locations(lo); + let (_, bounding_hi) = self.span_to_locations(hi); let mut highlights = vec![]; // To build up the result, we do this for each span: diff --git a/crates/ruff_annotate_snippets/tests/ruff.rs b/crates/ruff_annotate_snippets/tests/ruff.rs index 965635bf1a..791a2ae5fb 100644 --- a/crates/ruff_annotate_snippets/tests/ruff.rs +++ b/crates/ruff_annotate_snippets/tests/ruff.rs @@ -511,7 +511,6 @@ help: Replace three lines } #[test] -#[should_panic = "index out of bounds: the len is 11 but the index is 11"] fn multiple_insertions() { let source = "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\n"; let input = &[ @@ -531,12 +530,48 @@ fn multiple_insertions() { ), ]; let expected_ascii = str![[r#" +error[test-diagnostic]: main diagnostic message + --> example.py:2:1 + | + 2 | line 2 + | ^^^^^^ + | +help: Replace three lines + | + 2 ~ fixed line 2 + 3 | line 3 +... + 6 | line 6 + 7 ~ fixed line 7 + 8 | line 8 +... +12 | line 12 +13 ~ fixed line 13 + | "#]]; let renderer = Renderer::plain(); assert_data_eq!(renderer.render(input), expected_ascii); let expected_unicode = str![[r#" +error[test-diagnostic]: main diagnostic message + ╭▸ example.py:2:1 + │ + 2 │ line 2 + │ ━━━━━━ + ╰╴ +help: Replace three lines + ╭╴ + 2 ± fixed line 2 + 3 │ line 3 + … + 6 │ line 6 + 7 ± fixed line 7 + 8 │ line 8 + … +12 │ line 12 +13 ± fixed line 13 + ╰╴ "#]]; let renderer = renderer.decor_style(DecorStyle::Unicode); assert_data_eq!(renderer.render(input), expected_unicode); From a6637228518acc9bef52516a1cf71301d711e720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9r=C3=A8?= Date: Wed, 5 Aug 2026 10:05:38 -0700 Subject: [PATCH 283/390] [ty]Tokenize reStructuredText prefix roles (#27464) --- .../ty_ide/src/docstring/document/google.rs | 48 +---- .../ty_ide/src/docstring/document/syntax.rs | 194 +++++++++++++----- 2 files changed, 159 insertions(+), 83 deletions(-) diff --git a/crates/ty_ide/src/docstring/document/google.rs b/crates/ty_ide/src/docstring/document/google.rs index e3d56a0fcf..4cac1cfa93 100644 --- a/crates/ty_ide/src/docstring/document/google.rs +++ b/crates/ty_ide/src/docstring/document/google.rs @@ -32,12 +32,13 @@ use ruff_python_stdlib::identifiers::is_identifier; use ruff_python_trivia::Cursor; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::{Ranged, TextRange, TextSize}; use super::preformatted::PreformattedBlockScanner; use super::syntax::{ - ParsedLine, consume_quoted_string, container_block_end, indentation, is_dotted_identifier, - parsed_lines, split_once_at_top_level_colon, split_trailing_parenthetical, + InlineMarkupScanner, InlineMarkupToken, ParsedLine, consume_quoted_string, container_block_end, + indentation, is_dotted_identifier, parsed_lines, split_once_at_top_level_colon, + split_trailing_parenthetical, }; use super::{DescriptionBuilder, HeaderKind, SectionKind}; use crate::FxIndexMap; @@ -847,42 +848,14 @@ fn split_once_at_field_delimiter(line: &str) -> Option<(&str, &str)> { /// :exc:`ValueError` /// ``` fn consume_rest_prefix_role(cursor: &mut Cursor<'_>) -> bool { - let mut role = cursor.clone(); - - // First, require the candidate delimiter to be the opening colon of a role. - if !role.eat_char(':') { + let Some(InlineMarkupToken::RestPrefixRole { span, .. }) = + InlineMarkupScanner::new(cursor.as_str()).next() + else { return false; - } - - // Role names start with a Unicode alphanumeric run. Rejecting punctuation here preserves the - // first colon in `value::class:` as the field delimiter. - if !role.eat_if(char::is_alphanumeric) { - return false; - } - - // Next, scan the rest of the role name until its closing colon and the opening content - // backtick. - loop { - role.eat_while(char::is_alphanumeric); - if role.eat_char2(':', '`') { - break; - } - - // `-._+:` separators are allowed, but only internally to alphanumeric characters. - if !role.eat_if(|character| matches!(character, '-' | '.' | '_' | '+' | ':')) - || !role.eat_if(char::is_alphanumeric) - { - return false; - } - } - - // Finally, skip the role content so delimiter scanning resumes after its closing backtick. - role.eat_while(|character| character != '`'); - if !role.eat_char('`') { - return false; - } + }; - *cursor = role; + // Resume delimiter scanning after the closing backtick in e.g., `` :exc:`ValueError` ``. + cursor.skip_bytes(span.end().to_usize()); true } @@ -1614,6 +1587,7 @@ Returns: for (line, description) in [ ("value:foo..bar:`X`", "foo..bar:`X`"), ("value:foo-:`X`", "foo-:`X`"), + ("value:class:``X``", "class:``X``"), ] { assert_eq!( split_once_at_field_delimiter(line), diff --git a/crates/ty_ide/src/docstring/document/syntax.rs b/crates/ty_ide/src/docstring/document/syntax.rs index e23fbc9e39..2958bb27d7 100644 --- a/crates/ty_ide/src/docstring/document/syntax.rs +++ b/crates/ty_ide/src/docstring/document/syntax.rs @@ -73,33 +73,32 @@ pub(crate) fn is_wrapped_in_markdown_code_span(text: &str) -> bool { /// Emits non-overlapping tokens that completely span the source text. /// -/// Currently supports `Code` for complete, unescaped backtick-delimited segments and `Text` -/// for everything else. +/// Supports complete backtick-delimited segments, reStructuredText prefix roles, and plain text. /// /// For example: /// /// ```text -/// InlineMarkupScanner::new("before `code` after") -/// => Text("before "), Code("code"), Text(" after") +/// InlineMarkupScanner::new("before :class:`Value` and `code`") +/// => Text("before "), RestPrefixRole("class", "Value"), Text(" and "), Code("code") /// ``` -struct InlineMarkupScanner<'a> { +pub(crate) struct InlineMarkupScanner<'a> { /// The scanner used to find complete code spans. scanner: BacktickScanner<'a>, /// The end of the last token returned to the caller. last_token_end: TextSize, - /// A span saved while its preceding text is returned first. - pending_span: Option>, + /// A token saved while its preceding text is returned first. + pending_token: Option>, } impl<'a> InlineMarkupScanner<'a> { - /// Creates a lossless iterator over plain text and complete backtick-delimited code spans. + /// Creates a lossless iterator over plain text and complete inline markup. /// /// Escaped or unmatched backticks remain part of an [`InlineMarkupToken::Text`] token. - fn new(source: &'a str) -> Self { + pub(crate) fn new(source: &'a str) -> Self { Self { scanner: BacktickScanner::new(source), last_token_end: TextSize::ZERO, - pending_span: None, + pending_token: None, } } @@ -115,43 +114,50 @@ impl<'a> Iterator for InlineMarkupScanner<'a> { type Item = InlineMarkupToken<'a>; fn next(&mut self) -> Option { - let span = if let Some(span) = self.pending_span.take() { - // Emit the span saved while returning its preceding text on the previous call. - span - } else { - loop { - // Without another backtick run, the remaining source is all plain text. - let Some(opening) = self.scanner.next() else { - return self.take_remaining_text(); - }; - - // Escaped runs are literal source text, so continue looking for the next possible - // opening without emitting a token boundary. - if opening.is_escaped() { - continue; - } + if let Some(token) = self.pending_token.take() { + return Some(token); + } + + let span = loop { + // Without another backtick run, the remaining source is all plain text. + let Some(opening) = self.scanner.next() else { + return self.take_remaining_text(); + }; - // Without a closing delimiter, callers cannot treat the opening or any later runs as - // structured markup. Emit the remainder as one text token. - let Some(span) = self.scanner.eat_span(opening) else { - return self.take_remaining_text(); - }; - break span; + // Escaped runs are literal source text, so continue looking for the next possible + // opening without emitting a token boundary. + if opening.is_escaped() { + continue; } + + // Without a closing delimiter, callers cannot treat the opening or any later runs as + // structured markup. Emit the remainder as one text token. + let Some(span) = self.scanner.eat_span(opening) else { + return self.take_remaining_text(); + }; + break span; }; - if self.last_token_end < span.start() { - let preceding_text = TextRange::new(self.last_token_end, span.start()); - self.last_token_end = span.start(); - self.pending_span = Some(span); - return Some(InlineMarkupToken::Text( - &self.scanner.source[preceding_text], - )); - } + let preceding_range = TextRange::new(self.last_token_end, span.start()); + let preceding_text = &self.scanner.source[preceding_range]; + let (preceding_text, token) = if span.is_single() + && let Some((preceding_text, name)) = split_trailing_rest_prefix_role(preceding_text) + { + ( + preceding_text, + InlineMarkupToken::RestPrefixRole { name, span }, + ) + } else { + (preceding_text, InlineMarkupToken::Code(span)) + }; - debug_assert_eq!(self.last_token_end, span.start()); self.last_token_end = span.end(); - Some(InlineMarkupToken::Code(span)) + if preceding_text.is_empty() { + Some(token) + } else { + self.pending_token = Some(token); + Some(InlineMarkupToken::Text(preceding_text)) + } } } @@ -160,17 +166,62 @@ impl<'a> Iterator for InlineMarkupScanner<'a> { /// For example: /// /// ```text -/// source "before `code` after" -/// tokens Text("before "), Code("code"), Text(" after") +/// source "before :class:`Value` and `code`" +/// tokens Text("before "), RestPrefixRole("class", "Value"), Text(" and "), Code("code") /// ``` /// /// Escaped and unmatched backticks remain text. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum InlineMarkupToken<'a> { +pub(crate) enum InlineMarkupToken<'a> { /// Source text outside a complete, unescaped backtick span. Text(&'a str), /// A complete code span whose backtick delimiters have equal lengths. Code(BacktickSpan<'a>), + /// A reStructuredText prefix-role pattern and its single-backtick span. + RestPrefixRole { + /// The role name between the colons, for example `py:class`. + name: &'a str, + /// The complete single-backtick span following the role name. + span: BacktickSpan<'a>, + }, +} + +/// Splits a trailing reStructuredText prefix-role pattern from its preceding text. +/// +/// This deliberately recognizes role-shaped markup common in docstrings +/// (e.g. roles immediately after `=`, using a plural `s` immediately after a role) +/// without enforcing reStructuredText's surrounding inline-markup boundaries. +/// +/// For example, `"before :py:class:"` becomes `("before ", "py:class")`. +fn split_trailing_rest_prefix_role(text: &str) -> Option<(&str, &str)> { + let without_closing_colon = text.strip_suffix(':')?; + let mut role_start = without_closing_colon.len(); + let mut expects_alphanumeric = true; + + // Walk `before :py:class` backwards; separators must be between alphanumeric components. + for (index, character) in without_closing_colon.char_indices().rev() { + if character.is_alphanumeric() { + expects_alphanumeric = false; + role_start = index; + continue; + } + if !matches!(character, '-' | '.' | '_' | '+' | ':') { + break; + } + if expects_alphanumeric { + break; + } + + expects_alphanumeric = true; + role_start = index; + } + + if !expects_alphanumeric { + return None; + } + + let role_name = without_closing_colon[role_start..].strip_prefix(':')?; + Some((&without_closing_colon[..role_start], role_name)) } /// Source text delimited by ordered backtick runs of equal length. @@ -557,8 +608,8 @@ mod tests { assert_eq!( token_contents(source), vec![ - ("text", "é :class:"), - ("code", "~pkg.Widget"), + ("text", "é "), + ("rest role", "~pkg.Widget"), ("text", " or "), ("code", "literal`tick"), ("text", " β"), @@ -566,6 +617,33 @@ mod tests { ); } + #[test] + fn separates_rest_prefix_roles_from_preceding_text() { + assert_eq!( + token_contents("int-:class:`pkg.Model`"), + vec![("text", "int-"), ("rest role", "pkg.Model")] + ); + } + + #[test] + fn recognizes_common_role_uses_outside_rest_boundaries() { + assert_eq!( + token_contents("callable, default=:func:`sklearn.covariance.empirical_covariance`"), + vec![ + ("text", "callable, default="), + ("rest role", "sklearn.covariance.empirical_covariance"), + ] + ); + assert_eq!( + token_contents("sequence of :class:`numpy.array`s"), + vec![ + ("text", "sequence of "), + ("rest role", "numpy.array"), + ("text", "s"), + ] + ); + } + #[test] fn scans_code_at_source_boundaries() { assert_eq!( @@ -597,6 +675,29 @@ mod tests { } } + #[test] + fn recognizes_rest_prefix_roles() { + for (source, expected) in [ + (":class:`Value`", Some(("class", "Value"))), + (":py:class:`Value`", Some(("py:class", "Value"))), + ( + ":external+python:py:class:`Value`", + Some(("external+python:py:class", "Value")), + ), + (":étiquette:`valeur`", Some(("étiquette", "valeur"))), + (":foo..bar:`Value`", None), + ] { + let actual = InlineMarkupScanner::new(source).next().and_then(|token| { + if let InlineMarkupToken::RestPrefixRole { name, span } = token { + Some((name, span.content())) + } else { + None + } + }); + assert_eq!(actual, expected, "{source:?}"); + } + } + #[test] fn splits_after_nested_brackets() { assert_eq!( @@ -710,6 +811,7 @@ mod tests { .map(|token| match token { InlineMarkupToken::Text(text) => ("text", text), InlineMarkupToken::Code(code) => ("code", code.content()), + InlineMarkupToken::RestPrefixRole { span, .. } => ("rest role", span.content()), }) .collect() } From 59de3d2142d2250bc92de2beb61fdad0bc225937 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 30 Jul 2026 12:11:39 -0500 Subject: [PATCH 284/390] refactor(db): Delegate Diff::fmt to an inherent method Display will be going away, so this reduces noise later. --- crates/ruff_db/src/diagnostic/render/full.rs | 34 +++++++++++--------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 2f92c51293..9de8ab6298 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -113,21 +113,7 @@ impl<'a> Diff<'a> { }) } - fn write_gutter(&self, f: &mut std::fmt::Formatter, width: NonZeroUsize) -> std::fmt::Result { - writeln!( - f, - "{line} {separator}", - line = fmt_styled(Line { index: None, width }, self.stylesheet.line_no), - separator = fmt_styled("|", self.stylesheet.line_no), - ) - } -} - -/// Limit diffs to a narrow range around each fix rather than diffing the whole file. -const DIFF_CONTEXT_WINDOW: usize = 3; - -impl std::fmt::Display for Diff<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn write(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let source_code = self.diagnostic_source.as_source_code(); let source_text = source_code.text(); @@ -339,6 +325,24 @@ impl std::fmt::Display for Diff<'_> { Ok(()) } + + fn write_gutter(&self, f: &mut std::fmt::Formatter, width: NonZeroUsize) -> std::fmt::Result { + writeln!( + f, + "{line} {separator}", + line = fmt_styled(Line { index: None, width }, self.stylesheet.line_no), + separator = fmt_styled("|", self.stylesheet.line_no), + ) + } +} + +/// Limit diffs to a narrow range around each fix rather than diffing the whole file. +const DIFF_CONTEXT_WINDOW: usize = 3; + +impl std::fmt::Display for Diff<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.write(f) + } } struct Line { From 3a04e259c3da89c2a9c8645f48c0913c5d7aecc8 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 29 Jul 2026 10:22:17 -0500 Subject: [PATCH 285/390] refactor(db): Extract applicability note --- crates/ruff_db/src/diagnostic/render/full.rs | 24 ++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 9de8ab6298..c440b10675 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -295,6 +295,21 @@ impl<'a> Diff<'a> { self.write_gutter(f, digit_with)?; } + self.write_applicability_note(f)?; + + Ok(()) + } + + fn write_gutter(&self, f: &mut std::fmt::Formatter, width: NonZeroUsize) -> std::fmt::Result { + writeln!( + f, + "{line} {separator}", + line = fmt_styled(Line { index: None, width }, self.stylesheet.line_no), + separator = fmt_styled("|", self.stylesheet.line_no), + ) + } + + fn write_applicability_note(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.fix.applicability() { Applicability::Safe => {} Applicability::Unsafe => { @@ -325,15 +340,6 @@ impl<'a> Diff<'a> { Ok(()) } - - fn write_gutter(&self, f: &mut std::fmt::Formatter, width: NonZeroUsize) -> std::fmt::Result { - writeln!( - f, - "{line} {separator}", - line = fmt_styled(Line { index: None, width }, self.stylesheet.line_no), - separator = fmt_styled("|", self.stylesheet.line_no), - ) - } } /// Limit diffs to a narrow range around each fix rather than diffing the whole file. From 65e6104244ae79942bcb17fe61e033754bf99ef3 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 30 Jul 2026 12:19:46 -0500 Subject: [PATCH 286/390] refactor(db): Decouple applicability note from Diff Part 1 --- crates/ruff_db/src/diagnostic/render/full.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index c440b10675..10bac6d423 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -295,7 +295,7 @@ impl<'a> Diff<'a> { self.write_gutter(f, digit_with)?; } - self.write_applicability_note(f)?; + Self::write_applicability_note(self.fix, self.stylesheet, f)?; Ok(()) } @@ -309,17 +309,21 @@ impl<'a> Diff<'a> { ) } - fn write_applicability_note(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.fix.applicability() { + fn write_applicability_note( + fix: &Fix, + stylesheet: &DiagnosticStylesheet, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + match fix.applicability() { Applicability::Safe => {} Applicability::Unsafe => { writeln!( f, "{note}: {msg}", - note = fmt_styled("note", self.stylesheet.warning), + note = fmt_styled("note", stylesheet.warning), msg = fmt_styled( "This is an unsafe fix and may change runtime behavior", - self.stylesheet.emphasis + stylesheet.emphasis ) )?; } @@ -329,10 +333,10 @@ impl<'a> Diff<'a> { writeln!( f, "{note}: {msg}", - note = fmt_styled("note", self.stylesheet.error), + note = fmt_styled("note", stylesheet.error), msg = fmt_styled( "This is a display-only fix and is likely to be incorrect", - self.stylesheet.emphasis + stylesheet.emphasis ) )?; } From 9e18e0784f39d95c7ddc0b95d881fc6f4ffbfba9 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 30 Jul 2026 12:20:30 -0500 Subject: [PATCH 287/390] refactor(db): Decouple applicability note from Diff Part 2 --- crates/ruff_db/src/diagnostic/render/full.rs | 74 ++++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 10bac6d423..b6e94d1095 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -295,7 +295,7 @@ impl<'a> Diff<'a> { self.write_gutter(f, digit_with)?; } - Self::write_applicability_note(self.fix, self.stylesheet, f)?; + write_applicability_note(self.fix, self.stylesheet, f)?; Ok(()) } @@ -308,42 +308,6 @@ impl<'a> Diff<'a> { separator = fmt_styled("|", self.stylesheet.line_no), ) } - - fn write_applicability_note( - fix: &Fix, - stylesheet: &DiagnosticStylesheet, - f: &mut std::fmt::Formatter<'_>, - ) -> std::fmt::Result { - match fix.applicability() { - Applicability::Safe => {} - Applicability::Unsafe => { - writeln!( - f, - "{note}: {msg}", - note = fmt_styled("note", stylesheet.warning), - msg = fmt_styled( - "This is an unsafe fix and may change runtime behavior", - stylesheet.emphasis - ) - )?; - } - Applicability::DisplayOnly => { - // Note that this is still only used in tests. There's no `--display-only-fixes` - // analog to `--unsafe-fixes` for users to activate this or see the styling. - writeln!( - f, - "{note}: {msg}", - note = fmt_styled("note", stylesheet.error), - msg = fmt_styled( - "This is a display-only fix and is likely to be incorrect", - stylesheet.emphasis - ) - )?; - } - } - - Ok(()) - } } /// Limit diffs to a narrow range around each fix rather than diffing the whole file. @@ -387,6 +351,42 @@ fn show_nonprinting(s: &str) -> Cow<'_, str> { } } +fn write_applicability_note( + fix: &Fix, + stylesheet: &DiagnosticStylesheet, + f: &mut std::fmt::Formatter<'_>, +) -> std::fmt::Result { + match fix.applicability() { + Applicability::Safe => {} + Applicability::Unsafe => { + writeln!( + f, + "{note}: {msg}", + note = fmt_styled("note", stylesheet.warning), + msg = fmt_styled( + "This is an unsafe fix and may change runtime behavior", + stylesheet.emphasis + ) + )?; + } + Applicability::DisplayOnly => { + // Note that this is still only used in tests. There's no `--display-only-fixes` + // analog to `--unsafe-fixes` for users to activate this or see the styling. + writeln!( + f, + "{note}: {msg}", + note = fmt_styled("note", stylesheet.error), + msg = fmt_styled( + "This is a display-only fix and is likely to be incorrect", + stylesheet.emphasis + ) + )?; + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use ruff_diagnostics::{Applicability, Edit, Fix}; From ba5fdba51e2b5ea37aa022148ae2ffdab716014b Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 30 Jul 2026 12:22:59 -0500 Subject: [PATCH 288/390] refactor(db): Pull out common applicability code --- crates/ruff_db/src/diagnostic/render/full.rs | 43 ++++++++------------ 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index b6e94d1095..2158e207fc 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -356,35 +356,26 @@ fn write_applicability_note( stylesheet: &DiagnosticStylesheet, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { - match fix.applicability() { - Applicability::Safe => {} - Applicability::Unsafe => { - writeln!( - f, - "{note}: {msg}", - note = fmt_styled("note", stylesheet.warning), - msg = fmt_styled( - "This is an unsafe fix and may change runtime behavior", - stylesheet.emphasis - ) - )?; - } - Applicability::DisplayOnly => { + let (style, message) = match fix.applicability() { + Applicability::Safe => return Ok(()), + Applicability::Unsafe => ( + stylesheet.warning, + "This is an unsafe fix and may change runtime behavior", + ), + Applicability::DisplayOnly => ( // Note that this is still only used in tests. There's no `--display-only-fixes` // analog to `--unsafe-fixes` for users to activate this or see the styling. - writeln!( - f, - "{note}: {msg}", - note = fmt_styled("note", stylesheet.error), - msg = fmt_styled( - "This is a display-only fix and is likely to be incorrect", - stylesheet.emphasis - ) - )?; - } - } + stylesheet.error, + "This is a display-only fix and is likely to be incorrect", + ), + }; - Ok(()) + writeln!( + f, + "{note}: {message}", + note = fmt_styled("note", style), + message = fmt_styled(message, stylesheet.emphasis), + ) } #[cfg(test)] From 0ae4d9f5c7e1faa4a7b9f1cbff1ea2dd4a33d154 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 29 Jul 2026 12:41:58 -0500 Subject: [PATCH 289/390] refactor(db): Pull out applicability note call --- crates/ruff_db/src/diagnostic/render/full.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 2158e207fc..0693726a37 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -69,6 +69,7 @@ impl<'a> FullRenderer<'a> { Diff::from_diagnostic(diag, &stylesheet, self.resolver, self.config) { write!(f, "{diff}")?; + write_applicability_note(diff.fix, &stylesheet, f)?; } writeln!(f)?; @@ -295,8 +296,6 @@ impl<'a> Diff<'a> { self.write_gutter(f, digit_with)?; } - write_applicability_note(self.fix, self.stylesheet, f)?; - Ok(()) } From 3c09617a68dbc6b462fad4b3454a43e59b75e513 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 29 Jul 2026 12:48:16 -0500 Subject: [PATCH 290/390] refactor(db): Render Applicability through annotate-snippets --- crates/ruff_db/src/diagnostic/render/full.rs | 30 +++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 0693726a37..017ad43fd4 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -3,7 +3,9 @@ use std::num::NonZeroUsize; use similar::{ChangeTag, DiffOp, TextDiff}; -use annotate_snippets::Renderer as AnnotateRenderer; +use annotate_snippets::{ + Group as AnnotateGroup, Level as AnnotateLevel, Renderer as AnnotateRenderer, +}; use ruff_diagnostics::{Applicability, Fix}; use ruff_notebook::NotebookIndex; use ruff_source_file::OneIndexed; @@ -69,7 +71,9 @@ impl<'a> FullRenderer<'a> { Diff::from_diagnostic(diag, &stylesheet, self.resolver, self.config) { write!(f, "{diff}")?; - write_applicability_note(diff.fix, &stylesheet, f)?; + if let Some(applicability) = to_applicability_annotate(diff.fix) { + writeln!(f, "{}", renderer.render(&[applicability]))?; + } } writeln!(f)?; @@ -350,31 +354,23 @@ fn show_nonprinting(s: &str) -> Cow<'_, str> { } } -fn write_applicability_note( - fix: &Fix, - stylesheet: &DiagnosticStylesheet, - f: &mut std::fmt::Formatter<'_>, -) -> std::fmt::Result { - let (style, message) = match fix.applicability() { - Applicability::Safe => return Ok(()), +fn to_applicability_annotate(fix: &Fix) -> Option> { + let (level, message) = match fix.applicability() { + Applicability::Safe => return None, Applicability::Unsafe => ( - stylesheet.warning, + AnnotateLevel::WARNING, "This is an unsafe fix and may change runtime behavior", ), Applicability::DisplayOnly => ( // Note that this is still only used in tests. There's no `--display-only-fixes` // analog to `--unsafe-fixes` for users to activate this or see the styling. - stylesheet.error, + AnnotateLevel::ERROR, "This is a display-only fix and is likely to be incorrect", ), }; + let level = level.with_name("note"); - writeln!( - f, - "{note}: {message}", - note = fmt_styled("note", style), - message = fmt_styled(message, stylesheet.emphasis), - ) + Some(AnnotateGroup::with_title(level.primary_title(message))) } #[cfg(test)] From 3d1516fd86290c018d55f2c0b2a9f4b0e10159ad Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 5 Aug 2026 13:54:32 -0400 Subject: [PATCH 291/390] release: use self-repo syntax (#27514) Signed-off-by: William Woodruff --- .github/workflows/release.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b40853a71..7c27b4ef83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -117,7 +117,7 @@ jobs: needs: - plan if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }} - uses: ./.github/workflows/build-binaries.yml + uses: $/.github/workflows/build-binaries.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -127,7 +127,7 @@ jobs: - plan - release-gate if: ${{ always() && needs.plan.result == 'success' && (needs.release-gate.result == 'success' || needs.release-gate.result == 'skipped') && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run') }} - uses: ./.github/workflows/build-docker.yml + uses: $/.github/workflows/build-docker.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -141,7 +141,7 @@ jobs: needs: - plan if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }} - uses: ./.github/workflows/build-wasm.yml + uses: $/.github/workflows/build-wasm.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -248,7 +248,7 @@ jobs: - host - release-gate if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} - uses: ./.github/workflows/publish-pypi.yml + uses: $/.github/workflows/publish-pypi.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -263,7 +263,7 @@ jobs: - host - release-gate if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} - uses: ./.github/workflows/publish-wasm.yml + uses: $/.github/workflows/publish-wasm.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -281,7 +281,7 @@ jobs: - custom-publish-pypi # DIRTY: see #16989 - custom-publish-wasm # DIRTY: see #16989 if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} - uses: ./.github/workflows/publish-crates.yml + uses: $/.github/workflows/publish-crates.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -352,7 +352,7 @@ jobs: needs: - plan - announce - uses: ./.github/workflows/notify-dependents.yml + uses: $/.github/workflows/notify-dependents.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -361,7 +361,7 @@ jobs: needs: - plan - announce - uses: ./.github/workflows/publish-docs.yml + uses: $/.github/workflows/publish-docs.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -370,7 +370,7 @@ jobs: needs: - plan - announce - uses: ./.github/workflows/publish-playground.yml + uses: $/.github/workflows/publish-playground.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -379,7 +379,7 @@ jobs: needs: - plan - announce - uses: ./.github/workflows/publish-versions.yml + uses: $/.github/workflows/publish-versions.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit @@ -388,7 +388,7 @@ jobs: needs: - plan - announce - uses: ./.github/workflows/publish-mirror.yml + uses: $/.github/workflows/publish-mirror.yml with: plan: ${{ needs.plan.outputs.val }} secrets: inherit From d8e55078b415066a627eceef7bc47c073e67099d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Aug 2026 13:56:17 -0400 Subject: [PATCH 292/390] [ty] Target the mdtest binary when running individual mdtests (#27521) ## Summary Use `--test mdtest` when running an individual mdtest with Nextest, including when `MDTEST_TEST_FILTER` selects a specific case. This avoids compiling unrelated test binaries while preserving the existing test filters. In a representative incremental rebuild, explicitly selecting the mdtest binary reduced build time from 9.88 seconds to 7.28 seconds, a 26% reduction. --- AGENTS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ca99921b1c..c47ab3460a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,16 +30,16 @@ Run tests for a specific crate: CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic ``` -Run a single mdtest file. The path to the mdtest file should be relative to the `crates/ty_python_semantic/resources/mdtest` folder: +Run a single mdtest file. The path to the mdtest file should be relative to the `crates/ty_python_semantic/resources/mdtest` folder. Include `--test mdtest` to avoid building unrelated test binaries: ```sh -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic -- mdtest:: +CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic --test mdtest -- mdtest:: ``` To run a specific mdtest within a file, use a substring of the Markdown header text as `MDTEST_TEST_FILTER`. Only use this if it's necessary to isolate a single test case: ```sh -MDTEST_TEST_FILTER="" CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic -- mdtest:: +MDTEST_TEST_FILTER="" CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic --test mdtest -- mdtest:: ``` ### Fallback without nextest From 3eb4c71e53d093d1725f508d8cb78b55332e81b3 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 5 Aug 2026 11:01:00 -0700 Subject: [PATCH 293/390] [ty] Reject unhashable objects for Hashable protocols (#27441) ## Summary - Stop treating protocols that explicitly require `__hash__` as equivalent to `object`, because (even though it's not Liskov sound) subclasses disable hashing with `__hash__ = None`. - Reject unhashable classes, built-in containers, and mutable dataclasses for both standard-library `Hashable` spellings and equivalent user-defined protocols. - Preserve meaningful `Hashable | T` union members while keeping hashable `object()` sentinel values assignable to hash protocols. - Adjust pandas and static-frame benchmark diagnostic ceilings for newly surfaced hashability errors. Closes https://github.com/astral-sh/ty/issues/4157 ## Test plan - Added mdtests covering annotation-only and assigned `ClassVar[None]`, inherited and final unhashable classes, `list`/`dict`/`set`, both `Hashable` imports, equivalent custom protocols, mutable versus frozen dataclasses, valid hashable values, `object()` sentinel defaults, and explicit assignability/subtyping checks. - Updated existing mdtests for `object` non-equivalence and preservation of unions containing final unhashable classes, mutable dataclasses, type variables, and protocols. --- crates/ruff_benchmark/benches/ty_walltime.rs | 4 +- .../resources/mdtest/protocols.md | 187 +++++++++++++++--- .../ty_python_semantic/src/types/instance.rs | 11 +- .../ty_python_semantic/src/types/relation.rs | 18 +- 4 files changed, 188 insertions(+), 32 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index 9588fb055e..83653d1afb 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -172,7 +172,7 @@ static PANDAS: Benchmark = Benchmark::new( max_dep_date: TY_ECOSYSTEM_PIN, python_version: SupportedPythonVersion::Py311, }, - 6700, + 6800, ); static PYDANTIC: Benchmark = Benchmark::new( @@ -227,7 +227,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new( max_dep_date: TY_ECOSYSTEM_PIN, python_version: SupportedPythonVersion::Py311, }, - 2000, + 2200, ); #[track_caller] diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index a238f22ae5..e4d6558ce3 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -1242,9 +1242,11 @@ static_assert(not is_assignable_to(HasX, Foo)) static_assert(not is_subtype_of(HasX, Foo)) ``` -Since `object` defines a `__hash__` method, this means that the standard-library `Hashable` protocol -is currently understood by ty as being equivalent to `object`, much like `SupportsStr` and -`UniversalSet` above: +Although `object` defines a `__hash__` method, its subclasses can disable hashing by replacing that +method with `None`. The standard-library `Hashable` protocol is therefore not equivalent to +`object`, unlike `SupportsStr` and `UniversalSet` above. Modeling this distinction violates normal +subtyping rules and is therefore unsound, but it is widely relied on throughout the Python +ecosystem: ```py from typing import Hashable, Protocol @@ -1252,9 +1254,9 @@ from typing import Hashable, Protocol class SupportsHash(Protocol): def __hash__(self) -> int: ... -static_assert(is_equivalent_to(object, Hashable)) +static_assert(not is_equivalent_to(object, Hashable)) static_assert(is_assignable_to(object, Hashable)) -static_assert(is_subtype_of(object, Hashable)) +static_assert(not is_subtype_of(object, Hashable)) def check_object_or_hashable(x: object | Hashable): reveal_type(x) # revealed: object @@ -1266,14 +1268,13 @@ def check_hashable_or_supports_hash(x: Hashable | SupportsHash): reveal_type(x) # revealed: Hashable def check_hashable_or_universal(x: Hashable | UniversalSet): - reveal_type(x) # revealed: Hashable + reveal_type(x) # revealed: UniversalSet ``` -This means that any type considered assignable to `object` (which is all types) is considered by ty -to be assignable to `Hashable`. However, ty preserves a non-final nominal type in a union with -`Hashable` instead of discarding it as redundant. A non-final class can have unhashable subclasses, -so keeping the corresponding union element retains the annotation's more precise description of -those subclasses. For example, `list[str]` is unhashable but is a subtype of `Sequence[Hashable]`: +ty checks whether a type actually provides a callable `__hash__` method instead of assuming all +subtypes of `object` are hashable. It also preserves a non-final nominal type in a union with +`Hashable` instead of discarding it as redundant, since a subclass can disable hashing. For example, +`list[str]` is unhashable but is a subtype of `Sequence[Hashable]`: ```py from collections.abc import Hashable as AbcHashable @@ -1297,8 +1298,9 @@ static_assert(is_subtype_of(list[Hashable], Sequence[Hashable])) static_assert(is_subtype_of(list[str], Sequence[Hashable])) ``` -The additional union element is still simplified if it is a final class, because instances of the -class cannot override their inherited hashability: +The additional union element is still simplified if it is a final hashable class, because instances +of that class cannot override their inherited hashability. Final classes with `__hash__ = None` must +remain in the union: ```py from dataclasses import dataclass @@ -1330,9 +1332,8 @@ class UnhashableDataclass: ... def check_hashable_or_final(x: Hashable | C): reveal_type(x) # revealed: Hashable -# TODO: Preserve final classes that are known to be unhashable. def check_hashable_or_unhashable_final(x: Hashable | Unhashable): - reveal_type(x) # revealed: Hashable + reveal_type(x) # revealed: Hashable | Unhashable def check_hashable_or_eq_only(x: Hashable | EqOnly): reveal_type(x) # revealed: Hashable @@ -1341,10 +1342,10 @@ def check_hashable_or_eq_only_child(x: Hashable | EqOnlyChild): reveal_type(x) # revealed: Hashable def check_hashable_or_unhashable_dataclass(x: Hashable | UnhashableDataclass): - reveal_type(x) # revealed: Hashable + reveal_type(x) # revealed: Hashable | UnhashableDataclass ``` -The special case is currently limited to nominal instance types: +Type variables and protocols that can contain unhashable values also remain in the union: ```py from typing import TypeVar, TypedDict @@ -1354,24 +1355,24 @@ T = TypeVar("T") class Payload(TypedDict): value: int -# TODO: Preserve non-nominal types that can contain unhashable values. def check_hashable_or_typevar(x: Hashable | T): - reveal_type(x) # revealed: Hashable + reveal_type(x) # revealed: Hashable | T@check_hashable_or_typevar +# TODO: Preserve TypedDict types, which are unhashable at runtime. def check_hashable_or_typed_dict(x: Hashable | Payload): reveal_type(x) # revealed: Hashable def check_hashable_or_protocol(x: Hashable | HasX): - reveal_type(x) # revealed: Hashable + reveal_type(x) # revealed: Hashable | HasX ``` -We do not detect errors in cases like the following, which are flagged by other type checkers: +A list does not satisfy `Hashable`, even though it inherits from `object`: ```py def needs_something_hashable(x: Hashable): hash(x) -needs_something_hashable([]) +needs_something_hashable([]) # error: [invalid-argument-type] ``` ## Diagnostics for protocols with invalid attribute members @@ -1435,6 +1436,148 @@ class C(A, Protocol): x = 42 # fine, due to declaration in the base class ``` +## Hashable protocol assignability + +An explicitly disabled `__hash__` method makes an object incompatible with the standard-library +`Hashable` protocol, even though `object` itself defines a valid `__hash__` method. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from collections.abc import Hashable +from typing import ClassVar, Hashable as TypingHashable, final +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +def accepts_hashable(value: Hashable) -> None: ... + +class AnnotationOnly: + __hash__: ClassVar[None] + +class ExplicitNone: + __hash__: ClassVar[None] = None + +accepts_hashable(AnnotationOnly()) # error: [invalid-argument-type] +accepts_hashable(ExplicitNone()) # error: [invalid-argument-type] +``` + +Disabling `__hash__` also applies to subclasses, and declaring an unhashable class final does not +make it hashable. + +```py +class InheritedNone(ExplicitNone): ... + +@final +class FinalExplicitNone: + __hash__: ClassVar[None] = None + +accepts_hashable(InheritedNone()) # error: [invalid-argument-type] +accepts_hashable(FinalExplicitNone()) # error: [invalid-argument-type] +``` + +The standard-library stubs mark mutable built-in containers as unhashable. + +```py +accepts_hashable([]) # error: [invalid-argument-type] +accepts_hashable({}) # error: [invalid-argument-type] +accepts_hashable(set()) # error: [invalid-argument-type] +``` + +The `typing` alias imposes the same requirements as `collections.abc.Hashable`. + +```py +def accepts_typing_hashable(value: TypingHashable) -> None: ... + +accepts_typing_hashable(ExplicitNone()) # error: [invalid-argument-type] +accepts_typing_hashable([]) # error: [invalid-argument-type] +``` + +Explicitly hashable classes and immutable built-in values remain valid. + +```py +class ExplicitHash: + def __hash__(self) -> int: + return 1 + +accepts_hashable(ExplicitHash()) +accepts_hashable(1) +accepts_hashable("value") +accepts_hashable(("value",)) +``` + +Concrete `object()` instances are hashable and commonly used as sentinel values, even though the +`object` type can also include unhashable subclasses. + +```py +SENTINEL = object() + +def accepts_hashable_default(value: Hashable = SENTINEL) -> None: ... + +accepts_hashable(object()) +accepts_hashable(SENTINEL) +``` + +The same distinction applies to both assignability and subtyping checks. + +```py +static_assert(not is_assignable_to(ExplicitNone, Hashable)) +static_assert(not is_subtype_of(ExplicitNone, Hashable)) +static_assert(not is_assignable_to(list[str], Hashable)) +static_assert(not is_subtype_of(list[str], Hashable)) +``` + +## User-defined hash protocols + +A user-defined protocol that explicitly requires a callable `__hash__` method imposes the same +requirement as the standard-library `Hashable` protocol. + +```py +from typing import ClassVar, Protocol + +class SupportsHash(Protocol): + def __hash__(self) -> int: ... + +class ExplicitNone: + __hash__: ClassVar[None] = None + +class ExplicitHash: + def __hash__(self) -> int: + return 1 + +def accepts_hashable(value: SupportsHash) -> None: ... + +accepts_hashable(ExplicitNone()) # error: [invalid-argument-type] +accepts_hashable([]) # error: [invalid-argument-type] +accepts_hashable(ExplicitHash()) +accepts_hashable(object()) +``` + +## Hashability of dataclasses + +Mutable dataclasses disable hashing by default, while frozen dataclasses synthesize a callable +`__hash__` method. + +```py +from collections.abc import Hashable +from dataclasses import dataclass + +@dataclass +class Mutable: + value: int + +@dataclass(frozen=True) +class Frozen: + value: int + +def accepts_hashable(value: Hashable) -> None: ... + +accepts_hashable(Mutable(1)) # error: [invalid-argument-type] +accepts_hashable(Frozen(1)) +``` + ## Equivalence of protocols ```toml diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 1d59a26f6e..8eb48fc41b 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -1279,7 +1279,16 @@ impl<'db> ProtocolInstanceType<'db> { protocol: ProtocolInstanceType<'db>, _: (), ) -> bool { - let env = ProgramEnvironment::from_program(protocol.interface(db).base().program(db)); + let interface = protocol.interface(db); + + // Hashability is not preserved by inheritance: subclasses can replace + // `object.__hash__` with `None`. A protocol that explicitly requires `__hash__` + // therefore does not describe every object, despite `object` defining that method. + if interface.includes_member(db, "__hash__") { + return false; + } + + let env = ProgramEnvironment::from_program(interface.base().program(db)); let constraints = ConstraintSetBuilder::new(); let relation_visitor = HasRelationToVisitor::default(&constraints); let disjointness_visitor = IsDisjointVisitor::default(&constraints); diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 618ecd55d0..5db2315537 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -1699,17 +1699,21 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { self.always() } - // Fast path for various types that we know `object` is never a subtype of - // (`object` can be a subtype of some protocols, or of itself, but those cases are - // handled above). + // Fast path for various types that we know `object` is never a subtype of. ( Type::NominalInstance(source), - Type::NominalInstance(_) - | Type::SubclassOf(_) - | Type::Callable(_) - | Type::ProtocolInstance(_), + Type::NominalInstance(_) | Type::SubclassOf(_) | Type::Callable(_), ) if source.is_object() => self.never(), + // `object` is not a subtype of a non-universal protocol because some subclasses + // might not implement it. For assignability, still inspect its actual members: + // `object()` is hashable and commonly used as a sentinel for `Hashable` parameters. + (Type::NominalInstance(source), Type::ProtocolInstance(_)) + if source.is_object() && !self.relation.is_assignability() => + { + self.never() + } + // Fast path: `object` is not a subtype of any non-inferable type variable, since the // type variable could be specialized to a type smaller than `object`. (Type::NominalInstance(source), Type::TypeVar(typevar)) From 3618a85da2c89ad5a74db82d5e3d2b7fe7d953f8 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 5 Aug 2026 11:01:20 -0700 Subject: [PATCH 294/390] [ty] Restore bottom callable subtyping for gradual prefixes (#27519) ## Summary - Restore bottom-callable subtyping against gradual `Concatenate` signatures after validating their required positional prefixes. - Accept compatible object-variadic callables while preserving return-type constraints and rejecting signatures that restrict possible arguments. - Compare normalized callable parameters so unpacked positional prefixes continue to work on current `main`. Fixes astral-sh/ty#4186. ## Test plan - Add mdtests for bottom callables with single and multiple gradual positional prefixes, including the optional-prefix shape generated by the failing property test. - Cover matching positional-only, positional-or-keyword, and unpacked prefixes, plus additional target parameters consumed by an unrestricted variadic tail. - Verify incompatible prefixes, extra required parameters, restricted variadic arguments, keyword-only restrictions, and incompatible return types remain rejected. - Exercise the original bottom-callable property and the broader stable type-relation property suite. --- .../mdtest/type_properties/is_subtype_of.md | 114 ++++++++++++++++++ .../src/types/signatures.rs | 15 +++ 2 files changed, 129 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 1c3541efa7..7881a44ffd 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -1821,6 +1821,120 @@ def f(*args: Any, **kwargs: Any) -> Any: ... static_assert(not is_subtype_of(RegularCallableTypeOf[f], Callable[[], object])) ``` +#### Bottom callables with gradual positional prefixes + +A callable accepting every argument list is a subtype of a gradual callable with any positional +prefix when its return type is compatible. + +```py +from typing import Any, Callable, Concatenate, Never +from ty_extensions import static_assert +from ty_extensions._internal import RegularCallableTypeOf, is_subtype_of + +def bottom(*args: object, **kwargs: object) -> Never: + raise Exception() + +type BottomCallable = RegularCallableTypeOf[bottom] + +static_assert(is_subtype_of(BottomCallable, Callable[Concatenate[int, ...], None])) +static_assert(is_subtype_of(BottomCallable, Callable[Concatenate[int, str, ...], None])) +``` + +A callable with an optional positional-only parameter and a dynamically typed variadic tail is also +a supertype of the bottom callable. + +```py +def gradual_prefix(value: int = 0, /, *args: Any, **kwargs: Any) -> None: ... + +static_assert(is_subtype_of(BottomCallable, RegularCallableTypeOf[gradual_prefix])) +``` + +#### Object-variadic callables with matching gradual prefixes + +Object-variadic callables are subtypes of gradual callables when their required positional +parameters match the gradual callable's prefix. + +```py +from typing import Callable, Concatenate +from ty_extensions import static_assert +from ty_extensions._internal import RegularCallableTypeOf, is_subtype_of + +def positional_or_keyword(value: int, *args: object, **kwargs: object) -> None: ... +def positional_only(value: int, /, *args: object, **kwargs: object) -> None: ... + +type GradualIntCallable = Callable[Concatenate[int, ...], None] + +static_assert(is_subtype_of(RegularCallableTypeOf[positional_or_keyword], GradualIntCallable)) +static_assert(is_subtype_of(RegularCallableTypeOf[positional_only], GradualIntCallable)) +``` + +An unrestricted variadic parameter can satisfy additional positional parameters in the target. + +```py +static_assert( + is_subtype_of( + RegularCallableTypeOf[positional_only], + Callable[Concatenate[int, str, ...], None], + ) +) +``` + +Unpacked positional parameters are normalized before comparing an unrestricted variadic tail. + +```py +def unpacked_prefix(*args: *tuple[int, *tuple[object, ...]], **kwargs: object) -> None: ... + +static_assert(is_subtype_of(RegularCallableTypeOf[unpacked_prefix], GradualIntCallable)) +``` + +#### Object-variadic callables with incompatible gradual prefixes + +A source callable cannot be a subtype when its prefix has an incompatible parameter or requires more +positional arguments than the target's prefix. + +```py +from typing import Callable, Concatenate +from ty_extensions import static_assert +from ty_extensions._internal import RegularCallableTypeOf, is_subtype_of + +type GradualIntCallable = Callable[Concatenate[int, ...], None] + +def wrong_prefix(value: str, *args: object, **kwargs: object) -> None: ... +def extra_required(value: int, another: str, *args: object, **kwargs: object) -> None: ... + +static_assert(not is_subtype_of(RegularCallableTypeOf[wrong_prefix], GradualIntCallable)) +static_assert(not is_subtype_of(RegularCallableTypeOf[extra_required], GradualIntCallable)) +``` + +Both variadic parameters must accept every possible argument from the gradual tail. + +```py +def restricted_args(value: int, *args: int, **kwargs: object) -> None: ... +def restricted_kwargs(value: int, *args: object, **kwargs: int) -> None: ... + +static_assert(not is_subtype_of(RegularCallableTypeOf[restricted_args], GradualIntCallable)) +static_assert(not is_subtype_of(RegularCallableTypeOf[restricted_kwargs], GradualIntCallable)) +``` + +An additional keyword-only parameter also restricts the otherwise unrestricted variadic tail. + +```py +def required_keyword(value: int, *args: object, flag: int, **kwargs: object) -> None: ... +def optional_keyword(value: int, *args: object, flag: int = 0, **kwargs: object) -> None: ... + +static_assert(not is_subtype_of(RegularCallableTypeOf[required_keyword], GradualIntCallable)) +static_assert(not is_subtype_of(RegularCallableTypeOf[optional_keyword], GradualIntCallable)) +``` + +The return type must remain compatible even when the parameters accept every possible call. + +```py +def wrong_return(value: int, *args: object, **kwargs: object) -> int: + return 1 + +static_assert(not is_subtype_of(RegularCallableTypeOf[wrong_return], GradualIntCallable)) +``` + ### Classes with `__call__` ```py diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index ef40cac318..2daf5afdc9 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -3456,6 +3456,21 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } target_index += 1; } + + // Once every fixed source parameter matches the target prefix, an + // object-variadic tail accepts every materialization of the gradual remainder. + // Reject additional fixed or keyword-only parameters: they would make the + // source more restrictive than at least one possible target signature. + if let [source_prefix @ .., variadic, keyword_variadic] = + source_parameters.as_slice() + && source_prefix.len() <= target_prefix_params.len() + && variadic.is_variadic() + && variadic.annotated_type().is_object() + && keyword_variadic.is_keyword_variadic() + && keyword_variadic.annotated_type().is_object() + { + return result; + } } _ => {} From 8c7933a0afde21e763b1a14c6cf3d90d38b13270 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 5 Aug 2026 20:27:40 +0100 Subject: [PATCH 295/390] [ty] Shrink callable types in property tests (#27504) --- .../types/property_tests/type_generation.rs | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index 10a08692e8..6fc0a7a1ba 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -9,6 +9,7 @@ use crate::types::{ SpecialFormType, SubclassOfType, Type, UnionType, }; use crate::{Program, ProgramEnvironment}; +use itertools::Either; use quickcheck::{Arbitrary, Gen}; use ruff_db::files::system_path_to_file; use ruff_python_ast::name::Name; @@ -119,6 +120,39 @@ impl CallableParams { ), } } + + fn shrink(self) -> impl Iterator { + match self { + // If the failure does not depend on accepting arbitrary arguments, replace `...` + // with the simplest concrete signature: one that accepts no arguments. + Self::GradualForm => Either::Left(std::iter::once(Self::List(Vec::new()))), + Self::List(params) => { + // Removing one parameter at a time preserves the ordering and names of all + // remaining parameters, so each candidate is still a valid signature. + let removed_parameters = (0..params.len()).map({ + let params = params.clone(); + move |index| { + let mut shrunk = params.clone(); + shrunk.remove(index); + Self::List(shrunk) + } + }); + + // If a parameter cannot be removed without losing the failure, try simplifying its + // name, default, or annotation while preserving the rest of the signature. + let shrunk_parameters = (0..params.len()).flat_map(move |index| { + let params = params.clone(); + params[index].clone().shrink().map(move |parameter| { + let mut shrunk = params.clone(); + shrunk[index] = parameter; + Self::List(shrunk) + }) + }); + + Either::Right(removed_parameters.chain(shrunk_parameters)) + } + } + } } #[derive(Debug, Clone, PartialEq)] @@ -129,6 +163,44 @@ pub(crate) struct Param { default_ty: Option, } +impl Param { + fn shrink(self) -> impl Iterator { + let without_name = + (self.kind == ParamKind::PositionalOnly && self.name.is_some()).then(|| Self { + name: None, + ..self.clone() + }); + + let shrunk_defaults = self.default_ty.shrink().map({ + let parameter = self.clone(); + move |default_ty| Self { + default_ty, + ..parameter.clone() + } + }); + + let shrunk_annotations = shrink_callable_component(&self.annotated_ty).map({ + let parameter = self.clone(); + move |annotated_ty| Self { + annotated_ty, + ..parameter.clone() + } + }); + + without_name + .into_iter() + .chain(shrunk_defaults) + .chain(shrunk_annotations) + } +} + +fn shrink_callable_component(ty: &Ty) -> impl Iterator + use<> { + let object = Ty::KnownClassInstance(KnownClass::Object); + let simplified = (ty != &object).then_some(object); + + simplified.into_iter().chain(ty.shrink()) +} + #[derive(Debug, Clone, Copy, PartialEq)] enum ParamKind { PositionalOnly, @@ -624,6 +696,23 @@ impl Arbitrary for Ty { }), ) } + Ty::Callable { params, returns } => { + let shrunk_parameters = params.clone().shrink().map({ + let returns = returns.clone(); + move |params| Ty::Callable { + params, + returns: returns.clone(), + } + }); + + let shrunk_return_type = + shrink_callable_component(&returns).map(move |returns| Ty::Callable { + params: params.clone(), + returns: Box::new(returns), + }); + + Box::new(shrunk_parameters.chain(shrunk_return_type)) + } _ => Box::new(std::iter::empty()), } } @@ -655,3 +744,75 @@ pub(crate) fn union<'db>( ) -> Type<'db> { UnionType::from_elements(db, env, tys) } + +mod tests { + use super::*; + use test_case::test_case; + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum CallableShrink { + Parameter, + ParameterName, + ParameterDefault, + ParameterAnnotation, + ReturnType, + } + + // Test each independently removable signature detail separately so a failure identifies the + // exact shrink candidate that is missing. + #[test_case(CallableShrink::Parameter; "removes a parameter")] + #[test_case(CallableShrink::ParameterName; "removes a positional only parameter name")] + #[test_case(CallableShrink::ParameterDefault; "removes a parameter default")] + #[test_case(CallableShrink::ParameterAnnotation; "simplifies a parameter annotation")] + #[test_case(CallableShrink::ReturnType; "simplifies the return type")] + fn callable_shrinks_parameters_and_return_type(shrink: CallableShrink) { + let parameter = Param { + kind: ParamKind::PositionalOnly, + name: Some(Name::new_static("argument")), + annotated_ty: Ty::Union(vec![Ty::KnownClassInstance(KnownClass::Int), Ty::None]), + default_ty: Some(Ty::IntLiteral(1)), + }; + let callable = Ty::Callable { + params: CallableParams::List(vec![parameter.clone()]), + returns: Box::new(Ty::FixedLengthTuple(vec![])), + }; + + let mut expected_parameters = vec![parameter]; + let mut expected_return = Ty::FixedLengthTuple(vec![]); + match shrink { + CallableShrink::Parameter => expected_parameters.clear(), + CallableShrink::ParameterName => expected_parameters[0].name = None, + CallableShrink::ParameterDefault => expected_parameters[0].default_ty = None, + CallableShrink::ParameterAnnotation => { + expected_parameters[0].annotated_ty = Ty::KnownClassInstance(KnownClass::Object); + } + CallableShrink::ReturnType => { + expected_return = Ty::KnownClassInstance(KnownClass::Object); + } + } + + let expected = Ty::Callable { + params: CallableParams::List(expected_parameters), + returns: Box::new(expected_return), + }; + assert!(callable.shrink().any(|candidate| candidate == expected)); + } + + // A gradual `...` parameter list can become an empty concrete signature when accepting + // arbitrary arguments is not essential to the failing property. + #[test] + fn gradual_callable_shrinks_to_empty_parameter_list() { + let callable = Ty::Callable { + params: CallableParams::GradualForm, + returns: Box::new(Ty::KnownClassInstance(KnownClass::Object)), + }; + + assert_eq!( + callable.shrink().collect::>(), + vec![Ty::Callable { + params: CallableParams::List(vec![]), + returns: Box::new(Ty::KnownClassInstance(KnownClass::Object)), + }] + ); + } +} From 4ac5dee0c77bb7129f6fe9c96b27c5c541d0801f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 5 Aug 2026 20:27:56 +0100 Subject: [PATCH 296/390] [ty] Add subdiagnostic hint when an open `TypedDict` is not assignable to a certain `Mapping` type, but it would be if it were closed (#27512) --- .../mdtest/diagnostics/error_context.md | 27 ++++++++++++++ .../ty_python_semantic/src/types/relation.rs | 36 +++++++++++++++++- .../src/types/relation_error.rs | 37 +++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md index a805a76f91..36e9aa6f9f 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/error_context.md @@ -819,6 +819,33 @@ help: A TypedDict is not usually assignable to any `dict[..]` type; `dict` types help: Consider using `Mapping[..]` instead of `dict[..]`. ``` +Assigning an open `TypedDict` to a specialized `Mapping`: + +```py +from collections.abc import Mapping +from typing import TypedDict + +class D(TypedDict): + a: int + b: int + +def f(d: D) -> Mapping[str, int]: + return d # snapshot +``` + +```snapshot +error[invalid-return-type]: Return type does not match returned value + --> src/mdtest_snippet.py:40:12 + | +39 | def f(d: D) -> Mapping[str, int]: + | ----------------- Expected `Mapping[str, int]` because of return type +40 | return d # snapshot + | ^ expected `Mapping[str, int]`, found `D` +info: TypedDict `D` is not assignable to `Mapping[str, int]` +help: `D` would be assignable to this `Mapping` type if it were declared with `closed=True`, but TypedDicts are open by default. +help: A subclass of `D` could validly add a new field of an arbitrary type, violating subtyping with the `Mapping` type +``` + ## Generic `TypedDict` field conflicts in overload diagnostics A generic `TypedDict` relation can be unsatisfiable without being the `never` terminal. The diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 5db2315537..f5ea3d8990 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -2160,13 +2160,45 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { ) }; let result = self.check_type_pair(db, fallback, target); + if let Some(context) = self.report_context() && result.is_never_satisfied(db, env) && let Type::NominalInstance(instance) = target - && instance.class(db, env).is_known(db, KnownClass::Dict) { - context.push(ErrorContext::TypedDictNotAssignableToDict(typed_dict)); + match instance.class(db, env).known(db) { + Some(KnownClass::Dict) => { + context + .push(ErrorContext::TypedDictNotAssignableToDict(typed_dict)); + } + Some(KnownClass::Mapping) + if typed_dict.openness(db).is_implicitly_open() => + { + let field_types = + typed_dict.items(db).values().map(|field| field.declared_ty); + let mapping_fallback_spec = &[ + KnownClass::Str.to_instance(db, env), + UnionType::from_elements(db, env, field_types), + ]; + + let closed_typeddict_fallback = KnownClass::Mapping + .to_specialized_instance(db, env, mapping_fallback_spec); + + if self + .check_type_pair(db, closed_typeddict_fallback, target) + .is_always_satisfied(db, env) + { + let context_element = + ErrorContext::OpenTypedDictNotAssignableToMapping { + source: typed_dict, + target, + }; + context.push(context_element); + } + } + _ => {} + } } + result }) } diff --git a/crates/ty_python_semantic/src/types/relation_error.rs b/crates/ty_python_semantic/src/types/relation_error.rs index 91d00bf5c5..b869fd081d 100644 --- a/crates/ty_python_semantic/src/types/relation_error.rs +++ b/crates/ty_python_semantic/src/types/relation_error.rs @@ -92,6 +92,10 @@ pub(crate) enum ErrorContext<'db> { target_field: Type<'db>, }, TypedDictNotAssignableToDict(TypedDictType<'db>), + OpenTypedDictNotAssignableToMapping { + source: TypedDictType<'db>, + target: Type<'db>, + }, IncompatibleReturnTypes { source: Type<'db>, target: Type<'db>, @@ -279,6 +283,21 @@ impl<'db> ErrorContext<'db> { source = typed_dict_name(typed_dict) ) } + Self::OpenTypedDictNotAssignableToMapping { source, target } => { + let name = source.defining_class().map(|class| class.name(db)); + help_messages.insert(HelpMessages::OpenTypedDictNotAssignableToMapping { + typed_dict_name: name.cloned(), + }); + help_messages.insert(HelpMessages::ExplainOpenTypedDictUnsoundness { + typed_dict_name: name.cloned(), + }); + + format!( + "{source} is not assignable to `{target}`", + source = typed_dict_name(source), + target = target.display(db, env) + ) + } Self::IncompatibleReturnTypes { source, target } => format!( "incompatible return types: `{source}` is not assignable to `{target}`", source = source.display(db, env), @@ -426,6 +445,8 @@ enum HelpMessages { ConsiderUsingMappingInsteadOfDict, TopCallableExplanation, ConsiderAddingADefaultValue { parameter_name: Option }, + OpenTypedDictNotAssignableToMapping { typed_dict_name: Option }, + ExplainOpenTypedDictUnsoundness { typed_dict_name: Option }, } impl std::fmt::Display for HelpMessages { @@ -440,6 +461,22 @@ impl std::fmt::Display for HelpMessages { HelpMessages::ConsiderUsingMappingInsteadOfDict => { f.write_str("Consider using `Mapping[..]` instead of `dict[..]`.") } + HelpMessages::OpenTypedDictNotAssignableToMapping {typed_dict_name} => { + let name = typed_dict_name.as_ref().map(|name|format!("`{name}`")).unwrap_or_else(||"this TypedDict".to_string()); + write!( + f, + "{name} would be assignable to this `Mapping` type \ + if it were declared with `closed=True`, but TypedDicts are open by default." + ) + } + HelpMessages::ExplainOpenTypedDictUnsoundness {typed_dict_name} => { + let name = typed_dict_name.as_ref().map(|name|format!("`{name}`")).unwrap_or_else(||"this TypedDict".to_string()); + write!( + f, + "A subclass of {name} could validly add a new field of an arbitrary type, \ + violating subtyping with the `Mapping` type" + ) + } HelpMessages::TopCallableExplanation => f.write_str( "This type includes all possible parameter sets, \ so it cannot safely be called because there is no valid set of arguments for it", From 39ff33cf08537b43589895978233579b711707dc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Aug 2026 16:17:59 -0400 Subject: [PATCH 297/390] [ty] Preserve property getter call errors (#27509) ## Summary Previously, a failed property getter call was flattened into `call-non-callable: calling the getter failed`, and its declared return type was discarded: ```python class Example: @property def value(self) -> int: return 1 reveal_type(Example.value.__get__("wrong", Example)) # error: [invalid-argument-type] Expected `Example`, found `Literal["wrong"]` # revealed: int ``` We now preserve the getter's original call bindings and recovery type, reusing the structured accessor-error handling already used for property setters. This applies to both bound `prop.__get__(instance, owner)` and unbound `property.__get__(prop, instance, owner)` calls, with diagnostics attached to the actual instance argument. --- .../resources/mdtest/properties.md | 18 +++++ .../ty_python_semantic/src/types/call/bind.rs | 66 ++++++++++--------- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/properties.md b/crates/ty_python_semantic/resources/mdtest/properties.md index 93edff1d38..e60e3da0eb 100644 --- a/crates/ty_python_semantic/resources/mdtest/properties.md +++ b/crates/ty_python_semantic/resources/mdtest/properties.md @@ -248,6 +248,24 @@ c.attr = 1 reveal_type(c.attr) # revealed: Never ``` +### Attempting to call a getter with an incompatible instance + +Explicit bound and unbound `property.__get__` calls preserve the getter's receiver error and return +type. For the unbound call, the reported argument is the instance rather than the property itself. + +```py +class C: + @property + def attr(self) -> int: + return 1 + +# error: [invalid-argument-type] "Argument to function `C.attr` is incorrect: Expected `C`" +reveal_type(C.attr.__get__("wrong", C)) # revealed: int + +# error: [invalid-argument-type] "Argument to function `C.attr` is incorrect: Expected `C`" +reveal_type(property.__get__(C.attr, "wrong", C)) # revealed: int +``` + ### Non-returning setter ```py diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 3ca642ad74..5e74ab6f35 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1708,17 +1708,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, env, &CallArguments::positional([*instance])) - .map(|binding| binding.return_type(db, env)) - { - overload.set_return_type(return_ty); - } else { - overload.errors.push(BindingError::InternalCallError( - "calling the getter failed", - )); - overload.set_return_type(Type::unknown()); - } + overload.check_property_getter(db, env, getter, *instance, 1); } else { overload .errors @@ -1737,17 +1727,7 @@ impl<'db> Bindings<'db> { } [Some(instance), ..] => { if let Some(getter) = property.getter(db) { - if let Ok(return_ty) = getter - .try_call(db, env, &CallArguments::positional([*instance])) - .map(|binding| binding.return_type(db, env)) - { - overload.set_return_type(return_ty); - } else { - overload.errors.push(BindingError::InternalCallError( - "calling the getter failed", - )); - overload.set_return_type(Type::unknown()); - } + overload.check_property_getter(db, env, getter, *instance, 0); } else { overload.set_return_type(Type::Never); overload.errors.push(BindingError::InternalCallError( @@ -6718,6 +6698,29 @@ pub(crate) struct Binding<'db> { } impl<'db> Binding<'db> { + /// Checks the getter invoked by `property.__get__`, retaining its error and recovery type. + fn check_property_getter( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + getter: Type<'db>, + instance: Type<'db>, + argument_index_offset: usize, + ) { + match getter.try_call(db, env, &CallArguments::positional([instance])) { + Ok(bindings) => self.set_return_type(bindings.return_type(db, env)), + Err(CallError(_, bindings)) => { + self.set_return_type(bindings.return_type(db, env)); + self.errors.push(BindingError::PropertyGetterCallError( + PropertyAccessorCallError { + bindings, + argument_index_offset, + }, + )); + } + } + } + fn check_property_setter( &mut self, db: &'db dyn Db, @@ -6740,7 +6743,7 @@ impl<'db> Binding<'db> { } Err(CallError(_, bindings)) => { self.errors.push(BindingError::PropertySetterCallError( - PropertySetterCallError { + PropertyAccessorCallError { bindings, argument_index_offset, }, @@ -7965,10 +7968,11 @@ pub(crate) enum BindingError<'db> { }, PropertyHasNoSetter(PropertyInstanceType<'db>), PropertyHasNoDeleter(PropertyInstanceType<'db>), - PropertySetterCallError(PropertySetterCallError<'db>), + PropertyGetterCallError(PropertyAccessorCallError<'db>), + PropertySetterCallError(PropertyAccessorCallError<'db>), /// 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.__delete__`, - /// which can occur when the call to the underlying getter/deleter fails. + /// We use this variant to report errors in `property.__delete__`, which can occur when the + /// call to the underlying deleter fails. InternalCallError(&'static str), /// This overload binding of the callable does not match the arguments. // TODO: We could expand this with an enum to specify why the overload is unmatched. @@ -7984,12 +7988,12 @@ pub(crate) enum BindingError<'db> { } #[derive(Clone, Debug)] -pub(crate) struct PropertySetterCallError<'db> { +pub(crate) struct PropertyAccessorCallError<'db> { bindings: Box>, argument_index_offset: usize, } -impl PartialEq for PropertySetterCallError<'_> { +impl PartialEq for PropertyAccessorCallError<'_> { fn eq(&self, other: &Self) -> bool { self.argument_index_offset == other.argument_index_offset && self.bindings.callable_type() == other.bindings.callable_type() @@ -8006,7 +8010,7 @@ impl PartialEq for PropertySetterCallError<'_> { } } -impl Eq for PropertySetterCallError<'_> {} +impl Eq for PropertyAccessorCallError<'_> {} impl BindingError<'_> { /// Returns whether this error is relevant to `functools.partial(...)` construction. @@ -8094,6 +8098,7 @@ impl BindingError<'_> { | BindingError::UnmatchedOverload | BindingError::PropertyHasNoSetter(..) | BindingError::PropertyHasNoDeleter(..) + | BindingError::PropertyGetterCallError(..) | BindingError::PropertySetterCallError(..) => {} } } @@ -8154,6 +8159,7 @@ impl<'db> BindingError<'db> { | Self::InvalidDataclassArgument(_) | Self::PropertyHasNoSetter(_) | Self::PropertyHasNoDeleter(_) + | Self::PropertyGetterCallError(_) | Self::PropertySetterCallError(_) | Self::CalledTopCallable(_) | Self::InternalCallError(_) => false, @@ -8646,7 +8652,7 @@ impl<'db> BindingError<'db> { ); } - Self::PropertySetterCallError(error) => { + Self::PropertyGetterCallError(error) | Self::PropertySetterCallError(error) => { let context = CallDiagnosticContext { context: context.context, overrides: context.overrides, From c3ac42272d14fe0594722ba4c29d1b07790d12fd Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 29 Jul 2026 15:17:07 -0500 Subject: [PATCH 298/390] refactor(db): Better focus comments --- crates/ruff_db/src/diagnostic/render/full.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 017ad43fd4..dc2145decf 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -122,10 +122,8 @@ impl<'a> Diff<'a> { let source_code = self.diagnostic_source.as_source_code(); let source_text = source_code.text(); - // Partition the source code into end offsets for each cell. If `self.notebook_index` is - // `None`, indicating a regular script file, all the lines will be in one "cell" under the - // `None` key. let cells = if let Some(notebook_index) = &self.notebook_index { + // Partition the source code into end offsets for each cell. let mut last_cell_index = OneIndexed::MIN; let mut cells: Vec<(Option, TextSize)> = Vec::new(); for cell in notebook_index.iter() { @@ -138,6 +136,7 @@ impl<'a> Diff<'a> { cells.push((Some(last_cell_index), source_text.text_len())); cells } else { + // a regular script file, all the lines will be in one "cell" under the `None` key vec![(None, source_text.text_len())] }; From 89ddb33a66e25f746f9fc007fe03605d7309a674 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 30 Jul 2026 12:55:00 -0500 Subject: [PATCH 299/390] refactor(db): Extract cell calculation function --- crates/ruff_db/src/diagnostic/render/full.rs | 41 ++++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index dc2145decf..cd7ab5586c 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -122,23 +122,7 @@ impl<'a> Diff<'a> { let source_code = self.diagnostic_source.as_source_code(); let source_text = source_code.text(); - let cells = if let Some(notebook_index) = &self.notebook_index { - // Partition the source code into end offsets for each cell. - let mut last_cell_index = OneIndexed::MIN; - let mut cells: Vec<(Option, TextSize)> = Vec::new(); - for cell in notebook_index.iter() { - if cell.cell_index() != last_cell_index { - let offset = source_code.line_start(cell.start_row()); - cells.push((Some(last_cell_index), offset)); - last_cell_index = cell.cell_index(); - } - } - cells.push((Some(last_cell_index), source_text.text_len())); - cells - } else { - // a regular script file, all the lines will be in one "cell" under the `None` key - vec![(None, source_text.text_len())] - }; + let cells = self.cells(); let mut last_end = TextSize::ZERO; for (cell, offset) in cells { @@ -302,6 +286,29 @@ impl<'a> Diff<'a> { Ok(()) } + fn cells(&self) -> Vec<(Option, TextSize)> { + let source_code = self.diagnostic_source.as_source_code(); + let source_text = source_code.text(); + + if let Some(notebook_index) = &self.notebook_index { + // Partition the source code into end offsets for each cell. + let mut last_cell_index = OneIndexed::MIN; + let mut cells: Vec<(Option, TextSize)> = Vec::new(); + for cell in notebook_index.iter() { + if cell.cell_index() != last_cell_index { + let offset = source_code.line_start(cell.start_row()); + cells.push((Some(last_cell_index), offset)); + last_cell_index = cell.cell_index(); + } + } + cells.push((Some(last_cell_index), source_text.text_len())); + cells + } else { + // a regular script file, all the lines will be in one "cell" under the `None` key + vec![(None, source_text.text_len())] + } + } + fn write_gutter(&self, f: &mut std::fmt::Formatter, width: NonZeroUsize) -> std::fmt::Result { writeln!( f, From 973372e47e476a5d98423acbf9f8e59aece8d4be Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 30 Jul 2026 12:56:08 -0500 Subject: [PATCH 300/390] refactor(db): Prefer early return --- crates/ruff_db/src/diagnostic/render/full.rs | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index cd7ab5586c..0c5812c567 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -290,23 +290,23 @@ impl<'a> Diff<'a> { let source_code = self.diagnostic_source.as_source_code(); let source_text = source_code.text(); - if let Some(notebook_index) = &self.notebook_index { - // Partition the source code into end offsets for each cell. - let mut last_cell_index = OneIndexed::MIN; - let mut cells: Vec<(Option, TextSize)> = Vec::new(); - for cell in notebook_index.iter() { - if cell.cell_index() != last_cell_index { - let offset = source_code.line_start(cell.start_row()); - cells.push((Some(last_cell_index), offset)); - last_cell_index = cell.cell_index(); - } - } - cells.push((Some(last_cell_index), source_text.text_len())); - cells - } else { + let Some(notebook_index) = self.notebook_index.as_ref() else { // a regular script file, all the lines will be in one "cell" under the `None` key - vec![(None, source_text.text_len())] + return vec![(None, source_text.text_len())]; + }; + + // Partition the source code into end offsets for each cell. + let mut last_cell_index = OneIndexed::MIN; + let mut cells: Vec<(Option, TextSize)> = Vec::new(); + for cell in notebook_index.iter() { + if cell.cell_index() != last_cell_index { + let offset = source_code.line_start(cell.start_row()); + cells.push((Some(last_cell_index), offset)); + last_cell_index = cell.cell_index(); + } } + cells.push((Some(last_cell_index), source_text.text_len())); + cells } fn write_gutter(&self, f: &mut std::fmt::Formatter, width: NonZeroUsize) -> std::fmt::Result { From 8effcb8a8fa6a8231466f8e7775514b041538d7d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 30 Jul 2026 13:27:04 -0500 Subject: [PATCH 301/390] refactor(db): Clarify we are working with a cell index --- crates/ruff_db/src/diagnostic/render/full.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 0c5812c567..3a1f3aa2c5 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -125,12 +125,12 @@ impl<'a> Diff<'a> { let cells = self.cells(); let mut last_end = TextSize::ZERO; - for (cell, offset) in cells { + for (cell_index, offset) in cells { let range = TextRange::new(last_end, offset); last_end = offset; // For non-notebooks, construct and diff only the source surrounding the edits. - let (range, line_offset) = if cell.is_none() + let (range, line_offset) = if cell_index.is_none() && let Some(first) = self.fix.edits().first() && let Some(last) = self.fix.edits().last() { @@ -209,10 +209,10 @@ impl<'a> Diff<'a> { let digit_with = OneIndexed::new(largest_new).unwrap_or_default().digits(); - if let Some(cell) = cell { + if let Some(cell_index) = cell_index { // Room for 1 digit, 1 space, 1 `|`, and 1 more following space. This centers the // three colons on the pipe. - writeln!(f, "{:>1$} cell {cell}", ":::", digit_with.get() + 3)?; + writeln!(f, "{:>1$} cell {cell_index}", ":::", digit_with.get() + 3)?; } self.write_gutter(f, digit_with)?; From 9ed797b336a9a049bc1b91d5e4fa6ea5bf7e5103 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 30 Jul 2026 13:32:49 -0500 Subject: [PATCH 302/390] refactor(db): Pull out range calculations --- crates/ruff_db/src/diagnostic/render/full.rs | 25 +++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 3a1f3aa2c5..990884b153 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -122,13 +122,9 @@ impl<'a> Diff<'a> { let source_code = self.diagnostic_source.as_source_code(); let source_text = source_code.text(); - let cells = self.cells(); - - let mut last_end = TextSize::ZERO; - for (cell_index, offset) in cells { - let range = TextRange::new(last_end, offset); - last_end = offset; + let cell_ranges = self.cell_ranges(); + for (cell_index, range) in cell_ranges { // For non-notebooks, construct and diff only the source surrounding the edits. let (range, line_offset) = if cell_index.is_none() && let Some(first) = self.fix.edits().first() @@ -286,26 +282,33 @@ impl<'a> Diff<'a> { Ok(()) } - fn cells(&self) -> Vec<(Option, TextSize)> { + fn cell_ranges(&self) -> Vec<(Option, TextRange)> { let source_code = self.diagnostic_source.as_source_code(); let source_text = source_code.text(); + let mut last_end = TextSize::ZERO; let Some(notebook_index) = self.notebook_index.as_ref() else { // a regular script file, all the lines will be in one "cell" under the `None` key - return vec![(None, source_text.text_len())]; + let offset = source_text.text_len(); + let range = TextRange::new(last_end, offset); + return vec![(None, range)]; }; // Partition the source code into end offsets for each cell. let mut last_cell_index = OneIndexed::MIN; - let mut cells: Vec<(Option, TextSize)> = Vec::new(); + let mut cells: Vec<(Option, TextRange)> = Vec::new(); for cell in notebook_index.iter() { if cell.cell_index() != last_cell_index { let offset = source_code.line_start(cell.start_row()); - cells.push((Some(last_cell_index), offset)); + let range = TextRange::new(last_end, offset); + cells.push((Some(last_cell_index), range)); + last_end = offset; last_cell_index = cell.cell_index(); } } - cells.push((Some(last_cell_index), source_text.text_len())); + let offset = source_text.text_len(); + let range = TextRange::new(last_end, offset); + cells.push((Some(last_cell_index), range)); cells } From 2582b1aded915a1fc1615dfac476696a79b77c78 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 31 Jul 2026 12:15:44 -0500 Subject: [PATCH 303/390] refactor(db): Adjust cell index type --- crates/ruff_db/src/diagnostic/render/full.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 990884b153..10e99362c2 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -282,7 +282,7 @@ impl<'a> Diff<'a> { Ok(()) } - fn cell_ranges(&self) -> Vec<(Option, TextRange)> { + fn cell_ranges(&self) -> Vec<(Option, TextRange)> { let source_code = self.diagnostic_source.as_source_code(); let source_text = source_code.text(); @@ -296,19 +296,19 @@ impl<'a> Diff<'a> { // Partition the source code into end offsets for each cell. let mut last_cell_index = OneIndexed::MIN; - let mut cells: Vec<(Option, TextRange)> = Vec::new(); + let mut cells: Vec<(Option, TextRange)> = Vec::new(); for cell in notebook_index.iter() { if cell.cell_index() != last_cell_index { let offset = source_code.line_start(cell.start_row()); let range = TextRange::new(last_end, offset); - cells.push((Some(last_cell_index), range)); + cells.push((Some(last_cell_index.get()), range)); last_end = offset; last_cell_index = cell.cell_index(); } } let offset = source_text.text_len(); let range = TextRange::new(last_end, offset); - cells.push((Some(last_cell_index), range)); + cells.push((Some(last_cell_index.get()), range)); cells } From 08f48a69eddc79017e87a214d0165c46e8781819 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 30 Jul 2026 13:45:07 -0500 Subject: [PATCH 304/390] refactor(db): Decouple edit filters from diff render --- crates/ruff_db/src/diagnostic/render/full.rs | 28 +++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 10e99362c2..b06b07c08a 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs @@ -150,24 +150,26 @@ impl<'a> Diff<'a> { (range, 0) }; + let edits = self + .fix + .edits() + .iter() + .filter(|edit| range.contains_range(edit.range())) + .collect::>(); + // No edits were applied, so there's no need to diff. + if edits.is_empty() { + continue; + } + let input = source_code.slice(range); let mut output = String::with_capacity(input.len()); let mut last_end = range.start(); - let mut applied = 0; - for edit in self.fix.edits() { - if range.contains_range(edit.range()) { - output.push_str(source_code.slice(TextRange::new(last_end, edit.start()))); - output.push_str(edit.content().unwrap_or_default()); - last_end = edit.end(); - applied += 1; - } - } - - // No edits were applied, so there's no need to diff. - if applied == 0 { - continue; + for edit in edits { + output.push_str(source_code.slice(TextRange::new(last_end, edit.start()))); + output.push_str(edit.content().unwrap_or_default()); + last_end = edit.end(); } output.push_str(&source_text[usize::from(last_end)..usize::from(range.end())]); From 9501d97402f99f22a35483d6538e6d7a61ac0473 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Aug 2026 17:29:29 -0400 Subject: [PATCH 305/390] [ty] Simplify numeric tower displays (e.g., `float` over `int | float`) (#27406) ## Summary PEP 484's numeric tower means we represent a `float` annotation as the semantic union `int | float*` and `complex` as `int | float* | complex*`. We previously rendered those internal unions directly, which exposed an implementation detail in user-facing output: ```python def f() -> float: return 1 reveal_type(f()) # Before: int | float; after: float ``` This change recognizes complete numeric-tower unions during display and renders them using their canonical annotation spelling, `float` or `complex`. The implementation does _not_ preserve explicit unions, e.g., if the user writes out `int | float`, we still collapse that rather than trying to preserve it, mostly for simplicity (as per https://github.com/astral-sh/ty/issues/2184#issuecomment-4345219164). Closes https://github.com/astral-sh/ty/issues/2184. --- crates/ty_ide/src/hover.rs | 65 +++- crates/ty_ide/src/inlay_hints.rs | 69 +--- ...tests__hover_shadowed_numeric_builtin.snap | 17 + ..._numeric_builtin_in_keyword_parameter.snap | 17 + ...numeric_builtin_in_selected_signature.snap | 17 + .../mdtest/annotations/int_float_complex.md | 47 ++- .../resources/mdtest/annotations/new_types.md | 44 +-- .../resources/mdtest/assignment/augmented.md | 6 +- .../resources/mdtest/attributes.md | 4 +- .../resources/mdtest/bidirectional.md | 8 +- .../resources/mdtest/binary/booleans.md | 14 +- .../resources/mdtest/binary/instances.md | 16 +- .../resources/mdtest/binary/integers.md | 24 +- .../resources/mdtest/binary/unions.md | 12 +- .../resources/mdtest/call/function.md | 2 +- .../mdtest/call/functools_partial.md | 24 +- .../resources/mdtest/call/union.md | 4 +- .../comparison/instances/rich_comparison.md | 8 +- .../resources/mdtest/cycle.md | 2 +- .../mdtest/dataclasses/dataclass_transform.md | 2 +- .../resources/mdtest/descriptor_protocol.md | 2 +- .../diagnostics/invalid_argument_type.md | 4 +- .../resources/mdtest/enums.md | 4 +- .../mdtest/generics/legacy/classes.md | 2 +- .../mdtest/generics/legacy/functions.md | 2 +- .../mdtest/generics/legacy/variables.md | 4 +- .../mdtest/generics/pep695/classes.md | 2 +- .../mdtest/generics/pep695/concatenate.md | 2 +- .../mdtest/generics/pep695/functions.md | 2 +- .../mdtest/generics/pep695/paramspec.md | 2 +- .../mdtest/generics/pep695/variables.md | 2 +- .../resources/mdtest/import/stub_packages.md | 6 +- .../resources/mdtest/libraries/numpy.md | 2 +- .../mdtest/literal/collections/dictionary.md | 6 +- .../resources/mdtest/literal/complex.md | 2 +- .../resources/mdtest/literal/float.md | 2 +- .../resources/mdtest/literal/integer.md | 4 +- .../resources/mdtest/named_tuple.md | 10 +- .../resources/mdtest/narrow/match.md | 12 +- .../resources/mdtest/narrow/truthiness.md | 4 +- .../resources/mdtest/narrow/type_guards.md | 2 +- .../resources/mdtest/promotion.md | 8 +- .../resources/mdtest/protocols.md | 4 +- .../resources/mdtest/scopes/nonlocal.md | 2 +- ...e_for\342\200\246_(ffe39a3bae68cfe4).snap" | 8 +- ...n_wit\342\200\246_(dd80c593d9136f35).snap" | 24 +- ...n_wit\342\200\246_(f66e3a8a3977c472).snap" | 48 +-- ...n_wit\342\200\246_(8fdf5a06afc7d4fe).snap" | 48 +-- ...'s_bo\342\200\246_(fcd7ad5416c91629).snap" | 2 +- .../resources/mdtest/struct_unpack.md | 16 +- .../resources/mdtest/subscript/tuple.md | 8 +- .../resources/mdtest/unary/invert_add_usub.md | 4 +- .../ty_python_semantic/src/types/display.rs | 366 +++++++++++++++++- .../ty_python_semantic/src/types/function.rs | 6 +- .../src/types/ide_support.rs | 20 +- .../src/types/relation_error.rs | 35 +- .../src/types/set_theoretic.rs | 19 + 57 files changed, 773 insertions(+), 325 deletions(-) create mode 100644 crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin.snap create mode 100644 crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_keyword_parameter.snap create mode 100644 crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_selected_signature.snap diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 36b3024ff5..a1dc3a3f4b 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -12,7 +12,7 @@ use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::types::ide_support::{resolved_call_signature, typed_dict_key_hover}; use ty_python_semantic::types::{KnownInstanceType, Type, TypeAliasType, TypeVarVariance}; -use ty_python_semantic::{DisplaySettings, SemanticModel, TypeQualifiers}; +use ty_python_semantic::{SemanticModel, TypeQualifiers}; pub fn hover<'db>( db: &'db dyn Db, @@ -340,9 +340,7 @@ impl<'db> DisplayHoverContent<'_, 'db> { let db = self.db; // Special types like `` // render poorly with python syntax-highlighting but well as xml - let ty_string = ty - .display_with(db, self.env, DisplaySettings::default().multiline()) - .to_string(); + let ty_string = ty.display(db, self.env).multiline().to_string(); let syntax = if ty_string.starts_with('<') { "xml" } else { @@ -6067,13 +6065,13 @@ def function(): ); assert_snapshot!(test.hover(), @" - int | float + float --------------------------------------------- Convert a string or number to a floating-point number, if possible. --------------------------------------------- ```python - int | float + float ``` --- Convert a string or number to a floating-point number, if possible. @@ -6088,6 +6086,61 @@ def function(): "); } + #[test] + fn hover_shadowed_numeric_builtin() { + let test = hover_test( + r#" + import builtins + + class float: ... + + def f(x: builtins.float | float): + x + "#, + ); + + assert_snapshot!(test.hover()); + } + + #[test] + fn hover_shadowed_numeric_builtin_in_selected_signature() { + let test = hover_test( + r#" + import builtins + from typing import overload + + class float: ... + + @overload + def choose(value: builtins.float | float) -> None: ... + @overload + def choose(value: str) -> None: ... + def choose(value: object) -> None: ... + + choose(1.0) + "#, + ); + + assert_snapshot!(test.hover()); + } + + #[test] + fn hover_shadowed_numeric_builtin_in_keyword_parameter() { + let test = hover_test( + r#" + import builtins + + class float: ... + + def choose(*, value: builtins.float | float) -> None: ... + + choose(value=1.0) + "#, + ); + + assert_snapshot!(test.hover()); + } + #[test] fn hover_bare_final_annotation() { let test = hover_test( diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 265f9a6531..8a69580832 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -2442,7 +2442,7 @@ Source with applied edits: assert_snapshot!(test.inlay_hints(), @r#" a[: list[int]] = [1, 2] - b[: list[int | float]] = [1.0, 2.0] + b[: list[float]] = [1.0, 2.0] c[: list[bool]] = [True, False] d[: list[None | Unknown]] = [None, None] e[: list[str]] = ["hel", "lo"] @@ -2450,8 +2450,8 @@ Source with applied edits: g[: list[str]] = [f"{ft}", f"{ft}"] h[: list[Template]] = [t"wow %d", t"wow %d"] i[: list[bytes]] = [b'/x01', b'/x02'] - j[: list[int | float]] = [+1, +2.0] - k[: list[int | float]] = [-1, -2.0] + j[: list[float]] = [+1, +2.0] + k[: list[float]] = [-1, -2.0] --------------------------------------------- info[inlay-hint-location]: Inlay Hint Target @@ -2484,30 +2484,19 @@ Source with applied edits: info: Source --> main2.py:LL:5 | - LL | b[: list[int | float]] = [1.0, 2.0] + LL | b[: list[float]] = [1.0, 2.0] | ^^^^ - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/builtins.pyi:LL:7 - | - LL | class int: - | ^^^ - info: Source - --> main2.py:LL:10 - | - LL | b[: list[int | float]] = [1.0, 2.0] - | ^^^ - info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class float: | ^^^^^ info: Source - --> main2.py:LL:16 + --> main2.py:LL:10 | - LL | b[: list[int | float]] = [1.0, 2.0] - | ^^^^^ + LL | b[: list[float]] = [1.0, 2.0] + | ^^^^^ info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 @@ -2682,30 +2671,19 @@ Source with applied edits: info: Source --> main2.py:LL:5 | - LL | j[: list[int | float]] = [+1, +2.0] + LL | j[: list[float]] = [+1, +2.0] | ^^^^ - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/builtins.pyi:LL:7 - | - LL | class int: - | ^^^ - info: Source - --> main2.py:LL:10 - | - LL | j[: list[int | float]] = [+1, +2.0] - | ^^^ - info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class float: | ^^^^^ info: Source - --> main2.py:LL:16 + --> main2.py:LL:10 | - LL | j[: list[int | float]] = [+1, +2.0] - | ^^^^^ + LL | j[: list[float]] = [+1, +2.0] + | ^^^^^ info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 @@ -2715,30 +2693,19 @@ Source with applied edits: info: Source --> main2.py:LL:5 | - LL | k[: list[int | float]] = [-1, -2.0] + LL | k[: list[float]] = [-1, -2.0] | ^^^^ - info[inlay-hint-location]: Inlay Hint Target - --> stdlib/builtins.pyi:LL:7 - | - LL | class int: - | ^^^ - info: Source - --> main2.py:LL:10 - | - LL | k[: list[int | float]] = [-1, -2.0] - | ^^^ - info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:LL:7 | LL | class float: | ^^^^^ info: Source - --> main2.py:LL:16 + --> main2.py:LL:10 | - LL | k[: list[int | float]] = [-1, -2.0] - | ^^^^^ + LL | k[: list[float]] = [-1, -2.0] + | ^^^^^ --------------------------------------------- info[inlay-hint-edit]: Inlay hint edits @@ -2759,7 +2726,7 @@ Source with applied edits: - j = [+1, +2.0] - k = [-1, -2.0] 4 + a: list[int] = [1, 2] - 5 + b: list[int | float] = [1.0, 2.0] + 5 + b: list[float] = [1.0, 2.0] 6 + c: list[bool] = [True, False] 7 + d: list[None | Unknown] = [None, None] 8 + e: list[str] = ["hel", "lo"] @@ -2767,8 +2734,8 @@ Source with applied edits: 10 + g: list[str] = [f"{ft}", f"{ft}"] 11 + h: list[Template] = [t"wow %d", t"wow %d"] 12 + i: list[bytes] = [b'/x01', b'/x02'] - 13 + j: list[int | float] = [+1, +2.0] - 14 + k: list[int | float] = [-1, -2.0] + 13 + j: list[float] = [+1, +2.0] + 14 + k: list[float] = [-1, -2.0] | "#); } diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin.snap new file mode 100644 index 0000000000..30fd7ba89c --- /dev/null +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin.snap @@ -0,0 +1,17 @@ +--- +source: crates/ty_ide/src/hover.rs +expression: test.hover() +--- +builtins.float | main.float +--------------------------------------------- +```python +builtins.float | main.float +``` +--------------------------------------------- +info[hover]: Hovered content is + --> main.py:7:5 + | +7 | x + | ^- Cursor offset + | | + | source diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_keyword_parameter.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_keyword_parameter.snap new file mode 100644 index 0000000000..cf02fb1bd3 --- /dev/null +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_keyword_parameter.snap @@ -0,0 +1,17 @@ +--- +source: crates/ty_ide/src/hover.rs +expression: test.hover() +--- +(parameter) value: builtins.float | main.float +--------------------------------------------- +```python +(parameter) value: builtins.float | main.float +``` +--------------------------------------------- +info[hover]: Hovered content is + --> main.py:8:8 + | +8 | choose(value=1.0) + | ^^^^^- Cursor offset + | | + | source diff --git a/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_selected_signature.snap b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_selected_signature.snap new file mode 100644 index 0000000000..1f8cb88e37 --- /dev/null +++ b/crates/ty_ide/src/snapshots/ty_ide__hover__tests__hover_shadowed_numeric_builtin_in_selected_signature.snap @@ -0,0 +1,17 @@ +--- +source: crates/ty_ide/src/hover.rs +expression: test.hover() +--- +def choose(value: builtins.float | main.float) -> None +--------------------------------------------- +```python +def choose(value: builtins.float | main.float) -> None +``` +--------------------------------------------- +info[hover]: Hovered content is + --> main.py:13:1 + | +13 | choose(1.0) + | ^^^^^^- Cursor offset + | | + | source diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md b/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md index fada88bd9b..b35ee96fb2 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/int_float_complex.md @@ -40,12 +40,20 @@ def assigns_float_to_int(x: float): y: int = x ``` -Unlike other type checkers, we choose not to obfuscate this special case by displaying `int | float` -as just `float`; we display the actual type: +Ty displays these numeric-tower unions using the canonical spellings `float` and `complex`. Exact +runtime instances are displayed as `float*` and `complex*` to preserve the distinction. The starred +spellings are only used in type displays; use `ty_extensions.JustFloat` or `JustComplex` to write +the exact types in annotations. ```py def f(x: float): - reveal_type(x) # revealed: int | float + reveal_type(x) # revealed: float + +def returns_float() -> float: + return 1 + +reveal_type(returns_float()) # revealed: float +reveal_type(1.0) # revealed: float* ``` ## complex @@ -86,7 +94,32 @@ def assigns_complex(x: complex): z: float = x def f(x: complex): - reveal_type(x) # revealed: int | float | complex + reveal_type(x) # revealed: complex + +reveal_type(1j) # revealed: complex* +``` + +## Shadowed numeric builtins + +Canonical numeric names remain qualified when a module defines a class with the same name: + +```py +import builtins + +class float: ... +class complex: ... + +def reveal_shadowed_names( + x: builtins.float | float, + y: builtins.complex | complex, +): + reveal_type(x) # revealed: builtins.float | mdtest_snippet.float + reveal_type(y) # revealed: builtins.complex | mdtest_snippet.complex + +def takes_custom_float(x: float): ... +def pass_builtin_float(x: builtins.float): + # error: [invalid-argument-type] "Argument to function `takes_custom_float` is incorrect: Expected `mdtest_snippet.float`, found `builtins.float`" + takes_custom_float(x) ``` ## Narrowing @@ -99,14 +132,14 @@ from typing_extensions import assert_type from ty_extensions import JustFloat def f(x: complex): - reveal_type(x) # revealed: int | float | complex + reveal_type(x) # revealed: complex if isinstance(x, int): reveal_type(x) # revealed: int elif isinstance(x, float): - reveal_type(x) # revealed: float + reveal_type(x) # revealed: float* else: - reveal_type(x) # revealed: complex + reveal_type(x) # revealed: complex* assert isinstance(x, float) assert_type(x, JustFloat) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index bdc89a0c9d..a6869cf3f8 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -306,10 +306,10 @@ from ty_extensions._internal import is_assignable_to Foo = NewType("Foo", float) Foo(3.14) Foo(42) -Foo("hello") # error: [invalid-argument-type] "Argument is incorrect: Expected `int | float`, found `Literal["hello"]`" +Foo("hello") # error: [invalid-argument-type] "Argument is incorrect: Expected `float`, found `Literal["hello"]`" -reveal_type(Foo(3.14).__class__) # revealed: type[int | float] -reveal_type(Foo(42).__class__) # revealed: type[int | float] +reveal_type(Foo(3.14).__class__) # revealed: type[float] +reveal_type(Foo(42).__class__) # revealed: type[float] static_assert(is_assignable_to(Foo, float)) static_assert(is_assignable_to(Foo, int | float)) static_assert(is_assignable_to(Foo, int | float | None)) @@ -326,9 +326,9 @@ Bar(3.14) Bar(42) Bar("goodbye") # error: [invalid-argument-type] -reveal_type(Bar(1 + 2j).__class__) # revealed: type[int | float | complex] -reveal_type(Bar(3.14).__class__) # revealed: type[int | float | complex] -reveal_type(Bar(42).__class__) # revealed: type[int | float | complex] +reveal_type(Bar(1 + 2j).__class__) # revealed: type[complex] +reveal_type(Bar(3.14).__class__) # revealed: type[complex] +reveal_type(Bar(42).__class__) # revealed: type[complex] static_assert(is_assignable_to(Bar, complex)) static_assert(is_assignable_to(Bar, int | float | complex)) static_assert(is_assignable_to(Bar, int | float | complex | None)) @@ -340,8 +340,8 @@ static_assert(is_assignable_to(Bar, Bar | None)) ```py FooFoo = NewType("FooFoo", Foo) -reveal_type(FooFoo(Foo(3.14)).__class__) # revealed: type[int | float] -reveal_type(FooFoo(Foo(42)).__class__) # revealed: type[int | float] +reveal_type(FooFoo(Foo(3.14)).__class__) # revealed: type[float] +reveal_type(FooFoo(Foo(42)).__class__) # revealed: type[float] static_assert(is_assignable_to(FooFoo, float)) static_assert(is_assignable_to(FooFoo, Foo)) static_assert(is_assignable_to(FooFoo, int | float)) @@ -384,12 +384,12 @@ explicit `Union` on the left side: ```py reveal_type(Foo(3.14) < Foo(42)) # revealed: bool reveal_type(Foo(3.14) == Foo(42)) # revealed: bool -reveal_type(Foo(3.14) + Foo(42)) # revealed: int | float -reveal_type(Foo(3.14) / Foo(42)) # revealed: int | float +reveal_type(Foo(3.14) + Foo(42)) # revealed: float +reveal_type(Foo(3.14) / Foo(42)) # revealed: float reveal_type(FooFoo(Foo(3.14)) < FooFoo(Foo(42))) # revealed: bool reveal_type(FooFoo(Foo(3.14)) == FooFoo(Foo(42))) # revealed: bool -reveal_type(FooFoo(Foo(3.14)) + FooFoo(Foo(42))) # revealed: int | float -reveal_type(FooFoo(Foo(3.14)) / FooFoo(Foo(42))) # revealed: int | float +reveal_type(FooFoo(Foo(3.14)) + FooFoo(Foo(42))) # revealed: float +reveal_type(FooFoo(Foo(3.14)) / FooFoo(Foo(42))) # revealed: float ``` But again as above, we can't _always_ lower `Foo` to `int | float`, because there are also binary @@ -442,16 +442,16 @@ reveal_type(unknown * MyFloat(1.0)) # revealed: Unknown Unary operations take a different codepath and need their own test cases: ```py -reveal_type(-Foo(3.14)) # revealed: int | float -reveal_type(+Foo(3.14)) # revealed: int | float +reveal_type(-Foo(3.14)) # revealed: float +reveal_type(+Foo(3.14)) # revealed: float ~Foo(3.14) # error: [unsupported-operator] reveal_type(not Foo(3.14)) # revealed: bool -reveal_type(-Bar(1 + 2j)) # revealed: int | float | complex -reveal_type(+Bar(1 + 2j)) # revealed: int | float | complex +reveal_type(-Bar(1 + 2j)) # revealed: complex +reveal_type(+Bar(1 + 2j)) # revealed: complex ~Bar(1 + 2j) # error: [unsupported-operator] reveal_type(not Bar(1 + 2j)) # revealed: bool -reveal_type(-FooFoo(Foo(3.14))) # revealed: int | float -reveal_type(+FooFoo(Foo(3.14))) # revealed: int | float +reveal_type(-FooFoo(Foo(3.14))) # revealed: float +reveal_type(+FooFoo(Foo(3.14))) # revealed: float ~FooFoo(Foo(3.14)) # error: [unsupported-operator] reveal_type(not FooFoo(Foo(3.14))) # revealed: bool @@ -476,13 +476,13 @@ union: ```py def _(x: Foo | float, y: Bar | complex): - reveal_type(-x) # revealed: int | float - reveal_type(+x) # revealed: int | float + reveal_type(-x) # revealed: float + reveal_type(+x) # revealed: float ~x # error: [unsupported-operator] reveal_type(not x) # revealed: bool - reveal_type(-y) # revealed: int | float | complex - reveal_type(+y) # revealed: int | float | complex + reveal_type(-y) # revealed: complex + reveal_type(+y) # revealed: complex ~y # error: [unsupported-operator] reveal_type(not y) # revealed: bool ``` diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md index 76172dba8e..1787834ee6 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md @@ -9,7 +9,7 @@ reveal_type(x) # revealed: Literal[2] x = 1.0 x /= 2 -reveal_type(x) # revealed: int | float +reveal_type(x) # revealed: float x = (1, 2) x += (3, 4) @@ -140,7 +140,7 @@ def _(flag1: bool, flag2: bool): f = 42.0 f += 12 - reveal_type(f) # revealed: int | str | float + reveal_type(f) # revealed: float | str ``` ## Target union @@ -184,7 +184,7 @@ def f(flag: bool, flag2: bool): f = Bar() f += 12 - reveal_type(f) # revealed: int | str | float + reveal_type(f) # revealed: float | str ``` ## Implicit dunder calls on class objects diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index ad7442e7c0..03e5a1dfca 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -4021,8 +4021,8 @@ reveal_type(C.a_int) # revealed: int reveal_type(C.a_str) # revealed: str reveal_type(C.a_bytes) # revealed: bytes reveal_type(C.a_bool) # revealed: bool -reveal_type(C.a_float) # revealed: int | float -reveal_type(C.a_complex) # revealed: int | float | complex +reveal_type(C.a_float) # revealed: float +reveal_type(C.a_complex) # revealed: complex reveal_type(C.a_tuple) # revealed: tuple[int] reveal_type(C.a_range) # revealed: range # TODO: revealed: slice[Any, Literal[1], Any] diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index 3b1f729c3b..c03a3df57b 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -174,7 +174,7 @@ takes_float_sequence([1]) mutable_floats = [1.0] mutable_floats.append(1) -reveal_type(mutable_floats) # revealed: list[int | float] +reveal_type(mutable_floats) # revealed: list[float] ``` ### Exact complex types in covariant contexts @@ -1906,7 +1906,7 @@ def overloaded_call(data: object, dtype: object) -> object: def _(dtype: FloatDtype): x = overloaded_call([1.0], dtype) - reveal_type(x) # revealed: int | float + reveal_type(x) # revealed: float ``` ```py @@ -2278,7 +2278,7 @@ def _() -> int: ```py x7 = [] x7[:] = [1, "2", 3.0] -reveal_type(x7) # revealed: list[int | str | float] +reveal_type(x7) # revealed: list[float | str] ``` ```py @@ -2415,7 +2415,7 @@ x23 = [None, None, None] x23[0] = 1 x23[1] = "2" x23[2] = 3.0 -reveal_type(x23) # revealed: list[int | str | float | None] +reveal_type(x23) # revealed: list[float | str | None] ``` ```py diff --git a/crates/ty_python_semantic/resources/mdtest/binary/booleans.md b/crates/ty_python_semantic/resources/mdtest/binary/booleans.md index 9cebc36765..5766ac77d2 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/booleans.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/booleans.md @@ -35,8 +35,8 @@ reveal_type(b**a) # revealed: Literal[0] reveal_type(b**b) # revealed: Literal[1] # Division -reveal_type(a / a) # revealed: float -reveal_type(b / a) # revealed: float +reveal_type(a / a) # revealed: float* +reveal_type(b / a) # revealed: float* b / b # error: [division-by-zero] "Cannot divide object of type `Literal[False]` by zero" a / b # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero" @@ -89,7 +89,7 @@ def _(a: bool): reveal_type(x - a) # revealed: int reveal_type(x * a) # revealed: int reveal_type(x // a) # revealed: int - reveal_type(x / a) # revealed: int | float + reveal_type(x / a) # revealed: float reveal_type(x % a) # revealed: int def rhs_is_int(x: int): @@ -97,7 +97,7 @@ def _(a: bool): reveal_type(a - x) # revealed: int reveal_type(a * x) # revealed: int reveal_type(a // x) # revealed: int - reveal_type(a / x) # revealed: int | float + reveal_type(a / x) # revealed: float reveal_type(a % x) # revealed: int def lhs_is_bool(x: bool): @@ -105,7 +105,7 @@ def _(a: bool): reveal_type(x - a) # revealed: int reveal_type(x * a) # revealed: int reveal_type(x // a) # revealed: int - reveal_type(x / a) # revealed: int | float + reveal_type(x / a) # revealed: float reveal_type(x % a) # revealed: int def rhs_is_bool(x: bool): @@ -113,7 +113,7 @@ def _(a: bool): reveal_type(a - x) # revealed: int reveal_type(a * x) # revealed: int reveal_type(a // x) # revealed: int - reveal_type(a / x) # revealed: int | float + reveal_type(a / x) # revealed: float reveal_type(a % x) # revealed: int def both_are_bool(x: bool, y: bool): @@ -121,7 +121,7 @@ def _(a: bool): reveal_type(x - y) # revealed: int reveal_type(x * y) # revealed: int reveal_type(x // y) # revealed: int - reveal_type(x / y) # revealed: int | float + reveal_type(x / y) # revealed: float reveal_type(x % y) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/instances.md b/crates/ty_python_semantic/resources/mdtest/binary/instances.md index 3b8839588f..6782dbf908 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/instances.md @@ -405,17 +405,17 @@ dunder methods. Perhaps we could have a special-case on the special-case, to exc return annotations from the widening, and preserve a bit more precision here? ```py -reveal_type(3j + 3.14) # revealed: int | float | complex -reveal_type(4.2 + 42) # revealed: int | float -reveal_type(3j + 3) # revealed: int | float | complex -reveal_type(3.14 + 3j) # revealed: int | float | complex -reveal_type(42 + 4.2) # revealed: int | float -reveal_type(3 + 3j) # revealed: int | float | complex +reveal_type(3j + 3.14) # revealed: complex +reveal_type(4.2 + 42) # revealed: float +reveal_type(3j + 3) # revealed: complex +reveal_type(3.14 + 3j) # revealed: complex +reveal_type(42 + 4.2) # revealed: float +reveal_type(3 + 3j) # revealed: complex def _(x: bool, y: int): reveal_type(x + y) # revealed: int - reveal_type(4.2 + x) # revealed: int | float - reveal_type(y + 4.12) # revealed: int | float + reveal_type(4.2 + x) # revealed: float + reveal_type(y + 4.12) # revealed: float ``` ## With literal types diff --git a/crates/ty_python_semantic/resources/mdtest/binary/integers.md b/crates/ty_python_semantic/resources/mdtest/binary/integers.md index 3056198181..e9531d5dc5 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/integers.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/integers.md @@ -7,7 +7,7 @@ reveal_type(2 + 1) # revealed: Literal[3] reveal_type(3 - 4) # revealed: Literal[-1] reveal_type(3 * -1) # revealed: Literal[-3] reveal_type(-3 // 3) # revealed: Literal[-1] -reveal_type(-3 / 3) # revealed: float +reveal_type(-3 / 3) # revealed: float* reveal_type(5 % 3) # revealed: Literal[2] reveal_type(3 | 4) # revealed: Literal[7] reveal_type(5 & 6) # revealed: Literal[4] @@ -21,7 +21,7 @@ def lhs(x: int): reveal_type(x - 4) # revealed: int reveal_type(x * -1) # revealed: int reveal_type(x // 3) # revealed: int - reveal_type(x / 3) # revealed: int | float + reveal_type(x / 3) # revealed: float reveal_type(x % 3) # revealed: int def rhs(x: int): @@ -29,7 +29,7 @@ def rhs(x: int): reveal_type(3 - x) # revealed: int reveal_type(3 * x) # revealed: int reveal_type(-3 // x) # revealed: int - reveal_type(-3 / x) # revealed: int | float + reveal_type(-3 / x) # revealed: float reveal_type(5 % x) # revealed: int def both(x: int): @@ -37,7 +37,7 @@ def both(x: int): reveal_type(x - x) # revealed: int reveal_type(x * x) # revealed: int reveal_type(x // x) # revealed: int - reveal_type(x / x) # revealed: int | float + reveal_type(x / x) # revealed: float reveal_type(x % x) # revealed: int # Edge case: the runtime value is 9223372036854775808, @@ -70,8 +70,8 @@ reveal_type(1**0) # revealed: Literal[1] reveal_type(0**1) # revealed: Literal[0] reveal_type(0**0) # revealed: Literal[1] reveal_type((-1) ** 2) # revealed: Literal[1] -reveal_type(2 ** (-1)) # revealed: float -reveal_type((-1) ** (-1)) # revealed: float +reveal_type(2 ** (-1)) # revealed: float* +reveal_type((-1) ** (-1)) # revealed: float* ``` ## Division and Modulus @@ -117,7 +117,7 @@ subclass; we only emit the error if the LHS type is exactly `int` or `float`, no ```py a = 1 / 0 # error: "Cannot divide object of type `Literal[1]` by zero" -reveal_type(a) # revealed: float +reveal_type(a) # revealed: float* b = 2 // 0 # error: "Cannot floor divide object of type `Literal[2]` by zero" reveal_type(b) # revealed: int @@ -126,22 +126,22 @@ c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero" reveal_type(c) # revealed: int # error: "Cannot divide object of type `int` by zero" -reveal_type(int() / 0) # revealed: int | float +reveal_type(int() / 0) # revealed: float # error: "Cannot divide object of type `Literal[1]` by zero" -reveal_type(1 / False) # revealed: float +reveal_type(1 / False) # revealed: float* # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero" True / False # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero" bool(1) / False -# error: "Cannot divide object of type `float` by zero" -reveal_type(1.0 / 0) # revealed: int | float +# error: "Cannot divide object of type `float*` by zero" +reveal_type(1.0 / 0) # revealed: float class MyInt(int): ... # No error for a subclass of int -reveal_type(MyInt(3) / 0) # revealed: int | float +reveal_type(MyInt(3) / 0) # revealed: float ``` ## Bit-shifting diff --git a/crates/ty_python_semantic/resources/mdtest/binary/unions.md b/crates/ty_python_semantic/resources/mdtest/binary/unions.md index c450d8d8de..654719071b 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/unions.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/unions.md @@ -42,12 +42,12 @@ here: ```py def f4(x: float, y: float): - reveal_type(x + y) # revealed: int | float - reveal_type(x - y) # revealed: int | float - reveal_type(x * y) # revealed: int | float - reveal_type(x / y) # revealed: int | float - reveal_type(x // y) # revealed: int | float - reveal_type(x % y) # revealed: int | float + reveal_type(x + y) # revealed: float + reveal_type(x - y) # revealed: float + reveal_type(x * y) # revealed: float + reveal_type(x / y) # revealed: float + reveal_type(x // y) # revealed: float + reveal_type(x % y) # revealed: float ``` If any of the union elements leads to a division by zero, we will report an error: diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index 77db74c096..21bd125c8b 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -1023,7 +1023,7 @@ def _(args: tuple[str, str]) -> None: # But, with a fixed-length tuple that is too long, we get the expected error. def _(args: tuple[str, str, str]) -> None: - # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int | float`, found `str`" + # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `float`, found `str`" # error: [parameter-already-assigned] "Multiple values provided for parameter `c` of function `f`" f(*args, c=1.0) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/functools_partial.md b/crates/ty_python_semantic/resources/mdtest/call/functools_partial.md index 0b38c012ff..83c80a92cf 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/functools_partial.md +++ b/crates/ty_python_semantic/resources/mdtest/call/functools_partial.md @@ -57,7 +57,7 @@ def f(a: int, b: str, c: float) -> bool: return True p = partial(f, 1, c=3.14) -reveal_type(p) # revealed: partial[(b: str, *, c: int | float = ...) -> bool] +reveal_type(p) # revealed: partial[(b: str, *, c: float = ...) -> bool] ``` ### All args bound @@ -703,7 +703,7 @@ def f(a: int, b: str, c: float) -> bool: args: tuple[int, str] = (1, "hello") p = partial(f, *args) -reveal_type(p) # revealed: partial[(c: int | float) -> bool] +reveal_type(p) # revealed: partial[(c: float) -> bool] ``` ### Mixed positional and starred args @@ -716,7 +716,7 @@ def f(a: int, b: str, c: float) -> bool: args: tuple[str] = ("hello",) p = partial(f, 1, *args) -reveal_type(p) # revealed: partial[(c: int | float) -> bool] +reveal_type(p) # revealed: partial[(c: float) -> bool] ``` ### Fallback for starred args with variable-length tuple @@ -922,10 +922,10 @@ def f(a: int, b: str, c: float) -> bool: return True p1 = partial(f, 1) -reveal_type(p1) # revealed: partial[(b: str, c: int | float) -> bool] +reveal_type(p1) # revealed: partial[(b: str, c: float) -> bool] p2 = partial(p1, "hello") -reveal_type(p2) # revealed: partial[(c: int | float) -> bool] +reveal_type(p2) # revealed: partial[(c: float) -> bool] ``` ## Constructors and advanced signatures @@ -1192,7 +1192,7 @@ def f(a: int, b: str = "default", c: float = 0.0) -> bool: return True p = partial(f, 1, "hello") -reveal_type(p) # revealed: partial[(c: int | float = ...) -> bool] +reveal_type(p) # revealed: partial[(c: float = ...) -> bool] ``` ### Multiple keyword bindings @@ -1204,7 +1204,7 @@ def f(a: int, b: str, c: float, d: bool) -> int: return 0 p = partial(f, b="hello", d=True) -reveal_type(p) # revealed: partial[(a: int, *, b: str = "hello", c: int | float, d: bool = True) -> int] +reveal_type(p) # revealed: partial[(a: int, *, b: str = "hello", c: float, d: bool = True) -> int] ``` ### Mixed positional-only, regular, and keyword-only @@ -1217,15 +1217,15 @@ def f(a: int, /, b: str, *, c: float) -> bool: # Bind the positional-only param p1 = partial(f, 1) -reveal_type(p1) # revealed: partial[(b: str, *, c: int | float) -> bool] +reveal_type(p1) # revealed: partial[(b: str, *, c: float) -> bool] # Bind a keyword-only param by keyword p2 = partial(f, c=3.14) -reveal_type(p2) # revealed: partial[(a: int, /, b: str, *, c: int | float = ...) -> bool] +reveal_type(p2) # revealed: partial[(a: int, /, b: str, *, c: float = ...) -> bool] # Bind both positional-only and keyword-only p3 = partial(f, 1, c=3.14) -reveal_type(p3) # revealed: partial[(b: str, *, c: int | float = ...) -> bool] +reveal_type(p3) # revealed: partial[(b: str, *, c: float = ...) -> bool] ``` ### Starred args combined with keyword args @@ -1238,7 +1238,7 @@ def f(a: int, b: str, c: float) -> bool: args: tuple[int] = (1,) p = partial(f, *args, c=3.14) -reveal_type(p) # revealed: partial[(b: str, *, c: int | float = ...) -> bool] +reveal_type(p) # revealed: partial[(b: str, *, c: float = ...) -> bool] ``` ### Starred args with empty tuple @@ -1331,7 +1331,7 @@ def f(a: int, b: str, c: float) -> bool: return True p = partial(f, b="hello") -reveal_type(p) # revealed: partial[(a: int, *, b: str = "hello", c: int | float) -> bool] +reveal_type(p) # revealed: partial[(a: int, *, b: str = "hello", c: float) -> bool] # Override b at call time reveal_type(p(1, b="world", c=3.14)) # revealed: bool diff --git a/crates/ty_python_semantic/resources/mdtest/call/union.md b/crates/ty_python_semantic/resources/mdtest/call/union.md index 8f1272f748..88781cd899 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/union.md +++ b/crates/ty_python_semantic/resources/mdtest/call/union.md @@ -174,8 +174,8 @@ class IntDiag(DeferredDiagBase[int]): ... class StrDiag(DeferredDiagBase[str]): ... def _(factory: type[IntDiag] | type[StrDiag]): - # error: [invalid-argument-type] "Argument to `DeferredDiagBase.__init__` is incorrect: Expected `int`, found `float`" - # error: [invalid-argument-type] "Argument to `DeferredDiagBase.__init__` is incorrect: Expected `str`, found `float`" + # error: [invalid-argument-type] "Argument to `DeferredDiagBase.__init__` is incorrect: Expected `int`, found `float*`" + # error: [invalid-argument-type] "Argument to `DeferredDiagBase.__init__` is incorrect: Expected `str`, found `float*`" factory(1.2) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md index 837592fb10..0b4aa9d20c 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md @@ -329,13 +329,13 @@ reveal_type(1 >= 1.0) # revealed: bool reveal_type(1 == 2j) # revealed: bool reveal_type(1 != 2j) # revealed: bool -# error: [unsupported-operator] "Operator `<` is not supported between objects of type `Literal[1]` and `complex`" +# error: [unsupported-operator] "Operator `<` is not supported between objects of type `Literal[1]` and `complex*`" reveal_type(1 < 2j) # revealed: Unknown -# error: [unsupported-operator] "Operator `<=` is not supported between objects of type `Literal[1]` and `complex`" +# error: [unsupported-operator] "Operator `<=` is not supported between objects of type `Literal[1]` and `complex*`" reveal_type(1 <= 2j) # revealed: Unknown -# error: [unsupported-operator] "Operator `>` is not supported between objects of type `Literal[1]` and `complex`" +# error: [unsupported-operator] "Operator `>` is not supported between objects of type `Literal[1]` and `complex*`" reveal_type(1 > 2j) # revealed: Unknown -# error: [unsupported-operator] "Operator `>=` is not supported between objects of type `Literal[1]` and `complex`" +# error: [unsupported-operator] "Operator `>=` is not supported between objects of type `Literal[1]` and `complex*`" reveal_type(1 >= 2j) # revealed: Unknown def f(x: bool, y: int): diff --git a/crates/ty_python_semantic/resources/mdtest/cycle.md b/crates/ty_python_semantic/resources/mdtest/cycle.md index fa166a9db6..0e808c3477 100644 --- a/crates/ty_python_semantic/resources/mdtest/cycle.md +++ b/crates/ty_python_semantic/resources/mdtest/cycle.md @@ -164,7 +164,7 @@ JSONPrimitive = Union[str, int, float, bool, None] JSONValue = TypeAliasType("JSONValue", 'Union[JSONPrimitive, Sequence["JSONValue"], Mapping[str, "JSONValue"]]') def _(x: JSONValue): - reveal_type(x) # revealed: Sequence[JSONValue] | int | float | None | Mapping[str, JSONValue] + reveal_type(x) # revealed: Sequence[JSONValue] | float | None | Mapping[str, JSONValue] ``` ## Self-referential legacy type variables diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md index 0434c3bbe5..e6b431a7ec 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md @@ -2072,7 +2072,7 @@ WithClassConverter("1", "2.5") with_class_converter = WithClassConverter("1", "2.5") reveal_type(with_class_converter.a) # revealed: PermissiveNumber -reveal_type(with_class_converter.b) # revealed: int | float +reveal_type(with_class_converter.b) # revealed: float with_class_converter.a = "2" with_class_converter.a = 1.5 # error: [invalid-assignment] diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index 351da6f068..338d4a07dc 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -771,7 +771,7 @@ class Derived(Base): @other.setter def other(self, v: float) -> None: - reveal_type(self.value) # revealed: int | float + reveal_type(self.value) # revealed: float self.value = v ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md index dfe995cf9c..4e69c2a7a9 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md @@ -580,8 +580,8 @@ error[invalid-argument-type]: Argument to function `f` is incorrect --> src/mdtest_snippet.py:8:7 | 8 | f(x) # snapshot: invalid-argument-type - | ^ Expected `Number`, found `int | float` -info: element `int` of union `int | float` is not assignable to `Number` + | ^ Expected `Number`, found `float` +info: element `int` of union `int | float*` is not assignable to `Number` info: Function defined here --> src/mdtest_snippet.py:3:5 | diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 44fcbeba02..8f9d959b0c 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -2992,8 +2992,8 @@ reveal_type(StringyNames.A.value) # revealed: Literal["1"] reveal_type(StringyNames.B.value) # revealed: Literal["2"] reveal_type(BytesyNames.A.value) # revealed: bytes reveal_type(BytesyNames.B.value) # revealed: bytes -reveal_type(FloatyNames.A.value) # revealed: float -reveal_type(FloatyNames.B.value) # revealed: float +reveal_type(FloatyNames.A.value) # revealed: float* +reveal_type(FloatyNames.B.value) # revealed: float* # revealed: tuple[Literal["A"], Literal["B"]] reveal_type(enum_members(StringyNames)) 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 0469cec067..7a5b5a0da7 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -876,7 +876,7 @@ def test_seq(x: Sequence[T]) -> Sequence[T]: return x def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: tuple[()]): - reveal_type(test_seq(t1)) # revealed: Sequence[int | float | complex | list[int]] + reveal_type(test_seq(t1)) # revealed: Sequence[complex | list[int]] reveal_type(test_seq(t2)) # revealed: Sequence[int | str] reveal_type(test_seq(t3)) # revealed: Sequence[Never] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index 502e6fa6b0..2a5b233cbd 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -58,7 +58,7 @@ def f(x: T) -> T: return x reveal_type(f(1)) # revealed: Literal[1] -reveal_type(f(1.0)) # revealed: float +reveal_type(f(1.0)) # revealed: float* reveal_type(f(True)) # revealed: Literal[True] reveal_type(f("string")) # revealed: Literal["string"] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md index c58be073c8..6b7d39b442 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -442,7 +442,7 @@ T3 = TypeVar("T3", bound=str) # and the upper bound of `T` (`int`) is assignable to `int | float` S = TypeVar("S", default=T1, bound=float) -# error: [invalid-type-variable-default] "Default `T3` of TypeVar `U` is not assignable to upper bound `int | float` of `U` because its upper bound `str` is not assignable to `int | float`" +# error: [invalid-type-variable-default] "Default `T3` of TypeVar `U` is not assignable to upper bound `float` of `U` because its upper bound `str` is not assignable to `float`" U = TypeVar("U", default=T3, bound=float) ``` @@ -576,7 +576,7 @@ T = TypeVar("T", int, bool) reveal_type(T.__constraints__) # revealed: tuple[int, bool] S = TypeVar("S", float, str) -reveal_type(S.__constraints__) # revealed: tuple[int | float, str] +reveal_type(S.__constraints__) # revealed: tuple[float, str] ``` ### Cannot have only one constraint 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 f4c119a04b..48a9d0e383 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -590,7 +590,7 @@ def test_seq[T](x: Sequence[T]) -> Sequence[T]: return x def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: tuple[()]): - reveal_type(test_seq(t1)) # revealed: Sequence[int | float | complex | list[int]] + reveal_type(test_seq(t1)) # revealed: Sequence[complex | list[int]] reveal_type(test_seq(t2)) # revealed: Sequence[int | str] reveal_type(test_seq(t3)) # revealed: Sequence[Never] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md index 6cdf4ee465..917f1cfebf 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/concatenate.md @@ -450,7 +450,7 @@ def my_handler(env: str, x: int, y: float) -> bool: return True m = Middleware(my_handler) -reveal_type(m) # revealed: Middleware[(x: int, y: int | float), bool] +reveal_type(m) # revealed: Middleware[(x: int, y: float), bool] ``` ### Specializing `ParamSpec` with `Concatenate` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 9f0738a4cc..e639338822 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -77,7 +77,7 @@ def f[T](x: T) -> T: return x reveal_type(f(1)) # revealed: Literal[1] -reveal_type(f(1.0)) # revealed: float +reveal_type(f(1.0)) # revealed: float* reveal_type(f(True)) # revealed: Literal[True] reveal_type(f("string")) # revealed: Literal["string"] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index 965dff1cdb..8fb74af2aa 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -1457,7 +1457,7 @@ reveal_type(identity(1)) # revealed: Literal[1] reveal_type(identity("hello")) # revealed: Literal["hello"] reveal_type(pair(1, "a")) # revealed: tuple[Literal[1], Literal["a"]] -reveal_type(pair("x", 2.5)) # revealed: tuple[Literal["x"], float] +reveal_type(pair("x", 2.5)) # revealed: tuple[Literal["x"], float*] ``` ### Chained decorators with generic functions diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md index 3ab315003e..a95201a932 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variables.md @@ -124,7 +124,7 @@ When the default is a TypeVar, its upper bound must be assignable to the outer T def f[T1: int, S: float = T1](): ... # `T3` has bound `str`, which is not assignable to `int | float` -# error: [invalid-type-variable-default] "Default `T3` of TypeVar `U` is not assignable to upper bound `int | float` of `U` because its upper bound `str` is not assignable to `int | float`" +# error: [invalid-type-variable-default] "Default `T3` of TypeVar `U` is not assignable to upper bound `float` of `U` because its upper bound `str` is not assignable to `float`" def g[T3: str, U: float = T3](): ... ``` diff --git a/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md b/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md index 4c896e8bd9..7dc6cd2479 100644 --- a/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md +++ b/crates/ty_python_semantic/resources/mdtest/import/stub_packages.md @@ -126,7 +126,7 @@ from shapes.polygons.hexagon import Hexagon from shapes.polygons.pentagon import Pentagon reveal_type(Pentagon().sides) # revealed: int -reveal_type(Hexagon().area) # revealed: int | float +reveal_type(Hexagon().area) # revealed: float ``` ## Manual overrides from extra paths @@ -606,7 +606,7 @@ from shapes.polygons.pentagon import Pentagon from shapes.polygons.hexagon import Hexagon reveal_type(Pentagon().sides) # revealed: int -reveal_type(Hexagon().area) # revealed: int | float +reveal_type(Hexagon().area) # revealed: float ``` ## Stub package using `__init__.py` over `.pyi` @@ -644,7 +644,7 @@ class Hexagon: ... from shapes import Hexagon, Pentagon reveal_type(Pentagon().sides) # revealed: int -reveal_type(Hexagon().area) # revealed: int | float +reveal_type(Hexagon().area) # revealed: float ``` ## Relative import in stub package diff --git a/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md b/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md index c6516bf7c4..ce9b5895c1 100644 --- a/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md +++ b/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md @@ -71,5 +71,5 @@ _ScalarT = TypeVar("_ScalarT", bound=np.generic) def from_dtype_like(value: np._DTypeLike[_ScalarT]) -> np.dtype[_ScalarT]: raise NotImplementedError -reveal_type(from_dtype_like(np.bool)) # revealed: dtype[bool[bool]] +reveal_type(from_dtype_like(np.bool)) # revealed: dtype[mini_numpy.bool[builtins.bool]] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md index 084d9545ef..139553ca35 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/collections/dictionary.md @@ -240,7 +240,7 @@ def _(x: dict[str, dict[str, float | str]]): # A rejected dictionary assignment does not establish known key types. # error: [invalid-assignment] x["kwargs"] = {"nested": {"a": 1}} - reveal_type(x["kwargs"]["nested"]) # revealed: int | float | str + reveal_type(x["kwargs"]["nested"]) # revealed: float | str # error: [invalid-argument-type] f1(**x["kwargs"]) @@ -251,13 +251,13 @@ def _(x: dict[str, dict[str, float | str]]): # A rejected replacement also invalidates a prior known-key type. # error: [invalid-assignment] x["kwargs"] = {"nested": {"a": 1}} - reveal_type(x["kwargs"]["nested"]) # revealed: int | float | str + reveal_type(x["kwargs"]["nested"]) # revealed: float | str def accepts_value(**kwargs: object): ... def _(x: dict[str, dict[str, float | str]]): # error: [invalid-assignment] x = {"kwargs": {"nested": {"a": object()}}} - reveal_type(x["kwargs"]["nested"]) # revealed: int | float | str + reveal_type(x["kwargs"]["nested"]) # revealed: float | str # error: [invalid-argument-type] accepts_value(**x["kwargs"]["nested"]) diff --git a/crates/ty_python_semantic/resources/mdtest/literal/complex.md b/crates/ty_python_semantic/resources/mdtest/literal/complex.md index 4071a041f1..ff5cb69e17 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/complex.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/complex.md @@ -3,5 +3,5 @@ ## Complex numbers ```py -reveal_type(2j) # revealed: complex +reveal_type(2j) # revealed: complex* ``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/float.md b/crates/ty_python_semantic/resources/mdtest/literal/float.md index e4b0597ff2..6f2e48070c 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/float.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/float.md @@ -3,5 +3,5 @@ ## Basic ```py -reveal_type(1.0) # revealed: float +reveal_type(1.0) # revealed: float* ``` diff --git a/crates/ty_python_semantic/resources/mdtest/literal/integer.md b/crates/ty_python_semantic/resources/mdtest/literal/integer.md index 338a3b6107..97ca9d692e 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal/integer.md +++ b/crates/ty_python_semantic/resources/mdtest/literal/integer.md @@ -46,11 +46,11 @@ reveal_type(z) # revealed: Literal[987] ## Floats ```py -reveal_type(1.0) # revealed: float +reveal_type(1.0) # revealed: float* ``` ## Complex ```py -reveal_type(2j) # revealed: complex +reveal_type(2j) # revealed: complex* ``` diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index 4543bfce60..a10aea4fbe 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -211,7 +211,7 @@ class Point(NamedTuple("Point", [("x", int), ("y", int)])): p = Point(3, 4) reveal_type(p.x) # revealed: int reveal_type(p.y) # revealed: int -reveal_type(p.magnitude()) # revealed: int | float +reveal_type(p.magnitude()) # revealed: float ``` String annotations in dangling calls work correctly for forward references to classes defined in the @@ -1197,11 +1197,11 @@ class Property[T](NamedTuple): name: str value: T -reveal_type(Property("height", 3.4)) # revealed: Property[float] +reveal_type(Property("height", 3.4)) # revealed: Property[float*] reveal_type(Property.value) # revealed: property reveal_type(Property.value.fget) # revealed: (self, /) -> Unknown reveal_type(Property[str].value.fget) # revealed: (self, /) -> str -reveal_type(Property("height", 3.4).value) # revealed: float +reveal_type(Property("height", 3.4).value) # revealed: float* T = TypeVar("T") @@ -1213,7 +1213,7 @@ reveal_type(LegacyProperty("height", 42)) # revealed: LegacyProperty[int] reveal_type(LegacyProperty.value) # revealed: property reveal_type(LegacyProperty.value.fget) # revealed: (self, /) -> Unknown reveal_type(LegacyProperty[str].value.fget) # revealed: (self, /) -> str -reveal_type(LegacyProperty("height", 3.4).value) # revealed: int | float +reveal_type(LegacyProperty("height", 3.4).value) # revealed: float ``` ### Functional syntax with generics @@ -2030,5 +2030,5 @@ class GenericChild(GenericBase[T]): reveal_type(instance) # revealed: Self@__new__ return instance -reveal_type(GenericChild(x=3.14)) # revealed: GenericChild[int | float] +reveal_type(GenericChild(x=3.14)) # revealed: GenericChild[float] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index 0d266c2a34..5b4b938112 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -3079,17 +3079,17 @@ class C: def _(x: Literal["foo", "bar", 42, b"foo"] | bool | complex): match x: case "foo": - reveal_type(x) # revealed: Literal["foo"] | float | complex + reveal_type(x) # revealed: Literal["foo"] | float* | complex* case 42: - reveal_type(x) # revealed: Literal[42] | float | complex + reveal_type(x) # revealed: Literal[42] | float* | complex* case 6.0: - reveal_type(x) # revealed: Literal["bar", b"foo"] | (int & ~Literal[42]) | float | complex + reveal_type(x) # revealed: Literal["bar", b"foo"] | (int & ~Literal[42]) | float* | complex* case 1j: - reveal_type(x) # revealed: Literal["bar", b"foo"] | (int & ~Literal[42]) | float | complex + reveal_type(x) # revealed: Literal["bar", b"foo"] | (int & ~Literal[42]) | float* | complex* case b"foo": - reveal_type(x) # revealed: Literal[b"foo"] | float | complex + reveal_type(x) # revealed: Literal[b"foo"] | float* | complex* case _: - reveal_type(x) # revealed: Literal["bar"] | (int & ~Literal[42]) | float | complex + reveal_type(x) # revealed: Literal["bar"] | (int & ~Literal[42]) | float* | complex* ``` The same limitation applies inside a sequence. Matching a literal proves only that the element diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md index ce737c8ea3..0b08d4efc0 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md @@ -141,7 +141,7 @@ def bar(world: str, *args, **kwargs) -> float: x = foo if flag() else bar if x: - reveal_type(x) # revealed: (def foo(hello: int) -> bytes) | (def bar(world: str, *args, **kwargs) -> int | float) + reveal_type(x) # revealed: (def foo(hello: int) -> bytes) | (def bar(world: str, *args, **kwargs) -> float) else: reveal_type(x) # revealed: Never ``` @@ -589,7 +589,7 @@ def f(floaty: FloatNewType, complexy: ComplexNewType): if complexy: reveal_type(complexy) # revealed: ComplexNewType & ~AlwaysFalsy - reveal_type(complexy.real) # revealed: int | float + reveal_type(complexy.real) # revealed: float expects_complex(complexy) # fine expects_float(complexy) # error: [invalid-argument-type] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index df101cb4b9..9871446596 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -248,7 +248,7 @@ def _(a: object, flag: bool) -> TypeGuard[str]: # error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `TypeIs[str]`" def f(a: object, flag: bool) -> TypeIs[str]: if flag: - # error: [invalid-return-type] "Return type does not match returned value: expected `TypeIs[str]`, found `float`" + # error: [invalid-return-type] "Return type does not match returned value: expected `TypeIs[str]`, found `float*`" return 1.2 def g(a: Literal["foo", "bar"]) -> TypeIs[Literal["foo"]]: diff --git a/crates/ty_python_semantic/resources/mdtest/promotion.md b/crates/ty_python_semantic/resources/mdtest/promotion.md index bb7fb5875b..20f0e78b4a 100644 --- a/crates/ty_python_semantic/resources/mdtest/promotion.md +++ b/crates/ty_python_semantic/resources/mdtest/promotion.md @@ -55,12 +55,12 @@ reveal_type(x4) # revealed: Literal[MyEnum.A] reveal_type(promote(x4)) # revealed: list[MyEnum] x5 = 3.14 -reveal_type(x5) # revealed: float -reveal_type(promote(x5)) # revealed: list[int | float] +reveal_type(x5) # revealed: float* +reveal_type(promote(x5)) # revealed: list[float] x6 = 3.14j -reveal_type(x6) # revealed: complex -reveal_type(promote(x6)) # revealed: list[int | float | complex] +reveal_type(x6) # revealed: complex* +reveal_type(promote(x6)) # revealed: list[complex] def _(source: Literal["foo", "bar"]): x7 = f"hello" diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index e4d6558ce3..ff0b8d7f9e 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -2434,7 +2434,7 @@ class ReadClass(Protocol[JustT_co]): def __class__(self, /) -> type[JustT_co]: ... def takes_read_class(value: ReadClass[float]) -> None: - reveal_type(value) # revealed: ReadClass[int | float] + reveal_type(value) # revealed: ReadClass[float] takes_read_class(1) takes_read_class(1.0) @@ -2446,7 +2446,7 @@ class WritableValue(Protocol[JustT]): def value(self, value: type[JustT], /) -> None: ... def takes_writable_value(value: WritableValue[float]) -> None: - reveal_type(value) # revealed: WritableValue[int | float] + reveal_type(value) # revealed: WritableValue[float] ``` A read/write property on a protocol, where the setter accepts a subtype of the type returned by the diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md b/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md index 1889f209d9..7ee16f3b5e 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/nonlocal.md @@ -495,7 +495,7 @@ def _(maybe_float: float | None, certain_int: int, flag: bool) -> None: if flag: x = certain_int assert x is not None - reveal_type(x) # revealed: int | float + reveal_type(x) # revealed: float +x ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" index 60fc839161..34c9daec1b 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/assignment_diagnosti\342\200\246_-_Subscript_assignment\342\200\246_-_Wrong_value_type_for\342\200\246_(ffe39a3bae68cfe4).snap" @@ -22,25 +22,25 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia # Diagnostics ``` -error[invalid-assignment]: Invalid subscript assignment with key of type `Literal["retries"]` and value of type `float` on object of type `dict[str, int]` +error[invalid-assignment]: Invalid subscript assignment with key of type `Literal["retries"]` and value of type `float*` on object of type `dict[str, int]` --> src/mdtest_snippet.py:4:5 | 4 | config["retries"] = 3.0 | ^^^^^^^^^^^^^^^^^^^^--- | | - | Expected value of type `int`, got `float` + | Expected value of type `int`, got `float*` info: The full type of the subscripted object is `dict[str, int] | dict[str, str]` ``` ``` -error[invalid-assignment]: Invalid subscript assignment with key of type `Literal["retries"]` and value of type `float` on object of type `dict[str, str]` +error[invalid-assignment]: Invalid subscript assignment with key of type `Literal["retries"]` and value of type `float*` on object of type `dict[str, str]` --> src/mdtest_snippet.py:4:5 | 4 | config["retries"] = 3.0 | ^^^^^^^^^^^^^^^^^^^^--- | | - | Expected value of type `str`, got `float` + | Expected value of type `str`, got `float*` info: The full type of the subscripted object is `dict[str, int] | dict[str, str]` ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(dd80c593d9136f35).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(dd80c593d9136f35).snap" index a5452d1ef7..884cf4ab9c 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(dd80c593d9136f35).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(dd80c593d9136f35).snap" @@ -87,19 +87,19 @@ info: (a: str, b: str, c: int) -> Unknown info: (a: int, b: str, c: str) -> Unknown info: (a: str, b: str, c: str) -> Unknown info: (a: int, b: int, c: int) -> Unknown -info: (a: int | float, b: int, c: int) -> Unknown -info: (a: int, b: int | float, c: int) -> Unknown -info: (a: int, b: int, c: int | float) -> Unknown -info: (a: int | float, b: int | float, c: int) -> Unknown -info: (a: int, b: int | float, c: int | float) -> Unknown -info: (a: int | float, b: int | float, c: int | float) -> Unknown +info: (a: float, b: int, c: int) -> Unknown +info: (a: int, b: float, c: int) -> Unknown +info: (a: int, b: int, c: float) -> Unknown +info: (a: float, b: float, c: int) -> Unknown +info: (a: int, b: float, c: float) -> Unknown +info: (a: float, b: float, c: float) -> Unknown info: (a: str, b: str, c: str) -> Unknown -info: (a: int | float, b: str, c: str) -> Unknown -info: (a: str, b: int | float, c: str) -> Unknown -info: (a: str, b: str, c: int | float) -> Unknown -info: (a: int | float, b: int | float, c: str) -> Unknown -info: (a: str, b: int | float, c: int | float) -> Unknown -info: (a: int | float, b: int | float, c: int | float) -> Unknown +info: (a: float, b: str, c: str) -> Unknown +info: (a: str, b: float, c: str) -> Unknown +info: (a: str, b: str, c: float) -> Unknown +info: (a: float, b: float, c: str) -> Unknown +info: (a: str, b: float, c: float) -> Unknown +info: (a: float, b: float, c: float) -> Unknown info: Overload implementation defined here --> src/mdtest_snippet.py:47:5 | diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(f66e3a8a3977c472).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(f66e3a8a3977c472).snap" index 8d1907ec94..982ecbbbbe 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(f66e3a8a3977c472).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload\342\200\246_-_No_matching_overload\342\200\246_-_Call_to_function_wit\342\200\246_(f66e3a8a3977c472).snap" @@ -167,19 +167,19 @@ info: (a: str, b: str, c: int) -> Unknown info: (a: int, b: str, c: str) -> Unknown info: (a: str, b: str, c: str) -> Unknown info: (a: int, b: int, c: int) -> Unknown -info: (a: int | float, b: int, c: int) -> Unknown -info: (a: int, b: int | float, c: int) -> Unknown -info: (a: int, b: int, c: int | float) -> Unknown -info: (a: int | float, b: int | float, c: int) -> Unknown -info: (a: int, b: int | float, c: int | float) -> Unknown -info: (a: int | float, b: int | float, c: int | float) -> Unknown +info: (a: float, b: int, c: int) -> Unknown +info: (a: int, b: float, c: int) -> Unknown +info: (a: int, b: int, c: float) -> Unknown +info: (a: float, b: float, c: int) -> Unknown +info: (a: int, b: float, c: float) -> Unknown +info: (a: float, b: float, c: float) -> Unknown info: (a: str, b: str, c: str) -> Unknown -info: (a: int | float, b: str, c: str) -> Unknown -info: (a: str, b: int | float, c: str) -> Unknown -info: (a: str, b: str, c: int | float) -> Unknown -info: (a: int | float, b: int | float, c: str) -> Unknown -info: (a: str, b: int | float, c: int | float) -> Unknown -info: (a: int | float, b: int | float, c: int | float) -> Unknown +info: (a: float, b: str, c: str) -> Unknown +info: (a: str, b: float, c: str) -> Unknown +info: (a: str, b: str, c: float) -> Unknown +info: (a: float, b: float, c: str) -> Unknown +info: (a: str, b: float, c: float) -> Unknown +info: (a: float, b: float, c: float) -> Unknown info: (a: list[int], b: list[int], c: list[int]) -> Unknown info: (a: list[str], b: list[int], c: list[int]) -> Unknown info: (a: list[int], b: list[str], c: list[int]) -> Unknown @@ -188,19 +188,19 @@ info: (a: list[str], b: list[str], c: list[int]) -> Unknown info: (a: list[int], b: list[str], c: list[str]) -> Unknown info: (a: list[str], b: list[str], c: list[str]) -> Unknown info: (a: list[int], b: list[int], c: list[int]) -> Unknown -info: (a: list[int | float], b: list[int], c: list[int]) -> Unknown -info: (a: list[int], b: list[int | float], c: list[int]) -> Unknown -info: (a: list[int], b: list[int], c: list[int | float]) -> Unknown -info: (a: list[int | float], b: list[int | float], c: list[int]) -> Unknown -info: (a: list[int], b: list[int | float], c: list[int | float]) -> Unknown -info: (a: list[int | float], b: list[int | float], c: list[int | float]) -> Unknown +info: (a: list[float], b: list[int], c: list[int]) -> Unknown +info: (a: list[int], b: list[float], c: list[int]) -> Unknown +info: (a: list[int], b: list[int], c: list[float]) -> Unknown +info: (a: list[float], b: list[float], c: list[int]) -> Unknown +info: (a: list[int], b: list[float], c: list[float]) -> Unknown +info: (a: list[float], b: list[float], c: list[float]) -> Unknown info: (a: list[str], b: list[str], c: list[str]) -> Unknown -info: (a: list[int | float], b: list[str], c: list[str]) -> Unknown -info: (a: list[str], b: list[int | float], c: list[str]) -> Unknown -info: (a: list[str], b: list[str], c: list[int | float]) -> Unknown -info: (a: list[int | float], b: list[int | float], c: list[str]) -> Unknown -info: (a: list[str], b: list[int | float], c: list[int | float]) -> Unknown -info: (a: list[int | float], b: list[int | float], c: list[int | float]) -> Unknown +info: (a: list[float], b: list[str], c: list[str]) -> Unknown +info: (a: list[str], b: list[float], c: list[str]) -> Unknown +info: (a: list[str], b: list[str], c: list[float]) -> Unknown +info: (a: list[float], b: list[float], c: list[str]) -> Unknown +info: (a: list[str], b: list[float], c: list[float]) -> Unknown +info: (a: list[float], b: list[float], c: list[float]) -> Unknown info: (a: bool, b: bool, c: bool) -> Unknown info: (a: str, b: bool, c: bool) -> Unknown info: (a: bool, b: str, c: bool) -> Unknown diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Call_to_function_wit\342\200\246_(8fdf5a06afc7d4fe).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Call_to_function_wit\342\200\246_(8fdf5a06afc7d4fe).snap" index 930e95da09..ba81c8c5b3 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Call_to_function_wit\342\200\246_(8fdf5a06afc7d4fe).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/single_matching_over\342\200\246_-_Single_matching_over\342\200\246_-_Call_to_function_wit\342\200\246_(8fdf5a06afc7d4fe).snap" @@ -173,19 +173,19 @@ info: (a: str, b: str, c: int) -> Unknown info: (a: int, b: str, c: str) -> Unknown info: (a: str, b: str, c: str) -> Unknown info: (a: int, b: int, c: int) -> Unknown -info: (a: int | float, b: int, c: int) -> Unknown -info: (a: int, b: int | float, c: int) -> Unknown -info: (a: int, b: int, c: int | float) -> Unknown -info: (a: int | float, b: int | float, c: int) -> Unknown -info: (a: int, b: int | float, c: int | float) -> Unknown -info: (a: int | float, b: int | float, c: int | float) -> Unknown +info: (a: float, b: int, c: int) -> Unknown +info: (a: int, b: float, c: int) -> Unknown +info: (a: int, b: int, c: float) -> Unknown +info: (a: float, b: float, c: int) -> Unknown +info: (a: int, b: float, c: float) -> Unknown +info: (a: float, b: float, c: float) -> Unknown info: (a: str, b: str, c: str) -> Unknown -info: (a: int | float, b: str, c: str) -> Unknown -info: (a: str, b: int | float, c: str) -> Unknown -info: (a: str, b: str, c: int | float) -> Unknown -info: (a: int | float, b: int | float, c: str) -> Unknown -info: (a: str, b: int | float, c: int | float) -> Unknown -info: (a: int | float, b: int | float, c: int | float) -> Unknown +info: (a: float, b: str, c: str) -> Unknown +info: (a: str, b: float, c: str) -> Unknown +info: (a: str, b: str, c: float) -> Unknown +info: (a: float, b: float, c: str) -> Unknown +info: (a: str, b: float, c: float) -> Unknown +info: (a: float, b: float, c: float) -> Unknown info: (a: list[int], b: list[int], c: list[int]) -> Unknown info: (a: list[str], b: list[int], c: list[int]) -> Unknown info: (a: list[int], b: list[str], c: list[int]) -> Unknown @@ -194,19 +194,19 @@ info: (a: list[str], b: list[str], c: list[int]) -> Unknown info: (a: list[int], b: list[str], c: list[str]) -> Unknown info: (a: list[str], b: list[str], c: list[str]) -> Unknown info: (a: list[int], b: list[int], c: list[int]) -> Unknown -info: (a: list[int | float], b: list[int], c: list[int]) -> Unknown -info: (a: list[int], b: list[int | float], c: list[int]) -> Unknown -info: (a: list[int], b: list[int], c: list[int | float]) -> Unknown -info: (a: list[int | float], b: list[int | float], c: list[int]) -> Unknown -info: (a: list[int], b: list[int | float], c: list[int | float]) -> Unknown -info: (a: list[int | float], b: list[int | float], c: list[int | float]) -> Unknown +info: (a: list[float], b: list[int], c: list[int]) -> Unknown +info: (a: list[int], b: list[float], c: list[int]) -> Unknown +info: (a: list[int], b: list[int], c: list[float]) -> Unknown +info: (a: list[float], b: list[float], c: list[int]) -> Unknown +info: (a: list[int], b: list[float], c: list[float]) -> Unknown +info: (a: list[float], b: list[float], c: list[float]) -> Unknown info: (a: list[str], b: list[str], c: list[str]) -> Unknown -info: (a: list[int | float], b: list[str], c: list[str]) -> Unknown -info: (a: list[str], b: list[int | float], c: list[str]) -> Unknown -info: (a: list[str], b: list[str], c: list[int | float]) -> Unknown -info: (a: list[int | float], b: list[int | float], c: list[str]) -> Unknown -info: (a: list[str], b: list[int | float], c: list[int | float]) -> Unknown -info: (a: list[int | float], b: list[int | float], c: list[int | float]) -> Unknown +info: (a: list[float], b: list[str], c: list[str]) -> Unknown +info: (a: list[str], b: list[float], c: list[str]) -> Unknown +info: (a: list[str], b: list[str], c: list[float]) -> Unknown +info: (a: list[float], b: list[float], c: list[str]) -> Unknown +info: (a: list[str], b: list[float], c: list[float]) -> Unknown +info: (a: list[float], b: list[float], c: list[float]) -> Unknown info: (a: bool, b: bool, c: bool) -> Unknown info: (a: str, b: bool, c: bool) -> Unknown info: (a: bool, b: str, c: bool) -> Unknown diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Default_TypeVar's_bo\342\200\246_(fcd7ad5416c91629).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Default_TypeVar's_bo\342\200\246_(fcd7ad5416c91629).snap" index 4e0073ed49..ad9f6032e7 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Default_TypeVar's_bo\342\200\246_(fcd7ad5416c91629).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/variables.md_-_Legacy_type_variable\342\200\246_-_Type_variables_-_Invalid_defaults_-_Default_TypeVar's_bo\342\200\246_(fcd7ad5416c91629).snap" @@ -23,7 +23,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/variable 8 | # and the upper bound of `T` (`int`) is assignable to `int | float` 9 | S = TypeVar("S", default=T1, bound=float) 10 | -11 | # error: [invalid-type-variable-default] "Default `T3` of TypeVar `U` is not assignable to upper bound `int | float` of `U` because its upper bound `str` is not assignable to `int | float`" +11 | # error: [invalid-type-variable-default] "Default `T3` of TypeVar `U` is not assignable to upper bound `float` of `U` because its upper bound `str` is not assignable to `float`" 12 | U = TypeVar("U", default=T3, bound=float) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/struct_unpack.md b/crates/ty_python_semantic/resources/mdtest/struct_unpack.md index 1d87aaee8d..16da863c01 100644 --- a/crates/ty_python_semantic/resources/mdtest/struct_unpack.md +++ b/crates/ty_python_semantic/resources/mdtest/struct_unpack.md @@ -20,15 +20,15 @@ def _(buf: bytes): reveal_type(unpack("0c", buf)) # revealed: tuple[()] reveal_type(unpack("1s", buf)) # revealed: tuple[bytes] reveal_type(unpack("255s", buf)) # revealed: tuple[bytes] - reveal_type(unpack("e", buf)) # revealed: tuple[float] - reveal_type(unpack("2e", buf)) # revealed: tuple[float, float] - reveal_type(unpack("e4x", buf)) # revealed: tuple[float] - reveal_type(unpack("3eH", buf)) # revealed: tuple[float, float, float, int] + reveal_type(unpack("e", buf)) # revealed: tuple[float*] + reveal_type(unpack("2e", buf)) # revealed: tuple[float*, float*] + reveal_type(unpack("e4x", buf)) # revealed: tuple[float*] + reveal_type(unpack("3eH", buf)) # revealed: tuple[float*, float*, float*, int] reveal_type(unpack("?x?", buf)) # revealed: tuple[bool, bool] reveal_type(unpack("2?", buf)) # revealed: tuple[bool, bool] reveal_type(unpack("?2xI", buf)) # revealed: tuple[bool, int] - reveal_type(unpack("fd4x", buf)) # revealed: tuple[float, float] - reveal_type(unpack("d2xH", buf)) # revealed: tuple[float, int] + reveal_type(unpack("fd4x", buf)) # revealed: tuple[float*, float*] + reveal_type(unpack("d2xH", buf)) # revealed: tuple[float*, int] reveal_type(unpack("2i4x2h", buf)) # revealed: tuple[int, int, int, int] reveal_type(unpack("iP", buf)) # revealed: tuple[int, int] reveal_type(unpack("@n2xN", buf)) # revealed: tuple[int, int] @@ -46,8 +46,8 @@ python-version = "3.14" from struct import * def _(buf: bytes): - reveal_type(unpack("2F", buf)) # revealed: tuple[complex, complex] - reveal_type(unpack("3D", buf)) # revealed: tuple[complex, complex, complex] + reveal_type(unpack("2F", buf)) # revealed: tuple[complex*, complex*] + reveal_type(unpack("3D", buf)) # revealed: tuple[complex*, complex*, complex*] ``` ## Escape Large Repetition Counts diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md index f6690dc287..af3241bb78 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/tuple.md @@ -129,9 +129,9 @@ from ty_extensions._internal import reveal_mro reveal_type(os.stat("my_file.txt")) # revealed: stat_result reveal_type(os.stat("my_file.txt")[stat.ST_MODE]) # revealed: int -reveal_type(os.stat("my_file.txt")[stat.ST_ATIME]) # revealed: int | float +reveal_type(os.stat("my_file.txt")[stat.ST_ATIME]) # revealed: float -# revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) +# revealed: (, , , , , , , , typing.Protocol, typing.Generic, ) reveal_mro(os.stat_result) # There are no specific overloads for the `float` elements in `os.stat_result`, @@ -139,7 +139,7 @@ reveal_mro(os.stat_result) # gives the right result for those elements in the tuple, and we aim to synthesize # the minimum number of overloads for any given tuple # -# revealed: Overload[(self, index: Literal[-10, -9, -8, -7, -6, -5, -4, 0, 1, 2, 3, 4, 5, 6], /) -> int, (self, index: SupportsIndex, /) -> int | float, (self, index: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> tuple[int | float, ...]] +# revealed: Overload[(self, index: Literal[-10, -9, -8, -7, -6, -5, -4, 0, 1, 2, 3, 4, 5, 6], /) -> int, (self, index: SupportsIndex, /) -> float, (self, index: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> tuple[float, ...]] reveal_type(os.stat_result.__getitem__) ``` @@ -496,7 +496,7 @@ def test(val: tuple[str] | tuple[int]): reveal_type(val[0]) # revealed: str | int def test2(val: tuple[str, None] | list[int | float]): - reveal_type(val[0]) # revealed: str | int | float + reveal_type(val[0]) # revealed: str | float ``` ## Union subscript access with non-indexable type diff --git a/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md b/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md index 1a74ed0327..a235ed811f 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md @@ -47,11 +47,11 @@ from typing import TypeVar T = TypeVar("T", bound=float) def neg_float_bound(a: T) -> float: - reveal_type(-a) # revealed: int | float + reveal_type(-a) # revealed: float return -a def pos_float_bound(a: T) -> float: - reveal_type(+a) # revealed: int | float + reveal_type(+a) # revealed: float return +a ``` diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 8babe558a3..bd7087f3fd 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -33,9 +33,10 @@ use crate::types::typevar::BoundTypeVarIdentity; use crate::types::visitor::TypeVisitor; use crate::types::{ CallableType, IntersectionType, KnownBoundMethodType, KnownClass, KnownInstanceType, - LiteralValueType, LiteralValueTypeKind, MaterializationKind, PropertyInstanceType, Protocol, - SpecialFormType, StringLiteralType, SubclassOfInner, SubclassOfType, Type, TypeAliasType, - TypeGuardLike, TypedDictModule, TypedDictType, UnionType, WrapperDescriptorKind, visitor, + KnownUnion, LiteralValueType, LiteralValueTypeKind, MaterializationKind, PropertyInstanceType, + Protocol, SpecialFormType, StringLiteralType, SubclassOfInner, SubclassOfType, Type, + TypeAliasType, TypeGuardLike, TypedDictModule, TypedDictType, UnionType, WrapperDescriptorKind, + visitor, }; use ty_python_core::ProgramFile; use ty_python_core::definition::Definition; @@ -104,6 +105,16 @@ impl SignatureNameDisplay { } } +/// Controls whether numeric-tower unions use annotation spelling or expose their exact members. +#[derive(Debug, Clone, Copy, Default)] +enum NumericTowerDisplay { + /// Display `int | float*` as the annotation `float`. + #[default] + Canonical, + /// Display every exact member, such as `int | float*`. + Expanded, +} + /// Settings for displaying types and signatures #[derive(Debug, Clone, Default)] pub struct DisplaySettings<'db> { @@ -119,6 +130,8 @@ pub struct DisplaySettings<'db> { qualified_type_aliases: Rc>, /// Whether long unions and literals are displayed in full preserve_full_unions: bool, + /// How numeric-tower unions should be displayed. + numeric_tower_display: NumericTowerDisplay, /// Scopes that are currently active in the display context (e.g. function scopes /// whose type parameters are currently being displayed). /// Used to suppress redundant `@{scope}` suffixes for type variables. @@ -156,6 +169,18 @@ impl<'db> DisplaySettings<'db> { } } + /// Expands numeric-tower unions so explanations can refer to their individual members. + /// + /// For example, a relation error that discusses the `int` member of a `float` annotation + /// displays the union as `int | float*` instead of hiding that member behind `float`. + #[must_use] + pub(crate) fn expand_numeric_tower_unions(&self) -> Self { + Self { + numeric_tower_display: NumericTowerDisplay::Expanded, + ..self.clone() + } + } + #[must_use] pub(crate) fn disallow_signature_name(&self) -> Self { Self { @@ -664,6 +689,23 @@ pub struct DisplayType<'env, 'db> { } impl<'db> DisplayType<'_, 'db> { + /// Allows this type display to span multiple lines while preserving inferred qualification. + #[must_use] + pub fn multiline(self) -> Self { + Self { + settings: self.settings.multiline(), + ..self + } + } + + #[must_use] + pub(crate) fn preserve_long_unions(self) -> Self { + Self { + settings: self.settings.preserve_long_unions(), + ..self + } + } + pub fn to_string_parts(&self) -> TypeDisplayDetails<'db> { let mut f = TypeWriter::Details(TypeDetailsWriter::new()); self.fmt_detailed(&mut f).unwrap(); @@ -1003,6 +1045,14 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'_, 'db> { match (class, class.known(db)) { (_, Some(KnownClass::NoneType)) => f.with_type(self.ty).write_str("None"), (_, Some(KnownClass::NoDefaultType)) => f.with_type(self.ty).write_str("NoDefault"), + (_, Some(KnownClass::Float | KnownClass::Complex)) => { + f.set_invalid_type_annotation(); + class + .class_literal(self.db) + .display_with(self.db, self.settings.clone()) + .fmt_detailed(f)?; + f.write_char('*') + } (ClassType::Generic(alias), Some(KnownClass::Tuple)) => alias .specialization(db) .tuple(db) @@ -2271,12 +2321,35 @@ impl Display for DisplayCallableType<'_, '_> { } impl<'db> Signature<'db> { + /// Displays this signature with qualification inferred across all parameter and return types. + /// + /// For example, considering the annotations together keeps the two `float` classes distinct: + /// + /// ```python + /// import builtins + /// + /// class float: ... + /// + /// def f(value: builtins.float | float) -> None: ... + /// ``` pub(crate) fn display<'a>( &'a self, db: &'db dyn Db, env: &'a ProgramEnvironment<'db>, ) -> DisplaySignature<'a, 'db> { - Self::display_with(self, db, env, DisplaySettings::default()) + Self::display_with( + self, + db, + env, + DisplaySettings::from_possibly_ambiguous_types( + db, + env, + self.parameters() + .iter() + .map(Parameter::annotated_type) + .chain(std::iter::once(self.return_ty)), + ), + ) } pub(crate) fn display_with<'a>( @@ -2308,6 +2381,30 @@ pub(crate) struct DisplaySignature<'a, 'db> { } impl DisplaySignature<'_, '_> { + #[must_use] + pub(crate) fn multiline(self) -> Self { + Self { + settings: self.settings.multiline(), + ..self + } + } + + #[must_use] + pub(crate) fn disallow_name(self) -> Self { + Self { + settings: self.settings.disallow_signature_name(), + ..self + } + } + + #[must_use] + pub(crate) fn hide_return_type(self) -> Self { + Self { + settings: self.settings.hide_return_type(), + ..self + } + } + /// Get detailed display information including component ranges pub(crate) fn to_string_parts(&self) -> SignatureDisplayDetails { let mut f = TypeWriter::Details(TypeDetailsWriter::new()); @@ -2817,6 +2914,41 @@ const UNION_POLICY: TruncationPolicy = TruncationPolicy { max_when_elided: 3, }; +/// Finds the largest numeric-tower group among the given classes. +/// +/// Unrelated classes are ignored so a mixed annotation can still use canonical spelling: +/// +/// ```python +/// def f(value: str | float) -> None: ... +/// ``` +fn numeric_tower_group(known_classes: impl IntoIterator) -> Option { + let mut has_int = false; + let mut has_float = false; + let mut has_complex = false; + + for known_class in known_classes { + match known_class { + KnownClass::Int => has_int = true, + KnownClass::Float => has_float = true, + KnownClass::Complex => has_complex = true, + _ => {} + } + } + + match (has_int, has_float, has_complex) { + (true, true, true) => Some(KnownUnion::Complex), + (true, true, false) => Some(KnownUnion::Float), + _ => None, + } +} + +fn subclass_of_known_class(db: &dyn Db, subclass_of: SubclassOfType<'_>) -> Option { + match subclass_of.subclass_of() { + SubclassOfInner::Class(class) => class.known(db), + _ => None, + } +} + impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { fn singleline_union_element_label<'db>( @@ -2845,6 +2977,26 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { let db = self.db; let elements = self.ty.elements(db); + let numeric_tower = matches!( + self.settings.numeric_tower_display, + NumericTowerDisplay::Canonical + ) + .then(|| { + numeric_tower_group(elements.iter().filter_map(|element| { + element + .as_nominal_instance() + .and_then(|instance| instance.known_class(db)) + })) + }) + .flatten(); + let is_numeric_tower_element = |element: Type<'db>| { + numeric_tower.is_some_and(|group| { + element + .as_nominal_instance() + .and_then(|instance| instance.known_class(db)) + .is_some_and(|known_class| group.contains(known_class)) + }) + }; let mut condensed_types = vec![]; let mut condensed_element_count = 0usize; let mut subclass_of_types = vec![]; @@ -2852,8 +3004,10 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { .iter() .copied() .map(|element| { - (self.condensable_literals(element).is_none() && !element.is_subclass_of()) - .then(|| singleline_union_element_label(db, self.env, element, &self.settings)) + (self.condensable_literals(element).is_none() + && !element.is_subclass_of() + && !is_numeric_tower_element(element)) + .then(|| singleline_union_element_label(db, self.env, element, &self.settings)) }) .collect(); let duplicate_ambiguous_labels = duplicate_ambiguous_labels(&element_labels); @@ -2871,7 +3025,16 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { } } - let total_entries = elements.len() - condensed_element_count - subclass_of_types.len() + let numeric_tower_element_count = elements + .iter() + .copied() + .filter(|element| is_numeric_tower_element(*element)) + .count(); + let total_entries = elements.len() + - numeric_tower_element_count + - condensed_element_count + - subclass_of_types.len() + + usize::from(numeric_tower.is_some()) + usize::from(!condensed_types.is_empty()) + usize::from(!subclass_of_types.is_empty()); @@ -2883,6 +3046,7 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { let display_limit = UNION_POLICY.display_limit(total_entries, self.settings.preserve_full_unions); + let mut numeric_tower = numeric_tower; let mut condensed_types = Some(condensed_types); let mut subclass_of_types = Some(subclass_of_types); let mut displayed_entries = 0usize; @@ -2892,21 +3056,31 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { break; } - if self.condensable_literals(*element).is_some() { - if let Some(condensed_types) = condensed_types.take() { + if is_numeric_tower_element(*element) { + if let Some(union) = numeric_tower.take() { + displayed_entries += 1; + join.entry(&DisplayKnownUnion { + union, + db, + env: self.env, + settings: self.settings.singleline(), + }); + } + } else if self.condensable_literals(*element).is_some() { + if let Some(literals) = condensed_types.take() { displayed_entries += 1; join.entry(&DisplayLiteralGroup { - literals: condensed_types, + literals, db, env: self.env, settings: self.settings.singleline(), }); } } else if element.is_subclass_of() { - if let Some(subclass_of_types) = subclass_of_types.take() { + if let Some(types) = subclass_of_types.take() { displayed_entries += 1; join.entry(&DisplaySubclassOfGroup { - types: subclass_of_types, + types, db, env: self.env, settings: self.settings.singleline(), @@ -2945,6 +3119,30 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { } } +/// Displays a numeric-tower union through its canonical annotation class. +/// +/// Delegating to the class display preserves qualification and IDE navigation metadata. +struct DisplayKnownUnion<'env, 'db> { + union: KnownUnion, + db: &'db dyn Db, + env: &'env ProgramEnvironment<'db>, + settings: DisplaySettings<'db>, +} + +impl<'db> FmtDetailed<'db> for DisplayKnownUnion<'_, 'db> { + fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + let class = self.union.annotation_class(); + if let Some(class_literal) = class.try_to_class_literal(self.db, self.env) { + ClassLiteral::Static(class_literal) + .display_with(self.db, self.settings.clone()) + .fmt_detailed(f) + } else { + f.with_type(class.to_instance(self.db, self.env)) + .write_str(class.name(self.env.python_version(self.db))) + } + } +} + impl Display for DisplayUnionType<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_detailed(&mut TypeWriter::Formatter(f)) @@ -2968,11 +3166,58 @@ impl<'db> FmtDetailed<'db> for DisplaySubclassOfGroup<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { let db = self.db; f.write_str("type[")?; - let total_entries = self.types.len(); + let numeric_tower = matches!( + self.settings.numeric_tower_display, + NumericTowerDisplay::Canonical + ) + .then(|| { + numeric_tower_group( + self.types + .iter() + .filter_map(|subclass_of| subclass_of_known_class(self.db, *subclass_of)), + ) + }) + .flatten(); + let is_numeric_tower_subclass = |subclass_of: SubclassOfType<'db>| { + numeric_tower.is_some_and(|group| { + subclass_of_known_class(self.db, subclass_of) + .is_some_and(|known_class| group.contains(known_class)) + }) + }; + let numeric_tower_element_count = self + .types + .iter() + .copied() + .filter(|subclass_of| is_numeric_tower_subclass(*subclass_of)) + .count(); + let total_entries = + self.types.len() - numeric_tower_element_count + usize::from(numeric_tower.is_some()); let display_limit = UNION_POLICY.display_limit(total_entries, self.settings.preserve_full_unions); let mut join = f.join(" | "); - for subclass_of in self.types.iter().take(display_limit) { + let mut numeric_tower = numeric_tower; + let mut displayed_entries = 0usize; + + for subclass_of in &self.types { + if displayed_entries >= display_limit { + break; + } + + if is_numeric_tower_subclass(*subclass_of) { + if let Some(union) = numeric_tower.take() { + displayed_entries += 1; + join.entry(&DisplayKnownUnion { + union, + db, + env: self.env, + settings: self.settings.singleline(), + }); + } + continue; + } + + displayed_entries += 1; + match subclass_of.subclass_of() { SubclassOfInner::Class(ClassType::NonGeneric(class)) => { join.entry(&class.display_with(db, self.settings.singleline())); @@ -3007,7 +3252,7 @@ impl<'db> FmtDetailed<'db> for DisplaySubclassOfGroup<'_, 'db> { } } if !self.settings.preserve_full_unions { - let omitted_entries = total_entries.saturating_sub(display_limit); + let omitted_entries = total_entries.saturating_sub(displayed_entries); if omitted_entries > 0 { join.entry(&DisplayOmitted { count: omitted_entries, @@ -3213,10 +3458,17 @@ impl<'db> FmtDetailed<'db> for DisplayMaybeParenthesizedType<'_, 'db> { }; match self.ty { ty if should_parenthesize_callable_type(ty, db) => write_parentheses(f), - Type::KnownBoundMethod(_) - | Type::FunctionLiteral(_) - | Type::BoundMethod(_) - | Type::Union(_) => write_parentheses(f), + Type::KnownBoundMethod(_) | Type::FunctionLiteral(_) | Type::BoundMethod(_) => { + write_parentheses(f) + } + Type::Union(union) + if matches!( + self.settings.numeric_tower_display, + NumericTowerDisplay::Expanded + ) || union.known(db).is_none() => + { + write_parentheses(f) + } Type::Intersection(intersection) if !intersection.has_one_element(db) => { write_parentheses(f) } @@ -3569,7 +3821,10 @@ mod tests { use ruff_python_ast::name::Name; use crate::db::tests::{TestDb, setup_db}; - use crate::types::{KnownClass, Parameter, Parameters, Signature, Type}; + use crate::types::{ + DisplaySettings, KnownClass, KnownUnion, Parameter, Parameters, Signature, Type, + TypeDetail, UnionType, + }; #[test] fn string_literal_display() { @@ -3595,6 +3850,77 @@ mod tests { ); } + #[test] + fn numeric_tower_display() { + let db = setup_db(); + let env = db.program_environment(); + + let exact_float = KnownClass::Float.to_instance(&db, &env); + let exact_complex = KnownClass::Complex.to_instance(&db, &env); + let float_annotation = KnownUnion::Float.to_type(&db, &env); + let complex_annotation = KnownUnion::Complex.to_type(&db, &env); + + assert_snapshot!(exact_float.display(&db, &env), @"float*"); + assert_snapshot!(exact_complex.display(&db, &env), @"complex*"); + assert_snapshot!(float_annotation.display(&db, &env), @"float"); + assert_snapshot!(complex_annotation.display(&db, &env), @"complex"); + assert_eq!( + float_annotation + .display_with( + &db, + &env, + DisplaySettings::default().expand_numeric_tower_unions(), + ) + .to_string(), + "int | float*" + ); + assert_eq!( + complex_annotation + .display_with( + &db, + &env, + DisplaySettings::default().expand_numeric_tower_unions(), + ) + .to_string(), + "int | float* | complex*" + ); + assert_snapshot!(float_annotation.to_meta_type(&db, &env).display(&db, &env), @"type[float]"); + assert_snapshot!(complex_annotation.to_meta_type(&db, &env).display(&db, &env), @"type[complex]"); + + let list_of_float = + KnownClass::List.to_specialized_instance(&db, &env, &[float_annotation]); + assert_snapshot!(list_of_float.display(&db, &env), @"list[float]"); + + let string_or_float = UnionType::from_elements( + &db, + &env, + [KnownClass::Str.to_instance(&db, &env), float_annotation], + ); + assert_snapshot!(string_or_float.display(&db, &env), @"str | float"); + + assert!( + !exact_float + .display(&db, &env) + .to_string_parts() + .is_valid_syntax + ); + assert!( + float_annotation + .display(&db, &env) + .to_string_parts() + .is_valid_syntax + ); + assert!(matches!( + float_annotation + .display(&db, &env) + .to_string_parts() + .details + .as_slice(), + [TypeDetail::Type(Type::ClassLiteral(class))] + if class.known(&db) == Some(KnownClass::Float) + )); + } + fn display_signature<'db>( db: &'db TestDb, parameters: impl IntoIterator>, diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 9eb3ea838c..7f3907a086 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -2962,11 +2962,7 @@ pub(super) fn report_revealed_type<'db>( diag.annotate( Annotation::primary(context.span(argument_node)).message(format_args!( "`{}`", - revealed_type.display_with( - db, - env, - DisplaySettings::default().preserve_long_unions() - ) + revealed_type.display(db, env).preserve_long_unions() )), ); } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 0ff22b607e..d69ed2a015 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -13,7 +13,7 @@ use crate::types::{ KnownUnion, PropertyAccessorRole, SubclassOfInner, Type, TypeContext, TypeVarBoundOrConstraints, binding_type, }; -use crate::{Db, DisplaySettings, HasDefinition, HasType, ProgramEnvironment, SemanticModel}; +use crate::{Db, HasDefinition, HasType, ProgramEnvironment, SemanticModel}; use itertools::Either; use ruff_db::files::FileRange; use ruff_db::parsed::parsed_module; @@ -1773,11 +1773,7 @@ pub fn call_type_simplified_by_overloads( } let signature = resolve_single_overload(model, callable_type, call_expr)?; - Some( - signature - .display_with(db, env, DisplaySettings::default().multiline()) - .to_string(), - ) + Some(signature.display(db, env).multiline().to_string()) } /// Returns the definitions of the binary operation along with its callable type. @@ -3069,14 +3065,10 @@ pub fn constructor_signature(model: &SemanticModel, call_expr: &ast::ExprCall) - let env = &model.program_environment(); let display_sig = |signature: &Signature| { let params = signature - .display_with( - db, - env, - DisplaySettings::default() - .multiline() - .disallow_signature_name() - .hide_return_type(), - ) + .display(db, env) + .multiline() + .disallow_name() + .hide_return_type() .to_string(); format!("class {class_name}{params}") diff --git a/crates/ty_python_semantic/src/types/relation_error.rs b/crates/ty_python_semantic/src/types/relation_error.rs index b869fd081d..1269b78cfb 100644 --- a/crates/ty_python_semantic/src/types/relation_error.rs +++ b/crates/ty_python_semantic/src/types/relation_error.rs @@ -8,7 +8,7 @@ use ruff_python_ast::name::Name; use crate::types::context::LintDiagnosticGuard; use crate::types::tuple::TupleLength; -use crate::types::{Type, TypedDictType}; +use crate::types::{DisplaySettings, Type, TypedDictType}; use crate::{FxOrderSet, ProgramEnvironment}; /// Identifies a parameter, either by name or by position. @@ -188,17 +188,28 @@ impl<'db> ErrorContext<'db> { element, union, target, - } => format!( - "element `{}` of union `{}` is not assignable to `{}`", - element.display(db, env), - union.display(db, env), - target.display(db, env), - ), - Self::NotAssignableToAnyUnionElement { source, union } => format!( - "type `{}` is not assignable to any element of the union `{}`", - source.display(db, env), - union.display(db, env), - ), + } => { + let settings = DisplaySettings::from_possibly_ambiguous_types( + db, + env, + [*element, *union, *target], + ); + format!( + "element `{}` of union `{}` is not assignable to `{}`", + element.display_with(db, env, settings.clone()), + union.display_with(db, env, settings.expand_numeric_tower_unions()), + target.display_with(db, env, settings), + ) + } + Self::NotAssignableToAnyUnionElement { source, union } => { + let settings = + DisplaySettings::from_possibly_ambiguous_types(db, env, [*source, *union]); + format!( + "type `{}` is not assignable to any element of the union `{}`", + source.display_with(db, env, settings.clone()), + union.display_with(db, env, settings.expand_numeric_tower_unions()), + ) + } Self::NotAssignableToNOtherUnionElements { n } => format!( "... omitted {n} union element{} without additional context", if *n == 1 { "" } else { "s" } diff --git a/crates/ty_python_semantic/src/types/set_theoretic.rs b/crates/ty_python_semantic/src/types/set_theoretic.rs index 952773384f..dfed4e5d24 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic.rs @@ -504,6 +504,25 @@ pub(crate) enum KnownUnion { } impl KnownUnion { + /// Returns the class whose annotation denotes this numeric-tower union. + pub(crate) const fn annotation_class(self) -> KnownClass { + match self { + Self::Float => KnownClass::Float, + Self::Complex => KnownClass::Complex, + } + } + + /// Returns whether this union contains exact instances of `class`. + pub(crate) const fn contains(self, class: KnownClass) -> bool { + match self { + Self::Float => matches!(class, KnownClass::Int | KnownClass::Float), + Self::Complex => matches!( + class, + KnownClass::Int | KnownClass::Float | KnownClass::Complex + ), + } + } + pub(crate) fn to_type<'db>(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { match self { KnownUnion::Float => UnionType::from_two_elements( From c88d3c57b855e8ed20923fbeab3e6d60a039f0a1 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 5 Aug 2026 14:29:55 -0700 Subject: [PATCH 306/390] [ty] Avoid double inference of Unpack operands in Union (#27525) Fixes astral-sh/ty#4193. Ensure `Unpack` always infers its operand, including in invalid contexts, so `Union` can reuse the recorded type instead of inferring the same expression twice. Preserve runtime diagnostics for evaluated operands while suppressing them inside invalid string annotations. ## Test plan - Cover malformed `Unpack[]` nested inside `Union` and a generic specialization. - Cover syntactically valid `Unpack[int]` nested inside `Union` and preserve unsupported `Unpack[Ts]` recovery. - Verify invalid evaluated contexts report operand errors while invalid string annotations suppress them. --- .../mdtest/generics/legacy/unpack.md | 38 +++++++++- .../resources/mdtest/invalid_syntax.md | 17 +++++ .../types/infer/builder/type_expression.rs | 71 ++++++++++--------- 3 files changed, 89 insertions(+), 37 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/unpack.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/unpack.md index 8547a24329..5d1c336913 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/unpack.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/unpack.md @@ -92,8 +92,9 @@ def f( ## Unsupported union unpacking -Unpacking a type variable tuple into `Union` is currently not supported. Both the rejected union and -runtime element access recover to `object`. +Unpacking a type variable tuple into `Union` is currently not supported. The rejected union recovers +to `object` both on its own and inside another generic specialization. Runtime element access also +recovers to `object`. ```py from typing import TypeVarTuple, Union, Unpack @@ -106,6 +107,10 @@ def reject_union(value: Union[Unpack[Ts]]) -> None: # TODO: should reveal `Union[*Ts]` representation reveal_type(value) # revealed: object +# error: [invalid-type-form] "Unpacking a `TypeVarTuple` in `Union` is not supported" +def reject_nested_union(value: list[Union[Unpack[Ts], None]]) -> None: + reveal_type(value) # revealed: list[object] + def element_types(values: tuple[Unpack[Ts]]) -> None: # TODO: should reveal `Union[*Ts]` representation reveal_type(values[0]) # revealed: object @@ -115,6 +120,35 @@ def element_types(values: tuple[Unpack[Ts]]) -> None: reveal_type(value) # revealed: object ``` +## Invalid unpack operand nested in a union + +Although `Unpack[int]` is valid Python syntax, its non-tuple operand should report an ordinary +diagnostic when the union appears inside a generic specialization. + +```py +from typing import Union, Unpack + +# error: [invalid-type-form] "`Unpack` can only unpack a tuple type or `TypeVarTuple`" +def invalid_operand(value: list[Union[Unpack[int], None]]) -> None: + reveal_type(value) # revealed: list[tuple[Unknown, ...] | None] +``` + +## Invalid unpack contexts still infer the operand + +An invalid unpack context should not suppress runtime errors from its operand. String annotations do +not execute their contents, so unresolved names inside an invalid string annotation remain silent. + +```py +from typing import Unpack + +# error: [invalid-type-form] "`Unpack` is not allowed in parameter annotations" +# error: [unresolved-reference] "Name `Missing` used when not defined" +def invalid_context(value: Unpack[Missing]) -> None: ... + +# error: [invalid-type-form] "`Unpack` is not allowed in parameter annotations" +def invalid_stringified_context(value: "Unpack[Missing]") -> None: ... +``` + ## Concrete and nested tuple unpacking `Unpack` can expand a concrete tuple annotation for `*args`, including a nested unbounded tuple. diff --git a/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md index 7a88c70b62..d41bab9fa7 100644 --- a/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md +++ b/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md @@ -143,6 +143,23 @@ def _(u: InvalidEmptyUnion): reveal_type(u) # revealed: Unknown ``` +### `typing.Unpack` + +```toml +[environment] +python-version = "3.11" +``` + +An empty `Unpack` nested inside a union and a generic specialization should report its syntax error +without panicking. + +```py +from typing import Union, Unpack + +# error: [invalid-syntax] "Expected index or slice expression" +list[Union[Unpack[], None]] +``` + ### `typing.Annotated` ```py diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 6c280d1b84..a7cb70ee43 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -2069,17 +2069,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ty, Type::TypeVar(typevar) if typevar.is_typevartuple(db) ) || if let ast::Expr::Subscript(subscript) = argument { - let previously_in_unpack_type_argument = self - .context - .inference_flags - .replace(InferenceFlags::IN_UNPACK_TYPE_ARGUMENT, true); - let inner_ty = self.infer_type_expression(&subscript.slice); - self.context.inference_flags.set( - InferenceFlags::IN_UNPACK_TYPE_ARGUMENT, - previously_in_unpack_type_argument, - ); matches!( - inner_ty, + self.expression_type(&subscript.slice), Type::TypeVar(typevar) if typevar.is_typevartuple(db) ) } else { @@ -2470,10 +2461,40 @@ impl<'db> TypeInferenceBuilder<'db, '_> { TypeExpressionFlags::UNPACK, ); - if self - .inference_flags() - .contains(InferenceFlags::IN_UNPACK_TYPE_ARGUMENT) + let inference_flags = self.inference_flags(); + let is_nested_unpack = + inference_flags.contains(InferenceFlags::IN_UNPACK_TYPE_ARGUMENT); + let is_nested_kwargs = inference_flags + .contains(InferenceFlags::IN_KWARG_ANNOTATION) + && inference_flags.contains(InferenceFlags::IN_NESTED_TYPE_EXPRESSION); + let is_invalid_context = !inference_flags.intersects( + InferenceFlags::IN_VARARG_ANNOTATION + | InferenceFlags::IN_KWARG_ANNOTATION + | InferenceFlags::IN_VALID_UNPACK_CONTEXT, + ); + + let previously_in_unpack_type_argument = self + .context + .inference_flags + .replace(InferenceFlags::IN_UNPACK_TYPE_ARGUMENT, true); + let inner_ty = if self.in_string_annotation() + && (is_nested_unpack || is_nested_kwargs || is_invalid_context) { + // Invalid string annotations never execute, so their operands must not + // produce runtime errors even though their inferred types are still needed. + let mut speculative = self.speculate_without_diagnostics(); + let inner_ty = speculative.infer_type_expression(arguments_slice); + self.extend(speculative); + inner_ty + } else { + self.infer_type_expression(arguments_slice) + }; + self.context.inference_flags.set( + InferenceFlags::IN_UNPACK_TYPE_ARGUMENT, + previously_in_unpack_type_argument, + ); + + if is_nested_unpack { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { diagnostic::add_type_expression_reference_link( builder.into_diagnostic("`Unpack` cannot be nested"), @@ -2482,13 +2503,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return Type::unknown(); } - if self - .inference_flags() - .contains(InferenceFlags::IN_KWARG_ANNOTATION) - && self - .inference_flags() - .contains(InferenceFlags::IN_NESTED_TYPE_EXPRESSION) - { + if is_nested_kwargs { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { diagnostic::add_type_expression_reference_link(builder.into_diagnostic( "`Unpack` is only valid as the top-level `**kwargs` annotation form", @@ -2497,11 +2512,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return Type::unknown(); } - if !self.inference_flags().intersects( - InferenceFlags::IN_VARARG_ANNOTATION - | InferenceFlags::IN_KWARG_ANNOTATION - | InferenceFlags::IN_VALID_UNPACK_CONTEXT, - ) { + if is_invalid_context { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { diagnostic::add_type_expression_reference_link(builder.into_diagnostic( format_args!( @@ -2513,16 +2524,6 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return Type::unknown(); } - let previously_in_unpack_type_argument = self - .context - .inference_flags - .replace(InferenceFlags::IN_UNPACK_TYPE_ARGUMENT, true); - let inner_ty = self.infer_type_expression(arguments_slice); - self.context.inference_flags.set( - InferenceFlags::IN_UNPACK_TYPE_ARGUMENT, - previously_in_unpack_type_argument, - ); - if self .inference_flags() .contains(InferenceFlags::IN_KWARG_ANNOTATION) From 6dc1031889aa24a0461457409d39861e279242a9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 5 Aug 2026 17:53:55 -0400 Subject: [PATCH 307/390] Disable local ThinLTO for optimized development workflows (#27526) ## Summary Configure agent-directed Cargo commands and the existing `fast-test` profile to consistently use `opt-level = 1`, `debug = "line-tables-only"`, and `lto = "off"`. Apply the same settings to tests, Clippy, debug runs, and code generation while preserving ordinary development, CI, and release defaults. Cargo's default `lto = false` still applies local ThinLTO when optimization is enabled, adding substantial work to incremental Rust edits. Across 222 recent agent test commands, 110 ran one mdtest file, 41 ran the full `ty_python_semantic` suite, and 48 ran a two-crate suite. Holding `opt-level = 1` fixed, three alternating paired runs give: | Workflow | Local ThinLTO | Disabled | Change | | ------------------------------------------- | ------------: | -------: | -----: | | Rust edit + one mdtest file | 9.84 s | 6.81 s | -30.7% | | Rust edit + full semantic suite (797 tests) | 24.86 s | 18.11 s | -27.1% | | Warm semantic suite, no edits (797 tests) | 5.28 s | 5.64 s | +6.9% | Keeping optimization enabled remains worthwhile for broader runs. With ThinLTO disabled, `opt-level = 1` reduced a same-crate edit followed by all 797 semantic tests from 22.73 s to 17.27 s, and an already-built semantic suite from 13.64 s to 5.59 s. The mdtest executable grows from 49.0 MiB to 56.3 MiB (+14.7%). --- AGENTS.md | 28 ++++++++++++++-------------- Cargo.toml | 3 +++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c47ab3460a..2a7ce6e004 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,28 +18,28 @@ instructions to PR authors or flag unrelated pre-existing issues. ## Running Tests -Run all tests (using `nextest` for faster execution, setting `CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_DEBUG="line-tables-only"` to enable optimizations while retaining some debug info, and setting `INSTA_FORCE_PASS=1 INSTA_UPDATE=always MDTEST_UPDATE_SNAPSHOTS=1` to ensure all snapshots are updated): +Run all tests (using `nextest` for faster execution and setting `INSTA_FORCE_PASS=1 INSTA_UPDATE=always MDTEST_UPDATE_SNAPSHOTS=1` to ensure all snapshots are updated): ```sh -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run ``` Run tests for a specific crate: ```sh -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic ``` Run a single mdtest file. The path to the mdtest file should be relative to the `crates/ty_python_semantic/resources/mdtest` folder. Include `--test mdtest` to avoid building unrelated test binaries: ```sh -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic --test mdtest -- mdtest:: +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic --test mdtest -- mdtest:: ``` To run a specific mdtest within a file, use a substring of the Markdown header text as `MDTEST_TEST_FILTER`. Only use this if it's necessary to isolate a single test case: ```sh -MDTEST_TEST_FILTER="" CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic --test mdtest -- mdtest:: +MDTEST_TEST_FILTER="" CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo nextest run -p ty_python_semantic --test mdtest -- mdtest:: ``` ### Fallback without nextest @@ -48,16 +48,16 @@ If `cargo nextest` is not available, use `cargo test` with the same environment ```sh # Run all tests. -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test # Run tests for a specific crate. -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test -p ty_python_semantic +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test -p ty_python_semantic # Run a single mdtest file. -CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test -p ty_python_semantic --test mdtest -- +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test -p ty_python_semantic --test mdtest -- # Run a specific mdtest within a file. -MDTEST_TEST_FILTER="" CARGO_PROFILE_DEV_OPT_LEVEL=1 INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test -p ty_python_semantic --test mdtest -- +MDTEST_TEST_FILTER="" CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off INSTA_FORCE_PASS=1 INSTA_UPDATE=always CARGO_PROFILE_DEV_DEBUG="line-tables-only" MDTEST_UPDATE_SNAPSHOTS=1 cargo test -p ty_python_semantic --test mdtest -- ``` ### Snapshot updates @@ -80,7 +80,7 @@ Never edit snapshot files or inline snapshot bodies manually. Regenerate them by ## Running Clippy ```sh -cargo clippy --workspace --all-targets --all-features -- -D warnings +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off CARGO_PROFILE_DEV_DEBUG="line-tables-only" cargo clippy --workspace --all-targets --all-features -- -D warnings ``` ## Running Debug Builds @@ -90,13 +90,13 @@ Use debug builds (not `--release`) when developing, as release builds lack debug Run Ruff: ```sh -cargo run --bin ruff -- check path/to/file.py +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off CARGO_PROFILE_DEV_DEBUG="line-tables-only" cargo run --bin ruff -- check path/to/file.py ``` Run ty: ```sh -cargo run --bin ty -- check path/to/file.py +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off CARGO_PROFILE_DEV_DEBUG="line-tables-only" cargo run --bin ty -- check path/to/file.py ``` ## Working on ty @@ -160,9 +160,9 @@ Parts of `.github/workflows/release.yml` are generated by cargo-dist from `dist- - Before writing significant amounts of new code, look for existing utilities or mechanisms that could solve the problem. Avoid expanding the task to unrelated issues, but do not confuse keeping the task focused with minimizing the size of the implementation. Prefer addressing the underlying architectural problem over adding a localized workaround, even when doing so requires a substantial refactor or rearchitecture. Ask the user for guidance if in doubt about whether to attempt a larger refactor or not. - Try hard to avoid patterns that require `panic!`, `unreachable!`, `.unwrap()` or `.expect()`. Instead, try to encode those constraints in the type system. Don't be afraid to write code that's more verbose or requires largeish refactors if it enables you to avoid these unsafe calls. - Prefer let chains (`if let` combined with `&&`) and let guards (`PAT if let ... =>`) over nested `if let` statements to reduce indentation and improve readability. At the end of a task, always check your work to see if you missed opportunities to use `let` chains or `let` guards. -- If you *have* to suppress a Clippy lint, prefer to use `#[expect()]` over `[allow()]`, where possible. But if a lint is complaining about unused/dead code, it's usually best to just delete the unused code. +- If you _have_ to suppress a Clippy lint, prefer to use `#[expect()]` over `[allow()]`, where possible. But if a lint is complaining about unused/dead code, it's usually best to just delete the unused code. - Don't use comments to narrate code, but do use them to explain invariants and why something unusual was done a particular way. Make sure that a comment will make sense to somebody who's reading the code for the first time. Prefer plain language, avoid jargon, and don't be afraid to be more verbose if it's necessary to explain something well. Giving examples of the kind of Python code we're trying to model at this particular point in Ruff or ty can often be very helpful for future readers of the code. -- Run `cargo dev generate-all` after changing configuration options, CLI arguments, lint rules, or environment variable definitions, as these changes require regeneration of schemas, docs, and CLI references. +- Run `CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off CARGO_PROFILE_DEV_DEBUG="line-tables-only" cargo dev generate-all` after changing configuration options, CLI arguments, lint rules, or environment variable definitions, as these changes require regeneration of schemas, docs, and CLI references. - Don't prefix tests with `test_`. - Don't separate struct definitions from their `impl` blocks unless the `impl` is deliberately placed in a separate file, as for large structs. - Avoid running `uv run` for any scripts from the repository root unless you use `--no-project`, `--script` or similar. Using `uv run` from the Ruff repo root without these flags will build Ruff from source, which is very slow and usually unnecessary. diff --git a/Cargo.toml b/Cargo.toml index 05a918187f..0d79a518bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -343,6 +343,9 @@ lto = false [profile.fast-test] inherits = "dev" opt-level = 1 +debug = "line-tables-only" +# Avoid the local ThinLTO that Cargo enables at nonzero optimization levels. +lto = "off" # The profile that 'cargo dist' will build with. [profile.dist] From fce9727c830d17102b95dacad377d6d1ffc078a6 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 5 Aug 2026 17:39:50 -0700 Subject: [PATCH 308/390] [ty] Fix panic from mismatched OR-pattern bindings (#27533) ## Summary https://github.com/astral-sh/ruff/pull/27438 modeled OR-pattern alternatives as distinct control-flow paths rather than linear control flow. This is correct, but the implementation was inadequate; it effectively treated all of these branches simultaneously as "definitely taken", which becomes a problem in the (invalid syntax) case where not all branches bind the same name(s). That caused a panic. Fix this by introducing reachability predicates to explicitly model that only one of these "branches" can be taken. Closes astral-sh/ty#4194. ## Test plan Added mdtests covering distinct and overlapping captures, alternatives without bindings in either order, three-way alternatives, previously bound names, nested mismatches, guard references, and the original malformed-case panic. --- crates/ty_python_core/src/builder.rs | 76 ++++++++++--- crates/ty_python_core/src/predicate.rs | 4 + .../resources/mdtest/invalid_syntax.md | 105 ++++++++++++++++++ crates/ty_python_semantic/src/reachability.rs | 2 + crates/ty_python_semantic/src/types/narrow.rs | 3 + 5 files changed, 172 insertions(+), 18 deletions(-) diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index c343160776..8602cd638d 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -2168,6 +2168,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { PredicateNode::SubjectElementPattern(_) | PredicateNode::IsNonTerminalCall(_) | PredicateNode::IsNonEmptyIterable(_) + | PredicateNode::OrPatternAlternative(_) | PredicateNode::StarImportPlaceholder(_) => { // These predicates don't narrow any places PossiblyNarrowedPlaces::default() @@ -2279,6 +2280,40 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.current_statements.last_mut() } + /// Return whether a pattern contains any capture that changes the current flow state. + fn pattern_has_bindings(pattern: &ast::Pattern) -> bool { + match pattern { + ast::Pattern::MatchValue(_) | ast::Pattern::MatchSingleton(_) => false, + ast::Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) + | ast::Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => { + patterns.iter().any(Self::pattern_has_bindings) + } + ast::Pattern::MatchMapping(pattern) => { + pattern.rest.is_some() || pattern.patterns.iter().any(Self::pattern_has_bindings) + } + ast::Pattern::MatchClass(pattern) => pattern + .arguments + .patterns + .iter() + .chain( + pattern + .arguments + .keywords + .iter() + .map(|keyword| &keyword.pattern), + ) + .any(Self::pattern_has_bindings), + ast::Pattern::MatchStar(pattern) => pattern.name.is_some(), + ast::Pattern::MatchAs(pattern) => { + pattern.name.is_some() + || pattern + .pattern + .as_deref() + .is_some_and(Self::pattern_has_bindings) + } + } + } + fn predicate_kind(&mut self, pattern: &ast::Pattern) -> PatternPredicateKind<'db> { match pattern { ast::Pattern::MatchValue(pattern) => { @@ -4950,28 +4985,33 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { fn visit_pattern(&mut self, pattern: &'ast ast::Pattern) { if let ast::Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) = pattern - && let Some((first, alternatives)) = patterns.split_first() + && let Some((last, alternatives)) = patterns.split_last() + // Capture-free alternatives do not affect bindings and need no flow merge. + && patterns.iter().any(Self::pattern_has_bindings) { - let incoming = self.flow_snapshot(); - let first_definition = self.current_use_def_map().next_definition_id(); - self.visit_pattern(first); - - // Valid alternatives bind the same names, so capture-free patterns need no flow merge. - if self.current_use_def_map().next_definition_id() == first_definition { - for alternative in alternatives { - self.visit_pattern(alternative); + // Start each alternative without earlier captures so repeated names do not shadow one + // another. Complementary predicates preserve possible missing captures while all + // alternatives together recover the incoming reachability. + let mut successful_alternatives = None; + for alternative in alternatives { + let remaining_alternatives = self.flow_snapshot(); + let selected_alternative = + self.record_reachability_constraint(PredicateOrLiteral::Predicate(Predicate { + node: PredicateNode::OrPatternAlternative(self.current_scope_id()), + is_positive: true, + })); + self.visit_pattern(alternative); + if let Some(previous_alternatives) = successful_alternatives.take() { + self.flow_merge(previous_alternatives); } - return; + successful_alternatives = Some(self.flow_snapshot()); + self.flow_restore(remaining_alternatives); + self.record_negated_reachability_constraint(selected_alternative); } - // Each alternative starts with the same bindings. Otherwise a repeated capture in a - // later alternative shadows the earlier capture even though only one pattern matches. - let mut merged_alternatives = self.flow_snapshot(); - for alternative in alternatives { - self.flow_restore(incoming.clone()); - self.visit_pattern(alternative); - self.flow_merge(merged_alternatives); - merged_alternatives = self.flow_snapshot(); + self.visit_pattern(last); + if let Some(successful_alternative) = successful_alternatives { + self.flow_merge(successful_alternative); } return; } diff --git a/crates/ty_python_core/src/predicate.rs b/crates/ty_python_core/src/predicate.rs index 4241e92a95..b9af38d627 100644 --- a/crates/ty_python_core/src/predicate.rs +++ b/crates/ty_python_core/src/predicate.rs @@ -138,6 +138,10 @@ pub enum PredicateNode<'db> { /// semantically during type checking, so calls to a shadowed `range` remain ambiguous. IsNonEmptyIterable(Expression<'db>), Pattern(PatternPredicate<'db>), + /// Whether control flow takes one branch of an OR pattern instead of its remaining + /// alternatives. The selected branch is unknown, but recording a predicate and its negation + /// preserves the fact that exactly one branch is taken. + OrPatternAlternative(ScopeId<'db>), SubjectElementPattern(SubjectElementPatternPredicate<'db>), StarImportPlaceholder(StarImportPlaceholderPredicate<'db>), } diff --git a/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md b/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md index d41bab9fa7..0141aa139c 100644 --- a/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md +++ b/crates/ty_python_semantic/resources/mdtest/invalid_syntax.md @@ -106,6 +106,111 @@ out = (obj.attr := obj).attr out = (obj[0] := obj).attr ``` +## Match-pattern alternatives binding different names + +A capture present in only one invalid `or` alternative is possibly undefined. + +```py +match 0: + case first | second: # error: [invalid-syntax] "alternative patterns bind different names" + first # error: [possibly-unresolved-reference] + second # error: [possibly-unresolved-reference] +``` + +## Match-pattern alternative without a binding + +A capture missing from one alternative is possibly undefined, regardless of alternative order. + +```py +match (0,): + # error: [invalid-syntax] "alternative patterns bind different names" + case [first_value] | []: + first_value # error: [possibly-unresolved-reference] +``` + +An alternative without a capture can also occur first. + +```py +match (0,): + # error: [invalid-syntax] "alternative patterns bind different names" + case [] | [last_value]: + last_value # error: [possibly-unresolved-reference] +``` + +A capture limited to the middle of three alternatives also remains possibly undefined. + +```py +match (0,): + # error: [invalid-syntax] "alternative patterns bind different names" + case [] | [middle_value] | []: + middle_value # error: [possibly-unresolved-reference] +``` + +## Previously bound match-pattern captures + +A prior binding remains visible on alternatives that do not capture the name. + +```py +value = "previous" + +match (0,): + case [value] | []: # error: [invalid-syntax] "alternative patterns bind different names" + value + +value +``` + +## Partially overlapping match-pattern bindings + +Shared captures remain definitely bound; branch-specific captures are possibly undefined. + +```py +match (0, 1): + # error: [invalid-syntax] "alternative patterns bind different names" + case [first, shared] | [second, shared]: + first # error: [possibly-unresolved-reference] + second # error: [possibly-unresolved-reference] + shared +``` + +## Nested mismatched match-pattern bindings + +Syntax checking stops after an outer mismatch, but unchecked nested alternatives must still be +modeled safely. + +```py +match (0,): + # error: [invalid-syntax] "alternative patterns bind different names" + case [first] | [second] | [third | fourth]: + third # error: [possibly-unresolved-reference] + fourth # error: [possibly-unresolved-reference] +``` + +## Partially bound match-pattern capture in a guard + +A guard can observe a name that is bound by only one invalid alternative. + +```py +match (0,): + # error: [invalid-syntax] "alternative patterns bind different names" + # error: [possibly-unresolved-reference] + case [value] | [] if value: + pass +``` + +## Malformed match-case recovery + +Parser recovery treats the trailing name as an annotation-only statement, whose binding lookup must +not panic. + +```py +match 0: + # error: [invalid-syntax] "alternative patterns bind different names" + # error: [invalid-syntax] "Expected `:`, found name" + # error: [invalid-syntax] "Expected an expression" + case first | second first: +``` + ## Invalid annotation ### `typing.Callable` diff --git a/crates/ty_python_semantic/src/reachability.rs b/crates/ty_python_semantic/src/reachability.rs index 4635ae5b60..d6bfff29f5 100644 --- a/crates/ty_python_semantic/src/reachability.rs +++ b/crates/ty_python_semantic/src/reachability.rs @@ -547,6 +547,7 @@ fn predicate_scope<'db>(db: &'db dyn Db, predicate: &Predicate<'db>) -> ScopeId< callable.scope(db) } PredicateNode::Pattern(pattern) => pattern.scope(db), + PredicateNode::OrPatternAlternative(scope) => scope, PredicateNode::SubjectElementPattern(subject_element) => subject_element.pattern.scope(db), PredicateNode::IsNonEmptyIterable(expression) => expression.scope(db), PredicateNode::StarImportPlaceholder(star_import) => star_import.scope(db), @@ -1529,6 +1530,7 @@ fn analyze_single(db: &dyn Db, env: &ProgramEnvironment<'_>, predicate: &Predica }) => analyze_non_terminal_call(db, callable, call_expr, is_await) .negate_if(!predicate.is_positive), PredicateNode::Pattern(inner) => analyze_pattern_predicate(db, inner), + PredicateNode::OrPatternAlternative(_) => Truthiness::Ambiguous, PredicateNode::SubjectElementPattern(subject_element) => { analyze_pattern_predicate(db, subject_element.pattern) } diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index bbecb07346..9d1563f23f 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -104,6 +104,7 @@ pub(crate) fn infer_narrowing_constraints<'db>( } PredicateNode::IsNonTerminalCall(_) | PredicateNode::IsNonEmptyIterable(_) + | PredicateNode::OrPatternAlternative(_) | PredicateNode::StarImportPlaceholder(_) => (None, None), }; @@ -1143,6 +1144,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { } PredicateNode::IsNonTerminalCall(_) => return None, PredicateNode::IsNonEmptyIterable(_) => return None, + PredicateNode::OrPatternAlternative(_) => return None, PredicateNode::StarImportPlaceholder(_) => return None, }; @@ -2788,6 +2790,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { match self.predicate { PredicateNode::Expression(expression) => expression.scope(db), PredicateNode::Pattern(pattern) => pattern.scope(db), + PredicateNode::OrPatternAlternative(scope) => scope, PredicateNode::SubjectElementPattern(subject_element) => { subject_element.pattern.scope(db) } From 87aa1aafcc7d1b3c67ae5cbe775d12a853090fc1 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 6 Aug 2026 10:40:30 +0200 Subject: [PATCH 309/390] [ty] Gradual `isinstance` narrowing for generic classes (#27308) ## Summary This PR introduces a new `ty.analysis.strict-generic-narrowing` option that controls how `isinstance` and `issubclass` narrowing works for unspecialized generic classes. When this option is set to `true`, we use the top materialization of the generic class (e.g. `Top[list[Unknown]]`). This is the current behavior on `main` and the sound way to do `isinstance` narrowing with generic classes. When the option is set to `false` (the new default), we switch to an unsound mode that is intended to help users migrate from other type checkers. In this non-strict mode, we intersect with the `Unknown`-specialization of the generic class. For example, `x: object` is narrowed to `object & list[Unknown] = list[Unknown]` after `isinstance(x, list)`. If the type of `x` already contains a specialization of `list`, or a generic base of `list`, we narrow to the corresponding `list` specialization instead. For example, `x: Sequence[int]` is narrowed to `list[int]`, and `x: Mapping[str, int]` is narrowed to `dict[str, int]` after `isinstance(x, dict)`. We also add a special case for `TypedDict`s here: When starting from a `TypedDict` type, and narrowing via `isinstance(.., dict)` (or `Mapping`, `MutableMapping`, ..), we simply preserve the previous `TypedDict` type. We also apply those settings to class patterns in `match` statements, resolving this comment: https://github.com/astral-sh/ty/issues/3676#issuecomment-4897968972 ### `strict-generic-narrowing = true` (current behavior on `main`) ```py def f(xs: object): if isinstance(xs, list): reveal_type(xs) # Top[list[Unknown]] for x in xs: reveal_type(x) # object xs.append(1) # error! def g(xs: Sequence[int]): if isinstance(xs, list): reveal_type(xs) # Sequence[int] & Top[list[Unknown]] = Top[list[int & Unknown]] ``` ### `strict-generic-narrowing = false` (gradual / non-strict, new default behavior) ```py def f(xs: object): if isinstance(xs, list): reveal_type(xs) # list[Unknown] for x in xs: reveal_type(x) # Unknown xs.append(1) # okay def g(xs: Sequence[int]): if isinstance(xs, list): reveal_type(xs) # list[int] ``` closes https://github.com/astral-sh/ty/issues/3476 (basically asks for exactly this feature) closes https://github.com/astral-sh/ty/issues/3890 (we support all of these use cases as intended by the user) closes https://github.com/astral-sh/ty/issues/3676 (in the new default mode, the original use case is supported, in strict mode, we need to implement the intersection simplifications which are tracked in https://github.com/astral-sh/ty/issues/1824) closes https://github.com/astral-sh/ty/issues/2843 (we support the original use case now. we could keep this issue open if we really want to support this in strict mode as well?) closes https://github.com/astral-sh/ty/issues/3477 (already closed, but directly asks for a mode like this) also related: https://github.com/astral-sh/ty/issues/1130 (we have special handling for TypedDicts here, but we probably still want to solve this for strict mode) ## Previously considered alternatives * https://github.com/astral-sh/ruff/pull/26472 Intersecting with the gradual `Unknown` specialization. The problem here was that this introduces too much graduality. When starting from a fully static type like `Sequence[int]`, we would get `Sequence[int] & Sequence[Unknown] = Sequence[int & Unknown]` after an `isinstance(..., Sequence)` check. Iterating over that type would yield elements of type `int & Unknown`, which is too permissive. * https://github.com/astral-sh/ruff/pull/26797 Intersecting with transient top materializations. The idea here was to still intersect with `Top[C[Unknown]]`, but then remove that `Top` materialization after the intersection type had been simplified. This solved the problem with `Sequence[..]` above, because now we created the intersection type `Sequence[int] & Top[Sequence[Unknown]] = Sequence[int] & Sequence[object] = Sequence[int & object] = Sequence[int]` which was still fully static. However, this still caused problems when invariant generics were involved. For example, starting with `Sequence[int]` and narrowing using `isinstance(.., list)` would lead to `Sequence[int] & Top[list[Unknown]]`. We showed that this type is equivalent to `Top[list[int & Unknown]]`, but that didn't solve the problem that iterating over that type after removing the `Top` materialization still yielded elements of type `int & Unknown`. * https://github.com/astral-sh/ruff/pull/26848 isinstance narrowing using tagged `object`/`Never` types. This approach followed the suggestion in https://github.com/astral-sh/ty/issues/3375. The idea was to create types that act like `object`/`Never` in intersection simplification, but like `Unknown` elsewhere. One problem was that this didn't really address the problem of invariant generics. For those, we were still forced to use a special `Top*[..]` materialization that would tell us: this type came from a non-strict isinstance narrowing and it should produce `object*`/`Never*` types when methods/attributes are accessed on this type. So for the `Sequence[int] / isinstance(.., list)` example, we would still create `Sequence[int] & Top*[list[Unknown]]`, but the advantage was that we now created the intersection `int & object*` when iterating over that type. Since this simplified to `int`, it seemed like the problem from above was solved. However, introducing those two magical types didn't feel very satisfactory. And we still didn't match the behavior of other type checkers. When narrowing from `object` using `isinstance(..., list)`, we would create `Top*[list[Unknown]]`, whereas other type checkers simply inferred `list[Unknown]`. And there were also more subtle problems where merging a type like `object*` with another type in a control-flow join would create `object* | Other = object*` and therefore lose precision. ## Test plan New and updated Markdown tests ## Ecosystem Looks good. There are some cases where `TypedDict` types "survive" a negative `isinstance(.., dict)` check, but I think that's https://github.com/astral-sh/ty/issues/1130. Gradual mode doesn't do anything differently in negative branches. [PR_27308_ECOSYSTEM_SUMMARY.md](https://github.com/user-attachments/files/30749390/PR_27308_ECOSYSTEM_SUMMARY.md) (the egglog case was resolved since then) --- crates/ruff_benchmark/benches/ty_walltime.rs | 2 +- crates/ty/docs/configuration.md | 86 +++ crates/ty_project/src/metadata/options.rs | 30 + .../resources/mdtest/call/builtins.md | 10 + .../resources/mdtest/loops/for.md | 24 +- .../resources/mdtest/narrow/callable.md | 49 +- .../mdtest/narrow/conditionals/in.md | 5 + .../resources/mdtest/narrow/isinstance.md | 604 +++++++++++++++++- .../resources/mdtest/narrow/issubclass.md | 57 ++ .../resources/mdtest/narrow/match.md | 234 ++++++- .../resources/mdtest/typed_dict.md | 4 +- crates/ty_python_semantic/src/lib.rs | 4 + .../src/types/match_pattern.rs | 8 +- crates/ty_python_semantic/src/types/narrow.rs | 537 ++++++++++++++-- .../e2e__commands__debug_command.snap | 1 + crates/ty_test/src/config.rs | 3 + crates/ty_test/src/db.rs | 4 + ty.schema.json | 7 + 18 files changed, 1570 insertions(+), 99 deletions(-) diff --git a/crates/ruff_benchmark/benches/ty_walltime.rs b/crates/ruff_benchmark/benches/ty_walltime.rs index 83653d1afb..173117a14a 100644 --- a/crates/ruff_benchmark/benches/ty_walltime.rs +++ b/crates/ruff_benchmark/benches/ty_walltime.rs @@ -110,7 +110,7 @@ static ALTAIR: Benchmark = Benchmark::new( max_dep_date: TY_ECOSYSTEM_PIN, python_version: SupportedPythonVersion::Py311, }, - 5, + 9, ); static COLOUR_SCIENCE: Benchmark = Benchmark::new( diff --git a/crates/ty/docs/configuration.md b/crates/ty/docs/configuration.md index 94143a37b7..d60457fc28 100644 --- a/crates/ty/docs/configuration.md +++ b/crates/ty/docs/configuration.md @@ -266,6 +266,49 @@ def narrow_match(x: str) -> None: --- +### `strict-generic-narrowing` + +Whether ty should use strict narrowing for unspecialized generic classes in +`isinstance()` and `issubclass()` checks, as well as `match` class patterns. + +When enabled, ty narrows to the top materialization of the class. For example, +`isinstance(value, list)` narrows a value of type `object` to `Top[list[Unknown]]`, +representing the (infinite) union of all possible `list` specializations. Iterating +over the list would yield values of type `object`. + +When disabled, ty uses gradual generic narrowing, preserving compatible type +arguments from the original type where possible. For example, +`isinstance(value, list)` narrows a value of type `Sequence[int]` to `list[int]`. +If no specialization is available, the same check narrows a value of type `object` +to `list[Unknown]`; items of any type can then be appended to the list. Class +patterns such as `case list():` follow the same behavior. + +Defaults to `false`. + +**Default value**: `false` + +**Type**: `bool` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.ty.analysis] + # Use the top materialization when narrowing to an unspecialized generic class + strict-generic-narrowing = true + ``` + +=== "ty.toml" + + ```toml + [analysis] + # Use the top materialization when narrowing to an unspecialized generic class + strict-generic-narrowing = true + ``` + +--- + ## `environment` ### `extra-paths` @@ -866,6 +909,49 @@ def narrow_match(x: str) -> None: --- +#### `strict-generic-narrowing` + +Whether ty should use strict narrowing for unspecialized generic classes in +`isinstance()` and `issubclass()` checks, as well as `match` class patterns. + +When enabled, ty narrows to the top materialization of the class. For example, +`isinstance(value, list)` narrows a value of type `object` to `Top[list[Unknown]]`, +representing the (infinite) union of all possible `list` specializations. Iterating +over the list would yield values of type `object`. + +When disabled, ty uses gradual generic narrowing, preserving compatible type +arguments from the original type where possible. For example, +`isinstance(value, list)` narrows a value of type `Sequence[int]` to `list[int]`. +If no specialization is available, the same check narrows a value of type `object` +to `list[Unknown]`; items of any type can then be appended to the list. Class +patterns such as `case list():` follow the same behavior. + +Defaults to `false`. + +**Default value**: `false` + +**Type**: `bool` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.ty.overrides.analysis] + # Use the top materialization when narrowing to an unspecialized generic class + strict-generic-narrowing = true + ``` + +=== "ty.toml" + + ```toml + [overrides.analysis] + # Use the top materialization when narrowing to an unspecialized generic class + strict-generic-narrowing = true + ``` + +--- + ## `src` ### `exclude` diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 446bef4a56..a93d0ea234 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -1438,6 +1438,32 @@ pub struct TerminalOptions { #[serde(rename_all = "kebab-case", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct AnalysisOptions { + /// Whether ty should use strict narrowing for unspecialized generic classes in + /// `isinstance()` and `issubclass()` checks, as well as `match` class patterns. + /// + /// When enabled, ty narrows to the top materialization of the class. For example, + /// `isinstance(value, list)` narrows a value of type `object` to `Top[list[Unknown]]`, + /// representing the (infinite) union of all possible `list` specializations. Iterating + /// over the list would yield values of type `object`. + /// + /// When disabled, ty uses gradual generic narrowing, preserving compatible type + /// arguments from the original type where possible. For example, + /// `isinstance(value, list)` narrows a value of type `Sequence[int]` to `list[int]`. + /// If no specialization is available, the same check narrows a value of type `object` + /// to `list[Unknown]`; items of any type can then be appended to the list. Class + /// patterns such as `case list():` follow the same behavior. + /// + /// Defaults to `false`. + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + # Use the top materialization when narrowing to an unspecialized generic class + strict-generic-narrowing = true + "# + )] + pub strict_generic_narrowing: Option, + /// Configure ty's behavior regarding type inference and narrowing of equality /// checks. Defaults to `false`. /// @@ -1604,6 +1630,7 @@ impl AnalysisOptions { diagnostics: &mut Vec, ) -> AnalysisSettings { let Self { + strict_generic_narrowing, strict_equality_semantics, respect_type_ignore_comments, allowed_unresolved_imports, @@ -1611,6 +1638,7 @@ impl AnalysisOptions { } = self; let AnalysisSettings { + strict_generic_narrowing: strict_generic_narrowing_default, strict_equality_semantics: strict_equality_semantics_default, respect_type_ignore_comments: respect_type_ignore_default, allowed_unresolved_imports: allowed_unresolved_imports_default, @@ -1640,6 +1668,8 @@ impl AnalysisOptions { }; AnalysisSettings { + strict_generic_narrowing: strict_generic_narrowing + .unwrap_or(strict_generic_narrowing_default), strict_equality_semantics: strict_equality_semantics .unwrap_or(strict_equality_semantics_default), respect_type_ignore_comments: respect_type_ignore_comments diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index 70172094d6..163523ba4b 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -517,6 +517,11 @@ for function in map(Function, [object()]): Several `dict` overloads accept one positional argument. When none matches, an arbitrarily selected overload must not make an otherwise compatible return type fail. +```toml +[analysis] +strict-generic-narrowing = true +``` + ```py from collections.abc import Mapping @@ -531,6 +536,11 @@ def copy(value: object) -> dict[str, str]: An invalid `dict` call must not invalidate an assignment inside a branch where the original value has already been narrowed to a mapping. +```toml +[analysis] +strict-generic-narrowing = true +``` + ```py from collections.abc import Mapping diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index b85d7dadd4..da46edabc7 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -679,14 +679,13 @@ def _(x: Sequence[int], y: object): reveal_type(item) # revealed: int if isinstance(y, list): - reveal_type(y) # revealed: Top[list[Unknown]] + reveal_type(y) # revealed: list[Unknown] for item in y: - reveal_type(item) # revealed: object + reveal_type(item) # revealed: Unknown if isinstance(x, list): - reveal_type(x) # revealed: Sequence[int] & Top[list[Unknown]] + reveal_type(x) # revealed: list[int] for item in x: - # int & object simplifies to int reveal_type(item) # revealed: int ``` @@ -1541,12 +1540,10 @@ simplify to `Never`, leaving only the iterable parts. ```py def f[T: tuple[int, ...] | int](x: T): if isinstance(x, tuple): - reveal_type(x) # revealed: T@f & tuple[object, ...] + reveal_type(x) # revealed: T@f & tuple[int, ...] for item in x: - # The intersection `(tuple[int, ...] | int) & tuple[object, ...]` distributes to: - # `(tuple[int, ...] & tuple[object, ...]) | (int & tuple[object, ...])` - # which simplifies to `tuple[int, ...] | Never` = `tuple[int, ...]` - # so iterating gives `int`. + # The `int` alternative in the TypeVar bound is disjoint from `tuple`. The + # remaining `tuple[int, ...]` alternative supplies the narrowed specialization. reveal_type(item) # revealed: int ``` @@ -1558,13 +1555,10 @@ constraint, those parts should also simplify to `Never`. ```py def g[T: tuple[int, ...] | list[str]](x: T): if isinstance(x, tuple): - reveal_type(x) # revealed: T@g & tuple[object, ...] + reveal_type(x) # revealed: T@g & tuple[int, ...] for item in x: - # The intersection `(tuple[int, ...] | list[str]) & tuple[object, ...]` distributes to: - # `(tuple[int, ...] & tuple[object, ...]) | (list[str] & tuple[object, ...])` - # Since `list[str]` is disjoint from `tuple[object, ...]`, this simplifies to: - # `tuple[int, ...] | Never` = `tuple[int, ...]` - # so iterating gives `int`, NOT `int | str`. + # The `list[str]` alternative in the TypeVar bound is disjoint from `tuple`. The + # remaining `tuple[int, ...]` alternative supplies the narrowed specialization. reveal_type(item) # revealed: int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md index 613af0996b..66f5795d93 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/callable.md @@ -54,7 +54,15 @@ def f(x: object): ## Calling narrowed callables -The narrowed type `Top[Callable[..., object]]` represents the set of all possible callable types +### Strict generic narrowing mode + +```toml +[analysis] +strict-generic-narrowing = true +``` + +In strict generic narrowing mode, an `isinstance(.., Callable)` check intersects the type with +`Top[Callable[..., object]]`. This type represents the set of all possible callable types (including, e.g., functions that take no arguments and functions that require arguments). While such objects *are* callable (they pass `callable()`), no specific set of arguments can be guaranteed to be valid. @@ -80,6 +88,36 @@ def resolve(value: str): reveal_type(value()) # revealed: object ``` +### Gradual generic narrowing mode + +```toml +[analysis] +strict-generic-narrowing = false +``` + +In gradual generic narrowing mode, an `isinstance(.., Callable)` check narrows to a gradual +callable. Its parameters accept arbitrary arguments, and its return type is `Unknown`: + +```py +from typing import Callable + +def call_with_args(y: object): + if isinstance(y, Callable): + reveal_type(y) # revealed: (...) -> Unknown + + reveal_type(y()) # revealed: Unknown + reveal_type(y(1, "foo")) # revealed: Unknown + reveal_type(y(1, "foo", keyword_arg="bar")) # revealed: Unknown +``` + +An already-specialized callable retains its known parameter and return types: + +```py +def preserve_callable_signature(fn: Callable[[int], str]) -> None: + if isinstance(fn, Callable): + reveal_type(fn) # revealed: (int, /) -> str +``` + ## Narrowing with named expressions (walrus operator) When `callable()` is used with a named expression, the target of the named expression should be @@ -139,9 +177,14 @@ import collections.abc def f(x: object): if isinstance(x, typing.Callable): - reveal_type(x) # revealed: Top[(...) -> object] + reveal_type(x) # revealed: (...) -> Unknown + else: + reveal_type(x) # revealed: ~Top[(...) -> object] + if isinstance(x, collections.abc.Callable): - reveal_type(x) # revealed: Top[(...) -> object] + reveal_type(x) # revealed: (...) -> Unknown + else: + reveal_type(x) # revealed: ~Top[(...) -> object] ``` ## `Callable` special-form identity diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md index f6ae86bf6e..c9528545ee 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md @@ -1157,6 +1157,11 @@ After the `isinstance` check, `values` has type `Iterable[Literal[1]] & tuple[ob semantics were checked: the `tuple` component establishes that membership compares against its elements, while the `Iterable` component constrains those elements to `Literal[1]`. +```toml +[analysis] +strict-generic-narrowing = true +``` + ```py from collections.abc import Iterable from typing import Literal, final diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index ebcee31192..5c296845f0 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -305,7 +305,7 @@ def f(x: dict[str, int] | list[str], y: object): reveal_type(x) # revealed: list[str] if isinstance(y, t.Callable): - reveal_type(y) # revealed: Top[(...) -> object] + reveal_type(y) # revealed: (...) -> Unknown ``` ## Class types @@ -603,14 +603,19 @@ def f(x: Foo, y: Intersection[type[Bar], type[list[int]]]): ## Narrowing with generics +### Strict mode + ```toml [environment] python-version = "3.12" + +[analysis] +strict-generic-narrowing = true ``` -Narrowing to a generic class using `isinstance()` uses the top materialization of the generic. With -a covariant generic, this is equivalent to using the upper bound of the type parameter (by default, -`object`): +In strict mode, narrowing to a generic class using `isinstance()` uses the top materialization of +the generic. With a covariant generic, this is equivalent to using the upper bound of the type +parameter (by default, `object`): ```py from typing import Self @@ -841,13 +846,451 @@ def excludes_bounded_generic_subclass( return cls ``` -## Narrowing recursively bounded generics +### Gradual mode + +```toml +[environment] +python-version = "3.12" + +[analysis] +strict-generic-narrowing = false +``` + +In gradual mode, narrowing to a generic class using `isinstance()` preserves any compatible +specialization from the original type. If the original type does not provide a specialization, we +intersect with the `Unknown` specialization. The negative branch still excludes the top +materialization because a failed `isinstance()` check rules out every specialization of the class. + +```py +class Covariant[T]: + def get(self) -> T: + raise NotImplementedError + +def _(x: object): + if isinstance(x, Covariant): + # `object & Covariant[Unknown]` simplifies to `Covariant[Unknown]`. + reveal_type(x) # revealed: Covariant[Unknown] + reveal_type(x.get()) # revealed: Unknown + else: + reveal_type(x) # revealed: ~Covariant[object] +``` + +For contravariant generics, we similarly intersect with the `Unknown` specialization: + +```py +class Contravariant[T]: + def push(self, x: T) -> None: ... + +def _(x: object): + if isinstance(x, Contravariant): + reveal_type(x) # revealed: Contravariant[Unknown] + x.push(42) + x.push("foo") + else: + reveal_type(x) # revealed: ~Contravariant[Never] +``` + +Similarly, for invariant generics we intersect with the `Unknown` specialization. Reading produces +`Unknown`, while writing accepts arguments of any type: + +```py +class Invariant[T]: + def push(self, x: T) -> None: ... + def get(self) -> T: + raise NotImplementedError + +def _(x: object): + if isinstance(x, Invariant): + reveal_type(x) # revealed: Invariant[Unknown] + reveal_type(x.get) # revealed: bound method Invariant[Unknown].get() -> Unknown + reveal_type(x.get()) # revealed: Unknown + reveal_type(x.push) # revealed: bound method Invariant[Unknown].push(x: Unknown) -> None + x.push(42) + x.push("foo") + else: + reveal_type(x) # revealed: ~Top[Invariant[Unknown]] +``` + +Narrowing already specialized generics preserves their concrete type arguments: + +```py +class P: ... + +def _(x: Covariant[P], y: Contravariant[P], z: Invariant[P]): + if isinstance(x, Covariant): + reveal_type(x) # revealed: Covariant[P] + if isinstance(y, Contravariant): + reveal_type(y) # revealed: Contravariant[P] + if isinstance(z, Invariant): + reveal_type(z) # revealed: Invariant[P] +``` + +Specialized base classes also determine the type arguments of matching subclasses, including +subclasses with a stricter variance: + +```py +class SubOfCovariant[T](Covariant[T]): ... +class SubOfContravariant[T](Contravariant[T]): ... +class SubOfInvariant[T](Invariant[T]): ... + +class InvariantSubOfCovariant[T](Covariant[T]): + def push(self, value: T) -> None: ... + +class InvariantSubOfContravariant[T](Contravariant[T]): + def get(self) -> T: + raise NotImplementedError + +def narrow_generic_subclasses(covariant: Covariant[P], contravariant: Contravariant[P], invariant: Invariant[P]) -> None: + if isinstance(covariant, SubOfCovariant): + reveal_type(covariant) # revealed: SubOfCovariant[P] + + if isinstance(contravariant, SubOfContravariant): + reveal_type(contravariant) # revealed: SubOfContravariant[P] + + if isinstance(invariant, SubOfInvariant): + reveal_type(invariant) # revealed: SubOfInvariant[P] + + if isinstance(covariant, InvariantSubOfCovariant): + reveal_type(covariant) # revealed: InvariantSubOfCovariant[P] + + if isinstance(contravariant, InvariantSubOfContravariant): + reveal_type(contravariant) # revealed: InvariantSubOfContravariant[P] +``` + +Narrowing unions and intersections preserves unrelated types when they can overlap with the checked +class, while excluding unrelated final classes: + +```py +from typing import Sequence, final +from ty_extensions import Intersection + +@final +class Item: ... + +class OpenItem: ... + +def _(value: Item | OpenItem | Sequence[int]) -> None: + if isinstance(value, list): + reveal_type(value) # revealed: (OpenItem & list[Unknown]) | list[int] + +def _( + value: Intersection[OpenItem, Sequence[int]], +) -> None: + if isinstance(value, list): + reveal_type(value) # revealed: OpenItem & list[int] +``` + +When an intersection contains multiple specialized bases, each base contributes its known type +arguments to a matching subclass: + +```py +class Left[L]: ... +class Right[R]: ... + +class Both[L, R](Left[L], Right[R]): + left: L + right: R + +def _(value: Intersection[Left[int], Right[str]]) -> None: + if isinstance(value, Both): + reveal_type(value) # revealed: Both[int, str] + reveal_type(value.left) # revealed: int + reveal_type(value.right) # revealed: str +``` + +Subclass type arguments are inferred through their actual inheritance relationship, so this also +works correctly if type parameters change position: + +```py +class Base[A, B]: ... +class Child[X, Y](Base[Y, X]): ... + +def _(value: Base[int, str]) -> None: + if isinstance(value, Child): + reveal_type(value) # revealed: Child[str, int] +``` + +A subclass type parameter that cannot be inferred from its base remains `Unknown`: + +```py +class PartiallyInferredChild[Extra1, T, Extra2](Sequence[T]): ... + +def _(value: Sequence[int]) -> None: + if isinstance(value, PartiallyInferredChild): + reveal_type(value) # revealed: PartiallyInferredChild[Unknown, int, Unknown] +``` + +If we're "narrowing" in the opposite direction, we retain the existing subclass specialization: + +```py +def _(covariant: SubOfCovariant[P], contravariant: SubOfContravariant[P], invariant: SubOfInvariant[P]) -> None: + if isinstance(covariant, Covariant): + reveal_type(covariant) # revealed: SubOfCovariant[P] + + if isinstance(contravariant, Contravariant): + reveal_type(contravariant) # revealed: SubOfContravariant[P] + + if isinstance(invariant, Invariant): + reveal_type(invariant) # revealed: SubOfInvariant[P] +``` + +This also works for runtime-checkable protocols: + +```py +from typing import Protocol, runtime_checkable + +@runtime_checkable +class Reader[T](Protocol): + def read(self) -> T: ... + +class Concrete[T]: + def read(self) -> T: + raise NotImplementedError + +def _(value: Concrete[int]) -> None: + if isinstance(value, Reader): + reveal_type(value) # revealed: Concrete[int] + reveal_type(value.read()) # revealed: int +``` + +## Use cases: `isinstance` narrowing and generics + +### Strict mode + +```toml +[analysis] +strict-generic-narrowing = true +``` + +#### Covariance + +Narrowing from `object` via `isinstance(.., Sequence)`: + +```py +from typing import Sequence, final + +def _(xs: object): + if isinstance(xs, Sequence): + reveal_type(xs) # revealed: Sequence[object] + for x in xs: + reveal_type(x) # revealed: object + else: + reveal_type(xs) # revealed: ~Sequence[object] +``` + +Narrowing from `Item | Sequence[Item]` via `isinstance(.., Sequence)`: + +```py +@final +class Item: ... + +def _(xs: Item | Sequence[Item]): + if isinstance(xs, Sequence): + reveal_type(xs) # revealed: Sequence[Item] + for x in xs: + reveal_type(x) # revealed: Item + else: + reveal_type(xs) # revealed: Item +``` + +Narrowing from (non-final) `OpenItem | Sequence[OpenItem]` via `isinstance(.., Sequence)`: + +```py +class OpenItem: ... + +def _(xs: OpenItem | Sequence[OpenItem]): + if isinstance(xs, Sequence): + reveal_type(xs) # revealed: (OpenItem & Sequence[object]) | Sequence[OpenItem] + for x in xs: + reveal_type(x) # revealed: object + else: + reveal_type(xs) # revealed: OpenItem & ~Sequence[object] +``` + +#### Invariance + +Narrowing from `object` via `isinstance(.., list)`: + +```py +def _(xs: object): + if isinstance(xs, list): + reveal_type(xs) # revealed: Top[list[Unknown]] + for x in xs: + reveal_type(x) # revealed: object + + # This is an error in strict mode: + # error: [invalid-argument-type] "Expected `Never`, found `Literal[1]`" + xs.append(1) + + else: + reveal_type(xs) # revealed: ~Top[list[Unknown]] +``` + +Narrowing from `Item | list[Item]` via `isinstance(.., list)`: + +```py +from typing import final + +@final +class Item: ... + +def _(xs: Item | list[Item]): + if isinstance(xs, list): + reveal_type(xs) # revealed: list[Item] + for x in xs: + reveal_type(x) # revealed: Item + else: + reveal_type(xs) # revealed: Item +``` + +Narrowing from (non-final) `OpenItem | list[OpenItem]` via `isinstance(.., list)`: + +```py +class OpenItem: ... + +def _(xs: OpenItem | list[OpenItem]): + if isinstance(xs, list): + reveal_type(xs) # revealed: (OpenItem & Top[list[Unknown]]) | list[OpenItem] + for x in xs: + reveal_type(x) # revealed: object + else: + reveal_type(xs) # revealed: OpenItem & ~Top[list[Unknown]] +``` + +#### Exhaustiveness checking + +```py +def _(xs: list[str] | set[str]) -> str: + if isinstance(xs, list): + return "it's a list!" + elif isinstance(xs, set): + return "it's a set!" +``` + +### Gradual mode + +```toml +[environment] +python-version = "3.12" + +[analysis] +strict-generic-narrowing = false +``` + +#### Covariance + +Narrowing from `object` via `isinstance(.., Sequence)`: + +```py +from typing import Sequence, final + +def _(xs: object): + if isinstance(xs, Sequence): + reveal_type(xs) # revealed: Sequence[Unknown] + for x in xs: + reveal_type(x) # revealed: Unknown + else: + reveal_type(xs) # revealed: ~Sequence[object] +``` + +Narrowing from `Item | Sequence[Item]` via `isinstance(.., Sequence)`: + +```py +@final +class Item: ... + +def _(xs: Item | Sequence[Item]): + if isinstance(xs, Sequence): + reveal_type(xs) # revealed: Sequence[Item] + for x in xs: + reveal_type(x) # revealed: Item + else: + reveal_type(xs) # revealed: Item +``` + +Narrowing from (non-final) `OpenItem | Sequence[OpenItem]` via `isinstance(.., Sequence)`: + +```py +class OpenItem: ... + +def _(xs: OpenItem | Sequence[OpenItem]): + if isinstance(xs, Sequence): + reveal_type(xs) # revealed: (OpenItem & Sequence[Unknown]) | Sequence[OpenItem] + for x in xs: + reveal_type(x) # revealed: Unknown | OpenItem + else: + reveal_type(xs) # revealed: OpenItem & ~Sequence[object] +``` + +#### Invariance + +Narrowing from `object` via `isinstance(.., list)`: + +```py +def _(xs: object): + if isinstance(xs, list): + reveal_type(xs) # revealed: list[Unknown] + for x in xs: + reveal_type(x) # revealed: Unknown + + xs.append(1) + xs.append("foo") + + else: + reveal_type(xs) # revealed: ~Top[list[Unknown]] +``` + +Narrowing from `Item | list[Item]` via `isinstance(.., list)`: + +```py +from typing import final + +@final +class Item: ... + +def _(xs: Item | list[Item]): + if isinstance(xs, list): + reveal_type(xs) # revealed: list[Item] + for x in xs: + reveal_type(x) # revealed: Item + else: + reveal_type(xs) # revealed: Item +``` + +Narrowing from (non-final) `OpenItem | list[OpenItem]` via `isinstance(.., list)`: + +```py +class OpenItem: ... + +def _(xs: OpenItem | list[OpenItem]): + if isinstance(xs, list): + reveal_type(xs) # revealed: (OpenItem & list[Unknown]) | list[OpenItem] + for x in xs: + reveal_type(x) # revealed: Unknown | OpenItem + else: + reveal_type(xs) # revealed: OpenItem & ~Top[list[Unknown]] +``` + +#### Exhaustiveness checking + +```py +def _(xs: list[str] | set[str]) -> str: + if isinstance(xs, list): + return "it's a list!" + elif isinstance(xs, set): + return "it's a set!" +``` + +## Narrowing recursively bounded generics (strict mode) An `isinstance()` check must not recurse indefinitely when a generic bound refers to its own class. ```toml [environment] python-version = "3.12" + +[analysis] +strict-generic-narrowing = true ``` ```py @@ -886,6 +1329,54 @@ def narrow_mutual(value: object) -> None: reveal_type(value) # revealed: Right[object] ``` +## Narrowing recursively bounded generics (gradual mode) + +An `isinstance()` check must not recurse indefinitely when a generic bound refers to its own class. + +```toml +[environment] +python-version = "3.12" + +[analysis] +strict-generic-narrowing = false +``` + +```py +from typing import Any + +class Recursive[T: "Recursive[Any]"]: ... + +def narrow(value: object) -> None: + if isinstance(value, Recursive): + reveal_type(value) # revealed: Recursive[Unknown] +``` + +A self-referential bound must also be safe when its recursion is hidden behind a type alias. + +```py +class AliasedRecursive[T: "RecursiveAlias"]: ... + +type RecursiveAlias = AliasedRecursive[Any] + +def narrow_alias(value: object) -> None: + if isinstance(value, AliasedRecursive): + reveal_type(value) # revealed: AliasedRecursive[Unknown] +``` + +The same cycle recovery must handle bounds shared by mutually recursive generic classes. + +```py +class Left[T: "Right[Any]"]: ... +class Right[U: Left[Any]]: ... + +def narrow_mutual(value: object) -> None: + if isinstance(value, Left): + reveal_type(value) # revealed: Left[Unknown] + + if isinstance(value, Right): + reveal_type(value) # revealed: Right[Unknown] +``` + ## Narrowing generic defaults in Python 3.13 When a type parameter has a bare `Any` default, narrowing still materializes the substituted @@ -895,6 +1386,9 @@ instead), so the default value is irrelevant here: ```toml [environment] python-version = "3.13" + +[analysis] +strict-generic-narrowing = true ``` ```py @@ -947,8 +1441,9 @@ def box_with_default[T: str = str](value: Box[T] | T) -> Box[T]: assert_never(value) ``` -When `isinstance()` narrows an unknown value to a tuple subclass, its type argument comes from the -declared upper bound, not the default. Its element types are inherited from the specialized base. +When `isinstance()` narrows a value of type `object` to a tuple subclass, its type argument comes +from the declared upper bound, not the default. Its element types are inherited from the specialized +base. ```py class DefaultedTuple[T: int = bool](tuple[T, str]): ... @@ -974,6 +1469,29 @@ def excludes_defaulted_tuple(value: DefaultedTuple[Any] | bool) -> bool: return value ``` +## Narrowing bounded generic defaults in gradual mode + +In gradual mode, narrowing a value of type `object` to a tuple subclass leaves its type argument +`Unknown`. + +```toml +[environment] +python-version = "3.13" + +[analysis] +strict-generic-narrowing = false +``` + +```py +class DefaultedTuple[T: int = bool](tuple[T, str]): ... + +def narrow_defaulted_tuple(value: object) -> None: + if isinstance(value, DefaultedTuple): + reveal_type(value) # revealed: DefaultedTuple[Unknown] + reveal_type(value[0]) # revealed: Unknown + reveal_type(value[1]) # revealed: str +``` + ## Narrowing generic `classmethod` After an `isinstance(..., classmethod)` branch unwraps and replaces a generic `classmethod`, the @@ -1051,3 +1569,75 @@ def f(): reveal_type(value) # revealed: str reveal_type(result) # revealed: Literal[False] ``` + +## Preserving TypedDict interfaces when narrowing mappings + +A `TypedDict` is always a dictionary at runtime, but its static interface deliberately disallows +operations that could remove required keys or introduce undeclared ones. Narrowing to `dict`, +`Mapping`, or `MutableMapping` must not discard these restrictions. + +Use a `TypedDict` with one required key and one optional key to distinguish safe operations from +those that could invalidate its declared shape. + +```py +from typing import TypedDict, Mapping, MutableMapping +from typing_extensions import NotRequired + +class Payload(TypedDict): + key: int + optional: NotRequired[str] +``` + +Narrowing directly to `dict` preserves both the required-key restrictions and the optional key's +known type. + +```py +def narrow_typed_dict_to_dict(value: int | Payload) -> None: + if isinstance(value, dict): + reveal_type(value) # revealed: Payload + reveal_type(value["key"]) # revealed: int + value["key"] = 1 + value["optional"] = "present" + reveal_type(value.pop("optional")) # revealed: str + + # error: [unresolved-attribute] + value.clear() + # error: [invalid-argument-type] "Cannot pop required field 'key' from TypedDict `Payload`" + value.pop("key") + # error: [invalid-key] "Unknown key "unexpected" for TypedDict `Payload`" + value["unexpected"] = 1 + # error: [invalid-argument-type] "Cannot delete required key "key" from TypedDict `Payload`" + del value["key"] +``` + +Same for `MutableMapping`: + +```py +def narrow_typed_dict_to_mutable_mapping(value: Payload) -> None: + if isinstance(value, MutableMapping): + reveal_type(value) # revealed: Payload + # error: [unresolved-attribute] + value.clear() +``` + +And for `Mapping`: + +```py +def narrow_typed_dict_to_mapping(value: Payload) -> None: + if isinstance(value, Mapping): + reveal_type(value) # revealed: Payload + # error: [unresolved-attribute] + value.clear() +``` + +A type alias must retain the same `TypedDict` interface. + +```py +PayloadAlias = Payload + +def narrow_aliased_typed_dict_to_dict(value: PayloadAlias) -> None: + if isinstance(value, dict): + reveal_type(value) # revealed: Payload + # error: [unresolved-attribute] + value.clear() +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md index 3209b9fdbf..96591c250a 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md @@ -280,6 +280,63 @@ def f(x: type[int | str | bytes | range]): reveal_type(x) # revealed: ``` +## Narrowing with generic classes + +### Strict mode + +```toml +[analysis] +strict-generic-narrowing = true +``` + +Without a known specialization, narrowing to a generic class uses the top materialization: + +```py +def _(cls: type) -> None: + if issubclass(cls, list): + reveal_type(cls) # revealed: type[Top[list[Unknown]]] + reveal_type(cls()) # revealed: Top[list[Unknown]] +``` + +When narrowing from a generic superclass to a generic subclass, we intersect with the top +materialization of the subclass: + +```py +from typing import Sequence + +def narrow_sequence_to_list(cls: type[Sequence[int]]) -> None: + if issubclass(cls, list): + reveal_type(cls) # revealed: type[Sequence[int]] & type[Top[list[Unknown]]] + reveal_type(cls()) # revealed: Sequence[int] & Top[list[Unknown]] +``` + +### Gradual mode + +```toml +[analysis] +strict-generic-narrowing = false +``` + +Without a known specialization, narrowing to a generic class leaves its type argument unknown. + +```py +def _(cls: type) -> None: + if issubclass(cls, list): + reveal_type(cls) # revealed: type[list[Unknown]] + reveal_type(cls()) # revealed: list[Unknown] +``` + +Narrowing to a generic subclass preserves the specialized base class's type argument. + +```py +from typing import Sequence + +def _(cls: type[Sequence[int]]) -> None: + if issubclass(cls, list): + reveal_type(cls) # revealed: type[list[int]] + reveal_type(cls()) # revealed: list[int] +``` + ## `classinfo` is a generic final class ```toml diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index 5b4b938112..ea67ca70b9 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -91,9 +91,16 @@ def exhaustive_pattern_with_guard(x: A, flag: bool) -> None: ## Class patterns with generic classes +### Gradual mode + +Generic class patterns follow the same gradual filtering as `isinstance()` checks. + ```toml [environment] python-version = "3.12" + +[analysis] +strict-generic-narrowing = false ``` ```py @@ -112,6 +119,44 @@ def f(x: Covariant[int]): assert_never(x) ``` +A `list()` pattern preserves the type argument inherited from a specialized `Sequence`. + +```py +from typing import Sequence + +def narrow_sequence_to_list(value: Sequence[int]) -> None: + match value: + case list(): + reveal_type(value) # revealed: list[int] + case _: + reveal_type(value) # revealed: Sequence[int] & ~Top[list[Unknown]] +``` + +### Strict mode + +With strict generic narrowing enabled, class patterns retain their top materializations. + +```toml +[environment] +python-version = "3.12" + +[analysis] +strict-generic-narrowing = true +``` + +A `list()` pattern retains the original `Sequence` alongside the top-materialized list. + +```py +from typing import Sequence + +def narrow_sequence_to_list(value: Sequence[int]) -> None: + match value: + case list(): + reveal_type(value) # revealed: Sequence[int] & Top[list[Unknown]] + case _: + reveal_type(value) # revealed: Sequence[int] & ~Top[list[Unknown]] +``` + ## Generic patterns ignore type parameter defaults A generic class pattern matches every runtime specialization, not only the specialization described @@ -120,6 +165,9 @@ by its type parameter's default. ```toml [environment] python-version = "3.13" + +[analysis] +strict-generic-narrowing = true ``` ```py @@ -203,7 +251,7 @@ from typing import Any def test_isinstance(x: dict[Any, Any] | int) -> None: if isinstance(x, Mapping): - reveal_type(x) # revealed: dict[Any, Any] | (int & Top[Mapping[Unknown, object]]) + reveal_type(x) # revealed: dict[Any, Any] | (int & Mapping[Unknown, Unknown]) else: reveal_type(x) # revealed: int & ~Top[Mapping[Unknown, object]] @@ -976,12 +1024,176 @@ def test_incompatible_declared_class_capture(value: PatternBox[int]) -> None: ## Generic subclass captures -When a generic pattern class inherits from the subject's class through an invariant base, the -subject specialization determines the pattern class's type arguments. This applies to annotated -attributes and properties. Every pattern-class type parameter must have an exact solution; variant -bases and unconstrained parameters retain the existing conservative fallback. When the subject does -not provide type arguments, members declared by the pattern class use `Unknown`; a type parameter -default does not restrict which instances match at runtime. +### Gradual mode + +When a generic pattern class inherits from the subject's class, the subject specialization +determines any inferable pattern-class type arguments. This applies to annotated attributes and +properties, including classes with unconstrained type parameters or variant bases. When the subject +does not provide type arguments, members declared by the pattern class use `Unknown`; a type +parameter default does not restrict which instances match at runtime. + +```toml +[analysis] +strict-generic-narrowing = false +``` + +```py +from typing import final, Generic +from typing_extensions import TypeVar + +GenericPatternT = TypeVar("GenericPatternT") +ExtraGenericPatternT = TypeVar("ExtraGenericPatternT") +CovariantGenericPatternT = TypeVar("CovariantGenericPatternT", covariant=True) +DefaultGenericPatternT = TypeVar("DefaultGenericPatternT", default=str) + +class GenericPatternBase(Generic[GenericPatternT]): ... + +OptionalGenericPatternT = TypeVar( + "OptionalGenericPatternT", + bound=GenericPatternBase[int] | None, +) +UnionBoundGenericPatternT = TypeVar( + "UnionBoundGenericPatternT", + bound=GenericPatternBase[int] | GenericPatternBase[str], +) + +class GenericPatternChild(GenericPatternBase[GenericPatternT]): + item: GenericPatternT + items: list[GenericPatternT] + +class PartiallySpecializedGenericPatternChild( + GenericPatternBase[GenericPatternT], + Generic[GenericPatternT, ExtraGenericPatternT], +): + item: GenericPatternT + +class CovariantGenericPatternBase(Generic[CovariantGenericPatternT]): ... + +class CovariantGenericPatternChild(CovariantGenericPatternBase[CovariantGenericPatternT]): + item: CovariantGenericPatternT + +class GenericMemberBase(Generic[GenericPatternT]): + item: GenericPatternT + +class GenericMemberChild(GenericMemberBase[GenericPatternT]): ... +class IntGenericMemberChild(GenericMemberBase[int]): ... + +@final +class FinalGenericPatternBox(Generic[GenericPatternT]): + value: list[GenericPatternT] + +class DefaultGenericPatternBox(Generic[DefaultGenericPatternT]): + value: DefaultGenericPatternT + +ResultValueT = TypeVar("ResultValueT") +ResultErrorT = TypeVar("ResultErrorT") + +class MatchResult(Generic[ResultValueT, ResultErrorT]): ... + +class MatchOk(MatchResult[ResultValueT, ResultErrorT]): + __match_args__ = ("value",) + + @property + def value(self) -> ResultValueT: + raise NotImplementedError + +class MatchErr(MatchResult[ResultValueT, ResultErrorT]): + __match_args__ = ("error",) + + @property + def error(self) -> ResultErrorT: + raise NotImplementedError + +def test_match_generic_subclass_property_capture( + result: MatchResult[int, str], +) -> int: + match result: + case MatchOk(value): + reveal_type(value) # revealed: int + return value + case MatchErr(error): + reveal_type(error) # revealed: str + raise ValueError(error) + raise AssertionError + +def test_match_generic_subclass_capture(value: GenericPatternBase[int]) -> None: + match value: + case GenericPatternChild(item=item): + reveal_type(item) # revealed: int + +def test_match_generic_subclass_capture_from_optional_typevar_bound( + value: OptionalGenericPatternT, +) -> None: + match value: + case GenericPatternChild(item=item): + reveal_type(item) # revealed: int + +def test_match_generic_subclass_capture_from_union_typevar_bound( + value: UnionBoundGenericPatternT, +) -> None: + match value: + case GenericPatternChild(item=item): + reveal_type(item) # revealed: int | str + +def test_match_nested_generic_subclass_capture(value: GenericPatternBase[int]) -> list[int]: + match value: + case GenericPatternChild(items=items): + reveal_type(items) # revealed: list[int] + return items + return [] + +def test_match_partially_specialized_generic_subclass( + value: GenericPatternBase[int], +) -> None: + match value: + case PartiallySpecializedGenericPatternChild(item=item): + reveal_type(item) # revealed: int + +def test_match_covariant_generic_subclass( + value: CovariantGenericPatternBase[int], +) -> None: + match value: + case CovariantGenericPatternChild(item=item): + reveal_type(item) # revealed: int + +def test_match_inherited_generic_subclass_capture( + value: GenericMemberBase[GenericPatternT], +) -> GenericPatternT: + match value: + case GenericMemberChild(item=item): + # revealed: GenericPatternT@test_match_inherited_generic_subclass_capture + reveal_type(item) + return item + case _: + raise ValueError + +def test_match_generic_base_capture_preserves_subject_specialization( + value: IntGenericMemberChild, +) -> None: + match value: + case GenericMemberBase(item=item): + reveal_type(item) # revealed: int + +def test_match_direct_generic_pattern_preserves_declared_member(value: object) -> None: + match value: + case FinalGenericPatternBox(value=int() as item): + reveal_type(item) # revealed: Never + +def test_match_generic_pattern_ignores_typevar_default(value: object) -> None: + match value: + case DefaultGenericPatternBox(value=int() as item): + reveal_type(item) # revealed: Unknown & int +``` + +### Strict mode + +An invariant generic base determines its subclass's type arguments only when every argument has one +exact solution. Unconstrained arguments and variant bases retain conservative member types. + +```toml +[analysis] +strict-generic-narrowing = true +``` ```py from typing import final, Generic @@ -1093,8 +1305,6 @@ def test_match_partially_specialized_generic_subclass( ) -> None: match value: case PartiallySpecializedGenericPatternChild(item=item): - # `ExtraGenericPatternT` is not constrained by the subject, so the pattern class does - # not have one exact specialization. reveal_type(item) # revealed: Unknown def test_match_covariant_generic_subclass( @@ -1102,7 +1312,6 @@ def test_match_covariant_generic_subclass( ) -> None: match value: case CovariantGenericPatternChild(item=item): - # The subject constrains only one end of the possible pattern-class specializations. reveal_type(item) # revealed: Unknown def test_match_inherited_generic_subclass_capture( @@ -1338,7 +1547,8 @@ Two unrelated non-final classes can have a common subclass through multiple inhe successful pattern therefore preserves both class types. Attributes defined on both classes use the intersection of their declared types, consistent with ordinary attribute access on an intersection. For a generic pattern class whose type arguments are not known from the subject, its attributes use -`Unknown`. +`Unknown`. Iterating over a generic attribute likewise produces an unknown element type in gradual +mode. ```py from typing import Generic, TypeVar @@ -1434,7 +1644,7 @@ def test_match_generic_container_member_keeps_loop_reachable( match value: case GenericListOverlapB(values=items): for item in items: - reveal_type(item) # revealed: object + reveal_type(item) # revealed: Unknown ``` ## Class pattern captures from `Any` and `Unknown` diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 5c1eac5632..8f6feb0077 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -2644,8 +2644,8 @@ Item = A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S def _(item: Item) -> None: reveal_type(dict(item)) # revealed: dict[str, object] -# Runtime narrowing retains a `Top[dict[Unknown, Unknown]]` intersection around each `TypedDict`. -# Those intersections should still reuse the common protocol constraints of the union. +# Runtime narrowing preserves each `TypedDict` schema without exposing unrestricted dictionary +# operations. The union should still reuse its common protocol constraints. # Regression test for https://github.com/astral-sh/ty/issues/3974. def _(item: Item | str) -> None: if isinstance(item, dict): diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 103be21c14..68d8346dba 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -93,6 +93,9 @@ fn register_lints(registry: &mut LintRegistryBuilder) { #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] pub struct AnalysisSettings { + /// Whether narrowing with generic classes uses the top materialization. + pub strict_generic_narrowing: bool, + /// Whether ty should use conservative equality and inequality semantics. pub strict_equality_semantics: bool, @@ -113,6 +116,7 @@ pub struct AnalysisSettings { impl Default for AnalysisSettings { fn default() -> Self { Self { + strict_generic_narrowing: false, strict_equality_semantics: false, respect_type_ignore_comments: true, allowed_unresolved_imports: ModuleGlobSet::empty(), diff --git a/crates/ty_python_semantic/src/types/match_pattern.rs b/crates/ty_python_semantic/src/types/match_pattern.rs index 0ee2c3b1fd..4c8db1f59f 100644 --- a/crates/ty_python_semantic/src/types/match_pattern.rs +++ b/crates/ty_python_semantic/src/types/match_pattern.rs @@ -109,7 +109,11 @@ fn typed_dict_pattern_domain_satisfies<'db>( } /// Return whether every value in `ty` is represented by a `TypedDict` schema at runtime. -fn is_typed_dict_pattern_domain(db: &dyn Db, env: &ProgramEnvironment<'_>, ty: Type<'_>) -> bool { +pub(super) fn is_typed_dict_runtime_domain( + db: &dyn Db, + env: &ProgramEnvironment<'_>, + ty: Type<'_>, +) -> bool { typed_dict_pattern_domain_satisfies(db, env, ty, &|_| true) } @@ -273,7 +277,7 @@ fn class_pattern_is_exhaustive( kind: &ClassPatternPredicateKind<'_>, ) -> bool { let class_instance_ty = Type::instance(db, env, class.top_materialization(db)); - let is_typed_dict_match = is_typed_dict_pattern_domain(db, env, subject_ty) + let is_typed_dict_match = is_typed_dict_runtime_domain(db, env, subject_ty) && typed_dict_matches_class_pattern(db, env, class); if !is_typed_dict_match && !subject_ty.is_subtype_of(db, env, class_instance_ty) { return false; diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 9d1563f23f..cc0956b59c 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -43,6 +43,7 @@ use super::equality::{ ComparisonSoundnessPolicy, equality_exclusion_constraint, equality_truthiness, evaluate_type_equality, evaluate_type_inequality, }; +use super::match_pattern::is_typed_dict_runtime_domain; use super::variance::TypeVarVariance; use itertools::Itertools; use ruff_python_ast as ast; @@ -460,20 +461,32 @@ impl ClassInfoConstraintFunction { env: &ProgramEnvironment<'db>, classinfo: Type<'db>, is_positive: bool, + use_generic_filtering: bool, ) -> Option> { - let constraint_from_class_literal = |class: ClassLiteral<'db>| match self { - ClassInfoConstraintFunction::IsInstance => { - Type::instance(db, env, class.top_materialization(db)) - } - ClassInfoConstraintFunction::IsSubclass => { - SubclassOfType::from(db, env, class.top_materialization(db)) + let constraint_from_class_literal = |class: ClassLiteral<'db>| { + let specialization = if use_generic_filtering { + class.unknown_specialization(db) + } else { + // A negative result excludes every specialization of the class. + class.top_materialization(db) + }; + + match self { + ClassInfoConstraintFunction::IsInstance => Type::instance(db, env, specialization), + ClassInfoConstraintFunction::IsSubclass => { + SubclassOfType::from(db, env, specialization) + } } }; match classinfo { - Type::TypeAlias(alias) => { - self.generate_constraint(db, env, alias.value_type(db), is_positive) - } + Type::TypeAlias(alias) => self.generate_constraint( + db, + env, + alias.value_type(db), + is_positive, + use_generic_filtering, + ), Type::ClassLiteral(class_literal) => Some(constraint_from_class_literal(class_literal)), Type::SubclassOf(subclass_of_ty) => { // We can't narrow negatively from a `SubclassOf` type. `if !isinstance(x, y)` @@ -521,7 +534,13 @@ impl ClassInfoConstraintFunction { // target) should be SKIPPED, not abort narrowing on the // whole intersection. Narrowing on the remaining members // is still sound. - if let Some(c) = self.generate_constraint(db, env, *element, is_positive) { + if let Some(c) = self.generate_constraint( + db, + env, + *element, + is_positive, + use_generic_filtering, + ) { builder.add_positive_in_place(c); any_member = true; } @@ -537,16 +556,21 @@ impl ClassInfoConstraintFunction { } } Type::Union(union) => union.try_map(db, env, |element| { - self.generate_constraint(db, env, *element, is_positive) + self.generate_constraint(db, env, *element, is_positive, use_generic_filtering) }), Type::TypeVar(bound_typevar) => { match bound_typevar.typevar(db).bound_or_constraints(db, env)? { TypeVarBoundOrConstraints::UpperBound(bound) => { - self.generate_constraint(db, env, bound, is_positive) - } - TypeVarBoundOrConstraints::Constraints(constraints) => { - self.generate_constraint(db, env, constraints.as_type(db, env), is_positive) + self.generate_constraint(db, env, bound, is_positive, use_generic_filtering) } + TypeVarBoundOrConstraints::Constraints(constraints) => self + .generate_constraint( + db, + env, + constraints.as_type(db, env), + is_positive, + use_generic_filtering, + ), } } @@ -558,9 +582,15 @@ impl ClassInfoConstraintFunction { UnionType::try_from_elements( db, env, - tuple - .iter_element_types(db) - .map(|element| self.generate_constraint(db, env, element, is_positive)), + tuple.iter_element_types(db).map(|element| { + self.generate_constraint( + db, + env, + element, + is_positive, + use_generic_filtering, + ) + }), ) } @@ -582,9 +612,16 @@ impl ClassInfoConstraintFunction { env, KnownClass::NoneType.to_class_literal(db, env), is_positive, + use_generic_filtering, ) } else { - self.generate_constraint(db, env, element, is_positive) + self.generate_constraint( + db, + env, + element, + is_positive, + use_generic_filtering, + ) } }), ) @@ -596,25 +633,31 @@ impl ClassInfoConstraintFunction { env, alias.aliased_class().to_class_literal(db, env), is_positive, + use_generic_filtering, ), SpecialFormType::Tuple => self.generate_constraint( db, env, KnownClass::Tuple.to_class_literal(db, env), is_positive, + use_generic_filtering, ), SpecialFormType::Type => self.generate_constraint( db, env, KnownClass::Type.to_class_literal(db, env), is_positive, + use_generic_filtering, ), - // We don't have a good meta-type for `Callable`s right now, // so only apply `isinstance()` narrowing, not `issubclass()` SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable => { (self == ClassInfoConstraintFunction::IsInstance).then(|| { - Type::Callable(CallableType::unknown(db)).top_materialization(db, env) + if use_generic_filtering { + Type::Callable(CallableType::unknown(db)) + } else { + callable_pattern_type(db, env) + } }) } @@ -653,20 +696,50 @@ impl ClassInfoConstraintFunction { } } +#[derive(Hash, PartialEq, Debug, Eq, Clone, Copy, get_size2::GetSize, salsa::SalsaValue)] +enum NarrowingOperation<'db> { + /// Narrow the subject by intersecting it directly with this type. + Intersection(Type<'db>), + /// Narrow to this generic type while preserving type arguments already known about the subject. + GenericFiltering(Type<'db>), +} + +impl<'db> NarrowingOperation<'db> { + const fn ty(self) -> Type<'db> { + match self { + Self::Intersection(ty) | Self::GenericFiltering(ty) => ty, + } + } +} + #[derive(Hash, PartialEq, Debug, Eq, Clone, get_size2::GetSize, salsa::SalsaValue)] struct Conjunctions<'db> { - conjuncts: SmallVec<[Type<'db>; 2]>, + conjuncts: SmallVec<[NarrowingOperation<'db>; 2]>, } impl<'db> Conjunctions<'db> { fn singleton(ty: Type<'db>) -> Self { Self { - conjuncts: smallvec![ty], + conjuncts: smallvec![NarrowingOperation::Intersection(ty)], + } + } + + fn generic_filtering(ty: Type<'db>) -> Self { + Self { + conjuncts: smallvec![NarrowingOperation::GenericFiltering(ty)], } } fn and_with(mut self, other: Self) -> Self { - if self.conjuncts.iter().any(Type::is_never) || other.conjuncts.iter().any(Type::is_never) { + if self + .conjuncts + .iter() + .any(|conjunct| conjunct.ty().is_never()) + || other + .conjuncts + .iter() + .any(|conjunct| conjunct.ty().is_never()) + { return Self::singleton(Type::Never); } @@ -680,18 +753,291 @@ impl<'db> Conjunctions<'db> { fn evaluate_constraint_type(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> { if self.conjuncts.len() == 1 { - return self.conjuncts[0]; + return self.conjuncts[0].ty(); } // Collapse shared union arms before distributing the next constraint over them. self.conjuncts .into_iter() - .fold(Type::object(), |accumulated, conjunct| { - IntersectionType::from_two_elements(db, env, accumulated, conjunct) + .fold(Type::object(), |accumulated, conjunct| match conjunct { + NarrowingOperation::Intersection(ty) => { + IntersectionType::from_two_elements(db, env, accumulated, ty) + } + NarrowingOperation::GenericFiltering(ty) => { + filter_generic_narrowing_constraint(db, env, accumulated, ty) + } }) } } +/// Preserve known generic arguments when narrowing a specialized base to one of its subclasses. +/// +/// For example, filtering `Sequence[int]` with `list[Unknown]` first infers `list[int]` from +/// the target class's specialized `Sequence` base. Unrelated union arms and intersection elements +/// are still intersected with the original unknown-specialized target. +fn filter_generic_narrowing_constraint<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + subject: Type<'db>, + target: Type<'db>, +) -> Type<'db> { + match (subject, target) { + (Type::Union(union), target) => union.map(db, env, |element| { + filter_generic_narrowing_constraint(db, env, *element, target) + }), + (subject, Type::Union(union)) => union.map(db, env, |element| { + filter_generic_narrowing_constraint(db, env, subject, *element) + }), + (subject @ Type::Callable(_), Type::Callable(_)) => subject, + (subject, target) + if is_typed_dict_runtime_domain(db, env, subject) + && target.nominal_class(db, env).is_some_and(|class| { + !class.is_protocol(db) + && typed_dict_matches_class_pattern(db, env, class.class_literal(db)) + }) => + { + // A TypedDict is a dictionary at runtime, but intersecting it with the target would + // expose dict's unrestricted mutations and discard its required-key guarantees. + subject + } + (Type::Intersection(intersection), target) => { + let specialized_target = + specialize_narrowing_target_from_intersection(db, env, intersection, target) + .or_else(|| { + intersection.positive(db).iter().find_map(|element| { + specialize_narrowing_target(db, env, *element, target) + }) + }) + .unwrap_or(target); + IntersectionType::from_two_elements(db, env, subject, specialized_target) + } + (subject, target) => { + let specialized_target = + specialize_narrowing_target(db, env, subject, target).unwrap_or(target); + IntersectionType::from_two_elements(db, env, subject, specialized_target) + } + } +} + +/// Combine the constraints contributed by multiple specialized bases in an intersection. +/// +/// For example, if `Both[L, R]` inherits from `Left[L]` and `Right[R]`, narrowing +/// `Left[int] & Right[str]` to `Both` must infer `Both[int, str]`. +fn specialize_narrowing_target_from_intersection<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + intersection: IntersectionType<'db>, + target: Type<'db>, +) -> Option> { + let target_class = target.nominal_class(db, env)?.class_literal(db); + let generic_context = target_class.generic_context(db)?; + let target_identity = target_class.identity_specialization(db); + + let compatible_bases: SmallVec<[(ClassType<'db>, ClassType<'db>); 2]> = intersection + .positive(db) + .iter() + .filter_map(|element| { + let subject_class = element.nominal_class(db, env)?; + subject_class.static_class_literal(db)?.1?; + let target_base = target_identity + .iter_mro(db) + .filter_map(ClassBase::into_class) + .find(|base| base.class_literal(db) == subject_class.class_literal(db))?; + Some((target_base, subject_class)) + }) + .collect(); + + if compatible_bases.len() < 2 { + return None; + } + + let constraints = ConstraintSetBuilder::new(); + let mut base_constraints = compatible_bases + .into_iter() + .map(|(target_base, subject_class)| { + Type::instance(db, env, target_base).when_constraint_set_assignable_to( + db, + env, + Type::instance(db, env, subject_class), + &constraints, + ) + }); + let mut combined_constraints = base_constraints.next()?; + for base_constraint in base_constraints { + combined_constraints.intersect(db, &constraints, base_constraint); + } + + let solutions = combined_constraints.solutions( + db, + env, + &constraints, + generic_context.inferable_typevars(db), + ); + let specialized_class = + specialize_generic_class_from_solutions(db, env, target_class, solutions)?; + Some(Type::instance(db, env, specialized_class)) +} + +fn specialize_narrowing_target<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + subject: Type<'db>, + target: Type<'db>, +) -> Option> { + if let Type::TypeVar(typevar) = subject { + let bound = match typevar.typevar(db).bound_or_constraints(db, env)? { + TypeVarBoundOrConstraints::UpperBound(bound) => bound, + TypeVarBoundOrConstraints::Constraints(constraints) => constraints.as_type(db, env), + }; + + return match bound { + Type::Union(union) => { + let mut candidates = UnionBuilder::new(db, env); + for element in union.elements(db) { + if let Some(specialized) = + specialize_narrowing_target(db, env, *element, target) + { + candidates.add_in_place(specialized); + } + } + (!candidates.is_empty()).then(|| candidates.build()) + } + Type::Intersection(intersection) => intersection + .positive(db) + .iter() + .find_map(|element| specialize_narrowing_target(db, env, *element, target)), + bound if bound != subject => specialize_narrowing_target(db, env, bound, target), + _ => None, + }; + } + + let (target_class, subject_class, is_subclass) = match target { + Type::SubclassOf(target) => { + let SubclassOfInner::Class(target_class) = target.subclass_of() else { + return None; + }; + let Type::SubclassOf(subject) = subject else { + return None; + }; + let SubclassOfInner::Class(subject_class) = subject.subclass_of() else { + return None; + }; + (target_class, subject_class, true) + } + _ => ( + target.nominal_class(db, env)?, + subject.nominal_class(db, env)?, + false, + ), + }; + + // An unspecialized class cannot contribute type arguments to the narrowing target. + subject_class.static_class_literal(db)?.1?; + + let target_class = + if subject_class.is_subtype_of_class_literal(db, target_class.class_literal(db)) { + subject_class + } else { + specialize_generic_class_for_subject( + db, + env, + target_class.class_literal(db), + subject_class, + )? + }; + + Some(if is_subclass { + SubclassOfType::from(db, env, target_class) + } else { + Type::instance(db, env, target_class) + }) +} + +/// Infer a generic subclass specialization from a specialized base class. +/// +/// For example, if `target_class` is `list` and `subject_class` is `Sequence[int]`, +/// this returns the specialized class `list[int]`. +fn specialize_generic_class_for_subject<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target_class: ClassLiteral<'db>, + subject_class: ClassType<'db>, +) -> Option> { + let generic_context = target_class.generic_context(db)?; + let target_identity = target_class.identity_specialization(db); + let target_base = target_identity + .iter_mro(db) + .filter_map(ClassBase::into_class) + .find(|base| base.class_literal(db) == subject_class.class_literal(db)); + + let (source, target) = if let Some(target_base) = target_base { + (target_base, subject_class) + } else if target_class.is_protocol(db) { + (subject_class, target_identity) + } else if subject_class.is_protocol(db) { + (target_identity, subject_class) + } else { + return None; + }; + + let constraints = ConstraintSetBuilder::new(); + let solutions = Type::instance(db, env, source) + .assignable_solutions_with_inferable( + db, + env, + Type::instance(db, env, target), + generic_context.inferable_typevars(db), + ) + .solve(db, env, &constraints); + + specialize_generic_class_from_solutions(db, env, target_class, solutions) +} + +fn specialize_generic_class_from_solutions<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target_class: ClassLiteral<'db>, + solutions: Solutions<'db>, +) -> Option> { + let generic_context = target_class.generic_context(db)?; + let Solutions::Constrained(solutions) = solutions else { + return None; + }; + let [solution] = solutions.as_slice() else { + return None; + }; + + let typevars = generic_context.variables(db); + let unknown_specialization = generic_context.unknown_specialization(db, target_class.known(db)); + let types = typevars + .clone() + .map(|typevar| { + solution + .iter() + .find(|binding| binding.bound_typevar == typevar) + .map(|binding| binding.solution) + .or_else(|| unknown_specialization.get(db, typevar)) + }) + .collect::>>()?; + if types.iter().any(|ty| { + typevars + .clone() + .any(|typevar| ty.references_typevar(db, env, typevar.typevar(db).identity(db))) + }) { + return None; + } + + let specialization = if target_class.is_known(db, KnownClass::Tuple) + && let [element] = types.as_slice() + { + generic_context.specialize_tuple(db, *element, TupleType::homogeneous(db, env, *element)) + } else { + generic_context.specialize(db, types) + }; + + Some(target_class.apply_specialization(db, |_| specialization)) +} + /// Represents narrowing constraints in Disjunctive Normal Form (DNF). /// /// This is a disjunction (OR) of conjunctions (AND) of constraints. @@ -737,6 +1083,15 @@ impl<'db> NarrowingConstraint<'db> { } } + /// Create an intersection constraint that preserves generic arguments already known about + /// the subject when narrowing it to a subclass. + fn generic_filtering(constraint: Type<'db>) -> Self { + Self { + intersection_disjuncts: smallvec_inline![Conjunctions::generic_filtering(constraint)], + replacement_disjuncts: smallvec![], + } + } + /// Create a "replacement" constraint: the previous type will be /// replaced wholesale with this constraint fn replacement(constraint: Type<'db>) -> Self { @@ -972,10 +1327,15 @@ fn positive_class_pattern_type<'db>( db: &'db dyn Db, env: &ProgramEnvironment<'db>, class_expression_ty: Type<'db>, + use_generic_filtering: bool, ) -> Option> { match class_expression_ty { Type::SpecialForm(SpecialFormType::CollectionsAbcCallable) => { - Some(callable_pattern_type(db, env)) + Some(if use_generic_filtering { + Type::Callable(CallableType::unknown(db)) + } else { + callable_pattern_type(db, env) + }) } _ if class_expression_ty.is_assignable_to( db, @@ -988,6 +1348,7 @@ fn positive_class_pattern_type<'db>( env, class_expression_ty, true, + use_generic_filtering, ) } _ => None, @@ -1057,6 +1418,7 @@ fn necessary_match_pattern_type<'db>( db, env, infer_same_file_expression_type(db, kind.class, TypeContext::default()), + false, ) .unwrap_or_else(Type::object), PatternPredicateKind::Mapping(_) => mapping_pattern_type(db, env), @@ -1469,6 +1831,12 @@ impl<'db> PatternSuccessAnalyzer<'db> { ComparisonSoundnessPolicy::from_analysis_settings(db.analysis_settings(self.scope.file(db))) } + fn use_generic_filtering(&self) -> bool { + let db = self.db; + !db.analysis_settings(self.scope.file(db)) + .strict_generic_narrowing + } + fn merge_binding( bindings: &mut BTreeMap>, place: ScopedPlaceId, @@ -1764,6 +2132,13 @@ impl<'db> PatternSuccessAnalyzer<'db> { subject_ty: Type<'db>, ) -> Type<'db> { let db = self.db; + let intersect = |subject_ty| { + if self.use_generic_filtering() { + filter_generic_narrowing_constraint(db, &self.env, subject_ty, class_ty) + } else { + self.intersect_types(subject_ty, class_ty) + } + }; match subject_ty { Type::TypeAlias(alias) => { self.filter_class_pattern_subject_type(class, class_ty, alias.value_type(db)) @@ -1772,7 +2147,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { self.filter_class_pattern_subject_type(class, class_ty, *element) }), Type::Intersection(intersection) if intersection.positive(db).is_empty() => { - self.intersect_types(subject_ty, class_ty) + intersect(subject_ty) } Type::Intersection(intersection) => { intersection.map_positive(db, &self.env, |positive| { @@ -1781,7 +2156,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { } Type::NominalInstance(instance) => { let Some(class) = class else { - return self.intersect_types(subject_ty, class_ty); + return intersect(subject_ty); }; let subject_class = instance.class(db, &self.env); if subject_class.is_subtype_of_class_literal(db, class) { @@ -1789,7 +2164,7 @@ impl<'db> PatternSuccessAnalyzer<'db> { } else if subject_ty.is_disjoint_from(db, &self.env, class_ty) { Type::Never } else { - self.intersect_types(subject_ty, class_ty) + intersect(subject_ty) } } Type::TypedDict(_) @@ -1799,9 +2174,10 @@ impl<'db> PatternSuccessAnalyzer<'db> { { subject_ty } + Type::Callable(_) if matches!(class_ty, Type::Callable(_)) => subject_ty, _ if subject_ty.is_subtype_of(db, &self.env, class_ty) => subject_ty, _ if subject_ty.is_disjoint_from(db, &self.env, class_ty) => Type::Never, - _ => self.intersect_types(subject_ty, class_ty), + _ => intersect(subject_ty), } } @@ -1817,17 +2193,38 @@ impl<'db> PatternSuccessAnalyzer<'db> { let subject_is_final = subject_ty .nominal_class(db, &self.env) .is_some_and(|class| class.is_final(db)); - let specialized_pattern_class = - if context.positional_sources.is_empty() && kind.keywords.is_empty() { - None - } else { - context - .class - .zip(filtering_subject_ty.nominal_class(db, &self.env)) - .and_then(|(pattern_class, subject_class)| { - self.specialize_pattern_class_for_subject(pattern_class, subject_class) - }) - }; + let specialized_pattern_class = if context.positional_sources.is_empty() + && kind.keywords.is_empty() + { + None + } else if self.use_generic_filtering() { + context + .class + .filter(|pattern_class| pattern_class.generic_context(db).is_some()) + .and_then(|pattern_class| { + subject_ty + .nominal_class(db, &self.env) + .filter(|subject_class| subject_class.class_literal(db) == pattern_class) + .or_else(|| { + if let Type::Intersection(intersection) = subject_ty { + intersection.positive(db).iter().find_map(|element| { + element + .nominal_class(db, &self.env) + .filter(|class| class.class_literal(db) == pattern_class) + }) + } else { + None + } + }) + }) + } else { + context + .class + .zip(filtering_subject_ty.nominal_class(db, &self.env)) + .and_then(|(pattern_class, subject_class)| { + self.specialize_pattern_class_for_subject(pattern_class, subject_class) + }) + }; let member_type = |name: &Name| { let original_member_ty = original_subject_ty .member(db, &self.env, name.as_str()) @@ -1836,11 +2233,15 @@ impl<'db> PatternSuccessAnalyzer<'db> { let place = subject_ty.member(db, &self.env, name.as_str()).place; let mut member_ty = place.ignore_possibly_undefined(); - if let Some(specialized_pattern_class) = specialized_pattern_class { - member_ty = Type::instance(db, &self.env, specialized_pattern_class) - .member(db, &self.env, name.as_str()) - .place - .ignore_possibly_undefined(); + if let Some(specialized_pattern_class) = specialized_pattern_class + && let Some(specialized_member_ty) = + Type::instance(db, &self.env, specialized_pattern_class) + .member(db, &self.env, name.as_str()) + .place + .ignore_possibly_undefined() + && !specialized_member_ty.is_unknown() + { + member_ty = Some(specialized_member_ty); } else if let Some(pattern_class) = context.class && pattern_class .generic_context(db) @@ -1935,7 +2336,8 @@ impl<'db> PatternSuccessAnalyzer<'db> { /// the existing conservative member type. /// /// ```python - /// class Base[T]: ... + /// class Base[T]: + /// value: T /// /// class Child[T](Base[T]): /// item: T @@ -2011,12 +2413,18 @@ impl<'db> PatternSuccessAnalyzer<'db> { let db = self.db; let class_expr_ty = infer_same_file_expression_type(db, kind.class, TypeContext::default()) .resolve_type_alias(db); + let use_generic_filtering = self.use_generic_filtering(); let context = |class_expr_ty: Type<'db>| { let class = class_expr_ty.as_class_literal(); ClassPatternContext { class, - class_ty: positive_class_pattern_type(db, &self.env, class_expr_ty) - .unwrap_or_else(Type::object), + class_ty: positive_class_pattern_type( + db, + &self.env, + class_expr_ty, + use_generic_filtering, + ) + .unwrap_or_else(Type::object), positional_sources: class.map_or_else( || vec![ClassPatternPositionalSource::Unknown; kind.positional.len()], |class| { @@ -3886,16 +4294,31 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { let class_info_ty = inference.expression_type(second_arg); + let use_generic_filtering = is_positive + && !self + .db + .analysis_settings(self.scope().file(self.db)) + .strict_generic_narrowing; function - .generate_constraint(db, &self.env, class_info_ty, is_positive) + .generate_constraint( + db, + &self.env, + class_info_ty, + is_positive, + use_generic_filtering, + ) .map(|constraint| { NarrowingConstraints::from_iter([( place, - NarrowingConstraint::intersection(constraint.negate_if( - db, - &self.env, - !is_positive, - )), + if use_generic_filtering { + NarrowingConstraint::generic_filtering(constraint) + } else { + NarrowingConstraint::intersection(constraint.negate_if( + db, + &self.env, + !is_positive, + )) + }, )]) }) } diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index 45c48b2f97..090040611c 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -164,6 +164,7 @@ Settings: Settings { }, }, analysis: AnalysisSettings { + strict_generic_narrowing: false, strict_equality_semantics: false, respect_type_ignore_comments: true, allowed_unresolved_imports: ModuleGlobSet { diff --git a/crates/ty_test/src/config.rs b/crates/ty_test/src/config.rs index 28fbed4297..907dec5aac 100644 --- a/crates/ty_test/src/config.rs +++ b/crates/ty_test/src/config.rs @@ -148,6 +148,9 @@ pub(crate) struct Environment { #[derive(Deserialize, Default, Debug, Clone)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub(crate) struct Analysis { + /// Whether narrowing with generic classes uses the top materialization. + pub(crate) strict_generic_narrowing: Option, + /// Whether equality-based checks should preserve possible subclass behavior. #[serde(alias = "strict-literal-narrowing")] pub(crate) strict_equality_semantics: Option, diff --git a/crates/ty_test/src/db.rs b/crates/ty_test/src/db.rs index f2700d6fe0..e2f7d74788 100644 --- a/crates/ty_test/src/db.rs +++ b/crates/ty_test/src/db.rs @@ -272,6 +272,7 @@ fn mdtest_analysis_settings(options: Option<&Analysis>) -> AnalysisSettings { }; let AnalysisSettings { + strict_generic_narrowing: strict_generic_narrowing_default, strict_equality_semantics: strict_equality_semantics_default, respect_type_ignore_comments: respect_type_ignore_comments_default, allowed_unresolved_imports: allowed_unresolved_imports_default, @@ -305,6 +306,9 @@ fn mdtest_analysis_settings(options: Option<&Analysis>) -> AnalysisSettings { }; AnalysisSettings { + strict_generic_narrowing: options + .strict_generic_narrowing + .unwrap_or(strict_generic_narrowing_default), strict_equality_semantics: options .strict_equality_semantics .unwrap_or(strict_equality_semantics_default), diff --git a/ty.schema.json b/ty.schema.json index aafd177dd8..14d8b3f14e 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -105,6 +105,13 @@ "boolean", "null" ] + }, + "strict-generic-narrowing": { + "description": "Whether ty should use strict narrowing for unspecialized generic classes in\n`isinstance()` and `issubclass()` checks, as well as `match` class patterns.\n\nWhen enabled, ty narrows to the top materialization of the class. For example,\n`isinstance(value, list)` narrows a value of type `object` to `Top[list[Unknown]]`,\nrepresenting the (infinite) union of all possible `list` specializations. Iterating\nover the list would yield values of type `object`.\n\nWhen disabled, ty uses gradual generic narrowing, preserving compatible type\narguments from the original type where possible. For example,\n`isinstance(value, list)` narrows a value of type `Sequence[int]` to `list[int]`.\nIf no specialization is available, the same check narrows a value of type `object`\nto `list[Unknown]`; items of any type can then be appended to the list. Class\npatterns such as `case list():` follow the same behavior.\n\nDefaults to `false`.", + "type": [ + "boolean", + "null" + ] } }, "additionalProperties": false From c06c003eab2d8bab41620d46e34fa920ece3ac01 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 6 Aug 2026 12:41:04 +0200 Subject: [PATCH 310/390] [ty] Temporarily remove myself from the reviewer pool (#27541) ## Summary Temporarily remove myself from the ty semantic reviewer pool while out of office. --- .github/pr-reviewer-pools.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pr-reviewer-pools.toml b/.github/pr-reviewer-pools.toml index 549f48855d..490b917c8b 100644 --- a/.github/pr-reviewer-pools.toml +++ b/.github/pr-reviewer-pools.toml @@ -9,7 +9,7 @@ reviewers = ["ntBre"] [[pools]] name = "ty-semantic" paths = ["/crates/ty_python_core/**", "/crates/ty_python_semantic/**"] -reviewers = ["carljm", "charliermarsh", "sharkdp", "dcreager", "dhruvmanila", "ibraheemdev"] +reviewers = ["carljm", "charliermarsh", "dcreager", "dhruvmanila", "ibraheemdev"] [[pools]] name = "ty-module-resolver" From 64021cfab5c053d9ae0e221705f89a64fbbf202c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 6 Aug 2026 07:11:59 -0400 Subject: [PATCH 311/390] [ty] Require ClassVar declarations for protocol members (#27530) ## Summary Previously, an instance variable could satisfy a protocol member declared as `ClassVar` if its class provided a readable attribute: ```python from typing import ClassVar, Protocol class HasValue(Protocol): value: ClassVar[int] class InstanceValue: value: int = 1 class Base: value: ClassVar[int] = 1 class Child(Base): value = 2 invalid: HasValue = InstanceValue() # error: [invalid-assignment] valid: HasValue = Child() ``` We now reuse existing variable-kind inference to distinguish class and instance declarations during structural protocol matching. --- .../resources/mdtest/protocols.md | 84 ++++++++++++++++--- .../ty_python_semantic/src/types/overrides.rs | 6 +- .../src/types/protocol_class.rs | 83 ++++++++++++++++++ .../ty_python_semantic/src/types/relation.rs | 8 ++ .../src/types/relation_error.rs | 8 ++ 5 files changed, 174 insertions(+), 15 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index ff0b8d7f9e..15aabf3117 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -776,12 +776,12 @@ static_assert(is_assignable_to(Qux, HasXWithDefault)) class HasClassVarX(Protocol): x: ClassVar[int] -static_assert(is_subtype_of(FooWithZero, HasClassVarX)) -static_assert(is_assignable_to(FooWithZero, HasClassVarX)) +static_assert(not is_subtype_of(FooWithZero, HasClassVarX)) +static_assert(not is_assignable_to(FooWithZero, HasClassVarX)) -# TODO: these should pass -static_assert(not is_subtype_of(Foo, HasClassVarX)) # error: [static-assert-error] -static_assert(not is_assignable_to(Foo, HasClassVarX)) # error: [static-assert-error] +# An instance declaration does not become a class variable without an explicit qualifier. +static_assert(not is_subtype_of(Foo, HasClassVarX)) +static_assert(not is_assignable_to(Foo, HasClassVarX)) static_assert(not is_subtype_of(Qux, HasClassVarX)) static_assert(not is_assignable_to(Qux, HasClassVarX)) @@ -2050,14 +2050,16 @@ static_assert(is_assignable_to(UsesMeta, HasX)) If a protocol `ClassVarX` has a `ClassVar` attribute member `x` with type `int`, this indicates that the non-callable attribute must be readable with the same type through both an inhabitant of -`ClassVarX` and the type of that inhabitant: +`ClassVarX` and the type of that inhabitant. An implementing class must declare the member as a +`ClassVar`; an instance attribute does not satisfy the requirement merely because it has a default +value in the class body: `classvars.py`: ```py -from typing import Any, ClassVar, Protocol -from ty_extensions import static_assert -from ty_extensions._internal import is_subtype_of, is_assignable_to +from typing import Any, ClassVar, Protocol, final +from ty_extensions import Intersection, static_assert +from ty_extensions._internal import TypeOf, is_assignable_to, is_disjoint_from, is_subtype_of class ClassVarXProto(Protocol): x: ClassVar[int] @@ -2070,9 +2072,14 @@ def f(obj: ClassVarXProto): class InstanceAttrX: x: int -# 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] +static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto)) +static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto)) + +class InstanceAttrXWithDefault: + x: int = 42 + +static_assert(not is_assignable_to(InstanceAttrXWithDefault, ClassVarXProto)) +static_assert(not is_subtype_of(InstanceAttrXWithDefault, ClassVarXProto)) class PropertyX: @property @@ -2088,6 +2095,14 @@ class ClassVarX: static_assert(is_assignable_to(ClassVarX, ClassVarXProto)) static_assert(is_subtype_of(ClassVarX, ClassVarXProto)) +class InheritedClassVarX(ClassVarX): + x = 1 + +static_assert(is_assignable_to(InheritedClassVarX, ClassVarXProto)) +static_assert(is_subtype_of(InheritedClassVarX, ClassVarXProto)) +static_assert(is_assignable_to(TypeOf[InheritedClassVarX], type[ClassVarXProto])) +static_assert(is_subtype_of(TypeOf[InheritedClassVarX], type[ClassVarXProto])) + class XMeta(type): def x(cls) -> str: return "" @@ -2117,6 +2132,51 @@ class NotHashable: static_assert(is_assignable_to(NotHashable, NotHashableProto)) static_assert(is_subtype_of(NotHashable, NotHashableProto)) + +class Descriptor: + def __get__(self, instance: object, owner: type) -> "Descriptor": + return self + + def __set__(self, instance: object, value: "Descriptor") -> None: ... + +class HasClassDescriptor(Protocol): + descriptor: ClassVar[Descriptor] + +class DescriptorImplementation: + descriptor: ClassVar[Descriptor] = Descriptor() + +static_assert(is_assignable_to(DescriptorImplementation, HasClassDescriptor)) +static_assert(is_subtype_of(DescriptorImplementation, HasClassDescriptor)) + +@final +class FinalInstanceAttrX: + x: int = 42 + +@final +class FinalClassVarX: + x: ClassVar[int] = 42 + +static_assert(is_disjoint_from(FinalInstanceAttrX, ClassVarXProto)) +static_assert(not is_disjoint_from(InstanceAttrXWithDefault, ClassVarXProto)) +static_assert(not is_disjoint_from(FinalClassVarX, ClassVarXProto)) + +def impossible(value: Intersection[FinalInstanceAttrX, ClassVarXProto]) -> None: + reveal_type(value) # revealed: Never + +implementation: ClassVarXProto = InstanceAttrX() # snapshot: invalid-assignment +``` + +```snapshot +error[invalid-assignment]: Object of type `InstanceAttrX` is not assignable to `ClassVarXProto` + --> src/classvars.py:107:34 + | +107 | implementation: ClassVarXProto = InstanceAttrX() # snapshot: invalid-assignment + | -------------- ^^^^^^^^^^^^^^^ Incompatible value of type `InstanceAttrX` + | | + | Declared type +info: type `InstanceAttrX` is not assignable to protocol `ClassVarXProto` +info: └── protocol member `x` is incompatible +info: └── protocol member `x` is an instance variable on type `InstanceAttrX`, but a class variable is required ``` This is mentioned by the diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index 7127364896..df3aff88a9 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -1056,7 +1056,7 @@ fn method_override_types<'db>( /// Whether an attribute declaration is a class variable or an instance variable. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, get_size2::GetSize)] -enum VariableKind { +pub(super) enum VariableKind { /// A variable annotated with `ClassVar`. Class, /// An instance variable, including an unannotated class-body assignment. @@ -1121,7 +1121,7 @@ fn superclass_variable_kind<'db>( /// ``` #[allow(clippy::needless_pass_by_value)] #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] -fn effective_superclass_variable_kind<'db>( +pub(super) fn effective_superclass_variable_kind<'db>( db: &'db dyn Db, superclass: ClassType<'db>, name: Name, @@ -1155,7 +1155,7 @@ fn effective_superclass_variable_kind<'db>( superclass_scope, superclass_symbol_id, superclass.own_class_member(db, env, None, &name).inner, - Type::instance(db, env, superclass).member(db, env, &name), + superclass.own_instance_member(db, env, &name).inner, ); if superclass_variable_kind == Some(VariableKind::Instance) diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index b27ac8c293..e1714a93f5 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -13,6 +13,7 @@ use crate::types::attribute_write::{ ProtocolMemberWriteRequirement, attribute_write_requirement, }; use crate::types::call::{CallArguments, CallDunderError}; +use crate::types::overrides::{VariableKind, effective_superclass_variable_kind}; use crate::types::relation::{DisjointnessChecker, TypeRelationChecker}; use crate::types::visitor::any_over_type; use crate::types::{TypeContext, UpcastPolicy}; @@ -1860,6 +1861,65 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { self.data.qualifiers } + /// Returns whether an instance declaration conflicts with a required writable class variable. + /// + /// An unannotated assignment preserves an inherited `ClassVar`; an explicit instance + /// annotation does not: + /// + /// ```python + /// from typing import ClassVar + /// + /// class Base: + /// value: ClassVar[int] + /// + /// class Valid(Base): + /// value = 1 + /// + /// class Invalid(Base): + /// value: int = 1 + /// ``` + /// + /// Inspect declarations before descriptor binding, and ignore synthesized members without + /// source provenance. + pub(super) fn has_incompatible_class_variable_declaration( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + ) -> bool { + let qualifiers = self.qualifiers(); + qualifiers.contains(TypeQualifiers::CLASS_VAR) + && !qualifiers.contains(TypeQualifiers::FINAL) + && ty + .nominal_class(db, env) + .or_else(|| { + if !is_class_object_type(ty) { + return None; + } + + ty.to_meta_type(db, env) + .to_instance_approximation(db, env)? + .nominal_class(db, env) + }) + .is_some_and(|class| { + effective_superclass_variable_kind(db, class, Name::new(self.name)) + == Some(VariableKind::Instance) + && [ + class + .class_member(db, env, self.name, MemberLookupPolicy::default()) + .place, + class.instance_member(db, env, self.name).place, + ] + .into_iter() + .any(|place| { + matches!( + place, + Place::Defined(defined) if defined.provenance != Provenance::Unknown + ) + }) + }) + } + fn is_method(&self) -> bool { matches!(self.data.kind, ProtocolMemberKind::Method(..)) } @@ -2923,6 +2983,18 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { required: ProtocolMemberAccess<'db>, access: ProtocolMemberAccessMode, ) -> ConstraintSet<'db, 'c> { + if access == ProtocolMemberAccessMode::Class + && member.has_incompatible_class_variable_declaration(db, self.env, ty) + { + if let Some(context) = self.report_context() { + context.push(ErrorContext::ProtocolMemberClassVarMismatch { + member_name: member.name.into(), + ty, + }); + } + return self.never(); + } + if access == ProtocolMemberAccessMode::Class && member.is_instance_method() && required.read.is_some() @@ -2996,6 +3068,17 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { let instance_access = member.implementation_access(db, env, ty, ProtocolMemberAccessMode::Instance); if let Some(context) = self.report_context() { + if member.has_incompatible_class_variable_declaration(db, env, ty) { + context.push(ErrorContext::ProtocolMemberClassVarMismatch { + member_name: member.name.into(), + ty, + }); + context.push(ErrorContext::ProtocolMemberIncompatible { + member_name: member.name.into(), + }); + return self.never(); + } + let instance_read_missing = instance_access.read.is_some() && protocol_member_read_type( db, diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index f5ea3d8990..a7c7b79a7c 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -2806,6 +2806,14 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { db, &member, other, ) }) + .or(db, self.constraints, || { + ConstraintSet::from_bool( + self.constraints, + member.has_incompatible_class_variable_declaration( + db, env, other, + ), + ) + }) }) }) } diff --git a/crates/ty_python_semantic/src/types/relation_error.rs b/crates/ty_python_semantic/src/types/relation_error.rs index 1269b78cfb..9f7d0cc641 100644 --- a/crates/ty_python_semantic/src/types/relation_error.rs +++ b/crates/ty_python_semantic/src/types/relation_error.rs @@ -152,6 +152,10 @@ pub(crate) enum ErrorContext<'db> { member_name: Name, ty: Type<'db>, }, + ProtocolMemberClassVarMismatch { + member_name: Name, + ty: Type<'db>, + }, ProtocolSpecialMethodNotDefinedOnMetaType, ProtocolMemberIncompatible { member_name: Name, @@ -428,6 +432,10 @@ impl<'db> ErrorContext<'db> { "protocol member `{member_name}` is not defined on type `{}`", ty.display(db, env), ), + Self::ProtocolMemberClassVarMismatch { member_name, ty } => format!( + "protocol member `{member_name}` is an instance variable on type `{}`, but a class variable is required", + ty.display(db, env), + ), Self::ProtocolSpecialMethodNotDefinedOnMetaType => { "special methods must be defined on the meta-type when matching a protocol" .to_string() From e7a2d2c31f60a3ce2a1967ea614e042e30aae9d8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 6 Aug 2026 07:28:35 -0400 Subject: [PATCH 312/390] [ty] Infer variance through type[T] (#27534) ## Summary Previously, variance inference treated `type[T]` as independent of `T`. As a result, generic classes with writable class-object attributes could incorrectly be treated as covariant: ```python class Mutable[T]: cls: type[T] def overwrite(value: Mutable[object]) -> None: value.cls = str def unsound(value: Mutable[int]) -> None: overwrite(value) # error: [invalid-argument-type] ``` We now retain the wrapped type variable when inferring variance through `type[T]`. Return positions remain covariant, parameter positions become contravariant, and writable public attributes correctly make their containing class invariant. --- .../mdtest/generics/legacy/variance.md | 53 +++++++++++++++ .../mdtest/generics/pep695/variance.md | 65 ++++++++++++++++++- .../src/types/subclass_of.rs | 3 +- 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md index 268661e51d..006e470d60 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variance.md @@ -423,4 +423,57 @@ static_assert(not is_assignable_to(GoodInferredInvariant[B], GoodInferredInvaria static_assert(not is_assignable_to(GoodInferredInvariant[A], GoodInferredInvariant[B])) ``` +## Inferred variance for writable subclass-type attributes + +A writable public `type[T]` attribute makes a legacy type variable with inferred variance invariant. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Generic, TypeVar +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +T = TypeVar("T", infer_variance=True) + +class ClassContainer(Generic[T]): + cls: type[T] + +static_assert(not is_subtype_of(ClassContainer[int], ClassContainer[object])) +static_assert(not is_subtype_of(ClassContainer[object], ClassContainer[int])) + +static_assert(not is_assignable_to(ClassContainer[int], ClassContainer[object])) +static_assert(not is_assignable_to(ClassContainer[object], ClassContainer[int])) +``` + +## Inferred variance for subclass-type method parameters + +A method parameter annotated as `type[T]` makes a legacy type variable with inferred variance +contravariant. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Generic, TypeVar +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +T = TypeVar("T", infer_variance=True) + +class ClassContainer(Generic[T]): + def put(self, cls: type[T]) -> None: ... + +static_assert(is_subtype_of(ClassContainer[object], ClassContainer[int])) +static_assert(not is_subtype_of(ClassContainer[int], ClassContainer[object])) + +static_assert(is_assignable_to(ClassContainer[object], ClassContainer[int])) +static_assert(not is_assignable_to(ClassContainer[int], ClassContainer[object])) +``` + [spec]: https://typing.python.org/en/latest/spec/generics.html#variance diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md index 898c872655..7a0a7f8c5e 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/variance.md @@ -847,7 +847,8 @@ static_assert(not is_assignable_to(Intersection[C, Not[B]], Intersection[C, Not[ ## Subclass Types (type[T]) The `type[T]` construct represents the type of classes that are subclasses of `T`. It is covariant -in `T` because if `A <: B`, then `type[A] <: type[B]` holds. +in `T` because if `A <: B`, then `type[A] <: type[B]` holds. A public, writable `type[T]` attribute +still makes its enclosing class invariant, while a private attribute can remain covariant. ```py from ty_extensions import static_assert @@ -866,10 +867,10 @@ static_assert(not is_assignable_to(type[A], type[B])) # With generic classes using type[T] class ClassContainer[T]: def __init__(self, cls: type[T]) -> None: - self.cls = cls + self._cls = cls def create_instance(self) -> T: - return self.cls() + return self._cls() # ClassContainer is covariant in T due to type[T] static_assert(is_subtype_of(ClassContainer[B], ClassContainer[A])) @@ -887,6 +888,64 @@ b_container = ClassContainer[B](B) a_instance: A = use_a_class_container(b_container) # This should work ``` +## Subclass types in writable attributes + +A writable public `type[T]` attribute makes its enclosing class invariant in `T`. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +class ClassContainer[T]: + cls: type[T] + +static_assert(not is_subtype_of(ClassContainer[int], ClassContainer[object])) +static_assert(not is_subtype_of(ClassContainer[object], ClassContainer[int])) + +static_assert(not is_assignable_to(ClassContainer[int], ClassContainer[object])) +static_assert(not is_assignable_to(ClassContainer[object], ClassContainer[int])) +``` + +## Subclass types in return positions + +A `type[T]` return contributes covariance for `T`. Combining it with a method that accepts `T` +therefore makes the enclosing class invariant. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +class ClassContainer[T]: + def get(self) -> type[T]: + raise NotImplementedError + + def put(self, value: T) -> None: ... + +static_assert(not is_subtype_of(ClassContainer[int], ClassContainer[object])) +static_assert(not is_subtype_of(ClassContainer[object], ClassContainer[int])) + +static_assert(not is_assignable_to(ClassContainer[int], ClassContainer[object])) +static_assert(not is_assignable_to(ClassContainer[object], ClassContainer[int])) +``` + +## Subclass types in parameter positions + +A method parameter annotated as `type[T]` makes the enclosing class contravariant in `T`. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +class ClassContainer[T]: + def put(self, cls: type[T]) -> None: ... + +static_assert(is_subtype_of(ClassContainer[object], ClassContainer[int])) +static_assert(not is_subtype_of(ClassContainer[int], ClassContainer[object])) + +static_assert(is_assignable_to(ClassContainer[object], ClassContainer[int])) +static_assert(not is_assignable_to(ClassContainer[int], ClassContainer[object])) +``` + ## TypeIs ```toml diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index bb7eab33a8..76e97a7c2c 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -377,7 +377,8 @@ impl<'db> VarianceInferable<'db> for SubclassOfType<'db> { match self.subclass_of { SubclassOfInner::Class(class) => class.variance_of(db, env, typevar), SubclassOfInner::Protocol(protocol) => protocol.variance_of(db, env, typevar), - SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => TypeVarVariance::Bivariant, + SubclassOfInner::TypeVar(inner) => Type::TypeVar(inner).variance_of(db, env, typevar), + SubclassOfInner::Dynamic(_) => TypeVarVariance::Bivariant, } } } From c88946ebeb92be6d276087f0d528cd6471df4ead Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 6 Aug 2026 14:04:07 +0200 Subject: [PATCH 313/390] [ty] Bump ecosystem-analyzer for strict project settings (#27542) ## Summary Pick up astral-sh/ecosystem-analyzer#148, which enables strict equality and strict generic narrowing for ~half of ecosystem projects. --- .github/workflows/ty-ecosystem-analyzer.yaml | 2 +- .github/workflows/ty-ecosystem-report.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index 4d625e41d2..0597a034f5 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -44,7 +44,7 @@ env: CARGO_PROFILE_PROFILING_DEBUG: line-tables-only # TODO: Update the mypy-primer revision in scripts/setup_primer_project.py # and regenerate its lockfile when updating ecosystem-analyzer. - ECOSYSTEM_ANALYZER_COMMIT: f6f1b7b8586c8a6c60dc3d37c3f6ae917a9ad9c4 + ECOSYSTEM_ANALYZER_COMMIT: 27b644f296d70fccacb7d7c23c91c5d6ccd8713d jobs: build-ty: diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 49f5557781..1b1f2d9d45 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -20,7 +20,7 @@ env: RUST_BACKTRACE: 1 # Line-tables-only debug info: faster builds, backtraces still work. CARGO_PROFILE_PROFILING_DEBUG: line-tables-only - ECOSYSTEM_ANALYZER_COMMIT: f6f1b7b8586c8a6c60dc3d37c3f6ae917a9ad9c4 + ECOSYSTEM_ANALYZER_COMMIT: 27b644f296d70fccacb7d7c23c91c5d6ccd8713d jobs: ty-ecosystem-report: From baea3d0dcec6d6f6d1659321940f3725771c5f45 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 6 Aug 2026 14:07:51 +0200 Subject: [PATCH 314/390] [ty] Expose strict analysis options in the playground (#27543) ## Summary Expose `strict-equality-semantics` and `strict-generic-narrowing` in the default playground configuration so that users (and I) can easily change them when necessary. --- playground/ty/src/Playground.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/playground/ty/src/Playground.tsx b/playground/ty/src/Playground.tsx index 76b19e4021..5bdb44077a 100644 --- a/playground/ty/src/Playground.tsx +++ b/playground/ty/src/Playground.tsx @@ -292,6 +292,10 @@ export const DEFAULT_SETTINGS = JSON.stringify( environment: { "python-version": "3.14", }, + analysis: { + "strict-equality-semantics": false, + "strict-generic-narrowing": false, + }, rules: { "experimental-syntax": "ignore", "undefined-reveal": "ignore", From 05160d507f05345a72db9c28ab4edf7c92334819 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 6 Aug 2026 08:39:18 -0400 Subject: [PATCH 315/390] [ty] Diagnose invalid descriptor `__get__` calls (#27400) ## Summary This PR reports `invalid-attribute-access` when an attribute read implicitly calls a descriptor's `__get__` method with incompatible arguments. ```python class Descriptor: def __get__(self): pass class C: value = Descriptor() C().value # error: `__get__` receives the descriptor, instance, and owner ``` Previously, descriptor lookup recovered the inferred return type from a failed `__get__` call and discarded the call error. The core change here is straightforward, but it ends up being a significant diff because we need to preserve errors that we were previously discarding during attribute lookup, and that happens deep in the stack. Conceptually, we made this change: ```rust // Before member_lookup(...) -> PlaceAndQualifiers // After type MemberLookupResult = Result; member_lookup(...) -> MemberLookupResult ``` The lookup error retains the recovered member type to avoid cascading diagnostics, and is threaded through the various lookup sites. Closes https://github.com/astral-sh/ty/issues/4129. --- .../resources/mdtest/descriptor_protocol.md | 575 ++++++++++++- .../resources/mdtest/properties.md | 8 +- .../3593_function_known_decorators_cycle.md | 2 + crates/ty_python_semantic/src/place.rs | 24 +- crates/ty_python_semantic/src/types.rs | 793 +++++++++++++----- .../src/types/bound_super.rs | 16 +- crates/ty_python_semantic/src/types/call.rs | 18 + .../ty_python_semantic/src/types/call/bind.rs | 23 +- .../src/types/class/static_literal.rs | 3 +- .../src/types/diagnostic.rs | 56 ++ .../src/types/infer/builder.rs | 12 +- .../ty_python_semantic/src/types/overrides.rs | 5 +- .../src/types/protocol_class.rs | 16 +- 13 files changed, 1289 insertions(+), 262 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index 338d4a07dc..43b2330708 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -928,7 +928,9 @@ wrapper_descriptor(f, None, type(f), "one too many") ### `__get__` is called with correct arguments -This test makes sure that we call `__get__` with the right argument types for various scenarios: +Python passes the instance and its class to a descriptor on an instance access. On a class access, +it passes `None` and the class instead. A descriptor on a metaclass receives the class and its +metaclass. ```py from __future__ import annotations @@ -955,21 +957,52 @@ class C(metaclass=Meta): reveal_type(C.class_object_access) # revealed: int reveal_type(C().instance_access) # revealed: str reveal_type(C.metaclass_access) # revealed: bytes +``` + +An invalid descriptor access is reported, but we still use the declared return type of `__get__` to +avoid cascading errors. -# TODO: These should emit a diagnostic -# -# However, we use the return-type of `__get__` as the inferred type anyway: -# the way to specify that the descriptor object itself is returned when the -# attribute is accessed on the instance or the class is by overloading `__get__`. -# -# Using the return type of `__get__` even for `__get__` calls that have invalid -# arguments passed to them avoids false positives in situations where there are -# `__get__` calls that we don't sufficiently understand. +```py +# snapshot: invalid-attribute-access reveal_type(C().class_object_access) # revealed: int + +# snapshot: invalid-attribute-access reveal_type(C.instance_access) # revealed: str ``` -### Descriptors with incorrect `__get__` signature +```snapshot +error[invalid-attribute-access]: Invalid access to descriptor attribute `class_object_access` on type `C` + --> src/mdtest_snippet.py:26:13 + | +26 | reveal_type(C().class_object_access) # revealed: int + | ^^^ Expected `None`, found `C` +info: Argument to function `TailoredForClassObjectAccess.__get__` is incorrect +info: This access implicitly calls `__get__` on a descriptor of type `TailoredForClassObjectAccess` +info: Function defined here + --> src/mdtest_snippet.py:4:9 + | +4 | def __get__(self, instance: None, owner: type[C]) -> int: + | ^^^^^^^ -------------- Parameter declared here + + +error[invalid-attribute-access]: Invalid access to descriptor attribute `instance_access` on type `` + --> src/mdtest_snippet.py:29:13 + | +29 | reveal_type(C.instance_access) # revealed: str + | ^ Expected `C`, found `None` +info: Argument to function `TailoredForInstanceAccess.__get__` is incorrect +info: This access implicitly calls `__get__` on a descriptor of type `TailoredForInstanceAccess` +info: Function defined here + --> src/mdtest_snippet.py:8:9 + | +8 | def __get__(self, instance: C, owner: type[C] | None = None) -> str: + | ^^^^^^^ ----------- Parameter declared here +``` + +### Descriptors with an incorrect `__get__` signature + +Python calls `__get__` with the descriptor, an instance or `None`, and the owner class. A method +that accepts only the descriptor cannot handle that call. ```py class Descriptor: @@ -980,29 +1013,506 @@ class Descriptor: class C: descriptor: Descriptor = Descriptor() -# TODO: This should be an error +C().descriptor # snapshot: invalid-attribute-access + +# error: [invalid-attribute-access] "Invalid access to descriptor attribute `descriptor` on type ``" reveal_type(C.descriptor) # revealed: int +``` + +```snapshot +error[invalid-attribute-access]: Invalid access to descriptor attribute `descriptor` on type `C` + --> src/mdtest_snippet.py:9:1 + | +9 | C().descriptor # snapshot: invalid-attribute-access + | ^^^ Too many positional arguments to function `Descriptor.__get__`: expected 1, got 3 +info: This access implicitly calls `__get__` on a descriptor of type `Descriptor` +info: Function signature here + --> src/mdtest_snippet.py:3:9 + | +3 | def __get__(self) -> int: + | ^^^^^^^^^^^^^^^^^^^^ +``` + +### Recursive descriptor aliases terminate + +Inspecting a recursive attribute must not recurse forever. The recursive alternative also cannot +prove that the access will invoke an invalid descriptor. + +```toml +[environment] +python-version = "3.12" +``` + +```py +type Recursive = int | Recursive + +class C: + value: Recursive = 1 + +C().value +``` + +### Property getters reject invalid receiver specializations + +A property getter checks the same specialized receiver as an ordinary method. A generic alias with +alternatives that impose different type-variable bounds can produce an invalid property access. + +```py +from collections.abc import Callable +from typing import Generic, TypeVar + +AItem = TypeVar("AItem", bound=Callable[[int], str]) +BItem = TypeVar("BItem", bound=Callable[[str], str]) + +class A(Generic[AItem]): + @property + def callback(self) -> AItem: + raise NotImplementedError + +class B(Generic[BItem]): + @property + def callback(self) -> BItem: + raise NotImplementedError + +AnyCallback = TypeVar("AnyCallback", bound=Callable[..., str]) +Command = A[AnyCallback] | B[AnyCallback] +Callback = TypeVar("Callback", bound=Callable[[int], str]) + +def access(value: Callback | Command[Callback]) -> None: + if isinstance(value, A | B): + # error: [invalid-attribute-access] + value.callback +``` + +### Property getter failures preserve their underlying error and return type + +A property inherited from an unrelated class rejects the instance passed to its getter. The +diagnostic reports the getter's actual receiver mismatch and preserves its return type. + +```py +class Owner: + @property + def value(self) -> int: + return 1 + +class Other: + value = Owner.value -# TODO: This should be an error -reveal_type(C().descriptor) # revealed: int +# error: [invalid-attribute-access] "Expected `Owner`, found `Other`" +reveal_type(Other().value) # revealed: int ``` -### "Descriptors" with non-callable `__get__` attributes +### Every descriptor alternative must accept the call -If `__get__` is not callable at all, the interpreter will still attempt to call the method at -runtime, and this will raise an exception. As such, even for `__get__ = None`, we still "attempt to -call `__get__`" on the descriptor object (leading us to infer `Unknown`): +As with other operations on a union, an attribute access is invalid if any possible descriptor +cannot accept the implicit call. ```py class BrokenDescriptor: + def __get__(self) -> bytes: + return b"" + +class ValidDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> str: + return "" + +def descriptor() -> BrokenDescriptor | ValidDescriptor: + raise NotImplementedError + +class C: + value = descriptor() + +# error: [invalid-attribute-access] "Invalid access to descriptor attribute `value` on type `C`" +reveal_type(C().value) # revealed: bytes | str +``` + +### Descriptor diagnostics are reported through `super()` + +Accessing an inherited descriptor through `super()` still invokes its `__get__` method. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + +class Base: + value = Descriptor() + +class Derived(Base): + def access(self) -> None: + # error: [invalid-attribute-access] + super().value +``` + +### Type variables preserve invalid descriptor calls + +A type variable's bound does not prevent its receiver or descriptor value from reaching an invalid +`__get__` method. The same applies when accessing an attribute on `type[T]`. + +```py +from typing import TypeVar + +class Descriptor: + def __get__(self) -> int: + return 1 + +class Owner: + value = Descriptor() + +OwnerT = TypeVar("OwnerT", bound=Owner) +DescriptorT = TypeVar("DescriptorT", bound=Descriptor) + +def instance(owner: OwnerT) -> None: + # error: [invalid-attribute-access] + owner.value + +def class_object(owner: type[OwnerT]) -> None: + # error: [invalid-attribute-access] + owner.value + +def descriptor_value(descriptor: DescriptorT) -> None: + class C: + value = descriptor + + # error: [invalid-attribute-access] + C().value +``` + +### Intersections preserve invalid descriptor calls + +Intersecting a receiver or descriptor value with another type does not make its invalid `__get__` +method callable. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + +class Owner: + value = Descriptor() + +class Marker: ... + +def receiver(owner: Owner) -> None: + if isinstance(owner, Marker): + # error: [invalid-attribute-access] + owner.value + +def descriptor_value(descriptor: Descriptor) -> None: + if isinstance(descriptor, Marker): + class C: + value = descriptor + + # error: [invalid-attribute-access] + C().value +``` + +### Every `__get__` definition must accept the call + +A conditionally defined method can have several callable signatures. The access is invalid if any +possible definition rejects the call. + +```py +def access(flag: bool) -> None: + class Descriptor: + if flag: + def __get__(self, instance: object, owner: type | None = None) -> int: + return 1 + + else: + def __get__(self) -> str: + return "" + + class C: + value = Descriptor() + + # error: [invalid-attribute-access] "Invalid access to descriptor attribute `value` on type `C`" + reveal_type(C().value) # revealed: int | str +``` + +### A possible `__getattr__` fallback does not hide an invalid descriptor + +When a descriptor is only conditionally present, `__getattr__` handles the path where it is absent. +The other path still invokes the invalid descriptor and must produce a diagnostic. + +```py +def access(flag: bool) -> None: + class Descriptor: + def __get__(self) -> int: + return 1 + + class C: + if flag: + value = Descriptor() + + def __getattr__(self, name: str) -> str: + return name + + # error: [invalid-attribute-access] + reveal_type(C().value) # revealed: int | str +``` + +### A class-object lookup uses its declared member type + +Class-object member lookup uses the declared attribute type even when the declaration has no value. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + +class C: + value: Descriptor + +# error: [invalid-attribute-access] "Invalid access to descriptor attribute `value` on type ``" +C.value +``` + +### An instance `__getattribute__` can bypass descriptors + +A custom `__getattribute__` can return without invoking the malformed descriptor. The ordinary +member type remains unchanged, even when the override has the same return type as the descriptor. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + +class C: + value = Descriptor() + + def __getattribute__(self, name: str) -> int: + return 42 + +reveal_type(C().value) # revealed: int +``` + +### An instance `__getattribute__` may delegate to descriptor lookup + +The return annotation of an override does not establish whether it delegates to the default +attribute lookup. Since ty does not inspect the implementation, it cannot conclude that the +descriptor is invoked. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + +class C: + value = Descriptor() + + def __getattribute__(self, name: str) -> str: + return super().__getattribute__(name) + +C().value +``` + +### An assigned instance attribute shadows a non-data descriptor + +An instance attribute takes precedence over a non-data descriptor. After the assignment, reading the +attribute does not call the descriptor. + +```py +from typing import Literal + +class Descriptor: + def __get__(self) -> str: + return "" + +class C: + value = Descriptor() + + def replace(self) -> None: + self.value: int = 1 + reveal_type(self.value) # revealed: Literal[1] +``` + +### An instance assignment does not shadow a data descriptor + +Assigning to a data descriptor invokes its `__set__` method. A subsequent read still invokes its +`__get__` method, even though the attribute has a known assigned type. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + + def __set__(self, instance: object, value: int) -> None: + pass + +class C: + value = Descriptor() + + def access(self) -> None: + self.value = 1 + # error: [invalid-attribute-access] + self.value +``` + +### A conditional assignment does not hide an invalid descriptor call + +The assignment shadows the non-data descriptor on one path, but the other path still invokes its +invalid `__get__` method. + +```py +class Descriptor: + def __get__(self) -> str: + return "" + +class C: + value = Descriptor() + +def access(c: C, flag: bool) -> None: + if flag: + c.value = Descriptor() + + # error: [invalid-attribute-access] "Invalid access to descriptor attribute `value` on type `C`" + c.value +``` + +### Augmented assignment reads before writing + +An augmented assignment reads the descriptor before writing the operation's result. The malformed +`__get__` call is therefore reported even though `__set__` accepts the result. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + + def __set__(self, instance: object, value: int) -> None: + pass + +class C: + value = Descriptor() + +c = C() +# error: [invalid-attribute-access] "Invalid access to descriptor attribute `value` on type `C`" +c.value += 1 +``` + +### Deletion does not read a descriptor + +Deleting a descriptor calls `__delete__` without first calling `__get__`. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + + def __delete__(self, instance: object) -> None: + pass + +class C: + value = Descriptor() + +c = C() +del c.value +``` + +### A class attribute can shadow a metaclass non-data descriptor + +The class attribute takes precedence, so the malformed metaclass descriptor is not invoked. + +```py +class Descriptor: + def __get__(self) -> str: + return "" + +class Meta(type): + value = Descriptor() + +class C(metaclass=Meta): + value = 1 + +reveal_type(C.value) # revealed: int +``` + +### A possible class attribute does not shadow a metaclass descriptor + +A conditionally defined class attribute shadows a metaclass descriptor only when it exists. The +other path invokes the invalid descriptor. + +```py +class Descriptor: + def __get__(self) -> str: + return "" + +class Meta(type): + value = Descriptor() + +def access(flag: bool) -> None: + class C(metaclass=Meta): + if flag: + value = 1 + + # error: [invalid-attribute-access] + reveal_type(C.value) # revealed: str | int +``` + +### A metaclass data descriptor takes precedence over a class attribute + +A data descriptor on the metaclass runs even when the class defines an attribute with the same name, +so an invalid descriptor call must be reported. + +```py +class Descriptor: + def __get__(self) -> str: + return "" + + def __set__(self, instance: object, value: int) -> None: + pass + +class Meta(type): + value = Descriptor() + +class C(metaclass=Meta): + value = 1 + +# error: [invalid-attribute-access] +reveal_type(C.value) # revealed: str +``` + +### A metaclass data descriptor shadows an invalid class descriptor + +A data descriptor on the metaclass has priority over a descriptor stored on the class. The class +descriptor is never called, so its invalid signature does not affect the access. + +```py +class DataDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> int: + return 1 + + def __set__(self, instance: object, value: int) -> None: + pass + +class InvalidDescriptor: + def __get__(self) -> str: + return "" + +class Meta(type): + value = DataDescriptor() + +class C(metaclass=Meta): + value = InvalidDescriptor() + +reveal_type(C.value) # revealed: int +``` + +### `__get__` is not callable + +Python still attempts to call a non-callable `__get__` attribute, so the access fails and its type +is unknown. + +```py +class Descriptor: __get__: None = None -class Foo: - desc: BrokenDescriptor = BrokenDescriptor() +class C: + value: Descriptor = Descriptor() -# TODO: this raises `TypeError` at runtime due to the implicit call to `__get__`; -# we should emit a diagnostic -reveal_type(Foo().desc) # revealed: Unknown +# error: [invalid-attribute-access] "Invalid access to descriptor attribute `value` on type `C`" +reveal_type(C().value) # revealed: Unknown ``` ### Undeclared descriptor arguments @@ -1077,6 +1587,25 @@ def _(flag: bool): reveal_type(C().descriptor) # revealed: int | MaybeDescriptor ``` +### A possibly-unbound invalid `__get__` method still fails when present + +When a descriptor method is only conditionally defined, the branch where it exists must still accept +the implicit descriptor arguments. + +```py +def access(flag: bool) -> None: + class Descriptor: + if flag: + def __get__(self) -> int: + return 1 + + class C: + value = Descriptor() + + # error: [invalid-attribute-access] + reveal_type(C().value) # revealed: int | Descriptor +``` + ### Descriptors with non-function `__get__` callables that are descriptors themselves The descriptor protocol is recursive, i.e. looking up `__get__` can involve triggering the diff --git a/crates/ty_python_semantic/resources/mdtest/properties.md b/crates/ty_python_semantic/resources/mdtest/properties.md index e60e3da0eb..85e66e5098 100644 --- a/crates/ty_python_semantic/resources/mdtest/properties.md +++ b/crates/ty_python_semantic/resources/mdtest/properties.md @@ -243,8 +243,12 @@ class C: c = C() c.attr = 1 -# TODO: An error should be emitted here. -# See https://github.com/astral-sh/ruff/issues/16298 for more details. +# error: [call-non-callable] "property has no getter" +C.attr.__get__(c, C) +# error: [call-non-callable] "property has no getter" +type(C.attr).__get__(C.attr, c, C) + +# error: [invalid-attribute-access] "Cannot read property `attr` on object of type `C` because it has no getter" reveal_type(c.attr) # revealed: Never ``` diff --git a/crates/ty_python_semantic/resources/mdtest/regression/3593_function_known_decorators_cycle.md b/crates/ty_python_semantic/resources/mdtest/regression/3593_function_known_decorators_cycle.md index 2091b025ab..41dd5bc41a 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/3593_function_known_decorators_cycle.md +++ b/crates/ty_python_semantic/resources/mdtest/regression/3593_function_known_decorators_cycle.md @@ -13,7 +13,9 @@ from typing import Self, overload, reveal_type class C: a: D +# error: [invalid-attribute-access] C.a +# error: [invalid-attribute-access] reveal_type(C().a) # revealed: Unknown | D class D: diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 779844d753..2ddcfc382d 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -103,7 +103,9 @@ impl PublicTypePolicy { } /// The source definition provenance for a place. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +#[derive( + Debug, Clone, Copy, Default, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue, +)] pub(crate) enum Provenance<'db> { /// No source definition is known. #[default] @@ -142,7 +144,7 @@ impl<'db> Provenance<'db> { } /// A defined place with its raw type, origin, definedness, public-type policy, and provenance. -#[derive(Debug, Clone, Copy, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] pub(crate) struct DefinedPlace<'db> { pub(crate) ty: Type<'db>, pub(crate) origin: TypeOrigin, @@ -221,7 +223,9 @@ impl<'db> DefinedPlace<'db> { /// bound_or_declared: Place::Defined(DefinedPlace { ty: Literal[1], origin: TypeOrigin::Inferred, definedness: Definedness::PossiblyUndefined, .. }), /// non_existent: Place::Undefined, /// ``` -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +#[derive( + Debug, Clone, Copy, Default, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue, +)] pub(crate) enum Place<'db> { Defined(DefinedPlace<'db>), #[default] @@ -363,11 +367,13 @@ impl<'db> Place<'db> { }), Place::Defined(defined) => { - if let Some((dunder_get_return_ty, _)) = - defined.ty.try_call_dunder_get(db, env, None, owner) - { + let result = defined + .ty + .try_call_dunder_get(db, env, None, owner) + .unwrap_or_else(|error| Some(error.fallback())); + if let Some(result) = result { Place::Defined(DefinedPlace { - ty: dunder_get_return_ty, + ty: result.return_type, provenance: Provenance::Unknown, ..defined }) @@ -894,7 +900,9 @@ impl<'db> PlaceFromDeclarationsResult<'db> { /// that this comes with a [`CLASS_VAR`] type qualifier. /// /// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR -#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +#[derive( + Debug, Clone, Default, Copy, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue, +)] pub(crate) struct PlaceAndQualifiers<'db> { pub(crate) place: Place<'db>, pub(crate) qualifiers: TypeQualifiers, diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index f90a194083..33379da50c 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -66,7 +66,7 @@ pub(crate) use crate::types::callable::{CallableType, CallableTypes}; pub(crate) use crate::types::class_base::ClassBase; use crate::types::constraints::ConstraintSetBuilder; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; -use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM}; +use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, report_bad_dunder_get_call}; pub use crate::types::display::{DisplaySettings, TypeDetail, TypeDisplayDetails}; pub(crate) use crate::types::enums::{EnumClassLiteral, EnumComplementType, enum_metadata}; pub(crate) use crate::types::equality::{ComparisonSoundnessPolicy, equality_truthiness}; @@ -534,6 +534,220 @@ impl AttributeKind { } } +/// An interned description of an invalid implicit `__get__` call. +/// +/// Member lookup carries this compact context through unions and fallbacks. Expression inference +/// reconstructs the concrete [`CallError`] if the invalid access remains after applying lookup +/// fallbacks and local assignment information. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +struct DescriptorGetCallContext<'db> { + #[returns(copy)] + descriptor_type: Type<'db>, + #[returns(copy)] + callable_type: Type<'db>, + #[returns(copy)] + instance: Option>, + #[returns(copy)] + owner: Type<'db>, +} + +impl get_size2::GetSize for DescriptorGetCallContext<'_> {} + +impl<'db> DescriptorGetCallContext<'db> { + /// Reconstructs the implicit call and returns its error if the call is still invalid. + fn into_error(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { + let descriptor_type = self.descriptor_type(db); + let instance = self.instance(db).unwrap_or_else(|| Type::none(db, env)); + let owner = self.owner(db); + self.callable_type(db) + .try_call( + db, + env, + &CallArguments::positional([descriptor_type, instance, owner]), + ) + .err() + } +} + +/// The type and descriptor kind produced by an implicit `__get__` call. +#[derive(Clone, Debug, Copy, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +pub(crate) struct DescriptorGetResult<'db> { + pub(crate) return_type: Type<'db>, + kind: AttributeKind, +} + +/// A failed implicit descriptor call together with its recovery value. +#[derive(Clone, Debug, Copy, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +pub(crate) struct DescriptorGetError<'db> { + fallback: DescriptorGetResult<'db>, + context: DescriptorGetCallContext<'db>, +} + +impl<'db> DescriptorGetError<'db> { + /// Returns the descriptor's declared return type and kind despite the invalid call. + pub(crate) const fn fallback(self) -> DescriptorGetResult<'db> { + self.fallback + } +} + +fn descriptor_get_result<'db>( + return_type: Type<'db>, + kind: AttributeKind, + error: Option>, +) -> Result>, DescriptorGetError<'db>> { + let result = DescriptorGetResult { return_type, kind }; + match error { + Some(context) => Err(DescriptorGetError { + fallback: result, + context, + }), + None => Ok(Some(result)), + } +} + +/// An operation that failed while resolving an attribute. +#[derive(Clone, Debug, Copy, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] +enum MemberLookupErrorKind<'db> { + DescriptorGet(DescriptorGetCallContext<'db>), +} + +/// A failed member lookup together with the member used to recover from the error. +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +struct MemberLookupError<'db> { + #[returns(copy)] + fallback_member: PlaceAndQualifiers<'db>, + #[returns(copy)] + kind: MemberLookupErrorKind<'db>, +} + +impl get_size2::GetSize for MemberLookupError<'_> {} + +impl<'db> MemberLookupError<'db> { + /// Reports the failed implicit call unless the lookup is shadowed or used for deletion. + fn report_diagnostic( + self, + context: &InferContext<'db, '_>, + object_type: Type<'db>, + target: &ast::ExprAttribute, + assigned_type: Option>, + ) { + if matches!(target.ctx, ast::ExprContext::Del) { + return; + } + + let db = context.db(); + let env = context.program_environment(); + + match self.kind(db) { + MemberLookupErrorKind::DescriptorGet(call_context) + if (assigned_type.is_none() + || call_context.descriptor_type(db).is_data_descriptor(db, env)) + && let Some(failure) = call_context.into_error(db, env) => + { + report_bad_dunder_get_call( + context, + &failure, + object_type, + call_context.descriptor_type(db), + target, + ); + } + MemberLookupErrorKind::DescriptorGet(_) => {} + } + } +} + +/// A resolved member or an implicit-call error that retains its recovery value. +/// +/// Unlike [`crate::place::LookupResult`], errors here describe failed attribute-access operations, +/// not undefined or possibly undefined places. +type MemberLookupResult<'db> = Result, MemberLookupError<'db>>; + +fn member_lookup_result<'db>( + db: &'db dyn Db, + member: PlaceAndQualifiers<'db>, + error: Option>, +) -> MemberLookupResult<'db> { + match error { + Some(kind) => Err(MemberLookupError::new(db, member, kind)), + None => Ok(member), + } +} + +fn map_member_lookup_type<'db>( + db: &'db dyn Db, + result: MemberLookupResult<'db>, + f: impl FnOnce(Type<'db>) -> Type<'db>, +) -> MemberLookupResult<'db> { + match result { + Ok(member) => Ok(member.map_type(f)), + Err(error) => Err(MemberLookupError::new( + db, + error.fallback_member(db).map_type(f), + error.kind(db), + )), + } +} + +fn member_lookup_or_fall_back_to<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + result: MemberLookupResult<'db>, + fallback_fn: impl FnOnce() -> MemberLookupResult<'db>, +) -> MemberLookupResult<'db> { + let member = result.unwrap_or_else(|error| error.fallback_member(db)); + match member.place { + Place::Undefined => fallback_fn(), + Place::Defined(DefinedPlace { + definedness: Definedness::AlwaysDefined, + .. + }) => result, + Place::Defined(DefinedPlace { + definedness: Definedness::PossiblyUndefined, + .. + }) => { + let fallback = fallback_fn(); + let fallback_member = fallback.unwrap_or_else(|error| error.fallback_member(db)); + member_lookup_result( + db, + member.or_fall_back_to(db, env, || fallback_member), + result + .err() + .map(|error| error.kind(db)) + .or_else(|| fallback.err().map(|error| error.kind(db))), + ) + } + } +} + +fn cycle_normalized_member_lookup<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + result: MemberLookupResult<'db>, + previous: MemberLookupResult<'db>, + cycle: &salsa::Cycle, +) -> MemberLookupResult<'db> { + let error = result + .err() + .map(|error| error.kind(db)) + .filter(|_| cycle.iteration() <= crate::TAINTED_CYCLES || previous.is_err()); + let member = result.unwrap_or_else(|error| error.fallback_member(db)); + let previous = previous.unwrap_or_else(|error| error.fallback_member(db)); + member_lookup_result(db, member.cycle_normalized(db, env, previous, cycle), error) +} + +impl<'db> From> for MemberLookupResult<'db> { + fn from(member: PlaceAndQualifiers<'db>) -> Self { + Ok(member) + } +} + +impl<'db> From> for MemberLookupResult<'db> { + fn from(place: Place<'db>) -> Self { + Ok(place.into()) + } +} + /// 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 @@ -3472,43 +3686,98 @@ impl<'db> Type<'db> { } } - /// Look up `__get__` on the meta-type of self, and call it with the arguments `self`, `instance`, - /// and `owner`. `__get__` is different than other dunder methods in that it is not looked up using - /// the descriptor protocol itself. + /// Looks up `__get__` on the meta-type of `self` and calls it with `self`, `instance`, and + /// `owner`. Unlike other dunder methods, `__get__` is not itself looked up using the + /// descriptor protocol. /// - /// In addition to the return type of `__get__`, this method also returns the *kind* of attribute - /// that `self` represents: (1) a data descriptor or (2) a non-data descriptor / normal attribute. + /// Returns the resulting type and descriptor kind, or an error retaining the recovery value + /// when the implicit call is invalid. Returns `Ok(None)` when `__get__` is not defined. /// - /// If `__get__` is not defined on the meta-type, this method returns `None`. + /// For example, accessing `C().value` below implicitly supplies the descriptor value, the + /// `C` instance, and `C`, so the declared method is missing two parameters: + /// + /// ```python + /// class Descriptor: + /// def __get__(self): ... + /// + /// class C: + /// value = Descriptor() + /// + /// C().value + /// ``` pub(crate) fn try_call_dunder_get( self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, instance: Option>, owner: Type<'db>, - ) -> Option<(Type<'db>, AttributeKind)> { - #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _, _| None, heap_size=ruff_memory_usage::heap_size)] + ) -> Result>, DescriptorGetError<'db>> { + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _, _, _, _| Ok(None), heap_size=ruff_memory_usage::heap_size)] fn try_call_dunder_get_inner<'db>( db: &'db dyn Db, program: Program<'db>, ty: Type<'db>, instance: Option>, owner: Type<'db>, - ) -> Option<(Type<'db>, AttributeKind)> { + ) -> Result>, DescriptorGetError<'db>> { let env = &ProgramEnvironment::from_program(program); if let Some(fallback) = ty.materialized_divergent_fallback() { return fallback.try_call_dunder_get(db, env, instance, owner); } if let Some(dynamic) = ty.dynamic_descriptor_type() { - return Some((dynamic, AttributeKind::DataDescriptor)); + return Ok(Some(DescriptorGetResult { + return_type: dynamic, + kind: AttributeKind::DataDescriptor, + })); + } + + if let Some(union) = ty.as_union_like(db) { + let mut return_types = UnionBuilder::new(db, env); + let mut error = None; + let mut any_descriptor = false; + let mut all_data_descriptors = true; + + for alternative in union.elements(db) { + let result = alternative + .try_call_dunder_get(db, env, instance, owner) + .unwrap_or_else(|failure| { + error = error.or(Some(failure.context)); + Some(failure.fallback()) + }); + if let Some(DescriptorGetResult { return_type, kind }) = result { + any_descriptor = true; + all_data_descriptors &= kind.is_data(); + return_types = return_types.add(return_type); + } else { + all_data_descriptors = false; + return_types = return_types.add(*alternative); + } + } + + return if any_descriptor { + descriptor_get_result( + return_types.build(), + if all_data_descriptors { + AttributeKind::DataDescriptor + } else { + AttributeKind::NormalOrNonDataDescriptor + }, + error, + ) + } else { + Ok(None) + }; } match ty { Type::Callable(callable) if callable.is_staticmethod_like(db) => { // For "staticmethod-like" callables, model the behavior of `staticmethod.__get__`. // The underlying function is returned as-is, without binding self. - return Some((ty, AttributeKind::NormalOrNonDataDescriptor)); + return Ok(Some(DescriptorGetResult { + return_type: ty, + kind: AttributeKind::NormalOrNonDataDescriptor, + })); } Type::Callable(callable) if let is_function_like = callable.is_function_like(db) @@ -3517,24 +3786,28 @@ impl<'db> Type<'db> { // For "function-like" or "classmethod-like" callables, model the behavior of // `FunctionType.__get__` or `classmethod.__get__`. // - // It is a shortcut to model this in `try_call_dunder_get`. If we want to be really precise, - // we should instead return a new method-wrapper type variant for the synthesized `__get__` - // method of these synthesized functions. The method-wrapper would then be returned from - // `find_name_in_mro` when called on function-like `Callable`s. This would allow us to - // correctly model the behavior of *explicit* `SomeDataclass.__init__.__get__` calls. - return if instance.is_none() && is_function_like { - Some((ty, AttributeKind::NormalOrNonDataDescriptor)) + // It is a shortcut to model this in `try_call_dunder_get`. If we + // want to be really precise, we should instead return a new method-wrapper + // type variant for the synthesized `__get__` method of these synthesized + // functions. The method-wrapper would then be returned from + // `find_name_in_mro` when called on function-like `Callable`s. This would + // allow us to correctly model the behavior of *explicit* + // `SomeDataclass.__init__.__get__` calls. + let return_type = if instance.is_none() && is_function_like { + ty } else { let self_type = instance.unwrap_or_else(|| { // For classmethod-like callables, bind to the owner class. owner.to_instance_approximation(db, env).unwrap_or(owner) }); - Some(( - Type::Callable(callable.bind_self(db, env, Some(self_type))), - AttributeKind::NormalOrNonDataDescriptor, - )) + Type::Callable(callable.bind_self(db, env, Some(self_type))) }; + + return Ok(Some(DescriptorGetResult { + return_type, + kind: AttributeKind::NormalOrNonDataDescriptor, + })); } _ => {} } @@ -3546,13 +3819,13 @@ impl<'db> Type<'db> { .class_member_with_policy(db, env, "__get__", MemberLookupPolicy::REQUIRE_CONCRETE) .place else { - return None; + return Ok(None); }; // A recursive member lookup can yield the internal cycle marker. It does not // represent a concrete descriptor method and must not escape through the access. if concrete_descr_get.is_divergent() { - return None; + return Ok(None); } // Descriptor special-method lookup checks the descriptor's type, so instance storage @@ -3570,34 +3843,35 @@ impl<'db> Type<'db> { ) .place else { - return None; + return Ok(None); }; let instance_ty = instance.unwrap_or_else(|| Type::none(db, env)); - let return_ty = descr_get - .try_call( - db, - env, - &CallArguments::positional([ty, instance_ty, owner]), - ) - .map(|bindings| { - if descr_get_boundness == Definedness::AlwaysDefined { - bindings.return_type(db, env) - } else { - UnionType::from_two_elements(db, env, bindings.return_type(db, env), ty) - } - }) - // TODO: an error when calling `__get__` will lead to a `TypeError` or similar at runtime; - // we should emit a diagnostic here instead of silently ignoring the error. - .unwrap_or_else(|CallError(_, bindings)| bindings.return_type(db, env)); - - let descriptor_kind = if ty.is_data_descriptor(db, env) { + let kind = if ty.is_data_descriptor(db, env) { AttributeKind::DataDescriptor } else { AttributeKind::NormalOrNonDataDescriptor }; + let (return_type, error) = match descr_get.try_call( + db, + env, + &CallArguments::positional([ty, instance_ty, owner]), + ) { + Ok(bindings) => (bindings.return_type(db, env), None), + Err(error) => ( + error.return_type(db, env), + Some(DescriptorGetCallContext::new( + db, ty, descr_get, instance, owner, + )), + ), + }; + let return_type = if descr_get_boundness == Definedness::AlwaysDefined { + return_type + } else { + UnionType::from_two_elements(db, env, return_type, ty) + }; - Some((return_ty, descriptor_kind)) + descriptor_get_result(return_type, kind, error) } tracing::trace!( @@ -3612,7 +3886,7 @@ impl<'db> Type<'db> { // Function descriptors have fixed binding behavior, so avoid retaining a tracked query // for every function and access context. if let Type::FunctionLiteral(function) = self { - let descriptor_result = if function.is_classmethod(db) { + let return_type = if function.is_classmethod(db) { Type::BoundMethod(BoundMethodType::new(db, function, owner)) } else if let Some(instance) = instance && !function.is_staticmethod(db) @@ -3622,7 +3896,10 @@ impl<'db> Type<'db> { self }; - return Some((descriptor_result, AttributeKind::NormalOrNonDataDescriptor)); + return Ok(Some(DescriptorGetResult { + return_type, + kind: AttributeKind::NormalOrNonDataDescriptor, + })); } try_call_dunder_get_inner(db, env.program(db), self, instance, owner) @@ -3637,7 +3914,11 @@ impl<'db> Type<'db> { attribute: PlaceAndQualifiers<'db>, instance: Option>, owner: Type<'db>, - ) -> (PlaceAndQualifiers<'db>, AttributeKind) { + ) -> ( + PlaceAndQualifiers<'db>, + AttributeKind, + Option>, + ) { if let PlaceAndQualifiers { place: Place::Defined(DefinedPlace { @@ -3667,7 +3948,7 @@ impl<'db> Type<'db> { ); } - match attribute { + let (member, kind, error) = match attribute { // A directly dynamic attribute could be a data descriptor even though we cannot see // its methods. Preserve that uncertainty, along with the existing bottom and cycle // behavior, without performing member lookups that cannot add information. @@ -3678,7 +3959,7 @@ impl<'db> Type<'db> { .. }), qualifiers: _, - } => (attribute, AttributeKind::DataDescriptor), + } => (attribute, AttributeKind::DataDescriptor, None), PlaceAndQualifiers { place: @@ -3692,13 +3973,19 @@ impl<'db> Type<'db> { qualifiers, } => { let mut all_data_descriptors = true; - + let mut error = None; let place = union .map_with_boundness(db, env, |elem| { - let ty = match elem.try_call_dunder_get(db, env, instance, owner) { - Some((ty, kind)) => { + let result = elem + .try_call_dunder_get(db, env, instance, owner) + .unwrap_or_else(|failure| { + error = error.or(Some(failure.context)); + Some(failure.fallback()) + }); + let ty = match result { + Some(DescriptorGetResult { return_type, kind }) => { all_data_descriptors &= kind.is_data(); - ty + return_type } None => { all_data_descriptors = false; @@ -3722,7 +4009,7 @@ impl<'db> Type<'db> { AttributeKind::NormalOrNonDataDescriptor }; - (place, kind) + (place, kind, error) } attribute @ PlaceAndQualifiers { @@ -3735,16 +4022,22 @@ impl<'db> Type<'db> { provenance: attribute_provenance, }), qualifiers, - } => ( - if intersection.positive(db).is_empty() { + } => { + let mut error = None; + let place = if intersection.positive(db).is_empty() { attribute } else { intersection .map_with_boundness(db, env, |elem| { + let ty = elem + .try_call_dunder_get(db, env, instance, owner) + .unwrap_or_else(|failure| { + error = error.or(Some(failure.context)); + Some(failure.fallback()) + }) + .map_or(*elem, |result| result.return_type); Place::Defined(DefinedPlace { - ty: elem - .try_call_dunder_get(db, env, instance, owner) - .map_or(*elem, |(ty, _)| ty), + ty, origin, definedness, public_type_policy, @@ -3752,10 +4045,15 @@ impl<'db> Type<'db> { }) }) .with_qualifiers(qualifiers) - }, - // TODO: Discover data descriptors in intersections. - AttributeKind::NormalOrNonDataDescriptor, - ), + }; + ( + place, + // TODO: Discover data descriptors in intersections without decomposing the + // descriptor return type into an unsound intersection. + AttributeKind::NormalOrNonDataDescriptor, + error, + ) + } PlaceAndQualifiers { place: @@ -3768,27 +4066,35 @@ impl<'db> Type<'db> { }), qualifiers: _, } => { - if let Some((return_ty, attribute_kind)) = - attribute_ty.try_call_dunder_get(db, env, instance, owner) - { + let mut error = None; + let result = attribute_ty + .try_call_dunder_get(db, env, instance, owner) + .unwrap_or_else(|failure| { + error = Some(failure.context); + Some(failure.fallback()) + }); + if let Some(DescriptorGetResult { return_type, kind }) = result { ( Place::Defined(DefinedPlace { - ty: return_ty, + ty: return_type, origin, definedness: boundness, public_type_policy, provenance, }) .into(), - attribute_kind, + kind, + error, ) } else { - (attribute, AttributeKind::NormalOrNonDataDescriptor) + (attribute, AttributeKind::NormalOrNonDataDescriptor, None) } } - _ => (attribute, AttributeKind::NormalOrNonDataDescriptor), - } + _ => (attribute, AttributeKind::NormalOrNonDataDescriptor, None), + }; + + (member, kind, error) } /// Returns whether this type is a data descriptor, i.e. defines `__set__` or `__delete__`. @@ -3923,35 +4229,41 @@ impl<'db> Type<'db> { env: &ProgramEnvironment<'db>, key: MemberLookupKey<'db>, receiver: Type<'db>, - fallback: PlaceAndQualifiers<'db>, + fallback: MemberLookupResult<'db>, policy: InstanceFallbackShadowsNonDataDescriptor, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { let ty = key.ty(db); + let meta_attr_plain = Self::instance_lookup_class_member_with_policy(db, env, key); let ( PlaceAndQualifiers { place: meta_attr, qualifiers: meta_attr_qualifiers, }, meta_attr_kind, + meta_attr_error, ) = Self::try_call_dunder_get_on_attribute( db, env, - Self::instance_lookup_class_member_with_policy(db, env, key), + meta_attr_plain, Some(receiver), ty.to_meta_type(db, env), ); + let meta_attr_error = meta_attr_error.map(MemberLookupErrorKind::DescriptorGet); + let fallback_error = fallback.err().map(|error| error.kind(db)); let PlaceAndQualifiers { place: fallback, qualifiers: fallback_qualifiers, - } = fallback; + } = fallback.unwrap_or_else(|error| error.fallback_member(db)); match (meta_attr, meta_attr_kind, fallback) { // The fallback type is unbound, so we can just return `meta_attr` unconditionally, // no matter if it's data descriptor, a non-data descriptor, or a normal attribute. - (meta_attr @ Place::Defined(_), _, Place::Undefined) => { - meta_attr.with_qualifiers(meta_attr_qualifiers) - } + (meta_attr @ Place::Defined(_), _, Place::Undefined) => member_lookup_result( + db, + meta_attr.with_qualifiers(meta_attr_qualifiers), + meta_attr_error, + ), // `meta_attr` is the return type of a data descriptor and definitely bound, so we // return it. @@ -3962,7 +4274,11 @@ impl<'db> Type<'db> { }), AttributeKind::DataDescriptor, _, - ) => meta_attr.with_qualifiers(meta_attr_qualifiers), + ) => member_lookup_result( + db, + meta_attr.with_qualifiers(meta_attr_qualifiers), + meta_attr_error, + ), // `meta_attr` is the return type of a data descriptor, but the attribute on the // meta-type is possibly-unbound. This means that we "fall through" to the next @@ -3983,14 +4299,18 @@ impl<'db> Type<'db> { public_type_policy: fallback_public_type_policy, provenance: fallback_provenance, }), - ) => Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements(db, env, meta_attr_ty, fallback_ty), - origin: meta_origin.merge(fallback_origin), - definedness: fallback_boundness, - public_type_policy: fallback_public_type_policy, - provenance: fallback_provenance.or(meta_attr_provenance), - }) - .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), + ) => member_lookup_result( + db, + Place::Defined(DefinedPlace { + ty: UnionType::from_two_elements(db, env, meta_attr_ty, fallback_ty), + origin: meta_origin.merge(fallback_origin), + definedness: fallback_boundness, + public_type_policy: fallback_public_type_policy, + provenance: fallback_provenance.or(meta_attr_provenance), + }) + .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), + meta_attr_error.or(fallback_error), + ), // `meta_attr` is *not* a data descriptor. This means that the `fallback` type has // now the highest priority. However, we only return the pure `fallback` type if the @@ -4007,9 +4327,11 @@ impl<'db> Type<'db> { definedness: Definedness::AlwaysDefined, .. }), - ) if policy == InstanceFallbackShadowsNonDataDescriptor::Yes => { - fallback.with_qualifiers(fallback_qualifiers) - } + ) if policy == InstanceFallbackShadowsNonDataDescriptor::Yes => member_lookup_result( + db, + fallback.with_qualifiers(fallback_qualifiers), + fallback_error, + ), // `meta_attr` is *not* a data descriptor. The `fallback` symbol is either possibly // unbound or the policy argument is `No`. In both cases, the `fallback` type does @@ -4030,17 +4352,25 @@ impl<'db> Type<'db> { public_type_policy: fallback_public_type_policy, provenance: fallback_provenance, }), - ) => Place::Defined(DefinedPlace { - ty: UnionType::from_two_elements(db, env, meta_attr_ty, fallback_ty), - origin: meta_origin.merge(fallback_origin), - definedness: meta_attr_boundness.max(fallback_boundness), - public_type_policy: fallback_public_type_policy, - provenance: fallback_provenance.or(meta_attr_provenance), - }) - .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), + ) => member_lookup_result( + db, + Place::Defined(DefinedPlace { + ty: UnionType::from_two_elements(db, env, meta_attr_ty, fallback_ty), + origin: meta_origin.merge(fallback_origin), + definedness: meta_attr_boundness.max(fallback_boundness), + public_type_policy: fallback_public_type_policy, + provenance: fallback_provenance.or(meta_attr_provenance), + }) + .with_qualifiers(meta_attr_qualifiers.union(fallback_qualifiers)), + meta_attr_error.or(fallback_error), + ), // If the attribute is not found on the meta-type, we simply return the fallback. - (Place::Undefined, _, fallback) => fallback.with_qualifiers(fallback_qualifiers), + (Place::Undefined, _, fallback) => member_lookup_result( + db, + fallback.with_qualifiers(fallback_qualifiers), + fallback_error, + ), } } @@ -4049,16 +4379,31 @@ impl<'db> Type<'db> { /// /// See also: [`Type::static_member`] /// - /// TODO: We should return a `Result` here to handle errors that can appear during attribute - /// lookup, like a failed `__get__` call on a descriptor. #[must_use] - fn member( + pub(crate) fn member( self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, name: &str, ) -> PlaceAndQualifiers<'db> { - self.member_lookup_with_policy(db, env, name, MemberLookupPolicy::default()) + self.try_member_lookup(db, env, name) + .unwrap_or_else(|error| error.fallback_member(db)) + } + + /// Performs member lookup while retaining errors from implicit attribute-access methods. + fn try_member_lookup( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> MemberLookupResult<'db> { + self.member_lookup_with_policy_and_receiver( + db, + env, + name, + MemberLookupPolicy::default(), + None, + ) } /// Similar to [`Type::member`], but allows the caller to specify what policy should be used @@ -4071,6 +4416,7 @@ impl<'db> Type<'db> { policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { self.member_lookup_with_policy_and_receiver(db, env, name, policy, None) + .unwrap_or_else(|error| error.fallback_member(db)) } /// Perform member lookup while optionally binding descriptors and `Self` to a more precise @@ -4085,27 +4431,27 @@ impl<'db> Type<'db> { name: &str, policy: MemberLookupPolicy, receiver: Option>, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { #[salsa::tracked( returns(copy), - cycle_initial=|_, id, _| Place::bound(Type::divergent(id)).into(), - cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, member: PlaceAndQualifiers<'db>, key: MemberLookupKey<'db>| { - member.cycle_normalized(db, &ProgramEnvironment::from_program(key.program(db)), *previous, cycle) + cycle_initial=|_, id, _| Ok(Place::bound(Type::divergent(id)).into()), + cycle_fn=|db, cycle, previous: &MemberLookupResult<'db>, member: MemberLookupResult<'db>, key: MemberLookupKey<'db>| { + cycle_normalized_member_lookup(db, &ProgramEnvironment::from_program(key.program(db)), member, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] fn member_lookup_with_policy_inner<'db>( db: &'db dyn Db, key: MemberLookupKey<'db>, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { member_lookup_with_policy_impl(db, key, None) } #[salsa::tracked( returns(copy), - cycle_initial=|_, id, _, _| Place::bound(Type::divergent(id)).into(), - cycle_fn=|db, cycle, previous: &PlaceAndQualifiers<'db>, member: PlaceAndQualifiers<'db>, key: MemberLookupKey<'db>, _| { - member.cycle_normalized(db, &ProgramEnvironment::from_program(key.program(db)), *previous, cycle) + cycle_initial=|_, id, _, _| Ok(Place::bound(Type::divergent(id)).into()), + cycle_fn=|db, cycle, previous: &MemberLookupResult<'db>, member: MemberLookupResult<'db>, key: MemberLookupKey<'db>, _| { + cycle_normalized_member_lookup(db, &ProgramEnvironment::from_program(key.program(db)), member, *previous, cycle) }, heap_size=ruff_memory_usage::heap_size )] @@ -4113,7 +4459,7 @@ impl<'db> Type<'db> { db: &'db dyn Db, key: MemberLookupKey<'db>, receiver: Type<'db>, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { member_lookup_with_policy_impl(db, key, Some(receiver)) } @@ -4121,22 +4467,23 @@ impl<'db> Type<'db> { db: &'db dyn Db, key: MemberLookupKey<'db>, receiver: Option>, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { fn promote_inferred_attribute_class_literals<'db>( db: &'db dyn Db, env: &ProgramEnvironment<'db>, - result: PlaceAndQualifiers<'db>, - ) -> PlaceAndQualifiers<'db> { + result: MemberLookupResult<'db>, + ) -> MemberLookupResult<'db> { + let member = result.unwrap_or_else(|error| error.fallback_member(db)); let should_promote = matches!( - result.place, + member.place, Place::Defined(DefinedPlace { origin: TypeOrigin::Inferred, .. }) - ) && !result.qualifiers.contains(TypeQualifiers::FINAL); + ) && !member.qualifiers.contains(TypeQualifiers::FINAL); if should_promote { - result.map_type(|ty| ty.promote_class_literals(db, env)) + map_member_lookup_type(db, result, |ty| ty.promote_class_literals(db, env)) } else { result } @@ -4147,7 +4494,7 @@ impl<'db> Type<'db> { env: &ProgramEnvironment<'db>, key: MemberLookupKey<'db>, receiver: Type<'db>, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { let this = key.ty(db); let name = key.name(db); let name_str = name.as_str(); @@ -4179,11 +4526,15 @@ impl<'db> Type<'db> { env, key, receiver, - fallback, + fallback.into(), InstanceFallbackShadowsNonDataDescriptor::No, ); - if result.is_class_var() && this.is_typed_dict() { + if result + .unwrap_or_else(|error| error.fallback_member(db)) + .is_class_var() + && this.is_typed_dict() + { // `ClassVar`s on `TypedDictFallback` cannot be accessed on inhabitants of `SomeTypedDict`. // They can only be accessed on `SomeTypedDict` directly. return Place::Undefined.into(); @@ -4192,7 +4543,9 @@ impl<'db> Type<'db> { let result = this.fallback_to_getattr(db, env, name, result, key.policy(db)); // An inferred attribute accessed through an instance can resolve to an override // on a subclass, so an exact class object is not a safe public type here. - let result = result.map_type(|ty| ty.bind_self_typevars(db, env, receiver)); + let result = map_member_lookup_type(db, result, |ty| { + ty.bind_self_typevars(db, env, receiver) + }); promote_inferred_attribute_class_literals(db, env, result) } @@ -4214,27 +4567,42 @@ impl<'db> Type<'db> { } match this { - Type::Union(union) => union.map_with_boundness_and_qualifiers(db, env, |elem| { - elem.member_lookup_with_policy_and_receiver(db, env, name_str, policy, receiver) - }), + Type::Union(union) => { + let mut error = None; + let member = union.map_with_boundness_and_qualifiers(db, env, |elem| { + let result = elem.member_lookup_with_policy_and_receiver( + db, env, name_str, policy, receiver, + ); + error = error.or_else(|| result.err().map(|error| error.kind(db))); + result.unwrap_or_else(|error| error.fallback_member(db)) + }); + member_lookup_result(db, member, error) + } Type::Intersection(intersection) => { if let Some(complement) = intersection.enum_complement(db, env) { enums::member_lookup_for_enum_complement( db, env, complement, name_str, policy, ) + .into() } else { let receiver = Some(receiver.unwrap_or(this)); - intersection.map_with_boundness_and_qualifiers(db, env, |elem| { - elem.member_lookup_with_policy_and_receiver( - db, env, name_str, policy, receiver, - ) - }) + let mut error = None; + let member = + intersection.map_with_boundness_and_qualifiers(db, env, |elem| { + let result = elem.member_lookup_with_policy_and_receiver( + db, env, name_str, policy, receiver, + ); + error = error.or_else(|| result.err().map(|error| error.kind(db))); + result.unwrap_or_else(|error| error.fallback_member(db)) + }); + member_lookup_result(db, member, error) } } Type::EnumComplement(complement) => { enums::member_lookup_for_enum_complement(db, env, complement, name_str, policy) + .into() } Type::Dynamic(..) | Type::Divergent(_) | Type::Never => Place::bound(this).into(), @@ -4397,19 +4765,21 @@ impl<'db> Type<'db> { Place::bound(Type::FunctionLiteral(bound_method.function(db))).into() } _ => { - KnownClass::MethodType + let result = KnownClass::MethodType .to_instance(db, env) .member_lookup_with_policy_and_receiver( db, env, name_str, policy, receiver, - ) - .or_fall_back_to(db, env, || { - // If an attribute is not available on the bound method object, - // it will be looked up on the underlying function object. This - // changes the lookup object, so do not forward the bound-method - // receiver. - Type::FunctionLiteral(bound_method.function(db)) - .member_lookup_with_policy(db, env, name_str, policy) - }) + ); + member_lookup_or_fall_back_to(db, env, result, || { + // If an attribute is not available on the bound method object, + // it will be looked up on the underlying function object. This + // changes the lookup object, so do not forward the bound-method + // receiver. + Type::FunctionLiteral(bound_method.function(db)) + .member_lookup_with_policy_and_receiver( + db, env, name_str, policy, None, + ) + }) } }, Type::KnownBoundMethod(method) => method @@ -4471,7 +4841,7 @@ impl<'db> Type<'db> { Place::bound(Type::int_literal(i64::from(bool_value))).into() } - Type::ModuleLiteral(module) => module.static_member(db, env, name_str), + Type::ModuleLiteral(module) => module.static_member(db, env, name_str).into(), // If a protocol does not include a member and the policy disables falling back to // `object`, we return `Place::Undefined` here. This short-circuits attribute lookup @@ -4499,7 +4869,7 @@ impl<'db> Type<'db> { Type::NewTypeInstance(new_type_instance) if this.as_union_like(db).is_some() => { new_type_instance .concrete_base_type(db) - .member_lookup_with_policy(db, env, name_str, policy) + .member_lookup_with_policy_and_receiver(db, env, name_str, policy, None) } Type::TypeAlias(alias) => alias @@ -4508,15 +4878,17 @@ impl<'db> Type<'db> { _ if policy.no_instance_fallback() => { let receiver = receiver.unwrap_or(this); - Type::invoke_descriptor_protocol( + let result = Type::invoke_descriptor_protocol( db, env, key, receiver, Place::Undefined.into(), InstanceFallbackShadowsNonDataDescriptor::No, - ) - .map_type(|ty| ty.bind_self_typevars(db, env, receiver)) + ); + map_member_lookup_type(db, result, |ty| { + ty.bind_self_typevars(db, env, receiver) + }) } Type::LiteralValue(literal) @@ -4556,10 +4928,9 @@ impl<'db> Type<'db> { } Type::TypeVar(typevar) => { let receiver = receiver.unwrap_or(this); - if let Some(bound) = typevar - .typevar(db) - .bound_or_constraints(db, env) - .map(|bound| bound.as_type(db, env)) + let bound_or_constraints = typevar.typevar(db).bound_or_constraints(db, env); + if let Some(bound) = bound_or_constraints + .map(|bound_or_constraints| bound_or_constraints.as_type(db, env)) && bound.to_instance(db, env).is_some() { // A TypeVar can be bounded by a class-object type such as `type[A]`, which @@ -4623,7 +4994,10 @@ impl<'db> Type<'db> { db, env, name_str, policy, receiver, ); if name_str == "func" { - match nominal_lookup.place { + match nominal_lookup + .unwrap_or_else(|error| error.fallback_member(db)) + .place + { Place::Defined(DefinedPlace { origin, definedness, @@ -4694,21 +5068,25 @@ impl<'db> Type<'db> { let class_attr_plain = class_attr_plain .map_type(|ty| ty.bind_self_typevars(db, env, self_instance)); - let class_attr_fallback = Type::try_call_dunder_get_on_attribute( - db, - env, - class_attr_plain, - None, - receiver, - ) - .0; + let (class_attr_fallback, _, class_attr_error) = + Type::try_call_dunder_get_on_attribute( + db, + env, + class_attr_plain, + None, + receiver, + ); let result = Type::invoke_descriptor_protocol( db, env, key, receiver, - class_attr_fallback, + member_lookup_result( + db, + class_attr_fallback, + class_attr_error.map(MemberLookupErrorKind::DescriptorGet), + ), InstanceFallbackShadowsNonDataDescriptor::Yes, ); @@ -4736,7 +5114,7 @@ impl<'db> Type<'db> { if let Type::SubclassOf(subclass_of) = this && let SubclassOfInner::Dynamic(dynamic) = subclass_of.subclass_of() { - result.map_type(|ty| { + map_member_lookup_type(db, result, |ty| { if ty.is_dynamic() { ty } else { @@ -4764,7 +5142,7 @@ impl<'db> Type<'db> { bound_super .try_call_dunder_get_on_attribute(db, env, owner_attr) - .unwrap_or(owner_attr) + .unwrap_or_else(|| owner_attr.into()) } } } @@ -6134,19 +6512,20 @@ impl<'db> Type<'db> { /// Apply `__getattr__` / `__getattribute__` fallback to an attribute-lookup result. /// - /// If `result` is already always-defined, return it unchanged. Otherwise, fall back to calling - /// `__getattribute__` (and then `__getattr__`) on the meta-type of `self`. + /// A custom `__getattribute__` can intercept even an always-defined normal lookup result. + /// Otherwise, an undefined or possibly-undefined result falls back to `__getattribute__` and + /// then `__getattr__` on the meta-type of `self`. fn fallback_to_getattr( self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, name: &Name, - result: PlaceAndQualifiers<'db>, + result: MemberLookupResult<'db>, policy: MemberLookupPolicy, - ) -> PlaceAndQualifiers<'db> { + ) -> MemberLookupResult<'db> { let custom_getattr_result = || { if policy.no_getattr_lookup() { - return Place::Undefined.into(); + return MemberLookupResult::from(Place::Undefined); } self.try_call_dunder( @@ -6162,51 +6541,47 @@ impl<'db> Type<'db> { .into() }; - let custom_getattribute_result = || { - if "__getattribute__" == name.as_str() { - return Place::Undefined.into(); - } + let custom_getattribute = OnceCell::new(); + let custom_getattribute = || { + *custom_getattribute.get_or_init(|| { + if "__getattribute__" == name.as_str() { + return (MemberLookupResult::from(Place::Undefined), false); + } - // Skip `object.__getattribute__`, which is the default mechanism we - // already model via the normal attribute-lookup path. - self.try_call_dunder_with_policy( - db, - env, - "__getattribute__", - &mut CallArguments::positional([Type::string_literal(db, name)]), - TypeContext::default(), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, - ) - .map(|outcome| Place::bound(outcome.return_type(db, env))) - // TODO: Handle call errors here. - .unwrap_or_default() - .into() + // Skip `object.__getattribute__`, which is the default mechanism we + // already model via the normal attribute-lookup path. + match self.try_call_dunder_with_policy( + db, + env, + "__getattribute__", + &mut CallArguments::positional([Type::string_literal(db, name)]), + TypeContext::default(), + MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, + ) { + Ok(bindings) => (Place::bound(bindings.return_type(db, env)).into(), true), + Err( + CallDunderError::PossiblyUnbound { .. } | CallDunderError::CallError(..), + ) => (MemberLookupResult::from(Place::Undefined), true), + Err(CallDunderError::MethodNotAvailable) => { + (MemberLookupResult::from(Place::Undefined), false) + } + } + }) }; - match result { - member @ PlaceAndQualifiers { - place: - Place::Defined(DefinedPlace { - definedness: Definedness::AlwaysDefined, - .. - }), - qualifiers: _, - } => member, - member @ PlaceAndQualifiers { - place: - Place::Defined(DefinedPlace { - definedness: Definedness::PossiblyUndefined, - .. - }), - qualifiers: _, - } => member - .or_fall_back_to(db, env, custom_getattribute_result) - .or_fall_back_to(db, env, custom_getattr_result), - PlaceAndQualifiers { - place: Place::Undefined, - qualifiers: _, - } => custom_getattribute_result().or_fall_back_to(db, env, custom_getattr_result), - } + // A custom override runs before the descriptor and might return without invoking it. + let result = if matches!( + result.err().map(|error| error.kind(db)), + Some(MemberLookupErrorKind::DescriptorGet(_)) + ) && custom_getattribute().1 + { + Ok(result.unwrap_or_else(|error| error.fallback_member(db))) + } else { + result + }; + + let result = member_lookup_or_fall_back_to(db, env, result, || custom_getattribute().0); + member_lookup_or_fall_back_to(db, env, result, custom_getattr_result) } /// Flatten typevars in a union or intersection by resolving them to their upper bounds diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 584433ff51..0d442d16db 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -10,11 +10,13 @@ use crate::{ place::{Place, PlaceAndQualifiers}, types::{ BoundTypeVarInstance, ClassBase, ClassType, DivergentType, DynamicType, - IntersectionBuilder, KnownClass, MemberLookupPolicy, SpecialFormType, SubclassOfInner, - SubclassOfType, Type, TypeVarBoundOrConstraints, UnionBuilder, + IntersectionBuilder, KnownClass, MemberLookupErrorKind, MemberLookupPolicy, + MemberLookupResult, SpecialFormType, SubclassOfInner, SubclassOfType, Type, + TypeVarBoundOrConstraints, UnionBuilder, constraints::ConstraintSet, context::InferContext, diagnostic::{INVALID_SUPER_ARGUMENT, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS}, + member_lookup_result, relation::EquivalenceChecker, signatures::{Parameter, Parameters, Signature}, typevar::{TypeVarConstraints, TypeVarInstance}, @@ -940,9 +942,15 @@ impl<'db> BoundSuperType<'db> { db: &'db dyn Db, env: &ProgramEnvironment<'db>, attribute: PlaceAndQualifiers<'db>, - ) -> Option> { + ) -> Option> { let (instance, owner) = self.owner(db).descriptor_binding(db, env)?; - Some(Type::try_call_dunder_get_on_attribute(db, env, attribute, instance, owner).0) + let (member, _, descriptor_error) = + Type::try_call_dunder_get_on_attribute(db, env, attribute, instance, owner); + Some(member_lookup_result( + db, + member, + descriptor_error.map(MemberLookupErrorKind::DescriptorGet), + )) } /// Similar to `Type::find_name_in_mro_with_policy`, but performs lookup starting *after* the diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index e40e5d3cbc..763acacb62 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -315,6 +315,24 @@ impl<'db> CallError<'db> { self.1.return_type(db, env) } + /// Returns `Some(property)` if the call error was caused by an attempt to read a property + /// that has no getter, and `None` otherwise. + pub(crate) fn as_attempt_to_get_property_with_no_getter( + &self, + ) -> Option> { + if self.0 != CallErrorKind::BindingError { + return None; + } + self.1 + .iter_flat() + .flatten() + .flat_map(bind::Binding::errors) + .find_map(|error| match error { + BindingError::PropertyHasNoGetter(property) => Some(*property), + _ => None, + }) + } + /// Returns `Some(property)` if the call error was caused by an attempt to set a property /// that has no setter, and `None` otherwise. pub(crate) fn as_attempt_to_set_property_with_no_setter( diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 5e74ab6f35..e6633f2adf 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1712,7 +1712,7 @@ impl<'db> Bindings<'db> { } else { overload .errors - .push(BindingError::PropertyHasNoSetter(*property)); + .push(BindingError::PropertyHasNoGetter(*property)); overload.set_return_type(Type::Never); } } @@ -1730,9 +1730,9 @@ impl<'db> Bindings<'db> { overload.check_property_getter(db, env, getter, *instance, 0); } else { overload.set_return_type(Type::Never); - overload.errors.push(BindingError::InternalCallError( - "property has no getter", - )); + overload + .errors + .push(BindingError::PropertyHasNoGetter(property)); } } _ => {} @@ -7966,6 +7966,7 @@ pub(crate) enum BindingError<'db> { error: SpecializationError<'db>, argument_index: Option, }, + PropertyHasNoGetter(PropertyInstanceType<'db>), PropertyHasNoSetter(PropertyInstanceType<'db>), PropertyHasNoDeleter(PropertyInstanceType<'db>), PropertyGetterCallError(PropertyAccessorCallError<'db>), @@ -8096,6 +8097,7 @@ impl BindingError<'_> { | BindingError::InvalidDataclassArgument(..) | BindingError::MissingArguments { .. } | BindingError::UnmatchedOverload + | BindingError::PropertyHasNoGetter(..) | BindingError::PropertyHasNoSetter(..) | BindingError::PropertyHasNoDeleter(..) | BindingError::PropertyGetterCallError(..) @@ -8157,6 +8159,7 @@ impl<'db> BindingError<'db> { // Semantic errors: the overload matched, but the usage is invalid Self::InvalidDataclassApplication(_) | Self::InvalidDataclassArgument(_) + | Self::PropertyHasNoGetter(_) | Self::PropertyHasNoSetter(_) | Self::PropertyHasNoDeleter(_) | Self::PropertyGetterCallError(_) @@ -8628,6 +8631,18 @@ impl<'db> BindingError<'db> { } } + Self::PropertyHasNoGetter(_) => { + BindingError::InternalCallError("property has no getter").report_diagnostic( + context, + node, + callable_ty, + callable_description, + compound_diag, + matching_overload, + source_parameter_index_offset, + ); + } + Self::PropertyHasNoSetter(_) => { BindingError::InternalCallError("property has no setter").report_diagnostic( context, diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index 474f7ad68a..e36a3935af 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -1655,7 +1655,8 @@ impl<'db> StaticClassLiteral<'db> { if let Some(ref mut default_ty) = default_ty { *default_ty = default_ty .try_call_dunder_get(db, env, None, Type::from(self)) - .map(|(return_ty, _)| return_ty) + .unwrap_or_else(|error| Some(error.fallback())) + .map(|result| result.return_type) .unwrap_or_else(Type::unknown); } } diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 56fc74b772..c0b6d9d201 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -1739,6 +1739,62 @@ pub(super) fn report_invalid_attribute_assignment( error_context.attach_to(db, env, &mut diag); } +/// Reports an invalid implicit call to a descriptor's `__get__` method. +pub(super) fn report_bad_dunder_get_call<'db>( + context: &InferContext<'db, '_>, + failure: &CallError<'db>, + object_type: Type<'db>, + descriptor_type: Type<'db>, + target: &ast::ExprAttribute, +) { + let db = context.db(); + let env = &context.program_environment(); + let attribute = target.attr.as_str(); + if let Some(property) = failure.as_attempt_to_get_property_with_no_getter() { + let Some(builder) = context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target) else { + return; + }; + let object_type = object_type.display(db, env); + let mut diagnostic = builder.into_diagnostic(format_args!( + "Cannot read property `{attribute}` on object of type `{object_type}` because it has no getter", + )); + if let Some(file_range) = property + .setter(db) + .and_then(|setter| setter.definition(db, env)) + .or_else(|| { + property + .deleter(db) + .and_then(|deleter| deleter.definition(db, env)) + }) + .and_then(|definition| definition.focus_range(db)) + { + diagnostic.annotate(Annotation::secondary(Span::from(file_range)).message( + format_args!("Property `{object_type}.{attribute}` defined here with no getter"), + )); + diagnostic.set_primary_annotation_message(format_args!( + "Attempted access to `{object_type}.{attribute}` here" + )); + } + } else { + failure.report_diagnostics_with_override( + context, + target.into(), + &CallDiagnosticOverride { + lint: &INVALID_ATTRIBUTE_ACCESS, + message: format!( + "Invalid access to descriptor attribute `{attribute}` on type `{}`", + object_type.display(db, env), + ), + info: &format!( + "This access implicitly calls `__get__` on a descriptor of type `{}`", + descriptor_type.display(db, env), + ), + argument_ranges: &[target.range(), target.value.range(), target.value.range()], + }, + ); + } +} + pub(super) fn report_bad_dunder_set_call<'db>( context: &InferContext<'db, '_>, dunder_set_failure: &CallError<'db>, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6d72d6f46e..b8c5448e31 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -10317,9 +10317,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { assigned_type = Some(ty); } } - let fallback_place = value_type.member(db, env, &attr.id).map_type(|ty| { - self.narrow_expr_with_applicable_constraints(attribute, ty, &constraint_keys) - }); + let fallback_place = value_type + .try_member_lookup(db, env, &attr.id) + .unwrap_or_else(|error| { + error.report_diagnostic(&self.context, value_type, attribute, assigned_type); + error.fallback_member(db) + }) + .map_type(|ty| { + self.narrow_expr_with_applicable_constraints(attribute, ty, &constraint_keys) + }); let attr_name = &attr.id; let resolved_type = diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index df3aff88a9..8987a6f9f0 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -348,8 +348,9 @@ fn source_method_contract<'db>( return None; }; let ty = Type::FunctionLiteral(function) - .try_call_dunder_get(db, env, Some(receiver), receiver.to_meta_type(db, env))? - .0; + .try_call_dunder_get(db, env, Some(receiver), receiver.to_meta_type(db, env)) + .unwrap_or_else(|error| Some(error.fallback()))? + .return_type; Some((MethodDecorator::try_from_fn_type(db, function)?, ty)) } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index e1714a93f5..f2b533a374 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -2248,12 +2248,15 @@ fn descriptor_decorated_protocol_member<'db>( }; let receiver_ty = Type::instance(db, env, protocol); - let (read_ty, _) = descriptor_ty.try_call_dunder_get( - db, - env, - Some(receiver_ty), - receiver_ty.to_meta_type(db, env), - )?; + let read_ty = descriptor_ty + .try_call_dunder_get( + db, + env, + Some(receiver_ty), + receiver_ty.to_meta_type(db, env), + ) + .unwrap_or_else(|error| Some(error.fallback()))? + .return_type; let read = Some(ProtocolMemberType::with_definition(read_ty, definition)); let write = match descriptor_setter_domain(db, env, descriptor_ty, receiver_ty) { @@ -2505,6 +2508,7 @@ fn protocol_member_read_type<'db>( Place::Undefined.into(), InstanceFallbackShadowsNonDataDescriptor::No, ) + .unwrap_or_else(|error| error.fallback_member(db)) .place } else { receiver_ty.member(db, env, member.name).place From 22c7823c4e8bffcca97688d8438c9b567d6817d8 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 6 Aug 2026 14:03:53 +0100 Subject: [PATCH 316/390] [ty] Enable (but downrank) auto-import completion suggestions from stub-only modules (#27433) --- .../completion-evaluation-tasks.csv | 11 + crates/ty_completion_eval/src/main.rs | 1 + .../main.pyi | 3 + .../completion.toml | 2 + .../typing-only-auto-import-ranking/main.py | 9 + .../typing-only-auto-import-ranking/main.pyi | 4 + .../pyproject.toml | 5 + .../typing-only-auto-import-ranking/uv.lock | 8 + crates/ty_ide/src/all_symbols.rs | 22 +- crates/ty_ide/src/code_action.rs | 33 +++ crates/ty_ide/src/completion.rs | 207 ++++++++++++++++-- crates/ty_module_resolver/src/list.rs | 14 ++ crates/ty_module_resolver/src/module.rs | 13 ++ .../ty_python_semantic/src/semantic_model.rs | 13 -- crates/ty_server/tests/e2e/completions.rs | 202 ++++++++++++++++- ...n_existing_import_undefined_decorator.snap | 45 ++++ ...ions__code_action_undefined_decorator.snap | 45 ++++ ...code_action_undefined_reference_multi.snap | 45 ++++ ...tion_with_full_diagnostic_output_link.snap | 45 ++++ .../snapshots/e2e__notebook__auto_import.snap | 42 ++++ .../e2e__notebook__auto_import_docstring.snap | 42 ++++ ...2e__notebook__auto_import_from_future.snap | 42 ++++ .../e2e__notebook__auto_import_same_cell.snap | 42 ++++ crates/ty_vendored/build.rs | 1 + 24 files changed, 840 insertions(+), 56 deletions(-) create mode 100644 crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/main.pyi create mode 100644 crates/ty_completion_eval/truth/typing-only-auto-import-ranking/completion.toml create mode 100644 crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.py create mode 100644 crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.pyi create mode 100644 crates/ty_completion_eval/truth/typing-only-auto-import-ranking/pyproject.toml create mode 100644 crates/ty_completion_eval/truth/typing-only-auto-import-ranking/uv.lock diff --git a/crates/ty_completion_eval/completion-evaluation-tasks.csv b/crates/ty_completion_eval/completion-evaluation-tasks.csv index 83b14bc4d3..320fd6b7e5 100644 --- a/crates/ty_completion_eval/completion-evaluation-tasks.csv +++ b/crates/ty_completion_eval/completion-evaluation-tasks.csv @@ -21,6 +21,8 @@ import-deprioritizes-type_check_only,main.py,1,1 import-deprioritizes-type_check_only,main.py,2,1 import-deprioritizes-type_check_only,main.py,3,2 import-deprioritizes-type_check_only,main.py,4,3 +import-deprioritizes-type_check_only,main.pyi,0,1 +import-deprioritizes-type_check_only,main.pyi,1,1 import-keyword-completion,main.py,0,1 internal-typeshed-hidden,main.py,0,1 local-over-auto-import,main.py,0,1 @@ -46,3 +48,12 @@ typing-gets-priority,main.py,1,1 typing-gets-priority,main.py,2,1 typing-gets-priority,main.py,3,1 typing-gets-priority,main.py,4,1 +typing-only-auto-import-ranking,main.py,0,1 +typing-only-auto-import-ranking,main.py,1,1 +typing-only-auto-import-ranking,main.py,2,1 +typing-only-auto-import-ranking,main.py,3,2 +typing-only-auto-import-ranking,main.py,4,1 +typing-only-auto-import-ranking,main.py,5,1 +typing-only-auto-import-ranking,main.pyi,0,1 +typing-only-auto-import-ranking,main.pyi,1,1 +typing-only-auto-import-ranking,main.pyi,2,1 diff --git a/crates/ty_completion_eval/src/main.rs b/crates/ty_completion_eval/src/main.rs index 879b97dea9..c10ec73333 100644 --- a/crates/ty_completion_eval/src/main.rs +++ b/crates/ty_completion_eval/src/main.rs @@ -545,6 +545,7 @@ fn copy_project(src_dir: &SystemPath, dst_dir: &SystemPath) -> anyhow::Result +from module import unique_prefix_ diff --git a/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/completion.toml b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/completion.toml new file mode 100644 index 0000000000..cbd5805f07 --- /dev/null +++ b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/completion.toml @@ -0,0 +1,2 @@ +[settings] +auto-import = true diff --git a/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.py b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.py new file mode 100644 index 0000000000..d2400e625d --- /dev/null +++ b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.py @@ -0,0 +1,9 @@ +# Runtime symbols outrank alternatives from typing-only modules in Python files. +deprecated +NoneTy +Not + +# Typing-only symbols are included in auto-import suggestions. +static_ass +is_equiv +TypedDictFall diff --git a/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.pyi b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.pyi new file mode 100644 index 0000000000..1ae2ffaf50 --- /dev/null +++ b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.pyi @@ -0,0 +1,4 @@ +# Typing-only symbols retain their usual ranking in stub files. +deprecated +NoneTy +static_ass diff --git a/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/pyproject.toml b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/pyproject.toml new file mode 100644 index 0000000000..cd277d8097 --- /dev/null +++ b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "test" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [] diff --git a/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/uv.lock b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/uv.lock new file mode 100644 index 0000000000..a4937d10d3 --- /dev/null +++ b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "test" +version = "0.1.0" +source = { virtual = "." } diff --git a/crates/ty_ide/src/all_symbols.rs b/crates/ty_ide/src/all_symbols.rs index a9e64279f2..56d8c7269d 100644 --- a/crates/ty_ide/src/all_symbols.rs +++ b/crates/ty_ide/src/all_symbols.rs @@ -1,9 +1,7 @@ use compact_str::CompactString; use rayon::prelude::*; use ruff_db::files::File; -use ty_module_resolver::{ - ImportingFile, Module, ModuleName, all_modules, resolve_real_shadowable_module, -}; +use ty_module_resolver::{Module, all_modules}; use ty_project::{Db, parallel::ParallelIteratorExt}; use ty_python_core::ProgramFile; @@ -29,12 +27,8 @@ pub fn all_symbols<'db>( let all_symbols_span = tracing::debug_span!("all_symbols"); let _span = all_symbols_span.enter(); - let typing_extensions = ModuleName::new_static("typing_extensions").unwrap(); let program = importing_from.program(db); let resolver_environment = importing_from.resolver_environment(db); - let importing_file = ImportingFile::File(importing_from.file(db), resolver_environment); - let is_typing_extensions_available = importing_from.file(db).is_stub(db) - || resolve_real_shadowable_module(db, importing_file, &typing_extensions).is_some(); let results = all_modules(db, resolver_environment) .into_par_iter() @@ -49,15 +43,11 @@ pub fn all_symbols<'db>( // namespace packages in auto-import anyway.) let is_non_first_party = module.search_path(db).is_none_or(|sp| !sp.is_first_party()); - // Filter out non-first-party modules that are conventionally - // regarded as private or tests. - if is_non_first_party && (name.is_private() || name.is_test_module()) { - return Vec::new(); - } - - // TODO: also make it available in `TYPE_CHECKING` blocks - // (we'd need https://github.com/astral-sh/ty/issues/1553 to do this well) - if !is_typing_extensions_available && name == &typing_extensions { + // Filter out non-first-party test and private modules, while retaining private + // typeshed packages that are useful when writing type annotations. + if is_non_first_party + && (name.is_test_module() || name.is_private() && !module.is_type_check_only(db)) + { return Vec::new(); } diff --git a/crates/ty_ide/src/code_action.rs b/crates/ty_ide/src/code_action.rs index 67fd68257c..e6ed2b5ed0 100644 --- a/crates/ty_ide/src/code_action.rs +++ b/crates/ty_ide/src/code_action.rs @@ -664,6 +664,17 @@ mod tests { 2 | | + info[code-action]: import typing_extensions.reveal_type + --> main.py:2:1 + | + 2 | reveal_type(1) + | ^^^^^^^^^^^ + help: This is a preferred code action + | + 1 + from typing_extensions import reveal_type + 2 | + | + info[code-action]: Ignore 'undefined-reveal' for this line --> main.py:2:1 | @@ -698,6 +709,17 @@ mod tests { 2 | | + info[code-action]: import typing_extensions.deprecated + --> main.py:2:2 + | + 2 | @deprecated("do not use") + | ^^^^^^^^^^ + help: This is a preferred code action + | + 1 + from typing_extensions import deprecated + 2 | + | + info[code-action]: Ignore 'unresolved-reference' for this line --> main.py:2:2 | @@ -735,6 +757,17 @@ mod tests { 2 | | + info[code-action]: import typing_extensions.deprecated + --> main.py:4:2 + | + 4 | @deprecated("do not use") + | ^^^^^^^^^^ + help: This is a preferred code action + | + 1 + from typing_extensions import deprecated + 2 | + | + info[code-action]: qualify warnings.deprecated --> main.py:4:2 | diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index f4b196f348..37e61a3586 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -10,12 +10,14 @@ use ruff_python_ast::find_node::{CoveringNode, covering_node}; use ruff_python_ast::name::{Name, UnqualifiedName}; use ruff_python_ast::str::Quote; use ruff_python_ast::token::{Token, TokenKind, Tokens}; -use ruff_python_ast::{self as ast, AnyNodeRef}; +use ruff_python_ast::{self as ast, AnyNodeRef, PySourceType}; use ruff_python_codegen::Stylist; use ruff_python_literal::escape::{Escape, UnicodeEscape}; use ruff_text_size::{Ranged, TextRange, TextSize}; use rustc_hash::FxHashSet; -use ty_module_resolver::{ImportingFile, KnownModule, Module, ModuleName}; +use ty_module_resolver::{ + ImportingFile, KnownModule, Module, ModuleName, resolve_real_shadowable_module, +}; use ty_python_core::ProgramFile; use ty_python_semantic::HasType; use ty_python_semantic::types::{SpecialFormType, UnionType}; @@ -523,7 +525,12 @@ impl<'db> CompletionBuilder<'db> { let kind = self .kind .or_else(|| self.ty.and_then(|ty| completion_kind_from_type(db, ty))); - let relevance = Relevance::new(collection_context, query, &self); + let relevance = Relevance::new( + collection_context, + query, + &self, + program_file.file(db).source_type(db), + ); let (label, insert, insert_text_format, command) = if collection_context.should_complete_callable_parentheses(kind) { let label = self.insert.unwrap_or_else(|| self.name.clone()); @@ -1767,7 +1774,7 @@ struct Relevance { /// the user's project. is_module: Sort, /// Sorts based on whether this symbol is only available during - /// type checking and not at runtime. + /// type checking and not at runtime. This does not lower its rank in a stub file. type_check_only: Sort, /// Deprecated symbols appear lower in the completion result. deprecated: Sort, @@ -1788,7 +1795,12 @@ impl Relevance { /// /// A smaller rank means the completion should appear higher in the /// results shown to end users. - fn new(_ctx: &CollectionContext, query: &UserQuery, c: &CompletionBuilder) -> Relevance { + fn new( + _ctx: &CollectionContext, + query: &UserQuery, + c: &CompletionBuilder, + source_type: PySourceType, + ) -> Relevance { Relevance { definitively_usable: if c.is_context_specific { Sort::Higher @@ -1825,7 +1837,7 @@ impl Relevance { } else { Sort::Even }, - type_check_only: if c.is_type_check_only { + type_check_only: if c.is_type_check_only && !source_type.is_stub() { Sort::Lower } else { Sort::Even @@ -1956,6 +1968,32 @@ impl ModuleDependencyKind { } } +/// Returns whether importing this module would require a typing-only runtime context. +fn is_type_check_only_module<'db>( + db: &'db dyn Db, + importing_from: ProgramFile<'db>, + module: Module<'db>, +) -> bool { + if !module.is_type_check_only(db) { + return false; + } + + if module.name(db).first_component() != "typing_extensions" { + return true; + } + + // typeshed bundles `typing_extensions` with its standard-library stubs even + // though the actual module is a third-party package. Since the bundled stub + // takes precedence during module resolution, look past it to check whether a + // corresponding runtime module is also available from the project or site-packages. + let importing_file = ImportingFile::File( + importing_from.file(db), + importing_from.resolver_environment(db), + ); + resolve_real_shadowable_module(db, importing_file, &KnownModule::TypingExtensions.name()) + .is_none() +} + /// An instruction to indicate an ordering preference. #[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)] enum Sort { @@ -2338,6 +2376,7 @@ fn add_unimported_completions<'db>( .module_name(module_name) .import(import_action.import().cloned()) .deprecated(symbol.deprecated()) + .type_check_only(is_type_check_only_module(db, file, symbol.module())) .module_dependency_kind(ModuleDependencyKind::from_module(db, symbol.module())), ); } @@ -3085,7 +3124,15 @@ fn add_import_completions_impl<'db>( let env = ProgramEnvironment::from_file(completions.program_file); for semantic in semantic_completions { let module_dependency_kind = module_dependency_kind(&semantic); + let is_from_type_check_only_module = matches!( + semantic.ty, + Some(Type::ModuleLiteral(module)) + if is_type_check_only_module(db, completions.program_file, module.module(db)) + ); let mut builder = CompletionBuilder::from_semantic_completion(db, &env, semantic); + if is_from_type_check_only_module { + builder = builder.type_check_only(true); + } if let Some(module_dependency_kind) = module_dependency_kind { builder = builder.module_dependency_kind(module_dependency_kind); } @@ -9406,7 +9453,10 @@ if foo: #[test] fn from_import_no_space_not_suggests_import() { let builder = completion_test_builder("from typing"); - assert_snapshot!(builder.build().snapshot(), @"typing"); + assert_snapshot!(builder.build().snapshot(), @" + typing + typing_extensions + "); } #[test] @@ -9617,19 +9667,111 @@ from .imp } #[test] - fn typing_extensions_excluded_from_import() { + fn bundled_typing_extensions_module_completion() { let builder = completion_test_builder("from typing").module_names(); - assert_snapshot!(builder.build().snapshot(), @"typing :: "); + let completions = builder.build(); + assert!(completions.completions().iter().any(|completion| { + completion.name == "typing_extensions" && completion.is_type_check_only + })); + assert_snapshot!(completions.snapshot(), @" + typing :: + typing_extensions :: + "); } #[test] - fn typing_extensions_excluded_from_auto_import() { - let builder = completion_test_builder("deprecated").module_names(); - assert_snapshot!(builder.build().snapshot(), @"deprecated :: warnings"); + fn bundled_ty_extensions_module_completion() { + let builder = completion_test_builder("from ty_ex") + .module_names() + .filter(|completion| completion.name == "ty_extensions"); + let completions = builder.build(); + assert!( + completions + .completions() + .iter() + .all(|completion| completion.is_type_check_only) + ); + assert_snapshot!(completions.snapshot(), @"ty_extensions :: "); } #[test] - fn typing_extensions_included_from_import() { + fn bundled_typeshed_module_completion() { + let builder = completion_test_builder("from _type") + .module_names() + .filter(|completion| completion.name == "_typeshed"); + let completions = builder.build(); + assert!( + completions + .completions() + .iter() + .all(|completion| completion.is_type_check_only) + ); + assert_snapshot!(completions.snapshot(), @"_typeshed :: "); + } + + #[test] + fn runtime_ty_extensions_auto_import_is_not_type_check_only() { + let builder = CursorTest::builder() + .source("ty_extensions.py", "static_assert = 1") + .source("main.py", "static_ass") + .completion_test_builder() + .module_names() + .filter(|completion| completion.name == "static_assert"); + let completions = builder.build(); + assert!( + completions + .completions() + .iter() + .all(|completion| !completion.is_type_check_only) + ); + assert_snapshot!(completions.snapshot(), @"static_assert :: ty_extensions"); + } + + #[test] + fn ty_extensions_pydantic_auto_import_generates_import_edit() { + let builder = completion_test_builder("LaxDa") + .module_names() + .imports() + .filter(|completion| completion.name == "LaxDate"); + assert_snapshot!(builder.build().snapshot(), @"LaxDate :: ty_extensions.pydantic :: from ty_extensions.pydantic import LaxDate"); + } + + #[test] + fn ty_extensions_auto_import_is_type_check_only_in_stub() { + let builder = CursorTest::builder() + .source("main.pyi", "static_ass") + .completion_test_builder() + .module_names() + .filter(|completion| completion.name == "static_assert"); + let completions = builder.build(); + assert!( + completions + .completions() + .iter() + .all(|completion| completion.is_type_check_only) + ); + assert_snapshot!(completions.snapshot(), @"static_assert :: ty_extensions"); + } + + #[test] + fn typeshed_auto_import_is_type_check_only_in_stub() { + let builder = CursorTest::builder() + .source("main.pyi", "TypedDictFall") + .completion_test_builder() + .module_names() + .filter(|completion| completion.name == "TypedDictFallback"); + let completions = builder.build(); + assert!( + completions + .completions() + .iter() + .all(|completion| completion.is_type_check_only) + ); + assert_snapshot!(completions.snapshot(), @"TypedDictFallback :: _typeshed._type_checker_internals"); + } + + #[test] + fn runtime_typing_extensions_module_completion() { let builder = CursorTest::builder() .source("typing_extensions.py", "deprecated = 1") .source("foo.py", "from typing") @@ -9642,37 +9784,54 @@ from .imp } #[test] - fn typing_extensions_included_from_auto_import() { + fn runtime_typing_extensions_auto_import_is_not_type_check_only() { let builder = CursorTest::builder() .source("typing_extensions.py", "deprecated = 1") .source("foo.py", "deprecated") .completion_test_builder() .module_names(); - assert_snapshot!(builder.build().snapshot(), @" + let completions = builder.build(); + assert!( + completions.completions().iter().any(|completion| { + completion.module_name.map(ModuleName::as_str) == Some("typing_extensions") + && !completion.is_type_check_only + }), + "runtime `typing_extensions` should not be downranked", + ); + assert_snapshot!(completions.snapshot(), @" deprecated :: typing_extensions deprecated :: warnings "); } #[test] - fn typing_extensions_included_from_import_in_stub() { + fn typing_extensions_module_completion_is_type_check_only_in_stub() { let builder = CursorTest::builder() .source("foo.pyi", "from typing") .completion_test_builder() .module_names(); - assert_snapshot!(builder.build().snapshot(), @" + let completions = builder.build(); + assert!(completions.completions().iter().any(|completion| { + completion.name == "typing_extensions" && completion.is_type_check_only + })); + assert_snapshot!(completions.snapshot(), @" typing :: typing_extensions :: "); } #[test] - fn typing_extensions_included_from_auto_import_in_stub() { + fn typing_extensions_auto_import_is_type_check_only_in_stub() { let builder = CursorTest::builder() .source("foo.pyi", "deprecated") .completion_test_builder() .module_names(); - assert_snapshot!(builder.build().snapshot(), @" + let completions = builder.build(); + assert!(completions.completions().iter().any(|completion| { + completion.module_name.map(ModuleName::as_str) == Some("typing_extensions") + && completion.is_type_check_only + })); + assert_snapshot!(completions.snapshot(), @" deprecated :: typing_extensions deprecated :: warnings "); @@ -10495,15 +10654,17 @@ import typing from typing import Callable TypedDi ", - ); + ) + .imports() + .filter(|completion| matches!(completion.name.as_str(), "TypedDict" | "is_typeddict")); assert_snapshot!( - builder.imports().build().snapshot(), + builder.build().snapshot(), @" TypedDict :: , TypedDict is_typeddict :: , is_typeddict - _FilterConfigurationTypedDict :: from logging.config import _FilterConfigurationTypedDict + TypedDict :: from typing_extensions import TypedDict - _FormatterConfigurationTypedDict :: from logging.config import _FormatterConfigurationTypedDict + is_typeddict :: from typing_extensions import is_typeddict ", ); } diff --git a/crates/ty_module_resolver/src/list.rs b/crates/ty_module_resolver/src/list.rs index b8080af1e1..77658fd583 100644 --- a/crates/ty_module_resolver/src/list.rs +++ b/crates/ty_module_resolver/src/list.rs @@ -625,6 +625,20 @@ mod tests { ); } + #[test] + fn ty_extensions_vendored() { + let TestCase { db, .. } = TestCaseBuilder::new().with_vendored_typeshed().build(); + + insta::assert_debug_snapshot!( + list_snapshot_filter(&db, |module| module.name(&db).as_str() == "ty_extensions"), + @r#" + [ + Module::File("ty_extensions", "std-vendored", "stdlib/ty_extensions/__init__.pyi", Package, Some(TyExtensions)), + ] + "#, + ); + } + #[test] fn builtins_custom() { const TYPESHED: MockedTypeshed = MockedTypeshed { diff --git a/crates/ty_module_resolver/src/module.rs b/crates/ty_module_resolver/src/module.rs index 0820bc6b07..ec27fe4bc9 100644 --- a/crates/ty_module_resolver/src/module.rs +++ b/crates/ty_module_resolver/src/module.rs @@ -109,6 +109,19 @@ impl<'db> Module<'db> { } } + /// Returns whether this module resolves to a bundled typing-only stub. + /// + /// A project or installed module with the same name may still exist on a + /// lower-priority search path and be available at runtime. + pub fn is_type_check_only(self, db: &'db dyn Database) -> bool { + self.search_path(db) + .is_some_and(SearchPath::is_standard_library) + && matches!( + self.name(db).first_component(), + "_typeshed" | "typing_extensions" | "ty_extensions" + ) + } + /// Determine whether this module is a single-file module or a package pub fn kind(self, db: &'db dyn Database) -> ModuleKind { match self { diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index a203fc8e66..c6feb53ec2 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -12,7 +12,6 @@ use ruff_text_size::Ranged; use rustc_hash::FxHashMap; use ty_module_resolver::{ ImportingFile, KnownModule, Module, ModuleName, list_modules, resolve_module, - resolve_real_shadowable_module, }; use crate::Db; @@ -146,22 +145,10 @@ impl<'db> SemanticModel<'db> { /// Returns completions for symbols available in a `import ` context. pub fn import_completions(&self) -> Vec> { - let typing_extensions = ModuleName::new_static("typing_extensions").unwrap(); - let file = self.file(); let resolver_environment = self.program_environment().resolver_environment(self.db); - let is_typing_extensions_available = file.is_stub(self.db) - || resolve_real_shadowable_module( - self.db, - ImportingFile::File(file, resolver_environment), - &typing_extensions, - ) - .is_some(); list_modules(self.db, resolver_environment) .iter() .copied() - .filter(|module| { - is_typing_extensions_available || module.name(self.db) != &typing_extensions - }) .map(|module| { let builtin = module.is_known(self.db, KnownModule::Builtins); let ty = Type::module_literal(self.db, self.program_file(), module); diff --git a/crates/ty_server/tests/e2e/completions.rs b/crates/ty_server/tests/e2e/completions.rs index 23e21bd506..eb074a9eea 100644 --- a/crates/ty_server/tests/e2e/completions.rs +++ b/crates/ty_server/tests/e2e/completions.rs @@ -273,6 +273,32 @@ is_typedd "title": "Trigger parameter hints", "command": "ty.triggerParameterHints" } + }, + { + "label": "is_typeddict (import typing_extensions)", + "kind": 3, + "sortText": "1", + "insertText": "is_typeddict($0)", + "insertTextFormat": 2, + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import is_typeddict\n" + } + ], + "command": { + "title": "Trigger parameter hints", + "command": "ty.triggerParameterHints" + } } ] "#); @@ -319,10 +345,73 @@ TypedDi "sortText": "1", "insertText": "typing.is_typeddict" }, + { + "label": "TypedDict (import typing_extensions)", + "kind": 6, + "sortText": "2", + "insertText": "TypedDict", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import TypedDict\n" + } + ] + }, + { + "label": "TypedDictFallback (import _typeshed._type_checker_internals)", + "kind": 7, + "sortText": "3", + "insertText": "TypedDictFallback", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from _typeshed._type_checker_internals import TypedDictFallback\n" + } + ] + }, + { + "label": "is_typeddict (import typing_extensions)", + "kind": 3, + "sortText": "4", + "insertText": "is_typeddict", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import is_typeddict\n" + } + ] + }, { "label": "_FilterConfigurationTypedDict (import logging.config)", "kind": 7, - "sortText": "2", + "sortText": "5", "insertText": "_FilterConfigurationTypedDict", "additionalTextEdits": [ { @@ -343,7 +432,7 @@ TypedDi { "label": "_FormatterConfigurationTypedDict (import logging.config)", "kind": 6, - "sortText": "3", + "sortText": "6", "insertText": "_FormatterConfigurationTypedDict", "additionalTextEdits": [ { @@ -360,6 +449,27 @@ TypedDi "newText": "from logging.config import _FormatterConfigurationTypedDict\n" } ] + }, + { + "label": "_typeshed.dbapi (import _typeshed.dbapi)", + "kind": 9, + "sortText": "7", + "insertText": "_typeshed.dbapi", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "import _typeshed.dbapi\n" + } + ] } ] "#); @@ -433,10 +543,73 @@ TypedDi } ] }, + { + "label": "TypedDict (import typing_extensions)", + "kind": 6, + "sortText": "2", + "insertText": "TypedDict", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import TypedDict\n" + } + ] + }, + { + "label": "TypedDictFallback (import _typeshed._type_checker_internals)", + "kind": 7, + "sortText": "3", + "insertText": "TypedDictFallback", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from _typeshed._type_checker_internals import TypedDictFallback\n" + } + ] + }, + { + "label": "is_typeddict (import typing_extensions)", + "kind": 3, + "sortText": "4", + "insertText": "is_typeddict", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import is_typeddict\n" + } + ] + }, { "label": "_FilterConfigurationTypedDict (import logging.config)", "kind": 7, - "sortText": "2", + "sortText": "5", "insertText": "_FilterConfigurationTypedDict", "additionalTextEdits": [ { @@ -457,7 +630,7 @@ TypedDi { "label": "_FormatterConfigurationTypedDict (import logging.config)", "kind": 6, - "sortText": "3", + "sortText": "6", "insertText": "_FormatterConfigurationTypedDict", "additionalTextEdits": [ { @@ -474,6 +647,27 @@ TypedDi "newText": "from logging.config import _FormatterConfigurationTypedDict\n" } ] + }, + { + "label": "_typeshed.dbapi (import _typeshed.dbapi)", + "kind": 9, + "sortText": "7", + "insertText": "_typeshed.dbapi", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "import _typeshed.dbapi\n" + } + ] } ] "#); diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_existing_import_undefined_decorator.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_existing_import_undefined_decorator.snap index fa5ac20dc2..105a8654bc 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_existing_import_undefined_decorator.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_existing_import_undefined_decorator.snap @@ -48,6 +48,51 @@ expression: code_actions } } }, + { + "title": "import typing_extensions.deprecated", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 3, + "character": 1 + }, + "end": { + "line": 3, + "character": 11 + } + }, + "severity": 1, + "code": "unresolved-reference", + "codeDescription": { + "href": "https://ty.dev/rules#unresolved-reference" + }, + "source": "ty", + "message": "Name `deprecated` used when not defined" + } + ], + "isPreferred": true, + "edit": { + "changes": { + "file:///src/foo.py": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import deprecated\n" + } + ] + } + } + }, { "title": "qualify warnings.deprecated", "kind": "quickfix", diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_decorator.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_decorator.snap index f4035122a6..2111a88479 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_decorator.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_decorator.snap @@ -48,6 +48,51 @@ expression: code_actions } } }, + { + "title": "import typing_extensions.deprecated", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 1, + "character": 1 + }, + "end": { + "line": 1, + "character": 11 + } + }, + "severity": 1, + "code": "unresolved-reference", + "codeDescription": { + "href": "https://ty.dev/rules#unresolved-reference" + }, + "source": "ty", + "message": "Name `deprecated` used when not defined" + } + ], + "isPreferred": true, + "edit": { + "changes": { + "file:///src/foo.py": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import deprecated\n" + } + ] + } + } + }, { "title": "Ignore 'unresolved-reference' for this line", "kind": "quickfix", diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_reference_multi.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_reference_multi.snap index 0d5c74cb85..ff9357c46e 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_reference_multi.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_undefined_reference_multi.snap @@ -48,6 +48,51 @@ expression: code_actions } } }, + { + "title": "import typing_extensions.Literal", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 10 + } + }, + "severity": 1, + "code": "unresolved-reference", + "codeDescription": { + "href": "https://ty.dev/rules#unresolved-reference" + }, + "source": "ty", + "message": "Name `Literal` used when not defined" + } + ], + "isPreferred": true, + "edit": { + "changes": { + "file:///src/foo.py": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import Literal\n" + } + ] + } + } + }, { "title": "Ignore 'unresolved-reference' for this line", "kind": "quickfix", diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_with_full_diagnostic_output_link.snap b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_with_full_diagnostic_output_link.snap index 7378beac5e..11cea2b4d8 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_with_full_diagnostic_output_link.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__code_actions__code_action_with_full_diagnostic_output_link.snap @@ -48,6 +48,51 @@ expression: code_actions } } }, + { + "title": "import typing_extensions.Literal", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 0, + "character": 3 + }, + "end": { + "line": 0, + "character": 10 + } + }, + "severity": 1, + "code": "Click for full diagnostic", + "codeDescription": { + "href": "https://ty.dev/rules#unresolved-reference" + }, + "source": "ty", + "message": "Name `Literal` used when not defined" + } + ], + "isPreferred": true, + "edit": { + "changes": { + "file:///src/foo.py": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import Literal\n" + } + ] + } + } + }, { "title": "Ignore 'unresolved-reference' for this line", "kind": "quickfix", diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import.snap b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import.snap index a9740b7b97..0c246de7f8 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import.snap @@ -44,5 +44,47 @@ expression: completions "newText": "from typing import LiteralString\n" } ] + }, + { + "label": "Literal (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "Literal", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "from typing_extensions import Literal\n" + } + ] + }, + { + "label": "LiteralString (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "LiteralString", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "from typing_extensions import LiteralString\n" + } + ] } ] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_docstring.snap b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_docstring.snap index a9740b7b97..0c246de7f8 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_docstring.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_docstring.snap @@ -44,5 +44,47 @@ expression: completions "newText": "from typing import LiteralString\n" } ] + }, + { + "label": "Literal (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "Literal", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "from typing_extensions import Literal\n" + } + ] + }, + { + "label": "LiteralString (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "LiteralString", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "from typing_extensions import LiteralString\n" + } + ] } ] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_from_future.snap b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_from_future.snap index a9740b7b97..0c246de7f8 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_from_future.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_from_future.snap @@ -44,5 +44,47 @@ expression: completions "newText": "from typing import LiteralString\n" } ] + }, + { + "label": "Literal (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "Literal", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "from typing_extensions import Literal\n" + } + ] + }, + { + "label": "LiteralString (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "LiteralString", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 1, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "newText": "from typing_extensions import LiteralString\n" + } + ] } ] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_same_cell.snap b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_same_cell.snap index 713c26841e..a3a9b5d385 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_same_cell.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__notebook__auto_import_same_cell.snap @@ -44,5 +44,47 @@ expression: completions "newText": ", LiteralString" } ] + }, + { + "label": "Literal (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "Literal", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import Literal\n" + } + ] + }, + { + "label": "LiteralString (import typing_extensions)", + "kind": 6, + "sortText": "[RANKING]", + "insertText": "LiteralString", + "additionalTextEdits": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 0 + } + }, + "newText": "from typing_extensions import LiteralString\n" + } + ] } ] diff --git a/crates/ty_vendored/build.rs b/crates/ty_vendored/build.rs index 1dec35d2fd..e1ad1ece5a 100644 --- a/crates/ty_vendored/build.rs +++ b/crates/ty_vendored/build.rs @@ -84,6 +84,7 @@ fn write_zipped_typeshed_to(writer: File) -> ZipResult { } // Patch typeshed and add the stubs for the `ty_extensions` package. + zip.add_directory("stdlib/ty_extensions/", options)?; for (source, destination) in TY_EXTENSIONS_STUBS { println!("adding file {source} as {destination} ..."); zip.start_file(destination, options)?; From 2fc445f0053f4ec27c717fae0de3671d73c103be Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 6 Aug 2026 09:05:33 -0400 Subject: [PATCH 317/390] [ty] Diagnose invalid __getattr__ calls (#27502) ## Summary Previously, an invalid `__getattr__` method caused us to report that the requested attribute was missing, even though Python actually invokes the method and raises `TypeError`: ```py class Example: def __getattr__(self) -> str: return "fallback" Example().missing # error: [invalid-attribute-access] ``` We now propagate failed implicit `__getattr__` calls through the generalized member-lookup error introduced in #27400, so the diagnostic identifies the invalid call and preserves the method's return type for error recovery. This applies to instance and metaclass attribute lookup, including incompatible attribute-name parameters. Literal-restricted `__getattr__` methods continue to treat unsupported names as missing attributes, and already-defined attributes still bypass the fallback. Closes https://github.com/astral-sh/ty/issues/135. --- .../resources/mdtest/attributes.md | 65 ++++++++++++++++++- crates/ty_python_semantic/src/types.rs | 52 ++++++++++++--- .../src/types/diagnostic.rs | 35 ++++++++++ 3 files changed, 141 insertions(+), 11 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 03e5a1dfca..b488a2f05f 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2706,6 +2706,51 @@ accessed on the class itself: CustomGetAttr.whatever ``` +### Invalid `__getattr__` calls + +If `__getattr__` cannot accept the attribute name that Python passes to it, the access is invalid. +The method's return type remains available for error recovery, while defined attributes do not +invoke the fallback. + +```py +class InvalidGetAttr: + defined: bool = True + + def __getattr__(self) -> str: + return "fallback" + +InvalidGetAttr().missing # snapshot: invalid-attribute-access + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type `InvalidGetAttr`" +reveal_type(InvalidGetAttr().missing) # revealed: str +reveal_type(InvalidGetAttr().defined) # revealed: bool +``` + +```snapshot +error[invalid-attribute-access]: Invalid access to attribute `missing` on type `InvalidGetAttr` + --> src/mdtest_snippet.py:7:1 + | +7 | InvalidGetAttr().missing # snapshot: invalid-attribute-access + | ^^^^^^^^^^^^^^^^^^^^^^^^ Too many positional arguments to bound method `InvalidGetAttr.__getattr__`: expected 1, got 2 +info: This access implicitly calls `__getattr__` +info: Method signature here + --> src/mdtest_snippet.py:4:9 + | +4 | def __getattr__(self) -> str: + | ^^^^^^^^^^^^^^^^^^^^^^^^ +``` + +An incompatible type for the attribute name is also an invalid fallback call. + +```py +class InvalidNameType: + def __getattr__(self, name: int) -> bytes: + return b"fallback" + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type `InvalidNameType`" +reveal_type(InvalidNameType().missing) # revealed: bytes +``` + ### Type of the `name` parameter If the `name` parameter of the `__getattr__` method is annotated with a (union of) literal type(s), @@ -2724,8 +2769,8 @@ reveal_type(date.day) # revealed: int reveal_type(date.month) # revealed: int reveal_type(date.year) # revealed: int -# error: [unresolved-attribute] "Object of type `Date` has no attribute `century`" -reveal_type(date.century) # revealed: Unknown +# error: [invalid-attribute-access] "Invalid access to attribute `century` on type `Date`" +reveal_type(date.century) # revealed: int ``` ### `argparse.Namespace` @@ -2817,6 +2862,22 @@ class Foo(metaclass=Meta): ... reveal_type(Foo.whatever) # revealed: int ``` +### Invalid `__getattr__` calls + +Invalid metaclass `__getattr__` calls are reported on class attribute access while preserving the +method's return type for error recovery. + +```py +class Meta(type): + def __getattr__(cls) -> int: + return 1 + +class Foo(metaclass=Meta): ... + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type ``" +reveal_type(Foo.missing) # revealed: int +``` + ### Class attributes take precedence If the class defines the attribute directly, it takes precedence over the metaclass `__getattr__`: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 33379da50c..9cd5bf746e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -66,7 +66,9 @@ pub(crate) use crate::types::callable::{CallableType, CallableTypes}; pub(crate) use crate::types::class_base::ClassBase; use crate::types::constraints::ConstraintSetBuilder; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; -use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, report_bad_dunder_get_call}; +use crate::types::diagnostic::{ + INVALID_AWAIT, INVALID_TYPE_FORM, report_bad_dunder_get_call, report_bad_dunder_getattr_call, +}; pub use crate::types::display::{DisplaySettings, TypeDetail, TypeDisplayDetails}; pub(crate) use crate::types::enums::{EnumClassLiteral, EnumComplementType, enum_metadata}; pub(crate) use crate::types::equality::{ComparisonSoundnessPolicy, equality_truthiness}; @@ -609,6 +611,14 @@ fn descriptor_get_result<'db>( #[derive(Clone, Debug, Copy, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)] enum MemberLookupErrorKind<'db> { DescriptorGet(DescriptorGetCallContext<'db>), + + /// An invalid fallback call, represented by its receiver and requested attribute name. + /// + /// Retaining only these arguments avoids storing call bindings in cached lookup results. + GetAttr { + receiver: Type<'db>, + name: Type<'db>, + }, } /// A failed member lookup together with the member used to recover from the error. @@ -652,7 +662,21 @@ impl<'db> MemberLookupError<'db> { target, ); } - MemberLookupErrorKind::DescriptorGet(_) => {} + MemberLookupErrorKind::GetAttr { receiver, name } + if assigned_type.is_none() + && let Err(CallDunderError::CallError(kind, bindings, _)) = receiver + .try_call_dunder( + db, + env, + "__getattr__", + CallArguments::positional([name]), + TypeContext::default(), + ) => + { + let failure = CallError(kind, bindings); + report_bad_dunder_getattr_call(context, &failure, object_type, target); + } + MemberLookupErrorKind::DescriptorGet(_) | MemberLookupErrorKind::GetAttr { .. } => {} } } } @@ -6528,17 +6552,27 @@ impl<'db> Type<'db> { return MemberLookupResult::from(Place::Undefined); } - self.try_call_dunder( + let name_type = Type::string_literal(db, name); + match self.try_call_dunder( db, env, "__getattr__", - CallArguments::positional([Type::string_literal(db, name)]), + CallArguments::positional([name_type]), TypeContext::default(), - ) - .map(|outcome| Place::bound(outcome.return_type(db, env))) - // TODO: Handle call errors here. - .unwrap_or_default() - .into() + ) { + Ok(outcome) => Place::bound(outcome.return_type(db, env)).into(), + Err(CallDunderError::CallError(_, bindings, _)) => member_lookup_result( + db, + Place::bound(bindings.return_type(db, env)).into(), + Some(MemberLookupErrorKind::GetAttr { + receiver: self, + name: name_type, + }), + ), + Err( + CallDunderError::PossiblyUnbound { .. } | CallDunderError::MethodNotAvailable, + ) => Place::Undefined.into(), + } }; let custom_getattribute = OnceCell::new(); diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index c0b6d9d201..53cf5c97de 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -1795,6 +1795,41 @@ pub(super) fn report_bad_dunder_get_call<'db>( } } +/// Reports an invalid implicit `__getattr__` call at the original attribute access. +/// +/// ```python +/// class C: +/// def __getattr__(self) -> int: ... +/// +/// C().missing # Invalid: Python passes the attribute name to __getattr__. +/// ``` +/// +/// Preserves the underlying call diagnostic and explains why attribute access invoked the method. +pub(super) fn report_bad_dunder_getattr_call<'db>( + context: &InferContext<'db, '_>, + failure: &CallError<'db>, + object_type: Type<'db>, + target: &ast::ExprAttribute, +) { + let db = context.db(); + let env = &context.program_environment(); + let attribute = target.attr.as_str(); + + failure.report_diagnostics_with_override( + context, + target.into(), + &CallDiagnosticOverride { + lint: &INVALID_ATTRIBUTE_ACCESS, + message: format!( + "Invalid access to attribute `{attribute}` on type `{}`", + object_type.display(db, env), + ), + info: "This access implicitly calls `__getattr__`", + argument_ranges: &[target.range()], + }, + ); +} + pub(super) fn report_bad_dunder_set_call<'db>( context: &InferContext<'db, '_>, dunder_set_failure: &CallError<'db>, From 6ea296b96923e142eb13af2bc6ad261c280d8eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9r=C3=A8?= Date: Thu, 6 Aug 2026 08:26:29 -0700 Subject: [PATCH 318/390] [ty] Normalize type labels in structured docstrings (#26923) ## Summary This normalizes the source markup used to describe a type in a docstring before we wrap that type in backticks in order to render it as a code span. The outcome is that our rendering of types improves across all docstring formats. The normalization performs the following operations: 1. Removes embedded backtick delimiters from the type expression. 2. Removes valid reStructuredText role prefixes. 3. Uses the abbreviated display label requested by a leading `~` in a single-backtick span, including spans without an explicit role. 4. Removes Sphinx's leading-dot resolver marker from explicit Python-domain references, such as `` :py:obj:`.lines.line` ``, while preserving literal spans such as `.py`, `.env`, and `.Figure`. 5. Removes reStructuredText escapes from punctuation. Existing Markdown code spans otherwise continue to pass through unchanged. The table below gives examples of parameter docstrings with their emitted Markdown before and after this change (note that the changes are subtle!): | Category / real-world example | Before | After | | :--- | :--- | :--- | | [Embedded type markup](https://github.com/scipy/scipy/blob/75aedcaae44102a9abaeba389cf3a0f87ff4f61f/scipy/stats/_kde.py#L471) | `` **rng**: `{None, `Generator`}` `` | `` **rng**: `{None, Generator}` `` | | [Escaped punctuation](https://github.com/networkx/networkx/blob/3b4c227fda1c5462e6c04ac1ed7e7b0d8f2eacd4/networkx/drawing/nx_pylab.py#L1211) | `` **style**: `str ('-\\|>')` `` | `` **style**: `str ('-\|>')` `` | | [reST role and abbreviated label](https://github.com/astropy/astropy/blob/0c2b1f70ebd1203f8343fde9c7a59dd9d53c1a3e/astropy/units/quantity.py#L388) | `` **unit**: `:class:`~pkg.Unit` or str` `` | `` **unit**: `Unit or str` `` | This should improve even further with https://github.com/astral-sh/ruff/pull/27116, but I think this is already a considerable improvement given that it fixes some plainly broken Markdown. ## Test Plan See included tests. --- .../ty_ide/src/docstring/document/google.rs | 4 +- .../ty_ide/src/docstring/document/syntax.rs | 76 ++++++- .../src/docstring/markdown/structured.rs | 192 +++++++++++++++++- 3 files changed, 258 insertions(+), 14 deletions(-) diff --git a/crates/ty_ide/src/docstring/document/google.rs b/crates/ty_ide/src/docstring/document/google.rs index 4cac1cfa93..23709a73b9 100644 --- a/crates/ty_ide/src/docstring/document/google.rs +++ b/crates/ty_ide/src/docstring/document/google.rs @@ -848,14 +848,14 @@ fn split_once_at_field_delimiter(line: &str) -> Option<(&str, &str)> { /// :exc:`ValueError` /// ``` fn consume_rest_prefix_role(cursor: &mut Cursor<'_>) -> bool { - let Some(InlineMarkupToken::RestPrefixRole { span, .. }) = + let Some(InlineMarkupToken::RestPrefixRole(role)) = InlineMarkupScanner::new(cursor.as_str()).next() else { return false; }; // Resume delimiter scanning after the closing backtick in e.g., `` :exc:`ValueError` ``. - cursor.skip_bytes(span.end().to_usize()); + cursor.skip_bytes(role.span().end().to_usize()); true } diff --git a/crates/ty_ide/src/docstring/document/syntax.rs b/crates/ty_ide/src/docstring/document/syntax.rs index 2958bb27d7..2406b11004 100644 --- a/crates/ty_ide/src/docstring/document/syntax.rs +++ b/crates/ty_ide/src/docstring/document/syntax.rs @@ -145,7 +145,7 @@ impl<'a> Iterator for InlineMarkupScanner<'a> { { ( preceding_text, - InlineMarkupToken::RestPrefixRole { name, span }, + InlineMarkupToken::RestPrefixRole(Role { name, span }), ) } else { (preceding_text, InlineMarkupToken::Code(span)) @@ -178,12 +178,68 @@ pub(crate) enum InlineMarkupToken<'a> { /// A complete code span whose backtick delimiters have equal lengths. Code(BacktickSpan<'a>), /// A reStructuredText prefix-role pattern and its single-backtick span. - RestPrefixRole { - /// The role name between the colons, for example `py:class`. - name: &'a str, - /// The complete single-backtick span following the role name. - span: BacktickSpan<'a>, - }, + RestPrefixRole(Role<'a>), +} + +/// A reStructuredText prefix role recognized by [`InlineMarkupScanner`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Role<'a> { + name: &'a str, + span: BacktickSpan<'a>, +} + +impl<'a> Role<'a> { + /// Returns the complete single-backtick span following the role name. + /// + /// For `` :class:`Model ` ``, this returns the span + /// `` `Model ` ``. + pub(crate) fn span(self) -> BacktickSpan<'a> { + self.span + } + + /// Returns the source between the role's backtick delimiters. + /// + /// For `` :class:`Model ` ``, this returns `Model `. + pub(crate) fn content(self) -> &'a str { + self.span.content() + } + + /// Returns the explicit display title, if present. + /// + /// For `` :class:`Model ` ``, this returns `Some("Model")`. + pub(crate) fn explicit_title(self) -> Option<&'a str> { + self.content() + .strip_suffix('>') + .and_then(|content| content.split_once('<')) + .map(|(title, _)| title.trim_end()) + } + + /// Returns whether this is a Sphinx Python-domain cross-reference role. + /// + /// For example, this returns `true` for `class`, `py:func`, and + /// `external+python:py:obj`. + pub(crate) fn is_python_domain_cross_reference(self) -> bool { + let mut components = self.name.rsplit(':'); + let Some(role) = components.next() else { + return false; + }; + + matches!(components.next(), None | Some("py")) + && matches!( + role, + "attr" + | "class" + | "const" + | "data" + | "deco" + | "exc" + | "func" + | "meth" + | "mod" + | "obj" + | "type" + ) + } } /// Splits a trailing reStructuredText prefix-role pattern from its preceding text. @@ -688,8 +744,8 @@ mod tests { (":foo..bar:`Value`", None), ] { let actual = InlineMarkupScanner::new(source).next().and_then(|token| { - if let InlineMarkupToken::RestPrefixRole { name, span } = token { - Some((name, span.content())) + if let InlineMarkupToken::RestPrefixRole(role) = token { + Some((role.name, role.content())) } else { None } @@ -811,7 +867,7 @@ mod tests { .map(|token| match token { InlineMarkupToken::Text(text) => ("text", text), InlineMarkupToken::Code(code) => ("code", code.content()), - InlineMarkupToken::RestPrefixRole { span, .. } => ("rest role", span.content()), + InlineMarkupToken::RestPrefixRole(role) => ("rest role", role.content()), }) .collect() } diff --git a/crates/ty_ide/src/docstring/markdown/structured.rs b/crates/ty_ide/src/docstring/markdown/structured.rs index 918768a839..283e88f743 100644 --- a/crates/ty_ide/src/docstring/markdown/structured.rs +++ b/crates/ty_ide/src/docstring/markdown/structured.rs @@ -7,7 +7,8 @@ use super::general; use crate::docstring::document::SectionKind; use crate::docstring::document::preformatted::MarkdownFence; use crate::docstring::document::syntax::{ - is_wrapped_in_markdown_code_span, starts_with_markdown_list_item, + InlineMarkupScanner, InlineMarkupToken, is_wrapped_in_markdown_code_span, + starts_with_markdown_list_item, }; mod google; @@ -374,14 +375,98 @@ fn description_block_start(description: &str) -> Option { fn render_type_code_span_into(output: &mut String, ty: &str) { let normalized = normalize_type_for_code_span(ty); - if is_wrapped_in_markdown_code_span(&normalized) { + // Preserve existing code spans, except for abbreviated Sphinx references + // such as "`~pkg.Model`" (whose display should be normalized to "Model"). + if is_wrapped_in_markdown_code_span(&normalized) && !normalized.starts_with("`~") { output.push_str(&normalized); return; } + let normalized = normalize_embedded_type_markup(&normalized); render_code_span_into(output, normalized.as_ref()); } +/// Removes embedded markup before wrapping the normalize type label in a code span. +/// +/// For example: +/// - ``"str or :class:`pkg.Type` or `pkg.Other`"`` becomes `"str or pkg.Type or pkg.Other"` +/// - `"-\\|>"` becomes `"-|>"`. +fn normalize_embedded_type_markup(ty: &str) -> Cow<'_, str> { + if !ty.contains('`') && !ty.contains('\\') { + return Cow::Borrowed(ty); + } + + let mut normalized = String::with_capacity(ty.len()); + for token in InlineMarkupScanner::new(ty) { + match token { + InlineMarkupToken::Text(text) => push_unescaped(&mut normalized, text), + InlineMarkupToken::Code(span) => { + let markup = span.content(); + + // "`~pkg.Widget`" becomes "Widget" + // "``literal`tick``" becomes "literal`tick". + let display_text = if span.is_single() { + interpreted_text_label(markup, false) + } else { + markup + }; + + push_unescaped(&mut normalized, display_text); + } + InlineMarkupToken::RestPrefixRole(role) => { + let markup = role.content(); + + // ":class:`Model `" becomes "Model" + // ":obj:`.lines.line`" becomes "lines.line". + let display_text = role.explicit_title().unwrap_or_else(|| { + interpreted_text_label(markup, role.is_python_domain_cross_reference()) + }); + + push_unescaped(&mut normalized, display_text); + } + } + } + + Cow::Owned(normalized) +} + +/// Returns the display label for reStructuredText interpreted text. +/// +/// For example, "~pkg.Widget" becomes "Widget"; a Python role target like +/// ".lines.line" becomes "lines.line". +fn interpreted_text_label(text: &str, is_python_role_target: bool) -> &str { + let (is_abbreviated, target) = text + .strip_prefix('~') + .map_or((false, text), |target| (true, target)); + let target = if is_python_role_target { + target.strip_prefix('.').unwrap_or(target) + } else { + target + }; + if target.is_empty() { + return text; + } + + if is_abbreviated { + target.rsplit_once('.').map_or(target, |(_, label)| label) + } else { + target + } +} + +fn push_unescaped(output: &mut String, text: &str) { + let mut characters = text.chars().peekable(); + while let Some(character) = characters.next() { + if character == '\\' + && let Some(escaped) = characters.next_if(char::is_ascii_punctuation) + { + output.push(escaped); + } else { + output.push(character); + } + } +} + /// Normalizes type text so it fits in a single Markdown code span. /// /// One-line types are returned unchanged. Multi-line types are trimmed line by @@ -605,6 +690,109 @@ mod tests { "); } + #[test] + fn section_items_normalize_source_markup_in_types() { + let _snap = bind_markdown_snapshot_filters(); + let section = section_block(vec![ + SectionItem::new( + SectionKind::Parameters, + Some("rng"), + Some("{None, int, `numpy.random.Generator`, `numpy.random.RandomState`}, optional"), + "Random number generator.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("arrowstyle"), + Some(r"str (default='-\|>')"), + "Arrow style.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("model"), + Some("str or :class:`pkg.Model`"), + "Model type.", + ), + ]); + + assert_snapshot!(render_markdown(§ion), @" + ## Parameters + **rng**: `{None, int, numpy.random.Generator, numpy.random.RandomState}, optional` + Random number generator. + + **arrowstyle**: `str (default='-|>')` + Arrow style. + + **model**: `str or pkg.Model` + Model type. + "); + } + + #[test] + fn section_items_remove_rest_roles_from_types() { + let _snap = bind_markdown_snapshot_filters(); + let section = section_block(vec![ + SectionItem::new( + SectionKind::Parameters, + Some("colormap"), + Some( + "str or :class:`~matplotlib.colors.Colormap` or :mod:`matplotlib.colors` or `~.pandas.Index`", + ), + "Color mapping.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("model"), + Some("`~astropy.modeling.core.Model`"), + "Model.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("extension"), + Some("`.py`"), + "File extension.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("config"), + Some("str or `.env` or `.Figure` or `.lines.Line2D`"), + "Configuration source.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("line"), + Some(":py:obj:`.lines.line`"), + "Line helper.", + ), + SectionItem::new( + SectionKind::Parameters, + Some("model"), + Some(":class:`Model `"), + "Named model.", + ), + ]); + + assert_snapshot!(render_markdown(§ion), @" + ## Parameters + **colormap**: `str or Colormap or matplotlib.colors or Index` + Color mapping. + + **model**: `Model` + Model. + + **extension**: `.py` + File extension. + + **config**: `str or .env or .Figure or .lines.Line2D` + Configuration source. + + **line**: `lines.line` + Line helper. + + **model**: `Model` + Named model. + "); + } + #[test] fn section_items_keep_block_descriptions_in_block_context() { let _snap = bind_markdown_snapshot_filters(); From 17a00de2e298612201a8fe30790e9399204af1b9 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 6 Aug 2026 18:12:14 +0200 Subject: [PATCH 319/390] [ty] Reuse primer commands in memory reports (#27553) --- scripts/memory_report.py | 64 +++++++++++++++++++++------------ scripts/setup_primer_project.py | 8 +++++ 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/scripts/memory_report.py b/scripts/memory_report.py index 339f07750c..b4c4fd3224 100644 --- a/scripts/memory_report.py +++ b/scripts/memory_report.py @@ -22,6 +22,7 @@ import argparse import json import os +import shlex import subprocess import sys import tempfile @@ -244,36 +245,43 @@ def render_summary(projects: list[ProjectComparison]) -> str: return "\n".join(lines) -def setup_project(*, name: str, dest: Path) -> Path: +def setup_project(*, name: str, dest: Path) -> tuple[Path, list[str]]: """Clone a project and install its mypy-primer dependencies.""" project_path = dest / name + setup_script = Path(__file__).with_name("setup_primer_project.py") + setup_command = [ + "uv", + "run", + "--locked", + "--python", + sys.executable, + "--script", + str(setup_script), + ] + if project_path.exists(): print(f"Project {name} already exists at {project_path}", file=sys.stderr) - return project_path + else: + print(f"Setting up {name} and its dependencies...", file=sys.stderr) + subprocess.run( + [*setup_command, name, str(project_path)], + check=True, + stdout=sys.stderr, + ) - setup_script = Path(__file__).with_name("setup_primer_project.py") - print(f"Setting up {name} and its dependencies...", file=sys.stderr) - subprocess.run( - [ - "uv", - "run", - "--locked", - "--python", - sys.executable, - "--script", - str(setup_script), - name, - str(project_path), - ], + ty_command = subprocess.run( + [*setup_command, "--print-ty-command", name, str(project_path)], check=True, - stdout=sys.stderr, + capture_output=True, + text=True, ) - return project_path + return project_path, shlex.split(ty_command.stdout) def run_ty_memory_check( *, ty_path: str, + ty_command: list[str], project_path: Path, output_path: Path, ) -> None: @@ -283,8 +291,14 @@ def run_ty_memory_check( env["TY_MAX_PARALLELISM"] = "1" # For deterministic memory numbers print(f"Running {ty_path} on {project_path.name}...", file=sys.stderr) + command = [ + str(Path(ty_path).resolve()) if argument == "{ty}" else argument + for argument in ty_command + ] result = subprocess.run( - [ty_path, "check", "--project", str(project_path), "--exit-zero"], + command, + cwd=project_path, + check=True, capture_output=True, text=True, env=env, @@ -306,18 +320,24 @@ def run_memory_tests( new_reports_dir.mkdir(parents=True, exist_ok=True) for project_name in KNOWN_PROJECTS: - project_path = setup_project(name=project_name, dest=projects_dir) + project_path, ty_command = setup_project(name=project_name, dest=projects_dir) # Run old ty old_report_path = old_reports_dir / f"{project_name}.json" run_ty_memory_check( - ty_path=old_ty, project_path=project_path, output_path=old_report_path + ty_path=old_ty, + ty_command=ty_command, + project_path=project_path, + output_path=old_report_path, ) # Run new ty new_report_path = new_reports_dir / f"{project_name}.json" run_ty_memory_check( - ty_path=new_ty, project_path=project_path, output_path=new_report_path + ty_path=new_ty, + ty_command=ty_command, + project_path=project_path, + output_path=new_report_path, ) diff --git a/scripts/setup_primer_project.py b/scripts/setup_primer_project.py index 79ed417765..f971d0a903 100644 --- a/scripts/setup_primer_project.py +++ b/scripts/setup_primer_project.py @@ -93,12 +93,20 @@ def main() -> None: "--exclude-newer", help="Limit dependency resolution to packages uploaded before this timestamp", ) + parser.add_argument( + "--print-ty-command", + action="store_true", + help="Print the project-specific ty command without setting up the project", + ) args = parser.parse_args() project = find_project(args.project) revision = args.revision or project.revision target_dir = Path(args.directory or project.name).resolve() + if args.print_ty_command: + print(get_ty_command(project, ty_binary="{ty}", venv_dir=target_dir / ".venv")) + return # Use a full clone only when a historical ecosystem report revision must be checked out. clone_cmd = [ From c4e86fc0394c92a9334ba2eb026c77c21db403be Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Thu, 6 Aug 2026 21:54:55 -0400 Subject: [PATCH 320/390] [ty] Add helper extension methods for half-range and equality constraints (#27564) The `ConstraintSet.range` extension method is used in mdtests to create specific constraint sets for testing purposes. This is a simple (but largish) refactoring that adds three additional constructors for certain common constraint shapes: lower-bound-only, upper-bound-only, and equality constraints. --- .../regression/constraint_set_ordering.md | 103 ++++---- .../regression/derived_constraint_cycles.md | 3 +- .../mdtest/type_properties/constraints.md | 219 +++++++++++------- .../type_properties/implies_subtype_of.md | 180 +++++++------- .../type_properties/is_assignable_to.md | 2 +- .../mdtest/type_properties/quantification.md | 62 +++-- .../satisfied_by_all_typevars.md | 138 +++++------ crates/ty_python_semantic/src/types.rs | 33 +++ .../ty_python_semantic/src/types/call/bind.rs | 76 ++++++ .../ty_python_semantic/src/types/display.rs | 9 + crates/ty_python_semantic/src/types/method.rs | 70 +++++- .../ty_python_semantic/src/types/relation.rs | 3 + .../ty_vendored/ty_extensions/_internal.pyi | 21 ++ 13 files changed, 574 insertions(+), 345 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/regression/constraint_set_ordering.md b/crates/ty_python_semantic/resources/mdtest/regression/constraint_set_ordering.md index 044d66affd..1ae6e2a0ef 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/constraint_set_ordering.md +++ b/crates/ty_python_semantic/resources/mdtest/regression/constraint_set_ordering.md @@ -29,8 +29,8 @@ python-version = "3.13" from ty_extensions._internal import ConstraintSet def absorption[T]() -> None: - scalar = ConstraintSet.range(str, T, object) - tuple_ = ConstraintSet.range(tuple[str, ...], T, object) + scalar = ConstraintSet.lower_bound(str, T) + tuple_ = ConstraintSet.lower_bound(tuple[str, ...], T) # revealed: tuple[Solution[T=str]] reveal_type((scalar & (scalar | tuple_)).solutions_for(T, inferable=tuple[T])) @@ -53,26 +53,26 @@ from ty_extensions._internal import ConstraintSet def bindings_tuv[T, U, V]() -> None: # (T = int) ∧ (U = str) ∧ (V = bytes) - constraints = ConstraintSet.range(int, T, int) & ConstraintSet.range(str, U, str) & ConstraintSet.range(bytes, V, bytes) + constraints = ConstraintSet.equality(T, int) & ConstraintSet.equality(U, str) & ConstraintSet.equality(V, bytes) # revealed: tuple[Solution[T=int, U=str, V=bytes]] reveal_type(constraints.solutions(inferable=tuple[T, U, V])) def bindings_vtu[V, T, U]() -> None: # (T = int) ∧ (U = str) ∧ (V = bytes) - constraints = ConstraintSet.range(int, T, int) & ConstraintSet.range(str, U, str) & ConstraintSet.range(bytes, V, bytes) + constraints = ConstraintSet.equality(T, int) & ConstraintSet.equality(U, str) & ConstraintSet.equality(V, bytes) # revealed: tuple[Solution[T=int, U=str, V=bytes]] reveal_type(constraints.solutions(inferable=tuple[T, U, V])) def bindings_reverse_source[T, U, V]() -> None: # (V = bytes) ∧ (U = str) ∧ (T = int) - constraints = ConstraintSet.range(bytes, V, bytes) & ConstraintSet.range(str, U, str) & ConstraintSet.range(int, T, int) + constraints = ConstraintSet.equality(V, bytes) & ConstraintSet.equality(U, str) & ConstraintSet.equality(T, int) # revealed: tuple[Solution[V=bytes, U=str, T=int]] reveal_type(constraints.solutions(inferable=tuple[T, U, V])) def bindings_absorbed[T, U, X]() -> None: - t = ConstraintSet.range(str, T, object) - u = ConstraintSet.range(bytes, U, object) - x = ConstraintSet.range(int, X, object) + t = ConstraintSet.lower_bound(str, T) + u = ConstraintSet.lower_bound(bytes, U) + x = ConstraintSet.lower_bound(int, X) # ((X ≥ int) ∧ (T ≥ str) ∧ (U ≥ bytes)) | ((U ≥ bytes) ∧ (T ≥ str)) constraints = (x & t & u) | (u & t) @@ -88,14 +88,13 @@ and vice versa. Because we combine them with union, we are allowed to _either_ f `T` and `U`, _or_ find a solution for `V`. We are not _obligated_ to find a solution for all three. ```py -from typing import Never from ty_extensions._internal import ConstraintSet def nested_transitive[T, U, V]() -> None: # ((T ≤ list[U]) ∧ (U ≤ int) ∧ (list[int] ≤ T)) | (bytes ≤ V) constraints = ( - ConstraintSet.range(Never, T, list[U]) & ConstraintSet.range(Never, U, int) & ConstraintSet.range(list[int], T, object) - ) | ConstraintSet.range(bytes, V, object) + ConstraintSet.upper_bound(T, list[U]) & ConstraintSet.upper_bound(U, int) & ConstraintSet.lower_bound(list[int], T) + ) | ConstraintSet.lower_bound(bytes, V) # TODO: sometimes: revealed tuple[Solution[T=list[int]], Solution[T=Never], Solution[]] # TODO: sometimes: revealed tuple[Solution[T=list[int]], Solution[T=list[int]], Solution[]] @@ -127,14 +126,11 @@ includes both sides of the union, so any solution that includes `bytes ≤ U` sh solution for `T`. ```py -from typing import Never from ty_extensions._internal import ConstraintSet def negated_alternative[T, U]() -> None: # ¬((T ≤ int) ∨ (T ≤ str)) | (bytes ≤ U) - constraints = ~(ConstraintSet.range(Never, T, int) | ConstraintSet.range(Never, T, str)) | ConstraintSet.range( - bytes, U, object - ) + constraints = ~(ConstraintSet.upper_bound(T, int) | ConstraintSet.upper_bound(T, str)) | ConstraintSet.lower_bound(bytes, U) # TODO: sometimes: revealed tuple[Solution[], Solution[T=Never], Solution[]] # revealed: tuple[Solution[], Solution[]] @@ -155,15 +151,14 @@ Constructing the constraints in the opposite source order makes the derived unio elements should not be reordered merely because the TDD-variable order changes. ```py -from typing import Never from ty_extensions._internal import ConstraintSet def derived_solution[U, T]() -> None: # (U ≤ int) ∧ (int ≤ T) ∧ ((T ≤ int) | (T ≤ str)) constraints = ( - ConstraintSet.range(Never, U, int) - & ConstraintSet.range(int, T, object) - & (ConstraintSet.range(Never, T, int) | ConstraintSet.range(Never, T, str)) + ConstraintSet.upper_bound(U, int) + & ConstraintSet.lower_bound(int, T) + & (ConstraintSet.upper_bound(T, int) | ConstraintSet.upper_bound(T, str)) ) # TODO: The derived relationship should not leave an inferable `U` in the solution for `T`. @@ -174,7 +169,7 @@ def derived_solution[U, T]() -> None: # TODO: The derived relationship should not leave an inferable `T` in the solution for `U`. # TODO: revealed: tuple[Solution[U=int]] - # revealed: tuple[Solution[U=Never]] + # revealed: tuple[Solution[U=int & T@derived_solution]] reveal_type(constraints.solutions_for(U, inferable=tuple[T, U])) ``` @@ -185,37 +180,36 @@ range or two linked constraints. Logical equivalence and solution-element order in both declaration orders. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def orientation_st[S, T]() -> None: - lower = ConstraintSet.range(Never, S, T) - upper = ConstraintSet.range(S, T, object) + lower = ConstraintSet.upper_bound(S, T) + upper = ConstraintSet.lower_bound(S, T) # TODO: sometimes: error [static-assert-error] "Static assertion error: argument evaluates to `False`" static_assert(lower == upper) - equality_st = ConstraintSet.range(T, S, T) - equality_ts = ConstraintSet.range(S, T, S) + equality_st = ConstraintSet.equality(S, T) + equality_ts = ConstraintSet.equality(T, S) static_assert(equality_st == equality_ts) def orientation_ts[T, S]() -> None: - lower = ConstraintSet.range(Never, S, T) - upper = ConstraintSet.range(S, T, object) + lower = ConstraintSet.upper_bound(S, T) + upper = ConstraintSet.lower_bound(S, T) # TODO: sometimes: error [static-assert-error] "Static assertion error: argument evaluates to `False`" static_assert(lower == upper) - equality_st = ConstraintSet.range(T, S, T) - equality_ts = ConstraintSet.range(S, T, S) + equality_st = ConstraintSet.equality(S, T) + equality_ts = ConstraintSet.equality(T, S) static_assert(equality_st == equality_ts) def chain_stu[S, T, U]() -> None: chain = ConstraintSet.range(S, T, U) - linked = ConstraintSet.range(Never, S, T) & ConstraintSet.range(Never, T, U) + linked = ConstraintSet.upper_bound(S, T) & ConstraintSet.upper_bound(T, U) # TODO: sometimes: error [static-assert-error] "Static assertion error: argument evaluates to `False`" static_assert(chain == linked) - constraints = chain & ConstraintSet.range(int, S, object) & ConstraintSet.range(Never, U, int) + constraints = chain & ConstraintSet.lower_bound(int, S) & ConstraintSet.upper_bound(U, int) # TODO: inferable typevars should not remain in these concrete solutions. # TODO: sometimes: revealed tuple[Solution[S=int | U@chain_stu | T@chain_stu]] # revealed: tuple[Solution[S=int | T@chain_stu | U@chain_stu]] @@ -227,11 +221,11 @@ def chain_stu[S, T, U]() -> None: def chain_uts[U, T, S]() -> None: chain = ConstraintSet.range(S, T, U) - linked = ConstraintSet.range(Never, S, T) & ConstraintSet.range(Never, T, U) + linked = ConstraintSet.upper_bound(S, T) & ConstraintSet.upper_bound(T, U) # TODO: sometimes: error [static-assert-error] "Static assertion error: argument evaluates to `False`" static_assert(chain == linked) - constraints = chain & ConstraintSet.range(int, S, object) & ConstraintSet.range(Never, U, int) + constraints = chain & ConstraintSet.lower_bound(int, S) & ConstraintSet.upper_bound(U, int) # TODO: inferable typevars should not remain in these concrete solutions. # TODO: sometimes: revealed tuple[Solution[S=int | U@chain_uts | T@chain_uts]] # revealed: tuple[Solution[S=int | T@chain_uts | U@chain_uts]] @@ -249,14 +243,13 @@ leak onto the surviving paths. Universal abstraction of an alternative must like unrelated branch. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def noninferable_nested[T, U, V]() -> None: constraints = ( - ConstraintSet.range(Never, T, list[U]) & ConstraintSet.range(Never, U, int) & ConstraintSet.range(list[int], T, object) - ) | ConstraintSet.range(bytes, V, object) + ConstraintSet.upper_bound(T, list[U]) & ConstraintSet.upper_bound(U, int) & ConstraintSet.lower_bound(list[int], T) + ) | ConstraintSet.lower_bound(bytes, V) # `U` is deliberately non-inferable here. # TODO: We should not include a solution for non-inferable U. @@ -273,18 +266,16 @@ def noninferable_nested[T, U, V]() -> None: reveal_type(constraints.solutions_for(V, inferable=tuple[T, V])) quantified = constraints.for_all(tuple[T, U]) - expected = ConstraintSet.range(bytes, V, object) + expected = ConstraintSet.lower_bound(bytes, V) static_assert(quantified == expected) # revealed: tuple[Solution[V=bytes]] reveal_type(quantified.solutions_for(V, inferable=tuple[V])) def noninferable_negated[T, U]() -> None: - constraints = ~(ConstraintSet.range(Never, T, int) | ConstraintSet.range(Never, T, str)) | ConstraintSet.range( - bytes, U, object - ) + constraints = ~(ConstraintSet.upper_bound(T, int) | ConstraintSet.upper_bound(T, str)) | ConstraintSet.lower_bound(bytes, U) quantified = constraints.for_all(tuple[T]) - expected = ConstraintSet.range(bytes, U, object) + expected = ConstraintSet.lower_bound(bytes, U) static_assert(quantified == expected) # revealed: tuple[Solution[U=bytes]] reveal_type(quantified.solutions_for(U, inferable=tuple[U])) @@ -332,7 +323,7 @@ def listify[T](value: T) -> list[T]: return [value] def invariant_callable[U, V]() -> None: - constraints = ConstraintSet.range(bool, U, int) & ConstraintSet.range(int, V, int) + constraints = ConstraintSet.range(bool, U, int) & ConstraintSet.equality(V, int) # TODO: no error. Existential reduction of the callable's fresh typevar is currently lossy. # TODO: sometimes: no error # error: [static-assert-error] @@ -397,7 +388,7 @@ sequent fuel budget. The remaining solution, its element order, and the elements truncated diagnostic display must not depend on which implications were encountered first. ```py -from typing import Literal, Never +from typing import Literal from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -443,18 +434,18 @@ def high_fanout[ & ConstraintSet.range(Literal[11], L11, P) ) upper = ( - ConstraintSet.range(Never, P, R0) - & ConstraintSet.range(Never, P, R1) - & ConstraintSet.range(Never, P, R2) - & ConstraintSet.range(Never, P, R3) - & ConstraintSet.range(Never, P, R4) - & ConstraintSet.range(Never, P, R5) - & ConstraintSet.range(Never, P, R6) - & ConstraintSet.range(Never, P, R7) - & ConstraintSet.range(Never, P, R8) - & ConstraintSet.range(Never, P, R9) - & ConstraintSet.range(Never, P, R10) - & ConstraintSet.range(Never, P, R11) + ConstraintSet.upper_bound(P, R0) + & ConstraintSet.upper_bound(P, R1) + & ConstraintSet.upper_bound(P, R2) + & ConstraintSet.upper_bound(P, R3) + & ConstraintSet.upper_bound(P, R4) + & ConstraintSet.upper_bound(P, R5) + & ConstraintSet.upper_bound(P, R6) + & ConstraintSet.upper_bound(P, R7) + & ConstraintSet.upper_bound(P, R8) + & ConstraintSet.upper_bound(P, R9) + & ConstraintSet.upper_bound(P, R10) + & ConstraintSet.upper_bound(P, R11) ) inferable = tuple[ P, @@ -507,7 +498,7 @@ def high_fanout[ # revealed: tuple[Solution[R11=L1@high_fanout | Literal[2, 3, 4, 5, 6, 7, 8, 9, 10, 11] | L2@high_fanout | L3@high_fanout | L4@high_fanout | L5@high_fanout | L6@high_fanout | L7@high_fanout | L8@high_fanout | L9@high_fanout | L10@high_fanout | L11@high_fanout | P@high_fanout]] reveal_type(result) - impossible = constraints & ConstraintSet.range(Never, R11, Literal[0]) + impossible = constraints & ConstraintSet.upper_bound(R11, Literal[0]) # TODO: sometimes: error [static-assert-error] "Static assertion error: argument evaluates to `False`" static_assert(not impossible.satisfied_by_all_typevars(inferable=inferable)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/regression/derived_constraint_cycles.md b/crates/ty_python_semantic/resources/mdtest/regression/derived_constraint_cycles.md index 86ec59c3f7..fc4f2052c3 100644 --- a/crates/ty_python_semantic/resources/mdtest/regression/derived_constraint_cycles.md +++ b/crates/ty_python_semantic/resources/mdtest/regression/derived_constraint_cycles.md @@ -171,14 +171,13 @@ Structural fuel is charged only for depth introduced by a derivation. Propagatin concrete bound through a typevar therefore remains cheap. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet type Deep = tuple[tuple[tuple[tuple[tuple[tuple[tuple[tuple[tuple[tuple[int]]]]]]]]]] def check_deep_bound[T, U](): - constraints = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, Deep) + constraints = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, Deep) static_assert(constraints.implies_subtype_of(T, Deep)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md index ae66e75be4..3d18fd8600 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md @@ -49,22 +49,20 @@ def _[T]() -> None: ConstraintSet.range(Sub, T, Super) ``` -Every type is a supertype of `Never`, so a lower bound of `Never` is the same as having no lower -bound. +Every type is a supertype of `Never`, so `upper_bound` can omit the lower bound. ```py def _[T]() -> None: # (T@_ ≤ Base) - ConstraintSet.range(Never, T, Base) + ConstraintSet.upper_bound(T, Base) ``` -Similarly, every type is a subtype of `object`, so an upper bound of `object` is the same as having -no upper bound. +Similarly, every type is a subtype of `object`, so `lower_bound` can omit the upper bound. ```py def _[T]() -> None: # (Base ≤ T@_) - ConstraintSet.range(Base, T, object) + ConstraintSet.lower_bound(Base, T) ``` And a range constraint with a lower bound of `Never` and an upper bound of `object` allows the @@ -88,13 +86,13 @@ def _[T]() -> None: static_assert(not ConstraintSet.range(Base, T, Unrelated)) ``` -The lower and upper bound can be the same type, in which case the typevar can only be specialized to +When the lower and upper bounds are the same type, `equality` requires the typevar to specialize to that specific type. ```py def _[T]() -> None: # (T@_ = Base) - ConstraintSet.range(Base, T, Base) + ConstraintSet.equality(T, Base) ``` Constraints can only refer to fully static types, so the lower and upper bounds are transformed into @@ -103,7 +101,7 @@ their bottom and top materializations, respectively. ```py def _[T]() -> None: constraints = ConstraintSet.range(Base, T, Any) - expected = ConstraintSet.range(Base, T, object) + expected = ConstraintSet.lower_bound(Base, T) static_assert(constraints == expected) constraints = ConstraintSet.range(Sequence[Base], T, Sequence[Any]) @@ -111,7 +109,7 @@ def _[T]() -> None: static_assert(constraints == expected) constraints = ConstraintSet.range(Any, T, Base) - expected = ConstraintSet.range(Never, T, Base) + expected = ConstraintSet.upper_bound(T, Base) static_assert(constraints == expected) constraints = ConstraintSet.range(Sequence[Any], T, Sequence[Base]) @@ -119,6 +117,65 @@ def _[T]() -> None: static_assert(constraints == expected) ``` +### Lower bound + +A lower-bound constraint requires the type variable to be a supertype of its bound without providing +upper-bound evidence. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet, is_constraint_set_assignable_to + +def _[T]() -> None: + expected = is_constraint_set_assignable_to(int, T) + static_assert(ConstraintSet.lower_bound(int, T) == expected) +``` + +### Upper bound + +An upper-bound constraint requires the type variable to be a subtype of its bound without providing +lower-bound evidence. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet, is_constraint_set_assignable_to + +def _[T]() -> None: + expected = is_constraint_set_assignable_to(T, int) + static_assert(ConstraintSet.upper_bound(T, int) == expected) +``` + +Unlike an explicit two-sided range, an upper-bound constraint does not supply `Never` as lower-bound +inference evidence. + +```py +from typing import Never + +def inferred_solution[T]() -> None: + # revealed: tuple[Solution[T=int]] + reveal_type(ConstraintSet.upper_bound(T, int).solutions_for(T, inferable=tuple[T])) + + # revealed: tuple[Solution[T=Never]] + reveal_type(ConstraintSet.range(Never, T, int).solutions_for(T, inferable=tuple[T])) +``` + +### Equality + +An equality constraint requires the type variable to specialize exactly to the specified type. It is +equivalent to an explicit range with that type as both bounds. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +def _[T]() -> None: + equality = ConstraintSet.equality(T, int) + static_assert(equality == ConstraintSet.range(int, T, int)) + + # revealed: tuple[Solution[T=int]] + reveal_type(equality.solutions_for(T, inferable=tuple[T])) +``` + ### Negated range A _negated range_ constraint is the opposite of a range constraint: it requires the typevar to _not_ @@ -142,22 +199,20 @@ def _[T]() -> None: ~ConstraintSet.range(Sub, T, Super) ``` -Every type is a supertype of `Never`, so a lower bound of `Never` is the same as having no lower -bound. +Every type is a supertype of `Never`, so `upper_bound` can omit the lower bound. ```pyi def _[T]() -> None: # ¬(T@_ ≤ Base) - ~ConstraintSet.range(Never, T, Base) + ~ConstraintSet.upper_bound(T, Base) ``` -Similarly, every type is a subtype of `object`, so an upper bound of `object` is the same as having -no upper bound. +Similarly, every type is a subtype of `object`, so `lower_bound` can omit the upper bound. ```pyi def _[T]() -> None: # ¬(Base ≤ T@_) - ~ConstraintSet.range(Base, T, object) + ~ConstraintSet.lower_bound(Base, T) ``` And a negated range constraint with _both_ a lower bound of `Never` and an upper bound of `object` @@ -184,7 +239,7 @@ type other than that specific type. ```pyi def _[T]() -> None: # (T@_ ≠ Base) - ~ConstraintSet.range(Base, T, Base) + ~ConstraintSet.equality(T, Base) ``` Constraints can only refer to fully static types, so the lower and upper bounds are transformed into @@ -193,7 +248,7 @@ their bottom and top materializations, respectively. ```pyi def _[T]() -> None: constraints = ~ConstraintSet.range(Base, T, Any) - expected = ~ConstraintSet.range(Base, T, object) + expected = ~ConstraintSet.lower_bound(Base, T) static_assert(constraints == expected) constraints = ~ConstraintSet.range(Sequence[Base], T, Sequence[Any]) @@ -201,7 +256,7 @@ def _[T]() -> None: static_assert(constraints == expected) constraints = ~ConstraintSet.range(Any, T, Base) - expected = ~ConstraintSet.range(Never, T, Base) + expected = ~ConstraintSet.upper_bound(T, Base) static_assert(constraints == expected) constraints = ~ConstraintSet.range(Sequence[Any], T, Sequence[Base]) @@ -213,8 +268,8 @@ A negated _type_ is not the same thing as a negated _range_. ```pyi def _[T]() -> None: - negated_type = ConstraintSet.range(Never, T, ~int) - negated_constraint = ~ConstraintSet.range(Never, T, int) + negated_type = ConstraintSet.upper_bound(T, ~int) + negated_constraint = ~ConstraintSet.upper_bound(T, int) static_assert(negated_type != negated_constraint) ``` @@ -270,7 +325,7 @@ def _[T]() -> None: static_assert(constraints == expected) constraints = ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Base, T, Super) - expected = ConstraintSet.range(Base, T, Base) + expected = ConstraintSet.equality(T, Base) static_assert(constraints == expected) constraints = ConstraintSet.range(Sub, T, Super) & ConstraintSet.range(Sub, T, Super) @@ -283,7 +338,7 @@ If they don't overlap, the intersection is empty. ```pyi def _[T]() -> None: static_assert(not ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Base, T, Super)) - static_assert(not ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Unrelated, T, object)) + static_assert(not ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.lower_bound(Unrelated, T)) ``` Expanding on this, when intersecting two upper bounds constraints (`(T ≤ Base) ∧ (T ≤ Other)`), we @@ -291,16 +346,14 @@ intersect the upper bounds. Any type that satisfies both `T ≤ Base` and `T ≤ satisfy their intersection `T ≤ Base & Other`, and vice versa. ```pyi -from typing import Never - # This is not final, so it's possible for a subclass to inherit from both Base and Other. class Other: ... def upper_bounds[T](): # (T@upper_bounds ≤ Base & Other) - intersection_type = ConstraintSet.range(Never, T, Base & Other) + intersection_type = ConstraintSet.upper_bound(T, Base & Other) # (T@upper_bounds ≤ Base) ∧ (T@upper_bounds ≤ Other) - intersection_constraint = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, T, Other) + intersection_constraint = ConstraintSet.upper_bound(T, Base) & ConstraintSet.upper_bound(T, Other) static_assert(intersection_type == intersection_constraint) ``` @@ -311,9 +364,9 @@ bounds. Any type that satisfies both `Base ≤ T` and `Other ≤ T` must necessa ```pyi def lower_bounds[T](): # (Base | Other ≤ T@lower_bounds) - union_type = ConstraintSet.range(Base | Other, T, object) + union_type = ConstraintSet.lower_bound(Base | Other, T) # (Base ≤ T@upper_bounds) ∧ (Other ≤ T@upper_bounds) - intersection_constraint = ConstraintSet.range(Base, T, object) & ConstraintSet.range(Other, T, object) + intersection_constraint = ConstraintSet.lower_bound(Base, T) & ConstraintSet.lower_bound(Other, T) static_assert(union_type == intersection_constraint) ``` @@ -324,7 +377,7 @@ the negated range constraint provide a "hole" of types that should not be includ the intersection as removing the hole from the range constraint. ```py -from typing import final, Never +from typing import final from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -350,7 +403,7 @@ anything; the intersection is the positive range. ```py def _[T]() -> None: - constraints = ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Never, T, Unrelated) + constraints = ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.upper_bound(T, Unrelated) expected = ConstraintSet.range(Sub, T, Base) static_assert(constraints == expected) @@ -410,7 +463,7 @@ def _[T]() -> None: # ¬(Base ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Sub)) ~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super) # ¬(SubSub ≤ T@_ ≤ Sub) ∧ ¬(Unrelated ≤ T@_) - ~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Unrelated, T, object) + ~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.lower_bound(Unrelated, T) ``` In particular, the following does not simplify, even though it seems like it could simplify to @@ -493,7 +546,7 @@ def _[T]() -> None: # (Base ≤ T@_ ≤ Super) ∨ (SubSub ≤ T@_ ≤ Sub) ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super) # (SubSub ≤ T@_ ≤ Sub) ∨ (Unrelated ≤ T@_) - ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Unrelated, T, object) + ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.lower_bound(Unrelated, T) ``` In particular, the following does not simplify, even though it seems like it could simplify to @@ -518,19 +571,17 @@ as `T = Base | Other`) that satisfy the union type, but not the union constraint that satisfies the union constraint satisfies the union type. ```py -from typing import Never - # This is not final, so it's possible for a subclass to inherit from both Base and Other. class Other: ... def union[T](): # (T@union ≤ Base | Other) - union_type = ConstraintSet.range(Never, T, Base | Other) + union_type = ConstraintSet.upper_bound(T, Base | Other) # (T@union ≤ Base) ∨ (T@union ≤ Other) - union_constraint = ConstraintSet.range(Never, T, Base) | ConstraintSet.range(Never, T, Other) + union_constraint = ConstraintSet.upper_bound(T, Base) | ConstraintSet.upper_bound(T, Other) # (T = Base | Other) satisfies (T ≤ Base | Other) but not (T ≤ Base ∨ T ≤ Other) - specialization = ConstraintSet.range(Base | Other, T, Base | Other) + specialization = ConstraintSet.equality(T, Base | Other) static_assert(specialization.satisfies(union_type)) static_assert(not specialization.satisfies(union_constraint)) @@ -546,12 +597,12 @@ satisfies the union constraint (`(Base ≤ T) ∨ (Other ≤ T)`) but not the un ```py def union[T](): # (Base | Other ≤ T@union) - union_type = ConstraintSet.range(Base | Other, T, object) + union_type = ConstraintSet.lower_bound(Base | Other, T) # (Base ≤ T@union) ∨ (Other ≤ T@union) - union_constraint = ConstraintSet.range(Base, T, object) | ConstraintSet.range(Other, T, object) + union_constraint = ConstraintSet.lower_bound(Base, T) | ConstraintSet.lower_bound(Other, T) # (T = Base) satisfies (Base ≤ T ∨ Other ≤ T) but not (Base | Other ≤ T) - specialization = ConstraintSet.range(Base, T, Base) + specialization = ConstraintSet.equality(T, Base) static_assert(not specialization.satisfies(union_type)) static_assert(specialization.satisfies(union_constraint)) @@ -567,7 +618,7 @@ the negated range constraint provide a "hole" of types that should not be includ the union as filling part of the hole with the types from the range constraint. ```py -from typing import final, Never +from typing import final from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -593,7 +644,7 @@ the union is the negative range. ```py def _[T]() -> None: - constraints = ~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Never, T, Unrelated) + constraints = ~ConstraintSet.range(Sub, T, Base) | ConstraintSet.upper_bound(T, Unrelated) expected = ~ConstraintSet.range(Sub, T, Base) static_assert(constraints == expected) @@ -643,7 +694,7 @@ def _[T]() -> None: static_assert(constraints == expected) constraints = ~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Base, T, Super) - expected = ~ConstraintSet.range(Base, T, Base) + expected = ~ConstraintSet.equality(T, Base) static_assert(constraints == expected) constraints = ~ConstraintSet.range(Sub, T, Super) | ~ConstraintSet.range(Sub, T, Super) @@ -656,7 +707,7 @@ If the holes don't overlap, the union is always satisfied. ```py def _[T]() -> None: static_assert(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Base, T, Super)) - static_assert(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Unrelated, T, object)) + static_assert(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.lower_bound(Unrelated, T)) ``` ## Negation @@ -676,9 +727,9 @@ def _[T]() -> None: # ¬(Sub ≤ T@_ ≤ Base) ~ConstraintSet.range(Sub, T, Base) # ¬(T@_ ≤ Base) - ~ConstraintSet.range(Never, T, Base) + ~ConstraintSet.upper_bound(T, Base) # ¬(Sub ≤ T@_) - ~ConstraintSet.range(Sub, T, object) + ~ConstraintSet.lower_bound(Sub, T) # (T@_ ≠ *) ~ConstraintSet.range(Never, T, object) ``` @@ -694,7 +745,7 @@ def _[T]() -> None: ### Negation of constraints involving two variables ```py -from typing import final, Never +from typing import final from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -705,18 +756,18 @@ class Unrelated: ... def _[T, U]() -> None: # ¬(T@_ ≤ Base) ∨ ¬(U@_ ≤ Base) - ~(ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base)) + ~(ConstraintSet.upper_bound(T, Base) & ConstraintSet.upper_bound(U, Base)) ``` The union of a constraint and its negation should always be satisfiable. ```py def _[T, U]() -> None: - c1 = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base) + c1 = ConstraintSet.upper_bound(T, Base) & ConstraintSet.upper_bound(U, Base) static_assert(c1 | ~c1) static_assert(~c1 | c1) - c2 = ConstraintSet.range(Unrelated, T, object) & ConstraintSet.range(Unrelated, U, object) + c2 = ConstraintSet.lower_bound(Unrelated, T) & ConstraintSet.lower_bound(Unrelated, U) static_assert(c2 | ~c2) static_assert(~c2 | c2) @@ -733,20 +784,19 @@ being constrained. The other is then the lower or upper bound of the constraint. enforce an arbitrary ordering on typevars, and always place the constraint on the "earlier" typevar. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def f[S, T](): # (S@f ≤ T@f) - c1 = ConstraintSet.range(Never, S, T) - c2 = ConstraintSet.range(S, T, object) + c1 = ConstraintSet.upper_bound(S, T) + c2 = ConstraintSet.lower_bound(S, T) static_assert(c1 == c2) def f[T, S](): # (S@f ≤ T@f) - c1 = ConstraintSet.range(Never, S, T) - c2 = ConstraintSet.range(S, T, object) + c1 = ConstraintSet.upper_bound(S, T) + c2 = ConstraintSet.lower_bound(S, T) static_assert(c1 == c2) ``` @@ -756,14 +806,14 @@ the constraint, and the other the bound. ```py def f[S, T](): # (S@f = T@f) - c1 = ConstraintSet.range(T, S, T) - c2 = ConstraintSet.range(S, T, S) + c1 = ConstraintSet.equality(S, T) + c2 = ConstraintSet.equality(T, S) static_assert(c1 == c2) def f[T, S](): # (S@f = T@f) - c1 = ConstraintSet.range(T, S, T) - c2 = ConstraintSet.range(S, T, S) + c1 = ConstraintSet.equality(S, T) + c2 = ConstraintSet.equality(T, S) static_assert(c1 == c2) ``` @@ -788,17 +838,16 @@ The ordering of elements in a union or intersection do not affect what types sat set. ```pyi -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def f[T](): - c1 = ConstraintSet.range(Never, T, str | int) - c2 = ConstraintSet.range(Never, T, int | str) + c1 = ConstraintSet.upper_bound(T, str | int) + c2 = ConstraintSet.upper_bound(T, int | str) static_assert(c1 == c2) - c1 = ConstraintSet.range(Never, T, str & int) - c2 = ConstraintSet.range(Never, T, int & str) + c1 = ConstraintSet.upper_bound(T, str & int) + c2 = ConstraintSet.upper_bound(T, int & str) static_assert(c1 == c2) ``` @@ -815,15 +864,15 @@ from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def same_typevar[T](): - constraints = ConstraintSet.range(Never, T, T) + constraints = ConstraintSet.upper_bound(T, T) expected = ConstraintSet.range(Never, T, object) static_assert(constraints == expected) - constraints = ConstraintSet.range(T, T, object) + constraints = ConstraintSet.lower_bound(T, T) expected = ConstraintSet.range(Never, T, object) static_assert(constraints == expected) - constraints = ConstraintSet.range(T, T, T) + constraints = ConstraintSet.equality(T, T) expected = ConstraintSet.range(Never, T, object) static_assert(constraints == expected) ``` @@ -834,11 +883,11 @@ as shown above.) ```pyi def same_typevar[T](): - constraints = ConstraintSet.range(Never, T, T | None) + constraints = ConstraintSet.upper_bound(T, T | None) expected = ConstraintSet.range(Never, T, object) static_assert(constraints == expected) - constraints = ConstraintSet.range(T & None, T, object) + constraints = ConstraintSet.lower_bound(T & None, T) expected = ConstraintSet.range(Never, T, object) static_assert(constraints == expected) @@ -852,11 +901,11 @@ constraint set can never be satisfied, since every type is disjoint with its neg ```pyi def same_typevar[T](): - constraints = ConstraintSet.range(~T & None, T, object) + constraints = ConstraintSet.lower_bound(~T & None, T) expected = ~ConstraintSet.range(Never, T, object) static_assert(constraints == expected) - constraints = ConstraintSet.range(~T, T, object) + constraints = ConstraintSet.lower_bound(~T, T) expected = ~ConstraintSet.range(Never, T, object) static_assert(constraints == expected) ``` @@ -868,24 +917,23 @@ do not involve those typevars must remain in the result. The result holds whenev valid assignment to the quantified variables satisfies the expression being quantified over. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def preserves_remaining_conjunct[T, U]() -> None: - t_int = ConstraintSet.range(int, T, int) - u_str = ConstraintSet.range(str, U, str) + t_int = ConstraintSet.equality(T, int) + u_str = ConstraintSet.equality(U, str) quantified = (t_int & u_str).exists(tuple[U]) static_assert(quantified == t_int) def satisfies_uncertain_disjunct[T, U]() -> None: - t_int = ConstraintSet.range(int, T, int) - u_str = ConstraintSet.range(str, U, str) + t_int = ConstraintSet.equality(T, int) + u_str = ConstraintSet.equality(U, str) quantified = (t_int | u_str).exists(tuple[U]) static_assert(quantified == ConstraintSet.always()) def no_typevars_is_identity[T]() -> None: - constraints = ConstraintSet.range(Never, T, int) + constraints = ConstraintSet.upper_bound(T, int) static_assert(constraints.exists(tuple[()]) == constraints) ``` @@ -896,24 +944,23 @@ not involve those typevars must remain in the result. The result holds whenever assignment to the quantified variables satisfies the expression being quantified over. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def preserves_uncertain_disjunct[T, U]() -> None: - t_int = ConstraintSet.range(int, T, int) - u_str = ConstraintSet.range(str, U, str) + t_int = ConstraintSet.equality(T, int) + u_str = ConstraintSet.equality(U, str) quantified = (t_int | u_str).for_all(tuple[U]) static_assert(quantified == t_int) def removes_multiple_typevars[T, U]() -> None: - t_int = ConstraintSet.range(int, T, int) - u_str = ConstraintSet.range(str, U, str) + t_int = ConstraintSet.equality(T, int) + u_str = ConstraintSet.equality(U, str) quantified = (t_int | u_str).for_all(tuple[T, U]) static_assert(quantified == ConstraintSet.never()) def no_typevars_is_identity[T]() -> None: - constraints = ConstraintSet.range(Never, T, int) + constraints = ConstraintSet.upper_bound(T, int) static_assert(constraints.for_all(tuple[()]) == constraints) ``` @@ -926,8 +973,8 @@ from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def quantifier_order[S, T]() -> None: - source_is_int = ConstraintSet.range(int, S, int) - target_is_int = ConstraintSet.range(int, T, int) + source_is_int = ConstraintSet.equality(S, int) + target_is_int = ConstraintSet.equality(T, int) equal = source_is_int.satisfies(target_is_int) & target_is_int.satisfies(source_is_int) # ∀T.∃S.equal(S, T) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md index 1cd7451c47..7ee22c47e0 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md @@ -31,11 +31,10 @@ Moreover, for concrete types, the answer does not depend on which constraint set there isn't a valid specialization for the typevars we are considering. ```py -from typing import Never from ty_extensions._internal import ConstraintSet def even_given_constraints[T](): - constraints = ConstraintSet.range(Never, T, int) + constraints = ConstraintSet.upper_bound(T, int) static_assert(constraints.implies_subtype_of(bool, int)) static_assert(not constraints.implies_subtype_of(bool, str)) @@ -62,7 +61,7 @@ def assignability[T](): static_assert(constraints == expected) constraints = is_constraint_set_assignable_to(T, bool) - expected = ConstraintSet.range(Never, T, bool) + expected = ConstraintSet.upper_bound(T, bool) static_assert(constraints == expected) # TODO: is_assignable_to should eventually work the way is_constraint_set_assignable_to does @@ -72,7 +71,7 @@ def assignability[T](): static_assert(constraints == expected) constraints = is_constraint_set_assignable_to(T, int) - expected = ConstraintSet.range(Never, T, int) + expected = ConstraintSet.upper_bound(T, int) static_assert(constraints == expected) constraints = is_assignable_to(T, object) @@ -85,12 +84,12 @@ def assignability[T](): def subtyping[T](): constraints = is_subtype_of(T, bool) - # TODO: expected = ConstraintSet.range(Never, T, bool) + # TODO: expected = ConstraintSet.upper_bound(T, bool) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_subtype_of(T, int) - # TODO: expected = ConstraintSet.range(Never, T, int) + # TODO: expected = ConstraintSet.upper_bound(T, int) expected = ConstraintSet.never() static_assert(constraints == expected) @@ -123,53 +122,53 @@ def assignability[T](): static_assert(constraints == expected) constraints = is_assignable_to(T, Covariant[Any]) - # TODO: expected = ConstraintSet.range(Never, T, Covariant[object]) + # TODO: expected = ConstraintSet.upper_bound(T, Covariant[object]) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_assignable_to(Covariant[Any], T) - # TODO: expected = ConstraintSet.range(Covariant[Never], T, object) + # TODO: expected = ConstraintSet.lower_bound(Covariant[Never], T) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_assignable_to(T, Contravariant[Any]) - # TODO: expected = ConstraintSet.range(Never, T, Contravariant[Never]) + # TODO: expected = ConstraintSet.upper_bound(T, Contravariant[Never]) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_assignable_to(Contravariant[Any], T) - # TODO: expected = ConstraintSet.range(Contravariant[object], T, object) + # TODO: expected = ConstraintSet.lower_bound(Contravariant[object], T) expected = ConstraintSet.never() static_assert(constraints == expected) def subtyping[T](): constraints = is_subtype_of(T, Any) - # TODO: expected = ConstraintSet.range(Never, T, Never) + # TODO: expected = ConstraintSet.equality(T, Never) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_subtype_of(Any, T) - # TODO: expected = ConstraintSet.range(object, T, object) + # TODO: expected = ConstraintSet.equality(T, object) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_subtype_of(T, Covariant[Any]) - # TODO: expected = ConstraintSet.range(Never, T, Covariant[Never]) + # TODO: expected = ConstraintSet.upper_bound(T, Covariant[Never]) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_subtype_of(Covariant[Any], T) - # TODO: expected = ConstraintSet.range(Covariant[object], T, object) + # TODO: expected = ConstraintSet.lower_bound(Covariant[object], T) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_subtype_of(T, Contravariant[Any]) - # TODO: expected = ConstraintSet.range(Never, T, Contravariant[object]) + # TODO: expected = ConstraintSet.upper_bound(T, Contravariant[object]) expected = ConstraintSet.never() static_assert(constraints == expected) constraints = is_subtype_of(Contravariant[Any], T) - # TODO: expected = ConstraintSet.range(Contravariant[Never], T, object) + # TODO: expected = ConstraintSet.lower_bound(Contravariant[Never], T) expected = ConstraintSet.never() static_assert(constraints == expected) ``` @@ -193,12 +192,12 @@ def given_constraints[T](): static_assert(ConstraintSet.never().implies_subtype_of(T, bool)) static_assert(ConstraintSet.never().implies_subtype_of(T, str)) - given_int = ConstraintSet.range(Never, T, int) + given_int = ConstraintSet.upper_bound(T, int) static_assert(given_int.implies_subtype_of(T, int)) static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) - given_bool = ConstraintSet.range(Never, T, bool) + given_bool = ConstraintSet.upper_bound(T, bool) static_assert(given_bool.implies_subtype_of(T, int)) static_assert(given_bool.implies_subtype_of(T, bool)) static_assert(not given_bool.implies_subtype_of(T, str)) @@ -208,7 +207,7 @@ def given_constraints[T](): static_assert(given_both.implies_subtype_of(T, bool)) static_assert(not given_both.implies_subtype_of(T, str)) - given_str = ConstraintSet.range(Never, T, str) + given_str = ConstraintSet.upper_bound(T, str) static_assert(not given_str.implies_subtype_of(T, int)) static_assert(not given_str.implies_subtype_of(T, bool)) static_assert(given_str.implies_subtype_of(T, str)) @@ -222,26 +221,26 @@ BDD logic that is dependent on which variable ordering we end up with.) ```py def mutually_constrained[T, U](): # If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well. - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(T, int)) static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) # If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well. - given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(T, int)) static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) def mutually_constrained[U, T](): # If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well. - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(T, int)) static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) # If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well. - given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(T, int)) static_assert(not given_int.implies_subtype_of(T, bool)) static_assert(not given_int.implies_subtype_of(T, str)) @@ -252,7 +251,6 @@ def mutually_constrained[U, T](): All of the relationships in the above section also apply when a typevar appears in a compound type. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -271,12 +269,12 @@ def given_constraints[T](): static_assert(ConstraintSet.never().implies_subtype_of(Covariant[T], Covariant[str])) # For a covariant typevar, (T ≤ int) implies that (Covariant[T] ≤ Covariant[int]). - given_int = ConstraintSet.range(Never, T, int) + given_int = ConstraintSet.upper_bound(T, int) static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str])) - given_bool = ConstraintSet.range(Never, T, bool) + given_bool = ConstraintSet.upper_bound(T, bool) static_assert(given_bool.implies_subtype_of(Covariant[T], Covariant[int])) static_assert(given_bool.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_bool.implies_subtype_of(Covariant[T], Covariant[str])) @@ -289,14 +287,14 @@ def given_constraints[T](): def mutually_constrained[T, U](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Covariant[T] ≤ Covariant[int]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str])) # If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Covariant[T] ≤ Covariant[int]). - given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str])) @@ -305,14 +303,14 @@ def mutually_constrained[T, U](): def mutually_constrained[U, T](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Covariant[T] ≤ Covariant[int]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str])) # If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Covariant[T] ≤ Covariant[int]). - given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Covariant[T], Covariant[int])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[bool])) static_assert(not given_int.implies_subtype_of(Covariant[T], Covariant[str])) @@ -337,12 +335,12 @@ def given_constraints[T](): # For a contravariant typevar, (T ≤ int) implies that (Contravariant[int] ≤ Contravariant[T]). # (The order of the comparison is reversed because of contravariance.) - given_int = ConstraintSet.range(Never, T, int) + given_int = ConstraintSet.upper_bound(T, int) static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T])) - given_bool = ConstraintSet.range(Never, T, int) + given_bool = ConstraintSet.upper_bound(T, int) static_assert(given_bool.implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not given_bool.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_bool.implies_subtype_of(Contravariant[str], Contravariant[T])) @@ -350,14 +348,14 @@ def given_constraints[T](): def mutually_constrained[T, U](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Contravariant[int] ≤ Contravariant[T]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T])) # If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Contravariant[int] ≤ Contravariant[T]). - given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T])) @@ -366,14 +364,14 @@ def mutually_constrained[T, U](): def mutually_constrained[U, T](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Contravariant[int] ≤ Contravariant[T]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T])) # If (T ≤ U ∧ U ≤ int), then (T ≤ int) must be true as well, and therefore # (Contravariant[int] ≤ Contravariant[T]). - given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.upper_bound(T, U) & ConstraintSet.upper_bound(U, int) static_assert(given_int.implies_subtype_of(Contravariant[int], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[bool], Contravariant[T])) static_assert(not given_int.implies_subtype_of(Contravariant[str], Contravariant[T])) @@ -401,7 +399,7 @@ def given_constraints[T](): static_assert(ConstraintSet.never().implies_subtype_of(Invariant[T], Invariant[str])) # For an invariant typevar, (T ≤ int) does not imply that (Invariant[T] ≤ Invariant[int]). - given_int = ConstraintSet.range(Never, T, int) + given_int = ConstraintSet.upper_bound(T, int) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[int])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str])) @@ -412,7 +410,7 @@ def given_constraints[T](): static_assert(not given_int.implies_subtype_of(Invariant[str], Invariant[T])) # But (T = int) does imply both. - given_int = ConstraintSet.range(int, T, int) + given_int = ConstraintSet.equality(T, int) static_assert(given_int.implies_subtype_of(Invariant[T], Invariant[int])) static_assert(given_int.implies_subtype_of(Invariant[int], Invariant[T])) static_assert(not given_int.implies_subtype_of(Invariant[bool], Invariant[T])) @@ -423,14 +421,14 @@ def given_constraints[T](): def mutually_constrained[T, U](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well. But because T is invariant, that # does _not_ imply that (Invariant[T] ≤ Invariant[int]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[int])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str])) # If (T = U ∧ U = int), then (T = int) must be true as well. That is an equality constraint, so # even though T is invariant, it does imply that (Invariant[T] ≤ Invariant[int]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(int, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.equality(U, int) static_assert(given_int.implies_subtype_of(Invariant[T], Invariant[int])) static_assert(given_int.implies_subtype_of(Invariant[int], Invariant[T])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool])) @@ -442,14 +440,14 @@ def mutually_constrained[T, U](): def mutually_constrained[U, T](): # If (T = U ∧ U ≤ int), then (T ≤ int) must be true as well. But because T is invariant, that # does _not_ imply that (Invariant[T] ≤ Invariant[int]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.upper_bound(U, int) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[int])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[str])) # If (T = U ∧ U = int), then (T = int) must be true as well. That is an equality constraint, so # even though T is invariant, it does imply that (Invariant[T] ≤ Invariant[int]). - given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(int, U, int) + given_int = ConstraintSet.equality(T, U) & ConstraintSet.equality(U, int) static_assert(given_int.implies_subtype_of(Invariant[T], Invariant[int])) static_assert(given_int.implies_subtype_of(Invariant[int], Invariant[T])) static_assert(not given_int.implies_subtype_of(Invariant[T], Invariant[bool])) @@ -571,7 +569,7 @@ def quantifies_callable_typevars_together[V](): raise NotImplementedError actual = ConstraintSet.always().implies_subtype_of(RegularCallableTypeOf[source], RegularCallableTypeOf[target]) - expected = ConstraintSet.range(int, V, object) + expected = ConstraintSet.lower_bound(int, V) static_assert(actual == expected) ``` @@ -588,7 +586,7 @@ def listify[T](t: T) -> list[T]: return [t] def constrained_by_other_typevars[U, V]() -> None: - ok = ConstraintSet.range(bool, U, int) & ConstraintSet.range(int, V, int) + ok = ConstraintSet.range(bool, U, int) & ConstraintSet.equality(V, int) # TODO: no error # This does not depend on combining constraints from multiple call arguments. The callable # relation introduces constraints involving listify's fresh typevar and then existentially @@ -598,7 +596,7 @@ def constrained_by_other_typevars[U, V]() -> None: # error: [static-assert-error] static_assert(ok.implies_subtype_of(TypeOf[listify], Callable[[U], list[V]])) - bad = ConstraintSet.range(str, U, str) & ConstraintSet.range(int, V, int) + bad = ConstraintSet.equality(U, str) & ConstraintSet.equality(V, int) static_assert(not bad.implies_subtype_of(TypeOf[listify], Callable[[U], list[V]])) def recursive_listify[T](t: T) -> list[T]: @@ -616,40 +614,38 @@ def recursive_listify[T](t: T) -> list[T]: ### Transitivity can propagate across typevars ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def concrete_pivot[T, U](): # If [int ≤ T ∧ T ≤ U], then [int ≤ U] must be true as well. - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(T, U, object) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.lower_bound(T, U) static_assert(constraints.implies_subtype_of(int, U)) ``` ### Transitivity can propagate across fully static concrete types ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def concrete_pivot[T, U](): # If [T ≤ int ∧ int ≤ U], then [T ≤ U] must be true as well. - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(int, U, object) + constraints = ConstraintSet.upper_bound(T, int) & ConstraintSet.lower_bound(int, U) static_assert(constraints.implies_subtype_of(T, U)) ``` ### Transitivity cannot propagate across non-fully-static concrete types ```py -from typing import Any, Never +from typing import Any from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def concrete_pivot[T, U](): # If [T ≤ Any ∧ Any ≤ U], then the two `Any`s might materialize to different types. That means # [T ≤ U] is NOT necessarily true. - constraints = ConstraintSet.range(Never, T, Any) & ConstraintSet.range(Any, U, object) + constraints = ConstraintSet.upper_bound(T, Any) & ConstraintSet.lower_bound(Any, U) static_assert(not constraints.implies_subtype_of(T, U)) ``` @@ -659,7 +655,6 @@ When a typevar appears nested inside a covariant generic type in another constra propagate the bound "into" the generic type. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -670,7 +665,7 @@ class Covariant[T]: def upper_bound[T, U](): # If (T ≤ int) ∧ (U ≤ Covariant[T]), then by covariance, Covariant[T] ≤ Covariant[int], # and by transitivity, U ≤ Covariant[int]. - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Never, U, Covariant[T]) + constraints = ConstraintSet.upper_bound(T, int) & ConstraintSet.upper_bound(U, Covariant[T]) static_assert(constraints.implies_subtype_of(U, Covariant[int])) static_assert(not constraints.implies_subtype_of(U, Covariant[bool])) static_assert(not constraints.implies_subtype_of(U, Covariant[str])) @@ -678,21 +673,21 @@ def upper_bound[T, U](): def lower_bound[T, U](): # If (int ≤ T ∧ Covariant[T] ≤ U), then by covariance, Covariant[int] ≤ Covariant[T], # and by transitivity, Covariant[int] ≤ U. Since bool ≤ int, Covariant[bool] ≤ U also holds. - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Covariant[T], U, object) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.lower_bound(Covariant[T], U) static_assert(constraints.implies_subtype_of(Covariant[int], U)) static_assert(constraints.implies_subtype_of(Covariant[bool], U)) static_assert(not constraints.implies_subtype_of(Covariant[str], U)) # Repeat with reversed typevar ordering to verify BDD-ordering independence. def upper_bound[U, T](): - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Never, U, Covariant[T]) + constraints = ConstraintSet.upper_bound(T, int) & ConstraintSet.upper_bound(U, Covariant[T]) static_assert(constraints.implies_subtype_of(U, Covariant[int])) static_assert(not constraints.implies_subtype_of(U, Covariant[bool])) static_assert(not constraints.implies_subtype_of(U, Covariant[str])) def lower_bound[U, T](): # Since bool ≤ int, Covariant[bool] ≤ U also holds. - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Covariant[T], U, object) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.lower_bound(Covariant[T], U) static_assert(constraints.implies_subtype_of(Covariant[int], U)) static_assert(constraints.implies_subtype_of(Covariant[bool], U)) static_assert(not constraints.implies_subtype_of(Covariant[str], U)) @@ -704,7 +699,6 @@ The previous section also works for contravariant generic types, though one of t constraints is flipped. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -718,7 +712,7 @@ def upper_bound[T, U](): # Note: we need the *lower* bound on T (not the upper) because contravariance flips. # Since bool ≤ int, Contravariant[int] ≤ Contravariant[bool], so U ≤ Contravariant[bool] # also holds. - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Never, U, Contravariant[T]) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.upper_bound(U, Contravariant[T]) static_assert(constraints.implies_subtype_of(U, Contravariant[int])) static_assert(constraints.implies_subtype_of(U, Contravariant[bool])) static_assert(not constraints.implies_subtype_of(U, Contravariant[str])) @@ -728,20 +722,20 @@ def lower_bound[T, U](): # Contravariant[int] ≤ Contravariant[T], and by transitivity, Contravariant[int] ≤ U. # Contravariant[bool] is a supertype of Contravariant[int] (since bool ≤ int), so # Contravariant[bool] ≤ U does NOT hold. - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Contravariant[T], U, object) + constraints = ConstraintSet.upper_bound(T, int) & ConstraintSet.lower_bound(Contravariant[T], U) static_assert(constraints.implies_subtype_of(Contravariant[int], U)) static_assert(not constraints.implies_subtype_of(Contravariant[bool], U)) static_assert(not constraints.implies_subtype_of(Contravariant[str], U)) # Repeat with reversed typevar ordering to verify BDD-ordering independence. def upper_bound[U, T](): - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Never, U, Contravariant[T]) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.upper_bound(U, Contravariant[T]) static_assert(constraints.implies_subtype_of(U, Contravariant[int])) static_assert(constraints.implies_subtype_of(U, Contravariant[bool])) static_assert(not constraints.implies_subtype_of(U, Contravariant[str])) def lower_bound[U, T](): - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Contravariant[T], U, object) + constraints = ConstraintSet.upper_bound(T, int) & ConstraintSet.lower_bound(Contravariant[T], U) static_assert(constraints.implies_subtype_of(Contravariant[int], U)) static_assert(not constraints.implies_subtype_of(Contravariant[bool], U)) static_assert(not constraints.implies_subtype_of(Contravariant[str], U)) @@ -753,7 +747,6 @@ For invariant type parameters, only an equality constraint on the typevar allows one-sided bound (upper or lower only) is not sufficient. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -766,7 +759,7 @@ class Invariant[T]: def equality_constraint[T, U](): # (T = int ∧ U ≤ Invariant[T]) should imply U ≤ Invariant[int]. - constraints = ConstraintSet.range(int, T, int) & ConstraintSet.range(Never, U, Invariant[T]) + constraints = ConstraintSet.equality(T, int) & ConstraintSet.upper_bound(U, Invariant[T]) static_assert(constraints.implies_subtype_of(U, Invariant[int])) static_assert(not constraints.implies_subtype_of(U, Invariant[bool])) static_assert(not constraints.implies_subtype_of(U, Invariant[str])) @@ -774,7 +767,7 @@ def equality_constraint[T, U](): def upper_bound_only[T, U](): # (T ≤ int ∧ U ≤ Invariant[T]) should NOT imply U ≤ Invariant[int], because T is invariant # and we only have an upper bound, not equality. - constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(Never, U, Invariant[T]) + constraints = ConstraintSet.upper_bound(T, int) & ConstraintSet.upper_bound(U, Invariant[T]) static_assert(not constraints.implies_subtype_of(U, Invariant[int])) static_assert(not constraints.implies_subtype_of(U, Invariant[bool])) static_assert(not constraints.implies_subtype_of(U, Invariant[str])) @@ -782,14 +775,14 @@ def upper_bound_only[T, U](): def lower_bound_only[T, U](): # (int ≤ T ∧ Invariant[T] ≤ U) should NOT imply Invariant[int] ≤ U, because T is invariant # and we only have a lower bound, not equality. - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Invariant[T], U, object) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.lower_bound(Invariant[T], U) static_assert(not constraints.implies_subtype_of(Invariant[int], U)) static_assert(not constraints.implies_subtype_of(Invariant[bool], U)) static_assert(not constraints.implies_subtype_of(Invariant[str], U)) # Repeat with reversed typevar ordering. def equality_constraint[U, T](): - constraints = ConstraintSet.range(int, T, int) & ConstraintSet.range(Never, U, Invariant[T]) + constraints = ConstraintSet.equality(T, int) & ConstraintSet.upper_bound(U, Invariant[T]) static_assert(constraints.implies_subtype_of(U, Invariant[int])) static_assert(not constraints.implies_subtype_of(U, Invariant[bool])) static_assert(not constraints.implies_subtype_of(U, Invariant[str])) @@ -801,7 +794,6 @@ When a typevar is nested inside multiple layers of generics, variances compose. covariant type inside a contravariant type yields contravariant overall. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -816,25 +808,25 @@ class Contravariant[T]: def covariant_of_contravariant[T, U](): # Covariant[Contravariant[T]]: T is contravariant overall (covariant × contravariant). # So a lower bound on T should propagate (flipped). - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Never, U, Covariant[Contravariant[T]]) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.upper_bound(U, Covariant[Contravariant[T]]) static_assert(constraints.implies_subtype_of(U, Covariant[Contravariant[int]])) static_assert(not constraints.implies_subtype_of(U, Covariant[Contravariant[str]])) def contravariant_of_covariant[T, U](): # Contravariant[Covariant[T]]: T is contravariant overall (contravariant × covariant). # So a lower bound on T should propagate (flipped). - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Never, U, Contravariant[Covariant[T]]) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.upper_bound(U, Contravariant[Covariant[T]]) static_assert(constraints.implies_subtype_of(U, Contravariant[Covariant[int]])) static_assert(not constraints.implies_subtype_of(U, Contravariant[Covariant[str]])) # Repeat with reversed typevar ordering. def covariant_of_contravariant[U, T](): - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Never, U, Covariant[Contravariant[T]]) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.upper_bound(U, Covariant[Contravariant[T]]) static_assert(constraints.implies_subtype_of(U, Covariant[Contravariant[int]])) static_assert(not constraints.implies_subtype_of(U, Covariant[Contravariant[str]])) def contravariant_of_covariant[U, T](): - constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(Never, U, Contravariant[Covariant[T]]) + constraints = ConstraintSet.lower_bound(int, T) & ConstraintSet.upper_bound(U, Contravariant[Covariant[T]]) static_assert(constraints.implies_subtype_of(U, Contravariant[Covariant[int]])) static_assert(not constraints.implies_subtype_of(U, Contravariant[Covariant[str]])) ``` @@ -851,7 +843,6 @@ For example, `(Covariant[S] ≤ C) ∧ (S ≤ B)` should imply `Covariant[B] ≤ typevars.) ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -873,42 +864,42 @@ class Invariant[T]: def covariant_upper_bound_into_lower[S, B, C](): # (Covariant[S] ≤ C) ∧ (B ≤ S) → (Covariant[B] ≤ C) # B ≤ S, so Covariant[B] ≤ Covariant[S], and Covariant[S] ≤ C gives Covariant[B] ≤ C. - constraints = ConstraintSet.range(Covariant[S], C, object) & ConstraintSet.range(Never, B, S) + constraints = ConstraintSet.lower_bound(Covariant[S], C) & ConstraintSet.upper_bound(B, S) static_assert(constraints.implies_subtype_of(Covariant[B], C)) def covariant_lower_bound_into_upper[S, B, C](): # (C ≤ Covariant[S]) ∧ (S ≤ B) → (C ≤ Covariant[B]) # S ≤ B, so Covariant[S] ≤ Covariant[B], and C ≤ Covariant[S] ≤ Covariant[B]. - constraints = ConstraintSet.range(Never, C, Covariant[S]) & ConstraintSet.range(S, B, object) + constraints = ConstraintSet.upper_bound(C, Covariant[S]) & ConstraintSet.lower_bound(S, B) static_assert(constraints.implies_subtype_of(C, Covariant[B])) def contravariant_upper_bound_into_lower[S, B, C](): # (Contravariant[S] ≤ C) ∧ (S ≤ B) → (Contravariant[B] ≤ C) # S ≤ B gives Contravariant[B] ≤ Contravariant[S], so Contravariant[B] ≤ Contravariant[S] ≤ C. - constraints = ConstraintSet.range(Contravariant[S], C, object) & ConstraintSet.range(S, B, object) + constraints = ConstraintSet.lower_bound(Contravariant[S], C) & ConstraintSet.lower_bound(S, B) static_assert(constraints.implies_subtype_of(Contravariant[B], C)) def contravariant_lower_bound_into_upper[S, B, C](): # (C ≤ Contravariant[S]) ∧ (B ≤ S) → (C ≤ Contravariant[B]) # B ≤ S gives Contravariant[S] ≤ Contravariant[B], so C ≤ Contravariant[S] ≤ Contravariant[B]. - constraints = ConstraintSet.range(Never, C, Contravariant[S]) & ConstraintSet.range(Never, B, S) + constraints = ConstraintSet.upper_bound(C, Contravariant[S]) & ConstraintSet.upper_bound(B, S) static_assert(constraints.implies_subtype_of(C, Contravariant[B])) # Repeat with reversed typevar ordering. def covariant_upper_bound_into_lower[C, B, S](): - constraints = ConstraintSet.range(Covariant[S], C, object) & ConstraintSet.range(Never, B, S) + constraints = ConstraintSet.lower_bound(Covariant[S], C) & ConstraintSet.upper_bound(B, S) static_assert(constraints.implies_subtype_of(Covariant[B], C)) def covariant_lower_bound_into_upper[C, B, S](): - constraints = ConstraintSet.range(Never, C, Covariant[S]) & ConstraintSet.range(S, B, object) + constraints = ConstraintSet.upper_bound(C, Covariant[S]) & ConstraintSet.lower_bound(S, B) static_assert(constraints.implies_subtype_of(C, Covariant[B])) def contravariant_upper_bound_into_lower[C, B, S](): - constraints = ConstraintSet.range(Contravariant[S], C, object) & ConstraintSet.range(S, B, object) + constraints = ConstraintSet.lower_bound(Contravariant[S], C) & ConstraintSet.lower_bound(S, B) static_assert(constraints.implies_subtype_of(Contravariant[B], C)) def contravariant_lower_bound_into_upper[C, B, S](): - constraints = ConstraintSet.range(Never, C, Contravariant[S]) & ConstraintSet.range(Never, B, S) + constraints = ConstraintSet.upper_bound(C, Contravariant[S]) & ConstraintSet.upper_bound(B, S) static_assert(constraints.implies_subtype_of(C, Contravariant[B])) ``` @@ -919,7 +910,6 @@ When B's bound _contains_ a typevar (but is not a bare typevar), the same logic TODO: This is not implemented yet, since it requires different detection machinery. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -929,14 +919,14 @@ class Covariant[T]: def upper_bound_into_lower[B, C](): # (Covariant[int] ≤ C) ∧ (B ≤ int) → (Covariant[B] ≤ C) - constraints = ConstraintSet.range(Covariant[int], C, object) & ConstraintSet.range(Never, B, int) + constraints = ConstraintSet.lower_bound(Covariant[int], C) & ConstraintSet.upper_bound(B, int) # TODO: no error # error: [static-assert-error] static_assert(constraints.implies_subtype_of(Covariant[B], C)) def lower_bound_into_upper[B, C](): # (C ≤ Covariant[int]) ∧ (int ≤ B) → (C ≤ Covariant[B]) - constraints = ConstraintSet.range(Never, C, Covariant[int]) & ConstraintSet.range(int, B, object) + constraints = ConstraintSet.upper_bound(C, Covariant[int]) & ConstraintSet.lower_bound(int, B) # TODO: no error # error: [static-assert-error] static_assert(constraints.implies_subtype_of(C, Covariant[B])) @@ -945,7 +935,6 @@ def lower_bound_into_upper[B, C](): ### Nested typevar propagation also works when the replacement is a bare typevar ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -966,39 +955,39 @@ class Invariant[T]: def covariant_upper[B, S, U](): # (B ≤ S) ∧ (U ≤ Covariant[B]) -> (U ≤ Covariant[S]) - constraints = ConstraintSet.range(Never, B, S) & ConstraintSet.range(Never, U, Covariant[B]) + constraints = ConstraintSet.upper_bound(B, S) & ConstraintSet.upper_bound(U, Covariant[B]) static_assert(constraints.implies_subtype_of(U, Covariant[S])) def covariant_lower[B, S, U](): # (S ≤ B) ∧ (Covariant[B] ≤ U) -> (Covariant[S] ≤ U) - constraints = ConstraintSet.range(S, B, object) & ConstraintSet.range(Covariant[B], U, object) + constraints = ConstraintSet.lower_bound(S, B) & ConstraintSet.lower_bound(Covariant[B], U) static_assert(constraints.implies_subtype_of(Covariant[S], U)) def contravariant_upper[B, S, U](): # (S ≤ B) ∧ (U ≤ Contravariant[B]) -> (U ≤ Contravariant[S]) - constraints = ConstraintSet.range(S, B, object) & ConstraintSet.range(Never, U, Contravariant[B]) + constraints = ConstraintSet.lower_bound(S, B) & ConstraintSet.upper_bound(U, Contravariant[B]) static_assert(constraints.implies_subtype_of(U, Contravariant[S])) def contravariant_lower[B, S, U](): # (B ≤ S) ∧ (Contravariant[B] ≤ U) -> (Contravariant[S] ≤ U) - constraints = ConstraintSet.range(Never, B, S) & ConstraintSet.range(Contravariant[B], U, object) + constraints = ConstraintSet.upper_bound(B, S) & ConstraintSet.lower_bound(Contravariant[B], U) static_assert(constraints.implies_subtype_of(Contravariant[S], U)) def invariant_upper_requires_equality[B, S, U](): # Invariant replacement only holds under equality constraints on B. - constraints = ConstraintSet.range(S, B, S) & ConstraintSet.range(Never, U, Invariant[B]) + constraints = ConstraintSet.equality(B, S) & ConstraintSet.upper_bound(U, Invariant[B]) static_assert(constraints.implies_subtype_of(U, Invariant[S])) def invariant_lower_requires_equality[B, S, U](): - constraints = ConstraintSet.range(S, B, S) & ConstraintSet.range(Invariant[B], U, object) + constraints = ConstraintSet.equality(B, S) & ConstraintSet.lower_bound(Invariant[B], U) static_assert(constraints.implies_subtype_of(Invariant[S], U)) def invariant_upper_one_sided_is_not_enough[B, S, U](): - constraints = ConstraintSet.range(Never, B, S) & ConstraintSet.range(Never, U, Invariant[B]) + constraints = ConstraintSet.upper_bound(B, S) & ConstraintSet.upper_bound(U, Invariant[B]) static_assert(not constraints.implies_subtype_of(U, Invariant[S])) def invariant_lower_one_sided_is_not_enough[B, S, U](): - constraints = ConstraintSet.range(S, B, object) & ConstraintSet.range(Invariant[B], U, object) + constraints = ConstraintSet.lower_bound(S, B) & ConstraintSet.lower_bound(Invariant[B], U) static_assert(not constraints.implies_subtype_of(Invariant[S], U)) ``` @@ -1010,7 +999,6 @@ can decompose the bounds to extract constraints on the nested typevar. For insta `Covariant[int] ≤ Covariant[T]` requires `int ≤ T`. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -1091,14 +1079,14 @@ def subclass_lower_bound[T, A](): ### Transitivity should not introduce impossible constraints ```py -from typing import Never, TypeVar, Union +from typing import TypeVar, Union from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def impossible_result[A, T, U](): constraint_a = ConstraintSet.range(int, A, Union[T, U]) - constraint_t = ConstraintSet.range(Never, T, str) - constraint_u = ConstraintSet.range(Never, U, bytes) + constraint_t = ConstraintSet.upper_bound(T, str) + constraint_u = ConstraintSet.upper_bound(U, bytes) # Given (int ≤ A ≤ T | U), we can infer that (int ≤ T) ∨ (int ≤ U). If we intersect that with # (T ≤ str), we get false ∨ (int ≤ U) — that is, there is no valid solution for T. Therefore A diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 985d118c88..061f8285ca 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -1703,7 +1703,7 @@ class OuterCarrier[A_outer]: def check[R](self) -> None: actual = is_constraint_set_assignable_to(RegularCallableTypeOf[OuterCarrier[A_outer].method], Callable[..., R]) - expected = ConstraintSet.range(A_outer, R, object) + expected = ConstraintSet.lower_bound(A_outer, R) static_assert(actual == expected) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md b/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md index f4d4c1a1d9..f8d1cd2434 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/quantification.md @@ -30,7 +30,6 @@ implicit upper bound of `object`), but the only _satisfying_ assignment is `X = result should be equivalent to `A ≤ Invariant[int]`. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -42,18 +41,18 @@ class Invariant[T]: def grounded[X, A]() -> None: # ∃X. X = int ∧ A ≤ Invariant[X] - body = ConstraintSet.range(int, X, int) & ConstraintSet.range(Never, A, Invariant[X]) + body = ConstraintSet.equality(X, int) & ConstraintSet.upper_bound(A, Invariant[X]) quantified = body.exists(tuple[X]) # TODO: revealed: tuple[Solution[X=int, A=list[int]]] - # revealed: tuple[Solution[X=int, A=Never]] + # revealed: tuple[Solution[X=int, A=Invariant[int] & Invariant[X@grounded]]] reveal_type(body.solutions(inferable=tuple[X, A])) # TODO: revealed: tuple[Solution[A=list[int]]] - # revealed: tuple[Solution[A=Never]] + # revealed: tuple[Solution[A=Invariant[int]]] reveal_type(quantified.solutions(inferable=tuple[A])) # A ≤ Invariant[int] - expected = ConstraintSet.range(Never, A, Invariant[int]) + expected = ConstraintSet.upper_bound(A, Invariant[int]) static_assert(quantified == expected) static_assert(~quantified == ~expected) ``` @@ -63,21 +62,20 @@ def grounded[X, A]() -> None: There is an `X` satisfying `U ≤ X ∧ X = V` exactly when `U ≤ V`. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet def relational_bridge[X, U, V]() -> None: # ∃X. U ≤ X ∧ X = V - body = ConstraintSet.range(Never, U, X) & ConstraintSet.range(V, X, V) + body = ConstraintSet.upper_bound(U, X) & ConstraintSet.equality(X, V) quantified = body.exists(tuple[X]) # TODO: revealed: tuple[Solution[V=object, U=object]] - # revealed: tuple[Solution[U=Never, V=U@relational_bridge]] + # revealed: tuple[Solution[V=U@relational_bridge, U=V@relational_bridge]] reveal_type(quantified.solutions(inferable=tuple[U, V])) # U ≤ V - expected = ConstraintSet.range(Never, U, V) + expected = ConstraintSet.upper_bound(U, V) static_assert(quantified == expected) static_assert(~quantified == ~expected) ``` @@ -89,7 +87,6 @@ both `A` and `B`. `A = Invariant[str]` and `B ≤ int` cannot satisfy the expres satisfy its negation. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -101,22 +98,22 @@ class Invariant[T]: def inverse_image[X, A, B]() -> None: # ∃X. A ≤ Invariant[X] ∧ X ≤ B - body = ConstraintSet.range(Never, A, Invariant[X]) & ConstraintSet.range(Never, X, B) + body = ConstraintSet.upper_bound(A, Invariant[X]) & ConstraintSet.upper_bound(X, B) quantified = body.exists(tuple[X]) # TODO: revealed: tuple[Solution[A=Invariant[object], B=object, X=object]] - # revealed: tuple[Solution[A=Never, X=Never, B=X@inverse_image]] + # revealed: tuple[Solution[A=Invariant[X@inverse_image], B=X@inverse_image, X=B@inverse_image]] reveal_type(body.solutions(inferable=tuple[X, A, B])) # TODO: revealed: tuple[Solution[A=Invariant[object], B=object]] # revealed: tuple[()] reveal_type(quantified.solutions(inferable=tuple[A, B])) # Invariant[str] ≤ A ∧ B ≤ int - invalid = ConstraintSet.range(Invariant[str], A, object) & ConstraintSet.range(Never, B, int) + invalid = ConstraintSet.lower_bound(Invariant[str], A) & ConstraintSet.upper_bound(B, int) # revealed: None reveal_type((body & invalid).solutions(inferable=tuple[X, A, B])) # TODO: revealed: None - # revealed: tuple[Solution[A=Invariant[str], B=Never]] + # revealed: tuple[Solution[A=Invariant[str], B=int]] reveal_type((quantified & invalid).solutions(inferable=tuple[A, B])) static_assert(not (quantified & invalid)) @@ -132,7 +129,6 @@ satisfy the expression. `A ≥ int` and `B ≤ Invariant[str]` cannot satisfy it its negation. ```py -from typing import Never from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -144,7 +140,7 @@ class Invariant[T]: def witness_sensitive[X, A, B]() -> None: # ∃X. A ≤ X ∧ Invariant[X] ≤ B - body = ConstraintSet.range(A, X, object) & ConstraintSet.range(Invariant[X], B, object) + body = ConstraintSet.lower_bound(A, X) & ConstraintSet.lower_bound(Invariant[X], B) quantified = body.exists(tuple[X]) # Each solution for A and B depends on the compatible choice of X. @@ -156,11 +152,11 @@ def witness_sensitive[X, A, B]() -> None: reveal_type(quantified.solutions(inferable=tuple[A, B])) # int ≤ A ∧ B ≤ Invariant[str] - invalid = ConstraintSet.range(int, A, object) & ConstraintSet.range(Never, B, Invariant[str]) + invalid = ConstraintSet.lower_bound(int, A) & ConstraintSet.upper_bound(B, Invariant[str]) # revealed: None reveal_type((body & invalid).solutions(inferable=tuple[X, A, B])) # TODO: revealed: None - # revealed: tuple[Solution[A=int, B=Never]] + # revealed: tuple[Solution[A=int, B=Invariant[str]]] reveal_type((quantified & invalid).solutions(inferable=tuple[A, B])) static_assert(not (quantified & invalid)) @@ -187,12 +183,12 @@ class Invariant[T]: def correlated_outputs[X, Y, Z]() -> None: # C₁(X, Y) = (X = int ∧ Y = int) ∨ (X = str ∧ Y = str) - c1_int = ConstraintSet.range(int, X, int) & ConstraintSet.range(int, Y, int) - c1_str = ConstraintSet.range(str, X, str) & ConstraintSet.range(str, Y, str) + c1_int = ConstraintSet.equality(X, int) & ConstraintSet.equality(Y, int) + c1_str = ConstraintSet.equality(X, str) & ConstraintSet.equality(Y, str) c1 = c1_int | c1_str # C₂(X, Z) = (Z = Invariant[X]) - c2 = ConstraintSet.range(Invariant[X], Z, Invariant[X]) + c2 = ConstraintSet.equality(Z, Invariant[X]) # ∃X. C₁(X, Y) ∧ C₂(X, Z) body = c1 & c2 @@ -205,14 +201,14 @@ def correlated_outputs[X, Y, Z]() -> None: reveal_type(quantified.solutions(inferable=tuple[Y, Z])) # (Y = int ∧ Z = Invariant[int]) ∨ (Y = str ∧ Z = Invariant[str]) - expected_int = ConstraintSet.range(int, Y, int) & ConstraintSet.range(Invariant[int], Z, Invariant[int]) - expected_str = ConstraintSet.range(str, Y, str) & ConstraintSet.range(Invariant[str], Z, Invariant[str]) + expected_int = ConstraintSet.equality(Y, int) & ConstraintSet.equality(Z, Invariant[int]) + expected_str = ConstraintSet.equality(Y, str) & ConstraintSet.equality(Z, Invariant[str]) expected = expected_int | expected_str static_assert(quantified == expected) static_assert(~quantified == ~expected) # (Y = int ∧ Z = Invariant[str]) - invalid_cross = ConstraintSet.range(int, Y, int) & ConstraintSet.range(Invariant[str], Z, Invariant[str]) + invalid_cross = ConstraintSet.equality(Y, int) & ConstraintSet.equality(Z, Invariant[str]) static_assert(not (quantified & invalid_cross)) # revealed: None reveal_type((quantified & invalid_cross).solutions(inferable=tuple[Y, Z])) @@ -237,7 +233,7 @@ class Invariant[T]: def finite_domain[X: (int, str), Y, Z]() -> None: # ∃X ∈ {int, str}. C(X, Y, Z) # C(X, Y, Z) = (Y = X) ∧ (Z = Invariant[X]) - body = ConstraintSet.range(X, Y, X) & ConstraintSet.range(Invariant[X], Z, Invariant[X]) + body = ConstraintSet.equality(Y, X) & ConstraintSet.equality(Z, Invariant[X]) quantified = body.exists(tuple[X]) # TODO: revealed: tuple[Solution[X=int, Y=int, Z=Invariant[int]], Solution[X=str, Y=str, Z=Invariant[str]]] @@ -248,8 +244,8 @@ def finite_domain[X: (int, str), Y, Z]() -> None: reveal_type(quantified.solutions(inferable=tuple[Y, Z])) # (Y = int ∧ Z = Invariant[int]) ∨ (Y = str ∧ Z = Invariant[str]) - expected_int = ConstraintSet.range(int, Y, int) & ConstraintSet.range(Invariant[int], Z, Invariant[int]) - expected_str = ConstraintSet.range(str, Y, str) & ConstraintSet.range(Invariant[str], Z, Invariant[str]) + expected_int = ConstraintSet.equality(Y, int) & ConstraintSet.equality(Z, Invariant[int]) + expected_str = ConstraintSet.equality(Y, str) & ConstraintSet.equality(Z, Invariant[str]) expected = expected_int | expected_str # TODO: no error # error: [static-assert-error] @@ -259,13 +255,13 @@ def finite_domain[X: (int, str), Y, Z]() -> None: static_assert(~quantified == ~expected) # (Y = int ∧ Z = Invariant[str]) - invalid_cross = ConstraintSet.range(int, Y, int) & ConstraintSet.range(Invariant[str], Z, Invariant[str]) + invalid_cross = ConstraintSet.equality(Y, int) & ConstraintSet.equality(Z, Invariant[str]) static_assert(not (quantified & invalid_cross)) # revealed: None reveal_type((quantified & invalid_cross).solutions(inferable=tuple[Y, Z])) # (Y = bytes ∧ Z = Invariant[bytes]) - invalid_domain = ConstraintSet.range(bytes, Y, bytes) & ConstraintSet.range(Invariant[bytes], Z, Invariant[bytes]) + invalid_domain = ConstraintSet.equality(Y, bytes) & ConstraintSet.equality(Z, Invariant[bytes]) static_assert(not (quantified & invalid_domain)) # TODO: revealed: None # revealed: tuple[Solution[Z=Invariant[Y@finite_domain] | Invariant[bytes], Y=bytes]] @@ -285,10 +281,10 @@ from ty_extensions._internal import ConstraintSet def alternation[X: (int, str), Y: (int, str)]() -> None: # R(X, Y) = (X = int ∧ Y = int) ∨ (X = str ∧ Y = str) - x_int = ConstraintSet.range(int, X, int) - x_str = ConstraintSet.range(str, X, str) - y_int = ConstraintSet.range(int, Y, int) - y_str = ConstraintSet.range(str, Y, str) + x_int = ConstraintSet.equality(X, int) + x_str = ConstraintSet.equality(X, str) + y_int = ConstraintSet.equality(Y, int) + y_str = ConstraintSet.equality(Y, str) relation = (x_int & y_int) | (x_str & y_str) # ∀Y. ∃X. R(X, Y) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md b/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md index c220b10116..1c32e94e33 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/satisfied_by_all_typevars.md @@ -49,7 +49,7 @@ set. In a non-inferable position, that means the constraint set must be satisfie type. ```py -from typing import final, Never +from typing import final from ty_extensions import static_assert from ty_extensions._internal import ConstraintSet @@ -68,24 +68,24 @@ def unbounded[T](): static_assert(not ConstraintSet.never().satisfied_by_all_typevars()) # (T = Never) is a valid specialization, which satisfies (T ≤ Unrelated). - static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T ≤ Unrelated). - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars()) # (T = Base) is a valid specialization, which satisfies (T ≤ Super). - static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Super). - static_assert(not ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars()) # (T = Base) is a valid specialization, which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Base). - static_assert(not ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars()) # (T = Sub) is a valid specialization, which satisfies (T ≤ Sub). - static_assert(ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Sub). - static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Sub).satisfied_by_all_typevars()) ``` ## Typevar with an upper bound @@ -115,30 +115,30 @@ def bounded[T: Base](): static_assert(not ConstraintSet.never().satisfied_by_all_typevars()) # (T = Base) is a valid specialization, which satisfies (T ≤ Super). - static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars(inferable=tuple[T])) # Every valid specialization satisfies (T ≤ Base). Since (Base ≤ Super), every valid # specialization also satisfies (T ≤ Super). - static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars()) # (T = Base) is a valid specialization, which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars(inferable=tuple[T])) # Every valid specialization satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars()) # (T = Sub) is a valid specialization, which satisfies (T ≤ Sub). - static_assert(ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T ≤ Sub). - static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Sub).satisfied_by_all_typevars()) # (T = Never) is a valid specialization, which satisfies (T ≤ Unrelated). - constraints = ConstraintSet.range(Never, T, Unrelated) + constraints = ConstraintSet.upper_bound(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T ≤ Unrelated). static_assert(not constraints.satisfied_by_all_typevars()) # Never is the only type that satisfies both (T ≤ Base) and (T ≤ Unrelated). So there is no # valid specialization that satisfies (T ≤ Unrelated ∧ T ≠ Never). - constraints = constraints & ~ConstraintSet.range(Never, T, Never) + constraints = constraints & ~ConstraintSet.equality(T, Never) static_assert(not constraints.satisfied_by_all_typevars(inferable=tuple[T])) static_assert(not constraints.satisfied_by_all_typevars()) ``` @@ -163,18 +163,18 @@ def bounded_by_gradual[T: Any](): # If we choose Base as the materialization for the upper bound, then (T = Base) is a valid # specialization, which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars(inferable=tuple[T])) # We are free to choose any materialization of the upper bound, and only have to show that the # constraint set holds for that one materialization. Having chosen one materialization, we then # have to show that the constraint set holds for all valid specializations of that # materialization. If we choose Never as the materialization, then all valid specializations # must satisfy (T ≤ Never). That means there is only one valid specialization, (T = Never), # which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars()) # If we choose Unrelated as the materialization, then (T = Unrelated) is a valid specialization, # which satisfies (T ≤ Unrelated). - constraints = ConstraintSet.range(Never, T, Unrelated) + constraints = ConstraintSet.upper_bound(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Never as the materialization, then (T = Never) is the only valid specialization, # which satisfies (T ≤ Unrelated). @@ -182,7 +182,7 @@ def bounded_by_gradual[T: Any](): # If we choose Unrelated as the materialization, then (T = Unrelated) is a valid specialization, # which satisfies (T ≤ Unrelated ∧ T ≠ Never). - constraints = constraints & ~ConstraintSet.range(Never, T, Never) + constraints = constraints & ~ConstraintSet.equality(T, Never) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # There is no upper bound that we can choose to satisfy this constraint set in non-inferable # position. (T = Never) will be a valid assignment no matter what, and that does not satisfy @@ -206,7 +206,7 @@ def bounded_by_gradual[T: list[Any]](): # If we choose list[Base] as the materialization of the upper bound, then (T = list[Base]) is a # valid specialization, which satisfies (T ≤ list[Base]). - static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Base as the materialization, then all valid specializations must satisfy # (T ≤ list[Base]). # We are free to choose any materialization of the upper bound, and only have to show that the @@ -214,11 +214,11 @@ def bounded_by_gradual[T: list[Any]](): # have to show that the constraint set holds for all valid specializations of that # materialization. If we choose list[Base] as the materialization, then all valid specializations # must satisfy (T ≤ list[Base]), which is exactly the constraint set that we need to satisfy. - static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, list[Base]).satisfied_by_all_typevars()) # If we choose Unrelated as the materialization, then (T = list[Unrelated]) is a valid # specialization, which satisfies (T ≤ list[Unrelated]). - constraints = ConstraintSet.range(Never, T, list[Unrelated]) + constraints = ConstraintSet.upper_bound(T, list[Unrelated]) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Unrelated as the materialization, then all valid specializations must satisfy # (T ≤ list[Unrelated]). @@ -226,7 +226,7 @@ def bounded_by_gradual[T: list[Any]](): # If we choose Unrelated as the materialization, then (T = list[Unrelated]) is a valid # specialization, which satisfies (T ≤ list[Unrelated] ∧ T ≠ Never). - constraints = constraints & ~ConstraintSet.range(Never, T, Never) + constraints = constraints & ~ConstraintSet.equality(T, Never) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # There is no upper bound that we can choose to satisfy this constraint set in non-inferable # position. (T = Never) will be a valid assignment no matter what, and that does not satisfy @@ -261,53 +261,53 @@ def constrained[T: (Base, Unrelated)](): static_assert(not ConstraintSet.never().satisfied_by_all_typevars()) # (T = Unrelated) is a valid specialization, which satisfies (T ≤ Unrelated). - static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T ≤ Unrelated). - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars()) # (T = Base) is a valid specialization, which satisfies (T ≤ Super). - static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Super). - static_assert(not ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars()) # (T = Base) is a valid specialization, which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars(inferable=tuple[T])) # (T = Unrelated) is a valid specialization, which does not satisfy (T ≤ Base). - static_assert(not ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars()) # Neither (T = Base) nor (T = Unrelated) satisfy (T ≤ Sub). - static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) - static_assert(not ConstraintSet.range(Never, T, Sub).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Sub).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(not ConstraintSet.upper_bound(T, Sub).satisfied_by_all_typevars()) # (T = Base) and (T = Unrelated) both satisfy (T ≤ Super ∨ T ≤ Unrelated). - constraints = ConstraintSet.range(Never, T, Super) | ConstraintSet.range(Never, T, Unrelated) + constraints = ConstraintSet.upper_bound(T, Super) | ConstraintSet.upper_bound(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) static_assert(constraints.satisfied_by_all_typevars()) # (T = Base) and (T = Unrelated) both satisfy (T ≤ Base ∨ T ≤ Unrelated). - constraints = ConstraintSet.range(Never, T, Base) | ConstraintSet.range(Never, T, Unrelated) + constraints = ConstraintSet.upper_bound(T, Base) | ConstraintSet.upper_bound(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) static_assert(constraints.satisfied_by_all_typevars()) # (T = Unrelated) is a valid specialization, which satisfies (T ≤ Sub ∨ T ≤ Unrelated). - constraints = ConstraintSet.range(Never, T, Sub) | ConstraintSet.range(Never, T, Unrelated) + constraints = ConstraintSet.upper_bound(T, Sub) | ConstraintSet.upper_bound(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T ≤ Sub ∨ T ≤ Unrelated). static_assert(not constraints.satisfied_by_all_typevars()) # (T = Unrelated) is a valid specialization, which satisfies (T = Super ∨ T = Unrelated). - constraints = ConstraintSet.range(Super, T, Super) | ConstraintSet.range(Unrelated, T, Unrelated) + constraints = ConstraintSet.equality(T, Super) | ConstraintSet.equality(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T = Super ∨ T = Unrelated). static_assert(not constraints.satisfied_by_all_typevars()) # (T = Base) and (T = Unrelated) both satisfy (T = Base ∨ T = Unrelated). - constraints = ConstraintSet.range(Base, T, Base) | ConstraintSet.range(Unrelated, T, Unrelated) + constraints = ConstraintSet.equality(T, Base) | ConstraintSet.equality(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) static_assert(constraints.satisfied_by_all_typevars()) # (T = Unrelated) is a valid specialization, which satisfies (T = Sub ∨ T = Unrelated). - constraints = ConstraintSet.range(Sub, T, Sub) | ConstraintSet.range(Unrelated, T, Unrelated) + constraints = ConstraintSet.equality(T, Sub) | ConstraintSet.equality(T, Unrelated) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # (T = Base) is a valid specialization, which does not satisfy (T = Sub ∨ T = Unrelated). static_assert(not constraints.satisfied_by_all_typevars()) @@ -333,24 +333,24 @@ def constrained_by_gradual[T: (Base, Any)](): # If we choose Unrelated as the materialization of the gradual constraint, then (T = Unrelated) # is a valid specialization, which satisfies (T ≤ Unrelated). - static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = Base) is a valid specialization, which does # not satisfy (T ≤ Unrelated). - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars()) # If we choose Super as the materialization, then (T = Super) is a valid specialization, which # satisfies (T ≤ Super). - static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Never as the materialization, then (T = Base) and (T = Never) are the only valid # specializations, both of which satisfy (T ≤ Super). - static_assert(ConstraintSet.range(Never, T, Super).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Super).satisfied_by_all_typevars()) # If we choose Base as the materialization, then (T = Base) is a valid specialization, which # satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Never as the materialization, then (T = Base) and (T = Never) are the only valid # specializations, both of which satisfy (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars()) def constrained_by_two_gradual[T: (Any, Any)](): static_assert(ConstraintSet.always().satisfied_by_all_typevars(inferable=tuple[T])) @@ -361,17 +361,17 @@ def constrained_by_two_gradual[T: (Any, Any)](): # If we choose Unrelated as the materialization of either constraint, then (T = Unrelated) is a # valid specialization, which satisfies (T ≤ Unrelated). - static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Unrelated as the materialization of both constraints, then (T = Unrelated) is the # only valid specialization, which satisfies (T ≤ Unrelated). - static_assert(ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars()) # If we choose Base as the materialization of either constraint, then (T = Base) is a valid # specialization, which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Never as the materialization of both constraints, then (T = Never) is the only # valid specialization, which satisfies (T ≤ Base). - static_assert(ConstraintSet.range(Never, T, Base).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, Base).satisfied_by_all_typevars()) ``` When a constraint is a more complex gradual type, we are still free to choose any materialization @@ -391,33 +391,33 @@ def constrained_by_gradual[T: (list[Base], list[Any])](): # No matter which materialization we choose, every valid specialization will be of the form # (T = list[X]). Because Unrelated is final, it is disjoint from all lists. There is therefore # no materialization or specialization that satisfies (T ≤ Unrelated). - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars()) # If we choose list[Super] as the materialization, then (T = list[Super]) is a valid # specialization, which satisfies (T ≤ list[Super]). - static_assert(ConstraintSet.range(Never, T, list[Super]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Super]).satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = list[Base]) is a valid specialization, which # does not satisfy (T ≤ list[Super]). - static_assert(not ConstraintSet.range(Never, T, list[Super]).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, list[Super]).satisfied_by_all_typevars()) # If we choose list[Base] as the materialization, then (T = list[Base]) is a valid # specialization, which satisfies (T ≤ list[Base]). - static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose list[Base] as the materialization, then all valid specializations must satisfy # (T ≤ list[Base]). - static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, list[Base]).satisfied_by_all_typevars()) # If we choose list[Sub] as the materialization, then (T = list[Sub]) is a valid specialization, # which # satisfies (T ≤ list[Sub]). - static_assert(ConstraintSet.range(Never, T, list[Sub]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Sub]).satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = list[Base]) is a valid specialization, which # does not satisfy (T ≤ list[Sub]). - static_assert(not ConstraintSet.range(Never, T, list[Sub]).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, list[Sub]).satisfied_by_all_typevars()) # If we choose list[Unrelated] as the materialization, then (T = list[Unrelated]) is a valid # specialization, which satisfies (T ≤ list[Unrelated]). - constraints = ConstraintSet.range(Never, T, list[Unrelated]) + constraints = ConstraintSet.upper_bound(T, list[Unrelated]) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = list[Base]) is a valid specialization, which # does not satisfy (T ≤ list[Unrelated]). @@ -425,7 +425,7 @@ def constrained_by_gradual[T: (list[Base], list[Any])](): # If we choose list[Unrelated] as the materialization, then (T = list[Unrelated]) is a valid # specialization, which satisfies (T ≤ list[Unrelated] ∧ T ≠ Never). - constraints = constraints & ~ConstraintSet.range(Never, T, Never) + constraints = constraints & ~ConstraintSet.equality(T, Never) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # There is no materialization that we can choose to satisfy this constraint set in non-inferable # position. (T = Never) will be a valid assignment no matter what, and that does not satisfy @@ -442,33 +442,33 @@ def constrained_by_two_gradual[T: (list[Any], list[Any])](): # No matter which materialization we choose, every valid specialization will be of the form # (T = list[X]). Because Unrelated is final, it is disjoint from all lists. There is therefore # no materialization or specialization that satisfies (T ≤ Unrelated). - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) - static_assert(not ConstraintSet.range(Never, T, Unrelated).satisfied_by_all_typevars()) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(not ConstraintSet.upper_bound(T, Unrelated).satisfied_by_all_typevars()) # If we choose list[Super] as the materialization, then (T = list[Super]) is a valid # specialization, which satisfies (T ≤ list[Super]). - static_assert(ConstraintSet.range(Never, T, list[Super]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Super]).satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = list[Base]) is a valid specialization, which # does not satisfy (T ≤ list[Super]). - static_assert(ConstraintSet.range(Never, T, list[Super]).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, list[Super]).satisfied_by_all_typevars()) # If we choose list[Base] as the materialization, then (T = list[Base]) is a valid # specialization, which satisfies (T ≤ list[Base]). - static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Base]).satisfied_by_all_typevars(inferable=tuple[T])) # If we choose Base as the materialization, then all valid specializations must satisfy # (T ≤ list[Base]). - static_assert(ConstraintSet.range(Never, T, list[Base]).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, list[Base]).satisfied_by_all_typevars()) # If we choose list[Sub] as the materialization, then (T = list[Sub]) is a valid specialization, # which satisfies (T ≤ list[Sub]). - static_assert(ConstraintSet.range(Never, T, list[Sub]).satisfied_by_all_typevars(inferable=tuple[T])) + static_assert(ConstraintSet.upper_bound(T, list[Sub]).satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = list[Base]) is a valid specialization, which # does not satisfy (T ≤ list[Sub]). - static_assert(ConstraintSet.range(Never, T, list[Sub]).satisfied_by_all_typevars()) + static_assert(ConstraintSet.upper_bound(T, list[Sub]).satisfied_by_all_typevars()) # If we choose list[Unrelated] as the materialization, then (T = list[Unrelated]) is a valid # specialization, which satisfies (T ≤ list[Unrelated]). - constraints = ConstraintSet.range(Never, T, list[Unrelated]) + constraints = ConstraintSet.upper_bound(T, list[Unrelated]) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # No matter which materialization we choose, (T = list[Base]) is a valid specialization, which # does not satisfy (T ≤ list[Unrelated]). @@ -476,7 +476,7 @@ def constrained_by_two_gradual[T: (list[Any], list[Any])](): # If we choose list[Unrelated] as the materialization, then (T = list[Unrelated]) is a valid # specialization, which satisfies (T ≤ list[Unrelated] ∧ T ≠ Never). - constraints = constraints & ~ConstraintSet.range(Never, T, Never) + constraints = constraints & ~ConstraintSet.equality(T, Never) static_assert(constraints.satisfied_by_all_typevars(inferable=tuple[T])) # There is no constraint that we can choose to satisfy this constraint set in non-inferable # position. (T = Never) will be a valid assignment no matter what, and that does not satisfy diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 9cd5bf746e..b270f59447 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4662,6 +4662,30 @@ impl<'db> Type<'db> { .into() } + Type::ClassLiteral(class) + if name == "lower_bound" && class.is_known(db, KnownClass::ConstraintSet) => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetLowerBound, + )) + .into() + } + Type::ClassLiteral(class) + if name == "upper_bound" && class.is_known(db, KnownClass::ConstraintSet) => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetUpperBound, + )) + .into() + } + Type::ClassLiteral(class) + if name == "equality" && class.is_known(db, KnownClass::ConstraintSet) => + { + Place::bound(Type::KnownBoundMethod( + KnownBoundMethodType::ConstraintSetEquality, + )) + .into() + } Type::ClassLiteral(class) if name == "range" && class.is_known(db, KnownClass::ConstraintSet) => { @@ -7384,6 +7408,9 @@ impl<'db> Type<'db> { ) | Type::KnownBoundMethod( KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever @@ -7753,6 +7780,9 @@ impl<'db> Type<'db> { | Type::ModuleLiteral(_) | Type::KnownBoundMethod( KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever @@ -8048,6 +8078,9 @@ impl<'db> Type<'db> { | Type::WrapperDescriptor(_) | Type::KnownBoundMethod( KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index e6633f2adf..a0de38ac55 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -2790,6 +2790,82 @@ impl<'db> Bindings<'db> { } }, + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetLowerBound) => { + let [Some(lower), Some(typevar)] = overload.parameter_types() else { + return; + }; + let lower = lower.project_type_form(db, env); + let typevar = typevar.project_type_form(db, env); + let Type::TypeVar(typevar) = typevar else { + return; + }; + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + ConstraintSet::constrain_typevar_lower_bound( + db, + env, + constraints, + typevar, + lower, + ) + }); + let tracked = InternedConstraintSet::new(db, result); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::ConstraintSet(tracked), + )); + } + + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetUpperBound) => { + let [Some(typevar), Some(upper)] = overload.parameter_types() else { + return; + }; + let typevar = typevar.project_type_form(db, env); + let upper = upper.project_type_form(db, env); + let Type::TypeVar(typevar) = typevar else { + return; + }; + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + ConstraintSet::constrain_typevar_upper_bound( + db, + env, + constraints, + typevar, + upper, + ) + }); + let tracked = InternedConstraintSet::new(db, result); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::ConstraintSet(tracked), + )); + } + + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetEquality) => { + let [Some(typevar), Some(value)] = overload.parameter_types() else { + return; + }; + let typevar = typevar.project_type_form(db, env); + let value = value.project_type_form(db, env); + let Type::TypeVar(typevar) = typevar else { + return; + }; + let constraints = ConstraintSetBuilder::new(); + let result = constraints.into_owned(|constraints| { + ConstraintSet::constrain_typevar( + db, + env, + constraints, + typevar, + value, + value, + ) + }); + let tracked = InternedConstraintSet::new(db, result); + overload.set_return_type(Type::KnownInstance( + KnownInstanceType::ConstraintSet(tracked), + )); + } + Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetRange) => { let [Some(lower), Some(typevar), Some(upper)] = overload.parameter_types() else { diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index bd7087f3fd..61fbae4636 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1322,6 +1322,15 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'_, 'db> { )), Some(literal.value(db)), ), + KnownBoundMethodType::ConstraintSetLowerBound => { + return f.write_str("bound method `ConstraintSet.lower_bound`"); + } + KnownBoundMethodType::ConstraintSetUpperBound => { + return f.write_str("bound method `ConstraintSet.upper_bound`"); + } + KnownBoundMethodType::ConstraintSetEquality => { + return f.write_str("bound method `ConstraintSet.equality`"); + } KnownBoundMethodType::ConstraintSetRange => { return f.write_str("bound method `ConstraintSet.range`"); } diff --git a/crates/ty_python_semantic/src/types/method.rs b/crates/ty_python_semantic/src/types/method.rs index a622228b70..3e1454889c 100644 --- a/crates/ty_python_semantic/src/types/method.rs +++ b/crates/ty_python_semantic/src/types/method.rs @@ -220,6 +220,9 @@ pub enum KnownBoundMethodType<'db> { StrStartswith(StringLiteralType<'db>), // ConstraintSet methods + ConstraintSetLowerBound, + ConstraintSetUpperBound, + ConstraintSetEquality, ConstraintSetRange, ConstraintSetAlways, ConstraintSetNever, @@ -260,7 +263,10 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size LiteralValueType::promotable(LiteralValueTypeKind::String(string_literal)).into(), ); } - KnownBoundMethodType::ConstraintSetRange + KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality + | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) @@ -309,6 +315,9 @@ impl<'db> KnownBoundMethodType<'db> { )) } KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever @@ -332,7 +341,10 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::PropertyDunderDelete(_) => KnownClass::MethodWrapperType, KnownBoundMethodType::StrStartswith(_) => KnownClass::BuiltinFunctionType, - KnownBoundMethodType::ConstraintSetRange + KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality + | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) @@ -466,6 +478,42 @@ impl<'db> KnownBoundMethodType<'db> { ))) } + KnownBoundMethodType::ConstraintSetLowerBound => { + Either::Right(std::iter::once(Signature::new( + Parameters::standard([ + Parameter::positional_only(Some(Name::new_static("lower_bound"))) + .with_annotated_type(object_type_form()), + Parameter::positional_only(Some(Name::new_static("typevar"))) + .with_annotated_type(object_type_form()), + ]), + KnownClass::ConstraintSet.to_instance(db, env), + ))) + } + + KnownBoundMethodType::ConstraintSetUpperBound => { + Either::Right(std::iter::once(Signature::new( + Parameters::standard([ + Parameter::positional_only(Some(Name::new_static("typevar"))) + .with_annotated_type(object_type_form()), + Parameter::positional_only(Some(Name::new_static("upper_bound"))) + .with_annotated_type(object_type_form()), + ]), + KnownClass::ConstraintSet.to_instance(db, env), + ))) + } + + KnownBoundMethodType::ConstraintSetEquality => { + Either::Right(std::iter::once(Signature::new( + Parameters::standard([ + Parameter::positional_only(Some(Name::new_static("typevar"))) + .with_annotated_type(object_type_form()), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(object_type_form()), + ]), + KnownClass::ConstraintSet.to_instance(db, env), + ))) + } + KnownBoundMethodType::ConstraintSetRange => { Either::Right(std::iter::once(Signature::new( Parameters::standard([ @@ -632,6 +680,18 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { } ( + KnownBoundMethodType::ConstraintSetLowerBound, + KnownBoundMethodType::ConstraintSetLowerBound, + ) + | ( + KnownBoundMethodType::ConstraintSetUpperBound, + KnownBoundMethodType::ConstraintSetUpperBound, + ) + | ( + KnownBoundMethodType::ConstraintSetEquality, + KnownBoundMethodType::ConstraintSetEquality, + ) + | ( KnownBoundMethodType::ConstraintSetRange, KnownBoundMethodType::ConstraintSetRange, ) @@ -683,6 +743,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::PropertyDunderDelete(_) | KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever @@ -700,6 +763,9 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { | KnownBoundMethodType::PropertyDunderSet(_) | KnownBoundMethodType::PropertyDunderDelete(_) | KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index a7c7b79a7c..d5b140a0e6 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -258,6 +258,9 @@ impl<'db> Type<'db> { KnownBoundMethodType::FunctionTypeDunderGet(_) | KnownBoundMethodType::FunctionTypeDunderCall(_) | KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetLowerBound + | KnownBoundMethodType::ConstraintSetUpperBound + | KnownBoundMethodType::ConstraintSetEquality | KnownBoundMethodType::ConstraintSetRange | KnownBoundMethodType::ConstraintSetAlways | KnownBoundMethodType::ConstraintSetNever diff --git a/crates/ty_vendored/ty_extensions/_internal.pyi b/crates/ty_vendored/ty_extensions/_internal.pyi index d269f415ff..a6f6fe762e 100644 --- a/crates/ty_vendored/ty_extensions/_internal.pyi +++ b/crates/ty_vendored/ty_extensions/_internal.pyi @@ -99,6 +99,27 @@ class ConstraintSetSolution: """One solution path for a constraint set.""" class ConstraintSet: + @staticmethod + def lower_bound( + lower_bound: TypeForm[object], + typevar: TypeForm[object], + ) -> ConstraintSet: + """Returns a constraint set requiring `typevar` to be a supertype of `lower_bound`.""" + + @staticmethod + def upper_bound( + typevar: TypeForm[object], + upper_bound: TypeForm[object], + ) -> ConstraintSet: + """Returns a constraint set requiring `typevar` to be a subtype of `upper_bound`.""" + + @staticmethod + def equality( + typevar: TypeForm[object], + value: TypeForm[object], + ) -> ConstraintSet: + """Returns a constraint set requiring `typevar` to specialize exactly to `value`.""" + @staticmethod def range( lower_bound: TypeForm[object], From 1b9e5fc483b95a01fe02ff104820280b1b32e8ae Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 7 Aug 2026 08:33:01 -0400 Subject: [PATCH 321/390] Update Swatinem/rust-cache action to v2.9.2 (#27568) ## Summary Update `Swatinem/rust-cache` from v2.9.1 to v2.9.2 across all GitHub Actions workflows. The new release fixes cache cleanup for packages whose library target differs from the package name and preserves `cdylib`, `rlib`, `dylib`, and `staticlib` artifacts. This affects `libcst`: its library target is `libcst_native` and uses `cdylib`/`rlib`, so our current CI recompiles it even after an exact cache hit. The update also fixes Windows cache-path handling and supports the Cargo v2 build-directory layout. Existing Cargo profiles and workflow cache configuration are unchanged. --- .github/workflows/ci.yaml | 42 ++++++++++---------- .github/workflows/daily_fuzz.yaml | 2 +- .github/workflows/memory_report.yaml | 2 +- .github/workflows/publish-docs.yml | 2 +- .github/workflows/sync_typeshed.yaml | 2 +- .github/workflows/ty-ecosystem-analyzer.yaml | 2 +- .github/workflows/ty-ecosystem-report.yaml | 2 +- .github/workflows/typing_conformance.yaml | 2 +- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7705906eef..d2d277ba52 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -289,7 +289,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -311,7 +311,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "cargo publish dry-run" @@ -330,7 +330,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: ruff-linux-debug save-if: ${{ github.ref == 'refs/heads/main' }} @@ -397,7 +397,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -436,7 +436,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} # Fix for https://github.com/Swatinem/rust-cache/issues/341 @@ -467,7 +467,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -507,7 +507,7 @@ jobs: with: file: "Cargo.toml" field: "workspace.package.rust-version" - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -531,7 +531,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: "fuzz -> target" save-if: ${{ github.ref == 'refs/heads/main' }} @@ -561,7 +561,7 @@ jobs: - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: version: "0.12.1" - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: ruff-linux-debug save-if: false @@ -597,7 +597,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 @@ -657,7 +657,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: ruff-linux-debug save-if: false @@ -764,7 +764,7 @@ jobs: - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: version: "0.12.1" - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -830,7 +830,7 @@ jobs: - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: version: "0.12.1" - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -855,7 +855,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Prep README.md" @@ -910,7 +910,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -942,7 +942,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - name: "Install Rust toolchain" @@ -969,7 +969,7 @@ jobs: with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: ruff-linux-debug save-if: false @@ -1027,7 +1027,7 @@ jobs: persist-credentials: false - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -1072,7 +1072,7 @@ jobs: with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 @@ -1114,7 +1114,7 @@ jobs: with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -1231,7 +1231,7 @@ jobs: with: persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index f21772ef7c..b81d682f38 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -43,7 +43,7 @@ jobs: run: rustup show - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: Build ruff # A debug build means the script runs slower once it gets started, # but this is outweighed by the fact that a release build takes *much* longer to compile in CI diff --git a/.github/workflows/memory_report.yaml b/.github/workflows/memory_report.yaml index 3dd492d887..546fcd6be3 100644 --- a/.github/workflows/memory_report.yaml +++ b/.github/workflows/memory_report.yaml @@ -53,7 +53,7 @@ jobs: - name: Fetch full history without tags run: git -C ruff fetch --no-tags --filter=blob:none --unshallow origin - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: "ruff" diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 47f670b459..e773a08a44 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -65,7 +65,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: "Install dependencies" run: pip install -r docs/requirements.txt diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index 04d0955cf3..6239e9ed91 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -257,7 +257,7 @@ jobs: run: | git config --global user.name typeshedbot git config --global user.email '<>' - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: # Reuse the cache populated by the `cargo test (linux)` CI job on `main`. shared-key: ruff-linux-debug diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index 0597a034f5..1062a77076 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -60,7 +60,7 @@ jobs: persist-credentials: false ref: ${{ github.sha }} - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: lookup-only: false diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 1b1f2d9d45..0c0f041a5d 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -38,7 +38,7 @@ jobs: enable-cache: true version: "0.12.1" - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: lookup-only: false diff --git a/.github/workflows/typing_conformance.yaml b/.github/workflows/typing_conformance.yaml index f694aa1e3d..222fc89a97 100644 --- a/.github/workflows/typing_conformance.yaml +++ b/.github/workflows/typing_conformance.yaml @@ -61,7 +61,7 @@ jobs: path: typing persist-credentials: false - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: "ruff" From 5b48a040974781ba90b47c8df628f8fd9b6c95dd Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:12:50 -0400 Subject: [PATCH 322/390] Bump 0.16.2 (#27555) --- CHANGELOG.md | 20 ++++++ Cargo.lock | 74 +++++++++++----------- Cargo.toml | 72 +++++++++++----------- README.md | 6 +- crates/ruff/Cargo.toml | 2 +- crates/ruff/README.md | 2 +- crates/ruff_annotate_snippets/Cargo.toml | 2 +- crates/ruff_cache/Cargo.toml | 2 +- crates/ruff_cache/README.md | 4 +- crates/ruff_db/Cargo.toml | 2 +- crates/ruff_db/README.md | 4 +- crates/ruff_diagnostics/Cargo.toml | 2 +- crates/ruff_diagnostics/README.md | 4 +- crates/ruff_formatter/Cargo.toml | 2 +- crates/ruff_formatter/README.md | 4 +- crates/ruff_graph/Cargo.toml | 2 +- crates/ruff_graph/README.md | 4 +- crates/ruff_index/Cargo.toml | 2 +- crates/ruff_index/README.md | 4 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_linter/README.md | 4 +- crates/ruff_macros/Cargo.toml | 2 +- crates/ruff_macros/README.md | 4 +- crates/ruff_markdown/Cargo.toml | 2 +- crates/ruff_markdown/README.md | 4 +- crates/ruff_memory_usage/Cargo.toml | 2 +- crates/ruff_memory_usage/README.md | 4 +- crates/ruff_notebook/Cargo.toml | 2 +- crates/ruff_notebook/README.md | 4 +- crates/ruff_options_metadata/Cargo.toml | 2 +- crates/ruff_options_metadata/README.md | 4 +- crates/ruff_python_ast/Cargo.toml | 2 +- crates/ruff_python_ast/README.md | 4 +- crates/ruff_python_codegen/Cargo.toml | 2 +- crates/ruff_python_codegen/README.md | 4 +- crates/ruff_python_formatter/Cargo.toml | 2 +- crates/ruff_python_formatter/README.md | 4 +- crates/ruff_python_importer/Cargo.toml | 2 +- crates/ruff_python_importer/README.md | 4 +- crates/ruff_python_index/Cargo.toml | 2 +- crates/ruff_python_index/README.md | 4 +- crates/ruff_python_literal/Cargo.toml | 2 +- crates/ruff_python_literal/README.md | 4 +- crates/ruff_python_parser/Cargo.toml | 2 +- crates/ruff_python_parser/README.md | 4 +- crates/ruff_python_semantic/Cargo.toml | 2 +- crates/ruff_python_semantic/README.md | 4 +- crates/ruff_python_stdlib/Cargo.toml | 2 +- crates/ruff_python_stdlib/README.md | 4 +- crates/ruff_python_trivia/Cargo.toml | 2 +- crates/ruff_python_trivia/README.md | 4 +- crates/ruff_ranged_value/Cargo.toml | 2 +- crates/ruff_ranged_value/README.md | 4 +- crates/ruff_server/Cargo.toml | 2 +- crates/ruff_server/README.md | 4 +- crates/ruff_source_file/Cargo.toml | 2 +- crates/ruff_source_file/README.md | 4 +- crates/ruff_text_size/Cargo.toml | 2 +- crates/ruff_text_size/README.md | 4 +- crates/ruff_wasm/Cargo.toml | 2 +- crates/ruff_wasm/README.md | 4 +- crates/ruff_workspace/Cargo.toml | 2 +- crates/ruff_workspace/README.md | 4 +- crates/ty_combine/Cargo.toml | 2 +- crates/ty_combine/README.md | 4 +- crates/ty_module_resolver/Cargo.toml | 2 +- crates/ty_module_resolver/README.md | 4 +- crates/ty_python_core/Cargo.toml | 2 +- crates/ty_python_core/README.md | 4 +- crates/ty_python_semantic/Cargo.toml | 2 +- crates/ty_python_semantic/README.md | 4 +- crates/ty_site_packages/Cargo.toml | 2 +- crates/ty_site_packages/README.md | 4 +- crates/ty_static/Cargo.toml | 2 +- crates/ty_static/README.md | 4 +- crates/ty_vendored/Cargo.toml | 2 +- docs/formatter.md | 2 +- docs/integrations.md | 8 +-- docs/tutorial.md | 2 +- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- uv.lock | 78 ++++++++++++------------ 82 files changed, 249 insertions(+), 229 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46fdebb3b1..b871d0c132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 0.16.2 + +Released on 2026-08-06. + +### Bug fixes + +- \[`flake8-pyi`\] Avoid false positives on `singledispatch` functions (`PYI041`) ([#27335](https://github.com/astral-sh/ruff/pull/27335)) + +### Server + +- Register formatting capabilities dynamically to exclude TOML files ([#27332](https://github.com/astral-sh/ruff/pull/27332)) + +### Contributors + +- [@MeGaGiGaGon](https://github.com/MeGaGiGaGon) +- [@charliermarsh](https://github.com/charliermarsh) +- [@epage](https://github.com/epage) +- [@sharkdp](https://github.com/sharkdp) +- [@ntBre](https://github.com/ntBre) + ## 0.16.1 Released on 2026-07-30. diff --git a/Cargo.lock b/Cargo.lock index a5956f32d4..8c85da0d78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3065,7 +3065,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.16.1" +version = "0.16.2" dependencies = [ "anyhow", "argfile", @@ -3129,7 +3129,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anstream 1.0.0", "anstyle", @@ -3167,7 +3167,7 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.7" +version = "0.0.8" dependencies = [ "char_str", "filetime", @@ -3181,7 +3181,7 @@ dependencies = [ [[package]] name = "ruff_db" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anstyle", "arc-swap", @@ -3272,7 +3272,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.7" +version = "0.0.8" dependencies = [ "get-size2", "is-macro", @@ -3282,7 +3282,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.7" +version = "0.0.8" dependencies = [ "drop_bomb", "ruff_cache", @@ -3298,7 +3298,7 @@ dependencies = [ [[package]] name = "ruff_graph" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "clap", @@ -3319,7 +3319,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.7" +version = "0.0.8" dependencies = [ "get-size2", "ruff_macros", @@ -3329,7 +3329,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.16.1" +version = "0.16.2" dependencies = [ "aho-corasick", "anyhow", @@ -3392,7 +3392,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.7" +version = "0.0.8" dependencies = [ "heck", "itertools 0.15.0", @@ -3405,7 +3405,7 @@ dependencies = [ [[package]] name = "ruff_markdown" -version = "0.0.7" +version = "0.0.8" dependencies = [ "insta", "regex", @@ -3436,14 +3436,14 @@ dependencies = [ [[package]] name = "ruff_memory_usage" -version = "0.0.7" +version = "0.0.8" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "rand 0.10.2", @@ -3459,14 +3459,14 @@ dependencies = [ [[package]] name = "ruff_options_metadata" -version = "0.0.7" +version = "0.0.8" dependencies = [ "serde", ] [[package]] name = "ruff_python_ast" -version = "0.0.7" +version = "0.0.8" dependencies = [ "aho-corasick", "arrayvec", @@ -3503,7 +3503,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.7" +version = "0.0.8" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -3515,7 +3515,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "clap", @@ -3548,7 +3548,7 @@ dependencies = [ [[package]] name = "ruff_python_importer" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "insta", @@ -3563,7 +3563,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.7" +version = "0.0.8" dependencies = [ "ruff_python_ast", "ruff_python_parser", @@ -3574,7 +3574,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.7" +version = "0.0.8" dependencies = [ "bitflags 2.13.1", "icu_properties", @@ -3584,7 +3584,7 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -3613,7 +3613,7 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.7" +version = "0.0.8" dependencies = [ "bitflags 2.13.1", "insta", @@ -3634,7 +3634,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.7" +version = "0.0.8" dependencies = [ "bitflags 2.13.1", "unicode-ident", @@ -3642,7 +3642,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" -version = "0.0.7" +version = "0.0.8" dependencies = [ "itertools 0.15.0", "ruff_source_file", @@ -3663,7 +3663,7 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.7" +version = "0.0.8" dependencies = [ "get-size2", "ruff_db", @@ -3675,7 +3675,7 @@ dependencies = [ [[package]] name = "ruff_server" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "crossbeam", @@ -3718,7 +3718,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.7" +version = "0.0.8" dependencies = [ "get-size2", "memchr", @@ -3728,7 +3728,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.7" +version = "0.0.8" dependencies = [ "get-size2", "schemars", @@ -3739,7 +3739,7 @@ dependencies = [ [[package]] name = "ruff_wasm" -version = "0.16.1" +version = "0.16.2" dependencies = [ "console_error_panic_hook", "console_log", @@ -3766,7 +3766,7 @@ dependencies = [ [[package]] name = "ruff_workspace" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "colored", @@ -4633,7 +4633,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.7" +version = "0.0.8" dependencies = [ "ordermap", "ruff_db", @@ -4715,7 +4715,7 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "camino", @@ -4789,7 +4789,7 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -4823,7 +4823,7 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -4918,7 +4918,7 @@ dependencies = [ [[package]] name = "ty_site_packages" -version = "0.0.7" +version = "0.0.8" dependencies = [ "camino", "colored", @@ -4939,7 +4939,7 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.7" +version = "0.0.8" dependencies = [ "ruff_macros", ] @@ -4971,7 +4971,7 @@ dependencies = [ [[package]] name = "ty_vendored" -version = "0.0.7" +version = "0.0.8" dependencies = [ "path-slash", "ruff_db", diff --git a/Cargo.toml b/Cargo.toml index 0d79a518bf..aae537860f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,51 +14,51 @@ license = "MIT" [workspace.dependencies] char_str = { version = "0.0.2" } -ruff = { version = "0.16.1", path = "crates/ruff" } -ruff_annotate_snippets = { version = "0.0.7", path = "crates/ruff_annotate_snippets" } -ruff_cache = { version = "0.0.7", path = "crates/ruff_cache" } -ruff_db = { version = "0.0.7", path = "crates/ruff_db", default-features = false } -ruff_diagnostics = { version = "0.0.7", path = "crates/ruff_diagnostics" } -ruff_formatter = { version = "0.0.7", path = "crates/ruff_formatter" } -ruff_graph = { version = "0.0.7", path = "crates/ruff_graph" } -ruff_index = { version = "0.0.7", path = "crates/ruff_index" } -ruff_linter = { version = "0.16.1", path = "crates/ruff_linter" } -ruff_macros = { version = "0.0.7", path = "crates/ruff_macros" } -ruff_markdown = { version = "0.0.7", path = "crates/ruff_markdown" } -ruff_memory_usage = { version = "0.0.7", path = "crates/ruff_memory_usage" } -ruff_notebook = { version = "0.0.7", path = "crates/ruff_notebook" } -ruff_options_metadata = { version = "0.0.7", path = "crates/ruff_options_metadata" } -ruff_python_ast = { version = "0.0.7", path = "crates/ruff_python_ast" } -ruff_python_codegen = { version = "0.0.7", path = "crates/ruff_python_codegen" } -ruff_python_formatter = { version = "0.0.7", path = "crates/ruff_python_formatter" } -ruff_python_importer = { version = "0.0.7", path = "crates/ruff_python_importer" } -ruff_python_index = { version = "0.0.7", path = "crates/ruff_python_index" } -ruff_python_literal = { version = "0.0.7", path = "crates/ruff_python_literal" } -ruff_python_parser = { version = "0.0.7", path = "crates/ruff_python_parser" } -ruff_python_semantic = { version = "0.0.7", path = "crates/ruff_python_semantic" } -ruff_python_stdlib = { version = "0.0.7", path = "crates/ruff_python_stdlib" } -ruff_python_trivia = { version = "0.0.7", path = "crates/ruff_python_trivia" } -ruff_server = { version = "0.0.7", path = "crates/ruff_server" } -ruff_source_file = { version = "0.0.7", path = "crates/ruff_source_file" } +ruff = { version = "0.16.2", path = "crates/ruff" } +ruff_annotate_snippets = { version = "0.0.8", path = "crates/ruff_annotate_snippets" } +ruff_cache = { version = "0.0.8", path = "crates/ruff_cache" } +ruff_db = { version = "0.0.8", path = "crates/ruff_db", default-features = false } +ruff_diagnostics = { version = "0.0.8", path = "crates/ruff_diagnostics" } +ruff_formatter = { version = "0.0.8", path = "crates/ruff_formatter" } +ruff_graph = { version = "0.0.8", path = "crates/ruff_graph" } +ruff_index = { version = "0.0.8", path = "crates/ruff_index" } +ruff_linter = { version = "0.16.2", path = "crates/ruff_linter" } +ruff_macros = { version = "0.0.8", path = "crates/ruff_macros" } +ruff_markdown = { version = "0.0.8", path = "crates/ruff_markdown" } +ruff_memory_usage = { version = "0.0.8", path = "crates/ruff_memory_usage" } +ruff_notebook = { version = "0.0.8", path = "crates/ruff_notebook" } +ruff_options_metadata = { version = "0.0.8", path = "crates/ruff_options_metadata" } +ruff_python_ast = { version = "0.0.8", path = "crates/ruff_python_ast" } +ruff_python_codegen = { version = "0.0.8", path = "crates/ruff_python_codegen" } +ruff_python_formatter = { version = "0.0.8", path = "crates/ruff_python_formatter" } +ruff_python_importer = { version = "0.0.8", path = "crates/ruff_python_importer" } +ruff_python_index = { version = "0.0.8", path = "crates/ruff_python_index" } +ruff_python_literal = { version = "0.0.8", path = "crates/ruff_python_literal" } +ruff_python_parser = { version = "0.0.8", path = "crates/ruff_python_parser" } +ruff_python_semantic = { version = "0.0.8", path = "crates/ruff_python_semantic" } +ruff_python_stdlib = { version = "0.0.8", path = "crates/ruff_python_stdlib" } +ruff_python_trivia = { version = "0.0.8", path = "crates/ruff_python_trivia" } +ruff_server = { version = "0.0.8", path = "crates/ruff_server" } +ruff_source_file = { version = "0.0.8", path = "crates/ruff_source_file" } ruff_mdtest = { path = "crates/ruff_mdtest" } -ruff_ranged_value = { version = "0.0.7", path = "crates/ruff_ranged_value" } -ruff_text_size = { version = "0.0.7", path = "crates/ruff_text_size" } -ruff_workspace = { version = "0.0.7", path = "crates/ruff_workspace" } +ruff_ranged_value = { version = "0.0.8", path = "crates/ruff_ranged_value" } +ruff_text_size = { version = "0.0.8", path = "crates/ruff_text_size" } +ruff_workspace = { version = "0.0.8", path = "crates/ruff_workspace" } ty = { path = "crates/ty" } -ty_combine = { version = "0.0.7", path = "crates/ty_combine" } +ty_combine = { version = "0.0.8", path = "crates/ty_combine" } ty_completion_bench = { path = "crates/ty_completion_bench" } ty_completion_eval = { path = "crates/ty_completion_eval" } ty_ide = { path = "crates/ty_ide" } -ty_module_resolver = { version = "0.0.7", path = "crates/ty_module_resolver" } +ty_module_resolver = { version = "0.0.8", path = "crates/ty_module_resolver" } ty_project = { path = "crates/ty_project", default-features = false } -ty_python_semantic = { version = "0.0.7", path = "crates/ty_python_semantic" } -ty_python_core = { version = "0.0.7", path = "crates/ty_python_core" } +ty_python_semantic = { version = "0.0.8", path = "crates/ty_python_semantic" } +ty_python_core = { version = "0.0.8", path = "crates/ty_python_core" } ty_server = { path = "crates/ty_server" } -ty_site_packages = { version = "0.0.7", path = "crates/ty_site_packages" } -ty_static = { version = "0.0.7", path = "crates/ty_static" } +ty_site_packages = { version = "0.0.8", path = "crates/ty_site_packages" } +ty_static = { version = "0.0.8", path = "crates/ty_static" } ty_test = { path = "crates/ty_test" } -ty_vendored = { version = "0.0.7", path = "crates/ty_vendored" } +ty_vendored = { version = "0.0.8", path = "crates/ty_vendored" } mdtest = { path = "crates/mdtest" } diff --git a/README.md b/README.md index 21fc902fd5..2b8366f4aa 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,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.16.1/install.sh | sh -powershell -c "irm https://astral.sh/ruff/0.16.1/install.ps1 | iex" +curl -LsSf https://astral.sh/ruff/0.16.2/install.sh | sh +powershell -c "irm https://astral.sh/ruff/0.16.2/install.ps1 | iex" ``` You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), @@ -186,7 +186,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.16.1 + rev: v0.16.2 hooks: # Run the linter. - id: ruff-check diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index e98d2607ec..7a1f33dc66 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.16.1" +version = "0.16.2" description = "An extremely fast Python linter and code formatter" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff/README.md b/crates/ruff/README.md index c0029a5155..2364cf7d26 100644 --- a/crates/ruff/README.md +++ b/crates/ruff/README.md @@ -10,7 +10,7 @@ See the [documentation](https://docs.astral.sh/ruff/) or This crate is the entry point to the Ruff command-line interface. The Rust API exposed here is not considered public interface. -This is version 0.16.1. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff). +This is version 0.16.2. The source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff). The following Ruff workspace members are also available: diff --git a/crates/ruff_annotate_snippets/Cargo.toml b/crates/ruff_annotate_snippets/Cargo.toml index 0891b14e69..888ee9b3d9 100644 --- a/crates/ruff_annotate_snippets/Cargo.toml +++ b/crates/ruff_annotate_snippets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_annotate_snippets" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cache/Cargo.toml b/crates/ruff_cache/Cargo.toml index 410ee8004f..977ebef415 100644 --- a/crates/ruff_cache/Cargo.toml +++ b/crates/ruff_cache/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cache" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cache/README.md b/crates/ruff_cache/README.md index e43c934cc7..380cd45f4d 100644 --- a/crates/ruff_cache/README.md +++ b/crates/ruff_cache/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_cache). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_cache). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_db/Cargo.toml b/crates/ruff_db/Cargo.toml index 20202939b7..52d668e9af 100644 --- a/crates/ruff_db/Cargo.toml +++ b/crates/ruff_db/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_db" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_db/README.md b/crates/ruff_db/README.md index 32f39c655d..4ef95696f3 100644 --- a/crates/ruff_db/README.md +++ b/crates/ruff_db/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_db). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_db). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_diagnostics/Cargo.toml b/crates/ruff_diagnostics/Cargo.toml index 93d4942f69..5d1e392155 100644 --- a/crates/ruff_diagnostics/Cargo.toml +++ b/crates/ruff_diagnostics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_diagnostics" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_diagnostics/README.md b/crates/ruff_diagnostics/README.md index 89721632fb..ecec0c72c0 100644 --- a/crates/ruff_diagnostics/README.md +++ b/crates/ruff_diagnostics/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_diagnostics). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_diagnostics). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_formatter/Cargo.toml b/crates/ruff_formatter/Cargo.toml index 636ee9a116..f6a9b7cce8 100644 --- a/crates/ruff_formatter/Cargo.toml +++ b/crates/ruff_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_formatter" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_formatter/README.md b/crates/ruff_formatter/README.md index 71ec8676cf..f75009dcac 100644 --- a/crates/ruff_formatter/README.md +++ b/crates/ruff_formatter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_formatter). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_graph/Cargo.toml b/crates/ruff_graph/Cargo.toml index ba4e1ecd7c..aa1bcc33d0 100644 --- a/crates/ruff_graph/Cargo.toml +++ b/crates/ruff_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_graph" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" edition.workspace = true rust-version.workspace = true diff --git a/crates/ruff_graph/README.md b/crates/ruff_graph/README.md index a3acdc1867..ff4feff470 100644 --- a/crates/ruff_graph/README.md +++ b/crates/ruff_graph/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_graph). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_graph). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_index/Cargo.toml b/crates/ruff_index/Cargo.toml index 8090d7ddf5..1f62f61968 100644 --- a/crates/ruff_index/Cargo.toml +++ b/crates/ruff_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_index" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_index/README.md b/crates/ruff_index/README.md index 2b08a67721..faa6312359 100644 --- a/crates/ruff_index/README.md +++ b/crates/ruff_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_index). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 335592119d..94aebb3b7b 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.16.1" +version = "0.16.2" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/README.md b/crates/ruff_linter/README.md index 280497f7be..124078abe7 100644 --- a/crates/ruff_linter/README.md +++ b/crates/ruff_linter/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.16.1) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_linter). +This version (0.16.2) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_linter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_macros/Cargo.toml b/crates/ruff_macros/Cargo.toml index 7ea014674c..13d495f8bb 100644 --- a/crates/ruff_macros/Cargo.toml +++ b/crates/ruff_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_macros" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_macros/README.md b/crates/ruff_macros/README.md index d7f8afde7c..ac925be274 100644 --- a/crates/ruff_macros/README.md +++ b/crates/ruff_macros/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_macros). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_macros). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_markdown/Cargo.toml b/crates/ruff_markdown/Cargo.toml index 543cca4eaa..ec98be0bc9 100644 --- a/crates/ruff_markdown/Cargo.toml +++ b/crates/ruff_markdown/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_markdown" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ruff_markdown/README.md b/crates/ruff_markdown/README.md index 7b3358ede1..9320691af7 100644 --- a/crates/ruff_markdown/README.md +++ b/crates/ruff_markdown/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_markdown). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_markdown). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_memory_usage/Cargo.toml b/crates/ruff_memory_usage/Cargo.toml index e032ac417b..852b0e46b5 100644 --- a/crates/ruff_memory_usage/Cargo.toml +++ b/crates/ruff_memory_usage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_memory_usage" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_memory_usage/README.md b/crates/ruff_memory_usage/README.md index 4b5a36084e..bdb69d27c9 100644 --- a/crates/ruff_memory_usage/README.md +++ b/crates/ruff_memory_usage/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_memory_usage). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_memory_usage). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_notebook/Cargo.toml b/crates/ruff_notebook/Cargo.toml index 3993be1563..477f3fa14c 100644 --- a/crates/ruff_notebook/Cargo.toml +++ b/crates/ruff_notebook/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_notebook" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_notebook/README.md b/crates/ruff_notebook/README.md index d17feb9eee..bcf6f56751 100644 --- a/crates/ruff_notebook/README.md +++ b/crates/ruff_notebook/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_notebook). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_notebook). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_options_metadata/Cargo.toml b/crates/ruff_options_metadata/Cargo.toml index c910fb71f1..203bf5f5ce 100644 --- a/crates/ruff_options_metadata/Cargo.toml +++ b/crates/ruff_options_metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_options_metadata" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_options_metadata/README.md b/crates/ruff_options_metadata/README.md index 707687360b..6ae0120c4a 100644 --- a/crates/ruff_options_metadata/README.md +++ b/crates/ruff_options_metadata/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_options_metadata). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_options_metadata). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_ast/Cargo.toml b/crates/ruff_python_ast/Cargo.toml index 9bbb0a9b14..f3f65747e1 100644 --- a/crates/ruff_python_ast/Cargo.toml +++ b/crates/ruff_python_ast/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_ast" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_ast/README.md b/crates/ruff_python_ast/README.md index 4f14de15d6..77078685db 100644 --- a/crates/ruff_python_ast/README.md +++ b/crates/ruff_python_ast/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_ast). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_ast). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_codegen/Cargo.toml b/crates/ruff_python_codegen/Cargo.toml index 8c2317d2e5..15eb55c7fe 100644 --- a/crates/ruff_python_codegen/Cargo.toml +++ b/crates/ruff_python_codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_codegen" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_codegen/README.md b/crates/ruff_python_codegen/README.md index 9a75bff2a6..eb260851fa 100644 --- a/crates/ruff_python_codegen/README.md +++ b/crates/ruff_python_codegen/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_codegen). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_codegen). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_formatter/Cargo.toml b/crates/ruff_python_formatter/Cargo.toml index 0d55fdf608..cfa6732104 100644 --- a/crates/ruff_python_formatter/Cargo.toml +++ b/crates/ruff_python_formatter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_formatter" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index b1fa7818d3..c9a942f1bc 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -32,8 +32,8 @@ Head to [The Ruff Formatter](https://docs.astral.sh/ruff/formatter/) for usage i This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_formatter). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_formatter). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_importer/Cargo.toml b/crates/ruff_python_importer/Cargo.toml index 98a90c8741..98b44197b0 100644 --- a/crates/ruff_python_importer/Cargo.toml +++ b/crates/ruff_python_importer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_importer" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_importer/README.md b/crates/ruff_python_importer/README.md index 8559e159df..31a810a657 100644 --- a/crates/ruff_python_importer/README.md +++ b/crates/ruff_python_importer/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_importer). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_importer). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_index/Cargo.toml b/crates/ruff_python_index/Cargo.toml index 826104dc66..a177107337 100644 --- a/crates/ruff_python_index/Cargo.toml +++ b/crates/ruff_python_index/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_index" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_index/README.md b/crates/ruff_python_index/README.md index ad1f6a79ce..dc83a2c02e 100644 --- a/crates/ruff_python_index/README.md +++ b/crates/ruff_python_index/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_index). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_index). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_literal/Cargo.toml b/crates/ruff_python_literal/Cargo.toml index 094de8ef54..7aa7cdd0a4 100644 --- a/crates/ruff_python_literal/Cargo.toml +++ b/crates/ruff_python_literal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_literal" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_literal/README.md b/crates/ruff_python_literal/README.md index 2a433bf5b0..94ee9e81bc 100644 --- a/crates/ruff_python_literal/README.md +++ b/crates/ruff_python_literal/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_literal). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_literal). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_parser/Cargo.toml b/crates/ruff_python_parser/Cargo.toml index a23e4f45e7..9766dbb59c 100644 --- a/crates/ruff_python_parser/Cargo.toml +++ b/crates/ruff_python_parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_parser" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = ["Charlie Marsh ", "RustPython Team"] edition = { workspace = true } diff --git a/crates/ruff_python_parser/README.md b/crates/ruff_python_parser/README.md index c393a8ba57..20d8066dc4 100644 --- a/crates/ruff_python_parser/README.md +++ b/crates/ruff_python_parser/README.md @@ -19,8 +19,8 @@ Refer to the [contributing guidelines](./CONTRIBUTING.md) to get started and Git This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_parser). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_parser). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_semantic/Cargo.toml b/crates/ruff_python_semantic/Cargo.toml index ac12f2ec05..c73d7ea449 100644 --- a/crates/ruff_python_semantic/Cargo.toml +++ b/crates/ruff_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_semantic" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_semantic/README.md b/crates/ruff_python_semantic/README.md index b82ca3e975..b0275afaa4 100644 --- a/crates/ruff_python_semantic/README.md +++ b/crates/ruff_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_semantic). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_stdlib/Cargo.toml b/crates/ruff_python_stdlib/Cargo.toml index a721b52c69..c3e59fa976 100644 --- a/crates/ruff_python_stdlib/Cargo.toml +++ b/crates/ruff_python_stdlib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_stdlib" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_stdlib/README.md b/crates/ruff_python_stdlib/README.md index 19d792f380..d38692821e 100644 --- a/crates/ruff_python_stdlib/README.md +++ b/crates/ruff_python_stdlib/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_stdlib). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_stdlib). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_python_trivia/Cargo.toml b/crates/ruff_python_trivia/Cargo.toml index f302c8118f..465877e15c 100644 --- a/crates/ruff_python_trivia/Cargo.toml +++ b/crates/ruff_python_trivia/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_python_trivia" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_python_trivia/README.md b/crates/ruff_python_trivia/README.md index 2a5d845526..b467967292 100644 --- a/crates/ruff_python_trivia/README.md +++ b/crates/ruff_python_trivia/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_python_trivia). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_python_trivia). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_ranged_value/Cargo.toml b/crates/ruff_ranged_value/Cargo.toml index 2323710092..8f699f3a38 100644 --- a/crates/ruff_ranged_value/Cargo.toml +++ b/crates/ruff_ranged_value/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_ranged_value" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_ranged_value/README.md b/crates/ruff_ranged_value/README.md index e4a3197d87..068105df68 100644 --- a/crates/ruff_ranged_value/README.md +++ b/crates/ruff_ranged_value/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_ranged_value). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_ranged_value). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index a5819c7340..465d5b7d10 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_server" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_server/README.md b/crates/ruff_server/README.md index 54f7b07aec..fbb2f6b627 100644 --- a/crates/ruff_server/README.md +++ b/crates/ruff_server/README.md @@ -24,8 +24,8 @@ You can also join us on [**Discord**](https://discord.com/invite/astral-sh). This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_server). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_server). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_source_file/Cargo.toml b/crates/ruff_source_file/Cargo.toml index b8921cac60..e918a959a0 100644 --- a/crates/ruff_source_file/Cargo.toml +++ b/crates/ruff_source_file/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_source_file" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_source_file/README.md b/crates/ruff_source_file/README.md index e5f56b0bff..2d61cd4f33 100644 --- a/crates/ruff_source_file/README.md +++ b/crates/ruff_source_file/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_source_file). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_source_file). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_text_size/Cargo.toml b/crates/ruff_text_size/Cargo.toml index 8842992bd2..3ad9d4da5f 100644 --- a/crates/ruff_text_size/Cargo.toml +++ b/crates/ruff_text_size/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_text_size" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_text_size/README.md b/crates/ruff_text_size/README.md index 308ca891ee..e14fefa0f7 100644 --- a/crates/ruff_text_size/README.md +++ b/crates/ruff_text_size/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_text_size). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_text_size). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_wasm/Cargo.toml b/crates/ruff_wasm/Cargo.toml index 845b99f7ae..1e3b3000f7 100644 --- a/crates/ruff_wasm/Cargo.toml +++ b/crates/ruff_wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_wasm" -version = "0.16.1" +version = "0.16.2" description = "WebAssembly bindings for Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_wasm/README.md b/crates/ruff_wasm/README.md index 63a189fb46..29b092ebfd 100644 --- a/crates/ruff_wasm/README.md +++ b/crates/ruff_wasm/README.md @@ -55,8 +55,8 @@ const formatted = workspace.format(exampleDocument); This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.16.1) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_wasm). +This version (0.16.2) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_wasm). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ruff_workspace/Cargo.toml b/crates/ruff_workspace/Cargo.toml index 173a383e98..2ad0e6af61 100644 --- a/crates/ruff_workspace/Cargo.toml +++ b/crates/ruff_workspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_workspace" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_workspace/README.md b/crates/ruff_workspace/README.md index 7bf597aebc..996e2998cb 100644 --- a/crates/ruff_workspace/README.md +++ b/crates/ruff_workspace/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ruff_workspace). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ruff_workspace). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_combine/Cargo.toml b/crates/ty_combine/Cargo.toml index bb7838fd7d..99241b4673 100644 --- a/crates/ty_combine/Cargo.toml +++ b/crates/ty_combine/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_combine" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" edition.workspace = true rust-version.workspace = true diff --git a/crates/ty_combine/README.md b/crates/ty_combine/README.md index 630d310d21..de9f6647aa 100644 --- a/crates/ty_combine/README.md +++ b/crates/ty_combine/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ty_combine). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ty_combine). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_module_resolver/Cargo.toml b/crates/ty_module_resolver/Cargo.toml index e3eb41801b..dbd4c2c350 100644 --- a/crates/ty_module_resolver/Cargo.toml +++ b/crates/ty_module_resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_module_resolver" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_module_resolver/README.md b/crates/ty_module_resolver/README.md index d48136b649..1c094d7d90 100644 --- a/crates/ty_module_resolver/README.md +++ b/crates/ty_module_resolver/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ty_module_resolver). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ty_module_resolver). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_core/Cargo.toml b/crates/ty_python_core/Cargo.toml index 35238e3ea3..ca9f0e95fe 100644 --- a/crates/ty_python_core/Cargo.toml +++ b/crates/ty_python_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_python_core" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_python_core/README.md b/crates/ty_python_core/README.md index 1b439ca901..c25f0aed08 100644 --- a/crates/ty_python_core/README.md +++ b/crates/ty_python_core/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ty_python_core). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ty_python_core). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml index ebdbfb5af8..9f2867e94b 100644 --- a/crates/ty_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_python_semantic" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_python_semantic/README.md b/crates/ty_python_semantic/README.md index c81ed0bf32..96f36cc39e 100644 --- a/crates/ty_python_semantic/README.md +++ b/crates/ty_python_semantic/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ty_python_semantic). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ty_python_semantic). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_site_packages/Cargo.toml b/crates/ty_site_packages/Cargo.toml index 882efe6723..cb95e5ffd3 100644 --- a/crates/ty_site_packages/Cargo.toml +++ b/crates/ty_site_packages/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_site_packages" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ty_site_packages/README.md b/crates/ty_site_packages/README.md index 5353319c9e..c2ce5db95e 100644 --- a/crates/ty_site_packages/README.md +++ b/crates/ty_site_packages/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ty_site_packages). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ty_site_packages). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_static/Cargo.toml b/crates/ty_static/Cargo.toml index 0fbac5fceb..365a9854c6 100644 --- a/crates/ty_static/Cargo.toml +++ b/crates/ty_static/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_static" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/ty_static/README.md b/crates/ty_static/README.md index d374ebe91b..6952f9a0ff 100644 --- a/crates/ty_static/README.md +++ b/crates/ty_static/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [Ruff](https://crates.io/crates/ruff). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.7) is a component of [Ruff 0.16.1](https://crates.io/crates/ruff/0.16.1). The -source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.1/crates/ty_static). +This version (0.0.8) is a component of [Ruff 0.16.2](https://crates.io/crates/ruff/0.16.2). The +source can be found [here](https://github.com/astral-sh/ruff/blob/0.16.2/crates/ty_static). See Ruff's [crate versioning policy](https://docs.astral.sh/ruff/versioning/#crate-versioning) for details on versioning. diff --git a/crates/ty_vendored/Cargo.toml b/crates/ty_vendored/Cargo.toml index f072319122..ef78599682 100644 --- a/crates/ty_vendored/Cargo.toml +++ b/crates/ty_vendored/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ty_vendored" -version = "0.0.7" +version = "0.0.8" description = "This is an internal component crate of Ruff" authors = { workspace = true } edition = { workspace = true } diff --git a/docs/formatter.md b/docs/formatter.md index 5f3307f1b1..f1dbc9c726 100644 --- a/docs/formatter.md +++ b/docs/formatter.md @@ -303,7 +303,7 @@ support needs to be explicitly included by adding it to `types_or`: ```yaml title=".pre-commit-config.yaml" repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.16.1 + rev: v0.16.2 hooks: - id: ruff-format types_or: [python, pyi, jupyter, markdown] diff --git a/docs/integrations.md b/docs/integrations.md index cddb2cadd2..c4b1fdb036 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.16.1-alpine + name: ghcr.io/astral-sh/ruff:0.16.2-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.16.1 + rev: v0.16.2 hooks: # Run the linter. - id: ruff-check @@ -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.16.1 + rev: v0.16.2 hooks: # Run the linter. - id: ruff-check @@ -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.16.1 + rev: v0.16.2 hooks: # Run the linter. - id: ruff-check diff --git a/docs/tutorial.md b/docs/tutorial.md index 233e9a48b8..c58e24ec8f 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -372,7 +372,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.16.1 + rev: v0.16.2 hooks: # Run the linter. - id: ruff-check diff --git a/pyproject.toml b/pyproject.toml index 3a209bf5ab..e7dccf8b8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.16.1" +version = "0.16.2" 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 9adef2abf2..6a1171ac92 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scripts" -version = "0.16.1" +version = "0.16.2" description = "" authors = ["Charles Marsh "] diff --git a/uv.lock b/uv.lock index cbd2bdc168..b3b2bd7135 100644 --- a/uv.lock +++ b/uv.lock @@ -30,8 +30,8 @@ name = "anyio" version = "4.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "idna", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version == '3.12.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } wheels = [ @@ -43,7 +43,7 @@ name = "anysqlite" version = "0.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, + { name = "anyio", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/4b/cd5d66b9f87e773bc71344a368b9472987e33514e6627e28342b9c3e7c43/anysqlite-0.0.5.tar.gz", hash = "sha256:9dfcf87baf6b93426ad1d9118088c41dbf24ef01b445eea4a5d486bac2755cce", size = 3432, upload-time = "2023-10-02T13:49:25.135Z" } wheels = [ @@ -64,7 +64,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "python_full_version >= '3.12' and implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -176,11 +176,11 @@ name = "hishel" version = "0.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "anysqlite" }, - { name = "httpx" }, - { name = "msgpack" }, - { name = "typing-extensions" }, + { name = "anyio", marker = "python_full_version >= '3.12'" }, + { name = "anysqlite", marker = "python_full_version >= '3.12'" }, + { name = "httpx", marker = "python_full_version >= '3.12'" }, + { name = "msgpack", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/64/a104ccac48f123f853254483617b16e0efc1649bd7e35bcdc5a5a5ef0ae2/hishel-0.1.5.tar.gz", hash = "sha256:9d40c682cd94fd6e1394fb05713ae20a75ed8aeba6f5272380444039ce6257f2", size = 75468, upload-time = "2025-10-18T13:32:41.854Z" } wheels = [ @@ -192,8 +192,8 @@ name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "h11" }, + { name = "certifi", marker = "python_full_version >= '3.12'" }, + { name = "h11", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ @@ -205,10 +205,10 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, + { name = "anyio", marker = "python_full_version >= '3.12'" }, + { name = "certifi", marker = "python_full_version >= '3.12'" }, + { name = "httpcore", marker = "python_full_version >= '3.12'" }, + { name = "idna", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ @@ -229,7 +229,7 @@ name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mdurl" }, + { name = "mdurl", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ @@ -374,10 +374,10 @@ name = "pydantic" version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "annotated-types", marker = "python_full_version >= '3.12'" }, + { name = "pydantic-core", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ @@ -389,7 +389,7 @@ name = "pydantic-core" version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ @@ -519,7 +519,7 @@ name = "pygit2" version = "1.19.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi" }, + { name = "cffi", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/44/415aa93422b4bfc21a6448acb7e16280d5f33a9a3fae38a384e37b046ae4/pygit2-1.19.3.tar.gz", hash = "sha256:a543e6d4ebb43825564935758dc234e770016fed673b84370d46ae9580558831", size = 810489, upload-time = "2026-06-13T08:06:04.982Z" } wheels = [ @@ -594,8 +594,8 @@ name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, + { name = "markdown-it-py", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ @@ -607,14 +607,14 @@ name = "rooster" version = "0.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "hishel" }, - { name = "httpx" }, - { name = "marko" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pygit2" }, - { name = "tqdm" }, - { name = "typer" }, + { name = "hishel", marker = "python_full_version >= '3.12'" }, + { name = "httpx", marker = "python_full_version >= '3.12'" }, + { name = "marko", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pydantic", marker = "python_full_version >= '3.12'" }, + { name = "pygit2", marker = "python_full_version >= '3.12'" }, + { name = "tqdm", marker = "python_full_version >= '3.12'" }, + { name = "typer", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/02/8ce565271dc52bd0d0d812043b12ec60111d947f81dc30301d19d7bfd453/rooster-0.1.1.tar.gz", hash = "sha256:c9823122f0c2b035985e70384323cdd353477af988e0f065bc302646a49da482", size = 18608, upload-time = "2025-10-29T15:18:49.478Z" } wheels = [ @@ -623,7 +623,7 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.1" +version = "0.16.2" source = { editable = "." } [package.dev-dependencies] @@ -654,7 +654,7 @@ name = "tqdm" version = "4.68.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } wheels = [ @@ -666,10 +666,10 @@ name = "typer" version = "0.26.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "rich" }, - { name = "shellingham" }, + { name = "annotated-doc", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "rich", marker = "python_full_version >= '3.12'" }, + { name = "shellingham", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } wheels = [ @@ -690,7 +690,7 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ From 02ce4ba99459393bc18eaaed0c977cbe67d8f9bd Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 7 Aug 2026 14:26:38 +0100 Subject: [PATCH 323/390] [ty] Add an opt-in unsound-return-statement lint (#27561) --- .github/ty-ecosystem.toml | 1 + crates/ty/docs/rules.md | 366 ++++++++++++------ .../lint_docs/unsound-return-statement.md | 123 ++++++ .../resources/mdtest/function/return_type.md | 353 +++++++++++++++++ .../resources/mdtest/narrow/type_guards.md | 16 + crates/ty_python_semantic/src/types.rs | 6 +- .../src/types/diagnostic.rs | 57 +++ .../src/types/infer/builder/function.rs | 142 ++++--- .../ty_python_semantic/src/types/relation.rs | 112 ++++-- .../src/types/relation_error.rs | 123 ++++-- .../src/types/typed_dict.rs | 24 +- .../ty_python_semantic/src/types/visitor.rs | 61 ++- crates/ty_test/src/db.rs | 39 +- ty.schema.json | 10 + 14 files changed, 1176 insertions(+), 257 deletions(-) create mode 100644 crates/ty_python_semantic/resources/lint_docs/unsound-return-statement.md diff --git a/.github/ty-ecosystem.toml b/.github/ty-ecosystem.toml index 69c2b83665..295b2959ce 100644 --- a/.github/ty-ecosystem.toml +++ b/.github/ty-ecosystem.toml @@ -7,4 +7,5 @@ division-by-zero = "warn" possibly-missing-attribute = "warn" possibly-missing-import = "warn" possibly-unresolved-reference = "warn" +unsound-return-statement = "warn" unsupported-dynamic-base = "warn" diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index 0d56dd7288..df75037ed5 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.64 · Related issues · -View source +View source @@ -44,7 +44,7 @@ class Base(ABC): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class Derived(Base): # error Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -154,7 +154,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -237,7 +237,7 @@ value = unknown # ty: ignore[unresolved-reference] Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -292,7 +292,7 @@ Foo.method() # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -320,7 +320,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -355,7 +355,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -389,7 +389,7 @@ a = 1 # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -424,7 +424,7 @@ class C(A, B): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -460,7 +460,7 @@ class B(A): ... # error Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -496,7 +496,7 @@ type B = A # error Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -533,7 +533,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -572,7 +572,7 @@ old_func() # error: [deprecated] Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -605,7 +605,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -636,7 +636,7 @@ class B(A, A): ... # error Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -679,7 +679,7 @@ class A: # error Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -756,7 +756,7 @@ def foo() -> "intt\b": ... # error Default level: warn · Added in 0.0.50 · Related issues · -View source +View source @@ -796,7 +796,7 @@ def g(value: ~A) -> None: ... # error: [experimental-syntax] Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -831,7 +831,7 @@ def my_function() -> int: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -947,7 +947,7 @@ def test() -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -983,7 +983,7 @@ class C(A, B): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1013,7 +1013,7 @@ t[3] # error Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -1050,7 +1050,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -1151,7 +1151,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1183,7 +1183,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1214,7 +1214,7 @@ a: int = "" # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1272,7 +1272,7 @@ C.instance_only_var = 56 # error Default level: error · Added in 0.0.33 · Related issues · -View source +View source @@ -1318,7 +1318,7 @@ class Sub(Base): Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1360,7 +1360,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1387,7 +1387,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1417,7 +1417,7 @@ with 1: # error Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1470,7 +1470,7 @@ See: Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1506,7 +1506,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1538,7 +1538,7 @@ a: str # error Default level: warn · Added in 0.0.20 · Related issues · -View source +View source @@ -1595,7 +1595,7 @@ class Pet(Enum): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1659,7 +1659,7 @@ This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](h Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1712,7 +1712,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1763,7 +1763,7 @@ class NonFrozenChild(FrozenBase): # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1812,7 +1812,7 @@ class D(Generic[U, T]): ... # error Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1908,7 +1908,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1956,7 +1956,7 @@ carol = Person(name="Carol", aeg=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -2018,7 +2018,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2058,7 +2058,7 @@ def f(t: TypeVar("U")): ... # ty: ignore[invalid-type-form] Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -2108,7 +2108,7 @@ match object(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2143,7 +2143,7 @@ class B(metaclass=42): ... # error Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2261,7 +2261,7 @@ Correct use of `@override` is enforced by ty's [`invalid-explicit-override`](#in Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -2328,7 +2328,7 @@ TypeError: typing.ClassVar[int] is not valid as type argument Default level: warn · Added in 0.0.31 · Related issues · -View source +View source @@ -2376,7 +2376,7 @@ admin[0] # "Alice" Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -2414,7 +2414,7 @@ Baz = NewType("Baz", int | str) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2471,7 +2471,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2500,7 +2500,7 @@ def f(a: int = ""): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2536,7 +2536,7 @@ P2 = ParamSpec() # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2572,7 +2572,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2643,7 +2643,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2675,7 +2675,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2786,7 +2786,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2837,7 +2837,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2883,7 +2883,7 @@ InvalidAlias = TypeAliasType("InvalidAlias", list[T], type_params=(list[T],)) # Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2950,7 +2950,7 @@ Bar[int] # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2983,7 +2983,7 @@ TYPE_CHECKING = "" # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3019,7 +3019,7 @@ b: Annotated[int] # error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -3076,7 +3076,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -3120,7 +3120,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3177,7 +3177,7 @@ V = TypeVar("V", list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -3219,7 +3219,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.28 · Related issues · -View source +View source @@ -3255,7 +3255,7 @@ class Child(Base): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -3298,7 +3298,7 @@ def f(options: dict[str, object]): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -3333,7 +3333,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.25 · Related issues · -View source +View source @@ -3368,7 +3368,7 @@ def gen() -> Iterator[int]: Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -3435,7 +3435,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -3485,7 +3485,7 @@ def g(arg: object): Default level: warn · Added in 0.0.30 · Related issues · -View source +View source @@ -3528,7 +3528,7 @@ Movie = TypedDict("Film", {"title": str}) # error: [mismatched-type-name] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3559,7 +3559,7 @@ func() # error Default level: ignore · Added in 0.0.41 · Related issues · -View source +View source @@ -3618,7 +3618,7 @@ class ExplicitChild(Parent): Default level: ignore · Added in 0.0.45 · Related issues · -View source +View source @@ -3657,7 +3657,7 @@ def handle(m: re.Match[str]) -> str: Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -3696,7 +3696,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3734,7 +3734,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.30 · Related issues · -View source +View source @@ -3772,7 +3772,7 @@ class Sub(Super): ... # error: [non-callable-init-subclass] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3801,7 +3801,7 @@ for i in 34: # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3829,7 +3829,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -3866,7 +3866,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -3903,7 +3903,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3934,7 +3934,7 @@ f(1, x=2) # error Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3965,7 +3965,7 @@ f(x=1) # error Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4004,7 +4004,7 @@ A.c # error Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4043,7 +4043,7 @@ A()[0] # error Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4089,7 +4089,7 @@ from module import a # error Default level: warn · Added in 0.0.23 · Related issues · -View source +View source @@ -4121,7 +4121,7 @@ html.parser # error Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4158,7 +4158,7 @@ print(x) # error Default level: warn · Added in 0.0.60 · Related issues · -View source +View source @@ -4233,7 +4233,7 @@ def test() -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4268,7 +4268,7 @@ cast(int, f()) # error Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -4306,7 +4306,7 @@ class C: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -4350,7 +4350,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4385,7 +4385,7 @@ static_assert(int(2.0 * 3.0) == 6) # error Default level: warn · Added in 0.0.39 · Related issues · -View source +View source @@ -4436,7 +4436,7 @@ Consider using [`functools.total_ordering`][total_ordering] instead, which does Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4470,7 +4470,7 @@ class B(A): ... # error Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -4510,7 +4510,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4540,7 +4540,7 @@ f("foo") # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4579,7 +4579,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4637,7 +4637,7 @@ class A: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -4681,7 +4681,7 @@ class C(Generic[T]): Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4710,7 +4710,7 @@ reveal_type(1) # revealed: Literal[1] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4741,7 +4741,7 @@ f(x=1, y=2) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4774,7 +4774,7 @@ A().foo # error Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -4849,7 +4849,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4878,7 +4878,7 @@ import foo # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4900,13 +4900,151 @@ Using an undefined variable will raise a `NameError` at runtime. print(x) # error ``` +## `unsound-return-statement` + + +Default level: ignore · +Added in 0.0.70 · +Related issues · +View source + + + +**What it does** + + +Detects `return` statements that unsoundly return a type that is not a [subtype] of the function's +annotated return type. + +This lint is a stricter version of [`invalid-return-type`](#invalid-return-type). + +**Why is this bad?** + + +By default, type checkers consider a `return` statement valid if the inferred type of the object +being returned is [assignable] to the annotated return type of the function it's in. However, this +makes it easy for incorrect types to percolate through your code unexpectedly due to a single +expression being inferred as `Any`. This can easily lead to runtime errors that are not caught by +the type checker: + +```py +from typing import Any + + +def returns_any() -> Any: + return "foo" + + +def returns_int() -> int: + # error: "Unsound return statement: `Any` is not a subtype of `int`" + return returns_any() + + +# fails at runtime, even though the type checker infers both operands as being of type `int`! +returns_int() + 42 +``` + +This rule allows you to use ["fully static"][fully-static] return types as "typed boundaries" for +your code. With this rule enabled, ty would emit an error on the `return returns_any()` statement +in `returns_int`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not +a subtype of `int`. This helps prevent the unsoundness from spreading far from its original source +(in this case, the return type of the `returns_any` function). + +Note that this rule is only applied to functions annotated as returning +[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in +your return type, either implicitly or explicitly: + +```py +from typing import Any + + +def returns_any() -> Any: + return "foo" + + +# error: [missing-type-argument] +def returns_unparameterized_tuple() -> tuple: + # no error, since the return type is implicitly `tuple[Unknown, ...]` + # (which is what the `missing-type-argument` error is complaining about on the line above!) + return returns_any() + + +def returns_list_of_any() -> list[Any]: + # no error, since the return type is explicitly `list[Any]` + return returns_any() +``` + +This rule works especially well when combined with ty's +[`missing-type-argument`](#missing-type-argument) rule, and the Ruff rules [`ANN201`][ann201], +[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all +these rules at once effectively makes it much less likely that a `return` statement can lead to +unsoundness "leaking" out of a function unless that function has been *explicitly* annotated with +a dynamic type in some way (`-> Any` or `-> tuple[Any]`, for example). + +This rule is analogous to mypy's [`no-any-return`][no-any-return] error code, which is enabled by +mypy’s [`--strict`][mypy-strict] mode and can also be enabled on its own using mypy’s +[`--warn-return-any`][warn-return-any] option. + +**Examples** + + +```py +from typing import Any + + +def returns_any() -> Any: + return 42 + + +def returns_int() -> int: + # error: "Unsound return statement: `Any` is not a subtype of `int`" + return returns_any() +``` + +Narrow the type to a subtype of `int` to fix the diagnostic: + +```py +from typing import Any +from typing_extensions import reveal_type + + +def returns_any() -> Any: + return 42 + + +def returns_int() -> int: + my_int = returns_any() + assert isinstance(my_int, int) + reveal_type(my_int) # revealed: Any & int + return my_int # no error: `Any & int` is a subtype of `int` +``` + +**Default level** + + +This rule is disabled by default. It is intended for advanced users wanting additional soundness +checks from their type checker, not for users who have just started to use type checkers on their +Python code. + +[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ +[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/ +[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ +[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/ +[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/ +[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable +[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type +[mypy-strict]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict +[no-any-return]: https://mypy.readthedocs.io/en/stable/error_code_list2.html#code-no-any-return +[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype +[warn-return-any]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-warn-return-any + ## `unsupported-base` Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -4953,7 +5091,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5002,7 +5140,7 @@ b1 < b2 < b1 # error Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -5049,7 +5187,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5082,7 +5220,7 @@ A() + A() # error Default level: warn · Added in 0.0.21 · Related issues · -View source +View source @@ -5202,7 +5340,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -5281,7 +5419,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/lint_docs/unsound-return-statement.md b/crates/ty_python_semantic/resources/lint_docs/unsound-return-statement.md new file mode 100644 index 0000000000..3d8fb9bc5b --- /dev/null +++ b/crates/ty_python_semantic/resources/lint_docs/unsound-return-statement.md @@ -0,0 +1,123 @@ +## What it does + +Detects `return` statements that unsoundly return a type that is not a [subtype] of the function's +annotated return type. + +This lint is a stricter version of `invalid-return-type`. + +## Why is this bad? + +By default, type checkers consider a `return` statement valid if the inferred type of the object +being returned is [assignable] to the annotated return type of the function it's in. However, this +makes it easy for incorrect types to percolate through your code unexpectedly due to a single +expression being inferred as `Any`. This can easily lead to runtime errors that are not caught by +the type checker: + +```py +from typing import Any + + +def returns_any() -> Any: + return "foo" + + +def returns_int() -> int: + # error: "Unsound return statement: `Any` is not a subtype of `int`" + return returns_any() + + +# fails at runtime, even though the type checker infers both operands as being of type `int`! +returns_int() + 42 +``` + +This rule allows you to use ["fully static"][fully-static] return types as "typed boundaries" for +your code. With this rule enabled, ty would emit an error on the `return returns_any()` statement +in `returns_int`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not +a subtype of `int`. This helps prevent the unsoundness from spreading far from its original source +(in this case, the return type of the `returns_any` function). + +Note that this rule is only applied to functions annotated as returning +[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in +your return type, either implicitly or explicitly: + +```py +from typing import Any + + +def returns_any() -> Any: + return "foo" + + +# error: [missing-type-argument] +def returns_unparameterized_tuple() -> tuple: + # no error, since the return type is implicitly `tuple[Unknown, ...]` + # (which is what the `missing-type-argument` error is complaining about on the line above!) + return returns_any() + + +def returns_list_of_any() -> list[Any]: + # no error, since the return type is explicitly `list[Any]` + return returns_any() +``` + +This rule works especially well when combined with ty's +`missing-type-argument` rule, and the Ruff rules [`ANN201`][ann201], +[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all +these rules at once effectively makes it much less likely that a `return` statement can lead to +unsoundness "leaking" out of a function unless that function has been *explicitly* annotated with +a dynamic type in some way (`-> Any` or `-> tuple[Any]`, for example). + +This rule is analogous to mypy's [`no-any-return`][no-any-return] error code, which is enabled by +mypy’s [`--strict`][mypy-strict] mode and can also be enabled on its own using mypy’s +[`--warn-return-any`][warn-return-any] option. + +## Examples + +```py +from typing import Any + + +def returns_any() -> Any: + return 42 + + +def returns_int() -> int: + # error: "Unsound return statement: `Any` is not a subtype of `int`" + return returns_any() +``` + +Narrow the type to a subtype of `int` to fix the diagnostic: + +```py +from typing import Any +from typing_extensions import reveal_type + + +def returns_any() -> Any: + return 42 + + +def returns_int() -> int: + my_int = returns_any() + assert isinstance(my_int, int) + reveal_type(my_int) # revealed: Any & int + return my_int # no error: `Any & int` is a subtype of `int` +``` + +## Default level + +This rule is disabled by default. It is intended for advanced users wanting additional soundness +checks from their type checker, not for users who have just started to use type checkers on their +Python code. + +[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ +[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/ +[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ +[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/ +[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/ +[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable +[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type +[mypy-strict]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict +[no-any-return]: https://mypy.readthedocs.io/en/stable/error_code_list2.html#code-no-any-return +[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype +[warn-return-any]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-warn-return-any 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 6a6c6f6084..d4ef4520c6 100644 --- a/crates/ty_python_semantic/resources/mdtest/function/return_type.md +++ b/crates/ty_python_semantic/resources/mdtest/function/return_type.md @@ -625,3 +625,356 @@ from typing import Never, Any def f(func: Any) -> Never: # error: [invalid-return-type] func() ``` + +## `unsound-return-statement` + +In addition to `invalid-return-type`, we also offer a disabled-by-default stricter rule +`unsound-return-statement`. This rule forbids `return` statements that return an instance of a type +`A` unless `A` is a *subtype* of the annotated return type: + +```toml +[rules] +unsound-return-statement = "error" +``` + +```py +from typing import Any + +# no error, even though `str` is not a subtype of `Any`: +# the lint only applies to a function if its return annotation is not a dynamic +# type such as `Any` +def returns_any() -> Any: + return "foo" + +def g() -> int: + # snapshot: unsound-return-statement + return returns_any() +``` + +```snapshot +error[unsound-return-statement]: Unsound return statement + --> src/mdtest_snippet.py:11:12 + | + 9 | def g() -> int: + | --- Expected a subtype of `int` because of the return type +10 | # snapshot: unsound-return-statement +11 | return returns_any() + | ^^^^^^^^^^^^^ Inferred as `Any` +info: `Any` is assignable to `int`, but not a subtype of `int` +help: Consider using an `assert` to narrow the type prior to the `return` statement +``` + +An example with nested error context: + +```py +def h() -> tuple[tuple[int, int]]: + # snapshot: unsound-return-statement + return ((42, returns_any()),) +``` + +```snapshot +error[unsound-return-statement]: Unsound return statement + --> src/mdtest_snippet.py:14:12 + | +12 | def h() -> tuple[tuple[int, int]]: + | ---------------------- Expected a subtype of `tuple[tuple[int, int]]` because of the return type +13 | # snapshot: unsound-return-statement +14 | return ((42, returns_any()),) + | ^^^^^^^^^^^^^^^^^^^^^^ Inferred as `tuple[tuple[Literal[42], Any]]` +info: `tuple[tuple[Literal[42], Any]]` is assignable to `tuple[tuple[int, int]]`, but not a subtype of `tuple[tuple[int, int]]` +info: the first tuple element is not compatible: `tuple[Literal[42], Any]` is not a subtype of `tuple[int, int]` +info: └── the second tuple element is not compatible: `Any` is not a subtype of `int` +help: Consider using an `assert` to narrow the type prior to the `return` statement +``` + +The rule is also applied to generator functions: + +```py +from typing import Generator + +def f() -> Generator[None, None, int]: + yield + # snapshot: unsound-return-statement + return returns_any() +``` + +```snapshot +error[unsound-return-statement]: Unsound return statement + --> src/mdtest_snippet.py:20:12 + | +17 | def f() -> Generator[None, None, int]: + | -------------------------- Expected a subtype of `int` because of the return type +18 | yield +19 | # snapshot: unsound-return-statement +20 | return returns_any() + | ^^^^^^^^^^^^^ Inferred as `Any` +info: `Any` is assignable to `int`, but not a subtype of `int` +help: Consider using an `assert` to narrow the type prior to the `return` statement +``` + +Aliases of `Any` are also dynamic return annotations and must not trigger the rule: + +```py +from typing_extensions import TypeAliasType + +AnyAlias = TypeAliasType("AnyAlias", Any) + +def returns_any_alias() -> AnyAlias: + return "foo" +``` + +The same applies when an alias of `Any` is the return type of a generator: + +```py +def generator_returns_any_alias() -> Generator[None, None, AnyAlias]: + yield + return "foo" +``` + +The rule in fact will not trigger if `Any` appears anywhere in your return type, either implicitly +or explicitly: + +```py +from typing import Any + +# error: [missing-type-argument] +def returns_unparameterized_tuple() -> tuple: + # no error, since the return type is implicitly `tuple[Any, ...]` + # (which is what the `missing-type-argument` error is complaining about on the line above!) + return returns_any() + +def returns_tuple_of_any() -> tuple[Any, Any]: + # no error, since the return type is explicitly `tuple[Any, Any]` + return returns_any() +``` + +Edge case: for `TypeIs`-annotated functions, we want the error message to say "not a subtype of +`bool`" rather than "not a subtype of `TypeIs`": + +```py +from typing_extensions import TypeIs + +def f(x: object) -> TypeIs[int]: + # snapshot: unsound-return-statement + return returns_any() +``` + +```snapshot +error[unsound-return-statement]: Unsound return statement + --> src/mdtest_snippet.py:45:12 + | +43 | def f(x: object) -> TypeIs[int]: + | ----------- Expected a subtype of `bool` because of the return type +44 | # snapshot: unsound-return-statement +45 | return returns_any() + | ^^^^^^^^^^^^^ Inferred as `Any` +info: `Any` is assignable to `bool`, but not a subtype of `bool` +help: Consider using an `assert` to narrow the type prior to the `return` statement +``` + +Aliases of `TypeIs` still return `bool`, so diagnostics must mention `bool` rather than the alias: + +```py +TypeIsAlias = TypeAliasType("TypeIsAlias", TypeIs[int]) + +def returns_type_is_alias(value: object) -> TypeIsAlias: + # error: "Unsound return statement: `Any` is not a subtype of `bool`" + return returns_any() +``` + +Detailed error context for aliases of `TypeIs` must also compare each union member against `bool`, +rather than against the original `TypeIs` annotation: + +```py +def returns_type_is_alias_union(value: object, result: bool | Any) -> TypeIsAlias: + # snapshot: unsound-return-statement + return result +``` + +```snapshot +error[unsound-return-statement]: Unsound return statement + --> src/mdtest_snippet.py:53:12 + | +51 | def returns_type_is_alias_union(value: object, result: bool | Any) -> TypeIsAlias: + | ----------- Expected a subtype of `bool` because of the return type +52 | # snapshot: unsound-return-statement +53 | return result + | ^^^^^^ Inferred as `bool | Any` +info: `bool | Any` is assignable to `bool`, but not a subtype of `bool` +info: element `Any` of union `bool | Any` is not a subtype of `bool` +help: Consider using an `assert` to narrow the type prior to the `return` statement +``` + +A `Never` return annotation is still a typed boundary, so returning `Any` must trigger the rule: + +```py +from typing_extensions import Never + +def never_returns() -> Never: + return returns_any() # error: [unsound-return-statement] +``` + +The same applies when `Never` is the return type of a generator: + +```py +def generator_never_returns() -> Generator[None, None, Never]: + yield + return returns_any() # error: [unsound-return-statement] +``` + +There is currently a limitation in how this rule interacts with contextual inference for collection +literals. When a function is annotated as returning `list[int]`, the annotation is used as context +while inferring the type of a list literal in a `return` statement. As a result, a list literal +containing an `Any` value is inferred as `list[int]` rather than `list[Any]`. The rule therefore +does not emit a diagnostic for the following unsound return statement. Mypy's `--warn-return-any` +option has the same limitation. In fact, mypy only rejects return expressions whose entire type is +`Any`, whereas this rule also rejects an independently inferred `list[Any]` when the annotated +return type is `list[int]`: + +```py +def returns_list_containing_any() -> list[int]: + return [returns_any()] +``` + +## Regression test: `unsound-return-statement` uses "pure redundancy" + +Internally, the rule uses "pure redundancy" rather than "impure redundancy". The following example +is a regression test that shows why this internal implementation detail is important. As an +optimisation as of 06 August 2026, `Phantom[str]` is not currently considered "impurely redundant" +with `Phantom[int]` (we do not simplify the union `Phantom[str] | Phantom[int]`). But the two +protocols are considered equivalent, are considered mutual subtypes of each other, and are +considered mutually redundant, meaning that no `unsound-return-statement` error is reported on this +snippet: + +```toml +[rules] +unsound-return-statement = "error" +``` + +```py +from typing import Generator, Protocol, TypeVar + +T = TypeVar("T") + +class Phantom(Protocol[T]): + def ping(self) -> int: ... + +def returns_protocol(value: Phantom[int]) -> Phantom[str]: + return value + +def generator_returns_protocol(value: Phantom[int]) -> Generator[None, None, Phantom[str]]: + yield + return value +``` + +## Regression test: `unsound-return-statement` with non-fully-static `TypedDict`s + +A `TypedDict` with a field or explicit extra items of type `Any` is not fully static, even when the +dictionary is defined as a class or inherits its fields from another `TypedDict`. The rule is not +applied to `TypedDict`s like this that are not fully static: + +```toml +[rules] +unsound-return-statement = "error" +``` + +```py +from typing_extensions import Any, Generator, TypedDict + +class StaticPayload(TypedDict): + value: int + +class DynamicPayload(TypedDict): + value: Any + +class InheritedDynamicPayload(DynamicPayload): ... +class DynamicExtraPayload(TypedDict, extra_items=Any): ... + +FunctionalDynamicPayload = TypedDict("FunctionalDynamicPayload", {"value": Any}) + +def returns_dynamic_typed_dict(value: StaticPayload) -> DynamicPayload: + return value + +def returns_inherited_dynamic_typed_dict(value: StaticPayload) -> InheritedDynamicPayload: + return value + +def returns_functional_dynamic_typed_dict(value: StaticPayload) -> FunctionalDynamicPayload: + return value + +def returns_dynamic_extra_typed_dict(value: Any) -> DynamicExtraPayload: + return value + +def generator_returns_dynamic_typed_dict( + value: StaticPayload, +) -> Generator[None, None, DynamicPayload]: + yield + return value + +def returns_static_typed_dict(value: Any) -> StaticPayload: + return value # error: [unsound-return-statement] +``` + +## Regression test: `unsound-return-statement` + recursive structural types + +Recursively specializing a protocol can produce infinitely many distinct types. Checking whether +such a return annotation is fully static must recognize the recurring protocol definition and +terminate instead of expanding the recursive member indefinitely, which would lead to a stack +overflow: + +```toml +[rules] +unsound-return-statement = "error" +``` + +```py +from typing import Any, Protocol, TypeVar + +T = TypeVar("T") + +class Growing(Protocol[T]): + @property + def next(self) -> "Growing[list[T]]": ... + +def returns_recursive_protocol(value: Any) -> Growing[int]: + return value +``` + +The same protection is needed for class-based `TypedDict` fields that recursively specialize their +containing dictionary. + +```py +from typing import Generic, TypedDict + +class GrowingPayload(TypedDict, Generic[T]): + child: "GrowingPayload[list[T]]" + +def returns_recursive_typed_dict(value: Any) -> GrowingPayload[int]: + return value +``` + +## Regression test: `unsound-return-statement` + recursive type aliases + +Recursively specializing a generic type alias can also produce infinitely many distinct types. The +check for whether a type is fully static must recognize repeated visits to the same alias +definition, including when `Any` appears elsewhere in the recursive alias. + +```toml +[environment] +python-version = "3.12" + +[rules] +unsound-return-statement = "error" +``` + +```py +from typing import Any + +type GrowingAlias[T] = list[GrowingAlias[list[T]]] +type GrowingAliasWithAny[T] = list[GrowingAliasWithAny[list[T]] | Any] + +def returns_recursive_alias(value: Any) -> GrowingAlias[int]: + return value + +def returns_recursive_alias_with_any(value: Any) -> GrowingAliasWithAny[int]: + return value +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index 9871446596..3961414662 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -259,6 +259,22 @@ def g(a: Literal["foo", "bar"]) -> TypeIs[Literal["foo"]]: return False ``` +A valid boolean return must also be accepted when the predicate's return annotation is an alias of +`TypeIs` or `TypeGuard`, rather than incorrectly producing an `invalid-return-type` diagnostic. + +```py +from typing_extensions import TypeAliasType + +TypeIsAlias = TypeAliasType("TypeIsAlias", TypeIs[int]) +TypeGuardAlias = TypeAliasType("TypeGuardAlias", TypeGuard[int]) + +def aliased_type_is(value: object) -> TypeIsAlias: + return True + +def aliased_type_guard(value: object) -> TypeGuardAlias: + return True +``` + ## Calls ```py diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index b270f59447..f2c605577f 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -101,7 +101,7 @@ pub use crate::types::typevar::{ use crate::types::typevar::{TypeVarInstance, TypeVarSet}; pub use crate::types::variance::TypeVarVariance; use crate::types::variance::VarianceInferable; -use crate::types::visitor::any_over_type; +use crate::types::visitor::{any_over_type, dynamic_content}; use crate::{Db, FxOrderSet, HasType, NameKind, Program, SemanticModel}; pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, StaticClassLiteral}; pub use class::{KnownClass, MethodDecorator}; @@ -1554,6 +1554,10 @@ impl<'db> Type<'db> { }) } + pub(crate) fn is_fully_static(self, db: &'db dyn Db, env: &ProgramEnvironment) -> bool { + dynamic_content(db, env, self).is_absent() + } + const fn as_intersection(self) -> Option> { match self { Type::Intersection(intersection) => Some(intersection), diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 53cf5c97de..b7b3a43ad1 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -82,6 +82,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&ISINSTANCE_AGAINST_TYPED_DICT); registry.register_lint(&INVALID_ARGUMENT_TYPE); registry.register_lint(&INVALID_RETURN_TYPE); + registry.register_lint(&UNSOUND_RETURN_STATEMENT); registry.register_lint(&INVALID_YIELD); registry.register_lint(&INVALID_ASSIGNMENT); registry.register_lint(&INVALID_AWAIT); @@ -426,6 +427,16 @@ declare_lint! { } } +declare_lint! { + #[expect(clippy::doc_link_with_quotes)] + #[doc = include_str!("../../resources/lint_docs/unsound-return-statement.md")] + pub(crate) static UNSOUND_RETURN_STATEMENT = { + summary: "detects return statements that unsoundly return a type that is not a subtype of the function's annotated return type", + status: LintStatus::stable("0.0.70"), + default_level: Level::Ignore, + } +} + declare_lint! { #[doc = include_str!("../../resources/lint_docs/invalid-yield.md")] pub(crate) static INVALID_YIELD = { @@ -1992,6 +2003,52 @@ pub(super) fn report_invalid_return_type( error_context.attach_to(db, env, &mut diag); } +pub(super) fn report_unsound_return_statement( + context: &InferContext, + object_range: impl Ranged, + return_type_range: impl Ranged, + expected_ty: Type, + actual_ty: Type, +) { + let db = context.db(); + let Some(builder) = context.report_lint(&UNSOUND_RETURN_STATEMENT, object_range) else { + return; + }; + + let env = &context.program_environment(); + + // `TypeIs`-annotated functions are expected to return `bool`; + // this needs to be normalized before we figure out the error context + // and before we display the types. + let expected_ty = match expected_ty.resolve_type_alias(db) { + Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_instance(db, env), + _ => expected_ty, + }; + + let settings = + DisplaySettings::from_possibly_ambiguous_types(db, env, [expected_ty, actual_ty]); + + let mut diag = builder.into_diagnostic("Unsound return statement"); + let actual_ty_display = actual_ty.display_with(db, env, settings.clone()); + let expected_ty_display = expected_ty.display_with(db, env, settings); + + diag.set_concise_message(format_args!( + "Unsound return statement: `{actual_ty_display}` is not a subtype of `{expected_ty_display}`" + )); + diag.set_primary_annotation_message(format_args!("Inferred as `{actual_ty_display}`")); + diag.annotate(context.secondary(return_type_range).message(format_args!( + "Expected a subtype of `{expected_ty_display}` because of the return type", + ))); + + diag.info(format_args!( + "`{actual_ty_display}` is assignable to `{expected_ty_display}`, \ + but not a subtype of `{expected_ty_display}`", + )); + let error_context = actual_ty.pure_redundancy_error_context(db, env, expected_ty); + error_context.attach_to(db, env, &mut diag); + diag.help("Consider using an `assert` to narrow the type prior to the `return` statement"); +} + pub(super) fn report_invalid_generator_function_return_type( context: &InferContext, return_type_range: TextRange, diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs index 896ec53ce8..5450de0fc2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -1,16 +1,17 @@ -use crate::Db; -use crate::ProgramEnvironment; use crate::{ + Db, ProgramEnvironment, reachability::ReachabilityConstraintsExtension, types::{ KnownClass, KnownInstanceType, ParamSpecAttrKind, SubclassOfInner, SubclassOfType, Type, TypeContext, TypeVarKind, UnionType, + constraints::ConstraintSetBuilder, diagnostic::{ ABSTRACT_AND_FINAL_METHOD, FINAL_ON_NON_METHOD, INVALID_PARAMETER_DEFAULT, - INVALID_PARAMSPEC, INVALID_TYPE_FORM, USELESS_OVERLOAD_BODY, + INVALID_PARAMSPEC, INVALID_TYPE_FORM, UNSOUND_RETURN_STATEMENT, USELESS_OVERLOAD_BODY, add_type_expression_reference_link, is_invalid_typed_dict_literal, report_implicit_return_type, report_invalid_generator_function_return_type, report_invalid_return_type, report_shadowed_type_variable, + report_unsound_return_statement, }, function::{ FunctionBodyKind, FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, @@ -28,9 +29,11 @@ use crate::{ nearest_enclosing_function, original_class_type, }, infer_scope_types, + relation::TypeRelation, signatures::ReturnCallableTypeVarScope, tuple::{TupleSpecBuilder, TupleType}, typed_dict::extract_unpacked_typed_dict_keys_from_kwargs_annotation, + typevar::TypeVarSet, }, }; use ty_python_core::{ @@ -133,9 +136,9 @@ impl<'db> ExpectedReturnType<'db> { env: &ProgramEnvironment<'db>, ty: Type<'db>, ) -> Type<'db> { - match ty { + match ty.resolve_type_alias(db) { Type::TypeIs(_) | Type::TypeGuard(_) => KnownClass::Bool.to_instance(db, env), - ty => ty, + _ => ty, } } @@ -163,8 +166,21 @@ impl<'db> ExpectedReturnType<'db> { /// Returns `true` if `ty` is accepted by either the public return type or the lexical return /// type. - fn accepts(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>) -> bool { - ty.is_assignable_to(db, env, self.public) || ty.is_assignable_to(db, env, self.lexical) + fn accepts( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + relation: TypeRelation, + ) -> bool { + let builder = ConstraintSetBuilder::new(); + + let check = + |target| ty.has_relation_to(db, env, target, &builder, TypeVarSet::None, relation); + + check(self.public) + .or(db, &builder, || check(self.lexical)) + .is_always_satisfied(db, env) } } @@ -262,23 +278,34 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } if let Some(expected_return_ty) = declared_ty.generator_return_type(db, env) { - for invalid in - self.return_types_and_ranges - .iter() - .copied() - .filter(|actual_return_ty| { - !actual_return_ty - .ty - .is_assignable_to(db, env, expected_return_ty) - }) - { - report_invalid_return_type( - &self.context, - invalid.range, - returns.range(), - expected_return_ty, - invalid.ty, - ); + for &return_statement in &self.return_types_and_ranges { + if !return_statement + .ty + .is_assignable_to(db, env, expected_return_ty) + { + report_invalid_return_type( + &self.context, + return_statement.range, + returns.range(), + expected_return_ty, + return_statement.ty, + ); + } else if self.context.is_lint_enabled(&UNSOUND_RETURN_STATEMENT) + && expected_return_ty.is_fully_static(db, env) + && !return_statement.ty.is_pure_redundant_with( + db, + env, + expected_return_ty, + ) + { + report_unsound_return_statement( + &self.context, + return_statement.range, + returns.range(), + expected_return_ty, + return_statement.ty, + ); + } } let use_def = self.index.use_def_map(scope_id); @@ -301,30 +328,53 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return; } - for invalid in self - .return_types_and_ranges - .iter() - .copied() - .filter_map(|ty_range| match ty_range.ty { - // We skip `is_assignable_to` checks for `NotImplemented`, - // so we remove it beforehand. - Type::Union(union) => Some(TypeAndRange { - ty: union.filter(db, |ty| !ty.is_notimplemented(db)), - range: ty_range.range, - }), - ty if ty.is_notimplemented(db) => None, - _ => Some(ty_range), - }) - .filter(|ty_range| !expected_return.accepts(db, env, ty_range.ty)) + for return_statement in + self.return_types_and_ranges + .iter() + .copied() + .filter_map(|ty_range| match ty_range.ty { + // We skip `is_assignable_to` checks for `NotImplemented`, + // so we remove it beforehand. + Type::Union(union) => Some(TypeAndRange { + ty: union.filter(db, |ty| !ty.is_notimplemented(db)), + range: ty_range.range, + }), + ty if ty.is_notimplemented(db) => None, + _ => Some(ty_range), + }) { - report_invalid_return_type( - &self.context, - invalid.range, - returns.range(), - declared_ty, - invalid.ty, - ); + if !expected_return.accepts( + db, + env, + return_statement.ty, + TypeRelation::Assignability, + ) { + report_invalid_return_type( + &self.context, + return_statement.range, + returns.range(), + declared_ty, + return_statement.ty, + ); + } else if self.context.is_lint_enabled(&UNSOUND_RETURN_STATEMENT) + && expected_return.public.is_fully_static(db, env) + && !expected_return.accepts( + db, + env, + return_statement.ty, + TypeRelation::Redundancy { pure: true }, + ) + { + report_unsound_return_statement( + &self.context, + return_statement.range, + returns.range(), + declared_ty, + return_statement.ty, + ); + } } + let use_def = self.index.use_def_map(scope_id); if can_implicitly_return_none(db, use_def) && !Type::none(db, env).is_assignable_to(db, env, expected_ty) diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index d5b140a0e6..b2e880de58 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -206,19 +206,6 @@ pub(crate) enum TypeRelation { SubtypingAssuming, } -/// Determines when comparisons involving type variables are evaluated. -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub(crate) enum TypeVarEvaluation { - /// Check immediately whether the relation holds for all or any valid specializations, - /// depending on whether the type variable is inferable. - Eager, - - /// Move comparisons involving a type variable into the constraint set for later evaluation. - /// - /// This is currently opt-in, but will eventually replace eager type-variable evaluation. - Lazy, -} - impl TypeRelation { pub(crate) const fn is_assignability(self) -> bool { matches!(self, TypeRelation::Assignability) @@ -236,6 +223,26 @@ impl TypeRelation { } } } + + pub(super) const fn description(self) -> &'static str { + match self { + TypeRelation::Assignability => "assignable to", + _ => "a subtype of", + } + } +} + +/// Determines when comparisons involving type variables are evaluated. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub(crate) enum TypeVarEvaluation { + /// Check immediately whether the relation holds for all or any valid specializations, + /// depending on whether the type variable is inferable. + Eager, + + /// Move comparisons involving a type variable into the constraint set for later evaluation. + /// + /// This is currently opt-in, but will eventually replace eager type-variable evaluation. + Lazy, } #[salsa::tracked] @@ -405,15 +412,46 @@ impl<'db> Type<'db> { db: &'db dyn Db, env: &ProgramEnvironment<'db>, target: Type<'db>, + ) -> ErrorContextTree<'db> { + self.relation_error_context(db, env, TypeRelation::Assignability, target) + } + + /// Re-run the pure redundancy check with error context collection enabled. + /// + /// This should normally be called when `is_pure_redundant_with` has returned `false` + /// and we are now about to emit a diagnostic where additional context could be + /// useful. + /// + /// This is a separate method so that we can skip this expensive check when diagnostics + /// are suppressed. + pub(crate) fn pure_redundancy_error_context( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + target: Type<'db>, + ) -> ErrorContextTree<'db> { + self.relation_error_context(db, env, TypeRelation::Redundancy { pure: true }, target) + } + + /// Re-run the relation check with error context collection enabled. + /// + /// This is a separate method so that we can skip this expensive check when diagnostics + /// are suppressed. + pub(crate) fn relation_error_context( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + relation: TypeRelation, + target: Type<'db>, ) -> ErrorContextTree<'db> { let builder = ConstraintSetBuilder::new(); let checker = TypeRelationChecker { env, constraints: &builder, inferable: TypeVarSet::None, - relation: TypeRelation::Assignability, + relation, typevar_evaluation: TypeVarEvaluation::Eager, - context_tree: Some(ErrorContextTree::new()), + context_tree: Some(ErrorContextTree::new(relation)), given: ConstraintSet::from_bool(&builder, false), perform_expensive_checks: true, relation_visitor: &HasRelationToVisitor::default(&builder), @@ -615,7 +653,34 @@ impl<'db> Type<'db> { is_redundant_with_impl(db, TypePair::new(db, program, self, other)) } - fn has_relation_to<'c>( + /// Return `true` if `self` is redundant with `other` under the pure redundancy relation. + /// + /// Unlike [`Self::is_redundant_with`], this does not apply shortcuts intended for simplifying + /// unions. + pub(super) fn is_pure_redundant_with( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: Type<'db>, + ) -> bool { + if self == other { + return true; + } + + let program = env.program(db); + let env = ProgramEnvironment::from_program(program); + self.has_relation_to( + db, + &env, + other, + &ConstraintSetBuilder::new(), + TypeVarSet::None, + TypeRelation::Redundancy { pure: true }, + ) + .is_always_satisfied(db, &env) + } + + pub(super) fn has_relation_to<'c>( self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, @@ -964,7 +1029,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { inferable: TypeVarSet::None, relation: TypeRelation::Assignability, typevar_evaluation: TypeVarEvaluation::Lazy, - context_tree: Some(ErrorContextTree::new()), + context_tree: Some(ErrorContextTree::new(TypeRelation::Assignability)), given: ConstraintSet::from_bool(constraints, false), perform_expensive_checks: true, relation_visitor, @@ -988,7 +1053,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { inferable: TypeVarSet::None, relation: TypeRelation::Assignability, typevar_evaluation: TypeVarEvaluation::Eager, - context_tree: Some(ErrorContextTree::new()), + context_tree: Some(ErrorContextTree::new(TypeRelation::Assignability)), given: ConstraintSet::from_bool(constraints, false), perform_expensive_checks: true, relation_visitor, @@ -1033,7 +1098,8 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { /// Return the collected error context, or an empty tree if collection was disabled. pub(super) fn into_error_context(self) -> ErrorContextTree<'db> { - self.context_tree.unwrap_or_else(ErrorContextTree::new) + self.context_tree + .unwrap_or_else(|| ErrorContextTree::new(self.relation)) } pub(super) fn always(&self) -> ConstraintSet<'db, 'c> { @@ -1818,12 +1884,12 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { { let elements_without_context = elements.len() - elements_context.len(); if elements_without_context > 0 && elements_without_context < elements.len() { - elements_context.push( + elements_context.push(ErrorContextTree::from_context( ErrorContext::NotAssignableToNOtherUnionElements { n: elements_without_context, - } - .into(), - ); + }, + self.relation, + )); } self.set_context( ErrorContext::NotAssignableToAnyUnionElement { diff --git a/crates/ty_python_semantic/src/types/relation_error.rs b/crates/ty_python_semantic/src/types/relation_error.rs index 9f7d0cc641..2cb8851019 100644 --- a/crates/ty_python_semantic/src/types/relation_error.rs +++ b/crates/ty_python_semantic/src/types/relation_error.rs @@ -1,4 +1,5 @@ use crate::Db; +use crate::types::relation::TypeRelation; /// This module defines a tree structure for collecting contextual information about type relation errors /// ("why is this complex type not assignable to that other complex type?"). use std::cell::{Cell, RefCell}; @@ -175,6 +176,7 @@ impl<'db> ErrorContext<'db> { &self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, + relation: TypeRelation, help_messages: &mut FxOrderSet, ) -> Option { let typed_dict_name = |typed_dict: &TypedDictType<'db>| match typed_dict { @@ -199,9 +201,10 @@ impl<'db> ErrorContext<'db> { [*element, *union, *target], ); format!( - "element `{}` of union `{}` is not assignable to `{}`", + "element `{}` of union `{}` is not {} `{}`", element.display_with(db, env, settings.clone()), union.display_with(db, env, settings.expand_numeric_tower_unions()), + relation.description(), target.display_with(db, env, settings), ) } @@ -209,8 +212,9 @@ impl<'db> ErrorContext<'db> { let settings = DisplaySettings::from_possibly_ambiguous_types(db, env, [*source, *union]); format!( - "type `{}` is not assignable to any element of the union `{}`", + "type `{}` is not {} any element of the union `{}`", source.display_with(db, env, settings.clone()), + relation.description(), union.display_with(db, env, settings.expand_numeric_tower_unions()), ) } @@ -223,8 +227,9 @@ impl<'db> ErrorContext<'db> { element, intersection, } => format!( - "type `{}` is not assignable to element `{}` of intersection `{}`", + "type `{}` is not {} element `{}` of intersection `{}`", source.display(db, env), + relation.description(), element.display(db, env), intersection.display(db, env), ), @@ -232,8 +237,9 @@ impl<'db> ErrorContext<'db> { intersection, target, } => format!( - "no element of intersection `{}` is assignable to `{}`", + "no element of intersection `{}` is {} `{}`", intersection.display(db, env), + relation.description(), target.display(db, env), ), Self::TypedDictFieldMissing { field_name, source } => { @@ -283,39 +289,44 @@ impl<'db> ErrorContext<'db> { source_field, target_field, } => format!( - "field \"{field_name}\" on {source} has type `{source_field}` which is not assignable to type `{target_field}` expected by {target}", + "field \"{field_name}\" on {source} has type `{source_field}` which is not {relation} type `{target_field}` expected by {target}", source = typed_dict_name(source), target = typed_dict_name(target), + relation = relation.description(), source_field = source_field.display(db, env), target_field = target_field.display(db, env), ), Self::TypedDictNotAssignableToDict(typed_dict) => { - help_messages.insert(HelpMessages::TypedDictNotAssignableToDict); + help_messages.insert(HelpMessages::TypedDictNotAssignableToDict(relation)); help_messages.insert(HelpMessages::ConsiderUsingMappingInsteadOfDict); format!( - "{source} is not assignable to `dict`", - source = typed_dict_name(typed_dict) + "{source} is not {relation} `dict`", + source = typed_dict_name(typed_dict), + relation = relation.description() ) } Self::OpenTypedDictNotAssignableToMapping { source, target } => { let name = source.defining_class().map(|class| class.name(db)); help_messages.insert(HelpMessages::OpenTypedDictNotAssignableToMapping { typed_dict_name: name.cloned(), + relation, }); help_messages.insert(HelpMessages::ExplainOpenTypedDictUnsoundness { typed_dict_name: name.cloned(), }); format!( - "{source} is not assignable to `{target}`", + "{source} is not {relation} `{target}`", source = typed_dict_name(source), + relation = relation.description(), target = target.display(db, env) ) } Self::IncompatibleReturnTypes { source, target } => format!( - "incompatible return types: `{source}` is not assignable to `{target}`", + "incompatible return types: `{source}` is not {relation} `{target}`", source = source.display(db, env), + relation = relation.description(), target = target.display(db, env), ), Self::IncompatibleParameterTypes { @@ -325,8 +336,9 @@ impl<'db> ErrorContext<'db> { } => { // reversed order due to contravariance of parameter types format!( - "{parameter} has an incompatible type: `{target}` is not assignable to `{source}`", + "{parameter} has an incompatible type: `{target}` is not {relation} `{source}`", source = source.display(db, env), + relation = relation.description(), target = target.display(db, env), ) } @@ -391,7 +403,8 @@ impl<'db> ErrorContext<'db> { source_len, target_len, } => format!( - "a tuple of length {source_len} is not assignable to a tuple of length {}", + "a tuple of length {source_len} is not {} a tuple of length {}", + relation.description(), target_len.display_minimum(), ), Self::TupleElementNotCompatible { @@ -408,22 +421,25 @@ impl<'db> ErrorContext<'db> { (n, c) => format!("tuple element {n} of {c}"), }; format!( - "{which} is not compatible: `{source}` is not assignable to `{target}`", + "{which} is not compatible: `{source}` is not {relation} `{target}`", source = source.display(db, env), + relation = relation.description(), target = target.display(db, env) ) } Self::TypeNotCompatibleWithProtocol { ty, protocol } => { if let Type::ProtocolInstance(_) = ty { format!( - "protocol `{}` is not assignable to protocol `{}`", + "protocol `{}` is not {} protocol `{}`", ty.display(db, env), + relation.description(), protocol.display(db, env), ) } else { format!( - "type `{}` is not assignable to protocol `{}`", + "type `{}` is not {} protocol `{}`", ty.display(db, env), + relation.description(), protocol.display(db, env), ) } @@ -444,8 +460,9 @@ impl<'db> ErrorContext<'db> { format!("protocol member `{member_name}` is incompatible") } Self::ProtocolMemberReadTypeIncompatible { source, target } => format!( - "read type `{source}` is not assignable to `{target}`", + "read type `{source}` is not {relation} `{target}`", source = source.display(db, env), + relation = relation.description(), target = target.display(db, env), ), Self::ProtocolMemberNotWritable => "the member is not writable".to_string(), @@ -460,12 +477,19 @@ impl<'db> ErrorContext<'db> { #[derive(Clone, Debug, PartialEq, Eq, Hash)] enum HelpMessages { RequiredFieldCouldBeRemoved, - TypedDictNotAssignableToDict, + TypedDictNotAssignableToDict(TypeRelation), ConsiderUsingMappingInsteadOfDict, TopCallableExplanation, - ConsiderAddingADefaultValue { parameter_name: Option }, - OpenTypedDictNotAssignableToMapping { typed_dict_name: Option }, - ExplainOpenTypedDictUnsoundness { typed_dict_name: Option }, + ConsiderAddingADefaultValue { + parameter_name: Option, + }, + OpenTypedDictNotAssignableToMapping { + typed_dict_name: Option, + relation: TypeRelation, + }, + ExplainOpenTypedDictUnsoundness { + typed_dict_name: Option, + }, } impl std::fmt::Display for HelpMessages { @@ -474,18 +498,24 @@ impl std::fmt::Display for HelpMessages { HelpMessages::RequiredFieldCouldBeRemoved => { f.write_str("The required field could be removed through a destructive operation like `del` on the target.") } - HelpMessages::TypedDictNotAssignableToDict => { - f.write_str("A TypedDict is not usually assignable to any `dict[..]` type; `dict` types allow destructive operations like `clear()`.") + HelpMessages::TypedDictNotAssignableToDict(relation) => { + write!( + f, + "A TypedDict is not usually {} any `dict[..]` type; \ + `dict` types allow destructive operations like `clear()`.", + relation.description() + ) } HelpMessages::ConsiderUsingMappingInsteadOfDict => { f.write_str("Consider using `Mapping[..]` instead of `dict[..]`.") } - HelpMessages::OpenTypedDictNotAssignableToMapping {typed_dict_name} => { + HelpMessages::OpenTypedDictNotAssignableToMapping {typed_dict_name, relation} => { let name = typed_dict_name.as_ref().map(|name|format!("`{name}`")).unwrap_or_else(||"this TypedDict".to_string()); write!( f, - "{name} would be assignable to this `Mapping` type \ - if it were declared with `closed=True`, but TypedDicts are open by default." + "{name} would be {relation} this `Mapping` type \ + if it were declared with `closed=True`, but TypedDicts are open by default.", + relation = relation.description() ) } HelpMessages::ExplainOpenTypedDictUnsoundness {typed_dict_name} => { @@ -529,16 +559,18 @@ impl<'db> ErrorContextNode<'db> { matches!(self.context, ErrorContext::Empty) && self.children.is_empty() } + #[expect(clippy::too_many_arguments)] fn render_tree( &self, db: &'db dyn Db, env: &ProgramEnvironment<'db>, + relation: TypeRelation, output_lines: &mut Vec, help_messages: &mut FxOrderSet, prefix: &str, continuation: &str, ) { - if let Some(line) = self.context.render(db, env, help_messages) { + if let Some(line) = self.context.render(db, env, relation, help_messages) { output_lines.push(format!("{prefix}{line}")); } @@ -553,6 +585,7 @@ impl<'db> ErrorContextNode<'db> { child.render_tree( db, env, + relation, output_lines, help_messages, &child_prefix, @@ -566,34 +599,35 @@ impl<'db> ErrorContextNode<'db> { pub(crate) struct ErrorContextTree<'db> { root: Rc>>, enabled: Cell, + relation: TypeRelation, } impl PartialEq for ErrorContextTree<'_> { fn eq(&self, other: &Self) -> bool { - *self.root.borrow() == *other.root.borrow() + *self.root.borrow() == *other.root.borrow() && self.relation == other.relation } } impl Eq for ErrorContextTree<'_> {} -impl<'db> From> for ErrorContextTree<'db> { - fn from(context: ErrorContext<'db>) -> Self { +impl<'db> ErrorContextTree<'db> { + /// Create a new, empty error context tree with collection enabled. + pub(crate) fn new(relation: TypeRelation) -> Self { Self { - root: Rc::new(RefCell::new(ErrorContextNode { - context, - children: Vec::new(), - })), + root: Rc::default(), enabled: Cell::new(true), + relation, } } -} -impl<'db> ErrorContextTree<'db> { - /// Create a new, empty error context tree with collection enabled. - pub(crate) fn new() -> Self { + pub(crate) fn from_context(context: ErrorContext<'db>, relation: TypeRelation) -> Self { Self { - root: Rc::default(), + root: Rc::new(RefCell::new(ErrorContextNode { + context, + children: Vec::new(), + })), enabled: Cell::new(true), + relation, } } @@ -644,6 +678,7 @@ impl<'db> ErrorContextTree<'db> { ErrorContextTree { root: Rc::new(RefCell::new(std::mem::take(&mut *self.root.borrow_mut()))), enabled: Cell::new(self.enabled.get()), + relation: self.relation, } } @@ -656,9 +691,15 @@ impl<'db> ErrorContextTree<'db> { ) { let mut output_lines = Vec::new(); let mut help_messages = FxOrderSet::default(); - self.root - .borrow() - .render_tree(db, env, &mut output_lines, &mut help_messages, "", ""); + self.root.borrow().render_tree( + db, + env, + self.relation, + &mut output_lines, + &mut help_messages, + "", + "", + ); for line in output_lines { diag.info(line); } diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index c29900f475..e79349d5ad 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -1274,19 +1274,21 @@ pub(crate) fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( typed_dict: TypedDictType<'db>, visitor: &V, ) { - match typed_dict { - TypedDictType::Class(defining_class) => { - visitor.visit_type(db, defining_class.into()); - } - TypedDictType::Synthesized(synthesized) => { - for field in synthesized.items(db).values() { - visitor.visit_type(db, field.declared_ty); - } - if let Some(extra_items) = synthesized.openness(db).explicit_extra_items() { - visitor.visit_type(db, extra_items.declared_ty); - } + if let TypedDictType::Class(defining_class) = typed_dict { + visitor.visit_type(db, defining_class.into()); + + if !visitor.should_visit_lazy_type_attributes() { + return; } } + + for field in typed_dict.items(db).values() { + visitor.visit_type(db, field.declared_ty); + } + + if let Some(extra_items) = typed_dict.explicit_extra_items(db) { + visitor.visit_type(db, extra_items.declared_ty); + } } #[salsa::tracked( diff --git a/crates/ty_python_semantic/src/types/visitor.rs b/crates/ty_python_semantic/src/types/visitor.rs index b7c9b0bf97..72a2e6ba99 100644 --- a/crates/ty_python_semantic/src/types/visitor.rs +++ b/crates/ty_python_semantic/src/types/visitor.rs @@ -5,6 +5,7 @@ use std::hash::Hash; use rustc_hash::{FxBuildHasher, FxHashSet}; use smallvec::SmallVec; +use ty_python_core::definition::Definition; use crate::types::{ BoundMethodType, BoundSuperType, BoundTypeVarInstance, CallableType, EnumComplementType, @@ -392,12 +393,12 @@ impl SmallSet { } } -/// Whether a type contains a non-`Any` dynamic type. +/// Whether a type contains a dynamic type matching the requested filter. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(super) enum DynamicContent { - /// The type was fully inspected and contains no non-`Any` dynamic type. + /// The type was fully inspected and contains no matching dynamic type. Absent, - /// The type contains a non-`Any` dynamic type. + /// The type contains a matching dynamic type. Present, /// Recursive specialization prevented the type from being fully inspected. Indeterminate, @@ -409,6 +410,15 @@ impl DynamicContent { } } +/// Determine whether `ty` contains any dynamic type. +pub(super) fn dynamic_content<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, +) -> DynamicContent { + dynamic_content_impl(db, env, ty, true) +} + /// Determine whether `ty` contains a dynamic type other than `Any`. /// /// Class-based protocol interfaces can be recursively specialized. An exact recursive cycle adds @@ -429,12 +439,24 @@ pub(super) fn non_any_dynamic_content<'db>( db: &'db dyn Db, env: &ProgramEnvironment<'db>, ty: Type<'db>, +) -> DynamicContent { + dynamic_content_impl(db, env, ty, false) +} + +fn dynamic_content_impl<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + include_any: bool, ) -> DynamicContent { struct DynamicContentVisitor<'a, 'db> { env: &'a ProgramEnvironment<'db>, recursion_guard: TypeCollector<'db>, active_class_protocols: ActiveRecursionDetector>, + active_class_typed_dicts: ActiveRecursionDetector>, + active_type_aliases: ActiveRecursionDetector>, content: Cell, + include_any: bool, } impl DynamicContentVisitor<'_, '_> { @@ -459,7 +481,10 @@ pub(super) fn non_any_dynamic_content<'db>( return; } - if ty.is_dynamic() && !matches!(ty, Type::Dynamic(crate::types::DynamicType::Any)) { + if ty.is_dynamic() + && (self.include_any + || !matches!(ty, Type::Dynamic(crate::types::DynamicType::Any))) + { self.record(DynamicContent::Present); return; } @@ -467,6 +492,14 @@ pub(super) fn non_any_dynamic_content<'db>( walk_type_with_recursion_guard(db, ty, self, &self.recursion_guard); } + fn visit_type_alias_type(&self, db: &'db dyn Db, alias: TypeAliasType<'db>) { + self.active_type_aliases.visit( + &alias.definition(db), + || self.record(DynamicContent::Indeterminate), + || walk_type_alias_type(db, alias, self), + ); + } + fn visit_protocol_instance_type( &self, db: &'db dyn Db, @@ -501,13 +534,33 @@ pub(super) fn non_any_dynamic_content<'db>( }, ); } + + fn visit_typed_dict_type(&self, db: &'db dyn Db, typed_dict: TypedDictType<'db>) { + let Some(class) = typed_dict.defining_class() else { + walk_typed_dict_type(db, typed_dict, self); + return; + }; + let Some((origin, _)) = class.static_class_literal(db) else { + walk_typed_dict_type(db, typed_dict, self); + return; + }; + + self.active_class_typed_dicts.visit( + &origin, + || self.record(DynamicContent::Indeterminate), + || walk_typed_dict_type(db, typed_dict, self), + ); + } } let visitor = DynamicContentVisitor { env, recursion_guard: TypeCollector::default(), active_class_protocols: ActiveRecursionDetector::default(), + active_class_typed_dicts: ActiveRecursionDetector::default(), + active_type_aliases: ActiveRecursionDetector::default(), content: Cell::new(DynamicContent::Absent), + include_any, }; visitor.visit_type(db, ty); visitor.content.get() diff --git a/crates/ty_test/src/db.rs b/crates/ty_test/src/db.rs index e2f7d74788..a2bad29593 100644 --- a/crates/ty_test/src/db.rs +++ b/crates/ty_test/src/db.rs @@ -321,26 +321,31 @@ fn mdtest_analysis_settings(options: Option<&Analysis>) -> AnalysisSettings { } fn mdtest_rule_selection(rules: Option<&Rules>, required_rule: Option<&str>) -> RuleSelection { + // In general (as shown by the initialization of `selection` below), we enable even rules that + // are ignored by default in mdtests so that their behaviour is covered alongside the default + // rules. There are a few small exceptions to this, however: + static DISABLED_IN_MDTESTS: &[&str] = &[ + // `missing-override-decorator` is an exception: because it is extremely pedantic we have + // chosen to keep it opt-in to minimize churn in unrelated tests. + "missing-override-decorator", + // `experimental-syntax` is also an exception: we make use of `&` and `~` for intersection and + // negation types in our tests for better readability. + "experimental-syntax", + // `unsound-return-statement` is also an exception because it is very strict, would result in + // lots of additional diagnostics in mdtests, and is not the default behaviour we'll show to + // our users. + "unsound-return-statement", + ]; + let registry = default_lint_registry(); let mut selection = RuleSelection::all(registry, Severity::Info); - // In general (as shown by the initialization of `selection` above), we enable even rules that - // are ignored by default in mdtests so that their behaviour is covered alongside the default - // rules. - // - // `missing-override-decorator` is an exception: because it is extremely pedantic we have - // chosen to keep it opt-in to minimize churn in unrelated tests. - let missing_override_decorator = registry - .get("missing-override-decorator") - .expect("missing-override-decorator is a known lint rule"); - selection.disable(missing_override_decorator); - - // `experimental-syntax` is also an exception: we make use of `&` and `~` for intersection and - // negation types in our tests for better readability. - let experimental_syntax = registry - .get("experimental-syntax") - .expect("experimental-syntax is a known lint rule"); - selection.disable(experimental_syntax); + for rule in DISABLED_IN_MDTESTS { + let lint = registry + .get(rule) + .unwrap_or_else(|error| panic!("Unknown lint rule `{rule}`: {error}")); + selection.disable(lint); + } if let Some(rules) = rules { let set_lint_level = diff --git a/ty.schema.json b/ty.schema.json index 14d8b3f14e..0956a11078 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -1484,6 +1484,16 @@ } ] }, + "unsound-return-statement": { + "title": "detects return statements that unsoundly return a type that is not a subtype of the function's annotated return type", + "description": "## What it does\n\nDetects `return` statements that unsoundly return a type that is not a [subtype] of the function's\nannotated return type.\n\nThis lint is a stricter version of `invalid-return-type`.\n\n## Why is this bad?\n\nBy default, type checkers consider a `return` statement valid if the inferred type of the object\nbeing returned is [assignable] to the annotated return type of the function it's in. However, this\nmakes it easy for incorrect types to percolate through your code unexpectedly due to a single\nexpression being inferred as `Any`. This can easily lead to runtime errors that are not caught by\nthe type checker:\n\n```py\nfrom typing import Any\n\n\ndef returns_any() -> Any:\n return \"foo\"\n\n\ndef returns_int() -> int:\n # error: \"Unsound return statement: `Any` is not a subtype of `int`\"\n return returns_any()\n\n\n# fails at runtime, even though the type checker infers both operands as being of type `int`!\nreturns_int() + 42\n```\n\nThis rule allows you to use [\"fully static\"][fully-static] return types as \"typed boundaries\" for\nyour code. With this rule enabled, ty would emit an error on the `return returns_any()` statement\nin `returns_int`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not\na subtype of `int`. This helps prevent the unsoundness from spreading far from its original source\n(in this case, the return type of the `returns_any` function).\n\nNote that this rule is only applied to functions annotated as returning\n[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in\nyour return type, either implicitly or explicitly:\n\n```py\nfrom typing import Any\n\n\ndef returns_any() -> Any:\n return \"foo\"\n\n\n# error: [missing-type-argument]\ndef returns_unparameterized_tuple() -> tuple:\n # no error, since the return type is implicitly `tuple[Unknown, ...]`\n # (which is what the `missing-type-argument` error is complaining about on the line above!)\n return returns_any()\n\n\ndef returns_list_of_any() -> list[Any]:\n # no error, since the return type is explicitly `list[Any]`\n return returns_any()\n```\n\nThis rule works especially well when combined with ty's\n`missing-type-argument` rule, and the Ruff rules [`ANN201`][ann201],\n[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all\nthese rules at once effectively makes it much less likely that a `return` statement can lead to\nunsoundness \"leaking\" out of a function unless that function has been *explicitly* annotated with\na dynamic type in some way (`-> Any` or `-> tuple[Any]`, for example).\n\nThis rule is analogous to mypy's [`no-any-return`][no-any-return] error code, which is enabled by\nmypy’s [`--strict`][mypy-strict] mode and can also be enabled on its own using mypy’s\n[`--warn-return-any`][warn-return-any] option.\n\n## Examples\n\n```py\nfrom typing import Any\n\n\ndef returns_any() -> Any:\n return 42\n\n\ndef returns_int() -> int:\n # error: \"Unsound return statement: `Any` is not a subtype of `int`\"\n return returns_any()\n```\n\nNarrow the type to a subtype of `int` to fix the diagnostic:\n\n```py\nfrom typing import Any\nfrom typing_extensions import reveal_type\n\n\ndef returns_any() -> Any:\n return 42\n\n\ndef returns_int() -> int:\n my_int = returns_any()\n assert isinstance(my_int, int)\n reveal_type(my_int) # revealed: Any & int\n return my_int # no error: `Any & int` is a subtype of `int`\n```\n\n## Default level\n\nThis rule is disabled by default. It is intended for advanced users wanting additional soundness\nchecks from their type checker, not for users who have just started to use type checkers on their\nPython code.\n\n[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/\n[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/\n[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/\n[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/\n[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/\n[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable\n[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type\n[mypy-strict]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict\n[no-any-return]: https://mypy.readthedocs.io/en/stable/error_code_list2.html#code-no-any-return\n[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype\n[warn-return-any]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-warn-return-any", + "default": "ignore", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "unsupported-base": { "title": "detects class bases that are unsupported as ty could not feasibly calculate the class's MRO", "description": "## What it does\n\nChecks for class definitions that have bases which are unsupported by ty.\n\n## Why is this bad?\n\nIf a class has a base that is an instance of a complex type such as a union type,\nty will not be able to resolve the [method resolution order] (MRO) for the class.\nThis will lead to an inferior understanding of your codebase and unpredictable\ntype-checking behavior.\n\n## Examples\n\n```python\nimport datetime\n\n\nclass A: ...\n\n\nclass B: ...\n\n\nif datetime.date.today().weekday() != 6:\n C = A\nelse:\n C = B\n\n\nclass D(C): ... # error: [unsupported-base]\n```\n\n[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order", From 9a908ef2a99b00056e465dc7aec55f69304b64c0 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 7 Aug 2026 14:28:31 +0100 Subject: [PATCH 324/390] [ty] Enable missing ecosystem analysis rules (#27559) --- .github/ty-ecosystem.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ty-ecosystem.toml b/.github/ty-ecosystem.toml index 295b2959ce..613aaa8bc9 100644 --- a/.github/ty-ecosystem.toml +++ b/.github/ty-ecosystem.toml @@ -3,7 +3,9 @@ # Enable off-by-default rules. [rules] +blanket-ignore-comment = "warn" division-by-zero = "warn" +missing-type-argument = "warn" possibly-missing-attribute = "warn" possibly-missing-import = "warn" possibly-unresolved-reference = "warn" From 55c7d4850556ebba7d929d8ce340e79c745882b6 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 7 Aug 2026 08:10:16 -0700 Subject: [PATCH 325/390] [ty] Consider object members in protocol comparisons (#27532) ## Summary Consider attributes inherited from `object` when checking assignability between protocol types. - Reuse the same fallback policy in structural comparison and missing-member fast paths while preserving signature compatibility. - Preserve fast protocol mismatch rejection by caching `object` member names and each protocol's non-`object` requirement count. - Keep `__hash__` excluded because subclasses can disable hashing; `Iterator[int]` therefore remains incompatible with `Hashable`. Related to astral-sh/ty#4196. ## Test plan - Added mdtests for protocol requirements satisfied by `object.__repr__`. - Covered incompatible inherited method signatures and both custom and standard-library hashability requirements. - Verified the existing many-protocol-members mismatch benchmark retains its fast rejection path. --- .../resources/mdtest/protocols.md | 47 ++++++++++++++ .../src/types/protocol_class.rs | 62 +++++++++++++++++-- 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 15aabf3117..d476e1ae8c 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -4915,6 +4915,53 @@ static_assert(not is_assignable_to(MethodPSuper, MethodPUnrelated)) static_assert(not is_assignable_to(MethodPSuper, MethodPSub)) ``` +## Object members in protocol-to-protocol comparisons + +A protocol inherits ordinary `object` members even when they are not part of its declared interface. +Those inherited members can satisfy compatible requirements on another protocol. + +```py +from collections.abc import Hashable, Iterator +from typing import Literal, Protocol +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_subtype_of + +class HasValue(Protocol): + value: int + +class HasValueAndRepr(Protocol): + value: int + + def __repr__(self) -> str: ... + +static_assert(is_assignable_to(HasValue, HasValueAndRepr)) +static_assert(is_subtype_of(HasValue, HasValueAndRepr)) +``` + +An inherited method must still have a compatible signature. + +```py +class HasValueAndPreciseRepr(Protocol): + value: int + + def __repr__(self) -> Literal["precise"]: ... + +static_assert(not is_assignable_to(HasValue, HasValueAndPreciseRepr)) +``` + +Unlike other inherited `object` methods, `__hash__` can be disabled by a subclass. Protocols must +explicitly require a callable `__hash__` before they can satisfy a hashability requirement. + +```py +class HasValueAndHash(Protocol): + value: int + + def __hash__(self) -> int: ... + +static_assert(not is_assignable_to(HasValue, HasValueAndHash)) +static_assert(not is_assignable_to(Iterator[int], Hashable)) +``` + ## Subtyping between protocols with method members and protocols with non-method members A protocol with a method member can be considered a subtype of a protocol with a read-only diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index f2b533a374..d2135c6f9d 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -415,6 +415,22 @@ impl<'db> ProtocolInterfaceView<'db> { self.interface.includes_member(db, name) } + /// Includes inherited `object` members except `__hash__`, which subclasses can disable. + fn includes_member_or_object_fallback( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + name: &str, + ) -> bool { + self.includes_member(db, name) + || name != "__hash__" + && object_member_names(db, self.interface.program(db)).contains(name) + && matches!( + Type::object().member(db, env, name).place, + Place::Defined(place) if place.is_definitely_defined() + ) + } + /// Compare the original and materialized forms of members required by `required`. /// /// An unrelated materialized member must not prevent a protocol from retaining its @@ -3313,6 +3329,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { ) -> ConstraintSet<'db, 'c> { if source.member_count(db) < target.member_count(db) && !self.is_context_collection_enabled() + && source.member_count(db) < non_object_protocol_member_count(db, target.interface) { return self.never(); } @@ -3324,6 +3341,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { .when_all(db, self.constraints, |target_member| { let source_member = source.member_by_name(db, target_member.name); + if source_member.is_none() + && source.includes_member_or_object_fallback(db, env, target_member.name) + { + return self.type_satisfies_protocol_member(db, source_type, &target_member); + } + if let Some(context) = self.report_context() && source_member.is_none() { @@ -3569,6 +3592,35 @@ impl<'db> ProtocolMemberCandidate<'db> { } } +/// Cache `object` member names so missing protocol members can be rejected without member lookup. +#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] +fn object_member_names<'db>(db: &'db dyn Db, program: Program<'db>) -> FxHashSet { + let env = ProgramEnvironment::from_program(program); + let Some((object, _)) = ClassType::object(db, &env).static_class_literal(db) else { + return FxHashSet::default(); + }; + + let mut names = place_table(db, object.body_scope(db)) + .symbols() + .map(|symbol| symbol.name().clone()) + .collect::>(); + names.shrink_to_fit(); + names +} + +/// Count protocol requirements that cannot be supplied by inherited `object` members. +#[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] +fn non_object_protocol_member_count<'db>( + db: &'db dyn Db, + interface: ProtocolInterface<'db>, +) -> usize { + let inherited_member_count = object_member_names(db, interface.program(db)) + .iter() + .filter(|name| name.as_str() != "__hash__" && interface.includes_member(db, name)) + .count(); + interface.member_count(db) - inherited_member_count +} + /// Inner Salsa query for [`ProtocolClass::interface`]. #[salsa::tracked( returns(copy), @@ -3728,10 +3780,12 @@ pub(super) fn has_all_protocol_members_defined<'db>( Type::ProtocolInstance(source_protocol) => { let source_interface = source_protocol.interface(db); - source_interface.member_count(db) >= target_interface.member_count(db) - && target_interface - .members(db) - .all(|member| source_interface.includes_member(db, member.name())) + (source_interface.member_count(db) >= target_interface.member_count(db) + || source_interface.member_count(db) + >= non_object_protocol_member_count(db, target_interface.interface)) + && target_interface.members(db).all(|member| { + source_interface.includes_member_or_object_fallback(db, env, member.name()) + }) } _ => target_interface.members(db).all(|member| { matches!( From 2aff056803a3c92b38960e5af5fe987618e33423 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 7 Aug 2026 08:11:59 -0700 Subject: [PATCH 326/390] [ty] Stabilize recursive gradual type alias materialization (#27563) ## Summary Avoid hangs / stack overflows in materializing recursive type aliases. Preserve Top/Bottom polarity explicitly on the alias, materializing the alias body lazily through cycle-safe Salsa queries. - Retain alias specializations and fully static recursive identities while displaying materialized aliases unambiguously in diagnostics. Fixes astral-sh/ty#4205. ## Test plan - Equality narrowing of recursive sequences containing gradual mapping keys or values. - Subtyping of covariant recursive aliases containing gradual invariant branches. - Top/Bottom and mixed-polarity idempotence, recursive branch polarity, distinct nested alias displays and assignment diagnostics, manually constructed aliases, and distinguishable generic alias specializations. --- .../mdtest/narrow/conditionals/eq.md | 31 ++++++ .../mdtest/type_properties/is_subtype_of.md | 10 ++ .../mdtest/type_properties/materialization.md | 65 +++++++++++++ crates/ty_python_semantic/src/types.rs | 31 +++++- .../ty_python_semantic/src/types/display.rs | 22 ++++- .../src/types/infer/builder.rs | 6 +- .../src/types/type_alias.rs | 95 +++++++++++++++++-- 7 files changed, 242 insertions(+), 18 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index c404491f42..4d706a2c89 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -1138,6 +1138,37 @@ def _(left: Recursive, right: EnumValue): reveal_type(left == right) # revealed: bool ``` +## Recursive aliases containing gradual generic branches + +Equality narrowing must terminate when a recursive sequence alias contains a mapping with a gradual +key. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from collections.abc import Mapping, Sequence +from typing import Any + +type RecursiveMappingKey = Sequence[RecursiveMappingKey] | Mapping[Any, int] + +def narrow_recursive_mapping_key(value: RecursiveMappingKey) -> None: + assert value == 0 + _ = value +``` + +A gradual mapping value also must not cause recursive materialization to unfold indefinitely. + +```py +type RecursiveMappingValue = Sequence[RecursiveMappingValue] | Mapping[int, Any] + +def narrow_recursive_mapping_value(value: RecursiveMappingValue) -> None: + assert value == 0 + _ = value +``` + ## Known built-in equality behavior `bool`, `LiteralString`, `TypedDict`, and final classes that inherit `object.__eq__` have known diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 7881a44ffd..40b54fab60 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -977,6 +977,16 @@ static_assert(not is_subtype_of(type[Any], type[Arbitrary])) static_assert(is_subtype_of(type[Any], type[object])) ``` +A covariant specialization whose argument is a recursive alias remains a subtype of the same +specialization with `object`. A gradual invariant branch must not cause recursive materialization to +unfold indefinitely. + +```pyi +type RecursiveGradual = Covariant[RecursiveGradual] | Invariant[Any] + +static_assert(is_subtype_of(Covariant[RecursiveGradual], Covariant[object])) +``` + ## Callable The general principle is that a callable type is a subtype of another if it's more flexible in what diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md index 0cbc637f23..c291cf8c4f 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/materialization.md @@ -1121,6 +1121,11 @@ def _( `Top[T]` and `Bottom[T]` are always fully static types. Therefore, they have only one materialization (themselves) and applying `Top` or `Bottom` again does nothing. +```toml +[environment] +python-version = "3.12" +``` + ```py from typing import Any from ty_extensions import Top, Bottom, static_assert @@ -1133,6 +1138,66 @@ static_assert(is_equivalent_to(Bottom[Bottom[list[Any]]], Bottom[list[Any]])) static_assert(is_equivalent_to(Top[Bottom[list[Any]]], Bottom[list[Any]])) ``` +The same is true when a covariant specialization contains a recursive alias with a gradual invariant +branch. Materializing the recursive branch again must not unfold another layer. + +```py +class Covariant[T]: + def get(self) -> T: + raise NotImplementedError + +class Invariant[T]: + value: T + +type Recursive = Covariant[Recursive] | Invariant[Any] + +static_assert(is_equivalent_to(Top[Covariant[Recursive]], Top[Top[Covariant[Recursive]]])) +static_assert(is_equivalent_to(Bottom[Covariant[Recursive]], Bottom[Bottom[Covariant[Recursive]]])) +static_assert(is_equivalent_to(Top[Covariant[Recursive]], Bottom[Top[Covariant[Recursive]]])) +static_assert(is_equivalent_to(Bottom[Covariant[Recursive]], Top[Bottom[Covariant[Recursive]]])) +``` + +Both branches retain the requested materialization polarity. + +```py +def recursive_materializations(top: Top[Recursive], bottom: Bottom[Recursive]) -> None: + reveal_type(top) # revealed: Covariant[Top[Recursive]] | Top[Invariant[Any]] + reveal_type(bottom) # revealed: Covariant[Bottom[Recursive]] | Bottom[Invariant[Any]] +``` + +Nested recursive aliases preserve their materialization polarity in displays and diagnostics. + +```py +def nested_recursive_materializations(top: Top[Covariant[Recursive]], bottom: Bottom[Covariant[Recursive]]) -> None: + reveal_type(top) # revealed: Covariant[Top[Recursive]] + reveal_type(bottom) # revealed: Covariant[Bottom[Recursive]] + + # error: [invalid-assignment] "Object of type `Covariant[Top[Recursive]]` is not assignable to `Covariant[Bottom[Recursive]]`" + bottom = top +``` + +Explicitly constructed recursive aliases preserve the same materialized identity. + +```py +from typing_extensions import TypeAliasType + +ManualRecursive = TypeAliasType("ManualRecursive", "Covariant[ManualRecursive] | Invariant[Any]") + +static_assert(is_equivalent_to(Top[Covariant[ManualRecursive]], Top[Top[Covariant[ManualRecursive]]])) +``` + +Materialization also preserves the specialization of a recursive generic alias. + +```py +type GenericRecursive[T] = Covariant[GenericRecursive[T]] | Invariant[Any] | T + +static_assert(is_equivalent_to(Top[GenericRecursive[int]], Top[Top[GenericRecursive[int]]])) +static_assert(not is_equivalent_to(Top[GenericRecursive[int]], Top[GenericRecursive[str]])) + +def generic_recursive_materialization(value: Top[Covariant[GenericRecursive[int]]]) -> None: + reveal_type(value) # revealed: Covariant[Top[GenericRecursive[int]]] +``` + ## Subtyping Any `list[T]` is a subtype of `Top[list[Any]]`, but with more restrictive gradual types, not all diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index f2c605577f..8f42dea3c6 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -492,10 +492,10 @@ pub(crate) struct FindLegacyTypeVars; type SpecializationVisitor<'db> = CycleDetector<'db, VisitSpecialization, Type<'db>, (), 3>; struct VisitSpecialization; -/// How a generic type has been specialized. +/// Whether a type represents the upper or lower bound of a gradual type. /// -/// This matters only if there is at least one invariant or constrained type parameter. -/// For example, we represent `Top[list[Any]]` as a `GenericAlias` with +/// For generic specializations, this matters only if there is at least one invariant or constrained +/// type parameter. For example, we represent `Top[list[Any]]` as a `GenericAlias` with /// `MaterializationKind` set to Top, which we denote as `Top[list[Any]]`. /// A type `Top[list[T]]` includes all fully static list types `list[U]` where `U` is /// a supertype of `Bottom[T]` and a subtype of `Top[T]`. @@ -503,6 +503,9 @@ struct VisitSpecialization; /// Similarly, there is `Bottom[list[Any]]`. /// This type is harder to make sense of in a set-theoretic framework, but /// it is a subtype of all materializations of `list[Any]`. +/// +/// Recursive type aliases also retain their materialization kind so that materializing the alias +/// body preserves stable recursive references. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum MaterializationKind { Top, @@ -7678,6 +7681,13 @@ impl<'db> Type<'db> { Type::TypeAlias(alias) => { match type_mapping { + TypeMapping::Materialize(_) if alias.materialization_kind(db).is_some() => + { + self + } + TypeMapping::EagerExpansion if alias.materialization_kind(db).is_some() => { + alias.value_type(db).expand_eagerly(db, visitor.env) + } // For EagerExpansion, expand the raw value type. This path relies on Salsa's cycle // detection rather than the visitor's cycle detection, because the visitor tracks // Type values and `RecursiveList` is different from `RecursiveList[T]`. @@ -7722,9 +7732,20 @@ impl<'db> Type<'db> { }); // If the type mapping does not result in any change to this type alias, keep the - // alias node instead of eagerly expanding it. - if alias.value_type(db) == mapped { + // alias node instead of eagerly expanding it. A recursive backedge also returns + // the alias itself, and fully static aliases must retain their original identity. + if mapped == self || alias.value_type(db) == mapped { self + } else if let TypeMapping::Materialize(materialization_kind) = type_mapping + && matches!( + self.to_type_identity(db), + cyclic::TypeIdentity::RecursiveTypeAlias(_) + ) + { + Type::TypeAlias(alias.with_materialization_kind( + db, + Some(*materialization_kind), + )) } else { mapped } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 61fbae4636..a6b7f65a06 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1552,15 +1552,29 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'_, 'db> { f.write_char('>') } Type::TypeAlias(alias) => { + let materialization_kind = alias.materialization_kind(db); + if let Some(kind) = materialization_kind { + let (name, form) = match kind { + MaterializationKind::Top => ("Top", SpecialFormType::Top), + MaterializationKind::Bottom => ("Bottom", SpecialFormType::Bottom), + }; + f.with_type(Type::SpecialForm(form)).write_str(name)?; + f.write_char('[')?; + } + alias .display_with(db, self.settings.clone()) .fmt_detailed(f)?; - match alias.specialization(db) { - None => Ok(()), - Some(specialization) => specialization + if let Some(specialization) = alias.specialization(db) { + specialization .display_short(db, self.env, TupleSpecialization::No, self.settings.clone()) - .fmt_detailed(f), + .fmt_detailed(f)?; } + + if materialization_kind.is_some() { + f.write_char(']')?; + } + Ok(()) } Type::NewTypeInstance(newtype) => f.with_type(self.ty).write_str(newtype.name(db)), } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b8c5448e31..d0f2bd0184 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2081,7 +2081,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let type_alias_ty = Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695( - PEP695TypeAliasType::new(self.db(), alias_name, rhs_scope, None), + PEP695TypeAliasType::new(self.db(), alias_name, rhs_scope, None, None), ))); self.store_expression_type(&type_alias.name, type_alias_ty); @@ -3849,7 +3849,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.deferred.insert(definition); Type::KnownInstance(KnownInstanceType::TypeAliasType( - TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new(db, name, definition, None)), + TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( + db, name, definition, None, None, + )), )) } diff --git a/crates/ty_python_semantic/src/types/type_alias.rs b/crates/ty_python_semantic/src/types/type_alias.rs index 38398a465b..a2cbe880d0 100644 --- a/crates/ty_python_semantic/src/types/type_alias.rs +++ b/crates/ty_python_semantic/src/types/type_alias.rs @@ -5,7 +5,7 @@ use crate::{ Db, FxOrderSet, types::{ ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, GenericContext, - KnownInstanceType, Type, TypeContext, TypeMapping, TypeVarVariance, + KnownInstanceType, MaterializationKind, Type, TypeContext, TypeMapping, TypeVarVariance, definition_expression_type, display::qualified_name_components_from_scope, generics::{ApplySpecialization, Specialization, bind_typevar}, @@ -33,6 +33,10 @@ pub struct PEP695TypeAliasType<'db> { #[returns(copy)] pub(super) specialization: Option>, + + /// Keeps recursive references stable while their alias body is materialized lazily. + #[returns(copy)] + pub(super) materialization_kind: Option, } // The Salsa heap is tracked separately. @@ -43,7 +47,7 @@ pub(super) fn walk_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + ?Sized type_alias: PEP695TypeAliasType<'db>, visitor: &V, ) { - visitor.visit_type(db, type_alias.value_type(db)); + visitor.visit_type(db, TypeAliasType::PEP695(type_alias).value_type(db)); } #[salsa::tracked] @@ -106,6 +110,7 @@ impl<'db> PEP695TypeAliasType<'db> { self.name(db), self.rhs_scope(db), Some(specialization), + self.materialization_kind(db), ) } } @@ -144,6 +149,10 @@ pub struct ManualPEP695TypeAliasType<'db> { #[returns(copy)] pub(super) specialization: Option>, + + /// Keeps recursive references stable while their alias body is materialized lazily. + #[returns(copy)] + pub(super) materialization_kind: Option, } // The Salsa heap is tracked separately. @@ -154,7 +163,7 @@ pub(super) fn walk_manual_pep_695_type_alias<'db, V: visitor::TypeVisitor<'db> + type_alias: ManualPEP695TypeAliasType<'db>, visitor: &V, ) { - visitor.visit_type(db, type_alias.value_type(db)); + visitor.visit_type(db, TypeAliasType::ManualPEP695(type_alias).value_type(db)); } #[salsa::tracked] @@ -215,6 +224,7 @@ impl<'db> ManualPEP695TypeAliasType<'db> { self.name(db), self.definition(db), Some(f(generic_context)), + self.materialization_kind(db), ) } @@ -330,12 +340,40 @@ impl<'db> TypeAliasType<'db> { } pub fn value_type(self, db: &'db dyn Db) -> Type<'db> { + if let Some(materialization_kind) = self.materialization_kind(db) { + return self.materialized_value_type(db, materialization_kind); + } + match self { TypeAliasType::PEP695(type_alias) => type_alias.value_type(db), TypeAliasType::ManualPEP695(type_alias) => type_alias.value_type(db), } } + /// Materialize the alias body lazily, keeping this alias as the recursive fallback. + /// + /// Comparing a recursive specialization with its materialization can request this same body + /// before it has finished materializing. Returning the already-marked alias closes that cycle + /// without losing its materialization polarity. + #[salsa::tracked( + returns(copy), + cycle_initial=|_, _, alias: TypeAliasType<'db>, _| Type::TypeAlias(alias), + heap_size=ruff_memory_usage::heap_size + )] + fn materialized_value_type( + self, + db: &'db dyn Db, + materialization_kind: MaterializationKind, + ) -> Type<'db> { + let value_type = self.with_materialization_kind(db, None).value_type(db); + let env = ProgramEnvironment::from_definition(self.definition(db)); + value_type.materialize( + db, + materialization_kind, + &ApplyTypeMappingVisitor::new(&env), + ) + } + pub(crate) fn raw_value_type(self, db: &'db dyn Db) -> Type<'db> { match self { TypeAliasType::PEP695(type_alias) => type_alias.raw_value_type(db), @@ -343,7 +381,7 @@ impl<'db> TypeAliasType<'db> { } } - /// Returns the alias without an applied specialization. + /// Returns the alias without an applied specialization or pending materialization. pub(super) fn unspecialized(self, db: &'db dyn Db) -> Self { match self { TypeAliasType::PEP695(alias) => TypeAliasType::PEP695(PEP695TypeAliasType::new( @@ -351,10 +389,53 @@ impl<'db> TypeAliasType<'db> { alias.name(db), alias.rhs_scope(db), None, + None, )), - TypeAliasType::ManualPEP695(alias) => TypeAliasType::ManualPEP695( - ManualPEP695TypeAliasType::new(db, alias.name(db), alias.definition(db), None), - ), + TypeAliasType::ManualPEP695(alias) => { + TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( + db, + alias.name(db), + alias.definition(db), + None, + None, + )) + } + } + } + + pub(super) fn materialization_kind(self, db: &'db dyn Db) -> Option { + match self { + TypeAliasType::PEP695(alias) => alias.materialization_kind(db), + TypeAliasType::ManualPEP695(alias) => alias.materialization_kind(db), + } + } + + pub(super) fn with_materialization_kind( + self, + db: &'db dyn Db, + materialization_kind: Option, + ) -> Self { + if self.materialization_kind(db) == materialization_kind { + return self; + } + + match self { + TypeAliasType::PEP695(alias) => TypeAliasType::PEP695(PEP695TypeAliasType::new( + db, + alias.name(db), + alias.rhs_scope(db), + alias.specialization(db), + materialization_kind, + )), + TypeAliasType::ManualPEP695(alias) => { + TypeAliasType::ManualPEP695(ManualPEP695TypeAliasType::new( + db, + alias.name(db), + alias.definition(db), + alias.specialization(db), + materialization_kind, + )) + } } } From fbcaa204231a2f0b289719f38de5518737b11ae3 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 7 Aug 2026 17:43:55 +0100 Subject: [PATCH 327/390] [ty] Expand type aliases for `Generator` when evaluating a generator function's return/send/yield type (#27577) --- .../mdtest/expression/yield_and_yield_from.md | 53 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 1 + 2 files changed, 54 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md index 6889b128e2..dd0489da6a 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md @@ -186,6 +186,59 @@ def iterator_yield_from() -> Generator[int, None, int]: return 1 ``` +## Generator type aliases + +ty "sees through" type aliases used as return annotations when inferring a generator's yield type. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import AsyncGenerator, Generator, Iterator + +type GeneratorAlias[T] = Generator[T] + +def invalid_yield() -> GeneratorAlias[int]: + yield "foo" # error: [invalid-yield] + +def invalid_return() -> GeneratorAlias[int]: + yield 42 + return "foo" # error: [invalid-return-type] + +type NestedGeneratorAlias[T] = GeneratorAlias[T] + +def invalid_nested_yield() -> NestedGeneratorAlias[int]: + yield "foo" # error: [invalid-yield] + +type IteratorAlias[T] = Iterator[T] + +def invalid_iterator_return() -> IteratorAlias[int]: + yield 42 + return "foo" # error: [invalid-return-type] + +type AsyncGeneratorAlias[T] = AsyncGenerator[T] + +async def invalid_async_yield() -> AsyncGeneratorAlias[int]: + yield "foo" # error: [invalid-yield] +``` + +The same applies when inferring a generator's return type and send type: + +```py +type FullGeneratorAlias[YieldT, SendT, ReturnT] = Generator[YieldT, SendT, ReturnT] + +def inner_aliased_generator() -> FullGeneratorAlias[int, bytes, str]: + sent = yield 42 + reveal_type(sent) # revealed: bytes + return "done" + +def outer_aliased_generator() -> FullGeneratorAlias[int, bytes, None]: + result = yield from inner_aliased_generator() + reveal_type(result) # revealed: str +``` + ## Error cases ### Non-iterable type diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 8f42dea3c6..cc3168c3d4 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6794,6 +6794,7 @@ impl<'db> Type<'db> { .materialization_kind(db) .map_or(types, |kind| types.materialize(db, env, kind)) }), + Type::TypeAlias(alias) => alias.value_type(db).generator_types(db, env), Type::Union(union) => { let mut yield_builder = Some(UnionBuilder::new(db, env)); let mut send_builder = Some(UnionBuilder::new(db, env)); From fade22cdd9ffee55bb361dcbaed0ea7e47a15758 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 7 Aug 2026 11:25:11 -0700 Subject: [PATCH 328/390] [ty] Recognize overlapping NewTypes and their underlying values (#27522) ## Summary Fix `NewType` disjointness and `is` narrowing by distinguishing runtime objects from typed inhabitants. A typed inhabitant consists of a runtime object plus any invisible static tags, such as a generic specialization or a `NewType`. Two types overlap when they admit the same typed inhabitant, not merely the same runtime object. `NewType` constructors return the original object unchanged, so distinct tags can represent different typed inhabitants of the same runtime object: ```py from typing import NewType UserId = NewType("UserId", int) OrderId = NewType("OrderId", int) NestedUserId = NewType("NestedUserId", UserId) UserId(True) is OrderId(True) # True at runtime. ``` The inhabitants `(True, UserId)` and `(True, OrderId)` are distinct, so `UserId` and `OrderId` remain disjoint. Ordinary types do not require either tag, so `UserId` still overlaps `int`, `bool`, `Literal[True]`, and other nominal, structural, or literal types that overlap its concrete base. `NestedUserId` overlaps `UserId` because a nested tag remains a subtype of its parent. Apply these distinctions consistently to identity and narrowing: transfer object-wide generic guarantees and runtime-value constraints, but never transfer another operand's `NewType` tag or potentially tagged type variable. Preserve each operand's own tags and negations. Narrowing through `isinstance`, class patterns, type guards, enum equality, and exhaustive enum matches retains the original `NewType` tag. Fixes astral-sh/ty#4187. ## Test plan - Disjointness and intersection mdtests cover nominal and structural base overlaps, literals, boolean/numeric types, enum members, type guards, distinct and nested `NewType`s, and invariant, covariant, and gradual generic specializations. - Identity mdtests cover operand-specific `NewType` tags, static versus runtime negations, invariant and covariant generics, unconstrained/bounded/constrained type variables, singleton values, and compatibility with existing string-literal narrowing. - Narrowing mdtests cover `isinstance`, `TypeIs`, class patterns, branded enum members and complements, nested brands, cross-enum and custom equality, exhaustive enum/`IntEnum` matching, and recursive aliases. --- .../resources/mdtest/annotations/new_types.md | 21 +- .../resources/mdtest/comparison/identity.md | 8 +- .../mdtest/comparison/intersections.md | 12 + .../resources/mdtest/intersection_types.md | 67 ++ .../mdtest/narrow/conditionals/eq.md | 130 +++- .../mdtest/narrow/conditionals/is.md | 310 ++++++-- .../resources/mdtest/narrow/isinstance.md | 31 + .../resources/mdtest/narrow/match.md | 95 ++- .../resources/mdtest/narrow/type_guards.md | 19 + .../resources/mdtest/ty_extensions.md | 7 +- .../type_properties/is_disjoint_from.md | 717 +++++++++++++----- .../ty_python_semantic/src/types/equality.rs | 106 ++- .../src/types/equality/enums.rs | 17 +- .../src/types/infer/comparisons.rs | 45 +- crates/ty_python_semantic/src/types/narrow.rs | 6 +- .../ty_python_semantic/src/types/relation.rs | 11 + .../src/types/set_theoretic/builder.rs | 11 + 17 files changed, 1303 insertions(+), 310 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index a6869cf3f8..0488d8fe4a 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -13,6 +13,15 @@ def _(user_id: UserId): reveal_type(user_id) # revealed: UserId ``` +A `NewType` constructor preserves its argument's runtime identity but gives the result its own +static tag. Applying an unrelated `NewType` constructor replaces the previous tag. + +```py +MediaId = NewType("MediaId", int) + +reveal_type(MediaId(UserId(1))) # revealed: MediaId +``` + ## Subtyping The basic purpose of `NewType` is that it acts like a subtype of its base, but not the exact same @@ -644,9 +653,11 @@ info: Perhaps you were looking for: `Foo = NewType('Foo', X)` info: Definition of class `Foo` will raise `TypeError` at runtime ``` -## Don't narrow `NewType`-wrapped `Enum`s inside of match arms +## `NewType`-wrapped enums match their members -`Literal[Foo.X]` is actually disjoint from `N` here: +A `NewType` constructor returns its argument unchanged at runtime, and an ordinary literal does not +restrict `NewType` tags. An enum member can therefore inhabit both its literal type and a `NewType` +based on the enum. Each arm retains both types, and matching every enum member is exhaustive. ```py from enum import Enum @@ -661,11 +672,11 @@ N = NewType("N", Foo) def f(x: N): match x: case Foo.X: - reveal_type(x) # revealed: N + reveal_type(x) # revealed: N & Literal[Foo.X] case Foo.Y: - reveal_type(x) # revealed: N + reveal_type(x) # revealed: N & Literal[Foo.Y] case _: - reveal_type(x) # revealed: N + reveal_type(x) # revealed: Never ``` ## The base of a `NewType` can't be a protocol class or a `TypedDict` diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/identity.md b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md index d295ce3acf..db09cf8f99 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/identity.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/identity.md @@ -67,9 +67,9 @@ def f(x: str, y: int): reveal_type(x is not y) # revealed: Literal[True] ``` -but simple disjointness is not enough -- these two `NewType`s are disjoint, yet `B(True)` shares the -same memory address as `C(True)`. Disjointness of the nominal-instance types *backing* the `NewType` -is the necessary precondition: +Distinct `NewType` tags are mutually exclusive, so their types are disjoint. Their constructors +still return their arguments unchanged: `B(True)` and `C(True)` have different tags but share the +same memory address, so an identity comparison can succeed. ```py from typing import NewType, Literal @@ -79,7 +79,7 @@ B = NewType("B", bool) C = NewType("C", bool) reveal_type(is_disjoint_from(B, C)) # revealed: ConstraintSet[Literal[True]] -reveal_type(is_disjoint_from(B, Literal[True])) # revealed: ConstraintSet[Literal[True]] +reveal_type(is_disjoint_from(B, Literal[True])) # revealed: ConstraintSet[Literal[False]] def f(x: B, y: C): reveal_type(x is y) # revealed: bool diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md index 8c0630a142..2948faae9b 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md @@ -121,6 +121,18 @@ def f(value: Not[E]) -> None: value.does_not_exist # no error (unreachable branch) ``` +A `NewType` negation removes its static tag, not the runtime objects of its base: an integer without +that tag can still be identical to the integer passed into the `NewType` constructor. + +```py +from typing import NewType + +UserId = NewType("UserId", int) + +def f(value: Not[UserId]) -> None: + reveal_type(value is 1) # revealed: bool +``` + After `not isinstance(value, B)`, `value` cannot be identical to a `B` instance. This remains true when `value` has also been narrowed to `A`, so the inner branch is unreachable. diff --git a/crates/ty_python_semantic/resources/mdtest/intersection_types.md b/crates/ty_python_semantic/resources/mdtest/intersection_types.md index 7d43f6767b..7c2a89df85 100644 --- a/crates/ty_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/ty_python_semantic/resources/mdtest/intersection_types.md @@ -449,6 +449,27 @@ def example_type_bool_type_str( reveal_type(i) # revealed: Never ``` +Ordinary types accept values with any `NewType` tag, so an integer-based `NewType` can overlap +`bool`. Distinct `NewType` tags are mutually exclusive even when their runtime values overlap; +nested `NewType`s retain their relationship with their parent. + +```py +from typing import NewType + +UserId = NewType("UserId", int) +OtherUserId = NewType("OtherUserId", int) +NestedUserId = NewType("NestedUserId", UserId) + +def newtype_intersections( + user_bool: UserId & bool, + user_nested: UserId & NestedUserId, + user_other: UserId & OtherUserId, +) -> None: + reveal_type(user_bool) # revealed: UserId & bool + reveal_type(user_nested) # revealed: NestedUserId + reveal_type(user_other) # revealed: Never +``` + #### Positive and negative contributions If we intersect a type `X` with the negation `~Y` of a disjoint type `Y`, we can remove the negative @@ -821,6 +842,52 @@ def _(e: (Single | int) & ~Single) -> None: reveal_type(e) # revealed: int ``` +A `NewType` is preserved when all but one member of its underlying enum are excluded. The resulting +intersection is also assignable to the remaining member. + +```pyi +from typing import NewType +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_equivalent_to + +ColorId = NewType("ColorId", Color) +NestedColorId = NewType("NestedColorId", ColorId) +type NestedAlias = NestedColorId + +def enum_newtype(value: ColorId & ~(Red | Green), nested: NestedAlias & ~Red & ~Green) -> None: + reveal_type(value) # revealed: ColorId & Literal[Color.BLUE] + reveal_type(nested) # revealed: NestedColorId & Literal[Color.BLUE] + +static_assert(is_assignable_to(ColorId & ~(Red | Green), ColorId)) +static_assert(is_assignable_to(ColorId & ~(Red | Green), Blue)) +static_assert(is_equivalent_to(ColorId & ~(Red | Green), ColorId & Blue)) +``` + +Aliases name the same enum member, while `Flag` members are not exhaustive. + +```pyi +from enum import Flag + +class Aliased(Enum): + FIRST = 1 + FIRST_ALIAS = 1 + LAST = 2 + +AliasedId = NewType("AliasedId", Aliased) + +def aliased_member(value: AliasedId & ~Literal[Aliased.FIRST_ALIAS]) -> None: + reveal_type(value) # revealed: AliasedId & Literal[Aliased.LAST] + +class Permission(Flag): + READ = 1 + WRITE = 2 + +PermissionId = NewType("PermissionId", Permission) + +def non_exhaustive(value: PermissionId & ~Literal[Permission.READ]) -> None: + reveal_type(value) # revealed: PermissionId & ~Literal[Permission.READ] +``` + ## Addition of a type to an intersection with many non-disjoint types This slightly strange-looking test is a regression test for a mistake that was nearly made in a PR: diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index 4d706a2c89..d49f28d840 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -1118,7 +1118,8 @@ def _(answer: CoupledInequality): ## Recursive aliases containing enum domains -Enum domains nested in a recursive alias fall back to general comparison inference: +Comparisons involving recursive enum aliases remain valid. Comparing against a specific enum member +narrows both branches to their remaining members while preserving any `NewType` tag. ```toml [environment] @@ -1127,6 +1128,7 @@ python-version = "3.12" ```py from enum import Enum +from typing import NewType class EnumValue(Enum): VALUE = 1 @@ -1136,6 +1138,59 @@ type Recursive = EnumValue | Recursive def _(left: Recursive, right: EnumValue): reveal_type(left == right) # revealed: bool + +BrandedEnumValue = NewType("BrandedEnumValue", EnumValue) +type RecursiveBrand = BrandedEnumValue | RecursiveBrand + +def compare_recursive_brand_to_member(left: RecursiveBrand) -> None: + if left == EnumValue.VALUE: + reveal_type(left) # revealed: BrandedEnumValue & Literal[EnumValue.VALUE] + else: + reveal_type(left) # revealed: BrandedEnumValue & Literal[EnumValue.OTHER] + + if left != EnumValue.VALUE: + reveal_type(left) # revealed: BrandedEnumValue & Literal[EnumValue.OTHER] + else: + reveal_type(left) # revealed: BrandedEnumValue & Literal[EnumValue.VALUE] +``` + +A recursive alias with changing type arguments may introduce values outside its original enum +domain. Here, `True` compares equal to the integer-valued enum member, so the `bool` alternative +must remain reachable. + +```py +from enum import IntEnum + +class Number(IntEnum): + ONE = 1 + TWO = 2 + +BrandedNumber = NewType("BrandedNumber", Number) +type Changing[T] = T | Changing[bool] + +def compare_changing_specialization(value: Changing[BrandedNumber]) -> None: + if value == Number.ONE: + reveal_type(value) # revealed: (BrandedNumber & Literal[Number.ONE]) | bool + else: + reveal_type(value) # revealed: (BrandedNumber & Literal[Number.TWO]) | bool +``` + +Mutually recursive aliases can likewise admit values outside their enum domain. Intersecting the +aliases does not remove their shared `bool` alternative. + +```py +from ty_extensions import Intersection + +type RecursiveWithBool = RecursiveWithBrand | bool +type RecursiveWithBrand = RecursiveWithBool | BrandedNumber + +def compare_mutually_recursive_intersection( + value: Intersection[RecursiveWithBool, RecursiveWithBrand], +) -> None: + if value == Number.ONE: + reveal_type(value) # revealed: bool | BrandedNumber + else: + reveal_type(value) # revealed: bool | BrandedNumber ``` ## Recursive aliases containing gradual generic branches @@ -2202,8 +2257,9 @@ def tuple_with_erased_element_identity(value: NeverEqualTupleElement) -> None: ## Narrowing with NewTypes -`NewType` wrappers erase their distinction at runtime, so comparisons with an identity-based enum -literal remain ambiguous: +A `NewType` constructor returns its argument unchanged at runtime. A `WrappedIdentityEnum` value can +therefore be either `IdentityEnum.A` or `IdentityEnum.B`, so comparing it with `IdentityEnum.A` has +an unknown result: ```py from enum import Enum @@ -2220,6 +2276,74 @@ def literal_with_erased_identity(value: WrappedIdentityEnum) -> None: reveal_type(IdentityEnum.A != value) # revealed: bool ``` +When a `WrappedIdentityEnum` value is `IdentityEnum.B`, equality narrows another `IdentityEnum` +value to the same member. The first value keeps its `WrappedIdentityEnum` type, and both operands +can be passed to a function accepting `Literal[IdentityEnum.B]`. + +```py +from typing import Literal, TypeAlias +from ty_extensions import Intersection + +def accepts_b(value: Literal[IdentityEnum.B]) -> None: ... +def compare_branded_member( + branded: Intersection[WrappedIdentityEnum, Literal[IdentityEnum.B]], + other: IdentityEnum, +) -> None: + if branded == other: + reveal_type(branded) # revealed: WrappedIdentityEnum & Literal[IdentityEnum.B] + reveal_type(other) # revealed: Literal[IdentityEnum.B] + accepts_b(branded) + accepts_b(other) + else: + reveal_type(other) # revealed: Literal[IdentityEnum.A] + +NestedIdentityEnum = NewType("NestedIdentityEnum", WrappedIdentityEnum) +NestedAlias: TypeAlias = NestedIdentityEnum + +def compare_nested_brand(value: NestedAlias, other: Literal[IdentityEnum.A]) -> None: + if value == other: + reveal_type(value) # revealed: NestedIdentityEnum & Literal[IdentityEnum.A] + else: + reveal_type(value) # revealed: NestedIdentityEnum & Literal[IdentityEnum.B] +``` + +`NewType` does not change how an `IntEnum` compares: values from different `IntEnum` classes still +compare by their integer values. A custom enum `__eq__` method likewise still determines the result +after its value is passed through a `NewType` constructor. + +```py +from enum import IntEnum + +class FirstNumber(IntEnum): + ONE = 1 + TWO = 2 + +class SecondNumber(IntEnum): + ONE = 1 + THREE = 3 + +BrandedFirstNumber = NewType("BrandedFirstNumber", FirstNumber) +BrandedSecondNumber = NewType("BrandedSecondNumber", SecondNumber) + +def compare_branded_int_enums(left: BrandedFirstNumber, right: BrandedSecondNumber) -> None: + if left == right: + reveal_type(left) # revealed: BrandedFirstNumber & Literal[FirstNumber.ONE] + reveal_type(right) # revealed: BrandedSecondNumber & Literal[SecondNumber.ONE] + +class NeverEqualEnum(Enum): + A = 1 + B = 2 + + def __eq__(self, other: object) -> Literal[False]: + return False + +BrandedNeverEqual = NewType("BrandedNeverEqual", NeverEqualEnum) + +def branded_custom_equality(value: BrandedNeverEqual, other: NeverEqualEnum) -> None: + reveal_type(value == other) # revealed: Literal[False] + reveal_type(value != other) # revealed: bool +``` + ## Narrowing with enums that have custom `__eq__` methods Custom enum comparison methods with definite return types determine equality and inequality diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md index 76c7c2f863..f0ea2e73c1 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md @@ -28,6 +28,130 @@ def _(x: A, y: A | None): reveal_type(y) # revealed: A | None ``` +Identity also transfers facts about the shared object, such as whether a string is truthy. + +```py +def truthy_string(value: object, text: str) -> None: + if text: + if value is text: + reveal_type(value) # revealed: str & ~AlwaysFalsy +``` + +## `is` with invariant generic types + +A `list[int]` guarantees that values read from the list are integers. That guarantee must hold for +every reference to the same mutable list: if another reference could treat it as `list[str]`, it +could append a string that the first reference would then incorrectly read as an integer. An +identity comparison can therefore transfer the invariant type argument. + +```py +def generic_type(value: object, items: list[int]) -> None: + if value is items: + reveal_type(value) # revealed: list[int] +``` + +Incompatible invariant specializations cannot describe the same object in soundly typed code. + +```py +def incompatible_generic_types(integers: list[int], strings: list[str]) -> None: + reveal_type(integers is strings) # revealed: Literal[False] + if integers is strings: + reveal_type(integers) # revealed: Never + reveal_type(strings) # revealed: Never +``` + +## `is` with covariant generic types + +Covariant specializations can describe the same object: an empty tuple belongs to both +`tuple[int, ...]` and `tuple[str, ...]`. Identity therefore remains possible and preserves both sets +of type arguments. + +```py +def covariant_generic_type(value: object, items: tuple[int, ...]) -> None: + if value is items: + reveal_type(value) # revealed: tuple[int, ...] + +def overlapping_generic_types(integers: tuple[int, ...], strings: tuple[str, ...]) -> None: + reveal_type(integers is strings) # revealed: bool + if integers is strings: + # TODO: Ideally, these intersections would simplify to tuple[()]. + reveal_type(integers) # revealed: tuple[int, ...] & tuple[str, ...] + reveal_type(strings) # revealed: tuple[str, ...] & tuple[int, ...] +``` + +## `is` with a `NewType` + +A `NewType` constructor returns its argument unchanged, so its tag belongs to one static view rather +than the shared object. Identity can establish the underlying type without transferring that tag. + +```py +from typing import NewType + +UserId = NewType("UserId", int) + +def discard_newtype_tag(value: object, user_id: UserId) -> None: + if value is user_id: + reveal_type(value) # revealed: int + reveal_type(user_id) # revealed: UserId +``` + +## `is` with unconstrained type variables + +An unconstrained type variable can hold a `NewType`. Identity therefore cannot transfer the type +variable, since doing so would also transfer the `NewType` tag. + +```py +from typing import NewType, TypeVar + +T = TypeVar("T") +UserId = NewType("UserId", int) + +def type_variable(value: object, other: T) -> T: + if value is other: + reveal_type(value) # revealed: object + reveal_type(other) # revealed: T@type_variable + return other + +reveal_type(type_variable(1, UserId(1))) # revealed: UserId +``` + +## `is` with bounded type variables + +A type variable bounded by `int` can still hold an integer `NewType`. Identity transfers its `int` +bound without transferring the type variable or its possible `NewType` tag. + +```py +from typing import NewType, TypeVar + +BoundedT = TypeVar("BoundedT", bound=int) +UserId = NewType("UserId", int) + +def bounded_type_variable(value: object, other: BoundedT) -> BoundedT: + if value is other: + reveal_type(value) # revealed: int + reveal_type(other) # revealed: BoundedT@bounded_type_variable + return other + +reveal_type(bounded_type_variable(1, UserId(1))) # revealed: UserId +``` + +## `is` with constrained type variables + +Identity transfers a constrained type variable's possible runtime types without transferring any +`NewType` tags in its constraints. + +```py +from typing import NewType, TypeVar + +UserId = NewType("UserId", int) +TaggedChoice = TypeVar("TaggedChoice", UserId, str) + +def constrained_type_variable(value: object, other: TaggedChoice) -> None: + if value is other: + reveal_type(value) # revealed: int | str + reveal_type(other) # revealed: TaggedChoice@constrained_type_variable +``` + ## Narrowing tagged unions of nominal classes by attribute identity ```py @@ -339,13 +463,93 @@ def f(value: int | None | EllipsisType, other: T) -> None: reveal_type(value) # revealed: int | (None & ~T@f) | (EllipsisType & ~T@f) ``` +## `is` with a negated `NewType` + +Excluding a `NewType` removes its invisible tag, not the runtime objects accepted by its +constructor. An identity comparison preserves that negation without making a reachable branch +disappear. + +```py +from typing import Literal, NewType, TypeVar +from ty_extensions import Intersection, Not + +UserId = NewType("UserId", int) + +def excluded_newtype(value: Not[UserId], other: UserId) -> None: + if value is other: + reveal_type(value) # revealed: int & ~UserId + reveal_type(other) # revealed: UserId + + if other is value: + reveal_type(value) # revealed: int & ~UserId +``` + +A type variable can hide the same static negation in its upper bound. The reachable branch must +preserve that type variable. + +```py +ExcludedBound = TypeVar("ExcludedBound", bound=Intersection[int, Not[UserId]]) + +def excluded_newtype_in_bound( + value: ExcludedBound, + other: UserId, + without_one: Intersection[ExcludedBound, Not[Literal[1]]], +) -> None: + if value is other: + reveal_type(value) # revealed: ExcludedBound@excluded_newtype_in_bound + + if without_one is other: + reveal_type(without_one) # revealed: ExcludedBound@excluded_newtype_in_bound & ~Literal[1] +``` + +The same runtime overlap remains reachable when both operands are unions, while genuinely +incompatible alternatives are removed. + +```py +def excluded_newtype_in_unions( + value: Intersection[int, Not[UserId]] | None, + other: UserId | bytes, +) -> None: + if value is other: + reveal_type(value) # revealed: int & ~UserId + reveal_type(other) # revealed: UserId +``` + +Unlike a negated `NewType`, a negated runtime class genuinely rules out identity with its instances. + +```py +def excluded_runtime_class(not_int: Not[int], other: UserId) -> None: + if not_int is other: + reveal_type(not_int) # revealed: Never + reveal_type(other) # revealed: Never +``` + +## `is` with string types + +Identity comparisons preserve existing `LiteralString` narrowing and do not make negated string +literal comparisons unreachable. + +```py +from typing import Literal +from typing_extensions import LiteralString +from ty_extensions import Not + +def literal_string(value: object, text: LiteralString) -> None: + if value is text: + reveal_type(value) # revealed: LiteralString + +def negated_string_literal(value: Not[Literal["hello"]]) -> None: + if value is "hello": + reveal_type(value) # revealed: ~Literal["hello"] +``` + ## `is` with `NewType`s ### Distinct `NewType`s with the same base -Calling a `NewType` returns its argument unchanged. Values with distinct `NewType`s over `Foo` can -therefore be the same object even though their types are disjoint. The examples below cover direct -comparisons and narrowing through unions and intersections. +Distinct `NewType` tags are mutually exclusive, so their types are disjoint even when they have the +same concrete base. Their constructors still return their arguments unchanged: an identity +comparison can succeed, but each operand retains only its own tag. ```py from typing import NewType @@ -375,8 +579,8 @@ def intersection(left: Intersection[FooNewType1, FooSub], right: FooNewType2) -> ### `NewType`s in `TypeVar` bounds and constraints `NewType`s inside `TypeVar` bounds and constraints can likewise refer to the same runtime object. -Comparing the distinct `TypeVar`s below is not always false, and a true branch keeps the original -`TypeVar`. +Comparing distinct type variables is not always false, but a successful comparison preserves each +operand's own type variable and tag. ```py from typing import NewType, TypeVar @@ -391,29 +595,38 @@ FooNewType4 = NewType("FooNewType4", Foo) BoundedT = TypeVar("BoundedT", bound=FooNewType1) BoundedU = TypeVar("BoundedU", bound=FooNewType2) -def bounded_typevars(left: BoundedT, right: BoundedU) -> tuple[BoundedU, BoundedU]: +def bounded_typevars(left: BoundedT, right: BoundedU) -> None: reveal_type(left is right) # revealed: bool if left is right: - # TODO: This should narrow to `BoundedT & BoundedU` and avoid the false positive below. + # These are the same object, so substituting `left` for `right` in a return would be + # sound. But `BoundedT & BoundedU` is still empty because their `NewType` tags differ; + # inferring that intersection could incorrectly make reachable code disappear. reveal_type(left) # revealed: BoundedT@bounded_typevars - return (left, left) # error: [invalid-return-type] - return (right, right) + reveal_type(right) # revealed: BoundedU@bounded_typevars ConstrainedT = TypeVar("ConstrainedT", FooNewType1, FooNewType2) ConstrainedU = TypeVar("ConstrainedU", FooNewType3, FooNewType4) -def constrained_typevars(left: ConstrainedT, right: ConstrainedU) -> tuple[ConstrainedU, ConstrainedU]: +def constrained_typevars(left: ConstrainedT, right: ConstrainedU) -> None: reveal_type(left is right) # revealed: bool if left is right: - # TODO: This should narrow to `ConstrainedT & ConstrainedU` and avoid the false positive. reveal_type(left) # revealed: ConstrainedT@constrained_typevars - return (left, left) # error: [invalid-return-type] - return (right, right) + reveal_type(right) # revealed: ConstrainedU@constrained_typevars +``` + +A type variable bounded by a `NewType` also carries that `NewType` tag. Identity cannot transfer the +type variable to an untagged value, but can establish the underlying runtime class. + +```py +def object_with_bounded_newtype(value: object, tagged: BoundedT) -> None: + if value is tagged: + reveal_type(value) # revealed: Foo + reveal_type(tagged) # revealed: BoundedT@object_with_bounded_newtype ``` -Every constraint below is a `NewType` based on `EllipsisType`, so `other` always refers to the same -`...` object as a `SingletonC` value. After an `is not` check, repeating the opposite check must be -unreachable. +Every constraint below is a `NewType` based on `EllipsisType`. Although their tags are mutually +exclusive, all of these values refer to the same `...` object. An `is not` check therefore removes +the singleton alternative, making a subsequent `is` check unreachable. ```py from types import EllipsisType @@ -426,31 +639,40 @@ SingletonC = NewType("SingletonC", EllipsisType) SingletonT = TypeVar("SingletonT", SingletonA, SingletonB) -def direct(value: SingletonC | int, other: SingletonT) -> None: +def same_singleton(first: SingletonA, second: SingletonB) -> None: + reveal_type(first is second) # revealed: Literal[True] + if first is second: + reveal_type(first) # revealed: SingletonA + reveal_type(second) # revealed: SingletonB + +def contradictory_singleton_comparisons(value: SingletonC | int, other: SingletonT) -> None: if value is not other: + reveal_type(value) # revealed: int if value is other: assert_never(value) ``` -### Narrowing an object to a `NewType` in the true branch +### Narrowing an object to the generic base of a `NewType` -If an object is identical to a value with a `NewType`, the true branch narrows the object to that -`NewType` rather than its underlying type. +Identity does not transfer a `NewType` tag, but it preserves the invariant type arguments of the +underlying generic type. ```py from typing import NewType -UserId = NewType("UserId", int) +UserIds = NewType("UserIds", list[int]) -def preserve_newtype(x: object, user_id: UserId) -> None: - if x is user_id: - reveal_type(x) # revealed: UserId +def preserve_generic_base(value: object, user_ids: UserIds) -> None: + if value is user_ids: + reveal_type(value) # revealed: list[int] + reveal_type(user_ids) # revealed: UserIds ``` ### Comparing `NewType`s with literals Calls to `NewType` return their arguments unchanged. Comparisons with `bool` and `int` literals can -therefore succeed, so the true branches below remain reachable. +therefore succeed. Identity transfers the literal value to the tagged operand, but does not transfer +its `NewType` tag back to the literal. ```py from typing import Literal, NewType @@ -461,10 +683,10 @@ IntNewType = NewType("IntNewType", int) def literals(true: Literal[True], b: BoolNewType, forty_two: Literal[42], i: IntNewType) -> None: if b is true: reveal_type(true) # revealed: Literal[True] - reveal_type(b) # revealed: BoolNewType + reveal_type(b) # revealed: BoolNewType & Literal[True] if i is forty_two: reveal_type(forty_two) # revealed: Literal[42] - reveal_type(i) # revealed: IntNewType + reveal_type(i) # revealed: IntNewType & Literal[42] ``` ### `is not` with singleton `NewType`s @@ -482,38 +704,10 @@ SingletonB = NewType("SingletonB", EllipsisType) def singleton_is_not(value: SingletonA | int, other: SingletonB) -> None: if value is not other: reveal_type(value) # revealed: int -``` - -### Static exclusions - -The type `~Literal[True]` excludes the literal type but accepts the distinct `BoolNewType`. However, -`BoolNewType(True)` returns `True` unchanged, so `value is True` can be either true or false. - -```py -from __future__ import annotations -from typing import Literal, NewType - -BoolNewType = NewType("BoolNewType", bool) - -def excludes_true(value: ~Literal[True]) -> None: - reveal_type(value is True) # revealed: bool - -excludes_true(BoolNewType(True)) -``` - -Similarly, `int & ~Literal[1]` accepts `IntNewType(1)`, which returns the `1` object unchanged, so -the comparison remains possible. - -```py -from typing import Literal, NewType - -IntNewType = NewType("IntNewType", int) - -def excludes_one(value: int & ~Literal[1]) -> None: - reveal_type(value is 1) # revealed: bool - -excludes_one(IntNewType(1)) + if value is other: + reveal_type(value) # revealed: SingletonA + reveal_type(other) # revealed: SingletonB ``` ### Comparisons that are always false diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 5c296845f0..4d9ccf1d07 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -332,6 +332,37 @@ else: reveal_type(x) # revealed: ~A & ~B & ~C ``` +## `NewType` instances and concrete-base subclasses + +A `NewType` constructor returns its argument unchanged at runtime, and runtime class checks ignore +its static tag. The resulting value can therefore still be an instance of a subclass of its concrete +base. For example, `UserId(True)` is valid because `bool` is a subtype of `int`, and the returned +value remains a `bool`. + +```py +from typing import NewType + +class Base: ... +class Child(Base): ... + +BrandedBase = NewType("BrandedBase", Base) +UserId = NewType("UserId", int) + +UserId(True) + +def narrow_branded_subclass(value: BrandedBase) -> None: + if isinstance(value, Child): + reveal_type(value) # revealed: BrandedBase & Child + else: + reveal_type(value) # revealed: BrandedBase & ~Child + +def narrow_branded_boolean(value: UserId) -> None: + if isinstance(value, bool): + reveal_type(value) # revealed: UserId & bool + else: + reveal_type(value) # revealed: UserId & ~bool +``` + ## No narrowing for instances of `builtins.type` ```py diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index ea67ca70b9..644b303221 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -44,6 +44,24 @@ match x: reveal_type(x) # revealed: object ``` +## Class patterns on `NewType` instances + +A `NewType` does not change its argument's runtime class, so an integer-based `NewType` can match a +`bool` class pattern. + +```py +from typing import NewType + +UserId = NewType("UserId", int) + +def match_newtype_boolean(value: UserId) -> None: + match value: + case bool(): + reveal_type(value) # revealed: UserId & bool + case _: + reveal_type(value) # revealed: UserId & ~bool +``` + ## Class pattern with guard ```py @@ -3326,7 +3344,8 @@ python-version = "3.11" ```py from enum import Enum, IntEnum, StrEnum, auto -from typing import Literal, assert_never +from typing import Literal, NewType, assert_never +from ty_extensions import Intersection from ty_extensions._internal import Unknown class Color(StrEnum): @@ -3381,6 +3400,37 @@ class Second(IntEnum): ONE = 1 TWO = 2 +BrandedFirst = NewType("BrandedFirst", First) +BrandedSecond = NewType("BrandedSecond", Second) + +def branded_int_enum_literal_pattern_is_exhaustive(value: Intersection[BrandedFirst, Literal[First.ONE]]) -> int: + match value: + case 1: + return 1 + +def branded_int_enum_integer_patterns_are_exhaustive(value: BrandedFirst) -> int: + match value: + case 1: + reveal_type(value) # revealed: BrandedFirst & Literal[First.ONE] + return 1 + case 2: + reveal_type(value) # revealed: BrandedFirst & Literal[First.TWO] + return 2 + +def branded_int_enum_member_patterns_are_exhaustive(value: BrandedFirst) -> int: + match value: + case First.ONE: + return 1 + case First.TWO: + return 2 + +def branded_cross_int_enum_member_patterns(value: BrandedFirst | BrandedSecond) -> None: + match value: + case First.ONE: + reveal_type(value) # revealed: (BrandedFirst & Literal[First.ONE]) | (BrandedSecond & Literal[Second.ONE]) + case _: + reveal_type(value) # revealed: (BrandedFirst & Literal[First.TWO]) | (BrandedSecond & Literal[Second.TWO]) + def cross_int_enum_members(value: First | Second) -> None: match value: case First.ONE: @@ -3616,6 +3666,49 @@ def test_match_alias_ignores_custom_ne(flag: bool) -> str: return item ``` +## Recursive enum aliases in value patterns + +An enum value pattern narrows a recursive alias to the matching member while preserving its +`NewType` tag. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from enum import IntEnum +from typing import NewType + +class Number(IntEnum): + ONE = 1 + TWO = 2 + +BrandedNumber = NewType("BrandedNumber", Number) +type RecursiveNumber = BrandedNumber | RecursiveNumber + +def match_recursive_branded_enum(value: RecursiveNumber) -> None: + match value: + case Number.ONE: + reveal_type(value) # revealed: BrandedNumber & Literal[Number.ONE] + case Number.TWO: + reveal_type(value) # revealed: BrandedNumber & Literal[Number.TWO] +``` + +A recursive alias that changes its specialization can also contain values outside the enum. Since +`True` compares equal to `Number.ONE`, both branches preserve the possible boolean values. + +```py +type Changing[T] = T | Changing[bool] + +def match_changing_specialization(value: Changing[BrandedNumber]) -> None: + match value: + case Number.ONE: + reveal_type(value) # revealed: (BrandedNumber & Literal[Number.ONE]) | bool + case _: + reveal_type(value) # revealed: (BrandedNumber & Literal[Number.TWO]) | bool +``` + ## Value patterns with guard ```py diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index 3961414662..fea7269b4e 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -533,6 +533,25 @@ def _(x: Unrelated | Invariant[int]): reveal_type(x) # revealed: Unrelated ``` +## `TypeIs` narrowing of `NewType` instances + +`NewType` constructors return their arguments unchanged, so an integer-based `NewType` can contain a +`bool`. A `TypeIs[bool]` guard preserves both the `NewType` and its runtime class. + +```py +from typing import NewType +from typing_extensions import TypeIs + +UserId = NewType("UserId", int) + +def is_bool(value: object) -> TypeIs[bool]: + return isinstance(value, bool) + +def _(value: UserId): + if is_bool(value): + reveal_type(value) # revealed: UserId & bool +``` + ## `TypeGuard` special cases ```py diff --git a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md index 5b0e38225c..331cf1c1ae 100644 --- a/crates/ty_python_semantic/resources/mdtest/ty_extensions.md +++ b/crates/ty_python_semantic/resources/mdtest/ty_extensions.md @@ -31,10 +31,9 @@ o: Not[()] p: Not[(int,)] def static_truthiness(not_one: Not[Literal[1]]) -> None: - # A `NewType` over `int` is distinct from `Literal[1]` but can refer to the same runtime object, - # so neither identity comparison has a definite result. - reveal_type(not_one is not 1) # revealed: bool - reveal_type(not_one is 1) # revealed: bool + # Negating a literal rules out every literal with that value. + reveal_type(not_one is not 1) # revealed: Literal[True] + reveal_type(not_one is 1) # revealed: Literal[False] # But these are both `bool`, rather than `Literal[True]` or `Literal[False]` # as there are many runtime objects that inhabit the type `~Literal[1]` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index 3d89c97a54..360d525b9f 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -1,69 +1,33 @@ # Disjointness relation -Two types `S` and `T` are disjoint if their intersection `S & T` is empty (equivalent to `Never`). -This means that it is known that no possible runtime object inhabits both types simultaneously. +Two types `S` and `T` are disjoint if they have no overlap; that is, their intersection `S & T` is +empty (equivalent to `Never`). ## Basic builtin types +For basic builtin types, disjointness simply means that no runtime object can inhabit both types. + ```pyi -from typing_extensions import Literal, LiteralString, Any +from typing_extensions import LiteralString from ty_extensions import static_assert -from ty_extensions._internal import TypeOf, is_disjoint_from +from ty_extensions._internal import is_disjoint_from +# No object can be both a `bool` and a `str`. static_assert(is_disjoint_from(bool, str)) + +# But the same object can be a `bool`, an `int`, and an `object`. static_assert(not is_disjoint_from(bool, bool)) static_assert(not is_disjoint_from(bool, int)) static_assert(not is_disjoint_from(bool, object)) -static_assert(not is_disjoint_from(Any, bool)) -static_assert(not is_disjoint_from(Any, Any)) -static_assert(not is_disjoint_from(Any, ~Any)) - static_assert(not is_disjoint_from(LiteralString, LiteralString)) static_assert(not is_disjoint_from(str, LiteralString)) ``` -## Statically empty and non-empty ranges - -```py -from ty_extensions import static_assert -from ty_extensions._internal import TypeOf, is_disjoint_from - -static_assert(is_disjoint_from(TypeOf[range(0)], TypeOf[range(1)])) -static_assert(is_disjoint_from(TypeOf[range(1)], TypeOf[range(0)])) -static_assert(not is_disjoint_from(TypeOf[range(0)], range)) -static_assert(not is_disjoint_from(TypeOf[range(1)], range)) -``` - -## Enum complements - -```pyi -from enum import Enum -from typing import Literal -from ty_extensions import static_assert -from ty_extensions._internal import is_disjoint_from - -class Color(Enum): - RED = 1 - GREEN = 2 - BLUE = 3 - -static_assert( - is_disjoint_from( - Color & ~Literal[Color.RED], - Color & ~Literal[Color.GREEN, Color.BLUE], - ) -) -static_assert( - is_disjoint_from( - Color & ~Literal[Color.GREEN, Color.BLUE], - Color & ~Literal[Color.RED], - ) -) -``` - ## Class hierarchies +Classes overlap through a common subclass unless finality or incompatible metaclasses prevent it. + ```pyi from ty_extensions import static_assert from ty_extensions._internal import is_disjoint_from, is_subtype_of @@ -112,7 +76,7 @@ static_assert(is_disjoint_from(UsesMeta1, UsesMeta2)) ## `@final` builtin types -Some builtins types are declared as `@final`: +Some builtin types are declared as `@final`: ```py from ty_extensions import static_assert @@ -129,123 +93,18 @@ static_assert(is_disjoint_from(memoryview, Foo)) static_assert(is_disjoint_from(type[memoryview], type[Foo])) ``` -## Specialized `@final` types - -```toml -[environment] -python-version = "3.12" -``` - -```py -from typing import Any, final -from ty_extensions import static_assert -from ty_extensions._internal import is_disjoint_from - -@final -class Foo[T]: - def get(self) -> T: - raise NotImplementedError - -class A: ... -class B: ... - -static_assert(not is_disjoint_from(A, B)) -static_assert(not is_disjoint_from(Foo[A], Foo[B])) -static_assert(not is_disjoint_from(Foo[A], Foo[Any])) -static_assert(not is_disjoint_from(Foo[Any], Foo[B])) - -# `Foo[Never]` is a subtype of both `Foo[int]` and `Foo[str]`. -static_assert(not is_disjoint_from(Foo[int], Foo[str])) -``` - -## Invariant generic specializations and bases +## Gradual types -Only incompatible invariant generic arguments imply disjointness. Covariant generic arguments do -not: a covariant container can be inhabited by an empty value. +Gradual types are not disjoint if any possible materialization is not disjoint. ```pyi -from collections.abc import Sequence -from typing import Any, Generic, TypeVar -from ty_extensions import static_assert -from ty_extensions._internal import is_disjoint_from - -T = TypeVar("T") -U = TypeVar("U") -T_co = TypeVar("T_co", covariant=True) - -class A: ... -class B: ... - -class Invariant(Generic[T]): - x: T - -class InvariantPair(Generic[T, U]): - x: T - y: U - -class Covariant(Generic[T_co]): - def get(self) -> T_co: - raise NotImplementedError() - -class InvSubA(Invariant[A]): - pass - -class CoSubB(Covariant[B]): - pass - -static_assert(is_disjoint_from(Invariant[A], Invariant[B])) -static_assert(is_disjoint_from(InvSubA, Invariant[B])) -static_assert(not is_disjoint_from(Invariant[A], Invariant[A])) -static_assert(not is_disjoint_from(Invariant[Any], Invariant[B])) -static_assert(not is_disjoint_from(Invariant[B], Invariant[Any])) -# `A | Any` cannot materialize to be equivalent to `B`. -static_assert(is_disjoint_from(Invariant[A | Any], Invariant[B])) -static_assert(is_disjoint_from(Invariant[B], Invariant[A | Any])) -static_assert(is_disjoint_from(Invariant[A & Any], Invariant[B])) -static_assert(is_disjoint_from(Invariant[B], Invariant[A & Any])) -static_assert(is_disjoint_from(InvariantPair[A, A], InvariantPair[A, B])) -static_assert(not is_disjoint_from(Covariant[A], Covariant[B])) -static_assert(not is_disjoint_from(Covariant[A], CoSubB)) -static_assert(not is_disjoint_from(Sequence[int], Sequence[str])) -``` - -## Type-variable aliases and empty invariant arguments - -```toml -[environment] -python-version = "3.12" -``` - -```py -from typing import Generic, Never, TypeVar +from typing import Any from ty_extensions import static_assert from ty_extensions._internal import is_disjoint_from -T = TypeVar("T") - -class Invariant(Generic[T]): - x: T - -type Id[V] = V - -def _[U](): - static_assert(not is_disjoint_from(Invariant[U], Invariant[int])) - static_assert(not is_disjoint_from(Invariant[Id[U]], Invariant[int])) - -static_assert(not is_disjoint_from(Invariant[Id[int]], Invariant[int])) -static_assert(is_disjoint_from(Invariant[Id[int]], Invariant[str])) - -class Mixed[T, U]: - x: T - -# `Mixed` is bivariant in `U`, so the differing second argument cannot make these disjoint. -static_assert(not is_disjoint_from(Mixed[Never, int], Mixed[Never, str])) - -class Left(Invariant[Never]): ... -class Right(Invariant[Never]): ... -class Both(Left, Right): ... - -static_assert(not is_disjoint_from(Left, Right)) +static_assert(not is_disjoint_from(Any, bool)) +static_assert(not is_disjoint_from(Any, Any)) +static_assert(not is_disjoint_from(Any, ~Any)) ``` ## "Disjoint base" builtin types @@ -337,6 +196,8 @@ static_assert(not is_disjoint_from(D, A)) ## Dataclasses +Dataclasses with incompatible non-empty slots are disjoint; those with empty slots can overlap. + ```py from dataclasses import dataclass from ty_extensions import static_assert @@ -369,6 +230,8 @@ static_assert(is_disjoint_from(I, J)) ## Tuple types +Tuple types are disjoint when their lengths or corresponding element types cannot overlap. + ```py from typing_extensions import Literal, Never from ty_extensions import static_assert @@ -396,6 +259,8 @@ static_assert(is_disjoint_from(tuple[int, int], tuple[None, ...])) # error: [st ## Unions +A union is disjoint from another type when none of its alternatives overlap that type. + ```py from typing_extensions import Literal from ty_extensions import static_assert @@ -410,6 +275,8 @@ static_assert(not is_disjoint_from(Literal[1, 2], Literal[2, 3])) ## Intersections +Positive requirements and negations can make an intersection disjoint from another type. + ```pyi from typing_extensions import Literal, final, Any, LiteralString from ty_extensions import static_assert, AlwaysFalsy @@ -475,6 +342,8 @@ static_assert(is_disjoint_from(AlwaysFalsy, LiteralString & ~Literal[""])) # er ## Special types +Some typing constructs and precisely described runtime values have their own disjointness rules. + ### `Never` `Never` is disjoint from every type, including itself. @@ -492,6 +361,8 @@ static_assert(is_disjoint_from(Never, object)) ### `None` +`None` overlaps only with types that can contain the `None` object. + ```pyi from typing_extensions import Literal, LiteralString from ty_extensions import static_assert @@ -515,6 +386,8 @@ static_assert(is_disjoint_from(None, int & ~str)) ### Literals +Literal types are disjoint when their values or runtime types cannot overlap. + ```pyi from typing_extensions import Literal, LiteralString from ty_extensions import static_assert, AlwaysFalsy, AlwaysTruthy @@ -577,6 +450,8 @@ static_assert(is_disjoint_from(LiteralString & ~AlwaysFalsy, ~LiteralString | Al ### Class, module and function literals +Class, module, and function literal types for distinct runtime objects are disjoint. + ```toml [environment] python-version = "3.12" @@ -624,6 +499,8 @@ static_assert(not is_disjoint_from(TypeOf[f], object)) ### Bound methods +Bound methods are disjoint when their names or possible receiver types cannot overlap. + ```py from typing import final from ty_extensions import static_assert @@ -708,6 +585,8 @@ static_assert(not is_disjoint_from(TypeOf[F().foo], TypeOf[G().foo])) ### `AlwaysTruthy` and `AlwaysFalsy` +`AlwaysTruthy` and `AlwaysFalsy` are disjoint from types with incompatible truthiness. + ```py from ty_extensions import AlwaysFalsy, AlwaysTruthy, static_assert from ty_extensions._internal import is_disjoint_from @@ -783,6 +662,9 @@ static_assert(is_disjoint_from(type[UsesMeta1], type[UsesMeta2])) ### `property` +Property descriptors and property-bearing classes are disjoint from incompatible final classes or +protocol requirements. + ```py from ty_extensions import static_assert from ty_extensions._internal import TypeOf, is_disjoint_from @@ -832,6 +714,8 @@ static_assert(is_disjoint_from(HasReadWriteIntProp, E)) ### `TypeGuard` and `TypeIs` +`TypeGuard` and `TypeIs` represent boolean return values, so they overlap `bool` but not `str`. + ```py from ty_extensions import static_assert from ty_extensions._internal import is_disjoint_from @@ -908,6 +792,8 @@ static_assert(is_disjoint_from(type[Foo], BarNone)) ### `NamedTuple` +`NamedTuple`s overlap matching tuple shapes, but not different lengths or distinct final classes. + ```py from __future__ import annotations @@ -932,49 +818,6 @@ static_assert(is_disjoint_from(Path, tuple[Path | None, str, int])) static_assert(is_disjoint_from(Path, Path2)) ``` -## Generic aliases - -```toml -[environment] -python-version = "3.12" -``` - -```py -from typing import final -from ty_extensions import static_assert -from ty_extensions._internal import TypeOf, is_disjoint_from - -class GenericClass[T]: - x: T # invariant - -static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass])) # error: [missing-type-argument] -static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass])) # error: [missing-type-argument] -static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass[int]])) -static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass[int]])) -static_assert(is_disjoint_from(TypeOf[GenericClass[str]], type[GenericClass[int]])) - -class GenericClassIntBound[T: int]: - x: T # invariant - -static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound])) # error: [missing-type-argument] -static_assert( - # error: [missing-type-argument] - not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound]) -) -static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound[int]])) -static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound[int]])) - -@final -class GenericFinalClass[T]: - x: T # invariant - -static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass])) # error: [missing-type-argument] -static_assert(not is_disjoint_from(TypeOf[GenericFinalClass[int]], type[GenericFinalClass])) # error: [missing-type-argument] -static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass[int]])) -static_assert(not is_disjoint_from(TypeOf[GenericFinalClass[int]], type[GenericFinalClass[int]])) -static_assert(is_disjoint_from(TypeOf[GenericFinalClass[str]], type[GenericFinalClass[int]])) -``` - ## Callables No two callable types are disjoint because there exists a non-empty callable type @@ -1127,8 +970,24 @@ static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[OrderedDict])) static_assert(not is_disjoint_from(TypeOf[OrderedDict], Callable[..., Any])) ``` +## Statically empty and non-empty ranges + +Empty and non-empty ranges are disjoint, but both overlap the general `range` type. + +```py +from ty_extensions import static_assert +from ty_extensions._internal import TypeOf, is_disjoint_from + +static_assert(is_disjoint_from(TypeOf[range(0)], TypeOf[range(1)])) +static_assert(is_disjoint_from(TypeOf[range(1)], TypeOf[range(0)])) +static_assert(not is_disjoint_from(TypeOf[range(0)], range)) +static_assert(not is_disjoint_from(TypeOf[range(1)], range)) +``` + ## Custom enum classes +Enum members overlap their enum class and its ancestors, but not other members or unrelated classes. + ```py from enum import Enum from ty_extensions import static_assert @@ -1152,3 +1011,457 @@ static_assert(is_disjoint_from(Literal[MyAnswer.NO], UnrelatedClass)) static_assert(not is_disjoint_from(Literal[MyAnswer.NO], MyAnswer)) static_assert(not is_disjoint_from(Literal[MyAnswer.NO], MyEnum)) ``` + +## Enum complements + +Enum types with complementary negations are disjoint when no enum member satisfies both. + +```pyi +from enum import Enum +from typing import Literal +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + +static_assert( + is_disjoint_from( + Color & ~Literal[Color.RED], + Color & ~Literal[Color.GREEN, Color.BLUE], + ) +) +static_assert( + is_disjoint_from( + Color & ~Literal[Color.GREEN, Color.BLUE], + Color & ~Literal[Color.RED], + ) +) +``` + +## Static tags and typed inhabitants + +An inhabitant of a type can be more than a bare runtime object: it can also include static type +information, not present at runtime, which can be understood as an invisible "tag". For example, a +generic tag records the type arguments of a specialization such as `list[int]`, while a `NewType` +tag identifies the `NewType` applied to a value. Neither kind of tag is visible on the runtime +object itself, but both affect which types an inhabitant belongs to. + +Generic tags carry guarantees about how an object can be used. The invariant types `list[int]` and +`list[str]` are disjoint because one reference could append a string that the other would then +incorrectly read as an integer. These incompatible generic tags cannot describe the same object +simultaneously in soundly typed code. + +Unlike incompatible invariant generic tags, distinct `NewType` tags can describe different typed +inhabitants of the same runtime object. If `UserId` and `OrderId` are distinct integer `NewType`s, +both `UserId(value)` and `OrderId(value)` return the same integer unchanged at runtime, but their +tags are incompatible. The two `NewType`s are disjoint even though their values can identify the +same runtime object. Both types still overlap `int`, which does not require either specific tag. + +### Invariant and covariant generic specializations + +Incompatible invariant arguments make generic specializations disjoint. Covariant specializations +can still overlap when a common empty or bottom specialization satisfies both. + +```pyi +from collections.abc import Sequence +from typing import Any, Generic, TypeVar +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +static_assert(is_disjoint_from(list[int], list[str])) + +T = TypeVar("T") +U = TypeVar("U") +T_co = TypeVar("T_co", covariant=True) + +class A: ... +class B: ... + +class Invariant(Generic[T]): + x: T + +class InvariantPair(Generic[T, U]): + x: T + y: U + +class Covariant(Generic[T_co]): + def get(self) -> T_co: + raise NotImplementedError() + +class InvSubA(Invariant[A]): + pass + +class CoSubB(Covariant[B]): + pass + +static_assert(is_disjoint_from(Invariant[A], Invariant[B])) +static_assert(is_disjoint_from(InvSubA, Invariant[B])) +static_assert(not is_disjoint_from(Invariant[A], Invariant[A])) +static_assert(not is_disjoint_from(Invariant[Any], Invariant[B])) +static_assert(not is_disjoint_from(Invariant[B], Invariant[Any])) +# `A | Any` cannot materialize to be equivalent to `B`. +static_assert(is_disjoint_from(Invariant[A | Any], Invariant[B])) +static_assert(is_disjoint_from(Invariant[B], Invariant[A | Any])) +static_assert(is_disjoint_from(Invariant[A & Any], Invariant[B])) +static_assert(is_disjoint_from(Invariant[B], Invariant[A & Any])) +static_assert(is_disjoint_from(InvariantPair[A, A], InvariantPair[A, B])) +static_assert(not is_disjoint_from(Covariant[A], Covariant[B])) +static_assert(not is_disjoint_from(Covariant[A], CoSubB)) +static_assert(not is_disjoint_from(Sequence[int], Sequence[str])) +``` + +### Specialized `@final` types + +Final generic specializations can overlap through a shared subtype such as `Foo[Never]`. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, final +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +@final +class Foo[T]: + def get(self) -> T: + raise NotImplementedError + +class A: ... +class B: ... + +static_assert(not is_disjoint_from(A, B)) +static_assert(not is_disjoint_from(Foo[A], Foo[B])) +static_assert(not is_disjoint_from(Foo[A], Foo[Any])) +static_assert(not is_disjoint_from(Foo[Any], Foo[B])) + +# `Foo[Never]` is inhabited (`get` can raise) and is a subtype of both `Foo[int]` and `Foo[str]`. +static_assert(not is_disjoint_from(Foo[int], Foo[str])) +``` + +### Type-variable aliases and empty invariant arguments + +Type-variable aliases preserve potentially compatible generic arguments. Empty or irrelevant type +arguments do not make otherwise compatible subclasses disjoint. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Generic, Never, TypeVar +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +T = TypeVar("T") + +class Invariant(Generic[T]): + x: T + +type Id[V] = V + +def _[U](): + static_assert(not is_disjoint_from(Invariant[U], Invariant[int])) + static_assert(not is_disjoint_from(Invariant[Id[U]], Invariant[int])) + +static_assert(not is_disjoint_from(Invariant[Id[int]], Invariant[int])) +static_assert(is_disjoint_from(Invariant[Id[int]], Invariant[str])) + +class Mixed[T, U]: + x: T + +# `Mixed` is bivariant in `U`, so the differing second argument cannot make these disjoint. +static_assert(not is_disjoint_from(Mixed[Never, int], Mixed[Never, str])) + +class Left(Invariant[Never]): ... +class Right(Invariant[Never]): ... +class Both(Left, Right): ... + +static_assert(not is_disjoint_from(Left, Right)) +``` + +### NewTypes and overlapping types + +A `NewType` overlaps with any nominal or structural type that overlaps its concrete base. This +includes the base itself, its supertypes and subclasses, and protocols satisfied by the base. + +```py +from typing import NewType, Protocol, final +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +UserId = NewType("UserId", int) + +@final +class FinalInt(int): ... + +class OrdinaryInt(int): ... + +class SupportsInt(Protocol): + def __int__(self) -> int: ... + +FinalIntId = NewType("FinalIntId", FinalInt) + +static_assert(not is_disjoint_from(UserId, int)) +static_assert(not is_disjoint_from(UserId, object)) +static_assert(not is_disjoint_from(UserId, FinalInt)) +static_assert(not is_disjoint_from(UserId, OrdinaryInt)) +static_assert(not is_disjoint_from(UserId, SupportsInt)) +static_assert(is_disjoint_from(UserId, str)) +static_assert(not is_disjoint_from(FinalIntId, FinalInt)) +static_assert(not is_disjoint_from(FinalIntId, int)) +``` + +### NewTypes and literal types + +The same overlap rule applies to literal types: a `NewType` overlaps with any literal type that +overlaps with its concrete base. Because `bool` is a subtype of `int`, type checkers correctly +accept `UserId(True)`, and an integer-based `NewType` also overlaps boolean literals. + +```py +from typing import Literal, NewType +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +UserId = NewType("UserId", int) +StringId = NewType("StringId", str) +BytesId = NewType("BytesId", bytes) + +UserId(True) + +static_assert(not is_disjoint_from(UserId, Literal[True])) +static_assert(not is_disjoint_from(Literal[True], UserId)) +static_assert(not is_disjoint_from(UserId, Literal[False])) +static_assert(not is_disjoint_from(UserId, Literal[1])) +static_assert(not is_disjoint_from(UserId, bool)) +static_assert(not is_disjoint_from(bool, UserId)) +static_assert(is_disjoint_from(UserId, Literal["user"])) + +static_assert(not is_disjoint_from(StringId, Literal["user"])) +static_assert(not is_disjoint_from(BytesId, Literal[b"user"])) +``` + +An `IntEnum` and its members also overlap with an integer-based `NewType`. + +```py +from enum import IntEnum + +class Choice(IntEnum): + FIRST = 1 + SECOND = 2 + +static_assert(not is_disjoint_from(UserId, Choice)) +static_assert(not is_disjoint_from(UserId, Literal[Choice.FIRST])) +``` + +Nested NewTypes retain the overlap, and a float-based NewType also accepts `int` and `bool` through +the `int`/`float` special case. + +```py +NestedUserId = NewType("NestedUserId", UserId) +FloatId = NewType("FloatId", float) +BoolId = NewType("BoolId", bool) + +static_assert(not is_disjoint_from(NestedUserId, bool)) +static_assert(not is_disjoint_from(NestedUserId, Literal[True])) +static_assert(not is_disjoint_from(FloatId, bool)) +static_assert(not is_disjoint_from(FloatId, Literal[True])) +static_assert(not is_disjoint_from(FloatId, Literal[1])) +static_assert(not is_disjoint_from(FloatId, int)) +static_assert(not is_disjoint_from(BoolId, bool)) +``` + +### NewTypes and type guards + +`TypeGuard` and `TypeIs` represent boolean return values, so they overlap with `NewType`s whose +concrete bases accept booleans, including `int` and `float`. A `NewType` with an incompatible base +remains disjoint. + +```py +from typing import NewType +from typing_extensions import TypeGuard, TypeIs +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from + +Boolean = NewType("Boolean", bool) +Integer = NewType("Integer", int) +Numeric = NewType("Numeric", float) +Text = NewType("Text", str) + +static_assert(not is_disjoint_from(Boolean, TypeGuard[str])) +static_assert(not is_disjoint_from(TypeIs[str], Boolean)) + +static_assert(not is_disjoint_from(Integer, TypeGuard[str])) + +static_assert(not is_disjoint_from(Numeric, TypeIs[str])) + +static_assert(is_disjoint_from(Text, TypeGuard[str])) +static_assert(is_disjoint_from(TypeIs[str], Text)) +``` + +### Distinct NewTypes + +Unrelated `NewType` tags are mutually exclusive, even when their constructors return the same +runtime object. For the runtime object `True`, `(bool, First)` and `(bool, Second)` are different +(runtime type, tag) pairs. Both inhabit `int`, `bool`, and `Literal[True]`, but only the first +inhabits `First` and only the second inhabits `Second`. No pair inhabits both `NewType`s, so those +types are disjoint even though each overlaps the same ordinary types. + +```py +from typing import Literal, NewType +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_disjoint_from, is_subtype_of + +First = NewType("First", int) +Second = NewType("Second", int) +Numeric = NewType("Numeric", float) +Text = NewType("Text", str) + +static_assert(is_disjoint_from(First, Second)) +static_assert(is_disjoint_from(Second, First)) +static_assert(is_disjoint_from(First, Numeric)) +static_assert(is_disjoint_from(First, Text)) + +static_assert(not is_disjoint_from(First, int)) +static_assert(not is_disjoint_from(Second, int)) +static_assert(not is_disjoint_from(First, bool)) +static_assert(not is_disjoint_from(Second, bool)) +static_assert(not is_disjoint_from(First, Literal[True])) +static_assert(not is_disjoint_from(Second, Literal[True])) + +static_assert(not is_subtype_of(First, Second)) +static_assert(not is_assignable_to(First, Second)) +``` + +### Nested NewTypes + +A nested `NewType` remains a subtype of its parent, so their types overlap. Independently nested +`NewType`s remain disjoint, as do a nested `NewType` and an unrelated tag. + +```py +from typing import NewType +from ty_extensions import static_assert +from ty_extensions._internal import is_assignable_to, is_disjoint_from, is_subtype_of + +First = NewType("First", int) +Second = NewType("Second", int) +NestedFirst = NewType("NestedFirst", First) +OtherNestedFirst = NewType("OtherNestedFirst", First) + +static_assert(is_disjoint_from(NestedFirst, Second)) +static_assert(is_disjoint_from(NestedFirst, OtherNestedFirst)) +static_assert(not is_disjoint_from(NestedFirst, First)) +static_assert(not is_disjoint_from(First, NestedFirst)) +static_assert(is_subtype_of(NestedFirst, First)) +static_assert(is_assignable_to(NestedFirst, First)) +static_assert(not is_assignable_to(First, NestedFirst)) +``` + +### NewTypes and generic classes + +A `NewType` based on a covariant generic specialization overlaps with its generic supertypes and +subclasses. Two differently specialized covariant types can also overlap through a common, more +specific specialization. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, NewType +from ty_extensions import static_assert +from ty_extensions._internal import is_disjoint_from, is_subtype_of + +class Base[T]: + def get(self) -> T: + raise NotImplementedError + +class Child[T](Base[T]): ... + +BaseId = NewType("BaseId", Base[int]) + +static_assert(not is_disjoint_from(BaseId, Base[int])) +static_assert(not is_disjoint_from(BaseId, Base[object])) +# `Base[Never]` is inhabited (`get` can raise) and is a subtype of both `Base[int]` and `Base[str]`. +static_assert(not is_disjoint_from(BaseId, Base[str])) +static_assert(not is_disjoint_from(BaseId, Child[object])) +``` + +An ordinary gradual specialization can overlap a `NewType` even when strict subtyping does not hold. +Independently defined `NewType`s remain disjoint. + +```py +AnyListId = NewType("AnyListId", list[Any]) +IntListId = NewType("IntListId", list[int]) + +static_assert(not is_subtype_of(AnyListId, list[int])) +static_assert(not is_disjoint_from(AnyListId, list[int])) +static_assert(not is_disjoint_from(IntListId, list[Any])) +static_assert(is_disjoint_from(IntListId, list[str])) +static_assert(is_disjoint_from(IntListId, AnyListId)) +``` + +A generic type variable must not make a potentially compatible specialization appear disjoint. +Compatible constraints and bounds also preserve the overlap. + +```py +def unconstrained[T]() -> None: + static_assert(not is_disjoint_from(IntListId, list[T])) + static_assert(not is_disjoint_from(list[T], IntListId)) + +def compatible_constraints[T: (int, str)]() -> None: + static_assert(not is_disjoint_from(IntListId, list[T])) + +def compatible_bound[T: int]() -> None: + static_assert(not is_disjoint_from(IntListId, list[T])) +``` + +## Generic aliases + +Generic class objects and aliases overlap compatible `type[...]` types; incompatible invariant +specializations make them disjoint. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, final +from ty_extensions import static_assert +from ty_extensions._internal import TypeOf, is_disjoint_from + +class GenericClass[T]: + x: T # invariant + +static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass[Any]])) +static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass[Any]])) +static_assert(not is_disjoint_from(TypeOf[GenericClass], type[GenericClass[int]])) +static_assert(not is_disjoint_from(TypeOf[GenericClass[int]], type[GenericClass[int]])) +static_assert(is_disjoint_from(TypeOf[GenericClass[str]], type[GenericClass[int]])) + +class GenericClassIntBound[T: int]: + x: T # invariant + +static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound[Any]])) +static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound[Any]])) +static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound], type[GenericClassIntBound[int]])) +static_assert(not is_disjoint_from(TypeOf[GenericClassIntBound[int]], type[GenericClassIntBound[int]])) + +@final +class GenericFinalClass[T]: + x: T # invariant + +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass[Any]])) +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass[int]], type[GenericFinalClass[Any]])) +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass], type[GenericFinalClass[int]])) +static_assert(not is_disjoint_from(TypeOf[GenericFinalClass[int]], type[GenericFinalClass[int]])) +static_assert(is_disjoint_from(TypeOf[GenericFinalClass[str]], type[GenericFinalClass[int]])) +``` diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index 317e864ea8..4a6bcc11fa 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -5,6 +5,7 @@ //! methods. use rustc_hash::FxHashSet; +use ty_python_core::definition::Definition; use crate::{AnalysisSettings, Db, ProgramEnvironment, place::PlaceAndQualifiers}; @@ -13,6 +14,7 @@ use super::{ LiteralValueType, LiteralValueTypeKind, MemberLookupPolicy, Truthiness, Type, TypeContext, TypeVarBoundOrConstraints, UnionBuilder, bool::BoolError, + cyclic::ActiveRecursionDetector, enums::{enum_member_literals, enum_metadata}, }; @@ -999,23 +1001,60 @@ pub(super) fn is_same_enum_domain<'db>( ty: Type<'db>, right: EnumLiteralType<'db>, ) -> bool { - match ty.resolve_type_alias(db) { - Type::LiteralValue(literal) => matches!( - literal.kind(), - LiteralValueTypeKind::Enum(left) - if left.enum_class(db) == right.enum_class(db) - ), - Type::Union(union) => union - .elements(db) - .iter() - .all(|element| is_same_enum_domain(db, env, *element, right)), - Type::NominalInstance(instance) => instance.class_literal(db, env) == right.enum_class(db), - Type::EnumComplement(complement) => complement.enum_class(db) == right.enum_class(db), - Type::Intersection(intersection) => intersection - .enum_complement(db, env) - .is_some_and(|complement| complement.enum_class(db) == right.enum_class(db)), - _ => false, + // A proof made while another alias is active can still be disproved by a later union arm, so + // completed visits must not be cached. + #[derive(Default)] + struct EnumDomainVisitor<'db> { + active_specializations: ActiveRecursionDetector>, + active_definitions: ActiveRecursionDetector>, } + + fn visit<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ty: Type<'db>, + right: EnumLiteralType<'db>, + visitor: &EnumDomainVisitor<'db>, + ) -> bool { + match ty { + // The same specialization preserves the domain; different arguments can introduce + // values outside it even when the alias definition is the same. + Type::TypeAlias(alias) => visitor.active_specializations.visit( + &ty, + || true, + || { + visitor.active_definitions.visit( + &alias.definition(db), + || false, + || visit(db, env, alias.value_type(db), right, visitor), + ) + }, + ), + Type::LiteralValue(literal) => matches!( + literal.kind(), + LiteralValueTypeKind::Enum(left) + if left.enum_class(db) == right.enum_class(db) + ), + Type::Union(union) => union + .elements(db) + .iter() + .all(|&element| visit(db, env, element, right, visitor)), + Type::NewTypeInstance(newtype) => { + visit(db, env, newtype.concrete_base_type(db), right, visitor) + } + Type::NominalInstance(instance) => { + instance.class_literal(db, env) == right.enum_class(db) + } + Type::EnumComplement(complement) => complement.enum_class(db) == right.enum_class(db), + Type::Intersection(intersection) => intersection + .positive(db) + .iter() + .any(|&element| visit(db, env, element, right, visitor)), + _ => false, + } + } + + visit(db, env, ty, right, &EnumDomainVisitor::default()) } /// Evaluate each alternative of the union being constrained and combine their branch results. @@ -1298,11 +1337,42 @@ fn finite_alternatives<'db>( .then(|| complement.remaining_literal_types(db, env)) } Type::Intersection(intersection) => { - let complement = intersection.enum_complement(db, env)?; - KnownComparisonSemantics::of_type(db, env, ty, operator) + let (comparison_type, complement) = if let Some(complement) = + intersection.enum_complement(db, env) + { + (ty, complement) + } else { + if !intersection.positive(db).iter().any(|positive| { + matches!(positive.resolve_type_alias(db), Type::NewTypeInstance(_)) + }) { + return None; + } + + let expanded = intersection.with_expanded_typevars_and_newtypes(db, env); + let complement = match expanded { + Type::LiteralValue(literal) if literal.is_enum() => { + return KnownComparisonSemantics::of_type(db, env, expanded, operator) + .is_some() + .then(|| vec![expanded]); + } + Type::EnumComplement(complement) => complement, + Type::Intersection(intersection) => intersection.enum_complement(db, env)?, + _ => return None, + }; + (expanded, complement) + }; + KnownComparisonSemantics::of_type(db, env, comparison_type, operator) .is_some() .then(|| complement.remaining_literal_types(db, env)) } + Type::NewTypeInstance(newtype) => { + let base = newtype.concrete_base_type(db); + if base.is_enum(db, env) { + finite_alternatives(db, env, base, operator) + } else { + None + } + } Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Bool) => { Some(vec![Type::bool_literal(true), Type::bool_literal(false)]) } diff --git a/crates/ty_python_semantic/src/types/equality/enums.rs b/crates/ty_python_semantic/src/types/equality/enums.rs index afd8746ec5..d7ce2ac6b5 100644 --- a/crates/ty_python_semantic/src/types/equality/enums.rs +++ b/crates/ty_python_semantic/src/types/equality/enums.rs @@ -384,8 +384,8 @@ enum EnumValueSetMembers<'db> { impl<'db> EnumValueSet<'db> { /// Extract only structural enum membership facts from `ty`. /// - /// This deliberately does not use subtyping: a `NewType` over an enum is a subtype of the - /// enum but remains disjoint from the enum's literal members. + /// This deliberately does not use subtyping: extra nominal restrictions must not be + /// transferred to the other comparison operand. fn from_type( db: &'db dyn Db, env: &ProgramEnvironment<'db>, @@ -417,6 +417,9 @@ impl<'db> EnumValueSet<'db> { enum_class: instance.class_literal(db, env).into_enum_class(db)?, members: EnumValueSetMembers::All, }, + Type::NewTypeInstance(newtype) => { + EnumValueSet::from_type(db, env, newtype.concrete_base_type(db), active_types)? + } Type::EnumComplement(complement) => EnumValueSet { enum_class: complement.enum_class_literal(db), members: EnumValueSetMembers::AllExcept(complement), @@ -522,6 +525,16 @@ impl<'db> EnumValueSet<'db> { return Self::from_type(db, env, Type::EnumComplement(complement), active_types); } + if intersection + .positive(db) + .iter() + .any(|positive| matches!(positive.resolve_type_alias(db), Type::NewTypeInstance(_))) + && let expanded = intersection.with_expanded_typevars_and_newtypes(db, env) + && let Some(value_set) = Self::from_type(db, env, expanded, active_types) + { + return Some(value_set); + } + // Other intersection components can only reduce the represented enum values. Ignoring // them therefore preserves a safe upper bound without transferring them during narrowing. let mut value_sets = intersection diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index 0286852f20..2d1a2340a3 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -23,14 +23,24 @@ impl<'db> Type<'db> { /// Upcast `self` to a type that conservatively describes its possible runtime objects in an /// identity comparison. /// - /// A `NewType` wrapper is an identity function at runtime, so it contributes its concrete base - /// type here while remaining distinct for ordinary type relations and intersections. + /// A `NewType` constructor returns its argument unchanged, so its tag can differ between two + /// views of the same object: upcast a `NewType` to its concrete base. In contrast, preserve + /// invariant generic arguments because the same mutable object cannot satisfy incompatible + /// commitments such as `list[int]` and `list[str]` without some other code already being + /// unsound. /// - /// Negative intersection elements are generally omitted. A static exclusion does not imply a - /// runtime exclusion: `NewType("N", bool)(True)` can inhabit `~Literal[True]`, but evaluates - /// to the `True` singleton at runtime. However, excluding an entire nominal instance type is - /// stable under `NewType` erasure, so constraints such as `~None` and `~SomeClass` are - /// preserved. + /// Preserve negations that constrain the object itself, such as `~None`, `~SomeClass`, and + /// `~Literal[1]`. A `NewType` tag, type-variable selection, or type-guard proof can differ + /// between views. Retain the existing conservative handling of negated string types. + /// + /// A type variable can also hide a `NewType` tag: even a variable bounded by `int` can be + /// instantiated as an integer `NewType`. Expand variables to their upcast bounds or constraints + /// instead of transferring that potentially tagged relationship; an unbounded variable becomes + /// `object`. + /// + /// Use this upcast both to decide whether identity is possible and to narrow the other + /// operand when it succeeds. Each operand retains its own existing tags and type-variable + /// relationships when the resulting constraint is applied. pub(crate) fn identity_comparison_type( self, db: &'db dyn Db, @@ -48,13 +58,15 @@ impl<'db> Type<'db> { Type::TypeAlias(alias) => { visitor.visit_type(db, ty, || upcast(db, env, alias.value_type(db), visitor)) } - Type::NewTypeInstance(newtype) => newtype.concrete_base_type(db), + Type::NewTypeInstance(newtype) => { + upcast(db, env, newtype.concrete_base_type(db), visitor) + } Type::TypeVar(typevar) => visitor.visit_type(db, ty, || { match typevar.typevar(db).bound_or_constraints(db, env) { Some(bound_or_constraints) => { upcast(db, env, bound_or_constraints.as_type(db, env), visitor) } - None => ty, + None => KnownClass::Object.to_instance(db, env), } }), Type::Union(union) => { @@ -66,8 +78,19 @@ impl<'db> Type<'db> { builder = builder.add_positive(upcast(db, env, *element, visitor)); } for element in intersection.negative(db) { - if element.resolve_type_alias(db).is_nominal_instance() { - builder = builder.add_negative(*element); + // Static tags and predicate proofs can differ between views. Retain the + // existing conservative handling of negated string types. + match element.resolve_type_alias(db) { + Type::NewTypeInstance(_) + | Type::TypeVar(_) + | Type::TypeIs(_) + | Type::TypeGuard(_) => continue, + Type::LiteralValue(literal) + if literal.is_literal_string() || literal.is_string() => + { + continue; + } + _ => builder.add_negative_in_place(*element), } } builder.build() diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index cc0956b59c..151a98d83e 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -3632,9 +3632,11 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { Some(rhs_constraint.negate(db, &self.env)) } ast::CmpOp::Is => { - let mut builder = UnionBuilder::new(db, &self.env).add(rhs_ty); - let rhs_resolved = rhs_ty.resolve_type_alias(db); let rhs_identity_ty = rhs_ty.identity_comparison_type(db, &self.env); + // Identity transfers the runtime type, not a `NewType` tag or type-variable + // selection belonging to the other operand. + let mut builder = UnionBuilder::new(db, &self.env).add(rhs_identity_ty); + let rhs_resolved = rhs_ty.resolve_type_alias(db); let add_runtime_overlap = |builder: UnionBuilder<'db>, element: Type<'db>| { let overlaps_only_at_runtime = |rhs_element| { element.is_disjoint_from(db, &self.env, rhs_element) diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index b2e880de58..e9718029a5 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -3521,6 +3521,17 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { }) } + ( + Type::NewTypeInstance(newtype), + other @ (Type::LiteralValue(_) | Type::TypeIs(_) | Type::TypeGuard(_)), + ) + | ( + other @ (Type::LiteralValue(_) | Type::TypeIs(_) | Type::TypeGuard(_)), + Type::NewTypeInstance(newtype), + ) => nontrivial_check(self, || { + self.check_type_pair(db, newtype.concrete_base_type(db), other) + }), + (Type::TypeIs(_) | Type::TypeGuard(_), _) | (_, Type::TypeIs(_) | Type::TypeGuard(_)) => self.always(), diff --git a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs index c616a98d0c..c1a6daba7a 100644 --- a/crates/ty_python_semantic/src/types/set_theoretic/builder.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs @@ -1972,6 +1972,17 @@ impl<'db> InnerIntersectionBuilder<'db> { if speculative.is_never() { return Type::Never; } + + if let Type::EnumComplement(complement) = speculative + && complement.is_singleton(db) + && self + .positive + .iter() + .any(|positive| matches!(positive, Type::NewTypeInstance(_))) + { + // Preserve the NewType while making its remaining enum member explicit. + self.add_positive(db, env, complement.remaining_literal_union(db, env)); + } } if let Some(complement) = From a706896d775177790899ee4d4acf5553df2c96d1 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 7 Aug 2026 13:19:36 -0700 Subject: [PATCH 329/390] [ty] Infer precise TypedDict key-membership truthiness (#27579) ## Summary - Infer precise boolean literals for guaranteed-present and guaranteed-absent TypedDict keys while preserving `bool` for optional keys, open schemas, extra items, and non-literal keys. - Share TypedDict key-membership truthiness with narrowing so impossible membership branches are recognized by reachability analysis and surfaced as existing IDE unreachable-code hints. Closes astral-sh/ty#4210. ## Test plan - Add membership mdtests covering required and optional keys, closed and open TypedDicts, `extra_items=Never`, `NotRequired[Never]`, `in` and `not in`, union and non-literal keys, and functional TypedDict definitions. - Update the existing functional TypedDict expectation for membership in a required key. - Add a reachability regression covering absent closed-schema keys and negated membership checks for required keys. --- .../comparison/instances/membership_test.md | 105 ++++++++++++++++++ .../resources/mdtest/typed_dict.md | 2 +- .../src/types/ide_support/unreachable_code.rs | 22 ++++ .../src/types/infer/comparisons.rs | 10 ++ crates/ty_python_semantic/src/types/narrow.rs | 15 ++- .../src/types/typed_dict.rs | 15 +++ 6 files changed, 162 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md b/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md index 42508636e0..d101b57fde 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/instances/membership_test.md @@ -122,6 +122,111 @@ reveal_type(42 in AlwaysFalse()) # revealed: Literal[False] reveal_type(42 not in AlwaysFalse()) # revealed: Literal[True] ``` +## Required and optional `TypedDict` keys + +A required key is always present, while an optional key may or may not be present. + +```py +from typing_extensions import NotRequired, TypedDict + +class Items(TypedDict): + required: int + optional: NotRequired[int] + +def membership(items: Items) -> None: + reveal_type("required" in items) # revealed: Literal[True] + reveal_type("required" not in items) # revealed: Literal[False] + reveal_type("optional" in items) # revealed: bool + reveal_type("optional" not in items) # revealed: bool +``` + +## Absent keys in closed `TypedDict`s + +A closed `TypedDict` cannot contain an undeclared key or an optional key whose value type is +uninhabited. Declaring `extra_items=Never` closes a `TypedDict` in the same way as `closed=True`. + +```py +from typing_extensions import Never, NotRequired, TypedDict + +class Closed(TypedDict, closed=True): + present: int + impossible: NotRequired[Never] + +class ClosedByExtraItems(TypedDict, extra_items=Never): + present: int + +def closed_membership(closed: Closed, closed_by_extra_items: ClosedByExtraItems) -> None: + reveal_type("missing" in closed) # revealed: Literal[False] + reveal_type("missing" not in closed) # revealed: Literal[True] + reveal_type("impossible" in closed) # revealed: Literal[False] + reveal_type("impossible" not in closed) # revealed: Literal[True] + reveal_type("missing" in closed_by_extra_items) # revealed: Literal[False] + reveal_type("missing" not in closed_by_extra_items) # revealed: Literal[True] +``` + +## Undeclared keys in open `TypedDict`s + +Open `TypedDict`s and `TypedDict`s with nonempty extra items may contain keys that their schemas do +not declare. + +```py +from typing_extensions import TypedDict + +class Open(TypedDict): + present: int + +class ExtraItems(TypedDict, extra_items=int): + present: int + +def open_membership(open_items: Open, extra_items: ExtraItems) -> None: + reveal_type("missing" in open_items) # revealed: bool + reveal_type("missing" not in open_items) # revealed: bool + reveal_type("missing" in extra_items) # revealed: bool + reveal_type("missing" not in extra_items) # revealed: bool +``` + +## `TypedDict` membership with unions and non-literal keys + +Membership remains ambiguous when either the key or the `TypedDict` can vary between a present and +an absent alternative. A key missing from every closed alternative is always absent. + +```py +from typing_extensions import Literal, TypedDict + +class Left(TypedDict, closed=True): + left: int + +class Right(TypedDict, closed=True): + right: int + +def union_membership( + left: Left, + either: Left | Right, + literal_key: Literal["left", "missing"], + unknown_key: str, +) -> None: + reveal_type("missing" in either) # revealed: Literal[False] + reveal_type("missing" not in either) # revealed: Literal[True] + reveal_type("left" in either) # revealed: bool + reveal_type(literal_key in left) # revealed: bool + reveal_type(unknown_key in left) # revealed: bool +``` + +## Functional closed `TypedDict` membership + +Functional `TypedDict` definitions expose the same key-presence information as class-based +definitions. + +```py +from typing_extensions import TypedDict + +Closed = TypedDict("Closed", {"present": int}, closed=True) + +def functional_membership(closed: Closed) -> None: + reveal_type("present" in closed) # revealed: Literal[True] + reveal_type("missing" in closed) # revealed: Literal[False] +``` + ## No Fallback for `__contains__` If `__contains__` is implemented, checking membership of a type it doesn't accept is an error; it diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 8f6feb0077..22fc441f3a 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -5180,7 +5180,7 @@ def _(p: Person) -> None: reveal_type(p.setdefault("name", "Alice")) # revealed: str # __contains__ - reveal_type("name" in p) # revealed: bool + reveal_type("name" in p) # revealed: Literal[True] # __setitem__ p["name"] = "Alice" diff --git a/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs b/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs index 871bf4eb77..3da1ea8858 100644 --- a/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs +++ b/crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs @@ -389,6 +389,28 @@ mod tests { Ok(()) } + #[test] + fn reports_impossible_typed_dict_key_membership() -> anyhow::Result<()> { + let source = r#" + from typing_extensions import TypedDict + + class Items(TypedDict, closed=True): + present: int + + def f(items: Items) -> None: + if "missing" in items: + print("missing") + if "present" not in items: + print("present") + "#; + + let diagnostics = UnreachableTest::new().render(source)?; + assert_eq!(diagnostics.matches("Code is unreachable").count(), 2); + assert!(diagnostics.contains("print(\"missing\")")); + assert!(diagnostics.contains("print(\"present\")")); + Ok(()) + } + #[test] fn reports_statically_empty_loop_bodies() -> anyhow::Result<()> { let source = r#" diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index 2d1a2340a3..58ffe20789 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -1058,6 +1058,16 @@ fn infer_membership_test_comparison<'db>( ) -> Result, UnsupportedComparisonError<'db>> { let db = context.db(); let env = &context.program_environment(); + + if let Some(key) = left.as_string_literal() + && let Some(typed_dict) = right.as_typed_dict() + { + let truthiness = typed_dict + .key_membership_truthiness(db, key.value(db)) + .negate_if(op.is_not_in()); + return Ok(Type::from_truthiness(db, env, truthiness)); + } + let compare_result_opt = match right.try_call_dunder( db, env, diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 151a98d83e..49a3f56916 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -7,9 +7,7 @@ use crate::types::function::KnownFunction; use crate::types::infer::{ExpressionInference, infer_same_file_expression_type}; use crate::types::special_form::TypeQualifier; use crate::types::tuple::{TupleLength, TupleSpec, TupleSpecBuilder, TupleType, TupleUnpacker}; -use crate::types::typed_dict::{ - TypedDictField, TypedDictFieldBuilder, TypedDictSchema, TypedDictType, -}; +use crate::types::typed_dict::{TypedDictFieldBuilder, TypedDictSchema, TypedDictType}; use crate::types::{ CallableType, ClassBase, ClassLiteral, ClassPatternPositionalSource, ClassType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LiteralValueTypeKind, @@ -4042,9 +4040,7 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { } } else { let requires_key = |td: TypedDictType<'db>| -> bool { - td.items(db) - .get(key) - .is_some_and(TypedDictField::is_required) + td.key_membership_truthiness(db, key).is_always_true() }; let resolved_rhs_type = rhs_type.resolve_type_alias(db); @@ -4815,6 +4811,13 @@ impl<'db> NarrowingConstraintsBuilder<'db, '_> { Type::Union(union) => union.map(db, &self.env, |element| { self.narrow_with_present_key(*element, key) }), + Type::TypedDict(typed_dict) + if typed_dict + .key_membership_truthiness(db, key) + .is_always_false() => + { + Type::Never + } resolved if typeddict_declares_key(db, resolved, key) => resolved, // TODO: Extend this to subtypes of `Mapping[str, object]` whose membership and // subscript operations obey the `Mapping` contract. diff --git a/crates/ty_python_semantic/src/types/typed_dict.rs b/crates/ty_python_semantic/src/types/typed_dict.rs index e79349d5ad..be8748ecb2 100644 --- a/crates/ty_python_semantic/src/types/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/typed_dict.rs @@ -27,6 +27,7 @@ use crate::types::class::FieldKind; use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::relation::{DisjointnessChecker, TypeRelation, TypeRelationChecker}; use crate::{Db, ProgramEnvironment}; +use ty_python_core::Truthiness; use ty_python_core::definition::Definition; bitflags! { @@ -360,6 +361,20 @@ impl<'db> TypedDictType<'db> { .build() } + /// Returns whether a literal string key must, cannot, or might be present. + /// + /// An undeclared key can still exist in an implicitly open `TypedDict` or one with explicit + /// extra items. An optional field with an uninhabited value type can never be present. + pub(crate) fn key_membership_truthiness(self, db: &'db dyn Db, key: &str) -> Truthiness { + match self.items(db).get(key) { + Some(field) if field.is_required() => Truthiness::AlwaysTrue, + Some(field) if field.may_be_present(db) => Truthiness::Ambiguous, + Some(_) => Truthiness::AlwaysFalse, + None if self.openness(db).is_closed() => Truthiness::AlwaysFalse, + None => Truthiness::Ambiguous, + } + } + /// Returns the field exposed by a literal key. /// /// Undeclared keys synthesize a field only for explicit extra items. Hidden items on an From 2c5dcc21c5f1ee969802a19e027020d413321512 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Fri, 7 Aug 2026 21:37:08 +0100 Subject: [PATCH 330/390] [`ruff`] Also suggest `asyncio.TaskGroup` (`RUF006`) (#27461) ## Summary Suggest using `asyncio.TaskGroup` in [asyncio-dangling-task (RUF006)](https://docs.astral.sh/ruff/rules/asyncio-dangling-task/#asyncio-dangling-task-ruf006). Related to https://github.com/astral-sh/ruff/issues/8451#issuecomment-5177709084 ## Test Plan `cargo textest run` --- .../rules/ruff/rules/asyncio_dangling_task.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs b/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs index e93433377f..857844b974 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/asyncio_dangling_task.rs @@ -49,9 +49,23 @@ use crate::checkers::ast::Checker; /// task.add_done_callback(background_tasks.discard) /// ``` /// +/// Or, for Python 3.11 and later, use structured concurrency with +/// `asyncio.TaskGroup` when the tasks should be awaited as part of the current +/// operation: +/// ```python +/// import asyncio +/// +/// +/// async def main() -> None: +/// async with asyncio.TaskGroup() as tg: +/// for i in range(10): +/// tg.create_task(some_coro(param=i)) +/// ``` +/// /// ## References /// - [_The Heisenbug lurking in your async code_](https://textual.textualize.io/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/) -/// - [The Python Standard Library](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task) +/// - [Python documentation: `asyncio.create_task`](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task) +/// - [Python documentation: `asyncio.TaskGroup`](https://docs.python.org/3/library/asyncio-task.html#asyncio.TaskGroup) #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.247")] pub(crate) struct AsyncioDanglingTask { From df3216e414a9b78341a5e2fc6cd56adef69b7033 Mon Sep 17 00:00:00 2001 From: romero-deshaw Date: Fri, 7 Aug 2026 16:45:19 -0400 Subject: [PATCH 331/390] [`numpy`] Make `np.chararray` autofix backwards-compatible (`NPY201`) (#27527) ## Summary `NPY201` currently rewrites: ```python import numpy as np np.chararray((1,), 4, unicode=True) ``` to: ```python from numpy.char import chararray chararray((1,), 4, unicode=True) ``` On NumPy versions 1.x , this fails with: ```text ModuleNotFoundError: No module named 'numpy.char' ``` Although `numpy.char` is not an importable module, it is available as an attribute. The fix now reuses the existing NumPy binding: ```python import numpy as np np.char.chararray((1,), 4, unicode=True) ``` If necessary, Ruff instead imports `char` with `from numpy import char` and uses `char.chararray`. ## Test Plan This is a relatively minimal change, and I verified that it correctly handles both import cases: ### Reusing an existing NumPy module import Before: ```python import numpy as np array = np.chararray((1,), itemsize=4, unicode=True) ``` After running: ```bash ruff check --select NPY201 --fix example.py ``` ```python import numpy as np array = np.char.chararray((1,), itemsize=4, unicode=True) ``` ### Importing the `char` namespace Before: ```python from numpy import chararray array = chararray((1,), itemsize=4, unicode=True) ``` After running: ```bash ruff check --select NPY201 --fix example.py ``` ```python from numpy import chararray, char array = char.chararray((1,), itemsize=4, unicode=True) ``` The original `chararray` import is now unused and can subsequently be removed by `F401`. --------- Co-authored-by: Jackson Romero Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../rules/numpy/rules/numpy_2_0_deprecation.rs | 16 +++++++++++++++- ...y__tests__numpy2-deprecation_NPY201_2.py.snap | 9 +++------ ...y__tests__numpy2-deprecation_NPY201_3.py.snap | 9 +++------ 3 files changed, 21 insertions(+), 13 deletions(-) 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 efe8576b51..59b003d5db 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 @@ -683,12 +683,26 @@ pub(crate) fn numpy_2_0_deprecation(checker: &Checker, expr: &Expr) { compatibility, } => { diagnostic.try_set_fix(|| { + // `numpy.char` is not an importable module path on NumPy 1.x. + let (path, name, attribute) = if matches!( + (path, name), + ("numpy.char", "chararray" | "compare_chararrays") + ) { + ("numpy", "char", Some(name)) + } else { + (path, name, None) + }; let (import_edit, binding) = checker.importer().get_or_import_symbol( &ImportRequest::import_from(path, name), expr.start(), checker.semantic(), )?; - let replacement_edit = Edit::range_replacement(binding, expr.range()); + let replacement = if let Some(attribute) = attribute { + format!("{binding}.{attribute}") + } else { + binding + }; + let replacement_edit = Edit::range_replacement(replacement, expr.range()); Ok(match compatibility { Compatibility::BackwardsCompatible => { Fix::safe_edits(import_edit, [replacement_edit]) diff --git a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_2.py.snap b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_2.py.snap index ecc91ce8c2..0984b70ac5 100644 --- a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_2.py.snap +++ b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_2.py.snap @@ -434,13 +434,10 @@ NPY201 [*] `np.compare_chararrays` will be removed in NumPy 2.0. Use `numpy.char | help: Replace with `numpy.char.compare_chararrays` | -1 + from numpy.char import compare_chararrays -2 | def func(): --------------------------------------------------------------------------------- -58 | +57 | - np.compare_chararrays -59 + compare_chararrays -60 | +58 + np.char.compare_chararrays +59 | | NPY201 [*] `np.alltrue` will be removed in NumPy 2.0. Use `numpy.all` instead. diff --git a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_3.py.snap b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_3.py.snap index 10bb6c129c..b1d78c1d85 100644 --- a/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_3.py.snap +++ b/crates/ruff_linter/src/rules/numpy/snapshots/ruff_linter__rules__numpy__tests__numpy2-deprecation_NPY201_3.py.snap @@ -119,13 +119,10 @@ NPY201 [*] `np.chararray` will be removed in NumPy 2.0. Use `numpy.char.chararra | help: Replace with `numpy.char.chararray` | -1 + from numpy.char import chararray -2 | def func(): --------------------------------------------------------------------------------- -14 | +13 | - np.chararray -15 + chararray -16 | +14 + np.char.chararray +15 | | NPY201 [*] `np.format_parser` will be removed in NumPy 2.0. Use `numpy.rec.format_parser` instead. From 6f86de2eb6363f77a314998c414ac8b53849b92f Mon Sep 17 00:00:00 2001 From: Avasam Date: Fri, 7 Aug 2026 16:47:02 -0400 Subject: [PATCH 332/390] [`pylint`] Fix false negatives on negative numbers (`PLR6104`) (#27251) ## Summary Extracted from https://github.com/astral-sh/ruff/pull/27188 Rule [non-augmented-assignment (PLR6104)](https://docs.astral.sh/ruff/rules/non-augmented-assignment/#non-augmented-assignment-plr6104) was only checking for literals number, which missed constants with unary ops (like a negative number) that didn't parse as "literal". ```py # These were not flagged x = -1 + x x = -1.5 + x flags = ~0x1 & f # now becoming x += -1 x += -1.5 flags &= ~0x1 ``` Not simplifying further to `x -= 1` because it keeps this changeset simpler and safe (see https://github.com/astral-sh/ruff/issues/12890) ## Test Plan New tests and snapshots ## Coding Agent disclaimer Code was written by Claude Opus 5 with a few passes of caveman-review. Fully reviewed with limited Rust knowledge. PR description fully handwritten. --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com> --- .../mdtest/pylint/non-augmented-assignment.md | 114 ++++++++++++++++++ .../pylint/rules/non_augmented_assignment.rs | 15 ++- 2 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 crates/ruff_linter/resources/mdtest/pylint/non-augmented-assignment.md diff --git a/crates/ruff_linter/resources/mdtest/pylint/non-augmented-assignment.md b/crates/ruff_linter/resources/mdtest/pylint/non-augmented-assignment.md new file mode 100644 index 0000000000..63ec1db2e5 --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/pylint/non-augmented-assignment.md @@ -0,0 +1,114 @@ +# `non-augmented-assignment` (`PLR6104`) + +```toml +[lint] +preview = true +select = ["PLR6104"] +``` + +## Unary operators on literals + +When the assignment target is the right-hand operand, the rule only rewrites the assignment if the +other operand is a number or a boolean literal, because the operator has to commute for the rewrite +to preserve behavior. + +The parser does not fold constants, so `-1` is a unary `-` applied to `1` rather than a literal. Any +stack of `+`, `-`, `~` or `not` over a number or boolean literal still evaluates to a number or a +boolean, so the operand is peeled before the literal check. + +```py +to_multiply = -1 + to_multiply # snapshot: non-augmented-assignment +to_multiply = +1 * to_multiply # error: [non-augmented-assignment] +to_multiply = --1 + to_multiply # error: [non-augmented-assignment] +to_multiply = -1.5 + to_multiply # error: [non-augmented-assignment] +to_multiply = -1j + to_multiply # error: [non-augmented-assignment] +flags = ~0x1 & flags # error: [non-augmented-assignment] +flags = -True | flags # error: [non-augmented-assignment] +``` + +```snapshot +error[PLR6104]: Use `+=` to perform an augmented assignment directly + --> src/mdtest_snippet.py:1:1 + | +1 | to_multiply = -1 + to_multiply # snapshot: non-augmented-assignment + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: Replace with augmented assignment + | + - to_multiply = -1 + to_multiply # snapshot: non-augmented-assignment +1 + to_multiply += -1 # snapshot: non-augmented-assignment +2 | to_multiply = +1 * to_multiply # error: [non-augmented-assignment] + | +note: This is an unsafe fix and may change runtime behavior +``` + +Parentheses around the moved operand are preserved: + +```py +to_multiply = (not True) + to_multiply # snapshot: non-augmented-assignment +``` + +```snapshot +error[PLR6104]: Use `+=` to perform an augmented assignment directly + --> src/mdtest_snippet.py:8:1 + | +8 | to_multiply = (not True) + to_multiply # snapshot: non-augmented-assignment + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: Replace with augmented assignment + | +7 | flags = -True | flags # error: [non-augmented-assignment] + - to_multiply = (not True) + to_multiply # snapshot: non-augmented-assignment +8 + to_multiply += (not True) # snapshot: non-augmented-assignment + | +note: This is an unsafe fix and may change runtime behavior +``` + +## Target already on the left + +Commutativity is irrelevant when the target is the left-hand operand, so a unary operand needs no +literal check at all. The right-hand side of an augmented assignment accepts any expression, so the +moved operand never needs new parentheses either. + +```py +to_multiply = to_multiply**-1 # snapshot: non-augmented-assignment +to_multiply = to_multiply - -1 # error: [non-augmented-assignment] +``` + +```snapshot +error[PLR6104]: Use `**=` to perform an augmented assignment directly + --> src/mdtest_snippet.py:1:1 + | +1 | to_multiply = to_multiply**-1 # snapshot: non-augmented-assignment + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: Replace with augmented assignment + | + - to_multiply = to_multiply**-1 # snapshot: non-augmented-assignment +1 + to_multiply **= -1 # snapshot: non-augmented-assignment +2 | to_multiply = to_multiply - -1 # error: [non-augmented-assignment] + | +note: This is an unsafe fix and may change runtime behavior +``` + +## Unary operators on non-literals + +The unary operand is not a literal, so its type is unknown and the operator may not commute. + +```py +to_multiply = -a_number + to_multiply +to_multiply = -to_multiply + 1 +``` + +`not` evaluates to a boolean whatever it is applied to, so rewriting the case below would in fact be +safe. The check deliberately stays narrow and only looks for number and boolean literals underneath +the unary operators. + +```py +to_multiply = (not "") + to_multiply +``` + +## Non-commutative operators + +`-` does not commute, regardless of the operand's type. + +```py +to_multiply = -1 - to_multiply +``` diff --git a/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs b/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs index ad7d85f406..06b6aa5b85 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/non_augmented_assignment.rs @@ -123,10 +123,9 @@ pub(crate) fn non_augmented_assignment(checker: &Checker, assign: &ast::StmtAssi return; } - // If the operator is commutative, match, e.g., `x = 1 + x`, but limit such matches to primitive - // types. + // If the operator is commutative, match, e.g., `x = 1 + x`. if operator.is_commutative() - && (value.left.is_number_literal_expr() || value.left.is_boolean_literal_expr()) + && is_number_or_bool_constant(&value.left) && ComparableExpr::from(target) == ComparableExpr::from(&value.right) { let mut diagnostic = @@ -142,6 +141,16 @@ pub(crate) fn non_augmented_assignment(checker: &Checker, assign: &ast::StmtAssi } } +/// Returns `true` if `expr` evaluates to a number or a boolean, looking through +/// any unary operators applied to a number or boolean literal. +fn is_number_or_bool_constant(mut expr: &Expr) -> bool { + while let Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) = expr { + expr = operand; + } + + expr.is_number_literal_expr() || expr.is_boolean_literal_expr() +} + /// Generate a fix to convert an assignment statement to an augmented assignment. /// /// For example, given `x = x + 1`, the fix would be `x += 1`. From ba581e111aef8d46308406aa5b708f2ec8b23cef Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sat, 8 Aug 2026 16:33:02 +0200 Subject: [PATCH 333/390] [ty] Avoid composite Salsa keys for unspecialized MROs (#27592) --- .../src/types/class/static_literal.rs | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index e36a3935af..f26e8eb29b 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -791,24 +791,51 @@ impl<'db> StaticClassLiteral<'db> { /// attribute on a class at runtime. /// /// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order + pub(in crate::types) fn try_mro( + self, + db: &'db dyn Db, + specialization: Option>, + ) -> Result<&'db Mro<'db>, &'db StaticMroError<'db>> { + match specialization { + None => self.try_mro_unspecialized(db), + Some(specialization) => self.try_mro_specialized(db, specialization), + } + } + + #[salsa::tracked( + returns(as_ref), + cycle_initial=|db, _, self_: StaticClassLiteral<'db>| { + let env = ProgramEnvironment::from_scope(self_.body_scope(db)); + Err(StaticMroError::cycle( + db, &env, + self_.apply_optional_specialization(db, None), + )) + }, + heap_size=ruff_memory_usage::heap_size + )] + fn try_mro_unspecialized(self, db: &'db dyn Db) -> Result, StaticMroError<'db>> { + tracing::trace!("StaticClassLiteral::try_mro: {}", self.name(db)); + Mro::of_static_class(db, self, None) + } + #[salsa::tracked( returns(as_ref), cycle_initial=|db, _, self_: StaticClassLiteral<'db>, specialization| { let env = ProgramEnvironment::from_scope(self_.body_scope(db)); Err(StaticMroError::cycle( db, &env, - self_.apply_optional_specialization(db, specialization), + self_.apply_optional_specialization(db, Some(specialization)), )) }, heap_size=ruff_memory_usage::heap_size )] - pub(in crate::types) fn try_mro( + fn try_mro_specialized( self, db: &'db dyn Db, - specialization: Option>, + specialization: Specialization<'db>, ) -> Result, StaticMroError<'db>> { tracing::trace!("StaticClassLiteral::try_mro: {}", self.name(db)); - Mro::of_static_class(db, self, specialization) + Mro::of_static_class(db, self, Some(specialization)) } /// Iterate over the [method resolution order] ("MRO") of the class. From 463826d2a426214910853f187c248a87e7f45b7a Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 8 Aug 2026 17:52:59 +0100 Subject: [PATCH 334/390] [ty] Update flaky primer projects (#27597) --- crates/ty_python_semantic/resources/primer/flaky.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/primer/flaky.txt b/crates/ty_python_semantic/resources/primer/flaky.txt index 87506ad1d4..14f3ceb0d1 100644 --- a/crates/ty_python_semantic/resources/primer/flaky.txt +++ b/crates/ty_python_semantic/resources/primer/flaky.txt @@ -1,4 +1,2 @@ -Expression -scikit-build-core -dd-trace-py +meson steam.py From 3115cba9a4bd6dd7bc3e065555ecf55a4644f452 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 8 Aug 2026 13:59:30 -0400 Subject: [PATCH 335/390] [ty] Preserve contextual inference for declarations in loops (#27594) ## Summary Previously, a declaration inside a loop could be validated against a prior iteration's assignment before that assignment received the declaration's contextual type: ```python from typing import TypedDict class Record(TypedDict): value: int while True: record: Record record = {"value": 1} ``` The dictionary was inferred as `dict[str, int]`, so we incorrectly reported `invalid-declaration` and discarded the `TypedDict` context. We now initialize declaration-only inference cycles with the declared type when a synthetic loop-header binding is visible. Ordinary assignments already initialize their cycles directly in `DefinitionInference::cycle_initial`; for collection literals, this can seed types such as `list[Divergent]`. Annotated declarations require a separate builder path because resolving the annotation must respect postponed evaluation, Python 3.14 forward references, qualifiers, diagnostics, and metadata. `infer_annotated_assignment_cycle_initial` therefore infers only the annotation and preserves its builder state without performing full region inference. This applies to both `while` and `for` loops and preserves genuine declaration errors for incompatible pre-loop bindings. Closes https://github.com/astral-sh/ty/issues/4206. --- .../resources/mdtest/bidirectional.md | 111 ++++++++++++++++++ .../resources/mdtest/declaration/error.md | 12 ++ crates/ty_python_semantic/src/types/infer.rs | 33 ++++++ .../src/types/infer/builder.rs | 42 +++++-- 4 files changed, 191 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/bidirectional.md b/crates/ty_python_semantic/resources/mdtest/bidirectional.md index c03a3df57b..43e30e06d5 100644 --- a/crates/ty_python_semantic/resources/mdtest/bidirectional.md +++ b/crates/ty_python_semantic/resources/mdtest/bidirectional.md @@ -67,6 +67,117 @@ def f() -> list[Literal[1]]: return [1] ``` +## Loop-carried assignment context + +A declaration inside a loop provides context to assignments that reach it from an earlier iteration. + +### While loops + +A declaration inside a `while` loop applies to list literals assigned in each iteration. + +```py +while True: + values: list[object] + values = [1] + reveal_type(values) # revealed: list[object] +``` + +### For loops + +The same declaration context applies to assignments in a `for` loop. + +```py +for _ in range(2): + values: list[object] + values = [1] + reveal_type(values) # revealed: list[object] +``` + +### Nested dictionary values + +A declaration inside a loop also provides context for values nested within a dictionary literal. + +```py +from typing import TypedDict + +class Record(TypedDict): + values: list[float] + +while True: + record: Record + record = {"values": [1]} + reveal_type(record) # revealed: Record +``` + +### Invalid dictionary values + +An incompatible dictionary item is reported at the assignment, not at the declaration. + +```py +from typing import TypedDict + +class Record(TypedDict): + value: int + +while True: + record: Record + record = {"value": "invalid"} # error: [invalid-argument-type] + reveal_type(record) # revealed: Record +``` + +### Stringified annotations + +String annotations provide their resolved type when a loop-carried assignment needs context. + +```py +while True: + values: "list[object]" + values = [1] + reveal_type(values) # revealed: list[object] +``` + +### Deferred forward references + +Deferred annotations resolve a `TypedDict` defined after the loop before inferring its dictionary +assignments. + +```py +from __future__ import annotations +from typing import TypedDict + +for _ in range(2): + record: Record + record = {"value": 1} + reveal_type(record) # revealed: Record + + invalid: Record + invalid = {"value": "invalid"} # error: [invalid-argument-type] + +class Record(TypedDict): + value: int +``` + +### Deferred forward references on Python 3.14 + +Annotations are deferred by default in Python 3.14 and later. + +```toml +[environment] +python-version = "3.14" +``` + +```py +from typing import TypedDict + +for _ in range(2): + record: Record + record = {"value": 1} + reveal_type(record) # revealed: Record + +class Record(TypedDict): + value: int +``` + ## Collection literals ### Basic diff --git a/crates/ty_python_semantic/resources/mdtest/declaration/error.md b/crates/ty_python_semantic/resources/mdtest/declaration/error.md index 6633ba562d..5a228f12e5 100644 --- a/crates/ty_python_semantic/resources/mdtest/declaration/error.md +++ b/crates/ty_python_semantic/resources/mdtest/declaration/error.md @@ -7,6 +7,18 @@ x = 1 x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred type `Literal[1]`" ``` +## Declarations in loops reject incompatible earlier bindings + +An incompatible binding that predates the loop must still invalidate a declaration inside it. + +```py +values = [1] + +while True: + values: list[object] # error: [invalid-declaration] + values = [1] +``` + ## Incompatible declarations ```py diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 45c6f48532..d50f3cb5e7 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -1397,6 +1397,39 @@ impl<'db> DefinitionInference<'db> { DefinitionTypes::Binding(Type::instance(db, &env, divergent_collection)); } } + } else if let DefinitionKind::AnnotatedAssignment(assignment) = definition.kind(db) { + let program_file = definition.program_file(db); + let python_file = program_file.python_file(db); + let module = parsed_module(db, python_file).load(db); + let index = semantic_index(db, program_file); + + if assignment.value(&module).is_none() + && index + .use_def_map(definition.file_scope(db)) + .bindings_at_definition(definition) + .any(|binding| { + binding + .binding + .is_defined_and(|binding| binding.kind(db).is_loop_header()) + }) + { + // Loop-carried assignments need this annotation as context before validating + // the declaration can infer their binding types. + return TypeInferenceBuilder::new( + db, + &env, + InferenceRegion::Definition(definition), + python_file.file(db), + program_file, + index, + &module, + ) + .infer_annotated_assignment_cycle_initial( + definition, + assignment, + cycle_recovery, + ); + } } Self { diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index d0f2bd0184..43adc5b4a8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -4218,6 +4218,37 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + /// Infer an annotated assignment's annotation using the file's deferred-annotation semantics. + fn infer_annotated_assignment_annotation( + &mut self, + assignment: &AnnotatedAssignmentDefinitionKind, + ) -> TypeAndQualifiers<'db> { + let annotation = assignment.annotation(self.module()); + + // Pydantic supports field specifiers in annotations via `Annotated[T, Field(...)]`. + self.setup_dataclass_field_specifiers(); + let declared = self.infer_annotation_expression_allow_pep_613( + annotation, + DeferredExpressionState::from(self.defer_annotations()), + ); + self.dataclass_field_specifiers.clear(); + + declared + } + + /// Initialize a declaration cycle without discarding its annotation diagnostics or metadata. + pub(super) fn infer_annotated_assignment_cycle_initial( + mut self, + definition: Definition<'db>, + assignment: &AnnotatedAssignmentDefinitionKind, + cycle_recovery: Type<'db>, + ) -> DefinitionInference<'db> { + let declared = self.infer_annotated_assignment_annotation(assignment); + self.declarations.insert(definition, declared); + self.cycle_recovery = Some(cycle_recovery); + self.finish_inferred_definition(definition) + } + /// Infer the types in an annotated assignment definition. fn infer_annotated_assignment_definition( &mut self, @@ -4265,13 +4296,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let annotation = assignment.annotation(self.module()); - // Pydantic supports field specifiers in annotations via `Annotated[T, Field(...)]`. - self.setup_dataclass_field_specifiers(); - let mut declared = self.infer_annotation_expression_allow_pep_613( - annotation, - DeferredExpressionState::from(self.defer_annotations()), - ); - self.dataclass_field_specifiers.clear(); + let mut declared = self.infer_annotated_assignment_annotation(assignment); // P.args and P.kwargs are only valid as annotations on *args and **kwargs, // not as variable annotations. Check both resolved type and AST form. @@ -11322,7 +11347,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definition: Definition<'db>, ) -> DefinitionInference<'db> { self.infer_region(); + self.finish_inferred_definition(definition) + } + fn finish_inferred_definition(self, definition: Definition<'db>) -> DefinitionInference<'db> { let Self { context, expressions, From 24baf2cd8fd7a191625e7029d91a45a56dda9b85 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sat, 8 Aug 2026 15:13:34 -0400 Subject: [PATCH 336/390] [ty] Remove unnecessary constraint set display simplification logic (#27596) We had a lot of logic in our constraint set implementation that tried to simplify a BDD's boolean formula when displaying it. This is a lot of code for arguable benefit: the `display` method is only used for debug messages, and arguably it's better to see a rendering of the actual BDD structure, rather than a simplification of it. So this PR removes it! Removing it is also better for maintainability, since all changes to the constraint set implementation have to consider how it affects this vistigial code. --- .../src/types/constraints.rs | 902 +----------------- 1 file changed, 24 insertions(+), 878 deletions(-) diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index c12ef995a7..54faf59054 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -903,10 +903,8 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { impl Display for DisplayConstraintSet<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let db = self.db; - let mut storage = self.builder.storage.borrow_mut(); - let node = self.node.simplify_for_display(db, self.env, &mut storage); - Display::fmt(&node.display(db, self.env, &mut storage), f) + let storage = self.builder.storage.borrow(); + Display::fmt(&self.node.display(self.db, self.env, &storage), f) } } @@ -939,10 +937,13 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { impl Display for DisplayConstraintSet<'_, '_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let db = self.db; - let mut storage = self.builder.storage.borrow_mut(); - let node = self.node.simplify_for_display(db, self.env, &mut storage); - Display::fmt(&node.display_graph(db, self.env, &storage, self.prefix), f) + let storage = self.builder.storage.borrow(); + Display::fmt( + &self + .node + .display_graph(self.db, self.env, &storage, self.prefix), + f, + ) } } @@ -1052,8 +1053,6 @@ struct ConstraintSetStorage<'db> { /// Existential abstraction derives new constraints in source order and returns their /// source-order sidecar, so distinct orderings of the same BDD must not share a cache entry. exists_cache: FxHashMap, (NodeId, Option)>, - restrict_one_cache: FxHashMap<(NodeId, ConstraintAssignment), (NodeId, bool)>, - simplify_cache: FxHashMap, single_sequent_cache: FxHashMap, pair_sequent_cache: FxHashMap<(ConstraintId, ConstraintId), SequentMap>, @@ -1822,12 +1821,6 @@ enum IntersectionResult<'db> { Disjoint, } -impl IntersectionResult<'_> { - fn is_disjoint(self) -> bool { - matches!(self, IntersectionResult::Disjoint) - } -} - /// The index of a bound typevar within a [`ConstraintSetStorage`]. #[newtype_index] #[derive(Ord, PartialOrd, get_size2::GetSize)] @@ -2464,8 +2457,7 @@ impl ConstraintId { /// Returns whether this constraint implies another — i.e., whether every type that /// satisfies this constraint also satisfies `other`. /// - /// This is used to simplify how we display constraint sets, by removing redundant constraints - /// from a clause. + /// This is used to avoid adding redundant implications to a sequent map. fn implies<'db>( self, db: &'db dyn Db, @@ -3285,159 +3277,6 @@ impl NodeId { } } - /// Returns a new BDD that returns the same results as `self`, but with some inputs fixed to - /// particular values. (Those variables will not be checked when evaluating the result, and - /// will not be present in the result.) - /// - /// Also returns whether _all_ of the restricted variables appeared in the BDD. - fn restrict<'db>( - self, - db: &'db dyn Db, - storage: &mut ConstraintSetStorage<'db>, - assignment: impl IntoIterator, - ) -> (Self, bool) { - assignment - .into_iter() - .fold((self, true), |(restricted, found), assignment| { - let (restricted, found_this) = restricted.restrict_one(db, storage, assignment); - (restricted, found && found_this) - }) - } - - /// Returns a new BDD that returns the same results as `self`, but with one input fixed to a - /// particular value. (That variable will be not be checked when evaluating the result, and - /// will not be present in the result.) - /// - /// Also returns whether the restricted variable appeared in the BDD. - fn restrict_one<'db>( - self, - db: &'db dyn Db, - storage: &mut ConstraintSetStorage<'db>, - assignment: ConstraintAssignment, - ) -> (Self, bool) { - match self.node() { - Node::AlwaysTrue | Node::AlwaysFalse => (self, false), - Node::Interior(interior) => interior.restrict_one(db, storage, assignment), - } - } - - /// Returns a new BDD with any occurrence of `left ∧ right` replaced with `replacement`. - fn substitute_intersection<'db>( - self, - db: &'db dyn Db, - storage: &mut ConstraintSetStorage<'db>, - left: ConstraintAssignment, - right: ConstraintAssignment, - replacement: NodeId, - ) -> Self { - // We perform a Shannon expansion to find out what the input BDD evaluates to when: - // - left and right are both true - // - left is false - // - left is true and right is false - // This covers the entire truth table of `left ∧ right`. - let (when_left_and_right, both_found) = self.restrict(db, storage, [left, right]); - if !both_found { - // If left and right are not both present in the input BDD, we should not even attempt - // the substitution, since the Shannon expansion might introduce the missing variables! - // That confuses us below when we try to detect whether the substitution is consistent - // with the input. - return self; - } - let (when_not_left, _) = self.restrict(db, storage, [left.negated()]); - let (when_left_but_not_right, _) = self.restrict(db, storage, [left, right.negated()]); - - // The result should test `replacement`, and when it's true, it should produce the same - // output that input would when `left ∧ right` is true. When replacement is false, it - // should fall back on testing left and right individually to make sure we produce the - // correct outputs in the `¬(left ∧ right)` case. So the result is - // - // if replacement - // when_left_and_right - // else if not left - // when_not_left - // else if not right - // when_left_but_not_right - // else - // false - // - // (Note that the `else` branch shouldn't be reachable, but we have to provide something!) - let (left_node, _) = Node::new_satisfied_constraint(storage, left); - let (right_node, _) = Node::new_satisfied_constraint(storage, right); - let right_result = right_node.ite(storage, ALWAYS_FALSE, when_left_but_not_right); - let left_result = left_node.ite(storage, right_result, when_not_left); - let result = replacement.ite(storage, when_left_and_right, left_result); - - // Lastly, verify that the result is consistent with the input. (It must produce the same - // results when `left ∧ right`.) If it doesn't, the substitution isn't valid, and we should - // return the original BDD unmodified. - let intersection = left_node.and(storage, right_node); - let validity = replacement.iff(storage, intersection); - let constrained_original = self.and(storage, validity); - let constrained_replacement = result.and(storage, validity); - if constrained_original == constrained_replacement { - result - } else { - self - } - } - - /// Returns a new BDD with any occurrence of `left ∨ right` replaced with `replacement`. - fn substitute_union<'db>( - self, - db: &'db dyn Db, - storage: &mut ConstraintSetStorage<'db>, - left: ConstraintAssignment, - right: ConstraintAssignment, - replacement: NodeId, - ) -> Self { - // We perform a Shannon expansion to find out what the input BDD evaluates to when: - // - left and right are both true - // - left is true and right is false - // - left is false and right is true - // - left and right are both false - // This covers the entire truth table of `left ∨ right`. - let (when_l1_r1, both_found) = self.restrict(db, storage, [left, right]); - if !both_found { - // If left and right are not both present in the input BDD, we should not even attempt - // the substitution, since the Shannon expansion might introduce the missing variables! - // That confuses us below when we try to detect whether the substitution is consistent - // with the input. - return self; - } - let (when_l0_r0, _) = self.restrict(db, storage, [left.negated(), right.negated()]); - let (when_l1_r0, _) = self.restrict(db, storage, [left, right.negated()]); - let (when_l0_r1, _) = self.restrict(db, storage, [left.negated(), right]); - - // The result should test `replacement`, and when it's true, it should produce the same - // output that input would when `left ∨ right` is true. For OR, this is the union of what - // the input produces for the three cases that comprise `left ∨ right`. When `replacement` - // is false, the result should produce the same output that input would when - // `¬(left ∨ right)`, i.e. when `left ∧ right`. So the result is - // - // if replacement - // or(when_l1_r1, when_l1_r0, when_r0_l1) - // else - // when_l0_r0 - let when_l0_r1_or_l1_r1 = when_l0_r1.or(storage, when_l1_r1); - let when_either = when_l1_r0.or(storage, when_l0_r1_or_l1_r1); - let result = replacement.ite(storage, when_either, when_l0_r0); - - // Lastly, verify that the result is consistent with the input. (It must produce the same - // results when `left ∨ right`.) If it doesn't, the substitution isn't valid, and we should - // return the original BDD unmodified. - let (left_node, _) = Node::new_satisfied_constraint(storage, left); - let (right_node, _) = Node::new_satisfied_constraint(storage, right); - let union = left_node.or(storage, right_node); - let validity = replacement.iff(storage, union); - let constrained_original = self.and(storage, validity); - let constrained_replacement = result.and(storage, validity); - if constrained_original == constrained_replacement { - result - } else { - self - } - } - /// Invokes a closure for each unique BDD node that appears anywhere in a BDD. /// /// This treats the BDD as a DAG and does not revisit shared subgraphs. Use this when the @@ -3491,39 +3330,6 @@ impl NodeId { walk(self, storage, &mut FxHashSet::default(), f); } - /// Simplifies a BDD, replacing constraints with simpler or smaller constraints where possible. - /// - /// TODO: [Historical note] This is now used only for display purposes, but previously was also - /// used to ensure that we added the "transitive closure" to each BDD. The constraints in a BDD - /// are not independent; some combinations of constraints can imply other constraints. This - /// affects us in two ways: First, it means that certain combinations are impossible. (If - /// `a → b` then `a ∧ ¬b` can never happen.) Second, it means that certain constraints can be - /// inferred even if they do not explicitly appear in the BDD. It is important to take this - /// into account in several BDD operations (satisfiability, existential quantification, etc). - /// Before, we used this method to _add_ the transitive closure to a BDD, in an attempt to make - /// sure that it holds "all the facts" that would be needed to satisfy any query we might make. - /// We also used this method to calculate the "domain" of the BDD to help rule out invalid - /// inputs. However, this was at odds with using this method for display purposes, where our - /// goal is to _remove_ redundant information, so as to not clutter up the display. To resolve - /// this dilemma, all of the correctness uses have been refactored to use [`SequentMap`] - /// instead. It tracks the same information in a more efficient and lazy way, and never tries - /// to remove redundant information. For expediency, however, we did not make any changes to - /// this method, other than to stop tracking the domain (which was never used for display - /// purposes). That means we have some tech debt here, since there is a lot of duplicate logic - /// between `simplify_for_display` and `SequentMap`. It would be nice to update our display - /// logic to use the sequent map as much as possible. But that can happen later. - fn simplify_for_display<'db>( - self, - db: &'db dyn Db, - env: &ProgramEnvironment<'db>, - storage: &mut ConstraintSetStorage<'db>, - ) -> Self { - match self.node() { - Node::AlwaysTrue | Node::AlwaysFalse => self, - Node::Interior(interior) => interior.simplify(db, env, storage), - } - } - /// Returns clauses describing all of the variable assignments that cause this BDD to evaluate /// to `true`. (This translates the boolean function that this BDD represents into DNF form.) fn satisfied_clauses(self, storage: &ConstraintSetStorage<'_>) -> SatisfiedClauses { @@ -3566,32 +3372,31 @@ impl NodeId { self, db: &'db dyn Db, env: &'a ProgramEnvironment<'db>, - storage: &'a mut ConstraintSetStorage<'db>, + storage: &'a ConstraintSetStorage<'db>, ) -> impl Display + 'a { - // To render a BDD in DNF form, you perform a depth-first search of the BDD tree, looking - // for any path that leads to the AlwaysTrue terminal. Each such path represents one of the - // intersection clauses in the DNF form. The path traverses zero or more interior nodes, - // and takes either the true or false edge from each one. That gives you the positive or - // negative individual constraints in the path's clause. + // Render the BDD directly as an unsimplified DNF formula. Each root-to-true path becomes + // one clause, with true, uncertain, and false edges contributing positive, unconstrained, + // and negative assignments respectively. struct DisplayNode<'db, 'c> { node: NodeId, db: &'db dyn Db, env: &'c ProgramEnvironment<'db>, - storage: RefCell<&'c mut ConstraintSetStorage<'db>>, + storage: &'c ConstraintSetStorage<'db>, } impl Display for DisplayNode<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let db = self.db; match self.node.node() { Node::AlwaysTrue => f.write_str("always"), Node::AlwaysFalse => f.write_str("never"), - Node::Interior(_) => { - let mut storage = self.storage.borrow_mut(); - let mut clauses = self.node.satisfied_clauses(&storage); - clauses.simplify(db, self.env, &mut storage); - Display::fmt(&clauses.display(db, self.env, &storage), f) - } + Node::Interior(_) => Display::fmt( + &self.node.satisfied_clauses(self.storage).display( + self.db, + self.env, + self.storage, + ), + f, + ), } } } @@ -3600,7 +3405,7 @@ impl NodeId { node: self, db, env, - storage: RefCell::new(storage), + storage, } } @@ -4839,78 +4644,6 @@ impl InteriorNode { result } - fn restrict_one<'db>( - self, - db: &'db dyn Db, - storage: &mut ConstraintSetStorage<'db>, - assignment: ConstraintAssignment, - ) -> (NodeId, bool) { - let key = (self.node(), assignment); - if let Some(result) = storage.restrict_one_cache.get(&key) { - return *result; - } - - let self_interior = storage.interior_node_data(self.node()); - let self_ordering = self_interior.constraint.ordering(); - let result = if assignment.constraint().ordering() < self_ordering { - // If this node's variable is larger than the assignment's variable, then we have reached a - // point in the BDD where the assignment can no longer affect the result, - // and we can return early. - (self.node(), false) - } else { - // Otherwise, check if this node's variable is in the assignment. If so, substitute the - // variable by replacing this node with the appropriate edge(s). When restricting a - // TDD, the uncertain branch is folded in. - if assignment == self_interior.constraint.when_true() { - // restrict(n? C: U: D, n == true) = C ∨ U - ( - self_interior - .if_true - .or(storage, self_interior.if_uncertain), - true, - ) - } else if assignment == self_interior.constraint.when_false() { - // restrict(n? C: U: D, n == false) = D ∨ U - ( - self_interior - .if_false - .or(storage, self_interior.if_uncertain), - true, - ) - } else if assignment == self_interior.constraint.when_unconstrained() { - // restrict(n? C: U: D, n is unconstrained) = C ∨ U ∨ D - ( - self_interior - .if_true - .or(storage, self_interior.if_uncertain) - .or(storage, self_interior.if_false), - true, - ) - } else { - let (if_true, found_in_true) = - self_interior.if_true.restrict_one(db, storage, assignment); - let (if_uncertain, found_in_uncertain) = self_interior - .if_uncertain - .restrict_one(db, storage, assignment); - let (if_false, found_in_false) = - self_interior.if_false.restrict_one(db, storage, assignment); - ( - NodeId::with_uncertain( - storage, - self_interior.constraint, - if_true, - if_uncertain, - if_false, - ), - found_in_true || found_in_uncertain || found_in_false, - ) - } - }; - - storage.restrict_one_cache.insert(key, result); - result - } - fn path_assignments( self, storage: &mut ConstraintSetStorage<'_>, @@ -4934,373 +4667,6 @@ impl InteriorNode { }); PathAssignments::new(constraints) } - - /// Returns a simplified version of a BDD. - /// - /// This is calculated by looking at the relationships that exist between the constraints that - /// are mentioned in the BDD. For instance, if one constraint implies another (`x → y`), then - /// `x ∧ ¬y` is not a valid input, and we can rewrite any occurrences of `x ∨ y` into `y`. - fn simplify<'db>( - self, - db: &'db dyn Db, - env: &ProgramEnvironment<'db>, - storage: &mut ConstraintSetStorage<'db>, - ) -> NodeId { - let key = self.node(); - if let Some(result) = storage.simplify_cache.get(&key) { - return *result; - } - - // To simplify a non-terminal BDD, we find all pairs of constraints that are mentioned in - // the BDD. If any of those pairs can be simplified to some other BDD, we perform a - // substitution to replace the pair with the simplification. - // - // Some of the simplifications create _new_ constraints that weren't originally present in - // the BDD. If we encounter one of those cases, we need to check if we can simplify things - // further relative to that new constraint. - // - // To handle this, we keep track of the individual constraints that we have already - // discovered (`seen_constraints`), and a queue of constraint pairs that we still need to - // check (`to_visit`). - - // Seed the seen set with all of the constraints that are present in the input BDD, and the - // visit queue with all pairs of those constraints. (We use "combinations" because we don't - // need to compare a constraint against itself, and because ordering doesn't matter.) - let mut seen_constraints = FxHashSet::default(); - self.node() - .for_each_unique_constraint(storage, &mut |constraint| { - seen_constraints.insert(constraint); - }); - let mut to_visit: Vec<(_, _)> = (seen_constraints.iter().copied()) - .array_combinations() - .map(|[left, right]| (left, right)) - .collect(); - - // Repeatedly pop constraint pairs off of the visit queue, checking whether each pair can - // be simplified. - let mut simplified = self.node(); - while let Some((left_constraint, right_constraint)) = to_visit.pop() { - // If the constraints refer to different typevars, the only simplifications we can make - // are of the form `S ≤ T ∧ T ≤ int → S ≤ int`. - let left_constraint_data = storage.constraint_data(left_constraint); - let left_typevar = left_constraint_data.typevar; - let right_constraint_data = storage.constraint_data(right_constraint); - let right_typevar = right_constraint_data.typevar; - if !left_typevar.is_same_typevar_as(db, right_typevar) { - // We've structured our constraints so that a typevar's upper/lower bound can only - // be another typevar if the bound is "later" in our arbitrary ordering. That means - // we only have to check this pair of constraints in one direction — though we do - // have to figure out which of the two typevars is constrained, and which one is - // the upper/lower bound. - let (bound_constraint, constrained_constraint) = - if left_typevar.can_be_bound_for(db, storage, right_typevar) { - (left_constraint, right_constraint) - } else { - (right_constraint, left_constraint) - }; - let bound_constraint_data = storage.constraint_data(bound_constraint); - let bound_typevar = bound_constraint_data.typevar; - let constrained_constraint_data = storage.constraint_data(constrained_constraint); - let constrained_typevar = constrained_constraint_data.typevar; - - // We then look for cases where the "constrained" typevar's upper and/or lower - // bound matches the "bound" typevar. If so, we're going to add an implication to - // the constraint set that replaces the upper/lower bound that matched with the - // bound constraint's corresponding bound. - let (new_lower, new_upper) = match ( - constrained_constraint_data.bounds.lower, - constrained_constraint_data.bounds.upper, - ) { - // (B ≤ C ≤ B) ∧ (BL ≤ B ≤ BU) → (BL ≤ C ≤ BU) - ( - Some(Type::TypeVar(constrained_lower)), - Some(Type::TypeVar(constrained_upper)), - ) if constrained_lower.is_same_typevar_as(db, bound_typevar) - && constrained_upper.is_same_typevar_as(db, bound_typevar) => - { - ( - bound_constraint_data.bounds.lower, - bound_constraint_data.bounds.upper, - ) - } - - // (CL ≤ C ≤ B) ∧ (BL ≤ B ≤ BU) → (CL ≤ C ≤ BU) - (constrained_lower, Some(Type::TypeVar(constrained_upper))) - if constrained_upper.is_same_typevar_as(db, bound_typevar) => - { - (constrained_lower, bound_constraint_data.bounds.upper) - } - - // (B ≤ C ≤ CU) ∧ (BL ≤ B ≤ BU) → (BL ≤ C ≤ CU) - (Some(Type::TypeVar(constrained_lower)), constrained_upper) - if constrained_lower.is_same_typevar_as(db, bound_typevar) => - { - (bound_constraint_data.bounds.lower, constrained_upper) - } - - _ => continue, - }; - - let new_constraint = ConstraintId::new_with_bounds( - db, - env, - storage, - constrained_typevar, - new_lower, - new_upper, - ); - if seen_constraints.contains(&new_constraint) { - continue; - } - let (new_node, _) = Node::new_constraint(storage, new_constraint); - let (positive_left_node, _) = - Node::new_satisfied_constraint(storage, left_constraint.when_true()); - let (positive_right_node, _) = - Node::new_satisfied_constraint(storage, right_constraint.when_true()); - let lhs = positive_left_node.and(storage, positive_right_node); - let intersection = new_node.ite(storage, lhs, ALWAYS_FALSE); - simplified = simplified.and(storage, intersection); - continue; - } - - // From here on out we know that both constraints constrain the same typevar. The - // clause above will propagate all that we know about the current typevar relative to - // other typevars, producing constraints on this typevar that have concrete lower/upper - // bounds. That means we can skip the simplifications below if any bound is another - // typevar. - if left_constraint_data - .bounds - .lower - .is_some_and(Type::is_type_var) - || left_constraint_data - .bounds - .upper - .is_some_and(Type::is_type_var) - || right_constraint_data - .bounds - .lower - .is_some_and(Type::is_type_var) - || right_constraint_data - .bounds - .upper - .is_some_and(Type::is_type_var) - { - continue; - } - - // Containment: The range of one constraint might completely contain the range of the - // other. If so, there are several potential simplifications. - let larger_smaller = if left_constraint.implies(db, env, storage, right_constraint) { - Some((right_constraint, left_constraint)) - } else if right_constraint.implies(db, env, storage, left_constraint) { - Some((left_constraint, right_constraint)) - } else { - None - }; - if let Some((larger_constraint, smaller_constraint)) = larger_smaller { - let (positive_larger_node, _) = - Node::new_satisfied_constraint(storage, larger_constraint.when_true()); - let (negative_larger_node, _) = - Node::new_satisfied_constraint(storage, larger_constraint.when_false()); - - // larger ∨ smaller = larger - simplified = simplified.substitute_union( - db, - storage, - larger_constraint.when_true(), - smaller_constraint.when_true(), - positive_larger_node, - ); - - // ¬larger ∧ ¬smaller = ¬larger - simplified = simplified.substitute_intersection( - db, - storage, - larger_constraint.when_false(), - smaller_constraint.when_false(), - negative_larger_node, - ); - - // smaller ∧ ¬larger = false - // (¬larger removes everything that's present in smaller) - simplified = simplified.substitute_intersection( - db, - storage, - larger_constraint.when_false(), - smaller_constraint.when_true(), - ALWAYS_FALSE, - ); - - // larger ∨ ¬smaller = true - // (larger fills in everything that's missing in ¬smaller) - simplified = simplified.substitute_union( - db, - storage, - larger_constraint.when_true(), - smaller_constraint.when_false(), - ALWAYS_TRUE, - ); - } - - // There are some simplifications we can make when the intersection of the two - // constraints is empty, and others that we can make when the intersection is - // non-empty. - match left_constraint.intersect(db, env, storage, right_constraint) { - IntersectionResult::Simplified(intersection_constraint_data) => { - let intersection_constraint = - storage.intern_constraint(db, env, intersection_constraint_data); - - // If the intersection is non-empty, we need to create a new constraint to - // represent that intersection. We also need to add the new constraint to our - // seen set and (if we haven't already seen it) to the to-visit queue. - if seen_constraints.insert(intersection_constraint) { - to_visit.extend( - (seen_constraints.iter().copied()) - .filter(|seen| *seen != intersection_constraint) - .map(|seen| (seen, intersection_constraint)), - ); - } - let (positive_intersection_node, _) = Node::new_satisfied_constraint( - storage, - intersection_constraint.when_true(), - ); - let (negative_intersection_node, _) = Node::new_satisfied_constraint( - storage, - intersection_constraint.when_false(), - ); - - let (positive_left_node, _) = - Node::new_satisfied_constraint(storage, left_constraint.when_true()); - let (negative_left_node, _) = - Node::new_satisfied_constraint(storage, left_constraint.when_false()); - - let (positive_right_node, _) = - Node::new_satisfied_constraint(storage, right_constraint.when_true()); - let (negative_right_node, _) = - Node::new_satisfied_constraint(storage, right_constraint.when_false()); - - // left ∧ right = intersection - simplified = simplified.substitute_intersection( - db, - storage, - left_constraint.when_true(), - right_constraint.when_true(), - positive_intersection_node, - ); - - // ¬left ∨ ¬right = ¬intersection - simplified = simplified.substitute_union( - db, - storage, - left_constraint.when_false(), - right_constraint.when_false(), - negative_intersection_node, - ); - - // left ∧ ¬right = left ∧ ¬intersection - // (clip the negative constraint to the smallest range that actually removes - // something from positive constraint) - let replacement = positive_left_node.and(storage, negative_intersection_node); - simplified = simplified.substitute_intersection( - db, - storage, - left_constraint.when_true(), - right_constraint.when_false(), - replacement, - ); - - // ¬left ∧ right = ¬intersection ∧ right - // (save as above but reversed) - let replacement = positive_right_node.and(storage, negative_intersection_node); - simplified = simplified.substitute_intersection( - db, - storage, - left_constraint.when_false(), - right_constraint.when_true(), - replacement, - ); - - // left ∨ ¬right = intersection ∨ ¬right - // (clip the positive constraint to the smallest range that actually adds - // something to the negative constraint) - let replacement = negative_right_node.or(storage, positive_intersection_node); - simplified = simplified.substitute_union( - db, - storage, - left_constraint.when_true(), - right_constraint.when_false(), - replacement, - ); - - // ¬left ∨ right = ¬left ∨ intersection - // (save as above but reversed) - let replacement = negative_left_node.or(storage, positive_intersection_node); - simplified = simplified.substitute_union( - db, - storage, - left_constraint.when_false(), - right_constraint.when_true(), - replacement, - ); - } - - // If the intersection doesn't simplify to a single clause, we shouldn't update the - // BDD. - IntersectionResult::CannotSimplify => {} - - IntersectionResult::Disjoint => { - // All of the below hold because we just proved that the intersection of left - // and right is empty. - - let (positive_left_node, _) = - Node::new_satisfied_constraint(storage, left_constraint.when_true()); - let (positive_right_node, _) = - Node::new_satisfied_constraint(storage, right_constraint.when_true()); - - // left ∧ right = false - simplified = simplified.substitute_intersection( - db, - storage, - left_constraint.when_true(), - right_constraint.when_true(), - ALWAYS_FALSE, - ); - - // ¬left ∨ ¬right = true - simplified = simplified.substitute_union( - db, - storage, - left_constraint.when_false(), - right_constraint.when_false(), - ALWAYS_TRUE, - ); - - // left ∧ ¬right = left - // (there is nothing in the hole of ¬right that overlaps with left) - simplified = simplified.substitute_intersection( - db, - storage, - left_constraint.when_true(), - right_constraint.when_false(), - positive_left_node, - ); - - // ¬left ∧ right = right - // (save as above but reversed) - simplified = simplified.substitute_intersection( - db, - storage, - left_constraint.when_false(), - right_constraint.when_true(), - positive_right_node, - ); - } - } - } - - storage.simplify_cache.insert(key, simplified); - simplified - } } /// The result of solving a constraint set for per-typevar specializations. @@ -5352,78 +4718,6 @@ impl ConstraintAssignment { } } - fn negate(&mut self) { - *self = self.negated(); - } - - /// Returns whether this constraint implies another — i.e., whether every type that - /// satisfies this constraint also satisfies `other`. - /// - /// This is used to simplify how we display constraint sets, by removing redundant constraints - /// from a clause. - fn implies<'db>( - self, - db: &'db dyn Db, - env: &ProgramEnvironment<'db>, - storage: &mut ConstraintSetStorage<'db>, - other: Self, - ) -> bool { - match (self, other) { - // For two positive constraints, one range has to fully contain the other; the smaller - // constraint implies the larger. - // - // ....|----other-----|.... - // ......|---self---|...... - ( - ConstraintAssignment::Positive(self_constraint), - ConstraintAssignment::Positive(other_constraint), - ) => self_constraint.implies(db, env, storage, other_constraint), - - // For two negative constraints, one range has to fully contain the other; the ranges - // represent "holes", though, so the constraint with the larger range implies the one - // with the smaller. - // - // |-----|...other...|-----| - // |---|.....self......|---| - ( - ConstraintAssignment::Negative(self_constraint), - ConstraintAssignment::Negative(other_constraint), - ) => other_constraint.implies(db, env, storage, self_constraint), - - // For a positive and negative constraint, the ranges have to be disjoint, and the - // positive range implies the negative range. - // - // |---------------|...self...|---| - // ..|---other---|................| - ( - ConstraintAssignment::Positive(self_constraint), - ConstraintAssignment::Negative(other_constraint), - ) => self_constraint - .intersect(db, env, storage, other_constraint) - .is_disjoint(), - - // It's theoretically possible for a negative constraint to imply a positive constraint - // if the positive constraint is always satisfied (`Never ≤ T ≤ object`). But we never - // create constraints of that form, so with our representation, a negative constraint - // can never imply a positive constraint. - // - // |------other-------| - // |---|...self...|---| - (ConstraintAssignment::Negative(_), ConstraintAssignment::Positive(_)) => false, - - // An `Unconstrained` assignment means "this constraint can go either way." It does - // not imply any positive or negative assignment, and no positive or negative - // assignment implies it. The only trivially true case is Unconstrained => Unconstrained - // for the same constraint. - ( - ConstraintAssignment::Unconstrained(self_constraint), - ConstraintAssignment::Unconstrained(other_constraint), - ) => self_constraint == other_constraint, - (ConstraintAssignment::Unconstrained(_), _) - | (_, ConstraintAssignment::Unconstrained(_)) => false, - } - } - fn display<'db, 'a>( self, db: &'db dyn Db, @@ -7767,70 +7061,6 @@ impl SatisfiedClause { .expect("clause vector should not be empty"); } - /// Invokes a closure with the last constraint in this clause negated. Returns the clause back - /// to its original state after invoking the closure. - fn with_negated_last_constraint(&mut self, f: impl for<'a> FnOnce(&'a Self)) { - if self.constraints.is_empty() { - return; - } - let last_index = self.constraints.len() - 1; - self.constraints[last_index].negate(); - f(self); - self.constraints[last_index].negate(); - } - - /// Removes another clause from this clause, if it appears as a prefix of this clause. Returns - /// whether the prefix was removed. - fn remove_prefix(&mut self, prefix: &SatisfiedClause) -> bool { - if self.constraints.starts_with(&prefix.constraints) { - self.constraints.drain(0..prefix.constraints.len()); - return true; - } - false - } - - /// Simplifies this clause by removing constraints that are implied by other constraints in the - /// clause. (Clauses are the intersection of constraints, so if two clauses are redundant, we - /// want to remove the larger one and keep the smaller one.) - /// - /// Returns a boolean that indicates whether any simplifications were made. - fn simplify<'db>( - &mut self, - db: &'db dyn Db, - env: &ProgramEnvironment<'db>, - storage: &mut ConstraintSetStorage<'db>, - ) -> bool { - let mut changes_made = false; - let mut i = 0; - // Loop through each constraint, comparing it with any constraints that appear later in the - // list. - 'outer: while i < self.constraints.len() { - let mut j = i + 1; - while j < self.constraints.len() { - if self.constraints[j].implies(db, env, storage, self.constraints[i]) { - // If constraint `i` is removed, then we don't need to compare it with any - // later constraints in the list. Note that we continue the outer loop, instead - // of breaking from the inner loop, so that we don't bump index `i` below. - // (We'll have swapped another element into place at that index, and want to - // make sure that we process it.) - self.constraints.swap_remove(i); - changes_made = true; - continue 'outer; - } else if self.constraints[i].implies(db, env, storage, self.constraints[j]) { - // If constraint `j` is removed, then we can continue the inner loop. We will - // swap a new element into place at index `j`, and will continue comparing the - // constraint at index `i` with later constraints. - self.constraints.swap_remove(j); - changes_made = true; - } else { - j += 1; - } - } - i += 1; - } - changes_made - } - fn display<'db>( &self, db: &'db dyn Db, @@ -7880,90 +7110,6 @@ impl SatisfiedClauses { self.clauses.push(clause); } - /// Simplifies the DNF representation, removing redundancies that do not change the underlying - /// function. (This is used when displaying a BDD, to make sure that the representation that we - /// show is as simple as possible while still producing the same results.) - fn simplify<'db>( - &mut self, - db: &'db dyn Db, - env: &ProgramEnvironment<'db>, - storage: &mut ConstraintSetStorage<'db>, - ) { - // First simplify each clause individually, by removing constraints that are implied by - // other constraints in the clause. - for clause in &mut self.clauses { - clause.simplify(db, env, storage); - } - - while self.simplify_one_round() { - // Keep going - } - - // We can remove any clauses that have been simplified to the point where they are empty. - // (Clauses are intersections, so an empty clause is `false`, which does not contribute - // anything to the outer union.) - self.clauses.retain(|clause| !clause.constraints.is_empty()); - } - - fn simplify_one_round(&mut self) -> bool { - let mut changes_made = false; - - // First remove any duplicate clauses. (The clause list will start out with no duplicates - // in the first round of simplification, because of the guarantees provided by the BDD - // structure. But earlier rounds of simplification might have made some clauses redundant.) - // Note that we have to loop through the vector element indexes manually, since we might - // remove elements in each iteration. - let mut i = 0; - while i < self.clauses.len() { - let mut j = i + 1; - while j < self.clauses.len() { - if self.clauses[i] == self.clauses[j] { - self.clauses.swap_remove(j); - changes_made = true; - } else { - j += 1; - } - } - i += 1; - } - if changes_made { - return true; - } - - // Then look for "prefix simplifications". That is, looks for patterns - // - // (A ∧ B) ∨ (A ∧ ¬B ∧ ...) - // - // and replaces them with - // - // (A ∧ B) ∨ (...) - for i in 0..self.clauses.len() { - let (clause, rest) = self.clauses[..=i] - .split_last_mut() - .expect("index should be in range"); - clause.with_negated_last_constraint(|clause| { - for existing in rest { - changes_made |= existing.remove_prefix(clause); - } - }); - - let (clause, rest) = self.clauses[i..] - .split_first_mut() - .expect("index should be in range"); - clause.with_negated_last_constraint(|clause| { - for existing in rest { - changes_made |= existing.remove_prefix(clause); - } - }); - - if changes_made { - return true; - } - } - - false - } - fn display<'db>( &self, db: &'db dyn Db, From 344c279c251940c7d1d3564922c1a7eab1db470c Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sun, 9 Aug 2026 15:31:15 +0200 Subject: [PATCH 337/390] [ty] Resolve uv from PATH before workspace discovery (#27609) --- crates/ty/tests/cli/uv_workspace.rs | 15 ++++++++----- crates/ty_project/src/metadata/uv.rs | 33 ++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/crates/ty/tests/cli/uv_workspace.rs b/crates/ty/tests/cli/uv_workspace.rs index b321e9213e..a0c6b0fcd1 100644 --- a/crates/ty/tests/cli/uv_workspace.rs +++ b/crates/ty/tests/cli/uv_workspace.rs @@ -73,6 +73,10 @@ fn command_with_uv(case: &CliTest, virtual_env: Option<&Path>) -> anyhow::Result .env("UV_PYTHON_DOWNLOADS", "never") .env("TY_OUTPUT_FORMAT", "concise") .env("PATH", std::env::var_os("PATH").unwrap_or_default()); + #[cfg(windows)] + if let Some(path_ext) = std::env::var_os("PATHEXT") { + command.env("PATHEXT", path_ext); + } if let Some(virtual_env) = virtual_env { command.env("VIRTUAL_ENV", virtual_env); } @@ -333,12 +337,12 @@ fn uv_workspace_discovery_is_opt_in() -> anyhow::Result<()> { Ok(()) } -/// Failures to invoke uv are visible by default instead of silently disabling integration. +/// Failures to locate uv are visible by default instead of silently disabling integration. #[test] fn warns_when_uv_workspace_metadata_cannot_be_loaded() -> anyhow::Result<()> { let case = workspace_case()?.with_filter( - "program not found", - "No such file or directory (os error 2)", + "no path to search and provided name is not an absolute path", + "cannot find binary path", ); case.write_file("packages/member/member.py", "value: int = 1")?; @@ -347,7 +351,8 @@ fn warns_when_uv_workspace_metadata_cannot_be_loaded() -> anyhow::Result<()> { .current_dir(case.root().join("packages/member")) .arg(".") .env("TY_UV", "1") - .env("UV", "missing-uv-executable") + .env_remove("UV") + .env("PATH", "") .env("TY_OUTPUT_FORMAT", "concise"); assert_cmd_snapshot!(command, @" @@ -357,7 +362,7 @@ fn warns_when_uv_workspace_metadata_cannot_be_loaded() -> anyhow::Result<()> { All checks passed! ----- stderr ----- - WARN Failed to invoke `uv workspace metadata`: No such file or directory (os error 2) + WARN Failed to invoke `uv workspace metadata`: failed to resolve uv executable: cannot find binary path "); Ok(()) diff --git a/crates/ty_project/src/metadata/uv.rs b/crates/ty_project/src/metadata/uv.rs index 1d46104be7..8c2e0cc123 100644 --- a/crates/ty_project/src/metadata/uv.rs +++ b/crates/ty_project/src/metadata/uv.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use pep440_rs::Version; -use ruff_db::system::{System, SystemPath, SystemPathBuf}; +use ruff_db::system::{System, SystemPath, SystemPathBuf, WhichError}; use ruff_ranged_value::{RangedValue, ValueSource}; use serde::Deserialize; use thiserror::Error; @@ -21,9 +21,14 @@ impl UvWorkspace { path: &SystemPath, system: &dyn System, ) -> Result { - let uv = system - .env_var(EnvVars::UV) - .unwrap_or_else(|_| "uv".to_string()); + let uv = match system.env_var(EnvVars::UV) { + Ok(uv) => uv, + Err(_) => system + .which("uv") + .map(SystemPathBuf::into_string) + .map_err(uv_executable_error) + .map_err(UvWorkspaceError::Invocation)?, + }; // `uv check` has already selected and synchronized the environment. Keep this query // read-only so package selection and `--isolated` aren't overwritten by a second sync. @@ -86,6 +91,13 @@ impl UvWorkspace { } } +fn uv_executable_error(error: WhichError) -> std::io::Error { + std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("failed to resolve uv executable: {error}"), + ) +} + fn resolve_python_version( version: &Version, ) -> Result, UvWorkspaceError> { @@ -166,6 +178,7 @@ struct WorkspacePython { #[cfg(test)] mod tests { use ruff_db::system::{SystemPath, TestSystem}; + use ty_static::EnvVars; use super::{UvWorkspace, UvWorkspaceError}; @@ -179,6 +192,18 @@ mod tests { )); } + #[test] + fn explicit_uv_override_skips_path_lookup() { + let system = TestSystem::default(); + system.set_env_var(EnvVars::UV, "/custom/uv"); + + assert!(matches!( + UvWorkspace::discover(SystemPath::new("/app"), &system), + Err(UvWorkspaceError::Invocation(error)) + if error.kind() == std::io::ErrorKind::Unsupported + )); + } + #[test] fn environment_can_be_omitted() -> anyhow::Result<()> { let system = TestSystem::default(); From 4f312d3a22ce53693dd758e724f15b9146ed6b93 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sun, 9 Aug 2026 15:34:03 +0200 Subject: [PATCH 338/390] [ty] Avoid deadlock when scheduling watch checks (#27605) --- crates/ty/src/lib.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index 9e8f074da6..fd8fe97867 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -285,6 +285,10 @@ struct MainLoop { /// Receiver for the messages sent **to** the main loop. receiver: crossbeam_channel::Receiver, + /// Capacity-one channel used to coalesce pending workspace checks. + check_sender: crossbeam_channel::Sender<()>, + check_receiver: crossbeam_channel::Receiver<()>, + /// The file system watcher, if running in watch mode. watcher: Option, @@ -300,6 +304,7 @@ struct MainLoop { impl MainLoop { fn new(mode: MainLoopMode, printer: Printer) -> (Self, MainLoopCancellationToken) { let (sender, receiver) = crossbeam_channel::bounded(10); + let (check_sender, check_receiver) = crossbeam_channel::bounded(1); let cancellation_token_source = CancellationTokenSource::new(); let cancellation_token = cancellation_token_source.token(); @@ -309,6 +314,8 @@ impl MainLoop { mode, sender: sender.clone(), receiver, + check_sender, + check_receiver, watcher: None, printer, cancellation_token, @@ -332,7 +339,7 @@ impl MainLoop { } fn run(self, db: &mut ProjectDatabase) -> Result { - self.sender.send(MainLoopMessage::CheckWorkspace).unwrap(); + self.request_check(); let result = self.main_loop(db); @@ -341,13 +348,22 @@ impl MainLoop { result } + fn request_check(&self) { + // A pending request already represents a check of the latest database revision. + let _ = self.check_sender.try_send(()); + } + fn main_loop(mut self, db: &mut ProjectDatabase) -> Result { - // Schedule the first check. tracing::debug!("Starting main loop"); let mut revision = 0u64; - while let Ok(message) = self.receiver.recv() { + // Apply all queued changes before starting a pending check because every applied change + // cancels the running check. + while let Ok(message) = crossbeam_channel::select_biased! { + recv(self.receiver) -> message => message, + recv(self.check_receiver) -> request => request.map(|()| MainLoopMessage::CheckWorkspace), + } { match message { MainLoopMessage::CheckWorkspace => { let db = db.clone(); @@ -485,7 +501,7 @@ impl MainLoop { watcher.update(db); } - self.sender.send(MainLoopMessage::CheckWorkspace).unwrap(); + self.request_check(); } MainLoopMessage::Exit => { // Cancel any pending queries and wait for them to complete. From c56aad17fda103e987b0fe8bfd85e5cae2b0751d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 9 Aug 2026 09:41:24 -0400 Subject: [PATCH 339/390] [ty] Reduce Expr size to 64 bytes (#27591) ## Summary This PR reduces the size of `Expr` from 72 to 64 bytes by storing only the start offset on `ExprCall` (64 to 56 bytes) and boxing the uncommon implicitly concatenated string representation in `ExprStringLiteral` (64 to 48 bytes). Both changes are required to reduce the size of the containing `Expr` enum. `ExprCall` derives its end offset from its argument list; parser and error-recovery paths assert that their ranges share the same end. --------- Co-authored-by: Charlie Marsh --- .../src/checkers/ast/analyze/expression.rs | 2 +- crates/ruff_linter/src/checkers/ast/mod.rs | 4 +- .../src/rules/flake8_async/rules/sync_call.rs | 2 +- .../rules/suspicious_function_call.rs | 2 +- .../flake8_bugbear/rules/assert_false.rs | 2 +- .../rules/assert_raises_exception.rs | 14 +++--- .../rules/batched_without_explicit_strict.rs | 3 +- .../rules/function_uses_loop_variable.rs | 2 +- ...cessary_dict_comprehension_for_iterable.rs | 2 +- .../rules/unnecessary_list_call.rs | 2 +- .../rules/unnecessary_map.rs | 3 +- .../rules/call_datetime_fromtimestamp.rs | 3 +- .../rules/call_datetime_now_without_tzinfo.rs | 3 +- .../call_datetime_strptime_without_zone.rs | 3 +- .../rules/call_datetime_without_tzinfo.rs | 3 +- .../log_exception_outside_except_handler.rs | 2 +- .../flake8_logging/rules/root_logger_call.rs | 3 +- .../rules/multiple_starts_ends_with.rs | 6 +-- .../flake8_pytest_style/rules/fixture.rs | 2 +- .../rules/flake8_pytest_style/rules/marks.rs | 2 +- .../rules/unittest_assert.rs | 4 +- .../unnecessary_paren_on_raise_exception.rs | 2 +- .../flake8_simplify/rules/ast_bool_op.rs | 2 +- .../rules/flake8_simplify/rules/ast_expr.rs | 2 +- .../rules/flake8_simplify/rules/ast_ifexp.rs | 2 +- .../flake8_simplify/rules/ast_unary_op.rs | 2 +- .../if_else_block_instead_of_dict_get.rs | 4 +- .../flake8_simplify/rules/key_in_dict.rs | 2 +- .../flake8_simplify/rules/needless_bool.rs | 2 +- .../rules/reimplemented_builtin.rs | 2 +- .../rules/invalid_pathlib_with_suffix.rs | 2 +- crates/ruff_linter/src/rules/flynt/helpers.rs | 2 +- .../rules/manual_list_comprehension.rs | 5 +- .../rules/perflint/rules/manual_list_copy.rs | 6 ++- .../perflint/rules/unnecessary_list_cast.rs | 12 ++--- .../src/rules/pylint/rules/nested_min_max.rs | 4 +- ...convert_named_tuple_functional_to_class.rs | 2 +- .../convert_typed_dict_functional_to_class.rs | 4 +- .../rules/lru_cache_with_maxsize_none.rs | 2 +- .../rules/lru_cache_without_parameters.rs | 2 +- .../rules/pyupgrade/rules/native_literals.rs | 7 +-- .../ruff_linter/src/rules/refurb/helpers.rs | 2 +- .../refurb/rules/check_and_remove_from_set.rs | 2 +- .../refurb/rules/isinstance_type_none.rs | 5 +- .../src/rules/refurb/rules/read_whole_file.rs | 2 +- .../refurb/rules/reimplemented_starmap.rs | 4 +- .../src/rules/refurb/rules/repeated_append.rs | 2 +- .../rules/single_item_membership_test.rs | 2 +- .../rules/slice_to_remove_prefix_or_suffix.rs | 4 +- .../refurb/rules/unnecessary_enumerate.rs | 4 +- .../rules/ruff/rules/in_empty_collection.rs | 2 +- .../ruff/rules/legacy_form_pytest_raises.rs | 4 +- .../ruff/rules/map_int_version_parsing.rs | 2 +- .../ruff/rules/quadratic_list_summation.rs | 5 +- .../src/rules/ruff/rules/starmap_zip.rs | 2 +- .../unnecessary_literal_within_deque_call.rs | 4 +- .../rules/unnecessary_regular_expression.rs | 6 +-- .../src/rules/ruff/rules/unnecessary_round.rs | 2 +- crates/ruff_python_ast/ast.toml | 10 +++- crates/ruff_python_ast/generate.py | 16 ++++-- crates/ruff_python_ast/src/comparable.rs | 2 +- crates/ruff_python_ast/src/generated.rs | 17 +++---- crates/ruff_python_ast/src/helpers.rs | 2 +- crates/ruff_python_ast/src/nodes.rs | 38 +++++++++++--- crates/ruff_python_ast/src/relocate.rs | 9 +++- crates/ruff_python_ast/src/visitor.rs | 2 +- .../src/visitor/transformer.rs | 2 +- crates/ruff_python_codegen/src/generator.rs | 2 +- .../src/expression/expr_call.rs | 2 +- .../src/expression/mod.rs | 2 +- .../src/parser/expression.rs | 3 +- .../ruff_python_parser/src/parser/recovery.rs | 50 ++++++++++--------- .../src/types/infer/builder.rs | 2 +- 73 files changed, 205 insertions(+), 145 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 66b5cf0e2f..1c08d91168 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -539,7 +539,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, }, ) => { diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 029ac85d08..7a466eff55 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -1728,7 +1728,7 @@ impl<'a> Visitor<'a> for Checker<'a> { Expr::Call(ast::ExprCall { func, arguments: _, - range: _, + range_start: _, node_index: _, }) => { if let Expr::Name(ast::ExprName { @@ -1848,7 +1848,7 @@ impl<'a> Visitor<'a> for Checker<'a> { Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, }) => { self.visit_expr(func); diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/sync_call.rs b/crates/ruff_linter/src/rules/flake8_async/rules/sync_call.rs index 48a8f573a2..2887a5560c 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/sync_call.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/sync_call.rs @@ -87,7 +87,7 @@ pub(crate) fn sync_call(checker: &Checker, call: &ExprCall) { return; } - let mut diagnostic = checker.report_diagnostic(TrioSyncCall { method_name }, call.range); + let mut diagnostic = checker.report_diagnostic(TrioSyncCall { method_name }, call.range()); if checker.semantic().in_async_context() { diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( pad( 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 90c743c1da..c4ad14332e 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 @@ -954,7 +954,7 @@ pub(crate) fn suspicious_function_call(checker: &Checker, call: &ExprCall) { checker, call.func.as_ref(), Some(&call.arguments), - call.range, + call.range(), ); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs index 059515b084..7caf3561b3 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs @@ -71,7 +71,7 @@ fn assertion_error(msg: Option<&Expr>) -> Stmt { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }))), cause: None, diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs index 5eab2482ec..a4c0300a46 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_raises_exception.rs @@ -110,7 +110,7 @@ pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) { let Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, }) = &item.context_expr else { @@ -130,15 +130,13 @@ pub(crate) fn assert_raises_exception(checker: &Checker, items: &[WithItem]) { } /// B017 (call form) -pub(crate) fn assert_raises_exception_call( - checker: &Checker, - ast::ExprCall { +pub(crate) fn assert_raises_exception_call(checker: &Checker, call: &ast::ExprCall) { + let ast::ExprCall { func, arguments, - range, + range_start: _, node_index: _, - }: &ast::ExprCall, -) { + } = call; let semantic = checker.semantic(); if arguments.args.len() < 2 && arguments.find_argument("func", 1).is_none() { @@ -146,6 +144,6 @@ pub(crate) fn assert_raises_exception_call( } if let Some(exception) = detect_blind_exception(semantic, func.as_ref(), arguments) { - checker.report_diagnostic(AssertRaisesException { exception }, *range); + checker.report_diagnostic(AssertRaisesException { exception }, call.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs index 4091b13c80..8b7a75b39b 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/batched_without_explicit_strict.rs @@ -1,6 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::ExprCall; use ruff_python_ast::PythonVersion; +use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::rules::flake8_bugbear::helpers::is_infinite_iterable; @@ -94,5 +95,5 @@ pub(crate) fn batched_without_explicit_strict(checker: &Checker, call: &ExprCall return; } - checker.report_diagnostic(BatchedWithoutExplicitStrict, call.range); + checker.report_diagnostic(BatchedWithoutExplicitStrict, call.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs index 406bce0f60..0532007d60 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs @@ -128,7 +128,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, }) => { // Mark immediately-invoked lambdas as safe — the closure diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs index cdd08d5e16..95b80ae827 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_dict_comprehension_for_iterable.rs @@ -224,7 +224,7 @@ fn fix_unnecessary_dict_comprehension(value: &Expr, generator: &Comprehension) - node_index: ruff_python_ast::AtomicNodeIndex::NONE, })), arguments: args, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }) } diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs index 412f34bcea..b03d69333f 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_list_call.rs @@ -50,7 +50,7 @@ pub(crate) fn unnecessary_list_call(checker: &Checker, expr: &Expr, call: &ExprC let ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, } = call; diff --git a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rs b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rs index 5441bde04e..7850d9f60c 100644 --- a/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rs +++ b/crates/ruff_linter/src/rules/flake8_comprehensions/rules/unnecessary_map.rs @@ -6,6 +6,7 @@ use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{self as ast, Expr, ExprContext, Parameters, Stmt}; use ruff_python_ast::{ExprLambda, visitor}; use ruff_python_semantic::SemanticModel; +use ruff_text_size::Ranged; use crate::Fix; use crate::checkers::ast::Checker; @@ -146,7 +147,7 @@ pub(crate) fn unnecessary_map(checker: &Checker, call: &ast::ExprCall) { return; } - let mut diagnostic = checker.report_diagnostic(UnnecessaryMap { object_type }, call.range); + let mut diagnostic = checker.report_diagnostic(UnnecessaryMap { object_type }, call.range()); diagnostic.try_set_fix(|| { fixes::fix_unnecessary_map( call, diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs index 458dd6b90e..484b88fbd1 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_fromtimestamp.rs @@ -2,6 +2,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast}; use ruff_python_semantic::Modules; +use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; @@ -99,5 +100,5 @@ pub(crate) fn call_datetime_fromtimestamp(checker: &Checker, call: &ast::ExprCal None => DatetimeModuleAntipattern::NoTzArgumentPassed, }; - checker.report_diagnostic(CallDatetimeFromtimestamp(antipattern), call.range); + checker.report_diagnostic(CallDatetimeFromtimestamp(antipattern), call.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs index 5827cad81c..be0f86d395 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_now_without_tzinfo.rs @@ -2,6 +2,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::Modules; +use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; @@ -94,5 +95,5 @@ pub(crate) fn call_datetime_now_without_tzinfo(checker: &Checker, call: &ast::Ex None => DatetimeModuleAntipattern::NoTzArgumentPassed, }; - checker.report_diagnostic(CallDatetimeNowWithoutTzinfo(antipattern), call.range); + checker.report_diagnostic(CallDatetimeNowWithoutTzinfo(antipattern), call.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs index d01bfb628f..384e3ea58d 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs @@ -1,6 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr}; use ruff_python_semantic::Modules; +use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; @@ -140,7 +141,7 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &Checker, call: &ast: semantic.current_expression_grandparent(), semantic.current_expression_parent(), ) { - checker.report_diagnostic(CallDatetimeStrptimeWithoutZone(antipattern), call.range); + checker.report_diagnostic(CallDatetimeStrptimeWithoutZone(antipattern), call.range()); } } diff --git a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs index a621ad202b..c937149c5c 100644 --- a/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs +++ b/crates/ruff_linter/src/rules/flake8_datetimez/rules/call_datetime_without_tzinfo.rs @@ -2,6 +2,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast as ast; use ruff_python_semantic::Modules; +use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; @@ -91,5 +92,5 @@ pub(crate) fn call_datetime_without_tzinfo(checker: &Checker, call: &ast::ExprCa None => DatetimeModuleAntipattern::NoTzArgumentPassed, }; - checker.report_diagnostic(CallDatetimeWithoutTzinfo(antipattern), call.range); + checker.report_diagnostic(CallDatetimeWithoutTzinfo(antipattern), call.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs index 79d932e7b4..1e974df0d0 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/log_exception_outside_except_handler.rs @@ -125,7 +125,7 @@ pub(crate) fn log_exception_outside_except_handler(checker: &Checker, call: &Exp _ => return, }; - let mut diagnostic = checker.report_diagnostic(LogExceptionOutsideExceptHandler, call.range); + let mut diagnostic = checker.report_diagnostic(LogExceptionOutsideExceptHandler, call.range()); if let Some(fix) = fix { diagnostic.set_fix(fix); diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/root_logger_call.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/root_logger_call.rs index 48e9087848..c888c1da59 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/root_logger_call.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/root_logger_call.rs @@ -1,6 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::ExprCall; use ruff_python_semantic::Modules; +use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; @@ -65,5 +66,5 @@ pub(crate) fn root_logger_call(checker: &Checker, call: &ExprCall) { let kind = RootLoggerCall { attr: (*attr).to_string(), }; - checker.report_diagnostic(kind, call.range); + checker.report_diagnostic(kind, call.range()); } diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs index 6ae68e4abf..30cefb7075 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs @@ -90,7 +90,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, }) = &call else { @@ -151,7 +151,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, }) = expr else { @@ -203,7 +203,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &Checker, expr: &Expr) { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }); let call = node3; 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 285ea7d35a..0b784fdfba 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 @@ -723,7 +723,7 @@ fn check_fixture_decorator(checker: &Checker, func_name: &str, decorator: &Decor Expr::Call(ast::ExprCall { func: _, arguments, - range: _, + range_start: _, node_index: _, }) => { if checker.is_rule_enabled(Rule::PytestFixtureIncorrectParenthesesStyle) { diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs index 1981f8e586..172ede62b1 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs @@ -158,7 +158,7 @@ fn check_mark_parentheses(checker: &Checker, decorator: &Decorator, marker: &str Expr::Call(ast::ExprCall { func: _, arguments, - range: _, + range_start: _, node_index: _, }) => { if !checker.settings().flake8_pytest_style.mark_parentheses diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/unittest_assert.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/unittest_assert.rs index c5bea94b96..d799847935 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/unittest_assert.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/unittest_assert.rs @@ -397,7 +397,7 @@ impl UnittestAssert { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; let isinstance = node1.into(); @@ -446,7 +446,7 @@ impl UnittestAssert { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; let re_search = node2.into(); diff --git a/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs b/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs index 07ca59a9a2..baf44e1aa4 100644 --- a/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs +++ b/crates/ruff_linter/src/rules/flake8_raise/rules/unnecessary_paren_on_raise_exception.rs @@ -62,7 +62,7 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &Checker, expr: &Exp let Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, }) = expr else { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs index d6c78abb01..dbcbd1043c 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_bool_op.rs @@ -314,7 +314,7 @@ fn isinstance_target<'a>(call: &'a Expr, semantic: &'a SemanticModel) -> Option< range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, } = call.as_call_expr()?; if args.len() != 2 { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs index d42612453a..8c6db482f0 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_expr.rs @@ -256,7 +256,7 @@ pub(crate) fn dict_get_with_none_default(checker: &Checker, expr: &Expr) { let Expr::Call(ast::ExprCall { func, arguments: Arguments { args, keywords, .. }, - range: _, + range_start: _, node_index: _, }) = expr else { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs index 13cba8b3c7..8a80c3cdb7 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_ifexp.rs @@ -196,7 +196,7 @@ pub(crate) fn if_expr_with_true_false( range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, } .into(), diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_unary_op.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_unary_op.rs index 58fb30c037..ce9578f5f0 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_unary_op.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/ast_unary_op.rs @@ -297,7 +297,7 @@ pub(crate) fn double_negation(checker: &Checker, expr: &Expr, op: UnaryOp, opera range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs index 8f37e84613..90f33e67dd 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs @@ -209,7 +209,7 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &Checker, stmt_if: &ast range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; let node4 = expected_var.clone(); @@ -318,7 +318,7 @@ pub(crate) fn if_exp_instead_of_dict_get( range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs index 645a79d9e1..9aea515ef2 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/key_in_dict.rs @@ -60,7 +60,7 @@ fn key_in_dict(checker: &Checker, left: &Expr, right: &Expr, operator: CmpOp, pa let Expr::Call(ast::ExprCall { func, arguments: Arguments { args, keywords, .. }, - range: _, + range_start: _, node_index: _, }) = &right else { diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs index e544969efb..8a03721409 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/needless_bool.rs @@ -288,7 +288,7 @@ pub(crate) fn needless_bool(checker: &Checker, stmt: &Stmt) { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; Some(Expr::Call(call_node)) diff --git a/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs b/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs index 9a3342ec20..b3af421bed 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -435,7 +435,7 @@ fn return_stmt(id: Name, test: &Expr, target: &Expr, iter: &Expr, generator: Gen range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; let node3 = ast::StmtReturn { diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/invalid_pathlib_with_suffix.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/invalid_pathlib_with_suffix.rs index fc66c33855..2c3fa0707d 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/invalid_pathlib_with_suffix.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/invalid_pathlib_with_suffix.rs @@ -123,7 +123,7 @@ pub(crate) fn invalid_pathlib_with_suffix(checker: &Checker, call: &ast::ExprCal } let mut diagnostic = - checker.report_diagnostic(InvalidPathlibWithSuffix { single_dot }, call.range); + checker.report_diagnostic(InvalidPathlibWithSuffix { single_dot }, call.range()); if !single_dot { let after_leading_quote = string.start() + first_part.flags.opener_len(); diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( diff --git a/crates/ruff_linter/src/rules/flynt/helpers.rs b/crates/ruff_linter/src/rules/flynt/helpers.rs index 96852d8961..d0ea3ea97e 100644 --- a/crates/ruff_linter/src/rules/flynt/helpers.rs +++ b/crates/ruff_linter/src/rules/flynt/helpers.rs @@ -35,7 +35,7 @@ fn is_simple_call(expr: &Expr) -> bool { range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, }) => args.is_empty() && keywords.is_empty() && is_simple_callee(func), _ => false, 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 580f422906..fd1c376659 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 @@ -146,12 +146,13 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF range: _, node_index: _, }, - range, + range_start: _, node_index: _, }) = value.as_ref() else { return; }; + let call_range = value.range(); if !keywords.is_empty() { return; @@ -339,7 +340,7 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF is_async: for_stmt.is_async, comprehension_type: Some(comprehension_type), }, - *range, + call_range, ); // TODO: once this fix is stabilized, change the rule to always fixable diff --git a/crates/ruff_linter/src/rules/perflint/rules/manual_list_copy.rs b/crates/ruff_linter/src/rules/perflint/rules/manual_list_copy.rs index 978f84e74f..d282a81810 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/manual_list_copy.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/manual_list_copy.rs @@ -2,6 +2,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::helpers::any_over_expr; use ruff_python_ast::{self as ast, Arguments, Expr, Stmt}; use ruff_python_semantic::analyze::typing::is_list; +use ruff_text_size::Ranged; use crate::Violation; use crate::checkers::ast::Checker; @@ -73,12 +74,13 @@ pub(crate) fn manual_list_copy(checker: &Checker, for_stmt: &ast::StmtFor) { range: _, node_index: _, }, - range, + range_start: _, node_index: _, }) = value.as_ref() else { return; }; + let call_range = value.range(); if !keywords.is_empty() { return; @@ -123,5 +125,5 @@ pub(crate) fn manual_list_copy(checker: &Checker, for_stmt: &ast::StmtFor) { return; } - checker.report_diagnostic(ManualListCopy, *range); + checker.report_diagnostic(ManualListCopy, call_range); } diff --git a/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs b/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs index 04993d8ac2..a0109ff625 100644 --- a/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs +++ b/crates/ruff_linter/src/rules/perflint/rules/unnecessary_list_cast.rs @@ -3,7 +3,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt}; use ruff_python_ast::{self as ast, Arguments, Expr, Stmt}; use ruff_python_semantic::analyze::typing::find_assigned_value; -use ruff_text_size::TextRange; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits; @@ -76,7 +76,7 @@ pub(crate) fn unnecessary_list_cast(checker: &Checker, iter: &Expr, body: &[Stmt range: _, node_index: _, }, - range: list_range, + range_start: _, node_index: _, }) = iter else { @@ -104,8 +104,8 @@ pub(crate) fn unnecessary_list_cast(checker: &Checker, iter: &Expr, body: &[Stmt range: iterable_range, .. }) => { - let mut diagnostic = checker.report_diagnostic(UnnecessaryListCast, *list_range); - diagnostic.set_fix(remove_cast(checker, *list_range, *iterable_range)); + let mut diagnostic = checker.report_diagnostic(UnnecessaryListCast, iter.range()); + diagnostic.set_fix(remove_cast(checker, iter.range(), *iterable_range)); } Expr::Name(ast::ExprName { id, @@ -131,8 +131,8 @@ pub(crate) fn unnecessary_list_cast(checker: &Checker, iter: &Expr, body: &[Stmt return; } - let mut diagnostic = checker.report_diagnostic(UnnecessaryListCast, *list_range); - diagnostic.set_fix(remove_cast(checker, *list_range, *iterable_range)); + let mut diagnostic = checker.report_diagnostic(UnnecessaryListCast, iter.range()); + diagnostic.set_fix(remove_cast(checker, iter.range(), *iterable_range)); } } _ => {} diff --git a/crates/ruff_linter/src/rules/pylint/rules/nested_min_max.rs b/crates/ruff_linter/src/rules/pylint/rules/nested_min_max.rs index dffdf5029c..da5d6f46cf 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/nested_min_max.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/nested_min_max.rs @@ -137,7 +137,7 @@ fn collect_nested_args(min_max: MinMax, args: &[Expr], semantic: &SemanticModel) range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, }) = arg { @@ -207,7 +207,7 @@ pub(crate) fn nested_min_max( range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }); diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs index 461a6619cd..d442783fb7 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs @@ -148,7 +148,7 @@ fn match_named_tuple_assign<'a>( let Expr::Call(ast::ExprCall { func, arguments: Arguments { args, keywords, .. }, - range: _, + range_start: _, node_index: _, }) = value else { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index 1144f8cd5f..33c60d9ddf 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -133,7 +133,7 @@ fn match_typed_dict_assign<'a>( let Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, }) = value else { @@ -275,7 +275,7 @@ fn match_fields_and_total(arguments: &Arguments) -> Option<(Suite, Option<&Keywo Expr::Call(ast::ExprCall { func, arguments: Arguments { keywords, .. }, - range: _, + range_start: _, node_index: _, }) => Some((fields_from_dict_call(func, keywords)?, total)), _ => None, diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs index f8db620709..ff7d411dd9 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs @@ -71,7 +71,7 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &Checker, decorator_list: &[D range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, }) = &decorator.expression else { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs index 27aaa84e31..4a9127c68b 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs @@ -59,7 +59,7 @@ pub(crate) fn lru_cache_without_parameters(checker: &Checker, decorator_list: &[ let Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, }) = &decorator.expression else { 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 df90a5ac05..3484eb6b7c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/native_literals.rs @@ -196,7 +196,7 @@ pub(crate) fn native_literals( range: _, node_index: _, }, - range: call_range, + range_start: _, node_index: _, } = call; @@ -289,7 +289,8 @@ pub(crate) fn native_literals( // Ex) `bool(True)and None` no space between `)` and the keyword `and`. // // Subtract 1 from the end of the range to include `Rpar` token in the slice. - if let [paren_token, next_token, ..] = tokens.after(call_range.sub_end(1.into()).end()) + if let [paren_token, next_token, ..] = + tokens.after(call.range().sub_end(1.into()).end()) { needs_space = next_token.kind().is_keyword() && paren_token.range().end() == next_token.range().start(); @@ -328,7 +329,7 @@ pub(crate) fn native_literals( content.push(' '); } - let applicability = if checker.comment_ranges().intersects(call.range) { + let applicability = if checker.comment_ranges().intersects(call.range()) { Applicability::Unsafe } else { Applicability::Safe diff --git a/crates/ruff_linter/src/rules/refurb/helpers.rs b/crates/ruff_linter/src/rules/refurb/helpers.rs index d7c1863766..651bfb4ebe 100644 --- a/crates/ruff_linter/src/rules/refurb/helpers.rs +++ b/crates/ruff_linter/src/rules/refurb/helpers.rs @@ -36,7 +36,7 @@ pub(super) fn generate_method_call(name: Name, method: &str, generator: Generato range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; // And finally, turn it into a statement. diff --git a/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs b/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs index 15d46a92d2..4e436522f8 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs @@ -197,7 +197,7 @@ fn make_suggestion(set: &ast::ExprName, element: &Expr, generator: Generator) -> range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; // And finally, turn it into a statement. diff --git a/crates/ruff_linter/src/rules/refurb/rules/isinstance_type_none.rs b/crates/ruff_linter/src/rules/refurb/rules/isinstance_type_none.rs index ac06100b6f..c909bd044a 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/isinstance_type_none.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/isinstance_type_none.rs @@ -1,6 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_ast::{self as ast, Expr, Operator}; use ruff_python_semantic::SemanticModel; +use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::rules::refurb::helpers::replace_with_identity_check; @@ -69,9 +70,9 @@ pub(crate) fn isinstance_type_none(checker: &Checker, call: &ast::ExprCall) { return; } - let fix = replace_with_identity_check(expr, call.range, false, checker); + let fix = replace_with_identity_check(expr, call.range(), false, checker); checker - .report_diagnostic(IsinstanceTypeNone, call.range) + .report_diagnostic(IsinstanceTypeNone, call.range()) .set_fix(fix); } diff --git a/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs b/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs index 90ad96b1e3..ba2ff9b2c8 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs @@ -183,7 +183,7 @@ fn make_suggestion(open: &FileOpen<'_>, generator: Generator) -> String { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; generator.expr(&call.into()) diff --git a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs index 29580bb0dd..a52771bc2c 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/reimplemented_starmap.rs @@ -324,7 +324,7 @@ fn construct_starmap_call(starmap_binding: Name, iter: &Expr, func: &Expr) -> as range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, } } @@ -345,7 +345,7 @@ fn wrap_with_call_to(call: ast::ExprCall, func_name: Name) -> ast::ExprCall { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, } } diff --git a/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs b/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs index 5448b0c416..071949e7a8 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/repeated_append.rs @@ -364,7 +364,7 @@ fn make_suggestion(group: &AppendGroup, generator: Generator) -> String { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; // And finally, turn it into a statement. diff --git a/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs b/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs index f42cd6d36c..088f135056 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/single_item_membership_test.rs @@ -126,7 +126,7 @@ fn single_item<'a>(expr: &'a Expr, semantic: &'a SemanticModel) -> Option<&'a Ex Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, }) => { if arguments.len() != 1 || !is_set_method(func, semantic) { diff --git a/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs b/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs index fc5d03a337..5b61e58a1d 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/slice_to_remove_prefix_or_suffix.rs @@ -358,7 +358,7 @@ fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) - ( AffixKind::StartsWith, ast::Expr::Call(ast::ExprCall { - range: _, + range_start: _, node_index: _, func, arguments, @@ -407,7 +407,7 @@ fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) - _, ) => operand.as_call_expr().is_some_and( |ast::ExprCall { - range: _, + range_start: _, node_index: _, func, arguments, diff --git a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs index b8d87b7e67..2f2480d47a 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/unnecessary_enumerate.rs @@ -252,7 +252,7 @@ fn generate_range_len_call(name: Name, generator: Generator) -> String { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; // Construct `range(len(name))`. @@ -272,7 +272,7 @@ fn generate_range_len_call(name: Name, generator: Generator) -> String { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; // And finally, turn it into a statement. diff --git a/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs b/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs index 57339a102c..da8de4e505 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/in_empty_collection.rs @@ -80,7 +80,7 @@ fn is_empty(expr: &Expr, semantic: &SemanticModel) -> bool { Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, }) => { if arguments.is_empty() { diff --git a/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs b/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs index defd1fd010..445e8aa050 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs @@ -248,7 +248,7 @@ fn generate_with_statement( let context_call = ast::ExprCall { node_index: AtomicNodeIndex::NONE, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), func: legacy_call.func.clone(), arguments: ast::Arguments { node_index: AtomicNodeIndex::NONE, @@ -270,7 +270,7 @@ fn generate_with_statement( let func_call = ast::ExprCall { node_index: AtomicNodeIndex::NONE, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), func: Box::new(func.clone()), arguments: ast::Arguments { node_index: AtomicNodeIndex::NONE, diff --git a/crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs b/crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs index efc1b60f13..cc929be2c4 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/map_int_version_parsing.rs @@ -71,7 +71,7 @@ fn map_call_with_two_arguments<'a>( range: _, node_index: _, }, - range: _, + range_start: _, node_index: _, } = call; 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 141f120a44..e4339daa01 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 @@ -113,7 +113,7 @@ pub(crate) fn quadratic_list_summation(checker: &Checker, call: &ast::ExprCall) let ast::ExprCall { func, arguments, - range, + range_start: _, node_index: _, } = call; @@ -132,7 +132,8 @@ pub(crate) fn quadratic_list_summation(checker: &Checker, call: &ast::ExprCall) } let fix_style = QuadraticListSummationFixStyle::from_target_version(checker.target_version()); - let mut diagnostic = checker.report_diagnostic(QuadraticListSummation { fix_style }, *range); + let mut diagnostic = + checker.report_diagnostic(QuadraticListSummation { fix_style }, call.range()); diagnostic.try_set_fix(|| convert_to_fix(iterable, call, checker, fix_style)); } diff --git a/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs b/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs index 79881b8abc..1205767a2e 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/starmap_zip.rs @@ -107,7 +107,7 @@ pub(crate) fn starmap_zip(checker: &Checker, call: &ExprCall) { return; } - let mut diagnostic = checker.report_diagnostic(StarmapZip, call.range); + let mut diagnostic = checker.report_diagnostic(StarmapZip, call.range()); if let Some(fix) = replace_with_map(call, iterable_call, checker) { diagnostic.set_fix(fix); diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs index 767e139ae8..2659e71555 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_literal_within_deque_call.rs @@ -117,7 +117,7 @@ pub(crate) fn unnecessary_literal_within_deque_call(checker: &Checker, deque: &a UnnecessaryEmptyIterableWithinDequeCall { has_maxlen: maxlen.is_some(), }, - deque.range, + deque.range(), ); // Return without a fix in the presence of a starred argument because we can't accurately @@ -145,7 +145,7 @@ fn fix_unnecessary_literal_in_deque( ); let len_str = checker.locator().slice(maxlen); let deque_str = format!("{deque_name}(maxlen={len_str})"); - Edit::range_replacement(deque_str, deque.range) + Edit::range_replacement(deque_str, deque.range()) } else { remove_argument( &iterable, diff --git a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs index 41a076a5b0..1a445ffc21 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_regular_expression.rs @@ -6,7 +6,7 @@ use ruff_python_ast::{ }; use ruff_python_semantic::analyze::typing::find_binding_value; use ruff_python_semantic::{Modules, SemanticModel}; -use ruff_text_size::TextRange; +use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::{Applicability, Edit, Fix, FixAvailability, Violation}; @@ -181,7 +181,7 @@ impl<'a> ReFunc<'a> { let (comparison_to_none, range) = match comparison_to_none { Some((cmp, range)) => (Some(cmp), range), - None => (None, call.range), + None => (None, call.range()), }; match (func_name, call.arguments.len()) { @@ -356,7 +356,7 @@ impl<'a> ReFunc<'a> { range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }, - range: TextRange::default(), + range_start: ruff_text_size::TextSize::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }) } 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 e2ab51e1db..e21e14432d 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/unnecessary_round.rs @@ -225,5 +225,5 @@ fn unwrap_round_call( rounded_expr.to_string() }; - Edit::range_replacement(new_content, call.range) + Edit::range_replacement(new_content, call.range()) } diff --git a/crates/ruff_python_ast/ast.toml b/crates/ruff_python_ast/ast.toml index 62b42cc29a..e64e3f0928 100644 --- a/crates/ruff_python_ast/ast.toml +++ b/crates/ruff_python_ast/ast.toml @@ -421,8 +421,16 @@ fields = [ custom_source_order = true [Expr.nodes.ExprCall] -doc = "See also [Call](https://docs.python.org/3/library/ast.html#ast.Call)" +doc = """A call expression whose end offset is derived from its arguments. + +The parser and error-recovery code must ensure that the call and its arguments +end at the same offset. + +See also [Call](https://docs.python.org/3/library/ast.html#ast.Call)""" +custom_debug = true +custom_range = true fields = [ + { name = "range_start", type = "ruff_text_size::TextSize", skip_visit = true }, { name = "func", type = "Expr" }, { name = "arguments", type = "Arguments" }, ] diff --git a/crates/ruff_python_ast/generate.py b/crates/ruff_python_ast/generate.py index 6b85baba8a..4d11f3ba60 100644 --- a/crates/ruff_python_ast/generate.py +++ b/crates/ruff_python_ast/generate.py @@ -144,7 +144,9 @@ class Node: doc: str | None fields: list[Field] | None derives: list[str] + custom_debug: bool custom_source_order: bool + custom_range: bool source_order: list[str] | None def __init__(self, group: Group, node_name: str, node: dict[str, Any]) -> None: @@ -155,7 +157,9 @@ def __init__(self, group: Group, node_name: str, node: dict[str, Any]) -> None: fields = node.get("fields") if fields is not None: self.fields = [Field(f) for f in fields] + self.custom_debug = node.get("custom_debug", False) self.custom_source_order = node.get("custom_source_order", False) + self.custom_range = node.get("custom_range", False) self.derives = node.get("derives", []) self.doc = node.get("doc") self.source_order = node.get("source_order") @@ -456,6 +460,8 @@ def write_owned_enum(out: list[str], ast: Ast) -> None: out.append("}") for node in ast.all_nodes: + if node.custom_range: + continue out.append(f""" impl ruff_text_size::Ranged for {node.ty} {{ fn range(&self) -> ruff_text_size::TextRange {{ @@ -1045,7 +1051,9 @@ def write_node(out: list[str], ast: Ast) -> None: if node.doc is not None: write_rustdoc(out, node.doc) out.append( - "#[derive(Clone, Debug, PartialEq" + "#[derive(Clone" + + ("" if node.custom_debug else ", Debug") + + ", PartialEq" + "".join(f", {derive}" for derive in node.derives) + ")]" ) @@ -1053,7 +1061,8 @@ def write_node(out: list[str], ast: Ast) -> None: name = node.name out.append(f"pub struct {name} {{") out.append("pub node_index: crate::AtomicNodeIndex,") - out.append("pub range: ruff_text_size::TextRange,") + if not node.custom_range: + out.append("pub range: ruff_text_size::TextRange,") for field in node.fields: field_str = f"pub {field.name}: " ty = field.parsed_ty @@ -1098,7 +1107,8 @@ def write_source_order(out: list[str], ast: Ast) -> None: fields_list += f"{field.name}: _,\n" else: fields_list += f"{field.name},\n" - fields_list += "range: _,\n" + if not node.custom_range: + fields_list += "range: _,\n" fields_list += "node_index: _,\n" for field in node.fields_in_source_order(): diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index b068690072..c769effe62 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -1235,7 +1235,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { ast::Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, }) => Self::Call(ExprCall { func: func.into(), diff --git a/crates/ruff_python_ast/src/generated.rs b/crates/ruff_python_ast/src/generated.rs index 006ea651dd..086ba0ffeb 100644 --- a/crates/ruff_python_ast/src/generated.rs +++ b/crates/ruff_python_ast/src/generated.rs @@ -3822,12 +3822,6 @@ impl ruff_text_size::Ranged for crate::ExprCompare { } } -impl ruff_text_size::Ranged for crate::ExprCall { - fn range(&self) -> ruff_text_size::TextRange { - self.range - } -} - impl ruff_text_size::Ranged for crate::ExprFString { fn range(&self) -> ruff_text_size::TextRange { self.range @@ -9784,12 +9778,17 @@ pub struct ExprCompare { pub comparators: Box<[Expr]>, } +/// A call expression whose end offset is derived from its arguments. +/// +/// The parser and error-recovery code must ensure that the call and its arguments +/// end at the same offset. +/// /// See also [Call](https://docs.python.org/3/library/ast.html#ast.Call) -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] #[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] pub struct ExprCall { pub node_index: crate::AtomicNodeIndex, - pub range: ruff_text_size::TextRange, + pub range_start: ruff_text_size::TextSize, pub func: Box, pub arguments: crate::Arguments, } @@ -10831,9 +10830,9 @@ impl ExprCall { V: SourceOrderVisitor<'a> + ?Sized, { let ExprCall { + range_start: _, func, arguments, - range: _, node_index: _, } = self; visitor.visit_expr(func); diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 4728592d62..43d8547eb1 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -392,7 +392,7 @@ where Expr::Call(ast::ExprCall { func: call_func, arguments, - range: _, + range_start: _, node_index: _, }) => { any_over_expr(call_func, &mut *func) diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index 1bdac31f35..adc581243d 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -2,8 +2,8 @@ use crate::AtomicNodeIndex; use crate::generated::{ - ExprBytesLiteral, ExprDict, ExprFString, ExprList, ExprName, ExprSet, ExprStringLiteral, - ExprTString, ExprTuple, PatternMatchAs, PatternMatchOr, StmtClassDef, + ExprBytesLiteral, ExprCall, ExprDict, ExprFString, ExprList, ExprName, ExprSet, + ExprStringLiteral, ExprTString, ExprTuple, PatternMatchAs, PatternMatchOr, StmtClassDef, }; use std::borrow::Cow; use std::fmt; @@ -1328,6 +1328,28 @@ impl ExprStringLiteral { } } +impl Ranged for ExprCall { + fn range(&self) -> TextRange { + TextRange::new(self.range_start, self.arguments.end()) + } +} + +#[expect( + clippy::missing_fields_in_debug, + reason = "`range_start` is represented by the reconstructed `range` field" +)] +impl fmt::Debug for ExprCall { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ExprCall") + .field("node_index", &self.node_index) + .field("range", &self.range()) + .field("func", &self.func) + .field("arguments", &self.arguments) + .finish() + } +} + /// The value representing a [`ExprStringLiteral`]. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "get-size", derive(get_size2::GetSize))] @@ -1368,10 +1390,10 @@ impl StringLiteralValue { "Use `StringLiteralValue::single` to create single-part strings" ); Self { - inner: StringLiteralValueInner::Concatenated(ConcatenatedStringLiteral { + inner: StringLiteralValueInner::Concatenated(Box::new(ConcatenatedStringLiteral { strings, value: OnceLock::new(), - }), + })), } } @@ -1494,7 +1516,7 @@ enum StringLiteralValueInner { Single(StringLiteral), /// An implicitly concatenated string literals i.e., `"foo" "bar"`. - Concatenated(ConcatenatedStringLiteral), + Concatenated(Box), } bitflags! { @@ -3924,14 +3946,14 @@ mod tests { assert_eq!(std::mem::size_of::(), 72); assert_eq!(std::mem::size_of::(), 56); assert_eq!(std::mem::size_of::(), 40); - assert_eq!(std::mem::size_of::(), 72); + assert_eq!(std::mem::size_of::(), 64); assert_eq!(std::mem::size_of::(), 56); assert_eq!(std::mem::size_of::(), 24); assert_eq!(std::mem::size_of::(), 32); assert_eq!(std::mem::size_of::(), 40); assert_eq!(std::mem::size_of::(), 16); assert_eq!(std::mem::size_of::(), 48); - assert_eq!(std::mem::size_of::(), 64); + assert_eq!(std::mem::size_of::(), 56); assert_eq!(std::mem::size_of::(), 56); assert_eq!(std::mem::size_of::(), 40); assert_eq!(std::mem::size_of::(), 56); @@ -3951,7 +3973,7 @@ mod tests { assert_eq!(std::mem::size_of::(), 48); assert_eq!(std::mem::size_of::(), 40); assert_eq!(std::mem::size_of::(), 24); - assert_eq!(std::mem::size_of::(), 64); + assert_eq!(std::mem::size_of::(), 48); assert_eq!(std::mem::size_of::(), 32); assert_eq!(std::mem::size_of::(), 40); assert_eq!(std::mem::size_of::(), 24); diff --git a/crates/ruff_python_ast/src/relocate.rs b/crates/ruff_python_ast/src/relocate.rs index eea26d7a37..64fd01bed9 100644 --- a/crates/ruff_python_ast/src/relocate.rs +++ b/crates/ruff_python_ast/src/relocate.rs @@ -66,8 +66,13 @@ impl Transformer for Relocator { Expr::Compare(ast::ExprCompare { range, .. }) => { *range = self.range; } - Expr::Call(ast::ExprCall { range, .. }) => { - *range = self.range; + Expr::Call(ast::ExprCall { + range_start, + arguments, + .. + }) => { + *range_start = self.range.start(); + arguments.range = self.range; } Expr::FString(ast::ExprFString { range, .. }) => { *range = self.range; diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 8fa8cdfa9d..d7a985a804 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -530,7 +530,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, }) => { visitor.visit_expr(func); diff --git a/crates/ruff_python_ast/src/visitor/transformer.rs b/crates/ruff_python_ast/src/visitor/transformer.rs index 7b1d2098e3..9f037ee23f 100644 --- a/crates/ruff_python_ast/src/visitor/transformer.rs +++ b/crates/ruff_python_ast/src/visitor/transformer.rs @@ -517,7 +517,7 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, }) => { visitor.visit_expr(func); diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index 5de64531f5..9154a0807e 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -1197,7 +1197,7 @@ impl<'a> Generator<'a> { Expr::Call(ast::ExprCall { func, arguments, - range: _, + range_start: _, node_index: _, }) => { self.unparse_expr(func, precedence::MAX); diff --git a/crates/ruff_python_formatter/src/expression/expr_call.rs b/crates/ruff_python_formatter/src/expression/expr_call.rs index 086287b674..bd35aeaf48 100644 --- a/crates/ruff_python_formatter/src/expression/expr_call.rs +++ b/crates/ruff_python_formatter/src/expression/expr_call.rs @@ -24,7 +24,7 @@ impl FormatRuleWithOptions> for FormatExprCall { impl FormatNodeRule for FormatExprCall { fn fmt_fields(&self, item: &ExprCall, f: &mut PyFormatter) -> FormatResult<()> { let ExprCall { - range: _, + range_start: _, node_index: _, func, arguments, diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 3ef2f2340f..9819038d62 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -709,7 +709,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { ); } Expr::Call(ast::ExprCall { - range: _, + range_start: _, node_index: _, func, arguments: _, diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 175d8cfd43..73add7a073 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -788,11 +788,12 @@ impl<'src> Parser<'src> { /// See: fn parse_call_expression(&mut self, func: Expr, start: TextSize) -> ast::ExprCall { let arguments = self.parse_arguments(ArgumentsContext::Call); + debug_assert_eq!(self.node_range(start).end(), arguments.end()); ast::ExprCall { func: Box::new(func), arguments, - range: self.node_range(start), + range_start: start, node_index: AtomicNodeIndex::NONE, } } diff --git a/crates/ruff_python_parser/src/parser/recovery.rs b/crates/ruff_python_parser/src/parser/recovery.rs index 8de087c6f0..22b8e120b1 100644 --- a/crates/ruff_python_parser/src/parser/recovery.rs +++ b/crates/ruff_python_parser/src/parser/recovery.rs @@ -93,30 +93,34 @@ pub(super) fn pattern_to_expr(pattern: Pattern) -> Expr { node_index, cls, arguments, - }) => Expr::Call(ast::ExprCall { - range, - node_index: node_index.clone(), - func: cls, - arguments: ast::Arguments { - range: arguments.range, + }) => { + debug_assert_eq!(range.end(), arguments.end()); + + Expr::Call(ast::ExprCall { + range_start: range.start(), node_index: node_index.clone(), - args: arguments - .patterns - .into_iter() - .map(pattern_to_expr) - .collect(), - keywords: arguments - .keywords - .into_iter() - .map(|keyword_pattern| ast::Keyword { - range: keyword_pattern.range, - node_index: node_index.clone(), - arg: Some(keyword_pattern.attr), - value: pattern_to_expr(keyword_pattern.pattern), - }) - .collect(), - }, - }), + func: cls, + arguments: ast::Arguments { + range: arguments.range, + node_index: node_index.clone(), + args: arguments + .patterns + .into_iter() + .map(pattern_to_expr) + .collect(), + keywords: arguments + .keywords + .into_iter() + .map(|keyword_pattern| ast::Keyword { + range: keyword_pattern.range, + node_index: node_index.clone(), + arg: Some(keyword_pattern.attr), + value: pattern_to_expr(keyword_pattern.pattern), + }) + .collect(), + }, + }) + } Pattern::MatchStar(ast::PatternMatchStar { range, node_index, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 43adc5b4a8..c67a1f9bee 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8723,7 +8723,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let env = self.program_environment(); let ast::ExprCall { - range: _, + range_start: _, node_index: _, func, arguments, From 17980fe69f063ed95042e19e1ea3b0afb0152c6c Mon Sep 17 00:00:00 2001 From: chiri Date: Sun, 9 Aug 2026 16:48:32 +0300 Subject: [PATCH 340/390] Update mimalloc version to 0.1.52 (#27586) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index aae537860f..6279cc5a5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,7 +134,7 @@ lsp-server = { version = "0.10.0" } lsp-types = { package = "gen-lsp-types", version = "0.11.0", features = ["url"] } matchit = { version = "0.9.0" } memchr = { version = "2.7.1" } -mimalloc = { version = "0.1.49", features = ["v2"] } +mimalloc = { version = "0.1.52" } natord = { version = "1.0.9" } notify = { version = "8.0.0" } ordermap = { version = "1.0.0" } From 09a7441a394c4526c1b098a135bb43a7d1085554 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Sun, 9 Aug 2026 15:49:09 +0200 Subject: [PATCH 341/390] [ty] Split-off `Command` execution from `System` (#27608) --- crates/ruff_db/src/system.rs | 29 +++++++----- crates/ruff_db/src/system/command.rs | 68 ++++++++++++++++++++++++++++ crates/ruff_db/src/system/os.rs | 35 ++++++++------ crates/ruff_db/src/system/test.rs | 14 ++---- crates/ty_project/src/metadata/uv.rs | 12 ++--- crates/ty_server/src/session.rs | 26 +++++++++++ crates/ty_server/src/system.rs | 17 ++----- 7 files changed, 148 insertions(+), 53 deletions(-) create mode 100644 crates/ruff_db/src/system/command.rs diff --git a/crates/ruff_db/src/system.rs b/crates/ruff_db/src/system.rs index 42554e7031..dade3d560d 100644 --- a/crates/ruff_db/src/system.rs +++ b/crates/ruff_db/src/system.rs @@ -1,3 +1,4 @@ +pub use command::{Command, CommandExecutor}; pub use memory_fs::MemoryFileSystem; #[cfg(all(feature = "testing", feature = "os"))] @@ -22,6 +23,7 @@ pub use self::path::{ }; use crate::file_revision::FileRevision; +mod command; mod memory_fs; #[cfg(feature = "os")] mod os; @@ -101,18 +103,21 @@ pub trait System: Debug + Sync + Send { /// Find an executable binary's path by name. fn which(&self, binary_name: &str) -> WhichResult; - /// Runs a command in the given working directory, returning its output. - fn run_command( - &self, - program: &str, - args: &[&str], - current_directory: &SystemPath, - ) -> Result { - let _ = (program, args, current_directory); - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "running commands is not supported by this system", - )) + /// Runs `command` and captures its standard output and standard error. + fn run_command(&self, command: Command) -> Result { + let Some(executor) = self.command_executor() else { + return Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "running commands is not supported by this system", + )); + }; + + executor.execute(command) + } + + /// Returns the system's command executor, if it supports running commands. + fn command_executor(&self) -> Option<&dyn CommandExecutor> { + None } /// Reads the content of the file at `path` into a [`String`]. diff --git a/crates/ruff_db/src/system/command.rs b/crates/ruff_db/src/system/command.rs new file mode 100644 index 0000000000..155e63d865 --- /dev/null +++ b/crates/ruff_db/src/system/command.rs @@ -0,0 +1,68 @@ +use std::process::Output; + +use super::{Result, SystemPath, SystemPathBuf}; + +/// An owned description of a command to execute with a [`CommandExecutor`]. +#[derive(Debug)] +pub struct Command { + executable: String, + arguments: Vec, + current_directory: Option, +} + +impl Command { + /// Creates a command for the given executable. + pub fn new(executable: impl Into) -> Self { + Self { + executable: executable.into(), + arguments: Vec::new(), + current_directory: None, + } + } + + /// Adds an argument to the command. + pub fn arg(&mut self, argument: impl Into) -> &mut Self { + self.arguments.push(argument.into()); + self + } + + /// Adds multiple arguments to the command. + pub fn args(&mut self, arguments: I) -> &mut Self + where + I: IntoIterator, + S: Into, + { + self.arguments.extend(arguments.into_iter().map(Into::into)); + self + } + + /// Sets the working directory for the command. + pub fn current_dir(&mut self, directory: impl AsRef) -> &mut Self { + self.current_directory = Some(directory.as_ref().to_path_buf()); + self + } + + /// Returns the executable to invoke. + pub fn get_executable(&self) -> &str { + &self.executable + } + + /// Returns the arguments passed to the executable. + pub fn get_args(&self) -> &[String] { + &self.arguments + } + + /// Returns the command's working directory, if explicitly configured. + pub fn get_current_dir(&self) -> Option<&SystemPath> { + self.current_directory.as_deref() + } +} + +/// Executes [`Command`]s. +pub trait CommandExecutor: Send + Sync { + /// Runs a command and captures its standard output and standard error. + fn execute(&self, command: Command) -> Result; + + /// Creates an owned executor that can be moved to another thread. + fn dyn_clone(&self) -> Box; +} diff --git a/crates/ruff_db/src/system/os.rs b/crates/ruff_db/src/system/os.rs index f13d778de8..622a71e733 100644 --- a/crates/ruff_db/src/system/os.rs +++ b/crates/ruff_db/src/system/os.rs @@ -9,13 +9,13 @@ use super::walk_directory::{ }; use crate::max_parallelism; use crate::system::{ - DirectoryEntry, FileType, Metadata, Result, System, SystemPath, SystemPathBuf, - SystemVirtualPath, WhichError, WhichResult, WritableSystem, + Command, CommandExecutor, DirectoryEntry, FileType, Metadata, Result, System, SystemPath, + SystemPathBuf, SystemVirtualPath, WhichError, WhichResult, WritableSystem, }; use filetime::FileTime; use ruff_notebook::{Notebook, NotebookError}; use std::num::NonZeroUsize; -use std::process::{Command, Output}; +use std::process::Output; use std::sync::Arc; use std::{any::Any, path::PathBuf}; @@ -130,16 +130,8 @@ impl System for OsSystem { } } - fn run_command( - &self, - program: &str, - args: &[&str], - current_directory: &SystemPath, - ) -> Result { - Command::new(program) - .args(args) - .current_dir(current_directory.as_std_path()) - .output() + fn command_executor(&self) -> Option<&dyn CommandExecutor> { + Some(self) } fn current_directory(&self) -> &SystemPath { @@ -236,6 +228,23 @@ impl System for OsSystem { } } +impl CommandExecutor for OsSystem { + fn execute(&self, command: Command) -> Result { + let directory = command + .get_current_dir() + .unwrap_or_else(|| self.current_directory()); + + std::process::Command::new(command.get_executable()) + .args(command.get_args()) + .current_dir(directory.as_std_path()) + .output() + } + + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) + } +} + impl WritableSystem for OsSystem { fn create_new_file(&self, path: &SystemPath) -> Result<()> { std::fs::File::create_new(path).map(drop) diff --git a/crates/ruff_db/src/system/test.rs b/crates/ruff_db/src/system/test.rs index 5e01bffec9..6055cc5017 100644 --- a/crates/ruff_db/src/system/test.rs +++ b/crates/ruff_db/src/system/test.rs @@ -1,14 +1,13 @@ use ruff_notebook::{Notebook, NotebookError}; use rustc_hash::FxHashMap; use std::panic::RefUnwindSafe; -use std::process::Output; use std::sync::{Arc, Mutex}; use crate::Db; use crate::files::File; use crate::system::{ - DirectoryEntry, MemoryFileSystem, Metadata, Result, System, SystemPath, SystemPathBuf, - SystemVirtualPath, WhichError, WhichResult, + CommandExecutor, DirectoryEntry, MemoryFileSystem, Metadata, Result, System, SystemPath, + SystemPathBuf, SystemVirtualPath, WhichError, WhichResult, }; use super::WritableSystem; @@ -141,13 +140,8 @@ impl System for TestSystem { Err(WhichError::CannotFindBinaryPath) } - fn run_command( - &self, - program: &str, - args: &[&str], - current_directory: &SystemPath, - ) -> Result { - self.system().run_command(program, args, current_directory) + fn command_executor(&self) -> Option<&dyn CommandExecutor> { + self.system().command_executor() } fn read_directory<'a>( diff --git a/crates/ty_project/src/metadata/uv.rs b/crates/ty_project/src/metadata/uv.rs index 8c2e0cc123..8bda3071e1 100644 --- a/crates/ty_project/src/metadata/uv.rs +++ b/crates/ty_project/src/metadata/uv.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use pep440_rs::Version; -use ruff_db::system::{System, SystemPath, SystemPathBuf, WhichError}; +use ruff_db::system::{Command, System, SystemPath, SystemPathBuf, WhichError}; use ruff_ranged_value::{RangedValue, ValueSource}; use serde::Deserialize; use thiserror::Error; @@ -32,12 +32,12 @@ impl UvWorkspace { // `uv check` has already selected and synchronized the environment. Keep this query // read-only so package selection and `--isolated` aren't overwritten by a second sync. + let mut command = Command::new(uv); + command + .args(["workspace", "metadata", "--frozen", "--active"]) + .current_dir(path); let output = system - .run_command( - &uv, - &["workspace", "metadata", "--frozen", "--active"], - path, - ) + .run_command(command) .map_err(UvWorkspaceError::Invocation)?; if !output.status.success() { diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 808e9ecc0d..34330180e6 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -1992,3 +1992,29 @@ pub(super) fn warn_about_unknown_options( tracing::warn!("{message}"); client.show_warning_message(message); } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use ruff_db::system::{CommandExecutor, OsSystem, System as _}; + + use super::Index; + use crate::system::LSPSystem; + + /// Mutating the document index requires exclusive ownership after Salsa cancels the current + /// database snapshots. A background command executor must not retain an `LSPSystem`, because + /// that would keep the index alive and prevent the server from applying document changes. + #[test] + fn detached_command_executor_does_not_retain_document_index() { + let index = Arc::new(Index::new()); + let system = LSPSystem::new(index.clone(), Arc::new(OsSystem::default())); + let executor = system.command_executor().map(CommandExecutor::dyn_clone); + assert!(executor.is_some()); + drop(system); + + assert_eq!(Arc::strong_count(&index), 1); + + drop(executor); + } +} diff --git a/crates/ty_server/src/system.rs b/crates/ty_server/src/system.rs index b5125c98c1..3c0d20ce39 100644 --- a/crates/ty_server/src/system.rs +++ b/crates/ty_server/src/system.rs @@ -3,7 +3,6 @@ use std::fmt; use std::fmt::Display; use std::hash::{DefaultHasher, Hash, Hasher as _}; use std::panic::RefUnwindSafe; -use std::process::Output; use std::sync::Arc; use crate::Db; @@ -14,7 +13,7 @@ use ruff_db::file_revision::FileRevision; use ruff_db::files::{File, FilePath}; use ruff_db::system::walk_directory::WalkDirectoryBuilder; use ruff_db::system::{ - DirectoryEntry, FileType, Metadata, Result, System, SystemPath, SystemPathBuf, + CommandExecutor, DirectoryEntry, FileType, Metadata, Result, System, SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf, WhichResult, WritableSystem, }; use ruff_notebook::{Notebook, NotebookError}; @@ -182,16 +181,6 @@ impl System for LSPSystem { self.native_system.is_same_file(first, second) } - fn run_command( - &self, - program: &str, - args: &[&str], - current_directory: &SystemPath, - ) -> Result { - self.native_system - .run_command(program, args, current_directory) - } - fn source_type(&self, path: &SystemPath) -> Option { let document = self.system_path_to_document(path)?; Self::source_type_from_document(document, path.extension()) @@ -290,6 +279,10 @@ impl System for LSPSystem { self.native_system.env_var(name) } + fn command_executor(&self) -> Option<&dyn CommandExecutor> { + self.native_system.command_executor() + } + fn dyn_clone(&self) -> Box { Box::new(self.clone()) } From 78cad66655ddaf3e2f7a4858305e0833841dcfa4 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 9 Aug 2026 15:02:37 +0100 Subject: [PATCH 342/390] [ty] Add an opt-in `unsound-yield` lint (#27593) --- .github/ty-ecosystem.toml | 1 + crates/ty/docs/rules.md | 378 ++++++++++++------ .../lint_docs/unsound-return-statement.md | 4 + .../resources/lint_docs/unsound-yield.md | 127 ++++++ .../mdtest/expression/yield_and_yield_from.md | 264 ++++++++++++ .../src/types/diagnostic.rs | 82 ++++ .../src/types/infer/builder.rs | 84 ++-- .../src/types/infer/builder/function.rs | 4 + .../ty_python_semantic/src/types/iteration.rs | 6 +- crates/ty_test/src/db.rs | 7 +- ty.schema.json | 12 +- 11 files changed, 824 insertions(+), 145 deletions(-) create mode 100644 crates/ty_python_semantic/resources/lint_docs/unsound-yield.md diff --git a/.github/ty-ecosystem.toml b/.github/ty-ecosystem.toml index 613aaa8bc9..7d3f469aa7 100644 --- a/.github/ty-ecosystem.toml +++ b/.github/ty-ecosystem.toml @@ -10,4 +10,5 @@ possibly-missing-attribute = "warn" possibly-missing-import = "warn" possibly-unresolved-reference = "warn" unsound-return-statement = "warn" +unsound-yield = "warn" unsupported-dynamic-base = "warn" diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index df75037ed5..97d56a4d6b 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md @@ -8,7 +8,7 @@ Default level: error · Added in 0.0.64 · Related issues · -View source +View source @@ -44,7 +44,7 @@ class Base(ABC): Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -90,7 +90,7 @@ class Derived(Base): # error Default level: warn · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -154,7 +154,7 @@ class SubProto(BaseProto, Protocol): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -237,7 +237,7 @@ value = unknown # ty: ignore[unresolved-reference] Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -292,7 +292,7 @@ Foo.method() # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -320,7 +320,7 @@ Calling a non-callable object will raise a `TypeError` at runtime. Default level: error · Added in 0.0.7 · Related issues · -View source +View source @@ -355,7 +355,7 @@ def f(x: object): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -389,7 +389,7 @@ a = 1 # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -424,7 +424,7 @@ class C(A, B): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -460,7 +460,7 @@ class B(A): ... # error Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -496,7 +496,7 @@ type B = A # error Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -533,7 +533,7 @@ class Example: Default level: warn · Added in 0.0.1-alpha.16 · Related issues · -View source +View source @@ -572,7 +572,7 @@ old_func() # error: [deprecated] Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -605,7 +605,7 @@ false positives it can produce. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -636,7 +636,7 @@ class B(A, A): ... # error Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -679,7 +679,7 @@ class A: # error Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -756,7 +756,7 @@ def foo() -> "intt\b": ... # error Default level: warn · Added in 0.0.50 · Related issues · -View source +View source @@ -796,7 +796,7 @@ def g(value: ~A) -> None: ... # error: [experimental-syntax] Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -831,7 +831,7 @@ def my_function() -> int: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -947,7 +947,7 @@ def test() -> "Literal[5]": Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -983,7 +983,7 @@ class C(A, B): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1013,7 +1013,7 @@ t[3] # error Default level: warn · Added in 0.0.1-alpha.33 · Related issues · -View source +View source @@ -1050,7 +1050,7 @@ class MyClass: ... Default level: error · Added in 0.0.1-alpha.12 · Related issues · -View source +View source @@ -1151,7 +1151,7 @@ an atypical memory layout. Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1183,7 +1183,7 @@ func("foo") # error: [invalid-argument-type] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1214,7 +1214,7 @@ a: int = "" # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1272,7 +1272,7 @@ C.instance_only_var = 56 # error Default level: error · Added in 0.0.33 · Related issues · -View source +View source @@ -1318,7 +1318,7 @@ class Sub(Base): Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -1360,7 +1360,7 @@ asyncio.run(main()) Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1387,7 +1387,7 @@ class A(42): ... # error: [invalid-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1417,7 +1417,7 @@ with 1: # error Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1470,7 +1470,7 @@ See: Default level: error · Added in 0.0.13 · Related issues · -View source +View source @@ -1506,7 +1506,7 @@ class A: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1538,7 +1538,7 @@ a: str # error Default level: warn · Added in 0.0.20 · Related issues · -View source +View source @@ -1595,7 +1595,7 @@ class Pet(Enum): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1659,7 +1659,7 @@ This rule corresponds to Ruff's [`except-with-non-exception-classes` (`B030`)](h Default level: error · Added in 0.0.1-alpha.28 · Related issues · -View source +View source @@ -1712,7 +1712,7 @@ class D(A): Default level: error · Added in 0.0.1-alpha.35 · Related issues · -View source +View source @@ -1763,7 +1763,7 @@ class NonFrozenChild(FrozenBase): # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -1812,7 +1812,7 @@ class D(Generic[U, T]): ... # error Default level: error · Added in 0.0.12 · Related issues · -View source +View source @@ -1908,7 +1908,7 @@ a = 20 / 0 # type: ignore Default level: error · Added in 0.0.1-alpha.17 · Related issues · -View source +View source @@ -1956,7 +1956,7 @@ carol = Person(name="Carol", aeg=25) # typo! Default level: warn · Added in 0.0.15 · Related issues · -View source +View source @@ -2018,7 +2018,7 @@ def f(x, y, /): # Python 3.8+ syntax Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2058,7 +2058,7 @@ def f(t: TypeVar("U")): ... # ty: ignore[invalid-type-form] Default level: error · Added in 0.0.18 · Related issues · -View source +View source @@ -2108,7 +2108,7 @@ match object(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2143,7 +2143,7 @@ class B(metaclass=42): ... # error Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -2261,7 +2261,7 @@ Correct use of `@override` is enforced by ty's [`invalid-explicit-override`](#in Default level: error · Added in 0.0.1-alpha.19 · Related issues · -View source +View source @@ -2328,7 +2328,7 @@ TypeError: typing.ClassVar[int] is not valid as type argument Default level: warn · Added in 0.0.31 · Related issues · -View source +View source @@ -2376,7 +2376,7 @@ admin[0] # "Alice" Default level: error · Added in 0.0.1-alpha.27 · Related issues · -View source +View source @@ -2414,7 +2414,7 @@ Baz = NewType("Baz", int | str) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2471,7 +2471,7 @@ def foo(x: int) -> int: ... Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2500,7 +2500,7 @@ def f(a: int = ""): ... # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2536,7 +2536,7 @@ P2 = ParamSpec() # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2572,7 +2572,7 @@ TypeError: Protocols can only inherit from other protocols, got Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2643,7 +2643,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2675,7 +2675,7 @@ def func() -> int: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2786,7 +2786,7 @@ class C: ... Default level: error · Added in 0.0.10 · Related issues · -View source +View source @@ -2837,7 +2837,7 @@ class MyClass: Default level: error · Added in 0.0.1-alpha.6 · Related issues · -View source +View source @@ -2883,7 +2883,7 @@ InvalidAlias = TypeAliasType("InvalidAlias", list[T], type_params=(list[T],)) # Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -2950,7 +2950,7 @@ Bar[int] # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -2983,7 +2983,7 @@ TYPE_CHECKING = "" # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3019,7 +3019,7 @@ b: Annotated[int] # error Default level: error · Added in 0.0.1-alpha.11 · Related issues · -View source +View source @@ -3076,7 +3076,7 @@ class C: Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -3120,7 +3120,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3177,7 +3177,7 @@ V = TypeVar("V", list[int], int) # valid constrained Type Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -3219,7 +3219,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau Default level: error · Added in 0.0.28 · Related issues · -View source +View source @@ -3255,7 +3255,7 @@ class Child(Base): Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -3298,7 +3298,7 @@ def f(options: dict[str, object]): Default level: error · Added in 0.0.9 · Related issues · -View source +View source @@ -3333,7 +3333,7 @@ class Foo(TypedDict): Default level: error · Added in 0.0.25 · Related issues · -View source +View source @@ -3368,7 +3368,7 @@ def gen() -> Iterator[int]: Default level: error · Added in 0.0.14 · Related issues · -View source +View source @@ -3435,7 +3435,7 @@ def h(arg2: type): Default level: error · Added in 0.0.15 · Related issues · -View source +View source @@ -3485,7 +3485,7 @@ def g(arg: object): Default level: warn · Added in 0.0.30 · Related issues · -View source +View source @@ -3528,7 +3528,7 @@ Movie = TypedDict("Film", {"title": str}) # error: [mismatched-type-name] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3559,7 +3559,7 @@ func() # error Default level: ignore · Added in 0.0.41 · Related issues · -View source +View source @@ -3618,7 +3618,7 @@ class ExplicitChild(Parent): Default level: ignore · Added in 0.0.45 · Related issues · -View source +View source @@ -3657,7 +3657,7 @@ def handle(m: re.Match[str]) -> str: Default level: error · Added in 0.0.1-alpha.20 · Related issues · -View source +View source @@ -3696,7 +3696,7 @@ alice["age"] # KeyError Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3734,7 +3734,7 @@ func("string") # error: [no-matching-overload] Default level: error · Added in 0.0.30 · Related issues · -View source +View source @@ -3772,7 +3772,7 @@ class Sub(Super): ... # error: [non-callable-init-subclass] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3801,7 +3801,7 @@ for i in 34: # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3829,7 +3829,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt Default level: error · Added in 0.0.1-alpha.29 · Related issues · -View source +View source @@ -3866,7 +3866,7 @@ class B(A): Default level: error · Added in 0.0.16 · Related issues · -View source +View source @@ -3903,7 +3903,7 @@ class B(A): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -3934,7 +3934,7 @@ f(1, x=2) # error Default level: error · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -3965,7 +3965,7 @@ f(x=1) # error Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4004,7 +4004,7 @@ A.c # error Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4043,7 +4043,7 @@ A()[0] # error Default level: ignore · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -4089,7 +4089,7 @@ from module import a # error Default level: warn · Added in 0.0.23 · Related issues · -View source +View source @@ -4121,7 +4121,7 @@ html.parser # error Default level: ignore · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4158,7 +4158,7 @@ print(x) # error Default level: warn · Added in 0.0.60 · Related issues · -View source +View source @@ -4233,7 +4233,7 @@ def test() -> "int": Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4268,7 +4268,7 @@ cast(int, f()) # error Default level: warn · Added in 0.0.18 · Related issues · -View source +View source @@ -4306,7 +4306,7 @@ class C: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -4350,7 +4350,7 @@ class Outer[T]: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4385,7 +4385,7 @@ static_assert(int(2.0 * 3.0) == 6) # error Default level: warn · Added in 0.0.39 · Related issues · -View source +View source @@ -4436,7 +4436,7 @@ Consider using [`functools.total_ordering`][total_ordering] instead, which does Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4470,7 +4470,7 @@ class B(A): ... # error Default level: error · Added in 0.0.1-alpha.30 · Related issues · -View source +View source @@ -4510,7 +4510,7 @@ class F(NamedTuple): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4540,7 +4540,7 @@ f("foo") # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4579,7 +4579,7 @@ def _(x: int): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4637,7 +4637,7 @@ class A: Default level: error · Added in 0.0.20 · Related issues · -View source +View source @@ -4681,7 +4681,7 @@ class C(Generic[T]): Default level: warn · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4710,7 +4710,7 @@ reveal_type(1) # revealed: Literal[1] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4741,7 +4741,7 @@ f(x=1, y=2) # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4774,7 +4774,7 @@ A().foo # error Default level: warn · Added in 0.0.1-alpha.15 · Related issues · -View source +View source @@ -4849,7 +4849,7 @@ def g(): Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4878,7 +4878,7 @@ import foo # error Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -4906,7 +4906,7 @@ print(x) # error Default level: ignore · Added in 0.0.70 · Related issues · -View source +View source @@ -5026,6 +5026,11 @@ This rule is disabled by default. It is intended for advanced users wanting addi checks from their type checker, not for users who have just started to use type checkers on their Python code. +**See also** + + +- [`unsound-yield`](#unsound-yield) is a similar rule that triggers on unsound `yield` expressions rather than unsound `return` statements + [ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ [ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/ [ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ @@ -5038,13 +5043,156 @@ Python code. [subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype [warn-return-any]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-warn-return-any +## `unsound-yield` + + +Default level: ignore · +Added in 0.0.70 · +Related issues · +View source + + + +**What it does** + + +Detects `yield` and `yield from` expressions that unsoundly yield a type that is not a [subtype] of +the generator function's annotated yield type. + +This lint is a stricter version of [`invalid-yield`](#invalid-yield). + +**Why is this bad?** + + +By default, type checkers consider a yielded value valid if its inferred type is [assignable] to the +generator's annotated yield type. However, this +makes it easy for incorrect types to percolate through your code unexpectedly due to a single +expression being inferred as `Any`. This can easily lead to runtime errors that are not caught by +the type checker: + +```py +from typing import Any, Generator + + +def returns_any() -> Any: + return "not an integer" + + +def integers() -> Generator[int]: + # error: "Unsound `yield`: `Any` is not a subtype of `int`" + yield returns_any() + + +# Fails at runtime, even though the type checker infers `integers` as yielding only `int`s! +sum(integers()) +``` + +This rule treats [fully static][fully-static] yield types as "typed boundaries" for your code. With this rule enabled, ty would emit an error on the `yield returns_any()` statement +in `integers`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not +a subtype of `int`. This helps prevent the unsoundness from spreading far from its original source +(in this case, the return type of the `returns_any` function). + +Note that this rule is only applied to functions annotated as yielding +[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in +your function's yield type, either implicitly or explicitly. It will still trigger on functions that have non-fully-static send and/or return types, however: + +```py +from typing import Any, Generator + + +def returns_any() -> Any: + return "not an integer" + + +def dynamic_yield_type() -> Generator[Any]: + yield returns_any() + + +def static_yield_type() -> Generator[int, Any, Any]: + # error: "Unsound `yield`: `Any` is not a subtype of `int`" + yield returns_any() +``` + +This rule works especially well when combined with ty's +[`missing-type-argument`](#missing-type-argument) rule, and the Ruff rules [`ANN201`][ann201], +[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all +these rules at once effectively makes it much less likely that a `yield` expression can lead to +unsoundness "leaking" out of a function unless that function has been *explicitly* annotated with +a dynamic type in some way (`-> Generator[Any]` or `-> Generator[tuple[Any]]`, for example). + +**Examples** + + +```py +from typing import Any, Iterator + + +def returns_any() -> Any: + return "foo" + + +def any_iterator() -> Iterator[Any]: + yield "foo" + + +def integers() -> Iterator[int]: + # error: "Unsound `yield`: `Any` is not a subtype of `int`" + yield returns_any() + # error: "Unsound `yield from`: `Any` is not a subtype of `int`" + yield from any_iterator() +``` + +Narrow the value before yielding it to fix the diagnostics: + +```py +from typing import Any, Iterator + + +def returns_any() -> Any: + return 42 + + +def any_iterator() -> Iterator[Any]: + yield "foo" + + +def integers() -> Iterator[int]: + value = returns_any() + assert isinstance(value, int) + yield value + + for value in any_iterator(): + assert isinstance(value, int) + yield value +``` + +**Default level** + + +This rule is disabled by default. It is intended for users who want stricter soundness checks at +generator boundaries. + +**See also** + + +- [`unsound-return-statement`](#unsound-return-statement) is a similar rule that triggers on unsound `return` statements rather than unsound `yield` expressions + +[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ +[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/ +[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ +[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/ +[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/ +[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable +[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type +[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype + ## `unsupported-base` Default level: warn · Added in 0.0.1-alpha.7 · Related issues · -View source +View source @@ -5091,7 +5239,7 @@ class D(C): ... # error: [unsupported-base] Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5140,7 +5288,7 @@ b1 < b2 < b1 # error Default level: ignore · Added in 0.0.12 · Related issues · -View source +View source @@ -5187,7 +5335,7 @@ def factory(base: type[Base]) -> type: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source @@ -5220,7 +5368,7 @@ A() + A() # error Default level: warn · Added in 0.0.21 · Related issues · -View source +View source @@ -5340,7 +5488,7 @@ to `false`. Default level: warn · Added in 0.0.1-alpha.22 · Related issues · -View source +View source @@ -5419,7 +5567,7 @@ def foo(x: int | str) -> int | str: Default level: error · Added in 0.0.1-alpha.1 · Related issues · -View source +View source diff --git a/crates/ty_python_semantic/resources/lint_docs/unsound-return-statement.md b/crates/ty_python_semantic/resources/lint_docs/unsound-return-statement.md index 3d8fb9bc5b..85e5cf5fc8 100644 --- a/crates/ty_python_semantic/resources/lint_docs/unsound-return-statement.md +++ b/crates/ty_python_semantic/resources/lint_docs/unsound-return-statement.md @@ -110,6 +110,10 @@ This rule is disabled by default. It is intended for advanced users wanting addi checks from their type checker, not for users who have just started to use type checkers on their Python code. +## See also + +- `unsound-yield` is a similar rule that triggers on unsound `yield` expressions rather than unsound `return` statements + [ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ [ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/ [ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ diff --git a/crates/ty_python_semantic/resources/lint_docs/unsound-yield.md b/crates/ty_python_semantic/resources/lint_docs/unsound-yield.md new file mode 100644 index 0000000000..0944ad3e51 --- /dev/null +++ b/crates/ty_python_semantic/resources/lint_docs/unsound-yield.md @@ -0,0 +1,127 @@ +## What it does + +Detects `yield` and `yield from` expressions that unsoundly yield a type that is not a [subtype] of +the generator function's annotated yield type. + +This lint is a stricter version of `invalid-yield`. + +## Why is this bad? + +By default, type checkers consider a yielded value valid if its inferred type is [assignable] to the +generator's annotated yield type. However, this +makes it easy for incorrect types to percolate through your code unexpectedly due to a single +expression being inferred as `Any`. This can easily lead to runtime errors that are not caught by +the type checker: + +```py +from typing import Any, Generator + + +def returns_any() -> Any: + return "not an integer" + + +def integers() -> Generator[int]: + # error: "Unsound `yield`: `Any` is not a subtype of `int`" + yield returns_any() + + +# Fails at runtime, even though the type checker infers `integers` as yielding only `int`s! +sum(integers()) +``` + +This rule treats [fully static][fully-static] yield types as "typed boundaries" for your code. With this rule enabled, ty would emit an error on the `yield returns_any()` statement +in `integers`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not +a subtype of `int`. This helps prevent the unsoundness from spreading far from its original source +(in this case, the return type of the `returns_any` function). + +Note that this rule is only applied to functions annotated as yielding +[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in +your function's yield type, either implicitly or explicitly. It will still trigger on functions that have non-fully-static send and/or return types, however: + +```py +from typing import Any, Generator + + +def returns_any() -> Any: + return "not an integer" + + +def dynamic_yield_type() -> Generator[Any]: + yield returns_any() + + +def static_yield_type() -> Generator[int, Any, Any]: + # error: "Unsound `yield`: `Any` is not a subtype of `int`" + yield returns_any() +``` + +This rule works especially well when combined with ty's +`missing-type-argument` rule, and the Ruff rules [`ANN201`][ann201], +[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all +these rules at once effectively makes it much less likely that a `yield` expression can lead to +unsoundness "leaking" out of a function unless that function has been *explicitly* annotated with +a dynamic type in some way (`-> Generator[Any]` or `-> Generator[tuple[Any]]`, for example). + +## Examples + +```py +from typing import Any, Iterator + + +def returns_any() -> Any: + return "foo" + + +def any_iterator() -> Iterator[Any]: + yield "foo" + + +def integers() -> Iterator[int]: + # error: "Unsound `yield`: `Any` is not a subtype of `int`" + yield returns_any() + # error: "Unsound `yield from`: `Any` is not a subtype of `int`" + yield from any_iterator() +``` + +Narrow the value before yielding it to fix the diagnostics: + +```py +from typing import Any, Iterator + + +def returns_any() -> Any: + return 42 + + +def any_iterator() -> Iterator[Any]: + yield "foo" + + +def integers() -> Iterator[int]: + value = returns_any() + assert isinstance(value, int) + yield value + + for value in any_iterator(): + assert isinstance(value, int) + yield value +``` + +## Default level + +This rule is disabled by default. It is intended for users who want stricter soundness checks at +generator boundaries. + +## See also + +- `unsound-return-statement` is a similar rule that triggers on unsound `return` statements rather than unsound `yield` expressions + +[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ +[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/ +[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/ +[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/ +[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/ +[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable +[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type +[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype diff --git a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md index dd0489da6a..45091d4971 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md @@ -362,3 +362,267 @@ error[invalid-return-type]: Return type does not match returned value info: type `Literal[1]` is not assignable to protocol `Generator[int, int, None]` info: └── protocol member `__iter__` is not defined on type `Literal[1]` ``` + +## *Unsound* yield expressions + +In addition to `invalid-yield`, we also offer a disabled-by-default stricter rule `unsound-yield`. +This rule forbids `yield` expressions that yield an instance of a type `A` unless `A` is a *subtype* +of the annotated yield type: + +```toml +[rules] +unsound-yield = "error" +``` + +```py +from typing import Any, Generator, Iterator + +def returns_any() -> Any: + return "not an integer" + +def generator() -> Generator[int]: + # snapshot: unsound-yield + yield returns_any() +``` + +```snapshot +error[unsound-yield]: Unsound `yield` + --> src/mdtest_snippet.py:8:11 + | +6 | def generator() -> Generator[int]: + | -------------- Expected a subtype of `int` because of the yield type +7 | # snapshot: unsound-yield +8 | yield returns_any() + | ^^^^^^^^^^^^^ Inferred as `Any` +info: `Any` is assignable to `int`, but not a subtype of `int` +help: Consider using an `assert` to narrow the type before yielding it +``` + +The same check applies to generators annotated as iterators. Values that are not even assignable to +the annotated yield type still cause us to emit only `invalid-yield`. + +```py +def iterator() -> Iterator[int]: + yield returns_any() # error: [unsound-yield] + +def invalid_generator() -> Generator[int]: + yield "not an integer" # error: [invalid-yield] +``` + +Narrowing a dynamic value before yielding it makes the yield sound. + +```py +def narrowed_generator() -> Generator[int]: + value = returns_any() + assert isinstance(value, int) + yield value + +def unannotated_generator(): + yield returns_any() +``` + +An example with nested error context: + +```py +def nested_generator() -> Generator[tuple[tuple[int, int]]]: + # snapshot: unsound-yield + yield ((42, returns_any()),) +``` + +```snapshot +error[unsound-yield]: Unsound `yield` + --> src/mdtest_snippet.py:23:11 + | +21 | def nested_generator() -> Generator[tuple[tuple[int, int]]]: + | --------------------------------- Expected a subtype of `tuple[tuple[int, int]]` because of the yield type +22 | # snapshot: unsound-yield +23 | yield ((42, returns_any()),) + | ^^^^^^^^^^^^^^^^^^^^^^ Inferred as `tuple[tuple[Literal[42], Any]]` +info: `tuple[tuple[Literal[42], Any]]` is assignable to `tuple[tuple[int, int]]`, but not a subtype of `tuple[tuple[int, int]]` +info: the first tuple element is not compatible: `tuple[Literal[42], Any]` is not a subtype of `tuple[int, int]` +info: └── the second tuple element is not compatible: `Any` is not a subtype of `int` +help: Consider using an `assert` to narrow the type before yielding it +``` + +## Unsound yield statements with gradual yield types + +The rule applies only when the annotated yield type is fully static. An explicit `Any`, an alias of +`Any`, or an `Any` nested inside the yield type disables the strict check. + +```toml +[rules] +unsound-yield = "error" +``` + +```py +from typing import Any, Generator, Iterator +from typing_extensions import Never, TypeAliasType + +AnyAlias = TypeAliasType("AnyAlias", Any) + +def returns_any() -> Any: + return "not an integer" + +def dynamic_yield_type() -> Generator[Any]: + yield returns_any() + +def aliased_dynamic_yield_type() -> Generator[AnyAlias]: + yield returns_any() + +def nested_dynamic_yield_type() -> Iterator[tuple[int, Any]]: + yield returns_any() + +# error: [missing-type-argument] +def unknown_yield_type() -> Iterator: + yield returns_any() +``` + +Only the yield type determines whether the boundary is fully static; dynamic send and return types +do not disable the check. `Never` is also a fully static yield type. + +```py +def dynamic_send_and_return_types() -> Generator[int, Any, Any]: + yield returns_any() # error: [unsound-yield] + +def never_yields() -> Generator[Never]: + yield returns_any() # error: [unsound-yield] +``` + +## Unsound delegated yield expressions + +`yield from` exposes every value produced by the delegated iterator, so its element type must also +be a subtype of the outer generator's fully static yield type. + +```toml +[rules] +unsound-yield = "error" +``` + +```py +from typing import Any, Generator, Iterator + +def dynamic_values() -> Generator[Any]: + yield "not an integer" + +def delegated_generator() -> Generator[int]: + # snapshot: unsound-yield + yield from dynamic_values() +``` + +```snapshot +error[unsound-yield]: Unsound `yield from` + --> src/mdtest_snippet.py:8:16 + | +6 | def delegated_generator() -> Generator[int]: + | -------------- Expected a subtype of `int` because of the yield type +7 | # snapshot: unsound-yield +8 | yield from dynamic_values() + | ^^^^^^^^^^^^^^^^ Yielded elements inferred as `Any` +info: `Any` is assignable to `int`, but not a subtype of `int` +help: Consider using `assert`s to narrow the types of the elements before yielding them +``` + +Nested dynamic values are rejected too, while genuinely incompatible iterators cause us to emit +`invalid-yield` instead. + +```py +def nested_dynamic_values() -> Iterator[tuple[int, Any]]: + yield (1, "not an integer") + +def nested_delegated_generator() -> Iterator[tuple[int, int]]: + yield from nested_dynamic_values() # error: [unsound-yield] + +def invalid_delegated_generator() -> Iterator[int]: + yield from ["not an integer"] # error: [invalid-yield] + +def valid_delegated_generator() -> Iterator[int]: + yield from [1, 2] +``` + +## Edge case: `unsound-yield` combined with `yield from` expressions that are not iterable + +```toml +[rules] +unsound-yield = "error" +``` + +In the following situation, we only emit `not-iterable`, even though the inferred `yield` type here +is `Unknown` (not a subtype of `int`). Also emitting `unsound-yield` here would just add confusing +noise to our diagnostics: `Unknown` is just a fallback type here that we "spun out of thin air" +because `42` has no `__iter__` method to tell us any better. + +```py +from typing import Iterable, Iterator, Any + +def non_iterable_delegated_generator() -> Iterator[int]: + # Here we only emit `not-iterable`, even though the inferred yield type here + # is `Unknown`: also emitting `unsound-yield` would just add noise + yield from 42 # error: [not-iterable] +``` + +But the following situation is different: here we emit both `not-iterable` *and* `unsound-yield`, +because `Any` was not simply a fallback here that we "invented out of thin air". It's the annotated +iterable type of `BrokenIterable`'s `__iter__` method: + +```py +class BrokenIterable: + def __iter__(self, oh_no) -> Iterator[Any]: + raise NotImplementedError + +def broken_iterable_delegated_generator() -> Iterator[int]: + # snapshot: not-iterable + # snapshot: unsound-yield + yield from BrokenIterable() +``` + +```snapshot +error[not-iterable]: Object of type `BrokenIterable` is not iterable + --> src/mdtest_snippet.py:14:16 + | +14 | yield from BrokenIterable() + | ^^^^^^^^^^^^^^^^ +info: Its `__iter__` method has an invalid signature +info: type `BrokenIterable` is not assignable to protocol `Iterable[Unknown]` +info: └── protocol member `__iter__` is incompatible +info: └── unexpected extra parameter `oh_no` +help: Parameter `oh_no` must have a default value +info: Expected signature `def __iter__(self): ...` + + +error[unsound-yield]: Unsound `yield from` + --> src/mdtest_snippet.py:14:16 + | +11 | def broken_iterable_delegated_generator() -> Iterator[int]: + | ------------- Expected a subtype of `int` because of the yield type +12 | # snapshot: not-iterable +13 | # snapshot: unsound-yield +14 | yield from BrokenIterable() + | ^^^^^^^^^^^^^^^^ Yielded elements inferred as `Any` +info: `Any` is assignable to `int`, but not a subtype of `int` +help: Consider using `assert`s to narrow the types of the elements before yielding them +``` + +## Unsound asynchronous yield statements + +The strict yield check also applies to asynchronous generators and asynchronous iterators. + +```toml +[rules] +unsound-yield = "error" +``` + +```py +from typing import Any, AsyncGenerator, AsyncIterator + +def returns_any() -> Any: + return "not an integer" + +async def asynchronous_generator() -> AsyncGenerator[int]: + yield returns_any() # error: [unsound-yield] + +async def asynchronous_iterator() -> AsyncIterator[int]: + yield returns_any() # error: [unsound-yield] + +async def dynamic_asynchronous_generator() -> AsyncGenerator[Any]: + yield returns_any() +``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index b7b3a43ad1..715a178749 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -84,6 +84,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&INVALID_RETURN_TYPE); registry.register_lint(&UNSOUND_RETURN_STATEMENT); registry.register_lint(&INVALID_YIELD); + registry.register_lint(&UNSOUND_YIELD); registry.register_lint(&INVALID_ASSIGNMENT); registry.register_lint(&INVALID_AWAIT); registry.register_lint(&INVALID_BASE); @@ -446,6 +447,15 @@ declare_lint! { } } +declare_lint! { + #[doc = include_str!("../../resources/lint_docs/unsound-yield.md")] + pub(crate) static UNSOUND_YIELD = { + summary: "detects yield expressions that unsoundly yield a type that is not a subtype of the generator's annotated yield type", + status: LintStatus::stable("0.0.70"), + default_level: Level::Ignore, + } +} + declare_lint! { #[doc = include_str!("../../resources/lint_docs/empty-body.md")] pub(crate) static EMPTY_BODY = { @@ -2150,6 +2160,78 @@ pub(super) fn report_invalid_generator_yield_type( error_context.attach_to(db, env, &mut diag); } +pub(super) fn report_unsound_yield( + context: &InferContext, + yield_value: impl Ranged, + kind: YieldKind, + return_type_span: Option, + expected_ty: Type, + actual_ty: Type, +) { + let db = context.db(); + let Some(builder) = context.report_lint(&UNSOUND_YIELD, yield_value) else { + return; + }; + + let env = context.program_environment(); + let settings = + DisplaySettings::from_possibly_ambiguous_types(db, env, [expected_ty, actual_ty]); + let actual_display = actual_ty.display_with(db, env, settings.clone()); + let expected_display = expected_ty.display_with(db, env, settings); + + let mut diagnostic = builder.into_diagnostic(format_args!("Unsound `{kind}`")); + diagnostic.set_concise_message(format_args!( + "Unsound `{kind}`: `{actual_display}` is not a subtype of `{expected_display}`" + )); + + match kind { + YieldKind::Yield => diagnostic + .set_primary_annotation_message(format_args!("Inferred as `{actual_display}`")), + YieldKind::YieldFrom => diagnostic.set_primary_annotation_message(format_args!( + "Yielded elements inferred as `{actual_display}`" + )), + } + + if let Some(return_type_span) = return_type_span { + diagnostic.annotate( + Annotation::secondary(return_type_span).message(format_args!( + "Expected a subtype of `{expected_display}` because of the yield type" + )), + ); + } + + diagnostic.info(format_args!( + "`{actual_display}` is assignable to `{expected_display}`, \ + but not a subtype of `{expected_display}`" + )); + let error_context = actual_ty.pure_redundancy_error_context(db, env, expected_ty); + error_context.attach_to(db, env, &mut diagnostic); + + match kind { + YieldKind::Yield => { + diagnostic.help("Consider using an `assert` to narrow the type before yielding it"); + } + YieldKind::YieldFrom => diagnostic.help( + "Consider using `assert`s to narrow the types of the elements before yielding them", + ), + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub(super) enum YieldKind { + Yield, + YieldFrom, +} + +impl std::fmt::Display for YieldKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + YieldKind::Yield => f.write_str("yield"), + YieldKind::YieldFrom => f.write_str("yield from"), + } + } +} + pub(super) fn report_implicit_return_type( context: &InferContext, range: impl Ranged, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index c67a1f9bee..26a990e038 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use compact_str::CompactString; use itertools::Itertools; +use ruff_db::diagnostic::Span; use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; use ruff_db::source::source_text; @@ -61,10 +62,10 @@ use crate::types::diagnostic::{ INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM, INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPE_VARIABLE_DEFAULT, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_SUBMODULE, TypeCheckDiagnostics, - UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE, - UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, hint_if_stdlib_attribute_exists_on_other_versions, - report_attempted_protocol_instantiation, report_bad_dunder_delattr_call, - report_bad_dunder_delete_call, report_call_to_abstract_method, + UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE, UNSOUND_YIELD, + UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE, YieldKind, + hint_if_stdlib_attribute_exists_on_other_versions, report_attempted_protocol_instantiation, + report_bad_dunder_delattr_call, report_bad_dunder_delete_call, report_call_to_abstract_method, report_cannot_pop_required_field_on_typed_dict, report_invalid_assignment, report_invalid_class_match_pattern, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, @@ -74,8 +75,8 @@ use crate::types::diagnostic::{ report_match_pattern_against_non_runtime_checkable_protocol, report_match_pattern_against_typed_dict, report_mismatched_type_name, report_possibly_missing_attribute, report_possibly_unresolved_reference, - report_too_many_positional_patterns_for_class_pattern, report_unsupported_augmented_assignment, - report_unsupported_comparison, + report_too_many_positional_patterns_for_class_pattern, report_unsound_yield, + report_unsupported_augmented_assignment, report_unsupported_comparison, }; use crate::types::enums::{enum_ignored_names, is_enum_class_by_inheritance}; use crate::types::function::{ @@ -9379,16 +9380,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .as_deref() .map_or_else(|| yield_expression.into(), AnyNodeRef::from); - if let Some(expected_yield_ty) = expected_yield_ty - && !yielded_ty.is_assignable_to(db, env, expected_yield_ty) - { - report_invalid_generator_yield_type( - &self.context, + if let Some(expected_yield_ty) = expected_yield_ty { + self.validate_generator_yield_type( diagnostic_node, + YieldKind::Yield, return_type_span, expected_yield_ty, yielded_ty, - GeneratorMismatchKind::YieldType, ); } @@ -9427,24 +9425,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { })); let iterable_type = self.infer_expression(value, tcx); - let inner_yield_ty = iterable_type - .try_iterate(db, env) - .map(|tuple| tuple.homogeneous_element_type(db, env)) - .unwrap_or_else(|err| { - err.report_diagnostic(&self.context, iterable_type, value.as_ref().into()); - err.fallback_element_type(db, env) - }); + let known_inner_yield_type = match iterable_type.try_iterate(db, env) { + Ok(tuple) => Some(tuple.homogeneous_element_type(db, env)), + Err(err) => { + err.report_diagnostic(&self.context, iterable_type, AnyNodeRef::from(&**value)); + err.element_type(db, env) + } + }; if let Some(outer_yield_ty) = outer_expected.yield_ty - && !inner_yield_ty.is_assignable_to(db, env, outer_yield_ty) + && let Some(known_inner_yield_type) = known_inner_yield_type { - report_invalid_generator_yield_type( - &self.context, - value.as_ref(), + self.validate_generator_yield_type( + &**value, + YieldKind::YieldFrom, return_type_span.clone(), outer_yield_ty, - inner_yield_ty, - GeneratorMismatchKind::YieldType, + known_inner_yield_type, ); } @@ -9469,6 +9466,43 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .unwrap_or_else(Type::unknown) } + fn validate_generator_yield_type( + &self, + yielded_value: impl Ranged, + yield_kind: YieldKind, + return_type_span: Option, + expected_yield_ty: Type<'db>, + yielded_ty: Type<'db>, + ) { + let db = self.db(); + let env = self.program_environment(); + + if !yielded_ty.is_assignable_to(db, env, expected_yield_ty) { + report_invalid_generator_yield_type( + &self.context, + yielded_value, + return_type_span, + expected_yield_ty, + yielded_ty, + GeneratorMismatchKind::YieldType, + ); + } else if self.context.is_lint_enabled(&UNSOUND_YIELD) + && expected_yield_ty.is_fully_static(db, env) + && !yielded_ty.is_pure_redundant_with(db, env, expected_yield_ty) + { + // N.B. the implementation here is the ~same as for `UNSOUND_RETURN_STATEMENT`; + // update that too if updating this! + report_unsound_yield( + &self.context, + yielded_value, + yield_kind, + return_type_span, + expected_yield_ty, + yielded_ty, + ); + } + } + fn infer_await_expression( &mut self, await_expression: &ast::ExprAwait, diff --git a/crates/ty_python_semantic/src/types/infer/builder/function.rs b/crates/ty_python_semantic/src/types/infer/builder/function.rs index 5450de0fc2..ae2c79e0fc 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/function.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/function.rs @@ -298,6 +298,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { expected_return_ty, ) { + // N.B. the implementation here is the ~same as for `UNSOUND_YIELD`; + // update that too if updating this! report_unsound_return_statement( &self.context, return_statement.range, @@ -365,6 +367,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { TypeRelation::Redundancy { pure: true }, ) { + // N.B. the implementation here is the ~same as for `UNSOUND_YIELD`; + // update that too if updating this! report_unsound_return_statement( &self.context, return_statement.range, diff --git a/crates/ty_python_semantic/src/types/iteration.rs b/crates/ty_python_semantic/src/types/iteration.rs index a679e0541a..f226f061d5 100644 --- a/crates/ty_python_semantic/src/types/iteration.rs +++ b/crates/ty_python_semantic/src/types/iteration.rs @@ -532,7 +532,11 @@ impl<'db> IterationError<'db> { } /// Returns the element type if it is known, or `None` if the type is never iterable. - fn element_type(&self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Option> { + pub(super) fn element_type( + &self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Option> { let return_type = |result: Result, CallDunderError<'db>>| { result .map(|outcome| Some(outcome.return_type(db, env))) diff --git a/crates/ty_test/src/db.rs b/crates/ty_test/src/db.rs index a2bad29593..be206d9f0b 100644 --- a/crates/ty_test/src/db.rs +++ b/crates/ty_test/src/db.rs @@ -331,10 +331,11 @@ fn mdtest_rule_selection(rules: Option<&Rules>, required_rule: Option<&str>) -> // `experimental-syntax` is also an exception: we make use of `&` and `~` for intersection and // negation types in our tests for better readability. "experimental-syntax", - // `unsound-return-statement` is also an exception because it is very strict, would result in - // lots of additional diagnostics in mdtests, and is not the default behaviour we'll show to - // our users. + // The `unsound-*` rules are also exceptions because they are very strict, would + // result in lots of additional diagnostics in mdtests, and are not the default behaviour + // we'll show to our users. "unsound-return-statement", + "unsound-yield", ]; let registry = default_lint_registry(); diff --git a/ty.schema.json b/ty.schema.json index 0956a11078..dc45c5b18a 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -1486,7 +1486,17 @@ }, "unsound-return-statement": { "title": "detects return statements that unsoundly return a type that is not a subtype of the function's annotated return type", - "description": "## What it does\n\nDetects `return` statements that unsoundly return a type that is not a [subtype] of the function's\nannotated return type.\n\nThis lint is a stricter version of `invalid-return-type`.\n\n## Why is this bad?\n\nBy default, type checkers consider a `return` statement valid if the inferred type of the object\nbeing returned is [assignable] to the annotated return type of the function it's in. However, this\nmakes it easy for incorrect types to percolate through your code unexpectedly due to a single\nexpression being inferred as `Any`. This can easily lead to runtime errors that are not caught by\nthe type checker:\n\n```py\nfrom typing import Any\n\n\ndef returns_any() -> Any:\n return \"foo\"\n\n\ndef returns_int() -> int:\n # error: \"Unsound return statement: `Any` is not a subtype of `int`\"\n return returns_any()\n\n\n# fails at runtime, even though the type checker infers both operands as being of type `int`!\nreturns_int() + 42\n```\n\nThis rule allows you to use [\"fully static\"][fully-static] return types as \"typed boundaries\" for\nyour code. With this rule enabled, ty would emit an error on the `return returns_any()` statement\nin `returns_int`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not\na subtype of `int`. This helps prevent the unsoundness from spreading far from its original source\n(in this case, the return type of the `returns_any` function).\n\nNote that this rule is only applied to functions annotated as returning\n[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in\nyour return type, either implicitly or explicitly:\n\n```py\nfrom typing import Any\n\n\ndef returns_any() -> Any:\n return \"foo\"\n\n\n# error: [missing-type-argument]\ndef returns_unparameterized_tuple() -> tuple:\n # no error, since the return type is implicitly `tuple[Unknown, ...]`\n # (which is what the `missing-type-argument` error is complaining about on the line above!)\n return returns_any()\n\n\ndef returns_list_of_any() -> list[Any]:\n # no error, since the return type is explicitly `list[Any]`\n return returns_any()\n```\n\nThis rule works especially well when combined with ty's\n`missing-type-argument` rule, and the Ruff rules [`ANN201`][ann201],\n[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all\nthese rules at once effectively makes it much less likely that a `return` statement can lead to\nunsoundness \"leaking\" out of a function unless that function has been *explicitly* annotated with\na dynamic type in some way (`-> Any` or `-> tuple[Any]`, for example).\n\nThis rule is analogous to mypy's [`no-any-return`][no-any-return] error code, which is enabled by\nmypy’s [`--strict`][mypy-strict] mode and can also be enabled on its own using mypy’s\n[`--warn-return-any`][warn-return-any] option.\n\n## Examples\n\n```py\nfrom typing import Any\n\n\ndef returns_any() -> Any:\n return 42\n\n\ndef returns_int() -> int:\n # error: \"Unsound return statement: `Any` is not a subtype of `int`\"\n return returns_any()\n```\n\nNarrow the type to a subtype of `int` to fix the diagnostic:\n\n```py\nfrom typing import Any\nfrom typing_extensions import reveal_type\n\n\ndef returns_any() -> Any:\n return 42\n\n\ndef returns_int() -> int:\n my_int = returns_any()\n assert isinstance(my_int, int)\n reveal_type(my_int) # revealed: Any & int\n return my_int # no error: `Any & int` is a subtype of `int`\n```\n\n## Default level\n\nThis rule is disabled by default. It is intended for advanced users wanting additional soundness\nchecks from their type checker, not for users who have just started to use type checkers on their\nPython code.\n\n[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/\n[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/\n[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/\n[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/\n[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/\n[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable\n[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type\n[mypy-strict]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict\n[no-any-return]: https://mypy.readthedocs.io/en/stable/error_code_list2.html#code-no-any-return\n[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype\n[warn-return-any]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-warn-return-any", + "description": "## What it does\n\nDetects `return` statements that unsoundly return a type that is not a [subtype] of the function's\nannotated return type.\n\nThis lint is a stricter version of `invalid-return-type`.\n\n## Why is this bad?\n\nBy default, type checkers consider a `return` statement valid if the inferred type of the object\nbeing returned is [assignable] to the annotated return type of the function it's in. However, this\nmakes it easy for incorrect types to percolate through your code unexpectedly due to a single\nexpression being inferred as `Any`. This can easily lead to runtime errors that are not caught by\nthe type checker:\n\n```py\nfrom typing import Any\n\n\ndef returns_any() -> Any:\n return \"foo\"\n\n\ndef returns_int() -> int:\n # error: \"Unsound return statement: `Any` is not a subtype of `int`\"\n return returns_any()\n\n\n# fails at runtime, even though the type checker infers both operands as being of type `int`!\nreturns_int() + 42\n```\n\nThis rule allows you to use [\"fully static\"][fully-static] return types as \"typed boundaries\" for\nyour code. With this rule enabled, ty would emit an error on the `return returns_any()` statement\nin `returns_int`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not\na subtype of `int`. This helps prevent the unsoundness from spreading far from its original source\n(in this case, the return type of the `returns_any` function).\n\nNote that this rule is only applied to functions annotated as returning\n[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in\nyour return type, either implicitly or explicitly:\n\n```py\nfrom typing import Any\n\n\ndef returns_any() -> Any:\n return \"foo\"\n\n\n# error: [missing-type-argument]\ndef returns_unparameterized_tuple() -> tuple:\n # no error, since the return type is implicitly `tuple[Unknown, ...]`\n # (which is what the `missing-type-argument` error is complaining about on the line above!)\n return returns_any()\n\n\ndef returns_list_of_any() -> list[Any]:\n # no error, since the return type is explicitly `list[Any]`\n return returns_any()\n```\n\nThis rule works especially well when combined with ty's\n`missing-type-argument` rule, and the Ruff rules [`ANN201`][ann201],\n[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all\nthese rules at once effectively makes it much less likely that a `return` statement can lead to\nunsoundness \"leaking\" out of a function unless that function has been *explicitly* annotated with\na dynamic type in some way (`-> Any` or `-> tuple[Any]`, for example).\n\nThis rule is analogous to mypy's [`no-any-return`][no-any-return] error code, which is enabled by\nmypy’s [`--strict`][mypy-strict] mode and can also be enabled on its own using mypy’s\n[`--warn-return-any`][warn-return-any] option.\n\n## Examples\n\n```py\nfrom typing import Any\n\n\ndef returns_any() -> Any:\n return 42\n\n\ndef returns_int() -> int:\n # error: \"Unsound return statement: `Any` is not a subtype of `int`\"\n return returns_any()\n```\n\nNarrow the type to a subtype of `int` to fix the diagnostic:\n\n```py\nfrom typing import Any\nfrom typing_extensions import reveal_type\n\n\ndef returns_any() -> Any:\n return 42\n\n\ndef returns_int() -> int:\n my_int = returns_any()\n assert isinstance(my_int, int)\n reveal_type(my_int) # revealed: Any & int\n return my_int # no error: `Any & int` is a subtype of `int`\n```\n\n## Default level\n\nThis rule is disabled by default. It is intended for advanced users wanting additional soundness\nchecks from their type checker, not for users who have just started to use type checkers on their\nPython code.\n\n## See also\n\n- `unsound-yield` is a similar rule that triggers on unsound `yield` expressions rather than unsound `return` statements\n\n[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/\n[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/\n[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/\n[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/\n[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/\n[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable\n[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type\n[mypy-strict]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict\n[no-any-return]: https://mypy.readthedocs.io/en/stable/error_code_list2.html#code-no-any-return\n[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype\n[warn-return-any]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-warn-return-any", + "default": "ignore", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, + "unsound-yield": { + "title": "detects yield expressions that unsoundly yield a type that is not a subtype of the generator's annotated yield type", + "description": "## What it does\n\nDetects `yield` and `yield from` expressions that unsoundly yield a type that is not a [subtype] of\nthe generator function's annotated yield type.\n\nThis lint is a stricter version of `invalid-yield`.\n\n## Why is this bad?\n\nBy default, type checkers consider a yielded value valid if its inferred type is [assignable] to the\ngenerator's annotated yield type. However, this\nmakes it easy for incorrect types to percolate through your code unexpectedly due to a single\nexpression being inferred as `Any`. This can easily lead to runtime errors that are not caught by\nthe type checker:\n\n```py\nfrom typing import Any, Generator\n\n\ndef returns_any() -> Any:\n return \"not an integer\"\n\n\ndef integers() -> Generator[int]:\n # error: \"Unsound `yield`: `Any` is not a subtype of `int`\"\n yield returns_any()\n\n\n# Fails at runtime, even though the type checker infers `integers` as yielding only `int`s!\nsum(integers())\n```\n\nThis rule treats [fully static][fully-static] yield types as \"typed boundaries\" for your code. With this rule enabled, ty would emit an error on the `yield returns_any()` statement\nin `integers`, since the `returns_any()` call is inferred as having type `Any`, and `Any` is not\na subtype of `int`. This helps prevent the unsoundness from spreading far from its original source\n(in this case, the return type of the `returns_any` function).\n\nNote that this rule is only applied to functions annotated as yielding\n[fully static][fully-static] types. It will not trigger if `Any` or `Unknown` appear anywhere in\nyour function's yield type, either implicitly or explicitly. It will still trigger on functions that have non-fully-static send and/or return types, however:\n\n```py\nfrom typing import Any, Generator\n\n\ndef returns_any() -> Any:\n return \"not an integer\"\n\n\ndef dynamic_yield_type() -> Generator[Any]:\n yield returns_any()\n\n\ndef static_yield_type() -> Generator[int, Any, Any]:\n # error: \"Unsound `yield`: `Any` is not a subtype of `int`\"\n yield returns_any()\n```\n\nThis rule works especially well when combined with ty's\n`missing-type-argument` rule, and the Ruff rules [`ANN201`][ann201],\n[`ANN202`][ann202], [`ANN204`][ann204], [`ANN205`][ann205], and [`ANN206`][ann206]. Enabling all\nthese rules at once effectively makes it much less likely that a `yield` expression can lead to\nunsoundness \"leaking\" out of a function unless that function has been *explicitly* annotated with\na dynamic type in some way (`-> Generator[Any]` or `-> Generator[tuple[Any]]`, for example).\n\n## Examples\n\n```py\nfrom typing import Any, Iterator\n\n\ndef returns_any() -> Any:\n return \"foo\"\n\n\ndef any_iterator() -> Iterator[Any]:\n yield \"foo\"\n\n\ndef integers() -> Iterator[int]:\n # error: \"Unsound `yield`: `Any` is not a subtype of `int`\"\n yield returns_any()\n # error: \"Unsound `yield from`: `Any` is not a subtype of `int`\"\n yield from any_iterator()\n```\n\nNarrow the value before yielding it to fix the diagnostics:\n\n```py\nfrom typing import Any, Iterator\n\n\ndef returns_any() -> Any:\n return 42\n\n\ndef any_iterator() -> Iterator[Any]:\n yield \"foo\"\n\n\ndef integers() -> Iterator[int]:\n value = returns_any()\n assert isinstance(value, int)\n yield value\n\n for value in any_iterator():\n assert isinstance(value, int)\n yield value\n```\n\n## Default level\n\nThis rule is disabled by default. It is intended for users who want stricter soundness checks at\ngenerator boundaries.\n\n## See also\n\n- `unsound-return-statement` is a similar rule that triggers on unsound `return` statements rather than unsound `yield` expressions\n\n[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/\n[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/\n[ann204]: https://docs.astral.sh/ruff/rules/missing-return-type-special-method/\n[ann205]: https://docs.astral.sh/ruff/rules/missing-return-type-static-method/\n[ann206]: https://docs.astral.sh/ruff/rules/missing-return-type-class-method/\n[assignable]: https://typing.python.org/en/latest/spec/glossary.html#term-assignable\n[fully-static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type\n[subtype]: https://typing.python.org/en/latest/spec/glossary.html#term-subtype", "default": "ignore", "oneOf": [ { From 68799c4209aeb9bdab0bd79694646af2c5c24ed3 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Sun, 9 Aug 2026 16:20:35 -0400 Subject: [PATCH 343/390] [ty] Two equality constraints are usually disjoint (#27614) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes a small bug in our intersection of constraints: two _equality_ constraints are disjoint if the types in question are not equivalent. When consider upper-bound constraints, this is not true: `(T ≤ Foo) ∧ (T ≤ Bar)` is not empty, since you could define a new class that inherits from both `Foo` and `Bar`. With equality constraints, that doesn't matter: `(T = Foo) ∧ (T = Bar)` is empty. A joint subclass does not satisfy `T = Foo` or `T = Bar` individually, let alone their conjunction. --- .../mdtest/generics/legacy/functions.md | 27 +++++++++++ .../mdtest/type_properties/constraints.md | 41 +++++++++++++++++ .../src/types/constraints.rs | 17 +++++++ .../ty_python_semantic/src/types/relation.rs | 46 ++++++++++++++++++- 4 files changed, 130 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index 2a5b233cbd..447b34ca48 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -436,6 +436,33 @@ def consume_callback(callback: Callable[[Row], None]) -> Row: reveal_type(consume_callback(callback)) # revealed: tuple[Any, ...] ``` +## Incompatible invariant protocol members + +When the same inferred type variable appears in multiple invariant protocol members, those members +must agree on one exact specialization. Gradual consistency between their types is not sufficient. + +```py +from typing import Any, Generic, Protocol, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class Pair(Protocol[T]): + first: T + second: T + +class GradualPair(Generic[U]): + first: tuple[U, Any] + second: tuple[U, int] + +def infer_pair(value: Pair[T]) -> T: + raise NotImplementedError + +def check_pair(value: GradualPair[U]) -> None: + # TODO: error: [invalid-argument-type] "Argument to function `infer_pair` is incorrect" + reveal_type(infer_pair(value)) # revealed: Unknown +``` + ## Prefer specific compatible constraints over gradual constraints A gradual constraint can be compatible with a concrete argument and a more specific declared diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md index 3d18fd8600..eca4a0a144 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/constraints.md @@ -370,6 +370,47 @@ def lower_bounds[T](): static_assert(union_type == intersection_constraint) ``` +### Intersection of two equality constraints + +A type variable cannot be exactly equal to two non-equivalent types. This is stronger than checking +whether the types are disjoint: two classes can have a common subclass, which makes their +upper-bound constraints compatible, but that subclass is not exactly equal to either class. + +```py +from typing import Any +from ty_extensions import static_assert +from ty_extensions._internal import ConstraintSet + +class Row: ... +class RowTuple(Row, tuple[Any, ...]): ... + +def _[T, U, V]() -> None: + row = ConstraintSet.equality(T, Row) + tuple_ = ConstraintSet.equality(T, tuple[Any, ...]) + static_assert(~(row & tuple_)) + + equivalent = row & row + static_assert(equivalent == row) + + upper_bounds = ConstraintSet.upper_bound(T, Row) & ConstraintSet.upper_bound(T, tuple[Any, ...]) + static_assert(not ~upper_bounds) + + row_tuple = ConstraintSet.equality(T, RowTuple) + static_assert(row_tuple & upper_bounds == row_tuple) + + gradual_mismatch = ConstraintSet.equality(T, list[Any]) & ConstraintSet.equality(T, list[int]) + static_assert(~gradual_mismatch) + + any_mismatch = ConstraintSet.equality(T, Any) & ConstraintSet.equality(T, int) + static_assert(~any_mismatch) + + symbolic_mismatch = ConstraintSet.equality(T, tuple[U, Any]) & ConstraintSet.equality(T, tuple[U, int]) + static_assert(~symbolic_mismatch) + + symbolic_match = ConstraintSet.equality(T, list[U]) & ConstraintSet.equality(T, list[V]) + static_assert(not ~symbolic_match) +``` + ### Intersection of a range and a negated range The bounds of the range constraint provide a range of types that should be included; the bounds of diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 54faf59054..0937f9f355 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -1878,6 +1878,12 @@ impl<'db> ConstraintBounds<'db> { self.upper.is_some() } + fn as_equality(self) -> Option> { + let lower = self.lower?; + let upper = self.upper?; + (lower == upper).then_some(lower) + } + fn materialized_lower(self) -> Type<'db> { self.lower.unwrap_or(Type::Never) } @@ -2498,6 +2504,17 @@ impl ConstraintId { let self_constraint = storage.constraint_data(self); let other_constraint = storage.constraint_data(other); + // A typevar cannot be exactly equal to two different types under any specialization. This + // is stronger than checking whether the types are disjoint: two classes can have a common + // subclass, which makes their upper-bound constraints compatible, but that subclass is not + // exactly equal to either class. + if let Some(left) = self_constraint.bounds.as_equality() + && let Some(right) = other_constraint.bounds.as_equality() + && !left.can_be_constraint_set_equivalent_to(db, env, right) + { + return IntersectionResult::Disjoint; + } + // (s₁ ≤ α ≤ t₁) ∧ (s₂ ≤ α ≤ t₂) = (s₁ ∪ s₂) ≤ α ≤ (t₁ ∩ t₂)) let lower = match (self_constraint.bounds.lower, other_constraint.bounds.lower) { (Some(left), Some(right)) => Some(UnionType::from_two_elements(db, env, left, right)), diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index e9718029a5..574263accd 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -765,6 +765,7 @@ impl<'db> Type<'db> { other, &ConstraintSetBuilder::new(), materialization_visitor, + TypeVarEvaluation::Eager, ) .is_always_satisfied(db, materialization_visitor.env) } @@ -782,6 +783,44 @@ impl<'db> Type<'db> { other, constraints, &materialization_visitor, + TypeVarEvaluation::Eager, + ) + } + + /// Returns whether `self` and `other` can be equivalent under some typevar specialization. + pub(super) fn can_be_constraint_set_equivalent_to( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + other: Type<'db>, + ) -> bool { + #[salsa::tracked(returns(copy), cycle_initial=|_, _, _| true, heap_size=ruff_memory_usage::heap_size)] + fn can_be_constraint_set_equivalent_to_impl<'db>( + db: &'db dyn Db, + types: TypePair<'db>, + ) -> bool { + let env = ProgramEnvironment::from_program(types.program(db)); + let constraints = ConstraintSetBuilder::new(); + let materialization_visitor = ApplyTypeMappingVisitor::new(&env); + !types + .first(db) + .when_equivalent_to_with_materialization_visitor( + db, + types.second(db), + &constraints, + &materialization_visitor, + TypeVarEvaluation::Lazy, + ) + .is_never_satisfied(db, &env) + } + + if self == other { + return true; + } + + can_be_constraint_set_equivalent_to_impl( + db, + TypePair::new(db, env.program(db), self, other), ) } @@ -791,6 +830,7 @@ impl<'db> Type<'db> { other: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, materialization_visitor: &ApplyTypeMappingVisitor<'_, 'db>, + typevar_evaluation: TypeVarEvaluation, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); @@ -800,6 +840,7 @@ impl<'db> Type<'db> { constraints, given: ConstraintSet::from_bool(constraints, false), perform_expensive_checks: true, + typevar_evaluation, relation_visitor: &relation_visitor, disjointness_visitor: &disjointness_visitor, signature_relation_visitor: &signature_relation_visitor, @@ -2650,6 +2691,7 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { constraints: self.constraints, given: self.given, perform_expensive_checks: self.perform_expensive_checks, + typevar_evaluation: TypeVarEvaluation::Eager, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, signature_relation_visitor: self.signature_relation_visitor, @@ -2704,6 +2746,7 @@ pub(super) struct EquivalenceChecker<'a, 'c, 'db> { pub(super) constraints: &'c ConstraintSetBuilder<'db>, given: ConstraintSet<'db, 'c>, perform_expensive_checks: bool, + typevar_evaluation: TypeVarEvaluation, // N.B. these fields are private to reduce the risk of // "double-visiting" a given pair of types. You should @@ -2725,7 +2768,7 @@ impl<'c, 'db> EquivalenceChecker<'_, 'c, 'db> { TypeRelationChecker { env: self.env, relation: TypeRelation::Redundancy { pure: true }, - typevar_evaluation: TypeVarEvaluation::Eager, + typevar_evaluation: self.typevar_evaluation, constraints: self.constraints, context_tree: None, given: self.given, @@ -2836,6 +2879,7 @@ impl<'a, 'c, 'db> DisjointnessChecker<'a, 'c, 'db> { constraints: self.constraints, given: self.given, perform_expensive_checks: self.perform_expensive_checks, + typevar_evaluation: TypeVarEvaluation::Eager, relation_visitor: self.relation_visitor, disjointness_visitor: self.disjointness_visitor, signature_relation_visitor: self.signature_relation_visitor, From 18bb80d07b719011a229d14ceb745c031d96c190 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 10 Aug 2026 08:29:16 +0200 Subject: [PATCH 344/390] [ty] Fix signature help at end of file (#27622) --- crates/ty_ide/src/signature_help.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs index c7b00ab43d..ea0725d8b1 100644 --- a/crates/ty_ide/src/signature_help.rs +++ b/crates/ty_ide/src/signature_help.rs @@ -129,6 +129,8 @@ fn get_call_expr( | TokenKind::Complex | TokenKind::Float | TokenKind::Int => 1, + // Prefer the real token immediately before an empty recovery token at EOF. + TokenKind::Unknown => -1, _ => 0, })?; @@ -1256,6 +1258,32 @@ def ab(a: int, *, c: int): assert_eq!(result.active_signature, Some(0)); } + #[test] + fn signature_help_after_opening_paren_at_end_of_file() { + let test = cursor_test( + r#" + def func(first: int, second: str) -> None: ... + + func("#, + ); + + let result = test.signature_help().expect("Should have signature help"); + assert_eq!(result.signatures[0].active_parameter, Some(0)); + } + + #[test] + fn signature_help_after_comma_at_end_of_file() { + let test = cursor_test( + r#" + def func(first: int, second: str) -> None: ... + + func(1,"#, + ); + + let result = test.signature_help().expect("Should have signature help"); + assert_eq!(result.signatures[0].active_parameter, Some(1)); + } + #[test] fn signature_help_after_closing_paren_at_end_of_file() { let test = cursor_test( From 3440632b36ef0bbe9a08c3f3bd7b95d43aed49d6 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 10 Aug 2026 13:31:47 +0200 Subject: [PATCH 345/390] [`pylint`] Allow `continue` in `finally` on Python 3.8 (#27626) --- .../ruff_linter/src/checkers/ast/analyze/statement.rs | 2 +- crates/ruff_linter/src/rules/pylint/mod.rs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index e09bc64071..5d33fdb054 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -1355,7 +1355,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody); } if checker.is_rule_enabled(Rule::ContinueInFinally) { - if checker.target_version() <= PythonVersion::PY38 { + if checker.target_version() < PythonVersion::PY38 { pylint::rules::continue_in_finally(checker, finalbody); } } diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index 2b06835737..ed9e7e7b9a 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -292,6 +292,17 @@ mod tests { Ok(()) } + #[test] + fn continue_in_finally_python_38() -> Result<()> { + let diagnostics = test_path( + Path::new("pylint/continue_in_finally.py"), + &LinterSettings::for_rule(Rule::ContinueInFinally) + .with_target_version(PythonVersion::PY38), + )?; + assert!(diagnostics.is_empty()); + Ok(()) + } + #[test] fn allow_magic_value_types() -> Result<()> { let diagnostics = test_path( From d031eb1fc9b100d6bd5a0d392731c0475757a892 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 10 Aug 2026 13:25:10 +0100 Subject: [PATCH 346/390] [ty] Support Annotated inside type[...] (#27629) --- .../resources/mdtest/annotations/annotated.md | 39 +++++++++++++++++++ .../types/infer/builder/type_expression.rs | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md index a12d756c2e..cf7056bb0e 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/annotated.md @@ -22,6 +22,45 @@ def _(x: Annotated[tuple[str, int], bytes]): reveal_type(x) # revealed: tuple[str, int] ``` +## Inside `type[...]` + +`Annotated` can wrap a class or specialized generic class inside `type[...]` without changing the +resulting class object type. + +```py +from typing_extensions import Annotated + +def _( + simple: type[Annotated[int, "metadata"]], + generic: type[Annotated[list[str], "metadata"]], +): + reveal_type(simple) # revealed: type[int] + reveal_type(generic) # revealed: type[list[str]] +``` + +This also works for unions of classes and nested `Annotated` forms. + +```py +def _( + union: type[Annotated[int | str, "metadata"]], + nested: type[Annotated[Annotated[int, "inner"], "outer"]], +): + reveal_type(union) # revealed: type[int | str] + reveal_type(nested) # revealed: type[int] +``` + +Wrapping a non-class type in `Annotated` does not make it a valid argument to `type[...]`. + +```py +from typing import Callable + +def _( + # error: [invalid-type-form] "The argument to `type[]` must be a class object type" + invalid: type[Annotated[Callable[[], int], "metadata"]], +): + reveal_type(invalid) # revealed: type[Unknown] +``` + ## Parameterization It is invalid to parameterize `Annotated` with less than two arguments. diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index a7cb70ee43..8fbd270ae6 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1378,7 +1378,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { invalid_type_argument(self, slice) } value_ty @ (Type::SpecialForm( - SpecialFormType::Top | SpecialFormType::Bottom, + SpecialFormType::Top | SpecialFormType::Bottom | SpecialFormType::Annotated, ) | Type::KnownInstance(KnownInstanceType::TypeAliasType(_))) => { let slice_ty = self.infer_subscript_type_expression(subscript, value_ty); From c0824de94a1c9bf7c0042625c8a46ed5eafe9794 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 10 Aug 2026 15:30:41 +0100 Subject: [PATCH 347/390] Enable rustfmt on more files (#27630) --- crates/mdtest/src/lib.rs | 22 +- crates/mdtest/src/parser.rs | 56 ++-- crates/ruff_benchmark/benches/ty.rs | 44 ++- crates/ruff_db/src/testing.rs | 14 +- .../ruff_dev/src/generate_ty_cli_reference.rs | 6 +- crates/ruff_formatter/src/builders.rs | 10 +- crates/ruff_formatter/src/printer/mod.rs | 29 +- .../src/checkers/ast/analyze/expression.rs | 4 +- crates/ruff_linter/src/checkers/ast/mod.rs | 24 +- crates/ruff_linter/src/message/mod.rs | 7 +- crates/ruff_linter/src/noqa.rs | 33 ++- .../rules/fastapi_non_annotated_dependency.rs | 12 +- .../flake8_bandit/rules/mako_templates.rs | 4 +- .../flake8_bandit/rules/shell_injection.rs | 24 +- .../rules/suspicious_function_call.rs | 66 +++-- .../flake8_bandit/rules/suspicious_imports.rs | 13 +- .../src/rules/flake8_boolean_trap/helpers.rs | 108 +++---- .../rules/class_as_data_structure.rs | 18 +- .../rules/except_with_empty_tuple.rs | 8 +- .../rules/f_string_docstring.rs | 5 +- .../function_call_in_argument_default.rs | 9 +- .../rules/function_uses_loop_variable.rs | 9 +- .../rules/loop_iterator_mutation.rs | 39 ++- .../rules/map_without_explicit_strict.rs | 5 +- .../rules/reuse_of_groupby_generator.rs | 4 +- .../rules/zip_without_explicit_strict.rs | 9 +- .../rules/format_in_gettext_func_call.rs | 4 +- .../rules/bad_version_info_comparison.rs | 11 +- .../rules/future_annotations_in_stub.rs | 4 +- .../unaliased_collections_abc_set_import.rs | 4 +- .../rules/avoidable_escaped_quote.rs | 12 +- .../rules/check_string_quotes.rs | 6 +- .../src/rules/flake8_return/visitor.rs | 11 +- crates/ruff_linter/src/rules/isort/order.rs | 79 +++--- .../src/rules/numpy/rules/legacy_random.rs | 142 +++++----- .../constant_imported_as_non_constant.rs | 46 +-- .../rules/invalid_escape_sequence.rs | 48 ++-- .../missing_whitespace_around_operator.rs | 16 +- .../rules/pydoclint/rules/check_docstring.rs | 15 +- .../src/rules/pyflakes/rules/unused_import.rs | 67 +++-- .../ruff_linter/src/rules/pylint/helpers.rs | 191 ++++++------- .../rules/repeated_equality_comparison.rs | 11 +- .../pylint/rules/unnecessary_dunder_call.rs | 15 +- .../pylint/rules/useless_else_on_loop.rs | 4 +- .../pyupgrade/rules/deprecated_mock_import.rs | 77 +++-- .../refurb/rules/check_and_remove_from_set.rs | 20 +- crates/ruff_linter/src/rules/ruff/helpers.rs | 4 +- .../ruff/rules/falsy_dict_get_fallback.rs | 4 +- .../rules/parenthesize_chained_operators.rs | 4 +- crates/ruff_linter/src/rules/ruff/typing.rs | 5 +- crates/ruff_macros/src/rule_namespace.rs | 41 ++- crates/ruff_python_ast/src/helpers.rs | 12 +- .../src/comments/placement.rs | 7 +- .../src/expression/expr_attribute.rs | 10 +- .../ruff_python_formatter/src/pattern/mod.rs | 18 +- .../src/statement/suite.rs | 55 ++-- .../src/parser/expression.rs | 8 +- crates/ruff_python_parser/src/parser/mod.rs | 51 ++-- .../src/parser/statement.rs | 44 +-- crates/ruff_python_parser/tests/fixtures.rs | 40 ++- .../src/analyze/typing.rs | 5 +- crates/ruff_python_semantic/src/model/all.rs | 64 +++-- crates/ruff_python_stdlib/src/open_mode.rs | 12 +- crates/ruff_python_trivia/src/pragmas.rs | 25 +- crates/ruff_server/src/edit/notebook.rs | 8 +- crates/ruff_server/src/server/api.rs | 21 +- .../server/api/requests/execute_command.rs | 7 +- crates/ruff_server/src/session/options.rs | 10 +- crates/ruff_server/src/session/settings.rs | 3 +- crates/ruff_workspace/src/configuration.rs | 20 +- crates/ruff_workspace/src/options.rs | 38 ++- crates/ty/src/args.rs | 18 +- crates/ty/src/lib.rs | 27 +- crates/ty_ide/src/completion.rs | 5 +- .../ty_ide/src/docstring/document/google.rs | 18 +- crates/ty_ide/src/folding_range.rs | 9 +- crates/ty_ide/src/inlay_hints.rs | 16 +- crates/ty_module_resolver/src/typeshed.rs | 26 +- crates/ty_project/src/db/changes.rs | 31 ++- crates/ty_project/src/metadata/options.rs | 66 +++-- crates/ty_python_core/src/builder.rs | 29 +- crates/ty_python_semantic/src/types.rs | 263 +++++++++++------- .../src/types/bound_super.rs | 13 +- crates/ty_python_semantic/src/types/class.rs | 16 +- .../src/types/class/known.rs | 25 +- .../src/types/class_base.rs | 35 +-- .../src/types/diagnostic.rs | 88 +++--- .../ty_python_semantic/src/types/display.rs | 19 +- .../ty_python_semantic/src/types/equality.rs | 8 +- .../ty_python_semantic/src/types/generics.rs | 27 +- .../src/types/infer/builder.rs | 139 +++++---- .../src/types/infer/builder/subscript.rs | 247 ++++++++-------- .../types/infer/builder/type_expression.rs | 27 +- .../ty_python_semantic/src/types/iteration.rs | 194 ++++++++----- .../ty_python_semantic/src/types/overrides.rs | 8 +- .../ty_python_semantic/src/types/relation.rs | 10 +- .../src/types/relation_error.rs | 52 ++-- .../src/types/special_form.rs | 16 +- crates/ty_python_semantic/src/types/tuple.rs | 7 +- crates/ty_server/src/document/notebook.rs | 8 +- crates/ty_server/src/server/api.rs | 14 +- crates/ty_server/src/session.rs | 12 +- crates/ty_site_packages/src/lib.rs | 73 +++-- 103 files changed, 2017 insertions(+), 1410 deletions(-) diff --git a/crates/mdtest/src/lib.rs b/crates/mdtest/src/lib.rs index e99d68b34f..d7d226bd36 100644 --- a/crates/mdtest/src/lib.rs +++ b/crates/mdtest/src/lib.rs @@ -408,7 +408,9 @@ pub fn validate_inline_snapshot( failures.push( failure_line, vec![Failure::new( - "This code block has a `snapshot` code block but no `# snapshot` assertions. Remove the `snapshot` code block or add a `# snapshot:` assertion.", + "This code block has a `snapshot` code block but no `# snapshot` \ + assertions. Remove the `snapshot` code block or add a `# snapshot:` \ + assertion.", )], ); } @@ -432,7 +434,8 @@ pub fn validate_inline_snapshot( failures.push( line, vec![Failure::new(format!( - "Add a `snapshot` block for this `# snapshot` assertion, or set `{MDTEST_UPDATE_SNAPSHOTS}=1` to insert one automatically", + "Add a `snapshot` block for this `# snapshot` assertion, \ + or set `{MDTEST_UPDATE_SNAPSHOTS}=1` to insert one automatically", ))], ); } @@ -451,10 +454,14 @@ pub fn validate_inline_snapshot( } else { failures.push( failure_line, - vec![Failure::new(format_args!( - "inline diagnostics snapshot are out of date; set `{MDTEST_UPDATE_SNAPSHOTS}=1` to update the `snapshot` block", - )).with_diff(snapshot_code_block.expected.to_string(), actual)], - ); + vec![ + Failure::new(format_args!( + "inline diagnostics snapshot are out of date; \ + set `{MDTEST_UPDATE_SNAPSHOTS}=1` to update the `snapshot` block", + )) + .with_diff(snapshot_code_block.expected.to_string(), actual), + ], + ); } } @@ -674,7 +681,8 @@ pub fn check_panic(test: &MarkdownTest<'_, '_, C>, panic_info: Option { anyhow::ensure!( value.is_none(), - "The `{SECTION_CONFIG_SNAPSHOT}` directive does not take a value." + "The `{SECTION_CONFIG_SNAPSHOT}` directive \ + does not take a value." ); self.process_mdtest_directive( MdtestDirective::SnapshotDiagnostics, @@ -624,7 +625,8 @@ where SECTION_CONFIG_PULLTYPES => { anyhow::ensure!( value.is_none(), - "The `{SECTION_CONFIG_PULLTYPES}` directive does not take a value." + "The `{SECTION_CONFIG_PULLTYPES}` directive \ + does not take a value." ); self.process_mdtest_directive( MdtestDirective::PullTypesSkip, @@ -637,8 +639,9 @@ where _ => { if !HTML_COMMENT_ALLOWLIST.contains(&html_comment) { bail!( - "Unknown HTML comment `{html_comment}` -- possibly a typo? \ - (Add to `HTML_COMMENT_ALLOWLIST` if this is a false positive)" + "Unknown HTML comment `{html_comment}` -- \ + possibly a typo? (Add to `HTML_COMMENT_ALLOWLIST` \ + if this is a false positive)" ); } } @@ -677,7 +680,8 @@ where if self.preceding_blank_lines < 1 && self.explicit_path.is_none() { bail!( - "Code blocks must start on a new line and be preceded by at least one blank line." + "Code blocks must start on a new line \ + and be preceded by at least one blank line." ); } @@ -694,7 +698,8 @@ where if !self.cursor.eat_char('\n') { bail!( - "Trailing code-block metadata is not supported. Only the code block language can be specified." + "Trailing code-block metadata is not supported. \ + Only the code block language can be specified." ); } @@ -709,7 +714,8 @@ where .any(|attribute| attribute == r#"data-mdtest="ignore""#) } else { bail!( - "Trailing code-block metadata must use the `{{...}}` attribute-list syntax." + "Trailing code-block metadata must use the `{{...}}` \ + attribute-list syntax." ); }; @@ -853,7 +859,9 @@ where { let backtick_start = self.line_number(backtick_offsets.start()); bail!( - "File extension of test file path `{explicit_path}` in test `{test_name}` does not match language specified `{lang}` of code block on line `{backtick_start}`" + "File extension of test file path `{explicit_path}` \ + in test `{test_name}` does not match language specified `{lang}` \ + of code block on line `{backtick_start}`" ); } } @@ -874,12 +882,14 @@ where "ipynb" => EmbeddedFilePath::Autogenerated(PySourceType::Ipynb), "" => { bail!( - "Cannot auto-generate file name for code block with empty language specifier in test `{test_name}`" + "Cannot auto-generate file name for code block \ + with empty language specifier in test `{test_name}`" ); } _ => { bail!( - "Cannot auto-generate file name for code block with language `{lang}` in test `{test_name}`" + "Cannot auto-generate file name for code block \ + with language `{lang}` in test `{test_name}`" ); } }, @@ -892,7 +902,8 @@ where Entry::Vacant(entry) => { if has_merged_snippets { bail!( - "Merged snippets in test `{test_name}` are not allowed in the presence of other files." + "Merged snippets in test `{test_name}` are not allowed \ + in the presence of other files." ); } @@ -920,7 +931,8 @@ where if has_explicit_file_paths { bail!( - "Merged snippets in test `{test_name}` are not allowed in the presence of other files." + "Merged snippets in test `{test_name}` are not allowed \ + in the presence of other files." ); } @@ -970,7 +982,8 @@ where let backtick_start = line_number(offsets.start(), self.source); bail!( - "`snapshot` code block on line {backtick_start} must follow a checkable code block, but section has no files." + "`snapshot` code block on line {backtick_start} \ + must follow a checkable code block, but section has no files." ); }; @@ -980,7 +993,9 @@ where let backtick_start = line_number(offsets.start(), self.source); bail!( - "`snapshot` code block on line {backtick_start} must follow a checkable code block in the same section but it follows a `{}` block.", + "`snapshot` code block on line {backtick_start} \ + must follow a checkable code block in the same section \ + but it follows a `{}` block.", file.lang ); } @@ -993,7 +1008,9 @@ where let existing_start = line_number(existing_block.range.start(), self.source); bail!( - "Code block on line `{code_block_start}` has more than one `snapshot` block: first on line {existing_start} and another on line {backtick_start}.", + "Code block on line `{code_block_start}` \ + has more than one `snapshot` block: first on line {existing_start} \ + and another on line {backtick_start}.", ); } @@ -1651,7 +1668,8 @@ mod tests { let err = parse("file.md", &source).expect_err("Should fail to parse"); assert_eq!( err.to_string(), - "Cannot auto-generate file name for code block with empty language specifier in test `No language specifier`" + "Cannot auto-generate file name for code block \ + with empty language specifier in test `No language specifier`" ); } @@ -1669,7 +1687,8 @@ mod tests { let err = parse("file.md", &source).expect_err("Should fail to parse"); assert_eq!( err.to_string(), - "Cannot auto-generate file name for code block with language `json` in test `JSON test?`" + "Cannot auto-generate file name for code block with language `json` \ + in test `JSON test?`" ); } @@ -1744,7 +1763,8 @@ mod tests { let err = parse("file.md", &source).expect_err("Should fail to parse"); assert_eq!( err.to_string(), - "File extension of test file path `a.py` in test `Accidental stub` does not match language specified `pyi` of code block on line `6`" + "File extension of test file path `a.py` in test `Accidental stub` \ + does not match language specified `pyi` of code block on line `6`" ); } diff --git a/crates/ruff_benchmark/benches/ty.rs b/crates/ruff_benchmark/benches/ty.rs index 8900b21aa8..b6168f459c 100644 --- a/crates/ruff_benchmark/benches/ty.rs +++ b/crates/ruff_benchmark/benches/ty.rs @@ -584,9 +584,12 @@ fn benchmark_narrowed_str_enum_comparison(criterion: &mut Criterion) { fn benchmark_optional_str_enum_comparison(criterion: &mut Criterion) { const NUM_ENUM_MEMBERS: usize = 256; - let mut code = - "from dataclasses import dataclass\nfrom enum import StrEnum\n\nclass ModelSlug(StrEnum):\n" - .to_string(); + let mut code = "from dataclasses import dataclass +from enum import StrEnum + +class ModelSlug(StrEnum): +" + .to_string(); for index in 0..NUM_ENUM_MEMBERS { writeln!(&mut code, " M{index} = \"m{index}\"").ok(); } @@ -620,9 +623,12 @@ def belongs(slug: ModelSlug, category: Category) -> bool: fn benchmark_enum_literal_union_comparison(criterion: &mut Criterion) { const NUM_ENUM_MEMBERS: usize = 256; - let mut code = - "from enum import StrEnum\nfrom typing import Literal\n\nclass LargeEnum(StrEnum):\n" - .to_string(); + let mut code = "from enum import StrEnum +from typing import Literal + +class LargeEnum(StrEnum): +" + .to_string(); for index in 0..NUM_ENUM_MEMBERS { writeln!(&mut code, " VALUE_{index} = \"value_{index}\"").ok(); } @@ -668,7 +674,13 @@ fn benchmark_cross_str_enum_comparison(criterion: &mut Criterion) { } } code.push_str( - "\n\ndef compare(left: Left, right: Right):\n if left != right:\n return\n return left == right\n", + " + +def compare(left: Left, right: Right): + if left != right: + return + return left == right +", ); benchmark_enum_comparison(criterion, "ty_micro[cross_str_enum_comparison]", &code); @@ -700,7 +712,11 @@ fn benchmark_mixed_str_enum_comparison(criterion: &mut Criterion) { }; writeln!( &mut code, - "\ndef compare(left: {}, right: {}):\n if left != right:\n return\n return left == right", + " +def compare(left: {}, right: {}): + if left != right: + return + return left == right", class_union("Left"), class_union("Right"), ) @@ -1136,7 +1152,14 @@ fn literal_equality_fallthrough_code() -> String { } fn literal_or_pattern_reachability_code() -> String { - let mut code = "from typing import Any\n\ndef check(item: Any) -> None:\n x: int\n match item:\n case ".to_string(); + let mut code = "\ +from typing import Any + +def check(item: Any) -> None: + x: int + match item: + case " + .to_string(); for index in 0..NUM_LITERAL_OR_PATTERN_ALTERNATIVES { if index > 0 { @@ -1421,7 +1444,8 @@ fn bench_project_named( .join("\n "); assert!( diagnostics <= max_diagnostics, - "{project_name}: Expected <={max_diagnostics} diagnostics but got {diagnostics}:\n {details}", + "{project_name}: Expected <={max_diagnostics} diagnostics \ + but got {diagnostics}:\n {details}", ); } } diff --git a/crates/ruff_db/src/testing.rs b/crates/ruff_db/src/testing.rs index e74d8a5bc0..df0d307987 100644 --- a/crates/ruff_db/src/testing.rs +++ b/crates/ruff_db/src/testing.rs @@ -18,7 +18,10 @@ pub fn assert_function_query_was_not_run( db.attach(|_| { if let Some(will_execute_event) = will_execute_event { - panic!("Expected query {query_name}({id:?}) not to have run but it did: {will_execute_event:?}\n\n{events:#?}"); + panic!( + "Expected query {query_name}({id:?}) not to have run but it did: \ + {will_execute_event:?}\n\n{events:#?}" + ); } }); } @@ -40,7 +43,8 @@ pub fn assert_const_function_query_was_not_run( db.attach(|_| { if let Some(will_execute_event) = event { panic!( - "Expected query {query_name}() not to have run but it did: {will_execute_event:?}\n\n{events:#?}" + "Expected query {query_name}() not to have run but it did: \ + {will_execute_event:?}\n\n{events:#?}" ); } }); @@ -61,12 +65,14 @@ pub fn assert_function_query_was_not_run_by_name( match input { Some(input) => { panic!( - "Expected query {query_name}({input:?}) not to have run but it did: {will_execute_event:?}\n\n{events:#?}" + "Expected query {query_name}({input:?}) not to have run \ + but it did: {will_execute_event:?}\n\n{events:#?}" ); } None => { panic!( - "Expected query {query_name} not to have run for any input but it did: {will_execute_event:?}\n\n{events:#?}" + "Expected query {query_name} not to have run for any input \ + but it did: {will_execute_event:?}\n\n{events:#?}" ); } } diff --git a/crates/ruff_dev/src/generate_ty_cli_reference.rs b/crates/ruff_dev/src/generate_ty_cli_reference.rs index b9a612c226..cc6e7cc2e0 100644 --- a/crates/ruff_dev/src/generate_ty_cli_reference.rs +++ b/crates/ruff_dev/src/generate_ty_cli_reference.rs @@ -80,7 +80,11 @@ fn generate() -> String { let mut parents = Vec::new(); - output.push_str("\n\n"); + output.push_str( + "\n\n", + ); output.push_str("# CLI Reference\n\n"); generate_command(&mut output, &ty, &mut parents); diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index ab60103d99..7abdab8ce1 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -404,7 +404,9 @@ where fn debug_assert_no_newlines(text: &str) { debug_assert!( !text.contains('\r'), - "The content '{text}' contains an unsupported '\\r' line terminator character but text must only use line feeds '\\n' as line separator. Use '\\n' instead of '\\r' and '\\r\\n' to insert a line break in strings." + "The content '{text}' contains an unsupported '\\r' line terminator character \ + but text must only use line feeds '\\n' as line separator. \ + Use '\\n' instead of '\\r' and '\\r\\n' to insert a line break in strings." ); } @@ -2412,7 +2414,11 @@ where { #[inline] fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { - let formatter = self.formatter.take().expect("Tried to format a `format_once` at least twice. This is not allowed. You may want to use `format_with` or `format.memoized` instead."); + let formatter = self.formatter.take().expect( + "Tried to format a `format_once` at least twice. \ + This is not allowed. \ + You may want to use `format_with` or `format.memoized` instead.", + ); (formatter)(f) } diff --git a/crates/ruff_formatter/src/printer/mod.rs b/crates/ruff_formatter/src/printer/mod.rs index 9f59c3b5be..ebe182bc42 100644 --- a/crates/ruff_formatter/src/printer/mod.rs +++ b/crates/ruff_formatter/src/printer/mod.rs @@ -1998,7 +1998,8 @@ two lines`, &format_args![ space(), token( - "// Using reserved width causes this content to not fit even though it's a line suffix element" + "// Using reserved width causes this content \ + to not fit even though it's a line suffix element" ) ], 93 @@ -2007,7 +2008,8 @@ two lines`, assert_eq!( printed.as_code(), - "[\n 1, 2, 3\n]; // Using reserved width causes this content to not fit even though it's a line suffix element" + "[\n 1, 2, 3\n]; // Using reserved width causes this content \ + to not fit even though it's a line suffix element" ); } @@ -2026,8 +2028,15 @@ two lines`, group(&format_args![ token("This group breaks because:"), soft_line_break_or_space(), - if_group_fits_on_line(&token("This content fits but should not be printed.")).with_group_id(Some(group_id)), - if_group_breaks(&token("It measures with the 'if_group_breaks' variant because the referenced group breaks and that's just way too much text.")).with_group_id(Some(group_id)), + if_group_fits_on_line(&token( + "This content fits but should not be printed." + )) + .with_group_id(Some(group_id)), + if_group_breaks(&token( + "It measures with the 'if_group_breaks' variant because the \ + referenced group breaks and that's just way too much text." + )) + .with_group_id(Some(group_id)), ]) ] ) @@ -2037,7 +2046,11 @@ two lines`, assert_eq!( printed.as_code(), - "The referenced group breaks.\nThis group breaks because:\nIt measures with the 'if_group_breaks' variant because the referenced group breaks and that's just way too much text." + "\ +The referenced group breaks. +This group breaks because: +It measures with the 'if_group_breaks' variant because the referenced group breaks \ +and that's just way too much text." ); } @@ -2058,7 +2071,11 @@ two lines`, write!( f, [ - group(&token("Group with id-1 does not fit on the line because it exceeds the line width of 80 characters by")).with_id(Some(id_1)), + group(&token( + "Group with id-1 does not fit on the line \ + because it exceeds the line width of 80 characters by" + )) + .with_id(Some(id_1)), hard_line_break() ] )?; diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 1c08d91168..922e07ebf1 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -101,8 +101,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.is_rule_enabled(Rule::UnnecessaryLiteralUnion) { flake8_pyi::rules::unnecessary_literal_union(checker, expr); } + // Avoid duplicate checks inside `Optional`. if checker.is_rule_enabled(Rule::DuplicateUnionMember) - // Avoid duplicate checks inside `Optional` && !checker.semantic.inside_optional() { flake8_pyi::rules::duplicate_union_member(checker, expr); @@ -1593,9 +1593,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { // Avoid duplicate checks if the parent is a union, since these rules already // traverse nested unions. if !checker.semantic.in_nested_union() { + // Avoid duplicate checks inside `Optional`. if checker.is_rule_enabled(Rule::DuplicateUnionMember) && checker.semantic.in_type_definition() - // Avoid duplicate checks inside `Optional` && !checker.semantic.inside_optional() { flake8_pyi::rules::duplicate_union_member(checker, expr); diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 7a466eff55..aad92bd5e6 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -1683,13 +1683,14 @@ impl<'a> Visitor<'a> for Checker<'a> { return; } + // `in_deferred_type_definition()` will only be `true` if we're now visiting the deferred nodes + // after having already traversed the source tree once. If we're now visiting the deferred nodes, + // we can't defer again, or we'll infinitely recurse! if !self.semantic.in_typing_literal() - // `in_deferred_type_definition()` will only be `true` if we're now visiting the deferred nodes - // after having already traversed the source tree once. If we're now visiting the deferred nodes, - // 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.target_version().defers_annotations()) + && (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 { @@ -2780,15 +2781,14 @@ impl<'a> Checker<'a> { match parent { Stmt::TypeAlias(_) => flags.insert(BindingFlags::DEFERRED_TYPE_ALIAS), + // TODO: It is a bit unfortunate that we do this check twice. Maybe we should change how + // we visit this statement so the semantic flag for the type alias sticks around until + // after we've handled this store, so we can check the flag instead of duplicating this check. Stmt::AnnAssign(ast::StmtAnnAssign { annotation, .. }) - // TODO: It is a bit unfortunate that we do this check twice - // maybe we should change how we visit this statement - // so the semantic flag for the type alias sticks around - // until after we've handled this store, so we can check - // the flag instead of duplicating this check - if self.semantic.match_typing_expr(annotation, "TypeAlias") => { - flags.insert(BindingFlags::ANNOTATED_TYPE_ALIAS); - } + if self.semantic.match_typing_expr(annotation, "TypeAlias") => + { + flags.insert(BindingFlags::ANNOTATED_TYPE_ALIAS); + } _ => {} } diff --git a/crates/ruff_linter/src/message/mod.rs b/crates/ruff_linter/src/message/mod.rs index 9f33f21e04..72e6a50ab2 100644 --- a/crates/ruff_linter/src/message/mod.rs +++ b/crates/ruff_linter/src/message/mod.rs @@ -50,9 +50,10 @@ pub fn create_panic_diagnostic(error: &PanicError, path: Option<&Path>) -> Diagn match backtrace.status() { BacktraceStatus::Disabled => { diagnostic.sub(SubDiagnostic::new( - SubDiagnosticSeverity::Info, - "run with `RUST_BACKTRACE=1` environment variable to show the full backtrace information", - )); + SubDiagnosticSeverity::Info, + "run with `RUST_BACKTRACE=1` environment variable \ + to show the full backtrace information", + )); } BacktraceStatus::Captured => { diagnostic.sub(SubDiagnostic::new( diff --git a/crates/ruff_linter/src/noqa.rs b/crates/ruff_linter/src/noqa.rs index 55d6796856..9c8b973e4c 100644 --- a/crates/ruff_linter/src/noqa.rs +++ b/crates/ruff_linter/src/noqa.rs @@ -280,13 +280,16 @@ impl<'a> FileNoqaDirectives<'a> { for warning in warnings { warn!( - "Missing or joined rule code(s) at {path_display}:{line}: {warning}" + "Missing or joined rule code(s) at {path_display}:{line}: \ + {warning}" ); } if no_indentation_at_offset { warn!( - "Unexpected `# ruff: noqa` directive at {path_display}:{line}. File-level suppression comments must appear on their own line. For line-level suppression, omit the `ruff:` prefix." + "Unexpected `# ruff: noqa` directive at {path_display}:{line}. \ + File-level suppression comments must appear on their own line. \ + For line-level suppression, omit the `ruff:` prefix." ); continue; } @@ -748,14 +751,18 @@ pub(crate) enum LexicalError { impl Display for LexicalError { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - LexicalError::MissingCodes => fmt.write_str("expected a comma-separated list of codes (e.g., `# noqa: F401, F841`)."), - LexicalError::InvalidSuffix => { - fmt.write_str("expected `:` followed by a comma-separated list of codes (e.g., `# noqa: F401, F841`).") - } - LexicalError::InvalidCodeSuffix => { - fmt.write_str("expected code to consist of uppercase letters followed by digits only (e.g. `F401`)") - } - + LexicalError::MissingCodes => fmt.write_str( + "expected a comma-separated list of codes \ + (e.g., `# noqa: F401, F841`).", + ), + LexicalError::InvalidSuffix => fmt.write_str( + "expected `:` followed by a comma-separated list of codes \ + (e.g., `# noqa: F401, F841`).", + ), + LexicalError::InvalidCodeSuffix => fmt.write_str( + "expected code to consist of uppercase letters followed by digits only \ + (e.g. `F401`)", + ), } } } @@ -1216,7 +1223,8 @@ impl<'a> NoqaDirectives<'a> { let path_display = relativize_path(path); for warning in warnings { warn!( - "Missing or joined rule code(s) at {path_display}:{line}: {warning}" + "Missing or joined rule code(s) \ + at {path_display}:{line}: {warning}" ); } } @@ -1464,7 +1472,8 @@ mod tests { if second_count > 0 { writeln!( output, - "## Additional suppressions added on a second pass: {second_count}\n\n```py\n{fixed}\n```\n" + "## Additional suppressions added on a second pass: \ + {second_count}\n\n```py\n{fixed}\n```\n" )?; } 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 360299d488..4515962880 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 @@ -284,7 +284,11 @@ fn create_diagnostic( if is_default_argument_ellipsis && seen_default { // For ellipsis after a parameter with default, can't remove the default - diagnostic.info("Automatic fix is unavailable because a required parameter would follow an optional parameter. Consider reordering arguments to enable the fix."); + diagnostic.info( + "Automatic fix is unavailable because a required parameter \ + would follow an optional parameter. \ + Consider reordering arguments to enable the fix.", + ); return Ok(None); } @@ -316,7 +320,11 @@ fn create_diagnostic( } _ => { if seen_default { - diagnostic.info("Automatic fix is unavailable because a required parameter would follow an optional parameter. Consider reordering arguments to enable the fix."); + diagnostic.info( + "Automatic fix is unavailable because a required parameter \ + would follow an optional parameter. \ + Consider reordering arguments to enable the fix.", + ); return Ok(None); } format!( diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs index 8d963335ec..d0b9b3afc1 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/mako_templates.rs @@ -39,7 +39,9 @@ pub(crate) struct MakoTemplates; impl Violation for MakoTemplates { #[derive_message_formats] fn message(&self) -> String { - "Mako templates allow HTML and JavaScript rendering by default and are inherently open to XSS attacks".to_string() + "Mako templates allow HTML and JavaScript rendering by default \ + and are inherently open to XSS attacks" + .to_string() } } 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 d4d741d328..9569d72fa9 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 @@ -47,10 +47,18 @@ impl Violation for SubprocessPopenWithShellEqualsTrue { #[derive_message_formats] fn message(&self) -> String { match (self.safety, self.is_exact) { - (Safety::SeemsSafe, true) => "`subprocess` call with `shell=True` seems safe, but may be changed in the future; consider rewriting without `shell`".to_string(), - (Safety::Unknown, true) => "`subprocess` call with `shell=True` identified, security issue".to_string(), - (Safety::SeemsSafe, false) => "`subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`".to_string(), - (Safety::Unknown, false) => "`subprocess` call with truthy `shell` identified, security issue".to_string(), + (Safety::SeemsSafe, true) => "`subprocess` call with `shell=True` seems safe, \ + but may be changed in the future; consider rewriting without `shell`" + .to_string(), + (Safety::Unknown, true) => { + "`subprocess` call with `shell=True` identified, security issue".to_string() + } + (Safety::SeemsSafe, false) => "`subprocess` call with truthy `shell` seems safe, \ + but may be changed in the future; consider rewriting without `shell`" + .to_string(), + (Safety::Unknown, false) => { + "`subprocess` call with truthy `shell` identified, security issue".to_string() + } } } } @@ -181,8 +189,12 @@ impl Violation for StartProcessWithAShell { #[derive_message_formats] fn message(&self) -> String { match self.safety { - Safety::SeemsSafe => "Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without `shell`".to_string(), - Safety::Unknown => "Starting a process with a shell, possible injection detected".to_string(), + Safety::SeemsSafe => "Starting a process with a shell: seems safe, \ + but may be changed in the future; consider rewriting without `shell`" + .to_string(), + Safety::Unknown => { + "Starting a process with a shell, possible injection detected".to_string() + } } } } 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 c4ad14332e..41c09b6f70 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 @@ -59,7 +59,9 @@ pub(crate) struct SuspiciousPickleUsage; impl Violation for SuspiciousPickleUsage { #[derive_message_formats] fn message(&self) -> String { - "`pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue".to_string() + "`pickle` and modules that wrap it can be unsafe \ + when used to deserialize untrusted data, possible security issue" + .to_string() } } @@ -446,7 +448,9 @@ pub(crate) struct SuspiciousURLOpenUsage; impl Violation for SuspiciousURLOpenUsage { #[derive_message_formats] fn message(&self) -> String { - "Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.".to_string() + "Audit URL open for permitted schemes. \ + Allowing use of `file:` or custom schemes is often unexpected." + .to_string() } } @@ -534,7 +538,9 @@ pub(crate) struct SuspiciousXMLCElementTreeUsage; impl Violation for SuspiciousXMLCElementTreeUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -579,7 +585,9 @@ pub(crate) struct SuspiciousXMLElementTreeUsage; impl Violation for SuspiciousXMLElementTreeUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -624,7 +632,9 @@ pub(crate) struct SuspiciousXMLExpatReaderUsage; impl Violation for SuspiciousXMLExpatReaderUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -669,7 +679,9 @@ pub(crate) struct SuspiciousXMLExpatBuilderUsage; impl Violation for SuspiciousXMLExpatBuilderUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -714,7 +726,9 @@ pub(crate) struct SuspiciousXMLSaxUsage; impl Violation for SuspiciousXMLSaxUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -759,7 +773,9 @@ pub(crate) struct SuspiciousXMLMiniDOMUsage; impl Violation for SuspiciousXMLMiniDOMUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -804,7 +820,9 @@ pub(crate) struct SuspiciousXMLPullDOMUsage; impl Violation for SuspiciousXMLPullDOMUsage { #[derive_message_formats] fn message(&self) -> String { - "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents".to_string() + "Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; \ + use `defusedxml` equivalents" + .to_string() } } @@ -893,7 +911,10 @@ pub(crate) struct SuspiciousUnverifiedContextUsage; impl Violation for SuspiciousUnverifiedContextUsage { #[derive_message_formats] fn message(&self) -> String { - "Python allows using an insecure context via the `_create_unverified_context` that reverts to the previous behavior that does not validate certificates or perform hostname checks.".to_string() + "Python allows using an insecure context via the `_create_unverified_context` \ + that reverts to the previous behavior that does not validate certificates \ + or perform hostname checks." + .to_string() } } @@ -945,7 +966,9 @@ pub(crate) struct SuspiciousFTPLibUsage; impl Violation for SuspiciousFTPLibUsage { #[derive_message_formats] fn message(&self) -> String { - "FTP-related functions are being called. FTP is considered insecure. Use SSH/SFTP/SCP or some other encrypted protocol.".to_string() + "FTP-related functions are being called. FTP is considered insecure. \ + Use SSH/SFTP/SCP or some other encrypted protocol." + .to_string() } } @@ -964,17 +987,16 @@ pub(crate) fn suspicious_function_reference(checker: &Checker, func: &Expr) { } match checker.semantic().current_expression_parent() { - Some(Expr::Call(parent)) - // Avoid duplicate diagnostics. For example: - // - // ```python - // # vvvvvvvvvvvvvvvvvvvvvvvvv Already reported as a call expression - // shelve.open(lorem, ipsum) - // # ^^^^^^ Should not be reported as a reference - // ``` - if parent.func.range().contains_range(func.range()) => { - return; - } + // Avoid duplicate diagnostics. For example: + // + // ```python + // # vvvvvvvvvvvvvvvvvvvvvvvvv Already reported as a call expression + // shelve.open(lorem, ipsum) + // # ^^^^^^ Should not be reported as a reference + // ``` + Some(Expr::Call(parent)) if parent.func.range().contains_range(func.range()) => { + return; + } Some(Expr::Attribute(_)) => { // Avoid duplicate diagnostics. For example: // diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs index d099b7fe42..09ee508ed0 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_imports.rs @@ -31,7 +31,9 @@ pub(crate) struct SuspiciousTelnetlibImport; impl Violation for SuspiciousTelnetlibImport { #[derive_message_formats] fn message(&self) -> String { - "`telnetlib` and related modules are considered insecure. Use SSH or another encrypted protocol.".to_string() + "`telnetlib` and related modules are considered insecure. \ + Use SSH or another encrypted protocol." + .to_string() } } @@ -56,7 +58,9 @@ pub(crate) struct SuspiciousFtplibImport; impl Violation for SuspiciousFtplibImport { #[derive_message_formats] fn message(&self) -> String { - "`ftplib` and related modules are considered insecure. Use SSH, SFTP, SCP, or another encrypted protocol.".to_string() + "`ftplib` and related modules are considered insecure. \ + Use SSH, SFTP, SCP, or another encrypted protocol." + .to_string() } } @@ -306,7 +310,10 @@ pub(crate) struct SuspiciousHttpoxyImport; impl Violation for SuspiciousHttpoxyImport { #[derive_message_formats] fn message(&self) -> String { - "`httpoxy` is a set of vulnerabilities that affect application code running inCGI, or CGI-like environments. The use of CGI for web applications should be avoided".to_string() + "`httpoxy` is a set of vulnerabilities that affect application code \ + running inCGI, or CGI-like environments. \ + The use of CGI for web applications should be avoided" + .to_string() } } diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs index 55574b2bfe..33ded4bb21 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/helpers.rs @@ -86,77 +86,43 @@ fn is_user_allowed_func_call( /// /// See: fn is_operator_method(name: &str) -> bool { - matches!( - name, - "__contains__" // in - // item access ([]) - | "__getitem__" // [] - | "__setitem__" // []= - | "__delitem__" // del [] - // addition (+) - | "__add__" // + - | "__radd__" // + - | "__iadd__" // += - // subtraction (-) - | "__sub__" // - - | "__rsub__" // - - | "__isub__" // -= - // multiplication (*) - | "__mul__" // * - | "__rmul__" // * - | "__imul__" // *= - // division (/) - | "__truediv__" // / - | "__rtruediv__" // / - | "__itruediv__" // /= - // floor division (//) - | "__floordiv__" // // - | "__rfloordiv__" // // - | "__ifloordiv__" // //= - // remainder (%) - | "__mod__" // % - | "__rmod__" // % - | "__imod__" // %= - // exponentiation (**) - | "__pow__" // ** - | "__rpow__" // ** - | "__ipow__" // **= - // left shift (<<) - | "__lshift__" // << - | "__rlshift__" // << - | "__ilshift__" // <<= - // right shift (>>) - | "__rshift__" // >> - | "__rrshift__" // >> - | "__irshift__" // >>= - // matrix multiplication (@) - | "__matmul__" // @ - | "__rmatmul__" // @ - | "__imatmul__" // @= - // meet (&) - | "__and__" // & - | "__rand__" // & - | "__iand__" // &= - // join (|) - | "__or__" // | - | "__ror__" // | - | "__ior__" // |= - // xor (^) - | "__xor__" // ^ - | "__rxor__" // ^ - | "__ixor__" // ^= - // comparison (>, <, >=, <=, ==, !=) - | "__gt__" // > - | "__lt__" // < - | "__ge__" // >= - | "__le__" // <= - | "__eq__" // == - | "__ne__" // != - // unary operators (included for completeness) - | "__pos__" // + - | "__neg__" // - - | "__invert__" // ~ - ) + match name { + // Membership (`in`). + "__contains__" => true, + // Item access (`[]`, `[]=`, and `del []`). + "__getitem__" | "__setitem__" | "__delitem__" => true, + // Addition (`+` and `+=`). + "__add__" | "__radd__" | "__iadd__" => true, + // Subtraction (`-` and `-=`). + "__sub__" | "__rsub__" | "__isub__" => true, + // Multiplication (`*` and `*=`). + "__mul__" | "__rmul__" | "__imul__" => true, + // Division (`/` and `/=`). + "__truediv__" | "__rtruediv__" | "__itruediv__" => true, + // Floor division (`//` and `//=`). + "__floordiv__" | "__rfloordiv__" | "__ifloordiv__" => true, + // Remainder (`%` and `%=`). + "__mod__" | "__rmod__" | "__imod__" => true, + // Exponentiation (`**` and `**=`). + "__pow__" | "__rpow__" | "__ipow__" => true, + // Left shift (`<<` and `<<=`). + "__lshift__" | "__rlshift__" | "__ilshift__" => true, + // Right shift (`>>` and `>>=`). + "__rshift__" | "__rrshift__" | "__irshift__" => true, + // Matrix multiplication (`@` and `@=`). + "__matmul__" | "__rmatmul__" | "__imatmul__" => true, + // Meet (`&` and `&=`). + "__and__" | "__rand__" | "__iand__" => true, + // Join (`|` and `|=`). + "__or__" | "__ror__" | "__ior__" => true, + // Exclusive-or (`^` and `^=`). + "__xor__" | "__rxor__" | "__ixor__" => true, + // Comparison (`>`, `<`, `>=`, `<=`, `==`, and `!=`). + "__gt__" | "__lt__" | "__ge__" | "__le__" | "__eq__" | "__ne__" => true, + // Unary operators (`+`, `-`, and `~`), included for completeness. + "__pos__" | "__neg__" | "__invert__" => true, + _ => false, + } } /// Returns `true` if a function definition is allowed to use a boolean trap. diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs index 5a0e2025b4..1652670db7 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/class_as_data_structure.rs @@ -79,13 +79,13 @@ pub(crate) fn class_as_data_structure(checker: &Checker, class_def: &ast::StmtCl // skip `self` .skip(1) .all(|param| param.annotation().is_some() && !param.is_variadic()) - && (func_def.parameters.kwonlyargs.is_empty() || checker.target_version() >= PythonVersion::PY310) - // `__init__` should not have complicated logic in it - // only assignments - && func_def - .body - .iter() - .all(is_simple_assignment_to_attribute) + && (func_def.parameters.kwonlyargs.is_empty() + || checker.target_version() >= PythonVersion::PY310) + && ( + // `__init__` should not have complicated logic in it + // only assignments + func_def.body.iter().all(is_simple_assignment_to_attribute) + ) { has_dunder_init = true; } @@ -94,8 +94,8 @@ pub(crate) fn class_as_data_structure(checker: &Checker, class_def: &ast::StmtCl } } // Ignore class variables - ast::Stmt::Assign(_) | ast::Stmt::AnnAssign(_) | - // and expressions (e.g. string literals) + ast::Stmt::Assign(_) | ast::Stmt::AnnAssign(_) => {} + // Ignore expressions (e.g. string literals) ast::Stmt::Expr(_) => {} _ => { // Bail for anything else - e.g. nested classes diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs index 0609669a68..2b4a6ab243 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/except_with_empty_tuple.rs @@ -43,9 +43,13 @@ impl Violation for ExceptWithEmptyTuple { #[derive_message_formats] fn message(&self) -> String { if self.is_star { - "Using `except* ():` with an empty tuple does not catch anything; add exceptions to handle".to_string() + "Using `except* ():` with an empty tuple does not catch anything; \ + add exceptions to handle" + .to_string() } else { - "Using `except ():` with an empty tuple does not catch anything; add exceptions to handle".to_string() + "Using `except ():` with an empty tuple does not catch anything; \ + add exceptions to handle" + .to_string() } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs index bfccc9f0d6..305249b7bc 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/f_string_docstring.rs @@ -37,7 +37,10 @@ pub(crate) struct FStringDocstring; impl Violation for FStringDocstring { #[derive_message_formats] fn message(&self) -> String { - "f-string used as docstring. Python will interpret this as a joined string, rather than a docstring.".to_string() + "f-string used as docstring. \ + Python will interpret this as a joined string, \ + rather than a docstring." + .to_string() } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs index 47b57d56fd..a0c1f3473b 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_call_in_argument_default.rs @@ -72,10 +72,15 @@ impl Violation for FunctionCallInDefaultArgument { fn message(&self) -> String { if let Some(name) = &self.name { format!( - "Do not perform function call `{name}` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable" + "Do not perform function call `{name}` in argument defaults; \ + instead, perform the call within the function, \ + or read the default from a module-level singleton variable" ) } else { - "Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable".to_string() + "Do not perform function call in argument defaults; \ + instead, perform the call within the function, \ + or read the default from a module-level singleton variable" + .to_string() } } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs index 0532007d60..657435dc11 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs @@ -109,15 +109,14 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { return; } + // Mark `return lambda: x` as safe. Stmt::Return(ast::StmtReturn { value: Some(value), range: _, node_index: _, - }) - // Mark `return lambda: x` as safe. - if value.is_lambda_expr() => { - self.safe_functions.push(value); - } + }) if value.is_lambda_expr() => { + self.safe_functions.push(value); + } _ => {} } visitor::walk_stmt(self, stmt); diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs index 96a64093c6..a82b2d8c17 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/loop_iterator_mutation.rs @@ -70,28 +70,27 @@ pub(crate) fn loop_iterator_mutation(checker: &Checker, stmt_for: &StmtFor) { // Ex) Given, `for item in items:`, `item` is the index and `items` is the iterable. (&**target, &**target, &**iter) } + // Ex) Given `for i, item in enumerate(items):`, `i` is the index and `items` is the + // iterable. Expr::Call(ExprCall { func, arguments, .. - }) - // Ex) Given `for i, item in enumerate(items):`, `i` is the index and `items` is the - // iterable. - if checker.semantic().match_builtin_expr(func, "enumerate") => { - // Ex) `items` - let Some(iter) = arguments.args.first() else { - return; - }; - - let Expr::Tuple(ExprTuple { elts, .. }) = &**target else { - return; - }; - - let [index, target] = elts.as_slice() else { - return; - }; - - // Ex) `i` - (index, target, iter) - } + }) if checker.semantic().match_builtin_expr(func, "enumerate") => { + // Ex) `items` + let Some(iter) = arguments.args.first() else { + return; + }; + + let Expr::Tuple(ExprTuple { elts, .. }) = &**target else { + return; + }; + + let [index, target] = elts.as_slice() else { + return; + }; + + // Ex) `i` + (index, target, iter) + } _ => { return; } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs index a8b2ad601c..f9bc46860c 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/map_without_explicit_strict.rs @@ -67,10 +67,9 @@ pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCal if semantic.match_builtin_expr(&call.func, "map") && call.arguments.find_keyword("strict").is_none() && ( - // at least 2 iterables (+ 1 function) + // at least 2 iterables (+ 1 function), or a starred argument. call.arguments.args.len() >= 3 - // or a starred argument - || call.arguments.args.iter().any(ast::Expr::is_starred_expr) + || call.arguments.args.iter().any(ast::Expr::is_starred_expr) ) && !any_infinite_iterables(call.arguments.args.iter().skip(1), semantic) { diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs index 710785b509..0bc614d911 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/reuse_of_groupby_generator.rs @@ -40,7 +40,9 @@ pub(crate) struct ReuseOfGroupbyGenerator; impl Violation for ReuseOfGroupbyGenerator { #[derive_message_formats] fn message(&self) -> String { - "Using the generator returned from `itertools.groupby()` more than once will do nothing on the second usage".to_string() + "Using the generator returned from `itertools.groupby()` more than once \ + will do nothing on the second usage" + .to_string() } } diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs index db71c7b2fb..475a920b2c 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs +++ b/crates/ruff_linter/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs @@ -57,14 +57,11 @@ impl AlwaysFixableViolation for ZipWithoutExplicitStrict { pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCall) { let semantic = checker.semantic(); + // any call to `zip()` with at least 2 iterables, or a starred argument. if semantic.match_builtin_expr(&call.func, "zip") && call.arguments.find_keyword("strict").is_none() - && ( - // at least 2 iterables - call.arguments.args.len() >= 2 - // or a starred argument - || call.arguments.args.iter().any(ast::Expr::is_starred_expr) - ) + && (call.arguments.args.len() >= 2 + || call.arguments.args.iter().any(ast::Expr::is_starred_expr)) && !any_infinite_iterables(call.arguments.args.iter(), semantic) { checker diff --git a/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs b/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs index da85f34697..143c76d741 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs +++ b/crates/ruff_linter/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs @@ -57,7 +57,9 @@ impl Violation for FormatInGetTextFuncCall { if self.is_plural { "`format` method in plural argument is resolved before function call".to_string() } else { - "`format` method argument is resolved before function call; consider `_(\"string %s\") % arg`".to_string() + "`format` method argument is resolved before function call; \ + consider `_(\"string %s\") % arg`" + .to_string() } } } 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 630e267142..ec9a8a0e9d 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 @@ -107,7 +107,9 @@ pub(crate) struct BadVersionInfoOrder; impl Violation for BadVersionInfoOrder { #[derive_message_formats] fn message(&self) -> String { - "Put branches for newer Python versions first when branching on `sys.version_info` comparisons".to_string() + "Put branches for newer Python versions first \ + when branching on `sys.version_info` comparisons" + .to_string() } } @@ -142,8 +144,11 @@ pub(crate) fn bad_version_info_comparison(checker: &Checker, test: &Expr, has_el if matches!(op, CmpOp::Lt) { if checker.is_rule_enabled(Rule::BadVersionInfoOrder) - // See https://github.com/astral-sh/ruff/issues/15347 - && (checker.source_type.is_stub() || is_bad_version_info_in_non_stub_enabled(checker.settings())) + && ( + // See https://github.com/astral-sh/ruff/issues/15347. + checker.source_type.is_stub() + || is_bad_version_info_in_non_stub_enabled(checker.settings()) + ) { if has_else_clause { checker.report_diagnostic(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 cd3703c718..18febc7403 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 @@ -26,7 +26,9 @@ impl Violation for FutureAnnotationsInStub { #[derive_message_formats] fn message(&self) -> String { - "`from __future__ import annotations` has no effect in stub files, since type checkers automatically treat stubs as having those semantics".to_string() + "`from __future__ import annotations` has no effect in stub files, \ + since type checkers automatically treat stubs as having those semantics" + .to_string() } fn fix_title(&self) -> Option { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs index 47b111f97e..185b6fd4de 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/unaliased_collections_abc_set_import.rs @@ -48,7 +48,9 @@ impl Violation for UnaliasedCollectionsAbcSetImport { #[derive_message_formats] fn message(&self) -> String { - "Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin".to_string() + "Use `from collections.abc import Set as AbstractSet` \ + to avoid confusion with the `set` builtin" + .to_string() } fn fix_title(&self) -> Option { diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs index 07e47c1f83..a10c38b5c9 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs @@ -56,10 +56,14 @@ impl AlwaysFixableViolation for AvoidableEscapedQuote { pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike) { if checker.semantic().in_pep_257_docstring() || checker.semantic().in_string_type_definition() - // This rule has support for strings nested inside another f-strings but they're checked - // via the outermost f-string. This means that we shouldn't be checking any nested string - // or f-string. - || checker.semantic().in_interpolated_string_replacement_field() + || ( + // This rule has support for strings nested inside another f-strings but they're checked + // via the outermost f-string. This means that we shouldn't be checking any nested string + // or f-string. + checker + .semantic() + .in_interpolated_string_replacement_field() + ) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index 43fbf7ad6d..c646029590 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -376,8 +376,10 @@ fn strings(checker: &Checker, sequence: &[TextRange]) { *range, ))); } else if trivia.last_quote_char != quotes_settings.inline_quotes.as_char() - // If we're not using the preferred type, only allow use to avoid escapes. - && !relax_quote + && ( + // If we're not using the preferred type, only allow use to avoid escapes. + !relax_quote + ) { // If inline strings aren't enforced, ignore it. if !checker.is_rule_enabled(Rule::BadQuotesInlineString) { diff --git a/crates/ruff_linter/src/rules/flake8_return/visitor.rs b/crates/ruff_linter/src/rules/flake8_return/visitor.rs index 2a7dc234b0..f24d3aaa43 100644 --- a/crates/ruff_linter/src/rules/flake8_return/visitor.rs +++ b/crates/ruff_linter/src/rules/flake8_return/visitor.rs @@ -129,13 +129,12 @@ impl<'a> Visitor<'a> for ReturnVisitor<'_, 'a> { .non_locals .extend(names.iter().map(Identifier::as_str)); } - Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) - // Ex) `x: int` - if value.is_none() => { - if let Expr::Name(name) = target.as_ref() { - self.stack.annotations.insert(name.id.as_str()); - } + // Ex) `x: int` + Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) if value.is_none() => { + if let Expr::Name(name) = target.as_ref() { + self.stack.annotations.insert(name.id.as_str()); } + } Stmt::Return(stmt_return) => { // If the `return` statement is preceded by an `assignment` statement, then the // `assignment` statement may be redundant. diff --git a/crates/ruff_linter/src/rules/isort/order.rs b/crates/ruff_linter/src/rules/isort/order.rs index 40b74662bb..8d6be203bf 100644 --- a/crates/ruff_linter/src/rules/isort/order.rs +++ b/crates/ruff_linter/src/rules/isort/order.rs @@ -14,47 +14,46 @@ pub(crate) fn order_imports<'a>( ) -> Vec> { let straight_imports = block.import.into_iter(); - let from_imports = - // Include all non-re-exports. - block - .import_from - .into_iter() - .chain( - // Include all re-exports. - block - .import_from_as - .into_iter() - .map(|((import_from, ..), body)| (import_from, body)), - ) - .chain( - // Include all star imports. - block.import_from_star, - ) - .map( - |( - import_from, - ImportFromStatement { - first_index, - comments, - aliases, - trailing_comma, - }, - )| { - // Within each `Stmt::ImportFrom`, sort the members. - ( - import_from, - first_index.unwrap_or_default(), - comments, - trailing_comma, - aliases - .into_iter() - .sorted_by_cached_key(|(alias, _)| { - MemberKey::from_member(alias.name, alias.asname, settings) - }) - .collect::>(), - ) + // Include all non-re-exports. + let from_imports = block + .import_from + .into_iter() + .chain( + // Include all re-exports. + block + .import_from_as + .into_iter() + .map(|((import_from, ..), body)| (import_from, body)), + ) + .chain( + // Include all star imports. + block.import_from_star, + ) + .map( + |( + import_from, + ImportFromStatement { + first_index, + comments, + aliases, + trailing_comma, }, - ); + )| { + // Within each `Stmt::ImportFrom`, sort the members. + ( + import_from, + first_index.unwrap_or_default(), + comments, + trailing_comma, + aliases + .into_iter() + .sorted_by_cached_key(|(alias, _)| { + MemberKey::from_member(alias.name, alias.asname, settings) + }) + .collect::>(), + ) + }, + ); if matches!(section, ImportSection::Known(ImportType::Future)) { let ordered_from_imports = from_imports diff --git a/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs b/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs index 625a29829f..e39ac5303c 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs @@ -65,84 +65,68 @@ pub(crate) fn legacy_random(checker: &Checker, expr: &Expr) { return; } - if let Some(method_name) = - checker - .semantic() - .resolve_qualified_name(expr) - .and_then(|qualified_name| { - // seeding state - if matches!( - qualified_name.segments(), - [ - "numpy", - "random", - // Seeds - "seed" | - "get_state" | - "set_state" | - // Simple random data - "rand" | - "ranf" | - "sample" | - "randn" | - "randint" | - "random" | - "random_integers" | - "random_sample" | - "choice" | - "bytes" | - // Permutations - "shuffle" | - "permutation" | - // Distributions - "beta" | - "binomial" | - "chisquare" | - "dirichlet" | - "exponential" | - "f" | - "gamma" | - "geometric" | - "gumbel" | - "hypergeometric" | - "laplace" | - "logistic" | - "lognormal" | - "logseries" | - "multinomial" | - "multivariate_normal" | - "negative_binomial" | - "noncentral_chisquare" | - "noncentral_f" | - "normal" | - "pareto" | - "poisson" | - "power" | - "rayleigh" | - "standard_cauchy" | - "standard_exponential" | - "standard_gamma" | - "standard_normal" | - "standard_t" | - "triangular" | - "uniform" | - "vonmises" | - "wald" | - "weibull" | - "zipf" - ] - ) { - Some(qualified_name.segments()[2]) - } else { - None - } - }) - { - checker.report_diagnostic( - NumpyLegacyRandom { - method_name: method_name.to_string(), - }, - expr.range(), - ); + let Some(method_name) = checker.semantic().resolve_qualified_name(expr) else { + return; + }; + + let ["numpy", "random", method_name] = method_name.segments() else { + return; + }; + + match *method_name { + // seeds + "seed" | "get_state" | "set_state" => {} + + // simple random data + "rand" | "ranf" | "sample" | "randn" | "randint" | "random" | "random_integers" + | "random_sample" | "choice" | "bytes" => {} + + // permutations + "shuffle" | "permutation" => {} + + // distributions + "beta" + | "binomial" + | "chisquare" + | "dirichlet" + | "exponential" + | "f" + | "gamma" + | "geometric" + | "gumbel" + | "hypergeometric" + | "laplace" + | "logistic" + | "lognormal" + | "logseries" + | "multinomial" + | "multivariate_normal" + | "negative_binomial" + | "noncentral_chisquare" + | "noncentral_f" + | "normal" + | "pareto" + | "poisson" + | "power" + | "rayleigh" + | "standard_cauchy" + | "standard_exponential" + | "standard_gamma" + | "standard_normal" + | "standard_t" + | "triangular" + | "uniform" + | "vonmises" + | "wald" + | "weibull" + | "zipf" => {} + _ => return, } + + checker.report_diagnostic( + NumpyLegacyRandom { + method_name: method_name.to_string(), + }, + expr.range(), + ); } diff --git a/crates/ruff_linter/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs b/crates/ruff_linter/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs index f88853faab..0f54c1e474 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/rules/constant_imported_as_non_constant.rs @@ -72,24 +72,32 @@ pub(crate) fn constant_imported_as_non_constant( stmt: &Stmt, ignore_names: &IgnoreNames, ) { - if str::is_cased_uppercase(name) - && !(str::is_cased_uppercase(asname) - // Single-character names are ambiguous. - // It could be a class or a constant, so allow it to be imported - // as `SCREAMING_SNAKE_CASE` *or* `CamelCase`. - || (name.chars().nth(1).is_none() && helpers::is_camelcase(asname))) - { - // Ignore any explicitly-allowed names. - if ignore_names.matches(name) || ignore_names.matches(asname) { - return; - } - let mut diagnostic = checker.report_diagnostic( - ConstantImportedAsNonConstant { - name: name.to_string(), - asname: asname.to_string(), - }, - alias.range(), - ); - diagnostic.set_parent(stmt.start()); + if !str::is_cased_uppercase(name) { + return; } + + if str::is_cased_uppercase(asname) { + return; + } + + // Single-character names are ambiguous. + // It could be a class or a constant, so allow it to be imported + // as `SCREAMING_SNAKE_CASE` *or* `CamelCase`. + if name.chars().nth(1).is_none() && helpers::is_camelcase(asname) { + return; + } + + // Ignore any explicitly-allowed names. + if ignore_names.matches(name) || ignore_names.matches(asname) { + return; + } + + let mut diagnostic = checker.report_diagnostic( + ConstantImportedAsNonConstant { + name: name.to_string(), + asname: asname.to_string(), + }, + alias.range(), + ); + diagnostic.set_parent(stmt.start()); } diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index 0d6ea9c1a6..3642869ef8 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -167,32 +167,32 @@ fn analyze_escape_chars( // If the next character is a valid escape sequence, skip. // See: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals. + // + // N.B. 'N', 'u' and 'U' are escape sequences only recognized in string literals if matches!( next_char, - '\n' - | '\\' - | '\'' - | '"' - | 'a' - | 'b' - | 'f' - | 'n' - | 'r' - | 't' - | 'v' - | '0' - | '1' - | '2' - | '3' - | '4' - | '5' - | '6' - | '7' - | 'x' - // Escape sequences only recognized in string literals - | 'N' - | 'u' - | 'U' + '\n' | '\\' + | '\'' + | '"' + | 'a' + | 'b' + | 'f' + | 'n' + | 'r' + | 't' + | 'v' + | '0' + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | 'x' + | 'N' + | 'u' + | 'U' ) { contains_valid_escape_sequence = true; continue; diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs index ffe43270b0..db28627b28 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/logical_lines/missing_whitespace_around_operator.rs @@ -357,7 +357,7 @@ fn diagnostic_kind_for_operator<'a>( } fn is_whitespace_needed(kind: TokenKind) -> bool { - matches!( + if matches!( kind, TokenKind::DoubleStarEqual | TokenKind::StarEqual @@ -386,8 +386,14 @@ fn is_whitespace_needed(kind: TokenKind) -> bool { | TokenKind::ColonEqual | TokenKind::Slash | TokenKind::Percent - ) || kind.is_arithmetic() - || (kind.is_bitwise_or_shift() && - // As a special-case, pycodestyle seems to ignore whitespace around the tilde. - !matches!(kind, TokenKind::Tilde)) + ) { + return true; + } + + if kind.is_arithmetic() { + return true; + } + + // As a special-case, pycodestyle seems to ignore whitespace around the tilde. + kind.is_bitwise_or_shift() && kind != TokenKind::Tilde } 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 e15c74fc69..e3ea55510f 100644 --- a/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs +++ b/crates/ruff_linter/src/rules/pydoclint/rules/check_docstring.rs @@ -1277,20 +1277,19 @@ pub(crate) fn check_docstring( if !definition.is_property(extra_property_decorators, semantic) { if !body_entries.returns.is_empty() { match function_def.returns.as_deref() { + // Ignore it if it's annotated as returning `None` + // or it's a generator function annotated as returning `None`, + // i.e. any of `-> None`, `-> Iterator[...]` or `-> Generator[..., ..., None]` Some(returns) - // Ignore it if it's annotated as returning `None` - // or it's a generator function annotated as returning `None`, - // i.e. any of `-> None`, `-> Iterator[...]` or `-> Generator[..., ..., None]` if !returns.is_none_literal_expr() && !is_generator_function_annotated_as_returning_none( &body_entries, returns, semantic, - ) - => { - checker - .report_diagnostic(DocstringMissingReturns, docstring.range()); - } + ) => + { + checker.report_diagnostic(DocstringMissingReturns, docstring.range()); + } None if body_entries .returns .iter() 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 b64bcc0899..45fa7f984e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs @@ -164,12 +164,14 @@ impl Violation for UnusedImport { match context { UnusedImportContext::ExceptHandler => { format!( - "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability" + "`{name}` imported but unused; \ + consider using `importlib.util.find_spec` to test for availability" ) } UnusedImportContext::DunderInitFirstParty { .. } => { format!( - "`{name}` imported but unused; consider removing, adding to `__all__`, or using a redundant alias" + "`{name}` imported but unused; \ + consider removing, adding to `__all__`, or using a redundant alias" ) } UnusedImportContext::Other => format!("`{name}` imported but unused"), @@ -196,7 +198,8 @@ impl Violation for UnusedImport { submodule_import: true, } => { return Some(format!( - "Use an explicit re-export: `import {parent} as {parent}; import {binding}`", + "Use an explicit re-export: \ + `import {parent} as {parent}; import {binding}`", parent = binding .split('.') .next() @@ -409,19 +412,22 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) { } else if in_init && binding.scope.is_global() && is_first_party(&binding.import, checker) - // In the situation where we have - // ``` - // import a.b # <-- at this binding - // import a.c - // - // __all__ = ["a"] - // ``` - // we should not recommend that we re-export the - // symbol `a` or add it to `__all__`. - // - // So we look up the name `a` and see if it has - // a reference in `__all__`. - && (!is_refined_submodule_import_match_enabled(checker.settings())||!symbol_used_in_dunder_all(checker.semantic(), &binding)) + && ( + // In the situation where we have + // ``` + // import a.b # <-- at this binding + // import a.c + // + // __all__ = ["a"] + // ``` + // we should not recommend that we re-export the + // symbol `a` or add it to `__all__`. + // + // So we look up the name `a` and see if it has + // a reference in `__all__`. + !is_refined_submodule_import_match_enabled(checker.settings()) + || !symbol_used_in_dunder_all(checker.semantic(), &binding) + ) { UnusedImportContext::DunderInitFirstParty { dunder_all_count: DunderAllCount::from(dunder_all_exprs.len()), @@ -667,12 +673,16 @@ fn unused_imports_in_scope<'a, 'b>( .filter(|(_, bdg)| !bdg.is_global() && !bdg.is_nonlocal() && !bdg.is_explicit_export()) .flat_map(|(id, bdg)| { if is_refined_submodule_import_match_enabled(settings) - // No need to apply refined logic if there is only a single binding - && scope.shadowed_bindings(id).nth(1).is_some() - // Only apply the new logic in certain situations to avoid - // complexity, false positives, and intersection with - // `redefined-while-unused` (`F811`). - && has_simple_shadowed_bindings(scope, id, semantic) + && ( + // No need to apply refined logic if there is only a single binding + scope.shadowed_bindings(id).nth(1).is_some() + ) + && ( + // Only apply the new logic in certain situations to avoid + // complexity, false positives, and intersection with + // `redefined-while-unused` (`F811`). + has_simple_shadowed_bindings(scope, id, semantic) + ) { unused_imports_from_binding(semantic, id, scope) } else if bdg.is_used() { @@ -743,11 +753,14 @@ fn unused_imports_from_binding<'a, 'b>( for ref_id in binding.references() { let resolved_reference = semantic.reference(ref_id); if !marked_dunder_all && resolved_reference.in_dunder_all_definition() { - let first = *binding - .as_any_import() - .expect("binding to be import binding since current function called after restricting to these in `unused_imports_in_scope`") - .qualified_name() - .segments().first().expect("import binding to have nonempty qualified name"); + let first = binding + .as_any_import() + .expect( + "The binding should be an import binding since current function \ + called after restricting to these in `unused_imports_in_scope`", + ) + .qualified_name() + .segments()[0]; mark_uses_of_qualified_name(&mut marked, &QualifiedName::user_defined(first)); marked_dunder_all = true; continue; diff --git a/crates/ruff_linter/src/rules/pylint/helpers.rs b/crates/ruff_linter/src/rules/pylint/helpers.rs index 95f9054ca2..e3ac9bb5e8 100644 --- a/crates/ruff_linter/src/rules/pylint/helpers.rs +++ b/crates/ruff_linter/src/rules/pylint/helpers.rs @@ -222,99 +222,104 @@ pub(crate) fn is_dunder_operator_method(method: &str) -> bool { /// Returns `true` if a method is a known dunder method. pub(super) fn is_known_dunder_method(method: &str) -> bool { - is_dunder_operator_method(method) - || matches!( - method, - "__abs__" - | "__aenter__" - | "__aexit__" - | "__aiter__" - | "__anext__" - | "__attrs_init__" - | "__attrs_post_init__" - | "__attrs_pre_init__" - | "__await__" - | "__bool__" - | "__buffer__" - | "__bytes__" - | "__call__" - | "__ceil__" - | "__class__" - | "__class_getitem__" - | "__complex__" - | "__contains__" - | "__copy__" - | "__deepcopy__" - | "__del__" - | "__delattr__" - | "__delete__" - | "__delitem__" - | "__dict__" - | "__dir__" - | "__doc__" - | "__enter__" - | "__exit__" - | "__float__" - | "__floor__" - | "__format__" - | "__fspath__" - | "__get__" - | "__getattr__" - | "__getattribute__" - | "__getitem__" - | "__getnewargs__" - | "__getnewargs_ex__" - | "__getstate__" - | "__hash__" - | "__html__" - | "__index__" - | "__init__" - | "__init_subclass__" - | "__instancecheck__" - | "__int__" - | "__invert__" - | "__iter__" - | "__len__" - | "__length_hint__" - | "__missing__" - | "__module__" - | "__mro_entries__" - | "__neg__" - | "__new__" - | "__next__" - | "__pos__" - | "__post_init__" - | "__prepare__" - | "__reduce__" - | "__reduce_ex__" - | "__release_buffer__" - | "__replace__" - | "__repr__" - | "__reversed__" - | "__round__" - | "__set__" - | "__set_name__" - | "__setattr__" - | "__setitem__" - | "__setstate__" - | "__sizeof__" - | "__str__" - | "__subclasscheck__" - | "__subclasses__" - | "__subclasshook__" - | "__trunc__" - | "__weakref__" - // Overridable sunder names from the `Enum` class. - // See: https://docs.python.org/3/library/enum.html#supported-sunder-names - | "_add_alias_" - | "_add_value_alias_" - | "_name_" - | "_value_" - | "_missing_" - | "_ignore_" - | "_order_" - | "_generate_next_value_" - ) + if is_dunder_operator_method(method) { + return true; + } + + match method { + "__abs__" + | "__aenter__" + | "__aexit__" + | "__aiter__" + | "__anext__" + | "__attrs_init__" + | "__attrs_post_init__" + | "__attrs_pre_init__" + | "__await__" + | "__bool__" + | "__buffer__" + | "__bytes__" + | "__call__" + | "__ceil__" + | "__class__" + | "__class_getitem__" + | "__complex__" + | "__contains__" + | "__copy__" + | "__deepcopy__" + | "__del__" + | "__delattr__" + | "__delete__" + | "__delitem__" + | "__dict__" + | "__dir__" + | "__doc__" + | "__enter__" + | "__exit__" + | "__float__" + | "__floor__" + | "__format__" + | "__fspath__" + | "__get__" + | "__getattr__" + | "__getattribute__" + | "__getitem__" + | "__getnewargs__" + | "__getnewargs_ex__" + | "__getstate__" + | "__hash__" + | "__html__" + | "__index__" + | "__init__" + | "__init_subclass__" + | "__instancecheck__" + | "__int__" + | "__invert__" + | "__iter__" + | "__len__" + | "__length_hint__" + | "__missing__" + | "__module__" + | "__mro_entries__" + | "__neg__" + | "__new__" + | "__next__" + | "__pos__" + | "__post_init__" + | "__prepare__" + | "__reduce__" + | "__reduce_ex__" + | "__release_buffer__" + | "__replace__" + | "__repr__" + | "__reversed__" + | "__round__" + | "__set__" + | "__set_name__" + | "__setattr__" + | "__setitem__" + | "__setstate__" + | "__sizeof__" + | "__str__" + | "__subclasscheck__" + | "__subclasses__" + | "__subclasshook__" + | "__trunc__" + | "__weakref__" => true, + + // Overridable sunder names from the `Enum` class. + // See: https://docs.python.org/3/library/enum.html#supported-sunder-names + "_add_alias_" + | "_add_value_alias_" + | "_name_" + | "_value_" + | "_missing_" + | "_ignore_" + | "_order_" + | "_generate_next_value_" => true, + + _ => false, + } } pub(super) fn num_statements(stmts: &[Stmt]) -> usize { diff --git a/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs b/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs index 5ecdb74187..9ecd107caf 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/repeated_equality_comparison.rs @@ -65,16 +65,17 @@ impl AlwaysFixableViolation for RepeatedEqualityComparison { match (self.expression.full_display(), self.all_hashable) { (Some(expression), false) => { format!( - "Consider merging multiple comparisons: `{expression}`. Use a `set` if the elements are hashable." + "Consider merging multiple comparisons: `{expression}`. \ + Use a `set` if the elements are hashable." ) } (Some(expression), true) => { format!("Consider merging multiple comparisons: `{expression}`.") } - (None, false) => { - "Consider merging multiple comparisons. Use a `set` if the elements are hashable." - .to_string() - } + (None, false) => "\ + Consider merging multiple comparisons. \ + Use a `set` if the elements are hashable." + .to_string(), (None, true) => "Consider merging multiple comparisons.".to_string(), } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs index 2b27d04a66..7bbc469d03 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unnecessary_dunder_call.rs @@ -214,20 +214,23 @@ pub(crate) fn unnecessary_dunder_call(checker: &Checker, call: &ast::ExprCall) { if let Some((mut fixed, precedence)) = fixed { let dunder = DunderReplacement::from_method(attr); - // We never need to wrap builtin functions in extra parens - // since function calls have high precedence - let wrap_in_paren = (!matches!(dunder, Some(DunderReplacement::Builtin(_,_)))) - // If parent expression has higher precedence then the new replacement, + // If the parent expression has higher precedence then the new replacement, // it would associate with either the left operand (e.g. naive change from `a * b.__add__(c)` // becomes `a * b + c` which is incorrect) or the right operand (e.g. naive change from // `a.__add__(b).attr` becomes `a + b.attr` which is also incorrect). // This rule doesn't apply to function calls despite them having higher // precedence than any of our replacement, since they already wrap around - // our expression e.g. `print(a.__add__(3))` -> `print(a + 3)` + // our expression e.g. `print(a.__add__(3))` -> `print(a + 3)`. + // + // Note that we never need to wrap *builtin* functions in extra parens + // since function calls have high precedence + let wrap_in_paren = (!matches!(dunder, Some(DunderReplacement::Builtin(_, _)))) && checker .semantic() .current_expression_parent() - .is_some_and(|parent| !parent.is_call_expr() && OperatorPrecedence::from_expr(parent) > precedence); + .is_some_and(|parent| { + !parent.is_call_expr() && OperatorPrecedence::from_expr(parent) > precedence + }); if wrap_in_paren { fixed = format!("({fixed})"); diff --git a/crates/ruff_linter/src/rules/pylint/rules/useless_else_on_loop.rs b/crates/ruff_linter/src/rules/pylint/rules/useless_else_on_loop.rs index e1a1a20505..900a8d1e31 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/useless_else_on_loop.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/useless_else_on_loop.rs @@ -55,7 +55,9 @@ impl Violation for UselessElseOnLoop { #[derive_message_formats] fn message(&self) -> String { - "`else` clause on loop without a `break` statement; remove the `else` and dedent its contents".to_string() + "`else` clause on loop without a `break` statement; \ + remove the `else` and dedent its contents" + .to_string() } fn fix_title(&self) -> Option { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs index 4405cb6c1a..55d921cf42 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_mock_import.rs @@ -283,55 +283,54 @@ pub(crate) fn deprecated_mock_attribute(checker: &Checker, attribute: &ast::Expr /// UP026 pub(crate) fn deprecated_mock_import(checker: &Checker, stmt: &Stmt) { match stmt { + // Find all `mock` imports. Stmt::Import(ast::StmtImport { names, is_lazy: _, range: _, node_index: _, - }) - // Find all `mock` imports. - if names - .iter() - .any(|name| &name.name == "mock" || &name.name == "mock.mock") - => { - // Generate the fix, if needed, which is shared between all `mock` imports. - let content = if let Some(indent) = indentation(checker.source(), stmt) { - match format_import(stmt, indent, checker.locator(), checker.stylist()) { - Ok(content) => Some(content), - Err(e) => { - debug!("Failed to rewrite `mock` import: {e}"); - None - } + }) if names + .iter() + .any(|name| &name.name == "mock" || &name.name == "mock.mock") => + { + // Generate the fix, if needed, which is shared between all `mock` imports. + let content = if let Some(indent) = indentation(checker.source(), stmt) { + match format_import(stmt, indent, checker.locator(), checker.stylist()) { + Ok(content) => Some(content), + Err(e) => { + debug!("Failed to rewrite `mock` import: {e}"); + None } - } else { - None - }; + } + } else { + None + }; - // Add a `Diagnostic` for each `mock` import. - for name in names { - if (&name.name == "mock" || &name.name == "mock.mock") - && !is_import_required_by_isort( - &checker.settings().isort.required_imports, - stmt.into(), - name, - ) - { - let mut diagnostic = checker.report_diagnostic( - DeprecatedMockImport { - reference_type: MockReference::Import, - }, - name.range(), - ); - diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated); - if let Some(content) = content.as_ref() { - diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( - content.clone(), - stmt.range(), - ))); - } + // Add a `Diagnostic` for each `mock` import. + for name in names { + if (&name.name == "mock" || &name.name == "mock.mock") + && !is_import_required_by_isort( + &checker.settings().isort.required_imports, + stmt.into(), + name, + ) + { + let mut diagnostic = checker.report_diagnostic( + DeprecatedMockImport { + reference_type: MockReference::Import, + }, + name.range(), + ); + diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated); + if let Some(content) = content.as_ref() { + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + content.clone(), + stmt.range(), + ))); } } } + } Stmt::ImportFrom(ast::StmtImportFrom { module: Some(module), level, diff --git a/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs b/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs index 4e436522f8..b0dfb6dd76 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/check_and_remove_from_set.rs @@ -84,14 +84,20 @@ pub(crate) fn check_and_remove_from_set(checker: &Checker, if_stmt: &ast::StmtIf return; }; - // ` // `set` in the check should be the same as `set` in the body - if check_set.id != remove_set.id - // `element` in the check should be the same as `element` in the body - || !compare(&check_element.into(), &remove_element.into()) - // `element` shouldn't have a side effect, otherwise we might change the semantics of the program. - || contains_effect(check_element, |id| checker.semantic().has_builtin_binding(id)) - { + if check_set.id != remove_set.id { + return; + } + + // `element` in the check should be the same as `element` in the body + if !compare(&check_element.into(), &remove_element.into()) { + return; + } + + // `element` shouldn't have a side effect, otherwise we might change the semantics of the program. + if contains_effect(check_element, |id| { + checker.semantic().has_builtin_binding(id) + }) { return; } diff --git a/crates/ruff_linter/src/rules/ruff/helpers.rs b/crates/ruff_linter/src/rules/ruff/helpers.rs index a94ece694c..ba28950a92 100644 --- a/crates/ruff_linter/src/rules/ruff/helpers.rs +++ b/crates/ruff_linter/src/rules/ruff/helpers.rs @@ -30,10 +30,10 @@ fn is_attrs_field(func: &Expr, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(func) .is_some_and(|qualified_name| { + // See https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py#L33 matches!( qualified_name.segments(), ["attrs", "field" | "Factory"] - // See https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py#L33 | ["attr", "ib" | "attr" | "attrib" | "field" | "Factory"] ) }) @@ -121,8 +121,8 @@ pub(super) fn dataclass_kind<'a>( }; match qualified_name.segments() { - ["attrs" | "attr", func @ ("define" | "frozen" | "mutable")] // See https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py#L32 + ["attrs" | "attr", func @ ("define" | "frozen" | "mutable")] | ["attr", func @ ("s" | "attributes" | "attrs")] => { // `.define`, `.frozen` and `.mutable` all default `auto_attribs` to `None`, // whereas `@attr.s` implicitly sets `auto_attribs=False`. diff --git a/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs b/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs index de3c072bdd..c4a708cd69 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/falsy_dict_get_fallback.rs @@ -48,7 +48,9 @@ impl Violation for FalsyDictGetFallback { #[derive_message_formats] fn message(&self) -> String { - "Avoid providing a falsy fallback to `dict.get()` in boolean test positions. The default fallback `None` is already falsy.".to_string() + "Avoid providing a falsy fallback to `dict.get()` in boolean test positions. \ + The default fallback `None` is already falsy." + .to_string() } fn fix_title(&self) -> Option { diff --git a/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs b/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs index 6e2351aafb..ebb85cf307 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/parenthesize_chained_operators.rs @@ -40,7 +40,9 @@ pub(crate) struct ParenthesizeChainedOperators; impl AlwaysFixableViolation for ParenthesizeChainedOperators { #[derive_message_formats] fn message(&self) -> String { - "Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear".to_string() + "Parenthesize `a and b` expressions when chaining `and` and `or` together, \ + to make the precedence clear" + .to_string() } fn fix_title(&self) -> String { diff --git a/crates/ruff_linter/src/rules/ruff/typing.rs b/crates/ruff_linter/src/rules/ruff/typing.rs index 0c1b355860..22ebaa401e 100644 --- a/crates/ruff_linter/src/rules/ruff/typing.rs +++ b/crates/ruff_linter/src/rules/ruff/typing.rs @@ -253,9 +253,8 @@ pub(crate) fn type_hint_explicitly_allows_none<'a>( version: ast::PythonVersion, ) -> Option<&'a Expr> { match TypingTarget::try_from_expr(annotation, checker, version) { - None | - // Short circuit on top level `None`, `Any` or `Optional` - Some(TypingTarget::None | TypingTarget::Optional(_) | TypingTarget::Any) => None, + // Short-circuit on top level `None`, `Any` or `Optional` + None | Some(TypingTarget::None | TypingTarget::Optional(_) | TypingTarget::Any) => None, // Top-level `Annotated` node should check for the inner type and // return the inner type if it doesn't allow `None`. If `Annotated` // is found nested inside another type, then the outer type should diff --git a/crates/ruff_macros/src/rule_namespace.rs b/crates/ruff_macros/src/rule_namespace.rs index 34ef3252da..04b699dcaa 100644 --- a/crates/ruff_macros/src/rule_namespace.rs +++ b/crates/ruff_macros/src/rule_namespace.rs @@ -34,18 +34,44 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result return Err(Error::new(lit.span(), "expected prefix string to be non-empty")), - Some(c) => if !first_chars.insert(c) { - return Err(Error::new(lit.span(), format!("this variant already has another prefix starting with the character '{c}'"))) + None => { + return Err(Error::new( + lit.span(), + "expected prefix string to be non-empty", + )); + } + Some(c) => { + if !first_chars.insert(c) { + return Err(Error::new( + lit.span(), + format!( + "this variant already has another prefix \ + starting with the character '{c}'" + ), + )); + } } } if !all_prefixes.insert(str.clone()) { - return Err(Error::new(lit.span(), "prefix has already been defined before")); + return Err(Error::new( + lit.span(), + "prefix has already been defined before", + )); } Ok(str) }) @@ -155,7 +181,8 @@ fn parse_doc_attr(doc_attr: &Attribute) -> syn::Result<(String, String)> { .ok_or_else(|| { Error::new( doc_lit.span(), - "expected doc comment to be in the form of `/// [name](https://example.com/)`", + "expected doc comment to be in the form of \ + `/// [name](https://example.com/)`", ) }) } diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 43d8547eb1..288793a9eb 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -395,11 +395,15 @@ where range_start: _, node_index: _, }) => { + // Note that this is the evaluation order but not necessarily the declaration order + // (e.g. for `f(*args, a=2, *args2, **kwargs)` it's not) any_over_expr(call_func, &mut *func) - // Note that this is the evaluation order but not necessarily the declaration order - // (e.g. for `f(*args, a=2, *args2, **kwargs)` it's not) - || arguments.args.iter().any(|expr| any_over_expr(expr, &mut *func)) - || arguments.keywords + || arguments + .args + .iter() + .any(|expr| any_over_expr(expr, &mut *func)) + || arguments + .keywords .iter() .any(|keyword| any_over_expr(&keyword.value, &mut *func)) } diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index b7d00c790f..50e16e5fcc 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -624,10 +624,9 @@ fn handle_own_line_comment_between_branches<'a>( // pass // ``` || { - comment_indentation - // This can be any positive number - we just - // want to hit the `Less` branch below - + TextSize::new(1) + // We could use any positive number here, it doesn't have to be `1` + // - we just want to hit the `Less` branch below + comment_indentation + TextSize::new(1) }, ruff_text_size::TextLen::text_len, ); diff --git a/crates/ruff_python_formatter/src/expression/expr_attribute.rs b/crates/ruff_python_formatter/src/expression/expr_attribute.rs index e2784f885a..f8dec418ca 100644 --- a/crates/ruff_python_formatter/src/expression/expr_attribute.rs +++ b/crates/ruff_python_formatter/src/expression/expr_attribute.rs @@ -125,10 +125,12 @@ impl FormatNodeRule for FormatExprAttribute { if parenthesize_value || value.is_call_expr() || value.is_subscript_expr() - // Remember to update the doc-comment above when - // stabilizing this behavior. - || (is_fluent_layout_split_first_call_enabled(f.context()) - && call_chain_layout.is_first_call_like()) + || ( + // Remember to update the doc-comment above when + // stabilizing this behavior. + is_fluent_layout_split_first_call_enabled(f.context()) + && call_chain_layout.is_first_call_like() + ) { soft_line_break().fmt(f)?; } diff --git a/crates/ruff_python_formatter/src/pattern/mod.rs b/crates/ruff_python_formatter/src/pattern/mod.rs index 0b3487abb5..dd92a63f8b 100644 --- a/crates/ruff_python_formatter/src/pattern/mod.rs +++ b/crates/ruff_python_formatter/src/pattern/mod.rs @@ -288,16 +288,20 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> { } Pattern::MatchValue(value) => match &*value.value { - Expr::StringLiteral(_) | - Expr::BytesLiteral(_) | - // F-strings are allowed according to python's grammar but fail with a syntax error at runtime. - // That's why we need to support them for formatting. - Expr::FString(_) | - Expr::TString(_)| - Expr::NumberLiteral(_) | Expr::Attribute(_) | Expr::UnaryOp(_) => { + Expr::StringLiteral(_) + | Expr::BytesLiteral(_) + | Expr::TString(_) + | Expr::NumberLiteral(_) + | Expr::Attribute(_) + | Expr::UnaryOp(_) => { // require no state update other than visit_pattern does. } + Expr::FString(_) => { + // F-strings are allowed according to python's grammar but fail with a syntax error at runtime. + // That's why we need to support them for formatting. + } + // `case 4+3j:` or `case 4-3j: // Cannot contain arbitrary expressions. Limited to complex numbers. Expr::BinOp(_) => { diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 661ec08e5b..6b06012078 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -526,34 +526,33 @@ fn trailing_function_or_class_def<'a>( preceding.map(AnyNodeRef::from), AnyNodeRef::last_child_in_body, ) - .take_while(|last_child| - // If there is a comment between preceding and following the empty lines were - // inserted before the comment by preceding and there are no extra empty lines - // after the comment. - // ```python - // class Test: - // def a(self): - // pass - // # trailing comment - // - // - // # two lines before, one line after - // - // c = 30 - // ```` - // This also includes nested class/function definitions, so we stop recursing - // once we see a node with a trailing own line comment: - // ```python - // def f(): - // if True: - // - // def double(s): - // return s + s - // - // # nested trailing own line comment - // print("below function with trailing own line comment") - // ``` - !comments.has_trailing_own_line(*last_child)) + // If there is a comment between preceding and following the empty lines were + // inserted before the comment by preceding and there are no extra empty lines + // after the comment. + // ```python + // class Test: + // def a(self): + // pass + // # trailing comment + // + // + // # two lines before, one line after + // + // c = 30 + // ```` + // This also includes nested class/function definitions, so we stop recursing + // once we see a node with a trailing own line comment: + // ```python + // def f(): + // if True: + // + // def double(s): + // return s + s + // + // # nested trailing own line comment + // print("below function with trailing own line comment") + // ``` + .take_while(|last_child| !comments.has_trailing_own_line(*last_child)) .find(|last_child| { matches!( last_child, diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 73add7a073..7b7adc5e54 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -417,11 +417,11 @@ impl<'src> Parser<'src> { ); } } else { + // > The power operator `**` binds less tightly than an arithmetic + // > or bitwise unary operator on its right, that is, 2**-1 is 0.5. + // + // Reference: https://docs.python.org/3/reference/expressions.html#id21 if left_precedence > OperatorPrecedence::PosNegBitNot - // > The power operator `**` binds less tightly than an arithmetic - // > or bitwise unary operator on its right, that is, 2**-1 is 0.5. - // - // Reference: https://docs.python.org/3/reference/expressions.html#id21 && left_precedence != OperatorPrecedence::Exponent { self.add_error( diff --git a/crates/ruff_python_parser/src/parser/mod.rs b/crates/ruff_python_parser/src/parser/mod.rs index 641b0190f4..31a8d29414 100644 --- a/crates/ruff_python_parser/src/parser/mod.rs +++ b/crates/ruff_python_parser/src/parser/mod.rs @@ -393,16 +393,19 @@ impl<'src> Parser<'src> { /// Moves the parser to the next token. fn do_bump(&mut self, kind: TokenKind) { - if !matches!( - self.current_token_kind(), + if match self.current_token_kind() { // TODO explore including everything up to the dedent as part of the body. - TokenKind::Dedent + TokenKind::Dedent => false, + // Don't include newlines in the body - | TokenKind::Newline + TokenKind::Newline => false, + // TODO(micha): Including the semi feels more correct but it isn't compatible with lalrpop and breaks the // formatters semicolon detection. Exclude it for now - | TokenKind::Semi - ) { + TokenKind::Semi => false, + + _ => true, + } { self.prev_token_end = self.current_token_range().end(); } @@ -1230,24 +1233,26 @@ enum RecoveryContextKind { impl RecoveryContextKind { /// Returns `true` if a trailing comma is allowed in the current context. const fn allow_trailing_comma(self) -> bool { - matches!( - self, + match self { RecoveryContextKind::Slices - | RecoveryContextKind::TupleElements(_) - | RecoveryContextKind::SetElements - | RecoveryContextKind::ListElements - | RecoveryContextKind::DictElements - | RecoveryContextKind::Arguments - | RecoveryContextKind::MatchPatternMapping - | RecoveryContextKind::SequenceMatchPattern(_) - | RecoveryContextKind::MatchPatternClassArguments - // Only allow a trailing comma if the with item itself is parenthesized - | RecoveryContextKind::WithItems(WithItemKind::Parenthesized) - | RecoveryContextKind::Parameters(_) - | RecoveryContextKind::TypeParams - | RecoveryContextKind::DeleteTargets - | RecoveryContextKind::ImportFromAsNames(Parenthesized::Yes) - ) + | RecoveryContextKind::TupleElements(_) + | RecoveryContextKind::SetElements + | RecoveryContextKind::ListElements + | RecoveryContextKind::DictElements + | RecoveryContextKind::Arguments + | RecoveryContextKind::MatchPatternMapping + | RecoveryContextKind::SequenceMatchPattern(_) + | RecoveryContextKind::MatchPatternClassArguments + | RecoveryContextKind::Parameters(_) + | RecoveryContextKind::TypeParams + | RecoveryContextKind::DeleteTargets + | RecoveryContextKind::ImportFromAsNames(Parenthesized::Yes) => true, + + // Only allow a trailing comma if the with item itself is parenthesized + RecoveryContextKind::WithItems(WithItemKind::Parenthesized) => true, + + _ => false, + } } /// Returns `true` if the parser is at a token that terminates the list as per the context. diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 910a7fd04f..a13865afc3 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -1150,8 +1150,9 @@ impl<'src> Parser<'src> { } else { parser.add_error( ParseErrorType::OtherError( - "Only integer literals are allowed in subscript expressions in help end escape command" - .to_string() + "Only integer literals are allowed in subscript expressions \ + in help end escape command" + .to_string(), ), slice.range(), ); @@ -1168,8 +1169,9 @@ impl<'src> Parser<'src> { _ => { parser.add_error( ParseErrorType::OtherError( - "Expected name, subscript or attribute expression in help end escape command" - .to_string() + "Expected name, subscript or attribute expression \ + in help end escape command" + .to_string(), ), expr, ); @@ -2396,11 +2398,13 @@ impl<'src> Parser<'src> { self.add_error(error, &parsed_with_item.item.context_expr); } } else if self.at(TokenKind::Rpar) - // test_err with_items_parenthesized_missing_colon - // # `)` followed by a newline - // with (item1, item2) - // pass - && matches!(self.peek(), TokenKind::Colon | TokenKind::Newline) + && ( + // test_err with_items_parenthesized_missing_colon + // # `)` followed by a newline + // with (item1, item2) + // pass + matches!(self.peek(), TokenKind::Colon | TokenKind::Newline) + ) { if parsed_with_items.is_empty() { // No with items, treat it as a parenthesized expression to create an empty @@ -3050,7 +3054,9 @@ impl<'src> Parser<'src> { // x = 1 self.add_error( ParseErrorType::OtherError( - "Expected class, function definition or async function definition after decorator".to_string(), + "Expected class, function definition or async function definition \ + after decorator" + .to_string(), ), self.current_token_range(), ); @@ -3349,11 +3355,14 @@ impl<'src> Parser<'src> { let star_range = parser.current_token_range(); parser.bump(TokenKind::Star); - kwonlyargs_snapshot - .get_or_insert_with(|| parser.parameter_scratch.snapshot()); + kwonlyargs_snapshot.get_or_insert_with(|| parser.parameter_scratch.snapshot()); if parser.at_name_or_soft_keyword() { - let param = parser.parse_parameter(param_start, function_kind, AllowStarAnnotation::Yes); + let param = parser.parse_parameter( + param_start, + function_kind, + AllowStarAnnotation::Yes, + ); let param_star_range = parser.node_range(star_range.start()); if parser.at(TokenKind::Equal) { @@ -3405,7 +3414,8 @@ impl<'src> Parser<'src> { // def foo(a, *args, b, c, *): ... parser.add_error( ParseErrorType::OtherError( - "Keyword-only parameter separator not allowed after '*' parameter" + "Keyword-only parameter separator not allowed \ + after '*' parameter" .to_string(), ), star_range, @@ -3420,7 +3430,8 @@ impl<'src> Parser<'src> { let double_star_range = parser.current_token_range(); parser.bump(TokenKind::DoubleStar); - let param = parser.parse_parameter(param_start, function_kind, AllowStarAnnotation::No); + let param = + parser.parse_parameter(param_start, function_kind, AllowStarAnnotation::No); let param_double_star_range = parser.node_range(double_star_range.start()); if parameters.kwarg.is_some() { @@ -3547,8 +3558,7 @@ impl<'src> Parser<'src> { // test_err params_non_default_after_default // def foo(a=10, b, c: int): ... - parser - .add_error(ParseErrorType::NonDefaultParamAfterDefaultParam, ¶m); + parser.add_error(ParseErrorType::NonDefaultParamAfterDefaultParam, ¶m); } seen_default_param |= param.default.is_some(); diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index a08e6542c7..ca6560362a 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -69,7 +69,10 @@ fn test_valid_syntax(input_path: &Utf8Path, source: &str, root: &str) { let line_index = LineIndex::from_source_text(source); let source_code = SourceCode::new(source, &line_index); - let mut message = "Expected no syntax errors for a valid program but the parser generated the following errors:\n".to_string(); + let mut message = "\ + Expected no syntax errors for a valid program \ + but the parser generated the following errors:\n" + .to_string(); for error in parsed.errors() { writeln!( @@ -218,7 +221,8 @@ fn test_invalid_syntax(input_path: &Utf8Path, source: &str, root: &str) { assert!( parsed.has_syntax_errors() || !semantic_syntax_errors.is_empty(), - "Expected parser to generate at least one syntax error for a program containing syntax errors." + "Expected parser to generate at least one syntax error \ + for a program containing syntax errors." ); if !semantic_syntax_errors.is_empty() { @@ -463,7 +467,13 @@ impl ValidateAstVisitor<'_> { // At this point, next_token.end() > node.start() assert!( next.start() >= node.start(), - "The start of the node falls within a token.\nNode: {node:#?}\n\nToken: {next:#?}\n\nRoot: {root:#?}", + "\ +The start of the node falls within a token. +Node: {node:#?} + +Token: {next:#?} + +Root: {root:#?}", root = self.parents.first() ); } @@ -482,7 +492,13 @@ impl ValidateAstVisitor<'_> { // At this point, `next_token.end() > node.end()` assert!( next.start() >= node.end(), - "The end of the node falls within a token.\nNode: {node:#?}\n\nToken: {next:#?}\n\nRoot: {root:#?}", + "\ +The end of the node falls within a token. +Node: {node:#?} + +Token: {next:#?} + +Root: {root:#?}", root = self.parents.first() ); } @@ -500,7 +516,13 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { assert_ne!( previous.range().ordering(node.range()), Ordering::Greater, - "The ranges of the nodes are not strictly increasing when traversing the AST in pre-order.\nPrevious node: {previous:#?}\n\nCurrent node: {node:#?}\n\nRoot: {root:#?}", + "\ +The ranges of the nodes are not strictly increasing when traversing the AST in pre-order. +Previous node: {previous:#?} + +Current node: {node:#?} + +Root: {root:#?}", root = self.parents.first() ); } @@ -508,7 +530,13 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> { if let Some(parent) = self.parents.last() { assert!( parent.range().contains_range(node.range()), - "The range of the parent node does not fully enclose the range of the child node.\nParent node: {parent:#?}\n\nChild node: {node:#?}\n\nRoot: {root:#?}", + "\ +The range of the parent node does not fully enclose the range of the child node. +Parent node: {parent:#?} + +Child node: {node:#?} + +Root: {root:#?}", root = self.parents.first() ); } diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 404b33f2e3..eff0ff8a07 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -442,9 +442,8 @@ pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> b // for this specific check even if it's defined somewhere else, like the current module. // Ex) `if TYPE_CHECKING:` Expr::Name(ast::ExprName { id, .. }) => { - id == "TYPE_CHECKING" - // Ex) `if TC:` with `from typing import TYPE_CHECKING as TC` - || semantic.match_typing_expr(test, "TYPE_CHECKING") + // Ex) `if TC:` with `from typing import TYPE_CHECKING as TC` + id == "TYPE_CHECKING" || semantic.match_typing_expr(test, "TYPE_CHECKING") } // Ex) `if typing.TYPE_CHECKING:` Expr::Attribute(ast::ExprAttribute { attr, .. }) => attr == "TYPE_CHECKING", diff --git a/crates/ruff_python_semantic/src/model/all.rs b/crates/ruff_python_semantic/src/model/all.rs index 17ff47540c..4ffefcd717 100644 --- a/crates/ruff_python_semantic/src/model/all.rs +++ b/crates/ruff_python_semantic/src/model/all.rs @@ -159,42 +159,40 @@ impl SemanticModel<'_> { // Allow comprehensions, even though we can't statically analyze them. return (None, DunderAllFlags::empty()); } - Expr::Name(ast::ExprName { id, .. }) - // Ex) `__all__ = __all__ + multiprocessing.__all__` - if id == "__all__" => { - return (None, DunderAllFlags::empty()); - } - Expr::Attribute(ast::ExprAttribute { attr, .. }) - // Ex) `__all__ = __all__ + multiprocessing.__all__` - if attr == "__all__" => { - return (None, DunderAllFlags::empty()); - } + // Ex) `__all__ = __all__ + multiprocessing.__all__` + Expr::Name(ast::ExprName { id, .. }) if id == "__all__" => { + return (None, DunderAllFlags::empty()); + } + // Ex) `__all__ = __all__ + multiprocessing.__all__` + Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "__all__" => { + return (None, DunderAllFlags::empty()); + } + // Allow `tuple()`, `list()`, and their generic forms, like `list[int]()`. Expr::Call(ast::ExprCall { func, arguments, .. - }) - // Allow `tuple()`, `list()`, and their generic forms, like `list[int]()`. - if arguments.keywords.is_empty() && arguments.args.len() <= 1 - && self - .resolve_builtin_symbol(map_subscript(func)) - .is_some_and(|symbol| matches!(symbol, "tuple" | "list")) - => { - let [arg] = arguments.args.as_ref() else { - return (None, DunderAllFlags::empty()); - }; - match arg { - Expr::List(ast::ExprList { elts, .. }) - | Expr::Set(ast::ExprSet { elts, .. }) - | Expr::Tuple(ast::ExprTuple { elts, .. }) => { - return (Some(elts), DunderAllFlags::empty()); - } - _ => { - // We can't analyze other expressions, but they must be - // valid, since the `list` or `tuple` call will ultimately - // evaluate to a list or tuple. - return (None, DunderAllFlags::empty()); - } - } + }) if arguments.keywords.is_empty() + && arguments.args.len() <= 1 + && self + .resolve_builtin_symbol(map_subscript(func)) + .is_some_and(|symbol| matches!(symbol, "tuple" | "list")) => + { + let [arg] = arguments.args.as_ref() else { + return (None, DunderAllFlags::empty()); + }; + match arg { + Expr::List(ast::ExprList { elts, .. }) + | Expr::Set(ast::ExprSet { elts, .. }) + | Expr::Tuple(ast::ExprTuple { elts, .. }) => { + return (Some(elts), DunderAllFlags::empty()); } + _ => { + // We can't analyze other expressions, but they must be + // valid, since the `list` or `tuple` call will ultimately + // evaluate to a list or tuple. + return (None, DunderAllFlags::empty()); + } + } + } Expr::Named(ast::ExprNamed { value, .. }) => { // Allow, e.g., `__all__ += (value := ["A", "B"])`. return self.extract_dunder_all_elts(value); diff --git a/crates/ruff_python_stdlib/src/open_mode.rs b/crates/ruff_python_stdlib/src/open_mode.rs index e2257ebe73..3a145662ba 100644 --- a/crates/ruff_python_stdlib/src/open_mode.rs +++ b/crates/ruff_python_stdlib/src/open_mode.rs @@ -43,7 +43,11 @@ impl OpenMode { if open_mode.contains(OpenMode::UNIVERSAL_NEWLINES) && open_mode.intersects(OpenMode::WRITE | OpenMode::APPEND | OpenMode::CREATE) { - return Err("Open mode cannot contain the universal newlines (`U`) flag with write (`w`), append (`a`), or create (`x`) flags".to_string()); + return Err( + "Open mode cannot contain the universal newlines (`U`) flag \ + with write (`w`), append (`a`), or create (`x`) flags" + .to_string(), + ); } // Otherwise, reading, writing, creating, and appending are mutually exclusive. @@ -58,7 +62,11 @@ impl OpenMode { .count() != 1 { - return Err("Open mode must contain exactly one of the following flags: read (`r`), write (`w`), create (`x`), or append (`a`)".to_string()); + return Err( + "Open mode must contain exactly one of the following flags: \ + read (`r`), write (`w`), create (`x`), or append (`a`)" + .to_string(), + ); } Ok(open_mode) diff --git a/crates/ruff_python_trivia/src/pragmas.rs b/crates/ruff_python_trivia/src/pragmas.rs index 9f62e5e662..dfd17e84bf 100644 --- a/crates/ruff_python_trivia/src/pragmas.rs +++ b/crates/ruff_python_trivia/src/pragmas.rs @@ -18,16 +18,25 @@ pub fn is_pragma_comment(comment: &str) -> bool { let trimmed = content.trim_start(); // Case-insensitive match against `noqa` (which doesn't require a trailing colon). - matches!( + if matches!( trimmed.as_bytes(), [b'n' | b'N', b'o' | b'O', b'q' | b'Q', b'a' | b'A', ..] - ) || - // Case-insensitive match against pragmas that don't require a trailing colon. - trimmed.starts_with("nosec") || - // Case-sensitive match against a variety of pragmas that _do_ require a trailing colon. - trimmed - .split_once(':') - .is_some_and(|(maybe_pragma, _)| matches!(maybe_pragma, "isort" | "type" | "pyright" | "pyrefly" | "pylint" | "flake8" | "ruff" | "ty")) + ) { + return true; + } + + // Case-insensitive match against pragmas that don't require a trailing colon. + if trimmed.starts_with("nosec") { + return true; + } + + // Case-sensitive match against a variety of pragmas that _do_ require a trailing colon. + trimmed.split_once(':').is_some_and(|(maybe_pragma, _)| { + matches!( + maybe_pragma, + "isort" | "type" | "pyright" | "pyrefly" | "pylint" | "flake8" | "ruff" | "ty" + ) + }) } /// Returns the byte offset within `comment` where a trailing pragma comment starts, diff --git a/crates/ruff_server/src/edit/notebook.rs b/crates/ruff_server/src/edit/notebook.rs index 53d012edcf..6901ced8e7 100644 --- a/crates/ruff_server/src/edit/notebook.rs +++ b/crates/ruff_server/src/edit/notebook.rs @@ -99,8 +99,12 @@ impl NotebookDocument { nbformat_minor: 5, }; - ruff_notebook::Notebook::from_raw_notebook(raw_notebook, false) - .unwrap_or_else(|err| panic!("Server notebook document could not be converted to Ruff's notebook document format: {err}")) + ruff_notebook::Notebook::from_raw_notebook(raw_notebook, false).unwrap_or_else(|err| { + panic!( + "Server notebook document could not be converted to Ruff's \ + notebook document format: {err}" + ) + }) } pub(crate) fn update( diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs index a949150275..d811dc7839 100644 --- a/crates/ruff_server/src/server/api.rs +++ b/crates/ruff_server/src/server/api.rs @@ -99,9 +99,7 @@ pub(super) fn request(req: server::Request) -> Task { pub(super) fn notification(notif: server::Notification) -> Task { match LspNotificationMethod::from(notif.method.as_str()) { - notification::DidChange::METHOD => { - sync_notification_task::(notif) - } + notification::DidChange::METHOD => sync_notification_task::(notif), notification::DidChangeConfiguration::METHOD => { sync_notification_task::(notif) } @@ -138,7 +136,8 @@ pub(super) fn notification(notif: server::Notification) -> Task { tracing::error!("Encountered error when routing notification: {err}"); Task::sync(|_session, client| { client.show_error_message( - "Ruff failed to handle a notification from the editor. Check the logs for more details." + "Ruff failed to handle a notification from the editor. \ + Check the logs for more details.", ); }) }) @@ -309,8 +308,11 @@ where anyhow::anyhow!("JSON parsing failure:\n{json_err}") } server::ExtractError::MethodMismatch(_) => { - unreachable!("A method mismatch should not be possible here unless you've used a different handler (`Req`) \ - than the one whose method name was matched against earlier.") + unreachable!( + "A method mismatch should not be possible here \ + unless you've used a different handler (`Req`) than the one \ + whose method name was matched against earlier." + ) } }) .with_failure_code(server::ErrorCode::InternalError) @@ -361,8 +363,11 @@ where anyhow::anyhow!("JSON parsing failure:\n{json_err}") } server::ExtractError::MethodMismatch(_) => { - unreachable!("A method mismatch should not be possible here unless you've used a different handler (`N`) \ - than the one whose method name was matched against earlier.") + unreachable!( + "A method mismatch should not be possible here \ + unless you've used a different handler (`N`) than the one \ + whose method name was matched against earlier." + ) } }) .with_failure_code(server::ErrorCode::InternalError)?, diff --git a/crates/ruff_server/src/server/api/requests/execute_command.rs b/crates/ruff_server/src/server/api/requests/execute_command.rs index 8fcb03e8c6..93c26f361e 100644 --- a/crates/ruff_server/src/server/api/requests/execute_command.rs +++ b/crates/ruff_server/src/server/api/requests/execute_command.rs @@ -67,7 +67,12 @@ impl super::SyncRequestHandler for ExecuteCommand { // check if we can apply a workspace edit if !session.resolved_client_capabilities().apply_edit { - return Err(anyhow::anyhow!("Cannot execute the '{}' command: the client does not support `workspace/applyEdit`", command.label())).with_failure_code(ErrorCode::InternalError); + return Err(anyhow::anyhow!( + "Cannot execute the '{}' command: \ + the client does not support `workspace/applyEdit`", + command.label() + )) + .with_failure_code(ErrorCode::InternalError); } let mut arguments: Vec = params diff --git a/crates/ruff_server/src/session/options.rs b/crates/ruff_server/src/session/options.rs index 30be0ff0d4..b1d2d8f2ed 100644 --- a/crates/ruff_server/src/session/options.rs +++ b/crates/ruff_server/src/session/options.rs @@ -350,8 +350,14 @@ impl AllOptions { Self::from_init_options( serde_json::from_value(options) .map_err(|err| { - tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default client settings..."); - client.show_error_message("Ruff received invalid client settings - falling back to default client settings."); + tracing::error!( + "Failed to deserialize initialization options: {err}. \ + Falling back to default client settings..." + ); + client.show_error_message( + "Ruff received invalid client settings - \ + falling back to default client settings.", + ); }) .unwrap_or_default(), ) diff --git a/crates/ruff_server/src/session/settings.rs b/crates/ruff_server/src/session/settings.rs index 69f7e0b8a0..70151a4114 100644 --- a/crates/ruff_server/src/session/settings.rs +++ b/crates/ruff_server/src/session/settings.rs @@ -41,7 +41,8 @@ impl GlobalClientSettings { Ok(settings) => settings, Err(settings) => { self.client.show_error_message( - "Ruff received invalid settings from the editor. Refer to the logs for more information." + "Ruff received invalid settings from the editor. \ + Refer to the logs for more information.", ); settings } diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 5d3a52fdb3..41b6525731 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -801,7 +801,11 @@ impl LintConfiguration { 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." + "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." ); } options.common.ignore_init_module_imports @@ -1174,11 +1178,15 @@ impl LintConfiguration { [selection] => { let (prefix, code) = selection.prefix_and_code(); return Err(anyhow!( - "Selection of deprecated rule `{prefix}{code}` is not allowed when preview is enabled." + "Selection of deprecated rule `{prefix}{code}` is not allowed when \ + preview is enabled." )); } [..] => { - let mut message = "Selection of deprecated rules is not allowed when preview is enabled. Remove selection of:".to_string(); + let mut message = "\ + Selection of deprecated rules is not allowed \ + when preview is enabled. Remove selection of:" + .to_string(); for selection in deprecated_selectors { let (prefix, code) = selection.prefix_and_code(); message.push_str("\n\t- "); @@ -1725,8 +1733,10 @@ fn warn_about_deprecated_top_level_lint_options( ); warn_user_once_by_message!( - "The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. \ - Please update the following options in {thing_to_update}:\n {options_mapping}", + "The top-level linter settings are deprecated \ + in favour of their counterparts in the `lint` section. \ + Please update the following options in {thing_to_update}:\n \ + {options_mapping}", ); } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 33666e2112..10e96101e5 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -611,7 +611,8 @@ pub(crate) fn validate_required_version(required_version: &RequiredVersion) -> a .expect("RUFF_PKG_VERSION is not a valid PEP 440 version specifier"); if !required_version.contains(&ruff_pkg_version) { return Err(anyhow::anyhow!( - "Required version `{required_version}` does not match the running version `{RUFF_PKG_VERSION}`" + "Required version `{required_version}` does not match the running version \ + `{RUFF_PKG_VERSION}`" )); } Ok(()) @@ -730,9 +731,9 @@ pub struct LintCommonOptions { extend-ignore = ["F841"] "# )] - #[deprecated( - note = "The `extend-ignore` option is now interchangeable with [`ignore`](#lint_ignore). Please update your configuration to use the [`ignore`](#lint_ignore) option instead." - )] + #[deprecated(note = "The `extend-ignore` option is now interchangeable with \ + [`ignore`](#lint_ignore). Please update your configuration to use the \ + [`ignore`](#lint_ignore) option instead.")] pub extend_ignore: Option>, /// A list of rule codes or prefixes to enable, in addition to those @@ -778,9 +779,9 @@ pub struct LintCommonOptions { /// A list of rule codes or prefixes to consider non-auto-fixable, in addition to those /// specified by [`unfixable`](#lint_unfixable). - #[deprecated( - note = "The `extend-unfixable` option is now interchangeable with [`unfixable`](#lint_unfixable). Please update your configuration to use the `unfixable` option instead." - )] + #[deprecated(note = "The `extend-unfixable` option is now interchangeable with \ + [`unfixable`](#lint_unfixable). Please update your configuration to \ + use the `unfixable` option instead.")] pub extend_unfixable: Option>, /// A list of rule codes or prefixes that are unsupported by Ruff, but should be @@ -868,7 +869,10 @@ pub struct LintCommonOptions { )] #[deprecated( since = "0.4.4", - note = "`ignore-init-module-imports` will be removed in a future version because F401 now recommends appropriate fixes for unused imports in `__init__.py` (currently in preview mode). See documentation for more information and please update your configuration." + note = "`ignore-init-module-imports` will be removed in a future version because F401 now \ + recommends appropriate fixes for unused imports in `__init__.py` (currently in \ + preview mode). See documentation for more information and please update your \ + configuration." )] pub ignore_init_module_imports: Option, @@ -1390,7 +1394,8 @@ pub struct Flake8BuiltinsOptions { )] #[deprecated( since = "0.10.0", - note = "`builtins-allowed-modules` has been renamed to `allowed-modules`. Use that instead." + note = "`builtins-allowed-modules` has been renamed to `allowed-modules`. \ + Use that instead." )] pub(crate) builtins_allowed_modules: Option>, @@ -1414,7 +1419,8 @@ pub struct Flake8BuiltinsOptions { )] #[deprecated( since = "0.10.0", - note = "`builtins-strict-checking` has been renamed to `strict-checking`. Use that instead." + note = "`builtins-strict-checking` has been renamed to `strict-checking`. \ + Use that instead." )] pub(crate) builtins_strict_checking: Option, @@ -1786,7 +1792,8 @@ impl Flake8ImportConventionsOptions { let normalized_alias = alias.nfkc().collect::(); if normalized_alias == "__debug__" { anyhow::bail!( - "Invalid alias for module '{module}': alias normalizes to '__debug__', which is not allowed." + "Invalid alias for module '{module}': alias normalizes to '__debug__', \ + which is not allowed." ); } normalized_aliases.insert(module, normalized_alias); @@ -2952,7 +2959,8 @@ impl IsortOptions { let lines_between_types = self.lines_between_types.unwrap_or_default(); if force_sort_within_sections && lines_between_types != 0 { warn_user_once!( - "`lines-between-types` is ignored when `force-sort-within-sections` is set to `true`" + "`lines-between-types` is ignored when `force-sort-within-sections` \ + is set to `true`" ); } @@ -3774,7 +3782,8 @@ pub struct RuffOptions { )] #[deprecated( since = "0.10.0", - note = "The `extend-markup-names` option has been moved to the `flake8-bandit` section of the configuration." + note = "The `extend-markup-names` option has been moved to the `flake8-bandit` section of \ + the configuration." )] extend_markup_names: Option>, @@ -3810,7 +3819,8 @@ pub struct RuffOptions { )] #[deprecated( since = "0.10.0", - note = "The `allowed-markup-names` option has been moved to the `flake8-bandit` section of the configuration." + note = "The `allowed-markup-names` option has been moved to the `flake8-bandit` section \ + of the configuration." )] allowed_markup_calls: Option>, /// Whether to require `__init__.py` files to contain no code at all, including imports and diff --git a/crates/ty/src/args.rs b/crates/ty/src/args.rs index 33a2453e8c..83ba54d843 100644 --- a/crates/ty/src/args.rs +++ b/crates/ty/src/args.rs @@ -370,7 +370,11 @@ impl clap::Args for RulesArg { clap::Arg::new("error") .long("error") .action(ArgAction::Append) - .help("Treat the given rule as having severity 'error'. Can be specified multiple times. Use 'all' to apply to all rules.") + .help( + "Treat the given rule as having severity 'error'. \ + Can be specified multiple times. \ + Use 'all' to apply to all rules.", + ) .value_name("RULE") .help_heading(HELP_HEADING), ) @@ -378,7 +382,11 @@ impl clap::Args for RulesArg { clap::Arg::new("warn") .long("warn") .action(ArgAction::Append) - .help("Treat the given rule as having severity 'warn'. Can be specified multiple times. Use 'all' to apply to all rules.") + .help( + "Treat the given rule as having severity 'warn'. \ + Can be specified multiple times. \ + Use 'all' to apply to all rules.", + ) .value_name("RULE") .help_heading(HELP_HEADING), ) @@ -386,7 +394,11 @@ impl clap::Args for RulesArg { clap::Arg::new("ignore") .long("ignore") .action(ArgAction::Append) - .help("Disables the rule. Can be specified multiple times. Use 'all' to apply to all rules.") + .help( + "Disables the rule. \ + Can be specified multiple times. \ + Use 'all' to apply to all rules.", + ) .value_name("RULE") .help_heading(HELP_HEADING), ) diff --git a/crates/ty/src/lib.rs b/crates/ty/src/lib.rs index fd8fe97867..e2d6627609 100644 --- a/crates/ty/src/lib.rs +++ b/crates/ty/src/lib.rs @@ -104,13 +104,13 @@ fn run_check(args: CheckCommand) -> anyhow::Result { // The base path to which all CLI arguments are relative to. let cwd = { let cwd = std::env::current_dir().context("Failed to get the current working directory")?; - SystemPathBuf::from_path_buf(cwd) - .map_err(|path| { - anyhow!( - "The current working directory `{}` contains non-Unicode characters. ty only supports Unicode paths.", - path.display() - ) - })? + SystemPathBuf::from_path_buf(cwd).map_err(|path| { + anyhow!( + "The current working directory `{}` contains non-Unicode characters. \ + ty only supports Unicode paths.", + path.display() + ) + })? }; let project_path = args @@ -226,7 +226,8 @@ fn run_check(args: CheckCommand) -> anyhow::Result { Some("json") => writeln!(stdout, "{}", db.salsa_memory_dump().to_json())?, Some(other) => { tracing::warn!( - "Unknown value for `TY_MEMORY_REPORT`: `{other}`. Valid values are `short`, `full`, and `json`." + "Unknown value for `TY_MEMORY_REPORT`: `{other}`. \ + Valid values are `short`, `full`, and `json`." ); } None => {} @@ -400,7 +401,8 @@ impl MainLoop { } => { if check_revision != revision { tracing::debug!( - "Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}" + "Discarding check result for outdated revision: \ + current: {revision}, result revision: {check_revision}" ); continue; } @@ -480,7 +482,9 @@ impl MainLoop { if exit_status.is_internal_error() { tracing::warn!( - "A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details." + "A fatal error occurred while checking some files. \ + Not all project files were analyzed. \ + See the diagnostics list above for details." ); } @@ -561,7 +565,8 @@ impl MainLoop { let total = fixed + diagnostics_count; writeln!( self.printer.stream_for_failure_summary(), - "Found {total} diagnostic{} ({fixed} fixed, {diagnostics_count} remaining).", + "Found {total} diagnostic{} \ + ({fixed} fixed, {diagnostics_count} remaining).", if total == 1 { "" } else { "s" } )?; } else { diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 37e61a3586..7ca1ef529e 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -1823,10 +1823,9 @@ impl Relevance { } else { Sort::Even }, + // We only up-rank top-level modules. + // Doing this for sub-modules generates too much noise. is_module: if c.kind == Some(CompletionKind::Module) - // We only up-rank top-level modules. - // Doing this for sub-modules generates too - // much noise. && !c .qualified .as_ref() diff --git a/crates/ty_ide/src/docstring/document/google.rs b/crates/ty_ide/src/docstring/document/google.rs index 23709a73b9..85e1700d30 100644 --- a/crates/ty_ide/src/docstring/document/google.rs +++ b/crates/ty_ide/src/docstring/document/google.rs @@ -686,12 +686,18 @@ impl<'a> ItemLine<'a> { item_indent: TextSize, ) -> bool { // More deeply indented lines are unambiguously part of the current item. - line_indent > item_indent - // Although the style guide suggests indenting continuation lines, - // aligned parameter prose is common in practice. - || (line_indent == item_indent && section_kind.is_parameter_section()) - // Aligned URLs and paths are continuations despite resembling item headers. - || (line_indent == item_indent && self.is_item_like_continuation) + if line_indent > item_indent { + return true; + } + + // Although the style guide suggests indenting continuation lines, + // aligned parameter prose is common in practice. + if line_indent == item_indent && section_kind.is_parameter_section() { + return true; + } + + // Aligned URLs and paths are continuations despite resembling item headers. + line_indent == item_indent && self.is_item_like_continuation } fn classify( diff --git a/crates/ty_ide/src/folding_range.rs b/crates/ty_ide/src/folding_range.rs index 6507f3c19a..afcc3ea6c8 100644 --- a/crates/ty_ide/src/folding_range.rs +++ b/crates/ty_ide/src/folding_range.rs @@ -700,11 +700,10 @@ impl<'a> SourceOrderVisitor<'a> for FoldingRangeVisitor<'a> { AnyNodeRef::ExprList(_) | AnyNodeRef::ExprListComp(_) | AnyNodeRef::TypeParams(_) => { self.add_delimited_expression_range(node.range(), BRACKETS); } - AnyNodeRef::ExprTuple(tuple) - // Only fold parenthesized tuples. - if tuple.parenthesized => { - self.add_delimited_expression_range(node.range(), PARENTHESES); - } + // Only fold parenthesized tuples. + AnyNodeRef::ExprTuple(tuple) if tuple.parenthesized => { + self.add_delimited_expression_range(node.range(), PARENTHESES); + } AnyNodeRef::ExprDict(_) | AnyNodeRef::ExprSet(_) | AnyNodeRef::ExprSetComp(_) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 8a69580832..aea0e73361 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -622,19 +622,23 @@ fn type_hint_is_excessive_for_expr(expr: &Expr) -> bool { Expr::Tuple(expr_tuple) => expr_tuple.elts.iter().all(type_hint_is_excessive_for_expr), // Various Literal[...] types which are always excessive to hint - | Expr::BytesLiteral(_) + Expr::BytesLiteral(_) | Expr::NumberLiteral(_) | Expr::BooleanLiteral(_) - | Expr::StringLiteral(_) + | Expr::StringLiteral(_) => true, // `None` isn't terribly verbose, but still redundant - | Expr::NoneLiteral(_) + Expr::NoneLiteral(_) => true, // This one expands to `str` which isn't verbose but is redundant - | Expr::FString(_) + Expr::FString(_) => true, // This one expands to `Template` which isn't verbose but is redundant - | Expr::TString(_)=> true, + Expr::TString(_) => true, // You too `+1 and `-1`, get back here - Expr::UnaryOp(ExprUnaryOp { op: UnaryOp::UAdd | UnaryOp::USub, operand, .. }) => matches!(**operand, Expr::NumberLiteral(_)), + Expr::UnaryOp(ExprUnaryOp { + op: UnaryOp::UAdd | UnaryOp::USub, + operand, + .. + }) => matches!(**operand, Expr::NumberLiteral(_)), // Everything else is reasonable _ => false, diff --git a/crates/ty_module_resolver/src/typeshed.rs b/crates/ty_module_resolver/src/typeshed.rs index 94421882de..b7897cc2e7 100644 --- a/crates/ty_module_resolver/src/typeshed.rs +++ b/crates/ty_module_resolver/src/typeshed.rs @@ -370,7 +370,12 @@ mod tests { let relative_path = absolute_path .strip_prefix(&stdlib_stubs_path) - .unwrap_or_else(|_| panic!("Expected path to be a child of {stdlib_stubs_path:?} but found {absolute_path:?}")); + .unwrap_or_else(|_| { + panic!( + "Expected path to be a child of {stdlib_stubs_path:?} \ + but found {absolute_path:?}" + ) + }); let relative_path_str = relative_path.as_os_str().to_str().unwrap_or_else(|| { panic!("Expected all typeshed paths to be valid UTF-8; got {relative_path:?}") @@ -381,15 +386,22 @@ mod tests { let top_level_module = if let Some(extension) = relative_path.extension() { // It was a file; strip off the file extension to get the module name: - let extension = extension - .to_str() - .unwrap_or_else(||panic!("Expected all file extensions to be UTF-8; was not true for {relative_path:?}")); + let extension = extension.to_str().unwrap_or_else(|| { + panic!( + "Expected all file extensions to be UTF-8; \ + was not true for {relative_path:?}" + ) + }); relative_path_str .strip_suffix(extension) - .and_then(|string| string.strip_suffix('.')).unwrap_or_else(|| { - panic!("Expected path {relative_path_str:?} to end with computed extension {extension:?}") - }) + .and_then(|string| string.strip_suffix('.')) + .unwrap_or_else(|| { + panic!( + "Expected path {relative_path_str:?} to end \ + with computed extension {extension:?}" + ) + }) } else { // It was a directory; no need to do anything to get the module name relative_path_str diff --git a/crates/ty_project/src/db/changes.rs b/crates/ty_project/src/db/changes.rs index 4530a9d189..85792dcffc 100644 --- a/crates/ty_project/src/db/changes.rs +++ b/crates/ty_project/src/db/changes.rs @@ -203,7 +203,8 @@ impl ProjectDatabase { if configuration_paths.may_contain_configuration(path, &project_root) { tracing::debug!( - "Reload project because a configuration file may have been deleted." + "Reload project because a configuration file \ + may have been deleted." ); reload_project = true; } @@ -240,7 +241,8 @@ impl ProjectDatabase { if let Err(error) = metadata.apply_configuration_files(self.system()) { let error = anyhow::Error::new(error); tracing::error!( - "Failed to apply configuration files, continuing without applying them: {error:#}" + "Failed to apply configuration files, \ + continuing without applying them: {error:#}" ); } @@ -258,23 +260,24 @@ impl ProjectDatabase { } Err(error) => { tracing::error!( - "Failed to convert metadata to program settings, continuing without applying them: {error}" + "Failed to convert metadata to program settings, \ + continuing without applying them: {error}" ); Vec::new() } }; - let (settings, mut settings_diagnostics) = match merged_options - .to_settings(self, &FallibleStrategy) - { - Ok((settings, diagnostics)) => (Some(settings), diagnostics), - Err(error) => { - tracing::warn!( - "Keeping old project configuration because loading the new settings failed with: {error}" - ); - (None, vec![error.into_diagnostic()]) - } - }; + let (settings, mut settings_diagnostics) = + match merged_options.to_settings(self, &FallibleStrategy) { + Ok((settings, diagnostics)) => (Some(settings), diagnostics), + Err(error) => { + tracing::warn!( + "Keeping old project configuration because loading the new \ + settings failed with: {error}" + ); + (None, vec![error.into_diagnostic()]) + } + }; settings_diagnostics.extend( program_settings_diagnostics .into_iter() diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index a93d0ea234..a004813337 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -248,9 +248,15 @@ impl Options { let real_stdlib_path = python_environment.as_ref().and_then(|python_environment| { // For now this is considered non-fatal, we don't Need this for anything. - python_environment.real_stdlib_path(system).map_err(|err| { - tracing::info!("No real stdlib found, stdlib goto-definition may have degraded quality: {err}"); - }).ok() + python_environment + .real_stdlib_path(system) + .map_err(|err| { + tracing::info!( + "No real stdlib found, stdlib goto-definition \ + may have degraded quality: {err}" + ); + }) + .ok() }); let python_version = configured_python_version @@ -324,7 +330,8 @@ impl Options { let src = project_root.join("src"); if system.is_directory(&src) && !is_package(&src) { tracing::debug!( - "Including `./src` in `environment.root` because a `./src` directory exists and is not a package" + "Including `./src` in `environment.root` \ + because a `./src` directory exists and is not a package" ); roots.push(src); } @@ -337,7 +344,9 @@ impl Options { && !roots.contains(&project_name_dir) { tracing::debug!( - "Including `./{project_name}` in `environment.root` because a `./{project_name}/{project_name}` directory exists and `./{project_name}` is not a package" + "Including `./{project_name}` in `environment.root` because a \ + `./{project_name}/{project_name}` directory exists \ + and `./{project_name}` is not a package" ); roots.push(project_name_dir); } @@ -347,7 +356,8 @@ impl Options { let python = project_root.join("python"); if system.is_directory(&python) && !is_package(&python) && !roots.contains(&python) { tracing::debug!( - "Including `./python` in `environment.root` because a `./python` directory exists and is not a package" + "Including `./python` in `environment.root` \ + because a `./python` directory exists and is not a package" ); roots.push(python); } @@ -378,7 +388,8 @@ impl Options { Ok(path) => path, Err(path) => { tracing::debug!( - "Skipping `{path}` listed in `PYTHONPATH` because the path is not valid UTF-8", + "Skipping `{path}` listed in `PYTHONPATH` \ + because the path is not valid UTF-8", path = path.display() ); continue; @@ -389,13 +400,15 @@ impl Options { if !system.is_directory(&abspath) { tracing::debug!( - "Skipping `{abspath}` listed in `PYTHONPATH` because the path doesn't exist or isn't a directory" + "Skipping `{abspath}` listed in `PYTHONPATH` \ + because the path doesn't exist or isn't a directory" ); continue; } tracing::debug!( - "Adding `{abspath}` from the `PYTHONPATH` environment variable to `extra_paths`" + "Adding `{abspath}` from the `PYTHONPATH` environment variable \ + to `extra_paths`" ); extra_paths.push(abspath); @@ -602,7 +615,8 @@ fn unsupported_inferred_python_version_diagnostic( let mut diagnostic = OptionDiagnostic::new( DiagnosticId::UnsupportedPythonVersion, format!( - "Ignoring unsupported inferred Python version `{}`; ty will use Python {fallback} instead.", + "Ignoring unsupported inferred Python version `{}`; \ + ty will use Python {fallback} instead.", python_version.version ), Severity::Warning, @@ -643,7 +657,8 @@ fn unsupported_inferred_python_version_diagnostic( .sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, format!( - "The version was inferred from the `lib/{site_packages_parent_dir}/site-packages` directory layout.", + "The version was inferred from the \ + `lib/{site_packages_parent_dir}/site-packages` directory layout.", ), )), PythonVersionSource::Cli => diagnostic.sub(SubDiagnostic::new( @@ -1167,7 +1182,8 @@ fn build_include_filter( ) .sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, - "Remove the `include` option to match all files or add a pattern to match specific files", + "Remove the `include` option to match all files \ + or add a pattern to match specific files", )); // Add source annotation if we have source information @@ -1217,12 +1233,16 @@ fn build_include_filter( includes.build().map_err(|_| { let diagnostic = OptionDiagnostic::new( DiagnosticId::InvalidGlob, - format!("The `{}` patterns resulted in a regex that is too large", context.include_name()), + format!( + "The `{}` patterns resulted in a regex that is too large", + context.include_name() + ), Severity::Error, ); Box::new(diagnostic.sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, - "Please open an issue on the ty repository and share the patterns that caused the error.", + "Please open an issue on the ty repository \ + and share the patterns that caused the error.", ))) }) } @@ -1275,12 +1295,16 @@ fn build_exclude_filter( excludes.build().map_err(|_| { let diagnostic = OptionDiagnostic::new( DiagnosticId::InvalidGlob, - format!("The `{}` patterns resulted in a regex that is too large", context.exclude_name()), + format!( + "The `{}` patterns resulted in a regex that is too large", + context.exclude_name() + ), Severity::Error, ); Box::new(diagnostic.sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, - "Please open an issue on the ty repository and share the patterns that caused the error.", + "Please open an issue on the ty repository \ + and share the patterns that caused the error.", ))) }) } @@ -1710,7 +1734,8 @@ fn build_module_glob_set( Box::new(diagnostic.sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, - "Please open an issue on the ty repository and share the patterns that caused the error.", + "Please open an issue on the ty repository \ + and share the patterns that caused the error.", ))) }) } @@ -1973,7 +1998,9 @@ impl ToOverride for RangedValue { diagnostic = diagnostic.sub(SubDiagnostic::new( SubDiagnosticSeverity::Info, - "or remove the `[[overrides]]` section and merge the configuration into the root `[rules]` table if the configuration should apply to all files", + "or remove the `[[overrides]]` section \ + and merge the configuration into the root `[rules]` table \ + if the configuration should apply to all files", )); // Add source annotation if we have source information @@ -2154,7 +2181,8 @@ mod schema { all.insert( "description".to_string(), Value::String( - "Configure a default severity level for all rules. Individual rule settings override this default." + "Configure a default severity level for all rules. \ + Individual rule settings override this default." .to_string(), ), ); diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index 8602cd638d..06c4e7dca9 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -1195,14 +1195,21 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { self.narrowing_aliases.retain(|name, alias| { // Drop aliases that narrow the reassigned place or any of its members. // e.g. `is_none = x is None and ...; x = 1` - !alias.narrowed_places.contains(&place) - // e.g. `is_none = a.x is None; a = A()` - && !associated_members - .iter() - .any(|m| alias.narrowed_places.contains(&(*m).into())) - // Drop the alias whose own variable is the reassigned place. - // e.g. `is_none = x is None; is_none = False` - && reassigned_alias_name != Some(name) + if alias.narrowed_places.contains(&place) { + return false; + } + + // e.g. `is_none = a.x is None; a = A()` + if associated_members + .iter() + .any(|m| alias.narrowed_places.contains(&(*m).into())) + { + return false; + } + + // Drop the alias whose own variable is the reassigned place. + // e.g. `is_none = x is None; is_none = False` + reassigned_alias_name != Some(name) }); } @@ -4986,8 +4993,10 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> { fn visit_pattern(&mut self, pattern: &'ast ast::Pattern) { if let ast::Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) = pattern && let Some((last, alternatives)) = patterns.split_last() - // Capture-free alternatives do not affect bindings and need no flow merge. - && patterns.iter().any(Self::pattern_has_bindings) + && ( + // Capture-free alternatives do not affect bindings and need no flow merge. + patterns.iter().any(Self::pattern_has_bindings) + ) { // Start each alternative without earlier captures so repeated names do not shadow one // another. Complementary predicates preserve possible missing captures while all diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cc3168c3d4..3eaaf7d4cd 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2410,20 +2410,17 @@ impl<'db> Type<'db> { Type::LiteralValue(_) | Type::Never | Type::NewTypeInstance(_) - | Type::NominalInstance(_) + | Type::NominalInstance(_) => true, // `TypedDict` and `Protocol` can be synthesized, // but it's always possible to create an equivalent type using a class definition. - | Type::TypedDict(_) - | Type::ProtocolInstance(_) + Type::TypedDict(_) | Type::ProtocolInstance(_) => true, // Not all `Callable` types are spellable using the `Callable` type form, // but they are all spellable using callback protocols. - | Type::Callable(_) + Type::Callable(_) => true, // `Unknown` and `@Todo` are nonstandard extensions, // but they are both exactly equivalent to `Any` - | Type::Dynamic(_) - | Type::TypeVar(_) - | Type::TypeAlias(_) - | Type::SubclassOf(_) => true, + Type::Dynamic(_) => true, + Type::TypeVar(_) | Type::TypeAlias(_) | Type::SubclassOf(_) => true, Type::TypeForm(typeform) => typeform.type_argument(db).is_spellable(db), Type::Intersection(_) => false, Type::EnumComplement(complement) => complement.is_spellable(db), @@ -3266,7 +3263,8 @@ impl<'db> Type<'db> { ty.to_meta_type(db, env) .find_name_in_mro_with_policy(db, env, name, policy) .expect( - "`Type::find_name_in_mro()` should return `Some()` when called on a meta-type", + "`Type::find_name_in_mro()` should return `Some()` \ + when called on a meta-type", ) } } @@ -3287,7 +3285,8 @@ impl<'db> Type<'db> { .to_meta_type(db, env) .find_name_in_mro_with_policy(db, env, name, policy) .expect( - "`Type::find_name_in_mro()` should return `Some()` when called on a meta-type", + "`Type::find_name_in_mro()` should return `Some()` \ + when called on a meta-type", ), } } @@ -3333,8 +3332,9 @@ impl<'db> Type<'db> { let class_attr = self .find_name_in_mro_with_policy(db, env, name, policy) .expect( - "Calling `class_object_member` on class literals and subclass-of types should always find an MRO", - ); + "Calling `class_object_member` on class literals and subclass-of types \ + should always find an MRO", + ); let own_class = match self { Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { @@ -7214,7 +7214,8 @@ impl<'db> Type<'db> { )), Some(KnownClass::TypeVarTuple | KnownClass::ExtensionsTypeVarTuple) => { Ok(todo_type!( - "unrecognized `typing.TypeVarTuple` instances should be invalid type expressions" + "unrecognized `typing.TypeVarTuple` instances \ + should be invalid type expressions" )) } _ => Err(InvalidTypeExpressionError { @@ -7513,22 +7514,28 @@ impl<'db> Type<'db> { } match self { - Type::TypeVar(bound_typevar) => bound_typevar.apply_type_mapping_impl(db, type_mapping, visitor), - Type::KnownInstance(known_instance) => known_instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + Type::TypeVar(bound_typevar) => { + bound_typevar.apply_type_mapping_impl(db, type_mapping, visitor) + } + Type::KnownInstance(known_instance) => { + known_instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + } Type::FunctionLiteral(function) => visitor.visit(db, self, type_mapping, || { match type_mapping { // Promote the types within the signature before promoting the signature to its // callable form. TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular) => { - Type::FunctionLiteral(function.apply_type_mapping_impl(db, + Type::FunctionLiteral(function.apply_type_mapping_impl( + db, type_mapping, tcx, visitor, )) .promote_impl(db, visitor.env) } - _ => Type::FunctionLiteral(function.apply_type_mapping_impl(db, + _ => Type::FunctionLiteral(function.apply_type_mapping_impl( + db, type_mapping, tcx, visitor, @@ -7538,21 +7545,33 @@ impl<'db> Type<'db> { Type::BoundMethod(method) => Type::BoundMethod(BoundMethodType::new( db, - method.function(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor), - method.self_instance(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor), + method + .function(db) + .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + method + .self_instance(db) + .apply_type_mapping_impl(db, type_mapping, tcx, visitor), )), - Type::NominalInstance(instance) if matches!(type_mapping, TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular)) => { + Type::NominalInstance(instance) + if matches!( + type_mapping, + TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular) + ) => + { match instance.known_class(db) { - Some(KnownClass::Complex) => { - KnownUnion::Complex.to_type(db, visitor.env) - } + Some(KnownClass::Complex) => KnownUnion::Complex.to_type(db, visitor.env), Some(KnownClass::Float) => KnownUnion::Float.to_type(db, visitor.env), _ => instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor), } } - Type::NominalInstance(instance) if matches!(type_mapping, TypeMapping::Promote(PromotionMode::On, PromotionKind::SingletonsOnly)) => { + Type::NominalInstance(instance) + if matches!( + type_mapping, + TypeMapping::Promote(PromotionMode::On, PromotionKind::SingletonsOnly) + ) => + { if instance.is_singleton(db) { self.promote_singletons_impl(db, visitor.env) } else { @@ -7562,7 +7581,7 @@ impl<'db> Type<'db> { Type::NominalInstance(instance) => { instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor) - }, + } Type::NewTypeInstance(newtype) => visitor.visit(db, self, type_mapping, || { Type::NewTypeInstance(newtype.map_base_class_type(db, |class_type| { @@ -7615,12 +7634,14 @@ impl<'db> Type<'db> { Type::TypedDict(typed_dict.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) } - Type::SubclassOf(subclass_of) => subclass_of.apply_type_mapping_impl(db, type_mapping, tcx, visitor), - - Type::PropertyInstance(property) => { - Type::PropertyInstance(property.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + Type::SubclassOf(subclass_of) => { + subclass_of.apply_type_mapping_impl(db, type_mapping, tcx, visitor) } + Type::PropertyInstance(property) => Type::PropertyInstance( + property.apply_type_mapping_impl(db, type_mapping, tcx, visitor), + ), + Type::Union(union) => union.map_leave_aliases(db, visitor.env, |element| { element.apply_type_mapping_impl(db, type_mapping, tcx, visitor) }), @@ -7641,9 +7662,12 @@ impl<'db> Type<'db> { TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular) ) { for negative in intersection.negative(db) { - builder.add_negative_in_place( - negative.apply_type_mapping_impl(db, &type_mapping.flip(), tcx, visitor), - ); + builder.add_negative_in_place(negative.apply_type_mapping_impl( + db, + &type_mapping.flip(), + tcx, + visitor, + )); } } builder.build() @@ -7656,36 +7680,42 @@ impl<'db> Type<'db> { Type::TypeIs(type_is) => visitor.visit(db, self, type_mapping, || { type_is.with_type( db, - type_is - .type_argument(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + type_is.type_argument(db).apply_type_mapping_impl( + db, + type_mapping, + tcx, + visitor, + ), ) }), Type::TypeGuard(type_guard) => visitor.visit(db, self, type_mapping, || { type_guard.with_type( db, - type_guard - .return_type(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + type_guard.return_type(db).apply_type_mapping_impl( + db, + type_mapping, + tcx, + visitor, + ), ) }), Type::TypeForm(typeform) => visitor.visit(db, self, type_mapping, || { TypeFormType::from_type_expression( db, - typeform - .type_argument(db) - .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + typeform.type_argument(db).apply_type_mapping_impl( + db, + type_mapping, + tcx, + visitor, + ), ) }), Type::TypeAlias(alias) => { match type_mapping { - TypeMapping::Materialize(_) if alias.materialization_kind(db).is_some() => - { - self - } + TypeMapping::Materialize(_) if alias.materialization_kind(db).is_some() => self, TypeMapping::EagerExpansion if alias.materialization_kind(db).is_some() => { alias.value_type(db).expand_eagerly(db, visitor.env) } @@ -7694,19 +7724,21 @@ impl<'db> Type<'db> { // Type values and `RecursiveList` is different from `RecursiveList[T]`. TypeMapping::EagerExpansion => { alias.raw_value_type(db).expand_eagerly(db, visitor.env) - }, + } // When specializing a generic type alias, instead of specializing the expanded type, the type alias itself is specialized. // Without this special handling, recursive type aliases would result in cycles, returning an unspecialized fallback type. TypeMapping::ApplySpecialization(specialization) - | TypeMapping::ApplySpecializationWithMaterialization { specialization, .. } - if matches!( + | TypeMapping::ApplySpecializationWithMaterialization { + specialization, .. + } if matches!( specialization, ApplySpecialization::Specialization(_) | ApplySpecialization::TypeAlias(_) | ApplySpecialization::Partial { .. } ) => { - let mut current_specialization = specialization.as_specialization(db).unwrap(); + let mut current_specialization = + specialization.as_specialization(db).unwrap(); if let TypeMapping::ApplySpecializationWithMaterialization { materialization_kind, .. @@ -7715,21 +7747,24 @@ impl<'db> Type<'db> { current_specialization = current_specialization .with_materialization_kind(db, Some(*materialization_kind)); } - Type::TypeAlias(alias.apply_specialization(db, - |generic_context| { - alias - .specialization(db) - .unwrap_or_else(|| generic_context.default_specialization(db, None)) - .apply_specialization(db, current_specialization) - }, - )) + Type::TypeAlias(alias.apply_specialization(db, |generic_context| { + alias + .specialization(db) + .unwrap_or_else(|| generic_context.default_specialization(db, None)) + .apply_specialization(db, current_specialization) + })) } _ => { // IMPORTANT: All processing must happen inside a single visitor.visit() call so that if we encounter // this same TypeAlias again (e.g., in `type RecursiveT = int | tuple[RecursiveT, ...]`), the visitor // will detect the cycle and return the fallback value. let mapped = visitor.visit(db, self, type_mapping, || { - alias.value_type(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor) + alias.value_type(db).apply_type_mapping_impl( + db, + type_mapping, + tcx, + visitor, + ) }); // If the type mapping does not result in any change to this type alias, keep the @@ -7743,10 +7778,9 @@ impl<'db> Type<'db> { cyclic::TypeIdentity::RecursiveTypeAlias(_) ) { - Type::TypeAlias(alias.with_materialization_kind( - db, - Some(*materialization_kind), - )) + Type::TypeAlias( + alias.with_materialization_kind(db, Some(*materialization_kind)), + ) } else { mapped } @@ -7755,40 +7789,42 @@ impl<'db> Type<'db> { } Type::LiteralValue(_) => match type_mapping { - TypeMapping::ApplySpecialization(_) | - TypeMapping::ApplySpecializationWithMaterialization { .. } | - TypeMapping::BindLegacyTypevars(_) | - TypeMapping::FreshenBoundTypeVars { .. } | - TypeMapping::BindSelf { .. } | - TypeMapping::ReplaceSelf { .. } | - TypeMapping::Materialize(_) | - TypeMapping::ReplaceParameterDefaults | - TypeMapping::EagerExpansion | - TypeMapping::RescopeReturnCallables(_) | - TypeMapping::Promote(PromotionMode::Off, _) | - TypeMapping::Promote( + TypeMapping::ApplySpecialization(_) + | TypeMapping::ApplySpecializationWithMaterialization { .. } + | TypeMapping::BindLegacyTypevars(_) + | TypeMapping::FreshenBoundTypeVars { .. } + | TypeMapping::BindSelf { .. } + | TypeMapping::ReplaceSelf { .. } + | TypeMapping::Materialize(_) + | TypeMapping::ReplaceParameterDefaults + | TypeMapping::EagerExpansion + | TypeMapping::RescopeReturnCallables(_) + | TypeMapping::Promote(PromotionMode::Off, _) + | TypeMapping::Promote( PromotionMode::On, PromotionKind::ClassLiteralsOnly | PromotionKind::SingletonsOnly, ) => self, - TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular) => self.promote_impl(db, visitor.env), - } + TypeMapping::Promote(PromotionMode::On, PromotionKind::Regular) => { + self.promote_impl(db, visitor.env) + } + }, Type::Dynamic(_) => match type_mapping { - TypeMapping::ApplySpecialization(_) | - TypeMapping::ApplySpecializationWithMaterialization { .. } | - TypeMapping::BindLegacyTypevars(_) | - TypeMapping::FreshenBoundTypeVars { .. } | - TypeMapping::BindSelf(..) | - TypeMapping::ReplaceSelf { .. } | - TypeMapping::Promote(..) | - TypeMapping::ReplaceParameterDefaults | - TypeMapping::EagerExpansion | - TypeMapping::RescopeReturnCallables(_) => self, + TypeMapping::ApplySpecialization(_) + | TypeMapping::ApplySpecializationWithMaterialization { .. } + | TypeMapping::BindLegacyTypevars(_) + | TypeMapping::FreshenBoundTypeVars { .. } + | TypeMapping::BindSelf(..) + | TypeMapping::ReplaceSelf { .. } + | TypeMapping::Promote(..) + | TypeMapping::ReplaceParameterDefaults + | TypeMapping::EagerExpansion + | TypeMapping::RescopeReturnCallables(_) => self, TypeMapping::Materialize(materialization_kind) => match materialization_kind { MaterializationKind::Top => Type::object(), MaterializationKind::Bottom => Type::Never, - } - } + }, + }, // `Divergent` is an internal cycle marker rather than a gradual type like `Any` or // `Unknown`. Preserve the marker across materialization, while recording whether this // occurrence should behave like the top (`object`) or bottom (`Never`) bound. @@ -7819,16 +7855,17 @@ impl<'db> Type<'db> { | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) | KnownBoundMethodType::ConstraintSetSolutionsFor(_) | KnownBoundMethodType::ConstraintSetSolutions(_) - | KnownBoundMethodType::ConstraintSetWithDetailedDisplay(_) + | KnownBoundMethodType::ConstraintSetWithDetailedDisplay(_), ) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) + | Type::BoundSuper(_) + | Type::SpecialForm(_) => self, + // 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 // some other generic context's specialization is applied to it. - | Type::ClassLiteral(_) - | Type::BoundSuper(_) - | Type::SpecialForm(_) => self, + Type::ClassLiteral(_) => self, } } @@ -9215,7 +9252,8 @@ impl TypeQualifiers { Self::READ_ONLY => "ReadOnly", _ => { unreachable!( - "Only a single bit should be set when calling `TypeQualifiers::name` (got {self:?})" + "Only a single bit should be set \ + when calling `TypeQualifiers::name` (got {self:?})" ) } } @@ -9411,15 +9449,18 @@ impl<'db> InvalidTypeExpression<'db> { match self.error { InvalidTypeExpression::RequiresOneArgument(special_form) => write!( f, - "`{special_form}` requires exactly one argument when used in a {location}", + "`{special_form}` requires exactly one argument \ + when used in a {location}", ), InvalidTypeExpression::RequiresArguments(special_form) => write!( f, - "`{special_form}` requires at least one argument when used in a {location}", + "`{special_form}` requires at least one argument \ + when used in a {location}", ), InvalidTypeExpression::RequiresTwoArguments(special_form) => write!( f, - "`{special_form}` requires at least two arguments when used in a {location}", + "`{special_form}` requires at least two arguments \ + when used in a {location}", ), InvalidTypeExpression::Protocol => { write!(f, "`typing.Protocol` is not allowed in {location}s") @@ -9435,21 +9476,25 @@ impl<'db> InvalidTypeExpression<'db> { } InvalidTypeExpression::ConstraintSet => write!( f, - "`ty_extensions._internal.ConstraintSet` is not allowed in {location}s", + "`ty_extensions._internal.ConstraintSet` \ + is not allowed in {location}s", ), InvalidTypeExpression::ConstraintSetSolution => write!( f, - "`ty_extensions._internal.ConstraintSetSolution` is not allowed in {location}s", + "`ty_extensions._internal.ConstraintSetSolution` is not allowed \ + in {location}s", ), InvalidTypeExpression::GenericContext => { write!( f, - "`ty_extensions._internal.GenericContext` is not allowed in {location}s" + "`ty_extensions._internal.GenericContext` is not allowed \ + in {location}s" ) } InvalidTypeExpression::Specialization => write!( f, - "`ty_extensions._internal.Specialization` is not allowed in {location}s", + "`ty_extensions._internal.Specialization` \ + is not allowed in {location}s", ), InvalidTypeExpression::NamedTupleSpec => { write!(f, "`NamedTupleSpec` is not allowed in {location}s") @@ -9476,9 +9521,9 @@ impl<'db> InvalidTypeExpression<'db> { } else if qualifier.requires_one_argument() { write!( f, - "Type qualifier `{qualifier}` is not allowed in type expressions \ - (only in annotation expressions, and only with \ - exactly one argument)", + "Type qualifier `{qualifier}` is not allowed \ + in type expressions (only in annotation expressions, \ + and only with exactly one argument)", ) } else { write!( @@ -9498,7 +9543,8 @@ impl<'db> InvalidTypeExpression<'db> { f.write_str("`Self` cannot be used in a metaclass") } InvalidTypeExpression::TypingSelfWithIncompatibleReceiver(_) => f.write_str( - "`Self` requires `self: Self` or `cls: type[Self]` for annotated receivers", + "`Self` requires `self: Self` \ + or `cls: type[Self]` for annotated receivers", ), InvalidTypeExpression::InvalidType(Type::FunctionLiteral(function), _) => { write!( @@ -9519,17 +9565,20 @@ impl<'db> InvalidTypeExpression<'db> { ), InvalidTypeExpression::InvalidBareParamSpec(paramspec) => write!( f, - "Bare ParamSpec `{}` is not valid in this context in a {location}", + "Bare ParamSpec `{}` is not valid \ + in this context in a {location}", paramspec.name(db) ), InvalidTypeExpression::InvalidBareTypeVarTuple(typevartuple) => write!( f, - "Bare TypeVarTuple `{}` is not valid in this context in a {location}", + "Bare TypeVarTuple `{}` is not valid \ + in this context in a {location}", typevartuple.name(db) ), InvalidTypeExpression::Concatenate => write!( f, - "`typing.Concatenate` is not allowed in this context in a {location}", + "`typing.Concatenate` is not allowed \ + in this context in a {location}", ), } } diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 0d442d16db..3ad6a88c69 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -110,8 +110,9 @@ impl<'db> BoundSuperError<'db> { let env = context.program_environment(); if let Some(typevar_context) = typevar_context { let mut diagnostic = builder.into_diagnostic(format_args!( - "`{owner}` is a type variable with an abstract/structural type as \ - its bounds or constraints, in `super({pivot_class}, {owner})` call", + "`{owner}` is a type variable \ + with an abstract/structural type as its bounds or constraints, \ + in `super({pivot_class}, {owner})` call", pivot_class = pivot_class.display(db, env), owner = owner_type.display(db, env), )); @@ -167,9 +168,11 @@ impl<'db> BoundSuperError<'db> { if let Some(typevar_context) = typevar_context { Self::describe_typevar(db, env, &mut diagnostic, *typevar_context); diagnostic.info(format_args!( - "`{bounds_or_constraints}` is not an instance or subclass of `{pivot_class}`", - bounds_or_constraints = - typevar_context.bound_or_constraints_type(db, env).display(db, env), + "`{bounds_or_constraints}` is not an instance or subclass of \ + `{pivot_class}`", + bounds_or_constraints = typevar_context + .bound_or_constraints_type(db, env) + .display(db, env), pivot_class = pivot_class.display(db, env), )); let typevar = typevar_context.typevar(context.db()); diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 222241649e..9284d7f32f 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -764,7 +764,10 @@ impl<'db> ClassLiteral<'db> { ) -> Type<'db> { self.metaclass(db) .to_instance_approximation(db, env) - .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") + .expect( + "`Type::to_instance()` should always return `Some()` \ + when called on the type of a metaclass", + ) } /// Returns whether this class is type-check only. @@ -1727,10 +1730,12 @@ impl<'db> ClassType<'db> { db: &'db dyn Db, env: &ProgramEnvironment<'db>, ) -> Type<'db> { - self - .metaclass(db) + self.metaclass(db) .to_instance_approximation(db, env) - .expect("`Type::to_instance()` should always return `Some()` when called on the type of a metaclass") + .expect( + "`Type::to_instance()` should always return `Some()` \ + when called on the type of a metaclass", + ) } /// Returns the class member of this class named `name`. @@ -2038,7 +2043,8 @@ impl<'db> ClassType<'db> { assert_eq!( tuple.iter_element_types(db).count(), 1, - "Tuple specialization should have exactly one element when it has no length restriction" + "Tuple specialization should have exactly one element when it has \ + no length restriction" ); iterable_parameter = iterable_parameter.with_annotated_type( KnownClass::Iterable.to_specialized_instance( diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs index b9ec5226a6..08de9ae7a8 100644 --- a/crates/ty_python_semantic/src/types/class/known.rs +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -271,10 +271,6 @@ impl KnownClass { | Self::Mapping | Self::MutableMapping | Self::SupportsKeysAndGetItem - // 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) - | Self::NotImplementedType | Self::Staticmethod | Self::Classmethod | Self::Awaitable @@ -299,6 +295,11 @@ impl KnownClass { | Self::PydanticRootModel | Self::PydanticStrict => Some(Truthiness::Ambiguous), + // 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) + Self::NotImplementedType => Some(Truthiness::Ambiguous), + Self::Tuple => None, } } @@ -1716,7 +1717,6 @@ impl KnownClass { | Self::DefaultDict | Self::Deque | Self::OrderedDict - | Self::StdlibAlias // no equivalent class exists in typing_extensions, nor ever will | Self::ModuleType | Self::VersionInfo | Self::BaseException @@ -1783,7 +1783,12 @@ impl KnownClass { | Self::PydanticConfigDict | Self::PydanticRootModel | Self::PydanticStrict => module == self.canonical_module(python_version), + + // no equivalent class exists in typing_extensions, nor ever will + Self::StdlibAlias => module == self.canonical_module(python_version), + Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), + Self::SpecialForm | Self::TypeAliasType | Self::NoDefaultType @@ -1798,8 +1803,14 @@ impl KnownClass { | Self::Mapping | Self::MutableMapping | Self::ProtocolMeta - | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), - Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions), + | Self::NewType => { + matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) + } + + Self::Deprecated => matches!( + module, + KnownModule::Warnings | KnownModule::TypingExtensions + ), } } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 674f71f7e0..3722d9fde0 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -227,6 +227,10 @@ impl<'db> ClassBase<'db> { Type::KnownInstance(known_instance) => match known_instance { KnownInstanceType::SubscriptedGeneric(_) => Some(Self::Generic), KnownInstanceType::SubscriptedProtocol(_) => Some(Self::Protocol), + // A class inheriting from a newtype would make intuitive sense, but newtype + // wrappers are just identity callables at runtime, so this sort of inheritance + // doesn't work and isn't allowed. + KnownInstanceType::NewType(_) => None, KnownInstanceType::TypeAliasType(_) | KnownInstanceType::TypeVar(_) | KnownInstanceType::Deprecated(_) @@ -242,28 +246,19 @@ impl<'db> ClassBase<'db> { | KnownInstanceType::NamedTupleSpec(_) | KnownInstanceType::Sentinel(_) | KnownInstanceType::Range { .. } - // A class inheriting from a newtype would make intuitive sense, but newtype - // wrappers are just identity callables at runtime, so this sort of inheritance - // doesn't work and isn't allowed. - | KnownInstanceType::NewType(_) | KnownInstanceType::FunctoolsPartial(_) | KnownInstanceType::FunctoolsPartialCall(_) => None, - KnownInstanceType::TypeGenericAlias(_) => { - Self::try_from_type( - db, env, - KnownClass::Type.to_class_literal(db, env), - subclass, - ) - } - KnownInstanceType::Annotated(ty) => { - match ty.inner(db) { - Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), - Type::NominalInstance(instance) => { - Some(Self::Class(instance.class(db, env))) - } - _ => None, - } - } + KnownInstanceType::TypeGenericAlias(_) => Self::try_from_type( + db, + env, + KnownClass::Type.to_class_literal(db, env), + subclass, + ), + KnownInstanceType::Annotated(ty) => match ty.inner(db) { + Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)), + Type::NominalInstance(instance) => Some(Self::Class(instance.class(db, env))), + _ => None, + }, }, Type::SpecialForm(special_form) => match special_form { diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 715a178749..06aec1f75f 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -1543,7 +1543,8 @@ fn covariant_supertype_hint<'db>( ), [1], ) => Some( - "Consider using the supertype `collections.abc.Mapping`, which is covariant in its value type", + "Consider using the supertype `collections.abc.Mapping`, \ + which is covariant in its value type", ), _ => None, } @@ -1679,13 +1680,15 @@ pub(super) fn report_invalid_assignment<'db>( match target_ty { Type::ClassLiteral(class) => { diag.info(format_args!( - "Implicit shadowing of class `{}`. Add an annotation to make it explicit if this is intentional", + "Implicit shadowing of class `{}`. \ + Add an annotation to make it explicit if this is intentional", class.name(context.db()), )); } Type::FunctionLiteral(function) => { diag.info(format_args!( - "Implicit shadowing of function `{}`. Add an annotation to make it explicit if this is intentional", + "Implicit shadowing of function `{}`. \ + Add an annotation to make it explicit if this is intentional", function.name(context.db()), )); } @@ -1777,7 +1780,9 @@ pub(super) fn report_bad_dunder_get_call<'db>( }; let object_type = object_type.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot read property `{attribute}` on object of type `{object_type}` because it has no getter", + "Cannot read property `{attribute}` \ + on object of type `{object_type}` \ + because it has no getter", )); if let Some(file_range) = property .setter(db) @@ -1969,7 +1974,8 @@ pub(super) fn report_bad_dunder_delattr_call( )); if binding_error { diagnostic.info(format_args!( - "Type `{}` has a `__delattr__` method, but it cannot be called with the expected arguments", + "Type `{}` has a `__delattr__` method, \ + but it cannot be called with the expected arguments", object_type.display(db, env) )); diagnostic.info( @@ -2043,7 +2049,8 @@ pub(super) fn report_unsound_return_statement( let expected_ty_display = expected_ty.display_with(db, env, settings); diag.set_concise_message(format_args!( - "Unsound return statement: `{actual_ty_display}` is not a subtype of `{expected_ty_display}`" + "Unsound return statement: `{actual_ty_display}` is not a subtype \ + of `{expected_ty_display}`" )); diag.set_primary_annotation_message(format_args!("Inferred as `{actual_ty_display}`")); diag.annotate(context.secondary(return_type_range).message(format_args!( @@ -2126,14 +2133,16 @@ pub(super) fn report_invalid_generator_yield_type( "yield", "Yield expression type does not match annotation", format!( - "Yield type `{actual_display}` does not match annotated yield type `{expected_display}`" + "Yield type `{actual_display}` does not match annotated yield type \ + `{expected_display}`" ), ), GeneratorMismatchKind::SendType => ( "send", "Send type does not match annotation", format!( - "Send type `{actual_display}` does not match annotated send type `{expected_display}`" + "Send type `{actual_display}` does not match annotated send type \ + `{expected_display}`" ), ), }; @@ -2257,7 +2266,8 @@ pub(super) fn report_implicit_return_type( // If no return statement is defined in the function, then the function always returns `None` let mut diagnostic = if no_return { let mut diag = builder.into_diagnostic(format_args!( - "Function always implicitly returns `None`, which is not assignable to return type `{}`", + "Function always implicitly returns `None`, \ + which is not assignable to return type `{}`", expected_ty.display(db, env), )); diag.info( @@ -2517,14 +2527,15 @@ pub(crate) fn report_instance_layout_conflict( match disjoint_base.kind { DisjointBaseKind::DefinesSlots => { annotation = annotation.message(format_args!( - "`{base}` instances have a distinct memory layout because `{base}` defines non-empty `__slots__`", + "`{base}` instances have a distinct memory layout \ + because `{base}` defines non-empty `__slots__`", base = originating_base.name(db) )); } DisjointBaseKind::DisjointBaseDecorator => { annotation = annotation.message(format_args!( - "`{base}` instances have a distinct memory layout because of the way `{base}` \ - is implemented in a C extension", + "`{base}` instances have a distinct memory layout \ + because of the way `{base}` is implemented in a C extension", base = originating_base.name(db) )); } @@ -2543,8 +2554,8 @@ pub(crate) fn report_instance_layout_conflict( additional_annotation = match disjoint_base.kind { DisjointBaseKind::DefinesSlots => additional_annotation.message(format_args!( - "`{disjoint_base}` instances have a distinct memory layout because `{disjoint_base}` \ - defines non-empty `__slots__`", + "`{disjoint_base}` instances have a distinct memory layout \ + because `{disjoint_base}` defines non-empty `__slots__`", disjoint_base = disjoint_base.class.name(db), )), @@ -2820,7 +2831,8 @@ pub(crate) fn report_too_many_positional_patterns_for_class_pattern( return; }; builder.into_diagnostic(format_args!( - "Too many positional subpatterns for `{class_display}`: expected {positional_limit}, got {positional_count}" + "Too many positional subpatterns for `{class_display}`: \ + expected {positional_limit}, got {positional_count}" )); } @@ -2870,8 +2882,9 @@ pub(crate) fn report_runtime_check_against_non_runtime_checkable_protocol( diagnostic.set_primary_annotation_message("This call will raise `TypeError` at runtime"); add_non_runtime_checkable_protocol_context(db, &mut diagnostic, protocol); 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`" + "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(format_args!("See {RUNTIME_CHECKABLE_DOCS_URL}")); } @@ -2948,7 +2961,8 @@ pub(crate) fn report_runtime_check_against_typed_dict( }; let class_name = class.name(context.db()); let mut diagnostic = builder.into_diagnostic(format_args!( - "`TypedDict` class `{class_name}` cannot be used as the second argument to `{function_name}`", + "`TypedDict` class `{class_name}` cannot be used as the second argument \ + to `{function_name}`", function_name = function.name() )); diagnostic.set_primary_annotation_message("This call will raise `TypeError` at runtime"); @@ -2970,8 +2984,9 @@ pub(crate) fn report_match_pattern_against_non_runtime_checkable_protocol( )); } diagnostic.set_concise_message(format_args!( - "Unknown key \"{key}\" for TypedDict `{typed_dict_name}` - did you mean \"{suggestion}\"?", + "Unknown key \"{key}\" for TypedDict `{typed_dict_name}` - \ + did you mean \"{suggestion}\"?", )); } else { diagnostic .set_primary_annotation_message(format_args!("Unknown key \"{key}\"")); if let Some(full_ty) = full_object_ty { diagnostic.set_concise_message(format_args!( - "Unknown key \"{key}\" for TypedDict `{typed_dict_name}` (subscripted object has type `{full_ty}`)", + "Unknown key \"{key}\" for TypedDict `{typed_dict_name}` \ + (subscripted object has type `{full_ty}`)", full_ty = full_ty.display(db, env), )); } else { @@ -3714,7 +3733,8 @@ pub(crate) fn report_cannot_delete_typed_dict_key<'db>( "Cannot delete required key \"{field_name}\" from TypedDict `{typed_dict_name}`" )), TypedDictDeleteErrorKind::ReadOnlyExtraItem => builder.into_diagnostic(format_args!( - "Cannot delete read-only extra item \"{field_name}\" from TypedDict `{typed_dict_name}`" + "Cannot delete read-only extra item \"{field_name}\" \ + from TypedDict `{typed_dict_name}`" )), TypedDictDeleteErrorKind::UnknownKey => builder.into_diagnostic(format_args!( "Cannot delete unknown key \"{field_name}\" from TypedDict `{typed_dict_name}`" @@ -3756,7 +3776,8 @@ pub(crate) fn report_cannot_delete_typed_dict_key<'db>( // Add hint about how to allow deletion if matches!(error_kind, TypedDictDeleteErrorKind::RequiredKey) { diagnostic.info( - "Only keys marked as `NotRequired` (or in a TypedDict with `total=False`) can be deleted", + "Only keys marked as `NotRequired` \ + (or in a TypedDict with `total=False`) can be deleted", ); } } @@ -3783,8 +3804,9 @@ pub(crate) fn report_invalid_type_param_order<'db>( ) }) .expect( - "It should not be possible for a class to have a legacy generic context \ - if it does not inherit from `Protocol[]` or `Generic[]`", + "It should not be possible for a class to have \ + a legacy generic context if it does \ + not inherit from `Protocol[]` or `Generic[]`", ); let base_node = &node.bases()[base_index]; @@ -4037,10 +4059,12 @@ pub(crate) fn report_shadowed_type_variable<'db>( TypeVarKind::LegacyTypeVarTuple | TypeVarKind::Pep695TypeVarTuple => "TypeVarTuple", }; let mut diagnostic = builder.into_diagnostic(format_args!( - "Generic {kind} `{name}` uses {typevar_kind} `{typevar_name}` already bound by an enclosing scope", + "Generic {kind} `{name}` uses {typevar_kind} `{typevar_name}` \ + already bound by an enclosing scope", )); diagnostic.set_concise_message(format_args!( - "Generic {kind} `{name}` uses {typevar_kind} `{typevar_name}` already bound by an enclosing scope", + "Generic {kind} `{name}` uses {typevar_kind} `{typevar_name}` \ + already bound by an enclosing scope", )); diagnostic.set_primary_annotation_message(format_args!( "`{typevar_name}` used in {kind} definition here" @@ -4898,7 +4922,9 @@ pub(super) fn report_invalid_total_ordering_call( }; let mut diagnostic = builder.into_diagnostic( - "`@functools.total_ordering` requires at least one ordering method (`__lt__`, `__le__`, `__gt__`, or `__ge__`) to be defined", + "`@functools.total_ordering` requires at least one ordering method \ + (`__lt__`, `__le__`, `__gt__`, or `__ge__`) \ + to be defined", ); diagnostic.set_primary_annotation_message(format_args!( "`{}` does not define `__lt__`, `__le__`, `__gt__`, or `__ge__`", diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index a6b7f65a06..6379e778e8 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1044,7 +1044,9 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'_, 'db> { match (class, class.known(db)) { (_, Some(KnownClass::NoneType)) => f.with_type(self.ty).write_str("None"), - (_, Some(KnownClass::NoDefaultType)) => f.with_type(self.ty).write_str("NoDefault"), + (_, Some(KnownClass::NoDefaultType)) => { + f.with_type(self.ty).write_str("NoDefault") + } (_, Some(KnownClass::Float | KnownClass::Complex)) => { f.set_invalid_type_annotation(); class @@ -1056,13 +1058,18 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'_, 'db> { (ClassType::Generic(alias), Some(KnownClass::Tuple)) => alias .specialization(db) .tuple(db) - .expect("Specialization::tuple() should always return `Some()` for `KnownClass::Tuple`") + .expect( + "Specialization::tuple() should always return `Some()` for \ + `KnownClass::Tuple`", + ) + .display_with(db, self.env, self.settings.clone()) + .fmt_detailed(f), + (ClassType::NonGeneric(class), _) => class + .display_with(db, self.settings.clone()) + .fmt_detailed(f), + (ClassType::Generic(alias), _) => alias .display_with(db, self.env, self.settings.clone()) .fmt_detailed(f), - (ClassType::NonGeneric(class), _) => { - class.display_with(db, self.settings.clone()).fmt_detailed(f) - }, - (ClassType::Generic(alias), _) => alias.display_with(db, self.env, self.settings.clone()).fmt_detailed(f), } } Type::ProtocolInstance(protocol) => match protocol.inner { diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index 4a6bcc11fa..70e4d4fccb 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -1734,9 +1734,11 @@ impl KnownComparisonSemantics { Type::NominalInstance(instance) if instance.class(db, env).is_final(db) || soundness_policy.allow_unsafe_equality - // `object` can contain values whose classes define their own comparison - // method, so treating it as exact would incorrectly eliminate those values. - && !instance.has_known_class(db, KnownClass::Object) => + && ( + // `object` can contain values whose classes define their own comparison + // method, so treating it as exact would incorrectly eliminate those values. + !instance.has_known_class(db, KnownClass::Object) + ) => { Self::of_instance(db, env, ty, operator) } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 4eff6afd92..c18bc58674 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1755,24 +1755,27 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { TypeRelation::Subtyping | TypeRelation::SubtypingAssuming | TypeRelation::Redundancy { pure: false } - ) + ) && ( // Explicitly materialized sources are already static and cannot advance further. - && source.materialization_kind(db).is_none() + source.materialization_kind(db).is_none() + ) && ( // Performance only: `source_top != source` below already handles unchanged // arguments. Without expanding aliases, treat them as potentially gradual. - && source.types(db).iter().any(|ty| { + source.types(db).iter().any(|ty| { any_over_type(db, env, *ty, false, |ty| { ty.is_dynamic() || matches!(ty, Type::TypeAlias(_)) }) }) + ) && ( // Avoid the `self.always()` type-variable shortcut in // `check_subtyping_in_invariant_position`: it would incorrectly conclude // that `Top[Inv[Any]] <: Inv[T]` for an unresolved `T`. // TODO: remove this once that shortcut is removed. - && target + target .types(db) .iter() .all(|ty| !ty.has_typevar_or_typevar_instance(db, env)) + ) && ( // Only non-pure redundancy needs a target already equal to its top. // Materializing the source otherwise loses the bottom needed to // simplify `Covariant[Any] | Covariant[Any | str]`. Comparing both @@ -1782,14 +1785,14 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { // `class C[T: tuple[int, int]]`, `C[tuple[Any, int]]` and `C[tuple[int, Any]]` // have the same top and bottom but expose `Any` in different tuple positions. // TODO: Try resolving the above issues so we can compare top/bottom subtyping here. - && (!matches!(self.relation, TypeRelation::Redundancy { pure: false }) + !matches!(self.relation, TypeRelation::Redundancy { pure: false }) || target == target.materialize_impl( db, MaterializationKind::Top, self.materialization_visitor, - )) - { + ) + ) { let source_top = source.materialize_impl(db, MaterializationKind::Top, self.materialization_visitor); // Dynamic arguments can still be unchanged by top materialization; retrying @@ -2003,10 +2006,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { && let (Type::TypeVar(typevar), ty) | (ty, Type::TypeVar(typevar)) = (source_type, target_type) && !ty.is_type_var() - // Preserve union distribution before constructing constraints. Storing the - // entire union as an exact bound makes solving common generic calls involving - // large unions significantly more expensive. - && !ty.is_union() + && ( + // Preserve union distribution before constructing constraints. Storing the + // entire union as an exact bound makes solving common generic calls involving + // large unions significantly more expensive. + !ty.is_union() + ) { let ty = ty.materialized_divergent_fallback().unwrap_or(ty); let env = self.env; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 26a990e038..79d950f1c4 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -612,7 +612,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { #[expect( clippy::iter_over_hash_type, - reason = "constraints for distinct collection definitions are merged independently" + reason = "constraints for distinct collection definitions are merged \ + independently" )] for (collection_def, constraints) in &extra.collection_use_constraints { self.collection_use_constraints @@ -1717,10 +1718,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.report_lint(&INVALID_DECLARATION, node) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot shadow implicit global attribute `{place}` with declaration of type `{}`", + "Cannot shadow implicit global attribute `{place}` \ + with declaration of type `{}`", declared_type.display(db, env) )); - diagnostic.info(format_args!("The global symbol `{}` must always have a type assignable to `{}`", + diagnostic.info(format_args!( + "The global symbol `{}` \ + must always have a type assignable to `{}`", place, module_type_implicit_declaration.display(db, env) )); @@ -1730,11 +1734,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let declared_type = declared_ty.inner_type(); if inferred_ty.is_assignable_to(db, env, declared_type) { + // TODO We currently can't distinguish here between "no declared type" and + // "declared types is `Unknown` (e.g. due to a bad annotation, missing + // import, etc.)". Ideally we would still prefer `Unknown` declared type, + // but use inferred type if there is no declared type. if !should_preserve_inferred_binding_type(inferred_ty) - // TODO We currently can't distinguish here between "no declared type" and - // "declared types is `Unknown` (e.g. due to a bad annotation, missing - // import, etc.)". Ideally we would still prefer `Unknown` declared type, - // but use inferred type if there is no declared type. && !matches!(declared_type, Type::Dynamic(DynamicType::Unknown)) && declared_type.is_assignable_to(db, env, inferred_ty) { @@ -3926,7 +3930,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.report_lint(&INVALID_TYPE_ALIAS_TYPE, element) { builder.into_diagnostic(format_args!( - "Type parameter `{}` is bound in an outer scope and cannot be used in `type_params`", + "Type parameter `{}` is bound in an outer scope \ + and cannot be used in `type_params`", typevar.name(db), )); } @@ -3972,7 +3977,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { reported_default_order_error = true; builder.into_diagnostic(format_args!( - "Type parameter `{}` without a default cannot follow earlier parameter `{}` with a default", + "Type parameter `{}` without a default \ + cannot follow earlier parameter `{}` with a default", typevar.name(db), typevar_with_default.name(db), )); @@ -4012,7 +4018,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_TYPE_ALIAS_TYPE, &arguments.args[1]) { builder.into_diagnostic(format_args!( - "Type parameter `{}` used in the alias value must be included in `type_params`", + "Type parameter `{}` used in the alias value \ + must be included in `type_params`", typevar.name(self.db()), )); } @@ -4081,7 +4088,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_PARAMSPEC, annotation.as_ref()) { builder.into_diagnostic(format_args!( - "`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters", + "`{name}.{attr_name}` is only valid \ + for annotating `{variadic}` function parameters", )); } } else if let ast::Expr::Attribute(attr_expr) = annotation.as_ref() @@ -4103,7 +4111,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_PARAMSPEC, annotation.as_ref()) { builder.into_diagnostic(format_args!( - "`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters", + "`{name}.{attr_name}` is only valid \ + for annotating `{variadic}` function parameters", )); } } @@ -4312,7 +4321,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, annotation) { builder.into_diagnostic(format_args!( - "`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters", + "`{name}.{attr_name}` is only valid \ + for annotating `{variadic}` function parameters", )); } } else if let ast::Expr::Attribute(attr_expr) = annotation @@ -4333,7 +4343,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, annotation) { builder.into_diagnostic(format_args!( - "`{name}.{attr_name}` is only valid for annotating `{variadic}` function parameters", + "`{name}.{attr_name}` is only valid \ + for annotating `{variadic}` function parameters", )); } } @@ -4590,13 +4601,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(name_expr) = target.as_name_expr() && !name_expr.id.starts_with("__") && !matches!(name_expr.id.as_str(), "_ignore_" | "_value_" | "_name_") - // Not bare Final (bare Final is allowed on enum members) - && !(declared.qualifiers.contains(TypeQualifiers::FINAL) - && matches!(declared.inner_type(), Type::Dynamic(DynamicType::Unknown))) - // Value type would be an enum member at runtime (exclude callables, - // which are never members) - && !inferred_ty.is_subtype_of(db, env, Type::Callable(CallableType::unknown(self.db())) - .top_materialization(db, env), + && ( + // Not bare Final (bare Final is allowed on enum members) + !(declared.qualifiers.contains(TypeQualifiers::FINAL) + && matches!(declared.inner_type(), Type::Dynamic(DynamicType::Unknown))) + ) + && ( + // Value type would be an enum member at runtime (exclude callables, + // which are never members) + !inferred_ty.is_subtype_of( + db, + env, + Type::Callable(CallableType::unknown(self.db())) + .top_materialization(db, env), + ) ) { let current_scope_id = self.scope().file_scope_id(self.db()); @@ -5114,7 +5132,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { diag.set_primary_annotation_message(format_args!( "`{name}` has no declarations or bindings in the global scope" )); - diag.info("This limits ty's ability to make accurate inferences about the boundness and types of global-scope symbols"); + diag.info( + "This limits ty's ability to make accurate inferences \ + about the boundness and types of global-scope symbols", + ); diag.info(format_args!( "Consider adding a declaration to the global scope, e.g. `{name}: int`" )); @@ -6071,7 +6092,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_expression(&mut self, expression: &ast::Expr, tcx: TypeContext<'db>) -> Type<'db> { debug_assert!( !self.index.is_standalone_expression(expression), - "Calling `self.infer_expression` on a standalone-expression is not allowed because it can lead to double-inference. Use `self.infer_standalone_expression` instead." + "Calling `self.infer_expression` on a standalone-expression \ + is not allowed because it can lead to double-inference. \ + Use `self.infer_standalone_expression` instead." ); self.infer_expression_impl(expression, tcx) @@ -8270,12 +8293,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let in_stub = self.in_stub(); let previous_deferred_state = std::mem::replace(&mut self.deferred_state, in_stub.into()); + // TODO: We could perform multi-inference here if there are multiple `Callable` annotations + // in the union/intersection. let callable_tcx = if let Some(tcx) = tcx.annotation - // TODO: We could perform multi-inference here if there are multiple `Callable` annotations - // in the union/intersection. - && let Some(callable) = tcx - .filter_union(db, Type::is_callable_type) - .as_callable() + && let Some(callable) = tcx.filter_union(db, Type::is_callable_type).as_callable() { match callable.signatures(self.db()).overloads.as_slice() { [signature] => Some(signature), @@ -8848,13 +8869,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Type::TypedDict(typed_dict_ty) = value_type && matches!(method_name, "get" | "pop" | "setdefault") && !arguments.args.is_empty() - - // Validate the key argument for `TypedDict` methods - && let Some(first_arg) = arguments.args.first() + && let Some(first_arg) = ( + // Validate the key argument for `TypedDict` methods + arguments.args.first() + ) && let Some(key) = (match first_arg { ast::Expr::StringLiteral(ast::ExprStringLiteral { - value: key_literal, - .. + value: key_literal, .. }) => Some(key_literal.to_str()), _ => self .speculate_without_diagnostics() @@ -8919,7 +8940,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.report_lint(&INVALID_ARGUMENT_TYPE, first_arg) { builder.into_diagnostic(format_args!( - "Cannot {action} read-only extra item \"{key}\" {preposition} TypedDict `{}`", + "Cannot {action} read-only extra item \ + \"{key}\" {preposition} TypedDict `{}`", Type::TypedDict(typed_dict_ty).display(db, env), )); } @@ -9013,7 +9035,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INEFFECTIVE_FINAL, call_expression) { let mut diagnostic = builder.into_diagnostic( - "Type checkers will not prevent subclassing when `final()` is called as a function", + "Type checkers will not prevent subclassing \ + when `final()` is called as a function", ); diagnostic.info("Use `@final` as a decorator on a class or method instead"); } @@ -10402,16 +10425,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let bound_on_instance = match value_type { Type::ClassLiteral(class) => { - !class - .instance_member(db, env, None, attr) - .is_undefined() + !class.instance_member(db, env, None, attr).is_undefined() } Type::SubclassOf(subclass_of @ SubclassOfType { .. }) => { match subclass_of.subclass_of() { SubclassOfInner::Class(class) => { - !class - .instance_member(db, env, attr) - .is_undefined() + !class.instance_member(db, env, attr).is_undefined() } SubclassOfInner::Dynamic(_) => unreachable!( "Attribute lookup on a dynamic `SubclassOf` type \ @@ -10465,14 +10484,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut diag = builder.into_diagnostic(format_args!( "Special form `{special_form}` has no attribute `{attr_name}`", )); - if let Ok(defined_type) = value_type.in_type_expression(db, + if let Ok(defined_type) = value_type.in_type_expression( + db, self.scope(), self.typevar_binding_context, - self.inference_flags() - ) && !defined_type - .member(db, env, attr_name) - .place - .is_undefined() + self.inference_flags(), + ) && !defined_type.member(db, env, attr_name).place.is_undefined() { diag.help(format_args!( "Objects with type `{ty}` have a{maybe_n} `{attr_name}` \ @@ -10608,7 +10625,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut elements_missing_the_attribute = FxIndexSet::default(); for element in union.elements(db) { union_elements_missing_attribute( - db, env, + db, + env, *element, attr_name, &mut elements_missing_the_attribute, @@ -10621,17 +10639,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { { let missing_types = elements_missing_the_attribute .iter() - .map(|ty| { - format!("`{}`", ty.display(db, env)) - }) + .map(|ty| format!("`{}`", ty.display(db, env))) .collect::>() .join(", "); builder.into_diagnostic(format_args!( - "Attribute `{attr_name}` is not defined on {} in union `{union_like_type}`", + "Attribute `{attr_name}` is not defined on {} \ + in union `{union_like_type}`", missing_types, - union_like_type = - union_like_type.display(db, env), + union_like_type = union_like_type.display(db, env), )); } return type_when_bound; @@ -11291,7 +11307,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if bindings.len() > 20 { tracing::debug!( - "Inferred statement region `{:?}` contains {} bindings. Lookups by linear scan might be slow.", + "Inferred statement region `{:?}` contains {} bindings. \ + Lookups by linear scan might be slow.", self.region, bindings.len(), ); @@ -11299,7 +11316,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if declarations.len() > 20 { tracing::debug!( - "Inferred statement region `{:?}` contains {} declarations. Lookups by linear scan might be slow.", + "Inferred statement region `{:?}` contains {} declarations. \ + Lookups by linear scan might be slow.", self.region, declarations.len(), ); @@ -11490,7 +11508,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if bindings.len() > 20 { tracing::debug!( - "Inferred definition region `{:?}` contains {} bindings. Lookups by linear scan might be slow.", + "Inferred definition region `{:?}` contains {} bindings. \ + Lookups by linear scan might be slow.", self.region, bindings.len(), ); @@ -11498,7 +11517,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if declarations.len() > 20 { tracing::debug!( - "Inferred declaration region `{:?}` contains {} declarations. Lookups by linear scan might be slow.", + "Inferred declaration region `{:?}` contains {} declarations. \ + Lookups by linear scan might be slow.", self.region, declarations.len(), ); @@ -11868,7 +11888,8 @@ impl<'db> FullExpressionCacheEntry<'db> { .then(|| { if self.bindings.len() > 20 { tracing::debug!( - "Inferred expression region `{:?}` contains {} bindings. Lookups by linear scan might be slow.", + "Inferred expression region `{:?}` contains {} bindings. \ + Lookups by linear scan might be slow.", region, self.bindings.len() ); diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index d5f9680793..24a845fc99 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -23,7 +23,9 @@ use crate::types::infer::{InferenceFlags, TypeExpressionFlags}; use crate::types::special_form::AliasSpec; use crate::types::subscript::{LegacyGenericOrigin, SubscriptError, SubscriptErrorKind}; use crate::types::tuple::{Tuple, TupleSpecBuilder, TupleType, VariableSegment}; -use crate::types::typed_dict::{TypedDictAssignmentKind, TypedDictKeyAssignment}; +use crate::types::typed_dict::{ + TypedDictAssignmentKind, TypedDictExtraItems, TypedDictKeyAssignment, +}; use crate::types::typevar::TypeVarSet; use crate::types::{ BoundTypeVarInstance, CallArguments, CallDunderError, CallableBinding, CycleDetector, @@ -282,7 +284,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; builder.into_diagnostic( "Type arguments for `Literal` must be `None`, \ - a literal value (int, bool, str, or bytes), or an enum member", + a literal value (int, bool, str, or bytes), \ + or an enum member", ); } return Type::unknown(); @@ -1795,7 +1798,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_ASSIGNMENT, rhs_value_node) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot assign value of type `{}` to key of type `{}` on TypedDict `{}`", + "Cannot assign value of type `{}` to key of type `{}` \ + on TypedDict `{}`", rhs_value_ty.display(db, env), slice_ty.display(db, env), object_ty.display(db, env), @@ -1821,7 +1825,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_ASSIGNMENT, target.slice.as_ref()) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Cannot assign value of type `{assigned_d}` to key of type `{}` on TypedDict `{value_d}`", + "Cannot assign value of type `{assigned_d}` to key of type `{}` \ + on TypedDict `{value_d}`", slice_ty.display(db, env) )); attach_original_type_info(&mut diagnostic); @@ -1832,7 +1837,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .report_lint(&INVALID_KEY, target.slice.as_ref()) { let mut diagnostic = builder.into_diagnostic(format_args!( - "TypedDict `{value_d}` can only be subscripted with a string literal key, got key of type `{}`.", + "TypedDict `{value_d}` can only be subscripted \ + with a string literal key, got key of type `{}`.", slice_ty.display(db, env) )); attach_original_type_info(&mut diagnostic); @@ -1975,10 +1981,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let object_d = object_ty.display(db, env); let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid subscript assignment with key of type `{}` and value of \ - type `{assigned_d}` on object of type `{object_d}`", - slice_ty.display(db, env), - )); + "Invalid subscript assignment with key of type `{}` \ + and value of type `{assigned_d}` \ + on object of type `{object_d}`", + slice_ty.display(db, env), + )); // Special diagnostic for dictionaries if let Some([expected_key_ty, expected_value_ty]) = @@ -2026,10 +2033,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.report_lint(&CALL_NON_CALLABLE, target) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__setitem__` of type `{}` may not be callable on object of type `{}`", - bindings.callable_type().display(db, env), - object_ty.display(db, env), - )); + "Method `__setitem__` of type `{}` may not be callable \ + on object of type `{}`", + bindings.callable_type().display(db, env), + object_ty.display(db, env), + )); attach_original_type_info(&mut diagnostic); } } @@ -2168,146 +2176,137 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - match object_ty.try_call_dunder( + let Err(err) = object_ty.try_call_dunder( db, env, "__delitem__", CallArguments::positional([slice_ty]), TypeContext::default(), - ) { - Ok(_) => {} - Err(err) => { - match err { - CallDunderError::PossiblyUnbound { .. } => { - if let Some(builder) = self - .context - .report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, target) + ) else { + return; + }; + + match err { + CallDunderError::PossiblyUnbound { .. } => { + if let Some(builder) = self + .context + .report_lint(&POSSIBLY_MISSING_IMPLICIT_CALL, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` may be missing", + object_ty.display(db, env), + )); + attach_original_type_info(&mut diagnostic); + } + } + CallDunderError::CallError(call_error_kind, bindings, _) => { + match call_error_kind { + CallErrorKind::NotCallable => { + if let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, target) { let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` may be missing", + "Method `__delitem__` of type `{}` \ + is not callable on object of type `{}`", + bindings.callable_type().display(db, env), object_ty.display(db, env), )); attach_original_type_info(&mut diagnostic); } } - CallDunderError::CallError(call_error_kind, bindings, _) => { - match call_error_kind { - CallErrorKind::NotCallable => { - if let Some(builder) = - self.context.report_lint(&CALL_NON_CALLABLE, target) + CallErrorKind::BindingError => { + // For deletions of string literal keys on `TypedDict`, provide + // a more detailed diagnostic. + if let Some(typed_dict) = object_ty.as_typed_dict() { + if let Some(string_literal) = slice_ty.as_string_literal() { + let key = string_literal.value(db); + let items = typed_dict.items(db); + + if let Some(field) = items.get(key) { + // Key exists but is required (i.e., can't be deleted). + report_cannot_delete_typed_dict_key( + &self.context, + (&*target.slice).into(), + typed_dict, + key, + Some(field), + TypedDictDeleteErrorKind::RequiredKey, + ); + } else if typed_dict + .explicit_extra_items(db) + .is_some_and(TypedDictExtraItems::is_read_only) { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` is not callable \ - on object of type `{}`", - bindings.callable_type().display(db, env), - object_ty.display(db, env), - )); - attach_original_type_info(&mut diagnostic); + report_cannot_delete_typed_dict_key( + &self.context, + (&*target.slice).into(), + typed_dict, + key, + None, + TypedDictDeleteErrorKind::ReadOnlyExtraItem, + ); + } else { + // Key doesn't exist. + report_cannot_delete_typed_dict_key( + &self.context, + (&*target.slice).into(), + typed_dict, + key, + None, + TypedDictDeleteErrorKind::UnknownKey, + ); } - } - CallErrorKind::BindingError => { - // For deletions of string literal keys on `TypedDict`, provide - // a more detailed diagnostic. - if let Some(typed_dict) = object_ty.as_typed_dict() { - if let Some(string_literal) = - slice_ty.as_string_literal() - { - let key = string_literal.value(db); - let items = typed_dict.items(db); - - if let Some(field) = items.get(key) { - // Key exists but is required (i.e., can't be deleted). - report_cannot_delete_typed_dict_key( - &self.context, - (&*target.slice).into(), - typed_dict, - key, - Some(field), - TypedDictDeleteErrorKind::RequiredKey, - ); - } else if typed_dict - .explicit_extra_items(db) - .is_some_and(|extra_items| { - extra_items.is_read_only() - }) - { - report_cannot_delete_typed_dict_key( - &self.context, - (&*target.slice).into(), - typed_dict, - key, - None, - TypedDictDeleteErrorKind::ReadOnlyExtraItem, - ); - } else { - // Key doesn't exist. - report_cannot_delete_typed_dict_key( - &self.context, - (&*target.slice).into(), - typed_dict, - key, - None, - TypedDictDeleteErrorKind::UnknownKey, - ); - } - } else { - // Non-string-literal key on `TypedDict`. - if let Some(builder) = self - .context - .report_lint(&INVALID_ARGUMENT_TYPE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` cannot be called \ - with key of type `{}` on object of type `{}`", + } else { + // Non-string-literal key on `TypedDict`. + if let Some(builder) = + self.context.report_lint(&INVALID_ARGUMENT_TYPE, target) + { + let mut diagnostic = + builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` \ + cannot be called with key of type \ + `{}` on object of type `{}`", bindings.callable_type().display(db, env), slice_ty.display(db, env), object_ty.display(db, env), )); - attach_original_type_info(&mut diagnostic); - } - } - } else { - // Non-`TypedDict` object - if let Some(builder) = self - .context - .report_lint(&INVALID_ARGUMENT_TYPE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` cannot be called \ - with key of type `{}` on object of type `{}`", - bindings.callable_type().display(db, env), - slice_ty.display(db, env), - object_ty.display(db, env), - )); - attach_original_type_info(&mut diagnostic); - } + attach_original_type_info(&mut diagnostic); } } - CallErrorKind::PossiblyNotCallable => { - if let Some(builder) = - self.context.report_lint(&CALL_NON_CALLABLE, target) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Method `__delitem__` of type `{}` may not be callable \ - on object of type `{}`", + } else { + // Non-`TypedDict` object + if let Some(builder) = + self.context.report_lint(&INVALID_ARGUMENT_TYPE, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` cannot \ + be called with key of type `{}` on \ + object of type `{}`", bindings.callable_type().display(db, env), + slice_ty.display(db, env), object_ty.display(db, env), )); - attach_original_type_info(&mut diagnostic); - } + attach_original_type_info(&mut diagnostic); } } } - CallDunderError::MethodNotAvailable => { - report_not_subscriptable( - &self.context, - target, - object_ty, - "__delitem__", - ); + CallErrorKind::PossiblyNotCallable => { + if let Some(builder) = + self.context.report_lint(&CALL_NON_CALLABLE, target) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Method `__delitem__` of type `{}` may not be \ + callable on object of type `{}`", + bindings.callable_type().display(db, env), + object_ty.display(db, env), + )); + attach_original_type_info(&mut diagnostic); + } } } } + CallDunderError::MethodNotAvailable => { + report_not_subscriptable(&self.context, target, object_ty, "__delitem__"); + } } } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 8fbd270ae6..4b5ff55402 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -2058,24 +2058,28 @@ impl<'db> TypeInferenceBuilder<'db, '_> { std::slice::from_ref(arguments_slice) }; let mut has_unpacked_typevartuple = false; - let union_ty = UnionType::from_elements_leave_aliases(db, env, + let union_ty = UnionType::from_elements_leave_aliases( + db, + env, arguments.iter().map(|argument| { let ty = self.infer_type_expression(argument); if self .type_expression_flags(argument) .contains(TypeExpressionFlags::UNPACK) { - let is_typevartuple = matches!( - ty, - Type::TypeVar(typevar) if typevar.is_typevartuple(db) - ) || if let ast::Expr::Subscript(subscript) = argument { + let is_typevartuple = matches!( - self.expression_type(&subscript.slice), + ty, Type::TypeVar(typevar) if typevar.is_typevartuple(db) - ) - } else { - false - }; + ) || if let ast::Expr::Subscript(subscript) = argument { + matches!( + self.expression_type(&subscript.slice), + Type::TypeVar(typevar) if typevar.is_typevartuple(db) + ) + } else { + false + }; + if is_typevartuple { has_unpacked_typevartuple = true; if !ty.is_unknown() @@ -2084,7 +2088,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { { diagnostic::add_type_expression_reference_link( builder.into_diagnostic( - "Unpacking a `TypeVarTuple` in `Union` is not supported", + "Unpacking a `TypeVarTuple` in `Union` \ + is not supported", ), ); } diff --git a/crates/ty_python_semantic/src/types/iteration.rs b/crates/ty_python_semantic/src/types/iteration.rs index f226f061d5..04cc9292f8 100644 --- a/crates/ty_python_semantic/src/types/iteration.rs +++ b/crates/ty_python_semantic/src/types/iteration.rs @@ -120,11 +120,9 @@ impl<'db> Type<'db> { Type::NewTypeInstance(newtype) => { non_async_special_case(db, env, newtype.concrete_base_type(db)) } - Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => { - Some(Cow::Owned(TupleSpec::homogeneous(todo_type!( - "*tuple[] annotations" - )))) - } + Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => Some(Cow::Owned( + TupleSpec::homogeneous(todo_type!("*tuple[] annotations")), + )), Type::LiteralValue(literal) => match literal.kind() { LiteralValueTypeKind::Bytes(bytes) => { let bytes_literal = bytes.value(db); @@ -132,15 +130,13 @@ impl<'db> Type<'db> { TupleSpec::heterogeneous( bytes_literal .iter() - .map(|b| Type::int_literal( i64::from(*b))), + .map(|b| Type::int_literal(i64::from(*b))), ) } else { - TupleSpec::homogeneous( - KnownClass::Int.to_instance(db, env), - ) + TupleSpec::homogeneous(KnownClass::Int.to_instance(db, env)) }; Some(Cow::Owned(spec)) - }, + } LiteralValueTypeKind::String(string_literal_ty) => { let string_literal = string_literal_ty.value(db); let spec = if string_literal.len() < MAX_TUPLE_LENGTH { @@ -158,8 +154,8 @@ impl<'db> Type<'db> { LiteralValueTypeKind::LiteralString => { Some(Cow::Owned(TupleSpec::homogeneous(ty))) } - _ => None - } + _ => None, + }, Type::Never => { // The dunder logic below would have us return `tuple[Never, ...]`, which eagerly // simplifies to `tuple[()]`. That will will cause us to emit false positives if we @@ -168,9 +164,7 @@ impl<'db> Type<'db> { // diagnostic in unreachable code. Some(Cow::Owned(TupleSpec::homogeneous(Type::unknown()))) } - Type::TypeAlias(alias) => { - non_async_special_case(db, env, alias.value_type(db)) - } + Type::TypeAlias(alias) => non_async_special_case(db, env, alias.value_type(db)), Type::TypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db, env)? { TypeVarBoundOrConstraints::UpperBound(bound) => { non_async_special_case(db, env, bound) @@ -189,7 +183,9 @@ impl<'db> Type<'db> { .ok()?; let mut builder = TupleSpecBuilder::from(&*first_element_spec); for element in elements_iter { - builder = builder.union(db, env, + builder = builder.union( + db, + env, &*element .try_iterate_with_mode(db, env, EvaluationMode::Sync) .ok()?, @@ -218,13 +214,13 @@ impl<'db> Type<'db> { // If flattening didn't change anything, iterate the intersection directly. if flattened == ty { - let mut specs_iter = intersection.positive_elements_or_object(db).filter_map( - |element| { + let mut specs_iter = intersection + .positive_elements_or_object(db) + .filter_map(|element| { element .try_iterate_with_mode(db, env, EvaluationMode::Sync) .ok() - }, - ); + }); let first_spec = specs_iter.next()?; let mut builder = TupleSpecBuilder::from(&*first_spec); for spec in specs_iter { @@ -258,11 +254,6 @@ impl<'db> Type<'db> { | Type::DataclassTransformer(_) | Type::Callable(_) | Type::ModuleLiteral(_) - // We could infer a precise tuple spec for enum classes with members, - // but it's not clear whether that's worth the added complexity: - // you'd have to check that `EnumMeta.__iter__` is not overridden for it to be sound - // (enums can have `EnumMeta` subclasses as their metaclasses). - | Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::ProtocolInstance(_) | Type::SpecialForm(_) @@ -274,7 +265,13 @@ impl<'db> Type<'db> { | Type::TypeIs(_) | Type::TypeGuard(_) | Type::TypeForm(_) - | Type::TypedDict(_) => None + | Type::TypedDict(_) => None, + + // We could infer a precise tuple spec for enum classes with members, + // but it's not clear whether that's worth the added complexity: + // you'd have to check that `EnumMeta.__iter__` is not overridden for it to be sound + // (enums can have `EnumMeta` subclasses as their metaclasses). + Type::ClassLiteral(_) => None, } } @@ -744,10 +741,14 @@ impl<'db> IterationError<'db> { match kind { CallErrorKind::NotCallable => { - reporter.is_not(format_args!( - "Its `{method}` attribute has type `{dunder_iter_type}`, which is not callable", - dunder_iter_type = bindings.callable_type().display(db, env), - ), ErrorContext::Disabled); + reporter.is_not( + format_args!( + "Its `{method}` attribute has type `{dunder_iter_type}`, \ + which is not callable", + dunder_iter_type = bindings.callable_type().display(db, env), + ), + ErrorContext::Disabled, + ); } CallErrorKind::PossiblyNotCallable => { reporter.may_not( @@ -801,52 +802,83 @@ impl<'db> IterationError<'db> { }; match dunder_next_error { CallDunderError::MethodNotAvailable => { - reporter.is_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which has no `{dunder_next_name}` method", - iterator_type = iterator.display(db, env), - ), ErrorContext::Disabled); + reporter.is_not( + format_args!( + "Its `{dunder_iter_name}` method returns an object of type \ + `{iterator_type}`, which has no `{dunder_next_name}` method", + iterator_type = iterator.display(db, env), + ), + ErrorContext::Disabled, + ); } CallDunderError::PossiblyUnbound { .. } => { - reporter.may_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which may not have a `{dunder_next_name}` method", - iterator_type = iterator.display(db, env), - ), ErrorContext::Enabled); + reporter.may_not( + format_args!( + "Its `{dunder_iter_name}` method returns \ + an object of type `{iterator_type}`, \ + which may not have a `{dunder_next_name}` method", + iterator_type = iterator.display(db, env), + ), + ErrorContext::Enabled, + ); } CallDunderError::CallError(CallErrorKind::NotCallable, _, _) => { - reporter.is_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which has a `{dunder_next_name}` attribute that is not callable", - iterator_type = iterator.display(db, env), - ), ErrorContext::Disabled); + reporter.is_not( + format_args!( + "Its `{dunder_iter_name}` method returns \ + an object of type `{iterator_type}`, \ + which has a `{dunder_next_name}` attribute \ + that is not callable", + iterator_type = iterator.display(db, env), + ), + ErrorContext::Disabled, + ); } CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _, _) => { - reporter.may_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which has a `{dunder_next_name}` attribute that may not be callable", - iterator_type = iterator.display(db, env), - ), ErrorContext::Enabled); + reporter.may_not( + format_args!( + "Its `{dunder_iter_name}` method returns \ + an object of type `{iterator_type}`, \ + which has a `{dunder_next_name}` attribute \ + that may not be callable", + iterator_type = iterator.display(db, env), + ), + ErrorContext::Enabled, + ); } CallDunderError::CallError(CallErrorKind::BindingError, bindings, _) if bindings.is_single() => { reporter - .is_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which has an invalid `{dunder_next_name}` method", - iterator_type = iterator.display(db, env), - ), ErrorContext::Enabled) - .info(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`")); + .is_not( + format_args!( + "Its `{dunder_iter_name}` method returns \ + an object of type `{iterator_type}`, \ + which has an invalid `{dunder_next_name}` method", + iterator_type = iterator.display(db, env), + ), + ErrorContext::Enabled, + ) + .info(format_args!( + "Expected signature for `{dunder_next_name}` is \ + `def {dunder_next_name}(self): ...`" + )); } CallDunderError::CallError(CallErrorKind::BindingError, _, _) => { reporter - .may_not(format_args!( - "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \ - which may have an invalid `{dunder_next_name}` method", - iterator_type = iterator.display(db, env), - ), ErrorContext::Enabled) - .info(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`")); + .may_not( + format_args!( + "Its `{dunder_iter_name}` method returns an object \ + of type `{iterator_type}`, which may have \ + an invalid `{dunder_next_name}` method", + iterator_type = iterator.display(db, env), + ), + ErrorContext::Enabled, + ) + .info(format_args!( + "Expected signature for `{dunder_next_name}` is \ + `def {dunder_next_name}(self): ...`" + )); } } } @@ -889,8 +921,9 @@ impl<'db> IterationError<'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", + and its `__getitem__` attribute \ + (with type `{dunder_getitem_type}`) \ + may not be callable", dunder_getitem_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled, @@ -916,8 +949,10 @@ impl<'db> IterationError<'db> { let mut diag = 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", + 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, env), ), ErrorContext::Disabled, @@ -959,8 +994,8 @@ impl<'db> IterationError<'db> { reporter.is_not( format_args!( "It has no `__iter__` method and \ - its `__getitem__` attribute has type `{dunder_getitem_type}`, \ - which is not callable", + its `__getitem__` attribute has type `{dunder_getitem_type}`, \ + which is not callable", dunder_getitem_type = bindings.callable_type().display(db, env), ), ErrorContext::Disabled, @@ -976,13 +1011,16 @@ impl<'db> IterationError<'db> { ); } CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, bindings, _) => { - reporter.may_not( - "It has no `__iter__` method and its `__getitem__` attribute is invalid", - ErrorContext::Disabled, - ).info(format_args!( - "`__getitem__` has type `{dunder_getitem_type}`, which is not callable", - dunder_getitem_type = bindings.callable_type().display(db, env), - )); + reporter + .may_not( + "It has no `__iter__` method \ + and its `__getitem__` attribute is invalid", + ErrorContext::Disabled, + ) + .info(format_args!( + "`__getitem__` has type `{dunder_getitem_type}`, which is not callable", + dunder_getitem_type = bindings.callable_type().display(db, env), + )); } CallDunderError::CallError(CallErrorKind::BindingError, bindings, _) if bindings.is_single() => @@ -1005,8 +1043,10 @@ impl<'db> IterationError<'db> { .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", + 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, env), ), ErrorContext::Disabled, diff --git a/crates/ty_python_semantic/src/types/overrides.rs b/crates/ty_python_semantic/src/types/overrides.rs index 8987a6f9f0..e556ef7c3a 100644 --- a/crates/ty_python_semantic/src/types/overrides.rs +++ b/crates/ty_python_semantic/src/types/overrides.rs @@ -944,9 +944,11 @@ fn check_class_declaration<'db>( if !subclass_overrides_superclass_declaration && !has_dynamic_superclass - // accessing `.kind()` here is fine as `definition` - // will always be a definition in the file currently being checked - && first_reachable_definition.kind(db).is_function_def() + && ( + // accessing `.kind()` here is fine as `definition` + // will always be a definition in the file currently being checked + first_reachable_definition.kind(db).is_function_def() + ) { check_explicit_overrides(context, member, class_scope, class); } diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index 574263accd..2525e3a7da 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -287,13 +287,15 @@ impl<'db> Type<'db> { | Type::SpecialForm(_) | Type::KnownInstance(_) | Type::AlwaysFalsy - | Type::AlwaysTruthy + | Type::AlwaysTruthy => true, + // `T` is always a subtype of itself, // and `T` is always a subtype of `T | None` - | Type::TypeVar(_) + Type::TypeVar(_) => true, + // might inherit `Any`, but subtyping is still reflexive - | Type::ClassLiteral(_) - => true, + Type::ClassLiteral(_) => true, + Type::Dynamic(_) | Type::Divergent(_) | Type::NominalInstance(_) diff --git a/crates/ty_python_semantic/src/types/relation_error.rs b/crates/ty_python_semantic/src/types/relation_error.rs index 2cb8851019..06570acce6 100644 --- a/crates/ty_python_semantic/src/types/relation_error.rs +++ b/crates/ty_python_semantic/src/types/relation_error.rs @@ -266,7 +266,8 @@ impl<'db> ErrorContext<'db> { } => { help_messages.insert(HelpMessages::RequiredFieldCouldBeRemoved); format!( - "field \"{field_name}\" is required in {source} but not required and mutable in {target}", + "field \"{field_name}\" is required in {source} \ + but not required and mutable in {target}", source = typed_dict_name(source), target = typed_dict_name(target) ) @@ -289,7 +290,8 @@ impl<'db> ErrorContext<'db> { source_field, target_field, } => format!( - "field \"{field_name}\" on {source} has type `{source_field}` which is not {relation} type `{target_field}` expected by {target}", + "field \"{field_name}\" on {source} has type `{source_field}` \ + which is not {relation} type `{target_field}` expected by {target}", source = typed_dict_name(source), target = typed_dict_name(target), relation = relation.description(), @@ -374,7 +376,8 @@ impl<'db> ErrorContext<'db> { Self::TopCallableAssignedToNonTop { return_type } => { help_messages.insert(HelpMessages::TopCallableExplanation); format!( - "Object of type `Top[(...) -> {}]` is not safe to call; its signature is not known", + "Object of type `Top[(...) -> {}]` is not safe to call; \ + its signature is not known", return_type.display(db, env) ) } @@ -382,7 +385,8 @@ impl<'db> ErrorContext<'db> { source_name, target_name, } => format!( - "the parameter named `{source_name}` does not match `{target_name}` (and can be used as a keyword parameter)", + "the parameter named `{source_name}` does not match `{target_name}` \ + (and can be used as a keyword parameter)", ), Self::ParameterMustAcceptKeywordArguments { source_name, @@ -390,7 +394,8 @@ impl<'db> ErrorContext<'db> { } => { if let Some(source_name) = source_name { format!( - "parameter `{source_name}` is positional-only but must also accept keyword arguments", + "parameter `{source_name}` is positional-only \ + but must also accept keyword arguments", ) } else { format!("parameter `{target_name}` must accept keyword arguments") @@ -449,7 +454,8 @@ impl<'db> ErrorContext<'db> { ty.display(db, env), ), Self::ProtocolMemberClassVarMismatch { member_name, ty } => format!( - "protocol member `{member_name}` is an instance variable on type `{}`, but a class variable is required", + "protocol member `{member_name}` is an instance variable on type `{}`, \ + but a class variable is required", ty.display(db, env), ), Self::ProtocolSpecialMethodNotDefinedOnMetaType => { @@ -495,9 +501,10 @@ enum HelpMessages { impl std::fmt::Display for HelpMessages { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - HelpMessages::RequiredFieldCouldBeRemoved => { - f.write_str("The required field could be removed through a destructive operation like `del` on the target.") - } + HelpMessages::RequiredFieldCouldBeRemoved => f.write_str( + "The required field could be removed through a destructive operation \ + like `del` on the target.", + ), HelpMessages::TypedDictNotAssignableToDict(relation) => { write!( f, @@ -509,26 +516,37 @@ impl std::fmt::Display for HelpMessages { HelpMessages::ConsiderUsingMappingInsteadOfDict => { f.write_str("Consider using `Mapping[..]` instead of `dict[..]`.") } - HelpMessages::OpenTypedDictNotAssignableToMapping {typed_dict_name, relation} => { - let name = typed_dict_name.as_ref().map(|name|format!("`{name}`")).unwrap_or_else(||"this TypedDict".to_string()); + HelpMessages::OpenTypedDictNotAssignableToMapping { + typed_dict_name, + relation, + } => { + let name = typed_dict_name + .as_ref() + .map(|name| format!("`{name}`")) + .unwrap_or_else(|| "this TypedDict".to_string()); write!( f, "{name} would be {relation} this `Mapping` type \ - if it were declared with `closed=True`, but TypedDicts are open by default.", + if it were declared with `closed=True`, \ + but TypedDicts are open by default.", relation = relation.description() ) } - HelpMessages::ExplainOpenTypedDictUnsoundness {typed_dict_name} => { - let name = typed_dict_name.as_ref().map(|name|format!("`{name}`")).unwrap_or_else(||"this TypedDict".to_string()); + HelpMessages::ExplainOpenTypedDictUnsoundness { typed_dict_name } => { + let name = typed_dict_name + .as_ref() + .map(|name| format!("`{name}`")) + .unwrap_or_else(|| "this TypedDict".to_string()); write!( f, - "A subclass of {name} could validly add a new field of an arbitrary type, \ - violating subtyping with the `Mapping` type" + "A subclass of {name} could validly add a new field \ + of an arbitrary type, violating subtyping with the `Mapping` type" ) } HelpMessages::TopCallableExplanation => f.write_str( "This type includes all possible parameter sets, \ - so it cannot safely be called because there is no valid set of arguments for it", + so it cannot safely be called \ + because there is no valid set of arguments for it", ), HelpMessages::ConsiderAddingADefaultValue { parameter_name } => match parameter_name { Some(name) => write!(f, "Parameter `{name}` must have a default value"), diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 5932d4bd54..c9586d3893 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -612,16 +612,16 @@ impl SpecialFormType { pub(super) const fn is_callable(self) -> bool { match self { // TypedDict can be called as a constructor to create TypedDict types - Self::TypedDict(_) + Self::TypedDict(_) => true, // Collection constructors are callable // TODO actually implement support for calling them - | Self::LegacyStdlibAlias( + Self::LegacyStdlibAlias( LegacyStdlibAlias::ChainMap | LegacyStdlibAlias::Counter | LegacyStdlibAlias::DefaultDict | LegacyStdlibAlias::Deque - | LegacyStdlibAlias::OrderedDict + | LegacyStdlibAlias::OrderedDict, ) | Self::NamedTuple => true, Self::TypeForm => true, @@ -632,7 +632,7 @@ impl SpecialFormType { LegacyStdlibAlias::List | LegacyStdlibAlias::Dict | LegacyStdlibAlias::Set - | LegacyStdlibAlias::FrozenSet + | LegacyStdlibAlias::FrozenSet, ) | Self::Tuple | Self::Type => false, @@ -713,9 +713,11 @@ impl SpecialFormType { | Self::Divergent | Self::Todo | Self::TypeOf - | Self::Any // can be used in `issubclass()` but not `isinstance()`. - | Self::Unpack => false, - Self::TypeForm => false, + | Self::Unpack + | Self::TypeForm => false, + + // can be used in `issubclass()` but not `isinstance()`. + Self::Any => false, } } diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index c729d6cc19..d1927128e4 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -1544,9 +1544,10 @@ impl<'db> VariableLengthTuple, VariableSegment<'db>> { .map(move |index| { self.type_at_nonnegative_index(db, env, index) .unwrap_or_else(|| { - unreachable!( - "front-origin fixed slice positions are validated during plan construction" - ) + unreachable!( + "front-origin fixed slice positions are validated \ + during plan construction" + ) }) }) } diff --git a/crates/ty_server/src/document/notebook.rs b/crates/ty_server/src/document/notebook.rs index 97a1d843c5..e160d422eb 100644 --- a/crates/ty_server/src/document/notebook.rs +++ b/crates/ty_server/src/document/notebook.rs @@ -124,8 +124,12 @@ impl NotebookDocument { nbformat_minor: 5, }; - ruff_notebook::Notebook::from_raw_notebook(raw_notebook, false) - .unwrap_or_else(|err| panic!("Server notebook document could not be converted to ty's notebook document format: {err}")) + ruff_notebook::Notebook::from_raw_notebook(raw_notebook, false).unwrap_or_else(|err| { + panic!( + "Server notebook document could not be converted to ty's \ + notebook document format: {err}" + ) + }) } pub(crate) fn update( diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index 0e0637321b..dced17ca90 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -493,8 +493,11 @@ where anyhow::anyhow!("JSON parsing failure:\n{json_err}") } server::ExtractError::MethodMismatch(_) => { - unreachable!("A method mismatch should not be possible here unless you've used a different handler (`Req`) \ - than the one whose method name was matched against earlier.") + unreachable!( + "A method mismatch should not be possible here \ + unless you've used a different handler (`Req`) \ + than the one whose method name was matched against earlier." + ) } }) .with_failure_code(server::ErrorCode::InvalidParams) @@ -542,8 +545,11 @@ where anyhow::anyhow!("JSON parsing failure:\n{json_err}") } server::ExtractError::MethodMismatch(_) => { - unreachable!("A method mismatch should not be possible here unless you've used a different handler (`N`) \ - than the one whose method name was matched against earlier.") + unreachable!( + "A method mismatch should not be possible here \ + unless you've used a different handler (`N`) \ + than the one whose method name was matched against earlier." + ) } }) .with_failure_code(server::ErrorCode::InvalidParams)?, diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 34330180e6..d9e3f4a293 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -230,7 +230,11 @@ impl Session { .and_then(|request| { if !self.request_queue.incoming().is_pending(&request.id) { // Clear out the suspended request if the request has been cancelled. - tracing::debug!("Skipping suspended workspace diagnostics request `{}` because it was cancelled", request.id); + tracing::debug!( + "Skipping suspended workspace diagnostics request `{}` \ + because it was cancelled", + request.id + ); return None; } @@ -918,12 +922,14 @@ impl Session { match diagnostic_mode { DiagnosticMode::Off => { tracing::debug!( - "Skipping registration of diagnostic capability because diagnostics are turned off" + "Skipping registration of diagnostic capability \ + because diagnostics are turned off" ); } DiagnosticMode::OpenFilesOnly | DiagnosticMode::Workspace => { tracing::debug!( - "Registering diagnostic capability with {diagnostic_mode:?} diagnostic mode" + "Registering diagnostic capability \ + with {diagnostic_mode:?} diagnostic mode" ); registrations.push(Registration { id: DIAGNOSTIC_REGISTRATION_ID.into(), diff --git a/crates/ty_site_packages/src/lib.rs b/crates/ty_site_packages/src/lib.rs index 751d4f6236..7649164acf 100644 --- a/crates/ty_site_packages/src/lib.rs +++ b/crates/ty_site_packages/src/lib.rs @@ -137,8 +137,9 @@ impl SitePackagesPaths { debug_assert!( matches!(c, Utf8Component::Normal(_)), "Unexpected component in site-packages path `{c:?}` \ - (expected `site-packages` to be an absolute path with symlinks resolved, \ - located at `/lib/pythonX.Y/site-packages`)" + (expected `site-packages` to be an absolute path \ + with symlinks resolved, located at \ + `/lib/pythonX.Y/site-packages`)" ); c.as_str() @@ -769,13 +770,21 @@ impl VirtualEnvironment { let parent_environment = if created_with_uv { parent_environment .and_then(|sys_prefix| { - PythonEnvironment::new(sys_prefix, SysPrefixPathOrigin::DerivedFromPyvenvCfg, system) + PythonEnvironment::new( + sys_prefix, + SysPrefixPathOrigin::DerivedFromPyvenvCfg, + system, + ) .inspect_err(|err| { tracing::warn!( - "Failed to resolve the parent environment of this ephemeral uv virtual environment \ - from the `extends-environment` value specified in the `pyvenv.cfg` file at {pyvenv_cfg_path}. \ - Imports will not be resolved correctly if they refer to packages installed into the parent \ - environment. Underlying error: {err}", + "Failed to resolve the parent environment \ + of this ephemeral uv virtual environment \ + from the `extends-environment` value specified \ + in the `pyvenv.cfg` file at {pyvenv_cfg_path}. \ + Imports will not be resolved correctly \ + if they refer to packages installed \ + into the parent environment. \ + Underlying error: {err}", ); }) .ok() @@ -843,9 +852,11 @@ impl VirtualEnvironment { } Err(err) => { tracing::warn!( - "Failed to resolve the site-packages directories of this ephemeral uv virtual environment's \ - parent environment. Imports will not be resolved correctly if they refer to packages installed \ - into the parent environment. Underlying error: {err}" + "Failed to resolve the site-packages directories \ + of this ephemeral uv virtual environment's parent environment. \ + Imports will not be resolved correctly if they refer to packages \ + installed into the parent environment. \ + Underlying error: {err}" ); } } @@ -871,15 +882,16 @@ impl VirtualEnvironment { } else { tracing::warn!( "Failed to resolve `sys.prefix` of the system Python installation \ -from the `home` value in the `pyvenv.cfg` file at `{}`. \ -System site-packages will not be used for module resolution.", + from the `home` value in the `pyvenv.cfg` file at `{}`. \ + System site-packages will not be used for module resolution.", root_path.join("pyvenv.cfg") ); } } tracing::debug!( - "Resolved site-packages directories for this virtual environment are: {site_packages_directories}" + "Resolved site-packages directories for this virtual environment are: \ + {site_packages_directories}" ); Ok(site_packages_directories) } @@ -924,9 +936,9 @@ System site-packages will not be used for module resolution.", } else { let cfg_path = root_path.join("pyvenv.cfg"); tracing::debug!( - "Failed to resolve `sys.prefix` of the system Python installation \ -from the `home` value in the `pyvenv.cfg` file at `{cfg_path}`. \ -System stdlib will not be used for module definitions.", + "Failed to resolve `sys.prefix` of the system Python installation from the `home` \ + value in the `pyvenv.cfg` file at `{cfg_path}`. System stdlib will not be used \ + for module definitions.", ); Err(StdlibDiscoveryError::NoSysPrefixFound(cfg_path)) } @@ -1153,7 +1165,8 @@ impl SystemEnvironment { )?; tracing::debug!( - "Resolved site-packages directories for this environment are: {site_packages_directories}" + "Resolved site-packages directories for this environment are: \ + {site_packages_directories}" ); Ok(site_packages_directories) } @@ -1291,7 +1304,8 @@ impl std::fmt::Display for SitePackagesDiscoveryError { f, origin, inner, - "Failed to iterate over the contents of the `lib`/`lib64` directories of the Python installation", + "Failed to iterate over the contents \ + of the `lib`/`lib64` directories of the Python installation", None, &**system, ) @@ -1302,7 +1316,8 @@ impl std::fmt::Display for SitePackagesDiscoveryError { inner, &format!("Invalid {origin}"), Some( - "Could not find a `site-packages` directory for this Python installation/executable", + "Could not find a `site-packages` directory for this Python \ + installation/executable", ), &**system, ), @@ -1334,7 +1349,8 @@ impl std::fmt::Display for StdlibDiscoveryError { f, origin, inner, - "Failed to iterate over the contents of the `lib` directory of the Python installation", + "Failed to iterate over the contents \ + of the `lib` directory of the Python installation", None, &**system, ) @@ -1443,7 +1459,8 @@ impl fmt::Display for PyvenvCfgParseErrorKind { write!( f, "the following error was encountered \ -when trying to resolve the `home` value to a directory on disk: {io_err}" + when trying to resolve the `home` value \ + to a directory on disk: {io_err}" ) } } @@ -1557,7 +1574,10 @@ fn discover_package_dirs( } let path = entry.into_path(); let name = path.file_name().unwrap_or_else(|| { - panic!("File name should be non-null because path is guaranteed to be a child of `{prefix_dir}`") + panic!( + "File name should be non-null because path is guaranteed \ + to be a child of `{prefix_dir}`" + ) }); let matches_implementation = match implementation { @@ -2048,7 +2068,8 @@ impl SysPrefixPath { let path = entry.into_path(); let name = path.file_name().expect( - "File name should be non-null because path is guaranteed to be a child of `lib`", + "File name should be non-null \ + because path is guaranteed to be a child of `lib`", ); if !(name.starts_with("python3.") || name.starts_with("pypy3.")) { @@ -2523,7 +2544,8 @@ mod tests { ) { assert!( self.virtual_env.is_none(), - "`assert_system_environment` should only be used when `virtual_env` is not populated" + "`assert_system_environment` should only be used \ + when `virtual_env` is not populated" ); assert_eq!( @@ -3052,7 +3074,8 @@ mod tests { #[test] fn pyvenv_cfg_with_strange_whitespace_parses() { - let pyvenv_cfg = " home= /a path with whitespace/python\t \t \nversion_info = 3.13 \n\n\n\nimplementation =PyPy"; + let pyvenv_cfg = " home= /a path with whitespace/python\t \t \nversion_info = 3.13 \ + \n\n\n\nimplementation =PyPy"; let parsed = PyvenvCfgParser::new(pyvenv_cfg).parse().unwrap(); assert_eq!( parsed.base_executable_home_path, From 4992557467f8f6e2c08db3d3936b61c2bf86f74b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 10 Aug 2026 15:35:50 +0100 Subject: [PATCH 348/390] [ty] Preserve typing-only completion ranking in TYPE_CHECKING blocks (#27549) ## Summary Certain symbols are known not to exist at runtime (only at type-checking time), and we downrank these symbols in autocomplete suggestions in `.py` files. We don't need to do any such downranking when the user's cursor is inside an `if TYPE_CHECKING` block, however, information which is now easily retrievable from the semantic index. This PR updates our ranking logic to check for this before downranking type-check-only symbols. ## Test plan Various scenarios in `ty_completion_eval` were updated. --- AGENTS.md | 12 ++++ .../completion-evaluation-tasks.csv | 19 ++++- .../main.py | 36 ++++++++++ .../private_stub.pyi | 4 ++ .../typing-only-auto-import-ranking/main.py | 12 ++++ crates/ty_ide/src/completion.rs | 71 +++++++++++++++---- 6 files changed, 139 insertions(+), 15 deletions(-) create mode 100644 crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/private_stub.pyi diff --git a/AGENTS.md b/AGENTS.md index 2a7ce6e004..6324173da2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,6 +111,18 @@ When the task matches a more specific ty workflow, also read and follow that ski - Ecosystem report summaries: `.agents/skills/summarise-ecosystem-results/SKILL.md`. - Reproducing, investigating, or minimizing ecosystem or primer differences: `.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md`. +### Completion ranking + +When changing ty autocomplete ranking, add or update evaluation fixtures under `crates/ty_completion_eval/truth/`. Extend an existing project when it is a good fit for the behavior being tested; otherwise, add a new one. Use `` directives to assert ranking, and include the expected module for auto-import completions. Add `completion.rs` unit tests only when the evaluation fixtures cannot adequately cover the behavior. + +Regenerate and review the committed evaluation results after changing ranking behavior or fixtures: + +```sh +CARGO_PROFILE_DEV_OPT_LEVEL=1 CARGO_PROFILE_DEV_LTO=off CARGO_PROFILE_DEV_DEBUG="line-tables-only" cargo run --package ty_completion_eval -- all --threshold 0.4 --tasks crates/ty_completion_eval/completion-evaluation-tasks.csv +``` + +To inspect one evaluation task, run `cargo run --package ty_completion_eval -- show-one --file-name --index `. + ### Ad hoc reproductions When running ty against a temporary Python reproduction file, create it outside the Ruff checkout (for example, under `/tmp`). A file inside the checkout discovers Ruff's root `pyproject.toml`, whose `requires-python = ">=3.7"` causes ty to infer Python 3.7 as the default Python version. diff --git a/crates/ty_completion_eval/completion-evaluation-tasks.csv b/crates/ty_completion_eval/completion-evaluation-tasks.csv index 320fd6b7e5..04d45b6473 100644 --- a/crates/ty_completion_eval/completion-evaluation-tasks.csv +++ b/crates/ty_completion_eval/completion-evaluation-tasks.csv @@ -19,8 +19,20 @@ import-deprioritizes-sunder,main.py,0,1 import-deprioritizes-type_check_only,main.py,0,1 import-deprioritizes-type_check_only,main.py,1,1 import-deprioritizes-type_check_only,main.py,2,1 -import-deprioritizes-type_check_only,main.py,3,2 -import-deprioritizes-type_check_only,main.py,4,3 +import-deprioritizes-type_check_only,main.py,3,1 +import-deprioritizes-type_check_only,main.py,4,1 +import-deprioritizes-type_check_only,main.py,5,2 +import-deprioritizes-type_check_only,main.py,6,3 +import-deprioritizes-type_check_only,main.py,7,1 +import-deprioritizes-type_check_only,main.py,8,1 +import-deprioritizes-type_check_only,main.py,9,1 +import-deprioritizes-type_check_only,main.py,10,1 +import-deprioritizes-type_check_only,main.py,11,1 +import-deprioritizes-type_check_only,main.py,12,1 +import-deprioritizes-type_check_only,main.py,13,1 +import-deprioritizes-type_check_only,main.py,14,1 +import-deprioritizes-type_check_only,main.py,15,1 +import-deprioritizes-type_check_only,main.py,16,1 import-deprioritizes-type_check_only,main.pyi,0,1 import-deprioritizes-type_check_only,main.pyi,1,1 import-keyword-completion,main.py,0,1 @@ -54,6 +66,9 @@ typing-only-auto-import-ranking,main.py,2,1 typing-only-auto-import-ranking,main.py,3,2 typing-only-auto-import-ranking,main.py,4,1 typing-only-auto-import-ranking,main.py,5,1 +typing-only-auto-import-ranking,main.py,6,1 +typing-only-auto-import-ranking,main.py,7,1 +typing-only-auto-import-ranking,main.py,8,1 typing-only-auto-import-ranking,main.pyi,0,1 typing-only-auto-import-ranking,main.pyi,1,1 typing-only-auto-import-ranking,main.pyi,2,1 diff --git a/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/main.py b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/main.py index 52dd9ee9f8..d305ed7ccd 100644 --- a/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/main.py +++ b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/main.py @@ -1,12 +1,48 @@ +from typing import TYPE_CHECKING + +import private_stub + from module import UniquePrefixA from module import unique_prefix_ +from private_stub import _Al from module import Class Class.meth_ +private_stub._Al # TODO: bound methods don't preserve type-check-only-ness, this is a bug Class().meth_ # TODO: auto-imports don't take type-check-only-ness into account, this is a bug UniquePrefixA + +if TYPE_CHECKING: + from module import UniquePrefixA + from module import unique_prefix_ + from private_stub import _Al + + Class.meth_ + private_stub._Al + + def declared_in_type_checking_block() -> None: + private_stub._Al + + +def function_scope() -> None: + if TYPE_CHECKING: + from private_stub import _Al + + private_stub._Al + + +if not TYPE_CHECKING: + pass +else: + private_stub._Al + + +if TYPE_CHECKING: + pass +else: + from private_stub import _Al diff --git a/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/private_stub.pyi b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/private_stub.pyi new file mode 100644 index 0000000000..0d5750ca18 --- /dev/null +++ b/crates/ty_completion_eval/truth/import-deprioritizes-type_check_only/private_stub.pyi @@ -0,0 +1,4 @@ +from typing import TypeVar + +_Alpha = TypeVar("_Alpha") +_Alzeta = 1 diff --git a/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.py b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.py index d2400e625d..013ee5c43f 100644 --- a/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.py +++ b/crates/ty_completion_eval/truth/typing-only-auto-import-ranking/main.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + # Runtime symbols outrank alternatives from typing-only modules in Python files. deprecated NoneTy @@ -7,3 +9,13 @@ static_ass is_equiv TypedDictFall + +# Typing-only symbols retain their usual ranking inside TYPE_CHECKING blocks. +if TYPE_CHECKING: + deprecated + NoneTy + + +def function_scope() -> None: + if TYPE_CHECKING: + deprecated diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 7ca1ef529e..f9d3b2d4bf 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -1,3 +1,4 @@ +use std::cell::OnceCell; use std::cmp::Ordering; use std::collections::{BinaryHeap, binary_heap}; use ty_python_semantic::ProgramEnvironment; @@ -10,7 +11,7 @@ use ruff_python_ast::find_node::{CoveringNode, covering_node}; use ruff_python_ast::name::{Name, UnqualifiedName}; use ruff_python_ast::str::Quote; use ruff_python_ast::token::{Token, TokenKind, Tokens}; -use ruff_python_ast::{self as ast, AnyNodeRef, PySourceType}; +use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_python_codegen::Stylist; use ruff_python_literal::escape::{Escape, UnicodeEscape}; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -18,7 +19,7 @@ use rustc_hash::FxHashSet; use ty_module_resolver::{ ImportingFile, KnownModule, Module, ModuleName, resolve_real_shadowable_module, }; -use ty_python_core::ProgramFile; +use ty_python_core::{ProgramFile, semantic_index}; use ty_python_semantic::HasType; use ty_python_semantic::types::{SpecialFormType, UnionType}; use ty_python_semantic::{ @@ -525,12 +526,7 @@ impl<'db> CompletionBuilder<'db> { let kind = self .kind .or_else(|| self.ty.and_then(|ty| completion_kind_from_type(db, ty))); - let relevance = Relevance::new( - collection_context, - query, - &self, - program_file.file(db).source_type(db), - ); + let relevance = Relevance::new(db, program_file, collection_context, query, &self); let (label, insert, insert_text_format, command) = if collection_context.should_complete_callable_parentheses(kind) { let label = self.insert.unwrap_or_else(|| self.name.clone()); @@ -838,8 +834,13 @@ impl<'m> Context<'m> { settings: &CompletionSettings, capabilities: CompletionCapabilities, ) -> CollectionContext<'db> { + let type_checking_block = Some(TypeCheckingBlock::at_cursor(self.cursor.range)); + match self.kind { - ContextKind::Keywords(_) | ContextKind::Import(_) => CollectionContext::none(), + ContextKind::Keywords(_) | ContextKind::Import(_) => CollectionContext { + type_checking_block, + ..CollectionContext::none() + }, ContextKind::NonImport(_) => { let env = model.program_environment(); let exception_ty = self.cursor.exception_ty(db, &env); @@ -859,6 +860,7 @@ impl<'m> Context<'m> { CollectionContext { exception_ty, is_raising_exception: exception_ty.is_some(), + type_checking_block, complete_class_parentheses: complete_callable_parentheses && existing_class_bases.is_none() && !self.cursor.suppress_class_parentheses(model), @@ -1630,6 +1632,40 @@ impl UserQuery { } } +#[derive(Clone, Debug)] +struct TypeCheckingBlock { + range: TextRange, + is_inside: OnceCell, +} + +impl TypeCheckingBlock { + fn at_cursor(range: TextRange) -> Self { + Self { + range, + is_inside: OnceCell::new(), + } + } + + fn is_inside<'db>(&self, db: &'db dyn Db, file: ProgramFile<'db>) -> bool { + // Most completions are ranked independently of `TYPE_CHECKING`, so only query the + // semantic index when a typing-only completion needs to know the cursor's context. + *self.is_inside.get_or_init(|| { + let parsed = parsed_module(db, file.python_file(db)).load(db); + let index = semantic_index(db, file); + + covering_node(parsed.syntax().into(), self.range) + .ancestors() + .find_map(|node| { + let ast::AnyNodeRef::StmtIf(statement) = node else { + return None; + }; + index.try_expression_scope_id(statement.test.as_ref()) + }) + .is_some_and(|scope| index.is_in_type_checking_block(scope, self.range)) + }) + } +} + /// Context used to help filter completions when collecting them. #[derive(Clone, Debug, Default)] struct CollectionContext<'db> { @@ -1640,6 +1676,8 @@ struct CollectionContext<'db> { exception_ty: Option>, /// Whether we're in an exception context (`raise` or `except`) or not. is_raising_exception: bool, + /// Whether the cursor is inside a type-checking-only block, if a cursor is available. + type_checking_block: Option, /// Names of base classes that are already specified in the class definition, /// including the class being defined (unless its name was previously bound). /// Used to filter out duplicate and self-referential base class suggestions. @@ -1795,11 +1833,12 @@ impl Relevance { /// /// A smaller rank means the completion should appear higher in the /// results shown to end users. - fn new( - _ctx: &CollectionContext, + fn new<'db>( + db: &'db dyn Db, + program_file: ProgramFile<'db>, + ctx: &CollectionContext, query: &UserQuery, c: &CompletionBuilder, - source_type: PySourceType, ) -> Relevance { Relevance { definitively_usable: if c.is_context_specific { @@ -1836,7 +1875,13 @@ impl Relevance { } else { Sort::Even }, - type_check_only: if c.is_type_check_only && !source_type.is_stub() { + type_check_only: if c.is_type_check_only + && !program_file.file(db).source_type(db).is_stub() + && !ctx + .type_checking_block + .as_ref() + .is_some_and(|block| block.is_inside(db, program_file)) + { Sort::Lower } else { Sort::Even From 282d6f940f07e386170576331f78e2583f1f83b3 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 10 Aug 2026 16:41:45 +0100 Subject: [PATCH 349/390] [ty] Improve ecosystem minimization and reporting skills (#27632) --- .../minimizing-ty-ecosystem-changes/SKILL.md | 66 ++++++++++++++----- .../references/advanced-minimization.md | 4 +- .../summarise-ecosystem-results/SKILL.md | 14 ++-- .../assets/report-template.md | 23 +++++-- .../references/evidence-acquisition.md | 31 +++++++++ .../references/subagent-handoff.md | 13 ++-- 6 files changed, 119 insertions(+), 32 deletions(-) create mode 100644 .agents/skills/summarise-ecosystem-results/references/evidence-acquisition.md diff --git a/.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md b/.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md index a511397bb4..f6baa25367 100644 --- a/.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md +++ b/.agents/skills/minimizing-ty-ecosystem-changes/SKILL.md @@ -7,19 +7,24 @@ description: Use when a user says "minimize this ty ecosystem change", "reproduc ## Invariants -1. Use the exact Ruff revisions, PR config, dependency cutoff, mypy-primer revision, and project Python version from the Actions run. +1. Use the exact Ruff revisions, user-level PR config, dependency cutoff, mypy-primer revision, project Python version, and strictness settings from the Actions run. 2. Reproduce the reported project difference before explaining it or writing a smaller example. 3. Treat copied binaries and config as read-only, and verify every reduction against both binaries. +4. Derive every candidate from the preceding verified candidate; NEVER substitute an independently constructed example. +5. Preserve the underlying trigger, not merely the diagnostic rule, message, or displayed type. Start each investigation from fresh artifacts. Do not trust retained memories, previous minimizations, current upstream project state, or the helper script's default lockfile. ## Collect Exact-Run Metadata -Run the bundled helper with the Actions run ID or URL and every affected mypy-primer project name: +If a primary agent supplied an existing run-metadata manifest, verify that its run ID and attempt match the frozen report and that it contains each assigned project. Reuse the manifest without modifying it. + +Otherwise, run the bundled helper with the Actions run ID or URL, matching attempt, and every affected mypy-primer project name: ```bash scripts/collect_ty_ecosystem_run_metadata.py \ ... \ + --attempt \ --output target/ty-ecosystem-run.json ``` @@ -29,7 +34,7 @@ The current workflow splits compilation into `Build ty (base)` and `Build ty (pr ## Prepare ty -If a primary agent supplied freshly copied base and PR binaries plus the PR ecosystem config, verify the paths exist and reuse them. Do not rebuild, switch Ruff refs, or overwrite the shared artifacts. +If a primary agent supplied freshly copied base and PR profiling binaries plus the PR ecosystem config, preserve their absolute paths as `TY_ECOSYSTEM_BASE_BINARY` and `TY_ECOSYSTEM_PR_BINARY`, verify they exist, and reuse them. Do not rebuild those binaries, switch shared Ruff refs, or overwrite the shared artifacts. An agent may build an exact-revision debug binary on demand to identify an ambiguous internal type, using an isolated worktree if necessary; the profiling binaries remain the behavioral oracle. Otherwise, require a clean working tree, copy `.github/ty-ecosystem.toml` from the PR revision, and build ty on the manifest's merge base and PR revision: @@ -55,7 +60,7 @@ cp target/profiling/ty target/ty-ecosystem-bins/ty-pr ## Reproduce -Create a unique temporary directory for each project. Read its Python version and the pinned mypy-primer revision from the manifest, then bypass the adjacent script lockfile: +Create a unique temporary directory for each project and use its absolute path. Read its Python version and the pinned mypy-primer revision from the manifest. Obtain the project revision from the `/blob//` component of the original diagnostic's source permalink, and check that links for the same project agree. If no diagnostic permalink exists, inspect the matching diagnostics shard or Actions logs; if the exact revision cannot be recovered, explicitly report that limitation. Then bypass the adjacent script lockfile: ```bash uv run \ @@ -63,32 +68,63 @@ uv run \ --with "mypy-primer @ git+https://github.com/hauntsaninja/mypy_primer@" \ --no-project \ python scripts/setup_primer_project.py \ - \ + \ --revision \ --exclude-newer ``` -Use absolute paths and re-export `TY_CONFIG_FILE` in every new shell before running either binary: +Use the ecosystem config as user-level configuration, matching CI without replacing project-level config discovery, and re-export `XDG_CONFIG_HOME` in each new shell. If a primary agent supplied `TY_ECOSYSTEM_CONFIG_HOME`, reuse its installed config without modifying it; otherwise, install the copied config locally. Read the project's `strict` or `non-strict` label from the frozen detailed report, or its `strict_settings` value from the matching diagnostics shard. Preserve that mode when running either binary: ```bash -export TY_CONFIG_FILE="$PWD/target/ty-ecosystem-bins/ty-ecosystem.toml" -test -f "$TY_CONFIG_FILE" -project_dir="$PWD/" -ty_base="$PWD/target/ty-ecosystem-bins/ty-base" -ty_pr="$PWD/target/ty-ecosystem-bins/ty-pr" +if [[ -n "${TY_ECOSYSTEM_CONFIG_HOME:-}" ]]; then + export XDG_CONFIG_HOME="$TY_ECOSYSTEM_CONFIG_HOME" + test -f "$XDG_CONFIG_HOME/ty/ty.toml" || exit 1 +else + export XDG_CONFIG_HOME="$PWD/target/ty-ecosystem-config" + mkdir -p "$XDG_CONFIG_HOME/ty" + cp "$PWD/target/ty-ecosystem-bins/ty-ecosystem.toml" "$XDG_CONFIG_HOME/ty/ty.toml" +fi +unset TY_CONFIG_FILE + +project_dir="" +ty_base="${TY_ECOSYSTEM_BASE_BINARY:-$PWD/target/ty-ecosystem-bins/ty-base}" +ty_pr="${TY_ECOSYSTEM_PR_BINARY:-$PWD/target/ty-ecosystem-bins/ty-pr}" +test -x "$ty_base" && test -x "$ty_pr" || exit 1 +ecosystem_analysis_mode="" + +if [[ "$ecosystem_analysis_mode" != strict && "$ecosystem_analysis_mode" != non-strict ]]; then + echo "Unknown ecosystem analysis mode: $ecosystem_analysis_mode" >&2 + exit 1 +fi + +run_ecosystem_ty() { + if [[ "$ecosystem_analysis_mode" == strict ]]; then + \ + --config analysis.strict-equality-semantics=true \ + --config analysis.strict-generic-narrowing=true + else + + fi +} cd "$project_dir" ty_binary="$ty_base" - +base_exit_status=0 +run_ecosystem_ty || base_exit_status=$? ty_binary="$ty_pr" - +pr_exit_status=0 +run_ecosystem_ty || pr_exit_status=$? ``` -Confirm the detailed report's difference exactly, including duplicate diagnostics when present. +Confirm the detailed report's difference exactly, including duplicate diagnostics and both exit statuses. Ordinary diagnostics can produce exit status 1; do not mistake that for a failed reproduction. ## Minimize -Reduce the reproduced project iteratively, using the base-versus-PR output as the oracle after every change. Prefer a single file, no third-party imports, and the least complex code that preserves the difference. For nontrivial reductions, follow [references/advanced-minimization.md](references/advanced-minimization.md). +Reduce the reproduced project toward a self-contained single-file reproducer with minimal code and dependencies. A reduction is trivial only when the difference already occurs in one self-contained file and can be preserved solely by deleting obviously unrelated code. Multiple files, imports or dependencies, inlining, replacing language constructs, ambiguous types such as `@Todo`, or an uncertain cause make a reduction nontrivial. Before attempting any nontrivial reduction, read and follow [references/advanced-minimization.md](references/advanced-minimization.md). If in doubt, treat the reduction as nontrivial. + +Matching diagnostics or displayed types do not establish a shared cause. When the output is ambiguous, identify the original and minimized triggers using exact-revision debug output, a targeted `reveal_type`, or the producing Rust call site. + +Record the original source permalink, accepted reductions, both binaries' results, and any causal fingerprint. If source provenance or a matching cause cannot be established, return the original project excerpt explicitly marked as unminimized. ## Return diff --git a/.agents/skills/minimizing-ty-ecosystem-changes/references/advanced-minimization.md b/.agents/skills/minimizing-ty-ecosystem-changes/references/advanced-minimization.md index 49c7de0654..056b8f0c45 100644 --- a/.agents/skills/minimizing-ty-ecosystem-changes/references/advanced-minimization.md +++ b/.agents/skills/minimizing-ty-ecosystem-changes/references/advanced-minimization.md @@ -8,7 +8,7 @@ Prefer a single-file reproducer with no third-party imports, few definitions, an ## Reduction Loop -Work systematically from the reproduced project. Do not skip ahead to an explanation, hand-written reproducer, or a guessed subset of relevant code. Follow the stages below in order and exhaust each stage before advancing. Try one controlled reduction at a time, run both copied ty binaries after every change, and keep the reduction only if the original difference remains. After every successful reduction, restart at step 1 because it may make earlier reductions possible. +Work systematically from the reproduced project. NEVER skip ahead to an explanation, hand-written reproducer, or a guessed subset of relevant code. Follow the stages below in order and exhaust each stage before advancing. Try one controlled reduction at a time, run both copied ty binaries after every change, and keep the reduction only if the original difference and underlying trigger remain. After every successful reduction, restart at step 1 because it may make earlier reductions possible. 1. Delete unrelated files. 2. Remove imports, definitions, decorators, annotations, statements, and branches. @@ -23,4 +23,6 @@ Repeat the full loop until an exhaustive pass through every stage finds no furth Attempt to remove every remaining import and inline every remaining third-party definition. Record why any surviving import is essential. Keep these notes as working evidence; the caller decides whether they belong in its final artifact. +Verify that the recorded reduction chain connects the final reproducer to the original ecosystem entry and, when diagnostic output is ambiguous, preserves the original causal fingerprint. If either check fails, return the original project excerpt as unminimized instead of substituting an unrelated example. + Delete transient project and dependency copies after the investigation. diff --git a/.agents/skills/summarise-ecosystem-results/SKILL.md b/.agents/skills/summarise-ecosystem-results/SKILL.md index 1ad7e61dfd..1b258a151b 100644 --- a/.agents/skills/summarise-ecosystem-results/SKILL.md +++ b/.agents/skills/summarise-ecosystem-results/SKILL.md @@ -8,7 +8,7 @@ description: Use when a user says "summarise ecosystem results", "summarize this ## Priorities 1. Reproduce every retained behavior with the exact environment used by the Actions run. -2. Lead the report with analysis of diagnostic changes and clear minimized examples. +2. Lead the report with new or changed project failures, then cover meaningful flaky behavior, diagnostic changes, and clear minimized examples. 3. Keep execution, audit, and traceability bookkeeping out of the report. ## Deliverable @@ -21,9 +21,11 @@ If summarising an ecosystem report is the only thing you're asked to do in a Cod ## Workflow -1. **Locate the evidence.** Normalize the input to a PR number, find the ty ecosystem-results comment, open the linked detailed HTML report, and identify the exact Actions run that produced it. Use the comment as the change list and the detailed report as evidence. -2. **Reproduce from scratch.** Ignore retained memories and previous local artifacts. Load the `minimizing-ty-ecosystem-changes` skill, use its metadata helper and exact-run workflow, and reproduce each report entry before explaining or minimizing it. -3. **Minimize and curate.** Retain the smallest clear reproducer for each distinct behavior change. Group entries only when the same base-to-PR behavior, explanation, and reproducer account for every entry in the group. -4. **Write and verify.** Fill the report template, check every link and diagnostic, then run `uvx prek run --files PR__ECOSYSTEM_SUMMARY.md`. Present the Markdown file as the finished product. +1. **Freeze the evidence.** Preserve any report URL or ecosystem-results comment explicitly supplied by the user before identifying the PR. For PR-only input, find its ecosystem-results comment and linked detailed report. Capture the matching Actions run and attempt as described in [references/evidence-acquisition.md](references/evidence-acquisition.md); never replace a supplied report with the PR's current report. Ignore later comment edits, PR updates, and workflow runs. Use the frozen detailed report as the authoritative change list and the comment for orientation when available. +2. **Identify changed outcomes.** Check the detailed report for new, fixed, or changed project failures, panics, timeouts, abnormal exits, and meaningful flaky diagnostic or exit-status changes. Omit unchanged persistent failures. If neither project outcomes nor diagnostics changed, say explicitly that the run had no ecosystem impact and omit project-specific sections and reproduction details. +3. **Reproduce from scratch.** Ignore retained memories and previous local artifacts. Load the `minimizing-ty-ecosystem-changes` skill, use its metadata helper and exact-run workflow, and reproduce each report entry before explaining or minimizing it. Reproduce flaky behavior with the reported run counts. +4. **Minimize with provenance.** Include a standalone reproducer only when a verified reduction chain connects it to a cited ecosystem entry and preserves the same underlying trigger. If either cannot be verified, retain the original source excerpt and identify it as unminimized. +5. **Group by cause.** Group entries only when the same base-to-PR behavior, underlying trigger, explanation, and reproducer account for every entry. Identical diagnostic text or displayed `@Todo` types do not establish equivalence. +6. **Write and verify.** Fill the report template, record each affected project's strict or non-strict analysis mode, and include both strict-analysis flags in the comparison method when applicable. Check every link, diagnostic, reproducer's source provenance, and causal fingerprint when required, then run `uv run --only-group dev --locked prek run --files PR__ECOSYSTEM_SUMMARY.md`. Present the Markdown file as the finished product. -When parallelizing step 2, read [references/subagent-handoff.md](references/subagent-handoff.md). Otherwise, keep batches small and work through them sequentially. +When parallelizing reproduction or minimization, read [references/subagent-handoff.md](references/subagent-handoff.md). Otherwise, keep batches small and work through them sequentially. diff --git a/.agents/skills/summarise-ecosystem-results/assets/report-template.md b/.agents/skills/summarise-ecosystem-results/assets/report-template.md index 6a4d478d2c..c77654181a 100644 --- a/.agents/skills/summarise-ecosystem-results/assets/report-template.md +++ b/.agents/skills/summarise-ecosystem-results/assets/report-template.md @@ -1,8 +1,20 @@ - + # [PR #](https://github.com/astral-sh/ruff/pull/) ecosystem summary - + + + + +## + +**Affected projects:** + +- [](): merge base: ``; PR: ``. + + + + ## @@ -40,10 +52,11 @@ if x: ## Reproduction - Detailed report: [ecosystem-analyzer report]() -- Actions run: [run ]() +- Actions run: [run , attempt ]() - Ruff comparison: [``](https://github.com/astral-sh/ruff/commit/) to [``](https://github.com/astral-sh/ruff/commit/) - `ecosystem-analyzer`: [``](https://github.com/astral-sh/ecosystem-analyzer/commit/) - `mypy-primer`: [``](https://github.com/hauntsaninja/mypy_primer/commit/) -- Project Python: `` - Dependency cutoff: `` -- Comparison method: `` +- Project Python: `` +- Project analysis mode: `` +- Comparison method: `` diff --git a/.agents/skills/summarise-ecosystem-results/references/evidence-acquisition.md b/.agents/skills/summarise-ecosystem-results/references/evidence-acquisition.md new file mode 100644 index 0000000000..4c05457817 --- /dev/null +++ b/.agents/skills/summarise-ecosystem-results/references/evidence-acquisition.md @@ -0,0 +1,31 @@ +# Freeze Ecosystem Evidence + +At the beginning of the summary request, preserve any detailed report or ecosystem-results comment explicitly supplied by the user. Only look up the PR's current comment when the user provided no specific report or comment. Save the selected deployed report immediately when it is accessible, then identify its matching PR, Actions run, and attempt; never substitute a newer comment, report, PR revision, workflow run, or reporting attempt. + +If no comment matching an explicitly supplied report remains, continue with the supplied report and record that the matching comment is unavailable. If the exact Actions run or attempt cannot be identified uniquely, report that uncertainty instead of selecting the current PR report or guessing from matching Ruff revisions. + +Create a unique snapshot directory, save the matching comment when available and the selected attempt's effective job graph, and inspect the run's available artifacts: + +```bash +snapshot_dir="$(mktemp -d "${TMPDIR:-/tmp}/ty-ecosystem-report.XXXXXX")" +ecosystem_comment_id="" +if [[ -n "$ecosystem_comment_id" ]]; then + gh api "repos/astral-sh/ruff/issues/comments/$ecosystem_comment_id" > "$snapshot_dir/comment.json" +fi +gh run view --repo astral-sh/ruff --attempt \ + --json attempt,headSha,jobs,startedAt,updatedAt,url > "$snapshot_dir/run.json" +gh api "repos/astral-sh/ruff/actions/runs//artifacts" > "$snapshot_dir/artifacts.json" +``` + +`gh run download` cannot select an attempt, and a newer rerun can replace an older attempt's artifacts without changing Ruff's revisions. Before downloading, verify that `full-report` was created during the selected attempt's report-generation job and that each diagnostics shard was created during its matching successful shard job. Use the effective job graph, not the attempt start time: partial reruns legitimately inherit successful jobs and artifacts from earlier attempts. + +Download only artifacts that pass these checks; use the shard glob only when every matching artifact belongs to the selected job graph: + +```bash +gh run download --repo astral-sh/ruff --name full-report --dir "$snapshot_dir/full-report" +gh run download --repo astral-sh/ruff --pattern 'diagnostics-shard-*' --dir "$snapshot_dir/shards" +``` + +Record the selected Actions attempt and pass it to `scripts/collect_ty_ecosystem_run_metadata.py` with `--attempt `. Verify that the frozen report's Ruff base and PR revisions agree with the resulting manifest, then use the saved report, shards, run, attempt, and matching comment when available throughout the investigation. + +If the selected report's artifacts were replaced or are unavailable, use its frozen deployed report and explicitly describe any unavailable shards or resulting verification limitations. Never silently substitute artifacts produced by an unrelated reporting attempt. diff --git a/.agents/skills/summarise-ecosystem-results/references/subagent-handoff.md b/.agents/skills/summarise-ecosystem-results/references/subagent-handoff.md index de9d946718..78f2bf6565 100644 --- a/.agents/skills/summarise-ecosystem-results/references/subagent-handoff.md +++ b/.agents/skills/summarise-ecosystem-results/references/subagent-handoff.md @@ -4,23 +4,26 @@ Use this reference only when parallelizing reproduction and minimization. ## Primary-Agent Responsibilities -Prepare the copied base binary, PR binary, PR ecosystem config, and run-metadata manifest once. Treat them as read-only shared inputs. Batch related entries without creating more assignments than can run concurrently. +Prepare the frozen evidence snapshot, copied base binary, PR binary, PR ecosystem config, and one run-metadata manifest covering every affected project. Record the binaries' absolute paths as `TY_ECOSYSTEM_BASE_BINARY` and `TY_ECOSYSTEM_PR_BINARY`. Choose an absolute `TY_ECOSYSTEM_CONFIG_HOME` and install the copied config once at `$TY_ECOSYSTEM_CONFIG_HOME/ty/ty.toml`. Treat the snapshot, binaries, manifest, copied config, and installed configuration as read-only shared inputs. Batch related entries without creating more assignments than can run concurrently. ## Assignment Checklist Give each subagent: -- The PR, ecosystem comment, and detailed report links. +- The PR and detailed report links, plus the ecosystem comment link when available. +- The paths to the frozen detailed report and available diagnostics shards, plus the frozen comment path when available and the selected Actions run and attempt; use these captured inputs instead of refetching live evidence. - The exact report entries assigned to it. -- The paths to the copied binaries, copied config, and metadata manifest. +- The copied-config and metadata-manifest paths, plus the shared `TY_ECOSYSTEM_BASE_BINARY`, `TY_ECOSYSTEM_PR_BINARY`, and `TY_ECOSYSTEM_CONFIG_HOME` values. - The instruction to follow the `minimizing-ty-ecosystem-changes` skill using a unique temporary directory. -- The instruction not to rebuild ty, switch Ruff refs, overwrite shared artifacts, trust previous local reproductions, or substitute current dependency metadata. +- The instruction to preserve a verified reduction chain and underlying trigger, or return the original source explicitly marked as unminimized. +- Permission to build an exact-revision debug binary on demand for causal inspection, using an isolated worktree if necessary and retaining the profiling binaries as the behavioral oracle. +- The instruction not to rebuild profiling binaries, regenerate the supplied manifest, rewrite the installed configuration, switch shared Ruff refs, overwrite shared artifacts, trust previous local reproductions, or substitute current dependency metadata. ## Required Return Request: - Report-ready GitHub-flavored Markdown describing the exact base-versus-PR behavior and minimized code. -- Separate working notes covering reproduction, reductions, and the import audit. +- Separate working notes covering the original source permalink, reproduction, accepted reductions, both binaries' results, any necessary causal fingerprint, and the import audit. If a later entry has exactly the same behavior change and cause as an already minimized entry, the subagent may classify it as a duplicate instead of repeating the full minimization, but it must explain the match. From 60d22077812eabaaa3cf6df710800fc9c2a243fa Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 10 Aug 2026 11:56:16 -0400 Subject: [PATCH 350/390] Enable PGO for Linux x86-64 Ruff releases (#27570) ## Summary This PR enables PGO for Ruff releases, starting with Linux x86-64. ### Design The release pipeline is modified as follows: - We build an instrumented, stripped release binary. - We run `check` and `format` on a corpus of projects from our ecosystem reports -- specifically, eight of the pinned ecosystem projects that we use in the ty CI. (In total, it's 502 Python and stub files.) - We merge the profiles via `llvm-profdata`. - We feed the result back into the existing `maturin` build. ### Results We evaluate performance on a held-out corpus: Prefect, Django, Pandas, scikit-learn, SciPy, and SymPy. Results are as follows: | Held-out project | `ruff check` | `ruff format` | | --- | ---: | ---: | | Django | 10.9% faster | 6.6% faster | | pandas | 16.3% faster | 9.7% faster | | scikit-learn | 10.8% faster | 7.9% faster | | SciPy | 14.7% faster | 5.6% faster | | SymPy | 17.1% faster | 11.4% faster | | Geometric mean | **14.0% faster** | **8.3% faster** | Beyond runtime: - **Binary size decreased by 6.2%** (27.96 MB to 26.23 MB). - **Release pipeline gets about 2x longer** (non-PGO release build took 7m22s; PGO pipeline took 15m14s (8m35s instrumented training plus 6m39s optimized wheel). ### Stack Additional platforms are covered in subsequent PRs in the stacked; platforms that are lower-priority at at-all difficult to run on natively are omitted. In the end, I'm targeting Linux x86-64, Linux ARM, macOS ARM, and Windows x86-64. I also attempted BOLT in https://github.com/astral-sh/ruff/pull/27588, but I've decided against pursuing that for now; see the results in that PR which speak for themselves. ty and uv will follow the same approach; see the stacks here: - https://github.com/astral-sh/ty/pull/4213 - https://github.com/astral-sh/uv/pull/21001 See: https://github.com/astral-sh/ruff/issues/7055. --- .github/workflows/build-binaries.yml | 13 + scripts/build_ruff_pgo.py | 554 +++++++++++++++++++++++++++ 2 files changed, 567 insertions(+) create mode 100644 scripts/build_ruff_pgo.py diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 8ecb1fa0cf..ce537be022 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -18,6 +18,7 @@ on: - pyproject.toml # And when we change this workflow itself... - .github/workflows/build-binaries.yml + - scripts/build_ruff_pgo.py concurrency: group: build-binaries-${{ github.ref }} @@ -239,6 +240,18 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 + - name: "Install LLVM profiling tools" + if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} + run: rustup component add llvm-tools-preview + - name: "Train PGO Ruff" + if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} + run: | + python scripts/build_ruff_pgo.py \ + --target "${{ matrix.target }}" \ + --target-dir "${{ github.workspace }}/target/ruff-pgo" \ + --train-only + + echo "RUSTFLAGS=${RUSTFLAGS:+${RUSTFLAGS} }-Cprofile-use=${{ github.workspace }}/target/ruff-pgo/ruff.profdata" >> "$GITHUB_ENV" - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" diff --git a/scripts/build_ruff_pgo.py b/scripts/build_ruff_pgo.py new file mode 100644 index 0000000000..7966e6257c --- /dev/null +++ b/scripts/build_ruff_pgo.py @@ -0,0 +1,554 @@ +"""Build Ruff with profile-guided optimization using pinned ecosystem projects.""" + +# /// script +# requires-python = ">=3.11" +# dependencies = [] +# /// + +from __future__ import annotations + +import argparse +import os +import re +import shlex +import subprocess +import sys +import tempfile +import time +from dataclasses import dataclass +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parent.parent +EXCLUDED_DIRECTORIES = frozenset({"_tests", "_vendor", "test", "tests"}) + + +@dataclass(frozen=True, slots=True) +class EcosystemProject: + name: str + repository: str + revision: str + source_directories: tuple[str, ...] + + def __post_init__(self) -> None: + if re.fullmatch(r"[0-9a-f]{40}", self.revision) is None: + raise ValueError( + f"{self.repository} must be pinned to a full Git commit SHA, " + f"got {self.revision!r}" + ) + + @property + def url(self) -> str: + return f"https://github.com/{self.repository}.git" + + +# Train on a subset of the pinned ecosystem projects that we already use for +# linting, formatting, or type checking. The goal is to create a representative +# corpus that includes scientific computing, synchronous and asynchronous code, +# applications, libraries, and type stubs. +# +# But it wasn't a highly optimized selection process. (For example, during +# development, we added Zulip and Warehouse, which reduced Ruff's CPU time by +# 0.35% while increasing its wheel size by 0.44%.) +CORPUS_PROJECTS = ( + EcosystemProject( + name="pytest", + repository="pytest-dev/pytest", + revision="28e86a6c2ae0173831e4925a4af89b02a2936d09", + source_directories=("src/_pytest",), + ), + EcosystemProject( + name="httpx", + repository="encode/httpx", + revision="b5addb64f0161ff6bfe94c124ef76f6a1fba5254", + source_directories=("httpx",), + ), + EcosystemProject( + name="fastapi", + repository="fastapi/fastapi", + revision="a375f6b948b99fa4260129856bbf11d037f363ef", + source_directories=("fastapi",), + ), + EcosystemProject( + name="anyio", + repository="agronholm/anyio", + revision="ffe91331adb912c5d150f5d373f7cd28a0e96a62", + source_directories=("src/anyio",), + ), + EcosystemProject( + name="zulip", + repository="zulip/zulip", + revision="ccddbba7a3074283ccaac3bde35fd32b19faf042", + source_directories=("zerver/views", "zerver/models"), + ), + EcosystemProject( + name="warehouse", + repository="pypi/warehouse", + revision="5a4d2cadec641b5d6a6847d0127940e0f532f184", + source_directories=( + "warehouse/accounts", + "warehouse/oidc", + "warehouse/forklift", + ), + ), + EcosystemProject( + name="pip", + repository="pypa/pip", + revision="d1fd55753405fd728a0751a578e27c1054acdf48", + source_directories=("src/pip/_internal",), + ), + EcosystemProject( + name="sphinx", + repository="sphinx-doc/sphinx", + revision="b06d92e80eed130e1dd4e67cac4afa1267424f1a", + source_directories=( + "sphinx/builders", + "sphinx/ext/autodoc", + "sphinx/domains/python", + ), + ), + EcosystemProject( + name="astropy", + repository="astropy/astropy", + revision="b779108c7cec25c840c0f744fdf2a1550441e309", + source_directories=("astropy/units",), + ), + EcosystemProject( + name="typeshed", + repository="python/typeshed", + revision="e0efbeef901e9b6998d016e1ab9352678f09ae77", + source_directories=( + "stdlib/asyncio", + "stdlib/collections", + "stubs/requests", + ), + ), +) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target", help="Host-native Rust target triple") + parser.add_argument( + "--target-dir", + type=Path, + help="Cargo target directory (default: CARGO_TARGET_DIR or target/ruff-pgo)", + ) + parser.add_argument( + "--profile-dir", + type=Path, + help="Raw profile directory (default: /profiles)", + ) + parser.add_argument( + "--llvm-profdata", + type=Path, + help="Override the active Rust toolchain's llvm-profdata executable", + ) + mode = parser.add_mutually_exclusive_group() + mode.add_argument( + "--train-only", + action="store_true", + help="Only produce /ruff.profdata for a subsequent release build", + ) + mode.add_argument( + "--prepare-corpus", + action="store_true", + help="Only download and prepare the pinned ecosystem training corpus", + ) + args = parser.parse_args() + + target_dir = ( + args.target_dir + or Path( + os.environ.get("CARGO_TARGET_DIR", REPOSITORY_ROOT / "target" / "ruff-pgo") + ) + ).resolve() + profile_dir = (args.profile_dir or target_dir / "profiles").resolve() + merged_profile = target_dir / "ruff.profdata" + + environment = os.environ.copy() + if args.prepare_corpus: + corpus = ecosystem_python_files(target_dir / "corpus", environment=environment) + write_corpus_arguments(target_dir, corpus) + print(f"Prepared {len(corpus)} ecosystem Python files", flush=True) + return + + host = rustc_host() + target = args.target or host + if target != host: + parser.error( + f"PGO training requires the host-native target {host}, got {target}" + ) + + profiler = find_llvm_profdata(host, args.llvm_profdata) + corpus = ecosystem_python_files(target_dir / "corpus", environment=environment) + corpus_arguments = write_corpus_arguments(target_dir, corpus) + + profile_dir.mkdir(parents=True, exist_ok=True) + for profile in profile_dir.glob("ruff-*.profraw"): + profile.unlink() + + environment["CARGO_INCREMENTAL"] = "0" + if target.endswith("-apple-darwin"): + for variable in ("CFLAGS", "CXXFLAGS"): + environment[variable] = append_flags( + environment.get(variable), "-fno-profile-generate -fno-profile-use" + ) + + instrumented_target_dir = target_dir / "instrumented" + instrumented_environment = environment | { + "CARGO_TARGET_DIR": str(instrumented_target_dir), + "RUSTFLAGS": append_flags( + environment.get("RUSTFLAGS"), f"-Cprofile-generate={profile_dir}" + ), + } + print("Building instrumented release Ruff", flush=True) + run(cargo_command(target), environment=instrumented_environment) + + binary_name = "ruff.exe" if "windows" in target else "ruff" + instrumented_binary = instrumented_target_dir / target / "release" / binary_name + if not instrumented_binary.is_file(): + raise RuntimeError(f"Instrumented Ruff binary not found: {instrumented_binary}") + + profiles = train_ruff( + instrumented_binary, + corpus_arguments, + profile_dir, + corpus_size=len(corpus), + environment=instrumented_environment, + ) + merge_profiles(profiler, profiles, merged_profile, environment=environment) + + if args.train_only: + return + + optimized_environment = environment | { + "CARGO_TARGET_DIR": str(target_dir), + "RUSTFLAGS": append_flags( + environment.get("RUSTFLAGS"), f"-Cprofile-use={merged_profile}" + ), + } + print("Building optimized release Ruff", flush=True) + run(cargo_command(target), environment=optimized_environment) + print( + f"Optimized Ruff: {target_dir / target / 'release' / binary_name}", flush=True + ) + + +def train_ruff( + binary: Path, + corpus_arguments: Path, + profile_directory: Path, + *, + corpus_size: int, + environment: dict[str, str], +) -> list[Path]: + common_arguments = [ + "--isolated", + "--target-version", + "py314", + "--no-cache", + "--silent", + ] + workloads = ( + ("check", "--exit-zero", (0,)), + ("format", "--check", (0, 1)), + ) + print(f"Training on {corpus_size} ecosystem Python files", flush=True) + profiles = [] + + for mode, mode_argument, allowed_exit_codes in workloads: + run( + [ + str(binary), + mode, + *common_arguments, + mode_argument, + f"@{corpus_arguments}", + ], + environment=environment + | { + "LLVM_PROFILE_FILE": str( + profile_directory / f"ruff-{mode}-%m-%p.profraw" + ) + }, + allowed_exit_codes=allowed_exit_codes, + ) + + workload_profiles = sorted(profile_directory.glob(f"ruff-{mode}-*.profraw")) + if not workload_profiles or any( + profile.stat().st_size == 0 for profile in workload_profiles + ): + raise RuntimeError( + f"No complete Ruff {mode} profiling data found in {profile_directory}" + ) + profiles.extend(workload_profiles) + + return profiles + + +def merge_profiles( + profiler: Path, + profiles: list[Path], + destination: Path, + *, + environment: dict[str, str], +) -> None: + profile_size = sum(profile.stat().st_size for profile in profiles) + + with tempfile.NamedTemporaryFile( + dir=destination.parent, prefix="ruff-", suffix=".profdata", delete=False + ) as temporary_file: + temporary_profile = Path(temporary_file.name) + try: + run( + [ + str(profiler), + "merge", + "--output", + str(temporary_profile), + *map(str, profiles), + ], + environment=environment, + ) + temporary_profile.replace(destination) + finally: + temporary_profile.unlink(missing_ok=True) + print( + f"Merged {len(profiles)} PGO profiles ({profile_size:,} bytes): {destination}", + flush=True, + ) + + +def rustc_host() -> str: + version = subprocess.run( + ["rustc", "--version", "--verbose"], + cwd=REPOSITORY_ROOT, + check=True, + capture_output=True, + text=True, + ).stdout + for line in version.splitlines(): + if line.startswith("host: "): + return line.removeprefix("host: ") + raise RuntimeError("Could not determine the active Rust compiler's host target") + + +def find_llvm_profdata(host: str, override: Path | None) -> Path: + if override is not None: + profiler = override.resolve() + else: + sysroot = subprocess.run( + ["rustc", "--print", "sysroot"], + cwd=REPOSITORY_ROOT, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + binary_name = "llvm-profdata.exe" if "windows" in host else "llvm-profdata" + profiler = Path(sysroot) / "lib" / "rustlib" / host / "bin" / binary_name + + if not profiler.is_file() or not os.access(profiler, os.X_OK): + raise RuntimeError( + f"Rust toolchain llvm-profdata not found: {profiler}; " + "run `rustup component add llvm-tools-preview`" + ) + return profiler + + +def ecosystem_python_files( + corpus_directory: Path, *, environment: dict[str, str] +) -> list[str]: + corpus_directory.mkdir(parents=True, exist_ok=True) + git_environment = environment | { + "GIT_CONFIG_GLOBAL": os.devnull, + "GIT_TERMINAL_PROMPT": "0", + "GIT_LFS_SKIP_SMUDGE": "1", + } + paths: list[str] = [] + + for project in CORPUS_PROJECTS: + checkout = corpus_directory / project.name + checkout.mkdir(parents=True, exist_ok=True) + git = ["git", "-c", f"core.hooksPath={os.devnull}", "-C", str(checkout)] + + if not (checkout / ".git").is_dir(): + print(f"Preparing {project.repository}@{project.revision}", flush=True) + run([*git, "init", "--quiet"], environment=git_environment) + run( + [ + *git, + "remote", + "add", + "origin", + project.url, + ], + environment=git_environment, + ) + + remote = subprocess.run( + [*git, "config", "--local", "--get", "remote.origin.url"], + cwd=REPOSITORY_ROOT, + env=git_environment, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + if remote != project.url: + raise RuntimeError( + f"Unexpected origin for cached {project.name} checkout: " + f"expected {project.url}, got {remote}" + ) + + run( + [*git, "sparse-checkout", "set", "--cone", *project.source_directories], + environment=git_environment, + ) + + current_revision = subprocess.run( + [*git, "rev-parse", "--verify", "HEAD"], + cwd=REPOSITORY_ROOT, + env=git_environment, + check=False, + capture_output=True, + text=True, + ) + if ( + current_revision.returncode != 0 + or current_revision.stdout.strip() != project.revision + ): + run_git_with_retry( + [ + *git, + "fetch", + "--quiet", + "--no-tags", + "--no-recurse-submodules", + "--depth=1", + "--filter=blob:none", + "origin", + project.revision, + ], + environment=git_environment, + ) + + run_git_with_retry( + [ + *git, + "checkout", + "--quiet", + "--detach", + "--force", + "--no-recurse-submodules", + project.revision, + ], + environment=git_environment, + ) + + for source_directory in project.source_directories: + source = checkout / source_directory + if not source.is_dir(): + raise RuntimeError( + f"Missing training source directory {source_directory!r} " + f"in {project.repository}@{project.revision}" + ) + + tracked_files = subprocess.run( + [*git, "ls-files", "-z", "--", *project.source_directories], + cwd=REPOSITORY_ROOT, + env=git_environment, + check=True, + capture_output=True, + ).stdout.split(b"\0") + project_paths = [ + str(path) + for tracked_file in tracked_files + if tracked_file + and (path := checkout / os.fsdecode(tracked_file)).suffix in {".py", ".pyi"} + and path.is_file() + and not path.is_symlink() + and not EXCLUDED_DIRECTORIES.intersection( + path.relative_to(checkout).parts[:-1] + ) + ] + + if not project_paths: + raise RuntimeError( + f"No Python training files found in {project.repository}" + ) + paths.extend(sorted(project_paths)) + print(f" {project.name}: {len(project_paths)} Python files", flush=True) + + return paths + + +def run_git_with_retry(command: list[str], *, environment: dict[str, str]) -> None: + for attempt in range(3): + try: + run(command, environment=environment) + return + except subprocess.CalledProcessError: + if attempt == 2: + raise + delay = 2**attempt + print( + f"Git command failed; retrying in {delay}s (attempt {attempt + 2} of 3)", + file=sys.stderr, + flush=True, + ) + time.sleep(delay) + + +def write_corpus_arguments(target_directory: Path, corpus: list[str]) -> Path: + arguments = target_directory / "ruff-pgo.args" + arguments.write_text("\n".join(corpus) + "\n", encoding="utf-8", newline="\n") + return arguments + + +def cargo_command(target: str) -> list[str]: + return [ + "cargo", + "rustc", + "--release", + "--locked", + "--package", + "ruff", + "--bin", + "ruff", + "--target", + target, + "--", + "-C", + "strip=symbols", + ] + + +def append_flags(existing: str | None, additional: str) -> str: + return " ".join(flag for flag in (existing, additional) if flag) + + +def run( + command: list[str], + *, + environment: dict[str, str], + allowed_exit_codes: tuple[int, ...] = (0,), +) -> None: + logged_arguments = 16 + displayed_command = shlex.join(command[:logged_arguments]) + if len(command) > logged_arguments: + displayed_command += ( + f" ... ({len(command) - logged_arguments} arguments omitted)" + ) + print(f"> {displayed_command}", flush=True) + completed = subprocess.run( + command, cwd=REPOSITORY_ROOT, env=environment, check=False + ) + if completed.returncode not in allowed_exit_codes: + raise subprocess.CalledProcessError(completed.returncode, command) + + +if __name__ == "__main__": + try: + main() + except (OSError, RuntimeError, subprocess.CalledProcessError) as error: + print(f"error: {error}", file=sys.stderr) + raise SystemExit(1) from error From efccf4901022992309d1be43efbaa1a51f19a2e8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 10 Aug 2026 11:56:16 -0400 Subject: [PATCH 351/390] Enable PGO for macOS ARM64 Ruff releases (#27572) ## Summary This PR enables PGO for Ruff's macOS ARM64 releases, following the approach outlined in https://github.com/astral-sh/ruff/pull/27570. For macOS, `ruff check` gets 8.3% faster on the holdout set, `ruff format` gets 5.4% faster, and the release binary gets 1.1% smaller. --- .github/workflows/build-binaries.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index ce537be022..6e1d1111f2 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -116,6 +116,10 @@ jobs: macos-aarch64: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: ${{ github.repository == 'astral-sh/ruff' && 'namespace-profile-macos-15' || 'macos-15' }} + env: + # Use Rust's bundled Mach-O LLD, which supports ICF. + # ICF reduces the macOS aarch64 ruff binary size by ~0.8%. + RUSTFLAGS: "-C linker=rust-lld -C linker-flavor=ld64.lld -C link-arg=--icf=safe" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -125,6 +129,14 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: arm64 + - name: "Install LLVM profiling tools" + run: rustup component add llvm-tools-preview + - name: "Train PGO Ruff" + run: | + python scripts/build_ruff_pgo.py \ + --target aarch64-apple-darwin \ + --target-dir "${{ github.workspace }}/target/ruff-pgo" \ + --train-only - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels - aarch64" @@ -134,9 +146,10 @@ jobs: target: aarch64 args: --release --locked --out dist --compatibility pypi env: - # Use Rust's bundled Mach-O LLD, which supports ICF. - # ICF reduces the macOS aarch64 ruff binary size by ~0.8%. - RUSTFLAGS: "-C linker=rust-lld -C linker-flavor=ld64.lld -C link-arg=--icf=safe" + RUSTFLAGS: "${{ env.RUSTFLAGS }} -Cprofile-use=${{ github.workspace }}/target/ruff-pgo/ruff.profdata" + # Apple Clang cannot consume profile data from rustc's LLVM version. + CFLAGS: "-fno-profile-generate -fno-profile-use" + CXXFLAGS: "-fno-profile-generate -fno-profile-use" - name: "Test wheel - aarch64" run: | pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall From f32cff568f636aefabe82ac8a4977537136aba79 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 10 Aug 2026 11:56:17 -0400 Subject: [PATCH 352/390] Enable PGO for Windows x86-64 Ruff releases (#27573) ## Summary This PR enables PGO for Ruff's Windows x86-64 releases, following the approach outlined in https://github.com/astral-sh/ruff/pull/27570. --- .github/workflows/build-binaries.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 6e1d1111f2..912239f9d1 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -199,6 +199,21 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} architecture: ${{ matrix.platform.arch }} + - name: "Install LLVM profiling tools" + if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }} + run: rustup component add llvm-tools-preview + - name: "Train PGO Ruff" + if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }} + shell: bash + run: | + export RUSTFLAGS="${RUSTFLAGS:+${RUSTFLAGS} }-C target-feature=+crt-static" + + python scripts/build_ruff_pgo.py \ + --target "${{ matrix.platform.target }}" \ + --target-dir "${{ github.workspace }}/target/ruff-pgo" \ + --train-only + + echo "RUSTFLAGS=${RUSTFLAGS:+${RUSTFLAGS} }-Cprofile-use=${{ github.workspace }}/target/ruff-pgo/ruff.profdata" >> "$GITHUB_ENV" - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" @@ -210,6 +225,18 @@ jobs: env: # aarch64 build fails, see https://github.com/PyO3/maturin/issues/2110 XWIN_VERSION: 16 + - name: "Verify static Windows runtime" + if: ${{ matrix.platform.target == 'x86_64-pc-windows-msvc' }} + shell: bash + run: | + LLVM_READOBJ="$(rustc --print sysroot)/lib/rustlib/${{ matrix.platform.target }}/bin/llvm-readobj.exe" + IMPORTS_FILE="$RUNNER_TEMP/ruff-coff-imports.txt" + "$LLVM_READOBJ" --coff-imports "target/${{ matrix.platform.target }}/release/ruff.exe" > "$IMPORTS_FILE" + + if grep -Eiq 'vcruntime[[:digit:]_]*\.dll|api-ms-win-crt-' "$IMPORTS_FILE"; then + echo "Ruff must not dynamically link the Visual C++ runtime" >&2 + exit 1 + fi - name: "Test wheel" if: ${{ !startsWith(matrix.platform.target, 'aarch64') }} shell: bash From fbe3e251d07dd8f64a55fb77fbb4d69a4017e60a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 10 Aug 2026 11:56:17 -0400 Subject: [PATCH 353/390] Enable PGO for Linux ARM64 Ruff releases (#27574) ## Summary This PR enables PGO for Ruff's Linx ARM64 releases, following the approach outlined in https://github.com/astral-sh/ruff/pull/27570. (To enable PGO, we also move to a native ARM64 runner.) As a result, `ruff check` gets 11.2% faster on the holdout set, `ruff format` gets 8.6% faster, and the release binary gets 3.6% smaller. --- .github/workflows/build-binaries.yml | 72 +++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 912239f9d1..d2c874149a 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -333,18 +333,78 @@ jobs: *.tar.gz *.sha256 + linux-aarch64: + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} + runs-on: ubuntu-24.04-arm + env: + # see https://github.com/astral-sh/ruff/issues/3791 + # and https://github.com/gnzlbg/jemallocator/issues/170#issuecomment-1503228963 + JEMALLOC_SYS_WITH_LG_PAGE: "16" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: recursive + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: "Install LLVM profiling tools" + run: rustup component add llvm-tools-preview + - name: "Train PGO Ruff" + run: | + python scripts/build_ruff_pgo.py \ + --target aarch64-unknown-linux-gnu \ + --target-dir "${{ github.workspace }}/target/ruff-pgo" \ + --train-only + + echo "RUSTFLAGS=${RUSTFLAGS:+${RUSTFLAGS} }-Cprofile-use=${{ github.workspace }}/target/ruff-pgo/ruff.profdata" >> "$GITHUB_ENV" + - name: "Prep README.md" + run: python scripts/transform_readme.py --target pypi + - name: "Build wheels" + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 + with: + maturin-version: v1.14.1 + target: aarch64-unknown-linux-gnu + manylinux: 2_17 + docker-options: -e JEMALLOC_SYS_WITH_LG_PAGE=16 + args: --release --locked --out dist --compatibility pypi + - name: "Test wheel" + run: | + pip install dist/"${PACKAGE_NAME}"-*.whl --force-reinstall + "${MODULE_NAME}" --help + python -m "${MODULE_NAME}" --help + - name: "Upload wheels" + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: wheels-aarch64-unknown-linux-gnu + path: dist + - name: "Archive binary" + shell: bash + run: | + set -euo pipefail + + TARGET=aarch64-unknown-linux-gnu + ARCHIVE_NAME=ruff-$TARGET + ARCHIVE_FILE=$ARCHIVE_NAME.tar.gz + + mkdir -p $ARCHIVE_NAME + cp target/$TARGET/release/ruff $ARCHIVE_NAME/ruff + tar czvf $ARCHIVE_FILE $ARCHIVE_NAME + shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256 + - name: "Upload binary" + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: artifacts-aarch64-unknown-linux-gnu + path: | + *.tar.gz + *.sha256 + linux-cross: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} runs-on: ubuntu-latest strategy: matrix: platform: - - target: aarch64-unknown-linux-gnu - arch: aarch64 - manylinux: 2_17 - # see https://github.com/astral-sh/ruff/issues/3791 - # and https://github.com/gnzlbg/jemallocator/issues/170#issuecomment-1503228963 - maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16 - target: armv7-unknown-linux-gnueabihf arch: armv7 manylinux: 2_17 From 6f896347e78d51e1c7860e61c09f455b7c7da4aa Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 10 Aug 2026 16:13:23 -0400 Subject: [PATCH 354/390] [ty] Exclude quantified constraints from semantic type walks (#27613) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary An owned constraint set retains both its live decision diagram and source-order metadata. Existential quantification can remove a constraint from the diagram while keeping it in the ordering metadata so inferred solutions retain their original order. ```text exists T. (T = int and U = str) live constraint: U = str source ordering: T, U ``` Previously, `OwnedConstraintSet::types()` walked every retained constraint, including ordering-only entries. When checking a covariant generic protocol with an explicitly typed receiver, stale method-local type variables were repeatedly freshened (`T`, `T₁`, `T₂`, ...), preventing Salsa from reaching a fixed point and causing a `too many cycle iterations` panic. We now walk only unique constraints reachable from decision-diagram nodes while preserving the complete source-order metadata. Existing quantified-solution ordering stays intact, and incompatible protocol overrides produce the expected diagnostic instead of crashing. Closes https://github.com/astral-sh/ty/issues/4222. --- .../resources/mdtest/protocols.md | 31 +++++++++++ .../src/types/constraints.rs | 52 +++++++++++++++++-- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index d476e1ae8c..fd8aae0c9e 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -4011,6 +4011,37 @@ class DateTime(Protocol[T]): return datetime.now(tz) # error: [invalid-return-type] ``` +## Recursive protocol receiver binding with an incompatible override + +An incompatible override of a covariant generic protocol method can recursively compare the +protocol's explicitly annotated receiver with the implementing class. The receiver-binding and +assignability queries must converge and report the incompatible override instead of panicking. + +```toml +[environment] +python-version = "3.11" +``` + +```py +from typing import Generic, Protocol, TypeVar + +T = TypeVar("T") +T_co = TypeVar("T_co", covariant=True) + +class Result(Generic[T_co]): ... + +class SupportsMethod(Protocol[T_co]): + def method(self: "SupportsMethod[T]") -> Result[T]: ... + +class Compatible(SupportsMethod[T_co]): + def method(self: "Compatible[T]") -> Result[T]: + raise NotImplementedError + +class Incompatible(SupportsMethod[T_co]): + def method(self: "Incompatible[T]", value: int) -> Result[T]: # error: [invalid-method-override] + raise NotImplementedError +``` + ## Subtyping of protocols with generic method members Protocol method members can be generic. They can have generic contexts scoped to the class: diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 0937f9f355..f0133d0a2c 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -317,13 +317,23 @@ impl<'db> OwnedConstraintSet<'db> { f(&builder, set) } + /// Returns the types in constraints that are still reachable from the decision diagram. + /// + /// Source ordering also retains quantified-away constraints to preserve binding order, but + /// their type variables must not participate in semantic walks or callable freshening. pub(crate) fn types(&self) -> impl Iterator> + '_ { self.inner.iter().flat_map(|inner| { - inner.constraints.iter().flat_map(|constraint| { - std::iter::once(Type::TypeVar(constraint.typevar)) - .chain(constraint.bounds.lower) - .chain(constraint.bounds.upper) - }) + inner + .nodes + .iter() + .map(|node| node.constraint) + .unique() + .map(|constraint| inner.constraints[inner.retained_constraint_index(constraint)]) + .flat_map(|constraint| { + std::iter::once(Type::TypeVar(constraint.typevar)) + .chain(constraint.bounds.lower) + .chain(constraint.bounds.upper) + }) }) } } @@ -8957,6 +8967,38 @@ mod tests { assert!(owned.node.index() >= inner.nodes.len()); } + #[test] + fn owned_constraint_set_type_walk_excludes_quantified_constraints() { + let db = setup_db(); + let db = &db; + let env = db.program_environment(); + let t = create_typevar(db, "T"); + let u = create_typevar(db, "U"); + + let owned = ConstraintSetBuilder::new().into_owned(|builder| { + let t_int = create_constraint(db, builder, t, KnownClass::Int); + let u_str = create_constraint(db, builder, u, KnownClass::Str); + t_int.and(db, builder, || u_str).reduce_inferable( + db, + &env, + builder, + TypeVarSet::from_typevars(db, [t]), + ) + }); + + assert_eq!( + owned + .types() + .filter_map(Type::as_typevar) + .collect::>(), + vec![u], + ); + assert_eq!( + owned.inner.as_ref().map(|inner| inner.source_orders.len()), + Some(3), + ); + } + #[test] fn owned_constraint_set_source_order_ignores_construction_history() { let db = setup_db(); From a0157239894cd5649b1840c8f47bec6169fe2612 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 10 Aug 2026 16:51:54 -0400 Subject: [PATCH 355/390] [ty] Validate augmented assignment stores (#27545) ## Summary Previously, augmented assignments validated the operator but not the assignment of its result back to an attribute or subscript: ```python class A: def __add__(self, other: object) -> object: return other class B: x: A b = B() b.x += 1 # error: [invalid-assignment] ``` We now validate augmented attribute and subscript stores through the same statement-level assignment machinery as ordinary assignments, including property setters, descriptors, read-only targets, `Final`, `ClassVar`, `TypedDict` entries, and union receivers and keys. Failed operators preserve their recovery types without triggering a write. We also include augmented assignments when inferring public instance-attribute types, which lets inferred attributes evolve without assignment-specific exemptions. Existing receiver-correlation and unannotated-collection inference limitations remain out of scope. Closes https://github.com/astral-sh/ty/issues/2175. --- .../resources/mdtest/assignment/augmented.md | 429 ++++++++++++++++++ .../resources/mdtest/attributes.md | 8 +- .../src/types/class/static_literal.rs | 5 +- .../src/types/infer/builder.rs | 180 ++++++-- .../src/types/infer/builder/subscript.rs | 158 ++++--- 5 files changed, 664 insertions(+), 116 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md index 1787834ee6..9fde35b37a 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md @@ -187,6 +187,435 @@ def f(flag: bool, flag2: bool): reveal_type(f) # revealed: float | str ``` +## Declared attributes with in-place operators + +`+=` assigns the value returned by `__iadd__` back to its target. That value must be compatible with +the attribute's declared type. + +```py +class Value: + def __iadd__(self, other: int) -> str: + return "updated" + +class Holder: + value: Value + +holder = Holder() +# error: [invalid-assignment] +holder.value += 1 +reveal_type(holder.value) # revealed: Value +``` + +## Declared attributes without in-place operators + +When an object does not define `__iadd__`, `+=` falls back to `__add__`. Its result must still be +compatible with the attribute's declared type. + +```py +class Value: + def __add__(self, other: int) -> str: + return "updated" + +class Holder: + value: Value + +holder = Holder() +# error: [invalid-assignment] +holder.value += 1 +``` + +## Inferred attributes in loops + +An unannotated instance attribute may change type. After its initial `None` value is replaced, an +augmented assignment inside a loop must also contribute its result to the inferred attribute type. + +```py +class Counter: + def update(self) -> None: + self.value = None + self.value = 0 + for _ in range(1): + self.value += 1.0 + +reveal_type(Counter().value) # revealed: None | float +``` + +## Inferred class attributes + +An unannotated class attribute still has an inferred type that restricts assignments through an +instance. + +```py +class Holder: + value = 1 + +holder = Holder() +# error: [invalid-assignment] +holder.value += 0.5 +``` + +## Read-only properties + +`+=` writes its result back to the attribute. A property without a setter therefore cannot be the +target of an augmented assignment. + +```py +class ReadOnly: + @property + def value(self) -> int: + return 1 + +read_only = ReadOnly() +# error: [invalid-assignment] +read_only.value += 1 +``` + +## Properties with different getter and setter types + +A property can accept a wider type in its setter than it returns from its getter. The result of `/=` +is checked against the setter, while subsequent reads still use the getter's return type. + +```py +class Counter: + @property + def value(self) -> int: + return 1 + + @value.setter + def value(self, value: float) -> None: + pass + +counter = Counter() +counter.value /= 2 +reveal_type(counter.value) # revealed: int +``` + +## Attributes defined by descriptors + +When an unannotated class attribute is a data descriptor, its `__set__` method determines which +values may be assigned. + +```py +class Descriptor: + def __get__(self, instance: object, owner: type[object] | None = None) -> int: + return 1 + + def __set__(self, instance: object, value: str) -> None: + pass + +class Holder: + value = Descriptor() + +holder = Holder() +# error: [invalid-assignment] +holder.value += 1 +``` + +## Custom subscript assignments + +`/=` first reads an item, then writes the result back through `__setitem__`. The assigned value is +the result of the operation, not the right-hand operand. + +```py +class Container: + def __getitem__(self, key: int) -> int: + return 1 + + def __setitem__(self, key: int, value: int) -> None: + pass + +container = Container() +# error: [invalid-assignment] +container[0] /= 2 +reveal_type(container[0]) # revealed: int +``` + +## Subscript setters with different value types + +A collection can accept a wider type in `__setitem__` than `__getitem__` returns. After a valid +assignment, subsequent reads still use the return type of `__getitem__`. + +```py +class Container: + def __getitem__(self, key: int) -> int: + return 1 + + def __setitem__(self, key: int, value: float) -> None: + pass + +container = Container() +container[0] /= 2 +reveal_type(container[0]) # revealed: int +``` + +## Annotated collection entries + +An annotation fixes the element type of a list, so `/=` cannot write a `float` into a `list[int]`. + +```py +values: list[int] = [1] +# error: [invalid-assignment] +values[0] /= 2 +``` + +The same rule applies to the value type of an annotated dictionary. + +```py +mapping: dict[str, int] = {"value": 1} +# error: [invalid-assignment] +mapping["value"] /= 2 +``` + +An annotated collection remains constrained when it is accessed through an attribute. + +```py +class Holder: + values: list[int] + +holder = Holder() +# error: [invalid-assignment] +holder.values[0] /= 2 +``` + +## Typed dictionary entries + +A `TypedDict` field can only be assigned a value compatible with its declared type. + +```py +from typing import TypedDict + +class Payload(TypedDict): + value: int + +payload: Payload = {"value": 1} +# error: [invalid-assignment] +payload["value"] /= 2 +``` + +## Read-only subscripts + +A readable item cannot be reassigned when its container does not implement `__setitem__`. + +```py +values: tuple[int] = (1,) +# error: [invalid-assignment] +values[0] += 1 +``` + +## Missing attributes + +If an augmented assignment cannot read its target, it must report that failure only once; no +assignment is attempted. + +```py +class Missing: ... + +missing = Missing() +# error: [unresolved-attribute] +missing.value += 1 +``` + +The same applies when an attribute is missing from one member of a union. + +```py +class Counter: + count: int + +def update(counter: Counter | None) -> None: + # error: [unresolved-attribute] + counter.count += 1 +``` + +An augmented assignment should not define an otherwise missing instance attribute, because it must +read an existing value before writing its result. We currently treat it like an ordinary +self-referential assignment instead. + +```py +class UninitializedCounter: + def increment(self) -> None: + # TODO: Report an unresolved-attribute error instead of implicitly defining the attribute. + self.value += 1 + +reveal_type(UninitializedCounter().value) # revealed: Divergent +``` + +## Dynamically provided attributes + +A dynamic attribute hook can provide the initial value read by an augmented assignment. The +assignment currently infers a divergent attribute type instead of preserving the hook's return type. + +```py +class DynamicCounter: + def __getattr__(self, name: str) -> int: + return 0 + + def increment(self) -> None: + self.value += 1 + +# TODO: Infer `int` from the dynamic attribute hook. +reveal_type(DynamicCounter().value) # revealed: Divergent +``` + +The same behavior applies when the attribute is provided by `__getattribute__`. + +```py +class InterceptedCounter: + def __getattribute__(self, name: str) -> int: + return 0 + + def increment(self) -> None: + self.value += 1 + +reveal_type(InterceptedCounter().value) # revealed: Divergent +``` + +## Class-level defaults in diamond inheritance + +An overriding class-level default supplies the initial value even when another branch of the +inheritance hierarchy declares a wider instance attribute. + +```py +class Base: + value: int | None = None + +class First(Base): ... + +class Second(Base): + value: int | None + +class Child(First, Second): + value: int = 1 + + def update(self) -> None: + self.value |= 2 +``` + +## Invalid subscript reads + +An invalid key prevents an item from being read, so the failed assignment must not produce a second +error. + +```py +mapping: dict[str, int] = {} +# error: [invalid-argument-type] +mapping[1] += 1 +``` + +A value without `__getitem__` also fails before assignment can be attempted. + +```py +value = 1 +# error: [not-subscriptable] +value[0] += 1 +``` + +## Right-hand-side errors after failed reads + +Even when an attribute cannot be read, the right-hand side must still be checked for unrelated +errors. + +```py +class Missing: ... + +missing = Missing() +# error: [unresolved-attribute] +# error: [unresolved-reference] +missing.value += missing_attribute_operand +``` + +The same rule applies when a subscript cannot be read. + +```py +mapping: dict[str, int] = {} +# error: [invalid-argument-type] +# error: [unresolved-reference] +mapping[1] += missing_subscript_operand +``` + +## Failed in-place operations + +If `__iadd__` rejects its operand, its return type must not be treated as a value to assign. + +```py +class Value: + def __iadd__(self, other: int) -> str: + return "updated" + +class Holder: + value: Value + +holder = Holder() +# error: [unsupported-operator] +holder.value += "invalid" +``` + +## Union attribute assignments + +When objects in a union have different attribute types, each operator result should be checked +against the attribute from the same object. Ordinary assignments already lose this relationship, so +augmented assignments currently report the same false positive. + +```py +class AValue: + def __iadd__(self, other: int) -> "AValue": + return self + +class BValue: + def __iadd__(self, other: int) -> "BValue": + return self + +class A: + value: AValue + +class B: + value: BValue + +def update(value: A | B) -> None: + # TODO: Check each result against the attribute it came from. + # error: [invalid-assignment] + value.value += 1 +``` + +## Collections that may be read-only + +When a collection could be a writable list or a read-only tuple, an item assignment is invalid +because it cannot be performed on every possible value. + +```py +def update(value: list[int] | tuple[int, ...]) -> None: + # error: [invalid-assignment] + value[0] += 1 +``` + +## Typed dictionary assignments with multiple possible keys + +A key that can select fields with different value types must only be assigned a value accepted by +every possible field. + +```py +from typing import Literal, TypedDict + +class Payload(TypedDict): + whole: int + fractional: float + +def update(value: Payload, key: Literal["whole", "fractional"]) -> None: + # error: [invalid-assignment] + value[key] /= 2 +``` + +## Inferred collection entries + +Augmented assignments are not yet included when inferring the element type of an unannotated +collection. + +```py +values = [1] +# TODO: Infer `list[float]` instead of rejecting the assignment. +# error: [invalid-assignment] +values[0] /= 2 +``` + ## Implicit dunder calls on class objects ```py diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index b488a2f05f..016f1f2c27 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -259,6 +259,9 @@ reveal_type(c_instance.b) # revealed: int #### Augmented assignments +An augmented assignment contributes its result to the inferred type of an unannotated instance +attribute. + ```py class Weird: def __iadd__(self, other: None) -> str: @@ -269,9 +272,8 @@ class C: self.w = Weird() self.w += None -# TODO: Mypy and pyright do not support this, but it would be great if we could -# infer `str` here (`Weird` is not a possible type for the `w` attribute). -reveal_type(C().w) # revealed: Weird +# TODO: Infer only `str`, since the initial `Weird` value has been overwritten. +reveal_type(C().w) # revealed: Weird | str ``` #### Nested augmented assignments after narrowing diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index f26e8eb29b..ef23d92279 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -48,7 +48,7 @@ use crate::{ is_implicit_staticmethod, }, generics::Specialization, - infer::infer_unpack_types, + infer::{infer_definition_types, infer_unpack_types}, infer_expression_type, inferred_declaration, known_instance::DeprecatedInstance, member::{Member, class_member}, @@ -3106,8 +3106,7 @@ impl<'db> StaticClassLiteral<'db> { } } DefinitionKind::AugmentedAssignment(_) => { - // TODO: - None + Some(infer_definition_types(db, binding).binding_type(binding)) } DefinitionKind::NamedExpression(_) => { // A named expression whose target is an attribute is syntactically prohibited diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 79d950f1c4..cc9431c9c2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -1487,7 +1487,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn fallback_member_declared_type(&mut self, node: AnyNodeRef<'_>) -> Option> { let db = self.db(); if let AnyNodeRef::ExprAttribute(ast::ExprAttribute { value, attr, .. }) = node { - let value_type = self.infer_maybe_standalone_expression(value, TypeContext::default()); + let value_type = self.try_expression_type(value).unwrap_or_else(|| { + self.infer_maybe_standalone_expression(value, TypeContext::default()) + }); if let Place::Defined(DefinedPlace { ty, definedness: Definedness::AlwaysDefined, @@ -1507,9 +1509,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }, ) = node { - let value_ty = self.infer_expression(value, TypeContext::default()); - let slice_ty = self.infer_expression(slice, TypeContext::default()); - Some(self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx)) + let value_ty = self.get_or_infer_expression(value, TypeContext::default()); + let slice_ty = self.get_or_infer_expression(slice, TypeContext::default()); + Some( + self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx) + .unwrap_or_else(|recovery_ty| recovery_ty), + ) } else { None } @@ -3218,13 +3223,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } ast::Expr::Subscript(subscript_expr) => { if let Some(infer_assigned_ty) = infer_assigned_ty { + let object_ty = + self.infer_expression(&subscript_expr.value, TypeContext::default()); + let mut infer_slice_ty = |builder: &mut Self, tcx| { + builder.infer_expression(&subscript_expr.slice, tcx) + }; let infer_assigned_ty = &mut |builder: &mut Self, tcx| { let assigned_ty = infer_assigned_ty(builder, tcx); builder.store_expression_type(target, assigned_ty); assigned_ty }; - self.validate_subscript_assignment(subscript_expr, value, infer_assigned_ty); + self.validate_subscript_assignment( + subscript_expr, + value, + object_ty, + &mut infer_slice_ty, + infer_assigned_ty, + ); } } @@ -4676,23 +4692,49 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_definition(assignment); } else { // Non-name assignment targets are inferred as ordinary expressions, not definitions. - self.infer_augment_assignment(assignment); + if let Ok(result_ty) = self.infer_augment_assignment(assignment) { + let target = assignment.target.as_ref(); + match target { + ast::Expr::Attribute(attribute) => { + let object_ty = self.expression_type(&attribute.value); + self.validate_attribute_assignment( + attribute, + target, + object_ty, + attribute.attr.id(), + &mut |_, _| result_ty, + true, + ); + } + ast::Expr::Subscript(subscript) => { + let object_ty = self.expression_type(&subscript.value); + let slice_ty = self.expression_type(&subscript.slice); + self.validate_subscript_assignment( + subscript, + target, + object_ty, + &mut |_, _| slice_ty, + &mut |_, _| result_ty, + ); + } + _ => {} + } + } if let ast::Expr::Attribute(attr_expr) = assignment.target.as_ref() { - let object_ty = self.expression_type(&attr_expr.value); self.report_undeclared_protocol_attribute(attr_expr); - self.validate_final_attribute_assignment(attr_expr, object_ty, attr_expr.attr.id()); } } } + /// Infer an augmented operator, returning its recovery type if the operation fails. fn infer_augmented_op( &mut self, assignment: &ast::StmtAugAssign, target_type: Type<'db>, value_expr: &ast::Expr, infer_value_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, - ) -> Type<'db> { + ) -> Result, Type<'db>> { let db = self.db(); let env = self.program_environment(); // If the target defines, e.g., `__iadd__`, infer the augmented assignment as a call to that @@ -4703,7 +4745,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let binary_return_ty = |builder: &mut Self, value_ty| { builder .infer_binary_expression_type(assignment.into(), false, target_type, value_ty, op) - .unwrap_or_else(|| { + .ok_or_else(|| { report_unsupported_augmented_assignment( &builder.context, assignment, @@ -4722,14 +4764,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // equally applicable type contexts for each union member. infer_value_ty.infer_loud(self, TypeContext::default()); - union.map(db, env, |&elem_type| { - self.infer_augmented_op( + let mut operation_failed = false; + let result_ty = union.map(db, env, |&elem_type| { + match self.infer_augmented_op( assignment, elem_type, value_expr, &mut |builder, tcx| infer_value_ty.infer_silent(builder, tcx), - ) - }) + ) { + Ok(ty) => ty, + Err(recovery_ty) => { + operation_failed = true; + recovery_ty + } + } + }); + + if operation_failed { + Err(result_ty) + } else { + Ok(result_ty) + } } _ => { @@ -4741,7 +4796,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { infer_value_ty, ) { - return typed_dict_update_ty; + return Ok(typed_dict_update_ty); } let ast_arguments = [ArgOrKeyword::Arg(value_expr)]; @@ -4757,7 +4812,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { TypeContext::default(), ); match call { - Ok(outcome) => outcome.return_type(db, env), + Ok(outcome) => Ok(outcome.return_type(db, env)), Err(CallDunderError::MethodNotAvailable) => { let value_ty = infer_value_ty(self, TypeContext::default()); binary_return_ty(self, value_ty) @@ -4766,12 +4821,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { bindings: outcome, .. }) => { let value_ty = outcome.type_for_argument(&call_arguments, 0); - UnionType::from_two_elements( - db, - env, - outcome.return_type(db, env), - binary_return_ty(self, value_ty), - ) + match binary_return_ty(self, value_ty) { + Ok(binary_ty) => Ok(UnionType::from_two_elements( + db, + env, + outcome.return_type(db, env), + binary_ty, + )), + Err(recovery_ty) => Err(UnionType::from_two_elements( + db, + env, + outcome.return_type(db, env), + recovery_ty, + )), + } } Err(CallDunderError::CallError(_, bindings, _)) => { let value_ty = bindings.type_for_argument(&call_arguments, 0); @@ -4781,7 +4844,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { target_type, value_ty, ); - bindings.return_type(db, env) + Err(bindings.return_type(db, env)) } } } @@ -4793,12 +4856,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { assignment: &'ast ast::StmtAugAssign, definition: Definition<'db>, ) { - let target_ty = self.infer_augment_assignment(assignment); - self.add_binding(assignment.into(), definition) + let target_ty = self + .infer_augment_assignment(assignment) + .unwrap_or_else(|recovery_ty| recovery_ty); + self.add_binding(assignment.target.as_ref().into(), definition) .insert(self, target_ty); } - fn infer_augment_assignment(&mut self, assignment: &ast::StmtAugAssign) -> Type<'db> { + fn infer_augment_assignment( + &mut self, + assignment: &ast::StmtAugAssign, + ) -> Result, Type<'db>> { let ast::StmtAugAssign { range: _, node_index: _, @@ -4808,28 +4876,37 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = assignment; // Resolve the target type, assuming a load context. - let target_type = match &**target { + let target_result = match &**target { ast::Expr::Name(name) => { let previous_value = self.infer_name_load(name); self.store_expression_type(target, previous_value); - previous_value + Ok(previous_value) } ast::Expr::Attribute(attr) => { - let previous_value = self.infer_attribute_load(attr); + let result = self.infer_attribute_load(attr); + let previous_value = result.unwrap_or_else(|recovery_ty| recovery_ty); self.store_expression_type(target, previous_value); - previous_value + result } ast::Expr::Subscript(subscript) => { - let previous_value = self.infer_subscript_load(subscript); + let result = self.infer_subscript_load(subscript); + let previous_value = result.unwrap_or_else(|recovery_ty| recovery_ty); self.store_expression_type(target, previous_value); - previous_value + result } - _ => self.infer_expression(target, TypeContext::default()), + _ => Ok(self.infer_expression(target, TypeContext::default())), }; - self.infer_augmented_op(assignment, target_type, value, &mut |builder, tcx| { - builder.infer_expression(value, tcx) - }) + let target_type = target_result.unwrap_or_else(|recovery_ty| recovery_ty); + let operation_result = + self.infer_augmented_op(assignment, target_type, value, &mut |builder, tcx| { + builder.infer_expression(value, tcx) + }); + + match (target_result, operation_result) { + (Ok(_), Ok(result_ty)) => Ok(result_ty), + (_, Ok(recovery_ty) | Err(recovery_ty)) => Err(recovery_ty), + } } fn infer_dict_key_assignment_definition( @@ -9244,6 +9321,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let collection_generic_context = collection_literal.generic_context(db); let mut identity_bindings = self .infer_attribute_load_impl(attribute, identity_instance) + .unwrap_or_else(|recovery_ty| recovery_ty) .bindings(db, env) .match_parameters(db, env, &call_arguments) // Perform inference against the type variables on the receiver's generic context. @@ -10337,19 +10415,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - /// Infer the type of a [`ast::ExprAttribute`] expression, assuming a load context. - fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { + /// Infer an attribute load, returning its recovery type if lookup fails. + fn infer_attribute_load( + &mut self, + attribute: &ast::ExprAttribute, + ) -> Result, Type<'db>> { let value_type = self.infer_maybe_standalone_expression(&attribute.value, TypeContext::default()); self.infer_attribute_load_impl(attribute, value_type) } - /// Infer the type of a [`ast::ExprAttribute`] expression, assuming a load context. + /// Infer an attribute load on a known receiver, returning its recovery type if lookup fails. fn infer_attribute_load_impl( &mut self, attribute: &ast::ExprAttribute, mut value_type: Type<'db>, - ) -> Type<'db> { + ) -> Result, Type<'db>> { fn union_elements_missing_attribute<'db>( db: &'db dyn Db, env: &ProgramEnvironment<'db>, @@ -10412,8 +10493,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }); let attr_name = &attr.id; - let resolved_type = - fallback_place.unwrap_with_diagnostic(db, env, |lookup_err| match lookup_err { + let lookup_result = fallback_place.into_lookup_result(db, env); + let resolved_type = lookup_result.unwrap_or_else(|lookup_err| { + match lookup_err { LookupError::Undefined(_) => { let fallback = || { TypeAndQualifiers::new( @@ -10663,7 +10745,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { type_when_bound } - }); + } + }); let resolved_type = resolved_type.inner_type(); @@ -10671,7 +10754,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Even if we can obtain the attribute type based on the assignments, we still perform default type inference // (to report errors). - assigned_type.unwrap_or(resolved_type) + let inferred_type = assigned_type.unwrap_or(resolved_type); + lookup_result + .map(|_| inferred_type) + .map_err(|_| inferred_type) } fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { @@ -10684,13 +10770,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = attribute; match ctx { - ExprContext::Load => self.infer_attribute_load(attribute), + ExprContext::Load => self + .infer_attribute_load(attribute) + .unwrap_or_else(|recovery_ty| recovery_ty), ExprContext::Store => { self.infer_expression(value, TypeContext::default()); Type::Never } ExprContext::Del => { - self.infer_attribute_load(attribute); + let _ = self.infer_attribute_load(attribute); self.validate_attribute_deletion( attribute, self.expression_type(value), diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index 24a845fc99..41bc1b31ac 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -138,12 +138,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } = subscript; match ctx { - ExprContext::Load => self.infer_subscript_load(subscript), + ExprContext::Load => self + .infer_subscript_load(subscript) + .unwrap_or_else(|recovery_ty| recovery_ty), ExprContext::Store => { let value_ty = self.infer_expression(value, TypeContext::default()); self.store_typed_dict_key_expected_type(slice, value_ty); let slice_ty = self.infer_expression(slice, TypeContext::default()); - self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); + let _ = self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); Type::Never } ExprContext::Del => { @@ -156,20 +158,29 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ExprContext::Invalid => { let value_ty = self.infer_expression(value, TypeContext::default()); let slice_ty = self.infer_expression(slice, TypeContext::default()); - self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); + let _ = self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); Type::unknown() } } } - pub(super) fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { + /// Infer a subscript load, returning its inferred type when the subscription succeeds. + /// + /// If the subscription fails, report the error and return the type that should be used to + /// continue inference. This recovery type may be `Unknown` or, for example, the return type of + /// `__getitem__` when its arguments are invalid. Keeping it separate from a successful result + /// lets augmented assignments check their right-hand side without attempting a failed store. + pub(super) fn infer_subscript_load( + &mut self, + subscript: &ast::ExprSubscript, + ) -> Result, Type<'db>> { let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); // If we have an implicit type alias like `MyList = list[T]`, and if `MyList` is being // used in another implicit type alias like `Numbers = MyList[int]`, then we infer the // right hand side as a value expression, and need to handle the specialization here. if value_ty.is_generic_alias() { - return self.infer_explicit_type_alias_specialization(subscript, value_ty, false); + return Ok(self.infer_explicit_type_alias_specialization(subscript, value_ty, false)); } self.infer_subscript_load_impl(value_ty, subscript) @@ -179,7 +190,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, value_ty: Type<'db>, subscript: &ast::ExprSubscript, - ) -> Type<'db> { + ) -> Result, Type<'db>> { let env = self.program_environment(); let db = self.db(); @@ -188,7 +199,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { node_index: _, value: _, slice, - ctx: expr_context, + ctx: _, } = subscript; self.store_typed_dict_key_expected_type(slice, value_ty); @@ -212,13 +223,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Even if we can obtain the subscript type based on the assignments, we still perform default type inference // (to store the expression type and to report errors). let slice_ty = self.infer_expression(slice, TypeContext::default()); - self.infer_subscript_expression_types( - subscript, - value_ty, - slice_ty, - *expr_context, - ); - return ty; + return self + .infer_subscript_expression_types( + subscript, + value_ty, + slice_ty, + ExprContext::Load, + ) + .map(|_| ty) + .map_err(|_| ty); } } } @@ -237,43 +250,49 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // updating all of the subscript logic below to use custom callables for all of the _other_ // special cases, too. if class.is_tuple(db) { - return tuple_generic_alias(env, self.infer_tuple_type_expression(subscript)); + return Ok(tuple_generic_alias( + env, + self.infer_tuple_type_expression(subscript), + )); } else if class.is_known(db, KnownClass::Type) { let argument_ty = self.infer_type_expression(slice); - return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + return Ok(Type::KnownInstance(KnownInstanceType::TypeGenericAlias( InternedType::new(db, argument_ty), - )); + ))); } if let Some(generic_context) = class.generic_context(db) && let Some(class) = class.as_static() { - return self.infer_explicit_class_specialization( + return Ok(self.infer_explicit_class_specialization( subscript, value_ty, class, generic_context, - ); + )); } } Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => { if let Some(generic_context) = type_alias.generic_context(db) { - return self.infer_explicit_type_alias_type_specialization( + return Ok(self.infer_explicit_type_alias_type_specialization( subscript, value_ty, type_alias, generic_context, - ); + )); } } Type::SpecialForm(special_form) => match special_form { SpecialFormType::Tuple => { - return tuple_generic_alias(env, self.infer_tuple_type_expression(subscript)); + return Ok(tuple_generic_alias( + env, + self.infer_tuple_type_expression(subscript), + )); } SpecialFormType::Literal => match self.infer_literal_parameter_type(slice) { Ok(result) => { - return Type::KnownInstance(KnownInstanceType::Literal(InternedType::new( - db, result, + return Ok(Type::KnownInstance(KnownInstanceType::Literal( + InternedType::new(db, result), ))); } Err(nodes) => { @@ -288,16 +307,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { or an enum member", ); } - return Type::unknown(); + return Ok(Type::unknown()); } }, SpecialFormType::Annotated => { - return self + return Ok(self .parse_subscription_of_annotated_special_form( subscript, AnnotatedExprContext::TypeExpression, ) - .inner_type(); + .inner_type()); } SpecialFormType::Optional => { if matches!(**slice, ast::Expr::Tuple(_)) @@ -313,9 +332,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // `Optional[None]` is equivalent to `None`: if ty.is_none(db) { - return ty; + return Ok(ty); } - return Type::KnownInstance(KnownInstanceType::UnionType( + return Ok(Type::KnownInstance(KnownInstanceType::UnionType( UnionTypeInstance::new( db, None, @@ -326,7 +345,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::none(db, env), )), ), - )); + ))); } SpecialFormType::Union => match **slice { ast::Expr::Tuple(ref tuple) => { @@ -349,18 +368,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - return union_type; + return Ok(union_type); } _ => { - return self.infer_expression(slice, TypeContext::default()); + return Ok(self.infer_expression(slice, TypeContext::default())); } }, SpecialFormType::Type => { // Similar to the branch above that handles `type[…]`, handle `typing.Type[…]` let argument_ty = self.infer_type_expression(slice); - return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + return Ok(Type::KnownInstance(KnownInstanceType::TypeGenericAlias( InternedType::new(db, argument_ty), - )); + ))); } SpecialFormType::TypingCallable | SpecialFormType::CollectionsAbcCallable => { let callable = self @@ -368,7 +387,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .as_callable() .expect("always returns Type::Callable"); - return Type::KnownInstance(KnownInstanceType::Callable(callable)); + return Ok(Type::KnownInstance(KnownInstanceType::Callable(callable))); } SpecialFormType::Unpack => { self.store_type_expression_flags( @@ -386,19 +405,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { previously_in_unpack_type_argument, ); - return if matches!( - inner_ty, - Type::TypeVar(typevar) if typevar.is_typevartuple(db) - ) || inner_ty.exact_tuple_instance_spec(db).is_some() - { - inner_ty - } else { - self.store_type_expression_flags( - ast::ExprRef::from(subscript), - TypeExpressionFlags::INVALID_UNPACK, - ); - Type::unknown() - }; + return Ok( + if matches!( + inner_ty, + Type::TypeVar(typevar) if typevar.is_typevartuple(db) + ) || inner_ty.exact_tuple_instance_spec(db).is_some() + { + inner_ty + } else { + self.store_type_expression_flags( + ast::ExprRef::from(subscript), + TypeExpressionFlags::INVALID_UNPACK, + ); + Type::unknown() + }, + ); } SpecialFormType::LegacyStdlibAlias(alias) => { let AliasSpec { @@ -434,10 +455,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(|arg| self.infer_type_expression(arg)) .collect(); - return class + return Ok(class .to_specialized_class_type(db, env, arg_types) .map(Type::from) - .unwrap_or_else(Type::unknown); + .unwrap_or_else(Type::unknown)); } _ => {} }, @@ -448,7 +469,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | KnownInstanceType::Callable(_) | KnownInstanceType::TypeGenericAlias(_), ) => { - return self.infer_explicit_type_alias_specialization(subscript, value_ty, false); + return Ok( + self.infer_explicit_type_alias_specialization(subscript, value_ty, false) + ); } Type::Dynamic(DynamicType::Unknown) => { let slice_ty = self.infer_expression(slice, TypeContext::default()); @@ -460,15 +483,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut variables, ); let generic_context = GenericContext::from_typevar_instances(db, env, variables); - return Type::Dynamic(DynamicType::UnknownGeneric(generic_context)); + return Ok(Type::Dynamic(DynamicType::UnknownGeneric(generic_context))); } _ => {} } let slice_ty = self.infer_expression(slice, TypeContext::default()); - let result_ty = - self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *expr_context); - self.narrow_expr_with_applicable_constraints(subscript, result_ty, &constraint_keys) + self.infer_subscript_expression_types(subscript, value_ty, slice_ty, ExprContext::Load) + .map(|ty| self.narrow_expr_with_applicable_constraints(subscript, ty, &constraint_keys)) + .map_err(|recovery_ty| { + self.narrow_expr_with_applicable_constraints( + subscript, + recovery_ty, + &constraint_keys, + ) + }) } pub(super) fn infer_explicit_class_specialization( @@ -1399,13 +1428,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Err(()) } + /// Infer a subscription and report failures while preserving their recovery types. pub(super) fn infer_subscript_expression_types( &self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, slice_ty: Type<'db>, expr_context: ExprContext, - ) -> Type<'db> { + ) -> Result, Type<'db>> { let env = self.program_environment(); let db = self.db(); @@ -1465,7 +1495,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { SubscriptErrorKind::MultipleTypeVarTuples { origin }, ); error.report_diagnostics(&self.context, subscript); - return error.result_type(); + return Err(error.result_type()); } if has_invalid_unpack_argument { let error = SubscriptError::new( @@ -1476,7 +1506,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }, ); error.report_diagnostics(&self.context, subscript); - return error.result_type(); + return Err(error.result_type()); } } @@ -1519,9 +1549,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { _ => value_ty.subscript(db, env, slice_ty, expr_context), }; - subscript_result.unwrap_or_else(|e| { - e.report_diagnostics(&self.context, subscript); - e.result_type() + subscript_result.map_err(|error| { + error.report_diagnostics(&self.context, subscript); + error.result_type() }) } @@ -1556,6 +1586,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { &mut self, target: &ast::ExprSubscript, rhs_value: &ast::Expr, + object_ty: Type<'db>, + infer_slice_ty: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, infer_rhs_value: &mut dyn FnMut(&mut Self, TypeContext<'db>) -> Type<'db>, ) -> bool { let env = self.program_environment(); @@ -1569,15 +1601,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); - let object_ty = self.infer_expression(object, TypeContext::default()); self.store_typed_dict_key_expected_type(slice, object_ty); - let mut infer_slice_ty = |builder: &mut Self, tcx| builder.infer_expression(slice, tcx); let is_valid_assignment = self.validate_subscript_assignment_impl( target, None, object_ty, - &mut infer_slice_ty, + infer_slice_ty, rhs_value, infer_rhs_value, true, From d23b7e880effffb09b8315c8dff402aee595cd99 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 11 Aug 2026 02:02:38 +0500 Subject: [PATCH 356/390] [`pylint`] Fix false positives and negatives with `%b` format character (`PLE1300`, `PLE1307`) (#27560) ## Summary Noticed case below with possible false positive/negative when handling `%b` format character by rules https://docs.astral.sh/ruff/rules/bad-string-format-character/ and https://docs.astral.sh/ruff/rules/bad-string-format-type/ ```python # False negative: not flagged by bad-string-format-character # Runtime: ValueError: unsupported format character 'b' (0x62) at index 7 a = "hello %b" % 25 # False positive: bad-string-format-type # Runtime: ValueError: unsupported format character 'b' (0x62) at index 7 # It should report nothing, since the problem is not mismatching # formatter and provided value, but use of invalid format character in general. a = "hello %b" % "23" ``` This fix resolves both issues - first case is now reported and second results in no diagnostic. The root cause for was `bad-string-format-character` issue was `CFormatString` parser always parsing `%b` as `Bytes` type, while for bytes literals it's actually just an invalid character. Added `CFormatContext` enum, so parser can parse `%b` only when invoked from `CFormatBytes` and not from `CFormatString` and report issue when finds `%b` in string literals formatters. Second issue was caused by `bad-string-format-type` assuming `%b` is allowed only for integers, so it was reporting any other type as a mismatch. Now it just ignores `%b`, since it will be handled by `bad-string-format-character`. https://github.com/astral-sh/ruff/blob/17a00de2e298612201a8fe30790e9399204af1b9/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs#L100 For completeness I also refactored `FormatType::from` in that rule to rely on `CFormatType` variants instead of hardcoding formatter characters It really just made it more clean without any functional changes: - `Ascii` is now handled explicitly as `Repr`, instead of assuming it's unknown - dropped non-existing formatters `n` and `%`, they are present from the original implementation https://github.com/astral-sh/ruff/pull/2572 and are not valid formatters (possibly `%` is artifact from old cformat parser quirks). ## Test Plan Added tests for both rules, updated snapshots. All previous tests pass too. --- .../pylint/bad_string_format_character.py | 4 ++ .../fixtures/pylint/bad_string_format_type.py | 3 ++ ...LE1300_bad_string_format_character.py.snap | 9 +++++ crates/ruff_python_literal/src/cformat.rs | 40 ++++++++++++++----- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py b/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py index cffe53d723..3dcea82d8f 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py @@ -32,3 +32,7 @@ ## False negatives print(("%" "z") % 1) + +## `%b` is only valid for bytes formatting. +"%b" % b"25" +b"%b" % b"25" diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_type.py b/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_type.py index e95b8ed9a6..785ecaf659 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_type.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_type.py @@ -63,3 +63,6 @@ "%c" % ("x",) "%c" % "x" "%c" % "œ" + +# No errors here, will be reported separately by bad-string-format-character. +"%b" % b"xx" diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap index 8de582760c..cad0c10cd5 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap @@ -66,3 +66,12 @@ PLE1300 Unsupported format character 'y' 20 | "{0:.{prec}g}".format(1.23, prec=15) # OK (cannot validate after nested placeholder) 21 | "{0:.{foo}{bar}{foobar}y}".format(...) # OK (cannot validate after nested placeholders) | + +PLE1300 Unsupported format character 'b' + --> bad_string_format_character.py:37:1 + | +36 | ## `%b` is only valid for bytes formatting. +37 | "%b" % b"25" + | ^^^^^^^^^^^^ +38 | b"%b" % b"25" + | diff --git a/crates/ruff_python_literal/src/cformat.rs b/crates/ruff_python_literal/src/cformat.rs index 5427e7a854..5d2050d6b9 100644 --- a/crates/ruff_python_literal/src/cformat.rs +++ b/crates/ruff_python_literal/src/cformat.rs @@ -74,6 +74,12 @@ pub enum CFormatType { String(CFormatConversion), } +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum CFormatContext { + Str, + Bytes, +} + #[derive(Debug, PartialEq)] pub enum CFormatPrecision { Quantity(CFormatQuantity), @@ -123,14 +129,17 @@ impl FromStr for CFormatSpec { return Err((CFormatErrorType::MissingModuloSign, 1)); } - CFormatSpec::parse(&mut chars) + CFormatSpec::parse(&mut chars, CFormatContext::Str) } } pub type ParseIter = Peekable>; impl CFormatSpec { - pub fn parse(iter: &mut ParseIter) -> Result + pub fn parse( + iter: &mut ParseIter, + context: CFormatContext, + ) -> Result where T: Into + Copy, I: Iterator, @@ -140,7 +149,7 @@ impl CFormatSpec { let min_field_width = parse_quantity(iter)?; let precision = parse_precision(iter)?; consume_length(iter); - let (format_type, format_char) = parse_format_type(iter)?; + let (format_type, format_char) = parse_format_type(iter, context)?; Ok(CFormatSpec { mapping_key, @@ -204,7 +213,10 @@ where } } -fn parse_format_type(iter: &mut ParseIter) -> Result<(CFormatType, char), ParsingError> +fn parse_format_type( + iter: &mut ParseIter, + context: CFormatContext, +) -> Result<(CFormatType, char), ParsingError> where T: Into, I: Iterator, @@ -234,7 +246,9 @@ where 'c' => CFormatType::Character, 'r' => CFormatType::String(CFormatConversion::Repr), 's' => CFormatType::String(CFormatConversion::Str), - 'b' => CFormatType::String(CFormatConversion::Bytes), + // `%b` is only valid for bytes formatting (e.g. `b"%b" % b"x"`), not for string + // formatting. + 'b' if context == CFormatContext::Bytes => CFormatType::String(CFormatConversion::Bytes), 'a' => CFormatType::String(CFormatConversion::Ascii), _ => return Err((CFormatErrorType::UnsupportedFormatChar(c), index)), }; @@ -363,9 +377,11 @@ impl CFormatBytes { CFormatPart::Literal(std::mem::take(&mut literal)), )); } - let spec = CFormatSpec::parse(iter).map_err(|err| CFormatError { - typ: err.0, - index: err.1, + let spec = CFormatSpec::parse(iter, CFormatContext::Bytes).map_err(|err| { + CFormatError { + typ: err.0, + index: err.1, + } })?; parts.push((index, CFormatPart::Spec(spec))); if let Some(&(index, _)) = iter.peek() { @@ -418,9 +434,11 @@ impl CFormatString { CFormatPart::Literal(std::mem::take(&mut literal)), )); } - let spec = CFormatSpec::parse(iter).map_err(|err| CFormatError { - typ: err.0, - index: err.1, + let spec = CFormatSpec::parse(iter, CFormatContext::Str).map_err(|err| { + CFormatError { + typ: err.0, + index: err.1, + } })?; parts.push((index, CFormatPart::Spec(spec))); if let Some(&(index, _)) = iter.peek() { From b9afd02a11677174df1f4784eb0e98c5f86744d7 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 10 Aug 2026 23:16:05 +0100 Subject: [PATCH 357/390] [ty] Fix truthiness inference for subclassable known classes (#27638) --- .../resources/mdtest/type_properties/truthiness.md | 12 ++++++++++++ crates/ty_python_semantic/src/types/class/known.rs | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md index 3804903ef6..cacc816c9d 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md @@ -122,6 +122,18 @@ static_assert(is_subtype_of(types.MethodWrapperType, AlwaysTruthy)) static_assert(is_subtype_of(types.WrapperDescriptorType, AlwaysTruthy)) ``` +### Subclassable special-cased classes + +`Path` and `super` cannot be inferred as always truthy because subclasses can override `__bool__`. + +```py +from pathlib import Path + +def _(path: Path, superclass: super): + reveal_type(bool(path)) # revealed: bool + reveal_type(bool(superclass)) # revealed: bool +``` + ### `Callable` types always have ambiguous truthiness ```py diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs index 08de9ae7a8..66d8846b35 100644 --- a/crates/ty_python_semantic/src/types/class/known.rs +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -202,7 +202,6 @@ impl KnownClass { | Self::TypeVarTuple | Self::ExtensionsTypeVarTuple | Self::Sentinel - | Self::Super | Self::WrapperDescriptorType | Self::UnionType | Self::GeneratorType @@ -210,8 +209,7 @@ impl KnownClass { | Self::MethodWrapperType | Self::CoroutineType | Self::BuiltinFunctionType - | Self::Template - | Self::Path => Some(Truthiness::AlwaysTrue), + | Self::Template => Some(Truthiness::AlwaysTrue), Self::NoneType => Some(Truthiness::AlwaysFalse), @@ -273,6 +271,7 @@ impl KnownClass { | Self::SupportsKeysAndGetItem | Self::Staticmethod | Self::Classmethod + | Self::Super | Self::Awaitable | Self::Generator | Self::AsyncGenerator @@ -287,6 +286,7 @@ impl KnownClass { | Self::Specialization | Self::ProtocolMeta | Self::FunctoolsPartial + | Self::Path | Self::ExtensionTypedDictFallback | Self::TypedDictFallback | Self::PydanticBaseModel From 7be0508101b558de20f6fb17b2cbff5b62c7e0a0 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 10 Aug 2026 15:29:39 -0700 Subject: [PATCH 358/390] [ty] Preserve literal-string origin in comparison narrowing (#27582) ## Summary This PR implements a consistent view of the semantics of `LiteralString` and string `Literal` types. The key observation is that if `Literal["hello"] <: LiteralString <: str` holds, and `LiteralString` is not equivalent to `str`, that necessarily implies that `Literal["hello"]` does not include all runtime string objects with the value `"hello"` -- only those that are known to have literal origin (that is, those that also inhabit `LiteralString`). Similar to `NewType` and generic specializations, this can be described in terms of typed values having both a runtime object and possibly static-only "tags", which don't exist at runtime but still participate in determining which types they inhabit. So a runtime string object with value `"hello"` may be a typed value that carries the "literal origin" tag (in this case it inhabits `LiteralString` and `Literal["hello"]`), or may not carry that tag (in which case it inhabits neither `LiteralString` nor `Literal["hello"]`.) This PR just carries this understanding consistently through our narrowing support. - Preserve existing string-origin constraints when runtime equality, identity, membership, or value-pattern comparisons can succeed. - Treat an excluded string literal as an impossible runtime value only when the candidate already proves `LiteralString` origin. - Restore definitive equality and inequality results for known literal-origin strings with excluded values. Closes astral-sh/ty#4214. ## Test plan - Added mdtests cover `str & ~LiteralString`, `str & ~Literal["hello"]`, and `~Literal["hello"]` across comparison truthiness and equality/identity narrowing, including reversed operands, inequality branches, optional unions, and strict equality semantics. - Added mdtests cover membership and match value patterns for both strings without literal origin and known literal-origin exclusions. --- .../mdtest/comparison/intersections.md | 40 +++++++++--- .../mdtest/narrow/conditionals/eq.md | 63 ++++++++++++++++++- .../mdtest/narrow/conditionals/in.md | 22 +++++++ .../mdtest/narrow/conditionals/is.md | 31 ++++++++- .../resources/mdtest/narrow/match.md | 24 +++++++ .../ty_python_semantic/src/types/equality.rs | 29 +++++++++ .../src/types/infer/comparisons.rs | 17 +++-- 7 files changed, 209 insertions(+), 17 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md index 2948faae9b..f485531b04 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/intersections.md @@ -50,17 +50,13 @@ reveal_type(x) # revealed: LiteralString if x != "abc": reveal_type(x) # revealed: LiteralString & ~Literal["abc"] - # TODO: This should be `Literal[False]` - reveal_type(x == "abc") # revealed: bool - # TODO: This should be `Literal[False]` - reveal_type("abc" == x) # revealed: bool + reveal_type(x == "abc") # revealed: Literal[False] + reveal_type("abc" == x) # revealed: Literal[False] reveal_type(x == "something else") # revealed: bool reveal_type("something else" == x) # revealed: bool - # TODO: This should be `Literal[True]` - reveal_type(x != "abc") # revealed: bool - # TODO: This should be `Literal[True]` - reveal_type("abc" != x) # revealed: bool + reveal_type(x != "abc") # revealed: Literal[True] + reveal_type("abc" != x) # revealed: Literal[True] reveal_type(x != "something else") # revealed: bool reveal_type("something else" != x) # revealed: bool @@ -76,6 +72,34 @@ if x != "abc": reveal_type("abc" in x) # revealed: bool ``` +A negative literal-string constraint does not exclude a runtime string with that value unless the +candidate already has known literal origin. + +```py +from typing import Literal +from typing_extensions import LiteralString +from ty_extensions import Intersection, Not + +def without_literal_origin(value: Intersection[str, Not[LiteralString]]) -> None: + reveal_type(value == "hello") # revealed: bool + reveal_type("hello" == value) # revealed: bool +``` + +A negative string-literal constraint likewise leaves the same runtime value possible, with or +without an explicit `str` constraint. + +```py +def excluded_string_literal(value: Intersection[str, Not[Literal["hello"]]]) -> None: + reveal_type(value == "hello") # revealed: bool + reveal_type("hello" == value) # revealed: bool + reveal_type(value != "hello") # revealed: bool + +def excluded_literal(value: Not[Literal["hello"]]) -> None: + reveal_type(value == "hello") # revealed: bool + reveal_type("hello" == value) # revealed: bool + reveal_type(value != "hello") # revealed: bool +``` + #### Integers ```py diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index d49f28d840..d53a369176 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -1681,6 +1681,59 @@ def preserve_custom_comparison(value: str | AlwaysEqual): reveal_type(value) # revealed: Literal["a"] | AlwaysEqual ``` +## String-literal origin and exclusions + +A string without literal origin can equal a string literal without acquiring the literal's origin. +The successful branch remains reachable and preserves the original exclusion. + +```py +from typing import Literal +from typing_extensions import LiteralString +from ty_extensions import Intersection, Not + +def without_literal_origin(value: Intersection[str, Not[LiteralString]]) -> None: + if value == "hello": + reveal_type(value) # revealed: str & ~LiteralString + value.definitely_missing_attribute # error: [unresolved-attribute] + + if "hello" == value: + reveal_type(value) # revealed: str & ~LiteralString + + if value != "hello": + reveal_type(value) # revealed: str & ~LiteralString + else: + reveal_type(value) # revealed: str & ~LiteralString +``` + +Excluding a particular string literal also leaves its runtime value possible when literal origin is +not known. A different literal can still narrow the string normally. + +```py +def without_literal_value(value: Intersection[str, Not[Literal["hello"]]]) -> None: + if value == "hello": + reveal_type(value) # revealed: str & ~Literal["hello"] + + if value == "goodbye": + reveal_type(value) # revealed: Literal["goodbye"] +``` + +Optional alternatives that cannot compare equal are still removed without discarding the possible +string value. + +```py +def optional_without_literal_origin(value: Intersection[str, Not[LiteralString]] | None) -> None: + if value == "hello": + reveal_type(value) # revealed: str & ~LiteralString +``` + +Once literal origin is known, excluding a string literal really does exclude its runtime value. + +```py +def trusted_value_is_excluded(value: Intersection[LiteralString, Not[Literal["hello"]]]) -> None: + if value == "hello": + reveal_type(value) # revealed: Never +``` + ## `x != y` where `y` is of literal type ```py @@ -2557,7 +2610,8 @@ strict-equality-semantics = true ```py from enum import IntEnum, StrEnum -from typing import Any, Literal +from typing import Any, Literal, LiteralString +from ty_extensions import Intersection, Not def broad(value: str): if value == "a": @@ -2571,6 +2625,13 @@ def inequality(value: str): else: reveal_type(value) # revealed: str +def without_literal_origin(value: Intersection[str, Not[LiteralString]]): + if value == "a": + reveal_type(value) # revealed: str & ~LiteralString + +def trusted_value_is_excluded(value: Intersection[LiteralString, Not[Literal["a"]]]): + reveal_type(value == "a") # revealed: Literal[False] + def literal(value: Literal["a", "b"]): if value == "a": reveal_type(value) # revealed: Literal["a"] diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md index c9528545ee..d960e1018c 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md @@ -1281,8 +1281,12 @@ def _(x: bool | str): ## LiteralString +Known literal-origin strings can safely narrow to the matching members of a literal tuple. + ```py +from typing import Literal from typing_extensions import LiteralString +from ty_extensions import Intersection, Not def _(x: LiteralString): if x in ("a", "b", "c"): @@ -1297,6 +1301,24 @@ def _(x: LiteralString | int): reveal_type(x) # revealed: (LiteralString & ~Literal["a"] & ~Literal["b"] & ~Literal["c"]) | int ``` +A string without literal origin can match a tuple member without gaining that member's origin. + +```py +def without_literal_origin(value: Intersection[str, Not[LiteralString]]) -> None: + if value in ("hello",): + reveal_type(value) # revealed: str & ~LiteralString +``` + +An excluded value cannot appear in a tuple when the candidate already has known literal origin. + +```py +def trusted_value_is_excluded(value: Intersection[LiteralString, Not[Literal["hello"]]]) -> None: + reveal_type(value in ("hello",)) # revealed: Literal[False] + + if value in ("hello",): + reveal_type(value) # revealed: Never +``` + ## enums ```py diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md index f0ea2e73c1..3e69400f8e 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md @@ -526,21 +526,46 @@ def excluded_runtime_class(not_int: Not[int], other: UserId) -> None: ## `is` with string types -Identity comparisons preserve existing `LiteralString` narrowing and do not make negated string -literal comparisons unreachable. +Identity transfers known literal-string origin when the other operand already proves it. ```py from typing import Literal from typing_extensions import LiteralString -from ty_extensions import Not +from ty_extensions import Intersection, Not def literal_string(value: object, text: LiteralString) -> None: if value is text: reveal_type(value) # revealed: LiteralString +``` +A string without known literal origin can have the same runtime value as an excluded string literal. +Identity preserves the existing origin exclusion instead of making the successful branch +unreachable. + +```py def negated_string_literal(value: Not[Literal["hello"]]) -> None: if value is "hello": reveal_type(value) # revealed: ~Literal["hello"] + +def negated_literal_string(value: Intersection[str, Not[LiteralString]]) -> None: + reveal_type(value is "hello") # revealed: bool + + if value is "hello": + reveal_type(value) # revealed: str & ~LiteralString + + if "hello" is value: + reveal_type(value) # revealed: str & ~LiteralString +``` + +When literal origin is already known, excluding a literal string also excludes that runtime value. + +```py +def trusted_value_is_excluded(value: Intersection[LiteralString, Not[Literal["hello"]]]) -> None: + reveal_type(value is "hello") # revealed: Literal[False] + reveal_type("hello" is value) # revealed: Literal[False] + + if value is "hello": + reveal_type(value) # revealed: Never ``` ## `is` with `NewType`s diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/match.md b/crates/ty_python_semantic/resources/mdtest/narrow/match.md index 644b303221..9eebf500bf 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/match.md @@ -3330,6 +3330,30 @@ def test_match_value_sequence(value: object) -> None: reveal_type(value[0]) # revealed: object ``` +## String-literal origin in value patterns + +A string without literal origin can match a literal value pattern without gaining literal origin. + +```py +from typing import Literal +from typing_extensions import LiteralString +from ty_extensions import Intersection, Not + +def without_literal_origin(value: Intersection[str, Not[LiteralString]]) -> None: + match value: + case "hello": + reveal_type(value) # revealed: str & ~LiteralString +``` + +For a known literal-origin string, excluding the same literal makes the value pattern impossible. + +```py +def trusted_value_is_excluded(value: Intersection[LiteralString, Not[Literal["hello"]]]) -> None: + match value: + case "hello": + reveal_type(value) # revealed: Never +``` + ## Enum equality semantics Enum value patterns use the enum class's actual `__eq__` implementation. Members of an enum whose diff --git a/crates/ty_python_semantic/src/types/equality.rs b/crates/ty_python_semantic/src/types/equality.rs index 70e4d4fccb..402a9c8faa 100644 --- a/crates/ty_python_semantic/src/types/equality.rs +++ b/crates/ty_python_semantic/src/types/equality.rs @@ -695,6 +695,23 @@ fn evaluate_structural_comparison<'db>( (other, Type::Union(union)) => { evaluate_union_right(evaluator, other, union.elements(db), branch, operator) } + // An excluded string literal rules out its runtime value only when the intersection + // already proves that the string has literal origin. + (Type::Intersection(intersection), Type::LiteralValue(literal)) + | (Type::LiteralValue(literal), Type::Intersection(intersection)) + if literal.is_string() + && intersection + .positive(db) + .iter() + .any(|element| element.is_subtype_of(db, env, Type::literal_string())) + && Type::Intersection(intersection).is_disjoint_from( + db, + env, + Type::LiteralValue(literal), + ) => + { + operator.result_from_equality(false) + } (Type::Intersection(intersection), other) => evaluate_intersection_left( evaluator, Type::Intersection(intersection), @@ -1301,6 +1318,18 @@ fn evaluate_intersection_left<'db>( ComparisonResult::AlwaysTrue => any_true = true, ComparisonResult::AlwaysFalse => any_false = true, ComparisonResult::CanNarrow(narrowed) => { + // Literal-string origin is a static proof, not a runtime object property. An + // untrusted string can therefore equal a literal even when their static types + // are disjoint. Keep its original proof instead of making that branch unreachable. + if operator.condition_expects_equality(branch) + && original.is_disjoint_from(db, &evaluator.env, narrowed) + && original + .identity_comparison_truthiness(db, &evaluator.env, narrowed) + .may_be_true() + { + return ComparisonResult::Ambiguous; + } + any_narrowing = true; builder.add_positive_in_place(narrowed); } diff --git a/crates/ty_python_semantic/src/types/infer/comparisons.rs b/crates/ty_python_semantic/src/types/infer/comparisons.rs index 58ffe20789..573029e8ca 100644 --- a/crates/ty_python_semantic/src/types/infer/comparisons.rs +++ b/crates/ty_python_semantic/src/types/infer/comparisons.rs @@ -30,8 +30,9 @@ impl<'db> Type<'db> { /// unsound. /// /// Preserve negations that constrain the object itself, such as `~None`, `~SomeClass`, and - /// `~Literal[1]`. A `NewType` tag, type-variable selection, or type-guard proof can differ - /// between views. Retain the existing conservative handling of negated string types. + /// `~Literal[1]`. A `NewType` tag, type-variable selection, type-guard proof, or literal-string + /// origin can differ between views. A negated string literal excludes its runtime value only + /// when another constraint already establishes that the string has literal origin. /// /// A type variable can also hide a `NewType` tag: even a variable bounded by `int` can be /// instantiated as an integer `NewType`. Expand variables to their upcast bounds or constraints @@ -73,20 +74,26 @@ impl<'db> Type<'db> { union.map(db, env, |element| upcast(db, env, *element, visitor)) } Type::Intersection(intersection) => { + let has_literal_string_origin = intersection + .positive(db) + .iter() + .any(|element| element.is_subtype_of(db, env, Type::literal_string())); let mut builder = IntersectionBuilder::new(db, env); for element in intersection.positive(db) { builder = builder.add_positive(upcast(db, env, *element, visitor)); } for element in intersection.negative(db) { - // Static tags and predicate proofs can differ between views. Retain the - // existing conservative handling of negated string types. + // Static tags, predicate proofs, and literal-string origin can differ + // between views. Once literal origin is known, an excluded string literal + // also excludes its runtime value and must be preserved. match element.resolve_type_alias(db) { Type::NewTypeInstance(_) | Type::TypeVar(_) | Type::TypeIs(_) | Type::TypeGuard(_) => continue, Type::LiteralValue(literal) - if literal.is_literal_string() || literal.is_string() => + if literal.is_literal_string() + || literal.is_string() && !has_literal_string_origin => { continue; } From d00a1a27898d4963c593685622332115b4495008 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 10 Aug 2026 16:01:35 -0700 Subject: [PATCH 359/390] [ty] fix mdtest prose about string literal identity checks (#27639) Follow up to missed review comment on https://github.com/astral-sh/ruff/pull/27582. --- .../resources/mdtest/narrow/conditionals/is.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md index 3e69400f8e..18c8a2d437 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md @@ -538,9 +538,9 @@ def literal_string(value: object, text: LiteralString) -> None: reveal_type(value) # revealed: LiteralString ``` -A string without known literal origin can have the same runtime value as an excluded string literal. -Identity preserves the existing origin exclusion instead of making the successful branch -unreachable. +The same string object (same memory address) can be referenced by multiple different expressions +(due to aliasing or interning). Some of those expressions may be validly typed as having literal +origin and others may not. Checking string identity does not assume this is impossible: ```py def negated_string_literal(value: Not[Literal["hello"]]) -> None: From 2a42ea550b9911a9908213397f064665fd99764e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 10 Aug 2026 19:15:56 -0400 Subject: [PATCH 360/390] [ty] Clarify incompatible loop declaration regression (#27640) ## Summary Use an unambiguously incompatible `list[str]` declaration in the loop-declaration regression. The earlier binding remains `list[int]`, while the loop assignment now matches the declared type, so the test isolates the incompatible pre-loop binding. The previous `list[object]` declaration could become valid under whole-scope bidirectional inference, making it an ambiguous example for this diagnostic. Follow-up to [review feedback on #27594](https://github.com/astral-sh/ruff/pull/27594#discussion_r3753906101). --- .../ty_python_semantic/resources/mdtest/declaration/error.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/declaration/error.md b/crates/ty_python_semantic/resources/mdtest/declaration/error.md index 5a228f12e5..b003938e13 100644 --- a/crates/ty_python_semantic/resources/mdtest/declaration/error.md +++ b/crates/ty_python_semantic/resources/mdtest/declaration/error.md @@ -15,8 +15,8 @@ An incompatible binding that predates the loop must still invalidate a declarati values = [1] while True: - values: list[object] # error: [invalid-declaration] - values = [1] + values: list[str] # error: [invalid-declaration] + values = ["a"] ``` ## Incompatible declarations From d08b174e09a23c0a0413b7e7db7dc67d69593eac Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 10 Aug 2026 17:59:23 -0700 Subject: [PATCH 361/390] [ty] Validate boolean conversion in comprehension filters (#27641) ## Summary - Validate boolean conversion for every comprehension filter using the same check as ordinary conditions. - Report `unsupported-bool-conversion` when a comprehension filter has a disabled or incorrectly implemented `__bool__` method. Closes https://github.com/astral-sh/ty/issues/4227. ## Test plan - Add mdtests for list, set, dict, and generator comprehensions, including all asynchronous forms. - Cover multiple filters, filters on later `for` clauses, final operands of boolean expressions, and invalid `__bool__` return types. - The complete semantic mdtest suite passes. --- .../resources/mdtest/comprehensions/basic.md | 69 +++++++++++++++++++ .../src/types/infer/builder.rs | 6 +- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md index c03c1185db..a921f3efa0 100644 --- a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md @@ -24,6 +24,55 @@ class Table: {0: reveal_type(x) for x in range(3)} ``` +## Invalid comprehension filters + +A filter in any comprehension form must support boolean conversion: + +```py +class NotBoolable: + __bool__ = None + +[x for x in range(3) if NotBoolable()] # error: [unsupported-bool-conversion] +{x for x in range(3) if NotBoolable()} # error: [unsupported-bool-conversion] +{x: x for x in range(3) if NotBoolable()} # error: [unsupported-bool-conversion] +(x for x in range(3) if NotBoolable()) # error: [unsupported-bool-conversion] +``` + +Every filter is checked, including filters on subsequent `for` clauses: + +```py +[ + x + for x in range(3) + if NotBoolable() # error: [unsupported-bool-conversion] + if NotBoolable() # error: [unsupported-bool-conversion] +] + +[ + x + for x in range(3) + if NotBoolable() # error: [unsupported-bool-conversion] + for y in range(3) + if NotBoolable() # error: [unsupported-bool-conversion] +] +``` + +The final operand of a boolean expression is converted when it becomes the filter condition: + +```py +[x for x in range(3) if True and NotBoolable()] # error: [unsupported-bool-conversion] +``` + +Filter validation also rejects a `__bool__` method with an invalid return type: + +```py +class InvalidBoolReturn: + def __bool__(self) -> str: + return "invalid" + +[x for x in range(3) if InvalidBoolReturn()] # error: [unsupported-bool-conversion] +``` + ## Nested comprehension ```py @@ -347,6 +396,26 @@ async def _(): [reveal_type(x) async for x in range(3)] ``` +### Invalid async comprehension filters + +Filters in asynchronous comprehensions also require valid boolean conversion: + +```py +from collections.abc import AsyncIterator + +class NotBoolable: + __bool__ = None + +async def items() -> AsyncIterator[int]: + yield 1 + +async def invalid_filters() -> None: + [x async for x in items() if NotBoolable()] # error: [unsupported-bool-conversion] + {x async for x in items() if NotBoolable()} # error: [unsupported-bool-conversion] + {x: x async for x in items() if NotBoolable()} # error: [unsupported-bool-conversion] + (x async for x in items() if NotBoolable()) # error: [unsupported-bool-conversion] +``` + ## Comprehension value type The type of the expression being iterated over is immutable, and so should not be widened with diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index cc9431c9c2..050d6a3d79 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -8194,7 +8194,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }); for expr in ifs { - self.infer_maybe_standalone_expression(expr, TypeContext::default()); + let test_ty = self.infer_maybe_standalone_expression(expr, TypeContext::default()); + + if let Err(err) = test_ty.try_bool(db, env) { + err.report_diagnostic(&self.context, expr); + } } } From 58652e1f6b2cf54a5cb96866e397d5c58e11e894 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 11 Aug 2026 10:54:36 -0400 Subject: [PATCH 362/390] [ty] Validate unpacked callable argument shapes (#27516) ## Summary Previously, we treated unpacked `*args` annotations as ordinary variadic parameters during call binding, allowing missing tuple elements, excess positional arguments, and incompatible element types to pass unchecked: ```python def callback(*args: *tuple[int, str]) -> None: ... callback() # error: [missing-argument] callback(1, 2) # error: [invalid-argument-type] callback(1, "ok", 3) # error: [too-many-positional-arguments] ``` We now align positional arguments with the unpacked tuple's fixed prefix, homogeneous variadic segment, and fixed suffix, preserve each element's expected type through ordinary call checking, and enforce the tuple's minimum and maximum arity. The same path validates callable protocols and specializes bounded type variables, including ordinary type variables beside an unresolved `TypeVarTuple`. --------- Co-authored-by: Dhruv Manilawala --- .../resources/mdtest/annotations/starred.md | 4 +- .../annotations/unsupported_special_forms.md | 4 +- .../resources/mdtest/call/function.md | 173 ++++++++++++++++++ .../mdtest/generics/legacy/unpack.md | 3 +- .../mdtest/generics/pep695/typevartuple.md | 3 +- .../ty_python_semantic/src/types/call/bind.rs | 165 +++++++++++++++-- 6 files changed, 330 insertions(+), 22 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md index 94c8935f4b..2d5e4dd108 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/starred.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/starred.md @@ -25,6 +25,6 @@ reveal_type(append_int()) # revealed: tuple[*tuple[Unknown, ...], int] def first_arg_int(*args: *tuple[int, *tuple[str, ...]]): ... first_arg_int(42, "42", "42") # fine -first_arg_int("not an int", "42", "42") # TODO: should error -first_arg_int(56, "42", 56) # TODO: should error +first_arg_int("not an int", "42", "42") # error: [invalid-argument-type] +first_arg_int(56, "42", 56) # error: [invalid-argument-type] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 46bf83727f..8dc805ed6f 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -45,8 +45,8 @@ def ex3(msg: str): def first_arg_int(*args: Unpack[tuple[int, Unpack[tuple[str, ...]]]]): ... first_arg_int(42, "42", "42") # fine -first_arg_int("not an int", "42", "42") # TODO: should error -first_arg_int(56, "42", 56) # TODO: should error +first_arg_int("not an int", "42", "42") # error: [invalid-argument-type] +first_arg_int(56, "42", 56) # error: [invalid-argument-type] ``` ## Allowed `Unpack` contexts diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index 21bd125c8b..05246e82a1 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -1213,6 +1213,179 @@ def f(*args: int) -> int: reveal_type(f()) # revealed: int ``` +### Unpacked variadic arguments can require positional arguments + +An unpacked tuple can require arguments even though an ordinary variadic parameter can be empty. +Fixed tuples also reject positional arguments beyond their declared length. + +```toml +[environment] +python-version = "3.13" +``` + +```py +def at_least_one(*args: *tuple[*tuple[int, ...], int]) -> None: ... +def exactly_two(*args: *tuple[int, str]) -> None: ... +def exactly_zero(*args: *tuple[()]) -> None: ... + +at_least_one() # error: [missing-argument] +at_least_one(1) +at_least_one(1, 2) +at_least_one("wrong") # error: [invalid-argument-type] +at_least_one(1, "wrong") # error: [invalid-argument-type] + +exactly_two() # error: [missing-argument] +exactly_two(1) # error: [missing-argument] +exactly_two(1, "two") +exactly_two("one", "two") # error: [invalid-argument-type] +exactly_two(1, 2) # error: [invalid-argument-type] +exactly_two(1, "two", 3) # error: [too-many-positional-arguments] + +exactly_zero() +exactly_zero(1) # error: [too-many-positional-arguments] +``` + +### Unpacked variadic arity errors preserve element diagnostics + +Matched tuple elements should still be checked when a call has the wrong arity. + +```toml +[environment] +python-version = "3.13" +``` + +```py +def exactly_two(*args: *tuple[int, str]) -> None: ... + +exactly_two(1, "valid") +exactly_two("wrong", "valid") # error: [invalid-argument-type] + +# TODO: error: [invalid-argument-type] +# error: [missing-argument] +exactly_two("wrong") + +# TODO: error: [invalid-argument-type] +# error: [too-many-positional-arguments] +exactly_two("wrong", "valid", 3) + +# TODO: error: [invalid-argument-type] +# TODO: error: [invalid-argument-type] +# error: [too-many-positional-arguments] +exactly_two("wrong", 2, 3) +``` + +The same recovery should validate fixed prefixes when a required suffix is missing. + +```py +def with_suffix(*args: *tuple[int, *tuple[str, ...], bytes]) -> None: ... + +# TODO: error: [invalid-argument-type] +# error: [missing-argument] +with_suffix("wrong") +``` + +Forwarding a fixed-length tuple should preserve the same element and arity diagnostics. + +```py +def forward(values: tuple[str]) -> None: + # TODO: error: [invalid-argument-type] + # error: [missing-argument] + exactly_two(*values) +``` + +Callable protocols should use the same recovery as ordinary functions. + +```py +from typing import Protocol + +class ExactlyTwo(Protocol): + def __call__(self, *args: *tuple[int, str]) -> None: ... + +def call(callback: ExactlyTwo) -> None: + # TODO: error: [invalid-argument-type] + # error: [missing-argument] + callback("wrong") +``` + +### Unpacked variadic arguments preserve element positions + +Fixed prefixes, a homogeneous variadic segment, and fixed suffixes each retain their own argument +types. A required suffix also requires any preceding defaulted positional parameter to be filled. + +```toml +[environment] +python-version = "3.13" +``` + +```py +def mixed(*args: *tuple[int, *tuple[str, ...], bytes]) -> None: ... +def with_default(first: int = 0, *args: *tuple[*tuple[int, ...], int]) -> None: ... + +mixed(1, b"last") +mixed(1, "middle", b"last") +mixed("first", b"last") # error: [invalid-argument-type] +mixed(1, 2, b"last") # error: [invalid-argument-type] +mixed(1, "middle", "last") # error: [invalid-argument-type] + +with_default() # error: [missing-argument] +with_default(first=1) # error: [missing-argument] +with_default(1) # error: [missing-argument] +with_default(1, 2) +``` + +### Unpacked variadic elements preserve generic bounds + +Ordinary type variables are inferred from individual unpacked elements, even beside an unresolved +type-variable tuple. Their upper bounds remain enforced. + +```toml +[environment] +python-version = "3.13" +``` + +```py +def fixed[T: str](*args: *tuple[T]) -> T: + return args[0] + +def suffix[T: str, *Ts](*args: *tuple[*Ts, T]) -> T: + return args[-1] + +reveal_type(fixed("valid")) # revealed: Literal["valid"] +fixed(1) # error: [invalid-argument-type] + +reveal_type(suffix("prefix", "valid")) # revealed: Literal["valid"] +suffix("prefix", 1) # error: [invalid-argument-type] +``` + +### Callable protocols enforce unpacked variadic requirements + +Calling a callable protocol uses the same tuple element types and argument-count bounds as calling +an ordinary function. + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import Protocol + +class AtLeastOne(Protocol): + def __call__(self, *args: *tuple[*tuple[int, ...], int]) -> None: ... + +class ExactlyOne(Protocol): + def __call__(self, *args: *tuple[int]) -> None: ... + +def call(at_least_one: AtLeastOne, exactly_one: ExactlyOne) -> None: + at_least_one() # error: [missing-argument] + at_least_one(1) + at_least_one("wrong") # error: [invalid-argument-type] + + exactly_one(1) + exactly_one() # error: [missing-argument] + exactly_one(1, 2) # error: [too-many-positional-arguments] +``` + ### Keywords argument is not required ```py diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/unpack.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/unpack.md index 5d1c336913..435ca08fe7 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/unpack.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/unpack.md @@ -162,8 +162,7 @@ def accept( accept(True, "phase", "status", b"ok") accept(True, b"ok") -# TODO: error: [invalid-argument-type] "Argument to function `accept` is incorrect: Expected `tuple[bool, *tuple[str, ...], bytes]`" -accept(True, 1, b"bad") +accept(True, 1, b"bad") # error: [invalid-argument-type] ``` ## Defaults diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md index 0c1ee74061..dea784f91f 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/typevartuple.md @@ -791,8 +791,7 @@ def remove_bytes[*Prefix](*args: *tuple[*Prefix, bytes]) -> tuple[*Prefix]: accept_str_in_between(True, "phase", "status", b"ok") accept_str_in_between(True, b"ok") -# TODO: error: [invalid-argument-type] "Argument to function `accept_str_in_between` is incorrect: Expected `tuple[bool, *tuple[str, ...], bytes]`" -accept_str_in_between(True, 1, b"bad") +accept_str_in_between(True, 1, b"bad") # error: [invalid-argument-type] # TODO: Infer the `TypeVarTuple` from arguments matched to the variadic parameter. reveal_type(remove_bytes(1, "record", b"sum")) # revealed: tuple[Unknown, ...] diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index a0de38ac55..cf43da069a 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -4706,6 +4706,8 @@ struct ArgumentMatcher<'a, 'db> { next_positional: usize, first_excess_positional: Option, num_synthetic_args: usize, + /// Forwarded argument indices and the lengths of their fixed tuple prefixes and suffixes. + variable_length_positional_arguments: SmallVec<[(usize, usize, usize); 1]>, variadic_argument_matched_to_variadic_parameter: bool, /// Parameter indices that have explicit keyword arguments (e.g., `foo=value`). @@ -4741,6 +4743,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { next_positional: 0, first_excess_positional: None, num_synthetic_args: 0, + variable_length_positional_arguments: SmallVec::new(), variadic_argument_matched_to_variadic_parameter: false, explicit_keyword_parameters, } @@ -4803,6 +4806,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { matched_argument.parameters.push(MatchedParameter { index: parameter_index, argument_type, + expected_type: None, provenance, }); matched_argument.matched = true; @@ -5034,6 +5038,10 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { // `variable_element.is_some()`) or if we have a union of different fixed-length tuples (in // which case `variable_element.is_none()`). let is_variable = length.is_variable(); + if let TupleLength::Variable(prefix, suffix) = length { + self.variable_length_positional_arguments + .push((argument_index, prefix, suffix)); + } let has_fixed_union_tail = is_variable && variable_element.is_none(); // We must be able to match up the fixed-length portion of the argument with positional @@ -5214,6 +5222,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { matched_argument.parameters.push(MatchedParameter { index: parameter_index, argument_type: Some(extra_items_ty), + expected_type: None, provenance: InvalidArgumentTypeProvenance::Argument, }); } @@ -5242,7 +5251,123 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { } } - fn finish(self) -> Box<[MatchedArgument<'db>]> { + /// Checks the positional requirements encoded inside an unpacked variadic annotation. + /// + /// Unlike ordinary `*args`, an unpacked tuple can require arguments and prescribe a different + /// type for each position: + /// + /// ```python + /// def callback(*args: *tuple[int, *tuple[str, ...], bytes]) -> None: ... + /// + /// callback(1, b"last") + /// callback(1, "middle", b"last") + /// ``` + /// + /// Store each matched tuple element separately so ordinary argument checking and inference can + /// use its type instead of the complete tuple annotation. + fn match_unpacked_variadic( + &mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + missing: &mut Vec, + ) { + let Some((parameter_index, parameter)) = self.parameters.variadic() else { + return; + }; + if !parameter.has_starred_annotation() { + return; + } + let Some(tuple) = parameter.annotated_type().exact_tuple_instance_spec(db) else { + return; + }; + + let maximum = tuple.len().maximum(); + let mut argument_count = 0; + let mut first_variable = None; + let mut last_variable = None; + let mut first_excess_argument_index = None; + + for (argument_index, argument) in self.argument_matches.iter().enumerate() { + let match_count = argument.parameters.len(); + let variable_segment = self + .variable_length_positional_arguments + .iter() + .find(|(index, _, _)| *index == argument_index); + + for (position, matched) in argument.parameters.iter().enumerate() { + if matched.index != parameter_index { + continue; + } + + if maximum == Some(argument_count) { + first_excess_argument_index = self.get_argument_index(argument_index); + } + + if variable_segment.is_some_and(|(_, prefix, suffix)| { + position >= *prefix && position < match_count.saturating_sub(*suffix) + }) { + if first_variable.is_none() { + first_variable = Some(argument_count); + } + last_variable = Some(argument_count); + } + + argument_count += 1; + } + } + + let argument_length = first_variable + .zip(last_variable) + .map_or(TupleLength::Fixed(argument_count), |(first, last)| { + TupleLength::Variable(first, argument_count.saturating_sub(last + 1)) + }); + + if !argument_length.is_variable() && argument_count < tuple.len().minimum() { + missing.push(ParameterContext::new(parameter, parameter_index, false)); + // TODO: Check matched tuple elements even when required elements are missing. + return; + } + + if let Some(maximum) = maximum + && argument_length.minimum() > maximum + { + self.errors.push(BindingError::TooManyPositionalArguments { + first_excess_argument_index, + expected_positional_count: self.parameters.positional().count() + maximum, + provided_positional_count: self.next_positional, + }); + // TODO: Check matched tuple elements without inferring from excess arguments. + return; + } + + let Ok(expected) = tuple.resize(db, env, argument_length) else { + return; + }; + let variable_type = expected.variable_element_type(db); + let mut expected_types = expected.iter_element_types(db); + for (position, matched) in self + .argument_matches + .iter_mut() + .flat_map(|argument| argument.parameters.iter_mut()) + .filter(|matched| matched.index == parameter_index) + .enumerate() + { + matched.expected_type = if first_variable + .zip(last_variable) + .is_some_and(|(first, last)| position > first && position <= last) + { + variable_type + } else { + expected_types.next() + }; + } + } + + fn finish( + mut self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + ) -> Box<[MatchedArgument<'db>]> { if let Some(first_excess_argument_index) = self.first_excess_positional { self.errors.push(BindingError::TooManyPositionalArguments { first_excess_argument_index: self.get_argument_index(first_excess_argument_index), @@ -5280,6 +5405,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { missing.push(ParameterContext::new(param, index, false)); } } + self.match_unpacked_variadic(db, env, &mut missing); if !missing.is_empty() { self.errors.push(BindingError::MissingArguments { parameters: ParameterContexts(missing), @@ -5833,7 +5959,9 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { continue; } - let formal = parameters[parameter_index].annotated_type(); + let formal = matched_parameter + .expected_type + .unwrap_or_else(|| parameters[parameter_index].annotated_type()); let actual = matched_parameter .argument_type .unwrap_or_else(|| argument_types.get_for_declared_type(formal)); @@ -5865,15 +5993,16 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for matched_parameter in self.argument_matches[argument_index].iter() { let parameter_index = matched_parameter.index; let parameter = ¶meters[parameter_index]; - let declared_type = parameter.annotated_type(); + let parameter_type = parameter.annotated_type(); // TODO: Infer a `TypeVarTuple` from all matched positional arguments as a single - // tuple. Until then, skip per-argument inference. + // tuple. Fixed elements beside that pack can still infer ordinary type variables. if parameter.has_starred_annotation() + && matched_parameter.expected_type.is_none() && (matches!( - declared_type, + parameter_type, Type::TypeVar(typevar) if typevar.is_typevartuple(db) ) || matches!( - declared_type.exact_tuple_instance_spec(db).as_deref(), + parameter_type.exact_tuple_instance_spec(db).as_deref(), Some(TupleSpec::Variable(variable)) if matches!( variable.variable(), @@ -5887,6 +6016,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { continue; } + let declared_type = matched_parameter.expected_type.unwrap_or(parameter_type); let argument_type = argument_types.get_for_declared_type(declared_type); let specialization_result = builder.infer( declared_type, @@ -5968,7 +6098,9 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { Type::SubclassOf(subclass_of) if subclass_of.into_type_var().is_some() ); - let mut expected_ty = parameter.annotated_type(); + let mut expected_ty = matched_parameter + .expected_type + .unwrap_or_else(|| parameter.annotated_type()); if let Some(specialization) = self.specialization() { if !constructor_receiver { argument_type = argument_type.apply_specialization(db, specialization); @@ -6005,10 +6137,10 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // constraint set that we get from this assignability check, instead of inferring and // building them in an earlier separate step. // - // TODO: handle starred annotations, e.g. `*args: *Ts` or `*args: *tuple[int, *tuple[str, ...]]` + // An unresolved `*Ts` still has no per-element expected type. if !self.constraint_set_errors[argument_index] && !constructor_receiver - && !parameter.has_starred_annotation() + && (!parameter.has_starred_annotation() || matched_parameter.expected_type.is_some()) && !is_valid_isinstance_target() && argument_type .when_assignable_to( @@ -6530,6 +6662,9 @@ pub struct MatchedParameter<'db> { /// matching runs. argument_type: Option>, + /// The tuple element expected at this position in an unpacked variadic parameter. + expected_type: Option>, + /// Why this parameter match exists. provenance: InvalidArgumentTypeProvenance, } @@ -7034,13 +7169,15 @@ impl<'db> Binding<'db> { specialization: Option>, ) -> Option> { let argument_matches = self.matched_argument_for_call_argument(binding, argument_index)?; - let [parameter] = argument_matches.parameters.as_slice() else { + let [matched_parameter] = argument_matches.parameters.as_slice() else { return None; }; - let parameter = &self.signature.parameters()[parameter.index]; - let mut parameter_type = parameter.annotated_type(); - let original_parameter_type = parameter_type; + let parameter = &self.signature.parameters()[matched_parameter.index]; + let original_parameter_type = parameter.annotated_type(); + let mut parameter_type = matched_parameter + .expected_type + .unwrap_or(original_parameter_type); let paramspec_callable = |paramspec| { let Type::Callable(callable) = self .specialization(db) @@ -7268,7 +7405,7 @@ impl<'db> Binding<'db> { self.parameter_tys = vec![None; parameters.len()].into_boxed_slice(); self.variadic_argument_matched_to_variadic_parameter = matcher.variadic_argument_matched_to_variadic_parameter; - self.argument_matches = matcher.finish(); + self.argument_matches = matcher.finish(db, env); } fn check_types( From 96737aa506c035aa603e4e45c716f2e497aca8a1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 11 Aug 2026 11:09:47 -0400 Subject: [PATCH 363/390] Use Depot runners for release builds (#27627) ## Summary Move source distributions and Linux release builds onto pinned, four-core Ubuntu 24.04 Depot runners. This preserves the CPU allocation of the GitHub-hosted runners they replace while avoiding GitHub runner queues; recent Ruff releases were limited by native Linux, Docker, or publishing work rather than cross-build CPU capacity. --- .github/workflows/build-binaries.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index d2c874149a..0b55bbbd7a 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -38,7 +38,7 @@ env: jobs: sdist: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04-4 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -265,7 +265,7 @@ jobs: linux: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04-4 strategy: matrix: target: @@ -401,7 +401,7 @@ jobs: linux-cross: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04-4 strategy: matrix: platform: @@ -485,7 +485,7 @@ jobs: musllinux: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04-4 strategy: matrix: target: @@ -546,7 +546,7 @@ jobs: musllinux-cross: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04-4 strategy: matrix: platform: From a2e887a9d3518c7f29b872a2f514b45e7f1064ac Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 11 Aug 2026 08:52:22 -0700 Subject: [PATCH 364/390] [ty] Preserve constrained TypeVars when slicing (#27645) ## Summary - Preserve a constrained type variable when subscripting each declared constraint returns that same constraint. - Share alternative handling between unions and constrained type variables while retaining existing diagnostics and recursive-union behavior. - Infer precise unions when slicing constrained tuples. Mypy handles these constrained typevar situations by actually checking the entire function once per constraint type, I think. We may eventually decide to do the same, but for now we already have precedent for this approach in binary ops inference, and it seems to work well. Fixes astral-sh/ty#4226. ## Test plan - Mdtests cover overlapping `list` and `Sequence` constraints, distinct slice-preserving implementations, and legacy constrained `TypeVar` declarations on Python 3.10. - Mdtests retain errors for type-changing slices, upper-bounded `Sequence` variables, and unsupported constrained alternatives. - Existing tuple-slicing coverage verifies the newly precise union of slice results. --- .../resources/mdtest/subscript/typevar.md | 114 ++++++++++++++++-- .../ty_python_semantic/src/types/subscript.rs | 72 +++++++---- 2 files changed, 151 insertions(+), 35 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/subscript/typevar.md b/crates/ty_python_semantic/resources/mdtest/subscript/typevar.md index e2917c141c..dffeb7095c 100644 --- a/crates/ty_python_semantic/resources/mdtest/subscript/typevar.md +++ b/crates/ty_python_semantic/resources/mdtest/subscript/typevar.md @@ -1,14 +1,14 @@ # Subscripts involving type variables -## TypeVar bound/constrained to a tuple/int-literal/bool-literal - -The upper bounds of type variables are considered when analysing subscripts. - ```toml [environment] python-version = "3.12" ``` +## TypeVar bound/constrained to a tuple/int-literal/bool-literal + +The upper bounds of type variables are considered when analysing subscripts. + ```py from typing_extensions import TypeAlias, Literal @@ -41,21 +41,114 @@ def f[ # but it's hard to do that without introducing false positives elsewhere reveal_type(tuple_1[some_integer]) # revealed: str | int | bytes - # TODO: would ideally be `tuple[str, int] | tuple[int, bytes]` - reveal_type(tuple_2[:2]) # revealed: tuple[str | int | bytes, ...] + reveal_type(tuple_2[:2]) # revealed: tuple[str, int] | tuple[int, bytes] reveal_type(tuple_2[zero]) # revealed: str | int reveal_type(tuple_2[some_integer]) # revealed: str | int | bytes # fmt: on ``` -## TypeVars +## Slicing overlapping constrained sequence types + +A value-constrained type variable selects one declared constraint for the entire function call. +Slicing a `list` or a `Sequence` preserves the selected constraint, even though `list` is also a +subtype of `Sequence`. + +```py +from collections.abc import Sequence + +def slice_sequence[T: (list[int], Sequence[int])](value: T) -> T: + reveal_type(value[:2]) # revealed: T@slice_sequence + return value[:2] +``` + +## Slicing constrained types with distinct implementations + +Each constraint's `__getitem__` method must be called with its own receiver. When both methods +return their corresponding constraint, the result preserves the original type variable. + +```py +class First: + def __getitem__(self, index: slice) -> "First": + return self + +class Second: + def __getitem__(self, index: slice) -> "Second": + return self + +def slice_value[T: (First, Second)](value: T) -> T: + return value[:2] +``` + +## Slicing a legacy constrained type variable + +Legacy `TypeVar` declarations preserve the selected constraint in the same way as PEP 695 type +parameters. ```toml [environment] -python-version = "3.12" +python-version = "3.10" ``` +```py +from collections.abc import Sequence +from typing import TypeVar + +T = TypeVar("T", list[int], Sequence[int]) + +def slice_sequence(value: T) -> T: + return value[:2] +``` + +## Slicing a constrained type can change its type + +A result cannot retain the constrained type variable when one constraint's slice returns a different +type. + +```py +class First: + def __getitem__(self, index: slice) -> "First": + return self + +class ChangesType: + def __getitem__(self, index: slice) -> First: + return First() + +def slice_value[T: (First, ChangesType)](value: T) -> T: + reveal_type(value[:2]) # revealed: First + # error: [invalid-return-type] + return value[:2] +``` + +## Slicing an upper-bounded type variable + +An upper-bounded type variable can specialize to a `Sequence` subclass whose slice returns a +different sequence type, so the slice is not guaranteed to preserve the type variable. + +```py +from collections.abc import Sequence + +def slice_sequence[T: Sequence[int]](value: T) -> T: + # error: [invalid-return-type] + return value[:2] +``` + +## Subscripting an unsupported constrained type + +A subscript remains invalid when any declared constraint does not support it. + +```py +class Sliceable: + def __getitem__(self, index: slice) -> "Sliceable": + return self + +def slice_value[T: (Sliceable, int)](value: T) -> None: + # error: [not-subscriptable] + value[:2] +``` + +## TypeVars + ```py from typing import Protocol @@ -68,11 +161,6 @@ def f[K: SupportsLessThan](dictionary: dict[K, int], key: K): ## ParamSpecs -```toml -[environment] -python-version = "3.12" -``` - ```py from typing import Callable diff --git a/crates/ty_python_semantic/src/types/subscript.rs b/crates/ty_python_semantic/src/types/subscript.rs index 24bd6a55a9..806d46edf0 100644 --- a/crates/ty_python_semantic/src/types/subscript.rs +++ b/crates/ty_python_semantic/src/types/subscript.rs @@ -24,7 +24,7 @@ use super::instance::SliceLiteral; use super::special_form::SpecialFormType; use super::{ ClassLiteral, IntersectionBuilder, IntersectionType, KnownInstanceType, Type, TypeAliasType, - TypedDictType, UnionBuilder, UnionType, todo_type, + TypeVarBoundOrConstraints, TypedDictType, UnionBuilder, todo_type, }; /// The kind of subscriptable type that had an out-of-bounds index. @@ -385,25 +385,27 @@ impl<'db> SubscriptErrorKind<'db> { } } -fn map_union_subscript<'db, F>( +/// Preserve a constrained type variable when each alternative's result matches that constraint. +fn map_subscript_alternatives<'db>( db: &'db dyn Db, env: &ProgramEnvironment<'db>, - union: UnionType<'db>, - mut map_fn: F, -) -> Result, SubscriptError<'db>> -where - F: FnMut(Type<'db>) -> Result, SubscriptError<'db>>, -{ + full_object_ty: Type<'db>, + alternatives: impl IntoIterator>, + mut map_fn: impl FnMut(Type<'db>) -> Result, SubscriptError<'db>>, +) -> Result, SubscriptError<'db>> { let mut builder = UnionBuilder::new(db, env); let mut errors = Vec::new(); + let mut preserves_typevar = matches!(full_object_ty, Type::TypeVar(_)); - for element in union.elements(db) { - match map_fn(*element) { + for element in alternatives { + match map_fn(element) { Ok(result) => { + if preserves_typevar { + preserves_typevar = result.is_equivalent_to(db, env, element); + } builder = builder.add(result); } Err(error) => { - let full_object_ty = Type::Union(union); builder = builder.add(error.result_type()); errors.extend( error @@ -415,12 +417,17 @@ where } } - builder = builder.recursively_defined(union.recursively_defined(db)); - let result_ty = builder.build(); + if let Type::Union(union) = full_object_ty { + builder = builder.recursively_defined(union.recursively_defined(db)); + } if errors.is_empty() { - Ok(result_ty) + Ok(if preserves_typevar { + full_object_ty + } else { + builder.build() + }) } else { - Err(SubscriptError::with_errors(result_ty, errors)) + Err(SubscriptError::with_errors(builder.build(), errors)) } } @@ -581,13 +588,21 @@ impl<'db> Type<'db> { Some(value_ty.subscript(db, env, alias.value_type(db), expr_context)) } - (Type::Union(union), _) => Some(map_union_subscript(db, env, union, |element| { - element.subscript(db, env, slice_ty, expr_context) - })), + (Type::Union(union), _) => Some(map_subscript_alternatives( + db, + env, + value_ty, + union.elements(db).iter().copied(), + |element| element.subscript(db, env, slice_ty, expr_context), + )), - (_, Type::Union(union)) => Some(map_union_subscript(db, env, union, |element| { - value_ty.subscript(db, env, element, expr_context) - })), + (_, Type::Union(union)) => Some(map_subscript_alternatives( + db, + env, + slice_ty, + union.elements(db).iter().copied(), + |element| value_ty.subscript(db, env, element, expr_context), + )), (Type::EnumComplement(complement), _) => { Some(complement.remaining_literal_union(db, env).subscript( @@ -619,6 +634,19 @@ impl<'db> Type<'db> { |element| value_ty.subscript(db, env, element, expr_context), )), + (Type::TypeVar(typevar), _) + if let Some(TypeVarBoundOrConstraints::Constraints(constraints)) = + typevar.typevar(db).bound_or_constraints(db, env) => + { + Some(map_subscript_alternatives( + db, + env, + value_ty, + constraints.elements(db).iter().copied(), + |constraint| constraint.subscript(db, env, slice_ty, expr_context), + )) + } + // Ex) Given `person["name"]`, return `str` (Type::TypedDict(typed_dict), _) if expr_context != ast::ExprContext::Store => { Some(typed_dict_subscript(db, env, typed_dict, slice_ty)) @@ -849,7 +877,7 @@ impl<'db> Type<'db> { Some(Ok(todo_type!("Inference of subscript on special form"))) } - // TODO: more complex logic required for the `Type::TypeVar(_) branch! + // Upper-bounded and unconstrained type variables use ordinary method lookup. ( Type::FunctionLiteral(_) | Type::WrapperDescriptor(_) From 3436ac3e6a16ed8254c084bc316b60afa0c8a17e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 11 Aug 2026 12:21:35 -0400 Subject: [PATCH 365/390] Pin Depot runners to Ubuntu 24.04 (#27656) ## Summary Previously, our Depot-backed CI and release jobs used floating Ubuntu runner labels. We now pin the four-core release runners and eight-core CI runners to Ubuntu 24.04, update actionlint accordingly, and keep the cargo-dist runner configuration aligned with the generated release workflow. The existing release approval gate and checksum-verified cargo-dist installation remain unchanged. --- .github/actionlint.yaml | 2 +- .github/workflows/ci.yaml | 4 ++-- .github/workflows/release.yml | 8 ++++---- dist-workspace.toml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 1fd2d87add..52c63a57d8 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -5,7 +5,7 @@ self-hosted-runner: # Various runners we use that aren't recognized out-of-the-box by actionlint: labels: - depot-ubuntu-24.04-4 - - depot-ubuntu-latest-8 + - depot-ubuntu-24.04-8 - depot-ubuntu-22.04-16 - depot-ubuntu-22.04-32 - namespace-profile-macos-15 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d2d277ba52..197bc00015 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -491,7 +491,7 @@ jobs: cargo-build-msrv: name: "cargo build (msrv)" - runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }} + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-24.04-8' || 'ubuntu-latest' }} needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 20 @@ -625,7 +625,7 @@ jobs: ecosystem: name: "ecosystem" - runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }} + runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-24.04-8' || 'ubuntu-latest' }} needs: determine_changes # Only runs on pull requests, since that is the only we way we can find the base version for comparison. # Ecosystem check needs linter and/or formatter changes. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c27b4ef83..ae14d2744d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,7 +71,7 @@ jobs: # Run 'dist plan' (or host) to determine what tasks we need to do plan: - runs-on: "depot-ubuntu-latest-4" + runs-on: "depot-ubuntu-24.04-4" outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ (inputs.tag != 'dry-run' && inputs.tag) || '' }} @@ -153,7 +153,7 @@ jobs: - custom-build-binaries - custom-build-docker - custom-build-wasm - runs-on: "depot-ubuntu-latest-4" + runs-on: "depot-ubuntu-24.04-4" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json @@ -206,7 +206,7 @@ jobs: if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-binaries.result == 'skipped' || needs.custom-build-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') && (needs.custom-build-wasm.result == 'skipped' || needs.custom-build-wasm.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: "depot-ubuntu-latest-4" + runs-on: "depot-ubuntu-24.04-4" outputs: val: ${{ steps.host.outputs.manifest }} steps: @@ -304,7 +304,7 @@ jobs: # `custom-publish-crates` is intentionally not a dependency because a crates.io outage should # not block the rest of the release. if: ${{ always() && needs.host.result == 'success' && needs.release-gate.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') && (needs.custom-publish-wasm.result == 'skipped' || needs.custom-publish-wasm.result == 'success') }} - runs-on: "depot-ubuntu-latest-4" + runs-on: "depot-ubuntu-24.04-4" permissions: "attestations": "write" "contents": "write" diff --git a/dist-workspace.toml b/dist-workspace.toml index 34c319bc25..103921b8af 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -74,7 +74,7 @@ hosting = ["simple", "github"] simple-download-url = "https://releases.astral.sh/github/ruff/releases/download/{tag}" [dist.github-custom-runners] -global = "depot-ubuntu-latest-4" +global = "depot-ubuntu-24.04-4" [dist.github-action-commits] "actions/checkout" = "de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2 From 490f2b3aec96b9eb68470a1b453f6229f49b7743 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 11 Aug 2026 12:54:22 -0400 Subject: [PATCH 366/390] Use a four-core Depot runner for Linux ARM64 PGO builds (#27657) ## Summary Previously, our Linux ARM64 PGO release build took 15m 26s on a four-core GitHub runner. We now use a pinned, four-core Ubuntu 24.04 Depot ARM runner, which completed the same build in 13m 39s. An eight-core Depot runner finished in 12m 18s, but the existing Windows PGO lane completed only 14 seconds before the four-core ARM build, so additional ARM cores would not meaningfully shorten the release. --- .github/actionlint.yaml | 1 + .github/workflows/build-binaries.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 52c63a57d8..a57d94905e 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -11,6 +11,7 @@ self-hosted-runner: - namespace-profile-macos-15 - namespace-profile-windows-2022-x86-64-16x32 - depot-ubuntu-22.04-arm-4 + - depot-ubuntu-24.04-arm-4 - github-windows-2025-x86_64-8 - github-windows-2025-x86_64-16 - codspeed-macro diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 0b55bbbd7a..a8d13e2319 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -335,7 +335,7 @@ jobs: linux-aarch64: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: ubuntu-24.04-arm + runs-on: depot-ubuntu-24.04-arm-4 env: # see https://github.com/astral-sh/ruff/issues/3791 # and https://github.com/gnzlbg/jemallocator/issues/170#issuecomment-1503228963 From ed768f07cc536b54c0c8bc333831497110ba0499 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 11 Aug 2026 09:58:19 -0700 Subject: [PATCH 367/390] [ty] Preserve enum attributes on Self and bounded type variables (#27644) ## Summary - Delegate bounded and constrained type-variable member lookup to the complete lookup behavior of each bound or constraint while preserving the original receiver for descriptors and `Self`. - Restore enum-derived `name`, `_name_`, `value`, and `_value_` attributes for implicit `self`, explicit `Self`, and enum-bounded or enum-constrained type variables. - Preserve class-object lookup, mixed instance/class-object constraints, custom enum-property overrides, and classmethod binding for narrowed `Self` and mixin receivers. - Fixes https://github.com/astral-sh/ty/issues/4229. ## Test plan - Extend enum mdtests to cover implicit `self`, explicit `Self`, explicitly annotated enum receivers, enum-bounded and enum-constrained type variables, special name/value attributes, explicitly annotated `_value_`, custom `enum.property` overrides, strict return checking, and literal-preserving `Self` return values. - Add generic mdtest coverage for type variables constrained to both ordinary instances and class objects. - Extend `Self` mdtests to cover truthiness-narrowed classmethod receivers and mixins narrowed to unrelated classes. --------- Co-authored-by: Alex Waygood --- .../resources/mdtest/annotations/self.md | 30 +++++ .../resources/mdtest/enums.md | 115 +++++++++++++++++- .../mdtest/generics/legacy/functions.md | 24 ++++ crates/ty_python_semantic/src/types.rs | 60 +++++---- 4 files changed, 203 insertions(+), 26 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 57fa7b068c..1305e82f79 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -489,6 +489,36 @@ class Child(Parent): assert_type(self.create(), Self) ``` +Truthiness narrowing must also preserve `Self` when an instance accesses a class method. + +```py +from typing import Self, assert_type + +class MaybeEmpty: + @classmethod + def create(cls, other: Self) -> Self: + return cls() + + def copy_if_empty(self, other: Self) -> Self: + if not self: + assert_type(self.create(other), Self) + return self.create(other) + return self +``` + +A mixin narrowed to an unrelated class can also call that class's class methods. + +```py +class Base: + @classmethod + def warn(cls) -> None: ... + +class Mixin: + def method(self) -> None: + assert isinstance(self, Base) + self.warn() +``` + ## Attributes ```py diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md index 8f9d959b0c..acce0c731e 100644 --- a/crates/ty_python_semantic/resources/mdtest/enums.md +++ b/crates/ty_python_semantic/resources/mdtest/enums.md @@ -375,7 +375,8 @@ class Color(Enum): PURPLE = [] # error: [invalid-assignment] ``` -When `_value_` is annotated, `.value` and `._value_` are inferred as the declared type: +When `_value_` is annotated, `.value` and `._value_` are inferred as the declared type on both enum +members and method receivers: ```py from enum import Enum @@ -386,6 +387,11 @@ class Color2(Enum): RED = 1 GREEN = 2 + def read_value(self) -> int: + reveal_type(self._value_) # revealed: int + reveal_type(self.value) # revealed: int + return self.value + reveal_type(Color2.RED.value) # revealed: int reveal_type(Color2.RED._value_) # revealed: int @@ -1187,6 +1193,9 @@ class Choices(Enum): @enum_property def value(self) -> Any: ... + def read_value(self) -> Any: + reveal_type(self.value) # revealed: Any + return self.value reveal_type(Choices.A.value) # revealed: Any @@ -1198,6 +1207,10 @@ class BaseChoices(Enum): class InheritedChoices(BaseChoices): A = 1 + def read_value(self) -> str: + reveal_type(self.value) # revealed: str + return self.value + reveal_type(InheritedChoices.A.value) # revealed: str ``` @@ -2206,6 +2219,106 @@ def _(answer: Answer): reveal_type(answer.value) # revealed: Literal["yes", "no"] ``` +### Special attributes on method receivers + +Implicit receivers and receivers annotated with `Self` retain the special attributes of their enum +bound. Their `Self` type preserves the particular member at call sites. + +```toml +[environment] +python-version = "3.11" + +[rules] +unsound-return-statement = "error" +``` + +```py +from enum import Enum +from typing import Self + +class Answer(Enum): + YES = 1 + NO = 2 + + def implicit(self) -> int: + reveal_type(self) # revealed: Self@implicit + reveal_type(self.name) # revealed: Literal["YES", "NO"] + reveal_type(self._name_) # revealed: Literal["YES", "NO"] + reveal_type(self.value) # revealed: Literal[1, 2] + reveal_type(self._value_) # revealed: Literal[1, 2] + return self.value + + def explicit(self: Self) -> int: + reveal_type(self.value) # revealed: Literal[1, 2] + return self.value + + def concrete(self: "Answer") -> int: + reveal_type(self.value) # revealed: Literal[1, 2] + return self.value + + def identity(self) -> Self: + return self + +reveal_type(Answer.YES.identity()) # revealed: Literal[Answer.YES] +``` + +### Special attributes on bounded type variables + +An ordinary type variable bounded by an enum has the same special attributes as the enum itself. + +```toml +[rules] +unsound-return-statement = "error" +``` + +```py +from enum import Enum +from typing import TypeVar + +class Answer(Enum): + YES = 1 + NO = 2 + +AnswerT = TypeVar("AnswerT", bound=Answer) + +def value(answer: AnswerT) -> int: + reveal_type(answer.name) # revealed: Literal["YES", "NO"] + reveal_type(answer._name_) # revealed: Literal["YES", "NO"] + reveal_type(answer.value) # revealed: Literal[1, 2] + reveal_type(answer._value_) # revealed: Literal[1, 2] + return answer.value +``` + +### Special attributes on constrained type variables + +When a type variable can be one of several enum types, its special attributes include the values +from every possible enum. + +```toml +[rules] +unsound-return-statement = "error" +``` + +```py +from enum import Enum +from typing import TypeVar + +class Number(Enum): + ONE = 1 + TWO = 2 + +class Word(Enum): + LEFT = "left" + RIGHT = "right" + +EnumT = TypeVar("EnumT", Number, Word) + +def value(item: EnumT) -> int | str: + reveal_type(item.name) # revealed: Literal["ONE", "TWO", "LEFT", "RIGHT"] + reveal_type(item.value) # revealed: Literal[1, 2, "left", "right"] + return item.value +``` + ## Properties of enum types ### Implicitly final diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index 447b34ca48..ac6faa6d07 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -933,6 +933,30 @@ def union_bound(cls: U) -> None: reveal_type(cls.attr) # revealed: str | int ``` +## Attribute access on TypeVars constrained to instances and class objects + +A constrained type variable can contain both ordinary instances and class objects. Accessing a +shared attribute must inspect each constraint without treating the entire type variable as a class. + +```py +from typing import TypeVar + +class Instance: + @staticmethod + def keys() -> list[str]: + return [] + +class ClassObject: + @staticmethod + def keys() -> list[str]: + return [] + +T = TypeVar("T", Instance, type[ClassObject]) + +def read(value: T) -> list[str]: + return value.keys() +``` + ## Solving TypeVars with upper bounds in unions ```py diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3eaaf7d4cd..6e5a36d816 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4263,8 +4263,21 @@ impl<'db> Type<'db> { fallback: MemberLookupResult<'db>, policy: InstanceFallbackShadowsNonDataDescriptor, ) -> MemberLookupResult<'db> { - let ty = key.ty(db); let meta_attr_plain = Self::instance_lookup_class_member_with_policy(db, env, key); + // A TypeVar retains its class identity when lookup is delegated to its bound, including + // after narrowing. Narrowing can also add an unrelated class to a mixin's `Self`, in which + // case the TypeVar alone is not a valid owner for descriptors from that class. + let owner = match receiver { + Type::TypeVar(_) => receiver, + Type::Intersection(intersection) => intersection + .positive(db) + .iter() + .copied() + .find(|element| element.is_type_var() && element.is_subtype_of(db, env, key.ty(db))) + .unwrap_or(key.ty(db)), + _ => key.ty(db), + } + .to_meta_type(db, env); let ( PlaceAndQualifiers { place: meta_attr, @@ -4272,13 +4285,7 @@ impl<'db> Type<'db> { }, meta_attr_kind, meta_attr_error, - ) = Self::try_call_dunder_get_on_attribute( - db, - env, - meta_attr_plain, - Some(receiver), - ty.to_meta_type(db, env), - ); + ) = Self::try_call_dunder_get_on_attribute(db, env, meta_attr_plain, Some(receiver), owner); let meta_attr_error = meta_attr_error.map(MemberLookupErrorKind::DescriptorGet); let fallback_error = fallback.err().map(|error| error.kind(db)); @@ -4983,23 +4990,23 @@ impl<'db> Type<'db> { } Type::TypeVar(typevar) => { let receiver = receiver.unwrap_or(this); - let bound_or_constraints = typevar.typevar(db).bound_or_constraints(db, env); - if let Some(bound) = bound_or_constraints - .map(|bound_or_constraints| bound_or_constraints.as_type(db, env)) - && bound.to_instance(db, env).is_some() + if let Some(bound_or_constraints) = + typevar.typevar(db).bound_or_constraints(db, env) { - // A TypeVar can be bounded by a class-object type such as `type[A]`, which - // requires the full lookup path rather than instance-member lookup. - return bound.member_lookup_with_policy_and_receiver( - db, - env, - name_str, - policy, - Some(receiver), - ); + // Use the bound's complete lookup behavior, but retain the original + // receiver so descriptors and `Self` remain correctly specialized. + bound_or_constraints + .as_type(db, env) + .member_lookup_with_policy_and_receiver( + db, + env, + name_str, + policy, + Some(receiver), + ) + } else { + instance_like_member_lookup(db, env, key, receiver) } - - instance_like_member_lookup(db, env, key, receiver) } Type::NominalInstance(instance) @@ -5094,8 +5101,11 @@ impl<'db> Type<'db> { Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => { // A class-object lookup can originate from a TypeVar bound such as `type[A]`. - // Retain that TypeVar as the receiver so `Self` binds to `T'instance`, not `A`. - let receiver = receiver.unwrap_or(this); + // Retain that TypeVar as the receiver so `Self` binds to `T'instance`, not `A`, + // unless its constraints also include non-class-object types. + let receiver = receiver + .filter(|receiver| receiver.to_instance_approximation(db, env).is_some()) + .unwrap_or(this); let enum_class = match this { Type::ClassLiteral(literal) => literal.into_enum_class(db), Type::SubclassOf(subclass_of) => subclass_of From feb64f2cbfdab16442ba1d9d102a481e5058f282 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 11 Aug 2026 22:25:00 +0500 Subject: [PATCH 368/390] [`pylint`] Fix `PLE1307` false positive with bools (#27651) ## Summary Was working on a refactor for https://docs.astral.sh/ruff/rules/bad-string-format-type/ and found a bug - it incorrectly flags any use of bools unless they're used with `%s`, `%r`. See example below, on the runtime it runs without errors and prints `1 \x01 1.000000`. ```python | 1 | # PLE1307 Format type does not match argument type 2 | # OK. 3 | print("%d %c %f" % (True, True, True)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` ## Test Plan Added new test - note it's producing an error in 03625fe4876402aaf468f9e93856de7e13957cdd and fixed in 6f3e7bf32dce03603affc36627dfd03191136f44 All previous tests also pass. --- .../resources/test/fixtures/pylint/bad_string_format_type.py | 3 +++ .../src/rules/pylint/rules/bad_string_format_type.rs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_type.py b/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_type.py index 785ecaf659..61dfe779f0 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_type.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_type.py @@ -66,3 +66,6 @@ # No errors here, will be reported separately by bad-string-format-character. "%b" % b"xx" + +# bool is acceptable as int/float/character. +"%d %c %f" % (True, True, True) diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs index 3822b76b7a..d69e28ecf8 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_type.rs @@ -63,11 +63,11 @@ impl FormatType { self, FormatType::Unknown | FormatType::String | FormatType::Repr ), - PythonType::Number(NumberLike::Complex | NumberLike::Bool) => matches!( + PythonType::Number(NumberLike::Complex) => matches!( self, FormatType::Unknown | FormatType::String | FormatType::Repr ), - PythonType::Number(NumberLike::Integer) => matches!( + PythonType::Number(NumberLike::Integer | NumberLike::Bool) => matches!( self, FormatType::Unknown | FormatType::String From 93901e647859b7f5a15622d8f486da78a5e4f588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9r=C3=A8?= Date: Tue, 11 Aug 2026 12:07:40 -0700 Subject: [PATCH 369/390] [ty] Separate place-load resolution from type inference. (#27319) ## Summary This introduces a model called a `PlaceLoad`, which provides access to place-load resolution logic (i.e., name resolution and reaching definition analysis) independently from type inference. That logic can then be used for things outside of type inference (e.g., for language server refactors which require reaching definition analysis, like [file rename support](https://github.com/astral-sh/ty/issues/1560)). In order to clarify exactly where we've carved out place-load resolution from type inference, this PR makes the separation directly in `TypeInferenceBuilder`, but a [subsequent change](https://github.com/astral-sh/ruff/pull/27320) will move that logic to a location that is more easily shared. I recommend reading the new `place_load.rs` module first, and then the changes to `infer/builder.rs` that integrate it. ## Test Plan This is primarily a refactor that relies on existing test coverage, but I have tweaked or added test coverage in places where we have corrected our name resolution logic. --- .../resources/mdtest/expression/attribute.md | 83 ++ .../resources/mdtest/scopes/builtin.md | 10 +- .../mdtest/scopes/class_implicit_attrs.md | 12 + .../resources/mdtest/scopes/global.md | 24 + .../resources/mdtest/scopes/unbound.md | 11 + crates/ty_python_semantic/src/lib.rs | 1 + crates/ty_python_semantic/src/place_load.rs | 1105 +++++++++++++++++ .../src/types/infer/builder.rs | 697 +++-------- .../src/types/infer/builder/subscript.rs | 7 +- 9 files changed, 1432 insertions(+), 518 deletions(-) create mode 100644 crates/ty_python_semantic/src/place_load.rs diff --git a/crates/ty_python_semantic/resources/mdtest/expression/attribute.md b/crates/ty_python_semantic/resources/mdtest/expression/attribute.md index 8c19bca58a..03da8f60df 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/attribute.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/attribute.md @@ -47,3 +47,86 @@ def f() -> None: box = StrBox() reveal_type(box.attr) # revealed: str ``` + +## Local prefixes block enclosing whole-place bindings + +When a nested function binds the root name of a whole-place access, the local root binding takes +precedence over the root binding from the enclosing scope. + +```py +class IntBox: + attr: int + +class StrBox: + attr: str + +box = IntBox() + +def outer_root() -> None: + box.attr = 1 + + def inner() -> None: + box = StrBox() + reveal_type(box.attr) # revealed: str +``` + +Similarly, a nested rebinding of an intermediate member, rather than the root, takes precedence over +the enclosing binding of that same intermediate member. + +```py +class Holder: + box: IntBox | StrBox + +def outer_member() -> None: + holder = Holder() + holder.box = IntBox() + holder.box.attr = 1 + + def inner() -> None: + holder.box = StrBox() + reveal_type(holder.box.attr) # revealed: str +``` + +Under Python's function name-resolution rules, even a conditional assignment to the root name makes +the local root take precedence over the enclosing binding. When the condition is false, the unbound +local root cannot fall back to the binding in the enclosing scope. + +```py +def with_inner_conditional_root(flag: bool) -> None: + box = IntBox() + box.attr = 1 + + def inner() -> None: + if flag: + box = StrBox() + # error: [possibly-unresolved-reference] "Name `box` used when possibly not defined" + reveal_type(box.attr) # revealed: str +``` + +By contrast, binding an intermediate member does not affect resolution of the root, which still +comes from the enclosing scope. When the intermediate member is conditionally rebound, it can refer +to either object, so both member types remain visible in the whole-place access. + +```py +def with_inner_conditional_member(flag: bool) -> None: + holder = Holder() + holder.box = IntBox() + holder.box.attr = 1 + + def inner() -> None: + if flag: + holder.box = StrBox() + reveal_type(holder.box.attr) # revealed: int | str +``` + +If none of the prefixes are bound in the nested scope, the enclosing whole-place binding remains +visible. + +```py +def outer_fallback() -> None: + box = IntBox() + box.attr = 1 + + def inner() -> None: + reveal_type(box.attr) # revealed: int +``` diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md b/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md index 169931352f..a7eb07d429 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md @@ -2,8 +2,8 @@ ## Conditional local override of builtin -If a builtin name is conditionally shadowed by a local variable, a name lookup should union the -builtin type with the conditionally-defined type: +If a builtin name is conditionally shadowed by a local variable, the function's binding scope +terminates name resolution. The name can be unbound, but it cannot refer to the builtin: ```py def _(flag: bool) -> None: @@ -11,8 +11,10 @@ def _(flag: bool) -> None: abs = 1 chr: int = 1 - reveal_type(abs) # revealed: Literal[1] | (def abs[_T](x: SupportsAbs[_T], /) -> _T) - reveal_type(chr) # revealed: Literal[1] | (def chr(i: SupportsIndex, /) -> str) + # error: [possibly-unresolved-reference] + reveal_type(abs) # revealed: Literal[1] + # error: [possibly-unresolved-reference] + reveal_type(chr) # revealed: Literal[1] ``` ## Conditionally global override of builtin diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md b/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md index 7130538acf..e0281fcbb2 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md @@ -72,6 +72,18 @@ reveal_type(__qualname__) # revealed: Literal[42] reveal_type(__module__) # revealed: Literal[42] ``` +They also take priority over a possibly-bound snapshot from an enclosing `global` declaration: + +```py +def enclosing(flag: bool) -> None: + global __module__ + if flag: + __module__ = 1 + + class Foo: + reveal_type(__module__) # revealed: str +``` + ## `__firstlineno__` has priority over globals (Python 3.13+) The same applies to `__firstlineno__` on Python 3.13+: diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/global.md b/crates/ty_python_semantic/resources/mdtest/scopes/global.md index d32d7c554e..3e025cf78f 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/global.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/global.md @@ -299,6 +299,30 @@ def factory(): reveal_type(x) # revealed: Literal[1] ``` +If the rebinding is conditional, an unbound enclosing snapshot continues to the implicit global: + +```py +def conditional_factory(flag: bool): + global __file__ + if flag: + __file__ = "shadow" + + class C: + reveal_type(__file__) # revealed: str +``` + +An unbound snapshot can also continue through the module scope to a builtin. + +```py +def conditional_builtin_factory(flag: bool): + global len # error: [unresolved-global] "Invalid global declaration of `len`: `len` has no declarations or bindings in the global scope" + if flag: + len = 1 + + class C: + reveal_type(len) # revealed: Literal[1] | (def len(obj: Sized, /) -> int) +``` + ## References to variables before they are defined within a class scope are considered global If we try to access a variable in a class before it has been defined, the lookup will fall back to diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md b/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md index c697d41de6..98acc365a0 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/unbound.md @@ -71,3 +71,14 @@ def f(): # revealed: Literal[2] reveal_type(x) ``` + +## Unbound function local named `reveal_type` + +The convenience fallback for an unimported `reveal_type` only applies when name resolution does not +find the name. It does not replace an unbound local. + +```py +def f(): + reveal_type(1) # error: [unresolved-reference] + reveal_type = lambda value: value +``` diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 68d8346dba..dda6d04a67 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -55,6 +55,7 @@ mod dunder_all; mod fixes; pub mod lint; pub(crate) mod place; +pub(crate) mod place_load; mod reachability; mod semantic_model; mod subscript; diff --git a/crates/ty_python_semantic/src/place_load.rs b/crates/ty_python_semantic/src/place_load.rs new file mode 100644 index 0000000000..d9e062df68 --- /dev/null +++ b/crates/ty_python_semantic/src/place_load.rs @@ -0,0 +1,1105 @@ +//! This module combines the semantics of name resolution with the results of +//! reaching definition analysis to expose a [`PlaceLoadResolution`], which +//! provides a lazy iterator over the steps that resolve the value read from a +//! place. +//! +//! More specifically, a [`PlaceLoadResolution`] iterates over a series of +//! [`PlaceLoadResolutionStep`] values, each of which represents a phase of the +//! process that ultimately either supplies a definite value for a load or ends +//! in explicit failure: +//! +//! - A source ([`PlaceLoadResolutionStep::Source`]) (and its associated type- +//! narrowing constraints) which may supply the value for a load. +//! - A boolean condition ([`PlaceLoadResolutionStep::MemberResolutionCondition`]) +//! that determines whether resolution continues for member loads specifically +//! (i.e., `foo.bar.baz` as opposed to the plain symbol `foo`). This describes +//! the loads of member prefixes (e.g., `foo.bar` and `foo`) that must all be +//! unbound before resolution continues. +//! - A marker ([`PlaceLoadResolutionStep::Exhausted`]) which declares that the +//! resolution process ended in failure. +//! +//! We consume this model to different ends: +//! +//! - Type inference uses it to determine the type and definedness of a place +//! - The language server uses it to determine, e.g., what references to an +//! imported module should be rewritten in response to the module itself being +//! renamed +//! +//! ## Example +//! +//! ```py +//! from collections.abc import Callable +//! +//! def make_counter(start: int, enabled: bool) -> Callable[[], int | None]: +//! if enabled: +//! value = start +//! else: +//! value = None +//! +//! def next_value() -> int | None: +//! nonlocal value +//! if value is not None: +//! current = value # load U +//! value += 1 +//! return current +//! return None +//! +//! return next_value +//! ``` +//! +//! [`PlaceLoadResolution`] at `U` combines reaching definition analysis with a +//! lexical scope walk: +//! +//! 1. Reaching definition analysis from the use-def module supplies the binding +//! state for `value` at `U` in `next_value`. In this case, no value-binding +//! definition reaches U because the `value += 1` assignment occurs after `U`, +//! so the state records `value` as unbound. +//! 2. The use-def model also supplies the narrowing constraint `value is not None` +//! that is associated with the source. That constraint can affect an +//! inferred type but does not affect name resolution. +//! 3. Name resolution encounters the `nonlocal` declaration and continues +//! the lexical scope walk into `make_counter`, where `value` is owned. +//! 4. Once name resolution reaches the scope that owns value, it records +//! `make_counter.value` as an enclosing source of a potential value for `U`. +//! +//! Schematically, fully consuming the resulting [`PlaceLoadResolution`] yields: +//! +//! ```text +//! Source(Bindings(next_value.value at U)) // unbound +//! Source(DefinitionsFromOwningScope(make_counter.value)) +//! Exhausted(UnboundFree) +//! ``` +//! +//! While yielding those steps, the resolution accumulates `value is not None` +//! as a narrowing constraint and records that it crossed the `nonlocal` +//! declaration in `next_value`. +//! +//! The enclosing function's binding scope terminates name resolution, even if +//! none of its definitions supply a value at runtime. [`PlaceLoadResolution`] +//! therefore yields `Exhausted(UnboundFree)` if both sources are exhausted +//! instead of yielding module globals or builtins as later sources. In this +//! example, type inference establishes that the branches in `make_counter` +//! always define `value`, so the `Exhausted` step is unreachable. + +use ruff_python_ast::{self as ast, name::Name}; +use smallvec::SmallVec; +use ty_python_core::ast_ids::{HasScopedUseId, ScopedUseId}; +use ty_python_core::definition::Definition; +use ty_python_core::narrowing_constraints::ConstraintKey; +use ty_python_core::place::{PlaceExpr, PlaceExprRef, ScopedPlaceId}; +use ty_python_core::scope::{NodeWithScopeKind, ScopeId, ScopeKind}; +use ty_python_core::symbol::{ScopedSymbolId, Symbol}; +use ty_python_core::{ + AncestorsIter, BindingWithConstraintsIterator, EnclosingSnapshotResult, FileScopeId, + ProgramFile, SemanticIndex, +}; + +use crate::Db; + +/// Returns an iterator over the steps that resolve a value for a place load. +pub(crate) fn resolve_place_load<'db, 'ast>( + db: &'db dyn Db, + index: &'db SemanticIndex<'db>, + scope: ScopeId<'db>, + place_expr: PlaceExpr, + mode: PlaceLoadMode<'ast>, +) -> PlaceLoadResolution<'db, 'ast> { + PlaceLoadResolution::new( + PlaceLoadResolutionContext { + db, + index, + scope, + file: scope.program_file(db), + mode, + }, + place_expr, + ) +} + +/// Selects the binding state used for a place load's own scope. +#[derive(Clone, Copy)] +pub(crate) enum PlaceLoadMode<'ast> { + /// Resolve bindings live at an expression occurrence. + /// + /// For example, a caller resolving `value` in `print(value)` uses this mode so that only + /// bindings that reach that occurrence are considered. + AtExpression(ast::ExprRef<'ast>), + /// Resolve all bindings reachable in the scope. + /// + /// A caller uses this mode for an annotation in any of these contexts: + /// + /// - A stub file. + /// - A module containing `from __future__ import annotations`. + /// - Python 3.14 or later. + /// + /// Callers also use this mode for other deferred type expressions, including type-parameter + /// bounds and defaults and, in stub files, class bases and type alias values. + /// + /// For example, `Model` in `item: Model` can resolve to a class defined later in the scope. + Deferred, + /// Resolve reachable bindings in a parsed string annotation. + /// + /// A caller uses this mode for a name such as `Model` after parsing `item: "Model"`. The + /// parsed expression is not part of the original semantic index, so it may not have its own + /// place-table entry. + StringAnnotation, +} + +/// Exposes an iterator over the steps that resolve the value for a place load. +pub(crate) struct PlaceLoadResolution<'db, 'ast> { + /// The place expression whose loaded value is being resolved. + place_expr: PlaceExpr, + /// Read-only context shared by every source-selection phase. + context: PlaceLoadResolutionContext<'db, 'ast>, + /// The next node to visit in the resolution graph, or `None` after reaching a leaf. + next_node: Option>, + /// Narrowing constraints accumulated while resolution advances. + constraints: PlaceLoadConstraints, + /// Whether resolution has crossed a `global` or `nonlocal` declaration so far. + crosses_scope_declaration: bool, +} + +impl<'db> Iterator for PlaceLoadResolution<'db, '_> { + type Item = PlaceLoadResolutionStep<'db>; + + /// Lazily yields [`PlaceLoadResolutionStep`] values to describe the resolution process. + /// + /// Internally, this traverses a directed, acyclic graph that models the resolution process. + fn next(&mut self) -> Option { + while let Some(current_node) = self.next_node.take() { + match current_node { + PlaceLoadResolutionNode::LocalSource => { + self.next_node = + Some(PlaceLoadResolutionNode::AskConsumerWhetherToContinueForMember); + + if let Some((kind, exit_constraint)) = + self.context.local_source(self.place_expr()) + { + let source = self.constraints.source( + kind, + PlaceLoadSourceRole::Ordinary, + exit_constraint, + ); + return Some(PlaceLoadResolutionStep::Source(source)); + } + } + PlaceLoadResolutionNode::AskConsumerWhetherToContinueForMember => { + self.next_node = Some(PlaceLoadResolutionNode::DecideResolutionPath); + + if let Some(prefix_loads) = + self.context.place_expr_prefix_loads(self.place_expr()) + { + return Some(PlaceLoadResolutionStep::MemberResolutionCondition( + prefix_loads, + )); + } + } + PlaceLoadResolutionNode::DecideResolutionPath => { + self.next_node = Some(self.decide_resolution_path()); + } + PlaceLoadResolutionNode::DunderClassSource { + definition, + enclosing_scopes, + } => { + self.next_node = Some(PlaceLoadResolutionNode::EnclosingScopeSource( + enclosing_scopes, + )); + + return Some(PlaceLoadResolutionStep::Source( + PlaceLoadConstraints::unnarrowed_source( + PlaceLoadSourceKind::Implicit(ImplicitPlaceLoad::DunderClass( + definition, + )), + PlaceLoadSourceRole::Ordinary, + ), + )); + } + PlaceLoadResolutionNode::EnclosingScopeSource(mut scopes) => { + let (next_node, source) = self.resolve_enclosing_scopes(&mut scopes); + self.next_node = Some(next_node); + + if let Some(source) = source { + return Some(PlaceLoadResolutionStep::Source(source)); + } + } + PlaceLoadResolutionNode::ImplicitClassBodySource(forwarded_global_snapshot) => { + self.next_node = Some(forwarded_global_snapshot.map_or( + PlaceLoadResolutionNode::ExplicitGlobalSource( + PlaceLoadSourceRole::Ordinary, + ), + PlaceLoadResolutionNode::ForwardedGlobalSnapshotSource, + )); + + if self.context.is_class_body_scope() + && let Some(name) = self.loaded_symbol_name() + { + let source = self.constraints.source( + PlaceLoadSourceKind::Implicit(ImplicitPlaceLoad::ClassBodySymbol( + name.clone(), + )), + PlaceLoadSourceRole::Ordinary, + None, + ); + return Some(PlaceLoadResolutionStep::Source(source)); + } + } + PlaceLoadResolutionNode::ForwardedGlobalSnapshotSource(snapshot) => { + let ForwardedGlobalSnapshot { + bindings, + enclosing_scope, + } = snapshot; + self.next_node = Some(PlaceLoadResolutionNode::ImplicitGlobalSource); + + let source = self.constraints.source( + PlaceLoadSourceKind::Bindings(bindings), + PlaceLoadSourceRole::Ordinary, + Some(( + enclosing_scope, + ConstraintKey::NestedScope( + self.context.scope.file_scope_id(self.context.db), + ), + )), + ); + return Some(PlaceLoadResolutionStep::Source(source)); + } + PlaceLoadResolutionNode::ExplicitGlobalSource(role) => { + self.next_node = Some(PlaceLoadResolutionNode::ImplicitGlobalSource); + + if let Some(source) = self.resolve_global(role) { + return Some(PlaceLoadResolutionStep::Source(source)); + } + } + PlaceLoadResolutionNode::ImplicitGlobalSource => { + if let Some(name) = self.loaded_symbol_name().cloned() { + self.next_node = Some(PlaceLoadResolutionNode::BuiltinSource(name.clone())); + + let source = self.constraints.source( + PlaceLoadSourceKind::Implicit( + ImplicitPlaceLoad::ModuleImplicitGlobal { + file: self.context.file, + name, + }, + ), + PlaceLoadSourceRole::Ordinary, + None, + ); + return Some(PlaceLoadResolutionStep::Source(source)); + } + + self.next_node = + Some(PlaceLoadResolutionNode::Failure(PlaceLoadFailure::NotFound)); + } + PlaceLoadResolutionNode::BuiltinSource(name) => { + self.next_node = + Some(PlaceLoadResolutionNode::Failure(PlaceLoadFailure::NotFound)); + + return Some(PlaceLoadResolutionStep::Source( + PlaceLoadConstraints::unnarrowed_source( + PlaceLoadSourceKind::Implicit(ImplicitPlaceLoad::Builtin(name)), + PlaceLoadSourceRole::Ordinary, + ), + )); + } + PlaceLoadResolutionNode::Failure(failure) => { + return Some(PlaceLoadResolutionStep::Exhausted(failure)); + } + } + } + + None + } +} + +impl<'db, 'ast> PlaceLoadResolution<'db, 'ast> { + fn new(context: PlaceLoadResolutionContext<'db, 'ast>, place_expr: PlaceExpr) -> Self { + Self { + context, + place_expr, + next_node: Some(PlaceLoadResolutionNode::LocalSource), + constraints: PlaceLoadConstraints::default(), + crosses_scope_declaration: false, + } + } + + fn decide_resolution_path(&mut self) -> PlaceLoadResolutionNode<'db> { + let db = self.context.db; + let scope = self.context.scope; + let file_scope = scope.file_scope_id(db); + let place_table = self.context.index.place_table(file_scope); + + let mut symbol_is_local = false; + let place_expr = PlaceExprRef::from(&self.place_expr); + if let Some(symbol) = place_expr.as_symbol() + && let Some(symbol_id) = place_table.symbol_id(symbol.name()) + { + let indexed_symbol = place_table.symbol(symbol_id); + symbol_is_local = indexed_symbol.is_local(); + self.crosses_scope_declaration |= + indexed_symbol.is_global() || indexed_symbol.is_nonlocal(); + + let class_body_global_fallback = self.context.is_class_body_scope() && symbol_is_local; + if self.context.skips_non_global_scopes(symbol_id) || class_body_global_fallback { + return PlaceLoadResolutionNode::ExplicitGlobalSource( + if class_body_global_fallback { + PlaceLoadSourceRole::ClassBodyGlobalFallback + } else { + PlaceLoadSourceRole::Ordinary + }, + ); + } + } + + if symbol_is_local { + return if scope.node(db).scope_kind().is_module() { + PlaceLoadResolutionNode::ImplicitGlobalSource + } else { + PlaceLoadResolutionNode::Failure(PlaceLoadFailure::UnboundLocal) + }; + } + + let mut scopes = self.context.index.ancestor_scopes(file_scope); + // The first scope is the input scope itself; skip it to arrive at the first true ancestor. + scopes.next(); + + if let PlaceExprRef::Symbol(symbol) = place_expr + && symbol.name() == "__class__" + && let Some(definition) = self.context.dunder_class_cell_definition() + { + PlaceLoadResolutionNode::DunderClassSource { + definition, + enclosing_scopes: scopes, + } + } else { + PlaceLoadResolutionNode::EnclosingScopeSource(scopes) + } + } + + fn resolve_enclosing_scopes( + &mut self, + scopes: &mut AncestorsIter<'db>, + ) -> (PlaceLoadResolutionNode<'db>, Option>) { + let db = self.context.db; + let scope = self.context.scope; + let file_scope = scope.file_scope_id(db); + + for (enclosing_file_scope, _) in scopes { + if enclosing_file_scope.is_global() { + break; + } + + let enclosing_scope = self.context.index.scope(enclosing_file_scope); + let is_lexical_enclosing_scope = self + .context + .is_lexical_enclosing_scope(enclosing_file_scope); + + let enclosing_place_table = self.context.index.place_table(enclosing_file_scope); + let place_expr = PlaceExprRef::from(&self.place_expr); + let enclosing_place_id = enclosing_place_table.place_id(place_expr); + let enclosing_place = enclosing_place_id.map(|id| enclosing_place_table.place(id)); + // A `global` declaration forwards the place to the module instead of making this + // enclosing scope its owner. A possibly-unbound snapshot must still fall through. + let forwards_to_global = is_lexical_enclosing_scope + && enclosing_place + .is_some_and(|place| place.as_symbol().is_some_and(Symbol::is_global)); + let root_place_was_reassigned = || { + enclosing_place_table + .parents(place_expr) + .any(|root| enclosing_place_table.place(root).is_bound()) + }; + + let mut eagerly_undefined = false; + if self.context.uses_enclosing_snapshots() { + match self.context.index.enclosing_snapshot( + enclosing_file_scope, + place_expr, + file_scope, + ) { + EnclosingSnapshotResult::FoundConstraint(constraint) => { + self.constraints.push( + enclosing_file_scope, + ConstraintKey::NarrowingConstraint(constraint), + ); + if scope.scope(db).is_eager() { + eagerly_undefined = true; + } + } + EnclosingSnapshotResult::FoundBindings(bindings) => { + if forwards_to_global { + self.crosses_scope_declaration = true; + return ( + PlaceLoadResolutionNode::ImplicitClassBodySource(Some( + ForwardedGlobalSnapshot { + bindings, + enclosing_scope: enclosing_file_scope, + }, + )), + None, + ); + } + + return ( + Self::node_after_enclosing_scope(enclosing_scope.kind()), + Some(self.constraints.source( + PlaceLoadSourceKind::Bindings(bindings), + PlaceLoadSourceRole::Ordinary, + Some(( + enclosing_file_scope, + ConstraintKey::NestedScope(file_scope), + )), + )), + ); + } + EnclosingSnapshotResult::NotFound => { + if root_place_was_reassigned() { + return ( + Self::node_after_enclosing_scope(enclosing_scope.kind()), + None, + ); + } + continue; + } + EnclosingSnapshotResult::NoLongerInEagerContext => { + if root_place_was_reassigned() { + return ( + Self::node_after_enclosing_scope(enclosing_scope.kind()), + None, + ); + } + } + } + } + + if !is_lexical_enclosing_scope { + continue; + } + + let (Some(enclosing_place_id), Some(enclosing_place)) = + (enclosing_place_id, enclosing_place) + else { + continue; + }; + + if forwards_to_global { + self.crosses_scope_declaration = true; + return (PlaceLoadResolutionNode::ImplicitClassBodySource(None), None); + } + // Keep walking across `nonlocal` declarations until reaching the owning scope. + if enclosing_place.as_symbol().is_some_and(Symbol::is_nonlocal) { + self.crosses_scope_declaration = true; + continue; + } + if !(enclosing_place.is_bound() || enclosing_place.is_declared()) { + continue; + } + + // The first bound or declared place owns the load. Its public value includes nested + // writes represented by synthetic definitions in this scope. + return ( + Self::node_after_enclosing_scope(enclosing_scope.kind()), + (!eagerly_undefined).then(|| { + self.constraints.source( + PlaceLoadSourceKind::DefinitionsFromOwningScope { + scope: enclosing_file_scope.to_scope_id(db, self.context.file), + id: enclosing_place_id, + }, + PlaceLoadSourceRole::Ordinary, + None, + ) + }), + ); + } + + (PlaceLoadResolutionNode::ImplicitClassBodySource(None), None) + } + + /// Resolves a load that has reached the module's explicit global scope. + /// + /// An eager nested scope uses the global snapshot captured when it began, so a class body + /// cannot see a module binding created only after that body finishes. + fn resolve_global(&mut self, role: PlaceLoadSourceRole) -> Option> { + let current_scope = self.context.scope.file_scope_id(self.context.db); + if current_scope.is_global() { + return None; + } + + if self.context.uses_enclosing_snapshots() { + match self.context.index.enclosing_snapshot( + FileScopeId::global(), + PlaceExprRef::from(&self.place_expr), + current_scope, + ) { + EnclosingSnapshotResult::FoundConstraint(constraint) => { + self.constraints.push( + FileScopeId::global(), + ConstraintKey::NarrowingConstraint(constraint), + ); + return None; + } + EnclosingSnapshotResult::FoundBindings(bindings) => { + return Some(self.constraints.source( + PlaceLoadSourceKind::Bindings(bindings), + role, + Some(( + FileScopeId::global(), + ConstraintKey::NestedScope(current_scope), + )), + )); + } + EnclosingSnapshotResult::NotFound => return None, + EnclosingSnapshotResult::NoLongerInEagerContext => {} + } + } + + let name = self.loaded_symbol_name()?.clone(); + Some(self.constraints.source( + PlaceLoadSourceKind::Implicit(ImplicitPlaceLoad::ExplicitGlobalSymbol { + file: self.context.file, + name, + }), + role, + None, + )) + } + + fn node_after_enclosing_scope(kind: ScopeKind) -> PlaceLoadResolutionNode<'db> { + if kind.is_class() { + PlaceLoadResolutionNode::ImplicitGlobalSource + } else { + PlaceLoadResolutionNode::Failure(PlaceLoadFailure::UnboundFree) + } + } + + pub(crate) fn narrowing_constraints_for( + &self, + source: &PlaceLoadSource<'_>, + ) -> &[(FileScopeId, ConstraintKey)] { + self.constraints.narrowing_constraints_for(source) + } + + pub(crate) fn into_constraints(self) -> Vec<(FileScopeId, ConstraintKey)> { + self.constraints.into_constraints() + } + + pub(crate) fn place_expr(&self) -> PlaceExprRef<'_> { + PlaceExprRef::from(&self.place_expr) + } + + /// Returns the loaded symbol's name, or `None` when the loaded place is a member. + /// + /// For example, this returns a name for `value`, but not for `value.attr` or `value[0]`. + fn loaded_symbol_name(&self) -> Option<&Name> { + self.place_expr().as_symbol().map(Symbol::name) + } +} + +pub(crate) enum PlaceLoadResolutionStep<'db> { + // A source that can supply the value for a load. + Source(PlaceLoadSource<'db>), + // A condition that the caller must evaluate to determine whether resolution should continue + // for a member load. + MemberResolutionCondition(PlaceExprPrefixLoads<'db>), + // A marker that declares that resolution ended in a explicit failure. + Exhausted(PlaceLoadFailure), +} + +/// One source that can supply the value of a place load, along with the +/// type narrowing constraints that apply to it. +/// +/// ## How constraint tracking is implemented +/// +/// [`PlaceLoadResolution`] stores one shared list of constraint keys. Each +/// source maintains an `entry_checkpoint` into that list, which identifies the +/// constraints used to narrow the source. +/// +/// When a key identifies the binding state used to construct a source, that key +/// becomes active after the source is requested, but applying it to the same +/// source again would duplicate work. +/// +/// ### Example +/// +/// ```py +/// from collections.abc import Callable +/// +/// def make_counter(start: int, enabled: bool) -> Callable[[], int | None]: +/// if enabled: +/// value = start +/// else: +/// value = None +/// +/// def next_value() -> int | None: +/// nonlocal value +/// if value is not None: +/// current = value # load U +/// value += 1 +/// return current +/// return None +/// +/// return next_value +/// ``` +/// +/// For `U` above, the constraint representation after both sources have been +/// requested is schematically: +/// +/// ```text +/// PlaceLoadResolution { +/// constraint_keys: [ +/// (next_value, UseId(U)), +/// ], +/// } +/// PlaceLoadSource { +/// kind: Bindings(next_value at U), +/// entry_checkpoint: 0, +/// } +/// PlaceLoadSource { +/// kind: DefinitionsFromOwningScope(make_counter.value), +/// entry_checkpoint: 1, +/// } +/// ``` +/// +/// The first source already comes from `bindings_at_use(U)`, so its `UseId` key +/// becomes active when the source is requested but is not applied on entry. If +/// that source is undefined and the consumer requests the next source, the +/// `UseId` key narrows the enclosing `int | None` place to `int`. If both +/// sources are exhausted, the key remains active for expression-level narrowing. +pub(crate) struct PlaceLoadSource<'db> { + /// How this source supplies the loaded value. + pub(crate) kind: PlaceLoadSourceKind<'db>, + /// Selects the constraints used to narrow this source. + entry_checkpoint: usize, + /// The role this source plays in the load. + role: PlaceLoadSourceRole, +} + +impl PlaceLoadSource<'_> { + /// Returns whether this source is the module fallback for a class-local name. + pub(crate) fn is_class_body_global_fallback(&self) -> bool { + self.role == PlaceLoadSourceRole::ClassBodyGlobalFallback + } + + /// Returns whether this source is considered after lexical name resolution. + pub(crate) fn is_post_lexical(&self) -> bool { + matches!( + self.kind, + PlaceLoadSourceKind::Implicit( + ImplicitPlaceLoad::ModuleImplicitGlobal { .. } | ImplicitPlaceLoad::Builtin(_) + ) + ) + } +} + +/// Describes how a source can supply a place's value. +pub(crate) enum PlaceLoadSourceKind<'db> { + /// Bindings already selected for this load state. + /// + /// For an ordinary expression, these are the bindings that reach that point: + /// + /// ```py + /// value = 1 + /// reveal_type(value) # Only the first binding reaches this load. + /// value = "later" + /// ``` + /// + /// An enclosing eager snapshot is likewise a point-in-time view. A deferred load instead + /// selects all bindings reachable in its scope. + Bindings(BindingWithConstraintsIterator<'db, 'db>), + /// The whole place in the scope that owns it. + /// + /// A free-variable load in a lazy nested scope can observe any definition reachable for the + /// owning place, rather than the state at a single point: + /// + /// ```py + /// def outer(): + /// value: int | str = 1 + /// + /// def inner(): + /// return value + /// + /// value = "later" + /// ``` + /// + /// Keeping the scope and place ID lets inference evaluate both the declaration and all + /// reachable bindings for `outer.value`; an already-selected binding iterator does not retain + /// that whole-place information. + DefinitionsFromOwningScope { + /// The scope containing the place. + scope: ScopeId<'db>, + /// The place within `scope`. + id: ScopedPlaceId, + }, + /// A source represented by a specialized query or rule. + Implicit(ImplicitPlaceLoad<'db>), +} + +/// A source that consumers evaluate using a specialized query or rule. +pub(crate) enum ImplicitPlaceLoad<'db> { + /// The implicit `__class__` cell for a method, lambda, or generator expression defined directly + /// in a class body, e.g.: + /// + /// ```py + /// class C: + /// def method(self): + /// return __class__ + /// ``` + DunderClass(Definition<'db>), + /// An implicit symbol supplied directly in a class body, e.g.: + /// + /// ```py + /// class C: + /// defining_module = __module__ + /// ``` + ClassBodySymbol(Name), + /// A symbol in the module's explicit global namespace, e.g.: + /// + /// ```py + /// answer = 42 + /// + /// def get_answer(): + /// return answer + /// ``` + ExplicitGlobalSymbol { file: ProgramFile<'db>, name: Name }, + /// An implicit attribute supplied by a module, such as `__name__`. + ModuleImplicitGlobal { file: ProgramFile<'db>, name: Name }, + /// A name supplied by the builtin namespace. + Builtin(Name), +} + +/// The role a source plays in a place load. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PlaceLoadSourceRole { + /// The source follows ordinary Python name resolution rules. + Ordinary, + /// The source follows Python’s class-local-to-module fallback rules. + ClassBodyGlobalFallback, +} + +/// The reason resolution stops if the preceding sources do not supply a value. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum PlaceLoadFailure { + /// No additional place-load source applies. + /// + /// For a symbol load, this means runtime lookup raises `NameError`. + NotFound, + /// The current function-like binding scope owns the loaded symbol but + /// supplies no value. + /// + /// Loading the symbol at runtime raises `UnboundLocalError`. + UnboundLocal, + /// An enclosing function-like binding scope owns the place, so resolution + /// cannot continue to module globals or builtins. + /// + /// For a symbol load, an empty closure cell raises `NameError` at runtime. + UnboundFree, +} + +/// Compact descriptions of loads for the tracked prefixes of a place expression. +/// +/// Resolution continues past the local source only if every tracked prefix is locally undefined. +/// +/// For example, the enclosing binding of `obj.value` cannot supply the value read in `inner`: +/// +/// ```python +/// class Outer: +/// value: int +/// +/// class Inner: +/// value: str +/// +/// def outer(): +/// obj = Outer() +/// obj.value = 1 +/// +/// def inner(): +/// obj = Inner() +/// reveal_type(obj.value) # revealed: str +/// ``` +/// +/// The nested scope binds `obj` to a different object, so normal member lookup on the local +/// `obj` must handle the load instead. +pub(crate) struct PlaceExprPrefixLoads<'db> { + scope: ScopeId<'db>, + loads: SmallVec<[PlaceExprPrefixLoad; 2]>, +} + +impl<'db> PlaceExprPrefixLoads<'db> { + /// Creates prefix loads, returning `None` when the iterator is empty. + fn from_iter( + scope: ScopeId<'db>, + loads: impl IntoIterator, + ) -> Option { + let loads = loads.into_iter().collect::>(); + (!loads.is_empty()).then_some(Self { scope, loads }) + } + + /// Returns the scope containing the prefix loads. + pub(crate) fn scope(&self) -> ScopeId<'db> { + self.scope + } + + /// Iterates over the prefix loads. + pub(crate) fn iter(&self) -> impl Iterator + '_ { + self.loads.iter().copied() + } +} + +/// Describes how a consumer can evaluate one prefix of a place expression. +#[derive(Clone, Copy)] +pub(crate) enum PlaceExprPrefixLoad { + /// Use the bindings that reach this expression occurrence. + AtUse(ScopedUseId), + /// Use every binding reachable for this place in its scope. + AllReachable(ScopedPlaceId), + /// The syntax itself guarantees that the prefix is bound. + DefinitelyBound, +} + +/// Read-only context used to select sources for a place load. +#[derive(Clone, Copy)] +struct PlaceLoadResolutionContext<'db, 'ast> { + db: &'db dyn Db, + index: &'db SemanticIndex<'db>, + scope: ScopeId<'db>, + file: ProgramFile<'db>, + mode: PlaceLoadMode<'ast>, +} + +impl<'db> PlaceLoadResolutionContext<'db, '_> { + fn is_class_body_scope(self) -> bool { + self.scope.node(self.db).scope_kind().is_class() + } + + fn uses_enclosing_snapshots(self) -> bool { + matches!(self.mode, PlaceLoadMode::AtExpression(_)) + } + + fn is_lexical_enclosing_scope(self, enclosing_scope: FileScopeId) -> bool { + self.index.scope(enclosing_scope).kind().is_function_like() + || (self.scope.is_annotation(self.db) + && self.scope.scope(self.db).parent() == Some(enclosing_scope)) + } + + fn local_source( + self, + place_expr: PlaceExprRef, + ) -> Option<( + PlaceLoadSourceKind<'db>, + Option<(FileScopeId, ConstraintKey)>, + )> { + let scope = self.scope.file_scope_id(self.db); + let table = self.index.place_table(scope); + let use_def = self.index.use_def_map(scope); + + match self.mode { + PlaceLoadMode::AtExpression(expr_ref) => { + if expr_ref + .as_name_expr() + .is_some_and(|name| name.is_invalid()) + { + return None; + } + + let use_id = expr_ref.scoped_use_id(self.db, self.file); + Some(( + PlaceLoadSourceKind::Bindings(use_def.bindings_at_use(use_id)), + Some((scope, ConstraintKey::UseId(use_id))), + )) + } + PlaceLoadMode::Deferred | PlaceLoadMode::StringAnnotation => { + let source = table + .place_id(place_expr) + .map(|id| PlaceLoadSourceKind::Bindings(use_def.reachable_bindings(id))); + assert!( + source.is_some() || matches!(self.mode, PlaceLoadMode::StringAnnotation), + "Expected the place table to create a place for every valid PlaceExpr node" + ); + source.map(|source| (source, None)) + } + } + } + + /// Describes how to evaluate the tracked prefixes of `place_expr` in this scope. + fn place_expr_prefix_loads( + self, + place_expr: PlaceExprRef, + ) -> Option> { + let table = self.index.place_table(self.scope.file_scope_id(self.db)); + + PlaceExprPrefixLoads::from_iter( + self.scope, + table + .parents(place_expr) + .filter_map(|prefix_id| match self.mode { + PlaceLoadMode::Deferred | PlaceLoadMode::StringAnnotation => { + Some(PlaceExprPrefixLoad::AllReachable(prefix_id)) + } + PlaceLoadMode::AtExpression(mut prefix_expr_ref) => { + let prefix = table.place(prefix_id); + for _ in + 0..(place_expr.num_member_segments() - prefix.num_member_segments()) + { + prefix_expr_ref = match prefix_expr_ref { + ast::ExprRef::Attribute(attribute) => { + ast::ExprRef::from(&attribute.value) + } + ast::ExprRef::Subscript(subscript) => { + ast::ExprRef::from(&subscript.value) + } + _ => return None, + }; + } + + if prefix_expr_ref + .as_name_expr() + .is_some_and(|name| name.is_invalid()) + { + return None; + } + + if let ast::ExprRef::Named(named) = prefix_expr_ref { + return named + .target + .is_name_expr() + .then_some(PlaceExprPrefixLoad::DefinitelyBound); + } + + Some(PlaceExprPrefixLoad::AtUse( + prefix_expr_ref.scoped_use_id(self.db, self.file), + )) + } + }), + ) + } + + fn skips_non_global_scopes(self, symbol: ScopedSymbolId) -> bool { + let scope = self.scope.file_scope_id(self.db); + !scope.is_global() && self.index.symbol_is_global_in_scope(symbol, scope) + } + + fn dunder_class_cell_definition(self) -> Option> { + let current_scope = self.scope.file_scope_id(self.db); + if let Some(definition) = self.index.class_definition_of_method(current_scope) { + return Some(definition); + } + + let scope = self.index.scope(current_scope); + if !matches!( + scope.node(), + NodeWithScopeKind::Lambda(_) | NodeWithScopeKind::GeneratorExpression(_) + ) { + return None; + } + let class = self.index.parent_scope(current_scope)?.node().as_class()?; + Some(self.index.expect_single_definition(class)) + } +} + +/// A node in the acyclic graph traversed by a [`PlaceLoadResolution`]. +/// +/// Source-named nodes may yield a [`PlaceLoadResolutionStep::Source`]. The two verb-named nodes +/// either ask the consumer whether traversal should continue or decide which outgoing edge to +/// follow. Every transition advances toward a [`PlaceLoadResolutionNode::Failure`] leaf; no node +/// is revisited. +enum PlaceLoadResolutionNode<'db> { + /// The source selected from the load's own scope, if one exists. + LocalSource, + /// Ask the consumer whether resolution should continue for a member load. + AskConsumerWhetherToContinueForMember, + /// Decide whether resolution ends, continues through enclosing scopes, or moves to the module + /// scope. + DecideResolutionPath, + /// The implicit `__class__` source, followed by enclosing scopes. + DunderClassSource { + definition: Definition<'db>, + enclosing_scopes: AncestorsIter<'db>, + }, + /// A source from the remaining enclosing scopes, if one exists. + EnclosingScopeSource(AncestorsIter<'db>), + /// The implicit class-body source, followed by the applicable global source. + ImplicitClassBodySource(Option>), + /// Bindings from an enclosing `global` declaration that were visible when the nested eager + /// scope began. + ForwardedGlobalSnapshotSource(ForwardedGlobalSnapshot<'db>), + /// An explicit global source with the given role, if one exists. + ExplicitGlobalSource(PlaceLoadSourceRole), + /// An implicit global considered after explicit lookup, if one exists. + ImplicitGlobalSource, + /// The builtin with the given name. + BuiltinSource(Name), + /// The failure that ends resolution. + Failure(PlaceLoadFailure), +} + +struct ForwardedGlobalSnapshot<'db> { + bindings: BindingWithConstraintsIterator<'db, 'db>, + enclosing_scope: FileScopeId, +} + +/// Narrowing constraints accumulated while a consumer advances through a place load. +#[derive(Default)] +struct PlaceLoadConstraints { + constraint_keys: Vec<(FileScopeId, ConstraintKey)>, +} + +impl PlaceLoadConstraints { + /// Creates a source narrowed by the constraints accumulated before it. + /// + /// `exit_constraint`, when present, is activated only after the source is requested (that + /// source was already selected from the binding state identified by the constraint, so it is + /// deliberately not reapplied to the same source). + /// + /// For example, consider the load at `U`: + /// + /// ```py + /// def outer(value: int | None): + /// def inner(): + /// if value is not None: + /// return value # U + /// ``` + /// + /// The local source is selected by `bindings_at_use(U)`. Its `UseId(U)` is the exit constraint: + /// it is not applied again to that source, but becomes active if the source is unbound so that + /// the enclosing `outer.value` source is narrowed from `int | None` to `int`. + fn source<'db>( + &mut self, + kind: PlaceLoadSourceKind<'db>, + role: PlaceLoadSourceRole, + exit_constraint: Option<(FileScopeId, ConstraintKey)>, + ) -> PlaceLoadSource<'db> { + let entry_checkpoint = self.constraint_keys.len(); + self.constraint_keys.extend(exit_constraint); + PlaceLoadSource { + kind, + entry_checkpoint, + role, + } + } + + /// Creates a source without applying accumulated narrowing constraints to it. + fn unnarrowed_source( + kind: PlaceLoadSourceKind<'_>, + role: PlaceLoadSourceRole, + ) -> PlaceLoadSource<'_> { + PlaceLoadSource { + kind, + entry_checkpoint: 0, + role, + } + } + + /// Extends the list of constraints used by subsequent sources. + fn push(&mut self, scope: FileScopeId, key: ConstraintKey) { + self.constraint_keys.push((scope, key)); + } + + /// Returns the constraints used to narrow `source`. + fn narrowing_constraints_for( + &self, + source: &PlaceLoadSource<'_>, + ) -> &[(FileScopeId, ConstraintKey)] { + &self.constraint_keys[..source.entry_checkpoint] + } + + /// Returns the constraints activated by the sources that were requested. + fn into_constraints(self) -> Vec<(FileScopeId, ConstraintKey)> { + self.constraint_keys + } +} diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 050d6a3d79..9c04ec76d1 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -41,6 +41,10 @@ use crate::place::{ place_from_bindings_with_reachability_cache, place_from_declarations_with_reachability_cache, typing_extensions_symbol, }; +use crate::place_load::{ + ImplicitPlaceLoad, PlaceExprPrefixLoad, PlaceExprPrefixLoads, PlaceLoadFailure, PlaceLoadMode, + PlaceLoadResolutionStep, PlaceLoadSource, PlaceLoadSourceKind, resolve_place_load, +}; use crate::reachability::{ReachabilityEvaluationCache, evaluate_reachability_with_cache}; use crate::types::add_inferred_python_version_hint_to_diagnostic; use crate::types::attribute_write::{AssignmentAttributeMembers, assignment_attribute_members}; @@ -121,7 +125,6 @@ use crate::types::{ infer_complete_scope_types, infer_scope_types, is_discarded_dict_key_assignment, todo_type, }; use crate::{AnalysisSettings, Db, FxIndexSet, FxOrderSet}; -use ty_python_core::ast_ids::ScopedUseId; use ty_python_core::definition::{ AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, ComprehensionDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey, DefinitionState, ExceptHandlerDefinitionKind, @@ -135,10 +138,10 @@ use ty_python_core::node_key::NodeKey; use ty_python_core::place::{PlaceExpr, PlaceExprRef}; use ty_python_core::predicate::PatternPredicate; use ty_python_core::scope::{FileScopeId, NodeWithScopeKind, NodeWithScopeRef, ScopeId, ScopeKind}; -use ty_python_core::symbol::{ScopedSymbolId, Symbol}; +use ty_python_core::symbol::ScopedSymbolId; use ty_python_core::{ - ApplicableConstraints, EnclosingSnapshotResult, EvaluationMode, ProgramFile, SemanticIndex, - Truthiness, unpack::UnpackPosition, + ApplicableConstraints, EvaluationMode, ProgramFile, SemanticIndex, Truthiness, + unpack::UnpackPosition, }; use ty_python_core::{ExpressionNodeKey, Statement}; @@ -1899,31 +1902,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { diagnostic::report_undeclared_protocol_attribute(&self.context, target, protocol); } - /// Returns the implicit `__class__` cell in the current direct method body or - /// lazy scope defined directly in a class body. - fn dunder_class_cell_type(&self) -> Option> { - let current_scope_id = self.scope().file_scope_id(self.db()); - let class_definition = - if let Some(definition) = self.index.class_definition_of_method(current_scope_id) { - definition - } else { - let current_scope = self.index.scope(current_scope_id); - if !matches!( - current_scope.node(), - NodeWithScopeKind::Lambda(_) | NodeWithScopeKind::GeneratorExpression(_) - ) { - return None; - } - let class = self - .index - .parent_scope(current_scope_id)? - .node() - .as_class()?; - self.index.expect_single_definition(class) - }; - original_class_type(self.db(), class_definition) - } - /// If the current scope is a (non-lambda) function, return that function's AST node. /// /// If the current scope is not a function (or it is a lambda function), return `None`. @@ -9771,531 +9749,234 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_name_load(&mut self, name_node: &ast::ExprName) -> Type<'db> { let db = self.db(); - let ast::ExprName { - range: _, - node_index: _, - id: symbol_name, - ctx: _, - } = name_node; let expr = PlaceExpr::from_expr_name(name_node); - let (resolved, constraint_keys) = - self.infer_place_load(PlaceExprRef::from(&expr), ast::ExprRef::Name(name_node)); + let (resolved, _) = self.infer_place_load(expr, ast::ExprRef::Name(name_node)); let env = self.program_environment(); - let resolved_after_fallback = resolved - // 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, env, || { - module_type_implicit_global_symbol(db, self.program_file(), symbol_name).map_type( - |ty| { - self.narrow_place_with_applicable_constraints( - PlaceExprRef::from(&expr), - ty, - &constraint_keys, - ) - }, - ) - }) - // Not found in globals? Fallback to builtins - // (without infinite recursion if we're already in builtins.) - .or_fall_back_to(db, env, || { - if Some(self.scope()) == builtins_module_scope(db, env) { - Place::Undefined.into() - } else { - implicit_builtins_symbol(db, env, symbol_name) - } - }) - // Still not found? It might be `reveal_type`... - .or_fall_back_to(db, env, || { - if symbol_name == "reveal_type" { - if !self.in_stub() - && !self.is_in_type_checking_block(self.scope(), name_node) - && 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, env, symbol_name) - } else { - Place::Undefined.into() - } - }); - - let ty = resolved_after_fallback.unwrap_with_diagnostic(db, env, |lookup_error| { - match lookup_error { - LookupError::Undefined(qualifiers) => { - self.report_unresolved_reference(name_node); - TypeAndQualifiers::new(Type::unknown(), TypeOrigin::Inferred, qualifiers) - } - LookupError::PossiblyUndefined(type_when_bound) => { - report_possibly_unresolved_reference(&self.context, name_node); - type_when_bound - } + let ty = resolved.unwrap_with_diagnostic(db, env, |lookup_error| match lookup_error { + LookupError::Undefined(qualifiers) => { + self.report_unresolved_reference(name_node); + TypeAndQualifiers::new(Type::unknown(), TypeOrigin::Inferred, qualifiers) + } + LookupError::PossiblyUndefined(type_when_bound) => { + report_possibly_unresolved_reference(&self.context, name_node); + type_when_bound } }); ty.inner_type() } - fn infer_local_place_load( + /// Infer the type of a place expression from its ordered load sources. + /// + /// This also returns the [`ConstraintKey`]s used by expression-level narrowing. + fn infer_place_load( &self, - expr: PlaceExprRef, + place_expr: PlaceExpr, expr_ref: ast::ExprRef, - ) -> (Place<'db>, Option) { + ) -> (PlaceAndQualifiers<'db>, Vec<(FileScopeId, ConstraintKey)>) { let env = self.program_environment(); - let db = self.db(); - let scope = self.scope(); - let file_scope_id = scope.file_scope_id(db); - let place_table = self.index.place_table(file_scope_id); - let use_def = self.index.use_def_map(file_scope_id); - - // If we're inferring types of deferred expressions, look them up from end-of-scope. - if self.is_deferred() { - let place = if let Some(place_id) = place_table.place_id(expr) { - place_from_bindings_with_reachability_cache( - db, - env, - use_def.reachable_bindings(place_id), - self.reachability_cache(), - ) - .place - } else { - assert!( - self.in_string_annotation(), - "Expected the place table to create a place for every valid PlaceExpr node" - ); - Place::Undefined - }; - (place, None) + let mode = if self.is_deferred() && self.in_string_annotation() { + PlaceLoadMode::StringAnnotation + } else if self.is_deferred() { + PlaceLoadMode::Deferred } else { - if expr_ref - .as_name_expr() - .is_some_and(|name| name.is_invalid()) - { - return (Place::Undefined, None); - } - - // A named expression can show up here when resolving the parent place of something - // like `(foo := bar()).baz`. It binds `foo`, but it is not a normal load site and - // therefore has no `ScopedUseId`, so resolve it from its binding definition instead. - if let ast::ExprRef::Named(named) = expr_ref { - let place = if named.target.is_name_expr() { - let definition = self.index.expect_single_definition(named); - Place::bound(binding_type(self.db(), definition)).with_definition(definition) - } else { - Place::Undefined - }; - return (place, None); - } - - let use_id = expr_ref.scoped_use_id(db, self.program_file()); - let place = place_from_bindings_with_reachability_cache( - db, - env, - use_def.bindings_at_use(use_id), - self.reachability_cache(), - ) - .place; - - (place, Some(use_id)) - } - } - - /// Resolve a load that has fallen through to the module's explicit global scope. - /// - /// For eager nested scopes, this uses the global enclosing snapshot instead of the completed - /// module scope, so a class body cannot see a class name that is bound only after the body - /// finishes: - /// - /// ```python - /// class A: - /// A = A - /// ``` - /// - /// `symbol_name` is only needed when no snapshot is available: snapshots can resolve complex - /// places like `a.x`, but the fallback global query only works for bare symbols. `assume_bound` - /// preserves the class-body fallback behavior for names that are also local to the class body. - fn infer_explicit_global_symbol_load( - &self, - place_expr: PlaceExprRef, - symbol_name: Option<&str>, - current_scope_id: FileScopeId, - constraint_keys: &mut Vec<(FileScopeId, ConstraintKey)>, - assume_bound: bool, - ) -> PlaceAndQualifiers<'db> { - let db = self.db(); - if current_scope_id.is_global() { - return Place::Undefined.into(); - } - - if !self.is_deferred() { - match self - .index - .enclosing_snapshot(FileScopeId::global(), place_expr, current_scope_id) - { - EnclosingSnapshotResult::FoundConstraint(constraint) => { - constraint_keys.push(( - FileScopeId::global(), - ConstraintKey::NarrowingConstraint(constraint), - )); - // Reaching here means that no bindings are found in any scope. - // Since `explicit_global_symbol` may return a cycle initial value, we return `Place::Undefined` here. - return Place::Undefined.into(); - } - EnclosingSnapshotResult::FoundBindings(bindings) => { - let mut place_and_qualifiers = place_from_bindings_with_reachability_cache( - db, - self.program_environment(), - bindings, - self.reachability_cache(), - ); - if assume_bound && let Place::Defined(defined) = place_and_qualifiers.place { - place_and_qualifiers.place = - Place::Defined(defined.with_definedness(Definedness::AlwaysDefined)); + PlaceLoadMode::AtExpression(expr_ref) + }; + let mut resolution = + resolve_place_load(self.db(), self.index, self.scope(), place_expr, mode); + let mut place = PlaceAndQualifiers::from(Place::Undefined); + let mut failure = None; + let mut checked_deprecated = false; + + while let Some(step) = resolution.next() { + match step { + PlaceLoadResolutionStep::Source(source) => { + if !checked_deprecated && source.is_post_lexical() { + // Deprecation diagnostics apply to the result of lexical name resolution, + // before it is combined with implicit module globals or builtins. Hence, we + // check for deprecation here when the first post-lexical source is yielded. + // If resolution stops before this, then the check after the resolution loop + // handles the final lexical result instead. + if let Some(ty) = place.place.ignore_possibly_undefined() { + self.check_deprecated(expr_ref, ty); + } + checked_deprecated = true; } - let place = place_and_qualifiers.place.map_type(|ty| { - self.narrow_place_with_applicable_constraints( - place_expr, - ty, - constraint_keys, + let narrowing_constraints = resolution.narrowing_constraints_for(&source); + place = place.or_fall_back_to(self.db(), env, || { + self.infer_place_load_source( + resolution.place_expr(), + source, + narrowing_constraints, ) }); - constraint_keys.push(( - FileScopeId::global(), - ConstraintKey::NestedScope(current_scope_id), - )); - return place.into(); + if place.place.is_definitely_bound() { + break; + } } - // There are no visible bindings / constraint here. - EnclosingSnapshotResult::NotFound => { - return Place::Undefined.into(); + PlaceLoadResolutionStep::MemberResolutionCondition(prefix_loads) => { + if self.has_bound_place_expr_prefix(&prefix_loads) { + failure = Some(PlaceLoadFailure::NotFound); + break; + } + } + PlaceLoadResolutionStep::Exhausted(exhaustion_failure) => { + failure = Some(exhaustion_failure); + break; } - EnclosingSnapshotResult::NoLongerInEagerContext => {} } } - let Some(symbol_name) = symbol_name else { - return Place::Undefined.into(); + if !checked_deprecated && let Some(ty) = place.place.ignore_possibly_undefined() { + self.check_deprecated(expr_ref, ty); + } + + let place = if failure == Some(PlaceLoadFailure::NotFound) { + place.or_fall_back_to(self.db(), env, || { + self.infer_unimported_reveal_type_fallback(expr_ref) + }) + } else { + place }; - explicit_global_symbol(self.db(), self.program_file(), symbol_name).map_type(|ty| { - self.narrow_place_with_applicable_constraints(place_expr, ty, constraint_keys) - }) + let constraint_keys = resolution.into_constraints(); + + (place, constraint_keys) } - /// Infer the type of a place expression from definitions, assuming a load context. - /// This method also returns the [`ConstraintKey`]s for each scope associated with `expr`, - /// which is used to narrow by condition rather than by assignment. - fn infer_place_load( + fn infer_place_load_source( &self, place_expr: PlaceExprRef, - expr_ref: ast::ExprRef, - ) -> (PlaceAndQualifiers<'db>, Vec<(FileScopeId, ConstraintKey)>) { - let env = self.program_environment(); + source: PlaceLoadSource<'db>, + narrowing_constraints: &[(FileScopeId, ConstraintKey)], + ) -> PlaceAndQualifiers<'db> { let db = self.db(); - let scope = self.scope(); - let file_scope_id = scope.file_scope_id(db); - let place_table = self.index.place_table(file_scope_id); + let env = self.program_environment(); + let is_class_body_global_fallback = source.is_class_body_global_fallback(); - let mut constraint_keys = vec![]; - let (local_scope_place, use_id) = self.infer_local_place_load(place_expr, expr_ref); - if let Some(use_id) = use_id { - constraint_keys.push((file_scope_id, ConstraintKey::UseId(use_id))); - } + let place = match source.kind { + PlaceLoadSourceKind::Bindings(bindings) => { + let mut place = place_from_bindings_with_reachability_cache( + db, + env, + bindings, + self.reachability_cache(), + ) + .place; - let fallback = || { - let mut symbol_resolves_locally = false; - if let Some(symbol) = place_expr.as_symbol() - && let Some(symbol_id) = place_table.symbol_id(symbol.name()) - { - // Footgun: `place_expr` and `symbol` were probably constructed with all-zero - // flags. We need to read the place table to get correct flags. - symbol_resolves_locally = place_table.symbol(symbol_id).is_local(); - // If we try to access a variable in a class before it has been defined, the - // lookup will fall back to global. See the comment on `Symbol::is_local`. - let fallback_to_global = - scope.node(db).scope_kind().is_class() && symbol_resolves_locally; - if self.skip_non_global_scopes(file_scope_id, symbol_id) || fallback_to_global { - return self.infer_explicit_global_symbol_load( - place_expr, - Some(symbol.name()), - file_scope_id, - &mut constraint_keys, - fallback_to_global, - ); + // Compatibility policy: ty historically treats a possibly-bound module snapshot + // reached through a class-body global fallback as definitely bound. At runtime, + // an unbound snapshot would continue to builtins or produce a name error. + if is_class_body_global_fallback && let Place::Defined(defined) = place { + place = Place::Defined(defined.with_definedness(Definedness::AlwaysDefined)); } - } - // Symbols that are bound or declared in the local scope, and not marked `nonlocal` or - // `global`, never refer to an enclosing scope. (If you reference such a symbol before - // it's bound, you get an `UnboundLocalError`.) Short-circuit instead of walking - // enclosing scopes in this case. The one exception to this rule is the global fallback - // in class bodies, which we already handled above. - if symbol_resolves_locally { - return Place::Undefined.into(); + place.into() } - - if let PlaceExprRef::Symbol(symbol) = place_expr - && symbol.name() == "__class__" - && let Some(class) = self.dunder_class_cell_type() - { - return Place::bound(class).into(); - } - - for parent_id in place_table.parents(place_expr) { - let parent_expr = place_table.place(parent_id); - let mut expr_ref = expr_ref; - for _ in 0..(place_expr.num_member_segments() - parent_expr.num_member_segments()) { - match expr_ref { - ast::ExprRef::Attribute(attribute) => { - expr_ref = ast::ExprRef::from(&attribute.value); - } - ast::ExprRef::Subscript(subscript) => { - expr_ref = ast::ExprRef::from(&subscript.value); - } - _ => unreachable!(), + PlaceLoadSourceKind::DefinitionsFromOwningScope { scope, id } => place_by_id( + db, + scope, + id, + RequiresExplicitReExport::No, + ConsideredDefinitions::AllReachable, + ), + PlaceLoadSourceKind::Implicit(implicit) => match implicit { + ImplicitPlaceLoad::DunderClass(definition) => original_class_type(db, definition) + .map_or_else( + || Place::Undefined.into(), + |class| Place::bound(class).into(), + ), + ImplicitPlaceLoad::ClassBodySymbol(name) => { + let implicit = class_body_implicit_symbol(db, env, &name); + if implicit.place.is_definitely_bound() { + implicit + } else { + Place::Undefined.into() } } - let (parent_place, _use_id) = self.infer_local_place_load(parent_expr, expr_ref); - if let Place::Defined(_) = parent_place { - return Place::Undefined.into(); + ImplicitPlaceLoad::ExplicitGlobalSymbol { file, name } => { + explicit_global_symbol(db, file, &name) } - } - - // Walk enclosing scopes to resolve a free-variable load (`LOAD_DEREF` at runtime). - // There are two main ways we try to model these loads: - // - // 1. "Snapshots" record the bindings/constraints in the enclosing scope at the point - // just before a nested scope begins. For variables that aren't modified after that - // point, that's the only value that the nested scope can see. If a variable is - // reassigned later, lazy snapshots for that variable can be updated or swept. - // - // 2. Otherwise, we keep walking until we get to the variable's original defining - // scope, and we use its "public type" there, which respects all reachable bindings, - // not just end-of-scope bindings. That includes the synthetic `NestedBindings` - // definitions that we install after each nested scope is closed, so it has - // a complete view of the nested `global` and `nonlocal` writes beneath it. - // - // This walk only resolves free variables and explicit `nonlocal`s. A symbol that is - // local to the current scope never falls back to an enclosing scope, even if it's only - // possibly bound at the current use: Python would raise `UnboundLocalError` instead. - // - // Note that we only get to this walk via `or_fall_back_to` above. In other words, for - // definitely-locally-bound variables, we defer to the current scope's bindings instead - // of looking at enclosing scopes. Concretely: - // - // def f(): - // x = None - // - // def g(): - // nonlocal x - // if flag: - // x = 42 - // - // # `x` is possibly unbound here, so we walk enclosing scopes and see the - // # public type in `f`. - // reveal_type(x) # revealed: Literal[42, 99] | None - // - // x = 99 - // # But now `x` is definitely bound, so we don't do the walk. - // reveal_type(x) # revealed: Literal[99] - // - // Importantly, this approach isn't generally sound. The public type could include - // nested bindings from sibling scopes, which really could run at any time, and in some - // cases we're being too deferential to local bindings. Unfortunately the fully sound - // treatment would reveal `Literal[42, 99] | None` even immediately after `x = 99`, - // which is too frustrating for users in practice. - for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id).skip(1) { - // If the current enclosing scope is global, no place lookup is performed here, - // instead falling back to the module's explicit global lookup below. - if enclosing_scope_file_id.is_global() { - break; + ImplicitPlaceLoad::ModuleImplicitGlobal { file, name } => { + module_type_implicit_global_symbol(db, file, &name) } - - // Class scopes are not visible to nested scopes, and we need to handle global - // scope differently (because an unbound name there falls back to builtins), so - // check only function-like scopes. - // There is one exception to this rule: annotation scopes can see - // names defined in an immediately-enclosing class scope. - let enclosing_scope = self.index.scope(enclosing_scope_file_id); - - let is_immediately_enclosing_scope = scope.is_annotation(db) - && scope - .scope(db) - .parent() - .is_some_and(|parent| parent == enclosing_scope_file_id); - - let has_root_place_been_reassigned = || { - let enclosing_place_table = self.index.place_table(enclosing_scope_file_id); - enclosing_place_table - .parents(place_expr) - .any(|enclosing_root_place_id| { - enclosing_place_table - .place(enclosing_root_place_id) - .is_bound() - }) - }; - - // If the reference is in a nested eager scope, we need to look for the place at - // the point where the previous enclosing scope was defined, instead of at the end - // of the scope. (Note that the semantic index builder takes care of only - // registering eager bindings for nested scopes that are actually eager, and for - // enclosing scopes that actually contain bindings that we should use when - // resolving the reference.) - let mut eagerly_resolved_place = None; - if !self.is_deferred() { - match self.index.enclosing_snapshot( - enclosing_scope_file_id, - place_expr, - file_scope_id, - ) { - EnclosingSnapshotResult::FoundConstraint(constraint) => { - constraint_keys.push(( - enclosing_scope_file_id, - ConstraintKey::NarrowingConstraint(constraint), - )); - // If the current scope is eager, it is certain that the place is undefined in the current scope. - // Do not call the `place` query below as a fallback. - if scope.scope(db).is_eager() { - eagerly_resolved_place = Some(Place::Undefined.into()); - } - } - EnclosingSnapshotResult::FoundBindings(bindings) => { - let place = place_from_bindings_with_reachability_cache( - db, - env, - bindings, - self.reachability_cache(), - ) - .place - .map_type(|ty| { - self.narrow_place_with_applicable_constraints( - place_expr, - ty, - &constraint_keys, - ) - }); - constraint_keys.push(( - enclosing_scope_file_id, - ConstraintKey::NestedScope(file_scope_id), - )); - return place.into(); - } - // There are no visible bindings / constraint here. - // Don't fall back to non-eager place resolution. - EnclosingSnapshotResult::NotFound => { - if has_root_place_been_reassigned() { - return Place::Undefined.into(); - } - continue; - } - EnclosingSnapshotResult::NoLongerInEagerContext => { - if has_root_place_been_reassigned() { - return Place::Undefined.into(); - } - } + ImplicitPlaceLoad::Builtin(name) => { + if Some(self.scope()) == builtins_module_scope(db, env) { + Place::Undefined.into() + } else { + implicit_builtins_symbol(db, env, &name) } } + }, + }; - if !enclosing_scope.kind().is_function_like() && !is_immediately_enclosing_scope { - continue; - } + if narrowing_constraints.is_empty() { + place + } else { + place.map_type(|ty| { + self.narrow_place_with_applicable_constraints(place_expr, ty, narrowing_constraints) + }) + } + } - let enclosing_place_table = self.index.place_table(enclosing_scope_file_id); - let Some(enclosing_place_id) = enclosing_place_table.place_id(place_expr) else { - continue; - }; + /// Applies ty's convenience fallback for an unimported `reveal_type`. + fn infer_unimported_reveal_type_fallback( + &self, + expr_ref: ast::ExprRef, + ) -> PlaceAndQualifiers<'db> { + let Some(name) = expr_ref + .as_name_expr() + .filter(|name| name.id == "reveal_type") + else { + return Place::Undefined.into(); + }; - let enclosing_place = enclosing_place_table.place(enclosing_place_id); + if !self.in_stub() + && !self.is_in_type_checking_block(self.scope(), name) + && let Some(builder) = self.context.report_lint(&UNDEFINED_REVEAL, name) + { + 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"); + } - // Reads of "free" or `nonlocal` variables terminate at any enclosing scope that - // marks the variable `global`, whether or not that scope actually binds the - // variable. If we see a `global` declaration, stop walking scopes and proceed to - // the global handling below. (If we're walking from a prior/inner scope where this - // variable is `nonlocal`, then this is a semantic syntax error, but we don't - // enforce that here. See `SemanticIndexBuilder::pop_scope`.) - if enclosing_place.as_symbol().is_some_and(Symbol::is_global) { - break; - } + typing_extensions_symbol(self.db(), self.program_environment(), "reveal_type") + } - // Keep walking until we reach the defining scope of the variable. The synthetic - // nested bindings definitions installed there will see everything below it. - if enclosing_place.as_symbol().is_some_and(Symbol::is_nonlocal) { - continue; - } - if !(enclosing_place.is_bound() || enclosing_place.is_declared()) { - // Note that this check includes members like `x.y` and `x[0]`, which aren't - // symbols and can't be explicitly `nonlocal`. - continue; - } + /// Returns whether any tracked place-expression prefix has a definite or possible binding in + /// this scope. + fn has_bound_place_expr_prefix(&self, prefix_loads: &PlaceExprPrefixLoads<'db>) -> bool { + let db = self.db(); + let env = self.program_environment(); + let file_scope_id = prefix_loads.scope().file_scope_id(db); + let use_def = self.index.use_def_map(file_scope_id); - // We've reached the defining scope of the variable. Infer its public type. - debug_assert!(enclosing_place.is_bound() || enclosing_place.is_declared()); - let enclosing_scope_id = - enclosing_scope_file_id.to_scope_id(db, self.program_file()); - return eagerly_resolved_place.unwrap_or_else(|| { - place_by_id( - self.db(), - enclosing_scope_id, - enclosing_place_id, - RequiresExplicitReExport::No, - ConsideredDefinitions::AllReachable, + prefix_loads.iter().any(|prefix| { + let place = match prefix { + PlaceExprPrefixLoad::AtUse(use_id) => { + place_from_bindings_with_reachability_cache( + db, + env, + use_def.bindings_at_use(use_id), + self.reachability_cache(), ) - .map_type(|ty| { - self.narrow_place_with_applicable_constraints( - place_expr, - ty, - &constraint_keys, - ) - }) - }); - } - - PlaceAndQualifiers::default() - // If we're in a class body, check for implicit class body symbols first. - // These take precedence over globals. - .or_fall_back_to(db, env, || { - if scope.node(db).scope_kind().is_class() - && let Some(symbol) = place_expr.as_symbol() - { - let implicit = class_body_implicit_symbol(db, env, symbol.name()); - if implicit.place.is_definitely_bound() { - return implicit.map_type(|ty| { - self.narrow_place_with_applicable_constraints( - place_expr, - ty, - &constraint_keys, - ) - }); - } - } - Place::Undefined.into() - }) - // No nonlocal binding? Check the module's explicit globals. - // Avoid infinite recursion if `self.scope` already is the module's global scope. - .or_fall_back_to(db, env, || { - self.infer_explicit_global_symbol_load( - place_expr, - place_expr.as_symbol().map(|symbol| symbol.name().as_str()), - file_scope_id, - &mut constraint_keys, - false, + .place + } + PlaceExprPrefixLoad::AllReachable(place_id) => { + place_from_bindings_with_reachability_cache( + db, + env, + use_def.reachable_bindings(place_id), + self.reachability_cache(), ) - }) - }; - let place = PlaceAndQualifiers::from(local_scope_place).or_fall_back_to(db, env, fallback); - - if let Some(ty) = place.place.ignore_possibly_undefined() { - self.check_deprecated(expr_ref, ty); - } + .place + } + PlaceExprPrefixLoad::DefinitelyBound => return true, + }; - (place, constraint_keys) + !place.is_undefined() + }) } fn report_unresolved_reference(&self, expr_name_node: &ast::ExprName) { @@ -10472,10 +10153,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut assigned_type = None; if let Some(place_expr) = PlaceExpr::try_from_expr(attribute) { - let (resolved, keys) = self.infer_place_load( - PlaceExprRef::from(&place_expr), - ast::ExprRef::Attribute(attribute), - ); + let (resolved, keys) = + self.infer_place_load(place_expr, ast::ExprRef::Attribute(attribute)); constraint_keys.extend(keys); if let Place::Defined(DefinedPlace { ty, diff --git a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs index 41bc1b31ac..77dadab58c 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/subscript.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/subscript.rs @@ -36,7 +36,7 @@ use crate::types::{ }; use crate::{Db, FxOrderSet, ProgramEnvironment}; use ty_python_core::definition::Definition; -use ty_python_core::place::{PlaceExpr, PlaceExprRef}; +use ty_python_core::place::PlaceExpr; use ty_python_core::scope::FileScopeId; use ty_python_core::{SemanticIndex, place_table}; @@ -209,10 +209,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If `value` is a valid reference, we attempt type narrowing by assignment. if !value_ty.is_unknown() { if let Some(expr) = PlaceExpr::try_from_expr(subscript) { - let (place, keys) = self.infer_place_load( - PlaceExprRef::from(&expr), - ast::ExprRef::Subscript(subscript), - ); + let (place, keys) = self.infer_place_load(expr, ast::ExprRef::Subscript(subscript)); constraint_keys.extend(keys); if let Place::Defined(DefinedPlace { ty, From 6d8d1b92d4b8636d10a88d38aceaddc29b6425f7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 12 Aug 2026 02:45:30 +0500 Subject: [PATCH 370/390] [`pylint`] Improve handling of concatenated strings (`PLE1300`) (#27659) ## Summary https://docs.astral.sh/ruff/rules/bad-string-format-character/ (PLE1300) and https://docs.astral.sh/ruff/rules/percent-format-invalid-format/ (F509) check the same thing regarding percent string formatters (previously reported in https://github.com/astral-sh/ruff/issues/11403). This PR doesn't try to actually merge these rules, as it's postponed until new categorization introduced. The easy fixable problem I've noticed with `PLE1300` - it doesn't rely on general cformat parsing in `expression.rs` and it has its own code, which is a) reparsing string again using `CFormatString::from_str`, when it's already done in `expression.rs` when processing other similar rules b) in case of concatenated strings it's feeding each string to the parser separately, which may result in a false negative (though it's unlikely to occur in practice) - it was noted in the test case, but it's trivially solved just by passing full `StringLiteralValue` value into it. https://github.com/astral-sh/ruff/blob/ed768f07cc536b54c0c8bc333831497110ba0499/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py#L32-L34 As a result we get minor performance gain, removed redundant code, rules now will stay in sync, until they actually get merged. A minor note: previously `"%z" "%z"` would be reported by `PLE1300` twice - once for each concatenated string. Now there will be just 1 report, matching `F509`. But it doesn't seem important as it's a very rare case. ## Test Plan All previous tests pass, one previous test started to correctly mark true positive. --- .../pylint/bad_string_format_character.py | 2 +- .../src/checkers/ast/analyze/expression.rs | 13 ++++---- .../rules/bad_string_format_character.rs | 31 ++----------------- ...LE1300_bad_string_format_character.py.snap | 11 +++++++ 4 files changed, 20 insertions(+), 37 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py b/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py index 3dcea82d8f..eb1d9b2a55 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/bad_string_format_character.py @@ -29,7 +29,7 @@ f"{1:z}" # [bad-format-character] -## False negatives +## Supporting concatenated strings print(("%" "z") % 1) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 922e07ebf1..feedfa8373 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1469,6 +1469,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { Rule::PercentFormatPositionalCountMismatch, Rule::PercentFormatStarRequiresSequence, Rule::PercentFormatUnsupportedFormatCharacter, + Rule::BadStringFormatCharacter, ]) { let location = expr.range(); match pyflakes::cformat::CFormatSummary::try_from(value.to_str()) { @@ -1483,6 +1484,11 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { }, location, ); + // PLE1300 + checker.report_diagnostic_if_enabled( + pylint::rules::BadStringFormatCharacter { format_char: c }, + location, + ); } Err(e) => { // F501 @@ -1535,13 +1541,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { if checker.is_rule_enabled(Rule::PrintfStringFormatting) { pyupgrade::rules::printf_string_formatting(checker, bin_op, format_string); } - if checker.is_rule_enabled(Rule::BadStringFormatCharacter) { - pylint::rules::bad_string_format_character::percent( - checker, - expr, - format_string, - ); - } if checker.is_rule_enabled(Rule::BadStringFormatType) { pylint::rules::bad_string_format_type(checker, bin_op, format_string); } diff --git a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs index d853f603a5..6aca8f4dd0 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/bad_string_format_character.rs @@ -1,14 +1,10 @@ -use std::str::FromStr; - use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::{Expr, ExprStringLiteral, StringFlags, StringLiteral}; use ruff_python_literal::{ - cformat::{CFormatErrorType, CFormatString}, format::FormatPart, format::FromTemplate, format::{FormatSpec, FormatSpecError, FormatString}, }; -use ruff_text_size::{Ranged, TextRange}; +use ruff_text_size::TextRange; use crate::Violation; use crate::checkers::ast::Checker; @@ -29,7 +25,7 @@ use crate::checkers::ast::Checker; #[derive(ViolationMetadata)] #[violation_metadata(stable_since = "v0.0.283")] pub(crate) struct BadStringFormatCharacter { - format_char: char, + pub(crate) format_char: char, } impl Violation for BadStringFormatCharacter { @@ -72,26 +68,3 @@ pub(crate) fn call(checker: &Checker, string: &str, range: TextRange) { } } } - -/// PLE1300 -/// Ex) `"%z" % "1"` -pub(crate) fn percent(checker: &Checker, expr: &Expr, format_string: &ExprStringLiteral) { - for StringLiteral { - value: _, - node_index: _, - range, - flags, - } in &format_string.value - { - let string = checker.locator().slice(range); - let string = &string - [usize::from(flags.opener_len())..(string.len() - usize::from(flags.closer_len()))]; - - // Parse the format string (e.g. `"%s"`) into a list of `PercentFormat`. - if let Err(format_error) = CFormatString::from_str(string) { - if let CFormatErrorType::UnsupportedFormatChar(format_char) = format_error.typ { - checker.report_diagnostic(BadStringFormatCharacter { format_char }, expr.range()); - } - } - } -} diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap index cad0c10cd5..4049f2e97d 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE1300_bad_string_format_character.py.snap @@ -67,6 +67,17 @@ PLE1300 Unsupported format character 'y' 21 | "{0:.{foo}{bar}{foobar}y}".format(...) # OK (cannot validate after nested placeholders) | +PLE1300 Unsupported format character 'z' + --> bad_string_format_character.py:34:7 + | +32 | ## Supporting concatenated strings +33 | +34 | print(("%" "z") % 1) + | ^^^^^^^^^^^^^ +35 | +36 | ## `%b` is only valid for bytes formatting. + | + PLE1300 Unsupported format character 'b' --> bad_string_format_character.py:37:1 | From 17097a60ab56bb7919054324af03dfc968189f07 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 11 Aug 2026 18:13:19 -0400 Subject: [PATCH 371/390] Use eight-core runners for native Linux PGO builds (#27662) ## Summary Previously, our native Linux x86-64 and ARM64 PGO release builds both used four-core Depot runners. The ARM64 build finished in 13m 39s and could delay the release. We now use pinned, eight-core Ubuntu 24.04 Depot runners for both native PGO targets, matching ty. Linux i686, cross builds, musl builds, and source distributions retain their four-core runners. The previous eight-core ARM benchmark finished in 12m 25s, down from 13m 39s (9%). --- .github/actionlint.yaml | 2 +- .github/workflows/build-binaries.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index a57d94905e..c6411637e1 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -11,7 +11,7 @@ self-hosted-runner: - namespace-profile-macos-15 - namespace-profile-windows-2022-x86-64-16x32 - depot-ubuntu-22.04-arm-4 - - depot-ubuntu-24.04-arm-4 + - depot-ubuntu-24.04-arm-8 - github-windows-2025-x86_64-8 - github-windows-2025-x86_64-16 - codspeed-macro diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index a8d13e2319..e25be8d8e6 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -265,7 +265,7 @@ jobs: linux: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: depot-ubuntu-24.04-4 + runs-on: ${{ matrix.target == 'x86_64-unknown-linux-gnu' && 'depot-ubuntu-24.04-8' || 'depot-ubuntu-24.04-4' }} strategy: matrix: target: @@ -335,7 +335,7 @@ jobs: linux-aarch64: if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }} - runs-on: depot-ubuntu-24.04-arm-4 + runs-on: depot-ubuntu-24.04-arm-8 env: # see https://github.com/astral-sh/ruff/issues/3791 # and https://github.com/gnzlbg/jemallocator/issues/170#issuecomment-1503228963 From 8f11b10b926b1625cf35a5babb44854e2bbd423d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9r=C3=A8?= Date: Tue, 11 Aug 2026 16:02:01 -0700 Subject: [PATCH 372/390] [ty] Initialize `PlaceLoadResolution::crosses_scope_declaration` with a meaningful value. (#27667) ## Summary As a quick follow-up to https://github.com/astral-sh/ruff/pull/27319 this adjusts how we compute `PlaceLoadResolution::crosses_scope_declaration`, which declares whether or not a place load traverses a `global` or `nonlocal` declaration. Previously, we initialized that value unconditionally as `false` and then set it either (1) when we crossed a `global`/`nonlocal` declaration at any point during the lazy resolution process, (2) before continuing on to non-local sources if the place itself was declared `global` or `nonlocal`. That second condition is now used to initialize `crosses_scope_declaration` from the outset, since it is correct to do so and simplifies the implementation of an upcoming accessor for the field. ## Test Plan This is a refactor that relies on existing test coverage. --- crates/ty_python_semantic/src/place_load.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/src/place_load.rs b/crates/ty_python_semantic/src/place_load.rs index d9e062df68..37c8d0591f 100644 --- a/crates/ty_python_semantic/src/place_load.rs +++ b/crates/ty_python_semantic/src/place_load.rs @@ -312,12 +312,14 @@ impl<'db> Iterator for PlaceLoadResolution<'db, '_> { impl<'db, 'ast> PlaceLoadResolution<'db, 'ast> { fn new(context: PlaceLoadResolutionContext<'db, 'ast>, place_expr: PlaceExpr) -> Self { + let crosses_scope_declaration = + context.symbol_has_scope_declaration(PlaceExprRef::from(&place_expr)); Self { context, place_expr, next_node: Some(PlaceLoadResolutionNode::LocalSource), constraints: PlaceLoadConstraints::default(), - crosses_scope_declaration: false, + crosses_scope_declaration, } } @@ -334,8 +336,6 @@ impl<'db, 'ast> PlaceLoadResolution<'db, 'ast> { { let indexed_symbol = place_table.symbol(symbol_id); symbol_is_local = indexed_symbol.is_local(); - self.crosses_scope_declaration |= - indexed_symbol.is_global() || indexed_symbol.is_nonlocal(); let class_body_global_fallback = self.context.is_class_body_scope() && symbol_is_local; if self.context.skips_non_global_scopes(symbol_id) || class_body_global_fallback { @@ -863,6 +863,19 @@ struct PlaceLoadResolutionContext<'db, 'ast> { } impl<'db> PlaceLoadResolutionContext<'db, '_> { + fn symbol_has_scope_declaration(self, place_expr: PlaceExprRef) -> bool { + let Some(symbol) = place_expr.as_symbol() else { + return false; + }; + let scope = self.scope.file_scope_id(self.db); + let table = self.index.place_table(scope); + let Some(symbol_id) = table.symbol_id(symbol.name()) else { + return false; + }; + let symbol = table.symbol(symbol_id); + symbol.is_global() || symbol.is_nonlocal() + } + fn is_class_body_scope(self) -> bool { self.scope.node(self.db).scope_kind().is_class() } From 672d75c73fff97d89954b1c0d22e4c337d770bbf Mon Sep 17 00:00:00 2001 From: Auguste Lalande Date: Tue, 11 Aug 2026 21:30:06 -0400 Subject: [PATCH 373/390] [ty] Resolve generic type aliases subscripted inside `type[]` (#27663) ## Summary This adds inference for `type[Alias[int]]`, where `Alias` is a generic implicit or PEP 613 alias such as `Alias = list[T]`. ## Test Plan Added mdtests. --------- Co-authored-by: Carl Meyer --- .../resources/mdtest/implicit_type_aliases.md | 214 ++++++++++++++++++ .../types/infer/builder/type_expression.rs | 7 +- 2 files changed, 220 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index d1cb3caba1..601b9fa1f1 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -1354,6 +1354,220 @@ def _( reveal_type(invalid_subclass_of_literal) # revealed: ``` +### Subscripted generic alias inside `type[…]` + +A generic alias can also be specialized inside a `type[…]` annotation. + +#### Valid specializations + +The PEP 613 spelling and `typing.Type[…]` take the same path: + +```py +from typing import Generic, Type, TypeAlias, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class Pair(Generic[T, U]): ... + +PairAlias = Pair[T, U] +PairAliasExplicit: TypeAlias = Pair[T, U] + +def implicit(x: type[PairAlias[int, str]]): + reveal_type(x) # revealed: type[Pair[int, str]] + +def pep_613(x: type[PairAliasExplicit[int, str]]): + reveal_type(x) # revealed: type[Pair[int, str]] + +def uppercase_type(x: Type[PairAlias[int, str]]): + reveal_type(x) # revealed: type[Pair[int, str]] + +def partially_specialized(x: type[PairAlias[int, T]]): + reveal_type(x) # revealed: type[Pair[int, T@partially_specialized]] +``` + +#### Incorrect type-argument counts + +A generic alias specialized inside `type[…]` must receive the correct number of type arguments: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class Pair(Generic[T, U]): ... + +PairAlias = Pair[T, U] + +def _( + # error: [invalid-type-arguments] "No type argument provided for required type variable `U`" + too_few: type[PairAlias[int]], + # error: [invalid-type-arguments] "Too many type arguments: expected 2, got 3" + too_many: type[PairAlias[int, str, bool]], +): + reveal_type(too_few) # revealed: type[Pair[Unknown, Unknown]] + reveal_type(too_many) # revealed: type[Pair[Unknown, Unknown]] +``` + +#### Type-variable bounds + +Specializing an alias inside `type[…]` enforces the upper bound of its type variable: + +```py +from typing import Generic, TypeVar + +Bounded = TypeVar("Bounded", bound=int) + +class BoundedBox(Generic[Bounded]): ... + +BoundedAlias = BoundedBox[Bounded] + +def _( + # error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `Bounded@BoundedAlias`" + violated_bound: type[BoundedAlias[str]], +): + reveal_type(violated_bound) # revealed: type[BoundedBox[Unknown]] +``` + +#### Type-variable constraints + +Specializing an alias inside `type[…]` also enforces constraints on its type variable: + +```py +from typing import Generic, TypeVar + +Constrained = TypeVar("Constrained", int, str) + +class ConstrainedBox(Generic[Constrained]): ... + +ConstrainedAlias = ConstrainedBox[Constrained] + +def _( + # error: [invalid-type-arguments] "Type `bytes` does not satisfy constraints `int`, `str` of type variable `Constrained@ConstrainedAlias`" + violated_constraint: type[ConstrainedAlias[bytes]], +): + reveal_type(violated_constraint) # revealed: type[ConstrainedBox[Unknown]] +``` + +#### Bounds on union-valued aliases + +The upper bound of a type variable is enforced even when its alias resolves to a union: + +```py +from typing import TypeVar + +Bounded = TypeVar("Bounded", bound=int) +BoundedUnionAlias = list[Bounded] | set[Bounded] + +def _( + # error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `Bounded@BoundedUnionAlias`" + union_violated_bound: type[BoundedUnionAlias[str]], +): + reveal_type(union_violated_bound) # revealed: type[list[Unknown] | set[Unknown]] +``` + +#### Invalid nested subscripts + +Subscripting an already-subscripted alias inside `type[…]` is invalid, just as it is outside it: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class Pair(Generic[T, U]): ... + +PairAlias = Pair[T, U] + +def _( + # error: [invalid-type-form] "Only simple names and dotted names can be subscripted in parameter annotations" + double_subscript: type[PairAlias[T, U][int, str]], +): + reveal_type(double_subscript) # revealed: type[Unknown] +``` + +#### Assignments to class-backed aliases + +An object assigned to a specialized alias inside `type[…]` must match the class it represents: + +```py +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class Pair(Generic[T, U]): ... + +PairAlias = Pair[T, U] + +# error: [invalid-assignment] "Object of type `` is not assignable to `type[Pair[int, str]]`" +assigned: type[PairAlias[int, str]] = int +``` + +#### Assignments to union-valued aliases + +An object assigned to a union-valued alias inside `type[…]` must match one of the union elements: + +```py +from typing import TypeVar + +T = TypeVar("T") +UnionAlias = list[T] | set[T] + +# error: [invalid-assignment] "Object of type `` is not assignable to `type[list[int] | set[int]]`" +assigned_union: type[UnionAlias[int]] = str +``` + +#### Other alias representations + +An alias does not have to be backed by a class. Stringified, transparent, `Annotated` and +union-valued aliases all specialize inside `type[…]` the same way they do outside it: + +```py +from __future__ import annotations + +from typing import Annotated, TypeAlias, TypeVar + +T = TypeVar("T") + +StringAlias: TypeAlias = "list[T]" +TransparentAlias: TypeAlias = T +AnnotatedAlias = Annotated[list[T], "metadata"] +UnionAlias = list[T] | set[T] + +def _( + string: type[StringAlias[int]], + transparent: type[TransparentAlias[int]], + annotated: type[AnnotatedAlias[int]], + union: type[UnionAlias[int]], +): + reveal_type(string) # revealed: type[list[int]] + reveal_type(transparent) # revealed: type[int] + reveal_type(annotated) # revealed: type[list[int]] + reveal_type(union) # revealed: type[list[int] | set[int]] +``` + +#### Callable aliases + +A callable is not a class object, so specializing a `Callable` alias inside `type[…]` is rejected, +just as a directly spelled callable is: + +```py +from typing import Callable, TypeVar + +T = TypeVar("T") + +CallableAlias = Callable[[T], T] + +def _( + # error: [invalid-type-form] "The argument to `type[]` must be a class object type" + callable_: type[CallableAlias[int]], +): + reveal_type(callable_) # revealed: type[Unknown] +``` + ### `Type[…]` The same also works for `typing.Type[…]`: diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 4b5ff55402..0d92e39c31 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1297,6 +1297,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_expression(slice, TypeContext::default()); KnownClass::NoneType.to_subclass_of(db, env) } + ast::Expr::Subscript(ast::ExprSubscript { value, .. }) if !is_dotted_name(value) => { + infer_type_argument(self, slice) + } ast::Expr::Subscript( subscript @ ast::ExprSubscript { value, @@ -1380,7 +1383,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> { value_ty @ (Type::SpecialForm( SpecialFormType::Top | SpecialFormType::Bottom | SpecialFormType::Annotated, ) - | Type::KnownInstance(KnownInstanceType::TypeAliasType(_))) => { + | Type::KnownInstance(_) + | Type::GenericAlias(_) + | Type::Callable(_)) => { let slice_ty = self.infer_subscript_type_expression(subscript, value_ty); subclass_of_type_argument(self, slice, slice_ty) } From 95af58922eb517456dd75d668247f38d0c11c044 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 11 Aug 2026 22:18:36 -0400 Subject: [PATCH 374/390] [ty] Diagnose invalid __getattribute__ calls (#27506) ## Summary Previously, an invalid `__getattribute__` method caused us to discard the implicit call failure even though Python invokes the method before accessing any attribute: ```python class Example: defined: bool = True def __getattribute__(self) -> str: return "fallback" Example().defined # error: [invalid-attribute-access] Example().missing # error: [invalid-attribute-access] ``` We now propagate failed implicit `__getattribute__` calls through member lookup, preserve the declared member or method return type for error recovery, and report the invalid call before descriptor or `__getattr__` fallback. This applies to both instance and metaclass attribute access. --- .../resources/mdtest/attributes.md | 153 ++++++++++++++++ .../resources/mdtest/descriptor_protocol.md | 39 ++++ crates/ty_python_semantic/src/types.rs | 169 +++++++++++++----- crates/ty_python_semantic/src/types/class.rs | 16 +- .../src/types/class/static_literal.rs | 99 ++++++++-- .../src/types/diagnostic.rs | 23 ++- 6 files changed, 434 insertions(+), 65 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 016f1f2c27..07c14d19c7 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2788,6 +2788,8 @@ def _(ns: argparse.Namespace): ## Classes with custom `__getattribute__` methods +### Basic + If a type provides a custom `__getattribute__`, we use its return type as the type for unknown attributes. Note that this behavior differs from runtime, where `__getattribute__` is called unconditionally, even for known attributes. The rationale for doing this is that it allows users to @@ -2846,6 +2848,113 @@ class ThisFails: ThisFails().x ``` +### Invalid `__getattribute__` calls + +An invalid `__getattribute__` call fails before Python can look up either a defined or missing +attribute. A defined member retains its declared type, while a missing member uses the method's +return type for error recovery. + +```py +class InvalidGetAttribute: + defined: bool = True + + # error: [invalid-method-override] + def __getattribute__(self) -> str: + return "fallback" + +InvalidGetAttribute().missing # snapshot: invalid-attribute-access + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type `InvalidGetAttribute`" +reveal_type(InvalidGetAttribute().missing) # revealed: str + +# error: [invalid-attribute-access] "Invalid access to attribute `defined` on type `InvalidGetAttribute`" +reveal_type(InvalidGetAttribute().defined) # revealed: bool + +# error: [invalid-attribute-access] "Invalid access to attribute `__getattribute__` on type `InvalidGetAttribute`" +InvalidGetAttribute().__getattribute__ +``` + +```snapshot +error[invalid-attribute-access]: Invalid access to attribute `missing` on type `InvalidGetAttribute` + --> src/mdtest_snippet.py:8:1 + | +8 | InvalidGetAttribute().missing # snapshot: invalid-attribute-access + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Too many positional arguments to bound method `InvalidGetAttribute.__getattribute__`: expected 1, got 2 +info: This access implicitly calls `__getattribute__` +info: Method signature here + --> src/mdtest_snippet.py:5:9 + | +5 | def __getattribute__(self) -> str: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``` + +An incompatible type for the attribute name also makes the implicit call invalid. + +```py +class InvalidNameType: + # error: [invalid-method-override] + def __getattribute__(self, name: int) -> bytes: + return b"fallback" + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type `InvalidNameType`" +reveal_type(InvalidNameType().missing) # revealed: bytes +``` + +### Inherited invalid `__getattribute__` calls + +An invalid interceptor inherited from a base class also prevents access to attributes declared on +the subclass. + +```py +class InvalidBase: + # error: [invalid-method-override] + def __getattribute__(self) -> int: + return 1 + +class Child(InvalidBase): + defined: str = "hello" + +# error: [invalid-attribute-access] "Invalid access to attribute `defined` on type `Child`" +reveal_type(Child().defined) # revealed: str +``` + +### Invalid `__getattribute__` installed by a metaclass + +A metaclass can install an invalid interceptor in the namespace of each class it creates. + +```py +def invalid_getattribute(self) -> int: + return 1 + +class Meta(type): + def __init__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, object]) -> None: + # error: [invalid-assignment] + cls.__getattribute__ = invalid_getattribute + +class Example(metaclass=Meta): + defined: str = "hello" + +# error: [invalid-attribute-access] "Invalid access to attribute `defined` on type `Example`" +reveal_type(Example().defined) # revealed: str +``` + +### Invalid `__getattribute__` takes precedence over `__getattr__` + +An invalid `__getattribute__` raises before Python can call an otherwise valid `__getattr__` method. + +```py +class CustomAccess: + # error: [invalid-method-override] + def __getattribute__(self) -> int: + return 1 + + def __getattr__(self, name: str) -> str: + return "fallback" + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type `CustomAccess`" +reveal_type(CustomAccess().missing) # revealed: int +``` + ## Metaclasses with custom `__getattr__` methods A class is an instance of its metaclass. When attribute lookup on a class fails, Python falls back @@ -2970,6 +3079,50 @@ class Foo(metaclass=Meta): ... reveal_type(Foo.whatever) # revealed: int ``` +### Invalid `__getattribute__` calls + +A malformed metaclass `__getattribute__` prevents access to both defined and missing class +attributes. Their original types remain available for error recovery. + +```py +class Meta(type): + # error: [invalid-method-override] + def __getattribute__(cls) -> int: + return 1 + +class Foo(metaclass=Meta): + defined: str = "hello" + +# error: [invalid-attribute-access] "Invalid access to attribute `missing` on type ``" +reveal_type(Foo.missing) # revealed: int + +# error: [invalid-attribute-access] "Invalid access to attribute `defined` on type ``" +reveal_type(Foo.defined) # revealed: str + +# error: [invalid-attribute-access] "Invalid access to attribute `__getattribute__` on type ``" +Foo.__getattribute__ +``` + +### Inherited invalid `__getattribute__` calls + +A malformed interceptor inherited by a metaclass still runs before looking up attributes declared on +the class object. + +```py +class InvalidBaseMeta(type): + # error: [invalid-method-override] + def __getattribute__(cls) -> int: + return 1 + +class Meta(InvalidBaseMeta): ... + +class Foo(metaclass=Meta): + defined: str = "hello" + +# error: [invalid-attribute-access] "Invalid access to attribute `defined` on type ``" +reveal_type(Foo.defined) # revealed: str +``` + ### Class attributes take precedence ```py diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index 43b2330708..ade2257d3a 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -1287,6 +1287,24 @@ class C: reveal_type(C().value) # revealed: int ``` +### An unknown `__getattribute__` can bypass descriptors + +A dynamic base may provide an attribute interceptor that avoids a malformed descriptor, so the +descriptor access cannot be guaranteed to fail. + +```py +from typing import Any + +class Descriptor: + def __get__(self) -> int: + return 1 + +class C(Any): + value = Descriptor() + +reveal_type(C().value) # revealed: int +``` + ### An instance `__getattribute__` may delegate to descriptor lookup The return annotation of an override does not establish whether it delegates to the default @@ -1307,6 +1325,27 @@ class C: C().value ``` +### An invalid `__getattribute__` runs before descriptors + +A malformed `__getattribute__` fails before it can invoke a malformed descriptor. The diagnostic +therefore describes the `__getattribute__` call while preserving the descriptor's return type. + +```py +class Descriptor: + def __get__(self) -> int: + return 1 + +class C: + value = Descriptor() + + # error: [invalid-method-override] + def __getattribute__(self) -> str: + return "fallback" + +# error: [invalid-attribute-access] "Invalid access to attribute `value` on type `C`" +reveal_type(C().value) # revealed: int +``` + ### An assigned instance attribute shadows a non-data descriptor An instance attribute takes precedence over a non-data descriptor. After the assignment, reading the diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 6e5a36d816..476b2229fc 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -23,6 +23,7 @@ use smallvec::smallvec_inline; use ty_module_resolver::{ImportingFile, KnownModule, Module, ModuleName, resolve_module}; pub(crate) use self::callable::UpcastPolicy; +use self::class::ClassInstanceFlags; pub use self::cyclic::CycleDetector; pub(crate) use self::cyclic::TypeTransformer; pub(crate) use self::diagnostic::TypeCheckDiagnostics; @@ -67,7 +68,8 @@ pub(crate) use crate::types::class_base::ClassBase; use crate::types::constraints::ConstraintSetBuilder; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; use crate::types::diagnostic::{ - INVALID_AWAIT, INVALID_TYPE_FORM, report_bad_dunder_get_call, report_bad_dunder_getattr_call, + AttributeAccessMethod, INVALID_AWAIT, INVALID_TYPE_FORM, report_bad_attribute_access_call, + report_bad_dunder_get_call, }; pub use crate::types::display::{DisplaySettings, TypeDetail, TypeDisplayDetails}; pub(crate) use crate::types::enums::{EnumClassLiteral, EnumComplementType, enum_metadata}; @@ -622,6 +624,12 @@ enum MemberLookupErrorKind<'db> { receiver: Type<'db>, name: Type<'db>, }, + + /// An invalid attribute-interception call, represented by its receiver and attribute name. + GetAttribute { + receiver: Type<'db>, + name: Type<'db>, + }, } /// A failed member lookup together with the member used to recover from the error. @@ -665,21 +673,38 @@ impl<'db> MemberLookupError<'db> { target, ); } - MemberLookupErrorKind::GetAttr { receiver, name } - if assigned_type.is_none() - && let Err(CallDunderError::CallError(kind, bindings, _)) = receiver - .try_call_dunder( - db, - env, - "__getattr__", - CallArguments::positional([name]), - TypeContext::default(), - ) => - { - let failure = CallError(kind, bindings); - report_bad_dunder_getattr_call(context, &failure, object_type, target); + kind @ (MemberLookupErrorKind::GetAttr { receiver, name } + | MemberLookupErrorKind::GetAttribute { receiver, name }) => { + let method = if matches!(kind, MemberLookupErrorKind::GetAttr { .. }) { + AttributeAccessMethod::GetAttr + } else { + AttributeAccessMethod::GetAttribute + }; + + if method == AttributeAccessMethod::GetAttr && assigned_type.is_some() { + return; + } + + if let Err(CallDunderError::CallError(kind, bindings, _)) = receiver + .try_call_dunder( + db, + env, + method.as_str(), + CallArguments::positional([name]), + TypeContext::default(), + ) + { + let failure = CallError(kind, bindings); + report_bad_attribute_access_call( + context, + &failure, + object_type, + target, + method, + ); + } } - MemberLookupErrorKind::DescriptorGet(_) | MemberLookupErrorKind::GetAttr { .. } => {} + MemberLookupErrorKind::DescriptorGet(_) => {} } } } @@ -6575,6 +6600,49 @@ impl<'db> Type<'db> { } } + /// Return whether a custom `__getattribute__` could affect this lookup. + /// + /// Reusing the receiver class's existing MRO classification avoids interning a member-lookup + /// key just to determine whether an override exists. Class objects use their metaclass instead. + /// An unknown base can intercept a missing attribute or bypass a failing descriptor, but cannot + /// invalidate a definitely defined member. + fn custom_getattribute_may_affect_lookup( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + result: MemberLookupResult<'db>, + ) -> bool { + let Some(class) = self.nominal_class(db, env).or_else(|| { + self.to_meta_type(db, env) + .to_instance_approximation(db, env) + .and_then(|instance| instance.nominal_class(db, env)) + }) else { + return true; + }; + + let class = class.class_literal(db); + if class.as_static().is_none() { + return true; + } + + let flags = class.instance_flags(db); + if flags.contains(ClassInstanceFlags::HAS_CUSTOM_GETATTRIBUTE) { + return true; + } + + if !flags.contains(ClassInstanceFlags::HAS_DYNAMIC_GETATTRIBUTE) { + return false; + } + + !matches!( + result, + Ok(PlaceAndQualifiers { + place: Place::Defined(place), + .. + }) if place.is_definitely_defined() + ) + } + /// Apply `__getattr__` / `__getattribute__` fallback to an attribute-lookup result. /// /// A custom `__getattribute__` can intercept even an always-defined normal lookup result. @@ -6616,46 +6684,61 @@ impl<'db> Type<'db> { } }; - let custom_getattribute = OnceCell::new(); - let custom_getattribute = || { - *custom_getattribute.get_or_init(|| { - if "__getattribute__" == name.as_str() { - return (MemberLookupResult::from(Place::Undefined), false); - } + let getattribute_policy = MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK; + if !self.custom_getattribute_may_affect_lookup(db, env, result) + || self + .class_member_with_policy(db, env, "__getattribute__", getattribute_policy) + .place + .is_undefined() + { + return member_lookup_or_fall_back_to(db, env, result, custom_getattr_result); + } - // Skip `object.__getattribute__`, which is the default mechanism we - // already model via the normal attribute-lookup path. - match self.try_call_dunder_with_policy( - db, - env, - "__getattribute__", - &mut CallArguments::positional([Type::string_literal(db, name)]), - TypeContext::default(), - MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK, - ) { - Ok(bindings) => (Place::bound(bindings.return_type(db, env)).into(), true), - Err( - CallDunderError::PossiblyUnbound { .. } | CallDunderError::CallError(..), - ) => (MemberLookupResult::from(Place::Undefined), true), - Err(CallDunderError::MethodNotAvailable) => { - (MemberLookupResult::from(Place::Undefined), false) - } - } - }) + let name_type = Type::string_literal(db, name); + let custom_getattribute = match self.try_call_dunder_with_policy( + db, + env, + "__getattribute__", + &mut CallArguments::positional([name_type]), + TypeContext::default(), + getattribute_policy, + ) { + Ok(bindings) => Place::bound(bindings.return_type(db, env)).into(), + Err(CallDunderError::CallError(_, bindings, _)) => member_lookup_result( + db, + Place::bound(bindings.return_type(db, env)).into(), + Some(MemberLookupErrorKind::GetAttribute { + receiver: self, + name: name_type, + }), + ), + Err(CallDunderError::PossiblyUnbound { .. }) => Place::Undefined.into(), + Err(CallDunderError::MethodNotAvailable) => { + return member_lookup_or_fall_back_to(db, env, result, custom_getattr_result); + } }; + if let Err(error) = custom_getattribute { + let member = result.unwrap_or_else(|error| error.fallback_member(db)); + return Err(MemberLookupError::new( + db, + member.or_fall_back_to(db, env, || error.fallback_member(db)), + error.kind(db), + )); + } + // A custom override runs before the descriptor and might return without invoking it. let result = if matches!( result.err().map(|error| error.kind(db)), Some(MemberLookupErrorKind::DescriptorGet(_)) - ) && custom_getattribute().1 - { + ) { Ok(result.unwrap_or_else(|error| error.fallback_member(db))) } else { result }; - let result = member_lookup_or_fall_back_to(db, env, result, || custom_getattribute().0); + let result = member_lookup_or_fall_back_to(db, env, result, || custom_getattribute); member_lookup_or_fall_back_to(db, env, result, custom_getattr_result) } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 9284d7f32f..3c75c17777 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -111,7 +111,7 @@ fn dynamic_class_header_range<'db>( } bitflags::bitflags! { - /// Properties that affect the representation of instances of a class. + /// Properties shared by all instances of a class. /// /// This combines properties derived from the MRO into the existing class-classification /// query, avoiding a separate cached query for each property. @@ -121,6 +121,10 @@ bitflags::bitflags! { const TYPED_DICT = 1 << 0; /// The class directly or indirectly inherits from an explicit `Any` base. const INHERITS_FROM_EXPLICIT_ANY = 1 << 1; + /// The class may define or inherit a custom `__getattribute__` method. + const HAS_CUSTOM_GETATTRIBUTE = 1 << 2; + /// An unknown base may provide an attribute-interception method. + const HAS_DYNAMIC_GETATTRIBUTE = 1 << 3; } } @@ -610,8 +614,8 @@ impl<'db> ClassLiteral<'db> { MroIterator::new(db, self, None) } - /// Return the properties that affect how instances of this class are represented. - fn instance_flags(self, db: &'db dyn Db) -> ClassInstanceFlags { + /// Return the properties shared by all instances of this class. + pub(super) fn instance_flags(self, db: &'db dyn Db) -> ClassInstanceFlags { match self { Self::Static(literal) => literal.instance_flags(db), Self::DynamicTypedDict(_) => ClassInstanceFlags::TYPED_DICT, @@ -634,6 +638,12 @@ impl<'db> ClassLiteral<'db> { /// Return whether this class directly or indirectly inherits from an explicit `Any` base. pub(super) fn inherits_from_explicit_any(self, db: &'db dyn Db) -> bool { + if let Some(class) = self.as_static() + && (class.known(db).is_some() || !class.has_explicit_bases(db)) + { + return false; + } + self.instance_flags(db) .contains(ClassInstanceFlags::INHERITS_FROM_EXPLICIT_ANY) } diff --git a/crates/ty_python_semantic/src/types/class/static_literal.rs b/crates/ty_python_semantic/src/types/class/static_literal.rs index ef23d92279..c6b23967dd 100644 --- a/crates/ty_python_semantic/src/types/class/static_literal.rs +++ b/crates/ty_python_semantic/src/types/class/static_literal.rs @@ -867,7 +867,51 @@ impl<'db> StaticClassLiteral<'db> { .contains(&ClassBase::Class(other)) } - /// Return the properties that affect how instances of this class are represented. + /// Return whether this class defines its own non-default `__getattribute__`. + /// + /// An explicit metaclass can install the method even when the class body does not define it: + /// + /// ```python + /// def interceptor(self, name): ... + /// + /// class Meta(type): + /// def __init__(cls, *args): + /// cls.__getattribute__ = interceptor + /// + /// class Example(metaclass=Meta): ... + /// ``` + fn has_own_custom_getattribute(self, db: &'db dyn Db) -> bool { + if matches!(self.known(db), Some(KnownClass::Object | KnownClass::Type)) { + return false; + } + + if place_table(db, self.body_scope(db)) + .symbol_id("__getattribute__") + .is_some() + { + return true; + } + + if !self.has_explicit_metaclass(db) { + return false; + } + + let Some(metaclass) = self.metaclass(db).to_class_type(db) else { + return true; + }; + + metaclass.iter_mro(db).any(|base| match base { + ClassBase::Any | ClassBase::Dynamic(_) | ClassBase::Divergent(_) => true, + ClassBase::Class(base) => base.static_class_literal(db).is_none_or(|(base, _)| { + implicit_attribute_names(db, base.body_scope(db)) + .binary_search(&Name::new_static("__getattribute__")) + .is_ok() + }), + ClassBase::Generic | ClassBase::Protocol | ClassBase::TypedDict(_) => false, + }) + } + + /// Return the properties shared by all instances of this class. pub(super) fn instance_flags(self, db: &'db dyn Db) -> ClassInstanceFlags { #[salsa::tracked( returns(copy), @@ -880,28 +924,45 @@ impl<'db> StaticClassLiteral<'db> { ) -> ClassInstanceFlags { let mut flags = ClassInstanceFlags::empty(); for base in class.iter_mro(db, None) { - if base.is_typed_dict() { - flags.insert(ClassInstanceFlags::TYPED_DICT); - } - if base.is_explicit_any_base() { - flags.insert(ClassInstanceFlags::INHERITS_FROM_EXPLICIT_ANY); + match base { + ClassBase::Any => flags.insert( + ClassInstanceFlags::INHERITS_FROM_EXPLICIT_ANY + | ClassInstanceFlags::HAS_DYNAMIC_GETATTRIBUTE, + ), + ClassBase::Dynamic(_) | ClassBase::Divergent(_) => { + flags.insert(ClassInstanceFlags::HAS_DYNAMIC_GETATTRIBUTE); + } + ClassBase::TypedDict(_) => flags.insert(ClassInstanceFlags::TYPED_DICT), + ClassBase::Class(class) + if class + .static_class_literal(db) + .is_none_or(|(class, _)| class.has_own_custom_getattribute(db)) => + { + flags.insert(ClassInstanceFlags::HAS_CUSTOM_GETATTRIBUTE); + } + ClassBase::Class(_) | ClassBase::Generic | ClassBase::Protocol => {} } } flags } - if let Some(known) = self.known(db) { - return if known.is_typed_dict_subclass() { + let mut flags = if let Some(known) = self.known(db) { + if known.is_typed_dict_subclass() { ClassInstanceFlags::TYPED_DICT } else { ClassInstanceFlags::empty() - }; - } + } + } else if self.has_explicit_bases(db) { + return instance_flags_inner(db, self); + } else { + ClassInstanceFlags::empty() + }; - if !self.has_explicit_bases(db) { - return ClassInstanceFlags::empty(); - } - instance_flags_inner(db, self) + flags.set( + ClassInstanceFlags::HAS_CUSTOM_GETATTRIBUTE, + self.has_own_custom_getattribute(db), + ); + flags } /// Return the module defining the `TypedDict` base of this class. @@ -914,8 +975,14 @@ impl<'db> StaticClassLiteral<'db> { /// Return `true` if this class constitutes a typed dict specification (inherits from /// `typing.TypedDict` or `typing_extensions.TypedDict`, either directly or indirectly). pub fn is_typed_dict(self, db: &'db dyn Db) -> bool { - self.instance_flags(db) - .contains(ClassInstanceFlags::TYPED_DICT) + if let Some(known) = self.known(db) { + return known.is_typed_dict_subclass(); + } + + self.has_explicit_bases(db) + && self + .instance_flags(db) + .contains(ClassInstanceFlags::TYPED_DICT) } /// Return `true` if this class is, or inherits from, a `NamedTuple` (inherits from diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 06aec1f75f..1fca7b39bb 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -1821,7 +1821,23 @@ pub(super) fn report_bad_dunder_get_call<'db>( } } -/// Reports an invalid implicit `__getattr__` call at the original attribute access. +/// A special method invoked implicitly while accessing an attribute. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum AttributeAccessMethod { + GetAttr, + GetAttribute, +} + +impl AttributeAccessMethod { + pub(super) const fn as_str(self) -> &'static str { + match self { + Self::GetAttr => "__getattr__", + Self::GetAttribute => "__getattribute__", + } + } +} + +/// Reports an invalid implicit `__getattr__` or `__getattribute__` call. /// /// ```python /// class C: @@ -1831,11 +1847,12 @@ pub(super) fn report_bad_dunder_get_call<'db>( /// ``` /// /// Preserves the underlying call diagnostic and explains why attribute access invoked the method. -pub(super) fn report_bad_dunder_getattr_call<'db>( +pub(super) fn report_bad_attribute_access_call<'db>( context: &InferContext<'db, '_>, failure: &CallError<'db>, object_type: Type<'db>, target: &ast::ExprAttribute, + method: AttributeAccessMethod, ) { let db = context.db(); let env = &context.program_environment(); @@ -1850,7 +1867,7 @@ pub(super) fn report_bad_dunder_getattr_call<'db>( "Invalid access to attribute `{attribute}` on type `{}`", object_type.display(db, env), ), - info: "This access implicitly calls `__getattr__`", + info: &format!("This access implicitly calls `{}`", method.as_str()), argument_ranges: &[target.range()], }, ); From 16049e4545879e2aba2ecc09ee4751b831ded7e5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 11 Aug 2026 22:19:27 -0400 Subject: [PATCH 375/390] [ty] Handle class objects that may be descriptors (#26687) ## Summary When assigning to `C.attribute`, a data descriptor on the metaclass takes precedence over `C`'s class attribute. If the metaclass member is definitely non-data, the class attribute shadows it. For attributes annotated as `TypeForm[...]` or an inexact `type[Base]`, the annotation describes the instances produced by the represented class, but not the metaclass of the runtime class object. So the metaclass member could be _either_ a data descriptor or a non-data value, which means assignment has to be valid under both possible paths... ```python from typing_extensions import TypeForm class DescriptorMeta(type): def __set__(self, instance: object, value: str) -> None: ... class Descriptor(metaclass=DescriptorMeta): ... class Meta(type): attribute: TypeForm[Descriptor] = Descriptor class C(metaclass=Meta): attribute: int = 1 # Invalid under both possible runtime paths: # - If Meta.attribute is a data descriptor, DescriptorMeta.__set__ expects str. # - If Meta.attribute is non-data, C.attribute shadows it and expects int. C.attribute = Descriptor # error: [invalid-assignment] ``` Prior to this change, we treated these class-object types as definitely non-data descriptors. We now preserve their descriptor uncertainty and retain the class member as an alternative write target even when the metaclass member is always defined. Definitely data and definitely non-data members continue to use their single governing write path. --------- Co-authored-by: Carl Meyer --- .../resources/mdtest/descriptor_protocol.md | 151 ++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 10 +- .../src/types/attribute_write.rs | 145 +++++++++++++---- 3 files changed, 275 insertions(+), 31 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index ade2257d3a..bb1ec3de05 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -376,6 +376,157 @@ class UnionC(metaclass=UnionMeta): reveal_type(UnionC.attribute) # revealed: Any | Literal["descriptor"] ``` +### `TypeForm` metaclass attributes + +A `TypeForm` argument describes the instances produced by a type form, not the runtime type form +value itself. A metaclass attribute typed as `TypeForm[Descriptor]` can therefore be a class whose +own metaclass makes it a data descriptor, and must continue to take precedence over a class +attribute with the same name when assigning to the attribute: + +```py +from typing_extensions import TypeForm + +class DescriptorMeta(type): + def __set__(self, instance: object, value: str) -> None: + pass + +class Descriptor(metaclass=DescriptorMeta): ... + +class Meta(type): + attribute: TypeForm[Descriptor] = Descriptor + +class C(metaclass=Meta): + attribute: int = 1 + +C.attribute = 1 # error: [invalid-assignment] +# error: [invalid-assignment] +C.attribute = Descriptor # error: [invalid-assignment] +``` + +A quoted type expression remains valid when both possible write targets accept the same runtime +string: + +```py +class StringC(metaclass=Meta): + attribute: str = "" + +StringC.attribute = "valid" +StringC.attribute = "Descriptor" +``` + +The descriptor setter still rejects a class object even when the fallback attribute accepts that +same class object: + +```py +class TypeFormC(metaclass=Meta): + attribute: TypeForm[Descriptor] = Descriptor + +TypeFormC.attribute = Descriptor # error: [invalid-assignment] +``` + +The same contextual check applies when the metaclass attribute can also hold an ordinary string: + +```py +class UnionMeta(type): + attribute: TypeForm[Descriptor] | str = Descriptor + +class UnionC(metaclass=UnionMeta): + attribute: int = 1 + +UnionC.attribute = 1 # error: [invalid-assignment] +``` + +### Bounded class-object metaclass attributes + +An inexact `type[Base]` attribute can hold a subclass whose custom metaclass makes the class object +a data descriptor. It must therefore continue to take precedence over a class attribute with the +same name when assigning to the attribute: + +```py +class Base: ... + +class DescriptorMeta(type): + def __set__(self, instance: object, value: str) -> None: + pass + +class Descriptor(Base, metaclass=DescriptorMeta): ... + +class Meta(type): + attribute: type[Base] = Descriptor + +class C(metaclass=Meta): + attribute: int = 1 + +C.attribute = 1 # error: [invalid-assignment] +# error: [invalid-assignment] +C.attribute = Descriptor # error: [invalid-assignment] +``` + +An assignment succeeds when both the possible descriptor setter and class attribute accept the +assigned string: + +```py +class StringC(metaclass=Meta): + attribute: str = "" + +StringC.attribute = "valid" +``` + +An assignment fails when the class attribute accepts the assigned class but the descriptor setter +does not: + +```py +class ClassC(metaclass=Meta): + attribute: type[Base] = Base + +ClassC.attribute = Base # error: [invalid-assignment] +``` + +### Broad class-object metaclass attributes + +Both `type[object]` and bare `type` can contain a class whose metaclass implements `__set__`. Their +possible descriptor setters must therefore be checked independently of the class-attribute fallback: + +```py +class Base: ... + +class DescriptorMeta(type): + def __set__(self, instance: object, value: str) -> None: + pass + +class Descriptor(Base, metaclass=DescriptorMeta): ... + +class ObjectMeta(type): + attribute: type[object] = Descriptor + +class ObjectStringC(metaclass=ObjectMeta): + attribute: str = "" + +ObjectStringC.attribute = "valid" + +class ObjectClassC(metaclass=ObjectMeta): + attribute: type[Base] = Base + +ObjectClassC.attribute = Base # error: [invalid-assignment] +``` + +The unparameterized spelling follows the same descriptor and class-attribute paths: + +```py +class BareMeta(type): + attribute: type = Descriptor + +class BareStringC(metaclass=BareMeta): + attribute: str = "" + +BareStringC.attribute = "valid" + +class BareClassC(metaclass=BareMeta): + attribute: type[Base] = Base + +BareClassC.attribute = Base # error: [invalid-assignment] +``` + ### Class objects with unknown metaclasses A `type[Any]` value could contain a class whose metaclass implements the descriptor protocol. We diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 476b2229fc..3502bd6c09 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4171,8 +4171,10 @@ impl<'db> Type<'db> { /// Returns whether this type is known not to be a data descriptor. /// - /// Descriptor uncertainty only propagates through outer unions, intersections, and aliases; - /// type arguments do not affect the runtime descriptor class. + /// Descriptor uncertainty propagates through outer unions, intersections, and aliases. + /// `TypeForm` values and inexact `type[...]` values are also uncertain because their bounds + /// describe the represented instance types, not the runtime values whose metaclasses determine + /// descriptor behavior. fn is_definitely_non_data_descriptor( self, db: &'db dyn Db, @@ -4205,6 +4207,10 @@ impl<'db> Type<'db> { Type::TypeAlias(alias) => alias .value_type(db) .is_definitely_non_data_descriptor_impl(db, program), + Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Type) => { + false + } + Type::TypeForm(_) | Type::SubclassOf(_) => false, _ => !self.may_be_data_descriptor(db, env), } } diff --git a/crates/ty_python_semantic/src/types/attribute_write.rs b/crates/ty_python_semantic/src/types/attribute_write.rs index a23f82d58f..10eccd9433 100644 --- a/crates/ty_python_semantic/src/types/attribute_write.rs +++ b/crates/ty_python_semantic/src/types/attribute_write.rs @@ -9,6 +9,7 @@ use crate::Db; use ty_module_resolver::KnownModule; +use ty_python_core::use_def_map; use super::call::CallArguments; use super::callable::CallableTypeKind; @@ -16,7 +17,9 @@ use super::{ IntersectionType, KnownClass, KnownInstanceType, MemberLookupPolicy, Type, TypeQualifiers, }; use crate::ProgramEnvironment; -use crate::place::{DefinedPlace, Definedness, Place, PlaceAndQualifiers, builtins_symbol}; +use crate::place::{ + DefinedPlace, Definedness, Place, PlaceAndQualifiers, builtins_symbol, place_from_bindings, +}; /// The operation required to write an attribute. /// @@ -106,7 +109,8 @@ pub(super) enum InstanceAttributeWriteMember<'db> { /// /// A data descriptor on the metaclass takes precedence over the class object's own attributes, /// which in turn take precedence over definitely non-data metaclass members. If the metaclass -/// member is absent or possibly undefined, the class object's own attributes form the fallback. +/// member is absent, possibly undefined, or could be a non-data descriptor, the class object's own +/// attributes form the fallback. pub(super) enum ClassAttributeWriteMember<'db> { /// A metaclass member governs the write, optionally alongside a class-attribute fallback. Explicit { @@ -150,7 +154,7 @@ impl ExplicitAttributeWriteRequirement<'_> { } } -/// A write target found through a possibly absent fallback lookup. +/// A receiver-level write target that can govern the write instead of the type member. pub(super) enum FallbackAttributeWriteRequirement<'db> { /// Check the value against `ty`, retaining whether the declaration may be absent at runtime. AssignableTo { @@ -184,8 +188,8 @@ pub(super) enum FallbackAttributeWriteRequirement<'db> { /// ``` pub(super) enum AssignmentAttributeMembers<'db> { /// The type member governs the write, as `Meta.data` does above because it is a data descriptor. - /// If the type member may be missing, the corresponding receiver member (`C.data`) is retained - /// as `receiver_fallback`. + /// If the type member may be missing or may be a non-data descriptor, the corresponding + /// receiver member (`C.data`) is retained as `receiver_fallback`. TypeMember { member: PlaceAndQualifiers<'db>, receiver_fallback: Option>, @@ -440,16 +444,32 @@ fn class_attribute_write_requirement<'db>( let member = match type_member { PlaceAndQualifiers { - place: Place::Defined(DefinedPlace { ty, .. }), + place: Place::Defined(place @ DefinedPlace { ty, .. }), qualifiers, - } => ClassAttributeWriteMember::Explicit { - member: explicit_attribute_write_requirement( - db, env, object_ty, attribute, ty, qualifiers, - ), - fallback: receiver_fallback.map(|fallback| { - class_fallback_write_requirement(db, env, object_ty, class_attr_self_ty, fallback) - }), - }, + } => { + let descriptor_ty = receiver_fallback + .and_then(|_| possible_class_attribute_descriptor(db, env, place)) + .unwrap_or(ty); + ClassAttributeWriteMember::Explicit { + member: explicit_attribute_write_requirement( + db, + env, + object_ty, + attribute, + descriptor_ty, + qualifiers, + ), + fallback: receiver_fallback.map(|fallback| { + class_fallback_write_requirement( + db, + env, + object_ty, + class_attr_self_ty, + fallback, + ) + }), + } + } PlaceAndQualifiers { place: Place::Undefined, .. @@ -478,6 +498,46 @@ fn class_attribute_write_requirement<'db>( AttributeWriteRequirement::Class { object_ty, member } } +/// Recover the concrete descriptor hidden by an uncertain metaclass-member annotation. +/// +/// The declared type describes the descriptor object, not the values accepted by its setter. +/// Inspecting the binding preserves the setter's actual value contract: +/// +/// ```python +/// class DescriptorMeta(type): +/// def __set__(self, instance: object, value: str) -> None: ... +/// +/// class Descriptor(metaclass=DescriptorMeta): ... +/// +/// class Meta(type): +/// attribute: type[object] = Descriptor +/// ``` +fn possible_class_attribute_descriptor<'db>( + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + member: DefinedPlace<'db>, +) -> Option> { + if member.ty.is_data_descriptor(db, env) || member.ty.is_definitely_non_data_descriptor(db, env) + { + return None; + } + + let definition = member.provenance.definition()?; + let use_def = use_def_map(db, definition.scope(db)); + let descriptor_ty = + place_from_bindings(db, env, use_def.end_of_scope_bindings(definition.place(db))) + .place + .ignore_possibly_undefined()?; + let descriptor_ty = match descriptor_ty.resolve_type_alias(db) { + Type::TypeForm(typeform) => typeform.type_argument(db).to_meta_type(db, env), + descriptor_ty => descriptor_ty, + }; + + descriptor_ty + .is_data_descriptor(db, env) + .then_some(descriptor_ty) +} + /// Convert an explicitly resolved member into either a descriptor call or a direct type check. /// /// Descriptor behavior is used only when `__set__` is found with @@ -625,37 +685,64 @@ pub(super) fn property_setter_returns_never<'db>( }) } -/// Return the class member that takes precedence over a definitely non-data metaclass member. -fn class_member_preceding_non_data_metaclass_member<'db>( +/// Resolve class-object members when a class attribute can shadow its metaclass member. +/// +/// A definitely non-data metaclass member is shadowed entirely. If the metaclass member's +/// descriptor status is uncertain, both members remain possible write targets. +/// +/// ```python +/// class Meta(type): +/// attribute = object() +/// +/// class C(metaclass=Meta): +/// attribute: int +/// +/// C.attribute = 1 +/// ``` +fn class_object_assignment_members<'db>( db: &'db dyn Db, env: &ProgramEnvironment<'db>, object_ty: Type<'db>, attribute: &str, type_member: PlaceAndQualifiers<'db>, -) -> Option> { +) -> Option> { if !matches!( object_ty, Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) - ) || !type_member - .place - .ignore_possibly_undefined()? - .is_definitely_non_data_descriptor(db, env) + ) { + return None; + } + + let type_member_ty = type_member.place.ignore_possibly_undefined()?; + let definitely_non_data_descriptor = type_member_ty.is_definitely_non_data_descriptor(db, env); + if !definitely_non_data_descriptor + && (type_member_ty.is_divergent() || type_member_ty.is_data_descriptor(db, env)) { return None; } - object_ty + let receiver_member = object_ty .find_name_in_mro_with_policy(db, env, attribute, MemberLookupPolicy::default()) - .filter(|class_attr| !class_attr.place.is_undefined()) + .filter(|class_attr| !class_attr.place.is_undefined())?; + + Some(if definitely_non_data_descriptor { + AssignmentAttributeMembers::ReceiverMember(receiver_member) + } else { + AssignmentAttributeMembers::TypeMember { + member: type_member, + receiver_fallback: Some(receiver_member), + } + }) } /// Return the members considered by attribute assignment in lookup-precedence order. /// /// The type member comes from class-member lookup. A member found directly on the receiver is /// queried when the type member is absent or possibly undefined. For class objects, a class-MRO -/// member instead takes precedence over a definitely non-data metaclass member. Composite and -/// dynamic receiver types return `None`; their callers either decompose them before this point or -/// handle them without member lookup. +/// member instead takes precedence over a definitely non-data metaclass member and remains an +/// alternative when the metaclass member's descriptor status is uncertain. Composite and dynamic +/// receiver types return `None`; their callers either decompose them before this point or handle +/// them without member lookup. /// /// This helper deliberately does not bind `Self` or interpret descriptors so that assignment, /// protocol compatibility, and `Final` validation share exactly the same lookup precedence. @@ -680,10 +767,10 @@ pub(super) fn assignment_attribute_members<'db>( } else { object_ty.class_member(db, env, attribute) }; - if let Some(receiver_member) = - class_member_preceding_non_data_metaclass_member(db, env, object_ty, attribute, type_member) + if let Some(members) = + class_object_assignment_members(db, env, object_ty, attribute, type_member) { - return Some(AssignmentAttributeMembers::ReceiverMember(receiver_member)); + return Some(members); } let needs_receiver_fallback = matches!( type_member.place, From 845ef6d737255715ee3fc019424a8bed1c67f10b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:19:47 -0400 Subject: [PATCH 376/390] Update Rust crate clap to v4.6.5 (#27675) --- Cargo.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c85da0d78..4573a75a33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,9 +487,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.4" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" dependencies = [ "clap_builder", "clap_derive", @@ -497,9 +497,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" dependencies = [ "anstream 1.0.0", "anstyle", @@ -566,7 +566,7 @@ dependencies = [ "terminfo", "thiserror 2.0.19", "which", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -685,7 +685,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -1035,7 +1035,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -1115,7 +1115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -3839,7 +3839,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -4249,7 +4249,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -4259,7 +4259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -5431,7 +5431,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] From 10f8dc2b74bc2f1007db17f27895e6765ae3521c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:19:54 -0400 Subject: [PATCH 377/390] Update Rust crate globset to v0.4.20 (#27676) --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4573a75a33..b182875960 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1375,9 +1375,9 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "globset" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" dependencies = [ "aho-corasick", "bstr", @@ -2989,9 +2989,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", From 2a1f6f6fdd60f482f1168adc9f47924f4cd2c665 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:20:18 -0400 Subject: [PATCH 378/390] Update dependency prek to v0.4.12 (#27671) --- pyproject.toml | 2 +- uv.lock | 114 ++++++++++++++++++++++++------------------------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e7dccf8b8c..c0816c28e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ exclude = [ [dependency-groups] dev = [ - "prek==0.4.11", + "prek==0.4.12", ] release = [ "rooster==0.1.1", diff --git a/uv.lock b/uv.lock index b3b2bd7135..c789281c0d 100644 --- a/uv.lock +++ b/uv.lock @@ -30,8 +30,8 @@ name = "anyio" version = "4.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", marker = "python_full_version == '3.12.*'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } wheels = [ @@ -43,7 +43,7 @@ name = "anysqlite" version = "0.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.12'" }, + { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/4b/cd5d66b9f87e773bc71344a368b9472987e33514e6627e28342b9c3e7c43/anysqlite-0.0.5.tar.gz", hash = "sha256:9dfcf87baf6b93426ad1d9118088c41dbf24ef01b445eea4a5d486bac2755cce", size = 3432, upload-time = "2023-10-02T13:49:25.135Z" } wheels = [ @@ -64,7 +64,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "python_full_version >= '3.12' and implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -176,11 +176,11 @@ name = "hishel" version = "0.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.12'" }, - { name = "anysqlite", marker = "python_full_version >= '3.12'" }, - { name = "httpx", marker = "python_full_version >= '3.12'" }, - { name = "msgpack", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "anyio" }, + { name = "anysqlite" }, + { name = "httpx" }, + { name = "msgpack" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/64/a104ccac48f123f853254483617b16e0efc1649bd7e35bcdc5a5a5ef0ae2/hishel-0.1.5.tar.gz", hash = "sha256:9d40c682cd94fd6e1394fb05713ae20a75ed8aeba6f5272380444039ce6257f2", size = 75468, upload-time = "2025-10-18T13:32:41.854Z" } wheels = [ @@ -192,8 +192,8 @@ name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi", marker = "python_full_version >= '3.12'" }, - { name = "h11", marker = "python_full_version >= '3.12'" }, + { name = "certifi" }, + { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ @@ -205,10 +205,10 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.12'" }, - { name = "certifi", marker = "python_full_version >= '3.12'" }, - { name = "httpcore", marker = "python_full_version >= '3.12'" }, - { name = "idna", marker = "python_full_version >= '3.12'" }, + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ @@ -229,7 +229,7 @@ name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mdurl", marker = "python_full_version >= '3.12'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ @@ -338,26 +338,26 @@ wheels = [ [[package]] name = "prek" -version = "0.4.11" +version = "0.4.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/1a/73b6dae5ce7e997cb35a69bfe1d25a798e85fa3d2eabf95324563f461b30/prek-0.4.11.tar.gz", hash = "sha256:4a14cb9bbae850605ae3904fbdbb12f0e00c12455efaa2266da8fb8e5c0350d7", size = 516254, upload-time = "2026-07-24T17:05:35.107Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/5c/cb6e63f7e5a58a5313ddb70409174f4dc004e4b0910b8a8d3f59b2225a95/prek-0.4.12.tar.gz", hash = "sha256:04beeba7f40437cd2f36804b84101bd7f3c9fb40b52da46a25604642ab2bfb09", size = 519080, upload-time = "2026-08-03T11:28:33.147Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/d7/a00b2de492a80e99b1698e72c1d196ac3ed544dc7ee0ada261ac066e78e0/prek-0.4.11-py3-none-linux_armv6l.whl", hash = "sha256:3830cb7cc47e837888b8b464ecb21355a69235cfacef0fd89101e17f09345d63", size = 5770511, upload-time = "2026-07-24T17:05:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/c1/1e/f97c74defcd5d5645888cb99fcdd9b8b48cc7247cf31e64414d106d56d66/prek-0.4.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:45facadf9c2332b28e6ab2744312ae0275a93ab5a437da8bcc53a8c7260cb4b0", size = 6118049, upload-time = "2026-07-24T17:05:14.451Z" }, - { url = "https://files.pythonhosted.org/packages/d9/92/8367d26421ee6fe6019a63928fb0ed31179cd0d6199879c524f89ef4c95c/prek-0.4.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2f8a194b1d00d24dff8baff691c99e2668339da2da3482b4ee99c1a0f2409378", size = 5601478, upload-time = "2026-07-24T17:05:15.81Z" }, - { url = "https://files.pythonhosted.org/packages/e1/6f/7617c9b87afaadede4167720aae7ef12d7e51167db903bc8fbac0cadef7b/prek-0.4.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:21d93e6d76bf3d7a9bb70c6ac86ef372adaee50068c45749e6b9e1c2ac4ec939", size = 5932071, upload-time = "2026-07-24T17:05:17.217Z" }, - { url = "https://files.pythonhosted.org/packages/ef/41/6796a4011b04212333259064aa885a71d03d9581ba9bff52db3ce58d1f06/prek-0.4.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7fb07cde2d2156efa6980122b3f13dc88f20c84b933c6205675d0f1e7be2cde8", size = 5677617, upload-time = "2026-07-24T17:05:18.658Z" }, - { url = "https://files.pythonhosted.org/packages/03/5f/3e339901f8460b6073313619b4fd9bf4135e48430695c53640b83ace88ea/prek-0.4.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b7f5e446d2aca739bd18b380578e9c78bb4c086299abc3e167d51c47840c56f", size = 6106370, upload-time = "2026-07-24T17:05:20.219Z" }, - { url = "https://files.pythonhosted.org/packages/8e/4e/94e24b5c1910ec15692ecaf33d0d8ef0d02a02a8b5e40d3c976396880cba/prek-0.4.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7059d640e595d098600e2d97af961f46292b4112670edbe632de84f6828389e", size = 6884342, upload-time = "2026-07-24T17:05:21.587Z" }, - { url = "https://files.pythonhosted.org/packages/a2/7c/fc0daa033dcafe74990c00af2da1e16922790b9c1b8da7e8eebf1123838b/prek-0.4.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a4df33998fcac878bce3b2c624e1caeb9609f3d562c640702c1234ed815daf", size = 6331365, upload-time = "2026-07-24T17:05:22.87Z" }, - { url = "https://files.pythonhosted.org/packages/d7/36/49f152b8f539930e9685cff0509695ce4054abcecc22f4c16c4e1e5c23d0/prek-0.4.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:866991f387527c5f880ce2cc3ddea9b33cc5b986881f8c7f92524cf0969c1350", size = 5939075, upload-time = "2026-07-24T17:05:24.249Z" }, - { url = "https://files.pythonhosted.org/packages/4f/fe/7b097af9161edae7ddedcc9f5cda4a5f31346a492ce3f92583d96c46f628/prek-0.4.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:247e5d8740e137ebdf24fa96f01a95b2d2f1892e956a1ab00c7b1474a59ebcc0", size = 5799029, upload-time = "2026-07-24T17:05:25.652Z" }, - { url = "https://files.pythonhosted.org/packages/49/63/dc955ff99e1002d3cd375b21467c87046bc41deddfce86d898240757b151/prek-0.4.11-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:603ba9f2fd9d666dddb3ae190a25a5c55091b843cde90ed52e0a1116f50d4062", size = 5651211, upload-time = "2026-07-24T17:05:27.027Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b8/bf2139ec25eefb5afef43afac7620c5181f2c2661f220598beacb1771207/prek-0.4.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f2682ece3c5fc7201106c4fdd84b0587ccea4b8a2ecefa3e94d3074d2841f1df", size = 5954784, upload-time = "2026-07-24T17:05:28.391Z" }, - { url = "https://files.pythonhosted.org/packages/fc/29/3fe5990aee1bd7c4d50a03358ad867c5e912d9510aafb83a359c7347e5fa/prek-0.4.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:22721d30394192931fecc80d6fc6dd47e8ddf8db8b9693805aba8ec0f50087ec", size = 6448916, upload-time = "2026-07-24T17:05:29.837Z" }, - { url = "https://files.pythonhosted.org/packages/0f/fb/abddacf43738302242ecd237236c7c80cd7c1d27545e16e803770aee76e9/prek-0.4.11-py3-none-win32.whl", hash = "sha256:8b093e7624522146049e994d5cf283d01d71632b638620b79ae0f96afeaa2624", size = 5483539, upload-time = "2026-07-24T17:05:31.228Z" }, - { url = "https://files.pythonhosted.org/packages/00/1e/c293f7a15cb93963c4be36a02e144b93f43906bc02644a3f04e5708e7453/prek-0.4.11-py3-none-win_amd64.whl", hash = "sha256:5a3d7c80b970b456e5f1bcec8382008ee1ae6a3f324a6b9bb4ff7e666ab0f3c4", size = 5861119, upload-time = "2026-07-24T17:05:32.479Z" }, - { url = "https://files.pythonhosted.org/packages/cd/0c/05fe6eb9d6a54d0e02dfa8cc5ad6f86869bf953419ec15909a32466a28ee/prek-0.4.11-py3-none-win_arm64.whl", hash = "sha256:e7b0df37ce05e45a14a9da39ab104691474d72f139bf4f6c860f754763a322cb", size = 5626386, upload-time = "2026-07-24T17:05:33.813Z" }, + { url = "https://files.pythonhosted.org/packages/f3/23/5811a3161e072e5f93e4da01af611ee30c32922507b8ab4d9873df6affd3/prek-0.4.12-py3-none-linux_armv6l.whl", hash = "sha256:cd92000b051e433f26340821cf1cc8e6e3960f1275f3d516ca01f05905abba64", size = 5793226, upload-time = "2026-08-03T11:28:09.534Z" }, + { url = "https://files.pythonhosted.org/packages/a3/88/8607845d94eb1482e1bd335dadf098618f077a15775f7e98de99669052b4/prek-0.4.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5904fe6c6ab26e7d8792a3c7f1e3fc8d94fcfb63ad33b247c35f004b62cb6275", size = 6132269, upload-time = "2026-08-03T11:28:11.147Z" }, + { url = "https://files.pythonhosted.org/packages/ac/28/571d79ba457fbd9ecf40ae879c91952e12f5fa475306218c91139b86db7a/prek-0.4.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:df3eff1db9c24dc293010a07bc7a0ae0c541d55af828f5586405dedc28c4920d", size = 5614964, upload-time = "2026-08-03T11:28:12.983Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a9/3f5cb79a73c764a8ac38d5bcd51e0df57239856eca7949b09bdac4338bf3/prek-0.4.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c7733b44ca772ea32ec6a8bee669d0358bdf45873e79767afed196065084f31c", size = 5941047, upload-time = "2026-08-03T11:28:14.45Z" }, + { url = "https://files.pythonhosted.org/packages/8c/00/1dfed0ef8af10c5c32aa903486dccd33d2df171f3d945a037c5692f10760/prek-0.4.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87f170cf1ffd6e3a196f947b83dff1f6c2cd68635f8d49740278bebe7b682262", size = 5707994, upload-time = "2026-08-03T11:28:15.914Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bd/5f388f6cbdc0445b850e7c1a160d0be67fcef8bf221e3c8141a1feccef17/prek-0.4.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57dad513831f060cf73808df8edec29d46ec311435aa69f21c80edebf23dc5e1", size = 6133784, upload-time = "2026-08-03T11:28:17.184Z" }, + { url = "https://files.pythonhosted.org/packages/ba/47/342091a987bf68a74acec6d226a40ce7d51faf0019aa4126cc7bc952f8a7/prek-0.4.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b204844abc7ded983471f576ae8dc13b99e9b8d022e4d4b46176c6654769c9d8", size = 6901589, upload-time = "2026-08-03T11:28:18.545Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8a/3ef7bdc3c3441649ebc040b9e164a13163e1e5fabae23e7bbb901992f3de/prek-0.4.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b0a5a9d3f2f77871fdcb7893bfc5c8fe7e44f4e603ce6e4712bfec96b2d6f2", size = 6342189, upload-time = "2026-08-03T11:28:20Z" }, + { url = "https://files.pythonhosted.org/packages/c4/da/6277908442301b1b92a2879f6b04aaa03accb900f80e42776fc28b8197ef/prek-0.4.12-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0d188e572c306cc44b96e1bae5647e25b7bd311113f3f3f4a67320c257ee64a3", size = 5951250, upload-time = "2026-08-03T11:28:21.339Z" }, + { url = "https://files.pythonhosted.org/packages/a3/68/bff51a7332837edb1ecbe017325adb7fafd69b9c7828ddc81a1334b884af/prek-0.4.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:986f52d104b7066190f0f32aebe3467710356de265e9bfd892101ba99371db4d", size = 5804147, upload-time = "2026-08-03T11:28:22.656Z" }, + { url = "https://files.pythonhosted.org/packages/aa/de/b7f544971072ed7814125145dfeb1f7c15cce6b78ccea65a96298ff37838/prek-0.4.12-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:13e34d9e09bafcbf1f25a01cf86985e2c5e486591d3f45b2786ba3de82e5153a", size = 5680104, upload-time = "2026-08-03T11:28:24.271Z" }, + { url = "https://files.pythonhosted.org/packages/68/94/95942bcc20a6a91ec2989aa30fdeb00ad095be736ec48b4bbcf0376166b1/prek-0.4.12-py3-none-musllinux_1_1_i686.whl", hash = "sha256:3d0208370da73e8b5bc97f2492dc3975f8dd2c22f4bf6e1f2cf3342503764b52", size = 5975030, upload-time = "2026-08-03T11:28:25.683Z" }, + { url = "https://files.pythonhosted.org/packages/ef/6d/26e6497198d81cf9aa82495400aef46adea8df3e4a4efc5f00e3b6ab3292/prek-0.4.12-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b1005f42920111bec1403c25e8f2f12ec7af0be06686cc3b8dcf85429af908a8", size = 6458532, upload-time = "2026-08-03T11:28:27.121Z" }, + { url = "https://files.pythonhosted.org/packages/44/02/ee140c2eb4701bd194db429d84630733492be94897d5f72b61d6f11e6619/prek-0.4.12-py3-none-win32.whl", hash = "sha256:afee229488dcceaea282288e4d7096a93da5a8b85649d9ef506dbdbcd78f38a7", size = 5502213, upload-time = "2026-08-03T11:28:28.691Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/744cff84def48c1ce38c0b4f643a3553c66976c5bb7869ab7317044870e4/prek-0.4.12-py3-none-win_amd64.whl", hash = "sha256:fdd27bad8adafea8fe77606950ca09200d59296a47ab131cfb88718d460949d7", size = 5868065, upload-time = "2026-08-03T11:28:30.377Z" }, + { url = "https://files.pythonhosted.org/packages/46/1d/e2c0fc222904ef73df1739b11a83edc29e38bc4bc61259f2ca6d2f15abb0/prek-0.4.12-py3-none-win_arm64.whl", hash = "sha256:45e34a24fba4a4e4568682477158591698efc2375b8d1d418ae424691c4bd01b", size = 5632819, upload-time = "2026-08-03T11:28:31.743Z" }, ] [[package]] @@ -374,10 +374,10 @@ name = "pydantic" version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types", marker = "python_full_version >= '3.12'" }, - { name = "pydantic-core", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, - { name = "typing-inspection", marker = "python_full_version >= '3.12'" }, + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ @@ -389,7 +389,7 @@ name = "pydantic-core" version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ @@ -519,7 +519,7 @@ name = "pygit2" version = "1.19.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "python_full_version >= '3.12'" }, + { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/44/415aa93422b4bfc21a6448acb7e16280d5f33a9a3fae38a384e37b046ae4/pygit2-1.19.3.tar.gz", hash = "sha256:a543e6d4ebb43825564935758dc234e770016fed673b84370d46ae9580558831", size = 810489, upload-time = "2026-06-13T08:06:04.982Z" } wheels = [ @@ -594,8 +594,8 @@ name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "markdown-it-py" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ @@ -607,14 +607,14 @@ name = "rooster" version = "0.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "hishel", marker = "python_full_version >= '3.12'" }, - { name = "httpx", marker = "python_full_version >= '3.12'" }, - { name = "marko", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pydantic", marker = "python_full_version >= '3.12'" }, - { name = "pygit2", marker = "python_full_version >= '3.12'" }, - { name = "tqdm", marker = "python_full_version >= '3.12'" }, - { name = "typer", marker = "python_full_version >= '3.12'" }, + { name = "hishel" }, + { name = "httpx" }, + { name = "marko" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pygit2" }, + { name = "tqdm" }, + { name = "typer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/02/8ce565271dc52bd0d0d812043b12ec60111d947f81dc30301d19d7bfd453/rooster-0.1.1.tar.gz", hash = "sha256:c9823122f0c2b035985e70384323cdd353477af988e0f065bc302646a49da482", size = 18608, upload-time = "2025-10-29T15:18:49.478Z" } wheels = [ @@ -637,7 +637,7 @@ release = [ [package.metadata] [package.metadata.requires-dev] -dev = [{ name = "prek", marker = "python_full_version >= '3.12'", specifier = "==0.4.11" }] +dev = [{ name = "prek", marker = "python_full_version >= '3.12'", specifier = "==0.4.12" }] release = [{ name = "rooster", marker = "python_full_version >= '3.12'", specifier = "==0.1.1" }] [[package]] @@ -654,7 +654,7 @@ name = "tqdm" version = "4.68.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } wheels = [ @@ -666,10 +666,10 @@ name = "typer" version = "0.26.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "rich", marker = "python_full_version >= '3.12'" }, - { name = "shellingham", marker = "python_full_version >= '3.12'" }, + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } wheels = [ @@ -690,7 +690,7 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ From 0d773b07bffd6c43916e13ee2b75f3bfb0c59494 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:20:33 -0400 Subject: [PATCH 379/390] Update dependency ruff to v0.16.2 (#27672) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 9187a9af0d..69fb672360 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ PyYAML==6.0.3 -ruff==0.16.1 +ruff==0.16.2 mkdocs==1.6.1 mkdocs-material==9.7.7 mkdocs-redirects==1.2.3 From c52b66d217ab57fd544fb9ac9f7c5b55913084da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:20:35 -0400 Subject: [PATCH 380/390] Update Rust crate aho-corasick to v1.1.5 (#27674) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b182875960..d2ae8d288c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] From 2ff9cfd317b562d91b74573fc9628baab79dd9a2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 02:29:44 +0000 Subject: [PATCH 381/390] Update Rust crate ignore to v0.4.33 (#27677) --- Cargo.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2ae8d288c..915e24991c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -566,7 +566,7 @@ dependencies = [ "terminfo", "thiserror 2.0.19", "which", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -685,7 +685,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -1035,7 +1035,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -1115,7 +1115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -1598,9 +1598,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f8a7b8211e695a1d0cd91cace480d4d0bd57667ab10277cc412c5f7f4884f83" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" dependencies = [ "crossbeam-deque", "globset", @@ -3839,7 +3839,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -4249,7 +4249,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] @@ -4259,7 +4259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.0", + "windows-sys 0.59.0", ] [[package]] @@ -5431,7 +5431,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.52.0", ] [[package]] From 0e85931a80817f423394ceeed5ddd3d5018e2212 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:43:07 -0400 Subject: [PATCH 382/390] Update prek dependencies (#27673) --- .pre-commit-config.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 94890905af..5f0b1f04b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: priority: 0 - repo: https://github.com/crate-ci/typos - rev: bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # frozen: v1.48.0 + rev: 8a48f81b6c64dcfea44b3633223084c4be58ac5f # frozen: v1.49.0 hooks: - id: typos priority: 0 @@ -63,7 +63,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/zizmorcore/zizmor-pre-commit - rev: 067260dc5fe6ea86b7551bfd6f8b3ba4e6c93129 # frozen: v1.28.0 + rev: 451b56af716f9f0d0c2b816503a3fd0cf8b036fa # frozen: v1.29.0 hooks: - id: zizmor priority: 0 @@ -102,7 +102,7 @@ repos: - id: mdformat language: python # means renovate will also update `additional_dependencies` additional_dependencies: - - mdformat-mkdocs==5.2.1 + - mdformat-mkdocs==5.3.0 - mdformat-footnote==0.1.3 exclude: | (?x)^( @@ -113,13 +113,13 @@ repos: priority: 0 - repo: https://github.com/astral-sh/uv-pre-commit - rev: 8ff2449591c8de025b17661ba76d60237a1ae62b # frozen: 0.12.1 + rev: 8d582f54b8e4cc5a61a85eeed1f54bbdb1e294fc # frozen: 0.12.3 hooks: - id: uv-lock priority: 0 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 39d9ac5938dadb73df0564a45f163e25ff9fa6e2 # frozen: v0.16.1 + rev: 7c55798a78262d14b2074abf623d8a992ebb70d4 # frozen: v0.16.2 hooks: - id: ruff-format exclude: crates/ty_python_semantic/resources/corpus/ @@ -127,7 +127,7 @@ repos: # Priority 1: Second-pass fixers (e.g., markdownlint-fix runs after mdformat). - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 39d9ac5938dadb73df0564a45f163e25ff9fa6e2 # frozen: v0.16.1 + rev: 7c55798a78262d14b2074abf623d8a992ebb70d4 # frozen: v0.16.2 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix] @@ -150,7 +150,7 @@ repos: # Priority 2: ruffen-docs runs after markdownlint-fix (both modify markdown). - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 39d9ac5938dadb73df0564a45f163e25ff9fa6e2 # frozen: v0.16.1 + rev: 7c55798a78262d14b2074abf623d8a992ebb70d4 # frozen: v0.16.2 hooks: - id: ruff-format name: mdtest format From fe9af4fd9d06a2fb2ff91519b6c60fa5f9979686 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:41:31 +0200 Subject: [PATCH 383/390] Update CodSpeedHQ/action action to v5 (#27686) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 197bc00015..470babdb8c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1091,7 +1091,7 @@ jobs: run: cargo codspeed build -m simulation -m memory --features "codspeed,ruff_instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser - name: "Run benchmarks" - uses: CodSpeedHQ/action@f22792bfac16f3e14eb9fbea76f4a48e9cc22b93 # v4.19.1 + uses: CodSpeedHQ/action@0ca9cbbf4623b599a6c3ed4fc8a922942705d9f1 # v5.0.2 with: mode: "simulation,memory" run: cargo codspeed run @@ -1203,7 +1203,7 @@ jobs: run: find target/codspeed -type f -exec chmod +x {} + - name: "Run benchmarks" - uses: CodSpeedHQ/action@f22792bfac16f3e14eb9fbea76f4a48e9cc22b93 # v4.19.1 + uses: CodSpeedHQ/action@0ca9cbbf4623b599a6c3ed4fc8a922942705d9f1 # v5.0.2 with: mode: ${{ matrix.mode }} run: cargo codspeed run --bench "${{ matrix.target }}" "${{ matrix.filter }}" @@ -1307,7 +1307,7 @@ jobs: run: find target/codspeed -type f -exec chmod +x {} + - name: "Run benchmarks" - uses: CodSpeedHQ/action@f22792bfac16f3e14eb9fbea76f4a48e9cc22b93 # v4.19.1 + uses: CodSpeedHQ/action@0ca9cbbf4623b599a6c3ed4fc8a922942705d9f1 # v5.0.2 env: # enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't # appear to provide much useful insight for our walltime benchmarks right now From 56098063bc7c3aef67c079381e59d2e55736ac8d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:41:46 +0200 Subject: [PATCH 384/390] Update taiki-e/install-action action to v2.85.8 (#27680) --- .github/workflows/ci.yaml | 16 ++++++++-------- .github/workflows/sync_typeshed.yaml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 470babdb8c..2e67d43d72 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -339,7 +339,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest and insta" - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: | cargo-nextest @@ -405,7 +405,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest" - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-nextest - name: "Install uv" @@ -444,7 +444,7 @@ jobs: - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-nextest - name: "Install uv" @@ -1083,7 +1083,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-codspeed @@ -1122,7 +1122,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-codspeed @@ -1188,7 +1188,7 @@ jobs: version: "0.12.1" - name: "Install codspeed" - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-codspeed @@ -1242,7 +1242,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-codspeed @@ -1292,7 +1292,7 @@ jobs: version: "0.12.1" - name: "Install codspeed" - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: cargo-codspeed diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index 6239e9ed91..939eecdfea 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -268,7 +268,7 @@ jobs: - name: "Install mold" uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 - name: "Install cargo nextest and insta" - uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 + uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 with: tool: | cargo-nextest From 25e915447ef8b95cd00ecd6a1c9fc3a618341943 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:42:19 +0200 Subject: [PATCH 385/390] Update Rust crate similar to v3.1.2 (#27679) --- Cargo.lock | 14 +++++------ fuzz/Cargo.lock | 64 ++++++++++++++++++++++++------------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 915e24991c..9ddea2aca0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2110,7 +2110,7 @@ dependencies = [ "rustc-stable-hash", "salsa", "serde", - "similar 3.1.1", + "similar 3.1.2", "smallvec", "thiserror 2.0.19", "toml 1.1.4+spec-1.1.0", @@ -3214,7 +3214,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "similar 3.1.1", + "similar 3.1.2", "supports-hyperlinks", "tempfile", "thiserror 2.0.19", @@ -3256,7 +3256,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "similar 3.1.1", + "similar 3.1.2", "strum", "tempfile", "toml 1.1.4+spec-1.1.0", @@ -3375,7 +3375,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "similar 3.1.1", + "similar 3.1.2", "smallvec", "strum", "strum_macros", @@ -3539,7 +3539,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "similar 3.1.1", + "similar 3.1.2", "smallvec", "static_assertions", "thiserror 2.0.19", @@ -4082,9 +4082,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "similar" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6505efef05804732ed8a3f2d4f279429eb485bd69d5b0cc6b19cc02005cda16" +checksum = "85ee016af5d736b69fc89e19254540fa4b5f5492853fb5503920f084011c78b6" dependencies = [ "bstr", ] diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 10def6231b..2288fb1b6e 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -1510,7 +1510,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anstyle", "memchr", @@ -1519,7 +1519,7 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.7" +version = "0.0.8" dependencies = [ "char_str", "filetime", @@ -1532,7 +1532,7 @@ dependencies = [ [[package]] name = "ruff_db" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anstyle", "arc-swap", @@ -1570,7 +1570,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.7" +version = "0.0.8" dependencies = [ "get-size2", "is-macro", @@ -1580,7 +1580,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.7" +version = "0.0.8" dependencies = [ "drop_bomb", "ruff_cache", @@ -1595,7 +1595,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.7" +version = "0.0.8" dependencies = [ "get-size2", "ruff_macros", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.16.1" +version = "0.16.2" dependencies = [ "aho-corasick", "anyhow", @@ -1662,7 +1662,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.7" +version = "0.0.8" dependencies = [ "heck", "itertools 0.15.0", @@ -1675,14 +1675,14 @@ dependencies = [ [[package]] name = "ruff_memory_usage" -version = "0.0.7" +version = "0.0.8" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "rand 0.10.2", @@ -1697,7 +1697,7 @@ dependencies = [ [[package]] name = "ruff_python_ast" -version = "0.0.7" +version = "0.0.8" dependencies = [ "aho-corasick", "arrayvec", @@ -1721,7 +1721,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.7" +version = "0.0.8" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -1732,7 +1732,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "clap", @@ -1760,7 +1760,7 @@ dependencies = [ [[package]] name = "ruff_python_importer" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "ruff_diagnostics", @@ -1773,7 +1773,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.7" +version = "0.0.8" dependencies = [ "ruff_python_ast", "ruff_python_trivia", @@ -1783,7 +1783,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.7" +version = "0.0.8" dependencies = [ "bitflags 2.13.0", "icu_properties", @@ -1793,7 +1793,7 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.7" +version = "0.0.8" dependencies = [ "bitflags 2.13.0", "bstr", @@ -1813,7 +1813,7 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.7" +version = "0.0.8" dependencies = [ "bitflags 2.13.0", "is-macro", @@ -1830,7 +1830,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.7" +version = "0.0.8" dependencies = [ "bitflags 2.13.0", "unicode-ident", @@ -1838,7 +1838,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" -version = "0.0.7" +version = "0.0.8" dependencies = [ "itertools 0.15.0", "ruff_source_file", @@ -1849,7 +1849,7 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.7" +version = "0.0.8" dependencies = [ "ruff_db", "ruff_text_size", @@ -1859,7 +1859,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.7" +version = "0.0.8" dependencies = [ "get-size2", "memchr", @@ -1869,7 +1869,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.7" +version = "0.0.8" dependencies = [ "get-size2", "serde", @@ -2028,9 +2028,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "similar" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6505efef05804732ed8a3f2d4f279429eb485bd69d5b0cc6b19cc02005cda16" +checksum = "85ee016af5d736b69fc89e19254540fa4b5f5492853fb5503920f084011c78b6" dependencies = [ "bstr", ] @@ -2316,7 +2316,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.7" +version = "0.0.8" dependencies = [ "ordermap", "ruff_db", @@ -2326,7 +2326,7 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.7" +version = "0.0.8" dependencies = [ "anyhow", "camino", @@ -2349,7 +2349,7 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.7" +version = "0.0.8" dependencies = [ "bitflags 2.13.0", "bitvec", @@ -2377,7 +2377,7 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.7" +version = "0.0.8" dependencies = [ "bitflags 2.13.0", "char_str", @@ -2417,7 +2417,7 @@ dependencies = [ [[package]] name = "ty_site_packages" -version = "0.0.7" +version = "0.0.8" dependencies = [ "camino", "colored", @@ -2437,14 +2437,14 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.7" +version = "0.0.8" dependencies = [ "ruff_macros", ] [[package]] name = "ty_vendored" -version = "0.0.7" +version = "0.0.8" dependencies = [ "path-slash", "ruff_db", From 98aae2ec7b311879503356b07ba46e4ef6990cb2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:42:36 +0200 Subject: [PATCH 386/390] Update dependency pyrefly to v1.2.0 (#27682) --- scripts/ty_benchmark/pyproject.toml | 2 +- scripts/ty_benchmark/uv.lock | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/scripts/ty_benchmark/pyproject.toml b/scripts/ty_benchmark/pyproject.toml index b34da1e599..5bcb0a4ce9 100644 --- a/scripts/ty_benchmark/pyproject.toml +++ b/scripts/ty_benchmark/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ # Pyright is missing because we install it with `npm` to avoid measuring the overhead # of the Python wrapper script (that lazily installs Pyright). "mslex>=1.3.0", - "pyrefly==1.1.1", + "pyrefly==1.2.0", "pytest-benchmark>=4.0.0", "pytest>=8.0.0", "pygls>=2.0.0", diff --git a/scripts/ty_benchmark/uv.lock b/scripts/ty_benchmark/uv.lock index f121b4c1d6..dd30a3fb58 100644 --- a/scripts/ty_benchmark/uv.lock +++ b/scripts/ty_benchmark/uv.lock @@ -116,21 +116,21 @@ wheels = [ [[package]] name = "pyrefly" -version = "1.1.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/20/976165fa4b1517a1a92f393b3f4d4badabfff1165eff09d4cd4908428183/pyrefly-1.1.1.tar.gz", hash = "sha256:6deda959f8603a7dbdf112c48983e2275b2903cf33c8c739ed65d7e71a4fd520", size = 5880491, upload-time = "2026-06-18T23:45:43.785Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/01/a86e9f24722b095c3f88e3616132b75a21b0df53804bdc6a45314dd4d93c/pyrefly-1.2.0.tar.gz", hash = "sha256:5485f960fc2481617068c918335c39ab1507ef90b6b5bd35bf57726e60e73185", size = 6243654, upload-time = "2026-08-01T02:56:27.592Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/d6/02ba666018c6a1cb4ddfa2db98ada721adddd374db5c29ba47a0bf2637fa/pyrefly-1.1.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f4b8595f91885bc8b5e3c282ab68d1df21201668a84e6508b1e15f2feec0bb8d", size = 13631867, upload-time = "2026-06-18T23:45:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/71/47/7a3457dbbddb513a83cf4fe527d5d5ebda5201a1010ad2a6034030e3e358/pyrefly-1.1.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6b238e1362622d47a6eb5af704fd8b613c94e8c303386efd6350e3da59fecc8", size = 13075304, upload-time = "2026-06-18T23:45:16.865Z" }, - { url = "https://files.pythonhosted.org/packages/84/df/70f4b3f42d58ed686a80df31e04eca54d88036cea4f9b96195c64ad0b2b5/pyrefly-1.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b50d4510e4f8aaea79e2c4b343a4d7a060c9451c0b2aa9bfe10d7ca1ef33d68d", size = 13446966, upload-time = "2026-06-18T23:45:19.644Z" }, - { url = "https://files.pythonhosted.org/packages/3c/53/12a19bd6c7af985bcbc13c6910d0f9f6684069ead2282a5c08c2bfbb5d03/pyrefly-1.1.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f330cf039ef3da3b910c84f3a7e431f0cf8d0c1d2dad26491d6cadf3c7cd4759", size = 14449222, upload-time = "2026-06-18T23:45:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/93/f0/e55c48a50076fc0f9ecf4bdedec50456db383e01162f5e2121f8468be071/pyrefly-1.1.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6342d87c52b04f72156da04f554c4d57f3616f2b32d1763969efb22d05a1407", size = 14472947, upload-time = "2026-06-18T23:45:24.858Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e7/30e085b31fed978ecb675bdbb54df566673ab550469e5af2d350f6af0be6/pyrefly-1.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c08b814ad03175e9cf47111390537161828b472044c39ab3320252b3ac6b2edd", size = 13975252, upload-time = "2026-06-18T23:45:27.247Z" }, - { url = "https://files.pythonhosted.org/packages/47/58/49c3e67641133d3fe5d8d9a660dc0826c6c37ca197d86cad05fa7dd8bfd6/pyrefly-1.1.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d50cad97f19fc893b04deff7239626cffff5dd27ffb29b7d303a1b770247b208", size = 13471780, upload-time = "2026-06-18T23:45:29.775Z" }, - { url = "https://files.pythonhosted.org/packages/71/1e/65a7ba8355e2c39d8331832905fb74dcc85fc122a3f1dfd6dbf2a88907ad/pyrefly-1.1.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2150b450ee6a6bcbe69b2d45d9a4ebc934a609e1abcf65e490433f38eb873d84", size = 13989306, upload-time = "2026-06-18T23:45:32.576Z" }, - { url = "https://files.pythonhosted.org/packages/37/de/b7ee1ab2392c36945738246fba7524439810befa3cfcc03cb6157567fc10/pyrefly-1.1.1-py3-none-win32.whl", hash = "sha256:5ffd8a8ed62fe4e6bf0afe1837d1bad149bb3b9f80e928ef248c96b836db3742", size = 12608469, upload-time = "2026-06-18T23:45:35.419Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9c/a0f5b52934bf80e9c7eff08222e7caf318287b9aef76acb8d9ac5740581b/pyrefly-1.1.1-py3-none-win_amd64.whl", hash = "sha256:4e0430f3ef69c8ac73505fd6584db70ed504665a9f0816fef7f723de510f26cb", size = 13502172, upload-time = "2026-06-18T23:45:38.375Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/4c6bcb3d456835f51445d3662a428f56c3ea5643ec798c577030ae34298c/pyrefly-1.1.1-py3-none-win_arm64.whl", hash = "sha256:83baf0db71e172665db1fca0ced50b8f7773f5192ca57e8ac6773a772b6d2fc5", size = 12895979, upload-time = "2026-06-18T23:45:41.026Z" }, + { url = "https://files.pythonhosted.org/packages/7d/9d/3c0ef1d4843987b22f996ed381ec9cf5a3b1273e29804db276252e4c95eb/pyrefly-1.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7f46d983ac49ddd2b043694960a01dc6a19a5cfd8eec609d6bd9c42866f91b4e", size = 14026305, upload-time = "2026-08-01T02:56:02.611Z" }, + { url = "https://files.pythonhosted.org/packages/0a/06/03bbb78fbea54cdc65b626619f3597d5611aca4fdef11e72a4e8360e7e63/pyrefly-1.2.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:756f669b5555090f5c1a4fef30db1785fabe657764f7e4e6dc88994dfb8ca82d", size = 13463880, upload-time = "2026-08-01T02:56:04.93Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/7d8bc00a38e93bbc9c3e7bd14d305f7948717e667c9bcddeab9dd42fd255/pyrefly-1.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3465812ce5ef4781fb592edbf2724547296f0a3124be115d73c7e8b2401862d", size = 13907329, upload-time = "2026-08-01T02:56:07.104Z" }, + { url = "https://files.pythonhosted.org/packages/be/94/9e08b4bf799d0b8f36b55a2783c7ba5f51730cf0632a85a67b5b5ed876cd/pyrefly-1.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5de7b2ad2bba5c8055181681a84b74143eac2234a48ba5d1b7ed7e7a722b02bd", size = 15039020, upload-time = "2026-08-01T02:56:09.208Z" }, + { url = "https://files.pythonhosted.org/packages/5b/bd/bca5fd0c80f4daf8ee6903a29df9f3de1feb05ff0946b8f35ec8c5096b13/pyrefly-1.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25822ea9505f589ea8a725e4268b475132fb89e038fbf092e446510443ac142a", size = 14986199, upload-time = "2026-08-01T02:56:11.924Z" }, + { url = "https://files.pythonhosted.org/packages/97/f7/f07087f3d185ad2eced0c56cef89ca5474dfb4ff25f146cd50a861c97553/pyrefly-1.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90efe75e17491ef5d636e10469e9278d7d0256b3b4c5e1f4750069bf3ae0f5d1", size = 14393715, upload-time = "2026-08-01T02:56:14.143Z" }, + { url = "https://files.pythonhosted.org/packages/d3/70/0d142c320e284b9e3ce35e9b1e58b8ce2ee1f578f2a7234bc30e5022b94f/pyrefly-1.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:368aaf7eee4f511ddc0f8e564cf14e01ab2f10b0db9105c6d5b153bf498d07bf", size = 13933008, upload-time = "2026-08-01T02:56:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e8/e84f11b6e1f63fd453ad3654213b9a0f6f4de8cef6b58038eef2d0d5955d/pyrefly-1.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d52d5da7bc65fb7675fbaa80eda879d4f8787c494f04cac21603330d3abbdbbe", size = 14431827, upload-time = "2026-08-01T02:56:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/0f/06/810d31380f66c75e1c0779a408d3b16117b1b368b57894f6aa66bef21686/pyrefly-1.2.0-py3-none-win32.whl", hash = "sha256:8c90751de8506d938e8f802659c74cf35bd7a0036510ee6c634a38eebb280bfa", size = 13229447, upload-time = "2026-08-01T02:56:20.921Z" }, + { url = "https://files.pythonhosted.org/packages/ed/98/4dafa3c7a1caed2dc8cc708dde09ba27963c7736508f55b626fff3024113/pyrefly-1.2.0-py3-none-win_amd64.whl", hash = "sha256:8a8964c224ccc4882730130955815de21ff443c1ac3f0b90685b19bf63848170", size = 14087387, upload-time = "2026-08-01T02:56:23.188Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1c/df3cb0a2e5591660ded7a1836cd2f29dc48c91adb1c0a3a700a96f6d09e1/pyrefly-1.2.0-py3-none-win_arm64.whl", hash = "sha256:3a90bb8df39dfbac74b1f3b2e9d7c526b8f80568884c3944d955023a73ebf61e", size = 13430873, upload-time = "2026-08-01T02:56:25.425Z" }, ] [[package]] @@ -180,7 +180,7 @@ requires-dist = [ { name = "lsprotocol", specifier = ">=2025.0.0" }, { name = "mslex", specifier = ">=1.3.0" }, { name = "pygls", specifier = ">=2.0.0" }, - { name = "pyrefly", specifier = "==1.1.1" }, + { name = "pyrefly", specifier = "==1.2.0" }, { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-benchmark", specifier = ">=4.0.0" }, ] From 2936d578019d7844e1be01f84fb6668c4613cf89 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:42:55 +0200 Subject: [PATCH 387/390] Update dependency astral-sh/uv to v0.12.3 (#27670) --- .github/workflows/ci.yaml | 28 ++++++++++---------- .github/workflows/daily_fuzz.yaml | 2 +- .github/workflows/memory_report.yaml | 2 +- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/sync_typeshed.yaml | 6 ++--- .github/workflows/ty-ecosystem-analyzer.yaml | 4 +-- .github/workflows/ty-ecosystem-report.yaml | 2 +- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2e67d43d72..0ae975fe8a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -347,7 +347,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" enable-cache: "true" - name: ty mdtests (GitHub annotations) if: ${{ needs.determine_changes.outputs.ty == 'true' }} @@ -411,7 +411,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" enable-cache: "true" - name: "Run tests" run: cargo nextest run --cargo-profile profiling --all-features @@ -450,7 +450,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" enable-cache: "true" - name: "Run tests" run: | @@ -560,7 +560,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: shared-key: ruff-linux-debug @@ -602,7 +602,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - name: "Install Rust toolchain" run: rustup component add rustfmt # Run all code generation scripts, and verify that the current output is @@ -649,7 +649,7 @@ jobs: with: python-version: ${{ env.PYTHON_VERSION }} activate-environment: true - version: "0.12.1" + version: "0.12.3" - name: "Install Rust toolchain" run: rustup show @@ -763,7 +763,7 @@ jobs: run: git fetch --no-tags --filter=blob:none --unshallow origin - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -829,7 +829,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: save-if: ${{ github.ref == 'refs/heads/main' }} @@ -882,7 +882,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 @@ -920,7 +920,7 @@ jobs: with: python-version: 3.13 activate-environment: true - version: "0.12.1" + version: "0.12.3" - name: "Install dependencies" run: uv pip install -r docs/requirements.txt - name: "Update README File" @@ -1077,7 +1077,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - name: "Install Rust toolchain" run: rustup show @@ -1185,7 +1185,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - name: "Install codspeed" uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 @@ -1236,7 +1236,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - name: "Install Rust toolchain" run: rustup show @@ -1289,7 +1289,7 @@ jobs: - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - name: "Install codspeed" uses: taiki-e/install-action@cb33e69fad06166ca28a42b2575e4dadabf62ee8 # v2.85.8 diff --git a/.github/workflows/daily_fuzz.yaml b/.github/workflows/daily_fuzz.yaml index b81d682f38..3411600ca2 100644 --- a/.github/workflows/daily_fuzz.yaml +++ b/.github/workflows/daily_fuzz.yaml @@ -38,7 +38,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - name: "Install Rust toolchain" run: rustup show - name: "Install mold" diff --git a/.github/workflows/memory_report.yaml b/.github/workflows/memory_report.yaml index 546fcd6be3..2583d2c704 100644 --- a/.github/workflows/memory_report.yaml +++ b/.github/workflows/memory_report.yaml @@ -63,7 +63,7 @@ jobs: - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" enable-cache: true - name: Install Rust toolchain diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 981d5bace7..20b8b4e794 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -24,7 +24,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: wheels-* diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index 939eecdfea..6919d9f143 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -86,7 +86,7 @@ jobs: git config --global user.email '<>' - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - name: Sync typeshed stubs run: | rm -rf "ruff/${VENDORED_TYPESHED}" @@ -142,7 +142,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - name: Setup git run: | git config --global user.name typeshedbot @@ -184,7 +184,7 @@ jobs: ref: ${{ env.UPSTREAM_BRANCH}} - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" - name: Setup git run: | git config --global user.name typeshedbot diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml index 1062a77076..acd4b64879 100644 --- a/.github/workflows/ty-ecosystem-analyzer.yaml +++ b/.github/workflows/ty-ecosystem-analyzer.yaml @@ -129,7 +129,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" ignore-empty-workdir: true # The default of `enable-cache: auto` leads to warnings from setup-uv in this job, # since its cache-invalidation keys (pyproject.toml, uv.lock, etc.) aren't available @@ -189,7 +189,7 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version: "0.12.1" + version: "0.12.3" ignore-empty-workdir: true # The default of `enable-cache: auto` leads to warnings from setup-uv in this job, # since its cache-invalidation keys (pyproject.toml, uv.lock, etc.) aren't available diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml index 0c0f041a5d..9bcfc24f1f 100644 --- a/.github/workflows/ty-ecosystem-report.yaml +++ b/.github/workflows/ty-ecosystem-report.yaml @@ -36,7 +36,7 @@ jobs: uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: enable-cache: true - version: "0.12.1" + version: "0.12.3" - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: From 9ddde645c21c40fddfb90a375cc418c12141fdbc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:45:22 +0200 Subject: [PATCH 388/390] Update docker/login-action action to v4.6.0 (#27683) --- .github/workflows/build-docker.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index c97856d8f6..161459bb55 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -46,7 +46,7 @@ jobs: - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }} with: registry: ghcr.io @@ -142,7 +142,7 @@ jobs: type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }} type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }} - - uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -204,7 +204,7 @@ jobs: steps: - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 - - uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -322,7 +322,7 @@ jobs: type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }} type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }} - - uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.repository_owner }} From 1f21a772f832f25d059e3e81c61b0574b0f5c483 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:45:48 +0200 Subject: [PATCH 389/390] Update dependency mdformat-mkdocs to v5.3.0 (#27681) --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 69fb672360..c26757440a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,6 +4,6 @@ mkdocs==1.6.1 mkdocs-material==9.7.7 mkdocs-redirects==1.2.3 mdformat==1.0.0 -mdformat-mkdocs==5.2.1 +mdformat-mkdocs==5.3.0 mkdocs-github-admonitions-plugin @ git+https://github.com/PGijsbers/admonitions.git#7343d2f4a92e4d1491094530ef3d0d02d93afbb7 mkdocs-llmstxt==0.2.0 From a5bb9fb802dc2ea9286088d2de3e6f4ce59d7223 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:46:15 +0200 Subject: [PATCH 390/390] Update NPM Development dependencies (#27684) --- playground/api/package-lock.json | 90 +++-- playground/package-lock.json | 665 ++++++++++++++++++++----------- 2 files changed, 481 insertions(+), 274 deletions(-) diff --git a/playground/api/package-lock.json b/playground/api/package-lock.json index cf8d13533e..14ceceb5d6 100644 --- a/playground/api/package-lock.json +++ b/playground/api/package-lock.json @@ -46,9 +46,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260722.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260722.1.tgz", - "integrity": "sha512-vZOP8vIS3NwnuaO+gz0FZ7kIGeiO3bZmxV35Ph9zOXKSREhDFlH7wQ7mkCdhW3O4jnXsew+XT7b+DNEI2CcJGQ==", + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260730.1.tgz", + "integrity": "sha512-+MBHmPaiTe2KajryW0T24rZvWFxb41hD3d8anNzQqHzft6vSEb18+sp0znSwxgij7ApPhSM1+vhkNg4f3YMguA==", "cpu": [ "x64" ], @@ -63,9 +63,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260722.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260722.1.tgz", - "integrity": "sha512-EmIQymihDq6WNdER4+LF8Qn80yqayBUpJ+tkOO7wmY8pmgfyXjIUFNXotl21AHovTeu2seR7HdVUgeN/BilCWw==", + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260730.1.tgz", + "integrity": "sha512-SBHKntPkKvNPgaCrTe99xC1CAl8ygJDzlYfK0LbuJ1muKadIw35WnhO0wu894fKBtllsVQdNzDLee+cm0ppLSQ==", "cpu": [ "arm64" ], @@ -80,9 +80,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260722.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260722.1.tgz", - "integrity": "sha512-jvZ3k9fxcnEn04s80CgIYxQfpOyAiz/8qC42DP8EBa9tR27qWyg9wmm31zIobVlrgBZn/+8NfdP73avRGcQOjQ==", + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260730.1.tgz", + "integrity": "sha512-ouyPOSMbiKPeSwUJUvxtMcxGAXs2J4aPE4T5ABIYX5ClcQx5j5bbHTmnqOQEY8sAuLTPjH7dY+iB6UI5ISlwwA==", "cpu": [ "x64" ], @@ -97,9 +97,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260722.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260722.1.tgz", - "integrity": "sha512-BOSB55SMNdy+DA5uj2WirgiNanpHGis5PVvXH1wSfvjRKr4JGgWK+EZzxz0RFUo6QjjQQC/NimEzNZ7va7jmKg==", + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260730.1.tgz", + "integrity": "sha512-YQ+Mi78U3TPdgBPtwq+Sm6rJU+Ihl2y0pjYtuuKkdmUbYzL7oLR6Xqq9wljhasnuCFICssDJaqhMep5WizYoEQ==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260722.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260722.1.tgz", - "integrity": "sha512-sYM8YgUpKnRz2xjvdJLX1Ojzoi4MlA4gk8WTTExhGydjYB2UTs5NIbv0ZmpKgMoK9io3ixgmiW56ZnTbcWOdiA==", + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260730.1.tgz", + "integrity": "sha512-27fAN+vUECW1oYVc1KOcHYpkL8COM2Uxtxql7TL595kxbjoqS5yckw7NLz7bTf2pALFCZWjqXDjZGJ/xbG4ZKQ==", "cpu": [ "x64" ], @@ -131,9 +131,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "5.20260729.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260729.1.tgz", - "integrity": "sha512-X5r/4y0gKMq/B72qkz/tEwNK4c3v2regT3to6Ia8qqC66E0+YIN/fU7x0JG6ej2rLxtU3TV3aVhfK9y8+jMMAw==", + "version": "5.20260804.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260804.1.tgz", + "integrity": "sha512-B1dwxpN6e5RZXZkE5zpZj+ooNeNZ1mwavLIyHDYe10ojhlGTwDfe8sAl7R1mMXc1cyIsbr+jKVdvmMEyVcdTdg==", "dev": true, "license": "MIT OR Apache-2.0" }, @@ -1839,16 +1839,16 @@ } }, "node_modules/miniflare": { - "version": "4.20260722.1", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260722.1.tgz", - "integrity": "sha512-FJIg4omaCb2wwSyOeRosEdmVRi3JzGAOOH3pa3twmmtvWECl6BVZMIDwJbjByLKRyU+mrfk0M/n3oSiojFZvSA==", + "version": "4.20260730.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260730.0.tgz", + "integrity": "sha512-1Z9SB9r/o//80UA02Re3QhtcecSHAyAjf5EcKBfQVlQrCg7Miy79hl2PvtkwFLIaJ5rcrOPdDcRr577okwZPsg==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.28.0", - "workerd": "1.20260722.1", + "workerd": "1.20260730.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, @@ -2154,9 +2154,9 @@ } }, "node_modules/workerd": { - "version": "1.20260722.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260722.1.tgz", - "integrity": "sha512-NycKuc1x2onvsRfGGpM093vRlLFU2zHDAM0+APpccfg4+gZxDGCH27RmdDvkeBuoZyYqgLo3oAfF6re4mvC3vQ==", + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260730.1.tgz", + "integrity": "sha512-zmfNIjwYSWFY5chGBOjWtH3xAE7p97FTC6vR4Ep98290ho6AeAR/NVcBD274YCLEUYzqm8yxdtZlxMybU8a3jA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -2167,17 +2167,17 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260722.1", - "@cloudflare/workerd-darwin-arm64": "1.20260722.1", - "@cloudflare/workerd-linux-64": "1.20260722.1", - "@cloudflare/workerd-linux-arm64": "1.20260722.1", - "@cloudflare/workerd-windows-64": "1.20260722.1" + "@cloudflare/workerd-darwin-64": "1.20260730.1", + "@cloudflare/workerd-darwin-arm64": "1.20260730.1", + "@cloudflare/workerd-linux-64": "1.20260730.1", + "@cloudflare/workerd-linux-arm64": "1.20260730.1", + "@cloudflare/workerd-windows-64": "1.20260730.1" } }, "node_modules/wrangler": { - "version": "4.115.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.115.0.tgz", - "integrity": "sha512-+upG2VW66M1sjb43yzgUZ6Ss8iYpJ6+7F3U4GF8TY5EYd+08sYdS54d24AEwCnhuef3J9KlSPusqiqR9WYy1UA==", + "version": "4.118.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.118.0.tgz", + "integrity": "sha512-9pkBw/b8zWqGx2S+oLhgHMR1M/4VOE8SynUFABnGWiSFGlcOQ4xiI/B71Xf66RYP2xzngU37IQFPtUruij3lYw==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { @@ -2185,10 +2185,10 @@ "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "4.20260722.1", + "miniflare": "5.20260730.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260722.1" + "workerd": "1.20260730.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -2202,7 +2202,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^5.20260722.1" + "@cloudflare/workers-types": "^5.20260730.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -2210,6 +2210,24 @@ } } }, + "node_modules/wrangler/node_modules/miniflare": { + "version": "5.20260730.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260730.0-alpha.tgz", + "integrity": "sha512-8/dspSXDshP6nSkCpjKO7BYc2qZoYSXm7iM+QxY7qJyJpAB3onnQSaiu0cvKJlfuMGwULl55hG69FJCcCMXU1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.2", + "undici": "7.28.0", + "workerd": "1.20260730.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", diff --git a/playground/package-lock.json b/playground/package-lock.json index 31f3c3538f..ea9c77d9a5 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -284,40 +284,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -712,29 +678,10 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", "dev": true, "license": "MIT", "funding": { @@ -2540,9 +2487,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.2.tgz", + "integrity": "sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==", "cpu": [ "arm64" ], @@ -2557,9 +2504,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.2.tgz", + "integrity": "sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==", "cpu": [ "arm64" ], @@ -2574,9 +2521,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.2.tgz", + "integrity": "sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==", "cpu": [ "x64" ], @@ -2591,9 +2538,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.2.tgz", + "integrity": "sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==", "cpu": [ "x64" ], @@ -2608,9 +2555,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.2.tgz", + "integrity": "sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==", "cpu": [ "arm" ], @@ -2625,9 +2572,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.2.tgz", + "integrity": "sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==", "cpu": [ "arm64" ], @@ -2645,9 +2592,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.2.tgz", + "integrity": "sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==", "cpu": [ "arm64" ], @@ -2665,9 +2612,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.2.tgz", + "integrity": "sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==", "cpu": [ "ppc64" ], @@ -2685,9 +2632,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.2.tgz", + "integrity": "sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==", "cpu": [ "s390x" ], @@ -2705,9 +2652,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.2.tgz", + "integrity": "sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==", "cpu": [ "x64" ], @@ -2725,9 +2672,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.2.tgz", + "integrity": "sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==", "cpu": [ "x64" ], @@ -2745,9 +2692,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.2.tgz", + "integrity": "sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==", "cpu": [ "arm64" ], @@ -2761,29 +2708,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.2.tgz", + "integrity": "sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==", "cpu": [ "arm64" ], @@ -2798,9 +2726,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.2.tgz", + "integrity": "sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==", "cpu": [ "x64" ], @@ -3187,17 +3115,6 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/emscripten": { "version": "1.41.5", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz", @@ -3226,9 +3143,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "dev": true, "license": "MIT", "dependencies": { @@ -3236,9 +3153,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3253,17 +3170,17 @@ "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", - "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", + "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/type-utils": "8.65.0", - "@typescript-eslint/utils": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/type-utils": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -3276,7 +3193,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.65.0", + "@typescript-eslint/parser": "^8.66.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -3292,16 +3209,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", - "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3" }, "engines": { @@ -3317,14 +3234,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", - "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.65.0", - "@typescript-eslint/types": "^8.65.0", + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", "debug": "^4.4.3" }, "engines": { @@ -3339,14 +3256,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", - "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0" + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3357,9 +3274,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", - "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", "dev": true, "license": "MIT", "engines": { @@ -3374,15 +3291,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", - "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", + "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -3399,9 +3316,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", - "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", "dev": true, "license": "MIT", "engines": { @@ -3413,16 +3330,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", - "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.65.0", - "@typescript-eslint/tsconfig-utils": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -3451,26 +3368,26 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -3493,16 +3410,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", - "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", + "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0" + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3517,13 +3434,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", - "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/types": "8.66.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3939,9 +3856,9 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", - "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", "dev": true, "license": "MIT", "dependencies": { @@ -6653,9 +6570,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "dev": true, "funding": [ { @@ -6970,9 +6887,9 @@ } }, "node_modules/postcss": { - "version": "8.5.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.21.tgz", - "integrity": "sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -7318,13 +7235,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.2.tgz", + "integrity": "sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.139.0", + "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -7334,21 +7251,20 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" + "@rolldown/binding-android-arm64": "1.2.2", + "@rolldown/binding-darwin-arm64": "1.2.2", + "@rolldown/binding-darwin-x64": "1.2.2", + "@rolldown/binding-freebsd-x64": "1.2.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.2", + "@rolldown/binding-linux-arm64-gnu": "1.2.2", + "@rolldown/binding-linux-arm64-musl": "1.2.2", + "@rolldown/binding-linux-ppc64-gnu": "1.2.2", + "@rolldown/binding-linux-s390x-gnu": "1.2.2", + "@rolldown/binding-linux-x64-gnu": "1.2.2", + "@rolldown/binding-linux-x64-musl": "1.2.2", + "@rolldown/binding-openharmony-arm64": "1.2.2", + "@rolldown/binding-win32-arm64-msvc": "1.2.2", + "@rolldown/binding-win32-x64-msvc": "1.2.2" } }, "node_modules/ruff_wasm": { @@ -7999,16 +7915,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", - "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", + "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.65.0", - "@typescript-eslint/parser": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/utils": "8.65.0" + "@typescript-eslint/eslint-plugin": "8.66.0", + "@typescript-eslint/parser": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8092,16 +8008,16 @@ } }, "node_modules/vite": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", + "lightningcss": "^1.33.0", "picomatch": "^4.0.5", - "postcss": "^8.5.17", - "rolldown": "~1.1.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "bin": { @@ -8118,7 +8034,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", + "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -8192,6 +8108,279 @@ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",